拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32 HAL库驱动ILI9341 TFT LCD完整指南(含触摸屏控制)

简介本资源是一套面向嵌入式初学者与STM32进阶开发者的ILI9341 TFT LCD驱动实践方案聚焦于320×240分辨率屏幕的轻量级HAL库实现与触摸交互控制特别适合无DMA/中断经验的学习者快速上手图形界面开发。压缩包共35个文件含13个头文件.h定义驱动接口与字体资源、11个源文件.c实现SPI通信、图形渲染及触摸校准等核心逻辑另有工程配置文件.ioc、说明文档.md、许可证.license及备份文件.zbak整体体积12MB结构清晰、模块解耦度高。已有149人学习下载配套示例基于STM32F746ZG Nucleo板构建提供完整工程模板、函数调用范例及优化的刷新算法所有模块支持按需裁剪——开发者可直接复用主控驱动、增强绘图含5×5点阵字体或独立集成触摸屏控制功能大幅降低TFT显示开发门槛。 最近在做一个基于STM32的显示交互项目需要一块彩色屏幕做参数实时显示和触摸控制。翻遍了手头的模块最后选了ILI9341驱动的TFT LCD——这屏确实是目前小尺寸彩屏里的常青树2.4寸到3.5寸的模组几乎都是它在带动价格便宜、资料还多。但网上能找到的例程大部分是标准库版本而且代码结构绕得人头疼移植到HAL库的时候又是改引脚又是改延时折腾了两天才跑通。这篇就把我基于STM32的HAL库驱动ILI9341的思路、核心代码和触摸屏控制的完整流程写清楚给碰到同样需求的兄弟一个参考。不管你用的是STM32F103还是F4系列只要HAL库版本不是太老这套思路基本都能直接搬。我会从初始化序列、底层显示函数、DMA刷屏加速、触摸校准这几个方面拆开讲最后附上我实际调板时踩过的一堆坑。整个代码规模不大但每个函数我都尽量写出“为什么这么做”方便你按自己的分辨率去改。1. 整体设计思路为什么选择HAL库与ILI9341的组合1.1 从需求反推方案选型做显示类项目第一步不是急着写代码而是先想清楚“这屏到底要怎么用”。我的项目需求是实时刷新传感器曲线、显示几个设置页面、支持触摸切换参数。这三条直接决定了硬件选型和驱动方案。屏幕方面ILI9341支持240x320的RGB565输出最大64K色对于做曲线、仪表盘这类界面已经完全够用。这块IC的SPI接口最高能跑到几十MHz如果配合DMA刷整屏一帧的刷新时间可以压到30ms左右肉眼基本看不出闪烁感。相比直接用并口屏SPI走线少对PCB布局和MCU引脚压力都小很多尤其适合在STM32这种引脚资源不算丰富的环境里使用。驱动方案上我选了STM32CubeMX生成HAL库工程。这两年HAL库慢慢成了主流FSMC、SPI、DMA这些外设的抽象做得更统一跨芯片迁移时不用把寄存器逻辑重写一遍遇到问题也更容易找到参考案例。虽然HAL库的封装层让代码看起来比标准库“绕”一点但只要把底层函数和显示层剥离开调用逻辑反而更清晰。1.2 驱动库的模块分层设计在实际写代码之前我把驱动库拆成了三层这个设计思路很重要直接关系到后面移植和Debd出问题的效率底层硬件抽象层只负责和MCU外设打交道包括SPI收发、GPIO控制复位脚、背光脚、数据/命令脚、延时函数。显示设备驱动层封装ILI9341的寄存器操作比如初始化序列、设置窗口、写像素点、开显示。上层应用接口层向上提供画点、画线、填色、字符显示、图片显示等函数不需要关心底层是SPI还是FSMC在干活。这样分层的好处很明显。比如我现在用SPI的板子哪天想换成并口屏只需要改底层硬件抽象层上层画线画圆的函数一个都不用动。我之前在网上见过很多“一个文件干到底”的LCD驱动初始化、画点、画字符塞在一起看着是省事但一旦需要调底层时序整个文件都要跟着乱。1.3 ILI9341的核心工作原理写驱动之前你最好先理解IL9341是怎么把颜色“画”到屏幕上的。它就相当于一块带GRAM显存的控制器MCU不直接控制每个像素的亮灭而是往GRAM里写数据ILI9341内部自动把GRAM内容映射到屏幕上。整个显示流程可以简化成三个步骤MCU发送一个“写GRAM”的命令0x2C。告诉ILI9341本次颜色数据要写进的坐标范围列扫描地址和页扫描地址。连续发送RGB565的像素数据每像素两字节ILI9341自动按行列递增规律把数据填到对应的GRAM里。理解了这条链路你就明白为什么“设置窗口”函数是所有画点画图操作的基础。你画的每一个点、每一个矩形、每一张图本质上都是“先设一个范围再往里填颜色数据”。性能优化的关键也在于这里如果能减少重复设置窗口的次数、让数据发送过程用DMA接管整屏刷新的效率就能大幅提升。2. 硬件连接与SPI通信配置别在引脚上省心思2.1 明确引脚分配与接口定义我用的是一块2.8寸、320x240分辨率的ILI9341模组触摸IC是XPT2046板和屏是连好的模组引出了14个PINVCC、GND、CS、RESET、DCRS、SDIMOSI、SCK、LED背光、SDOMISO、T_CLK、T_CS、T_DIN、T_DO、T_IRQ。其中LED背光这个脚值得单独说一下。有些模组直接把背光串联电阻接到VCC有些则引出来让MCU控制。如果你希望实现“屏幕休眠时能关背光省电”这个脚就派上用场了。我习惯用一个GPIO去控制它初始化为高电平点亮省电时拉低即可。STM32F103C8T6的SPI1引脚和我的模组连接方式如下表所示TFT模组引脚STM32引脚说明VCC3.3V必须共地电源纹波大会导致花屏GNDGND电源地务必和MCU共地CSPA4SPI片选软件控制RESETPA3复位脚低电平复位DCPA2数据/命令选择低电平命令高电平数据SDI (MOSI)PA7SPI1主输出SCKPA5SPI1时钟SDO (MISO)PA6SPI1主输入触摸用纯LCD显示也可不接LEDPA1背光控制默认高T_CLKPB13触摸SPI时钟T_CSPB12触摸片选T_DINPB15触摸SPI输入MCU到触摸ICT_DOPB14触摸SPI输出触摸IC到MCUT_IRQPB10触摸中断/忙信号每次采样前需查询我特地把LCD和触摸的SPI分开了各用一组SPI总线避免频繁切换CS导致时序混乱。如果你想让触摸和LCD共用一组SPI也可以但需要在切换CS时处理毛刺问题新手上手容易踩坑。2.2 SPI模式与波特率设置STM32的HAL库在初始化SPI时需要配置几个关键参数。ILI9341在SPI模式下用的基本上是模式0CPOL0CPHA1或者模式3CPOL1CPHA1我实测两款不同品牌的模组用模式0和模式3都能正常驱动这取决于模组厂家在设计时有没有对时序做额外处理。保险起见你可以先用模式0试如果出现颜色错乱再切模式3。提示模式0是CPOL0、CPHA1即空闲时时钟为低在第二个边沿采样数据。模式3则是空闲时为高也是在第二个边沿采样。如果你的屏初始化后出现“灰屏但背光亮”的现象先别急着怀疑代码试试切换SPI模式我遇到过好几次这种情况。波特率方面我的STM32F103C8T6主频是72MHzSPI1挂在APB2总线上最高36MHz。实际测试下来18MHz二分频是比较稳妥的选择传输稳定性和EMI表现都还可以。36MHz不分频在短排线、接插件质量好的情况下也能跑但一旦排线一长就很容易出数据错位。你的项目如果追求极致刷屏速度可以先提升到36MHz试一下但务必检查一下颜色有没有“拖影”或者“雪花粒”的干扰点。2.3 CubeMX配置参考如果你是第一次用CubeMX生成工程我建议按下面这些参数来配置跑通之后再按需微调RCCHSE晶振主频72MHz。SPI1全双工主机Motorola模式数据大小8位时钟极性Low时钟相位1EdgeNSS软件管理波特率分频2或4。GPIOPA1、PA2、PA3、PA4设为GPIO输出默认都高电平PB10设为输入模式触摸查询用PB12、PB13、PB14、PB15配置为通用SPI的CS、SCK、MOSI、MISO功能。注意SPI的CSNSS要选软件管理否则硬件的NSS信号抢占CS控制权会出现莫名其妙的片选错位。这个我在初学时踩过一次当时屏幕始终不亮看了半天才发现是NSS引脚被硬件拉低CS信号完全乱了。3. 驱动库核心实现从初始化到快速刷屏3.1 初始化序列一步一个坑的寄存器配置ILI9341的初始化是整个驱动中最“玄学”的部分。每个模组厂家提供的初始化命令序列多少会有点差异但绝大多数模组都兼容标准的初始化流程。下面这段是我在N种屏上验证过、稳定工作的初始化序列用HAL库实现void LCD_Init(void) { LCD_Reset(); // 硬件复位 LCD_WriteCommand(0x01); // 软复位 HAL_Delay(10); LCD_WriteCommand(0x11); // 退出睡眠模式 HAL_Delay(120); LCD_WriteCommand(0x36); // 存储器访问控制扫描方向 LCD_WriteData(0xC8); // 行列交换具体值取决于屏幕安装方向 LCD_WriteCommand(0x3A); // 像素格式 LCD_WriteData(0x05); // 16位RGB565 LCD_WriteCommand(0x29); // 开显示 LCD_SetWindow(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1); LCD_WriteCommand(0x2C); // 写GRAM命令 LCD_Fill(0, 0, LCD_WIDTH-1, LCD_HEIGHT-1, COLOR_BLACK); }这个序列看上去很简单但里面有几个必须注意的点。第一是0x36寄存器的方向位。这个寄存器控制扫描方向和RGB/BGR顺序也就是说它同时影响“屏幕图像是否倒转”和“红蓝是否互换”。不同的模组、不同的安装方向这个寄存器的值往往都不一样。我的板子正立安装所以设成0xC8如果你的屏幕显示镜像了试着改成0xC0、0x28、0x88等值多试几次就能找到正确的方向组合。第二个关键是0x3A设为0x05也就是RGB565。如果是0x06则是RGB666虽然颜色更细腻但占用的传输带宽更大大多数应用用RGB565已经够。0x55是RGB1616看起来数据位数更多但兼容性反而不高一般不建议用。关于初始化序列我还有一点心得很多网上例程会贴出一大段厂家私有的“初始化魔法序列”里面设置伽马曲线、电源控制等一堆寄存器。我实际对比过在当前主流模组上只靠上面这个迷你序列就已经能正常显示了。那些长序列主要是在早期屏的驱动上用于修正特定批次面板的显示效果。所以如果你在调试时发现画面颜色有点淡可以再往里加伽马寄存器配置否则不要过度迷信那串长代码初始化太长反而增加了出问题的概率。3.2 写命令和写数据DC引脚状态的两次翻转ILI9341和MCU之间通过DC引脚区分当前发的是命令还是数据。DC为低时SPI发出的字节被当作命令DC为高时发出的字节被当作数据。所以底层最核心的两个函数就是void LCD_WriteCommand(uint8_t cmd) { HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_RESET); // 命令模式 CS_Select_Low(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); CS_Select_High(); } void LCD_WriteData(uint8_t data) { HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET); // 数据模式 CS_Select_Low(); HAL_SPI_Transmit(hspi1, data, 1, 100); CS_Select_High(); }这里有个优化细节连续发送多个数据字节时不要每发一个字节就拉高CS一次。比如填充颜色时你只需要设置一次DC为高然后连续发送一长串像素数据最后再拉高CS。如果每个字节都重新拉一次CSSPI每次传输都需要重新建立同步状态时间损耗非常明显。void LCD_WriteDataBuffer(uint8_t *buffer, uint32_t len) { HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET); CS_Select_Low(); HAL_SPI_Transmit(hspi1, buffer, len, 1000); CS_Select_High(); }用这个函数去填充一个160x240的矩形区域传输效率会提升不少配合DMA还能进一步释放CPU。3.3 设置窗口所有绘图操作的地基设置了操作窗口后ILI9341收到的后续数据会按照“从左到右、从上到下”的顺序自动填入到该窗口内的GRAM中。调用方式void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { 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); // 写内存开始 }注意ILI9341地址坐标的最大值取决于分辨率。在240x320的屏上x范围从0到239y范围从0到319。如果你的应用是320x240横屏使用就要把行列地址的方向寄存器0x36配合调好否则会出现图像显示区域和实际定义区域错位。基于这个窗口函数画点就特别简单先把窗口设置为单个像素点的大小然后发送一个RGB565的颜色数据。但如果你要连续画一条斜线每次都重新设置窗口就很浪费了效率会比直接画矩形低得多。所以实际画线时我很少用逐点画而是把线条拆成若干小矩形填充速度能快好多倍。这个就是对窗口机制灵活应用的体现。3.4 RGB565颜色的存储顺序RGB565把16位颜色分成5位红色、6位绿色、5位蓝色在内存中占两个字节。传输顺序上ILI9341默认先收高字节、再收低字节也就是说颜色0xF800红色传输时先发0xF8再发0x00这个顺序写反了画面就会变成红蓝互换或者颜色完全错乱。刚开始玩屏的朋友最容易在这栽跟头明明代码逻辑都正确但显示出来的蓝色变成了橙色。我给你的排查思路是先画一个大红色块如果画面显示的是红色说明高低字节顺序没问题如果变蓝或变绿那就是0x36寄存器的RGB/BGR位没设置对或者字节顺序反了。3.5 DMA刷屏让刷新率提升一个量级传统方式是CPU把每个像素的数据逐字节发给SPI刷一整屏320x240需要3202402 153600字节即使SPI跑18MHz纯传输时间也需要约8.5ms但加上每传输一个字节CPU都需要参与的IO开销实际耗时常常超过40ms。刷屏时CPU一直被占用其它任务全都跟着卡顿。DMA方案把数据搬运交给DMA控制器CPU只需要设置一次传输地址和长度然后就可以去处理其他逻辑等传输完成中断回来收尾。在HAL库中开启SPI DMA发送的代码很简单void LCD_Fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint16_t w x1 - x0 1; uint16_t h y1 - y0 1; uint32_t total (uint32_t)w * h; static uint16_t *colorBuf NULL; if (colorBuf NULL) { colorBuf (uint16_t *)malloc(total * sizeof(uint16_t)); } for (uint32_t i 0; i total; i) { colorBuf[i] color; } LCD_SetWindow(x0, y0, x1, y1); HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET); HAL_SPI_Transmit_DMA(hspi1, (uint8_t *)colorBuf, total * 2); }但这里要小心一点DMA传输是异步的函数返回时数据可能还没发完。如果紧接着又去设置新的窗口就会干扰还没发完的数据造成颜色错位。我的做法是加一个发送完成标志在HAL_SPI_TxCpltCallback回调里置位volatile uint8_t lcd_dma_busy 0; void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { lcd_dma_busy 0; } } void LCD_Fill_DMA(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { while (lcd_dma_busy); // 等上一次DMA发完 // 填充颜色缓冲、发DMA lcd_dma_busy 1; HAL_SPI_Transmit_DMA(hspi1, (uint8_t *)colorBuf, total * 2); }还有一个细节使用DMA时缓冲区地址的字节对齐问题。STM32F1系列的DMA在非对齐访问时会触发错误或者速度骤降所以colorBuf用malloc分配的时候最好用带有对齐属性的声明方式比如__attribute__((aligned(4)))或者用全局数组。不过对于颜色重复的纯色填充更常见也更省内存的做法是只准备一行宽度的缓冲区每行都重新触发一次DMA发送这样内存占用少速度也不差。比如窗口宽是320你只需准备320*2640字节的缓冲区整屏分240行发送。3.6 图片显示用取模工具生成C数组显示图片主要是两种方式一种是把图片转为C语言数组直接烧录到Flash里另一种是从SD卡读取图片文件再解析显示。前者简单适合做固定logo或图标后者灵活但需要文件系统支持。我用的方式是把图片转成RGB565数组。处理步骤用图像编辑软件把图片尺寸调整到和显示区域一致比如120x120。用Image2Lcd之类的取模工具选择RGB565格式输出C语言数组。把数组复制到代码里调用批量写像素函数把整个数组写入屏幕。这里有一个重要的技巧大数组不要定义在函数内部。STM32F103的栈默认只有2KB左右一个120x120的RGB565数组就要1201202 28800字节放栈里直接溢出跑飞是轻的严重时连初始化都过不去。要把数组定义为全局或者用const关键字放到Flash里。如果你遇到“程序一执行就进HardFault”的情况多半就是踩了栈空间溢出的坑。这也是为什么我会优先建议用小区域分块传输而不是一次性发送整张大图。4. 触摸屏控制从坐标读取到稳定校准4.1 读取触摸坐标的底层逻辑我的触摸芯片是XPT2046这是一个典型的电阻式触摸屏控制IC核心原理就是通过ADC采集触摸点所在位置的电压值进而换算出坐标。比起电容触摸电阻屏的精度在工业控制、仪器仪表这类场景中反而更可靠不受表面水汽影响戴手套也能操作。XPT2046通过SPI接口和MCU通信每次读取坐标之前MCU要先发送一个控制字节告诉芯片要测量X方向还是Y方向以及分辨率。一个读取流程如下uint16_t TP_ReadX(void) { uint8_t buf[2]; uint16_t raw; buf[0] 0xD0; // 启动测量X坐标12位分辨率 HAL_SPI_TransmitReceive(hspi2, buf, buf, 2, 100); raw ((buf[0] 8) | buf[1]) 4; return raw; } uint16_t TP_ReadY(void) { uint8_t buf[2]; uint16_t raw; buf[0] 0x90; // 启动测量Y坐标12位分辨率 HAL_SPI_TransmitReceive(hspi2, buf, buf, 2, 100); raw ((buf[0] 8) | buf[1]) 4; return raw; }注意这里XPT2046的12位ADC输出范围是0~4095但触摸屏的有效区域并不会完全覆盖整个ADC范围所以原始值往往达不到满幅度后续校准的时候要以实际测得的边界为准。4.2 软件滤波别让抖动的触摸值折磨你电阻屏的触摸值天生带着抖动尤其是人手指接触时压力不均匀会导致读数上下波动很大。如果不对读数做滤波处理按一个按钮时可能会触发两三次点击事件用户体验非常差。我试过几种滤波方案最好用的是中值滤波加均值滤波的组合连续采样5次去掉最大最小值剩下3个取平均。这样既能滤掉随机毛刺又不会让响应变慢很多。代码实现uint16_t TP_ReadX_Filtered(void) { uint16_t samples[5]; for (int i 0; i 5; i) { samples[i] TP_ReadX(); HAL_Delay(1); } // 简单冒泡排序选出中间三个求平均 for (int i 0; i 4; i) { for (int j i 1; j 5; j) { if (samples[j] samples[i]) { uint16_t tmp samples[j]; samples[j] samples[i]; samples[i] tmp; } } } return (samples[1] samples[2] samples[3]) / 3; }这里每次采样之间延时1ms主要是让芯片内部ADC完成采样保持和转换如果连读太快转换没完成就读取数值反而会乱。实际测试下来最终得到的坐标已经比较稳定按钮的误触率大幅降低。4.3 坐标校准把ADC值映射到屏幕像素触摸屏采集到的原始ADC值是一个0~4095的数值屏幕坐标则是0~239或0~319两者不是线性等比关系因为触摸屏的ITO导电层和液晶屏之间可能存在轻微的角度偏移贴合工艺也有误差。所以必须做校准映射。最简单的线性校准方式是“两点校准比例缩放”前提是触摸屏的X轴和Y轴生产时已经与LCD基本平行这在绝大多数现成模组上成立。首先取屏幕左上角和右下角两个固定位置分别得到ADC坐标 (x0, y0) 和 (x1, y1)然后建立线性映射uint16_t TP_GetX(uint16_t adc_x) { // 映射到0~239 return (uint16_t)((adc_x - tp_x0) * 239 / (tp_x1 - tp_x0)); } uint16_t TP_GetY(uint16_t adc_y) { // 映射到0~319 return (uint16_t)((adc_y - tp_y0) * 319 / (tp_y1 - tp_y0)); }tp_x0、tp_y0、tp_x1、tp_y1是校准时得到的边界ADC值。实际校准的操作流程是在屏幕四个角画四个十字标记用户依次点击程序记录点击位置的ADC值。如果四个角测出来的映射都能准确压中标记点说明线性度很好。如果你的屏要支持横竖屏切换校准参数必须跟着屏幕方向一起变。因为扫描方向寄存器改了ADC值和像素坐标的对应关系也随之改变。我的做法是在配置结构体里保存一组“当前屏幕方向”对应的校准参数切换方向时自动加载。4.4 触摸判定与按键防抖触摸坐标拿到手之后最后一步就是判定用户按了哪个控件。比较常见的做法是采用区域判定比如按钮的矩形范围是 (x, y, w, h)只要触摸点落在这个矩形内就触发点击事件。但直接这样判定会有一个问题手指按上去的瞬间和抬起来的瞬间坐标都会跳一次如果不加防抖一次点击可能触发两次事件。我的处理方案是“按下检测释放确认”void Touch_Scan(void) { if (TP_ReadIRQ() 0) { // 触摸中断脚为低表示有触摸 uint16_t rx TP_ReadX_Filtered(); uint16_t ry TP_ReadY_Filtered(); uint16_t px TP_GetX(rx); uint16_t py TP_GetY(ry); if (press_cnt 3) { // 连续多次采样确认按下 handleButtonEvent(px, py, BUTTON_PRESS); } else { press_cnt; } } else { press_cnt 0; } }这样做的好处是用户按下时不会立刻触发需要连续几次确认后才响应有效过滤了快速触碰产生的毛刺。对于按键触发类需求这个方案足够稳。5. 常见问题与排查技巧实录5.1 白屏背光亮但没有任何画面这是最常见的一种故障。排查顺序应该是先检查RESET引脚复位时序确认初始化前有一个完整的低电平脉冲至少10us再看DC引脚是否配置正确如果DC接错命令和数据就全乱了然后检查SPI的波特率和极性尤其是极性不对时屏幕虽然能初始化但画面数据全错最后检查背光引脚是否点亮。手动按顺序排查基本能定位问题。5.2 颜色偏蓝或偏红图像镜像这是0x36寄存器方向位没配置好。RGB/BGR位bit3为0表示RGB顺序为1表示BGR顺序。如果显示红色变成了蓝色就把这一位取反试试。镜像问题则是bit6和bit7控制的多试几个组合值就能找到正确方向。5.3 画面出现“雪花”干扰或色块错乱多数是SPI传输速率过高或者排线质量不佳导致信号串扰。可以先把波特率降到9MHz如果画面稳定了就说明问题出在信号完整性上。另外检查一下VCC和GND是否稳定ILI9341在电流需求大的瞬间如果供电不足也会出现花屏现象。建议在模组的VCC引脚附近并联一个10uF100nF的电容。5.4 触摸点击不准越靠近边缘偏移越大这就是校准没做好。如果边缘偏差特别大甚至出现点击偏转到相邻按钮的情况多半不是线性映射的问题而是触摸屏的X/Y轴和LCD的扫描方向不对应需要考虑交换X/Y坐标或者取反。写一个简单的调试函数在屏幕上实时显示当前触摸ADC值和换算后的像素坐标你就很容易找到是哪个轴出了偏差。5.5 使用DMA刷屏时画面有时出现“撕裂”“撕裂”是指屏幕上半部分还是上一帧内容下半部分已经是新一帧了。这是因为MCU写入GRAM的速度和LCD扫描输出的速度不同步。解决思路有两个一是在刷屏前等一下屏幕的VSYNC信号ILI9341在页扫描的特定时刻会有状态变化可以通过轮询读取状态寄存器实现二是将一帧图像的传输切分成几个小块逐帧发送减少错位窗口。对于中小型项目采用第二种方式更简单且基本看不出来差异。下面把容易踩的坑整理成一个速查表方便你对照检查现象可能原因排查/解决方法白屏无显示SPI模式不对、复位时序不对切换SPI模式0/3确认复位脚低脉冲时序颜色红蓝互换0x36寄存器RGB/BGR位错误修改0x36的bit3图像上下/左右镜像扫描方向寄存器设置错误试0x36的不同组合值显示颜色错乱字节顺序不对、SPI速率过高检查RGB565高低字节顺序降波特率局部区域颜色干扰排线接触不良、供电不足检查接线VCC并联去耦电容触摸点击偏移校准参数不对或轴方向取反打印ADC值重新校准DMA发送后花屏同步问题数据未发完就操作增加busy标志位等待传输完成程序启动卡死大数组占用栈空间溢出大数组改全局或const放进Flash触摸响应慢滤波采样太多、延时过大减少采样次数优化延时5.6 进阶排查用逻辑分析仪解决疑难杂症如果前面这些常规手段都试过了还是没能解决我强烈建议你借一台逻辑分析仪把SPI的SCK、MOSI、DC、CS四路信号抓下来对照驱动代码里的时序逻辑手动检查。很多看似“玄学”的问题一抓波形就真相大白。我印象最深的一次排查屏幕偶尔会在开机后随机出现一块花屏区域代码逻辑完全找不到问题。用逻辑分析仪抓了波形才发现是SPI的CS信号在DMA传输完成之后没有及时拉高导致DMA空闲期的非法电平被LCD误识别成了数据。加上busy标志位的等待后这个故障就彻底消失了。对于这种间歇性故障靠肉眼调试和猜根本找不到根源示波器和逻辑分析仪是必需的。6. 移植经验和扩展建议6.1 从F103迁到F4/F7的注意事项如果你打算把这套驱动移植到STM32F407或者其他更高性能的板子上主要改动集中在底层的SPI配置和延时函数上。HAL库的接口都是统一的所以LCD_WriteData、LCD_SetWindow这些函数基本不用改动。F4系列如果没有修改时钟树SPI时钟可能高达84MHz这时一定要注意分频系数否则一上来就用最大速度的话可能反而会因为信号反射导致数据错误。我建议F4上先把SPI配到21MHz左右稳定后再往上拉。F407在PIN对PIN兼容的情况下可以直接把工程里的启动文件和链接脚本换掉代码层面几乎零改动就能跑起来。6.2 想上复杂界面配合轻量级GUI框架如果你只是画几个固定控件手写画点画线函数够用了。但如果要做一个有窗口、动画、进度条甚至下拉菜单的GUI手写代码量会爆炸。我目前在用的一套方案是LVGL配合这套ILI9341驱动做底层移植LVGL只需要你提供“写像素块”的底层回调函数其他画控件、响应触摸事件都由它来管。LVGL的移植一般分两步一是配置lv_conf.h把颜色深度设为16位二是把tick和flush两个回调函数对接好。flush回调里直接复用我们上面的LCD_Fill改造成支持任意矩形的函数就行。这样做好之后一个带渐变按钮和滑条的仪表控制界面我大概两三天就写完效率比手写界面高一个量级。6.3 关于屏幕供电和EMC的提醒ILI9341背光全开的时候电流大约在40~80mA加上触摸IC的功耗整个模组峰值可以突破100mA。如果你用3.3V稳压芯片供电注意稳压芯片的输出能力要留够余量。我试过用一片AMS1117-3.3给MCU和屏幕同时供电触摸滑动时偶然会出现电压跌落导致花屏。后来我增加了电容并对背光使用了单独的MOS管开关控制问题就消失了。另外提一句即使项目只是前期原型验证也建议把接地处理干净所有模块共地不要在GND上形成回路。SPI信号线如果距离电源线太近刷屏时屏幕上会出现斜纹干扰这在示波器上看就是信号线上叠加了周期性噪声。把信号线和电源线分开走线之后斜纹就消失了。细节决定成败玩屏玩到最后发现困住你的其实不是驱动代码而是一堆硬件层面的隐性问题。这套驱动和触摸方案目前已经跑在我手头的几个项目上包括一个带历史曲线显示的环境监测仪和一个带参数设置面板的电机控制盒表现都挺稳。如果你也在做类似的东西欢迎按我这个思路去改碰到问题再走一遍上面这节排查表大部分坑都能绕过去。最后再分享一个小技巧写LCD驱动的时候把底层函数全部做成带时间戳的日志输出比如每次SPI传输完成后记录一下耗时。这个方法在我优化刷屏速度时帮了大忙能精确算出每段代码的性能瓶颈在哪里。做驱动优化不是靠感觉而是靠数据。你试着把刷一帧的时间打印出来再对比DMA和轮询两种方式的数据你会对“优化”这件事有完全不同的体会。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门