蓝桥杯第八届国赛代码错误总结

蓝桥杯第八届国赛代码错误总结

题目信息

  • 题目名称: 超声波测距机的功能设计与实现

  • 考察重点: EEPROM数据存储、数据回显、循环覆盖机制、多界面显示

  • 难度: ★★★★☆


错误列表

1. :cross_mark: EEPROM存储逻辑严重错误

错误代码:

void Get_Distance()
{
    EEPROM_Write(&EEPROM_Lock, 12, 1);  // ❌ 每次测量都写校验码?位置不对!
​
    if(Dec_Flag==1)
    {
        Dis_Last=Distance;
        Distance = Ut_Wave_Data();
​
        // ❌ 三个严重错误:
        EEPROM_Write(&Distance, 0, 1);
        // 1. Distance是unsigned int(2字节),却只写1字节!
        // 2. 每次都写到地址0,没有循环覆盖!
        // 3. 没有更新Measure_Count循环索引!
​
        Dis_Sum=Distance+Dis_Last;
        Dec_Flag=0;
        LED1_Rea=1;
    }
}

错误原因:

问题1: EEPROM写入字节数错误

EEPROM_Write(&Distance, 0, 1);  // ❌ 只写1字节
  • Distanceunsigned int 类型,占2字节

  • 但只写入了1字节(最低字节)

  • 高字节丢失,数据不完整

示例:

Distance = 305 (0x0131)
高字节: 0x01
低字节: 0x31
​
EEPROM_Write(&Distance, 0, 1) 只写入0x31
重新读取时: EEPROM_Read(&Distance, 0, 1) 只读到0x31
Distance = 0x0031 = 49 ❌ 数据错误!

问题2: 没有实现循环覆盖机制

EEPROM_Write(&Distance, 0, 1);  // ❌ 每次都写地址0
  • 题目要求:保存最近10次测量结果,超过10次循环覆盖

  • 实际代码:每次都写到地址0,只保存了最后一次

  • 没有使用 Measure_Count 来实现循环索引

问题3: 没有更新和保存测量次数计数器

  • 缺少 Measure_Count++ 更新计数器

  • 缺少 EEPROM_Write(&Measure_Count, 21, 1) 保存计数器

  • 导致无法知道当前保存到第几次了

问题4: 校验码写入位置和时机错误

EEPROM_Write(&EEPROM_Lock, 12, 1);  // ❌ 地址12?每次测量都写?
  • 校验码应该在上电初始化时写一次,不是每次测量都写

  • 地址分配混乱(注释说地址8,代码写的12)

EEPROM地址分配方案:

┌──────────────────────────────────────┐
│ 地址 0-1:   第0次测量 (2字节)         │
│ 地址 2-3:   第1次测量 (2字节)         │
│ 地址 4-5:   第2次测量 (2字节)         │
│ 地址 6-7:   第3次测量 (2字节)         │
│ 地址 8-9:   第4次测量 (2字节)         │
│ 地址10-11:  第5次测量 (2字节)         │
│ 地址12-13:  第6次测量 (2字节)         │
│ 地址14-15:  第7次测量 (2字节)         │
│ 地址16-17:  第8次测量 (2字节)         │
│ 地址18-19:  第9次测量 (2字节)         │
│ 地址20:     测量盲区参数 (1字节)      │
│ 地址21:     测量次数 Measure_Count    │
│ 地址22:     校验码 EEPROM_Lock=8      │
└──────────────────────────────────────┘

循环覆盖机制示例:

第1次测量: Distance=30cm
  → EEPROM_Write(&Distance, 0*2, 2)  // 写入地址0-1
  → Measure_Count = 0 → 1
​
第2次测量: Distance=45cm
  → EEPROM_Write(&Distance, 1*2, 2)  // 写入地址2-3
  → Measure_Count = 1 → 2
​
...
​
第10次测量: Distance=25cm
  → EEPROM_Write(&Distance, 9*2, 2)  // 写入地址18-19
  → Measure_Count = 9 → 0 (循环回0)
​
第11次测量: Distance=50cm
  → EEPROM_Write(&Distance, 0*2, 2)  // 覆盖地址0-1 ✓
  → Measure_Count = 0 → 1

正确代码:

void Get_Distance()
{
    if(Dec_Flag==1)
    {
        Dis_Last = Distance;
        Distance = Ut_Wave_Data();  // 获取新的测量结果
​
        // ✓ 保存测量结果到EEPROM(2字节)
        // 地址 = Measure_Count * 2(因为每次测量占2字节)
        EEPROM_Write((unsigned char*)&Distance, Measure_Count*2, 2);
​
        // ✓ 更新测量次数(0-9循环)
        Measure_Count++;
        if(Measure_Count >= 10)
            Measure_Count = 0;
​
        // ✓ 保存测量次数到EEPROM
        EEPROM_Write(&Measure_Count, 21, 1);
​
        Dis_Sum = Distance + Dis_Last;
        Dec_Flag = 0;
        LED1_Rea = 1;
    }
}

