1. 设备树基础:硬件描述的"地图"

刚开始接触Linux驱动开发时,最让我头疼的就是硬件配置问题。以前写驱动,硬件信息都是直接写死在代码里的——寄存器地址、中断号、GPIO引脚,全都用宏定义写在驱动文件开头。换个板子或者换个引脚,就得重新修改代码、编译,麻烦得要命。

后来用了设备树,才发现原来硬件描述可以这么优雅。设备树就像一张详细的"地图",告诉Linux内核这块板子上有什么硬件、地址在哪、怎么连接。驱动代码只需要关心"怎么用硬件",不需要知道硬件"在哪里"。

设备树源文件(.dts)其实就是一种文本格式的硬件描述文件,我们用设备树编译器(dtc)把它编译成二进制格式(.dtb),然后由bootloader加载到内存中,最后传递给内核。内核启动时会解析这个设备树,建立起完整的硬件设备模型。

举个例子,我们要描述一个GPIO控制器,在设备树里可以这样写:

gpio1: gpio-controller@0xC001A000 {
    compatible = "vendor,gpio-controller";
    reg = <0xC001A000 0x1000>;
    #gpio-cells = <2>;
    gpio-controller;
};

这段代码告诉内核:有一个GPIO控制器在地址0xC001A000开始的地方,长度0x1000字节,兼容"vendor,gpio-controller"这个驱动,每个GPIO需要用2个cell来描述。

2. GPIO子系统:硬件操作的标准接口

GPIO(通用输入输出)是最常用的硬件接口之一,LED、按键、传感器这些外设基本都要用到GPIO。Linux内核提供了GPIO子系统,给驱动开发者提供了一套统一的GPIO操作接口。

以前我操作GPIO都是直接映射物理地址,然后操作寄存器:

// 老式做法:直接操作寄存器
void __iomem *gpio_base = ioremap(0xC001A000, 0x1000);
writel(readl(gpio_base + 0x04) | (1 << 5), gpio_base + 0x04);  // 设置GPIO5为输出
writel(readl(gpio_base + 0x00) & ~(1 << 5), gpio_base + 0x00); // GPIO5输出低电平

这种方式虽然直接,但是可移植性太差。换块板子就得改代码,而且容易出错。

用了GPIO子系统之后,代码就简洁多了:

// 新式做法:使用GPIO子系统
gpio_request(5, "my_led");          // 申请GPIO5
gpio_direction_output(5, 0);        // 设置为输出,初始低电平
gpio_set_value(5, 1);               // 输出高电平
gpio_free(5);                       // 释放GPIO

GPIO子系统还提供了设备树支持,我们可以在设备树里定义GPIO的使用方式:

leds {
    compatible = "gpio-leds";
    user_led {
        label = "user-led";
        gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>;
        default-state = "off";
    };
};

然后在驱动代码中这样获取GPIO:

struct device_node *np = pdev->dev.of_node;
int led_gpio = of_get_named_gpio(np, "user_led", 0);
gpio_request(led_gpio, "user_led");

3. 设备树与GPIO结合实战:LED控制

现在我来演示一个完整的例子,用设备树描述LED硬件,用GPIO子系统控制LED。

首先在设备树中添加LED节点:

/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target-path = "/";
        __overlay__ {
            leds {
                compatible = "gpio-leds";
                led1 {
                    label = "led1";
                    gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>;
                    default-state = "off";
                    linux,default-trigger = "heartbeat";
                };
            };
        };
    };
};

编译设备树:

dtc -I dts -O dtb -o led-overlay.dtbo led.dts

然后编写驱动代码:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>

struct gpio_led_data {
    struct gpio_desc *gpiod;
};

static int gpio_led_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct gpio_led_data *data;
    int ret;
    
    data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;
    
    data->gpiod = devm_gpiod_get(dev, NULL, GPIOD_OUT_LOW);
    if (IS_ERR(data->gpiod)) {
        dev_err(dev, "Failed to get GPIO\n");
        return PTR_ERR(data->gpiod);
    }
    
    platform_set_drvdata(pdev, data);
    gpiod_set_value(data->gpiod, 1);  // 点亮LED
    
    dev_info(dev, "LED driver probed successfully\n");
    return 0;
}

static int gpio_led_remove(struct platform_device *pdev)
{
    struct gpio_led_data *data = platform_get_drvdata(pdev);
    
    gpiod_set_value(data->gpiod, 0);  // 熄灭LED
    dev_info(&pdev->dev, "LED driver removed\n");
    return 0;
}

static const struct of_device_id gpio_led_of_match[] = {
    { .compatible = "gpio-leds", },
    { },
};
MODULE_DEVICE_TABLE(of, gpio_led_of_match);

