ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SPI通信详解:CPOL与CPHA时序原理及实战调试技巧

SPI通信详解:CPOL与CPHA时序原理及实战调试技巧 1. SPI到底是个什么东西从一次调不通的屏说起第一次调SPI屏幕或者SPI Flash的时候大概率都遇到过“读ID全是0xFF”或者“屏幕花屏”这种问题。我当时排查到深夜拿示波器一根线一根线点最后发现是CPOL和CPHA没对上。从那次以后我就觉得有必要把SPI从头到尾梳理清楚因为很多开发者其实会用CubeMX点几下配置但真正问起“为什么这么配”往往答不上来。SPI的全称是Serial Peripheral Interface最早由Motorola提出是一种同步串行通信接口。它最讨喜的地方就是“快”和“简单”四根线就能实现全双工通信速率动不动几十MHz在MCU生态里几乎是标配外设。日常开发里我拿它接显示屏、Flash、SD卡、ADC、传感器用得非常频繁。这篇文章会从原理、时序、配置、实战排查一条线讲下来争取让刚接触的人看完能少走弯路也让有经验的兄弟能查漏补缺。1.1 为什么嵌入式里到处都是SPISPI之所以到处都是核心原因是它把“快”做到了极致。对比一下几种常见总线你就能感受到UART靠波特率对齐双方得约定好速率收发任何一边时钟漂移都可能乱码I2C靠SCL时钟同步但受限于开漏结构和上拉电阻速率一般也就几百K到几MSPI则完全不同它由主机主动产生时钟从机被动跟随主机说什么时候采样就什么时候采样所以传输速率可以拉得很高。像QSPI Flash、LCD驱动IC这些高频外设基本都选SPI接口。还有个容易被忽略的优势是SPI全双工。主机发数据的同时还能收数据这在读取传感器寄存器、读写Flash时特别省事。比如读W25Q256这种Flash主机在发送读命令和地址的同时从机已经准备好了要返回的数据一个周期就完成了交换不需要像I2C那样先发地址再切方向收数据效率和代码复杂度都不一样。1.2 一根时钟线撑起的高速公路SPI的主从模型SPI四根线分别是SCLK、MOSI、MISO、CS/SS。主机控制SCLK和CS数据走MOSI主机到从机和MISO从机到主机。连接方式有点像移位寄存器组成的环一次传输是双方向的——主机往MOSI上放一位数据同时从机的MISO也会把它寄存器里的数据送回来。所以项目里我经常说一句话“SPI时序出问题先看时钟极性和相位再看片选信号。”这句话能解决80%的调试问题。剩下的20%可能是硬件电气问题、引脚配置问题或者从机初始化顺序问题后面我会逐个展开。在开始讲时序之前先明确一个概念SPI是主从架构通信永远由主机发起从机永远是被动的。也就是说主机不产生SCLK时钟从机就算有话要说也只能憋着。这个架构决定了SPI非常容易做多从机扩展——每个从机一根CS线主机想跟谁说话就拉低谁的CS其他从机虽然挂在同一条总线上但看到自己的CS是高电平就会老老实实地把输出引脚置成高阻态不干扰总线。2. 时序图背后的四个模式如何一劳永逸搞懂CPOL和CPHA很多人一看SPI时序图就头大满眼的上升沿、下降沿、数据建立时间、保持时间。其实把问题拆开就两个变量时钟极性CPOL和时钟相位CPHA。这两个词翻译成人话就是——“空闲时时钟线是什么电平”和“数据在哪条边沿被采样”。搞懂这两点整个SPI时序图就通了。2.1 CPOL和CPHA到底在决定什么CPOL决定SCLK空闲时的电平CPOL0空闲时SCLK为低电平。CPOL1空闲时SCLK为高电平。CPHA决定数据在哪个边沿采样CPHA0在第一个跳变沿采样数据。CPHA1在第二个跳变沿采样数据。这里最容易绕晕的是“第一个跳变沿”和“第二个跳变沿”取决于CPOL。如果CPOL0空闲是低电平那么第一个跳变沿就是上升沿第二个就是下降沿如果CPOL1空闲是高电平那么第一个跳变沿就是下降沿第二个才是上升沿。所以很多人背“CPHA0用上升沿采样”是不严谨的正确说法是“CPHA0在第一个边沿采样具体是上升还是下降要看CPOL定的空闲电平。”2.2 一张时序图读懂四种工作模式把CPOL和CPHA组合起来就是SPI经典的四种模式模式CPOLCPHA空闲电平采样边沿Mode 000低电平第一个边沿上升沿Mode 101低电平第二个边沿下降沿Mode 210高电平第一个边沿下降沿Mode 311高电平第二个边沿上升沿实际项目中我遇到最多的是Mode 0大多数Flash、屏幕、SD卡默认都支持模式0。其次是Mode 3比如某些传感器和无线芯片会在数据手册里明确写“SPI Mode 3”。Mode 1和Mode 2相对少见但也有所以不能想当然一定要看从机数据手册里的时序图。这里有个实操技巧读数据手册时序图时去找图上有没有标注“First Edge”和“Second Edge”的箭头或者看它写的采样沿是上升还是下降再对照CPOL就能推出来是哪个Mode。如果手册写得含糊直接拿逻辑分析仪抓一把主机发给从机的数据把Clock IDLE电平和数据变化位置对照一下马上就能反推出模式。2.3 选择模式时的判断方法选择工作模式的标准只有一条满足从机的要求而不是主机的喜好。STM32、Arduino、ESP32的SPI外设基本都支持任意配置难点从来不是MCU侧能不能做到而是你有没有看明白从机的时序要求。我的建议是拿到一个新设备先别急着写代码花五分钟把三件事搞清楚SCLK空闲电平是高还是低。数据在上升沿还是下降沿被采样。数据位序是MSB First还是LSB First。第三点经常被忽略很多国产传感器默认MSB但某些寄存器配置芯片用LSB。SPI外设一般都有位序配置项比如STM32的HAL库通过SPI_InitTypeDef里的FirstBit字段设置Arduino的SPI.beginTransaction也可以传LSBFIRST或MSBFIRST。位序搞反的典型现象是读寄存器能通但读出来的值跟手册对不上比如读ID出来是0xB0而不是0x0D这时候把位反过来看就对了。3. 片选这件事软件做和硬件做差别很大片选CS/SS是SPI总线里最不起眼、却最容易埋坑的环节。很多人以为片选就是“通信前拉低、通信后拉高”这么简单但实际工程里软件片选和硬件片选各有各的坑。3.1 软件片选简单但费CPU软件片选就是用一个GPIO通信前手动拉低通信结束后手动拉高。优点是完全可控——你可以在任意时刻决定片选拉低多久也可以在片选拉低期间插入延时非常适合操作W25Q256这类Flash擦除、读状态寄存器都需要片选保持拉低等内部操作完成。比如极路由4增强版这类路由器很多人用SPI编程器直接夹住Flash芯片刷固件本质上就是用软件片选加上一个极慢的SPI时钟把整个Flash内容读出来或者写进去。编程器在这里的角色就是SPI主机而软件片选让它能灵活控制每个命令的时序不会被硬件自动片选逻辑干扰。软件片选的缺点是占CPU。每次通信都要手动控制GPIO尤其是在中断里频繁读写时很容易因为片选操作不够快而拖累整体性能。不过对于绝大多数MCU应用来说这个开销完全可以接受。3.2 硬件片选STM32的NSS与实时性硬件片选就是由SPI外设自动控制NSS引脚通信开始时硬件自动拉低通信结束时硬件自动拉高。它的好处是省CPU但问题在于“通信什么时候结束”不一定符合你的预期。以STM32为例默认的NSS硬件模式下每次传输完一个数据帧NSS会自动拉高。如果你需要连续读写一大块数据比如从Flash连续读出1024字节中途NSS拉高Flash就会认为当前命令结束了后面的数据全部作废。所以很多人的代码“单字节读没问题连续读就乱套”原因就在这。3.3 如何避开硬件NSS自动切换的坑针对这个坑业界有个很实用的做法把NSS引脚配置成普通GPIO输出也就是“手动控制片选”但SPI外设本身还是用硬件模式只是让NSS不受外设自动控制。在STM32的CubeMX里就是选择“Disable”硬件片选然后自己在代码里用GPIO控制片选。这样既保留了硬件SPI的高速传输又能灵活控制片选的拉低拉高时机。另外还要注意一个细节某些从机要求片选拉低后等待一小段时间再开始SCLK比如ST7789这类屏幕IC需要tCSS时间。如果是硬件自动片选这个时序不一定能满足用软件控制GPIO后就可以在拉低CS后加几个空的时钟周期或者微秒级延时很多花屏问题就是这么解决的。我自己的项目里除非是极简单的单字节读写否则基本都用软件片选。这个选择在复杂系统里能省很多事代价只是多占用一个GPIO和一点CPU时间。4. 别再把SPI、I2C、UART搞混三兄弟的定位完全不同嵌入式串行总线里SPI、I2C、UART是出场率最高的三个也是初学者最容易搞混的三个。虽然都叫串行通信但它们的设计哲学和适用场景差别非常大。4.1 一张表看清SPI、I2C、UART的各自强项总线线数时钟来源最高速率常见是否全双工特点SPI4根可复用主机提供SCLK几十MHz是高速、简单、无应答I2C2根SDASCL主机提供SCL几百K~几M否半双工多设备寻址、有ACK、线少UART2根TXRX双方各自时钟115200~数M是异步、适合点对点、不需要时钟线I2C和SPI最容易让人纠结。I2C的优势是只需两根线而且每个设备有地址总线上挂几十个设备都不用额外拉线。但它的速率上限摆在那而且每个字节都有ACK机制对高速大流量传输不友好。SPI虽然要用四根线但速度快得多数据格式也随便你定不需要地址帧、应答帧这些额外开销适合对吞吐量有要求的外设。UART则是完全不同的思路——异步通信没有时钟线靠双方约定波特率。它的优点是最简单成本最低适合调试日志、GPS模块、蓝牙模块这种点对点低速场景。但如果两边波特率稍有偏差长时间传输就会积累错误所以UART不太适合高频大数据量传输。4.2 什么时候死磕SPI什么时候换I2C选型时我一般按这个逻辑判断需要高速刷新SPI屏幕、摄像头、ADC采样率高用SPI。需要多设备且布线紧张传感器仓里挂一堆温湿度、气压、陀螺仪用I2C。只是调试输出或者接串口设备UART。这里特别提醒一句像DHT11这种传感器用的既不是SPI也不是I2C而是单总线协议——一根数据线既当电源又传数据时序全靠延时控制。这类器件虽然便宜但时序非常苛刻调试起来比SPI还折腾选型时要心里有数。4.3 SPI还能变出哪些花样QSPI、Dual SPI、TI模式很多人以为SPI只能一根线发一根线收其实它的扩展形态很多。比如Dual SPI就是MOSI和MISO都用来发数据速率翻倍QSPI更是四根数据线全用来传数据Flash读取速度能提升四倍。W25Q256这种大容量Flash就支持QSPI模式在需要频繁读取固件资源的场景下很实用。还有一个是TI模式也就是德州仪器定义的同步串行格式和Motorola经典SPI时序略有差异主要在DSP领域常见。STM32的SPI外设在CubeMX里可以切换SPI模式和TI模式一般用默认的Motorola模式就行除非你在跟某个DSP对接。我自己选型时还有个原则能用SPI解决的不轻易上并行总线。并行总线速度快但占用的引脚太多布线也麻烦。SPI虽然在四线基础上也能通过软件扩展成“多从机共享总线”只要每个从机单独片选总线上挂七八个设备都没问题关键是片选别选错。5. 从零开始配置一个能用的SPI以STM32 CubeMX为例理论说完了来点实际的。我用STM32来走一遍完整配置流程因为STM32生态成熟、CubeMX生成代码方便很多做嵌入式的兄弟都会用到。其实不管是STM32、ESP32还是Arduino思路都一样时钟极性、时钟相位、位序、速率、片选。5.1 CubeMX关键参数怎么选打开CubeMX选择SPI外设后你会看到一堆配置项核心就这几个Mode选择Transmit Only、Receive Only还是Transmit Receive。大多数场景选Transmit Receive因为SPI是全双工的你总需要一边发一边收。Hardware NSS Signal默认建议选Disable用软件片选前面已经解释过了。Parameter Settings里的Baud Rate Prescaler波特率分频器这个决定SPI速率APB外设时钟除以分频系数。比如APB时钟是80MHz选Prescaler16就是5MHz。CPOL和CPHA根据从机数据手册选大多数设备选Low和1 Edge也就是Mode 0。Data Size一般是8位但有些Flash支持16位传输可以按需选。First BitMSB First或LSB First默认MSB。举个例子我驱动W25Q256时把速率设为10MHz左右W25Q256最高支持104MHz但PCB走线和杜邦线质量会限制实际速率CPOLLowCPHA1 EdgeData Size8BitFirst BitMSB。这个配置能稳定读到JEDEC ID。5.2 手写一个读写函数CubeMX生成的HAL库提供了HAL_SPI_Transmit、HAL_SPI_Receive、HAL_SPI_TransmitReceive这些函数。实际项目里我最常用的是HAL_SPI_TransmitReceive因为一次调用就能同时完成收发效率最高uint8_t spi_read_write(uint8_t tx_data) { uint8_t rx_data 0; HAL_SPI_TransmitReceive(hspi1, tx_data, rx_data, 1, 100); return rx_data; }读Flash JEDEC ID的代码就很简单了uint8_t cmd[4] {0x9F, 0x00, 0x00, 0x00}; // 0x9F: Read JEDEC ID uint8_t resp[4] {0}; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, cmd, resp, 4, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // resp[1]Manufacturer ID, resp[2]Memory Type, resp[3]Capacity这段代码的含义是拉低片选发送读ID命令和三个空字节同时接收四字节数据然后拉高片选。因为SPI全双工的特性发送0x00的同时正好把从机返回的数据收进来。5.3 SPI DMA什么时候值得用SPI DMA适合大数据量连续传输的场景比如往屏幕刷新一整屏图像数据或者从Flash读取几百字节固件。用DMA后CPU只需要设置好源地址、目标地址和长度剩下的搬运全交给DMA控制器CPU可以去做其他事情。但DMA也有坑。最常见的坑是DMA传输完成后数据可能还在FIFO里没完全落盘立刻操作数据会拿到不完整的内容。解决方法是等待DMA传输完成标志位或者使用HAL_SPI_TransmitReceive_DMA配合回调函数。还有一个坑是DMA缓冲区必须是内存地址连续的数组如果你用指针跳来跳去DMA根本搬不对数据。我的建议是少于几十字节的传输直接用轮询就够了上百字节的连续传输再考虑DMA。刻意追求DMA反而会让简单问题复杂化。6. 实操现场的常见坑从花屏到读错ID的完整排查最后这部分是重点中的重点。我把自己和身边同事踩过的坑整理了一遍按现象归类每个都给排查思路和解决办法。6.1 现象一读Flash ID全是0xFF这是SPI调试里最常见的问题。0xFF代表总线一直是高电平也就是MOSI上发的数据从机没收到或者MISO上从机根本没返回数据。排查顺序先确认片选有没有正常工作。用示波器或者万用表量CS引脚通信时有没有拉低。如果CS一直高从机就永远不会应答。再确认SCLK有没有波形。如果SCLK一直是恒定电平说明主机根本没启动传输。然后检查CPOL和CPHA是否匹配。不匹配时从机在错误的时间点采样收到的全是无效数据。检查MOSI和MISO是不是接反了。这个错误非常常见尤其是用杜邦线的时候。检查从机供电。有些Flash芯片供电电压是3.3V你用了5V供电它可能直接不工作。我遇到过最离谱的一次是Flash焊盘虚焊。示波器上波形看起来一切正常但MISO就是没数据最后用放大镜一看Flash的一个引脚根本没焊上。有时候硬件问题比软件问题隐蔽得多信号线全查一遍没问题的话不妨看看焊接。6.2 现象二SPI屏幕花屏或者白屏屏幕花屏尤其是ST7789这类驱动IC多数原因跟速率和初始化时序有关。ST7789的SPI速率极限大概在几十MHz但如果你用了杜邦线连接线长超过10cm高速信号就会严重劣化导致采样错误。我的经验是杜邦线连接时SPI速率不要超过10MHz最好是4MHz~8MHz用PCB走线时再往高拉。白屏则多半是初始化配置没正确执行比如RESET引脚的复位时序不够。很多屏幕驱动要求复位信号拉低至少10ms再拉高然后等120ms再开始发初始化命令。你如果上来就发初始化命令屏幕还没准备好自然白屏。还有一个容易忽略的点屏幕的数据/命令引脚DC必须在每字节发送前设置正确。你是发命令还是发数据完全由DC引脚电平决定。有些人把DC引脚忘了初始化结果整屏画的都是命令。6.3 现象三SPI和SD卡共享总线互相干扰ESP32这类芯片上很多人会把屏幕和SD卡挂同一条SPI总线靠不同片选切换。这个方案本身没问题但实际操作时有个坑SD卡和屏幕的电气特性差异很大SD卡需要较高的时钟速率和严格的上拉电阻屏幕则对时序更敏感。两者共享总线时一个设备的信号反射会影响另一个设备。解决方法有几个在每个设备的CS引脚上加一个上拉电阻到VCC确保未选中时CS稳定为高。在SCLK、MOSI、MISO上串接33Ω左右的电阻抑制信号反射。降低SPI速率找一个两者都能稳定工作的平衡点。如果实在不行就拆成两条独立SPI总线一个SPI外设接屏幕另一个接SD卡。我在一个项目里实测过屏幕刷新率和SD卡读写同时进行时共享总线会出现偶发卡死。最后就是用两个SPI外设分开解决虽然多占用了几个引脚但稳定性提升非常明显。6.4 排查工具与方法速查调试SPI最推荐的工具是逻辑分析仪不需要太贵几十块钱的8通道24MHz采样率版本就够用。抓信号时注意采样率至少是SPI时钟的四倍以上否则波形会失真。排查流程我总结成了一张速查表排查项方法常见结果片选信号示波器/逻辑分析仪看CS跳变CS没拉低GPIO配置错误时钟极性看空闲时SCLK电平空闲电平不对CPOL配错采样边沿看数据变化点和SCLK边沿关系采样边沿不对CPHA配错位序用已知数据对比抓取结果收发反转MSB/LSB反了电气连接万用表量通断不通/短路虚焊、杜邦线松动速率逐步降低分频系数测试高速失败低速成功信号完整性差有一个很容易被忽略的细节总线上所有设备都必须把未使用的引脚设为高阻输入或者推挽输出不允许出现浮空输入。如果某个从机的MISO引脚浮空它会在总线上产生随机电平干扰其他设备通信。所以多从机设计时从机的MISO最好带三态输出能力CS未选中时自动释放总线。最后再分享一个我自己养成的习惯拿到任何一个新SPI设备先把数据手册里时序图截图打印出来在图上标出CPOL、CPHA、片选有效电平、位序、最大速率贴在工位上再写代码。这样看似多花了几分钟实际上每次都能避开大多数低级错误。SPI不是难在原理而是难在细节。一次排查彻底了后面再碰到类似问题就能直接条件反射。
返回列表