蓝桥杯第七周之第二讲

**

蓝桥杯单片机备赛指南第十四讲:IIC 总线与AT24C02 (EEPROM)

**

1. 硬件原理与存储机制

1.1 AT24C02 芯片详解

AT24C02 是2K bit 的串行EEPROM(电可擦除可编程只读存储器)。

  • 容量:$256 \text{ Bytes}$(地址0x00~ 0xFF)。

  • 接口:IIC 总线(SCL, SDA)。

  • 设备地址

    • 写地址0xA0

    • 读地址0xA1

1.2 IIC 通信基础

与上一讲(PCF8591)共用IIC 底层驱动。

  • SCL (P2.0):时钟线。

  • SDA (P2.1):数据线。


2. 底层驱动模块(iic.c核心补充)

在官方提供的基础时序(Start, Stop, Send, WaitAck…)之上,必须补充针对AT24C02 的字节读写函数

2.1 字节写函数( EEPROM_Write)

标准时序:Start$\rightarrow$设备写地址(0xA0)$\rightarrow$ACK$\rightarrow$ 内存地址 $\rightarrow$ACK$\rightarrow$ 数据 $\rightarrow$ACK$\rightarrow$Stop。

// 向指定地址写入一个字节
void EEPROM_Write(unsigned char addr, unsigned char dat)
{
    I2CStart();
    I2CSendByte(0xA0); // 设备写地址
    I2CWaitAck();
    I2CSendByte(addr); // 内存地址 (0x00~0xFF)
    I2CWaitAck();
    I2CSendByte(dat);  // 写入数据
    I2CWaitAck();
    I2CStop();
    
    // 【关键点】硬件写入需要时间,此处必须由调用者保证延时,或在此处死等
    // 建议:在此处不加延时,由上层逻辑控制,灵活性更高
}

2.2 字节读函数( EEPROM_Read)

标准时序:Start$\rightarrow$写地址(0xA0)$\rightarrow$ACK$\rightarrow$ 内存地址 $\rightarrow$ACK$\rightarrow$ Restart $\rightarrow$ 读地址(0xA1) $\rightarrow$ACK$\rightarrow$读取数据$\rightarrow$ NO_ACK $\rightarrow$Stop。

// 从指定地址读取一个字节
unsigned char EEPROM_Read(unsigned char addr)
{
    unsigned char dat;
    
    I2CStart();
    I2CSendByte(0xA0); // 伪写入,为了发送地址
    I2CWaitAck();
    I2CSendByte(addr); // 目标地址
    I2CWaitAck();
    
    I2CStart();        // 重启 IIC 总线
    I2CSendByte(0xA1); // 设备读地址
    I2CWaitAck();
    dat = I2CReceiveByte(); // 读取数据
    I2CSendAck(1);     // 发送非应答 (1),告知从机停止发送
    I2CStop();
    
    return dat;
}

3. 核心难点:写入周期(The 5ms Trap)

这是新手最容易挂的地方。

  • 现象:连续保存两个变量(如Save(a); Save(b);),结果a存进去了,b没存进去。

  • 原理:EEPROM 是物理烧写,写入一个字节后,芯片内部需要约5ms的搬运时间。在这5ms 内,芯片处于“忙”状态,不响应任何IIC 指令。

  • 解决方案:在两次写操作之间,必须插入**Delay5ms()**。

void Delay5ms() //@12.000MHz
{
    unsigned char i, j;
    i = 54;
    j = 199;
    do {
        while (--j);
    } while (--i);
}

4. 实战代码逻辑(main.c解析)

基于您上传的代码,这是一个典型的“读-改-写”模型。

4.1 变量定义

u8 Dat[2] = {10, 20}; // 数据数组
u8 a = 2;             // 独立数据

4.2 初始化读取(Read on Init)

上电时,必须先把EEPROM 里的旧数据读出来,覆盖掉代码里的默认值。

