Day56 LCD显示原理与驱动配置
day56 LCD显示原理与驱动配置
本日内容围绕LCD(Liquid Crystal Display,液晶显示器)的显示原理、关键参数、时序控制及在i.MX6ULL开发板上的具体驱动配置展开。核心目标是理解如何通过配置处理器内部的LCD控制器和相关外设,最终点亮一块RGB888接口的LCD屏幕并实现基础显示。
一、 ADC回顾:模拟信号到数字信号的桥梁
在深入LCD之前,简要回顾ADC(Analog-to-Digital Converter,模数转换器)的基础概念,因为后续的传感器数据采集常需依赖ADC。
- 核心功能: 将连续变化的模拟信号(如电压)转换为离散的数字信号。
- 关键参数:
- 量程 (Range): 指ADC能测量的输入电压范围。在我们的开发环境中,通常为0V至3.3V。
- 精度/分辨率 (Resolution): 以“位”(bit)表示。例如,12位ADC意味着它将量程等分为
2^12 = 4096
份。每次转换的结果代表输入电压占总量程的多少份。 - 转换速率 (Conversion Rate): 单位时间内完成转换的次数。高转换速率对于实时系统至关重要。我们的设备支持至少1MHz的转换速率,即每秒可完成百万次转换。
- 误差: 精度直接关联到误差。一个12位ADC的理论最小分辨电压为
3.3V / 4096 ≈ 0.8mV
,这是其固有的量化误差。
二、 LCD显示基础 核心指标
LCD作为人机交互的主要输出设备,其性能由几个核心参数决定。
1. 分辨率 (Resolution)
指屏幕上像素点的总数,通常用“横向像素 x 纵向像素”表示。
- 示例:
1920x1080
表示屏幕横向有1920个像素点,纵向有1080个像素点,总计约207万像素。 - 重要性: 分辨率越高,图像越精细。但单独谈论分辨率意义不大,必须结合屏幕尺寸来评估清晰度。
2. 色域/色深 (Color Depth)
指显示器能显示的颜色种类数量。
- 原理: 现代彩色显示器基于RGB三原色模型。每个像素点由红(Red)、绿(Green)、蓝(Blue)三个子像素组成。
- 常见格式:
- RGB565: 每个像素占用16位(2字节)。其中红色占5位,绿色占6位,蓝色占5位。总共可显示
2^5 * 2^6 * 2^5 = 65,536
种颜色。 - RGB888: 每个像素占用24位(3字节)。R、G、B各占8位。总共可显示
2^8 * 2^8 * 2^8 = 16,777,216
种颜色,即常说的“1600万真彩色”。 - ARGB8888: 每个像素占用32位(4字节)。在RGB888基础上增加了一个8位的Alpha通道,用于表示透明度。
- RGB565: 每个像素占用16位(2字节)。其中红色占5位,绿色占6位,蓝色占5位。总共可显示
- 为什么是888? 8位足以提供平滑的色彩过渡,满足人眼对色彩的需求,且在存储和传输效率上达到平衡。
3. 刷新率 (Refresh Rate)
指显示器每秒钟刷新画面的次数,单位为Hz(赫兹)或fps(帧每秒)。
- 常见值: 30Hz, 60Hz, 120Hz, 144Hz, 240Hz。
- 作用: 刷新率越高,画面运动越流畅,越不易产生拖影或闪烁感。对于日常办公和电影观看,60Hz已足够;对于电竞游戏,更高的刷新率(如144Hz或240Hz)能带来更佳体验。
4. 物理尺寸与PPI
- 尺寸: 指屏幕对角线的长度,单位为英寸(inch)。1英寸等于2.54厘米。
- PPI (Pixels Per Inch): 每英寸所含像素点的数量,是衡量屏幕清晰度的关键指标。人眼的极限分辨能力约为326 PPI。因此,手机屏幕即使物理尺寸小,只要PPI足够高,就能呈现清晰的画面。
5. 接口类型
常见的视频接口有:
- VGA (Video Graphics Array): 传输模拟信号,使用15针D型接口。
- HDMI (High-Definition Multimedia Interface): 传输数字信号,是目前最主流的接口。
- DVI (Digital Visual Interface): 也传输数字信号,多见于投影仪等设备。
- DP (DisplayPort): 一种较新的数字接口,常用于高端显示器。
- Type-C: 部分新型笔记本电脑通过Type-C接口输出视频信号。
三、 LCD工作原理:从电子枪到时序控制
早期的CRT显示器(“大屁股”显示器)通过电子枪逐行扫描荧光粉来成像。现代LCD虽然不再使用电子枪,但其数据传输和刷新的逻辑依然沿用了“逐行扫描”的思想,并通过精确的时序信号来控制。
1. 核心时序信号
LCD控制器通过一组同步信号来协调数据的传输,确保图像稳定显示。这些信号包括:
- DE (Data Enable, 数据使能): 高电平时,表示当前传输的数据有效,LCD会接收并显示该数据;低电平时,数据无效。
- CLK (Clock, 时钟): 提供基准时钟,同步所有信号和数据的传输节奏。
- HSYNC (Horizontal Synchronization, 水平同步): 控制一行像素的开始与结束。
HFP + HSYNC + HBP
构成一个完整的水平周期。 - VSYNC (Vertical Synchronization, 垂直同步): 控制一帧画面的开始与结束。
VFP + VSYNC + VBP
构成一个完整的垂直周期。
- HFP (Horizontal Front Porch, 水平前廊): 在一行数据传输结束后,到下一个HSYNC脉冲到来前的等待时间。
- HBP (Horizontal Back Porch, 水平后廊): 在HSYNC脉冲结束后,到下一行数据传输开始前的等待时间。
- VFP (Vertical Front Porch, 垂直前廊): 在一帧画面传输结束后,到下一个VSYNC脉冲到来前的等待时间。
- VBP (Vertical Back Porch, 垂直后廊): 在VSYNC脉冲结束后,到下一帧画面数据传输开始前的等待时间。
这些“廊”和“同步脉冲”的存在是为了给显示器内部电路留出足够的响应和准备时间,避免图像撕裂或错位。
2. 数据传输方式
LCD与控制器之间的数据传输通常是同步、并行、单工的。
- 同步: 数据传输与CLK时钟信号同步进行。
- 并行: 多根数据线同时传输一个像素点的所有颜色信息(如RGB888需要24根数据线)。
- 单工: 数据只从控制器流向LCD,不反向传输。
四、 i.MX6ULL平台LCD驱动配置
我们的目标是在i.MX6ULL开发板上,通过配置寄存器来点亮一块型号为ATK4384的RGB LCD屏(分辨率800x480,RGB888格式)。
步骤1: 计算像素时钟频率 (Pixel Clock)
根据LCD的时序参数,计算所需的像素时钟频率。
参数 | 值 | 单位 |
---|---|---|
水平显示区域 (HOZVAL) | 800 | tCLK |
HSPW (thp) | 48 | tCLK |
HBP (thb) | 88 | tCLK |
HFP (thf) | 40 | tCLK |
垂直显示区域 (LINE) | 480 | th |
VSPW (tvp) | 3 | th |
VBP (tvb) | 32 | th |
VFP (tvf) | 13 | th |
- 计算一帧所需时钟数:
(VSPW + VBP + LINE + VFP) * (HSPW + HBP + HOZVAL + HFP)
= (3 + 32 + 480 + 13) * (48 + 88 + 800 + 40)
= 528 * 976
= 515,328
个时钟周期。 - 计算像素时钟频率:
如果要求60帧/秒,则像素时钟频率为:
515,328 * 60 = 30,919,680 Hz ≈ 31 MHz
。
因此,我们需要将LCD的像素时钟(PCLK)配置为31MHz。
步骤2: 编写驱动代码
我们将代码组织为三个主要部分:时钟初始化、引脚初始化、LCD控制器初始化。
1. 头文件 lcd.h
定义结构体和函数声明。
#ifndef _LCD_H_
#define _LCD_H_// 定义LCD参数结构体
typedef struct __lcd_info
{unsigned short width; // 水平显示区域宽度unsigned short hspw; // 水平同步脉冲宽度unsigned short hbp; // 水平后廊unsigned short hfp; // 水平前廊unsigned short height; // 垂直显示区域高度unsigned short vspw; // 垂直同步脉冲宽度unsigned short vbp; // 垂直后廊unsigned short vfp; // 垂直前廊unsigned int cur_addr; // 当前帧缓冲区地址unsigned int next_addr; // 下一帧缓冲区地址
} lcd_info_t;// 函数声明
void lcd_clk_init(void); // 初始化LCD时钟
void lcd_pin_init(void); // 初始化LCD引脚
void lcd_init(void); // 初始化LCD控制器
void clear_screen(void); // 清屏函数#endif
2. 源文件 lcd.c
实现上述函数。
#include "lcd.h"
#include "../imx6ull/MCIMX6Y2.h"
#include "../imx6ull/core_ca7.h"
#include "../imx6ull/fsl_iomuxc.h"// 全局变量,存储LCD参数
static lcd_info_t info =
{.width = 800,.hspw = 48,.hbp = 88,.hfp = 40,.height = 480,.vspw = 3,.vbp = 32,.vfp = 13,.cur_addr = 0x89000000, // 假设帧缓冲区起始地址.next_addr = 0x89000000 // 双缓冲,此处简化为同一地址
};/*** @brief 初始化LCD像素时钟 (PCLK) 为31MHz* * 通过配置CCM_ANALOG模块中的PLL_VIDEO寄存器来生成31MHz时钟。*/
void lcd_clk_init(void)
{// 1. 关闭PLL_VIDEO电源和旁路模式,准备重新配置CCM_ANALOG->PLL_VIDEO &= ~(1 << 16); // 关闭旁路CCM_ANALOG->PLL_VIDEO &= ~(1 << 12); // 关闭电源// 2. 配置PLL_VIDEO寄存器unsigned int tmp = CCM_ANALOG->PLL_VIDEO;tmp &= ~(0x3 << 19); // 清除DIV_SELECT位tmp |= (0x2 << 19); // 设置DIV_SELECT为2 (选择PL5)tmp |= (1 << 13); // 启用PLLtmp &= ~(0x7f << 0); // 清除MFD位tmp |= (31 << 0); // 设置MFD为31 (倍频系数)CCM_ANALOG->PLL_VIDEO_NUM = 0; // 分子为0CCM_ANALOG->PLL_VIDEO_DENOM = 1; // 分母为1CCM_ANALOG->PLL_VIDEO = tmp; // 写入配置// 3. 等待PLL锁定while(0 == (CCM_ANALOG->PLL_VIDEO & (1 << 31))); // 等待LOCK位为1// 4. 开启PLL_VIDEO电源和旁路模式CCM_ANALOG->PLL_VIDEO |= (1 << 12); // 开启电源CCM_ANALOG->PLL_VIDEO |= (1 << 16); // 开启旁路// 5. 配置CSCDR2寄存器,选择PLL_VIDEO作为LCD_CLK源tmp = CCM->CSCDR2;tmp &= ~(0x7 << 15); // 清除PRE_PERIPH_CLK_SEL位tmp |= (0x2 << 15); // 选择PLL5作为源tmp &= ~(0x7 << 12); // 清除PERIPH_CLK_SEL位tmp |= (0x3 << 12); // 选择分频器输出tmp &= ~(0x7 << 9); // 清除LCDIF_CLK_SEL位CCM->CSCDR2 = tmp; // 写入配置// 6. 配置CBCMR寄存器,设置LCDIF分频器tmp = CCM->CBCMR;tmp &= ~(0x7 << 23); // 清除LCDIF_PREDIV位tmp |= (0x5 << 23); // 设置分频系数为5 (744MHz / 5 = 148.8MHz, 这里可能需要调整以得到31MHz)CCM->CBCMR = tmp; // 写入配置
}/*** @brief 初始化LCD相关的GPIO引脚* * 将LCD的数据线、时钟线、同步信号线等配置为LCDIF功能。*/
void lcd_pin_init(void)
{// 配置24根数据线 (LCD_DATA00 - LCD_DATA23)IOMUXC_SetPinMux(IOMUXC_LCD_DATA00_LCDIF_DATA00, 1);IOMUXC_SetPinMux(IOMUXC_LCD_DATA01_LCDIF_DATA01, 1);// ... (省略中间部分,实际代码中需列出全部24根数据线)IOMUXC_SetPinMux(IOMUXC_LCD_DATA23_LCDIF_DATA23, 1);// 配置控制信号线IOMUXC_SetPinMux(IOMUXC_LCD_CLK_LCDIF_CLK, 1); // 时钟IOMUXC_SetPinMux(IOMUXC_LCD_HSYNC_LCDIF_HSYNC, 1); // 水平同步IOMUXC_SetPinMux(IOMUXC_LCD_VSYNC_LCDIF_VSYNC, 1); // 垂直同步IOMUXC_SetPinMux(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 1); // 数据使能// 配置引脚电气特性IOMUXC_SetPinConfig(IOMUXC_LCD_DATA00_LCDIF_DATA00, 0x10B0);IOMUXC_SetPinConfig(IOMUXC_LCD_DATA01_LCDIF_DATA01, 0x10B0);// ... (省略中间部分,实际代码中需列出全部数据线和控制线)IOMUXC_SetPinConfig(IOMUXC_LCD_ENABLE_LCDIF_ENABLE, 0x10B0);// 配置背光引脚 (GPIO1_IO08),并将其拉高以开启背光IOMUXC_SetPinMux(IOMUXC_GPIO1_IO08_GPIO1_IO08, 0); // 设置为GPIO功能IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO08_GPIO1_IO08, 0x10B0);GPIO1->GDIR |= (1 << 8); // 设置为输出方向GPIO1->DR |= (1 << 8); // 输出高电平,打开背光
}/*** @brief 初始化LCD控制器* * 配置LCDIF模块的各种寄存器,包括控制、时序、缓冲区地址等。*/
void lcd_init()
{// 1. 初始化时钟和引脚lcd_clk_init();lcd_pin_init();// 2. 复位LCD控制器LCDIF->CTRL |= (1 << 31); // 置位复位位delay_ms(5); // 等待复位完成LCDIF->CTRL &= ~(1 << 31); // 清除复位位// 3. 配置LCDIF控制寄存器 (CTRL)LCDIF->CTRL = (1 << 19) | // 启用运行模式(1 << 17) | // 使用时钟同步模式(0x3 << 10) | // 设置数据总线宽度为24位 (3字节)(0x3 << 8) | // 每个像素24位(1 << 5); // 设置为主机模式// 4. 配置LCDIF控制寄存器1 (CTRL1)LCDIF->CTRL1 &= ~(0xf << 16); // 清除数据格式位LCDIF->CTRL1 |= (0x7 << 16); // 设置为RGB888格式 (A通道未使用)// 5. 配置传输计数寄存器 (TRANSFER_COUNT)LCDIF->TRANSFER_COUNT = (info.height << 16) | (info.width << 0); // 设置分辨率// 6. 配置垂直控制寄存器0 (VDCTRL0)LCDIF->VDCTRL0 = (1 << 28) | // 使能数据使能信号(1 << 24) | // 使能垂直同步信号(1 << 21) | // 使能水平同步信号(1 << 20) | // 使能时钟信号(info.vspw << 0); // 设置垂直同步脉冲宽度// 7. 配置垂直控制寄存器1 (VDCTRL1)LCDIF->VDCTRL1 = (info.height + info.vspw + info.vbp + info.vfp); // 设置垂直总周期// 8. 配置垂直控制寄存器2 (VDCTRL2)LCDIF->VDCTRL2 = (info.hspw << 18) | // 设置水平同步脉冲宽度(info.width + info.hspw + info.hbp + info.hfp); // 设置水平总周期// 9. 配置垂直控制寄存器3 (VDCTRL3)LCDIF->VDCTRL3 = ((info.hspw + info.hbp) << 16) | // 设置水平等待周期(info.vspw + info.vbp); // 设置垂直等待周期// 10. 配置垂直控制寄存器4 (VDCTRL4)LCDIF->VDCTRL4 = (1 << 18) | // 启用同步信号(info.width << 0); // 设置水平像素数// 11. 配置当前和下一帧缓冲区地址LCDIF->CUR_BUF = info.cur_addr; // 当前帧缓冲区地址LCDIF->NEXT_BUF = info.next_addr; // 下一帧缓冲区地址// 12. 启动LCD控制器LCDIF->CTRL |= (1 << 0); // 置位RUN位,启动传输
}/*** @brief 清屏函数* * 将整个帧缓冲区填充为白色 (0xFFFFFF)。*/
void clear_screen(void)
{int i = 0;unsigned char *p = (unsigned char *)info.cur_addr; // 指向帧缓冲区起始地址for(i = 0 ; i < 800 * 480 * 3; i++) // 800x480像素,每个像素3字节{*p++ = 0xff; // R,G,B都设置为最大值,即白色}
}
3. 主程序 main.c
在主函数中调用初始化函数。
#include "MCIMX6Y2.h"
#include "lcd.h" // 引入LCD头文件int main(void)
{system_interrupt_init(); // 初始化中断系统clock_init(); // 初始化系统时钟led_init(); // 初始化LEDbeep_init(); // 初始化蜂鸣器gpt1_init(); // 初始化GPT定时器uart1_init(); // 初始化UART1i2c_init(I2C1); // 初始化I2C1adc1_init(); // 初始化ADC1led_on(); // 打开LED指示灯// 初始化LCDlcd_init();delay_ms(10); // 等待LCD稳定// 清屏,显示白色clear_screen();while(1){// 主循环,保持程序运行}return 0;
}
代码运行的理想结果
当程序成功编译并烧录到开发板后,理想情况下,你应该看到:
- 背光亮起: 屏幕背景变亮,表明背光电路已被正确驱动。
- 屏幕显示纯白色: 由于
clear_screen()
函数将整个帧缓冲区填充为0xFFFFFF
(RGB888格式下的白色),屏幕应完全显示为白色。
如果屏幕没有反应,请检查:
- 时钟配置是否正确,特别是PLL锁定状态。
- 引脚复用和电气配置是否无误。
- 帧缓冲区地址是否正确且可访问。
- LCD控制器的RUN位是否被正确置位。
总结
本日课程从ADC的基础概念出发,深入探讨了LCD的核心参数、工作原理和时序控制,并通过实战项目,在i.MX6ULL平台上完成了LCD的驱动配置。这不仅加深了对嵌入式系统图形显示的理解,也为后续更复杂的图形界面开发打下了坚实的基础。成功的硬件驱动往往始于对数据手册的细致阅读和对底层寄存器的精准操作。