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

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

错误列表

1. :cross_mark: 使用浮点数据类型导致精度和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)   │ ← 数据有效标志
└─────────────────────────────────┘

校验机制的工作原理:

:one: 首次上电(EEPROM是空的):

EEPROM_Read(&EEPROM_Temp, 8, 1);  // 从地址8读取校验码
if (EEPROM_Temp == EEPROM_Lock)   // 比较:读到的 == 5?
{
    // ✗ 不相等(EEPROM空的,读到的是随机值)
    // 所以不执行,使用默认值
}
else
{
    // ✓ 首次运行,写入校验码
    EEPROM_Write(&EEPROM_Lock, 8, 1);  // 在地址8写入5
}

:two: 用户修改参数并保存(按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  (校验码)

:three: 重新上电(恢复保存的参数):

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;              // 恢复成功!
}

为什么要分开存储?

:bullseye: 防止误判:

  • 如果只存参数值在地址0,EEPROM空的时候地址0可能是随机值(比如碰巧是15)

  • 程序会误以为用户设置过15(1.5V),导致错误

  • 同时检查地址8的校验码,可以确认数据有效性

:bullseye: 数据校验:

  • 地址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. :cross_mark: 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. :cross_mark: 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_10xAD_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_10xAD_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. :cross_mark: 重复计数逻辑导致错误(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. :cross_mark: 计数判断不要用标志位,要用新旧电位与参数比较!(最关键的错误!)

错误代码 (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;
}

错误原因:

:cross_mark: 标志位方法的问题:

时间线分析(假设阈值=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 ❌ 继续设置标志
    ...继续重复计数

标志位的三大问题:

  1. 无法区分"刚跨越阈值"和"持续低于阈值"

    • Count_Flag = 1 只表示"当前低于阈值"

    • 无法知道上一次是什么状态

    • 导致只要低于阈值就计数

  2. 标志清零时机难以控制

    • Led_Proc清零后,AD_DA又会设置

    • 如果AD值持续低于阈值,标志会反复设置

    • 导致重复计数

  3. 跨函数传递状态不可靠

    • AD_DA设置标志 → Led_Proc读取标志

    • 调度器周期不同可能导致错位

    • 状态传递容易出错


:check_mark:正确方法:直接比较新旧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)

  • 只在跨越瞬间计数一次,避免重复计数

  • 这是嵌入式系统中事件检测的标准方法

为什么不能用标志位:

  1. 标志位只表示当前状态,无法表示状态变化

  2. 无法区分"刚跨越阈值"和"持续低于阈值"

  3. 标志清零时机难以控制,容易导致重复计数或漏计数

  4. 跨函数传递状态不可靠,调度器周期不同会导致错位

为什么要用新旧值比较:

  1. 明确检测状态变化:从"大于"变为"小于等于"

  2. 只在跨越瞬间触发一次,自动避免重复计数

  3. 逻辑简单可靠,不需要额外的标志位和清零操作

  4. 题目要求计数电压下降次数,这是事件(跨越瞬间),不是状态(低于阈值)

调试过程总结:

33.6分 → 修复float类型 → 65.1分
65.1分 → 尝试flag+Led_Proc计数 (AI错误建议) → 55.6分 ❌
55.6分 → 移除Seg_Proc重复计数 → 回到65.1分
65.1分 → 对比3个满分代码,发现要用新旧值比较而非标志位 → 70分 ✓

总结

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

  1. 使用浮点数据类型 (33.6分) - 导致精度问题、EEPROM读写错误、运算速度慢

  2. 计数判断用标志位而非新旧值比较 (65.1分) - 无法准确检测电压下降事件

中等错误 (功能异常或数据不一致):

  1. S12按键时序错误 - 先切换模式后处理,导致EEPROM保存失败

  2. AD转换多次读取 - 导致数据不一致,显示与判断基于不同的AD值

  3. 重复计数逻辑 (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
蓝桥杯第十一届省赛(第一套)代码错误总结