蓝桥杯第十二届省赛(第二次)代码错误总结

蓝桥杯第十二届省赛(第二次)代码错误总结

错误列表

1. :cross_mark: PCF8591 AD采样没有读取两次导致数据不准确

错误代码:

void AD_DA()
{
    // ❌ 每个通道只读一次!
    AD_1_Data= (float)Ad_Read(0x41) / 51.0f ;
    AD_1_Data_100x=AD_1_Data*100;
​
    AD_3_Data= (float)Ad_Read(0x43) / 51.0f ;
    AD_3_Data_100x=AD_3_Data*100;
​
    Voltage_100x=(Output_Mode==1)?AD_1_Data_100x:AD_3_Data_100x;
}

错误原因:

PCF8591的工作特性:

  • 第一次读取返回的是上一次AD转换的结果(可能是旧数据或初始值)

  • 第二次读取才是本次转换的真实值

  • 如果只读一次,可能读到的是上一次的旧值

导致的问题:

  • 数据准确性下降,特别是低电压值时误差更明显

  • 第一次采样可能读到默认值0或上次的残留值

  • 导致显示不准确或显示0.00

4T测评失败点:

序号9:通道1,电压值1.3V,切换电压显示界面
- 正确结果:U-1 1.30
- 你的结果:数码管显示:U-1 0.00 ❌
​
序号10:通道1,电压值4.5V
- 正确结果:U-1 4.50
- 你的结果:数码管显示:U-1 0.00 ❌
​
序号11:通道1,电压值0.4V
- 正确结果:U-1 0.40
- 你的结果:数码管显示:U-1 0.00 ❌
​
序号12:通道3,电压值3.5V,切换通道3
- 正确结果:U-3 3.50
- 你的结果:数码管显示:U-3 0.39 ❌
​
序号13:通道3,电压值1.6V
- 正确结果:U-3 1.60
- 你的结果:数码管显示:U-3 0.39 ❌
​
序号14:通道3,电压值0.7V
- 正确结果:U-3 0.70
- 你的结果:数码管显示:U-3 0.39 ❌

正确代码(读两次 - 满分代码标准做法):

void AD_DA()
{
    // 通道1(光敏):读两次
    Ad_Read(0x41);  // ✓ 第一次:清空上次结果
    AD_1_Data= (float)Ad_Read(0x41) / 51.0f ;  // ✓ 第二次:获取真实值
    AD_1_Data_100x=AD_1_Data*100;
​
    // 通道3(RB2):读两次
    Ad_Read(0x43);  // ✓ 第一次:清空上次结果
    AD_3_Data= (float)Ad_Read(0x43) / 51.0f ;  // ✓ 第二次:获取真实值
    AD_3_Data_100x=AD_3_Data*100;
​
    Voltage_100x=(Output_Mode==1)?AD_1_Data_100x:AD_3_Data_100x;
}

满分代码都采用读两次的方式:

// 学员Ꮶ的代码
ad_read(0x43);  // 先读一次(清空上次结果)
voltage3 = ad_read(0x43) * 100 / 51;  // 再读一次(获取本次结果)
​
ad_read(0x41);
voltage1 = ad_read(0x41) * 100 / 51;
// 学员cindyyy的代码(使用0x03直接读通道3)
AD_Channel3_RB2_100x = Ad_Read(0x03) * 100 / 51;
AD_Channel1_Light_100x = Ad_Read(0x01) * 100/51;
// 注:有些IIC库直接读通道号,但原理相同

关键点:

  • 每个通道必须读两次:第一次清空旧值,第二次获取真实值

  • PCF8591是单通道依次转换的,第一次读取可能是上次通道的残留值

  • 特别是低电压值(<1V)时,读两次的必要性更明显

  • 这是所有满分代码的共同特征

为什么只读一次会出错:

假设依次读取通道1和通道3:
第1次调用Ad_Read(0x41):
  → 启动通道1转换
  → 但返回的是上一次(可能是通道3)的值
​
第1次调用Ad_Read(0x43):
  → 启动通道3转换
  → 但返回的是上一次(通道1)的值
​
结果:
  AD_1_Data = 上次通道3的值(错误!)
  AD_3_Data = 上次通道1的值(错误!)

读两次的正确流程:

第1次调用Ad_Read(0x41):
  → 启动通道1转换,返回旧值(丢弃)
​
第2次调用Ad_Read(0x41):
  → 再次读取,返回通道1的真实值 ✓
​
第1次调用Ad_Read(0x43):
  → 启动通道3转换,返回旧值(丢弃)
​
第2次调用Ad_Read(0x43):
  → 再次读取,返回通道3的真实值 ✓

2. :cross_mark: 长按按键逻辑使用Key_Old容易出错

错误代码:

