资讯动态

嵌入式Linux驱动入门:基于GPIO子系统的LED驱动详解

发布时间:2026/10/14 23:08:00 来源:尧图企业网站定制
说到嵌入式Linux驱动入门“点灯”几乎是所有人绕不开的第一课。我自己当初也是跟着某套很经典的手把手教程从裸机寄存器操作一路写到基于GPIO子系统的LED驱动。这套东西看起来简单但它背后串起来的知识点一点都不少设备树怎么描述硬件、platform总线怎么匹配驱动、GPIO子系统怎么帮你管理引脚、字符设备怎么和用户态交互。把这些理清楚后面再写按键、中断、串口驱动都会顺很多。这篇文章就围绕“基于GPIO子系统的LED驱动程序”这个经典任务展开我把设备树写法、驱动代码骨架、Makefile、调试手段和常见坑一次说清楚。适合刚看完裸机点灯、正准备进入Linux驱动开发的同学参考也适合那些写过杂七杂八驱动、但始终没把GPIO子系统完整走一遍的人查漏补缺。1. 这个驱动为什么值得认真写一遍1.1 从寄存器直操作到子系统管理在单片机和裸机阶段点LED逻辑很简单找到GPIO控制寄存器的地址查数据手册找到对应位往寄存器里写值电平就出来了。无非是“配置方向、设置输出、循环延时”整个工作量很小出了问题翻手册就好。但一旦跑起Linux内核事情就不一样了。系统里几十个驱动同时在跑如果每个驱动都直接去操作物理寄存器必然乱套。你今天把某个引脚当GPIO输出用明天声卡驱动把它当I2C引脚初始化一遍两边就会打架。内核给出的解决方案是资源统一管理把引脚的使用权收归几个子系统驱动要用引脚就去“申请”用完就“释放”。这也是GPIO子系统出现的原因。它给驱动开发者提供了一套统一API比如申请引脚、设置方向、读写电平。驱动不用关心引脚在哪个bank、偏移多少位、寄存器地址是什么只需要给系统一个gpio号剩下的硬件操作由底层芯片驱动完成。换一颗SoC只要子系统适配好了驱动代码一行不用改或者只改设备树。1.2 GPIO子系统和pinctrl子系统一起工作刚开始接触这套东西很多人分不清GPIO子系统和pinctrl子系统以为它们是同一个东西。实际分工很明确。pinctrl负责管引脚本身包括引脚复用、上下拉、驱动能力。比如某颗SoC的一个引脚默认可能是普通GPIO也可以复用成UART的TX。想让这个引脚作为GPIO使用必须先通过pinctrl把它配置成GPIO功能否则它是UART状态你写电平根本没反应。GPIO子系统则负责管电平读写、方向设置和中断。pinctrl配置好的是“这个引脚现在能干什么”GPIO子系统管的是“具体怎么干”。在设备树里体现得更清楚。一个LED节点里既有pinctrl-0属性用来指定引脚的复用配置又有led-gpios属性用来告诉GPIO子系统要用几号引脚、什么极性。1.3 设备树就是驱动的硬件说明书老式内核时代驱动里经常直接写死寄存器地址、中断号、GPIO编号。换一块板子引脚变了就得改驱动代码重新编译维护起来非常痛苦。设备树把这种信息从源码里剥离了出来。它描述“硬件长什么样、引脚怎么连、中断是哪个”驱动通过节点、属性去读取这些资源。同一个驱动只要设备树里属性写得对拿到不同的硬件上都能适配。这也是现在的驱动Why普遍喜欢使用设备树的原因。学习GPIO子系统LED驱动本质上就是学习驱动如何从设备树节点中获取gpio资源然后调用GPIO子系统的API完成操作。这条链路是理解后续很多外设驱动的样板。1.4 学完这套得到的通用骨架别小看这个LED驱动它几乎包含了字符设备驱动的全部要素module_init/module_exit入口platform_driver结构体与设备树compatible匹配probe函数里获取资源、初始化硬件file_operations结构体的open、write、ioctl实现class_create和device_create创建设备节点Makefile的模块编译方式。这个骨架换个马甲再加点中断、定时器、内核线程、设备模型就是按键驱动、PWM驱动、I2C驱动、SPI驱动的雏形。所以这套代码值得反复敲几遍直到能不看源码独立写出来为止。2. 写驱动之前先把这些准备工作做扎实2.1 硬件接线与原理图阅读别一上来就写代码先把硬件搞明白。LED一般是长脚接正极、短脚接负极嵌在开发板上的LED其实已经通过限流电阻接到了某个GPIO上。你需要做的是找到原理图确认LED连接的是哪个GPIO以及LED点亮方式是“GPIO输出高电平点亮”还是“输出低电平点亮”。很多人在这一步就埋了坑。同一个LED你的板子可能是低电平点亮驱动里如果按高电平写灯死活不亮。极性判断错了代码写得再对也没用。还需要确认这个GPIO是否引出了其他用途。有些开发板的引脚同时连接了按键、I2C、音频芯片等外设被别的驱动占用了你申请GPIO时会失败。所以准备阶段最好查一下开发板默认设备树里这个引脚有没有被其他地方使用。2.2 内核源码、设备树和交叉编译环境这套驱动需要编译成内核模块所以本地得有一套与开发板匹配的内核源码。建议直接用板卡厂商提供的内核源码不要自己找一份版本不对的否则编译出来的模块加载很可能直接报错。内核源码需要预先编译通过至少要有编译设备树和模块的基础环境。交叉编译工具链也要配好确认gcc版本和内核源码使用的编译器一致。模块编译好之后怎么拷到开发板上也需要提前准备好。最简单的是通过NFS挂载根文件系统或者用tftp、U盘、scp拷贝根据自己的习惯来。2.3 先确认Linux内核能否启动很多初学者拿到开发板就开始写驱动结果驱动加载失败排查半天发现是内核本身没起来或者rootfs有问题。建议先跑一遍官方提供的出厂系统确认内核、文件系统、网络都正常再着手开发。启动后先执行mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/gpio这个文件会列出当前系统中所有GPIO的占用状态、方向、当前电平。先看清楚目标GPIO是不是已经被占用再决定要不要继续用这个引脚。如果板子默认的GPIO信息太多也可以用下面命令过滤关键内容cat /sys/kernel/debug/gpio | grep -i led2.4 准备一个最简单的验证用户程序驱动写完总要测不用写多复杂的程序一个最简用户态测试工具就够了。直接写一个脚本或者小程序往设备节点里写0或1观察LED对应亮灭变化。用户程序的逻辑越简单越好这样驱动行为异常时你能把问题锁定在驱动侧还是用户侧。3. 设备树节点的写法这一步错了后面全白搭3.1 一个完整的LED设备树节点设备树是驱动对外部世界的唯一认识来源节点写错驱动里怎么补救都有限。下面给出一个典型的LED节点你可以对照自己的平台修改led_test { compatible example,led; pinctrl-names default; pinctrl-0 pinctrl_led_default; led-gpios gpio1 18 GPIO_ACTIVE_LOW; status okay; };这个节点挂在设备树的根下或者某个soc主节点下都可以关键是要能被内核正确解析到。名称不唯一也没关系内核匹配主要看compatible属性。3.2 各属性含义逐个拆解先说compatible。驱动里platform_driver的of_match_table里必须写一个一模一样的字符串两者对上驱动probe才会被调用。这是设备和驱动握手的关键。pinctrl-names和pinctrl-0告诉内核这个设备有一组默认的引脚配置配置内容指向pinctrl_led_default。内核在设备probe之前会主动去应用这组配置把引脚复用成GPIO功能。如果少了这个引脚可能还停留在别的复用状态。led-gpios是真正给GPIO子系统用的属性。它由三个部分组成第一个是GPIO控制器的phandle你可以理解为指向哪个GPIO寄存器组比如gpio1第二个是引脚在该控制器内的编号第三个是极性标志GPIO_ACTIVE_HIGH表示高电平有效GPIO_ACTIVE_LOW表示低电平有效。3.3 pinctrl子节点怎么配pinctrl子节点没有完全通用的写法各芯片厂商定义的格式五花八门。拿主流SoC来说通常是在pinctrl控制器节点下面增加一个子节点内容大致如下pinctrl { pinctrl_led_default: led_default { pinmux LED_GPIO_PINMUX; bias-disable; drive-strength 2; }; };这里的pinmux具体值需要查你所用平台的数据手册。有的平台用宏定义有的平台直接用寄存器地址和掩码。你不用把每种平台记牢重要的是理解这个节点存在的目的是告诉pinctrl子系统把某个引脚配置成GPIO模式并设置上下拉和驱动能力。强烈建议先用厂家提供的设备树模板找到某个已有的pinctrl节点照着改比从零写省力得多。3.4 编译设备树并反向验证设备树文件改好后需要编译成dtb再烧录或替换到开发板的/boot分区。具体命令依赖你的内核构建系统常见做法是在内核源码根目录执行make dtbs如果设备树语法写错了编译会直接报错这也是第一道检查。能编过不代表正确最好反编译看看实际生成的结构dtc -I dtb -O dts -o test.dts test.dtb打开test.dts检查led_test节点是否存在、led-gpios里的phandle和值是否正确。很多情况下设备树在dts里看起来没问题但解析出来的gpio号不对反编译能提前发现问题。加载内核后还可以通过/sys/firmware/devicetree/base目录查看实际生效的设备树。如果能看到led_test节点说明设备树确实被内核正确加载了ls /sys/firmware/devicetree/base/led_test cat /sys/firmware/devicetree/base/led_test/compatible4. 驱动代码骨架照着敲一遍就理解了4.1 让驱动和设备树对上话驱动部分的头尾框架每个模块都差不多关键是module_init、module_exit和platform_driver结构体的注册方式。下面是一个基础模板#include linux/module.h #include linux/platform_device.h #include linux/of_gpio.h #include linux/gpio.h #include linux/fs.h #include linux/uaccess.h #include linux/device.h #include linux/cdev.h #include linux/slab.h static struct platform_driver led_driver; static int led_probe(struct platform_device *pdev) { // 获取设备树资源、初始化LED return 0; } static int led_remove(struct platform_device *pdev) { // 释放资源、关闭LED return 0; } static const struct of_device_id led_of_match[] { { .compatible example,led, }, { }, }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name example_led, .of_match_table led_of_match, }, }; module_platform_driver(led_driver); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(GPIO LED Driver);module_platform_driver宏会依次展开成module_init和module_exit把platform_driver注册进内核。of_match_table里写了compatible内核在遍历设备树时发现compatible匹配就会调用probe。有个细节值得说明platform_driver的driver.name只是一个额外标识符设备树匹配主要依靠of_match_table而不是这个字符串。所以有时候你在别处看到name和compatible完全不一样这完全是正常的。4.2 probe函数获取GPIO并完成初始化probe是驱动的核心它的任务是获取设备树里的资源并使用GPIO子系统API完成LED初始化。这里我写一个完整可参考的版本static int led_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct device_node *np dev-of_node; int ret, gpio; if (!np) { dev_err(dev, no device node\n); return -EINVAL; } gpio of_get_named_gpio(np, led-gpios, 0); if (gpio 0) { dev_err(dev, failed to get gpio: %d\n, gpio); return gpio; } dev_info(dev, gpio%d\n, gpio); if (!gpio_is_valid(gpio)) { dev_err(dev, invalid gpio %d\n, gpio); return -EINVAL; } ret devm_gpio_request(dev, gpio, led); if (ret 0) { dev_err(dev, failed to request gpio %d: %d\n, gpio, ret); return ret; } ret gpio_direction_output(gpio, 0); if (ret 0) { dev_err(dev, failed to set output\n); return ret; } led_gpio gpio; return 0; }逐个解释关键API的作用。of_get_named_gpio从当前设备的device_node中查找led-gpios属性返回一个gpio号。这个gpio号是把GPIO控制器phandle和引脚号换算后的全局编号GPIO子系统内部用它定位硬件。gpio_is_valid用来检查gpio号是否合法防止设备树写错导致后续操作存取越界。devm_gpio_request是“devm”版本带有自动释放功能probe失败或设备移除时不光不用手动释放系统还会自动调用gpio_free。早期驱动使用gpio_request卸载时如果忘记gpio_free再次加载就会申请失败这个经典问题很多人遇到过。所以新代码里优先用devm_前缀API是个好习惯。gpio_direction_output设置引脚方向为输出并给初值。第二个参数0对应低电平1对应高电平。这里别忘了如果LED是低电平点亮给0实际上是点亮给1是熄灭。方向与初始电平一次完成省去单独再调用gpio_set_value。4.3 file_operations把用户操作映射到GPIO驱动最终要面向用户态需要实现操作函数。这里实现最基本的open、write和ioctl还有一个release。static int led_open(struct inode *inode, struct file *filp) { if (led_gpio 0) return -EINVAL; return 0; } static ssize_t led_write(struct file *filp, const char __user *buf, size_t len, loff_t *off) { char value; if (len 1) return -EINVAL; if (copy_from_user(value, buf, 1)) return -EFAULT; if (value 1) gpio_set_value(led_gpio, 1); else gpio_set_value(led_gpio, 0); return len; } static long led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case 0: gpio_set_value(led_gpio, 0); break; case 1: gpio_set_value(led_gpio, 1); break; default: return -EINVAL; } return 0; } static int led_release(struct inode *inode, struct file *filp) { return 0; } static const struct file_operations led_fops { .owner THIS_MODULE, .open led_open, .write led_write, .unlocked_ioctl led_ioctl, .release led_release, };write实现里copy_from_user从用户空间拷贝1个字节然后判断是不是字符1。这已经是最简驱动器能应付的交互方式很直观。ioctl是很多驱动用来收发控制命令的接口。这里我把0和1作为点亮、熄灭的命令方便用户态调用。真正复杂的驱动会定义大量私有命令号但在LED这里保持简单即可。open函数里没有做太多事情只是一次合法性检查。注意LED状态不应该在open时重置否则用户程序刚打开设备就把灯关了体验很怪。4.4 设备节点的创建与销毁只有register_chrdev还不够用户态访问/dev/led_drv需要设备节点。最常用的方式是利用class_create和device_create让udev或mdev自动创建设备节点。static dev_t led_dev_num; static struct class *led_class; static struct device *led_device; static int __init led_init(void) { int ret; ret alloc_chrdev_region(led_dev_num, 0, 1, led_drv); if (ret 0) return ret; cdev_init(led_cdev, led_fops); ret cdev_add(led_cdev, led_dev_num, 1); if (ret 0) goto err_cdev; led_class class_create(THIS_MODULE, led_class); if (IS_ERR(led_class)) { ret PTR_ERR(led_class); goto err_class; } led_device device_create(led_class, NULL, led_dev_num, NULL, led_dev); if (IS_ERR(led_device)) { ret PTR_ERR(led_device); goto err_device; } return 0; err_device: class_destroy(led_class); err_class: cdev_del(led_cdev); err_cdev: unregister_chrdev_region(led_dev_num, 1); return ret; } static void __exit led_exit(void) { device_destroy(led_class, led_dev_num); class_destroy(led_class); cdev_del(led_cdev); unregister_chrdev_region(led_dev_num, 1); } module_init(led_init); module_exit(led_exit);alloc_chrdev_region分配动态主设备号主设备号由内核分配避免手动指定时和其他设备冲突。cdev_add把字符设备注册进内核之后用户open时就能找到我们实现的file_operations。class_create之后在sysfs里会生成/sys/class/led_class目录。device_create则会在该class下创建一个叫led_dev的设备udev/mdev检测到后自动生成/dev/led_dev。这也是为什么很多教程里强调必须做class_create和device_create否则/dev下没有设备节点用户态根本打不开文件。4.5 Makefile写法与常见编译问题内核模块的编译方式不同于普通应用程序要用内核的kbuild系统。一个标准的Makefile长这样obj-m : led_drv.o KDIR : /home/xxx/linux-kernel PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) cleanobj-m表示编译成外部模块。KDIR是内核源码绝对路径建议使用与开发板运行内核完全对应的源码目录。编译时不要用sudo普通用户权限就够了除非内核源码目录本身权限受限。编译输出是led_drv.ko用file命令看一下架构对不对file led_drv.ko如果交叉编译环境没配对这里会显示x86而不是arm加载时必然报错。4.6 加载、测试和卸载的基础操作把led_drv.ko拷贝到开发板后按顺序执行insmod led_drv.ko dmesg | tail ls -l /dev/led_dev echo 1 /dev/led_dev echo 0 /dev/led_dev rmmod led_drvinsmod加载后dmesg能看到probe里的dev_info输出包括gpio号。ls确认设备节点存在。echo写1和写0切换LED状态。如果echo的时候提示Operation not permitted多半是设备节点权限问题可以直接chmod 666或者用root用户测试。驱动开发阶段建议直接root操作别让权限问题干扰调试。卸载前先确认没有进程占用设备节点否则rmmod会提示Device or resource busy。可以用fuser命令查看占用进程。5. 实际开发中必然遇到的坑我一个个踩给你看5.1 probe没有被调用最常见的低级失误驱动模块加载成功insmod没有报错但dmesg里完全没有probe的打印。这种情况90%是compatible字符串不匹配。检查驱动里的of_match_table和实际设备树里compatible属性是不是一字不差。常见错误包括多打了个空格、大小写不统一、忘了加引号。比较稳妥的办法是在开发板上直接读设备树节点的compatible然后写进驱动。另一个原因是设备树虽然改了但内核启动时用的不是新dtb。很多开发板启动用的是boot分区里的dtb而不是你编辑的那个dts。改了dts但没重新生成dtb或者生成了dtb但没有替换到正确位置都会导致probe不跑。排查手段就是前面说的查看/sys/firmware/devicetree/base下节点是否存在。5.2 设备树里明明写着gpio驱动读出来却是负数of_get_named_gpio返回负数常见原因是属性名拼写错误。比如设备树里写的是led-gpio驱动里面写led-gpios两者没对上就会返回-ENOENT。还有一类原因GPIO控制器的phandle写错了。比如节点里写成gpio1但这个平台根本没有gpio1这个节点或者实际引脚在gpio0上解析自然失败。这类问题可以通过反编译dtb来快速确认phandle指向是否正确。还有一点容易忽略如果设备树里led-gpios属性所在的节点本身没有处于enabled状态内核可能根本不会解析这个节点。常见做法是显式加上status okay有些平台默认是disabled忘记了就完了。5.3 gpio_request失败引脚被谁抢走了devm_gpio_request失败时dmesg会打印“failed to request gpio X”。此时打开/sys/kernel/debug/gpio看看这个gpio的占用者是谁。我遇到过一次某颗SoC自带音频编解码芯片默认占用了大量GPIO其中恰好包含想用的LED引脚。这种情况下要么换一个空闲引脚要么在内核配置里关掉不用外设。驱动层面没有太好的解决办法毕竟引脚管理由内核全局统筹单个驱动无权抢占别的设备正在使用的资源。还有一种情况是同一个gpio被同一个驱动重复request。比如模块卸载不彻底又重新加载或者probe被调用两次。使用devm_gpio_request后这类问题少了很多因为系统会自动释放但如果你同时用了gpio_request又没释放地址很容易踩雷。5.4 流程都正常灯就是不亮有时候dmesg里gpio号正确gpio_request也成功了echo写值也不报错但灯纹丝不动。这种问题最费时间往往是几个地方同时出问题。先排查极性。用万用表量一下GPIO引脚电平echo 1时是高还是低。如果LED是低电平点亮echo 1对应熄灭echo 0才对应点亮逻辑写反了。再审pinctrl配置。如果引脚还被复用成其他功能输出电平由其他外设控制GPIO子系统设置的输出根本不起作用。偏置也可能干扰比如引脚被外部电路拉死GPIO输出的电平被强行拉低。这时要重新检查pinctrl子节点里bias-disable、bias-pull-up这些配置。最后查硬件连接。某次我发现开发板上的LED不亮是因为焊接的LED引脚松了或者限流电阻虚焊跟驱动半毛钱关系没有。硬件问题往往要借助万用表来确认光看代码没有用。5.5 模块卸载后再次加载设备节点起冲突只执行rmmod然后立刻insmod第二次加载时dmesg可能提示Device or resource busy或者看不到/dev/led_dev节点。这类现象多由资源释放不完整导致。比如手动request的gpio没有释放字符设备cdev没删除class和device没有销毁。使用devm_后gpio释放问题缓解但cdev和class的清理必须自己保证。尤其要注意cdev_del必须在unregister_chrdev_region之前调用顺序反了会留下僵尸设备号和僵尸节点。如果遇到设备节点残留的情况先手动清理rm -f /dev/led_dev然后再重新加载驱动。当然这只是暂时止血根本办法还是把module_exit里的释放逻辑捋清楚。5.6 设备节点没自动创建权限还跟着捣乱insmod成功后/dev/led_dev并不存在原因通常是class_create或device_create失败。dmesg里会有明确报错检查一下是不是class名字被其他驱动占用了换个类名重新试。设备节点只要存在但不属于当前用户普通用户echo就会报Permission denied。学习阶段最简单的是chmod 666 /dev/led_dev或者用root跑测试程序。正式产品里可以用udev规则给设备指定用户和权限组避免直接开放所有权限。5.7 调试技巧多打日志但别打满屏驱动开发调试主要靠printk/dev_info。probe开始、成功获取gpio、成功request、成功配置方向每一个关键节点都可以打印一条信息。这样出错时dmesg能直接定位到失败在哪一步。不过打印太多会影响性能调试完成后记得删除无用日志。可以用dev_dbg替代dev_info然后通过内核动态调试机制控制输出这样平时不打印、需要时再打开比改源码重新编译友好得多。最后说点实际的体会这个驱动我自己至少写了五遍。第一遍照着教程抄很多API看完就忘第二遍试图不看教程自己写结果卡在device_create的参数上第三遍开始理解为什么用devm、为什么compatible不匹配不probe到第五遍的时候我基本能闭着眼睛把整个骨架默写出来。我觉得这个项目真正的价值不在于点亮一颗LED而在于它把Linux驱动开发里“设备树解析、总线匹配、字符设备、内核资源管理”这些核心概念串成了一条完整的链路。走通一次再去看I2C驱动、SPI驱动、中断子系统都会有一种似曾相识的感觉。如果你正在学这套东西我的建议是别看懂了就急着过找一块板子从设备树写起到用户态点灯完整走一遍中途遇到问题解决问题。踩过一遍坑比看过十遍教程都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