:open_book: EEPROM函数参数详解

函数定义:

void EEPROM_Read(unsigned char *str, unsigned char addr, unsigned num);
void EEPROM_Write(unsigned char *str, unsigned char addr, unsigned num);
                  ↑                   ↑                  ↑
                  参数1                参数2              参数3

三个参数详解:

参数 类型 含义 简单理解
参数1 unsigned char* 指向数据变量的内存地址 读/写到哪里
参数2 unsigned char EEPROM内部的存储地址(0-255) 从哪个地址开始
参数3 unsigned 要读/写的字节数 读/写几个字节

为什么要用 (unsigned char*)&Display_History

// 我们定义的变量
idata unsigned int Display_History = 0;  // unsigned int 类型

// 函数要求的参数1
unsigned char *str  // 要求是 unsigned char* 类型

// 直接传递 - 类型不匹配!
EEPROM_Read(&Display_History, 0, 2);  // ❌ 编译警告
//          ↑
//          &Display_History 是 unsigned int* 类型
//          函数要求的是 unsigned char* 类型

// 解决:强制类型转换
EEPROM_Read((unsigned char*)&Display_History, 0, 2);  // ✓ 正确
//          ↑
//          强制转换为 unsigned char* 类型

详细拆解步骤:

步骤1: 取地址 &Display_History

idata unsigned int Display_History = 0;

&Display_History  // 取Display_History变量的内存地址

内存示意图:

RAM内存布局:
地址0x40:  [低字节]  ← Display_History的低字节
地址0x41:  [高字节]  ← Display_History的高字节

&Display_History = 0x40 (起始地址)

步骤2: 强制类型转换 (unsigned char*)

(unsigned char*)&Display_History
↑               ↑
类型转换        取地址

为什么要转换?

  • &Display_History 的类型是 unsigned int*(指向int的指针)

  • 函数参数要求是 unsigned char*(指向char的指针)

  • C语言要求类型必须严格匹配

  • 所以需要强制转换:(unsigned char*)

转换的本质:

// 转换前
unsigned int *p = &Display_History;  // int指针,指向2字节数据

// 转换后
unsigned char *p = (unsigned char*)&Display_History;  // char指针,按字节访问

// 本质上都指向同一个内存地址,只是"看待"数据的方式不同

类型匹配规则总结:

变量类型 占用字节 取地址类型 是否需要转换 正确写法
unsigned char 1字节 unsigned char* :cross_mark: 不需要 &variable
unsigned int 2字节 unsigned int* :white_check_mark: 需要 (unsigned char*)&variable
unsigned long 4字节 unsigned long* :white_check_mark: 需要 (unsigned char*)&variable
float 4字节 float* :white_check_mark: 需要 (unsigned char*)&variable

实战示例:

// ✅ 正确写法

// 1字节变量 - 不需要转换
unsigned char count = 0;
EEPROM_Read(&count, 21, 1);  // ✓

// 2字节变量 - 需要转换
unsigned int distance = 0;
EEPROM_Read((unsigned char*)&distance, 0, 2);  // ✓

// 4字节变量 - 需要转换
unsigned long timestamp = 0;
EEPROM_Read((unsigned char*)&timestamp, 10, 4);  // ✓


// ❌ 错误写法

// 忘记强制转换
unsigned int distance = 0;
EEPROM_Read(&distance, 0, 2);  // ❌ 编译警告或错误

// 字节数不匹配
unsigned int distance = 0;
EEPROM_Read((unsigned char*)&distance, 0, 1);  // ❌ 只读1字节,数据不完整

// 地址和字节数搞反
unsigned char count = 0;
EEPROM_Read(&count, 1, 21);  // ❌ 从地址1读21字节?错了!

底层原理(进阶理解):

为什么参数1要求unsigned char* 类型?

因为EEPROM是按字节操作的:

void EEPROM_Read(unsigned char *str, unsigned char addr, unsigned num)
{
    // ... I2C通信代码 ...

    while(num--)
    {
        *str++ = I2CReceiveByte();  // ← 每次读1个字节
        //  ↑
        //  str必须是unsigned char*,才能逐字节赋值
    }
}

如果是unsigned int*会怎样?

// 假设函数是这样定义的(错误示范)
void EEPROM_Read_Wrong(unsigned int *str, unsigned char addr, unsigned num)
{
    while(num--)
    {
        *str++ = I2CReceiveByte();  // ❌ 问题!
        //  ↑
        //  每次会跳过2个字节的地址!
        //  因为int*指针++会移动2字节
    }
}

