树莓派4B设备树实战:从零开始手把手教你用DTS控制LED(附完整代码)
树莓派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@7e200000 | GPIO控制器 |
| /soc/serial@7e201000 | UART接口 |
| /soc/mmc@7e300000 | SD卡控制器 |
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实现以下功能:
- 设备树匹配
- GPIO资源映射
- 字符设备接口
- 用户空间控制
关键数据结构:
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 常见问题排查
-
设备树加载失败:
- 检查
dmesg | grep myled - 确认
/boot/overlays/myled.dtbo存在
- 检查
-
GPIO冲突:
cat /sys/kernel/debug/gpio -
驱动加载错误:
- 检查
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 性能优化技巧
-
延迟加载:
module_init(myled_init); late_initcall(myled_late_init); // 延迟初始化 -
中断优化:
request_threaded_irq(gpio_to_irq(gpio), NULL, myled_isr, IRQF_ONESHOT, "myled", data); -
电源管理:
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更直观。
更多推荐


所有评论(0)