第十四届蓝桥杯单片机省赛(一)错误分析笔记
题目: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 == 0 的 if 块内:
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 - 44 对 unsigned 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 > 255,unsigned 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); |
五、经验教训
-
函数职责单一:
Led_Proc()只负责 LED 显示,不能包含数据采集;AD_DA()负责采集触发。 -
事件触发 vs 轮询:数据采集是"一次性事件"(光照上升沿),不能放在定时轮询函数里反复执行。
-
小数点显示方法:蓝桥杯模板通过
Seg_Buf[i] > 20判断是否显示小数点,触发方式是数字 + ','(+44),必须是加法。 -
复制粘贴后必须检查差异:
Seg_Buf[6]和Seg_Buf[7]只差一个/10%10和%10,复制后忘记修改是常见错误。 -
类型选择:累加变量要预估最大值,
unsigned char仅 255,温度累加 10 次以上就必须用unsigned int。
第十四届蓝桥杯单片机省赛 - 开发排雷笔记
1. 数组初始化引起的“雪崩”报错
-
问题描述:编译时报出几十行
redefinition和syntax error,指向seg.c和main.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};
2. switch-case 语法错误与变量赋值空缺
-
问题描述:报
error C141: syntax error near 'break', expected 'sizeof'或near ';'。 -
原因解析:
-
上一行代码漏写了分号
;。 -
变量赋值等号
=后面直接跟着分号;,没有填入具体数值(如Seg_Buf[6]= ;)。
-
-
解决方案:补全分号,并为暂未写明逻辑的变量填入占位符(如
10代表数码管熄灭)。
3. LED 数组与数码管数组“张冠李戴”
-
问题描述:时间数据赋给了
ucLed数组,导致数码管不显示时间,而 LED 灯群魔乱舞。 -
原因解析:定时器扫描用的是
Seg_Buf,但准备显示数据时错用成了控制 LED 的ucLed数组。 -
解决方案:在
Seg_Proc()中,将对应数码管显示的变量全部替换为Seg_Buf。
4. 复杂按键状态机(短按、长按、松开分离)
-
问题描述:S9 键的长按清零逻辑无效,且在其他界面误触会导致参数归零;
switch(Key_Down)内部嵌套错误。 -
原因解析:
-
Key_Down只在按下瞬间为真,无法在其中判断长按。 -
缺少针对不同界面的严格隔离(
Seg_Mode判断位置不对)。 -
参数边界没有正确锁死,乱用
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;
}
}
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 趋势报警
}
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;
}
}
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 复位逻辑放在定时器中断中即可)
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)
如果有哪里还有细节想要微调,随时来找我探讨哦!祝你在比赛中代码一遍过,满血拿奖!