嵌入式 MCU

SPI

SPI的学习笔记。

1. 摘要与目标

SPI 是嵌入式系统中非常常见的同步串行通信接口,典型用途是让 MCU 连接 Flash、OLED、ADC/DAC、IMU、无线模块、以太网控制器等片外设备。很多初学者能调用 HAL_SPI_Transmit()HAL_SPI_TransmitReceive(),但遇到读数据全是 0xFF、CPOL/CPHA 配错、片选时序不对、读寄存器多出一个空字节、DMA 接收异常时,很容易不知道问题到底在接线、时序、设备命令还是驱动流程。

本文从工程视角整理 SPI 的核心机制:先建立 SCKMOSIMISOCS/NSS 四类信号的总线模型,再解释全双工移位寄存器、片选控制、四种时钟模式、寄存器访问流程,最后映射到 STM32 HAL 的阻塞、中断和 DMA 调用方式,并给出常见故障排查顺序。

本文适用于常见 MCU 作为 SPI 主机访问外设的场景,示例以 STM32 HAL 为主。本文不展开 Quad SPI / Octo SPI、SDIO、I2S、SPI NAND 完整文件系统驱动,也不把某一个具体传感器或 Flash 的命令表当作通用 SPI 规则。

学习时可以按三层模型建立理解:硬件层关注四根信号线、片选和电气连接;协议层关注时钟边沿、全双工移位和设备自定义命令;工程层再将这些流程映射到 STM32 HAL 配置、收发 API、错误码和逻辑分析仪波形。

SPI 主机通过 SCK、MOSI、MISO 和独立 CS 线连接多个从设备,主机产生时钟并用片选决定当前通信对象

图 1:SPI 的关键不是“总线上自动寻址”,而是主机用独立片选选择从设备,再用时钟推动 MOSI/MISO 同步移位。

2. 原理与关键机制

2.1 核心概念:SCK、MOSI、MISO 与 CS/NSS

SPI 通常由一个主机和一个或多个从设备组成。最常见的四线 SPI 包含:

信号常见名称方向作用
SCK / SCLKSerial Clock主机输出主机产生时钟,决定数据移位节奏
MOSIMaster Out Slave In主机到从机主机发送命令、地址或写数据
MISOMaster In Slave Out从机到主机从机返回状态或读数据
CS / SS / NSSChip 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 控制器产生时钟的方式,就是向发送数据寄存器写入一个无意义字节,例如 0x000xFF。这个无意义字节通常被称为 dummy byte。

主机想读 1 字节:
CS 拉低 → 主机发送 dummy byte 产生 8 个 SCK → 从机在 MISO 返回 1 字节 → CS 拉高

从波形上看,MOSIMISO 总是跟着同一组 SCK 同步变化。只不过在写操作中,主机更关心 MOSI 上的命令和数据;在读操作中,主机更关心 MISO 上返回的数据。若使用 HAL_SPI_Receive(),不同 STM32 HAL 实现内部也可能通过发送 dummy 数据来产生接收时钟;学习阶段更推荐直接理解并使用 HAL_SPI_TransmitReceive()

SPI 读操作的记忆法

SPI 不是“我等从机发数据”,而是“我发时钟把从机的数据移出来”。所以读寄存器时,经常会看到命令字节、dummy 字节和有效返回字节之间存在一个字节的错位关系。

2.3 片选 CS/NSS:谁被选中,谁才能说话

CSNSS 是 SPI 工程中最容易被低估的信号。对大多数从设备来说,CS 拉低表示一次 transaction 开始,CS 拉高表示 transaction 结束。设备会在这个低电平窗口内解释主机发来的命令、地址、dummy 和数据,并在需要时驱动 MISO。

多从设备共用 SCKMOSIMISO 时,必须保证同一时刻只有一个从设备的 CS 被拉低。原因是多个从设备如果同时驱动 MISO,一个输出高电平、另一个输出低电平,就可能造成总线冲突或读出错误数据。未选中的从设备应释放 MISO,让其处于高阻态。

片选还承担“帧边界”的作用。有些设备要求命令、地址和数据必须在同一次 CS 低电平期间连续完成;如果中途把 CS 拉高,设备可能认为当前命令已经结束,下一次再拉低时会重新解析命令。也有些设备允许分段访问,但这必须以设备数据手册为准。

从 MCU 侧看,NSS 可以由硬件管理,也可以由普通 GPIO 软件管理。工程中访问寄存器型外设时,常见做法是把 CS 配成 GPIO 输出,由软件在一次完整交易前后手动拉低和拉高,这样更容易满足不同外设对片选时序的要求。

2.4 CPOL/CPHA:四种 SPI 模式到底在选什么

SPI 没有统一规定所有设备必须在哪个边沿采样数据。因此主机和从机必须使用同一组时钟模式。常见模式由两个参数决定:

  • CPOL:Clock Polarity,决定 SCK 空闲时是低电平还是高电平。
  • CPHA:Clock Phase,决定在第几个有效边沿采样数据。

可以先记住下面这张表:

SPI 模式CPOLCPHASCK 空闲电平常见采样边沿
Mode 000低电平上升沿采样,下降沿移出
Mode 101低电平下降沿采样,上升沿移出
Mode 210高电平下降沿采样,上升沿移出
Mode 311高电平上升沿采样,下降沿移出

