4. SPI驱动框架:Linux SPI子系统架构、SPI设备驱动注册、SPI模式与时钟配置
SPI总线,在传感器领域太常见了。加速度计、陀螺仪、磁力计、ADC……很多高速或高精度传感器都走SPI。我刚开始写SPI驱动时,总觉得不就是四条线嘛,MISO、MOSI、SCLK、CS,能有多复杂?结果第一次调一个24位ADC的SPI时序,数据死活不对,折腾了两天才发现是时钟极性配反了。
嗯,咱们今天就把SPI子系统彻底讲透。从内核架构到驱动注册,再到模式和时钟的细节,一步到位。
4.1 Linux SPI子系统架构
Linux的SPI子系统,说白了就是三层结构:SPI核心层、SPI控制器驱动层、SPI设备驱动层。你想想看,这种分层设计的好处是什么?隔离变化。控制器驱动由芯片厂商写,设备驱动由传感器厂商或我们自己写,核心层把两边粘起来。
spi_master和spi_device两个核心数据结构,把硬件控制器和具体外设解耦。
下面这张图,是我画的一个简化版架构图,帮你快速建立整体认知。
从上图可以看到,用户空间的应用程序通过字符设备或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。你想想看,内核为什么提供这个宏?就是为了让驱动开发者少写两行样板代码。
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。
时钟配置方面,核心参数是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 = ®
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驱动模板,以后遇到新传感器,直接套模板改改寄存器地址和模式就行。