Linux 驱动研究 —— SPI (4)

1.2 数据交互(接口层)

在上一篇文章中研究了接口层中的设备创建过程,本篇文章对数据交互的流程进行分析研究。

1.2.1 通过 write( )分析

1.2.2.1 数据交互路径说明

在开始研究之前,我们用"邮局"和"快递员"来类比 Linux 的 SPI 子系统。

  1. 什么是 spidev?(通用邮局)
    spidev 是内核提供的一个用户态访问接口 。
  • 它的角色:它是为了方便开发者在用户空间(应用程序)直接控制 SPI 设备而存在的 。
  • 使用场景:如果你在开发一个产品,连接了一个 SPI 外设,但你不想写内核驱动(因为写内核驱动调试困难、编译繁琐),你就可以直接使用 spidev 驱动 。
  • 运行机制:应用程序通过 open("/dev/spidev0.0") 打开设备,调用 write() 时,数据会通过 spidev_write 进入内核,最终通过 spi_sync 发出 。
  1. 为什么它不是"万能驱动"?
    在 Linux 的设计哲学里,"万能"意味着"低效且不专业" 。
  • 特定驱动(如 inv_mpu6050):这是专业定制驱动。它非常懂 MPU6050 芯片的寄存器结构,知道什么时候该读取、什么时候该解析数据 。它直接调用 regmap_write 或 regmap_read,这些函数最终也调用 spi_sync,但它完全不经过 spidev_write 。
  • 对比说明:
    spidev 路径:应用程序 →\rightarrow→ write() →\rightarrow→ spidev_write →\rightarrow→ spi_sync →\rightarrow→ 硬件 。
    特定驱动路径:内核驱动 →\rightarrow→ regmap_write →\rightarrow→ regmap_spi_write →\rightarrow→ spi_sync →\rightarrow→ 硬件 。
  1. spi_sync 是"SPI 控制器的唯一出入口":所有的 SPI 通信,不管是 spidev 发起的,还是传感器驱动发起的,最终必须把"快递包裹"交给 spi_sync,因为它负责直接指挥底层的 SPI 控制器硬件(如 spi-imx.c)发波形 。 spidev_write 只是"众多调用者之一":它仅仅是 spidev 这个特定驱动的入口。如果你在写传感器驱动,你完全不需要 spidev_write,直接调用 regmap 就行了,它更高效、更安全 。

1.2.2.2 数据交互分析

通过 write() 系统调用来研究接口层,本质上是在探究 "用户态的数据是如何穿越内核边界,最终触达物理总线" 的。

这里我们直接用 spidev 进行研究:

  • 什么用它研究接口层?
    它是研究的"白板":因为 spidev 不做任何逻辑加工,它是最简单、最纯粹的 SPI 使用者。如果你用它来追踪 write(),你看到的就是最原始的内核 SPI 数据流,不会被传感器驱动那复杂的寄存器逻辑所干扰。
    解耦硬件与逻辑:
    特定驱动(如 MPU6050):它"知道"芯片内部有 ID 寄存器、FIFO 寄存器,它在调用 write 时,会执行复杂的逻辑运算。
    spidev:它"不知道"你在操作什么。它只管把用户传过来的字节流,原封不动地通过 SPI 发出去。它像是一个纯净的管道。

步骤一:埋点

既然我们选择用 spidev,那么 write()自然会调用 spidev_write(),所以我们在这个函数进行日志打印,追踪调用链:

c 复制代码
static ssize_t
spidev_write(struct file *filp, const char __user *buf,
		size_t count, loff_t *f_pos)
{
	struct spidev_data	*spidev;
	ssize_t			status = 0;
	unsigned long		missing;

	pr_info("KD_LOGO:here is the function:spidev_write,belong to pid: %d",current->pid);
	dump_stack();

	/* chipselect only toggles at start or end of operation */
	if (count > bufsiz)
		return -EMSGSIZE;

	spidev = filp->private_data;

	mutex_lock(&spidev->buf_lock);
	missing = copy_from_user(spidev->tx_buffer, buf, count);
	if (missing == 0)
		status = spidev_sync_write(spidev, count);
	else
		status = -EFAULT;
	mutex_unlock(&spidev->buf_lock);

	return status;
}

步骤二:编写程序

编写程序的目的是为了验证埋点是否能捕获到真实的写操作,并观察它在内核中如何一层层传递。

先来看我们有哪些 spidev 设备:

