蓝桥杯第十一届省赛(第一套)代码错误总结
错误列表
1.
使用浮点数据类型导致精度和EEPROM存储问题
错误代码:
// 全局变量定义
idata float Param = 0; // ❌ 使用float
idata float Param_Set = 0; // ❌ 使用float
idata float AD_3_Data_10x = 0; // ❌ 使用float
void Key_Proc()
{
switch(Key_Down)
{
case 12:
if(Seg_Mode == 0)
Param_Set = Param;
if(Seg_Mode == 1)
{
Param = Param_Set;
EEPROM_Write(&Param, 0, 1); // ❌ 写入float到EEPROM (4字节)
EEPROM_Write(&EEPROM_Lock, 8, 1);
}
Seg_Mode = (++Seg_Mode) % 3;
break;
}
}
void main()
{
EEPROM_Read(&EEPROM_Temp, 8, 1);
if (EEPROM_Temp == EEPROM_Lock)
{
EEPROM_Read(&Param, 0, 1); // ❌ 只读1字节,但Param是4字节float!
Param_Set = Param;
}
}
错误原因:
问题1: 数据类型不匹配
-
参数值范围是 0.0V - 5.0V,步进 0.5V,共11个有效值(0, 5, 10, …, 50,单位0.1V)
-
使用
float类型完全没必要,且占用4字节内存 -
单片机浮点运算极慢,且容易产生精度误差
-
题目要求的是0.1V为单位的整数,应该用
unsigned char
问题2: EEPROM读写字节数不一致
-
EEPROM_Write(&Param, 0, 1)只写入1字节 -
但
Param是4字节的float,只写了最低字节 -
EEPROM_Read(&Param, 0, 1)只读取1字节 -
导致数据不完整,参数值错误
EEPROM读写函数参数详解
很多初学者容易被EEPROM函数的三个参数搞混,这里详细说明:
函数原型:
void EEPROM_Write(unsigned char *buf, unsigned char addr, unsigned char num);
void EEPROM_Read(unsigned char *buf, unsigned char addr, unsigned char num);
三个参数的含义:
| 参数位置 | 参数名 | 含义 | 示例 |
|---|---|---|---|
| 第1个参数 | *buf |
数据变量的地址(用&取地址) |
&Param_Val、&EEPROM_Lock |
| 第2个参数 | addr |
EEPROM存储地址(0-255) | 0(数据)、8(校验码) |
| 第3个参数 | num |
读写字节数 | 1(unsigned char占1字节) |
实例解析:
EEPROM_Write(&Param_Val, 0, 1);
// ↑ ↑ ↑
// | | └─ 写1个字节
// | └──── 写到EEPROM地址0
// └──────────── 把Param_Val的值写入
EEPROM_Write(&EEPROM_Lock, 8, 1);
// ↑ ↑ ↑
// | | └─ 写1个字节
// | └──── 写到EEPROM地址8
// └───────────────── 把EEPROM_Lock的值写入
EEPROM_Read(&AD_3_Data_10x, 0, 1);
// ↑ ↑ ↑
// | | └─ 读1个字节
// | └──── 从EEPROM地址0读取
// └──────────────── 读到AD_3_Data_10x变量
EEPROM_Read(&EEPROM_Temp, 8, 1);
// ↑ ↑ ↑
// | | └─ 读1个字节
// | └──── 从EEPROM地址8读取
// └───────────────── 读到EEPROM_Temp变量
为什么使用地址0和地址8?(数据校验机制)
EEPROM存储布局:
┌─────────────────────────────────┐
│ 地址0: 参数值 (Param_Val) │ ← 实际数据
├─────────────────────────────────┤
│ 地址1-7: 未使用 │
├─────────────────────────────────┤
│ 地址8: 校验码 (EEPROM_Lock=5) │ ← 数据有效标志
└─────────────────────────────────┘
校验机制的工作原理:
首次上电(EEPROM是空的):
EEPROM_Read(&EEPROM_Temp, 8, 1); // 从地址8读取校验码
if (EEPROM_Temp == EEPROM_Lock) // 比较:读到的 == 5?
{
// ✗ 不相等(EEPROM空的,读到的是随机值)
// 所以不执行,使用默认值
}
else
{
// ✓ 首次运行,写入校验码
EEPROM_Write(&EEPROM_Lock, 8, 1); // 在地址8写入5
}
用户修改参数并保存(按S12切换时):
if(Seg_Mode == 1) // 在参数界面按S12
{
Param_Val = Param_Set; // 例如设置为20(2.0V)
EEPROM_Write(&Param_Val, 0, 1); // 在地址0写入20
EEPROM_Write(&EEPROM_Lock, 8, 1); // 在地址8写入5(确保校验码有效)
}
// 此时EEPROM内容:
// 地址0: 20 (参数值)
// 地址8: 5 (校验码)
重新上电(恢复保存的参数):
EEPROM_Read(&EEPROM_Temp, 8, 1); // 从地址8读取 → 得到5
if (EEPROM_Temp == EEPROM_Lock) // 5 == 5?
{
// ✓ 相等!说明EEPROM有有效数据
EEPROM_Read(&AD_3_Data_10x, 0, 1); // 从地址0读取 → 得到20
Param_Val = AD_3_Data_10x; // Param_Val = 20
Param_Set = Param_Val; // 恢复成功!
}
为什么要分开存储?
防止误判:
-
如果只存参数值在地址0,EEPROM空的时候地址0可能是随机值(比如碰巧是15)
-
程序会误以为用户设置过15(1.5V),导致错误
-
同时检查地址8的校验码,可以确认数据有效性
数据校验:
-
地址8 == 5 → EEPROM数据有效,可以读取地址0
-
地址8 != 5 → EEPROM数据无效,使用默认值
完整流程:
首次上电:
└─ 读地址8 → 不是5 → 不读地址0,使用默认值10
用户设置参数为2.0V(20):
└─ 按S12保存 → 写地址0=20,写地址8=5
重新上电:
└─ 读地址8 → 是5 ✓ → 读地址0 → 得到20 → 参数恢复成功!
记忆口诀:
第一个参数是变量,第二个是地址,第三个是字节数!
地址0存数据,地址8存校验,分开存储更可靠!
问题3: 初始化时的类型转换错误
// main函数中
EEPROM_Read(&AD_3_Data_10x, 0, 1); // 读取1字节到float变量
Param = AD_3_Data_10x / 10.0f; // ❌ 浮点除法,且逻辑错误
-
从EEPROM读取的是以0.1V为单位的整数值(例如25表示2.5V)
-
应该直接赋值,不需要除以10
导致的测试失败:
-
参数设置保存后重新上电,参数值错误
-
计数逻辑基于错误的阈值,导致计数不准确
-
测评系统检测到参数保存/读取失败
-
初始分数: 33.6/70
正确代码:
// 全局变量定义
idata unsigned char Param_Val = 10; // ✓ 使用unsigned char,单位0.1V
idata unsigned char Param_Set = 10; // ✓ 使用unsigned char
idata unsigned char AD_3_Data_10x = 0; // ✓ 使用unsigned char
void Key_Proc()
{
switch(Key_Down)
{
case 12:
if(Seg_Mode == 0)
Param_Set = Param_Val;
if(Seg_Mode == 1)
{
Param_Val = Param_Set;
EEPROM_Write(&Param_Val, 0, 1); // ✓ 写入1字节unsigned char
EEPROM_Write(&EEPROM_Lock, 8, 1);
}
Seg_Mode = (++Seg_Mode) % 3;
break;
}
}
void main()
{
EEPROM_Read(&EEPROM_Temp, 8, 1);
if (EEPROM_Temp == EEPROM_Lock)
{
EEPROM_Read(&AD_3_Data_10x, 0, 1); // ✓ 读取1字节
Param_Val = AD_3_Data_10x; // ✓ 直接赋值,不需要类型转换
Param_Set = Param_Val;
}
}
关键点:
-
嵌入式系统优先使用整数类型,避免浮点运算
-
数据类型大小必须与EEPROM读写字节数严格匹配
-
以0.1V为单位的整数值,范围0-50,
unsigned char完全够用 -
EEPROM读写时,读几字节就写几字节
-
校验码(address 8)和数据(address 0)分开存储
-
初始化时直接赋值,不需要浮点除法
2.
S12按键处理时序错误:必须先处理再切换模式!
错误代码:
void Key_Proc()
{
switch(Key_Down)
{
case 12:
Seg_Mode = (++Seg_Mode) % 3; // ❌ 先切换!
if(Seg_Mode == 0) // ❌ 此时判断的是新状态!
Param_Set = Param_Val;
if(Seg_Mode == 1) // ❌ 此时判断的是新状态!
{
Param_Val = Param_Set;
EEPROM_Write(&Param_Val, 0, 1);
}
break;
}
}
导致的问题:
-
按S12切换模式时,先改变了Seg_Mode的值
-
判断
if(Seg_Mode == 1)时,Seg_Mode已经是2了 -
导致参数没有保存到EEPROM
-
重新上电后参数丢失
正确代码:
void Key_Proc()
{
switch(Key_Down)
{
case 12:
if(Seg_Mode == 0) // ✓ 判断当前状态(旧状态)
Param_Set = Param_Val;
if(Seg_Mode == 1) // ✓ 判断当前状态(旧状态)
{
Param_Val = Param_Set;
EEPROM_Write(&Param_Val, 0, 1); // ✓ 保存参数
EEPROM_Write(&EEPROM_Lock, 8, 1);
}
Seg_Mode = (++Seg_Mode) % 3; // ✓ 处理完后再切换!
break;
}
}
3.
AD转换多次读取导致数据不一致
错误代码:
void AD_DA()
{
// ❌ 第1次读取AIN3
AD_3_Data_10x = Ad_Read(0x43) * 10 / 51;
// ❌ 第2次读取AIN3
AD_Val = Ad_Read(0x43) * 100 / 51;
// ... 其他逻辑
}
错误原因:
问题: 同一个ADC通道读取两次
-
PCF8591每次读取需要一定时间
-
连续两次读取可能得到不同的AD值(因为输入电压在变化)
-
导致
AD_3_Data_10x和AD_Val基于不同的采样值
示例:
第1次 Ad_Read(0x43) 返回: 128
→ AD_3_Data_10x = 128 * 10 / 51 = 25 (2.5V)
第2次 Ad_Read(0x43) 返回: 130 (电压微小变化)
→ AD_Val = 130 * 100 / 51 = 254 (2.54V)
结果:
AD_3_Data_10x = 25
AD_Val = 254
❌ 不一致! (应该都是基于同一个AD值)
导致的问题:
-
数码管显示的电压值(基于
AD_Val) -
与LED判断、计数判断(基于
AD_3_Data_10x或AD_Val)不一致 -
可能导致显示2.5V但LED状态不对,或计数错误
正确代码:
void AD_DA()
{
idata unsigned char ad_temp;
ad_temp = Ad_Read(0x43); // ✓ 只读取一次!
AD_3_Data_10x = ad_temp * 10 / 51; // ✓ 基于同一个值计算
AD_Val = ad_temp * 100 / 51; // ✓ 基于同一个值计算
// ... 其他逻辑
}
关键点:
-
ADC传感器只读取一次
-
所有需要的数据都基于这一次读取的值进行计算
-
避免多次调用
Ad_Read()导致数据不一致 -
保证显示、判断、计数使用相同的AD值
-
这是数据一致性原则
4.
重复计数逻辑导致错误(Seg_Proc和AD_DA都有计数)
错误代码 (分数从65.1降到55.6):
void Seg_Proc()
{
switch(Seg_Mode)
{
case 0:
// 数据显示
Seg_Buf[5] = AD_Val/100%10+',';
Seg_Buf[6] = AD_Val/10%10;
Seg_Buf[7] = AD_Val%10;
// ❌ Seg_Proc中也有计数逻辑!
if(Count_Flag == 1)
{
Count++;
Count_Flag = 0;
}
break;
// ...
}
}
void AD_DA()
{
idata unsigned char ad_temp;
ad_temp = Ad_Read(0x43);
AD_3_Data_10x = ad_temp * 10 / 51;
AD_Val = ad_temp * 100 / 51;
// ❌ AD_DA中也有计数逻辑!
if(AD_Val < Param_Val * 10)
Count_Flag = 1;
else
Count_Flag = 0;
}
错误原因:
重复计数问题分析:
调度器执行周期:
- Seg_Proc: 每20ms执行一次
- AD_DA: 每150ms执行一次
时间线:
0ms: AD_DA执行 → AD_Val < threshold → Count_Flag = 1
20ms: Seg_Proc执行 → Count_Flag==1 → Count++ ✓ (第1次计数)
40ms: Seg_Proc执行 → Count_Flag==1 → Count++ ❌ (第2次计数!)
60ms: Seg_Proc执行 → Count_Flag==1 → Count++ ❌ (第3次计数!)
...
150ms: AD_DA执行 → 如果AD_Val还<threshold → Count_Flag保持1
如果AD_Val>=threshold → Count_Flag = 0
导致的问题:
-
Count_Flag设为1后,在下一次AD_DA执行之前(150ms周期) -
Seg_Proc会执行多次(20ms周期)
-
每次Seg_Proc都会Count++
-
导致一次下降沿被重复计数7-8次(150ms/20ms≈7.5)
-
计数值完全错误
为什么会有这个错误版本:
-
这是我(AI助手)给出的错误建议
-
误以为要在Seg_Proc中处理计数显示更新
-
没有意识到调度器周期不同导致的重复计数问题
-
用户反馈: “我草你别乱改啊,分数更低了55.6分”
正确做法:
-
计数逻辑只能在一个地方
-
应该在AD_DA函数中直接判断和计数
-
不使用Count_Flag标志位
-
使用边沿检测而不是电平触发
5.
计数判断不要用标志位,要用新旧电位与参数比较!(最关键的错误!)
错误代码 (65.1分,缺失的最后一块拼图):
void Led_Proc()
{
// ❌ 使用Count_Flag标志位计数
if(Count_Flag == 1)
{
Count++;
Count_Flag = 0;
}
else if(Count_Flag == 0)
{
// 什么都不做
}
}
void AD_DA()
{
idata unsigned char ad_temp;
ad_temp = Ad_Read(0x43);
AD_3_Data_10x = ad_temp * 10 / 51;
AD_Val = ad_temp * 100 / 51;
// ❌ 用标志位:只要AD_Val小于阈值就设置标志
if(AD_Val < Param_Val * 10)
Count_Flag = 1;
else
Count_Flag = 0;
}
错误原因:
标志位方法的问题:
时间线分析(假设阈值=2.0V=200):
t1: AD_Val=250 (2.50V) → Count_Flag=0 (大于阈值)
t2: AD_Val=240 (2.40V) → Count_Flag=0 (大于阈值)
t3: AD_Val=190 (1.90V) → Count_Flag=1 ❌ 立即设置标志
Led_Proc检测到Count_Flag==1 → Count++ (第1次)
Count_Flag清零
t4: AD_Val=180 (1.80V) → Count_Flag=1 ❌ 仍然小于阈值,又设置标志
Led_Proc检测到Count_Flag==1 → Count++ (第2次) ❌ 重复计数!
t5: AD_Val=185 (1.85V) → Count_Flag=1 ❌ 继续设置标志
...继续重复计数
标志位的三大问题:
-
无法区分"刚跨越阈值"和"持续低于阈值"
-
Count_Flag = 1只表示"当前低于阈值" -
无法知道上一次是什么状态
-
导致只要低于阈值就计数
-
-
标志清零时机难以控制
-
Led_Proc清零后,AD_DA又会设置
-
如果AD值持续低于阈值,标志会反复设置
-
导致重复计数
-
-
跨函数传递状态不可靠
-
AD_DA设置标志 → Led_Proc读取标志
-
调度器周期不同可能导致错位
-
状态传递容易出错
-
正确方法:直接比较新旧AD值与参数
核心思想:
保存上一次的AD值(AD_Val_Old)
每次采样后,比较旧值和新值与阈值的关系
只有"旧值>阈值 且 新值≤阈值"时才计数
正确的判断逻辑:
// 需要两个变量:
idata unsigned int AD_Val_Old = 0; // 上一次的AD值
idata unsigned int AD_Val = 0; // 当前的AD值
// 判断条件:
if((AD_Val_Old > Param_Val * 10) && (AD_Val <= Param_Val * 10))
{
Count++; // 只在从"大于阈值"变为"小于等于阈值"时计数!
}
// 保存当前值作为下次的旧值
AD_Val_Old = AD_Val;
时间线分析(正确方法):
t1: AD_Val_Old=250, AD_Val=240
判断: (250>200) && (240<=200)? → false (240还没小于等于200)
不计数 ✓
t2: AD_Val_Old=240, AD_Val=220
判断: (240>200) && (220<=200)? → false (220还没小于等于200)
不计数 ✓
t3: AD_Val_Old=220, AD_Val=190
判断: (220>200) && (190<=200)? → true ✓ 检测到跨越瞬间!
Count++ (计数1次)
AD_Val_Old = 190 ← 更新旧值
t4: AD_Val_Old=190, AD_Val=180
判断: (190>200) && (180<=200)? → false (190不大于200)
不计数 ✓ 避免重复计数!已经在低于阈值的状态了
t5: AD_Val_Old=180, AD_Val=185
判断: (180>200) && (185<=200)? → false
不计数 ✓ 持续低于阈值,不计数
t6: AD_Val_Old=185, AD_Val=210
判断: (185>200) && (210<=200)? → false (210不小于等于200)
不计数 ✓ 上升沿不计数
AD_Val_Old = 210 ← 更新旧值
t7: AD_Val_Old=210, AD_Val=190
判断: (210>200) && (190<=200)? → true ✓ 又一次跨越!
Count++ (计数1次)
为什么这个方法准确?
| 条件 | 含义 | 作用 |
|---|---|---|
AD_Val_Old > threshold |
上一次在阈值上方 | 确保从高电位开始 |
AD_Val <= threshold |
当前在阈值下方 | 确保已经跨越到低电位 |
| 两个条件同时满足 | 刚好跨越阈值 | 只在跨越瞬间计数一次 |
对比三个满分代码的边沿检测实现:
满分代码1 (白鹭霜华):
void AD_DA(void)
{
ad_in_100x = Ad_Read(0x03) * 100 / 51;
// ✓ 边沿检测: 从大于变为小于
if((old_ad_in_100x > vol_para_10x*10) && (ad_in_100x < vol_para_10x*10))
value_count++;
old_ad_in_100x = ad_in_100x; // ✓ 保存当前值作为下次的旧值
}
满分代码2 (emo哥):
void state(void)
{
dat_vol = read_AD(0x03);
// ✓ 使用state_vol作为状态标志,检测状态变化
if(dat_vol <= dat_vol_set)
state_vol = 1;
else
state_vol = 0;
// ✓ old_state_vol != state_vol 检测状态变化
if((old_state_vol != state_vol) && (state_vol == 1))
num++;
old_state_vol = state_vol; // ✓ 保存状态
}
满分代码3 (学员代码) - 较复杂的实现:
void led_proc()
{
// ... LED逻辑
// ✓ 使用else if确保互斥,但本质还是边沿检测
if((ad_in_10x <= vol_para_10x) && (f_check == 0))
{
f_check = 1;
num_count++;
}
else if((ad_in_10x > vol_para_10x) && (f_check == 1))
{
f_check = 0;
}
}
最终正确代码 (用户的70分满分代码):
// 全局变量
idata unsigned int AD_Val = 0; // 当前AD值(放大100倍)
idata unsigned int AD_Val_Old = 0; // 上一次的AD值
void AD_DA()
{
idata unsigned char ad_temp;
ad_temp = Ad_Read(0x43); // 只读一次
AD_3_Data_10x = ad_temp * 10 / 51;
AD_Val = ad_temp * 100 / 51;
// ✓ 边沿检测: 检测从"大于阈值"到"小于等于阈值"的跨越
if((AD_Val_Old > Param_Val * 10) && (AD_Val <= Param_Val * 10))
{
Count++; // 只在跨越瞬间计数一次!
}
AD_Val_Old = AD_Val; // ✓ 保存当前值,供下次比较
}
关键点:
-
不要用标志位(Count_Flag),标志位无法区分"刚跨越"和"持续低于"
-
直接比较新旧AD值与参数:
(AD_Val_Old > threshold) && (AD_Val <= threshold) -
需要保存上一次的AD值(AD_Val_Old)
-
只在跨越瞬间计数一次,避免重复计数
-
这是嵌入式系统中事件检测的标准方法
为什么不能用标志位:
-
标志位只表示当前状态,无法表示状态变化
-
无法区分"刚跨越阈值"和"持续低于阈值"
-
标志清零时机难以控制,容易导致重复计数或漏计数
-
跨函数传递状态不可靠,调度器周期不同会导致错位
为什么要用新旧值比较:
-
明确检测状态变化:从"大于"变为"小于等于"
-
只在跨越瞬间触发一次,自动避免重复计数
-
逻辑简单可靠,不需要额外的标志位和清零操作
-
题目要求计数电压下降次数,这是事件(跨越瞬间),不是状态(低于阈值)
调试过程总结:
33.6分 → 修复float类型 → 65.1分
65.1分 → 尝试flag+Led_Proc计数 (AI错误建议) → 55.6分 ❌
55.6分 → 移除Seg_Proc重复计数 → 回到65.1分
65.1分 → 对比3个满分代码,发现要用新旧值比较而非标志位 → 70分 ✓
总结
最严重的错误 (会导致4T测评失败):
-
使用浮点数据类型 (33.6分) - 导致精度问题、EEPROM读写错误、运算速度慢
-
计数判断用标志位而非新旧值比较 (65.1分) - 无法准确检测电压下降事件
中等错误 (功能异常或数据不一致):
-
S12按键时序错误 - 先切换模式后处理,导致EEPROM保存失败
-
AD转换多次读取 - 导致数据不一致,显示与判断基于不同的AD值
-
重复计数逻辑 (55.6分) - 两个函数都有计数,导致重复计数
学到的经验
1. 嵌入式系统优先使用整数类型
核心原则:
能用整数就不用浮点 - 快速、精确、省内存
为什么:
-
51单片机没有FPU(浮点运算单元)
-
浮点运算是软件模拟,速度极慢(慢几十倍)
-
浮点数精度有限,可能出现
0.1 + 0.2 != 0.3的问题 -
float占4字节,unsigned char只占1字节
最佳实践:
-
使用定点数表示小数: 放大10倍、100倍、1000倍
-
例如: 2.5V → 存储为25(0.1V单位) 或 250(0.01V单位)
-
计算时先放大,最后再缩小显示
-
EEPROM读写时,字节数必须匹配数据类型大小
2. 事件检测不要用标志位,要用新旧值比较
核心思想:
不要用标志位(Flag)表示事件
直接保存旧值,比较新旧值的变化
只在状态跨越瞬间执行操作
为什么不能用标志位:
-
标志位只能表示当前状态(高/低),无法表示状态变化(刚跨越)
-
标志位的清零时机难以控制,容易重复触发或漏触发
-
跨函数传递标志位不可靠,调度器周期不同会导致错位
为什么要用新旧值比较:
-
明确检测状态变化瞬间:从一个状态到另一个状态
-
自动避免重复触发:只要旧值不更新,就不会重复检测
-
逻辑简单可靠:不需要额外的标志位管理
应用场景:
-
按键检测: 检测按下/抬起瞬间(而不是持续按下状态)
Key_Down = Key_Val & (Key_Val ^ Key_Old); // 检测按下瞬间 Key_Up = ~Key_Val & (Key_Val ^ Key_Old); // 检测抬起瞬间 Key_Old = Key_Val; // 保存当前值 -
电压阈值检测: 检测跨越阈值瞬间(而不是低于阈值状态)
if((AD_Val_Old > threshold) && (AD_Val <= threshold)) Count++; // 只在跨越瞬间计数 AD_Val_Old = AD_Val; -
频率计数: 检测上升沿/下降沿
-
状态机: 检测状态变化
标准实现模式:
// 1. 定义新旧值变量
idata unsigned int old_value = 0;
idata unsigned int current_value = 0;
// 2. 读取新值
current_value = read_sensor();
// 3. 比较新旧值,检测变化
if((old_value > threshold) && (current_value <= threshold))
{
// 检测到下降沿(从高到低跨越阈值)
count++;
}
// 4. 保存当前值作为下次的旧值
old_value = current_value;
错误示例(用标志位):
// ❌ 错误方法
if(current_value < threshold)
flag = 1; // 只要低于阈值就设置标志
else
flag = 0;
if(flag == 1)
{
count++; // 持续低于阈值期间会重复计数!
flag = 0;
}
正确示例(用新旧值比较):
// ✓ 正确方法
if((old_value > threshold) && (current_value <= threshold))
{
count++; // 只在跨越瞬间计数一次
}
old_value = current_value;
3. 状态机设计: 先判断旧状态,处理逻辑,再更新状态
错误顺序:
state = new_state; // 先改状态
if(state == old_state_value) // 判断已经失效!
process();
正确顺序:
if(state == old_state_value) // 先判断当前状态
process(); // 处理逻辑
state = new_state; // 最后更新状态
适用场景:
-
按键处理: 根据当前界面决定操作,然后切换界面
-
模式切换: 先保存当前模式的数据,再切换模式
-
状态机: 先执行当前状态的退出动作,再进入新状态
4. 数据一致性原则
核心原则:
同一个数据不要多次读取
所有计算都基于同一次读取的值
为什么:
-
传感器值在不断变化
-
多次读取可能得到不同的值
-
导致显示、判断、计算基于不同的数据
-
产生逻辑错误
最佳实践:
// ✓ 只读一次
value = read_sensor();
value_10x = value * 10;
value_100x = value * 100;
// ❌ 读多次
value_10x = read_sensor() * 10; // 第1次读取
value_100x = read_sensor() * 100; // 第2次读取 (可能不同!)
5. 调度器周期要匹配任务需求
核心原则:
不同任务有不同的执行周期
计数逻辑应该在AD采样任务中处理
不要在显示任务中处理计数逻辑
调度器周期设计:
task_t Scheduler_Task[] = {
{Led_Proc, 1, 0}, // LED控制: 1ms (快速响应)
{Key_Proc, 100, 0}, // 按键扫描: 100ms (消抖)
{Seg_Proc, 20, 0}, // 数码管刷新: 20ms (显示更新)
{AD_DA, 150, 0} // AD采样: 150ms (传感器采样)
};
为什么计数要在AD_DA中:
-
计数逻辑依赖AD采样值
-
AD_DA每150ms执行一次,每次采样后立即判断
-
如果放在20ms的Seg_Proc中,会重复判断同一个AD值
-
导致重复计数
6. 4T测评系统的严格性
特点:
-
微秒级精度检测
-
检测每一个状态变化的瞬间
-
任何瞬时错误都会被捕捉
-
不能依赖"人眼看不出来"的侥幸心理
对策:
-
状态变化必须立即反映到硬件
-
计数逻辑必须绝对准确(用新旧值比较,不用标志位)
-
EEPROM读写必须严格匹配
-
数据类型必须精确选择
复盘检查清单
在蓝桥杯单片机比赛中,遇到类似题目时,务必检查:
-
全局变量使用整数类型(unsigned char/int),不使用float
-
EEPROM读写字节数与数据类型大小严格匹配
-
校验码与数据分开存储(例如address 8和address 0)
-
按键处理: 先判断旧状态并处理,最后再更新状态
-
ADC传感器只读取一次,所有计算基于同一个值
-
计数判断不用标志位,用新旧值比较(保存AD_Val_Old)
-
计数条件: (Old > threshold) && (Current <= threshold)
-
计数逻辑放在AD采样任务中,不放在显示任务中
-
避免重复计数: 确保只在一个地方有计数逻辑
-
调度器任务周期合理分配: LED 1ms, 按键 100ms, 数码管 20ms, AD 150ms
生成时间: 2026-02-09
蓝桥杯第十一届省赛(第一套)代码错误总结