平台设备驱动:platform bus 与设备树

平台设备驱动:platform bus 与设备树

嵌入式 SoC 上的片上外设(I2C、SPI、UART、GPIO 控制器等)挂在 platform 总线上。这篇讲 platform 驱动框架、设备树 DTS 语法、驱动和设备怎么匹配,以及现代 Linux 驱动的标准写法。

大家好,我是黒漂技术佬。

前面讲的字符设备驱动是直接注册 cdev,但实际嵌入式驱动不是这么写的。SoC 上的外设(比如 I2C 控制器、SPI 控制器、GPIO 控制器)都挂在 platform 总线上,驱动要按 platform 驱动的框架来写。

再加上设备树(Device Tree),硬件信息从代码里分离出来,不用每个板子都改驱动代码。

这篇讲 platform 总线、设备树、以及现代 Linux 驱动的标准结构。


一、platform 总线

为什么需要 platform 总线?

Linux 有 PCI、USB 等总线,设备可以被枚举(扫描出来)。但 SoC 内部的外设是集成的,没法动态枚举,地址都是固定的。

所以发明了 platform 总线,专门挂这类「平台设备」。

平台设备 + 平台驱动

和其他总线一样,分设备和驱动两边:

复制代码
平台设备(platform_device):描述硬件信息(地址、中断、资源)
平台驱动(platform_driver):驱动代码,匹配设备后初始化

匹配上了就调用驱动的 probe 函数。

优点

  • 硬件信息和驱动代码分离
  • 一个驱动可以支持多个同类型设备
  • 符合 Linux 设备模型,自动生成 sysfs 节点

二、平台驱动结构

定义 platform_driver

c 复制代码
#include <linux/platform_device.h>

static int my_probe(struct platform_device *pdev)
{
    // 匹配成功,在这里初始化设备
    // 拿资源、注册字符设备、申请中断...
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    // 卸载时清理
    return 0;
}

static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,my-device" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);

static struct platform_driver my_driver = {
    .probe = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my-device",
        .of_match_table = my_of_match,  // 设备树匹配表
    },
};

module_platform_driver(my_driver);

module_platform_driver 宏

代替手动写 module_init/module_exit:

c 复制代码
module_platform_driver(my_driver);

展开就是注册和注销 platform_driver 的代码。

probe 函数

匹配成功后调用,驱动初始化的核心:

  1. 从设备树或资源里拿硬件信息(寄存器地址、中断号)
  2. 映射寄存器地址
  3. 申请中断
  4. 注册字符设备 / 输入设备 / 其他子系统设备
  5. 初始化硬件

remove 函数

卸载时调用,probe 里申请的资源都要在这里释放。


三、设备树(Device Tree)是什么?

为什么要有设备树?

以前 ARM Linux 把硬件信息硬编码在内核里,每个板子一份代码,内核里堆满了板级文件,很乱。

设备树把硬件描述从内核代码里抽出来,用一个数据结构描述板子上有什么设备、地址多少、中断多少。

内核启动时解析设备树,动态创建设备。同一个内核镜像,配上不同的 dtb(设备树二进制)就能跑在不同板子上。

设备树文件

  • .dts:设备树源文件,文本格式,描述硬件
  • .dtsi:设备树头文件,公共部分可以 include
  • .dtb:编译后的二进制,bootloader 加载到内存传给内核

基本结构

树形结构,每个节点代表一个设备:

dts 复制代码
/ {
    model = "My Board";
    compatible = "vendor,my-board";
    
    soc {
        compatible = "simple-bus";
        
        uart@10000000 {
            compatible = "vendor,uart-1.0";
            reg = <0x10000000 0x1000>;
            interrupts = <12>;
            status = "okay";
        };
    };
};

四、DTS 基本语法

节点

复制代码
节点名@地址 {
    属性 = 值;
    子节点 { ... };
};

节点名一般格式:设备名@单元地址

属性类型

