ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32驱动ILI9486 SPI屏填充矩形出现随机像素的排查与解决

STM32驱动ILI9486 SPI屏填充矩形出现随机像素的排查与解决 最近在做一块 STM32F767ZI 开发板的外设扩展外接 Waveshare 4.0 寸 LCD ShieldSKU13587主控是 ILI9486跑 SPI 接口。刚开始一切正常初始化能刷背景色点个像素、画条直线都没毛病。等我开始调用 fillRect 填充一个大矩形的时候屏幕上突然出现零星的随机像素点位置每次刷新都不一样颜色也不固定就像屏幕上撒了一把细盐。第一反应是排线松了、屏坏了但重新压紧排线、换了一块屏幕后问题照旧。后来把整个显示链路从硬件到驱动代码重新过了一遍才确认这根本不是屏的问题而是 SPI 传输细节里几个非常容易忽略的坑叠在了一起。这篇记录的就是完整的排查过程和最终修法遇到类似“大区域填充出现随机像素”的朋友可以直接照着排。1. 故障现象还原填充矩形时出现的“雪花点”到底长什么样1.1 最开始的复现步骤我的环境是这样的STM32CubeMX 生成工程HAL 库SPI1 做主模式查询方式发送F767 主频 216MHzSPI 时钟最初配置在 27MHz。屏幕初始化序列用的从 Waveshare 官方例程改过来的 ILI9486 初始化代码颜色格式选 RGB565。复现步骤如下全屏填充黑色0x0000调用 fillRect(50, 50, 250, 250) 画一个红色矩形期望结果是边缘清晰、内部纯色的矩形实际结果是矩形内部出现大约三四十个随机像素点。这些像素点有一个共同特点位置不固定。同一段代码重复执行每次出现的位置都不一样但横坐标和纵坐标也不会偏到矩形外面去。矩形面积越小出现概率越低全屏填充时数量最多肉眼很明显。小矩形比如 16x16 的填充有时候完全看不出问题这可能也是很多人最初没当回事的原因。1.2 为什么偏偏是“填充矩形”暴露出问题这个问题最迷惑人的地方在于画点、画线都正常凭什么填充矩形就出乱子关键在于数据量。画一个点SPI 上只需要发送几条命令加两三个字节数据整段时间不到几十微秒传输间隙极短就算 SPI 配置有轻微偏差也很难积累出肉眼可见的错误。画线稍微长一点但每条线最多也就是几百个点数据量依然有限。填充矩形是完全不同的场景。一个 200x200 的矩形RGB565 格式下就是 200x200x2 80000 字节如果是 320x480 全屏一次要往 SPI 灌 307200 字节。这么长的连续数据流任何一字节的解释错误、任何一次时钟采样点偏差、任何一次片选信号异常都会被放大成可见像素错误。SPI 协议本身是同步串行传输发送端和接收端只要有一个 bit 错位后续所有数据全部错位直到下一次命令序列重新同步。所以当时我就意识到屏幕和初始化大概率没坏问题出在“长数据流”的传输机制上也就是 SPI 外设配置、片选控制、以及底层填充函数的实现方式。2. 从硬件开始排除确认 SPI 接口与片选控制2.1 ILI9486 的接口模式比你想的更复杂很多人把 ILI9486 当成一块“SPI 屏”直接用 SPI 外设去驱动。实际上 ILI9486 内部支持多种接口模式包括 I8080 并行接口、3 线 SPI、4 线 SPI型号引脚 IM[3:0] 决定当前用哪种。Waveshare 这种 Shield 板卡硬件上通常有模式选择电阻出厂可能是 I8080 并口模式也可能是 SPI 模式必须看这块板子的原理图或者丝印说明确认。我当时最初犯过一个粗心错误以为 Shield 插上就能用结果白屏了半小时后来发现是 DC 引脚的复用没配好。ILI9486 在 4 线 SPI 模式下DCX 引脚必须由主机 GPIO 控制用来区分当前传输的是命令还是数据。如果 DCX 没接或者被固定拉高/拉低芯片会把所有字节都当成同一种类型命令和数据全混在一起。画点和画线时由于命令较短偶然能“碰对”但填充矩形时命令和数据字节数量不对等必然错乱。另外SKU13587 这类 Shield 上还带 SD 卡槽SD 卡也是走 SPI 的。如果 SD 卡和 LCD 共用一条 SPI 总线必须各自独立 CS。很多代码在初始化阶段会先探测 SD 卡占住 SPI 总线一段时间如果 CS 控制没做好SD 卡的通信垃圾数据会被 LCD 误收屏幕上也会出现随机点。2.2 每根线都值得重查一遍在排查这个问题时我把接线重新整理了一遍这里给出一份参考接线表不一定适用于所有开发板但作为 STM32F767ZI SPI1 的典型接法没问题信号STM32 引脚说明CLKPA5 / SPI1_SCK接到屏幕 SCL线尽量短MOSIPA7 / SPI1_MOSI接到屏幕 SDI/SDAMISO可不接ILI9486 的 SDO 只用于读显存不用可以不连CS任意 GPIO例如 PB0软件控制后续会重点讲DC任意 GPIO例如 PB1命令/数据选择RST任意 GPIO例如 PB2复位引脚这里特别提醒一点MISO 如果没用到不要在主机的 SPI 配置里强制开启读功能。STM32F767 的 SPI 外设如果是全双工模式发送每个字节的同时都会在 MISO 上采样。MISO 悬空时采到的电平是随机的虽然大多不会影响输出数据但如果你开启了 SPI 接收中断或者 DMA 双缓冲这些随机垃圾数据会被 CPU/DMA 处理反而干扰主流程。对只写屏的场景建议直接用 SPI 发送专用配置或者把 MISO 引脚在 GPIO 初始化时配置为普通输入并下拉。还有一个硬件层面的检查点供电。4 寸 TFT 背光电流很容易到几十毫安如果从开发板的 3.3V 排针直接取电当背光占空比变化或屏幕刷新大块面积时电源纹波会增大进而干扰 SPI 电平阈值。排障时最好用稳压源或单独的 LDO 给屏幕单独供电排除电源因素。2.3 硬件片选和软件片选随机像素能否消失的分水岭说到 SPI 片选很多人会直接想到 STM32 的 NSS 引脚。NSS 可以作为硬件片选自动控制但这个“自动控制”在主机模式下并不总符合预期。特别是当你使用 HAL 的HAL_SPI_Transmit时如果你把 NSS 配成了硬件模式外设会在某些帧边界翻转 NSS或在多字节传输过程中产生额外的电平变化。对于 ILI9486 这种对命令序列完整性有要求的芯片CS 在命令中间被拉高是灾难性的。记住一个结论驱动 ILI9486 这类需要连续多字节传输的 SPI 屏时推荐使用 GPIO 软件片选不要使用硬件 NSS。软件片选的核心原则是一个完整事务内CS 保持低电平不动事务结束后才拉高。比如发送“设置窗口命令 0x2A 四个参数”CS 必须在这五个字节全部发送完毕后再释放。下面给一个参考实现void ili9486_write_command(uint8_t cmd) { CS_LOW(); DC_LOW(); // 命令 spi_write_byte(cmd); CS_HIGH(); } void ili9486_write_data(uint8_t data) { CS_LOW(); DC_HIGH(); // 数据 spi_write_byte(data); CS_HIGH(); }看起来更细的拆分就是每个字节一个事务。这样在画点时没有问题但大填充时一定要演变成行缓冲模式不要让 CS 在像素之间反复翻转。后面第 4 章会专门讲 fillRect 的实现方式。3. SPI 参数与 ILI9486 时序的匹配时钟频率和采样边沿3.1 检查 CPOL/CPHA别让采样边沿卡在悬崖上SPI 协议有四种模式由时钟极性 CPOL 和时钟相位 CPHA 决定。ILI9486 的 4 线 SPI 通常工作在 Mode 0也就是 CPOL0、CPHA0SCK 空闲低电平数据在上升沿采样。如果你的初始化配置成 Mode 1、Mode 2 甚至 Mode 3屏幕可能也能亮因为芯片时序容忍度有一定余量低速时尤其不明显。但问题恰恰出在“低速时正常高速时随机错”上。当 SPI 时钟拉到 20MHz 以上SCK 的建立时间和保持时间余量都很小如果 CPOL/CPHA 和芯片要求不一致采样点可能落在数据线翻转的附近。这时候数据线上的毛刺、走线串扰、电平上升沿不陡都会导致某一 bit 被采错。一个 bit 采错反映到屏幕上就是一个颜色错误的随机像素点。在 CubeMX 里检查 SPI 配置时重点确认这几个参数Clock Polarity (CPOL)LowClock Phase (CPHA)1 EdgeData Size8 bitFirst BitMSB First如果排查过程中不确定可以先按 Mode 0 跑最低频确认现象有没有变化。如果 Mode 0 低频稳定再逐步升频。3.2 分频降频是百试百灵的定位手段SPI 时钟频率不是越高越好尤其当你用杜邦线连接屏幕时。F767 的 SPI1 挂在 APB2 上PCLK2 理论最高 108MHzSPI 外设分频最小 2 就是 54MHz。但实际上 ILI9486 的 SPI 时钟上限通常在 20MHz 左右Waveshare 官方 Arduino 例程一般也用 15~20MHz。超过这个范围就算协议没错信号完整性也会出问题。信号完整性出问题时具体表现就是长线传输下出现偶发错位。我最初用 27MHz随机像素很多降到 13.5MHz随机像素明显变少但仍有再配合其他修复后回到 16MHz 才彻底稳定。这个过程说明两点时钟频率是诱发因素但不是根因。给你一个实用的分频对照表方便你在 CubeMX 里做估算。假设 PCLK2 108MHzPrescalerSPI 时钟254 MHz427 MHz813.5 MHz166.75 MHz323.375 MHz排障时建议从最低频率开始比如 6.75MHz确认屏幕显示一切正常后再往上升频。如果 6.75MHz 也有随机像素那就基本可以排除频率因素。3.3 数据位序和像素格式的隐性影响SPI 字节传输还有一个容易被忽略的选项MSB first 还是 LSB first。STM32 的 SPI 外设默认 MSB firstILI9486 的数据手册也是按 MSB 先传来定义的。如果不小心把 SPI 配置成了 LSB first整个数据流按位翻转图像会左右镜像或颜色通道错乱表现上也可能是满屏噪点。所以要在 CubeMX 里把First Bit设为 MSB First。更关键的还是像素格式。ILI9486 本身是 18 位色深也就是 RGB666但 SPI 模式下可以通过 0x3A 寄存器设置输入数据格式0x5516 位/像素RGB5650x6618 位/像素RGB6660x113 位/像素RGB111一般不用于正常显示驱动库和初始化寄存器必须匹配。比如初始化时把 0x3A 设成了 0x66但 fillRect 按 RGB565 每像素 2 字节发送那么从第二个像素开始数据就错位了芯片会把 RGB 分量拆错屏幕上的表现就是密密麻麻的彩色噪点有时会被误认为“随机像素”。我在最后检查初始化序列时确认 0x3A 0x55才排除这个因素。4. ILI9486 初始化序列与显存窗口故障最可能的藏身之处4.1 关键初始化参数怎么调ILI9486 的初始化序列可以从 Waveshare 官方代码或任何成熟驱动库抄但别从 ILI9341 的代码直接改芯片型号就完事。这两颗芯片寄存器差异很大比如 ILI9486 的显示分辨率是 320x480而 ILI9341 是 240x320窗口边界和像素格式的设置完全不同。我见过不少“初始化后花屏”的案例查到最后都是因为用了别的芯片的初始化序列。这里列出几个关键寄存器和它们的作用方便排查时对照寄存器作用常用值0x3A像素格式设置0x55 RGB5650x66 RGB6660x36显存访问控制MADCTL控制扫描方向、RGB/BGR 顺序0xC0电源控制 1跟随官方初始化0xB0显示模式/帧率等跟随官方初始化0x11退出睡眠模式初始化最后需要0x29打开显示初始化完成打开显示特别注意 0x36 MADCTL。如果你的窗口设置函数和 MADCTL 的扫描方向不匹配填充矩形时内容方向会偏但不至于出现随机点。不过 RGB/BGR 位如果搞反颜色通道会错乱在某些颜色过渡区域出现类似噪点的纹理。排障时可以先固定 MADCTL 为 0x00 或 0x48测试纯色矩形是否正常。4.2 窗口设置命令被截断地址指针错乱的源头ILI9486 的显存写入流程是先用 0x2A 设置列地址范围CASET再用 0x2B 设置行地址范围PASET最后用 0x2C 连续写入像素数据。任何一条命令后面的参数如果没收到完整芯片内部的窗口地址就会指向一个错误位置。错误窗口范围内的数据会被写入显存但不一定是当前你要填充的矩形区域。这些写到错误区域的数据最终会在屏幕上表现为显示内容混乱如果只是一小部分越界看起来就是零星的异常像素。窗口命令的格式如下0x2A: [x0_high, x0_low, x1_high, x1_low] // CASET 0x2B: [y0_high, y0_low, y1_high, y1_low] // PASET 0x2C: [pixel data...] // RAMWR所有参数都必须在一个 CS 低电平区间内连续发送。如果发送中间 CS 被拉高或者 SCK 意外停摆ILI9486 只能收到部分参数后续数据就会落到错误地址。这也解释了为什么“CS 拆包”会导致随机像素窗口地址设错了数据还在往显存里写但位置不对。自查方法用逻辑分析仪抓 CS 和 MOSI放大 0x2A 后面一段确认 4 个地址字节是否连续、CS 是否一直维持低电平。一旦发现高脉冲插在参数中间直接定位。4.3 fillRect 实现方式对比逐像素发送是原罪接下来看 fillRect 本身。很多从画点函数扩展来的矩形填充代码长这样void fillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t color) { set_window(x0, y0, x0 w - 1, y0 h - 1); for (uint32_t i 0; i w * h; i) { CS_LOW(); DC_HIGH(); spi_write_byte(color 8); spi_write_byte(color 0xFF); CS_HIGH(); } }这代码从功能上没错画小矩形时看起来也正常。但每次只写一个像素就把 CS 拉高意味着每两个像素之间 CS 都会产生一个高脉冲。对于 ILI9486 来说每个高脉冲都意味着当前 SPI 事务结束芯片状态机回到空闲或命令接收状态。当这个操作发生在 RAMWR 数据流中间时芯片内部可能会认为数据写入结束或者显存地址指针被重置。最终写进去的数据不连续显存里就出现了空洞。正确做法是把窗口设置和像素数据分成两个大事务窗口设置一次性发完像素数据按行缓冲连续发送。推荐这样实现static uint8_t line_buffer[480 * 2]; // 行缓冲按屏幕宽度算 void fillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t color) { uint32_t row_bytes w * 2; // 填满行缓冲 for (uint16_t i 0; i w; i) { line_buffer[i * 2] color 8; line_buffer[i * 2 1] color 0xFF; } set_window(x0, y0, x0 w - 1, y0 h - 1); CS_LOW(); DC_HIGH(); // 数据模式 for (uint16_t row 0; row h; row) { spi_write_buffer(line_buffer, row_bytes); } CS_HIGH(); }这个版本里
返回列表