1. 从零开始:理解SPI Master驱动开发的核心

如果你刚开始接触嵌入式Linux驱动开发,看到SPI Master驱动可能会觉得有点复杂。别担心,我刚开始搞这个的时候也是一头雾水,感觉数据手册里全是天书。但实际摸爬滚打几年下来,我发现SPI驱动其实就像搭积木,只要把几个关键模块搞清楚了,剩下的就是按部就班地组装。这篇文章,我就想用最直白的话,把我这些年从设备树配置到数据传输实战踩过的坑、总结的经验,毫无保留地分享给你。

简单来说,SPI Master驱动就是让Linux系统能够通过SPI总线去控制和通信外设的“桥梁”。SPI本身是一种高速、全双工的同步串行通信接口,在嵌入式领域用得特别广,比如连接Flash存储器、触摸屏、传感器等等。开发一个SPI Master驱动,核心任务就三件事:第一,在设备树里正确描述硬件连接和参数;第二,在内核框架下搭建驱动的基本骨架;第三,实现具体的数据收发功能。听起来是不是没那么吓人了?

我见过很多新手朋友一上来就扎进内核源码里看spi.c,结果被各种结构体指针绕晕。我的建议是,先别急着看代码,咱们得把SPI通信的基本流程在脑子里过一遍。你可以把它想象成一条双向单车道的马路(SPI总线),主设备(Master)是交警,负责指挥交通(产生时钟SCK),从设备(Slave)是等待通行的车辆。交警通过MOSI线把指令发给车辆,车辆通过MISO线把状态报告给交警。而片选信号CS,就像是交警点名让哪一辆车开始通行。驱动要做的,就是扮演好这个“交警系统”的软件部分,管理好这条马路上的通信秩序。

2. 基石:设备树配置详解与避坑指南

在动手写代码之前,设备树配置是第一步,也是决定驱动能否正常工作的基石。很多驱动问题追根溯源,都是设备树没写对。设备树的作用就是告诉内核:“嘿,咱们板子上有这么个SPI控制器硬件,它连着哪些引脚,外接了啥设备,参数是什么”。内核在启动时解析这个“硬件描述文件”,然后动态地创建相应的设备。

2.1 SPI控制器节点配置

一个SPI Master控制器在设备树里是一个独立的节点。我们以虚拟的SPI控制器为例,看看它长什么样:

virtual_spi_master {
    compatible = "100ask,virtual_spi_master";
    status = "okay";
    cs-gpios = <&gpio4 27 GPIO_ACTIVE_LOW>;
    num-chipselects = <1>;
    #address-cells = <1>;
    #size-cells = <0>;
    // 子节点会放在这里,代表连接的SPI设备
};

我来逐行解释一下这些属性的含义,这些都是我调试时总结出来的关键点:

  • compatible:这是最重要的属性,没有之一。内核正是通过这个字符串去匹配对应的驱动。格式通常是“厂商,设备型号”。比如这里"100ask,virtual_spi_master",内核就会去寻找of_device_id表中包含这个字符串的驱动。写错了,驱动就永远探测不到你的设备。
  • status:设为"okay"表示启用这个节点。有时候为了调试,可以临时改成"disabled",非常方便。
  • cs-gpios:指定片选信号所使用的GPIO。这是一个GPIO列表,每个SPI从设备对应一个。GPIO_ACTIVE_LOW表示低电平有效,这是最常见的。有些芯片可能高电平有效,那就用GPIO_ACTIVE_HIGH。我遇到过因为这里没写对,导致片选信号反了,数据怎么也读不出来的情况。
  • num-chipselects:声明这个控制器支持多少个片选信号,也就是最多能接多少个SPI设备。这个值要和cs-gpios列表的长度对应上。
  • #address-cells 和 #size-cells:这是设备树的标准语法,用于描述子节点的地址信息。对于SPI总线,通常设置为<1>和<0>。意思是子节点(SPI设备)的reg属性只需要一个cell(通常就是片选编号),并且没有大小范围的概念。

2.2 SPI设备子节点配置

控制器节点下面,就可以挂载具体的SPI设备了。每个设备是一个子节点。

