4. GPIO驱动实战-点灯
4.1 本章概述与学习目标
本章是驱动开发岗位实战的第一个完整驱动案例。我们将从零开始,基于设备树描述硬件,编写一个完整的GPIO字符设备驱动,最终在用户空间通过sysfs接口控制LED的亮灭。通过本章学习,你将掌握嵌入式Linux驱动开发的核心流程:设备树节点定义 → 驱动匹配 → 资源获取 → 硬件操作 → 用户空间接口暴露。
岗位对标:本章内容直接对应驱动开发岗位的“板级bring-up”与“外设驱动开发”两个核心职责。面试中常被问及的“设备树与驱动如何匹配”、“GPIO控制流程”、“sysfs接口实现”等问题,均在本章得到实战解答。
4.2 整体架构:从设备树到用户空间
下图展示了本章驱动的完整软件架构。左侧是硬件描述层(设备树),中间是内核驱动层,右侧是用户空间接口层。数据流从用户空间的sysfs写入开始,经过驱动解析、GPIO操作,最终控制LED硬件。
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" |
| gpios | GPIO描述:控制器句柄 + 引脚号 + 标志位 | &gpio1 15 GPIO_ACTIVE_HIGH |
| label | sysfs中显示的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 |
| 4 | insmod my_led.ko 或编译进内核 | dmesg显示"LED 'user-led0' registered" |
| 5 | ls /sys/class/leds/ | 看到 user-led0 目录 |
| 6 | echo 1 > brightness | LED点亮 |
| 7 | echo 0 > brightness | LED熄灭 |
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