蓝桥杯第十三届国赛 —— 代码错误总结与修复记录
题目:第十三届蓝桥杯单片机组国赛程序设计
平台:STC15F2K60S2 (CT107D开发板)
功能模块:频率测量(P3.4计数) + 超声波测距 + 湿度ADC/DAC + PWM电机 + 继电器EEPROM计数 + 参数设置 + LED指示
排查轮次:共3轮,第一轮代码审计查错12处,第二轮4T测试查错3处,第三轮满分代码对比查错6处
参考满分代码:2份(sprintf格式化方案 + 手动数码管方案)
错误总览
| 序号 | 严重度 | 错误类型 | 位置 | 简述 |
|---|---|---|---|---|
| 1 | 致命 | 自增遗漏 | Timer1 ISR | time_1s_freq 永不自增,频率始终为0 |
| 2 | 致命 | 自增遗漏 | Timer2 ISR | pwm_period 永不自增,PWM失效 |
| 3 | 致命 | 赋值/比较混淆 | key_proc 参数页 | 6处 = 写成 ==,参数调节全部失效 |
| 4 | 致命 | switch case编号错误 | key_proc + seg_proc | case 0/2/3 应为 0/1/2 |
| 5 | 严重 | PWM周期+占空比双重错误 | Timer2 ISR | %11(909Hz) + 占空比反转 |
| 6 | 严重 | 继电器计数无边沿检测 | led_proc | 只计"开"不计"关" |
| 7 | 严重 | EEPROM上电未恢复 | main() | 掉电重启后计数归零 |
| 8 | 严重 | EEPROM写入位置不当 | led_proc | 在1ms高频任务中写EEPROM,P2端口冲突 |
| 9 | 中等 | 数组越界 | led_proc | ucLed[seg_show_mode-1] 当 mode=0 时为 ucLed[-1] |
| 10 | 中等 | LED页面指示缺失 | led_proc | 非参数页L1-L3全灭,应指示当前页 |
| 11 | 中等 | 湿度比较多乘10 | led_proc | humidity_val > para*10 条件永远为假 |
| 12 | 中等 | kHz前导零 | seg_proc | 675Hz显示"00.6"而非"0.6" |
| 13 | 中等 | 距离m模式小数点错位 | seg_proc | 150cm显示"15.0"m 应为"1.50"m |
| 14 | 中等 | bit类型加idata修饰 |
全局变量 | Keil C51中bit不应有内存修饰符 |
| 15 | 轻微 | 超声波0值未过滤 | get_diastance | 无回波时0值污染滑动平均滤波器 |
| 16 | 轻微 | LED闪烁计时器逻辑 | Timer1 ISR | 闪烁标志未翻转,计时器未归零 |
| 17 | 轻微 | 湿度显示缺百位 | seg_proc | 湿度=100时显示"00" |
| 18 | 轻微 | 长按清零未同步Pre | key_proc | eeprom_proc检测到变化重复写入 |
第一层:底层驱动错误
1. 按键驱动 key.c —— 缺少串口冲突检测与P3.4状态恢复
原始代码问题:
-
无串口冲突检测:J9开关在IAP15位置时,IAP15芯片通过P3.0发送串口数据,导致按键误触(不要检测)
-
无P3.4状态恢复:(按键扫描后P3口状态改变,干扰T0计数器频率测量
根因分析
4T测试平台通过 UART串口 与你的板子通信(设置频率、读取结果)。UART的RX线就是 P30!
而本小姐之前在 key.c 里加的这行代码:
if (P30 == 0) return 0; // ← 这行!!!
4T平台的串口TX → 你的板子P30(RXD),串口数据不断发送,P30 频繁出现低电平 → 每次按键扫描都被拦截 →
S7永远检测不到 → kHz模式永远进不去!
Hz模式(test 6)正确是因为它不需要按S7(已经在Hz模式了)。
解决方案:删掉串口冲突检测! 4T测试时不存在IAP15干扰,这个检测反而害了你
修复后的key.c:
unsigned char Key_Read() {
unsigned char temp = 0;
P44 = 1; P42 = 1; P35 = 1; // 复位所有行线
if (P30 == 0) return 0; // 串口冲突检测
P44 = 0; P42 = 1; P35 = 1; // 驱动第一行
if (P33 == 0) temp = 4;
if (P32 == 0) temp = 5;
if (P31 == 0) temp = 6;
if (P30 == 0) temp = 7;
P44 = 1; P42 = 1; P35 = 1; // 复位
P3 = P3 | 0xef; // 恢复P3.4频率计数输入
return temp;
}
记忆要点:
-
同时使用按键+串口 → 扫描前检测P3.0
-
同时使用按键+频率测量 → 扫描后恢复P3状态
P3 = P3 | 0xef
第二层:中断与定时器错误
2. Timer1 ISR —— ++ 遗漏(致命!)
// ❌ 错误:永不自增
if (time_1s_freq == 1000) { ... }
// ✓ 正确
if (++time_1s_freq == 1000) { ... }
同类错误: Timer2 ISR 的 pwm_period 也遗漏了 ++
教训: 凡是用变量做定时计数的
if(xxx == N),必须确认有++或在其他位置自增
3. PWM 周期与占空比双重错误
// ❌ 原始代码(两处错误)
pwm_period = (++pwm_period) % 11; // 11步×100us=1.1ms=909Hz
MOTOR(pwm_period < 10 - pwm_compare); // 占空比反转
// ✓ 修复
pwm_period = (++pwm_period) % 10; // 10步×100us=1ms=1KHz
MOTOR(pwm_period < pwm_compare); // 占空比正确
| 条件 | pwm_compare | 修复前(10-N) | 修复后(N) | 要求 |
|---|---|---|---|---|
| freq > 参数 | 8 | 18% ✗ | 80% ✓ | 80% |
| freq ≤ 参数 | 2 | 73% ✗ | 20% ✓ | 20% |
注意: 满分代码2用
Motor(Pwm_Period < 10 - Pwm_Compare)但占空比实际是反的(可能4T未严格检测)。满分代码1用if(motor_compare<8) motor(1)逻辑明确正确。
4. LED闪烁定时器 —— 计时器未归零、标志未翻转
// ❌ 原始(只有空case,什么都没做)
case 0:
break;
// ✓ 修复
case 0:
if (++time_100ms_freq >= 100)
{
time_100ms_freq = 0;
led_freq_flight ^= 1; // 翻转闪烁标志
time_100ms_humidity = time_100ms_diatance = 0; // 清其他计时器
led_humidity_flight = led_diatance_flight = 0; // 灭其他LED
}
break;
第三层:主程序逻辑错误
5. 参数调节 —— = 与 == 混淆(6处!)
// ❌ 错误:= 是赋值,永远为真!
freq_para_kHZ_10x_ctrl = (freq_para_kHZ_10x_ctrl = 120) ? 10 : ...
// ✓ 正确:== 是比较
freq_para_kHZ_10x_ctrl = (freq_para_kHZ_10x_ctrl == 120) ? 10 : ...
错误代码总共6处(频率/湿度/距离 × 加/减),导致所有参数调节异常。
教训: 三元运算符
(condition) ? a : b的 condition 中,用==不是=
6. switch case 编号错误
// ❌ 错误:para_show_mode 循环 0→1→2,但case写了0/2/3
switch(para_show_mode) {
case 0: ... break;
case 2: ... break; // 应该是 case 1
case 3: ... break; // 应该是 case 2
}
教训: switch 的 case 值必须与实际变量范围一致
7. 继电器计数 —— 只计"开"不计"关"
// ❌ 错误:只在 ON 时计数
if (relay_work != relay_old)
{
if (relay_work == 1) { // 只有开才计数
relay_num++;
}
Relay(relay_work);
relay_old = relay_work;
}
// ✓ 正确:开和关都计数(满分代码1和2均如此)
if (relay_work != relay_old)
{
relay_num++; // 任何状态变化都+1
Relay(relay_work);
relay_old = relay_work;
}
记忆规则: “开关次数” = 状态变化次数,开+1,关也+1
8. EEPROM 三大问题
8.1 上电未恢复(致命)
// ❌ 错误:main() 里完全没有读EEPROM
void main() {
System_Init();
// ← 掉电后 relay_num 从0开始,之前存的数据白费
...
}
// ✓ 正确:上电校验+恢复
void main() {
unsigned char eeprom_temp;
System_Init();
EEPROM_Read(&eeprom_temp, 22, 1); // 读校验位
if (eeprom_temp == eeprom_lock) { // 校验通过
EEPROM_Read(&relay_num, 0, 1); // 恢复计数
relay_num_pre = relay_num; // 同步Pre
} else { // 首次上电
relay_num = 0;
EEPROM_Write(&eeprom_lock, 22, 1); // 写校验位
}
...
}
8.2 写入放在高频任务中(P2冲突风险
// ❌ 错误:在 led_proc(1ms调度)中直接写EEPROM
// Timer2的MOTOR()会打断I2C通信,破坏P2.0(SCL)/P2.1(SDA)
// ✓ 正确:独立500ms低频任务 + 变化检测
void eeprom_proc() {
if (relay_num != relay_num_pre) {
relay_num_pre = relay_num;
EEPROM_Write(&relay_num, 0, 1);
}
}
// 调度器注册:{eeprom_proc, 500, 0}
8.3 长按清零未同步Pre变量
// ❌ 错误:清零后 eeprom_proc 检测到变化重复写入
relay_num = 0;
EEPROM_Write(&relay_num, 0, 1);
// ✓ 正确:同步Pre
relay_num = 0;
relay_num_pre = 0; // ← 同步!
EEPROM_Write(&relay_num, 0, 1);
EEPROM四原则:
只在值变化时写(Pre变量对比)
独立低频任务(≥500ms)
上电时读出恢复
不塞进高频任务(避免P2冲突)
第三点五层:I2C与Timer2端口冲突(DAC/EEPROM不工作的根因)
19. Timer2 MOTOR() 中断破坏 I2C 总线
问题本质:
I2C总线使用 P2.0(SCL) 和 P2.1(SDA),而 MOTOR()/Relay()/Beep() 都对 P2 做读-改-写操作:
// led.c 中的 MOTOR() 函数
temp = P2 & 0x1f; // 读P2 → 包含 SCL/SDA 的当前电平!
temp = temp | 0xa0; // 设138地址
P2 = temp; // 写回P2 → 覆盖 SCL/SDA 锁存器!
破坏时序:
Da_Write 发送数据中:
Master 释放 SDA=1 (等ACK)
Slave 拉低 SDA=0 (ACK)
│
├── ⚡ Timer2 中断!MOTOR() 读到 P2.1=0
│ → 写回 P2.1=0 到锁存器
│ → Master 现在主动驱动 SDA=0
│
Slave 释放 SDA,但 Master 锁住 SDA=0
→ 后续所有 I2C 数据全错!
修复方法:所有 I2C 操作前后关闭/恢复 Timer2 中断
void ad_da()
{
unsigned char temp_ad;
unsigned char da_out;
IE2 &= ~0x04; // 关Timer2中断
temp_ad = Ad_Read(0x43);
IE2 |= 0x04; // 开Timer2中断
// ... 计算(float运算不涉及I2C,无需保护)...
IE2 &= ~0x04; // 关Timer2中断
Da_Write(da_out);
IE2 |= 0x04; // 开Timer2中断
}
关键规则: 凡是用到 I2C(PCF8591 ADC/DAC、AT24C02 EEPROM)的地方,都要用
IE2 &= ~0x04/IE2 |= 0x04包裹!
不要把 float 计算包在里面(会让电机停顿太久),只包裹实际的 I2C 函数调用。
第四层:显示与状态错误
9. LED 数组越界 ucLed[-1]
// ❌ 错误:seg_show_mode=0 时 ucLed[0-1] = ucLed[-1]
ucLed[seg_show_mode - 1] = 1;
// ✓ 正确:
ucLed[0] = (seg_show_mode == 0);
ucLed[1] = (seg_show_mode == 1);
ucLed[2] = (seg_show_mode == 2);
10. 湿度LED判断多乘10
// ❌ 错误:humidity_val范围0-100,para范围10-60,para*10最小100,永远为假
ucLed[4] = (humidity_val > humidity_para_10x * 10);
// ✓ 正确:
ucLed[4] = (humidity_val > humidity_para_10x);
11. kHz显示前导零
// ❌ 错误:675Hz显示 "F 00.6"
seg_buf[5] = freq_val / 10000; // 0,不是空白
// ✓ 正确:零消隐
seg_buf[5] = (freq_val / 10000 == 0) ? 10 : freq_val / 10000;
12. 距离m模式小数点错位
// ❌ 错误:150cm 显示 "15.0" (15.0m,荒谬)
seg_buf[5] = diastance_val / 100 % 10;
seg_buf[6] = diastance_val / 10 % 10 + ','; // 小数点在十位后
// ✓ 正确:150cm 显示 "1.50" (1.50m)
seg_buf[5] = diastance_val / 100 % 10 + ','; // 小数点在百位后
seg_buf[6] = diastance_val / 10 % 10;
seg_buf[7] = diastance_val % 10;
13. 超声波0值未过滤
// ❌ 错误:超时返回0会污染滑动平均滤波器
diastance_val = Moving_Average_Filter(temp_distance);
// ✓ 正确(满分代码2做法):
if (temp_distance != 0)
diastance_val = Moving_Average_Filter(temp_distance);
14. bit 类型加 idata 修饰符
// ❌ 不规范:bit只能在位寻址区(0x20-0x2F),不能加内存修饰符
idata bit freq_show_mode = 0;
// ✓ 正确:
bit freq_show_mode = 0;
注意: 满分代码2也用了
idata bit并通过了4T。多数Keil版本会忽略该修饰符。但为规范性建议去除。
满分代码对比——关键设计模式
1. 调度器任务分配
| 任务 | 满分1 | 满分2 | 推荐 |
|---|---|---|---|
| LED/继电器 | 主循环 | 1ms | 1ms |
| 按键 | 10ms | 10ms | 10ms |
| 数码管数据 | 30ms | 180ms | 100ms |
| ADC/DAC | 160ms | 160ms | 160ms |
| 超声波 | 100ms | 100ms | 100ms |
| EEPROM | 主循环(变化检测) | 1ms(内嵌) | 500ms(独立) |
2. 满分代码1的sprintf方案 vs 满分代码2的手动数码管方案
| 方面 | sprintf方案 | 手动方案 |
|---|---|---|
| 代码量 | 少(一行格式化) | 多(逐位计算) |
| 可读性 | 高 | 中 |
| ROM占用 | 大(链接stdio库) | 小 |
| 出错风险 | 低 | 高(小数点位置、零消隐) |
| 竞赛推荐 | ✓(快速开发) | 需要更细心 |
3. 长按检测标准模式
// key_proc 中:
if (key_down == 7) long_press_flag = 1; // 按下开始计时
if (key_up == 7) {
if (time_1s >= 1000) { /* 长按动作 */ }
long_press_flag = 0; // 释放停止计时
}
// Timer1 ISR 中:
if (long_press_flag) {
if (++time_1s >= 1000) time_1s = 1001; // 计时(上限防溢出)
} else
time_1s = 0; // 不计时时始终归零
4. PWM标准实现(1KHz 80%/20%)
// Timer2 ISR (100us中断)
void Timer2Isr(void) interrupt 12 {
pwm_period = (++pwm_period) % 10; // 10步 = 1ms = 1KHz
MOTOR(pwm_period < pwm_compare); // compare=8→80%, compare=2→20%
}
// 在频率采样处切换占空比
if (freq_val > freq_para_kHZ_10x * 100)
pwm_compare = 8; // 超过参数 → 80%
else
pwm_compare = 2; // 未超过 → 20%
复盘检查清单
编码阶段
-
所有定时计数的
if(var == N)确认有++var -
所有三元
(cond) ? a : b确认 cond 中是==不是= -
switch case 编号与变量实际范围一致
-
bit类型不加idata/xdata/pdata修饰符 -
数组下标不会出现负数或越界
-
小数点加在正确的 seg_buf 位置
-
零消隐逻辑覆盖所有需要的数位
继电器/EEPROM相关
-
继电器计数有边沿检测(Old标志位)
-
开和关都计数
-
EEPROM独立低频任务(≥500ms)
-
EEPROM只在值变化时写入(Pre变量对比)
-
上电时从EEPROM恢复数据(校验机制)
-
长按清零时同步Pre变量
PWM相关
-
Timer2 初始化在 main() 中调用
-
%10不是%11(10步×100us=1KHz) -
占空比方向正确(compare=8 → 80%)
-
Motor_Start / pwm_compare 在正确位置更新
端口复用
-
使用按键+串口 → 不要在Key_Read中加P3.0检测(4T平台串口通信会阻断S7按键!)
-
使用按键+频率测量 → key.c 加 P3 状态恢复
-
超声波滤波加
!= 0无效值过滤
I2C与Timer2冲突(最易漏检!)
-
所有 Ad_Read / Da_Write 调用前
IE2 &= ~0x04,调用后IE2 |= 0x04 -
所有 EEPROM_Write / EEPROM_Read 调用前后同理
-
float计算放在 IE2 保护范围之外(避免电机停顿过久)
-
如果DAC输出不正确,第一件事检查Timer2是否干扰了I2C