复制代码
[root@100ask:/dev]# ls
adxl345          gpiochip3     loop6               pps1        ram7       tty12  tty3   tty47  tty7
autofs           gpiochip4     loop7               ptmx        ram8       tty13  tty30  tty48  tty8
block            gpiochip5     mem                 ptp0        ram9       tty14  tty31  tty49  tty9
bus              hwrng         memory_bandwidth    ptp1        random     tty15  tty32  tty5   ttymxc0
char             i2c-0         mmcblk0             pts         rtc        tty16  tty33  tty50  ttymxc2
console          i2c-1         mmcblk0p1           pxp_device  rtc0       tty17  tty34  tty51  ttymxc5
cpu_dma_latency  iio:device0   mmcblk1             ram0        shm        tty18  tty35  tty52  ubi_ctrl
dht11            iio:device1   mmcblk1boot0        ram1        snd        tty19  tty36  tty53  urandom
disk             input         mmcblk1boot1        ram10       spidev0.0  tty2   tty37  tty54  usb-ffs
dri              irda          mmcblk1p1           ram11       spidev0.1  tty20  tty38  tty55  v4l
ds18b20          kmsg          mmcblk1p2           ram12       spidevx    tty21  tty39  tty56  vcs
fb0              log           mmcblk1p3           ram13       stderr     tty22  tty4   tty57  vcs1
fb1              loop-control  mmcblk1rpmb         ram14       stdin      tty23  tty40  tty58  vcsa
fd               loop0         mxc_asrc            ram15       stdout     tty24  tty41  tty59  vcsa1
full             loop1         network_latency     ram2        tty        tty25  tty42  tty6   video0
fuse             loop2         network_throughput  ram3        tty0       tty26  tty43  tty60  watchdog
gpiochip0        loop3         null                ram4        tty1       tty27  tty44  tty61  watchdog0
gpiochip1        loop4         ppp                 ram5        tty10      tty28  tty45  tty62  zero
gpiochip2        loop5         pps0                ram6        tty11      tty29  tty46  tty63

编写测试程序(test_spidev.c)

这个程序将演示如何打开设备、设置 SPI 参数(可选)、并发送数据,从而触发内核的 spidev_write。

c 复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>

int main() {
    // 1. 打开设备节点
    int fd = open("/dev/spidev0.0", O_RDWR);
    if (fd < 0) {
        perror("打开 /dev/spidev0.0 失败");
        return -1;
    }

    // 2. 构造数据 (0xAA, 0x55 是典型的测试数据,电平翻转明显)
    unsigned char tx[] = {0xAA, 0x55, 0xDE, 0xAD};
    
    printf("正在通过 spidev0.0 发送数据...\n");

    // 3. 执行 write(),这将触发你在 spidev.c 中埋点的 pr_info 和 dump_stack
    ssize_t ret = write(fd, tx, sizeof(tx));
    
    if (ret < 0) {
        perror("写入失败");
    } else {
        printf("成功写入 %zd 字节\n", ret);
    }

    close(fd);
    return 0;
}

之所以 write() 会调用 spidev_write(),是因为你打开的那个"文件"(/dev/spidev0.0)与 spidev 驱动程序之间存在着一种预先建立的绑定关系。

交叉编译后把可执行文件传到开发板中执行:

root@100ask:/# ./test_spidev

正在通过 spidev0.0 发送数▒ 74.111161 KD_LOGO:here is the function:spidev_write,belong to pid: 357▒▒...

74.121392 KD_LOG:spi_sync for device: spi0.0, modalias: spidev

成功写入 4 字节

写入成功,日志成功打印,接下来查看调用链:

root@100ask:/# dmesg | grep "KD_LOGO:here is the function:spidev_write,belong to pid: 357" -A 50

74.111161 KD_LOGO:here is the function:spidev_write,belong to pid: 357

74.120872 CPU: 0 PID: 357 Comm: test_spidev Tainted: G W 4.9.88 #15

74.121095 Hardware name: Freescale i.MX6 UltraLite (Device Tree)

74.121158 \<80112a34\> (unwind_backtrace) from \<8010dc2c\> (show_stack+0x20/0x24)

74.121195 \<8010dc2c\> (show_stack) from \<80469964\> (dump_stack+0x80/0x94)

74.121232 \<80469964\> (dump_stack) from \<80667f6c\> (spidev_write+0x4c/0x168)

74.121269 \<80667f6c\> (spidev_write) from \<80254938\> (__vfs_write+0x38/0x120)

74.121303 \<80254938\> (__vfs_write) from \<802557fc\> (vfs_write+0xb4/0x178)

74.121335 \<802557fc\> (vfs_write) from \<802566b8\> (SyS_write+0x4c/0xa0)

74.121369 \<802566b8\> (SyS_write) from \<80109280\> (ret_fast_syscall+0x0/0x48)

74.121392 KD_LOG:spi_sync for device: spi0.0, modalias: spidev

74.131303 CPU: 0 PID: 357 Comm: test_spidev Tainted: G W 4.9.88 #15

74.131320 Hardware name: Freescale i.MX6 UltraLite (Device Tree)

74.131382 \<80112a34\> (unwind_backtrace) from \<8010dc2c\> (show_stack+0x20/0x24)

74.131417 \<8010dc2c\> (show_stack) from \<80469964\> (dump_stack+0x80/0x94)

74.131450 \<80469964\> (dump_stack) from \<80667620\> (spi_sync+0x3c/0x6c)

74.131480 \<80667620\> (spi_sync) from \<80667f08\> (spidev_sync+0x64/0x7c)

74.131510 \<80667f08\> (spidev_sync) from \<8066804c\> (spidev_write+0x12c/0x168)

74.131547 \<8066804c\> (spidev_write) from \<80254938\> (__vfs_write+0x38/0x120)

74.131579 \<80254938\> (__vfs_write) from \<802557fc\> (vfs_write+0xb4/0x178)

74.131611 \<802557fc\> (vfs_write) from \<802566b8\> (SyS_write+0x4c/0xa0)

74.131647 \<802566b8\> (SyS_write) from \<80109280\> (ret_fast_syscall+0x0/0x48)

用户态执行 write() 时之所以会调用内核的 SyS_write,是因为 Linux 内核通过"系统调用机制" 建立了一道从用户空间穿越到内核空间的"受控入口"。

