告别硬件IIC:STM32F103用软件模拟IIC读写AT24C02/04/16全攻略(含地址计算详解)
STM32软件模拟IIC驱动AT24C系列EEPROM实战指南
1. 为什么选择软件模拟IIC?
在嵌入式开发中,IIC总线因其简单的两线制(SDA和SCL)和灵活的多设备连接特性,成为连接各类传感器的首选方案。然而,STM32的硬件IIC模块在实际应用中常会遇到各种问题:
- 硬件IIC的局限性:某些STM32型号(如F1系列)的硬件IIC存在稳定性问题,特别是在高时钟频率下容易出现通信失败
- 引脚冲突:硬件IIC引脚固定,当这些引脚被其他功能占用时无法灵活调整
- 库函数复杂性:HAL库的硬件IIC接口相对复杂,调试困难
相比之下,软件模拟IIC具有以下优势:
- 引脚可任意配置:可以使用任何GPIO引脚作为SDA和SCL线
- 调试方便:可以灵活添加调试信息,逐步跟踪通信过程
- 兼容性强:同一套代码可以适配不同型号的STM32芯片
// 软件IIC引脚配置示例 #define IIC_SCL_PIN GPIO_PIN_6 #define IIC_SCL_PORT GPIOB #define IIC_SDA_PIN GPIO_PIN_7 #define IIC_SDA_PORT GPIOB void IIC_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; // 使能GPIO时钟 __HAL_RCC_GPIOB_CLK_ENABLE(); // 配置SCL和SDA为开漏输出模式 GPIO_InitStruct.Pin = IIC_SCL_PIN | IIC_SDA_PIN; 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(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_SET); }2. AT24C系列EEPROM关键特性解析
AT24C系列是Microchip公司生产的串行EEPROM存储器,具有以下共同特点:
- 工作电压宽:1.8V至5.5V
- 存储容量:从1Kbit(AT24C01)到512Kbit(AT24C512)多种选择
- 接口:标准IIC接口,支持400kHz高速模式
- 耐久性:可擦写100万次,数据保存100年
不同型号的主要区别在于存储容量和地址空间分配:
| 型号 | 容量(Kbit) | 字节容量 | 页大小 | 设备地址位 | 地址字节数 |
|---|---|---|---|---|---|
| AT24C01 | 1 | 128 | 8 | A2,A1,A0 | 1 |
| AT24C02 | 2 | 256 | 8 | A2,A1,A0 | 1 |
| AT24C04 | 4 | 512 | 16 | A2,A1 | 1 |
| AT24C08 | 8 | 1024 | 16 | A2 | 1 |
| AT24C16 | 16 | 2048 | 16 | 无 | 1 |
| AT24C32 | 32 | 4096 | 32 | A2,A1,A0 | 2 |
| AT24C64 | 64 | 8192 | 32 | A2,A1,A0 | 2 |
注意:AT24C01-AT24C16使用单字节地址,而AT24C32及以上型号需要使用双字节地址
3. 设备地址计算与页写入策略
3.1 设备地址计算
AT24C系列设备的IIC地址由固定部分和可配置部分组成:
- 固定部分:高4位固定为1010
- 可配置部分:低3位由芯片型号和硬件连接决定
对于不同容量的芯片,设备地址计算方式不同:
AT24C01/02:A2,A1,A0引脚状态直接决定设备地址低3位
- 设备地址格式:1010 A2 A1 A0 R/W
- 同一IIC总线上最多可挂8个设备
AT24C04:仅使用A2,A1引脚,A0悬空
- 设备地址格式:1010 A2 A1 P0 R/W
- P0位用于页选择(高地址位)
- 同一IIC总线上最多可挂4个设备
AT24C16:不使用A2,A1,A0引脚
- 设备地址格式:1010 P2 P1 P0 R/W
- P2,P1,P0用于页选择
- 同一IIC总线上只能挂1个设备
// AT24C16设备地址计算函数 uint8_t AT24C16_GetDeviceAddress(uint16_t memAddr) { uint8_t page = memAddr / 256; // 每页256字节 return 0xA0 | ((page << 1) & 0x0E); // 1010 + P2P1P0 + 0(写) }3.2 页写入策略
AT24C系列支持页写入模式,可以一次性写入一页数据,显著提高写入效率:
- 页大小:不同型号页大小不同(AT24C01/02为8字节,AT24C04及以上为16字节或更大)
- 跨页处理:当写入数据跨越页边界时,需要分多次写入
void AT24C_WritePage(uint8_t devAddr, uint16_t memAddr, uint8_t *data, uint8_t len) { uint8_t pageSize = 16; // AT24C16页大小 uint8_t offset = memAddr % pageSize; uint8_t remain = pageSize - offset; if(len <= remain) { // 单次写入不跨页 IIC_Start(); IIC_SendByte(devAddr); IIC_SendByte(memAddr & 0xFF); for(uint8_t i=0; i<len; i++) { IIC_SendByte(data[i]); } IIC_Stop(); delay_ms(5); // 写入周期等待 } else { // 分两次写入跨页数据 AT24C_WritePage(devAddr, memAddr, data, remain); AT24C_WritePage(devAddr, memAddr+remain, data+remain, len-remain); } }4. 完整驱动实现与优化技巧
4.1 基础驱动函数实现
完整的软件IIC驱动需要实现以下基本函数:
- 起始信号:SCL高电平时,SDA从高到低的跳变
- 停止信号:SCL高电平时,SDA从低到高的跳变
- 发送字节:SCL低电平时改变SDA,SCL高电平时保持稳定
- 接收字节:SCL高电平时读取SDA状态
- 等待应答:发送完字节后检测从机应答
// 产生IIC起始信号 void IIC_Start(void) { SDA_HIGH(); SCL_HIGH(); delay_us(4); SDA_LOW(); delay_us(4); SCL_LOW(); // 钳住总线,准备发送数据 } // 产生IIC停止信号 void IIC_Stop(void) { SDA_LOW(); SCL_LOW(); delay_us(4); SCL_HIGH(); SDA_HIGH(); // 发送结束信号 delay_us(4); } // 等待应答信号 uint8_t IIC_Wait_Ack(void) { uint8_t timeout = 0; SDA_INPUT(); // SDA设置为输入 SDA_HIGH(); delay_us(1); SCL_HIGH(); delay_us(1); while(SDA_READ()) { timeout++; if(timeout > 250) { IIC_Stop(); return 1; // 应答超时 } } SCL_LOW(); return 0; // 正常应答 }4.2 读写函数优化
针对AT24C系列EEPROM的特点,可以进行以下优化:
- 批量读写优化:减少起始/停止信号的次数
- 写入延迟处理:AT24Cxx内部写入需要时间(典型值5ms)
- 错误重试机制:增加通信失败时的自动重试
// 带错误重试的读取函数 uint8_t AT24C_ReadWithRetry(uint16_t addr, uint8_t *buf, uint16_t len, uint8_t retry) { while(retry--) { if(AT24C_Read(addr, buf, len) == 0) { return 0; // 成功 } delay_ms(1); } return 1; // 失败 } // 带写入延迟的页写入函数 void AT24C_WriteWithDelay(uint8_t devAddr, uint16_t memAddr, uint8_t *data, uint8_t len) { uint8_t retry = 3; while(retry--) { AT24C_WritePage(devAddr, memAddr, data, len); delay_ms(5); // 等待内部写入完成 // 验证写入是否正确 uint8_t verify[16]; AT24C_Read(devAddr, memAddr, verify, len); if(memcmp(data, verify, len) == 0) { break; // 验证成功 } } }4.3 驱动使用示例
下面是一个完整的使用示例,演示如何初始化、写入和读取数据:
#include "at24cxx.h" #include "stdio.h" #define TEST_ADDR 0x100 #define TEST_SIZE 16 int main(void) { // 初始化硬件 HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C_Init(); // 初始化AT24C16 AT24C_Init(); // 测试数据 uint8_t writeData[TEST_SIZE] = {0}; uint8_t readData[TEST_SIZE] = {0}; // 填充测试数据 for(uint8_t i=0; i<TEST_SIZE; i++) { writeData[i] = i; } // 写入数据 if(AT24C_Write(TEST_ADDR, writeData, TEST_SIZE)) { printf("Write failed!\r\n"); while(1); } // 读取数据 if(AT24C_Read(TEST_ADDR, readData, TEST_SIZE)) { printf("Read failed!\r\n"); while(1); } // 验证数据 if(memcmp(writeData, readData, TEST_SIZE) == 0) { printf("Test passed!\r\n"); } else { printf("Test failed!\r\n"); } while(1); }5. 常见问题与调试技巧
在实际开发中,可能会遇到以下常见问题:
通信失败:
- 检查上拉电阻(通常4.7kΩ)
- 确认SCL/SDA引脚配置正确
- 降低通信速度测试
写入后读取数据不正确:
- 确保写入后留有足够延迟(>5ms)
- 实现写入验证机制
- 检查设备地址计算是否正确
跨页写入数据丢失:
- 正确实现页边界检查
- 分多次写入跨页数据
调试建议:
- 使用逻辑分析仪捕获IIC波形
- 在关键位置添加调试打印
- 实现逐步调试的测试函数
// 调试用函数:打印IIC总线状态 void IIC_DebugBusState(void) { printf("SCL: %d, SDA: %d\r\n", HAL_GPIO_ReadPin(IIC_SCL_PORT, IIC_SCL_PIN), HAL_GPIO_ReadPin(IIC_SDA_PORT, IIC_SDA_PIN)); } // 逐步调试的测试函数 void IIC_StepTest(void) { printf("Testing IIC start condition...\r\n"); IIC_Start(); IIC_DebugBusState(); delay_ms(100); printf("Testing IIC stop condition...\r\n"); IIC_Stop(); IIC_DebugBusState(); delay_ms(100); printf("Testing byte transmission...\r\n"); IIC_Start(); IIC_SendByte(0xA0); uint8_t ack = IIC_Wait_Ack(); printf("ACK received: %d\r\n", ack); IIC_Stop(); }6. 性能优化与高级应用
6.1 提高读写速度
- 提高时钟频率:在保证可靠性的前提下提高SCL频率
- 减少延迟:优化微秒级延迟函数
- 批量操作:使用页写入/读取减少通信开销
// 优化后的微秒延迟函数 static inline void delay_us(uint32_t us) { uint32_t ticks = us * (SystemCoreClock / 1000000) / 5; while(ticks--) { __NOP(); } } // 高速页读取函数 void AT24C_FastRead(uint16_t addr, uint8_t *buf, uint16_t len) { uint8_t devAddr = AT24C16_GetDeviceAddress(addr); IIC_Start(); IIC_SendByte(devAddr); IIC_SendByte(addr & 0xFF); IIC_Start(); // 重复起始条件 IIC_SendByte(devAddr | 0x01); // 读模式 for(uint16_t i=0; i<len; i++) { buf[i] = IIC_ReadByte(i == len-1); // 最后一个字节发送NACK } IIC_Stop(); }6.2 数据存储结构设计
对于需要存储结构化数据的应用,可以设计更高效的数据组织方式:
- 数据分块:按功能或类型将数据存储在不同地址区域
- 数据版本控制:存储数据时包含版本信息
- 冗余存储:重要数据多份存储,提高可靠性
// 数据头结构 typedef struct { uint16_t magic; // 魔数标识 uint16_t version; // 数据版本 uint32_t crc; // CRC校验 uint32_t length; // 数据长度 } DataHeader; // 带校验的数据存储函数 uint8_t AT24C_WriteWithHeader(uint16_t addr, void *data, uint16_t len) { DataHeader header = { .magic = 0x55AA, .version = 1, .crc = Calculate_CRC(data, len), .length = len }; // 写入头和数据 if(AT24C_Write(addr, (uint8_t*)&header, sizeof(header))) return 1; if(AT24C_Write(addr+sizeof(header), (uint8_t*)data, len)) return 1; return 0; } // 带校验的数据读取函数 uint8_t AT24C_ReadWithHeader(uint16_t addr, void *data, uint16_t maxLen) { DataHeader header; // 读取头 if(AT24C_Read(addr, (uint8_t*)&header, sizeof(header))) return 1; // 验证头 if(header.magic != 0x55AA || header.length > maxLen) return 1; // 读取数据 if(AT24C_Read(addr+sizeof(header), (uint8_t*)data, header.length)) return 1; // 验证CRC if(Calculate_CRC(data, header.length) != header.crc) return 1; return 0; }6.3 多设备管理
当系统中需要连接多个IIC设备时,可以设计统一的管理接口:
typedef struct { uint8_t devAddr; // 设备地址 uint16_t pageSize; // 页大小 uint32_t capacity; // 总容量 uint8_t addrBytes; // 地址字节数 } IIC_Device; IIC_Device devices[] = { {0xA0, 16, 2048, 1}, // AT24C16 {0x68, 1, 32, 1} // DS3231 RTC }; uint8_t IIC_DeviceRead(uint8_t devIndex, uint32_t addr, uint8_t *buf, uint16_t len) { IIC_Device *dev = &devices[devIndex]; IIC_Start(); IIC_SendByte(dev->devAddr); if(dev->addrBytes == 2) { IIC_SendByte(addr >> 8); // 高地址字节 } IIC_SendByte(addr & 0xFF); // 低地址字节 IIC_Start(); // 重复起始条件 IIC_SendByte(dev->devAddr | 0x01); // 读模式 for(uint16_t i=0; i<len; i++) { buf[i] = IIC_ReadByte(i == len-1); // 最后一个字节发送NACK } IIC_Stop(); return 0; }