目录
- 前言
- 晶振概述
- 时序概述
- 定时器概述
- 工作方式寄存器(TMOD)
- 定时器配置
- 初值的简便算法
- 微秒级定时中断的注意事项
- T2定时器概述
- 定时器2控制寄存器(T2CON)
- 定时器2模式寄存器(T2MOD)
- 定时器2配置
- 软件实现
- 1. 定时器测试延时精度
- 2. 单个独立按键的定时器消抖
- 3. 按键事件封装(短按、长按、双击、组合键)
- 4. 数码管的定时器刷新
- 5. 矩阵按键的定时器扫描检测
- 遇到的问题
- 总结
前言
你是否好奇过电子时钟的实现机理?其实在本质上与机械时钟原理是一致的,都需要一个频率极度稳定且精确的振荡装置,用于精确计数。由于振荡频率已知,从而实现计时。
在单片机中,定时器是非常重要的片内资源,依靠晶振实现精确计时。本章,我们来谈一谈定时器。
本节涉及到的封装源文件可在《模块功能封装汇总》中找到。
本节完整工程文件已上传GitHub,仓库地址,欢迎下载交流!
晶振概述
晶振,即石英晶体振荡器,誉为单片机的心脏。
从一块石英晶体上按一定方位角切下薄片,并在两端施加电场,晶体会产生机械变形。反之,若在晶片的两侧施加机械压力,则在晶片相应的方向上将产生电场,这种物理现象称为压电效应。
晶振正是利用压电效应制成的,对晶振两端施加直流电压(多种频率的叠加),晶振会和其固有频率一致的电波产生共振(类似于选频器),这个输出频率十分的稳定。
晶振一般分为石英晶体谐振器(Quartz Crystal, XTAL
)和石英晶体振荡器(Crystal Oscillator, XO
)两种。
- 谐振器即无源晶振,需要外部接振荡电路
- 振荡器即有源晶振,已内置振荡电路
晶振的主要参数
- 标称频率:即晶振的理想输出频率。常用的有8MHZ、11.0592MHZ、12MHZ、16MHZ等等。
- 调整频差(
Frequency Tolerance
):在25℃下晶振的输出频率与标称频率的偏差。一般用单位ppm(parts per million
, 1 0 − 6 10^{-6} 10−6)、ppb(parts per billion
, 1 0 − 9 10^{-9} 10−9)表示。 - 负载电容(
Load Capacitance
):晶体的频率会根据串联的电容而改变。用于设计振荡电路。
时序概述
- 振荡周期:又称时钟周期,即晶振振荡一次的时间。
T = 1 f o s c T= \frac{1}{f_{osc}} T=fosc1 - 状态周期:由振荡周期二分频得到。
T = 1 f o s c 2 = 2 f o s c T= \frac{1}{\frac{f_{osc}}{2}}=\frac{2}{f_{osc}} T=2fosc1=fosc2 - 机器周期:即执行基本操作所需最短时间。对于
12T
单片机(STC89C52为12T
单片机),机器周期为振荡周期的12分频;对于1T
单片机,机器周期等于振荡周期,不分频。因此1T
单片机运行速度理论上为12T
单片机的12倍。
T = 1 f o s c 12 = 12 f o s c T= \frac{1}{\frac{f_{osc}}{12}}=\frac{12}{f_{osc}} T=12fosc1=fosc12 - 指令周期:执行一条指令所需的时间,通常由若干个机器周期组成。51单片机采用的是51指令集(Intel 8031指令集),显然它属于复杂指令集运算(
Complex Instruction Set Computing
,CISC),特点是每个指令执行时间不一。
T = n × 12 f o s c T=n\times \frac{12}{f_{osc}} T=n×fosc12
对于1T
单片机,已经可以实现单指令周期,即执行一条指令只需一个机器周期。
定时器概述
定时器一般指定时计数器,本质是一个计数器,即每接收一个机器周期脉冲,计数器内部加1,直至溢出归0(向上计数),此时会向CPU申请溢出中断。由于晶振频率一定,也可以实现计时。
以51单片机为例,如果外接晶振12MHz,一个机器周期由12个振荡周期组成(12分频),故一个机器周期 T T T 为
T = 1 12 × 1 0 6 × 12 = 1 μ s T=\dfrac{1}{12\times 10^6}\times 12 = 1\mu s T=12×1061×12=1μs
那么由计数个数 × \times ×机器周期,即可得到时长。
关于定时器,需要强调两点:
- 定时器和中断是两个不同的概念,不存在包含与被包含的关系。我们完全可以只使用定时器而不启用中断。只不过在实际开发中,定时器与中断结合使用比较多。
- 定时器是独立于CPU的外设,只要上电就不停工作。因此在定时器中断程序的内容并不影响定时器的计时精度,延迟函数也只是使CPU保持空转。但对于程序本身而言,我们若希望准确记录定时器的每次溢出(不丢数),应当保证程序的畅通,否则从结果来看,计时是 " 不准的 "。
工作方式寄存器(TMOD)
一个定时器一共有16位,分为低8位(TL
)和高8位(TH
)。我们通过工作方式寄存器(TMOD
)来配置其工作方式。
寄存器 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
TMOD | GATE | C/T | M1 | M0 | GATE | C/T | M1 | M0 |
- 工作方式寄存器(TMOD):低四位配置T0,高四位配置T1。字节地址89H。
-
GATE:门控位。决定定时器的启动是否受外部中断的影响
- 置0:TR0 =1 ,TR1 = 1,定时器启动(常用)
- 置1:TR0 =1 ,TR1 = 1,INT0/INT1 = 1, 定时器启动
-
C/T:定时计数器选择位。0为定时器,1为计数器。
-
M1/M0:工作方式设置。
M1/M0 定时器工作方式 00 13位定时器/计数器 01 16位定时器/计数器(常用于定时器) 10 8位自动重装定时器/计数器 (常用于串口) 11 T0分为两个独立的8位定时器/计数器;T1停止计数
-
定时器配置
- 对工作方式寄存器(
TMOD
)赋值,确定定时器 T0 或 T1 的工作方式。TMOD |= 0X01;
- 如果使用中断,则计算初值(即计数目标次数后刚好发生溢出中断),并写入TH0和TL0、TH1和TL1,共16位。
TH0 = (65536-9216)/256; TL0 = (65536-9216)%256;
- 如果使用中断,则打开关总中断允许位(EA=1),使能定时器中断(ET0=1/ET1=1)。
ET0 = 1; EA = 1;
- 使TR0或TR1置1,启动定时计数器。
TR0 = 1;
初值的简便算法
已知定时器一共16位,故最大计数值为 2 16 − 1 = 65535 2^{16}-1=65535 216−1=65535,对于任意初值 x x x,如何计算它的高八位和低八位呢?
例如,希望计时1ms,对于12MHz晶振,需要计数1000次后溢出,则反推出定时器初值为 65536 − 1000 = 64536 65536-1000=64536 65536−1000=64536,则
- 高八位: T H 0 = 64536 / 256 = 252 TH0=64536/256=252 TH0=64536/256=252
- 低八位: T L 0 = 64536 % 256 = 24 TL0=64536\%256=24 TL0=64536%256=24
因为低八位最大值为 2 8 − 1 = 255 2^8-1=255 28−1=255,故存在非负整数 k = T H 0 < 256 k=TH0<256 k=TH0<256 和非负整数 b = T L 0 < 256 b=TL0<256 b=TL0<256,使 x = 256 × k + b x=256\times k+b x=256×k+b,因此以256整除即可得高八位(十进制),取余即可得低八位(十进制)。
注:这里要避免一个误区,给寄存器赋值,可以用任意进制,实际存储时都会转化成二进制。但在书写时,为了简洁,往往不会直接写二进制,而是用十六进制代替。
微秒级定时中断的注意事项
51单片机理论上单条指令运行周期为1微秒左右,在对定时器进行微秒级别的定时中断时,中断服务函数中定时器初值表达式的形式将大大影响定时中断的正常执行。
对于STC89C52芯片,开启毫秒级别的定时,采用
TH0 = (65536-9216)/256;
TL0 = (65536-9216)%256;
对定时器中断影响很小,因为这两句赋值语句所耗时间相对于计时时间短得多。但对于微秒级别的定时,重装初值就不再建议写成上述形式,因为数学运算所消耗的时间会比较长,可以通过事先计算初值,直接赋值的方式,缩短定时中断所占用的时间。
TH0 = 220;
TL0 = 0;
T2定时器概述
这里额外介绍一下新增的T2定时器。当项目比较复杂时,两个普通定时器难以满足需求,就可以使用T2定时器。T2定时器提供外部控制的方式(相当于结合了外部中断和定时器),有捕获和自动重装两种模式。
-
捕获:当外部引脚电平产生下降沿时,将此时寄存器
TH2
,TL2
中的值捕获至寄存器RCAP2H
,RCAP2L
中。 -
自动重装:
TH2
,TL2
默认向上计数溢出,RCAP2H
,RCAP2L
中的值自动重装至TH2
,TL2
中。
定时器2控制寄存器(T2CON)
类似的,我们需要熟悉T2定时器相关的寄存器。
寄存器 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
T2CON | TF2 | EXF2 | RCLK | TCLK | EXEN2 | TR2 | C/T2 | CP/RL2 |
- 定时器2控制寄存器(T2CON):设置定时器2的基本配置。字节地址C8H。
- CP/RL2:捕获、重装标志。
1
当外部使能时(EXEN2 = 1
),T2EX(P1.1)
负跳变产生捕获。0
当外部失能时(EXEN2 = 0
),定时器溢出和T2EX
负跳变均触发自动重装。 - C/T2:定时器/计数器选择位,
0
定时器,1
外部事件计数器(下降沿触发) - TR2:启动控制位,
1
启动T2,0
停止 - EXEN2:外部使能标志。
1
允许外部T2EX
控制定时器,0
对T2EX
跳变不响应。 - TCLK:发送时钟标志。
1
T2作为串口时钟,0
T1作为串口时钟(默认) - RCLK:接收时钟标志。
1
T2作为串口时钟,0
T1作为串口时钟(默认) - EXF2:外部触发标志。当外部使能时(
EXEN2 = 1
),T2EX
负跳变,EXF2置位。T2中断使能时(ET2 = 1
),会进入中断服务程序。该标志须软件清零。 - TF2:定时器溢出标志。当T2中断使能时(
ET2 = 1
),会进入中断服务程序。该标志须软件清零。
- CP/RL2:捕获、重装标志。
定时器2模式寄存器(T2MOD)
寄存器 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
T2MOD | - | - | - | - | - | - | T2OE | DCEN |
- 定时器2模式寄存器(T2MOD):设置定时器2的模式。字节地址C9H。
- DCEN:向下计数使能位。一般置
0
。 - T2OE:输出使能位。一般置
0
。
- DCEN:向下计数使能位。一般置
定时器2配置
- 对模式寄存器(T2MOD)赋值,确定定时器 T2 为向上计数。
T2MOD |= 0X00;
- 配置控制寄存器(T2CON),确定是否外部使能,及捕获或重装模式。
T2CON = 0x08;
- 计算初值(即计数目标次数后刚好发生溢出中断),并写入TH2和TL2、(用于重装)RCAP2H和RCAP2L,共16位。
TL2 = RCAP2L = 0xA0; TH2 = RCAP2H = 0xff;
- 如果使用中断,则打开关总中断允许位(EA=1),使能定时器2中断(ET2=1)。
ET2 = 1; EA = 1;
- 使TR2置1,启动定时器2。
TR2 = 1;
注:由于定时器2可以有外部触发和内部触发两种方式,所以在中断服务程序中需要判断EXF2
,TF2
两个标志位,以确定中断的触发源头。
软件实现
1. 定时器测试延时精度
在学习定时器之前,我们都是通过简单的软件延时来实现定时的效果。我们可以用定时器来测试一下延时函数的实际延时的时间和精度。
main.c
#include "smg.h"
/** ** @brief 定时器测试延时精度** @author QIU** @data 2023.08.31**//*-------------------------------------------------------------------*/void time0_init(){TMOD |= 0x01; // 配置定时器0工作方式为16位定时器TH0 = TL0 = 0; // 初值设置为0TR0 = 1; // 开启定时器
}void main(){u16 num; // 存放计数器的值time0_init(); // 定时器初始化delay_ms(1); // 延时num = (TH0 << 8) + TL0; // 得到此时定时器的值while(1){// 显示计数次数smg_showInt(num, 1);}
}
由于只需要记录定时器值的变化,不用考虑溢出,所以不用开启定时器溢出中断。
数码管显示结果为954
,由于板载晶振为11.0592MHZ,可得
t = 954 × 1 × 1 0 3 11.0592 × 1 0 6 12 = 1.03515625 m s t = 954\times \frac{1\times 10^{3}}{\frac{11.0592\times10^{6}}{12} }=1.03515625\ ms t=954×1211.0592×1061×103=1.03515625 ms
软件延时大致还是比较准确的。
2. 单个独立按键的定时器消抖
在按键篇中,我们采用软件延时消除按键抖动,如今,我们可以尝试用更加高效的定时器来完成。
timer.h
#ifndef __TIMER_H__
#define __TIMER_H__#include "delay.h"void TIMERx_init(u8, u16);#endif
timer.c
#include "timer.h"
/** ** @brief 定时器封装** @author QIU** @data 2023.08.31**//*-------------------------------------------------------------------*//**** @brief 定时器x的初始化** @param num:定时器初值** @retval 无**/
void TIMERx_init(u8 x, u16 num){switch(x){case 0://1.配置TMOD工作方式TMOD |= 0x01; //配置定时器T0,同时不改变T1的配置//2.计算初值,存入TH0、TL0TL0 = (65536-num)%256; //低8位TH0 = (65536-num)/256; //高8位//3.打开中断总开关EA和定时器中断允许位ET0EA = 1;ET0 = 1;//4.打开定时器// TR0 = 1;break;case 1://1.配置TMOD工作方式TMOD |= 0x10; //配置定时器T1,同时不改变T0的配置//2.计算初值,存入TH1、TL1TL1 = (65536-num)%256; //低8位TH1 = (65536-num)/256; //高8位//3.打开中断总开关EA和定时器中断允许位ET1EA = 1;ET1 = 1;//4.打开定时器// TR1 = 1;break;}
}// 定时器0的中断服务程序模板
//void TIMER0_serve() interrupt 1{
// TL0 = (65536-num)%256; //低8位
// TH0 = (65536-num)/256; //高8位
//}// 定时器1的中断服务程序模板
//void TIMER1_serve() interrupt 3{
// TL1 = (65536-num)%256; //低8位
// TH1 = (65536-num)/256; //高8位
//}
key.h
#ifndef __KEY_H__
#define __KEY_H__#include "delay.h"// 按键单次响应(0)或连续响应(1)开关
#define KEY3_MODE 1// 按键引脚定义
sbit key3 = P3^2;// 按键基础状态枚举(低四位可表示4个独立按键)
typedef enum{KEY_UNPRESS = 0x00, // 0000 0000KEY3_PRESS = 0x04, // 0000 0100
}Key_State;// 外部声明时,尽量带上数据类型,否则会产生重复定义的错误
extern Key_State key_state;void scan_key();
void check_key();
void scan_key_ByTimer();#endif
key.c
#include "key.h"
// 包含的头文件(按需求修改)
#include "led.h"
#include "smg.h"
/** ** @brief 独立按键的定时器实现** 1. 单键的按下响应与松开响应** 2. 单键的单次响应与连续响应** @author QIU** @date 2023.08.31**//*-------------------------------------------------------------------*/// 实时按键状态、当前确认状态、前确认状态
Key_State key_state, key_now_state, key_pre_state;
// 按键累积
u16 num = 0;/*------------------------按键响应函数定义区------------------------------*//**** @brief 按键3按键响应** @param 参数说明** @retval 返回值**/
void key3_press(){num++;smg_showInt(num, 1);led_on(1);
}void key3_unpressed(){num = 0;smg_showChar('0', 1, false);led_turn(1);
}/*-------------------------------------------------------------------*//**** @brief 按键松开响应** @param 无** @retval 无**/
void key_unpress(){switch(key_pre_state){case KEY_UNPRESS: break;case KEY3_PRESS: key3_unpressed();break;}
}/*---------------------------定时器法按键检测--------------------------------*//**** @brief (轮询方式)判断按键状态与模式, 进行相应处理** @param 无** @retval 无**/
void check_key(){static bit flag = true;// 检查按键模式switch(key_now_state){case KEY3_PRESS:// 按下响应if(flag) key3_press();// 按键模式流转if(!KEY3_MODE) flag = false;break;case KEY_UNPRESS:// 按下响应key_unpress();key_pre_state = KEY_UNPRESS;flag = true;break;default:break;}}/**** @brief (定时器)确认按键是否稳定按下/松开,去抖** @param 无** @retval 无**/
void scan_key_ByTimer(){static u8 flag = 0xff;flag <<= 1;flag |= key3; // 检查键3引脚状态(按下为0)switch(flag){case 0xff:// 连续8次检测到按键松开,视为松开状态。key_state = KEY_UNPRESS;// 按键状态更新key_pre_state = key_now_state;key_now_state = key_state;break;case 0x00:// 更新按键状态key_pre_state = key_now_state;key_now_state = key_state;break;}
}
main.c
#include "timer.h"
#include "interrupt.h"
#include "key.h"
/** ** @brief 采用外部中断响应,定时器消抖结合的方式。实现单个按键功能demo** 1. 实现按下响应:key3键按下,数码管数字持续增加,led1保持常亮** 2. 实现松开响应:key3键松开,数码管数字归零,led1熄灭** @author QIU** @data 2023.08.31**//*-------------------------------------------------------------------*/// 定时器初值,延时1ms计算
u16 time_init = 922;void main(){INTx_init(0); // 中断0TIMERx_init(0, time_init); // 定时器0TR0 = 1; // 开启定时器0while(1){// 判断按键并处理check_key();}
}// 外部中断0的中断服务程序模板
void INT0_serve() interrupt 0{// 更新按键状态标志key_state |= KEY3_PRESS;
}// 定时器0的中断服务程序
void TIMER0_serve() interrupt 1{TL0 = (65536-time_init)%256; //低8位TH0 = (65536-time_init)/256; //高8位// 如果检测到实时按键按下if(key_state){// 开始检查确认按键实际状态,去抖scan_key_ByTimer();}
}
本例完整展示了一个独立按键的最佳处理程序。
比较关键的部分在定时器中断的scan_key_ByTimer()
函数中,通过定义一个8位的flag
变量,移位定时检查按键状态,当且仅当连续8次检测为低电平时,才视为按键按下。按键释放也同理。
利用定时器,可以避免延时所造成的CPU资源浪费,而仅是定时去检查按键的状态。再者,延时方法实际上只检查了两次按键状态,这对于按键状态的判断可信度并不高。显然,检查的次数越多,同一判断的可信度就越大。定时器方法可以实现8次甚至16次判断,从而保证了按键响应的可靠性。
注:对于不同的按键,其机械性能不同,振动时间也有区别,一般可以通过调节定时间隔和判断次数来不断调试,直到按键响应稳定可靠。
3. 按键事件封装(短按、长按、双击、组合键)
学习了定时器后,我们可以实现一些按键的复杂用法。通过按键篇我们知道,按键事件可以分为长按,短按,双击,组合键等。
所谓的长短,即考察按下响应和释放响应之间的时间差。一般的,我们不妨规定,按键持续按下小于300ms被视作短按,大于300ms被视作长按。
- 由于短按时间极短,一般仅考虑松开响应,且默认为单次响应。
- 长按一般仅考虑按下响应,可以分别考虑单次响应与连续响应。
双击即考察第一次按下释放某个键到第二次再次按下同一个键所经历的时长,且第一次按键须为短按。 一般的,我们不妨规定,按键在80ms内再次被按下可视作双击,否则被视作普通短按。
- 双击的判定会影响短按的响应时间。如果将双击容许时间调至
0
,即可视为删去双击响应(成立条件太苛刻)。 - 双击一般仅考虑按下响应,即对于第二次按下何时释放不再关心。一般默认为单次响应。
组合键即考察按下两个不同的按键之间的时间差。组合键(多键)与单键的逻辑实际上是一致的,组合键也有长按,短按,双击等响应,单键可以看作是组合键的一种特殊情况。
- 组合键要求在第一个被按下的按键确定按键模式之前按下新的按键,构成组合条件。这对部分灵活度不够的使用人员来说不够友好,实现苛刻。
key.h
#ifndef __KEY_H__
#define __KEY_H__#include "delay.h"// 按键单次响应(0)或连续响应(1)开关,针对长按事件
#define KEY1_MODE 0
#define KEY2_MODE 0
#define KEY3_MODE 0
#define KEY4_MODE 1
#define KEY_1_2_MODE 0
#define KEY_1_3_MODE 0
#define KEY_1_4_MODE 0
#define KEY_2_3_MODE 0
#define KEY_2_4_MODE 0
#define KEY_3_4_MODE 1// 按键时长(短按、长按分界)
# define KEY_DOWN_DURATION 300
// 双击时长(单击、双击分界),从第一次短按松开开始计算。取0时退化为单击
# define KEY_DOUBLE_DURATION 80// 按键引脚定义
sbit key1 = P3^1;
sbit key2 = P3^0;
sbit key3 = P3^2;
sbit key4 = P3^3;// 按键基础状态枚举(低四位可表示4个独立按键)
typedef enum{KEY_UNPRESS = 0x00, // 0000 0000KEY1_PRESS = 0x01, // 0000 0001KEY2_PRESS = 0x02, // 0000 0010KEY3_PRESS = 0x04, // 0000 0100KEY4_PRESS = 0x08, // 0000 1000KEY1_2_PRESS = 0x03, // 0000 0011KEY1_3_PRESS = 0x05, // 0000 0101KEY1_4_PRESS = 0x09, // 0000 1001KEY2_3_PRESS = 0x06, // 0000 0110KEY2_4_PRESS = 0x0A, // 0000 1010KEY3_4_PRESS = 0x0C, // 0000 1100
}Key_State;// 按键事件模式枚举
typedef enum{FREE_MODE = 0xff, // 按键模式待确定阶段SHORT_PRESS = 0x00, // 0000 0000LONG_PRESS = 0x01, // 0000 0001DOUBLE_CLICK = 0x02, // 0000 0010
}Key_Mode;// 外部声明时,尽量带上数据类型,否则会产生重复定义的错误
extern Key_State key_state;void scan_key();
void check_key();
void scan_double_click();
void scan_key_ByTimer();#endif
key.c
#include "key.h"
// 包含的头文件(按需求修改)
#include "led.h"
#include "smg.h"/** ** @brief 独立按键的函数封装** 1. 单键的短按与长按事件(长按事件分单次或连续响应)** 2. 单键的单击与双击事件** 3. 组合键的短按与长按事件** @author QIU** @date 2024.02.07**//*-------------------------------------------------------------------*/// 实时按键状态、当前确认状态、前确认状态
Key_State key_state, key_now_state, key_pre_state;
// 当前按键模式
Key_Mode key_mode = FREE_MODE;
// 双击标志
u8 Double_Click_flag = false;
// 双击最大延时计数器
u16 Double_Click_Counter = 0;// 按键累积
u16 num = 0;
// LED流水灯速度
u16 led_speed = 10000;/*------------------------按键响应函数定义区------------------------------*//**** @brief 按键3短按,短按响应,在按键松开后判定执行** @param 参数说明** @retval 返回值**/
void key3_short_press(){led_turn(1);
}/**** @brief 按键3长按,按下响应** @param 参数说明** @retval 返回值**/
void key3_long_press(){led_run(led_speed);
}/**** @brief 按键3双击** @param 参数说明** @retval 返回值**/
void key3_double_click(){led_turn(2);
}/**** @brief 按键4短按,短按响应,在按键松开后判定执行** @param 参数说明** @retval 返回值**/
void key4_short_press(){led_turn(3);
}/**** @brief 按键4长按,按下响应** @param 参数说明** @retval 返回值**/
void key4_long_press(){led_stream(led_speed);
}/**** @brief 按键四双击** @param 参数说明** @retval 返回值**/
void key4_double_click(){led_turn(4);
}/**** @brief 按键3、4组合键,短按响应** @param 参数说明** @retval 返回值**/
void key3_4_combination(){led_turn(5);
}/**** @brief 按键3、4组合键,长按响应** @param 参数说明** @retval 返回值**/
void key3_4_long_combination(){num++;smg_showInt(num, 1);
}/*--------------------------延时法按键检测-------------------------------*/#if 0/**** @brief 按键松开响应** @param 无** @retval 无**/
void key_unpress(){switch(key_pre_state){case KEY_UNPRESS: break;case KEY3: key3_unpressed();break;case KEY4: key4_unpressed();break;case KEY3_4: key3_4_unpressed();break;}
}/**** @brief (轮询方式)扫描独立按键,判断哪个键按下** @param 无** @retval 无**/
void scan_key(){static u8 flag = 1;// 如果有按键按下if(flag && (!key1||!key2||!key3||!key4)){flag = 0; // 清零delay_ms(10); // 延时10ms消抖delay_ms(50); // 延时50ms 容许间隔// 获取当前所有按下的键if(!key1) key_now_state |= KEY1; if(!key2) key_now_state |= KEY2; if(!key3) key_now_state |= KEY3; if(!key4) key_now_state |= KEY4; // 如果按键全部松开}else if(key1&&key2&&key3&&key4){flag = 1;delay_ms(10); // 延时10ms消抖,松开响应有逻辑判断时,需要加上消抖。否则可以省略。if(key1&&key2&&key3&&key4)key_now_state = KEY_UNPRESS;}
}#endif/*---------------------------定时器法按键检测--------------------------------*//**** @brief 双击事件检测** @param 参数说明** @retval 返回值**/
void scan_double_click(){if(Double_Click_flag){Double_Click_Counter++;// 如果已经超过了双击时间界线if(Double_Click_Counter >= KEY_DOUBLE_DURATION){// 上次短按视为单击事件key_mode = SHORT_PRESS;Double_Click_Counter = 0;Double_Click_flag = false;}}else{Double_Click_Counter = 0;}
}/**** @brief (轮询方式)判断按键状态与模式, 进行相应处理** @param 无** @retval 无**/
void check_key(){// 检查按键模式switch(key_mode){case SHORT_PRESS:// 按键模式流转key_mode = FREE_MODE;// 短按只有松开响应,故考虑前状态// 短按均为单次响应switch(key_pre_state){case KEY1_PRESS:break;case KEY2_PRESS:break;case KEY3_PRESS:key3_short_press();break;case KEY4_PRESS:key4_short_press();break;case KEY1_2_PRESS:break;case KEY1_3_PRESS:break;case KEY1_4_PRESS:break;case KEY2_3_PRESS:break;case KEY2_4_PRESS:break;case KEY3_4_PRESS:key3_4_combination();break;}break;case LONG_PRESS:// 长按只有按下响应,故考虑当前状态// 长按分为单次响应和连续响应switch(key_now_state){case KEY1_PRESS:break;case KEY2_PRESS:break;case KEY3_PRESS:if(!KEY3_MODE) key_mode = FREE_MODE;key3_long_press();break;case KEY4_PRESS:if(!KEY4_MODE) key_mode = FREE_MODE;key4_long_press();break;case KEY1_2_PRESS:break;case KEY1_3_PRESS:break;case KEY1_4_PRESS:break;case KEY2_3_PRESS:break;case KEY2_4_PRESS:break;case KEY3_4_PRESS:// 按键模式流转if(!KEY_3_4_MODE) key_mode = FREE_MODE;key3_4_long_combination();break;}break;case DOUBLE_CLICK:key_mode = FREE_MODE;// 双击只有按下响应,故考虑当前状态// 双击只有单次响应switch(key_now_state){case KEY1_PRESS:break;case KEY2_PRESS:break;case KEY3_PRESS:key3_double_click();break;case KEY4_PRESS:key4_double_click();break;case KEY1_2_PRESS:break;case KEY1_3_PRESS:break;case KEY1_4_PRESS:break;case KEY2_3_PRESS:break;case KEY2_4_PRESS:break;case KEY3_4_PRESS:break;}break;default:break;}}/**** @brief (定时器)确认按键是否稳定按下/松开,去抖** @param 无** @retval 无**/
void scan_key_ByTimer(){static u16 counter = 0; static u8 flag = 0xff;// 长按事件开关static bit flag_LongMode = true;// 双击事件开关static bit flag_DoubleClickMode = false;// 组合事件开关static bit flag_CombinationMode = false;flag <<= 1;if(key_state & KEY3_PRESS) flag |= key3; // 如果实时按键状态中包含键3,则检查键3引脚状态(按下为0)if(key_state & KEY4_PRESS) flag |= key4; // 如果实时按键状态中包含键4,则检查键4引脚状态(按下为0)switch(flag){case 0xff:// 连续8次检测到按键松开,视为松开状态。key_state &= ((~key3*KEY3_PRESS) | (~key4*KEY4_PRESS));// 按键状态更新key_pre_state = key_now_state;key_now_state = key_state;if(flag_DoubleClickMode){flag_DoubleClickMode = false;}else{if(flag_CombinationMode){// 解除屏蔽flag_CombinationMode = false;}else{// 如果小于阈值,则为短按操作if(counter < KEY_DOWN_DURATION){// 等待给定双击阈值,判断是否为双击事件Double_Click_flag = true;// 清零counter = 0;}else{// 长按只有按下响应key_mode = FREE_MODE;// 清零counter = 0;flag_LongMode = true;}}}// 如果键尚未完全松开,则剩余键被屏蔽if(key_now_state != KEY_UNPRESS) flag_CombinationMode = true;break;case 0x00:// 如果按键状态未变化if(key_state == key_now_state){if(flag_DoubleClickMode){// 如果是双击事件,则不再关心第二次单击所耗时长// 按住期间会屏蔽其他按键}else if(flag_CombinationMode){// 如果处于组合按键屏蔽的状态,不做响应}else{if(counter >= KEY_DOWN_DURATION){if(flag_LongMode){// 视为该键的长按模式flag_LongMode = false;key_mode = LONG_PRESS;}}else{counter++; // 开始计时}}}else{// 连续8次检测到按键按下,视为按下状态。// 如果按键动作相同且Double_Click_flag为真(即未超时),可视为双击事件if((key_state == key_pre_state) && Double_Click_flag){key_mode = DOUBLE_CLICK;flag_DoubleClickMode = true;Double_Click_flag = false;}else{// 重新计时(开始记录组合键时长)counter = 0;}// 更新按键状态key_pre_state = key_now_state;key_now_state = key_state;} break;}
}
main.c
#include "timer.h"
#include "interrupt.h"
#include "key.h"
/** ** @brief 采用外部中断响应,定时器消抖结合的方式。实现按键功能的总体实现** 1. 实现短按:key3键点亮led1,key4键点亮led3** 2. 实现长按:key3键跑马灯,key4键流水灯** 3. 实现双击:key3键点亮led2,key4键点亮led4** 4. 实现组合键:key3+key4短按点亮led5,长按数码管数字递增** 5. 实现按键屏蔽** @author QIU** @data 2024.02.07**//*-------------------------------------------------------------------*/// 定时器初值,延时1ms计算
u16 time_init = 922;void main(){INTx_init(0); // 中断0INTx_init(1); // 中断1TIMERx_init(0, time_init); // 定时器0TR0 = 1; // 开启定时器0while(1){// 判断按键并处理check_key();}
}// 外部中断0的中断服务程序模板
void INT0_serve() interrupt 0{// 更新按键状态标志key_state |= KEY3_PRESS;
}// 外部中断1的中断服务程序
void INT1_serve() interrupt 2{// 更新按键状态标志key_state |= KEY4_PRESS;
}// 定时器0的中断服务程序
void TIMER0_serve() interrupt 1{TL0 = (65536-time_init)%256; //低8位TH0 = (65536-time_init)/256; //高8位// 如果检测到实时按键按下if(key_state){// 开始检查确认按键实际状态,去抖scan_key_ByTimer();}// 双击检测开启scan_double_click();}
4. 数码管的定时器刷新
在LED篇中,我们采用延时方法实现数码管的刷新,显然刷新频率是难以控制的。现更新定时器刷新方法。
smg.h
#ifndef _SMG_H_
#define _SMG_H_#include "public.h"#define SMG_PORT P0 // 位选引脚,与38译码器相连
sbit A1 = P2^2;
sbit A2 = P2^3;
sbit A3 = P2^4;void smg_showChar(u8, u8, bit); // 静态字符显示函数
void smg_showString(u8*, u8); // 动态字符串显示函数(延时法)
void smg_showInt(int, u8); // 动态整数显示函数(延时法)
void smg_showFloat(double, u8, u8); // 动态浮点数显示函数(延时法)void smg_showString_Bytimer(u8*, u8); // 动态字符串显示函数(定时器法)#endif
smg.c
#include "smg.h"
/** ** @brief 数码管封装** 1. 延时刷新** (1) 字符静态显示:仅需一次输入。输入字符。可用于初始清屏。** (2) 字符串数据动态显示** (3) 浮点型数据动态显示:可以显示小数。** (4) 整型数据动态显示:可以显示负数。** 2. 定时器刷新** @author QIU** @date 2024.02.13**//*-------------------------------------------------------------------*///共阴极数码管字形码编码
u8 code smgduan[] = {0x3f,0x06,0x5b,0x4f,0x66, //0 1 2 3 40x6d,0x7d,0x07,0x7f,0x6f, //5 6 7 8 90x77,0x7c,0x58,0x5e,0x79, //A b c d E0x71,0x76,0x30,0x0e,0x38, //F H I J L0x54,0x3f,0x73,0x67,0x50, //n o p q r0x6d,0x3e,0x3e,0x6e,0x40};//s U v y - /**** @brief 指定第几个数码管点亮,38译码器控制位选(不对外声明)** @param pos:从左至右,数码管位置 1~8** @retval 无**/
void select_38(u8 pos){u8 temp_pos = 8 - pos; // 0~7A1 = temp_pos % 2; //高位temp_pos /= 2;A2 = temp_pos % 2; temp_pos /= 2;A3 = temp_pos % 2; //低位
}/**** @brief 解析数据并取得相应数码管字形码编码** @param dat:想要显示的字符** @retval 对应字形码编码值**/
u8 parse_data(u8 dat){switch(dat){case '0':case '1':case '2':case '3':case '4':case '5':case '6':case '7':case '8':case '9':return smgduan[dat-'0'];case 'a':case 'A':return smgduan[10];case 'b':case 'B':return smgduan[11];case 'c':case 'C':return smgduan[12];case 'd':case 'D':return smgduan[13];case 'e':case 'E':return smgduan[14];case 'f':case 'F':return smgduan[15];case 'h':case 'H':return smgduan[16];case 'i':case 'I':return smgduan[17];case 'j':case 'J':return smgduan[18];case 'l':case 'L':return smgduan[19];case 'n':case 'N':return smgduan[20];case 'o':case 'O':return smgduan[21];case 'p':case 'P':return smgduan[22];case 'q':case 'Q':return smgduan[23];case 'r':case 'R':return smgduan[24];case 's':case 'S':return smgduan[25];case 'u':case 'U':return smgduan[26];case 'v':case 'V':return smgduan[27];case 'y':case 'Y':return smgduan[28];case '-':return smgduan[29];default:return 0x00; //不显示}
}/**** @brief 根据输入的ASCII码,显示对应字符(1字节)** @param dat:字符数据,或其ASCII值** @param pos:显示位置 1~8** @retval 无**/
void smg_showChar(u8 dat, u8 pos, bit flag){// 解析点亮哪一个数码管select_38(pos);// 解析数据SMG_PORT = parse_data(dat);// 加标点if(flag) SMG_PORT |= 0x80;
}/*-------------------------------------------------------------------*/
/*-----------------------延时法刷新----------------------------------*/
/*-------------------------------------------------------------------*//**** @brief 延时法刷新** @param dat:字符数组,需以'\0'结尾** @param pos:显示位置** @param dot:小数点位置** @retval 无**/
void smg_flush_Bydelay(u8 dat[], u8 pos, u8 dot){u8 i;// 超出部分直接截断for(i=0;(i<9-pos)&&(dat[i]!='\0');i++){// 如果是小数点,跳过,往前移一位if(dat[i] == '.'){pos -= 1;continue;}// 显示smg_showChar(dat[i], pos+i, (dot == i+1)?true:false);// 延时1msdelay_ms(1);// 消影SMG_PORT = 0x00; }
}/**** @brief 显示字符串(动态显示)** @param dat:字符数组,需以'\0'结尾** @param pos:显示位置** @retval 无**/
void smg_showString(u8 dat[], u8 pos){u8 i = 0, dot = 0;// 先判断是否存在小数点while(dat[i]!='\0'){if(dat[i] == '.') break;i++;}// 记录下标点位置if(i < strlen(dat)) dot = i;// 延时法刷新smg_flush_Bydelay(dat, pos, dot);
}/**** @brief 数码管显示整数(含正负)** @param dat: 整数** @param pos: 显示位置** @retval 无**/
void smg_showInt(int dat, u8 pos){xdata u8 temp[9];sprintf(temp, "%d", dat); // 含正负smg_showString(temp, pos);
}/**** @brief 数码管显示浮点数(含小数点)** @param dat: 浮点数** @param len: 指定精度** @param pos: 显示位置** @retval 无**/
void smg_showFloat(double dat, u8 len, u8 pos){xdata u8 temp[10];int dat_now;dat_now = dat * pow(10, len) + 0.5 * (dat>0?1:-1); // 四舍五入(正负),由于浮点数存在误差,结果未必准确sprintf(temp, "%d", dat_now); // 含正负smg_flush_Bydelay(temp, pos, len?(strlen(temp) - len):0);
}/*-------------------------------------------------------------------*/
/*--------------------------定时器法刷新-----------------------------*/
/*-------------------------------------------------------------------*//**** @brief 数码管显示字符串(定时器法刷新)** @param dat:字符数组,需以'\0'结尾** @param pos:显示位置** @retval 返回值**/
void smg_showString_Bytimer(u8 dat[], u8 pos){// 数码管计数器, 小数点位置static u8 smg_counter = 0, dot_counter = 0, dot_port[8];// 暂存当前位置u8 temp;// 先消影SMG_PORT = 0x00; // 如果是小数点,跳出。if(dat[smg_counter] == '.'){// 记录小数点位置,下一轮刷新dot_port[smg_counter-1] = true;// 计数器后移一位smg_counter++;// 小数点计数器自增dot_counter++;return;}// 计算当前位置temp = pos+smg_counter-dot_counter;// 判断是否加小数点(检测到小数点的后面几位整体前移)smg_showChar(dat[smg_counter], temp, dot_port[smg_counter]);// 如果是结束符,跳出(超出部分截断)if(temp == 8 | dat[smg_counter] == '\0'){// 重置smg_counter = 0;// 根据标志决定是否清除小数点if(dot_counter){// 清零dot_counter = 0;}else{// 清空strcpy(dot_port, "");}return;}else{smg_counter++;}
}
main.c
#include "smg.h"
#include "timer.h"
/** ** @brief 数码管定时器刷新** @author QIU** @date 2024.02.13**//*-------------------------------------------------------------------*/u8 dat[] = "LOVE3.14a";
u8 pos = 1;void main(){// 配置定时器0TIMERx_init(0, 1843);TR0 = 1;// smg_showChar('f', 1, 0); // 静态字符显示示例while(1){// smg_showInt(-12345, 1); // 整数显示示例// smg_showString("Iloveyou", 1); // 字符串显示示例// smg_showFloat(-3.15678, 3, 1); // 浮点数显示示例}
}// 定时器0的中断服务程序模板
void TIMER0_serve() interrupt 1{// 重装初值TL0 = (65536-2765)%256; //低8位TH0 = (65536-2765)/256; //高8位smg_showString_Bytimer(float2String(-3.1415927, 6), pos);
// smg_showString_Bytimer(int2String(-6432, true), pos);
// smg_showString_Bytimer(dat, pos);
}
通过测试,TIME0_INIT = 2765
时,即定时3ms
基本察觉不出闪烁。TIME0_INIT = 1843
时,即定时2ms
效果很好。
可以计算出此时数码管的刷新率(一秒多少帧画面)
f = 1000 2 × 8 = 62.5 ( H z ) f=\frac{1000}{2\times 8}=62.5\ (Hz) f=2×81000=62.5 (Hz)
5. 矩阵按键的定时器扫描检测
主要实现了矩阵按键的定时器扫描。
matrix_key.h
#ifndef _MATRIX_KEY_H_
#define _MATRIX_KEY_H_#include "public.h"#define MATRIX_PORT P1// 矩阵按键单次响应(0)或连续响应(1)开关
#define MatrixKEY_MODE 0sbit ROW_PORT_1 = P1^7;
sbit ROW_PORT_2 = P1^6;
sbit ROW_PORT_3 = P1^5; // 共用了蜂鸣器引脚
sbit ROW_PORT_4 = P1^4;sbit COL_PORT_1 = P1^3;
sbit COL_PORT_2 = P1^2;
sbit COL_PORT_3 = P1^1;
sbit COL_PORT_4 = P1^0;// 对外声明键值
extern u8 key_val;// 矩阵按键反转法状态机
typedef enum{COL_Test = 0, // 列检测(空闲状态)Filter, // 滤抖ROW_Test, // 行检测
}Turn_State;void check_matrixKey_turn();
void check_matrixKey_scan();
void check_matrixKey_turn_ByTimer();
void check_matrixKey_scan_ByTimer();#endif
matrix_key.c
#include "matrix_key.h"/** ** @brief 实现了矩阵按键的两种扫描方式** 1. 实现了延时法和定时器法两种刷新方式** @author QIU** @date 2024.02.18**//*-------------------------------------------------------------------*/// 存储按下的行列
u8 row, col;
// 按键事件处理状态,true已处理,false未处理
u8 key_is_dealed = false;// 键值对应显示数值
u8 key_val = 0; // 反转法状态机
Turn_State turn_state = COL_Test;/**** @brief 读取电平** @param state: 0-列,1-行** @retval 返回列(行)数**/
u8 read_port(bit state){u8 dat;if(state) dat = MATRIX_PORT >> 4; // 如果是行,取高四位else dat = MATRIX_PORT & 0x0f; // 如果是列,取低四位// 从左上开始为第一行,第一列switch(dat){// 0000 1110 第4列(行)case 0x0e: return 4;// 0000 1101 第3列(行)case 0x0d: return 3;// 0000 1011 第2列(行)case 0x0b: return 2;// 0000 0111 第1列(行)case 0x07: return 1;// 0000 1111 没有按下case 0x0f: return 0xff;// 多键同时按下不响应default: return 0;}
}/**** @brief 矩阵按键处理函数** @param 参数说明** @retval 返回值**/
void key_pressed(){// 如果不是连续模式,则按键事件标记为已处理if(!MatrixKEY_MODE) key_is_dealed = true; // 数码管数据key_val = (row - 1) * 4 + (col - 1);
}/**** @brief (反转法)检测按键(单键),按住过程中屏蔽其他按键。同列需全部松开才能再次响应** @param 无** @retval 无**/
void check_matrixKey_turn(){// 所有行置低电平,列置高电平MATRIX_PORT = 0x0f;// 读取所有列电平col = read_port(0);// 如果按键松开if(col == 0xff) {key_is_dealed = false; return;}// 如果有效键按下,延时消抖else if(col && !key_is_dealed) delay_ms(10);else return; // 所有列置低电平,行置高电平MATRIX_PORT = 0xf0;// 读取所有行电平row = read_port(1);// 如果有键按下,响应if(row && row != 0xff) key_pressed();else return;
}/**** @brief (扫描法)检测按键,本例扫描列** @param 无** @retval 无**/
void check_matrixKey_scan(){u8 i;for(i=0;i<4;i++){MATRIX_PORT = ~(0x08>>i); // 逐列置0,且所有行置1row = read_port(1); // 读取行// 保证之前记录按下的列为当前扫描列if(!row && col == i+1) continue; // 当前扫描列无有效键按下else if(row == 0xff && col == i+1) {key_is_dealed = false; continue;}else if(row && !key_is_dealed){ // 有效键按下且为未处理状态delay_ms(10);row = read_port(1); // 再次读取行if(row && row != 0xff) {col = i+1;key_pressed();} }}
}/**** @brief (反转法)采用定时器** @param 参数说明** @retval 返回值**/
void check_matrixKey_turn_ByTimer(){static u8 counter = 0;switch(turn_state){case COL_Test:// 所有行置低电平,列置高电平MATRIX_PORT = 0x0f;// 读取所有列电平col = read_port(0);// 如果按键未按下(已松开)if(col == 0xff) {key_is_dealed = false; break;}// 如果有效键按下,且按键未处理时,状态流转else if(col && !key_is_dealed) turn_state = Filter;break;case Filter:counter++;// 一般定时1ms,即过滤10ms防抖if(counter >= 10){counter = 0; turn_state = ROW_Test;}break;case ROW_Test:// 所有列置低电平,行置高电平MATRIX_PORT = 0xf0;// 读取所有行电平row = read_port(1);// 如果有键按下,响应if(row && row != 0Xff) key_pressed();// 状态流转turn_state = COL_Test;break;}
}/**** @brief (扫描法)采用定时器** @param 无** @retval 无**/
void check_matrixKey_scan_ByTimer(){static u8 i, counter;// 开始滤抖if(counter){if(counter > 10){counter = 0;row = read_port(1); // 再次读取行if(row && row != 0xff) {col = i+1; key_pressed();}}else{counter++;}}else{MATRIX_PORT = ~(0x08>>i); // 逐列置0,且所有行置1row = read_port(1); // 读取行// 保证之前记录按下的列为当前扫描列if(row == 0xff && col == i+1) {key_is_dealed = false;}else if(row && row != 0xff && !key_is_dealed) {counter++; return;} // 有效键按下且为未处理状态i++;if(i >= 4) i = 0;}
}
main.c
#include "matrix_key.h"
#include "smg.h"
#include "timer.h"int main(void){TIMERx_init(0, 1843); // 2msTR0 = 1;while(1){
// check_matrixKey_turn();
// check_matrixKey_scan();}
}// 定时器0的中断服务程序模板
void TIMER0_serve() interrupt 1{TL0 = (65536-1843)%256; //低8位TH0 = (65536-1843)/256; //高8位// check_matrixKey_turn_ByTimer();check_matrixKey_scan_ByTimer();smg_showString_Bytimer(int2String(key_val, false), 1);}
遇到的问题
- 定时器中断服务程序中不宜添加过长代码,更不能有任何阻塞片段。
- 手动重装初值存在一定累计误差,条件允许使用8位自动重装。
- 手动重装初值需要在中断服务程序中首先执行,以减小计时损失。
- 手动重装初值的表达式尽可能简洁,复杂的计算式也会导致定时器中断结果异常。
STC89C52
单片机定时器中断不宜太过频繁。相较之下,中断处理的时间过长,导致主循环不断被打断,影响主循环顺利执行。对于微秒级别的时序,一般还是通过延时(比如_nop_()
)去处理。
总结
本章节将之前各模块包含延时的部分重新封装,可以根据需要自行选择、扩展或剪裁。本章是一个重要的分水岭。