ARTICLE DETAIL

资讯详情

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

OpenHarmony RK3568 I2C实战:从设备树配置到用户态ioctl排障全链路

OpenHarmony RK3568 I2C实战:从设备树配置到用户态ioctl排障全链路 I2C 这东西说简单也简单两根线一挂设备就能聊天说难也真难时序差一点、上拉电阻选错一点、设备树写歪一点总线就直接躺平给你看。我在 RK3568 上跑 OpenHarmony 的那段时间光是让一颗 OLED 稳定点亮就折腾了整整两天后来把 I2C 的排障链路彻底摸了一遍才发现很多问题根本不是驱动没写好而是从硬件到设备树再到用户态调用每一层都埋着坑。这篇就把 I2C 在 OpenHarmony RK3568 这套组合下的完整用法和排障思路讲透从协议本质、设备树配置、内核驱动、用户态 ioctl 读写到示波器抓波形定位问题尽量让你少走我踩过的弯路。不管你是刚接触嵌入式总线的新手还是已经在调 RK3568 外设的老手都能从里面找到能直接抄作业的配置和排查方法。1. 先把 I2C 的物理层和时序讲明白不然后面全是玄学很多人调 I2C 出问题第一反应是去翻驱动代码其实大部分故障的根子在物理层和时序理解上。我见过太多人把 SDA 和 SCL 接反、上拉电阻随手抓一个 10K 就焊上去然后对着内核日志发呆。所以这一节先把底层讲清楚后面排障才有依据。1.1 两根线到底怎么说话开漏输出与上拉电阻的本质I2C 只有两根信号线SDA串行数据线和SCL串行时钟线。关键在于这两个引脚在电气上都是开漏Open-Drain输出结构也就是说器件只能把线拉低不能主动拉高。线要变高靠的是外部的上拉电阻把电平拉上去。这个设计不是随便定的它带来两个直接好处一是多设备可以共享同一根总线而不会短路——因为谁都不会主动输出高电平去和别人的低电平打架二是实现了**线与Wired-AND**逻辑只要有一个设备拉低整条线就是低。上拉电阻的取值是个经典问题我实测下来有个经验公式可以估算电阻太大上升沿变缓高速通信时波形还没爬到高电平阈值下一个时钟就来了直接误码。电阻太小灌电流过大器件拉低时功耗高严重时烧引脚。一般经验值总线速率典型上拉电阻说明100kHz标准模式4.7K ~ 10K最宽松容错高400kHz快速模式2.2K ~ 4.7K常用区间1MHz快速模式1K ~ 2.2K对布线敏感提示总线上挂的设备越多、走线越长总线电容越大上升沿越慢这时候要适当减小上拉电阻。RK3568 的 I2C 引脚内部通常有可配置的弱上拉但强烈建议外部再补上拉电阻内部弱上拉驱动能力有限长走线或高速率下根本不够用。1.2 起始、停止、应答三个必须刻进脑子里的时序I2C 的通信全靠 SCL 和 SDA 的配合来定义事件核心就三个起始条件STARTSCL 为高时SDA 由高变低。这是所有通信的开场哨。停止条件STOPSCL 为高时SDA 由低变高。通信结束。应答ACK/NACK每传输 8 位数据后第 9 个时钟周期接收方把 SDA 拉低表示 ACK保持高表示 NACK。数据位的规则是SCL 为低时 SDA 可以变化SCL 为高时 SDA 必须稳定。这条规则是理解一切 I2C 波形的基础。如果你用示波器抓波形发现 SCL 高电平期间 SDA 在跳变那基本可以断定是时序配置错了或者总线被干扰。1.3 数据帧格式7 位地址 读写位是怎么拼出来的一次典型的 I2C 传输长这样START | 7位从机地址 | R/W位 | ACK | 数据字节1 | ACK | ... | 数据字节N | ACK | STOP那个 R/W 位很关键0 表示写1 表示读。所以从机地址在总线上实际传输时是 8 位——高 7 位是地址最低位是读写方向。比如一个 7 位地址为0x3C的 OLEDSSD1306 常见地址写操作时总线上发的是0x78读操作时是0x79。我见过不少人在这里翻车驱动里填地址填了0x78结果内核又帮你左移一位实际发出去变成0xF0设备当然不理你。记住一个原则Linux/OpenHarmony 的设备树和驱动里填的永远是 7 位地址移位是内核 I2C 子系统帮你做的。2. RK3568 上 OpenHarmony 的 I2C 设备树怎么配才不翻车设备树是 OpenHarmony以及所有 Linux 系内核里硬件描述的入口I2C 相关的配置几乎全在这里。RK3568 的 I2C 控制器有好几路配置错了或者漏了用户态根本看不到设备节点。2.1 找到你的 I2C 控制器RK3568 的 I2C 资源分布RK3568 一般有 6 路 I2C 控制器i2c0 ~ i2c5分布在不同的引脚组上。配置前第一件事是确认你的外设接在哪一路、哪几个引脚。这需要对照芯片手册的引脚复用表Pinmux。一个典型的 I2C 控制器节点配置长这样i2c3 { status okay; clock-frequency 100000; pinctrl-names default; pinctrl-0 i2c3m0_xfer; oled: ssd13063c { compatible solomon,ssd1306; reg 0x3c; status okay; }; };几个关键点status okay不写这个控制器就是关闭的用户态看不到/dev/i2c-3。clock-frequency总线速率单位 Hz。100000 就是 100kHz。这个值要和从机支持的最大速率匹配别一上来就写 400000有些便宜传感器只支持 100kHz。pinctrl-0引脚复用配置写错了引脚根本没接到 I2C 控制器上。子节点里的reg 0x3c7 位从机地址别填 8 位。2.2 引脚复用pinctrl配错是设备不响应的头号原因我踩过最坑的一次就是 pinctrl 引用了错误的引脚组。现象是/dev/i2c-3节点存在用i2cdetect扫描却一个设备都扫不到示波器一抓SCL 和 SDA 全程高电平压根没波形。原因就是引脚复用没配对I2C 控制器的信号根本没引到物理引脚上。RK3568 的 pinctrl 定义通常在rk3568-pinctrl.dtsi里命名规则类似i2c3m0_xfer其中m0表示引脚组 0。你必须对照原理图确认外设实际接的是哪一组引脚然后引用对应的 pinctrl 节点。排查方法很直接抓波形。如果 SCL 上完全没有时钟输出八成是 pinctrl 或控制器没使能如果有波形但设备不响应那问题在地址或从机本身。2.3 从机节点地址、compatible 与中断的填写规范从机节点的compatible属性决定了内核用哪个驱动去匹配它。如果你用的是现成驱动比如 SSD1306 有内核驱动compatible必须和驱动里的of_match_table完全一致否则驱动不会 probe。oled: ssd13063c { compatible solomon,ssd1306; reg 0x3c; status okay; };如果暂时没有内核驱动只想在用户态用 ioctl 直接读写那从机节点其实可以简化甚至不写——因为用户态访问/dev/i2c-X时内核只关心控制器不关心从机节点。但建议还是写上一是方便管理二是将来接内核驱动时不用改。注意reg属性填的是 7 位地址。有些厂商文档给的地址是 8 位含读写位比如写0x78这时候你要自己右移一位变成0x3C。这个转换错误极其常见务必核对。3. 内核态驱动与用户态 ioctl两条路怎么选在 OpenHarmony 上访问 I2C 设备有两条路写内核驱动或者在用户态用 ioctl 直接操作。两条路各有适用场景选错了会白白增加工作量。3.1 什么时候该写内核驱动什么时候用户态就够了判断标准其实很简单用户态 ioctl 够用设备逻辑简单只是偶尔读写几个寄存器比如读个传感器温度、点个 OLED。开发快不用编译内核模块改完直接跑。需要写内核驱动设备需要频繁访问、需要中断处理、需要接入 OpenHarmony 的 HDI硬件设备接口框架、或者需要被多个进程共享访问。我在项目里调 OLED 显示一开始就是用用户态 ioctl 快速验证硬件通不通确认没问题后再决定要不要上内核驱动。先用用户态验证硬件再决定架构这是我强烈推荐的做法能省下大量反复编译内核的时间。3.2 用户态 ioctl 读写 I2C 的完整代码骨架用户态访问 I2C 的核心是ioctl配合i2c_msg结构体。下面是一个可直接用的读写封装#include linux/i2c.h #include linux/i2c-dev.h #include sys/ioctl.h #include fcntl.h #include unistd.h #include stdio.h int i2c_write_reg(int fd, unsigned char dev_addr, unsigned char reg, unsigned char val) { unsigned char buf[2] { reg, val }; struct i2c_msg msgs[1]; struct i2c_rdwr_ioctl_data data; msgs[0].addr dev_addr; /* 7 位地址 */ msgs[0].flags 0; /* 写 */ msgs[0].len 2; msgs[0].buf buf; data.msgs msgs; data.nmsgs 1; if (ioctl(fd, I2C_RDWR, data) 0) { perror(i2c write failed); return -1; } return 0; } int i2c_read_reg(int fd, unsigned char dev_addr, unsigned char reg, unsigned char *val) { struct i2c_msg msgs[2]; struct i2c_rdwr_ioctl_data data; /* 先写寄存器地址 */ msgs[0].addr dev_addr; msgs[0].flags 0; msgs[0].len 1; msgs[0].buf reg; /* 再读数据 */ msgs[1].addr dev_addr; msgs[1].flags I2C_M_RD; msgs[1].len 1; msgs[1].buf val; data.msgs msgs; data.nmsgs 2; if (ioctl(fd, I2C_RDWR, data) 0) { perror(i2c read failed); return -1; } return 0; }打开设备节点的代码int fd open(/dev/i2c-3, O_RDWR); if (fd 0) { perror(open i2c-3 failed); return -1; }这里有个关键技巧读寄存器时用两个i2c_msg组合成一次I2C_RDWR中间不发 STOP这叫复合传输或重复起始Repeated START。很多传感器比如常见的温湿度、加速度计要求写寄存器地址 读数据之间不能有 STOP否则内部状态机会复位读出来全是 0xFF。如果你用两次独立的 ioctl中间会插入 STOP就会踩这个坑。3.3 复合传输与重复起始读寄存器为什么老返回 0xFF上面提到的 0xFF 问题我遇到过不止一次。现象是写寄存器正常读寄存器永远返回 0xFF 或 0x00。原因就是读写之间被插入了 STOP 条件。用I2C_RDWR把写地址和读数据打包成一次传输内核 I2C 子系统会自动在两次 msg 之间发重复起始而不是 STOP这样才符合大多数传感器的时序要求。提示如果你用的是SMBUS_READ_BYTE_DATA之类的 smbus 接口内核也会帮你处理重复起始但 smbus 对时序和数据的限制更多灵活性不如I2C_RDWR。我一般优先用I2C_RDWR。4. 排障实战从设备扫不到到数据读不对的完整链路排障最忌讳东一榔头西一棒子。我总结了一套从物理层到应用层逐层排查的链路按顺序走基本能定位 90% 的 I2C 问题。4.1 第一步永远是抓波形示波器/逻辑分析仪怎么用不管问题多复杂先抓波形。这一步能直接区分是硬件问题还是软件问题。抓波形看几个关键点SCL 有没有时钟没有时钟说明控制器没工作或 pinctrl 没配对。SDA 有没有数据跳变SCL 有波形但 SDA 不动说明控制器在发但设备没应答或者 SDA 线断了。起始条件是否正常SCL 高时 SDA 有没有下降沿。第 9 个时钟的 ACK如果第 9 位是高NACK说明从机没应答地址错了或者设备没上电。上升沿是否太缓如果上升沿像爬坡一样慢就是上拉电阻太大或总线电容太大。我一般用逻辑分析仪配合解码功能直接看解码出来的地址和数据比人眼看波形快得多。解码出来的地址如果和你预期的不一样先怀疑地址移位问题。4.2 i2cdetect 扫不到设备先查这五个地方i2cdetect -y 3是排查 I2C 的第一把利器。如果扫不到任何设备按这个顺序查排查项检查方法常见问题控制器是否使能ls /dev/i2c-*设备树 status 没写 okay引脚复用抓波形看有无输出pinctrl 引用错误上拉电阻万用表测空闲电平没焊上拉或阻值过大从机供电万用表测 VCC设备没上电从机地址对照手册核对7 位/8 位搞混我遇到过一次i2cdetect扫不到查了半天发现是从机模块的 VCC 没接光接了 SDA、SCL 和 GND。这种低级错误在赶进度的时候特别容易犯所以排查一定要从最基础的供电开始。4.3 能扫到地址但读写失败地址、时序与从机状态机能扫到地址说明物理层和地址都没问题问题往往在时序或从机的状态机上。常见原因速率太快从机只支持 100kHz你配了 400kHz。降速试试。重复起始问题读寄存器返回 0xFF参考 3.3 节的复合传输。从机需要初始化有些设备上电后要先写配置寄存器才能正常读写直接读会失败。寄存器地址位宽有些设备寄存器地址是 16 位的你按 8 位发自然读不对。4.4 数据偶尔出错总线电容、干扰与时钟拉伸如果数据不是全错而是偶尔出错那基本是信号完整性问题总线电容过大挂太多设备或走线太长减小上拉电阻。干扰I2C 走线靠近高频信号线加屏蔽或拉开距离。时钟拉伸Clock Stretching从机拉低 SCL 要求主机等待如果主机不支持就会出错。RK3568 的 I2C 控制器一般支持时钟拉伸但要在设备树里确认配置。时钟拉伸这个概念值得单独说有些从机比如某些 EEPROM处理慢会在接收一个字节后把 SCL 拉低告诉主机等一下。主机必须检测到 SCL 被拉低后暂停时钟直到从机释放。如果主机不支持数据就会错位。5. 几个真实案例OLED、EEPROM 和传感器踩坑记录理论讲再多不如看几个真实案例。这几个都是我在 RK3568 OpenHarmony 上实际踩过的坑每个都对应一类典型问题。5.1 0.9 寸 OLED 的 I2C 兼容问题与 SSD1306 初始化0.9 寸 OLED 用 SSD1306 驱动芯片的特别多I2C 地址一般是0x3C或0x3D。我遇到的问题是i2cdetect能扫到0x3C但屏幕就是不亮。排查后发现两个问题一是初始化序列没发全SSD1306 上电后必须按手册发一串初始化命令设置对比度、扫描方向、电荷泵等少一条都不亮二是电荷泵没开SSD1306 需要内部电荷泵产生驱动电压初始化里必须发0x8D, 0x14打开它。初始化序列的关键几条/* SSD1306 初始化片段 */ {0x00, 0xAE}, /* 关闭显示 */ {0x00, 0xD5, 0x80}, /* 设置时钟分频 */ {0x00, 0xA8, 0x3F}, /* 设置多路复用比 */ {0x00, 0x8D, 0x14}, /* 开启电荷泵关键 */ {0x00, 0xAF}, /* 开启显示 */注意 SSD1306 的命令和数据是靠控制字节区分的0x00表示后面是命令0x40表示后面是数据。发命令时第一个字节是0x00发显存数据时第一个字节是0x40。这个细节搞错屏幕要么不亮要么花屏。5.2 EEPROM 读写页写、写周期与地址回绕EEPROM比如 AT24C 系列是练手 I2C 的经典器件但坑也不少页写边界EEPROM 按页组织常见 8 字节或 16 字节一页一次连续写不能跨页跨页会回绕覆盖。写多字节数据时要自己按页拆分。写周期等待EEPROM 写完一个字节后需要几毫秒的内部写周期这期间不响应任何命令。如果你连续写不等待后面的写会失败。正确做法是写完后轮询 ACK直到设备应答再继续。地址回绕读超过末尾地址会回绕到开头读多字节时要注意长度。我写 EEPROM 时最常犯的错就是忘了写周期等待连续写一串数据结果只有第一个字节写进去了。后来加了 ACK 轮询才稳定。5.3 传感器读值异常从 BH1750 到 AS5600 的排查思路光照传感器 BH1750、磁编码器 AS5600 这类器件读值异常通常有几个共性原因测量模式没设置BH1750 上电后默认是掉电模式必须先发命令进入连续测量模式否则读出来是 0。转换时间没等够BH1750 一次测量需要几十到上百毫秒发完命令立刻读读到的是上次的旧值或 0。寄存器地址搞错AS5600 的角度寄存器地址是0x0C高字节和0x0D低字节读错地址自然不对。排查这类问题的通用思路先确认设备在正确的模式再确认等待时间够最后核对寄存器地址。这三步走完大部分读值异常都能解决。6. 把 I2C 用稳的几个工程习惯调通一次不难难的是让它长期稳定运行。最后分享几个我在项目里养成的习惯都是踩坑换来的。第一永远先抓波形再改代码。我见过太多人一遇到问题就改驱动改了半天发现是硬件没接好。波形是唯一不会骗你的证据先看波形能省掉一半的无效调试。第二设备树配置做好注释。引脚组、地址、速率这些信息过一个月你自己都记不清。在设备树里写清楚这路 I2C 接的是什么设备、地址多少、为什么选这个速率将来维护的人包括你自己会感谢你。第三用户态验证和内核驱动分离。先用用户态 ioctl 把硬件跑通确认地址、时序、初始化序列都对再考虑要不要写内核驱动。这样能把硬件问题和驱动问题分开排查效率高很多。第四给关键操作加超时和重试。I2C 是共享总线偶尔被干扰导致一次传输失败很正常。读传感器时加个重试机制比一次失败就报错要健壮得多。但要注意重试不能掩盖根本问题如果频繁失败还是要回去查硬件。第五速率能低就不高。除非带宽真的不够否则 100kHz 足够大多数传感器和 OLED 用。速率越低对布线和上拉电阻的要求越宽松稳定性越好。我调 OLED 时一开始用 400kHz 偶尔花屏降到 100kHz 后再没出过问题。I2C 这套东西协议本身不复杂难的是把硬件、设备树、驱动、应用这几层串起来任何一层出问题现象都差不多。我的经验是从物理层往上排查用波形说话用最小系统验证。把这套链路走顺了以后调任何 I2C 设备都是同一套方法。
返回列表