第十六届省赛错误总结

第十六届省赛错误总结

错误概述

本次练习基于蓝桥杯第十六届单片机省赛题目,涉及 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);}

两个问题叠加:

  1. relay_count++ 没有边沿检测,继电器保持吸合期间每10ms就+1
  2. 清除标志位导致继电器反复断开→吸合,进一步放大计数

修复

// 新增全局变量: 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 的功能完全无效。

原因

三个问题叠加:

  1. 组合按键 Bug(错误2) 导致 key_val 永远不等于 89
  2. key_long 设置和 timer_2000ms 清零逻辑冲突,key_proc 每 10ms 执行时走 else 分支反复清零计时器
  3. 释放检测条件写错(重复条件 / 应判断 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 秒置位)。在没有新数据的调用中,distancedistance_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_100msled_flagtimer_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_paradistance_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_flagtemperature_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+时钟) 高(多传感器+运动检测+状态机+参数锁定)
新出现的错误类型 传感器时序、边沿检测、状态冻结

本届核心教训:多传感器+多状态的系统中,最容易出错的不是单个模块的逻辑,而是模块之间的时序配合和状态同步。写完功能代码后,要从"数据流"和"时序"两个维度做全局审查。