4. GPIO驱动实战-点灯

4.1 本章概述与学习目标

本章是驱动开发岗位实战的第一个完整驱动案例。我们将从零开始,基于设备树描述硬件,编写一个完整的GPIO字符设备驱动,最终在用户空间通过sysfs接口控制LED的亮灭。通过本章学习,你将掌握嵌入式Linux驱动开发的核心流程:设备树节点定义 → 驱动匹配 → 资源获取 → 硬件操作 → 用户空间接口暴露。

岗位对标:本章内容直接对应驱动开发岗位的“板级bring-up”与“外设驱动开发”两个核心职责。面试中常被问及的“设备树与驱动如何匹配”、“GPIO控制流程”、“sysfs接口实现”等问题,均在本章得到实战解答。

4.2 整体架构:从设备树到用户空间

下图展示了本章驱动的完整软件架构。左侧是硬件描述层(设备树),中间是内核驱动层,右侧是用户空间接口层。数据流从用户空间的sysfs写入开始,经过驱动解析、GPIO操作,最终控制LED硬件。

GPIO点灯驱动软件架构图 设备树(Device Tree) led-gpios = &gpio1 15 GPIO_ACTIVE_HIGH compatible = "vendor,my-led" label = "user-led0" 解析 内核驱动(Kernel Driver) of_match_table → probe() devm_gpiod_get_index_optional() gpiod_set_value() / gpiod_get_value() 暴露 用户空间(User Space) /sys/class/leds/user-led0/ brightness (0/1) trigger, max_brightness 硬件层:SoC GPIO Controller → 外部LED电路 GPIO Bank 1, Pin 15 → 限流电阻 → LED (阳极接VCC) 控制

4.3 设备树节点设计

设备树是驱动与硬件之间的“契约”。我们需要在板级dts文件中添加一个LED子节点,描述其使用的GPIO引脚、极性以及标签。

4.3.1 设备树节点示例

/* 在根节点或合适的总线节点下添加 */
myleds {
    compatible = "vendor,my-led";
    status = "okay";

    led0: user-led0 {
        label = "user-led0";
        gpios = &gpio1 15 GPIO_ACTIVE_HIGH;
        default-state = "off";
        linux,default-trigger = "none";
    };
};

4.3.2 关键属性说明

属性含义典型值
compatible驱动匹配字符串,必须与驱动中of_match_table一致"vendor,my-led"
gpiosGPIO描述:控制器句柄 + 引脚号 + 标志位&gpio1 15 GPIO_ACTIVE_HIGH
labelsysfs中显示的LED名称"user-led0"
default-state驱动加载时的初始状态"off" / "on" / "keep"
linux,default-trigger默认触发器,如"heartbeat"、"mmc0"等"none"
常见陷阱:GPIO_ACTIVE_HIGH / GPIO_ACTIVE_LOW 标志位直接影响gpiod_set_value()的行为。如果硬件上LED是低电平点亮(阴极接GPIO),应使用GPIO_ACTIVE_LOW,这样驱动中写1代表点亮,写0代表熄灭,逻辑与用户期望一致。

4.4 驱动代码实现

驱动代码分为三个核心部分:设备树匹配表、probe/remove回调、以及sysfs文件操作。我们使用Linux内核的LED class框架,它已经封装好了sysfs接口,我们只需注册一个led_classdev设备。

4.4.1 头文件与数据结构

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/gpio/consumer.h>
#include <linux/leds.h>

struct my_led_data {
    struct led_classdev cdev;
    struct gpio_desc *gpiod;
};

4.4.2 设备树匹配表

static const struct of_device_id my_led_of_match[] = {
    { .compatible = "vendor,my-led", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_led_of_match);

4.4.3 probe函数:获取GPIO并注册LED

static int my_led_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_led_data *led;
    struct fwnode_handle *child;
    int count, ret;

    count = device_get_child_node_count(dev);
    if (count == 0)
        return -ENODEV;

    device_for_each_child_node(dev, child) {
        led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL);
        if (!led)
            return -ENOMEM;

        led->gpiod = devm_fwnode_gpiod_get(dev, child, NULL, GPIOD_ASIS, NULL);
        if (IS_ERR(led->gpiod)) {
            ret = PTR_ERR(led->gpiod);
            if (ret != -EPROBE_DEFER)
                dev_err(dev, "Failed to get GPIO: %d\n", ret);
            return ret;
        }

        /* 设置初始状态:默认关闭 */
        gpiod_direction_output(led->gpiod, 0);

        /* 填充led_classdev结构 */
        led->cdev.name = fwnode_get_name(child);
        led->cdev.brightness_set_blocking = my_led_set;
        led->cdev.max_brightness = 1;  /* 仅支持开关 */

        ret = devm_led_classdev_register(dev, &led->cdev);
        if (ret) {
            dev_err(dev, "Failed to register LED: %d\n", ret);
            return ret;
        }

        dev_info(dev, "LED '%s' registered\n", led->cdev.name);
    }

    return 0;
}

