4. SPI驱动框架

SPI总线,在传感器领域太常见了。加速度计、陀螺仪、磁力计、ADC……很多高速或高精度传感器都走SPI。我刚开始写SPI驱动时,总觉得不就是四条线嘛,MISO、MOSI、SCLK、CS,能有多复杂?结果第一次调一个24位ADC的SPI时序,数据死活不对,折腾了两天才发现是时钟极性配反了。

嗯,咱们今天就把SPI子系统彻底讲透。从内核架构到驱动注册,再到模式和时钟的细节,一步到位。

4.1 Linux SPI子系统架构

Linux的SPI子系统,说白了就是三层结构:SPI核心层、SPI控制器驱动层、SPI设备驱动层。你想想看,这种分层设计的好处是什么?隔离变化。控制器驱动由芯片厂商写,设备驱动由传感器厂商或我们自己写,核心层把两边粘起来。

核心思想:SPI子系统通过spi_master和spi_device两个核心数据结构,把硬件控制器和具体外设解耦。

下面这张图,是我画的一个简化版架构图,帮你快速建立整体认知。

Linux SPI 子系统三层架构 用户空间 (User Space) 内核空间 SPI 设备驱动层 (SPI Device Driver) spi_driver / spi_device / probe() / remove() SPI 核心层 (SPI Core) spi_register_master() / spi_register_driver() / 数据传输接口 SPI 控制器驱动层 (SPI Controller Driver) spi_master / transfer_one_message() / 硬件寄存器操作 硬件层 (SPI Controller + Sensor) 分层解耦 · 接口统一 · 可移植

从上图可以看到,用户空间的应用程序通过字符设备或IIO框架,调用到SPI设备驱动。设备驱动再通过核心层提供的API,把数据交给控制器驱动。控制器驱动最终操作硬件寄存器,完成实际的SPI时序收发。

我个人习惯把SPI子系统比作一个快递系统:控制器驱动是快递员(负责跑腿),核心层是分拣中心(负责调度),设备驱动是收件人(负责拆包处理)。

4.2 SPI设备驱动注册

写一个SPI设备驱动,核心就是注册一个spi_driver结构体。这个结构体长什么样?我直接上代码,咱们边看边讲。

#include <linux/spi/spi.h>

static int sensor_probe(struct spi_device *spi)
{
    // 在这里初始化传感器
    // 读取设备ID、配置寄存器、注册IIO设备等
    dev_info(&spi->dev, "Sensor probed\n");
    return 0;
}

static void sensor_remove(struct spi_device *spi)
{
    // 清理资源
    dev_info(&spi->dev, "Sensor removed\n");
}

static const struct spi_device_id sensor_id[] = {
    { "my_sensor", 0 },
    { }
};

static struct spi_driver sensor_driver = {
    .driver = {
        .name   = "my_sensor",
        .of_match_table = of_match_ptr(sensor_of_match), // 设备树匹配
        .owner  = THIS_MODULE,
    },
    .probe    = sensor_probe,
    .remove   = sensor_remove,
    .id_table = sensor_id,
};

module_spi_driver(sensor_driver);

这段代码里,module_spi_driver是一个宏,展开后就是spi_register_driver和spi_unregister_driver。你想想看,内核为什么提供这个宏?就是为了让驱动开发者少写两行样板代码。

我的经验:在probe函数里,一定要先检查spi->max_speed_hz是否被正确设置。我曾经遇到过设备树里配了10MHz,但板级文件里默认只有1MHz,probe时没检查,结果传感器初始化时序跑飞了。

设备驱动注册的完整流程,我总结成下面这个表格,方便你对照理解。

步骤 动作 关键函数/数据结构 说明
1 定义spi_driver struct spi_driver 填充probe、remove、id_table
2 注册驱动 spi_register_driver() 将驱动加入内核SPI总线
3 匹配设备 of_match_table / id_table 与设备树或ACPI中的节点匹配
4 执行probe probe() 初始化硬件、分配资源、注册子系统
5 数据传输 spi_write() / spi_read() / spi_sync() 通过SPI总线读写传感器寄存器
6 移除驱动 remove() 释放资源、注销设备

这里有个细节要注意:spi_write()和spi_read()是同步接口,会阻塞当前进程直到传输完成。如果你在中断上下文或原子上下文中调用,那就等着内核报错吧。这时候需要用spi_async()。

4.3 SPI模式与时钟配置

SPI模式,说白了就是时钟极性(CPOL)和时钟相位(CPHA)的组合。CPOL决定SCLK空闲时是高电平还是低电平,CPHA决定数据是在第一个边沿采样还是第二个边沿采样。

四种模式,我直接给你列出来:

