1. 从一根线说起:为什么I2C总让人又爱又恨
搞嵌入式的人,几乎都绕不开I2C。两根线,一根SCL时钟,一根SDA数据,挂上一堆设备,EEPROM、OLED、传感器、数字电位器、DAC,甚至某些电源管理芯片的反馈调节都能走I2C。省引脚、协议简单、支持多设备,听起来很美。但真正上手之后你会发现,I2C是那种“入门五分钟,调通两星期”的典型代表。尤其是当你面对一个0.9寸的OLED死活不亮,或者读写EEPROM偶尔丢一个字节的时候,那种抓狂感非常真实。
这个项目标题叫“硬件I2C和软件I2C谁更坑”,其实问的是每个嵌入式工程师迟早要面对的一个选择题:我到底该用MCU自带的硬件I2C外设,还是自己用GPIO模拟时序?这两个方案各有各的坑,而且坑的类型完全不同。硬件I2C的坑往往藏在参考手册的某个角落,软件I2C的坑则更多体现在时序精度和代码结构上。
这篇文章适合所有正在用STM32、GD32、CH32或者其他MCU做I2C通信的朋友,不管你是刚接触I2C通信协议的新手,还是已经调过好几块板子的老手,我都会把这两种方案的核心细节、实操要点、常见问题和排查思路讲清楚。关键词I2C、硬件I2C、软件I2C、GPIO会贯穿全文,我会尽量用实际项目中的例子来说明,而不是照本宣科地念时序图。
先说结论方向:硬件I2C和软件I2C没有绝对的优劣,只有适不适合你的场景。但如果你不了解它们各自的“坑点分布”,那不管选哪个都会踩得很惨。下面我从设计思路开始拆解。
2. 硬件I2C与软件I2C的方案选型与核心思路拆解
2.1 硬件I2C到底帮你做了什么
硬件I2C的本质是MCU内部有一个专门的I2C外设控制器,它帮你处理了起始条件、停止条件、ACK/NACK应答、时钟生成、数据移位这些底层动作。你只需要配置好寄存器,把数据丢进数据寄存器,然后等标志位就行了。从CPU的角度看,这大大减轻了负担,因为时序的精确控制由硬件保证,不受中断延迟或代码执行时间的影响。
以STM32F4系列为例,它的I2C外设支持标准模式100kHz、快速模式400kHz,部分型号还支持1MHz的快速模式Plus。你通过配置I2C_CR2寄存器里的FREQ字段告诉外设你的APB时钟频率,然后设置CCR寄存器来决定SCL的时钟高低电平时间。这些参数的计算在参考手册里有明确公式,比如标准模式下:
CCR = T_PCLK1 * (SCL_high + SCL_low) / 2其中T_PCLK1是APB1时钟周期。如果你APB1跑42MHz,想要100kHz的SCL,那CCR大约等于210。这个计算过程看起来简单,但实际配置的时候,很多人会忘记TRISE寄存器的设置,导致上升沿时间不满足规范,通信不稳定。
硬件I2C最大的优势在于:时序由硬件保证,CPU占用低,适合高速或大数据量传输的场景。但它的坑也很集中:不同厂商的I2C外设行为差异大,有些型号的硬件I2C存在已知的errata,中断标志位的清除顺序有讲究,DMA配合使用时更容易出问题。
2.2 软件I2C为什么还有人用
软件I2C,说白了就是用两个GPIO引脚,一个当时钟线,一个当数据线,通过代码手动拉高拉低来模拟I2C时序。你不需要MCU有硬件I2C外设,甚至可以用任意两个普通GPIO来实现。这对于那些硬件I2C外设不够用、或者硬件I2C有bug的MCU来说,是非常实用的备选方案。
软件I2C的核心在于延时控制。I2C协议规定了标准模式下SCL频率最高100kHz,快速模式400kHz。你在代码里通过插入延时函数来控制高低电平的持续时间。比如下面这段典型的软件I2C起始条件代码:
void I2C_Start(void) { SDA_HIGH(); SCL_HIGH(); delay_us(4); SDA_LOW(); delay_us(4); SCL_LOW(); delay_us(4); }这段代码看起来简单,但delay_us的精度直接决定了时序是否合规。如果你用的是系统滴答定时器做延时,那中断一来,时序就可能被拉长。更麻烦的是,不同编译器的优化等级会影响代码执行时间,你调好的延时在Debug模式下能跑,切到Release模式就可能因为优化而变短,导致通信失败。
软件I2C的优势在于灵活:引脚随便选,不受硬件外设限制,调试的时候可以用逻辑分析仪直接抓波形,出问题容易定位。但它的缺点也很明显:占用CPU时间,高速通信时CPU几乎被占满,而且时序精度依赖延时函数的稳定性。
2.3 选型背后的真实考量
在实际项目中,我选择硬件I2C还是软件I2C,通常看几个因素。第一,通信速率要求。如果我要读写EEPROM,数据量不大,100kHz足够了,软件I2C完全能胜任。但如果我要驱动一个128x64的OLED,刷新率要求高,那硬件I2C的400kHz甚至1MHz优势就体现出来了。
第二,MCU的硬件I2C外设是否可靠。有些早期型号的硬件I2C确实存在死锁问题,比如在从设备拉低SCL做时钟延展的时候,主设备如果处理不当就会卡死。这种情况下,软件I2C反而更可控。
第三,引脚资源。如果PCB已经画好了,I2C引脚刚好不在硬件I2C外设的复用引脚上,那只能用软件I2C。这种情况在项目后期改板或者兼容旧板子的时候特别常见。
第四,开发周期。硬件I2C的配置和调试需要查手册、算参数、处理中断,前期投入大。软件I2C的代码结构简单,移植方便,但后期如果通信不稳定,排查起来也不轻松。
提示:不要因为硬件I2C“看起来高级”就无脑选它,也不要因为软件I2C“简单”就轻视它。选型的核心是匹配你的实际需求和调试能力。
3. 核心细节解析:硬件I2C的寄存器配置与软件I2C的时序控制
3.1 硬件I2C的时钟配置与常见参数计算
硬件I2C的配置核心是时钟参数。以STM32F407为例,假设APB1时钟为42MHz,我们要配置100kHz的标准模式。参考手册给出的公式是:
- 当CCR值大于等于4时,使用公式:CCR = PCLK1 / (2 * SCL_freq)
- 所以CCR = 42000000 / (2 * 100000) = 210
然后TRISE寄存器的值取决于SCL上升沿的最大允许时间。标准模式下最大上升时间是1000ns,快速模式是300ns。TRISE的计算公式是:TRISE = (最大上升时间 / T_PCLK1) + 1。对于42MHz的APB1,T_PCLK1约等于23.8ns,所以标准模式下TRISE = (1000 / 23.8) + 1 ≈ 43。
这些参数配置好之后,你还需要使能I2C外设,设置地址模式(7位还是10位),配置ACK使能等。发送数据的时候,流程通常是:发送起始条件,等待SB标志置位,发送从机地址加写位,等待ADDR标志,发送寄存器地址,等待TXE标志,发送数据,等待BTF标志,发送停止条件。
这个流程里最容易出问题的地方是标志位的清除顺序。比如ADDR标志的清除方式是“先读SR1寄存器,再读SR2寄存器”,如果你顺序搞反了,ADDR标志清不掉,后续通信就会卡住。这种细节在参考手册里写了,但很多人调试的时候不会仔细看,结果就是代码跑着跑着就死在while循环里。
3.2 软件I2C的延时精度与GPIO模式选择
软件I2C的时序控制全靠GPIO的拉高拉低和延时。这里有两个关键点:GPIO的输出模式选择和延时函数的精度。
先说GPIO模式。I2C总线是开漏结构,SCL和SDA都需要外部上拉电阻。所以GPIO应该配置为开漏输出模式,这样引脚只能拉低,拉高的时候靠外部上拉电阻。如果你配置成推挽输出,那当多个设备同时驱动总线的时候,就可能出现两个设备一个拉高一个拉低,形成短路电流,长期下来可能损坏引脚。
在STM32的HAL库中,配置开漏输出的代码是这样的:
GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);注意这里的Mode是GPIO_MODE_OUTPUT_OD,也就是开漏输出。Pull设置为上拉,虽然外部已经有上拉电阻了,但内部上拉可以作为辅助,特别是在外部上拉电阻值较大的时候。
再说延时精度。软件I2C的延时通常用两种方式实现:一种是空循环,一种是系统滴答定时器。空循环的延时时间受编译器优化影响很大,比如:
void delay_us(uint32_t us) { for(uint32_t i = 0; i < us * 10; i++) { __NOP(); } }这个10这个系数需要根据你的MCU主频和编译器优化等级来调整。我一般会用一个示波器或者逻辑分析仪来实测SCL的频率,然后反推这个系数。比如你写的是delay_us(4),实测SCL高电平时间是6微秒,那就说明系数偏大,需要调小。
用系统滴答定时器做延时的话,精度更高,但要注意滴答定时器的中断优先级。如果滴答中断被其他高优先级中断打断,延时就会变长,导致SCL频率降低。所以软件I2C的延时函数最好用硬件定时器或者精确的NOP循环来实现。
3.3 开漏模式与推挽模式的本质区别
I2C总线为什么必须用开漏模式?这个问题很多人没想明白。简单来说,I2C是多主多从的总线结构,任何时刻只能有一个设备驱动总线。如果两个设备同时驱动,一个想拉高,一个想拉低,推挽输出就会形成从电源到地的低阻通路,电流可能达到几十毫安,足以烧毁引脚。
开漏输出的结构是:输出级只有一个N沟道MOS管,漏极开路。当输出低电平时,MOS管导通,引脚被拉到地。当输出高电平时,MOS管截止,引脚处于高阻态,靠外部上拉电阻把电平拉高。这样即使多个设备同时输出高电平,也不会有电流冲突,因为大家都是高阻态。
所以你在配置软件I2C的GPIO时,一定要用开漏模式。如果你用的是推挽模式,短时间可能也能通信,因为大多数时候只有一个设备在驱动。但一旦出现总线竞争,或者从设备做时钟延展拉低SCL的时候,问题就会暴露出来。
注意:有些MCU的GPIO在开漏模式下,内部上拉电阻比较弱,典型值在30k到50k欧姆。如果你的I2C总线电容较大,比如挂了多个设备或者走线较长,上升沿会变得很慢,导致通信失败。这时候需要在外部加2.2k到4.7k欧姆的上拉电阻。
4. 实操过程:从零搭建一个稳定的I2C通信链路
4.1 硬件I2C的初始化与EEPROM读写实操
我以STM32F407读写AT24C02 EEPROM为例,走一遍硬件I2C的完整流程。AT24C02的7位地址是0x50,写操作地址是0xA0,读操作地址是0xA1。
首先配置I2C外设:
I2C_HandleTypeDef hi2c1; hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(&hi2c1);注意NoStretchMode这个参数,我一般设置为DISABLE,也就是允许从设备做时钟延展。AT24C02在写周期内会拉低SCL,如果主设备不支持时钟延展,就会误判为通信错误。
写一个字节到AT24C02的流程是:发送起始条件,发送设备地址0xA0,等待ACK,发送内存地址,等待ACK,发送数据,等待ACK,发送停止条件。用HAL库的话,一行代码就能搞定:
HAL_I2C_Mem_Write(&hi2c1, 0xA0, mem_addr, I2C_MEMADD_SIZE_8BIT, &data, 1, 1000);但这里有个坑:AT24C02在收到停止条件后,会进入内部写周期,大约5毫秒。在这期间,它不会响应任何I2C命令。如果你紧接着就发下一个写命令,就会因为收不到ACK而失败。所以每次写操作之后,要么延时5毫秒,要么用“应答查询”的方式,反复发送起始条件和设备地址,直到收到ACK为止。
读操作的流程稍微复杂一点:先发送设备地址0xA0,发送内存地址,然后重新发送起始条件,发送设备地址0xA1,然后读取数据。用HAL库的话:
HAL_I2C_Mem_Read(&hi2c1, 0xA1, mem_addr, I2C_MEMADD_SIZE_8BIT, &data, 1, 1000);这个函数内部会自动处理重复起始条件。但如果你用寄存器操作,就要手动实现这个流程,注意在发送完内存地址后,要重新发送起始条件,而不是停止条件。
4.2 软件I2C驱动0.9寸OLED的完整流程
0.9寸OLED通常用SSD1306驱动芯片,I2C地址是0x78。我用软件I2C来驱动它,引脚选PB6作为SCL,PB7作为SDA。
首先定义基本的GPIO操作宏:
#define SCL_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET) #define SCL_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET) #define SDA_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET) #define SDA_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET) #define SDA_READ() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7)然后实现起始条件、停止条件、发送字节、接收应答这些基本函数。发送字节的时候,先发最高位,然后拉高SCL,延时,拉低SCL,循环8次。发送完8位后,释放SDA,拉高SCL,读取SDA电平,判断是否收到应答。
uint8_t I2C_WriteByte(uint8_t data) { for(uint8_t i = 0; i < 8; i++) { if(data & 0x80) SDA_HIGH(); else SDA_LOW(); data <<= 1; delay_us(2); SCL_HIGH(); delay_us(4); SCL_LOW(); delay_us(2); } SDA_HIGH(); delay_us(2); SCL_HIGH(); delay_us(4); uint8_t ack = SDA_READ(); SCL_LOW(); delay_us(2); return ack; }这段代码里,delay_us的参数需要根据你的MCU主频来调整。我用的STM32F407跑168MHz,delay_us(2)大约对应2微秒。实测下来,SCL频率大约在100kHz左右,符合标准模式的要求。
驱动OLED的时候,先发送命令字节,再发送数据字节。SSD1306的命令控制字节是0x00,数据控制字节是0x40。初始化序列包括设置显示时钟分频、多路复用率、显示偏移、起始行、电荷泵使能、内存寻址模式等。这些命令的具体值可以参考SSD1306的数据手册。
实操心得:软件I2C驱动OLED的时候,如果屏幕不亮,先用逻辑分析仪抓一下SCL和SDA的波形,确认起始条件和地址字节是否正确。很多时候问题出在地址上,0x78是写地址,0x79是读地址,别搞混了。
4.3 逻辑分析仪抓波形与参数验证
不管是硬件I2C还是软件I2C,调试的时候逻辑分析仪是必备工具。我用的是一款几十块钱的8通道逻辑分析仪,配合开源软件就能解码I2C协议。
抓波形的时候,重点看几个地方:起始条件的建立时间、SCL的高电平和低电平持续时间、数据建立时间和保持时间、ACK应答是否正常。标准模式下,SCL高电平至少4微秒,低电平至少4.7微秒。数据建立时间至少250纳秒,保持时间至少0纳秒。
如果你发现SCL频率远低于100kHz,比如只有50kHz,那可能是延时函数太长了。如果SCL波形上升沿很慢,像锯齿波一样,那可能是上拉电阻太大或者总线电容太大。这时候可以尝试减小上拉电阻,比如从10k换成4.7k。
还有一种情况是通信偶尔失败,抓波形发现某个字节的ACK位是高电平,说明从设备没有应答。这可能是从设备忙,比如EEPROM在写周期内,或者地址不对,或者从设备根本没接好。排查的时候先确认硬件连接,再确认地址,最后检查时序。
5. 常见问题与排查技巧实录
5.1 硬件I2C的典型死锁与总线恢复
硬件I2C最常见的问题是死锁。现象是代码卡在等待某个标志位的while循环里,比如等待ADDR标志或者BTF标志。造成死锁的原因通常是:从设备在通信过程中复位了,或者总线被意外拉低,导致主设备无法产生停止条件。
解决死锁的一个常用方法是手动恢复总线。具体操作是:把SCL配置为普通GPIO,手动发送9个时钟脉冲,让从设备把剩余的数据位发完,然后发送停止条件。代码大概是这样:
void I2C_Bus_Recovery(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_6; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); for(int i = 0; i < 9; i++) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); delay_us(5); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); delay_us(5); } // 发送停止条件 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET); delay_us(5); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); delay_us(5); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); delay_us(5); }这个恢复流程在STM32的I2C errata文档里也有提到。如果你用的是HAL库,可以在I2C初始化失败或者通信超时的时候调用这个函数,然后重新初始化I2C外设。
5.2 软件I2C的时序漂移与中断干扰
软件I2C的时序漂移通常有两个原因:一是延时函数不精确,二是中断干扰。延时函数的问题前面说过了,这里重点说中断。
假设你的软件I2C正在发送数据,突然来了一个中断,CPU去处理中断了,SCL和SDA的电平就保持不动。如果这个中断处理时间比较长,比如几百微秒,那从设备可能会认为通信超时,或者把这段时间当成时钟延展。但问题是,从设备做时钟延展是拉低SCL,而你的SCL是高电平,从设备可能会误判。
解决方法是:在软件I2C的时序关键段,关闭全局中断。比如在发送起始条件、发送字节、接收应答这些函数里,用__disable_irq()和__enable_irq()把中断关掉。但这样会影响系统的实时性,所以关中断的时间要尽量短。
另一种方法是提高软件I2C任务的优先级,或者把软件I2C放在定时器中断里执行,保证时序的确定性。但这样代码结构会复杂一些。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 通信完全无响应 | 硬件连接错误 | 检查SCL/SDA是否接反,上拉电阻是否焊接 | 重新接线,补焊上拉电阻 |
| 偶尔丢数据 | 时序不满足规范 | 用逻辑分析仪抓波形,检查建立/保持时间 | 调整延时参数,减小上拉电阻 |
| 硬件I2C卡死 | 总线死锁 | 检查从设备是否复位,SCL/SDA是否被拉低 | 执行总线恢复流程,重新初始化 |
| 软件I2C频率偏低 | 延时函数太长 | 实测SCL频率 | 减小延时系数,或用硬件定时器 |
| EEPROM写失败 | 写周期未结束 | 检查是否延时5ms或应答查询 | 增加延时或实现应答查询 |
| OLED不亮 | 地址错误或初始化序列不对 | 抓波形确认地址字节 | 确认地址0x78,检查初始化命令 |
| ACK位为高 | 从设备忙或地址不对 | 确认从设备地址和状态 | 等待从设备就绪,检查地址 |
| SCL上升沿缓慢 | 上拉电阻太大或总线电容大 | 测量上升时间 | 换小阻值上拉电阻,缩短走线 |
避坑技巧:如果你用的是GD32或者CH32这些国产MCU,硬件I2C的行为可能和STM32有差异。比如GD32F407的I2C时序参数计算方式和STM32不完全一样,建议直接参考GD32的固件库例程,不要照搬STM32的代码。
6. 硬件I2C与软件I2C的实战对比与个人选择建议
6.1 性能、资源占用与调试难度对比
从性能上看,硬件I2C在高速通信时优势明显。400kHz的快速模式下,硬件I2C的CPU占用率很低,因为数据移位和时钟生成都是硬件完成的。软件I2C在400kHz时,CPU几乎一直在执行GPIO操作和延时,很难同时处理其他任务。
从资源占用上看,硬件I2C需要占用专用的外设引脚,而且不同MCU的I2C外设数量有限。软件I2C可以用任意GPIO,灵活性更高。但软件I2C会占用CPU时间,如果你的系统对实时性要求高,软件I2C可能会成为瓶颈。
从调试难度上看,硬件I2C的问题往往更隐蔽,因为你看不到底层的时序,只能通过标志位和错误码来判断。软件I2C的时序是代码控制的,你可以直接抓波形,看到每一个电平变化,排查起来更直观。
| 对比维度 | 硬件I2C | 软件I2C |
|---|---|---|
| 通信速率 | 最高可达1MHz以上 | 通常100kHz到400kHz |
| CPU占用 | 低 | 高 |
| 引脚灵活性 | 受限于外设复用引脚 | 任意GPIO |
| 时序精度 | 硬件保证,精度高 | 依赖延时函数,易受干扰 |
| 调试难度 | 问题隐蔽,依赖标志位 | 波形可见,排查直观 |
| 多设备支持 | 原生支持 | 需要软件处理总线仲裁 |
| 代码移植性 | 不同MCU差异大 | 移植方便,改引脚定义即可 |
6.2 我的实际项目选择经验
在我做过的项目里,EEPROM读写、传感器配置这类低速、小数据量的场景,我倾向于用软件I2C。因为代码简单,移植方便,而且出问题容易定位。比如我之前用CH32V307驱动一个I2C接口的温度传感器,硬件I2C的例程跑不通,换成软件I2C十分钟就调通了。
但如果是OLED刷新、音频数据流这类高速、大数据量的场景,我会优先用硬件I2C。比如用STM32F4驱动128x64的OLED,硬件I2C跑400kHz,刷新率能到60帧以上,软件I2C最多只能到20帧左右。
还有一种情况是硬件I2C外设不够用。比如你要挂5个I2C设备,但MCU只有2个硬件I2C外设,那剩下的3个设备就只能用软件I2C。这时候可以用GPIO模拟多路I2C,每路用不同的引脚,互不干扰。
个人体会:不要迷信硬件I2C,也不要轻视软件I2C。我见过太多人因为硬件I2C调不通,项目卡了好几天,最后换成软件I2C半小时搞定。也见过软件I2C在高速场景下丢数据,换成硬件I2C后问题消失。关键是理解两者的原理和适用边界。
6.3 混合方案与进阶思路
有时候,最好的方案是混合使用。比如主通信链路用硬件I2C,保证速率和稳定性。备用链路或者低速设备用软件I2C,提高灵活性。这样既能发挥硬件I2C的性能优势,又能利用软件I2C的引脚灵活性。
还有一种进阶思路是用DMA配合硬件I2C。比如你要从EEPROM读取大量数据,可以用DMA把I2C数据寄存器里的数据自动搬运到内存,CPU只需要在DMA传输完成中断里处理数据就行了。这样CPU占用率极低,适合低功耗场景。
软件I2C也可以优化。比如用定时器中断来产生SCL时钟,而不是用延时函数。这样时序更精确,而且CPU可以在等待期间处理其他任务。不过这种实现方式代码复杂度较高,适合对时序要求严格的场景。
最后再分享一个小技巧:不管用硬件I2C还是软件I2C,都建议在PCB上预留I2C总线的测试点,方便用逻辑分析仪抓波形。另外,上拉电阻的阻值不要照搬参考设计,要根据实际的总线电容和通信速率来调整。一般来说,100kHz可以用4.7k到10k,400kHz建议用2.2k到4.7k。如果总线电容超过200pF,可能需要更小的阻值。
这个内容后续还可以这样扩展:比如用Python的linuxpy库在Linux系统下操作I2C设备,或者用Verilog实现I2C从机控制器。这些方向都很有意思,但那是另一个话题了。