17.1 为什么需要"总线类比"
初学者最大的困惑是:I²C 有 i2c_driver/i2c_client,SPI 有 spi_driver/spi_device, USB 有 usb_driver/usb_interface,它们名字不同、API 不同,但骨架完全一样:
任何总线子系统都是这三层:
┌──────────────────────────────────────────────┐
│ 总线核心 (core) :注册、匹配、设备模型 │
├──────────────────────────────────────────────┤
│ 控制器驱动 (host/ctrl):操作 SoC 上的控制器 │
├──────────────────────────────────────────────┤
│ 设备驱动 (client/device):操作具体芯片 │
└──────────────────────────────────────────────┘
只要掌握了这条"三横"主线,学新总线就是查三张表:核心结构、控制器 API、设备驱动 API。
17.2 SPI 子系统
SPI(Serial Peripheral Interface)是四线同步串行总线:
| 信号 | 全称 | 作用 |
|---|---|---|
| SCLK | Serial Clock | 时钟,由主机产生 |
| MOSI | Master Out Slave In | 主机→设备 |
| MISO | Master In Slave Out | 设备→主机 |
| CS/SS | Chip Select | 片选,每个设备一根 |
与 I²C 的关键差异:
| 特性 | I²C | SPI |
|---|---|---|
| 线数 | 2(+地址) | 4(每设备一根 CS) |
| 寻址 | 7/10 位地址 | 片选(无地址) |
| 速率 | 100k~5M | 数 MHz~上百 MHz |
| 双工 | 半双工(典型) | 全双工 |
| 模式 | 固定 | CPOL/CPHA 四种模式 |
| 多设备 | 地址区分 | 片选区分 |
| 确认 | ACK/NACK | 无 |
| 典型器件 | EEPROM/RTC/PMIC | Flash/显示屏/ADC/以太网 |
17.2.1 核心数据结构
cpp
/* include/linux/spi/spi.h */
/* 控制器(主机)------ 7.2.5 叫 spi_controller,不再叫 spi_master */
struct spi_controller {
struct device dev;
struct list_head list;
s16 bus_num;
u16 num_chipselect;
u16 mode_bits;
u32 min_speed_hz, max_speed_hz;
int (*setup)(struct spi_device *spi);
int (*transfer)(struct spi_device *spi, struct spi_message *msg);
int (*transfer_one)(struct spi_controller *ctlr,
struct spi_device *spi,
struct spi_transfer *xfer);
...
};
/* 设备 */
struct spi_device {
struct device dev;
struct spi_controller *controller;
struct spi_driver *driver;
u32 max_speed_hz;
u8 chip_select; /* 用哪根 CS */
u8 bits_per_word;
u32 mode; /* SPI_CPOL/SPI_CPHA/SPI_CS_HIGH... */
int irq;
...
};
/* 驱动 */
struct spi_driver {
const struct spi_device_id *id_table;
int (*probe)(struct spi_device *spi); /* 单参数 */
void (*remove)(struct spi_device *spi); /* 返回 void */
void (*shutdown)(struct spi_device *spi);
struct device_driver driver;
};
/* 一次传输 */
struct spi_transfer {
const void *tx_buf;
void *rx_buf;
unsigned len;
dma_addr_t tx_dma, rx_dma;
u32 speed_hz;
u16 delay_usecs;
u8 bits_per_word;
u8 cs_change;
...
};
struct spi_message {
struct list_head transfers; /* 一组 spi_transfer */
struct spi_device *spi;
void (*complete)(void *context);
...
};
7.2.5 命名变化 :spi_master 已重命名为 spi_controller , spi_alloc_master() 变成 spi_alloc_host(),spi_master_get()/spi_master_put() 变成 spi_controller_get()/spi_controller_put()。旧的 spi_master 名字是兼容别名,但新代码应统一用 spi_controller。
17.2.2 控制器驱动
cpp
#include <linux/spi/spi.h>
#include <linux/platform_device.h>
static int myspi_transfer_one(struct spi_controller *ctlr,
struct spi_device *spi,
struct spi_transfer *xfer)
{
/* 把 xfer->tx_buf 发出去,把收到的放到 xfer->rx_buf */
return my_hw_xfer(ctlr, spi, xfer);
}
static int myspi_probe(struct platform_device *pdev)
{
struct spi_controller *ctlr;
ctlr = devm_spi_alloc_host(&pdev->dev, sizeof(struct my_priv));
if (!ctlr)
return -ENOMEM;
ctlr->bus_num = -1; /* 动态分配 */
ctlr->num_chipselect = 4;
ctlr->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;
ctlr->transfer_one = myspi_transfer_one;
ctlr->dev.of_node = pdev->dev.of_node;
ctlr->bits_per_word_mask = SPI_BPW_MASK(8) | SPI_BPW_MASK(16);
return devm_spi_register_controller(&pdev->dev, ctlr);
}
static void myspi_remove(struct platform_device *pdev) { }
static const struct of_device_id myspi_of_match[] = {
{ .compatible = "myvendor,myspi" },
{ }
};
MODULE_DEVICE_TABLE(of, myspi_of_match);
static struct platform_driver myspi_driver = {
.probe = myspi_probe,
.remove = myspi_remove,
.driver = { .name = "myspi", .of_match_table = myspi_of_match },
};
module_platform_driver(myspi_driver);
控制器 API 对比:
| 4.0 | 7.2.5 | 说明 |
|---|---|---|
spi_alloc_master() |
spi_alloc_host() |
分配控制器 |
手动 spi_master_put() |
devm_spi_alloc_host() |
推荐 |
spi_register_master() |
spi_register_controller() |
注册 |
spi_unregister_master() |
spi_unregister_controller() |
注销 |
master->transfer |
ctlr->transfer / transfer_one |
传输实现 |
struct spi_master |
struct spi_controller |
类型重命名 |
transfer 与 transfer_one 的区别:
transfer():整个spi_message交给你,你负责拆解、CS 控制、DMA。transfer_one():核心层帮你管 CS 和消息循环,你只管一个spi_transfer。 新驱动优先实现transfer_one。
17.2.3 设备驱动
cpp
#include <linux/spi/spi.h>
#include <linux/module.h>
#include <linux/regmap.h>
static int mysensor_probe(struct spi_device *spi)
{
struct regmap *regmap;
int val;
/* 1. 设置模式与速度 */
spi->mode = SPI_MODE_0;
spi->max_speed_hz = 1000000;
spi->bits_per_word = 8;
if (spi_setup(spi))
return -EINVAL;
/* 2. 单次同步传输(寄存器读) */
{
u8 tx[2] = { 0x80 | 0x00, 0x00 }; /* 读 0x00,高位=1 */
u8 rx[2] = { 0 };
struct spi_transfer t = {
.tx_buf = tx, .rx_buf = rx, .len = 2,
};
struct spi_message m;
spi_message_init(&m);
spi_message_add_tail(&t, &m);
if (spi_sync(spi, &m))
return -EIO;
val = rx[1];
dev_info(&spi->dev, "reg0 = 0x%02x\n", val);
}
/* 3. 现代做法:用 regmap */
{
static const struct regmap_config cfg = {
.reg_bits = 8,
.val_bits = 8,
.read_flag_mask = 0x80,
.max_register = 0x7f,
.cache_type = REGCACHE_MAPLE,
};
regmap = devm_regmap_init_spi(spi, &cfg);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
}
return 0;
}
static void mysensor_remove(struct spi_device *spi) { }
static const struct spi_device_id mysensor_id[] = {
{ "mysensor", 0 }, { }
};
MODULE_DEVICE_TABLE(spi, mysensor_id);
static const struct of_device_id mysensor_of_match[] = {
{ .compatible = "myvendor,mysensor" }, { }
};
MODULE_DEVICE_TABLE(of, mysensor_of_match);
static struct spi_driver mysensor_driver = {
.driver = { .name = "mysensor", .of_match_table = mysensor_of_match },
.probe = mysensor_probe,
.remove = mysensor_remove,
.id_table = mysensor_id,
};
module_spi_driver(mysensor_driver);
MODULE_LICENSE("GPL");
SPI 设备驱动 API 速查:
| API | 作用 |
|---|---|
spi_setup(spi) |
应用 mode/speed/bits 设置 |
spi_sync(spi, msg) |
同步执行消息(可睡眠) |
spi_sync_locked(spi, msg) |
已持控制器锁时使用 |
spi_async(spi, msg) |
异步执行,msg->complete 回调 |
spi_write(spi, buf, len) |
同步写封装 |
spi_read(spi, buf, len) |
同步读封装 |
spi_write_then_read(spi, tx, txlen, rx, rxlen) |
先写后读(最常用) |
spi_message_init(m) / spi_message_add_tail(t, m) |
组装消息 |
spi_new_device(ctlr, &info) |
动态创建设备 |
spi_add_device(spi) |
添加已分配的 spi_device |
spi_register_driver(drv) / module_spi_driver(drv) |
注册驱动 |
17.2.4 在 QEMU 上做 SPI 实验
SPI 没有像 i2c-stub 那样的"假控制器",但有三种可用路径:
路径一:spi-gpio(GPIO 模拟 SPI,最通用)
drivers/spi/spi-gpio.c(CONFIG_SPI_GPIO)用 4 根 GPIO 位脉冲模拟 SPI 主机。 配合 gpio-sim 就能在 QEMU 里凭空造出一块 SPI 控制器:
bash
CONFIG_SPI=y
CONFIG_SPI_GPIO=y
CONFIG_SPI_SPIDEV=y
CONFIG_GPIO_SIM=m
CONFIG_GPIO_VIRTIO=y # 若 QEMU 提供 virtio-gpio
cpp
/* 用 device tree overlay 或直接在 dts 里描述 */
spi_gpio: spi-gpio {
compatible = "spi-gpio";
sck-gpios = <&gpio0 1 GPIO_ACTIVE_HIGH>;
mosi-gpios = <&gpio0 2 GPIO_ACTIVE_HIGH>;
miso-gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>;
cs-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>;
num-chipselects = <1>;
spidev@0 {
compatible = "rohm,dh2228fv"; /* spidev 的通用 compatible */
reg = <0>;
spi-max-frequency = <1000000>;
};
};
路径二:spi-virtio(QEMU 原生 virtio SPI)
drivers/spi/spi-virtio.c(CONFIG_SPI_VIRTIO)对应 QEMU 的 virtio-spi 设备 (新版本 QEMU 支持)。检查:
bash
qemu-system-aarch64 -machine virt -device help | grep -i spi
路径三:spi-loopback-test(内核自带测试驱动,最省事)
drivers/spi/spi-loopback-test.c(CONFIG_SPI_LOOPBACK_TEST)是一个内核测试模块 , 它要求 SPI 控制器支持"回环"(把 MOSI 接到 MISO)。配合 spi-gpio 或 支持回环的虚拟控制器,可以自动验证控制器驱动的传输正确性。
用户空间访问:spidev
drivers/spi/spidev.c(CONFIG_SPI_SPIDEV)提供 /dev/spidev<bus>.<cs>:
bash
ls /dev/spidev*
# 用 spidev_test(内核源码 tools/spi/spidev_test.c)测试
gcc -o spidev_test tools/spi/spidev_test.c
./spidev_test -D /dev/spidev0.0 -v
用 spidev 的 ioctl :SPI_IOC_MESSAGE(N)、SPI_IOC_WR_MODE、SPI_IOC_WR_MAX_SPEED_HZ、 SPI_IOC_WR_BITS_PER_WORD(include/uapi/linux/spi/spidev.h)。
17.3 I2C、SPI、USB 驱动架构横向对比
17.3.1 三张表:核心结构、控制器、设备驱动
| 维度 | I²C | SPI | USB(主机侧) |
|---|---|---|---|
| 控制器结构 | i2c_adapter |
spi_controller |
usb_hcd |
| 控制器驱动结构 | i2c_algorithm |
transfer_one 回调 |
hc_driver |
| 设备结构 | i2c_client |
spi_device |
usb_interface |
| 驱动结构 | i2c_driver |
spi_driver |
usb_driver |
| 驱动 probe 签名 | probe(client) |
probe(spi) |
probe(intf, id) |
| 驱动 remove 返回 | void |
void |
void |
| 注册宏 | module_i2c_driver() |
module_spi_driver() |
module_usb_driver() |
| 传输载体 | struct i2c_msg |
struct spi_transfer |
struct urb |
| 传输 API | i2c_transfer() |
spi_sync()/spi_async() |
usb_submit_urb() |
| 寄存器抽象 | regmap |
regmap |
regmap(部分设备) |
| 匹配表类型 | i2c_device_id |
spi_device_id |
usb_device_id |
| 设备树匹配 | of_match_table |
of_match_table |
少见(USB 用 VID/PID) |
| 用户空间 | /dev/i2c-N |
/dev/spidevB.C |
/dev/bus/usb/BBB/DDD |
17.3.2 "从芯片眼里看 Linux 世界"
原书 17.2 讲的"I²C 主机和外设眼里的 Linux 世界",本质是同一颗芯片在不同总线上, 驱动代码长什么样。下面用同一颗假想"温度传感器"演示:
cpp
/* ---- I2C 版本 ---- */
static int ts_probe(struct i2c_client *client)
{
struct regmap *map = devm_regmap_init_i2c(client, &cfg);
regmap_read(map, REG_TEMP, &val);
return 0;
}
/* ---- SPI 版本 ---- */
static int ts_probe(struct spi_device *spi)
{
struct regmap *map = devm_regmap_init_spi(spi, &cfg);
regmap_read(map, REG_TEMP, &val);
return 0;
}
/* ---- USB 版本(HID 类,走 hid 框架)---- */
static int ts_probe(struct usb_interface *intf, const struct usb_device_id *id)
{
usb_control_msg(...); /* 或走 hid_input_report */
return 0;
}
规律 :芯片业务逻辑(读温度、算补偿、上报 input 事件)完全一样; 变化的只是"怎么把寄存器读写请求送到芯片"------这就是总线子系统的全部价值。
17.3.3 三类驱动的"主机/外设"心智模型
I2C SPI
┌──────────────┐ ┌──────────────┐
│ 主机 SoC │ │ 主机 SoC │
│ i2c_adapter │ │ spi_controller│
└──────┬───────┘ └──────┬───────┘
│ SCL/SDA │ SCLK/MOSI/MISO/CS
┌────┴────┬────────┐ ┌────┴────┬────────┐
┌─┴──┐ ┌─┴──┐ ┌──┴─┐ ┌─┴──┐ ┌─┴──┐ ┌──┴─┐
│0x50│ │0x48│ │0x68│ │CS0 │ │CS1 │ │CS2 │
│EE │ │温度│ │RTC │ │Flash│ │ADC │ │Disp│
└────┘ └────┘ └────┘ └────┘ └────┘ └────┘
每个从设备有地址 每个从设备有片选
USB 不同:它是主机发起、设备枚举的树形总线,主机控制器下挂 Hub, Hub 下再挂设备,最多 127 个地址(7 位),由主机在枚举时分配。
17.4 常见错误
| 错误 | 现象 | 修正 |
|---|---|---|
用 struct spi_master |
编译告警/风格问题 | 7.2.5 用 spi_controller |
spi_alloc_master() 新代码 |
已弃用 | 用 spi_alloc_host() |
spi_driver.probe 两参数 |
编译错误 | 单参数 probe(spi) |
spi_driver.remove 返回 int |
编译错误 | 返回 void |
忘记 spi_setup() |
用默认低速/错误模式 | probe 里先设置 |
spi_sync() 在原子上下文 |
睡眠警告 | 原子上下文用 spi_async() |
spidev 无 reg |
设备创建失败 | dts 里必须给 reg(片选号) |
把 spi_transfer 放栈上后异步 |
UAF | 异步传输的 transfer 必须持久 |
USB 用 probe(client) 写法 |
编译错误 | USB 仍是两参数 |
17.5 本章实验清单
| 实验 | 目的 | 关键观察 |
|---|---|---|
| spidev 基础读写 | 学习 SPI 用户空间接口 | /dev/spidev0.0 + spidev_test |
| spi-gpio 控制器 | 学习 transfer_one |
/sys/bus/spi/devices/ |
| spi-loopback-test | 验证控制器驱动 | 测试模块自动跑 |
| spi-virtio(若可用) | 学习真实控制器 | qemu-system-aarch64 -device virtio-spi |
| regmap over SPI | 学习统一寄存器抽象 | /sys/kernel/debug/regmap/ |
| I²C/SPI 同芯片对比 | 建立类比心智 | 两版 probe 业务逻辑相同 |
| 三条总线结构表 | 建立总线子系统模型 | 手写三张 API 表 |
17.6 总结
- 一切总线子系统都是"核心 + 控制器驱动 + 设备驱动"三层。
- SPI 控制器在 7.2.5 叫
spi_controller(不再是spi_master), 分配用spi_alloc_host(),注册用spi_register_controller()。 - SPI 新驱动优先实现
transfer_one,把 CS 和消息循环交给核心层。 spi_driver.probe是单参数 、remove返回void------与 I²C 一致,与 USB 不同。regmap是 I²C/SPI 设备驱动的统一寄存器抽象,能大幅减少重复代码。- 无开发板时,SPI 用
spi-gpio+gpio-sim或spi-loopback-test;spidev提供用户空间/dev/spidevB.C。 - 芯片业务逻辑与总线无关;总线只负责"把寄存器读写送过去"。
- 原书第 17 章的"架构类比"知识点,由本章的 SPI 完整章节 + 三总线对比表完整承接。