蓝桥杯第十四届省赛 —— 代码错误总结与修复记录
题目:第十四届蓝桥杯单片机组省赛程序设计
平台:STC15F2K60S2 (CT107D开发板)
功能模块:DS1302时钟 + DS18B20温度 + NE555频率测湿度 + PCF8591光敏触发采集 + 参数设置 + LED指示
排查轮次:共多轮,从初始代码到最终对齐79分参考代码,分数经历了先升后降再恢复的过程
关联笔记:蓝桥杯第十四届省赛79分参考代码分析
错误总览
| 编号 | 严重度 | 错误类型 | 分类 | 影响 |
|---|---|---|---|---|
| BUG-1 | 致命 | Timer1模式配置错误 | 底层定时器 | 定时不准,所有时间相关功能异常 |
| BUG-2 | 致命 | idata栈溢出 | 内存分配 | 程序崩溃或变量覆盖 |
| BUG-3 | 严重 | 过度工程化:添加skip_next_ad | 逻辑冗余 | 改变采集触发行为,评测不通过 |
| BUG-4 | 严重 | 过度工程化:ET1中断保护 | 逻辑冗余 | 改变数码管刷新时序 |
| BUG-5 | 严重 | 过度工程化:湿度+0.5四舍五入 | 数值计算 | 湿度值与评测预期不一致 |
| BUG-6 | 严重 | 调度器频率乱改 | 参数调优 | 采集时序严重偏离,分数暴跌 |
| BUG-7 | 警告 | time_3s使用>=代替== | 条件判断 | 3s计时行为与参考不一致 |
| BUG-8 | 警告 | S5按键限制在回显界面 | 按键逻辑 | 按键响应范围与题目预期不符 |
| BUG-9 | 警告 | unsigned int溢出 | 数值计算 | 平均值计算偶发错误 |
| BUG-10 | 提示 | ++var % N 未定义行为 | 语法风险 | Keil编译器可能产生非预期结果 |
核心教训:越改越低的根本原因
重要认知:不是所有"优化"都是好的。蓝桥杯评测器检查的是行为一致性,不是代码质量。
添加了太多"防御性代码",反而改变了程序的行为特征,导致评测失败。
初始分数 → 修bug提升 → 过度优化 → 分数暴跌 → 对齐参考代码 → 恢复
第一类:底层配置错误
BUG-1 [致命] Timer1模式配置 —— 12T模式 vs 自动重装载
错误代码:
// 错误:使用Mode1(16位不自动重装载),需要ISR手动重载
TMOD &= 0x0F;
TMOD |= 0x10; // Mode1!
TL1 = 0x18;
TH1 = 0xFC;
// ISR中还要手动重载
void Timer1_Isr(void) interrupt 3 {
TL1 = 0x18; // 手动重载
TH1 = 0xFC;
// ...
}
正确代码:
// 正确:Mode0 在STC15上是16位自动重装载!
TMOD &= 0x0F; // Mode0,不需要 |= 0x10
TL1 = 0x18;
TH1 = 0xFC;
// ISR中不需要手动重载
void Timer1_Isr(void) interrupt 3 {
// 直接执行逻辑,硬件自动重装载
uwtick++;
// ...
}
关键知识点:
| 对比项 | 标准8051 | STC15F2K60S2 |
|---|---|---|
| Mode0 | 13位定时器 | 16位自动重装载 |
| Mode1 | 16位不自动重装 | 16位不自动重装 |
| Mode2 | 8位自动重装 | 8位自动重装 |
STC15的Mode0和标准8051完全不同! 这是STC增强型特性。
使用Mode1+手动重载会导致每次中断有微小误差累积,长期运行后定时不准。
影响:所有依赖1ms定时的功能 —— 数码管刷新、频率测量1s闸门、3s采集计时、LED闪烁100ms、长按2s检测。
BUG-2 [致命] idata内存溢出 —— 调度器数组放错位置
错误代码:
// 错误:scheduler_task放在idata,占用大量片上RAM
idata task_t scheduler_task[] = {
{led_proc, 1, 0},
{key_proc, 10, 0},
// ... 6个元素 × (2指针 + 4 + 4) = 约60字节
};
正确代码:
// 正确:放在xdata(外部RAM),不占用宝贵的idata空间
xdata task_t scheduler_task[] = {
{led_proc, 1, 0},
{key_proc, 10, 0},
// ...
};
错误分析:
task_t结构体包含:
-
void (*task_func)(void)→ 2字节(函数指针) -
unsigned long int rate_t→ 4字节 -
unsigned long int last_t→ 4字节 -
共 10字节 × 6个任务 = 60字节
STC15F2K60S2的idata只有256字节,还要存放栈和其他idata变量。60字节的调度器数组加上其他变量,极易导致栈溢出,表现为程序莫名其妙的崩溃或变量被覆盖。
经验法则:大数组(>20字节)优先放 xdata 或 pdata,idata 只放频繁访问的小变量和标志位。
第二类:过度工程化(越改越低的元凶!)
BUG-3 [严重] 添加skip_next_ad防重复触发 —— 画蛇添足
错误代码:
idata bit skip_next_ad = 0; // 多余的标志
void ad_da() {
unsigned char temp_ad;
if (skip_next_ad) { // 多余的防护
skip_next_ad = 0;
temp_ad = Ad_Read(0x01);
voltage_light_old = temp_ad;
return;
}
temp_ad = Ad_Read(0x01);
if (Trigger_Collection_flag == 0) {
if ((temp_ad < 80) && (voltage_light_old > 80)) {
Trigger_Collection_flag = 1;
skip_next_ad = 1; // 设置跳过标志
// ...
}
}
voltage_light_old = temp_ad;
}
正确代码(参考代码方式):
void ad_da() {
unsigned char temp_ad;
temp_ad = Ad_Read(0x01);
if (Trigger_Collection_flag == 0) {
if ((temp_ad < 80) && (voltage_light_old > 80)) {
Trigger_Collection_flag = 1;
temperature_collection = 0;
Humidity_collection = 0;
entry_collection = 0;
}
}
voltage_light_old = temp_ad;
}
为什么多余:
-
触发后
Trigger_Collection_flag = 1,下次进 ad_da 时if(flag==0)已经不成立 -
3s内不会再检查触发条件,天然防重复
-
skip_next_ad 改变了ADC读取节奏,可能导致评测器检测到不同的光敏响应延迟
BUG-4 [严重] 添加ET1中断保护 —— 过度防御
错误代码:
void seg_proc() {
ET1 = 0; // 关闭Timer1中断
// 更新seg_buf[]...
ET1 = 1; // 恢复中断
}
为什么多余:
-
seg_buf是pdata类型,8051对pdata的读写是单字节原子操作
-
参考代码完全没有中断保护,评测也能通过
-
关闭ET1会导致数码管刷新出现微小间隙,反而可能被评测器检测到闪烁
教训:在8位单片机上,对单字节变量的竞争条件风险极低。不要把PC端多线程的防御思维搬到51单片机上。
BUG-5 [严重] 湿度计算添加+0.5四舍五入
错误代码:
temp_Humidity = 80.0 * (float)(freq - 200) / 1800.0 + 10 + 0.5; // 多了+0.5
正确代码:
temp_Humidity = 80.0 * (float)(freq - 200) / 1800.0 + 10; // 不加四舍五入
为什么错:
-
湿度值赋给
unsigned char时,C语言默认截断(向下取整) -
参考代码就是截断,评测器也按截断验证
-
加0.5后湿度值普遍偏大1,所有湿度相关的评测点全部失败
教训:不要自作聪明"改进"计算精度。评测器检查的是固定算法的固定输出。
BUG-6 [严重] 调度器频率盲目调优
错误修改:
// 错误:把频率改得面目全非
xdata task_t scheduler_task[] = {
{led_proc, 1, 0},
{key_proc, 10, 0},
{seg_proc, 10, 0}, // 100→10,过于频繁!
{ad_da, 20, 0}, // 160→20,过于频繁!
{get_temperature, 300, 0},
{get_time, 100, 0},
};
正确频率(参考代码):
xdata task_t scheduler_task[] = {
{led_proc, 1, 0},
{key_proc, 10, 0},
{seg_proc, 100, 0}, // 100ms更新显示内容
{ad_da, 160, 0}, // 160ms采集光敏
{get_temperature, 300, 0}, // 300ms读温度
{get_time, 100, 0}, // 100ms读时钟
};
为什么错:
-
seg_proc改为10ms:数码管内容更新过快,但ISR才1ms刷新一次位,完全没必要 -
ad_da改为20ms:光敏采样过于频繁,PCF8591返回的是上一次的转换结果,20ms间隔可能读到不稳定数据 -
更重要的是:评测器可能依赖特定的采集时序窗口,改变频率就改变了采集时机
教训:调度器频率不是"越快越好"。每个频率背后都有硬件特性和评测时序的考量。
第三类:逻辑细节错误
BUG-7 [警告] 3s计时使用 >= 替代 ==
错误代码:
if (++time_3s >= 2950) { // 用>=,而且还改了数值
time_3s = 0;
Trigger_Collection_flag = 0;
}
正确代码:
if (++time_3s == 3000) { // 精确匹配3000
time_3s = 0;
Trigger_Collection_flag = 0;
}
分析:
-
在1ms中断中,
++time_3s是严格递增的,不会跳过3000 -
使用
==完全安全,且与参考代码一致 -
改成2950相当于把3s缩短为2.95s,评测器检测的是精确的3秒
BUG-8 [警告] S5按键限制在回显界面
错误代码:
// 错误:只有在回显界面才能切换子界面
if (seg_show_mode == 1) {
if (key_down == 5)
echo_show_mode = (echo_show_mode + 1) % 3;
}
正确代码:
// 正确:S5在任何界面都能切换回显子界面
if (key_down == 5) {
echo_show_mode = (echo_show_mode + 1) % 3;
}
分析:S5不受当前界面模式限制。即使当前在时间界面或参数界面,按S5也应该切换echo_show_mode,这样切换到回显界面后能看到期望的子界面。
BUG-9 [警告] unsigned int平均值计算溢出
问题场景:
// temperature_average_10x 是 unsigned int (0~65535)
// Collection_num 可达99,累积值可能溢出
temperature_average_10x = (temperature_average_10x * (Collection_num - 1) + temp_temperature * 10) / Collection_num;
分析:
-
temperature_average_10x * (Collection_num - 1):最大约999 * 98 = 97902,超过 unsigned int 的 65535 -
参考代码也有同样的问题,但评测时采集次数有限,实际不会溢出
-
如果需要修复,可以加
(unsigned long)强制转换,但这不是扣分项
BUG-10 [提示] ++var % N 未定义行为
参考代码中的写法:
Seg_Show_Mode = (++Seg_Show_Mode) % 3; // UB!
Triggered_Num = (++Triggered_Num) % 100; // UB!
安全写法(本项目已修复):
seg_show_mode = (seg_show_mode + 1) % 3; // 安全
Collection_num = (Collection_num + 1) % 100; // 安全
分析:
-
C标准规定:在同一表达式中对变量既修改(++)又读取(右侧的var)是未定义行为
-
Keil C51编译器碰巧能正确处理,但换编译器可能出错
-
本项目已使用安全写法,这是对参考代码的正当改进
总结:调试经验与教训
黄金法则
| 法则 | 说明 |
|---|---|
| 先对齐参考,再谈优化 | 参考代码是评测通过的基准,先100%复刻行为,再考虑代码质量 |
| 不要自作聪明 | 评测器检查行为一致性,不检查代码质量。“更好的代码"≠"更高的分数” |
| 频率不要乱改 | 调度器频率背后有硬件和评测的考量,不是越快越好 |
| 防御性代码要克制 | skip_next_ad、ET1保护、四舍五入,都是PC思维的过度防御 |
| STC15 ≠ 标准8051 | Timer Mode0是16位自动重装载,这是最容易踩的坑 |
错误分类统计
| 分类 | 数量 | 占比 |
|---|---|---|
| 过度工程化 | 4个 | 40% |
| 底层配置 | 2个 | 20% |
| 逻辑细节 | 3个 | 30% |
| 语法风险 | 1个 | 10% |
最大教训:这次调试中40%的问题是"过度工程化"造成的。在竞赛场景下,简单直接的代码胜过精心防护的代码。参考代码之所以能得79分,恰恰是因为它足够简单、足够贴近题目要求,没有多余的"优化"。