FEATURED · 精选文章

SPI协议详解:从时序原理到STM32实战与调试技巧

发布时间 / 2026/9/13 22:19:15
来源 / 创域科博编辑部
栏目 / 资讯中心
SPI协议详解:从时序原理到STM32实战与调试技巧 1. 协议概述为什么搞嵌入式离不开SPISPISerial Peripheral Interface串行外设接口是Motolora公司在20世纪80年代定义的一种同步串行通信协议。在嵌入式开发里SPI几乎是和UART、I2C并列的三大串行协议之一但凡你碰过MCU开发大概率都跟它打过照面。它的定位非常清晰短距离、高速率、板级芯片间通信典型场景包括读写Flash存储芯片W25Q64、GD25Q128E这些、驱动LCD/OLED屏幕、连接SD卡、配置传感器寄存器、读写ADC比如AD7124甚至在一些工业现场SPI还被用来扩展CAN控制器也就是所谓SPI转CAN的方案。SPI这个协议的设计思路并不复杂一对主从设备之间用一根时钟线同步两根数据线各走各的方向再加一根片选线决定现在跟谁说话。相比UART需要双方约定好波特率、起始位停止位这些繁琐参数SPI的同步机制让它天然可以跑很高的速率普通STM32的SPI外设随手就能跑到18MHz甚至更高FPGA里实现一个SPI主站也只要几十行状态机代码。但也正因为设计简单到朴素很多细节一旦处理不好就会出现通信时好时坏数据错位一位接上逻辑分析仪就好了这类玄学问题。这篇文章我会从协议本身的工作原理讲起把时序图、四种模式、片选管理这些基础概念拆开揉碎然后结合我实际做过的STM32 W25Q64的读写案例以及软件模拟SPI的实现方法把从原理到落地的完整链路走一遍。适合刚接触嵌入式通信的初学者也适合那些用SPI用了一阵子但一直没把时序彻底搞明白的开发者。看完你应该能独立完成一次SPI通信的开发调试也能自己排查大部分SPI通信故障。2. 从物理层到数据链路SPI的通信机制拆解2.1 四根线的分工与主从拓扑SPI总线只需要四根信号线这是它普及度高的一个重要原因。我们逐个说清楚SCLKSerial Clock串行时钟由主设备产生负责给整个通信过程打拍子。SCLK的每一个上升沿或下降沿双方各采样一位数据。时钟频率直接决定了通信速率比如把SCLK配到10MHz意味着每秒传1000万位也就是约1.25MB/s的有效数据率。MOSIMaster Out Slave In主设备输出、从设备输入。数据方向永远是主到从。MISOMaster In Slave Out从设备输出、主设备输入。数据方向永远是从到主。CS/SSChip Select / Slave Select片选主设备控制低电平有效。CS拉低表示选中你了准备通信CS拉高表示通信结束你休息去吧。CS是SPI里最容易被忽视、但恰恰是最关键的一根线稍后我会专门展开讲。拓扑上SPI是主从架构一个SPI总线上只能有一个主设备通常就是MCU但可以挂多个从设备。多从机有两种接法第一种是每个从设备各用一根独立的CS线主设备通过拉低对应的CS来选择通信对象这种叫独立片选也是最常用的方式第二种是菊花链拓扑所有设备共用数据线从设备的输出接到下一个从设备的输入形成一个链主设备只需要一根CS把所有设备同时选中数据在链上依次移位传播这种方式适合多个同类设备级联的场景但实现起来要小心数据对齐的问题。2.2 同时收发的全双工移位寄存器机制SPI为什么叫Serial Peripheral Interface设计上为什么可以同时发送和接收这就要说到它的核心构件——移位寄存器。主设备和从设备内部各有一个8位或更多位的移位寄存器这两个移位寄存器和MOSI、MISO两根线实际上组成了一个环形结构。想象两排并排的8个存储单元主设备把要发送的数据加载进自己的移位寄存器。CS拉低SCLK开始打拍。每一个时钟脉冲到来时主设备移位寄存器最高位的数据通过MOSI送到从设备同时从设备移位寄存器最高位的数据通过MISO送到主设备。两边各自左移一位腾出位置接收对方刚送来的那一位。8个时钟脉冲后主设备发完8位也同时收完了8位。这就是全双工的由来发送和接收在物理上就是同一个动作的两个面。你写一个字节进SPI数据寄存器硬件会自动完成8拍的数据交换然后你在接收寄存器里读到的就是从设备在这8拍里回传的那个字节。哪怕你只想给从设备发命令不关心返回值你也得把收到的字节读出来否则数据会溢出反过来你想读从设备的数据也必须要发点什么作为时钟的载体哪怕发的是0x00或0xFF。这个机制解释了为什么SPI协议里不存在只读或只写的通信——本质上是每一拍都在双向交换只是有时候某一方的数据无意义罢了。2.3 时钟极性与相位SPI四种工作模式的本质SPI最劝退新手的一个概念就是CPOLClock Polarity时钟极性和CPHAClock Phase时钟相位。好多人在配置SPI的时候对着Mode 0、Mode 1、Mode 2、Mode 3一头雾水因为绝大多数芯片手册上不画时序图你根本看不出区别。先说CPOL。它决定的是SCLK空闲时的电平状态CPOL 0SCLK空闲时是低电平。CPOL 1SCLK空闲时是高电平。再说CPHA。它决定的是数据在SCLK的哪个边沿被采样CPHA 0在SCLK的第一个边沿采样数据如果是CPOL0就是第一个上升沿如果是CPOL1就是第一个下降沿。CPHA 1在SCLK的第二个边沿采样数据。两个参数自由组合就成了四种模式模式CPOLCPHA空闲电平数据采样边沿数据变化边沿Mode 000低上升沿下降沿Mode 101低下降沿上升沿Mode 210高下降沿上升沿Mode 311高上升沿下降沿实践里绝大多数传感器和存储芯片都默认支持Mode 0CPOL0、CPHA0这也是我最常用的模式。但凡事有例外比如某些温湿度传感器、特定的ADC芯片可能默认就是Mode 3比如我调试过的某款外部ADC就是典型的Mode 3器件。配置之前务必去翻一下目标芯片的数据手册找到时序图里标注的SCLK空闲电平和采样边沿按图配置。硬件SPI模式下主从设备的模式必须完全一致否则数据会整体错位一位甚至更多表现出来就是收到的数据全是乱的但波形看起来好像又有规律。3. 片选管理的学问硬件片选与软件片选的取舍SPI的CS线看似简单实际玩起来门道不少。2024年前后在嵌入式社区里关于硬件片选与软件片选的讨论特别多很多人在STM32上踩了坑才明白两者差别。我在这里把概念和我的实践经验一次讲清楚。3.1 硬件片选Hardware NSS的工作方式所谓硬件片选是指MCU的SPI外设自带一个NSS引脚由外设硬件自动控制它的电平。当SPI外设被使能并向发送缓冲区写入数据时硬件会自动把NSS拉低当传输完成、发送缓冲区空了硬件自动把NSS拉高。听起来很省事但在实际使用中硬件NSS有几个让人头疼的问题NSS引脚的复用冲突STM32的SPI外设NSS引脚往往和别的功能复用。如果你用的是固定的硬件NSS引脚这脚可能还要接外部上拉电阻来处理多主机冲突检测这在单体主从通信里完全可以省掉。传输间隙不受控硬件NSS的拉高时机是发送缓冲区为空但SPI在高速传输时如果你连续写入多个数据硬件NSS可能在整个传输序列的字节间隙出现毛刺式的短暂拉高这对某些严格要求CS在整个操作期间保持低电平的从设备来说就是灾难。比如操作W25Q64做页编程或连续读时CS必须全程保持低电平中间被硬件拉高哪怕一瞬状态机就复位了通信直接失败。多从机切换复杂挂多个从设备时硬件NSS只有一根还是得靠GPIO软件去控制其他从设备的CS。所以我的结论是在绝大多数MCU应用场景里不要依赖硬件NSS直接把CS配置成普通GPIO用软件控制拉高拉低。这才是真正的软件片选。3.2 软件片选的优势与实现软件片选就是在主设备侧把每一根CS线接到一个独立的GPIO上通信时手动拉低通信结束手动拉高。为什么这种土办法反而更可靠时序完全可控你可以在拉低CS后插入延时等从设备内部状态稳定后再启动时钟也可以在最后一个字节传输结束后等SCLK完全停止再拉高CS。该稳的时候稳该快的时候快。兼容性极强GPIO控制CS是通吃逻辑不管MCU是哪家、SPI外设是哪代代码移植基本只改引脚编号就行。多从机天然友好你有几个从设备就配几个GPIO想选谁就拉谁完全不会有中断冲突。软件片选的一个常见问题是有时候你拉低了CS但CS拉低和SCLK启动之间的建立时间不够个别时序要求严苛的从设备可能不认。解决办法就是主动加一个小的延时比如拉低CS后延时1到10微秒而这个延时的量级要根据从设备的时序参数表来定大部分器件手册会在CS setup time这一栏给出最小要求值。3.3 一个典型的CS控制时序我在实际项目里操作Flash芯片的读操作通常是这个顺序1. 拉低CSGPIO写0 2. 延时 1~5 us确保CS建立从设备就绪 3. 发送读命令字节0x03 4. 发送24位地址高字节在前 5. 持续发送任意字节如0x00同时读取MISO返回的数据 6. 所有数据读完后先确保SCLK无剩余边沿 7. 拉高CS如果你的MCU用的是硬件SPI外设步骤3到5就是连续往SPI数据寄存器里写字节同时每次写入后读取接收寄存器。步骤7一定要在所有数据都收完之后再做。很多人第一次写Flash驱动时喜欢在循环里发一个、读一个、拉高CS结果读出来的数据总是缺最后的几个字节其实就是CS拉高太早了。注意CS不是简单通信前拉低、通信后拉高就完事了。对于多字节操作CS必须精确地包裹整个操作序列——早了不行从设备会认为操作被中止晚了也不行CS和SCLK空闲期间的电平状态从设备可能误判。最好的习惯是在一段完整的通信前后各放置一次CS操作中间不要碰CS。4. 软件模拟SPI不依赖外设的备用方案在某些场景下你不得不用GPIO去模拟SPI时序MCU的硬件SPI外设被其他功能占用、引脚不够只能复用普通IO、或者你用的芯片压根没有SPI外设比如某些便宜的8位单片机。软件模拟SPI听起来原始但它有一个无可替代的优势引脚任意、模式任意、时序可控粒度极细。我在香橙派Zero3上用Linux的GPIO子系统做软件SPI驱动一个OLED屏也在51上写过SPI库文件搞定过一个射频模块事实证明只要时序写得对软件SPI在低速场景下完全够用。4.1 软件SPI的核心框架软件模拟SPI本质就是用延时代替硬件时钟。我用一段C语言伪代码来说明这个框架这段代码几乎可以套到任何MCU上只需替换底层的GPIO读写函数#define SPI_CS_LOW() gpio_write(CS_PIN, 0) #define SPI_CS_HIGH() gpio_write(CS_PIN, 1) #define SPI_SCLK_LOW() gpio_write(SCLK_PIN, 0) #define SPI_SCLK_HIGH() gpio_write(SCLK_PIN, 1) #define SPI_MOSI_HIGH() gpio_write(MOSI_PIN, 1) #define SPI_MOSI_LOW() gpio_write(MOSI_PIN, 0) #define SPI_MISO_READ() gpio_read(MISO_PIN) static void spi_delay(void) { // 延时具体时长取决于目标速率通常1us左右 // 示例在72MHz主频下空转几十个周期可达到1us volatile uint32_t i 20; while (i--) __NOP(); } // 写一个字节同时读回一个字节Mode 0 示例 uint8_t spi_write_read_byte(uint8_t tx_data) { uint8_t rx_data 0; for (int bit 7; bit 0; bit--) { // 先设置MOSI数据在SCLK边沿到来之前稳定 if (tx_data (1 bit)) SPI_MOSI_HIGH(); else SPI_MOSI_LOW(); spi_delay(); // Mode 0SCLK上升沿从设备采样MOSI主设备采样MISO SPI_SCLK_HIGH(); spi_delay(); // 读取MISO在上升沿之后的稳定期 if (SPI_MISO_READ()) rx_data | (1 bit); // 拉低SCLK完成一个时钟周期 SPI_SCLK_LOW(); spi_delay(); } return rx_data; }需要注意上面的代码是Mode 0CPOL0CPHA0的实现特点是空闲时钟为低数据在上升沿被采样。如果你要实现Mode 1、2、3只需要调整SCLK的空闲电平和数据变化/采样的先后顺序逻辑完全一样。4.2 软件SPI的速率控制技巧软件SPI的速率主要由spi_delay()决定。同一个函数里延时太短可能导致从设备跟不上总线延时太长则通信速率太低比如你要刷一个分辨率稍高的LCD屏每像素16位色一帧数据几千字节要是软件SPI只有100kHz刷新率会卡到肉眼可见。所以软件SPI的关键就是找到延时函数的平衡点。我的经验是先去读从设备手册里最小的SCLK高电平时间SCLK high time和低电平时间SCLK low time把spi_delay设计成至少满足这两个参数的大小。比如某芯片要求的SCLK最小时钟周期是100ns你只要保证每个半周期大于等于50ns即可。然后在逻辑分析仪上实测逐渐缩短延时直到通信出现误码再往回退一点留出20%~30%的余量——这就是这个从设备在当前布线条件下能跑的极限速率。软件SPI还有个小技巧如果你的GPIO库足够快可以把循环里反复判断的模式选择拆成多个独立函数比如spi_write_read_byte_mode0、spi_write_read_byte_mode3避免每次都在循环里判断CPOL/CPHA能省下不少CPU周期。4.3 软件SPI的局限与适用边界软件SPI也不是万能的。它最大的问题是占用CPU因为每个时钟周期都需要CPU参与翻转IO和延时在高速率下几乎把CPU跑满。如果系统里还有别的任务比如RTOS里的多个线程、ADC采样、按键扫描软件SPI的阻塞特性很可能拖垮实时性。所以我的建议是通信速率要求低于2MHz、数据量不大、CPU有余力时用软件SPI没问题。需要高速率、大数据量比如刷屏、连续读Flash、或者有多任务并发需求时老老实实上硬件SPI外设加DMA这才是正路。软件模拟SPI也非常适合临时调试——比如怀疑硬件SPI外设配置有问题可以用软件SPI做对照实验。5. 基于STM32 CubeMX HAL库的硬件SPI实战读写W25Q64STM32是目前学习SPI最主流的平台网上关于stm32怎么做spicubemx spi怎么配置这类问题一抓一大把。这里我以经典的STM32F103 W25Q64华邦的SPI NOR Flash容量8Mbit也就是1MB为例从CubeMX配置到HAL库函数调用完整走一遍读写流程。这套流程同样适用于GD25Q128E等同类SPI Flash命令集基本兼容。5.1 CubeMX配置要点在STM32CubeMX里选中一个SPI外设后需要配置以下几个关键参数Mode选择Full-Duplex Master全双工主模式。如果只是单向控制OLED屏也可以选Transmit Only Master只发送主模式但做Flash读写必须用全双工。Hardware NSS Signal选Disable或用软件管理原因前面已经分析过——我们自己在GPIO上控制CS。这里直接把NSS引脚设成普通GPIO输出。Parameter SettingsData Size8位对W25Q64是8位个别器件支持16位First BitMSB First大多数SPI器件默认高位先出Prescaler分频系数要保证SPI时钟不超过从设备允许的最大值。W25Q64的读时钟上限是50MHzSTM32F103的PCLK2一般是72MHz所以选2分频得到36MHz也在范围内但为了稳定性我通常选择4分频18MHz或者8分频9MHz。CPOL、CPHAW25Q64支持Mode 0和Mode 3这里用Mode 0。GPIO设置SCLK、MOSI、MISO配置为复用推挽输出/输入CS配置为普通推挽输出初始电平设为High。CubeMX最方便的地方是配置完它会自动生成MX_SPI1_Init()等初始化代码HAL库的SPI驱动函数也封装好了你不需要去操作复杂的寄存器位。5.2 HAL库读写函数的正确使用方式HAL库给SPI提供了三组核心API阻塞式HAL_SPI_Transmit、HAL_SPI_Receive、HAL_SPI_TransmitReceive、中断式带_IT后缀和DMA式带_DMA后缀。这里我用阻塞式来讲因为它最容易理解和调试。读W25Q64芯片IDJEDEC ID的典型流程uint8_t spi_flash_read_id(void) { uint8_t cmd 0x9F; // JEDEC ID 读命令 uint8_t id[3] {0}; CS_LOW(); // 拉低片选 HAL_SPI_Transmit(hspi1, cmd, 1, 100); // 发送命令 HAL_SPI_Receive(hspi1, id, 3, 100); // 读回3字节ID CS_HIGH(); // 拉高片选 // W25Q64 的 ID 应为 EF 40 17 return (id[0] 0xEF id[1] 0x40) ? id[2] : 0; }这里有个细节需要注意在HAL库中HAL_SPI_Receive并不是接收它本质上是发0x00的同时接收因为SPI是双工的时钟必须由主设备产生即使主设备只想收数据也必须发送字节去产生时钟。所以上面的代码里HAL_SPI_Receive内部其实在MISO上读数据的同时持续在MOSI上输出0x00。 如果你希望读数据期间MOSI线输出0xFF某些从设备需要或者保持特殊的电平就得用HAL_SPI_TransmitReceive自己指定发送内容例如uint8_t dummy 0xFF; uint8_t rx_buffer[3] {0}; HAL_SPI_TransmitReceive(hspi1, dummy, rx_buffer, 3, 100);再举一个写数据的例子对W25Q64写入一页数据之前必须先发送写使能命令0x06然后拉低CS发送页编程命令0x02 3字节地址 最多256字节数据最后拉高CS再等待芯片内部完成编程轮询读状态寄存器判断忙标志位是否为0。代码骨架如下// 写使能 uint8_t cmd 0x06; CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); CS_HIGH(); // 页编程 uint8_t buf[4 256]; buf[0] 0x02; // Page Program 命令 buf[1] (addr 16) 0xFF; // 地址高字节 buf[2] (addr 8) 0xFF; // 地址中字节 buf[3] addr 0xFF; // 地址低字节 memcpy(buf[4], data, len); CS_LOW(); HAL_SPI_Transmit(hspi1, buf, 4 len, 1000); CS_HIGH(); // 等待内部写操作完成读状态寄存器0x05检查bit0 uint8_t status_cmd 0x05; uint8_t status 0x01; while (status 0x01) { CS_LOW(); HAL_SPI_Transmit(hspi1, status_cmd, 1, 100); HAL_SPI_Receive(hspi1, status, 1, 100); CS_HIGH(); }5.3 一个常见的坑HAL_SPI_TransmitReceive同时收发时的数据错位很多人在使用HAL_SPI_TransmitReceive时总觉得发送一个字节、同时接收一个字节那么rx_buffer[0]就对应tx_buffer[0]。这个理解基本正确但要小心的是HAL库的阻塞式TransmitReceive在连续传输多字节时因为内部有延迟补偿处理rx数据对齐通常没有问题但如果你在中断服务函数或回调里混用了Transmit和Receive就可能出现错位——读回来的第一个字节其实是发送第一个字节之前的垃圾字节。遇到这种情况我的排查方法是在发送实际命令之前先进行一轮预发送/预接收比如连续发几个0x00、丢掉收到的数据把总线状态理顺再开始正式的传输序列。另外检查SCLK是否有多余的边沿CS拉高前后的电平变化是否干净。6. SPI、I2C、UART怎么选三种串行协议的横向对比很多新手对着USART、UART、I2C、SPI区别这类问题一头雾水。我把这三种最常见的板级通信协议放在一起从工程选型的角度做个对比。特性SPII2CUART/UART信号线数量4根SCLK、MOSI、MISO、CS2根SCL、SDA2根TX、RX通信方式同步全双工同步半双工异步全双工时钟来源主设备提供主设备提供双方各自约定波特率最大速率几十MHz级别标准模式100kHz快速模式400kHz高速模式3.4MHz典型9600bps到数Mbps多设备支持通过CS片选支持多从机通过设备地址支持多从机点对点为主协议复杂度低无帧格式、无应答中有地址、有应答、有起始停止位低但有起始位、停止位、校验位典型应用Flash、LCD、SD卡、ADC环境传感器、EEPROM、RTC调试日志、GPS模块、蓝牙模块、工业串口设备工程选型上我的经验是追求速率、传输大量数据、且从设备是存储类或显示类芯片时首选SPI。W25Q64这种Flash如果不靠SPI的速率优势数据吞吐量根本没法看。需要连接多个低速传感器、且引脚紧张时首选I2C。比如挂一堆温湿度、气压传感器I2C用两根线就能搞定地址不同互不干扰。需要和外部设备通信板卡之间、模块之间时用UART更合适因为它最简单也最通用波特率匹配好就能跑支持流控和中断唤醒。如果从设备的接口是固定的比如某些传感器只提供I2C接口那没什么好选的跟随器件走。简单总结成一句话片内、板内高速大流量选SPI片内多设备低速低引脚选I2C板间短距离异步通信选UART。注意这只是一个经验法则实战中要综合看器件的支持情况和系统的带宽占用。7. SPI协议的扩展形态DSPI、QSPI与工程应用标准SPI已经很好用了但工程师们总想要更快、更省引脚于是出现了各种SPI变体。热词里提到的DSPI和QSPI就是其中两种。7.1 DSPI和QSPI的工作原理DSPIDual SPI在标准SPI基础上把MOSI和MISO在部分阶段变为双向数据线使数据能同时从两根线上传输。常见于SPI NOR Flash的双输出快读模式读速度提升一倍。QSPIQuad SPI更进一步把MOSI、MISO以及额外的两根IO线通常叫IO2、IO3全部用于数据传输一次时钟可传4位数据。所以QSPI读速度是标准SPI的4倍。这也是为什么用QSPI接口的Flash在大数据量读取时吞吐量优势明显。SDR/DDR模式标准的SPI在一个时钟周期传1位称为单倍率SDRQSPI配合DDR双沿采样模式一个时钟周期的高电平采样一次、低电平采样一次速率又能翻倍。在GD25Q128E这类高性能Flash上QPIDDR的快读性能非常可观。实际的工程应用里QSPI最常见的场景是MCU把外部QSPI Flash映射到内存地址空间代码可以直接从这个伪内存里取指执行俗称XIPExecute in Place本地执行。这就把Flash变成了一个只读的代码存储介质配合Bootloader可以做OTA升级很多物联网模块就是这种架构。7.2 一个跨界例子SPI转CAN热词里出现了rk spi转can和xy2-100振镜协议这两个方向本质上是不同的通信协议之间的桥接。SPI转CAN就是在MCU和CAN收发器之间加一个SPI接口的CAN控制器芯片比如MCP2515。因为很多MCU没有CAN外设或者CAN外设数量不够用SPI转CAN就成了一个非常实用的用法主MCU通过SPI向CAN控制器发送命令帧比如发送一帧CAN数据。CAN控制器负责CAN协议的完整处理仲裁、填充、错误检测、应答。主MCU再通过SPI读取CAN控制器的接收缓冲区拿到总线上收到的CAN帧。这种方案的好处是让不具备CAN硬件的MCU也能快速接入CAN网络把底层的位时序交给专门的控制芯片去处理。工程上的难点在于SPI通信协议的封装——发送一帧CAN数据需要遵循CAN控制器芯片规定的多字节命令序列比如MCP2515要先用SPI写配置寄存器、再把数据写入发送缓冲区、最后触发发送请求。这中间任何时序不对、地址不对帧就发不出去。7.3 从SPI到更高级协议的学习路径如果你已经掌握了SPI接下来学习CAN、以太网、USB这些更复杂的协议时会发现它们有不少相通的地方都有物理层的电平规范、都有数据链路层的帧格式、都有流控和错误处理机制。SPI是一个极好的入门协议因为它把同步通信、数据帧、时钟同步这些最底层的概念都浓缩在了一个简单的模型里。学习SPI时我建议你手里常备一个逻辑分析仪哪怕是最便宜的8通道24MHz采样率的USB逻辑分析仪也行。测SPI时序的时候把SCLK、MOSI、MISO、CS四根线挂上去抓一段波形跟数据手册的时序图对照着看能少走很多弯路。没有逻辑分析仪之前我调SPI全靠猜效率极低有了逻辑分析仪之后SPI的调试时间从小时级缩短到分钟级。8. SPI常见问题与排查技巧实录SPI通信出问题的时候表象千奇百怪但根因通常就那么几类。我把这些年踩过的坑排除掉纯属写错寄存器的低级错误后提炼成下面这个速查表供大家排查时参考。现象常见原因排查方法读回的数据全为0xFFMISO没接对、从设备没上电、从设备一直处于复位状态、CS没真正选中设备检查接线和电源用万用表量MISO空闲电平确认CS拉低读回的数据全为0x00MOSI没接对、主设备输出的数据从设备没收到、MISO被拉死到地检查MOSI连接示波器看MOSI是否有正常翻转数据错位一位、整体偏移主从设备的CPOL/CPHA设置不一致用逻辑分析仪抓SCLK和数据线的对齐关系重新核对模式高频率正常、低频率乱码SCLK上升沿/下降沿没有避开数据变化时序裕量不足降低时钟频率试一下或者调整CPHA看从设备手册最小建立时间偶发丢字节、时好时坏CS控制时序不对、SPI时钟频率临界、电源纹波干扰用逻辑分析仪长时间抓波形检查CS是否在传输中间被意外拉高上电后第一次通信总是失败MCU和从设备上电时序差异从设备还没就绪在初始化后、正式通信前加上50~100ms延时先把所有从设备的CS拉高DMA传输数据错乱DMA配置的地址递增模式不对、缓冲区大小没对齐检查DMA的方向、数据宽度、内存地址增量设置确保长度与DMA配置一致这里再单独说一个我很想强调的经验SPI调试先看波形再改代码。很多人一上来就怀疑代码里的配置不对于是反复修改寄存器、换HAL库函数最后一查是MISO和MOSI两根线在排针上插反了或者CS接到了别的引脚上。用逻辑分析仪抓一遍波形所有信号线的实际连接问题一目了然。没有分析仪的话至少用万用表确认每根线的连通性和空闲电平。另一个高频坑是从设备的时序限制没有被认真对待。有些传感器要求SCLK频率不能超过1MHz你默认用了10MHz通信失败但你把频率降到1MHz后就好了——这不是玄学就是芯片本身的能力边界。在调试任何一颗不熟悉的SPI器件时第一件事就是打开它的数据手册找到SPI Timing Characteristics或AC Characteristics部分把最高SCLK频率、CS建立时间、CS保持时间这几个关键参数圈出来然后照着配置。最后关于上拉电阻SPI的SCLK、MOSI、MISO、CS四根线在低速短距离板级cm级通信时一般不需要额外上拉。但如果你用的是杜邦线连接、走线比较长或者环境电磁干扰较多建议在SCLK和CS上各加一个10kΩ上拉电阻保持空闲电平稳定。MISO在有多个从设备并联时从设备输出自然是三态的不选中时高阻此时上拉MISO可以让主设备在没选中任何从机时读到确定的高电平便于判断总线空闲状态。在实际项目里我还养成了一个习惯每次新接一个SPI从设备先写一个最简的读ID程序。不管是Flash、传感器还是ADC大多数SPI器件都支持读取ID或状态寄存器之类的命令。如果能稳定读回预期的ID再开始写复杂的读写逻辑如果连ID都读不对那就别急着往下写先把时序和接线问题搞定。这个习惯帮我省了无数排查时间。比如我调试AD7124这类高精度ADC时第一步就是读寄存器确认SPI链路畅通再配置通道和增益。哪怕出问题也基本能定位到是模式不匹配还是接线错误而不会一头扎进算法和数据处理里出不来。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