1. 空属性(布尔值)
dts 复制代码
property;  // 存在就是真,不存在就是假
2. 32位无符号整数
dts 复制代码
property = <0x1234 5678>;  // 一个或多个32位数,用<>包起来
3. 字符串
dts 复制代码
property = "hello";
4. 字符串列表
dts 复制代码
property = "abc", "def", "ghi";
5. 字节数组
dts 复制代码
property = [01 02 03 04];

常用属性

compatible

最关键的属性,用来匹配驱动。

dts 复制代码
compatible = "vendor,device-name";

一般是「厂商,设备名」的格式。驱动里 of_match_table 里有对应的字符串就匹配上。

可以有多个,从前往后匹配:

dts 复制代码
compatible = "vendor,chip-uart", "generic-uart";
reg

设备的寄存器地址范围:

dts 复制代码
reg = <起始地址 长度>;

可以有多段:

dts 复制代码
reg = <0x10000000 0x1000>, <0x20000000 0x2000>;
interrupts

中断号:

dts 复制代码
interrupt-parent = <&gic>;
interrupts = <0 12 4>;

具体格式取决于中断控制器。

status

设备状态:

  • "okay":启用
  • "disabled":禁用
  • "fail":故障
#address-cells / #size-cells

子节点的 reg 属性用几个 cell 表示地址、几个表示长度:

dts 复制代码
soc {
    #address-cells = <1>;  // 地址用1个32位数
    #size-cells = <1>;     // 长度用1个32位数
};

64位地址的话就是 #address-cells = <2>

引用节点

用 label 引用:

dts 复制代码
uart0: uart@10000000 {
    ...
};

// 别处引用
&uart0 {
    status = "okay";
};

头文件 include

dts 复制代码
#include "chip.dtsi"

.dtsi 放通用的 SoC 部分,板子级的 .dts 引用并修改。


五、驱动里读取设备树属性

拿到设备节点

c 复制代码
struct device_node *np = pdev->dev.of_node;

读整数

c 复制代码
u32 val;
of_property_read_u32(np, "property-name", &val);

// 读数组
u32 vals[4];
of_property_read_u32_array(np, "property-name", vals, 4);

读字符串

c 复制代码
const char *str;
of_property_read_string(np, "compatible", &str);

读布尔值

c 复制代码
if (of_property_read_bool(np, "some-flag")) {
    // 属性存在
}

获取资源(寄存器地址、中断)

寄存器地址
c 复制代码
struct resource *res;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);

// 映射
void __iomem *base = devm_ioremap_resource(&pdev->dev, res);
中断号
c 复制代码
int irq = platform_get_irq(pdev, 0);

devm_ 系列函数(托管资源)

带 devm_ 前缀的函数,分配的资源会在设备释放时自动释放,不用手动清理,remove 函数省事很多。

c 复制代码
devm_ioremap_resource()   // 自动释放的IO映射
devm_kmalloc()            // 自动释放的内存
devm_request_irq()        // 自动释放的中断
devm_kzalloc()            // 自动释放的清零内存

推荐尽量用 devm_ 版本,省得漏释放。


六、platform 驱动完整示例框架

c 复制代码
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>
#include <linux/interrupt.h>

struct my_dev {
    void __iomem *base;
    int irq;
    // ... 其他私有数据
};

static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,my-device" },
    {}
};
MODULE_DEVICE_TABLE(of, my_of_match);

static irqreturn_t my_irq(int irq, void *dev_id)
{
    // 中断处理
    return IRQ_HANDLED;
}

static int my_probe(struct platform_device *pdev)
{
    struct my_dev *dev;
    struct resource *res;
    int ret;
    
    // 分配私有数据(devm自动释放)
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev) return -ENOMEM;
    
    // 获取寄存器资源
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    dev->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(dev->base))
        return PTR_ERR(dev->base);
    
    // 获取中断号
    dev->irq = platform_get_irq(pdev, 0);
    if (dev->irq < 0)
        return dev->irq;
    
    // 申请中断
    ret = devm_request_irq(&pdev->dev, dev->irq, my_irq,
                           IRQF_TRIGGER_RISING, "my_dev", dev);
    if (ret) return ret;
    
    // 保存私有数据
    platform_set_drvdata(pdev, dev);
    
    // 初始化硬件...
    
    pr_info("my-device probed\n");
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    // 用了devm的话大部分资源自动释放
    // 这里做一些额外清理
    pr_info("my-device removed\n");
    return 0;
}

