单片机省赛十四(一)

第十四届蓝桥杯单片机省赛(一)错误分析笔记

题目:DS18B20 温度、DS1302 时钟、ADC 采集(频率→湿度)


一、错误总览

# 错误类型 涉及文件 影响测试项
1 架构错误:数据采集逻辑放在 Led_Proc() main.c:264~298 3/5/7/19~21/28/30~33/35~36/38/40~43/45~47/49/54~60 几乎全部
2 缺少硬件输出Led_Proc() 末尾没有调用 Led_Disp(ucLed) main.c:298后 2/4/6/8/31/33/43/47/56/62 所有LED测试项
3 显示格式:时间回显界面 Seg_Buf[7] 写错 main.c:195 7/21/36/46/55
4 显示格式:平均值小数点触发符号写反(-',' 应为 +',' main.c:174/184 3/5/19/20/28/30/32/35/41/42/54/58/60
5 数据类型Temp_Sum/Wet_Sum 声明为 unsigned char 易溢出 main.c:64/65 长时间运行后平均值错误

二、逐条详解


错误 1(最严重):数据采集逻辑误放在 Led_Proc()

问题代码(main.c:264~298)

void Led_Proc()
{
    // ... 设置 ucLed ...
​
    if(Flag_Wet==1)
    {
        // ⚠️ 这里每 20ms 都会执行一次!
        Temperature_Old = Temperature;   // 保存旧值
        Wet_Old = Wet;
        Num++;                           // 采集次数 +1(每 20ms 就加一次)
        Num_ucRtc[0] = ucRtc[0];        // 记录时间
        Num_ucRtc[1] = ucRtc[1];
        Num_ucRtc[2] = ucRtc[2];
        if (Temperature > Temperature_Max) Temperature_Max = Temperature;
        if (Wet > Wet_Max) Wet_Max = Wet;
        Temp_Sum += Temperature;         // 每 20ms 就累加一次,数值飞涨
        Wet_Sum += Wet;
        Temperature_Average_100x = (Temp_Sum * 10) / Num;
        Wet_Average_100x = (Wet_Sum * 10) / Num;
    }
    if ((Num >= 2) && (Temperature > Temperature_Old) && (Wet > Wet_Old))
        ucLed[5] = 1;
}

为什么错误?

症状 原因
初始状态(测试3/5/7)切换到回显界面就立刻显示数据(不是空) Num++ 每 20ms 执行一次,程序刚运行几秒 Num 就远大于 0
L6(温湿度均升高指示)永远不亮 先执行了 Temperature_Old = Temperature,再判断 Temperature > Temperature_Old 永远为 false
最大值/平均值数据混乱 每 20ms 累加一次,数据不代表"一次光照触发的采集"
按 S9 长按清除后数据不归零(测试57) Num 还在持续自增

正确做法

采集操作应当且仅当"光照由暗变亮(上升沿)触发一次"时执行。

采集逻辑应放在 AD_DA() 函数中检测到上升沿且 Flag_3s_Lock == 0if 块内:

void AD_DA()
{
    if (AD_DA_Slow_Down < 120) return;
    AD_DA_Slow_Down = 0;
​
    AD_Data = Ad_Read(0x01);
    Light_State_Current = (AD_Data > 100) ? 1 : 0;
​
    // 检测上升沿(暗→亮)
    if ((Light_State_Old == 0) && (Light_State_Current == 1))
    {
        if (Flag_3s_Lock == 0)
        {
            Flag_3s_Lock = 1;
            Time_3s_Count = 0;
            Seg_Mode_Backup = Seg_Mode;
            Seg_Mode = 3;  // 切换到 E 界面
​
            // ✅ 采集逻辑放在这里,每次触发只执行一次
            Temperature_Old = Temperature;
            Wet_Old = Wet;
            Num++;
            Num_ucRtc[0] = ucRtc[0];
            Num_ucRtc[1] = ucRtc[1];
            Num_ucRtc[2] = ucRtc[2];
            if (Temperature > Temperature_Max) Temperature_Max = Temperature;
            if (Wet > Wet_Max)               Wet_Max = Wet;
            Temp_Sum += Temperature;
            Wet_Sum  += Wet;
            Temperature_Average_100x = ((unsigned int)Temp_Sum * 10) / Num;
            Wet_Average_100x         = ((unsigned int)Wet_Sum  * 10) / Num;
        }
    }
​
    Light_State_Old = Light_State_Current;
}

同时,Led_Proc()if(Flag_Wet==1) 块和 L6 的判断改为只读,不再做采集:

void Led_Proc()
{
    ucLed[0]=ucLed[1]=ucLed[2]=ucLed[3]=ucLed[4]=ucLed[5]=0;
​
    switch(Seg_Mode)
    {
        case 0: ucLed[0]=1; break;  // L1
        case 1: ucLed[1]=1; break;  // L2
        case 3: ucLed[2]=1; break;  // L3
    }
​
    if (Temperature > Temperature_Ctrl)
        ucLed[3] = Flag_Time_100ms;  // L4 闪烁
​
    if (Flag_Wet == 0)
        ucLed[4] = 1;  // L5:湿度无效
​
    // L6:本次温湿度均高于上次
    if ((Num >= 2) && (Temperature > Temperature_Old) && (Wet > Wet_Old))
        ucLed[5] = 1;
​
    Led_Disp(ucLed);  // ← 见错误2
}

错误 2:Led_Proc() 末尾缺少 Led_Disp(ucLed) 调用

问题

Led_Proc() 函数体内只对 ucLed[] 数组赋值,但没有调用 Led_Disp(ucLed) 将数据写入硬件,导致 LED 灯始终不随程序逻辑变化。

修复

Led_Proc() 的最后一行添加:

Led_Disp(ucLed);  // ✅ 必须调用,否则 LED 不更新

错误 3:时间回显界面 Seg_Buf[7] 写成了分钟十位

问题代码(main.c:195)

case 2:  // 时间回显(F界面)
    Seg_Buf[0] = 14;  // F
    Seg_Buf[1] = Num / 10 % 10;
    Seg_Buf[2] = Num % 10;
    Seg_Buf[3] = (Num==0)?10:Num_ucRtc[0]/10%10;  // 小时十位
    Seg_Buf[4] = (Num==0)?10:Num_ucRtc[0]%10;     // 小时个位
    Seg_Buf[5] = (Num==0)?10:11;                   // -
    Seg_Buf[6] = (Num==0)?10:Num_ucRtc[1]/10%10;  // 分钟十位
    Seg_Buf[7] = (Num==0)?10:Num_ucRtc[1]/10%10;  // ⚠️ 错!写成了分钟十位

修复

    Seg_Buf[7] = (Num==0)?10:Num_ucRtc[1]%10;  // ✅ 应为分钟个位

影响

测试7 期望显示 F00(Num=0),但因错误1(Num不为0)且此行复制错误,实际显示 F 810-33


错误 4:平均值小数点触发符号写反(-',' 应为 +','

背景:小数点显示机制

Timer1_Isr() 中,判断逻辑如下:

if (Seg_Buf[Seg_Pos] > 20)
    Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos] - ',', 1);  // 带小数点显示
