简介:面向基于STM32F4系列微控制器的嵌入式开发者,这份工程示例演示了如何借助STM32F429的LTDC外设驱动7英寸、1024×600分辨率的RGB液晶屏,并同步支持触摸屏输入。实现采用纯寄存器操作方式,开发者可直接面对内存映射和硬件接口,通过配置帧缓冲区、行场同步时序、RGB565/ARGB8888色彩格式、图层透明度与混合模式、VSYNC中断等底层参数,深入掌握LTDC显示链路的工作机制。工程代码按LCD显示、SDRAM帧缓冲、触摸控制、延时定时、按键检测、看门狗等功能模块拆分,结构清晰,便于按需裁剪和移植到其他STM32F4系列芯片。压缩包共43个文件,以C源文件和头文件为主,配合Keil工程文件、启动汇编文件、hex固件与批处理辅助脚本,整体仅235KB,导入工程即可直接编译验证。目前已有1003人学习下载,对需要快速搭建RGB屏显示与触摸联调环境的开发者而言,这份寄存器级参考代码兼具学习与二次开发价值。
1. 在 STM32F429 上点不亮 RGB 屏之前,先承认 LTDC 是套组合拳
同样是接屏幕,SPI OLED 只是把一帧数据挪进 SRAM,而 7 寸 1024×600 的 RGB 屏一旦接到 STM32F429 的 LTDC 上,就意味着一件事:微控制器不能再“想到哪写到哪”,而是要承诺以固定像素时钟持续输出一整条显存总线。这个资源把 HAL 抽象拆掉了,用寄存器从头配置 LTDC、SDRAM、GPIO 和背光,既不依赖库函数的初始化顺序,也没有“默认参数”替你兜底。适合两种人:一是把 7 寸屏当产品核心却总在花屏、偏移、黑屏里打转的嵌入式工程师;二是想把手头 F4 板子接 40pin LCD 接口却不知道从哪查手册的人。读完后你至少能回答三个问题:像素时钟到底从哪里来,帧缓冲为什么必须放在 SDRAM,以及层配置里的 width/pitch 是不是可以直接写 1024。
2. LTDC 与 RGB 屏的握手:先填对同步时序,再谈画点
2.1 从屏参表换算 LTDC 寄存器:HBP、HFP、HSYNC 不只是四个数字
RGB 屏不需要像 SPI 屏那样发“写像素”命令,它要求主机持续输出行列同步信号、数据使能 DE 和像素数据。LTDC 正是为此设计的控制器,但它不会自动识别你的屏,你需要把屏厂 datasheet 里的 blanking 参数填进寄存器。7 寸 1024×600 屏的典型时序参数如下表,具体数值必须以你手头屏型号为准。
| 参数 | 含义 | 典型值 |
|---|---|---|
| HSYNC | 行同步脉冲宽度 | 30 像素时钟 |
| HBP | 行后肩 | 160 像素时钟 |
| HFP | 行前肩 | 160 像素时钟 |
| VSYNC | 帧同步脉冲宽度 | 13 行 |
| VBP | 帧后肩 | 23 行 |
| VFP | 帧前肩 | 12 行 |
| Pixel Clock | 像素时钟 | 48~51.2 MHz |
很多初次接触的人把“有效区宽高”直接填进AWCR,却忘记 LTDC 内部计数是从 0 开始的。实际应该填参数值减 1。而总周期是HSYNC + HBP + HACT + HFP,不是单纯把横向的 4 个参数相加后填一个“分辨率”就能了事。行方向总周期 = 30 + 160 + 1024 + 160 = 1374,帧方向总周期 = 13 + 23 + 600 + 12 = 648。这个数值决定像素时钟与行频、场频的关系,也就是你最终在屏幕点亮后能用示波器验证的那个“心跳”。
2.2 用寄存器把时序写进 LTDC:同步宽度、后肩、有效区和总宽
下面的代码是典型的 LTDC 同步时序配置顺序。资源工程里没有引入 HAL,直接访问LTDC外设寄存器,所有时序位都和 2.1 的参数一一对应。
void LTDC_Timing_Config(void) { // SSCR:行同步宽 HSYNC=30,帧同步宽 VSYNC=13,均减 1 LTDC->SSCR = (30 - 1) | ((13 - 1) << 16); // BPCR:后肩 HBP=160,VBP=23 LTDC->BPCR = (160 - 1) | ((23 - 1) << 16); // AWCR:有效区是 1024 x 600 LTDC->AWCR = (1024 - 1) | ((600 - 1) << 16); // TWCR:行总周期 1374,帧总周期 648 LTDC->TWCR = (1374 - 1) | ((648 - 1) << 16); }四个寄存器分别对应同步脉冲、后沿、有效区和总周期。SSCR决定同步脉冲宽度;BPCR决定同步结束后等待多久才开始有效数据;AWCR是指真正要显示的像素区域;TWCR则是整条扫描线或整个帧的绝对长度。这样拆开后,你会发现“花屏”往往不是因为 DCLK 抖动,而是TWCR比实际时序少算了一个像素,导致数据偏移成斜纹。
接着要配置像素时钟极性和 DE 极性。这一项容易被忽略,尤其是从别的工程抄初始化代码时。LTDC 的GCR寄存器里,PCPOL控制采样沿,DEPOL控制 DE 有效电平。多数 RGB 屏在 DE 高有效、上升沿采样时工作正常,少数屏要求下降沿。如果你点亮后屏幕偏色、有细密横纹,优先检查这里。
// PCPOL=0:上升沿采样;DEPOL=0:DE 高有效 LTDC->GCR &= ~(1u << 1); LTDC->GCR &= ~(1u << 3);这段代码的作用是让 LTDC 在普通 RGB 逻辑电平下工作。GCR位 1 是像素时钟极性,位 3 是 DE 极性。颜色格式、抖动使能等也在这个寄存器里,但初调时保持默认更稳妥。
2.3 像素时钟 PLLSAI 从哪里来:分频器不是随便填的
STM32F429 的系统时钟 PLL 用于产生 HCLK,LTDC 的像素时钟则由另一个 PLLSAI 提供。很多工程在 SDRAM 工作正常、GPIO 也都复用正确的情况下,屏依然黑,就是因为 LTDC 时钟没使能或者频率不对。PLLSAI 不是把晶振频率直接搬给 LTDC,而是经过一个 VCO 再分频。
void LTDC_PixelClock_Config(void) { // 假设 HSE=25MHz // PLLSAIM=25,PLLSAIN=192,PLLSAIR=4 // VCO = 25MHz / 25 * 192 = 192MHz // LTDC clock = VCO / 4 = 48MHz RCC->PLLSAICFGR = (25u << 28) | (192u << 6) | (25u << 0); // 使能 PLLSAI 并等待锁定 RCC->CR |= RCC_CR_PLLSAION; while ((RCC->CR & RCC_CR_PLLSAIRDY) == 0) { } // LTDC 时钟不分频 RCC->DCKCFGR &= ~RCC_DCKCFGR_PLLSAIDIVR; }这段配置让 LTDC 得到 48MHz 像素时钟。PLLSAICFGR的高 7 位是分频系数 N,低 6 位是倍频系数 M,位 27:24 是 PLLSAIR。常见错误是只改了PLLSAIN而不动PLLSAIM,导致 VCO 频率漂移,刷新率也随之跑偏。如果你的屏显示偏右、偏下超过一两个像素,也可以用手头屏的实际像素时钟反推 PLLSAIR,不需要改TWCR去“找齐”。
3. 把 40pin LCD 接口接到 F429:GPIO 复用、DE 极性与背光 PWM
3.1 信号分组与 F429 的 AF14 映射
7 寸 LCD 的 40pin 接口通常会引出 RGB 数据、HSYNC、VSYNC、DE、PCLK、RESET、背光控制以及电源。F429 的 LTDC 引脚复用功能是 AF14,但不是说任何 GPIO 都能接任意颜色位。你要先在 datasheet 里查每个引脚对应的 AF14 信号,再决定用 RGB565 还是 RGB888。资源工程按寄存器驱动方式配置,GPIO 初始化时用MODER选备用功能,用AFRL/AFRH写入复用编号 14。
void LCD_GPIO_Init(void) { // 使能对应 GPIO 时钟,这里以 GPIOA 和 GPIOB 为例 RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOBEN; // 将 PB0、PB1 设为 AF14,作为 LTDC_R3、LTDC_R4 GPIOB->MODER &= ~(0x3u << 0); GPIOB->MODER |= (0x2u << 0); GPIOB->AFR[0] |= (0xEu << 0); // AF14:0b1110 // 高速输出,避免像素边界变糊 GPIOB->OSPEEDR |= (0x3u << 0); // HSYNC/VSYNC/DE/PCLK 同理,全部设为 AF14 }GPIO 速度要和像素时钟匹配。48MHz 像素时钟下,如果OSPEEDR还停留在低速档,显示屏上会出现色彩渐变边界发虚的现象,尤其是在黑白跳变的竖线边缘。每个引脚的复用模式和速度都要配置,不能只配置数据线而忽略同步线,因为 LTDC 的同步信号引脚阻抗不匹配会造成抖动,图像偶发性偏移。
从板级设计角度,40pin 接口上相邻数据线之间如果没有地线隔离,高频信号容易串扰。软件上能做的补救是降低OSPEEDR到合适档位,以及让 LTDC 时钟极性选择下降沿采样,避开数据线上的振铃区间。这些都是硬件板子画完之后才有的调试手段,但在初始化代码里留好配置位比重新打板便宜得多。
3.2 背光 PWM 与 LCD 亮度控制
RGB 屏通常没有自带背光驱动,需要外接升压电路或直接控制 LED 电流。软件上不会去控制电流,而是通过 PWM 占空比调亮度。F429 里用定时器产生 PWM 非常常见,工程HARDWARE/TIMER里就是给这类用途准备的基础。
void LCD_BackLight_Init(void) { // 使能 TIM4 时钟 RCC->APB1ENR |= RCC_APB1ENR_TIM4EN; // 预分频 99,计数到 999,PWM 频率为 APB1 时钟的 1/100000 TIM4->PSC = 99; TIM4->ARR = 999; // CH1 PWM 模式 1,输出比较预装载 TIM4->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; TIM4->CCMR1 |= TIM_CCMR1_OC1PE; // 使能 CH1 输出 TIM4->CCER |= TIM_CCER_CC1E; // 初始亮度 50% TIM4->CCR1 = 500; // 启动定时器 TIM4->CR1 |= TIM_CR1_CEN; }CCR1值决定占空比,0 是全暗,999 是全亮。如果背光 PWM 频率太低,人眼会看到闪烁,常见处理是 PWM 频率设置在 1kHz 以上,20kHz 以下。部分屏的背光控制脚是低电平时点亮,这时CCER里要配置CC1P极性,或者直接让CCR1含义取反,具体看屏的背光使能脚是接在 LED 阳极还是阴极。
3.3 LCD 复位与上电顺序
RGB 屏的控制器也需要复位,但和 SPI 屏不同,LTDC 初始化必须在 LCD 面板上电完成、复位释放之后进行。如果先开 LTDC 再拉屏的 RESET,屏控制器可能锁在一个未定义状态。
// 假设 RESET 引脚是 PE1,低电平复位 GPIOE->BSRR = (1u << 1); // RESET 拉高 delay_ms(20); GPIOE->BSRR = (1u << 1) << 16; // RESET 拉低 delay_ms(20); GPIOE->BSRR = (1u << 1); // 释放复位 delay_ms(50);这个延时不是随便加的。许多 7 寸屏的电源和复位时序要求 VCC 稳定后至少 10ms 再拉高 RESET,随后再等 50ms 让面板内部控制逻辑和电荷泵稳定。如果你在点亮过程中反复出现“第一次黑屏,按复位后正常”,大概率是上电顺序没做对。
4. 帧缓冲选址:1024×600 的三字节彩色是不能交给 RAM 的
4.1 内存占用与色彩格式选择
在配置 LTDC 层之前,必须先决定帧缓冲格式。同一块屏,RGB565、RGB888、ARGB8888 的显存占用差很多。STM32F429 片内 SRAM 一共 256KB,而 1024×600 的 RGB565 一帧就要 1.17MB,ARGB8888 更是达到 2.34MB。片内根本放不下,所以外部 SDRAM 是必须的。
| 色彩格式 | 每像素位宽 | 一帧 1024×600 占用 |
|---|---|---|
| RGB565 | 16 bit | 1,228,800 B(约 1.17 MiB) |
| RGB888 | 24 bit | 1,843,200 B(约 1.76 MiB) |
| ARGB8888 | 32 bit | 2,457,600 B(约 2.34 MiB) |
字体、图标和 UI 控件都放在片内 Flash 或外部 SPI Flash,运行时的画布放 SDRAM。这样安排下,CPU 写一帧再切一帧,LTDC 控制器持续从 SDRAM 读取,二者互不阻塞。对 7 寸屏来说,RGB565 在色彩过渡上会有轻微色阶,但资源工程里默认 RGB565 的原因很简单:显存减半、带宽减半,对 SDRAM 访问 latency 的容忍度也更高。
4.2 FMC 外接 SDRAM 初始化:不是把地址指过去就能用
SDRAM 上电之后必须经历预充电、自动刷新、配置模式寄存器三步,否则 LTDC 从里面读到的是随机数据。FMC 控制器提供了FMC_Bank5_6寄存器组,其中SDCR1配置行列宽度、突发长度和 CAS Latency,SDTR1配置时序延迟,SDCMCR下发命令。
void SDRAM_Init(void) { // 以 16bit SDRAM、12 列地址、13 行地址为例 FMC_Bank5_6->SDCR1 = (0x2u << 12) | /* SDCLK = HCLK/2 */ (0x2u << 7) | /* 行地址 13 bit */ (0x1u << 4) | /* 列地址 12 bit */ (0x3u << 9); /* CAS = 3 */ // 配置加载模式、刷新率等时序参数 FMC_Bank5_6->SDTR1 = (0x4u << 0) | /* TMRD */ (0x7u << 4) | /* TXSR */ (0x4u << 8) | /* TRAS */ (0x7u << 12) | /* TRC */ (0x2u << 16) | /* TWR */ (0x2u << 20) | /* TRP */ (0x2u << 24); /* TRCD */ // 发送 NOP 命令 FMC_Bank5_6->SDCMCR = (0x1u << 0); delay_ms(1); // 预充电 FMC_Bank5_6->SDCMCR = (0x2u << 0); delay_ms(1); // 连续 8 次自动刷新 FMC_Bank5_6->SDCMCR = (0x3u << 0) | (0x7u << 5); delay_ms(1); // 加载模式寄存器 FMC_Bank5_6->SDCMCR = (0x4u << 0) | (0x13u << 9); delay_ms(1); // 使能自动刷新 FMC_Bank5_6->SDCR1 |= (0x1u << 1); }这里有个很容易踩的坑:SDTR1里的数值单位是以 FMC 时钟周期为单位的,具体值由 SDRAM 芯片的 tRCD、tRP 等参数和HCLK共同决定。如果你把板子主频从 168MHz 改到 180MHz,HCLK变了,但SDTR1没同步调整,SDRAM 读写就会偶发性失败。表现是屏幕大多数时候正常,一旦画面大面积刷新,会出现随机的整块噪点。
FMC SDRAM 的地址映射从0xC0000000开始。如果你的板子用的是 Bank2,则起始地址是0xD0000000。资源工程里通常只在system_stm32f4xx.c里改外部内存起始地址,LTDC 层基址只要和 SDRAM 初始化对应的 Bank 一致即可。
4.3 把帧缓冲地址告诉 LTDC 层
LTDC 每个层都有独立的帧缓冲地址寄存器CFBAR。我们需要把已经初始化好的 SDRAM 首地址写入这个寄存器,并告诉层控制器每行有多少字节、每帧有多少行。
// 帧缓冲地址,这里以 0xC0000000 为例 LTDC_Layer1->CFBAR = 0xC0000000; // 行有效像素宽度 LTDC_Layer1->CACR = 1024; // 每行字节数 = 宽度 * 2(RGB565) LTDC_Layer1->CACR = (1024 * 2) / 8; // 层大小寄存器:行数 600,行宽 1024 LTDC_Layer1->CLUTS = (600 << 16) | (1024);注意CACR和CLUTS这两个名字是示意性的,不同固件库里的命名有差异。关键是理解三个参数:像素行宽、帧缓冲 pitch(以 32bit 字为单位时通常是 8 的倍数)、窗口高度。1024×2 字节本身就是 2048,是 8 的倍数,所以不用手动补齐。如果你改用 RGB888,每行字节数是 3072,也是 8 的倍数;但如果是 900×600 这类不凑 8 的宽度,就要把 pitch 向上取整到 8 的倍数,否则行末会出现一条斜切错位。
层配置完成后,还必须在LTDC->GCR里使能 LTDC,然后等待至少一个帧周期。很多人只配置LTDC_Layer1而忘记使能 LTDC 本身,结果是 LTDC 中断不产生,屏幕保持黑屏。
5. 触摸屏接入与坐标校正:从原始 ADC 到 UI 坐标
5.1 工程里的 TPAD 不是触摸屏,别把两个概念混在一起
打开HARDWARE目录时,会看到TPAD这个文件夹,但它对应的是 STM32 的电容触摸按键,不是一个触摸屏控制器。真正的 7 寸 RGB 屏触摸面板有两种主流接口:电阻屏通过 SPI 采样,典型芯片是 XPT2046;电容屏通过 I2C 读取,典型芯片是 GT911。资源工程没有随屏附带触摸驱动,需要你根据屏的型号把手写驱动接进来。好消息是,触摸屏的坐标读取和屏的显示驱动是松耦合的,LTDC 初始化完成后,触摸驱动可以独立挂到任意 I2C 或 SPI 总线上。
如果你买的是 “40pin LCD 接口 + 触摸排线” 组合屏,触摸排线上通常包含SDA、SCL、INT、RST和 3.3V 电源。GT911 在复位后根据INT高低电平选择 I2C 地址,常见是0x14或0x5D。软件上必须先给触摸芯片复位,再按屏厂给的寄存器地址读取触点数据。
5.2 读取触摸点:GT911 的坐标寄存器流
GT911 的坐标报告放在一组连续寄存器里,读取顺序是先读状态寄存器,确认有触摸,再读坐标寄存器。下面的代码是一个最小读点流程。
// 读取 GT911 状态寄存器,地址 0x814E uint8_t status = I2C_ReadReg(GT_ADDR, 0x814E); if (status & 0x80) { // 第 7 位表示 buffer 已准备好 uint8_t touch_buf[4]; // 读取第一点的 x/y 坐标,寄存器 0x8150 起 I2C_ReadRegs(GT_ADDR, 0x8150, touch_buf, 4); uint16_t x = (touch_buf[0] & 0x0F) | (touch_buf[1] << 8); uint16_t y = (touch_buf[2] & 0x0F) | (touch_buf[3] << 8); // 读完后写 0 清状态 I2C_WriteReg(GT_ADDR, 0x814E, 0x00); }GT911 的单点坐标实际只用低 12 位,所以touch_buf[0]的高 4 位要屏蔽。I2C_ReadRegs这类底层函数可以基于项目里的软件 I2C,也可以直接用HARDWARE里的 delay 和 GPIO 模拟。这里没有用中断,是因为电容屏自带触点跟踪,轮询已经能满足大多数 UI 需求。如果你追求功耗控制,可以引入INT中断,在触摸按下时才通过 EXTI 触发读取。
5.3 坐标校准:三点校准把所有误差放进一个线性映射
触摸屏的 ADC 坐标和 LCD 像素坐标之间不是简单的倍率换算,屏贴合偏移、触摸面板旋转和放大电阻差异都会造成读数偏差。最常用的校准方式是三点校准,假设 X 方向映射为screen_x = a0 * raw_x + b0 * raw_y + c0,Y 方向同理。先让用户依次点击屏幕上的三个校准点,解出六个系数。
typedef struct { int a0, b0, c0; // X 方向映射系数 int a1, b1, c1; // Y 方向映射系数 } TouchCal_t; void Touch_MapPoint(TouchCal_t *cal, int raw_x, int raw_y, int *px, int *py) { *px = (cal->a0 * raw_x + cal->b0 * raw_y + cal->c0) >> 16; *py = (cal->a1 * raw_x + cal->b1 * raw_y + cal->c1) >> 16; }用右移 16 位代替浮点除法,在 Cortex-M4 上可以避免浮点库开销。系数本身通过解三元一次方程得到,校准点选取四个角落和中心点,取三次采样平均。实际项目中,很多人把触摸芯片的原始坐标直接当作像素坐标用,结果 UI 按钮总是点偏。加一个校准函数,整体体验会立刻从“能出图”变成“可点准”。
电阻屏 XPT2046 的读取方式更简单,它通过 SPI 发一个 8bit 命令,例如0x90表示测 X,0xD0表示测 Y,返回 12bit ADC。校准思路完全一样,但要注意电阻屏需要做阈值滤波,手指按下的瞬间会有抖动,通常连续读 4 次去掉最大值最小值再取平均。
6. 用 VSYNC 中断验证刷新率,再让 RGB 屏快速显示中文
6.1 帧中断测量:看 PB3 的翻转频率对不对
屏点亮后,第一件事不是画 UI,而是验证 LTDC 实际刷新率是否接近你计算的帧频。打开 LTDC 的行中断或 VSYNC 中断,在中断服务函数里翻转一个 GPIO,用示波器量 GPIO 频率,这是最直接的板级验证手段。
void LTDC_IRQHandler(void) { if (LTDC->ISR & LTDC_ISR_LI) { // 行中断标志 GPIOB->ODR ^= (1u << 3); // PB3 翻转一次 LTDC->ICR = LTDC_ICR_CLI; // 清除中断标志 } }在LTDC_Timing_Config()之后使能行中断,并配置 NVIC。如果你按 2.1 里的总周期计算,实际的脉冲频率应该是像素时钟 / 行总周期 / 帧总周期。比如 48MHz、1374 行、648 帧周期,帧率大约 53Hz。如果示波器看到的频率明显偏低,通常是 PLLSAI 输出没达到预期,而不是 LTDC 时序错了。
6.2 快速显示中文:行偏移决定可写速度
RGB 屏显示中文最自然的做法是使用全字库点阵,把字模放在 Flash 里。16×16 点阵的中文字模一个字符占 32 字节,UI 上无需配置外接 GBK 字库芯片,只要把字模数据按行列写入显存即可。与 SPI OLED 需要逐字节往外传不同,这里可以直接用行指针向 SDRAM 写入连续内存。
void LCD_DrawFont16(uint16_t *buf, int x, int y, const uint8_t *font, uint16_t color) { for (int row = 0; row < 16; row++) { uint8_t lo = font[row * 2]; uint8_t hi = font[row * 2 + 1]; // buf 指向 SDRAM 帧缓冲首地址,1024 为屏宽 uint16_t *line = buf + (y + row) * 1024 + x; for (int bit = 0; bit < 8; bit++) { if (lo & (0x80 >> bit)) line[bit] = color; } for (int bit = 0; bit < 8; bit++) { if (hi & (0x80 >> bit)) line[bit + 8] = color; } } }这套写法避免了逐像素调用绘制函数,让 16 个像素点一次性落在连续显存区域。由于 LTDC 总会自动从帧缓冲刷新屏幕,写显存后的下一个行周期,新字符就会出现在物理屏上,不需要额外刷新指令。这也是 RGB 屏相比 SPI OLED 在显示中文时可以更流畅的原因:大量字模数据走 SDRAM 总线,而不是被限制在 10MHz 的 SPI 上。
最后说一个常见的性能隐患:如果你在启用 D-Cache 的工程里调试显存更新,CPU 写入的 SDRAM 数据可能还留在 Cache 里,而 LTDC 直接读物理内存,画面会出现局部花屏。遇到这种问题,可以先直接关闭 D-Cache,或者对显存区域执行SCB_CleanDCache_by_Addr刷新写入。这个细节和 LTDC 时序无关,但往往会在验证刷新率之后成为下一块绊脚石。
本文还有配套的精品资源,点击获取