1. 为什么不能直接调用?(权限与安全)

用户程序运行在 CPU 的用户态 (User Mode),内核运行在 CPU 的内核态 (Kernel Mode)。

  • 隔离保护:如果用户程序可以直接调用内核函数,它就可以随意读写内核内存、甚至重写系统核心代码,这会导致系统瞬间崩溃或被恶意程序篡改。
  • 受控边界:因此,Linux 强制要求:所有对底层资源的访问(磁盘、外设、内存分配),都必须通过一条合法的"特权指令"切换到内核态,由内核代理完成。

2. 它是如何跳转的?(系统调用流程)

当你的 test_spidev 代码执行 write() 时,内部发生了以下一系列精确的硬件与软件协同:

  1. 触发软中断 (Trap):write() 是一个库函数(glibc 提供),它内部执行了一条特殊的 CPU 指令(如 ARM 架构上的 svc 指令)。
  2. 上下文切换:CPU 立即暂停执行用户代码,强制跳转到内核预先配置好的一个异常入口地址(Vector Table)。
  3. 系统调用分发 (Dispatch):内核接收到中断,查看你请求的编号(write 在 Linux 内核中对应的系统调用号通常是 4)。
  4. 跳转到 SyS_write:内核在"系统调用表" (sys_call_table) 中查表,找到编号对应的函数地址,也就是 SyS_write。

直接看到 __vfs_write 的定义:

c 复制代码
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
		    loff_t *pos)
{
	if (file->f_op->write)
		return file->f_op->write(file, p, count, pos);
	else if (file->f_op->write_iter)
		return new_sync_write(file, p, count, pos);
	else
		return -EINVAL;
}

file->f_op->write:根据当前这个文件(file),找到它所关联的驱动操作集合(f_op),然后执行该集合中专门负责写入的函数(write)

结构化拆解:

  • file (结构体指针):

    代表了用户打开的那个文件(在你的例子中是 /dev/spidev0.0)。它包含文件的当前偏移量、模式等信息。

  • ->f_op (文件操作指针):

    全称是 file_operations。这是一个函数指针表(Jump Table)。当驱动程序初始化时,它会将一组自定义的函数地址(如 read, write, open, ioctl)填入这个表。

  • ->write (具体的函数):

    这是表里的一个条目,指向了该设备驱动中真正的写入函数。在 spidev 驱动中,这个指针指向的就是你埋点的那个 spidev_write 函数。

总结:

file->f_op->write 是驱动的"接口约定"。

它标志着通用内核(VFS)与特定硬件驱动(spidev)之间的界限:

VFS 侧:只负责在"按键表"里查找名为 write 的函数。

驱动侧:只负责实现一个名为 spidev_write 的函数并把它放进"按键表"里。

1.2.2 通过设备创建过程分析

首先在 spi_sync 函数中添加打印信息,作为后续的线索:

c 复制代码
int spi_sync(struct spi_device *spi, struct spi_message *message)
{
	
	int ret;
	pr_info("KD_LOG:spi_sync for device: %s, modalias: %s\n", 
            dev_name(&spi->dev), spi->modalias);
	dump_stack();

	mutex_lock(&spi->master->bus_lock_mutex);
	ret = __spi_sync(spi, message);
	mutex_unlock(&spi->master->bus_lock_mutex);

	return ret;
}

root@100ask:\~# dmesg | grep KD_LOG:spi_sync

2.370415 KD_LOG:spi_sync for device: spi32766.0, modalias: 74hc595

4.135368 KD_LOG:spi_sync for device: spi32766.0, modalias: 74hc595

4.151040 KD_LOG:spi_sync for device: spi32766.0, modalias: 74hc595

4.178175 KD_LOG:spi_sync for device: spi32766.0, modalias: 74hc595

6.927685 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.084553 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.122766 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.160483 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.200007 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.263925 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.331673 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.383862 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.467702 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.532143 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.603518 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.685157 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.741053 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.763451 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.793812 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.828009 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.858914 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.918398 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

发现有两个 SPI 调用了,spi_sync,那么我们分别提取它们的调用链:

74hc595:

4.178175 KD_LOG:spi_sync for device: spi32766.0, modalias: 74hc595

4.184692 CPU: 0 PID: 117 Comm: kworker/0:3 Tainted: G W 4.9.88 #14

4.184699 Hardware name: Freescale i.MX6 UltraLite (Device Tree)

4.184716 Workqueue: events deferred_probe_work_func

4.184745 \<80112a34\> (unwind_backtrace) from \<8010dc2c\> (show_stack+0x20/0x24)

4.184761 \<8010dc2c\> (show_stack) from \<80469964\> (dump_stack+0x80/0x94)

4.184777 \<80469964\> (dump_stack) from \<80667620\> (spi_sync+0x3c/0x6c)

4.184793 \<80667620\> (spi_sync) from \<804ad7a4\> (__gen_74x164_write_config+0x84/0x8c)

4.184807 \<804ad7a4\> (__gen_74x164_write_config) from \<804ad8c8\> (gen_74x164_set_value+0x70/0x7c)

4.184824 \<804ad8c8\> (gen_74x164_set_value) from \<804a79e0\> (_gpiod_set_raw_value+0x70/0x150)

4.184841 \<804a79e0\> (_gpiod_set_raw_value) from \<804a8e8c\> (gpiod_set_raw_value_cansleep+0x48/0x90)

