目录
5.3.屏幕分辨率设置 - LCDIF_TRANSFER_COUNT
5.4.VSYNC/DOTCLK 模式的控制寄存器 - LCDIF_VDCTRL0
5.5.VSYNC 信号周期设置 - LCDIF_VDCTRL1
5.6.HSYNC 信号宽度和周期设置 - LCDIF_VDCTRL2
5.7.HSYNC 和 VSYNC 信号的等待时间 - LCDIF_VDCTRL3
5.8.DOTCLK 模式控制寄存器 - LCDIF_VDCTRL4
5.10.下一帧图像显存地址 - LCDIF_NEXT_BUF
7.1.2.初始化 M0~M2 的引脚(用于读取屏幕 ID)
1.屏幕基础概念
本小节对屏幕分辨率、像素格式等基本概念进行简单说明
1.1.分辨率
描述显示器分辨率时常用 720P、1080P、2K、4K 等参数,需要注意的是,1080P 和 2K 这两类参数本质上并不一样
1.1.1.描述方法 - 720P、1080P
过去的 CRT 电视机使用的是阴极射线管成像,CRT 成像时,射线管将 R、G、B 三色光,按从左到右,从上到下的方式,逐行将每个像素点投射到屏幕上,如下图所示:
因此,在描述屏幕分辨率时,采用的是 xxxP,这里的 P 意为 Progressive,表示逐行扫描(除此以外,还有 1080I 等,I 表示隔行扫描,目前基本不使用),这种描述由美国电影电视工程协会 SMPTE 提出,主要用于描述数字高清电视的分辨率
采用 xxxP 描述分辨率时,xxx 表示有多少行,例如 1080P 表示在垂直方向上有 1080 行,注意,这里并未指定水平方向有多少列,分辨率中的列数可以根据屏幕的长宽比计算得出
屏幕的长宽比由各种协会、厂商等定义,目前主流的有 16:9、4:3 等,以 16:9 的长宽比为例,在 1080P 分辨率下,即垂直方向有 1080 行,则有 1080/9*16 = 1920,因此显示器的水平方向有 1920 列,这就是常见的 1920*1080 (16:9) 分辨率的由来
1.1.2.描述方法 - 2K、4K
分辨率 xK 的描述方式由数字电影联盟 DCI 提出,与 xxxP 描述分辨率的方式相反,xK 中的 x 表示水平方向上有多少列,其中的 K 为 Kilo(千),即 x 描述的是近似值,例如,2048 可以称为 2K(电影联盟标准定义的视频分辨率)、2560 也可以称为 2K(实际硬件屏幕的分辨率)
需要注意的是,2K、4K 一般描述的是视频源的分辨率,而非屏幕硬件的分辨率,例如,电影行业中的 2K 表示视频的分辨率为 2048*1080,而屏幕硬件的 2K 则为 2560*1440 (16:9)
1.1.3.小结
xxxP 描述分辨率时,数字部分 xxx 表示行数,分辨率中的列数通过屏幕的长宽比(如 16:9)计算获得
2K、4K 等一般用于描述视频源的分辨率,用于描述硬件分辨率不是很准确,例如,用 2K 描述屏幕,不如用 2560P 描述直观
对于研发人员,需要关注屏幕的实际参数,即屏幕有多少行、多少列像素点,例如,对于分辨率为 1920*1080 (16:9) 的屏幕,其水平方向有 1920 列(每一行有 1920 个像素点),垂直方向有 1080 行(每一列有 1080 个像素点),如下图所示:
因此,该屏幕的总像素数量 = 1920 * 1080 = 2073600 Pixel (即常说的 2 百万像素:2MP)
注意,成像时采用的是自上而下,自左向右的方式,因此屏幕的原点为 A(0, 0)
1.2.像素密度 - PPI
PPI (Pixels per Inch) 即每英寸包含的像素数量,该值越大,表示屏幕单位面积内包含更多的像素点,因此显示效果也会更为细腻
屏幕的大小一般使用英寸描述,其为屏幕对角线的长度,如下图所示:
以图中 5.2 英寸的屏幕为例,这里的屏幕大小为 13.208cm(对角线长度)
PPI 的计算公式为:
则上图中,5.2 英寸大小、1920*1080 分辨率屏幕的 PPI = (√((1920^2)+(1080^2)))/5.2 = 423.64 ≈ 424
需要注意的是,对于 OLED 屏幕,直接计算其 PPI 是不准确的,因为 OLED 屏幕 RGB 三色的像素点受排列方式(例如钻石排列、周冬雨排列等)的影响,因此,厂商一般会在手册中给出 PPI 系数,计算出的 PPI 值需要乘上该系数才能得到屏幕大致准确的 PPI 值
1.3.像素格式 - ARGB8888
屏幕的一个像素由 R、G、B 三个像素点(通道)组成,通过控制 RGB 各个通道的值便可以显示出各种颜色,常用的像素格式有 RGB565、RGB555、RGB888、RGB8888、YUV 等,其中:
-
RGB565:R、G、B 各占 5-bit、6-bit、5-bit,共 16-bit;
-
RGB555:R、G、B 各占 5-bit、5-bit、5-bit,一个未使用位,共 16-bit;
-
RGB888:R、G、B 各占 8-bit、8-bit、8-bit,共 24-bit;
-
RGB8888:R、G、B 各占 8-bit、8-bit、8-bit,透明度 Alpha 占 8-bit,共 32-bit;
-
YUV:Y 表示亮度,U 和 V 表示色度,U 和 V 为 0 时就表示黑白图像,YUV 与 RGB 可以通过计算互相转换,同样的,YUV 也有 YUV422、YUV420、YUV420P 等格式;
以 ARGB8888 为例,其共占用 32-bit,每个通道(包含透明度)占用 8-bit,单一通道可表示 2^8 = 0~255 种颜色,具体的像素格式如下图所示:
其中,[31:24] 位为透明度 Alpha、[23:16] 位为 RED 通道,[15:8] 位为 GREEN 通道、[7:0] 位为 BLUE 通道
因此,如果要画面显示纯红色,则可以将像素值全部设置为 0x00FF0000,通过调节三种颜色的值,就可以产生其他各种颜色,例如,可以在画板中打开颜色选取界面,在该界面中就可以看到各种颜色对应的 RGB 值:
ARGB8888 颜色格式中的 A 为透明度,修改该值可以改变透明度,例如手机中的透明度调整就是 Alpha 值
1.4.RGBLCD 接口
目前常用的屏幕接口为 MIPI-DSI 和 RGBLCD 两种,其中 MIPI 接口常用于手机等移动设备,RGBLCD 则用于一些传统显示设备,开发板使用的是 RGBLCD 接口,其包含以下引脚(信号线):
信号线 | 描述 |
R[7:0] | 8 根红色数据线 |
G[7:0] | 8 根绿色数据线 |
B[7:0] | 8 根蓝色数据线 |
DE | 数据使能线 |
VSYNC | 垂直同步信号线 |
HSYNC | 水平同步信号线 |
PCLK | 像素时钟信号线 |
RGBLCD 接口有两种工作模式:DE 模式和 HV 模式:
-
DE 模式必须使用 DE 信号线,但是可以不使用 HSYNC 信号线;
-
HV 模式必须使用 VSYNC 和 HSYNC 信号线,可以不使用 DE 信号线;
其中,DE、VSYNC、HSYNC、PCLK 四个信号为同步信号(控制信号),用于控制图像的传输,这四个信号的作用如下:
-
PCLK:Pixel Clock,像素时钟,控制每个像素数据的传输时间,每个 PCLK 周期传输一个像素的 RGB 数据(如 24 位的 R8+G8+B8,即每个 PCLK 传输一个 32-bit 的像素点);
-
HSYNC:Horizontal Sync,行同步信号,标记一行像素数据传输结束,指示 LCD 从当前行的末尾去到下一行的开始位置,即在 HSYNC 信号有效时间内传输的像素点,均视为同一行;
-
VSYNC:Vertical Sync,帧同步信号,标记一帧图像数据传输结束,指示 LCD 从屏幕底部回到顶部,例如,假设 VSYNC 信号高电平有效,则 VSYNC 信号维持高电平的这段时间内传输的图像数据,视为属于同一帧图像,当 VSYNC 信号拉低并重新置为高电平后,视为下一帧图像开始传输;
-
DE:Data Enable,数据使能信号,直接指示有效像素的传输时间,例如,当 DE 信号为高电平时,数据线上的 RGB 值有效,LCD 应该显示这些像素;当 DE 为低电平时数据无效,LCD 无需处理。DE 信号在行同步信号 HSYNC 和帧同步信号 VSYNC 的 Blank 时间外保持高电平,可替代 HSYNC/VSYNC 简化接口,如 MIPI-DSI 协议仅需 DE 和 PCLK 信号;
2.屏幕的时间参数
编写屏幕驱动时,除了配置基本的分辨率与刷新率外,还需要配置与时间相关的参数,如 VSYNC、HSYNC、HBP、VFP 等,这些参数对显示效果的影响很大,如果配置不正确,会导致屏幕出现画面撕裂等严重问题
2.1.Blank 和 Back/Front Porch
分析时间参数前,需要先熟悉 Blank(消隐)时间和 Back/Front Porch
注意:VSYNC 信号和 HSYNC 信号存在正负极性,如果信号中高电平的时间长,低电平的时间短为负极性,反之为正极性,下面以负极性信号为例进行说明,信号的极性依屏幕而定,需要参考具体的屏幕手册,则一个完整的 VSYNC 信号如下图所示:
HSYNC 和 VSYNC 信号均有固定的频率,例如,在刷新率为 60Hz 时,每秒有 60 个 VSYNC 信号,每个 VSYNC 信号的高电平约为 16.6 ms,如下图所示:
假如当前 VSYNC 信号为高电平,正在进行当前帧图像的传输,传输完成后,VSYNC 信号由高电平切换为低电平,则在 VSYNC 信号为低电平的这段时间内,不会进行图像数据的传输,或者认为这段时间内传输的图像数据无效,如下图所示:
在低电平维持的这段时间中,硬件会获取下一帧需要传输的图像数据 (释放当前 Buffer、获取下一个 Buffer),因此,也可以认为,VSYNC 信号的到来,表示将要开始传输新一帧的图像
当 VSYNC 信号由低电平切换为高电平后,开始进行下一帧图像的传输工作,如下图所示:
理想情况下,VSYNC 信号拉高时应立即开始传输数据,但实际上,信号会出现抖动或异常(如高电平时间过短),同时,屏幕内部 IC 需要确认待发送的图像数据是否已准备就绪,因此,为避免这些异常情况造成的影响,需要在信号拉高后等待一段时间,待信号稳定或数据准备就绪后再开始发送数据,这段等待的时间称为 Back Porch(对于 VSYNC 信号称为 VBP,对于 HSYNC 信号称为 HBP),如下图所示:
图像数据按照从左到右,从上到下的方式,逐行发送至屏幕,当一行数据发送完成后,需要去到下一行的开始位置,类比 CRT 显示器的工作原理,即电子枪需要从当前行的末尾,去到下一行的开始位置,该过程需要耗费一段时间,这段时间就称为 Front Porch(对于 VSYNC 信号称为 VFP,对于 HSYNC 信号称为 HFP):
Front Porch + 低电平时间 (同步时间) + Back Porch 所表示的时间也称为 Blank 时间:
-
对于 VSYNC 即为 VBlank 时间,即电子枪从屏幕的最后一行,回到屏幕第一行所需要的时间;
-
对于 HSYNC 即为 HBlank 时间,即电子枪从当前行的末尾,去到下一行开始位置所需要的时间;
2.2.VLW 和 HLW
对于 VSYNC 信号和 HSYNC 信号,低电平持续的时间称为 Low Pulse Width,类比 CRT 显示原理,这段时间中,电子枪关闭,等待下一次有效的 VSYNC/HSYNC 信号,如下图所示:
-
对于 VSYNC 信号,即为 VLP (Vertical Low Pulse Width);
-
对于 HSYNC 信号,即为 HLP (Horizontal Low Pulse width);
这两段时间为低电平,不会发送图像数据,或者认为这段时间发送的图像数据无效
2.3.HACT 和 VACT
对于 VSYNC 和 HSYNC 信号,除去 Back/Front Porch 以及 Low Pulse Width 的这段时间称为有效时间 Active,仅在该时间内发送的数据才会认为有效,如下图所示:
-
对于 VSYNC 信号,有效时间称为 VACT;
-
对于 HSYNC 信号,有效时间称为 HACT;
2.4.RGB LCD 屏幕时序
综上所述,图像数据的发送方式大致如下图所示:
图中蓝色部分即为有效的图像数据,各部分信号的含义及作用如下所示:
-
HS:HSYNC 信号;
-
VS:VSYNC 信号;
-
有效图像数据 = VACT + HACT 这段时间内发送的图像数据;
下面的图片展示了图像发送的时序:
该时序图包含两个部分,上半部分为 VSYNC 信号的时序(多帧图像),下半部分为 HSYNC 信号的时序(单帧图像),其中部分信号缩写的含义及作用如下:
-
DCK:Data Clock,为屏幕的像素时钟信号;
-
DB[23:0]:Data Base,为实际的 RGB 图像数据(有效数据);
-
DTST:Data Start Time,实际为 HLW + HBP,这段时间用于准备下一行/帧图像;
-
VACT:VSYNC Active,仅这段时间内发送的图像数据视为有效且属于同一帧;
-
HACT:HSYNC Active,仅这段时间内发送的图像数据视为有效且属于同一行;
2.4.1.行时序
行显示的时序如下图所示:
-
HSYNC:Horizontal Sync,行同步信号,该信号有效时表示开始显示新一行数据,查阅所使用的 LCD 数据手册可以确认该信号是低电平有效还是高电平有效;
-
HSPW:Horizontal Sync Pulse Width,HSYNC 信号低电平的宽度,也称为 THP,即 HSYNC 信号低电平的持续时间,HSYNC 信号不是一个脉冲,而是需要维持一段时间,单位为 CLK;
-
HBP:Horizontal Back Porch,行同步信号后肩,也称 THB,单位为 CLK;
-
HOZVAL:Horizontal Value,显示一行数据所需要的时间(行数),也称为 THD,屏幕分辨率为 1024*600,则 HOZVAL 就是 1024,单位为 CLK;
-
HFP:Horizontal Front Porch,行同步信号前肩,也称为 THF,单位为 CLK;
HSYNC 信号发出后,需要等待 HSPW + HBP 个 CLK 时间才会接收到真正有效的像素数据
显示完一行数据后,需要等待 HFP 个 CLK 时间才能发出下一个 HSYNC 信号,所以显示一行所需要的时间为:
HSPW + HBP + HOZVAL + HFP
2.4.2.帧时序
一帧图像由多个行组成,帧显示的时序如下图所示:
-
VSYNC:Vertical Sync,帧同步信号,该信号有效表示开始显示新一帧图像,查阅所使用的 LCD 数据手册可以确认该信号是低电平有效还是高电平有效;
-
VSPW:Vertical Sync Pulse Width,VSYNC 信号的宽度,也称为 TVP,表示 VSYNC 信号的持续时间;
-
VBP:Vertical Back Porch,VSYNC 信号的后肩,也称为 TVB;
-
LINE:也称为 TVD,显示一帧有效图像数据所需的时间,假如屏幕分辨率为 1024*600,则 LINE 为显示 600 行所需要的时间;
-
VFP:Vertical Front Porch,VSYNC 信号的前肩,也称为 TVF;
显示一帧图像所需要的时间为:(VSPW + VBP + LINE + VFP)*显示一行所需的时间,即:
T = (VSPW + VBP + LINE + VFP) * (HSPW + HBP + HOZVAL + HFP)
2.4.3.ALIENTEK 屏幕参数表
综上所述,在编写驱动时需要知道屏幕相关的参数,例程使用的 ALIENTEK 屏幕的参数如下图所示:
3.IMX6U eLCDIF 接口的时钟配置
IMX6U 的 eLCDIF (Enhanced LCD Interface) 为显示外设的控制器,其结构如下图所示:
图中各标号部分的含义如下:
-
引脚:用于连接外部显示设备(如 LCD 屏幕)和 eLCDIF 接口模块;
-
通过系统总线 AXI 向 eLCDIF 写入数据;通过控制总线 APB 读写寄存器或进行 DMA 操作,通过控制总线的寄存器可以配置显存地址、输入像素的格式、输出数据信号的宽度、控制信号的有效极性,以及控制时序中的 VBP、VSPW 等参数,还可以配置是否使用 DMA 传输;
-
LCD 接口:受控制总线 ControlBus 的寄存器控制,从系统总线 SystemBus 获取输入的像素数据,eLCDIF 初始化完成后,会从 LFIFO 和 TXFIFO 中获取数据并进行转换处理(格式转换、移位等操作)并传输出去,当 FIFO 中的数据量低于一定程度时,它会向系统总线 SystemBus 发起请求,系统总线会把显存地址的数据搬运至 FIFO 中,FIFO 可以配置阈值,低于该阈值时系统总线会提高获取数据的优先级;
-
总线的时钟信号 apb_clk 和 LCD 接口的时钟信号 pix_clk;
像素时钟 pix_clk 为 RGBLCD 接口的时钟信号,以 ATK7016 这款屏幕为例,显示一帧图像所需要的时钟数为:
= (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,因此,如果屏幕的刷新率为 60Hz,则像素时钟的频率应该设置为 51.2MHz
3.1.时钟树节点
IMX6U eLCDIF 接口的时钟树节点如下图所示:
图中标号部分的作用如下:
- ① 时钟源选择器 1:选择 eLCDIF 的时钟源,由 CCM_CSCDR2 寄存器的 LCDIF1_PRE_CLK_SEL[17:15] 位域控制,可选的时钟源如下:
- 0b000:PLL2;
- 0b001:PLL3_PFD3
- 0b010:PLL5;
- 0b011:PLL2_PFD0;
- 0b100:PLL2_PFD1;
- 0b101:PLL3_PFD1;
- 0b110~0b111:保留;
-
IMX6U 的时钟源 PLL5 专用于 Video 模块,因此例程选择 PLL5 作为 LCDIF 的时钟源,则 CCM_CSCDR2 寄存器的 LCDIF1_PRE_CLK_SEL[17:15] 设置为 0b010;
- ② 分频器 1:控制时钟源的分频值,由寄存器 CCM_CSCDR2 的 LCDIF1_PRED[14:12] 位域控制,可以设置 0b000~0b111,对应 1~8 分频;
- ③ 分频器 2:控制时钟源的分频值,由寄存器 CCM_CBCMR 的 LCDIF1_PODF[25:23] 位域控制,可以设置 0b000~0b111,对应 1~8 分频;
-
④ 时钟源选择器 2:选择 eLCDIF 的时钟源,由寄存器 CCM_CSCDR2 的 LCDIF1_CLK_SEL[11:9] 位域控制:
-
0b000:选择时钟源选择器 1 输出的时钟信号作为时钟源;
-
0b001:选择 ipp_di0_clk 作为时钟源;
-
0b010:选择 ipp_di1_clk 作为时钟源;
-
0b011:选择 ldb_di0_clk 作为时钟源;
-
0b100:选择 ldb_di1_clk 作为时钟源;
-
0b101~0b111:保留;
-
IMX6U 的时钟源 PLL5 专用于 Video 模块,因此需要选择 PLL5 作为 eLCDIF 的时钟源,在时钟源选择器 1 中已经设置 PLL5 作为输出时钟源,因此,这里将时钟源选择器 2 的值(寄存器 CCM_CSCDR2 的 LCDIF1_CLK_SEL[11:9] 位域)设置为 0b000,从而选择时钟源选择器 1 输出的时钟源作为 eLCDIF 最终的时钟源,即 PLL5;
3.2.时钟源选择
IMX6U 的时钟源 PLL5 专用于 Video 模块,因此选择 PLL5 作为 LCDIF 的时钟源,两个时钟源选择器对应的寄存器位域的值设置如下:
-
时钟源选择器 1:CCM_CSCDR2 寄存器的 LCDIF1_PRE_CLK_SEL[17:15] 设置为 0b010(选择 PLL5 作为时钟源);
-
时钟源选择器 2:CCM_CSCDR2 寄存器的 LCDIF1_CLK_SEL[11:9] 设置为 0b000(选择时钟源选择器 1 输出的时钟源作为 eLCDIF 的最终时钟源,即最终选择 PLL5 作为 eLCDIF 的时钟源);
3.3.设置 PLL5 时钟源的频率
配置分频系数以获取所需的时钟频率,以 ATK7016 屏幕所需的 51.2MHz 时钟频率为例,时钟源已选择为 PLL5:
设置时钟源 PLL5 的频率(分频系数)涉及以下三个寄存器:
-
CCM_ANALOG_PLL_VIDEO(0x020C80A0)
-
CCM_ANALOG_PLL_VIDEO_NUM(0x020C80B0)
-
CCM_ANALOG_PLL_VIDEO_DENOM(0x020C80C0)
其中 CCM_ANALOG_PLL_VIDEO_NUM 和 CCM_ANALOG_PLL_VIDEO_DENOM 这两个寄存器用于配置小数分频的分子和分母
时钟源 PLL5 时钟频率的计算方式如下:
PLL5_CLK = OSC24M * (loopDivider + (numerator / denominator))
其中各部分的含义,以及涉及到的相关寄存器位域如下所示:
-
OSC24M:24MHz 的晶振;
-
loopDivider:由寄存器 CCM_ANALOG_PLL_VIDEO 的 DIV_SELECT[6:0] 位域设置,可设置的范围为 27~54;
-
numerator:由寄存器 CCM_ANALOG_PLL_VIDEO_NUM 的 A[29:0] 位域设置,30-bit 长度;
-
denominator:由寄存器 CCM_ANALOG_PLL_VIDEO_DENOM 的 B[29:0] 位域设置,30-bit 长度;
设置 PLL5 时钟源的频率,不考虑小数部分,则公式简化为:
PLL5_CLK = OSC24M * loopDivider
-
OSC24M 为固定频率为 24MHz 的晶振;
-
loopDivider 由寄存器 CCM_ANALOG_PLL_VIDEO 的 DIV_SELECT[6:0] 位域设置,可设置的范围为 21~54,例程中将其设置为 32;
因此,PLL5 时钟源的频率为:
PLL5_CLK = 24MHz * 32 = 768 MHz
3.4.时钟源 PLL5 的输出频率
时钟源 PLL5 输出时可以进行分频,由寄存器 CCM_ANALOG_PLL_VIDEO 的 POST_DIV_SELECT[20:19] 位域,以及寄存器 CCM_ANALOG_MISC2 的 VIDEO_DIV[31:30] 共同决定,如下图所示:
输出频率计算方式如下:
OUTPUT_CLK = PLL5_CLK / POST_DIV / VIDEO_DIV
= PLL5_CLK / postDivider
-
PLL5_CLK:已经设置为 768MHz;
-
postDivider:由寄存器 CCM_ANALOG_PLL_VIDEO 的 POST_DIV_SELECT[20:19] 位域,以及寄存器 CCM_ANALOG_MISC2 的 VIDEO_DIV[31:30] 共同决定;
CCM_ANALOG_PLL_VIDEO.POST_DIV_SELECT[20:19] 位域可设置的值如下:
-
0b00:4 分频(除以 4);
-
0b01:2 分频(除以 2);
-
0b10:1 分频(除以 1);
-
0b11:保留;
CCM_ANALOG_MISC2.VIDEO_DIV[31:30] 位域可设置的值如下:
-
0b00:1 分频(除以 1);
-
0b01:2 分频(除以 2);
-
0b10:1 分频(除以 1);
-
0b11:4 分频(除以 4);
例程中 postDivider 的设置如下:
-
CCM_ANALOG_PLL_VIDEO 寄存器的 POST_DIV_SELECT[20:19] 设置为 0b01,即 1 分频;
-
CCM_ANALOG_MISC2 寄存器的 VIDEO_DIV[31:30] 设置为 0b00,即 1 分频;
因此,不会对 PLL5 时钟源的输出进行分频操作,时钟源 PLL5 的输出频率为:
PLL5_CLK = 768MHz / 1 / 1 = 768MHz
3.5.设置 eLCDIF 接口的时钟频率
eLCDIF 接口的时钟源选择为 PLL5,时钟源 PLL5 的输出频率已经设置为 768MHz
现在需要将 eLCDIF 接口的时钟频率设置为 51.2MHz,可以通过 eLCDIF 的两个分频器分频获得:
-
eLCDIF 的分频器 1 由寄存器 CCM_CSCDR2 的 LCDIF1_PRED[14:12] 位域控制,可以设置 0b000~0b111,对应 1~8 分频,例程中将其设置为 0b010,即 3 分频;
-
eLCDIF 的分频器 2,由寄存器 CCM_CBCMR 的 LCDIF1_PODF[25:23] 位域控制,可以设置 0b000~0b111,对应 1~8 分频,例程中将其设置为 0b101,即 5 分频;
因此,eLCDIF 接口的最终频率为:
Freq = 768MHz / 3 / 5
= 51.2MHz
3.6.时钟使能
时钟源及时钟频率设置完成后,通过将 CCM_ANALOG_PLL_VIDEO 寄存器的 ENABLE[13] 位置 1 使能时钟输出:
4.eLCDIF 的屏幕接口
eLCDIF 接口的特性如下:
-
支持 RGBLCD 的 DE 模式;
-
支持 VSYNC 模式以实现高速数据传输;
-
支持 ITU-R BT.656 格式 4:2:2 的 YCbCr 数字视频,并且可以将其转换为模拟 TV 信号;
-
支持 8/16/18/24/32 位 LCD 屏幕;
eLCDIF 支持三种接口:MPU 接口、VSYNC 接口和 DOTCLK 接口,这三种接口的区别如下
4.1.MPU 接口
使用 MPU 接口时,I.MX6U 和 LCD 屏幕直接传输数据和命令,该接口用于 6080/8080 接口的 LCD 屏幕,比如 STM32 的 MCU 屏幕
寄存器 LCDIF_CTRL 的位 DOTCLK_MODE[17]、VSYNC_MODE[18]、DVI_MODE[20] 均为 0 时表示 eLCDIF 工作在 MPU 接口模式
关于 MPU 接口的详细信息及时序参考《I.MX6ULL 参考手册》的 34.4.6 MPU Interface 小节,例程不使用 MPU 接口
4.2.VSYNC 接口
VSYNC 接口的时序和 MPU 接口的时序基本一致,只是多了 VSYNC 作为帧同步信号,寄存器 LCDIF_CTRL 的位 VSYNC_MODE[18] 为 1 时使用该接口
关于 VSYNC 接口的详细信息及时序参考《I.MX6ULL 参考手册》的 34.4.7 VSYNC Interface 小节,例程不使用 VSYNC 接口
4.3.DOTCLK 接口
DOTCLK 接口用于连接 RGBLCD 屏幕,其包含 VSYNC、HSYNC、DOTCLK、ENABLE (可选) 四个信号,该接口通常称为 RGBLCD 接口,例程使用该接口驱动屏幕,DOTCLK 接口的时序如下图所示:
4.4.屏幕显存
如果采用 ARGB8888 格式进行显示,一个像素需要 4 个字节的内存来存放像素数据,因此 1024*600 分辨率需要 1024 * 600 * 4 = 2457600B ≈ 2.4MB 内存
但 LCD 屏幕内部没有内存,所以需要在开发板的 DDR3 内存中分出一部分作为 LCD 屏幕的显存,如果要在屏幕上显示图像,则可以直接操作这部分内存,这部分内存称为显存
5.eLCDIF 相关配置寄存器
5.1.控制寄存器 - LCDIF_CTRL
LCDIF_CTRL 寄存器控制所使用的接口、数据端序、数据宽度等基本配置,其结构如下所示:
其中各个位域的含义如下:
-
SFTRST[31]:软件复位控制位,向该位写 1 会强制复位 eLCDIF 接口,eLCDIF 正常工作时该位必须为 0;
-
CLKGATE[30]:时钟信号控制位,向该位写 1 会关闭 eLCDIF 的时钟信号,eLCDIF 正常工作时该位必须为 0;
-
YCBCR422_INPUT[29]:颜色空间选择,该位为 0 表示输入为 RGB 格式;该位为 1 表示输入为 YCbCr422 格式,数据按 YCbYCr 的格式重新打包为 32-bit 的数据,因此 32-bit 的数据中其实包含 2 个像素(每个像素占用 4 + 2 = 6-bit);该位置 1 后(表示使用 YCbCr422 格式),软件还需要配置 LCDIF_TRANSFER_COUNT 寄存器的 H_COUNT[16:0] 位域,将其设置为 eLCDIF 每个行时钟周期(每个 HSYNC 信号)需要获取的像素个数(32-bit),LCDIF_CTRL1 寄存器的 BYTE_PACKING_FORMAT[19:16] 需要设置为 0xF,这种情况下 WORD_LENGTH[9:8] 的值无关紧要;
-
READ_WRITEB[28]:eLCDIF 默认为写模式,该位置 1 表示硬件工作在 6800/8080 MPU 读取模式,此时必须将 MASTER[5] 位置 0,因为总线的 Master 模式只能向屏幕写入数据(无法读取);
-
WAIT_FOR_VSYNC_EDGE[27]:该位置 1 后,硬件会在检测到 VSYNC 信号的边沿后开始向 LCD 写入数据,该位仅用于 VSYNC 模式;
-
DATA_SHIFT_DIR[26]:控制数据发送时的移位方向,在 DVI 模式中仅对有效数据起作用,与时序信号和辅助数据无关:
-
0x0:TXDATA_SHIFT_LEFT,要传输的数据左移 SHIFT_NUM_BITS[25:21] 位;
-
0x1:TXDATA_SHIFT_RIGHT,要传输的数据右移 SHIFT_NUM_BITS[25:21] 位;
-
-
SHIFT_NUM_BITS[25:21]:每次左移或右移的位数;
-
DVI_MODE[20]:该位置 1 进入 ITU-R BT.656 Video 模式,向该位写 0 后,硬件会在当前所有数据发送完成,且 RUN[0] 位为 0 时退出 DIV 模式;
-
BYPASS_COUNT[19]:该位为 0 时,eLCDIF 会在发送完 LCDIF_TRANSFER_COUNT 寄存器中指定的数据量后停止工作,并将 RUN[0] 位置 0;该位为 1 时,eLCDIF 会在指定量的数据发送完成后继续工作,直至软件设置其停止;MPU 模式和 VSYNC 模式中,该位必须置 0;DOTCLK 模式和 DVI 模式中,该位必须置 1;
-
VSYNC_MODE[18]:该位置 1 时 eLCDIF 工作在 VSYNC 模式,此时可以使用 WAIT_FOR_VSYNC_EDGE[27] 位控制是否等待 VSYNC 信号的边沿,如果需要输出 VSYNC 信号,则必须将 LCDIF_VDCTRL4 寄存器的 SYNC_SIGNALS_ON[18] 位置 1;
-
DOTCLK_MODE[17]:该位置 1 时 eLCDIF 工作在 DOTCLK 模式,此时 VSYNC/HSYNC/DOTCLK/ENABLE 这四个同步信号可用,其中 ENABLE 信号可以不使用(由 LCDIF_VDCTRL0 寄存器的 ENABLE_PRESENT[28] 位控制),向该位写 0 后,硬件会在当前所有数据发送完成,且 RUN[0] 位为 0 时退出 DOTCLK 模式;
-
DATA_SELECT[16]:Command 模式选择,该位仅在 RUN[0] 位为 0 时可以修改:
-
0x0:CMD_MODE,LCD_RS 信号为低电平;
-
0x1:DATA_MODE,LCD_RS 信号为高电平;
-
-
INPUT_DATA_SWIZZLE[15:14]:输入数据字节翻转(修改字节序),受 WORD_LENGTH[9:8] 位的影响,支持的翻转模式如下:
-
0x0:NO_SWAP,不进行翻转,小端序;
-
0x0:LITTLE_ENDIAN,小端序,和 NO_SWAP 一样;
-
0x1:BIG_ENDIAN_SWAP,大端序翻转,第 0 字节和第 3 字节交换,第 1 字节和第 2 字节交换;
-
0x1:SWAP_ALL_BYTES,交换所有字节,和大端序翻转一样;
-
0x2:HWD_SWAP,按半个字节进行翻转;
-
0x3:HWD_BYTE_SWAP,对每个半字节内部进行翻转;
-
-
CSC_DATA_SWIZZLE[13:12]:数据转换为 24-bit 的像素后,通过 LCD 接口传输前可以再次翻转,数据翻转后,总是优先传输最低位,先通过 INPUT_DATA_SWIZZLE[15:14] 对数据进行翻转,然后通过 CSC_DATA_SWIZZLE[13:12] 再次对数据进行翻转,数据翻转设置受 WORD_LENGTH[9:8] 和 LCD_DATABUS_WIDTH[11:10] 位域的影响,如果 RGB_TO_YCRCB422_CSC[7] 位为 1,则会对 Y、Cb、Cr 进行位置交换,支持的翻转模式如下:
-
0x0:NO_SWAP,不进行翻转,小端序;
-
0x0:LITTLE_ENDIAN,小端序,和 NO_SWAP 一样;
-
0x1:BIG_ENDIAN_SWAP,大端序翻转,第 0 字节和第 3 字节交换,第 1 字节和第 2 字节交换;
-
0x1:SWAP_ALL_BYTES,交换所有字节,和大端序翻转一样;
-
0x2:HWD_SWAP,半字节翻转;
-
0x3:HWD_BYTE_SWAP,对每个半字节进行翻转;
-
-
LCD_DATABUS_WIDTH[11:10]:控制 LCD 总线的数据宽度:
-
0x0:16_BIT;
-
0x1:8_BIT;
-
0x2:18_BIT;
-
0x3:24_BIT;
-
-
WORD_LENGTH[9:8]:控制输入数据的宽度(每个像素占用多少位):
-
0x0:16_BIT;
-
0x1:8_BIT;
-
0x2:18_BIT;
-
0x3:24_BIT;
-
-
RGB_TO_YCBCR422_CSC[7]:该位置 1 后,会将数据从 RGB 颜色空间转换为 YCbCr 颜色空间,详情请参考 LCDIF_CSC_xxx 相关寄存器;
-
ENABLE_PXP_HANDSHAKE[6]:当 eLCDIF 工作在 Master 模式时(MASTER[5] 为 1),如果将该位置 1,则 eLCDIF 会成为总线中的 Master 节点,同时启用 eLCDIF 和 PXP 之间的握手机制,如果 MASTER[5] 为 0,则该位无效;
-
MASTER[5]:该位置 1 时,该 eLCDIF 模块会成为总线中的 Master 节点;
-
DATA_FORMAT_16_BIT[3]:当该位为 1 且 WORD_LENGTH[9:8] 为 0x0 时(输入数据的宽度为 16-bit),表示当前像素格式为 16-bit 宽度的 RGB555;当该位为 0 且 WORD_LENGTH[9:8] 为 0x0 时(输入数据的宽度为 16-bit),表示当前像素格式为 16-bit 宽度的 RGB565;当 WORD_LENGTH[9:8] 不为 0x0 时,该位无效;
-
DATA_FORMAT_18_BIT[2]:仅当 WORD_LENGTH[9:8] 为 0x2 时(输入数据的宽度为 18-bit)该位有效,此时,该位值的含义如下:
-
0x0:LOWER_18_BITS_VALID,输入数据为 18bpp (bit per pixel) RGB666 格式,则低 18-bit 为有效数据,高 14-bit 为无效数据;
-
0x1:UPPER_18_BITS_VALID,输入数据为 18bpp (bit per pixel) RGB666 格式,则高 18-bit 为有效数据,低 14-bit 为无效数据;
-
-
DATA_FORMAT_24_BIT[1]:仅当 WORD_LENGTH[9:8] 为 0x3 时(输入数据的宽度为 24-bit)该位有效,此时,该位值的含义如下:
-
0x0:ALL_24_BITS_VALID,输入数据为 24bpp RGB888 格式,所有的 24-bit 均为有效数据;
-
0x1:DROP_UPPER_2_BITS_PER_BYTE,输入数据的长度为 18bpp RGB666,此时每个字节的最高两位不包含有效数据,将其丢弃;
-
-
RUN[0]:将该位置 1 后,eLCDIF 开始运行(在 SoC 和屏幕之间传输数据),该位为 eLCDIF 的使能位,在当前数据传输完成前,该位必须为 1;
5.2.控制寄存器 - LCDIF_CTRL1
LCDIF_CTRL1 寄存器控制 MPU 接口、中断、数据打包方式等配置,其结构如下图所示:
其中各个位域的含义如下:
-
COMBINE_MPU_WR_STRB[27]:eLCDIF 工作在 MPU 模式下时,控制写选通信号所驱动的引脚,该位不会影响读选通信号,各个值的含义如下:
-
0b0:8080 模式下写选通信号驱动 LCD_WR_RWn 引脚;6800 模式下写选通信号驱动 LCD_RD_E 引脚;
-
0b1:8080 模式和 6800 模式的写选通信号均驱动 LCD_WR_RW 引脚;
-
-
BM_ERROR_IRQ_EN[26]:eLCDIF 接口为 Master 节点时,使能 Bus Master Error 中断;
-
BM_ERROR_IRQ[25]:eLCDIF 接口为 Master 节点时,如果 Slave 节点向其发送了 Error Response,则会将该位置 1,软件通过向 SCT 清除地址写 1 清除该中断标志:
-
0x0:NO_REQUEST,未产生中断请求;
-
0x1:REQUEST,存在挂起的中断请求;
-
-
RECOVER_ON_UNDERFLOW[24]:该位置 1 时,若当前帧产生 UnderFlow 错误,则会在下一帧开始传输时汇报该错误;
-
INTERLACE_FIELDS[23]:仅当 LCDIF_CTRL 寄存器的 MASTER[5] 位为 1 时有效,该位置 1 后,eLCDIF 会分别在两块内存/两个寄存器中,分别获取奇数行的数据和偶数行的数据;
-
START_INTERLACE_FROM_SECOND_FIELD[22]:默认情况下,传输图像数据时,会先获取偶数行的数据,再获取奇数行的数据,如果该位置 1,则会先获取奇数行的数据(行编号从 1 开始);
-
FIFO_CLEAR[21]:向该位写 1 会清除所有 Latency FIFO (LFIFO)、TXFIFO、RXFIFO 中的数据;
-
IRQ_ON_ALTERNATE_FIELDS[20]:该位置 1 后,eLCDIF 只会在奇数行与偶数行切换时,检查是否需要产生 cur_frame_done 中断(检查当前帧传输完成);若该位为 0,则 eLCDIF 在开始获取奇数行或偶数行数据时,均会检查是否需要产生 cur_frame_done 中断;
-
BYTE_PACKING_FORMAT[19:16]:该位域用于标记 32-bit 的数据中,哪些字节为有效数据,默认值 0xF 表示一个字节全部的 8-bit 均为有效位:
-
数据宽度为 8-bit 时,该位域的所有值都表示该字节中存在有效数据;
-
数据宽度为 16-bit 时,该位域的值为 0b0011、0b1100、0b1111 时,表示该数据有效,当值为 0b0000 时表示所有的位均为无效数据;
-
例如,颜色格式为 ARGB8888,每个颜色通道占 8-bit,若不需要传输透明度 Alpha,则有效数据占 24-bit,则此时需要将 BYTE_PACKING_FORMAT[19:16] 的值设置为 0x7 (0b0111);
-
例如,颜色格式为 YCbCr422 (此时,每个 32-bit 的输入数据中包含两个像素,即所有的位均为有效位),则 LCDIF_TRANSFER_COUNT 寄存器的 H_COUNT[15:0] 位域应该设置为每次传输时需要获取的像素数量,BYTE_PACKING_FORMAT[19:16] 应该设置为 0xF (0b1111);
-
-
OVERFLOW_IRQ_EN[15]:eLCDIF 为写入模式时,将该位置 1 使能 TXFIFO 的 OverFlow 中断;
-
UNDERFLOW_IRQ_EN[14]:eLCDIF 为写入模式时,将该位置 1 使能 TXFIFO 的 UnderFlow 中断;
-
CUR_FRAME_DONE_IRQ_EN[13]:该位置 1 后,每次 eLCDIF 进入 VBLANK 状态时都会产生中断(即每一帧图像传输完成时产生一次中断);
-
VSYNC_EDGE_IRQ_EN[12]:在 VSYNC 和 DOTCLK 模式中,该位置 1 后,每个 VSYNC 信号的上升沿产生一次中断;DVI 模式中,每次开始传输新的 Field 时产生中断;
-
OVERFLOW_IRQ[11]:Latency FIFO (LFIFO) OverFlow 时产生中断,同时数据会丢失:
-
0x0:NO_REQUEST,未产生中断(没有出现 LFIFO OverFlow);
-
0x1:REQUEST,存在挂起的中断请求(LFIFO OverFlow);
-
-
UNDERFLOW_IRQ[10]:TXFIFO UnderFlow 时产生中断:
-
0x0:NO_REQUEST,未产生中断(没有出现 TXFIFO UnderFlow);
-
0x1:REQUEST,存在挂起的中断请求(TXFIFO UnderFlow);
-
-
CUR_FRAME_DONE_IRQ[9]:该位为 1 时表示当前帧传输完成,DOTCLK/DVI 模式中还表示硬件处于 VBLANK 区间;在 MPU 和 VSYNC 模式中,发送完 LCDIF_TRANSFER_COUNT 中指定的数据量后,会设置该标志位:
-
0x0:NO_REQUEST,未产生中断;
-
0x1:REQUEST,存在挂起的中断请求;
-
-
VSYNC_EDGE_IRQ[8]:VSYNC 模式和 DOTCLK 模式中,VSYNC 信号的上升沿和下降沿均会设置该中断标志位;DVI 模式中,开始传输新的 Field 时设置该标志位:
-
0x0:NO_REQUEST,未产生中断;
-
0x1:REQUEST,存在挂起的中断请求;
-
-
BUSY_ENABLE[2]:该位置 1 后,启用接口的 Busy 信号输入,如果 LCD 控制器有 Busy 信号线,则应该将该位置 1(在 LCD 屏幕准备就绪前,阻止 eLCDIF 发送更多的数据),否则应该将该位置 0:
-
0x0:BUSY_DISABLED,忽略 LCD 控制器的 Busy 信号;
-
0x1:BUSY_ENABLED,启用 LCD 控制器的 Busy 信号;
-
-
MODE86[1]:该位设置 eLCDIF 接口使用 8080 模式还是 6800 模式,仅当 LCDIF_CTRL 寄存器的 RUN[0] 为 0 时才允许修改:
-
0x0:8080_MODE,引脚 LCD_WR_RWn 和 LCD_RD_E 分别用作低电平有效的 WR 信号和低电平有效的 RD 信号;
-
0x1:6800_MODE,引脚 LCD_WR_RWn 和 LCD_RD_E 分别用作读/写信号和高电平有效的使能信号;
-
-
RESET[0]:外部 LCD 控制器的复位信号,任何时间都可以修改该位,但是无法通过 SFTRST 进行清除:
-
0x0:LCDRESET_LOW,LCD_RESET 输出信号为低电平;
-
0x1:LCDRESET_HIGH,LCD_RESET 输出信号为高电平;
-
5.3.屏幕分辨率设置 - LCDIF_TRANSFER_COUNT
该寄存器用于设置屏幕的分辨率,即有多少行、多少列,其结构如下图所示:
该寄存器仅包含 V_COUNT[31:16] 和 H_COUNT[15:0] 两个位域:
-
V_COUNT[31:16]:每一帧图像包含的有效行的数量,在 DOTCLK 模式中,其值应该等于一帧图像包含的有效行的数量;在 DVI 模式中,其值应该等于一帧图像包含的有效行的数量,而不是每个 Field 的数量;
-
H_COUNT[15:0]:每一行包含的有效像素点的数量(即有效列的数量),输入图像格式为 YCbCr422 时,H_COUNT[15:0] 应该 32-bit 像素点的数量;输入图像格式为 RGB 时,24-bit 长度的像素点对应的 H_COUNT[15:0] 应该为 4 的倍数,16-bit 长度的像素点对应的 H_COUNT[15:0] 应该为 2 的倍数;
以 1024*600 分辨率的屏幕为例,V_COUNT[31:16] 应该设置为 600,H_COUNT[15:0] 应该设置为 1024
5.4.VSYNC/DOTCLK 模式的控制寄存器 - LCDIF_VDCTRL0
LCDIF_VDCTRL0 寄存器用于配置 VSYNC/DOTCLK 模式中的 VSYNC/HSYNC 信号:
-
VSYNC_OEB[29]:该位为 0 表示 VSYNC 信号为输出信号,为 1 表示 VSYNC 信号为输入信号,在 DOTCLK 模式中应该设置为 0:
-
0x0:VSYNC_OUTPUT,VSYNC 引脚为输出引脚,VSYNC 信号由 eLCDIF 生成;
-
0x1:VSYNC_INPUT,VSYNC 引脚为输入引脚,LCD 控制器向 eLCDIF 发送 VSYNC 信号;
-
-
ENABLE_PRESENT[28]:DOTCLK 模式中,该位置 1 后,硬件会生成 ENABLE 信号;
-
VSYNC_POL[27]:默认值为 0,VSYNC_PULSE_WIDTH 时间内 VSYNC 信号为低电平,其他时间为高电平,如果将该位设置为 1,则会翻转电平极性;
-
HSYNC_POL[26]:默认值为 0,HSYNC_PULSE_WIDTH 时间内 HSYNC 信号为低电平,其他时间为高电平,如果将该位设置为 1,则会翻转电平极性;
-
DOTCLK_POL[25]:默认值为 0,DOTCLK 信号的下降沿锁存数据,上升沿获取新数据,该位置 1 翻转电平极性,在 DVI 模式中,必须设置为 0;
-
ENABLE_POL[24]:默认值为 0,ENABLE 信号低电平有效;为 1 时高电平有效;
-
VSYNC_PERIOD_UNIT[21]:默认值为 0,VSYNC 周期为像素时钟 pix_clk;该位置 1 时,发送完一帧图像后对 VSYNC_PERIOD 进行计数;VSYNC 模式中需要启用 DISPLAY_CLOCK (pix_clk),DOTCLK 模式中应该在一帧图像发送完成后对 VSYNC_PERIOD 进行计数;
-
VSYNC_PULSE_WIDTH_UNIT[20]:VSYNC 信号的宽度,默认值为 0,其宽度单位为 DISPLAY_CLOCK (pix_clk);为 1 时为发送完一行数据所需要的时间;
-
HALF_LINE[19]:该位置 1 后,VSYNC 周期等于 VSYNC_PERIOD 加上 HORIZONTAL_PERIOD 的一半;该位为 0 时,VSYNC 周期等于 VSYNC_PERIOD,仅在 DOTCLK 模式中可以使用;
-
HALF_LINE_MODE[18]:该位为 0 时,第一个 VSYNC 信号在 HSYNC 信号的一半结束,第二个 VSYNC 信号在 HSYNC 信号的一半开始;该位为 1 时,所有 VSYNC 信号均在 HSYNC 信号的中间位置结束,没有 VSYNC 信号在 HSYNC 信号的中间开始;
-
VSYNC_PULSE_WIDTH[17:0]:VSYNC 信号的宽度,DOTCLK 模式中,该位域的值和 VSYNC_PULSE_WIDTH_UNIT[20] 相关,如果其为 0,则 VSYNC_PULSE_WIDTH[17:0] 必须小于LCDIF_VDCTRL2 寄存器 HSYNC_PERIOD[15:0] 中设置的值;VSYNC 模式中,该值只和 DISPLAY_CLOCK (pix_clk) 相关;
5.5.VSYNC 信号周期设置 - LCDIF_VDCTRL1
该寄存器用于设置 VSYNC 信号的周期,其结构如下图所示:
-
VSYNC_PERIOD[31:0]:设置一个 VSYNC 信号的周期(一个低电平时间加上一个高电平的总时间),即:屏幕高度 + VSPW + VBP + VFP;
5.6.HSYNC 信号宽度和周期设置 - LCDIF_VDCTRL2
LCDIF_VDCTRL2 寄存器用于设置 DOTCLK 模式中的 HSYNC 信号,其结构如下图所示:
-
HSYNC_PULSE_WIDTH[31:18]:设置 HSYNC 信号低电平的宽度,单位为时钟周期;
-
HSYNC_PERIOD[17:0]:设置一个 HSYNC 信号的周期(一个低电平时间加上一个高电平的总时间);
5.7.HSYNC 和 VSYNC 信号的等待时间 - LCDIF_VDCTRL3
该寄存器用于设置 VSYNC/HSYNC 信号有效时间之前的等待时间(HBP、VFP 等),其结构如下图所示:
其中各个位域的含义如下:
-
MUX_SYNC_SIGNALS[29]:该位置 1 后,eLCDIF 内部会复用 HSYNC 和 LCD_D14、DOTCLK 和 LCD_D13、以及 ENABLE 和 LCD_D12;该位为 0 时,这些信号会使用各自独立的引脚;
-
VSYNC_ONLY[28]:VSYNC 模式中该位必须为 1,DOTCLK 模式中该位必须为 0;
-
HORIZONTAL_WAIT_CNT[27:16]:DOTCLK 模式中,为 HSYNC 信号发送有效数据前的等待时间,即 HSPW + HBP;
-
VERTICAL_WAIT_CNT[15:0]:VSYNC 模式中,为发送数据前的等待时间,最小值为 CMD_SETUP + 5;在 DOTCLK 模式中,为 VSPW + VBP;
5.8.DOTCLK 模式控制寄存器 - LCDIF_VDCTRL4
LCDIF_VDCTRL4 寄存器用于控制 DOTCLK 模式,其结构如下图所示:
其中相关位域的含义如下:
-
DOTCLK_DLY_SEL[31:29]:LCD_DOTCK 引脚 DOTCLK 的延迟时间,0 = 2ns;1 = 4ns;2 = 6ns;3 = 8ns;其余为保留值;
-
SYNC_SIGNALS_ON[18]:如果 LCD 控制器要求 VSYNC 或 VSYNC/HSYNC/DOTCLK 控制信号在数据传输实际开始前至少有一帧处于活动状态,并在数据传输结束后至少有一帧保持活动状态,则将该位置 1;硬件不会自动计算已经发送的帧数,软件可以监控 VSYNC 的边沿中断,以计算实际发送的帧数,在 DOTCLK 模式下,必须将该位设置为 1;在 VSYNC 模式下,当 VSYNC 信号作为输出时,必须将该位设置为 1;
-
DOTCLK_H_VALID_DATA_CNT[17:0]:DOTCLK 模式中,为一行所包含的有效像素的数量,即 LCD 屏幕的宽度;
5.9.当前图像显存地址 - LCDIF_CUR_BUF
LCDIF_CUR_BUF 寄存器保存当前图像数据所位于的内存地址,其结构如下所示:
-
ADDR[31:0]:当前需要发送的图像数据的内存地址;
5.10.下一帧图像显存地址 - LCDIF_NEXT_BUF
LCDIF_NEXT_BUF 寄存器保存下一帧图像数据所位于的内存地址,其结构如下所示:
6.硬件原理图
RGBLCD 接口与开发板连接的硬件原理图如下所示:
三个 SGM3157 模拟开关的目的是在未使用 RGBLCD 时,隔离 LCD_DATA7、LCD_DATA15 和 LCD_DATA23 这三根线,因为 ALIENTEK 屏幕的 LCD_R7/G7/B7 这几根线用于设置 LCD 的 ID,所以这几根线上有上拉/下拉电阻,但是 I.MX6U 的 BOOT 设置也用到了 LCD_DATA7、LCD_DATA15 和 LCD_DATA23 这三个引脚,所以接上屏幕以后,屏幕上的 ID 电阻会影响 BOOT 设置,导致代码无法运行,所以先将其隔离开来,要使用 RGBLCD 屏幕时再通过 LCD_DE 将其连接起来
-
三个 SGM3157 模拟开关分别与 LCD_DATA23、LCD_DATA15、LCD_DATA07 引脚连接;
-
系统启动时,三个模拟开关均为断开状态;
-
系统启动后,通过让 LCD_VSYNC 引脚输出高电平,打开三个模拟开关;
-
模拟开关打开后,通过读取 LCD_DATA23、LCD_DATA15、LCD_DATA07 引脚,分别获取 M0~M2 的值;
-
依据 M0:M1:M2 的值获取屏幕的 ID,判断当前屏幕的大小以及分辨率;
7.LCD 驱动代码实现
以 4.3 英寸大小,480*272 分辨率的屏幕为例,其时间相关参数如下图所示:
7.1.获取屏幕 ID
正点原子的屏幕使用三个 SGM3157 模拟开关 M0/M1/M2 识别 ID,M0/M1/M2 的各个值对应不同型号的屏幕,如下图所示:
M0~M2 的值与屏幕 ID 的对应关系如下:
M0 : M1 : M2 | 屏幕类型 |
0 0 0 | 4.3 英寸大小,480*272 分辨率 |
0 0 1 | 7 英寸大小,800*480 分辨率 |
0 1 0 | 7 英寸大小,1024*600 分辨率 |
0 1 1 | 7 英寸大小,1280*800 分辨率 |
1 0 0 | 4.3 英寸大小,800*480 分辨率 |
以 4.3 英寸大小,480*272 分辨率的屏幕为例,其对应的 M0/M1/M2 值为 000
7.1.1.屏幕 ID 定义
/* LCD 屏幕 ID 对应的值 */
#define ATK4342 0x4342 // 4.3 英寸,480*272 分辨率
#define ATK4384 0x4384 // 4.3 英寸,800*480 分辨率
#define ATK7084 0x7084 // 7 英寸,800*480 分辨率
#define ATK7016 0x7016 // 7 英寸,1024*600 分辨率
#define ATK1018 0x1018 // 10.1 英寸,1280*800 分辨率
#define ATKVGA 0xFF00 // VGA 接口
#define ATKUNKNOWN 0x0000
/*
* LCD 屏幕 ID
* 三个模拟开关的值 Mx 与 Panel ID 的对应关系如下:
* M0: M1: M2 --> Panel ID
* 0 : 0 : 0 --> 4.3 Inch, 480*272, ID = 0x4342
* 0 : 0 : 1 --> 7 Inch, 800*480, ID = 0x7084
* 0 : 1 : 0 --> 7 Inch, 1024*600, ID = 0x7016
* 1 : 0 : 1 --> 10.1 Inch, 1280*800, ID = 0x1018
* 1 : 0 : 0 --> 4.3 Inch, 800*480, ID = 0x4384
*/
typedef enum panel_id_t {
ID_ATK4342 = 0,
ID_ATK7084 = 1,
ID_ATK7016 = 2,
ID_ATK4384 = 4,
ID_ATK1018 = 5,
ID_ATKVGA = 7,
ID_MAX,
} panel_id_t;
7.1.2.初始化 M0~M2 的引脚(用于读取屏幕 ID)
/*
* 初始化 SGM3157 模拟开关使用的控制引脚,以及 Panel ID 相关的三个输入引脚
* Panel 的 R7、G7、B7 引脚连接了上拉/下拉电阻,用于识别屏幕 ID (屏幕 ID 由硬件厂商定义)
* Panel 引脚与芯片 GPIO 的连接如下所示 (括号中的 M0/M1/M2 为模拟开关):
* LCD_DATA23 <--> R7 (M0)
* LCD_DATA15 <--> G7 (M1)
* LCD_DATA07 <--> B7 (M2)
* 三个模拟开关的值与 Panel ID 的对应关系如下:
* M0: M1: M2 --> Panel ID
* 0 : 0 : 0 --> 4.3 Inch, 480*272, ID = 0x4342
* 0 : 0 : 1 --> 7 Inch, 800*480, ID = 0x7084
* 0 : 1 : 0 --> 7 Inch, 1024*600, ID = 0x7016
* 1 : 0 : 1 --> 10.1 Inch, 1280*800, ID = 0x1018
* 1 : 0 : 0 --> 4.3 Inch, 800*480, ID = 0x4384
*/
void lcd_panel_id_gpio_init(void)
{
/* 配置模拟开关的控制引脚:将 LCD_VSYNC 引脚配置为输出引脚 */
/*
* GPIO3_IO03 引脚功能选择为 GPIO 输出
* 配置寄存器 IOMUXC_SW_MUX_CTL_PAD_LCD_VSYNC (地址:0x020E0110)
* MUX_MODE[3:0]: 设置为 0b0101 (0x5) 引脚功能选择为 GPIO3_IO03
* SION[4]: 即 IOMUXC_SetPinMux() 的最后一个参数,
* 设置为 1 会强制引脚功能为对应的输入引脚,这里设置为 0,
* 即引脚功能由 MUX_MODE[3:0] 决定
*/
IOMUXC_SetPinMux(IOMUXC_LCD_VSYNC_GPIO3_IO03, 0);
/*
* 设置 GPIO3_IO03 引脚的电气特性
* 配置寄存器 IOMUXC_SW_PAD_CTL_PAD_LCD_VSYNC (地址:0x020E039C)
* 该寄存器中各位域的含义,以及设置值的含义如下:
* HYS[16]: 使能迟滞比较器, 0b0, 禁用
* PUS[15:14]: 上拉/下拉电阻值,0b00, 100K 下拉
* PUE[13]: 使用上拉/下拉(脉冲输出/输入) or 状态保持器(维持输出电平),0b0,维持输出电平
* PKE[12]: 状态保持器使能, 0b1, 使能
* ODE[11]: 开漏输出使能, 0b0, 禁用
* SPEED[7:6]: IO 速率, 0b10, 100MHz
* DSE[5:3]: IO 驱动能力, 0b110, R0/6
* SRE[0]: 压摆率, 0b0, 低压摆率
* 因此,最终要写入寄存器的值为:0x000010B0
*/
IOMUXC_SetPinConfig(IOMUXC_LCD_VSYNC_GPIO3_IO03, 0x000010B0);
/* 打开模拟开关 SGM3157 */
pin_cfg sgm_ctrl;
sgm_ctrl.dir = GPIO_OUTPUT; // 引脚功能为输出
sgm_ctrl.def_val = LEVEL_HIGH; // 引脚默认输出高电平
gpio_init(GPIO3, 3, &sgm_ctrl); // 初始化引脚:SGM3157 的控制引脚 LCD_VSYNC 输出高电平,打开模拟开关
/* 配置屏幕 ID 识别使用的三个引脚:LCD_DATA23、LCD_DATA15、LCD_DATA07 */
/*
* 以 LCD_DATA07 为例进行说明,将该引脚配置为 GPIO 输入
* 设置寄存器 IOMUXC_SW_MUX_CTL_PAD_LCD_DATA07 (地址:0x020E0134)
* MUX_MODE[3:0]: 设置为 0b0101 (0x5) 引脚功能选择为 GPIO3_IO12
* SION[4]:设置为 0,引脚功能由 MUX_MODE[3:0] 决定
*/
IOMUXC_SetPinMux(IOMUXC_LCD_DATA07_GPIO3_IO12, 0); // B7 (LCD_DATA07) <--> M2
IOMUXC_SetPinMux(IOMUXC_LCD_DATA15_GPIO3_IO20, 0); // G7 (LCD_DATA15) <--> M1
IOMUXC_SetPinMux(IOMUXC_LCD_DATA23_GPIO3_IO28, 0); // R7 (LCD_DATA23) <--> M0
/*
* 设置 GPIO3_IO12 引脚的电气特性
* 配置寄存器 IOMUXC_SW_PAD_CTL_PAD_LCD_DATA07(地址:0x020E03C0)
* 该寄存器中各位域的含义,以及设置值的含义如下:
* HYS[16]: 使能迟滞比较器, 0b0, 禁用
* PUS[15:14]: 上拉/下拉电阻值,0b11, 22K 上拉
* PUE[13]: 使用上拉/下拉(脉冲输出/输入) or 状态保持器(维持输出电平),0b1,脉冲输出/输入
* PKE[12]: 状态保持器使能, 0b1, 启用
* ODE[11]: 开漏输出使能, 0b0, 禁用
* SPEED[7:6]: IO 速率, 0b10, 100MHz
* DSE[5:3]: IO 驱动能力, 0b000, 禁用输出功能
* SRE[0]: 压摆率, 0b0, 低压摆率
* 因此,最终要写入寄存器的值为:0x0000F080
*/
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA07_GPIO3_IO12, 0x0000F080);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA15_GPIO3_IO20, 0x0000F080);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA23_GPIO3_IO28, 0x0000F080);
/* 初始化 LCD_DATA23、LCD_DATA15、LCD_DATA07 引脚 */
pin_cfg id_m2;
pin_cfg id_m1;
pin_cfg id_m0;
id_m2.dir = GPIO_INPUT; // LCD_DATA07 配置为输入引脚
id_m1.dir = GPIO_INPUT; // LCD_DATA15 配置为输入引脚
id_m0.dir = GPIO_INPUT; // LCD_DATA23 配置为输入引脚
gpio_init(GPIO3, 12, &id_m2); // LCD_DATA07 为 GPIO3 组的第 12 个引脚,M2
gpio_init(GPIO3, 20, &id_m1); // LCD_DATA15 为 GPIO3 组的第 20 个引脚,M1
gpio_init(GPIO3, 28, &id_m0); // LCD_DATA23 为 GPIO3 组的第 28 个引脚,M0
}
7.1.3.读取屏幕 ID
/*
* 获取 Panel ID(读取 M0、M1、M2 的值,确定 ID)
* Panel 的 R7、G7、B7 引脚连接了上拉/下拉电阻,用于识别屏幕 ID (屏幕 ID 由硬件厂商定义)
* Panel 引脚与芯片 GPIO 的连接如下所示 (括号中的 M0/M1/M2 为模拟开关):
* LCD_DATA23 <--> R7 (M0)
* LCD_DATA15 <--> G7 (M1)
* LCD_DATA07 <--> B7 (M2)
* 三个模拟开关的值与 Panel ID 的对应关系如下:
* M0: M1: M2 --> Panel ID
* 0 : 0 : 0 --> 4.3 Inch, 480*272, ID = 0x4342
* 0 : 0 : 1 --> 7 Inch, 800*480, ID = 0x7084
* 0 : 1 : 0 --> 7 Inch, 1024*600, ID = 0x7016
* 1 : 0 : 1 --> 10.1 Inch, 1280*800, ID = 0x1018
* 1 : 0 : 0 --> 4.3 Inch, 800*480, ID = 0x4384
*/
uint16_t lcd_read_panel_id(void)
{
uint8_t panel_id = 0;
uint16_t panel = 0;
uint8_t id_m0 = 0;
uint8_t id_m1 = 0;
uint8_t id_m2 = 0;
id_m0 = (uint8_t)pin_read(GPIO3, 28); // 读取 GPIO3_IO28 (LCD_DATA23),获取 M0 的值
id_m1 = (uint8_t)pin_read(GPIO3, 20); // 读取 GPIO3_IO20 (LCD_DATA15),获取 M1 的值
id_m2 = (uint8_t)pin_read(GPIO3, 12); // 读取 GPIO3_IO12 (LCD_DATA07),获取 M2 的值
/* 确认 Panel ID */
panel_id = id_m0 | (id_m1 << 1) | (id_m2 << 2);
switch (panel_id)
{
case ID_ATK4342:
panel = ATK4342; // 4.3 英寸,480*272 分辨率
break;
case ID_ATK7084:
panel = ATK7084; // 7 英寸,800*480 分辨率
break;
case ID_ATK7016:
panel = ATK7016; // 7 英寸,1024*600 分辨率
break;
case ID_ATK4384:
panel = ATK4384; // 4.3 英寸,800*480 分辨率
break;
case ID_ATK1018:
panel = ATK1018; // 10.1 英寸,1280*800 分辨率
break;
case ID_ATKVGA:
panel = ATKVGA; // VGA 模块,1366*768 分辨率
break;
default:
panel = ATKUNKNOWN;
break;
}
return panel;
}
7.1.4.主函数读取 ID 并输出至串口
LCD 初始化函数,在该函数中读取屏幕 ID 并通过串口输出:
/* LCD Panel 初始化 */
void lcd_init(void)
{
uint16_t lcd_id = 0;
lcd_panel_id_gpio_init(); // 初始化 Panel ID 相关的引脚
lcd_id = lcd_read_panel_id(); // 获取 Panel ID
printf("Panel ID = 0x%x\r\n", lcd_id); // 通过串口输出屏幕 ID
}
主函数中调用该接口:
int main(void)
{
static uint8_t led_state = ACT_OFF;
int_init(); // 中断初始化
clock_init(); // 初始化 PLL
delay_init(); // 初始化延迟
clock_enable(); // 使能所有时钟
led_init(); // 初始化 LED 使用的 GPIO
beep_init(); // 初始化蜂鸣器
uart_init(); // 串口初始化
lcd_init(); // LCD 初始化
while (1)
{
led_state = !led_state;
led_switch(LED0, led_state); // 翻转 LED
delay_ms(1000); // 延时 1s
}
return 0;
}
代码正常运行后,会通过串口打印屏幕 ID,同时开发板上的 LED 每 1 秒亮/灭一次
7.2.初始化 LCD 屏幕的数据与控制引脚
-
IMX 的引脚 LCD_DATAn (n = 00~23) 为图像数据信号引脚,分别对应 RGB 数据的 B[7:0]、G[7:0]、R[7:0];
-
IMX 的控制信号引脚 LCD_CLK、LCD_VSYNC、LCD_HSYNC、LCD_ENABLE 分别对应屏幕的 LCD_PCLK、LCD_VSYNC、LCD_HSYNC、LCD_DE 引脚;
-
屏幕的 PWM 背光控制引脚对应 IMX GPIO1_IO08 引脚,该引脚默认为高电平输出,因此上电后屏幕就会点亮;
/*
* 初始化 IMX 与屏幕连接的引脚:数据引脚和控制引脚
*
* IMX 的引脚 LCD_DATA00 ~ LCD_DATA07 连接屏幕的 LCD_B0 ~ LCD_B7 引脚,
* 用于传输 RGB 图像数据的 BLUE 通道,8-bit 长度对应 8 个引脚;
*
* IMX 的引脚 LCD_DATA08 ~ LCD_DATA015 连接屏幕的 LCD_G0 ~ LCD_G7 引脚,
* 用于传输 RGB 图像数据的 GREEN 通道,8-bit 长度对应 8 个引脚;
*
* IMX 的引脚 LCD_DATA16 ~ LCD_DATA23 连接屏幕的 LCD_R0 ~ LCD_R7 引脚,
* 用于传输 RGB 图像数据的 RED 通道,8-bit 长度对应 8 个引脚;
*
* 四个控制信号引脚 LCD_CLK, LCD_VSYNC, LCD_HSYNC, LCD_DE 连接关系如下:
* IMX 的引脚 LCD_CLK 和屏幕的 LCD_PCLK 引脚连接;
* IMX 的引脚 LCD_VSYNC 和屏幕的 LCD_VSYNC 引脚连接;
* IMX 的引脚 LCD_HSYNC 和屏幕的 LCD_HSYNC 引脚连接;
* IMX 的引脚 LCD_ENABLE 和屏幕的 LCD_DE 引脚连接;
*
* 屏幕的 BLT_PWM 引脚用于背光控制,和 IMX 的 GPIO1_IO08 引脚连接
*/
void lcd_gpio_init(void)
{
/* 设置 IMX 引脚的功能复用 */
/* IMX 的 LCD_DATA00 ~ LCD_DATA07 引脚为 RGB 图像数据的 BLUE[7:0] 通道 */
IOMUXC_SetPinMux(IOMUXC_LCD_DATA00_LCDIF_DATA00, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA01_LCDIF_DATA01, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA02_LCDIF_DATA02, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA03_LCDIF_DATA03, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA04_LCDIF_DATA04, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA05_LCDIF_DATA05, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA06_LCDIF_DATA06, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA07_LCDIF_DATA07, 0);
/* IMX 的 LCD_DATA08 ~ LCD_DATA15 引脚为 RGB 图像数据的 GREEN[7:0] 通道 */
IOMUXC_SetPinMux(IOMUXC_LCD_DATA08_LCDIF_DATA08, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA09_LCDIF_DATA09, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA10_LCDIF_DATA10, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA11_LCDIF_DATA11, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA12_LCDIF_DATA12, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA13_LCDIF_DATA13, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA14_LCDIF_DATA14, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA15_LCDIF_DATA15, 0);
/* IMX 的 LCD_DATA16 ~ LCD_DATA23 引脚为 RGB 图像数据的 RED[7:0] 通道 */
IOMUXC_SetPinMux(IOMUXC_LCD_DATA16_LCDIF_DATA16, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA17_LCDIF_DATA17, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA18_LCDIF_DATA18, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA19_LCDIF_DATA19, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA20_LCDIF_DATA20, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA21_LCDIF_DATA21, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA22_LCDIF_DATA22, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA23_LCDIF_DATA23, 0);
/* 屏幕的控制信号引脚:ENABLE, CLK, VSYNC, HSYNC */
IOMUXC_SetPinMux(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 0); // IMX 的 LCD_ENABLE 引脚连接屏幕的 LCD_DE 引脚
IOMUXC_SetPinMux(IOMUXC_LCD_HSYNC_LCDIF_HSYNC, 0); // IMX 的 LCD_HSYNC 引脚连接屏幕的 LCD_HSYNC 引脚
IOMUXC_SetPinMux(IOMUXC_LCD_VSYNC_LCDIF_VSYNC, 0); // IMX 的 LCD_VSYNC 引脚连接屏幕的 LCD_VSYNC 引脚
IOMUXC_SetPinMux(IOMUXC_LCD_CLK_LCDIF_CLK, 0); // IMX 的 LCD_CLK 引脚连接屏幕的 LCD_PCLK 引脚
/* IMX 的 GPIO1_IO08 引脚连接屏幕的背光控制引脚 BLT_PWM */
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO08_GPIO1_IO08, 0);
/*
* 设置引脚的电气特性
* 设置引脚对应的 IOMUXC_SW_PAD_CTL_PAD_LCD_xxx 寄存器
* 引脚对应寄存器位域的值,含义如下所示:
* HYS[16]: 使能迟滞比较器, 0b0, 禁用
* PUS[15:14]: 上拉/下拉电阻值,0b00, 100K 下拉
* PUE[13]: 使用上拉/下拉(脉冲输出/输入) or 状态保持器(维持输出电平),0b0,维持输出电平
* PKE[12]: 状态保持器使能, 0b0, 禁用
* ODE[11]: 开漏输出使能, 0b0, 禁用
* SPEED[7:6]: IO 速率, 0b10, 100MHz
* DSE[5:3]: IO 驱动能力, 0b111, R0/7
* SRE[0]: 压摆率, 0b1, 高压摆率
* 因此,最终要写入寄存器的值为:0x000000B9
*/
/* RGB 图像数据的 BLUE[7:0] 通道引脚的电气特性 */
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA00_LCDIF_DATA00, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA01_LCDIF_DATA01, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA02_LCDIF_DATA02, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA03_LCDIF_DATA03, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA04_LCDIF_DATA04, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA05_LCDIF_DATA05, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA06_LCDIF_DATA06, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA07_LCDIF_DATA07, 0x000000B9);
/* RGB 图像数据的 GREEN[7:0] 通道引脚的电气特性 */
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA08_LCDIF_DATA08, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA09_LCDIF_DATA09, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA10_LCDIF_DATA10, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA11_LCDIF_DATA11, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA12_LCDIF_DATA12, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA13_LCDIF_DATA13, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA14_LCDIF_DATA14, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA15_LCDIF_DATA15, 0x000000B9);
/* RGB 图像数据的 RED[7:0] 通道引脚的电气特性 */
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA16_LCDIF_DATA16, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA17_LCDIF_DATA17, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA18_LCDIF_DATA18, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA19_LCDIF_DATA19, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA20_LCDIF_DATA20, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA21_LCDIF_DATA21, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA22_LCDIF_DATA22, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA23_LCDIF_DATA23, 0x000000B9);
/* 控制信号 CLK, ENABLE, HSYNC, VSYNC 引脚的电气特性 */
IOMUXC_SetPinConfig(IOMUXC_LCD_CLK_LCDIF_CLK, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_HSYNC_LCDIF_HSYNC, 0x000000B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_VSYNC_LCDIF_VSYNC, 0x000000B9);
/* 背光控制引脚 GPIO1_IO08 引脚的电气特性 */
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO08_GPIO1_IO08, 0x000000B9);
/*
* 初始化背光控制引脚
* 背光默认打开
*/
pin_cfg bl_cfg;
bl_cfg.dir = GPIO_OUTPUT;
bl_cfg.def_val = LEVEL_HIGH; // 默认打开背光
gpio_init(GPIO1, 8, &bl_cfg);
}
屏幕初始化时,配置 LCD 相关引脚:
/* LCD Panel 初始化 */
void lcd_init(void)
{
uint16_t lcd_id = 0;
lcd_panel_id_gpio_init(); // 初始化 Panel ID 相关的引脚
lcd_id = lcd_read_panel_id(); // 获取 Panel ID
printf("Panel ID = 0x%x\r\n", lcd_id); // 通过串口输出屏幕 ID
lcd_gpio_init(); // 初始化数据、控制、背光引脚
}
7.3.eLCDIF 模块复位
eLCDIF 模块的 LCDIF_CTRL 寄存器的 SFTRST[31] 为复位控制位:
-
SFTRST[31] 置 1 强制复位;
-
eLCDIF 正常运行时必须将该位置 0;
/*
* eLCDIF 模块复位
* eLCDIF 模块的 LCDIF_CTRL 寄存器的 SFTRST[31] 为复位控制位
* SFTRST[31] 置 1 强制复位
* eLCDIF 正常运行时必须将该位置 0
*/
void lcd_reset(void)
{
LCDIF->CTRL |= (1 << 31); // eLCDIF 模块复位
}
void lcd_noreset(void)
{
LCDIF->CTRL &= ~(1 << 31); // eLCDIF 模块取消复位
}
LCD 各引脚初始化完成后,复位 eLCDIF 模块:
/* LCD Panel 初始化 */
void lcd_init(void)
{
uint16_t lcd_id = 0;
lcd_panel_id_gpio_init(); // 初始化 Panel ID 相关的引脚
lcd_id = lcd_read_panel_id(); // 获取 Panel ID
printf("Panel ID = 0x%x\r\n", lcd_id); // 通过串口输出屏幕 ID
lcd_gpio_init(); // 初始化数据、控制、背光引脚
lcd_reset(); // eLCDIF 模块复位
delay_ms(10); // 等待 10ms,等待 eLCDIF 模块复位完成
lcd_noreset(); // 取消 eLCDIF 模块复位
}
7.4.eLCDIF 模块使能
eLCDIF 模块的 LCDIF_CTRL 寄存器的 RUN[0] 为使能控制位:
-
RUN[0] 置 1 使能 eLCDIF,开始将数据从 SoC 传输至屏幕;
-
RUN[0] 置 0 禁用 eLCDIF 模块;
/*
* eLCDIF 模块使能
* eLCDIF 模块的 LCDIF_CTRL 寄存器的 RUN[0] 为使能控制位
* RUN[0] 置 1 使能 eLCDIF,开始将数据从 SoC 传输至屏幕
* RUN[0] 置 0 禁用 eLCDIF 模块
*/
void lcd_enable(void)
{
LCDIF->CTRL |= (1 << 0); // eLCDIF 模块使能
}
void lcd_disable(void)
{
LCDIF->CTRL &= ~(1 << 0); // eLCDIF 模块禁用
}
7.5.eLCDIF 时钟初始化
eLCDIF 的时钟源选择为 PLL5 (VIDEO PLL),根据屏幕大小,设置合适的时钟频率:
/*
* eLCDIF 时钟初始化
*
* eLCDIF 的时钟源为 PLL5,其时钟频率计算公式如下:
* PLL5_CLK = OSC24M * (loopDivider + (numerator / denominator))
* 这里为了简化计算,不使用小数分频,因此不设置 denominator 和 numerator
* 因此,公式简化为:
* PLL5_CLK = OSC24M * (loopDivider)
* 其中,loopDivider 的值由寄存器 CCM_ANALOG_PLL_VIDEO 的 DIV_SELECT[6:0] 设置
*
* 时钟源 PLL5 输出时可以进行分频
* 由寄存器 CCM_ANALOG_PLL_VIDEO 的 POST_DIV_SELECT[20:19] 位域,
* 以及寄存器 CCM_ANALOG_MISC2 的 VIDEO_DIV[31:30] 共同决定
* 输出频率的计算方式如下:
* OUTPUT_CLK = PLL5_CLK / POST_DIV / VIDEO_DIV
* = PLL5_CLK / postDivider
*
* eLCDIF 接口的时钟频率由时钟源经过分频器 1 和分频器 2 分频获得
* 分频器 1 由寄存器 CCM_CSCDR2 的 LCDIF1_PRED[14:12] 位域控制,可以设置 0b000~0b111,对应 1~8 分频
* 分频器 2 由寄存器 CCM_CBCMR 的 LCDIF1_PODF[25:23] 位域控制,可以设置 0b000~0b111,对应 1~8 分频
* 因此,eLCDIF 的输出频率为:
* CLK = OUTPUT_CLK / PRED / PODF
*/
void lcd_clk_init(uint8_t loop_div, uint8_t pre_div, uint8_t post_div)
{
/* 设置时钟源 PLL5 的频率 */
/*
* 不使用小数分频,因此:
* CCM_ANALOG_PLL_VIDEO_DENOM 寄存器的 B[29:0] 位域设置为 1
* CCM_ANALOG_PLL_VIDEO_NUM 寄存器的 A[29:0] 位域设置为 0
*/
CCM_ANALOG->PLL_VIDEO_DENOM = 1; // 分母设置为 1
CCM_ANALOG->PLL_VIDEO_NUM = 0; // 分子设置为 0
/*
* 设置时钟源 PLL5 的 loopDivider
* CCM_ANALOG_PLL_VIDEO 寄存器的 DIV_SELECT[6:0] 位域设置 loopDivider 的值
*/
CCM_ANALOG->PLL_VIDEO &= ~(0x7F << 0); // 清空 DIV_SELECT[6:0]
CCM_ANALOG->PLL_VIDEO |= (loop_div << 0); // 将 DIV_SELECT[6:0] 设置为用户给定的值 loop_div
/*
* 设置时钟源 PLL5 输出频率的 postDivider
* postDivider 由寄存器 CCM_ANALOG_PLL_VIDEO 的 POST_DIV_SELECT[20:19] 位域,
* 以及寄存器 CCM_ANALOG_MISC2 的 VIDEO_DIV[31:30] 位域共同决定
*/
CCM_ANALOG->PLL_VIDEO &= ~(0b11 << 19); // 清空 POST_DIV_SELECT[20:19]
CCM_ANALOG->PLL_VIDEO |= (0b10 << 19); // POST_DIV_SELECT[20:19] 设置为 0b10 (1 分频)
CCM_ANALOG->MISC2 &= ~(0b11 << 30); // 清空 VIDEO_DIV[31:30]
CCM_ANALOG->MISC2 |= (0b00 << 30); // VIDEO_DIV[31:30] 设置为 0b00 (1 分频)
/* 使能 VIDEO PLL (PLL5) 时钟输出:ENABLE[13] 置 1 */
CCM_ANALOG->PLL_VIDEO |= (0b1 << 13);
/*
* eLCDIF 的时钟源选择 PLL5 (VIDEO PLL)
* 寄存器 CCM_CSCDR2 的 LCDIF1_PRE_CLK_SEL[17:15] 设置 eLCDIF 的时钟源
* 将其设置为 0b010,选择 PLL5 (VIDEO PLL) 作为 eLCDIF 的时钟源
*/
CCM->CSCDR2 &= ~(0b111 << 15); // 清空 LCDIF1_PRE_CLK_SEL[17:15]
CCM->CSCDR2 |= (0b010 << 15); // 选择 PLL5 (VIDEO PLL) 作为 eLCDIF 的时钟源
/*
* 设置 eLCDIF 接口的时钟频率
* 分频器 1 由寄存器 CCM_CSCDR2 的 LCDIF1_PRED[14:12] 位域控制,可以设置 0b000~0b111,对应 1~8 分频
* 分频器 2 由寄存器 CCM_CBCMR 的 LCDIF1_PODF[25:23] 位域控制,可以设置 0b000~0b111,对应 1~8 分频
*/
/* LCDIF1_PRED[14:12] 设置为用户传入的值 pre_div */
CCM->CSCDR2 &= ~(0b111 << 12); // 清空 LCDIF1_PRED[14:12]
CCM->CSCDR2 |= ((pre_div - 1) << 12); // 设置 LCDIF1_PRED[14:12] 为用户传入的值 pre_div
/* LCDIF1_PODF[25:23] 设置为用户传入的值 post_div */
CCM->CBCMR &= ~(0b111 << 23); // 清空 LCDIF1_PODF[25:23]
CCM->CBCMR |= ((post_div - 1) << 23); // 设置 LCDIF1_PODF[25:23] 为用户传入的值 post_div
/* eLCDIF 的时钟源选择 PLL5 输出的时钟源 */
CCM->CSCDR2 &= ~(0b111 << 9); // 清空 LCDIF1_CLK_SEL[11:9]
CCM->CSCDR2 |= (0b000 << 9);
}
7.6.设置屏幕参数
7.6.1.屏幕参数结构体
使用结构体保存屏幕的参数,包括高度、宽度、HBP、VFP 等:
/* LCD 屏幕参数结构体 */
typedef struct tft_lcd_t{
uint16_t height; // 屏幕高度
uint16_t width; // 屏幕宽度
uint8_t pixel_size; // 每个像素占用大小
// VSPW, VBP, VFP
uint16_t vspw;
uint16_t vbp;
uint16_t vfp;
// HSPW, HBF, HFP
uint16_t hspw;
uint16_t hbp;
uint16_t hfp;
uint32_t buf; // 显存起始地址
uint32_t forecolor; // 屏幕前景色
uint32_t backcolor; // 屏幕背景色
uint32_t id; // 屏幕 ID
} tft_lcd_t;
/* 屏幕参数 */
tft_lcd_t tft_lcd;
7.6.2.显存起始地址
默认显存起始地址:
/* LCD 显存首地址 */
#define LCD_FRAMEBUF_ADDR (0x89000000)
7.6.3.常用颜色定义
常用颜色定义:
/* 屏幕颜色定义 */
#define LCD_BLUE 0x000000FF
#define LCD_GREEN 0x0000FF00
#define LCD_RED 0x00FF0000
#define LCD_CYAN 0x0000FFFF
#define LCD_MAGENTA 0x00FF00FF
#define LCD_YELLOW 0x00FFFF00
#define LCD_LIGHTBLUE 0x008080FF
#define LCD_LIGHTGREEN 0x0080FF80
#define LCD_LIGHTRED 0x00FF8080
#define LCD_LIGHTCYAN 0x0080FFFF
#define LCD_LIGHTMAGENTA 0x00FF80FF
#define LCD_LIGHTYELLOW 0x00FFFF80
#define LCD_DARKBLUE 0x00000080
#define LCD_DARKGREEN 0x00008000
#define LCD_DARKRED 0x00800000
#define LCD_DARKCYAN 0x00008080
#define LCD_DARKMAGENTA 0x00800080
#define LCD_DARKYELLOW 0x00808000
#define LCD_WHITE 0x00FFFFFF
#define LCD_LIGHTGRAY 0x00D3D3D3
#define LCD_GRAY 0x00808080
#define LCD_DARKGRAY 0x00404040
#define LCD_BLACK 0x00000000
#define LCD_BROWN 0x00A52A2A
#define LCD_ORANGE 0x00FFA500
#define LCD_TRANSPARENT 0x00000000
7.6.4.初始化屏幕参数
LCD 屏幕初始化时,根据读取的 ID 值,初始化对应的屏幕参数:
/* LCD Panel 初始化 */
void lcd_init(void)
{
uint16_t lcd_id = 0;
lcd_panel_id_gpio_init(); // 初始化 Panel ID 相关的引脚
lcd_id = lcd_read_panel_id(); // 获取 Panel ID
printf("Panel ID = 0x%x\r\n", lcd_id); // 通过串口输出屏幕 ID
lcd_gpio_init(); // 初始化数据、控制、背光引脚
lcd_reset(); // eLCDIF 模块复位
delay_ms(10); // 等待 10ms,等待 eLCDIF 模块复位完成
lcd_noreset(); // 取消 eLCDIF 模块复位
/* 屏幕参数初始化:根据 ID 设置对应的屏幕参数 */
if (lcd_id == ATK4342) { // 4.3 英寸大小,480*272 分辨率
tft_lcd.height = 272;
tft_lcd.width = 481;
tft_lcd.vspw = 1;
tft_lcd.vbp = 8;
tft_lcd.vfp = 8;
tft_lcd.hspw = 1;
tft_lcd.hbp = 40;
tft_lcd.hfp = 5;
lcd_clk_init(27, 8, 8); // eLCDIF 时钟频率初始化:10.1 MHz
} else if (lcd_id == ATK4384) { // 4.3 英寸大小, 800*480 分辨率
tft_lcd.height = 480;
tft_lcd.width = 800;
tft_lcd.vspw = 3;
tft_lcd.vbp = 32;
tft_lcd.vfp = 13;
tft_lcd.hspw = 48;
tft_lcd.hbp = 88;
tft_lcd.hfp = 40;
lcd_clk_init(42, 4, 8); // eLCDIF 时钟频率初始化:31.5MHz
} else if (lcd_id == ATK7084) { // 7 英寸大小,800*480 分辨率
tft_lcd.height = 480;
tft_lcd.width = 800;
tft_lcd.vspw = 1;
tft_lcd.vbp = 23;
tft_lcd.vfp = 22;
tft_lcd.hspw = 1;
tft_lcd.hbp = 46;
tft_lcd.hfp = 210;
lcd_clk_init(30, 3, 7); // eLCDIF 时钟频率初始化:34.2MHz
} else if (lcd_id == ATK7016) { // 7 英寸大小,1024*600 分辨率
tft_lcd.height = 600;
tft_lcd.width = 1024;
tft_lcd.vspw = 3;
tft_lcd.vbp = 20;
tft_lcd.vfp = 12;
tft_lcd.hspw = 20;
tft_lcd.hbp = 140;
tft_lcd.hfp = 160;
lcd_clk_init(32, 3, 5); // eLCDIF 时钟频率初始化:51.2MHz
} else if (lcd_id == ATK1018) { // 10.1 英寸大小,1280*800 分辨率
tft_lcd.height = 800;
tft_lcd.width = 1280;
tft_lcd.vspw = 3;
tft_lcd.vbp = 10;
tft_lcd.vfp = 10;
tft_lcd.hspw = 10;
tft_lcd.hbp = 80;
tft_lcd.hfp = 70;
lcd_clk_init(35, 3, 5); // eLCDIF 时钟频率初始化:56MHz
} else if (lcd_id == ATKVGA) { // VGA 接口,1366*768 分辨率
tft_lcd.height = 768;
tft_lcd.width = 1366;
tft_lcd.vspw = 3;
tft_lcd.vbp = 24;
tft_lcd.vfp = 3;
tft_lcd.hspw = 143;
tft_lcd.hbp = 213;
tft_lcd.hfp = 70;
lcd_clk_init(32, 3, 3); // eLCDIF 时钟频率初始化:85MHz
}
tft_lcd.id = lcd_id; // 屏幕 ID
tft_lcd.pixel_size = PIXEL_SIZE_ARGB8888; // 像素点大小
tft_lcd.framebuffer = LCD_FRAMEBUF_ADDR; // 显存首地址
tft_lcd.backcolor = LCD_WHITE; // 背景色为黑色
tft_lcd.forecolor = LCD_WHITE; // 前景色为白色
}
7.7.初始化 eLCDIF 接口
初始化 eLCDIF 模块,使用 DOTCLK 模式,配置相关寄存器如 LCDIF_CTRL、LCDIF_VDCTRL0 等:
/* LCD Panel 初始化 */
void lcd_init(void)
{
...
/* 初始化 eLCDIF 接口 */
/*
* 配置 LCDIF_CTRL 寄存器,相关位域的配置值及含义如下:
* SFTRST[31]: 软件复位控制位,设置为 0,停止复位
* BYPASS_COUNT[19]: 使能旁路计数器模式,设置为 1,使能计数器
* DOTCLK_MODE[17]: DOTCLK 模式使能,设置为1,LCD 工作在 DOTCLK 模式
* INPUT_DATA_SWIZZLE[15:14]: 输入数据位置交换,设置为 0b00,输入数据不交换位置
* CSC_DATA_SWIZZLE[13:12]: CSC 数据位置交换,设置为 0b00,CSC 不交换位置
* LCD_DATABUS_WIDTH[11:10]: 总线数据宽度,设置为 0b11,24 位总线宽度
* WORD_LENGTH[9:8]: 输入数据宽度,设置为 0b11,24 位数据宽度,即 RGB888
* MASTER[5]: 主机模式,设置为 1,即 eLCDIF 工作在主机模式
* DATA_FORMAT_24_BIT[1]: 输入数据格式,设置为 0,即所有的 24 位均有效
*/
LCDIF->CTRL &= ~(0xFFFFFFFF); // 清空 LCDIF_CTRL 寄存器
LCDIF->CTRL |= (1 << 19) | (1 << 17) | (3 << 10) | (3 << 8) | (1 << 5);
printf("[lcd_init] LCDIF->CTRL = 0x%x\r\n", LCDIF->CTRL);
/*
* 配置 LCDIF_CTRL1 寄存器,相关位域的配置值及含义如下:
* BYTE_PACKING_FORMAT[19:16]:有效数据标记,设置为 0b0111,即 ARGB8888 格式,不使用 Alpha 通道
*/
LCDIF->CTRL1 &= ~(0b1111 << 16); // 清空 BYTE_PACKING_FORMAT[19:16]
LCDIF->CTRL1 |= (0b0111 << 16); // 设置输入格式为 ARGB888,不使用 Alpha 通道的数据
/*
* 配置 LCDIF_TRANSFER_COUNT 寄存器,设置屏幕分辨率,相关位域的配置值及含义如下:
* V_COUNT[31:16]:每一帧图像包含的有效行的数量
* H_COUNT[15:0]:每一行包含的有效像素点的数量(即有效列的数量)
*/
LCDIF->TRANSFER_COUNT &= ~(0xFFFFFFFF); // 清空 LCDIF_TRANSFER_COUNT 寄存器
LCDIF->TRANSFER_COUNT |= ((tft_lcd.height << 16) | (tft_lcd.width << 0)); // 设置分辨率
/*
* VSYNC/DOTCLK 模式控制寄存器 LCDIF_VDCTRL0 配置,相关位域的配置值及含义如下:
* VSYNC_OEB[29]: 0 : VSYNC 为输出信号
* ENABLE_PRESENT[28]: 1 : 使能 ENABLE 信号输出
* VSYNC_POL[27]: 0 : VSYNC 低电平有效
* HSYNC_POL[26]: 0 : HSYNC 低电平有效
* DOTCLK_POL[25]: 0 : DOTCLK 上升沿有效
* ENABLE_POL[24]: 1 : ENABLE 信号高电平有效
* VSYNC_PERIOD_UNIT[21]: 1 : DOTCLK 模式下设置为 1
* VSYNC_PULSE_WIDTH_UNIT[20]: 1 : DOTCLK 模式下设置为 1
* VSYNC_PULSE_WIDTH[17:0]: VSPW : VSYNC 信号的宽度
*/
LCDIF->VDCTRL0 &= ~(0xFFFFFFFF); // 清空 LCDIF_VDCTRL0 寄存器
if (lcd_id == ATKVGA) { // VGA 需要特殊处理
LCDIF->VDCTRL0 |= (1 << 28) | (1 << 25) | (1 << 21) | (1 << 20) | (tft_lcd.vspw << 0);
} else {
LCDIF->VDCTRL0 |= (1 << 28) | (1 << 24) | (1 << 21) | (1 << 20) | (tft_lcd.vspw << 0);
}
/*
* 配置 LCDIF_VDCTRL1 寄存器,设置 VSYNC 信号的周期
* VSYNC_PERIOD[31:0]:设置一个 VSYNC 信号的周期(一个低电平时间加上一个高电平的总时间),即:屏幕高度 + VSPW + VBP + VFP
*/
LCDIF->VDCTRL1 &= ~(0xFFFFFFFF); // 清空 LCDIF_VDCTRL1 寄存器
LCDIF->VDCTRL1 |= (tft_lcd.height + tft_lcd.vspw + tft_lcd.vfp + tft_lcd.vbp);
/*
* 配置 LCDIF_VDCTRL2 寄存器,设置 HSYNC 信号的宽度和周期,相关位域的配置值及含义如下:
* HSYNC_PULSE_WIDTH[31:18]:HSYNC 信号低电平的宽度,即 HSPW
* HSYNC_PERIOD[17:0]:一个 HSYNC 信号的周期(一个低电平时间加上一个高电平的总时间)
*/
LCDIF->VDCTRL2 &= ~(0xFFFFFFFF); // 清空 LCDIF_VDCTRL2 寄存器
LCDIF->VDCTRL2 |= ((tft_lcd.hspw << 18) |
(tft_lcd.width + tft_lcd.hspw + tft_lcd.hfp + tft_lcd.hbp));
/*
* 配置 LCDIF_VDCTRL3 寄存器,设置 VSYNC 和 HSYNC 信号的 Porch 时间,相关位域的配置值及含义如下:
* HORIZONTAL_WAIT_CNT[27:16]:DOTCLK 模式中,为 HSYNC 信号发送有效数据前的等待时间,即 HSPW + HBP
* VERTICAL_WAIT_CNT[15:0]:DOTCLK 模式中,为 VSPW + VBP
*/
LCDIF->VDCTRL3 &= ~(0xFFFFFFFF); // 清空 LCDIF_VDCTRL3 寄存器
LCDIF->VDCTRL3 |= ((tft_lcd.hbp + tft_lcd.hspw) << 16) | (tft_lcd.vbp + tft_lcd.vspw);
/*
* 配置 LCDIF_VDCTRL4 寄存器,DOTCLK 模式控制,相关位域的配置值及含义如下:
* SYNC_SIGNALS_ON[18]:DOTCLK 模式中该位为 1
* DOTCLK_H_VALID_DATA_CNT[17:0]:DOTCLK 模式中,为一行所包含的有效像素的数量,即 LCD 屏幕的宽度
*/
LCDIF->VDCTRL4 &= ~(0xFFFFFFFF); // 清空 LCDIF_VDCTRL4 寄存器
LCDIF->VDCTRL4 |= ((1 << 18) | (tft_lcd.width));
/*
* 设置 eLCDIF 接口显存的首地址,配置 LCDIF_CUR_BUF 和 LCDIF_NEXT_BUF 寄存器
*/
LCDIF->CUR_BUF = (uint32_t)tft_lcd.buf;
LCDIF->NEXT_BUF = (uint32_t)tft_lcd.buf;
/* 屏幕配置完成后使能 eLCDIF 并清除屏幕 */
lcd_enable(); // eLCDIF 使能
delay_ms(10); // 延时 10ms
lcd_clear(LCD_RED); // 清屏 (将屏幕设置为指定颜色)
}
eLCDIF 初始化完成后,测试屏幕是否能正常显示,这里调用 lcd_clear() 将屏幕设置为红色:
/* 清屏接口 (将全屏设置为指定的颜色) */
void lcd_clear(uint32_t color)
{
uint32_t *buf_addr; // 显存首地址
uint32_t frame_size; // 一帧图像包含多少个像素点
uint32_t pixel = 0;
buf_addr = (uint32_t *)tft_lcd.buf; // 获取显存起始地址
frame_size = (uint32_t)(tft_lcd.width * tft_lcd.height); // 计算一帧图像包含多少个像素点
for (pixel = 0; pixel < frame_size; pixel++) {
buf_addr[pixel] = color; // 将每个像素点填充为指定的颜色
}
}
7.8.画图 API
7.8.1.绘制矩形区域
将指定位置的像素点设置为指定颜色:
/* 在屏幕上指定位置画一个点 */
inline void lcd_draw_point(uint32_t x, uint32_t y, uint32_t color)
{
uint32_t *buf_addr;
uint32_t *target_addr;
uint32_t position;
buf_addr = (uint32_t *)tft_lcd.buf; // 获取显存的起始地址
position = tft_lcd.pixel_size * (tft_lcd.width * y + x);
target_addr = (uint32_t *)((uint32_t)buf_addr + position); // 计算目标地址
*target_addr = color;
}
读取指定位置像素点的颜色:
/* 读取指定像素点的颜色 */
inline uint32_t lcd_read_point(uint32_t x, uint32_t y)
{
uint32_t *buf_addr;
uint32_t *target_addr;
uint32_t position;
buf_addr = (uint32_t *)tft_lcd.buf; // 获取显存的起始地址
position = tft_lcd.pixel_size * (tft_lcd.width * y + x);
target_addr = (uint32_t *)((uint32_t)buf_addr + position); // 计算目标地址
return *target_addr;
}
在屏幕上绘制一个纯色的矩形区域:
/* 绘制一个矩形块,矩形的左上角点坐标为 (x0, y0),右下角点坐标为 (x1, y1) */
void lcd_draw_rectangle(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint32_t color)
{
uint8_t x = 0;
uint8_t y = 0;
x0 = (x0 < 0) ? 0 : x0;
y0 = (y0 < 0) ? 0 : y0;
x1 = (x1 > tft_lcd.width) ? tft_lcd.width : x1;
y1 = (y1 > tft_lcd.height) ? tft_lcd.height : y1;
for (y = y0; y <= y1; y++) {
for (x = x0; x <= x1; x++) {
lcd_draw_point(x, y, color);
}
}
}
7.8.2.绘制线条
/* 绘制线条 */
void lcd_draw_line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
u16 t;
uint32_t xerr = 0, yerr = 0, delta_x, delta_y, distance;
uint32_t incx, incy, uRow, uCol;
delta_x = x2 - x1; /* 计算坐标增量 */
delta_y = y2 - y1;
uRow = x1;
uCol = y1;
if (delta_x > 0) /* 设置单步方向 */
incx = 1;
else if (delta_x == 0) /* 垂直线 */
incx = 0;
else
{
incx = -1;
delta_x = -delta_x;
}
if (delta_y > 0)
incy = 1;
else if (delta_y == 0) /* 水平线 */
incy = 0;
else
{
incy = -1;
delta_y = -delta_y;
}
if (delta_x > delta_y) /* 选取基本增量坐标轴 */
distance = delta_x;
else
distance = delta_y;
for (t = 0; t <= distance + 1; t++) /* 画线输出 */
{
lcd_draw_point(uRow, uCol, tft_lcd.forecolor); /* 画点 */
xerr += delta_x ;
yerr += delta_y ;
if (xerr > distance)
{
xerr -= distance;
uRow += incx;
}
if (yerr > distance)
{
yerr -= distance;
uCol += incy;
}
}
}
7.8.3.绘制矩形框
/* 绘制矩形框 */
void lcd_draw_rectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
lcd_draw_line(x1, y1, x2, y1);
lcd_draw_line(x1, y1, x1, y2);
lcd_draw_line(x1, y2, x2, y2);
lcd_draw_line(x2, y1, x2, y2);
}
7.8.4.绘制圆形
/* 绘制圆形 */
void lcd_draw_circle(uint16_t x0, uint16_t y0, uint8_t r)
{
int32_t mx = x0, my = y0;
int32_t x = 0, y = r;
int32_t d = 1 - r;
while (y > x) /* y > x 即第一象限的第 1 区八分圆 */
{
lcd_draw_point(x + mx, y + my, tft_lcd.forecolor);
lcd_draw_point(y + mx, x + my, tft_lcd.forecolor);
lcd_draw_point(-x + mx, y + my, tft_lcd.forecolor);
lcd_draw_point(-y + mx, x + my, tft_lcd.forecolor);
lcd_draw_point(-x + mx, -y + my, tft_lcd.forecolor);
lcd_draw_point(-y + mx, -x + my, tft_lcd.forecolor);
lcd_draw_point(x + mx, -y + my, tft_lcd.forecolor);
lcd_draw_point(y + mx, -x + my, tft_lcd.forecolor);
if ( d < 0)
{
d = d + 2 * x + 3;
}
else
{
d = d + 2 * (x - y) + 5;
y--;
}
x++;
}
}
7.8.5.显示一个字符
/* 指定位置显示 1 个字符 */
void lcd_show_char(u16 x, u16 y, u8 num, u8 size, u8 mode)
{
u8 temp, t1, t;
u16 y0 = y;
u8 csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); /* 计算字体一个字符对应点阵集所占的字节数 */
num = num - ' '; /* 计算偏移后的值,ASCII 字库从空格开始取模,所以 -' ' 就是对应字符的字库 */
for (t = 0; t < csize; t++)
{
if (size == 12) temp = asc2_1206[num][t]; /* 调用 1206 字体 */
else if (size == 16)temp = asc2_1608[num][t]; /* 调用 1608 字体 */
else if (size == 24)temp = asc2_2412[num][t]; /* 调用 2412 字体 */
else if (size == 32)temp = asc2_3216[num][t]; /* 调用 3216 字体 */
else return; /* 未找到对应字库 */
for (t1 = 0; t1 < 8; t1++)
{
if (temp & 0x80)lcd_draw_point(x, y, tft_lcd.forecolor);
else if (mode==0)lcd_draw_point(x, y, tft_lcd.backcolor);
temp <<= 1;
y++;
if (y >= tft_lcd.height) return; /* 超出区域 */
if ((y - y0) == size)
{
y = y0;
x++;
if (x >= tft_lcd.width) return; /* 超出区域 */
break;
}
}
}
}
7.8.6.显示字符串
/* 显示字符串 */
void lcd_show_string(u16 x, u16 y, u16 width, u16 height, u8 size, u8 *p)
{
u8 x0 = x;
width += x;
height += y;
while((*p <= '~') && (*p >= ' ')) /* 判断是不是非法字符! */
{
if (x >= width) {x = x0; y += size;}
if (y >= height) break; /* 退出 */
lcd_show_char(x, y, *p , size, 0);
x += size / 2;
p++;
}
}
7.8.7.计算 n 次方
/* 计算 m 的 n 次方 */
uint32_t lcd_pow(uint8_t m, uint8_t n)
{
uint32_t result = 1;
while (n--) result *= m;
return result;
}
7.8.8.显示数字(高位为 0 不显示)
/* 显示指定的数字,高位为 0 不显示 */
void lcd_show_num(u16 x, u16 y, u32 num, u8 len, u8 size)
{
u8 t, temp;
u8 enshow = 0;
for (t = 0; t < len; t++)
{
temp = (num / lcd_pow(10, len - t - 1)) % 10;
if (enshow == 0 && t < (len - 1))
{
if (temp == 0)
{
lcd_show_char(x + (size / 2) * t, y, ' ', size, 0);
continue;
} else {
enshow = 1;
}
}
lcd_show_char(x + (size / 2) * t, y, temp + '0', size, 0);
}
}
7.8.9.显示数字(高位为 0 显示)
/* 显示指定的数字,高位为 0 也显示 */
void lcd_show_xnum(u16 x, u16 y, u32 num, u8 len, u8 size, u8 mode)
{
u8 t, temp;
u8 enshow = 0;
for (t = 0; t < len; t++)
{
temp = (num / lcd_pow(10, len - t- 1)) % 10;
if (enshow == 0 && t < (len - 1))
{
if (temp == 0)
{
if (mode & 0X80) lcd_show_char(x + (size / 2) * t, y, '0', size, mode & 0X01);
else lcd_show_char(x + (size / 2) * t, y , ' ', size, mode & 0X01);
continue;
} else {
enshow=1;
}
}
lcd_show_char( x + (size / 2) * t, y, temp + '0' , size , mode & 0X01);
}
}
7.9.主函数
/* 背景色索引 */
uint32_t backcolor[10] = {
LCD_BLUE, LCD_GREEN, LCD_RED, LCD_CYAN, LCD_YELLOW,
LCD_LIGHTBLUE, LCD_DARKBLUE, LCD_WHITE, LCD_BLACK, LCD_ORANGE
};
int main(void)
{
static uint8_t led_state = ACT_OFF;
uint32_t color = 0;
uint8_t index = 0;
int_init(); // 中断初始化
clock_init(); // 初始化 PLL
delay_init(); // 初始化延迟
clock_enable(); // 使能所有时钟
led_init(); // 初始化 LED 使用的 GPIO
beep_init(); // 初始化蜂鸣器
uart_init(); // 串口初始化
lcd_init(); // LCD 初始化
// 测试屏幕四个角显示是否正常
lcd_draw_point(1, 0, LCD_GREEN);
lcd_draw_point(tft_lcd.width - 1, 0, LCD_GREEN);
lcd_draw_point(1, tft_lcd.height - 1, LCD_GREEN);
lcd_draw_point(tft_lcd.width - 1, tft_lcd.height - 1, LCD_GREEN);
// 读取指定位置的颜色
color = lcd_read_point(1, 0);
printf("Position (1, 0) color = 0x%x", color);
delay_ms(1000);
// 显示字符串
tft_lcd.forecolor = LCD_BLACK; // 前景色设置为黑色
while (1)
{
lcd_clear(backcolor[index]);
delay_ms(10);
lcd_show_string(10, 40, 260, 32, 32, (u8*)"ALPHA IMX6U");
lcd_show_string(10, 80, 240, 24, 24, (u8*)"RGBLCD TEST");
lcd_show_string(10, 110, 240, 16, 16, (u8*)"~MaoXian~");
lcd_draw_circle(140, 200, 50);
index++;
index = (index == 10) ? 0 : index;
led_state = !led_state;
led_switch(LED0, led_state); // 翻转 LED
delay_ms(1000); // 延时 1s
}
return 0;
}
8.问题记录
8.1.屏幕参数设置完成后,无法正常将屏幕背景色设置为红色
未能将屏幕设置为红色,判断初始化屏幕参数不成功
正点原子自己的例程正常运行,将所有寄存器的值打印出来:
// 正点原子例程,可以正常运行
LCD ID=0x4342
[lcd_clk_init] CCM_ANALOG->PLL_VIDEO = 0x10201B
CCM_ANALOG->PLL_VIDEO_NUM = 0x0
CCM_ANALOG->PLL_VIDEO_DENOM = 0x0
CCM_ANALOG->MISC2 = 0x80008
CCM->CSCDR2 = 0x8010201B
CCM->CBCMR = 0x8010201B
LCDIF->CTRL = 0xA0F20
LCDIF->CTRL1 = 0x70100
LCDIF->TRANSFER_COUNT = 0x11001E0
LCDIF->VDCTRL0 = 0x11300001
LCDIF->VDCTRL1 = 0x121
LCDIF->VDCTRL2 = 0x4020E
LCDIF->VDCTRL3 = 0x290009
LCDIF->VDCTRL4 = 0x401E0
LCDIF->CUR_BUF = 0x89000000
LCDIF->NEXT_BUF = 0x89000000
在自己编写的程序中,打印所有的寄存器值:
// 自己的代码,异常
Panel ID = 0x4342
[lcd_clk_init] CCM_ANALOG->PLL_VIDEO = 0x10201B
CCM_ANALOG->PLL_VIDEO_NUM = 0x0
CCM_ANALOG->PLL_VIDEO_DENOM = 0x1
CCM_ANALOG->MISC2 = 0x672767
CCM->CSCDR2 = 0x8010201B
CCM->CBCMR = 0x8010201B
LCDIF->CTRL = 0x40000000
LCDIF->CTRL1 = 0xF0000
LCDIF->TRANSFER_COUNT = 0x10000
LCDIF->VDCTRL0 = 0x0
LCDIF->VDCTRL1 = 0x0
LCDIF->VDCTRL2 = 0x0
LCDIF->VDCTRL3 = 0x0
LCDIF->VDCTRL4 = 0x0
LCDIF->CUR_BUF = 0x0
LCDIF->NEXT_BUF = 0x0
首先,CCM_ANALOG->MISC2 寄存器的值不一样,通过查阅手册,发现这些值均和欠压保护相关,暂时不管
接下来是 LCDIF->CTRL 寄存器,发现该寄存器的 CLKGATE[30] 位被至 1,该位置 1 后会关闭 eLCDIF 的时钟:
因此,在代码中写 CTRL 寄存器前,先将其清 0:
LCDIF->CTRL &= ~(0xFFFFFFFF);
LCDIF->CTRL |= (1 << 19) | (1 << 17) | (3 << 10) | (3 << 8) | (1 << 5);
printf("[lcd_init] LCDIF->CTRL = 0x%x\r\n", LCDIF->CTRL);
修改后,寄存器的值可以正常设置,屏幕按照设置显示为红色:
// 修改后,显示正常
Panel ID = 0x4342
[lcd_clk_init] CCM_ANALOG->PLL_VIDEO = 0x10201B
CCM_ANALOG->PLL_VIDEO_NUM = 0x0
CCM_ANALOG->PLL_VIDEO_DENOM = 0x1
CCM_ANALOG->MISC2 = 0x672767
CCM->CSCDR2 = 0x8010201B
CCM->CBCMR = 0x8010201B
[lcd_init] LCDIF->CTRL = 0xA0F20
LCDIF->CTRL = 0xA0F20
LCDIF->CTRL1 = 0x70100
LCDIF->TRANSFER_COUNT = 0x11001E0
LCDIF->VDCTRL0 = 0x11300001
LCDIF->VDCTRL1 = 0x121
LCDIF->VDCTRL2 = 0x4020E
LCDIF->VDCTRL3 = 0x290009
LCDIF->VDCTRL4 = 0x401E0
LCDIF->CUR_BUF = 0x89000000
LCDIF->NEXT_BUF = 0x89000000
8.2.屏幕边缘存在一条白线
设置完屏幕参数后,将屏幕背景色设置为红色,此时屏幕边缘出现一条白线,如下图所示:
使用正点原子自己的例程运行,该白线依然存在
白线位于屏幕边缘,判断屏幕分辨率设置有误,目前的屏幕分辨率设置为 480*272,将其修改为 481*272 后,问题解决,如下图所示:
这里判断可能有两个原因:
-
正点原子给出的屏幕参数有误;
-
屏幕质量缺陷,黑边未能正常覆盖废弃像素点;
-
寄存器设置错误;
屏幕分辨率修改后,需要修改时钟频率,根据屏幕参数计算出来的时钟频率大约为 9.5MHz,程序中设置为 10.1MHz,因此时钟频率可以不用修改(但是要注意,时钟频率稍微大一点是可以的,大很多就会有问题)
使用画点函数测试屏幕时发现,(0, y) 这一列像素无法显示出来,结合上述问题,像是屏幕整体右移了一列,暂时先通过修改分辨率的方式 WorkRound
8.3.类型转换问题
裸机驱动好像不支持自动类型转换,因此,如果类型不对应,会导致程序直接无法运行
例如,函数指定需要 uint32_t 类型的参数,如果传入参数的类型为 uint16_t,可能导致程序无法运行,因此务必注意这个问题