SPI 通信原理与实践
SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工的同步串行通信协议。GSRL 中 IMU(惯性测量单元)的加速度计和陀螺仪均通过 SPI 与 MCU 通信。
上一篇:UART 通信原理与实践
1. 概念——高速同步通信
1.1 与 UART 的对比
UART 是异步的、全双工的、仅两根数据线。但它的速度受限于时钟精度,极限通常在几 Mbps。
SPI 则通过独立的时钟线同步收发双方,可以获得高得多的速率(几十 Mbps),代价是需要更多的连线。
为什么 SPI 需要 4 根线而 UART 只需 2 根? UART 省线的代价是双方必须精确匹配波特率——任何一个设备的时钟偏差都会导致采样错误,因此 UART 速度上限通常在几 Mbps。SPI 用一根独立的时钟线(SCLK)消除了这种不确定性:主设备告诉从设备"现在采样",从设备不需要猜测。这让 SPI 可以达到 10~50 倍于 UART 的速度。同时,CS 信号线使主设备能直接选择从设备,无需在数据流中嵌入地址字节,进一步提高了有效吞吐。
UART (异步) SPI (同步)
MCU_A MCU_B MCU (Master) Sensor (Slave)
TX ─────→ RX SCLK ────────────→ SCLK
RX ←───── TX MOSI ────────────→ MOSI
MISO ←──────────── MISO
CS ────────────→ CS (低有效)
1.2 主从架构
SPI 是严格的主从(Master-Slave)架构:
- Master(主设备):产生时钟信号(SCLK),控制通信的发起与结束
- Slave(从设备):响应主设备的时钟,接收或发送数据
- 一个 Master 可以连接多个 Slave,通过 CS(片选)信号选择与哪个 Slave 通信
2. 物理层——四根信号线
| 信号 | 方向 | 全称 | 说明 |
|---|---|---|---|
| SCLK | Master → Slave | Serial Clock | 时钟信号,由 Master 产生 |
| MOSI | Master → Slave | Master Out, Slave In | 主设备发送的数据 |
| MISO | Slave → Master | Master In, Slave Out | 从设备发送的数据 |
| CS (或 SS) | Master → Slave | Chip Select / Slave Select | 低电平有效,选中从设备 |
典型连接拓扑(一主多从):
┌──────────┐
│ Master │
│ (MCU) │
└──┬──┬──┬─┘
│ │ │
SCLK ────┼──┼──┼──── 所有 Slave 共享
MOSI ────┼──┼──┼──── 所有 Slave 共享
MISO ────┼──┼──┼──── 所有 Slave 共享
│ │ │
CS1 ────┤ │ │ 每个 Slave 独立 CS
CS2 ───┼──┤ │
CS3 ───┼──┼──┤
┌──┴──┐┌──┴──┐┌──┴──┐
│Slave││Slave││Slave│
│ 1 ││ 2 ││ 3 │
└─────┘└─────┘└─────┘
CS 是关键:只有 CS 拉低的 Slave 才会响应总线上的 SCLK/MOSI,其余 Slave 的 MISO 引脚处于高阻态(不影响总线)。
为什么用 CS 引脚而不是像 I2C 那样在数据中发送地址? 这是硬件简单性与引脚数量的权衡。CS 方式的好处是:不需要在数据流中编码和解码地址,通信帧更短(没有地址字节开销),从设备也不需要地址匹配电路。缺点也很明显——每个从设备需要一根独立的 GPIO,连接 5 个从设备就需要 5 根 CS 线。但在典型嵌入式系统中,SPI 从设备数量通常很少(2~4 个),这个代价完全可接受。
为什么 MISO 和 MOSI 要分开? 两条独立的数据线实现了真正的全双工通信——主设备和从设备可以在同一个时钟周期内同时发送和接收数据。虽然 UART 也支持全双工(TX 和 RX 各一根),但 SPI 的全双工不需要收发双方匹配波特率,因为时钟由主设备统一提供。在读取 IMU 传感器时,这一特性意味着"发送读命令"和"接收数据"可以在同一次传输中完成——效率极高。
3. SPI 的四种工作模式(CPOL & CPHA)
SPI 通过两个参数定义时钟极性和数据采样时机:
| 参数 | 全称 | 含义 |
|---|---|---|
| CPOL | Clock Polarity | 时钟空闲时的电平:0=低,1=高 |
| CPHA | Clock Phase | 数据采样边沿:0=第一个边沿,1=第二个边沿 |
Mode 0 (CPOL=0, CPHA=0) —— 最常用
SCLK: ──┐ ┌───┐ ┌───
│ │ │ │
└───┘ └───┘
MOSI/MISO: ──[D7][D6][D5]... ← 在上升沿改变数据,下降沿采样
↑
空闲为低
Mode 3 (CPOL=1, CPHA=1)
SCLK: ┌───┐ ┌───┐ ┌──
│ │ │ │ │
┘ └───┘ └───┘
MOSI/MISO: ──[D7][D6][D5]... ← 在上升沿采样(第二个边沿)
| Mode | CPOL | CPHA | 空闲电平 | 采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低 | 上升沿(第1个边沿) |
| 1 | 0 | 1 | 低 | 下降沿(第2个边沿) |
| 2 | 1 | 0 | 高 | 下降沿(第1个边沿) |
| 3 | 1 | 1 | 高 | 上升沿(第2个边沿) |
关键:Master 和 Slave 的 CPOL/CPHA 必须匹配,否则数据会在错误的时刻被采样。
为什么需要四种模式(CPOL/CPHA)? 不同芯片厂商设计 SPI 接口时有不同的时钟偏好——有些芯片的内部逻辑在时钟上升沿锁存数据,有些在下降沿锁存。CPOL/CPHA 的四种组合为 SPI 提供了兼容任意芯片的灵活性。如果没有这四种模式,遇到时钟极性不匹配的芯片就需要外加反相器电路来翻转时钟。这四种模式不是"可选配置",而是 SPI 协议的核心设计——一次配置正确,即可连接任何 SPI 从设备。
4. SPI 通信流程
一次完整的 SPI 传输:
① Master 拉低 CS
② Master 在 SCLK 上产生时钟
③ Master 和 Slave 同时通过移位寄存器交换数据(全双工)
- Master 的 MOSI 数据逐位移入 Slave
- Slave 的 MISO 数据逐位移入 Master
④ 传输完成,Master 拉高 CS
Master 移位寄存器 Slave 移位寄存器
┌──────────────────┐ ┌──────────────────┐
│ D7 D6 D5 ... D0 │──MOSI──→ │ D7 D6 D5 ... D0 │
│ │←─MISO── │ │
└──────────────────┘ └──────────────────┘
↑ SCLK (由 Master 产生)
每来一个时钟脉冲,Master 和 Slave 的寄存器
都右移一位,同时最高位被发送出去
重要特性:SPI 的发送和接收是同步进行的。即使你只想读数据,也必须发送同样数量的字节(通常是 0x00 或 0xFF 作为"空字节"),才能把 MISO 上的数据"时钟出来"。
5. HAL 库的 SPI 操作
5.1 CubeMX 配置
在 CubeMX 中配置 SPI 需要关注:
| 参数 | 含义 | GSRL 典型值 |
|---|---|---|
| Mode | 主/从模式 | Full-Duplex Master |
| NSS | 硬件/软件片选 | Software(用 GPIO 模拟) |
| Data Size | 数据帧大小 | 8 Bits |
| CPOL / CPHA | 时钟模式 | 根据从设备数据手册 |
| Baud Rate Prescaler | 时钟分频 | 影响 SCLK 频率 |
| First Bit | 先发 MSB 还是 LSB | MSB First |
5.2 传输模式
HAL 提供四种 SPI 传输 API:
// 阻塞模式:发送 + 接收(分别调用)
HAL_SPI_Transmit(&hspi1, txData, len, timeout);
HAL_SPI_Receive(&hspi1, rxData, len, timeout);
// 阻塞模式:全双工(同时收发,效率高)
HAL_SPI_TransmitReceive(&hspi1, txData, rxData, len, timeout);
// 中断模式:全双工
HAL_SPI_TransmitReceive_IT(&hspi1, txData, rxData, len);
// DMA 模式:全双工(GSRL 推荐,零 CPU 开销)
HAL_SPI_TransmitReceive_DMA(&hspi1, txData, rxData, len);
5.3 GSRL Driver 封装
drv_spi.c 对 HAL SPI API 做了统一封装,并实现了 HAL 回调:
// 初始化 SPI 驱动
void SPI_Init(SPI_HandleTypeDef *hspi, SPI_Call_Back rxCallbackFunction);
// DMA 全双工传输
HAL_StatusTypeDef SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi,
uint8_t *pTxData, uint16_t exchangeDataLength);
// HAL 传输完成回调 → 调用用户注册的回调
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
SPI_Manage_Object_t *spi_obj = SPI_Get_Object(hspi);
if (spi_obj->rxCallbackFunction != NULL) {
spi_obj->rxCallbackFunction(spi_obj->rxBuffer, spi_obj->exchangeDataLength);
}
}
6. GSRL 中的 SPI 应用:BMI088 IMU
6.1 BMI088 的 SPI 接口
BMI088 是一款 6 轴 IMU(3 轴加速度计 + 3 轴陀螺仪),其加速度计和陀螺仪是独立的两颗芯片,各自有独立的 SPI 接口和 CS 引脚:
STM32 BMI088
┌────────┐ ┌──────────────┐
│ │──SCLK──┬─────│SCLK_ACC │
│ SPI1 │──MOSI──┤ │ │
│ │──MISO──┼─────│MISO (共用) │
│ │ │ │ │
│ GPIO │──CS1───┤ │CS_ACC │ ← 加速度计 CS
│ GPIO │──CS2───┘ │CS_GYRO │ ← 陀螺仪 CS
└────────┘ └──────────────┘
6.2 逐寄存器读写
BMI088 使用典型的 SPI 寄存器读写协议:
读寄存器格式(发送 2 字节,接收 1 字节有用数据):
Byte0: [0│R/W=1│A6 A5 A4 A3 A2 A1 A0] ← 寄存器地址,bit7=1 表示读
Byte1: 任意值(用于把数据时钟出来)
收到: Byte1 = 寄存器的值
写寄存器格式(发送 2 字节):
Byte0: [0│R/W=0│A6 A5 A4 A3 A2 A1 A0] ← 寄存器地址,bit7=0 表示写
Byte1: 要写入的值
6.3 GSRL 中的 IMU 对象
BMI088 类(dvc_imu.hpp)封装了 SPI 通信细节:
struct SPIConfig {
SPI_HandleTypeDef *hspi; // SPI 句柄(如 &hspi1)
GPIO_TypeDef *csGPIOGroup; // CS 引脚 GPIO 端口
uint16_t csGPIOPin; // CS 引脚编号
};
class BMI088 : public IMU
{
SPIConfig m_accelSPIConfig; // 加速度计 SPI 配置
SPIConfig m_gyroSPIConfig; // 陀螺仪 SPI 配置
// 内部实现
void readSingleReg(const SPIConfig &cfg, uint8_t reg, uint8_t &rxData);
void readMultiReg(const SPIConfig &cfg, uint8_t reg, uint8_t *pRxData, uint8_t len);
};
读取寄存器的实际过程:
void BMI088::readSingleReg(const SPIConfig &cfg, uint8_t reg, uint8_t &rxData)
{
uint8_t txData[2] = {static_cast<uint8_t>(reg | 0x80), 0xFF}; // 读命令
uint8_t rxDataBuf[2];
HAL_GPIO_WritePin(cfg.csGPIOGroup, cfg.csGPIOPin, GPIO_PIN_RESET); // 拉低 CS
HAL_SPI_TransmitReceive(cfg.hspi, txData, rxDataBuf, 2, 100); // 全双工传输
HAL_GPIO_WritePin(cfg.csGPIOGroup, cfg.csGPIOPin, GPIO_PIN_SET); // 拉高 CS
rxData = rxDataBuf[1]; // 第 2 个字节是有效的寄存器值
}
7. SPI 对比 UART
| 特性 | SPI | UART |
|---|---|---|
| 时钟 | 独立时钟线(同步) | 无时钟(异步) |
| 连线数 | 4 根(SCLK/MOSI/MISO/CS) | 2 根(TX/RX) |
| 拓扑 | 一主多从 | 一对一 |
| 最大速率 | 几十 Mbps | 几 Mbps |
| 全双工 | 是 | 是 |
| 从设备选择 | CS 信号 | 地址协议(如 RS-485 地址位) |
| 应用 | IMU、Flash、SD 卡 | 调试、遥控器、GPS |
8. 总结
| 要点 | 说明 |
|---|---|
| 同步通信 | 独立的 SCLK 时钟线,收发同步 |
| 四根信号线 | SCLK + MOSI + MISO + CS |
| 全双工 | 发送和接收同时进行 |
| CPOL/CPHA | 定义时钟极性和采样边沿,共 4 种模式 |
| CS 信号 | 低电平选中从设备,一主多从的关键 |
| GSRL 应用 | SPI1/SPI2 连接 BMI088 加速度计+陀螺仪 |
与 GSRL 的关系
GSRL 的 BMI088 IMU 通过 SPI 通信,drv_spi.c 封装了 HAL SPI API,dvc_imu.hpp 中的 BMI088 类在 SPI 驱动之上实现了寄存器的读写、校准和姿态解算。
下一篇:I2C 通信原理与实践
作者: Qing | 修改日期: 2026-07-18