4.184861 \<804a8e8c\> (gpiod_set_raw_value_cansleep) from \<805314c8\> (gpio_reset_set+0x34/0x38)

4.184877 \<805314c8\> (gpio_reset_set) from \<80531560\> (gpio_reset+0x4c/0x74)

4.184893 \<80531560\> (gpio_reset) from \<80530e40\> (reset_control_reset+0x4c/0x8c)

4.184908 \<80530e40\> (reset_control_reset) from \<80531424\> (device_reset+0x40/0x54)

4.184925 \<80531424\> (device_reset) from \<804f1960\> (sii902x_probe+0x54/0x494)

4.184942 \<804f1960\> (sii902x_probe) from \<80772820\> (i2c_device_probe+0x1d0/0x254)

4.184957 \<80772820\> (i2c_device_probe) from \<805a1f38\> (driver_probe_device+0x214/0x450)

4.184972 \<805a1f38\> (driver_probe_device) from \<805a234c\> (__device_attach_driver+0xac/0x14c)

4.184989 \<805a234c\> (__device_attach_driver) from \<8059fdb8\> (bus_for_each_drv+0x54/0xa4)

4.185004 \<8059fdb8\> (bus_for_each_drv) from \<805a1b78\> (__device_attach+0xc0/0x150)

4.185018 \<805a1b78\> (__device_attach) from \<805a2448\> (device_initial_probe+0x1c/0x20)

4.185032 \<805a2448\> (device_initial_probe) from \<805a0f04\> (bus_probe_device+0x94/0x9c)

4.185045 \<805a0f04\> (bus_probe_device) from \<805a14f8\> (deferred_probe_work_func+0x98/0xd8)

4.185061 \<805a14f8\> (deferred_probe_work_func) from \<8014f2e0\> (process_one_work+0x1f8/0x570)

4.185076 \<8014f2e0\> (process_one_work) from \<80150610\> (worker_thread+0x2c4/0x5c8)

4.185092 \<80150610\> (worker_thread) from \<80155aa8\> (kthread+0x118/0x120)

4.185109 \<80155aa8\> (kthread) from \<80109350\> (ret_from_fork+0x14/0x24)

特点:它经过了 GPIO 子系统 + Reset 框架 的多重抽象。

交互逻辑:驱动调用 reset_control_reset -> gpiod_set_value -> 74x164_write_config -> spi_sync。

研究价值:适合研究 跨子系统协作。它展示了 Linux 如何将一个跨总线(I2C设备依赖 SPI 引脚)的物理行为,统一抽象成标准的系统资源操作。

icm20608:

7.918398 KD_LOG:spi_sync for device: spi2.0, modalias: icm20608

7.959632 CPU: 0 PID: 185 Comm: udevd Tainted: G W 4.9.88 #14

7.959641 Hardware name: Freescale i.MX6 UltraLite (Device Tree)

7.959685 \<80112a34\> (unwind_backtrace) from \<8010dc2c\> (show_stack+0x20/0x24)

7.959705 \<8010dc2c\> (show_stack) from \<80469964\> (dump_stack+0x80/0x94)

7.959724 \<80469964\> (dump_stack) from \<80667620\> (spi_sync+0x3c/0x6c)

7.959745 \<80667620\> (spi_sync) from \<805c9024\> (regmap_spi_write+0x80/0x88)

7.959763 \<805c9024\> (regmap_spi_write) from \<805c3ad8\> (_regmap_raw_write+0x39c/0x934)

7.959778 \<805c3ad8\> (_regmap_raw_write) from \<805c40ec\> (_regmap_bus_raw_write+0x7c/0xa4)

7.959792 \<805c40ec\> (_regmap_bus_raw_write) from \<805c2a78\> (_regmap_write+0x70/0x164)

7.959805 \<805c2a78\> (_regmap_write) from \<805c4400\> (regmap_write+0x4c/0x6c)

7.959838 \<805c4400\> (regmap_write) from \<7f04230c\> (inv_mpu6050_set_power_itg+0x6c/0x98 inv_mpu6050)

7.959871 \<7f04230c\> (inv_mpu6050_set_power_itg inv_mpu6050) from \<7f042e8c\> (inv_mpu_core_probe+0x28c/0x3d4 inv_mpu6050)

7.959904 \<7f042e8c\> (inv_mpu_core_probe inv_mpu6050) from \<7f049100\> (inv_mpu_probe+0x68/0x98 inv_mpu6050_spi)

7.959925 \<7f049100\> (inv_mpu_probe inv_mpu6050_spi) from \<80664210\> (spi_drv_probe+0x8c/0xb8)

7.959942 \<80664210\> (spi_drv_probe) from \<805a1f38\> (driver_probe_device+0x214/0x450)

7.959957 \<805a1f38\> (driver_probe_device) from \<805a2284\> (__driver_attach+0x110/0x12c)

7.959976 \<805a2284\> (__driver_attach) from \<8059fce8\> (bus_for_each_dev+0x5c/0xac)

7.959993 \<8059fce8\> (bus_for_each_dev) from \<805a17a4\> (driver_attach+0x2c/0x30)

7.960007 \<805a17a4\> (driver_attach) from \<805a123c\> (bus_add_driver+0x1d0/0x274)

7.960022 \<805a123c\> (bus_add_driver) from \<805a3038\> (driver_register+0x88/0x104)