模式 CPOL CPHA 空闲时钟电平 数据采样边沿
SPI_MODE_0 0 0 低电平 第一个边沿(上升沿)
SPI_MODE_1 0 1 低电平 第二个边沿(下降沿)
SPI_MODE_2 1 0 高电平 第一个边沿(下降沿)
SPI_MODE_3 1 1 高电平 第二个边沿(上升沿)

大多数传感器默认使用SPI_MODE_0或SPI_MODE_3。为什么?因为这两种模式都是在上升沿采样数据,对时钟的抖动容忍度更高。我个人习惯,拿到一个新传感器的数据手册,第一件事就是翻到SPI时序图,确认CPOL和CPHA。

避坑指南:我曾经遇到一个传感器,数据手册上写的是SPI_MODE_0,但实际测试发现必须用SPI_MODE_3才能正确通信。后来仔细看手册才发现,手册里画的是CPOL=0、CPHA=1,但文字描述写错了。所以,永远以时序图为准,不要只看文字描述。

时钟配置方面,核心参数是max_speed_hz。这个值通常在设备树里指定:

&spi1 {
    status = "okay";
    cs-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>;

    sensor@0 {
        compatible = "my_sensor";
        reg = <0>;          // CS片选索引
        spi-max-frequency = <10000000>;  // 10MHz
        spi-cpol;           // 如果模式不是0,需要显式指定
        spi-cpha;
    };
};

在驱动代码里,你可以通过spi->mode和spi->max_speed_hz读取这些配置。如果需要在probe里动态调整,可以这样做:

static int sensor_probe(struct spi_device *spi)
{
    // 检查当前模式
    dev_info(&spi->dev, "Current mode: 0x%x, speed: %d Hz\n",
             spi->mode, spi->max_speed_hz);

    // 如果需要强制修改模式
    spi->mode = SPI_MODE_3;
    spi->max_speed_hz = 5000000; // 5MHz
    spi_setup(spi);  // 重新配置控制器

    // 继续初始化...
    return 0;
}

注意,spi_setup()这个函数很关键。你改了mode或speed之后,必须调用它,内核才会把新配置下发给控制器驱动。我见过不少新手,改了spi->mode就以为完事了,结果通信还是老样子。

4.4 数据传输:spi_transfer与spi_message

SPI数据传输的核心是两个结构体:spi_transfer和spi_message。一个message包含一个或多个transfer,每个transfer描述一段连续的SPI收发操作。

看一个典型的读写传感器寄存器的例子:

static int sensor_read_reg(struct spi_device *spi, u8 reg, u8 *val)
{
    int ret;
    struct spi_message msg;
    struct spi_transfer tr[2] = {0};

    // 第一个transfer:发送寄存器地址
    tr[0].tx_buf = &reg;
    tr[0].len = 1;

    // 第二个transfer:读取寄存器值
    tr[1].rx_buf = val;
    tr[1].len = 1;

    spi_message_init(&msg);
    spi_message_add_tail(&tr[0], &msg);
    spi_message_add_tail(&tr[1], &msg);

    ret = spi_sync(spi, &msg);
    return ret;
}

这里有个小技巧:很多传感器在写寄存器地址时,需要把最高位置1表示读操作。比如reg | 0x80。这个细节一定要看数据手册,别想当然。

我的建议:如果你的传感器支持多字节连续读写,尽量用spi_write_then_read()这个辅助函数。它内部帮你处理了CS片选的保持逻辑,比手动拼transfer更安全。

4.5 常见问题与调试方法

SPI驱动调试,说难不难,说简单也不简单。我总结几个最常见的坑:

  • 时钟极性/相位不匹配:用逻辑分析仪抓SCLK和MOSI,对比数据手册的时序图。这是最直接的排查方法。
  • 片选信号异常:检查CS是否被正确拉低,是否有其他设备抢占了SPI总线。我曾经遇到过两个传感器共用一条CS线,结果数据互相干扰。
  • 时钟频率过高:传感器有最大时钟限制,别超过。另外,PCB走线太长或阻抗不匹配时,高频信号会失真。我一般先降到1MHz调通,再逐步提频。
  • DMA传输问题:如果使用DMA,确保tx/rx buffer是DMA安全的(用kmalloc分配,不要用栈上的局部变量)。

调试工具方面,我推荐spidev_test这个用户空间工具。它可以直接操作/dev/spidevX.Y设备节点,帮你快速验证硬件连接是否正确。用法很简单:

# 发送0x90并读取一个字节(假设是读ID命令)
spidev_test -D /dev/spidev1.0 -p "\x90\x00" -s 1000000

如果spidev_test能读到正确的数据,说明硬件和SPI控制器驱动都没问题,问题大概率出在你的设备驱动里。

嗯,SPI驱动框架的核心内容就这些。从架构到注册,从模式到时钟,再到数据传输和调试,每一步都有坑,但也都有套路。多写几次,你就能形成自己的SPI驱动模板,以后遇到新传感器,直接套模板改改寄存器地址和模式就行。