所以必须用unsigned char*:

  • unsigned char* 指针++移动1字节

  • 正好对应EEPROM的字节操作

  • 可以精确控制每个字节的读写

完整调用示例:

EEPROM_Read((unsigned char*)&Display_History, Data_Index*2, 2);
            ↑                                 ↑              ↑
            |                                 |              |
            参数1: 目标内存地址                参数2: EEPROM地址  参数3: 字节数
            |                                 |              |
            Display_History的地址              从哪个地址开始   读几个字节
            需要强制转换为unsigned char*        0-255          1、2、4...

记忆口诀:

第一个参数用 & 取地址,
类型不对要强转!
unsigned char 直接用 &,
unsigned int 要加 (unsigned char*)!

第二个参数是 EEPROM 地址,
第三个参数是读几个字节!

实例解析:

EEPROM_Write((unsigned char*)&Distance, Measure_Count*2, 2);
//             ↑                         ↑               ↑
//             |                         |               └─ 写2个字节(unsigned int)
//             |                         └─────────────── 写到EEPROM地址 Measure_Count*2
//             └───────────────────────────────────────── 把Distance的值写入
//                                                         (需要强制转换为unsigned char*)

// 为什么要 Measure_Count*2 ?
// 因为每个Distance占2字节:
// Measure_Count=0 → 地址0-1
// Measure_Count=1 → 地址2-3
// Measure_Count=2 → 地址4-5
// ...

关键点:

  • 2字节数据必须写2字节: EEPROM_Write((unsigned char*)&var, addr, 2)

  • 循环覆盖地址计算: 地址 = Measure_Count * 2

  • 计数器循环: Measure_Count = (Measure_Count + 1) % 10

  • 保存计数器: 否则下次测量会覆盖错误的位置

  • 类型转换: (unsigned char*)&Distance 强制转换为字节指针

  • 只能操作unsigned char*: 因为EEPROM按字节读写,指针++必须移动1字节


2. :cross_mark: 缺少上电EEPROM初始化和校验机制

错误代码:

void main()
{
    System_Init();

    // ❌ 完全没有从EEPROM读取数据!
    // ❌ 没有校验机制判断是否第一次上电!

    Scheduler_Init();
    Timer1_Init();

    while (1)
    {
        Scheduler_Run();
    }
}

错误原因:

问题1: 第一次上电时EEPROM是随机数据

第一次上电时,EEPROM中的数据是未定义的(随机值):
地址21可能是: 237 (随机值)

程序执行:
  Get_Distance()第1次测量:
    → EEPROM_Write(&Distance, 237*2, 2)  // ❌ 地址474,超出范围!
    → EEPROM_Write(&Measure_Count, 21, 1) // 写入237

  第2次测量:
    → Measure_Count = 237 → 238
    → EEPROM_Write(&Distance, 238*2, 2)  // ❌ 地址476,继续错误!

问题2: 上电后没有读取Measure_Count

  • 即使EEPROM有有效数据,上电后 Measure_Count = 0(初始值)

  • 导致从地址0开始覆盖,丢失之前的历史数据

问题3: 没有校验机制区分"第一次上电"和"重新上电"

  • 第一次上电:应该初始化 Measure_Count = 0 并写入EEPROM

  • 重新上电:应该从EEPROM读取 Measure_Count

为什么需要校验码?

情况1: 第一次上电(EEPROM空的)
  读取地址22 → 得到随机值(比如143)
  143 != 8 → 说明是第一次上电
  → 初始化 Measure_Count = 0
  → 写入校验码 EEPROM_Write(&EEPROM_Lock, 22, 1)  // 写入8
  → 写入初始值 EEPROM_Write(&Measure_Count, 21, 1)  // 写入0

情况2: 重新上电(EEPROM有数据)
  读取地址22 → 得到8
  8 == 8 ✓ → 说明EEPROM数据有效
  → 读取保存的测量次数
  → EEPROM_Read(&Measure_Count, 21, 1)  // 比如读到7
  → 下次测量从第8次继续

正确代码:

void main()
{
    System_Init();

    // ✓ EEPROM加锁机制:上电校验
    EEPROM_Read(&EEPROM_Temp, 22, 1);  // 读取校验码

    if(EEPROM_Temp == EEPROM_Lock)  // 校验通过(==8)
    {
        // ✓ EEPROM数据有效,读取保存的测量次数
        EEPROM_Read(&Measure_Count, 21, 1);
    }
    else  // 第一次上电
    {
        // ✓ 使用默认值
        Measure_Count = 0;

        // ✓ 写入校验码,标记EEPROM已初始化
        EEPROM_Write(&EEPROM_Lock, 22, 1);  // 写入8
        EEPROM_Write(&Measure_Count, 21, 1);  // 写入0
    }

    Scheduler_Init();
    Timer1_Init();

    while (1)
    {
        Scheduler_Run();
    }
}

