从零写OS内核|349:SPI 总线——高速串行通信

摘要:SPI 是比 I2C 更高速的串行总线,常见于 Flash 存储、显示屏、传感器等场景。本篇文章详解 SPI 的四种模式、协议时序、Linux spidev 驱动、以及它在 Flash 存储中的应用。


1. SPI 协议:四根线的全双工

1.1 信号线

SPI 有 4 根核心信号线(有时 CS 有多根):

Bash
         Master                    Slave
        ┌──────┐                ┌──────┐
   CLK ─┤      ├────────────────┤      ├── CLK
  MOSI ─┤      ├────────────────┤      ├── MOSI (有时叫 SDO)
  MISO ─┤      │◄───────────────┤      ├── MISO (有时叫 SDI)
   CS ──┤      ├────────────────┤      ├── CS
        └──────┘                └──────┘
  • SCLK / CLK:时钟线,由 Master 产生
  • MOSI(Master Out Slave In):主到从的数据线
  • MISO(Master In Slave Out):从到主的数据线
  • CS / SS:片选线,可以有多根,每根选一个从设备

1.2 全双工 vs 半双工

SPI 是全双工:Master 发送 1 bit 的同时,Slave 返回 1 bit。所以 SPI 的实际吞吐量是时钟频率的 2 倍(每根数据线各传 1 bit per clock)。

Bash
Clock:   ─┐  ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌──
          └──┘  └──┘  └──┘  └──┘  └──┘  └──┘  └──┘
MOSI:    ──[D7]──[D6]──[D5]──[D4]──[D3]──[D2]──[D1]──[D0]
          ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑
MISO:    ──[R7]──[R6]──[R5]──[R4]──[R3]──[R2]──[R1]──[R0]
          ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑
       MSB先发,每bit在clock上升沿或下降沿采样(可配置)

1.3 四种模式(Mode 0/1/2/3)

SPI 有 4 种模式,由两个参数决定:

模式 CPOL(时钟极性) CPHA(时钟相位) 空闲时钟 采样边沿
Mode 0 0 0 上升沿
Mode 1 0 1 下降沿
Mode 2 1 0 下降沿
Mode 3 1 1 上升沿

Bash
Mode 0 (CPOL=0, CPHA=0):
         SCLK: ____________/
               └──┘└──┘└──┘
          MOSI: ─[D7]──[D6]──
                      ↑ 采样(上升沿)
          MISO: ─[R7]──[R6]──

Mode 1 (CPOL=0, CPHA=1):
         SCLK: ____________/
               └──┘└──┘└──┘
          MOSI: ─[D7]──[D6]──
               ↑ 采样(下降沿)

不同从设备使用不同模式,必须查阅数据手册确认。常见误区是所有 SPI 设备都兼容 Mode 0,实际很多传感器偏好 Mode 3。


2. Flash 存储:SPI 的主场

2.1 NOR Flash vs NAND Flash

SPI Flash 通常是 NOR Flash——它支持 XIP(Execute In Place,直接在 Flash 上运行代码),而不是 NAND 的块设备模式。

Bash
典型 SPI Flash 芯片:Winbond W25Q128 (16MB)

命令集:
0x06  WREN        写使能
0x04  WRDI        写禁止
0x05  RDSR        读状态寄存器
0x01  WRSR         写状态寄存器
0x03  READ        读数据 (最大 50MHz)
0x0B  FAST_READ   快速读 (最大 104MHz,带 dummy bytes)
0x02  PAGE_PROG   页编程 (256 bytes)
0x20  SECTOR_ERASE 扇区擦除 (4KB)
0xD8  BLOCK_ERASE  块擦除 (64KB)
0xC7  CHIP_ERASE  整片擦除

2.2 读取 Flash 数据

Bash
# 用 flashrom 读取 SPI Flash
$ sudo flashrom -p linux_spi:dev=/dev/spidev0.0 -r backup.bin
$ sudo flashrom -p linux_spi:dev=/dev/spidev0.0 --flash-size
Found Winbond flash chip "W25Q128.V" (16384 kB, SPI) on linux_spi.

# 读取 ID
$ sudo flashrom -p linux_spi:dev=/dev/spidev0.0 --fmap -r /dev/null 2>&1 | head -5

2.3 读取状态寄存器

Bash
Master → Slave:
  字节1: 0x05 (READ_STATUS_REGISTER)
  字节2: (clock out) → 读取返回的状态值

状态寄存器格式:
  bit 0:  WIP  (Write In Progress) - 1=编程中
  bit 1:  WEL  (Write Enable Latch) - 1=写使能
  bit 2:  BP0  (Block Protection)   - 配合 BP1/BP2 保护区域
  bit 3:  BP1
  bit 4:  BP2
  bit 7:  SRP0

3. Linux spidev:用户态 SPI 设备

3.1 启用 spidev

在设备树中启用 spidev 或通过 sysfs:

