# 总线之别:IIC与UART的本质分野及51单片机IIC协议全拆解
串行通信协议繁多,初学者最易混淆的便是IIC与UART。两者均使用两根信号线,却遵循截然不同的设计哲学——**UART是全双工、点对点的字符流传输;IIC是半双工、多设备的总线式寄存器访问**。理解这一本质差异,比死记硬背时序图更为重要。本文从IIC的物理层、帧结构、通信流程切入,并与UART对比辨析,最终在51单片机上完整实现软件模拟IIC主机驱动。
---
## 一、IIC vs UART:设计定位的根本差异
**UART**:异步串行通信,两根线各自独立(TX/RX)。**全双工**,收发可同时进行。**点对点**,不支持多机挂接(仅限标准TTL电平)。帧结构面向字符,无设备地址概念。
**IIC**:同步串行通信,两根线为时钟SCL与数据SDA。**半双工**,同一时刻只能单向传输。**多主多从**,总线可挂载上百个设备,通过**7位从机地址**寻址。帧结构面向寄存器,包含起始、地址、读写位、数据、应答、停止等多个阶段。
**选型直觉**:连接GPS模块、蓝牙模块用UART;挂载温湿度传感器、EEPROM、RTC用IIC。
---
## 二、IIC物理层:开漏输出与上拉电阻
IIC总线的电气特性是**开漏输出**。这意味着:
- 器件只能拉低总线(输出低电平),无法主动输出高电平;
- 高电平由**上拉电阻**提供;
- 多个设备可并联,任一设备拉低总线即总线为低——**线与**特性。
**上拉电阻取值**:1kΩ~10kΩ。100kHz标准模式常用4.7kΩ,400kHz快速模式可降至2.2kΩ。阻值过小增加功耗,过大则上升沿变缓限制速率。
51单片机I/O口无开漏模式,模拟IIC需将引脚配置为**准双向口**(标准51模式),输出低电平时直接清零,输出高电平时置1并由外部上拉电阻拉高。
```c
sbit SDA = P1^0;
sbit SCL = P1^1;
#define SDA_HIGH() SDA = 1
#define SDA_LOW() SDA = 0
#define SCL_HIGH() SCL = 1
#define SCL_LOW() SCL = 0
```
---
## 三、IIC帧结构:从起始到停止的完整序列
**起始信号**:SCL高电平期间,SDA发生下降沿。
```c
void IIC_Start(void) {
SDA_HIGH();
SCL_HIGH();
delay_us(5);
SDA_LOW(); // 下降沿
delay_us(5);
SCL_LOW(); // 拉低时钟,准备传输数据
}
```
**停止信号**:SCL高电平期间,SDA发生上升沿。
```c
void IIC_Stop(void) {
SDA_LOW();
SCL_HIGH();
delay_us(5);
SDA_HIGH(); // 上升沿
delay_us(5);
}
```
**字节发送**:SCL低电平时设置SDA,SCL高电平时从机采样。**高位先行**。
```c
void IIC_SendByte(unsigned char dat) {
unsigned char i;
for (i = 0; i < 8; i++) {
SCL_LOW();
if (dat & 0x80) SDA_HIGH();
else SDA_LOW();
dat <<= 1;
SCL_HIGH(); // 从机在此沿采样
delay_us(2);
}
SCL_LOW();
}
```
<"k8.a8k1.org.cn"><"p0.a8k1.org.cn"><"m4.a8k1.org.cn">
**应答检测**:第9个时钟周期,主机释放SDA,检测从机是否拉低SDA作为应答。
```c
unsigned char IIC_WaitAck(void) {
unsigned char ack;
SDA_HIGH(); // 释放总线
SCL_HIGH();
delay_us(2);
ack = SDA; // 0:应答, 1:非应答
SCL_LOW();
return ack;
}
```
**字节接收**:主机释放SDA,每时钟周期读取一位。
```c
unsigned char IIC_RecvByte(void) {
unsigned char i, val = 0;
SDA_HIGH(); // 释放总线
for (i = 0; i < 8; i++) {
val <<= 1;
SCL_HIGH();
if (SDA) val |= 0x01;
SCL_LOW();
}
return val;
}
```
**应答/非应答发送**:主机在第9时钟控制SDA。
```c
void IIC_SendAck(void) {
SDA_LOW(); // 应答
SCL_HIGH(); delay_us(2); SCL_LOW();
SDA_HIGH();
}
void IIC_SendNak(void) {
SDA_HIGH(); // 非应答
SCL_HIGH(); delay_us(2); SCL_LOW();
}
```
---
## 四、完整通信流程:以24C02 EEPROM为例
**写单字节**:起始 → 从机地址+写位 → 等待应答 → 发送寄存器地址 → 等待应答 → 发送数据 → 等待应答 → 停止。
```c
bit EEPROM_WriteByte(unsigned char addr, unsigned char dat) {
IIC_Start();
IIC_SendByte(0xA0); // 24C02器件地址+写
if (IIC_WaitAck()) return 1;
IIC_SendByte(addr); // 寄存器地址
if (IIC_WaitAck()) return 1;
IIC_SendByte(dat); // 写入数据
if (IIC_WaitAck()) return 1;
IIC_Stop();
return 0;
}
```
<"a6.a8k1.org.cn"><"d3.a8k1.org.cn"><"h7.a8k1.org.cn">
**读单字节**:起始 → 从机地址+写位 → 发送寄存器地址 → 重启 → 从机地址+读位 → 接收数据 → 主机非应答 → 停止。
```c
unsigned char EEPROM_ReadByte(unsigned char addr) {
unsigned char dat;
IIC_Start();
IIC_SendByte(0xA0); // 伪写,设置寄存器指针
IIC_WaitAck();
IIC_SendByte(addr);
IIC_WaitAck();
IIC_Start(); // 重启信号
IIC_SendByte(0xA1); // 读操作
IIC_WaitAck();
dat = IIC_RecvByte(); // 接收数据
IIC_SendNak(); // 非应答,结束读取
IIC_Stop();
return dat;
}
```
**重启信号**:不停止总线直接发起新起始,用于从写切换为读。
---
## 五、UART与IIC对比汇总
| 维度 | UART | IIC |
|--------------|-------------------------------|----------------------------------|
| 信号线 | TX, RX(独立) | SCL, SDA(复用) |
| 工作模式 | 全双工 | 半双工 |
| 拓扑结构 | 点对点 | 多主多从,总线式 |
| 时钟 | 异步,双方约定波特率 | 同步,主机产生SCL |
| 起始条件 | 下降沿(空闲高电平) | SCL高时SDA下降沿 |
| 停止条件 | 空闲高电平 | SCL高时SDA上升沿 |
| 设备寻址 | 无 | 7位从机地址 |
| 典型速率 | 9600~115200bps | 100kHz/400kHz/1MHz |
| 适用场景 | GPS/蓝牙/调试信息 | 传感器/存储器/RTC |
---
## 六、常见问题与调试要点
**总线死锁**:从机异常拉低SDA,主机无法产生起始/停止。**发送9个以上SCL时钟**迫使从机释放总线。
```c
void IIC_Recover(void) {
unsigned char i;
SDA_HIGH();
for (i = 0; i < 9; i++) {
SCL_HIGH(); delay_us(5);
SCL_LOW(); delay_us(5);
}
IIC_Stop(); // 尝试停止
}
```
**延时匹配**:IIC时序对延时敏感。标准模式100kHz需单周期≥5μs,快速模式400kHz需≥1.25μs。51单片机软件模拟需根据晶振调整`delay_us()`。
**地址左移**:7位从机地址如0x50在IIC总线上实际发送为`(0x50 << 1) | R/W`。初学者易直接发送0x50导致寻址失败。
**应答检测忽略**:部分示例代码不检测应答位,认为从机一定存在。**工程代码必须检测ACK**,超时或错误时应复位总线。
---
IIC与UART的混淆,源于对“总线”与“连接”概念的模糊。UART是**连接**,连接两端设备;IIC是**总线**,总线上所有设备按地址响应。理解这一层,便不会再用IIC去接蓝牙模块,也不会试图用UART挂载八个传感器。协议选对的背后,是对通信本质的洞察。