第十五届省赛错误总结
错误概述
本次练习基于蓝桥杯第十五届单片机省赛题目,涉及 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),但显示时用的还是原始 freq,addtive 完全被浪费了。
修复
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的最终结果"这类定义性描述,必须把它当作全局变量的重新赋值来理解——后续所有引用该概念的地方,都要跟着变。