蓝桥杯第十二届国赛(超声波物位计)代码错误总结

蓝桥杯第十二届国赛(超声波物位计)代码错误总结

题目信息

  • 题目名称: 超声波物位计

  • 考察模块: DS1302时钟、超声波测距、PCF8591 ADC/DAC、LED、数码管、按键

  • 核心功能:

    • 两种距离采集模式(触发模式、定时模式)

    • 数据统计(最大值、最小值、平均值)

    • DAC电压输出(距离映射)

    • 报警功能(连续3次在范围内)


错误列表

1. :cross_mark: 平均值计算完全错误 - 同一变量既当累加器又当结果

严重等级: :red_circle: 严重(功能完全失效)

错误代码:

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 :cross_mark: 550
第3次(70) +70 → 2870 2870 -
第3次 ×10 → 28700 28700 -
第3次 ÷3 → 9566 9566 :cross_mark: 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. :cross_mark: Data_Min 初始化为 0,永远无法更新

严重等级: :red_circle: 严重(功能完全失效)

错误代码:

idata unsigned int Data_Min = 0;  // ❌ 初始值为0
​
void Get_Distance()
{
    // ...
​
    // ❌ 条件永远不成立!
    if(Distance < Data_Min)  // Distance是unsigned char (0~255)
        Data_Min = Distance;  // 永远 >= 0,所以永远不会执行!
}

错误原因:

  • Distanceunsigned 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. :cross_mark: 统计代码在采集条件外执行,导致除零和重复计算旧数据

严重等级: :red_circle: 严重(除零 + 数据严重错误)

错误代码:

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 /= Countif外面,每次 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. :cross_mark: 定时模式连续触发而非单次触发

严重等级: :yellow_circle: 中等(功能异常)

错误代码 - 版本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 :sparkles: 14
秒14 第2~8次 14 14 0 14
秒15 第1次 14 15 0 15
秒16 第1次 15 16 1 :sparkles: 16

每 Tim_Para 秒恰好触发一次!

关键点:

  • 定时触发必须加边沿检测(秒数变化才触发)

  • 用 static 变量记住上次的秒数

  • 条件: 秒变化 && 秒%Tim_Para==0

  • 触发后立即更新 last_sec,避免同秒内重复触发


总结

最严重的错误(会导致功能完全失效):

  1. 平均值计算错误 - 同一变量既当累加器又当结果,指数级偏离

  2. Data_Min初始化为0 - 永远无法更新最小值

  3. 统计代码位置错误 - 导致除零风险和重复计算旧数据,数据完全失控

中等错误(功能异常或数据不准确):

  1. 定时模式连续触发 - 缺少边沿检测,同一秒内触发多次

学到的经验

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() 应该只做三件事:

  1. 检测触发条件(边沿检测)

  2. 采集数据(调用传感器)

  3. 统计数据(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
蓝桥杯第十二届国赛(超声波物位计)代码错误总结