文章目录
-
- 前言
- 一、LCD学前相关知识
- 二、IMX6ULL上的LCD
-
- [2.1 LCD原理图](#2.1 LCD原理图)
- [2.2 LCD引脚初始化](#2.2 LCD引脚初始化)
- 三、CRT显示器
- 四、LCD时序
- 五、ATK4384显示器的时间参数
- 六、LCD时钟初始化操作
-
- [6.1 配置PLL5的相关寄存器](#6.1 配置PLL5的相关寄存器)
- [6.2 配置PLL5后路线](#6.2 配置PLL5后路线)
- [6.3 实现代码](#6.3 实现代码)
- 七、LCD相关寄存器配置
- 八、实现清屏、画点
- 九、移植framebuff小型图像库

前言
LCD 液晶显示屏是嵌入式开发中最核心的人机交互外设之一,广泛应用于工控、物联网、智能设备等场景。在 IMX6ULL 裸机开发中,掌握 LCD 驱动是深入学习图像显示、界面开发的关键基础。不同于现成库开发,裸机驱动需要从零理解屏幕成像原理、行场时序、时钟架构与寄存器配置。本文将从 LCD 基础理论出发,对比传统 CRT 扫描机制,详细讲解 IMX6ULL 的 LCD 时序参数、PLL5 像素时钟配置、硬件引脚初始化与 LCDIF 寄存器驱动,最终实现清屏、画点等基础显示功能,完成一套完整的裸机 LCD 驱动开发流程。
一、LCD学前相关知识
LCD 全称是 Liquid Crystal Display,也就是液晶显示器,是现在最常用到的显示器,手机、电脑、各种人机交互设备等基本都用到了 LCD,最常见就是手机和电脑显示器。在屏幕里显示的图像是由一个一个像素点组成的,提起 LCD 显示器,我们都会听到 720P、1080P、2K 或 4K 这样的字眼,这个就是
LCD 显示器分辨率,这是由像素点的个数而决定的。
分辨率 :横向像素点的个数 * 纵向像素点的个数
1920 * 1080
屏幕尺寸:屏幕左上角到屏幕右下角的距离,单位为inch(英寸),1inch = 2.54cm
分辨率相同,屏幕尺寸越小显示效果越好;
屏幕尺寸相同,分辨率越大显示效果越好;
像素格式 (Pixel Format):规定了一个像素在内存里怎么存、通道怎么分配、占多少 bit 的规范。一般使用的像素格式为:RGB 像素格式(RGB888/RGBA8888/RGB565)
RGB888:一个像素点占三个通道,一个通道占8个bit,一个像素点占3个字节;
RGB565
ARGB8888 :一个像素点占4个字节,更便于计算机操作
A:透明色

现在的LCD显示器主要分为:8bit面板与10bit面板,这里说的 bit,就是单色彩通道的色深 ,bit数越高,颜色就越细腻。
8bit面板 :一个通道占8个bit;
10bit面板 :一个通道占10个bit;
8bit+FRC(抖动模拟 10bit):面板硬件本身只有 8bit,靠 FRC 帧抖动算法,视觉模拟出接近 10bit 的色彩,不是真 10bit 面板。
显存 :专门存放图像数据的内存空间;
对于IMX6ULL开发板上这块4.3inch的ARGB8888屏幕,分辨率为800480,所以它的显存大小至少为800480*4=1.5M。我们一般从IMX6ULL上的DDR3划出一块内存(地址:0x89000000)来作为显存。
二、IMX6ULL上的LCD
2.1 LCD原理图
IMX6ULL开发板的LCD原理图:

其中有24根数据线;
PCLK:PixelClock像素时钟信号线。同步像素点发送的时间间隔;
HSYNC:HorizontalSync行/水平同步时钟信号线。同步每一行发送的时间间隔;
VSYNC:帧/垂直同步时钟信号线,同步每一帧图像的时间间隔;
DE:Data Enable 数据使能信号线。
RGB LCD 一般有两种驱动模式:DE 模式和 HV 模式,这两个模式的区别是 DE 模式需要用到 DE 信号线,而 HV 模式不需要用到 DE 信号线,在 DE模式下是可以不需要 HSYNC 信号线的,即使不接 HSYNC 信号线 LCD 也可以正常工作。
2.2 LCD引脚初始化
c
void lcd_gpio_init()
{
//1.引脚复用功能
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);
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);
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);
//LCD 时钟
IOMUXC_SetPinMux(IOMUXC_LCD_CLK_LCDIF_CLK, 0); //pclk
IOMUXC_SetPinMux(IOMUXC_LCD_VSYNC_LCDIF_VSYNC, 0); //vsync
IOMUXC_SetPinMux(IOMUXC_LCD_HSYNC_LCDIF_HSYNC, 0); //hsync
IOMUXC_SetPinMux(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 0); //enable
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO08_GPIO1_IO08, 0); //backlight_pwm
//2.引脚电气属性
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA00_LCDIF_DATA00, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA01_LCDIF_DATA01, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA02_LCDIF_DATA02, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA03_LCDIF_DATA03, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA04_LCDIF_DATA04, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA05_LCDIF_DATA05, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA06_LCDIF_DATA06, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA07_LCDIF_DATA07, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA08_LCDIF_DATA08, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA09_LCDIF_DATA09, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA10_LCDIF_DATA10, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA11_LCDIF_DATA11, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA12_LCDIF_DATA12, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA13_LCDIF_DATA13, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA14_LCDIF_DATA14, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA15_LCDIF_DATA15, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA16_LCDIF_DATA16, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA17_LCDIF_DATA17, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA18_LCDIF_DATA18, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA19_LCDIF_DATA19, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA20_LCDIF_DATA20, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA21_LCDIF_DATA21, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA22_LCDIF_DATA22, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA23_LCDIF_DATA23, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_CLK_LCDIF_CLK, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_VSYNC_LCDIF_VSYNC, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_HSYNC_LCDIF_HSYNC, 0x00B9);
IOMUXC_SetPinConfig(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 0x00B9);
//配置pwm引脚
gpio_init(GPIO1, 8, &(GPIO_Type_t){gpio_output, 1});
gpio_set_value(GPIO1, 8, 1);
}
三、CRT显示器
说到显示器,不得不提一下显示器的"祖先"CRT 显示器,CRT 显示器就是以前很常见的那种大屁股显示器,目前已经很少见了。

