第十六届省赛错误总结
错误概述
本次练习基于蓝桥杯第十六届单片机省赛题目,涉及 DS18B20 温度测量、光敏电阻(PCF8591 ADC)光强分级、超声波测距、运动状态检测(静止/徘徊/跑动)、继电器控制、LED 指示、数码管四界面显示(环境状态/运动检测/参数设置/统计数据)、参数界面锁定、组合按键长按清零等功能。调试过程中共发现 12 个错误,涵盖继电器逻辑、按键检测、状态锁定、定时器残留值、参数生效时机、传感器采样频率等多个方面。
错误1:继电器无边沿检测 + 清除标志位
表现
继电器吸合次数(NC)暴涨。例如:期望 NC 3,实际 NC 125。
原因
// 错误代码
if((approach_flag == 1) && (temperature_flag == 1))
{
relay(1);
relay_count++; // ← 每10ms执行一次led_proc就+1,无边沿检测
approach_flag = temperature_flag = 0; // ← 清除标志位,下一周期继电器断开,等标志重新置1又吸合
}
else
{relay(0);}
两个问题叠加:
relay_count++没有边沿检测,继电器保持吸合期间每10ms就+1- 清除标志位导致继电器反复断开→吸合,进一步放大计数
修复
// 新增全局变量: idata bit relay_state;
if((approach_flag == 1) && (temperature_flag == 1))
{
if(relay_state == 0) // 边沿检测:只在 OFF→ON 时计数
{relay_count++;}
relay(1);
relay_state = 1;
}
else
{
relay(0);
relay_state = 0;
}
// 不清除 approach_flag 和 temperature_flag
教训
继电器计数必须用边沿检测(记录上一次状态,只在状态跳变时计数),不能在持续满足条件的循环中累加。标志位由产生它的模块管理,消费方不应清除。
错误2:组合按键 S8+S9 被单独按键覆盖
表现
S8+S9 同时按下时,key_read() 返回 8 而非 89,组合按键功能(长按清零)完全失效。
原因
// 错误代码
if(P32 == 0)temp = 9;
if(P33 == 0)temp = 8;
if((P32 == 0) && (P33 == 0))temp = 89;
当 S8+S9 同时按下时,三个 if 全为真,temp 依次被赋值为 9 → 8 → 89。虽然最终值是 89,但如果顺序不对(组合判断在前,单独判断在后),89 会被覆盖为 8。
修复
将组合判断放在最后(当前代码已是正确顺序),或改用 else if 确保互斥:
if((P32 == 0) && (P33 == 0))temp = 89;
else if(P32 == 0)temp = 9;
else if(P33 == 0)temp = 8;
教训
矩阵按键扫描中,组合按键判断必须优先于单独按键判断,或使用
else if保证互斥。写完后要手动代入同时按下的情况走一遍逻辑。
错误3:参数界面 LED/继电器未锁定
表现
进入参数设置界面(seg_mode==2)后,LED 和继电器状态仍在实时变化,未按题目要求锁定。
原因
led_proc() 中没有判断 seg_mode == 2,参数界面期间 L1-L4、继电器逻辑照常执行。
修复
在 led_proc() 中,L8 更新之后、L1-L4 和继电器逻辑之前,加入提前返回:
void led_proc()
{
led_disp(ucled);
switch(move_mode) // L8 始终更新
{
case 1: ucled[7] = 0; break;
case 2: ucled[7] = 1; break;
case 3: ucled[7] = led_flag; break;
}
if(seg_mode == 2) return; // 参数界面锁定,直接返回
// 以下 L1-L4 和继电器逻辑...
}
教训
题目要求"锁定状态"时,要明确锁定的范围(哪些该冻结,哪些不该)。本题 L8 不锁定(运动状态始终显示),L1-L4 和继电器锁定。
错误4:进入参数界面 para_mode 未重置
表现
从运动检测界面切到参数界面时,可能显示距离参数子界面(PL),而非题目要求的温度参数子界面(PC)。
原因
S4 按键处理中,切换 seg_mode 时没有重置 para_mode:
// 错误代码
case 4:
if(++seg_mode == 4)
{seg_mode = 0;}
break;
// 缺少: para_mode = 0;
修复
case 4:
if(seg_mode == 2)
{
temperature_10x_para = temperature_10x_para_ctrl;
distance_para = distance_para_ctrl;
}
para_mode = 0; // 每次切换界面都重置为温度子界面
if(++seg_mode == 4)
{seg_mode = 0;}
break;
教训
界面切换时,要把目标界面的所有子状态恢复到默认值。不能只切主界面,忘了子界面的初始化。
错误5:运动状态锁定期间 distance_old 处理不当
表现
3 秒锁定结束后,运动状态判断出错。例如:物体实际静止,但判定为跑动。
原因
锁定期间 distance_old 不应更新。如果更新了,锁定结束后第一次比较的基准是锁定期间最后一次的距离,而非锁定结束后的新基准。但如果完全不更新,锁定结束后第一次ΔL会用3秒前的旧距离,也会导致计算错误。
修复
锁定期间不更新 distance_old,但锁定结束时重置 catch_count = 0,让下一次采集作为新的基准值:
// get_distance() 中
if(move_flag == 1)
{
return; // 锁定期间不更新 distance_old,也不做运动状态判断
}
// Timer1Isr 中锁定到期时
if(++timer_3000ms == 3000)
{
timer_3000ms = 0;
move_flag = 0;
catch_count = 0; // 重置采集计数,下一次采集作为新基准
}
教训
状态锁定期间,要想清楚"解锁后的第一次比较基准是什么"。通过重置
catch_count让解锁后第一次读数成为新基准,避免用过期的旧值做差值计算。
错误6:长按检测逻辑全链路错误
表现
统计界面(seg_mode==3)下,S8+S9 长按 2 秒清零 relay_count 的功能完全无效。
原因
三个问题叠加:
- 组合按键 Bug(错误2) 导致
key_val永远不等于 89 key_long设置和timer_2000ms清零逻辑冲突,key_proc 每 10ms 执行时走 else 分支反复清零计时器- 释放检测条件写错(重复条件 / 应判断 89)
修复
// key_proc 中
if(seg_mode == 3)
{
if(key_old == 89)
{
key_long = 1;
}
else
{key_long = timer_2000ms = 0;}
}
if(timer_2000ms > 2000)
{
relay_count = 0;
}
教训
长按检测需要完整的链路:按键识别 → 计时启动 → 计时累加 → 阈值判断 → 动作执行。任何一环断裂都会导致功能失效。调试时要从链路头(按键返回值)开始逐环验证。
错误7:ΔL 比较频率过高(无新数据保护)
表现
运动状态判断不稳定,静止时可能被判为徘徊或跑动。
原因
get_distance() 调度周期为 150ms,但超声波数据每 1 秒才采集一次(catch_flag 由定时器 1 秒置位)。在没有新数据的调用中,distance 和 distance_old 相同,change_distance = 0,但 distance_old = distance 赋值仍在执行,导致真正有新数据时基准值已被覆盖。
修复
增加 new_data_flag 保护,仅在有新数据时才执行后续逻辑:
void get_distance()
{
unsigned char new_data_flag = 0;
if(catch_flag == 1)
{
catch_count++;
distance = us_wave_data();
catch_flag = 0;
new_data_flag = 1;
}
if(new_data_flag == 0) return; // 无新数据,直接返回
// 以下逻辑仅在有新距离数据时执行...
}
教训
当数据源(传感器)的更新频率低于处理函数的调用频率时,必须加"新数据标志"保护,防止在无新数据时重复处理旧数据导致状态污染。
错误8:timer_100ms 残留值导致 L8 闪烁异常
表现
运动状态变为"跑动"时,L8 的闪烁间隔不是 0.1 秒,而是一个随机的短间隔。
原因
进入跑动状态时,没有清零 timer_100ms 和 led_flag。timer_100ms 保留了上一次的计数值,导致第一个闪烁周期被截短。
修复
if(move_mode == 3)
{
led_shining_flag = 1;
timer_100ms = 0; // 清零计时器
led_flag = 0; // 清零闪烁标志,确保从灭开始
}
else
{led_shining_flag = timer_100ms = 0;}
教训
启动定时器驱动的周期性行为(如 LED 闪烁)时,必须同时重置计时器和状态标志,确保从一个确定的初始状态开始。
错误9:参数编辑时立即生效
表现
在参数设置界面调节温度/距离参数时,阈值立即改变,导致继电器和 LED 在调参过程中就跟着变化。
原因
S8/S9 按键直接修改 temperature_10x_para 和 distance_para(实际生效的参数),而非临时编辑变量。
修复
引入 _ctrl 后缀的临时变量,编辑时只改 _ctrl,退出参数界面时才复制到实际参数:
// 编辑时只改 _ctrl
case 8:
if((seg_mode == 2) && (para_mode == 0))
{
if((temperature_10x_para_ctrl += 10) == 810)
{temperature_10x_para_ctrl = 200;}
}
break;
// S4 退出参数界面时生效
case 4:
if(seg_mode == 2)
{
temperature_10x_para = temperature_10x_para_ctrl;
distance_para = distance_para_ctrl;
}
break;
教训
参数设置界面需要"编辑-确认"模式:编辑时改副本,确认时写入正本。这是 UI 设计的基本原则——用户应该有机会在提交前撤销修改。
错误10:approach_flag 未在新数据保护内更新
表现
approach_flag 在没有新距离数据时仍被反复更新,虽然值不变,但增加了不必要的逻辑执行。
原因
approach_flag 的更新位于 new_data_flag == 0 判断之前,导致每次 get_distance() 调用(150ms)都会重新判断接近状态。
修复
将 approach_flag 更新移到 if(new_data_flag == 0) return; 之后:
if(new_data_flag == 0) return;
if(seg_mode != 2)
{
if(distance < distance_para)
{approach_flag = 1;}
else
{approach_flag = 0;}
}
教训
标志位的更新应与其数据源的更新频率一致。距离数据 1 秒更新一次,approach_flag 也应该 1 秒判断一次。
错误11:参数界面期间 approach_flag / temperature_flag 仍在更新
表现
参数界面期间修改了距离或温度阈值后退出,继电器可能产生一次虚假的断开→吸合跳变,导致 relay_count 多计 1 次。
原因
get_distance() 和 get_temperature() 在参数界面(seg_mode==2)期间仍在用旧阈值更新 approach_flag 和 temperature_flag。退出参数界面后,新阈值生效,标志位可能与实际状态不一致,导致继电器产生一次虚假的 OFF→ON 跳变。
修复
在两个函数中加入 seg_mode != 2 判断,参数界面期间冻结标志位:
// get_distance() 中
if(seg_mode != 2)
{
if(distance < distance_para)
{approach_flag = 1;}
else
{approach_flag = 0;}
}
// get_temperature() 中
if(seg_mode != 2)
{
if(temperature_10x > temperature_10x_para)
{temperature_flag = 1;}
else
{temperature_flag = 0;}
}
教训
参数界面的"锁定"不仅是输出端(LED/继电器)的锁定,判断逻辑端(标志位)也要冻结。否则退出时新旧参数切换会导致标志位跳变。
错误12:get_temperature 调用频率过高(100ms)
表现
继电器吸合次数(NC)固定多 1。例如:期望 NC 3,实际 NC 4。
原因
get_temperature() 调度周期为 100ms,每次调用都读取 DS18B20。但 DS18B20 一次温度转换需要 750ms(12 位精度),100ms 读一次读到的是未完成转换的不稳定值。当实际温度在阈值附近时,temperature_flag 在连续读取中抖动(1→0→1),每次 0→1 跳变且 approach_flag 为 1 时,relay_count 就会多计一次。
t=0ms: temperature_flag=1 → 继电器吸合, relay_count++
t=100ms: temperature_flag=0(不稳定读数)→ 继电器断开, relay_state=0
t=200ms: temperature_flag=1 → relay_state=0 → relay_count++ ← 多计1次!
修复
将 get_temperature 调度周期从 100ms 改为 500ms,给 DS18B20 足够的转换时间:
task_t scheduler_task[] =
{
{key_proc,10,0},
{seg_proc,100,0},
{led_proc,10,0},
{ad_da,100,0},
{get_distance,150,0},
{get_temperature,500,0} // 从100改为500
};
教训
传感器的读取频率不能超过其转换频率。DS18B20 在 12 位精度下转换需要 750ms,读取间隔至少 500ms 以上。读取过快不仅浪费资源,还会得到不稳定的中间值,导致下游逻辑产生毛刺。
总结
| 序号 | 错误类型 | 严重程度 | 根因分类 |
|---|---|---|---|
| 1 | 继电器无边沿检测 + 清除标志位 | 致命 | 逻辑设计错误 |
| 2 | 组合按键被单独按键覆盖 | 致命 | 条件判断顺序 |
| 3 | 参数界面 LED/继电器未锁定 | 严重 | 需求遗漏 |
| 4 | para_mode 未重置 | 严重 | 状态初始化遗漏 |
| 5 | 锁定期间 distance_old 处理不当 | 严重 | 状态管理 |
| 6 | 长按检测全链路断裂 | 严重 | 多 Bug 叠加 |
| 7 | ΔL 比较无新数据保护 | 严重 | 采样与处理频率不匹配 |
| 8 | timer_100ms 残留值 | 中等 | 定时器初始化遗漏 |
| 9 | 参数编辑立即生效 | 严重 | 编辑-确认模式缺失 |
| 10 | approach_flag 更新位置不当 | 中等 | 逻辑位置 |
| 11 | 参数界面标志位未冻结 | 严重 | 锁定范围不完整 |
| 12 | DS18B20 读取频率过高 | 严重 | 传感器时序 |
错误规律
规律一:状态锁定不彻底(错误 3、9、11)
题目要求参数界面"锁定状态",但代码只做了部分锁定:
- 错误3:输出端(LED/继电器)未锁定
- 错误9:参数编辑直接写入生效变量,绕过了锁定
- 错误11:判断端(标志位)未冻结
这三个错误本质上是同一个问题的三个面:没有完整理解"锁定"的含义。锁定 = 输出冻结 + 判断冻结 + 编辑隔离。
自查方法:遇到"锁定/保持/不变"类需求时,画出完整的数据流(输入→判断→标志位→输出),确认每一环都被冻结。
规律二:采样频率与处理频率不匹配(错误 7、12)
- 错误7:超声波 1 秒采集一次,但处理函数 150ms 调用一次,无新数据时重复处理
- 错误12:DS18B20 转换需要 750ms,但 100ms 就读一次,读到不稳定值
自查方法:对每个传感器,确认采集周期 ≥ 转换时间,处理函数加新数据标志保护。
规律三:边沿检测缺失(错误 1)
计数类功能(继电器吸合次数)必须用边沿检测,不能在持续满足条件的循环中累加。
自查方法:凡是 count++ 出现在周期性函数中,都要检查是否有"上一次状态"的比较。
与第十五届对比
| 对比维度 | 第十五届 | 第十六届 |
|---|---|---|
| 错误数量 | 6 个 | 12 个 |
| 致命错误 | 4 个 | 2 个 |
| 主要错误类型 | 概念理解偏差(频率数据定义) | 状态管理、时序匹配、锁定机制 |
| 重复犯的错 | 整数除法(第二次) | 无 |
| 题目复杂度 | 中等(频率+DAC+时钟) | 高(多传感器+运动检测+状态机+参数锁定) |
| 新出现的错误类型 | — | 传感器时序、边沿检测、状态冻结 |
本届核心教训:多传感器+多状态的系统中,最容易出错的不是单个模块的逻辑,而是模块之间的时序配合和状态同步。写完功能代码后,要从"数据流"和"时序"两个维度做全局审查。