第十五届省赛错误总结

第十五届省赛错误总结

错误概述

本次练习基于蓝桥杯第十五届单片机省赛题目,涉及 NE555 频率测量、频率校准参数、DAC 线性输出、DS1302 时钟、最大频率追踪与回显、LED 报警闪烁、数码管多界面显示、按键参数调节等功能。调试过程中共发现 6 个错误,其中 5 个错误源自同一个根本原因:没有理解题目对"频率数据"的重新定义,到处使用了原始测量值而非校准后的值。另有 1 个 C 语言整数除法经典陷阱。


核心错误:未理解"频率数据"的定义

题目原文

3.4 节第 2 条:“直接测量到的频率数据加校准值参数,作为频率数据的最终结果。”

含义

这句话重新定义了"频率数据"这个术语。从这句话开始,题目后续所有出现"频率数据"、"频率"的地方,指的都是:

频率数据 = 原始测量值(freq) + 校准参数(check_para)

而不是原始的 freq。这一个理解偏差,直接导致了下面错误 1~4 共四处出错。


错误1:频率显示使用原始值而非校准值

表现

数码管频率界面显示的数值与期望不符。例如:原始频率 50Hz,校准参数 +100,期望显示 150,实际显示 50。

原因

// 错误代码
addtive = freq + check_para;   // 算了校准值
if(addtive >= 0)
{
    seg_buf[3] = freq / 10000 % 10;   // ← 显示的却是原始 freq!
    seg_buf[4] = freq / 1000 % 10;
    seg_buf[5] = freq / 100 % 10;
    seg_buf[6] = freq / 10 % 10;
    seg_buf[7] = freq % 10;
}

校准值算出来了(addtive),但显示时用的还是原始 freqaddtive 完全被浪费了。

修复

seg_buf[3] = addtive / 10000 % 10;
seg_buf[4] = addtive / 1000 % 10;
seg_buf[5] = addtive / 100 % 10;
seg_buf[6] = addtive / 10 % 10;
seg_buf[7] = addtive % 10;

教训

定义了一个变量来存校准后的值,就要检查后续是否真的用了它。"算了但没用"比"没算"更难发现。


错误2:DAC 输出公式整数除法截断 + 未使用校准值

表现

DAC 输出电压恒为 1.0V,无论频率如何变化。

原因

问题一:整数除法截断

// 错误代码(先除后乘)
da_write(freq * (4 / (over_para - 500)) + (1 - (2000 / (over_para - 500))));

// 以 freq=1500, over_para=5000 为例:
// 4 / (5000 - 500) = 4 / 4500 = 0      ← C语言整数除法,直接截断为0
// 2000 / (5000 - 500) = 2000 / 4500 = 0 ← 同样截断为0
// 最终:1500 * 0 + (1 - 0) = 1
// da_write(1 * 51) = 51 → 输出 1.0V    ← 恒定值!

问题二:未使用校准值

范围判断和公式中用的是原始 freq,应该用 freq + check_para

修复

void ad_da()
{
    int freq_cal = freq + check_para;  // 使用校准后的频率
    ad_10x = ad_read(0x43) * 10 / 51;

    if(freq_cal < 0)
    {da_write(0);}
    else if(freq_cal < 500)
    {da_write(51);}
    else if(freq_cal <= over_para)
    {da_write((unsigned char)(51 + (long)(freq_cal - 500) * 204 / (over_para - 500)));}
    else
    {da_write(255);}
}

核心思路:先乘后除(freq_cal - 500) * 204 先得到一个大数,再除以 (over_para - 500),避免整数截断。

验证

freq=1500, check=-100, over_para=5000 → freq_cal=1400
51 + (1400-500)*204/(5000-500) = 51 + 900*204/4500 = 51 + 40 = 91
91/256*5V = 1.78V ≈ 1.8V

教训

C 语言整数运算必须先乘后除4/4500 在数学上是 0.00089,在 C 语言中就是 0。这是嵌入式开发中反复出现的经典陷阱——第十四届省赛的 AD 公式犯过同样的错(/ 51 * 10 → 应为 * 10 / 51)。


错误3:最大频率追踪使用原始值

表现

回显界面的最大频率值(HF)与期望不符。例如:原始频率 150Hz,校准参数 +100,期望 HF 显示 250,实际显示 150。对应的最大频率出现时间也不正确。

原因

// 错误代码 — get_time()
if(freq_max < freq)          // ← 比较的是原始 freq
{
    read_rtc(ucrtc_old);
    freq_max = freq;          // ← 存的也是原始 freq
}

// 错误代码 — Timer1 ISR 第一次捕获
if(catch_count == 1)
{
    freq_max = freq;          // ← 同样用了原始 freq
    ...
}