else
    Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], 0);         // 普通显示

',' 的 ASCII 码为 44(0x2C)
要触发带小数点显示,需要 Seg_Buf[i] > 20,因此:

Seg_Buf[i] = 实际数字(0~9)+ 44   →   范围 44~53,均 > 20 ✅

问题代码(main.c:174 温度平均值 / 184 湿度平均值)

// 温度平均值个位(应带小数点)
Seg_Buf[6]=(Num==0)?10:Temperature_Average_100x/10%10-','; // ⚠️ -44 变为负数/超大值
// 湿度平均值个位(应带小数点)
Seg_Buf[6]=(Num==0)?10:Wet_Average_100x/10%10-',';         // ⚠️ 同上

digit - 44unsigned char 来说会下溢,产生垃圾值(如 0 - 44 = 212)。

修复

// 温度平均值个位,+44 触发小数点
Seg_Buf[6]=(Num==0)?10:Temperature_Average_100x/10%10+','; // ✅
// 湿度平均值个位,+44 触发小数点
Seg_Buf[6]=(Num==0)?10:Wet_Average_100x/10%10+',';         // ✅

完整温度回显格式(Mode1=0 时,C XX-YY.Z)

case 0:
    Seg_Buf[0] = 12;  // C
    Seg_Buf[1] = 10;  // 空
    Seg_Buf[2] = (Num==0)?10:Temperature_Max/10%10;            // 最高温度十位
    Seg_Buf[3] = (Num==0)?10:Temperature_Max%10;               // 最高温度个位
    Seg_Buf[4] = (Num==0)?10:11;                               // -
    Seg_Buf[5] = (Num==0)?10:Temperature_Average_100x/100%10;  // 平均温度十位
    Seg_Buf[6] = (Num==0)?10:Temperature_Average_100x/10%10+','; // ✅ 平均温度个位(带小数点)
    Seg_Buf[7] = (Num==0)?10:Temperature_Average_100x%10;      // 平均温度小数位
    break;