CRT 显示器屁股后面是个电子枪,这个电子枪就是我们上面说的"画笔",电子枪打出的电子撞击到屏幕上的荧光物质使其发光。只要控制电子枪从左到右扫完一行(也就是扫描一行),然后从上到下扫描完所有行,这样一帧图像就显示出来了。也就是说,显示一帧图像电子枪是按照'Z'形在运动,当扫描速度很快的时候看起来就是一幅完成的画面了。看下面这一张图:

当显示完一行以后会发出 HSYNC 信号,此时电子枪就会关闭,然后迅速的移动到屏幕的左边,当 HSYNC 信号结束以后就可以显示新的一行数据了,电子枪就会重新打开。在 HSYNC信号结束到电子枪重新打开之间会插入一段延时,这段延时就图 中的 HBP。当显示完一行以后就会关闭电子枪等待
HSYNC 信号产生,关闭电子枪到 HSYNC 信号产生之间会插入一段延时,这段延时就是图 中的 HFP 信号。同理,当显示完一帧图像以后电子枪也会关闭,然后等到 VSYNC 信号产生,期间也会加入一段延时,这段延时就是图 中的 VFP。VSYNC 信号产生,电子枪移动到左上角,当 VSYNC 信号结束以后电子枪重新打开,中间也会加入一段延时,这段延时就是图中的VBP。 HBP、HFP、VBP 和 VFP 就是导致当时显示器黑边的原因。
四、LCD时序

五、ATK4384显示器的时间参数

我们定义一个结构体来存储LCD的相关参数:
c
#define LCD_FRAMEBUF_ADDR (0x89000000)
//LCD参数
struct tftlcd_t
{
unsigned short height; // 高度
unsigned short width; // 宽度
unsigned short pix_size; // 像素大小
unsigned short vspw; // 帧同步信号宽度
unsigned short vbp; // 帧同步信号后廊
unsigned short vfp; // 帧同步信号前廊
unsigned short hspw; // 行同步信号宽度
unsigned short hbp; // 行同步信号后廊
unsigned short hfp; // 行同步信号前廊
unsigned int frame_buffer; // 帧缓冲区地址
unsigned int fore_color; // 前景色
unsigned int back_color; // 背景色
};
struct tftlcd_t lcd_dev;
//lcd参数配置
lcd_dev.height = 480;
lcd_dev.width = 800;
lcd_dev.pix_size = 4;
lcd_dev.hspw = 48;
lcd_dev.hfp = 40;
lcd_dev.hbp = 88;
lcd_dev.vspw = 3;
lcd_dev.vfp = 13;
lcd_dev.vbp = 32;
lcd_dev.frame_buffer = LCD_FRAMEBUF_ADDR;
lcd_dev.fore_color = 0x00ff0000;
lcd_dev.back_color = 0xffffffff;
显示一帧图像所需要的时钟数是:(VSPW+VBP+LINE+VFP) * (HSPW + HBP + HOZVAL + HFP)= (3 + 32 + 480 + 13) * (48 + 88 + 800 + 40) =515328。显示一帧图像需要515328个时钟数,那么显示60帧就是:515328 * 60=30,919,680≈31M,所以像素时钟就是 31MHz。
这里的31MHz指的就是IMX这片SoC的LCD控制器的工作频率。我们必须按照这个数值来配置好LCD控制器的时钟。
在IMX6ULL参考手册中的时钟树中:

