之前学习了I2C、AT24C02、LM75温度传感器等外设,今天完成IMX6ULL片上eLCDIF驱动RGB LCD屏幕,点亮4.7寸800*480屏幕,在屏幕上打点、画线,实时显示LM75采集的温度。
本次是RGB并行同步LCD裸机驱动,不使用Linux内核驱动,直接操作寄存器。
一、RGB LCD基础原理
1. 屏幕基础参数
本次使用4.7寸LCD,分辨率 800×480
像素格式:ARGB8888,每个像素4字节,R/G/B/A各8bit,范围0~255;
色域还有RGB565(2字节),占用内存更小,本次实验用32bit色;
刷新率:由PCLK像素时钟 + 前后廊时序共同决定。
2. 硬件连接:eLCDIF与LCD
IMX6ULL内置eLCDIF(LCD控制器),连接LCD的信号分为两大类:
- 24根并行数据线 LCD_DATA0~LCD_DATA23:R8bit + G8bit + B8bit,传输像素颜色;
- 4根控制信号线
PCLK:像素时钟,每来1个PCLK,传输1个像素;
DE:数据使能,高电平代表当前数据有效;
HSYNC:行同步信号,一行扫描结束,触发行同步;
VSYNC:场同步信号,一整帧画面扫描完成,触发场同步。
数据通过DMA,直接从内存帧缓存区搬运到eLCDIF,不需要CPU逐个搬运像素,减轻CPU负担。帧缓存地址我们设_FRAME_RAM_ADDR_。
3. 行场时序(H/V 同步 + 前后廊)
这是RGB LCD最核心的知识点,一张完整图像扫描不是直接800×480,每一行、每一帧都包含同步脉冲 + 前廊(FP) + 有效像素 + 后廊(BP)
行时序 HSYNC
HSPW:行同步脉冲宽度;
HFP:行前廊,行同步结束到有效像素开始的间隔;
HBP:行后廊,有效像素结束到下一次行同步。
一行总PCLK = HSPW + HFP + 800(有效宽度) + HBP
场时序 VSYNC
VSPW:场同步脉冲宽度;
VFP:场前廊;
VBP:场后廊。
一帧总行数 = VSPW + VFP + 480(有效高度) + VBP
只有时序参数和屏幕手册严格匹配,屏幕才能正常点亮,时序不对会出现花屏、黑屏、闪烁。
4. 实验整体流程
- LCD引脚IOMUX配置:复用为eLCDIF功能,配置PAD电气属性;
- 配置LCD像素时钟PCLK(PLL5视频PLL,分频得到目标PCLK);
- LCDIF控制器软件复位;
- 填充时序参数(HSPW/HFP/HBP、VSPW/VFP/VBP、分辨率);
- 设置帧缓存地址,告诉eLCDIF从内存哪个地址读取像素;
- 使能eLCDIF,开始输出图像;
- 封装底层API:打点、清屏、画水平线;
- 应用层:读取LM75温度,LCD屏幕实时刷新温度字符串。
二、完整源码解析
lcd.h 对应的 lcd.c 完整代码
c
#include "lcd.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "gpt.h"
#include "stdio.h"
void lcd_pad_init(void)
{
//复用功能,全部引脚设置为LCDIF功能
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);
IOMUXC_SetPinMux(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 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_GPIO1_IO08_GPIO1_IO08, 0);
//电气特性配置,所有PAD配置0xB9
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA00_LCDIF_DATA00, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA01_LCDIF_DATA01, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA02_LCDIF_DATA02, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA03_LCDIF_DATA03, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA04_LCDIF_DATA04, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA05_LCDIF_DATA05, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA06_LCDIF_DATA06, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA07_LCDIF_DATA07, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA08_LCDIF_DATA08, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA09_LCDIF_DATA09, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA10_LCDIF_DATA10, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA11_LCDIF_DATA11, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA12_LCDIF_DATA12, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA13_LCDIF_DATA13, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA14_LCDIF_DATA14, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA15_LCDIF_DATA15, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA16_LCDIF_DATA16, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA17_LCDIF_DATA17, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA18_LCDIF_DATA18, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA19_LCDIF_DATA19, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA20_LCDIF_DATA20, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA21_LCDIF_DATA21, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA22_LCDIF_DATA22, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_DATA23_LCDIF_DATA23, 0xB9);
IOMUXC_SetPinConfig(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 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_GPIO1_IO08_GPIO1_IO08, 0xB9);
//背光引脚GPIO1_IO08设置为输出,高电平点亮背光
GPIO1->GDIR |= (1 << 8);
GPIO1->DR |= (1 << 8);
}
void lcd_clk_init(void)
{
//PLL5视频PLL配置,先进入旁路,断电
CCM_ANALOG->PLL_VIDEO |= (1 << 16); //BYPASS=1
CCM_ANALOG->PLL_VIDEO |= (1 << 12); //POWERDOWN=1
//PLL5计算公式:744MHZ = 24M *31
CCM_ANALOG->PLL_VIDEO_NUM = 0;
CCM_ANALOG->PLL_VIDEO_DENOM = 1;
unsigned int r = CCM_ANALOG->PLL_VIDEO;
r &= ~(3 << 19);
r |= (2 << 19);
r &= ~(0x7F << 0);
r |= (31 << 0);
CCM_ANALOG->PLL_VIDEO = r;
//上电,等待PLL锁频
CCM_ANALOG->PLL_VIDEO &= ~(1 << 12);
CCM_ANALOG->PLL_VIDEO |= (1 << 13);
while ((CCM_ANALOG->PLL_VIDEO & (1 << 31)) == 0){
printf("Not locked \n");
};
printf("Is locked \n");
//关闭旁路,使用PLL5输出
CCM_ANALOG->PLL_VIDEO &= ~(1 << 16); //BYPASS=0
//配置CCM时钟选择,PLL5作为LCDIF时钟源
CCM_ANALOG->MISC2 &= ~(3 << 30);
r = CCM->CSCDR2;
//CSCDR2[17:15] LCDIF1_PRE_CLK_SEL 010选择PLL5
r &= ~(7 << 15);
r |= (2 << 15);
//预分频PRED
r &= ~(7 << 12);
r |= (3 << 12);
//CSCDR2[LCDIF1_CLK_SEL]
r &= ~(7 << 9);
CCM->CSCDR2 = r;
//后分频PODF
CCM->CBCMR &= ~(7 << 23);
CCM->CBCMR |= (5 << 23);
//打印时钟链路信息,调试查看分频
unsigned int cscdr2 = CCM->CSCDR2;
unsigned int cbcmr = CCM->CBCMR;
printf("PRE_CLK_SEL=%d, PRED=%d(div-%d), CLK_SEL=%d\n",
(cscdr2 >> 15) & 7,
(cscdr2 >> 12) & 7, ((cscdr2 >> 12) & 7) + 1,
(cscdr2 >> 9) & 7);
printf("PODF=%d(div-%d)\n",
(cbcmr >> 23) & 7, ((cbcmr >> 23) & 7) + 1);
}
void lcd_reset(void)
{
//LCDIF控制器软件复位
LCDIF->CTRL |= (1 << 31);
delay_ms(20);
LCDIF->CTRL &= ~(1 << 31);
LCDIF->CTRL &= ~(1 << 30);
}
struct tftlcd_t lcd_dev;
void lcd_init(void)
{
//第一步:引脚PAD初始化
lcd_pad_init();
//第二步:配置LCD像素时钟PCLK
lcd_clk_init();
//填充LCD设备结构体,屏幕时序参数
lcd_dev.width = 800;
lcd_dev.height = 480;
lcd_dev.pix_size = 4; //ARGB8888 4字节/像素
lcd_dev.vspw = 3;
lcd_dev.vfp = 13;
lcd_dev.vbp = 32;
lcd_dev.hspw = 48;
lcd_dev.hfp = 40;
lcd_dev.hbp = 88;
lcd_dev.frame_addr = _FRAME_RAM_ADDR_; //帧缓存起始地址
lcd_dev.fore_color = 0x00FF0000;
lcd_dev.back_color = 0x00FFFFFF;
//复位LCDIF
lcd_reset();
delay_ms(50);
//配置eLCDIF控制寄存器
LCDIF->CTRL |= (1 << 19) | (1 << 17) | (3 << 10) | (3 << 8) | (1 << 5);
LCDIF->CTRL1 = (0x07 << 16);
LCDIF->TRANSFER_COUNT = (lcd_dev.height << 16) | (lcd_dev.width << 0);
LCDIF->CUR_BUF = lcd_dev.frame_addr;
LCDIF->NEXT_BUF = lcd_dev.frame_addr;
//行场时序寄存器配置
LCDIF->VDCTRL0 = (1 << 28) | (1 << 24) | (1 << 21) | (1 << 20) | (lcd_dev.vspw);
LCDIF->VDCTRL1 = (lcd_dev.height + lcd_dev.vspw + lcd_dev.vfp + lcd_dev.vbp);
LCDIF->VDCTRL2 = (lcd_dev.hspw << 18) | ((lcd_dev.width + lcd_dev.hspw + lcd_dev.hfp + lcd_dev.hbp) << 0);
LCDIF->VDCTRL3 = ((lcd_dev.hspw + lcd_dev.hbp) << 16) | ((lcd_dev.vspw + lcd_dev.vbp) << 0);
LCDIF->VDCTRL4 = (1 << 18) | (lcd_dev.width << 0);
//使能eLCDIF,开始输出画面
LCDIF->CTRL |= (1 << 0);
delay_ms(50);
}
//清屏函数:遍历所有像素填充颜色
void screen_clear(unsigned int color)
{
int i = 0, j = 0;
for(i = 0; i < lcd_dev.height; i++)
{
for (j = 0; j < lcd_dev.width; j++)
{
lcd_drawpoint(j, i, color);
}
}
}
//底层画点函数,向帧缓存对应位置写入颜色
void lcd_drawpoint(int x, int y, unsigned int color)
{
//边界检查,防止越界访问
if ((x >= lcd_dev.width || x < 0) || (y >= lcd_dev.height || y < 0))
{
return;
}
unsigned int (* pF)[800] = (unsigned int (*)[800])lcd_dev.frame_addr;
pF[y][x] = color;
}
//画水平线API
void lcd_h_drawline(int x, int y, unsigned int length, unsigned int color)
{
int i = 0;
for (i = x; i <= x + length; i++)
{
lcd_drawpoint(i, y, color);
}
}
main.c 主函数代码
c
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "led.h"
#include "beep.h"
#include "key.h"
#include "interrupt.h"
#include "clock.h"
#include "epit.h"
#include "gpt.h"
#include "uart.h"
#include "stdio.h"
#include "string.h"
#include "i2c.h"
#include "at24c02.h"
#include "lm75.h"
#include "lcd.h"
#include "framebuffer.h"
int main(void)
{
system_interrupt_init();
clock_init();
led_init();
beep_init();
key_init();
//epit1_init();
gpt1_init();
uart1_init();
at24c02_init();
float temp = 0;
char buffer[128] = {0};
lcd_init();
screen_clear(0x00FFFFFF); //清屏,填充白色背景
//lcd_h_drawline(100, 100, 200, 0x00FF0000);
//extern void lcd_show_string(unsigned short x,unsigned short y, unsigned short width, unsigned short height, unsigned char size,char *p);
//lcd_show_string(200,200, 300, 32, 32, "hello world!");
while(1)
{
temp = get_temp(); //读取LM75温度
int m = 0, n = 0;
m = (int)(temp * 1000) / 1000;
n = (int)(temp * 1000) % 1000;
//把浮点数温度转为字符串
sprintf(buffer,"temp = %d.%d\n", m, n);
//LCD屏幕显示字符串
lcd_show_string(200, 200, 300, 32, 32, buffer);
delay_ms(1000);
}
return 0;
}
三、代码模块讲解
-
lcd_pad_init:IOMUX引脚复用。24根RGB数据线 + 4根控制信号线全部复用为LCDIF外设功能,PAD电气属性统一配置0xB9;
GPIO1_IO08作为LCD背光GPIO,输出高电平点亮背光。这里背光是GPIO控制,只有亮灭两种状态,后面PWM实验我们就改成PWM驱动背光,实现亮度可调。
-
lcd_clk_init:配置PLL5视频锁相环,分频产生LCD像素时钟PCLK。等待PLL锁频成功,配置CCM选择时钟源与分频系数,串口打印分频信息用于调试。
-
lcd_reset:eLCDIF控制器软件复位,上电必须复位,保证寄存器处于稳定状态。
-
lcd_init
填充
tftlcd_t结构体,存放屏幕分辨率、前后廊时序、帧缓存地址、前景背景色;配置eLCDIF的VDCTRL系列寄存器,写入行场同步时序;
设置
CUR_BUF当前帧缓存地址,eLCDIF会从这块内存读取像素;使能LCDIF,屏幕开始输出图像。
-
屏幕绘图API
lcd_drawpoint:最底层接口,根据(x,y)坐标,计算帧缓存内存地址,写入颜色值;增加边界判断,防止越界访问内存;screen_clear:双重循环遍历全部像素,填充统一颜色,清屏;lcd_h_drawline:循环调用打点,画一条水平直线。 -
main函数
初始化顺序:中断 → 系统时钟 → LED、蜂鸣器、按键 → GPT → UART → AT24C02 → LCD。
清屏为白色,主循环:读取LM75温度,sprintf将浮点温度转为字符串,调用
lcd_show_string在屏幕上打印温度,1秒刷新一次。
四、实验现象
- 开发板上电,LCD背光点亮,屏幕清为纯白色;
- 屏幕(200,200)位置每秒刷新一次LM75采集的温度
temp = xx.xxx; - 取消注释
lcd_h_drawline可以在屏幕画红色水平线; - 取消注释
lcd_show_string可以直接在屏幕打印hello world。
总结
- 时序参数必须严格匹配屏幕手册,HBP/HFP/VBP/VFP、同步脉冲设置错误,直接黑屏或者花屏。
- 帧缓存地址不能随便设置,要确保是空闲的内存区域,不能覆盖程序代码、栈空间。
- PLL锁频:必须等待PLL的lock标志位,不等待直接使用时钟,PCLK不稳定,屏幕闪烁、乱码。
- 引脚复用:24根RGB数据线、HSYNC/VSYNC/PCLK/DE,任何一根引脚复用错,屏幕都会异常。
- 浮点数LCD打印:LCD不支持printf,用sprintf把数值转字符串,调用字库函数
lcd_show_string显示。 - 背光GPIO:背光引脚忘记设置输出高电平,屏幕背光不亮,屏幕漆黑,但eLCDIF其实已经正常输出图像。