ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32驱动MT6835 LED不亮?SPI帧边界与CS锁存时序排查实录

STM32驱动MT6835 LED不亮?SPI帧边界与CS锁存时序排查实录 说实话这次调试一度让我怀疑自己是不是根本不会用 SPI。项目本身不复杂一颗 STM32G031 做主控去驱动一片 MT6835 恒流 LED 驱动芯片三根线——SCLK、MOSI、CS——把一串 RGB 像素灯点起来。按道理这就是最普通的“SPI 从机写数据”的活儿结果我第一次把程序烧进去一个灯珠都不亮。更离谱的是后面整整一个下午我把能查的地方全查了一遍波形、电平、代码逻辑看起来全是对的屏就是不理人。后来终于找到原因才发现这根本不是玄学而是我一开始就把“SPI”这三个字想得太简单了。先说结论MT6835 虽然挂在 SPI 总线风格的三根线上但它不是标准 SPI 从机它内部把 CS 当成“帧锁存信号”数据以固定 bit 数为一帧CS 从低到高的上升沿才是真正生效的时刻。如果只用标准的 HAL_SPI_Transmit 一口气发字节然后拉高 CS大概率会踩到帧长和锁存时序的坑。下面我把整个排查过程完整写出来给做类似类 SPI 芯片驱动的朋友一个参考。1. 项目背景与初版方案一个看似“开箱即用”的硬件 SPI1.1 为什么会选 STM32G031 和 MT6835 这个组合这颗板子的需求是把 8 颗 RGB LED 做成一个小像素屏每颗 LED 需要独立的灰度控制。MT6835 是国产恒流驱动芯片里比较常见的一颗支持级联输出端可以直接挂 LED内部有灰度寄存器主机只需要通过 CLK、DATA、CS 三根线把每颗灯的灰度数据写进去。它对外看起来确实就是 SPI 的样子一个时钟脚、一个数据脚、一个片选脚。主控选 STM32G031 是因为项目对成本敏感、板上空间也小。这颗芯片是 Cortex-M0 内核主频 64MHzFlash 和 RAM 都不大但一个 SPI 外设和一个定时器足够。我在原理图上看到 MT6835 的三根线直接连到 STM32G031 的 SPI1 引脚当时心想这太常规了CubeMX 里配置一下写个发送函数就能跑。1.2 最初版本的代码一切看起来都没毛病CubeMX 里的配置是这样的SPI1 全双工主机模式、8 位数据长度、CPOL 低、CPHA 第一个边沿就是最常用的 Mode 0NSS 选择软件控制预分频 16实际波特率在 4MHz 左右。CS 引脚配成普通 GPIO 推挽输出默认高电平。SPI_HandleTypeDef hspi1; 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_16; HAL_SPI_Init(hspi1);发送三个字节的代码更简单uint8_t tx_data[3] {0x12, 0x34, 0x56}; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, tx_data, 3, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);逻辑上完全通顺CS 拉低发三个字节CS 拉高。SPI 设备不都是这么操作的吗我甚至用串口把 HAL_SPI_Transmit 的返回值打印出来确认返回的是 HAL_OK。但上电之后连第一个灯都没亮。1.3 当时的心态芯片坏了还是我焊接有问题这是我第一次和 MT6835 打交道第一反应是怀疑自己哪里搞错了硬件连接。反复核对原理图确认 SCLK、MOSI、CS 三根线没接反用万用表量供电3.3V 正常地线也通再用示波器看 CS 引脚确实有从高拉低再拉高的动作。一切都符合“程序正在正常运行”的预期但负载端就是毫无反应。这种感受做过硬件调试的人应该都懂越是看起来全对越让人头大。2. 常规排查全部无效“波形对得上”和“芯片认账”是两回事2.1 电气层面的反复检查为了排除接线和电平问题我把整个链路拆开重新查。先用示波器量 STM32G031 的 MOSI 引脚输出高电平稳定在 3.3V低电平接近 0VMT6835 的数字输入阈值在标准 3.3V 逻辑范围里不存在电平不匹配。又查了 CS 引脚是否有上拉电阻、复位电容、电感之类的影响结果板子干净得很没有任何会干扰 SPI 信号的因素。我还怀疑过是不是供电电流不够因为 LED 驱动芯片点亮瞬间可能有个比较大的冲击电流。于是我把电源换成稳压电源直接给 MT6835 供电观察电流表待机电流和点亮后电流都属于正常范围。既然供电没问题、接线没问题剩下的解释只能是代码或者 SPI 外设行为有问题。2.2 换 CPOL 和 CPHA 的四组组合全部试了一遍还是不亮当时我觉得最可疑的就是 SPI 的极性和相位。MT6835 的数据手册里时序图我大致扫了一眼SCLK 空闲时是低电平、数据在上升沿被采样这对应 Mode 0也就是我最初配的那组。但为了排除万一我把 CPOL/CPHA 的四种组合全试了一遍每次配置完重新生成代码、重新烧录结果没有任何一组能让灯亮起来。这个结果其实很有信息量只是当时我没意识到。它说明问题不在于“第几个边沿采样”而在于更上层的帧结构。现在回头看如果当时就拿起逻辑分析仪对比 CS、SCLK、DATA 三根线的时间关系可能能省下一整晚。2.3 把 SPI 速率降到极低芯片依然无动于衷这是另一个经典的“排除变量”做法。我把 SPI 预分频从 16 一路调到 256实际波特率从 4MHz 降到 250kHz然后在 while 循环里反复发同一帧数据发完一次延时 10ms。理论上即使 MT6835 最高时钟频率有限250kHz 也一定在它正常工作的范围之内再怎么说也该有一个灯闪一下让我看到希望。结果依然是什么都没有。当时的挫败感很强因为在嵌入式领域SPI 通信调不通90% 的情况靠降速和换 Mode 就能解决这两种方法都失灵我就开始怀疑自己拿到的是不是一块坏片。我甚至专门换了一颗新的 MT6835 焊上去结果还是老样子。2.4 开始怀疑芯片本体的时候同事一句话点醒了我在我准备放弃硬件 SPI、改去翻芯片手册确认帧格式的时候旁边一位调试经验丰富的同事说了一句特别朴素的话“你既然软件怎么发都不亮干脆先用 GPIO 一根一根地把时序模拟出来看它到底吃什么样的波形。”这句话听起来像退步毕竟放着现成的 SPI 外设不用跑去用 GPIO 模拟位操作总觉得是在开历史的倒车。但后来证明这是整个排查过程中最正确的一步。3. 关键转折GPIO 模拟这一“笨办法”反而把协议跑通了3.1 为什么说 GPIO 模拟能把问题限定在极小的范围内GPIO 模拟的本质是每一根信号线都由程序直接控制每一位数据发送时 CS、SCLK、MOSI 三个引脚处于什么状态程序员一清二楚。它把“HAL 库 SPI 外设做了什么”这个黑盒完全拆掉变成了最直接可见的引脚操作。我把发送函数改成按位操作每一帧固定 16 bitCS 从高拉低后开始逐位移入移完 16 bit 后把 CS 拉高让 MT6835 在 CS 上升沿完成锁存。static void mt6835_delay(void) { for (volatile int i 0; i 20; i); } static void mt6835_write_bit(uint8_t bit) { // SCLK 空闲低电平先放数据再拉高时钟 HAL_GPIO_WritePin(MOSI_GPIO_Port, MOSI_Pin, bit ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(SCLK_GPIO_Port, SCLK_Pin, GPIO_PIN_SET); mt6835_delay(); HAL_GPIO_WritePin(SCLK_GPIO_Port, SCLK_Pin, GPIO_PIN_RESET); mt6835_delay(); } void mt6835_write_frame(uint16_t frame) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); for (int i 15; i 0; i--) { mt6835_write_bit((frame i) 0x01); } // 帧结束CS 拉高触发锁存 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); mt6835_delay(); }3.2 一帧 16 bit 发下去第一个灯珠亮了这个版本写完之后整个 Wi-Fi 模块的运行逻辑没有变我依然是在初始化后调用一个写入函数但这次结果截然不同屏幕上的第一颗 LED 亮起来了虽然亮度很低、颜色也不太对但它确实亮了。那一刻我几乎想拍桌子——同一块板子、同一颗芯片、同样三根线只是把发送方式从硬件 SPI 改成了 GPIO 模拟为什么结果完全不同我静下来对比两个版本最大的差异有两点第一帧长是固定的 16 bit不是随意连续的 8 bit 字节流第二CS 在每发完一帧后就拉高一次且拉高之前 SCLK 已经回到低电平。而最初的硬件 SPI 版本里CS 在整个发送过程中一直处于低电平三个字节发完之后才一次性拉高。3.3 用逻辑分析仪同时抓两种波形差异一目了然我把逻辑分析仪接到 CS、SCLK、MOSI 三根线上分别抓 GPIO 模拟版本和硬件 SPI 版本的波形然后放在一起对比。GPIO 模拟版本非常清爽每 16 个 SCLK 脉冲为一个小组CS 在小组结束后抬升紧接着进入下一个小组。硬件 SPI 版本则是 CS 低电平期间连续出现了 24 个 SCLK 脉冲然后 CS 才抬升且最后一个字节的最后一位和 CS 抬升沿之间的时间关系并不像 GPIO 模拟版本那样“从容”。单看 SCLK 和 MOSI 上的数据位两个版本都是高电平在前、低电平在后完全符合“MSB first”的直觉。这也解释了为什么我最初用示波器看 MOSI 时觉得一切正常——示波器只看单根线的电平跳变看不到 CS 和多根线之间的帧关系。4. 逻辑分析仪抓包CS 才是真正的“锁存信号”4.1 MT6835 手册里关于帧长度的硬性要求把逻辑分析仪抓到的波形和 MT6835 数据手册里的时序放在一起看破绽就出来了。MT6835 的数据输入虽然用了 SCLK、CS、DATA 这三根典型的 SPI 线但它的 CS 并不是普通 SPI 的“片选”概念而是类似于移位寄存器的“锁存”概念。芯片对于一帧数据的 bit 数是有明确要求的超出或不足都会被当作无效帧丢弃。芯片手册里写得很清楚CS 拉低后SCLK 每来一个上升沿DATA 引脚上的电平被移入内部移位寄存器当 CS 从低变高时移位寄存器里的数据被锁存到灰度寄存器控制输出电流。也就是说CS 上升沿才是所有数据真正生效的瞬间。如果 CS 一直处于低电平SCLK 连续脉冲数超过帧长芯片就不知道该在哪一位停止整个帧就会作废。4.2 第一版代码为什么必死一次性发 3 字节等于 24 bit 超长帧第一版代码里的场景是CS 拉低后HAL_SPI_Transmit 连续发送 24 bit3 字节然后 CS 拉高。如果 MT6835 严格按 16 bit 一帧来解析前 16 bit 可以作为一帧但后面多余的 8 bit 会紧跟着进入下一帧而 CS 在第三字节全部结束后才拉高等于把 24 bit 全部当作一帧去解析芯片自然判定帧长非法不会点亮任何输出。这就像一个只能接收 16 位指令的芯片你硬塞给它 24 位它根本不知道你要干什么。问题的根源在于我太习惯“标准 SPI 按字节传、片选只管头尾”的思维忽略了非标准 SPI 芯片对帧边界非常敏感。4.3 为什么改成 2 字节硬件 SPI 还是不行BSY 标志的“时间差”知道了 16 bit 帧长规律之后我立刻把硬件 SPI 版本改成发送两个字节。代码变成了下面这样uint8_t frame_tx[2] {0x12, 0x34}; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, frame_tx, 2, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);我当时觉得这下总该亮了吧结果还是不亮。这就是整件事里最“见鬼”的部分从逻辑分析仪看SPI 输出端确实发出了 16 个 SCLK 脉冲MOSI 上的数据位也完全正确但芯片就是不锁存。后来我仔细看逻辑分析仪抓到的波形发现问题出在 CS 上升沿和最后一个数据位的关系上。HAL_SPI_Transmit 这个函数在数据都写入发送寄存器后就会返回返回时最后一个字节可能还没有完全移出 SPI 移位寄存器。我在函数返回后立刻拉高 CS此时最后一个 bit 还在物理线上“走”芯片的移位寄存器还没有稳定CS 上升沿来得太早把半截数据锁存进去帧内容也就错了。4.4 为什么“函数返回成功”不等于“发送真正完成”这是 STM32 的 HAL 库里特别容易踩坑的一点。HAL_SPI_Transmit 返回 HAL_OK 只代表数据被成功移交给了 SPI 外设不代表 SCLK 已经把所有 bit 都送出去了。SPI 外设内部有发送缓冲区和移位寄存器两级结构最后一个字节在移位寄存器里时发送缓冲区已经空了TXE 标志会置位HAL 库就在这时返回。如果这时候马上操作其他引脚比如把 CS 拉高、或者改变 MOSI 的电平就破坏了 MT6835 在 CS 上升沿锁存时对数据稳定性的要求。正确做法是等待 SPI 的 BSY 标志清零这代表移位寄存器已经彻底送完最后一个 bitSCLK 也回到了空闲状态。5. 最终解决方案等待 BSY 清零之后再动 CS5.1 先修一个“时序洁癖”等 BSY 清零再拉高 CS最终代码其实只改了几行核心逻辑是发送完两字节后先等 BSY 位清零再补一个极短延时让 SCLK 完全回到低电平然后才拉高 CS。CS 高电平保持一段时间确保芯片完成锁存再开始下一帧。void mt6835_write_frame_spi(uint16_t frame) { uint8_t tx_buf[2]; tx_buf[0] (uint8_t)(frame 8); tx_buf[1] (uint8_t)(frame 0xFF); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, tx_buf, 2, 100); // 关键等 SPI 移位寄存器彻底移完不要提前释放 CS while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY) ! RESET); // 给一小段余量确保 SCLK 回落到空闲低电平 delay_us(1); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 帧间 CS 高电平保持时间按手册要求至少给足 delay_us(10); }这个版本烧进去之后灯珠终于开始按照 my 写入的灰度值点亮了。说实话那一刻还专门看了下逻辑分析仪波形里 CS 的上升沿和最后一个 SCLK 下降沿之间已经留出了明显的时间窗口芯片能稳定锁存每一帧数据。5.2 统一接口单一入口底层随便切换GPIO 模拟版本在排查阶段帮了大忙但实际产品里我还是更喜欢用硬件 SPI因为它不占 CPU后续可以配合 DMA 做更复杂的刷新逻辑。为此我封装了一层统一的帧写入接口上层只传 16 bit 灰度帧底层选择 GPIO 模拟还是硬件 SPI 都是实现细节。void mt6835_write_frame(uint16_t frame) { #if defined(MT6835_USE_HW_SPI) mt6835_write_frame_spi(frame); #else mt6835_write_frame_gpio(frame); #endif }这样做的另一个好处是如果以后换一颗帧长不同、或者时序要求有差异的驱动芯片只需要在新芯片驱动里实现同一个 write_frame 函数上层逻辑完全不用动。5.3 为什么不建议直接上硬件 NSS 和 DMA有人可能会问STM32G031 的 SPI 有硬件 NSS 功能能不能把 CS 交给硬件自动控制顺便配合 DMA 发数据理论上可以但实际用起来很别扭。硬件 NSS 在主机模式下的自动拉低/拉高行为按字节或按 SSOE 配置工作很难精确满足“一帧 16 bit 然后自动释放”这种非标准时序尤其当你要连续刷新多颗级联芯片时帧间 CS 高电平时间也未必留得住。DMA 是另一个坑DMA 传输完成中断触发时最后一个字节同样可能还在移位寄存器里依然需要等待 BSY 清零才能操作 CS。如果你在 DMA 中断里立刻拉高 CS会重现我之前的“半截数据锁存”错误。所以我的建议是对这种类 SPI 驱动芯片先用 GPIO 模拟跑通协议再决定要不要用硬件外设不要一上来就开 DMA。5.4 这轮调试中踩过的坑整理成一张速查表问题现象根本原因解决办法上电后芯片完全无反应CS 低电平期间发了 24 bit超过芯片帧长按固定 16 bit 一帧组织数据CS 每帧抬升一次改成 2 字节后还是不亮HAL_SPI_Transmit 返回时最后一个 bit 仍在移位寄存器CS 抬升过早等待 SPI_FLAG_BSY 清零后再操作 CS波形看起来全是好事灯不亮只看了 MOSI 和 SCLK没看 CS 与数据的锁存关系用逻辑分析仪同时抓 CS、SCLK、MOSI 三根线换了 CPOL/CPHA 所有组合都没用问题不在采样边沿而在帧边界先查手册里的 CS 锁存时序再谈 Mode 配置换芯片后故障依旧排除硬件损坏问题在软件发送方式用 GPIO 模拟逐位发送验证物理链路是否正常5.5 性能估算GPIO 模拟到底够不够用在最终方案落定之前我还算过一笔账。芯片主频 64MHzGPIO 模拟一个 bit 包括一次写 MOSI、一次拉高 SCLK、若干条延时指令、一次拉低 SCLK大概需要二十几个 CPU 周期约 400ns 左右。一帧 16 bit 需要 16 个时钟周期也就是 6.4us。一颗灯珠一帧 16 bit8 颗灯珠需要 8 帧约 51us。按 100Hz 刷新率算一帧画面大约要 5msGPIO 模拟完全承受得住。如果做更大的屏再切回硬件 SPI 也不迟。6. 这次调试教会我的 SPI 通用排错思路6.1 别把“类 SPI”芯片当成标准 SPI 从机很多芯片的接口长得像 SPI有 CLK、有 DATA、有 CS数据手册里也写着 SPI 接口但实际时序标准可能完全不同。有的芯片 CS 是锁存脚有的要求 CS 在数据中段就必须拉高有的根本没有标准的片选概念只是引脚的物理顺序和 SPI 相同。拿到一款新芯片第一件事不是打开 CubeMX 配外设而是把时序图里的“帧”这个概念确定下来一帧是多少 bitCS 在什么时刻拉高数据在哪个边沿采样空闲电平是高还是低这些细节每一条都不能想当然。6.2 排查顺序先用 GPIO 模拟再上硬件外设GPIO 模拟的好处在于你从第一行代码开始就在亲手塑造时序任何不合理的帧结构都会被立刻发现。我自己调试时如果一开始就用 GPIO 模拟把 16 bit 一帧发通可能半小时就结束了而不会在硬件 SPI 的黑盒里耗上大半天。现在的习惯是所有非标准 SPI 芯片第一步永远先用 GPIO 模拟把单帧点亮然后再考虑外设。这叫“先证明协议能通再追求传输效率”。6.3 逻辑分析仪要看多根线的时间关系而不是单根线的电平示波器适合看波形形状和电压幅值逻辑分析仪适合看多根数字信号之间的时序关系。调试 SPI 这类同步串行接口时CS、SCLK、DATA 三根线必须放在同一个时间轴上对比。只看 MOSI 的话哪怕数据位完全正确也捕捉不到 CS 抬升过早或过晚这类帧级问题。6.4 关于 HAL 库 API 的一个重要认知返回成功不等于物理发送完成STM32 的 HAL 库封装度高方便的同时也屏蔽了很多底层状态。SPI 这类带移位寄存器的外设发送函数返回时最后一个字节可能还没完全出去。需要操作 CS 或者切换引脚时务必检查 BSY 标志和适当延时。这一点不止适用于 STM32G031任何带 SPI 外设的单片机都有类似的“最后一字节”问题只是 HAL 库把这个坑藏得更深了。6.5 这类问题的本质芯片在等一帧完整数据而你只发了字节流再往深一层想这次调试的“谜底”其实很朴素芯片的世界里只有“帧”这个概念而我的代码里只有“字节”这个概念。标准 SPI 按字节收发字节天然是单片机和外设之间的最小交互单位但 MT6835 这类 LED 驱动芯片内部是移位寄存器加锁存的架构它不关心你发的是几字节它只关心一次 CS 低电平期间进了多少 bit、以及 CS 上升沿到来时这些 bit 是否稳定。两者之间的空档就是所有“见鬼”现象的来源。这次之后我调其他类 SPI 芯片比如 ST7789 这类小屏控制器、AD9833 这类波形发生器时都会先问自己三个问题一帧多少 bitCS 什么时候释放SCLK 空闲电平是什么把这三个问题写下来放在面前再对着逻辑分析仪一条条核对基本不会再被“波形明明都对但就是不出数据”这种事折磨。最后分享一个很小的习惯调试这种芯片时我会先在纸上画出 CS、SCLK、DATA 三根线的预期时序每一帧的 bit 数用竖线标出来然后再写代码。画图的过程看起来多余但在脑子短路的时候这张纸能把你拉回正确的轨道。
返回列表