可以选取各种时钟源来配置,本篇文章以配置PLL5 来解释。里先把PLL5的倍频因子设置为42,那么从PLL5输出的频率就是1008MHz,之后在2这个位置4分频,再在3这个位置8分频,那么结果为1008/4/8=31.5MHz,最后再把4这个位置选通就可以了,当然也可以将倍频设置为其他数,分频改为其他数,最终凑出31MHZ的像素时钟就行。
在之前【ARM 裸机开发 (IMX6ULL-mini)】中断嵌套实现、代码 OCP 改造与时钟配置这篇文章的时钟配置部分有这样一张图:

我们这次要使用的LCD也就属于视频相关设备,因此选取PLL5作为时钟源,在参考手册中找到Audio / Video PLL,在第18章节中。

配置PLL5的步骤与配置PLL1是相同的,在参考手册这张图中需要转换旁路配置的一部分没有画出。

音频 PLL 与视频 PLL 均基于24MHz 参考时钟 ,生成低抖动时钟 。该 PLL 的时钟输出频率范围为 650MHz ~ 1.3GHz。
六、LCD时钟初始化操作
6.1 配置PLL5的相关寄存器

根据手册可以知道,配置PLL5主要需要使用CCM_ANALOG_PLL_VIDEOn、CCM_ANALOG_PLL_VIDEO_NUM、CCM_ANALOG_PLL_VIDEO_DENOM、CCM_ANALOG_MISC2n这四个寄存器。
CCM_ANALOG_PLL_VIDEOn



CCM_ANALOG_PLL_VIDEO_NUM

CCM_ANALOG_PLL_VIDEO_DENOM

CCM_ANALOG_MISC2n

6.2 配置PLL5后路线

选通门选择PLL5,第一个分频器为4分频,第二个分频器为8分频,最后的选通门选择为LCDIF1 clock。
CSCDR2

CBCMR


6.3 实现代码
c
void lcd_clock_init()
{
//切换到旁路
CCM_ANALOG->PLL_VIDEO |= (1 << 16);
//关闭PLL
CCM_ANALOG->PLL_VIDEO |= (1 << 12);
CCM_ANALOG->PLL_VIDEO_NUM = 0;
CCM_ANALOG->PLL_VIDEO_DENOM = 1;
//设置42倍频因子
CCM_ANALOG->PLL_VIDEO &= ~(0x7f << 0);
CCM_ANALOG->PLL_VIDEO |= (42 << 0);
//设置1分频
CCM_ANALOG->PLL_VIDEO &= ~(3 << 19);
CCM_ANALOG->PLL_VIDEO |= (1 << 20);
//打开PLL
CCM_ANALOG->PLL_VIDEO &= ~(1 << 12);
//使能PLL
CCM_ANALOG->PLL_VIDEO |= (1 << 13);
//等待PLL稳定
while((CCM_ANALOG->PLL_VIDEO & (1 << 31)) == 0);
//切回主路
CCM_ANALOG->PLL_VIDEO &= ~(1 << 16);
//选择PLL5 作为时钟源
unsigned int t = CCM->CSCDR2;
t &= ~(7 << 15);
t |= (1 << 16);
//分频器配置
t &= ~(7 << 12);
t |= (3 << 12);
t &= ~(7 << 9);
CCM->CSCDR2 = t;
//CCM->CBCMR &= ~(7 << 23);
CCM->CBCMR |= (7 << 23);
}
七、LCD相关寄存器配置

