正点原子 I.MX6ULL MINI 开发板学习笔记:LCD 显示屏与 Framebuffer

这篇笔记继续正点原子 I.MX6ULL MINI 开发板裸机学习,主题是LCD 显示屏与 Framebuffer。代码来自实际工程 /home/zhao/code/2026/touch_screen,实现 800x480 RGB LCD 的驱动。
1. 完成什么实验?
驱动 800x480 分辨率的 RGB LCD,配置 I.MX6ULL 的 LCDIF 控制器,通过 Framebuffer 实现画点、清屏、显示字符等基本绘图功能。
2. 什么是 LCDIF?
LCDIF(LCD Interface)是 I.MX6ULL 内置的显示控制器,负责把 Framebuffer 中的像素数据按照 RGB 时序发送给 LCD 屏幕。
3. RGB 接口有哪些信号线?
主要包括:DATA[23:0] 颜色数据总线、CLK 像素时钟、HSYNC 行同步、VSYNC 场同步、ENABLE 数据使能(DE)。

4. 什么是 Framebuffer?
Framebuffer 是一块连续的内存区域,每个像素对应一个颜色值。LCDIF 会自动把这块内存的数据刷到屏幕上,CPU 只需要修改内存里的像素值就能改变显示内容。
5. 屏幕时序参数有哪些?
代码中配置的 800x480 屏幕时序包括:HSPW=48(行同步脉宽)、HBP=88(行后肩)、HFP=40(行前肩);VSPW=3(场同步脉宽)、VBP=32(场后肩)、VFP=13(场前肩)。有效显示区为 800x480。
6. 像素格式是什么?
代码中每个像素占 4 字节,使用 0x00RRGGBB 格式,即高字节补 0,实际为 24 位 RGB 颜色。

