第一次接触IIC总线时,我被它的简洁性惊艳到了——仅用两根线(SDA和SCL)就能实现主从设备之间的通信。这种优雅的设计在嵌入式系统中非常实用,特别是当我们需要连接多个外设时。记得当时我用STM32F103驱动OLED屏幕,硬件IIC总是出问题,最后改用软件模拟反而稳定了,这让我对软件模拟IIC产生了浓厚兴趣。
IIC总线就像教室里的师生问答:SCL时钟线相当于老师打拍子控制节奏,SDA数据线则是学生根据节奏传递答案。总线上的每个设备都有唯一地址,就像学生学号。当老师(主机)点名(发送地址)时,对应的学生(从机)才会应答。
几个关键特性需要特别注意:
STM32的硬件IIC外设理论上效率更高,但实际使用中常遇到坑:
c复制// 硬件IIC初始化代码示例(I2C1)
void I2C1_Init(void)
{
I2C_InitTypeDef I2C_InitStruct;
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStruct.I2C_OwnAddress1 = 0x00;
I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStruct.I2C_ClockSpeed = 400000; // 400kHz
I2C_Init(I2C1, &I2C_InitStruct);
I2C_Cmd(I2C1, ENABLE);
}
软件模拟的优势在于:
写模拟IIC驱动就像编排舞蹈动作,每个步骤的时机都要精确。以下是起始信号的典型实现:
c复制void IIC_Start(void)
{
SDA_OUT(); // 设置SDA为输出
IIC_SDA = 1; // 先拉高SDA
IIC_SCL = 1; // 再拉高SCL
delay_us(4); // 保持时间>4.7μs(标准模式)
IIC_SDA = 0; // SDA下降沿
delay_us(4);
IIC_SCL = 0; // 钳住总线,准备数据传输
}
常见时序问题排查技巧:
去年做智能家居项目时,需要保存设备配置参数,AT24C16成了我的首选。它的可靠性令人印象深刻——数据保存100年,写寿命100万次。有次意外断电,配置信息依然完好无损,这让我对EEPROM有了新的认识。
AT24Cxx的地址系统像邮政编码:
特殊的是AT24C16:
markdown复制| 型号 | 容量 | 页数 | 器件地址位 |
|---------|------|------|------------|
| AT24C01 | 128B | 16 | A2,A1,A0 |
| AT24C16 | 2KB | 128 | 无(全用作页地址) |
地址计算示例(访问第100页第3字节):
c复制Page = 100; // 0x64
WordAddr = (Page & 0x0F) << 4 | 0x03; // 低4位页地址+偏移
DeviceAddr = 0xA0 | ((Page >> 4) & 0x0E); // 高3位页地址
直接字节写入效率低,我发现页写能提升10倍速度。关键点:
优化后的页写函数:
c复制void AT24CXX_Write_Page(u16 addr, u8 *buf, u8 len)
{
u8 remain = AT24CXX_PAGE_SIZE - (addr % AT24CXX_PAGE_SIZE);
len = MIN(len, remain); // 不超过当前页剩余空间
IIC_Start();
IIC_Send_Byte(0xA0 | ((addr >> 7) & 0x0E));
IIC_Wait_Ack();
IIC_Send_Byte(addr & 0xFF);
IIC_Wait_Ack();
while(len--) {
IIC_Send_Byte(*buf++);
IIC_Wait_Ack();
}
IIC_Stop();
delay_ms(10); // 等待写入完成
}
在工业环境中,我总结了这些经验:
最近为无人机项目开发黑匣子功能,需要记录飞行数据。AT24C16的2KB空间显得捉襟见肘,于是我设计了环形缓冲区方案。
采用类似数据库的记录格式:
c复制#pragma pack(1)
typedef struct {
u32 timestamp; // 4字节
u16 altitude; // 2字节
u16 speed; // 2字节
u8 status; // 1字节
u8 checksum; // 1字节
} FlightRecord; // 共10字节
这样每页可存储1条完整记录+6字节,或2条精简记录。
为提高可移植性,我抽象出存储接口:
c复制typedef struct {
u16 (*read)(u16 addr, u8 *buf, u16 len);
u16 (*write)(u16 addr, const u8 *buf, u16 len);
u16 (*erase)(void);
} StorageDriver;
StorageDriver EEPROM_Driver = {
AT24CXX_Read,
AT24CXX_Write,
NULL // EEPROM无需擦除
};
对比测试结果(基于STM32F103@72MHz):
| 操作方式 | 写入1KB耗时 | 稳定性 |
|---|---|---|
| 单字节写入 | 1120ms | ★★★★☆ |
| 页写入(16B) | 82ms | ★★★☆☆ |
| 优化页写入 | 68ms | ★★★★★ |
调试IIC就像侦探破案,需要观察各种蛛丝马迹。记得有次设备间歇性失灵,最后发现是上拉电阻值不当导致。
问题1:从机无应答
问题2:数据校验错误
c复制// 添加简单的异或校验
u8 calc_checksum(u8 *data, u8 len)
{
u8 sum = 0;
while(len--) sum ^= *data++;
return sum;
}
当总线上挂载多个设备时:
电池供电设备需注意:
最后分享一个实用技巧:在GPIO初始化时,先设置为开漏输出并拉高,这样既符合IIC规范,又避免总线冲突。调试复杂问题时,不妨用LED指示灯实时显示总线状态,往往能快速定位问题节点。