7.960037 \<805a3038\> (driver_register) from \<8066413c\> (__spi_register_driver+0x84/0x88)

7.960057 \<8066413c\> (__spi_register_driver) from \<7f04b020\> (inv_mpu_driver_init+0x20/0x24 inv_mpu6050_spi)

7.960080 \<7f04b020\> (inv_mpu_driver_init inv_mpu6050_spi) from \<80101cc4\> (do_one_initcall+0x54/0x180)

7.960099 \<80101cc4\> (do_one_initcall) from \<801ffc58\> (do_init_module+0x74/0x1f4)

7.960118 \<801ffc58\> (do_init_module) from \<801b84c4\> (load_module+0x1f98/0x2620)

7.960136 \<801b84c4\> (load_module) from \<801b8dc0\> (SyS_finit_module+0xc4/0xfc)

7.960152 \<801b8dc0\> (SyS_finit_module) from \<80109280\> (ret_fast_syscall+0x0/0x48)

特点:它使用了 Regmap 框架。

交互逻辑:驱动调用 regmap_write -> 框架进行格式封装 -> 底层调用 spi_sync。

研究价值:适合研究 框架层 如何将高层 API 转化为总线任务"。它展示了数据是如何在"寄存器操作"和"SPI 数据帧"之间完成抽象映射的。

icm20608 的 调用链更适合作为"接口层数据交互"的研究对象。

理由如下:

数据流路径:Regmap 是 Linux 内核中专门为"寄存器总线访问"设计的纯粹的数据交互接口层。它的代码极其标准,逻辑清晰(地址+数据封装),非常符合你研究"数据交互"的初衷。

数据交互的闭环完整:通过 Regmap,可以清楚地看到:

输入端:驱动程序调用 regmap_write(map, reg, val)。

处理端:Regmap 负责计算地址偏移、处理缓存、进行字节序转换。

输出端:Regmap 构造 spi_transfer 结构体并触发 spi_sync。

这一过程就是"业务逻辑 -> 数据帧封装 -> 总线传输"的完整链路。

通过之前的设备注册流程,我们已经分析到了 driver_probe_device ,所以跳过通用的驱动模型框架,直接切入 spi_drv_probe 进行分析:

c 复制代码
static int spi_drv_probe(struct device *dev)
{
	/* 1. 类型转换:从通用的 device 结构体还原回 spi_driver 和 spi_device */
	const struct spi_driver		*sdrv = to_spi_driver(dev->driver);
	struct spi_device		*spi = to_spi_device(dev);
	int ret;

	/* 2. 时钟配置:处理设备树 (DT) 中定义的时钟默认配置 */
	ret = of_clk_set_defaults(dev->of_node, false);
	if (ret)
		return ret;

	/* 3. 中断资源解析:如果设备树定义了中断,将其解析并存入 spi->irq */
	if (dev->of_node) {
		spi->irq = of_irq_get(dev->of_node, 0);
		if (spi->irq == -EPROBE_DEFER)
			return -EPROBE_DEFER; // 资源未就绪,触发延迟探测
		if (spi->irq < 0)
			spi->irq = 0;
	}

	/* 4. 电源域挂载:处理电源管理相关的 PM Domain (确保设备供电正常) */
	ret = dev_pm_domain_attach(dev, true);
	if (ret != -EPROBE_DEFER) {
		/* 5. 核心调用:调用具体驱动(如 inv_mpu6050)的 probe 函数 */
		ret = sdrv->probe(spi);
		if (ret)
			dev_pm_domain_detach(dev, true); // 如果驱动探测失败,解绑电源域
	}

	return ret;
}

这段代码从通用的 SPI 总线逻辑,切换到具体的寄存器映射接口 (Regmap)

输入:物理抽象 (struct spi_device *spi)

这是内核看到的原始硬件设备,它拥有总线号、片选引脚和中断号。

处理:封装抽象 (devm_regmap_init_spi)

它将物理总线的读写逻辑(SPI 传输)和硬件的寄存器配置(地址宽度、字节序等)打包,生成了一个统一的 struct regmap 接口。

输出:业务接口 (struct regmap *regmap)

从这一刻起,核心驱动逻辑 inv_mpu_core_probe 彻底脱离了 SPI 总线的束缚。它变成了一个"协议无关"的模块,只要能通过 regmap 读写寄存器,无论底层是 SPI 还是 I2C,它都能正常工作。

