简介:这份资源面向嵌入式开发初学者与需要快速搭建显示交互界面的工程师,聚焦STM32平台下LCD显示与触摸屏的初始化配置。内容围绕HAL库展开,涵盖接口选择、GPIO与时钟配置、分辨率与颜色深度设定、初始化命令序列发送,以及触摸屏I2C/SPI通信与中断处理等关键环节,帮助读者理解从底层驱动到业务逻辑的完整链路。压缩包共179个文件,以h头文件、c源文件、o目标文件与d依赖文件为主,另含uvprojx工程、hex固件、map映射及少量说明文档,整体约1.14MB,工程结构完整可直接参考。目前已有763人学习下载,适合作为STM32显示模块开发的实践模板,便于对照代码梳理初始化流程与排错思路。
1. 从一块点不亮的屏说起:STM32+HAL 的 LCD 初始化到底在配什么
很多人第一次用 STM32 驱动 LCD,代码烧进去,屏幕要么全白、要么全黑、要么花屏,串口打印一切正常,就是没图像。问题十有八九不在 LCD 本身,而在初始化配置这一层。STM32+HAL 的 LCD 显示初始化,本质上是三件事的叠加:把 FSMC/FMC 或 SPI 外设配成 LCD 能听懂的时序,把 GPIO 复用和片选信号接对,再把 LCD 控制器自己的寄存器序列按手册写进去。这三件事任何一件错位,屏幕都不会给你面子。这篇笔记面向正在用 HAL 库点屏的嵌入式开发者,尤其是从标准库转过来、对 CubeMX 生成代码一知半解的人。我会按“先讲清总线选型,再给可复现的配置步骤,最后把踩过的坑摊开”的顺序写,目标是你照着能点亮一块 8080 并口或 SPI 接口的 TFT 屏,并且知道每一行配置在改什么。
2. 选 FSMC 还是 SPI:先看屏的接口,再看你的刷新需求
2.1 两种接口的本质差别与选型判断
LCD 模块常见接口分两类:并行接口(8080/6800)和串行接口(SPI/I2C)。STM32 这边对应的驱动路径完全不同。并行屏一般走 FSMC(F1/F4 系列叫 FSMC,F7/H7 叫 FMC),把 LCD 当成一个外部 SRAM 来读写,优点是刷屏快,320x240 的屏整屏刷新可以做到毫秒级;缺点是占引脚多,数据线 8 位或 16 位,加上控制线,轻松吃掉 20 多个 IO。SPI 屏接线少,四根线就能跑,但刷新率受 SPI 时钟限制,全屏刷新动辄几十毫秒,适合参数显示、小面积更新,不适合视频或高频波形。
选型判断可以按这个顺序走:先看手头屏的排线,有 D0-D15 和 RD/WR/CS/RS 的就是并行屏,只有 SCL/SDA/CS/DC/RES 的就是 SPI 屏。再看你的刷新需求,如果每秒要刷超过 10 帧全屏,SPI 基本没戏,老老实实上 FSMC。最后看引脚余量,FSMC 会占用特定端口的连续引脚,布线前先查芯片数据手册的复用表,别等到 PCB 画完才发现引脚冲突。
我一般会建议新手先用 SPI 屏跑通流程,因为接线简单、时序容错高,等逻辑跑顺了再换 FSMC 屏做性能优化。下面两节分别给两条路径的最小配置。
2.2 FSMC 并行接口的 CubeMX 配置与地址映射
以常见的 16 位 8080 并口屏为例,FSMC 配置的核心是把 LCD 的 RS 信号接到某根地址线上,这样写不同地址就等于写命令或写数据。常见做法是 RS 接 A0(或 A6,取决于你选的 bank),那么基地址 + 0 是命令,基地址 + 2 是数据(16 位宽度下地址步进为 2)。
CubeMX 里的操作步骤:
- 在 Connectivity 里选 FSMC,LCD Interface 选 LCD Interface。
- Chip Select 选 NE1(对应 Bank1),Memory type 选 LCD Interface。
- LCD Register Select 选 A0,Data 宽度选 16 位。
- 时序参数先填保守值:AddressSetupTime=2,AddressHoldTime=1,DataSetupTime=5,BusTurnAround=1,ClockDivision=HCLK/1,DataLatency 不用管(那是同步模式才用)。
生成代码后,HAL 会初始化 FSMC,但不会帮你定义读写宏。你需要自己加:
/* lcd_fsmc.h —— FSMC 地址映射与读写宏 */ #define LCD_BASE ((uint32_t)0x60000000) /* Bank1 NE1 基地址 */ #define LCD_CMD_ADDR (LCD_BASE) /* A0=0 写命令 */ #define LCD_DATA_ADDR (LCD_BASE + 2) /* A0=1 写数据,16位步进2 */ #define LCD_WR_CMD(cmd) (*(__IO uint16_t *)LCD_CMD_ADDR = (cmd)) #define LCD_WR_DATA(dat) (*(__IO uint16_t *)LCD_DATA_ADDR = (dat)) #define LCD_RD_DATA() (*(__IO uint16_t *)LCD_DATA_ADDR)逻辑说明:FSMC 把外部地址映射到 0x60000000 开始的区域,NE1 对应 Bank1 的片选。A0 接 RS 后,地址最低位决定 RS 电平,所以基地址写命令、基地址加 2 写数据。参数说明:AddressSetupTime 是地址建立时间,屏手册里一般写 tAS,单位是 HCLK 周期;DataSetupTime 对应写脉冲宽度 tPW,太小会丢数据,太大只是慢一点,所以宁可先大后调小。BusTurnAround 是读写切换的等待周期,读 LCD 的 ID 或状态寄存器时如果读回 0xFFFF 或 0x0000,先把这个值加到 2 以上试试。
2.3 SPI 接口的时钟极性与分频设置
SPI 屏的初始化重点在时钟极性和相位。绝大多数 LCD 控制器(比如常见的 ILI 系列)要求 CPOL=0、CPHA=0,也就是空闲低电平、第一个边沿采样。CubeMX 里 SPI 模式选 Full-Duplex Master 或 Transmit Only Master 都行,实际只用 MOSI,MISO 可以不接。
关键参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Clock Polarity | Low | 空闲低,匹配多数 LCD 控制器 |
| Clock Phase | 1 Edge | 第一个边沿采样 |
| Prescaler | 4 或 8 | 先慢后快,跑通再提 |
| Data Size | 8 bit | 命令和参数都是 8 位 |
| First Bit | MSB | 高位先出 |
初始化代码里,SPI 屏通常需要先拉低 CS,发命令,再发数据,最后拉高 CS。如果你用硬件 NSS,注意 HAL 的 SPI 传输函数不会自动控制 NSS,得手动写 GPIO:
/* lcd_spi.c —— SPI 写命令与写数据 */ static void LCD_SPI_WriteByte(uint8_t byte) { HAL_SPI_Transmit(&hspi1, &byte, 1, HAL_MAX_DELAY); } void LCD_WriteCmd(uint8_t cmd) { LCD_DC_Clr(); /* DC=0 表示命令 */ LCD_CS_Clr(); LCD_SPI_WriteByte(cmd); LCD_CS_Set(); } void LCD_WriteData(uint8_t dat) { LCD_DC_Set(); /* DC=1 表示数据 */ LCD_CS_Clr(); LCD_SPI_WriteByte(dat); LCD_CS_Set(); }逻辑说明:DC 引脚区分命令和数据,CS 每次传输都拉低再拉高,保证时序干净。参数说明:Prescaler 先设 8 或 16,等屏幕能正常显示后再逐步降到 2 或 4。如果降频后花屏,说明你的排线太长或没有加匹配电阻,不是 SPI 配置的问题。HAL_MAX_DELAY 在调试阶段可以用,量产代码里建议换成具体超时值,避免总线卡死时程序挂住。
3. 上电初始化序列:LCD 控制器寄存器怎么写才不花屏
3.1 从复位到 Sleep Out 的标准流程
LCD 控制器上电后不能直接写显示数据,必须按手册给的初始化序列走一遍。这个序列通常包括:硬件复位(RES 引脚拉低至少 10ms 再拉高)、软件复位(写 0x01)、Sleep Out(写 0x11)、设置像素格式(写 0x3A)、设置显示方向(写 0x36)、最后开显示(写 0x29)。每一步之间要留延时,尤其是 Sleep Out 之后,手册一般要求 120ms 以上。
我一般会把这些步骤写成一个初始化数组,用查表方式执行,方便换屏时只改表不改逻辑:
/* lcd_init_table.h —— 初始化命令表,以 ILI 类控制器为例 */ typedef struct { uint8_t cmd; uint8_t data[16]; uint8_t len; uint16_t delay_ms; } LCD_InitSeq; static const LCD_InitSeq lcd_init_table[] = { {0x01, {0}, 0, 120}, /* 软件复位,等 120ms */ {0x11, {0}, 0, 120}, /* Sleep Out,等 120ms */ {0x3A, {0x55}, 1, 10}, /* 像素格式 16bit/pixel */ {0x36, {0x00}, 1, 10}, /* 显示方向,先默认 */ {0x29, {0}, 0, 50}, /* Display ON */ };执行逻辑:
void LCD_Init(void) { LCD_Reset(); /* 硬件复位 */ for (int i = 0; i < sizeof(lcd_init_table)/sizeof(lcd_init_table[0]); i++) { LCD_WriteCmd(lcd_init_table[i].cmd); for (int j = 0; j < lcd_init_table[i].len; j++) { LCD_WriteData(lcd_init_table[i].data[j]); } if (lcd_init_table[i].delay_ms) { HAL_Delay(lcd_init_table[i].delay_ms); } } }参数说明:0x3A 的参数 0x55 表示 RGB565 格式,如果你写 0x66 就是 RGB666,但多数小屏只支持 16 位。0x36 的参数控制扫描方向和 RGB 顺序,常见值有 0x00、0x60、0xA0、0xC0,分别对应四个旋转方向,如果显示镜像或颜色反了,先改这个寄存器。延时不能省,尤其是复位和 Sleep Out,省了会出现“第一次上电正常、复位后花屏”的玄学问题。
3.2 设置显示窗口与写显存的正确姿势
初始化完成后,要往屏幕上画东西,必须先设置显示窗口(Column Address Set 和 Page Address Set),再写显存。窗口设置命令是 0x2A(列地址)和 0x2B(页地址),每个命令后面跟四个参数:起始高 8 位、起始低 8 位、结束高 8 位、结束低 8 位。设完窗口后写 0x2C,后面连续发的数据就会自动填充窗口区域。
/* 设置显示窗口,x0,y0 左上角,x1,y1 右下角 */ void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); LCD_WriteData(x0 >> 8); LCD_WriteData(x0 & 0xFF); LCD_WriteData(x1 >> 8); LCD_WriteData(x1 & 0xFF); LCD_WriteCmd(0x2B); LCD_WriteData(y0 >> 8); LCD_WriteData(y0 & 0xFF); LCD_WriteData(y1 >> 8); LCD_WriteData(y1 & 0xFF); LCD_WriteCmd(0x2C); /* 准备写显存 */ } /* 填充纯色,用于测试 */ void LCD_Fill(uint16_t color) { LCD_SetWindow(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1); for (uint32_t i = 0; i < LCD_WIDTH * LCD_HEIGHT; i++) { LCD_WriteData(color >> 8); LCD_WriteData(color & 0xFF); } }逻辑说明:窗口设好后,0x2C 之后的每个像素数据会自动按从左到右、从上到下的顺序填充,超出窗口会自动换行。参数说明:x0、x1 是列地址,范围 0 到屏宽减一;y0、y1 是页地址,范围 0 到屏高减一。如果你的屏是 240x320,但显示出来只有一半或者错位,检查窗口设置是否超出了实际分辨率,有些屏的显存比可视区域大,窗口设错会导致偏移。
4. 避坑与排查:点屏失败时先查这五个地方
4.1 现象:屏幕全白或全黑,背光亮但无图像
原因:初始化序列没执行完,或者复位引脚没接对。有些模块的 RES 引脚内部有上拉,悬空也能工作,但有些必须外部拉低再拉高。另外,如果你用的是 FSMC,片选信号 NE1 没有正确映射到 LCD 的 CS,写操作根本到不了屏。
解决:先用万用表量 RES 引脚,上电后应该是高电平,复位瞬间有一个低脉冲。如果没有,检查 GPIO 初始化和复位函数。FSMC 的话,用调试器看 0x60000000 地址写数据时,CS 引脚有没有拉低,没有就是 FSMC 配置里片选没使能。
4.2 现象:花屏、噪点、颜色错乱
原因:时序参数太快,或者数据线有干扰。FSMC 的 DataSetupTime 设成 1 或 2 的时候,长排线或高时钟下很容易出现建立时间不足。SPI 的话,Prescaler 太小导致时钟超过屏的手册上限。
解决:FSMC 先把 DataSetupTime 加到 10 以上,AddressSetupTime 加到 5,看是否改善。如果改善,再逐步往下调,找到稳定工作的最小值。SPI 先把 Prescaler 翻倍,如果花屏消失,说明是时钟太快,不是数据错。另外检查排线,杜邦线超过 10cm 就容易出问题,能焊就焊。
4.3 现象:显示方向不对,或者镜像
原因:0x36 寄存器的参数不对。这个寄存器的 bit7 控制页地址顺序,bit6 控制列地址顺序,bit5 控制行刷新方向,不同厂商的定义有细微差别。
解决:把 0x36 的参数在 0x00、0x60、0xA0、0xC0 四个值里轮换试一遍,同时配合窗口设置的坐标映射。如果旋转后触摸坐标不对,那是触摸屏的事,显示和触摸要分开调。我一般会在初始化表里把 0x36 单独拎出来,方便快速切换。
4.4 现象:读 LCD ID 返回 0xFFFF 或 0x0000
原因:读时序不对,或者 RS 地址映射错了。FSMC 读操作需要 BusTurnAround 时间,写操作不需要。如果 BusTurnAround 设成 0,读回来的数据就是乱的。
解决:把 BusTurnAround 设成 2 或 3,再读。如果还是不对,检查 RS 接的是 A0 还是 A6,地址偏移要跟着改。有些屏的读时序要求 RD 信号有最小脉冲宽度,FSMC 的 DataSetupTime 对读也生效,适当加大。
4.5 现象:换一块同型号屏,代码不变却不亮
原因:不同批次的屏,控制器可能换了,或者初始化序列有细微差别。尤其是那些没有明确型号的模块,卖家给的例程只能参考。
解决:先读 ID,确认控制器型号。读不到 ID 就按最通用的 ILI 序列试,再根据显示效果微调。我习惯在代码里留一个 LCD_ID 的读取和打印,换屏时先看 ID,再决定用哪套初始化表。这个习惯帮我省了很多次“代码没动但屏不亮”的后悔药。
5. 进阶技巧:用 DMA 和局部刷新把刷屏速度提上去
5.1 FSMC + DMA 的配置要点
FSMC 写显存是 CPU 逐字节搬,刷全屏时 CPU 基本被占满。用 DMA 可以把数据从内存搬到 FSMC 的数据地址,CPU 腾出来干别的。配置时注意:DMA 的外设地址填 LCD_DATA_ADDR,内存地址填你的显存缓冲区,传输宽度选半字(16 位),模式选 Normal 或 Circular。每次刷屏前重新设窗口,然后启动 DMA。
/* 用 DMA 刷全屏,color_buf 是预先填好的显存缓冲区 */ void LCD_Fill_DMA(uint16_t *color_buf, uint32_t pixel_count) { LCD_SetWindow(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1); HAL_DMA_Start(&hdma_memtomem, (uint32_t)color_buf, LCD_DATA_ADDR, pixel_count); /* 等待传输完成,实际项目里可以用中断或标志位 */ while (HAL_DMA_PollForTransfer(&hdma_memtomem, HAL_DMA_FULL_TRANSFER, 100) != HAL_OK); }参数说明:pixel_count 是像素个数,不是字节数,DMA 配置里 Data Width 选 Half Word 才能对应 16 位像素。如果传输一半就停,检查 DMA 的传输长度寄存器是不是 16 位,有些型号最大 65535,超过要分多次。另外,FSMC 的地址在 DMA 传输期间不能变,所以窗口设好后不要再写其他命令。
5.2 局部刷新与脏矩形思路
全屏刷新再快也有上限,实际 UI 里大部分区域是不变的。我一般会维护一个“脏矩形”列表,只刷新变化区域。实现上就是每次要更新某个控件时,把它的坐标范围记下来,合并重叠区域,最后只对合并后的矩形设窗口、写数据。这样即使 SPI 屏,也能做到流畅的局部更新。
验证方法:在屏幕上开一个每秒计数的数字,用全屏刷新时数字闪烁明显,用局部刷新只更新数字区域,闪烁基本消失。这个对比能直观看出局部刷新的价值。我自己的习惯是,任何 LCD 项目先跑通全屏填充,再改成局部刷新,最后才上 DMA。顺序反了,出了问题很难定位是 DMA 配置错还是窗口设错。希望帮到你。
本文还有配套的精品资源,点击获取