You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 19:45:01