蓝桥杯第十二届国赛(超声波物位计)代码错误总结
题目信息
-
题目名称: 超声波物位计
-
考察模块: DS1302时钟、超声波测距、PCF8591 ADC/DAC、LED、数码管、按键
-
核心功能:
-
两种距离采集模式(触发模式、定时模式)
-
数据统计(最大值、最小值、平均值)
-
DAC电压输出(距离映射)
-
报警功能(连续3次在范围内)
-
错误列表
1.
平均值计算完全错误 - 同一变量既当累加器又当结果
严重等级:
严重(功能完全失效)
错误代码:
idata unsigned int Data_Ave = 0; // 平均值,放大十倍,两位整数显示一位小数
void Get_Distance()
{
// ...采集逻辑...
// ❌ 错误的平均值计算
Data_Ave += Distance; // 在上一次结果基础上累加
Data_Ave *= 10; // 放大10倍
Data_Ave /= Count; // 除以次数
}
错误原因:
Data_Ave 同时被当作「累加器」和「结果」使用,每次调用都会在上一次的结果基础上继续加减乘除,导致数值指数级偏离!
错误推演(假设依次测量 50cm 和 60cm):
| 步骤 | 操作 | Data_Ave | 正确值应为 |
|---|---|---|---|
| 初始 | - | 0 | 0 |
| 第1次 | +50 → 50 | 50 | - |
| 第1次 | ×10 → 500 | 500 | - |
| 第1次 | ÷1 → 500 | 500 | 500 ✓ |
| 第2次 | +60 → 560 | 560 | - |
| 第2次 | ×10 → 5600 | 5600 | - |
| 第2次 | ÷2 → 2800 | 2800 |
550 |
| 第3次(70) | +70 → 2870 | 2870 | - |
| 第3次 | ×10 → 28700 | 28700 | - |
| 第3次 | ÷3 → 9566 | 9566 |
600 |
数据偏离速度:
第1次: 500 (正确)
第2次: 2800 (应该550, 偏离510%)
第3次: 9566 (应该600, 偏离1494%)
第N次: 指数级爆炸 💥
正确代码:
idata unsigned int Data_Ave = 0; // 平均值结果
idata unsigned int Data_Sum = 0; // ✓ 新增独立累加变量
void Get_Distance()
{
if(Dect0_Flag==1 || Dect1_Flag==1)
{
Distance = Ut_Wave_Data();
Count++;
// ✓ 正确的平均值计算
Data_Sum += Distance; // 累加到独立变量
Data_Ave = (unsigned int)((unsigned long)Data_Sum * 10 / Count);
// 先转unsigned long防止中间溢出,再除以次数得到平均值×10
}
}
为什么要转 unsigned long:
假设 Data_Sum = 30000, Count = 5:
// ❌ 错误: unsigned int * 10 可能溢出
Data_Ave = Data_Sum * 10 / Count;
// 30000 * 10 = 300000 > 65535 (unsigned int最大值) → 溢出!
// ✓ 正确: 先转unsigned long再计算
Data_Ave = (unsigned int)((unsigned long)Data_Sum * 10 / Count);
// (unsigned long)30000 * 10 = 300000 → 正常
// 300000 / 5 = 60000 → 正常
// (unsigned int)60000 → 正常
关键点:
-
累加器(Data_Sum)和结果(Data_Ave)必须分离
-
中间计算要防溢出,用
(unsigned long)转换 -
平均值公式:
(Sum × 10) / Count,先放大再除,保留一位小数
2.
Data_Min 初始化为 0,永远无法更新
严重等级:
严重(功能完全失效)
错误代码:
idata unsigned int Data_Min = 0; // ❌ 初始值为0
void Get_Distance()
{
// ...
// ❌ 条件永远不成立!
if(Distance < Data_Min) // Distance是unsigned char (0~255)
Data_Min = Distance; // 永远 >= 0,所以永远不会执行!
}
错误原因:
-
Distance是unsigned char类型,范围 0~255 -
Data_Min初始化为 0 -
条件
Distance < 0永远不成立(无符号数永远 >= 0) -
最小值将永远保持为 0!
测试推演:
测量第1次: Distance = 50
50 < 0? ❌ 不成立 → Data_Min 保持 0
测量第2次: Distance = 30
30 < 0? ❌ 不成立 → Data_Min 保持 0
测量第N次: Distance = 任何值
任何值 < 0? ❌ 永远不成立 → Data_Min 永远是 0
正确代码 - 方法1(初始化为大数):
idata unsigned int Data_Min = 999; // ✓ 初始化为一个足够大的数
void Get_Distance()
{
if(Dect0_Flag==1 || Dect1_Flag==1)
{
Distance = Ut_Wave_Data();
Count++;
// ✓ 第一次测量必定小于999
if(Distance < Data_Min)
Data_Min = Distance;
}
}
正确代码 - 方法2(首次特殊处理):
idata unsigned int Data_Min = 0;
void Get_Distance()
{
if(Dect0_Flag==1 || Dect1_Flag==1)
{
Distance = Ut_Wave_Data();
Count++;
// ✓ 第一次测量直接赋值
if(Count == 1)
Data_Min = Distance;
else if(Distance < Data_Min)
Data_Min = Distance;
}
}
关键点:
-
无符号数的最小值不能初始化为 0
-
初始化为一个足够大的数(如 999、65535)
-
或者首次测量时特殊处理(直接赋值)
3.
统计代码在采集条件外执行,导致除零和重复计算旧数据
严重等级:
严重(除零 + 数据严重错误)
错误代码:
idata unsigned char Count = 0; // 初始值为0
void Get_Distance()
{
// 触发检测...
// 采集逻辑
if(Dect0_Flag==1 || Dect1_Flag==1)
{
Distance = Ut_Wave_Data(); // 只在这里更新Distance
Count++; // 只在if块内累加
// 报警逻辑...
}
// ❌ 这些代码在 if 块外面!每120ms都执行!
if(Distance > Data_Max)
Data_Max = Distance; // 用旧Distance重复比较(无害但无意义)
if(Distance < Data_Min)
Data_Min = Distance; // 用旧Distance重复比较(无害但无意义)
Data_Ave += Distance; // ❌ 把旧Distance重复加入!
Data_Ave *= 10;
Data_Ave /= Count; // ❌ Count初始为0,首次调用直接除零!
}
错误原因1 - 除零风险:
-
Count++只在采集条件成立时执行(if块内) -
Data_Ave /= Count在if块外面,每次Get_Distance()都会执行 -
第一次调用时若无采集触发,
Count=0,产生除以零!
错误原因2 - 重复计算旧数据:
即使没有新的测量发生,这段代码每 120ms 都会:
-
用旧的
Distance值反复比较(对 Max/Min 无害但无意义) -
把旧的
Distance重复加入Data_Ave并重新计算(严重破坏平均值!)
除零后果(8051平台):
// 整数除零通常不会崩溃,但会返回垃圾值
unsigned int result = 100 / 0; // result = 未定义值(可能是0、65535或其他)
// 某些编译器行为:
Keil C51: 返回被除数(100 / 0 = 100)
其他编译器: 返回0或其他值
执行流程分析(除零问题):
程序启动 → Count = 0
↓
第1次调用 Get_Distance():
if(Dect0_Flag==1 || Dect1_Flag==1) ← 条件不满足(还没采集)
{
// 这里不执行
}
Data_Ave /= Count; ❌ 除以0!
↓
第2次调用 Get_Distance():
if(Dect0_Flag==1 || Dect1_Flag==1) ← 条件满足(触发采集)
{
Count++; → Count = 1
}
Data_Ave /= Count; → 除以1(正常但已经晚了)
错误推演(重复计算问题,Get_Distance每120ms调用一次):
时间0ms: 触发采集,Distance = 50
if块内: Count=1, Data_Ave=50×10/1=500 ✓
if块外: Data_Ave+=50→550, ×10→5500, ÷1→5500 ❌
时间120ms: 未触发,Distance还是50
if块内: 不执行
if块外: Data_Ave+=50→5550, ×10→55500, ÷1→55500 ❌
时间240ms: 触发采集,Distance = 60
if块内: Count=2, Distance=60
if块外: Data_Ave+=60→55560, ×10→555600, ÷2→277800 ❌
结果: 完全失控!
正确代码:
void Get_Distance()
{
// 定时模式触发检测
if(ucRtc[2] != last_sec && ucRtc[2] % Tim_Para == 0)
Dect1_Flag = 1;
else
Dect1_Flag = 0;
last_sec = ucRtc[2];
// 触发模式检测
if((Light_Old==1) && (Light_Flag==0))
Dect0_Flag = 1;
else
Dect0_Flag = 0;
// ✓ 采集和统计全部在同一个 if 块内
if(Dect0_Flag==1 || Dect1_Flag==1)
{
Distance = Ut_Wave_Data(); // 获取新距离
Count++; // 先递增Count(避免除零)
// 最大值(用新Distance,只计算一次)
if(Distance > Data_Max)
Data_Max = Distance;
// 最小值(用新Distance,只计算一次)
if(Distance < Data_Min)
Data_Min = Distance;
// 平均值(Count已经>=1,不会除零,只累加新Distance一次)
Data_Sum += Distance;
Data_Ave = (unsigned int)((unsigned long)Data_Sum * 10 / Count);
// 报警逻辑...
}
// ✓ if块外什么都不做,避免除零和重复计算!
}
关键点:
-
所有依赖 Count 的计算都要放在
Count++之后 -
确保 Count ≥ 1 时才执行除法
-
采集、统计、报警逻辑必须全部在同一个 if 块内
-
只有新测量发生时才更新统计数据
-
避免用旧数据重复计算
4.
定时模式连续触发而非单次触发
严重等级:
中等(功能异常)
错误代码 - 版本1(除法判断):
void Get_Distance()
{
// ❌ 错误的定时检测
if(ucRtc[2] / Tim_Para == 0)
Dect1_Flag = 1;
else
Dect1_Flag = 0;
// ...
}
错误原因(除法版本):
ucRtc[2] / Tim_Para == 0 的含义是「当秒数 < Tim_Para 时为真」。
当 Tim_Para=2 时的行为:
| 秒 | ucRtc[2] / 2 | == 0? | Dect1_Flag |
|---|---|---|---|
| 0 | 0 | ✓ | 1 ← 触发 |
| 1 | 0 | ✓ | 1 ← 触发 |
| 2 | 1 | ✗ | 0 |
| 3~59 | ≥1 | ✗ | 0 |
结果:
-
每分钟的前2秒内连续触发(约120ms一次,共触发~16次)
-
然后58秒内不触发
-
这不是「每2秒采集一次」!
错误代码 - 版本2(取模无边沿):
void Get_Distance()
{
// ❌ 缺少边沿检测
if(ucRtc[2] % Tim_Para == 0)
Dect1_Flag = 1;
else
Dect1_Flag = 0;
// ...
}
错误原因(取模无边沿):
虽然改用了取模,但缺少边沿检测。
当 Tim_Para=2 时的行为(Get_Distance 每120ms调用一次):
| 时刻 | 秒 | %2==0? | Dect1_Flag | 触发? |
|---|---|---|---|---|
| 第0秒 第1次 | 0 | ✓ | 1 | ✓ |
| 第0秒 第2次 | 0 | ✓ | 1 | ✓ |
| 第0秒 第3~8次 | 0 | ✓ | 1 | ✓ ← 同一秒内触发~8次! |
| 第1秒 第1次 | 1 | ✗ | 0 | ✗ |
| 第2秒 第1~8次 | 2 | ✓ | 1 | ✓ ← 又是~8次! |
结果:
-
每个偶数秒内触发 ~8 次(而不是1次)
-
Count 增加 ~8 而非 1
-
平均值的样本权重不均
正确代码(取模+边沿检测):
idata unsigned char last_sec = 0xFF; // ✓ 静态变量记住上次的秒
void Get_Distance()
{
// ✓ 定时模式 - 边沿检测
if(ucRtc[2] != last_sec && ucRtc[2] % Tim_Para == 0)
Dect1_Flag = 1;
else
Dect1_Flag = 0;
last_sec = ucRtc[2]; // 更新记录
// 触发模式检测...
if(Dect0_Flag==1 || Dect1_Flag==1)
{
// 采集逻辑...
}
}
工作原理(Tim_Para=2,初始 last_sec=0xFF):
| 时刻 | last_sec | ucRtc[2] | !=? | %2==0? | Dect1_Flag | 更新后last_sec |
|---|---|---|---|---|---|---|
| 秒13 第1次 | 0xFF | 13 | ✓ | ✗ | 0 | 13 |
| 秒13 第2~8次 | 13 | 13 | ✗ | — | 0 | 13 |
| 秒14 第1次 | 13 | 14 | ✓ | ✓ | 1 |
14 |
| 秒14 第2~8次 | 14 | 14 | ✗ | — | 0 | 14 |
| 秒15 第1次 | 14 | 15 | ✓ | ✗ | 0 | 15 |
| 秒16 第1次 | 15 | 16 | ✓ | ✓ | 1 |
16 |
每 Tim_Para 秒恰好触发一次! ✓
关键点:
-
定时触发必须加边沿检测(秒数变化才触发)
-
用 static 变量记住上次的秒数
-
条件:
秒变化 && 秒%Tim_Para==0 -
触发后立即更新 last_sec,避免同秒内重复触发
总结
最严重的错误(会导致功能完全失效):
-
平均值计算错误 - 同一变量既当累加器又当结果,指数级偏离
-
Data_Min初始化为0 - 永远无法更新最小值
-
统计代码位置错误 - 导致除零风险和重复计算旧数据,数据完全失控
中等错误(功能异常或数据不准确):
- 定时模式连续触发 - 缺少边沿检测,同一秒内触发多次
学到的经验
1. 数据统计的正确实现方式
核心原则:
-
累加器和结果必须分离 - 不能用同一个变量既累加又存结果
-
防止中间溢出 - 用
(unsigned long)扩展精度 -
统计和采集在同一作用域 - 避免重复计算旧数据
平均值标准实现:
// 分离累加器和结果
idata unsigned int Data_Sum = 0; // 累加器
idata unsigned int Data_Ave = 0; // 结果
// 统计逻辑
Data_Sum += Distance; // 累加
Data_Ave = (unsigned int)((unsigned long)Data_Sum * 10 / Count); // 防溢出
2. 边界值初始化要合理
最值初始化原则:
-
最大值初始化为 0(第一次测量必定大于0)
-
最小值初始化为最大可能值(如999、65535)
-
或者首次测量特殊处理(直接赋值)
错误示例:
Data_Max = 65535; // ❌ 最大值初始化太大
Data_Min = 0; // ❌ 最小值初始化太小
正确示例:
Data_Max = 0; // ✓ 最大值从0开始
Data_Min = 999; // ✓ 最小值从大数开始
3. 边沿检测不可或缺
定时触发必须加边沿检测:
// ❌ 错误:同一秒内重复触发
if(ucRtc[2] % Tim_Para == 0)
trigger();
// ✓ 正确:秒变化才触发
static unsigned char last_sec = 0xFF;
if(ucRtc[2] != last_sec && ucRtc[2] % Tim_Para == 0)
trigger();
last_sec = ucRtc[2];
触发模式的边沿检测:
// ✓ 暗→亮的边沿
Light_Old = Light_Flag; // 先保存旧值
// 更新Light_Flag...
if((Light_Old==1) && (Light_Flag==0)) // 检测边沿
Dect0_Flag = 1;
4. 采集逻辑的职责分离
Get_Distance() 应该只做三件事:
-
检测触发条件(边沿检测)
-
采集数据(调用传感器)
-
统计数据(Max/Min/Ave/报警)
代码结构:
void Get_Distance()
{
// 第一步:检测触发(边沿检测)
边沿检测逻辑...
// 第二步:采集和统计(全在if块内)
if(trigger)
{
采集新数据
更新统计
检查报警
}
// if块外什么都不做!
}
5. 数据类型要匹配使用场景
计数器类型选择:
unsigned char Count; // ❌ 最大255,容易溢出
unsigned int Count; // ✓ 最大65535,竞赛足够
unsigned long Count; // 过度设计,浪费内存
防溢出转换:
// ❌ 中间溢出
unsigned int result = a * 10 / b; // a*10可能>65535
// ✓ 先转换再计算
unsigned int result = (unsigned int)((unsigned long)a * 10 / b);
复盘检查清单
在蓝桥杯单片机比赛中,遇到类似题目时,务必检查:
数据统计相关
-
平均值:累加器(Data_Sum)和结果(Data_Ave)分离
-
平均值计算:
(unsigned long)Data_Sum * 10 / Count防溢出 -
最小值初始化为大数(如999),不能为0
-
最大值初始化为0
-
所有统计代码在采集条件if块内,避免重复计算
-
Count类型为unsigned int,防止溢出
触发检测相关
-
定时触发加边沿检测:
秒变化 && 秒%Tim_Para==0 -
触发模式加边沿检测:
(Old==1) && (New==0) -
用static变量记住上次的秒数
除零防护相关
-
除法前确保Count≥1
-
统计代码在Count++之后
-
统计逻辑全部在采集条件if块内
生成时间: 2026-02-21
蓝桥杯第十二届国赛(超声波物位计)代码错误总结