跳转至

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 配置

  1. SPI1 → Mode: Full-Duplex Master
  2. Data Size: 8 Bits,First Bit: MSB First
  3. Prescaler 决定时钟速率(APB2/Prescaler)
  4. CPOL / CPHA 根据从机数据手册设置(通常 0/0)
  5. NSS:选 Software(软件控制 CS,灵活)
  6. 对应引脚: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