static struct platform_driver gpio_led_driver = {
    .driver = {
        .name = "gpio-led",
        .of_match_table = gpio_led_of_match,
    },
    .probe = gpio_led_probe,
    .remove = gpio_led_remove,
};

module_platform_driver(gpio_led_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("GPIO LED Driver with Device Tree");

这个例子展示了如何用设备树描述硬件,如何在驱动中获取GPIO,以及如何控制LED。这种方式的优点是驱动代码通用,不需要关心具体的硬件细节。

4. 中断处理:GPIO中断实战

GPIO中断在驱动开发中非常常见,比如按键检测、传感器数据就绪等场景。Linux内核提供了完善的中断子系统,结合设备树和GPIO子系统,可以很方便地实现中断处理。

首先在设备树中描述中断:

button {
    compatible = "gpio-keys";
    button1 {
        label = "user-button";
        gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
        linux,code = <KEY_POWER>;
        gpio-key,wakeup;
    };
};

然后在驱动中处理中断:

#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>

struct button_data {
    struct gpio_desc *gpiod;
    int irq;
};

static irqreturn_t button_interrupt(int irq, void *dev_id)
{
    struct button_data *data = dev_id;
    int value = gpiod_get_value(data->gpiod);
    
    printk(KERN_INFO "Button %s\n", value ? "released" : "pressed");
    return IRQ_HANDLED;
}

static int button_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct button_data *data;
    int ret;
    
    data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;
    
    data->gpiod = devm_gpiod_get(dev, NULL, GPIOD_IN);
    if (IS_ERR(data->gpiod))
        return PTR_ERR(data->gpiod);
    
    data->irq = gpiod_to_irq(data->gpiod);
    if (data->irq < 0)
        return data->irq;
    
    ret = devm_request_irq(dev, data->irq, button_interrupt,
                          IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                          "gpio-button", data);
    if (ret) {
        dev_err(dev, "Failed to request IRQ: %d\n", ret);
        return ret;
    }
    
    platform_set_drvdata(pdev, data);
    return 0;
}

在这个例子中,我们通过gpiod_to_irq()将GPIO转换为中断号,然后用devm_request_irq()注册中断处理函数。设备树中定义了按键的GPIO和触发方式,驱动代码不需要关心具体的硬件细节。

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

在实际开发中,肯定会遇到各种问题。我这里分享几个调试设备树和GPIO的实用技巧。

查看设备树节点:编译好设备树后,可以用fdtdump工具查看内容:

fdtdump led-overlay.dtbo | less

检查GPIO状态:在系统启动后,可以查看GPIO状态:

cat /sys/kernel/debug/gpio

检查设备树节点是否生效:在/proc/device-tree下可以查看内核解析后的设备树:

find /proc/device-tree -name "*led*"

手动导出GPIO:如果驱动没有正确申请GPIO,可以手动操作:

echo 5 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio5/direction
echo 1 > /sys/class/gpio/gpio5/value

检查中断状态:查看系统中断统计:

cat /proc/interrupts

我在实际项目中遇到过很多坑,比如设备树语法错误、GPIO编号错误、中断触发方式配置错误等。最重要的是耐心调试,充分利用内核提供的调试接口。

6. 最佳实践:驱动设计建议

根据我多年的经验,总结出以下几点最佳实践:

设备树配置要规范:设备树节点命名要清晰,属性使用要规范。比如GPIO属性应该用gpios而不是自定义的命名。

错误处理要完善:GPIO申请、中断注册等操作都可能失败,一定要做好错误处理。

资源管理要自动化:使用devm_系列函数自动管理资源,避免资源泄漏。

代码要可移植:不要依赖具体的硬件地址,通过设备树获取硬件信息。

功耗管理要考虑:合理配置GPIO的功耗状态,特别是电池供电的设备。

文档要齐全:设备树的兼容性字符串、GPIO用途等都要有清晰的文档。

举个例子,一个好的设备树节点应该是这样的:

// 好的示例:清晰的节点命名,规范的属性使用
led-controller {
    compatible = "pwm-leds";
    
    led-red {
        label = "red-led";
        pwms = <&pwm1 0 1000000>;
        max-brightness = <255>;
        default-brightness = <128>;
    };
};

对应的驱动代码应该这样写:

static int led_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct pwm_device *pwm;
    
    pwm = devm_pwm_get(dev, NULL);
    if (IS_ERR(pwm)) {
        dev_err(dev, "Failed to get PWM: %ld\n", PTR_ERR(pwm));
        return PTR_ERR(pwm);
    }
    
    // ... 其他初始化代码
    return 0;
}

Linux驱动开发确实有一定门槛,但掌握了设备树和GPIO子系统之后,你会发现硬件驱动开发变得简单和规范很多。关键是要理解Linux的设计哲学:机制与策略分离,驱动只关心机制,硬件配置交给设备树。

Logo

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

更多推荐