今天和大家分享一下2019年第十届蓝桥杯单片机决赛的程序题,我在做这道题的时候属实有被难到,今天就和大家分享一下;先看题目:
4、功能描述
4.1功能概述
1)使用超声波测距单元完成测距功能。
2) 通过DS18B20温度传感器完成温度测量功能。
3)通过 PCF8591 D/A 转换芯片完成模拟电压输出功能。
4)通过AT24C02E2PROM存储器完成参数变动次数记录功能。
5)通过竞赛板上的USB 转串口模块实现串口收发功能。
6) 通过键盘、数码管、LED指示灯等完成人机交互操作。
7) 温度(T)、 距离(S)测量结果刷新时间要求
●温度(T)≤0.5秒。
距离(S)≤1秒。
8)距离(S)测量说明
测量范围要求: 10cm - 50cm。
●声音在空气中的传播速度: 340米/秒。
4.2显示功能
1) 数据界面
一 温度数据显示
温度数据界面如图2所示.显示内容包括提示符C和温度值。温度数据单位为摄氏度C,数据保留小数点后两位有效数字,占用4位数码管。
●距高数据显示
距离数据界面如图3所示,显示内容包括提示符L和距离数据。距离数据单位为cm,可显示距离范围0cm-99cm,占用两位数码管。
变更次数显示
显示内容包括提示符n和参数变动次数。参数变动次数记录范围为0-65535次,占用5位数码管,数据长度不足5位时,高位数码管熄灭。
2) 参数界面
●温度参数
显示内容包括提示符P、参数编号l和温度参数,温度参数可调整范围0-99。
●距离参数
显示内容包括提示符只、参数编号2和距离参数,距离参数可调整范围0-99。
好,所有的显示功能都已经在这里了,那么我们就先把这些显示功能的函数写下来,在写显示函数之前,我们要先把准备工作做好,比如关闭继电器、蜂鸣器等,准备好数码管的段选数组:
typedef unsigned int u16; typedef unsigned char u8; u8 code smgduan[17]= {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07, 0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};//共阴显示0~F的值 void delayms(u16 i) //延迟函数 { u16 aa; while(i--) { for(aa=0;aa<845;aa++); } } void InitHC138(u8 add) //Y4、Y5、Y6、Y7通道选择函数 { switch(add) { case(4): P2 = ( P2 & 0X1F ) | 0X80;break; case(5): P2 = ( P2 & 0X1F ) | 0Xa0;break; case(6): P2 = ( P2 & 0X1F ) | 0Xc0;break; case(7): P2 = ( P2 & 0X1F ) | 0Xe0;break; } } void InitHC573() //关闭蜂鸣器、继电器、LED函数 { InitHC138(5); P0 = 0X00; InitHC138(4); P0 = 0XFF; } void DigDisplay(u8 add,u8 dat) //数码管段选、位选函数 { InitHC138(6); P0 = 0X01 << add; InitHC138(7); P0 = dat; } void GetDatPro() //温度获取 { distance_get = distance; temp_get = Tempget(); temp_get = temp_get*100; } void wave_rec(void) //超声波距离获取 { u8 num = 10; TX = 0; TL0 = 0xF4; //设置定时初值 TH0 = 0xFF; //设置定时初值 TR0 = 1; //定时器0打开 while(num--) { while(!TF0); TF0 = 0; TX ^= 1; } TR0 = 0; //定时器0关闭 TL0 = 0; //设置定时初值 TH0 = 0; //设置定时初值 TR0 = 1; //定时器0打开 while(RX && !TF0); TR0 = 0; if(TF0) { TF0 = 0; distance = 99; } else { distance = ((TH0 << 8) | TL0) * 0.017; if(distance > 99) distance = 99; } }
好,准备工作做好后,就先把显示函数写出来:
u16 distance_get,distance_setup = 35;//定义距离变量和参数 u16 temp_get ,temp_setup = 30; //定义温度变量和参数 u8 setup_count; //设置参数 //===================温度数据显示======================== void DisplayTempGet() { DigDisplay(0,~smgduan[12]); delayms(1); DigDisplay(4,~smgduan[temp_get/1000]); delayms(1); DigDisplay(5,~(smgduan[temp_get/100%10] | 0x80)); delayms(1); DigDisplay(6,~smgduan[temp_get/10%10]); delayms(1); DigDisplay(7,~smgduan[temp_get%10]); delayms(1); } //===================距离数据显示======================== void DisplayDistanceGet() { DigDisplay(0,0xc7); delayms(1); DigDisplay(6,~smgduan[distance_get/10]); delayms(1); DigDisplay(7,~smgduan[distance_get%10]); delayms(1); } //===================参数数据变动显示======================== void DisplaySetupCount() { DigDisplay(0,0xc8); delayms(1); DigDisplay(7,~smgduan[setup_count%10]); delayms(1); if(setup_count>9) { DigDisplay(6,~smgduan[setup_count/10%10]); delayms(1); } if(setup_count>99) { DigDisplay(5,~smgduan[setup_count/100%10]); delayms(1); } if(setup_count>999) { DigDisplay(4,~smgduan[setup_count/1000%10]); delayms(1); } if(setup_count>9999) { DigDisplay(3,~smgduan[setup_count/10000]); delayms(1); } } //===================温度参数显示======================== void DisplayTempSetup() { DigDisplay(0,0x8c); delayms(1); DigDisplay(3,~smgduan[1]); delayms(1); DigDisplay(6,~smgduan[temp_setup/10]); delayms(1); DigDisplay(7,~smgduan[temp_setup%10]); delayms(1); } //===================距离参数显示======================== void DisplayDistanceSetup() { DigDisplay(0,0x8c); delayms(1); DigDisplay(3,~smgduan[2]); delayms(1); DigDisplay(6,~smgduan[distance_setup/10]); delayms(1); DigDisplay(7,~smgduan[distance_setup%10]); delayms(1); }
接下来,我们来看按键模块:
4.3按键功能
1) “短按键”功能说明
●S13:定义为“界面”按键,按下SI3按键,切换数据界面和参数界面,按键S13切换模式如下图所示:
界面切换要求:
1)每次从数据界面进入参数界面,默认当前为温度参数。2)每次从参数界面进入数据界面,默认当前为温度数据。.
●S12:定义为“切换”按键。
在数据界面下,按下S12按键,切换显示温度数据、距离数据和参数变更次数。切换模式如下图所示:
在参数界面下,按下SI2按键,切换显示温度参数和距离参数,切换模式如下图所示:
●S16: 定义为“减”按键。
在温度参数界面下,按下S16,温度参数减少2℃.
在距离参数界面下,按下S16,距离参数减少5cm
●S17: 定义为“加”按键。
在温度参数界面下,按下S17,温度参数增加2℃。
2)“长按键” 功能说明
●任何界面状态下,长按S12按键,可重置参数变动次数记录为0次。
●任何界面状态下,长按S13按键,可切换DAC输出功能,详见4. 6 DAC输出功能。 切换模式如下图所示:
3) 其它要求
长按键功能触发时间要求:按键按下时间超过1秒,触发长按键功能,否则为短按键。
按键应做好消抖处理,避免出现一次按键功能多次触发等问题。按键长按、短按对应的功能和效果不可互相影响。
请严格按照以上要求,定义各按键长按、短按功能。
这个按键是阉割版的矩阵按键,比较好处理,功能要求看起来似乎有点复杂,其实,只要我们把他们的逻辑写出来就会一目了然,S13只有一个切换功能,我们就用位定义一个变量,只有0和1两个状态,再设几个变量,每当按键按下,只是变量的值在变,不要再按键处理函数里调用显示函数,这样太乱:
void KeyScan() { R1 = 0; R2 = 1; C1 = 1;C2 = 1; if(C1 == 0) //S13 { if(turn_dat_setup == 0) //数据界面 { get_dat++; } else if(turn_dat_setup == 1) { setup_dat++; } DA_flag = 1; while(!C1); DA_flag = 0; } else if(C2 == 0) //S17 { switch(setup_dat) { case(1):temp_setup = temp_setup+2;break; case(2):distance_setup = distance_setup+5;break; } setup_count++; while(!C2); } R2 = 0; R1 = 1; C1 = 1;C2 = 1; if(C1 == 0) //S12 { turn_dat_setup = ~turn_dat_setup; count_flag = 1; while(!C1); count_flag = 0; EEPROM_write(0x01,setup_count/256); delayms(5); EEPROM_write(0x02,setup_count%256); delayms(5); } else if(C2 == 0) //S16 { switch(setup_dat) { case(1):temp_setup = temp_setup-2;break; case(2):distance_setup = distance_setup-5;break; } setup_count++; while(!C2); } } void DisplayDat() //对于按键控制的参数进行显示 { if(turn_dat_setup == 0) { switch(get_dat) { case(1):DisplayTempGet();break; case(2):DisplayDistanceGet();break; case(3):DisplaySetupCount();break; } } else if(turn_dat_setup == 1) { switch(setup_dat) { case(1):DisplayTempSetup();break; case(2):DisplayDistanceSetup();break; } } } void SetupDatPro() //数据的边界范围处理 { if(get_dat > 3) { get_dat = 1; } if(get_dat < 1) { get_dat = 3; } if(setup_dat > 2) { setup_dat = 1; } if(setup_dat < 1) { setup_dat = 2; } if(temp_setup > 99) { temp_setup = 0; } if(temp_setup < 0) { temp_setup = 99; } if(distance_setup > 99) { distance_setup = 0; } if(distance_setup < 0) { distance_setup = 99; } } void Timer2() interrupt 12 //这个是定时器2,我们后面会讨论到 { u16 count_sec_setup,count_sec_AD; if (DA_flag) //长按键 DA切换 { count_sec_AD++; if(count_sec_AD == 500) { count_sec_AD = 0; DA_turn = ~DA_turn; } } if(count_flag == 1) //长按键清零 { count_sec_setup++; if(count_sec_setup == 500) { setup_count = 0; count_sec_setup = 0; } } }
4.4存储功能
1)数据的存储
每当一次参数设置操作完成(从参数界面退出,切换回数据界面),如果参数发生变化,参数变动次数加1,将参数变动次数保存在E2RPOM存储器,要求可记录范围: 0-65535.
2)数据的重置.
参数变动次数可以通过长按S12重置为0次。
存储功能也已经写在按键扫描函数里了;这个是读取函数:
void GetSetupCount() //读取EEPROM的数值 { u16 count_h,count_l; count_h = EEPROM_read(0x01); count_l = EEPROM_read(0x02); setup_count = ((count_h * 256) + count_l); }
4.5串口功能
1)通信模式
波特率: 4800 bps
校验位:无校验
停止位: 1位。
2)数据召测功能
通过串口调试软件下发数据召测指令,设备接收到正确指令后,上报数据或参数。召测指令格式要求如下:
①查询数据指令:"ST\r\n”, 接收到指令后,设备返回距离和温度数据,数据格式要求如下:
以字符串形式输出,以’$’开头, "\r\n”结尾。
距离数据(S)和温度数据(T)以’,’间隔。
-格式: $距离数据,温度数据\r\n,
举例: $20,24. 32\r\n
上报的距离数据为整数,温度数据保留小数点后两位有效数字。
②查询参数指令: ”PARA\r\n”,接收到指令后,设备返回当前的距离参数和温度参数。返回数据格式要求如下:
以字符串形式输出,以‘$’开头,” r\n”结尾。
距离参数(S)和温度参数(T)以’,’间隔。
格式: #距离参数,温度参数\r\n
~举例: #35,30\r\n
③如设备接收到错误指令,返回”ERROR\r\n”。
备注:
1) 串口查询参数、数据指令响应时间要求: ≤500ms。
2) 串口逻辑功能错乱,发送乱码、错误数据将被酌情扣分。
3) S12、 S13、 S16、 S17按键扫描过程和串口通讯功能应互不影响。
串口这一模块我一直觉得是最难的一块,因为总是不能够顺畅的打开PC和单片机之间的通信。而这道题的难点在于,超声波已经占据一个定时器资源了,我们还需要一个定时器进行定时工作,这个时候就需要打开第三个定时器了,对于第三个定时器,我们使用的<reg52>的头文件没有包含相关的寄存器,需要我们查阅资料进行配置:
sfr T2H = 0xD6; //定时器2的高八位寄存器 sfr T2L = 0xD7; //定时器2的低八位寄存器 sfr AUXR = 0X8E; sfr T2MOD = 0XC9; sfr IE2 = 0XAF; u8 RevBuf[20]; //数据接收缓冲区 bit rx_flag; bit send_enable = 0; //发送标志位 u8 rx_tt; // u8 rx_cnt; void UartInit(void) //4800bps@12.000MHz { SCON = 0x50; //8位数据,可变波特率 AUXR |= 0x40; //定时器1时钟为Fosc,即1T AUXR &= 0xFE; //串口1选择定时器1为波特率发生器 TMOD &= 0x0F; //设定定时器1为16位自动重装方式 TL1 = 0x8F; //设定定时初值 TH1 = 0xFD; //设定定时初值 ET1 = 0; //禁止定时器1中断 TR1 = 1; //启动定时器1 ES = 1; } void Time2Init() //定时器2打开 { EA = 1; IE2 = (1<<2); AUXR &= 0XFB; T2L = 0X30; //2ms T2H = 0XF8; AUXR |= 0X10; } void SendByte(u8 dat) //串口发送字节 { SBUF = dat; while(!TI); TI = 0; } void SendDat(u8 *str) //串口一位一位的发送需要发送的数据 { while(*str != '\0') { SendByte(*str++); } } void Urat() interrupt 4 //串口接收,接收到的数据暂存缓冲区数组中 { if(RI == 1) { RI = 0; rx_flag = 1; //表示数据已经接收完毕 RevBuf[rx_cnt++] = SBUF; // 放入暂存区 } } void UratPro() { if(send_enable) //允许向PC发送数据 { send_enable = 0; SendDat(RevBuf); } } void Timer2() interrupt 12 { if(rx_flag && ++rx_tt >= 50) //如果数据接收完毕 { rx_flag = 0; rx_tt = 0; RevBuf[rx_cnt++] = '\0'; rx_cnt = 0; if(strcmp(RevBuf,"ST\r\n") == 0) //判断电脑发送的信息是否一致,如 { //果一致,发送距离数据 send_enable = 1; sprintf(RevBuf,"$%d,%d\r\n",distance_get,temp_get%100); } else if(strcmp(RevBuf,"PARA\r\n") == 0) { send_enable = 1; sprintf(RevBuf,"$%d,%d\r\n",distance_setup,temp_setup); } else { send_enable = 1; sprintf(RevBuf,"ERROR\r\n"); } } }
4.6DAC输出功能
1) 在启动状态下,DAC输出电压值Vout取决于测距数据(S), 对应关系如下表所示
2)在停止状态下,DAC 固定输出0. 4V.
DAC功能看起来比较生疏,但其实将底层驱动完善后只用进行一个简单的判断就可以输出了:
void DAPro() { if(DA_turn == 1) { if(distance_get <= distance_setup) { DA_write(102); } else if(distance_get > distance_setup) { DA_write(204); } } }
4. 7LED指示灯功能
1) 温度指示灯
当温度数据超过温度参数时,指示灯L1点亮,否则熄灭。
2) 距离指示灯
当距离数据小于距离参数时,指示灯L2点亮,否则熄灭。
3)DAC功能指示灯
启动状态下,指示灯L3点亮,停止状态下,指示灯L3熄灭。
每一次试题的最后都要考到LED和蜂鸣器之类的控制,做到这里的时候一定要静下心,必要的时候在纸上写一个逻辑框图就很清晰了:
void LedPro() { InitHC138(4); P0 = 0XFF; if(temp_get > temp_setup ) { L1 = 0; }else L1 = 1; if(distance_get < temp_setup) { L2 = 0; }else L2 = 1; if(DA_turn == 1) { L3 = 0; }else L3 = 1; }
4)本试题未涉及的 LED指示灯应处于熄灭状态,不同功能的指示灯状态切换时应互不影响。
1. 8初始状态说明
请严格按照以下要求设计作品的上电初始状态。
1) DAC处于启动状态。
2) 作品上电后。未经任何操作的状态下,数码管处于数据界面下,显示温度
3) 工作参数在每次上电时重置为默认值。
●温度参数: 30℃
●距离参数: 35cm
最后,我们写一个主函数,把核心的函数全部放进去:
void main() { InitHC573(); Timer0Init(); UartInit(); GetSetupCount(); Time2Init(); InitHC138(4); P0 = 0XFF; while(1) { wave_rec(); DisplayDat(); KeyScan(); SetupDatPro(); GetDatPro(); UratPro(); DAPro(); LedPro(); } }
在这里,给大家附上完善过的底层驱动代码以供大家参考:
//=======================================================================// //==========================温度读取底层驱动============================// //=====================================================================// #include "onewire.h" /****************************单总线延时函数****************************/ void Delay_OneWire(unsigned int t) { unsigned char aa; while(t--) { for(aa=0;aa<10;aa++); } } /****************************DS18B20芯片初始化****************************/ 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; } /*******************通过单总线向DS18B20写一个字节****************/ 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); } /****************************从DS18B20读取一个字节***********************/ 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; } /****************************温度读取处理函数****************************/ unsigned char Tempget() { unsigned char low,high,temp; Init_DS18B20(); Write_DS18B20(0xcc); Write_DS18B20(0x44); Delay_OneWire(20); Init_DS18B20(); Write_DS18B20(0xcc); Write_DS18B20(0xbe); low=Read_DS18B20(); high=Read_DS18B20(); temp=high<<4; temp|=(low>>4); return temp; } //======================================================================// //==========================EEPROM底层驱动=============================// //=====================================================================// #include <REG52.H> #include "intrins.h" #include "iic.h" #define somenop {_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();} #define SlaveAddrW 0xA0 #define SlaveAddrR 0xA1 //总线引脚定义 sbit SDA = P2^1; /* 数据线 */ sbit SCL = P2^0; /* 时钟线 */ void DA_write(uchar dat) { IIC_Start(); IIC_SendByte(0x90); IIC_WaitAck(); IIC_SendByte(0x43); IIC_WaitAck(); IIC_SendByte(dat); IIC_WaitAck(); IIC_Stop(); } uchar AD_read(uchar add) { uchar temp; IIC_Start(); IIC_SendByte(0x90); IIC_WaitAck(); IIC_SendByte(add); IIC_WaitAck(); IIC_Stop(); IIC_Start(); IIC_SendByte(0x91); IIC_WaitAck(); temp=IIC_RecByte(); IIC_Stop(); return temp; } uchar EEPROM_read(uchar add) { uchar temp; IIC_Start(); IIC_SendByte(0xA0); IIC_WaitAck(); IIC_SendByte(add); IIC_WaitAck(); IIC_Stop(); IIC_Start(); IIC_SendByte(0xA1); IIC_WaitAck(); temp=IIC_RecByte(); IIC_Stop(); return temp; } void EEPROM_write(uchar add,uchar dat) { IIC_Start(); IIC_SendByte(0xA0); IIC_WaitAck(); IIC_SendByte(add); IIC_WaitAck(); IIC_SendByte(dat); IIC_WaitAck(); IIC_Stop(); } //总线启动条件 void IIC_Start(void) { SDA = 1; SCL = 1; somenop; SDA = 0; somenop; SCL = 0; } //总线停止条件 void IIC_Stop(void) { SDA = 0; SCL = 1; somenop; SDA = 1; } //等待应答 bit IIC_WaitAck(void) { SDA = 1; somenop; SCL = 1; somenop; if(SDA) { SCL = 0; IIC_Stop(); return 0; } else { SCL = 0; return 1; } } //通过I2C总线发送数据 void IIC_SendByte(unsigned char byt) { unsigned char i; for(i=0;i<8;i++) { if(byt&0x80) { SDA = 1; } else { SDA = 0; } somenop; SCL = 1; byt <<= 1; somenop; SCL = 0; } } //从I2C总线上接收数据 unsigned char IIC_RecByte(void) { unsigned char da; unsigned char i; for(i=0;i<8;i++) { SCL = 1; somenop; da <<= 1; if(SDA) da |= 0x01; SCL = 0; somenop; } return da; } //======================================================================// //==========================DS1302时钟底层驱动=========================// //=====================================================================// #include "ds1302.h" //---DS1302写入和读取时分秒的地址命令---// //---秒分时日月周年 最低位读写位;-------// unsigned char code READ_RTC_ADDR[7] = {0x81, 0x83, 0x85, 0x87, 0x89, 0x8b, 0x8d}; unsigned char code WRITE_RTC_ADDR[7] = {0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c}; //---DS1302时钟初始化2016年5月7日星期六23点59分50秒。---// //---存储顺序是秒分时日月周年,存储格式是用BCD码---// unsigned char TIME[7] = {50, 59, 23, 0x07, 0x05, 0x06, 0x16}; //从DS1302寄存器读出数据 unsigned char read(unsigned char add) { unsigned char i,temp; unsigned char dat1,dat2; CE=0; SCLK=0; CE=1; writebyte(add); for(i=0;i<8;i++) { SCLK=0; temp>>=1; if(IO) { temp|=0x80; } SCLK=1; } IO=0; dat1=temp/16;//0XFF 1111 1111 dat2=temp%16; temp=dat1*10+dat2; return temp; } //写字节 void writebyte(unsigned char dat) { unsigned char i; for(i=0;i<8;i++) { SCLK=0; IO=dat&0x01; SCLK=1; dat>>=1; } } //向DS1302寄存器写入数据 void write(unsigned char add,unsigned char dat) { unsigned char num; CE=0; SCLK=0; CE=1; writebyte(add); num=(dat/10<<4)|(dat%10);// 55 writebyte(num); CE=0; } void Init_DS1302() { unsigned char i; write(0x8e,0x00); for(i=0;i<7;i++) { write(WRITE_RTC_ADDR[i],TIME[i]); } write(0x8e,0x80); } void readtime() { unsigned char i; write(0x8e,0x00); for(i=0;i<7;i++) { TIME[i]=read(READ_RTC_ADDR[i]); } write(0x8e,0x80); }