【51单片机实验笔记】中断篇(二) 定时器与中断


前言

你是否好奇过电子时钟的实现机理?其实在本质上与机械时钟原理是一致的,都需要一个频率极度稳定且精确振荡装置,用于精确计数。由于振荡频率已知,从而实现计时

在单片机中,定时器是非常重要的片内资源,依靠晶振实现精确计时。本章,我们来谈一谈定时器

本节涉及到的封装源文件可在《模块功能封装汇总》中找到。

本节完整工程文件已上传GitHub仓库地址,欢迎下载交流!


晶振概述

晶振,即石英晶体振荡器,誉为单片机的心脏

从一块石英晶体上按一定方位角切下薄片,并在两端施加电场晶体会产生机械变形。反之,若在晶片的两侧施加机械压力,则在晶片相应的方向上将产生电场,这种物理现象称为压电效应

晶振正是利用压电效应制成的,对晶振两端施加直流电压(多种频率叠加),晶振会和其固有频率一致的电波产生共振(类似于选频器),这个输出频率十分的稳定

晶振一般分为石英晶体谐振器Quartz Crystal, XTAL)和石英晶体振荡器Crystal Oscillator, XO)两种。

  • 谐振器无源晶振,需要外部接振荡电路
  • 振荡器有源晶振,已内置振荡电路

晶振主要参数

  • 标称频率:即晶振理想输出频率。常用的有8MHZ11.0592MHZ12MHZ16MHZ等等。
  • 调整频差Frequency Tolerance):在25℃晶振输出频率标称频率偏差。一般用单位ppmparts per million 1 0 − 6 10^{-6} 106)、ppbparts per billion 1 0 − 9 10^{-9} 109)表示。
  • 负载电容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单片机(STC89C5212T单片机),机器周期振荡周期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 ComputingCISC),特点是每个指令执行时间不一
    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 ×机器周期,即可得到时长


关于定时器,需要强调两点:

  1. 定时器中断是两个不同的概念,不存在包含与被包含的关系。我们完全可以只使用定时器而不启用中断。只不过在实际开发中,定时器中断结合使用比较多。
  2. 定时器独立CPU的外设,只要上电不停工作。因此在定时器中断程序的内容并不影响定时器的计时精度,延迟函数也只是使CPU保持空转。但对于程序本身而言,我们若希望准确记录定时器的每次溢出(不丢数),应当保证程序的畅通,否则从结果来看,计时" 不准的 "

工作方式寄存器(TMOD)

一个定时器一共有16位,分为低8位TL)和高8位TH)。我们通过工作方式寄存器TMOD)来配置其工作方式。

寄存器76543210
TMODGATEC/TM1M0GATEC/TM1M0
  • 工作方式寄存器(TMOD低四位配置T0高四位配置T1。字节地址89H
    • GATE门控位。决定定时器的启动是否受外部中断的影响

      • 置0TR0 =1 ,TR1 = 1定时器启动(常用)
      • 置1TR0 =1 ,TR1 = 1,INT0/INT1 = 1定时器启动
    • C/T定时计数器选择位。0为定时器,1为计数器

    • M1/M0工作方式设置

      M1/M0定时器工作方式
      0013位定时器/计数器
      0116位定时器/计数器(常用于定时器
      108位自动重装定时器/计数器 (常用于串口
      11T0分为两个独立的8位定时器/计数器;T1停止计数

定时器配置

  1. 工作方式寄存器TMOD)赋值,确定定时器 T0T1 的工作方式。TMOD |= 0X01;
  2. 如果使用中断,则计算初值(即计数目标次数后刚好发生溢出中断),并写入TH0TL0TH1TL1,共16位。TH0 = (65536-9216)/256; TL0 = (65536-9216)%256;
  3. 如果使用中断,则打开关总中断允许位EA=1),使能定时器中断ET0=1/ET1=1)。ET0 = 1; EA = 1;
  4. 使TR0TR1置1,启动定时计数器。TR0 = 1;

初值的简便算法

已知定时器一共16位,故最大计数值 2 16 − 1 = 65535 2^{16}-1=65535 2161=65535,对于任意初值 x x x,如何计算它的高八位低八位呢?

