ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从零编写Linux GPIO LED驱动:设备树与字符设备全链路实战

从零编写Linux GPIO LED驱动:设备树与字符设备全链路实战 如果你正卡在Linux GPIO LED灯驱动这类入门项目的起点又想把Linux设备驱动开发的完整链路一次性跑通那我强烈建议你先不要碰复杂的网卡、USB这类外设找一块最简单的开发板把一颗LED点起来就够了。这颗LED虽然小但它背后牵扯到设备树、平台驱动、字符设备、GPIO子系统、应用层交互、模块编译加载、系统调试这些几乎覆盖了Linux设备驱动开发里逃不掉的所有核心概念。很多朋友学驱动上来直接啃《Linux设备驱动程序》第三版结果卡在“模块能加载但内核到底怎么和设备绑定”这个坎上。从一颗LED驱动入手恰恰能把这个坎直接迈过去。我写这篇内容的出发点是把从零开始、在真实开发板上把一个GPIO LED驱动完整跑通的整个过程连同踩过的坑一起整理出来。它适合刚接触嵌入式Linux、分不清设备树和驱动代码关系、或者虽然能insmod但完全不知道下一步该干什么的朋友。1. 为什么我把LED驱动当作Linux驱动开发的第一课LED点灯看起来是个“Hello World”但它在驱动开发里的地位远比Hello World高。因为Hello World只验证了内核模块能编译、能加载而一颗LED的驱动从头到尾验证的是一整条设备驱动开发链路。1.1 一个LED点灯里藏着的驱动全链路先别急着写代码我们把“点亮一颗LED”这件事拆开看看它到底需要哪些环节。第一你要确认硬件。LED接在哪个GPIO上高电平点亮还是低电平点亮要不要串联限流电阻这些信息只能来自原理图和硬件手册不是代码能告诉你的。这一步叫硬件摸底。第二要在设备树里把这个LED描述出来。Linux内核现在通过设备树来枚举设备如果你不在设备树里写节点驱动代码写得再漂亮内核也不知道这颗LED存在。第三要写驱动代码。驱动要完成至少三件事从设备树里拿到GPIO信息完成GPIO的申请和初始化注册字符设备提供开、关、读写接口把硬件状态和应用层传下来的数据对应起来。第四要编译和加载。写Makefile用交叉编译工具链把驱动编成.ko然后拷到板子上用insmod加载。第五应用层交互。加载成功只是开始你还要能在板上通过echo、cat或者一个简单的C程序去控制LED这才算真正打通。第六调试。LED不亮到底是驱动没跑起来还是GPIO被别的驱动占了还是引脚电平不对这些排查手段和复杂设备完全一样。把这六步走完你就把设备树、platform总线、字符设备、GPIO子系统、模块机制、调试方法全部串了一遍。以后再学I2C、SPI、USB驱动虽然协议不同但开发流程和代码结构都是同一套思路。这就是LED驱动作为第一课的价值信息密度高但硬件简单到不会让你分心。1.2 动手前的准备清单环境和资料缺一不可我见过太多人拿到板子什么资料都没看就开始写驱动最后卡住半天发现是内核源码版本不对。准备工作比写代码更重要。硬件方面你需要一块能跑Linux的开发板IMX6ULL、STM32MP1、全志H3这类都行。板子最好引出可用GPIOLED不要直接焊死用杜邦线方便测量。确认你手里的LED常见的是红色或蓝色直插灯珠工作电流大约5到20毫安必须串一个几百欧到1千欧的限流电阻直接怼到GPIO上很容易把引脚烧坏。软件方面重点是内核源码。这里有一条铁律内核源码的版本必须和板子上正在运行的内核版本一致。你可以用uname -r查看板子内核版本也可以用uname -a看详细信息。如果不一致编译出来的.ko在insmod时大概率直接报Invalid module format因为模块的vermagic字符串对不上。交叉编译工具链也要配好。32位ARM板子常用arm-linux-gnueabihf-64位板子用aarch64-linux-gnu-。很多板厂出厂SDK里都带好了工具链别自己乱装一个高版本老内核配太新的gcc经常会编出各种莫名其妙的错误。最后是原理图。不要只看板卡商给的引脚图一定要找到完整的底板原理图PDF确认LED那一页的接法LED的正极通过电阻接到GPIO还是负极通过电阻接到GPIO。这个信息直接决定你在驱动里应该写高电平点亮还是低电平点亮也决定设备树里GPIO_ACTIVE_HIGH还是GPIO_ACTIVE_LOW。资料齐了再动手后面会省很多时间。2. 先别急着写代码设备树与GPIO子系统的匹配逻辑很多初学者最大的困惑是设备树、平台驱动、probe函数这三者到底是怎么串起来的我先把这条线理清楚再讲代码。2.1 设备树描述LED一个节点就是一个设备设备树本质是一棵描述硬件信息的树。它不负责逻辑只负责“报户口”这里有一颗LED它接在GPIO1的29号引脚上高电平有效。驱动代码则需要去这棵树里找到对应节点把信息取出来使用。一个最简单的LED设备树节点长这样/ { myled { compatible mycompany,led; label user-led; gpios gpio1 29 GPIO_ACTIVE_HIGH; default-state off; }; };这个节点里compatible是灵魂。它是一张“身份证”内核靠它来寻找匹配的驱动。字符串的格式通常是“厂商名,设备名”要全局唯一不要写太通用的名字比如就叫led那样很容易和别的驱动撞车。gpios属性描述接的是哪组GPIO控制器下的哪一根引脚GPIO_ACTIVE_HIGH表示高电平有效。这里特别要说一句设备树里描述的“有效电平”和你实际的电路是挂钩的。如果LED是阴极接地、阳极通过电阻接GPIO那GPIO输出高电平时LED亮这就是GPIO_ACTIVE_HIGH。反过来LED阳极接电源、阴极通过电阻接GPIO那GPIO输出低电平时LED亮就要写GPIO_ACTIVE_LOW。很多人在这个细节上吃过亏。代码里明明写的是gpiod_set_value(desc, 1)但灯就是不亮。其实不是代码错而是设备树里有效电平标错了。GPIO子系统很聪明它会在内部根据active低有效自动反转逻辑。你用驱动API操作“逻辑电平”它帮你处理“物理电平”。2.2 驱动与设备是怎么“牵手”的compatible匹配机制现在有了设备树节点驱动怎么写从内核的角度看设备树节点会被内核解析成一个platform_device挂到platform总线上。总线本身不干活它只负责一件事当有新的platform_driver注册进来时去遍历所有挂在这条总线上的设备逐个检查compatible属性。驱动里需要声明一张of_device_id表。这张表里写明了“我支持哪些compatible”内核在匹配时逐个对比static const struct of_device_id led_of_match[] { { .compatible mycompany,led }, {} }; MODULE_DEVICE_TABLE(of, led_of_match);当总线发现某个节点的compatible和这个表里的条目一样时就会调用驱动里的probe函数。这个probe就是驱动代码真正开始执行逻辑的入口。反过来如果你加载了驱动模块但dmesg里看不到任何probe相关的打印最优先考虑的就是compatible不匹配或者设备树根本没编纂进dtb。我打个比方。设备树是“招聘信息”它写着我们需要一个叫mycompany,led的员工。驱动是“求职者简历”它写着我能胜任mycompany,led这个岗位。platform总线就是简历投递系统两者匹配上了就触发面试面试就是probe函数。搞清楚这个机制之后你再看那些驱动代码就不会晕了。probe是入口remove是出口中间是设备的具体操作逻辑。2.3 为什么说GPIO子系统让你省下大量移植成本在Linux内核里操作GPIO现在最推荐的是GPIO描述符gpiod接口而不是老的gpio编号接口。这套接口设计得非常巧妙驱动里不关心引脚具体是哪个控制器下的第几号而是通过devm_gpiod_get函数从设备树节点里拿一个GPIO描述符之后操作的都是这个描述符。这样做的好处非常明显。第一设备树里已经写好了使用哪组GPIO、是否active低有效驱动代码不需要再关心硬件极性。第二驱动不写死GPIO编号换板子、换引脚只需要改设备树不需要动C代码。第三devm_前缀的资源是设备管理机制自动回收的驱动卸载或者probe失败时GPIO请求会自动释放不用你手动清理。顺便提一个知识点很多人搜GPIO资料时会看到“GPIO的8种工作模式”那个说法是STM32系列单片机里的概念。Linux的GPIO子系统不是这么分类的它用输入、输出、中断、开漏等组合来描述引脚属性并通过pinctrl子系统管理引脚的复用。你不需要把这两套体系强行对应起来只要记住Linux侧通过设备树属性来控制这些行为就够了。还有一点很容易忽略有些引脚除了能做GPIO还能被复用成I2C、UART、PWM等功能。如果某个引脚已经被别的功能占用gpiod_get就会失败驱动会报错。这种时候要去查pinctrl配置看看这个引脚被谁占了。通常设备树里会通过pinctrl-0属性指定这个节点使用哪组引脚配置。3. 手写一个完整的LED驱动框架选型与代码实现第2章讲了匹配机制这一章就真正动手写驱动。为了让效果最直观我会按一个真实、精简但结构完整的方案来写平台驱动加设备树加字符设备。3.1 两条实现路线我为什么推荐先“重复造轮子”如果你去查资料会发现内核里已经有一个叫gpio-leds的驱动compatible是gpio-leds。它把所有GPIO LED的公共逻辑都封装好了还附带各种触发器功能比如心跳灯、mmc读写闪烁。真实产品项目里我一般是直接用这个现成驱动只需要在设备树里加子节点根本不用写C代码。但学习阶段我强烈建议你先把现成驱动放一边自己手写一遍。原因很直接gpio-leds驱动把probe、字符设备、file_operations这些机制全都封装在通用逻辑里了你改完设备树虽然能让灯亮但对内核为什么能驱动它仍然是一头雾水。等你手写一遍再回过头看gpio-leds的代码才会真正看懂它内部在做什么。手写驱动的路线选择上我也给你一个建议用platform_driver不要用单纯的module_init加gpio_request那种老式写法。因为前者是当前主流配合设备树能让你理解整条匹配链路而后者代码虽然更短但它把所有流程都绕开了学完只会点灯换个环境依然不会写驱动。3.2 设备树节点这样写才不容易踩坑在根节点下新建一个子节点这是学习阶段最不容易出问题的方式。有些朋友喜欢把LED节点写进pinctrl节点或者soc节点里不是不行只是初学者很容易写错父子关系导致probe匹配不上。我用的完整节点是这样/ { myled { compatible mycompany,led; label user0; gpios gpio1 29 GPIO_ACTIVE_HIGH; }; };如果你的板子的GPIO控制器不是gpio1改成实际的比如有些板子是gpio4。怎么确认在板子启动日志里看GPIO控制器的注册信息或者去查芯片手册的GPIO章节。关于pinctrl如果这颗引脚平时被其他外设占用了你还需要在节点里配上pinctrl-0属性。但很多学习板的GPIO默认就是纯GPIO功能初期先不用配。真遇到gpiod_get失败并且提示“pin is already requested”之类的时候再回头补pinctrl配置。这个排错顺序很重要先跑通再深入。3.3 驱动源码字符设备接口与GPIO操作下面是一份完整的驱动代码。我刻意把代码写得短一些但保留了完整框架。它做的事情是匹配设备树节点取GPIO注册字符设备创建/dev/led节点然后定义读和写两个接口。写操作应用层写入字符1点亮LED写入0熄灭LED。 读操作读取当前LED状态返回字符1或0。#include linux/module.h #include linux/platform_device.h #include linux/of.h #include linux/gpio/consumer.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #include linux/slab.h #define LED_DRV_NAME myled struct led_dev { struct gpio_desc *desc; dev_t devno; struct cdev cdev; struct class *cls; struct device *dev; }; static int led_open(struct inode *inode, struct file *filp) { struct led_dev *ldev container_of(inode-i_cdev, struct led_dev, cdev); filp-private_data ldev; return 0; } static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { struct led_dev *ldev filp-private_data; char kbuf[8]; if (count sizeof(kbuf)) count sizeof(kbuf); if (copy_from_user(kbuf, buf, count)) return -EFAULT; if (kbuf[0] 1) gpiod_set_value(ldev-desc, 1); else if (kbuf[0] 0) gpiod_set_value(ldev-desc, 0); else return -EINVAL; return count; } static ssize_t led_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct led_dev *ldev filp-private_data; char val; int ret; if (*ppos ! 0) return 0; if (count 1) return -EINVAL; val gpiod_get_value(ldev-desc) ? 1 : 0; ret copy_to_user(buf, val, 1); if (ret) return -EFAULT; *ppos 1; return 1; } static const struct file_operations led_fops { .owner THIS_MODULE, .open led_open, .read led_read, .write led_write, }; static int led_probe(struct platform_device *pdev) { struct led_dev *ldev; int ret; ldev devm_kzalloc(pdev-dev, sizeof(*ldev), GFP_KERNEL); if (!ldev) return -ENOMEM; ldev-desc devm_gpiod_get(pdev-dev, NULL, GPIOD_OUT_LOW); if (IS_ERR(ldev-desc)) { dev_err(pdev-dev, led: failed to get gpio\n); return PTR_ERR(ldev-desc); } ret alloc_chrdev_region(ldev-devno, 0, 1, LED_DRV_NAME); if (ret 0) return ret; cdev_init(ldev-cdev, led_fops); ldev-cdev.owner THIS_MODULE; ret cdev_add(ldev-cdev, ldev-devno, 1); if (ret 0) goto err_region; ldev-cls class_create(THIS_MODULE, LED_DRV_NAME); if (IS_ERR(ldev-cls)) { ret PTR_ERR(ldev-cls); goto err_cdev; } ldev-dev device_create(ldev-cls, pdev-dev, ldev-devno, NULL, led); if (IS_ERR(ldev-dev)) { ret PTR_ERR(ldev-dev); goto err_class; } dev_set_drvdata(pdev-dev, ldev); dev_info(pdev-dev, led probe ok\n); return 0; err_class: class_destroy(ldev-cls); err_cdev: cdev_del(ldev-cdev); err_region: unregister_chrdev_region(ldev-devno, 1); return ret; } static void led_remove(struct platform_device *pdev) { struct led_dev *ldev dev_get_drvdata(pdev-dev); device_destroy(ldev-cls, ldev-devno); class_destroy(ldev-cls); cdev_del(ldev-cdev); unregister_chrdev_region(ldev-devno, 1); } static const struct of_device_id led_of_match[] { { .compatible mycompany,led }, {} }; MODULE_DEVICE_TABLE(of, led_of_match); static struct platform_driver led_driver { .probe led_probe, .remove led_remove, .driver { .name LED_DRV_NAME, .of_match_table led_of_match, }, }; module_platform_driver(led_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple GPIO LED driver);我挑几个关键点解释一下。devm_gpiod_get的第二个参数传NULL是因为我们的设备树节点里只有一个gpios属性。如果节点里有多个GPIO比如既有LED又有某个传感器使能引脚那就需要给属性起名比如led-gpios然后在驱动里写devm_gpiod_get(pdev-dev, led, 0)。gpiod_set_value操作的第一个参数是描述符第二个参数是逻辑电平。如果设备树里写了GPIO_ACTIVE_LOW传1进去它会自动把物理引脚拉到低电平。这也是前面说的用gpiod接口省心。class_create和device_create配合使用会在/dev目录下自动生成名为led的设备节点。如果你只注册了cdev没有创建device那还得手动mknod。对于开发阶段能用自动创建就自动创建少一步人工操作就少一个出错点。错误处理路径一定要写完整。很多新手写驱动probe里只关注成功路径出错了直接return负数结果后面想卸载模块时系统提示设备还在使用或者反复加载卸载后设备号越积越多。你在代码里看到的那些err标签就是我处理出错路径的方式Devm资源不用手动释放但cdev和device_create、class_create这些不是devm管理的必须手动清理。3.4 Makefile与编译交叉编译环境的关键配置驱动不能像普通程序一样用gcc编译必须通过内核的kbuild系统来编译。Makefile很简单但有几个变量要特别小心。obj-m : led_drv.o KDIR : /home/user/linux-imx CROSS_COMPILE : arm-linux-gnueabihf- ARCH : arm PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) modules clean: $(MAKE) -C $(KDIR) M$(PWD) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) cleanKDIR必须指向你目标板同一个版本的内核源码而且这个源码最好已经完整编译过一遍至少也执行过make modules_prepare。如果KDIR只解压出来从来没编译过直接make modules会报一些头文件找不到之类的错。ARCH和CROSS_COMPILE要和板子架构匹配。你是64位ARM板子就写ARCHarm64、CROSS_COMPILEaarch64-linux-gnu-。还有一点如果你的内核源码根目录的.config配置了CONFIG_MODVERSIONS那么编译出的.ko里会记录符号版本信息insmod时内核会校验不匹配照样拒绝加载。所以源码和你跑的内核版本一致性再怎么强调都不过分。编译完成之后你会在当前目录看到led_drv.ko。拷到板子上之前先用file命令看一眼file led_drv.ko如果显示的是ARM架构说明交叉编译生效了。如果显示x86-64说明工具链没用对这时候直接拷到板子上insmod就会报Exec format error。4. 上板实测全记录加载、验证与报错排查代码编译通过只是第一步代码能不能在真实硬件上按预期工作才是驱动开发的真正考验。这一章我复盘一次完整的实测过程。4.1 模块加载后如何确认驱动真的“活了”把led_drv.ko拷贝到板子上方式很多nfs挂载、scp、U盘都行。开发阶段我习惯用nfs因为省去反复拷贝的麻烦。然后执行insmod ./led_drv.ko注意开发前期用insmod不要一上来就modprobe。insmod直接加载指定路径的模块没有依赖解析出错信息也直观。modprobe要配合depmod生成的依赖数据库你刚拷贝过去没有跑depmod很可能直接失败。加载之后立刻看内核日志dmesg | tail -20如果正常你会看到一行led probe ok。这说明设备树节点匹配上了probe执行成功。接着验证模块是否真的在lsmod | grep led cat /proc/devices | grep myled ls -l /dev/led/proc/devices能看到主设备号/dev/led则应该是确确实实存在的节点。如果/dev/led不存在检查class和device_create那段代码有没有执行或者dmesg里有没有对应的错误打印。如果节点是普通用户权限你还得先chmod 666 /dev/led否则echo进去会报Permission denied。此时如果一切正常你已经有了一颗在Linux系统眼里“登记在册”的LED。但这时它还不亮因为驱动要求应用层写入指令。这就进入下一步。4.2 应用层访问设备从手写App到命令行验证最简单的验证方式不需要写任何代码直接用shellecho 1 /dev/led看灯有没有亮。再echo 0 /dev/led看灯有没有灭。如果这一步正常你的字符设备驱动write回调已经生效。再验证读接口cat /dev/led应该输出当前电平对应的字符。不过echo只能证明write能跑通真实项目里应用层通常是用C程序来访问设备的。我给你一份最简应用层代码#include stdio.h #include fcntl.h #include unistd.h #include string.h int main(int argc, char *argv[]) { char buf[8] {0}; int fd; fd open(/dev/led, O_RDWR); if (fd 0) { perror(open); return -1; } if (argc 1) { if (strcmp(argv[1], on) 0) write(fd, 1, 1); else if (strcmp(argv[1], off) 0) write(fd, 0, 1); } read(fd, buf, 1); printf(led status: %c\n, buf[0]); close(fd); return 0; }编译时用交叉编译器arm-linux-gnueabihf-gcc led_test.c -o led_test拷到板子上执行./led_test on ./led_test off到这里你已经完成了一个完整闭环设备树描述硬件驱动获取并操作GPIO应用层通过文件接口控制设备。这就是Linux设备驱动开发的典型流程所有复杂的驱动在架构上都是这个模式的延伸。4.3 点名几个高频报错与排查思路我在实际开发中遇到过不少奇怪现象这里选几个最有代表性的按“现象—原因—排查方式”列出来。现象常见原因排查方式insmod报Invalid module format内核版本不一致或编译器版本不一致用uname -r对比KDIR内核版本确认.config的CONFIG_MODVERSIONSinsmod报Exec format error.ko其实编成了x86格式用file命令检查文件格式确认交叉编译变量加载无报错但没有probe日志设备树节点没编进dtb或者compatible不匹配检查dtb是否更新cat /proc/device-tree/查看节点是否存在gpiod_get返回-EPROBE_DEFER或错误引脚被其他驱动占用或GPIO控制器驱动未初始化dmesg看完整错误搜索pin usage调整设备树echo 1后灯不亮active low配置反了或硬件接法问题用万用表测GPIO电压确认电平输出和预期是否一致这里重点说说dtb的问题。很多板卡默认启动加载的dtb是boot分区的不是内核源码编译出来的。你在设备树里加了节点编译了内核但如果没把新的dtb刷进boot分区系统启动时加载的还是旧dtb驱动自然匹配不到。排查方法很简单在板子上看ls /proc/device-tree/myled如果这个目录不存在说明设备树根本没有这个节点。这时候别怀疑驱动先回去搞设备树。还有一个非常隐蔽的坑是引脚电平。我曾经遇到过一块板子设备树里写的是GPIO_ACTIVE_HIGH代码里gpiod_set_value(desc, 1)理论上灯应该亮。但实际测量引脚电压只有0.3V左右灯不亮。查了很久发现这颗LED的电源域是1.8V的而GPIO配置的是3.3V开漏模式根本拉不到高电平。这就是硬件细节问题不拿万用表量一遍光看代码永远发现不了。5. 调试手段与进阶扩展让这颗LED发挥更多价值LED驱动跑通之后你等于掌握了一套通用的调试方法和知识框架。这一章我讲讲调试时真正有效的手段以及下一步可以往哪个方向进阶。5.1 调试三板斧printk、sysfs与万用表驱动的调试不像应用层程序那样可以随意打断点最基础也最有效的工具就是printk。别嫌它土很多内核工程师排查问题就是从printk开始的。printk有日志级别常用的有KERN_ERR、KERN_INFO、KERN_DEBUG。在probe入口、GPIO获取成功之后、write回调里各加一条打印基本就能定位问题出在哪一段。查看日志dmesg | tail如果dmesg刷得太快看不到你的打印可以临时调整控制台日志级别echo 8 /proc/sys/kernel/printk这样所有级别的printk都会输出到你当前终端。调试完记得恢复否则调试信息太多会严重影响系统性能。第二板斧是sysfs和debugfs。手写驱动虽然没有生成/sys/class/leds但内核提供了GPIO的调试接口路径是cat /sys/kernel/debug/gpio这个文件会列出当前系统里所有GPIO的申请情况和状态你的LED引脚是哪个控制器、被谁申请、当前输出是高还是低一眼就能看到。如果这里显示的引脚和预期不符多半是设备树里gpios写错了引脚号。第三板斧是万用表。软件说输出了高电平灯不亮问题可能在硬件侧。把万用表打到直流电压档黑表笔接地红表笔点LED连接GPIO的那一端看电压是否和代码设置一致。如果代码设了1但电压是0问题在驱动或引脚配置如果电压正常但灯不亮检查电阻和LED极性。测量动作本身不复杂但很多人忽略了导致在软件里瞎查几个小时。5.2 从点灯到更广阔的内核世界这颗LED跑通之后你的能力边界已经不只在LED本身。我建议按下面几个方向继续往下走。第一个方向换成内核自带的gpio-leds驱动。把你手写的LED当作真实产品功能没必要维护自己的驱动直接用内核通用驱动即可。设备树改成/ { leds { compatible gpio-leds; user-led { label user-led; gpios gpio1 29 GPIO_ACTIVE_HIGH; linux,default-trigger heartbeat; }; }; };然后你在板子上会看到/sys/class/leds/user-led这个目录操作它echo 1 /sys/class/leds/user-led/brightness echo heartbeat /sys/class/leds/user-led/trigger这里最大的学习价值是对比你的手写驱动和内核通用驱动看看内核是怎么封装设备操作、怎么对接led class的你会学到很多工程化的写法。第二个方向GPIO按键驱动。LED是GPIO输出按键是GPIO输入还涉及中断和去抖。你可以参考内核里的gpio-keys驱动设备树compatible是gpio-keys。把按键按下去、松开来的状态上报给输入子系统应用层通过/dev/input/event读取。学完你就掌握了中断上下文和input子系统这是驱动开发里非常常用的一块。第三个方向PWM调光。LED亮度可调是很多产品的基本功能涉及PWM子系统和leds-pwm驱动。设备树里配置PWM通道、周期、默认占空比驱动控制PWM输出来改变LED平均功率。这个方向会带你接触内核的timekeeping和时钟管理扩展视野。这三个方向走完你就有了至少在嵌入式Linux领域独立承担外设驱动开发的能力。到时候回头再看当初那颗LED它早就不只是一颗灯而是你把整条内核驱动链路打通之后最直观的见证。最后再分享一个我自己的习惯。每次拿到一块新开发板不管是工作还是私人折腾我都会先写一个最简单的外设驱动把某个LED点亮。这样做不是为了炫技而是用最短时间验证一套工具链、一份设备树、一条编译加载调试链路是否畅通。这套链路一旦通了后面所有驱动开发都只是外设协议的差异而已。你把这颗LED做利索嵌入式Linux大门后面的大片区域也就真正向你打开了。
返回列表