简介:一套基于 STM32F103VE 的 2.8 寸 ILI9341 LCD 显示图片完整工程资源,适合正在学习 STM32 外设驱动、TFT 彩屏应用的开发者参考。资源共 413 个文件,压缩包约 11.94MB,其中包含 84 个 .h 头文件与 80 个 .c 源文件,覆盖 LCD 初始化、SPI 通信、图片取模与显示等核心模块;另有 .uvprojx 等 Keil 工程文件,可打开直接编译,并附 .jpg 图片素材及清理编译产物的批处理脚本。代码中的显示函数已封装好,配合 Image2LCD 工具解释,可快速理解图片数据格式与传输流程,也便于移植到正点原子 2.8 寸屏等硬件平台。已有 1531 人浏览学习,适合嵌入式入门者理解 ILI9341 驱动时序,也可为中级开发者提供完整的工程组织参考,省去从零调试底层驱动与通信协议的时间,快速实现 LCD 显示图片功能。
1. 项目概述与整体方案选型
1.1 核心需求拆解:为什么选择 ILI9341 + STM32F103VE
STM32F103VE接ILI9341这块2.8寸屏,其实是个被很多人写过、但依然值得从头捋一遍的项目。市面上大量2.8寸LCD模块走的都是SPI接口,控制芯片就是ILI9341,分辨率固定为320x240,色彩深度最大支持262K色(RGB666),但在实际驱动中我们一般只用RGB565格式,也就是16位色。这个组合在成本和显示效果之间卡得刚刚好:2.8寸的大小放在便携仪表、桌面小摆件、入门级MP4风格项目上都非常合适,既不会像0.96寸OLED那样只有巴掌大,也不至于像7寸屏那样对内存和布线提出高要求。
STM32F103VE这颗芯片的主频是72MHz,Flash 512KB,SRAM 64KB。注意,64KB的SRAM听起来不小,但ILI9341的显存是320x240x2字节,算下来约153.6KB,这远超片上SRAM容量。所以,“直接开一个全屏帧缓冲再往屏上刷”这条路在F103VE上是走不通的,你需要理解ILI9341本身自带GRAM(显存)这件事,MCU只是通过SPI把像素数据灌进去,屏幕自己会Hold住画面。这个认知非常重要,很多新手一开始把“LCD屏”当“显存设备”而不是“串行外设”,导致后续代码结构完全跑偏。
这个项目适合谁?适合已经点亮过OLED、会用GPIO模拟时序、但第一次接触真彩色大屏的嵌入式学习者。也适合想在STM32上显示完整图片(LOGO、开机画面、照片风格素材)但不知道如何把图片转成数组、又如何高效灌进屏幕的人。
1.2 适配现有场景:FSMC 并口方案 vs SPI 方案
你可能看到其他教程里用FSMC(灵活的静态存储控制器)并口驱动同款屏幕,刷新速度快、CPU占用低,但代价是占用几十个引脚。VS1053、RA8875这些模块常用FSMC,而市面上便宜的2.8寸ILI9341模块大多是SPI接口,甚至只引出了SPI引脚,这时候你没法上FSMC。
SPI方案的优势是引脚少(经典接法只要7根线:VCC、GND、CS、DC、RESET、SDA、SCL),刚好匹配F103VE这种“引脚多但也不想全扔给屏幕”的情况;缺点是刷新速度受限于SPI时钟,理论上ILI9341的SPI最大支持约18MHz,而实际稳定跑10MHz左右已经能比较流畅地刷新静态图片和简单动画。如果你只是显示静止图片、翻页、仪表数值变化,SPI方案完全够用。
这次项目我选SPI硬件外设(不是GPIO模拟),用SPI1,复用推挽输出,速率配置在9MHz~18MHz之间,实测显示图片效果稳定,没有出现花屏和闪烁。FSMC方案更适合需要视频级刷新率的场景,而本项目核心是“显示图片”,SPI是性价比最高的路径。
2. 硬件连接与材料准备
2.1 引脚分配与最小接线方案
我用的2.8寸模块是红板那种,丝印标注了VCC、GND、CS、RESET、DC、SDI(MOSI)、SCK、LED,有些还额外引出了SDO(MISO)和触摸引脚。如果你只显示图片不搞触摸,MISO和触摸引脚悬空即可。
表:SPI直连接线方案
| LCD引脚 | STM32F103VE引脚 | 说明 |
|---|---|---|
| VCC | 3.3V或5V | 模块上有稳压芯片时可选5V,推荐3.3V |
| GND | GND | 共地必须可靠 |
| CS | PB12(软件控制) | 片选,低电平有效 |
| RESET | PB13(软件控制) | 复位,低电平复位 |
| DC | PB14(软件控制) | 数据/命令选择,低电平命令,高电平数据 |
| SDI | PB15(SPI2_MOSI) | 也可以接到SPI1的PA7,看你怎么规划 |
| SCK | PB13(SPI2_SCK) | 与RESET共用PB13会冲突,注意不要复用 |
| LED | 3.3V串100Ω电阻 | 背光控制脚,也可以接PWM调亮度 |
需要特别提醒:PB13和PB14在某些模块例程里是RESET和DC,但SPI2的SCK是PB13,如果RESET占用PB13,两者冲突。我实际用的是SPI1:SCK=PA5、MOSI=PA7、CS=PA4、DC=PA3、RESET=PA2,这样引脚和GPIO模拟的冲突最少,代码也直观。接线时最好画一张自己的引脚映射表贴在调试台旁边,我因为临时换过一次引脚没更新映射表,导致排查了半天显示异常,后来发现是CS接到了复用功能脚上。
2.2 电源、背光与电平匹配的几个关键注意点
2.8寸LCD模块的背光电流不小,实测全亮时约20~40mA,加上模块逻辑功耗,从STM32的3.3V LDO直接取电有一定风险。如果开发板USB口供电能力不足,背光一亮MCU就重启,这种问题非常典型。建议从USB 5V取电经过模块上的稳压芯片(如果有)给VCC,或者单独用一节锂电池经稳压模块供电,确保纹波小于100mV。电源供电不稳会导致屏幕出现水波纹、随机花屏和初始化偶发失败,这不是代码能完全扛住的。
背光引脚LED不能直接悬空,否则有屏闪或亮度不稳定的问题。想用PWM调亮度的话,接一个NPN三极管或MOS管做开关,MCU的PWM输出控制基极,而不是直接让PA口驱动LED。直接灌电流会拉低MCU引脚电平,严重时还会反串干扰SPI时钟。如果只是固定亮度,串一个100~220Ω电阻到3.3V即可,亮度太高反而费电且看久了眼睛累。
电平匹配上,STM32F103VE是3.3V逻辑,LCD模块大多也是3.3V逻辑,但如果模块上有AMS1117-3.3稳压,可以直接接5V供电,此时SPI引脚电平还是3.3V,不需要额外转电平。千万别把5V直接接到模块的逻辑电源引脚,ILI9341控制器超过4.2V就存在烧毁风险。
3. ILI9341 驱动关键点拆解
3.1 SPI 时序与初始化序列:控制命令背后的意义
ILI9341的初始化序列网上有一堆例程,直接抄其实也能跑,但如果不知道每条命令在做什么,后面遇到问题会很痛苦。初始化本质上是让控制器进入你期望的颜色格式、扫描方向、Gamma曲线和显示窗口状态。
核心命令就几条:
- 0x01 Software Reset:软复位,之后要延时至少120ms,给内部模拟电路稳定时间。
- 0x11 Sleep Out:退出睡眠模式,同样需要延时,常见是150ms左右,别一复位完就立刻刷数据,会偶发白屏。
- 0x3A Pixel Format Set:设置像素格式,RGB565对应0x55,RGB666是0x66。我们在STM32上通常只用0x55,因为16位色刚好一个像素两个字节,SPI传输效率最高。
- 0x36 MADCTL:控制扫描方向和RGB顺序。这个寄存器是图片显示方向的命门。默认值0x00是从左上角横着往右扫,但你的屏模块可能做了物理翻转,需要根据实际丝印和观察调整。常见值是0x08(BGR顺序)和0xC0/0xA0等组合,建议写一个小函数在初始化时切换几个值刷纯色块来判断横竖方向。
- 0x2A Column Address Set 和 0x2B Page Address Set:设置写入窗口的列坐标和行坐标范围。
- 0x2C Memory Write:开始写入像素数据,之后SPI发送的所有数据都会被当作像素颜色填进刚才设定的窗口,写完自动换行。
传输格式上,ILI9341支持4线SPI。在发送指令时,DC拉低;发送数据时,DC拉高。CS在整个传输期间拉低。很多SPI外设配置为8位数据宽度,但像素是16位,所以写入像素时要注意字节顺序。ILI9341默认是大端先传高位字节,比如RGB565颜色0xF800(红色),先发0xF8再发0x00。部分模块配置了BGR标志位,会导致红蓝互换,后面单独说。
初始化完成后,先刷一个全屏纯色(比如红色0xF800)验证基本通路。写一个填充函数,循环320x240次写入相同颜色即可,这个过程用SPI直接灌,3秒以内可以刷完一屏。
3.2 RAM 写窗口:显示图片的核心机制
理解写窗口是显示图片的关键。ILI9341的GRAM地址范围是列0~239(x方向)、行0~319(y方向),和屏幕物理方向相关,但重要的是,你每次写像素前都可以通过0x2A和0x2B命令重新划定一个矩形窗口,随后的0x2C数据只填充这个窗口内的像素,填满后地址指针自动回到窗口左上角,而不是越过边界写坏别的区域。
举个例子:你想在屏幕左上角显示一张100x100的小图标,就应该设置列地址为0~99,行地址为0~99。然后把图标的像素数据按“从左到右、从上到下”的顺序连续发送100x100x2字节,屏幕会自己按窗口回绕,不需要你逐像素计算GRAM地址。这个机制比老式LCD的“直接设地址再写数据、写完还要地址+1”的寄存器操作快捷太多了。
实现上,设置窗口的标准代码是发送0x2A,再发送4个字节参数(起始列高8位、起始列低8位、结束列高8位、结束列低8位),0x2B同理设置行范围。注意别把列和行的坐标系搞反,320x240屏幕中,列地址范围是0~239还是0~319取决于MADCTL的MX/MY位。我在调试图片整体转了90度时,第一反应是改MADCTL,而不是改窗口设置,最后发现MADCTL一改,坐标轴跟着换,改完要重新理清坐标系。
3.3 读 ID 校准:确认命令集版本
ILI9341有一个读取ID的指令0x04(Read ID),可以通过MISO引脚读回三个字节的控制器ID。很多模块虽然标着ILI9341,实际可能用ST7789V、ILI9342或其他兼容控制器,初始化序列不完全一样,如果发完初始化序列后屏幕依然白屏或颜色怪异,先读一下ID确认是不是真的ILI9341。
读取指令在SPI半双工模式下比较麻烦,因为需要MCU发送命令字节后立刻切到接收模式,再读3个字节。STM32的SPI外设支持BSY标志和RXNE中断,配置为全双工模式时,发送0x04后再读取3个字节即可,前提是模块引出了MISO引脚。如果没有MISO,就用“纯色验证法”:初始化后刷红色,如果实际显示蓝色,说明BGR顺序反了;如果横向红绿蓝条纹错位,说明MADCTL设置不对;如果整个画面有条纹噪点,大概率是RGB565字节序反了。我实测下来,用读ID方法确认控制器型号后再针对性修改初始化序列,成功率远高于盲目套用网上代码。
4. 图片数据生成与显示流程
4.1 Image2LCD 工具配置:从图片到 C 数组
要让ILI9341显示图片,前提是把图片变成MCU能认识的像素数组。常见做法是用Image2LCD这个Windows小工具,把BMP或PNG转换成C语言数组,输出一个.h文件。配置参数不能乱选,否则生成的数组很可能没法直接用。
表:Image2LCD关键配置项与推荐值
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 输出格式 | C数组 | 直接生成const unsigned char数组 |
| 最大宽度/高度 | 240x320或320x240 | 必须和要显示的窗口匹配,否则图片会错位 |
| 颜色格式 | RGB565 | 16位真彩色,和ILI9341参数0x55对应 |
| 数据排列 | 水平扫描 | 从左到右、从上到下,和写窗口顺序一致 |
| 字节序 | 高位在前(MSB First) | 对应SPI发送时先发高字节 |
| 压缩方式 | 无压缩或RLE | 简单图片可以不压缩,大幅图片用RLE可省Flash但解码代码要自己写 |
工具生成后,打开.h文件检查前几行数据。一张全屏320x240的RGB565图片,数组长度应为153600字节(320x240x2)。如果你看到数组长度只有76800,那说明你选了RGB233格式或输出成了索引色,这时屏幕会显示一片混乱的颜色。
生成的数组直接放到工程里编译即可,但要注意F103VE的Flash是512KB,一张全屏图片占153.6KB,大约接近芯片Flash的三分之一。如果你要放多张图片,Flash会迅速吃紧。所以除非是做开机画面这种静态需求,否则尽量缩小图片尺寸,或者把图片数据存到外部SPI Flash/SD卡里,运行时按需读取再刷屏。
4.2 图片数据的烧写与加载方案
图片数组编译进固件最简单,缺点是编译速度慢且占用Flash,但好在不需要额外硬件。我的实际做法是:先用Image2LCD生成一张240x320的LOGO图片数组,编译烧录,确认显示无误后再考虑多张图片方案。用外部SPI Flash(W25Q64)存图片是更通用的方案,可以先把图片数据做成.bin文件烧进Flash,然后MCU通过SPI读取再转发给LCD。这里有个关键点:Flash的SPI和LCD的SPI可以共用同一个SPI外设,只要CS片选分开控制就行,但要注意驱动时序中CS的切换间隙。实测中,Flash读一个扇区再转发给LCD,整个流程占用CPU较多,但静止图片完全没问题。
如果图片尺寸是240x320,直接用Image2LCD生成,然后通过串口或ST-LINK烧写进外部Flash;如果是小图,比如128x128的头像,则可以直接生成C数组,显示时用窗口定位到屏幕中间,计算好偏移位置填充。这里就需要用到前面说的窗口坐标换算能力:数组的数据排列是从左到右、从上到下连续的,只要确定左上角坐标和宽高,设置好窗口后逐字节发送即可。
4.3 坐标换算与缩放:解决图片显示位置错乱
显示局部图片时,最容易犯的错是窗口坐标没有按比例缩放。假设你想在320x240的屏幕上居中显示一张128x128图片,左上角坐标应该是( (320-128)/2, (240-128)/2 ) = (96, 56)。设置列地址范围就是96~223,行地址范围是56~183。如果直接设置0~127、0~127,图片会被压缩到左上角一小块区域,甚至出现残缺和错位。
放大图片则更复杂,例如要把128x128的图放大到240x240显示,最简单的方式是“最近邻插值”:计算目标像素对应原图的坐标,取整后填入。这对于显示LOGO或者像素风格图片完全够用,不会产生太多锯齿,但处理一张240x240图需要循环7万个像素,在72MHz主频下耗时几十毫秒,可以接受。真正要避免的是在按键响应里直接做这种计算,否则会有明显卡顿,建议刷新前预先算好结果,或者直接用Image2LCD输出240x240版本。
缩放和旋转的问题,归根结底是要坚持一个原则:图片数据的生成工具参数、显示窗口、MADCTL方向三者必须完全一致。我调试时遇到过图片是倒的,就是因为Image2LCD生成时默认从上到下扫描,但我初始化时MADCTL设置了从下到上的扫描方向,两个方向一结合图片就置底了。
5. 常见问题与排错实录
5.1 白屏:先查硬件还是先查初始化
白屏是ILI9341项目里最普遍的问题,没有之一。按照我的排错顺序:先量电压,确认VCC和LED引脚分别有3.3V和2.8V以上电压;再用示波器或逻辑分析仪看RESET引脚是否有一串复位低脉冲,没有复位信号则初始化代码根本没执行到。硬件没问题后再查软件:是否发送了0x11并等待了足够延时;MADCTL是否设置成了会把内容刷到屏幕可视范围之外的方向;0x2C之后有没有发送足够像素数据。很多时候白屏不是初始化不对,而是SPI的MOSI接线接错到了MISO,导致所有命令和数据的传输实际都在“读”方向,屏幕收不到任何信息。
另外有个经验:把SPI时钟降到1MHz后再试白屏问题。如果降速后正常,说明速率太快导致时序建立时间不足。ILI9341的SPI虽然标称支持18MHz,但那是在布局合理、线短、无干扰的前提下。杜邦线连接时建议跑4~9MHz,我用10MHz以上时偶发整屏偏移,降到4MHz后再也没出现。
5.2 颜色失真与反色:RGB 顺序与颜色格式不匹配
显示图片颜色不对,常见是两种现象:一种是红色和蓝色互换,这基本就是BGR顺序问题。MADCTL寄存器的第3位(BGR位)用来切换RGB和BGR顺序,ILI9341默认是RGB还是BGR看模块设计,所以你要试:0x08还是0x00,哪个让纯红色显示成红色就选哪个。另一种是整体颜色发绿发紫、噪点很多,这通常是颜色格式不匹配,比如MCU发送RGB565数据,但LCD被初始化成了RGB666格式,低字节丢失或错位,颜色自然全乱。确保0x3A命令写入0x55,同时Image2LCD也输出RGB565,两边格式必须一致。
还要注意纯色验证的顺序:先刷红色0xF800,绿色0x07E0,蓝色0x001F。如果红蓝互换,改MADCTL的BGR位;如果红绿互换,说明字节序反了,要把SPI发送函数改成先发低字节后发高字节,或者用SPI的16位数据帧直接传输。F103VE的SPI支持16位帧格式,能减少一点CPU开销。
5.3 花屏、残影与闪烁:时序、刷新策略与电源稳定性
花屏分两种:静态图片显示后局部花,多半是图片数组数据有误,或者窗口设置和图片尺寸不符;动态刷新时花屏,多是SPI传输被中断打断、写窗口过程中CS异常拉高、或电源纹波过大导致控制器内部状态错乱。我遇到过SPI中断和定时器中断优先级配置不当,刷屏过程中被高优先级中断频繁打断,导致CS时序被拉长,屏幕出现随机干扰线。解决方法是把刷屏的SPI发送放在一个临界区里,关闭其他中断,或者把刷屏函数放进DMA传输并设置高优先级。
残影和闪烁通常是刷新策略问题。ILI9341刷新整屏时,如果你先清屏再画图,就会出现一瞬间的空白或残影。图片切换时优先使用“直接写目标图片数据”的方式,不要在MCU端做二次缓冲,更不要清屏后再刷。背光的PWM频率如果太低(低于1kHz)也会看到闪烁,人眼对200Hz~1kHz的PWM调光尤其敏感,最好用20kHz以上的PWM频率,或者干脆直流点亮。
5.4 下载失败:error: device not found 排查
有个热词是“error: device not found - device:vendor: 'stm32f103ve''stmicroelectronics'”,这其实是程序下载阶段的问题。接上LCD后影响下载,常见原因有三:一是LCD模块的VCC和GND接线在面包板上造成短路或压降过大,导致STM32核心电压不稳,ST-LINK无法建立连接;二是LCD的SPI引脚占用了SWD调试引脚(PA13/PA14),但大部分LCD并没有用这两个引脚,更多是LCD电源线干扰;三是固件里初始化了引脚复用,把SWD引脚配置成了普通GPIO,下载器当然找不到设备。
解决办法:下载前拔掉LCD的VCC和GND,只保留调试器连接;如果仍找不到设备,按住开发板复位键的同时点击下载,等下载器连接上后再松开复位,这是我试过无数次最稳的办法。另外检查ST-LINK的接线是否过长,杜邦线超过20cm时SWD信号会有明显衰减,会导致连接不稳定。这个问题和LCD本身关系不大,但在调试过程中遇到的频率极高,先记下来省得慌了手脚。
刷图片的流程里还有一个隐藏坑:用Keil下载时如果Flash容量设置成默认的64KB,编译超过64KB的图片数组会直接报错,提示Flash溢出或下载时写不进去。要在Target选项卡里把Flash大小改成512KB,对应F103VE。按下F8之前最好先编译一次确认Program Size,如果数据量在200KB以上,下载时间会明显变长,这正常。
表:常见问题速查
| 现象 | 优先检查项 | 常用解决办法 |
|---|---|---|
| 白屏 | 电源、复位、SPI接线 | 降SPI时钟到1MHz,确认RESET引脚波形 |
| 红蓝互换 | MADCTL的BGR位 | 0x36改为0x08或0x00测试 |
| 颜色发绿花屏 | 颜色格式 | 确认0x3A=0x55,Image2LCD输出RGB565 |
| 图片倒立/翻转 | MADCTL扫描方向 | 调整MX/MY位后重新生成图片数组 |
| 图片局部花 | 窗口坐标 | 检查0x2A/0x2B设置是否和图片尺寸匹配 |
| 闪烁 | 背光PWM、电源纹波 | 背光直流点亮,电源加100uF电容 |
| 一刷图就死机 | SRAM不足 | 不要申请全屏buffer,直接流式发送像素 |
| 下载失败 | SWD冲突/电压不稳 | 断开LCD供电再下载,按住复位键连接 |
(这里可以自然收尾,比如聊一下实际做完这个项目的体会和可以继续扩展的方向。)这个项目做完之后我自己的一点体会是:ILI9341这种带GRAM的屏幕,驱动难点不在于“把命令抄对”,而在于理解“写窗口 + 连续数据流”这一套模型。你一旦理解了窗口机制,后续做GUI菜单、做动画、做绘图函数都会顺畅很多。扩展方向上,你可以给这个屏加上触摸(XPT2046),做一个完整的图片浏览器;也可以把图片数据放到SD卡里,F103VE配FatFS文件系统,直接读取BMP显示,自由度会大很多。如果你正在做类似的显示项目,建议先把一张纯色图、一张全屏照片、一张小尺寸LOGO三步走通,后面再折腾局部刷新,整个流程会踏实很多。
本文还有配套的精品资源,点击获取