Bash
$ ls /dev/spidev*
/dev/spidev0.0  /dev/spidev0.1  /dev/spidev1.0
# spidev{BUS}.{CS},例如 spidev0.0 = SPI bus 0, CS 0

通过设备树注册 spidev 节点:

C
&spi0 {
    status = "okay";
    spidev@0 {
        compatible = "spidev";
        reg = <0>;
        spi-max-frequency = <10000000>;  // 10MHz
    };
};

3.2 用 spidev_test 测试

spidev 附带一个测试工具:

Bash
# 编译 spidev_test.c
$ gcc spidev_test.c -o spidev_test

# 发送一个字节(0x9F = 读 JEDEC ID)
$ sudo ./spidev_test -D /dev/spidev0.0 -v -p "9F"
spi mode: 0
bits per word: 8
max speed: 500000 Hz (500 KHz)

TX | 9F __ __ __ __ __ __ __ | 9F 00 00 00 00 00 00 00 |
RX | 00 00 00 00 00 00 00 00 00 | 00 EF 40 18 00 00 00 00 |
                       ↑ ↑ ↑
                   Winbond 0xEF
                   0x40 = W25Q128
                   0x18 = 128Mbit

返回 EF 40 18 就是 Winbond W25Q128 的 JEDEC ID。

3.3 用 Python 操作 spidev

Python
import spidev

spi = spidev.SpiDev()
spi.open(0, 0)  # bus 0, cs 0
spi.max_speed_hz = 10000000  # 10MHz
spi.mode = 0  # CPOL=0, CPHA=0

# 读 JEDEC ID
resp = spi.xfer2([0x9F, 0x00, 0x00, 0x00])
print(f"JEDEC: {resp[1]:02X} {resp[2]:02X} {resp[3]:02X}")
# 输出: EF 40 18

# 读 Flash ID 另一个方式
spi.xfer2([0x90, 0x00, 0x00, 0x00, 0x00, 0x00])  # Manufacture ID

# 读状态寄存器
status = spi.xfer2([0x05, 0x00])
print(f"Status: 0x{status[1]:02X}")

# 写使能
spi.xfer2([0x06])

spi.close()

4. Linux 内核 SPI 子系统

4.1 架构

Bash
用户空间 (spidev, 驱动)
    ↓
内核 SPI Core (spi.c, spidev.c)
    ↓
SPI Controller Driver (linux_spi, spi-pxa2xx, spi-imx, spi-qup...)
    ↓
硬件 SPI 控制器

4.2 注册 SPI 设备驱动

C
static int my_flash_probe(struct spi_device *spi) {
    dev_info(&spi->dev, "SPI Flash probed: %sn", dev_name(&spi->dev));
    // 设置最大速度
    spi->max_speed_hz = 10000000;
    spi->mode = SPI_MODE_0;
    spi_setup(spi);

    // 读 ID 验证
    u8 id[2];
    u8 cmd = 0x9F;
    spi_write_then_read(spi, &cmd, 1, id, 2);
    dev_info(&spi->dev, "JEDEC: %02X %02Xn", id[0], id[1]);

    return 0;
}

static const struct of_device_id my_flash_dt[] = {
    { .compatible = "winbond,w25q128" },
    { },
};

static struct spi_driver my_flash_driver = {
    .driver = {
        .name = "w25q128",
        .of_match_table = my_flash_dt,
    },
    .probe = my_flash_probe,
};

module_spi_driver(my_flash_driver);

5. I2C vs SPI 对比

特性 I2C SPI
线数 2 根 4+ 根
速率 100K-3.4M 几十 MHz
地址 总线共享(7/10位) 独立 CS
全双工 半双工 全双工
多主 支持 不支持
协议复杂度 高(有 ACK/NACK) 低(无协议层)
典型设备 传感器、RTC、EEPROM Flash、显示屏、ADC

6. wandos 对比:SPI 模拟

wandos 的 SPI 实现同样是 GPIO bit-banging(软件模拟),目前主要用于访问 SPI Flash 启动介质。在没有硬件 SPI 控制器的嵌入式平台上,这是唯一的选择。wandos 的 GPIO 输出速度限制了 SPI 速率,但足以支持低速 Flash 读取(1-10MHz)。


公众号底部

📢 关注公众号【内核栈】,获取更多 OS/内核干货

💡 本文属于「从零写OS内核」系列,欢迎回顾:

  • 篇 348:《I2C 总线——嵌入式系统的血管》
  • 篇 347:《显示器——从 Framebuffer 到 GPU》
  • 篇 346:《键盘——按键是怎么变成字符的》

🔗 回复「SPI」获取 Linux spidev 完整示例代码


作者:内核栈 | 原创出品
本文永久链接:https://kernelstack.dev/349-spi

最后修改: 2024年5月1日

作者

评论

发表评论

您的邮箱地址不会被公开。