4.4.4 brightness_set_blocking回调

static void my_led_set(struct led_classdev *cdev,
                       enum led_brightness value)
{
    struct my_led_data *led =
        container_of(cdev, struct my_led_data, cdev);

    gpiod_set_value(led->gpiod, !!value);
}

4.4.5 remove函数与module入口

static int my_led_remove(struct platform_device *pdev)
{
    /* devm_* 系列API自动释放资源,无需手动操作 */
    dev_info(&pdev->dev, "LED driver removed\n");
    return 0;
}

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_AUTHOR("Your Name");
MODULE_DESCRIPTION("Device Tree based GPIO LED driver");
MODULE_LICENSE("GPL v2");

4.5 用户空间sysfs接口操作

驱动加载成功后,内核会自动在 /sys/class/leds/ 目录下创建以label命名的子目录。以下是常用的操作命令:

4.5.1 查看已注册的LED

# ls /sys/class/leds/
user-led0

4.5.2 点亮LED

# echo 1 > /sys/class/leds/user-led0/brightness

4.5.3 熄灭LED

# echo 0 > /sys/class/leds/user-led0/brightness

4.5.4 查看当前亮度值

# cat /sys/class/leds/user-led0/brightness
1

4.5.5 设置触发器(如心跳闪烁)

# echo heartbeat > /sys/class/leds/user-led0/trigger
# cat /sys/class/leds/user-led0/trigger
[none] mmc0 mmc1 heartbeat timer oneshot ...
调试技巧:如果写入brightness后LED无反应,首先检查GPIO引脚复用是否正确(是否被其他外设占用),其次用万用表测量GPIO引脚电平是否变化。也可以先通过内核的GPIO debugfs接口手动测试:echo 15 > /sys/kernel/debug/gpio。

4.6 驱动加载与测试流程

下表总结了从编译到验证的完整步骤:

步骤操作预期结果
1修改设备树,添加LED节点编译生成新的dtb文件
2编译驱动模块 my_led.ko生成.ko文件,无编译错误
3更新设备树并重启系统内核启动时解析新dtb
4insmod my_led.ko 或编译进内核dmesg显示"LED 'user-led0' registered"
5ls /sys/class/leds/看到 user-led0 目录
6echo 1 > brightnessLED点亮
7echo 0 > brightnessLED熄灭

4.7 常见问题与排查方法

  • 问题:probe函数未被调用。
    排查:检查compatible字符串是否完全匹配;检查设备树节点status是否为"okay";检查内核是否编译了对应的驱动。
  • 问题:GPIO获取失败,返回-EPROBE_DEFER。
    排查:GPIO控制器驱动可能尚未加载,内核会自动重试probe。如果持续失败,检查GPIO编号和bank是否正确。
  • 问题:写入brightness无反应,但dmesg无错误。
    排查:检查GPIO_ACTIVE_LOW/GPIO_ACTIVE_HIGH是否与硬件电路匹配;检查GPIO引脚是否被其他驱动占用(如pinctrl)。
  • 问题:sysfs目录出现但brightness文件权限不足。
    排查:默认权限为0644,root用户可读写。普通用户需通过udev规则或chmod修改权限。

4.8 本章小结

本章完成了一个完整的GPIO点灯驱动实战,覆盖了从设备树描述、驱动代码编写、到用户空间sysfs接口验证的全流程。核心知识点包括:

  • 设备树中GPIO节点的标准写法与属性含义
  • 基于of_match_table的驱动匹配机制
  • 使用devm_gpiod_get系列API获取并控制GPIO
  • 利用LED class框架自动生成sysfs接口
  • 用户空间通过/sys/class/leds/控制LED