简介:面向STM32H7系列嵌入式开发者的7英寸RGB LCD驱动工程包,以STM32H750为例,采用纯寄存器方式直接操作LTDC控制器,避开HAL抽象层,清晰呈现显示控制底层细节,实现800x480分辨率图像输出与触摸屏交互。压缩包共44个文件,包含22个.h头文件、15个.c源码文件,以及Keil工程配置、启动文件、hex固件等,整体仅406KB,结构清晰便于移植学习。目前已有1306人学习下载,适用于工控界面、仪表显示、人机交互等场景,尤其适合希望深入理解MCU显示驱动原理的开发者。工程覆盖LTDC同步时序与像素格式配置、DMA2D高效图像传输、FT6x06触摸控制器事件解析等关键环节,并附有SDRAM、OLED、定时器等外设驱动,可直接编译烧录验证,为快速掌握STM32H7系列LCD驱动开发提供完整参考。
1. 为什么STM32H750驱动800x480 RGB屏要回到寄存器
STM32H750的内置Flash只有128KB,但主频跑到480MHz,配上LTDC之后,驱动一块7寸800x480的RGB屏并不是什么夸张的事。问题是,HAL库的LTDC驱动在初始化时要穿过多层结构体、时序参数校验和状态等待,代码一多,反而看不清硬件到底做了什么。尤其是做产品时换了一块分辨率相同但时序不同的屏,或者需要在启动早期就把画面拉起来,寄存器驱动那种“直接写LTDC_GCR、LTDC_AWCR、LTDC_LxSAAR”的方式,改起来最快,也最容易定位问题。这篇从LTDC的时序列起,落到H750上寄存器级驱动代码,最后把调试经验和几个常被问到的大坑一起说清楚。
2. LTDC时序结构与H750时钟域:先把像素时钟算准
2.1 LTDC在H750里的时钟来源与分频链路
LTDC不是随便给个时钟就能工作的,它的像素时钟需要和屏幕的时序严格匹配。H750上,LTDC的时钟一般来自PLL3的Q输出,再经过RCC里的一个可编程分频器,最终得到LCD_PCLK。这个时钟频率决定了屏幕每秒刷新多少帧,也决定了DMA2D搬运显存数据的节奏。
常见做法是在内存中先跑通一个最小系统:外部晶振25MHz,PLL1给CPU跑到480MHz,PLL3专门给LTDC生成像素时钟。比如要得到33.3MHz的像素时钟,就把PLL3Q配成66.6MHz,再让LTDC分频器除以2。分频系数放在RCC_D1CCIPR的相关位段里,写之前先清零,写之后要等待硬件就绪。
关键点在于,LTDC的时序寄存器里填的是时钟周期数,不是时间。所以算像素时钟时要反过来,先用屏幕要求的帧率和行列总数算出目标PCLK,再反推PLL3Q和分频器的值。下面这段代码演示了如何设置LTDC时钟源并写入分频因子。
RCC->D1CCIPR &= ~(3UL << 16); RCC->D1CCIPR |= (1UL << 16); // LTDC时钟源选择PLL3_Q uint32_t pll3q_hz = 66600000UL; // 假设PLL3Q输出66.6MHz uint32_t plck = pll3q_hz / ltdc_pixel_clock_hz; // 计算分频比 uint32_t div = plck - 1; RCC->D1CCIPR &= ~(0x1FUL << 24); // 清空LTDC分频字段 RCC->D1CCIPR |= (div & 0x1F) << 24; // 写入分频值这段代码的关键在于RCC_D1CCIPR的两段操作,选择时钟源和分频系数是独立的位段,必须分开操作,不能直接整寄存器赋值,否则会覆盖其他外设的时钟配置。pl3q_hz / ltdc_pixel_clock_hz算出来的是整数分频比,如果像素时钟不是整数倍,屏幕会出现缓慢的水平滚动或水波纹,所以PLL3Q尽量配置成像素时钟的整数倍。H750的LTDC分频字段宽度有限,像素时钟太低时会导致分频值溢出,这时候应该降低PLL3Q输出而不是盲目加大分频数。
2.2 800x480@60Hz的时序参数到底怎么填
7寸800x480的RGB屏,绝大多数使用40pin FPC接口,内部RGB666或RGB888信号直接走排线接到核心板。时序上,除了800x480的有效区域,还需要同步脉冲、后沿、前沿这三段消隐时间。以常见7寸屏的典型参数为例,用一组数据展示寄存器里实际填入的应该是多少。
| 参数 | 单位 | 典型值 | 说明 |
|---|---|---|---|
| 像素时钟PCLK | MHz | 33.3 | 可根据帧率微调 |
| HSW(行同步宽度) | 像素时钟周期 | 48 | 对应LTDC_SSCR的HSW位段 |
| HBP(行后沿) | 像素时钟周期 | 88 | 对应LTDC_BPCR的AHBP位段 |
| HFP(行前沿) | 像素时钟周期 | 40 | 出现在每行有效像素之后 |
| VSW(帧同步宽度) | 行数 | 3 | 对应LTDC_SSCR的VSH位段 |
| VBP(帧后沿) | 行数 | 32 | 对应LTDC_BPCR的AVBP位段 |
| VFP(帧前沿) | 行数 | 13 | 出现在每帧有效行之后 |
整帧总行宽等于800+48+88+40,也就是976像素周期;总行数是480+3+32+13,即528行。按60Hz帧率算,像素时钟是976乘以528再乘以60,约等于30.9MHz。这也是为什么很多屏标33.3MHz——用33.3MHz时实际帧率是64.6Hz左右,不会影响使用。
写入LTDC_SSCR和LTDC_BPCR时要注意,寄存器的值大部分需要减1。比如水平同步宽度48,写入HSW字段的是47。同理,LTDC_AWCR里填的有效宽度是800减1,有效高度是480减1。后面总宽度和总高度也要减1。这些寄存器都从0开始数,直接填时序表里的原始值会导致图像整体偏移一行或几像素。
提示:买屏时看时序表,有些厂商给的是HSW/HBP/HFP的具体值,有些给的是“最小/典型/最大”三段。寄存器驱动里尽量用典型值,不要为了缩边框把HBP压到最小值,否则在部分屏幕上会出现右侧颜色异常。
2.3 信号极性与屏幕发白:寄存器里要避开的LCD极化坑
LCD屏的显示效果受两个“极化”因素影响。一个是屏幕自身的偏光片方向,那个在硬件上已经固定,软件碰不到;另一个是LTDC输出的信号极性,这在寄存器里完全可控。实际调试中,屏幕整体发白、有雾感、或者画面像蒙了一层纱,很多时候不是偏光片问题,而是信号极性配置与屏幕不匹配。
LTDC_GCR里集中了四组极性控制:像素时钟采样边沿、DE数据使能有效电平、VSYNC有效电平、HSYNC有效电平。常见的RGB屏,PCLK在上升沿采样数据,DE为高有效,VSYNC和HSYNC为低有效。如果PCLK极性反了,画面会整体右移且颜色明显偏淡;如果DE极性反了,会出现满屏横条纹,严重时完全没法看。
极性的判断方法很简单,用逻辑分析仪抓屏的PCLK和DE,对照屏幕数据手册里的时序图看有效电平。没有逻辑分析仪时,可以按“画面发白优先试PCLK极性,横条纹优先试DE极性”的顺序排查。LTDC_GCR对应位段的值可以在运行时修改,每次只改一项,不要同时翻转多个极性,否则分辨不出来是哪个信号导致的问题。
3. 寄存器驱动LTDC初始化:从GCR到Layer配置的落地代码
3.1 全局初始化:先把GCR、SSCR、BPCR、AWCR、TWCR一次配齐
全局配置是LTDC工作的基础,它不涉及图层,只负责产生正确的时序。LTDC_GCR的使能位LTDCEN要放在最后打开,在此之前先把同步宽度、后沿、有效区域、总宽高全部写入。如果先使能LTDC再改时序,屏幕上会出现短暂的撕裂或抖动。
下面是一段可以在H750上直接使用的全局初始化核心代码,结构体方式组织时序参数,避免函数参数过多。
typedef struct { uint16_t hsync_pw; // HSW uint16_t hbp; // HBP uint16_t hfp; // HFP uint16_t vsync_pw; // VSW uint16_t vbp; // VBP uint16_t vfp; // VFP uint16_t width; // 有效宽度 uint16_t height; // 有效高度 } lcd_timing_t; void ltdc_global_init(const lcd_timing_t *tm) { LTDC->GCR = 0; // 先关闭LTDC LTDC->SSCR = ((uint32_t)(tm->vsync_pw - 1) << 16) | (uint32_t)(tm->hsync_pw - 1); LTDC->BPCR = ((uint32_t)(tm->vbp - 1) << 16) | (uint32_t)(tm->hbp - 1); LTDC->AWCR = ((uint32_t)(tm->height - 1) << 16) | (uint32_t)(tm->width - 1); LTDC->TWCR = ((uint32_t)(tm->vbp + tm->height + tm->vfp - 1) << 16) | (uint32_t)(tm->hbp + tm->width + tm->hfp - 1); LTDC->BCCR = 0x00000000; // 背景色黑色 LTDC->GCR |= LTDC_GCR_PCPOL | LTDC_GCR_DEPOL; // 按屏设定极性 LTDC->GCR |= LTDC_GCR_LTDCEN; // 最后使能 }GPIO复用配置、RCC时钟使能、以及AF引脚映射需要在调用这个函数之前完成。LTDC_GCR里的极性位,不同屏不一样,这段代码里先默认PCLK上升沿采样、DE高有效。如果换屏后出现发白,优先改这里。LTDC->TWCR的总宽高是通过后沿加有效区域加前沿算出来的,注意这个值也要减1,和前面对齐。
3.2 图层寄存器配置:一层ARGB8888还是两层ARGB1555
LTDC支持两个图层,每个图层有独立的窗口位置、像素格式、帧缓冲地址和alpha混合系数。800x480的屏幕,显存如果走RGB565,一帧只需768KB;走ARGB8888则要1.5MB。H750内部RAM不够放一帧,一般把显存放在外部SDRAM。
图层初始化的关键寄存器是LxWPR(窗口起点)、LxWVPCR(垂直起止)、LxWHPCR(水平起止)、LxPFCR(像素格式)、LxCACR(恒定alpha)、LxSAAR(帧缓冲首地址)和LxCFBLR(行长度与间距)。下面这段代码把一层配成全屏RGB565输出。
static LTDC_Layer_TypeDef *ltdc_layer = LTDC_Layer1; void ltdc_layer_init_rgb565(uint32_t fb_addr, uint16_t width, uint16_t height) { ltdc_layer->WPR = (0 << 16) | 0; // 窗口左上角(0,0) ltdc_layer->WVPCR = ((uint32_t)(height - 1) << 16) | 0; ltdc_layer->WHPCR = ((uint32_t)(width - 1) << 16) | 0; ltdc_layer->PFCR = 0x05; // RGB565 ltdc_layer->CACR = 0xFF; // alpha=255不透明 ltdc_layer->CFBLR = ((uint32_t)(width * 2) << 0) // 每行字节数 | ((uint32_t)(width * 2) << 16); // 行间距 ltdc_layer->CFBLNR = height; ltdc_layer->SAAR = fb_addr; ltdc_layer->CR = 1; // LEN使能图层 }这里WVPCR和WHPCR的高16位是终点,低16位是起点,和WPR的含义不同。WPR的起点坐标已经由(0,0)定义,WVPCR和WHPCR里的终点就可以直接填入width/height减1。CFBLR的低16位是帧缓冲每行字节数,高16位是行间距,当显存紧密排列且没有自定义间距时,两者相同。如果以后要做图像旋转或显存带调试信息,可以在高16位留出额外间距。
3.3 DMA2D填充与整屏刷新:寄存器模式下的搬运代码
页面切换、清屏、刷背景色,这些操作不能靠CPU把每个像素写给显存,那样在800x480下会占用大量CPU时间。用DMA2D的寄存器到存储器模式,一条指令就能完成整屏填充。DMA2D的填充模式不需要源地址,只需要目标地址、颜色值和像素总数。
void dma2d_fill_rgb565(uint32_t dst, uint16_t color, uint32_t pixel_cnt) { DMA2D->CR = 0x00030000; // MODE=11 寄存器到存储器 DMA2D->OMAR = dst; // 目标显存地址 DMA2D->OCOLR = (uint32_t)color; // RGB565颜色 DMA2D->OPFCCR = 0x05 | (0xFFUL << 24); // RGB565,alpha=255 DMA2D->NVR = pixel_cnt - 1; // 像素数减1 DMA2D->CR |= 1; // START while (DMA2D->CR & 1) {} // 等待完成 }DMA2D一次最多可填充的像素数受NVR位宽限制,800x480整屏是384000个像素,在这个限制内,直接算好总数调用即可。颜色值color是RGB565格式,比如红色是0xF800,绿色是0x07E0。如果想填充渐变色或半透明效果,DMA2D的寄存器到存储器模式做不到,需要用存储器到存储器带混合的模式,把源图像和目标混合后再写入显存。
提示:DMA2D填充期间不要让LCD的图层同时读取同一个SDRAM bank,否则数据总线仲裁会拉低DMA2D吞吐。常见的做法是把显存放在SDRAM的bank0,DMA2D的描述符或临时缓冲放到bank1。
4. 实际项目里的三点:SDRAM缓冲、L8字模显示中文、片外App不卡死
4.1 双缓冲与显存布局:为什么必须放在SDRAM
RGB屏没有内部显存,LTDC每一帧都要从帧缓冲地址读取全部像素数据,因此帧缓冲必须常驻内存。H750内部DTCM RAM虽然快,但LTDC不能直接访问DTCM,能被LTDC访问的只有AXI SRAM和外部SDRAM。800x480的RGB565帧缓冲需要768KB,已经超过了AXI SRAM的总量,所以外部SDRAM几乎是必然选择。
工程上常见的显存布局是:SDRAM低地址放两个RGB565全屏缓冲,第一个给LTDC当前显示,第二个给应用层绘制;两个缓冲地址相差正好一帧大小。在LTDC的垂直消隐中断里切换LxSAAR地址,就可以实现无撕裂的双缓冲。切换时要注意,LxSAAR不是写进去立刻生效,它会在下一帧开始的瞬间被硬件锁存,所以应用层只管改寄存器,不能紧接着清空旧缓冲。
| 区域 | 地址范围 | 用途 |
|---|---|---|
| FrameBuffer0 | 0xC0000000 - 0xC00BFFFF | LTDC全屏缓冲0,实际使用800x480x2字节 |
| FrameBuffer1 | 0xC00C0000 - 0xC017FFFF | LTDC全屏缓冲1,双缓冲切换用 |
| FontCache | 0xC0180000 - 0xC01FFFFF | 字模缓存或临时绘制区 |
| GUI堆区 | 0xC0200000 起 | 窗体控件或其他动态数据 |
双缓冲切换代码一般放在LTDC中断里,通过读当前显示地址决定下一次切到哪个缓冲。写入LxSAAR后置一个标志位,让应用层知道当前缓冲是否已经安全。如果屏幕出现撕裂,问题不是出在切换时刻,而是写入LxSAAR后没有等到垂直消隐就开始了新一帧的绘制。
4.2 用L8图层在LCD屏显示中文:省一半显存的字模方案
业内很多方案是把中文转成点阵字模,然后用ARGB8888的单色位图刷到显存。这种做法一屏显示几十个汉字时还没问题,字多了CPU占用就上去了。寄存器驱动下更干净的做法是使用LTDC的L8像素格式,配合官方叫CLUT的颜色查找表,字模数据直接作为显存内容。
中文显示需要先解决字模数据来源。16x16点阵汉字每个字占32字节,按HZK16字库的排列方式,通过GB2312编码算出偏移量。下面是一段从字库数组中提取字模并按单色写像素的示例。
static const uint8_t font16[]; // 16x16汉字字模数据,外部数组或闪存映射 void lcd_draw_gb2312_16(uint16_t x, uint16_t y, uint16_t hi, uint16_t lo, uint16_t color, uint16_t *fb) { uint32_t offset = (((uint32_t)(hi - 0xA1) * 94 + (lo - 0xA1)) * 32); for (int row = 0; row < 16; row++) { for (int col = 0; col < 16; col++) { uint8_t byte = font16[offset + row * 2 + col / 8]; if (byte & (0x80 >> (col % 8))) { fb[(y + row) * 800 + (x + col)] = color; } } } }这段代码按HZK16的布局逐行取模,每个字32字节,每行2字节。hi和lo是汉字GB2312内码的高字节和低字节,偏移公式把汉字内码转换成字库文件中的物理位置。每个点是否被点亮,取决于对应字节中位的值,一句话点阵字模的核心就是“位为1就画前景色”。这幅代码性能不理想,但作为验证字库路径完全没有问题。
要大幅度提速,就把所有要显示的汉字在初始化时转成L8格式的位图,每个像素一个字节,内容不是颜色值而是CLUT索引。这样真正显示时只需要用DMA2D把L8数据从外部Flash拷贝到显存,不再逐点判断。配合LTDC的CLUT功能,0号索引设成屏幕背景色,1号索引设成文字颜色,整体显示效率和位图相差无几,但显存占用只有RGB565的一半多点。
4.3 STM32H750片外App不再卡死:QSPI XIP启动时LTDC初始化顺序
H750的片内Flash只有128KB,工程稍大一点就会把应用放到外部QSPI Flash,用XIP方式直接执行。很多人在这种启动方式下遇到一个诡异现象:直接跳转片外App卡死,屏幕白屏或花屏。最常见的根因是向量表和时钟初始化顺序问题。
片外App里如果再一次重新初始化PLL和LTDC时钟,而此时代码还在QSPI Flash中执行,PLL重新锁定期间Flash读取速度被拉低,XTIP加速失效,直接导致取指失败。寄存器驱动下正确的做法是,把PLL、LTDC时钟树、SDRAM和LTDC全局初始化全部放在内部flash的boot里做完,片外App只做图层配置和功能逻辑。boot跳转前把QSPI的连续读传输模式配好,App中不要再改动PLL3和LTDC时钟分频字段。
另一个隐蔽的卡死原因是DMA2D的目标地址不小心写到了0x90000000附近,也就是QSPI Flash映射区。DMA2D往Flash地址写数据,硬件不会拦截,但会一直等到总线超时,表现为程序停在DMA2D等待完成标志处。调试这类问题时,先查看DMA2D的CR寄存器是否还停在START位,如果停了,检查OMAR是不是落进了Flash映射区。
提示:片外App里做软件复位后,如果boot代码立即检查复位标志并再次初始化LTDC,要先复位LTDC的图层使能位,否则新初始化的图层寄存器不会干净,屏幕上会出现残留透明色块。
4.4 背光亮度控制:别让背光PWM干扰LTDC画面
7寸RGB屏的背光通常是40pin接口里的LED+序列,通过一路PWM控制亮度。背光PWM频率太低会出现肉眼可见的闪烁,工程上普遍做法是使用定时器PWM输出,频率放在1kHz以上,比如8kHz。PWM极性需要注意,部分屏的背光驱动板低电平点亮,占空比越大越暗,和常规直觉相反。
背光控制不应该影响LTDC的时钟和DMA2D带宽。PWM定时器挂在APB总线上,和LTDC的像素时钟走不同的时钟域,两者之间没有直接同步关系,一般不会对画面产生干扰。但背光PWM和PCLK如果恰好频率接近,屏幕上可能出现缓慢移动的明暗条纹,这种问题只能通过调整PWM频率或PCLK频率来错开。
如果想在寄存器驱动里加亮度渐变,可以不用软件PWM逐个延时翻转IO,而是使用定时器的自动重载值配合捕获比较寄存器做“硬件呼吸灯”。亮度调节范围按8位线性即可,不需要对数曲线,因为LED背光的非线性并不明显。
5. 调试技巧:从错误画面反推LTDC寄存器配置
5.1 白屏、花屏、偏色分别看什么寄存器
白屏是最常见的现象,优先确认LTDC_GCR的LTDCEN是否被外部代码关闭,其次看LTDC_GCR的极性位是否和屏匹配。若这两项正常,再量LCD_PCLK引脚的实际频率,和LTDC_AWCR中有效宽度对应起来计算。PCLK频率差得太多时,屏幕不会白屏,而是出现宽幅滚动横条。
花屏要区分是满屏雪花还是局部颜色错乱。满屏雪花且不断跳变,一般是PCLK采样边沿相反,导致每个像素的数据错位一位,肉眼看起来像噪声。局部颜色错乱,比如红色通道和蓝色通道互换,先检查RGB信号线序,不要急着改寄存器。40pin接口里,有些屏按RGB888排列,有些只接RGB666,高两位悬空时颜色饱和度会异常偏淡。
画面整体右移或下移,是消隐参数问题。比如图像右侧黑边宽度不对,改HBP;图像顶部有黑线或压缩感,改VBP。移位有一整屏大小,比如画面看起来像横着挪了半个屏,那多半是总宽度TWCR算错,导致LTDC从错误的行位置开始同步。
5.2 用LIPCR中断验证帧率是否真实达到60fps
很多项目调完屏幕能显示就收工,但实际帧率只有40fps甚至更低。判断标准很简单,在LTDC中断里统计行中断或帧中断次数,用定时器计时,就能算出真实刷新率。这里使用LTDC的行中断LIPCR,把它设成总的高度减1,每次扫描到最后一行的时刻触发中断。
void ltdc_isr(void) { uint32_t isr = LTDC->ISR; if (isr & LTDC_ISR_LI) { LTDC->ICR = LTDC_ISR_LI; // 清除行中断标志 frame_counter++; // 帧计数器递增 } }把LIPCR寄存器的值设置成TWCR的低16位,也就是总行数减1。这样中断实际发生在每帧的最后一行,等效于帧结束中断。应用层每秒读取一次frame_counter,差值就是实际帧率。如果实际帧率远大于预期,可能是LTDC时钟配置错误导致像素时钟过高,这时画面一样能显示,但屏体发热和信号质量都会变差。
帧率偏低时,检查DMA2D或CPU是否频繁访问SDRAM。LTDC读取显存的带宽优先级低于CPU和DMA2D,当CPU大量写显存时,LTDC会因为等不到总线而丢帧。此时把绘制窗口改成脏矩形区域,或者插入DMA2D的FIFO等待时间,都是寄存器驱动下直接可用的优化手段。
本文还有配套的精品资源,点击获取