前言在上一篇文章中我们引入了互斥锁mutex保护共享数据让驱动在多进程并发场景下也能稳定运行。但前三篇的驱动都是纯内存虚拟设备没有真正触及硬件即使只有一台x86虚拟机也能完整跑通。从本文开始我们将正式进入嵌入式Linux最核心的领域——硬件控制。我们将利用GPIO子系统点亮开发板上的LED在驱动中实现对物理硬件的实际操作。本文需要一块嵌入式Linux开发板如韦东山老师的i.MX6ULL、STM32MP157、树莓派等因为x86虚拟机没有GPIO外设。如果你的开发板不在手边可以先学习代码逻辑后续拿到板子再实测。读完本文你将掌握设备树中GPIO节点的理解与修改使用新一代gpiodAPI获取和控制GPIO引脚在file_operations的write中根据用户写入内容控制LED亮灭使用platform_driver架构匹配设备树节点自动加载驱动一、GPIO子系统简介1.1 什么是GPIO子系统GPIOGeneral Purpose Input/Output通用输入输出是嵌入式处理器最基本的外设。Linux内核提供了完整的GPIO子系统将各个芯片厂商的GPIO控制器统一抽象为标准的API接口。GPIO子系统分为两个层次GPIO控制器驱动层芯片原厂负责实现硬件寄存器的操作将具体引脚映射为数字引脚号。GPIO消费者接口层驱动开发者使用提供统一的API获取和操作GPIO引脚。1.2 新旧两套API对比Linux内核有两套GPIO操作接口特性旧版API整数描述符新版APIgpiod描述符头文件linux/gpio.hlinux/gpio/consumer.h获取GPIOgpio_request()gpiod_get()设置方向gpio_direction_output()gpiod_direction_output()设置值gpio_set_value()gpiod_set_value()释放GPIOgpio_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等宏。/* 在根节点 / 下添加 */ gpioled { compatible yourname,gpioled; /* 用于与驱动匹配 */ gpios gpio5 3 GPIO_ACTIVE_LOW; /* 使用GPIO5_IO03低电平有效 */ status okay; };关键字段说明compatible驱动中的of_match_table将与此字符串匹配格式通常为厂商,设备名。gpiosgpio5表示使用GPIO5控制器3表示该控制器的第3号引脚GPIO_ACTIVE_LOW表示低电平有效因为板载LED通常一端接VCC另一端通过GPIO拉低来点亮。其他常见开发板树莓派的设备树在arch/arm/boot/dts/broadcom/目录下引脚属性可能命名为gpios gpio 23 0;。请根据实际硬件调整。修改后重新编译设备树并替换makedtbs# 将生成的.dtb文件复制到/boot目录并重启开发板四、驱动代码实现4.1 驱动代码新建文件gpioled_drv.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韦东山课程配套板 */#includelinux/module.h#includelinux/fs.h#includelinux/cdev.h#includelinux/device.h#includelinux/uaccess.h#includelinux/platform_device.h/* platform_driver 相关 */#includelinux/gpio/consumer.h/* gpiod API */#includelinux/of.h/* of_match_table */#defineDEVICE_NAMEgpioled#defineCLASS_NAMEgpioled_classstaticdev_tdev_num;staticstructcdevmy_cdev;staticstructclass*my_class;staticstructdevice*my_device;staticstructgpio_desc*led_gpio;/* GPIO描述符 *//* 打开设备 */staticintgpioled_open(structinode*inode,structfile*file){pr_info(gpioled: device opened\n);return0;}/* 关闭设备 */staticintgpioled_release(structinode*inode,structfile*file){pr_info(gpioled: device closed\n);return0;}/* 写入控制 * 用户写入 0 或 1驱动控制 LED 熄灭/点亮。 */staticssize_tgpioled_write(structfile*file,constchar__user*buf,size_tcount,loff_t*f_pos){charkbuf[2]{0};/* 期望接收 0 或 1 */intvalue;if(count1)count1;/* 只关心第一个字符 */if(copy_from_user(kbuf,buf,count))return-EFAULT;/* 将字符转换为整数值 */if(kbuf[0]1){value1;gpiod_set_value(led_gpio,1);/* 输出高电平 */pr_info(gpioled: LED ON\n);}elseif(kbuf[0]0){value0;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;}returncount;}/* 读取当前LED状态返回0\n或1\n */staticssize_tgpioled_read(structfile*file,char__user*buf,size_tcount,loff_t*f_pos){charkbuf[4];/* 足够容纳 0\n\0 或 1\n\0 */intvalue;intlen;valuegpiod_get_value(led_gpio);/* 读取当前GPIO电平 */lensnprintf(kbuf,sizeof(kbuf),%d\n,value);if(*f_poslen)return0;/* EOF */if(copy_to_user(buf,kbuf,len))return-EFAULT;*f_poslen;returnlen;}staticstructfile_operationsgpioled_fops{.ownerTHIS_MODULE,.opengpioled_open,.releasegpioled_release,.readgpioled_read,.writegpioled_write,};/* ---------------- platform_driver 部分 ---------------- *//* probe设备树节点匹配成功后被调用 */staticintgpioled_probe(structplatform_device*pdev){intret;structdevice*devpdev-dev;pr_info(gpioled: probe called, device matched\n);/* 1. 从设备树获取 GPIO 描述符。 * NULL 表示使用设备树中第一个 gpios 属性指定的 GPIO。 * GPIOD_OUT_LOW 表示初始化为输出模式且初始低电平LED灭。 */led_gpiogpiod_get(dev,NULL,GPIOD_OUT_LOW);if(IS_ERR(led_gpio)){pr_err(gpioled: failed to get gpio descriptor\n);returnPTR_ERR(led_gpio);}pr_info(gpioled: gpio descriptor obtained\n);/* 2. 动态分配设备号 */retalloc_chrdev_region(dev_num,0,1,DEVICE_NAME);if(ret0){pr_err(gpioled: alloc_chrdev_region failed\n);gotoerr_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.ownerTHIS_MODULE;retcdev_add(my_cdev,dev_num,1);if(ret){pr_err(gpioled: cdev_add failed\n);gotoerr_cdev_add;}/* 4. 创建class */my_classclass_create(THIS_MODULE,CLASS_NAME);if(IS_ERR(my_class)){pr_err(gpioled: class_create failed\n);retPTR_ERR(my_class);gotoerr_class_create;}/* 5. 创建设备节点 */my_devicedevice_create(my_class,dev,dev_num,NULL,DEVICE_NAME);if(IS_ERR(my_device)){pr_err(gpioled: device_create failed\n);retPTR_ERR(my_device);gotoerr_device_create;}pr_info(gpioled: /dev/%s created, you can now control LED\n,DEVICE_NAME);return0;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);returnret;}/* remove设备移除时调用 */staticintgpioled_remove(structplatform_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);return0;}/* 设备树匹配表 */staticconststructof_device_idgpioled_of_match[]{{.compatibleyourname,gpioled},/* 必须与设备树中的compatible一致 */{}};MODULE_DEVICE_TABLE(of,gpioled_of_match);/* platform_driver 结构体 */staticstructplatform_drivergpioled_driver{.probegpioled_probe,.removegpioled_remove,.driver{.namegpioled,.ownerTHIS_MODULE,.of_match_tablegpioled_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 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开发板用的驱动时需要设置工具链和架构exportARCHarmexportCROSS_COMPILEarm-linux-gnueabihf-# 根据你的工具链调整make六、测试与验证6.1 确认设备树生效开发板启动后检查设备树节点是否被内核识别ls/proc/device-tree/|grepgpioled# 应该能看到 gpioled 目录6.2 加载驱动将编译好的gpioled_drv.ko拷贝到开发板通过U盘、NFS或scp然后insmod gpioled_drv.ko如果设备树修改正确probe函数会被自动调用。查看内核日志dmesg|tail-n6预期输出gpioled: probe called, device matched gpioled: gpio descriptor obtained gpioled: major238, minor0 gpioled: /dev/gpioled created, you can now control LED6.3 确认设备节点ls-l/dev/gpioled# crw------- 1 root root 238, 0 Jan 1 00:00 /dev/gpioled如果普通用户无权访问赋权chmod666/dev/gpioled6.4 控制LED点亮LEDecho1/dev/gpioled此时开发板上的LED应点亮且内核日志显示gpioled: LED ON。熄灭LEDecho0/dev/gpioledLED熄灭内核日志显示gpioled: LED OFF。6.5 读取LED状态cat/dev/gpioled# 输出当前GPIO逻辑电平1 或 0 后跟换行6.6 卸载驱动rmmod gpioled_drv卸载时LED会自动熄灭/dev/gpioled自动删除。七、LED子系统简介除了自己从头写GPIO驱动Linux内核还提供了专门的LED子系统可以更方便地管理LED设备。对于简单的GPIO控制LED内核已内置通用驱动drivers/leds/leds-gpio.c只需在设备树中配置即可使用无需编写任何C代码。设备树配置示例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/目录下可以通过以下命令控制echo1/sys/class/leds/system-led/brightness# 点亮echo0/sys/class/leds/system-led/brightness# 熄灭echoheartbeat/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的亮度实现呼吸灯效果。敬请期待如果本文对你有帮助欢迎点赞、收藏、关注。有任何技术疑问欢迎在评论区留言交流