简介:STM32 ILI9325驱动是一套针对STMicroelectronics 32位微控制器平台开发的LCD显示控制代码,专门用于驱动ILI9325控制器的TFT彩色液晶屏,适合嵌入式初学者和项目开发者在快速搭建人机界面时直接参考。资源包内共6个文件,包含3个头文件和3个源文件,核心代码包括lcd.c/lcd.h显示驱动、main.c主程序示例以及stm32f10x_it.c/stm32f10x_conf.h等基础配置与中断处理文件,压缩包整体仅10KB,结构紧凑、易于移植。代码覆盖了初始化寄存器配置、数据传输、基础绘图与屏幕刷新流程等关键环节,能够帮助使用者理解STM32的GPIO控制、串行通信协议以及显示驱动的整体架构。已有240人学习浏览,适合需要在STM32项目中快速集成ILI9325屏幕、缩短底层调试周期的开发者下载参考。
1. 这块 3.2 寸屏让 STM32 驱动变得有章可循:从初始化到刷满屏的实测路径
年前接了个小项目,要在 STM32 上点亮一块 3.2 寸 320×240 的液晶屏,主控是 ILI9325。折腾了三天才把画面稳定刷出来,回头一看,真正卡人的不是时序手册,而是寄存器序列里那几个被抄来抄去又不解释用途的配置项。这篇文章把我拆过的 ILI9325 驱动整个摊开:控制器特性、寄存器初始化、像素操作函数、翻车现场和最后的帧率优化,全是我按实际板子调试过的路径。适合正准备在 F103 或 F407 这类 MCU 上接 8080 并口屏的开发者,新手能一步步点屏,老手也能直接拿走初始化序列去核对时序参数。
2. ILI9325 控制器与 8080 并口:为什么选这块屏、怎么接才不翻车
2.1 控制器特性速览:16 位色深、内部 GRAM 与像素格式的对应关系
ILI9325 是一颗 TFT LCD 控制芯片,常见搭配是 3.2 寸 320×240 面板,RGB 666 或 RGB 565 两种像素格式。它内部带了 240×320×18bit 的 GRAM,也就是说每一个像素点在内部是 18 位存储的,但通过 16 位数据线可以一次写入一个像素的 RGB 565 数据。这句话看着简单,实际对着寄存器手册时有个隐蔽点:写 16 位颜色值时,高 5 位是 R,中间 6 位是 G,低 5 位是 B,但控制器里的颜色位序可以通过寄存器 0x0036 翻转,这一下就坑了不少人。
另一个特性是它自带一个振荡器,外部晶振不是必需的,大部分板子直接把 OSC 引脚接 VCC 用内部时钟。我手头有块板子,外部没接晶振,照样跑得好好的。读取寄存器用 8080 并口的 RD 时序也能读回 GRAM 里的数据,这意味着可以做屏幕内容的回读校验,对调试来说非常有用。刷色块、显示图片、画几何图形这一类需求,内部 GRAM 足够支撑,不需要额外显存芯片。
从并行接口的角度看,ILI9325 支持 8080 和 6800 两种时序,我们通常用 8080 模式,因为 STM32 的 FSMC 接口就是照着 8080 的时序设计的,直接硬件握手不需要软件模拟。6800 模式在开发板上很少见,如果布线时只引了 8080 的信号,就没法在软件里切换,这点在画板子之前就要确认。
像素格式上,ILI9325 默认上电是 18 位透传,不设寄存器的话,16 位数据线写进去会出现颜色明显偏灰的问题,因为低两位被忽略或者错位。必须在初始化序列里把 16 位作为数据位宽,并设置 RGB 565 格式,这一条是无数白屏和颜色怪异案例的根源之一。
2.2 引脚连接与 FSMC 配置:地址线复用、数据宽度、建立时间三个关键决策
接线看起来是把模块上的 LCD 引脚和对齐 STM32 的 FSMC 引脚,但其中有两个点要提前想清楚。
第一点是地址线复用。模块上通常有 RS 引脚(也叫 DC/D/C),接到 STM32 的某个地址线,比如 F103 上很多例程用 FSMC 的 A16 或 A10。选哪根地址线决定了寄存器地址的偏移量,因为 8080 时序里 RS 高低电平对应写命令还是写数据,而 FSMC 把 RS 当作地址线的一位来翻转。常见做法是把 RS 接到 A16,那么命令地址就是 Bank1 基址加 0x40000,如果用 A10 就是加 0x80000,这个值写错的话,发出的所有命令都会落在错误的位置。
第二点是数据宽度。FSMC 可以配置成 8 位或 16 位,ILI9325 的数据线是 DB0 到 DB15,直接用 16 位模式最省事。如果要省引脚接到 8 位模式,读写都要分两次,速度直接砍半,除非引脚实在不够,不然别走这条路。我在一块板子上用过 8 位模式,刷图时那个慢到让人怀疑程序死循环了。
第三点是建立时间和保持时间。很多教程给的是通用值,但如果你在 F407 上配置 FSMC 时没调时间参数,极个别屏会出现偶发花屏或首行显示异常。常见的做法是读屏厂家的时序手册,算出一组 ACTVAL 值,也可以直接给一组偏保守的值:地址建立时间 10ns,数据建立时间 20ns,总线恢复时间 5ns。算出来这组值以后,初始化序列要等至少 120ms,保证屏内电源稳压已经稳定,否则后面无论怎么调寄存器都会看到随机条纹。
2.3 初始化序列的物理基理:软复位、延时与写命令写数据的底层动作
写初始化序列之前,先搞清楚 FSMC 写命令和写数据在时序层面到底做了什么。选好 RS 所接的地址线后,对某个地址写数据,FSMC 会同时驱动 RS 引脚为高或低。具体说,用 A16 做 RS,地址的第 16 位为 0 时,写操作触发 RS 低电平,控制器识别为命令;第 16 位为 1 时,RS 高电平,识别为数据。这个机制在代码里表现为两个宏或两个函数。
#define ILI9325_CMD_REG *(volatile uint16_t *)0x60020000 #define ILI9325_DATA_REG *(volatile uint16_t *)0x60020000 + 0x100000x60020000 是 FSMC Bank1 的起始地址,加了地址线偏移后对应命令通道,再偏移 0x10000 对应数据通道。这个地址映射就是这块屏能正常吐字的基础,拿错一个偏移量,所有寄存器写入都等于对着空气说话。
软复位动作要在正经寄存器配置前做。常见步骤如下:
void ili9325_reset(void) { LCD_RST_LOW(); delay_ms(10); LCD_RST_HIGH(); delay_ms(120); }先把 RST 拉低 10ms 再拉高,之后要等 120ms,这是屏内部电源建立时间。市面上大部分模块会把 RST 脚跟 MCU 复位脚连在一起,一上电自动复位,但如果你要改寄存器重新初始化,手动软复位是必须的。每个初始化函数开头都要带这一步,别偷懒。
3. 寄存器配置序列:把一份文档中的初始化代码翻译成可控参数
3.1 驱动初始化函数:时钟使能、FSMC 配置和寄存器写入的代码骨架
初始化函数核心是三件事:使能 FSMC 和对应 GPIO 时钟、配置 FSMC 时序、然后按序列写寄存器。时钟使能是 STM32 上最容易漏的一步,缺了任何一个 GPIO 端口的时钟,对应引脚就是死的。
void ili9325_init(void) { // 使能时钟 RCC->AHBENR |= RCC_AHBENR_GPIOD | RCC_AHBENR_GPIOE; RCC->AHBENR |= RCC_AHBENR_FSMC; // 配置引脚为复用推挽输出 GPIO_SetMode(GPIOD, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5, GPIO_MODE_AF_PP, GPIO_SPEED_50MHz); // 数据线等其他引脚 // FSMC 配置 FSMC_BCR1 = FSMC_BCR1_MBKEN | FSMC_BCR1_MWID_0 | FSMC_BCR1_WREN; FSMC_BTR1 = (10 << 8) | (20 << 0); // 数据建立 20ns,地址建立 10ns // 软复位 ili9325_reset(); // 初始化序列 write_reg(0x00E5, 0x78F0); write_reg(0x0001, 0x0100); // ... 后续寄存器配置 }代码里有一组关键参数:FSMC_BCR1 的 MWID 位必须设置成 16 位宽度,FSMC_BTR1 里的 DATAST 和 ADDSET 分别对应数据建立时间和地址建立时间,单位是 HCLK 周期。F103 上如果 HCLK 是 72MHz,一个周期约 13.9ns,所以 20ns 的数据建立时间实际上是 2 个时钟周期。这里面有个容易犯错的地方:直接抄一个基于其他主频的例程,若主频不同,建仓时间会漂移。
3.2 寄存器表拆解:0x00A4、0x00E0/0x00E1、0x0007 这几个核心值的含义
很多初始化例程直接贴一整个数组,但里面哪些寄存器管扫描方向、哪些管偏压、哪些管伽马,大多数人说不出所以然。拆几个核心的:
- 0x00A4 是偏压设定寄存器,值通常设为 0x0001,意思是选择 1/4 偏压。偏压设置错了会导致屏整体特别暗或者颜色不均匀。这个值跟面板厂家的玻璃基板有关,换一批屏可能需要调。
- 0x00E0 和 0x00E1 是伽马校正寄存器组,各 15 位,分别配置正向和负向灰阶电压的斜率。这两个值直接影响灰阶平滑度,设成不合适的值以后会出现明显的色阶断层,尤其是纯色渐变背景下。
- 0x0007 是显示控制寄存器,低 3 位分别控制显示开、睡眠输出和基板电压输出。初始化最后必须把显示开这一位置 1,否则屏是黑屏,但这个位很多人序列里写错位置,导致前面寄存器全配好了就是没画面。
void write_reg(uint16_t reg, uint16_t value) { ILI9325_CMD_REG = reg; ILI9325_DATA_REG = value; }这个函数是把寄存器和值分离写入,实际执行时 FSMC 会产生两次独立的 8080 写周期。中间如果有别的代码插进来,可能把命令和数据割裂,所以要么配合关中断,要么确保这两个操作是原子的。我在调试时遇到过一次诡异现象:屏幕某些行颜色不对,最后定位是 DMA 中断插到了命令和数据之间,写寄存器被传出去了。
3.3 初始化函数的整体序列:给出可直接用的数组,附带每段代码的用途说明
初始化序列按功能分段,屏幕驱动里习惯把寄存器配置放在一个大数组里,但我更推荐按阶段拆成几个函数,方便定位问题。
const uint16_t lcd_regs_init[][2] = { {0x00E5, 0x78F0}, // 内部时钟分频 {0x0001, 0x0100}, // 软复位延迟控制 {0x0002, 0x0700}, // 电源控制 {0x0003, 0x1038}, // 电源控制 2 {0x0004, 0x0000}, // 电源控制 3 {0x0008, 0x0202}, // 显示模式 1 {0x0009, 0x0000}, // 显示模式 2 {0x000A, 0x0000}, // 显示模式 3 {0x000C, 0x0000}, // 显示模式 4 {0x000D, 0x0000}, // 显示模式 5 {0x000F, 0x0000}, // 显示模式 6 {0x0010, 0x0000}, // 显示模式 7 {0x0011, 0x0007}, // 显示模式 8 {0x0012, 0x0000}, // 显示模式 9 {0x0013, 0x0000}, // 显示模式 10 }; void ili9325_init_v2(void) { ili9325_reset(); for (int i = 0; i < sizeof(lcd_regs_init) / sizeof(lcd_regs_init[0]); i++) { write_reg(lcd_regs_init[i][0], lcd_regs_init[i][1]); } }参数说明:0x0011 的 0x0007 是先把显存分成 8 个窗口吗,并不是,这是设置显示扫描方式。后面还要单独写窗口地址来真正激活区域。上面数组里没写全 0x0020 到 0x0052 的地址设定,因为那部分和像素操作函数关联更紧密,我习惯把它放到画点函数里去写,保持初始化序列聚焦在电源和扫描模式。
3.4 扫描方向与显存映射:寄存器 0x0036 对坐标轴的翻转作用与代码示例
扫描方向的坑最经典:画出来的图片左右或上下反了。由寄存器 0x0036 控制,也叫地址控制寄存器,它的第 7 位和第 8 位分别控制列和行的方向,修改这两位可以让显示内容水平翻转或垂直翻转,而不需要改坐标计算公式。
void ili9325_set_scan_dir(uint8_t dir) { uint16_t val = 0x0048; // 默认方向 switch (dir) { case 0: val = 0x0048; break; // 横向 case 1: val = 0x0049; break; // 纵向 case 2: val = 0x0088; break; // 横向反向 case 3: val = 0x0089; break; // 纵向反向 } write_reg(0x0036, val); }这个函数的参数 dir 跟板子安装方向强相关。如果你的屏装在设备上是竖着的,但程序里按横屏写,要么对显存做旋转,要么直接改这个寄存器把扫描起点换到左上角。很多初学者遇到画面整体反向,喜欢去改画点函数的坐标公式,那样改了可能这一处对了下一处又错了,不如认准一个寄存器改全局状态。注意,0x0036 的高两位还影响 GRAM 写入时地址自动增加的方向,画填充区域时靠它决定横着扫还是竖着扫,速度差很大。
4. 像素操作函数与基础绘图:从单点画线到填充性能实测
4.1 画点函数与显存地址跳变:为什么你画一条竖线反而成了斜线
画点是整个图形库的地基,地基歪了,后面画线、画圆、写字全歪。画点函数的本质是:把坐标换算成 GRAM 地址,然后写一次颜色值。
void ili9325_draw_point(uint16_t x, uint16_t y, uint16_t color) { // 设置列地址 write_reg(0x0020, x); // 设置行地址 write_reg(0x0021, y); // 写 GRAM,颜色数据写入后地址自动加 1 ILI9325_CMD_REG = 0x0022; ILI9325_DATA_REG = color; }这里有个关键参数:寄存器 0x0022 是 GRAM 写入口,写完颜色数据以后,控制器内部地址会根据 0x0036 设置的自动递增方向移动,不需要重新设置坐标就能连续写。这一特性对填充矩形特别有价值。
竖线画出来是斜线的现象,真正常见原因是坐标被当成 GRAM 地址直接用了,没有考虑 320×240 这个尺寸的地址边界。如果你设置列地址超出 319 或者行地址超出 239,控制器会按自己的规则滚动到另一行,表现出来就是画竖线时第一段画完跳到别处。解决办法是在画点函数里做边界裁剪,或者保证所有调用都传了合法坐标。
很多人初学时怀疑地址写错了,实际是把 0x0020 和 0x0021 的顺序写反了。ILI9325 要求先设列再设行,反了之后同样画斜线,而且是原因最隐蔽的那种。我之前被这个问题卡了整整一小时,最后是用逻辑分析仪抓 FSMC 总线才看出来顺序错了。
4.2 填充矩形与区域填充速度:使用连续写模式时如何把刷屏速度翻倍
画一个点要写 3 次 8080 周期,但填充一整块区域时,可以利用 GRAM 地址自动增加的特性,连续写颜色而不重复写坐标。
void ili9325_fill_rect(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { uint32_t pixel_count = (x2 - x1 + 1) * (y2 - y1 + 1); // 设置起始列 write_reg(0x0020, x1); // 设置起始行 write_reg(0x0021, y1); // 设置结束列 write_reg(0x0050, x2); // 设置结束行 write_reg(0x0051, y2); // 激活窗口 write_reg(0x0002, 0x0000); ILI9325_CMD_REG = 0x0022; for (uint32_t i = 0; i < pixel_count; i++) { ILI9325_DATA_REG = color; } }这个函数比逐个画点快很多。像素计数用 uint32_t,因为填充全屏要 320×240 即 76800 次写入,uint16_t 会溢出导致死循环或者只刷一半。填充固色用循环写一个固定色值即可,刷屏测试最喜欢的白色 0xFFFF 和黑色 0x0000。
速度瓶颈在于循环里每次都做一次 FSMC 写,所以还可以用 DMA 把颜色值批量搬过去。不过 DMA 写 ILI9325 有个特殊性:目的地地址不变但数据不断变化,适合把寄存器地址写到 DMA 外设地址,内存地址指向一个颜色数组,重复的 0xFFFF 也能搬。真正常用的做法是用超大数组存一帧颜色数据,DMA 一次搬完,但那样占 RAM,F103 上不太现实,所以我一般在填充大色块时用硬件循环,真正刷图片时才会做分块 DMA。
4.3 画线画圆与字符显示:把 Bresenham 算法和英文字模接到驱动上
画线的中点算法在 MCU 上很常见,但接到底层的是通过设置坐标和写 GRAM 两个动作循环。以画线为例:
void ili9325_draw_line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { int16_t dx = abs(x1 - x0); int16_t dy = -abs(y1 - y0); int16_t sx = (x0 < x1) ? 1 : -1; int16_t sy = (y0 < y1) ? 1 : -1; int16_t err = dx + dy; while (1) { ili9325_draw_point(x0, y0, color); if (x0 == x1 && y0 == y1) break; int16_t e2 = 2 * err; if (e2 >= dy) { err += dy; x0 += sx; } if (e2 <= dx) { err += dx; y0 += sy; } } }这里 dy 是负值,误差项 err 初始为 dx+dy。很多人照抄这个算法但不去理解误差更新机制,真遇到线段端点偏移时会不知道怎么调。实际排查思路看 end 点的收敛条件:如果循环一直不退出,说明坐标差是 0 但符号方向算错了,检查 sx/sy 的赋值逻辑。
字符显示就是取模字模数组,按 8×16 或 16×16 的点阵循环画点。取模软件生成的数组通常是一个字节代表一列 8 个点,遍历每个字节再逐位判断写不写点。这里性能瓶颈不是取模函数本身,而是对每个点都调用画点函数,中间还带着寄存器设置。优化空间在于连续写同一行里的多个点,可以先设一次坐标,然后循环写 GRAM。不过这种优化比较折腾,我一般是先用 32×32 的大字模,靠字模数量少来缓解性能问题。
4.4 在 F103 上实测:刷全屏 0xFFFF 时需要 78ms,性能瓶颈在 FSMC 等待
手头这块 F103 运行在 72MHz,刷一整屏白色,用上面的 fill_rect 函数实测下来 80ms 左右。整个刷屏过程就是 76800 次 16 位写,单次写操作约 1µs 上下。FSMC 的写入包括地址建立、数据建立和保持三个时间段,即使全调成最小值,单次写也快不到哪去。
如果觉得这个速度太慢想要提到 20ms 以内,那就得靠 DMA 加双缓冲。F407 支持同时把数据从内存搬到外设,且搬运期间 CPU 可以干别的事。我的经验做法是先把整帧数据拼好放到内存,再启动 DMA 搬运,CPU 等着下一次数据准备。这样刷屏操作本身不阻塞逻辑,显示效果也流畅很多。
F103 上真要靠 DMA 就得用一下 FSMC 的 NOR/SRAM 时序,并且保证 DMA 的突发模式配置正确。如果 DMA 停不下来,多半是外设地址固定但内存地址写错了增量模式。
5. 避坑与常见问题排查:白屏、花屏、颜色错乱的五个现场记录
5.1 白屏问题:初始化序列全部执行了,但屏一片白,无任何显示
白屏这个现象要分清是无背光还是无内容。插上电源背光亮但屏体全白,说明背光正常但显示部分没有驱动起来,大概率是初始化序列里 0x0007 的显示开关位没置 1,或者未正确配置电源电路相关寄存器。按我的排查习惯,第一步先打 0x0007 读回,0x0007 的值如果不是 0x0033 左右,就说明写命令和写数据通道有问题。第二步检查地址映射,确认命令地址和数据地址的区别是否真正对应 RS 引脚的电平翻转。很多时候是在 F407 上换了一个 FSMC BANK,但地址偏移用错了宏定义。
5.2 花屏问题:初始化后能看到色块图案,但画面随机闪烁或出现雪花
花屏分两类:一类是初始化过程中寄存器设置不全,扫描边界乱跳;另一类是时序不匹配,数据在 FSMC 总线上采样出错。第一类常见原因是窗口边界寄存器 0x0050 和 0x0051 没有设置成合法终点,ILI9325 在边界外会自动滚屏,表现出来就是花屏或者画面偏移。建议初始化完成后先写满一个纯色,再画一个横跨全屏的方块,看边界是否吻合。第二类就调 FSMC 时序参数,数据建立时间从 20ns 逐步加大到 50ns,看是否稳定。长期实践下来,偶发花屏优先怀疑保持时间不够,而不是建立时间。
5.3 颜色错乱:黄色显示成了蓝色,或渐变图像出现明显断层
颜色错乱的原因很可能是 0x0036 寄存器里的颜色位序配置不对。ILI9325 有专门的位序控制位,RGB 还是 BGR 由这一位决定。选屏方案时,同一型号玻璃可能在不同的批次里有不同默认位序。解决方式是在初始化序列后面加一个测试函数,打出一排已知颜色条,肉眼比对是否与预期一致。GAMMA 寄存器设置不当也会让颜色看起来偏灰或断层,如果调整颜色位序没有缓解,就推倒重来,把伽马寄存器恢复成厂家推荐值。
5.4 坐标镜像与显示反向:触控坐标对不上、图片左右翻转
图片左右翻转但触控坐标没翻转,这个在带触摸的模组上尤其麻烦。显示部分用 0x0036 翻过来后,触摸控制器是另一套坐标系统,两个会一起反。一般处理思路是让显示和触摸反转保持同向,要么都做硬件翻转,要么都用软件换算。我曾在一个项目里先改了显示寄存器,没同步改触摸采样坐标,结果所有点击位置都偏到屏幕对角去了。最稳妥的做法是在固件里建立全局的屏幕方向枚举,所有涉及坐标的地方都走同一个转换函数。
5.5 DMA 刷屏中断卡死:画面刷了一半就停住,程序跳到 HardFault
DMA 刷屏偶尔会遇到传输到一半卡死,原因是 DMA 搬运的地址是 FSMC 的数据通道地址,但命令通道设置不正确,或者 DMA 配置了突发模式时不支持 AHB 上的某些地址对齐。排障的方法很原始:把 DMA 搬运长度设成 1 个像素,逐步增加长度,看哪一个长度开始出错。实际上最常见的原因是 DMA 的内存地址用了一个局部数组,函数退出后栈被回收,DMA 还在搬,搬出来的全是野数据。
6. 进阶技巧:把刷屏帧率从 12 帧提到 30 帧的实操细节
如果前面的初始化和画点函数都跑通了,接下来能调的只剩刷新速度。一个全屏 76800 像素,理想状态下每个像素 1 个 FSMC 写周期,实际花在坐标设置上的时间往往比写颜色数据还多。
优化思路是尽量使用连续写填充。画点函数对每个点都重新设坐标,而矩形填充只设一次坐标。把部分图形操作改成区域填充后,空闲出来的总线时间可以被 DMA 利用。我试验过一个折中方案:小矩形用循环填充,全屏刷色用 DMA 一次搬运。
还有一个经常被忽视的优化点:关掉 FSMC 中断。FSMC 在每次写完成时不会主动产生中断,但万一你的系统里开了 DMA 传输完成中断,并且中断服务函数执行时间较长,就会卡在刷屏循环中间。刷屏期间临时把中断优先级调低,或者严格保证刷屏函数不被其他任务打断,这能让帧率波动小很多。
另外,我自己还习惯做一个自检函数,开机后刷三条不同颜色的竖带,记录坐标和颜色值,用来确认初始化序列是否稳定适用于当前这块屏。在接触新批次的屏模组时,这个自检函数几乎成了习惯,哪一批显示偏色就直接在初始化时微调伽马值,省去了反复截图的麻烦。
帧率提升到 30 帧后,系统的其他任务就不用再等刷屏了。把那以后我每次接 ILI9325 新板子,都会强制先跑一遍官方推荐初始化序列和自检色带,再动手改寄存器抄优化。这套流程帮我省下过两次几乎要换屏模组的排查时间,希望也能帮到你。
提示:初始化序列里的延时参数和 FSMC 时序值不是随便抄的,拿到新屏模组第一件事是对照示波器实测一次写时序,偏差大了后续所有优化都是空谈。
本文还有配套的精品资源,点击获取