static struct platform_driver my_driver = {
    .probe = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my-device",
        .of_match_table = my_of_match,
    },
};

module_platform_driver(my_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Heipiao");
MODULE_DESCRIPTION("Platform driver example");

对应的设备树节点:

dts 复制代码
my_device@10000000 {
    compatible = "vendor,my-device";
    reg = <0x10000000 0x1000>;
    interrupts = <25>;
    status = "okay";
};

七、设备树匹配流程

  1. 内核解析 DTB,创建设备节点
  2. platform 总线创建设备
  3. 驱动注册时,遍历设备,比较 compatible 字符串
  4. 匹配上了调用 probe

匹配优先级

  1. of_match_table(设备树匹配,优先)
  2. acpi_match_table(ACPI,x86用)
  3. name 字符串匹配(老式的)

设备树驱动主要靠 of_match_table。


八、常见坑

坑 1:compatible 字符串不匹配

驱动里写的和 dts 里的不一样,probe 根本不调用。检查拼写、大小写、厂商前缀。

坑 2:reg 地址长度不对

reg 的长度错了,ioremap 映射范围不对,访问寄存器越界。

坑 3:中断号格式不对

不同中断控制器的 interrupts 属性格式不一样(几 cell、每个的含义),要查对应 SoC 的文档。

坑 4:忘了加 MODULE_DEVICE_TABLE

不加的话,自动加载模块机制识别不了,设备插上不会自动加载驱动。

坑 5:devm 资源和手动释放混着来

一部分用 devm 一部分手动释放,顺序容易搞反出错。要么全用 devm,要么都手动。

坑 6:probe 失败没回滚

probe 中间某一步失败,前面申请的资源要释放。用 devm 可以省很多事,但非 devm 的要手动回滚。


九、本篇小结

  • platform 总线:挂 SoC 片上外设,分为平台设备和平台驱动,匹配后调用 probe
  • 设备树 DTS:把硬件信息从内核代码分离,描述设备树结构,编译成 dtb 传给内核
  • DTS 语法:节点、属性、compatible、reg、interrupts、status
  • platform_driver:probe 初始化、remove 清理、of_match_table 匹配表
  • 驱动读取设备树:of_property_read_* 读属性,platform_get_resource 拿资源
  • devm_ 系列:设备托管资源,自动释放,减少 remove 工作量和泄漏
  • module_platform_driver:简化注册注销的宏
  • 匹配靠 compatible 字符串,驱动和 dts 里要一致

下一篇讲 GPIO 子系统与 pinctrl:Linux 下怎么操作 GPIO、怎么用 gpiolib、pinctrl 引脚复用。

我是黒漂技术佬。

相关推荐
三品吉他手会点灯1 小时前
嵌入式机器学习 - 学习笔记1.0.2 - 学习路线导览:从机器学习基础到 TinyML应用
人工智能·笔记·嵌入式硬件·学习·机器学习
阿拉斯攀登1 小时前
并发与同步:自旋锁、信号量、互斥锁
linux·网络·嵌入式硬件·linux驱动·字符设备驱动
纯音乐sjc1 小时前
51单片机入门Day1:从概念到第一个IO操作踩坑记录
单片机·嵌入式硬件·51单片机
网络小白不怕黑1 小时前
10.邮件服务器搭建并脚本实现监控httpd状态
linux·运维·服务器
xu_wenming2 小时前
Zephyr上开发 BLE(低功耗蓝牙)入门应用
c语言·嵌入式硬件·物联网
JNX_SEMI2 小时前
ATR2652 GNSS双频LNA:0.75dB噪声系数与22dB高增益设计
前端·单片机·嵌入式硬件·物联网·硬件工程
Carlos_Xu3 小时前
Linux基础-基础命令
linux
Luminous.3 小时前
51 单片机--day1
单片机·嵌入式硬件
风曦Kisaki3 小时前
# Linux实战笔记-HIS项目实战Day01:版本控制与Git基础
linux·笔记·git