树莓派4B设备树实战:从零开始手把手教你用DTS控制LED(附完整代码)

当你第一次拿到树莓派4B开发板时,可能会被它丰富的GPIO接口所吸引。但如何安全、高效地控制这些接口?设备树(Device Tree)技术就是解决这个问题的金钥匙。不同于传统的直接寄存器操作,设备树提供了一种更优雅的硬件描述方式,让驱动开发与硬件配置完美解耦。

本文将带你从设备树的基础概念出发,通过完整的LED控制案例,掌握树莓派4B上设备树的实战应用。无论你是刚接触嵌入式开发的初学者,还是希望系统学习Linux驱动开发的工程师,都能从中获得可直接复用的工程经验。

1. 设备树基础与开发环境搭建

1.1 为什么需要设备树?

在早期的嵌入式Linux开发中,硬件信息通常直接硬编码在内核源码中。这种方式存在明显的弊端:

  • 内核臃肿:需要为不同硬件编译不同内核
  • 维护困难:硬件变更需重新编译内核
  • 兼容性差:难以支持同一SOC的不同板级设计

设备树的出现完美解决了这些问题。它采用分离式设计,将硬件描述(设备树)与驱动实现(内核)解耦。具体到树莓派4B:

// 典型设备树节点示例
gpio@7e200000 {
    compatible = "brcm,bcm2835-gpio";
    reg = <0x7e200000 0xb4>;
    interrupts = <2 17>;
};

1.2 开发环境准备

开始前需要准备:

  • 硬件:

    • 树莓派4B开发板
    • LED模块及220Ω电阻
    • 杜邦线若干
  • 软件:

    • 最新版Raspberry Pi OS
    • 内核源码(建议与运行系统版本一致)
    • 交叉编译工具链

提示:获取内核源码可使用sudo apt install raspberrypi-kernel-headers,或从GitHub克隆官方仓库。

验证开发环境:

# 检查设备树编译器
dtc --version
# 查看当前加载的设备树
ls /proc/device-tree/

2. 设备树节点编写与编译

2.1 理解树莓派4B的设备树结构

树莓派4B采用Broadcom BCM2711 SoC,其设备树文件主要位于:

/boot/bcm2711-rpi-4-b.dtb       # 编译后的二进制设备树
/arch/arm/boot/dts/bcm2711-rpi-4-b.dts # 源文件

关键节点包括:

节点路径描述
/soc/gpio@7e200000GPIO控制器
/soc/serial@7e201000UART接口
/soc/mmc@7e300000SD卡控制器

2.2 添加自定义LED节点

在/boot/overlays目录下创建myled.dts:

/dts-v1/;
/plugin/;

/ {
    compatible = "brcm,bcm2711";

    fragment@0 {
        target = <&gpio>;
        __overlay__ {
            myled_pin: myled_pin {
                brcm,pins = <4>;       // 使用GPIO4
                brcm,function = <1>;   // 输出模式
            };
        };
    };

    fragment@1 {
        target-path = "/";
        __overlay__ {
            myled {
                compatible = "myled";
                pinctrl-names = "default";
                pinctrl-0 = <&myled_pin>;
                status = "okay";
            };
        };
    };
};

编译设备树:

dtc -@ -I dts -O dtb -o myled.dtbo myled.dts
sudo cp myled.dtbo /boot/overlays/

2.3 加载自定义设备树

在/boot/config.txt末尾添加:

dtoverlay=myled

重启后验证节点:

# 查看节点是否存在
ls /proc/device-tree/myled
# 检查GPIO状态
raspi-gpio get 4

3. Platform驱动开发实战

3.1 驱动框架设计

创建myled_driver.c实现以下功能:

  1. 设备树匹配
  2. GPIO资源映射
  3. 字符设备接口
  4. 用户空间控制

关键数据结构:

static const struct of_device_id myled_ids[] = {
    { .compatible = "myled" },
    { /* sentinel */ }
};

static struct platform_driver myled_driver = {
    .driver = {
        .name = "myled",
        .of_match_table = myled_ids,
    },
    .probe = myled_probe,
    .remove = myled_remove,
};

3.2 资源获取与映射

在probe函数中处理设备树参数:

