【蓝桥杯单片机专题】第十五届省赛真题解析与思路梳理

第十五届省赛真题代码题解析

  • 前言
  • 赛题代码思路笔记
  • 竞赛板配置
  • 建立模板
  • 明确基本要求
  • 显示功能部分
  • 频率界面
  • 正常显示
  • 高位熄灭
  • 参数界面
  • 基础写法:两个界面分开来写
  • 优化写法:两个界面合一起写
  • 时间界面
  • 回显界面
  • 校准
  • 校准过程
  • 校准错误显示
  • DAC输出部分
  • 按键功能部分
  • S4:“界面”按键
  • S5:“选择”按键
  • 优化写法
  • S8、S9:“加”按键 、“减”按键
  • LED指示灯功能部分
  • 调试时发现的问题
  • 最终代码
  • User文件
  • main.c
  • Driver文件
  • init.c
  • Key.c
  • Led.c
  • Seg.c
  • iic.c
  • ds1302.c
  • 结语
  • 前言

    本文是对蓝桥杯第十五届省赛真题的代码题做的解题笔记。
    我在看完西风老师的解析视频后,自己重新写了一遍,做了点整理也加入了一点个人的思考,因此在细节上(比方说:变量名…)会有一点出入。

    思路参考西风第二十三讲内容(资料链接已经在下方给出)

    https://www.bilibili.com/video/BV1TR4y1k7iz?p=23&vd_source=e2191f89c557f5ac44bb6c7aa3967c7c

    关于蓝桥杯第十五届省赛真题可以在官网查看(资料链接已经在下方给出)

    https://www.lanqiao.cn/courses/2786/learning/?id=2870978&compatibility=false

    赛题代码思路笔记

    竞赛板配置

    建立模板

    根据赛题中的硬件框图确定本赛题的框架

    如图,在Driver文件夹里建立LED、数码管、按键、iic模块(DAC输出)、DS1302,在User文件夹中建立主函数模块(大模板参考西风老师的2024版大模板)。

    明确基本要求



    注意1.的内容:

  • P34引脚和矩阵按键有冲突,写按键模块(Key.h)时注意屏蔽掉P34=0的情况
  • 板子的P34口和SIGNAL的跳帽要接上

  • 注意2.的内容:

  • 按键延迟时间要控制在100ms以内
  • 	/*按键10ms的减速*/
    	if(Slow_Down % 10 == 0)
    	{
    		Key_Flag = 0;
    	}
    


    注意第三点的内容:

  • 数码管延迟时间要控制在100ms以内
  •     /*数码管90ms的减速*/
    	if(++Slow_Down == 90)
    	{
    		Seg_Flag = Slow_Down = 0;
    	}
    

    显示功能部分

    1. 显示部分一共4个界面,定义一个变量界面显示模式变量Seg_Disp_Mode各个界面。
      后续通过对 Seg_Disp_Mode (界面显示模式变量)的判断切换不同的界面。
    unsigned char Seg_Disp_Mode;           //界面显示模式变量 0-频率界面 1-参数界面 2-时间界面 3-回显界面
    
    	switch(Seg_Disp_Mode)
    	{
    		case 0://频率界面
    		
    		break;
    		
    		case 1://参数界面
    			
    		break;
    		
    		case 2://时间界面
    			
    		break;
    		
    		case 3://回显界面
    			
    		break;
    	}
    
    1. 在显示功能部分中,需要显示的字符有:0-9、熄灭、A、F、H、P、-,在seg.c文件段选数组中添加对应的十六进制表示。
    code unsigned char Seg_dula[] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0xff,0x88,0x8e,0x89,0x8c,0xBF};//0-9,10-熄灭,11-A,12-F,13-H,14-P,15--
    

    频率界面

    频率界面由三部分组成:

  • 第一部分为编号(Seg_Buf[0]),在该界面下全程显示字母F。
  • 第二部分熄灭部分(Seg_Buf[1] – Seg_Buf[2]),在该界面下全程熄灭。
  • 第三部分为频率显示部分(Seg_Buf[3] – Seg_Buf[7]),用NE555实时计算频率。
  • 正常显示

    数码管显示数组Seg_Buf[])对应的数码管中输入要显示的字符在seg.c文件段选数组Seg_dula[])对应的十六进制表示的位置。

    			Seg_Buf[0] = 12;//F
    		  	Seg_Buf[1] = Seg_Buf[2] = 10;
    			Seg_Buf[3] = Freq / 10000 % 10;
    		  	Seg_Buf[4] = Freq / 1000 % 10;
    		  	Seg_Buf[5] = Freq / 100 % 10;
    			Seg_Buf[6] = Freq / 10 % 10;
    			Seg_Buf[7] = Freq % 10;
    
    高位熄灭


    while循环高位开始检测,当高位是0时,将该位置的数码管置为熄灭状态。

    注意:
    显示最小为0,即数码管的最后一位(Seg_Buf[7])不论是否为0都要保证正常显示,所以在while循环中不对最后一位检测。

    	unsigned char i=3;
    
     /*高位为0时熄灭*/
    		  while(Seg_Buf[i] == 0)
    			{
    				Seg_Buf[i] = 10;
    				if(++i == 7) break;
    			}
    		  break;
    

    参数界面


    参数界面中又有两个子界面,定义参数界面模式变量Seg_DM1_Mode对两个子界面标记。

    bit Seg_DM1_Mode = 1;                      //参数界面模式变量 0-超限参数界面 1-校准值参数界面
    

    定义参数数组Seg_Set_Arr,存储两个界面下的参数值。
    介于校准值界面的参数值有负数的情况,可以将参数数组(Seg_Set_Arr)直接定义为有符号的数组,后续只要对校准值变量的正负进行判断就可以确定校准值的符号位要不要点亮。

    int Seg_Set_Arr[2] = {2000,0};         //参数数组 [0]-超限参数 [1]-校准值变量
    
    基础写法:两个界面分开来写
    1. if 判断当前子界面:Seg_DM1_Mode(参数界面模式变量)为0,处于超限参数界面;反之,处于校准值参数界面。
    2. 超限参数界面(逻辑和频率界面的正常显示部分相同,不再赘述)
    3. 校准值参数界面时:
      用if检测校准值,分为校准值参数不为0校准值参数为0两种情况。
      校准值参数不为0时:用一组 if 判断校准值的正负性,先确定符号位,再对数值进行显示。
      注意:校准值是有符号的数据,要取出各位数可以对他先取它的绝对值(math.h文件中的abs函数
      当校准值参数为0时:显示一位0。
    #include "math.h"  //abs函数
    
     		    if(Seg_DM1_Mode == 0)//超限参数界面
    			{
    				Seg_Buf[4] = Seg_Set_Arr[0] /1000 % 10;
    				Seg_Buf[5] = Seg_Set_Arr[0] /100 % 10;
    				Seg_Buf[6] = Seg_Set_Arr[0] /10 % 10;
    				Seg_Buf[7] = Seg_Set_Arr[0] % 10;
    			}
    			else//校准值参数界面
    			{
    				if(Seg_Set_Arr[1] != 0)
    				{
    					/*确定符号位*/
    					if(Seg_Set_Arr[1] < 0)
    						Seg_Buf[4] = 15;//-
    					else 
    						Seg_Buf[4] = 10;//熄灭
    					
    					
    					Seg_Buf[5] = abs(Seg_Set_Arr[1]) /100 % 10;
    				 	Seg_Buf[6] = abs(Seg_Set_Arr[1]) /10 % 10;
    				  	Seg_Buf[7] = abs(Seg_Set_Arr[1]) % 10;
    				}
    				else
    				{
    					Seg_Buf[5] = Seg_Buf[6] = 10;
    				  	Seg_Buf[7] = 0;
    				}				
    			}
    
    优化写法:两个界面合一起写

    (这个部分是我对本套题解析视频做的笔记,西风老师这个写法的思路真的蛮好的)

    先看到具体代码:

    		Seg_Buf[0] = 14;//P
    		Seg_Buf[1] = (unsigned char)Seg_DM1_Mode+1;
    		Seg_Buf[2] = Seg_Buf[3] = 10;
    	    Seg_Buf[4] = Seg_DM1_Mode?(10 + 5 * (Seg_Set_Arr[Seg_DM1_Mode] < 0)):abs(Seg_Set_Arr[Seg_DM1_Mode]) /1000 % 10;
    		Seg_Buf[5] = abs(Seg_Set_Arr[Seg_DM1_Mode]) /100 % 10;
    	    Seg_Buf[6] = abs(Seg_Set_Arr[Seg_DM1_Mode]) /10 % 10;
    		Seg_Buf[7] = abs(Seg_Set_Arr[Seg_DM1_Mode]) % 10;
    		  
    		  /*校准值为0时,只显示一位0*/
    		if(Seg_Set_Arr[Seg_DM1_Mode] == 0)
    				Seg_Buf[5] = Seg_Buf[6] = 10;
    

    优化写法重点在于:

    1. Seg_DM1_Mode(参数界面模式变量)和显示相关变量的代数关系
    2. 条件运算符

    简单回顾一下条件运算符:
    <表达式1>?<表达式2>:<表达式3>
    当表达式1为真,返回表达式2的值;如果为假,则返回表达式3的值。

    基础的显示不再赘述,在此对 Seg_Buf[1]Seg_Buf[4] 做简单的分析:

    1. Seg_Buf[1]:
      由题目所给信息可知,Seg_Buf[1]在1和2之间变化,对应的是两个子界面
      在此前我们已经给这两个界面定义好了标志变量Seg_DM1_Mode(参数界面模式变量),它在0和1之间变化
      不难得出,Seg_Buf[1]和Seg_DM1_Mode之间的代数关系,即Seg_Buf[1]的值比Seg_DM1_Mode大1
      注意: Seg_DM1_Mode是bit型数据,不能到达2,所以让Seg_DM1_Mode直接+1不能达到效果。那么需要解决的问题是增大它数值的可变区间,可以用强制类型转换。
    2. Seg_Buf[4]:
      Seg_Buf[4]对应了两种显示状态,即超限参数界面下的高位校准值参数界面下的符号位
      条件运算符对Seg_DM1_Mode(参数界面模式变量)进行判断,可以划分出两种状态,即当Seg_DM1_Mode为真(Seg_DM1_Mode为1)时,是校准值参数界面;当Seg_DM1_Mode为假(Seg_DM1_Mode为0)时,是超限参数界面。
      那么,只要找出Seg_Buf[4]在两种显示状态下的数值在段选数组(Seg.h文件下的Seg_dula[])中的位置就可以完成显示。
      Seg_Buf[4]在超限参数界面下的高位状态时(Seg_DM1_Mode为0):只要获取超限参数(Seg_Set_Arr[0])的最高位就行。
      Seg_Buf[4]在校准值参数界面下的符号位状态时(Seg_DM1_Mode为1):Seg_Buf[4]在Seg_Set_Arr[1](校准值参数)为正,Seg_Buf[4]不点亮;Seg_Set_Arr[1](校准值参数)为负,Seg_Buf[4]才点亮,显示符号位。所以还要找熄灭对应的位置和负号对应的位置之间的关系
      这里我觉得比较妙的一点是用条件是否为真(Seg_Set_Arr[Seg_DM1_Mode] < 0)进行了熄灭和负号点亮之间的切换。)

    时间界面



    定义**时间参数数组ucRtc[]**用来存储时分秒的数据。

    unsigned char ucRtc[3] = {13,03,05};   //时间参数数组 [0]-时 [1]-分 [2]-秒
    

    调用ds1302里写入的函数Set_Rtc(),将初始显示状态的数据在上电时写入

    	Set_Rtc(ucRtc);
    

    每次进入信息处理函数时读取(Read_Rtc())时钟信息更新时间参数数组ucRtc[] 的数据,为后续显示做准备。

    	Read_Rtc(ucRtc);
    

    (显示可以一位数码管一位数码管写,和频率界面的正常显示相似,就不再赘述了。)
    for循环 将数码管显示的时分秒数据写入。
    这里要找出时分秒在数码管的位置之间的代数关系,时分秒的高位分别占0 3 6,低位占1 4 7,可知高位满足 y = 3x ,低位满足 y = 3x+1。
    在一次循环里实现一组高低位的取值。

    			for(j = 0;j < 3;j++)
    			{
    				Seg_Buf[j * 3] = ucRtc[j] / 10;
    				Seg_Buf[j * 3 + 1] = ucRtc[j] %10;
    			}
    			Seg_Buf[2] = Seg_Buf[5] = 15;//-
    

    回显界面


    回显界面也有两个子界面,声明回显界面模式变量bit Seg_DM3_Mode标记两个子界面。
    频率回显界面要显示最大频率值,时间回显界面要显示最大频率发生时间,分别声明最大频率值变量Max_Freq和**最大频率发生时间数组Max_Freq_Time[3]**存储这两个值。

    bit Seg_DM3_Mode;                      //回显界面模式变量 0-频率回显界面 1-时间回显界面
    unsigned int Max_Freq;                 //最大频率值变量
    unsigned int Max_Freq_Time[3];          //最大频率发生时间数组
    

    每次进入信息处理函数后获取最大频率及其发生时间,存到对应的变量里,以供后续调用。

    /*获取最大频率及其发生时间*/
    	if(Max_Freq < Freq)
    	{
    		Max_Freq = Freq;
    		for(j = 0;j < 3;j ++)
    			Max_Freq_Time[j] = ucRtc[j];
    	}
    

    频率回显界面要注意高位熄灭
    时间回显界面用循环获取时间的话,和时间界面相似,要找出各位之间的代数关系。

    			Seg_Buf[0] = 13;//H
    			if(Seg_DM3_Mode == 0)//频率回显界面
    			{
    				Seg_Buf[1] = 12;//F
    				Seg_Buf[2] = 10;
    				Seg_Buf[3] = Max_Freq / 10000 % 10;
    				Seg_Buf[4] = Max_Freq / 1000 % 10;
    				Seg_Buf[5] = Max_Freq / 100 % 10;
    				Seg_Buf[6] = Max_Freq / 10 % 10;
    				Seg_Buf[7] = Max_Freq % 10;
    				
    				/*高位为0时熄灭*/
    				while(Seg_Buf[i] == 0)
    				{
    					Seg_Buf[i] = 10;
    					if(++i == 7) break;
    				}
    			}
    			else//时间回显界面
    			{
    				Seg_Buf[1] = 11;//A
    				for(j = 0;j < 3;j ++)
    				{
    					Seg_Buf[2+2*j] =  Max_Freq_Time[j] / 10;
    					Seg_Buf[2+2*j+1] =  Max_Freq_Time[j] % 10;
    				}
    			}
    

    校准

    校准过程

    显示的频率都是实时频率加上校准值后的结果,那么只要在计算完频率之后加上校准值即可。

    Freq += Seg_Set_Arr[1];  //加上校准值
    
    校准错误显示

    校准错误显示在频率显示界面(Seg_Disp_Mode为0)下实现
    可以将显示分为两种情况

    1. 频率正常
    2. 频率错误

    if 对校准后的频率进行判断,当频率大于0时,正常读取正常显示;当频率小于0时,只显示首位的编号和末两位的LL。

    		if(Freq >= 0)
    		{
    			Seg_Buf[3] = Freq / 10000 % 10;
    		  Seg_Buf[4] = Freq / 1000 % 10;
    		  Seg_Buf[5] = Freq / 100 % 10;
    			Seg_Buf[6] = Freq / 10 % 10;
    			Seg_Buf[7] = Freq % 10;
    			
    			 /*高位为0时熄灭*/
    		  while(Seg_Buf[i] == 0)
    			{
    				Seg_Buf[i] = 10;
    				if(++i == 7) break;
    			}
    		}
    		else
    		{
    			Seg_Buf[3] = Seg_Buf[4] = Seg_Buf[5] = 10;//熄灭
    			Seg_Buf[6] = Seg_Buf[7] = 16;//LL
    		} 
    

    注意: Freq要用 long int型
    考虑两个点 :

    1. 频率的正负。因为加上校验值后频率可能为负数,如果用无符号型无论如何都得不到负数,所以要用有符号型的数据。
    2. 范围。int型的取值范围为 -32768 到 32767 ,用来存储单纯的频率是够用的,但是加上校验值后就可能产生数据溢出的问题,使得显示数据不准,所以要扩大取值范围

    DAC输出部分



    声明输出电压变量V_OutPut,存储要输出的电压大小。

    float V_OutPut;                         //输出电压变量
    

    整体的输出可以把分成四段:

    1. 频率错误(Frqe<0)时,DAC输出0V。
    2. 频率大于等于0且小于500Hz时,DAC输出1V
    3. 频率大于超限参数时,DAC输出5V
    4. 频率在500Hz和超过限制频率之间时,DAC输出要求出该斜线的表达式。
      易得数学表达式:
      V = ( ( 5 – 1 ) / ( 超限参数 – 500 ) ) * (F-500) + 1
    5. 频率错误时,DAC输出0V。

    注意: V_OutPut(输出电压变量)写入的 0-5 的值是需要输出的模拟输出值,而在调用DAC输出函数(Da_Write)时写入的参数是 0 – 255 范围内的数字输入值。如果直接用V_OutPut的值写入参数,那么实际输出的电压值将会很小,所以在此要将 0-5 范围的值线性映射成 0-255 范围的量值,不难得出这两个范围的倍数关系是51,因此只要对小的值(输出电压变量V_OutPut)乘上51即可完成映射。

    /*DAC相关*/
    	if(Freq < 0) V_OutPut = 0;
    	else if(Freq >= 0 && Freq <= 500) V_OutPut = 1;
    	else if(Freq >= Seg_Set_Arr[0]) V_OutPut = 5;
    	else V_OutPut = 1 + (float)(5.0 - 1.0)/(Seg_Set_Arr[0] - 500) * (Freq - 500);
    	Da_Write(V_OutPut * 51);
    

    按键功能部分


    /*按键处理函数*/
    void Key_Proc()
    {
    	if(Key_Flag) return;
    	Key_Flag = 1;
    	
    	Key_Val = Key_Read();
    	Key_Down = Key_Val & (Key_Val ^ Key_Old);
    	Key_Old = Key_Val;
    	
    	switch(Key_Down)
    	{
    		case 4://“界面”按键
    		
    		break;
    		
    		case 5://“选择”按键
    			
    		break;
    		
    		case 8://“加”按键
    			
    		break;
    		
    		case 9://“减”按键
    			
    		break;
    	}
    }
    
    S4:“界面”按键

    界面切换要考虑模式变量的取值情况
    按键按下一次模式切换一次,即 Seg_Disp_Mode(界面显示模式变量)+1。Seg_Disp_Mode在 0-3 之间变化,对应4个模式,最大值不能为3,所以当 Seg_Disp_Mode 加到4时,要将它重新赋0,以此形成一个循环的切换

    if(++Seg_Disp_Mode == 4) Seg_Disp_Mode = 0;
    
    S5:“选择”按键


    分成两种情况对各自界面下的模式变量操作。
    因为这两个界面下的模式变量都是bit型,可以不用像“界面”按键那里一样进行加法操作,可以用异或。
    这里用异或,其实就相当于取反。因为bit型数据本身就只有0和1两个数值,0异或后为1,1异或后为0。

    			if(Seg_Disp_Mode == 1)
    				Seg_DM1_Mode ^= 1;
    			else if(Seg_Disp_Mode == 3)
    				Seg_DM3_Mode ^= 1;
    

    注意:

    1. 频率界面切换到参数界面和时间界面切换到回显界面时,有默认显示的子界面,所以在每次在完成这两种切换时,都要保证当前显示处于默认子界面,但是在上面的操作中并不能实现。
    2. 默认子界面的显示是在按下“界面”按键时生效的(Key_Down为4),即要对“界面”按键按下后将要显示的界面进行判断,决定是否要重置子界面的模式变量
                if(++Seg_Disp_Mode == 1)  Seg_DM1_Mode = 0;   //默认显示超限参数子界面
    			if(Seg_Disp_Mode == 3)  Seg_DM3_Mode = 0;     //默认显示频率回显子界面
    			if(Seg_Disp_Mode == 4)  Seg_Disp_Mode = 0;    //退回到最初界面
    
    优化写法

    上面是对按键按下后的界面进行了判断后才执行重置。但是,其实不进行判断也是可以的。因为子界面模式变量的值是多少对于另外两个界面的显示没有任何影响,所以可以省去判断的那一步,在每次按下按键就重置。

    			if(++Seg_Disp_Mode == 4) 
    				Seg_DM1_Mode = Seg_DM3_Mode = Seg_Disp_Mode = 0;
    
    S8、S9:“加”按键 、“减”按键


    “加”按键和“减”按键逻辑类似,此处只对“加”按键做简单的解析。
    在进行加法运算时,要考虑单次加的单位值大小参数的上限值
    此处是对同界面下的两个子界面的参数进行的加法,所以可以将两个子界面的单次加的单位值和参数上限值分组后分别存在两个数组中(单次加减大小数组Step_Arr[2]上限值数组Max_Arr[2]),前一个数是超限参数对应的值,后一个数是校准参数对应的值。

    //按键功能相关变量
    int code Step_Arr[2] = {1000,100};     //单次加减大小数组 [0]-超限参数单次增加量/减小量 [1]-校准参数单次增加量/减小量
    int code Max_Arr[2] = {9000,900};      //上限值数组 [0]-超限参数上限值 [1]-校准参数上限值
    int code Min_Arr[2] = {1000,-900};     //下限值数组 [0]-超限参数下限值 [1]-校准参数下限值
    

    通过参数界面模式变量(Seg_DM1_Mode) 根据当前界面的不同获取对应的值,进行加法处理。
    每次加完都要对当前数值进行判断,避免超出限制
    (减法运算逻辑类似,不赘述)

    			case 8://“加”按键
    			if(Seg_Disp_Mode == 1 )
    			{
    				Seg_Set_Arr[Seg_DM1_Mode] += Step_Arr[Seg_DM1_Mode];
    				if(Seg_Set_Arr[Seg_DM1_Mode] > Max_Arr[Seg_DM1_Mode])
    					Seg_Set_Arr[Seg_DM1_Mode] = Max_Arr[Seg_DM1_Mode];
    			}
    		break;
    		
    		case 9://“减”按键
    			if(Seg_Disp_Mode == 1 )
    			{
    				Seg_Set_Arr[Seg_DM1_Mode] -= Step_Arr[Seg_DM1_Mode];
    				if(Seg_Set_Arr[Seg_DM1_Mode] < Min_Arr[Seg_DM1_Mode])
    					Seg_Set_Arr[Seg_DM1_Mode] = Min_Arr[Seg_DM1_Mode];
    			}
    		break;
    

    LED指示灯功能部分


    这块的重点是两个地方:

    1. 0.2s间隔的闪烁。
    2. 亮、灭状态的切换。

    声明闪烁间隔计时变量Time_200ms,后续用于0.2间隔闪烁的计时。
    声明闪烁标志位Led_Flag,用于标记亮灭状态,并且通过它的改变实现亮灭的切换。

    //LED指示灯功能相关变量
    unsigned char Time_200ms;              //闪烁间隔计时变量
    bit Led_Flag;                          //闪烁标志位
    

    在定时中断服务函数中完成。
    每计时满200ms重新开始计时,并且改变一次亮灭状态。

    /*LED闪烁200ms计时*/
    	if(++Time_200ms == 200)
    	{
    		Time_200ms = 0;
    		Led_Flag ^= 1;
    	}
    

    (在实现点亮时,可以用最基础的 if 的方式一步一步判断,这个逻辑很简单,暂时先不写了。下面这个是西风老师的逻辑,我认为比最基础版写法更好,因为更能体现你对C语言运算符运用,对此我做个简要的分析一下)
    题目涉及的LED只有L1和L2,所以只要对Led控制数组(ucLed[])前两位进行操作。

    1. L1在频率界面下生效(Seg_Disp_Mode为0),用逻辑非运算符( !) 对 Seg_Disp_Mode 的逻辑值取反。即Seg_Disp_Mode为 0时(当前处于频率界面),那逻辑值就是假,! 运算之后的结果就是 1;Seg_Disp_Mode若不为 0(当前不处于频率界面),那逻辑值就是真,! 运算之后的结果就是 0。
      再和闪烁标志位(Led_Flag)进行逻辑与(有0出0)的运算,那么当Seg_Disp_Mode为 0时(当前处于频率界面),L1的点亮状态全由 Led_Flag(闪烁标志位)决定
    2. L2在频率大于超限参数 或者 频率为负数时生效,因为两种情况中任意满足一种它就生效,所以可以对这两个情况进行逻辑或运算(其中一个操作数的逻辑值为真,整个表达式的结果就为真)。
      处于频率大于超限参数时,在这可以直接对频率判断 也 可以对DAC输出电压变量(V_OutPut)进行判断。对 V_OutPut 判断(回顾DAC输出部分),当 V_OutPut 为 5 时表示当前处于频率大于超限参数的状态,要实现L2闪烁。那么只要对 V_OutPut == 5 进行真假逻辑的判断就可以用1和0来表示 频率大于超限参数(L2闪烁)频率不大于超限参数(L2不闪烁) 两种情况。此时就和L1的思路相似,将 V_OutPut == 5 逻辑真假的输出值和闪烁标志位(Led_Flag)进行逻辑与(有0出0)的运算,那么当逻辑为真(频率大于超限参数(L2闪烁))时,L2 的点亮状态全由 Led_Flag(闪烁标志位)决定。
      处于频率为负数时,直接对Freq < 0进行逻辑判断,那么当频率为负数是输出为1,此时L1点亮。
    	/**LED相关**/
    	ucLed[0] = Led_Flag & (!Seg_Disp_Mode);
    	ucLed[1] = (Led_Flag & (V_OutPut == 5)) || (Freq < 0);
    
    

    调试时发现的问题

    最终代码

    User文件

    main.c

    /*头文件声明区*/
    #include <STC15F2K60S2.H>
    #include "Init.h"
    #include "Seg.h"
    #include "Key.h"
    #include "Led.h"
    #include "iic.h"
    #include "ds1302.h"
    #include "math.h"
    
    /*变量声明区*/
    unsigned char Key_Down,Key_Val,Key_Up,Key_Old;
    unsigned char Seg_Buf[8] = {10,10,10,10,10,10,10,10};
    unsigned char Seg_Point[8] = {0,0,0,0,0,0,0,0};
    unsigned char Seg_Pos;
    unsigned char ucLed[8] = {0,0,0,0,0,0,0,0};
    unsigned int Slow_Down;
    bit Seg_Flag,Key_Flag;
    unsigned int Time_1s;
    long int Freq;
    
    //界面相关变量
    unsigned char Seg_Disp_Mode;           //界面显示模式变量 0-频率界面 1-参数界面 2-时间界面 3-回显界面
    bit Seg_DM1_Mode;                      //参数界面模式变量 0-超限参数界面 1-校准值参数界面
    int Seg_Set_Arr[2] = {2000,0};         //参数数组 [0]-超限参数 [1]-校准值变量
    unsigned char ucRtc[3] = {13,03,05};   //时间参数数组 [0]-时 [1]-分 [2]-秒
    bit Seg_DM3_Mode;                      //回显界面模式变量 0-频率回显界面 1-时间回显界面
    unsigned int Max_Freq;                 //最大频率值变量
    unsigned int Max_Freq_Time[3];         //最大频率发生时间数组
    
    //DAC相关变量
    float V_OutPut;                        //输出电压变量
    
    
    //按键功能相关变量
    int code Step_Arr[2] = {1000,100};     //单次加减大小数组 [0]-超限参数单次增加量/减小量 [1]-校准参数单次增加量/减小量
    int code Max_Arr[2] = {9000,900};      //上限值数组 [0]-超限参数上限值 [1]-校准参数上限值
    int code Min_Arr[2] = {1000,-900};     //下限值数组 [0]-超限参数下限值 [1]-校准参数下限值
    
    //LED指示灯功能相关变量
    unsigned char Time_200ms;              //闪烁间隔计时变量
    bit Led_Flag;                          //闪烁标志位
    
    /*按键处理函数*/
    void Key_Proc()
    {
    	if(Key_Flag) return;
    	Key_Flag = 1;
    	
    	Key_Val = Key_Read();
    	Key_Down = Key_Val & (Key_Val ^ Key_Old);
    	Key_Old = Key_Val;
    	
    	switch(Key_Down)
    	{
    		case 4://“界面”按键
    //			 /*分开写*/
    //			if(++Seg_Disp_Mode == 1)  Seg_DM1_Mode = 0;   //默认显示超限参数子界面
    //			if(Seg_Disp_Mode == 3)  Seg_DM3_Mode = 0;     //默认显示频率回显子界面
    //			if(Seg_Disp_Mode == 4)  Seg_Disp_Mode = 0;    //退回到最初界面
    			
    		   /*合在一起写*/
    			if(++Seg_Disp_Mode == 4) 
    				Seg_DM1_Mode = Seg_DM3_Mode = Seg_Disp_Mode = 0;
    		break;
    		
    		case 5://“选择”按键
    			if(Seg_Disp_Mode == 1)
    				Seg_DM1_Mode ^= 1;
    			else if(Seg_Disp_Mode == 3)
    				Seg_DM3_Mode ^= 1;
    		break;
    		
    		case 8://“加”按键
    			if(Seg_Disp_Mode == 1 )
    			{
    				Seg_Set_Arr[Seg_DM1_Mode] += Step_Arr[Seg_DM1_Mode];
    				if(Seg_Set_Arr[Seg_DM1_Mode] >= Max_Arr[Seg_DM1_Mode])
    					Seg_Set_Arr[Seg_DM1_Mode] = Max_Arr[Seg_DM1_Mode];
    			}
    		break;
    		
    		case 9://“减”按键
    			if(Seg_Disp_Mode == 1 )
    			{
    				Seg_Set_Arr[Seg_DM1_Mode] -= Step_Arr[Seg_DM1_Mode];
    				if(Seg_Set_Arr[Seg_DM1_Mode] < Min_Arr[Seg_DM1_Mode])
    					Seg_Set_Arr[Seg_DM1_Mode] = Min_Arr[Seg_DM1_Mode];
    			}
    		break;
    	}
    }
    
    /*信息处理函数*/
    void Seg_Proc()
    {
    	unsigned char i = 3;
    	unsigned char j = 0;
    	
    	if(Seg_Flag) return;
    	Seg_Flag = 1;
    	
    	/*更新时钟*/
    	Read_Rtc(ucRtc);
    	
    	/*获取最大频率及其发生时间*/
    	if(Max_Freq < Freq)
    	{
    		Max_Freq = Freq;
    		for(j = 0;j < 3;j ++)
    			Max_Freq_Time[j] = ucRtc[j];
    	}
    	
    	switch(Seg_Disp_Mode)
    	{
    		case 0://频率界面
    			Seg_Buf[0] = 12;//F
    		  Seg_Buf[1] = Seg_Buf[2] = 10;
    		if(Freq >= 0)
    		{
    			Seg_Buf[3] = Freq / 10000 % 10;
    		  Seg_Buf[4] = Freq / 1000 % 10;
    		  Seg_Buf[5] = Freq / 100 % 10;
    			Seg_Buf[6] = Freq / 10 % 10;
    			Seg_Buf[7] = Freq % 10;
    			
    			 /*高位为0时熄灭*/
    		  while(Seg_Buf[i] == 0)
    			{
    				Seg_Buf[i] = 10;
    				if(++i == 7) break;
    			}
    		}
    		else
    		{
    			Seg_Buf[3] = Seg_Buf[4] = Seg_Buf[5] = 10;//熄灭
    			Seg_Buf[6] = Seg_Buf[7] = 16;//LL
    		} 
    		break;
    		
    		case 1://参数界面
    			Seg_Buf[0] = 14;//P
    		  Seg_Buf[1] = (unsigned char)Seg_DM1_Mode+1;
    		  Seg_Buf[2] = Seg_Buf[3] = 10;
    		
    //		  if(Seg_DM1_Mode == 0)//超限参数界面
    //			{
    //				Seg_Buf[4] = Seg_Set_Arr[0] /1000 % 10;
    //				Seg_Buf[5] = Seg_Set_Arr[0] /100 % 10;
    //				Seg_Buf[6] = Seg_Set_Arr[0] /10 % 10;
    //				Seg_Buf[7] = Seg_Set_Arr[0] % 10;
    //			}
    //			else//校准值参数界面
    //			{
    //				if(Seg_Set_Arr[1] != 0)
    //				{
    //					/*确定符号位*/
    //					if(Seg_Set_Arr[1] < 0)
    //						Seg_Buf[4] = 15;//-
    //					else 
    //						Seg_Buf[4] = 10;//熄灭
    //					
    //					
    //					Seg_Buf[5] = abs(Seg_Set_Arr[1]) /100 % 10;
    //				  Seg_Buf[6] = abs(Seg_Set_Arr[1]) /10 % 10;
    //				  Seg_Buf[7] = abs(Seg_Set_Arr[1]) % 10;
    //				}
    //				else
    //				{
    //					Seg_Buf[5] = Seg_Buf[6] = 10;
    //				  Seg_Buf[7] = 0;
    //				}				
    //			}
    
    			Seg_Buf[4] = Seg_DM1_Mode?(10 + 5 * (Seg_Set_Arr[Seg_DM1_Mode] < 0)):abs(Seg_Set_Arr[Seg_DM1_Mode]) /1000 % 10;
    			Seg_Buf[5] = abs(Seg_Set_Arr[Seg_DM1_Mode]) /100 % 10;
    	    Seg_Buf[6] = abs(Seg_Set_Arr[Seg_DM1_Mode]) /10 % 10;
    			Seg_Buf[7] = abs(Seg_Set_Arr[Seg_DM1_Mode]) % 10;
    		  
    		  /*校准值为0时,只显示一位0*/
    		  if(Seg_Set_Arr[Seg_DM1_Mode] == 0)
    				Seg_Buf[5] = Seg_Buf[6] = 10;
       	break;
    		
    		case 2://时间界面
    			for(j = 0;j < 3;j++)
    			{
    				Seg_Buf[j * 3] = ucRtc[j] / 10;
    				Seg_Buf[j * 3 + 1] = ucRtc[j] %10;
    			}
    			Seg_Buf[2] = Seg_Buf[5] = 15;//-
    		break;
    		
    		case 3://回显界面
    			Seg_Buf[0] = 13;//H
    			if(Seg_DM3_Mode == 0)//频率回显界面
    			{
    				Seg_Buf[1] = 12;//F
    				Seg_Buf[2] = 10;
    				Seg_Buf[3] = Max_Freq / 10000 % 10;
    				Seg_Buf[4] = Max_Freq / 1000 % 10;
    				Seg_Buf[5] = Max_Freq / 100 % 10;
    				Seg_Buf[6] = Max_Freq / 10 % 10;
    				Seg_Buf[7] = Max_Freq % 10;
    				
    				/*高位为0时熄灭*/
    				while(Seg_Buf[i] == 0)
    				{
    					Seg_Buf[i] = 10;
    					if(++i == 7) break;
    				}
    			}
    			else//时间回显界面
    			{
    				Seg_Buf[1] = 11;//A
    				for(j = 0;j < 3;j ++)
    				{
    					Seg_Buf[2+2*j] =  Max_Freq_Time[j] / 10;
    					Seg_Buf[2+2*j+1] =  Max_Freq_Time[j] % 10;
    				}
    			}
    		break;
    	}
    }
    
    /*其他显示函数*/
    void Led_Proc()
    {
    	/*DAC相关*/
    	if(Freq < 0) V_OutPut = 0;
    	else if(Freq >= 0 && Freq <= 500) V_OutPut = 1;
    	else if(Freq >= Seg_Set_Arr[0]) V_OutPut = 5;
    	else V_OutPut = 1 + (float)(5.0 - 1.0)/(Seg_Set_Arr[0] - 500) * (Freq - 500);
    	Da_Write(V_OutPut * 51);
    	
    	/**LED相关**/
    	ucLed[0] = Led_Flag & (!Seg_Disp_Mode);
    	ucLed[1] = (Led_Flag & (V_OutPut == 5)) || (Freq < 0);
    }
    
    /*定时器0初始化函数*/
    void Timer0Init(void)		//1毫秒@12.000MHz
    {
    	AUXR &= 0x7F;		//定时器时钟12T模式
    	TMOD &= 0xF0;		//设置定时器模式
    	TMOD |= 0x05;		
    	TL0 = 0;		//设置定时初值
    	TH0 = 0;		//设置定时初值
    	TF0 = 0;		//清除TF0标志
    	TR0 = 1;		//定时器0开始计时
    }
    
    /*定时器1初始化函数*/
    void Timer1Init(void)		//1毫秒@12.000MHz
    {
    	AUXR &= 0xBF;		//定时器时钟12T模式
    	TMOD &= 0x0F;		//设置定时器模式
    	TL1 = 0x18;		//设置定时初值
    	TH1 = 0xFC;		//设置定时初值
    	TF1 = 0;		//清除TF1标志
    	TR1 = 1;		//定时器1开始计时
    	ET1 = 1;
    	EA = 1;
    }
    
    /*定时器1中断服务函数*/
    void Timer1_Isr(void) interrupt 3
    {
    	/*数码管90ms的减速*/
    	if(++Slow_Down == 90)
    	{
    		Seg_Flag = Slow_Down = 0;
    	}
    	
    	/*按键10ms的减速*/
    	if(Slow_Down % 10 == 0)
    	{
    		Key_Flag = 0;
    	}
    	
    	/*1s的计数*/
    	if(++Time_1s == 1000)
    	{
    		Time_1s = 0;
    		Freq = TH0 << 8 | TL0;   //计算频率
    		Freq += Seg_Set_Arr[1];  //加上校准值
    		TH0 = 0;
    		TL0 = 0;
    	}
    	
    	/*信息处理*/
    	Seg_Disp(Slow_Down % 8, Seg_Buf[Slow_Down % 8],Seg_Point[Slow_Down % 8]);
    	Led_Disp(Slow_Down % 8,ucLed[Slow_Down % 8]);
    	
    	/*LED闪烁200ms计时*/
    	if(++Time_200ms == 200)
    	{
    		Time_200ms = 0;
    		Led_Flag ^= 1;
    	}
    }
    
    /*主函数*/
    void main()
    {
    	Sys_Init();
    	Timer0Init();
    	Timer1Init();
    	Set_Rtc(ucRtc);
    	while(1)
    	{
    		Key_Proc();
    		Seg_Proc();
    		Led_Proc();
    	}
    }
    

    Driver文件

    init.c

    #include "Init.h"
    
    void Sys_Init()
    {
    	P0 = 0xff;
    	P2 = P2 & 0x1f | 0x80;
    	P2 &= 0x1f;
    	
    	P0 = 0x00;
    	P2 = P2 & 0x1f | 0xa0;
    	P2 &= 0x1f;
    }
    

    Key.c

    #include "Key.h"
    
    unsigned char Key_Read()
    {
    	unsigned char temp=0;
    	
    	P44 = 0; P42 = 1; P35 = 1; P34 = 1;
    	if(P33 == 0) temp = 4;
    	if(P32 == 0) temp = 5;
    	if(P31 == 0) temp = 6;
    	if(P30 == 0) temp = 7;
    	
    	P44 = 1; P42 = 0; P35 = 1; P34 = 1;
    	if(P33 == 0) temp = 8;
    	if(P32 == 0) temp = 9;
    	if(P31 == 0) temp = 10;
    	if(P30 == 0) temp = 11;
    	
    	P44 = 1; P42 = 1; P35 = 0; P34 = 1;
    	if(P33 == 0) temp = 12;
    	if(P32 == 0) temp = 13;
    	if(P31 == 0) temp = 14;
    	if(P30 == 0) temp = 15;
    	
    	  /*P34引脚和NE555冲突*/
    //	P44 = 1; P42 = 1; P35 = 1; P34 = 0;
    //	if(P33 == 0) temp = 16;
    //	if(P32 == 0) temp = 17;
    //	if(P31 == 0) temp = 18;
    //	if(P30 == 0) temp = 19;
    
    	return temp;
    }
    

    Led.c

    #include "Led.h"
    
    void Led_Disp(unsigned char addr,enable)
    {
    	unsigned char temp = 0x00;
    	unsigned char temp_old = 0xff;
    	
    	if(enable)
    		temp |= 0x01 << addr;
    	else
    		temp &= ~(0x01 << addr);
    	
    	if(temp != temp_old)
    	{
    		P0 = ~temp;
    	  P2 = P2 & 0x1f | 0x80;
    	  P2 &= 0x1f;
    		temp_old = temp;
    	}
    }
    

    Seg.c

    #include "Seg.h"
    
    code unsigned char Seg_wela[] = {0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};
    code unsigned char Seg_dula[] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0xff,0x88,0x8e,0x89,0x8c,0xBF,0xC7};//0-9,10-熄灭,11-A,12-F,13-H,14-P,15--,16-L
    
    void Seg_Disp(unsigned char wela,dula,point)
    {
    	P0 = 0xff;
    	P2 = P2 & 0x1f | 0xe0;
    	P2 &= 0x1f;
    	
    	P0 = Seg_wela[wela];
    	P2 = P2 & 0x1f | 0xc0;
    	P2 &= 0x1f;
    	
    	P0 = Seg_dula[dula];
    	if(point)
    		P0 &= 0x7f;
    	P2 = P2 & 0x1f | 0xe0;
    	P2 &= 0x1f;
    }
    

    iic.c

    #include "iic.h"
    #include "intrins.h"
    #define DELAY_TIME	10
    
    sbit scl = P2 ^ 0;
    sbit sda = P2 ^ 1;
    
    //
    static void I2C_Delay(unsigned char n)
    {
        do
        {
            _nop_();_nop_();_nop_();_nop_();_nop_();
            _nop_();_nop_();_nop_();_nop_();_nop_();
            _nop_();_nop_();_nop_();_nop_();_nop_();		
        }
        while(n--);      	
    }
    
    //
    void I2CStart(void)
    {
        sda = 1;
        scl = 1;
    	I2C_Delay(DELAY_TIME);
        sda = 0;
    	I2C_Delay(DELAY_TIME);
        scl = 0;    
    }
    
    //
    void I2CStop(void)
    {
        sda = 0;
        scl = 1;
    	I2C_Delay(DELAY_TIME);
        sda = 1;
    	I2C_Delay(DELAY_TIME);
    }
    
    //
    void I2CSendByte(unsigned char byt)
    {
        unsigned char i;
    	
        for(i=0; i<8; i++){
            scl = 0;
    		I2C_Delay(DELAY_TIME);
            if(byt & 0x80){
                sda = 1;
            }
            else{
                sda = 0;
            }
    		I2C_Delay(DELAY_TIME);
            scl = 1;
            byt <<= 1;
    		I2C_Delay(DELAY_TIME);
        }
    	
        scl = 0;  
    }
    
    //
    unsigned char I2CReceiveByte(void)
    {
    	unsigned char da;
    	unsigned char i;
    	for(i=0;i<8;i++){   
    		scl = 1;
    		I2C_Delay(DELAY_TIME);
    		da <<= 1;
    		if(sda) 
    			da |= 0x01;
    		scl = 0;
    		I2C_Delay(DELAY_TIME);
    	}
    	return da;    
    }
    
    //
    unsigned char I2CWaitAck(void)
    {
    	unsigned char ackbit;
    	
        scl = 1;
    	I2C_Delay(DELAY_TIME);
        ackbit = sda; 
        scl = 0;
    	I2C_Delay(DELAY_TIME);
    	
    	return ackbit;
    }
    
    //
    void I2CSendAck(unsigned char ackbit)
    {
        scl = 0;
        sda = ackbit; 
    	I2C_Delay(DELAY_TIME);
        scl = 1;
    	I2C_Delay(DELAY_TIME);
        scl = 0; 
    	sda = 1;
    	I2C_Delay(DELAY_TIME);
    }
    
    void Da_Write(unsigned char dat)
    {
    	I2CStart();
    	I2CSendByte(0x90);
    	I2CWaitAck();
    	I2CSendByte(0x41);
    	I2CWaitAck();
    	I2CSendByte(dat);
    	I2CWaitAck();
    	I2CStop();
    }
    

    ds1302.c

    #include "ds1302.h"
    #include "intrins.h"
    
    sbit SCK = P1 ^ 7;
    sbit SDA = P2 ^ 3;
    sbit RST = P1 ^ 3;
    //
    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 Set_Rtc(unsigned char *ucRtc)
    {
    	unsigned char i;
    	
    	Write_Ds1302_Byte(0x8e,0x00);
    	for(i = 0;i < 3; i++)
    		Write_Ds1302_Byte(0x84 - 2 * i, ucRtc[i] / 10 % 10 << 4 | ucRtc[i] % 10);
    	Write_Ds1302_Byte(0x8e,0x80);
    }
    
    void Read_Rtc(unsigned char *ucRtc)
    {
    	unsigned char i;
    	unsigned temp;
    	
    	for(i = 0; i < 3; i++)
    	{
    		temp = Read_Ds1302_Byte(0x85 - 2 * i);
    		ucRtc[i] = temp /16 * 10 + temp % 16;
    	}
    }
    
    
    

    结语

    这套题整理也花了不少时间,但在整理的同时也让我对这套题的解法和思路有了更清晰的认知,收获还是蛮大的。
    感谢你能看到这里,希望这篇笔记不仅对我有所帮助,也能对你能有所启发。
    那么,文章里面有问题的话,也感谢大家指出。

    作者:LK_07

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【蓝桥杯单片机专题】第十五届省赛真题解析与思路梳理

    发表回复