// 变量定义
idata unsigned int Led_Flag=1;          // LED使能标志
idata unsigned int Led_Old_Flag=0;      // ❌ 额外的标志变量
idata unsigned int Key7_Flag=0;
idata unsigned int Count_1000ms=0;
​
// 按键处理
if(Count_1000ms<1000)
{
    if(Key_Up==7)
    {
        Key7_Flag=Count_1000ms=0;
        Freq_Store=Freq;
    }
}
else
{
    if((Key_Old==7)&&(Led_Old_Flag!=Led_Flag))  // ❌ 使用Key_Old判断!
        Led_Flag^=1;
    if(Key_Up==7)
        Key7_Flag=Count_1000ms=0;
}

错误原因:

使用Key_Old判断按键保持状态的问题:

  • Key_Old 表示按键保持按下的状态

  • 在按键保持期间,Key_Old==7 一直为真

  • Key_Proc() 每10ms执行一次

  • 容易导致重复执行或需要额外的标志变量(如Led_Old_Flag)来防止重复

  • 逻辑复杂,容易出错

额外标志变量的问题:

  • Led_Old_Flag 需要手动更新和维护

  • 如果忘记更新,逻辑就会出错

  • 增加了代码复杂度

4T测评可能失败点:

虽然最终修改了这个逻辑,但使用Key_Old的方式容易导致:
- 长按功能重复触发
- 需要额外的防重复标志
- 逻辑混乱,不易维护

正确代码(使用Key_Down和Key_Up):

// 变量定义
idata unsigned int Led_Flag=1;
idata unsigned int Key7_Flag=0;
idata unsigned int Count_1000ms=0;
// ✓ 不需要Led_Old_Flag这个多余的变量!
​
// 按键处理
if(Key_Down==7)
    Key7_Flag=1;  // ✓ 按下时开始监测
​
if(Key_Up==7)  // ⭐ 关键:在松开时判断!
{
    if(Count_1000ms>=1000)  // 如果超过1秒
        Led_Flag^=1;        // 长按:切换LED使能
    else
        Freq_Store=Freq;    // 短按:缓存频率
​
    Key7_Flag=Count_1000ms=0;  // 清零标志和计数器
}
​
// 中断中计时
if(Key7_Flag==1)
{
    Count_1000ms++;
    if(Count_1000ms>=1000)
        Count_1000ms=1000;
}

使用Key_Down和Key_Up的优势:

特性 使用Key_Old 使用Key_Down和Key_Up
触发时机 按键保持期间一直为真 按下/松开瞬间触发一次
重复执行 容易重复执行 不会重复执行 ✓
额外标志 需要防重复标志 不需要额外标志 ✓
逻辑清晰度 较复杂 简洁清晰 ✓
符合习惯 在保持时判断 在松开时判断 ✓

满分代码的统一模式:

┌─────────────┐
│ 按键按下(S7) │
└──────┬──────┘
       │
       ▼
┌─────────────────┐
│ Key_Down==7触发  │ ← 只触发一次,开始计时
└──────┬──────────┘
       │
       ▼
┌─────────────────┐
│ 中断中持续计时   │ ← Timer1_Isr()中每1ms累加
└──────┬──────────┘
       │
       ▼
┌─────────────────┐
│ Key_Up==7触发    │ ← 只触发一次,判断时长
└──────┬──────────┘
       │
       ▼
  ┌────┴────┐
  │ 判断时长 │
  └────┬────┘
       │
   ┌───┴───┐
   │       │
   ▼       ▼
 <1秒    ≥1秒
短按功能  长按功能

所有满分代码都使用Key_Down和Key_Up:

// 学员Ꮶ的代码
if(key_down == 7)
    key_long_flag = 1;
​
if(key_up == 7)
{
    if(Time_count<1000)
        freq_save = freq;    // 短按
    else
        led_flag ^= 1;       // 长按
    Time_count = 0;
    key_long_flag = 0;
}
// 学员cindyyy的代码
case 7:
    Long_Press_Detection = 1;  // Key_Down时设置标志
break;
​
if(Key_Up == 7)
{
    Long_Press_Detection = 0;
    if(Time_Tick >= 1000)
        Led_Show_Flag ^= 1;    // 长按
    else
        Freq_Cache = Freq;     // 短按
}
// 学员emo哥的代码
if(key_down==7)
    ms_key_7=0;  // 按下时重置计时
​
if((key_up==7)&&(ms_key_7>1000))
    led_on ^=1;  // 长按
else if((key_up==7)&&(ms_key_7<1000))
    freq_cun=freq_sure;  // 短按

关键点:

  • 永远使用Key_Down检测按下,Key_Up检测松开

  • 不要使用Key_Old做长按判断

  • Key_Down和Key_Up只在状态变化时触发一次,不会重复

  • 逻辑清晰:按下→计时→松开判断

  • 不需要额外的防重复标志变量

  • 符合用户使用习惯(松开时生效)

