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

目录

  • 前言
  • 晶振概述
  • 时序概述
  • 定时器概述
    • 工作方式寄存器(TMOD)
    • 定时器配置
    • 初值的简便算法
    • 微秒级定时中断的注意事项
  • T2定时器概述
    • 定时器2控制寄存器(T2CON)
    • 定时器2模式寄存器(T2MOD)
    • 定时器2配置
  • 软件实现
    • 1. 定时器测试延时精度
    • 2. 单个独立按键的定时器消抖
    • 3. 按键事件封装(短按、长按、双击、组合键)
    • 4. 数码管的定时器刷新
    • 5. 矩阵按键的定时器扫描检测
  • 遇到的问题
  • 总结


前言

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

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

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

本节完整工程文件已上传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中。


定时器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 启动T2, 0 停止
    • 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. 计算初值(即计数目标次数后刚好发生溢出中断),并写入TH2TL2、(用于重装RCAP2HRCAP2L,共16位。TL2 = RCAP2L = 0xA0; TH2 = RCAP2H = 0xff;
  4. 如果使用中断,则打开关总中断允许位EA=1),使能定时器2中断ET2=1)。ET2 = 1; EA = 1;
  5. 使TR2置1,启动定时器2。TR2 = 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;  // 初值设置为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_())去处理。

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/735079.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

智慧城市的新引擎:物联网技术引领城市创新与发展

目录 一、引言 二、物联网技术与智慧城市的融合 三、物联网技术在智慧城市中的应用 1、智慧交通管理 2、智慧能源管理 3、智慧环保管理 4、智慧公共服务 四、物联网技术引领城市创新与发展的价值 五、挑战与前景 六、结论 一、引言 随着科技的日新月异&#xff0c;物…

注意!!墙裂推荐几个好用的实用小工具!一定会用到的!

前言 在开发的世界里&#xff0c;面对各种挑战和问题时&#xff0c;拥有一套合适的工具箱至关重要。这不仅能提升我们的工作效率&#xff0c;还能让复杂的任务变得简单&#xff0c;甚至在解决棘手问题的同时&#xff0c;还能让我们的心情略微舒畅。众所周知&#xff0c;有用的…

STM32F103 CubeMX ADC 驱动 PS2游戏摇杆控制杆传感器模块

STM32F103 CubeMX ADC 驱动 PS2游戏摇杆控制杆传感器模块 1. 工程配置1.1 配置debug口1.2 配置时钟1.3 配置ADC1.4 配置串口1.5 配置时钟1.6 生成工程 2. 代码编写2.1 串口代码2.2 ADC读取数据的代码 1. 工程配置 1.1 配置debug口 1.2 配置时钟 1.3 配置ADC 1.4 配置串口 1.5 …

笔记本电脑使用时需要一直插电吗?笔记本正确的充电方式

随着科技的不断发展&#xff0c;笔记本电脑已经成为人们日常生活和工作中不可或缺的电子设备。而在使用笔记本电脑时&#xff0c;很多人会有一个疑问&#xff0c;那就是笔记本电脑使用时需要一直插电吗&#xff1f;本文将就此问题展开讨论。 不一定需要一直插电&#xff0c;如果…

开源组件安全风险及应对

在软件开发的过程中&#xff0c;为了提升开发效率、软件质量和稳定性&#xff0c;并降低开发成本&#xff0c;使用开源组件是开发人员的不二选择&#xff08;实际上&#xff0c;所有软件开发技术的演进都是为了能够更短时间、更低成本地构建软件&#xff09;。这里的开源组件指…

面向对象设计之里氏替换原则

设计模式专栏&#xff1a;http://t.csdnimg.cn/4Mt4u 思考&#xff1a;什么样的代码才算违反里氏替换原则&#xff1f; 目录 1.里氏替换原则的定义 2.里氏替换原则与多态的区别 3.违反里氏替换原则的反模式 4.总结 1.里氏替换原则的定义 里氏替换原则&#xff08;Liskov S…

【Web开发】深度学习HTML(超详细,一篇就够了)

&#x1f493; 博客主页&#xff1a;从零开始的-CodeNinja之路 ⏩ 收录文章&#xff1a;【Web开发】深度学习html(超详细,一篇就够了) &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 HTML1. HTML基础1.1 什么是HTML1.2 认识HTML标签1.3 HTML文件基本…

【linux进程信号】信号的产生

【Linux进程信号】信号的产生 目录 【Linux进程信号】信号的产生信号概念生活中的信号技术应用角度的信号注意信号概念用kill -l命令可以察看系统定义的信号列表信号处理常见方式概览 产生信号通过终端按键产生信号调用系统函数向进程发信号由软件条件产生信号由硬件异常产生信…

Linux 理解进程

