蓝桥杯第十三届国赛代码错误总结

蓝桥杯第十三届国赛代码错误总结

错误列表


1. :cross_mark: 继电器计数没有边沿检测,疯狂自增

错误代码:

void Led_Proc()  // 每1ms调用一次!
{
    if(Distance > Para_Dist)
    {
        Relay(1);
        Count_Relay++;   // ❌ 每1ms都+1!1秒就加了1000次!
    }
    else
        Relay(0);
}

错误原因:

Led_Proc() 被调度器以 1ms间隔调用。只要距离持续超过阈值,Count_Relay 就会每毫秒加一次,而不是在状态变化时才加一次。

题目要求统计的是继电器开关次数(状态转换次数),不是持续时间。

导致的问题:

距离持续 > 阈值 3秒钟:
  ❌ 你的代码:Count_Relay += 3000(加了3000次!)
  ✓ 正确结果:Count_Relay += 1(只在首次超过时+1)

正确代码(对照满分代码用标志位做边沿检测):

// 全局变量
idata unsigned char Relay_Old = 0;  // 继电器上一次状态

void Led_Proc()
{
    if((Distance > Para_Dist) && (Relay_Old == 0))  // 首次超过
    {
        Relay(1);
        Relay_Old = 1;       // 锁住,防止重复
        Count_Relay++;       // ✓ 只加一次
    }
    else if((Relay_Old == 1) && (Distance <= Para_Dist))  // 首次回落
    {
        Relay(0);
        Relay_Old = 0;       // 解锁
        Count_Relay++;       // ✓ 关闭也计一次
    }
}

满分代码的边沿检测:

// 满分代码用 sign6 做状态锁
if((dist_m>dist_val)&&(sign6==0))
{
    relay(1);
    sign6=1;             // 锁住
    relay_count+=1;      // 只加一次
}
else if((sign6==1)&&(dist_m<dist_val))
{
    relay(0);
    sign6=0;             // 解锁
    relay_count+=1;      // 关闭也计一次
}

关键点:

  • 开和关都要计数,不是只计"开"的次数
  • 必须用状态标志做边沿检测,只在状态变化瞬间计数
  • Relay_OldCount_Relay_Pre两个不同的变量
    • Relay_Old:记录继电器的开关状态(0或1),用于边沿检测
    • Count_Relay_Pre:记录上次写入EEPROM时的计数值(0~255),用于防止重复写EEPROM

2. :cross_mark: EEPROM 在1ms任务中频繁读写

错误代码:

void Led_Proc()  // 每1ms调用一次!
{
    if(Distance > Para_Dist)
    {
        Relay(1);
        Count_Relay++;
        EEPROM_Read(&EEPROM_Temp, 22, 1);      // ❌ 每1ms读一次EEPROM!
        if(EEPROM_Temp == EEPROM_Lock)
            EEPROM_Write(&Count_Relay, 0, 1);   // ❌ 每1ms写一次EEPROM!
    }
}

错误原因:

Led_Proc()(每1ms执行)中做 I2C 读写操作,有三重问题:

  1. 阻塞系统:I2C 读写是阻塞操作,一次 EEPROM_Write 可能耗时 5~10ms,严重拖慢调度器
  2. 磨损EEPROM:AT24C02 写入寿命约 100万次,每毫秒写一次约 16分钟写爆
  3. 完全不必要:EEPROM 只需在值发生变化时写入一次即可

正确代码(独立低频任务 + 变化检测):

// 全局变量
idata unsigned char Count_Relay_Pre = 0;

// 独立EEPROM处理函数
void EEPROM_Proc()
{
    if(Count_Relay != Count_Relay_Pre)  // ✓ 只在值变化时才写
    {
        Count_Relay_Pre = Count_Relay;
        EEPROM_Write(&Count_Relay, 0, 1);
    }
}

// 注册到调度器,500ms执行一次
idata task_t Scheduler_Task[] = {
    // ...其他任务...
    {EEPROM_Proc, 500, 0}    // ✓ 低频任务
};

满分代码的EEPROM策略:

// 满分代码:在主循环中判断,只在值变化时写
if(relay_count != relay_count_pre)
{
    eeprom_string[0] = relay_count;
    eeprom_wirte(eeprom_string, 0, 2);
}

关键点:

  • EEPROM 写入是昂贵操作(时间 + 寿命),绝不能放在高频任务中
  • 使用 Pre 变量做变化检测,只在值真正改变时才写入
  • 放到独立的低频任务(500ms一次足够)或主循环中

3. :cross_mark: 上电时未从 EEPROM 恢复继电器计数

错误代码:

void main()
{
    System_Init();
    Timer0_Init();
    Scheduler_Init();
    Timer1_Init();
    // ❌ 完全没有读取EEPROM!上电后Count_Relay永远从0开始
    while (1) { Scheduler_Run(); }
}

错误原因:

题目要求继电器开关次数掉电保存。写入EEPROM的数据如果上电后不读出来,等于白存。

为什么上电一定要读EEPROM?

