蓝桥杯单片机 第三周

基于定时器的倒计时程序设计

模块化编程

User–用户(main)

Driver–底层(固定)

建立工程模板–编写底层函数(Key,Seg)

.c–编写底层函数

.h–声明底层函数

步骤

  1. 头文件

  2. 变量声明

  3. Key_Proc()

  4. Seg_Proc()

  5. Led_Proc()

  6. Timer0Init()

  7. Server

  8. main

数码管

![image-20260127213528173](file:///D:/softword/typora/%E7%AC%94%E8%AE%B0/%E8%93%9D%E6%A1%A5%E6%9D%AF%E5%8D%95%E7%89%87%E6%9C%BA%E7%AC%AC%E4%B8%80%E5%91%A8.assets/image-20260127213528173.png?lastModify=1770356509)

数码管显示界面

看到数码管里有多种显示界面的,二话不说,直接设置一个变量unsigned char Seg_Mode();//数码管显示界面

  • 0–显示界面

  • 1–设置界面

按键/数码管延时

unsigned char Key/Seg_Slow_Down;

这是最常见的用法,用来记录 “减速按键” 的当前状态,比如:

  1. 0:减速按键未按下 / 未触发

    1:减速按键已按下 / 触发有效

    也可能扩展为多状态(如 2 表示长按、3 表示短按)

  • 在定时器中断服务中

         // ******** 必须保留:按键减速标志清零 ********
         if(Key_Slow_Down) Key_Slow_Down--;
         if(Seg_Slow_Down) Seg_Slow_Down--;
    
  1. 在处理函数开头加锁定

     // 按键处理函数中:锁定按键,防止频繁触发
     void Key_Proc()
     {
         if(Key_Slow_Down) return;  // 已锁定,直接返回
         Key_Slow_Down = 1;         // 未锁定,置1锁定,开始处理按键逻辑
         // ... 后续按键处理代码
     }
     ​
     // 数码管处理函数中:锁定缓冲区,防止频繁更新
     void Seg_Proc()
     {
         if(Seg_Slow_Down) return;  // 已锁定,直接返回
         Seg_Slow_Down = 1;         // 未锁定,置1锁定,开始更新缓冲区
         // ... 后续数码管更新代码
     }
    

模式标识

 void Seg_Proc()
 {
     if(Seg_Slow_Down) return;
     Seg_Slow_Down=1;
     Seg_Buf[0]=Seg_Mode+1;
     if(Seg_Mode=0)//系统处于显示界面
     {
      Seg_Buf[4] = Time_Count / 10 % 10;  // 倒计时十位
      Seg_Buf[5] = Time_Count % 10;       // 倒计时个位
     }
     else if(Seg_Mode == 1)  // 系统处于【设置界面】:展示预设参数(可加闪烁)
     {
         // 可配合Seg_Flag标志位实现闪烁,提升交互体验
         if(Seg_Flag == 1)
         {
             Seg_Buf[4] = Seg_Dat[Seg_Dat_Indix] / 10 % 10;
             Seg_Buf[5] = Seg_Dat[Seg_Dat_Indix] % 10;
         }
         else
         {
             Seg_Buf[4] = 10;  // 10代表数码管熄灭
             Seg_Buf[5] = 10;
         }
 }
 ​

倒计时

unsigned int Timer_1000ms; //1000毫秒标志位

unsigned char Time_Count = 30; //系统计时变量

bit System_Flag; // 倒计时启停标志(0=暂停,1=开始,可选,提升灵活性)

将下面代码放入定时器中断服务函数

/30秒倒计时/

 if(++Timer_1000ms==1000)
 {
     Timer_1000ms=0;
     Time_Count--;
     if(Time_Count==255)//倒计时结束后重置为0
     Time_Count=0;
 }

bit只有两个值0/1,常常作为标志位

异或

  • Seg_Mod^=1; 核心功能是将Seg_Mod在 0 和 1 之间翻转,实现二态切换。

  • 语法上等价于Seg_Mod = Seg_Mod ^ 1;,利用按位异或 “相同为 0,不同为 1” 的规则实现翻转。

  • 实际开发中常用于模式切换、开关控制等二态场景,代码简洁高效。

按键

s1-开始倒计时

 case 1:
     if(Seg_Mode == 0) // 仅显示界面生效,设置界面屏蔽
     {
         System_Flag = 1; // 置位启停标志,触发定时器中的递减逻辑
     }
     break;

s2-复位

 case 2:
     if(Seg_Mode == 0) // 仅显示界面生效
     {
         System_Flag = 0; // 暂停倒计时
         Time_Count = Seg_Dat[Seg_Dat_Indix]; // 复位为当前选中的预设参数
     }
     break;
  • 先暂停,后复位(核心顺序):若倒计时正在运行(System_Flag=1),直接修改Time_Count,会出现 “刚复位的数值被中断立即递减” 的冲突,导致数值跳变。先置System_Flag=0暂停倒计时,再修改数值,保证复位结果准确。

s3-显示与设置界面切换

 case 3:
     Seg_Mode ^= 1; // 二态翻转,0↔1(简洁高效,嵌入式二态切换首选)
     Time_Count = Seg_Dat[Seg_Dat_Indix]; // 切换后复位为当前选中参数
     System_Flag = 0; // 切换后默认暂停,避免自动运行
     break;

s4-参数在15-30-60间切换

 case 4:
     if(Seg_Mode == 1) // 仅设置界面生效,显示界面屏蔽
     {
         Seg_Dat_Indix++; // 数组索引自增
         if(Seg_Dat_Indix == 3) // 索引闭环,防止数组越界
         {
             Seg_Dat_Indix = 0;
         }
     }
     break;
  • 预设参数存储在Seg_Dat[3]={15,30,60}中,用Seg_Dat_Indix(0-2)作为数组索引,对应 15、30、60。

Led和蜂鸣器

当倒计时到 0 时,所有 LED 点亮,复位后熄灭。

当倒计时到 0 时,蜂鸣器使能,复位后停止。

 void Led_Proc()
 {
     if(Time_Count==0)
     {
         P1=0x00;
         P2_3=0;
     }
     else
     {
         P1=0xFF;
         P2_3=1;
     }
 }

模板

 /*库函数声明*/
 #include <REGX52.H>
 #include "Key.h"
 #include "Seg.h"
 ​
 /*变量声明区*/
 unsigned char Key_Val,Key_Down,Key_Old;//按键专用变量
 unsigned char Key_Slow_Down;//按键减速变量
 unsigned char Seg_Slow_Down;//数码管减速专用变量
 unsigned char Seg_Pos;//数码管扫描专用变量
 unsigned int Seg_Buf[6]={10,10,10,10,10,10};//数码管显示数据存储数组
 ​
 /*按键处理函数*/
 void Key_Proc()
 {
     if(Key_Slow_Down) return;
     Key_Slow_Down = 1;//按键减速程序
     
     Key_Val = Key_Read();//读取键码值
     Key_Down = Key_Val & (Key_Val ^ Key_Old);//检测下降沿
     Key_Old = Key_Val;//扫描辅助变量
     
 }
 ​
 /*信息处理函数*/
 void Seg_Proc()
 {
     if(Seg_Slow_Down) return;
     Seg_Slow_Down = 1;//数码管减速程序
     
 }
 ​
 /*其他显示函数*/
 void Led_Proc()
 {
     
 }
 ​
 /*定时器0中断初始化函数*/
 void Timer0Init(void)       //1毫秒@12.000MHz
 {
     TMOD &= 0xF0;       //设置定时器模式
     TMOD |= 0x01;       //设置定时器模式
     TL0 = 0x18;     //设置定时初值
     TH0 = 0xFC;     //设置定时初值
     TF0 = 0;        //清除TF0标志
     TR0 = 1;        //定时器0开始计时
     ET0=1;        //定时器0中断打开
     EA=1;        //总中断打开
 }
 ​
 /*定时器0中断服务函数*/
 void Timer0Ser() interrupt 1
 {
     TL0 = 0x18;     //设置定时初值
     TH0 = 0xFC;     //设置定时初值
     if(++Key_Slow_Down==10) Key_Slow_Down=0;//键盘减速专用
     if(++Seg_Slow_Down==10) Seg_Slow_Down=0;//数码管减速专用
     
     
     if(++Seg_Pos == 6) Seg_Pos=0;//数码管显示专用
     Seg_Disp(Seg_Pos,Seg_Buf[Seg_Pos]);
 ​
 }
 ​
 /*Main*/
 void main()
 {
     Timer0Init();
     while(1)
     {
         Key_Proc();
         Seg_Proc();
         Led_Proc();
     }
 }

基于定时器的时钟程序设计

时钟显示界面

数码管显示

![image-20260129164854527](file:///D:/softword/typora/%E7%AC%94%E8%AE%B0/%E8%93%9D%E6%A1%A5%E6%9D%AF%E5%8D%95%E7%89%87%E6%9C%BA%E7%AC%AC%E4%B8%80%E5%91%A8.assets/image-20260129164854527.png?lastModify=1770356509)

  1. unsigned char Seg_Disp_Mode; //数码管显示模式 0–时钟显示界面 1–时钟设置界面

  2. 显示时钟,分钟,和小时—用数组很方便

    unsigned char Clock_Disp[3]={23,59,55};

  3. 在信息处理函数中,有俩个方法可以使数码管上电是初始化时间

    • 法一:switch选择

       switch(Seg_Disp_Mode)
       {
          case 0:     //时钟显示界面
          Seg_Buf[0]=Clock_Disp[0]/10%10;  //数码管第一位显示小时的十位
          Seg_Buf[1]=Clock_Disp[0]%10;//显示个位
          
          Seg_Buf[2]=Clock_Disp[1]/10%10;
          Seg_Buf[3]=Clock_Disp[1]%10;
          
          Seg_Buf[4]=Clock_Disp[2]/10%10;
          Seg_Buf[5]=Clock_Disp[2]%10;
          break;
       }
      

      写完这步可以导入试试可不可以编译

    • 法二:For循环

      定义一个局部变量:unsigned char i;//用于for循环

       for(i=0;i<3;i++)
       {
        Seg_Buf[0+2*i]=Clock_Disp[i]/10%10;
        Seg_Buf[1+2*i]=Clock_Disp[i]%10;
       }
      

计时(利用中断)

  1. unsigned int Timer_1000ms; //计时专用变量

    放在“中断服务函数里”:每过一秒,秒针加1,等到60s后清零,分钟+1

     Timer_1000ms++;
     if(Timer_1000ms == 1000)
     {
        Timer_1000ms = 0;
        Clock_Disp[2]++;
        if(Clock_Disp[2] == 60)
        {
           Clock_Disp[2] = 0;
           Clock_Disp[1]++;
           if(Clock_Disp[1] == 60)
           {
             Clock_Disp[1] = 0;
             Clock_Disp[0]++;
             if(Clock_Disp[0] == 24)
             {
               Clock_Disp[0] = 0;
             }
           }
        }
     }
    

关于显示中“.”的显示方法

  1. 显示.的二进制:1000 0000

    十六进制:0x80

    如果是“2.”: 0x56|0x80 (“|”相当于加法)

  2. 要改的话只能在底层改,也就是Seg.c

    多加一个变量point

     #include "Seg.h"
     ​
     unsigned char Seg_Dula[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x00};//数码管段码储存数组 
     unsigned char Seg_Wela[] = {0xfe,0xfd,0xfb,0xf7,0xef,0xdf};//数码管位码储存数组
     ​
     void Seg_Disp(unsigned char Wela,unsigned char Dula,unsigned char point)
     {
         P0=0x00;     //消隐
         P2_6=1;
         P2_6=0;
         P0=Seg_Wela[Wela];
         P2_7=1;
         P2_7=0;
         if(point == 1)
         {
             P0=Seg_Dula[Dula]|0x80;
         }
         else
             P0=Seg_Dula[Dula]
         P2_6=1;
         P2_6=0;
     }
    
  3. 改完之后,在main.c中添加变量point

    unsigned char Seg_Point[6]={0,1,0,1,0,1};

  4. 然后在中断服务中用

    Seg_Disp(Seg_Pos,Seg_Buf[Seg_Pos],Seg_Point[Seg_Pos]);

时钟设置界面(按键)

s1–系统切换到时钟设置界面

设置和显示要用两个不同的数组,这样数据互不干扰

  1. 时钟设置数组:unsigned char CLock_Set[3];

  2. 哪个时间按下的按键,就要在模式变成设置之前,把当时时间赋值给数组

     switch(Key_Down)
     {
         CLock_Set[0] = Clock_Disp[0];      小时
         CLock_Set[1] = Clock_Disp[1];      分钟
         CLock_Set[2] = Clock_Disp[2];      秒
         Seg_Disp_Mode = 1;
     break;
     }
    

设置时-闪烁(时/分钟/秒)

  • 利用指针

unsigned char Clock_Set_Index; //0-小时 1-分钟 2-秒

  1. 选中的数码管以0.5/次的周期闪烁

    unsigned int Timer_500ms;

    bit Seg_FLag; //数码管闪烁标志位

    在定时器中断服务中服务

  2. 先让数码管显示正常的界面,然后利用一个switch选择覆盖原有界面

     swtich(Clock_Set_Index)
     {
        case 1:
           Seg_Buf[0]=Seg_Flag?Clock_Set[0]/10%10:10;
           Seg_Buf[1]=Seg_Flag?Clock_Set[0]%10:10;
        break;
        case 2:
           Seg_Buf[2]=Seg_Flag?Clock_Set[1]/10%10:10;
           Seg_Buf[3]=Seg_Flag?Clock_Set[1]%10:10;
        break;
        case 3:
           Seg_Buf[4]=Seg_Flag?Clock_Set[2]/10%10:10;
           Seg_Buf[5]=Seg_Flag?Clock_Set[2]%10:10;
        break;
     }
    
  3. 简化

     Seg_Buf[0+Clock_Set_Index*2]=Seg_Flag?Clock_Set[Clock_Set_Index]/10%10:10;
     Seg_Buf[1+Clock_Set_Index*2]=Seg_Flag?Clock_Set[Clock_Set_Index]%10:10;
    

s3–设置中时分秒的切换

 if(Seg_Disp_Mode == 1) //只有在设置界面才能加减
 {
     Clock_Set_Index++;
     if(Clock_Set_Index == 3)
    {
      Clock_Set_Index = 0;
    }
 }

s5/s6–加/减

数组里的第指针位加加

Clock_Set[Clock_Set_Index]++;

加到上限值变为下限值

if(Clock_Set[Clock_Set_Index] == (Clock_Set_Index == 0?24:60));

Clock_Set[Clock_Set_Index] = 0;

减到下限值变为上限值

一个char型的数据,他的下限永远都是255

if(Clock_Set[Clock_Set_Index] == 255);

Clock_Set[Clock_Set_Index] == Clock_Set_Index == 0?23:59;

s7–确认

确认之前先保存

保存后在返回

s8–不保存

Seg_Disp_Mode = 0;

闹钟设置界面

定义两个闹钟的数组

unsigned char Alarm[]={0,0,0};

unsigned char Alarm_Set[3];

s4–保存闹钟设置

bit Alarm_Falg = 1; //1-闹钟开启,0-关闭

Alarm_Falg^=1;(case 4 决定)

led灯的闪烁

  • 高四位闪烁,第四位不变0xf0

  • 低四位闪烁,高四位不变0x0f

     if(Clock_Disp[0] >= 12)
        Led ^= 0xf0;
     else
        Led ^= 0x0f;
    
  •  void Led_Proc()
     {
         // 第一重判断:闹钟功能是否整体开启(Alarm_Flag=1 表示开启)
         if(Alarm_Flag == 1)
         {
             // 子判断1:判断当前实时时钟是否与闹钟时间完全匹配(时、分、秒都一致)
             if(Clock_Disp[0] == Alarm[0] && Clock_Disp[1] == Alarm[1] && Clock_Disp[2] == Alarm[2] )
             {
                 // 若完全匹配,置位闹钟触发标志,标记「需要报警」
                 Alarm_Enable_Flag = 1;
             }
     ​
             // 子判断2:根据闹钟触发标志,控制硬件外设
             if(Alarm_Enable_Flag == 1)
             {
                 // 触发报警:按Led值点亮P1端口的LED,拉低P2_3开启外接外设(如蜂鸣器)
                 P1 = Led;
                 P2_3 = 0;
             }
             else
             {
                 // 未触发报警:P1端口置0xff(所有LED熄灭),拉高P2_3关闭外接外设
                 P1 = 0xff;
                 P2_3 = 1;
             }
         }
         // 闹钟功能整体关闭(Alarm_Flag=0)
         else
         {
             // 直接关闭所有报警外设,无论时间是否匹配
             P1 = 0xff;
             P2_3 = 1;
         }
     }
    
  • 为什么时间匹配只做「置位标志」,不直接操作硬件?

    1. 这段函数会在main循环中反复执行,只要时间匹配,就会反复触发硬件操作,可能导致外设抖动。

    2. Alarm_Enable_Flag作为中间标志,一旦置 1 就保持报警状态,直到被外部逻辑(如按键)置 0,报警更稳定,也符合「闹钟到点后持续报警,直到用户干预」的使用习惯。