
简介本资源面向使用STM32F103C8T6最小系统板驱动2.8寸TFT液晶的嵌入式开发者与学习者针对48脚芯片不具备FSMC功能这一限制采用GPIO模拟方式实现ILI9341的16位并口显示A端口0至15位作为数据总线适合入门液晶驱动、练习时序控制与模块化硬件搭建的初中级工程师参考。压缩包共182个文件约3.16MB包含33个c源文件与37个h头文件构成驱动与STM32标准外设库主体另有41个汇编启动文件、22个o编译产物、14个d依赖文件及uvproj、uvopt等Keil工程配置还附带hex、axf、map等可直接烧录与调试的产物目录结构完整便于二次开发。目前已有5092人学习下载读者可获取一套可运行的ILI9341驱动代码与工程模板理解模拟并口刷屏的时序实现并在此基础上扩展图形显示与触屏功能触屏输入暂未实现。1. 一块 2.8 寸屏点不亮八成不是屏的问题STM32F103C8T6 驱动 ILI9341 的 2.8 寸 TFT LCD是嵌入式入门里最经典也最容易翻车的一组搭配。很多人拿到一块标着 2.8 寸、240×320 分辨率、带 XPT2046 触摸的模块焊上排针、接好杜邦线、烧进例程屏幕要么纯白、要么花屏、要么干脆一点反应没有。第一反应往往是“屏坏了”或者“买到了假货”但实际排查下来绝大多数问题出在供电、复位时序、FSMC 配置或者初始化命令序列上屏本身反而是最不容易坏的那一环。这篇笔记面向的是手上已经有这块最小系统板和这块屏、想把图形刷出来的开发者。我会把从硬件连线、并口时序、ILI9341 初始化、到刷屏和局部刷新验证的完整路径讲清楚参数怎么设、坑在哪、失败时先看什么都落到可复现的步骤上。看完你应该能自己判断是接线错了、时序不对还是初始化命令少发了一条。2. 先搞清楚 ILI9341 到底怎么被“驱动”起来2.1 8080 并口和 SPI 的选型差别ILI9341 这颗驱动 IC 支持三种主机接口8080 并口、SPI 三线/四线、以及 RGB 接口。2.8 寸模块上最常见的是 8080 并口和 SPI 两种引出方式。选哪种直接决定你后面代码的写法和你 MCU 的引脚预算。8080 并口分 8 位和 16 位两种数据宽度。16 位并口一次传一个像素的高低位刷一屏 240×320 只需要 76800 次写操作速度上明显占优适合做动画或频繁刷新。代价是它要占用 16 根数据线加若干控制线对 STM32F103C8T6 这种 48 脚封装来说引脚会非常紧张通常得靠 FSMC可变静态存储控制器来扛否则软件模拟并口时序会慢到怀疑人生。SPI 接口只需要 4 根线SCK、MOSI、CS、DC再加一根 RST 和 BLK引脚压力小得多接线也简单。代价是速度SPI 一次只能传一位刷一屏要传 240×320×16 个时钟位即使跑到 18MHz整屏刷新也在几十毫秒量级做静态界面够用做流畅动画就吃力。我一般的判断是如果只是显示文字、图标、简单界面SPI 足够接线省心如果要做波形、视频帧、频繁局部刷新优先上 FSMC 并口。下面两条路径我都会给到关键配置你可以按自己的板子选。2.2 供电和背光最容易被忽略的第一道坎ILI9341 模块的逻辑供电是 3.3V但背光 LED 那一路经常是独立的。很多模块把背光 LED 的限流电阻做在板上直接接 3.3V 就能亮也有模块把背光引脚单独引出需要你自己串电阻或者用三极管控制。如果你接的是 5V 逻辑而模块没有电平转换长期跑下来逻辑口容易受损表现就是时好时坏。背光不亮和屏幕不显示是两回事。背光不亮你看到的是全黑背光亮但没数据你看到的是白屏或者灰屏。排查时先把这两个现象分开能省掉一半时间。提示上电前用万用表量一下模块 VCC 和 GND 之间有没有短路再量背光引脚对地电阻确认限流电阻存在。这一步花两分钟能避免烧掉整块屏。2.3 用 SPI 跑通最小显示接线与初始化先给一条 SPI 路径的最小可跑通方案。以 STM32F103C8T6 的 SPI1 为例引脚分配如下信号STM32 引脚说明SCKPA5SPI1 时钟MOSIPA7SPI1 数据输出CSPA4片选低有效DCPA3数据/命令选择RSTPA2复位低有效BLKPB0背光控制可接 3.3V 常亮初始化 SPI 的代码大致如下用的是标准外设库风格HAL 库把寄存器操作换成对应函数即可// SPI1 初始化主机模式8 位数据模式 0 void LCD_SPI_Init(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // SCK PA5 和 MOSI PA7 复用推挽 gpio.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); // CS PA4、DC PA3、RST PA2 普通推挽输出 gpio.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_3 | GPIO_Pin_2; gpio.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, gpio); SPI_InitStructure.SPI_Direction SPI_Direction_1Line_Tx; // 只发不收 SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; // 模式 0 SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; // 72M/418M SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, spi); SPI_Cmd(SPI1, ENABLE); }这段代码里几个参数值得说清楚。SPI_BaudRatePrescaler_4把 72MHz 的 PCLK2 分频到 18MHz这是 ILI9341 在 3.3V 下比较稳妥的上限再往上跑有些批次会丢数据。SPI_CPOL_Low和SPI_CPHA_1Edge组合成模式 0是 ILI9341 的默认采样方式如果你接的是模式 3 的屏这里要改成SPI_CPOL_High加SPI_CPHA_2Edge。SPI_Direction_1Line_Tx表示只发送不接收省掉 MISO 引脚因为写屏根本不需要读回数据。写命令和写数据靠 DC 引脚区分封装成两个函数// 写一个命令字节 void LCD_WriteCmd(uint8_t cmd) { LCD_DC_CLR(); // DC 拉低表示命令 LCD_CS_CLR(); SPI_WriteByte(cmd); LCD_CS_SET(); } // 写一个数据字节 void LCD_WriteData(uint8_t data) { LCD_DC_SET(); // DC 拉高表示数据 LCD_CS_CLR(); SPI_WriteByte(data); LCD_CS_SET(); }SPI_WriteByte内部就是等 TXE 标志、写 DR、等 BSY 清零这三步。DC 的拉高拉低必须在 CS 有效之前完成否则命令和数据会被屏当成同一种东西表现就是初始化全乱。2.4 ILI9341 初始化命令序列的关键几条初始化序列网上一搜一大把但真正决定能不能点亮的是前几条。下面这段是精简后的核心序列省略了伽马校正等不影响点亮的项void LCD_Init(void) { LCD_RST_CLR(); Delay_ms(20); LCD_RST_SET(); Delay_ms(120); // 复位后必须等够 120ms LCD_WriteCmd(0x01); // 软复位 Delay_ms(120); LCD_WriteCmd(0x11); // 退出睡眠 Delay_ms(120); LCD_WriteCmd(0x36); // 内存访问控制 LCD_WriteData(0x48); // 横屏BGR 顺序 LCD_WriteCmd(0x3A); // 像素格式 LCD_WriteData(0x55); // 16 位 RGB565 LCD_WriteCmd(0x29); // 开显示 }0x11退出睡眠后那 120ms 不能省很多白屏就是这里等得不够。0x36的 0x48 决定扫描方向和 RGB/BGR 顺序如果你发现红色显示成蓝色改这一位就行。0x3A的 0x55 是 16 位色写 0x66 是 18 位一般用 16 位省带宽。0x29开显示必须放在最后前面配置没完成就开显示屏会锁在异常状态。3. 用 FSMC 并口把刷新速度拉起来3.1 FSMC 映射到 ILI9341 的地址规划STM32F103C8T6 的 FSMC 支持 NOR/PSRAM 模式可以把 ILI9341 当成一个 16 位宽的 SRAM 来访问。关键是把 RS也就是 DC引脚接到某根地址线上这样写命令和写数据就变成访问两个不同地址不用再手动拉 DC。常见接法是把 RS 接到 FSMC_A16那么写命令地址 基地址 0写数据地址 基地址 (1 16) × 2因为 FSMC 是 16 位宽地址线每加 1 对应 2 字节所以 A16 对应偏移是 0x20000。基地址选0x60000000NE1 片选于是命令地址0x60000000数据地址0x60020000。#define LCD_BASE ((uint32_t)0x60000000) #define LCD_CMD_ADDR (LCD_BASE) #define LCD_DATA_ADDR (LCD_BASE 0x20000) #define LCD_WR_CMD(cmd) (*(__IO uint16_t *)LCD_CMD_ADDR (cmd)) #define LCD_WR_DATA(dat) (*(__IO uint16_t *)LCD_DATA_ADDR (dat))这样写屏就是一条赋值语句编译器直接生成 FSMC 总线写速度比软件模拟快一个数量级。3.2 FSMC 时序参数怎么算FSMC 的读写时序由 BTCR 寄存器控制核心是三个参数地址建立时间 ADDSET、地址保持时间 ADDHLD、数据建立时间 DATAST。ILI9341 的 8080 时序要求是写脉冲宽度最小 66ns读周期最小 355ns读 ID 时。以 72MHz 的 HCLK 为例一个时钟周期约 13.9ns。写时序取ADDSET 1约 14nsDATAST 5约 70ns满足 66ns 最小写脉冲读时序要慢得多取ADDSET 15DATAST 60配置代码// FSMC NOR/SRAM 模式配置NE1 片选 FSMC_NORSRAMInitTypeDef fsmc; FSMC_NORSRAMTimingInitTypeDef timing; timing.FSMC_AddressSetupTime 1; timing.FSMC_AddressHoldTime 0; timing.FSMC_DataSetupTime 5; timing.FSMC_BusTurnAroundDuration 0; timing.FSMC_CLKDivision 0; timing.FSMC_DataLatency 0; timing.FSMC_AccessMode FSMC_AccessMode_B; fsmc.FSMC_Bank FSMC_Bank1_NORSRAM1; fsmc.FSMC_DataWidth FSMC_DataWidth_16b; fsmc.FSMC_MemoryType FSMC_MemoryType_SRAM; fsmc.FSMC_MemoryDataWidth FSMC_MemoryDataWidth_16b; fsmc.FSMC_WriteOperation FSMC_WriteOperation_Enable; fsmc.FSMC_ExtendedMode FSMC_ExtendedMode_Disable; fsmc.FSMC_ReadWriteTimingStruct timing; FSMC_NORSRAMInit(fsmc); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE);FSMC_AccessMode_B表示 SRAM 模式读写用同一套时序。如果你要读 ID 或者读显存得开FSMC_ExtendedMode_Enable给读操作单独配一套更慢的时序否则读回来的数据是错的。3.3 刷一屏纯色验证并口通路并口配好后用刷纯色做第一次验证void LCD_FillColor(uint16_t color) { uint32_t i; LCD_SetWindow(0, 0, 239, 319); // 设置全屏窗口 LCD_WR_CMD(0x2C); // 写显存命令 for (i 0; i 240 * 320; i) { LCD_WR_DATA(color); } }0x2C是写显存命令发完之后连续写数据就会自动按窗口递增地址。LCD_SetWindow内部发的是0x2A列地址和0x2B页地址各带四个参数起始高、起始低、结束高、结束低。窗口设错颜色会只刷一部分或者刷到屏幕外。如果刷出来是纯色但颜色不对先查0x36的 BGR 位如果是条纹或者雪花查 FSMC 时序和接线如果是全白回到初始化序列重点看0x11后的延时和0x29的位置。4. 避坑与排查点不亮时按这个顺序查4.1 现象背光亮但屏幕全白原因通常是初始化序列没跑完或者复位时序不对。ILI9341 上电后内部状态机需要完整的复位脉冲RST 低电平至少要维持 10us很多代码只拉低一两个指令周期就拉高屏根本没复位。解决把 RST 拉低后延时 20ms 再拉高拉高后再等 120ms 再发命令。这个延时宁长勿短屏不会因为你多等而坏。4.2 现象SPI 模式下花屏、颜色错位原因多半是 SPI 模式选错。ILI9341 默认模式 0但有些模块因为走线或者上拉电阻实际工作在模式 3。表现是数据错位、颜色块乱跳。解决把 CPOL 和 CPHA 换成模式 3 试一次。如果换完正常说明模块的采样边沿和你原来的配置反了。另外检查 SPI 分频18MHz 跑不稳就降到 9MHz 验证确认是速度问题还是模式问题。4.3 现象FSMC 并口刷屏有随机噪点原因是 FSMC 写时序太快数据还没稳定就被屏采样。尤其是杜邦线连接、线长超过 10cm 的时候信号完整性会明显变差。解决把 DATAST 从 5 加到 8 或 10ADDSET 从 1 加到 2。如果噪点消失说明就是时序余量不够。长期方案是缩短接线、加地线隔离或者把屏直接插在转接板上。4.4 现象显示内容整体偏移或镜像原因是0x36内存访问控制的扫描方向位设错。这一字节的 bit7 控制 MY、bit6 控制 MX、bit5 控制 MV、bit3 控制 BGR。横屏和竖屏切换就是改这几位。解决竖屏常用 0x48 或 0x08横屏常用 0x28 或 0xE8具体取决于你的排线方向和想要的坐标系。改完重新刷一屏看原点在哪个角再决定要不要翻转。4.5 现象局部刷新时旧内容残留原因是没设窗口就直接写数据或者窗口设了但没重新发0x2C。ILI9341 的显存地址在每次0x2C后从窗口起点开始如果你连续写两次局部区域但中间没重设窗口第二次会从上次结束的位置继续。解决每次局部刷新前都完整走一遍0x2A、0x2B、0x2C不要图省事复用窗口。这个开销很小但能避免大部分残影问题。5. 把刷新率再压一压局部刷新和 DMA 的配合前面刷一屏纯色SPI 路径大概要几十毫秒FSMC 路径能压到几毫秒。但实际界面里你不需要每次都刷全屏状态栏、按钮、数值这些区域是分开变的。把全屏刷新改成局部刷新是提升响应最直接的手段。局部刷新的核心是只对变化区域设窗口。比如一个 80×24 的数值显示区每次只刷这 1920 个像素SPI 下也就一两毫秒。做法是把要刷的区域坐标传进LCD_SetWindow然后只写这块区域的数据。// 只刷新 (x, y) 起、宽 w 高 h 的区域 void LCD_UpdateArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const uint16_t *buf) { uint32_t i, n (uint32_t)w * h; LCD_SetWindow(x, y, x w - 1, y h - 1); LCD_WR_CMD(0x2C); for (i 0; i n; i) { LCD_WR_DATA(buf[i]); } }buf是你在内存里维护的一块像素缓冲界面逻辑只改这块缓冲改完调一次LCD_UpdateArea推过去。这样界面逻辑和刷屏解耦也方便后面接 DMA。FSMC 路径下如果 MCU 支持 DMA 到 FSMC 的通道可以把buf直接交给 DMACPU 去处理下一帧逻辑。STM32F103C8T6 的 DMA 通道和 FSMC 的配合需要查具体通道映射不是所有通道都能触发 FSMC 写。一个稳妥的验证方法是先用 CPU 轮询写测出单区域耗时再换 DMA 对比看提升是否值得。最后说一个我自己的习惯每次改完初始化序列或者时序参数先刷三屏纯色——红、绿、蓝再刷一屏棋盘格。纯色验证颜色通道和窗口棋盘格验证地址递增和边界。这四屏过了再往上叠文字和图形出问题也能快速定位是底层还是上层。这个习惯帮我省过很多次“以为是屏坏了”的返工。希望帮到你。本文还有配套的精品资源点击获取