1. IIC总线基础与软件模拟原理
IIC(Inter-Integrated Circuit)总线是飞利浦半导体(现恩智浦)在1980年代推出的两线式串行通信协议。在嵌入式开发中,当硬件IIC控制器不可用或需要更灵活的时序控制时,软件模拟(bit-banging)成为常用解决方案。
1.1 IIC协议核心特性
- 双线制设计:仅需SCL(时钟线)和SDA(数据线)两根信号线
- 多主多从架构:支持多个主设备与从设备共享总线
- 地址寻址:7位或10位从机地址寻址方式
- 速率分级:
- 标准模式:100kbps
- 快速模式:400kbps
- 高速模式:3.4Mbps
注意:软件模拟时实际速率受GPIO翻转速度和延时函数精度限制,通常难以达到硬件控制器的高速性能。
1.2 软件模拟关键点
通过GPIO模拟IIC需要精确控制:
- 引脚方向切换(开漏输出/输入模式)
- 时序间隔(起始条件、停止条件、数据建立/保持时间)
- 时钟拉伸(Clock Stretching)处理
- 错误检测与重试机制
2. 软件IIC驱动实现详解
2.1 硬件接口配置
以STM32为例,典型GPIO初始化代码:
c复制void IIC_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 配置SCL和SDA为开漏输出
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7; // 假设使用PB6,PB7
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);
// 初始状态置高
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_SET);
}
2.2 基础时序函数实现
起始条件(START):
c复制void IIC_Start(void) {
SDA_HIGH();
SCL_HIGH();
Delay_us(4); // 满足t_HD;STA时间
SDA_LOW();
Delay_us(4);
SCL_LOW(); // 钳住总线
}
停止条件(STOP):
c复制void IIC_Stop(void) {
SDA_LOW();
SCL_LOW();
Delay_us(4);
SCL_HIGH();
Delay_us(4);
SDA_HIGH();
Delay_us(4);
}
2.3 字节传输流程
完整的数据发送函数示例:
c复制uint8_t IIC_WriteByte(uint8_t data) {
uint8_t i, ack;
for(i=0; i<8; i++) {
(data & 0x80) ? SDA_HIGH() : SDA_LOW();
data <<= 1;
Delay_us(2);
SCL_HIGH();
Delay_us(4);
SCL_LOW();
Delay_us(2);
}
// 读取ACK
SDA_HIGH(); // 释放SDA
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 临时切换输入模式
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
Delay_us(2);
SCL_HIGH();
Delay_us(4);
ack = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7);
SCL_LOW();
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 恢复输出模式
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
return ack;
}
3. 典型问题排查与优化
3.1 常见故障现象分析表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 无ACK响应 | 从机地址错误 从机未上电 上拉电阻过大 |
检查地址(含R/W位) 测量电源电压 减小上拉电阻(通常4.7K) |
| 数据错位 | 时序间隔不足 信号毛刺 |
增加延时时间 添加小电容滤波 |
| 随机错误 | 总线冲突 电源干扰 |
添加重试机制 改善电源去耦 |
3.2 时序优化技巧
- 动态延时调整:
c复制void IIC_Delay(uint8_t mode) {
static const uint8_t delay_table[] = {4,2,1}; // 标准/快速/高速模式
Delay_us(delay_table[current_speed_mode]);
}
- 时钟同步检测:
c复制while(SCL_READ() == 0) { // 处理时钟拉伸
if(--timeout == 0) return IIC_TIMEOUT;
Delay_us(1);
}
- 错误恢复流程:
c复制void IIC_Recover(void) {
SDA_HIGH();
SCL_HIGH();
for(int i=0; i<9; i++) { // 发送9个时钟脉冲
SCL_LOW();
Delay_us(5);
SCL_HIGH();
Delay_us(5);
}
IIC_Stop();
}
4. 实战案例:AT24C02 EEPROM驱动
4.1 器件特性配置
c复制#define AT24C02_ADDR 0xA0 // 1010+A2A1A0+R/W
#define PAGE_SIZE 8 // 页写入限制
#define WRITE_DELAY 5 // 写入周期ms
4.2 随机地址读取实现
c复制uint8_t AT24C02_Read(uint8_t addr) {
uint8_t data;
IIC_Start();
IIC_WriteByte(AT24C02_ADDR | 0); // 写模式
IIC_WriteByte(addr);
IIC_Start(); // 重复起始条件
IIC_WriteByte(AT24C02_ADDR | 1); // 读模式
data = IIC_ReadByte(0); // 发送NACK结束
IIC_Stop();
return data;
}
4.3 页写入注意事项
c复制void AT24C02_PageWrite(uint8_t addr, uint8_t *buf) {
// 检查页边界
uint8_t remain = PAGE_SIZE - (addr % PAGE_SIZE);
uint8_t len = (length > remain) ? remain : length;
IIC_Start();
IIC_WriteByte(AT24C02_ADDR | 0);
IIC_WriteByte(addr);
for(int i=0; i<len; i++) {
IIC_WriteByte(buf[i]);
}
IIC_Stop();
Delay_ms(WRITE_DELAY); // 必须等待写入完成
}
关键细节:EEPROM页写入时,若跨页边界会自动回卷到页首覆盖数据,必须手动分次写入。
5. 进阶优化方向
5.1 DMA辅助传输
对于高速应用,可结合定时器和DMA实现准硬件级模拟:
- 配置定时器产生精确时钟脉冲
- 使用DMA自动搬运GPIO输出数据
- 通过中断处理状态切换
5.2 多线程安全设计
在RTOS环境中需添加:
c复制void IIC_Lock(void) {
while(__sync_lock_test_and_set(&iic_lock, 1)) {
osDelay(1);
}
}
void IIC_Unlock(void) {
__sync_lock_release(&iic_lock);
}
5.3 性能评估方法
-
示波器测量实际波形参数:
- 上升/下降时间
- 时钟占空比
- 建立/保持时间余量
-
压力测试指标:
c复制uint32_t error_count = 0;
for(int i=0; i<10000; i++) {
uint8_t wdata = rand()%256;
AT24C02_Write(0, wdata);
uint8_t rdata = AT24C02_Read(0);
if(wdata != rdata) error_count++;
}
printf("Error rate: %.2f%%\n", error_count/100.0);
通过示波器抓取的典型IIC波形显示,软件模拟在400kHz速率下时钟抖动约±5%,建议在关键应用中对时序敏感的从器件预留10%的时序余量。实际项目中,我通常会先用逻辑分析仪捕获完整通信过程,对照器件手册逐项检查时序参数,这种"示波器调试法"能快速定位90%以上的接口问题。