
前几天有个朋友发来一段视频他的STM32F767ZI加一块Waveshare 4.0寸ILI9486 Shield在SPI接口下跑填充矩形demo屏幕上总是出现一些随机像素——不是固定的点也不是整条线而是像有人往纯色块上面撒了一把胡椒面。他第一反应是屏幕坏了我看了一眼代码告诉他这大概率是SPI驱动时序的经典问题。这篇文章就是我把他的板子拿过来实测排查的全过程整理。如果你也遇到“填充矩形时出现随机像素”“刷屏偶尔花屏”“纯色背景上有噪点”这类问题可以直接按我的思路走一遍。我会从硬件接线、SPI速率、初始化序列、DMA传输、Cache一致性几个方向逐个排查每步都会说清楚原理和判断依据尽量让你看完就能定位问题。1. 故障复现与硬件环境确认1.1 板卡与接线的基本确认先交代一下硬件平台主控是STM32F767ZIT6Nucleo-144板Cortex-M7核216MHz屏是Waveshare 4.0寸模块 SKU13587主控是ILI9486分辨率480×320通过SPI接口驱动。我这里用的是SPI1接法如下SCK - PA5MOSI - PA7CS - PA4软件控制DCD/CX- PB0软件控制RST - PB1软件控制注意这块Waveshare Shield默认是按树莓派40Pin排针设计的转到Nucleo-144上一般用杜邦线或者自己画转接板。如果你也是杜邦线飞线先别急着怀疑代码问题线长、线序、供电都会影响信号质量。排查第一步是确认供电。ILI9486模块通常需要3.3V逻辑电源部分模块带背光驱动可能需要5V输入再稳压到3.3V。SKU13587板载了电平转换和稳压电路供电范围比较宽但如果你用的是裸屏或者非官方模块先确认VCC是3.3V还是5V逻辑电平是否匹配。STM32F767的IO是3.3V如果屏模块是5V逻辑且没有板载电平转换SPI信号会超出MCU承受范围出现随机像素只是最轻的症状严重时可能烧IO。还有一个我见过很多次的坑SPI的MISO。如果只做写屏操作MISO确实可以悬空但HAL库初始化SPI时默认是Full-Duplex Master会检查RXNE状态。如果你初始化成双向模式或者开了硬件CRC但是没接MISO有可能在通讯过程中触发异常。最稳的做法是在CubeMX里选择Transmit Only Master或者即使接上了MISO也保持正常全双工配置但不要依赖读回值。1.2 随机像素的长相与分类随机像素并不是一个固定表象不同长相对应的问题差很远。我把这类故障分成了三种第一类纯色块内部随机分布孤立点位置不固定颜色杂乱。这类多半是数据线采样问题或时钟相位不对导致个别字节被错误采样。第二类固定位置的亮点、暗点或“横线”出现在矩形边缘。这类往往是窗口地址计算错误、坐标越界或CS拉高水平时机太早把最后一两个字节丢掉了。第三类刷屏时偶尔出现整片花屏、颜色错位、或随机闪烁。这类多半是SPI传输被中断干扰、DMA配置问题、D-Cache数据没回写或者初始化序列不完整导致电源和Gamma不稳定。拿到故障先不要盲目调代码先在屏幕上画三幅纯色画面全红、全绿、全蓝。如果三种颜色都有随机点那基本可以肯定是时序或数据路径问题如果只有某一种颜色出现随机点那和像素格式、Gamma寄存器或RGB顺序的关系更大。2. 先砍掉八成问题SPI速率与时钟相位2.1 把SPI频率先降下来我在调试SPI外设的时候“先降速”永远是第一动作不是最终方案而是用来缩小排查范围。ST官方的ILI9486应用电路和Waveshare官方例程里SPI时钟一般不建议超过20MHz很多屏甚至建议在9MHz以下工作。但人总有侥幸心理觉得主控支持54MHz屏也应该能跑结果直接把SPI预分频调到最低。STM32F767的SPI1挂在APB2上APB2时钟默认是108MHz。CubeMX里SPI Baud Rate Prescaler可以设置2、4、8、16等分频。如果选Prescaler2SPI时钟就是54MHzPrescaler4是27MHzPrescaler8是13.5MHzPrescaler16是6.75MHz。调试时直接从Prescaler16开始测也就是6.75MHz。如果降到这个频率随机像素完全消失就说明信号链路确实有采样裕量不足的问题。然后再逐步提高频率找到稳定工作的最高频率。我在实测中发现用长约15cm的杜邦线连接时SPI能稳定跑到13.5MHz但偶尔会随机丢点用短排线压到6.75MHz后就非常干净。这里有一个很关键的物理原因SPI时钟频率越高上升沿和下降沿越陡信号在长线上的反射和串扰就越明显尤其SCK和MOSI两条线如果平行走线SCK的边沿会耦合到MOSI上导致数据位被误采样。提示如果杜邦线比较长可以在SCK上串一个22Ω到33Ω的电阻适当减慢边沿、抑制振铃。这是很常见的信号完整性处理手段对随机像素问题有奇效。2.2 时钟极性相位与8位模式另一个容易被忽略的点是SPI的CPOL和CPHA。ILI9486在SPI接口下标准写法是模式0CPOL0CPHA0也就是SCK空闲为低电平数据在第一个边沿上升沿被采样。很多从英文论坛抄来的例程为了和某些SD卡驱动共用代码把SPI设成了模式3CPOL1CPHA1SCK空闲为高数据在下降沿采样。模式3在某些屏上也能跑因为ILI9486内部对时序有一定的容忍度但一旦周围有干扰、走线过长、或者频率提高模式3就很容易出错表现就是随机像素、偶发花屏、或者首字节丢失导致整帧错位。如果你不确定自己现在的配置直接在初始化代码里显式设置SPI_InitTypeDef *init hspi1.Init; init-Mode SPI_MODE_MASTER; init-Direction SPI_DIRECTION_2LINES; // 或 SPI_DIRECTION_1LINE_TX init-DataSize SPI_DATASIZE_8BIT; init-CLKPolarity SPI_POLARITY_LOW; // CPOL 0 init-CLKPhase SPI_PHASE_1EDGE; // CPHA 0 init-NSS SPI_NSS_SOFT; init-BaudRatePrescaler SPI_BAUDRATEPRESCALER_16; init-FirstBit SPI_FIRSTBIT_MSB;另外DataSize必须是8位。ILI9486的SPI接口是8位命令/数据模式如果你用了16位数据帧来直接传RGB565绝大多数情况下画面会乱七八糟。正确的做法是初始化寄存器时按8位发送指令和参数像素数据也按8位一个字节地发送颜色值拆成高字节和低字节。关于“6针SPI”和“4线SPI”的区别这里也顺带提一句ILI9486模块上常见的SPI接口是4线制即CS、SCK、MOSI、DC不包含MISO。DC脚用来区分当前发的是命令还是数据DC0时发命令DC1时发数据。如果你的屏是“3线9位SPI”或者“5线SPI”需要看板子的硬件配置不能直接套用4线驱动。3. 初始化序列与窗口寄存器最容易忽略的坑3.1 初始化时序的关键点随机像素问题有时候不在传输层而在初始化序列。ILI9486上电后必须经过完整的软件复位流程直接发配置命令会导致内部寄存器状态不确定最终呈现出来就是显示异常、噪点、颜色混乱。我的初始化流程是这样安排的拉高RST延时20ms以上拉低RST保持至少10ms再拉高延时120ms等待内部电源稳定发送0x01Software Reset再延时120ms发送0x11Sleep Out再延时120ms依次配置电源、Gamma、像素格式、帧率等寄存器最后发送0x29Display ON。很多随机点问题出在步骤3上。ILI9486的内部电源和振荡器需要时间稳定如果你上电后立刻初始化寄存器写入可能失败或写错但后续显示画面时并不会马上爆出问题而是要过一会或者刷到某个区域时才显现。所以初始化延时一定不要省0x01之后的120ms和0x11之后的120ms我都建议保留。然后是0x3APixel Format寄存器。ILI9486支持16位像素0x55和18位像素0x66。这里有一个特别经典的错误初始化里设置成0x6618bpp但填充矩形时按RGB565每像素2字节发送数据或者反过来初始化设置0x5516bpp但发送时按RGB666每像素3字节。这个不匹配会让屏对数据的解释完全错乱画出来的东西要么颜色随机要么边缘锯齿要么有大量杂点。如果你用Waveshare官方例程一般配的是16bpp模式也就是0x3A 0x55填充时每个像素发送2字节。如果还想启用18bpp不仅每个像素要发3字节还要处理好RGB565到RGB666的转换不建议新手折腾。初始化序列里还有一个位容易被漏掉0xF7Adjust Control 3。部分ILI9486初始化例程会写入一组0xF7参数比如0xA9、0x51、0x2C、0x82用于调整内部DC-DC和振荡器。如果漏掉这一项屏幕也能亮但内部电源纹波偏大显示纯色时可能出现随机噪点。我遇到过一次非常隐蔽的问题整屏红色正常整屏绿色有少量随机白点查了半天最后发现就是0xF7没写。注意不同批次的ILI9486对Gamma寄存器的默认值有差异官方Demo的初始化数组虽然可以互通但最好先用卖家提供的原版初始化代码跑通再去改自己的优化。3.2 窗口地址、MADCTL与坐标映射填充矩形时出现随机像素除了SPI传输问题还有一个高频原因列地址和页地址设置窗口不准确或者扫描方向配置和坐标计算不一致。ILI9486写显存不是直接写一个绝对坐标。要先通过0x2AColumn Address Set设置列范围通过0x2BPage Address Set设置行范围然后发送0x2CMemory Write之后SPI连续发送的数据就会自动填充到这个窗口内。窗口设置错了数据就会写到意料之外的地址看起来就像随机点或者残影。窗口地址的命令参数是4个字节顺序是起始列高8位、起始列低8位、结束列高8位、结束列低8位。行地址同理。如果你把高8位和低8位顺序写反或者忘了地址包含边界值填充矩形时右边界和下边界就可能错位。举个例子如果我要填充从(50,80)到(100,160)的矩形正确命令是LCD_WriteCmd(0x2A); // 列地址 LCD_WriteData(0x00); LCD_WriteData(50); // 起始列 LCD_WriteData(0x00); LCD_WriteData(100); // 结束列 LCD_WriteCmd(0x2B); // 页地址 LCD_WriteData(0x00); LCD_WriteData(80); // 起始行 LCD_WriteData(0x00); LCD_WriteData(160); // 结束行 LCD_WriteCmd(0x2C); // 开始写显存为什么很多新手会在这里出问题因为有些库封装的接口叫GLCD_SetWindow(x0, y0, x1, y1)但传入参数是“宽高”而不是“结束坐标”你按宽高填进去窗口会比预期大或小一圈涂抹出来的矩形边缘就会出现不属于当前填充内容的杂散像素。排查时建议先在代码里算出实际窗口范围与屏分辨率哪个越界。MADCTL0x36是另一个隐藏比较深的坑。它控制扫描方向、行/列交换、RGB/BGR顺序。默认值0x48通常是从左上角开始、RGB顺序。如果你从网上抄了一段代码那段代码可能是为竖屏240×320设置的而你的屏是横屏480×320MADCTL里MY、MX、MV组合不对画面会整体镜像、倒置或旋转同时坐标映射错乱表现出来就是随机位置出现不属于本矩形的颜色块。MADCTL寄存器的高3位含义MYbit7行地址方向0从上到下1从下到上MXbit6列地址方向0从左到右1从右到左MVbit5行/列交换1时行列互换用于横竖屏切换BGRbit30为RGB顺序1为BGR顺序当MV1时0x2A对应X坐标还是Y坐标会变化如果你的填充函数还是按MV0去设置窗口就会出现奇怪的跳变。我的建议是在调试随机像素问题时先用最保守的0x48横屏配置确认画面正常后再去调整方向。4. 填充函数的写法与DMA传输4.1 一个稳的填充矩形函数排查过SPI速率和初始化序列之后如果随机像素依然存在那就要看数据发送代码本身了。我见过很多自定义的填充函数逐像素把颜色写到SPI但每次写像素前都重新发送一次0x2A、0x2B、0x2C性能差不说还容易因为窗口命令和数据的边界混乱产生随机点。高效的写法是设置一次窗口然后连续发送窗口内所有像素数据。下面这个函数是我在实际项目里用的模板逻辑非常直白void LCD_FillRect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { if (x0 x1) { uint16_t t x0; x0 x1; x1 t; } if (y0 y1) { uint16_t t y0; y0 y1; y1 t; } LCD_CS_LOW(); // 设置窗口 LCD_WriteCmd(0x2A); LCD_WriteData(x0 8); LCD_WriteData(x0 0xFF); LCD_WriteData(x1 8); LCD_WriteData(x1 0xFF); LCD_WriteCmd(0x2B); LCD_WriteData(y0 8); LCD_WriteData(y0 0xFF); LCD_WriteData(y1 8); LCD_WriteData(y1 0xFF); // 持续写像素 LCD_WriteCmd(0x2C); uint32_t pixels (uint32_t)(x1 - x0 1) * (y1 - y0 1); uint8_t hi color 8; uint8_t lo color 0xFF; for (uint32_t i 0; i pixels; i) { LCD_WriteData(hi); LCD_WriteData(lo); } LCD_CS_HIGH(); }这个函数的优点是窗口只设置一次不会因为反复写命令而引入时序抖动。缺点是逐像素循环调LCD_WriteData每写一个字节都要等待TXE标志CPU占用高。如果只是验证问题这个速度够了要优化效率下一步就该用DMA。在实际测试中如果这个函数在6.75MHz下不再出现随机像素那基本可以判定问题出在数据发送路径的稳定性和速度。接下来再讨论DMA方案。4.2 DMA D-CacheF767特有的坑STM32F767一个非常容易踩坑的地方就是D-Cache。Cortex-M7核带有数据缓存默认情况下CPU写入内存的数据会先放在Cache里Cache还没写回主存时DMA外设直接访问主存读到的可能是旧数据。简单打个比方CPU和DMA之间隔着一个“缓存代理人”CPU写完数据后跟代理人说“你帮我记着”但没让代理人立刻转告DMA。DMA去问主存要数据主存手里拿到的还是上一版内容。结果就是SPI发出的像素颜色和程序里设置的颜色对不上显示出来就是随机像素、色块残影。在F7上使用SPIDMA传输帧缓冲区必须在启动DMA之前把对应的Cache区域写回内存。最常用的方法是调用CMSIS提供的接口SCB_CleanDCache_by_Addr((uint32_t *)((uint32_t)buf ~0x1F), len 32);为什么地址要做 ~0x1F对齐因为D-Cache的cache line大小一般是32字节。SCB_CleanDCache_by_Addr要求起始地址按32字节对齐如果你直接传一个未对齐的指针可能漏掉边界部分。加上32字节是为了覆盖尾部的超额区域。具体到SPI DMA传输流程我的实现是这样的void LCD_DMA_Send(uint8_t *buf, uint32_t len) { // 1. 确保DMA要读取的内存是最新数据 SCB_CleanDCache_by_Addr((uint32_t *)((uint32_t)buf ~0x1F), len 32); // 2. 启动SPI DMA发送 HAL_SPI_Transmit_DMA(hspi1, buf, len); // 3. 等待DMA传输完成 while (hspi1.State HAL_SPI_STATE_BUSY_TX); // 4. 等SPI模块真正移出最后一个字节 while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY)); }第四步非常关键。HAL_SPI_Transmit_DMA返回后DMA可能已经把最后一个字写到了SPI数据寄存器但SPI移位寄存器还在往外移出此时如果你立刻拉高CS最后一个字节会被截断。Waveshare这类屏对CS时序有一定容忍度但如果正好截断在数据末尾矩形边缘最后一列或最后一行就可能会出现随机像素。注意等待BSY清0后再拉高CS这条原则不仅适用于DMA也适用于轮询方式发送。只要你是用软件GPIO控制CS就必须保证这一点。我发现有些网友解决不了问题就干脆把D-Cache关掉这个办法能用但不推荐作为长期方案。关掉D-Cache会显著降低CPU访问内存的速度尤其在大数组操作时性能损失明显。更好的做法是保留Cache在每次DMA之前清理对应区域。或者如果你用的是CubeMX生成代码可以在MPU配置里把存放帧缓冲的SRAM区设为Non-Cacheable这样DMA和CPU对这块区域的读写都是直接操作主存天然一致。4.3 软件片选与BSY标志用软件GPIO控制CS时时序上还要注意一个细节CS拉低后需要等待一小段时间再开始传数据CS拉高前必须确保所有数据已经完整送出。很多人喜欢用GPIO_WriteBit拉高CS但并没有检查SPI的BSY标志最后在屏幕边缘出现零星乱点找半天找不到原因。我建议把发送函数封装成统一的接口所有写完数据后都执行一次SPI_Wait_BSY()static inline void SPI_Wait_BSY(SPI_TypeDef *SPIx) { while (SPIx-SR SPI_SR_BSY); }然后在LCD_CS_HIGH()之前调用。还有一种情况是SPI传输期间被中断抢占。如果外部中断或者RTOS调度发生在数据发送过程中且中断服务里恰好也操作了SPI就会导致SPI状态错乱表现就是刷新画面时随机出现一两个错点。解决方法是给SPI发送代码加临界区保护或者在中断服务里不要碰同一个SPI外设。5. 排查速查表与实操总结5.1 故障排查速查表我把这次排查中遇到的现象、可能原因和解决方法整理成了表格方便你对照现象可能原因排查方法纯色块内随机孤立点位置不固定SPI时钟过快、信号振铃、采样裕量不足降到6.75MHz测试SCK串22-33Ω电阻缩短连线矩形边缘最后一列/行有杂点CS拉高太早最后一个字节被截断传输结束等待SPI_BSY清0后再拉高CS固定位置的点或残影窗口地址0x2A/0x2B设置错误、坐标越界打印窗口参数检查结束坐标是否包含边界颜色错乱、RGB颜色随机互换MADCTL的BGR位或者像素格式0x3A设置错误确认0x36和0x3A的值检查是16bpp还是18bpp刷屏时偶发花屏或闪烁D-Cache未写回DMA读到旧数据DMA前调用SCB_CleanDCache_by_Addr初始化后一段时间才出现噪点初始化序列缺0xF7、电源和振荡器不稳检查初始化数组补齐上电和复位延时使用DMA后随机丢字DMA缓冲区未对齐、中断抢占SPI缓冲区32字节对齐给SPI发送加临界区保护整屏镜像或倒置且伴随杂点MADCTL方向位设置与实际扫描方向不符参考3.2节用0x48先验证横屏5.2 我的排查顺序习惯如果你现在正处于“随机像素困扰”中我建议按照下面的顺序排查不要跳步先降速。把SPI预分频调到16确认随机像素是否消失。这一步能区分是信号完整性还是寄存器配置问题。跑单色全屏测试。分别填充纯红、纯绿、纯蓝观察随机点是否随颜色变化。检查初始化序列。重点看0x3A、0x36、0xF7以及上电复位延时。检查填充函数窗口设置。确认0x2A/0x2B参数是否符合横屏分辨率。如果启用DMA先关掉DMA用轮询方式连续写数据。如果轮询画矩形正常但DMA不正常基本就是Cache一致性问题。最后再优化速度逐步提高SPI频率直到出现临界点。在实际调试中我最后发现朋友的这块板和代码其实有三个问题叠加SPI频率跑到了27MHz太高、初始化序列漏了0xF7、DMA传输前没有做Cache清理。单看每一个都不至于完全不能工作但三个问题叠在一起就让随机像素变得非常明显。这也很符合嵌入式调试的常态——大多数疑难杂症都不是由一个原因造成的。最后再分享一个小技巧在屏幕上画一条十字交叉线通过窗口模式一次只画一条线一条横线一条竖线。如果某条线边缘有毛刺、端点有多余像素或者线条某一段出现断裂说明窗口坐标或扫描方向还有问题。这种测试比整屏填充更容易把坐标问题暴露出来。把随机像素看成“症状”而不是直接去改颜色输出你会发现真正的病根往往藏在时序和配置里。