第4章:GPIO驱动实战——GPIO子系统框架、设备树GPIO配置、GPIO控制接口、实现LED驱动
各位同学,今天我们来啃一块硬骨头——GPIO驱动。说实话,GPIO在嵌入式开发里就像空气一样无处不在。你按个按键、点个灯、控制个继电器,背后都是GPIO在干活。但很多新手一上来就被设备树、子系统框架这些东西搞懵了。别急,我带你一步步捋清楚。
4.1 GPIO子系统框架:内核怎么管GPIO的?
先说说内核里的GPIO子系统。你想想看,一个SoC可能有几十上百个GPIO引脚,每个引脚还能复用成不同的功能。如果每个驱动都直接操作寄存器,那代码就乱成一锅粥了。所以内核搞了个GPIO子系统,统一管理这些引脚。
这个子系统分三层:
- GPIO控制器驱动层:芯片厂商写的,负责操作具体寄存器。比如瑞芯微的pinctrl驱动。
- GPIO核心层:内核提供的通用接口,屏蔽底层差异。
- GPIO使用者层:我们写驱动时调用的API,比如
gpiod_get_value()。
我画了个图,帮你理解这三层的关系:
说白了,我们写驱动时只需要调用核心层提供的API,不用管底层寄存器怎么操作。内核会帮我们把请求路由到对应的GPIO控制器上。
4.2 设备树GPIO配置:怎么告诉内核引脚信息?
设备树是Linux下描述硬件信息的“说明书”。GPIO的配置主要涉及两个属性:
gpio-controller:标记这个节点是一个GPIO控制器。#gpio-cells:指定引用GPIO时需要几个参数。通常是2个:引脚号和标志位。
来看一个RV1126设备树的实际例子:
// 在pinctrl节点中定义GPIO控制器
pinctrl: pinctrl {
compatible = "rockchip,rk1126-pinctrl";
// ... 其他属性
gpio0: gpio0 {
gpio-controller;
#gpio-cells = <2>;
// 中断相关配置
interrupt-controller;
#interrupt-cells = <2>;
};
gpio1: gpio1 {
gpio-controller;
#gpio-cells = <2>;
};
};
// 在LED设备节点中引用GPIO
leds {
compatible = "gpio-leds";
status_led: led-0 {
label = "status_led";
gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_LOW>;
linux,default-trigger = "heartbeat";
};
};
这里有个细节要注意:GPIO_ACTIVE_LOW表示低电平有效。我遇到过有人把高电平有效和低电平有效搞反了,结果灯死活不亮,查了半天才发现是标志位写反了。
gpio-controller属性。结果内核根本不认这个节点,驱动probe都进不去。检查设备树时,一定要确认控制器节点有gpio-controller标志。
4.3 GPIO控制接口:内核提供了哪些API?
内核提供了两套GPIO控制接口:老式的gpio_xxx和新式的gpiod_xxx。我强烈建议你用新接口,它更安全、更现代。
常用的API如下:
| 函数 | 功能 | 说明 |
|---|---|---|
gpiod_get() |
获取GPIO描述符 | 通过设备树中的名字获取 |
gpiod_direction_output() |
设置GPIO为输出 | 同时设置初始电平 |
gpiod_direction_input() |
设置GPIO为输入 | 读取引脚电平前调用 |
gpiod_set_value() |
设置GPIO输出电平 | 0或1 |
gpiod_get_value() |
读取GPIO输入电平 | 返回0或1 |
gpiod_put() |
释放GPIO描述符 | 驱动卸载时调用 |
这些API用起来很简单,但有个坑:gpiod_get()的第二个参数是GPIO的名字,必须和设备树里的label属性对应上。我刚开始用的时候,名字写错了,结果返回NULL,驱动直接崩了。
4.4 实现LED驱动:从零写一个完整的例子
好了,理论知识讲完了,我们来写一个真正的LED驱动。这个驱动会控制RV1126开发板上的一个LED,让它以1秒为周期闪烁。
先看设备树配置:
// rv1126-evb.dts 中添加
/ {
my_led: my-led {
compatible = "my-led-driver";
led-gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_LOW>;
status = "okay";
};
};
然后是驱动代码:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/delay.h>
#include <linux/timer.h>
struct my_led_data {
struct gpio_desc *led_gpio;
struct timer_list timer;
int state;
};
static void led_timer_callback(struct timer_list *t)
{
struct my_led_data *data = from_timer(data, t, timer);
// 翻转LED状态
data->state = !data->state;
gpiod_set_value(data->led_gpio, data->state);
// 重新设置定时器,1秒后再次触发
mod_timer(&data->timer, jiffies + msecs_to_jiffies(1000));
}
static int my_led_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct my_led_data *data;
int ret;
// 分配私有数据
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
// 获取GPIO描述符
data->led_gpio = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(data->led_gpio)) {
dev_err(dev, "Failed to get led gpio\n");
return PTR_ERR(data->led_gpio);
}
// 初始化定时器
timer_setup(&data->timer, led_timer_callback, 0);
data->state = 0;
// 启动定时器
mod_timer(&data->timer, jiffies + msecs_to_jiffies(1000));
platform_set_drvdata(pdev, data);
dev_info(dev, "LED driver probed successfully\n");
return 0;
}
static int my_led_remove(struct platform_device *pdev)
{
struct my_led_data *data = platform_get_drvdata(pdev);
// 删除定时器
del_timer_sync(&data->timer);
// 关闭LED
gpiod_set_value(data->led_gpio, 0);
dev_info(&pdev->dev, "LED driver removed\n");
return 0;
}
static const struct of_device_id my_led_of_match[] = {
{ .compatible = "my-led-driver" },
{ }
};
MODULE_DEVICE_TABLE(of, my_led_of_match);
static struct platform_driver my_led_driver = {
.probe = my_led_probe,
.remove = my_led_remove,
.driver = {
.name = "my_led",
.of_match_table = my_led_of_match,
},
};
module_platform_driver(my_led_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple LED driver for RV1126");
代码看着不长,但每个细节都有讲究。我重点说几个地方:
devm_gpiod_get():带devm_前缀的函数会自动管理资源,驱动卸载时自动释放GPIO。省心!timer_setup():内核新版本的定时器初始化方式。老版本用init_timer(),但新内核已经废弃了。mod_timer():修改定时器的超时时间。这里用来实现周期性闪烁。
编译这个驱动,生成.ko文件,然后加载到开发板上:
# 编译
make -C /path/to/kernel M=$(pwd) modules
# 加载
insmod my_led.ko
# 查看内核日志
dmesg | tail
# 卸载
rmmod my_led
如果一切正常,你会看到LED以1秒为周期闪烁。嗯,那种感觉还是挺有成就感的。
gpioinfo命令查看GPIO的状态。比如gpioinfo gpiochip0可以列出所有引脚的方向和电平。这比看代码快多了。
最后说一句,GPIO驱动是嵌入式Linux驱动开发的入门砖。你把这个搞透了,后面学I2C、SPI、PWM这些子系统就会轻松很多。因为它们的设计思路是相通的——设备树描述硬件,子系统提供API,驱动开发者只管调用。
好了,这一章的内容就到这儿。代码我已经测试过了,在RV1126上跑得很稳。你照着写一遍,遇到问题欢迎交流。