校验机制的工作原理:

首次上电:
  └─ 读地址22 → 不是8 → 写入校验码8和初始值0

用户测量5次后断电:
  └─ EEPROM保存了5次测量结果
  └─ 地址21 = 5, 地址22 = 8

重新上电:
  └─ 读地址22 → 是8 ✓ → 读地址21 → 得到5
  └─ Measure_Count = 5
  └─ 下次测量从第6次继续,不会覆盖前5次数据 ✓

关键点:

  • 校验码分开存储: 地址22存校验码,地址21存数据

  • 上电时必须校验: 区分第一次上电和重新上电

  • 读取恢复状态: 从EEPROM读取 Measure_Count 恢复循环索引

  • 只写一次校验码: 在第一次上电时写入,之后不再修改


3. :cross_mark: 数据编号显示错误(第10次显示为9)

错误代码:

case 1://数据回显
    Seg_Buf[0] = Data_Index;  // ❌ Data_Index=9时只显示9
    Seg_Buf[1] = 10;          // ❌ 熄灭
    Seg_Buf[2] = 10;
    Seg_Buf[3] = 10;
    Seg_Buf[4] = 10;
    Seg_Buf[5] = Display_History / 100 % 10;
    Seg_Buf[6] = Display_History / 10 % 10;
    Seg_Buf[7] = Display_History % 10;
break;

错误原因:

问题1: Data_Index范围是0-9,但编号应该是1-10

Data_Index:  0   1   2  ...  9
应该显示:    1   2   3  ... 10  (用户理解的"第几次")
实际显示:    0   1   2  ...  9  ❌ 第10次显示为9

问题2: 数据编号应该占两位数字

题目图2要求的格式:

位1  位2  位3  位4  位5  位6  位7  位8
 0    5    8    8    8    0    3    0
数据编号:05                第5次测量:30cm
  • 位1+位2 共同显示数据编号(05)

  • 位3-5 熄灭

  • 位6-8 显示测量结果

当前代码的问题:

Data_Index = 5
Seg_Buf[0] = 5   // 位1显示5
Seg_Buf[1] = 10  // 位2熄灭
→ 显示: "5 - - - - 0 3 0"
→ 应该显示: "0 5 - - - 0 3 0"

Data_Index = 9 (第10次)
Seg_Buf[0] = 9   // 位1显示9 ❌
Seg_Buf[1] = 10  // 位2熄灭
→ 显示: "9 - - - - 0 3 0"
→ 应该显示: "1 0 - - - 0 3 0" (编号10)

正确代码:

case 1://数据回显
    // ✓ 数据编号占两位:位1显示十位,位2显示个位
    // Data_Index+1: 将0-9映射到1-10
    Seg_Buf[0] = (Data_Index+1) / 10 % 10;  // 十位
    Seg_Buf[1] = (Data_Index+1) % 10;       // 个位
    Seg_Buf[2] = 10;
    Seg_Buf[3] = 10;
    Seg_Buf[4] = 10;
    Seg_Buf[5] = Display_History / 100 % 10;
    Seg_Buf[6] = Display_History / 10 % 10;
    Seg_Buf[7] = Display_History % 10;
break;

显示效果验证:

Data_Index 编号计算 十位 个位 显示 说明
0 0+1=1 0 1 01 第1次测量 ✓
1 1+1=2 0 2 02 第2次测量 ✓
4 4+1=5 0 5 05 第5次测量 ✓
9 9+1=10 1 0 10 第10次测量 ✓

计算公式详解:

(Data_Index+1) / 10 % 10  // 十位
(Data_Index+1) % 10       // 个位

示例: Data_Index = 9
  → Data_Index+1 = 10
  → 十位 = 10 / 10 % 10 = 1 % 10 = 1 ✓
  → 个位 = 10 % 10 = 0 ✓
  → 显示 "10"

示例: Data_Index = 4
  → Data_Index+1 = 5
  → 十位 = 5 / 10 % 10 = 0 % 10 = 0 ✓
  → 个位 = 5 % 10 = 5 ✓
  → 显示 "05"

关键点:

  • +1映射: Data_Index+1 将内部索引(0-9)映射到用户可见编号(1-10)

  • 两位显示: 位1显示十位,位2显示个位

  • 十位计算: (Data_Index+1) / 10 % 10

  • 个位计算: (Data_Index+1) % 10

  • 前导0: 1-9会显示为01-09,符合题目要求


4. :cross_mark: 按键处理中缺少EEPROM读取逻辑

错误代码:

void Key_Proc()
{
    Key_Val = Key_Read();
    Key_Down = Key_Val & (Key_Val ^ Key_Old);
    Key_Up = ~Key_Val & (Key_Val ^ Key_Old);
    Key_Old = Key_Val;

    switch(Key_Down)
    {
        case 5:  // S5按键:数据回显
            if(Seg_Mode!=1)
                Seg_Mode=1;  // ❌ 只切换模式,没有读取数据!
            else
                Seg_Mode=0;
            Led_Proc();
        break;

        case 7:  // S7按键
            if(Seg_Mode==0)
                Cal_Flag^=1;
            else if(Seg_Mode==1)
            {
                // ❌ 完全没有翻页逻辑!
                // ❌ 没有读取EEPROM数据!
            }
            else
            {
                Param_Set+=10;
                if(Param_Set==100)
                    Param_Set=0;
            }
            Led_Proc();
        break;
    }
}

错误原因:

问题1: S5进入数据回显时没有初始化和读取数据

case 5:
    if(Seg_Mode!=1)
        Seg_Mode=1;  // ❌ 只切换了模式
    else
        Seg_Mode=0;
break;

导致的后果:

用户按S5进入数据回显界面:
  → Seg_Mode = 1 ✓ 界面切换成功
  → Data_Index = ??? (未初始化,可能是随机值)
  → Display_History = ??? (未读取,可能是0或旧值)

数码管显示:
  位1-2: 显示随机编号 ❌
  位6-8: 显示0或旧数据 ❌
  → 用户看到的完全是错误的数据!

问题2: S7在数据回显界面下没有翻页逻辑

else if(Seg_Mode==1)
{
    // ❌ 空的!完全没有任何逻辑!
}

导致的后果:

用户在数据回显界面按S7:
  → 什么都不发生 ❌
  → 无法翻页查看其他历史数据 ❌
  → 数据回显功能完全失效 ❌

问题3: 缺少必要的全局变量定义

没有定义这些变量,即使有读取逻辑也无法工作:

  • Data_Index - 当前显示第几次测量(0-9)

  • Display_History - 从EEPROM读取的历史数据临时变量

  • Measure_Count - 已测量次数(0-9循环)

正确代码:

步骤1: 新增全局变量

//题目要求的变量---------------------------
idata unsigned char Seg_Mode=0;
idata unsigned char Param=20;
idata unsigned char Param_Set=20;
idata unsigned int Distance;
idata unsigned int Dis_Last=0;
idata unsigned int Dis_Sum=0;
// ... 其他变量 ...

// ✓ 新增:数据回显相关变量
idata unsigned char Data_Index = 0;       // 当前显示的数据编号(0-9)
idata unsigned int Display_History = 0;   // 用于显示的历史数据临时变量
idata unsigned char Measure_Count = 0;    // 已测量次数(0-9循环)

步骤2: S5按键进入数据回显时初始化并读取

case 5:  // S5按键:数据回显
    if(Seg_Mode!=1)  // 从其他界面切换进来
    {
        Seg_Mode=1;

        // ✓ 初始化:显示第1次测量(内部索引为0)
        Data_Index = 0;

        // ✓ 从EEPROM读取第0次测量的数据(2字节)
        EEPROM_Read((unsigned char*)&Display_History, Data_Index*2, 2);
        //          ↑                                 ↑              ↑
        //          强制转换为unsigned char*           地址0           读2字节
    }
    else  // 已经在数据回显界面,再按S5退出
    {
        Seg_Mode=0;
    }
    Led_Proc();
break;

步骤3: S7按键在数据回显界面下翻页

case 7:  // S7按键:多功能键
    if(Seg_Mode==0)  // 测距显示界面
    {
        Cal_Flag^=1;  // 切换操作标志
    }
    else if(Seg_Mode==1)  // ✓ 数据回显界面
    {
        // ✓ 翻到下一页
        Data_Index++;
        if(Data_Index >= 10)  // 超过9回到0
            Data_Index = 0;

        // ✓ 从EEPROM读取对应编号的历史数据(2字节)
        EEPROM_Read((unsigned char*)&Display_History, Data_Index*2, 2);
        //                                             ↑
        //                                             地址根据Data_Index动态计算
        //                                             Data_Index=0 → 地址0
        //                                             Data_Index=1 → 地址2
        //                                             Data_Index=5 → 地址10
    }
    else  // 参数设置界面
    {
        Param_Set+=10;
        if(Param_Set==100)
            Param_Set=0;
    }
    Led_Proc();
break;

步骤4: 数据回显显示逻辑(在Seg_Proc函数中)