题目要求统计的是校准后"频率数据"的最大值,但 freq_max 的比较和存储都用了原始 freq

修复

// get_time()
void get_time()
{
    int freq_cal = freq + check_para;
    read_rtc(ucrtc);
    if((freq_cal >= 0) && (freq_cal > freq_max))
    {
        read_rtc(ucrtc_old);
        freq_max = freq_cal;
    }
}

// Timer1 ISR 第一次捕获
if(catch_count == 1)
{
    freq_max = freq + check_para;
    ucrtc_old[0] = ucrtc[0];
    ucrtc_old[1] = ucrtc[1];
    ucrtc_old[2] = ucrtc[2];
}

教训

"频率数据"被题目重新定义后,所有涉及频率的业务逻辑(显示、输出、比较、存储)都必须跟着变。不能只改显示,忘了比较和存储。


错误4:LED 报警条件使用原始值

表现

LED2 报警闪烁的触发阈值与期望不符。

原因

// 错误代码
if((freq > over_para) && (freq + check_para >= 0))      // ← freq 应为 freq+check_para

题目要求"当前频率数据大于超限参数时"L2 闪烁,"频率数据"是校准后的值。

修复

if(((freq + check_para) > over_para) && (freq + check_para >= 0))

教训

同错误 1~3,还是"频率数据"定义未贯彻的问题。写完代码后应该全局搜索 freq,逐个确认每处是否该用校准值。


错误5:seg_pos 自增存在未定义行为

表现

大多数编译器下表现正常,但属于 C 语言标准中的未定义行为,换编译器或优化等级可能出错。

原因

// 错误代码
seg_pos = (++seg_pos) % 8;
// 同一表达式中对 seg_pos 既读又写,C 标准未定义求值顺序

修复

seg_pos = (seg_pos + 1) % 8;

教训

避免在同一个表达式中对同一个变量既做自增(++)又做赋值(=)。拆开写更清晰、更安全。


错误6:key.c 第四行扫描为死代码

表现

无直接影响(该行按键未使用),但代码不整洁。

原因

P44 = 1;P42 = 1;P35=1;//P34=0;    // P34=0 被注释掉了
if(P30 == 0)temp = 19;              // ← 这些 if 永远不会成立
if(P31 == 0)temp = 18;              //    因为没有拉低任何行线
if(P32 == 0)temp = 17;
if(P33 == 0)temp = 16;

第四行扫描的行选择 P34=0 被注释掉了(因为 P3.4 用作 T0 计数器输入),但后面的 if 判断还在,永远不会触发。

修复

将整段第四行扫描代码注释或删除,避免混淆。

教训

注释掉功能代码时,要把相关联的所有代码一起处理,不要只注释一半留一半。


总结

序号 错误类型 严重程度 根因分类
1 频率显示用原始值 致命 概念理解偏差
2 DAC 整数除法截断 + 用原始值 致命 概念理解偏差 + 整数除法
3 freq_max 追踪原始值 致命 概念理解偏差
4 LED 报警比较原始值 致命 概念理解偏差
5 seg_pos 未定义行为 C 语言基础
6 key.c 死代码 提示 代码整洁

错误规律

规律一:概念理解偏差导致系统性错误(4次)

题目用一句话重新定义了"频率数据" = 测量值 + 校准参数,但代码中四个模块(显示、DAC、最大值、报警)全部使用了原始测量值。

这不是"写错了",而是"理解错了"。 一个概念理解偏差,导致了 4 处独立的代码错误。

自查方法:遇到题目中出现"XX数据作为最终结果"这类定义性语句时,高亮标注,然后全局搜索代码中所有使用该数据的地方,逐一确认是否用了"最终结果"版本。

规律二:整数除法截断(第二次犯)

第十四届:ad_read() / 51 * 10(先除后乘,精度丢失)
第十五届:4 / (over_para - 500)(小数除大数,直接截断为 0)

同一个坑踩了两次,说明还没形成条件反射。

自查方法:代码中每出现一次除法 /,都要停下来问自己——被除数比除数小吗? 如果小,结果就是 0。解决办法永远是先乘后除

与第十四届对比

对比维度 第十四届 第十五届
错误数量 11 个 6 个
致命错误 7 个 4 个
主要错误类型 顺序搞反、嵌套错误、清零遗漏 概念理解偏差、整数除法
重复犯的错 无(首次) 整数除法(第二次)
进步 嵌套和清零类错误消失

本届核心教训:读题时,凡是"作为XX的最终结果"这类定义性描述,必须把它当作全局变量的重新赋值来理解——后续所有引用该概念的地方,都要跟着变。