1. 这不是教科书,是我在产线调了三年I2C设备后撕下来的笔记
“嵌入式外设调试思路——I2C设备篇”,这标题看着像培训PPT,但我要说清楚:它是我蹲在工厂车间、焊台旁、示波器前,用烧坏的5片EEPROM、3块OLED屏、2块RK3568开发板和一摞被划掉的时序图,换来的实操心法。不是理论推演,是故障单堆出来的经验。
I2C——这个被称作“两线制总线”的协议,在嵌入式系统里几乎无处不在:温湿度传感器(SHT30)、EEPROM(AT24C02)、OLED显示屏(SSD1306)、RTC芯片(PCF8563)、电源管理IC(TPS65910)……你手头那块刚上电就黑屏的板子,八成问题出在I2C链路上。而调试它的第一反应,往往不是查代码,而是抄起万用表量电压、换根杜邦线、把示波器探头怼到SCL/SDA上——因为I2C的“哑巴式失败”太常见了:没报错、不崩溃、只是死寂无声。它不像UART能吐乱码给你看,也不像SPI有明确的CS信号告诉你“我正在通信”,I2C的沉默,是硬件层、驱动层、应用层三重耦合失效后的集体失语。
所以这篇分享不讲I2C协议标准文档里的7种状态机转换,不列FS模式下400kHz的理论带宽计算,而是直击现场:当你发现i2cdetect -l能列出总线,i2cdetect -y 1却扫不出地址,i2cget返回Operation not permitted,或者OLED只亮半屏、EEPROM写入后读出来全是0xFF——这些具体到手指发麻的瞬间,该怎么拆解、怎么验证、怎么归因。我会把设备树里那一行&i2c1 { status = "okay"; };背后牵扯的引脚复用冲突、上拉电阻取值偏差、内核配置开关、用户态工具链依赖,全给你掰开揉碎;也会告诉你为什么在RK3568上用i2c-tools读CH341A USB-I2C适配器的数据会卡住,而在STM32F4上跑同样的i2c_read_reg()函数却能稳定工作——差异不在代码,而在时钟源精度、GPIO驱动能力、甚至PCB走线长度带来的容性负载。
关键词里反复出现的“设备树”“i2c-tools”“推挽/开漏”“电平转换”,都不是孤立概念。它们是一张网:设备树定义了I2C控制器的寄存器基址和中断号,也硬编码了从设备的地址和兼容性字符串;i2c-tools是用户态的“听诊器”,但它依赖内核I2C core提供的/dev/i2c-*节点,而这个节点能否创建,又取决于设备树中i2c@...节点是否被正确解析、对应的clock/reset资源是否enable;推挽与开漏,决定了你能不能把3.3V主控和5V传感器挂在同一组SCL/SDA线上——这不是选型题,是物理层生死线;而电平转换器(比如TXS0108E)一旦选错型号或布错去耦电容,就会让原本稳定的通信在高温环境下间歇性丢包,这种问题连示波器都难抓,得靠热风枪局部加热才能复现。
适合谁看?如果你是刚拿下蓝桥杯嵌入式组别省赛、正为毕业设计里那块CH32V307+OLED12864 I2C显示模块焦头烂额的学生;如果你是入职三个月、被组长扔了一块瑞芯微RK3568开发板要求“把温感数据读出来”的 junior 工程师;如果你是做了十年单片机、第一次接触PetaLinux、对着petalinux-config -c kernel里一堆CONFIG_I2C_*选项发懵的转型者——这篇就是为你写的。它不假设你熟读ARM Cortex-A手册,但要求你愿意拿起万用表、打开终端、修改一行设备树并重新编译。所有步骤,我都按真实产线节奏来组织:先确认物理连接是否成立,再验证内核驱动是否加载,然后检查用户态接口是否可用,最后才深入寄存器级调试。跳过任何一环,都可能让你在i2c_transfer()返回-EREMOTEIO时,对着逻辑分析仪波形干瞪眼两小时。
2. 调试不是玄学,是分层拆解的工程动作
2.1 物理层:先让SCL和SDA“活”过来,再谈通信
I2C调试的第一道门槛,永远在PCB和接线上。很多工程师一上来就敲i2cdetect,看到空列表就怀疑驱动没起来,结果折腾半天发现是开发板上的SCL引脚被误焊到了GND——这种低级错误,在量产返修单里占比超过37%(我们去年统计过)。所以我的固定流程是:断电→目视→通电→测压→查波形。
目视检查重点有三处:
第一,I2C总线是否被意外短路。常见于排针焊接飞锡、FPC排线压伤导致SCL/SDA短接、或者调试时误将跳线帽插在错误位置(比如把I2C1的SCL和SPI0的MOSI短在一起)。
第二,上拉电阻是否存在且阻值合理。I2C是开漏输出,必须靠外部上拉才能产生高电平。标准模式(100kHz)常用4.7kΩ,快速模式(400kHz)建议2.2kΩ~3.3kΩ。阻值过大(如10kΩ),上升沿缓慢,易受噪声干扰;阻值过小(如470Ω),则总线驱动电流超限,主控GPIO可能过热甚至损坏。我见过最离谱的案例:某客户用1kΩ上拉驱动8个I2C设备,结果在-20℃环境下,SDA上升时间长达1.2μs,超出标准规定的300ns,导致从机无法识别起始条件。
第三,电平匹配是否处理。3.3V主控(如STM32H7)直接挂5V从机(如某些老款EEPROM),看似能通信,实则SDA线在从机输出高电平时被钳位在3.3V+0.7V≈4.0V,长期运行加速主控GPIO氧化,最终表现为间歇性ACK失败。此时必须加双向电平转换器,且注意其方向控制引脚(如TXB0108的OE)是否接对——OE悬空会导致转换器内部MOSFET截止,总线彻底开路。
通电后,用万用表直流电压档测SCL/SDA对地电压。正常情况下,两者应接近VCC(如3.3V),因为上拉电阻将线路拉高。若测得0V,说明存在硬短路或从机内部ESD保护二极管击穿;若测得1.8V左右,则大概率是上拉电阻缺失或虚焊,或者某个从机漏电严重(可逐个断开从机排查)。这里有个速判技巧:用镊子轻触SCL线,观察电压是否瞬间跌落再回升——能跌落说明线路未完全短路,回升慢则暗示上拉不足或容性负载过大。
真正要确认物理层健康,必须用示波器看波形。探头接地夹接GND,信号钩接SCL(或SDA)。触发方式设为边沿触发,下降沿,触发电平1.5V。理想波形应具备:
- 上升沿陡峭(<300ns),由上拉电阻和线路分布电容决定,公式为t_r ≈ 2.2 × R_pullup × C_bus;
- 下降沿更陡(<100ns),由主控/从机开漏管放电能力决定;
- 高电平稳定在VCC±5%,低电平接近0V;
- 无振铃、无过冲(如有,需在靠近主控端串接22Ω~47Ω阻尼电阻)。
我曾调试一块RK3568核心板,i2cdetect始终扫不到地址,示波器显示SCL有波形但SDA恒为高电平。最终发现是OLED模块的SDA引脚虚焊,锡球仅搭在焊盘边缘,机械应力下接触不良。用热风枪补焊后,SDA立刻出现清晰的方波。这个案例说明:示波器不是奢侈品,是I2C调试的听诊器;而“看到波形”比“看到地址”更早一步,也更可靠。
2.2 驱动层:设备树不是配置文件,是硬件描述的契约
当物理层确认无误,下一步是验证内核是否正确加载了I2C控制器驱动。很多人以为dmesg | grep i2c看到i2c-x: i2c-x: registered as i2c bus就万事大吉,其实这只是驱动注册成功,不代表总线功能正常。真正的检验,是看设备树(Device Tree)是否精准描述了硬件,并被内核正确解析。
以瑞芯微RK3568为例,其I2C控制器节点在arch/arm64/boot/dts/rockchip/rk3568.dtsi中定义:
i2c1: i2c@fe5a0000 { compatible = "rockchip,rk3399-i2c", "rockchip,rk3066-i2c"; reg = <0x0 0xfe5a0000 0x0 0x1000>; interrupts = <GIC_SPI 61 IRQ_TYPE_LEVEL_HIGH>; #address-cells = <1>; #size-cells = <0>; clocks = <&cru SCLK_I2C1>, <&cru PCLK_I2C1>; clock-names = "i2c", "pclk"; i2c-scl-rising-time-ns = <150>; i2c-scl-falling-time-ns = <20>; status = "disabled"; };关键点在于:
reg属性指定了控制器寄存器基址(0xfe5a0000),若与实际硬件不符,驱动根本无法访问寄存器;clocks和clock-names声明了时钟源,若&cru节点未正确定义SCLK_I2C1,驱动初始化时会因clk_get()失败而退出;i2c-scl-rising-time-ns等参数用于计算SCL时钟周期,直接影响i2c-adap-rtk驱动中的set_scl_rate()函数行为;status = "disabled"是默认状态,必须在板级DTS(如rk3568-evb.dts)中显式改为"okay",否则该总线不会被启用。
而从设备(如AT24C02 EEPROM)的描述,必须放在对应I2C总线下:
&i2c1 { status = "okay"; eeprom@50 { compatible = "atmel,24c02"; reg = <0x50>; pagesize = <16>; }; };这里reg = <0x50>是7位地址(0x50),不是8位读写地址(0xA0/0xA1)。若写成<0xA0>,内核解析时会因地址越界(>0x7F)而忽略该节点,导致/sys/bus/i2c/devices/下无对应设备目录。
一个典型陷阱是引脚复用冲突。RK3568的I2C1默认使用GPIO2_A0(SCL)和GPIO2_A1(SDA),但这两个引脚同时也是SPI0的CS0和MISO。若板级DTS中未正确配置pinmux,比如遗漏了:
&phoenix { i2c1_pins: i2c1-pins { rockchip,pins = <RK_PIN_REG(2, 0) RK_PIN_REG(2, 1)>; rockchip,pin-control = <RK_PINCTRL_FUNC_2>; // I2C function }; }; &i2c1 { pinctrl-names = "default"; pinctrl-0 = <&i2c1_pins>; };那么即使设备树语法正确,GPIO引脚仍处于默认SPI功能,I2C信号根本无法输出。此时dmesg会打印i2c rk3568-i2c ff5a0000.i2c: Failed to get pins for node,但新手常忽略这条信息,转头去查I2C驱动源码。
验证设备树解析是否成功,最直接的方法是检查/proc/device-tree/。执行:
ls /proc/device-tree/i2c@fe5a0000/应能看到compatible、reg、interrupts等属性文件。再看从设备:
ls /proc/device-tree/i2c@fe5a0000/eeprom@50/若有compatible、reg,说明DTS节点已被解析。接着检查/sys/bus/i2c/devices/:
ls /sys/bus/i2c/devices/应出现i2c-1目录(对应/dev/i2c-1),进入该目录:
ls /sys/bus/i2c/devices/i2c-1/应看到1-0050(即地址0x50的设备)。若缺失,说明设备树中&i2c1未启用,或从设备节点reg地址错误,或compatible字符串与驱动不匹配(如写成"atmel,24c01"而内核只支持24c02)。
提示:
compatible字符串是设备树与驱动匹配的核心。内核启动时,会遍历所有of_i2c_register_devices()注册的驱动,比对of_match_table中的compatible字段。若不匹配,从设备不会被绑定,/sys/bus/i2c/devices/下自然无踪影。可通过cat /sys/bus/i2c/drivers/查看已注册的I2C驱动列表,确认目标驱动(如at24)是否存在。
2.3 用户态层:i2c-tools不是玩具,是诊断的手术刀
当设备树解析无误,/dev/i2c-1节点生成,下一步是用i2c-tools这套用户态工具进行交互式诊断。很多人把它当作“扫地址”的命令集合,其实它提供了从物理层到协议层的完整探针能力。
安装与基础验证:
# Ubuntu/Debian sudo apt install i2c-tools # 编译安装(嵌入式环境常用) git clone https://github.com/groeck/i2c-tools.git cd i2c-tools && make && sudo make install首先确认I2C总线列表:
i2cdetect -l # 输出示例: # i2c-0 i2c 60010000.i2c I2C adapter # i2c-1 i2c fe5a0000.i2c I2C adapteri2cdetect -l列出的是内核通过i2c_add_adapter()注册的适配器,对应/dev/i2c-*设备节点。若此处为空,说明I2C控制器驱动未加载或设备树未启用,无需往下走。
接着扫描总线上的从设备地址:
sudo i2cdetect -y 1 # 输出示例: # 0 1 2 3 4 5 6 7 8 9 a b c d e f # 00: -- -- -- -- -- -- -- -- -- -- -- -- -- # 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # 50: -- UU -- -- -- -- -- -- -- -- -- -- -- -- -- -- # 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # 70: -- -- -- -- -- -- -- --关键看UU标记:它表示该地址被内核驱动占用(如at24驱动已绑定EEPROM),而非空闲。若期望的地址(如0x50)显示--,说明从设备未响应,原因可能是:
- 物理连接断开(线没插牢、从机未供电);
- 从机地址拨码错误(AT24C02的A0/A1/A2引脚电平组合决定地址);
- 从机本身故障(EEPROM擦写次数超限、OLED驱动IC损坏);
- 总线电平异常(上拉不足、噪声干扰)。
i2cdetect的-y参数跳过交互确认,-r参数使用read-byte方式扫描(更可靠,但可能触发某些从机的非预期操作),-q参数静默模式(适合脚本调用)。
定位到设备后,用i2cget读取寄存器:
# 读取AT24C02地址0x00处的字节(EEPROM首字节) sudo i2cget -y 1 0x50 0x00 # 返回:0x00 (十六进制) # 读取SSD1306 OLED的显示开关寄存器(0xAE) sudo i2cget -y 1 0x3c 0xae # 返回:0xae (若返回0xff,说明通信失败)i2cget的第三个参数是寄存器地址,第四个参数可指定数据长度(b=byte,w=word,i=I2C block),默认为byte。若返回Error: Read failed,需检查:
- 从机是否支持该寄存器地址(OLED的0xAE是命令,不能读,应返回
Invalid argument); - 主控是否发送了正确的读写位(
i2cget默认读,i2cset默认写); - 从机是否处于可读状态(如EEPROM写入后需等待tWR=5ms才能读)。
i2cset用于写入:
# 向AT24C02地址0x00写入0xAA sudo i2cset -y 1 0x50 0x00 0xaa # 再读回验证 sudo i2cget -y 1 0x50 0x00 # 应返回0xaa注意:EEPROM写入有页限制(AT24C02为16字节/页),跨页写入会导致数据覆盖。i2cset一次只能写一个字节,批量写需用i2cdump或自定义程序。
i2cdump是深度扫描工具:
# 以字节模式dump AT24C02前32字节 sudo i2cdump -y 1 0x50 # 输出: # 0 1 2 3 4 5 6 7 8 9 a b c d e f 0123456789abcdef # 00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ # 10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................若dump结果全为0xFF,说明EEPROM未编程或擦除失败;若某区域连续0x00,可能是写入时地址偏移错误。
注意:
i2c-tools所有命令均需sudo权限,因其操作/dev/i2c-*设备节点,而该节点默认属root:root,权限600。生产环境中,可将用户加入i2c组:sudo usermod -a -G i2c $USER,注销重登后即可免sudo。
2.4 协议层:读懂时序图,才能抓住通信的脉搏
当i2cdetect扫到地址、i2cget能读到数据,是否就代表I2C通信完美?不一定。很多诡异问题藏在时序细节里:OLED显示残影、EEPROM写入后部分字节丢失、传感器数据跳变——这些往往是时序裕量不足的表现。
I2C标准定义了严格的时序参数,以标准模式(100kHz)为例:
- T_LOW:SCL低电平时间 ≥ 4.7μs;
- T_HIGH:SCL高电平时间 ≥ 4.0μs;
- T_SU:STA:起始条件建立时间(SCL高时SDA下降)≥ 4.0μs;
- T_HD:STA:起始条件保持时间(SDA下降后SCL保持低)≥ 4.0μs;
- T_SU:DAT:数据建立时间(SDA在SCL高期间稳定)≥ 250ns;
- T_HD:DAT:数据保持时间(SCL下降后SDA保持)≥ 5μs;
- T_BUF:总线空闲时间(STOP后到START)≥ 4.7μs。
这些参数不是理论值,是硬件能力的硬约束。主控I2C控制器的时钟分频器(clock divider)决定了SCL频率,而SCL高低电平时间由分频值和内核驱动中的set_scl_rate()函数共同决定。例如RK3568的i2c-rk3399.c驱动中:
static void rk3x_i2c_set_scl_rate(struct rk3x_i2c *i2c, unsigned long rate) { u32 div; div = DIV_ROUND_UP(i2c->clk_rate, rate * 2); // 粗略计算 writel(div, i2c->regs + REG_CLKDIV); }若clk_rate为100MHz,目标rate为100kHz,则div≈500,SCL周期≈10μs,符合标准。但若PCB走线长、容性负载大,实际上升时间变长,可能导致T_SU:DAT不满足,从机采样错误。
示波器抓取I2C波形时,重点观察:
- 起始/停止条件:SCL为高时,SDA是否干净地下降/上升?若有毛刺,说明存在竞争或上拉不足;
- 数据采样点:在SCL高电平中点,SDA电平是否稳定?若在SCL高期间SDA跳变,从机会采样到错误值;
- ACK/NACK:从机在第9个时钟周期拉低SDA为ACK,释放为NACK。若始终NACK,说明从机未响应(地址错、忙、故障);若偶发NACK,可能是时序裕量不足或电源波动。
一个经典案例:某客户用STM32F4驱动SSD1306 OLED,在室温下正常,-10℃时屏幕闪烁。示波器抓取发现,低温下SDA上升时间从200ns增至600ns,导致T_SU:DAT < 250ns,OLED控制器采样错误。解决方案不是改代码,而是将上拉电阻从4.7kΩ换成2.2kΩ,并在SDA线上增加100pF补偿电容(减缓上升沿,避免过冲)。
实操心得:不要迷信数据手册的“最大速率”。AT24C02标称支持400kHz,但在8个设备挂载、总线电容达400pF时,实测稳定速率仅200kHz。我的经验是:设计阶段按标准模式(100kHz)布局,调试时用示波器实测裕量,再逐步提速。每次提速后,必须做72小时老化测试,观察通信错误率。
3. 从“能用”到“可靠”:实战中的坑与填法
3.1 设备树陷阱:那些让你重启三次才发现的拼写错误
设备树调试中最耗时的,往往不是逻辑错误,而是肉眼难辨的拼写错误。我整理了近三年踩过的TOP5设备树坑:
坑1:compatible字符串大小写敏感
错误写法:
compatible = "Atmel,24c02"; // 'A'大写正确写法:
compatible = "atmel,24c02"; // 全小写内核匹配时严格区分大小写。at24.c驱动中的of_match_table定义为:
static const struct of_device_id at24_of_match[] = { { .compatible = "atmel,24c01", }, { .compatible = "atmel,24c02", }, // 注意小写 { } };若写成"Atmel",匹配失败,设备不会被创建。
坑2:reg地址格式混淆
错误写法(当成8位地址):
eeprom@A0 { // 0xA0是读地址 compatible = "atmel,24c02"; reg = <0xA0>; };正确写法(7位地址):
eeprom@50 { // 0x50是7位地址 compatible = "atmel,24c02"; reg = <0x50>; };内核解析reg时,会将其左移1位作为7位地址,再根据读写位生成8位地址。0x50 << 1 = 0xA0,正确;0xA0 << 1 = 0x140,超出7位范围,解析失败。
坑3:status属性位置错误
错误写法(在父节点写):
&i2c1 { status = "okay"; }; &i2c1 { eeprom@50 { compatible = "atmel,24c02"; reg = <0x50>; }; };正确写法(在子节点内):
&i2c1 { status = "okay"; eeprom@50 { compatible = "atmel,24c02"; reg = <0x50>; }; };DTS语法中,&i2c1是引用,后续内容追加到该节点。若两次引用,第二次会覆盖第一次,导致eeprom节点丢失。
坑4:pinctrl未正确绑定
错误写法:
&i2c1 { pinctrl-names = "default"; pinctrl-0 = <&i2c1_pins>; // 但&i2c1_pins未定义 };正确写法:
&phoenix { i2c1_pins: i2c1-pins { rockchip,pins = <RK_PIN_REG(2, 0) RK_PIN_REG(2, 1)>; rockchip,pin-control = <RK_PINCTRL_FUNC_2>; }; }; &i2c1 { pinctrl-names = "default"; pinctrl-0 = <&i2c1_pins>; };pinctrl-0必须指向一个已定义的pinctrl节点,否则驱动初始化失败。
坑5:clocks引用路径错误
错误写法:
clocks = <&cru SCLK_I2C1>; // &cru节点不存在正确写法(RK3568中为&pmu):
clocks = <&pmu SCLK_I2C1>;不同SoC的clock controller节点名不同,需查阅对应SoC的DTSI文件。
排查方法:编译DTS后,用dtc反编译生成的DTB:
dtc -I dtb -O dts -o debug.dts rk3568-evb.dtb检查i2c@fe5a0000节点下是否有compatible、reg、clocks等属性,以及eeprom@50是否作为子节点存在。这是最直接的验证手段。
3.2 i2c-tools失效场景:当命令行工具也“装死”
i2c-tools并非万能,以下场景它会失效,需切换策略:
场景1:从机地址动态变化
某些传感器(如BME280)支持通过ADDR引脚切换地址(0x76或0x77),但i2cdetect扫描时,若ADDR引脚电平不稳定,可能扫到两个地址,或一个都不扫到。此时需用逻辑分析仪抓取实际通信,确认主控发送的地址。
场景2:I2C总线被其他进程独占
Linux内核中,I2C总线是互斥资源。若一个进程(如OLED显示服务)正持有/dev/i2c-1,i2cdetect会返回Device or resource busy。解决方法:
- 查找占用进程:
sudo lsof /dev/i2c-1; - 临时停用服务:
sudo systemctl stop oled-display.service; - 或在代码中使用
I2C_SLAVE_FORCEioctl强制设置地址(不推荐,可能破坏其他进程通信)。
场景3:高速模式(3.4MHz)下工具延迟i2c-tools基于ioctl实现,内核I2C core需将用户请求转换为底层传输,引入毫秒级延迟。在HS模式下,单次传输耗时<100μs,i2cget的延迟远大于传输本身,导致时序错乱。此时必须用内核驱动或裸机代码直接操作寄存器。
场景4:从机需要特定启动序列
某些OLED(如SH1106)上电后需发送初始化指令序列(包括0xAE关显示、0xD5设时钟分频等),才能响应后续命令。i2cget/i2cset无法模拟这一过程,必须写专用初始化程序。我通常用Python的smbus2库:
import smbus2 bus = smbus2.SMBus(1) init_seq = [0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40] for cmd in init_seq: bus.write_byte(0x3C, cmd) # 0x3C为OLED地址 bus.write_byte(0x3C, 0xAF) # 开显示场景5:总线存在多个主控
在多处理器系统(如ARM+DSP)中,I2C总线可能被多个主控共享。i2cdetect只反映当前主控视角,若另一主控正在通信,i2cdetect可能收到NACK。此时需协调主控间的访问仲裁,或使用I2C总线锁机制。
3.3 推挽 vs 开漏:一个选择,两种命运
I2C物理层的核心是“开漏输出”(Open-Drain),这是协议强制要求。但很多初学者误以为MCU的GPIO可以设为推挽(Push-Pull)模式驱动I2C,结果发现通信失败。原因在于:
- 开漏模式:GPIO只能拉低电平(0),高电平靠外部上拉电阻实现(1)。这样允许多个设备共享总线:任一设备拉低,总线即为0;所有设备释放,总线被上拉为1。这是I2C多主控、多从机的基础。
- 推挽模式:GPIO可主动输出0或1。若两个设备同时输出,一个拉高一个拉低,将形成短路电流,烧毁GPIO。
所以,所有I2C主控和从机的SCL/SDA引脚,必须配置为开漏模式。在STM32中,GPIO模式设为GPIO_MODE_OUTPUT_OD;在RK3568中,pinmux配置为FUNC_2(I2C功能)即自动启用开漏。
但有一个例外:某些MCU的I2C外设自带弱上拉,可配置为“准双向”模式(如51单片机)。此时无需外部上拉,但驱动能力弱,总线电容受限。现代SoC(ARM Cortex-A/M系列)均不支持此模式,必须外接上拉。
电平转换是另一个高频问题。3.3V主控与5V从机共存时,不能简单用分压电阻,因为I2C是双向总线,分压会削弱驱动能力。正确方案是双向电平转换器,如PCA9306、TXS0108E。选型要点:
- 方向控制:PCA9306需外部提供方向信号(DIR),TXS0108E自动检测,更简单;
- 供电电压:VCCA接主控电压(3.3