
简介这是一份面向嵌入式开发者和Linux系统开发学习者的专业参考文献系统梳理了嵌入式Linux设备驱动程序从概念到落地的完整知识框架。内容围绕驱动开发的核心环节展开涵盖设备驱动接口、文件操作接口等通用模块讲解file_operations结构体等关键代码的实现方式并给出了设计、编写、测试、调试的标准开发流程适合需要夯实驱动开发基础或完成课程项目设计的读者。资源为1个pdf文件压缩包大小176KB来源为期刊文章结构紧凑、要点集中便于快速阅读与对照实践。该资源已有2208人学习下载是理解嵌入式Linux驱动工作原理与编写方法的高性价比入门资料。1. 嵌入式Linux设备驱动开发先认清三件套做嵌入式Linux也好几年了最常见的误解是驱动开发是内核维护者的活儿应用工程师只需要会 open、read、write。但当你拿到一块 BSP 不完善的板子需要点亮一颗 LED 或者接入一个串口传感器时就会发现官方文档再全也替代不了自己动手实现一个 platform_driver。真正决定驱动难不难的不是硬件寄存器有多绕而是内核的设备模型、设备树和字符设备框架这三件事有没有串起来。这篇文章就顺着一条最常用的路径走一遍从驱动如何被内核“认领”到最小字符设备的完整落地再到用设备树描述 GPIO 和中断最后给出调试验证手段。以驱动为重点的嵌入式Linux学习路线里这几步走通了后面再碰复杂外设都是同一套方法论的延伸。2. 设备驱动模型与设备树嵌入式Linux驱动匹配的底层逻辑2.1 为什么嵌入式Linux驱动不直接操作物理地址接触驱动开发初期我的困惑是既然 U-Boot 里可以用指针直接读寄存器为什么内核里还要绕一个 platform_driver。答案是内核要同时管理几十个设备硬件地址、中断号这些资源如果散落在驱动代码里换一块板子就要重编内核。现代内核给出的解法是设备模型设备负责描述“硬件上有什么”驱动负责描述“怎么操作”总线负责把两者牵线。设备树出现之前这些信息写在 arch/arm/mach-xxx 的板级 C 文件里硬编码的 resource 数组难以复用多个板子互相拷贝改一个引脚就要重编整个内核。设备树把硬件拓扑从 C 代码里剥离成一个独立的、可编译的 dts 文件驱动源码从此不用关心具体板卡地址一套驱动可以在几十种板子上共享。2.2 设备树从 dts 到 dtb 的交接流程设备树是嵌入式Linux驱动开发中绕不开的一环它的工作链路是编写 .dts/.dtsi 源文件用 dtc 编译成 .dtb再由 bootloader 在启动时把 dtb 传给内核解析。树形结构里每一个节点就是一个潜在的设备节点上挂载的属性描述设备资源。一个最简节点如下/dts-v1/; / { model Demo Board; compatible vendor,demo; soc { compatible simple-bus; demo_led: led1c20800 { compatible vendor,demo-led; reg 0x01c20800 0x100; }; }; };这段 dts 里compatible 是驱动找设备的“身份证”reg 描述物理基地址 0x01c20800 和映射长度 0x100这个地址只是演示格式具体值要查对应 SoC 的手册。内核启动后设备树的内容会暴露在 /sys/firmware/devicetree/base 下每个节点在 sysfs 里变成一层目录可以直接 cat 属性来验证设备树有没有按预期解析。这对接下来的驱动匹配非常有用。2.3 of_match_table 与 platform_match 是怎么把驱动和设备对上号的设备树只是描述了硬件终端的分工还是由驱动侧的 of_match_table 完成。内核里的 platform_match 会拿 dtb 节点的 compatible 属性逐个与驱动表中 of_device_id 的 compatible 比较命中后调用驱动的 probe 回调。一个只做资源映射的骨架如下#include linux/module.h #include linux/platform_device.h #include linux/of.h static const struct of_device_id demo_led_of_match[] { { .compatible vendor,demo-led }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, demo_led_of_match); static int demo_led_probe(struct platform_device *pdev) { struct resource *mem platform_get_resource(pdev, IORESOURCE_MEM, 0); void __iomem *base; if (!mem) return -ENXIO; base devm_ioremap_resource(pdev-dev, mem); if (IS_ERR(base)) return PTR_ERR(base); dev_info(pdev-dev, led regs 0x%llx remapped\n, (u64)mem-start); return 0; } static int demo_led_remove(struct platform_device *pdev) { // 老内核用 int 返回6.1 之后偏好 void按你的内核版本调整 return 0; } static struct platform_driver demo_led_driver { .probe demo_led_probe, .remove demo_led_remove, .driver { .name demo_led, .of_match_table demo_led_of_match, }, }; module_platform_driver(demo_led_driver); MODULE_LICENSE(GPL);代码里 module_platform_driver 是 platform_driver_register 与 module_init/module_exit 的封装省去两段样板代码。devm_ioremap_resource 会把 reg 属性里的物理地址映射为内核可访问的虚拟地址后续操作寄存器用的是这个 base。驱动被卸载或设备被移除时devm 资源会自动释放不需要手动 iounmap。MODULE_DEVICE_TABLE 的作用是生成 modinfo 信息方便模块自动加载工具识别。drivers 中常见的资源获取方式可以按下面的对应关系来记设备树属性驱动侧获取 API返回内容regplatform_get_resource(pdev, IORESOURCE_MEM, 0)struct resource含起始地址、长度、flagsinterruptsplatform_get_resource(pdev, IORESOURCE_IRQ, 0)中断号clocksdevm_clk_get(dev, clk_name)struct clk 句柄gpiosdevm_gpiod_get(dev, led, ...)struct gpio_desc 指针普通字符串of_property_read_stringchar* 输出参数提示compatible 的推荐写法是“厂商,型号”例如“vendor,demo-led”。一个节点可以写多个 compatible 字符串驱动表里也可以放多个条目匹配按顺序进行这是嵌入式Linux面试里经常考到的一个点。3. 从 Makefile 到 /dev 节点嵌入式Linux字符设备驱动的最小实现3.1 交叉编译环境的三件套准备平台总线驱动只是骨架真正让用户态能 open、read、write 的是字符设备框架。开发前要先确认三件事一是交叉编译器与板端的 glibc/内核符号表匹配二是内核源码树版本与板子运行的内核一致三是 ARCH 和 CROSS_COMPILE 环境变量没有设反。以 ARM 32 位平台常见做法为例CROSS_COMPILE 通常形如 arm-linux-gnueabihf-64 位则是 aarch64-linux-gnu-。内核源码树只要解压出来即可不需要先编译整个内核外部模块编译只依赖内核源码里的头文件和 Makefile 规则。3.2 一个可用的驱动 Makefile编译外部模块的 Makefile 通常只有几行obj-m : demo_dev.o KDIR ? /home/lijin/linux-5.15 PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules clean: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm CROSS_COMPILEarm-linux-gnueabihf- clean-C 参数让 make 进入内核源码树使用它顶层的 MakefileM$(PWD) 告诉内核构建系统要为这个外部目录生成模块ARCH 和 CROSS_COMPILE 必须与目标板一致否则编出来的 .ko 无法在板子上 insmod。obj-m 表示该目标以模块方式编译如果是 obj-y 则会编入内核镜像。驱动开发初期最常见的错误就是 KDIR 指错内核版本导致 vermagic 不匹配。3.3 字符设备驱动的核心代码一个不需要操作硬件的最简字符驱动只实现读写一个内核缓冲代码骨架如下#include linux/module.h #include linux/kernel.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #define DEV_NAME demo static dev_t demo_devno; static struct cdev demo_cdev; static struct class *demo_class; static char demo_buf[256]; static ssize_t demo_read(struct file *filp, char __user *out, size_t size, loff_t *off) { if (size sizeof(demo_buf)) size sizeof(demo_buf); if (copy_to_user(out, demo_buf, size)) return -EFAULT; return size; } static ssize_t demo_write(struct file *filp, const char __user *in, size_t size, loff_t *off) { if (size sizeof(demo_buf)) size sizeof(demo_buf); if (copy_from_user(demo_buf, in, size)) return -EFAULT; return size; } static const struct file_operations demo_fops { .owner THIS_MODULE, .read demo_read, .write demo_write, }; static int __init demo_init(void) { struct device *dev; int ret; ret alloc_chrdev_region(demo_devno, 0, 1, DEV_NAME); if (ret 0) return ret; cdev_init(demo_cdev, demo_fops); ret cdev_add(demo_cdev, demo_devno, 1); if (ret 0) goto err_add; demo_class class_create(DEV_NAME); if (IS_ERR(demo_class)) { ret PTR_ERR(demo_class); goto err_class; } dev device_create(demo_class, NULL, demo_devno, NULL, DEV_NAME); if (IS_ERR(dev)) { ret PTR_ERR(dev); goto err_device; } return 0; err_device: class_destroy(demo_class); err_class: cdev_del(demo_cdev); err_add: unregister_chrdev_region(demo_devno, 1); return ret; } static void __exit demo_exit(void) { device_destroy(demo_class, demo_devno); class_destroy(demo_class); cdev_del(demo_cdev); unregister_chrdev_region(demo_devno, 1); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL);这段代码有几处值得拆开说。alloc_chrdev_region 动态分配主设备号主设备号是 /dev 节点和设备注册表之间的索引。cdev_init 把 file_operations 挂到 cdev 结构体上cdev_add 之后内核就认为这个字符设备可用了。class_create 和 device_create 是给用户态设备管理器用的接口insmod 之后udev/mdev 正是靠这两个回调在 /dev 下自动生成节点。copy_to_user 和 copy_from_user 是内核态与用户态缓冲拷贝的唯一正确方式不能直接 memcpy因为用户态地址在内核态不做段保护直接访问可能引起 page fault 或安全问题。3.4 加载验证与 file_operations 触发时机编译得到 demo_dev.ko 后在板子上依次执行insmod demo_dev.ko # 加载模块 lsmod | grep demo # 确认模块在模块列表 cat /proc/devices | grep demo # 查看动态分配的设备号 echo hello /dev/demo # 触发 write cat /dev/demo # 触发 read rmmod demo_dev # 卸载模块 dmesg | tail # 查看驱动打印加载后 /proc/devices 能看到 demo 字符设备的设备号/dev/demo 存在说明 device_create 生效。如果 /dev 下没有节点多半是板子上没有跑 udev/mdev或者 class_create 的 name 与 device_create 的 name 不一致。file_operations 各回调与用户态系统调用的对应关系是驱动开发最常问的基础题file_operations 成员触发的系统调用说明.openopen(/dev/demo, ...)打开节点时调用可做设备初始化.readread(fd, buf, n)从设备读数据到用户缓冲.writewrite(fd, buf, n)从用户缓冲写数据到设备.unlocked_ioctlioctl(fd, cmd, arg)设备专有控制命令如改波特率.releaseclose(fd)最后一个引用关闭时调用不实现 .open/.release 也能工作内核会提供默认的空操作。但 read/write 是否阻塞、ioctl 怎么与用户态协议对齐是驱动开发中容易出错的地方。从应用开发转向驱动开发的菜鸟进阶路线第一道坎通常就卡在这一层用户态以为 read(fd, buf, n) 会等数据驱动却没实现 wait_event结果返回的是空缓冲或 -EAGAIN。先把这个最小骨架跑通再往里面加硬件逻辑会简单得多。4. 嵌入式Linux驱动解析设备树GPIO与中断的完整接入4.1 一个带 GPIO 和中断的设备树节点字符设备框架解决了用户态接口的问题但驱动真正要干的事是操作寄存器、读引脚、响应中断。这一章从设备树开始到 probe 里申请 GPIO、注册中断走一遍嵌入式Linux项目实战中最高频的外设接入套路。以下面这个按键节点为例/dts-v1/; #include dt-bindings/gpio/gpio.h #include dt-bindings/interrupt-controller/irq.h / { demo-key { compatible vendor,demo-key; gpios gpio4 14 GPIO_ACTIVE_LOW; interrupt-parent gpio4; interrupts 14 IRQ_TYPE_EDGE_FALLING; }; };gpios 属性里 gpio4 是 phandle指向 SoC 设备树里某个 GPIO 控制器的节点14 是该控制器内的引脚号GPIO_ACTIVE_LOW 表示低电平有效。interrupts 属性则声明同一物理引脚用第 14 号中断、下降沿触发。这里的“第 14 号中断”由 gpio4 控制器节点里 interrupt-controller 和 #interrupt-cells 共同决定。驱动开发中很多人会搞混引脚号和中断号其实同一物理引脚在 GPIO 子系统里用“控制器序号”表示在中断子系统里用“中断号”表示gpiod_to_irq 就是做这两者转换的函数。4.2 probe 里申请 GPIO 并注册中断对应的驱动侧代码如下#include linux/gpio/consumer.h #include linux/interrupt.h static irqreturn_t demo_key_isr(int irq, void *data) { struct gpio_desc *in data; int val gpiod_get_value(in); pr_info(key triggered, level %d\n, val); return IRQ_HANDLED; } static int demo_key_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct gpio_desc *in; int irq, ret; in devm_gpiod_get(dev, NULL, GPIOD_IN); if (IS_ERR(in)) return PTR_ERR(in); irq gpiod_to_irq(in); if (irq 0) return irq; ret devm_request_irq(dev, irq, demo_key_isr, IRQF_TRIGGER_FALLING | IRQF_SHARED, dev_name(dev), in); if (ret) return ret; return 0; }devm_gpiod_get 的第一个参数是设备指针第二个传 NULL 表示取设备树节点里第一个未命名的 gpios如果 dts 里写的是 led-gpios这里就传 led。GPIOD_IN 会把引脚方向设为输入。gpiod_to_irq 把 GPIO 描述符转成中断号之后 devm_request_irq 注册中断处理函数。dts 的 interrupts 里已经声明了下降沿触发所以严格来说 IRQF_TRIGGER_FALLING 可以省略显式写出来会覆盖设备树里的触发方式两者不一致时调试起来很隐蔽。中断回调里只做了 gpiod_get_value 和打印这是刻意保持轻量。中断上下文不能调用可能睡眠的函数比如 mutex_lock、kmalloc 带 GFP_KERNEL 的版本、printk 在极端情况也可能引发问题。高频率中断下正确做法是把耗时工作放到 bottom half 或 request_threaded_irq 的内核线程里这也解释了很多真实项目里“按键能用但系统卡顿”的原因。4.3 devm 资源管理的释放时机上面代码全部使用 devm_ 前缀 API这不是为了少写几行 remove 函数而是保证 probe 中途出错时已经申请的资源能按顺序自动释放。devm 系列调用的机制是每个 device 维护一个 devres 资源链表设备释放时统一执行链表上的 release 回调。与手写 goto 的错误处理相比资源泄漏分支少了一半这是现代内核驱动中的主流风格。常用对应关系如下传统 APIdevm 版本自动释放时机ioremap / iounmapdevm_ioremap_resource设备释放时自动 iounmapgpio_request / gpio_freedevm_gpiod_get设备释放时自动 put gpio descrequest_irq / free_irqdevm_request_irq设备释放时自动 free irqkzalloc / kfreedevm_kzalloc设备释放时自动 kfree使用 devm 时remove 回调甚至可以只返回 0它不再承担资源清理职责。但有顺序要求的外设例外比如先停 DMA 再释放缓冲这类操作还是需要手动在 remove 里保证顺序。4.4 设备树节点没生效时的排查顺序probe 不执行是设备树驱动最常见的问题。排查不要只看驱动代码先确认设备树节点有没有被内核实际加载ls /sys/bus/platform/devices/ | grep demo-key cat /sys/firmware/devicetree/base/demo-key/compatible cat /sys/firmware/devicetree/base/demo-key/status第一条命令看平台设备是否注册第二条看内核拿到的 compatible 值第三条看节点使能状态。设备树节点没有写 status 属性时默认是 okay一旦显式写了 status disabledplatform_match 会直接跳过这个节点probe 不会被调用驱动代码再对也没用。此外gpios 属性里引用的 controller 在 dts 里必须声明为 gpio-controller否则 phandle 解析失败devm_gpiod_get 会返回 -EINVAL。这类问题不需要 gdb把 /sys/firmware/devicetree/base 下的属性 cat 出来和 dts 原稿对比一遍基本就能定位。5. 嵌入式Linux驱动调试验证资源映射与匹配的四个手段5.1 动态调试不用重新编译往代码里打点驱动代码里用 dev_dbg/pr_debug 打的日志默认不会输出但可以在运行时通过 debugfs 打开避免频繁重编模块echo file demo_key.c p /sys/kernel/debug/dynamic_debug/control dmesg -w前提是内核开启了 CONFIG_DYNAMIC_DEBUG且 debugfs 已挂载。开发阶段用 pr_info 临时打点没有错但提交前要把敏感日志降级为 dev_dbg动态调试在生产环境排查问题时价值更大。5.2 用 devmem 直接读寄存器验证映射驱动里 ioremap 拿到的是虚拟地址用户态无法直接对照。devmem 工具通过 /dev/mem 提供物理地址访问能力devmem 0x01c20814 devmem 0x01c20814 32 0x01第一条命令读 32 位寄存器第二条把 0x01 写入同一地址。devmem 读写的是物理地址可以验证驱动写下去的值是否真的到达硬件也可以反过来确认驱动读到的寄存器值与示波器或逻辑分析仪采到的外部电平是否一致。老内核上如果 CONFIG_STRICT_DEVMEM 开启部分地址段会被拒绝访问此时只能靠驱动内的调试接口验证。5.3 确认驱动绑定关系是否建立驱动是否绑定到设备除了看 dmesg更直接的是看 sysfs 里的软链接ls -l /sys/bus/platform/drivers/demo_key/该目录下应当有指向设备节点的 symlink。只有设备注册、驱动注册、compatible 匹配三步全部成功probe 才会执行。拿着新板子调外设时先把这四件事按顺序做一遍cat 设备树节点确认 compatible 是对的看 /sys/bus/platform/drivers 下驱动有没有挂在总线上用 devmem 确认寄存器能读写再启用动态调试追 probe 内部逻辑。哪一步断了问题就锁定在哪一步。本文还有配套的精品资源点击获取