void main() {
    System_Init();
    
    // 从地址 0 读取数据到 Dat[0]
    Dat[0] = EEPROM_Read(0);
    // 连续读取技巧:如果不跨页,地址会自动+1,但为了稳妥建议用循环读单字节
    Dat[1] = EEPROM_Read(1); 
    
    // 从地址 8 读取数据到 a
    a = EEPROM_Read(8);
    
    while(1) {
        Key_Proc();
        Seg_Proc();
    }
}

4.3 写入策略(Write on Event)

绝对禁止while(1)或定时器里循环写EEPROM(寿命仅约100 万次)。只能在按键按下时写入。

void Key_Proc() {
    // ... 消抖代码 ...
    
    if(Key_Down == 4) { // S4: 保存键
        // 1. 保存 Dat[0]
        EEPROM_Write(0, Dat[0]);
        Delay5ms(); // 【必背】等待写完
        
        // 2. 保存 Dat[1]
        EEPROM_Write(1, Dat[1]);
        Delay5ms(); // 【必背】
        
        // 3. 保存 a
        EEPROM_Write(8, a);
        Delay5ms(); // 【必背】养成好习惯,最后一次写完也加延时,防止后续立即读取出错
    }
    // ... 其他按键修改 Dat 和 a 的值 ...
}

5. 蓝桥杯通用工程大模板(EEPROM 专项优化版)

针对所有存储类题目,我总结了一套**“参数镜像+ 首次运行检测”**的标准架构。这套模板能解决“第一次烧录程序数据乱码”和“参数管理混乱”的问题。

5.1 架构核心思想

  1. RAM 镜像(Mirror):定义一个全局数组Para[],程序运行时只读写这个数组,不直接操作EEPROM。

  2. 上电加载(Load)main函数开头将EEPROM 数据读入Para[]

  3. 首次检测(First Run Check):检查EEPROM 特定地址是否有“标记”。如果没有(说明是新芯片或刚烧录),则写入默认值;如果有,则加载旧值。

  4. 批量保存(Batch Save):在“退出设置界面”或按下“保存键”时,将Para[]里的数据逐个写入EEPROM,并处理延时。

5.2 核心代码模板(直接背诵)

// === 1. 参数定义 ===
// 假设有4个参数:温度上限、下限、亮度等级、灵敏度
u8 Para[4] = {30, 20, 5, 2}; 

// === 2. 初始化检查函数 (放在 main 开头) ===
void EEPROM_Init_Check() {
    u8 check_val;
    
    // 读取地址 0xFF 处的标记位
    check_val = EEPROM_Read(0xFF);
    
    // 判断标记:如果不是 0xA5 (自定义的魔法数字),说明是第一次运行
    if(check_val != 0xA5) {
        // 写入默认参数 (将代码开头定义的初始值写入 EEPROM)
        EEPROM_Write(0x00, Para[0]); Delay5ms();
        EEPROM_Write(0x01, Para[1]); Delay5ms();
        EEPROM_Write(0x02, Para[2]); Delay5ms();
        EEPROM_Write(0x03, Para[3]); Delay5ms();
        
        // 写入标记,表示初始化完成
        EEPROM_Write(0xFF, 0xA5); Delay5ms();
    } 
    else {
        // 如果标记存在,说明不是第一次,从 EEPROM 加载旧参数覆盖 Para[]
        Para[0] = EEPROM_Read(0x00);
        Para[1] = EEPROM_Read(0x01);
        Para[2] = EEPROM_Read(0x02);
        Para[3] = EEPROM_Read(0x03);
    }
}

// === 3. 批量保存函数 (在 Key_Proc 中调用) ===
void System_Save() {
    u8 i;
    for(i=0; i<4; i++) {
        EEPROM_Write(i, Para[i]); // 地址 i 存 Para[i]
        Delay5ms();               // 这里的延时是必须的
    }
}

// === 4. 主函数调用 ===
void main() {
    System_Init();
    EEPROM_Init_Check(); // 上电自检与加载
    
    while(1) {
        Key_Proc(); // 在按键处理中修改 Para[],并决定何时调用 System_Save()
        Seg_Proc(); // 显示 Para[]
    }
}

5.3 模板优势总结