错误 5:Temp_Sum/Wet_Sum 声明为 unsigned char 易溢出

问题代码(main.c:64/65)

idata unsigned char Temp_Sum;  // ⚠️ 最大只能存 255
idata unsigned char Wet_Sum;   // ⚠️ 最大只能存 255

影响

温度 25℃,采集 11 次后:25 × 11 = 275 > 255unsigned char 溢出,平均值计算错误。

修复

idata unsigned int Temp_Sum;   // ✅ 最大 65535,可支持更多次采集
idata unsigned int Wet_Sum;    // ✅

三、错误根源总结

              ┌─────────────────────────────────────────┐
              │         最根本错误(错误1+2)              │
              │  数据采集逻辑放错位置 + LED 没有输出      │
              └────────────────┬────────────────────────┘
                               │ 导致
              ┌────────────────▼────────────────────────┐
              │  Num 每 20ms 自增一次                   │
              │  Temperature_Old 每次都被覆盖为当前值   │
              │  Temp_Sum 快速溢出                      │
              └────────────────┬────────────────────────┘
                               │ 进而
    ┌──────────────────────────▼─────────────────────────────────┐
    │  初始界面不为空(测试3/5/7)                               │
    │  L6 永远不亮(因为 Temperature_Old 始终 == Temperature)  │
    │  L2/L1/L3 永远不更新(没有 Led_Disp)                    │
    │  平均值显示乱码(符号写反 + 溢出)                        │
    └────────────────────────────────────────────────────────────┘

四、修改点速查

文件 行号 原内容(关键部分) 修改后
main.c 64 unsigned char Temp_Sum unsigned int Temp_Sum
main.c 65 unsigned char Wet_Sum unsigned int Wet_Sum
main.c 174 ...Average_100x/10%10-',' ...Average_100x/10%10+','
main.c 184 ...Average_100x/10%10-',' ...Average_100x/10%10+','
main.c 195 Num_ucRtc[1]/10%10 (Seg_Buf[7]) Num_ucRtc[1]%10
main.c 264~293 整个 if(Flag_Wet==1){...} 采集块 移至 AD_DA() 的上升沿触发处
main.c 298后 (无) 添加 Led_Disp(ucLed);

五、经验教训

  1. 函数职责单一Led_Proc() 只负责 LED 显示,不能包含数据采集;AD_DA() 负责采集触发。

  2. 事件触发 vs 轮询:数据采集是"一次性事件"(光照上升沿),不能放在定时轮询函数里反复执行。

  3. 小数点显示方法:蓝桥杯模板通过 Seg_Buf[i] > 20 判断是否显示小数点,触发方式是 数字 + ','(+44),必须是加法

  4. 复制粘贴后必须检查差异Seg_Buf[6]Seg_Buf[7] 只差一个 /10%10%10,复制后忘记修改是常见错误。

  5. 类型选择:累加变量要预估最大值,unsigned char 仅 255,温度累加 10 次以上就必须用 unsigned int


:trophy: 第十四届蓝桥杯单片机省赛 - 开发排雷笔记

:bug: 1. 数组初始化引起的“雪崩”报错

  • 问题描述:编译时报出几十行 redefinitionsyntax error,指向 seg.cmain.c

  • 原因解析:在定义段码表数组 seg_dula 时,误将逗号 , 打成了小数点 .(如 0x89.0x8e)。C 语言解析到非法的 .0 后语法彻底崩溃,导致后续所有代码都被当成了全局变量的非法声明。

  • 解决方案:将小数点改回逗号。

C

// ❌ 错误写法
pdata unsigned char seg_dula[]={..., 0xbf,0xc6,0x89.0x8e,0x8c,0x86,0x88};
// ✅ 正确写法
pdata unsigned char seg_dula[]={..., 0xbf,0xc6,0x89,0x8e,0x8c,0x86,0x88};

