蓝桥杯第十三届国赛代码错误总结
错误列表
1.
继电器计数没有边沿检测,疯狂自增
错误代码:
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_Old和Count_Relay_Pre是两个不同的变量:Relay_Old:记录继电器的开关状态(0或1),用于边沿检测Count_Relay_Pre:记录上次写入EEPROM时的计数值(0~255),用于防止重复写EEPROM
2.
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 读写操作,有三重问题:
- 阻塞系统:I2C 读写是阻塞操作,一次 EEPROM_Write 可能耗时 5~10ms,严重拖慢调度器
- 磨损EEPROM:AT24C02 写入寿命约 100万次,每毫秒写一次约 16分钟写爆
- 完全不必要: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.
上电时未从 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_Freq→Motor_Start = 1→ Timer2 走 80% 分支Freq <= Para_Freq→Motor_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
蓝桥杯第十三届国赛代码错误总结