Exynos4412——LCD驱动

版权声明:本文采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,欢迎转载,但转载请注明来自hceng blog(www.hceng.cn),并保持转载后文章内容的完整。本人保留所有版权相关权利。 https://blog.csdn.net/hceng_linux/article/details/89873988

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硬件控制思路

  1. 查看LCD芯片手册,查看相关的时间参数、分辨率、引脚极性等;
  2. 根据以上信息设置LCD控制器寄存器,让其发出正确信号;
  3. 在内存里面分配一个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.cfb_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_LENTH
BITS_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_numscale_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,图像还定义了JSAMPROWJSAMPARRAY,分别表示一行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这块拦路虎的“心结”,有信心去迎接更复杂的模块。

后续的话,搞定电容屏驱动,就可以考虑学习下摄像头驱动了。

猜你喜欢

转载自blog.csdn.net/hceng_linux/article/details/89873988