目录 一、基本概念 二、描述进程-PCB 1、task_struct-PCB的一种 2、task_ struct内容分类 三、组织进程 四、查看进程 1、ps指令 2、top命令 3、/proc文件系统 4、在/proc文件中查看指定进程 5、进程的工作目录 五、通过系统调用获取进程标示符 1、getpid()/get…

css--浮动

一. 浮动的简介 在最初&#xff0c;浮动是用来实现文字环绕图片效果的&#xff0c;现在浮动是主流的页面布局方式之一。 二. 元素浮动后的特点 &#x1f922;脱离文档流。&#x1f60a;不管浮动前是什么元素&#xff0c;浮动后&#xff1a;默认宽与高都是被内容撑开&#xff0…

Redis基础篇:初识Redis(认识NoSQL,单机安装Redis,配置Redis自启动,Redis客户端的基本使用)

目录 1.认识NoSQL2.认识Redis3.安装Redis1.单机安装Redis2.配置redis后台启动3.设置redis开机自启 4.Redis客户端1.Redis命令行客户端2.图形化桌面客户端 1.认识NoSQL NoSQL&#xff08;Not Only SQL&#xff09;数据库是一种非关系型数据库&#xff0c;它不使用传统的关系型数…

ORACLE Linux(OEL) - Primavera P6EPPM 安装及分享

引言 继上一期发布的CentOS版环境发布之后&#xff0c;近日我制作了基于ORACLE Linux的P6虚拟机环境&#xff0c;同样里面包含了全套P6 最新版应用服务 此虚拟机仅用于演示、培训和测试目的。如您在生产环境中使用此虚拟机&#xff0c;请先与Oracle Primavera销售代表取得联系…

【Spring】Spring状态机

1.什么是状态机 (1). 什么是状态 先来解释什么是“状态”&#xff08; State &#xff09;。现实事物是有不同状态的&#xff0c;例如一个自动门&#xff0c;就有 open 和 closed 两种状态。我们通常所说的状态机是有限状态机&#xff0c;也就是被描述的事物的状态的数量是有…

Python 一步一步教你用pyglet制作汉诺塔游戏

目录 汉诺塔游戏 1. 抓取颜色 2. 绘制圆盘 3. 九层汉塔 4. 绘制塔架 5. 叠加圆盘 6. 游戏框架 汉诺塔游戏 汉诺塔&#xff08;Tower of Hanoi&#xff09;&#xff0c;是一个源于印度古老传说的益智玩具。这个传说讲述了大梵天创造世界的时候&#xff0c;他做了三根金刚…

golang sync.Pool 指针数据覆盖问题

场景 1. sync.Pool设置 var stringPool sync.Pool{New: func() any {return new([]string)}, }func NewString() *[]string {v : stringPool.Get().(*[]string)return v }func PutString(s *[]string) {if s nil {return}if cap(*s) > 2048 {s nil} else {*s (*s)[:0]…

【Leetcode每日一刷】滑动窗口:209.长度最小的子数组

一、209.长度最小的子数组 1.1&#xff1a;题目 题目链接 1.2&#xff1a;解题思路 题型&#xff1a;滑动窗口&#xff1b;时间复杂度&#xff1a;O(n) &#x1faa7; 滑动窗口本质也是双指针的一种技巧&#xff0c;特别适用于字串问题 ❗❗核心思想/ 关键&#xff1a;左右…

【笔记】原油阳谋论

文章目录 石油的属性能源属性各国石油替代 金融属性黄金石油美元 油价历史油价传导路径 石油供需格局与发展供需格局各国状况美国俄罗斯沙特 产油国困境运输 分析格局分析供需平衡分析价差分析价差概念基本面的跨区模型跨区模型下的价差逻辑 长中短三期分析长期视角——供应看投…

【笔记】全国大学生GIS应用技能大赛练习总结

该总结笔记为小组成员在练习完毕了历届题目后自我总结的结果&#xff0c;如有不足之处可以在评论区提出&#xff0c;排版较乱往谅解 绘制带空洞的面要素&#xff1a; 法一&#xff1a; 1、矢量化整个区域。2、矢量化空洞区域。3、将矢量化空洞区域进行合并&#xff08;编辑器…

Spring MVC 全局异常处理器

如果不加以异常处理&#xff0c;错误信息肯定会抛在浏览器页面上&#xff0c;这样很不友好&#xff0c;所以必须进行异常处理。 1.异常处理思路 系统的dao、service、controller出现都通过throws Exception向上抛出&#xff0c;最后由springmvc前端控制器交由异常处理器进行异…

MySQL的页与行格式

什么是MySQL的页&#xff1f; 页是指存储引擎使用的最小的数据存储单位。 当 MySQL 执行读取或写入操作时&#xff0c;是以页为基本单位来进行操作的。即使读写一条数据&#xff0c;MySQL 也会按页操作。 MySQL 的存储引擎会将数据分成多个页&#xff0c;并根据需要将这些页加…