例如,希望计时1ms,对于12MHz晶振,需要计数1000次后溢出,则反推出定时器初值 65536 − 1000 = 64536 65536-1000=64536 655361000=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 281=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定时器提供外部控制的方式(相当于结合了外部中断定时器),有捕获自动重装波特率发生器三种模式。

  • 捕获:当外部引脚电平产生下降沿时,将此时寄存器TH2TL2中的值捕获寄存器RCAP2HRCAP2L中。
  • 自动重装TH2TL2默认向上计数溢出RCAP2HRCAP2L中的值自动重装TH2TL2中。属于16位自动重装
  • 波特率发生器:可以作为串口时钟,默认为定时器T1

定时器2控制寄存器(T2CON)

类似的,我们需要熟悉T2定时器相关的寄存器

寄存器76543210
T2CONTF2EXF2RCLKTCLKEXEN2TR2C/T2CP/RL2
  • 定时器2控制寄存器(T2CON设置定时器2的基本配置。字节地址C8H
    • CP/RL2捕获重装标志。
      • 1 :当外部使能时(EXEN2 = 1),T2EX(P1.1) 负跳变产生捕获
      • 0 :当外部失能时(EXEN2 = 0),定时器溢出T2EX 负跳变均触发自动重装
    • C/T2:定时器/计数器选择位0 定时器1 外部事件计数器下降沿触发
    • TR2启动控制位1 启动T20 停止
    • EXEN2外部使能标志。1 允许外部T2EX控制定时器0T2EX 跳变不响应。
    • TCLK发送时钟标志。1 T2作为串口时钟0 T1作为串口时钟默认
    • RCLK接收时钟标志。1 T2作为串口时钟0 T1作为串口时钟默认
    • EXF2外部触发标志。
      • 外部使能时(EXEN2 = 1),T2EX 负跳变EXF2置位。
      • T2中断使能时(ET2 = 1),会进入中断服务程序该标志须软件清零
    • TF2:定时器溢出标志。当T2中断使能时(ET2 = 1),会进入中断服务程序该标志须软件清零

定时器2模式寄存器(T2MOD)

寄存器76543210
T2MOD------T2OEDCEN
  • 定时器2模式寄存器(T2MOD设置定时器2的模式。字节地址C9H
    • DCEN向下计数使能位。一般置0
    • T2OE输出使能位。一般置0

定时器2配置

  1. 模式寄存器T2MOD)赋值,确定定时器 T2 为向上计数。T2MOD |= 0X00;
  2. 配置控制寄存器T2CON),确定是否外部使能,及捕获重装模式。T2CON = 0x08;
  3. 计算初值,并写入TH2TL2RCAP2HRCAP2L(用于重装),共16位TL2 = RCAP2L = 0xA0; TH2 = RCAP2H = 0xff;
  4. 如果使用中断,则打开关总中断允许位EA),使能定时器2中断ET2)。ET2 = 1; EA = 1;
  5. 使TR2置1,启动定时器2TR2 = 1;

注:由于定时器2可以有外部触发内部触发两种方式,所以在中断服务程序中需要判断EXF2TF2两个标志位,以确定中断的触发源头


软件实现

1. 定时器测试延时精度

在学习定时器之前,我们都是通过简单的软件延时来实现定时的效果。我们可以用定时器来测试一下延时函数实际延时的时间精度

main.c

#include "smg.h"
/** 
 **  @brief    定时器测试延时精度
 **  @author   QIU
 **  @data     2023.08.31
 **/

/*-------------------------------------------------------------------*/

void time0_init(){
	TMOD |= 0x01;   // 配置定时器0工作方式为16位定时器
	TH0 = TL0 = 0;  // 初值设置为0
	TR0 = 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、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次判断,从而保证了按键响应可靠性

:对于不同的按键,其机械性能不同振动时间也有区别,一般可以通过调节定时间隔判断次数来不断调试,直到按键响应稳定可靠。


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 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();

}

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 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)


5. 矩阵按键的定时器扫描检测

主要实现了矩阵按键定时器扫描

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);
	
}

遇到的问题

  • 定时器中断服务程序中不宜添加过长代码,更不能有任何阻塞片段
  • 手动重装初值存在一定累计误差,条件允许使用8位自动重装
  • 手动重装初值需要在中断服务程序首先执行,以减小计时损失
  • 手动重装初值的表达式尽可能简洁复杂的计算式也会导致定时器中断结果异常
  • STC89C52单片机定时器中断不宜太过频繁。相较之下,中断处理的时间过长,导致主循环不断被打断,影响主循环顺利执行。对于微秒级别时序,一般还是通过延时(比如_nop_())去处理。

总结

本章节将之前各模块包含延时的部分重新封装,可以根据需要自行选择扩展剪裁。本章是一个重要的分水岭

  • 17
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悬铃木下的青春

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值