当前位置:首页>Linux>不懂 Linux SPI 驱动,看完这篇就明白了

不懂 Linux SPI 驱动,看完这篇就明白了

  • 2026-10-10 18:30:29
不懂 Linux SPI 驱动,看完这篇就明白了

在嵌入式 Linux 开发中,SPI 是使用率极高的高速串行通信总线,广泛用于驱动 Flash、显示屏、传感器、ADC 等外设。很多嵌入式开发者熟悉 SPI 基础协议与时序,但在接触 Linux 内核 SPI 驱动时,常常因为内核分层架构、总线匹配机制和设备树配置复杂而难以理解。相比于裸机单片机的简单寄存器操作,Linux SPI 驱动采用标准化总线框架,拥有固定的注册、匹配、传输流程,新手很容易混淆设备、总线与驱动三者的工作关系。

其实 Linux SPI 驱动有着清晰、固定的开发逻辑,并不需要死记复杂源码。本文将用通俗易懂的方式,从零讲解 Linux SPI 驱动的整体框架、分层原理、设备树匹配方式与数据传输流程,避开晦涩的底层源码堆砌,梳理核心工作机制。帮助新手快速理清 SPI 驱动的运行逻辑,彻底看懂 Linux SPI 通信的底层原理,解决开发中看不懂、写不出、调不通的常见问题。

一、温故 SPI 总线协议

面试题写作模版

1.1 什么是 SPI?

SPI,即串行外设接口(Serial Peripheral Interface) ,是一种高速、全双工、同步的通信总线,由摩托罗拉公司于上世纪 80 年代开发,在嵌入式系统中广泛应用,用于微控制器与各种外设之间的通信,如传感器、存储器、显示屏等。

SPI 采用主从架构,一个 SPI 系统中,必然有一个主设备,同时可以连接一个或多个从设备 。主设备就像乐队的指挥,掌控着整个通信的节奏和流程,负责发起通信并产生时钟信号(SCK);从设备则像乐队的成员,听从主设备的指挥,根据主设备提供的时钟信号进行数据的接收和发送。比如在一个智能手表的系统中,主控芯片作为 SPI 主设备,与作为从设备的心率传感器、加速度传感器等进行通信,获取各种数据。

SPI 通信硬件简单,通常仅需四条线即可完成通信,这四条线分别是:串行时钟线(SCK)、主机输出 / 从机输入数据线(MOSI)、主机输入 / 从机输出数据线(MISO)和从机选择线(SS/CS,Slave Select/Chip Select) 。这种简单的硬件架构,不仅节约了芯片的管脚资源,也让 PCB 的布局更加简洁,为电路设计带来了极大的便利。

1.2 SPI 硬件组成与信号解析

SPI 通信依靠四条信号线实现数据交互。MISO 为主入从出线路,负责从设备向主设备传输数据,比如主设备读取温度传感器信息时,传感器就通过这条线路传回数据。MOSI 为主出从入线路,用于主设备向从设备下发指令、地址和待写入数据,像主设备向 Flash 存储器保存数据,数据便会经由这条线路完成传输。

SCK 是由主设备产生的串行时钟信号,相当于通信的节拍器,用来同步主从设备的数据收发节奏,时钟频率越高,传输速度也就越快。SS/CS 为片选信号,在多从设备的系统中,主设备通过拉低对应电平选中目标设备,未被选中的设备保持静默,不会响应总线上的信号。

1.3 SPI 通信原理与过程

SPI 通信基于串行移位寄存器来实现数据传输。在通信前,主设备和从设备都配置好相同的通信参数,包括数据位宽、时钟极性(CPOL)、时钟相位(CPHA)以及数据传输顺序(MSB 先还是 LSB 先)等。以主设备向从设备发送数据 0x55(二进制为 01010101),同时从设备向主设备发送数据 0xAA(二进制为 10101010)为例:

  1. 片选阶段:主设备首先将目标从设备的 SS/CS 信号拉低,通知该从设备准备通信 。这就像是老师点名叫某个学生来回答问题,学生听到自己的名字后,就做好回答问题的准备。
  2. 时钟与数据传输阶段:主设备开始产生 SCK 时钟信号。在每个时钟周期内,主设备通过 MOSI 线将数据 0x55 的一位发送出去,从设备在 SCK 信号的边沿(根据 CPHA 的设置,可能是上升沿或下降沿)接收该位数据,并将其移入自己的移位寄存器;同时,从设备通过 MISO 线将数据 0xAA 的一位发送给主设备,主设备也在相应的时钟边沿接收该位数据并移入自己的移位寄存器 。这个过程就像是两个人在传递纸条,一个人写一个字递过去,另一个人同时也写一个字递回来,而且他们的动作要按照一定的节奏(时钟信号)来进行。
  3. 数据接收与处理阶段:经过 8 个时钟周期(假设数据位宽为 8 位),主设备和从设备都完成了一个字节数据的发送和接收。主设备将接收到的数据存储起来进行后续处理,从设备也同样处理接收到的数据 。比如主设备接收到从设备发送的温度数据后,可能会进行分析、显示等操作;从设备接收到主设备发送的控制指令后,会根据指令执行相应的动作。
  4. 通信结束阶段:主设备完成数据传输后,将 SS/CS 信号拉高,结束与该从设备的通信 。这就像是老师和学生交流完问题后,让学生坐下,结束这次交流。

