蓝桥杯第八届国赛代码错误总结
题目信息
-
题目名称: 超声波测距机的功能设计与实现
-
考察重点: EEPROM数据存储、数据回显、循环覆盖机制、多界面显示
-
难度: ★★★★☆
错误列表
1.
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字节
-
Distance是unsigned 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;
}
}
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* |
&variable |
|
unsigned int |
2字节 | unsigned int* |
(unsigned char*)&variable |
|
unsigned long |
4字节 | unsigned long* |
(unsigned char*)&variable |
|
float |
4字节 | float* |
(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*)×tamp, 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.
缺少上电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.
数据编号显示错误(第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.
按键处理中缺少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;
}
}
为什么进入界面和翻页要分别读取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 编号变了但数据没变!
为什么两次读取缺一不可?
-
进入界面时读取:
-
初始化
Data_Index = 0 -
从EEPROM读取第0次数据到
Display_History -
保证用户进入界面时看到正确的第1次数据
-
如果不读取:
Display_History是0或旧值,显示错误
-
-
翻页时读取:
-
Data_Index递增(0→1→2…) -
从EEPROM读取对应编号的数据到
Display_History -
更新数码管显示
-
如果不读取:
Display_History还是第1次的数据,编号变了但数据没变
-
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
总结
最严重的错误(会导致功能完全失效):
-
EEPROM写入字节数错误 - 2字节数据只写1字节,数据不完整
-
没有循环覆盖机制 - 每次都写地址0,没有实现循环保存10次数据
-
缺少上电初始化 - 没有从EEPROM读取Measure_Count
中等错误(功能异常或数据错误):
-
缺少校验机制 - 第一次上电时Measure_Count可能是随机值
-
数据编号显示错误 - 第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
蓝桥杯第八届国赛代码错误总结
题目: 超声波测距机的功能设计与实现