<Linux开发>驱动开发 -之- Linux LCD 驱动
交叉编译环境搭建:
<Linux开发> linux开发工具-之-交叉编译环境搭建
uboot移植可参考以下:
<Linux开发> -之-系统移植 uboot移植过程详细记录(第一部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第二部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第三部分)(uboot移植完结)
Linux内核及设备树移植可参考以下:
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第一部分)
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第二部分完结)
Linux文件系统构建移植参考以下:
<Linux开发>系统移植 -之- linux构建BusyBox根文件系统及移植过程详细记录
<Linux开发>系统移植 -之-使用buildroot构建BusyBox根文件系统
Linux驱动开发参考以下:
<Linux开发>驱动开发 -之-pinctrl子系统
<Linux开发>驱动开发 -之-gpio子系统
<Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的LED驱动
<Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的beep驱动
<Linux开发>驱动开发 -之-资源的并发与竞争处理
<Linux开发>驱动开发 -之-内核定时器与中断
<Linux开发>驱动开发 -之-阻塞、非阻塞IO和异步通知
<Linux开发>驱动开发 -之-Linux MISC 驱动
<Linux开发>驱动开发 -之-Linux INPUT 子系统
一 前言
LCD 液晶屏是常用到的外设,通过 LCD 可以显示绚丽的图形、界面等,提高人机交互的效率。本系列文章所用的开发板I.MX6U 提供了一个 eLCDIF 接口用于连接 RGB 接口的液晶屏。
二 LCD
2.1 LCD简介
LCD 全称是 Liquid Crystal Display,也就是液晶显示器,是现在最常用到的显示器,手机、电脑、各种人机交互设备等基本都用到了 LCD,最常见就是手机和电脑显示器了。由于笔者不是 LCD 从业人员,对于 LCD 的具体原理不了解,百度百科对于 LCD 的原理解释如下:
LCD 的构造是在两片平行的玻璃基板当中放置液晶盒,下基板玻璃上设置 TFT(薄膜晶体管),上基板玻璃上设置彩色滤光片,通过 TFT 上的信号与电压改变来控制液晶分子的转动方向,从而达到控制每个像素点偏振光出射与否而达到显示目的。
我们现在要在 I.MX6U开发板上使用 LCD,所以不需要去研究 LCD 的具体实现原理,我们只需要从使用的角度去关注 LCD 的几个重要点:
① 分辨率
② 像素格式
③ LCD 屏幕接口
④ LCD 时间参数
⑤ RGB LCD 屏幕时序
⑥ 像素时钟
⑦ 显存
2.2 分辨率
提起 LCD 显示器,我们都会听到 720P、1080P、2K 或 4K 这样的字眼,这个就是 LCD 显示器分辨率。LCD 显示器都是由一个一个的像素点组成,像素点就类似一个灯(在 OLED 显示器中,像素点就是一个小灯),这个小灯是 RGB 灯,也就是由 R(红色)、G(绿色)和 B(蓝色)这三种颜色组成的,而 RGB 就是光的三原色。1080P 的意思就是一个 LCD 屏幕上的像素数量是1920*1080 个,也就是这个屏幕一列 1080 个像素点,一共 1920 列,如下图所示:
图2.2
上图就是 1080P 显示器的像素示意图,X 轴就是 LCD 显示器的横轴,Y 轴就是显示器的竖轴。图中的小方块就是像素点,一共有 19201080=2073600 个像素点。左上角的 A 点是第一个像素点,右下角的 C 点就是最后一个像素点。2K 就是 25601440 个像素点,4K 是3840*2160 个像素点。很明显,在 LCD 尺寸不变的情况下,分辨率越高越清晰。同样的,分辨率不变的情况下,LCD 尺寸越小越清晰。比如我们常用的 24 寸显示器基本都是 1080P 的,而我们现在使用的 5 寸的手机基本也是 1080P 的,但是手机显示细腻程度就要比 24 寸的显示器要好很多!
由此可见,LCD 显示器的分辨率是一个很重要的参数,但是并不是分辨率越高的 LCD 就越好。衡量一款 LCD 的好坏,分辨率只是其中的一个参数,还有色彩还原程度、色彩偏离、亮度、可视角度、屏幕刷新率等其他参数。
2.3 像素格式
上面讲了,一个像素点就相当于一个 RGB 小灯,通过控制 R、G、B 这三种颜色的亮度就可以显示出各种各样的色彩。那该如何控制 R、G、B 这三种颜色的显示亮度呢?一般一个 R、G、B 这三部分分别使用 8bit 的数据,那么一个像素点就是 8bit*3=24bit,也就是说一个像素点3 个字节,这种像素格式称为 RGB888。如果再加入 8bit 的 Alpha(透明)通道的话一个像素点就是 32bit,也就是 4 个字节,这种像素格式称为 ARGB8888。如果学习过 STM32 的话应该还听过 RGB565 这种像素格式,在本章实验中我们使用 ARGB8888 这种像素格式,一个像素占用 4个字节的内存,这四个字节每个位的分配如下图所示:
图 2.3.1
在上图中,一个像素点是 4 个字节,其中 bit31-bit24 是 Alpha 通道,bit23-bit16 是RED 通道,bit15-bit14 是 GREEN 通道,bit7-bit0 是 BLUE 通道。所以红色对应的值就是0X00FF0000,蓝色对应的值就是 0X000000FF,绿色对应的值为 0X0000FF00。通过调节 R、G、B的比例可以产生其它的颜色,比如0X00FFFF00就是黄色,0X00000000就是黑色,0X00FFFFFF就是白色。大家可以打开电脑的“画图”工具,在里面使用调色板即可获取到想要的颜色对应的数值,如下图所示:
图 2.3.2
2.4 LCD 屏幕接口
LCD 屏幕或者说显示器有很多种接口,比如在显示器上常见的 VGA、HDMI、DP、MIPI、LVDS 等等,但是I.MX6U开发板不支持这些接口。I.MX6U-ALPHA支持RGB接口的LCD,RGBLCD接口的信号线如下表所示:
表 2.4.1
信号线 | 描述 |
---|---|
R[7:0] | 8 根红色数据线。 |
G[7:0] | 8 根绿色数据线。 |
B[7:0] | 8 根蓝色数据线。 |
DE | 数据使能线 |
VSYNC | 垂直同步信号线 |
HSYNC | 水平同步信号线 |
PCLK | 像素时钟信号线 |
上表就是RGBLCD的信号线,R[7:0]、G[7:0]和B[7:0]这24根是数据线,DE、VSYNC、HSYNC 和 PCLK 这四根是控制信号线。
RGB LCD 一般有两种驱动模式:DE 模式和 HV 模式,这两个模式的区别是 DE 模式需要用到 DE 信号线,而 HV 模式不需要用到 DE 信号线,在 DE模式下是可以不需要 HSYNC 信号线的,即使不接 HSYNC 信号线 LCD 也可以正常工作。
本次实验使用的是 ATK-7016(7 寸,1024*600)这款 RGB LCD 屏幕,ATK-7016 的屏幕接口原理图如下图所示:
图 2.4.1
图中 J1 就是对外接口,是一个 40PIN 的 FPC 座(0.5mm 间距),通过 FPC 线,可以连接到 I.MX6U开发板上面,从而实现和 I.MX6U 的连接。该接口十分完善,采用 RGB888格式,并支持 DE&HV 模式,还支持触摸屏和背光控制。右侧的几个电阻,并不是都焊接的,而是可以用户自己选择。默认情况,R1 和 R6 焊接,设置 LCD_LR 和 LCD_UD,控制 LCD 的
扫描方向,是从左到右,从上到下(横屏看)。而 LCD_R7/G7/B7 则用来设置 LCD 的 ID,由于RGBLCD 没有读写寄存器,也就没有所谓的 ID,这里我们通过在模块上面,控制 R7/G7/B7 的上/下拉,来自定义 LCD 模块的 ID,帮助 MCU 判断当前 LCD 面板的分辨率和相关参数,以提高程序兼容性。这几个位的设置关系如下表所示:
M0 LCD_R7 | M1 LCD_G7 | M2 LCD_B7 | LCD ID | 描述 |
---|---|---|---|---|
0 | 0 | 0 | 40342 | ATK-4342 RGBLCD 模块,分辨率:480*272 |
0 | 0 | 1 | 7084 | ATK-7084 RGBLCD 模块,分辨率:800*480 |
0 | 1 | 0 | 7016 | ATK-7016,RGBLCD 模块,分辨率:1024*600 |
1 | 0 | 0 | 4384 | ATK-4384,RGBLCD 模块,分辨率:800*480 |
X | X | X | NC | NC |
ATK-7016 模块,就设置 M2:M0=010 即可。这样,我们在程序里面,读取 LCD_R7/G7/B7,得到 M0:M2 的值,从而判断 RGBLCD 模块的型号,并执行不同的配置,即可实现不同 LCD 模块的兼容。
2.5 LCD 时间参数
如果将 LCD 显示一帧图像的过程想象成绘画,那么在显示的过程中就是用一根“笔”在不同的像素点画上不同的颜色。这根笔按照从左至右、从上到下的顺序扫描每个像素点,并且在像素画上对应的颜色,当画到最后一个像素点的时候一幅图像就绘制好了。假如一个 LCD 的分辨率为 1024*600,那么其扫描如下图所示:
图 2.5.1
结合图 2.5.1 我们来看一下 LCD 是怎么扫描显示一帧图像的。一帧图像也是由一行一行组成的。
HSYNC 是水平同步信号,也叫做行同步信号,当产生此信号时就表示开始显示新的一行了,所以此信号都是在图 2.5.1 的最左边。
VSYNC 信号是垂直同步信号,也叫做帧同步信号,当产生此信号时就表示开始显示新的一帧图像了,所以此信号在图2.5.1 的左上角。
在图2.5.1 可以看到有一圈“黑边”,真正有效的显示区域是中间的白色部分。那这一圈“黑边”是什么东西呢?这就要从显示器的“祖先”CRT 显示器开始说起了,CRT 显示器就是以前很常见的那种大屁股显示器,如今基本都淘汰了。CRT 显示器屁股后面是个电子枪,这个电子枪就是我们上面说的“画笔”,电子枪打出的电子撞击到屏幕上的荧光物质使其发光。只要控制电子枪从左到右扫完一行(也就是扫描一行),然后从上到下扫描完所有行,这样一帧图像就显示出来了。也就是说,显示一帧图像电子枪是按照‘Z’形在运动,当扫描速度很快的时候看起来就是一幅完成的画面了。
当显示完一行以后会发出 HSYNC 信号,此时电子枪就会关闭,然后迅速的移动到屏幕的左边,当 HSYNC 信号结束以后就可以显示新的一行数据了,电子枪就会重新打开。在 HSYNC信号结束到电子枪重新打开之间会插入一段延时,这段延时就图 2.5.1 中的 HBP。当显示完一行以后就会关闭电子枪等待 HSYNC 信号产生,关闭电子枪到 HSYNC 信号产生之间会插入一段延时,这段延时就是图 2.5.1 中的 HFP 信号。同理,当显示完一帧图像以后电子枪也会关闭,然后等到 VSYNC 信号产生,期间也会加入一段延时,这段延时就是图 2.5.1 中的 VFP。VSYNC 信号产生,电子枪移动到左上角,当 VSYNC 信号结束以后电子枪重新打开,中间也会加入一段延时,这段延时就是图 24.1.1.5 中的 VBP。
HBP、HFP、VBP 和 VFP 就是导致图 24.1.1.5 中黑边的原因,但是这是 CRT 显示器存在黑边的原因,现在是 LCD 显示器,不需要电子枪了,那么为何还会有黑边呢?这是因为 RGB LCD屏幕内部是有一个 IC 芯片的,发送一行或者一帧数据给 IC芯片,IC芯片 是需要反应时间的。通过这段反应时间可以让 IC芯片 识别到一行数据扫描完了,要换行了,或者一帧图像扫描完了,要开始下一帧图像显示了。因此,在 LCD 屏幕中继续存在 HBP、HFP、VPB 和 VFP 这四个参数的主要目的是为了锁定有效的像素数据。这四个时间是 LCD 重要的时间参数,后面编写 LCD 驱动的时候要用到的,至于这四个时间参数具体值是多少,那要需要去查看所使用的 LCD 数据手册了。
2.6 RGB LCD 屏幕时序
上面讲了行显示和帧显示,我们来看一下行显示对应的时序图,如下图所示:
图 2.6.1
图2.6.1 就是 RGB LCD 的行显示时序,我们来分析一下其中重要的几个参数:
HSYNC:行同步信号,当此信号有效的话就表示开始显示新的一行数据,查阅所使用的LCD 数据手册可以知道此信号是低电平有效还是高电平有效,假设此时是低电平有效。
HSPW:有些地方也叫做 thp,是 HSYNC 信号宽度,也就是 HSYNC 信号持续时间。HSYNC信号不是一个脉冲,而是需要持续一段时间才是有效的,单位为 CLK。
HBP:有些地方叫做 thb,前面已经讲过了,术语叫做行同步信号后肩,单位是 CLK。
HOZVAL:有些地方叫做 thd,显示一行数据所需的时间,假如屏幕分辨率为 1024*600,那么 HOZVAL 就是 1024,单位为 CLK。
HFP:有些地方叫做 thf,前面已经讲过了,术语叫做行同步信号前肩,单位是 CLK。
当 HSYNC 信号发出以后,需要等待 HSPW+HBP 个 CLK 时间才会接收到真正有效的像素数据。当显示完一行数据以后需要等待 HFP 个 CLK 时间才能发出下一个 HSYNC 信号,所以显示一行所需要的时间就是:HSPW + HBP + HOZVAL + HFP。
一帧图像就是由很多个行组成的,RGB LCD 的帧显示时序如下图所示:
图 2.6.2
图 2.6.2 就是 RGB LCD 的帧显示时序,我们来分析一下其中重要的几个参数:
VSYNC:帧同步信号,当此信号有效的话就表示开始显示新的一帧数据,查阅所使用的LCD 数据手册可以知道此信号是低电平有效还是高电平有效,假设此时是低电平有效。
VSPW:有些地方也叫做 tvp,是 VSYNC 信号宽度,也就是 VSYNC 信号持续时间,单位为 1 行的时间。
VBP:有些地方叫做 tvb,前面已经讲过了,术语叫做帧同步信号后肩,单位为 1 行的时间。
LINE:有些地方叫做 tvd,显示一帧有效数据所需的时间,假如屏幕分辨率为 1024*600,那么 LINE 就是 600 行的时间。
VFP:有些地方叫做 tvf,前面已经讲过了,术语叫做帧同步信号前肩,单位为 1 行的时间。
显示一帧所需要的时间就是:VSPW+VBP+LINE+VFP 个行时间,最终的计算公式:
T = (VSPW+VBP+LINE+VFP) * (HSPW + HBP + HOZVAL + HFP)
因此我们在配置一款 RGB LCD 的时候需要知道这几个参数:HOZVAL(屏幕有效宽度)、LINE(屏幕有效高度)、HBP、HSPW、HFP、VSPW、VBP 和 VFP。本文所用 RGB LCD屏幕的参数如下表所示:
参数 | 值 | 单位 |
---|---|---|
水平显示区域 HOZVAL | 1024 | tCLK |
HSPW(thp) | 20 | tCLK |
HBP(thb) | 140 | tCLK |
HFP(thf) | 160 | tCLK |
垂直显示区域 LINE | 600 | th |
VSPW(tvp) | 3 | th |
VBP(tvb) | 20 | th |
VFP(tvf) | 12 | th |
像素时钟 | 51.2 | MHz |
2.7 像素时钟
像素时钟就是 RGB LCD 的时钟信号,以 本次使用的LCD款屏幕为例,显示一帧图像所需要的时钟数就是:
= (VSPW+VBP+LINE+VFP) * (HSPW + HBP + HOZVAL + HFP)
= (3 + 20 + 600 + 12) * (20 + 140 + 1024 + 160)
= 635 * 1344
= 853440。
显示一帧图像需要853440个时钟数,那么显示60帧就是:853440 * 60 = 51206400≈51.2M,所以像素时钟就是 51.2MHz。
查阅IMX6UL参考手册.pdf,I.MX6U 的 eLCDIF 接口时钟图如下图所示:
图 2.7.1
①:此部分是一个选择器,用于选择哪个 PLL 可以作为 LCDIF 时钟源,由寄存器CCM_CSCDR2 的位 LCDIF1_PRE_CLK_SEL(bit17:15)来决定,LCDIF1_PRE_CLK_SEL 选择设置如下表所示:
值 | 时钟源 |
---|---|
0 | PLL2 作为 LCDIF 的时钟源 |
1 | PLL3_PFD3 作为 LCDIF 的时钟源 |
2 | PLL5 作为 LCDIF 的时钟源 |
3 | PLL2_PFD0 作为 LCDIF 的时钟源 |
4 | PLL2_PFD1 作为 LCDIF 的时钟源 |
5 | PLL3_PFD1 作为 LCDIF 的时钟源 |
②、此部分是 LCDIF 时钟的预分频器,由寄存器 CCM_CSCDR2 的位 LCDIF1_PRED 来决定预分频值。可设置值为 0-7,分别对应 1-8 分频。
③、此部分进一步分频,由寄存器 CBCMR 的位 LCDIF1_PODF 来决定分频值。可设置值为 0-7,分别对应 1-8 分频。
④、此部分是一个选择器,选择 LCDIF 最终的根时钟,由寄存器 CSCDR2 的位LCDIF1_CLK_SEL 决定,LCDIF1_CLK_SEL 选择设置如下表所示:
值 | 时钟源 |
---|---|
0 | 前面复用器出来的时钟,也就是前面 PLL5 出来的时钟作为 LCDIF 的根时钟。 |
1 | ipp_di0_clk 作为 LCDIF 的根时钟 |
2 | ipp_di1_clk 作为 LCDIF 的根时钟 |
3 | ldb_di0_clk 作为 LCDIF 的根时钟 |
4 | ldb_di1_clk 作为 LCDIF 的根时钟 |
这里肯定选择 PLL5 出来的那一路时钟作为 LCDIF 的根时钟,因此 LCDIF1_CLK_SEL 设置为 0。LCDIF 既然选择了 PLL5 作为时钟源,那么还需要初始化 PLL5,LCDIF 的时钟是由PLL5 和图 2.7.1 中的②、③这两个分频值决定的,所以需要对这三个进行合理的设置以搭配出所需的时钟值,我们就以 ATK7016 屏幕所需的 51.2MHz 为例,看看如何进行配置。
PLL5 频率设置涉及到四个寄存器:CCM_PLL_VIDEO、CCM_PLL_VIDEO_NUM、CCM_PLL_VIDEO_DENOM 、 CCM_MISC2 。
其 中 CCM_PLL_VIDEO_NUM 和CCM_PLL_VIDEO_DENOM 这两个寄存器是用于小数分频的,我们这里为了简单不使用小数分频,因此这两个寄存器设置为 0。
PLL5 的时钟计算公式如下:
PLL5_CLK = OSC24M * (loopDivider + (denominator / numerator)) / postDivider
不使用小数分频的话 PLL5 时钟计算公式就可以简化为 :
PLL5_CLK = OSC24M * loopDivider / postDivider
OSC24M 就是 24MHz 的有源晶振,现在的问题就是设置 loopDivider 和 postDivider。
先来看一下寄存器 CCM_PLL_VIDEO,此寄存器结构如下图所示:
寄存器 CCM_PLL_VIDEO 用到的重要的位如下:
POST_DIV_SLECT(bit20:19):此位和寄存器 CCM_ANALOG_CCMSC2 的 VIDEO_DIV 位共同决定了 postDivider,为 0 的话是 4 分频,为 1 的话是 2 分频,为 2 的话是 1 分频。本章设置为 2,也就是 1 分频。
ENABLE(bit13):PLL5(PLL_VIDEO)使能位,为 1 的话使能 PLL5,为 0 的话关闭 PLL5。
DIV_SELECT(bit6:0):loopDivider 值,范围为 27~54,本章设置为 32。
寄存器 CCM_MISC2 ,此寄存器结构如下图所示:
寄存器 CCM_ANALOG_MISC2 的位 VIDEO_DIV(bit31:30)与寄存器 CCM_PLL_VIDEO 的位 POST_DIV_SLECT(bit20:19)共同决定了 postDivider,通过这两个的配合可以获得 2、4、8、16 分频。本章将 CCM_ANALOG_MISC2::VIDEO_DIV 设置为 0,也就是 1 分频,因此 postDivider 就是 1,CCM_PLL_VIDEO :: DIV_SELECT(bit6:0):loopDivider设置为 32,PLL5 的时钟频率就是:
PLL5_CLK = OSC24M * loopDivider / postDivider
= 24M * 32 / 1
= 768MHz。
PLL5 此时为 768MHz,在经过图 2.7.1 中的②和③进一步分频,设置②中为 3 分频,也就是寄存器 CCM_CSCDR2 的位 LCDIF1_PRED(bit14:12)为 2。设置③中为 5 分频,就是寄存器CCM_CBCMR 的位 LCDIF1_PODF(bit25:23)为 4。设置好以后最终进入到 LCDIF 的时钟频率就是:768/3/5 =51.2MHz,这就是我们需要的像素时钟频率。
2.8 显存
在讲像素格式的时候就已经说过了,如果采用 ARGB8888 格式的话一个像素需要 4 个字节的内存来存放像素数据,那么 1024600 分辨率就需要 1024600*4=2457600B≈2.4MB 内存。但是 RGB LCD 内部是没有内存的,所以就需要在开发板上的 DDR3 中分出一段内存作为 RGB LCD 屏幕的显存,我们如果要在屏幕上显示什么图像的话直接操作这部分显存即可。
三 eLCDIF 简介
3.1 eLCDIF 接口介绍
eLCDIF 是 I.MX6U 自带的液晶屏幕接口,用于连接 RGB LCD 接口的屏幕,eLCDIF 接口特性如下:
①、支持 RGB LCD 的 DE 模式。
②、支持 VSYNC 模式以实现高速数据传输。
③、支持 ITU-R BT.656 格式的 4:2:2 的 YCbCr 数字视频,并且将其转换为模拟 TV 信号。
④、支持 8/16/18/24/32 位 LCD。
eLCDIF 支持三种接口:MPU 接口、VSYNC 接口和 DOTCLK 接口,这三种接口区别如下:
(1) MPU 接口
MPU 接口用于在 I.MX6U 和 LCD 屏幕直接传输数据和命令,这个接口用于 6080/8080 接口的 LCD 屏幕,比如我们学习 STM32 的时候常用到的 MCU 屏幕。如果寄存器 LCDIF_CTRL的位 DOTCLK_MODE、DVI_MODE 和 VSYNC_MODE 都为 0 的话就表示 LCDIF 工作在 MPU接口模式。关于 MPU 接口的详细信息以及时序参考《I.MX6ULL 参考手册》第 2150 页的“34.4.6MPU Interface”小节,本教程不使用 MPU 接口。
(2) VSYNC 接口
VSYNC 接口时序和 MPU 接口时序基本一样,只是多了 VSYNC 信号来作为帧同步,当LCDIF_CTRL 的位 VSYNC_MODE 为 1 的时候此接口使能。关于 VSYNC 接口的详细信息请参考《I.MX6ULL 参考手册》第 2152 页的“34.4.7 VSYNC Interface”小节,本教程不使用 VSYNC接口。
(3) DOTCLK 接口
DOTCLK 接口就是用来连接 RGB LCD 接口屏幕的, 它包括 VSYNC、HSYNC、DOTCLK和 ENABLE(可选的)这四个信号,这样的接口通常被称为 RGB 接口。DOTCLK 接口时序如下图所示:
图 3.1
图3.1 是不是和图 2.6.1、图 2.6.2 很类似,因为 DOTCLK 接口就是连接 RGB 屏幕的,本教程使用的就是 DOTCLK 接口。
3.2 LCDIF_CTRL 寄存器
eLCDIF 要驱动起来 RGB LCD 屏幕,重点是配置好上一小节讲解的那些时间参数即可,这个通过配置相应的寄存器就可以了,所以我们接下来看一下 eLCDIF 接口的几个重要的寄存器,首先看一下 LCDIF_CTRL 寄存器,此寄存器结构如下图所示:
图 3.2
寄存器 LCDIF_CTRL 用到的重要位如下:
SFTRST(bit31):eLCDIF 软复位控制位,当此位为 1 的话就会强制复位 LCD。
CLKGATE(bit30):正常运行模式下,此位必须为 0!如果此位为 1 的话时钟就不会进入到LCDIF。
BYPASS_COUNT(bit19):如果要工作在 DOTCLK 模式的话就此位必须为 1。
VSYNC_MODE(bit18):此位为 1 的话 LCDIF 工作在 VSYNC 接口模式。
DOTCLK_MODE(bit17):此位为 1 的话 LCDIF 工作在 DOTCLK 接口模式。
INPUT_DATA_SWIZZLE(bit15:14):输入数据字节交换设置,此位为 0 的话不交换字节也就是小端模式;为 1 的话交换所有字节,也就是大端模式;为 2 的话半字交换;为 3 的话在每个半字内进行字节交换。本章我们设置为 0,也就是不使用字节交换。
CSC_DATA_SWIZZLE(bit13:12) : CSC 数 据 字 节 交 换 设 置 , 交 换 方 式 和INPUT_DATA_SWIZZLE 一样,本章设置为 0,不使用字节交换。
LCD_DATABUS_WIDTH(bit11:10):LCD 数据总线宽度,为 0 的话总线宽度为 16 位;为1 的话总线宽度为 8 位;为 2 的话总线宽度为 18 位;为 3 的话总线宽度为 24 位。本章我们使用 24 位总线宽度。
WORD_LENGTH(bit9:8):输入的数据格式,也就是像素数据宽度,为 0 的话每个像素 16位;为 1 的话每个像素 8 位;为 2 的话每个像素 18 位;为 3 的话每个像素 24 位。
MASTER(bit5):为 1 的话设置 eLCDIF 工作在主模式。
DATA_FORMAT_16_BIT(bit3):当此位为 1 并且 WORD_LENGTH 为 0 的时候像素格式为 ARGB555,当此位为 0 并且 WORD_LENGTH 为 0 的时候像素格式为 RGB565。
DATA_FORMAT_18_BIT(bit2):只有当 WORD_LENGTH 为 2 的时候此位才有效,此位为 0 的话低 18 位有效,像素格式为 RGB666,高 14 位数据无效。当此位为 1 的话高 18 位有效,像素格式依旧是 RGB666,但是低 14 位数据无效。
DATA_FORMAT_24_BIT(bit1):只有当 WORD_LENGTH 为 3 的时候此位才有效,为 0 的时候表示全部的 24 位数据都有效。为 1 的话实际输入的数据有效位只有 18 位,虽然输入的是24 位数据,但是每个颜色通道的高 2 位数据会被丢弃掉。
RUN(bit0):eLCDIF 接口运行控制位,当此位为 1 的话 eLCDIF 接口就开始传输数据,也就是 eLCDIF 的使能位。
3.3 LCDIF_CTRL1 寄存器
接 下 来 看 一 下 寄 存 器 LCDIF_CTRL1 , 此 寄 存 器 我 们 只 用 到 位
BYTE_PACKING_FORMAT(bit19:16),此位用来决定在 32 位的数据中哪些字节的数据有效,默认值为 0XF,也就是所有的字节有效,当为 0 的话表示所有的字节都无效。如果显示的数据是24 位(ARGB 格式,但是 A 通道不传输)的话就设置此位为 0X7。
图 3.3
3.4 LCDIF_TRANSFER_COUNT寄存器
接下来看一下寄存器 LCDIF_TRANSFER_COUNT,这个寄存器用来设置所连接的 RGB LCD 屏幕分辨率大小,此寄存器结构如图 24.1.2.2 所示:
图 3.4
寄存器LCDIF_TRANSFER_COUNT 分为两部分,高16位和低16位,高16位是V_COUNT,是 LCD 的垂直分辨率。低 16 位是 H_COUNT,是 LCD 的水平分辨率。如果 LCD 分辨率为1024*600 的话,那么 V_COUNT 就是 600,H_COUNT 就是 1024。
3.5 LCDIF_VDCTRL0寄存器
接下来看一下寄存器 LCDIF_VDCTRL0,这个寄存器是 VSYNC 和 DOTCLK 模式控制寄存器 0,寄存器结构如下图所示:
图 3.5
寄存器 LCDIF_VDCTRL0 用到的重要位如下:
VSYNC_OEB(bit29):VSYNC 信号方向控制位,为 0 的话 VSYNC 是输出,为 1 的话VSYNC 是输入。
ENABLE_PRESENT(bit28):EBABLE 数据线使能位,也就是 DE 数据线。为 1 的话使能ENABLE 数据线,为 0 的话关闭 ENABLE 数据线。
VSYNC_POL(bit27):VSYNC 数据线极性设置位,为 0 的话 VSYNC 低电平有效,为 1 的话 VSYNC 高电平有效,要根据所使用的 LCD 数据手册来设置。
HSYNC_POL(bit26):HSYNC 数据线极性设置位,为 0 的话 HSYNC 低电平有效,为 1 的话 HSYNC 高电平有效,要根据所使用的 LCD 数据手册来设置。
DOTCLK_POL(bit25):DOTCLK 数据线(像素时钟线 CLK) 极性设置位,为 0 的话下降沿锁存数据,上升沿捕获数据,为 1 的话相反,要根据所使用的 LCD 数据手册来设置。
ENABLE_POL(bit24):EANBLE 数据线极性设置位,为 0 的话低电平有效,为 1 的话高电平有效。
VSYNC_PERIOD_UNIT(bit21):VSYNC 信号周期单位,为 0 的话 VSYNC 周期单位为像素时钟。为 1 的话 VSYNC 周期单位是水平行,如果使用 DOTCLK 模式话就要设置为 1。
VSYNC_PULSE_WIDTH_UNIT(bit20) : VSYNC 信 号 脉 冲 宽 度 单 位 , 和VSYNC_PERIOD_UNUT 一样,如果使用 DOTCLK 模式的话要设置为 1。
VSYNC_PULSE_WIDTH(bit17:0):VSPW 参数设置位。
3.6 LCDIF_VDCTRL1寄存器
接下来看一下寄存器 LCDIF_VDCTRL1,这个寄存器是 VSYNC 和 DOTCLK 模式控制寄存器 1,此寄存器只有一个功能,用来设置 VSYNC 总周期,就是:屏幕高度+VSPW+VBP+VFP。
图 3.6
3.7 LCDIF_VDCTRL2寄存器
接下来看一下寄存器 LCDIF_VDCTRL2,这个寄存器分为高 16 位和低 16 位两部分,高 16位是 HSYNC_PULSE_WIDTH,用来设置 HSYNC 信号宽度,也就是 HSPW。低 16 位是HSYNC_PERIOD,设置 HSYNC 总周期,就是:屏幕宽度+HSPW+HBP+HFP。
图 3.7
3.8 LCDIF_VDCTRL3寄存器
接下来看一下寄存器 LCDIF_VDCTRL3,此寄存器结构如下图所示:
图 3.8
寄存器 LCDIF_VDCTRL3 用到的重要位如下:
HORIZONTAL_WAIT_CNT(bit27:16):此位用于 DOTCLK 模式,用于设置 HSYNC 信号产生到有效数据产生之间的时间,也就是 HSPW+HBP。
VERTICAL_WAIR_CNT(bit15:0):和 HORIZONTAL_WAIT_CNT 一样,只是此位用于VSYNC 信号,也就是 VSPW+VBP。
3.9 LCDIF_VDCTRL4寄存器
接下来看一下寄存器 LCDIF_VDCTRL4,此寄存器结构如下图所示:
寄存器 LCDIF_VDCTRL4 用到的重要位如下:
SYNC_SIGNALS_ON(bit18):同步信号使能位,设置为 1 的话使能 VSYNC、HSYNC、DOTCLK 这些信号。
DOTCLK_H_VALID_DATA_CNT(bit15:0):设置 LCD 的宽度,也就是水平像素数量。最后在看一下寄存器 LCDIF_CUR_BUF 和 LCDIF_NEXT_BUF,这两个寄存器分别为当前帧和下一帧缓冲区,也就是 LCD 显存。一般这两个寄存器保存同一个地址,也就是划分给 LCD的显存首地址。
四 eLCDIF使用总结
关于 eLCDIF 接口的寄存器就介绍到这里,关于这些寄存器详细的描述,请参考《I.MX6ULL参考手册》第 2165 页的 34.6 小节。一般 I.MX6U 的 eLCDIF 接口来驱动屏幕的配置步骤如下:
1、初始化 LCD 所使用的 IO
首先肯定是初始化 LCD 所示使用的 IO,将其复用为 eLCDIF 接口 IO。
2、设置 LCD 的像素时钟
查阅所使用的 LCD 屏幕数据手册,或者自己计算出的时钟像素,然后设置 CCM 相应的寄存器。
3、配置 eLCDIF 接口
设置 LCDIF 的寄存器 CTRL、CTRL1、TRANSFER_COUNT、VDCTRL0~4、CUR_BUF 和NEXT_BUF。根据 LCD 的数据手册设置相应的参数。
4、编写 API 函数
驱动 LCD 屏幕的目的就是显示内容,所以需要编写一些基本的 API 函数,比如画点、画线、画圆函数,字符串显示函数等。
五 LCD背光
不管是使用显示器还是手机,其屏幕背光都是可以调节的,通过调节背光就可以控制屏幕的亮度。在户外阳光强烈的时候可以通过调高背光来看清屏幕,在光线比较暗的地方可以调低背光,防止伤眼睛并且省电。目前市面上的LCD 大都支持背光调节,我们来看下如何调节 LCD 背光。
5.1 LCD 背光调节简介
一般LCD 都有一个背光控制引脚,给这个背光控制引脚输入高电平就会
点亮背光,输入低电平就会关闭背光。假如我们不断的打开和关闭背光,当速度足够快的时候就不会感觉到背光关闭这个过程了。这个正好可以使用 PWM 来完成,PWM 全称是 Pulse Width Modulation,也就是脉冲宽度调制,PWM 信号如下图所示:
图 5.1
PWM 信号有两个关键的术语:频率和占空比,频率就是开关速度,把一次开关算作一个周期,那么频率就是 1 秒内进行了多少次开关。占空比就是一个周期内高电平时间和低电平时间的比例,一个周期内高电平时间越长占空比就越大,反之占空比就越小。占空比用百分之表示,如果一个周期内全是低电平那么占空比就是 0%,如果一个周期内全是高电平那么占空比就是100%。
我们给 LCD 的背光引脚输入一个 PWM 信号,这样就可以通过调整占空比的方式来调整LCD 背光亮度了。提高占空比就会提高背光亮度,降低占空比就会降低背光亮度。重点就在于PWM 信号的产生和占空比的控制,很幸运的是,I.MX6U 提供了 PWM 外设,因此我们可以配置 PWM 外设来产生 PWM 信号。
5.2 I.MX6ULL的PWM
打开《I.MX6ULL 参考手册》的第 40 章“Chapter 40 Pulse Width Modulation(PWM)”,I.MX6U一共有 8 路 PWM 信号,每个 PWM 包含一个 16 位的计数器和一个 4 x 16 的数据 FIFO,I.MX6U的 PWM 外设结构如图 29.1.2 所示:
图 5.2
上图中的各部分功能如下:
①、此部分是一个选择器,用于选择 PWM 信号的时钟源,一共有三种时钟源:ipg_clk、ipg_clk_highfreq 和 ipg_clk_32k。
②、这是一个 12 位的分频器,可以对①中选择的时钟源进行分频。
③、这是 PWM 的 16 位计数器寄存器,保存着 PWM 的计数值。
④、这是 PWM 的 16 位周期寄存器,此寄存器用来控制 PWM 的频率。
⑤、这是 PWM 的 16 位采样寄存器,此寄存器用来控制 PWM 的占空比。
⑥、此部分是 PWM 的中断信号,PWM 是提供中断功能的,如果使能了相应的中断的话就会产生中断。
⑦、此部分是 PWM 对应的输出 IO,产生的 PWM 信号就会从对应的 IO 中输出,I.MX6U 开发板的 LCD 背光控制引脚连接在 I.MX6U 的 GPIO1_IO8 上,GPIO1_IO8 可以复用为 PWM1_OUT。
可以通过配置相应的寄存器来设置 PWM 信号的频率和占空比,PWM 的 16 位计数器是个向上计数器,此计数器会从 0X0000 开始计数,直到计数值等于寄存器 PWMx_PWMPR(x=1~8) + 1,然后计数器就会重新从 0X0000 开始计数,如此往复。所以寄存器 PWMx_PWMPR 可以设置 PWM 的频率。
在一个周期内,PWM 从 0X0000 开始计数的时候,PWM 引脚先输出高电平(默认情况下,可以通过配置输出低电平)。采样 FIFO 中保存的采样值会在每个时钟和计数器值进行比较,当采样值和计数器相等的话 PWM 引脚就会改为输出低电平(默认情况下,同样可以通过配置输出高电平)。计数器会持续计数,直到和周期寄存器 PWMx_PWMPR(x=1~8) + 1 的值相等,这样一个周期就完成了。所以,采样 FIFO 控制着占空比,而采样 FIFO 里面的值来源于采样寄存器PWMx_PWMSAR,因此相当于 PWMx_PWMSAR 控制着占空比。至此,PWM 信号的频率和占空比设置我们就知道该如何去做了。
PWM 开启以后会按照默认值运行,并产生 PWM 波形,而这个默认的 PWM 一般并不是我们需要的波形。如果这个 PWM 波形控制着设备的话就会导致设备因为接收到错误的 PWM 信号而运行错误,严重情况下可能会损坏设备,甚至人身安全。因此,在开启 PWM 之前最好设置好 PWMx_PWMPR 和 PWMx_PWMSAR 这两个寄存器,也就是设置好 PWM 的频率和占空比。
当我们向 PWMx_PWMSAR 寄存器写入采样值的时候,如果 FIFO 没满的话其值会被存储到 FIFO 中。如果 FIFO 满的时候写入采样值就会导致寄存器 PWMx_PWMSR 的位 FWE(bit6)置1,表示 FIFO 写错误,FIFO 里面的值也并不会改变。FIFO 可以在任何时候写入,但是只有在PWM 使能的情况下读取。寄存器 PWMx_SR 的位 FIFOAV(bit2:0)记录着当前 FIFO 中有多少个数据。从采样寄存器 PWMx_PWMSAR 读取一次数据,FIFO 里面的数据就会减一,每产生一个周期的 PWM 信号,FIFO 里面的数据就会减一,相当于被用掉了。PWM 有个 FIFO 空中断,当FIFO 为空的时候就会触发此中断,可以在此中断处理函数中向 FIFO 写入数据。
查阅手册可pwm有关的寄存器目录如下:
5.3 PWM1_PWMCR寄存器
我们使用的是 PWM1,首先看一下寄存器 PWM1_PWMCR 寄存器,此寄存器结构如下图所示:
图 5.3
寄存器 PWM1_PWMCR 用到的重要位如下:
FWM(bit27:26):FIFO 水位线,用来设置 FIFO 空余位置为多少的时候表示 FIFO 为空。设置为 0 的时候表示 FIFO 空余位置大于等于 1 的时候 FIFO 为空;设置为 1 的时候表示 FIFO 空余位置大于等于 2 的时候 FIFO 为空;设置为 2 的时候表示 FIFO 空余位置大于等于 3 的时候
FIFO 为空;设置为 3 的时候表示 FIFO 空余位置大于等于 4 的时候 FIFO 为空。
STOPEN(bit25):此位用来设置停止模式下 PWM 是否工作,为 0 的话表示在停止模式下PWM 继续工作,为 1 的话表示停止模式下关闭 PWM。
DOZEN(bit24):此位用来设置休眠模式下 PWM 是否工作,为 0 的话表示在休眠模式下PWM 继续工作,为 1 的话表示休眠模式下关闭 PWM。
WAITEN(bit23):此位用来设置等待模式下 PWM 是否工作,为 0 的话表示在等待模式下PWM 继续工作,为 1 的话表示等待模式下关闭 PWM。
DEGEN(bit22):此位用来设置调试模式下 PWM 是否工作,为 0 的话表示在调试模式下PWM 继续工作,为 1 的话表示调试模式下关闭 PWM。
BCTR(bit21):字节交换控制位,用来控制 16 位的数据进入 FIFO 的字节顺序。为 0 的时候不进行字节交换,为 1 的时候进行字节交换。
HCRT(bit20):半字交换控制位,用来决定从 32 位 IP 总线接口传输来的哪个半字数据写入采样寄存器的低 16 位中。
POUTC(bit19:18):PWM 输出控制控制位,用来设置 PWM 输出模式,为 0 的时候表示PWM 先输出高电平,当计数器值和采样值相等的话就输出低电平。为 1 的时候相反,当为 2 或者 3 的时候 PWM 信号不输出。本章我们设置为 0,也就是一开始输出高电平,当计数器值和采样值相等的话就改为低电平,这样采样值越大高电平时间就越长,占空比就越大。
CLKSRC(bit17:16):PWM 时钟源选择,为 0 的话关闭;为 1 的话选择 ipg_clk 为时钟源;为 2 的话选择 ipg_clk_highfreq 为时钟源;为 3 的话选择 ipg_clk_32k 为时钟源。本章我们设置为 1,也就是选择 ipg_clk 为 PWM 的时钟源,因此 PWM 时钟源频率为 66MHz。
PRESCALER(bit15:4):分频值,可设置为 0-4095,对应着 1-4096 分频。
SWR(bit3):软件复位,向此位写 1 就复位 PWM,此位是自清零的,当复位完成以后此位会自动清零。
REPEAT(bit2:1):重复采样设置,此位用来设置 FIFO 中的每个数据能用几次。可设置 0~3,分别表示 FIFO 中的每个数据能用 1~4 次。本章我们设置为 0,即 FIFO 中的每个数据只能用一次。
EN(bit0):PWM 使能位,为 1 的时候使能 PWM,为 0 的时候关闭 PWM。
5.4 PWM1_PWMIR寄存器
接下来看一下寄存器 PWM1_PWMIR 寄存器,这个是 PWM 的中断控制寄存器,此寄存器结构如下图所示:
图 5.4
寄存器 PWM1_PWMIR 只有三个位,这三个位的含义如下:
CIE(bit2):比较中断使能位,为 1 的时候使能比较中断,为 0 的时候关闭比较中断。
RIE(bit1):翻转中断使能位,当计数器值等于采样值并回滚到 0X0000 的时候就会产生此中断,为 1 的时候使能翻转中断,为 0 的时候关闭翻转中断。
FIE(bit0):FIFO 空中断,为 1 的时候使能,为 0 的时候关闭。
5.5 PWM1_PWMSR寄存器
再来看一下状态寄存器 PWM1_PWMSR,此寄存器结构如下图所示:
图 5.5
寄存器 PWM1_PWMSR 各个位的含义如下:
FWE(bit6):FIFO 写错误事件,为 1 的时候表示发生了 FIFO 写错误。
CMP(bit5):FIFO 比较事件发标志位,为 1 的时候表示发生 FIFO 比较事件。
ROV(bit4):翻转事件标志位,为 1 的话表示翻转事件发生。
FE(bit3):FIFO 空标志位,为 1 的时候表示 FIFO 位空。
FIFOAV(bit2:1):此位记录 FIFO 中的有效数据个数,有效值为 0-4,分别表示 FIFO 中有0~4 个有效数据。
5.6 PWM1_PWMPR寄存器
接下来是寄存器 PWM1_PWMPR 寄存器,这个是 PWM 周期寄存器,可以通过此寄存器来设置 PWM 的频率,此寄存器结构如下图所示:
图 5.6
从上图可以看出,寄存器 PWM1_PWMPR 只有低 16 位有效,当 PWM 计数器的值等于 PERIOD+1 的时候就会从 0X0000 重新开始计数,开启另一个周期。PWM 的频率计算公式
如下:
PWMO(Hz) = PCLK(Hz) / (PERIOD + 2)
其中 PCLK 是最终进入 PWM 的时钟频率,假如 PCLK 的频率为 1MHz,现在我们要产生一个频率为 1KHz 的 PWM 信号,那么就可以设置 PERIOD = 1000000 / 1000 – 2 = 998。
5.7 PWM1_PWMSAR寄存器
最后来看一下寄存器 PWM1_PWMSAR,这是采样寄存器,用于设置占空比的,此寄存器结构如下图所示:
此寄存器也是只有低 16 位有效,为采样值。通过这个采样值即可调整占空比,当计数器的值小于 SAMPLE 的时候输出高电平(或低电平)。当计数器值大于等于 SAMPLE,小于寄存器PWM1_PWMPR 的 PERIO 的时候输出低电平(或高电平)。同样在上面的例子中,假如我们要设置 PWM 信号的占空比为 50%,那么就可以将 SAMPLE 设置为(PERIOD + 2) / 2 = 1000 / 2=500。
关于 PWM 有关的寄存器就介绍到这里,关于这些寄存器详细的描述,请参考《I.MX6ULL参考手册》第 2480 页的 40.7 小节。
5.8 PWM小节
根据板子连接使用 I.MX6U 的 PWM1,PWM1 的输出引脚为GPIO1_IO8,一般配置步骤如下:
(1) 配置引脚 GPIO1_IO8
配置 GPIO1_IO08 的复用功能,将其复用为 PWM1_OUT 信号线。
(2) 初始化 PWM1
初始化 PWM1,配置所需的 PWM 信号的频率和默认占空比。
(3) 设置中断
因为 FIFO 中的采样值每个周期都会少一个,所以需要不断的向 FIFO 中写入采样值,防止其为空。我们可以使能 FIFO 空中断,这样当 FIFO 为空的时候就会触发相应的中断,然后在中断处理函数中向 FIFO 写入采样值。
(4) 使能 PWM1
配置好 PWM1 以后就可以开启了。
六 Linux 下 LCD 驱动
6.1 linux的fb 设备
前面分析LCD 驱动编写流程如下:
①、初始化 I.MX6U 的 eLCDIF 控制器,重点是 LCD 屏幕宽(width)、高(height)、hspw、hbp、hfp、vspw、vbp 和 vfp 等信息。
②、初始化 LCD 像素时钟。
③、设置 RGBLCD 显存。
④、应用程序直接通过操作显存来操作 LCD,实现在 LCD 上显示字符、图片等信息。
在 Linux 中应用程序最终也是通过操作 RGB LCD 的显存来实现在 LCD 上显示字符、图片等信息。一般在裸机中我们可以随意的分配显存,但是在 Linux 系统中内存的管理很严格,显存是需要申请的,不是你想用就能用的。而且因为虚拟内存的存在,驱动程序设置的显存和应用程序访问的显存要是同一片物理内存。
为了解决上述问题,Framebuffer 诞生了, Framebuffer 翻译过来就是帧缓冲,简称 fb,因此大家在以后的 Linux 学习中见到“Framebuffer”或者“fb”的话第一反应应该想到 RGBLCD或者显示设备。fb 是一种机制,将系统中所有跟显示有关的硬件以及软件集合起来,虚拟出一个 fb 设备,当我们编写好 LCD 驱动以后会生成一个名为/dev/fbX(X=0~n)的设备,应用程序通过访问/dev/fbX 这个设备就可以访问 LCD。NXP 官方的 Linux 内核默认已经开启了 LCD 驱动,因此我们是可以看到/dev/fb0 这样一个设备,如下图所示:
图6.1
上图中的/dev/fb0 就是 LCD 对应的设备文件,/dev/fb0 是个字符设备,因此肯定有file_operations 操作集,fb 的 file_operations 操作集定义在 drivers/video/fbdev/core/fbmem.c 文件中,如下所示:
路径:drivers/video/fbdev/core/fbmem.c
static const struct file_operations fb_fops = {
.owner = THIS_MODULE,
.read = fb_read,
.write = fb_write,
.unlocked_ioctl = fb_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = fb_compat_ioctl,
#endif
.mmap = fb_mmap,
.open = fb_open,
.release = fb_release,
#ifdef HAVE_ARCH_FB_UNMAPPED_AREA
.get_unmapped_area = get_fb_unmapped_area,
#endif
#ifdef CONFIG_FB_DEFERRED_IO
.fsync = fb_deferred_io_fsync,
#endif
.llseek = default_llseek,
};
对于fb相关,不做深入分析,有兴趣的读者可自行研究。
6.2 I.MX6ULL官方源码驱动分析
根据第三、四大节内容可知,LCD 裸机例程主要分两部分:
①、获取 LCD 的屏幕参数。
②、根据屏幕参数信息来初始化 eLCDIF 接口控制器。
不同分辨率的 LCD 屏幕其 eLCDIF 控制器驱动代码都是一样的,只需要修改好对应的屏幕参数即可。屏幕参数信息属于屏幕设备信息内容,这些肯定是要放到设备树中的,因此我们本节实验的主要工作就是修改设备树,NXP 官方的设备树已经添加了 LCD 设备节点,只是此节点的 LCD 屏幕信息是针对 NXP 官方 EVK 开发板所使用的 4.3 寸 480*272 编写的,我们需要将其改为我们所使用的屏幕参数。
6.2.1 NXP官方LCD设备树节点
们简单看一下 NXP 官方编写的 Linux 下的 LCD 驱动,打开 imx6ull.dtsi,然后找到 lcdif节点内容,如下所示:
路径:arch/arm/boot/dts/imx6ull.dtsi
lcdif: lcdif@021c8000 {
compatible = "fsl,imx6ul-lcdif", "fsl,imx28-lcdif";
reg = <0x021c8000 0x4000>;
interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
<&clks IMX6UL_CLK_LCDIF_APB>,
<&clks IMX6UL_CLK_DUMMY>;
clock-names = "pix", "axi", "disp_axi";
status = "disabled";
};
lcdif 节点信息是所有使用 I.MX6ULL 芯片的板子所共有的,并不是完整的 lcdif 节点信息。像屏幕参数这些需要根据不同的硬件平台去添加,比如向imx6ull-water-emmc.dts 中的 lcdif 节点添加其他的属性信息。
从示例代码 可以看出 lcdif 节点的 compatible 属性值为“fsl,imx6ul-lcdif”和“fsl,imx28-lcdif”,因此在 Linux 源码中搜索这两个字符串即可找到 I.MX6ULL 的 LCD 驱动文件。
6.2.2 NXP官方LCD驱动
根据设备树compatible 属性值搜索到I.MX6ULL 的 LCD 驱动文件,在此文件中找到如下内容:
路径:drivers/video/fbdev/mxsfb.c
tatic const struct of_device_id mxsfb_dt_ids[] = {
{ .compatible = "fsl,imx23-lcdif", .data = &mxsfb_devtype[0], },
{ .compatible = "fsl,imx28-lcdif", .data = &mxsfb_devtype[1], },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mxsfb_dt_ids);
.....
static struct platform_driver mxsfb_driver = {
.probe = mxsfb_probe,
.remove = mxsfb_remove,
.shutdown = mxsfb_shutdown,
.id_table = mxsfb_devtype,
.driver = {
.name = DRIVER_NAME,
.of_match_table = mxsfb_dt_ids,
.pm = &mxsfb_pm_ops,
},
};
module_platform_driver(mxsfb_driver);
从上述代码 可以看出,这是一个标准的 platform 驱动,当驱动和设备匹配以后mxsfb_probe 函数就会执行。在看 mxsfb_probe 函数之前我们先简单了解一下 Linux 下Framebuffer 驱动的编写流程,Linux 内核将所有的 Framebuffer 抽象为一个叫做 fb_info 的结构体,fb_info 结构体包含了 Framebuffer 设备的完整属性和操作集合,因此每一个 Framebuffer 设备都必须有一个 fb_info。换言之就是,LCD 的驱动就是构建 fb_info,并且向系统注册 fb_info的过程。fb_info 结构体定义在 include/linux/fb.h 文件里面,内容如下:
路径:include/linux/fb.h
struct fb_info {
atomic_t count;
int node;
int flags;
struct mutex lock; /* Lock for open/release/ioctl funcs */ /* 互斥锁 */
struct mutex mm_lock; /* Lock for fb_mmap and smem_* fields *//* 互斥锁,用于 fb_mmap 和 smem_*域*/
struct fb_var_screeninfo var; /* Current var *//* 当前可变参数 */
struct fb_fix_screeninfo fix; /* Current fix */ /* 当前固定参数 */
struct fb_monspecs monspecs; /* Current Monitor specs */ /* 当前显示器特性 */
struct work_struct queue; /* Framebuffer event queue */ /* 帧缓冲事件队列 */
struct fb_pixmap pixmap; /* Image hardware mapper */ /* 图像硬件映射 */
struct fb_pixmap sprite; /* Cursor hardware mapper */ /* 光标硬件映射 */
struct fb_cmap cmap; /* Current cmap */ /* 当前调色板 */
struct list_head modelist; /* mode list */ /* 当前模式列表 */
struct fb_videomode *mode; /* current mode */ /* 当前视频模式 */
#ifdef CONFIG_FB_BACKLIGHT /* 如果 LCD 支持背光的话 */
/* assigned backlight device */
/* set before framebuffer registration,
remove after unregister */
struct backlight_device *bl_dev; /* 背光设备 */
/* Backlight level curve */
struct mutex bl_curve_mutex;
u8 bl_curve[FB_BACKLIGHT_LEVELS];
#endif
#ifdef CONFIG_FB_DEFERRED_IO
struct delayed_work deferred_work;
struct fb_deferred_io *fbdefio;
#endif
struct fb_ops *fbops; /* 帧缓冲操作函数集 */
struct device *device; /* This is the parent */ /* 父设备 */
struct device *dev; /* This is this fb device */ /* 当前 fb 设备 */
int class_flag; /* private sysfs flags */ /* 私有 sysfs 标志 */
#ifdef CONFIG_FB_TILEBLITTING
struct fb_tile_ops *tileops; /* Tile Blitting */
#endif
char __iomem *screen_base; /* Virtual address */ /* 虚拟内存基地址(屏幕显存) */
unsigned long screen_size; /* Amount of ioremapped VRAM or 0 */ /* 虚拟内存大小(屏幕显存大小) */
void *pseudo_palette; /* Fake palette of 16 colors */ /* 伪 16 位调色板 */
#define FBINFO_STATE_RUNNING 0
#define FBINFO_STATE_SUSPENDED 1
u32 state; /* Hardware state i.e suspend */
void *fbcon_par; /* fbcon use-only private area */
/* From here on everything is device dependent */
void *par;
/* we need the PCI or similar aperture base/size not
smem_start/size as smem_start may just be an object
allocated inside the aperture so may not actually overlap */
struct apertures_struct {
unsigned int count;
struct aperture {
resource_size_t base;
resource_size_t size;
} ranges[0];
} *apertures;
bool skip_vt_switch; /* no VT switch on suspend/resume required */
};
fb_info 结构体的成员变量很多,我们重点关注 var、fix、fbops、screen_base、screen_size和 pseudo_palette。mxsfb_probe 函数的主要工作内容为:
①、申请 fb_info。
②、初始化 fb_info 结构体中的各个成员变量。
③、初始化 eLCDIF 控制器。
④、使用 register_framebuffer 函数向 Linux 内核注册初始化好的 fb_info。
register_framebuffer函数原型如下:
int register_framebuffer(struct fb_info *fb_info)
函数参数和返回值含义如下:
fb_info:需要上报的 fb_info。
返回值:0,成功;负值,失败。
6.2.3 NXP官方LCD驱动xxx_probe函数
简单看一下 mxsfb_probe 函数,函数内容如下:
路径:drivers/video/fbdev/mxsfb.c
static int mxsfb_probe(struct platform_device *pdev)
{
const struct of_device_id *of_id =
of_match_device(mxsfb_dt_ids, &pdev->dev);
struct resource *res;
struct mxsfb_info *host;
struct fb_info *fb_info;
struct pinctrl *pinctrl;
int irq = platform_get_irq(pdev, 0);
int gpio, ret;
if (of_id)
pdev->id_entry = of_id->data;
gpio = of_get_named_gpio(pdev->dev.of_node, "enable-gpio", 0);
if (gpio == -EPROBE_DEFER)
return -EPROBE_DEFER;
if (gpio_is_valid(gpio)) {
ret = devm_gpio_request_one(&pdev->dev, gpio, GPIOF_OUT_INIT_LOW, "lcd_pwr_en");
if (ret) {
dev_err(&pdev->dev, "faild to request gpio %d, ret = %d\n", gpio, ret);
return ret;
}
}
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "Cannot get memory IO resource\n");
return -ENODEV;
}
host = devm_kzalloc(&pdev->dev, sizeof(struct mxsfb_info), GFP_KERNEL);
if (!host) {
dev_err(&pdev->dev, "Failed to allocate IO resource\n");
return -ENOMEM;
}
fb_info = framebuffer_alloc(sizeof(struct fb_info), &pdev->dev);
if (!fb_info) {
dev_err(&pdev->dev, "Failed to allocate fbdev\n");
devm_kfree(&pdev->dev, host);
return -ENOMEM;
}
host->fb_info = fb_info;
fb_info->par = host;
ret = devm_request_irq(&pdev->dev, irq, mxsfb_irq_handler, 0,
dev_name(&pdev->dev), host);
if (ret) {
dev_err(&pdev->dev, "request_irq (%d) failed with error %d\n",
irq, ret);
ret = -ENODEV;
goto fb_release;
}
host->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(host->base)) {
dev_err(&pdev->dev, "ioremap failed\n");
ret = PTR_ERR(host->base);
goto fb_release;
}
host->pdev = pdev;
platform_set_drvdata(pdev, host);
host->devdata = &mxsfb_devdata[pdev->id_entry->driver_data];
host->clk_pix = devm_clk_get(&host->pdev->dev, "pix");
if (IS_ERR(host->clk_pix)) {
host->clk_pix = NULL;
ret = PTR_ERR(host->clk_pix);
goto fb_release;
}
host->clk_axi = devm_clk_get(&host->pdev->dev, "axi");
if (IS_ERR(host->clk_axi)) {
host->clk_axi = NULL;
ret = PTR_ERR(host->clk_axi);
goto fb_release;
}
host->clk_disp_axi = devm_clk_get(&host->pdev->dev, "disp_axi");
if (IS_ERR(host->clk_disp_axi)) {
host->clk_disp_axi = NULL;
ret = PTR_ERR(host->clk_disp_axi);
goto fb_release;
}
host->reg_lcd = devm_regulator_get(&pdev->dev, "lcd");
if (IS_ERR(host->reg_lcd))
host->reg_lcd = NULL;
fb_info->pseudo_palette = devm_kzalloc(&pdev->dev, sizeof(u32) * 16,
GFP_KERNEL);
if (!fb_info->pseudo_palette) {
ret = -ENOMEM;
goto fb_release;
}
INIT_LIST_HEAD(&fb_info->modelist);
pm_runtime_enable(&host->pdev->dev);
ret = mxsfb_init_fbinfo(host);
if (ret != 0)
goto fb_pm_runtime_disable;
mxsfb_dispdrv_init(pdev, fb_info);
if (!host->dispdrv) {
pinctrl = devm_pinctrl_get_select_default(&pdev->dev);
if (IS_ERR(pinctrl)) {
ret = PTR_ERR(pinctrl);
goto fb_pm_runtime_disable;
}
}
if (!host->enabled) {
writel(0, host->base + LCDC_CTRL);
mxsfb_set_par(fb_info);
mxsfb_enable_controller(fb_info);
pm_runtime_get_sync(&host->pdev->dev);
}
ret = register_framebuffer(fb_info);
if (ret != 0) {
dev_err(&pdev->dev, "Failed to register framebuffer\n");
goto fb_destroy;
}
console_lock();
ret = fb_blank(fb_info, FB_BLANK_UNBLANK);
console_unlock();
if (ret < 0) {
dev_err(&pdev->dev, "Failed to unblank framebuffer\n");
goto fb_unregister;
}
dev_info(&pdev->dev, "initialized\n");
return 0;
fb_unregister:
unregister_framebuffer(fb_info);
fb_destroy:
if (host->enabled)
clk_disable_unprepare(host->clk_pix);
fb_destroy_modelist(&fb_info->modelist);
fb_pm_runtime_disable:
pm_runtime_disable(&host->pdev->dev);
devm_kfree(&pdev->dev, fb_info->pseudo_palette);
fb_release:
framebuffer_release(fb_info);
devm_kfree(&pdev->dev, host);
return ret;
}
第 7行,host 结构体指针变量,表示 I.MX6ULL 的 LCD 的主控接口,mxsfb_info 结构体是 NXP 定义的针对 I.MX 系列 SOC 的 Framebuffer 设备结构体。也就是我们前面一直说的设备结构体,此结构体包含了 I.MX 系列 SOC 的 Framebuffer 设备详细信息,比如时钟、eLCDIF控制器寄存器基地址、fb_info 等。
第 28行,从设备树中获取 eLCDIF 接口控制器的寄存器首地址,设备树中 lcdif 节点已经设置了 eLCDIF 寄存器首地址为 0X021C8000,因此 res=0X021C8000。
第 34行,给 host 申请内存,host 为 mxsfb_info 类型结构体指针。
第 40行,给 fb_info 申请内存,也就是申请 fb_info。
第 46~47行,设置 host 的 fb_info 成员变量为 fb_info,设置 fb_info 的 par 成员变量为host。通过这一步就将前面申请的 host 和 fb_info 联系在了一起。
第 49行,申请中断,中断服务函数为 mxsfb_irq_handler。
第 58行,对从设备树中获取到的寄存器首地址(res)进行内存映射,得到虚拟地址,并保存到 host 的 base 成员变量。因此通过访问 host 的 base 成员即可访问 I.MX6ULL 的整个 eLCDIF寄存器。其实在 mxsfb.c 中已经定义了 eLCDIF 各个寄存器相比于基地址的偏移值,如下所示:
路径:drivers/video/fbdev/mxsfb.c
#define LCDC_CTRL 0x00
#define LCDC_CTRL1 0x10
#define LCDC_V4_CTRL2 0x20
#define LCDC_V3_TRANSFER_COUNT 0x20
#define LCDC_V4_TRANSFER_COUNT 0x30
#define LCDC_V4_CUR_BUF 0x40
#define LCDC_V4_NEXT_BUF 0x50
#define LCDC_V3_CUR_BUF 0x30
#define LCDC_V3_NEXT_BUF 0x40
#define LCDC_TIMING 0x60
#define LCDC_VDCTRL0 0x70
#define LCDC_VDCTRL1 0x80
#define LCDC_VDCTRL2 0x90
#define LCDC_VDCTRL3 0xa0
#define LCDC_VDCTRL4 0xb0
#define LCDC_DVICTRL0 0xc0
#define LCDC_DVICTRL1 0xd0
#define LCDC_DVICTRL2 0xe0
#define LCDC_DVICTRL3 0xf0
#define LCDC_DVICTRL4 0x100
#define LCDC_V4_DATA 0x180
#define LCDC_V3_DATA 0x1b0
#define LCDC_V4_DEBUG0 0x1d0
#define LCDC_V3_DEBUG0 0x1f0
大家可以对比着《I.MX6ULL 参考手册》中的 eLCDIF 章节检查一下代码中的这些寄存器有没有错误。
继续回到mxsfb_probe 函数
第95行,给 fb_info中的 pseudo_palette申请内存。
第 106行,调用 mxsfb_init_fbinfo 函数初始化 fb_info,重点是 fb_info 的 var、fix、fbops,screen_base 和 screen_size。其中 fbops 是 Framebuffer 设备的操作集,NXP 提供的 fbops 为mxsfb_ops,内容如下:
路径:drivers/video/fbdev/mxsfb.c
static struct fb_ops mxsfb_ops = {
.owner = THIS_MODULE,
.fb_check_var = mxsfb_check_var,
.fb_set_par = mxsfb_set_par,
.fb_setcolreg = mxsfb_setcolreg,
.fb_ioctl = mxsfb_ioctl,
.fb_blank = mxsfb_blank,
.fb_pan_display = mxsfb_pan_display,
.fb_mmap = mxsfb_mmap,
.fb_fillrect = cfb_fillrect,
.fb_copyarea = cfb_copyarea,
.fb_imageblit = cfb_imageblit,
};
mxsfb_init_fbinfo 函数通过调用 mxsfb_init_fbinfo_dt 函数从设备树中获取到 LCD 的各个参数信息。最后,mxsfb_init_fbinfo函数会调用 mxsfb_map_videomem 函数申请 LCD 的帧缓冲内存(也就是显存)。
第 122~123行,设置 eLCDIF 控制器的相应寄存器。
第 127行,最后调用 register_framebuffer 函数向 Linux 内核注册 fb_info。
mxsfb.c 文件很大,还有一些其他的重要函数,比如 mxsfb_remove、mxsfb_shutdown 等,这里我们就简单的介绍了一下 mxsfb_probe 函数,至于其他的函数大家自行查阅。
七 LCD驱动适配
前面已经说了,6ULL 的 eLCDIF 接口驱动程序 NXP 已经编写好了,因此 LCD 驱动部分不需要去修改。我们需要做的就是按照所使用的 LCD 来修改设备树。重点要注意三个地方:
①、LCD 所使用的 IO 配置。
②、LCD 屏幕节点修改,修改相应的属性值,换成我们所使用的 LCD 屏幕参数。
③、LCD 背光节点信息修改,要根据实际所使用的背光 IO 来修改相应的设备节点信息。
接下来我们依次来看一下上面这两个节点改如何去修改:
7.1 LCD 屏幕 IO 配置
首先要检查一下设备树中 LCD 所使用的 IO 配置,这个其实 NXP 都已经给我们写好了,不需要修改,不过我们还是要看一下。打开 imx6ull-water-emmc.dts 文件,在 iomuxc 节点中找到如下内容:
路径:arch/arm/boot/dts/imx6ull-water-emmc.dts
pinctrl_lcdif_dat: lcdifdatgrp {
fsl,pins = <
MX6UL_PAD_LCD_DATA00__LCDIF_DATA00 0x79
MX6UL_PAD_LCD_DATA01__LCDIF_DATA01 0x79
MX6UL_PAD_LCD_DATA02__LCDIF_DATA02 0x79
MX6UL_PAD_LCD_DATA03__LCDIF_DATA03 0x79
MX6UL_PAD_LCD_DATA04__LCDIF_DATA04 0x79
MX6UL_PAD_LCD_DATA05__LCDIF_DATA05 0x79
MX6UL_PAD_LCD_DATA06__LCDIF_DATA06 0x79
MX6UL_PAD_LCD_DATA07__LCDIF_DATA07 0x79
MX6UL_PAD_LCD_DATA08__LCDIF_DATA08 0x79
MX6UL_PAD_LCD_DATA09__LCDIF_DATA09 0x79
MX6UL_PAD_LCD_DATA10__LCDIF_DATA10 0x79
MX6UL_PAD_LCD_DATA11__LCDIF_DATA11 0x79
MX6UL_PAD_LCD_DATA12__LCDIF_DATA12 0x79
MX6UL_PAD_LCD_DATA13__LCDIF_DATA13 0x79
MX6UL_PAD_LCD_DATA14__LCDIF_DATA14 0x79
MX6UL_PAD_LCD_DATA15__LCDIF_DATA15 0x79
MX6UL_PAD_LCD_DATA16__LCDIF_DATA16 0x79
MX6UL_PAD_LCD_DATA17__LCDIF_DATA17 0x79
MX6UL_PAD_LCD_DATA18__LCDIF_DATA18 0x79
MX6UL_PAD_LCD_DATA19__LCDIF_DATA19 0x79
MX6UL_PAD_LCD_DATA20__LCDIF_DATA20 0x79
MX6UL_PAD_LCD_DATA21__LCDIF_DATA21 0x79
MX6UL_PAD_LCD_DATA22__LCDIF_DATA22 0x79
MX6UL_PAD_LCD_DATA23__LCDIF_DATA23 0x79
>;
};
pinctrl_lcdif_ctrl: lcdifctrlgrp {
fsl,pins = <
MX6UL_PAD_LCD_CLK__LCDIF_CLK 0x79
MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE 0x79
MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC 0x79
MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC 0x79
>;
};
pinctrl_pwm1: pwm1grp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO08__PWM1_OUT 0x110b0
>;
};
第 3~28 行,子节点 pinctrl_lcdif_dat,为 RGB LCD 的 24 根数据线配置项。
第 31~37 行,子节点 pinctrl_lcdif_ctrl,RGB LCD 的 4 根控制线配置项,包括 CLK、ENABLE、VSYNC 和 HSYNC。
第 40~44 行,子节点 pinctrl_pwm1,LCD 背光 PWM 引脚配置项。这个引脚要根据实际情况设置,这里我们建议大家在以后的学习或工作中,LCD 的背光 IO 尽量和半导体厂商的官方开发板一致。
注意上述代码中默认将 LCD 的电气属性都设置为 0X79,这里将其都改为 0X49,也就是将 LCD 相关 IO 的驱动能力改为 R0/1,也就是降低 LCD 相关 IO 的驱动能力。因为前面已经说了,笔者所用 开发板上的 LCD 接口用了三个 SGM3157 模拟开关,为了防止模拟开关影响到网络,因此这里需要降低 LCD 数据线的驱动能力,如果你所使用的板子没有用到模拟开关那么就不需要将 0X79 改为 0X49。
修改后的设备树如下:
pinctrl_lcdif_dat: lcdifdatgrp {
fsl,pins = <
MX6UL_PAD_LCD_DATA00__LCDIF_DATA00 0x49
MX6UL_PAD_LCD_DATA01__LCDIF_DATA01 0x49
MX6UL_PAD_LCD_DATA02__LCDIF_DATA02 0x49
MX6UL_PAD_LCD_DATA03__LCDIF_DATA03 0x49
MX6UL_PAD_LCD_DATA04__LCDIF_DATA04 0x49
MX6UL_PAD_LCD_DATA05__LCDIF_DATA05 0x49
MX6UL_PAD_LCD_DATA06__LCDIF_DATA06 0x49
MX6UL_PAD_LCD_DATA07__LCDIF_DATA07 0x49
MX6UL_PAD_LCD_DATA08__LCDIF_DATA08 0x49
MX6UL_PAD_LCD_DATA09__LCDIF_DATA09 0x49
MX6UL_PAD_LCD_DATA10__LCDIF_DATA10 0x49
MX6UL_PAD_LCD_DATA11__LCDIF_DATA11 0x49
MX6UL_PAD_LCD_DATA12__LCDIF_DATA12 0x49
MX6UL_PAD_LCD_DATA13__LCDIF_DATA13 0x49
MX6UL_PAD_LCD_DATA14__LCDIF_DATA14 0x49
MX6UL_PAD_LCD_DATA15__LCDIF_DATA15 0x49
MX6UL_PAD_LCD_DATA16__LCDIF_DATA16 0x49
MX6UL_PAD_LCD_DATA17__LCDIF_DATA17 0x49
MX6UL_PAD_LCD_DATA18__LCDIF_DATA18 0x49
MX6UL_PAD_LCD_DATA19__LCDIF_DATA19 0x49
MX6UL_PAD_LCD_DATA20__LCDIF_DATA20 0x49
MX6UL_PAD_LCD_DATA21__LCDIF_DATA21 0x49
MX6UL_PAD_LCD_DATA22__LCDIF_DATA22 0x49
MX6UL_PAD_LCD_DATA23__LCDIF_DATA23 0x49
>;
};
pinctrl_lcdif_ctrl: lcdifctrlgrp {
fsl,pins = <
MX6UL_PAD_LCD_CLK__LCDIF_CLK 0x49
MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE 0x49
MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC 0x49
MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC 0x49
>;
};
pinctrl_pwm1: pwm1grp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO08__PWM1_OUT 0x110b0
>;
};
7.2 LCD 屏幕参数节点信息修改
继续在imx6ull-water-emmc.dts 文件中找到 lcdif 节点,节点内容如下所示:
&lcdif {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_lcdif_dat /* 使用到的 IO */
&pinctrl_lcdif_ctrl
&pinctrl_lcdif_reset>;
display = <&display0>;
status = "okay";
display0: display { /* LCD 属性信息 */
bits-per-pixel = <16>; /* 一个像素占用几个 bit */
bus-width = <24>; /* 总线宽度 */
display-timings { /* 时序信息 */
native-mode = <&timing0>;
timing0: timing0 {
clock-frequency = <9200000>; /* LCD 像素时钟,单位 Hz */
hactive = <480>; /* LCD X 轴像素个数 */
vactive = <272>; /* LCD Y 轴像素个数 */
hfront-porch = <8>; /* LCD hfp 参数 */
hback-porch = <4>; /* LCD hbp 参数 */
hsync-len = <41>; /* LCD hspw 参数 */
vback-porch = <2>; /* LCD vbp 参数 */
vfront-porch = <4>; /* LCD vfp 参数 */
vsync-len = <10>; /* LCD vspw 参数 */
hsync-active = <0>; /* hsync 数据线极性 */
vsync-active = <0>; /* vsync 数据线极性 */
de-active = <1>; /* de 数据线极性 */
pixelclk-active = <0>; /* clk 数据线先极性 */
};
};
};
};
上述就是向 imx6ull.dtsi 文件中的 lcdif 节点追加的内容,我们依次来看一下上述代码中的这些属性都是些什么含义。
第 3 行,pinctrl-0 属性,LCD 所使用的 IO 信息,这里用到了 pinctrl_lcdif_dat、pinctrl_lcdif_ctrl和 pinctrl_lcdif_reset 这三个 IO 相关的节点,前两个在7.1小节中已经讲解了。pinctrl_lcdif_reset 是 LCD 复位 IO 信息节点, 作者用的I.MX6U 开发板的 LCD 没有用到复位 IO,因此 pinctrl_lcdif_reset 可以删除掉。
第 6 行,display 属性,指定 LCD 属性信息所在的子节点,这里为 display0,下面就是 display0子节点内容。
第 9~32 行,display0 子节点,描述 LCD 的参数信息,第 10 行的 bits-per-pixel 属性用于指明一个像素占用的 bit 数,默认为 16bit。本教程我们将 LCD 配置为 RGB888 模式,因此一个像素点占用 24bit,bits-per-pixel 属性要改为 24。第 11 行的 bus-width 属性用于设置数据线宽度,因为要配置为 RGB888 模式,因此 bus-width 也要设置为 24。
第 13~30 行,这几行非常重要!因为这几行设置了 LCD 的时序参数信息,NXP 官方的 EVK开发板使用了一个 4.3 寸的 480*272 屏幕,因此这里默认是按照 NXP 官方的那个屏幕参数设置的。每一个属性的含义后面的注释已经写的很详细了,大家自己去看就行了,这些时序参数就是我们重点要修改的,需要根据自己所使用的屏幕去修改,如2.6节分析的参数值。
修改lcdif 节点后如下:
&lcdif {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_lcdif_dat
&pinctrl_lcdif_ctrl>;
display = <&display0>;
status = "okay";
display0: display {
bits-per-pixel = <24>;
bus-width = <24>;
display-timings {
native-mode = <&timing0>;
timing0: timing0 {
clock-frequency = <51200000>;
hactive = <1024>;
vactive = <600>;
hfront-porch = <160>;
hback-porch = <140>;
hsync-len = <20>;
vback-porch = <20>;
vfront-porch = <12>;
vsync-len = <3>;
hsync-active = <0>;
vsync-active = <0>;
de-active = <1>;
pixelclk-active = <0>;
};
};
};
};
第 3 行,设置 LCD 屏幕所使用的 IO,删除掉原来的 pinctrl_lcdif_reset,因为没有用到屏
幕复位 IO,其他的 IO 不变。
第 9 行,使用 RGB888 模式,所以一个像素点是 24bit。
第 15~23 行,ATK7016 屏幕时序参数,根据自己所使用的屏幕修改即可(笔者这里参考2.4小节所分析的参数设定)。
7.3 LCD 屏幕背光节点信息
作者的LCD 接口背光控制 IO 连接到了 I.MX6U 的 GPIO1_IO08 引脚上,GPIO1_IO08复用为 PWM1_OUT,通过 PWM 信号来控制 LCD 屏幕背光的亮度,这个我们已经在第五节分析过了。作者的I.MX6U-开发板的 LCD 背光引脚和 NXP 官方 EVK 开发板的背光引脚一样,因此背光的设备树节点是不需要修改的,但是考虑到读者可能使用别的开发板或者屏幕,LCD 背光引脚和 NXP 官方 EVK 开发板可能不同,因此我们还是来看一下如何在设备树中添加背光节点信息。
首先是 GPIO1_IO08 这个 IO 的配置,在imx6ull-water-emmc.dts 中找到如下内容:
pinctrl_pwm1: pwm1grp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO08__PWM1_OUT 0x110b0
>;
};
pinctrl_pwm1 节点就是 GPIO1_IO08 的配置节点,从第 3 行可以看出,设置 GPIO1_IO08这个 IO 复用为 PWM1_OUT,并且设置电气属性值为 0x110b0。LCD 背光要用到 PWM1,因此也要设置 PWM1 节点,在 imx6ull.dtsi 文件中找到如下内容:
路径:arch/arm/boot/dts/imx6ull.dtsi
pwm1: pwm@02080000 {
compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm";
reg = <0x02080000 0x4000>;
interrupts = <GIC_SPI 83 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_PWM1>,
<&clks IMX6UL_CLK_PWM1>;
clock-names = "ipg", "per";
#pwm-cells = <2>;
};
imx6ull.dtsi 文件中的 pwm1 节点信息大家不要修改,如果要修改 pwm1 节点内容的话请在imx6ull-water-emmc.dts 文件中修改。在整个 Linux 源码文件中搜索 compatible 属性的这两个值即可找到 imx6ull 的 pwm 驱动文件,imx6ull 的 PWM 驱动文件为 drivers/pwm/pwm-imx.c
,这里我们就不详细的去分析这个文件了。继续在imx6ull-water-emmc.dts 文件中找到向 pwm1追加的内容,如下所示:
&pwm1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_pwm1>;
status = "okay";
};
第 3 行,设置 pwm1 所使用的 IO 为 pinctrl_pwm1,也就是pinctrl定义的GPIO1_IO08 这个 IO。
第 4 行,将 status 设置为 okay。
如果背光用的其他 pwm 通道,比如 pwm2,那么就需要仿照上述pinctrl代码的内容,向pwm2 节点追加相应的内容。pwm 和相关的 IO 已经准备好了,但是 Linux 系统怎么知道PWM1_OUT 就是控制 LCD背光的呢?因此我们还需要一个节点来将 LCD 背光和 PWM1_OUT连接起来。
这个节点就是 backlight , backlight 节点描述可以参考
Documentation/devicetree/indings/video/backlight/pwm-backlight.txt 这个文档,此文档详细讲解了backlight 节点该如何去创建,这里大概总结一下:
①、节点名称要为“backlight”。
②、节点的 compatible 属性值要为“pwm-backlight”,因此可以通过在 Linux 内核中搜索“ pwm-backlight ” 来 查 找 PWM 背 光 控 制 驱 动 程 序 , 这 个 驱 动 程 序 文 件 为drivers/video/backlight/pwm_bl.c,感兴趣的可以去看一下这个驱动程序。
③、pwms属性用于描述背光所使用的PWM以及PWM频率,比如本章我们要使用的pwm1,pwm 频率设置为 200Hz(NXP 官方推荐设置)。
④、brightness-levels 属性描述亮度级别,范围为 0~255,0 表示 PWM 占空比为 0%,也就是亮度最低,255 表示 100%占空比,也就是亮度最高。至于设置几级亮度,大家可以自行填写此属性。
⑤、default-brightness-level 属性为默认亮度级别。
根据上述 5 点设置 backlight 节点,这个 NXP 已经给我们设置好了,大家在 imx6ull-water-emmc.dts 文件中找到如下内容:
backlight {
compatible = "pwm-backlight";
pwms = <&pwm1 0 5000000>;
brightness-levels = <0 4 8 16 32 64 128 255>;
default-brightness-level = <6>;
status = "okay";
};
第 3 行,设置背光使用 pwm1,PWM 频率为 200Hz。
第 4 行,设置背 8 级背光(0~7),分别为 0、4、8、16、32、64、128、255,对应占空比为0%、1.57%、3.13%、6.27%、12.55%、25.1%、50.19%、100%,如果嫌少的话可以自行添加一些其他的背光等级值。
第 5 行,设置默认背光等级为 6,也就是 50.19%的亮度。
关于背光的设备树节点信息就讲到这里,整个的 LCD 设备树节点内容我们就讲完了,按照这些节点内容配置自己的开发板即可。
八 LCD驱动适配验证
8.1 编译DTS/kernel
根据第七节的修改后重新编译DTS,用新编译的DTB文件启动。
linux内核有一个自带的开机Logo,为了验证LCD是否正常显示,我们开启自带的Logo,配置如下:
上图中这三个选项分别对应黑白、16 位、24 位色彩格式的 logo,我们把这三个都选中,都编译进 Linux 内核里面。设置好以后保存退出,重新编译 Linux 内核,编译完成以后使用新编译出来的 imx6ull-water-emmc.dtb 和 zImage 镜像启动系统,如果 LCD 驱动工作正常的话就会在 LCD 屏幕左上角出现一个彩色的小企鹅 logo,屏幕背景色为黑色,如下图所示:
8.2 设置 LCD 作为终端控制台
我们一直使用SecureCRT作为Linux开发板终端,开发板通过串口和 SecureCRT进行通信。现在我们已经驱动起来 LCD 了,所以可以设置 LCD 作为终端,也就是开发板使用自己的显示设备作为自己的终端,然后接上键盘就可以直接在开发板上敲命令了,将 LCD 设置为终端控制台的方法如下:
(1)设置 uboot 中的 bootargs
重启开发板,进入 Linux 命令行,重新设置 bootargs 参数的 console 内容,命令如下所示:
setenv bootargs 'console=tty1 console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.0.116:/home/water/imax/nfs/buildrootfs,v3 proto=tcp rw ip=192.168.0.117:192.168.0.116:192.168.0.1:255.255.255.0::eth0:off'
saveenv //保存环境变量
这里我们设置了两遍 console,第一次设置 console=tty1,也就是设置 LCD 屏幕为控制台,第二遍又设置console=ttymxc0,115200,也就是设置串口也作为控制台。相当于我们打开了两个 console,一个是 LCD,一个是串口,大家重启开发板就会发现 LCD 和串口都会显示 Linux 启动 log 信息。但是此时我们还不能使用 LCD 作为终端进行交互,因为我们的设置还未完成。
(2)修改/etc/inittab 文件
打开开发板根文件系统中的/etc/inittab 文件,在里面加入下面这一行:
tty1::respawn:/sbin/getty -L tty1 0 vt100 # GENERIC_SERIAL
可能有的教程是加tty1::askfirst:-/bin/sh
,其实加入的内容与console保持一直即可。
重启设备后如下:
前面学习<Linux开发>驱动开发 -之-Linux INPUT 子系统时已经将开发板上的key0设置会回车键, 所以按下key0可看到LCD显示屏有换行的变化。
也可以接上一个 USB 键盘,Linux 内核默认已经使能了 USB 键盘驱动
了,因此可以直接使用 USB 键盘。
8.3 LCD 背光调节
背光设备树节点设置了 8 个等级的背光调节,可以设置为 0~7,可以通过设置背光等级来实现 LCD 背光亮度的调节,进入如下目录:
/sys/devices/platform/backlight/backlight/backlight
当前屏幕亮度等级为 6,根据前面的分析可以,这个是 50%亮度。屏幕最大亮度等级为 7。如果我们要修改屏幕亮度,只需要向 brightness 写入需要设置的屏幕亮度等级即可。比如设置屏幕亮度等级为 7,那么可以使用如下命令:
echo 7 > brightness
输入上述命令以后就会发现屏幕亮度增大了,如果设置 brightness 为 0 的话就会关闭 LCD背光,屏幕就会熄灭。
8.4 LCD 自动关闭解决方法
默认情况下 10 分钟以后 LCD 就会熄屏,这个并不是代码有问题,而是 Linux 内核设置的,就和我们用手机或者电脑一样,一段时间不操作的话屏幕就会熄灭,以节省电能。解决这个问题有多种方法,我们依次来看一下:
(1)键盘唤醒
按下开发板上的 KEY 按键即可唤醒屏幕。如果开发板上没有按键的话可以外接 USB 键盘,然后按下 USB 键盘上的回车键唤醒屏幕。
(2)关闭 10 分钟熄屏功能
在 Linux 源码中找到 drivers/tty/vt/vt.c 这个文件,在此文件中找到 blankinterval 变量,如下所示:
路径:drivers/tty/vt/vt.c
static int vesa_blank_mode; /* 0:none 1:suspendV 2:suspendH 3:powerdown */
static int vesa_off_interval;
static int blankinterval = 10*60;
core_param(consoleblank, blankinterval, int, 0444);
blankinterval 变量控制着 LCD 关闭时间,默认是 10*60,也就是 10 分钟。将 blankinterval的值改为 0 即可关闭 10 分钟熄屏的功能,修改完成以后需要重新编译 Linux 内核,得到新的zImage,然后用新的 zImage 启动开发板。
(3) 编写一个 APP 来关闭熄屏功能
在 ubuntu 中新建一个名为 lcd_always_on.c 的文件,然后在里面输入如下所示内容:
#include <fcntl.h>
#include <stdio.h>
#include <sys/ioctl.h>
int main(int argc, char *argv[])
{
int fd;
fd = open("/dev/tty1", O_RDWR);
write(fd, "\033[9;0]", 8);
close(fd);
return 0;
}
使用如下命令编译 lcd_always_on.c 这个文件:
arm-linux-gnueabihf-gcc lcd_always_on.c -o lcd_always_on
编译生成 lcd_always_on 以后将此可执行文件拷贝到开发板根文件系统的/usr/bin 目录中,然后给予可执行权限。设置 lcd_always_on 这个软件为开机自启动,打开/etc/init.d/rcS,在此文件最后面加入如下内容:
cd /usr/bin
./lcd_always_on
cd ..
修改完成以后保存/etc/init.d/rcS 文件,然后重启开发板即可。
九 总结
本文主要是了解IMX6ULL平台下的LCD使用,第二节主要是对LCD的有个认识,第三节是对IMX6ULL平台下的eLCDIF 外设的使用进行介绍。不同平台LCD的使用接口不同,而且不同LCD也有不同的通信接口方式。本文主要是对IMX平台LCD进行介绍。后续笔者也会出其它平台/其它接口的LCD相关文章吗,欢迎关注。