
在嵌入式 Linux 开发中,SPI 是使用率极高的高速串行通信总线,广泛用于驱动 Flash、显示屏、传感器、ADC 等外设。很多嵌入式开发者熟悉 SPI 基础协议与时序,但在接触 Linux 内核 SPI 驱动时,常常因为内核分层架构、总线匹配机制和设备树配置复杂而难以理解。相比于裸机单片机的简单寄存器操作,Linux SPI 驱动采用标准化总线框架,拥有固定的注册、匹配、传输流程,新手很容易混淆设备、总线与驱动三者的工作关系。
其实 Linux SPI 驱动有着清晰、固定的开发逻辑,并不需要死记复杂源码。本文将用通俗易懂的方式,从零讲解 Linux SPI 驱动的整体框架、分层原理、设备树匹配方式与数据传输流程,避开晦涩的底层源码堆砌,梳理核心工作机制。帮助新手快速理清 SPI 驱动的运行逻辑,彻底看懂 Linux SPI 通信的底层原理,解决开发中看不懂、写不出、调不通的常见问题。
一、温故 SPI 总线协议
面试题写作模版SPI,即串行外设接口(Serial Peripheral Interface) ,是一种高速、全双工、同步的通信总线,由摩托罗拉公司于上世纪 80 年代开发,在嵌入式系统中广泛应用,用于微控制器与各种外设之间的通信,如传感器、存储器、显示屏等。
SPI 采用主从架构,一个 SPI 系统中,必然有一个主设备,同时可以连接一个或多个从设备 。主设备就像乐队的指挥,掌控着整个通信的节奏和流程,负责发起通信并产生时钟信号(SCK);从设备则像乐队的成员,听从主设备的指挥,根据主设备提供的时钟信号进行数据的接收和发送。比如在一个智能手表的系统中,主控芯片作为 SPI 主设备,与作为从设备的心率传感器、加速度传感器等进行通信,获取各种数据。
SPI 通信硬件简单,通常仅需四条线即可完成通信,这四条线分别是:串行时钟线(SCK)、主机输出 / 从机输入数据线(MOSI)、主机输入 / 从机输出数据线(MISO)和从机选择线(SS/CS,Slave Select/Chip Select) 。这种简单的硬件架构,不仅节约了芯片的管脚资源,也让 PCB 的布局更加简洁,为电路设计带来了极大的便利。
SPI 通信依靠四条信号线实现数据交互。MISO 为主入从出线路,负责从设备向主设备传输数据,比如主设备读取温度传感器信息时,传感器就通过这条线路传回数据。MOSI 为主出从入线路,用于主设备向从设备下发指令、地址和待写入数据,像主设备向 Flash 存储器保存数据,数据便会经由这条线路完成传输。
SCK 是由主设备产生的串行时钟信号,相当于通信的节拍器,用来同步主从设备的数据收发节奏,时钟频率越高,传输速度也就越快。SS/CS 为片选信号,在多从设备的系统中,主设备通过拉低对应电平选中目标设备,未被选中的设备保持静默,不会响应总线上的信号。
SPI 通信基于串行移位寄存器来实现数据传输。在通信前,主设备和从设备都配置好相同的通信参数,包括数据位宽、时钟极性(CPOL)、时钟相位(CPHA)以及数据传输顺序(MSB 先还是 LSB 先)等。以主设备向从设备发送数据 0x55(二进制为 01010101),同时从设备向主设备发送数据 0xAA(二进制为 10101010)为例:
二、Linux SPI 驱动架构剖析
面试题写作模版Linux 驱动设计秉持着分层与分离的精妙思想 ,这一思想在 SPI 驱动中展现得淋漓尽致。分层,就如同搭建高楼,每一层都有其独特的职责与功能。SPI 驱动主要分为核心层、主机控制器驱动层和设备驱动层 。核心层位于中间位置,起着承上启下的关键作用,它向上为设备驱动提供统一的 API 接口,向下则为不同的主机控制器驱动提供通用的管理机制 。
而分离思想,则是让主机控制器驱动和设备驱动各自专注于自己的领域 。主机控制器驱动负责与硬件层面的 SPI 控制器打交道,根据具体的硬件手册操作控制器,产生 SPI 总线上的各种波形信号,它不关心连接的具体外设是什么;设备驱动则聚焦于特定的外设,如温度传感器、Flash 存储器等,通过核心层提供的 API 来与主机控制器进行通信,无需了解主机控制器的硬件细节 。
这种分层与分离的设计,极大地提升了代码的可维护性、可扩展性以及复用性。比如,当更换不同型号的 SPI 主机控制器时,只需要修改主机控制器驱动层的代码,设备驱动层和核心层可以保持不变;同样,当连接新的 SPI 外设时,只需编写对应的设备驱动,主机控制器驱动和核心层也无需改动 。
SPI 核心层是整个 SPI 驱动架构的中枢神经,它承担着注册 SPI 总线、管理 SPI 设备和驱动的注册与匹配,以及提供通用的 SPI 通信 API 等重要职责。在 SPI 核心层中,有许多关键的 API,它们是开发者与 SPI 驱动交互的重要工具。
①spi_register_driver (struct spi_driver *sdrv):用于注册一个 SPI 设备驱动。当我们编写好一个 SPI 设备驱动,想要让内核识别并使用它时,就需要调用这个函数。例如,我们为一个 SPI 接口的加速度传感器编写了驱动,通过 spi_register_driver 将其注册到内核中,内核就会开始管理这个驱动,并在合适的时候调用它的相关函数。
// SPI 驱动注册示例(对应 spi_register_driver)staticstruct spi_driver spi_sensor_driver = { .probe = sensor_probe, // 设备匹配成功后执行 .remove = sensor_remove, // 设备移除时执行 .driver = { .name = "spi_sensor", // 驱动名 .owner = THIS_MODULE, },};// 注册驱动到内核static int __init sensor_init(void){ return spi_register_driver(&spi_sensor_driver);}// 卸载驱动static void __exit sensor_exit(void){ spi_unregister_driver(&spi_sensor_driver);}module_init(sensor_init);module_exit(sensor_exit);②spi_sync (struct spi_device *spi, struct spi_message *message):实现同步数据传输。它会阻塞当前线程,直到 SPI 数据传输完成。在实际应用中,当我们需要确保数据准确无误地传输完成后再进行下一步操作时,就可以使用 spi_sync。比如在向 SPI Flash 写入数据时,调用 spi_sync 可以保证数据完整写入后,再进行后续的处理,避免因数据传输未完成而导致的错误。
// SPI 同步传输示例(对应 spi_sync)struct spi_message msg;struct spi_transfer xfer = { .tx_buf = write_buf, // 要发送的数据 .rx_buf = read_buf, // 接收数据的缓冲区 .len = 8, // 数据长度};// 初始化并提交同步传输spi_message_init(&msg);spi_message_add_tail(&xfer, &msg);// 阻塞等待传输完成spi_sync(spi_dev, &msg);spi_master 结构体是 SPI 主机驱动的核心数据结构,它就像是 SPI 主机控制器的 “身份名片”,包含了众多描述主机控制器特性和功能的成员。
①s16 bus_num:表示 SPI 主机控制器的编号。在一个系统中可能存在多个 SPI 主机控制器,bus_num 用于唯一标识每个控制器。比如,一个开发板上有两个 SPI 主机控制器,它们的 bus_num 可能分别为 0 和 1,通过这个编号,内核可以准确地区分和管理不同的控制器。
// 定义 SPI 主机编号,唯一标识控制器.bus_num = 0, // 该主机控制器编号为 0②u16 num_chipselect:定义了控制器支持的片选数量,即该控制器能够连接并控制的 SPI 从设备的数量。如果 num_chipselect 的值为 4,那就意味着这个 SPI 主机控制器最多可以连接 4 个 SPI 从设备,通过不同的片选信号来选择与之通信的从设备。
// 配置主机最多支持 4 个片选从设备.num_chipselect = 4,③int (*setup)(struct spi_device *spi):这是一个函数指针,指向用于设置 SPI 设备工作参数的函数。在 SPI 设备与主机控制器进行通信前,需要对 SPI 设备的工作模式(如 CPOL、CPHA)、时钟频率等参数进行设置,setup 函数就是用来完成这些设置工作的。例如,我们可以在 setup 函数中设置 SPI 设备的工作模式为 SPI_MODE_0,时钟频率为 1MHz。
// SPI 主机 setup 配置函数示例static int my_spi_setup(struct spi_device *spi){ // 设置模式:SPI_MODE_0,时钟频率 1MHz spi->mode = SPI_MODE_0; spi->max_speed_hz = 1000000; return 0;}// 挂载到 spi_master.setup = my_spi_setup,④int (*transfer)(struct spi_device *spi, struct spi_message *mesg):同样是函数指针,指向 SPI 数据传输函数。当 SPI 设备需要进行数据传输时,就会调用 transfer 函数。它负责将 spi_message 结构体中包含的数据按照 SPI 协议的要求,通过主机控制器发送到 SPI 总线上,并接收从设备返回的数据。
// SPI 主机底层传输函数实现static int my_spi_transfer(struct spi_device *spi, struct spi_message *mesg){ // 调用硬件控制器,完成 SPI 数据收发 spi_hw_transfer(spi, mesg); mesg->status = 0; spi_message_complete(mesg); return 0;}// 挂载到 spi_master.transfer = my_spi_transfer,spi_driver 结构体是 SPI 设备驱动的关键组成部分,它定义了设备驱动与内核交互的接口和行为。
①const struct spi_device_id *id_table:设备 ID 表,用于匹配设备驱动和设备。在这个表中,列出了该驱动能够支持的 SPI 设备的 ID。当内核检测到一个 SPI 设备时,会将设备的 ID 与 id_table 中的 ID 进行比对,如果匹配成功,就会调用该驱动的 probe 函数。
// SPI 设备 ID 匹配表示例(对应 id_table)static conststruct spi_device_id spi_sensor_ids[] = { { "temp_sensor", 0 }, // 支持温度传感器 { "imu_sensor", 1 }, // 支持惯性传感器 { },};MODULE_DEVICE_TABLE(spi, spi_sensor_ids);②int (*probe)(struct spi_device *spi):探测函数,当设备驱动与设备匹配成功后,内核会调用 probe 函数。在 probe 函数中,主要完成设备的初始化工作,例如申请设备资源(如内存、中断等)、设置设备的工作参数、创建设备节点等。以 SPI 温度传感器为例,在 probe 函数中,我们可以初始化传感器的寄存器,设置数据读取的格式和频率,同时创建一个设备节点,以便用户空间能够通过该节点与传感器进行交互。
// SPI 设备 probe 实现示例static int spi_sensor_probe(struct spi_device *spi){ // 1. 设置 SPI 通信模式、时钟 spi_setup(spi); // 2. 初始化传感器硬件 sensor_init_reg(spi); // 3. 创建设备节点,供用户层使用 misc_device_create(&sensor_dev); printk("SPI 传感器探测成功\n"); return 0;}probe 函数和 id_table 在设备驱动中至关重要,它们是设备驱动与设备建立联系的桥梁。准确实现 probe 函数,正确设置 id_table,是确保 SPI 设备驱动能够正常工作的关键。
三、SPI 设备与驱动的匹配机制
面试题写作模版在基于设备树(Device Tree)的嵌入式系统中,设备树匹配是 SPI 设备与驱动匹配的主要方式 。设备树是一种描述硬件资源的数据结构,它以树形结构组织,包含了系统中各种设备的信息,如设备的名称、地址、中断号、SPI 通信参数等 。
在设备树中,每个 SPI 设备都有一个对应的节点,节点中的 compatible 属性用于描述设备的兼容性信息 。例如,对于一个 SPI 接口的 Flash 存储器,其设备树节点可能如下:
spi1: spi@02100000 { compatible = "fsl,imx6ul-ecspi", "fsl,imx6q-ecspi"; reg = <0x02100000 0x4000>; interrupts = <GIC_SPI 29 IRQ_TYPE_LEVEL_HIGH>; status = "okay"; flash: flash@0 { compatible = "micron,n25q128a13ef"; reg = <0>; spi-max-frequency = <50000000>; };};其中,flash 节点的 compatible 属性值为"micron,n25q128a13ef",它表示这个设备是美光(Micron)公司的 N25Q128A13EF 型号的 SPI Flash 。
在 SPI 设备驱动中,会定义一个 of_match_table 数组,用于存储驱动支持的设备树 compatible 属性值 。例如:
static conststruct of_device_id my_spi_flash_of_match[] = { {.compatible = "micron,n25q128a13ef"}, {},};MODULE_DEVICE_TABLE(of, my_spi_flash_of_match);当内核启动时,会解析设备树,创建对应的 spi_device 对象 。同时,当 SPI 设备驱动注册时,内核会调用 of_driver_match_device 函数,将设备树节点的 compatible 属性值与驱动的 of_match_table 数组中的值进行比较 。如果找到匹配的项,就认为设备和驱动匹配成功,内核会调用驱动的 probe 函数,对设备进行初始化 。
ACPI(Advanced Configuration and Power Interface,高级配置与电源管理接口)匹配主要用于 x86 平台以及一些支持 ACPI 的系统中 。在 ACPI 系统中,每个设备都有一个 ACPI 名称空间,通过硬件 ID(HID)和兼容 ID(CID)来标识设备 。SPI 设备驱动可以通过定义 acpi_match_table 数组来匹配 ACPI 设备 。例如:
static conststruct acpi_device_id my_spi_acpi_ids[] = { {"ACPI0001", 0}, // 匹配 ACPI 设备的硬件 ID {},};MODULE_DEVICE_TABLE(acpi, my_spi_acpi_ids);当内核检测到 ACPI 设备时,会将设备的 HID 和 CID 与驱动的 acpi_match_table 中的值进行比较,若匹配成功,则完成设备与驱动的匹配 。
在没有设备树的情况下,SPI 设备与驱动的匹配采用传统的匹配方式 。一种方式是对比驱动 id_table 中的设备名 。驱动中定义 id_table 数组,包含支持的设备名称 。例如:
static conststruct spi_device_id my_spi_sensor_ids[] = { {"my_sensor", 0}, {},};MODULE_DEVICE_TABLE(spi, my_spi_sensor_ids);内核将 spi_device 的 modalias 与 id_table 中的设备名进行比较,若一致则匹配成功 。另一种方式是直接校验 spi_device 的 modalias 与驱动 name 是否一致 。若相同,也可认为设备与驱动匹配 。不过,随着设备树的广泛应用,这种传统匹配方式在现代嵌入式开发中使用相对较少 。
四、编写 SPI 驱动实战:以温度传感器为例
面试题写作模版开发 SPI 设备驱动前,首先要排查 SPI 控制器与外设的物理连接,重点检查 SCK、MOSI、MISO、SS/CS 四条核心信号线,以及供电、接地等辅助线路,线路的通断与稳定性直接决定 SPI 通信能否正常进行。其中 SCK 时钟线异常,会造成主从设备数据传输时序错乱;MOSI 主机输出线故障,会导致主机数据无法下发给从设备;MISO 从机输出线异常,主机就接收不到外设的反馈数据;而 SS/CS 片选线连接错误,主设备将无法精准匹配目标从设备,引发通信异常。我们可以借助万用表检测线路连通性,排查短路、断路、线路松动等问题,保障硬件基础连接稳定可靠。
硬件检查完成后,需要配置内核,开启 SPI 子系统与对应控制器的驱动支持,这是 SPI 驱动正常运行的软件基础。我们只需进入内核源码目录,执行 make menuconfig 命令打开内核配置界面,在设备驱动选项中开启 SPI 基础功能支持,同时根据开发板芯片型号,选中对应的 SPI 控制器驱动。除此之外,可根据开发需求灵活开启 SPI 调试、DMA 传输等辅助功能,方便后续开发调试工作。完成所有配置后保存退出,内核就具备了 SPI 设备的运行基础环境,后续即可开展 SPI 设备树配置与驱动开发工作。
在 SPI 设备驱动中,需要声明支持的设备名列表和 OF 匹配表 。首先,定义设备 ID 表,它用于匹配设备驱动和设备 。例如:
static conststruct spi_device_id my_temp_sensor_ids[] = { {"my_temp_sensor", 0}, {},};MODULE_DEVICE_TABLE(spi, my_temp_sensor_ids);这里定义了一个 my_temp_sensor_ids 数组,其中包含了驱动支持的设备名 my_temp_sensor 。
同时,在基于设备树的系统中,要定义 OF 匹配表,用于与设备树中的设备节点进行匹配 。假设温度传感器的设备树节点 compatible 属性值为"vendor,my-temp-sensor" ,则 OF 匹配表的定义如下:
static conststruct of_device_id my_temp_sensor_of_match[] = { {.compatible = "vendor,my-temp-sensor"}, {},};MODULE_DEVICE_TABLE(of, my_temp_sensor_of_match);这样,当内核检测到设备树中的设备节点时,会将其 compatible 属性值与驱动的 OF 匹配表进行比对,若匹配成功,就会调用驱动的 probe 函数 。
probe 函数是 SPI 设备驱动的核心函数之一,当设备驱动与设备匹配成功后,内核会调用 probe 函数 。在 probe 函数中,首先要初始化硬件相关的资源 。例如,分配设备私有数据结构体,用于存储与设备相关的信息 :
static int my_temp_sensor_probe(struct spi_device *spi) { int ret; // 分配设备私有数据结构体struct my_temp_sensor_private *priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; spi_set_drvdata(spi, priv);接着,设置 SPI 工作模式、时钟频率等参数 。假设温度传感器要求 SPI 工作在模式 0,时钟频率为 1MHz :
spi->mode = SPI_MODE_0; spi->max_speed_hz = 1000000; ret = spi_setup(spi); if (ret) { dev_err(&spi->dev, "spi setup failed: %d\n", ret); return ret; }完成硬件初始化后,就可以实现数据读写操作了 。以读取温度传感器的数据为例,使用 spi_sync 函数进行同步数据传输 :
// 构建 SPI 传输消息结构体struct spi_transfer tr = { .tx_buf = NULL, .rx_buf = priv->rx_buffer, .len = sizeof(priv->rx_buffer),};struct spi_message msg;spi_message_init(&msg);spi_message_add_tail(&tr, &msg);ret = spi_sync(spi, &msg);if (ret) { dev_err(&spi->dev, "SPI read failed: %d\n", ret); return ret;}// 解析读取到的数据,获取温度值// 这里假设温度数据存储在 rx_buffer 的前两个字节,并且需要进行一定的转换uint16_t temp_raw = ((uint16_t)priv->rx_buffer[0] << 8) | priv->rx_buffer[1];float temperature = convert_temp(temp_raw);定义 spi_driver 结构并注册驱动是让驱动生效的关键步骤 。定义 spi_driver 结构如下:
staticstruct spi_driver my_temp_sensor_driver = { .driver = { .name = "my_temp_sensor_driver", .owner = THIS_MODULE, .of_match_table = my_temp_sensor_of_match, }, .id_table = my_temp_sensor_ids, .probe = my_temp_sensor_probe, .remove = my_temp_sensor_remove,};然后,在驱动模块加载时,使用 spi_register_driver 函数注册驱动 :
static int __init my_temp_sensor_init(void) { return spi_register_driver(&my_temp_sensor_driver);}module_init(my_temp_sensor_init);在设备树中添加 SPI 设备描述信息,确保设备树与驱动能够正确匹配 。假设 SPI 控制器节点为 spi1 ,温度传感器连接到 spi1 的片选 0,设备树节点示例如下:
spi1: spi@02100000 { compatible = "fsl,imx6ul-ecspi", "fsl,imx6q-ecspi"; reg = <0x02100000 0x4000>; interrupts = <GIC_SPI 29 IRQ_TYPE_LEVEL_HIGH>; status = "okay"; temp_sensor: temp_sensor@0 { compatible = "vendor,my-temp-sensor"; reg = <0>; spi-max-frequency = <1000000>; };};其中,compatible 属性与驱动的 OF 匹配表中的值一致,spi-max-frequency 指定了 SPI 通信的时钟频率 。
将编写好的 SPI 设备驱动代码编译为内核模块,可以通过修改驱动目录下的 Makefile 文件来实现 。假设驱动源文件为 my_temp_sensor_driver.c ,Makefile 文件内容示例如下:
obj-m += my_temp_sensor_driver.oall: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modulesclean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean执行 make 命令,即可生成内核模块文件 my_temp_sensor_driver.ko 。
调试 SPI 驱动时,可以使用 dmesg 命令查看内核日志,了解驱动加载过程中是否有错误信息 。例如,执行 dmesg | grep spi ,可以查看与 SPI 相关的日志 。若发现问题,可以在驱动代码中添加 printk 语句,输出调试信息 。
优化驱动性能方面,可以从多个角度入手 。比如,合理设置 SPI 的工作模式和时钟频率,以平衡数据传输速度和稳定性;优化数据读写算法,减少不必要的内存拷贝和系统调用次数 。同时,要注意资源的合理使用,避免内存泄漏和资源竞争等问题 。
end
如果这篇文章对你有所启发,欢迎点赞、在看,转发三连。星标⭐账号,还可以第一时间收到推送,感谢你的收看,我们下期再见~
往期干货推荐