Linux字符设备驱动开发(四):进入硬件世界——GPIO子系统与LED设备驱动

前言

上一篇文章中,我们引入了互斥锁(mutex)保护共享数据,让驱动在多进程并发场景下也能稳定运行。但前三篇的驱动都是纯内存虚拟设备,没有真正触及硬件,即使只有一台x86虚拟机也能完整跑通。

从本文开始,我们将正式进入嵌入式Linux最核心的领域------硬件控制。我们将利用GPIO子系统点亮开发板上的LED,在驱动中实现对物理硬件的实际操作。

本文需要一块嵌入式Linux开发板(如韦东山老师的i.MX6ULL、STM32MP157、树莓派等),因为x86虚拟机没有GPIO外设。如果你的开发板不在手边,可以先学习代码逻辑,后续拿到板子再实测。

读完本文你将掌握:

  • 设备树中GPIO节点的理解与修改
  • 使用新一代gpiod API获取和控制GPIO引脚
  • file_operationswrite中根据用户写入内容控制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_initmodule_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  # 设置为心跳闪烁

可用的触发器包括:nonemmc0timerheartbeatdefault-on等。

自己写驱动的意义:虽然LED子系统很方便,但从零编写GPIO驱动能帮助我们更深入地理解设备树、platform总线、gpiod API的底层机制,这些知识在编写更复杂的驱动(如I2C/SPI传感器、自定义外设)时至关重要。


八、常见问题排查

  1. insmodprobe没有被调用

    检查设备树中的compatible字符串是否与驱动中的of_match_table完全一致;确认设备树已正确编译并烧录到开发板。

  2. gpiod_get返回错误

    • 检查设备树中gpios属性的格式是否正确(<&gpio5 3 GPIO_ACTIVE_LOW>之间没有多余逗号)。
    • 确认GPIO引脚没有被其他驱动占用。可通过cat /sys/kernel/debug/gpio查看当前GPIO占用情况。
  3. LED点不亮

    • 检查原理图确认LED的接线极性(共阳还是共阴)。
    • 尝试将GPIOD_OUT_LOW改为GPIOD_OUT_HIGH(或反过来)。
    • 确保没有漏掉GPIOD_OUT_LOW标志,否则GPIO仍为输入模式。
  4. 编译时找不到<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的亮度,实现呼吸灯效果。敬请期待!


如果本文对你有帮助,欢迎点赞、收藏、关注。有任何技术疑问,欢迎在评论区留言交流!

相关推荐
云计算磊哥@1 小时前
运维开发宝典026-MySQL02数据库表操作
运维·数据库·运维开发
weixin_523185321 小时前
Collections.unmodifiableMap详解:真的不可修改吗?
java·linux·前端
天天进步20152 小时前
Tunnelto 源码解析 #9:控制服务器设计:Warp、WebSocket、Ping/Pong 与连接保活
运维·服务器·websocket
凡人叶枫2 小时前
Effective C++ 条款04:确定对象被使用前已先被初始化
java·linux·开发语言·c++·嵌入式开发
云栖梦泽2 小时前
玩转RK3506SDK
linux·嵌入式硬件
极客先躯2 小时前
高级java每日一道面试题-2026年02月01日-实战篇[Docker]-Docker Volume 的生命周期管理是怎样的?
java·运维·docker·容器·持久化·架构图·容器卷
Java面试题总结3 小时前
Linux-Ubantu-贴士-apt的地盘
linux·运维·服务器
志栋智能3 小时前
超自动化巡检:提升MTTR,缩短业务影响时间
运维·自动化
kong@react3 小时前
Rocky Linux 10.2 全面解析:企业级 CentOS 替代方案及保姆级docker安装
java·linux·运维·docker
凡人叶枫4 小时前
Effective C++ 条款07:为多态基类声明 virtual 析构函数
linux·c语言·开发语言·c++