SPI
SPI的学习笔记。
1. 摘要与目标
SPI 是嵌入式系统中非常常见的同步串行通信接口,典型用途是让 MCU 连接 Flash、OLED、ADC/DAC、IMU、无线模块、以太网控制器等片外设备。很多初学者能调用 HAL_SPI_Transmit() 或 HAL_SPI_TransmitReceive(),但遇到读数据全是 0xFF、CPOL/CPHA 配错、片选时序不对、读寄存器多出一个空字节、DMA 接收异常时,很容易不知道问题到底在接线、时序、设备命令还是驱动流程。
本文从工程视角整理 SPI 的核心机制:先建立 SCK、MOSI、MISO、CS/NSS 四类信号的总线模型,再解释全双工移位寄存器、片选控制、四种时钟模式、寄存器访问流程,最后映射到 STM32 HAL 的阻塞、中断和 DMA 调用方式,并给出常见故障排查顺序。
本文适用于常见 MCU 作为 SPI 主机访问外设的场景,示例以 STM32 HAL 为主。本文不展开 Quad SPI / Octo SPI、SDIO、I2S、SPI NAND 完整文件系统驱动,也不把某一个具体传感器或 Flash 的命令表当作通用 SPI 规则。
学习时可以按三层模型建立理解:硬件层关注四根信号线、片选和电气连接;协议层关注时钟边沿、全双工移位和设备自定义命令;工程层再将这些流程映射到 STM32 HAL 配置、收发 API、错误码和逻辑分析仪波形。
图 1:SPI 的关键不是“总线上自动寻址”,而是主机用独立片选选择从设备,再用时钟推动 MOSI/MISO 同步移位。
2. 原理与关键机制
2.1 核心概念:SCK、MOSI、MISO 与 CS/NSS
SPI 通常由一个主机和一个或多个从设备组成。最常见的四线 SPI 包含:
| 信号 | 常见名称 | 方向 | 作用 |
|---|---|---|---|
SCK / SCLK | Serial Clock | 主机输出 | 主机产生时钟,决定数据移位节奏 |
MOSI | Master Out Slave In | 主机到从机 | 主机发送命令、地址或写数据 |
MISO | Master In Slave Out | 从机到主机 | 从机返回状态或读数据 |
CS / SS / NSS | Chip Select / Slave Select | 通常主机输出 | 选择当前通信的从设备,常见为低有效 |
SPI 与 I2C 一个很大的区别是:SPI 本身没有统一的设备地址阶段,也没有统一的 ACK/NACK 应答位。I2C 依靠地址和 ACK 在协议层确认“谁响应了我”;SPI 则主要依靠片选线决定“当前正在和谁说话”。因此,多从设备 SPI 通常要求主机给每个从设备提供一根独立 CS 线,通信前拉低目标设备的 CS,通信结束后再释放。
主机通常是唯一产生 SCK 的一方。没有时钟时,即使主机调用了“接收”函数,从设备也不会主动把数据一位一位送出来。换句话说,SPI 的读操作不是“从机自己发给主机”,而是主机一边发出时钟,一边从 MISO 采样数据。
2.2 全双工移位:为什么“读也要发 dummy byte”
标准四线 SPI 是典型的同步全双工接口。主机和从机内部通常都有移位寄存器:每来一个 SCK 时钟边沿,发送侧移出 1 bit,接收侧移入 1 bit。经过 8 个时钟后,一个字节从主机移到从机,同时另一个字节也从从机移到主机。
这会带来一个很重要的工程结论:SPI 的发送和接收在时钟层面同时发生。即使你只想读取从设备的数据,主机也必须继续产生时钟;而很多 MCU 的 SPI 控制器产生时钟的方式,就是向发送数据寄存器写入一个无意义字节,例如 0x00 或 0xFF。这个无意义字节通常被称为 dummy byte。
主机想读 1 字节:CS 拉低 → 主机发送 dummy byte 产生 8 个 SCK → 从机在 MISO 返回 1 字节 → CS 拉高从波形上看,MOSI 和 MISO 总是跟着同一组 SCK 同步变化。只不过在写操作中,主机更关心 MOSI 上的命令和数据;在读操作中,主机更关心 MISO 上返回的数据。若使用 HAL_SPI_Receive(),不同 STM32 HAL 实现内部也可能通过发送 dummy 数据来产生接收时钟;学习阶段更推荐直接理解并使用 HAL_SPI_TransmitReceive()。
SPI 读操作的记忆法
SPI 不是“我等从机发数据”,而是“我发时钟把从机的数据移出来”。所以读寄存器时,经常会看到命令字节、dummy 字节和有效返回字节之间存在一个字节的错位关系。
2.3 片选 CS/NSS:谁被选中,谁才能说话
CS 或 NSS 是 SPI 工程中最容易被低估的信号。对大多数从设备来说,CS 拉低表示一次 transaction 开始,CS 拉高表示 transaction 结束。设备会在这个低电平窗口内解释主机发来的命令、地址、dummy 和数据,并在需要时驱动 MISO。
多从设备共用 SCK、MOSI、MISO 时,必须保证同一时刻只有一个从设备的 CS 被拉低。原因是多个从设备如果同时驱动 MISO,一个输出高电平、另一个输出低电平,就可能造成总线冲突或读出错误数据。未选中的从设备应释放 MISO,让其处于高阻态。
片选还承担“帧边界”的作用。有些设备要求命令、地址和数据必须在同一次 CS 低电平期间连续完成;如果中途把 CS 拉高,设备可能认为当前命令已经结束,下一次再拉低时会重新解析命令。也有些设备允许分段访问,但这必须以设备数据手册为准。
从 MCU 侧看,NSS 可以由硬件管理,也可以由普通 GPIO 软件管理。工程中访问寄存器型外设时,常见做法是把 CS 配成 GPIO 输出,由软件在一次完整交易前后手动拉低和拉高,这样更容易满足不同外设对片选时序的要求。
2.4 CPOL/CPHA:四种 SPI 模式到底在选什么
SPI 没有统一规定所有设备必须在哪个边沿采样数据。因此主机和从机必须使用同一组时钟模式。常见模式由两个参数决定:
CPOL:Clock Polarity,决定SCK空闲时是低电平还是高电平。CPHA:Clock Phase,决定在第几个有效边沿采样数据。
可以先记住下面这张表:
| SPI 模式 | CPOL | CPHA | SCK 空闲电平 | 常见采样边沿 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低电平 | 上升沿采样,下降沿移出 |
| Mode 1 | 0 | 1 | 低电平 | 下降沿采样,上升沿移出 |
| Mode 2 | 1 | 0 | 高电平 | 下降沿采样,上升沿移出 |
| Mode 3 | 1 | 1 | 高电平 | 上升沿采样,下降沿移出 |
更工程化的理解是:发送方要在接收方采样之前把数据准备稳定;接收方在约定的采样边沿读取数据。如果 CPOL/CPHA 配错,轻则读到的 bit 整体偏移,重则命令字节无法被从设备识别,表现为 MISO 全 0xFF、全 0x00 或偶发错误。
2.5 数据帧大小、位序与“协议帧”
SPI 硬件通常支持 8 bit、16 bit 或更多位宽的数据帧,具体取决于 MCU 系列。多数常见外设以 8 bit 为基本单位,但也有 ADC、显示屏或专用芯片使用 9 bit、16 bit 或更长的数据单元。
位序也需要确认。常见 SPI 设备使用 MSB first,即最高位先传;也有少数设备支持或要求 LSB first。主机和从机位序不一致时,波形上看似有数据,但设备解析出的命令会完全不同。
这里要区分两个“帧”:
| 名称 | 所属层次 | 含义 |
|---|---|---|
| SPI 硬件数据帧 | MCU SPI 外设层 | 一次移位的数据宽度,例如 8 bit 或 16 bit |
| 设备协议帧 / transaction | 外设命令层 | CS 低电平期间的一串命令、地址、dummy 和数据 |
例如一次读取寄存器可能包含 3 个 8 bit 硬件数据帧:命令字节、dummy 字节、返回字节。不要把“SPI 配置成 8 bit 数据帧”误解为“一次 SPI 只能传 1 字节”。只要 CS 保持有效,主机可以连续发送多个字节,外设会按自己的命令格式解释这串字节。
2.6 寄存器访问:SPI 没有统一格式,必须看设备手册
I2C 寄存器读写常见流程比较统一:先发设备地址,再发寄存器地址,读操作常用 Repeated START 切换方向。SPI 则不同:SPI 只提供时钟、片选和双向移位通道,不规定命令字节、寄存器地址、读写位、dummy 周期和多字节自增规则。
很多寄存器型 SPI 设备会采用类似下面的格式,但这只是常见形式,不是 SPI 标准:
读寄存器:CS↓ → Command/RegisterAddr(R) → Dummy → DataOut → CS↑写寄存器:CS↓ → Command/RegisterAddr(W) → DataIn → CS↑其中,读写方向可能由命令字节最高位表示,也可能由独立 opcode 表示;寄存器地址可能是 7 bit、8 bit、16 bit 或更多;读操作中可能需要 0 个、1 个或多个 dummy byte;多字节读取是否自动地址递增也由设备决定。
因此,SPI 驱动不能只写成“给一个寄存器地址就能读”。真正需要确认的是:
CS是否必须在整个命令期间保持低电平。- 第一个字节是 opcode、寄存器地址,还是 opcode + 地址混合字段。
- 读写方向位在哪里,是否需要设置自增位。
- 命令后是否需要 dummy byte 或 dummy clock。
- 返回数据从第几个字节开始有效。
- 多字节数据的字节序和拼接方式。
2.7 全双工、单工、半双工与 Daisy Chain
最常见的 SPI 是四线全双工:MOSI 和 MISO 分离,主机和从机可以在同一组时钟下同时移出和移入数据。除此之外,工程中还会遇到几种变体:
| 模式 | 典型连接 | 特点 | 注意事项 |
|---|---|---|---|
| Full-duplex | SCK + MOSI + MISO + CS | 收发同时进行 | 最常见,读也要发时钟 |
| Simplex | 只用单方向数据线 | 只发送或只接收 | 未用引脚可作其他用途,取决于芯片支持 |
| Half-duplex / 3-wire | 共享一根数据线 | 分时发送和接收 | 方向切换必须同步,避免双方同时驱动 |
| Daisy Chain | 多个设备串成链 | 共用一个 CS,数据逐级传递 | 设备必须支持,时钟数与链上设备数量相关 |
半双工模式容易出现的问题是方向切换时序。如果主机还在驱动数据线,而从机也开始驱动同一根线,就可能发生临时短路或读出错误。部分设计会在共享数据线上串联小电阻,以降低方向切换瞬间的冲突风险,但根本上仍要依靠正确的软件和硬件时序。
Daisy Chain 则适合多个同类设备级联,例如部分移位寄存器、LED 驱动器或专用采集芯片。它不是“所有 SPI 设备都天然支持”的能力,必须确认目标设备是否提供串行级联输出。
2.8 速度、线长与信号完整性
SPI 没有像 I2C 那样依赖上拉电阻形成高电平,常见 MCU SPI 引脚多为推挽输出,因此上升沿通常更快,也更适合较高速短距离通信。但这并不意味着 SPI 速度可以随便拉高。
SPI 最高可用速率受这些因素限制:
- 主机 SPI 外设时钟和分频器配置。
- 从设备数据手册给出的最大
SCK频率。 - 从设备从采样到输出有效数据的延迟。
- PCB 走线长度、飞线长度、负载电容和阻抗不连续。
CS到第一个时钟的建立时间,以及最后一个时钟到CS释放的保持时间。- 主机软件是否能及时读取接收缓冲,避免 overrun。
当 1 MHz 正常、8 MHz 或 18 MHz 不稳定时,不能只说“SPI 坏了”。更合理的排查是:先降低分频确认协议流程无误,再逐步提高 SCK;同时用示波器或逻辑分析仪观察 SCK、MOSI、MISO 和 CS 的边沿关系。如果高速下 MISO 数据在采样边沿附近才变化,就可能违反建立/保持时间。
2.9 SPI 没有 ACK:错误要靠读回、状态位和超时兜底
SPI 的简单和高速来自于协议层很薄,但代价是缺少统一错误反馈。I2C 地址阶段无 ACK 时,主机能较快知道没有设备响应;SPI 即使 CS 没接好、从机没上电,主机也可能照样产生时钟并收回一串 0xFF 或 0x00。
因此,SPI 驱动通常要靠设备层机制判断通信是否可信:
- 读取固定 ID 寄存器,例如
WHO_AM_I、JEDEC ID或芯片版本号。 - 写入某个配置寄存器后再读回确认。
- 查询状态寄存器中的 busy、ready、error 位。
- 对关键数据增加 CRC、校验和或长度字段,前提是设备协议支持。
- 对 HAL 调用设置超时,避免等待硬件状态位时卡死。
从工程角度看,SPI 的故障定位比 I2C 更依赖逻辑分析仪和设备手册。因为 SPI 波形本身只能说明“主机有没有拉片选、有没有发时钟、MOSI/MISO 有什么电平变化”,不能自动说明“从设备是否正确理解了命令”。
3. STM32 HAL 使用方法
STM32 HAL 将 SPI 外设的初始化、片上状态机、阻塞/中断/DMA 收发封装为一组函数。学习时不必先背完整函数原型,更重要的是理解每类 API 对应的 SPI 行为:是只发送、只接收,还是在同一组时钟下同时发送和接收。
3.1 使用前准备
- 从从设备数据手册确认:SPI 模式、最大
SCK、数据帧位宽、位序、CS有效电平、命令格式、dummy byte 数量和上电等待时间。 - 在 CubeMX 或工程配置中设置
SCK/MOSI/MISO复用功能;若用软件片选,将CS配置为普通 GPIO 输出,并默认拉高。 - 初次调试建议使用较低
SCK,例如先从几百 kHz 到 1 MHz 量级开始,确认命令和读回 ID 正常后再提高速度。 - 多从设备共用 SPI 总线时,确认每个从设备有独立
CS,且未选中设备不会驱动 MISO。
3.2 常用 HAL API
| API | 用途 | 对应的底层机制 |
|---|---|---|
HAL_SPI_Init() | 初始化 SPI 外设 | 配置主从模式、方向、数据位宽、CPOL/CPHA、NSS、分频、位序等 |
HAL_SPI_Transmit() | 只关心发送缓冲区 | 主机产生时钟,把 pData 依次移出到 MOSI |
HAL_SPI_Receive() | 只关心接收缓冲区 | 主机产生接收时钟,并把 MISO 采样结果写入缓冲区 |
HAL_SPI_TransmitReceive() | 同时发送和接收 | 每发送一个数据帧,同时接收一个数据帧,最符合 SPI 全双工本质 |
HAL_SPI_Transmit_IT() / _DMA() | 异步发送 | 由中断或 DMA 处理数据搬运,完成后进入回调 |
HAL_SPI_Receive_IT() / _DMA() | 异步接收 | 由中断或 DMA 接收数据,仍需要主机时钟推动 |
HAL_SPI_TransmitReceive_IT() / _DMA() | 异步全双工收发 | 适合连续读写、显示屏刷屏、较大数据块搬运 |
HAL_SPI_GetState() | 查询句柄状态 | 判断 SPI 是否 busy、ready 或处于错误状态 |
HAL_SPI_GetError() | 获取错误码 | 继续定位 overrun、mode fault、CRC error 等硬件/驱动错误 |
Transmit/Receive 看起来像 UART 的发送和接收,但 SPI 的底层逻辑不同:SPI 接收必须有时钟,时钟通常由主机发送数据或 dummy 数据触发。因此访问寄存器型外设时,HAL_SPI_TransmitReceive() 往往比把发送和接收拆开更直观。
3.3 关键参数与配置
| 参数 / 配置 | 含义 | 常见误区或选择依据 |
|---|---|---|
Mode | 主机或从机 | MCU 访问外设通常配置为 Master |
Direction | 全双工、单线、只收等方向 | 常见四线 SPI 选全双工;3-wire 需要半双工配置 |
DataSize | 每个硬件数据帧位宽 | 8 bit 最常见;16 bit 时注意缓冲区类型和字节序 |
CLKPolarity | CPOL,SCK 空闲电平 | 必须与从设备手册一致 |
CLKPhase | CPHA,采样边沿选择 | 配错常导致 bit 偏移或命令不识别 |
NSS | 片选管理 | 访问复杂外设时常用软件 GPIO 控制 CS |
BaudRatePrescaler | SPI 时钟分频 | 不能超过从设备最大 SCK 和板级信号质量能力 |
FirstBit | MSB first 或 LSB first | 大多数外设为 MSB first,但必须查手册 |
TIMode | TI 帧格式 | 普通 SPI 外设通常关闭 |
CRCCalculation | 硬件 CRC | 只有设备协议配套支持时才开启 |
下面是一个最小的寄存器读交易示例。它不假设所有设备都使用同一种命令格式,只展示“片选包住一次完整 transaction”和“发送命令时同步接收”的基本结构:
uint8_t tx[2];uint8_t rx[2];
/* tx[0] 应按目标设备手册填写:可能是 opcode,也可能是读位 + 寄存器地址 */tx[0] = 0x00; // 示例占位:命令 / 寄存器地址,实际项目必须替换tx[1] = 0xFF; // dummy byte,用于继续产生 8 个 SCK
HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_RESET);HAL_StatusTypeDef status = HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 100);HAL_GPIO_WritePin(SPI_CS_GPIO_Port, SPI_CS_Pin, GPIO_PIN_SET);
if (status == HAL_OK) { uint8_t value = rx[1]; // 具体哪个字节有效取决于从设备时序}这段代码的重点不是 tx[0] 的具体值,而是流程:先拉低 CS,再连续传输命令和 dummy,让从设备在同一次片选窗口内返回数据,最后拉高 CS 结束交易。真实项目中,必须把 tx[0]、dummy 数量和有效返回字节位置替换为目标芯片手册规定的格式。
3.4 阻塞、中断与 DMA 模式
| 模式 | 常见 API 后缀 | 特点 | 适用场景 |
|---|---|---|---|
| 阻塞 | 无后缀 | 函数等待传输完成或超时,流程直观 | 初次调试、低频传感器、小数据量读写 |
| 中断 | _IT | 函数快速返回,由中断搬运数据并在完成后回调 | 不希望 CPU 一直等待,但数据量不特别大 |
| DMA | _DMA | DMA 负责搬运发送和接收缓冲区 | 屏幕刷图、Flash 连续读写、高频大块数据 |
学习阶段建议先用阻塞模式读设备 ID,确认 SPI 模式、片选和命令格式都正确。切换到中断或 DMA 后,再额外处理以下问题:
- 传输完成回调中再拉高
CS,避免 DMA 尚未完成就结束 transaction。 - 发送缓冲区和接收缓冲区在异步传输完成前不能被释放或改写。
- 全双工 DMA 往往需要同时配置 TX DMA 和 RX DMA。
- 若只想接收,也要确认底层如何产生时钟以及 dummy 数据来源。
- RTOS 中要用互斥量或状态机保护同一个 SPI 总线,避免多个任务同时拉不同 CS。
3.5 返回状态与错误定位
阻塞 API 通常返回 HAL_OK、HAL_ERROR、HAL_BUSY 或 HAL_TIMEOUT。返回值不是 HAL_OK 时,可以继续调用 HAL_SPI_GetError() 获取错误信息,并结合芯片系列 HAL 文档或参考手册判断具体含义。
if (status != HAL_OK) { uint32_t error = HAL_SPI_GetError(&hspi1); printf("SPI status=%d, error=0x%08lX\r\n", status, error);}SPI 常见硬件或驱动错误包括:
| 错误类型 | 直观含义 | 常见诱因 |
|---|---|---|
| Overrun / OVR | 新数据到达时旧数据未及时读取 | 接收太慢、中断被长时间屏蔽、DMA 配置错误 |
| Mode Fault / MODF | 主模式下 NSS 状态异常或多主冲突 | 硬件 NSS 配置不当、多主环境冲突 |
| CRC Error | CRC 校验失败 | 开启 SPI CRC 且双方 CRC 配置或数据不一致 |
| Timeout | 等待状态位超时 | 时钟未产生、SPI busy 卡住、外设或 DMA 状态异常 |
注意:HAL 没报错并不等于从设备理解了命令。因为 SPI 协议层没有 ACK,主机只能确认“本机 SPI 外设完成了移位”,不能自动确认“对面的芯片收到了正确命令”。所以还要通过读 ID、读状态寄存器、读回配置值和实际业务数据来验证。
4. 故障排查
4.1 排查原则
SPI 故障建议按这个顺序定位:供电与共地 → CS 电平 → SCK 是否产生 → MOSI 命令是否正确 → CPOL/CPHA 与位序 → MISO 是否被正确驱动 → 设备命令格式与 dummy → HAL 状态和 DMA/中断流程。
不要一开始就改 DMA、中断和任务调度。先用低速、阻塞、单设备、手动 CS 的方式读一个固定 ID 寄存器,是最容易把硬件连接和协议参数验证清楚的办法。
4.2 问题一:SCK 没有波形
- 发生条件:调用 SPI 收发函数后,逻辑分析仪或示波器看不到
SCK翻转。 - 可观察证据:
CS可能被拉低,但SCK一直保持空闲电平;HAL 可能返回 timeout、busy 或 error。 - 原因假设:SPI 外设未初始化、引脚复用配置错误、时钟未使能、HAL 句柄错误、只拉了 CS 但没有真正调用收发函数、从机模式误配置为主机模式。
- 排查顺序:先确认
HAL_SPI_Init()被调用,再检查 GPIO 复用和 SPI instance;把速度降低,使用阻塞HAL_SPI_Transmit()发送固定字节;最后查看 HAL 返回值和错误码。 - 处理方法:修正 CubeMX 配置、确认
hspi对应的 SPI 外设、检查时钟和引脚复用,并用最小阻塞发送验证。
4.3 问题二:MISO 一直是 0xFF 或 0x00
- 发生条件:主机能产生
SCK,MOSI 上也有命令,但读回数据全是固定值。 - 可观察证据:逻辑分析仪显示 MISO 长期高电平或低电平;日志中读数固定为
0xFF、0x00或重复无效值。 - 原因假设:
CS没有真正选中从设备、MISO 接线错误、从设备未上电或未共地、CPOL/CPHA 配错、命令字节错误、读写位或 dummy 周期不符合手册。 - 排查顺序:先看
CS是否在整个交易期间保持有效;再确认 MISO 是否接到主机 MISO;随后核对 SPI Mode、最大频率和命令格式;最后尝试读取固定 ID 寄存器。 - 处理方法:修正片选和接线;统一主从双方 SPI 模式;按手册重写命令字节和 dummy 处理。
4.4 问题三:读寄存器结果整体错一位或错一个字节
- 发生条件:能读到变化的数据,但数值不对,或者第一个接收字节总是无效,第二个字节才像有效数据。
- 可观察证据:逻辑分析仪显示命令发出后,从设备在后续字节才开始返回有效 MISO;日志中
rx[0]和rx[1]取值关系异常。 - 原因假设:忽略了全双工同时收发,误把命令阶段收到的无效字节当成数据;CPHA 配错导致 bit 位移;设备要求 dummy byte 但代码没发;设备返回数据有 pipeline 延迟。
- 排查顺序:把一次 transaction 展开成
tx[]和rx[]对照表;确认哪个字节是命令阶段回读、哪个字节是 dummy 后回读;再核对 CPOL/CPHA。 - 处理方法:调整有效数据下标,补充必要 dummy byte;修改 SPI Mode;用逻辑分析仪对齐
CS、SCK、MOSI、MISO四路波形。
示例:读 1 字节寄存器时的常见错位TX: [读命令] [dummy]RX: [无效/状态] [有效数据]4.5 问题四:低速正常,高速不稳定
- 发生条件:低分频下读写正常,提高
SCK后出现数据错误、偶发 ID 读取失败或 DMA 错误。 - 可观察证据:错误率随
SCK升高而增加;示波器显示边沿过冲、振铃或 MISO 在采样边沿附近才稳定。 - 原因假设:超过从设备最大时钟频率、飞线过长、信号完整性差、片选建立时间不够、主机采样边沿不适合当前延迟。
- 排查顺序:恢复低速验证协议正确;逐档提高分频;缩短连接线;查看从设备 AC timing;必要时调整 SPI Mode 或增加片选到首个时钟的延时。
- 处理方法:降低
SCK,优化布线和地线;必要时加串联阻尼电阻;严格满足数据手册中的 setup/hold 和CS时序。
4.6 问题五:多个从设备共用总线时数据混乱
- 发生条件:单独连接某个设备正常,多个设备挂在同一 SPI 总线上后读写异常。
- 可观察证据:MISO 波形被多个设备同时驱动;不同设备返回数据互相干扰;某个设备通信后另一个设备失败。
- 原因假设:多个
CS同时为低、未选中设备没有释放 MISO、不同设备要求不同 SPI Mode 或不同最大频率、任务并发访问 SPI 总线。 - 排查顺序:逐个设备接入;确认所有
CS默认拉高;每次交易只拉低一个CS;对不同设备切换 SPI 配置前确保 SPI 空闲;RTOS 中加总线互斥。 - 处理方法:修正片选逻辑;必要时给 MISO 加隔离或三态缓冲;为不同设备建立独立配置切换流程;用 mutex 管理总线访问。
4.7 问题六:DMA 或中断模式下偶发丢字节、HAL_BUSY
- 发生条件:阻塞模式正常,切换到
_IT或_DMA后偶发错误、回调不进、再次启动返回HAL_BUSY。 - 可观察证据:HAL 状态长期 busy;完成回调未执行;接收缓冲区数据错乱;overrun 错误出现。
- 原因假设:上一次传输未完成就启动下一次;CS 太早拉高;缓冲区在传输完成前被复用;TX/RX DMA 未同时配置;中断优先级或回调逻辑有问题。
- 排查顺序:先确认阻塞模式稳定;再只测试单次 DMA;在完成回调中拉高 CS 并释放信号量;检查 DMA 通道、NVIC 和缓冲区生命周期。
- 处理方法:用状态机管理 SPI transaction;完成回调中再允许下一次传输;RTOS 中用互斥量和二值信号量同步;必要时清理错误并重新初始化 SPI 外设。
5. 性能、可靠性与安全性
5.1 性能
SPI 性能主要由 SCK 频率、数据帧宽度、片选开销、命令/dummy 开销、CPU 搬运方式和外设内部处理时间决定。对于大块连续数据,例如显示屏刷屏或 Flash 连续读取,DMA 可以显著减少 CPU 逐字节搬运负担;但对于只读几个寄存器的传感器,阻塞模式已经足够简单可靠。
提升 SPI 速度时,不要只改分频器。应同时确认从设备最大频率、板级信号质量、MISO 数据有效延迟、CS 建立/保持时间和软件是否能及时处理接收数据。没有真实波形或稳定性测试时,不应声称某个 SPI 频率“必然可靠”。
5.2 可靠性
可靠的 SPI 驱动至少要考虑:
- 每次 transaction 都有清晰的
CS拉低和拉高边界。 - 阻塞调用设置合理超时,异步调用有完成回调和错误回调。
- 初始化后先读取固定 ID,确认接线、模式和命令格式正确。
- 关键配置写入后读回确认。
- 多任务环境下用互斥量保护同一个 SPI 总线。
- DMA 传输完成前不复用或释放缓冲区。
- 错误时记录 HAL 状态、错误码和关键波形,而不是盲目重试。
5.3 安全性
SPI 多用于板内或板间短距离通信,通常不是直接暴露到网络的安全边界。但如果 SPI 连接外部 Flash、认证芯片、无线模块或安全存储芯片,就需要注意敏感信息保护:不要把密钥、token、完整固件内容或认证中间值直接打印到日志;对外部 Flash 中的重要配置要考虑校验、版本号和掉电一致性;涉及固件升级时还要配合签名校验和回滚策略。
6. 结论
SPI 的学习重点不是背 HAL_SPI_Transmit() 和 HAL_SPI_Receive() 的函数名,而是理解“主机用片选确定对象,用时钟推动主从移位寄存器同步交换数据”。只要掌握 SCK/MOSI/MISO/CS 的职责、全双工收发、dummy byte、CPOL/CPHA、数据帧位宽、片选边界和设备自定义命令格式,就能把大多数 SPI 问题拆成可验证的硬件、时序、协议或驱动问题。实际项目中,最终结论应由设备手册、HAL 错误码、日志和逻辑分析仪波形共同支撑。
7. 官方参考资料
- STMicroelectronics:Getting started with SPI
- STMicroelectronics:STM32 Serial Peripheral Interface training PDF
- STMicroelectronics:HAL SPI APIs
- Analog Devices:Introduction to SPI Interface
- Microchip:TB3215 Getting Started with SPI
- Microchip Developer Help:SPI Introduction
- Analog Devices:AN-1248 SPI Interface