c 复制代码
static int inv_mpu_probe(struct spi_device *spi)
{
	struct regmap *regmap;
	// ... (变量声明)

	/* 1. ID 匹配:区分不同芯片型号 (如 MPU6050, MPU6500 等) */
	if ((spi_id = spi_get_device_id(spi))) {
		chip_type = (enum inv_devices)spi_id->driver_data;
		name = spi_id->name;
	} else if ((acpi_id = acpi_match_device(spi->dev.driver->acpi_match_table, &spi->dev))) {
		chip_type = (enum inv_devices)acpi_id->driver_data;
	} else {
		return -ENODEV;
	}

	/* 2. 建立接口枢纽:这是最关键的一步!
	 * 将物理 SPI 设备 (spi) 与 Regmap 映射配置 (inv_mpu_regmap_config) 关联。
	 * 之后,驱动程序再也不需要手动拼写 SPI 报文,只需操作 regmap 指针即可。 */
	regmap = devm_regmap_init_spi(spi, &inv_mpu_regmap_config);
	if (IS_ERR(regmap)) {
		return PTR_ERR(regmap);
	}

	/* 3. 切换到通用核心逻辑:
	 * 驱动将封装好的 regmap 接口交给 core 层,core 层从此只认 regmap。 */
	return inv_mpu_core_probe(regmap, spi->irq, name,
				  inv_mpu_i2c_disable, chip_type);
}
c 复制代码
int inv_mpu_core_probe(struct regmap *regmap, int irq, const char *name,
		int (*inv_mpu_bus_setup)(struct iio_dev *), int chip_type)
{
	struct inv_mpu6050_state *st;
	struct iio_dev *indio_dev;
	struct inv_mpu6050_platform_data *pdata;
	struct device *dev = regmap_get_device(regmap);
	int result;

	// 1. 分配 IIO 设备内存,并将私有数据结构 inv_mpu6050_state 一并分配
	indio_dev = devm_iio_device_alloc(dev, sizeof(*st));
	if (!indio_dev)
		return -ENOMEM;

	// 2. 硬件信息完整性校验
	BUILD_BUG_ON(ARRAY_SIZE(hw_info) != INV_NUM_PARTS);
	if (chip_type < 0 || chip_type >= INV_NUM_PARTS) {
		dev_err(dev, "Bad invensense chip_type=%d name=%s\n", chip_type, name);
		return -ENODEV;
	}

	// 3. 初始化私有数据结构体 (st)
	st = iio_priv(indio_dev);
	st->chip_type = chip_type;
	st->powerup_count = 0;
	st->irq = irq;
	st->map = regmap; // 关键点:保存 regmap,后续所有硬件交互均通过它进行

	// 4. 解析设备树中的挂载矩阵 (Mounting Matrix),处理传感器安装方向偏移
	pdata = dev_get_platdata(dev);
	if (!pdata) {
		result = of_iio_read_mount_matrix(dev, "mount-matrix", &st->orientation);
		if (result) return result;
	} else {
		st->plat_data = *pdata;
	}

	// 5. 硬件初始化:检查芯片类型、复位芯片、上电
	result = inv_check_and_setup_chip(st);
	if (result) return result;

	// 6. 执行特定总线的额外配置 (inv_mpu_bus_setup 通常指向 i2c 或 spi 的特定逻辑)
	if (inv_mpu_bus_setup)
		inv_mpu_bus_setup(indio_dev);

	// 7. 配置芯片采样率、滤波器等基础参数
	result = inv_mpu6050_init_config(indio_dev);
	if (result) return result;

	// 8. 关联 IIO 设备与底层 device
	dev_set_drvdata(dev, indio_dev);
	indio_dev->dev.parent = dev;
	indio_dev->name = name ? name : dev_name(dev);
	
	// 9. 设置 IIO 通道 (Channels) 和信息回调函数 (info)
	indio_dev->channels = inv_mpu_channels;
	indio_dev->num_channels = ARRAY_SIZE(inv_mpu_channels);
	indio_dev->info = &mpu_info; // 包含 read_raw 等回调函数的核心结构体
	indio_dev->modes = INDIO_BUFFER_TRIGGERED; // 设置为触发式缓冲区模式

	// 10. 配置缓冲区 (FIFO),关联中断处理函数和数据读取函数
	result = iio_triggered_buffer_setup(indio_dev,
					    inv_mpu6050_irq_handler, // 顶半部 (中断触发)
					    inv_mpu6050_read_fifo,   // 底半部 (读FIFO数据)
					    NULL);
	if (result) return result;

	// 11. 配置并探测触发器 (Trigger)
	result = inv_mpu6050_probe_trigger(indio_dev);
	if (result) goto out_unreg_ring;

	// 12. 初始化时间戳队列和锁
	INIT_KFIFO(st->timestamps);
	spin_lock_init(&st->time_stamp_lock);

	// 13. 最终将 IIO 设备注册到内核,用户态即可通过 /dev/iio:deviceX 访问
	result = iio_device_register(indio_dev);
	if (result) goto out_remove_trigger;

	return 0;

out_remove_trigger:
	inv_mpu6050_remove_trigger(st);
out_unreg_ring:
	iio_triggered_buffer_cleanup(indio_dev);
	return result;
}

在 inv_mpu_core_probe 中,调用 inv_mpu6050_set_power_itg 的逻辑是通过其内部调用的 inv_check_and_setup_chip 函数来实现的。

这是一种典型的分层调用架构。inv_mpu_core_probe 并不直接硬编码电源操作,而是通过调用专门的"芯片初始化"辅助函数来完成。

