ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux SPI设备树驱动开发全解析:从硬件描述到用户空间接口

2026/8/12 17:33:53 拓冰建站 浏览量
Linux SPI设备树驱动开发全解析:从硬件描述到用户空间接口 1. 项目概述为什么要在Linux下搞懂SPI设备树驱动如果你正在玩一块像瑞芯微RK3568这样的国产开发板或者手头有个SPI接口的屏幕、传感器比如ICM20948陀螺仪需要驱动那你大概率绕不开“设备树”和“SPI驱动”这两个词。十年前你可能得在内核源码里到处写platform_device结构体来硬编码硬件信息换块板子就得重新编译内核麻烦得很。现在设备树Device Tree成了Linux内核特别是ARM体系结构下的标准硬件描述语言它把硬件配置从内核代码中剥离出来以.dts文件的形式存在。这意味着同一份内核镜像配合不同的设备树文件就能跑在不同的硬件平台上。SPISerial Peripheral Interface作为一种高速、全双工、同步的串行通信总线在嵌入式领域应用极广从Nor Flash存储芯片到TFT液晶屏再到各种传感器都离不开它。编写一个基于设备树的SPI驱动本质上就是教会内核三件事第一我的板子上有哪些SPI控制器比如主控芯片自带的一号、二号SPI总线第二这些SPI总线上挂了哪些设备比如一个挂在SPI0上的屏幕片选是GPIO0_10第三每个设备对应哪个驱动以及需要怎样的通信参数比如时钟频率1MHz数据位宽8位。所以这个项目标题“基于Linux系统设备树的SPI驱动编写方法”拆解开来核心就是如何利用设备树语法描述SPI硬件拓扑并编写与之匹配的内核驱动模块最终让用户空间程序能够通过标准的文件接口如/dev/spidev0.0或者更具体的设备节点来访问你的SPI外设。无论你是驱动工程师、嵌入式爱好者还是正在做毕业设计的学生搞懂这套流程就相当于拿到了在Linux世界里与各种SPI芯片对话的万能钥匙。接下来我会以一个虚拟的“my_spi_sensor”设备为例带你从设备树节点编写一路走到驱动模块的加载测试把每个环节的“为什么”和“怎么做”都讲透。2. 核心思路与框架设计从硬件连接到软件抽象在动手写代码之前我们必须先理清整个驱动框架的脉络。Linux内核的SPI子系统设计得非常清晰遵循了典型的“总线-设备-驱动”模型。理解这个模型是写出正确驱动的前提。2.1 Linux SPI子系统架构总览你可以把SPI子系统想象成一个公司。SPI核心层SPI Core是公司的管理层和规章制度它不直接管具体业务但定义了SPI总线如何注册、设备与驱动如何匹配通过设备树里的compatible属性、以及提供了一整套API供大家使用。SPI控制器驱动也叫主机驱动Master Driver是公司的各个事业部比如“RK3568 SPI事业部”、“STM32 SPI事业部”。它们负责具体硬件SPI控制器的操作实现最底层的时序、时钟、数据传输。这部分通常由芯片原厂或社区维护我们一般不用动。我们要写的是SPI设备驱动Client Driver相当于公司里的一个具体项目组。这个项目组专门为某个特定的SPI外设比如我们的虚拟传感器my_spi_sensor服务。它的核心任务是第一向SPI核心层“报到”声明自己能为哪些设备服务通过of_device_id匹配表第二当有匹配的设备被枚举出来时初始化这个设备并创建出用户空间可以访问的接口第三实现具体的业务逻辑比如读取传感器数据、控制屏幕显示。设备树.dts文件在这里扮演了“硬件配置清单”的角色。它告诉内核“在RK3568的SPI0总线上片选0连接了一个设备它的兼容标识是my-company,my-spi-sensor通信时钟最高10MHz。” 驱动通过读取这份清单就能获取所有必要的硬件参数而无需在代码里写死。2.2 设备树节点设计硬件信息的蓝图设备树节点的编写是驱动成功的第一步也是最容易出错的一步。一个完整的SPI设备节点通常需要嵌套在SPI控制器节点之下。我们以在spi0总线上添加设备为例// 在板级设备树文件如 rk3568-evb.dts中 spi0 { status okay; // 确保控制器使能 pinctrl-names default; pinctrl-0 spi0_pins; // 引脚复用配置参考原厂提供的pinctrl my_spi_sensor: my-spi-sensor0 { compatible my-company,my-spi-sensor; reg 0; // 片选编号对应硬件连接。如果接在CS0上就是0。 spi-max-frequency 10000000; // 最大SPI时钟频率单位Hz。必须根据芯片手册设置。 spi-cpol; // 可选时钟极性CPOL1空闲时为高电平 spi-cpha; // 可选时钟相位CPHA1在第二个时钟边沿采样数据 // 更多自定义属性比如中断引脚、复位引脚等 interrupt-parent gpio0; interrupts 12 IRQ_TYPE_EDGE_RISING; // 假设传感器中断接在GPIO0_12 reset-gpios gpio1 5 GPIO_ACTIVE_LOW; // 复位引脚低电平有效 }; };关键属性解析compatible: 这是驱动匹配设备的唯一关键字符串。格式通常是“制造商,设备型号”。驱动代码里的of_device_id表必须包含完全相同的字符串。reg: 这里的0不是内存地址而是SPI控制器的片选索引号。它直接对应硬件上你连接到了哪个CS引脚。spi-max-frequency:务必查阅芯片数据手册这是最容易导致通信失败的地方。比如你的传感器最高支持5MHz你设成10MHz数据就可能出错。保守起见调试初期可以设低一点比如1MHz。spi-cpol和spi-cpha: 这两个属性定义了SPI的四种工作模式Mode 0-3。是否添加它们完全取决于你的外设芯片要求。Mode 0两者都不设和Mode 3两者都设是最常见的。同样必须看芯片手册。实操心得设备树调试设备树编译后生成dtb文件加载到内核后可以通过cat /proc/device-tree/来查看解析后的树状结构。更直接的是用ls /proc/device-tree/soc/spiff000000/路径根据实际平台变化查看你的节点是否存在用hexdump -C查看属性值。如果驱动没匹配上首先检查这里你的节点和属性是否正确生成。2.3 驱动代码框架搭建一个最基础的SPI设备驱动包含以下几个核心部分模块加载与卸载函数module_init和module_exit。设备匹配表一个of_device_id结构体数组其中的.compatible字符串必须和设备树节点里的compatible属性完全一致。SPI驱动结构体struct spi_driver里面需要填充.probe,.remove,.driver等成员。.driver.of_match_table要指向上面的匹配表。Probe函数这是驱动的“入口函数”。当内核发现一个SPI设备的compatible属性与驱动匹配表里的某一项吻合时就会调用这个函数。我们所有重要的初始化工作都在这里进行申请设备结构体、初始化SPI通信参数、注册字符设备或生成sysfs节点、申请中断等。Remove函数与Probe对应负责释放资源。#include linux/module.h #include linux/spi/spi.h #include linux/of.h // 1. 定义设备私有数据结构体通常用于存放驱动状态、缓冲区等 struct my_spi_sensor_data { struct spi_device *spi; struct mutex lock; // 用于保护并发访问 u8 buffer[32]; // ... 其他成员 }; // 2. 定义设备树匹配表 static const struct of_device_id my_spi_sensor_of_match[] { { .compatible my-company,my-spi-sensor }, {}, }; MODULE_DEVICE_TABLE(of, my_spi_sensor_of_match); // 3. Probe函数核心 static int my_spi_sensor_probe(struct spi_device *spi) { struct my_spi_sensor_data *data; int ret; // 打印调试信息确认进入Probe dev_info(spi-dev, Probing my SPI sensor\n); // 3.1 初始化SPI通信参数覆盖设备树设置或进行微调 spi-mode SPI_MODE_0; // 明确设置模式如果设备树已设这里可省略或保持一致 spi-bits_per_word 8; // 设置数据位宽为8位 ret spi_setup(spi); // 应用设置重要 if (ret 0) { dev_err(spi-dev, Failed to setup SPI: %d\n, ret); return ret; } // 3.2 分配并初始化设备私有数据 data devm_kzalloc(spi-dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >static int my_spi_sensor_read_reg(struct my_spi_sensor_data *data, u8 reg, u16 *val) { int ret; struct spi_device *spi >static ssize_t value_show(struct device *dev, struct device_attribute *attr, char *buf) { struct my_spi_sensor_data *data dev_get_drvdata(dev); u16 val; int ret my_spi_sensor_read_reg(data, READ_VALUE_CMD, val); if (ret) return ret; return sprintf(buf, %d\n, val); } static DEVICE_ATTR_RO(value); // 创建只读属性value // 在probe中 device_create_file(spi-dev, dev_attr_value);用户空间通过cat /sys/bus/spi/devices/spi0.0/value即可读取。字符设备 (/dev/xxx)提供最大的灵活性支持read,write,ioctl等系统调用。适合需要复杂控制逻辑或大数据量传输的设备。你需要实现file_operations结构体。这是最通用但也最复杂的方式。IIOIndustrial I/O框架这是驱动传感器的首选和标准方式。IIO为传感器ADC、陀螺仪、光感等提供了一套统一的框架自动生成标准化的sysfs接口和事件支持用户空间工具如iio_info,iio_readdev可以直接使用。虽然初始设置稍复杂但长期来看最规范、最省事。#include linux/iio/iio.h #include linux/iio/sysfs.h // 需要实现struct iio_info定义通道struct iio_chan_spec并在probe中注册iio_device。使用内核自带的spidev这是一个通用的SPI用户空间驱动。如果你的设备非常特殊或者只想快速验证SPI通信是否通畅可以在设备树中将compatible设为spidev内核就会自动创建类似/dev/spidev0.0的设备节点。用户空间可以直接用ioctl(SPI_IOC_MESSAGE)进行原始SPI数据包收发。注意spidev绕过了具体的设备驱动失去了硬件抽象且在生产环境中通常不建议使用因为它无法管理多个进程对同一设备的并发访问也不安全。但对于原型验证和调试它非常方便。实操心得接口选择对于传感器强烈建议使用IIO框架。它学习曲线稍陡但一旦掌握后续添加新通道、增益控制、触发缓冲等功能都非常方便而且社区支持好。如果只是简单的GPIO扩展芯片或DAC字符设备或sysfs可能更直接。在项目初期可以用spidev配合一个小用户态程序快速验证硬件连线、时钟极性和相位是否正确这能帮你快速定位是硬件问题还是驱动逻辑问题。4. 编译、加载与调试全流程实录理论说再多不如动手跑一遍。我们假设驱动文件为my_spi_sensor.c对应的Makefile如下obj-m my_spi_sensor.o KDIR : /path/to/your/kernel/source # 替换为你的内核源码路径或使用/lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean4.1 驱动编译与加载步骤编译在驱动源码目录下执行make。如果成功会生成my_spi_sensor.ko文件。部署设备树修改你的板级设备树文件.dts添加如前所述的SPI设备节点。使用设备树编译器DTC编译dtc -I dts -O dtb -o my-board.dtb my-board.dts。将生成的.dtb文件放到启动分区如U-Boot加载的位置或直接在内核启动参数中指定新的dtb文件路径。加载驱动将.ko文件拷贝到开发板。执行insmod my_spi_sensor.ko。使用dmesg | tail查看内核日志应该能看到驱动的probe函数被调用并打印出初始化成功的信息。使用ls /sys/bus/spi/devices/查看应该能看到类似spi0.0的设备目录进去后能看到of_node链接和driver链接指向你的驱动。使用cat /sys/bus/spi/devices/spi0.0/modalias可以查看设备匹配的别名。4.2 典型问题排查与调试技巧即使按照指南操作第一次也难免失败。下面是一个常见问题排查清单问题现象可能原因排查方法驱动insmod成功但probe函数没被调用1. 设备树节点compatible与驱动不匹配。2. 设备树节点未编译进dtb或未加载。3. SPI控制器节点status不是okay。4. 引脚复用pinctrl冲突。1. 检查/proc/device-tree/下你的节点是否存在核对compatible属性值。2. 确认使用的dtb文件是否正确。用fdtdump工具查看dtb内容。3. 检查SPI控制器节点状态和pinctrl配置。4. 使用cat /sys/kernel/debug/pinctrl/pinctrl-handles如果支持查看引脚复用状态。probe被调用但spi_setup失败1. 请求的SPI模式或频率控制器不支持。2. 硬件连线错误如MISO/MOSI接反。1. 检查控制器驱动源码看其mode_bits和最大频率限制。2. 用示波器或逻辑分析仪抓取SPI波形看时钟、数据线是否有信号。能probe但数据读写错误1. SPI模式CPOL/CPHA设置错误。2. 片选信号异常。3. 时序不满足芯片要求如命令间延迟不够。4. 电源或复位引脚未正确初始化。1.这是最高频问题反复核对芯片手册的时序图与示波器抓取的波形对比。Mode 0和Mode 3最易混淆。2. 查看片选波形是否在spi_message传输期间保持有效3. 在spi_transfer中增加.delay_usecs。4. 在probe中确保给芯片上电、释放复位。用户空间无法访问设备节点1. 未成功创建设备节点如/dev/下的文件。2. 文件权限问题。1. 检查驱动代码中字符设备注册或IIO设备注册是否成功返回值。2. 使用ls -l /dev/查看节点权限或检查/sys/class/下对应的类是否存在。系统运行不稳定或死机1. 内存访问越界如缓冲区溢出。2. 中断处理函数ISR中进行了可能导致睡眠的操作。3. 并发保护锁缺失或使用不当导致死锁。1. 使用kmemleak或KASAN等内核内存检测工具。2. 确保ISR中只使用spinlock不能调用kmalloc(GFP_KERNEL)、mutex_lock等可能睡眠的函数。3. 检查锁的获取和释放是否成对顺序是否一致。调试利器逻辑分析仪和内核日志逻辑分析仪对于SPI驱动开发一个哪怕是最便宜的逻辑分析仪配合PulseView/Sigrok都是神器。它能直观地显示时钟、数据线上的每一位让你精确验证模式、频率、数据内容是否正确。很多软件问题如字节序、位顺序在波形面前一目了然。内核动态调试在你的驱动函数里多放一些dev_dbg()。然后可以通过echo -n file my_spi_sensor.c p /sys/kernel/debug/dynamic_debug/control来动态开启这个文件的所有调试信息无需重新编译。5. 进阶优化与生产环境考量当基本功能跑通后我们需要考虑驱动在真实产品中的鲁棒性、性能和可维护性。5.1 电源管理与睡眠唤醒嵌入式设备省电是关键。你的驱动需要支持系统的睡眠Suspend和唤醒Resume回调。#ifdef CONFIG_PM_SLEEP static int my_spi_sensor_suspend(struct device *dev) { struct spi_device *spi to_spi_device(dev); struct my_spi_sensor_data *data spi_get_drvdata(spi); // 1. 如果芯片有低功耗模式发送命令进入该模式。 // 2. 关闭不需要的时钟或电源域如果由驱动控制。 // 3. 保存必要的寄存器状态如果需要。 dev_dbg(dev, Device suspended\n); return 0; } static int my_spi_sensor_resume(struct device *dev) { struct spi_device *spi to_spi_device(dev); struct my_spi_sensor_data *data spi_get_drvdata(spi); // 1. 恢复电源/时钟。 // 2. 从低功耗模式唤醒芯片可能需要重新发送初始化序列。 // 3. 恢复保存的寄存器状态。 dev_dbg(dev, Device resumed\n); return 0; } #endif static const struct dev_pm_ops my_spi_sensor_pm_ops { SET_SYSTEM_SLEEP_PM_OPS(my_spi_sensor_suspend, my_spi_sensor_resume) }; // 在spi_driver的.driver结构体中添加 .driver { // ... 其他成员 .pm my_spi_sensor_pm_ops, },5.2 使用Regmap API简化寄存器操作如果你的设备有大量的寄存器需要读写内核的regmap框架可以极大地简化代码。它抽象了底层总线SPI、I2C等操作提供了统一的、带缓存的寄存器访问接口还支持调试fs。#include linux/regmap.h #include linux/spi/spi.h static const struct regmap_config my_spi_sensor_regmap_config { .reg_bits 8, // 寄存器地址是8位 .val_bits 8, // 寄存器值是8位 .max_register 0x7F, // 最大寄存器地址 .cache_type REGCACHE_NONE, // 根据需求选择缓存类型 }; static int my_spi_sensor_probe(struct spi_device *spi) { struct regmap *regmap; regmap devm_regmap_init_spi(spi, my_spi_sensor_regmap_config); if (IS_ERR(regmap)) { // 错误处理 } // 之后就可以用 regmap_read, regmap_write, regmap_update_bits 等函数了 regmap_write(regmap, REG_CTRL, 0x01); }5.3 处理并发与中断并发我们已经用了mutex。对于非常高频的操作可以考虑spinlock但要注意在可能睡眠的上下文如spi_sync内部可能会睡眠中不能使用自旋锁。中断如果设备有中断引脚比如数据就绪在设备树中定义interrupts属性在驱动probe中使用devm_request_irq申请中断。切记中断处理函数要短平快通常只是唤醒一个工作队列workqueue或任务队列tasklet将耗时的操作放到下半部执行。5.4 代码贡献与上游合并如果你希望自己的驱动能被主线内核接受需要遵循内核的编码风格用checkpatch.pl脚本检查提供完整的设备树绑定文档Documentation/devicetree/bindings/并确保没有依赖任何非标准的、特定于某个发行版的API。提交补丁到内核邮件列表是一个学习社区规范、提升代码质量的绝佳过程。从看懂设备树节点到搭建驱动框架实现数据收发再到处理电源管理和并发最后考虑优化和上游化这就是一个完整的、生产可用的Linux SPI设备驱动开发闭环。每一步都踩过坑之后你会发现再面对任何新的SPI芯片思路都会清晰很多。驱动开发没有银弹多动手、多调试、善用工具和社区资源是解决问题的唯一捷径。