ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F103C8T6驱动ILI9341 TFT LCD完整教程:从SPI配置到白屏排错

STM32F103C8T6驱动ILI9341 TFT LCD完整教程:从SPI配置到白屏排错 简介这是基于STM32F103C8T6最小系统板驱动ILI9341 2.8寸TFT液晶屏的完整Keil工程面向入门STM32、需要快速完成屏幕显示的嵌入式开发者和学生。由于C8T6是48脚封装、没有FSMC控制器工程没有使用串行屏模块而是用GPIO模拟16位并口时序把A端口0~15作为数据线再配合读写控制脚完成LCD初始化、画点、画矩形、填充和颜色显示等基础函数触摸输入暂未实现但利于集中学习液晶屏驱动原理和GPIO时序。压缩包共182个文件约3.16MB包含C源码、H头文件、启动汇编s以及uvproj/uvopt工程文件、编译链接生成的o、lst、hex、map等.s为启动文件c/h为驱动与应用代码hex可直接烧录目录结构清晰适合直接打开编译和烧录到开发板验证。已有5078人学习下载。对想要理解STM32模拟并口驱动LCD、移植ILI9341底层初始化代码的读者这套资源提供了完整可跑的工程参考也可据此排查并口接线和时序问题。 不知道你现在手里是不是也有一块吃灰的2.8寸TFT LCD液晶屏。当年我第一块ILI9341屏幕就是这么来的——买了STM32F103C8T6最小系统板顺手加了一块2.8寸TFT LCD觉得不过就是个显示屏嘛点亮还不是分分钟的事。结果连续折腾了三个晚上白屏、花屏、斜条纹全都遇了一遍最后才搞明白问题出在哪。做STM32F103C8T6驱动ILI9341这件事本身并不复杂难的是网上教程默认你“该懂但其实不懂”的细节太多。本文把我从SPI引脚映射、ILI9341初始化序列、窗口设置到刷屏性能优化和白屏排错的实际经验完整写出来适合刚用C8T6点亮液晶屏、对寄存器一头雾水的朋友。不用再翻几百页数据手册也能跑通但你会在读完之后知道每一条命令到底在干什么。1. 这套经典组合的选型逻辑C8T6为什么要配ILI93411.1 一个48脚小板和一块屏的典型搭档STM32F103C8T6属于F1系列里的“最小系统板之王”Cortex-M3内核72MHz主频64KB Flash20KB SRAMLQFP48封装。虽然引出的GPIO不算多但SPI、USART、I2C这些常用外设一个不缺。ILI9341则是一颗极其常见的TFT LCD驱动芯片最大支持240x320分辨率16位色RGB565下色彩表现足够细腻喜欢折腾的还可以支持到18位色。这两者组合起来刚好是一套“资源不多但正好够用”的经典搭配——C8T6的SPI1跑在18~36MHzILI9341的SPI接口完全吃得下引脚占用少驱动代码量也小特别适合作为入门的第一块屏幕。我见过不少朋友一上来就纠结“要不要换个大屏、要不要上F407”。如果只是学显示逻辑、做仪表盘或者信息展示C8T6加ILI9341的绰绰有余。等到之后需要复杂图形渲染或视频播放再考虑换平台也不迟。1.2 硬件清单与模块选型的坑我这边准备的东西非常常规STM32F103C8T6最小系统板一块蓝色pill板最常见2.8寸ILI9341 TFT LCD模块一个ST-Link V2下载器一个杜邦线若干3.3V和5V供电都要备好有的屏模块背光部分需要5V这里有一个非常容易踩的坑同样是2.8寸屏接口可能完全不同。市面上常见两种一种是16位8080并口排针一大排需要十几个GPIO才能驱动另一种是SPI接口几根线就能控制。C8T6引脚资源本身不富裕如果你拿到的是8080并口版本同时还要外接其他传感器引脚会非常紧张。所以买之前一定要问清楚或者看原理图确认是SPI接口。另外部分模块会带触摸功能XPT2046芯片触摸部分又额外占两个引脚。如果只是做显示可以忽略触摸相关引脚但要注意有些模块的触摸IC和屏幕共用供电与复位线上电后不初始化触摸IC不影响屏幕显示这部分可以不接。2. 先把ILI9341当一块可寻址的画板理解命令与GRAM机制2.1 驱动芯片就是屏幕自带的大脑很多教程上来就扔给你一大段初始化数组然后说“照着写就行”。这确实能跑通但一旦遇到白屏、花屏、颜色不对你完全不知道从哪排查。所以我建议先从原理层搞清楚ILI9341到底是个什么东西。ILI9341内部有一块240x320的GRAM每一个地址对应屏幕上的一个物理像素点。它就像一个自带画板的画师MCU不需要逐一点亮每个像素只需要通过SPI告诉他三件事第一从哪个位置开始画第二画多大一块区域第三往这块区域里填充什么颜色数据。剩下的像素排列工作全部由ILI9341自己完成。所以驱动ILI9341的核心其实是和它打交道发送一串配置命令让它进入你想要的显示模式然后不断往GRAM里填数据。2.2 DC引脚决定了SPI线上走的是命令还是数据SPI是一种四线通信协议但对ILI9341来说还需要额外一个DC引脚也叫RS、D/C用来区分当前字节是“命令”还是“数据”。这是整个驱动中最容易忽略却最关键的信号。DC拉低SPI上的字节被当作命令比如0x2A代表“设置列地址”。DC拉高SPI上的字节被当作数据比如列地址的具体数值。如果没有这个DC引脚屏幕就无法区分进来的是指令还是像素数据。实际代码里写命令和写数据就是两个长得几乎一样的函数唯一的区别就是DC引脚的高低电平时序。2.3 初始化序列几条关键命令的含义初始化序列是很多新手“劝退”的地方一长串寄存器配置看着就像天书。其实大部分命令用不到能影响显示效果的核心命令就那么几个命令名称关键参数说明0x01Software Reset软件复位必须延时等待0x11Sleep Out退出睡眠模式必须延时等待0x36Memory Access Control控制扫描方向、RGB顺序0x3APixel Format Set像素格式0x55表示RGB565 16bit0x29Display ON打开显示其中0x36最常被单独拎出来讲。它的参数决定屏幕的横竖方向和颜色顺序比如0x00通常是竖屏、0x60或0xA0是横屏bit3置1表示BGR颜色顺序。不同模块的默认扫描方向不一样这也是同一个初始化代码在不同模块上出现“上下颠倒、左右镜像”的原因。2.4 写一个像素的最小流程设置窗口加填色任何显示操作到最后都会归结为一个动作往GRAM写颜色数据。而写数据前必须先用三件事框定范围——列地址、页地址、写内存命令。列地址范围0x2A命令 4字节数据起始列高8位、低8位结束列高8位、低8位 页地址范围0x2B命令 4字节数据起始行高8位、低8位结束行高8位、低8位 写GRAM0x2C命令之后连续送入像素数据只要理解了这套流程你就能自己扩展出画点、画线、填充矩形、显示图片这些功能。所谓“画点”不过是连续执行两次设置窗口加上一次写像素而“全屏填充”只是把窗口设为整个屏幕然后连续发送十几万字节的颜色数据。3. 引脚连接与上电前检查照着接就能点亮3.1 我用的这套引脚分配C8T6上自带SPI1外设这是最方便的选择因为SPI1的引脚可以映射到标准位置PA5是SCKPA6是MISOPA7是MOSI。我实际使用的引脚映射如下功能MCU引脚备注屏幕SCKPA5SPI1_SCK屏幕MOSIPA7SPI1_MOSI屏幕MISOPA6SPI1_MISO只读寄存器时用可不接屏幕CSPA4软件控制片选屏幕DCPA3命令/数据选择屏幕RSTPA2屏幕硬件复位背光BL3.3V或PWM引脚直接接高电平点亮VCC3.3V有的模块内部有LDO可接5V看清标注CS、DC、RST这三个引脚只要不和外设冲突可以随便换。SCK/MOSI则要用SPI1对应的引脚如果你想用SPI2那就要换到PB13和PB15了。3.2 上电前必须确认的三件事第一件事电平匹配。ILI9341的逻辑电压一般是2.8V到3.3VC8T6的IO输出也是3.3V理论上直连没问题。但有些模块为了兼容Arduino板上带了电平转换或者LDO这时候供电电压反而要看模块标注——有的VCC必须接5V才能让内部稳压器工作接3.3V会出现“背光亮但屏幕无内容”的假死状态。第二件事共地。MCU和屏幕模块的GND必须连在一起这是最基础但很容易漏掉的一步。我曾经因为接了一根不太牢靠的杜邦线导致GND接触不良屏幕时好时坏排查了半天最后发现只是地线松了。第三件事背光默认不一定亮。有的模块背光引脚上默认有上拉电阻上电就亮有的必须你手动拉高。调驱动之前先把BL引脚接3.3V确保背光能亮否则你会误以为是屏幕坏了实际上只是没开背光。3.3 为什么我坚持让你先飞线而不是直接画PCB如果你只是想把屏幕点亮跑通代码我强烈建议先用杜邦线连接而不是一上来就打板。C8T6驱动ILI9341的SPI时钟一般在18MHz左右这个频率下杜邦线勉强够用但线长超过20厘米就容易出现波形畸变导致花屏。飞线的好处是你随时可以缩短线长、重新分配引脚、用示波器夹在线上量波形。等程序完全跑通、确认引脚分配合理之后再画PCB才是稳妥的流程。4. 代码落地从SPI配置到第一个像素的完整链路4.1 SPI外设初始化我用的是STM32标准外设库HAL库的配置思路完全一样只是API名称不同。核心是把SPI1配置为主模式、8位数据、MSB先行、SPI Mode 0CPOL0CPHA0时钟分频选择4分频也就是18MHz。SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE);关于分频系数这里多说一句SPI1挂在APB2总线上时钟72MHz2分频就是36MHz这在理论上已经达到ILI9341 SPI接口的上限附近。但实际用杜邦线连接时36MHz很容易出现信号反射和串扰导致花屏。我调试时先降到18MHz4分频把功能跑通确定没问题后再慢慢往上升这也是一个稳妥的调试思路。4.2 写命令与写数据函数的时序拆分底层最重要的两个函数就是写命令和写数据。实现上就是在发送字节之前控制DC引脚的电平。void ILI9341_WriteCmd(uint8_t cmd) { LCD_DC_LOW(); // 命令模式 LCD_CS_LOW(); // 片选拉低选中屏幕 SPI1_SendByte(cmd); LCD_CS_HIGH(); } void ILI9341_WriteData(uint8_t dat) { LCD_DC_HIGH(); // 数据模式 LCD_CS_LOW(); SPI1_SendByte(dat); LCD_CS_HIGH(); }注意DC信号必须要在片选拉低之前稳定下来否则屏幕可能把第一个字节误判成错误类型。这个时序问题比较隐蔽——初始化序列对时序不敏感时偶尔能跑通但显示大量像素数据时就会偶发花屏。4.3 设置窗口与填充矩形理解了2.4节的流程后代码就非常直接了。设置窗口的函数负责把列地址和行地址都告诉屏幕void ILI9341_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { ILI9341_WriteCmd(0x2A); ILI9341_WriteData(x0 8); ILI9341_WriteData(x0 0xFF); ILI9341_WriteData(x1 8); ILI9341_WriteData(x1 0xFF); ILI9341_WriteCmd(0x2B); ILI9341_WriteData(y0 8); ILI9341_WriteData(y0 0xFF); ILI9341_WriteData(y1 8); ILI9341_WriteData(y1 0xFF); ILI9341_WriteCmd(0x2C); }填充矩形时先把窗口设好然后连续发送颜色数据。因为颜色是16位的RGB565所以每个像素需要发送两个字节。void ILI9341_FillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t color) { uint32_t i; uint32_t total (uint32_t)w * h; ILI9341_SetWindow(x0, y0, x0 w - 1, y0 h - 1); LCD_DC_HIGH(); LCD_CS_LOW(); for (i 0; i total; i) { SPI1_SendByte(color 8); SPI1_SendByte(color 0xFF); } LCD_CS_HIGH(); }为什么窗口设置这么重要因为ILI9341的GRAM写入指针是连续递增的。如果屏幕当前扫描方向是“从左到右、从上到下”那么写完一行后指针自动跳到下一行开头。你不设置窗口它就会按默认顺序一直写当你只想在屏幕中间画一个框时数据就会错位显示出来的就是一整屏乱码或斜条纹。花屏问题里有相当比例都出在窗口没设对上。4.4 显示字符字库本质上是像素点阵数据显示字符和ILI9341本身没有关系它只是把字符当成一张小图来填充。ASCII字库本质上就是一个二维数组数组里每个元素是0或11代表该像素点亮0代表熄灭。以16x24的ASCII字库为例每个字符占用24个字节每行2字节共24行。显示字符时遍历这个二维数组遇到1就填前景色遇到0就跳过去或者填背景色。字库来源可以是取模软件生成也可以直接从网上找现成的ASC16等文件。有一个很关键的认知字库并不是ILI9341的一部分它只是存放在MCU Flash里的一段数据你完全可以自己用取模软件生成任意汉字、符号甚至Logo点阵。理解到这一层再看各种GUI库的底层源码就会觉得非常清晰——无非都是“把一块内存里的位图数据搬运到GRAM”而已。5. 刷屏性能实测与优化DMA、窗口缓冲和SPI时钟的取舍5.1 三种刷屏方式的实测差距很多人点亮屏幕后做的第一件事就是跑一个“屏幕颜色不断切换”的循环然后立刻发现这个刷新速度怎么这么感人这不是你代码写得有问题而是刷屏本身是一个大数据量传输任务。我们算一下240x320的屏幕RGB565格式每个像素2字节一帧数据就是240乘以320乘以2约150KB。如果SPI时钟18MHz理论传输时间大约是68ms也就是每秒最多14帧左右。听起来还行但如果你用最朴素的GPIO模拟SPI时钟可能只有几百KHz一帧就要好几秒。我实测下来的数据如下传输方式全屏填充一帧耗时说明GPIO模拟SPI约2~3秒每个bit都要翻转GPIO极慢硬件SPI 18MHz 循环发送约300~400ms每次发送都有函数调用开销硬件SPI 18MHz DMA发送约100~150ms接近理论值CPU几乎不参与如果你只是显示固定内容300ms刷一帧其实够用。但如果要做动画或者波形刷新建议直接用DMA。5.2 为什么C8T6不能像F407那样开全屏缓冲你可能会想一次性把150KB的RGB565像素数据准备好然后DMA一股脑发过去这样不最快吗问题是C8T6的SRAM只有20KB根本放不下一整帧数据。所以必须分块处理。我的做法是开一块行缓冲大小是320乘以2等于640字节每次填充一行或者几行然后通过DMA发送发完再填下一块。这样不仅内存占用小DMA传输也能持续保持高效率不会因为数据断流而频繁拉低CS重新发起传输。5.3 为什么选RGB565而不是RGB888ILI9341确实支持18位色RGB888格式每像素3字节但那个模式对C8T6来说性价比不高。RGB565每个像素只需要2字节数据量减少三分之一色彩表现上人眼很难看出区别而且很多图标、字库取模工具默认就是565格式。我平时所有代码都统一用RGB565这样在画图和显示图片时不需要做额外的格式转换速度和内存都更友好。6. 白屏花屏排错笔记我在实际调试中踩过的坑6.1 白屏排查链路按这个顺序查白屏是ILI9341调试中最常见的问题。我自己的排查顺序是固定的从电源到信号到配置一层层缩小范围。背光亮不亮不亮就是供电或背光控制问题先查BL引脚的电压。复位引脚时序检查RST引脚是否先拉低至少10ms再拉高如果复位时间不够屏幕状态机可能卡在未知状态。SPI信号是否真的送到用示波器或逻辑分析仪量SCK和MOSI脚看初始化序列到底有没有发出去。没有示波器的话可以用GPIO翻转一个LED来标记代码执行位置确认程序跑到哪一步卡住了。初始化序列是否完整最少需要Software Reset、Sleep Out、Pixel Format Set、Display ON这几条命令缺一条都可能白屏或显示异常。6.2 花屏、斜条纹的根因花屏的排查思路和白屏完全不同。如果屏幕有反应但显示乱七八糟大概率是以下几个原因SPI频率太高信号质量差。杜邦线环境下18MHz已经算保守了如果还花屏试着降到9MHz甚至4.5MHz花屏消失说明问题在信号完整性。窗口设置不对。这个前面说过只设置一次窗口然后连续写大量数据时如果坐标超出屏幕范围后续数据会写错位置。像素格式不匹配。初始化里设置了0x5516位色但发送数据时每次多发了或漏发了字节会造成颜色错位。MADCTL方向参数不对。这个最典型的表现是屏幕内容上下颠倒或左右镜像修改0x36寄存器的值即可。6.3 一个高效的调试顺序建议每次新拿到一块ILI9341模块我不会直接扔完整的初始化序列进去跑而是先用最简初始化序列让它亮屏然后依次测试单色填充、彩色渐变、240x320全屏色块、显示ASCII字符。每一步都确认无误后再叠加功能。这样做的好处是当某个环节出错时你能把问题范围缩小到极小——是全屏颜色不对还是局部显示错位还是字符乱码对应的排查路径完全不同。我见过太多人初始化序列一贴然后整屏花线就开始怀疑这块屏是坏的其实只是某个字节发错了而已。调试工具方面逻辑分析仪是我最推荐的设备几十块钱的就能用能直接看到SPI波形、DC电平切换时序和CS片选状态。如果屏幕上什么都没有但波形完全正常下次再遇到类似问题你的排查速度会比别人快很多。本文还有配套的精品资源点击获取
返回列表