4. GPIO 实战:LED 驱动与心跳灯

GPIO 是嵌入式驱动里最基础、也最直观的外设。说白了,它就是芯片伸出来的一个个引脚,你可以控制它输出高电平或低电平。今天我们就拿全志 T113 上的 LED 练手,从硬件连接一路写到驱动代码,最后做个心跳灯效果。

我个人习惯,学一个新平台的 GPIO 驱动,一定先点亮一个 LED。这步走通了,后面 I2C、SPI、UART 的驱动框架就都顺了。

本章核心目标: 掌握 T113 GPIO 的设备树描述方式、驱动框架编写方法,以及用户态控制 GPIO 的完整链路。

4.1 LED 硬件连接

先看硬件。T113 开发板上通常预留了用户 LED,我手头这块板子的 LED 接在 PH11 引脚上。什么意思?P 表示端口,H 是端口号,11 是端口内的引脚序号。

电路很简单:LED 正极通过一个 470Ω 限流电阻接到 PH11,负极接地。GPIO 输出高电平时 LED 亮,低电平时灭。

信号 引脚 方向 说明
LED_PIN PH11 输出 高电平点亮
GND 地 - 公共地
小提示: 拿到新板子,第一件事就是看原理图,确认 LED 引脚号和电平逻辑。我遇到过板子 LED 是低电平点亮,驱动写反了,灯死活不亮。

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 就是内核自带的心跳灯效果。
注意: 端口号从 0 开始计数。PA=0, PB=1, PC=2, PH=3。写错了设备树编译不会报错,但驱动加载时会找不到引脚。

4.3 驱动代码实现

设备树写好了,内核自带的 gpio-leds 驱动就能自动识别。但为了讲清楚原理,我们自己写一个简单的 LED 驱动。

驱动核心就三步:

  1. 从设备树获取 GPIO 编号
  2. 申请 GPIO 并设置为输出
  3. 控制 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 知识结构图

下面这张图总结了本章的核心流程,从硬件到驱动再到用户态控制:

GPIO LED 驱动知识结构图 GPIO LED 驱动开发流程 硬件连接 设备树节点 驱动代码 描述 匹配 用户态控制 sysfs 接口 心跳灯效果 定时器控制 💡 关键点: 设备树解耦硬件描述,驱动专注逻辑控制,用户态通过 sysfs 灵活操作。

4.6 调试与故障定位

驱动写好了,灯不亮怎么办?别急,按下面几步排查:

现象 可能原因 排查方法
设备树加载失败 compatible 不匹配 检查 dmesg,看是否有 "of_device_id" 相关错误
GPIO 申请失败 引脚被其他驱动占用 查看 /sys/kernel/debug/gpio,确认引脚状态
LED 不亮 电平逻辑反了 用万用表测引脚电压,确认高电平是否输出
心跳灯不闪烁 定时器未启动 检查 mod_timer 返回值,确认定时器已加入队列
我的经验: 调试 GPIO 驱动,最有效的工具就是 /sys/kernel/debug/gpio。它能显示每个 GPIO 的当前状态、被谁申请、方向是什么。我每次写 GPIO 驱动,第一步就是 cat 这个文件,确认引脚没被占用。

嗯,到这里,LED 驱动和心跳灯效果就全部实现了。从硬件连接、设备树、驱动代码到用户态控制,这条链路走通了,T113 的 GPIO 开发你就入门了。后面遇到更复杂的外设,思路是一样的——先确认硬件,再写设备树,最后实现驱动逻辑。