virtual_spi_master {
    // ... 控制器属性同上
    virtual_spi_dev: virtual_spi_dev@0 {
        compatible = "spidev";
        reg = <0>;
        spi-max-frequency = <100000>;
        // 其他SPI模式参数
    };
};
  • compatible:同样用于匹配设备驱动。这里用了"spidev",这是一个内核提供的通用用户态SPI设备驱动,方便测试。在实际产品中,你会换成自己外设的驱动名,比如"jedec,spi-nor"。
  • reg:这个值就是该设备使用的片选编号。对应cs-gpios列表中的索引。比如reg = <0>就表示使用cs-gpios列表里的第一个GPIO作为片选。
  • spi-max-frequency:必选项!指定该设备支持的最大SPI时钟频率,单位是Hz。驱动会确保不超过这个速度。设置时一定要参考外设数据手册的最大值,设高了可能通信失败。
  • 其他模式参数:这些是可选的,但非常重要,用于配置SPI的四种工作模式。
    • spi-cpol:空属性(只写名字,不赋值),表示时钟极性CPOL=1(空闲时为高电平)。不写则默认为0。
    • spi-cpha:空属性,表示时钟相位CPHA=1(在第二个时钟边沿采样数据)。不写则默认为0。
    • spi-cs-high:空属性,表示片选信号高电平有效。不写则默认为低电平有效。
    • spi-3wire:空属性,表示使用三线制SPI(没有MISO,数据半双工)。不写则使用标准的四线制。
    • spi-lsb-first:空属性,表示先传输最低有效位。不写则默认先传输最高有效位。

避坑经验:SPI模式(CPOL/CPHA)必须和外设严格匹配,这是通信成功的前提。我建议在设备树里明确写上这些属性,即使使用默认值,也最好注释清楚,避免后期维护时遗忘。曾经有个项目,硬件同事把某个传感器的模式改了,但设备树没更新,我们花了整整两天才定位到这个“低级错误”。

3. 驱动框架搭建:两种方法深入剖析

设备树配置好,编译进内核或作为模块加载后,内核就能识别到我们的SPI控制器了。接下来就是驱动本身。Linux内核的SPI子系统提供了非常清晰的框架,我们需要实现一个platform_driver,并在其probe函数中完成核心初始化。这里内核提供了两种实现数据传输的路径,我称之为“老方法”和“新方法”,它们各有适用场景。

3.1 核心结构体:spi_controller

无论用哪种方法,驱动都需要分配并初始化一个spi_controller结构体(旧版本内核中叫spi_master)。这个结构体是驱动对内核的“承诺书”,里面包含了你的控制器能力描述和关键的操作函数指针。

struct spi_controller *master;
master = spi_alloc_master(&pdev->dev, sizeof(struct my_private_data));
if (!master) {
    dev_err(&pdev->dev, "spi_alloc_master failed\n");
    return -ENOMEM;
}

spi_alloc_master这个函数名虽然没改,但返回的其实是spi_controller。第二个参数是你要分配的私有数据大小,你可以把一些硬件寄存器地址、状态变量等放在这里,通过spi_controller_get_devdata()来获取。这是驱动开发的常用技巧。

初始化时,有几个关键字段必须设置:

  • master->dev.of_node = pdev->dev.of_node;:关联设备树节点。
  • master->num_chipselect:片选数量,通常从设备树读取。
  • master->bits_per_word_mask:支持的每字位数(如8位、16位)。
  • master->mode_bits:支持的SPI模式位(如SPI_CPOL,SPI_CPHA)。
  • 最重要的,是设置master->transfer_one_message或master->transfer,这就是数据传输的入口。两种方法的区别就在这里。

3.2 方法一:老方法(实现transfer函数)

这是比较传统和直接的方式。你需要实现spi_controller->transfer函数。这个函数的职责是处理一个完整的spi_message。spi_message可能包含多个spi_transfer,它们代表一次通信中不同参数(比如速度、延时)的连续传输段。