二、Linux SPI 驱动架构剖析

面试题写作模版

2.1 驱动分层与分离设计思想

Linux 驱动设计秉持着分层与分离的精妙思想 ,这一思想在 SPI 驱动中展现得淋漓尽致。分层,就如同搭建高楼,每一层都有其独特的职责与功能。SPI 驱动主要分为核心层、主机控制器驱动层和设备驱动层 。核心层位于中间位置,起着承上启下的关键作用,它向上为设备驱动提供统一的 API 接口,向下则为不同的主机控制器驱动提供通用的管理机制 。

而分离思想,则是让主机控制器驱动和设备驱动各自专注于自己的领域 。主机控制器驱动负责与硬件层面的 SPI 控制器打交道,根据具体的硬件手册操作控制器,产生 SPI 总线上的各种波形信号,它不关心连接的具体外设是什么;设备驱动则聚焦于特定的外设,如温度传感器、Flash 存储器等,通过核心层提供的 API 来与主机控制器进行通信,无需了解主机控制器的硬件细节 。

这种分层与分离的设计,极大地提升了代码的可维护性、可扩展性以及复用性。比如,当更换不同型号的 SPI 主机控制器时,只需要修改主机控制器驱动层的代码,设备驱动层和核心层可以保持不变;同样,当连接新的 SPI 外设时,只需编写对应的设备驱动,主机控制器驱动和核心层也无需改动 。

2.2 核心层功能与关键 API

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);

2.3 主机驱动 spi_master 核心解析

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,

2.4 设备驱动 spi_driver 实现要点

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 设备与驱动的匹配机制

面试题写作模版

3.1 设备树匹配方式

在基于设备树(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 函数,对设备进行初始化 。

3.2 ACPI 匹配与传统匹配

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 驱动实战:以温度传感器为例

面试题写作模版

4.1 前期准备:硬件与内核环境搭建

开发 SPI 设备驱动前,首先要排查 SPI 控制器与外设的物理连接,重点检查 SCK、MOSI、MISO、SS/CS 四条核心信号线,以及供电、接地等辅助线路,线路的通断与稳定性直接决定 SPI 通信能否正常进行。其中 SCK 时钟线异常,会造成主从设备数据传输时序错乱;MOSI 主机输出线故障,会导致主机数据无法下发给从设备;MISO 从机输出线异常,主机就接收不到外设的反馈数据;而 SS/CS 片选线连接错误,主设备将无法精准匹配目标从设备,引发通信异常。我们可以借助万用表检测线路连通性,排查短路、断路、线路松动等问题,保障硬件基础连接稳定可靠。

硬件检查完成后,需要配置内核,开启 SPI 子系统与对应控制器的驱动支持,这是 SPI 驱动正常运行的软件基础。我们只需进入内核源码目录,执行 make menuconfig 命令打开内核配置界面,在设备驱动选项中开启 SPI 基础功能支持,同时根据开发板芯片型号,选中对应的 SPI 控制器驱动。除此之外,可根据开发需求灵活开启 SPI 调试、DMA 传输等辅助功能,方便后续开发调试工作。完成所有配置后保存退出,内核就具备了 SPI 设备的运行基础环境,后续即可开展 SPI 设备树配置与驱动开发工作。

4.2 定义设备 ID 表与兼容设备树节点

在 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 函数 。

4.3 实现 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);

4.4 注册驱动与配置设备树

定义 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 通信的时钟频率 。

4.5 编译、调试与优化

将编写好的 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

如果这篇文章对你有所启发,欢迎点赞、在看,转发三连。星标⭐账号,还可以第一时间收到推送,感谢你的收看,我们下期再见~

往期干货推荐

☑【专栏模块】嵌入式Linux
☑【专栏模块】性能优化
☑【专栏模块】面试八股文
☑【专栏模块】项目实战
☑【硬核干货】缺了这些,别说你懂 Linux内核
☑【硬核干货】缺了这些,别说你懂 Linux C/C++
☑【学习思维导图】Linux内核源码自主学习路线

最新文章

随机文章