参加蓝桥杯单片机决赛的同学们都知道,从省赛到决赛,难度提升不是一星半点。我去年带队参加第十四届比赛时,就深刻体会到模块驱动到系统联调这个过程的挑战。决赛项目不再是简单的功能堆砌,而是要求各个模块能够协同工作,形成一个完整的系统。
单片机决赛项目通常包含数码管显示、温度传感器、按键处理、LED控制等基础模块。但难点在于如何让这些模块"和谐共处"。比如当你在处理按键中断时,数码管显示不能出现闪烁;超声波测距的同时,温度采集也不能中断。这就需要对单片机资源进行合理分配,建立清晰的状态机模型。
在资源分配方面,STC15系列单片机虽然性能不错,但资源依然有限。定时器、中断、IO口都需要精心规划。我的经验是:
这样分配可以最大限度避免资源冲突。记得去年有个队伍就是因为定时器配置不当,导致数码管显示和超声波测距互相干扰,最终成绩大受影响。
数码管显示看似简单,但在决赛中往往会有特殊要求。比如第十四届决赛就要求"高位为0时数码管熄灭",这个功能让不少同学头疼。
传统数码管显示是这样处理的:
c复制Nixie_num[0]=value/1000%10; //千位
Nixie_num[1]=value/100%10; //百位
Nixie_num[2]=value/10%10; //十位
Nixie_num[3]=value%10; //个位
但这样无法实现高位熄灭功能。我当时的解决方案是使用三目运算符动态判断:
c复制Nixie_num[0]=value/1000>0 ? value/1000%10:20; //20表示熄灭
Nixie_num[1]=value/100>0 ? value/100%10:20;
Nixie_num[2]=value/10>0 ? value/10%10:20;
Nixie_num[3]=value%10; //个位始终显示
小数点的处理也有讲究。我扩展了段码表,使每个数字都对应带小数点和不带小数点两个版本:
c复制code unsigned char Seg_Table[] = {
0xc0, //0
0xf9, //1
//... 0-9
0x40, //0.
0x79, //1.
//... 0.-9.
0xFF, //熄灭
0xBF //负号
};
这样要显示带小数点的数字时,只需要在基础值上加10即可,比如Nixie_num[1]=12表示显示"2."。
温度传感器DS18B20的读数处理在决赛中往往要求更高精度。省赛可能只需要整数部分,但决赛通常要求保留小数点后一位。
常规读取方式会丢弃小数部分:
c复制T=high;
T&=0x0F;
T<<=8;
T|=low;
T>>=4; //直接右移4位丢弃小数
为了保留小数,我改进了算法:
c复制unsigned char xiaoshu=low&0x0F; //获取低4位小数部分
temp=temp*10+xiaoshu; //整数部分×10+小数部分
这样处理后,温度值被放大了10倍,既保留了小数精度,又避免了使用浮点数带来的性能损耗。比如实际温度25.6°C,读出来就是256。
在实际应用中要注意:
决赛的按键功能往往比较复杂,比如第十四届就要求:
长按检测的关键是定时器计数。我定义了一个标志位:
c复制bit is_2s_changan=0; //长按2秒标志
在定时器中断中计数:
c复制if(is_2s_changan==0){
if(++count_2s>2000){ //1ms中断×2000=2s
is_2s_changan=1;
count_2s=0;
}
}
按键检测时这样使用:
c复制while(P32==0){ //S9按下
run();
is_2s_changan=0; //重置计数
while(P33==0){ //S8也按下
run();
if(is_2s_changan==1){
restart=1; //触发复位
break;
}
}
}
对于按键失效需求,设置一个标志位即可:
c复制if(is_jilu==1){ //记录状态
key_value=0; //丢弃所有按键
return;
}
LED控制看似简单,但在系统集成时容易出问题。比如题目要求:
直接操作LED的代码优化很重要。我放弃了通用的LED_ON宏,改为直接操作Led_Num变量:
c复制if(mod==10){ //测距界面
Led_Num=~remote; //距离值取反作为LED状态
P0=Led_Num;
//锁存器操作...
}
继电器控制要注意逻辑严谨:
c复制if(remote_canshu-5<=remote
&& remote<=remote_canshu+5
&& temp/10<=wendu_canshu){
RELAY_ON();
}else{
RELAY_OFF();
}
特别要注意的是,继电器状态变化不要太频繁,可以加入防抖处理:
c复制static unsigned char relay_count=0;
if(需要打开){
if(++relay_count>5){ //连续5次检测到才执行
RELAY_ON();
relay_count=0;
}
}
系统联调阶段最容易遇到各种奇怪问题。我总结了几点经验:
一个典型的调试案例是超声波测距不稳定。后来发现是中断优先级问题,调整后代码:
c复制void Timer1_Init(void){ //超声波定时器
AUXR |= 0x40; //1T模式
TMOD &= 0x0F;
TL1 = 0x00;
TH1 = 0x00;
//不开启中断,仅作计时用
}
数码管显示闪烁问题可以通过调整扫描频率解决:
c复制void Timer0_Init(void){ //数码管扫描
AUXR |= 0x80; //1T模式
TMOD &= 0xF0;
TL0 = 0x20; //1ms中断
TH0 = 0xD1;
TR0 = 1;
ET0 = 1;
}
好的代码结构能事半功倍。我的建议是:
模块化编程:每个功能单独成文件
全局变量管理:相关变量组成结构体
c复制struct {
unsigned int temp;
unsigned int remote;
unsigned char mod;
} SystemStatus;
c复制void KeyProcess(void); //按键处理
void DisplayUpdate(void); //显示更新
void SensorRead(void); //传感器读取
c复制/*
* 功能:读取DS18B20温度
* 返回:温度×10,如25.6°C返回256
* 注意:需要至少200ms的转换时间
*/
unsigned int read_temp(void);
决赛时间紧张,提前准备好驱动模板很重要。我通常会准备:
这样比赛时就能专注于业务逻辑开发,省去底层调试时间。去年我们队伍能在决赛中取得好成绩,这套方法功不可没。