更工程化的理解是:发送方要在接收方采样之前把数据准备稳定;接收方在约定的采样边沿读取数据。如果 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 驱动不能只写成“给一个寄存器地址就能读”。真正需要确认的是:

  1. CS 是否必须在整个命令期间保持低电平。
  2. 第一个字节是 opcode、寄存器地址,还是 opcode + 地址混合字段。
  3. 读写方向位在哪里,是否需要设置自增位。
  4. 命令后是否需要 dummy byte 或 dummy clock。
  5. 返回数据从第几个字节开始有效。
  6. 多字节数据的字节序和拼接方式。

2.7 全双工、单工、半双工与 Daisy Chain

最常见的 SPI 是四线全双工:MOSIMISO 分离,主机和从机可以在同一组时钟下同时移出和移入数据。除此之外,工程中还会遇到几种变体:

模式典型连接特点注意事项
Full-duplexSCK + 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;同时用示波器或逻辑分析仪观察 SCKMOSIMISOCS 的边沿关系。如果高速下 MISO 数据在采样边沿附近才变化,就可能违反建立/保持时间。

2.9 SPI 没有 ACK:错误要靠读回、状态位和超时兜底

SPI 的简单和高速来自于协议层很薄,但代价是缺少统一错误反馈。I2C 地址阶段无 ACK 时,主机能较快知道没有设备响应;SPI 即使 CS 没接好、从机没上电,主机也可能照样产生时钟并收回一串 0xFF0x00

因此,SPI 驱动通常要靠设备层机制判断通信是否可信:

  • 读取固定 ID 寄存器,例如 WHO_AM_IJEDEC ID 或芯片版本号。
  • 写入某个配置寄存器后再读回确认。
  • 查询状态寄存器中的 busy、ready、error 位。
  • 对关键数据增加 CRC、校验和或长度字段,前提是设备协议支持。
  • 对 HAL 调用设置超时,避免等待硬件状态位时卡死。

从工程角度看,SPI 的故障定位比 I2C 更依赖逻辑分析仪和设备手册。因为 SPI 波形本身只能说明“主机有没有拉片选、有没有发时钟、MOSI/MISO 有什么电平变化”,不能自动说明“从设备是否正确理解了命令”。

3. STM32 HAL 使用方法

STM32 HAL 将 SPI 外设的初始化、片上状态机、阻塞/中断/DMA 收发封装为一组函数。学习时不必先背完整函数原型,更重要的是理解每类 API 对应的 SPI 行为:是只发送、只接收,还是在同一组时钟下同时发送和接收。

3.1 使用前准备

  1. 从从设备数据手册确认:SPI 模式、最大 SCK、数据帧位宽、位序、CS 有效电平、命令格式、dummy byte 数量和上电等待时间。
  2. 在 CubeMX 或工程配置中设置 SCK/MOSI/MISO 复用功能;若用软件片选,将 CS 配置为普通 GPIO 输出,并默认拉高。
  3. 初次调试建议使用较低 SCK,例如先从几百 kHz 到 1 MHz 量级开始,确认命令和读回 ID 正常后再提高速度。
  4. 多从设备共用 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 时注意缓冲区类型和字节序
CLKPolarityCPOL,SCK 空闲电平必须与从设备手册一致
CLKPhaseCPHA,采样边沿选择配错常导致 bit 偏移或命令不识别
NSS片选管理访问复杂外设时常用软件 GPIO 控制 CS
BaudRatePrescalerSPI 时钟分频不能超过从设备最大 SCK 和板级信号质量能力
FirstBitMSB first 或 LSB first大多数外设为 MSB first,但必须查手册
TIModeTI 帧格式普通 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_DMADMA 负责搬运发送和接收缓冲区屏幕刷图、Flash 连续读写、高频大块数据

学习阶段建议先用阻塞模式读设备 ID,确认 SPI 模式、片选和命令格式都正确。切换到中断或 DMA 后,再额外处理以下问题:

  • 传输完成回调中再拉高 CS,避免 DMA 尚未完成就结束 transaction。
  • 发送缓冲区和接收缓冲区在异步传输完成前不能被释放或改写。
  • 全双工 DMA 往往需要同时配置 TX DMA 和 RX DMA。
  • 若只想接收,也要确认底层如何产生时钟以及 dummy 数据来源。
  • RTOS 中要用互斥量或状态机保护同一个 SPI 总线,避免多个任务同时拉不同 CS。

3.5 返回状态与错误定位

阻塞 API 通常返回 HAL_OKHAL_ERRORHAL_BUSYHAL_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 ErrorCRC 校验失败开启 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 一直是 0xFF0x00

  • 发生条件:主机能产生 SCK,MOSI 上也有命令,但读回数据全是固定值。
  • 可观察证据:逻辑分析仪显示 MISO 长期高电平或低电平;日志中读数固定为 0xFF0x00 或重复无效值。
  • 原因假设: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;用逻辑分析仪对齐 CSSCKMOSIMISO 四路波形。
示例:读 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. 官方参考资料

  1. STMicroelectronics:Getting started with SPI
  2. STMicroelectronics:STM32 Serial Peripheral Interface training PDF
  3. STMicroelectronics:HAL SPI APIs
  4. Analog Devices:Introduction to SPI Interface
  5. Microchip:TB3215 Getting Started with SPI
  6. Microchip Developer Help:SPI Introduction
  7. Analog Devices:AN-1248 SPI Interface