7. 真实可用的 LCD 代码
下面是工程中验证过的 lcd.c 核心代码。
void init_lcd_io(void)
{
IOMUXC_SetPinMux(IOMUXC_LCD_DATA00_LCDIF_DATA00, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_DATA01_LCDIF_DATA01, 0);
/* DATA02 ~ DATA22 省略,同上 */
IOMUXC_SetPinMux(IOMUXC_LCD_DATA23_LCDIF_DATA23, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_CLK_LCDIF_CLK, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_HSYNC_LCDIF_HSYNC, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_VSYNC_LCDIF_VSYNC, 0);
IOMUXC_SetPinMux(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 0);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA00_LCDIF_DATA00, 0xB9);
/* 省略 DATA01~DATA22 */
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA23_LCDIF_DATA23, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_CLK_LCDIF_CLK, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_HSYNC_LCDIF_HSYNC, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_VSYNC_LCDIF_VSYNC, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 0xB9);
}
void init_lcd_clk(void)
{
unsigned int t;
CCM_ANALOG->PLL_VIDEO_NUM = 0;
CCM_ANALOG->PLL_VIDEO_DENOM = 0;
t = CCM_ANALOG->PLL_VIDEO;
t &= ~(3 << 19);
t |= (2 << 19);
t |= (1 << 13);
t &= ~(0x7F << 0);
t |= (42 << 0);
CCM_ANALOG->PLL_VIDEO = t;
while((CCM_ANALOG->PLL_VIDEO & (1 << 31)) == 0); /* 等待 PLL 锁定:bit31 为 LOCK,1=已锁定 */
CCM->CSCDR2 |= (2 << 15) | (3 << 12);
CCM_ANALOG->MISC2 &= ~(3 << 30);
CCM->CBCMR |= (7 << 23);
CCM->CSCDR2 &= ~(7 << 9);
}
struct tftlcd_t
{
unsigned int width;
unsigned int height;
unsigned int pix_size;
unsigned int hspw;
unsigned int hbp;
unsigned int hfp;
unsigned int vspw;
unsigned int vbp;
unsigned int vfp;
unsigned int framebuffer;
unsigned int fore_color;
unsigned int back_color;
};
struct tftlcd_t lcd_dev;
void init_lcd(void)
{
init_lcd_io();
reset_lcd();
init_lcd_clk();
delay_ms(500);
lcd_dev.width = 800;
lcd_dev.height = 480;
lcd_dev.pix_size = 4;
lcd_dev.hspw = 48;
lcd_dev.hbp = 88;
lcd_dev.hfp = 40;
lcd_dev.vspw = 3;
lcd_dev.vbp = 32;
lcd_dev.vfp = 13;
lcd_dev.framebuffer = FRAME_BUFFER_ADDRESS;
lcd_dev.fore_color = 0x00FF0000;
lcd_dev.back_color = 0x00FFFFFF;
LCDIF->CTRL |= (1 << 19) | (1 << 17) | (3 << 10) | (3 << 8) | (1 << 5);
LCDIF->CTRL1 &= ~(0x0F << 16);
LCDIF->CTRL1 |= (7 << 16);
LCDIF->TRANSFER_COUNT = (lcd_dev.height << 16) | lcd_dev.width;
LCDIF->CUR_BUF = lcd_dev.framebuffer;
LCDIF->NEXT_BUF = lcd_dev.framebuffer;
LCDIF->VDCTRL0 = (1 << 28) | (1 << 24) | (1 << 21) | (1 << 20) | lcd_dev.vspw;
LCDIF->VDCTRL1 = lcd_dev.height + lcd_dev.vspw + lcd_dev.vbp + lcd_dev.vfp;
LCDIF->VDCTRL2 = (lcd_dev.hspw << 18) | (lcd_dev.hspw + lcd_dev.hbp + lcd_dev.hfp + lcd_dev.width);
LCDIF->VDCTRL3 = ((lcd_dev.hspw + lcd_dev.hbp) << 16) | (lcd_dev.vspw + lcd_dev.vbp);
LCDIF->VDCTRL4 = (1 << 18) | (lcd_dev.width);
LCDIF->CTRL |= (1 << 0);
delay_ms(20);
printf("lcd init OK\n");
lcd_clear(lcd_dev.back_color);
}
void lcd_clear(unsigned int color)
{
unsigned int (*p)[lcd_dev.width] = (unsigned int (*)[lcd_dev.width])lcd_dev.framebuffer;
int i, j;
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.framebuffer);
if(x >= lcd_dev.width || y >= lcd_dev.height)
{
return;
}
*(p + lcd_dev.width * y + x) = color;
}
下面是 framebuffer.c 中的字符串显示函数示例。
void lcd_fill(unsigned short x0, unsigned short y0,
unsigned short x1, unsigned short y1, unsigned int color)
{
unsigned short x, y;
if(x0 < 0) x0 = 0;
if(y0 < 0) y0 = 0;
if(x1 >= lcd_dev.width) x1 = lcd_dev.width - 1;
if(y1 >= lcd_dev.height) y1 = lcd_dev.height - 1;
for(y = y0; y <= y1; y++)
{
for(x = x0; x <= x1; x++)
lcd_drawpoint(x, y, color);
}
}
void lcd_show_string(unsigned short x, unsigned short y,
unsigned short width, unsigned short height,
unsigned char size, const char *p)
{
unsigned char x0 = x;
width += x;
height += y;
while((*p <= '~') && (*p >= ' '))
{
if(x >= width) { x = x0; y += size; }
if(y >= height) break;
lcd_showchar(x, y, *p, size, 0);
x += size / 2;
p++;
}
}
8. 关键点说明
- 引脚复用:LCD_DATA00~23、CLK、HSYNC、VSYNC、ENABLE 全部复用为 LCDIF 功能。
- 像素时钟:由 PLL_VIDEO 和 CSCDR2 分频得到,代码中按屏幕规格配置。
- PLL 锁定等待:PLL_VIDEO 的 bit31 是 LOCK(1=已锁定),等待循环应写成 == 0(未锁定就继续等)。工程原代码误写为 != 0,只是 PLL 锁定很快碰巧没出问题,上文已修正。
- Framebuffer 地址:FRAME_BUFFER_ADDRESS 定义为 0x89000000。
- 像素格式:每个像素 4 字节,实际颜色为 0x00RRGGBB。
- 时序参数:HSPW/HBP/HFP、VSPW/VBP/VFP 必须与 LCD 屏手册匹配,否则花屏。
- 自动刷新:配置 CUR_BUF 和 NEXT_BUF 后,LCDIF 自动循环输出。
9. 实验现象
程序运行后,屏幕先被清成白色背景,随后可以在屏幕上画点、画矩形、显示字符串。例如调用 lcd_fill(100, 100, 300, 200, 0x00FF0000) 会在屏幕左上角画一个红色矩形。
总结
| 问题 | 答案 |
|---|---|
| 完成什么实验? | 驱动 800x480 RGB LCD,配置 LCDIF,通过 Framebuffer 绘图 |
| 什么是 LCDIF? | I.MX6ULL 内置 LCD 接口控制器 |
| RGB 信号线? | DATA[23:0]、CLK、HSYNC、VSYNC、ENABLE |
| 什么是 Framebuffer? | 一块内存,像素数据存在这里,LCDIF 自动刷到屏幕 |
| 时序参数? | HSPW=48, HBP=88, HFP=40, VSPW=3, VBP=32, VFP=13 |
| 像素格式? | 32 位 ARGB,实际颜色 0x00RRGGBB |
LCD 显示为后续人机交互打下了基础。下一篇进入触摸输入:GT9147 电容触摸屏驱动。