c 复制代码
static int inv_check_and_setup_chip(struct inv_mpu6050_state *st)
{
	int result;
	unsigned int regval;

	// 1. 初始化寄存器映射表:根据之前探测到的芯片类型 (chip_type),
	// 绑定对应的寄存器偏移量表 (st->reg) 和硬件能力信息 (st->hw)
	st->hw  = &hw_info[st->chip_type];
	st->reg = hw_info[st->chip_type].reg;

	/* 2. 硬件复位:写入 H_RESET 位,确保芯片从干净的默认状态启动,
	   排除之前可能残留的配置干扰 */
	result = regmap_write(st->map, st->reg->pwr_mgmt_1, INV_MPU6050_BIT_H_RESET);
	if (result)
		return result;
	// 必须延时,等待硬件完成内部复位流程
	msleep(INV_MPU6050_POWER_UP_TIME);

	/* 3. 身份校验 (WHO_AM_I):读取芯片的 ID 寄存器,
	   确认物理芯片与驱动识别到的型号是否匹配 */
	result = regmap_read(st->map, INV_MPU6050_REG_WHOAMI, &regval);
	if (result)
		return result;
	if (regval != st->hw->whoami) {
		// ID 不匹配通常意味着硬件配置有误或总线通信异常
		dev_warn(regmap_get_device(st->map),
				"whoami mismatch got %#02x expected %#02hhx for %s\n",
				regval, st->hw->whoami, st->hw->name);
	}

	/* 4. 电源状态对齐:在复位后,芯片可能处于休眠态或工作态 (取决于 OTP 熔丝配置)。
	   通过先关后开 (toggle) 电源,强制将硬件状态置为一个确定的"工作态",
	   确保软件定义的电源状态与硬件实际物理状态同步 */
	result = inv_mpu6050_set_power_itg(st, false); // 设置为待机
	if (result)
		return result;
	result = inv_mpu6050_set_power_itg(st, true);  // 设置为工作
	if (result)
		return result;

	/* 5. 引擎启动:分别开启加速度计和陀螺仪的引擎。
	   这里调用 switch_engine 将相关的待机位 (STBY) 置为 false,即取消待机 */
	// 开启加速度计引擎
	result = inv_mpu6050_switch_engine(st, false, INV_MPU6050_BIT_PWR_ACCL_STBY);
	if (result)
		return result;
	// 开启陀螺仪引擎
	result = inv_mpu6050_switch_engine(st, false, INV_MPU6050_BIT_PWR_GYRO_STBY);
	if (result)
		return result;

	return 0; // 初始化成功
}
c 复制代码
int regmap_write(struct regmap *map, unsigned int reg, unsigned int val)
{
	int ret;

	/* 1. 对齐检查 (Alignment Check):
	 * 确保寄存器地址符合 stride (步长) 要求。
	 * 这是接口层的第一道防护,防止非法内存访问导致硬件行为异常。 */
	if (!IS_ALIGNED(reg, map->reg_stride))
		return -EINVAL;

	/* 2. 锁保护 (Concurrency Control):
	 * 因为寄存器访问必须是原子性的,Regmap 必须通过 map->lock 
	 * 防止多线程竞争导致 SPI/I2C 总线传输的数据帧乱序或冲突。 */
	map->lock(map->lock_arg);

	/* 3. 实际传输:调用内部封装好的 write 实现。
	 * 此时数据开始脱离业务层,进入 Regmap 的封装流水线。 */
	ret = _regmap_write(map, reg, val);

	/* 4. 解锁:释放资源,允许下一个请求进入。 */
	map->unlock(map->lock_arg);

	return ret;
}
c 复制代码
int _regmap_write(struct regmap *map, unsigned int reg, unsigned int val)
{
	int ret;
	void *context = _regmap_map_get_context(map);

	/* 1. 权限校验:查看该寄存器是否被允许写入 */
	if (!regmap_writeable(map, reg))
		return -EIO;

	/* 2. 缓存处理 (Cache Handling):
	 * 这是性能优化的核心。如果不绕过缓存 (cache_bypass),则优先写入缓存。
	 * 如果开启了 cache_only 模式,数据直接更新在内存中,不进行物理总线通信。 */
	if (!map->cache_bypass && !map->defer_caching) {
		ret = regcache_write(map, reg, val);
		if (ret != 0)
			return ret;
		if (map->cache_only) {
			map->cache_dirty = true;
			return 0; // 直接返回,节省一次昂贵的 SPI 总线传输
		}
	}

	/* 3. 追踪与日志:用于性能分析和调试 */
	trace_regmap_reg_write(map, reg, val);

	/* 4. 最终的总线输出:
	 * 这一行是通往物理世界的终点。map->reg_write 是一个函数指针,
	 * 对于 SPI 设备,它最终会指向 regmap_spi_write。 */
	return map->reg_write(context, reg, val);
}
c 复制代码
static int _regmap_bus_raw_write(void *context, unsigned int reg, unsigned int val)
{
	struct regmap *map = context;

	/* 1. 安全性检查:确认底层总线(SPI/I2C)已连接,且数据格式化函数存在 */
	WARN_ON(!map->bus || !map->format.format_val);

	/* 2. 数据格式化 (Serialization):
	 * 这里的 map->format.format_val 就像是一个"协议翻译官"。
	 * 它将 val (unsigned int) 转换成大端或小端的字节流,
	 * 并填入 map->work_buf 的特定偏移位置(跳过寄存器地址和填充字节)。 */
	map->format.format_val(map->work_buf + map->format.reg_bytes
			       + map->format.pad_bytes, val, 0);

	/* 3. 调用底层原始写函数:
	 * 将准备好的二进制缓冲区发往总线。 */
	return _regmap_raw_write(map, reg,
				 map->work_buf +
				 map->format.reg_bytes +
				 map->format.pad_bytes,
				 map->format.val_bytes);
}

在这一层,数据交互彻底脱离了逻辑意义("寄存器地址"和"值"),转而变成了一串要在总线上发送的二进制字节流。

