linux device driver development

    写一个 Linux I2C 传感器驱动:设备树、probe() 和那些浪费一天的错误

    一份能直接跑的 Linux I2C 设备驱动讲解:什么时候真的需要内核驱动、驱动骨架、设备树节点、regmap、延后探测,以及设备没出现时的排查清单。

    ·XTELL 工程团队

    先判断:你到底需不需要内核驱动

    很多 I2C 设备其实不需要内核驱动。Linux 通过 i2c-dev 接口把每一个 I2C 适配器暴露给用户空间,用 i2c-tools 工具或一个小程序就能读写寄存器。对于原型、一次性使用的传感器,或者只有你自己的应用会访问的设备,这是最快的路,而且你的代码出错时不会连累内核。

    出现下面任一情况时,才写内核驱动:

    • 设备会产生中断,需要低延迟处理。
    • 多个程序,或内核本身,需要共享这个设备,访问需要仲裁。
    • 设备属于现有子系统(传感器、输入、稳压器、RTC、音频、显示),产品应当使用它的标准接口。
    • 它有电源管理、时钟、稳压器或复位,需要和系统其他部分按顺序配合。

    对于传感器,在自己发明接口之前,先看一下工业 I/O(IIO)子系统。它已经为用户空间提供了统一的读取、缩放和缓冲采样的方式。

    最小 I2C 驱动长什么样

    下面是一个完整的最小驱动,对应一个假想的、寄存器为 8 位的传感器。它在 probe 时读取芯片标识,别的什么都不做,这正是第一步点亮想要的:在写任何功能代码之前,先证明总线、地址、供电和绑定都是通的。

    #include <linux/module.h>
    #include <linux/i2c.h>
    #include <linux/regmap.h>
    
    #define REG_ID 0x00
    
    static const struct regmap_config demo_regmap = {
    	.reg_bits = 8,
    	.val_bits = 8,
    };
    
    static int demo_probe(struct i2c_client *client)
    {
    	struct regmap *map;
    	unsigned int id;
    	int ret;
    
    	map = devm_regmap_init_i2c(client, &demo_regmap);
    	if (IS_ERR(map))
    		return PTR_ERR(map);
    
    	ret = regmap_read(map, REG_ID, &id);
    	if (ret)
    		return dev_err_probe(&client->dev, ret, "cannot read chip id\n");
    
    	dev_info(&client->dev, "chip id 0x%02x\n", id);
    	i2c_set_clientdata(client, map);
    	return 0;
    }
    
    static const struct of_device_id demo_of_match[] = {
    	{ .compatible = "acme,demo-sensor" },
    	{ }
    };
    MODULE_DEVICE_TABLE(of, demo_of_match);
    
    static struct i2c_driver demo_driver = {
    	.driver = {
    		.name = "demo-sensor",
    		.of_match_table = demo_of_match,
    	},
    	.probe = demo_probe,
    };
    module_i2c_driver(demo_driver);
    
    MODULE_LICENSE("GPL");

    有三个细节要注意。上面这种只有 client 一个参数的 probe 写法,是 6.3 及以后内核使用的,更老的内核签名不同,要看你编译所用的版本。devm_ 系列函数会在设备消失时自动释放资源,省掉大部分清理代码和大部分泄漏。regmap 负责寄存器访问、缓存和总线差异,同一份寄存器描述,将来这颗芯片提供 SPI 接口时也能复用。

    设备树节点

    驱动之所以能绑定到硬件,是因为设备树声明了这个硬件存在。节点放在它所连接的 I2C 控制器下面:

    &i2c1 {
    	status = "okay";
    	pinctrl-names = "default";
    	pinctrl-0 = <&i2c1_pins>;
    
    	sensor@44 {
    		compatible = "acme,demo-sensor";
    		reg = <0x44>;
    	};
    };

    compatible 字符串必须和驱动里的表逐字符完全一致。reg 是 7 位 I2C 地址,不含读写位,最常见的错误值,就是把数据手册里的 8 位地址直接抄了进来。供电、复位和中断线也属于这个节点,作为驱动读取的属性,这样硬件有变动时,改的是设备树,而不是代码。

    设备没出现时怎么办

    按顺序走完下面的清单,每一步都能排除一整类问题。

    1. 总线是不是活的?用正确的总线号运行 i2cdetect -y N。如果地址没有应答,那是硬件或供电问题,任何驱动都救不了:检查上拉电阻、电源、地址脚和电平。
    2. 控制器是否使能?它的节点要有 status = "okay",时钟和引脚复用要设置好。缺少 pinctrl,得到的会是一条存在却永远不工作的总线。
    3. 驱动匹配了吗?看 /sys/bus/i2c/devices/。设备在列表里但没有驱动链接,问题就在 compatible 字符串或模块上。对一次从未发生过的匹配,dmesg 不会说什么。
    4. probe 运行了吗,返回了什么?返回 -EPROBE_DEFER 表示驱动需要的某个资源(稳压器、时钟、GPIO)还没准备好,内核稍后会重试。像上面那样使用 dev_err_probe(),原因就会记录在 /sys/kernel/debug/devices_deferred 里,而不是凭空消失。
    5. 电源真的开着吗?如果没有任何节点声明需要某个稳压器,内核可能在启动后期把它关掉,而驱动其实已经运行过一次。请在设备节点上声明供电。

    动态调试(dyndbg)可以在运行时打开某个驱动的调试信息而无需重新编译,SCL 和 SDA 上的逻辑分析仪则能终结任何关于"线上到底跑了什么"的争论。

    怎么扩展驱动而不把它搞乱

    标识读对之后,按这个顺序加功能,每加一步就测试一次:先是通过正确子系统暴露的单次读取,再是中断,再是电源管理(运行时挂起),再是校准数据,最后才是可选模式。把寄存器表集中放在一处,不要在中断上下文里睡眠,并让驱动经得起被解绑再重新绑定:echo 1-0044 > /sys/bus/i2c/drivers/demo-sensor/unbind 是一个快速测试,能发现大部分资源处理上的错误。

    把数据手册里的时序数字当作需要验证的限值,而不是前提:长线缆、弱上拉带来的缓慢边沿、会拉伸时钟的芯片,都只会在真实的板子上才显现。

    在真实产品里的位置

    驱动只是整个板级点亮中的一层。我们的 i.MX6 CAN 总线数据采集系统就是这样一个 Linux 项目,其中真正有意思的是驱动周围的数据通路:高负载下的采集、解析和存储。如果你需要为自己的硬件做驱动、设备树,或完整的板级点亮,请看我们的 Linux BSP 与驱动服务,更完整的全景见点亮指南。

    需要专业服务?

    联系我们获取定制化解决方案和报价