为什么不用Key_Old:

Key_Old == 7 的问题:
  按下S7后,Key_Old一直等于7
  Key_Proc()每10ms执行一次
​
  如果在超过1秒后判断:
    第1次:Key_Old==7为真 → 执行功能
    第2次:Key_Old==7还是真 → 又执行一次!
    第3次:Key_Old==7还是真 → 又执行一次!
    ...
​
  必须用额外的标志防止重复,增加复杂度
Key_Up == 7 的优势:
  只在松开瞬间触发一次
  不会重复执行
  不需要防重复标志
  逻辑简洁清晰

总结

最严重的错误(会导致4T测评失败):

  1. PCF8591 AD采样没有读取两次 - 数据不准确,特别是低电压值,序号9-14所有电压测试失败

中等错误(逻辑混乱容易出错):

  1. 长按按键逻辑使用Key_Old - 容易重复触发,需要额外标志,逻辑复杂

学到的经验

1. PCF8591必须读两次才能获取准确数据

核心原理:

PCF8591工作机制:
  第1次读取 → 返回上一次转换的结果(旧值)
  第2次读取 → 返回本次转换的真实值

标准写法:

// 每个通道读两次
Ad_Read(0x41);  // 第一次:清空旧值
AD_1_Data = Ad_Read(0x41) / 51.0f;  // 第二次:获取真实值
​
Ad_Read(0x43);
AD_3_Data = Ad_Read(0x43) / 51.0f;

优势:

  • 确保数据准确性,特别是低电压值

  • 避免读到上一次转换的残留值

  • 所有满分代码都采用这种方式

  • 符合PCF8591的工作特性

记忆技巧:

  • 第一次是"热身",第二次才是"真实数据"

  • 第一次"清场",第二次"采样"

2. 长按按键永远使用Key_Down和Key_Up

标准模式(所有满分代码一致):

// 1. 按下时:设置标志开始计时
if(Key_Down==7)
    Key7_Flag=1;
​
// 2. 松开时:判断时长并执行功能
if(Key_Up==7)
{
    if(Count_1000ms>=1000)
        // 长按功能
    else
        // 短按功能
​
    Key7_Flag=Count_1000ms=0;  // 清零
}
​
// 3. 中断中:持续计时
if(Key7_Flag==1)
{
    Count_1000ms++;
    if(Count_1000ms>=1000)
        Count_1000ms=1000;  // 限制最大值
}

为什么不用Key_Old:

  • Key_Old==7 在按键保持期间一直为真

  • 会导致重复执行,需要额外的防重复标志

  • 逻辑复杂,容易出错

使用Key_Down和Key_Up的好处:

  • :white_check_mark: 只在按下/松开瞬间触发一次

  • :white_check_mark: 不会重复执行

  • :white_check_mark: 不需要额外标志变量

  • :white_check_mark: 逻辑清晰简洁

  • :white_check_mark: 符合用户使用习惯(松开时生效)

记忆规则:

  • 按键检测只用Key_Down和Key_Up

  • Key_Old只用于计算Key_Down和Key_Up,不要在业务逻辑中使用


复盘检查清单

在蓝桥杯单片机比赛中,遇到类似题目时,务必检查:

AD采样相关:

  • PCF8591每个通道读两次(第一次清空,第二次使用)

  • 通道号正确(0x41→通道1,0x43→通道3)

  • 变量名和通道号一一对应(AD_1_Data对应0x41)

  • 低电压值时特别注意读两次的必要性

按键长按/短按相关:

  • 永远使用Key_Down检测按下

  • 永远使用Key_Up检测松开

  • 不要用Key_Old做长按判断

  • 在Key_Up(松开)时判断时长

  • 逻辑清晰:按下→计时→松开判断

  • 不需要额外的防重复标志变量

代码质量相关:

  • 删除未使用的变量

  • 整数运算优于浮点运算

  • 变量命名清晰明确

  • 职责分离,逻辑清晰


对比满分代码的差距

满分代码的共同特征:

  1. AD采样读两次
// 学员Ꮶ
ad_read(0x43);
voltage3 = ad_read(0x43) * 100 / 51;
​
ad_read(0x41);
voltage1 = ad_read(0x41) * 100 / 51;
  1. 长按按键使用Key_Down和Key_Up
// 学员Ꮶ
if(key_down == 7)
    key_long_flag = 1;
​
if(key_up == 7)
{
    if(Time_count>=1000)
        led_flag ^= 1;      // 长按
    else
        freq_save = freq;    // 短按
    Time_count = 0;
    key_long_flag = 0;
}
voltage1  // 对应通道1
voltage3  // 对应通道3

生成时间: 2026-02-08
蓝桥杯第十二届省赛(第二次)代码错误总结