你是否好奇过电子时钟的实现机理?其实在本质上与机械时钟原理是一致的,都需要一个频率极度稳定且精确的振荡装置,用于精确计数。由于振荡频率已知,从而实现计时。
在单片机中,定时器是非常重要的片内资源,依靠晶振实现精确计时。本章,我们来谈一谈定时器。
本节完整工程文件已上传GitHub,仓库地址,欢迎下载交流!
晶振,即石英晶体振荡器,誉为单片机的心脏。
从一块石英晶体上按一定方位角切下薄片,并在两端施加电场,晶体会产生机械变形。反之,若在晶片的两侧施加机械压力,则在晶片相应的方向上将产生电场,这种物理现象称为压电效应。
晶振正是利用压电效应制成的,对晶振两端施加直流电压(多种频率的叠加),晶振会和其固有频率一致的电波产生共振(类似于选频器),这个输出频率十分的稳定。
晶振一般分为石英晶体谐振器(Quartz Crystal, XTAL
)和石英晶体振荡器(Crystal Oscillator, XO
)两种。
晶振的主要参数
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
):晶体的频率会根据串联的电容而改变。用于设计振荡电路。12T
单片机(STC89C52为12T
单片机),机器周期为振荡周期的12分频;对于1T
单片机,机器周期等于振荡周期,不分频。因此1T
单片机运行速度理论上为12T
单片机的12倍。Complex Instruction Set Computing
,CISC),特点是每个指令执行时间不一。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 ×机器周期,即可得到时长。
关于定时器,需要强调两点:
一个定时器一共有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 |
GATE:门控位。决定定时器的启动是否受外部中断的影响
C/T:定时计数器选择位。0为定时器,1为计数器。
M1/M0:工作方式设置。
M1/M0 | 定时器工作方式 |
---|---|
00 | 13位定时器/计数器 |
01 | 16位定时器/计数器(常用于定时器) |
10 | 8位自动重装定时器/计数器 (常用于串口) |
11 | T0分为两个独立的8位定时器/计数器;T1停止计数 |
TMOD
)赋值,确定定时器 T0 或 T1 的工作方式。TMOD |= 0X01;
TH0 = (65536-9216)/256; TL0 = (65536-9216)%256;
ET0 = 1; EA = 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,则
因为低八位最大值为 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定时器提供外部控制的方式(相当于结合了外部中断和定时器),有捕获、自动重装、波特率发生器三种模式。
TH2
,TL2
中的值捕获至寄存器RCAP2H
,RCAP2L
中。TH2
,TL2
默认向上计数溢出,RCAP2H
,RCAP2L
中的值自动重装至TH2
,TL2
中。属于16位自动重装。类似的,我们需要熟悉T2定时器相关的寄存器。
寄存器 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
T2CON | TF2 | EXF2 | RCLK | TCLK | EXEN2 | TR2 | C/T2 | CP/RL2 |
1
:当外部使能时(EXEN2 = 1
),T2EX(P1.1)
负跳变产生捕获。0
:当外部失能时(EXEN2 = 0
),定时器溢出和T2EX
负跳变均触发自动重装。0
定时器,1
外部事件计数器(下降沿触发)1
启动T2, 0
停止1
允许外部T2EX
控制定时器,0
对 T2EX
跳变不响应。1
T2作为串口时钟,0
T1作为串口时钟(默认)1
T2作为串口时钟,0
T1作为串口时钟(默认)EXEN2 = 1
),T2EX
负跳变,EXF2置位。ET2 = 1
),会进入中断服务程序。该标志须软件清零。ET2 = 1
),会进入中断服务程序。该标志须软件清零。寄存器 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
T2MOD | - | - | - | - | - | - | T2OE | DCEN |
0
。0
。T2MOD |= 0X00;
T2CON = 0x08;
TL2 = RCAP2L = 0xA0; TH2 = RCAP2H = 0xff;
EA
),使能定时器2中断(ET2
)。ET2 = 1; EA = 1;
TR2 = 1;
注:由于定时器2可以有外部触发和内部触发两种方式,所以在中断服务程序中需要判断EXF2
,TF2
两个标志位,以确定中断的触发源头。
在学习定时器之前,我们都是通过简单的软件延时来实现定时的效果。我们可以用定时器来测试一下延时函数的实际延时的时间和精度。
main.c
#include "smg.h"
void time0_init(){
TMOD |= 0x01; // 配置定时器0工作方式为16位定时器
TH0 = TL0 = 0; // 初值设置为0
TR0 = 1; // 开启定时器
}
int main(void){
u16 num; // 存放计数器的值
time0_init(); // 定时器初始化
delay_ms(1); // 延时
num = (TH0 << 8) + TL0; // 得到此时定时器的值
while(1){
// 显示计数次数
smg_showInt(num, 1); // 换算时间 num * 12 / f (us)
}
}
由于只需要记录定时器值的变化,不用考虑溢出,所以不用开启定时器溢出中断。
数码管显示结果为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
软件延时大致还是比较准确的。
在按键篇中,我们采用软件延时消除按键抖动,如今,我们可以尝试用更加高效的定时器来完成。
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、TL0
TL0 = (65536-num)%256; //低8位
TH0 = (65536-num)/256; //高8位
//3.打开中断总开关EA和定时器中断允许位ET0
EA = 1;
ET0 = 1;
//4.打开定时器
// TR0 = 1;
break;
case 1:
//1.配置TMOD工作方式
TMOD |= 0x10; //配置定时器T1,同时不改变T0的配置
//2.计算初值,存入TH1、TL1
TL1 = (65536-num)%256; //低8位
TH1 = (65536-num)/256; //高8位
//3.打开中断总开关EA和定时器中断允许位ET1
EA = 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 0000
KEY3_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); // 中断0
TIMERx_init(0, time_init); // 定时器0
TR0 = 1; // 开启定时器0
while(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次判断,从而保证了按键响应的可靠性。
注:对于不同的按键,其机械性能不同,振动时间也有区别,一般可以通过调节定时间隔和判断次数来不断调试,直到按键响应稳定可靠。
学习了定时器后,我们可以实现一些按键的复杂用法。通过按键篇我们知道,按键事件可以分为长按,短按,双击,组合键等。
所谓的长短,即考察按下响应和释放响应之间的时间差。一般的,我们不妨规定,按键持续按下小于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 0000
KEY1_PRESS = 0x01, // 0000 0001
KEY2_PRESS = 0x02, // 0000 0010
KEY3_PRESS = 0x04, // 0000 0100
KEY4_PRESS = 0x08, // 0000 1000
KEY1_2_PRESS = 0x03, // 0000 0011
KEY1_3_PRESS = 0x05, // 0000 0101
KEY1_4_PRESS = 0x09, // 0000 1001
KEY2_3_PRESS = 0x06, // 0000 0110
KEY2_4_PRESS = 0x0A, // 0000 1010
KEY3_4_PRESS = 0x0C, // 0000 1100
}Key_State;
// 按键事件模式枚举
typedef enum{
FREE_MODE = 0xff, // 按键模式待确定阶段
SHORT_PRESS = 0x00, // 0000 0000
LONG_PRESS = 0x01, // 0000 0001
DOUBLE_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); // 中断0
INTx_init(1); // 中断1
TIMERx_init(0, time_init); // 定时器0
TR0 = 1; // 开启定时器0
while(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();
}
在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 4
0x6d,0x7d,0x07,0x7f,0x6f, //5 6 7 8 9
0x77,0x7c,0x58,0x5e,0x79, //A b c d E
0x71,0x76,0x30,0x0e,0x38, //F H I J L
0x54,0x3f,0x73,0x67,0x50, //n o p q r
0x6d,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~7
A1 = 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);
// 延时1ms
delay_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(){
// 配置定时器0
TIMERx_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)
主要实现了矩阵按键的定时器扫描。
matrix_key.h
#ifndef _MATRIX_KEY_H_
#define _MATRIX_KEY_H_
#include "public.h"
#define MATRIX_PORT P1
// 矩阵按键单次响应(0)或连续响应(1)开关
#define MatrixKEY_MODE 0
sbit 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,且所有行置1
row = 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,且所有行置1
row = 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); // 2ms
TR0 = 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);
}
STC89C52
单片机定时器中断不宜太过频繁。相较之下,中断处理的时间过长,导致主循环不断被打断,影响主循环顺利执行。对于微秒级别的时序,一般还是通过延时(比如_nop_()
)去处理。本章节将之前各模块包含延时的部分重新封装,可以根据需要自行选择、扩展或剪裁。本章是一个重要的分水岭。