前言
在上一篇文章中,我们引入了互斥锁(mutex)保护共享数据,让驱动在多进程并发场景下也能稳定运行。但前三篇的驱动都是纯内存虚拟设备,没有真正触及硬件,即使只有一台x86虚拟机也能完整跑通。
从本文开始,我们将正式进入嵌入式Linux最核心的领域------硬件控制。我们将利用GPIO子系统点亮开发板上的LED,在驱动中实现对物理硬件的实际操作。
本文需要一块嵌入式Linux开发板(如韦东山老师的i.MX6ULL、STM32MP157、树莓派等),因为x86虚拟机没有GPIO外设。如果你的开发板不在手边,可以先学习代码逻辑,后续拿到板子再实测。
读完本文你将掌握:
- 设备树中GPIO节点的理解与修改
- 使用新一代
gpiodAPI获取和控制GPIO引脚 - 在
file_operations的write中根据用户写入内容控制LED亮灭 - 使用
platform_driver架构匹配设备树节点,自动加载驱动

一、GPIO子系统简介
1.1 什么是GPIO子系统
GPIO(General Purpose Input/Output,通用输入输出)是嵌入式处理器最基本的外设。Linux内核提供了完整的GPIO子系统,将各个芯片厂商的GPIO控制器统一抽象为标准的API接口。
GPIO子系统分为两个层次:
- GPIO控制器驱动层(芯片原厂负责):实现硬件寄存器的操作,将具体引脚映射为数字引脚号。
- GPIO消费者接口层(驱动开发者使用):提供统一的API获取和操作GPIO引脚。
1.2 新旧两套API对比
Linux内核有两套GPIO操作接口:
| 特性 | 旧版API(整数描述符) | 新版API(gpiod描述符) |
|---|---|---|
| 头文件 | <linux/gpio.h> |
<linux/gpio/consumer.h> |
| 获取GPIO | gpio_request() |
gpiod_get() |
| 设置方向 | gpio_direction_output() |
gpiod_direction_output() |
| 设置值 | gpio_set_value() |
gpiod_set_value() |
| 释放GPIO | gpio_free() |
gpiod_put() |
| 设备树支持 | 弱(需手动指定引脚号) | 强(通过名称自动匹配) |
强烈建议所有新代码都使用新版gpiod_前缀的API,旧版API已不推荐使用,且无法充分利用设备树的优势。
1.3 GPIO与LED的硬件连接
在典型的嵌入式开发板上,LED的硬件连接如下:
SoC GPIO引脚 ──── 限流电阻 ──── LED ──── GND(或VCC)
当GPIO输出高电平时,LED点亮;输出低电平时,LED熄灭。有些板子采用低电平有效的设计(LED另一端接VCC),此时输出低电平时LED亮,高电平时LED灭。具体电平极性需要通过原理图确认。
二、设计思路
本文以韦东山课程常用的i.MX6ULL开发板为例,驱动框架如下:
- 在设备树中定义一个LED节点,指定所用的GPIO引脚。
- 驱动使用
platform_driver架构,自动匹配设备树节点。 - 在
probe函数中获取GPIO描述符,初始化LED为熄灭状态。 - 在
file_operations.write中根据用户写入的值("0"或"1")控制LED亮灭。 - 驱动加载后在
/dev下生成/dev/gpioled设备节点。
注意:以下代码以i.MX6ULL为例,采用韦东山课程中常用的引脚号方式通过设备树传递GPIO信息。不同芯片(如树莓派、全志D1-H)的设备树结构和GPIO控制器不同,需要根据实际情况调整。
三、设备树修改
3.1 确定GPIO引脚
查看开发板原理图,确定LED连接的GPIO引脚。以常见的i.MX6ULL开发板为例,LED通常连接到 GPIO5_IO03 ,对应的引脚号为 (5 - 1) * 32 + 3 = 131。
引脚号计算公式 :对于i.MX系列芯片,GPIOx_IOy的引脚号 =
(x - 1) * 32 + y。例如GPIO5_IO03 →(5 - 1) * 32 + 3 = 131。但更推荐的做法是直接在设备树中使用&gpio5 3 GPIO_ACTIVE_LOW这种phandle + specifier的形式,让内核自动解析。
韦东山老师的i.MX6ULL_mini开发板上,LED由GPIO5_3控制。
3.2 添加LED设备树节点
在你的板级设备树文件(如arch/arm/boot/dts/imx6ull-xxx.dts)中添加如下节点。注意文件顶部需包含<dt-bindings/gpio/gpio.h>以使用GPIO_ACTIVE_LOW等宏。
dts
/* 在根节点 / 下添加 */
gpioled {
compatible = "yourname,gpioled"; /* 用于与驱动匹配 */
gpios = <&gpio5 3 GPIO_ACTIVE_LOW>; /* 使用GPIO5_IO03,低电平有效 */
status = "okay";
};
关键字段说明:
compatible:驱动中的of_match_table将与此字符串匹配,格式通常为"厂商,设备名"。gpios:&gpio5表示使用GPIO5控制器,3表示该控制器的第3号引脚,GPIO_ACTIVE_LOW表示低电平有效(因为板载LED通常一端接VCC,另一端通过GPIO拉低来点亮)。
其他常见开发板 :树莓派的设备树在
arch/arm/boot/dts/broadcom/目录下,引脚属性可能命名为gpios = <&gpio 23 0>;。请根据实际硬件调整。
修改后重新编译设备树并替换:
bash
make dtbs
# 将生成的.dtb文件复制到/boot目录并重启开发板
四、驱动代码实现
4.1 驱动代码
新建文件 gpioled_drv.c,完整代码如下(修正了读函数缓冲区大小,确保安全):
c
/*
* gpioled_drv.c
* GPIO LED 字符设备驱动。
* 基于platform_driver架构,匹配设备树节点,使用gpiod API控制LED。
* 加载后在 /dev/gpioled 生成设备节点,写入"0"灭LED,写入"1"亮LED。
* 作者:[你的ID]
* 适配内核:Linux 5.x (4.x 亦可)
* 参考开发板:i.MX6ULL(韦东山课程配套板)
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/platform_device.h> /* platform_driver 相关 */
#include <linux/gpio/consumer.h> /* gpiod API */
#include <linux/of.h> /* of_match_table */
#define DEVICE_NAME "gpioled"
#define CLASS_NAME "gpioled_class"
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static struct device *my_device;
static struct gpio_desc *led_gpio; /* GPIO描述符 */
/* 打开设备 */
static int gpioled_open(struct inode *inode, struct file *file)
{
pr_info("gpioled: device opened\n");
return 0;
}
/* 关闭设备 */
static int gpioled_release(struct inode *inode, struct file *file)
{
pr_info("gpioled: device closed\n");
return 0;
}
/* 写入控制:
* 用户写入 "0" 或 "1",驱动控制 LED 熄灭/点亮。
*/
static ssize_t gpioled_write(struct file *file, const char __user *buf,
size_t count, loff_t *f_pos)
{
char kbuf[2] = {0}; /* 期望接收 "0" 或 "1" */
int value;
if (count > 1)
count = 1; /* 只关心第一个字符 */
if (copy_from_user(kbuf, buf, count))
return -EFAULT;
/* 将字符转换为整数值 */
if (kbuf[0] == '1') {
value = 1;
gpiod_set_value(led_gpio, 1); /* 输出高电平 */
pr_info("gpioled: LED ON\n");
} else if (kbuf[0] == '0') {
value = 0;
gpiod_set_value(led_gpio, 0); /* 输出低电平 */
pr_info("gpioled: LED OFF\n");
} else {
pr_warn("gpioled: invalid value '%c', use '0' or '1'\n", kbuf[0]);
return -EINVAL;
}
return count;
}
/* 读取当前LED状态:返回"0\n"或"1\n" */
static ssize_t gpioled_read(struct file *file, char __user *buf,
size_t count, loff_t *f_pos)
{
char kbuf[4]; /* 足够容纳 "0\n\0" 或 "1\n\0" */
int value;
int len;
value = gpiod_get_value(led_gpio); /* 读取当前GPIO电平 */
len = snprintf(kbuf, sizeof(kbuf), "%d\n", value);
if (*f_pos >= len)
return 0; /* EOF */
if (copy_to_user(buf, kbuf, len))
return -EFAULT;
*f_pos += len;
return len;
}
static struct file_operations gpioled_fops = {
.owner = THIS_MODULE,
.open = gpioled_open,
.release = gpioled_release,
.read = gpioled_read,
.write = gpioled_write,
};
/* ---------------- platform_driver 部分 ---------------- */
/* probe:设备树节点匹配成功后被调用 */
static int gpioled_probe(struct platform_device *pdev)
{
int ret;
struct device *dev = &pdev->dev;
pr_info("gpioled: probe called, device matched\n");
/* 1. 从设备树获取 GPIO 描述符。
* NULL 表示使用设备树中第一个 gpios 属性指定的 GPIO。
* GPIOD_OUT_LOW 表示初始化为输出模式且初始低电平(LED灭)。
*/
led_gpio = gpiod_get(dev, NULL, GPIOD_OUT_LOW);
if (IS_ERR(led_gpio)) {
pr_err("gpioled: failed to get gpio descriptor\n");
return PTR_ERR(led_gpio);
}
pr_info("gpioled: gpio descriptor obtained\n");
/* 2. 动态分配设备号 */
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("gpioled: alloc_chrdev_region failed\n");
goto err_alloc;
}
pr_info("gpioled: major=%d, minor=%d\n", MAJOR(dev_num), MINOR(dev_num));
/* 3. 初始化cdev */
cdev_init(&my_cdev, &gpioled_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret) {
pr_err("gpioled: cdev_add failed\n");
goto err_cdev_add;
}
/* 4. 创建class */
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) {
pr_err("gpioled: class_create failed\n");
ret = PTR_ERR(my_class);
goto err_class_create;
}
/* 5. 创建设备节点 */
my_device = device_create(my_class, dev, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(my_device)) {
pr_err("gpioled: device_create failed\n");
ret = PTR_ERR(my_device);
goto err_device_create;
}
pr_info("gpioled: /dev/%s created, you can now control LED\n", DEVICE_NAME);
return 0;
err_device_create:
class_destroy(my_class);
err_class_create:
cdev_del(&my_cdev);
err_cdev_add:
unregister_chrdev_region(dev_num, 1);
err_alloc:
gpiod_put(led_gpio);
return ret;
}
/* remove:设备移除时调用 */
static int gpioled_remove(struct platform_device *pdev)
{
pr_info("gpioled: remove called\n");
/* 先关闭LED(设为低电平)再释放资源 */
gpiod_set_value(led_gpio, 0);
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
gpiod_put(led_gpio); /* 释放GPIO */
pr_info("gpioled: module unloaded\n");
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id gpioled_of_match[] = {
{ .compatible = "yourname,gpioled" }, /* 必须与设备树中的compatible一致 */
{ }
};
MODULE_DEVICE_TABLE(of, gpioled_of_match);
/* platform_driver 结构体 */
static struct platform_driver gpioled_driver = {
.probe = gpioled_probe,
.remove = gpioled_remove,
.driver = {
.name = "gpioled",
.owner = THIS_MODULE,
.of_match_table = gpioled_of_match, /* 绑定设备树匹配表 */
},
};
module_platform_driver(gpioled_driver); /* 替代 module_init/module_exit 的便捷宏 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A GPIO LED character device driver");
MODULE_VERSION("1.0");
4.2 核心代码讲解
为什么使用 platform_driver?
在嵌入式Linux中,大多数片上外设(GPIO、I2C、SPI等)都是通过平台总线(platform bus)来管理的。platform_driver架构让驱动可以与设备树节点自动匹配:内核解析设备树时发现compatible = "yourname,gpioled"的节点后,会自动创建一个platform_device,然后与我们的platform_driver匹配并调用probe函数。这样就实现了设备与驱动的分离 ------设备信息在设备树中描述,驱动逻辑在驱动代码中实现,两者通过compatible属性关联。
gpiod_get 的工作机制
gpiod_get(dev, NULL, GPIOD_OUT_LOW) 会从设备树中获取与dev关联的第一个GPIO资源(因为con_id传了NULL,且设备树中只有一个gpios属性),并初始化为输出模式、初始低电平。成功后返回gpio_desc描述符,后续所有操作都通过它来完成。
电平有效性问题
设备树中使用了 GPIO_ACTIVE_LOW。这意味着gpiod API内部会自动处理电平翻转:调用gpiod_set_value(led_gpio, 1)时,内核理解"1"代表"逻辑有效/点亮",因此实际向硬件输出的是低电平。驱动开发者无需关心物理电平,只需操作逻辑值即可。
module_platform_driver 宏
该宏封装了module_init和module_exit,自动将gpioled_driver注册到平台总线。其内部实现等价于在module_init中调用platform_driver_register(),在module_exit中调用platform_driver_unregister()。
五、Makefile
makefile
# Makefile for gpioled
# 嵌入式开发需要指定交叉编译后的内核源码路径,例如:
# KERNEL_DIR := /home/user/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
# 以下为x86本地编译时使用当前运行内核(仅供编译测试,无法运行)
KERNEL_DIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
obj-m := gpioled_drv.o
all:
make -C $(KERNEL_DIR) M=$(PWD) modules
clean:
make -C $(KERNEL_DIR) M=$(PWD) clean
交叉编译注意:编译给ARM开发板用的驱动时,需要设置工具链和架构:
bash
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf- # 根据你的工具链调整
make
六、测试与验证
6.1 确认设备树生效
开发板启动后,检查设备树节点是否被内核识别:
bash
ls /proc/device-tree/ | grep gpioled
# 应该能看到 gpioled 目录
6.2 加载驱动
将编译好的 gpioled_drv.ko 拷贝到开发板(通过U盘、NFS或scp),然后:
bash
insmod gpioled_drv.ko
如果设备树修改正确,probe函数会被自动调用。查看内核日志:
bash
dmesg | tail -n 6
预期输出:
gpioled: probe called, device matched
gpioled: gpio descriptor obtained
gpioled: major=238, minor=0
gpioled: /dev/gpioled created, you can now control LED
6.3 确认设备节点
bash
ls -l /dev/gpioled
# crw------- 1 root root 238, 0 Jan 1 00:00 /dev/gpioled
如果普通用户无权访问,赋权:
bash
chmod 666 /dev/gpioled
6.4 控制LED
点亮LED:
bash
echo "1" > /dev/gpioled
此时开发板上的LED应点亮,且内核日志显示 gpioled: LED ON。
熄灭LED:
bash
echo "0" > /dev/gpioled
LED熄灭,内核日志显示 gpioled: LED OFF。
6.5 读取LED状态
bash
cat /dev/gpioled
# 输出当前GPIO逻辑电平(1 或 0 后跟换行)
6.6 卸载驱动
bash
rmmod gpioled_drv
卸载时LED会自动熄灭,/dev/gpioled自动删除。
七、LED子系统简介
除了自己从头写GPIO驱动,Linux内核还提供了专门的LED子系统 ,可以更方便地管理LED设备。对于简单的GPIO控制LED,内核已内置通用驱动drivers/leds/leds-gpio.c,只需在设备树中配置即可使用,无需编写任何C代码。
设备树配置示例:
dts
leds {
compatible = "gpio-leds";
led0 {
label = "system-led";
gpios = <&gpio5 3 GPIO_ACTIVE_LOW>;
default-state = "off";
linux,default-trigger = "heartbeat"; /* 心跳闪烁 */
};
};
加载leds-gpio驱动后,LED将出现在/sys/class/leds/system-led/目录下,可以通过以下命令控制:
bash
echo 1 > /sys/class/leds/system-led/brightness # 点亮
echo 0 > /sys/class/leds/system-led/brightness # 熄灭
echo heartbeat > /sys/class/leds/system-led/trigger # 设置为心跳闪烁
可用的触发器包括:none、mmc0、timer、heartbeat、default-on等。
自己写驱动的意义:虽然LED子系统很方便,但从零编写GPIO驱动能帮助我们更深入地理解设备树、platform总线、gpiod API的底层机制,这些知识在编写更复杂的驱动(如I2C/SPI传感器、自定义外设)时至关重要。
八、常见问题排查
-
insmod后probe没有被调用检查设备树中的
compatible字符串是否与驱动中的of_match_table完全一致;确认设备树已正确编译并烧录到开发板。 -
gpiod_get返回错误- 检查设备树中
gpios属性的格式是否正确(<&gpio5 3 GPIO_ACTIVE_LOW>之间没有多余逗号)。 - 确认GPIO引脚没有被其他驱动占用。可通过
cat /sys/kernel/debug/gpio查看当前GPIO占用情况。
- 检查设备树中
-
LED点不亮
- 检查原理图确认LED的接线极性(共阳还是共阴)。
- 尝试将
GPIOD_OUT_LOW改为GPIOD_OUT_HIGH(或反过来)。 - 确保没有漏掉
GPIOD_OUT_LOW标志,否则GPIO仍为输入模式。
-
编译时找不到
<linux/gpio/consumer.h>内核版本过旧(低于3.x),请升级内核或使用旧版API(不推荐)。
九、总结与下篇预告
本文完成了从纯软件驱动到硬件控制的跨越,利用GPIO子系统成功点亮了开发板上的LED。核心要点回顾:
- 设备树 描述了硬件资源(GPIO引脚),驱动通过
gpiod_get获取资源。 - platform_driver 实现了设备与驱动的自动匹配,通过
compatible字符串关联。 - gpiod API屏蔽了底层硬件差异,开发者只需操作逻辑电平。
前三篇文章中,我们可以在x86虚拟机中编译和测试所有代码。本文开始正式进入嵌入式硬件世界,需要真实的ARM/Linux开发板。如果你手上暂时没有开发板,可以先理解代码逻辑和驱动架构,待拿到板子后再实际验证。
下篇预告 :LED的开和关是GPIO最基本的输出控制。下一篇文章我们将实现LED的PWM调光,通过修改占空比来调节LED的亮度,实现呼吸灯效果。敬请期待!
如果本文对你有帮助,欢迎点赞、收藏、关注。有任何技术疑问,欢迎在评论区留言交流!