void Seg_Proc()
{
    switch(Seg_Mode)
    {
        case 0:  // 测距显示界面
            // ... 测距显示代码 ...
        break;

        case 1:  // ✓ 数据回显界面
            // 显示数据编号(两位)
            Seg_Buf[0] = (Data_Index+1) / 10 % 10;  // 十位
            Seg_Buf[1] = (Data_Index+1) % 10;       // 个位
            Seg_Buf[2] = 10;  // 熄灭
            Seg_Buf[3] = 10;
            Seg_Buf[4] = 10;
            // 显示从EEPROM读取的历史数据
            Seg_Buf[5] = Display_History / 100 % 10;
            Seg_Buf[6] = Display_History / 10 % 10;
            Seg_Buf[7] = Display_History % 10;
        break;

        case 2:  // 参数设置界面
            // ... 参数设置代码 ...
        break;
    }
}

:open_book: 为什么进入界面和翻页要分别读取EEPROM?

两次读取的不同触发场景:

场景 触发按键 条件 Data_Index EEPROM读取 作用
进入界面 S5 Seg_Mode!=1 初始化为0 读地址0(第1次数据) 初始化显示
翻页 S7 Seg_Mode==1 递增(0→1→2…→9→0) 读地址Data_Index*2 更新显示

时间线分析:

✓ 正确方式(两次读取)

用户操作          内部状态                      EEPROM读取                数码管显示
─────────────────────────────────────────────────────────────────────────
按S5进入          Seg_Mode: 0→1               读地址0                   01---030 ✓
                  Data_Index: 初始化为0        (第1次: 30cm)              第1次数据

按S7翻页          Seg_Mode: 1                 读地址2                   02---045 ✓
                  Data_Index: 0→1             (第2次: 45cm)              第2次数据

按S7翻页          Seg_Mode: 1                 读地址4                   03---050 ✓
                  Data_Index: 1→2             (第3次: 50cm)              第3次数据

✗ 错误方式(只在翻页时读取)

用户操作          内部状态                      EEPROM读取                数码管显示
─────────────────────────────────────────────────────────────────────────
按S5进入          Seg_Mode: 0→1               不读取 ❌                  01---000 ❌
                  Data_Index: 0 (但没读数据)                              空数据/旧值

按S7翻页          Seg_Mode: 1                 读地址2 ✓                 02---045 ✓
                  Data_Index: 0→1             (第2次: 45cm)              跳过了第1次!

✗ 错误方式(只在进入时读取)

用户操作          内部状态                      EEPROM读取                数码管显示
─────────────────────────────────────────────────────────────────────────
按S5进入          Seg_Mode: 0→1               读地址0 ✓                 01---030 ✓
                  Data_Index: 0               (第1次: 30cm)              第1次数据

按S7翻页          Seg_Mode: 1                 不读取 ❌                  02---030 ❌
                  Data_Index: 0→1                                        编号变了但数据没变!

为什么两次读取缺一不可?

  1. 进入界面时读取

    • 初始化 Data_Index = 0

    • 从EEPROM读取第0次数据到 Display_History

    • 保证用户进入界面时看到正确的第1次数据

    • 如果不读取Display_History 是0或旧值,显示错误 :cross_mark:

  2. 翻页时读取

    • Data_Index 递增(0→1→2…)

    • 从EEPROM读取对应编号的数据到 Display_History

    • 更新数码管显示

    • 如果不读取Display_History 还是第1次的数据,编号变了但数据没变 :cross_mark:

EEPROM地址计算说明:

EEPROM_Read((unsigned char*)&Display_History, Data_Index*2, 2);
//                                             ↑
//                                             地址 = Data_Index * 2

// 为什么要 * 2 ?
// 因为每个Distance占2字节(unsigned int):

Data_Index = 0  → 地址 = 0*2 = 0   → 读地址0-1   (第1次测量)
Data_Index = 1  → 地址 = 1*2 = 2   → 读地址2-3   (第2次测量)
Data_Index = 2  → 地址 = 2*2 = 4   → 读地址4-5   (第3次测量)
...
Data_Index = 9  → 地址 = 9*2 = 18  → 读地址18-19 (第10次测量)

关键点:

  • S5进入界面时必须读取:初始化显示第1次数据

  • S7翻页时必须读取:更新显示对应编号的数据

  • 两次读取场景不同:进入 vs 翻页

  • 两次读取缺一不可:保证用户体验完整

  • 地址动态计算Data_Index * 2(因为2字节数据)

  • 类型强制转换(unsigned char*)&Display_History


总结

最严重的错误(会导致功能完全失效):

  1. EEPROM写入字节数错误 - 2字节数据只写1字节,数据不完整

  2. 没有循环覆盖机制 - 每次都写地址0,没有实现循环保存10次数据

  3. 缺少上电初始化 - 没有从EEPROM读取Measure_Count

中等错误(功能异常或数据错误):

  1. 缺少校验机制 - 第一次上电时Measure_Count可能是随机值

  2. 数据编号显示错误 - 第10次显示为9,且只占一位


学到的经验