痛点 解决方案 效果
新芯片数据乱码 check_val != 0xA5判断 第一次上电自动初始化为默认值,无需手动预设
写入卡死 Delay5ms() 保证硬件有足够时间处理数据,杜绝程序跑飞
寿命损耗 System_Save() 只有在需要保存时才调用,避免while(1)狂写
逻辑混乱 Para[]数组 统一管理所有参数,地址与数组下标一一对应,不易出错
根据您提供的核心逻辑,我为您将这段话进行了学术化提炼结构化重组,形成了蓝桥杯单片机最权威的**“分时轮询系统架构(The Grand Template)”**总结。

这段总结已加入到第12讲:AT24C02的文末,作为全系列驱动与逻辑的最终收束。


6. 终极工程架构总结(The Grand Template)

经过前面12 讲的学习,我们将一套成熟的**“分时轮询系统架构”**固化为标准模板。该架构将硬件驱动与业务逻辑完全解耦,是解决复杂省赛/国赛题目的通用钥匙。

6.1 系统顶层设计

程序执行流被划分**为主循环(Main Loop)中断服务(ISR)**两大并行轨。

  • 主循环( while(1)):负责业务逻辑的调度,包含三大核心进程的轮转:

    1. 按键处理( Key_Proc) :负责输入。

    2. 信息处理( Seg_Proc):负责数据获取、运算与显示数据生成。

    3. 显示反馈( LED_Proc) :负责状态指示。

  • 定时器中断( Timer0_Isr) :负责底层硬件驱动与时基提供。

6.2 模块职责详解

A. 定时器中断层(The Heartbeat)

中断是系统的“心跳”,严禁执行耗时逻辑(如IIC/OneWire 读取),只负责三个轻量级任务:

  1. 软件减速(Time Slicing) :为Key_ProcSeg_Proc提供非阻塞的时间片信号(Slow_Down变量)。

  2. 硬件扫描(Scanning):负责数码管( Seg_Disp) 和LED ( LED_Disp) 的动态刷新,保证视觉暂留。

  3. 时序控制(Timing) :处理闪烁频率(如500ms 翻转)和长按计时。

B. 按键处理层(Input Layer)

采用**“四行代码状态机”**实现复杂交互:

  • 动作捕捉:通过位运算提取按下( Key_Down)、**松手( Key_Up)长按保持( Key_Old)**三种状态。

  • 逻辑触发:参数修改、界面切换、模式变更均在此触发。

  • 存储写入:EEPROM ( AT24C02) 的写入操作必须在此层(按键触发时)执行,严禁在循环中狂写。

C. 信息处理层(Logic & Data Layer)

这是系统的大脑,负责数据的“吞吐”与“映射”:

  • 数据获取(Input)读取时钟(DS1302)读取温度(DS18B20)、**模数转换(PCF8591 AD)**等耗时操作均放置于此。

  • 显存映射(Mapping):通过修改全局数组**Seg_Buf[]LED_Buf[]**来改变显示内容。业务逻辑只管改数组,不需要关心硬件是如何点亮的(实现了逻辑与硬件的解耦)。

D. 数据存储层(Storage Strategy)

  • 读取(Read):EEPROM 的读取操作一般放在**主函数初始化(main开头)**中,上电即加载参数到全局变量。

  • 写入(Write):如前所述,写入操作封装在按键逻辑中。

6.3 架构图解(Cheat Sheet)

程式码片段

graph TD
    subgraph ISR [定时器中断 Timer0]
        T1[软件减速计数] --> T2[数码管/LED扫描]
        T2 --> T3[闪烁计时]
    end

    subgraph Main [主循环 While(1)]
        Init[EEPROM读取 / 硬件初始化] --> Loop
        
        Loop --> K[按键处理 Key_Proc]
        K --> |按下/长按/松手| Logic[修改参数/切换界面/EEPROM写入]
        
        Loop --> S[信息处理 Seg_Proc]
        S --> |耗时任务| Read[读取温度/时钟/AD]
        Read --> Map[更新显存 Seg_Buf / LED_Buf]
        
        Loop --> L[显示处理 LED_Proc]
        L --> Ind[更新指示灯状态]
    end