:puzzle_piece: 2. switch-case 语法错误与变量赋值空缺

  • 问题描述:报 error C141: syntax error near 'break', expected 'sizeof'near ';'

  • 原因解析

    1. 上一行代码漏写了分号 ;

    2. 变量赋值等号 = 后面直接跟着分号 ;,没有填入具体数值(如 Seg_Buf[6]= ;)。

  • 解决方案:补全分号,并为暂未写明逻辑的变量填入占位符(如 10 代表数码管熄灭)。

:light_bulb: 3. LED 数组与数码管数组“张冠李戴”

  • 问题描述:时间数据赋给了 ucLed 数组,导致数码管不显示时间,而 LED 灯群魔乱舞。

  • 原因解析:定时器扫描用的是 Seg_Buf,但准备显示数据时错用成了控制 LED 的 ucLed 数组。

  • 解决方案:在 Seg_Proc() 中,将对应数码管显示的变量全部替换为 Seg_Buf

:joystick: 4. 复杂按键状态机(短按、长按、松开分离)

  • 问题描述:S9 键的长按清零逻辑无效,且在其他界面误触会导致参数归零;switch(Key_Down) 内部嵌套错误。

  • 原因解析

    1. Key_Down 只在按下瞬间为真,无法在其中判断长按。

    2. 缺少针对不同界面的严格隔离(Seg_Mode 判断位置不对)。

    3. 参数边界没有正确锁死,乱用 else 导致误清零。

  • 解决方案:将瞬间按下、持续按下、松开瞬间严格分离。采用“只写通过条件”的方式实现边界锁死。

C

// ================= 按键逻辑终极方案 =================
void Key_Proc()
{
    if (Key_Slow_Down < 10) return;
    Key_Slow_Down = 0;
​
    Key_Val = Key_Read();
    Key_Down = Key_Val & (Key_Val ^ Key_Old);
    Key_Up = ~Key_Val & (Key_Val ^ Key_Old);
    Key_Old = Key_Val;
​
    if (Seg_Mode != 3) // 温湿度界面下,所有按键无效
    {   
        // 1. 处理短按 (瞬间触发)
        switch(Key_Down)
        {
            case 4:
                Seg_Mode = (++Seg_Mode) % 3; // 切换界面
                break;
            case 5:
                if (Seg_Mode == 1) Mode1 = (++Mode1) % 3; // 切换子界面
                break;
            case 8:
                if (Seg_Mode == 2 && Temperature_Ctrl < 99) // 边界锁死:只允许<99时加
                    Temperature_Ctrl++;
                break;
            case 9:
                if (Seg_Mode == 2 && Temperature_Ctrl > 0) // 边界锁死:只允许>0时减
                    Temperature_Ctrl--;
                break;
        }
​
        // 2. 处理按键抬起 (长按松开瞬间)
        if (Key_Up == 9) 
        {
            if (Seg_Mode == 1 && Mode1 == 2) 
            {
                if (Time_2s >= 200) // 200 * 10ms = 2000ms
                {
                     Temperature_Max = 0;
                     Temperature_Average_100x = 0;
                     Temp_Sum = 0; // 核心:累加器也要清零!
                     Wet_Max = 0;
                     Wet_Average_100x = 0;
                     Wet_Sum = 0;
                     Num = 0;
                     Num_ucRtc[0] = 0; Num_ucRtc[1] = 0; Num_ucRtc[2] = 0; 
                }
            }
            Time_2s = 0; // 松开后复位计时器
        }
        
        // 3. 处理长按过程 (持续按下)
        if (Key_Val == 9) Time_2s++; // 每 10ms 加 1
        else if (Key_Val != 9) Time_2s = 0;
    }
}

:horizontal_traffic_light: 5. LED 报警指示灯的“清零打底”法则

  • 问题描述:LED 报警解除后灯不灭,或者切换界面后多个状态灯同时常亮。

  • 原因解析:全局数组会保留历史状态。如果不每次“擦黑板”,上一个界面的高电平会一直残留。

  • 解决方案:在 Led_Proc() 开头,先把所有受控灯清零(打底),然后再根据当前状态重新赋值。

C

void Led_Proc()
{
    // 第一步:全部清零打底 
    ucLed[0] = 0; ucLed[1] = 0; ucLed[2] = 0; ucLed[3] = 0; ucLed[4] = 0; ucLed[5] = 0;
    
    // 第二步:界面指示灯
    switch(Seg_Mode)
    {
        case 0: ucLed[0] = 1; break;
        case 1: ucLed[1] = 1; break;
        case 3: ucLed[2] = 1; break;
    }
​
    // 第三步:报警灯
    if (Temperature > Temperature_Ctrl) ucLed[3] = Flag_Time_100ms; // L4 闪烁
    if (Flag_Wet == 0) ucLed[4] = 1; // L5 无效报警
    if (Num >= 2 && Temperature > Temperature_Old && Wet > Wet_Old) ucLed[5] = 1; // L6 趋势报警
}

