第十四届代码错误总结

蓝桥杯第十四届省赛 —— 代码错误总结与修复记录


题目:第十四届蓝桥杯单片机组省赛程序设计


平台: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分,恰恰是因为它足够简单、足够贴近题目要求,没有多余的"优化"。