Linux驱动开发实战:从设备树到GPIO子系统
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的设计哲学:机制与策略分离,驱动只关心机制,硬件配置交给设备树。
更多推荐
所有评论(0)