17-SPI 驱动与 I2C/SPI/USB 架构类比

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 总结

  1. 一切总线子系统都是"核心 + 控制器驱动 + 设备驱动"三层。
  2. SPI 控制器在 7.2.5 叫 spi_controller (不再是 spi_master), 分配用 spi_alloc_host(),注册用 spi_register_controller()。
  3. SPI 新驱动优先实现 transfer_one,把 CS 和消息循环交给核心层。
  4. spi_driver.probe 是单参数 、remove 返回 void------与 I²C 一致,与 USB 不同。
  5. regmap 是 I²C/SPI 设备驱动的统一寄存器抽象,能大幅减少重复代码。
  6. 无开发板时,SPI 用 spi-gpio + gpio-sim 或 spi-loopback-test ; spidev 提供用户空间 /dev/spidevB.C。
  7. 芯片业务逻辑与总线无关;总线只负责"把寄存器读写送过去"。
  8. 原书第 17 章的"架构类比"知识点,由本章的 SPI 完整章节 + 三总线对比表完整承接。
相关推荐
Snasph1 天前
09-设备驱动中的异步通知与异步 I/O
linux 驱动学习
Snasph1 天前
21-Linux 设备驱动的调试
linux 驱动学习
Snasph1 天前
22-QEMU 实验总览
linux 驱动学习
Snasph1 天前
13-Linux 块设备驱动
linux 驱动学习
Snasph1 天前
15- Linux I2C 核心、总线与设备驱动
linux 驱动学习
Snasph1 天前
附录-API 速查与术语表
linux 驱动学习
Snasph1 天前
11-内存与 I/O 访问
linux 驱动学习
Snasph2 天前
14-Linux 网络设备驱动
linux 驱动学习
Snasph2 天前
18-ARM Linux 设备树
linux 驱动学习