GD32F103硬件I2C0驱动24LC256 EEPROM实战指南
在嵌入式开发中,外部存储扩展是常见需求。24LC256作为一款32KB容量的I2C接口EEPROM,因其非易失性、低功耗和简单接口而广受欢迎。本文将手把手教你使用GD32F103的硬件I2C0模块与24LC256实现可靠通信。
1. 硬件准备与引脚配置
GD32F103系列MCU提供了两个I2C接口:I2C0和I2C1。我们选择I2C0并重映射到PB8(SCL)和PB9(SDA)引脚,这种配置能有效避免与常用调试接口冲突。
关键配置步骤:
void EEPROM_PIN_Init(void) { rcu_periph_clock_enable(RCU_AF); // 使能AFIO时钟 rcu_periph_clock_enable(RCU_GPIOB); // 使能GPIOB时钟 // 重映射I2C0到PB8/PB9 gpio_pin_remap_config(GPIO_I2C0_REMAP, ENABLE); gpio_init(GPIOB, GPIO_MODE_AF_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_8|GPIO_PIN_9); rcu_periph_clock_enable(RCU_I2C0); // 使能I2C0时钟 i2c_clock_config(I2C0, 400000, I2C_DTCY_2); // 快速模式400kHz i2c_mode_addr_config(I2C0, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, 0xA0); i2c_enable(I2C0); // 使能I2C外设 i2c_ack_config(I2C0, I2C_ACK_ENABLE); // 启用ACK应答 }注意:开漏输出模式(GPIO_MODE_AF_OD)是I2C通信的必要配置,外部需要接上拉电阻(通常4.7kΩ)。
2. I2C通信基础与24LC256特性
24LC256采用标准的I2C协议,7位设备地址为0x50(二进制1010000),但实际发送时需要左移一位,最低位表示读写操作:
- 写操作:0xA0 (10100000)
- 读操作:0xA1 (10100001)
24LC256关键参数:
| 参数 | 值 | 说明 |
|---|---|---|
| 容量 | 32KB | 地址范围0x0000-0x7FFF |
| 页大小 | 64字节 | 单次写入不能跨页 |
| 写周期时间 | 5ms(典型) | 需要轮询等待完成 |
| 耐久性 | 100万次 | 每个存储单元的擦写次数 |
3. 核心功能实现
3.1 单字节写入
单字节写入是最基本的操作,但需要注意写周期时间:
void EEPROM_U8_Data_Write(uint8_t data, uint16_t addr) { // 等待I2C总线空闲 while(i2c_flag_get(I2C0, I2C_FLAG_I2CBSY)); i2c_start_on_bus(I2C0); // 发送起始条件 while(!i2c_flag_get(I2C0, I2C_FLAG_SBSEND)); // 发送设备地址(写) i2c_master_addressing(I2C0, 0xA0, I2C_TRANSMITTER); while(!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND)); i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND); // 发送16位存储地址(高位在前) i2c_data_transmit(I2C0, (addr >> 8) & 0xFF); while(!i2c_flag_get(I2C0, I2C_FLAG_BTC)); i2c_data_transmit(I2C0, addr & 0xFF); while(!i2c_flag_get(I2C0, I2C_FLAG_BTC)); // 发送数据 i2c_data_transmit(I2C0, data); while(!i2c_flag_get(I2C0, I2C_FLAG_BTC)); i2c_stop_on_bus(I2C0); // 发送停止条件 while(I2C_CTL0(I2C0) & 0x0200); eeprom_wait_standby_state(); // 等待写入完成 }3.2 页写入操作
24LC256支持页写入(最多64字节),大幅提高写入效率:
void eeprom_page_write(uint8_t* buffer, uint16_t addr, uint8_t count) { // 检查地址是否页对齐 uint8_t page_offset = addr % 64; if(page_offset + count > 64) { count = 64 - page_offset; // 限制不跨页 } // 标准I2C起始序列 while(i2c_flag_get(I2C0, I2C_FLAG_I2CBSY)); i2c_start_on_bus(I2C0); while(!i2c_flag_get(I2C0, I2C_FLAG_SBSEND)); // 发送设备地址和存储地址 i2c_master_addressing(I2C0, 0xA0, I2C_TRANSMITTER); while(!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND)); i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND); i2c_data_transmit(I2C0, (addr >> 8) & 0xFF); while(!i2c_flag_get(I2C0, I2C_FLAG_BTC)); i2c_data_transmit(I2C0, addr & 0xFF); while(!i2c_flag_get(I2C0, I2C_FLAG_BTC)); // 发送数据 for(uint8_t i = 0; i < count; i++) { i2c_data_transmit(I2C0, buffer[i]); while(!i2c_flag_get(I2C0, I2C_FLAG_BTC)); } i2c_stop_on_bus(I2C0); while(I2C_CTL0(I2C0) & 0x0200); }3.3 连续读取实现
读取操作比写入简单,不需要等待周期:
uint8_t EEPROM_U8_Data_Read(uint16_t addr) { while(i2c_flag_get(I2C0, I2C_FLAG_I2CBSY)); // 第一阶段:发送写地址 i2c_start_on_bus(I2C0); while(!i2c_flag_get(I2C0, I2C_FLAG_SBSEND)); i2c_master_addressing(I2C0, 0xA0, I2C_TRANSMITTER); while(!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND)); i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND); // 发送存储地址 i2c_data_transmit(I2C0, (addr >> 8) & 0xFF); while(!i2c_flag_get(I2C0, I2C_FLAG_BTC)); i2c_data_transmit(I2C0, addr & 0xFF); while(!i2c_flag_get(I2C0, I2C_FLAG_BTC)); // 第二阶段:重启并读取 i2c_start_on_bus(I2C0); while(!i2c_flag_get(I2C0, I2C_FLAG_SBSEND)); i2c_master_addressing(I2C0, 0xA1, I2C_RECEIVER); i2c_ack_config(I2C0, I2C_ACK_DISABLE); // 最后一个字节不发送ACK while(!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND)); i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND); i2c_stop_on_bus(I2C0); // 提前发送停止条件 // 读取数据 while(!i2c_flag_get(I2C0, I2C_FLAG_RBNE)); uint8_t data = i2c_data_receive(I2C0); while(I2C_CTL0(I2C0) & 0x0200); // 等待停止完成 i2c_ack_config(I2C0, I2C_ACK_ENABLE); // 恢复ACK return data; }4. 高级应用与优化技巧
4.1 大数据块传输策略
当需要写入超过64字节的数据时,需要分页处理:
void EEPROM_Write_Buffer(uint8_t* buffer, uint16_t size, uint16_t addr) { uint8_t bytes_remaining = size; uint8_t bytes_to_write; while(bytes_remaining > 0) { // 计算当前页剩余空间 uint8_t page_offset = addr % 64; bytes_to_write = 64 - page_offset; if(bytes_to_write > bytes_remaining) { bytes_to_write = bytes_remaining; } eeprom_page_write(buffer, addr, bytes_to_write); eeprom_wait_standby_state(); buffer += bytes_to_write; addr += bytes_to_write; bytes_remaining -= bytes_to_write; } }4.2 写周期等待优化
24LC256内部写操作需要时间,轮询等待的低效实现:
void eeprom_wait_standby_state(void) { uint32_t timeout = 500; // 5ms超时(假设系统时钟1ms) while(timeout--) { i2c_start_on_bus(I2C0); if(i2c_flag_get(I2C0, I2C_FLAG_ADDSEND)) { i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND); i2c_stop_on_bus(I2C0); return; } i2c_stop_on_bus(I2C0); delay_1ms(1); // 简单延时 } }更高效的实现可以利用I2C的ACK polling特性:
void eeprom_wait_standby_enhanced(void) { while(1) { i2c_start_on_bus(I2C0); i2c_master_addressing(I2C0, 0xA0, I2C_TRANSMITTER); uint32_t status = I2C_STAT0(I2C0); while(0 == (status & (I2C_STAT0_ADDSEND | I2C_STAT0_AERR))); if(status & I2C_STAT0_ADDSEND) { i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND); i2c_stop_on_bus(I2C0); return; } i2c_flag_clear(I2C0, I2C_FLAG_AERR); i2c_stop_on_bus(I2C0); } }4.3 错误处理与鲁棒性增强
实际项目中需要完善的错误处理机制:
typedef enum { EEPROM_OK = 0, EEPROM_TIMEOUT, EEPROM_NACK, EEPROM_BUS_ERROR } EEPROM_Status; EEPROM_Status EEPROM_Write_With_Retry(uint8_t data, uint16_t addr, uint8_t retries) { while(retries--) { EEPROM_Status status = EEPROM_U8_Data_Write_Ex(data, addr); if(status == EEPROM_OK) { return EEPROM_OK; } delay_1ms(10); } return EEPROM_TIMEOUT; } EEPROM_Status EEPROM_U8_Data_Write_Ex(uint8_t data, uint16_t addr) { uint32_t timeout = 1000; // 1ms超时 // 等待总线空闲 while(i2c_flag_get(I2C0, I2C_FLAG_I2CBSY)) { if(--timeout == 0) return EEPROM_TIMEOUT; } // ...其余写入流程类似,增加各步骤超时检测 return EEPROM_OK; }5. 实际应用示例
5.1 系统配置存储
typedef struct { uint32_t magic_number; uint16_t version; uint8_t device_id[8]; uint16_t calibration_data; uint32_t operation_hours; } SystemConfig; void Save_Config(SystemConfig* config) { uint8_t* p = (uint8_t*)config; EEPROM_Write_Buffer(p, sizeof(SystemConfig), CONFIG_START_ADDR); } bool Load_Config(SystemConfig* config) { uint8_t* p = (uint8_t*)config; for(uint16_t i = 0; i < sizeof(SystemConfig); i++) { p[i] = EEPROM_U8_Data_Read(CONFIG_START_ADDR + i); } return (config->magic_number == CONFIG_MAGIC_NUMBER); }5.2 数据日志存储
循环缓冲区实现日志存储:
#define LOG_START_ADDR 0x1000 #define LOG_END_ADDR 0x7FFF #define LOG_ENTRY_SIZE 32 uint16_t current_log_addr = LOG_START_ADDR; void Write_Log_Entry(uint8_t* data) { if(current_log_addr + LOG_ENTRY_SIZE > LOG_END_ADDR) { current_log_addr = LOG_START_ADDR; // 循环 } EEPROM_Write_Buffer(data, LOG_ENTRY_SIZE, current_log_addr); current_log_addr += LOG_ENTRY_SIZE; // 存储当前指针位置 EEPROM_U8_Data_Write(current_log_addr >> 8, LOG_POINTER_ADDR); EEPROM_U8_Data_Write(current_log_addr & 0xFF, LOG_POINTER_ADDR + 1); }6. 性能优化与注意事项
时钟速度选择:
- 标准模式(100kHz):兼容性最好
- 快速模式(400kHz):性能最佳,但需注意信号完整性
上拉电阻选择:
- 4.7kΩ:标准选择
- 2.2kΩ:长距离或高容性负载时使用
- 10kΩ:低功耗应用
PCB布局建议:
- SDA/SCL走线尽量短且等长
- 避免与高频信号线平行走线
- 在MCU引脚附近放置上拉电阻
软件优化技巧:
- 批量写入时尽量填满整个页(64字节)
- 读取操作不需要延时,可以全速进行
- 关键数据写入后立即读取验证
// 写入验证示例 bool EEPROM_Verify_Write(uint8_t data, uint16_t addr) { EEPROM_U8_Data_Write(data, addr); return (EEPROM_U8_Data_Read(addr) == data); }在实际项目中,GD32F103的硬件I2C配合24LC256能够构建可靠的配置存储系统。通过合理设计写入策略和错误处理机制,可以确保数据安全性和系统稳定性。