题目虽然没有逐字写出"上电恢复",但让你用 EEPROM 存数据,本身就意味着要掉电保存 + 上电恢复

  • 如果只需要在运行中记录计数值,一个普通变量就够了,根本不需要 EEPROM
  • EEPROM 的唯一用途就是掉电不丢失,只写不读等于白写
  • 4T 测评流程通常是:操作触发继电器 → 断电重启 → 检查计数是否保留。如果只写不读,重启后 Count_Relay 回到 0,直接扣分
只写 EEPROM,不读 → 等于白写,数据存了但永远不用
写 + 上电读恢复   → 才是完整的掉电保存功能

正确代码(对照满分代码):

void main()
{
    System_Init();

    // ✓ 上电恢复EEPROM数据
    EEPROM_Read(&EEPROM_Temp, 22, 1);      // 读校验位
    if(EEPROM_Temp == EEPROM_Lock)          // 校验通过
    {
        EEPROM_Read(&Count_Relay, 0, 1);   // 恢复计数
        Count_Relay_Pre = Count_Relay;      // 同步Pre值
    }
    else  // 首次上电
    {
        Count_Relay = 0;
        EEPROM_Write(&EEPROM_Lock, 22, 1); // 写校验位
    }

    Timer0_Init();
    Scheduler_Init();
    Timer1_Init();
    while (1) { Scheduler_Run(); }
}

满分代码的上电恢复:

// 满分代码在 main() 最开头就恢复数据
eeprom_read(&key, 1, 1);
if(key == lock)
{
    eeprom_read(&relay_count, 0, 1);
}

关键点:

  • EEPROM 校验和恢复必须放在 main() 中、开中断之前
  • Count_Relay_Pre 也要同步赋值,防止上电后立刻触发一次无意义的EEPROM写入

电机脉冲输出(PWM)实现讲解

题目要求

  • 输出 1KHz 的 PWM 方波驱动电机
  • 当频率 > 频率参数时,占空比 80%
  • 当频率 ≤ 频率参数时,占空比 20%

核心思路

1KHz = 周期 1ms = 1000us

把 1 个 PWM 周期分成 10 等份,每份 100us。用 Timer2 产生 100us 中断,每次中断时决定输出高电平还是低电平:

80%占空比(10份中8份为高电平):
┌──────────────────────────┐    ┌──┐
│ HIGH  HIGH  HIGH  HIGH   │    │  │
│  0  1  2  3  4  5  6  7  │ 8  │9 │
└──────────────────────────┘    └──┘
         8份高电平              2份低电平

20%占空比(10份中2份为高电平):
┌──┐                            ┌──────────────────────────┐
│  │                            │ HIGH  HIGH               │
│ 0│  1  2  3  4  5  6  7      │  8  9                    │
└──┘                            └──────────────────────────┘
  反过来:8份低电平              2份高电平

需要的变量

idata unsigned char PWM_Count = 0;  // PWM计数器,0~9 循环
idata unsigned char Motor_Start = 0;                // 0=20%占空比, 1=80%占空比

Timer2 初始化(产生100us中断)

void Timer2_Init(void)    // 100微秒@12.000MHz
{
    AUXR &= 0xFB;    // 定时器时钟12T模式
    T2L = 0x9C;      // 设置定时初值
    T2H = 0xFF;      // 设置定时初值
    AUXR |= 0x10;    // 定时器2开始计时
    IE2 |= 0x04;     // 使能定时器2中断
}

计算过程:

  • 12MHz / 12T = 1MHz → 每个计数 = 1us
  • 100us 需要计数 100 次
  • 初值 = 65536 - 100 = 65436 = 0xFF9C

Timer2 中断服务函数(PWM核心逻辑)

void Timer2_Isr(void) interrupt 12
{
    if(Motor_Start == 1)       // 频率 > 参数 → 80%占空比
    {
        if(PWM_Count < 8)
            Motor(1);          // 0~7: 高电平(8份)
        else
            Motor(0);          // 8~9: 低电平(2份)
    }
    else                       // 频率 ≤ 参数 → 20%占空比
    {
        if(PWM_Count < 8)
            Motor(0);          // 0~7: 低电平(8份)
        else
            Motor(1);          // 8~9: 高电平(2份)
    }
    PWM_Count++;
    if(PWM_Count == 10)
        PWM_Count = 0;         // 10份一个周期,回到0
}

逻辑解析:

  • PWM_Count 从 0 数到 9,共 10 步,然后归零(一个完整 PWM 周期)
  • 80% 和 20% 其实是互补关系< 8 时一个输出高另一个输出低,>= 8 时反过来
  • 每 100us 进一次中断,10 次 = 1000us = 1ms = 1KHz ✓

在 Led_Proc() 中控制占空比切换

void Led_Proc()
{
    // ...继电器和LED的逻辑...

    // 电机占空比控制(一行搞定)
    Motor_Start = (Freq > Para_Freq) ? 1 : 0;
}

效果:

  • Freq > Para_FreqMotor_Start = 1 → Timer2 走 80% 分支
  • Freq <= Para_FreqMotor_Start = 0 → Timer2 走 20% 分支

在 main() 中初始化 Timer2