:droplet: 6. NE555 频率转湿度与数学转换

  • 问题描述:如何将获取到的频率转换为湿度并判断有效性。

  • 原因解析:需利用一次函数两点式求出线性方程。公式推导后为 $Humidity = \frac{4}{90} \times (Freq - 200) + 10$。

  • 解决方案:编写转换函数,不在范围内的频率标记为无效。

C

void Get_Wet()
{
    if (Freq >= 200 && Freq <= 2000) 
    {
        Flag_Wet = 1; 
        Wet = (Freq - 200) * 4 / 90 + 10; // 先乘后除防止截断
    }
    else 
    {
        Flag_Wet = 0; 
        Wet = 0; 
    }
}

:sun: 7. PCF8591 光敏触发与 3 秒界面锁死状态机

  • 问题描述:如何检测“由亮变暗”并强制切换界面 3 秒,且 3 秒内不再触发。

  • 原因解析:需要结合边沿检测(当前暗且上一次亮)、标志位加锁(Flag_3s_Lock)、界面备份(Seg_Mode_Backup)和定时器累加来实现。

  • 解决方案

C

// ================= AD读取与核心业务逻辑 =================
void AD_DA()
{
    if (AD_DA_Slow_Down < 120) return;
    AD_DA_Slow_Down = 0;
  
    AD_Data = Ad_Read(0x01); // 0x01 为光敏电阻通道
​
    if (AD_Data > 100) Light_State_Current = 1; // 挡光 -> 暗
    else Light_State_Current = 0;               // 未挡光 -> 亮
​
    // 捕捉下降沿
    if (Light_State_Old == 0 && Light_State_Current == 1) 
    {
        if (Flag_3s_Lock == 0) 
        {
            Flag_3s_Lock = 1;  
            Time_3s_Count = 0; 
            Seg_Mode_Backup = Seg_Mode; 
            Seg_Mode = 3;               // 跳转到温湿度界面
​
            // === 核心业务:触发后更新数据 ===
            if (Flag_Wet == 1) 
            {
                Temperature_Old = Temperature;
                Wet_Old = Wet;
                Num++;
                Num_ucRtc[0] = ucRtc[0]; Num_ucRtc[1] = ucRtc[1]; Num_ucRtc[2] = ucRtc[2];
​
                if (Temperature > Temperature_Max) Temperature_Max = Temperature;
                if (Wet > Wet_Max) Wet_Max = Wet;
​
                Temp_Sum += Temperature;
                Wet_Sum += Wet;
                // 放大10倍,方便后续显示一位小数
                Temperature_Average_100x = (Temp_Sum * 10) / Num; 
                Wet_Average_100x = (Wet_Sum * 10) / Num; 
            }
        }
    }
    Light_State_Old = Light_State_Current; // 刷新历史状态
}

(注:3 秒计时的具体 Time_3s_Count >= 3000 复位逻辑放在定时器中断中即可)

:1234: 8. 整型运算中的“保留一位小数”魔术

  • 问题描述:单片机整数除法会丢失小数部分,如何在数码管上显示带 1 位小数的平均值。

  • 原因解析:直接除会抹零。需要将总和先乘以 10 再相除,将小数位挤到“个位”上来保存。

  • 解决方案:提取数值时重算位权,并在十位(实际的个位)加上 20(利用中断里 >20 点亮小数点的逻辑)。

C

// 截取自 Seg_Proc() 的 case 1 -> case 0 (温度回显)
// 假设 Temperature_Avg_100x 是放大了10倍的平均值 (例如 235 代表 23.5)
Seg_Buf[5] = (Num==0) ? 10 : Temperature_Average_100x / 100 % 10;      // 拿百位(2)
Seg_Buf[6] = (Num==0) ? 10 : Temperature_Average_100x / 10 % 10 + 20;  // 拿十位(3)并点亮小数点
Seg_Buf[7] = (Num==0) ? 10 : Temperature_Average_100x % 10;            // 拿个位(5)

如果有哪里还有细节想要微调,随时来找我探讨哦!祝你在比赛中代码一遍过,满血拿奖!