3、GPIO子系统与LED驱动实战:GPIO子系统框架、设备树配置、实现LED亮灭控制
各位同学,欢迎来到第三章。
这一章我们聊点实在的——GPIO子系统。说白了,就是教你怎么让Linux内核里的一个引脚听话,让它输出高电平就高电平,低电平就低电平。你可能会想:“这有什么难的?不就是写个寄存器吗?”嗯,在裸机开发里确实是这样。但在Linux内核里,事情就没那么简单了。
3.1 GPIO子系统框架:内核为什么非要“绕弯子”?
先说说为什么要有GPIO子系统。我个人习惯把内核想象成一个公司,GPIO引脚就是公司的员工。你不能让每个部门经理都直接去指挥员工干活,那样就乱套了。所以内核搞了个“人力资源部”——GPIO子系统,统一管理所有引脚。
这个框架分三层:
- GPIO硬件层:最底层,直接操作寄存器。不同芯片(比如STM32、全志、瑞芯微)的寄存器都不一样。
- GPIO核心层:中间层,提供统一的API。比如gpio_request()、gpio_direction_output()、gpio_set_value()。你写驱动时调的就是这些函数。
- GPIO消费层:最上层,也就是你的驱动代码。你只需要调用核心层的API,不用关心底层寄存器怎么配。
为什么要这么分层?我在项目中遇到过一件事:某次换芯片,从A公司的换成B公司的。如果直接操作寄存器,整个驱动都得重写。但用了GPIO子系统,只需要改设备树和底层驱动,上层代码一行都不用动。你想想看,这省了多少事?
核心思想:GPIO子系统的本质就是“抽象”。把硬件差异封装在底层,给上层提供统一的接口。
3.2 设备树配置:让内核认识你的LED
设备树,说白了就是一张“硬件配置表”。你告诉内核:“我这个LED接在GPIO1_3上,默认是低电平。”内核读到这张表,就知道该怎么初始化了。
来看一个典型的设备树节点:
leds {
compatible = "gpio-leds";
status = "okay";
led0: led_red {
label = "red-led";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
default-state = "off";
linux,default-trigger = "none";
};
};
这里有几个关键点:
- compatible:匹配驱动。内核看到"gpio-leds",就知道该用哪个驱动来处理这个节点。
- gpios:指定引脚。&gpio1 3 表示GPIO1组的第3号引脚。GPIO_ACTIVE_LOW表示低电平有效——也就是说,写1时引脚输出低电平,写0时输出高电平。这个很容易搞反,我曾经就因为这个让LED死活不亮,查了半天才发现是电平极性配反了。
- default-state:默认状态。可以是"on"或"off"。
- linux,default-trigger:默认触发方式。比如"heartbeat"可以让LED像心跳一样闪烁,"mmc0"可以让LED在读写SD卡时闪烁。这里设成"none",就是纯手动控制。
小技巧:设备树里配GPIO时,一定要确认引脚号对不对。不同芯片的GPIO编号方式不一样,有的从0开始,有的从1开始。我建议你先把芯片手册翻出来,对着引脚图一个一个核对。
3.3 实现LED亮灭控制:从设备树到用户空间
设备树配好了,接下来就是写驱动。不过好消息是,Linux内核已经自带了gpio-leds驱动。你只要在设备树里配好节点,内核启动后就会自动在/sys/class/leds/目录下创建对应的控制接口。
比如,上面配的"red-led",启动后你会看到:
/sys/class/leds/red-led/
├── brightness
├── max_brightness
├── trigger
└── ...
控制LED亮灭,其实就是往brightness文件里写值:
# 点亮LED(注意:如果配的是GPIO_ACTIVE_LOW,写1其实是灭)
echo 1 > /sys/class/leds/red-led/brightness
# 熄灭LED
echo 0 > /sys/class/leds/red-led/brightness
就这么简单?嗯,就这么简单。但如果你非要自己写驱动,那也可以。我带你走一遍流程:
#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
static int led_gpio;
static int __init led_init(void)
{
int ret;
// 从设备树获取GPIO号
led_gpio = of_get_named_gpio(dev->of_node, "gpios", 0);
if (!gpio_is_valid(led_gpio)) {
pr_err("Invalid GPIO\n");
return -EINVAL;
}
// 申请GPIO
ret = gpio_request(led_gpio, "my-led");
if (ret) {
pr_err("Failed to request GPIO\n");
return ret;
}
// 设置为输出,默认低电平
gpio_direction_output(led_gpio, 0);
// 点亮LED
gpio_set_value(led_gpio, 1);
return 0;
}
static void __exit led_exit(void)
{
gpio_set_value(led_gpio, 0);
gpio_free(led_gpio);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
这段代码的核心就三步:
- 获取GPIO号:从设备树里解析出引脚编号。
- 申请GPIO:告诉内核“这个引脚我用了,别人别碰”。
- 控制电平:gpio_set_value()写1或0。
注意:gpio_request()一定要做。我曾经偷懒没调这个函数,结果另一个驱动也用了同一个引脚,两个驱动互相抢资源,系统直接死机。嗯,从那以后我再也不敢省这一步了。
3.4 避坑指南:那些年我踩过的GPIO坑
做GPIO驱动,有几个坑是绕不开的。我列出来,你记一下:
| 坑 | 现象 | 原因 | 解决方法 |
|---|---|---|---|
| 引脚复用冲突 | GPIO不受控,电平乱跳 | 同一个引脚被多个外设占用 | 检查设备树,确保pinctrl配置正确 |
| 电平极性搞反 | 写1灭灯,写0亮灯 | GPIO_ACTIVE_LOW/GPIO_ACTIVE_HIGH配反 | 对照原理图确认LED接法 |
| GPIO号不对 | 驱动报错"Invalid GPIO" | 设备树里的引脚号与芯片手册不一致 | 用gpioinfo命令查看实际引脚号 |
| 忘记申请GPIO | 驱动加载成功但控制无效 | 没有调用gpio_request() | 养成习惯,申请后再操作 |
这里我特别想强调一下引脚复用的问题。现在的SoC功能越来越复杂,一个引脚往往能当GPIO用,也能当I2C、SPI、UART用。如果你在设备树里配了GPIO,但pinctrl里又配成了I2C,那这个引脚到底听谁的?答案是——谁先申请谁先用,但结果往往是两个都用不了。
所以我的习惯是:写设备树之前,先打开芯片手册,把引脚功能表看一遍。确认这个引脚没有被其他外设占用,再动手配。
3.5 实战总结:从点亮一个LED开始
好了,这一章的内容就这些。我们来捋一捋:
- GPIO子系统把硬件操作封装成了统一的API,你只需要调函数,不用管寄存器。
- 设备树是硬件配置表,告诉内核LED接在哪个引脚上、电平极性是什么。
- 控制LED亮灭,要么用/sys/class/leds/接口,要么自己写驱动调gpio_set_value()。
点亮一个LED,听起来很简单。但它是你理解Linux驱动开发的起点。后面我们会在这个基础上,一步步加上中断、定时器、PWM,甚至把它变成一个完整的物联网设备。
下一章,我们聊中断子系统。你会看到,当一个按键按下时,内核是怎么“听到”并“响应”的。嗯,那才是真正有意思的地方。