在老方法中,你需要自己管理消息队列的推进。通常做法是:

  1. 在transfer函数里,把传入的spi_message加入到控制器自己的待处理队列尾部。
  2. 启动硬件传输(如果是DMA或中断方式)。
  3. 在传输完成的中断处理函数中,标记当前spi_transfer完成,并检查message中是否还有下一个transfer,如果有就启动下一个;如果没有,就调用message->complete()回调函数,通知上层本次消息传输全部完成。
  4. 从队列中取出下一个spi_message,重复上述过程。

这种方式给了驱动开发者最大的灵活性,但同时也需要处理更多的细节,比如队列锁、状态机管理、错误处理等。对于简单的GPIO模拟SPI或者非常特定的硬件,这种方式可能更直接。但它的缺点也很明显:代码容易冗长,且容易出错。

3.3 方法二:新方法(实现transfer_one_message并使用核心队列)

这是内核推荐的新方式,也是更简单、更不容易出错的方式。你不再需要实现transfer,而是实现transfer_one_message函数。同时,你需要调用spi_controller_initialize_queue()来启用内核提供的通用消息队列和kthread工作线程。

在这种模式下,驱动的任务被简化了:

  1. 你只需要实现transfer_one_message。这个函数只负责处理一个spi_message。内核的队列线程会保证一次只给你一个message。
  2. 在这个函数里,你遍历message->transfers链表,依次处理每一个spi_transfer。处理完所有transfer后,设置message->status并返回。
  3. 内核的队列线程会自动调用你设置的message->complete回调,并处理下一个排队的message。

核心优势:驱动开发者无需关心队列调度、锁竞争等复杂问题,可以更专注于单个消息的硬件传输逻辑。内核已经帮你把复杂的异步、排队机制做好了。绝大多数情况下,我都强烈推荐使用这种方法。

为了让对比更清晰,我画了一个简单的表格:

特性老方法 (实现transfer)新方法 (实现transfer_one_message)
队列管理驱动自己管理内核通用队列管理
并发控制驱动自己处理锁内核处理
代码复杂度高,需处理完整流程低,只需处理单次传输
推荐度特定硬件或学习原理绝大多数情况推荐
核心函数master->transfermaster->transfer_one_message
初始化手动初始化队列等调用spi_controller_initialize_queue()

4. 数据传输实战:代码示例与逐行解析

理论说再多,不如看代码来得实在。下面我分别用两种方法,实现一个“虚拟”的SPI Master驱动。这个驱动不操作真实硬件,但完整展示了框架和流程,你可以在任何平台上编译测试,是学习的最佳模板。

4.1 老方法实现示例

首先看设备树,我们假设这个虚拟控制器使用一个GPIO作为片选:

// 设备树节点
virtual_spi_master {
    compatible = "100ask,virtual_spi_master";
    status = "okay";
    cs-gpios = <&gpio4 27 GPIO_ACTIVE_LOW>;
    num-chipselects = <1>;
    #address-cells = <1>;
    #size-cells = <0>;
    virtual_spi_dev@0 {
        compatible = "spidev";
        reg = <0>;
        spi-max-frequency = <100000>;
    };
};

驱动代码如下,我加了大量注释:

#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/platform_device.h>
#include <linux/workqueue.h>

static struct spi_controller *g_virtual_master;
static struct work_struct g_virtual_ws; // 用于模拟异步传输的工作队列

