自上次参加蓝桥杯单片机组也是过去一年了,真是恍惚不再获啊,想起一年前的我此刻估计还在学习备赛蓝桥杯单片机的国赛,也正在编写这个自己的这个模板,当时真是觉得这就是我一路学来的结晶啊,现在看来也不过是个加了定时器中断的前后台裸机系统罢了......
真不是什么拿得出手的货,这里就分享一下吧......
这个模板可以开启几乎所有模块,并解算它们传感回的数据:
文章提供测试代码讲解、完整工程下载、测试效果图
目录
前后台裸机系统:
裸机前后台系统是一个嵌入式系统的概念,它通常指的是没有操作系统(如Linux、Windows等)参与的嵌入式环境。在这样的环境中,开发者需要直接管理硬件资源,如内存、中断、定时器等。
在裸机前后台系统中,定时器(Timer)是一个非常重要的组件,用于实现定时任务、延时操作、周期性检查等功能。前后台系统通常包含一个后台程序(Background Program)和一个或多个前台程序(Foreground Program)。
- 后台程序:后台程序通常是一个无限循环的程序,它负责整个系统的资源管理、任务调度和事件处理。在后台程序中,可以使用定时器来周期性地检查任务状态、更新系统时间、处理超时事件等。
- 前台程序:前台程序负责处理具体的业务逻辑和中断服务程序(ISR)。当中断发生时,中断服务程序会立即响应并处理中断事件。如果中断事件需要耗时较长的时间来处理,中断服务程序可以标记事件的发生,并返回。然后,后台程序会在适当的时机调度前台程序来处理这些事件。
在裸机前后台系统中,定时器的实现方式取决于具体的硬件平台和编程语言。一般来说,可以使用硬件定时器(如CPU内置的定时器)或软件定时器(通过编程实现的定时器)来实现。
硬件定时器通常具有更高的精度和可靠性,因为它们是由硬件直接支持的。但是,硬件定时器的数量和使用方式可能受到硬件平台的限制。软件定时器则是通过编程实现的,它们可以在没有硬件定时器支持的情况下使用。软件定时器通常使用一个计数器或定时器中断来实现定时功能。
关键部分代码逻辑:
时序安排函数:
这是个由若干个if(++n == x )组成的安排时序的函数,它就像一个任务列表,不断刷新各个模块任务的标志位,然后主函数接收到多余标志位就调用对应的函数处理具体的任务
而刷新这些任务的条件就是"时间",有的任务被安排在定时器中断 计数到 6ms时执行,有的被安排到 25ms时,并且执行完任务后相应的 计数标志还会被清零,以便后续循环做这个任务
//时序决定函数 ————决定各个模块运转时序 的函数
//此函数十分重要!乃是程序运行之心
//(在定时器0中断服务函数被调用 )
void Task_Clock()
{
if(++smg_cnt==6) //6ms 一次打印数码管
{smg_cnt=0;give_nr();smg_display(nr1,nr2,nr3,nr4,nr5,nr6,nr7,nr8);}
if(URX_Num > 0) //33ms 一次串口接收
{ if(++URX_tt == 33) { URX_Over = 1;} }
if(++key_cnt==25) //25ms 一次按键扫描
{key_cnt=0;key_flag=1;}
if(wei!=0) {if(++lm_cnt==600) {lm_cnt=0;lm_flag=!lm_flag;}} //600ms 改变一次亮灭标志(前提是有位被选中)
if(wei==0) {lm_flag=0;} //没位被选中,就不灭
if(++DS1302_cnt==145) //145ms 一次读取ds1302
{DS1302_cnt=0;DS1302_flag=1;}
if(++temperature_cnt==601) //601ms 一次读取温度
{temperature_cnt=0;temperarure_flag = 1;}
if(++LCM_cnt==655) //655ms读取一次超声波
{LCM_cnt=0;LCM_flag=1;}
if(++adc_cnt==122) //122ms读取一次ADC
{adc_cnt=0;adc_flag=1;}
if(++dac_cnt==172) //172msDAC电压输出一次
{dac_cnt=0;dac_flag=1;}
if(++uart_send_cnt==1251) //1251ms集中进行一次串口发送数据
{uart_send_cnt=0;uart_send_flag=1;}
}
定时器中断服务函数:
这个函数十分重要,他的中断频率决定了各个标志位的刷新频率,它还包含调用了之前的时序安排函数 并能够为按键长按等特殊需要计时的操作提供计时
//1毫秒@12.000MHz 定时器0 初始化函数
//此函数可在软件生成,别忘了添加 EA=1; ET0=1;这俩句
//此函数初始化定时器0为 1ms 中断一次,满足普遍要求
//当要使用NE555时,要避免使用定时器0 转而使用定时器1
void Timer0Init(void)
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TL0 = 0x18; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
EA=1; //打开总中断
ET0=1; //打开定时器0中断
}
//定时器0中断服务函数
//调用 时序决定函数
//为按键长按计数计时,开启相应功能
void TIMER0_serv() interrupt 1
{
Task_Clock(); //调用 时序决定函数
//为按键的长按计数计时:
if(key4_flag==1){k4_cnt++;} else if(key4_flag==0) {k4_cnt=0;}
if(key5_flag==1){k5_cnt++;} else if(key5_flag==0) {k5_cnt=0;}
if(key8_flag==1){k8_cnt++;} else if(key8_flag==0) {k8_cnt=0;}
if(key9_flag==1){k9_cnt++;} else if(key9_flag==0) {k9_cnt=0;}
//为按键长按到达做相应功能:
//别忘了让 长按到达标志(key_long_state)置 1,以消除短按影响:
//此处 nrx++ 仅做测试用,nrx++ 可改变 数码管对应nr打印内容,删去会使长按取消短按功能
if(k4_cnt==1000) {AT24C02_flag=1;key_long_state=1;}
if(k5_cnt==1000 && jm==2) {clear_time(); key_long_state=1;}
if(k8_cnt==1000) {key_long_state=1;}
if(k9_cnt==1000) {key_long_state=1;}
}
串口与上位机通信部分:
当时还注意学习了如何与上位机进行简单的互动,但像这样简单的定义一个数组堆栈的接收方式,现在看来十分不严谨,还是要用状态机思维进行接收,数据以包的形式收发,有帧头帧尾才好......
//串口命令 接收处理按键函数
//会先向上位机 返回单片机接收到的字符串,然后执行一些反馈,反馈如下:
//接收到 START\r\n 就返回 START OVER\r\n
//接收到 HI\r\n 就返回 HELLO\r\n
//接收到 其他字符串命令一律返回 ERROR\r\n
//此函数 封装包含了 对应标志位的 监测作用
//注意需要头文件 #include "string.h"
void handle_uart()
{
if(URX_Over==1)
{
URX_Over=0;
printf("%s\n",&URX);
if(URX_Num==7) //先判断命令字符串长度,在进行比较( str1 的长度为7 )
{if(strncmp(URX,str1,7)==0) {Uart_Sendstring("START OVER\r\n");}
else Uart_Sendstring("ERROR\r\n");}
else if(URX_Num==4) //先判断命令字符串长度,在进行比较( str2 的长度为4 )
{if(strncmp(URX,str2,4)==0) {Uart_Sendstring("HELLO\r\n");}
else Uart_Sendstring("ERROR\r\n");}
else Uart_Sendstring("ERROR\r\n");
URX_Num = 0; //处理完命令别忘了将其清零,以便接收下个命令
memset(URX,0,sizeof(URX));//处理完命令别忘了将其清零,以便接收下个命令
}
}
//串口1 中断服务函数
//这里默认串口1中断服务是要接收 字符串命令
//接收到字符就清零URX_tt,阻止其在定时器中的计数,直到没有字符进来为止
void Uart_1_serv() interrupt 4
{
if(RI)
{
RI=0; URX_tt = 0;
if(URX_Num < 10) { URX[URX_Num++] = SBUF; }
}
}
按键的处理:
当时对于按键的处理还停留在行列式扫描的阶段,也许是那时觉得这样写最稳定把,我还是建议用状态机思维去写按键
//按键扫描初始化函数
//通过参数来选择初始化哪一行哪一列
void key_scan_inint(u8 n)
{
switch(n)
{
case 1:X1=0;X2=1;Y1=1;Y2=1;break;
case 2:X1=1;X2=0;Y1=1;Y2=1;break;
}
}
//按键返回键值函数
//给S4 S5 S8 S9每个都配备了长按功能
u8 key_return()
{
u8 key_value;
key_value=0;
Delay12ms(); //延时12ms 消抖
key_scan_inint(1);
if(Y1==0){while(Y1==0){key4_flag=1;} key4_flag=0; key_value=4;}
if(Y2==0){while(Y2==0){key8_flag=1;} key8_flag=0; key_value=8;}
key_scan_inint(2);
if(Y1==0){while(Y1==0){key5_flag=1;} key5_flag=0; key_value=5;}
if(Y2==0){while(Y2==0){key9_flag=1;} key9_flag=0; key_value=9;}
if(key_long_state==1) //如果上次进行了长按
{
key_long_state=0; //清零长按标志
key_value=0; //清除长按松手误发的键值
}
return key_value;
}
//按键短按键值接收处理按键函数
//此函数 封装包含了 对应标志位的 监测作用
//此处 nrx++ 仅做测试用,nrx++可改变 数码管对应nr打印内容
void handle_keyreturn()
{
u8 key_value;
if(key_flag==1)
{
key_flag=0;
key_value=key_return();
switch(key_value)
{
case 4:jm++; if(jm>=3) {jm=0;} break;
case 5:if(jm==2) { wei++; if(wei>=4) {wei=0;} } break;
case 8:if(jm==2) write_ds1302_wei(1); else{value++;if(value>=255){value=255;}}break;
case 9:if(jm==2) write_ds1302_wei(0); else{value--;if(value<=0) {value=0;}}break;
}
}
}
数码管的处理:
当时对数码管还是有点废了相当时间去研究的,这也是当时觉得比较显示稳定的写法了:
//数码管打印函数 //传入八个参数u8 nr1,nr2,nr3,nr4,nr5,nr6,nr7,nr8 进行打印 //一次性打印八位数码管 void smg_display(u8 nr1,nr2,nr3,nr4,nr5,nr6,nr7,nr8) { u8 i; i=0; for(i=0;i<=8;i++) { inint_port(6); switch(i) { case 1:P0=0X01;inint_port(7);P0=smgZK[nr1];Delay250us();P0=0Xff;break; case 2:P0=0X02;inint_port(7);P0=smgZK[nr2];Delay250us();P0=0Xff;break; case 3:P0=0X04;inint_port(7);P0=smgZK[nr3];Delay250us();P0=0Xff;break; case 4:P0=0X08;inint_port(7);P0=smgZK[nr4];Delay250us();P0=0Xff;break; case 5:P0=0X10;inint_port(7);P0=smgZK[nr5];Delay250us();P0=0Xff;break; case 6:P0=0X20;inint_port(7);P0=smgZK[nr6];Delay250us();P0=0Xff;break; case 7:P0=0X40;inint_port(7);P0=smgZK[nr7];Delay250us();P0=0Xff;break; case 8:P0=0X80;inint_port(7);P0=smgZK[nr8];Delay250us();P0=0Xff;break; } } }
用数组思想操作LED:
当时为了操作LED方便,写了个数组来操作LED,想让那个灯亮灭进行与/或运算即可,而且还不会影响到其余LED的亮灭情况,算是小有聪明的设计了吧,哈哈......
//LED操作库数组: //与之进行相与运算,以打开对应led //1_L1 2_L2 3_L3 4_L4 5_L5 6_L6 7_L7 8_L8 u8 code LED_codeON[10]={0xff,0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f}; //与之进行相或运算,以关闭对应led //1_L1 2_L2 3_L3 4_L4 5_L5 6_L6 7_L7 8_L8 u8 code LED_codeOFF[10]={0xff,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};
DS1302修改时间:
这个当时觉得挺麻烦的,主要是需要进行进制转换,谁又知道一个即将参加国赛的大二大学生,其实连进制转换都弄不明白呢......
自学之路总是要浪费相当的时间与精神的,毕竟之前都没学过单片机就参加这个比赛了.....
//ds1302时间选位修改函数 //修改所选择的位的时间值,不是写入 具体值,是将当前值 加1 或 减1 //传入参数介绍: //add表示加减,1是加1 0是减1 //每次写要先关写保护,写完后要开写保护。 //该函数可能开机无法正常 使用,因为开机瞬间无读取值 void write_ds1302_wei(u8 add) { char time_temp; u8 time_temp_1; u8 time_temp_2; //从 read_t[] 对应时间位 读取到 10进制时间值 time_temp=read_time[wei-1]; //根据add数值决定当前位 加1 还是 减1 if(add==1) { time_temp++; if(wei==1 && time_temp>=59) time_temp=59; if(wei==2 && time_temp>=59) time_temp=59; if(wei==3 && time_temp>=24) time_temp=24; } if(add==0) { time_temp--;if(time_temp<=0) time_temp=0;} //以下三行将time_temp重新转化为16进制值方便写操作的进行 time_temp_1=time_temp/10; time_temp_2=time_temp%10; time_temp=time_temp_1*16+time_temp_2; //写操作 Write_Ds1302_Byte(0x8e,0x00); //允许写入数据 Write_Ds1302_Byte(Write_DS1302_adrr[wei-1],time_temp); //写入数据 Write_Ds1302_Byte(0x8e,0x80); //禁止写入数据 } //读取当前时钟数据 //读取的数据存储在 数组 Timer[] //一次读取三个位的值,秒、分、时 //并且用 read_t[] 数组进行转化为 十进制的值 void read_DS1302_Timer() { u8 i; for(i=0;i<3;i++) { Timer[i]=Read_Ds1302_Byte(Read_DS1302_adrr[i]); read_time[i]=Timer[i]/16*10+Timer[i]%16; } } //时间标志位 处理函数 void handle_Timer() { if(DS1302_flag==1) { DS1302_flag=0; read_DS1302_Timer(); } }
完整测试工程下载:
https://download.csdn.net/download/qq_64257614/89342529?spm=1001.2014.3001.5503