尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

OpenHarmony I2C驱动开发实战:从设备树配置到排障全解析

OpenHarmony I2C驱动开发实战:从设备树配置到排障全解析 1. 从一根线说起I2C 在 OpenHarmony 里到底扮演什么角色很多人第一次接触 I2C是在单片机上点一颗 OLED 或者读一颗 EEPROM两根线一接代码一跑屏幕就亮了。但当你把这套经验搬到 OpenHarmony 这类带内核、带驱动框架、带设备树描述的系统上时会发现事情没那么简单——引脚怎么配、设备怎么挂、驱动怎么注册、上层应用怎么拿到数据每一层都有它自己的规矩。这篇内容就是把我自己在 OpenHarmony 上折腾 I2C 的完整过程摊开来讲从总线原理、设备树配置、驱动适配到实际排障时怎么用逻辑分析仪抓波形、怎么判断是硬件问题还是软件问题尽量讲透。I2C 全称 Inter-Integrated Circuit中文叫集成电路总线是飞利浦在上世纪八十年代搞出来的一种同步、多主多从、半双工的串行总线。它只用两根线一根 SCL 时钟线一根 SDA 数据线两根线都要接上拉电阻。所有挂在总线上的设备都并联在这两根线上每个设备有自己唯一的 7 位地址也有 10 位地址的扩展模式但日常用得少。主设备发起通信通过地址选中某个从设备然后进行读写。这套机制的好处是引脚占用极少一颗 MCU 用两个引脚就能挂十几个传感器坏处是速率不高标准模式 100kHz快速模式 400kHz高速模式 3.4MHz而且总线是共享的一个设备出问题可能把整条总线拉死。在 OpenHarmony 的体系里I2C 属于 HDFHardware Driver Foundation驱动框架下的一类标准设备。系统启动时内核根据设备树Device Tree里的描述去初始化对应的 I2C 控制器然后 HDF 框架加载对应的驱动向上层提供统一的文件描述符接口。应用层通过open、read、write、ioctl这些标准系统调用来操作 I2C 设备不需要关心底层寄存器怎么配。这套分层设计的好处是驱动可复用、设备可插拔但代价是你必须把设备树写对、把 HDF 配置写对否则设备根本枚举不出来。这篇文章适合谁看如果你已经会用单片机点 I2C 设备但想在 OpenHarmony 上把驱动跑通那这篇就是给你写的。如果你完全没接触过 I2C也没关系我会从时序图开始讲把起始条件、地址帧、应答位这些基础概念用生活化的方式解释清楚。如果你已经在做 OpenHarmony 驱动开发那排障部分和常见问题速查表应该能帮你省不少时间。2. I2C 通信协议核心机制拆解2.1 起始、停止与应答三件事撑起整个协议I2C 的通信过程看起来复杂但核心就三件事起始条件、数据传输、停止条件。起始条件是在 SCL 为高电平的时候SDA 从高变低停止条件是在 SCL 为高电平的时候SDA 从低变高。这两个条件由主设备产生从设备检测到起始条件后开始监听总线检测到停止条件后释放总线。数据传输以字节为单位每个字节 8 位高位先发。每发完一个字节接收方要拉低 SDA 一个时钟周期作为应答ACK表示“我收到了”。如果接收方没有拉低那就是非应答NACK主设备据此判断从设备是否在线、是否准备好。地址帧是数据传输的第一个字节高 7 位是从设备地址最低位是读写标志位0 表示写1 表示读。用生活化的类比来说I2C 总线就像一条对讲机频道所有设备都在这条频道上。主设备喊一声“地址 0x3C 的设备在吗”如果 0x3C 设备在线它会回一声“在”然后主设备开始说正事。其他设备听到地址不是自己的就继续沉默。这套机制简单但有效缺点是所有设备共享带宽而且一旦某个设备把 SDA 一直拉低总线就死锁了。2.2 时钟同步与仲裁多主设备场景下的隐形规则I2C 支持多主设备也就是说总线上可以挂多个主设备它们都能发起通信。但同一时刻只能有一个主设备在说话这就涉及到仲裁。仲裁的规则是谁先拉低 SDA谁就赢得总线。如果两个主设备同时发起始条件然后一个发高电平一个发低电平发低电平的那个赢得仲裁发高电平的那个自动退出并转为从设备模式。时钟同步是另一个容易被忽略的机制。SCL 线是线与逻辑所有主设备的 SCL 输出都是开漏的只要有一个设备拉低 SCL总线就是低电平。所以当多个主设备同时工作时SCL 的实际频率由最慢的那个设备决定。这个机制保证了不同速度的设备可以共存但也意味着如果你挂了一个慢速设备整条总线的速率都会被拉下来。在实际项目中多主设备场景其实很少见大多数时候都是一主多从。但理解这两个机制有助于你排查一些诡异问题比如总线速率上不去、某个设备偶尔不响应等。2.3 7 位地址与 10 位地址地址空间怎么分配I2C 的 7 位地址空间是 0x00 到 0x7F其中 0x00 是通用呼叫地址0x01 到 0x07 是保留地址0x78 到 0x7F 也是保留地址实际可用的地址是 0x08 到 0x77。这意味着一条总线上最多挂 112 个设备对于大多数应用来说足够了。10 位地址是为了扩展地址空间而设计的它用两个字节来传输地址第一个字节的高 5 位是固定的 11110后面跟着地址的高 2 位和读写位第二个字节是地址的低 8 位。10 位地址的设备很少见而且很多主控制器不支持所以实际项目中基本用不到。地址冲突是 I2C 排障中最常见的问题之一。很多传感器芯片的地址是通过引脚电平来配置的比如某些型号的加速度计地址引脚接低电平是 0x68接高电平是 0x69。如果你在一条总线上挂了两颗同型号的传感器又没有把地址引脚配置成不同电平那就会冲突表现为两个设备都不响应或者响应异常。3. OpenHarmony 下 I2C 驱动框架与设备树配置3.1 HDF 驱动框架I2C 设备是怎么被系统识别的OpenHarmony 的 HDF 驱动框架把设备驱动分成几个层次最底层是内核态的 I2C 控制器驱动负责操作寄存器、产生时序中间层是 HDF 的 I2C 核心层提供统一的接口抽象上层是具体的设备驱动比如 OLED 驱动、传感器驱动。应用层通过/dev/i2c-x这样的设备节点来访问。系统启动时内核先根据设备树初始化 I2C 控制器注册到 I2C 子系统。然后 HDF 框架根据配置文件加载对应的设备驱动驱动通过DeviceAdd接口注册到 HDF 设备管理器中。应用层打开设备节点后通过ioctl发送读写请求请求经过 HDF 核心层转发到控制器驱动最终转换成实际的 I2C 时序。这套分层设计的好处是驱动可复用。比如你写了一个 OLED 驱动只要它遵循 HDF 的 I2C 接口规范就可以在不同的 OpenHarmony 设备上运行不需要改代码。代价是你必须把设备树和 HDF 配置写对否则驱动加载失败或者设备枚举不出来。3.2 设备树配置I2C 控制器与从设备的描述方法设备树是 OpenHarmony 内核用来描述硬件拓扑的一种数据结构。对于 I2C 来说设备树里需要描述两部分内容I2C 控制器本身以及挂在控制器下面的从设备。I2C 控制器的设备树节点通常长这样i2c0: i2c12000000 { compatible vendor,i2c-controller; reg 0x12000000 0x1000; interrupts GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH; clocks cru CLK_I2C0; clock-frequency 400000; pinctrl-names default; pinctrl-0 i2c0_pins; status okay; oled3c { compatible vendor,ssd1306; reg 0x3c; status okay; }; };这里clock-frequency是总线速率单位是 Hz400000 就是 400kHz。reg属性在 I2C 控制器节点里是寄存器基地址和长度在从设备节点里是从设备地址。compatible是驱动匹配字符串内核和 HDF 框架用它来找到对应的驱动。写设备树时有几个坑要注意。第一clock-frequency不能超过控制器支持的最大速率也不能超过从设备支持的最大速率取两者中较小的那个。第二从设备的reg属性是 7 位地址不要写成 8 位地址。第三如果从设备需要额外的 GPIO 来控制复位或者中断要在设备树里描述清楚。3.3 HDF 配置驱动加载与设备节点生成设备树描述的是硬件拓扑HDF 配置描述的是驱动加载关系。在 OpenHarmony 里HDF 配置通常放在vendor/xxx/hdf_config/目录下包括device_info.hcs和input_config.hcs等文件。device_info.hcs里需要声明 I2C 控制器和从设备的驱动信息device_i2c :: device { device0 :: deviceNode { policy 2; priority 100; preload 0; permission 0664; moduleName I2C_DRIVER; serviceName i2c_service; deviceMatchAttr i2c_config; }; };policy是服务发布策略2 表示对内核和用户态都发布。permission是设备节点权限0664 表示所有者和同组用户可读写。moduleName是驱动模块名要和驱动代码里注册的名字一致。配置写完后编译系统会生成对应的设备节点通常是/dev/i2c-0、/dev/i2c-1这样的形式。应用层打开这个节点就可以通过ioctl发送 I2C 读写请求了。4. I2C 读写实操从寄存器操作到应用层调用4.1 硬件连接与上拉电阻选择在动手写代码之前先把硬件接对。I2C 需要两根线SCL 和 SDA都要接上拉电阻。上拉电阻的阻值选择有讲究阻值太小功耗大而且可能超过设备的灌电流能力阻值太大上升沿变缓高速通信时波形会变形。经验值是标准模式 100kHz 用 4.7kΩ 到 10kΩ快速模式 400kHz 用 2.2kΩ 到 4.7kΩ高速模式 3.4MHz 用 1kΩ 左右。如果总线上挂了多个设备或者走线比较长要适当减小阻值。我实测下来3.3V 系统、400kHz、总线长度 10cm 以内用 4.7kΩ 最稳。还有一个容易忽略的点有些传感器模块自带上拉电阻如果你再外接上拉等效阻值会变小可能导致上升沿过冲。所以接之前先用万用表量一下 SCL 和 SDA 对 VCC 的阻值如果已经有 10kΩ 左右就不用外接了。4.2 用 ioctl 读写 I2C 设备完整代码示例OpenHarmony 的 I2C 用户态接口通过ioctl实现核心结构体是I2cMsgstruct I2cMsg { uint16_t addr; uint16_t flags; uint16_t len; uint8_t *buf; };addr是从设备地址flags是读写标志和地址位宽标志len是数据长度buf是数据缓冲区。读写操作通过I2C_RDWR命令一次性提交多个消息#include fcntl.h #include sys/ioctl.h #include linux/i2c.h #include linux/i2c-dev.h int fd open(/dev/i2c-0, O_RDWR); if (fd 0) { perror(open i2c device failed); return -1; } uint8_t reg_addr 0x00; uint8_t read_buf[2] {0}; struct i2c_msg msgs[2]; msgs[0].addr 0x3c; msgs[0].flags 0; msgs[0].len 1; msgs[0].buf reg_addr; msgs[1].addr 0x3c; msgs[1].flags I2C_M_RD; msgs[1].len 2; msgs[1].buf read_buf; struct i2c_rdwr_ioctl_data data; data.msgs msgs; data.nmsgs 2; if (ioctl(fd, I2C_RDWR, data) 0) { perror(i2c read failed); close(fd); return -1; } close(fd);这段代码先写寄存器地址再读两个字节是典型的 I2C 读流程。注意msgs[0]的flags是 0表示写msgs[1]的flags是I2C_M_RD表示读。两次操作之间没有停止条件这是 I2C 的重复起始机制用于在读之前先指定寄存器地址。4.3 写操作的注意事项与常见错误写操作比读操作简单但有几个坑要注意。第一有些设备要求写操作必须一次性完成不能分多次写否则内部状态机会复位。第二写操作后要给设备留足够的处理时间比如 EEPROM 写一个字节需要 5ms 左右如果你紧接着读会读到旧数据。第三有些设备的寄存器地址是 16 位的需要发两个字节的地址这时候msgs[0].len要设成 2。常见错误包括地址写错7 位地址写成 8 位、寄存器地址写错、数据长度写错、没有检查ioctl返回值。我踩过最坑的一次是把 7 位地址 0x3C 写成了 8 位地址 0x78结果设备一直不响应查了半天才发现是地址格式问题。5. I2C 排障实战从波形到代码的完整排查链路5.1 逻辑分析仪抓波形判断硬件问题的第一步当 I2C 通信失败时第一步是用逻辑分析仪抓波形。逻辑分析仪接在 SCL 和 SDA 上设置触发条件为 SCL 下降沿或者 SDA 下降沿然后运行程序看波形是否正常。正常的 I2C 波形应该满足起始条件清晰SCL 高电平期间 SDA 从高变低地址帧的 8 位数据完整第 9 位是 ACK数据帧的 8 位数据完整第 9 位是 ACK停止条件清晰SCL 高电平期间 SDA 从低变高。如果波形异常常见情况有几种。第一种是 SCL 一直被拉低说明有设备把时钟线拉死了可能是设备复位不完整或者电源异常。第二种是 SDA 一直被拉低说明有设备把数据线拉死了可能是设备内部状态机卡死。第三种是波形上升沿太缓说明上拉电阻太大或者总线电容太大。第四种是 ACK 位缺失说明从设备没有响应可能是地址不对、设备没供电、设备损坏。5.2 常见问题速查表现象可能原因排查方法解决方法设备完全不响应地址错误、供电异常、接线错误用逻辑分析仪看地址帧是否有 ACK检查地址、量电压、检查接线偶尔响应偶尔不响应上拉电阻不合适、总线干扰、时序余量不足看波形上升沿、看 ACK 位是否稳定调整上拉电阻、降低速率、加屏蔽读到的数据全是 0xFF设备没准备好、寄存器地址错误读设备 ID 寄存器验证加延时、检查寄存器地址读到的数据全是 0x00设备复位中、电源不稳量电源纹波、看复位引脚加滤波电容、检查复位时序总线死锁某个设备拉死 SDA 或 SCL逐个断开设备定位加总线恢复电路、改进驱动速率上不去从设备不支持高速、上拉电阻太大查从设备手册、看波形降低速率、减小上拉电阻这张表是我在实际项目中总结出来的覆盖了八成以上的 I2C 问题。遇到问题时先查表能快速缩小排查范围。5.3 总线死锁的恢复方法与预防措施总线死锁是 I2C 最头疼的问题之一。现象是 SCL 或 SDA 一直被某个设备拉低主设备无法发起新的通信。造成死锁的原因通常是主设备在从设备还没发完数据时就发了停止条件或者从设备在错误的时间拉低了 SDA。恢复方法有两种。第一种是手动恢复把 SCL 配置成 GPIO 输出然后手动发 9 个时钟脉冲让从设备把剩余的数据发完然后发停止条件。第二种是硬件恢复用一个 MOS 管或者专用芯片在检测到总线死锁时自动断开总线并重新初始化。预防措施包括在驱动里加超时机制如果ioctl超过一定时间没返回就复位控制器在设备树里配置 I2C 控制器的恢复引脚在应用层加重试机制通信失败时先复位总线再重试。6. 实战案例0.9 寸 OLED 在 OpenHarmony 上的适配过程6.1 SSD1306 驱动移植与设备树配置0.9 寸 OLED 常用的驱动芯片是 SSD1306I2C 地址通常是 0x3C 或 0x3D。在 OpenHarmony 上适配这颗屏需要做三件事设备树里添加 OLED 节点、HDF 配置里注册驱动、写驱动代码。设备树节点oled3c { compatible vendor,ssd1306; reg 0x3c; reset-gpios gpio0 12 GPIO_ACTIVE_LOW; status okay; };驱动代码的核心是初始化序列和显存刷新。SSD1306 的初始化序列包括设置显示时钟、设置多路复用率、设置显示偏移、设置起始行、设置对比度、设置显示模式等。这些命令通过 I2C 写控制字节 0x00 加命令字节的方式发送。显存刷新是把一个 128x64 的位图数据写到 SSD1306 的 GDDRAM 里。GDDRAM 分 8 页每页 128 字节总共 1024 字节。写数据时先发控制字节 0x40然后连续发 1024 字节数据。6.2 兼容性问题与解决方案0.9 寸 OLED 的兼容性问题主要有两个。第一是地址冲突有些模块默认地址是 0x3C有些是 0x3D如果设备树里写死了 0x3C遇到 0x3D 的模块就不响应。解决方法是在驱动里做地址探测先试 0x3C不响应再试 0x3D。第二是初始化序列差异不同批次的 SSD1306 可能对初始化序列的要求略有不同比如对比度设置、预充电周期等。解决方法是参考厂家提供的初始化代码不要直接用网上的通用代码。我遇到过一批屏用通用初始化代码显示正常但对比度偏低后来把对比度从 0xCF 改成 0xFF 就正常了。6.3 显示异常排查花屏、闪烁、不亮的处理OLED 显示异常通常有三种表现花屏、闪烁、完全不亮。花屏一般是显存数据错位或者初始化序列不对。排查方法是先用固定的测试图案比如全亮、棋盘格验证驱动是否正常如果测试图案正常但实际显示花屏那就是显存刷新逻辑有问题。闪烁一般是刷新率太低或者电源不稳。SSD1306 的刷新率取决于 I2C 速率和刷新数据量128x64 全屏刷新需要 1024 字节400kHz 下大约需要 25ms也就是 40fps足够流畅。如果闪烁先检查电源纹波再检查刷新逻辑是否每次都全屏刷新。完全不亮先检查供电和复位引脚再检查 I2C 通信是否正常。用逻辑分析仪看初始化命令是否发出、是否有 ACK。如果通信正常但不亮可能是对比度设置太低或者显示模式不对。7. 我个人在实际操作中的几点体会I2C 这东西说简单也简单两根线一接就能通说复杂也复杂一旦出问题可能是硬件、可能是驱动、可能是配置排查起来很考验耐心。我自己的经验是先把硬件确认好上拉电阻、供电、接线这些基础的东西不能省再用逻辑分析仪抓波形波形正常了再查软件软件排查从设备树开始确认控制器和从设备都枚举出来了再查 HDF 配置最后查驱动代码。还有一个习惯我觉得很有用每接一个新设备先读它的 ID 寄存器。大部分传感器和显示芯片都有 ID 寄存器读到了说明通信正常读不到说明硬件或地址有问题。这一步能省掉很多瞎猜的时间。最后分享一个小技巧如果总线经常死锁可以在 SCL 和 SDA 上各加一个 100Ω 的电阻串联到设备这样即使某个设备拉死总线也不会把整条总线拉死方便定位问题设备。这个技巧在调试多设备总线时特别有用。
返回列表