CSDN仅用于增加百度收录权重,排版未优化,日常不维护。请访问:www.hceng.cn 查看、评论。
本博文对应地址: https://hceng.cn/2017/12/22/Exynos4412——LCD驱动/#more
Exynos4412的LCD驱动。
终于迎来了LCD驱动,本该10月初就搞定的事,一直拖到了12月份。
不过,晚来总比不来好,完成这个心结,才好进入下一个阶段。
1. 基础知识
开发板的液晶屏型号S702,7寸电容型TFT触摸屏,分辨率为800X480。
先记录下一些图像显示的基本知识。
1.1 颜色的量化
显示器的颜色一般采用RGB标准,通过对红(RED)、绿(GREEN),蓝(BLUE)三个颜色相互叠加得到各种不同的颜色。
1)通过对颜色的编码来对颜色进行量化(即转换成数字量,RGB是一种编码方式);
2)每种颜色根据RGB格式不同,每种颜色的量化位不相同;
3) 常见的RGB格式有RGB565/RGB888,即:
RGB565: red :5 green : 6 blue:5(16BPP)
RGB888: red :8 green : 8 blue:8(24BPP)
4)有的还会有个alpha参数,用于实现图形渐变效果,以及半透明效果,0xFFF=全透明,0x0=不透明;
1.2 图像的构成
像素:
显示的最小单位;
用若干位数据来表示一个像素,比如使用R8、G8、B8共24位来表示一个像素,这个也称为像素深度,单位为BPP 常见的有16BPP/24BPP;
像素越高,则一个像素点所显示的颜色就越多,所显示的颜色更广;
帧:
一幅图像被称为一帧,
每帧里面由行列排列的像素组成;
调色板:
画油画的时候,通常先在调色板里配好想要的颜色,再用画笔沾到画布上作画。
LCD控制器里也借用了这个概念,从FrameBuffer获得数据,这个数据作为索引从调色板获得对应数据,再发给电子枪显示出来。
如图,假如是16BPP的数据,LCD控制器从FB取出16bit数据,显示到LCD上。
当如果想节约内存,对颜色要求也没那么高,就可以采用调色板的方式,调色板里存放了256个16bit的数据,FB只存放每个像素的索引,根据索引去调色板找到对应的数据传给LCD控制器,再通过电子枪显示出来。
1.3 LCD显示原理
常见的TFT显示屏幕都会有如下控制信号:
1)使用HSYNC信号来控制一行的显示;
2)使用VSYNC信号来控制一帧(列)的显示;
3)使用VCLK信号来控制一个像素的显示;
4)使用VDEN信号来控制数据的输出;
想象每个像素点是由电子枪发射出来的,电子枪依次扫描整个LCD界面,就显示了一副完整的图像。
当发出一个HSYNC信号后,电子枪就会从行末花费HBP时长移动到行首;然后在VCLK时钟下,在图中阴影区域显示像素;等到了行末后,再等待HFP时长待HSYNC信号到来,再移动到行首,如此往复。因此,HBP和HFP分别决定了左边和右边的黑框。
同理,当发出一个VSYNC信号后,电子枪就会从列末花费VBP时长移动到列首;然后在VCLK时钟下,在图中阴影区域显示像素;等到了列末后,再等待VFP时长待VSYNC信号到来,再移动到列首,如此往复。因此,VBP和VFP分别决定了上边和下边的黑框。
真实显示区域为图中阴影部分。
1.4 LCD硬件控制思路
- 查看LCD芯片手册,查看相关的时间参数、分辨率、引脚极性等;
- 根据以上信息设置LCD控制器寄存器,让其发出正确信号;
- 在内存里面分配一个FrameBuffer,在里面用若干位表示一个像素,再把首地址告诉LCD控制器;
之后LCD控制器就能周而复始取出FrameBuffer里面的像素数据,配合其它控制信号,发送给电子枪,电子枪再让在LCD上显示出来。以后我们想显示图像,只需要编写程序向FrameBuffer填入相应数据即可,硬件会自动的完成显示操作。
1.5 LCD驱动框架
LCD框架的fbmem.c
已经帮我们完成了日常驱动程序的工作,如:注册字符设备、分配设备号、创建类等。
也有了操作函数fb_fops
,但它只是一个框架,在具体执行的时候需要知道硬件具体的一些参数,比如分辨率、数据基地址等信息。
因此,我们要利用这一框架,就得构造一个fb_info
结构体,完成硬件初始化,设置相关参数等操作,再使用register_framebuffer()
将fb_info
向上注册。
这样,fbmem.c
就可以从registered_fb[]
这个数组获得fb_info
参数,进行相关的硬件操作。
比如:应用层app想read()
,就会调用fbmem.c
的fb_read()
,在fb_read
里面会先尝试使用xxfb.c
提供的read()
操作函数,如果没有,再根据fb_info
信息得到数据基地址,将基地址开始的数据,返回给应用层,实现读操作。
2. 原理图
- Tiny4412SDK-1506-Schematic.pdf:
从上到下,接口依次是:
接口 | 引脚 | 含义 |
---|---|---|
1. 图像数据信号接口 | B[0:7] | 蓝色数据信号线 |
- | G[0:7] | 绿色数据信号线 |
- | R[0:7] | 红色数据信号线 |
2. “一线触摸”接口 | XEINT10_OUT | 用于 触摸/背光 控制 |
3. 时序信号控制接口 | DEN | 数据使能信号 |
- | VSYNC | 垂直同步信号 |
- | HSYNC | 水平同步信号 |
- | VLCK | LCD时钟信号 |
4. I2C接口 | i2cSCL1_OUT/i2cSDA1_OUT | 用于实现I2C接口的触摸屏驱动 |
- Tiny4412-1412-Schematic.pdf:
LCD数据和控制部分使用了GPF0_0-GPF0_7、GPF1_0-GPF1_7、GPF2_0-GPF2_7、GPF3_0-GPF3_5,共3*8+4=28个引脚。
3. 设备树文件
lcd_s702@11C00000 {
compatible = "tiny4412, lcd_s702";
reg = <0x11C00000 0x20c0 0x10010210 0x08 0x10023c80 0x04 0x1003c000 0x1000>;
pinctrl-names = "default";
pinctrl-0 = <&lcd_s702>;
clocks = <&clock CLK_FIMD0 &clock CLK_ACLK160>;
clock-names = "fimd0","aclk160";
};
&pinctrl_0 {
lcd_s702:lcd {
samsung,pins = "gpf0-0", "gpf0-1", "gpf0-2", "gpf0-3", "gpf0-4",
"gpf0-5", "gpf0-6","gpf0-7", "gpf1-0", "gpf1-1",
"gpf1-2", "gpf1-3", "gpf1-4", "gpf1-5", "gpf1-6",
"gpf1-7", "gpf2-0", "gpf2-1", "gpf2-2", "gpf2-3",
"gpf2-4", "gpf2-5", "gpf2-6","gpf2-7", "gpf3-0",
"gpf3-1", "gpf3-2", "gpf3-3";
samsung,pin-function = <2>;
samsung,pin-pud = <0>;
samsung,pin-drv = <0>;
};
};
其中,
0x11C00000是LCD寄存器基地址;
0x10010210是LCD时钟寄存器基地址;
0x10023c80是LCD时钟寄存器基地址;
0x10023c80是LCD时钟寄存器基地址;
Samsung GPIO and Pin Mux/Config controller摘录:
“samsung,pins” property of the child node. The following pin configuration properties are supported.
- samsung,pin-val: Initial value of pin output buffer.
- samsung,pin-pud: Pull up/down configuration.
- samsung,pin-drv: Drive strength configuration.
- samsung,pin-pud-pdn: Pull up/down configuration in power down mode.
- samsung,pin-drv-pdn: Drive strength configuration in power down mode.
4. 驱动分析
LCD驱动也属于字符驱动,框架和其它字符驱动差不多,难点是LCD的众多硬件配置。
和以往的框架一样,加载驱动后调用lcd_init()
函数,然后lcd_init()
调用platform_driver_register(&lcd_driver)
注册平台设备,lcd_driver
结构体里面的.compatible
与设备树里面的compatible
进行字符串比较,匹配成功则调用核心的lcd_probe()
函数。
下面对lcd_probe()
函数里面的内容进行详细介绍。
4.1 lcd_probe
lcd_probe()
需要的配置如下:
1.分配一个fb_info
2.设置fb_info
2.1 设置 fix 固定的参数
2.2 设置 var 可变的参数
2.3 设置操作函数
2.4 其他的设置
3.硬件相关的操作
3.1 配置GPIO用于LCD
3.2 根据LCD手册设置LCD控制器, 比如VCLK的频率等
3.3 分配显存(framebuffer), 并把地址告诉LCD控制器
4.注册fb_info
4.1.1 分配一个fb_info
{% codeblock lang:c %}
tiny4412_lcd = framebuffer_alloc(0, NULL); //不要额外空间设置私有数据
if(!tiny4412_lcd) {
return -ENOMEM;
}
{% endcodeblock %}
4.1.2 设置fb_info
4.1.2.1 设置 fix 固定的参数
{% codeblock lang:c %}
strcpy(tiny4412_lcd->fix.id, “s702”); //设置fix名称
tiny4412_lcd->fix.smem_len = LCD_LENTHLCD_WIDTHBITS_PER_PIXEL/8; //显存的长度=分辨率每象素字节数
tiny4412_lcd->fix.type = FB_TYPE_PACKED_PIXELS; //类型:填充式像素(常用在TFT屏幕)
tiny4412_lcd->fix.visual = FB_VISUAL_TRUECOLOR; //TFT 真彩色
tiny4412_lcd->fix.line_length = LCD_LENTHBITS_PER_PIXEL/8; //每行的长度,以字节为单位
{% endcodeblock %}
4.1.2.2 设置 var 可变的参数
{% codeblock lang:c %}
tiny4412_lcd->var.xres = LCD_LENTH; //x方向分辨率
tiny4412_lcd->var.yres = LCD_WIDTH; //y方向分辨率
tiny4412_lcd->var.xres_virtual = LCD_LENTH; //x方向虚拟分辨率
tiny4412_lcd->var.yres_virtual = LCD_WIDTH; //y方向虚拟分辨率
tiny4412_lcd->var.xoffset = 0; //x方向真实值和虚拟值得差值
tiny4412_lcd->var.yoffset = 0; //y方向真实值和虚拟值得差值
tiny4412_lcd->var.bits_per_pixel = BITS_PER_PIXEL; //每个像素占多少位
/* RGB:888 */
tiny4412_lcd->var.red.length = 8;
tiny4412_lcd->var.red.offset = 16; //红
tiny4412_lcd->var.green.length = 8;
tiny4412_lcd->var.green.offset = 8; //绿
tiny4412_lcd->var.blue.length = 8;
tiny4412_lcd->var.blue.offset = 0; //蓝
tiny4412_lcd->var.activate = FB_ACTIVATE_NOW; //使设置的值立即生效
{% endcodeblock %}
4.1.2.3 设置操作函数
{% codeblock lang:c %}
tiny4412_lcd->fbops = &tiny4412_lcdfb_ops; //绑定操作函数
{% endcodeblock %}
这里的绑定的操作函数有:
{% codeblock lang:c %}
static struct fb_ops tiny4412_lcdfb_ops =
{
.owner = THIS_MODULE,
.fb_setcolreg = cfb_setcolreg, //设置RGB颜色,实现伪颜色表
.fb_fillrect = cfb_fillrect, //矩形填充
.fb_copyarea = cfb_copyarea, //数据复制
.fb_imageblit = cfb_imageblit, //图形填充
};
{% endcodeblock %}
除了cfb_setcolreg()
,其它三个函数内核都提供了具体的实现。
4.1.2.4 其他的设置
{% codeblock lang:c %}
tiny4412_lcd->pseudo_palette = pseudo_palette; //存放调色板所调颜色的数组
tiny4412_lcd->screen_size = LCD_LENTH * LCD_WIDTH * BITS_PER_PIXEL / 8; //显存大小
{% endcodeblock %}
4.1.3 硬件相关的操作
4.1.3.1 配置GPIO用于LCD
在设备树中,将 GPF0_0-GPF0_7、GPF1_0-GPF1_7、GPF2_0-GPF2_7、GPF3_0-GPF3_3配置为了复用第二功能(LCD),禁止内部上拉,驱动强度配置设置为0,因此这里就不需要任何设置了。
4.1.3.2 根据LCD手册设置LCD控制器, 比如VCLK的频率等
a. 首先是获取设备树中的寄存器资源,并进行映射:
{% codeblock lang:c %}
res0 = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res0 == NULL)
{
printk(“platform_get_resource error.\n”);
return -EINVAL;
}
lcd_regs_base = devm_ioremap_resource(&pdev->dev, res0);
if (lcd_regs_base == NULL)
{
printk(“devm_ioremap_resource error.\n”);
return -EINVAL;
}
res1 = platform_get_resource(pdev, IORESOURCE_MEM, 1);
if (res1 == NULL)
{
printk("platform_get_resource error.\n");
return -EINVAL;
}
lcdblk_regs_base = devm_ioremap_resource(&pdev->dev, res1);
if (lcdblk_regs_base == NULL)
{
printk("devm_ioremap_resource error.\n");
return -EINVAL;
}
res2 = platform_get_resource(pdev, IORESOURCE_MEM, 2);
if (res2 == NULL)
{
printk("platform_get_resource error.\n");
return -EINVAL;
}
//bug:
/*
devm_ioremap()和devm_ioremap_resource()区别:
devm_ioremap()可以重复map相同的地址空间,devm_ioremap_resource()不可以。
一般SoC的中,各个硬件模块各自的memory region都有严格的划分(比如说USB host的地址空间绝对不会和flash host冲突),
所以一般的driver使用devm_ioremap()和devm_ioremap_resource()都行。
但这里,应该系统已经映射过一次了,所以使用devm_ioremap_resource()会报错。
*/
//lcd0_configuration = devm_ioremap_resource(&pdev->dev, res2);
lcd0_configuration = devm_ioremap(&pdev->dev, res2->start, resource_size(res2));
if (lcd0_configuration == NULL)
{
printk("devm_ioremap_resource error.\n");
return -EINVAL;
}
*(unsigned long *)lcd0_configuration = 7; //Reset Value = 0x00000007
res3 = platform_get_resource(pdev, IORESOURCE_MEM, 3);
if (res3 == NULL)
{
printk("platform_get_resource error.\n");
return -EINVAL;
}
//clk_regs_base = devm_ioremap_resource(&pdev->dev, res3);
clk_regs_base = devm_ioremap(&pdev->dev, res3->start, resource_size(res3));
if (clk_regs_base == NULL)
{
printk("devm_ioremap_resource error.\n");
return -EINVAL;
}
{% endcodeblock %}
这里后期测试出一个bug:
devm_ioremap()和devm_ioremap_resource()区别:
devm_ioremap()可以重复map相同的地址空间,devm_ioremap_resource()不可以。
一般SoC的中,各个硬件模块各自的memory region都有严格的划分(比如说USB host的地址空间绝对不会和flash host冲突),
所以一般的driver使用devm_ioremap()和devm_ioremap_resource()都行。
但这里,应该系统已经映射过一次了,所以使用devm_ioremap_resource()会报错。
b. 设置时钟
时钟部分还是有点乱,没有从头开始去分析,当这里的目的是生成VCLK,因此配置出VCLK即可:
{% codeblock lang:c %}
//时钟源选择\使能时钟
//Selects clock source for LCD_BLK
//FIMD0_SEL:bit[3:0]=0110=SCLKMPLL_USER_T=800M
temp = readl(clk_regs_base + CLK_SRC_LCD0);
temp &= ~(0x0F<<0);
temp |= (0x3<<1);
writel(temp, clk_regs_base + CLK_SRC_LCD0);
//Clock source mask for LCD_BLK
//FIMD0_MASK:Mask output clock of MUXFIMD0 (1=Unmask)
temp = readl(clk_regs_base + CLK_SRC_MASK_LCD);
temp |= (0x01<<0);
writel(temp, clk_regs_base + CLK_SRC_MASK_LCD);
//Clock source mask for LCD_BLK
//SCLK_FIMD0 = MOUTFIMD0/(FIMD0_RATIO + 1),分频比 1/1
temp = readl(clk_regs_base + CLK_DIV_LCD);
temp &= ~(0x0F<<0);
writel(temp, clk_regs_base + CLK_DIV_LCD);
//Controls IP clock gating for LCD_BLK
//CLK_FIMD0:Gating all clocks for FIMD0 (1=Pass)
temp = readl(clk_regs_base + CLK_GATE_IP_LCD);
temp |= (0x01<<0);
writel(temp, clk_regs_base + CLK_GATE_IP_LCD);
//FIMDBYPASS_LBLK0:FIMD of LBLK0 Bypass Selection (1=FIMD Bypass)
temp = readl(lcdblk_regs_base + LCDBLK_CFG);
temp |= (0x01<<1);
writel(temp, lcdblk_regs_base + LCDBLK_CFG);
//MIE0_DISPON:MIE0_DISPON: PWM output control (1=PWM outpupt enable)
temp = readl(lcdblk_regs_base + LCDBLK_CFG2);
temp |= (0x01<<0);
writel(temp, lcdblk_regs_base + LCDBLK_CFG2);
mdelay(1000);
//LCD时钟: VCLK=FIMD*SCLK/(CLKVAL+1), where CLKVAL>=1
//800/(19+1) == 40M<80M
temp = readl(lcd_regs_base + VIDCON0);
temp |= (19<<6);
writel(temp, lcd_regs_base + VIDCON0);
{% endcodeblock %}
思路就是选择时钟源,然后启用时钟源,再分频得到所需VCLK。
c. 设置引脚极性和时序
- LCD手册“S702-AT070TN92.pdf”中的时序图和时间参数:
(PS:手册图片做得真烂,源PDF都模糊)
上半部分为水平输入时序图,下半部分为垂直方向输入时序图。
然后是图中的时间参数:
- Exynos 4412手册“Exynos 4412 SCP_Users Manual.pdf”中的时序图:
我的经验就是结合LCD的时序图和控制器的时序图,对比两者的时序得出关系式和极性。
因此,可以得出:
极性方面:
VS与VSYNC极性相反,
HS与HSYNC极性相反,
DE与VDEN极性一致,
DCLK是在上升沿触发。
对于寄存器VIDCON1:
{% codeblock lang:c %}
/*
* VIDTCON1:
* [5]:IVSYNC ===> 1 : Inverted(反转)
* [6]:IHSYNC ===> 1 : Inverted(反转)
* [7]:IVCLK ===> 1 : Fetches video data at VCLK rising edge (上降沿触发)
* [10:9]:FIXVCLK ====> 01 : VCLK running
*/
temp = readl(lcd_regs_base + VIDCON1);
temp |= (1 << 9) | (1 << 7) | (1 << 5) | (1 << 6);
writel(temp, lcd_regs_base + VIDCON1);
{% endcodeblock %}
时序方面:
VSPW+1=tvpw=1~20(暂取11) --> VSPW=10
VBPD+1=tvb-tvpw=23-11=12 --> VBPD=11
VFPD+1=tvfp=22 --> VFPD=21
HSPW+1=hpw=1~40(暂取21) --> HSPW=20
HBPD+1=thb-hpw=46-21=25 --> HBPD=24
HOZVAL+1=thd=800 --> HOZVAL=799
HFPD+1=thfp=210 --> HFPD=209
对于寄存器VIDTCON0:
{% codeblock lang:c %}
/*
* VIDTCON0:
* [23:16]: VBPD+1=tvb-tvpw=23-11=12 --> VBPD=11
* [15:8] : VFPD+1=tvfp=22 --> VFPD=21
* [7:0] : VSPW+1=tvpw=1~20(暂取11) --> VSPW=10
*/
temp = readl(lcd_regs_base + VIDTCON0);
temp |= (11 << 16) | (21 << 8) | (10 << 0);
writel(temp, lcd_regs_base + VIDTCON0);
{% endcodeblock %}
对于寄存器VIDTCON1:
{% codeblock lang:c %}
/*
* VIDTCON1:
* [23:16]: HBPD+1=thb-hpw=46-21=25 --> HBPD=24
* [15:8] : HFPD+1=thfp=210 --> HFPD=209
* [7:0] : HSPW+1=hpw=1~40(暂取21) --> HSPW=20
*/
temp = readl(lcd_regs_base + VIDTCON1);
temp |= (24 << 16) | (209 << 8) | (20 << 0);
writel(temp, lcd_regs_base + VIDTCON1);
{% endcodeblock %}
d. 设置分辨率
{% codeblock lang:c %}
/*
* HOZVAL = (Horizontal display size) - 1 and LINEVAL = (Vertical display size) - 1.
* Horizontal(水平) display size : 800
* Vertical(垂直) display size : 480
*/
temp = ((LCD_WIDTH-1) << 11) | (LCD_LENTH << 0);
writel(temp, lcd_regs_base + VIDTCON2);
{% endcodeblock %}
e. 设置数据格式
如前面可变参数所设置的一样,本次采用的是24BPP格式,每个像素占用32位(实际使用24位)。
当使能字节交换时(BSWP=0, HWSWP=0, WSWP=1),则低位像素存放在低字节,即[23:0]放像素1,[31:24]空,[55:32]放第二个像素,[63:56]空,依次类推,这种存放方式更符合日常习惯。
{% codeblock lang:c %}
/*
* WINCON0:
* [15]:Specifies Word swap control bit. 1 = Enables swap 低位像素存放在低字节
* [5:2]: Selects Bits Per Pixel (BPP) mode for Window image : 1101 ===> Unpacked 25 BPP (non-palletized A:1-R:8-G:8-B:8)
* [0]:Enables/disables video output 1 = Enables
*/
temp = readl(lcd_regs_base + WINCON0);
temp &= ~(0x0F << 2);
temp |= (0X01 << 15) | (0x0D << 2) | (0x01<<0);
writel(temp, lcd_regs_base + WINCON0);
{% endcodeblock %}
f. 设置/使能显示窗口
{% codeblock lang:c %}
//Enables Channel 0.
temp = readl(lcd_regs_base + SHADOWCON);
writel(temp | 0x01, lcd_regs_base + SHADOWCON);
//Selects Channel 0
temp = readl(lcd_regs_base + WINCHMAP2);
temp &= ~(7 << 16);
temp |= (0x01 << 16);//CH0FISEL:Selects Channel 0’s channel.001 = Window 0
temp &= ~(7 << 0);
temp |= (0x01 << 0);//W0FISEL:Selects Window 0’s channel.001 = Channel 0
writel(temp, lcd_regs_base + WINCHMAP2);
{% endcodeblock %}
g. 设置OSD功能
注:OSD是on-screen display的简称,即屏幕菜单式调节方式。即在当前显示上叠加一层显示,就像显示器的调节菜单。
{% codeblock lang:c %}
//设置OSD显示大小
//Window Size For example. Height * Width (number of word)
temp = (LCD_LENTH * LCD_WIDTH) >> 1;
writel(temp, lcd_regs_base + VIDOSD0C);
/*
* bit0-10 : 指定OSD图像左上像素的垂直屏幕坐标
* bit11-21: 指定OSD图像左上像素的水平屏幕坐标
/
writel(0, lcd_regs_base + VIDOSD0A);
/
* bit0-10 : 指定OSD图像右下像素的垂直屏幕坐标
* bit11-21: 指定OSD图像右下像素的水平屏幕坐标
*/
writel(((LCD_LENTH-1) << 11) | (LCD_WIDTH-1), lcd_regs_base + VIDOSD0B);
{% endcodeblock %}
f. 启动显示
Display On: ENVID and ENVID_F are set to “1”.
Direct Off: ENVID and ENVID_F are set to “0” simultaneously.
Per Frame Off: ENVID_F is set to “0” and ENVID is set to “1”.
{% codeblock lang:c %}
//Display On: ENVID and ENVID_F are set to “1”.
temp = readl(lcd_regs_base + VIDCON0);
writel(temp | (0x01<<0) | (0x01<<1), lcd_regs_base + VIDCON0);
{% endcodeblock %}
4.1.3.3 分配显存(framebuffer), 并把地址告诉LCD控制器
这里因为分配的显存要是连续的,因此不能使用传统的kalloc()
函数,这里使用dma_alloc_writecombine()
。
先用物理地址映射出一块虚拟内存,以后对该虚拟内存操作,就等同于对物理地址数据进行操作。
然后把物理地址的起始/结束地址告诉LCD控制器,以后LCD控制器就会去物理地址不断获取显示数据。
这样就实现了操作虚拟内存,更新图像数据的效果。
{% codeblock lang:c %}
// tiny4412_lcd->screen_base 显存虚拟地址
// tiny4412_lcd->fix.smem_len 显存大小,前面计算的
// tiny4412_lcd->fix.smem_start 显存物理地址
tiny4412_lcd->screen_base = dma_alloc_writecombine(NULL, tiny4412_lcd->fix.smem_len, (dma_addr_t *)&tiny4412_lcd->fix.smem_start, GFP_KERNEL);
//显存起始地址
writel(tiny4412_lcd->fix.smem_start, lcd_regs_base + VIDW00ADD0B0);
//显存结束地址
writel(tiny4412_lcd->fix.smem_start + tiny4412_lcd->fix.smem_len, lcd_regs_base + VIDW00ADD1B0);
{% endcodeblock %}
4.1.4 注册fb_info
{% codeblock lang:c %}
ret = register_framebuffer(tiny4412_lcd);
return ret;
{% endcodeblock %}
4.2 cfb_setcolreg
前面绑定的操作函数中,cfb_fillrect()
,cfb_copyarea()
、cfb_imageblit()
都在内核的其它文件中实现了,无需再构造,剩下的cfb_setcolreg()
是和调色板相关的函数,是为了兼容8BPP模式。
我们这里一直使用的24BPP,不提供这个函数的实现也行。
调色板的介绍前面说过了,就是为了减少数据量,fb只存放数据索引,根据索引再去内存找到对应的颜色数据传给LCD控制器,LCD控制器再控制时序和数据在LCD上显示出来。
static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf)
{
chan &= 0xFFFF;//保留低16位
chan >>= 16 - bf->length;//保留高bf->length位
return chan << bf->offset;//返回保留的位,且在原位置
}
static int cfb_setcolreg(unsigned int regno, unsigned int red,
unsigned int green, unsigned int blue,
unsigned int transp, struct fb_info *info)
{
unsigned int color = 0;
uint32_t *p;
color = chan_to_field(red, &info->var.red);
color |= chan_to_field(green, &info->var.green);
color |= chan_to_field(blue, &info->var.blue);
p = info->pseudo_palette;
p[regno] = color;
return 0;
}
4.3 lcd_remove
需要关闭LCD,注销和释放framebuffer,释放显存:
{% codeblock lang:c %}
static int lcd_remove(struct platform_device *pdev)
{
//Direct Off: ENVID and ENVID_F are set to “0” simultaneously.
unsigned int temp;
temp = readl(lcd_regs_base + VIDCON0);
temp &= ~(0x01<<1 | 0x01<<0);
writel(temp, lcd_regs_base + VIDCON0);
unregister_framebuffer(tiny4412_lcd);
dma_free_writecombine(NULL, tiny4412_lcd->fix.smem_len, tiny4412_lcd->screen_base, tiny4412_lcd->fix.smem_start);
framebuffer_release(tiny4412_lcd);
return 0;
}
{% endcodeblock %}
4.4 完整驱动代码
{% codeblock lang:c [lcd_drv.c] https://github.com/hceng/learn/blob/master/tiny4412/02_lcd_drv/lcd_drv.c %}
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/fb.h>
#include <linux/init.h>
#include <linux/dma-mapping.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/wait.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <linux/io.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <asm/div64.h>
#include <asm/mach/map.h>
#include <linux/fb.h>
#include <asm/types.h>
#define VIDCON0 0x00
#define VIDCON1 0x04
#define VIDTCON0 0x10
#define VIDTCON1 0x14
#define VIDTCON2 0x18
#define WINCON0 0x20
#define VIDOSD0C 0x48
#define SHADOWCON 0x34
#define WINCHMAP2 0x3c
#define VIDOSD0A 0x40
#define VIDOSD0B 0x44
#define VIDW00ADD0B0 0xA0
#define VIDW00ADD1B0 0xD0
#define CLK_SRC_LCD0 0x234
#define CLK_SRC_MASK_LCD 0x334
#define CLK_DIV_LCD 0x534
#define CLK_GATE_IP_LCD 0x934
#define LCDBLK_CFG 0x00
#define LCDBLK_CFG2 0x04
#define LCD_LENTH 800
#define LCD_WIDTH 480
#define BITS_PER_PIXEL 32
static struct fb_info *tiny4412_lcd;
static volatile void __iomem *lcd_regs_base;
static volatile void __iomem *lcdblk_regs_base;
static volatile void __iomem *lcd0_configuration;//Configures power mode of LCD0.0x10020000+0x3C80
static volatile void __iomem *clk_regs_base;
static u32 pseudo_palette[16];
static struct resource *res0, *res1, *res2, *res3;
/* from pxafb.c */
static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf)
{
chan &= 0xFFFF;//保留低16位
chan >>= 16 - bf->length;//保留高bf->length位
return chan << bf->offset;//返回保留的位,且在原位置
}
static int cfb_setcolreg(unsigned int regno, unsigned int red,
unsigned int green, unsigned int blue,
unsigned int transp, struct fb_info *info)
{
unsigned int color = 0;
uint32_t *p;
color = chan_to_field(red, &info->var.red);
color |= chan_to_field(green, &info->var.green);
color |= chan_to_field(blue, &info->var.blue);
p = info->pseudo_palette;
p[regno] = color;
return 0;
}
static struct fb_ops tiny4412_lcdfb_ops =
{
.owner = THIS_MODULE,
.fb_setcolreg = cfb_setcolreg, //设置调色板,实现伪颜色表
.fb_fillrect = cfb_fillrect, //填充矩形
.fb_copyarea = cfb_copyarea, //数据复制
.fb_imageblit = cfb_imageblit, //图形填充
};
static int lcd_probe(struct platform_device *pdev)
{
int ret;
unsigned int temp;
/* 1. 分配一个fb_info */
tiny4412_lcd = framebuffer_alloc(0, NULL); //不要额外空间设置私有数据
if(!tiny4412_lcd) {
return -ENOMEM;
}
/* 2. 设置 */
/* 2.1 设置 fix 固定的参数 */
strcpy(tiny4412_lcd->fix.id, "s702"); //设置fix名称
tiny4412_lcd->fix.smem_len = LCD_LENTH*LCD_WIDTH*BITS_PER_PIXEL/8; //显存的长度=分辨率*每象素字节数
tiny4412_lcd->fix.type = FB_TYPE_PACKED_PIXELS; //类型:填充式像素(常用在TFT屏幕)
tiny4412_lcd->fix.visual = FB_VISUAL_TRUECOLOR; //TFT 真彩色
tiny4412_lcd->fix.line_length = LCD_LENTH*BITS_PER_PIXEL/8; //每行的长度,以字节为单位
/* 2.2 设置 var 可变的参数 */
tiny4412_lcd->var.xres = LCD_LENTH; //x方向分辨率
tiny4412_lcd->var.yres = LCD_WIDTH; //y方向分辨率
tiny4412_lcd->var.xres_virtual = LCD_LENTH; //x方向虚拟分辨率
tiny4412_lcd->var.yres_virtual = LCD_WIDTH; //y方向虚拟分辨率
tiny4412_lcd->var.xoffset = 0; //x方向真实值和虚拟值得差值
tiny4412_lcd->var.yoffset = 0; //y方向真实值和虚拟值得差值
tiny4412_lcd->var.bits_per_pixel = BITS_PER_PIXEL; //每个像素占多少位
/* RGB:888 */
tiny4412_lcd->var.red.length = 8;
tiny4412_lcd->var.red.offset = 16; //红
tiny4412_lcd->var.green.length = 8;
tiny4412_lcd->var.green.offset = 8; //绿
tiny4412_lcd->var.blue.length = 8;
tiny4412_lcd->var.blue.offset = 0; //蓝
tiny4412_lcd->var.activate = FB_ACTIVATE_NOW; //使设置的值立即生效
/* 2.3 设置操作函数 */
tiny4412_lcd->fbops = &tiny4412_lcdfb_ops; //绑定操作函数
/* 2.4 其他的设置 */
tiny4412_lcd->pseudo_palette = pseudo_palette; //存放调色板所调颜色的数组
tiny4412_lcd->screen_size = LCD_LENTH * LCD_WIDTH * BITS_PER_PIXEL / 8; //显存大小
/* 3. 硬件相关的操作 */
/* 3.1 配置GPIO用于LCD */
//在设备树中,将 GPF0_0-GPF0_7、GPF1_0-GPF1_7、GPF2_0-GPF2_7、GPF3_0-GPF3_3
//配置为了复用第二功能(LCD),禁止内部上拉,驱动强度配置设置为0;
/* 3.2 根据LCD手册设置LCD控制器, 比如VCLK的频率等 */
//寄存器映射
res0 = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res0 == NULL)
{
printk("platform_get_resource error.\n");
return -EINVAL;
}
lcd_regs_base = devm_ioremap_resource(&pdev->dev, res0);
if (lcd_regs_base == NULL)
{
printk("devm_ioremap_resource error.\n");
return -EINVAL;
}
res1 = platform_get_resource(pdev, IORESOURCE_MEM, 1);
if (res1 == NULL)
{
printk("platform_get_resource error.\n");
return -EINVAL;
}
lcdblk_regs_base = devm_ioremap_resource(&pdev->dev, res1);
if (lcdblk_regs_base == NULL)
{
printk("devm_ioremap_resource error.\n");
return -EINVAL;
}
res2 = platform_get_resource(pdev, IORESOURCE_MEM, 2);
if (res2 == NULL)
{
printk("platform_get_resource error.\n");
return -EINVAL;
}
//bug:
/*
devm_ioremap()和devm_ioremap_resource()区别:
devm_ioremap()可以重复map相同的地址空间,devm_ioremap_resource()不可以。
一般SoC的中,各个硬件模块各自的memory region都有严格的划分(比如说USB host的地址空间绝对不会和flash host冲突),
所以一般的driver使用devm_ioremap()和devm_ioremap_resource()都行。
但这里,应该系统已经映射过一次了,所以使用devm_ioremap_resource()会报错。
*/
//lcd0_configuration = devm_ioremap_resource(&pdev->dev, res2);
lcd0_configuration = devm_ioremap(&pdev->dev, res2->start, resource_size(res2));
if (lcd0_configuration == NULL)
{
printk("devm_ioremap_resource error.\n");
return -EINVAL;
}
*(unsigned long *)lcd0_configuration = 7; //Reset Value = 0x00000007
res3 = platform_get_resource(pdev, IORESOURCE_MEM, 3);
if (res3 == NULL)
{
printk("platform_get_resource error.\n");
return -EINVAL;
}
//clk_regs_base = devm_ioremap_resource(&pdev->dev, res3);
clk_regs_base = devm_ioremap(&pdev->dev, res3->start, resource_size(res3));
if (clk_regs_base == NULL)
{
printk("devm_ioremap_resource error.\n");
return -EINVAL;
}
//时钟源选择\使能时钟
//Selects clock source for LCD_BLK
//FIMD0_SEL:bit[3:0]=0110=SCLKMPLL_USER_T=800M
temp = readl(clk_regs_base + CLK_SRC_LCD0);
temp &= ~(0x0F<<0);
temp |= (0x3<<1);
writel(temp, clk_regs_base + CLK_SRC_LCD0);
//Clock source mask for LCD_BLK
//FIMD0_MASK:Mask output clock of MUXFIMD0 (1=Unmask)
temp = readl(clk_regs_base + CLK_SRC_MASK_LCD);
temp |= (0x01<<0);
writel(temp, clk_regs_base + CLK_SRC_MASK_LCD);
//Clock source mask for LCD_BLK
//SCLK_FIMD0 = MOUTFIMD0/(FIMD0_RATIO + 1),分频比 1/1
temp = readl(clk_regs_base + CLK_DIV_LCD);
temp &= ~(0x0F<<0);
writel(temp, clk_regs_base + CLK_DIV_LCD);
//Controls IP clock gating for LCD_BLK
//CLK_FIMD0:Gating all clocks for FIMD0 (1=Pass)
temp = readl(clk_regs_base + CLK_GATE_IP_LCD);
temp |= (0x01<<0);
writel(temp, clk_regs_base + CLK_GATE_IP_LCD);
//FIMDBYPASS_LBLK0:FIMD of LBLK0 Bypass Selection (1=FIMD Bypass)
temp = readl(lcdblk_regs_base + LCDBLK_CFG);
temp |= (0x01<<1);
writel(temp, lcdblk_regs_base + LCDBLK_CFG);
//MIE0_DISPON:MIE0_DISPON: PWM output control (1=PWM outpupt enable)
temp = readl(lcdblk_regs_base + LCDBLK_CFG2);
temp |= (0x01<<0);
writel(temp, lcdblk_regs_base + LCDBLK_CFG2);
mdelay(1000);
//LCD时钟: VCLK=FIMD*SCLK/(CLKVAL+1), where CLKVAL>=1
//800/(19+1) == 40M<80M
temp = readl(lcd_regs_base + VIDCON0);
temp |= (19<<6);
writel(temp, lcd_regs_base + VIDCON0);
/*
* VIDTCON1:
* [5]:IVSYNC ===> 1 : Inverted(反转)
* [6]:IHSYNC ===> 1 : Inverted(反转)
* [7]:IVCLK ===> 1 : Fetches video data at VCLK rising edge (上降沿触发)
* [10:9]:FIXVCLK ====> 01 : VCLK running
*/
temp = readl(lcd_regs_base + VIDCON1);
temp |= (1 << 9) | (1 << 7) | (1 << 5) | (1 << 6);
writel(temp, lcd_regs_base + VIDCON1);
/*
* VIDTCON0:
* [23:16]: VBPD+1=tvb-tvpw=23-11=12 --> VBPD=11
* [15:8] : VFPD+1=tvfp=22 --> VFPD=21
* [7:0] : VSPW+1=tvpw=1~20(暂取11) --> VSPW=10
*/
temp = readl(lcd_regs_base + VIDTCON0);
temp |= (11 << 16) | (21 << 8) | (10 << 0);
writel(temp, lcd_regs_base + VIDTCON0);
/*
* VIDTCON1:
* [23:16]: HBPD+1=thb-hpw=46-21=25 --> HBPD=24
* [15:8] : HFPD+1=thfp=210 --> HFPD=209
* [7:0] : HSPW+1=hpw=1~40(暂取21) --> HSPW=20
*/
temp = readl(lcd_regs_base + VIDTCON1);
temp |= (24 << 16) | (209 << 8) | (20 << 0);
writel(temp, lcd_regs_base + VIDTCON1);
/*
* HOZVAL = (Horizontal display size) - 1 and LINEVAL = (Vertical display size) - 1.
* Horizontal(水平) display size : 800
* Vertical(垂直) display size : 480
*/
temp = ((LCD_WIDTH-1) << 11) | (LCD_LENTH << 0);
writel(temp, lcd_regs_base + VIDTCON2);
/*
* WINCON0:
* [15]:Specifies Word swap control bit. 1 = Enables swap 低位像素存放在低字节
* [5:2]: Selects Bits Per Pixel (BPP) mode for Window image : 1101 ===> Unpacked 25 BPP (non-palletized A:1-R:8-G:8-B:8)
* [0]:Enables/disables video output 1 = Enables
*/
temp = readl(lcd_regs_base + WINCON0);
temp &= ~(0x0F << 2);
temp |= (0X01 << 15) | (0x0D << 2) | (0x01<<0);
writel(temp, lcd_regs_base + WINCON0);
//Enables Channel 0.
temp = readl(lcd_regs_base + SHADOWCON);
writel(temp | 0x01, lcd_regs_base + SHADOWCON);
//Selects Channel 0
temp = readl(lcd_regs_base + WINCHMAP2);
temp &= ~(7 << 16);
temp |= (0x01 << 16);//CH0FISEL:Selects Channel 0's channel.001 = Window 0
temp &= ~(7 << 0);
temp |= (0x01 << 0);//W0FISEL:Selects Window 0's channel.001 = Channel 0
writel(temp, lcd_regs_base + WINCHMAP2);
//设置OSD显示大小
//Window Size For example. Height * Width (number of word)
temp = (LCD_LENTH * LCD_WIDTH) >> 1;
writel(temp, lcd_regs_base + VIDOSD0C);
/*
* bit0-10 : 指定OSD图像左上像素的垂直屏幕坐标
* bit11-21: 指定OSD图像左上像素的水平屏幕坐标
*/
writel(0, lcd_regs_base + VIDOSD0A);
/*
* bit0-10 : 指定OSD图像右下像素的垂直屏幕坐标
* bit11-21: 指定OSD图像右下像素的水平屏幕坐标
*/
writel(((LCD_LENTH-1) << 11) | (LCD_WIDTH-1), lcd_regs_base + VIDOSD0B);
//Display On: ENVID and ENVID_F are set to "1".
temp = readl(lcd_regs_base + VIDCON0);
writel(temp | (0x01<<1) | (0x01<<0), lcd_regs_base + VIDCON0);
/* 3.3 分配显存(framebuffer), 并把地址告诉LCD控制器 */
// tiny4412_lcd->screen_base 显存虚拟地址
// tiny4412_lcd->fix.smem_len 显存大小,前面计算的
// tiny4412_lcd->fix.smem_start 显存物理地址
tiny4412_lcd->screen_base = dma_alloc_writecombine(NULL, tiny4412_lcd->fix.smem_len, (dma_addr_t *)&tiny4412_lcd->fix.smem_start, GFP_KERNEL);
//显存起始地址
writel(tiny4412_lcd->fix.smem_start, lcd_regs_base + VIDW00ADD0B0);
//显存结束地址
writel(tiny4412_lcd->fix.smem_start + tiny4412_lcd->fix.smem_len, lcd_regs_base + VIDW00ADD1B0);
/* 4. 注册 */
ret = register_framebuffer(tiny4412_lcd);
return ret;
}
static int lcd_remove(struct platform_device *pdev)
{
//Direct Off: ENVID and ENVID_F are set to “0” simultaneously.
unsigned int temp;
temp = readl(lcd_regs_base + VIDCON0);
temp &= ~(0x01<<1 | 0x01<<0);
writel(temp, lcd_regs_base + VIDCON0);
unregister_framebuffer(tiny4412_lcd);
dma_free_writecombine(NULL, tiny4412_lcd->fix.smem_len, tiny4412_lcd->screen_base, tiny4412_lcd->fix.smem_start);
framebuffer_release(tiny4412_lcd);
return 0;
}
static const struct of_device_id lcd_dt_ids[] =
{
{ .compatible = “tiny4412, lcd_s702”, },
{},
};
MODULE_DEVICE_TABLE(of, lcd_dt_ids);
static struct platform_driver lcd_driver =
{
.driver = {
.name = “lcd_s702”,
.of_match_table = of_match_ptr(lcd_dt_ids),
},
.probe = lcd_probe,
.remove = lcd_remove,
};
static int lcd_init(void)
{
int ret;
printk(“enter %s\n”, func);
ret = platform_driver_register(&lcd_driver);
if (ret)
{
printk(KERN_ERR "lcd: probe fail: %d\n", ret);
}
return ret;
}
static void lcd_exit(void)
{
printk(“enter %s\n”, func);
platform_driver_unregister(&lcd_driver);
}
module_init(lcd_init);
module_exit(lcd_exit);
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“hceng [email protected]”);
MODULE_DESCRIPTION(“Tiny4412 LCD driver.”);
MODULE_ALIAS(“Exynos4412_s702”);
MODULE_VERSION(“V1.0”);
{% endcodeblock %}
参考博客:设备树学习之(十二)LCD驱动
5.测试程序
5.1 图像显示
前面完成了驱动,要想显示内容,只需要mmap()
一块内存,然后向里面写数据即可。
但这种操作一般显示用于显示几何图形,像色块,圆形,矩形等,实用性不大,显示一张图片似乎更有意思。
显示图片没那么容易了,像常见的JPEG格式图片,它是一个“压缩文件”,需要解压得到RGB数据。
这就要用到libjpeg-turbo
,一个用C语言写的JPEG图像解码器。
5.1.1 移植libjpeg
下载目前最新的libjpeg-turbo
源码libjpeg-turbo-1.5.3.tar.gz
。
a. 解压
sudo tar xzf libjpeg-turbo-1.5.3.tar.gz
b.设置配置文件
--prefix=
:指定安装路径
--host==
:指定目标程序运行主机类型
cd libjpeg-turbo-1.5.3
mkdir tmp //创建临时安装路径
cd ..
./configure --prefix=/work/drv/2_lcd/libjpeg_new/libjpeg-turbo-1.5.3/tmp --host=arm-none-linux-gnueabi
make
ma
make install
5.1.2 编写应用程序
这块几乎全是参考韦东山老师第三期视频的电子相册项目。
解压操作过程如下:
1、分配jpeg对象结构体空间,并初始化
2、指定解压数据源
3、获取解压文件信息
4、为解压设定参数,包括图像大小和颜色空间
5、开始解压缩
6、取数据并显示
7、解压完毕
8、释放资源和退出程序
1、分配jpeg对象结构体空间、并初始化
解压缩过程中使用的JPEG对象是一个jpeg_decompress_struct
的结构体。
同时还需要定义一个用于错误处理的结构体对象,IJG中标准的错误结构体是jpeg_error_mgr
。
{% codeblock lang:c %}
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
{% endcodeblock %}
绑定jerr错误结构体至jpeg对象结构体。
{% codeblock lang:c %}
cinfo.err = jpeg_std_error(&jerr);
{% endcodeblock %}
这个标准的错误处理结构将使程序在出现错误时调用exit()退出程序,如果不希望使用标准的错误处理方式,则可以通过自定义退出函数的方法自定义错误处理结构。
初始化cinfo结构体。
{% codeblock lang:c %}
jpeg_create_decompress(&cinfo);
{% endcodeblock %}
2、指定解压数据源
{% codeblock lang:c %}
FILE * infile;
if ((infile = fopen(argv[1], "rb")) == NULL) {
fprintf(stderr, "can't open %s\n", argv[1]);
return -1;
}
jpeg_stdio_src(&cinfo, infile);
{% endcodeblock %}
3、获取解压文件信息
将图像的缺省信息填充到cinfo结构中以便程序使用。
{% codeblock lang:c %}
jpeg_read_header(&cinfo, TRUE);
{% endcodeblock %}
此时,常见的可用信息包括图像的:
宽cinfo.image_width
,高cinfo.image_height
,色彩空间cinfo.jpeg_color_space
,颜色通道数cinfo.num_components
等。
4、为解压设定参数,包括图像大小和颜色空间
比如可以设定解出来的图像的大小,也就是与原图的比例。
使用scale_num
和scale_denom
两个参数,解出来的图像大小就是scale_num/scale_denom
,但是IJG当前仅支持1/1, 1/2, 1/4,和1/8这几种缩小比例。
{% codeblock lang:c %}
printf(“enter scale M/N:\n”);
scanf("%d/%d", &cinfo.scale_num, &cinfo.scale_denom);
printf(“scale to : %d/%d\n”, cinfo.scale_num, cinfo.scale_denom);
{% endcodeblock %}
假如想让图片变为原来的一般,只需要输入1/2
即可。
也可以设定输出图像的色彩空间,即cinfo.out_color_space
,可以把一个原本彩色的图像由真彩色JCS_RGB变为灰度JCS_GRAYSCALE。
{% codeblock lang:c %}
cinfo.out_color_space=JCS_GRAYSCALE;
{% endcodeblock %}
5、开始解压缩
根据设定的解压缩参数进行图像解压缩操作。
{% codeblock lang:c %}
jpeg_start_decompress(&cinfo);
{% endcodeblock %}
在完成解压缩操作后,会将解压后的图像信息填充至cinfo
结构中。比如,输出图像宽度cinfo.output_width
,输出图像高度cinfo.output_height
,每个像素中的颜色通道数cinfo.output_components
(比如灰度为1,全彩色为3)等。
一般情况下,这些参数是在jpeg_start_decompress
后才被填充到cinfo
中的,如果希望在调用jpeg_start_decompress
之前就获得这些参数,可以通过调用jpeg_calc_output_dimensions()
的方法来实现。
6、取数据并显示
解开的数据是按照行取出的,数据像素按照scanline来
存储,scanline
是从左到右,从上到下的顺序,每个像素对应的各颜色或灰度通道数据是依次存储。
比如一个24-bit RGB真彩色的图像中,一个scanline
中的数据存储模式是R,G,B,R,G,B,R,G,B,…,每条scanline
是一个JSAMPLE
类型的数组,一般来说就是 unsigned char
,定义于jmorecfg.h
中。
除了JSAMPLE
,图像还定义了JSAMPROW
和JSAMPARRAY
,分别表示一行JSAMPLE
和一个2D的JSAMPLE
数组。
在此,我定义一个JSAMPARRAY(unsigned char)
类型的缓冲区变量buffer
来存放图像数据。
{% codeblock lang:c %}
int row_stride;
unsigned char *buffer;
// 然后是计算每行需要的空间大小,比如RGB图像就是宽度×3,灰度图就是宽度×1
row_stride = cinfo.output_width * cinfo.output_components;
buffer = malloc(row_stride);
// 循环调用jpeg_read_scanlines来一行一行地获得解压的数据
while (cinfo.output_scanline < cinfo.output_height)
{
(void) jpeg_read_scanlines(&cinfo, &buffer, 1);//每次读取1行
// 写到LCD去
fb_show_line(0, cinfo.output_width, cinfo.output_scanline, buffer);
}
{% endcodeblock %}
然后需要实现函数fb_show_line()
显示每行数据,要实现fb_show_line()
还得先实现每个像素点的描绘fb_show_pixel()
。
因此需要先初始化fb,获取LCD参数信息,如分辨率,多少BPP。再实现显示每个像素点,最后实现每一行的显示。
初始化:
{% codeblock lang:c %}
static int fb_device_init(void)
{
int ret;
fd = open(FB_DEVICE_NAME, O_RDWR);
if (fd < 0)
{
printf("Can't open %s\n", FB_DEVICE_NAME);
return -1;
}
//获取可变信息
ret = ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
if (ret < 0)
{
printf("Can't get fb's var\n");
return -1;
}
//获取固定信息
ret = ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
if (ret < 0)
{
printf("Can't get fb's fix\n");
return -1;
}
//映射fb
screen_size = fb_var.xres * fb_var.yres * fb_var.bits_per_pixel / 8;
fb_mem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (fb_mem < 0)
{
printf("Can't mmap\n");
return -1;
}
line_width = fb_var.xres * fb_var.bits_per_pixel / 8;
pixel_width = fb_var.bits_per_pixel / 8;
return 0;
}
{% endcodeblock %}
显示每个像素点:
{% codeblock lang:c %}
static int fb_show_pixel(int x, int y, unsigned int color)
{
unsigned char *fb_show;
unsigned short *fb_show_16bpp;
unsigned int fb_show_32bpp;
unsigned short fb_show_16bpp_new; / 565 */
int red;
int green;
int blue;
if ((x >= fb_var.xres) || (y >= fb_var.yres))
{
printf("Out of region\n");
return -1;
}
fb_show = fb_mem + line_width * y + pixel_width * x;//定位
fb_show_16bpp = (unsigned short *)fb_show;
fb_show_32bpp = (unsigned int *)fb_show;
switch (fb_var.bits_per_pixel)
{
case 8:
{
*fb_show = (unsigned char)color;
break;
}
case 16:
{
red = (color >> (16+3)) & 0x1F;
green = (color >> (8+2)) & 0x3F;
blue = (color >> 3) & 0x1F;
fb_show_16bpp_new = (red << 11) | (green << 5) | blue;
*fb_show_16bpp = fb_show_16bpp_new;
break;
}
case 32:
{
*fb_show_32bpp = color;
break;
}
default :
{
printf("Can't support %d bpp\n", fb_var.bits_per_pixel);
return -1;
}
}
return 0;
}
{% endcodeblock %}
显示每行:
{% codeblock lang:c %}
static int fb_show_line(int x_start, int x_end, int y, unsigned char *color_array)
{
int i = x_start * 3;
int x;
unsigned int color;
if (y >= fb_var.yres)
return -1;
if (x_start >= fb_var.xres)
return -1;
if (x_end >= fb_var.xres)
{
x_end = fb_var.xres;
}
for (x = x_start; x < x_end; x++)
{
/* 0xRRGGBB */
color = (color_array[i]<<16) + (color_array[i+1]<<8) + (color_array[i+2]<<0);
i += 3;
fb_show_pixel(x, y, color);
}
return 0;
}
{% endcodeblock %}
除此之外,每次显示前还需要清除原来的显示信息,也就是清空显示信息一次。
{% codeblock lang:c %}
static int fb_clean_screen(unsigned int back_color)
{
unsigned char *fb_show;
unsigned short *fb_show_16bpp;
unsigned int fb_show_32bpp;
unsigned short fb_show_16bpp_new; / 565 */
int red;
int green;
int blue;
int i = 0;
fb_show = fb_mem;
fb_show_16bpp = (unsigned short *)fb_show;
fb_show_32bpp = (unsigned int *)fb_show;
switch (fb_var.bits_per_pixel)
{
case 8:
{
memset(fb_mem, back_color, screen_size);
break;
}
case 16:
{
red = (back_color >> (16+3)) & 0x1F;
green = (back_color >> (8+2)) & 0x3F;
blue = (back_color >> 3) & 0x1F;
fb_show_16bpp_new = (red << 11) | (green << 5) | blue;
while (i < screen_size)
{
*fb_show_16bpp = fb_show_16bpp_new;
fb_show_16bpp++;
i += 2;
}
break;
}
case 32:
{
while (i < screen_size)
{
*fb_show_32bpp = back_color;
fb_show_32bpp++;
i += 4;
}
break;
}
default :
{
printf("Can't support %d bpp\n", fb_var.bits_per_pixel);
return -1;
}
}
return 0;
}
{% endcodeblock %}
7、解压完毕
{% codeblock lang:c %}
jpeg_finish_decompress(&cinfo);
{% endcodeblock %}
8、释放资源和退出程序
{% codeblock lang:c %}
free(buffer);
jpeg_destroy_decompress(&cinfo);
fclose(infile);
{% endcodeblock %}
参考博客:libjpeg库的简单使用使用----jpeg图片解压
5.1.3 完整应用程序
{% codeblock lang:c [jpg_rgb.c] https://github.com/hceng/learn/blob/master/tiny4412/02_lcd_drv/libjpeg_new/jpg_rgb.c %}
#include <stdio.h>
#include “jpeglib.h”
#include <setjmp.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include <string.h>
#include <stdlib.h>
#define FB_DEVICE_NAME “/dev/fb0”
static int fd;
static struct fb_var_screeninfo fb_var;
static struct fb_fix_screeninfo fb_fix;
static unsigned char *fb_mem;
static unsigned int screen_size;
static unsigned int line_width;
static unsigned int pixel_width;
static int fb_device_init(void)
{
int ret;
fd = open(FB_DEVICE_NAME, O_RDWR);
if (fd < 0)
{
printf("Can't open %s\n", FB_DEVICE_NAME);
return -1;
}
//获取可变信息
ret = ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
if (ret < 0)
{
printf("Can't get fb's var\n");
return -1;
}
//获取固定信息
ret = ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
if (ret < 0)
{
printf("Can't get fb's fix\n");
return -1;
}
//映射fb
screen_size = fb_var.xres * fb_var.yres * fb_var.bits_per_pixel / 8;
fb_mem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (fb_mem < 0)
{
printf("Can't mmap\n");
return -1;
}
line_width = fb_var.xres * fb_var.bits_per_pixel / 8;
pixel_width = fb_var.bits_per_pixel / 8;
return 0;
}
//color:0x00RRGGBB
static int fb_show_pixel(int x, int y, unsigned int color)
{
unsigned char *fb_show;
unsigned short *fb_show_16bpp;
unsigned int fb_show_32bpp;
unsigned short fb_show_16bpp_new; / 565 */
int red;
int green;
int blue;
if ((x >= fb_var.xres) || (y >= fb_var.yres))
{
printf("Out of region\n");
return -1;
}
fb_show = fb_mem + line_width * y + pixel_width * x;//定位
fb_show_16bpp = (unsigned short *)fb_show;
fb_show_32bpp = (unsigned int *)fb_show;
switch (fb_var.bits_per_pixel)
{
case 8:
{
*fb_show = (unsigned char)color;
break;
}
case 16:
{
red = (color >> (16+3)) & 0x1F;
green = (color >> (8+2)) & 0x3F;
blue = (color >> 3) & 0x1F;
fb_show_16bpp_new = (red << 11) | (green << 5) | blue;
*fb_show_16bpp = fb_show_16bpp_new;
break;
}
case 32:
{
*fb_show_32bpp = color;
break;
}
default :
{
printf("Can't support %d bpp\n", fb_var.bits_per_pixel);
return -1;
}
}
return 0;
}
static int fb_clean_screen(unsigned int back_color)
{
unsigned char *fb_show;
unsigned short *fb_show_16bpp;
unsigned int fb_show_32bpp;
unsigned short fb_show_16bpp_new; / 565 */
int red;
int green;
int blue;
int i = 0;
fb_show = fb_mem;
fb_show_16bpp = (unsigned short *)fb_show;
fb_show_32bpp = (unsigned int *)fb_show;
switch (fb_var.bits_per_pixel)
{
case 8:
{
memset(fb_mem, back_color, screen_size);
break;
}
case 16:
{
red = (back_color >> (16+3)) & 0x1F;
green = (back_color >> (8+2)) & 0x3F;
blue = (back_color >> 3) & 0x1F;
fb_show_16bpp_new = (red << 11) | (green << 5) | blue;
while (i < screen_size)
{
*fb_show_16bpp = fb_show_16bpp_new;
fb_show_16bpp++;
i += 2;
}
break;
}
case 32:
{
while (i < screen_size)
{
*fb_show_32bpp = back_color;
fb_show_32bpp++;
i += 4;
}
break;
}
default :
{
printf("Can't support %d bpp\n", fb_var.bits_per_pixel);
return -1;
}
}
return 0;
}
static int fb_show_line(int x_start, int x_end, int y, unsigned char *color_array)
{
int i = x_start * 3;
int x;
unsigned int color;
if (y >= fb_var.yres)
return -1;
if (x_start >= fb_var.xres)
return -1;
if (x_end >= fb_var.xres)
{
x_end = fb_var.xres;
}
for (x = x_start; x < x_end; x++)
{
/* 0xRRGGBB */
color = (color_array[i]<<16) + (color_array[i+1]<<8) + (color_array[i+2]<<0);
i += 3;
fb_show_pixel(x, y, color);
}
return 0;
}
/*
- Uage: jpg_rgb <jpg_file>
*/
int main(int argc, char **argv)
{
//1、分配jpeg对象结构体空间、并初始化
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE * infile;
int row_stride;
unsigned char *buffer;
if (argc != 2)
{
printf("Usage: \n");
printf("%s <jpg_file>\n", argv[0]);
return -1;
}
if (fb_device_init())
{
return -1;
}
fb_clean_screen(0);
//绑定jerr错误结构体至jpeg对象结构体
cinfo.err = jpeg_std_error(&jerr);
//初始化cinfo结构体
jpeg_create_decompress(&cinfo);
//2、指定解压数据源
if ((infile = fopen(argv[1], "rb")) == NULL) {
fprintf(stderr, "can't open %s\n", argv[1]);
return -1;
}
jpeg_stdio_src(&cinfo, infile);
//3、获取解压文件信息
jpeg_read_header(&cinfo, TRUE);
/* 源信息 */
printf("image_width = %d\n", cinfo.image_width);
printf("image_height = %d\n", cinfo.image_height);
printf("num_components = %d\n", cinfo.num_components);
//4、为解压设定参数,包括图像大小和颜色空间
printf("enter scale M/N:\n");
scanf("%d/%d", &cinfo.scale_num, &cinfo.scale_denom);
printf("scale to : %d/%d\n", cinfo.scale_num, cinfo.scale_denom);
//5、开始解压缩
jpeg_start_decompress(&cinfo);
/* 输出的图象的信息 */
printf("output_width = %d\n", cinfo.output_width);
printf("output_height = %d\n", cinfo.output_height);
printf("output_components = %d\n", cinfo.output_components);
//6、取数据并显示
//一行的数据长度
row_stride = cinfo.output_width * cinfo.output_components;
buffer = malloc(row_stride);
// 循环调用jpeg_read_scanlines来一行一行地获得解压的数据
while (cinfo.output_scanline < cinfo.output_height)
{
(void) jpeg_read_scanlines(&cinfo, &buffer, 1);
// 写到LCD去
fb_show_line(0, cinfo.output_width, cinfo.output_scanline, buffer);
}
//7、解压完毕
jpeg_finish_decompress(&cinfo);
//8、释放资源和退出程序
free(buffer);
jpeg_destroy_decompress(&cinfo);
return 0;
}
{% endcodeblock %}
5.1.4 交叉编译应用程序
方法一:
jpg_rgb.c
要想在开发板上运行,就得先交叉编译,里面用到了一非系统的头文件和函数,因此还需要指定头文件和库路径。
-I
:指定头文件路径
-L
:指定库路径
-i
:指定具体哪个库
arm-none-linux-gnueabi-gcc -o jpg_rgb jpg_rgb.c \
-I /work/drv/2_lcd/libjpeg_new/libjpeg-turbo-1.5.3/tmp/include \
-L /work/drv/2_lcd/libjpeg_new/libjpeg-turbo-1.5.3/tmp/lib \
-ljpeg
方法二:
前面的方法,每次编译都要指定一堆路径,比较麻烦,解决方法是将头文件和库复制到交叉编译工具链所在的路径:
//拷贝头文件
cd /work/drv/2_lcd/show_jpeg/libjpeg-turbo-1.5.3/tmp/include/
cp * /work/arm-2014.05/arm-none-linux-gnueabi/libc/usr/include
//拷贝库
cd /work/drv/2_lcd/show_jpeg/libjpeg-turbo-1.5.3/tmp/lib/
cp * /work/arm-2014.05/arm-none-linux-gnueabi/libc/lib
//编译
arm-none-linux-gnueabi-gcc -o jpg_rgb jpg_rgb.c -ljpeg
5.1.5 运行应用程序
a. 先把应用程序和动态库拷贝到开发板上:
cp jpg_rgb /work/nfs_rootfs/
cp libjpeg-turbo-1.5.3/tmp/lib/*so* /work/nfs_rootfs/lib/ -d
b. 加载背光和LCD驱动:
insmod backlight_drv.ko
insmod lcd_drv.ko
c. 开启背光:
./app 200
d. 运行应用程序:
./jpg_rgb cq.jpg
5.1.6 实际效果
5.2移除左上角光标
现在已经实现了图片的显示,仔细观察,发现左上角有个小光标一直在闪烁。
查阅了相关资料,解决方案如下:
修改Linux内核中的文件:drivers/video/console/fbcon.c
-
去掉光标闪烁:
将函数static void fbcon_cursor(struct vc_data *vc, int mode)
改为空函数即可。 -
去掉光标显示:
将函数static void fb_flashcursor(struct work_struct *work)
改为空函数即可。
再重新编译、烧写内核。
6.总结
Beautiful Chongqing.
总算搞完了LCD,真累。
拖拖拉拉了差不多一个月。
对LCD这块,不管是基础知识、驱动还是应用程序,都有了一个新的认识,也除去了LCD这块拦路虎的“心结”,有信心去迎接更复杂的模块。
后续的话,搞定电容屏驱动,就可以考虑学习下摄像头驱动了。