第十二届省赛单片机真题(第二场) — 刷题总结

蓝桥杯第十二届省赛单片机真题(第二场) — 刷题总结

一、题目概述

基于 STC15F2K60S2 单片机(12MHz),实现一个 频率/周期/电压综合测量系统,包含:

  • Timer0 计数器模式测量频率,并计算周期
  • PCF8591 双通道 AD 采集(通道1光敏 / 通道3变阻器)
  • 3个显示界面(S4切换):频率 / 周期 / 电压
  • S7 短按保存频率参数,长按切换 LED 使能
  • LED 指示灯联动显示

二、踩坑记录(重点)

踩坑1:按键长按在 ISR 中检测 Key_Up — 条件互斥,永远无法触发

错误代码:

// Timer1 ISR 中
if(Key_Old == 7)
{
    if(++ Key_Long >= 1000)
    {
        if(Key_Up == 7)       // BUG:与外层条件互斥
        {
            Led_Enable ^= 1;
            Key_Long = 0;
        }
    }
}

问题分析(3个致命错误):

编号 错误 原因
1 Key_Up 在 ISR 中无效 Key_UpKey_Proc()(主循环10ms)中的瞬态边沿值,ISR 每1ms执行,几乎不可能在 Key_Long >= 1000 时恰好捕获到
2 外层与内层条件互斥 松手时 Key_Old 变为0 → 外层 Key_Old == 7 为假;而 Key_Up == 7 恰好在松手时才为真。两个条件永远无法同时成立
3 Key_Long 未在松手时清零 短按后残留累加值,下次按下从残值继续计数,可能误触发

修复:按住计时、达阈值立即触发、松手清零

// Timer1 ISR 中
if(Key_Val == 7)
{
    Key_Long++;
    if(Key_Long == 1000)  // 用 == 防止持续触发
    {
        Led_Enable ^= 1;
    }
}
else
{
    Key_Long = 0;          // 松手或按其他键时清零
}

教训:长按检测的核心三要素 — ①检测当前按键状态(不是边沿);②计数达阈值立即触发(不要等松手);③松手时必须清零计数器。


踩坑2:长按短按未做互斥 — 长按同时触发短按动作

错误代码:

// Key_Proc() 中
switch(Key_Down)
{
    case 7:
        Freq_Para = Freq;   // S7 按下瞬间就触发
    break;
}

问题: S7 按下的瞬间 Key_Down == 7 立即成立,短按动作已经执行。之后即使触发了长按,短按动作无法撤回。

修复:S7 短按改为松手时判断,长按阈值内才执行

// Key_Proc() 中 — S7 从 Key_Down 移到 Key_Up 判断
if(Key_Up == 7 && Key_Long < 1000)
{
    Freq_Para = Freq;      // 松手时计数<1000,说明是短按
}

互斥逻辑流程:

按下S7 → ISR开始计时 Key_Long++
  ├─ <1秒松手 → Key_Up==7 且 Key_Long<1000 → 执行短按(保存频率)
  └─ ≥1秒按住 → Key_Long==1000 → 执行长按(切换LED使能)
                  → 之后松手 → Key_Long≥1000 → 短按条件不成立,不误触

教训:长按短按互斥的标准做法 — 短按动作延迟到松手(Key_Up)时执行,并判断 Key_Long < 阈值 才生效。


踩坑3:LED 使能关闭后未清零 — LED 维持最后状态不灭

错误代码:

void Led_Proc()
{
    if(Led_Enable == 1)
    {
        // 正常设置 ucLed[0]~ucLed[4]...
    }
    // Led_Enable == 0 时,什么都不做!
}

问题: Led_Enable 切换为0后,Led_Proc 跳过整个 if 块。ucLed[] 数组保留上次的值,ISR 中 Led_Disp(ucLed) 持续输出 → LED 不灭。

修复: 增加 else 分支清零:

if(Led_Enable == 1)
{
    // 正常逻辑...
}
else
{
    memset(ucLed, 0, 8);   // 关闭所有LED
}

教训:使能标志控制的外设,禁用分支必须显式清除输出状态,不能依赖"不更新就会灭"的假设。


三、按键长按模板(通用)

/* === 全局变量 === */
unsigned int Key_Long = 0;    // 长按计时器

/* === Timer ISR(1ms)中 === */
if(Key_Val == TARGET_KEY)
{
    Key_Long++;
    if(Key_Long == THRESHOLD)  // == 只触发一次
    {
        // 长按动作
    }
}
else
{
    Key_Long = 0;              // 松手清零
}

/* === Key_Proc() 中 === */
// 需要互斥的短按:用 Key_Up 而非 Key_Down
if(Key_Up == TARGET_KEY && Key_Long < THRESHOLD)
{
    // 短按动作
}

四、通用经验总结

编号 经验 适用场景
1 长按检测三要素:检测当前状态、达阈值触发、松手清零 所有长按需求
2 长按用 == 不用 >=,防止持续重复触发 ISR 中的计数触发
3 长短按互斥:短按延迟到 Key_Up 判断,条件加 Key_Long < 阈值 S7/S4 等需要区分长短按的按键
4 ISR 中不要依赖主循环的瞬态边沿变量(Key_Up/Key_Down 任何 ISR 与主循环交互的场景
5 使能标志的禁用分支必须显式清除输出 LED、DAC、蜂鸣器等外设控制