// 1. 定义设备树匹配表
static const struct of_device_id spi_virtual_dt_ids[] = {
    { .compatible = "100ask,virtual_spi_master", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, spi_virtual_dt_ids);

// 2. 工作队列处理函数:模拟传输完成
static void spi_virtual_work(struct work_struct *work)
{
    struct spi_message *mesg;
    struct spi_controller *master = g_virtual_master;
    unsigned long flags;

    // 加锁访问控制器队列
    spin_lock_irqsave(&master->queue_lock, flags);
    while (!list_empty(&master->queue)) {
        // 从队列头取出一个消息
        mesg = list_first_entry(&master->queue, struct spi_message, queue);
        list_del_init(&mesg->queue); // 从队列移除

        spin_unlock_irqrestore(&master->queue_lock, flags); // 处理期间可释放锁

        /* 模拟硬件传输完成 */
        mesg->status = 0; // 成功
        mesg->actual_length = mesg->frame_length; // 假设全部传输完成

        // 调用上层应用设置的回调函数,通知传输完成
        if (mesg->complete)
            mesg->complete(mesg->context);

        spin_lock_irqsave(&master->queue_lock, flags); // 继续处理下一个消息前加锁
    }
    spin_unlock_irqrestore(&master->queue_lock, flags);
}

// 3. 核心:transfer函数实现(老方法)
static int spi_virtual_transfer(struct spi_device *spi, struct spi_message *mesg)
{
    struct spi_controller *master = spi->controller;
    unsigned long flags;

    // 初始化消息状态
    mesg->actual_length = 0;
    mesg->status = -EINPROGRESS; // 进行中

    // 将消息加入控制器队列尾部
    spin_lock_irqsave(&master->queue_lock, flags);
    list_add_tail(&mesg->queue, &master->queue);
    spin_unlock_irqrestore(&master->queue_lock, flags);

    // 调度工作队列,模拟异步传输启动
    schedule_work(&g_virtual_ws);

    // 注意:这里直接返回0,表示成功接收了消息。
    // 真正的传输完成是通过工作队列异步回调 mesg->complete 通知的。
    return 0;
}

// 4. probe函数:驱动初始化
static int spi_virtual_probe(struct platform_device *pdev)
{
    struct spi_controller *master;
    int ret;

    // 分配spi_controller结构体
    master = spi_alloc_master(&pdev->dev, 0);
    if (!master) {
        dev_err(&pdev->dev, "spi_alloc_master failed\n");
        return -ENOMEM;
    }
    g_virtual_master = master;

    // 设置老方法的核心:transfer函数指针
    master->transfer = spi_virtual_transfer;

    // 初始化工作队列(模拟硬件中断/完成机制)
    INIT_WORK(&g_virtual_ws, spi_virtual_work);

    // 设置片选数量(应从设备树解析,这里简化)
    master->num_chipselect = 1;
    master->dev.of_node = pdev->dev.of_node;

    // 注册SPI控制器
    ret = spi_register_controller(master);
    if (ret < 0) {
        dev_err(&pdev->dev, "spi_register_controller failed\n");
        spi_controller_put(master);
        return ret;
    }

    dev_info(&pdev->dev, "virtual SPI master registered\n");
    return 0;
}

// 5. remove函数:驱动卸载
static int spi_virtual_remove(struct platform_device *pdev)
{
    spi_unregister_controller(g_virtual_master);
    // 取消可能未完成的工作
    cancel_work_sync(&g_virtual_ws);
    return 0;
}

// 6. 定义platform_driver
static struct platform_driver spi_virtual_driver = {
    .driver = {
        .name = "virtual_spi",
        .of_match_table = spi_virtual_dt_ids,
    },
    .probe = spi_virtual_probe,
    .remove = spi_virtual_remove,
};
module_platform_driver(spi_virtual_driver);

MODULE_DESCRIPTION("Virtual SPI Master Driver (Old Method)");
MODULE_LICENSE("GPL");

代码要点解析:

  • spi_virtual_transfer是核心,它仅仅是把spi_message放入队列,然后调度一个工作队列来模拟“传输完成”。真实驱动中,这里会启动DMA或配置硬件寄存器开始传输。
  • 工作队列函数spi_virtual_work模拟了传输完成中断:它从队列取出消息,标记成功,并调用complete回调。这是异步传输的典型模式。
  • 这种模式下,驱动必须自己管理queue_lock来保护队列,代码稍显繁琐。

4.2 新方法实现示例(推荐)

新方法我们使用spi_bitbang作为基础,它封装了更多细节,让我们更关注单次传输。设备树节点完全一样,我们看驱动代码的变化:

#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/spi/spi_bitbang.h> // 引入bitbang头文件
#include <linux/platform_device.h>
#include <linux/completion.h>

static struct spi_controller *g_virtual_master;
static struct spi_bitbang *g_virtual_bitbang;
static struct completion g_xfer_done; // 用于同步等待传输完成

// 1. 设备树匹配表(同上)
static const struct of_device_id spi_virtual_dt_ids[] = {
    { .compatible = "100ask,virtual_spi_master", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, spi_virtual_dt_ids);

// 2. 核心:处理单个spi_transfer的函数
static int spi_virtual_txrx_bufs(struct spi_device *spi,
                                 struct spi_transfer *transfer)
{
    // 这个函数处理一个transfer。对于真实硬件,这里会:
    // 1. 配置硬件时钟、模式
    // 2. 写入tx_buf数据到发送寄存器,或从接收寄存器读取rx_buf
    // 3. 等待传输完成(中断或轮询)

    // 我们这里用completion模拟等待硬件中断完成
    reinit_completion(&g_xfer_done);

    /* 模拟启动硬件传输...
     * 真实情况:写寄存器启动DMA/中断
     */
    // 假设传输立即完成,触发“完成事件”
    complete(&g_xfer_done);

    // 等待传输完成,超时设置100毫秒
    if (!wait_for_completion_timeout(&g_xfer_done, msecs_to_jiffies(100))) {
        dev_err(&spi->dev, "SPI transfer timeout\n");
        return -ETIMEDOUT;
    }

    // 返回成功传输的字节数
    return transfer->len;
}

// 3. 片选控制函数(可选,但建议实现)
static void spi_virtual_chipselect(struct spi_device *spi, int is_on)
{
    // is_on: 0表示取消片选,非0表示选中
    // 这里应该控制对应的GPIO电平
    // dev_dbg(&spi->dev, "CS %s\n", is_on ? "assert" : "deassert");
}

// 4. probe函数:使用bitbang框架
static int spi_virtual_probe(struct platform_device *pdev)
{
    struct spi_controller *master;
    struct spi_bitbang *bb;
    int ret;

    // 分配master,并为spi_bitbang预留空间
    master = spi_alloc_master(&pdev->dev, sizeof(struct spi_bitbang));
    if (!master) {
        dev_err(&pdev->dev, "spi_alloc_master failed\n");
        return -ENOMEM;
    }
    g_virtual_master = master;

    // 获取bitbang结构体指针
    bb = spi_controller_get_devdata(master);
    g_virtual_bitbang = bb;
    init_completion(&g_xfer_done);

    // 设置bitbang结构体
    bb->master = master;
    bb->txrx_bufs = spi_virtual_txrx_bufs; // 核心函数
    bb->chipselect = spi_virtual_chipselect;

    // 设置控制器能力
    master->dev.of_node = pdev->dev.of_node;
    master->num_chipselect = 1;
    master->bits_per_word_mask = SPI_BPW_MASK(8); // 支持8位/字
    master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH; // 支持的SPI模式

    // 使用bitbang框架启动控制器(内部会调用spi_register_controller)
    ret = spi_bitbang_start(bb);
    if (ret) {
        dev_err(&pdev->dev, "spi_bitbang_start failed: %d\n", ret);
        spi_controller_put(master);
        return ret;
    }

    dev_info(&pdev->dev, "virtual SPI master (bitbang) registered\n");
    return 0;
}

// 5. remove函数
static int spi_virtual_remove(struct platform_device *pdev)
{
    spi_bitbang_stop(g_virtual_bitbang);
    spi_controller_put(g_virtual_master);
    return 0;
}

// 6. platform_driver定义(同上)
static struct platform_driver spi_virtual_driver = {
    .driver = {
        .name = "virtual_spi",
        .of_match_table = spi_virtual_dt_ids,
    },
    .probe = spi_virtual_probe,
    .remove = spi_virtual_remove,
};
module_platform_driver(spi_virtual_driver);

MODULE_DESCRIPTION("Virtual SPI Master Driver (New/Bitbang Method)");
MODULE_LICENSE("GPL");

新方法优势解析:

  • 更简洁:我们不再需要管理消息队列和工作队列,只需要实现txrx_bufs来处理最基本的spi_transfer。
  • 更健壮:队列调度、锁、多设备并发都由spi_bitbang_start和内核SPI核心处理好了。
  • 更专注:驱动开发者可以集中精力在硬件寄存器的操作上,也就是spi_virtual_txrx_bufs函数里。
  • spi_bitbang_start这个函数非常关键,它内部完成了spi_controller的注册,并设置好了transfer_one_message等函数指针,将我们的txrx_bufs函数接入内核的标准流程。

5. 调试技巧与常见问题排查

驱动写好了,加载模块,发现没反应或者数据不对,这是最考验人的时候。根据我的经验,SPI驱动调试可以遵循以下步骤,能帮你快速定位问题。

第一步:检查驱动是否成功加载 使用dmesg | grep spi或dmesg | grep virtual查看内核日志。你应该能看到probe函数里的dev_info打印信息。如果没有,说明驱动和设备树没匹配上,回头检查compatible字符串和of_device_id表是否完全一致,包括大小写。

第二步:检查SPI设备是否创建成功 驱动加载后,内核会为设备树里每个SPI子节点创建对应的设备。可以查看/sys/bus/spi/devices/目录。你应该能看到类似spi0.0这样的目录,其中的modalias文件内容就是compatible属性。如果这里没有,说明设备树子节点解析或驱动注册有问题。

第三步:使用spidev工具进行基础测试 如果设备树里用了compatible = "spidev",那么用户空间会出现类似/dev/spidev0.0的设备文件。你可以用Linux内核源码自带的spidev_test工具(在tools/spi/目录下)进行测试。一个最简单的命令是:

# 发送0xAA, 0xBB, 0xCC, 0xDD四个字节,并读取回数据
spidev_test -D /dev/spidev0.0 -s 100000 -p "\xAA\xBB\xCC\xDD"

这个工具能帮你验证最基本的读写通路是否畅通。如果这里都失败,那问题肯定在驱动或硬件连接上。

第四步:逻辑分析仪是你的好朋友 这是硬件驱动调试的终极武器。用逻辑分析仪抓取SCK、MOSI、MISO、CS四条线上的实际波形。重点关注:

  1. 片选CS:是否在传输前拉低,传输后拉高?电平是否和配置一致(ACTIVE_LOW)?
  2. 时钟SCK:频率是否正确?空闲电平(CPOL)和采样边沿(CPHA)是否符合设备树设置?
  3. 数据线:MOSI上发出的数据是否和你预期的一致?MISO上是否有数据返回?数据位序(MSB/LSB)对吗?

我无数次靠逻辑分析仪发现“以为是软件问题,其实是硬件问题”的坑,比如PCB上引脚连错了、上拉电阻没焊、电平不匹配等等。

第五步:内核驱动内部调试 如果硬件波形都正确,但应用层还是收不到数据,那就要深入驱动内部了。可以在txrx_bufs或transfer函数里增加print_hex_dump_bytes来打印发送和接收缓冲区的数据。同时,检查spi_transfer结构体里的len、tx_buf、rx_buf、delay_usecs等字段是否被正确设置。

常见问题清单:

  • 完全没反应:检查设备树status、compatible;检查驱动probe是否被调用;检查片选GPIO配置是否正确(复用功能、方向)。
  • 能写不能读:检查MISO线连接;检查驱动是否实现了读取数据的逻辑(对于bitbang,txrx_bufs需要同时处理收发)。
  • 数据错位或乱码:首先用逻辑分析仪确认SPI模式(CPOL/CPHA)是否匹配;然后检查驱动中设置的字长(bits_per_word_mask)和数据位序(spi_lsb_first)。
  • 速度上不去:检查设备树spi-max-frequency;对于GPIO模拟的SPI(bitbang),速度受CPU调度和软件延时限制,通常很难超过1MHz。

最后,分享一个我自己的习惯:在驱动开发初期,我会先实现一个“傻瓜式”的驱动,比如在txrx_bufs里固定返回一些数据,先确保整个从应用层到驱动层的路径是通的。然后再一步步替换成真实的硬件操作。这种“分步验证”的方法,能极大减少同时面对多个不确定因素时的调试复杂度。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