void main()
{
    System_Init();
    // ...EEPROM恢复...
    Timer0_Init();
    Scheduler_Init();
    Timer1_Init();
    Timer2_Init();    // ← 加上Timer2初始化
    while (1) { Scheduler_Run(); }
}

为什么不能用 Timer1?

Timer1 已经被用于 1ms 系统心跳(调度器、数码管扫描、频率测量、按键计时等全靠它)。如果把 Timer1 改成 100us 中断:

  • 所有基于 uwTick 的时间计算都要乘以 10
  • Time_1s 的 1000 要改成 10000
  • LED 闪烁的 100 要改成 1000
  • 按键长按的 1000 要改成 10000
  • 牵一发动全身,改动量巨大且容易出错

所以用独立的 Timer2 是最优解,职责分离、互不干扰


总结

错误严重度:

序号 错误 影响
1 继电器计数无边沿检测 计数值疯狂自增,EEPROM数据错误
2 EEPROM在1ms任务中频繁读写 系统阻塞 + EEPROM磨损
3 上电未读EEPROM 继电器计数掉电丢失

学到的经验

1. 继电器/LED等状态统计必须做边沿检测

标准模式:

// 用标志位记录上一次状态
idata unsigned char State_Old = 0;

// 只在状态变化时执行
if((condition) && (State_Old == 0))
{
    // 状态从 OFF → ON
    State_Old = 1;
    Count++;
}
else if((State_Old == 1) && (!condition))
{
    // 状态从 ON → OFF
    State_Old = 0;
    Count++;
}

记忆规则:

  • 需要"计次"的功能,必须做边沿检测
  • 用 Old 变量锁住状态,防止持续触发

2. EEPROM 写入四原则

原则1:只在值变化时写(用 Pre 变量对比)
原则2:必须单独注册一个低频调度任务(≥500ms间隔)
原则3:上电时读出恢复
原则4:绝对不要把 EEPROM 操作塞进其他任务里(如 Led_Proc)

为什么原则4很重要(踩坑经验):

Timer2 中断中的 Motor() 函数会对 P2 端口做读-改-写操作(temp = P2 & 0x1f),而 I2C 总线恰好用的是 P2.0(SCL)P2.1(SDA)。如果 Motor() 在 I2C 通信的 WaitAck 阶段打断进来读写 P2,会导致 SDA 被锁死在低电平,I2C 通信彻底损坏,EEPROM 读写失败。

把 EEPROM 放在独立的低频任务中,时序上恰好能错开 Motor() 的 P2 操作时刻,经 4T 测评验证有效。而如果塞进 Led_Proc 等高频任务里,时序对齐方式不同,容易撞上冲突导致 EEPROM 存储失败。

❌ 错误做法:把 EEPROM 写入塞进 Led_Proc(1ms任务)
   → 时序容易和 Timer2 的 Motor() 冲突
   → EEPROM 存储值为 0,4T 测评扣分

✓ 正确做法:EEPROM_Proc 单独注册为 500ms 调度任务
   → 时序错开,经验证无冲突
   → 不阻塞其他高频任务

标准写法:

// 上电恢复(在 main 中,开中断之前)
EEPROM_Read(&key, lock_addr, 1);
if(key == lock_value)
    EEPROM_Read(&count, data_addr, 1);

// 独立的 EEPROM 任务函数
void EEPROM_Proc()
{
    if(count != count_pre)
    {
        count_pre = count;
        EEPROM_Write(&count, data_addr, 1);
    }
}

// 注册到调度器
{EEPROM_Proc, 500, 0}    // 500ms 低频任务

3. PWM 脉冲输出标准模式

第1步:用 Timer2 产生 100us 中断(1KHz / 10步 = 100us)
第2步:PWM_Count 从 0 数到 9 循环
第3步:根据 Motor_Start 标志位选择 80% 或 20% 占空比
第4步:在 Led_Proc 中根据条件更新 Motor_Start

记忆规则:

  • 1KHz = 10 × 100us,用 Timer2 独立产生
  • 80% 和 20% 是互补的,< 8>= 8 的输出正好反过来
  • bit 类型不能加 idata/xdata/pdata 修饰符

复盘检查清单

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

继电器/EEPROM相关:

  • 继电器计数有边沿检测(Relay_Old 标志位)
  • 开和关都计数(不是只计开的次数)
  • EEPROM 在低频任务中写入(≥500ms)
  • EEPROM 只在值变化时写入(Pre 变量对比)
  • 上电时从 EEPROM 恢复数据
  • EEPROM 有校验机制(Lock值)

PWM相关:

  • Timer2 初始化在 main() 中调用
  • Timer2 中断函数定义正确(interrupt 12)
  • PWM_Count 从 0 到 9 循环(10步 × 100us = 1ms = 1KHz)
  • 占空比判断逻辑:freq > para → 80%,否则 20%
  • Motor_Start 用 bit 类型,不加 idata/xdata/pdata
  • 在 Led_Proc 中更新 Motor_Start 状态

生成时间: 2026-02-22
蓝桥杯第十三届国赛代码错误总结

1 个赞