从零写OS内核|349:SPI 总线——高速串行通信
摘要:SPI 是比 I2C 更高速的串行总线,常见于 Flash 存储、显示屏、传感器等场景。本篇文章详解 SPI 的四种模式、协议时序、Linux spidev 驱动、以及它在 Flash 存储中的应用。
1. SPI 协议:四根线的全双工
1.1 信号线
SPI 有 4 根核心信号线(有时 CS 有多根):
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)。
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 | 高 | 上升沿 |
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 的块设备模式。
典型 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 数据
# 用 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 -52.3 读取状态寄存器
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: SRP03. Linux spidev:用户态 SPI 设备
3.1 启用 spidev
在设备树中启用 spidev 或通过 sysfs:
$ ls /dev/spidev*
/dev/spidev0.0 /dev/spidev0.1 /dev/spidev1.0
# spidev{BUS}.{CS},例如 spidev0.0 = SPI bus 0, CS 0通过设备树注册 spidev 节点:
&spi0 {
status = "okay";
spidev@0 {
compatible = "spidev";
reg = <0>;
spi-max-frequency = <10000000>; // 10MHz
};
};3.2 用 spidev_test 测试
spidev 附带一个测试工具:
# 编译 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
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 架构
用户空间 (spidev, 驱动)
↓
内核 SPI Core (spi.c, spidev.c)
↓
SPI Controller Driver (linux_spi, spi-pxa2xx, spi-imx, spi-qup...)
↓
硬件 SPI 控制器4.2 注册 SPI 设备驱动
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
评论