SPI¶
SPI(Serial Peripheral Interface)是高速同步串行总线,由 Motorola 提出。速度可达几十 Mbps,是板内连接高速外设(Flash、LCD、ADC、编码器)的首选。
一、原理¶
信号线定义¶
| 信号线 | 方向 | 说明 |
|---|---|---|
| SCLK(SCK) | 主→从 | 时钟,由主机产生 |
| MOSI | 主→从 | Master Out Slave In,主机发数据 |
| MISO | 从→主 | Master In Slave Out,从机发数据 |
| CS / NSS | 主→从 | 片选,低电平选中该从机(每个从机一根) |
主机 (Master) 从机 (Slave)
SCLK ─────────────────► SCLK
MOSI ─────────────────► MOSI
MISO ◄───────────────── MISO
CS0 ─────────────────► CS
全双工
SPI 是全双工:每个 CLK 时钟沿,主机和从机同时各发 1 bit。发送和接收同步进行。
四种模式(CPOL / CPHA)¶
| 模式 | CPOL | CPHA | 空闲时钟 | 采样沿 | 常用芯片 |
|---|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低 | 上升沿 | MPU6050、W25Q Flash |
| Mode 1 | 0 | 1 | 低 | 下降沿 | 部分 ADC |
| Mode 2 | 1 | 0 | 高 | 下降沿 | 少见 |
| Mode 3 | 1 | 1 | 高 | 上升沿 | AS5048B 磁编码器 |
Mode 0(最常用):
SCLK ‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_
MOSI ─X 7 X 6 X 5 X 4 X 3 X 2 X 1 X 0 X─
MISO ─X D7 X D6 X D5 X D4 X D3 X D2 X D1 X D0X─
↑采样(上升沿)
多从机连接¶
主机
├── SCLK ──────┬──────┬──────┐
├── MOSI ──────┼──────┼──────┤
├── MISO ──────┼──────┼──────┤
├── CS0 ────── 从机0 │ │
├── CS1 ──────────── 从机1 │
└── CS2 ─────────────────── 从机2
二、STM32 使用¶
CubeMX 配置¶
- SPI1 → Mode:
Full-Duplex Master - Data Size:
8 Bits,First Bit:MSB First - Prescaler 决定时钟速率(APB2/Prescaler)
- CPOL / CPHA 根据从机数据手册设置(通常 0/0)
- NSS:选
Software(软件控制 CS,灵活) - 对应引脚:PA5(SCK),PA6(MISO),PA7(MOSI),CS 用普通 GPIO 输出
HAL 库基础操作¶
// CS 控制宏(CS 用 GPIO 输出引脚)
#define CS_LOW() HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET)
#define CS_HIGH() HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET)
/* ---- 发送 / 接收 ---- */
uint8_t spi_transfer(uint8_t tx_data) {
uint8_t rx_data;
HAL_SPI_TransmitReceive(&hspi1, &tx_data, &rx_data, 1, 10);
return rx_data;
}
/* ---- 读寄存器(典型:写地址,读数据)---- */
uint8_t spi_read_reg(uint8_t reg_addr) {
uint8_t tx[2] = {reg_addr | 0x80, 0x00}; // MSB=1 表示读(AS5048B 规则)
uint8_t rx[2];
CS_LOW();
HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 10);
CS_HIGH();
return rx[1];
}
/* ---- 写寄存器 ---- */
void spi_write_reg(uint8_t reg_addr, uint8_t value) {
uint8_t tx[2] = {reg_addr & 0x7F, value}; // MSB=0 表示写
CS_LOW();
HAL_SPI_Transmit(&hspi1, tx, 2, 10);
CS_HIGH();
}
/* ---- DMA 方式(大数据量,如读 Flash)---- */
uint8_t tx_buf[256], rx_buf[256];
CS_LOW();
HAL_SPI_TransmitReceive_DMA(&hspi1, tx_buf, rx_buf, 256);
// 在 HAL_SPI_TxRxCpltCallback 中处理完成
实战:AS5048B 磁编码器¶
AS5048B 是机器人关节常用的 14 位磁角度传感器,SPI Mode 1。
#include <math.h>
#define AS5048_REG_ANGLE 0x3FFF
uint16_t as5048_read_angle(void) {
// AS5048B 使用 16-bit SPI 帧
uint8_t tx[2] = {
(AS5048_REG_ANGLE >> 8) | 0x40, // 读命令标志
AS5048_REG_ANGLE & 0xFF
};
uint8_t rx[2];
// 第一帧:发送读请求(SPI Mode 1 / Mode 3)
CS_LOW();
HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 10);
CS_HIGH();
HAL_Delay(1);
// 第二帧:读取数据(流水线模式)
CS_LOW();
HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 10);
CS_HIGH();
uint16_t raw = ((uint16_t)(rx[0] & 0x3F) << 8) | rx[1];
return raw; // 0 ~ 16383 对应 0 ~ 360°
}
float as5048_get_angle_deg(void) {
return (float)as5048_read_angle() / 16383.0f * 360.0f;
}
实战:W25Q128 SPI Flash 读写¶
#define W25Q_CMD_READ_ID 0x9F
#define W25Q_CMD_READ_DATA 0x03
#define W25Q_CMD_WRITE_EN 0x06
#define W25Q_CMD_PAGE_PROG 0x02
#define W25Q_CMD_SECTOR_ERASE 0x20
void w25q_read_id(uint8_t *id) {
uint8_t cmd = W25Q_CMD_READ_ID;
CS_LOW();
HAL_SPI_Transmit(&hspi1, &cmd, 1, 10);
HAL_SPI_Receive(&hspi1, id, 3, 10);
CS_HIGH();
}
void w25q_read(uint32_t addr, uint8_t *buf, uint16_t len) {
uint8_t cmd[4] = {
W25Q_CMD_READ_DATA,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
(addr >> 0) & 0xFF
};
CS_LOW();
HAL_SPI_Transmit(&hspi1, cmd, 4, 10);
HAL_SPI_Receive(&hspi1, buf, len, 100);
CS_HIGH();
}
三、Linux 使用¶
使能 SPI 设备¶
# 树莓派:/boot/config.txt 中确认
dtparam=spi=on
# 查看 SPI 设备节点
ls /dev/spidev*
# /dev/spidev0.0 (SPI0, CS0)
# /dev/spidev0.1 (SPI0, CS1)
# 安装 Python 库
pip install spidev
Python spidev 基础¶
import spidev
import time
spi = spidev.SpiDev()
spi.open(0, 0) # bus=0, device=0 → /dev/spidev0.0
spi.max_speed_hz = 1000000 # 1 MHz
spi.mode = 0b00 # Mode 0 (CPOL=0, CPHA=0)
spi.bits_per_word = 8
# 发送并接收(全双工)
rx = spi.xfer2([0x80, 0x00]) # 发送 2 字节,同时接收 2 字节
print(f"收到: {rx}")
# 只发送(接收数据丢弃)
spi.writebytes([0x06])
# 分段传输(CS 保持低电平)
spi.xfer([0x03, 0x00, 0x00, 0x00]) # CS 每字节后拉高(不适合大多数芯片)
spi.close()
xfer vs xfer2
spi.xfer(data):每字节间 CS 会短暂拉高(不适合大多数器件)spi.xfer2(data):整个传输过程 CS 保持低电平(通常用这个)
读 AS5048B 磁编码器(Python)¶
import spidev
import struct
class AS5048B:
REG_ANGLE = 0x3FFF
def __init__(self, bus=0, device=0, speed=1_000_000):
self.spi = spidev.SpiDev()
self.spi.open(bus, device)
self.spi.max_speed_hz = speed
self.spi.mode = 0b01 # Mode 1
self.spi.bits_per_word = 8
def _spi_frame(self, cmd: int) -> int:
"""发送 16-bit 帧并返回响应"""
tx = [(cmd >> 8) & 0xFF, cmd & 0xFF]
rx = self.spi.xfer2(tx)
return ((rx[0] & 0x3F) << 8) | rx[1]
def read_raw(self) -> int:
"""读取原始角度值 0~16383"""
# 第一帧:发读地址
self._spi_frame(self.REG_ANGLE | 0x4000)
# 第二帧:获取数据(流水线)
return self._spi_frame(0xC000) # NOP 命令
def read_degrees(self) -> float:
return self.read_raw() / 16383.0 * 360.0
def close(self):
self.spi.close()
encoder = AS5048B(bus=0, device=0)
while True:
angle = encoder.read_degrees()
print(f"角度: {angle:.2f}°")
import time; time.sleep(0.01)
自定义 CS(多从机)¶
树莓派硬件 CS 只有 2 个,更多从机需用 GPIO 软件控制:
import spidev
import gpiod
import time
class SoftwareCS_SPI:
def __init__(self, bus, device, cs_chip, cs_pin, speed=1_000_000):
self.spi = spidev.SpiDev()
self.spi.open(bus, device)
self.spi.max_speed_hz = speed
self.spi.mode = 0
# 禁用硬件CS(通过固定 device=0 并手动控制)
self.spi.no_cs = True # 某些版本支持
chip = gpiod.Chip(cs_chip)
self.cs_line = chip.get_line(cs_pin)
cfg = gpiod.LineRequest()
cfg.consumer = "spi_cs"
cfg.request_type = gpiod.LineRequest.DIRECTION_OUTPUT
self.cs_line.request(cfg)
self.cs_line.set_value(1) # 默认高(未选中)
def transfer(self, data: list) -> list:
self.cs_line.set_value(0) # CS 拉低
time.sleep(0.000001) # 建立时间
rx = self.spi.xfer2(data)
time.sleep(0.000001) # 保持时间
self.cs_line.set_value(1) # CS 拉高
return rx
四、注意事项¶
接线与电气
- MISO 需要上拉/下拉:从机未被选中时 MISO 为高阻态,总线上会有噪声,通常加 10kΩ 上拉到 VCC
- CS 不用时要保持高电平:一些芯片 CS 悬空会误触发
- 电平兼容:5V 从机的 MISO 接 3.3V MCU 时需要分压或电平转换
- 走线长度:高速 SPI(> 10 MHz)信号走线要尽量短,并行线要等长
速率与稳定性
- 先用低速(100 kHz ~ 1 MHz)验证通信正确,再提速
- 示波器抓 CLK 和 MISO 波形,确认无过冲和振铃
- 长线传输时加 33Ω 串联电阻 抑制反射
- SPI 没有 ACK 机制,读回的数据是否有效需自己验证(如读 ID 寄存器对比)
调试技巧
- 先读芯片 ID,如果 ID 正确说明 SPI 基本通路没问题
- Mode 选错是最常见的问题,看数据手册的时序图确认 CPOL/CPHA
- STM32 CubeMX 生成的 SPI,NSS 引脚若配置为硬件模式,一定要单独处理,推荐改为
Software