目录
前言
目前能分享的处理方式前边都已经分享过了,这里还是只分享本届赛题中一些独特的处理方式。做过十四届赛题之后,感觉前边的题目都很简单,所以在省赛之前,可能会刷一些国赛题来代替省赛。关于选择题的部分,还是不再分享了,感兴趣的可以自己看看。
如果今年省赛之前,公布了新的资源数据包,或者有了新的要求,作者也会第一时间如何使用新的资源数据包完成我之前写的代码的相关修改。
这里还是以第十四届资源数据包为基础:
单片机资源数据包_2023
一、第十三届比赛省赛
1.比赛题目
2.赛题解读
本届比赛只有三个菜单,比起十四届的六个菜单,真是简单了许多。
本届比赛使用到了继电器,虽然最开始学的时候也已经介绍了继电器,但是说实话用的次数不多,而且原理与LED和数码管想通,所以后续就很少介绍了,甚至上一篇的省三模板代码也没提到继电器。本篇文章会再次重温一下继电器的使用。
其他的要就就是中规中矩了,有一个长按S17显示分秒,正常情况显示时分的功能,实现起来也比判断短按按键和判断长按按键简单的多。而且好像去年比赛也用到了这个功能。
二、部分功能实现
1.继电器的开启与关闭
开启/关闭继电器其实和点亮/熄灭一个LED灯的道理是一样的,只是操纵的锁存器不一样而已,而且中间多了一个芯片用于大电流驱动,但是这个芯片几乎不影响处理。
左边的锁存器就是我们用来控制及继电器和电机的。右边的ULN2003是一个常用的达林顿晶体管阵列,它主要用于放大输入信号,以驱动继电器或其他大电流负载。**需要注意到是,ULN2003芯片输入高电平时,输出的是低电平,输入低电平时,输出的是高电平。**比如现在ULN的IN5是高电平,那么对应的OUT5输出的就是低电平。
下图是蜂鸣器的原理图:
其中N RELAY就是信号输入引脚,不难看出,当N RELAYT引脚为低电平时,有电流流过继电器,继电器闭合,结合上边关于锁存器和ULN的原理图,N RELAY连接的是ULN的OUT5(N RELAY有用红色框框标注欧),OUT5对应ULN的IN5,而IN5连接的是锁存器的Q5,锁存器Q5对应的输入是D5,D5连接的是P04,也就是说,我们只需要把P04置为高电平,然后开关一下锁存器让数据保存进去,就可以完成继电器的开启,同理,将P04置为低电平,然后开关一下锁存器,就可以将锁存器熄灭。是不是跟点亮一个LED灯一样呢,只是说点亮一个LED灯需要给一个低电平,熄灭给高电平,而继电器和蜂鸣器则是高电平闭合/响,低电平不闭合/不响。
至于锁存器的开启与关闭相比大家应该都已熟知,控制用来存储ULN数据的锁存器的使能端连接的是Y5C接口(上边图片也有标注),高电平有效,我们要想开启ULN连接的锁存器,只需要将ULN连接的锁存器的使能端置为高电平,在置为低电平,即完成了一次开关。
而Y5C接口连接的是三八译码器(右边的或非门可以选择性忽视)。三八译码器的输出是P25 P26和P27,当P27 P26 P25为101时(也就是十进制的5),就可以使得Y5C为高电平,同理,如果当P27 P26 P25不为101时,Y5C输出的就是低电平,这样我们就完成了一次开关连接ULN的锁存器。
当然,开关锁存器之前我们要先给P0置为。我们可以向控制LED灯一样控制继电器,定义一个变量用来记录ULN的状态。然后通过宏函数开改变ULN中对应的值,并且开关一次锁存器。代码如下:
unsigned char ULN=0x00;//ULN芯片状态(控制继电器和蜂鸣器)
#define RELAY_ON() ULN|=0x01<<4; P0=ULN;P2|=0xA0;P2&=0xBF;P2&=0x1F;
#define RELAY_OFF() ULN&=~(0x01<<4); P0=ULN;P2|=0xA0;P2&=0xBF;P2&=0x1F;
2.长按切换显示状态功能的实现
先看题目要求
简单点说就是按下S17之后,显示分秒,其他状态下显示时分。
不需要判断长短按,反正就是按下去之后切换一下显示,我们直接在按下按键的while循环中加上我们需要显示的数据即可(由于就这一个长按,所以长按时显示的数据就没写在菜单显示函数里了),同时不再调用菜单显示函数即可(避免发生冲突)。注意这个长按功能仅在时间显示菜单有效。
P34=0;
if(P32==0)
{
Delay5ms();
while(P32==0)//如果长按了17,由于只在长按时有效果,所以就把菜单直接显示在这里了
{//一旦跳出这个循环,show_menu函数就会开始运行,这个临时菜单就会被清除
if(mod==1)
{
Nixie_num[0]=21;//U
Nixie_num[1]=2;//2
Nixie_num[2]=20;
Nixie_num[3]=Time[1]/10%10;//分钟
Nixie_num[4]=Time[1]/1%10;
Nixie_num[5]=22;
Nixie_num[6]=Time[0]/10%10;//秒
Nixie_num[7]=Time[0]/1%10;
}
}
Delay5ms();
key_value=17;
}
3.温度传感器小数部分的读取
温度传感器的指令就不在说了,忘记的可以看下边这篇文章:
蓝桥杯电子类单片机学习二------DS18B20温度传感器(onewire驱动)_蓝桥别ds18b20驱动文件是要自己写吗-CSDN博客
这里只回忆一下用于存储寄存器。温度数据的存储使用了两个8为寄存器,分别即为高八位(high)和低八位(low)。
高八位的高四位是符号位,为0时说明是正温度;高八位的低四位和低八位的高四位合起来也刚好是八位,是温度的整数部分;低八位的低四位是小数部分,精度可以达到1/16=0.0625.
之前我处理温度传感器时,都是只取高八位的低四位和低八位的高四位作为返回值,也就是返回的是温度的整数部分:
temp=high;
temp&=0x0F;
temp<<=8;
temp|=low;
temp>>=4;
return temp;
现在我们需要连同返回温度的小数部分,题目是只要求精确到小数点后一位,所以我们只需要将温度数据扩大十倍,并且将温度的小数部分最高位给加上去即可,这样就做到了保留小数点后一位。小数部分是低八位的低四位,只需要取出低八位的低四位,让它x10*/16即可。*10是为了扩大十倍,/16十位是因为最开始的精度是1/16度。
最开始学的时候,老师都是教的温度扩大16倍,读取温度之后,再把温度值乘以个1/16,我感觉很正式但是确实十分繁琐,尤其是我写的代码只为了应对这场比赛时,这样做就显得没太多必要。
temp=high;
temp&=0x0F;
temp<<=8;
temp|=low;
temp>>=4;
xiaoshu=low;
xiaoshu&=0x0F;//提取温度的小数部分
temp=temp*10+xiaoshu*10/16;
return temp;
4.其他处理
关于这些特殊处理肯定有许多,也肯定有一些是大家比较好奇的,但是之前已经分享过许多了,这里就只在提一下,需要的可以去查看对应的文章。
1)数码管显示小数的处理
我是把断码表0-9显示数字0到9,10-19显示带小数点的0到9,也就也是0. 1.等等。这样如果我需要显示小数点,比如需要显示num的十位,并且加上小数点,只需要
NIxie_num[0]=num/10%10+10;
即可。
对应的Seg_Table为
code unsigned char Seg_Table[] =
{
0xc0, //0
0xf9, //1
0xa4, //2
0xb0, //3
0x99, //4
0x92, //5
0x82, //6
0xf8, //7
0x80, //8
0x90, //9
0x40,0x79,0x24,0x30,0x19,0x12,0x02,0x78,0x00,0x10,
};
2)5s后继电器开启的处理
这里的处理主要是指5s,我们定义一个标志位is_5s,如果is_5s被置为0之后,5s后,定时器会把is_5s置为1.我们在需要开始计时的地方加上is_5s=0;然后开始检查is_5s,当is_5s变为1之后,说明已经过去5s了,具体在定时器1里的实现如下:
unsigned int count_5s=1;
void Timer0_Isr(void) interrupt 1
{
P0=0x01<<location;NIXIE_CHECK();
P0=Seg_Table[Nixie_num[location]];NIXIE_ON();
if(++location==8)
location=0;
if(is_5s==0)//如果标志位被置为0,
{
if(++count_5s==5000)//5s后将标志位置为1
{
is_5s=1;
count_5s=0;
}
}
}
这个的处理思路可以应用到包括长按短按按键,各种几秒后怎么怎么样等。当然关于这个的处理不唯一,虽然我这样写的可能会比较繁琐,但我敢保证我的处理方式绝对不会出问题,我也一直在用这种处理方式。
3)点亮熄灭LED灯的策略
我对于点亮/熄灭LED的处理都是定义一个标志位,用于记录某个LED灯处理点亮还是熄灭状态,如果满足点亮某个LED灯的条件,并且这个LED灯处于熄灭状态,则点亮这个LED灯,并且修改标志位;如果满足熄灭某个LED的条件,并且这个LED灯处于点亮状态,则熄灭这个LED灯,并且修改标志位。
这样子处理可以避免重复熄灭或点亮时,造成LED灯异常闪烁或者出现残影,也可以保证当LED灯停止闪烁之后,LED灯总是处在熄灭状态。
我们假设点亮LED的条件为tiaojian,tiaojian为1时说明需要点亮LED灯1,否则需要熄灭LED1,则根据上述可以写出以下代码:
bit L1_is_on=0;//LED灯状态标志位,为0说明未点亮,为1说明点亮了
void Led_run(void)
{
if(L1_is_on==0&&tiaojian==0)//如果点亮满足条件,并且LED并未点亮
{//则点亮LED灯
LED_ON(0);
L1_is_on=1;
}
else if(L1_is_on==1&&tiaojian==1)//如果满足熄灭条件,并且LED灯未熄灭
{//则熄灭
LED_OFF(0);
L1_is_on=0;
}
}
三、完整代码演示
main.c
cpp
#include <stc15.h>
#include <intrins.h>
#include "onewire.h"
#include "ds1302.h"
code unsigned char Seg_Table[] =
{
0xc0, //0
0xf9, //1
0xa4, //2
0xb0, //3
0x99, //4
0x92, //5
0x82, //6
0xf8, //7
0x80, //8
0x90, //9
//0.到9.
0x40,0x79,0x24,0x30,0x19,0x12,0x02,0x78,0x00,0x10,
0xFF,//20 熄灭
0xC1,//21 U
0xBF//22 -
};
unsigned char Led_Num=0xFF;//LED灯状态
unsigned char ULN=0x00;//ULN芯片状态(控制继电器和蜂鸣器)
#define LED_ON(x) Led_Num&=~(0x01<<x);P0=Led_Num;P2|=0x80;P2&=0x9F;P2&=0x1F;
#define LED_OFF(x) Led_Num|=0x01<<x; P0=Led_Num;P2|=0x80;P2&=0x9F;P2&=0x1F;
#define LED_OFF_ALL() Led_Num=0xFF; P0=Led_Num;P2|=0x80;P2&=0x9F;P2&=0x1F;
#define RELAY_ON() ULN|=0x01<<4; P0=ULN;P2|=0xA0;P2&=0xBF;P2&=0x1F;
#define RELAY_OFF() ULN&=~(0x01<<4); P0=ULN;P2|=0xA0;P2&=0xBF;P2&=0x1F;
#define NIXIE_CHECK() P2|=0xC0;P2&=0xDF;P2&=0x1F;
#define NIXIE_ON() P2|=0xE0;P2&=0xFF;P2&=0x1F;
void Timer0_Init(void); //1毫秒@12.000MHz
void Delay100ms(void); //@12.000MHz
void get_key(void);//读取按键
void show_menu(void);//显示菜单
void run(void);
void Led_run(void);//LED控制
unsigned char Nixie_num[]={20,20,20,20,20,20,20,20};
unsigned char location=0;//当前扫描到数码管的位置,中间变量
unsigned char key_value=0;//读取到的键值
unsigned char mod=0;//菜单模式
unsigned char temp_canshu=23;//温度参数
unsigned int temp=0;//读取到的温度,已扩大十倍
bit work=0;//工作状态 0温度控制 1时间控制
void main()
{
LED_OFF_ALL();//熄灭所有LED灯
ds_init();//初始化ds1302
read_temp();//上电读取一次温度传感器
RELAY_OFF();//关闭继电器
Delay100ms();
Timer0_Init();//定时器0初始化
EA=1;
while(1)
{
run();
get_key();
show_menu();
}
}
bit relay_is_on=0;
bit is_5s=0;
void run()
{
temp=read_temp();//读取温度
ds_read();//读取时间
Led_run();//控制LED灯
if(Time[1]==0&&Time[0]==0)//如果到了整点
{//由于下边有两处都需要判断整点,所以把整点的判断放在了work上边
is_5s=0;//将5s标志位置为0,5s后标志位会被置为1
}
if(work==0)//如果处在时间控制模式下
{
//如果继电器处于关闭状态,并且当前读到的温度高于温度参数(注意读取到的温度为方便处理,以扩大十倍)
if(relay_is_on==0&&temp/10>temp_canshu)
{//则打开继电器
RELAY_ON();
relay_is_on=1;
}
//如果当前继电器处于开启状态,并且读取到的温度低于温度参数
else if(relay_is_on==1&&temp/10<temp_canshu)
{//则熄灭继电器
RELAY_OFF();
relay_is_on=0;
}
}
else if(work==1)//如果处在时间控制模式下
{
if(is_5s==0&&relay_is_on==0)
{
RELAY_ON();//开启继电器
relay_is_on=1;
}
else if(relay_is_on==1&&is_5s==1)//如果已经到了5s并且继电器还没关闭
{//则关闭继电器
RELAY_OFF();
relay_is_on=0;
}
}
Delay100ms();
}
unsigned int count_5s=1;
void Timer0_Isr(void) interrupt 1
{
P0=0x01<<location;NIXIE_CHECK();
P0=Seg_Table[Nixie_num[location]];NIXIE_ON();
if(++location==8)
location=0;
if(is_5s==0)//如果标志位被置为0,
{
if(++count_5s==5000)//5s后将标志位置为1
{
is_5s=1;
count_5s=0;
}
}
}
void Timer0_Init(void) //1毫秒@12.000MHz
{
AUXR |= 0x80; //定时器时钟1T模式
TMOD &= 0xF0; //设置定时器模式
TL0 = 0x20; //设置定时初始值
TH0 = 0xD1; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; //使能定时器0中断
}
void Delay100ms(void) //@12.000MHz
{
unsigned char data i, j, k;
_nop_();
_nop_();
i = 5;
j = 144;
k = 71;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void Delay5ms(void) //@12.000MHz
{
unsigned char data i, j;
i = 59;
j = 90;
do
{
while (--j);
} while (--i);
}
void get_key(void)
{
unsigned char key_P3=P3;
unsigned char key_P4=P4;
P3=0xFF;
P4=0xFF;
//按键读取的while(P32==0)中运行了run()函数,使得按下按键时不会影响其他设备的运行
P35=0;
if(P32==0){Delay5ms();while(P32==0){run();}Delay5ms();key_value=13;}
else if(P33==0){Delay5ms();while(P33==0){run();}Delay5ms();key_value=12;}
P35=1;
P34=0;
if(P32==0)
{
Delay5ms();
while(P32==0)//如果长按了17,由于只在长按时有效果,所以就把菜单直接显示在这里了
{//一旦跳出这个循环,show_menu函数就会开始运行,这个临时菜单就会被清除
if(mod==1)
{
run();
Nixie_num[0]=21;//U
Nixie_num[1]=2;//2
Nixie_num[2]=20;
Nixie_num[3]=Time[1]/10%10;//分钟
Nixie_num[4]=Time[1]/1%10;
Nixie_num[5]=22;
Nixie_num[6]=Time[0]/10%10;//秒
Nixie_num[7]=Time[0]/1%10;
}
}
Delay5ms();
key_value=17;
}
else if(P33==0){Delay5ms();while(P33==0){run();}Delay5ms();key_value=16;}
P34=1;
//S12 菜单切换
if(key_value==12)
{
if(mod==0)
mod=1;
else if(mod==1)
mod=2;
else if(mod==2)
mod=0;
}
//S13 工作状态切换
else if(key_value==13)
{
work=~work;
}
//S16 加
else if(key_value==16)
{
if(mod==2)
temp_canshu=temp_canshu<99 ? temp_canshu+1:99;
}
//S17 减 长按的功能在上边
else if(key_value==17)
{
if(mod==2)
temp_canshu=temp_canshu>10 ? temp_canshu-1:10;
}
key_value=0;
P3=key_P3;
P4=key_P4;
}
void show_menu(void)
{
Nixie_num[0]=21;//显示U
Nixie_num[1]=mod+1;//显示菜单数
if(mod==0)
{
Nixie_num[2]=20;//熄灭
Nixie_num[3]=20;//熄灭
Nixie_num[4]=20;//熄灭
Nixie_num[5]=temp/100%10;//显示2
Nixie_num[6]=temp/10%10+10;//显示3.
Nixie_num[7]=temp/1%10;//显示5
}
else if(mod==1)
{
Nixie_num[2]=20;
Nixie_num[3]=Time[2]/10%10;
Nixie_num[4]=Time[2]/1%10;
Nixie_num[5]=22;
Nixie_num[6]=Time[1]/10%10;
Nixie_num[7]=Time[1]/1%10;
}
else if(mod==2)
{
Nixie_num[2]=20;
Nixie_num[3]=20;
Nixie_num[4]=20;
Nixie_num[5]=20;
Nixie_num[6]=temp_canshu/10%10;
Nixie_num[7]=temp_canshu/1%10;
}
}
bit L1_is_on=0;
bit L2_is_on=0;
bit L3_is_on=0;
void Led_run(void)
{
if(L1_is_on==0&&is_5s==0)//如果处在整点之后的5s内
{
LED_ON(0);
L1_is_on=1;
}
else if(L1_is_on==1&&is_5s==1)
{
LED_OFF(0);
L1_is_on=0;
}
if(L2_is_on==0&&work==1)//如果处在时间控制模式,则L2点亮
{
LED_ON(1);
L2_is_on=1;
}
else if(L2_is_on==1&&work==0)
{
LED_OFF(1);
L2_is_on=0;
}
if(relay_is_on==1)//如果继电器闭合,则L3闪烁
{
if(L3_is_on==0)
{
LED_ON(2);
L3_is_on=1;
}
else if(L3_is_on==1)
{
LED_OFF(2);
L3_is_on=0;
}
}
else if(relay_is_on==0&&L3_is_on==1)//如果继电器没有闭合,并且L3被点亮了,则熄灭L3
{
LED_OFF(2);
L3_is_on=0;
}
}
onewire.c
cpp
/* # 单总线代码片段说明
1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。
2. 参赛选手可以自行编写相关代码或以该代码为基础,根据所选单片机类型、运行速度和试题
中对单片机时钟频率的要求,进行代码调试和修改。
*/
#include <stc15.h>
#include <intrins.h>
#include "onewire.h"
sbit DQ=P1^4;
//
void Delay_OneWire(unsigned int t)
{
unsigned char i;
while(t--){
for(i=0;i<12;i++);
}
}
//
void Write_DS18B20(unsigned char dat)
{
unsigned char i;
for(i=0;i<8;i++)
{
DQ = 0;
DQ = dat&0x01;
Delay_OneWire(5);
DQ = 1;
dat >>= 1;
}
Delay_OneWire(5);
}
//
unsigned char Read_DS18B20(void)
{
unsigned char i;
unsigned char dat;
for(i=0;i<8;i++)
{
DQ = 0;
dat >>= 1;
DQ = 1;
if(DQ)
{
dat |= 0x80;
}
Delay_OneWire(5);
}
return dat;
}
//
bit init_ds18b20(void)
{
bit initflag = 0;
DQ = 1;
Delay_OneWire(12);
DQ = 0;
Delay_OneWire(80);
DQ = 1;
Delay_OneWire(10);
initflag = DQ;
Delay_OneWire(5);
return initflag;
}
unsigned int read_temp(void)
{
unsigned char low,high;
unsigned int temp=0;
unsigned char xiaoshu=0;
init_ds18b20();
Write_DS18B20(0xCC);
Write_DS18B20(0x44);
Delay_OneWire(200);
init_ds18b20();
Write_DS18B20(0xCC);
Write_DS18B20(0xBE);
low=Read_DS18B20();
high=Read_DS18B20();
temp=high;
temp&=0x0F;
temp<<=8;
temp|=low;
temp>>=4;
xiaoshu=low;
xiaoshu&=0x0F;//提取温度的小数部分
temp=temp*10+xiaoshu*10/16;
return temp;
}
oneiwre.h
cpp
#ifndef _ONE_WIRE_H_
#define _ONE_WIRE_H_
unsigned int read_temp(void);
#endif
ds1302.c
cpp
/* # DS1302代码片段说明
1. 本文件夹中提供的驱动代码供参赛选手完成程序设计参考。
2. 参赛选手可以自行编写相关代码或以该代码为基础,根据所选单片机类型、运行速度和试题
中对单片机时钟频率的要求,进行代码调试和修改。
*/
#include <stc15.h>
#include <intrins.h>
#include "ds1302.h"
sbit SCK=P1^7;
sbit SDA=P2^3;
sbit RST=P1^3;
unsigned char Time[3]={20,53,23};
//
void Write_Ds1302(unsigned char temp)
{
unsigned char i;
for (i=0;i<8;i++)
{
SCK = 0;
SDA = temp&0x01;
temp>>=1;
SCK=1;
}
}
//
void Write_Ds1302_Byte( unsigned char address,unsigned char dat )
{
RST=0; _nop_();
SCK=0; _nop_();
RST=1; _nop_();
Write_Ds1302(address);
Write_Ds1302(dat);
RST=0;
}
//
unsigned char Read_Ds1302_Byte ( unsigned char address )
{
unsigned char i,temp=0x00;
RST=0; _nop_();
SCK=0; _nop_();
RST=1; _nop_();
Write_Ds1302(address);
for (i=0;i<8;i++)
{
SCK=0;
temp>>=1;
if(SDA)
temp|=0x80;
SCK=1;
}
RST=0; _nop_();
SCK=0; _nop_();
SCK=1; _nop_();
SDA=0; _nop_();
SDA=1; _nop_();
return (temp);
}
void ds_init(void)
{
unsigned char add=0x80;
unsigned char i=0;
Write_Ds1302_Byte(0x8E,0x00);
for(;i<3;i++)
{
Write_Ds1302_Byte(add,(Time[i]/10<<4)|(Time[i]%10));
add+=2;
}
Write_Ds1302_Byte(0x8E,0x80);
}
void ds_read(void)
{
unsigned char add=0x81;
unsigned char i=0,dat=0;
for(;i<3;i++)
{
dat=Read_Ds1302_Byte(add);
Time[i]=dat/16*10+dat%16;
add+=2;
}
}
ds1302.h
cpp
#ifndef _DS_1302_H_
#define _DS_1302_H_
extern unsigned char Time[3];
void ds_read(void);
void ds_init(void);
#endif