从零写OS内核 | SPI 总线——高速串行通信
你有没有想过:一片 NOR Flash 芯片,是怎么和 CPU 交换数据的? 它没有地址线、没有复杂的主从协议,就靠 4 根线就能以几十 MHz 的速度传输数据。这就是 SPI(Serial Peripheral Interface)。
相比 I2C,SPI 更快、更简单、更适合高速场景。本文从协议原理讲到四种模式、Daisy Chain,最后给出 Linux 下用 spidev 调试的命令。
一、SPI 四种模式:CPOL 和 CPHA 的组合
SPI 有 4 种通信模式,由两个参数决定:
- CPOL(Clock Polarity):空闲时时钟电平(0=低,1=高)
- CPHA(Clock Phase):数据采样时机(0=第一个边沿,1=第二个边沿)
Mode 0 (CPOL=0, CPHA=0): 空闲低时钟,第一个边沿采样
────────────────────────────────────────────────────
SCLK ───┐ ┌──────┐ ┌──────┐ ┌──
┌────┘ └────┘ └────┘
SDA ──[D7]──[D6]──[D5]──[D4]──[D3]──[D2]──...
MOSI ↑ ↑ ↑ ↑
第一个边沿采样(CPHA=0)
Mode 3 (CPOL=1, CPHA=1): 空闲高时钟,第二个边沿采样
────────────────────────────────────────────────────
SCLK ───┘ ┌──────┐ ┌──────┐ ┌─
┌────┘ └────┘ └────┘
SDA ──[D7]──[D6]──[D5]──[D4]──[D3]──[D2]──...
↑ ↑ ↑
第二个边沿采样(CPHA=1)四种模式速查:
| 模式 | CPOL | CPHA | SCLK 空闲 | 采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低 | 上升沿 |
| 1 | 0 | 1 | 低 | 下降沿 |
| 2 | 1 | 0 | 高 | 下降沿 |
| 3 | 1 | 1 | 高 | 上升沿 |
常见芯片对应的模式:
- NOR Flash(W25Q64):Mode 0 或 Mode 3
- SD 卡:Mode 0
- 传感器(陀螺仪 MPU6000):Mode 3
二、SPI 全双工和 Daisy Chain
SPI 是全双工总线,同时有 MOSI(Master Out Slave In)和 MISO(Master In Slave Out),主设备在发送一个字节的同时也在接收一个字节。
Master Slave 1 Slave 2
│ │ │
MOSI ────────────────[D7..D0]─────────────────────────[D7..D0]──→ │
MISO ←───────────────[D7..D0]─────────────────────────[D7..D0]── │ │
│ │ │
SCLK ─────────────────────────[Clock]─────────────────────────
CS0 ───[L]──────────────────[H]──────────────────[H]
CS1 ───[H]──────────────────[H]──────────────────[L]Daisy Chain(菊花链)模式:
多个从设备串联,数据像流水线一样传递:
Master → [Slave1] → [Slave2] → [Slave3] → Master
移位寄存器 移位寄存器 移位寄存器
发 3 字节:Master 发 byte1 → Slave1 收 byte1 并把 byte2 推到 Slave2...Linux SPI Daisy Chain 例子(flashrom):
# 查看 SPI 控制器信息
$ lspci | grep SPI
00:00.2 SPI controller: Intel Corporation Device 9a13 (rev 01)
# Linux 下通过 spidev 测试
$ ls /sys/bus/spi/devices/
spi0.0 spi0.1 spi0.2
$ cat /sys/bus/spi/devices/spi0.0/modalias
spi:spi-max7315三、SPI Flash:NOR 和 NAND 的区别
嵌入式系统最常用 SPI Flash 存储固件:
| 特性 | SPI NOR Flash | SPI NAND Flash |
|---|---|---|
| 结构 | 透明或非透明 | 需要 ECC |
| 读取 | 直接随机读取(XIP) | 顺序页读取 |
| 写入 | 字节编程(Page Program) | 页编程 |
| 擦除 | 扇区擦除(4K-64K) | 块擦除(64K-128K) |
| 价格 | 高(存储代码) | 低(存储数据) |
| 特点 | 可直接执行代码(XIP) | 容量大 |
W25Q64(常见 NOR Flash,8MB)结构:
- 128 个块(64KB 每块)
- 每块 16 个扇区(4KB 每扇区)
- 支持 Dual/Quad SPI(用 2 或 4 根数据线加速)
四、Linux spidev 实战
# 1. 确认有 SPI 控制器和 spidev 驱动
$ ls /dev/spidev*
/dev/spidev0.0 /dev/spidev0.1
# 2. 查看 SPI 设备信息
$ cat /sys/bus/spi/devices/spi0.0/modalias
spi:brainboxes
# 3. 用 spidev_test 发送一个字节(读 Flash ID)
$ cat /dev/spidev0.0
# spidev_test 需要构造十六进制命令字
$ ./spidev_test -D /dev/spidev0.0 -p "9F" -s 51200000 -b 8
# 发 9F(Read JEDEC ID),读 3 字节厂商+设备 ID
# 4. 读 Flash 芯片 ID(W25Q64)
$ ./spidev_test -D /dev/spidev0.0 -p "9F" -t
# 返回:EF 40 17 = Winbond 16MB Flash ✓
# 5. 读取 Flash 前 256 字节
$ ./spidev_test -D /dev/spidev0.0 -p "03 00 00 00" -r 256 -t
# 发 0x03(Read Data)+ 3 字节地址,读取 256 字节spidev_test 源码片段(发送 Read JEDEC ID):
// spidev_test.c 片段
uint8_t tx[] = {0x9F}; // JEDEC ID 命令
uint8_t rx[3] = {0};
ioctl(fd, SPI_IOC_MESSAGE(1), &tr); // 发送并接收
printf("JEDEC ID: %02X %02X %02Xn", rx[0], rx[1], rx[2]);
// EF 40 17 → Winbond 16MB五、wandos 的 SPI 实现
wandos 目前在 arch/x86/core/spi.cpp 实现了基于 Port I/O 的 bitbang SPI:
// wandos spi.cpp(简化)
void spi_transfer(uint8_t *tx, uint8_t *rx, int len) {
for (int i = 0; i < len; i++) {
uint8_t data = tx ? tx[i] : 0xFF;
rx[i] = spi_shift_byte(data); // 移位寄存器
}
}
uint8_t spi_shift_byte(uint8_t byte) {
uint8_t rx = 0;
for (int i = 7; i >= 0; i--) {
// MOSI:字节的最高位先发
if (byte & (1 << i)) MOSI_HIGH; else MOSI_LOW;
SCK_HIGH; // 上升沿采样
udelay(1);
if (MISO_READ()) rx |= (1 << i); // 读 MISO
SCK_LOW; // 时钟拉低,准备下一位
udelay(1);
}
return rx;
}wandos SPI 的局限:
- 纯 bitbang 模拟,速度慢(< 1MHz)
- 尚未实现硬件 SPI 控制器(Intel SPI 等)
- 没有 DMA 支持
公众号 footer
相关阅读:
- 《从零写OS内核 | I2C 总线——嵌入式系统的血管》(348)
- 《从零写OS内核 | USB 协议——从 HID 到高速设备》(345)
动手环节:
如果你有树莓派或嵌入式开发板,运行 ls /dev/spidev* 看有没有 SPI 设备。然后尝试用 spidev_test 读取 Flash ID,或接一个 SPI OLED 屏幕验证通信。
仓库地址:
github.com/golang12306/os-kernel-from-scratch
关注公众号「AI不着急」,回复”资料”获取内核学习路线图。
评论