基于定时器的倒计时程序设计
模块化编程
User–用户(main)
Driver–底层(固定)
建立工程模板–编写底层函数(Key,Seg)
.c–编写底层函数
.h–声明底层函数
步骤
-
头文件
-
变量声明
-
Key_Proc()
-
Seg_Proc()
-
Led_Proc()
-
Timer0Init()
-
Server
-
main
数码管

数码管显示界面
看到数码管里有多种显示界面的,二话不说,直接设置一个变量unsigned char Seg_Mode();//数码管显示界面
-
0–显示界面
-
1–设置界面
按键/数码管延时
unsigned char Key/Seg_Slow_Down;
这是最常见的用法,用来记录 “减速按键” 的当前状态,比如:
-
0:减速按键未按下 / 未触发
1:减速按键已按下 / 触发有效
也可能扩展为多状态(如
2表示长按、3表示短按)
-
在定时器中断服务中
// ******** 必须保留:按键减速标志清零 ******** if(Key_Slow_Down) Key_Slow_Down--; if(Seg_Slow_Down) Seg_Slow_Down--;
-
在处理函数开头加锁定
// 按键处理函数中:锁定按键,防止频繁触发 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();
}
}
基于定时器的时钟程序设计
时钟显示界面
数码管显示

-
unsigned char Seg_Disp_Mode; //数码管显示模式 0–时钟显示界面 1–时钟设置界面
-
显示时钟,分钟,和小时—用数组很方便
unsigned char Clock_Disp[3]={23,59,55};
-
在信息处理函数中,有俩个方法可以使数码管上电是初始化时间
-
法一: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; }
-
计时(利用中断)
-
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; } } } }
关于显示中“.”的显示方法
-
显示.的二进制:1000 0000
十六进制:0x80
如果是“2.”: 0x56|0x80 (“|”相当于加法)
-
要改的话只能在底层改,也就是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; } -
改完之后,在main.c中添加变量point
unsigned char Seg_Point[6]={0,1,0,1,0,1};
-
然后在中断服务中用
Seg_Disp(Seg_Pos,Seg_Buf[Seg_Pos],Seg_Point[Seg_Pos]);
时钟设置界面(按键)
s1–系统切换到时钟设置界面
设置和显示要用两个不同的数组,这样数据互不干扰
-
时钟设置数组:unsigned char CLock_Set[3];
-
哪个时间按下的按键,就要在模式变成设置之前,把当时时间赋值给数组
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-秒
-
选中的数码管以0.5/次的周期闪烁
unsigned int Timer_500ms;
bit Seg_FLag; //数码管闪烁标志位
在定时器中断服务中服务
-
先让数码管显示正常的界面,然后利用一个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; } -
简化
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; } } -
为什么时间匹配只做「置位标志」,不直接操作硬件?
-
这段函数会在
main循环中反复执行,只要时间匹配,就会反复触发硬件操作,可能导致外设抖动。 -
用
Alarm_Enable_Flag作为中间标志,一旦置 1 就保持报警状态,直到被外部逻辑(如按键)置 0,报警更稳定,也符合「闹钟到点后持续报警,直到用户干预」的使用习惯。
-