经典三问
Q: I2C 是什么?
A: 两根线(SCL 时钟、SDA 数据)上的同步串行协议,靠地址在一根总线上挑出要通信的那台设备。
Q: 为什么 I2C 能一根总线挂很多设备?
A: 所有设备共用这两根线,靠地址区分目标;串口则更像一对一的通道;空闲时线是高电平,谁说话由主机安排。
Q: I2C 常见使用场景?
A: 同一块板子上的屏幕、陀螺仪、EEPROM、各种传感器------引脚紧张、设备多、速率要求不高时最合适。
你早就在用它了。
从 OLED 上出现第一行字那天起,每一帧画面都是这条总线送过去的:清屏、写一个像素、显示一个数字,背后都是 PB8 和 PB9 上的一串电平。那时候我们只需要知道"接对四根线,调驱动就能显示",不需要知道线上到底发生了什么。
下面回头把那两根线讲透:为什么两根线能挂住一堆设备、为什么它们必须"松手"、为什么数据只能在时钟低电平的时候改、地址和应答在线上长什么样。讲完给你一份最小的软件 I2C------每一拍都由程序亲手拨出来,再用一次地址扫描把"地址"从概念变成屏幕上的一个数字。
串口是一对一,I2C 是一对多
串口那几篇里,PA9 和 PA10 连的是电脑,一条线两头各一个设备。I2C 的目标不一样:一根总线上挂好几台设备,还要让它们互不打架。
| 串口(UART) | I2C | |
|---|---|---|
| 线 | TX、RX,各走一个方向 |
SCL、SDA,共用 |
| 双工 | 全双工(能同时收发) | 半双工(同一时刻只有一个方向) |
| 时钟 | 异步:两边事先约好波特率 | 同步:有一根专门的时钟线 |
| 设备数 | 点对点,两个 | 一条总线多台,靠地址区分 |
| 谁定节奏 | 两边各自按波特率计时 | 主机产生时钟,从机跟着走 |
最后一行是理解 I2C 的钥匙:产生时钟的那个人是主机。 从机不能自己开口说"我要发数据",它只能等主机点名。所以"多主多从"这种说法里,主机的身份是动态的------谁先占用总线、谁在产生时钟,谁就是这一刻的主机。
时钟线带来的好处很直接:不需要预先约定速度,慢一点的从机可以随时拉住时钟让主机等(这叫时钟延展),主机收到的是一个准确的"还没准备好",而不是一串错位的数据。
两根线凭什么挂住一堆设备:开漏加上拉
先看空闲时的状态:没人说话的时候,SCL 和 SDA 都是高电平。
这个高电平不是哪个设备"推"上去的,而是上拉电阻把线拉上去的。每个设备的这两个引脚都配成开漏输出 :它只能主动把线拉到低电平,想要高电平就只能松手,让上拉电阻把线拉回去。
写成代码就是一句容易误会的话:
c
GPIO_SetBits(GPIOB, GPIO_Pin_9); /* SDA 写 1:不是推高,而是松手 */
松手之后线是高是低,由别人决定。
为什么非得这样? 因为一根线上有七八个设备,如果每个都能把线推高、也能拉低,那么一个推高、一个拉低的时候,电流直接从电源穿过两个输出级------轻则波形出错,重则烧管子。开漏把"推高"这个动作取消了,所有设备只能拉低,于是:
- 任何一个设备拉低,线就是低(这叫线与);
- 所有设备都松手时,线才回到高。
这条规则马上带来一个必须的后果:接收方才有资格在第 9 拍把数据线拉低,表示"我收到了"。 如果主机是推挽输出、把线牢牢顶在高电平,从机根本拉不动它,应答就没法实现。开漏不只是为了安全,它是应答机制能存在的前提。
上拉电阻的取值要挑一下:
- 太大(比如 100kΩ):松手以后线回升得慢,上升沿变缓,速率一高就来不及拉到高电平,通信时好时坏;
- 太小(比如 1kΩ):线被拉低时,灌进驱动级的电流变大,可能超过器件能承受的范围;
- 常见取值在 4.7kΩ 左右,这也是各种模块和自己搭电路时最常写的一个数。
还有两件常被忽略的事:
模块自带的上拉会并联。 你手上那些 OLED、传感器模块,很多已经在板上焊了上拉电阻。挂两块,等效上拉就变小一半;挂四块,拉低时的电流翻两番------挂得多的时候要按并联后的阻值算一次灌电流。
上拉接到哪一路电压,总线的高电平就是多少。 上拉接 3.3V,总线高电平就按 3.3V 算;这条总线上如果有 5V 设备,就得先看清楚它的引脚能不能接受 3.3V 的高电平,以及它内部有没有自己的上拉(那会变成额外的一路)。
最后是线长和电容。线越长,越容易拾取干扰,同时线本身的电容也越大------设备挂得多、杜邦线拉得长,上升沿都会变缓。所以 I2C 天生是板内协议:几十厘米以内很舒服,拉出一米就开始看运气。真要跑得远,要么降速,要么换差分或无线那一类方案。
时序的四个基本单元
I2C 的全部规则可以压缩成一句:时钟线为高电平的时候,数据线必须保持稳定。
实际规则是:数据只允许在 SCL 为低电平时改变。两个例外是起始和停止,它们故意违反这条规则,用来标记"开始"和"结束"。记住这一句,时序图就不用背了。
四个基本单元,逐个看:
| 单元 | 线怎么动 | 谁在动 |
|---|---|---|
| 起始 START | SCL 为高时,SDA 由高变低 |
主机 |
| 停止 STOP | SCL 为高时,SDA 由低变高 |
主机 |
| 发一个字节 | SCL 低时放好数据位 → SCL 拉高(对方在这期间读)→ 再拉低,重复 8 次 |
发送方 |
| 应答 | 第 9 拍:接收方拉低表示收到(ACK),松手表示没收到(NACK) | 接收方 |
起始与停止的动作都在时钟高电平期间完成,所以它们和普通的数据位不会混淆。总线空闲时两根线都是高。
发送一个字节 是高位先行:先把最高位放到 SDA,再把 SCL 拉高,让对方在这个高电平窗口里读走,然后拉低时钟准备下一位。八次之后,一个字节就出去了。
接收一个字节 反过来:接收方要先松手(把 SDA 释放成高),由对方逐位放数据,自己在 SCL 高电平期间读。
第 9 拍是应答,这一拍最容易被漏掉,也最有用:
- 发送方发完 8 位之后,第 9 拍要松手,把线让给接收方;
- 接收方如果收到了,就把
SDA拉低,这一拍读出来是 0,叫 ACK ;没收到或者不想再收,就松手,读出来是 1,叫 NACK; - 主机发完一个字节,要收 对方的应答;主机收完一个字节,要发应答;
- 主机读最后一个字节时,必须发 NACK,意思是"够了,别再发"。我第一次写 I2C 读时序时就漏过这一步,结果从机继续往外吐数据。
地址:一根线上怎么点名
起始条件之后,主机送出的第一个字节不是数据,而是地址:
text
1 个字节: D7 D6 D5 D4 D3 D2 D1 D0
└──── 7 位地址 ────┘ └ 方向:0 写,1 读
所以 7 位地址在线上是两个不同的数。常见的 OLED 地址 0x3C 是 7 位地址:
- 7 位地址:
0x3C=011 1100; - 写地址字节:
0x3C << 1 | 0=0x78; - 读地址字节:
0x3C << 1 | 1=0x79。
0x3C 和 0x78 经常出现在同一个工程的两处注释里,说的就是这两个层次。判断的办法很简单:看它有没有把读写位算进去。
地址还有几个要知道的点:
- 10 位地址 是另一套格式,用两个字节发送,前 5 位固定成
11110。手上大部分小模块用不到,遇到了按手册写就行。 - 地址
0x00是广播地址,所有支持广播的设备都会应答,写进去的数据会同时到达所有设备。有些设备靠它做统一复位。 - 同一条总线上不能有两个同地址的设备。 两个都焊上去以后,它们会同时应答、同时听数据:读的时候数据会"打架",写的时候两台设备都被改掉。地址冲突是那种"现象随机、查起来很累"的故障------设备上的地址引脚(比如 MPU6050 的
AD0)就是为了让你换地址的。
还有一个概念要在这里立起来:从机内部通常有自己的"地址指针"。 主机除了要知道"跟哪台设备说话",还要告诉它"读/写它内部的哪个寄存器"。这个内部地址不在这 7 位里,而是跟在设备地址后面,作为普通数据发过去的------下一节就能看到它。
一次完整的读写:三种最常用的时序
把上面的单元拼起来,就得到实际使用中反复出现的三条时序。
指定地址写(往某个设备的某个寄存器写一个字节):
text
起始 → 设备地址+写 → ACK → 寄存器地址 → ACK → 数据 → ACK → 停止
当前地址读 (从设备内部指针当前指的寄存器读):
text
起始 → 设备地址+读 → ACK → 数据 → NACK → 停止
它读的是"上次停在哪儿",所以单独用它的场合不多。
指定地址读(读某个指定寄存器,最常用):
text
起始 → 设备地址+写 → ACK → 寄存器地址 → ACK → 重复起始 → 设备地址+读 → ACK → 数据 → NACK → 停止
注意中间那个重复起始 :它是"再发一次起始条件",但不是先停止再起始。为什么要这么绕?因为从机不知道自己该读哪个寄存器------得先用一次"写"把它的内部指针挪到目标位置,然后马上转成读。中间如果插一个停止条件,总线会被释放,别的设备可能抢进来,内部指针的状态也不再由我们掌控。重复起始就是为这件事准备的。
拿 OLED 对照一下:我们平时那一串 0x00、0x40、0xB0 | page,就发生在"数据"那一段里。这些字节怎么解释,是 SSD1306 自己的规定,不在 I2C 协议里------协议只管把字节安全地送到,送到以后是什么意思,归设备自己说。
自己拨一遍:最小软件 I2C
到这里,协议已经完整了。剩下的问题是:这些电平谁来拨?
我们前面用的 OLED 驱动就是这么干的:代码直接操作 PB8 和 PB9,一位一位把波形拨出来。这种实现叫软件 I2C。它的好处是每一拍都看得见------时序图上的每个箭头都能在代码里找到对应的那一行。
引脚沿用前面的分配:PB8 是 SCL,PB9 是 SDA。
先把最底下那三个小函数写出来:写 SDA、写 SCL、读 SDA。整个软件 I2C 的执行机构就是它们。
c
static void SoftI2C_SDA(uint8_t high)
{
if (high != 0U)
{
GPIO_SetBits(GPIOB, GPIO_Pin_9);
}
else
{
GPIO_ResetBits(GPIOB, GPIO_Pin_9);
}
}
static void SoftI2C_SCL(uint8_t high)
{
if (high != 0U)
{
GPIO_SetBits(GPIOB, GPIO_Pin_8);
}
else
{
GPIO_ResetBits(GPIOB, GPIO_Pin_8);
}
}
static uint8_t SoftI2C_ReadSda(void)
{
return (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9) == Bit_SET) ? 1U : 0U;
}
写的那两个函数里,GPIO_SetBits() 就是"松手":引脚配成开漏之后,写 1 不会把线推高,只把它交还给上拉电阻。
读的那个函数里,GPIO_ReadInputDataBit() 拿到的是引脚上真实 的电平(Bit_SET 就是库里表示"高"的那个值)。开漏输出模式下,引脚的实际电平仍然可以被读回来,这正是 I2C 能在同一根线上"我松手、你说话"的原因------同一个引脚,写的时候是输出,读的时候是输入。
再初始化:两个引脚配成开漏输出,然后都松手,让总线回到空闲状态。
c
static void SoftI2C_Init(void)
{
GPIO_InitTypeDef gpio;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
gpio.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
gpio.GPIO_Mode = GPIO_Mode_Out_OD; /* 开漏:只能拉低,写 1 是松手 */
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &gpio);
SoftI2C_SDA(1U); /* 空闲:两根线都松手 */
SoftI2C_SCL(1U);
}
速率由一个空转函数控制:
c
static void SoftI2C_Delay(void)
{
volatile uint32_t i;
for (i = 0U; i < 20U; i++)
{
}
}
它没有任何"技术含量",但它是软件 I2C 里唯一决定速率的东西:每处电平变化之间插一次,循环次数越大,波形越慢。(循环变量写成 volatile 是为了让编译器别把这段空循环优化掉。)常见写法里也有人干脆不写延时,靠函数自身的开销把速度压下来;显式写出来,是为了让"快慢握在自己手里"这件事看得见。别把它当成精确的微秒延时------它的作用是拉长每一拍,不是定时。
为什么不用前面那套用 SysTick 做的精确延时? 因为它要解决的问题不是"等一个准确的时间"。
从机是靠时钟沿同步的,所以这里需要的是"够慢",不是"准"。换成 Delay_us(1) 反而带来两个麻烦:
- 那个函数每次调用都要读
SysTick->VAL、做一圈跨零判断,开销和你要等的时间是同一个量级,甚至更长。你叫它等 1µs,实际花掉多久并不由那个1决定,而由函数自身决定;想靠调整这个数字来控制 I2C 速率,等于把速率交给了一个与总线无关的机制; - 它依赖 SysTick 一直在跑。软件 I2C 经常被用在中断里(比如传感器拉了一下中断脚,程序就在那个中断里读它的寄存器),再者,
Delay_us()内部会把TICKINT临时关掉再恢复,嵌套调用还会把等待拉长。
但"够慢"这件事有下限,也有上限。 协议里规定的是各种电平的最小宽度,那是下限;上限来自另外两处:
- 从机手册里的最大时钟频率。 SSD1306 这一档标的是 400kHz,再快的从机也总有一个上限,超过了它就不认账;
- 总线上高电平的爬升时间。 上拉电阻和线电容决定了线从低爬到高要多久。时钟周期短到高电平还没爬到位就被拉低,从机读到的就不是"高",波形直接废掉。
这两条一叠加,就成了软件 I2C 移植时最容易踩的坑:它的速率由代码本身决定。 主频、编译优化等级、每次调用函数的那点开销,全都算进每一拍的长短里。于是同一份代码搬到一个主频更高的芯片上,每一拍会自动变快------原本在 72MHz 上刚好合适的空循环,到一百多兆的芯片上可能已经超过从机能接受的频率,甚至短到高电平还没爬上去就被拉低。
现象就是:代码一个字没改,换块板子就黑屏,而且没有任何报错。 没有报错是因为 I2C 从设计上就不打算吼你------它只会安静地不应答。
排查办法很短,要背下来:先把 SoftI2C_Delay() 的循环次数加大十倍。 如果这样就能亮,那问题就是速率,不是接线、不是地址、不是驱动逻辑;然后再逐步往回减,减到刚好稳定,最后留在那个值的两倍上,给换排线、换板子、多加一块设备留出余量。比黑屏更早拿到证据的还有地址扫描:扫到 0x3C,说明此刻这个速率从机还跟得上;一个 ACK 都扫不到,就先别怀疑显示代码。
还有一个和速率无关、症状却一模一样的坑,移植时同样常见:把这两个引脚配成了推挽输出。 那样主机把线牢牢顶在高电平,从机根本拉不低它,第 9 拍永远收不到 ACK,屏幕照样是黑的,但原因完全不同------一个是太快,一个是根本没给从机留说话的余地。开漏输出不是风格问题,是应答能不能存在的前提。
需要精确时间的地方,精确延时是必需的:传感器上电后要等的稳定时间、复位脉冲的宽度、超声波回波超时,那些场合差一点就是错的。而 I2C 的位延时是另一类需求------把节奏放慢到从机跟得上(顺带一提,兼容 SMBus 的那类器件还规定了最长超时,慢到几十毫秒一级就要小心,普通 I2C 器件没有这条)。真嫌它不够快、不够准,答案不是换一个延时函数,而是换一种实现:把产生时钟这件事整个交给硬件。
那次黑屏的经过,和更标准的查法
这类坑我自己踩过一次。把一份 OLED 代码移到另一块芯片上,接线上电,屏幕全黑。接线、地址、供电、初始化顺序都对着查过一遍,代码是验证过能跑的那一份,一个字没改;换一块同样的屏,还是黑。
最后是灵光一闪:把 SoftI2C_Delay() 里的循环次数加大,屏幕亮了。问题确实是速度------新的芯片更快,每一拍短到超过了从机能接受的范围。但我一直记得当时那种别扭:改对了,却不知道自己改对了什么。 那个循环次数是试出来的,不是算出来的;换一块更快的芯片,同样的黑屏会再来一次。
灵感能救急。想要下次不再靠灵感,遇到这类"神秘 bug"更标准的做法是这四步:
第一步,把看不见的东西变成看得见的。 首选逻辑分析仪或示波器:接上 SCL,量一个完整周期有多长(高电平段加低电平段),再和从机手册里的最大频率对一下。没有仪器也有替代办法------地址扫描(扫到 0x3C,说明此刻的速率从机还跟得上),或者用周期计数把每一拍的时间直接"读"出来。
第二步,一次只变一个量,做对照。 把延时翻十倍再跑一遍:亮了,问题就在速率和时序上,接线、地址、驱动逻辑可以先放下;还是黑,那就不是速度,回去查开漏还是推挽、上拉和地址。一次改动排除一半可能性,这就是对照实验的全部价值。
第三步,找临界值,然后退到它的一半。 逐步减小延时,直到刚好出错,记下这个临界点,再退回去一倍------让程序工作在临界速率的一半。留这份余量,是因为换一根排线、换一批模块、温度变一点,临界点都会移动。正好踩在临界点上的代码,是"今天能用"的代码。
第四步,把结论写下来,别让它再变回灵感。 那个循环次数旁边要有一行注释:主频多少、从机手册的上限多少、实际工作频率是多少、为什么留了一倍余量。一个没有来历的数字,就是下一次"神秘"的来源------讲丢字节那一回,屏幕上那两个数字就是为了把"偶尔抽风"变成"有个数字在涨",这里是同一件事。
四个基本单元,逐个落成函数。起始与停止:
c
static void SoftI2C_Start(void)
{
SoftI2C_SDA(1U);
SoftI2C_SCL(1U);
SoftI2C_Delay();
SoftI2C_SDA(0U); /* SCL 高电平期间,SDA 由高变低 = 起始 */
SoftI2C_Delay();
SoftI2C_SCL(0U);
SoftI2C_Delay();
}
static void SoftI2C_Stop(void)
{
SoftI2C_SDA(0U);
SoftI2C_SCL(1U);
SoftI2C_Delay();
SoftI2C_SDA(1U); /* SCL 高电平期间,SDA 由低变高 = 停止 */
SoftI2C_Delay();
}
发一个字节,顺便把第 9 拍的应答读回来:
c
static uint8_t SoftI2C_SendByte(uint8_t data)
{
uint8_t bit;
uint8_t acknowledged;
for (bit = 0U; bit < 8U; bit++)
{
SoftI2C_SDA((uint8_t)((data & 0x80U) != 0U)); /* 高位先行 */
SoftI2C_Delay();
SoftI2C_SCL(1U); /* 对方在这一拍读走 */
SoftI2C_Delay();
SoftI2C_SCL(0U);
SoftI2C_Delay();
data = (uint8_t)(data << 1);
}
SoftI2C_SDA(1U); /* 第 9 拍:松手,把线让给接收方 */
SoftI2C_Delay();
SoftI2C_SCL(1U);
SoftI2C_Delay();
acknowledged = (SoftI2C_ReadSda() == 0U) ? 1U : 0U; /* 被拉低 = ACK */
SoftI2C_SCL(0U);
SoftI2C_Delay();
return acknowledged;
}
收一个字节,应答由调用者决定(读最后一个字节要传 NACK):
c
static uint8_t SoftI2C_ReadByte(uint8_t send_ack)
{
uint8_t bit;
uint8_t data = 0U;
SoftI2C_SDA(1U); /* 先松手,把数据线让给从机 */
for (bit = 0U; bit < 8U; bit++)
{
SoftI2C_SCL(1U);
SoftI2C_Delay();
if (SoftI2C_ReadSda() != 0U)
{
data |= (uint8_t)(0x80U >> bit);
}
SoftI2C_SCL(0U);
SoftI2C_Delay();
}
SoftI2C_SDA((send_ack == 0U) ? 1U : 0U); /* 0 = 发 NACK,1 = 发 ACK */
SoftI2C_Delay();
SoftI2C_SCL(1U);
SoftI2C_Delay();
SoftI2C_SCL(0U);
SoftI2C_Delay();
SoftI2C_SDA(1U); /* 松手,别一直占着数据线 */
return data;
}
对着前面的时序表数一遍:SCL 为高的时候,SDA 从来没有变过------除了起始与停止那两处故意的地方。这就是全部的秘密。
一处观察:地址扫描
现在把"地址"这件事从概念变成一个数字。
做法很直接:取每一个可能的 7 位地址,发一次"起始 + 地址 + 写",然后看第 9 拍有没有 ACK。 有 ACK 就说明这个地址上真有设备在听。
打印地址要用到一个小函数,把 0~15 变成一个十六进制字符:
c
static uint8_t Uart_HexChar(uint8_t value)
{
if (value < 10U)
{
return (uint8_t)('0' + value);
}
return (uint8_t)('A' + (value - 10U));
}
然后就是扫描:
c
static void SoftI2C_Scan(void)
{
uint8_t address;
Uart_SendString("I2C scan start\r\n");
for (address = 0x08U; address <= 0x77U; address++)
{
SoftI2C_Start();
if (SoftI2C_SendByte((uint8_t)(address << 1)) != 0U)
{
Uart_SendString("ACK at 0x");
Uart_SendByte(Uart_HexChar((uint8_t)(address >> 4)));
Uart_SendByte(Uart_HexChar((uint8_t)(address & 0x0FU)));
Uart_SendString("\r\n");
}
SoftI2C_Stop();
}
Uart_SendString("I2C scan end\r\n");
}
Uart_SendString() 和 Uart_SendByte() 是串口那几篇里的两个发送函数:一个发字符串,一个发单个字节。扫描里为什么每次都要配一对 SoftI2C_Start() 和 SoftI2C_Stop()?因为没被应答的那一次传输也把总线占着了,必须用停止条件把它放回空闲,下一个地址才能重新发起始。
扫描范围从 0x08 到 0x77,是因为 0x00~0x07 和 0x78~0x7F 在协议里有特殊用途,正经设备不会把地址放在那里。
接上 OLED 跑一次,串口里应该出现:
text
I2C scan start
ACK at 0x3C
I2C scan end
这一个数字有三层意思,比现象本身重要得多:
- 地址不是别处抄来的一个写法,它真的是线上的一个字节。
0x3C左移一位,就是刚才发出去的那个0x78; - ACK 是设备真的在场。 它把
SDA拉低了,这个动作只有接在总线上的芯片做得到; - 没有 ACK 的时候,问题多半在物理层、地址或者速率上------接线、上拉、供电、地址算错、每一拍太快,五个里面总有一个。程序逻辑还没轮到怀疑。
按现象查的几个坑
一个 ACK 都没有。 先量电压:SDA、SCL 在空闲时是不是高电平?不是高,说明没有上拉或者线被拉死;是高电平但扫不到任何地址,先检查 SDA/SCL 有没有接反、设备有没有供电。
扫描结果时有时无。 这是上升沿太慢的典型表现:上拉太大、线太长、挂的设备太多。把线缩短、把速率降下来(加大 SoftI2C_Delay() 的循环次数)再试。
换了一块芯片就全黑,代码一个字没改。 先加延时,把 SoftI2C_Delay() 的循环次数翻十倍到几十倍再试一次。软件 I2C 的速率跟着主频和编译优化走,芯片更快就是每一拍更短;太快会同时踩到从机的最大频率和总线的爬升时间这两条线。这也是移植时第一个该试的动作------它花不到一分钟,却能立刻把"速率问题"和"接线问题"分开。
总线挂死:某一次通信中途停了,之后所有设备都不应答。 原因通常是主机在从机"正在往外吐字节"的时候复位了------从机还在等时钟,就会一直把 SDA 拉低不放,总线永远是忙的。恢复办法是让它把话说完:手动产生 9 个时钟脉冲 (把 SCL 拉高拉低 9 次,期间让 SDA 松手),然后补一个停止条件。这也是很多驱动里带一段"总线恢复"代码的原因。
两个设备同地址。 表现是扫描时出现不该出现的应答,写数据时两台一起被改。查地址引脚,或者换掉其中一个设备的地址。
从机拉住时钟不放。 这不是故障,是时钟延展:它在说"等一下"。软件 I2C 天然容忍它;换成硬件外设时,也要确认它开着对应的容忍机制。
上拉接错电压。 上拉接在 5V 上,这条总线的"高电平"就按 5V 算,接 3.3V 设备之前要确认它能不能接受。反过来,上拉接 3.3V 而设备期望更高的高电平,它可能认不出高电平,也就扫不到。
最后橘猫说
回头看前面建立的几件事:
- 两根线,靠地址分设备。 主机产生时钟,从机只能被点名------这是它和串口最根本的区别;
- 开漏加上拉。 所有人只能拉低,写 1 是松手;上拉是这根线的"默认高",也是上升沿速度的来源;
SCL高时数据不许变。 唯一的例外是起始和停止;- 第 9 拍看应答。 它是"设备真的在"的唯一证据,也是排查时第一个该看的地方。
还有一件事留在这里:这套时序目前是 CPU 一位一位拨出来的,所以叫软件 I2C。那么,它一定要由 CPU 来拨吗? STM32 里有专门干这件事的外设:它自己产生时钟、自己发起始和停止、自己收发应答;代价是它把每一拍藏了起来,看得见的东西少了一半,速率和总线的脾气也换成另一套。这里先讲到软件这一份,那个外设留到以后再说。
先把软件这一份看熟。等你清楚每一拍该长什么样,再去看那个外设替掉了哪几行代码,才知道自己换来了什么、让出了什么。