树莓派Linux字符设备驱动开发:从零实现GPIO控制
发布时间:2026/8/26 12:38:56 作者:尧图编辑部 阅读量:1,286

1. 项目概述从应用层到驱动层的IO控制之旅在嵌入式Linux开发里控制一个GPIO通用输入输出口听起来是基础操作但很多朋友尤其是从单片机转过来的开发者第一次接触时会感到困惑为什么不能像在STM32上用HAL库那样直接写个HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET)就完事了在Linux世界里你想点个灯或者读个按键得先经过一套完整的“仪式”——从用户空间的应用层程序通过系统调用穿越内核空间最终到达硬件寄存器。这个过程就是Linux设备驱动框架的缩影。今天我们就以树莓派Raspberry Pi这个极佳的硬件平台为例手把手从零开始构建一个完整的字符设备驱动来实现对单个GPIO口的控制。这不仅仅是点亮一个LED更是理解Linux内核驱动开发精髓的绝佳入口。无论你是想为自定义硬件编写驱动还是单纯想深入理解操作系统如何与硬件对话这个“麻雀虽小五脏俱全”的项目都将为你铺平道路。2. 核心思路与驱动框架选型2.1 为什么需要驱动框架在裸机或RTOS环境下程序对硬件拥有绝对控制权可以直接操作内存映射的寄存器。但在像Linux这样的宏内核操作系统中为了系统的稳定性、安全性和多任务管理用户态应用程序被禁止直接访问硬件。内核充当了硬件的“管家”和“保镖”。驱动就是内核中专门与特定硬件对话的“翻译官”和“执行者”。它运行在内核态拥有硬件访问权限并为用户空间提供一套标准化的访问接口通常是文件操作接口。当我们选择/dev/gpio_demo这个设备文件并通过open、write、read、ioctl等系统调用与之交互时实际上是在调用内核中我们编写的驱动函数。2.2 字符设备驱动我们的选择Linux设备驱动主要分为三类字符设备、块设备和网络设备。GPIO控制本质上是一种“流式”的、按字节访问的操作没有固定的块大小也不需要复杂的缓存机制这完美契合字符设备的特点。字符设备驱动是Linux驱动中最基础、最直观的模型它通过一个主设备号Major Number来标识在/dev目录下对应一个设备节点。我们的目标就是创建一个字符设备让用户程序像读写普通文件一样通过向这个设备文件写入‘1’或‘0’来控制GPIO输出高低电平或者通过读取它来获取输入状态。2.3 平台与工具链准备我们选择树莓派4B作为硬件平台它不仅普及度高其Broadcom BCM2711芯片的GPIO控制器也已被内核良好支持。这意味着我们不需要从零编写最底层的GPIO控制器驱动那属于平台设备驱动或设备树插件范畴而是可以基于内核现有的GPIO子系统gpiolib来访问具体的GPIO引脚这让我们能更专注于驱动框架本身。你需要准备硬件树莓派4B一块LED和电阻或一个按键用于测试。系统在树莓派上安装一个Linux发行版如Raspberry Pi OS基于Debian。开发环境在树莓派本地或通过SSH远程登录进行开发。确保已安装内核头文件这对于编译驱动至关重要。sudo apt update sudo apt install raspberrypi-kernel-headers build-essential连接假设我们使用物理引脚GPIO 17即BCM编码的17号引脚对应板子上的引脚11来控制LED阳极串联一个220Ω电阻到3.3V阴极接地。注意内核驱动开发涉及内核空间错误的代码可能导致系统崩溃内核恐慌。建议先在虚拟机或备用设备上练习或者确保你的工作已保存。编写驱动时printk是你的好朋友但它输出到内核日志dmesg而非终端。3. 驱动模块的骨架初始化与退出一个最简单的Linux内核模块就像一段可以动态插入和移除的内核代码。我们的驱动将以模块的形式存在。3.1 模块的入口与出口创建一个文件例如gpio_demo_driver.c。每个模块都必须有两个函数初始化函数和清理函数。#include linux/init.h #include linux/module.h #include linux/kernel.h // 模块的初始化函数在insmod时被调用 static int __init gpio_demo_init(void) { printk(KERN_INFO GPIO Demo Driver: Initialization started.\n); // 后续所有的初始化工作都将在这里添加 // 例如申请设备号、创建设备节点、初始化GPIO、申请内存等。 printk(KERN_INFO GPIO Demo Driver: Initialization finished successfully.\n); return 0; // 返回0表示成功 } // 模块的清理函数在rmmod时被调用 static void __exit gpio_demo_exit(void) { printk(KERN_INFO GPIO Demo Driver: Exiting and cleaning up.\n); // 在这里释放所有在init中申请的资源 // 例如释放设备号、删除设备节点、释放GPIO、释放内存等。 } // 这两行宏告诉内核哪个是初始化函数哪个是清理函数。 module_init(gpio_demo_init); module_exit(gpio_demo_exit); // 模块的元信息 MODULE_LICENSE(GPL); // 声明模块采用GPL许可证这是必须的 MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple character device driver for GPIO control); MODULE_VERSION(0.1);这个骨架现在什么具体功能都没有但它已经是一个合法的内核模块。我们可以编写一个简单的Makefile来编译它。3.2 编写驱动模块的MakefileMakefile告诉make工具如何构建我们的模块。关键点是使用内核的构建系统kbuild。# 指定模块名称最终会生成 gpio_demo_driver.ko obj-m : gpio_demo_driver.o # 获取当前运行的内核版本和构建目录 KERNELDIR ? /lib/modules/$(shell uname -r)/build # 指定当前模块源码所在目录 PWD : $(shell pwd) # 默认构建目标 all: $(MAKE) -C $(KERNELDIR) M$(PWD) modules # 清理构建产物 clean: $(MAKE) -C $(KERNELDIR) M$(PWD) clean现在在终端执行make命令如果一切顺利你会看到生成了gpio_demo_driver.ko文件。使用sudo insmod gpio_demo_driver.ko加载模块再用dmesg | tail查看内核日志应该能看到我们打印的初始化信息。使用sudo rmmod gpio_demo_driver卸载模块同样能在日志中看到退出信息。实操心得printk的日志级别如KERN_INFO很重要。默认情况下只有KERN_ERR或更紧急的消息会打印到控制台。dmesg命令可以查看所有级别的内核日志。在驱动开发初期多用printk进行调试但产品化时应减少或移除非必要的调试信息以提升性能。4. 构建字符设备创建设备节点有了模块骨架接下来要创建真正的字符设备使得用户空间程序能够找到并打开它。4.1 设备号主设备号与次设备号内核通过设备号来唯一标识一个设备。设备号由主设备号标识设备类型如所有GPIO demo驱动和次设备号标识同类设备中的具体实例组成。我们可以静态指定一个未被使用的主设备号或者让内核动态分配。动态分配更安全避免冲突。我们需要一个结构体dev_t来保存设备号以及一个struct cdev结构体来表示字符设备本身。#include linux/fs.h // 包含 file_operations 结构体和设备号相关函数 #include linux/cdev.h static dev_t dev_num; // 存储动态分配的设备号 static struct cdev gpio_cdev; // 字符设备结构体 static struct class *dev_class; // 设备类用于在/sys/class创建条目并自动生成/dev节点4.2 初始化流程扩充现在我们来大幅扩充gpio_demo_init函数。static int __init gpio_demo_init(void) { int ret; printk(KERN_INFO GPIO Demo Driver: Initialization started.\n); // 1. 动态申请一个字符设备号主次 ret alloc_chrdev_region(dev_num, 0, 1, gpio_demo); if (ret 0) { printk(KERN_ERR Failed to allocate device number.\n); return ret; } printk(KERN_INFO Allocated major number %d, minor number %d.\n, MAJOR(dev_num), MINOR(dev_num)); // 2. 初始化cdev结构体并将其与我们的文件操作函数集合关联 cdev_init(gpio_cdev, gpio_demo_fops); // gpio_demo_fops 我们稍后定义 gpio_cdev.owner THIS_MODULE; // 3. 将cdev添加到内核系统中 ret cdev_add(gpio_cdev, dev_num, 1); if (ret 0) { printk(KERN_ERR Failed to add cdev to system.\n); goto err_cdev_add; } // 4. 创建设备类会在/sys/class下出现 dev_class class_create(THIS_MODULE, gpio_demo_class); if (IS_ERR(dev_class)) { printk(KERN_ERR Failed to create device class.\n); ret PTR_ERR(dev_class); goto err_class_create; } // 5. 在/dev目录下创建设备节点 // 这一步会自动根据设备类和设备号在/dev下生成 gpio_demo 设备文件 device_create(dev_class, NULL, dev_num, NULL, gpio_demo); if (IS_ERR(device_create(dev_class, NULL, dev_num, NULL, gpio_demo))) { printk(KERN_ERR Failed to create device node.\n); ret PTR_ERR(device_create(dev_class, NULL, dev_num, NULL, gpio_demo)); goto err_device_create; } printk(KERN_INFO GPIO Demo Driver: Device node /dev/gpio_demo created.\n); printk(KERN_INFO GPIO Demo Driver: Initialization finished successfully.\n); return 0; // 错误处理使用goto进行资源的反向释放是内核代码的常见做法 err_device_create: class_destroy(dev_class); err_class_create: cdev_del(gpio_cdev); err_cdev_add: unregister_chrdev_region(dev_num, 1); return ret; }对应的清理函数gpio_demo_exit需要按相反顺序释放所有资源static void __exit gpio_demo_exit(void) { printk(KERN_INFO GPIO Demo Driver: Exiting and cleaning up.\n); // 1. 销毁设备节点 device_destroy(dev_class, dev_num); // 2. 销毁设备类 class_destroy(dev_class); // 3. 从系统中删除cdev cdev_del(gpio_cdev); // 4. 释放设备号 unregister_chrdev_region(dev_num, 1); printk(KERN_INFO GPIO Demo Driver: Cleanup completed.\n); }4.3 定义文件操作函数集上面代码中引用的gpio_demo_fops是一个struct file_operations类型的结构体。它定义了当用户空间程序对我们的设备文件执行open、read、write、close等操作时内核应该调用哪个函数来处理。这是驱动逻辑的核心。我们先定义一个骨架后续再填充具体函数。static struct file_operations gpio_demo_fops { .owner THIS_MODULE, // 防止模块在使用中被卸载 .open gpio_demo_open, .release gpio_demo_release, .read gpio_demo_read, .write gpio_demo_write, // 我们暂时不使用 .unlocked_ioctl用 write/read 实现简单控制 };现在编译加载模块执行ls -l /dev/gpio_demo你应该能看到这个设备文件已经创建其主设备号就是动态分配的那个。不过现在任何操作都会因为函数未实现而失败或者调用默认的空操作。5. 集成GPIO子系统控制硬件引脚Linux内核提供了强大的GPIO子系统gpiolib它抽象了不同芯片厂商的GPIO控制器细节提供统一的API。我们要在驱动中请求并使用一个GPIO。5.1 包含头文件与定义GPIO#include linux/gpio/consumer.h // 推荐使用新的GPIO描述符API #include linux/gpio.h // 包含旧API我们可能用到一些宏 // 假设我们要控制的GPIO是BCM编号的17树莓派引脚11 #define DEMO_GPIO_NUM 17 static struct gpio_desc *demo_gpio_desc NULL; // GPIO描述符指针5.2 在初始化函数中申请和配置GPIO在gpio_demo_init函数中在创建设备节点之前添加GPIO初始化代码。// ... 设备号申请、cdev初始化之后 ... // 申请GPIO描述符并设置为输出模式初始输出低电平 demo_gpio_desc gpiod_get_index(NULL, NULL, 0, GPIOD_OUT_LOW); // 注意gpiod_get_index的第三个参数是索引对于从设备树获取GPIO很有用。 // 这里我们使用简化方法但更规范的做法是通过设备树指定GPIO。 // 另一种传统方法是使用gpio_request和gpio_direction_output。 if (IS_ERR(demo_gpio_desc)) { printk(KERN_ERR Failed to get GPIO %d.\n, DEMO_GPIO_NUM); ret PTR_ERR(demo_gpio_desc); // 回退到传统API试试需要先export GPIO printk(KERN_INFO Trying legacy GPIO API...\n); ret gpio_request(DEMO_GPIO_NUM, gpio_demo); if (ret) { printk(KERN_ERR Legacy GPIO request also failed: %d\n, ret); goto err_gpio; } ret gpio_direction_output(DEMO_GPIO_NUM, 0); if (ret) { printk(KERN_ERR Failed to set GPIO direction.\n); gpio_free(DEMO_GPIO_NUM); goto err_gpio; } demo_gpio_desc NULL; // 标记我们使用的是传统API printk(KERN_INFO GPIO %d requested and set as output (legacy).\n, DEMO_GPIO_NUM); } else { printk(KERN_INFO GPIO requested and set as output (descriptor API).\n); } // ... 继续创建设备类和节点 ...在错误处理部分goto标签后需要添加GPIO释放的逻辑err_gpio: if (!IS_ERR_OR_NULL(demo_gpio_desc)) { gpiod_put(demo_gpio_desc); } else if (demo_gpio_desc NULL) { // 使用了传统API gpio_free(DEMO_GPIO_NUM); } // ... 继续其他资源的释放 ...同样在gpio_demo_exit函数中也需要释放GPIO// 释放GPIO if (!IS_ERR_OR_NULL(demo_gpio_desc)) { gpiod_put(demo_gpio_desc); } else { gpio_free(DEMO_GPIO_NUM); // 释放传统API申请的GPIO }5.3 实现文件操作函数现在我们可以填充gpio_demo_opengpio_demo_releasegpio_demo_readgpio_demo_write这些函数了。它们的原型可以在linux/fs.h中查到。static int gpio_demo_open(struct inode *inode, struct file *filp) { printk(KERN_INFO GPIO Demo: Device opened.\n); // 这里可以增加打开计数检查防止多次打开冲突但简单演示我们直接返回成功 return 0; } static int gpio_demo_release(struct inode *inode, struct file *filp) { printk(KERN_INFO GPIO Demo: Device closed.\n); return 0; } static ssize_t gpio_demo_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { int value; char kbuf[2]; // 存储0或1以及换行符 // 1. 获取GPIO当前电平值 if (!IS_ERR_OR_NULL(demo_gpio_desc)) { value gpiod_get_value(demo_gpio_desc); } else { value gpio_get_value(DEMO_GPIO_NUM); } // 2. 将值转换为字符 kbuf[0] (value ? 1 : 0); kbuf[1] \n; // 添加换行方便显示 // 3. 将数据从内核空间拷贝到用户空间缓冲区 // copy_to_user 返回未能拷贝的字节数成功时返回0 if (copy_to_user(buf, kbuf, 2)) { return -EFAULT; // 拷贝失败返回错误码 } // 4. 更新文件位置指针虽然对字符设备通常不重要 *f_pos 2; return 2; // 返回成功读取的字节数 } static ssize_t gpio_demo_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { char kbuf[2]; int value; // 1. 安全检查用户缓冲区不能为空且我们只处理少量数据 if (count 0) { return 0; } if (count 2) { // 我们只期望一个字符‘0’或‘1’加可能的换行 count 2; } // 2. 将数据从用户空间拷贝到内核空间 if (copy_from_user(kbuf, buf, count)) { return -EFAULT; } // 3. 解析用户输入 if (kbuf[0] 1) { value 1; } else if (kbuf[0] 0) { value 0; } else { printk(KERN_WARNING GPIO Demo: Invalid input %c, expect 0 or 1.\n, kbuf[0]); return -EINVAL; // 无效参数错误 } // 4. 设置GPIO电平 if (!IS_ERR_OR_NULL(demo_gpio_desc)) { gpiod_set_value(demo_gpio_desc, value); } else { gpio_set_value(DEMO_GPIO_NUM, value); } printk(KERN_INFO GPIO Demo: Set GPIO to %d.\n, value); return count; // 返回成功写入的字节数 }注意事项内核空间和用户空间的内存是隔离的。绝对不能直接用指针访问用户空间的数据如buf必须使用copy_from_user和copy_to_user这类函数在内核提供的临时缓冲区之间进行拷贝。直接访问会导致内核崩溃或安全漏洞。6. 编写用户空间测试程序驱动写好了我们需要一个用户程序来验证它。创建一个test_gpio.c文件。#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include string.h int main() { int fd; char write_buf[2]; char read_buf[2]; ssize_t ret; // 1. 打开设备文件 fd open(/dev/gpio_demo, O_RDWR); if (fd 0) { perror(Failed to open device); exit(EXIT_FAILURE); } printf(Device opened successfully.\n); // 2. 测试写操作点亮LED假设高电平点亮 write_buf[0] 1; write_buf[1] \n; ret write(fd, write_buf, 2); if (ret 0) { perror(Write failed); close(fd); exit(EXIT_FAILURE); } printf(Wrote 1 to device. LED should be ON.\n); sleep(2); // 等待2秒 // 3. 测试读操作读取当前GPIO状态 ret read(fd, read_buf, sizeof(read_buf)); if (ret 0) { perror(Read failed); close(fd); exit(EXIT_FAILURE); } read_buf[ret] \0; // 确保字符串终止 printf(Read from device: GPIO state is %s, read_buf); // 4. 测试写操作熄灭LED write_buf[0] 0; ret write(fd, write_buf, 1); // 只写一个字符也可以 if (ret 0) { perror(Write failed); close(fd); exit(EXIT_FAILURE); } printf(Wrote 0 to device. LED should be OFF.\n); sleep(2); // 5. 再次读取状态 lseek(fd, 0, SEEK_SET); // 重置文件指针对于简单驱动read函数可能不依赖f_pos ret read(fd, read_buf, sizeof(read_buf)); if (ret 0) { perror(Read failed); } else { read_buf[ret] \0; printf(Read from device: GPIO state is %s, read_buf); } // 6. 关闭设备 close(fd); printf(Test finished.\n); return 0; }编译测试程序gcc -o test_gpio test_gpio.c7. 完整测试流程与问题排查现在让我们进行端到端的测试并记录可能遇到的问题。7.1 测试步骤编译驱动在驱动源码目录执行make。确保没有错误。加载驱动模块sudo insmod gpio_demo_driver.ko检查内核日志dmesg | tail -20。你应该看到驱动初始化成功并打印出分配的主设备号以及GPIO申请成功的信息。检查设备节点ls -l /dev/gpio_demo。确认设备文件已创建并注意其主设备号是否与日志一致。权限通常是crw-------只有root可读写。我们可以修改权限或使用sudo运行测试程序。运行测试程序sudo ./test_gpio。观察终端输出并同时观察连接到GPIO 17的LED是否先亮2秒再熄灭2秒。检查内核日志再次运行dmesg | tail -10应该能看到驱动打印的“Device opened”、“Set GPIO to 1/0”等信息。卸载驱动sudo rmmod gpio_demo_driver。再次查看dmesg确认清理信息。/dev/gpio_demo文件应该自动消失。7.2 常见问题与排查技巧即使代码逻辑正确在实际操作中也可能遇到各种问题。下面是一个常见问题速查表问题现象可能原因排查步骤与解决方案insmod失败提示Invalid module format模块编译所用的内核版本与当前运行内核版本不匹配。1. 检查uname -r与/lib/modules/下的目录名是否一致。2. 确保已安装raspberrypi-kernel-headers且版本匹配。3. 在树莓派上直接编译驱动避免交叉编译环境配置错误。insmod失败提示Unknown symbol驱动引用了未导出的内核符号或者依赖的其他模块未加载。1. 使用modinfo gpio_demo_driver.ko查看依赖。2. 我们的驱动依赖GPIO子系统但它是内核核心部分通常已内置。如果使用gpiod_get等较新API确保内核配置了CONFIG_GPIOLIB和CONFIG_OF_GPIO树莓派默认已开启。/dev/gpio_demo设备文件未创建1.device_create失败。2.udev规则问题较新系统。3. 驱动初始化中途出错未执行到创建设备节点那一步。1. 仔细查看dmesg日志看是否有相关错误。2. 检查/sys/class/gpio_demo_class/目录是否存在。如果存在但/dev下没有可能是权限问题可以手动创建sudo mknod /dev/gpio_demo c [主设备号] [次设备号]。3. 确保驱动初始化函数成功返回0。测试程序open失败提示Permission denied/dev/gpio_demo设备文件默认权限为600root读写。1. 使用sudo运行测试程序。2. 或者修改设备文件权限sudo chmod 666 /dev/gpio_demo不安全仅用于测试。3. 更正规的做法是在驱动中或通过udev规则设置默认权限。测试程序write或read失败返回-11. 驱动中的copy_from/to_user失败EFAULT。2. 驱动文件操作函数返回错误码。3. GPIO申请失败导致操作无效指针。1. 查看dmesg驱动通常会打印错误信息。2. 检查测试程序传入的缓冲区地址和大小。3. 确认GPIO引脚编号正确且未被系统其他驱动如LED触发器、串口等占用。可以通过cat /sys/kernel/debug/gpio查看GPIO使用情况。LED无反应但程序运行和驱动日志正常1. 硬件连接错误引脚接错、LED极性接反、电阻过大。2. GPIO模式设置错误应为输出。3. 树莓派引脚编号混淆BCM编号 vs 物理引脚编号 vs WiringPi编号。1. 用万用表测量GPIO引脚电压写‘1’时应为~3.3V写‘0’时为~0V。2. 在驱动初始化后加载前可以尝试通过sysfs手动控制GPIO验证硬件echo 17 /sys/class/gpio/exportecho out /sys/class/gpio/gpio17/directionecho 1 /sys/class/gpio/gpio17/value。卸载模块失败提示Module in use设备文件仍被某个进程打开着。1. 使用sudo lsof /dev/gpio_demo查看是哪个进程打开了设备。2. 关闭测试程序或那个进程。3. 在驱动中实现open计数是更健壮的做法确保所有文件关闭后才允许卸载。实操心得内核驱动调试printk是生命线。但要注意打印频率过于频繁的打印可能淹没日志或影响性能。可以使用pr_debug或dev_dbg配合动态调试DYNAMIC_DEBUG在需要时才开启。另外strace工具可以跟踪测试程序的系统调用帮助你确认是用户程序问题还是驱动内部问题。8. 进阶优化与扩展思路一个最简单的驱动已经完成但工业级的驱动需要考虑更多。这里提供几个优化和扩展方向8.1 增加互斥锁保护我们的驱动假设同一时间只有一个进程打开设备。如果多个进程同时write可能会产生竞态条件。需要添加互斥锁mutex来保护共享资源主要是GPIO操作。#include linux/mutex.h static DEFINE_MUTEX(gpio_demo_mutex); // 定义并初始化一个互斥锁 static int gpio_demo_open(struct inode *inode, struct file *filp) { // 尝试获取锁如果设备忙则返回 -EBUSY if (!mutex_trylock(gpio_demo_mutex)) { printk(KERN_WARNING Device is busy.\n); return -EBUSY; } // ... 其他初始化 ... return 0; } static int gpio_demo_release(struct inode *inode, struct file *filp) { // 释放锁 mutex_unlock(gpio_demo_mutex); // ... 其他清理 ... return 0; } // 在 read/write 函数中如果操作涉及共享状态也需要用 mutex_lock/unlock 保护。8.2 使用ioctl实现更复杂的控制read/write适合简单的数据流。对于像“设置GPIO方向”、“配置上拉下拉”、“同时读写多个GPIO”等复杂命令使用ioctl更合适。你需要定义自己的命令码通常使用_IO_IOR_IOW_IOWR宏并在文件操作中实现.unlocked_ioctl函数。8.3 通过设备树Device Tree指定GPIO硬编码GPIO引脚#define DEMO_GPIO_NUM 17是不灵活且不推荐的做法。正规的驱动应该从设备树DT中获取硬件资源。这需要在设备树源文件.dts或.dtsi中为你的驱动节点添加一个属性例如demo-gpios gpio 17 GPIO_ACTIVE_HIGH;然后在驱动中使用gpiod_get从平台设备struct platform_device中获取这个描述符。这使你的驱动与硬件配置解耦同一个驱动可以用于不同板卡上不同的GPIO引脚。8.4 支持多个设备实例当前的驱动只控制一个GPIO。你可以通过次设备号来区分多个实例。在init函数中可以申请多个次设备号alloc_chrdev_region(dev_num, 0, COUNT, gpio_demo)然后为每个实例创建独立的cdev、设备节点和GPIO资源。用户打开/dev/gpio_demo0/dev/gpio_demo1等可以控制不同的引脚。8.5 添加sysfs接口除了字符设备文件你也可以通过sysfs/sys/class/...暴露一些简单的属性比如当前电平状态、方向等方便shell脚本直接操作。这可以通过device_create_file或sysfs_create_group来实现。从在用户空间懵懂地敲下echo 1 /sys/class/gpio/gpio17/value到在内核空间亲手构建起一个完整的字符设备驱动并让用户程序通过标准的文件接口与之交互这个过程是对Linux“一切皆文件”哲学的一次深刻实践。驱动开发就像是在内核的国度里建造一座桥梁桥的一端是抽象、安全的用户世界另一端是具体、危险的硬件世界。这座桥必须坚固稳定、守规矩遵循内核框架、并且有明确的通行规则文件操作。虽然我们这次只造了一座控制单颗LED的小桥但其中涉及的设备号管理、cdev注册、文件操作集实现、内核/用户空间数据拷贝、GPIO子系统API使用、以及资源申请与释放的严谨流程是所有复杂驱动共同的基石。下次当你再使用一个外设时不妨想想它的驱动里是不是也有一套类似的open、read、write在默默工作呢