1. EEPROM数据存储必须注意字节数匹配

核心原则:

读几字节就写几字节
数据类型大小必须严格匹配

常见错误:

// ❌ 错误:2字节数据只写1字节
unsigned int value = 305;
EEPROM_Write(&value, 0, 1);  // 只写了最低字节

// ✓ 正确:2字节数据写2字节
unsigned int value = 305;
EEPROM_Write((unsigned char*)&value, 0, 2);

数据类型大小:

  • unsigned char: 1字节

  • unsigned int: 2字节

  • unsigned long: 4字节

  • float: 4字节(避免使用!)

EEPROM读写字节数规则:

EEPROM_Write(&var, addr, sizeof(var));  // 最保险的写法
EEPROM_Read(&var, addr, sizeof(var));

2. 循环缓冲区(Ring Buffer)的实现

核心思想:

使用循环索引实现固定大小的缓冲区
新数据覆盖最旧的数据

标准实现模式:

// 1. 定义循环索引
unsigned char index = 0;
#define BUFFER_SIZE 10

// 2. 写入数据
buffer[index] = new_data;
EEPROM_Write(&new_data, index*2, 2);  // 如果是2字节数据

// 3. 更新索引(循环)
index++;
if(index >= BUFFER_SIZE)
    index = 0;

// 4. 保存索引(重要!)
EEPROM_Write(&index, INDEX_ADDR, 1);

循环覆盖示例:

Buffer size = 10

写入第1-10次: index = 0,1,2,3,4,5,6,7,8,9
写入第11次:   index = 0 (覆盖第1次的数据)
写入第12次:   index = 1 (覆盖第2次的数据)

为什么要保存索引?

  • 断电后重新上电,索引从EEPROM恢复

  • 否则会从0开始,覆盖所有历史数据

3. EEPROM校验机制(数据有效性检测)

核心原则:

校验码与数据分开存储
第一次上电:写入校验码和初始值
重新上电:校验通过则读取数据

标准实现模式:

// 定义校验码常量
#define EEPROM_LOCK 8  // 可以是任意固定值

// 上电时检查
EEPROM_Read(&temp, CHECK_ADDR, 1);
if(temp == EEPROM_LOCK)
{
    // 校验通过,读取数据
    EEPROM_Read(&data, DATA_ADDR, 1);
}
else
{
    // 第一次上电,初始化
    data = DEFAULT_VALUE;
    EEPROM_Write(&EEPROM_LOCK, CHECK_ADDR, 1);
    EEPROM_Write(&data, DATA_ADDR, 1);
}

为什么需要校验码?

防止读取随机数据:

第一次上电(EEPROM空):
  地址0可能是: 157 (随机值)
  如果直接读取: data = 157 ❌ 错误的初始值

  使用校验码:
  地址8是: 234 (随机值)
  234 != 8 → 判断为第一次上电 ✓
  → 使用默认值 data = 20

地址分配建议:

数据地址与校验地址要分开,不要相邻
建议间隔至少8个地址

地址0:  数据
地址8:  校验码  ✓ 间隔8个地址

4. 多界面显示的按需数据加载

核心原则:

不要一次性加载所有数据到RAM
按需加载:进入界面时才从EEPROM读取

对比两种方法:

方法1: 全部加载(占用RAM)

// ❌ 在RAM中维护完整数组
unsigned int History[10];

// 上电时全部读取
for(i=0; i<10; i++)
{
    EEPROM_Read(&History[i], i*2, 2);
}

// 显示时从RAM读取
Seg_Buf[5] = History[Data_Index] / 100 % 10;

方法2: 按需加载(节省RAM)

// ✓ 只用一个临时变量
unsigned int Display_History;

// 切换到数据回显界面时读取
if(Seg_Mode!=1)
{
    Seg_Mode=1;
    Data_Index = 0;
    // 从EEPROM读取第0次数据
    EEPROM_Read((unsigned char*)&Display_History, 0, 2);
}

// S7翻页时读取对应数据
Data_Index++;
EEPROM_Read((unsigned char*)&Display_History, Data_Index*2, 2);

// 显示临时变量
Seg_Buf[5] = Display_History / 100 % 10;

对比:

方法 RAM占用 EEPROM读取次数 优点 缺点
全部加载 20字节 上电时10次 显示速度快 占用RAM多
按需加载 2字节 切换/翻页时1次 节省RAM 翻页时有延迟

为什么选择按需加载?

  • 51单片机RAM资源极其有限(STC15只有512字节内部RAM)

  • 历史数据不需要频繁访问

  • EEPROM读取速度足够快(几毫秒)

  • 用户翻页时的延迟可以接受

为什么进入界面和翻页要分别读取?

两次读取的不同触发时机:

