ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenHarmony I2C驱动开发实战:从协议原理到排障

OpenHarmony I2C驱动开发实战:从协议原理到排障 1. 从一根线说起I2C 到底解决了什么问题搞嵌入式开发的人迟早都会跟 I2C 打交道。你接一个温湿度传感器、一块 OLED 屏、一颗 EEPROM甚至一个手势识别模块翻看它们的规格书十有八九通信接口那一栏写着 I2C。为什么偏偏是它说白了就一个字省。省引脚省布线省成本。I2C 只用两根线——一根 SCL 时钟线一根 SDA 数据线就能把几十个设备串在同一条总线上。每个设备有自己的地址主机喊谁的地址谁就应答其他人闭嘴。这就像一条对讲机频道大家共用同一个频率但靠呼叫编号来区分对象。对比 SPI 的片选线一根一根拉、UART 只能点对点I2C 在多传感器场景下的优势非常明显。但省归省I2C 也是出了名的“容易翻车”。时序不对、上拉电阻选错、地址冲突、总线被拉死、设备树配错……随便一个环节出问题现象都是“读不到数据”。更让人头疼的是它不像 UART 那样能直接看串口打印也不像 SPI 那样逻辑分析仪一抓就清楚很多时候你得靠示波器、逻辑分析仪加上对协议帧格式的理解才能定位到根因。这篇内容围绕 OpenHarmony 系统下的 I2C 实战展开从协议原理、设备树配置、驱动调用到实际排障方法把这条总线从“会用”讲到“会修”。不管你是刚接触 OpenHarmony 驱动开发的新手还是从 STM32 裸机转过来的老手都能从中找到可以直接复用的操作步骤和踩坑经验。2. I2C 协议核心机制拆解2.1 两根线怎么传数据起始、应答与停止I2C 的物理层很简单SCL 和 SDA 都是开漏输出必须外接上拉电阻到电源正极。开漏的好处是支持多设备共享总线——任何设备都可以把线拉低但没人能强行拉高高电平靠上拉电阻恢复。这就从电气层面避免了两个设备同时输出高和低导致的短路。协议层面一次完整的 I2C 传输由几个关键信号组成起始条件StartSCL 保持高电平时SDA 从高变低。这个特殊组合不会出现在正常数据传输中所以被用作“我要开始通信了”的标志。地址帧起始条件之后主机发送 7 位从机地址加 1 位读写方向位。比如某传感器地址是 0x48写操作就是 0x90读操作就是 0x91。应答位ACK/NACK每发送完 8 位数据接收方需要在第 9 个时钟周期把 SDA 拉低表示应答ACK保持高表示非应答NACK。主机靠这个判断从机是否在线、是否成功接收。停止条件StopSCL 保持高电平时SDA 从低变高表示本次传输结束。注意起始和停止条件只能由主机产生。从机如果检测到异常时序可能会进入不确定状态这时候需要发送 9 个时钟脉冲来复位总线。2.2 时钟同步与仲裁多主机场景下的隐形规则虽然大多数嵌入式场景是单主机但 I2C 协议本身支持多主机。当两个主机同时发起传输时靠“线与”逻辑做仲裁谁先发出低电平谁就赢得总线输的那个自动退出。时钟同步也是类似原理SCL 的实际频率由所有主机中周期最长的那个决定。这个机制在实际项目中很少触发但理解它有助于排查一些诡异现象。比如总线上某个设备异常拉低 SCL会导致整个总线时钟停摆所有通信卡死。这时候用示波器看 SCL 是否一直被拉低就能快速判断是哪个设备出了问题。2.3 标准模式、快速模式与高速模式的区别I2C 根据速率分为几个档位模式最高速率典型上拉电阻适用场景标准模式100 kbps4.7kΩ低速传感器、EEPROM快速模式400 kbps2.2kΩOLED、多数消费级传感器快速模式1 Mbps1kΩ高速数据采集高速模式3.4 Mbps特殊驱动专业级应用上拉电阻的选择跟速率直接相关。速率越高总线电容充放电时间要求越短上拉电阻就得越小。但电阻太小又会导致功耗增加低电平时的灌电流可能超过器件的承受能力。一般 3.3V 系统下400kbps 用 2.2kΩ 到 4.7kΩ 都比较常见具体要看总线电容和器件手册推荐值。3. OpenHarmony 下的 I2C 设备树配置实战3.1 设备树是什么给内核一张硬件地图在 OpenHarmony 的驱动开发中设备树Device Tree是绕不开的一环。它的作用用一句话概括把硬件信息从代码里剥离出来用配置的方式告诉内核“板子上有什么、接在哪、怎么访问”。以前写裸机驱动引脚号、寄存器地址都是硬编码在 C 文件里的。换一块板子就得改代码、重新编译。设备树把这些信息抽出来驱动代码只负责逻辑硬件描述放在.dts或.dtsi文件里。内核启动时解析设备树把对应的硬件资源注册成设备节点驱动通过匹配规则找到自己的节点再从中读取寄存器地址、中断号、时钟频率等参数。对于 I2C 来说设备树需要描述两层信息一是 I2C 控制器本身比如 RK3568 上有几个 I2C 控制器、寄存器基地址是多少、时钟源是什么二是挂在控制器下面的从设备地址是多少、用哪个驱动、有没有中断引脚。3.2 RK3568 平台 I2C 节点配置示例以瑞芯微 RK3568 为例I2C 控制器的设备树节点通常长这样i2c1: i2cfe5a0000 { compatible rockchip,rk3568-i2c; reg 0x0 0xfe5a0000 0x0 0x1000; interrupts GIC_SPI 51 IRQ_TYPE_LEVEL_HIGH; clocks cru CLK_I2C1, cru PCLK_I2C1; clock-names i2c, pclk; pinctrl-names default; pinctrl-0 i2c1_xfer; #address-cells 1; #size-cells 0; status okay; };几个关键字段需要重点关注compatible驱动匹配字符串内核靠它找到对应的 I2C 控制器驱动。reg控制器的寄存器基地址和范围。clocks时钟源I2C 通信速率依赖正确的时钟配置。pinctrl-0引脚复用配置把对应的 GPIO 切换成 I2C 功能。statusokay表示启用disabled表示关闭。从设备节点挂在控制器下面i2c1 { status okay; eeprom50 { compatible atmel,24c02; reg 0x50; pagesize 8; }; sensor48 { compatible ti,tmp102; reg 0x48; interrupt-parent gpio0; interrupts 12 IRQ_TYPE_EDGE_FALLING; }; };这里的reg 0x50就是从设备地址。注意设备树里的地址是 7 位格式不包含读写位。有些规格书给的是 8 位地址比如 0xA0需要右移一位才是设备树里填的值。3.3 引脚复用与上拉配置的注意事项I2C 的引脚配置有两个容易忽略的点。第一是引脚复用很多 SoC 的 GPIO 默认功能不是 I2C需要在 pinctrl 里显式切换。第二是内部上拉部分 SoC 的 I2C 引脚支持内部上拉但内部上拉电阻通常较大几十 kΩ在 400kbps 速率下可能不够用还是需要外部上拉电阻。实操心得调试新板子时先用万用表量一下 SCL 和 SDA 对地的电阻。如果只有几十 kΩ 且没有外部上拉大概率通信会不稳定。正常应该看到 2kΩ 到 10kΩ 左右的上拉。另外如果总线上挂了多个设备要确认所有设备的供电电压一致。3.3V 设备和 5V 设备混挂轻则通信失败重则损坏器件。必要时加电平转换芯片。4. OpenHarmony I2C 驱动调用与 HDI 接口4.1 HDI 层是什么驱动与框架的桥梁OpenHarmony 的硬件驱动框架分了几层HDIHardware Device Interface是其中承上启下的部分。它定义了一套标准接口上层服务通过 HDI 调用底层硬件底层驱动实现这些接口。这样做的好处是上层业务不用关心具体硬件是什么换平台只要换驱动实现就行。对于 I2CHDI 提供了打开控制器、读写数据、配置速率等接口。驱动开发者需要实现这些接口把请求翻译成对具体 I2C 控制器的操作。4.2 用户态访问 I2C 的完整流程在 OpenHarmony 中用户态程序访问 I2C 设备一般走以下流程打开 I2C 控制器设备节点通常是/dev/i2c-1这样的路径数字对应控制器编号。设置从设备地址通过 ioctl 调用I2C_SLAVE命令告诉内核接下来要跟哪个地址通信。构造数据帧并读写使用read()/write()或者ioctl的I2C_RDWR命令进行组合读写。关闭设备节点通信完成后释放资源。一个典型的读寄存器操作需要先写寄存器地址再读数据。这两步之间不能插入停止条件否则很多设备会认为事务结束。所以要用I2C_RDWR组合消息struct i2c_msg msgs[2]; char reg_addr 0x00; char data[2]; msgs[0].addr 0x48; msgs[0].flags 0; msgs[0].len 1; msgs[0].buf reg_addr; msgs[1].addr 0x48; msgs[1].flags I2C_M_RD; msgs[1].len 2; msgs[1].buf data; struct i2c_rdwr_ioctl_data rdwr; rdwr.msgs msgs; rdwr.nmsgs 2; ioctl(fd, I2C_RDWR, rdwr);这段代码的关键在于nmsgs 2内核会把两个消息合并成一次传输起始、写地址、写寄存器、重复起始、读地址、读数据、停止。中间不插入停止条件符合绝大多数传感器的读时序要求。4.3 内核态驱动注册 I2C 设备的步骤如果要在内核态写驱动流程略有不同定义i2c_driver结构体填充probe、remove、id_table等字段。在probe函数中获取i2c_client通过i2c_smbus_read_byte_data等接口读写寄存器。用module_i2c_driver宏注册驱动。设备树中的compatible字符串要与id_table中的一致否则probe不会被调用。static const struct i2c_device_id tmp102_id[] { { tmp102, 0 }, { } }; static int tmp102_probe(struct i2c_client *client, const struct i2c_device_id *id) { int ret; ret i2c_smbus_read_byte_data(client, 0x00); if (ret 0) { dev_err(client-dev, read failed\n); return ret; } return 0; } static struct i2c_driver tmp102_driver { .driver { .name tmp102, .of_match_table tmp102_of_match, }, .probe tmp102_probe, .id_table tmp102_id, }; module_i2c_driver(tmp102_driver);注意probe函数里不要做耗时操作否则会阻塞其他设备的初始化。需要延迟处理的逻辑放到工作队列或定时器里。5. I2C 排障实战从现象到根因5.1 读不到数据先查这五个地方I2C 通信失败是最常见的问题现象可能是read返回 -1也可能是读到的数据全是 0xFF 或 0x00。按以下顺序排查能覆盖大部分情况确认设备地址用i2cdetect工具扫描总线看目标地址是否出现。如果没出现说明设备没上电、地址不对或者接线有问题。检查上拉电阻用示波器看 SCL 和 SDA 的 idle 电平。如果不是高电平说明上拉缺失或阻值过大。看波形质量上升沿是否太缓下降沿是否有过冲波形畸变严重会导致采样错误。确认时钟频率有些设备不支持 400kbps只能跑 100kbps。速率不匹配会表现为偶发失败。检查设备树配置status是否为okay引脚复用是否正确地址是否与规格书一致5.2 总线被拉死如何复位与恢复总线被拉死是 I2C 最棘手的问题之一。现象是 SCL 或 SDA 一直被某个设备拉低主机无法发起新的传输。常见原因是从机在传输过程中被复位状态机卡在某个中间态一直等待时钟但主机已经放弃了。恢复方法有两种。软件方式是在 SDA 释放的情况下手动翻转 SCL 九个周期让从机把剩余的数据位发完然后发送停止条件。硬件方式是给从机断电重启或者用 GPIO 强制拉高 SCL 一段时间。在 OpenHarmony 中可以通过i2c-gpio的方式模拟时序来恢复void i2c_bus_recover(int scl_gpio, int sda_gpio) { gpio_direction_output(sda_gpio, 1); for (int i 0; i 9; i) { gpio_direction_output(scl_gpio, 0); udelay(5); gpio_direction_output(scl_gpio, 1); udelay(5); } gpio_direction_output(sda_gpio, 0); udelay(5); gpio_direction_output(scl_gpio, 1); udelay(5); gpio_direction_output(sda_gpio, 1); udelay(5); }实操心得如果总线频繁被拉死优先检查从设备的电源是否稳定。电源纹波大或者上电时序不对容易导致从机状态机异常。5.3 逻辑分析仪抓包分析实例逻辑分析仪是 I2C 排障的利器。抓包时重点看几个地方起始条件是否正常产生地址帧发出后从机有没有回 ACK数据帧的每一位是否稳定停止条件是否完整如果地址帧后没有 ACK说明从机没有响应。可能原因包括地址错误、从机未上电、从机忙。如果数据帧中间出现 NACK可能是从机缓冲区满或者寄存器地址越界。解码时注意区分 7 位地址和 8 位地址。逻辑分析仪通常显示的是 8 位格式含读写位比如显示 0x90实际 7 位地址是 0x48。5.4 常见问题速查表现象可能原因排查方法扫描不到设备供电异常、地址错误、接线松动万用表量电压确认地址检查焊点偶发读写失败上拉电阻过大、时钟频率过高减小上拉电阻降低速率总线一直被拉低从机状态机异常、电源不稳九脉冲复位检查电源纹波读到的数据全 0xFF从机未响应、寄存器地址错误逻辑分析仪抓包确认地址和寄存器读到的数据全 0x00从机复位中、数据未准备好增加延时检查从机初始化流程多设备冲突地址重复、电平不匹配逐个挂载测试加电平转换6. 进阶话题多路复用与软件 I2C6.1 I2C 多路复用器的配置方法当总线上设备太多或者有多个相同地址的设备时就需要 I2C 多路复用器。它的作用像一个开关把一条总线分成多路同一时刻只导通其中一路。常见芯片如 TCA9548A支持 8 路输出。配置时多路复用器本身也是一个 I2C 设备有自己的地址。主机先向它写控制字节选择要导通的路然后再跟目标设备通信。在设备树中多路复用器下面的设备需要嵌套描述i2c_mux: mux70 { compatible nxp,pca9548; reg 0x70; #address-cells 1; #size-cells 0; channel0: i2c0 { reg 0; #address-cells 1; #size-cells 0; sensor48 { compatible ti,tmp102; reg 0x48; }; }; };这样内核在访问sensor48时会自动先切换到通道 0。6.2 软件模拟 I2C 的适用场景有些情况下硬件 I2C 控制器不够用或者引脚被其他功能占用就需要用 GPIO 模拟 I2C 时序。软件 I2C 的优点是灵活任何 GPIO 都能用缺点是占用 CPU 资源速率上不去通常只能跑 100kbps 以下。在 OpenHarmony 中可以用i2c-gpio驱动来实现软件 I2C设备树里配置i2c_gpio: i2c-gpio { compatible i2c-gpio; sda-gpios gpio0 12 GPIO_ACTIVE_HIGH; scl-gpios gpio0 13 GPIO_ACTIVE_HIGH; i2c-gpio,delay-us 5; #address-cells 1; #size-cells 0; };delay-us控制半周期延时5us 对应大约 100kbps。这个值要根据实际波形调整太小会导致时序不满足规格书要求。注意软件 I2C 在中断上下文或高负载场景下容易丢时序关键通信还是优先用硬件控制器。7. 几个容易踩的坑和我的处理习惯调试 I2C 这些年有几个坑我反复踩过也总结了一些处理习惯。第一个坑是地址混淆。规格书上的地址有的是 7 位有的是 8 位还有的把读写位算进去。我的习惯是拿到规格书先确认地址格式然后在设备树和代码里统一用 7 位格式避免来回换算出错。第二个坑是上拉电阻照搬参考设计。参考设计用的是 4.7kΩ但我的板子总线走线长、挂了更多设备电容大了4.7kΩ 上升沿太缓。后来用示波器量了上升时间换成 2.2kΩ 就稳了。所以上拉电阻要根据实际总线和速率算不能无脑抄。第三个坑是忽略电源时序。有些传感器要求电源稳定后延时一段时间才能通信如果驱动 probe 太快第一次读写必然失败。我的做法是在 probe 里加一个msleep(10)到msleep(50)的延时等设备内部初始化完成。第四个坑是设备树 status 忘了改。默认很多控制器是disabled配置完忘了改成okay结果驱动一直不 probe。现在我的检查清单里第一条就是确认 status。第五个坑是逻辑分析仪采样率不够。400kbps 的 I2C采样率至少要 10MHz 以上才能看清细节。早期用低采样率的分析仪波形全是锯齿根本没法判断时序。这些经验说起来简单但每一个都是实际调试中花时间换来的。I2C 本身不复杂复杂的是硬件环境的不确定性和多设备共存的边界情况。把协议吃透把工具用熟再养成系统化的排查习惯大部分问题都能在半小时内定位到根因。
返回列表