ILI9325驱动FSMC时序极限与刷屏优化实战
简介:这份资源面向使用STM32F103系列单片机驱动TFT液晶屏的嵌入式开发者,重点解决ILI9325(兼容ILI9328)控制器在FSMC总线下的初始化与绘图问题。代码以STM32F103VET6为平台,将LCD的RS引脚接至A16、片选使用NE1,并给出RS接不同地址线时的地址计算思路,便于读者按自己的硬件接线快速移植。包内还实现了多种基础画图函数与字符串输出函数,附有RGB 24位转16位的换算公式及常用颜色定义,可直接用于界面绘制与字体显示。资源共22个文件,以13个C源文件和8个头文件为主体,另有1个HTML说明页,压缩包约36KB,按控制器型号与字体模块分目录组织,结构清晰。目前已有266人学习下载,适合需要快速搭建LCD底层驱动、理解FSMC时序与显存映射的初中级嵌入式开发者参考。
1. ILI9325 驱动:一块 240×320 的屏,为什么能把 FSMC 时序逼到极限
很多人第一次点亮 ILI9325,都会遇到一个反直觉的现象:代码逻辑完全正确,初始化序列逐条照抄,屏幕就是不亮,或者亮起来满屏雪花。问题往往不在驱动本身,而在总线时序——这块屏的写周期最短只有 66ns,而 STM32F103 的 FSMC 如果按默认参数配置,一个写周期轻松超过 100ns。ILI9325 与它的近亲 ILI9328 是当年最典型的 16 位并口 TFT 控制器,靠 FSMC 驱动几乎是 STM32F103 开发板的标准搭配。搞懂这套驱动,本质上是搞懂「一条 16 位数据从内核寄存器到屏幕像素」这条链路上,每一个时钟周期被谁吃掉了。这也是为什么值得把它的底层机制拆开看:它是最小、最完整的一块试验田,时序、寄存器、显存映射三件事一次讲透。
2. FSMC 与 ILI9325 的握手:一次写操作到底走了几步
2.1 从内核写寄存器到屏幕收到数据
在 STM32F103 上,CPU 并不直接操作 LCD 的引脚。开发者写的是*(volatile uint16_t *)0x60000000 = data这样一行代码,地址落在 FSMC 的 Bank1 NOR/PSRAM 区域。FSMC 控制器把这个地址翻译成片选信号 NE、地址线 A、写使能 NWE 和数据线 D0-D15 上的一组电平动作。ILI9325 侧看到的是:片选拉低、地址建立、写脉冲有效、数据稳定、写脉冲撤销。这五段各自对应 FSMC 的一个可配置时间参数:ADDSET(地址建立)、DATAST(数据保持)、ADDHOLD(地址保持)、BUSTURN(总线周转)、CLKDIV(时钟分频)。
常见的实现思路是把 ILI9325 挂在 FSMC_NE1 上,用 A16 或 A17 作为 RS(寄存器/数据选择)线。这样写0x60000000就是写命令,写0x60020000就是写数据,靠地址位区分,不需要额外的 GPIO 翻转。这个设计动机很直接:省掉一次 GPIO 操作,也就省掉几十纳秒的软件开销,在刷屏这种高频写场景里差别明显。
2.2 时序参数怎么算才不翻车
ILI9325 的数据手册给出的关键约束是写周期最小 66ns、写脉冲低电平最小 15ns、地址建立最小 0ns。FSMC 在 HCLK=72MHz 时,一个时钟周期约 13.9ns。要让写脉冲宽度满足 15ns,DATAST 至少取 2(2×13.9≈27.8ns,留余量)。要让整个写周期超过 66ns,ADDSET+DATAST 的组合要够。下面是一段典型的配置代码:
/* FSMC Bank1 NOR/SRAM1 时序配置,HCLK = 72MHz */ FSMC_NORSRAMInitTypeDef init; init.FSMC_AddressSetupTime = 1; /* ADDSET: 1 个 HCLK ≈ 13.9ns */ init.FSMC_AddressHoldTime = 0; /* ADDHOLD: 模式A 下通常为 0 */ init.FSMC_DataSetupTime = 3; /* DATAST: 3 个 HCLK ≈ 41.7ns */ init.FSMC_BusTurnAroundDuration = 0; init.FSMC_CLKDivision = 0; /* 异步模式不使用 */ init.FSMC_DataLatency = 0; init.FSMC_AccessMode = FSMC_AccessMode_A; /* SRAM 模式A */这里 ADDSET=1、DATAST=3,一个完整写周期约 4 个 HCLK,即 55.6ns,看起来低于 66ns。实际能稳定工作,是因为 FSMC 在模式 A 下写操作的实际波形还包含片选和写使能的额外建立/保持,加上 GPIO 输出延迟,真实总线周期会比理论值长。这也是为什么很多人照抄「DATAST=3」能跑、换一块屏就花屏——余量本来就不大。我一般会建议 DATAST 取 4 到 5,牺牲一点刷屏帧率换稳定,尤其是排线较长或屏体批次不同的情况。
提示:时序参数不是越小越「高效」。写周期压到极限,屏幕可能大部分时候正常,只在温度变化或供电波动时偶发花屏,这种玄学问题排查成本极高。
2.3 模式 A 与模式 B 的选择
FSMC 的 NOR/PSRAM 控制器支持模式 A、B、C、D 四种访问模式,区别在于地址建立、数据保持、写脉冲的先后关系。ILI9325 这类并口屏几乎都用模式 A:地址在写脉冲之前建立,写脉冲期间数据有效。模式 B 把地址保持拉长,适合某些需要地址在写脉冲后仍稳定的器件。选错模式不会立刻报错,但会出现「命令写进去了、数据没写进去」的诡异现象,因为 RS 线在错误的时刻翻转了。判断方法很简单:用逻辑分析仪抓 NWE 和 RS,看 RS 是否在 NWE 有效期间保持稳定。
3. 初始化序列:为什么寄存器顺序不能随便调
3.1 上电、复位、寄存器写入的三段式
ILI9325 的初始化不是「写一堆寄存器就完事」。它有一个明确的状态机:上电后必须先给复位脉冲(RESX 拉低至少 10us 再拉高),然后等待至少 5ms 让内部电荷泵稳定,再开始写寄存器。顺序错了,最常见的表现是屏幕全白或全黑,但读 ID 正常——因为控制器活着,只是显示子系统没启动。
初始化序列里几个关键寄存器承担不同职责:0x0000 是振荡器启动,0x0001 是驱动输出控制,0x0003 是进入待机前的入口设置,0x0007 是显示控制,0x00E5 到 0x00EF 是电源管理相关(电荷泵、VCOM 电压)。电源类寄存器必须按手册给的顺序和延时写,因为电荷泵是一级一级升压的,跳步会导致电压不足,屏幕偏色或亮度异常。
3.2 显存窗口与坐标映射
ILI9325 内部有 172800 字节的 GRAM,对应 240×320×2 字节。写像素前要先设置「窗口」:写 0x0050/0x0051 设水平起始/结束,0x0052/0x0053 设垂直起始/结束,再写 0x0020/0x0021 设当前写入坐标,最后写 0x0022 进入显存写入模式,之后连续写数据就会自动按窗口递增。这套机制的动机是减少每次画点的地址开销——设置一次窗口,可以连续刷一整块区域。
/* 设置矩形窗口,之后连续写 0x0022 即按窗口自动填充 */ static void lcd_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { lcd_write_cmd(0x0050); lcd_write_data(x0); /* 水平起始 */ lcd_write_cmd(0x0051); lcd_write_data(x1); /* 水平结束 */ lcd_write_cmd(0x0052); lcd_write_data(y0); /* 垂直起始 */ lcd_write_cmd(0x0053); lcd_write_data(y1); /* 垂直结束 */ lcd_write_cmd(0x0020); lcd_write_data(x0); /* 列地址 */ lcd_write_cmd(0x0021); lcd_write_data(y0); /* 行地址 */ lcd_write_cmd(0x0022); /* 进入 GRAM 写 */ }注意坐标的递增方向由 0x0003(AM/ID 位)决定,默认可能是横屏或竖屏,改方向要同时改窗口设置逻辑,否则会出现「画在 (0,0) 却显示在右上角」的错位。这是新手最容易踩的坑之一。
3.3 读 ID 与型号兼容
ILI9325 和 ILI9328 的寄存器集高度相似,但个别电源参数和 GRAM 读写时序有差异。初始化前读 0x0000 寄存器(读 ID)可以区分型号:ILI9325 返回 0x9325,ILI9328 返回 0x9328。很多驱动代码直接跳过这一步,结果在 9328 的屏上跑 9325 的序列,表现为颜色偏绿或对比度异常。加一个 ID 判断分支,成本很低,能省掉大量「这块屏是不是坏了」的怀疑。
4. 刷屏性能:为什么你的 fill 比别人的慢一半
4.1 单点写与块写的差距
最朴素的画点函数是「设坐标 → 写 0x0022 → 写颜色」,每次画点三次总线操作。刷满一屏 76800 个像素,就是 23 万次写。如果每次写之间还有函数调用开销和 GPIO 判断,帧率会低到肉眼可见的撕裂。块写的思路是:设一次窗口,然后连续写 0x0022 加数据,让控制器自己递增地址。这样每个像素只剩一次数据写,理论带宽提升接近三倍。
/* 块填充:窗口设好后连续写颜色,避免逐点设坐标 */ void lcd_fill_rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t n = (uint32_t)(x1 - x0 + 1) * (y1 - y0 + 1); lcd_set_window(x0, y0, x1, y1); while (n--) { LCD_DATA_PORT = color; /* 直接写 FSMC 数据地址,无函数调用 */ } }这里的LCD_DATA_PORT通常定义为*(volatile uint16_t *)0x60020000,编译器会生成一条 STRH 指令,没有函数调用、没有参数压栈。这是刷屏优化的第一层:把「写数据」从函数变成宏或内联。
4.2 DMA 能不能用
FSMC 挂在 AHB 总线上,理论上可以用 DMA 把内存里的图像数据搬到 LCD 数据地址。但 ILI9325 的写操作需要「先命令后数据」的严格顺序,DMA 只能负责数据段,命令段还得 CPU 写。实际收益取决于图像大小:小图 DMA 启动开销占比高,大图(比如整屏位图)能明显降低 CPU 占用。常见的实现思路是双缓冲加 DMA,CPU 准备下一帧的同时 DMA 刷当前帧,但要注意 FSMC 和 DMA 争抢总线时的优先级配置,配错了会出现数据错位。
4.3 颜色格式与字节序
ILI9325 支持 16 位 RGB565 和 18 位 RGB666。用 16 位并口时一般选 RGB565,一个像素两个字节。但字节序是个隐藏坑:STM32 是小端,FSMC 数据线 D0-D15 对应关系如果和屏的接线不一致,红蓝通道会互换,表现为「本该是红色显示成蓝色」。这不是代码 bug,是硬件接线和软件定义没对齐。解决办法是在初始化时写 0x0003 的入口模式位调整,或者在软件里做颜色通道交换。我一般会在驱动里留一个LCD_COLOR_SWAP宏,换屏时改一处即可。
5. 避坑与排查:那些让屏幕「时好时坏」的细节
5.1 现象:上电偶尔白屏,复位后正常
原因通常是复位脉冲宽度不够或电荷泵稳定时间不足。ILI9325 要求 RESX 低电平至少 10us,很多代码用delay_us(1)就拉高,边界情况下控制器没完成内部复位。解决是把复位低电平拉到 20us 以上,拉高后延时 5ms 再开始写寄存器,别省这几毫秒。
5.2 现象:刷屏时屏幕边缘有细碎噪点
原因是 FSMC 写周期余量不足,尤其在排线较长、屏体供电不稳时。DATAST 从 3 加到 4 或 5,噪点通常消失。另一个可能是 BUSTURN 设为 0 导致连续读写切换时总线冲突,适当加 1 到 2 个周期。这类问题用逻辑分析仪抓一次波形就能定位,比反复改代码快得多。
5.3 现象:读 ID 正常但显示全黑
读 ID 走的是命令+读数据路径,显示走的是 GRAM 写入路径,两者独立。全黑说明 GRAM 写进去了但显示没打开,检查 0x0007(显示控制)是否置了显示使能位,以及 0x0010(电源控制)的睡眠位是否还开着。初始化序列里漏写 0x0007 是高频错误。
5.4 现象:颜色整体偏绿或偏紫
优先怀疑型号不匹配(9325 序列跑在 9328 上)或 RGB 通道接线顺序。先读 ID 确认型号,再检查 0x0003 的入口模式位和颜色格式位。如果硬件已经焊死,软件做通道交换是最省事的补救。
5.5 现象:窗口设置后填充区域错位一行或一列
ILI9325 的窗口结束坐标是「包含」语义,不是「不包含」。设 0x0051 为 x1 时,实际填充到 x1 为止。如果按「结束坐标 = 起始 + 宽度」来算,会多填一个像素,累积到整屏就是错位。把宽度计算改成x1 - x0(不含)或x1 - x0 + 1(含)要统一,别两处混用。
6. 设计取舍:为什么这块屏的驱动至今仍值得读
ILI9325 的驱动代码在今天看不算优雅:寄存器序列冗长、时序靠经验值、型号兼容靠分支判断。但它把嵌入式显示的几个核心矛盾暴露得最干净。第一个矛盾是「总线带宽 vs 时序余量」——FSMC 参数调快一点帧率就上去,但稳定性下降,这个取舍在每一代显示接口上都存在,只是 SPI、MIPI 把它藏进了协议层。第二个矛盾是「硬件抽象 vs 性能」——把写数据封装成函数可读性好,但刷屏时函数调用开销吃掉大量带宽,所以成熟驱动都在关键路径上用宏和内联,把抽象留在上层。
第三个值得琢磨的是「初始化序列的本质是状态机」。很多人把初始化当成一张要背的表,实际上每条寄存器写入都在推动控制器从一个状态走到下一个状态,顺序和延时都是状态转移条件。理解这一点,换一块同系列但型号不同的屏时,就不是盲目试错,而是对照手册找差异点。我一般会在驱动里把初始化序列按功能分组(电源、显示、GRAM 配置),每组之间留注释说明依赖关系,这样下次改屏时能快速定位该动哪一段。
验证一套 ILI9325 驱动是否真的稳,我的习惯是三步:先用逻辑分析仪确认写周期波形满足手册最小值并留 20% 余量;再跑整屏三原色填充和渐变,看有没有局部色块异常;最后做连续刷屏半小时的压力测试,观察是否出现偶发花屏。这三步走完,基本能把时序和初始化的问题筛干净。从那以后我每次调新的并口屏驱动,都强制先抓波形再写代码,省下的返工时间远超那台逻辑分析仪的成本。希望帮到你。
本文还有配套的精品资源,点击获取