蓝桥杯单片机第十四届省赛难点剖析与满分代码实现
1. 赛题难点深度解析
第十四届蓝桥杯单片机省赛题目确实有点难度,我做完之后发现有几个地方特别容易踩坑。这次比赛用到了PCF8591、NE555N、DS18B20、DS1302这几个外设,都是平时常见的模块,但组合起来就有不少 tricky 的地方。
先说说湿度测量这部分,题目要求通过NE555N的频率来推算湿度值。这里有个线性转换关系:当频率在200Hz到2000Hz之间时,湿度范围是10%~90%。这个计算本身不难,但很多人忽略了无效数据的处理。当湿度值超出有效范围(小于等于10%或大于等于90%)时,需要显示"AA"而不是实际数值,这个细节在测评中很容易被遗漏。
平均温湿度的计算也是个坑。我第一次做的时候,前两次计算结果都是对的,还以为没问题了。后来发现第三次开始就不对了,原来是忘了把之前的平均值乘以次数。正确的算法应该是:新平均值 = (旧平均值 × 旧次数 + 本次测量值) / (新次数)。这个公式要记牢,不然数据会越算越偏。
光敏触发机制设计得挺巧妙的。要求在三秒内不能重复采集,这个需要用状态机的方式来处理。我用了下降沿检测的方式,配合一个标志位来判断是否允许采集。关键是要在光敏电阻从亮变暗的瞬间触发采集,而且只能触发一次。
2. 湿度-频率转换算法实现
湿度测量这块是本届省赛的重点难点。NE555N输出的频率信号需要通过定时器来捕获,然后转换成湿度值。我用的STC15系列单片机,开启定时器0的计数模式,每1秒读取一次计数值。
void Timer0Init(void) //0毫秒@12.000MHz
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x05; //设置定时器模式为计数模式
TL0 = 0x00; //设置定时初值
TH0 = 0x00; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
}
每秒钟读取一次计数值,这个值就是频率。然后按照题目给的公式进行转换:
if(Freq <= 200)
Hum = 10;
else if(Freq >= 2000)
Hum = 90;
else if(Freq < 2000 && Freq > 200)
Hum = (90.0-10.0)/(2000-200)*(Freq-200) + 10;
这里要注意数据类型的选择。我用的是float类型,因为计算过程中有小数运算。如果用整型的话,精度会不够,导致转换结果有偏差。
在实际测试中,我发现NE555N的输出频率会有小幅波动,所以最好加上软件滤波。我用了滑动平均滤波,取最近5次的平均值,这样显示出来的湿度值就稳定多了。
3. 动态平均温湿度计算技巧
平均值的计算看似简单,其实暗藏玄机。很多人直接用累加再除以次数,这样确实可以算出平均值,但存在两个问题:一是数据溢出风险,二是无法实现动态更新。
我采用的算法是递推平均法,每次更新时都不需要保存所有历史数据,只需要记住上次的平均值和总次数:
// 温度平均值更新
AVG_Input[0] = (AVG_Input[0]*AVG_Input_Index + T_Disp)/(AVG_Input_Index + 1);
AVG_Input_Index++;
// 湿度平均值更新
AVG_Input[1] = (AVG_Input[1]*AVG_Input_Index_Hum + Hum_Disp)/(AVG_Input_Index_Hum + 1);
AVG_Input_Index_Hum++;
这种方法的好处是节省内存,只需要存储两个变量(当前平均值和总次数)就能持续更新平均值。特别是在单片机这种资源受限的环境中,这种算法特别实用。
还有一个细节要注意:只有当湿度值在有效范围内(10%~90%)时才参与平均值计算。如果湿度无效,不仅要显示"AA",还要跳过平均值计算:
if(Hum_Disp > 10 && Hum_Disp < 90)
{
// 参与平均值计算
AVG_Input[0] = (AVG_Input[0]*AVG_Input_Index + T_Disp)/(AVG_Input_Index + 1);
AVG_Input_Index++;
}
我在调试过程中发现,如果忘了加这个条件,无效数据也会被计算进去,导致平均值失真。测评时这个错误很隐蔽,因为前几次测量可能都是有效数据,等到出现无效数据时才会暴露问题。
4. 光敏触发与防重入机制
光敏触发这部分设计得很巧妙,要求只有在温湿度界面且光敏电阻被遮挡(由亮变暗)时才触发采集,而且3秒内不能重复触发。
我用了状态机的方式来实现这个功能。首先定义了两个状态变量:SMG_Mode表示当前是否在温湿度界面,Tick_Mode表示是否允许触发采集。
if(SMG_Mode == 0 && Voltage > 80) // 在温湿度界面且光敏电阻亮
{
Tick_Mode = 1; // 允许触发标志置一
}
else if(Tick_Mode == 1 && Voltage < 80) // 允许触发且光敏电阻变暗
{
// 执行数据采集
T_Disp = T;
Hum_Disp = Hum;
SMG_Mode = 1;
// ...其他处理代码
Tick_Mode = 0; // 触发后立即禁止再次触发
Time_3000ms = 0; // 重置3秒计时器
}
防重入机制是通过Time_3000ms计时器实现的。触发采集后,界面会切换到采集显示模式,并开始计时:
if(SMG_Mode == 1 && Voltage > 80) // 在采集显示模式且光敏电阻亮
{
if(++Time_3000ms == 3000) // 计时3秒
{
Time_3000ms = 0;
SMG_Mode = 0; // 返回正常界面
}
}
这样设计的好处是,即使用户一直遮挡光敏电阻,也不会重复触发采集。只有等到3秒超时后,再次经历从亮到暗的变化过程,才会触发下一次采集。
5. 按键长按功能实现
长按功能在参数设置界面用到,要求长按2秒以上才能重置系统。这个功能看似简单,但实现起来有几个细节要注意。
我用了Key_Flag作为长按标志,Time_2000ms作为计时器。在按键处理函数中:
if(Key_Old == 9) // 按键按下
Key_Flag = 1; // 开始计时
else if(Key_Flag) // 按键松开
{
Key_Flag = 0;
if(Time_2000ms == 2000) // 如果计时达到2秒
{
// 执行重置操作
Tick = 0;
ARG_Ctrol_T = 30;
// ...其他重置代码
}
Time_2000ms = 0; // 重置计时器
}
在中断服务函数中计时:
if(Key_Flag == 1)
{
if(++Time_2000ms >= 2000)
{
Time_2000ms = 2000; // 达到2秒后不再增加
}
}
这里有个技巧:计时达到2秒后,将时间固定在2000,这样可以避免持续累加导致溢出。同时,只有在按键松开后才判断是否达到长按时间,这样就实现了长按功能。
在实际测试中,我发现需要加上去抖动处理,否则容易误触发。我在按键扫描函数中加了10ms的减速变量,有效解决了抖动问题。
6. 报警指示灯控制逻辑
报警指示灯有两个:LED4用于温度超限报警,LED6用于温湿度同时超过历史最大值报警。
LED4的控制逻辑相对简单:
ucLED[3] = (T_Disp > ARG_Ctrol_T)*LED4_Flag;
这里用了两个条件:一是当前温度超过设定阈值,二是闪烁标志位LED4_Flag为1。LED4_Flag在100ms中断中翻转,实现闪烁效果:
if(++Time_100ms == 100)//LED4闪烁
{
Time_100ms = 0;
LED4_Flag^=1;
}
LED6的逻辑复杂一些,需要满足三个条件:一是温湿度同时超过历史最大值,二是采集次数大于等于2(避免第一次采集就触发),三是采集到的数据有效:
if(T > T_Disp && Hum > Hum_Disp)
LED6_Flag = 1;
else
LED6_Flag = 0;
// ...
ucLED[5] = (Tick >=2 && LED6_Flag == 1);
这里有个细节:比较的是当前测量值和上次显示值,而不是和历史最大值比较。这是因为每次采集后都会更新显示值,所以实际上就是在和历史最大值比较。
我在调试时发现,如果没有Tick >=2这个条件,上电后的第一次采集就可能触发报警,因为初始值为0,任何测量值都会大于0。加上这个条件后,只有在第二次及以后的采集才可能触发报警,符合题目要求。
7. 无效数据处理与显示
无效数据的处理是本题的一个难点,主要体现在两个方面:一是湿度值超出有效范围时要显示"AA",二是无效数据不参与统计计算。
在湿度显示部分,需要判断湿度值是否有效:
if(Hum_Disp > 10 && Hum_Disp < 90)
{
SMG_Buff[6]=Hum_Disp/10%10;
SMG_Buff[7]=Hum_Disp%10;
}
else
{
SMG_Buff[6]=17; // 显示"A"
SMG_Buff[7]=17; // 显示"A"
}
这里用了数字17来显示字母"A",需要提前在数码管段码表中定义好A的显示码。
在统计数据计算部分,也要排除无效数据:
if(Hum_Disp > 10 && Hum_Disp < 90)
{
// 参与最大值统计
if(MAX[0] < T_Disp)
MAX[0] = T_Disp;
if(MAX[1] < Hum_Disp)
MAX[1] = Hum_Disp;
// 参与平均值计算
AVG_Input[0] = (AVG_Input[0]*AVG_Input_Index + T_Disp)/(AVG_Input_Index + 1);
AVG_Input_Index++;
// 更新触发时间
for(i=0;i<3;i++)
ucRtc_MAX[i] = ucRtc[i];
}
这个判断非常重要,如果忘了加,无效数据也会被统计进去,导致最大值和平均值计算错误。我在第一次实现时就漏掉了这个判断,调试了好久才发现问题。
8. 完整代码实现与优化建议
虽然给出了完整代码,但我想分享几个优化建议。首先是变量命名,原代码中的变量名可以更直观一些,比如AVG_Input可以改为AverageValue,ARG_Ctrol_T可以改为TemperatureThreshold,这样代码可读性会更好。
其次是模块化设计,可以把功能相关的代码封装成函数,比如:
// 读取传感器数据
void ReadSensors()
{
Read_Rtc(ucRtc);
T = Read_t();
Voltage = Ad_Read(0x41);
// 频率转湿度计算
if(Freq <= 200)
Hum = 10;
else if(Freq >= 2000)
Hum = 90;
else
Hum = (90.0-10.0)/(2000-200)*(Freq-200) + 10;
}
// 更新平均值
void UpdateAverage()
{
if(Hum_Disp > 10 && Hum_Disp < 90)
{
AVG_Input[0] = (AVG_Input[0]*AVG_Input_Index + T_Disp)/(AVG_Input_Index + 1);
AVG_Input_Index++;
AVG_Input[1] = (AVG_Input[1]*AVG_Input_Index_Hum + Hum_Disp)/(AVG_Input_Index_Hum + 1);
AVG_Input_Index_Hum++;
}
}
这样重构后,主循环会更加清晰:
void main()
{
// 初始化代码
while(1)
{
Key_Prco();
ReadSensors();
ProcessData();
UpdateDisplay();
}
}
最后是增加注释,特别是对于复杂的算法和状态机,详细的注释有助于后期维护和调试。比如在光敏触发部分,可以注明各个状态变量的含义和转换条件。
在实际比赛中,时间有限可能做不到这么完善,但基本的代码规范和模块化思想还是应该坚持的。好的代码结构不仅能减少错误,也便于调试和查错。
更多推荐


所有评论(0)