ARTICLE DETAIL

资讯详情

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

OpenHarmony I2C驱动开发实战:从设备树配置到HDF读写与排障

OpenHarmony I2C驱动开发实战:从设备树配置到HDF读写与排障 1. 从一根线说起I2C 在 OpenHarmony 里到底扮演什么角色搞嵌入式开发的人迟早都会跟 I2C 打交道。不管你是调传感器、点屏幕、读 EEPROM还是接一颗小小的 RTC 芯片I2C 几乎无处不在。它只用两根线——一根时钟线 SCL一根数据线 SDA——就能挂上一堆设备这种“两根线走天下”的设计让它在板级内部通信里活得非常滋润。但问题也恰恰出在这里线少意味着协议层要承担更多责任一旦时序、地址、上拉电阻、设备树配置任何一环出问题你面对的往往不是“报个错”而是“毫无反应”。在 OpenHarmony 系统实战开发里I2C 的地位更加特殊。OpenHarmony 面向的是万物互联场景从智能家居面板、穿戴设备到工业控制终端、车载中控大量外设都是通过 I2C 挂载到主控上的。你写一个 HDF 驱动想让触摸屏工作、想读加速度计数据、想控制一颗功放芯片第一步往往就是把 I2C 通道打通。打不通后面所有上层应用都是空中楼阁。这篇内容我想从一个实际做过 OpenHarmony 外设适配的开发者角度把 I2C 这件事讲透。不是照本宣科地念协议手册而是把“怎么用”和“怎么排障”这两件事揉在一起说。因为在实际项目里这两件事根本分不开——你用得不对故障就来找你你排障排得多了自然就知道怎么用才稳。适合谁看如果你刚接触 OpenHarmony 驱动开发正在为设备树里那个 i2c 节点发愁或者你已经写过几个驱动但遇到 I2C 通信失败时只能靠“重启试试”“换根线试试”这种玄学手段那这篇内容应该能帮你把思路理清楚。我会从协议核心、设备树配置、HDF 驱动框架、实际读写流程一直讲到排障方法论和常见坑尽量做到你照着就能复现。2. I2C 协议核心别被“简单”两个字骗了2.1 两根线背后的电气逻辑I2C 的物理层看起来极其简单SCL 和 SDA 都是开漏输出必须外接上拉电阻。这个“必须”不是建议是硬性要求。很多新手第一次画板子忘了加上拉电阻或者上拉阻值选得离谱结果就是通信时好时坏甚至完全没波形。为什么是开漏因为 I2C 支持多主多从同一根线上可能挂了好几个设备。开漏输出意味着任何设备都只能把线拉低不能主动拉高。线要变高靠的是上拉电阻。这样一来就不会出现两个设备一个想拉高、一个想拉低导致短路的情况。这是 I2C 总线仲裁机制的电气基础。上拉电阻的阻值怎么选这跟总线电容和通信速率有关。标准模式 100kHz 下4.7kΩ 是常见值快速模式 400kHz 下通常用 2.2kΩ 到 4.7kΩ到了高速模式 3.4MHz可能要用到 1kΩ 甚至更小。但阻值不是越小越好太小会导致低电平时灌电流过大超过器件的驱动能力。经验公式是R(min) (VDD - VOL) / IOLR(max) tr / (0.8473 × Cb)。其中 tr 是上升时间要求Cb 是总线电容。实际选型时我一般先用 4.7kΩ 打底用示波器看上升沿如果太缓就减小如果低电平下不去就增大。注意总线电容不能超过 400pF这是 I2C 规范的上限。每挂一个设备、每长一截走线电容都会增加。如果你挂了七八个设备线又拉得长通信不稳定几乎是必然的。这时候要考虑用 I2C 多路复用器来分叉而不是硬扛。2.2 时序起始、停止、应答一个都不能少I2C 的协议层核心就是几种基本时序单元。起始条件START是 SCL 为高时SDA 从高变低停止条件STOP是 SCL 为高时SDA 从低变高。这两个条件必须由主机产生从机只负责识别。数据传输时SDA 上的数据必须在 SCL 高电平期间保持稳定因为接收方就是在 SCL 高电平期间采样。SDA 的变化只能发生在 SCL 低电平期间。这个规则如果被打破接收方就会把数据变化误判成起始或停止条件通信直接乱套。每传输 8 位数据后接收方要拉低 SDA 一个时钟周期这就是应答位ACK。如果接收方没有拉低就是非应答NACK。主机读数据时最后一个字节通常要发 NACK告诉从机“我读完了”然后发停止条件。很多 I2C 通信失败就是因为主机在读取时没有正确处理 NACK导致从机一直等着总线挂死。2.3 地址7 位还是 10 位别搞混I2C 设备地址有 7 位和 10 位两种。7 位地址最常用实际传输时是 7 位地址加 1 位读写方向位组成一个字节。比如一个设备的 7 位地址是 0x48写操作时发送 0x90读操作时发送 0x91。很多数据手册直接给 8 位地址你要自己判断它给的是“已经移位过的”还是“原始 7 位”。我见过太多人把 0x90 当成 7 位地址填进设备树结果怎么都不通。10 位地址用得少但有些大容量 EEPROM 会用。它的传输方式是先发一个特殊的保留地址 11110xx其中 xx 是 10 位地址的高两位然后再发剩下的 8 位。这种格式在驱动里需要特殊处理OpenHarmony 的 I2C HDI 接口对 10 位地址的支持要看具体实现。2.4 时钟拉伸从机也会“踩刹车”I2C 有一个很人性化的机制叫时钟拉伸Clock Stretching。当从机处理不过来时它可以主动把 SCL 拉低强制主机等待。主机必须检测 SCL 的实际电平如果发现被拉低了就不能继续发时钟要等从机释放。这个机制在低速设备上很常见比如某些温湿度传感器转换一次数据要几十毫秒它就会在转换期间拉低 SCL。如果主机驱动没有处理时钟拉伸就会在从机还没准备好时强行发时钟导致数据错误。OpenHarmony 的 I2C 控制器驱动通常会在硬件层面处理这个但如果你用 GPIO 模拟 I2C就必须在代码里手动检测。3. OpenHarmony 下的 I2C 设备树配置把硬件信息告诉内核3.1 设备树里 I2C 节点的基本结构在 OpenHarmony 的驱动框架里设备树Device Tree是硬件描述的核心。I2C 控制器节点通常在 SoC 的 dtsi 文件里已经定义好了你要做的是在板级 dts 里把它使能并挂上你的外设节点。以瑞芯微 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 disabled; };你要在板级 dts 里把 status 改成 okay然后添加你的设备子节点。比如挂一颗 GT911 触摸屏i2c1 { status okay; clock-frequency 400000; gt911: touchscreen5d { compatible goodix,gt911; reg 0x5d; interrupt-parent gpio0; interrupts RK_PB5 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio0 RK_PB6 GPIO_ACTIVE_LOW; irq-gpios gpio0 RK_PB5 GPIO_ACTIVE_HIGH; }; };这里有几个关键点。reg 属性就是 I2C 从机地址必须是 7 位格式不要移位。clock-frequency 是总线时钟频率标准模式 100000快速模式 400000。中断和复位 GPIO 的配置要根据实际硬件原理图来配错了触摸屏根本不会产生中断。3.2 地址冲突设备树里最容易踩的坑I2C 总线上每个设备的地址必须唯一。但现实是很多常用芯片的地址是固定的或者只有两三个可选值。比如 SSD1306 OLED 屏地址通常是 0x3C 或 0x3DAT24C02 EEPROM地址是 0x50 到 0x57 取决于 A0-A2 引脚。如果你在同一个总线上挂了两颗地址相同的芯片通信必然失败。排查地址冲突的方法很简单在设备树里只保留一个设备节点看是否能通然后逐个添加找到冲突的那个。或者用 i2c-tools 里的 i2cdetect 命令扫描总线看看哪些地址有响应。OpenHarmony 的 shell 里如果集成了 i2c-tools可以直接用如果没有可以自己交叉编译一个。提示有些设备的地址是通过引脚电平决定的比如 A0 接地是 0x50接 VCC 是 0x51。画原理图时就要规划好不要等到软件调试时才发现两个设备地址撞了。3.3 引脚复用与电气配置I2C 的 SCL 和 SDA 引脚在很多 SoC 上是复用的可能同时能当 GPIO、UART 或 PWM 用。设备树里的 pinctrl 节点就是用来配置引脚功能的。如果 pinctrl 配错引脚可能根本没切到 I2C 功能自然没有波形。以 RK3568 为例pinctrl 配置大概是这样i2c1_xfer: i2c1-xfer { rockchip,pins 0 RK_PB3 1 pcfg_pull_none_smt, 0 RK_PB4 1 pcfg_pull_none_smt; };这里的 1 表示复用功能编号具体值要查 SoC 手册。pcfg_pull_none_smt 表示不上拉、施密特触发输入。但 I2C 需要上拉这个上拉通常是在板级用外部电阻实现的而不是靠 SoC 内部上拉。内部上拉阻值太大通常几十 kΩ驱动能力不够。如果你发现波形上升沿特别缓先检查外部上拉电阻是否焊接、阻值是否合适。我遇到过一块板子上拉电阻焊成了 100kΩ结果 100kHz 都跑不稳换成 4.7kΩ 立刻正常。4. HDF 驱动框架下的 I2C 读写实战4.1 HDF I2C 接口概览OpenHarmony 的驱动框架 HDFHardware Driver Foundation对 I2C 做了统一封装。你不需要直接操作寄存器而是通过 HDF 提供的 I2C 接口来收发数据。核心接口包括I2cOpen打开 I2C 控制器获取句柄I2cClose关闭句柄I2cTransfer执行一次传输可以包含多个消息I2cTransferWithLock带锁的传输多线程环境下用这些接口定义在hdf_i2c_if.h里。实际使用时你需要在驱动入口里注册 I2C 设备然后在服务层调用这些接口。4.2 一次完整的 I2C 读写流程假设我们要读一颗 AS5600 磁编码器的角度寄存器。AS5600 的 7 位地址是 0x36角度值在寄存器 0x0C 和 0x0D。写寄存器地址的流程是发起始条件发设备地址加写位0x36 1 | 0 0x6C等 ACK发寄存器地址 0x0C等 ACK发停止条件。然后读数据的流程是发起始条件发设备地址加读位0x36 1 | 1 0x6D等 ACK读两个字节第一个字节回 ACK第二个字节回 NACK发停止条件。在 HDF 里这两步可以合并成一次 I2cTransfer用两个消息msg表示struct I2cMsg msgs[2]; uint8_t regAddr 0x0C; uint8_t data[2] {0}; msgs[0].addr 0x36; msgs[0].flags 0; // 写 msgs[0].len 1; msgs[0].buf regAddr; msgs[1].addr 0x36; msgs[1].flags I2C_FLAG_READ; msgs[1].len 2; msgs[1].buf data; int32_t ret I2cTransfer(i2cHandle, msgs, 2); if (ret ! 2) { HDF_LOGE(I2C transfer failed, ret%d, ret); }这里 ret 返回的是成功传输的消息数量如果是 2 就表示两个消息都成功了。如果返回负数就是出错了。4.3 读写 EEPROM 的注意事项EEPROM 的读写跟普通传感器不太一样。写 EEPROM 时发完数据后设备需要内部擦写时间通常是 5ms 左右。在这段时间内设备不会响应任何 I2C 命令。如果你紧接着发下一个写命令会收到 NACK。正确的做法是写完一个字节或一页后等待 5ms 以上或者用“应答轮询”的方式——反复发起始条件和设备地址直到收到 ACK 为止。应答轮询更高效因为实际擦写时间可能小于 5ms。另外EEPROM 有页写限制。比如 AT24C02 每页 8 字节如果你一次写超过 8 字节且跨页地址会回卷到页首覆盖之前的数据。所以写 EEPROM 时要按页对齐分多次写。// 按页写 EEPROM 的伪代码 #define PAGE_SIZE 8 for (int i 0; i len; i PAGE_SIZE) { int chunk (len - i) PAGE_SIZE ? PAGE_SIZE : (len - i); write_eeprom(addr i, buf i, chunk); usleep(6000); // 等待擦写完成 }4.4 中断与轮询什么时候用哪种I2C 设备的数据就绪通知有两种方式中断和轮询。触摸屏、加速度计通常用中断因为数据就绪是异步事件轮询会浪费 CPU。而 EEPROM、RTC 这类设备通常用轮询就够了。在 OpenHarmony 的 HDF 里中断通过 GpioInterruptEnable 等接口注册。你需要在设备树里配好中断 GPIO然后在驱动初始化时注册中断处理函数。中断处理函数里不要做耗时操作通常只是发个信号量或消息让工作队列去读 I2C。轮询的话可以在定时器或工作队列里周期性调用 I2cTransfer。轮询周期要根据设备的数据更新率来定太快浪费 CPU太慢丢数据。注意I2C 传输本身是同步阻塞的如果在中断上下文里直接调用 I2cTransfer可能会导致睡眠这是不允许的。一定要把 I2C 读写放到工作队列或内核线程里。5. 排障实战I2C 通信失败怎么一步步定位5.1 先看波形再谈代码I2C 不通第一件事不是改代码而是拿示波器或逻辑分析仪看波形。这是最直接的证据。你看 SCL 和 SDA 上有没有波形波形幅度对不对上升沿是否太缓有没有毛刺。如果 SCL 完全没有波形说明控制器根本没工作。检查设备树 status 是否 okay时钟是否使能引脚复用是否正确。如果 SCL 有波形但 SDA 一直高说明主机发了地址但没有从机应答。检查从机地址是否正确从机是否供电从机是否复位。如果 SDA 被一直拉低说明总线可能挂死了。这种情况通常是某个从机在通信中途出错一直拉着 SDA 不放。解决办法是主机发送 9 个时钟脉冲让从机把剩余的数据位发完然后发停止条件。如果还不行只能硬件复位。5.2 常见故障速查表现象可能原因排查方法SCL 无波形控制器未使能、时钟未开、引脚复用错误检查设备树 status、clock、pinctrlSCL 有波形SDA 一直高从机地址错误、从机未供电、从机复位中用 i2cdetect 扫描、量从机电压、检查复位引脚SDA 一直被拉低总线挂死、从机故障发 9 个时钟脉冲、断电重启通信时好时坏上拉电阻不合适、总线电容过大、干扰换小阻值上拉、缩短走线、加屏蔽读数据全 0 或全 FF寄存器地址错误、读时序错误查数据手册、用逻辑分析仪看时序写数据后读回不一致EEPROM 未等待擦写、页写回卷加延时、按页写中断不触发中断 GPIO 配错、中断触发方式错误检查设备树、量中断引脚电平5.3 逻辑分析仪抓包分析实例有一次我调一颗 GT911 触摸屏I2C 地址扫描能扫到 0x5D但读寄存器一直失败。用逻辑分析仪抓包发现主机发了设备地址和寄存器地址后从机回了 ACK但主机在读数据时第一个字节还没读完就发了停止条件。后来查代码发现I2cTransfer 的消息数组里读消息的 len 设成了 1但实际上要读两个字节。改成 2 之后正常。这种问题看代码很难发现但看波形一目了然。逻辑分析仪的好处是能解码 I2C 协议直接显示地址、数据、ACK/NACK。我用的是一款几十块钱的 8 通道逻辑分析仪配合开源软件抓 I2C 足够用了。采样率至少要是总线频率的 10 倍以上400kHz 的 I2C 建议用 10MHz 以上采样率。5.4 设备树调试技巧设备树配错了内核启动时可能不会报错但设备就是不通。有几个调试手段第一看/proc/device-tree下的节点。OpenHarmony 内核启动后你可以通过 shell 查看设备树是否被正确解析。比如ls /proc/device-tree/i2cfe5a0000/看看你的设备子节点在不在。第二看内核日志。用dmesg | grep i2c过滤 I2C 相关日志。如果控制器初始化失败或者设备探测失败通常会有报错。第三用i2cdetect -l列出所有 I2C 总线i2cdetect -y 1扫描总线 1 上的设备。如果扫描不到你的设备说明硬件或设备树有问题。提示有些 SoC 的 I2C 控制器需要配置 DMA如果 DMA 配置错误小数据量传输可能正常大数据量就失败。遇到这种情况可以先在设备树里禁用 DMA 试试。6. 进阶话题多路复用、自由数据模式与性能优化6.1 I2C 多路复用器解决地址冲突当总线上设备太多或者地址冲突无法通过引脚配置解决时I2C 多路复用器如 TCA9548A就是救星。它把一条 I2C 总线分成 8 条子总线你可以通过写多路复用器的寄存器来选择当前激活哪条子总线。在设备树里多路复用器本身是一个 I2C 设备它的子总线下面再挂其他设备i2c1: i2cfe5a0000 { status okay; tca9548: mux70 { compatible ti,tca9548; reg 0x70; #address-cells 1; #size-cells 0; i2c1_0: i2c0 { #address-cells 1; #size-cells 0; reg 0; gt911: touchscreen5d { compatible goodix,gt911; reg 0x5d; }; }; }; };这样GT911 就挂在多路复用器的通道 0 上。驱动在访问 GT911 之前需要先写 TCA9548A 的寄存器选择通道 0。OpenHarmony 的 I2C 框架对多路复用器的支持需要看具体版本有些版本需要自己在驱动里手动切换通道。6.2 I2C 自由数据模式有些 SoC 的 I2C 控制器支持“自由数据模式”Free Data Mode也叫“无地址模式”。在这种模式下控制器不发送标准的起始条件和地址而是直接收发数据时序完全由软件控制。这种模式通常用于跟一些非标准 I2C 设备通信或者实现自定义协议。在 OpenHarmony 里如果 HDF 的 I2C 接口不支持自由数据模式你可能需要直接操作控制器寄存器或者用 GPIO 模拟。GPIO 模拟 I2C 的好处是时序完全可控缺点是占用 CPU速率上不去。如果只是偶尔发几个字节GPIO 模拟完全够用。6.3 性能优化批量传输与 DMAI2C 的速率本身不高标准模式 100kHz快速模式 400kHz高速模式 3.4MHz。如果你要传输大量数据比如升级固件、读写大容量 EEPROMI2C 会成为瓶颈。优化手段有几个第一提高总线频率。如果从机支持 400kHz就不要用 100kHz。第二使用 DMA。很多 SoC 的 I2C 控制器支持 DMA可以在不占用 CPU 的情况下传输数据。第三合并消息。如果多个读写操作可以合并成一次 I2cTransfer就合并减少起始和停止条件的开销。但要注意提高频率会增加总线电容的影响走线要尽量短上拉电阻要相应减小。DMA 配置错误可能导致数据错位调试时可以先禁用 DMA确认基本功能正常后再开启。7. 我踩过的那些坑与实操心得第一个坑设备树里 reg 属性填了 8 位地址。我一开始把数据手册上的 0x90 直接填进 reg结果内核报“invalid address”设备根本探测不到。后来才明白reg 要填 7 位地址 0x48。这个错误很低级但新手很容易犯。第二个坑上拉电阻焊错。有一块板子硬件工程师把上拉电阻焊成了 10kΩ100kHz 下勉强能通400kHz 下就频繁出错。换成 2.2kΩ 后稳定。所以画板子时I2C 上拉电阻最好留可调位置或者准备几个不同阻值的样品。第三个坑中断 GPIO 配成了输出。GT911 的中断引脚是输出给主机的设备树里应该配成输入。我一开始配成了输出结果触摸屏一直不产生中断。后来改成GPIO_ACTIVE_HIGH并且不设置输出方向才正常。第四个坑I2C 传输没有加锁。在多线程环境下两个线程同时调用 I2cTransfer会导致消息交错通信失败。后来改用I2cTransferWithLock问题解决。如果你的驱动会在多个线程里访问同一个 I2C 设备一定要加锁。第五个坑EEPROM 写后立即读。写完 EEPROM 后没有等待擦写完成立即读回结果读到旧数据。后来加了 5ms 延时或者用应答轮询才稳定。最后分享一个小技巧如果你不确定 I2C 设备是否正常工作可以先在 U-Boot 里用i2c probe和i2c md命令测试。U-Boot 的 I2C 驱动通常比内核简单如果 U-Boot 里能通说明硬件没问题问题在内核配置或驱动。如果 U-Boot 里也不通那就是硬件或引脚问题。这个分界线能帮你快速缩小排查范围。I2C 这个东西说简单也简单两根线而已说复杂也复杂时序、地址、上拉、设备树、驱动框架每一环都能让你卡半天。但只要你掌握了波形分析这个核心手段再配合设备树和驱动的系统化排查绝大多数问题都能定位。希望这些经验能帮你少走点弯路。
返回列表