c 复制代码
int _regmap_raw_write(struct regmap *map, unsigned int reg,
		      const void *val, size_t val_len)
{
	struct regmap_range_node *range;
	unsigned long flags;
	// 指向工作缓冲区中数据段的起始位置 (跳过寄存器地址和补齐位)
	void *work_val = map->work_buf + map->format.reg_bytes + map->format.pad_bytes;
	void *buf;
	int ret = -ENOTSUPP;
	size_t len;
	int i;

	WARN_ON(!map->bus);

	/* 1. 安全校验:遍历数据包,确保所有地址在写权限范围内 */
	if (map->writeable_reg)
		for (i = 0; i < val_len / map->format.val_bytes; i++)
			if (!map->writeable_reg(map->dev, reg + regmap_get_offset(map, i)))
				return -EINVAL;

	/* 2. 缓存同步:如果开启了缓存,先写入 cache,如果 cache_only 则直接返回 */
	if (!map->cache_bypass && map->format.parse_val) {
		// ... 解析并将值写入 regcache ...
		if (map->cache_only) { map->cache_dirty = true; return 0; }
	}

	/* 3. 页窗口处理:处理多页寄存器映射,如果写入跨越了窗口,则拆分请求 */
	range = _regmap_range_lookup(map, reg);
	if (range) {
		// ... 复杂的窗口边界分割逻辑 ...
		ret = _regmap_select_page(map, &reg, range, val_num);
		if (ret != 0) return ret;
	}

	/* 4. 格式化封装:将寄存器地址填入 work_buf 的前缀部分 */
	map->format.format_reg(map->work_buf, reg, map->reg_shift);
	regmap_set_work_buf_flag_mask(map, map->format.reg_bytes, map->write_flag_mask);

	/* 5. 传输执行:根据硬件能力选择最高效的写入方式 */
	
	// A. 异步写入:如果有异步传输队列,将任务挂载到队列中
	if (map->async && map->bus->async_write) {
		// ... 异步任务管理与调度 ...
		return ret;
	}

	// B. 同步写入(性能优化):
	// 如果是单寄存器写,直接发送 work_buf;如果是多字节,优先使用 gather_write
	if (val == work_val)
		ret = map->bus->write(map->bus_context, map->work_buf, ...);
	else if (map->bus->gather_write)
		ret = map->bus->gather_write(map->bus_context, ...);

	// C. 保底策略:如果硬件不支持上述方式,则手工线性化(拼接缓存和数据后发送)
	if (ret == -ENOTSUPP) {
		len = map->format.reg_bytes + map->format.pad_bytes + val_len;
		buf = kzalloc(len, GFP_KERNEL);
		memcpy(buf, map->work_buf, map->format.reg_bytes);
		memcpy(buf + ..., val, val_len);
		ret = map->bus->write(map->bus_context, buf, len);
		kfree(buf);
	}

	return ret;
}

在 _regmap_raw_write 中,并没有直接出现 map→reg_write 这个具体的成员名称。它的调用是通过 map→bus→write 来触发的,而这个 bus→write 在 SPI 模式下,底层就是对应到 regmap_spi_write。

c 复制代码
/* 就在这里,通过总线结构体调用写函数 */
if (val == work_val)
    ret = map->bus->write(map->bus_context, map->work_buf, ...);
c 复制代码
static int regmap_spi_write(void *context, const void *data, size_t count)
{
	struct device *dev = context;
	struct spi_device *spi = to_spi_device(dev);

	return spi_write(spi, data, count);
}
c 复制代码
static inline int
spi_write(struct spi_device *spi, const void *buf, size_t len)
{
    // 定义一个传输单元 (Transfer)
	struct spi_transfer	t = {
			.tx_buf		= buf, // 指定发送缓冲区,准备发送给 SPI 从设备的数据
			.len		= len, // 指定数据长度
		};

    // 直接发起同步传输,无需手动初始化 message 和添加链表
	return spi_sync_transfer(spi, &t, 1); 
}
c 复制代码
int spi_sync_transfer(struct spi_device *spi, struct spi_transfer *xfers,
	unsigned int num_xfers)
{
	struct spi_message msg; // 1. 在栈上分配一个消息容器

	// 2. 将传入的传输数组 (xfers) 打包进这个消息容器中
	// 这比手动调用 spi_message_add_tail 一次次添加要高效得多
	spi_message_init_with_transfers(&msg, xfers, num_xfers);

	// 3. 提交给 SPI 总线框架,执行同步阻塞调用
	return spi_sync(spi, &msg);
}
c 复制代码
static inline void
spi_message_init_with_transfers(struct spi_message *m,
                                struct spi_transfer *xfers, 
                                unsigned int num_xfers)
{
    unsigned int i;

    /* 1. 初始化消息容器:
     * 将 spi_message 中的链表头 (transfers) 指向自身,
     * 并清空其中的状态位 (如实际传输字节数、完成回调函数等)。
     */
    spi_message_init(m);

    /* 2. 批量挂载:
     * 遍历传入的 xfers 数组,将每一个 transfer 片段 
     * 通过 spi_message_add_tail 添加到消息 m 的链表末尾。
     * 这使得驱动可以在一次 spi_sync 系统调用中,
     * 完成多个物理上的读写周期(例如:先发地址,再读取数据)。
     */
    for (i = 0; i < num_xfers; ++i)
        spi_message_add_tail(&xfers[i], m);
}