// 第1次读取:进入数据回显界面时
case 5:  // S5按键
    if(Seg_Mode!=1)  // 从其他界面切换进来
    {
        Seg_Mode=1;
        Data_Index = 0;
        // ✓ 读取第0次(显示第1次)
        EEPROM_Read((unsigned char*)&Display_History, 0, 2);
    }
    else  // 已经在数据回显,再按退出
    {
        Seg_Mode=0;
    }
break;

// 第2次读取:在数据回显界面下翻页时
case 7:
    else if(Seg_Mode==1)  // 已经在数据回显界面
    {
        Data_Index++;
        if(Data_Index >= 10)
            Data_Index = 0;
        // ✓ 读取当前Data_Index对应的数据
        EEPROM_Read((unsigned char*)&Display_History, Data_Index*2, 2);
    }
break;

如果只在翻页时读取会怎样?

用户操作:        按S5进入             按S7翻页
Data_Index:      0                   0→1
EEPROM读取:      不读取              读地址2(第2次)
数码管显示:      01---000 ❌          02---045 ✓
                 (空数据或旧值)       (跳过了第1次)

关键点:

  • 进入界面时读取: 初始化显示第1次数据

  • 翻页时读取: 更新显示对应编号的数据

  • 两次读取缺一不可: 保证用户体验完整

5. 数据编号的显示技巧

题目常见要求:

内部索引: 0-9 (编程方便)
用户编号: 1-10 (用户理解)

映射方法:

// 内部索引转用户编号
user_number = index + 1;

// 两位显示
tens = user_number / 10;      // 十位
ones = user_number % 10;      // 个位

// 完整代码
Seg_Buf[0] = (index+1) / 10 % 10;  // 十位
Seg_Buf[1] = (index+1) % 10;       // 个位

前导0处理:

编号1:  显示 "01"  (十位=0, 个位=1)
编号5:  显示 "05"  (十位=0, 个位=5)
编号10: 显示 "10"  (十位=1, 个位=0)

为什么不消除前导0?

  • 题目要求显示两位数字(看题目示意图)

  • 保持显示格式统一

  • 便于用户识别(01比1更清晰)

6. EEPROM地址分配规划

核心原则:

数组数据连续存储
单个变量分散存储
校验码单独存储

标准地址分配:

┌────────────────────────────────────┐
│ 0-N:    数组/循环缓冲区(连续)     │
│ N+1-M:  预留空间                   │
│ M+1:    单个变量1                  │
│ M+2:    单个变量2                  │
│ M+3:    循环索引                   │
│ M+4:    校验码                     │
└────────────────────────────────────┘

本题的地址分配:

0-19:   10次测量结果(每次2字节,共20字节)
20:     测量盲区参数(1字节)
21:     测量次数索引(1字节)
22:     校验码(1字节)

为什么这样分配?

  • 数组连续: 地址计算简单 index * 2

  • 间隔存储: 防止数据覆盖

  • 校验独立: 校验码不会被数据覆盖


复盘检查清单

在蓝桥杯单片机比赛中,遇到EEPROM数据存储题目时,务必检查:

EEPROM存储:

  • 2字节数据必须写2字节: EEPROM_Write((unsigned char*)&var, addr, 2)

  • 类型转换正确: unsigned int/long需要 (unsigned char*)&

  • 循环覆盖地址计算: index * sizeof(data_type)

  • 更新并保存循环索引: index++; if(index>=10) index=0; EEPROM_Write(&index, ...)

  • 上电时从EEPROM读取索引: EEPROM_Read(&index, INDEX_ADDR, 1)

  • 校验机制: 校验码与数据分开存储,第一次上电初始化

数据回显功能:

  • 定义必要的全局变量: Data_Index, Display_History, Measure_Count

  • 进入数据回显界面时从EEPROM读取第一条数据

  • S7翻页功能:Data_Index++并读取对应数据

  • 两次读取不能合并: 进入界面读取 + 翻页读取

  • 显示逻辑正确: 编号占两位,历史数据占三位

  • 数据编号映射: (Data_Index+1) 显示1-10而非0-9

地址分配:

  • 数组数据连续存储: 0-19存10次测量结果

  • 单个变量分散存储: 20存参数,21存索引

  • 校验码独立存储: 22存校验码

  • 地址间隔合理: 避免数据覆盖

显示逻辑:

  • 数据编号占两位: 位1十位+位2个位

  • 数据编号+1映射: 内部0-9映射到用户1-10

  • 历史数据显示: 从Display_History读取

按需加载:

  • 不维护完整数组: 节省RAM

  • 切换界面时读取: 进入数据回显时读第一条

  • 翻页时读取: 按S7时读对应编号的数据


生成时间: 2026-02-20
蓝桥杯第八届国赛代码错误总结
题目: 超声波测距机的功能设计与实现

1 个赞