ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

嵌入式驱动开发实战:从设备树到probe的完整流程与避坑指南

2026/9/30 1:13:57 拓冰建站 浏览量
嵌入式驱动开发实战:从设备树到probe的完整流程与避坑指南 1. 嵌入式驱动开发到底在忙什么很多人一听到“嵌入式驱动开发”脑子里浮现的画面要么是对着 datasheet 一行行啃寄存器要么是抱着开发板反复插拔串口线看 log。外人看着觉得枯燥做久了的人却知道这活儿真正的日常是“在硬件和内核之间当翻译”。硬件工程师跟你说“这个 PHY 的寄存器配一下就能通”内核那边告诉你“probe 函数没进来说明设备树没匹配上”而你要做的就是把这两套语言对上号。先把范围说清楚。嵌入式驱动开发指的是在嵌入式 Linux或者 RTOS环境下为特定外设编写让内核能够识别、控制它的那层代码。它上承应用层下接硬件寄存器中间还要跟内核的各种子系统打交道。你写的不是业务逻辑而是“让硬件能被系统正常使用”的胶水层。小到一个 CP2102 这样的 USB 转串口芯片大到 GPU、DSP、网络 PHY都属于这个范畴。这篇文章适合谁看如果你正在学嵌入式 Linux想知道驱动开发每天具体在忙什么如果你是从单片机转过来的发现裸机那套直接写寄存器的思路在 Linux 下不太灵了或者你是应用层开发者想搞清楚为什么有时候 open 一个设备节点会失败——那这篇内容应该能帮你把这条链路串起来。我会尽量用从业者之间聊天的口吻把驱动开发的核心工作、常见坑和实操方法讲透而不是堆一堆教科书式的定义。2. 驱动开发的核心工作拆解2.1 驱动工程师的一天到底在干什么先破除一个误解驱动开发不等于“天天写代码”。真实的比例大概是三成时间读手册和原理图三成时间调试和抓 log两成时间写和改代码剩下两成在跟硬件、应用、测试的人对齐问题。为什么读手册占这么多因为驱动的本质是“按照硬件的规则去操作它”规则没吃透代码写得再漂亮也是白搭。举个具体的例子。假设你要给一块板子上的网络 PHY 写驱动。第一步不是打开编辑器而是先确认这颗 PHY 是怎么接到主控上的。常见的有两种一种是通过 MDIO 总线管理寄存器、通过 RGMII 传数据另一种是某些场景下不走 MDIO改用 I2C 去访问它的寄存器。这两种接法决定了你驱动里注册的是 mdio_driver 还是 i2c_driverprobe 的入口完全不一样。热词里出现的“linux phy 不使用 mdio 使用 i2c”就是这类真实场景很多人第一次遇到会懵因为默认思路都是 MDIO。所以驱动开发的第一层工作是搞清楚硬件连接方式。这一步做错后面全错。我一般会拿一张纸把主控、外设、总线、电源、时钟、复位这几条线画出来标清楚谁控制谁。这张图比任何文档都管用。2.2 从设备树到 probe设备是怎么被“认出来”的Linux 驱动模型里设备和驱动是分开注册的靠“匹配”走到一起。以设备树Device Tree为例你在 .dts 里写一个节点描述这个硬件挂在哪条总线、寄存器地址是多少、用哪个中断、时钟从哪来。内核启动时解析设备树把每个节点变成 platform_device 或者对应总线的 device。然后你的驱动用 of_match_table 声明“我能处理 compatible 等于某个字符串的设备”两者一匹配probe 函数就被调用。这个过程听起来简单但坑特别多。最常见的就是 compatible 字符串写得不一致设备树里写vendor,chip-a驱动里写vendor,chip_a一个横杠一个下划线probe 永远不进。我踩过这个坑查了两个小时才发现是拼写问题。所以我的习惯是compatible 字符串在设备树和驱动里各复制一份绝不手敲两遍。probe 函数里要做的事情通常有固定套路申请寄存器内存区域devm_ioremap_resource、申请中断devm_request_irq、初始化时钟和电源、注册到对应的子系统比如网络设备用 register_netdev字符设备用 cdev_add。用 devm_ 开头的资源管理函数是好习惯它会在设备卸载时自动释放省得你手动写一堆 goto 清理减少内存泄漏。2.3 字符设备、平台设备、总线设备别被名字绕晕新手最容易迷糊的就是驱动类型。其实按“设备挂在哪”来分就清楚了。平台设备platform device是那些直接挂在 SoC 内部总线上的比如 GPIO 控制器、I2C 控制器本身、DMA 控制器。它们没有物理上的可枚举总线所以用 platform 这套虚拟总线来管理。I2C 设备、SPI 设备、USB 设备则挂在各自的总线上用对应的 i2c_driver、spi_driver、usb_driver 注册。字符设备是另一维度的事它描述的是“用户空间怎么访问这个设备”。你可以在一个平台驱动里同时注册一个字符设备让用户通过 /dev/xxx 来读写。比如很多传感器驱动底层是 I2C 设备上层暴露一个字符设备或者 input 设备给应用。所以“平台设备”和“字符设备”不是互斥的一个是硬件归属一个是访问接口。理解这一点很重要因为它决定了你写驱动时该继承哪套框架。给一个 I2C 温度传感器写驱动你注册的是 i2c_driverprobe 里拿到 client 指针然后决定往上暴露成 hwmon 设备还是字符设备。方向搞对了代码结构自然就清晰了。3. 核心细节与实操要点3.1 寄存器操作readl/writel 不是随便用的裸机时代我们直接*(volatile unsigned int *)addr val到了 Linux 内核里这套写法要换成 readl/writel 这类封装。为什么因为要处理内存屏障、字节序、以及不同架构的差异。ARM 和 x86 对 MMIO 的处理不一样readl/writel 帮你屏蔽了这些。但有个细节很多人忽略readl 和 writel 自带内存屏障而 readl_relaxed/writel_relaxed 不带。如果你在紧密循环里连续写多个寄存器且顺序无所谓用 relaxed 版本性能更好。但如果写寄存器的顺序有严格要求比如先写地址再写数据触发就必须用带屏障的版本否则编译器或 CPU 可能重排导致硬件行为异常。这个坑我在调一个 SPI 控制器时踩过寄存器写进去顺序乱了波形完全不对换成 writel 就好了。另外寄存器地址一定要用 ioremap 映射后的虚拟地址不能直接用物理地址。在 64 位系统上还要注意用 ioremap 而不是 ioremap_nocache后者已废弃以及 readl 操作的是 32 位8 位和 16 位要用 readb/readw。3.2 中断处理上半部和下半部的分工中断是驱动里最容易出问题的地方。核心原则是中断处理函数上半部要尽可能短只做最紧急的事比如清中断标志、读走数据然后把耗时的处理丢给下半部softirq、tasklet 或 workqueue。为什么因为中断处理期间当前 CPU 的本地中断是关闭的你在这里面耗时太长其他中断就会被延迟系统响应变卡。我见过有人在中断处理里直接做 I2C 读写结果系统卡死因为 I2C 传输可能睡眠而中断上下文不允许睡眠。这种错误一旦触发log 里会出现 “scheduling while atomic” 的报错看到这个基本就是中断里干了不该干的事。正确的做法是中断里只记录状态、唤醒一个 workqueue真正的数据处理放到 workqueue 里做。workqueue 运行在进程上下文可以睡眠可以用 I2C、可以申请内存安全得多。3.3 并发与锁驱动里的隐形杀手驱动代码会被多个上下文同时访问用户空间的系统调用、中断、内核线程、定时器。这些上下文可能同时操作同一份数据不加锁就会出问题。常见的锁有自旋锁spinlock和互斥锁mutex。选择原则很简单如果临界区里可能睡眠比如要调用可能睡眠的函数、要等 I2C 传输只能用 mutex如果临界区在中断上下文里只能用 spinlock因为中断里不能睡眠。用错了轻则报错重则死锁。我个人的经验是驱动里共享数据尽量少能不用锁就不用锁。如果必须共享优先用内核提供的原子操作或者 RCU实在不行再上锁。锁的粒度也要控制别一把大锁锁住整个驱动那样并发性能会很差。4. 完整实操流程从零bring up一个I2C传感器4.1 硬件确认与设备树编写假设我们要 bring up 一颗挂在 I2C1 上的温度传感器地址 0x48。第一步是确认硬件查原理图确认它接在哪个 I2C 控制器、地址是多少、有没有中断引脚、供电是多少伏。这些信息决定了设备树怎么写。设备树里I2C 控制器节点下加一个子节点i2c1 { status okay; clock-frequency 100000; temp_sensor: temp48 { compatible vendor,temp-sensor; reg 0x48; interrupt-parent gpio1; interrupts 12 IRQ_TYPE_EDGE_FALLING; }; };这里reg 0x48就是 I2C 从机地址compatible是驱动匹配的关键。中断配置如果硬件没接可以省略驱动里用轮询方式读。4.2 驱动骨架与probe实现驱动的基本结构是定义一个 i2c_driver填上 probe、remove、of_match_table。probe 里拿到 client 后先做一次寄存器读确认设备真的在很多传感器有 WHO_AM_I 寄存器读出来对得上才继续。这一步很重要能区分“设备没接好”和“驱动写错了”。static int temp_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct temp_data *data; int ret; data devm_kzalloc(client-dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >