STM32G031与MT6835的SPI通信调试:CPOL/CPHA时序匹配问题解析

STM32G031与MT6835的SPI通信调试:CPOL/CPHA时序匹配问题解析 一次“见鬼”的调试STM32G031与MT6835的SPI首次通信之谜搞嵌入式这几年最让人抓狂的不是需求复杂而是那种“代码看着没问题、接线也查了八百遍、示波器上去波形也挺正常可就是通信不上”的玄学现场。我最近就栽了这么一回STM32G031做主控接了一个 MT6835 的 SPI 外设芯片本想着参考例程一改、上电就能跑结果前前后后折腾了一个下午外加大半个晚上最后发现问题居然出在一个特别不起眼的时序细节上。这篇文章想把整个排查过程、中间踩过的坑、以及最后定位问题的方法完整记录下来。如果你也在用 STM32 做 SPI 通信尤其是第一次接触像 MT6835 这种不太常见的外设芯片那这篇内容应该能帮你省掉不少弯路。我尽量把每个“为什么”都讲清楚而不只是告诉你“改成这样就对了”。1. 项目背景与硬件环境1.1 STM32G031 与 MT6835 是谁先介绍一下这次调试里的两个主角。STM32G031 是 ST 家一颗非常典型的入门级 Cortex-M0 内核 MCU。它的主频最高 64MHz片上资源不算夸张但是该有的都有SPI、I2C、USART、ADC、定时器、DMA一个不少。最关键的是价格便宜、功耗低、封装选择多做小批量的物联网节点或者控制板非常合适。我手上这块板子用的是 LQFP32 封装引出来的引脚够用又不占地方。MT6835 可能知道的人不多它是一颗通过 SPI 接口与主控通信的触控/信号采集类芯片常用于触控面板、按键感应这类场景。芯片本身作为 SPI 从机支持标准的 4 线 SPISCK、MOSI、MISO、CS另外还有 INT 中断引脚和 RST 复位引脚。这类芯片的特点是功能不算复杂但它的内部寄存器访问时序、命令帧格式往往有固定的要求不按手册来伺候它它就给你脸色看。1.2 电路连接与电平匹配这套系统的连接方式很常规STM32G031 的 PA5 - MT6835 的 SCKPA7 - MT6835 的 MOSIPA6 - MT6835 的 MISOPA4 - MT6835 的 CS3.3V 供电两边共地这种连接几乎是教科书级别的 SPI 主从接线按理说不应该有任何悬念。但恰恰是这种“太常规了”容易让人第一次排查时直接跳过硬件问题扎进软件里找原因。有一点值得先提一下MT6835 是 3.3V 供电的器件而 G031 的 IO 口在 3.3V 下工作两者的电平标准一致不需要额外的电平转换电路。这个确认过没问题。同时我量过 MT6835 的 VDD 引脚电源纹波大概在 30mV 左右这个范围对数字芯片来说完全可以接受供电也不是问题。1.3 为什么要用硬件 SPI 而不是模拟在设计方案的时候我其实纠结过要不要直接用 GPIO 模拟 SPI。后来还是选了硬件 SPI原因有两个第一硬件 SPI 由外设自动产生时钟和移位操作MCU 内核只需要往数据寄存器里写值就能启动一次传输CPU 占用率低。后续如果要接 DMA硬件 SPI 是天生的好搭档。软件模拟 SPI 虽然引脚自由度更大但每一 bit 都要 CPU 去翻转电平、延时、采样传输大块数据时非常费时。第二STM32G031 的 SPI 外设在这个项目里除了 MT6835后面还打算扩展一个 SPI Flash 作为参数存储。使用硬件 SPI 可以让代码架构更统一驱动层可以抽象成一套接口扩展新器件时只需要给一份寄存器配置表就行。现在回看这个决定本身没毛病问题出在“配置”这件事上——我一开始太信任默认值了。2. 首次上电现象与初步排查2.1 我当时的 CubeMX 配置和代码初稿项目用的是 STM32CubeMX 生成的工程SPI 配置如下ModeFull-Duplex MasterHardware NSS SignalDisable用软件控制 CSData Size8 BitFirst BitMSB FirstBaud Rate Prescaler16也就是 4MHz主频 64MHz 分频CPOLLow / CPHA1 Edge这俩就是后来翻车的根源生成的 SPI 初始化代码是这样/* SPI1 init function */ static void MX_SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKDiv SPI_CLOCKDIV_16; hspi1.Init.CPOL SPI_POLARITY_LOW; hspi1.Init.CPHA SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_16; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; if (HAL_SPI_Init(hspi1) ! HAL_OK) { Error_Handler(); } }然后我写了一个非常简单的读寄存器函数用来读 MT6835 的设备 ID 寄存器uint8_t mt6835_read_reg(uint8_t reg_addr) { uint8_t tx_buf[2]; uint8_t rx_buf[2] {0, 0}; tx_buf[0] 0x80 | reg_addr; // 读命令最高位置1表示读 MT6835_CS_LOW(); HAL_SPI_TransmitReceive(hspi1, tx_buf, rx_buf, 1, 100); HAL_SPI_TransmitReceive(hspi1, tx_buf 1, rx_buf 1, 1, 100); MT6835_CS_HIGH(); return rx_buf[1]; }思路很简单先发读命令再发一个无关字节把从机寄存器里的数据“顶”出来。这是 SPI 读寄存器最常见的软件套路理论上没有任何问题。2.2 见鬼了读回来的全是 0xFF上电后我通过串口打印 MT6835 的设备 ID结果屏幕上刷了一串read dev id: 0xFF read dev id: 0xFF read dev id: 0xFF一开始我以为是芯片没焊好或者供电有问题。但量了 VDD 是 3.3VGND 也通RST 引脚也拉了高。再用万用表量 MOSI、SCK、CS 的波形——这个用万用表量不太准只能测平均电压但也大概能看到有信号在跳。我甚至把 MISO 直接飞线到另一个 GPIO 上用 GPIO 模拟时序去读结果还是 0xFF。当时心里只有一个想法“这不科学。”2.3 排查三板斧引脚、电平、供电遇到这种“代码没问题但结果不对”的情况我的排查顺序基本固定先查硬件连接引脚有没有接反、虚焊、短路。再查电平匹配从机的输入高电平阈值是多少、主机的输出能不能满足。最后查供电供电电压对不对、纹波大不大、有没有掉电。这三样我都查完了结论全是正常的。于是我把目光转向了 SPI 的时序参数和 MT6835 的寄存器访问格式。结果在重读数据手册时我注意到一个被我忽略的细节MT6835 的 SPI 从机在读写寄存器时要求主机发送的时钟极性 CPOL 和相位 CPHA 必须和芯片内部采样逻辑完全一致否则数据线即使在跳变芯片也采不到正确的 bit。那一刻我隐约觉得这次的锅很可能不是“线没接对”而是“时序模式不对”。3. 深入 SPI 时序的本质CPOL 和 CPHA 到底怎么影响通信3.1 用打乒乓球的例子理解 SPI 时序SPI 通信本质上像两个人打乒乓球。SCK 就是裁判的哨子每吹一声哨子双方各自出一个球一个 bit。听起来很简单但这里有两个关键信息不说话的时候空闲时哨子拿在手里是吹还是不吹这对应 CPOL决定 SCK 空闲时的电平是高还是低。吹哨子的瞬间是从“抬手”到“落下”的哪个时刻算数这对应 CPHA决定数据是在第一个边沿被采样还是第二个边沿被采样。CPOL 和 CPHA 组合起来就是 SPI 的四种工作模式模式CPOLCPHA空闲时钟电平数据采样边沿Mode 000低电平上升沿Mode 101低电平下降沿Mode 210高电平下降沿Mode 311高电平上升沿大多数 SPI Flash 和 LCD 屏都支持 Mode 0 和 Mode 3因为这两种模式中数据在时钟边沿的采样窗口比较宽松。但总有芯片很挑食只支持一种模式。MT6835 就是这种“挑食”的孩子。3.2 为什么模式不匹配时通信会“看似正常实则无效”我当时配置的是 Mode 0CPOL0CPHA1 Edge。这意味着 SCK 空闲为低数据在上升沿采样。如果 MT6835 要求的是 Mode 3CPOL1CPHA2 Edge那么 SCL 空闲为高数据在第二个边沿采样。主机以 Mode 0 发数据从机却在等待完全不同的采样时机结果就是主机的 MOSI 线上明明有数据在跳但从机的内部移位寄存器根本采不到正确的电平导致它给 MISO 的回应也完全错位。这里有个特别坑的地方如果只是读错数据、返回 0xFF很多人会怀疑地址错了、命令格式错了、或者芯片坏了很少有人会第一时间想到是 CPOL/CPHA 的问题。因为示波器上看波形SCK 和 MOSI 确实在动而且频率、幅度都对。问题的关键是采样点不对而这种“软性”时序错误用万用表根本测不出来。3.3 怎么精确判断从机要求的模式判断 MT6835 到底要哪种模式有两条路第一查数据手册的时序图。手册里通常会画 SCK 空闲电平是拉高还是拉低、数据在哪个边沿稳定。我手里这份 MT6835 的手册写得比较简单时序图就一张但标明了一句“Data is latched on the rising edge of SCK while SCK idles high.”——这句话翻译过来就是SCK 空闲为高数据在上升沿锁存。上升沿采样对应 CPOL1、CPHA1也就是 Mode 3。第二如果没有手册那就用穷举法。把 CubeMX 里的 CPOL 和 CPHA 四种组合都试一遍每次读相同的寄存器看哪一组能读到非 0xFF 的合理数据。这个方法粗暴但有效特别是在找不到手册或者手册不够准确的情况下。我后来把 CPOL 改成 High、CPHA 改成 2 Edge重新编译下载串口立刻输出了read dev id: 0x31那一刻我终于松了一口气——不是芯片坏了也不是线接错了就是 Mode 0 和 Mode 3 的区别。3.4 用逻辑分析仪看波形确认为了弄清楚整个过程我后来把一台逻辑分析仪挂了上去抓了 Mod e0 和 Mode 3 两种配置下的时序波形。在 Mode 0 下SCK 空闲时确实是低电平数据在第一个上升沿采样。但把 MT6835 内部要求的标准叠加上去就能看出来芯片在 SCK 空闲期就开始检查电平等到第一个边沿到来时MOSI 上的数据其实才刚开始变化芯片采样到的电平处于“不稳定区间”所以读出来的自然全是错值。在 Mode 3 下SCK 空闲为高数据在上升沿采样MOSI 上的数据在 SCK 下降沿之后才更新留给芯片足够长的建立时间。这个波形一对比问题一目了然。从这件事我得到一个教训对于不常见的芯片不要凭经验默认它支持 Mode 0一定要先看时序图或者直接拿逻辑分析仪抓波形对比。4. 解决、验证与扩展从“能通”到“用得稳”4.1 修改后的 CubeMX 配置与代码确认 Mode 3 之后修改其实只有两处CPOLLow - HighCPHA1 Edge - 2 Edge改完初始化函数变成这样static void MX_SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKDiv SPI_CLOCKDIV_16; hspi1.Init.CPOL SPI_POLARITY_HIGH; hspi1.Init.CPHA SPI_PHASE_2EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_16; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; if (HAL_SPI_Init(hspi1) ! HAL_OK) { Error_Handler(); } }就这两行的差别通信结果从 0xFF 变成了 0x31。这再次验证了我前面说的SPI 调不通的时候别光盯着线序和电平CPOL/CPHA 是一票否决项。4.2 从寄存器读到批量数据稳定性测试能读到设备 ID 只是第一步后面还需要通过 SPI 读取触摸坐标数据。MT6835 的坐标寄存器是连续排列的我写了一个批量读取函数void mt6835_read_regs(uint8_t start_reg, uint8_t *buf, uint8_t len) { uint8_t tx_cmd 0x80 | start_reg; MT6835_CS_LOW(); HAL_SPI_TransmitReceive(hspi1, tx_cmd, buf, 1, 100); for (uint8_t i 0; i len; i) { uint8_t dummy 0x00; HAL_SPI_TransmitReceive(hspi1, dummy, buf i, 1, 100); } MT6835_CS_HIGH(); }连续读取 16 字节坐标数据打印出来验证了稳定性和正确性。这里有个小经验批量读的时候读命令发完之后紧跟的第一个字节往往是地址指示或厂商保留字节真正的数据从第二个字节开始有效。具体偏移要看芯片手册我当时试的时候第一个字节固定是 0x00后面的 5 字节坐标才随触摸位置变化。另外我还做了压力测试连续读 1000 次寄存器统计出错率。在 4MHz SCK 下1000 次读操作没有一个字节出错。又把时钟频率往上提到了 8MHz仍然稳定。G031 这颗芯片的 SPI 外设跑到这个速率完全没有压力MT6835 的数据手册标称最高支持 10MHz 左右余量也够。4.3 硬件片选与软件片选的取舍这个项目里CS 用的是普通 GPIO 软件控制。当时也纠结过要不要直接用硬件 NSS后来还是保留了软件片选。原因是MT6835 的 SPI 读操作虽然有固定流程但有时候需要在两次写操作之间把 CS 拉高再拉低以触发芯片内部命令的开始或结束。如果交给硬件 NSS 自动控制它会在每次字节传输结束后自动拉高这就破坏了多字节命令的连贯性。软件片选虽然多占几行代码但胜在完全可控。尤其当你需要把多个 SPI 设备挂在同一条总线上时软件片选几乎是必须的。SPI 协议本身是“一主多从、片选区分”的架构只要你保证同一时刻只有一个 CS 被拉低多个设备共享 SCK/MOSI/MISO 是完全没有问题的。这也是我后续扩展 SPI Flash 的方案基础。4.4 软件模拟 SPI 兜底方案虽然硬件 SPI 跑通了但我还是额外写了一套 GPIO 模拟 SPI 的程序留着备用。不为别的就是在极端情况下比如引脚不够用、必须把 SCK 挪到非 SPI 映射引脚上不至于完全抓瞎。软件模拟的代码不复杂标准 Mode 3 逻辑如下void spi_sw_delay(void) { // 空转几个周期控制 SCK 频率 for (volatile uint8_t i 0; i 5; i); } uint8_t spi_sw_transfer_byte(uint8_t byte) { uint8_t rx_byte 0; for (uint8_t bit 0; bit 8; bit) { // CPOL1空闲时 SCK 为高 // 在下降沿发送数据 SCK_LOW(); spi_sw_delay(); if (byte 0x80) MOSI_HIGH(); else MOSI_LOW(); SCK_HIGH(); spi_sw_delay(); // 在上升沿采样数据 rx_byte 1; if (MISO_READ()) rx_byte | 0x01; byte 1; } return rx_byte; }这段代码的逻辑顺序其实是先拉低 SCK、再放数据、再拉高 SCK、再采样。为啥要先拉低因为 CPOL1 时 SCK 空闲为高下降沿先把时钟拉低数据在这个低电平阶段设置好然后上升沿到来时从机正好采样。时序上用延时函数控制简单但够用。软件模拟的缺点也很明显速度慢、CPU 占用高但作为调试手段和应急方案价值非常大。5. 常见问题与排查技巧实录5.1 SPI 通信故障速查表把这次调试及以往项目中遇到的 SPI 问题汇总一下做成一个速查表。下次再遇到通信不通可以按表逐项排查排查项检查方法常见结果接线万用表量通断确认引脚无虚焊断路、错位、顺序反电平量 VDD、确认从机供电电压电压不足、无共地时钟极性看手册时序图或用逻辑分析仪抓波形CPOL/CPHA 不匹配时钟频率确认是否超过从机最高支持频率高频通信不稳定片选控制检查 CS 拉低时机和持续时间CS 拉高太快、命令被打断命令格式核对寄存器地址位、读写标志位地址错误、缺读写位MISO 虚焊示波器量 MISO 是否跟随主机发送变化从机没回应、MISO 悬空总线冲突确认同一总线其他设备 CS 都拉高多个从机同时响应5.2 我踩过的几个“无声”的坑除了这次的 CPOL/CPHA 之外还有几个类似“看着没问题但其实有问题”的坑值得单独记一笔。第一个是 GPIO 复用配置。G031 的 PA5、PA6、PA7 默认不是 SPI 引脚CubeMX 会帮你配置复用功能但如果你手写寄存器忘了把 AF 寄存器设成 SPI1 的复用号那么即使 SPI 外设初始化成功引脚也不会输出任何时钟和数据。这个问题的隐蔽之处在于GPIO 模式配置成输出了示波器上也能看到电平但完全是静止的。第二个是片选拉低后第一个字节的时序窗口。有些从机要求 CS 拉低后必须等一段时间几 us再发 SCK否则芯片还没进入接收状态。我当时给 MT6835 的代码里加了个 10us 的延时虽然不是必需的但在一些更“挑剔”的芯片上这个小延时能救大命。第三个是从机 MISO 在非通信期间的状态。有些芯片的 MISO 是开漏输出需要外部上拉电阻才能读到有效电平。如果电路板上忘了加上拉MISO 在空闲时就是浮空状态读回的数据可能会出现随机错误。检查方法是在 CS 拉高时量 MISO 的电平如果既不是高也不是低就要考虑加上拉电阻。5.3 调试工具怎么选这次排查帮了大忙的工具按优先级排一下逻辑分析仪价格便宜几十块的就能用能抓完整的 SPI 波形直接看 CPOL/CPHA、数据内容和时序关系。SPI 这种低速协议逻辑分析仪是绝对的第一选择。示波器主要看信号质量和时序边沿。如果你需要确认上升沿是否过缓、信号有没有振铃示波器比逻辑分析仪更直观。串口打印MCU 侧最朴素的调试手段。关键寄存器值、错误码、收发缓冲内容都通过串口打出来能快速缩小问题范围。5.4 关于 IIC 与 SPI 的差异顺带说两句MT6835 只支持 SPI但很多同类触控芯片也提供 I2C 接口。经常会有人问二者到底怎么选。I2C 走的是两根线SDA、SCL地址寻址速度一般在 100kHz~1MHz 之间接线少但协议复杂每次通信都有起始条件、停止条件、从机地址、寄存器地址等一大堆“握手”开销。SPI 走的是四根线SCK、MOSI、MISO、CS全双工、速度可以做到几十 MHz协议简单直接但需要额外片选引脚。实际选型时如果引脚够用、需要高速传大量数据SPI 更合适如果引脚紧张、设备数量多、速度要求不高I2C 更方便。从调试难度上说SPI 比 I2C 友好不少因为全双工模式和数据线分组清晰逻辑分析仪一眼就能看懂。而 I2C 的时序状态机更多遇到死锁或者地址冲突的时候排查成本更高。这次如果 MT6835 同时支持 I2C我说不定会直接选 I2C 来绕开 SPI 的模式问题但这也算是“换个坑踩”——I2C 模式下的上拉电阻、ACK 时序同样有的是细节要处理。6. 把这次经历变成可复用的经验回头看这次“见鬼”的调试其实一点鬼都没有全部问题都出在“默认值思维”上。我默认 MT6835 跟大多数 SPI 芯片一样支持 Mode 0默认 CubeMX 生成代码后引脚和时序都没问题默认读寄存器命令写对了芯片就会给我回应。这三个“默认”每一个都让我离真相更远一步。现在我自己写 SPI 驱动时已经养成了几个固定的习惯分享给你作为参考。第一拿到一颗新的 SPI 外设芯片第一件事不是写代码而是查阅手册里的 SPI 时序图。把空闲电平和采样边沿确认清楚再动 CubeMX 配置。这一步比调试十次都管用。第二写最初的通信测试代码时不要直接跳到业务功能。先固定从机地址或寄存器地址循环读同一个值用串口打印确认读到的数据是否稳定。写入操作也类似先写一个已知值再读回对比两次结果是否一致。这能帮你把“通信链路”和“业务逻辑”彻底解耦出了问题时排查面一下就缩小了。第三保留一个软件模拟 SPI 的调试函数尤其是项目里用到的从机芯片未知数比较多的时候。硬件 SPI 调不通软件模拟未必调不通。如果你用软件模拟按正确的时序能读到设备 ID那就说明从机本身没坏、接线也没问题问题一定在硬件 SPI 外设的配置上。这个反向排除法非常实用。第四最小化系统跑通之后第一时间做稳定性测试。不要以为能读到设备 ID 就等于通信完全正常。批量读取、高频连续读取、发热环境下的长时运行都可能暴露出信号完整性问题或时序裕量不足的问题。最后再说一个小技巧关于怎么避免“从机没应答”和“总线上数据被其他设备干扰”这两类看起来很像的问题。SPI 总线上挂多设备时除了每个从机有独立的 CS 引脚我建议在硬件设计上给每个从机的 MISO 加一个串阻几十欧姆这样不同器件的 MISO 驱动能力差异不容易互相打架。软件上也务必确认任何时刻只有目标从机的 CS 是低电平其它从机的 CS 都保持高。你可以在每次片选切换时打印一下所有 CS 引脚的状态确保没有两个同时为低。这种问题在单从机系统里不出现一旦上多从机就是典型的“资料上看了没用、踩过才知道疼”的坑。这次调试的最后我用逻辑分析仪把 Mode 3 下的完整波形截了个图存到工程文档里标注上“MT6835 必须使用 CPOL1/CPHA2 Edge”防止自己下次再犯同样的默认错误。如果你也正在跟一颗陌生的 SPI 芯片较劲听我一句劝先去手册里翻时序图确认 CPOL 和 CPHA再考虑要不要把示波器探头戳到板子上。很多时候答案早就写在手册里了只是我们太习惯相信自己已有的经验。