static int myled_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct myled_data *data;
    
    // 获取设备树节点
    np = dev->of_node;
    
    // 分配私有数据结构
    data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
    
    // 获取GPIO编号
    data->led_gpio = of_get_named_gpio(np, "led-gpios", 0);
    
    // GPIO申请与配置
    ret = devm_gpio_request(dev, data->led_gpio, "myled");
    gpio_direction_output(data->led_gpio, 0);
    
    // 注册字符设备
    alloc_chrdev_region(&data->devno, 0, 1, "myled");
    cdev_init(&data->cdev, &myled_fops);
    cdev_add(&data->cdev, data->devno, 1);
    
    platform_set_drvdata(pdev, data);
    return 0;
}

3.3 用户空间接口实现

文件操作集实现:

static ssize_t myled_write(struct file *file, const char __user *buf,
                          size_t count, loff_t *ppos)
{
    struct myled_data *data = container_of(file->private_data,
                                          struct myled_data, cdev);
    char val;
    
    if (copy_from_user(&val, buf, 1))
        return -EFAULT;
    
    gpio_set_value(data->led_gpio, val ? 1 : 0);
    return 1;
}

static const struct file_operations myled_fops = {
    .owner = THIS_MODULE,
    .write = myled_write,
};

4. 完整案例测试与调试

4.1 编译与加载驱动

Makefile示例:

obj-m := myled_driver.o
KDIR := /lib/modules/$(shell uname -r)/build

all:
    make -C $(KDIR) M=$(PWD) modules

加载驱动:

make
sudo insmod myled_driver.ko
dmesg | tail  # 查看内核日志

4.2 用户空间测试程序

创建ledtest.c:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    int fd;
    char val;
    
    fd = open("/dev/myled", O_WRONLY);
    if (fd < 0) {
        perror("open");
        return -1;
    }
    
    val = argv[1][0] == '1' ? 1 : 0;
    write(fd, &val, 1);
    close(fd);
    return 0;
}

编译测试:

gcc -o ledtest ledtest.c
sudo ./ledtest 1  # LED亮
sudo ./ledtest 0  # LED灭

4.3 常见问题排查

  1. 设备树加载失败:

    • 检查dmesg | grep myled
    • 确认/boot/overlays/myled.dtbo存在
  2. GPIO冲突:

    cat /sys/kernel/debug/gpio
    
  3. 驱动加载错误:

    • 检查modinfo myled_driver.ko
    • 验证/proc/device-tree/myled节点

5. 进阶技巧与优化建议

5.1 多LED控制扩展

修改设备树支持多LED:

myled {
    compatible = "myled";
    leds {
        led0 = <&gpio 4 0>;
        led1 = <&gpio 17 0>;
    };
};

驱动中解析:

int led_count;
struct gpio_desc *leds;

led_count = of_gpio_named_count(np, "leds");
leds = devm_kcalloc(dev, led_count, sizeof(*leds), GFP_KERNEL);

for (i = 0; i < led_count; i++) {
    leds[i] = of_get_named_gpio(np, "leds", i);
    gpio_request(leds[i], "myled");
}

5.2 添加PWM调光功能

利用树莓派硬件PWM:

fragment@2 {
    target = <&pwm>;
    __overlay__ {
        pinctrl-names = "default";
        pinctrl-0 = <&pwm0_gpio18>;
        assigned-clock-rates = <1000000>;
        status = "okay";
    };
};

驱动实现:

struct pwm_device *pwm;

pwm = pwm_request(0, "myled-pwm");
pwm_config(pwm, 500000, 1000000);  // 50%占空比
pwm_enable(pwm);

5.3 性能优化技巧

  1. 延迟加载:

    module_init(myled_init);
    late_initcall(myled_late_init);  // 延迟初始化
    
  2. 中断优化:

    request_threaded_irq(gpio_to_irq(gpio), NULL, 
                        myled_isr, IRQF_ONESHOT,
                        "myled", data);
    
  3. 电源管理:

    static int myled_suspend(struct device *dev)
    {
        struct myled_data *data = dev_get_drvdata(dev);
        gpio_set_value(data->led_gpio, 0);
        return 0;
    }
    

在实际项目中,我发现设备树的版本兼容性需要特别注意。不同内核版本对设备树语法支持有差异,建议在项目开始时就确定内核版本,并保持开发环境与目标系统一致。调试时可以使用fdtdump工具查看DTB文件内容,这比直接阅读DTS更直观。

更多推荐