在参考手册中,我们可以找到LCD相关的寄存器,我们主要用的就是下面几个,由于寄存器过多,这里就不一一解释了,大家可以去参考手册中自己理解一下。
| 寄存器名 | 中文名称 | 简要作用 |
|---|---|---|
| LCDIF_CTRLn* | LCDIF 控制寄存器 n | 主控制寄存器,模块使能、时钟、中断、模式选择等核心配置 |
| LCDIF_CTRL1n* | LCDIF 控制寄存器 1n | 次一级控制,如 FIFO、回写、字节交换、极性控制等 |
| LCDIF_CTRL2n | LCDIF 控制寄存器 2n | 更多高级控制:VSYNC/HSYNC 相关、翻转、掩码等 |
| LCDIF_TRANSFER_COUNT* | 传输计数寄存器 | 一帧图像的像素 / 行数计数,配置画面分辨率相关 |
| LCDIF_CUR_BUF* | 当前缓冲区地址寄存器 | 当前正在扫描输出的帧缓存物理地址(双缓冲) |
| LCDIF_NEXT_BUF* | 下一缓冲区地址寄存器 | 下一帧将要显示的帧缓存地址,用于画面切换、无闪烁刷新 |
| LCDIF_TIMING | 时序寄存器 | LCD 时序参数:行场同步(HSYNC/VSYNC)、前后肩(porch)配置 |
| LCDIF_VDCTRL0n* | 视频数据控制寄存器 0n | 视频输出控制 0,使能、同步信号、数据使能 DE 等 |
| LCDIF_VDCTRL1* | 视频数据控制寄存器 1 | 行总周期配置(H period) |
| LCDIF_VDCTRL2* | 视频数据控制寄存器 2 | 场总周期配置(V period) |
| LCDIF_VDCTRL3* | 视频数据控制寄存器 3 | 同步信号宽度、前后肩时序参数 |
| LCDIF_VDCTRL4* | 视频数据控制寄存器 4 | 显示尺寸、信号极性等配置 |
实现代码
c
void lcd_reset()
{
//复位LCD
LCDIF->CTRL |= (1 << 31);
delay_ms(20);
LCDIF->CTRL &= ~(3 << 30);
}
lcd_reset();
//LCD寄存器配置
LCDIF->CTRL |= (1 << 19);
LCDIF->CTRL |= (1 << 17);
LCDIF->CTRL |= (3 << 10);
LCDIF->CTRL |= (3 << 8);
LCDIF->CTRL |= (1 << 5);
LCDIF->CTRL1 = (7 << 16);
LCDIF->TRANSFER_COUNT |= (lcd_dev.height << 16) | (lcd_dev.width << 0);
LCDIF->CUR_BUF = lcd_dev.frame_buffer;
LCDIF->NEXT_BUF = lcd_dev.frame_buffer;
LCDIF->VDCTRL0 |= (1 << 28) | (1 << 24) | (1 << 21) | (1 << 20) | (lcd_dev.vspw << 0);
LCDIF->VDCTRL1 |= (lcd_dev.height + lcd_dev.vspw + lcd_dev.vbp + lcd_dev.vfp);
LCDIF->VDCTRL2 |= (lcd_dev.hspw << 18) | (lcd_dev.width + lcd_dev.hbp + lcd_dev.hfp + lcd_dev.hspw);
LCDIF->VDCTRL3 |= ((lcd_dev.hspw + lcd_dev.hbp) << 16) | ((lcd_dev.vspw + lcd_dev.vbp) << 0);
LCDIF->VDCTRL4 |= (1 << 18) | (lcd_dev.width << 0);
//LCD使能
LCDIF->CTRL |= (1 << 0);
八、实现清屏、画点
lcd_clear_screen将帧缓冲区所有像素填充为指定颜色完成清屏,但存在屏幕宽度硬编码的问题;lcd_drawpoint根据坐标计算像素在帧缓存中的偏移,向对应位置写入颜色实现画点
c
void lcd_clear_screen(unsigned int color)
{
unsigned int (*p)[800] = (unsigned int (*)[800])lcd_dev.frame_buffer;
int i = 0;
int j = 0;
for(i = 0; i < lcd_dev.height; i++)
{
for(j = 0; j < lcd_dev.width; ++j)
{
p[i][j] = color;
}
}
}
void lcd_drawpoint(int x, int y, unsigned int color)
{
unsigned int *p = (unsigned int *)lcd_dev.frame_buffer;
if(x > lcd_dev.width || y > lcd_dev.height)
{
return;
}
*(p + y * lcd_dev.width + x) = color;
}
九、移植framebuff小型图像库

自己一个一个编写图像代码效率很低,可以搜寻一些小型图像库的代码,将其移植在自己的工程中,我移植的图像库中包含了画线、显示字符等功能。若是整个文件夹移植,记得需要修改Makefile。