第4章 GPIO驱动开发:GPIO子系统介绍、设备树中GPIO的配置、基于GPIO的LED驱动实战
各位同学,今天我们来聊聊GPIO驱动开发。说实话,GPIO是嵌入式开发里最基础也最常用的外设了。我刚开始做驱动时,第一个练手的项目就是点灯。你别笑,能把灯点得稳、点得准,说明你对整个驱动框架已经摸透了。
4.1 GPIO子系统:内核里的“万能接口”
GPIO子系统,说白了就是Linux内核给开发者提供的一套标准API。你想想看,不同芯片的GPIO寄存器地址、控制方式都不一样,要是每个驱动都直接操作寄存器,那代码就没法移植了。
内核的GPIO子系统分三层:
- 底层:芯片厂商实现的gpio_chip,负责读写寄存器
- 中间层:gpiolib,提供统一的接口封装
- 上层:驱动开发者调用的gpiod_xxx系列API
我在项目中遇到过一件事:有个同事直接操作GPIO寄存器来实现按键检测,结果换了颗芯片后,整个驱动都得重写。这就是没用好子系统的教训。
核心API速查表
| 函数 | 作用 |
|---|---|
| gpiod_get() | 获取GPIO描述符 |
| gpiod_direction_output() | 设置为输出模式 |
| gpiod_set_value() | 设置输出电平 |
| gpiod_direction_input() | 设置为输入模式 |
| gpiod_get_value() | 读取输入电平 |
4.2 设备树中GPIO的配置:把硬件描述交给DTS
设备树,就是用来描述硬件拓扑的。GPIO的配置都在这里完成。我个人习惯把设备树里的GPIO节点想象成“硬件接线图”——哪个引脚接了什么东西,一目了然。
来看一个典型的LED节点配置:
leds {
compatible = "gpio-leds";
led0: led-red {
label = "red-led";
gpios = &gpio1 15 GPIO_ACTIVE_LOW;
default-state = "off";
};
};
这里有几个关键点:
gpios属性:第一个参数是GPIO控制器,第二个是引脚号,第三个是有效电平GPIO_ACTIVE_LOW:表示低电平点亮。嗯,这里要注意,很多新手会搞反default-state:上电后的默认状态
我的小技巧:调试设备树时,可以在内核启动参数里加gpio_debug,这样系统会打印出所有GPIO的申请和释放信息。我曾经靠这个定位过一个GPIO被两个驱动同时占用的bug。
4.3 基于GPIO的LED驱动实战:从零开始点灯
好,理论说完了,咱们直接上手写代码。这个LED驱动,我建议你用gpio-leds框架来做,内核已经封装好了,你只需要提供设备树节点就行。
但为了让你真正理解底层原理,我带你手写一个简单的LED驱动:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
struct led_data {
struct gpio_desc *led_gpio;
};
static int led_probe(struct platform_device *pdev)
{
struct led_data *data;
struct device *dev = &pdev->dev;
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
// 从设备树获取GPIO
data->led_gpio = devm_gpiod_get(dev, NULL, GPIOD_OUT_LOW);
if (IS_ERR(data->led_gpio))
return PTR_ERR(data->led_gpio);
gpiod_set_value(data->led_gpio, 1); // 点亮LED
dev_info(dev, "LED is on!\n");
platform_set_drvdata(pdev, data);
return 0;
}
static int led_remove(struct platform_device *pdev)
{
struct led_data *data = platform_get_drvdata(pdev);
gpiod_set_value(data->led_gpio, 0); // 熄灭LED
return 0;
}
static const struct of_device_id led_of_match[] = {
{ .compatible = "my-led-driver" },
{ }
};
MODULE_DEVICE_TABLE(of, led_of_match);
static struct platform_driver led_driver = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "my_led",
.of_match_table = led_of_match,
},
};
module_platform_driver(led_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple LED driver");
这段代码里,我用了devm_gpiod_get这个函数。为什么用带devm_前缀的版本?因为它会自动管理资源,驱动卸载时自动释放GPIO。你想想看,要是忘了释放GPIO,下次加载驱动就会报错。
避坑指南:我曾经在probe函数里直接调用gpio_request和gpio_direction_output,结果在设备树里改了引脚号后,驱动就崩溃了。后来改用gpiod_系列API,问题就解决了。因为gpiod_会自动解析设备树里的gpios属性,不用你手动传引脚号。
4.4 调试技巧:让LED“说话”
驱动写完了,怎么验证?我一般用这几个方法:
- 看内核日志:
dmesg | grep my_led,看probe有没有成功 - 操作sysfs:如果用了gpio-leds框架,
/sys/class/leds/下会有对应节点,直接echo 1 > brightness就能控制 - 示波器测量:实在不行就上示波器,看引脚电平有没有变化
我记得有一次,LED死活不亮。查了半天,发现是设备树里GPIO_ACTIVE_LOW写成了GPIO_ACTIVE_HIGH。电平反了,灯当然不亮。这种低级错误,犯过一次就记住了。
好了,这一章的内容就到这里。GPIO驱动开发,说白了就是“获取描述符、设置方向、读写电平”这三步。但每一步里都有坑,多动手、多踩坑,你才能真正成为老手。