[IMX] 07.LCD 显示

目录

1.屏幕基础概念

1.1.分辨率

1.1.1.描述方法 - 720P、1080P

1.1.2.描述方法 - 2K、4K

1.1.3.小结

1.2.像素密度 - PPI

1.3.像素格式 - ARGB8888

1.4.RGBLCD 接口

2.屏幕的时间参数

2.1.Blank 和 Back/Front Porch

2.2.VLW 和 HLW

2.3.HACT 和 VACT

2.4.RGB LCD 屏幕时序

2.4.1.行时序

2.4.2.帧时序

2.4.3.ALIENTEK 屏幕参数表

3.IMX6U eLCDIF 接口的时钟配置

3.1.时钟树节点

3.2.时钟源选择

3.3.设置 PLL5 时钟源的频率

3.4.时钟源 PLL5 的输出频率

3.5.设置 eLCDIF 接口的时钟频率

3.6.时钟使能

4.eLCDIF 的屏幕接口

4.1.MPU 接口

4.2.VSYNC 接口

4.3.DOTCLK 接口

4.4.屏幕显存

5.eLCDIF 相关配置寄存器

5.1.控制寄存器 - LCDIF_CTRL

5.2.控制寄存器 - LCDIF_CTRL1

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.9.当前图像显存地址 - LCDIF_CUR_BUF

5.10.下一帧图像显存地址 - LCDIF_NEXT_BUF

6.硬件原理图

7.LCD 驱动代码实现

7.1.获取屏幕 ID

7.1.1.屏幕 ID 定义

7.1.2.初始化 M0~M2 的引脚(用于读取屏幕 ID)

7.1.3.读取屏幕 ID

7.1.4.主函数读取 ID 并输出至串口

7.2.初始化 LCD 屏幕的数据与控制引脚

7.3.eLCDIF 模块复位

7.4.eLCDIF 模块使能

7.5.eLCDIF 时钟初始化

7.6.设置屏幕参数

7.6.1.屏幕参数结构体

7.6.2.显存起始地址

7.6.3.常用颜色定义

7.6.4.初始化屏幕参数

7.7.初始化 eLCDIF 接口

7.8.画图 API

7.8.1.绘制矩形区域

7.8.2.绘制线条

7.8.3.绘制矩形框

7.8.4.绘制圆形

7.8.5.显示一个字符

7.8.6.显示字符串

7.8.7.计算 n 次方

7.8.8.显示数字(高位为 0 不显示)

7.8.9.显示数字(高位为 0 显示)

7.9.主函数

8.问题记录

8.1.屏幕参数设置完成后,无法正常将屏幕背景色设置为红色

8.2.屏幕边缘存在一条白线

8.3.类型转换问题


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) 为显示外设的控制器,其结构如下图所示:

图中各标号部分的含义如下:

  1. 引脚:用于连接外部显示设备(如 LCD 屏幕)和 eLCDIF 接口模块;

  2. 通过系统总线 AXI 向 eLCDIF 写入数据;通过控制总线 APB 读写寄存器或进行 DMA 操作,通过控制总线的寄存器可以配置显存地址、输入像素的格式、输出数据信号的宽度、控制信号的有效极性,以及控制时序中的 VBP、VSPW 等参数,还可以配置是否使用 DMA 传输;

  3. LCD 接口:受控制总线 ControlBus 的寄存器控制,从系统总线 SystemBus 获取输入的像素数据,eLCDIF 初始化完成后,会从 LFIFO 和 TXFIFO 中获取数据并进行转换处理(格式转换、移位等操作)并传输出去,当 FIFO 中的数据量低于一定程度时,它会向系统总线 SystemBus 发起请求,系统总线会把显存地址的数据搬运至 FIFO 中,FIFO 可以配置阈值,低于该阈值时系统总线会提高获取数据的优先级;

  4. 总线的时钟信号 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] 位域控制,可选的时钟源如下:
  1. 0b000:PLL2;
  2. 0b001:PLL3_PFD3
  3. 0b010:PLL5;
  4. 0b011:PLL2_PFD0;
  5. 0b100:PLL2_PFD1;
  6. 0b101:PLL3_PFD1;
  7. 0b110~0b111:保留;
  8. 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] 位域控制:

  1. 0b000:选择时钟源选择器 1 输出的时钟信号作为时钟源;

  2. 0b001:选择 ipp_di0_clk 作为时钟源;

  3. 0b010:选择 ipp_di1_clk 作为时钟源;

  4. 0b011:选择 ldb_di0_clk 作为时钟源;

  5. 0b100:选择 ldb_di1_clk 作为时钟源;

  6. 0b101~0b111:保留;

  7. 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 后,问题解决,如下图所示:

这里判断可能有两个原因:

  1. 正点原子给出的屏幕参数有误;

  2. 屏幕质量缺陷,黑边未能正常覆盖废弃像素点;

  3. 寄存器设置错误;

屏幕分辨率修改后,需要修改时钟频率,根据屏幕参数计算出来的时钟频率大约为 9.5MHz,程序中设置为 10.1MHz,因此时钟频率可以不用修改(但是要注意,时钟频率稍微大一点是可以的,大很多就会有问题)

使用画点函数测试屏幕时发现,(0, y) 这一列像素无法显示出来,结合上述问题,像是屏幕整体右移了一列,暂时先通过修改分辨率的方式 WorkRound

8.3.类型转换问题

裸机驱动好像不支持自动类型转换,因此,如果类型不对应,会导致程序直接无法运行

例如,函数指定需要 uint32_t 类型的参数,如果传入参数的类型为 uint16_t,可能导致程序无法运行,因此务必注意这个问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值