尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SPI通信协议深度解析:从CPOL/CPHA到调试避坑实战

SPI通信协议深度解析:从CPOL/CPHA到调试避坑实战 年前帮一个朋友排查一块 SPI 屏的显示问题波形抓出来看SCK 和 MOSI 都正常片选拉低的时间也对可屏幕就是花屏。折腾了一下午最后发现是 CPOL 和 CPHA 的搭配跟屏的时序要求正好反了。那一刻我意识到SPI 这个协议表面上看只有四根线简单得不能再简单但真正把它用明白靠的不是背时序图而是理解它背后那套“主从换位”的机制。这篇文章想写写我对 SPI 通信协议的完整理解从底层机制到工程落地再到实际调试中容易踩的坑。内容会覆盖 SPI 与 I2C、UART 的对比时钟极性和相位的本质硬件片选与软件片选的取舍以及 Flash、屏幕、FPGA 从机这几类常见设备的实战经验。适合刚接触嵌入式通信的新手也适合那些已经会配置 SPI 外设、但遇到问题还是只能靠试的开发者。1. 为什么“会配 SPI”不等于“会用 SPI”先破除三个常见误解SPI 的名字叫 Serial Peripheral Interface串行外设接口由 Motorola 在 1979 年提出。它在嵌入式领域的普及程度不亚于 UART但有意思的是越是看起来简单的东西越容易在细节上翻车。1.1 误解一全双工就是主机发完数据从机再回数据这是很多人对 SPI 全双工的第一个错误认知。UART 的全双工是两条独立的数据线各走各的发送和接收互不干扰但 SPI 的全双工不是“先发后收”而是“一发一收同时进行”。SPI 的 MOSI 和 MISO 两条线在每一个时钟脉冲内同时传输一位数据。主机通过 MOSI 发出数据的同时从机也通过 MISO 把数据传回来。这意味着一次完整的 SPI 传输主机发出的每一位都会从从机那里换回一位数据。理解这一点非常重要因为后面讲 SPI Flash 读操作的时候你会发现读指令发出后主机必须继续产生时钟才能把从机的数据“换”回来。1.2 误解二从机标注的最高速率在任何条件下都能跑满SPI 没有像 UART 那样的波特率误差协商机制也没有像 I2C 那样的时钟拉伸Clock Stretching功能通信速率完全由主机产生的 SCK 决定从机只能被动跟随。所以很多人觉得只要从机手册上写着支持 50MHz我直接配 50MHz 就行。实际上这个“最高速率”往往是有前提条件的比如供电电压、PCB 走线长度、负载电容、甚至温度。实际项目中电源纹波偏大或者飞线太长SPI 速率降到手册标称的四分之一才能稳定工作的情况很常见。我的习惯是新器件第一次调通速率先按手册标称的十分之一配功能正常后再逐步往上升。1.3 误解三片选只是一根“使能线”拉低就行片选CS/SS/NSS在 SPI 通信中的地位比大多数人想象的重要得多。它不仅仅是告诉从机“我要跟你说话”还承担着从机内部状态复位的功能。很多从机芯片在片选拉高之后内部的状态机才会回到空闲态等待下一次通信的开始。如果片选信号处理不好比如拉低的时机不对、拉高的过程中有毛刺、或者时序上比 SCK 慢半拍就会出现极其诡异的问题第一次通信正常第二次数据错位第三次直接无响应。这一类问题在逻辑分析仪上非常难抓因为它不是数据线上的问题而是控制时序上的问题。2. SPI 的底层机制一组移位寄存器把主从双方串成一个环要真正理解 SPI不能只看时序图要看它内部的移位寄存器结构。SPI 的本质是主从双方各有一个移位寄存器这两个寄存器通过 MOSI 和 MISO 首尾相连形成一个环形结构。2.1 四线制的分工逻辑标准的 SPI 有四根线SCLKSCK串行时钟由主机产生决定数据传输的节奏MOSIMaster Out Slave In主机输出、从机输入MISOMaster In Slave Out从机输出、主机输入CS/SS片选信号低电平有效由主机控制有的设备是三线制比如部分温湿度传感器只有 SCLK、DATA 和 CSDATA 是双向的。这种三线制本质上是一种半双工的 SPI 变体比如热搜词里提到的“IO口模拟三线SPI”就是通过控制 GPIO 的方向寄存器在发送和接收之间切换 DATA 线的方向实现半双工通信。2.2 为什么要“一个时钟脉冲交换一位”主机在 SCK 的上升沿或下降沿把 MOSI 上的一位数据移出同时从 MISO 上采样一位数据。与此同时从机在相反的沿上采样 MOSI并把自己的数据移到 MISO 上。这种“主发从收、从发主收”同时进行的机制靠的就是移位寄存器在时钟边沿的触发移位。我的理解方式是SPI 的两个移位寄存器像两个人面对面站着中间有两根管子一根从左往右一根从右往左。每响一次钟声两个人同时把手里的一颗珠子推进管子同时接住对方推过来的珠子。响八次钟两个人手里的八颗珠子就完全交换了。这也是为什么 SPI 一次传输的数据长度通常是 8 位、16 位、或者 32 位——移位寄存器的宽度决定的。STM32 的 SPI 外设支持 4 到 16 位的数据帧格式如果从机需要非标准的位数比如 12 位就需要通过修改数据帧格式来实现而不是简单地多发几个字节。2.3 SPI 与 I2C、UART 的选型边界很多初学者搞不清楚什么场景该用 SPI什么场景该用 I2C什么场景该用 UART。这三者的本质区别在于它们的“同步机制”和“设备寻址方式”。协议同步方式设备寻址数据方向典型速率适用场景UART异步靠波特率对齐点对点无寻址全双工9600bps ~ 数Mbps调试日志、GPS、蓝牙模组I2C同步带时钟拉伸7位/10位地址总线多设备半双工100Kbps ~ 3.4Mbps传感器、EEPROM、低速外设SPI同步主机产生时钟片选线选择无地址全双工可达数十MHzFlash、屏幕、ADC、SD卡I2C 的优势是省引脚两根线就能挂一堆设备但半双工和速率上限决定了它不适合大数据量吞吐。SPI 的优势是速率高、全双工、时序简单直接但代价是每个从机都要占一个片选引脚设备多了引脚压力大。实际项目中我会按“数据量”和“实时性”来选。只是读个温度、湿度这种低频数据I2C 足够如果是刷屏、读写 Flash、采集高速 ADCSPI 是更合理的选择。UART 则更多用于与外部模块通信因为几乎所有无线模组、GPS 模组都原生支持 UART它不需要额外的时钟线接线最简单。3. 时钟极性与相位CPOL/CPHA 四个模式怎么选才不出错SPI 最容易让人懵的就是四种工作模式也就是 CPOLClock Polarity时钟极性和 CPHAClock Phase时钟相位的不同组合。我见过太多人在这上面翻车包括我自己刚开始接触 STM32 的时候也栽过。3.1 CPOL空闲电平是低还是高CPOL 决定的是 SCK 在空闲状态没有数据传输时的电平。CPOL 0空闲时 SCK 为低电平第一个时钟边沿是上升沿CPOL 1空闲时 SCK 为高电平第一个时钟边沿是下降沿这个参数本身不难理解难点在于它和 CPHA 配合起来后决定了数据是在哪个边沿被采样。很多从机手册上的时序图会把 SCK 画成同一种极性但实际工作时用的可能是另一种这点在国产芯片的数据手册上尤其常见需要特别注意。3.2 CPHA数据在哪个边沿被采样CPHA 决定的是数据采样时刻相对于时钟边沿的位置。CPHA 0在第一个时钟边沿即 SCK 从空闲电平跳变的那个边沿采样数据CPHA 1在第二个时钟边沿采样数据把 CPOL 和 CPHA 组合起来就得到了常用的四种模式模式CPOLCPHA采样边沿数据移出边沿Mode 000上升沿下降沿Mode 101下降沿上升沿Mode 210下降沿上升沿Mode 311上升沿下降沿Mode 0 和 Mode 3 是最常见的两种。很多 SPI Flash 和屏幕控制器默认支持 Mode 0 和 Mode 3但部分传感器只支持其中一种。3.3 实战选型建议跟着从机手册走别跟着惯例走这里要强调一个原则SPI 模式的最终依据永远是从机芯片数据手册里的时序图而不是你以为的“大家默认用 Mode 0”。我踩过的一个真实案例某款国产加速度传感器手册上写的时序图看起来是 CPOL0、CPHA0Mode 0但实际测试发现 Mode 0 下读出来的数据偶尔会跳变最后抓波形才发现手册画的采样点位置模棱两可实际支持的是 Mode 1。那次问题定位花了大半天最后还是靠示波器同时抓 SCK 和 MISO比对数据在哪个边沿稳定下来才确定正确答案。如果你手头没有示波器可以用一个土办法把速率降到 100kHz 以下然后依次切换四种模式读取从机的 ID 寄存器。如果某个模式下读到的 ID 是预期值且稳定重复那大概率就是这个模式。这个方法虽然笨但在没有测量设备的时候非常实用。4. 片选信号处理硬件片选与软件片选的高频场景取舍片选信号在 SPI 通信里的重要性前面已经提过。这里展开讲讲硬件片选和软件片选的区别以及什么时候该用哪种。4.1 硬件片选NSS 硬件模式的坑STM32 等 MCU 的 SPI 外设支持硬件 NSS 模式即片选信号由外设自动控制。主机在发送数据前自动拉低 NSS发送完成后自动拉高。听起来很方便实际上有不少坑。最大的问题是时序不可控。硬件 NSS 的拉高时机由外设内部状态机决定有些 MCU 的 SPI 外设在发送完最后一个字节后会立刻拉高 NSS但某些从机要求在最后一个时钟边沿之后保持片选低电平一段时间比如 tCSH片选保持时间硬件模式可能不满足这个要求。第二个问题是多从机场景。硬件 NSS 模式通常只能控制一个从机如果需要接多个从机就要用软件方式控制额外的 GPIO 作为片选不然没法实现“选中 A、不选中 B”的独立控制。4.2 软件片选为什么往往是更稳的选择软件片选就是用普通 GPIO 来控制 CS 引脚发送数据前手动拉低发送完成后手动拉高。这样可以精确控制片选的时机在片选拉低之后、数据发送之前可以加一点延时保证从机有足够时间准备接收。我在实际项目里几乎全部使用软件片选哪怕只有一个从机。原因很简单软件片选的时序完全可控遇到问题时可以通过调整延时来规避从机的时序约束。代价仅仅是每次传输前多写两行 GPIO 操作代码对整个系统性能的影响可以忽略不计。软件片选还有一个隐藏好处可以很方便地实现“连续读”操作。比如 SPI Flash 的快速读指令可以在一次片选有效期间连续读取多个字节中间片选不能拉高。如果用软件片选这个过程就是控制好一次片选低电平期间连续调用多次发送接收函数逻辑非常清晰。4.3 多设备总线仲裁与“总线空闲”问题多个 SPI 从机共享同一组 SCK、MOSI、MISO 线各用各的片选这是常见的多设备接法。但这里有一个容易忽略的问题从机的 MISO 输出必须在片选无效时处于高阻态否则多个从机同时驱动 MISO就会产生总线冲突。有些从机芯片的 MISO 引脚在片选无效时并不是高阻态而是持续输出某个电平。这种芯片就不适合直接并联在共享的 SPI 总线上要么加一个三态缓冲器要么给它单独分配一组 SPI 引脚。选型的时候一定要看芯片手册里对 MISO或 SDO输出使能的描述确认是否支持“三态输出”或“高阻态”。还有一个与总线空闲相关的细节在所有片选都无效的情况下MISO 线是悬空的主机读到的是一个不确定的电平。某些从机协议要求主机在发起通信前先读一次 MISO 来检测从机是否在线如果总线上有外部下拉电阻读到的就是稳定的低电平检测逻辑才能可靠工作。所以多设备共享 SPI 总线时建议在 MISO 上加一个 10kΩ 下拉电阻给总线一个确定的状态。5. 从零写一套 SPI 驱动GPIO 模拟与硬件外设的落地对比不管你是用 STM32、Arduino 还是 ESP32SPI 驱动的方式无非两种GPIO 模拟和硬件外设。两种方式各有各的使用场景我分别说说。5.1 GPIO 模拟三线/四线 SPI 的适用场景GPIO 模拟 SPI就是用代码控制普通 IO 口来产生 SCK 和 MOSI 的波形同时读取 MISO 的电平。这种方式的好处是几乎不受 MCU 型号限制任何有 GPIO 的芯片都能实现。模拟 SPI 的逻辑核心就是“一个时钟一个位”。发送一个字节的函数基本长这样void spi_soft_write_byte(uint8_t data) { for (int i 7; i 0; i--) { // 先设置数据线电平 if (data (1 i)) { MOSI_GPIO_HIGH(); } else { MOSI_GPIO_LOW(); } // 产生时钟上升沿 SCK_GPIO_LOW(); delay_ns(100); SCK_GPIO_HIGH(); delay_ns(100); } }所谓“IO口模拟三线SPI”就是在这个基础上把 MOSI 和 MISO 合并成一根双向数据线发送时把引脚配置为推挽输出接收前把引脚切换为浮空输入或上拉输入同时改变收发方向。每次操作 GPIO 的方向寄存器都会花几十个时钟周期所以模拟三线 SPI 的速率一般上不去实测在普通 STM32 上稳定跑 1MHz 就不错了。GPIO 模拟适合以下几种场景MCU 没有硬件 SPI 外设比如某些低成本 8 位单片机SPI 引脚被复用占用只剩普通 GPIO需要极其灵活的时序控制比如某些从机的时序很怪异只是临时调试不想初始化外设和中断缺点是 CPU 占用高、速率上不去、时序抖动大。所以只要 MCU 有硬件 SPI量产项目我一般不建议用 GPIO 模拟。5.2 硬件 SPI 外设的初始化要点以 STM32 为例硬件 SPI 的优势是速率高、CPU 介入少配合 DMA 可以实现真正的后台传输。以 STM32 为例用 CubeMX 配置 SPI 外设时有几个关键参数数据大小通常选 8 Bits部分传感器要求 16 Bits按手册来时钟极性和相位即前面说的 CPOL/CPHA必须和从机匹配预分频器决定 SCK 频率初学阶段建议从小频率开始试帧格式MSB First 还是 LSB First大部分设备是 MSB First软件 NSS强烈建议使能软件片选用普通 GPIO 控制 CS初始化完成后STM32 的 SPI 发送一个字节的典型代码如下uint8_t spi_read_write_byte(uint8_t tx_data) { while (!(hspi1.Instance-SR SPI_FLAG_TXE)); hspi1.Instance-DR tx_data; while (!(hspi1.Instance-SR SPI_FLAG_RXNE)); return (uint8_t)hspi1.Instance-DR; }这段代码的逻辑是等待发送缓冲区空写入数据然后等待接收缓冲区非空读回数据。前面说过 SPI 是全双工的发送一个字节的同时会收到一个字节所以这个函数既能当发送用也能当接收用——读操作的本质就是“发一个字节收一个字节”。5.3 收发流程一次完整读操作要拆成两段很多新手写 SPI Flash 读数据时经常卡住原因就是没有理解“全双工交换”对读操作的约束。以常见的 W25Q128 为例读数据的流程是拉低片选发送读指令0x03发送 24 位地址分三个字节从第 4 个字节开始每发一个字节可以是任意值从机返回一个字节的数据读完后拉高片选这里的关键在于第 4 步你想读 N 个字节就必须额外发出 N 个时钟周期的 SCK。主机发的是什么内容不重要重要的是产生时钟。在实际代码里通常这样做uint8_t w25q_read_byte(void) { uint8_t dummy 0x00; return spi_read_write_byte(dummy); }发送一个 dummy 字节目的就是为了产生 8 个 SCK 时钟脉冲把从机移位寄存器里的数据“换”回来。如果只发指令不发时钟MISO 上是不会有数据出来的。这就是为什么 SPI 读操作比写操作多一个“假数据”环节。不理解这个机制写出来的代码往往少发几个时钟导致读回来的数据整体错位或者缺字节。6. 高频场景实测SPI Flash、屏幕刷新与 FPGA 从机的数据链路SPI 协议本身是中立的但不同类型的从机设备对 SPI 的用法有各自的特点。这里挑三个最常见的场景展开讲讲我的实测经验。6.1 读 SPI Flash W25Q 系列的正确姿势W25Q 系列 SPI Flash 是市面上最常见的 SPI 存储芯片ST 的开发板上几乎都能看到它的身影。它的 SPI 接口支持 Mode 0 和 Mode 3使用前建议先读一下 JEDEC IDManufacturer Device ID来验证通信是否正常。读 ID 的命令是 0x9F接下来需要发送 3 个 dummy 字节来换取 3 个 ID 字节。Java 代码里会有一个常见错误发送完 0x9F 后直接调用接收函数但接收函数内部没有产生时钟结果读回来全是 0xFF。正确的做法是使用前面提到的“交换式读写函数”发送 0x9F 后继续发送三个 dummy 字节每次发送的同时读取返回数据uint8_t id[3]; spi_cs_low(); spi_read_write_byte(0x9F); id[0] spi_read_write_byte(0x00); id[1] spi_read_write_byte(0x00); id[2] spi_read_write_byte(0x00); spi_cs_high();注意读回来的字节顺序第一个返回的是厂商 IDW25Q128 是 0xEF第二个是内存类型0x40第三个是容量0x18。实际测试中我习惯把 SPI 速率先设为 1MHz 左右读 ID确认通信正常后再调到 10MHz 或更高进行大数据量读写。因为 ID 读取时序最简单任何模式不匹配都会直接导致 ID 错误定位问题很快。6.2 屏幕场景SPI 刷新率与 DMA 收益SPI 屏幕是另一个高频场景。很多 TFT 屏比如 ST7789、ILI9341都支持 SPI 接口这类屏幕本身分辨率不高用 SPI 驱动完全够用。刷一帧 240x240 的 RGB565 图像数据量是 240 * 240 * 2 115200 字节如果用 40MHz 的 SPI 速率理论上大约需要 23ms加上命令和延时实际帧率能到 30fps 左右。但这里有一个前提MCU 不能一个字节一个字节地阻塞发送否则 CPU 全都耗在等发送完成上帧率会骤降到个位数。解决办法是 SPI DMA。SPI DMA 的收益在屏幕驱动上非常明显。配置好 DMA 通道后可以直接把显示缓冲区的内容一次性地搬运到 SPI 外设发送寄存器整个传输过程中 CPU 只需要初始化传输和等待传输完成中断中间可以处理其他任务。实测在 STM32F103 上同样刷一屏阻塞方式需要约 30ms CPU 全占用DMA 方式只需不到 1ms 的 CPU 介入其余时间由 DMA 控制器搬运数据。配置 SPI DMA 的要点是DMA 外设地址设为 SPI 的发送数据寄存器SPI_DR内存地址设为显示缓冲区传输方向为内存到外设数据宽度、传输大小要一致使能 SPI 的 DMA 发送请求。CubeMX 里配置好后代码层面只需要调用 HAL_SPI_Transmit_DMA 函数。6.3 FPGA 做从机时序边界更容易暴露问题FPGA 作为 SPI 从机是另一种常见的应用场景。MCU 作为主机通过 SPI 与 FPGA 通信FPGA 内部用 Verilog 实现 SPI 从机逻辑。这个场景最考验对协议细节的理解因为 FPGA 的逻辑是你自己写的任何时序边界问题都会直接反映在数据上。SPI slave Verilog 实现的核心是一个移位寄存器和一个位计数器。在 SCK 的采样沿把 MOSI 的数据移入寄存器在输出沿把要发送的数据移到 MISO 上。需要注意的一点是MISO 输出必须在片选无效时置为高阻态用assign MISO (cs_n 1b0) ? shift_reg[7] : 1bz;否则多个设备共享总线时会冲突。用 FPGA 做从机时我遇到过最典型的问题是MCU 的 SPI 模式配置为 Mode 0FPGA 在上升沿采样 MOSI、下降沿更新 MISO逻辑上没问题但实际抓波形发现 MISO 数据在下降沿之后有一小段毛刺MCU 在下一个上升沿采样时刚好采到了毛刺。解决方法是让 FPGA 的移位寄存器采 SCK 下降沿更新 MISO或者在 MISO 输出路径上加一个寄存器打拍保证数据变化沿和采样沿之间留出足够的建立保持时间。这是在 RTL 代码层面经常被忽略、却又影响稳定性的关键点。7. 一次“读回全FF”问题的完整排查链路下面还原一个真实的排查过程。这个案例来自一个使用 SPI 通信的温湿度传感器项目现象是主机读取传感器数据时读回来的字节全是 0xFF。这类问题在 SPI 调试中非常典型排查链路也值得完整记录。7.1 现象MISO 一直是高电平读回全 FF设备正常上电传感器数据线上拉电阻正常主机发送读取指令后读取函数返回的所有字节都是 0xFF。0xFF 表示所有位都是 1说明 MISO 线上读到的一直是高电平。遇到这个现象第一反应不要直接改代码。0xFF 可能意味着从机根本没响应也可能意味着主机根本没产生有效的采样时序甚至可能意味着 MISO 线压根就没接对。信息不足的情况下改代码只会把问题越改越乱。7.2 排查链路先看 SCK再看片选再看数据线我习惯的排查顺序是固定的这个顺序能快速缩小问题范围第一步测 SCK。用示波器或逻辑分析仪抓主机的 SCK 引脚确认主机确实产生了时钟脉冲频率和预期一致。如果 SCK 没有波形问题在主机的 SPI 外设配置或时钟使能属于初始化层面的错误。第二步测片选。确认片选信号在传输期间确实被拉低而且保持时间足够。如果片选根本没拉低或者拉低的时间太短从机根本不会进入接收状态MISO 会保持高电平。第三步测 MOSI。确认主机发出去的数据确实在 MOSI 上出现了正确的电平序列。有时候问题出在 GPIO 复用配置上比如 MOSI 引脚被其他外设占用导致数据根本没输出到引脚上。第四步测 MISO。在保证前三步正常的情况下仔细观察 MISO 的电平变化。如果 MISO 一直稳定在高电平那问题多半在从机侧供电、复位、或从机根本没进入正常工作状态。7.3 最终根因从机供电/复位/模式不匹配回到那个温湿度传感器的案例。前三步都正常SCK、片选、MOSI 波形都对但 MISO 就是不动。后来用万用表量了从机 VCC 引脚的电压发现只有 2.8V而传感器要求的最小工作电压是 3.0V。问题出在供电线路上传感器由一颗低压差稳压器供电稳压器的输入来自主电源主电源在同时驱动其他负载时电压跌落导致稳压器输出不到 3.0V。传感器在这种情况下并没有完全死掉内部电路还能部分工作但 SPI 接口的驱动器不在工作范围内MISO 无法正常输出。换了一颗输出能力更强的稳压器电压稳定在 3.3V 之后MISO 波形立刻正常数据读取也恢复了。这个案例说明一个道理SPI 通信异常不一定是 SPI 本身的问题供电、复位、时钟这些“外围因素”往往是更隐蔽的元凶。尤其是从机的电源引脚电压只要低于芯片的最低工作电压即使没有触发掉电复位芯片的数字接口也可能处于不确定状态。7.4 排查清单SPI 通信异常的通用定位表结合多次调试经验我整理了一个通用的排查清单现象可能原因排查方向读回全 0xFFMISO 无输出、从机未响应、从机供电异常查 MISO 接线、从机供电、从机复位读回全 0x00MISO 被拉低、从机输出三态但 MISO 无上拉查 MISO 是否有短路、总线电阻数据错位模式不匹配、数据帧格式错误、采集的位不在正确位置查 CPOL/CPHA、数据位顺序、SCK 频率第一次正常后续出错片选时序异常、从机状态机未复位查片选保持时间、传输之间是否拉高片选SCK 频率高了就不稳定走线过长、电平转换器件带宽不足、从机负载过重降速率、缩短飞线、检查电平匹配数据偶尔跳变采样边沿接近数据变化沿、干扰耦合检查模式配置、增加滤波、优化布线这张表不是万能药但它能帮你在面对“不知从何查起”的 SPI 问题时先给出一个有方向的起点。排查 SPI 问题最忌讳的是一上来就怀疑代码逻辑然后在代码里反复试各种参数——没有数据支撑的瞎试只会浪费时间。8. 不同平台的 SPI 适配要点STM32、Arduino 与 FPGA最后聊聊不同平台上 SPI 的使用差异。虽然协议本身是标准的但各个平台的 API 设计和底层实现各有不同适配的时候有一些需要注意的地方。8.1 STM32 的 SPI 外设配置细节STM32 的 SPI 外设配置主要围绕 CubeMX核心参数前面已经提过这里补充几个容易忽略的细节。第一个是 SPI 时钟的预分频问题。STM32 的 SPI 时钟源来自 APB 总线时钟不同系列的 APB 时钟频率不同。同样是预分频 16在 F103 上算出的 SCK 频率和 F429 上是不一样的。配置之前先用HAL_RCC_GetPCLK2Freq()之类的函数确认一下总线时钟频率再反推预分频值。第二个是 SPI 引脚的速度等级。GPIO 的输出速度配置会影响 SPI 的信号质量。SCK 速率在 10MHz 以上时GPIO 输出速度至少要配到 High50MHz档否则波形上升沿变缓高速通信时容易出错。很多人 SPI 低速正常、高速失败就是这个原因。第三个是 SPI 外设的 CRC 功能默认是关闭的但某些 HAL 库版本在初始化时会默认使能 CRC导致正常通信时数据尾部的时序与从机期望不一致。遇到通信“偶尔多了一个字节”的现象可以检查一下 SPI 的 CRC 是否被意外打开了。8.2 Arduino 的 SPI 库与主从适配Arduino 的 SPI 库封装得比较友好核心操作就几个函数SPI.begin()、SPI.beginTransaction(settings)、SPI.transfer(data)、SPI.endTransaction()。其中SPI.beginTransaction()的参数是SPISettings里面包含了速率、位序、模式三项内容。这里有个细节SPISettings的构造函数是编译期常量如果需要在运行时动态切换不同从机的参数就要在每次切换时重新调用beginTransaction。Arduino 的默认片选引脚在大多数板子上是固定的比如 Uno 的 10 号引脚但调用beginTransaction并不会自动拉低片选需要自己控制 CS 引脚。这就回到了前面说的“软件片选”逻辑——Arduino 库的设计默认就是让用户自己管片选。Arduino 驱动 ST7789 这类 SPI 屏时需要注意库函数SPI.transfer是阻塞式的如果屏幕分辨率高、刷新频繁建议使用SPI.transfer(buffer, size)这种批量传输版本能显著减少函数调用开销。8.3 FPGA 侧Verilog 实现与跨时钟域提醒FPGA 做 SPI 从机时最容易出的问题反而是设计之外的“跨时钟域”。MCU 的 SCK 频率与 FPGA 的系统时钟频率不同直接用系统时钟去采样 SCK 的话容易采到亚稳态。一个稳妥的做法是先用两级触发器同步 SCK再在同步后的 SCK 边沿上采样数据。这样可以显著降低亚稳态出现的概率。同时位计数器要根据同步后的 SCK 来计数保证片选拉低期间恰好采够一个数据帧的位数。// SPI 从机接收部分 reg sck_d1, sck_d2; always (posedge clk) begin sck_d1 sck; sck_d2 sck_d1; end wire sck_negedge sck_d2 ~sck_d1; // 下降沿检测 wire sck_posedge ~sck_d2 sck_d1; // 上升沿检测上面这段是常见的打拍同步写法。收到一个有效的 SCK 边沿后把 MOSI 的数据移入移位寄存器同时把移位寄存器最高位的数据输出到 MISO。片选拉高时位计数器清零移位寄存器复位等待下一次传输。FPGA 侧还要注意一点MISO 的输出时序要与主机的采样沿对齐。如果主机配置的是 Mode 0上升沿采样那么 FPGA 应该在下降沿更新 MISO这样数据在上升沿来临时已经稳定建立时间充足。另一种做法是直接用 SCK 下降沿驱动assign MISO reg_data[7]前提是组合逻辑路径的延时足够小不违背时序约束。GPIO 模拟三线 SPI、硬件片选与软件片选、SPI DMA、SPI Flash、屏幕刷新、FPGA 从机这几个方向的实践经验写下来我自己又过了一遍这些年踩过的坑。SPI 的简单只是表面上的简单真正稳定好用的 SPI 通信靠的是对时序每一个细节的尊重——从模式选择到片选控制从供电稳定到信号完整性每一步都值得认真对待。
返回列表