为什么引导阶段要动i2c设备驱动
嵌入式Linux开发中,修改引导阶段的硬件设备驱动往往不是改一个probe函数那么简单,真正卡住大家的,是字符设备节点注册时机和i2c核心框架的初始化顺序。 早年调试一块传感器板子时,我在/dev/i2c-2上反复open失败,折腾半天才发现驱动在引导阶段还没注册进i2c总线,应用层根本摸不到设备,如果你的板子在uboot引导完、内核起来后需要立即用某个i2c外设,比如触摸屏、电源管理芯片或陀螺仪,那你面对的就是同一类问题。
行业共识认为,引导阶段涉及的外设驱动修改,核心矛盾在于内核子系统初始化顺序和应用层访问需求之间的错位,i2c设备的字符设备驱动,本质上是把内核i2c核心框架里的设备操作暴露成/dev节点,要在引导阶段就让这个节点可用,必须搞清楚i2c适配器注册、设备驱动probe、以及miscdevice或cdev注册这三者的时序关系。
i2c设备驱动与字符设备驱动注册流程差异
很多人搞混一个概念:i2c设备驱动和字符设备驱动是两套注册逻辑,但它们最终会在/dev节点上汇合。 i2c设备驱动走i2c_add_driver注册到i2c总线,由内核匹配设备树里的compatible属性后触发probe,字符设备驱动走alloc_chrdev_region和cdev_add,依赖设备号,引导阶段要修改的,往往是让i2c设备在probe后立刻创建对应的字符设备节点。
标准流程下的两个阶段
内核正常启动时,i2c适配器(i2c controller)先注册到i2c核心,然后挂载在适配器上的设备由i2c_driver完成匹配,这套流程在系统启动早期就会执行,但字符设备节点的生成通常依赖udev或mdev,在initramfs阶段可能尚未运行。
| 阶段 | i2c设备驱动动作 | 字符设备动作 | 引导期可达性 |
|---|---|---|---|
| 内核早期 | 注册i2c_driver,等待匹配 | 尚未创建设备节点 | 不可访问 |
| devtmpfs挂载后 | probe执行,寄存器初始化 | 可通过devtmpfs自动创建设备节点 | 基本可达 |
| 用户态启动后 | 设备已就绪 | 应用层可直接open | 完全可达 |
引导阶段修改的关键,就是把字符设备节点的创建时机从依赖用户态udev,
提前到驱动的probe内部完成。
为什么字符设备比i2c协议层更好用
i2c设备驱动里直接写i2c_smbus_read_byte_data操作硬件很顺手,但应用层没法直接调用内核函数,字符设备驱动封装了file_operations接口,用户态通过read/write/ioctl就能操作设备,引导阶段做系统初始化时,要先用字符设备把硬件调通,后面再做协议封装就顺理成章。
修改引导阶段的i2c字符设备驱动实操步骤
这套修改思路我在imx6和stm32mp1平台上都验证过,核心思路是:在i2c_driver的probe回调里直接注册字符设备,不依赖用户态事件。
第一步:确认设备树中的i2c外设状态
先看设备树里外设节点是否使能,如果节点状态是disabled,引导阶段i2c适配器扫描不到设备,驱动永远不会probe。
&i2c2 {
status = "okay";
touch: touch@38 {
compatible = "myvendor,touch";
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
};
};
确保status = "okay",reg地址与硬件实际的i2c从机地址一致,这一步出错,引导阶段设备就直接消失了。
第二步:在probe中动态创建设备节点
这是修改的核心,在struct i2c_driver的probe函数里,调用class_create和device_create生成/dev/my_i2c_dev节点,这样绕开了udev,只要内核的devtmpfs挂载,节点就自动出现在文件系统里。
static int my_i2c_probe(struct i2c_client client,
const struct i2c_device_id id)
{
struct my_dev dev;
dev = devm_kzalloc(&client->dev, sizeof(dev), GFP_KERNEL);
dev->major = register_chrdev(0, "my_i2c_dev", &my_fops);
dev->cls = class_create(THIS_MODULE, "my_i2c_class");
device_create(dev->cls, NULL, MKDEV(dev->major, 0),
NULL, "my_i2c_dev");
i2c_set_clientdata(client, dev);
return 0;
}
引导阶段的内核日志里,你能看到my_i2c_dev节点在i2c-2 adapter注册之后出现,而不再是系统启动很久后被用户态mdev补建。
第三步:调整注册时机
如果节点出来了,但上电太快设备ready标志还没拉高,可以把
probe里的硬件初始化拆成两部分:一部分只读寄存器确认设备在线,另一部分通过schedule_delayed_work延时到引导阶段后期做完整配置,这样既保证了引导阶段驱动能加载,又不跟设备上电时序打架。
第四步:验证字符设备可见性
在内核启动参数里加initcall_debug,能清楚看到i2c驱动注册的调用顺序,用udevadm info查看设备属性,或者直接在引导阶段的initramfs shell里执行:
ls -l /dev/my_i2c_dev hexdump -C /dev/my_i2c_dev
如果第二步做对了,这两个命令能看到设备节点和原始寄存器值,说明字符设备驱动在引导阶段已经生效。
引导阶段修改的坑和解决思路
设备节点优先级问题
devtmpfs在挂载根文件系统之前可能没有执行权限规则,导致节点有了但权限不对,在device_create里指定devnode回调,强制设置为0660或0666,解决权限不一致的问题。
i2c总线号漂移
引导阶段如果有多个i2c控制器,总线号可能在每次启动时变化,在设备节点里增加alias属性,或者在驱动里通过of_find_i2c_adapter_by_node绑定固定的device_node,这一点在修改引导设备驱动时特别容易忽略,一旦总线号漂移,字符设备节点对应到错误的适配器上。
寄存器初始化冲突
有些i2c外设芯片的复位逻辑在引导阶段会被bootloader操作过一遍,内核驱动probe时如果重复复位,寄存器状态反而乱了,在驱动里加一个判断:如果芯片ID寄存器已经是预期值,跳过复位流程,这是修改引导设备驱动时最需要仔细的细节。
i2c设备驱动修改后如何验证引导影响
验证修改是否成功,不能只盯着字符设备节点,业内专家指出,引导阶段的设备驱动改动必须做双重确认:设备树匹配成功 + 应用层真实读写成功。
一个快速有效的方法是写一个小的initramfs脚本,在系统init进程启动前完成i2c设备自检:
#!/bin/sh
mount -t devtmpfs devtmpfs /dev
if [ -e /dev/my_i2c_dev ]; then
echo "I2C device ready in early boot"
# 读取设备的版本寄存器做自检
i2cget -y 2 0x38 0x00
else
echo "I2C device NOT ready"
exit 1
fi
把这个脚本放进initramfs的init脚本靠前位置,就能在引导阶段真实检验驱动修改是否满足了硬件设备的需求。
i2c设备驱动与字符设备驱动的适配关系
很多从事驱动开发的朋友经常在选型时纠结:既然i2c设备驱动已经很成熟了,为什么还要额外包一层字符设备驱动?在引导阶段这个场景下,答案很明确:i2c设备驱动不直接面向应用层,它解决的是内核里谁匹配谁的问题;字符设备驱动解决的是用户态怎么操作设备的问题。
引导阶段想通过简单的shell脚本操作外设,就必须有字符设备节点,修改引导的硬件设备驱动,本质上是把i2c设备驱动里能做的事,提前在字符设备驱动里曝光出来。
固定一个优先级判断:i2c设备驱动负责跟硬件沟通,字符设备驱动负责跟用户态沟通。 两者之间通过container_of和i2c_get_clientdata互相索引,在修改引导阶段的驱动时,这个索引关系要保证在内核的device_register和bus_add_driver之间完整建立。
常见问题与排查方法
为什么probe执行了但/dev节点不存在
可能是字符设备的device_create用了错误的父设备,检查device_create的parent参数是否传了&client->dev,如果传NULL,devtmpfs无法把它关联到i2c总线上,节点即使创建了也会被udev规则清理掉。
引导阶段二次probe导致资源冲突
如果驱动同时注册到了i2c核心和设备树中,可能触发两次probe,检查.id_table和of_match_table是否重复匹配了同一个设备,多数情况下,删掉其中一种匹配方式就能解决。
内核版本之间的API差异
Linux 6.x移除了部分老版本字符设备接口,如果你在移植旧驱动,register_chrdev仍然可用,但它分配的是静态设备号范围,更推荐用alloc_chrdev_region配合cdev_add,避免引导阶段设备号冲突,具体差异可以参考内核文档Documentation/driver-api/driver-model/下的说明。
Q&A模块中另一个典型问题是:修改i2c设备驱动后没有重新编译整个内核,只生成了.ko模块,引导阶段怎么加载?答案就是检查initramfs里是否包含了该模块,或者将模块用modprobe预加载到initramfs中,第三个问题关于i2c设备驱动如何添加调试打印,建议用dev_info和dev_err配合dynamic_debug控制,不要在内核配置里打开所有调试宏,引导阶段日志量越大,越难定位问题点。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/580920.html




