ATtiny402基于MPLAB X IDE的I2C EEPROM读写代码问题求助
ATtiny402 24C32 I2C读写问题排查与解决方案
1. 现有代码正确性验证
现有代码存在多处违反I2C时序规范的问题,是读取失败的核心原因:
- 起始/停止时序错误:标准I2C起始条件是SCL保持高电平时,SDA从高电平拉低;停止条件是SCL保持高电平时,SDA从低电平拉高。当前
i2c_start()和i2c_stop()的电平变化顺序完全不符合规范。 - ACK等待逻辑错误:
i2c_data_write()中等待从机ACK时,SCL的状态错误,且未正确处理ACK超时(若从机无响应会无限死循环)。 - 读取时序错误:
i2c_data_read()中SCL拉高后采样SDA的时机过早,未留足信号稳定时间;且读取完成后在函数内部调用i2c_stop(),与主函数的流程冲突。 - 硬编码引脚混乱:代码中混用
PIN1/PIN2和宏定义SDA_PIN/SCL_PIN,易引发引脚操作错误。 - 读取结果未验证:主函数中读取到的
data仅做自赋值,无输出或存储逻辑,无法确认读取结果。
2. 代码修正方案
以下是修正后的完整代码,注释标注关键修改点:
#include <xc.h> #include <util/delay.h> #define SDA_PIN 1 #define SCL_PIN 2 #define EEPROM_ADDR_WRITE 0xA0 // 24C32写地址(A2/A1/A0接地) #define EEPROM_ADDR_READ 0xA1 // 24C32读地址 // 函数声明 void i2c_start(void); void i2c_stop(void); uint8_t i2c_data_write(uint8_t data); uint8_t i2c_data_read(uint8_t send_ack); void i2c_addr_write(uint16_t addr); int main(void) { main_init(); while(1) { main_loop(); } return 0; } void main_init(void) { // 配置引脚为输出,开启内部上拉(I2C开漏输出需要上拉) PORTA.DIRSET = (1 << SDA_PIN) | (1 << SCL_PIN); PORTA.PIN1CTRL |= PORT_PULLUPEN_bm; PORTA.PIN2CTRL |= PORT_PULLUPEN_bm; // 写入测试数据到地址0x0001 i2c_start(); i2c_data_write(EEPROM_ADDR_WRITE); i2c_addr_write(0x0001); i2c_data_write('C'); i2c_stop(); _delay_ms(10); // 等待EEPROM写周期完成(最大10ms) // 读取地址0x0001的数据 uint8_t read_data = 0; i2c_start(); i2c_data_write(EEPROM_ADDR_WRITE); i2c_addr_write(0x0001); i2c_start(); // 重复起始条件 i2c_data_write(EEPROM_ADDR_READ); read_data = i2c_data_read(0); // 读取最后一个字节,发送NACK i2c_stop(); // 这里可添加UART输出或LED指示读取结果,方便验证 while(1) { // 示例:若read_data为'C',点亮LED(需自行配置LED引脚) // PORTA.OUTSET = (1 << 3); } } void main_loop(void) { } // 标准I2C起始条件:SCL高时SDA拉低 void i2c_start(void) { PORTA.OUT |= (1 << SDA_PIN); PORTA.OUT |= (1 << SCL_PIN); _delay_us(4); PORTA.OUT &= ~(1 << SDA_PIN); _delay_us(4); PORTA.OUT &= ~(1 << SCL_PIN); } // 标准I2C停止条件:SCL高时SDA拉高 void i2c_stop(void) { PORTA.OUT &= ~(1 << SDA_PIN); PORTA.OUT |= (1 << SCL_PIN); _delay_us(4); PORTA.OUT |= (1 << SDA_PIN); _delay_us(4); } // 写入8位数据,返回0表示ACK,1表示NACK uint8_t i2c_data_write(uint8_t data) { uint8_t ack = 1; // 发送8位数据(高位先) for(uint8_t i=0; i<8; i++) { if(data & 0x80) { PORTA.OUT |= (1 << SDA_PIN); } else { PORTA.OUT &= ~(1 << SDA_PIN); } _delay_us(2); PORTA.OUT |= (1 << SCL_PIN); _delay_us(2); PORTA.OUT &= ~(1 << SCL_PIN); data <<= 1; } // 等待从机ACK PORTA.DIRCLR = (1 << SDA_PIN); // 释放SDA _delay_us(2); PORTA.OUT |= (1 << SCL_PIN); _delay_us(2); if(!(PORTA.IN & (1 << SDA_PIN))) { ack = 0; // 收到ACK } PORTA.OUT &= ~(1 << SCL_PIN); PORTA.DIRSET = (1 << SDA_PIN); // 重新控制SDA _delay_us(2); return ack; } // 读取8位数据,send_ack=1发送ACK,0发送NACK uint8_t i2c_data_read(uint8_t send_ack) { uint8_t data = 0; PORTA.DIRCLR = (1 << SDA_PIN); // 释放SDA,由从机控制 for(uint8_t i=0; i<8; i++) { data <<= 1; PORTA.OUT |= (1 << SCL_PIN); _delay_us(4); // 留足信号稳定时间后采样 if(PORTA.IN & (1 << SDA_PIN)) { data |= 0x01; } PORTA.OUT &= ~(1 << SCL_PIN); _delay_us(2); } // 发送ACK/NACK PORTA.DIRSET = (1 << SDA_PIN); if(send_ack) { PORTA.OUT &= ~(1 << SDA_PIN); // ACK } else { PORTA.OUT |= (1 << SDA_PIN); // NACK } _delay_us(2); PORTA.OUT |= (1 << SCL_PIN); _delay_us(2); PORTA.OUT &= ~(1 << SCL_PIN); PORTA.OUT |= (1 << SDA_PIN); // 释放SDA _delay_us(2); return data; } // 写入16位EEPROM地址 void i2c_addr_write(uint16_t addr) { i2c_data_write((uint8_t)(addr >> 8)); i2c_data_write((uint8_t)(addr & 0xFF)); }
关键修改说明:
- 修正了
i2c_start()和i2c_stop()的时序,符合I2C标准规范。 - 优化了
i2c_data_write()的ACK等待逻辑,添加返回值标识ACK状态,避免死循环。 - 调整
i2c_data_read()的采样时机,将ACK/NACK的控制权交给调用者(读取最后一个字节时发送NACK),移除函数内部的i2c_stop()调用。 - 统一使用宏定义的引脚和设备地址,避免硬编码错误。
- 开启引脚内部上拉电阻,符合I2C总线的开漏输出要求。
- 添加了EEPROM写周期的等待时间,确保写入完成后再读取。
3. ATtiny402 I2C EEPROM读写规范与最佳实践
硬件层面
- 优先使用硬件TWI模块:ATtiny402集成了硬件TWI(I2C)外设,相比软件模拟,硬件模块能自动遵循时序规范,降低出错概率,提升通信稳定性。
- 引脚配置:I2C引脚(PA1/SDA、PA2/SCL)需配置为开漏输出,并开启内部上拉电阻(或外接10kΩ上拉电阻),确保总线电平稳定。
- 电源与接地:给EEPROM提供稳定的3.3V/5V电源,确保电源纹波在允许范围内,避免通信干扰。
软件层面
- 严格遵循I2C时序:参考I2C总线规范,确保起始、停止、数据位、ACK/NACK的时序参数符合要求(如SCL时钟频率不超过100kHz(标准模式)或400kHz(快速模式))。
- 处理EEPROM写周期:24C32的写周期最大为10ms,写入数据后必须等待足够时间,或通过轮询从机ACK的方式确认写入完成,再进行后续操作。
- 错误处理:在数据发送/接收时检查ACK状态,若未收到ACK,需进行重试或错误处理,避免程序卡死。
- 代码模块化:将I2C通信封装为独立函数(如起始、停止、写数据、读数据),提高代码可维护性和复用性。
- 调试技巧:使用逻辑分析仪或I2C调试器抓取总线波形,对比标准时序,快速定位时序错误。
- 避免硬编码:用宏定义统一设备地址、引脚、时序参数,便于后续修改和移植。
内容的提问来源于stack exchange,提问作者S mani
相关产品推荐
相关产品推荐

