简介:2.8寸ILI9341 SPI串口屏软硬件资料包,面向STM32与C51单片机开发者及嵌入式爱好者,旨在解决小尺寸彩色液晶屏的快速驱动与界面显示需求。内容围绕ILI9341驱动芯片和SPI通信协议展开,包含显示屏规格书、用户手册、硬件设计参考,以及可直接编译运行的DEMO软件源码,源码覆盖初始化配置、屏幕刷新、颜色处理等关键函数,兼顾入门学习与工程复用场景。压缩包共437个文件,体积约11.5MB,文件类型以C语言源码、H头文件、hex固件、uvproj工程和PDF文档为主,另有chm帮助手册、原理图备份及编译中间文件,目录结构清晰,方便按模块查找。目前已有330人学习下载。借助这套资料,开发者可以快速理解ILI9341在SPI模式下的驱动流程,拿到适配STM32与C51两套平台的示例工程,从而在实际项目中直接移植或二次开发,避免从零编写底层驱动,有效缩短产品原型搭建周期。
1. 拆这份2.8寸ILI9341 SPI串口屏资料:到底给了什么、能解决什么问题
这类屏幕资料包,我最关心的是它能不能让一块240×320的ILI9341 SPI串口屏从「通电白屏」变成「刷出界面」。这份资源里给的STM32和C51两个平台DEMO源码、规格书与配套文档,恰好覆盖了两条经典路径:一边是ARM生态的HAL库硬件SPI,一边是8位机的GPIO模拟SPI。对刚把手伸进嵌入式屏幕开发的单片机工程师来说,它能省掉你翻数据手册和对着初始化序列发呆的那几天;对想把串口屏快速接入项目的老手,里面的C51移植参考也能帮你在低算力平台上少走弯路。我会从驱动原理讲到双平台复现,再把读ID、白屏、花屏这些高频坑全部摊开。
2. 为什么是SPI:ILI9341接口选型与驱动原理拆解
2.1 从并口到SPI:串口屏的资源占用差异
先明确一点,ILI9341本身是一颗支持多种接口的显示驱动芯片,8080并口、SPI、RGB接口它都认识。但市面上被打上「串口屏」标签的2.8寸模组,绝大多数只把SPI引脚引出来,原因很简单:MCU的引脚资源太贵了。在STM32F103C8T6这种48脚芯片上,8080并口至少要16根数据线加若干控制线,接完屏以后剩下的引脚连几个按键都紧张。而SPI只需要SCLK、MOSI、CS、DC加上复位,5根线就把读写全包了,代价是刷屏速度比并口慢。
这块屏的逻辑分辨率是240×320,按RGB565格式算,一帧完整数据是240×320×2=153600字节。并口一个字节一次写入,SPI得串行带时钟一个个bit推出去,所以SPI刷满屏天然比并口慢,这是接口物理特性决定的,不是驱动代码写得不好。做菜单、仪表盘这类静态界面完全够用;做视频流或高频动画,就得靠后面提的DMA和局部刷新。
选择SPI的第二个理由是它几乎没有「桥接成本」。STM32自带SPI外设,C51没有硬件SPI也可以用GPIO模拟,模组的厂家DEMO通常两种都给了。所以这套资料的价值不在于代码有多高级,而在于它把两种平台的接线、初始化序列和刷屏函数都串通了。拿到手第一件事,就是先对照原理图确认CS、DC、RST三个引脚的默认配置,再按DEMO的IO定义改到自己的板子上。
2.2 ILI9341的初始化时序:命令、参数与延时的执行顺序
ILI9341的初始化过程本质上是一串命令序列。芯片上电以后先处于睡眠模式,屏幕不亮,必须按顺序退出睡眠(SLPOUT,0x11)、设置像素格式(PIXSET,0x3A)、定义帧率和伽马(0xC0-0xC8)、最后开显示(DISPON,0x29)。注意0x11退出睡眠之后要等120ms以上,数据手册明确写了SLPOUT需要的时间,时序上的等待不能靠串口命令的发送间隔去硬顶。很多DEMO把这个延时就放在0x11后面,为的是让模组内部稳压器和振荡器完成准备。
命令格式分两类。一类是纯命令,比如0x11、0x29,没有参数;另一类是带参命令,比如0x36(MADCTL,存储访问控制)要带一个字节,0x3A带一个字节,0x2A/0x2B设置窗口要带4个字节。发送时先拉低DC引脚表示命令,发完命令字节再拉高DC表示参数。这一点在模拟SPI和硬件SPI下都必须严格保证,否则屏完全不会响应。
void LCD_WriteCommand(uint8_t cmd) { LCD_DC_LOW(); // DC拉低,告诉屏幕接下来是命令 SPI_TransferByte(cmd); // 发送命令字节 LCD_DC_HIGH(); // DC拉高,回到数据态 }这段代码是读写屏里最基础的原语。LCD_DC_LOW和LCD_DC_HIGH在不同平台上实现不同,STM32里就是GPIO_WriteBit,C51里可能直接操作P1口的某一位。SPI_TransferByte在STM32里是HAL_SPI_Transmit,在C51模拟SPI里是8次时钟翻转加移位。所有高层函数,刷屏、填色、画点,最终都落在这两个原语上。
初始化序列里最容易翻车的是0x36这个MADCTL参数。它控制的是扫描方向,也就是RGB顺序和XY递增方向。同一个参数,横屏和竖屏显示的坐标方向完全不同。资料包里DEMO用的参数是0xC0还是0x08,取决于模组走线把扫描方向定死了,换一块屏直接烧录大概率方向是反的。我一般会先用DEMO默认值点亮,再通过0x36的参数逐位调整到需要的方向,不要一上来就改扫描顺序。
初始化序列执行完毕不是马上就能刷屏。正确流程是先发送0x11,延时150ms;再发送0x36、0x3A等参数命令;然后0x29开显示,再延时50ms。这里有个血的教训:如果把0x29放在0x11前面,或者干脆漏掉0x29,屏就是白屏或者背光亮但什么都不显示。检查时先确认背光有没有亮,背光亮而屏幕白,问题基本在初始化序列。
3. 在STM32与C51上复现DEMO:从接线到刷出第一行字
3.1 STM32 HAL库硬件SPI:引脚配置与分频设置
STM32端我一般用CubeMX生成SPI1的初始化,再把屏的驱动文件挂进去。SPI1的默认引脚是PA5(SCK)、PA7(MOSI),MISO在这块屏上只有一个ID读取功能,平时可以不用,但读ID时就得用。CS片选接PA4,DC接PA2。这块屏的引脚分配可以参考如下对应关系,实际以资料包原理图为准:
| 屏幕引脚 | STM32引脚 | C51引脚 | 说明 |
|---|---|---|---|
| SCLK | PA5 | P1.0 | SPI时钟 |
| MOSI | PA7 | P1.1 | 主出从入,写数据 |
| MISO | PA6 | P1.4 | 读ID时启用 |
| CS | PA4 | P1.2 | 片选,低电平有效 |
| DC | PA2 | P1.3 | 命令/数据选择 |
| RST | PA1 | P3.5 | 复位,低电平有效 |
这样接的好处是CubeMX默认配置改得少,对只想验证DEMO的场合最快。C51那边因为全部走GPIO模拟,引脚理论上是任意的,但建议跟DEMO保持一致,省得改代码。
hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1);这是HAL库的SPI1主模式配置块。CLKPolarity和CLKPhase都是LOW和1EDGE,对应SPI Mode 0,ILI9341的串行接口就是按Mode 0和Mode 3都能跑设计的,但DEMO里用的是Mode 0。NSS_SOFT务必保留,因为片选我们手工用GPIO操作,不用硬件NSS,这样CS拉低拉高的时机完全由代码控制,避免硬件自动管理导致时序错位。
分频预分频器这里用了4分频,在72MHz主频下SPI时钟是18MHz。这个频率对ILI9341来说是能跑的正常范围,但不建议一开始就冲到36MHz或72MHz。刷屏花不花和SPI时钟关系极大,尤其在杜邦线连接的开发板上,线一长信号边沿变差,高频跑不上去。先用4分频点亮,稳定了再往上压分频器。
发送数据我习惯用HAL_SPI_Transmit单字节发送,虽然慢但是好调试。初始化阶段无所谓性能,性能瓶颈在刷屏循环里,后面会提到DMA方案。还有一点,读ID时需要用到MISO线,HAL里对应HAL_SPI_Receive,发送读ID命令后直接接收三个字节,而不是去手动制造读时序。
3.2 C51的GPIO模拟SPI:没有硬件外设时的替代方案
C51这边就是另一个世界了。STC89C52这类8位机没有硬件SPI,只能用普通GPIO去模拟。模拟SPI的好处是引脚任意、逻辑透明,坏处是性能全靠延时和IO翻转速度。对240×320的界面类应用来说,刷一屏全屏内容是能接受的速度,但别指望流畅动画。
#define LCD_SCLK P1_0 #define LCD_MOSI P1_1 #define LCD_CS P1_2 #define LCD_DC P1_3 void SPI_WriteByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { LCD_SCLK = 0; if (dat & 0x80) LCD_MOSI = 1; else LCD_MOSI = 0; LCD_SCLK = 1; dat <<= 1; } }这是标准的SPI Mode 0发送字节实现。每个bit先拉低SCLK,把数据放到MOSI上,再拉高SCLK让数据被锁存,最后左移一位处理下一个bit。高位在前,这是ILI9341要求的字节序。dat & 0x80判断最高位,配合<<=1逐位输出。
C51模拟SPI的性能瓶颈在LCD_SCLK = 0/1的IO操作,每个bit要两次IO写,一个字节16次。如果需要提速,可以把延时控制在每个bit加两三个空操作,但不要为了提速把延时去掉,IO翻转本身有建立时间,太快了数据不稳。
注意一点:如果屏幕花得厉害而且MOSI上电平看着是对的,去查初始化时DC引脚的电平状态。C51上电瞬间如果DC被外部器件拉高,第一条命令就会当数据发,屏幕连续收命令直接乱。我一般会在初始化函数开头先把DC拉低、CS拉高,做一个明确的引脚状态复位。
3.3 刷屏主流程:窗口设置、像素格式与RAM写命令
刷屏的核心思路不是逐个像素写,而是先设置一个「窗口」,再往窗口里连续灌颜色数据。ILI9341的窗口由0x2A(列地址)、0x2B(行地址)两个命令划定,最后写0x2C命令进入RAM写模式。窗口越大,填充活动越密集;窗口设为全屏,就是一整屏的填充。
void LCD_Fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint16_t i, total; LCD_WriteCommand(0x2A); // 设置列地址 LCD_WriteData(x0 >> 8); LCD_WriteData(x0 & 0xFF); LCD_WriteData(x1 >> 8); LCD_WriteData(x1 & 0xFF); LCD_WriteCommand(0x2B); // 设置行地址 LCD_WriteData(y0 >> 8); LCD_WriteData(y0 & 0xFF); LCD_WriteData(y1 >> 8); LCD_WriteData(y1 & 0xFF); LCD_WriteCommand(0x2C); // 开始写RAM total = (x1 - x0 + 1) * (y1 - y0 + 1); for (i = 0; i < total; i++) { LCD_WriteData(color >> 8); LCD_WriteData(color & 0xFF); } }窗口命令的参数是两字节的16位坐标,所以每个值都要拆成高8位低8位分两次发。0x2C命令本身不带参数,发完之后连续发数据字节即可,屏幕会自动按窗口范围把数据填进去,填满之后窗口关闭。这里的总点数计算注意左闭右闭,如果用x1 - x0不把加1算进去,最后一行一列永远填不满,屏幕上会出现一条固定颜色的边。
颜色格式是RGB565,也就是16位色。color & 0xFF是低字节、color >> 8是高字节。同理,如果要刷一张图,把图取模成RGB565数组,每次循环发送一个像素的两个字节就行。需要注意的是RGB565的颜色顺序是R高5位、G中6位、B低5位,如果取模数据是按BGR排列的,屏幕上颜色会明显偏色。
C51平台要注意RAM容量。全屏填充一帧是153600字节的数据,即使不缓存直接逐字节发送,C51的for循环也要跑很久。建议先把窗口缩小,比如刷个进度条区域,再逐步扩大到全屏。STM32有DMA可以缓解,但C51只能靠简单的循环加延时,这决定了在这个平台上做全屏动画是不现实的。
4. 避坑与常见问题排查:读ID、白屏、花屏的五个真实案例
4.1 读ID返回0xA1A1:不一定是坏屏,先查这三处
读ID是排查一块陌生ILI9341屏的最快手段。正常流程是让屏幕退出睡眠、延时100ms以上,然后发送0xD3(或0x04)读ID命令,随后通过MISO读回三个字节,其中后两个是0x93、0x41。但很多人读到的却是0xA1A1,于是怀疑屏幕坏了。
现象:MISO读回来的字节是0xA1A1,屏幕本身可能正常显示也可能白屏。原因有三个。一是MISO没有接。SPI读ID依赖MISO,很多DEMO代码默认只用MOSI发数据,压根没有初始化MISO引脚,读回来的全是高电平或低电平。二是屏幕还在睡眠模式就发读命令,返回的自然是空数据。三是复位的时序不对,上电后RST至少要拉低再拉高,中间延时50ms,否则芯片还在内部复位流程里。
解决顺序是这样:先确认MISO引脚配置成GPIO输入或SPI的接收模式,再检查初始化过程是否完成了0x11退出睡眠和对应延时,最后核实RST时序。我一般是把读ID命令放到初始化序列的最后,确保所有初始化命令都发送成功之后再去读。读到的ID如果确实是0x9341,屏硬件基本是好的,问题出在应用侧。
4.2 白屏与花屏:复位时序和初始化顺序的连带影响
白屏是串口屏项目里最常出现的现象,表现形式有两种:背光亮但屏幕什么都没有、背光不亮且屏幕空白。后者先查背光供电和背光控制脚,通常是单独的一根LED或者BL引脚;前者则几乎可以断定是初始化序列没有完整执行。ILI9341上电默认在睡眠模式,睡眠模式下屏幕是不显示的,只有执行完0x11并延时再0x29开显示之后界面才会亮。
花屏比白屏难定位一些。最常见的是初始化序列里漏了一个命令或者命令顺序错误,导致显示控制器内部状态混乱。还有一个反复被踩的坑是0x36(MADCTL)和0x3A(像素格式)都设置完毕后,才想起来去改0x29的位置,结果屏幕开显示后用的是错误的扫描方向和颜色深度。遇到这种情况,不要单独重发某个命令,直接把初始化序列整套重新执行一遍,比在错误状态上打补丁可靠得多。
有的DEMO里初始化序列超长,几十个命令带两行伽马值,看着像早期的寄存器配置流程。这些命令不能随便调整顺序。我习惯把初始化序列按「电源系统→时序→扫描方向→显示开启」四个阶段分段,每一个阶段后面留一个固定延时节点,排查时配合逻辑分析仪看DC和CS的波形,能很快锁定是哪一段没执行完。
4.3 SPI分频玄学:刷屏越慢越稳、越快越花的边界
SPI时钟速度是刷屏质量和速度之间的杠杆。同一块屏,72MHz主频下8分频刷屏稳定,4分频开始出现零星杂色,2分频直接花屏,这就是信号完整性的边界。杜邦线长度超过20cm时,这个边界还会更低,因为SPI的时钟线和数据线在长线上产生反射,接收端采样到的已经不是稳定的电平了。
提示:判断是时序问题还是代码问题时,先用示波器抓一下SCLK和MOSI的波形。花屏只出现在高频、低频完全正常,基本就是信号完整性问题;低频也花,那才是代码或接线问题。
解决高频花屏的办法有四个:降低SPI分频、缩短杜邦线、在SCLK和MOSI之间加一个串联电阻、或者在PCB上让走线等长。其中降低分频最直接,代价是刷屏时间变长。做产品时我一般会把SPI时钟放在12MHz以下,用DMA传输抵消掉速度损失,这样既有稳定度又有吞吐量。
4.4 5V单片机与3.3V屏的电平拉扯
C51单片机大多是5V供电,而2.8寸ILI9341模组的逻辑电平是3.3V。直接用5V的IO去驱动3.3V的SCLK、MOSI、CS、DC,短期看可能能点亮,但长期会损坏模组的输入保护二极管。更隐蔽的问题是,5V高电平经过屏内部的上拉或分压后会反灌到3.3V电源上,造成屏幕工作不稳定、复位失败。
处理方式我推荐两种。一种是给信号线串220Ω电阻,利用分压把5V拉到接近3.3V;另一种是加电平转换芯片,比如TXS0108E或者三极管搭建的简易电平转换,最稳妥。模拟SPI的方式下,转接后的输出波形会有边沿变缓的问题,所以转接后SPI频率要适当下降,比如原来4MHz的只能降到2MHz用。
C51平台上还有一个额外注意点:如果单片机和屏幕共用一个电源,而电源纹波比较大,屏幕复位会失败。给屏幕的供电最好单独从3.3V LDO出来,不要和MCU抢一路电。我见过不少翻车案例,背光电源和逻辑电源混在一起,屏幕一开背光,整板电压被拉低,初始化直接失败。
4.5 Keil5同时装C51和MDK:工程文件被覆盖的翻车现场
同一台电脑上既要编译STM32的ARM工程又要编译C51的8051工程,常规做法是装两套Keil:Keil C51和Keil MDK。但这两个版本的IDE在默认安装路径、工程文件后缀和编译器之间是有冲突风险的,最典型的是安装目录重叠或注册表互相干扰导致打开工程时编译器错乱。
现象:打开STM32工程却报找不到ARM编译器,或者打开C51工程时编译器路径指向了MDK。原因基本就是两个版本装到了同一个默认目录,Keil的TOOLS.INI配置被后者覆盖。解决方式是安装时分开目录,ARM版装在C:\Keil_v5_ARM,C51版装在C:\Keil_v5_C51,不要用默认的同一个C盘路径。编译时会提示选择设备,如果报编译器错误,去Project界面手动切换编译器版本。
我自己还遇到过烧录器冲突:J-Link和ST-Link同时插在电脑上,Keil偶尔会把程序下载到错误的调试器上。量产阶段我把下载和调试分开,下载用ST-Link的SWD模式,调试用J-Link,各自单独开工程窗口,避免同时占用。这个坑不常在文档里出现,但现场翻车的人真不少。
5. 进阶:刷图、DMA刷屏与GBK字库验证,让DEMO变成可用模块
刷图和刷纯色是两回事。全屏一帧153600字节,直接用const uint8_t image[240][320][2]这种静态大数组在嵌入式里不现实,要么用W25Q系列Flash存图,要么拆成BMP取模后分段发送。我习惯的做法是先把图片导出为RGB565的二进制文件,再用取模工具切成长度不超过512字节的块,刷屏时逐块写入窗口,配合DMA传输。
#define IMAGE_BLOCK_SIZE 512 uint8_t image_buffer[IMAGE_BLOCK_SIZE]; // 从Flash或SD卡读取一块,然后通过DMA发送到屏幕 HAL_SPI_Transmit_DMA(&hspi1, image_buffer, IMAGE_BLOCK_SIZE); while (hspi1.State == HAL_SPI_STATE_BUSY);HAL_SPI_Transmit_DMA发送完要检查hspi1.State,避免在下一次DMA传输还没完成时就又写缓冲区。这个状态轮询虽然简单,但在FreeRTOS环境下要小心调度器把当前任务切走,导致DMA缓冲区被其他任务改写,画面出现错位条纹。如果频繁出现这种问题,可以改用信号量或者DMA传输完成中断来同步。
GBK字库是最后一道验证。中文字符串要显示,需要先把字符串从GBK编码转换到UTF-8,再按字库的偏移地址索引到对应的点阵数据。资料包里的C51 DEMO一般自带ASCII字库,中文需要自己取模,取模方向(高位在前还是低位在前)必须和刷图函数匹配,否则每个字的字形会左右翻转。
从那以后我每次拿到一块新的屏幕模组,都强制先跑三个步骤:读ID验证硬件、跑厂家DEMO验证接线、再从DEMO里剥出初始化序列复用。这套流程走完,屏幕质量问题基本能定位到具体环节。希望帮到你。
本文还有配套的精品资源,点击获取