4. GPIO 实战:LED 驱动与心跳灯
GPIO 是嵌入式驱动里最基础、也最直观的外设。说白了,它就是芯片伸出来的一个个引脚,你可以控制它输出高电平或低电平。今天我们就拿全志 T113 上的 LED 练手,从硬件连接一路写到驱动代码,最后做个心跳灯效果。
我个人习惯,学一个新平台的 GPIO 驱动,一定先点亮一个 LED。这步走通了,后面 I2C、SPI、UART 的驱动框架就都顺了。
4.1 LED 硬件连接
先看硬件。T113 开发板上通常预留了用户 LED,我手头这块板子的 LED 接在 PH11 引脚上。什么意思?P 表示端口,H 是端口号,11 是端口内的引脚序号。
电路很简单:LED 正极通过一个 470Ω 限流电阻接到 PH11,负极接地。GPIO 输出高电平时 LED 亮,低电平时灭。
| 信号 | 引脚 | 方向 | 说明 |
|---|---|---|---|
| LED_PIN | PH11 | 输出 | 高电平点亮 |
| GND | 地 | - | 公共地 |
4.2 设备树节点编写
Linux 下,硬件信息通过设备树描述。T113 的设备树文件在 arch/arm/boot/dts/sun8i-t113.dtsi 里。我们要做的是在板级 dts 文件中添加一个 LED 子节点。
为什么用设备树?因为驱动代码不写死引脚号,换板子只需改 dts,不用重新编译驱动。这个设计思路很实用。
// 在 &pio 节点下添加
leds {
compatible = "gpio-leds";
status = "okay";
heartbeat-led {
label = "heartbeat";
gpios = &pio 3 11 GPIO_ACTIVE_HIGH;
linux,default-trigger = "heartbeat";
};
};
这里解释几个关键字段:
- compatible:匹配内核里的 gpio-leds 驱动,这个驱动是内核自带的。
- gpios:&pio 3 11 表示 PH 端口(端口号 3),引脚 11。GPIO_ACTIVE_HIGH 表示高电平有效。
- linux,default-trigger:指定默认触发器,heartbeat 就是内核自带的心跳灯效果。
4.3 驱动代码实现
设备树写好了,内核自带的 gpio-leds 驱动就能自动识别。但为了讲清楚原理,我们自己写一个简单的 LED 驱动。
驱动核心就三步:
- 从设备树获取 GPIO 编号
- 申请 GPIO 并设置为输出
- 控制 GPIO 输出电平
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
static int led_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
int gpio, ret;
// 从设备树获取 GPIO
gpio = of_get_named_gpio(dev->of_node, "gpios", 0);
if (!gpio_is_valid(gpio)) {
dev_err(dev, "invalid gpio\n");
return -EINVAL;
}
// 申请 GPIO
ret = devm_gpio_request(dev, gpio, "my_led");
if (ret) {
dev_err(dev, "gpio request failed\n");
return ret;
}
// 设置为输出,初始低电平
gpio_direction_output(gpio, 0);
// 保存 gpio 到平台数据
platform_set_drvdata(pdev, (void *)(long)gpio);
dev_info(dev, "led probe ok, gpio=%d\n", gpio);
return 0;
}
static int led_remove(struct platform_device *pdev)
{
int gpio = (long)platform_get_drvdata(pdev);
gpio_set_value(gpio, 0);
dev_info(&pdev->dev, "led removed\n");
return 0;
}
static const struct of_device_id led_of_match[] = {
{ .compatible = "my-led" },
{ }
};
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");
驱动写好后,设备树 compatible 要改成 "my-led",然后编译、加载。加载成功后,在 /sys/class/gpio 下就能看到对应的 gpio 节点。
echo 1 > /sys/class/gpio/gpioXXX/value 可以手动控制 LED 亮灭。这是最直接的验证方法。
4.4 创建心跳灯效果
心跳灯就是 LED 按一定节奏闪烁,模拟心跳。内核里已经实现了 heartbeat 触发器,但为了加深理解,我们自己写一个。
思路很简单:用内核定时器,每隔一定时间翻转 GPIO 电平。时间间隔模拟心跳节奏——快一下、慢一下。
static struct timer_list led_timer;
static int led_gpio;
static int beat_count = 0;
static void led_heartbeat(struct timer_list *t)
{
int delay;
// 翻转电平
gpio_set_value(led_gpio, beat_count % 2);
beat_count++;
// 心跳节奏:快-慢-快-慢
if (beat_count % 2 == 0)
delay = 200; // 亮 200ms
else
delay = 600; // 灭 600ms
mod_timer(&led_timer, jiffies + msecs_to_jiffies(delay));
}
static int __init heartbeat_init(void)
{
// 获取 GPIO(这里简化,实际应从设备树获取)
led_gpio = 11; // PH11
gpio_request(led_gpio, "heartbeat");
gpio_direction_output(led_gpio, 0);
timer_setup(&led_timer, led_heartbeat, 0);
mod_timer(&led_timer, jiffies + HZ);
return 0;
}
static void __exit heartbeat_exit(void)
{
del_timer(&led_timer);
gpio_set_value(led_gpio, 0);
gpio_free(led_gpio);
}
module_init(heartbeat_init);
module_exit(heartbeat_exit);
这段代码里,我用了 mod_timer 重新设置定时器,而不是用 add_timer。为什么?因为 mod_timer 可以动态修改超时时间,适合心跳这种变节奏的场景。
gpio_request、kmalloc。因为定时器回调是在软中断上下文执行的。我当时在回调里调了 printk 太多,导致系统卡死,查了半天才发现是日志刷屏。
4.5 知识结构图
下面这张图总结了本章的核心流程,从硬件到驱动再到用户态控制:
4.6 调试与故障定位
驱动写好了,灯不亮怎么办?别急,按下面几步排查:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 设备树加载失败 | compatible 不匹配 | 检查 dmesg,看是否有 "of_device_id" 相关错误 |
| GPIO 申请失败 | 引脚被其他驱动占用 | 查看 /sys/kernel/debug/gpio,确认引脚状态 |
| LED 不亮 | 电平逻辑反了 | 用万用表测引脚电压,确认高电平是否输出 |
| 心跳灯不闪烁 | 定时器未启动 | 检查 mod_timer 返回值,确认定时器已加入队列 |
/sys/kernel/debug/gpio。它能显示每个 GPIO 的当前状态、被谁申请、方向是什么。我每次写 GPIO 驱动,第一步就是 cat 这个文件,确认引脚没被占用。
嗯,到这里,LED 驱动和心跳灯效果就全部实现了。从硬件连接、设备树、驱动代码到用户态控制,这条链路走通了,T113 的 GPIO 开发你就入门了。后面遇到更复杂的外设,思路是一样的——先确认硬件,再写设备树,最后实现驱动逻辑。