I2C设备驱动开发:从一次诡异的寄存器读取说起
2026/4/18 8:31:21 网站建设 项目流程

上周调试一块传感器板子,遇到个怪事:用i2c-tools读取设备地址0x68的0x00寄存器,每次返回的值都不一样,有时是0x3F,有时是0x00,甚至偶尔会超时。但用逻辑分析仪抓波形,发现SCL/SDA信号明明很干净。这个坑让我重新梳理了I2C驱动的那些细节,今天咱们就聊聊这个看似简单实则暗藏玄机的总线。

I2C驱动的三层结构

Linux的I2C框架分三层。最上层是设备驱动,就是我们平时写的那个struct i2c_driver。中间层是核心层,内核已经实现好了,负责匹配设备和驱动。最下层是控制器驱动,也就是适配器驱动,这个一般芯片原厂会提供。我们写驱动主要关注上层,但有时候问题出在中间层甚至下层。

staticstructi2c_drivermy_sensor_driver={.driver={.name="my_sensor",.owner=THIS_MODULE,},.probe=my_sensor_probe,// 设备匹配成功时调用.remove=my_sensor_remove,// 设备移除时调用.id_table=my_sensor_ids,// 设备ID匹配表};

这里有个细节:.probe函数不一定只调用一次。如果系统里有多个同型号设备,每个设备都会触发一次probe。所以里面的资源分配要小心,别写成全局变量。

读写寄存器的那些坑

开头说的那个问题,最后发现是时序问题。虽然逻辑分析仪看波形正常,但设备对tSU:DAT(数据建立时间)要求比较苛刻。先看一个典型的错误写法:

// 错误示例:直接连续调用staticintread_reg(structi2c_client*client,u8 reg,u8*val){intret;ret=i2c_master_send(client,&reg,1);// 发送寄存器地址if(ret<0)returnret;ret=i2c_master_recv(client,val,1);// 读取数据returnret;}

这种写法在大多数情况下能工作,但遇到挑剔的设备就容易出问题。两个i2c传输之间没有停止条件,有些设备需要完整的“起始-地址-寄存器-停止”序列,然后再来一次“起始-地址-数据-停止”。应该用i2c_transfer:

// 推荐写法:使用i2c_transferstaticintread_reg(structi2c_client*client,u8 reg,u8*val){intret;structi2c_msgmsg[2];msg[0].addr=client->addr;msg[0].flags=0;// 写标志msg[0].len=1;msg[0].buf=&reg;msg[1].addr=client->addr;msg[1].flags=I2C_M_RD;// 读标志msg[1].len=1;msg[1].buf=val;ret=i2c_transfer(client->adapter,msg,2);if(ret<0){dev_err(&client->dev,"读寄存器0x%02x失败: %d\n",reg,ret);returnret;}return0;}

注意msg[0].flags = 0表示写操作,不是写数据的意思,是写设备地址(带写标志位)。这个参数名容易误解。

设备树的配置艺术

现在驱动都往设备树靠拢。一个典型的I2C设备节点长这样:

&i2c1 { status = "okay"; clock-frequency = <100000>; // 标准模式100kHz my_sensor: sensor@68 { compatible = "vendor,my-sensor"; reg = <0x68>; // 7位设备地址 vdd-supply = <&vdd_3v3>; // 电源引用 interrupt-parent = <&gpio>; interrupts = <5 IRQ_TYPE_EDGE_FALLING>; // 中断引脚 reset-gpios = <&gpio 6 GPIO_ACTIVE_LOW>; }; };

这里容易踩的坑是地址:设备树里写的是7位地址,不包含读写位。有些芯片手册给的是8位地址(包含了读写位),需要右移一位。比如手册说写地址0xD0,读地址0xD1,那么设备树里应该填0x68(0xD0 >> 1)。

并发访问与电源管理

I2C总线是共享的,多个设备(甚至同一设备的多个操作)可能竞争。内核已经做了很多保护,但自己写驱动时还是要注意:

// 在probe函数里初始化互斥锁staticintmy_sensor_probe(structi2c_client*client){structmy_sensor_data*data;data=devm_kzalloc(&client->dev,sizeof(*data),GFP_KERNEL);mutex_init(&data->lock);// 初始化互斥锁// ... 其他初始化}// 在需要保护的操作中使用staticssize_tread_value_show(structdevice*dev,structdevice_attribute*attr,char*buf){structmy_sensor_data*data=dev_get_drvdata(dev);intval;mutex_lock(&data->lock);val=read_reg(data->client,REG_VALUE);mutex_unlock(&data->lock);returnsprintf(buf,"%d\n",val);}

电源管理容易被忽略。设备可能进入休眠,唤醒后需要重新初始化:

staticintmy_sensor_resume(structdevice*dev){structmy_sensor_data*data=dev_get_drvdata(dev);// 重新配置寄存器,不是所有设备都能保持状态write_reg(data->client,REG_CONFIG,DEFAULT_CONFIG);msleep(10);// 等设备稳定,这个时间看具体器件return0;}

调试技巧:不用逻辑分析仪也能定位问题

不是每个人都有逻辑分析仪。遇到I2C通信失败时,可以这样排查:

  1. 先看/sys/bus/i2c/devices/下有没有你的设备节点。没有的话,可能是设备树没匹配或者probe失败了。

  2. i2cdetect -y 1(假设是i2c-1总线)扫描设备。能看到地址但读写出错,可能是时序或电源问题。

  3. 打开内核动态调试:echo 1 > /sys/module/i2c_core/parameters/debug,能看到所有I2C传输的日志。

  4. 检查电源和上拉电阻。I2C总线需要上拉,通常4.7kΩ。电压不匹配也会导致通信不稳定。

  5. 注意设备的工作电压。有些传感器是1.8V电平,直接接到3.3V的I2C总线可能能通信但不可靠。

个人经验谈

做了这么多年嵌入式,I2C是我又爱又恨的总线。简单的时候真简单,复杂的时候能让你怀疑人生。几个血泪教训:

第一,数据手册的时序图要仔细看,特别是那些小字注释。tSU:STA、tHD:STA这些参数,虽然大多数设备不挑剔,但遇到挑剔的就得调适配器的时钟延展。

第二,上拉电阻不能随便选。总线电容大的时候要用小一点的电阻(比如2.2kΩ),但要注意驱动能力。长距离传输时,可以适当减小上拉电阻值。

第三,I2C设备地址冲突很常见。特别是使用多个同型号传感器时,要确认芯片有没有地址选择引脚。有些芯片的地址选择引脚内部有弱上拉,悬空和接地可能不一样。

第四,probe函数里加个版本寄存器读取。很多传感器都有WHO_AM_I之类的寄存器,probe时读一下确认是不是预期的值。这个习惯帮我抓过好几次硬件贴错芯片的问题。

最后,真正复杂的不是写驱动,是调试那些若隐若现的问题。有时候加热风枪吹一下板子问题就出来了,有时候用手摸一下GND就好了。这种玄学问题,多半是电源或地线的问题。记住:I2C是开漏输出,靠上拉电阻到高电平,如果电源不稳,一切都白搭。

下次遇到I2C问题,先别急着改代码,量量电源纹波,看看地线是否扎实。这些基础的东西,比任何驱动技巧都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询