按键检测的原理与应用
基本概念
按键是单片机系统中核心的人机交互元件,通过机械接触或电容感应将用户操作转化为电信号,为单片机提供输入控制。常见类型包括:
- 机械按键:实体按压式,结构简单,成本低,适用于多数场景;
- 薄膜按键:轻触式,体积小、寿命长,常用于家电面板;
- 电容式按键:非接触感应,无机械磨损,适用于防水、防尘场景。
轻触按键因体积小、质量轻、操作便捷,广泛应用于电视机、键盘、显示器、照明设备等家用电器中。
工作原理
轻触按键的核心是内部金属弹片,按下时弹片变形使触点闭合(电路导通),松开时弹片复位使触点断开(电路截止)。由于机械触点的弹性特性,按键闭合和断开瞬间会产生5ms-20ms的抖动,导致电信号不稳定,需通过硬件或软件方式消抖,才能确保单片机准确检测按键状态。
单片机检测按键的核心逻辑为“按下→检测→响应”闭环,可通过两种方式实现:
- 扫描方式:单片机周期性读取IO口电平,判断按键状态;
- 中断方式:按键动作触发IO口中断,单片机在中断服务函数中处理按键事件。
电路分析
基础按键电路
典型按键电路采用上拉电阻设计:IO口通过10KΩ上拉电阻连接VDD(3.3V),按键另一端接地。
- 按键未按下时:IO口被上拉电阻拉为高电平;
- 按键按下时:IO口通过按键直接接地,检测到低电平。
2个IO口实现3个按键检测
电路设计
电路原理
- 无按键按下:IO1高电平、IO2高电平
- 上面按键按下:IO1低电平、IO2高电平
- 中间按键按下:IO1低电平、IO2低电平
- 下面按键按下:IO1高电平、IO2低电平
输入模式
单片机IO口配置为输入模式时,核心特性如下:
- 输出缓冲器关闭,施密特触发器打开;
- 可通过GPIOx_PUPDR寄存器配置上拉电阻(IO口默认高)、下拉电阻(IO口默认低)或浮空(IO口电平由外部电路决定);
- 输入数据寄存器(GPIOx_IDR)每1个AHB1时钟周期采样一次IO口电平,通过读取该寄存器获取IO口状态。
施密特触发器的作用:将不稳定的模拟电平转换为稳定的数字电平(高/低),其对正向递增和负向递减的输入信号有不同阈值电压,可增强抗干扰能力。
程序设计
GPIO输入初始化函数
/** * @brief 按键GPIO端口初始化(配置PA0为输入模式) * @param None 无参数 * @retval None 无返回值 * @note 初始化步骤:1.开启GPIOA时钟;2.配置引脚为输入模式;3.选择上拉/下拉模式;4.初始化GPIO端口 */voidKEY_Config(void){// 定义GPIO初始化结构体(存储GPIO配置参数)GPIO_InitTypeDef GPIO_InitStructure;// 开启GPIOA端口时钟(AHB1总线外设,必须先使能时钟才能操作GPIO)RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);// 配置要初始化的引脚:PA0GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;// 配置为输入模式GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN;// 配置为无上下拉电阻(电平由外部电路决定)GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_NOPULL;// 初始化GPIOA端口(将配置参数写入寄存器)GPIO_Init(GPIOA,&GPIO_InitStructure);}GPIO输入数据读取
GPIOx_IDR寄存器为32位只读寄存器,低16位对应16个IO口的输入状态(高电平为1,低电平为0),高16位保留。
// 读取PA0引脚电平(判断是否为高电平)if(GPIOA->IDR&(1<<0)){// PA0为高电平,按键未按下}else{// PA0为低电平,按键可能按下(需消抖确认)}消抖处理
硬件消抖
通过RS触发器或RC积分电路实现,原理是利用电容的充放电特性过滤抖动信号。优点是无需占用CPU资源,缺点是增加硬件复杂度和成本,适用于对实时性要求极高的场景。
软件消抖
核心思路:检测到IO口电平变化后,延时一段时间(5ms-10ms),待机械抖动稳定后再读取电平,确认按键状态。
① 软件延时消抖(循环实现)
/** * @brief 10ms延时函数(用于按键消抖) * @param None 无参数 * @retval None 无返回值 * @note 延时时间通过循环递减实现,需根据CPU主频调整循环次数; * 10ms是兼顾消抖效果和程序实时性的最优值,过短无法消抖,过长影响响应速度。 */voiddelay_10ms(void){// 循环次数根据168MHz主频校准(135000次≈10ms)unsignedintcnt=135000;while(cnt--){// 空循环延时}}/** * @brief 按键状态检测函数(带软件消抖) * @param GPIOx GPIO端口指针(如GPIOA、GPIOB) * @param GPIO_Pin 按键对应的GPIO引脚(如GPIO_Pin_0) * @retval uint8_t 1-按键按下,0-按键未按下 * @note 检测流程:1.检测到低电平→延时消抖;2.再次检测低电平→确认按下; * 避免因抖动导致的误触发。 */uint8_tKEY_Scan(GPIO_TypeDef*GPIOx,uint16_tGPIO_Pin){// 第一次检测到按键按下(低电平)if(!GPIO_ReadInputDataBit(GPIOx,GPIO_Pin)){delay_10ms();// 消抖延时// 第二次检测,确认按键按下if(!GPIO_ReadInputDataBit(GPIOx,GPIO_Pin)){// 等待按键松开while(!GPIO_ReadInputDataBit(GPIOx,GPIO_Pin));return1;// 按键按下}}return0;// 按键未按下}② 定时器消抖
利用单片机定时器产生固定周期中断(如1ms中断),在中断服务函数中计数按键电平稳定的时间,达到阈值(如5ms)则确认按键状态。优点是不阻塞主程序,实时性强;缺点是占用定时器资源,代码复杂度略高。
状态机按键检测
状态机通过定义按键的不同状态(空闲、按下、长按、双击等),根据电平变化和时间判断状态转换,可实现单击、双击、长按、短按等复杂按键操作,是实际项目中常用的高级方案。
按键状态机流程图
实现
#include"stm32f4xx.h"#include"led.h"/************************** 宏定义 **************************/// 按键硬件相关宏定义 - PA0引脚作为按键输入#defineKEY_GPIO_PORTGPIOA// 按键GPIO端口#defineKEY_GPIO_PINGPIO_Pin_0// 按键GPIO引脚#defineKEY_EXTI_LINEEXTI_Line0// 按键对应外部中断线#defineKEY_EXTI_PORTEXTI_PortSourceGPIOA// 外部中断端口源#defineKEY_EXTI_PINGPIO_PinSource0// 外部中断引脚源// 按键时间阈值宏定义(单位:ms)#defineLONG_PRESS_THRESHOLD1000// 长按判定阈值:按下持续1000ms判定为长按#defineDOUBLE_CLICK_THRESHOLD500// 双击间隔阈值:两次按下间隔≤500ms判定为双击#defineKEY_DEBOUNCE_TIME5// 按键消抖时间:连续5ms电平稳定才判定有效/************************** 枚举定义 **************************/// 按键状态机枚举 - 定义按键的所有工作状态typedefenum{KEY_STATE_IDLE=0,// 空闲态:无任何按键操作,初始状态KEY_STATE_FIRST_PRESSED=1,// 第一次按下态:仅此状态检测长按(核心:避免双击触发长按)KEY_STATE_LONG_PRESS=2,// 长按态:已判定为长按,等待按键松开KEY_STATE_DOUBLE_WAIT=3,// 双击等待态:第一次按键松开后,等待第二次按下KEY_STATE_SECOND_PRESSED=4// 第二次按下态:双击的第二次按下(完全禁止长按判定)}Key_StateTypeDef;// 按键事件枚举 - 定义对外暴露的按键事件类型typedefenum{KEY_EVENT_NONE=0,// 无事件:默认值KEY_EVENT_CLICK=1,// 单击事件:单次短按后松开,且未触发双击KEY_EVENT_LONG_PRESS=2,// 长按事件:第一次按下持续时间≥1000msKEY_EVENT_DOUBLE_CLICK=3// 双击事件:两次按下间隔≤500ms}Key_EventTypeDef;/************************** 静态变量 **************************/staticKey_StateTypeDef key_state=KEY_STATE_IDLE;// 按键状态机当前状态(初始为空闲)staticuint16_tkey_time=0;// 时间计数器(单位:ms,由TIM6 1ms中断驱动)staticuint8_tclick_cnt=0;// 单击次数计数器(1=第一次按下,2=第二次按下)staticKey_EventTypeDef key_event=KEY_EVENT_NONE;// 按键事件缓存(供外部读取)staticuint8_tkey_level=1;// 按键当前电平(1=高电平/松开,0=低电平/按下)staticuint8_tkey_debounce_cnt=0;// 消抖计数器(累计电平不一致的时间)/************************** 函数声明 **************************/voidKEY_GPIO_Config(void);// 按键GPIO初始化(上拉输入)voidKEY_EXTI_Config(void);// 按键外部中断初始化(双边沿触发)voidTIM6_Config(void);// 定时器6初始化(1ms中断,驱动状态机)voidKEY_StateMachine_Process(void);// 按键状态机核心处理函数(1ms执行一次)Key_EventTypeDefKEY_GetEvent(void);// 获取按键事件(读取后清空缓存)uint8_tKEY_Get_Level(void);// 获取消抖后的按键电平/************************** 函数实现 **************************//** * @brief 按键GPIO配置函数 * @note PA0引脚配置为上拉输入模式: * - 按键未按下时:上拉电阻使引脚为高电平(1) * - 按键按下时:引脚接地,为低电平(0) */voidKEY_GPIO_Config(void){GPIO_InitTypeDef GPIO_InitStruct;// GPIO初始化结构体// 1. 使能GPIOA时钟(AHB1总线)RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);// 2. 配置PA0为上拉输入GPIO_InitStruct.GPIO_Pin=KEY_GPIO_PIN;// 选择PA0引脚GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IN;// 输入模式GPIO_InitStruct.GPIO_PuPd=GPIO_PuPd_UP;// 上拉电阻使能GPIO_Init(KEY_GPIO_PORT,&GPIO_InitStruct);// 应用配置// 3. 初始化按键初始电平(上电时读取PA0电平)key_level=GPIO_ReadInputDataBit(KEY_GPIO_PORT,KEY_GPIO_PIN);}/** * @brief 按键外部中断配置函数 * @note 配置EXTI0中断(映射PA0),双边沿触发: * - 按下(下降沿)和松开(上升沿)都会触发中断 * - 仅清中断标志,不处理逻辑(逻辑在定时器中) */voidKEY_EXTI_Config(void){EXTI_InitTypeDef EXTI_InitStruct;// 外部中断初始化结构体NVIC_InitTypeDef NVIC_InitStruct;// 中断优先级配置结构体// 1. 使能SYSCFG时钟(APB2总线,用于EXTI引脚映射)RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG,ENABLE);// 2. 将EXTI0映射到GPIOA的Pin0SYSCFG_EXTILineConfig(KEY_EXTI_PORT,KEY_EXTI_PIN);// 3. 配置EXTI0为中断模式,双边沿触发EXTI_InitStruct.EXTI_Line=KEY_EXTI_LINE;// 选择EXTI0中断线EXTI_InitStruct.EXTI_Mode=EXTI_Mode_Interrupt;// 中断模式(非事件模式)EXTI_InitStruct.EXTI_Trigger=EXTI_Trigger_Rising_Falling;// 双边沿触发(上升+下降)EXTI_InitStruct.EXTI_LineCmd=ENABLE;// 使能EXTI0中断线EXTI_Init(&EXTI_InitStruct);// 4. 配置NVIC中断优先级(抢占优先级14,子优先级0)NVIC_InitStruct.NVIC_IRQChannel=EXTI0_IRQn;// 选择EXTI0中断通道NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=14;// 抢占优先级(数值越小优先级越高)NVIC_InitStruct.NVIC_IRQChannelSubPriority=0;// 子优先级NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;// 使能该中断通道NVIC_Init(&NVIC_InitStruct);}/** * @brief 定时器6配置函数 * @note 配置TIM6为1ms中断(系统时钟84MHz): * - 预分频器:84-1 → 84MHz/84=1MHz * - 自动重装值:1000-1 → 1MHz/1000=1kHz(1ms中断一次) * - 用于驱动按键状态机和消抖计时 */voidTIM6_Config(void){TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;// 定时器时基结构体NVIC_InitTypeDef NVIC_InitStruct;// 中断优先级结构体// 1. 使能TIM6时钟(APB1总线)RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE);// 2. 配置TIM6时基参数TIM_TimeBaseStruct.TIM_Prescaler=84-1;// 预分频器值TIM_TimeBaseStruct.TIM_Period=1000-1;// 自动重装值TIM_TimeBaseStruct.TIM_CounterMode=TIM_CounterMode_Up;// 向上计数模式TIM_TimeBaseInit(TIM6,&TIM_TimeBaseStruct);// 应用配置// 3. 使能TIM6更新中断(溢出中断)TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE);// 4. 配置TIM6中断优先级(抢占优先级15,最低优先级)NVIC_InitStruct.NVIC_IRQChannel=TIM6_DAC_IRQn;// TIM6中断通道NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=15;// 抢占优先级15(低于EXTI0)NVIC_InitStruct.NVIC_IRQChannelSubPriority=0;// 子优先级0NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;// 使能中断NVIC_Init(&NVIC_InitStruct);// 5. 启动TIM6计数器TIM_Cmd(TIM6,ENABLE);}/** * @brief 获取消抖后的按键电平 * @note 消抖逻辑:连续5ms电平一致才判定为稳定电平,避免机械按键抖动 * @retval 1:按键松开(高电平);0:按键按下(低电平) */uint8_tKEY_Get_Level(void){staticuint8_tstable_level=1;// 保存稳定的按键电平(初始为松开)// 读取当前GPIO引脚的原始电平uint8_tcurrent_level=GPIO_ReadInputDataBit(KEY_GPIO_PORT,KEY_GPIO_PIN);// 消抖核心逻辑:if(current_level==stable_level){// 当前电平与稳定电平一致 → 重置消抖计数器key_debounce_cnt=0;}else{// 当前电平与稳定电平不一致 → 累加消抖计数器key_debounce_cnt++;// 消抖计数器达到阈值 → 更新稳定电平(判定为有效电平变化)if(key_debounce_cnt>=KEY_DEBOUNCE_TIME){stable_level=current_level;key_debounce_cnt=0;// 重置计数器}}returnstable_level;// 返回消抖后的稳定电平}/** * @brief 按键状态机核心处理函数 * @note 由TIM6 1ms中断调用,核心改进点: * 1. 仅第一次按下时检测长按 * 2. 第二次按下(双击)完全禁止长按判定 * 3. 单击事件仅在双击等待超时后触发 */voidKEY_StateMachine_Process(void){// 第一步:获取5ms消抖后的按键电平(确保电平稳定)key_level=KEY_Get_Level();// 第二步:根据当前状态执行对应逻辑(状态机核心)switch(key_state){caseKEY_STATE_IDLE:// 空闲态:等待第一次按键按下if(key_level==0)// 检测到按键按下(低电平){key_state=KEY_STATE_FIRST_PRESSED;// 进入第一次按下态key_time=0;// 重置长按计时click_cnt=1;// 标记第一次按下}break;caseKEY_STATE_FIRST_PRESSED:// 第一次按下态:仅此处检测长按if(key_level==0)// 按键仍保持按下状态{key_time++;// 长按计时累加(1ms)// 长按计时达到阈值 → 判定为长按if(key_time>=LONG_PRESS_THRESHOLD){key_state=KEY_STATE_LONG_PRESS;// 进入长按态key_event=KEY_EVENT_LONG_PRESS;// 标记长按事件click_cnt=0;// 长按后清空计数(避免触发双击)}}else// 按键松开(未达到长按阈值){key_state=KEY_STATE_DOUBLE_WAIT;// 进入双击等待态key_time=0;// 重置双击等待计时// 注意:此处不触发单击,仅启动双击等待定时器}break;caseKEY_STATE_LONG_PRESS:// 长按态:等待按键松开if(key_level==1)// 检测到按键松开(高电平){key_state=KEY_STATE_IDLE;// 回到空闲态click_cnt=0;// 清空计数}break;caseKEY_STATE_DOUBLE_WAIT:// 双击等待态:等待第二次按下if(key_level==0)// 500ms内检测到第二次按下{key_state=KEY_STATE_SECOND_PRESSED;// 进入第二次按下态key_time=0;// 重置计时(此处无实际意义)click_cnt++;// 计数变为2(标记双击)}else// 未检测到第二次按下,判断是否超时{key_time++;// 双击等待计时累加// 双击等待超时,且仅一次按下 → 触发单击事件if(key_time>=DOUBLE_CLICK_THRESHOLD&&click_cnt==1){key_state=KEY_STATE_IDLE;// 回到空闲态key_event=KEY_EVENT_CLICK;// 标记单击事件click_cnt=0;// 清空计数}}break;caseKEY_STATE_SECOND_PRESSED:// 第二次按下态:禁止长按判定if(key_level==1)// 检测到第二次按键松开{key_state=KEY_STATE_IDLE;// 回到空闲态// 计数为2 → 判定为双击事件if(click_cnt==2){key_event=KEY_EVENT_DOUBLE_CLICK;// 标记双击事件click_cnt=0;// 清空计数}}// 核心:此处无任何长按计时逻辑,无论按下多久都不会触发长按break;default:// 异常状态:强制重置为初始状态(容错处理)key_state=KEY_STATE_IDLE;key_time=0;click_cnt=0;break;}}/** * @brief 获取按键事件(非阻塞) * @note 读取后清空事件缓存,避免重复处理同一事件 * @retval 按键事件类型(NONE/CLICK/LONG_PRESS/DOUBLE_CLICK) */Key_EventTypeDefKEY_GetEvent(void){Key_EventTypeDef event=key_event;// 读取当前事件key_event=KEY_EVENT_NONE;// 清空事件缓存returnevent;// 返回事件}/************************** 中断服务函数 **************************//** * @brief EXTI0中断服务函数(PA0按键中断) * @note 仅清空中断标志,不处理按键逻辑(逻辑在TIM6中断中) */voidEXTI0_IRQHandler(void){// 检查EXTI0中断标志是否置位if(EXTI_GetITStatus(KEY_EXTI_LINE)!=RESET){EXTI_ClearITPendingBit(KEY_EXTI_LINE);// 清空中断挂起位(必须)// 注意:此处不处理按键逻辑,避免中断嵌套和抖动问题}}/** * @brief TIM6/DAC中断服务函数 * @note 1ms中断一次,驱动按键状态机和消抖逻辑 */voidTIM6_DAC_IRQHandler(void){// 检查TIM6更新中断标志是否置位if(TIM_GetITStatus(TIM6,TIM_IT_Update)!=RESET){KEY_StateMachine_Process();// 执行按键状态机处理TIM_ClearITPendingBit(TIM6,TIM_IT_Update);// 清空中断挂起位(必须)}}/************************** 初始化函数 **************************//** * @brief 按键模块初始化入口函数 * @note 依次初始化GPIO、EXTI、TIM6,以及LED(用于测试) */voidKEY_Init(void){KEY_GPIO_Config();// 初始化按键GPIOKEY_EXTI_Config();// 初始化按键外部中断TIM6_Config();// 初始化1ms定时器LED_Config();// 初始化LED(用于按键事件反馈)}/************************** 主函数示例 **************************/intmain(void){// 配置NVIC优先级分组为4(仅抢占优先级,0-15)NVIC_SetPriorityGrouping(NVIC_PriorityGroup_4);// 初始化按键模块KEY_Init();// 主循环while(1){// 获取按键事件(非阻塞)Key_EventTypeDef event=KEY_GetEvent();// 根据事件执行对应操作switch(event){caseKEY_EVENT_CLICK:GPIO_ResetBits(GPIOF,GPIO_Pin_9);// 单击:点亮LED0break;caseKEY_EVENT_LONG_PRESS:GPIO_SetBits(GPIOF,GPIO_Pin_9);// 长按:熄灭LED0break;caseKEY_EVENT_DOUBLE_CLICK:GPIO_ToggleBits(GPIOF,GPIO_Pin_10);// 双击:翻转LED1状态break;default:break;}}}Systick嘀嗒定时器的原理与应用
基本概念
Systick定时器是ARM Cortex-M3/M4内核内置的24位递减定时器,属于内核级外设,嵌入在NVIC(嵌套向量中断控制器)中,无需依赖芯片厂商的额外设计,因此在所有Cortex-M3/M4内核单片机(如STM32F4系列)中均兼容,移植性极强。
核心功能:提供精准的时间基准,用于实现延时、任务调度等功能。
- 定时原理:定时时间 = 计数次数 × 计数周期;
- 计数周期:1 / 时钟源频率;
- 最大定时时间:(2²⁴ - 1) × 计数周期(因寄存器为24位,最大计数值为2²⁴ - 1)。
基本应用
- 裸机开发:实现微秒(μs)、毫秒(ms)级延时,用于按键消抖、LED闪烁、传感器采样等场景;
- 操作系统:为RTOS(如FreeRTOS、UCOS)提供时钟节拍(Tick),用于任务调度(如任务切换、延时等待)。
时钟分析
Systick定时器的时钟源由内核提供,需先明确系统时钟(SYSCLK)的配置,而系统时钟由MCU的时钟源经过PLL倍频后生成。
MCU时钟源分类
| 时钟源类型 | 名称 | 频率范围 | 核心特点 | 应用场景 |
|---|---|---|---|---|
| 高速内部时钟 | HSI | 16MHz(典型值) | 无外部元件,成本低,启动快,精度低(±1%) | 应急场景、对精度要求不高的应用 |
| 高速外部时钟 | HSE | 4MHz-26MHz | 外部晶振,精度高(±10ppm),启动慢 | 对定时精度要求高的场景(如通信、测量) |
| 低速内部时钟 | LSI | 32kHz左右 | 低功耗,精度低 | 独立看门狗(IWDG)、停机/待机模式唤醒 |
| 低速外部时钟 | LSE | 32.768kHz | 外部晶振,精度高,低功耗 | 实时时钟(RTC)、日历功能 |
PLL倍频配置(以STM32F407为例)
系统时钟(SYSCLK)通过PLL倍频生成,核心参数包括PLL_M(分频系数)、PLL_N(倍频系数)、PLL_P(分频系数):
实际配置(HSE=xMHz):
- PLL_M=x:将HSE xMHz分频为1MHz;
- PLL_N=336:将1MHz倍频为336MHz;
- PLL_P=2:将336MHz分频为168MHz;
- 最终SYSCLK=(xMHz ÷ x)× 336 ÷ 2 = 168MHz。
假设HSE为8M(看产品原理图)
Systick时钟源选择
Systick有两个可选时钟源,通过SysTick控制及状态寄存器(SysTick->CTRL)的CLKSOURCE位配置:
假设HSE为8M(看产品原理图)
- 内核时钟(FCLK):与SYSCLK同源,频率=168MHz;
- 外部时钟(STCLK):AHB总线时钟的8分频,频率=168MHz ÷ 8 = 21MHz。
时钟配置步骤
- 修改
stm32f4xx.h头文件:将HSE_VALUE宏定义从25000000(25MHz)改为8000000(8MHz),匹配实际外部晶振频率; - 修改
system_stm32f4xx.c文件:将PLL_M宏定义从25改为8,确保PLL倍频参数正确; - 编译工程,使时钟配置生效。
接口设计(寄存器与函数)
Systick定时器的核心寄存器有3个,地址固定(内核外设):
微秒延时函数(时钟源=21MHz)
/** * @brief 微秒级延时函数(Systick时钟源=21MHz) * @param nus 待延时时间(单位:μs),范围:1μs ~ 798915μs(约800ms) * @retval None 无返回值 * @note 时钟源=21MHz时,计数1次=1/21μs,因此1μs需计数21次; * 最大定时时间=(2²⁴-1)/21 ≈ 798915μs,超过需分段延时; * 延时过程中CPU阻塞,适用于短时间延时场景。 */voiddelay_us(uint32_tnus){uint32_treload_val;// 1. 关闭Systick定时器SysTick->CTRL=0;// 2. 计算重装载值:nus × 21(1μs对应21个计数周期)reload_val=nus*21;// 3. 设置重装载寄存器(定时计数初始值)SysTick->LOAD=reload_val-1;// 计数从reload_val-1递减到0,共reload_val次// 4. 清零当前值寄存器SysTick->VAL=0;// 5. 使能Systick定时器,选择外部时钟源(STCLK=21MHz)SysTick->CTRL=SysTick_CTRL_CLKSOURCE_Msk;// 6. 等待计数完成(COUNTFLAG位为1表示计数到0)while(!(SysTick->CTRL&SysTick_CTRL_COUNTFLAG_Msk));// 7. 关闭Systick定时器SysTick->CTRL=0;}毫秒延时函数(基于微秒延时)
/** * @brief 毫秒级延时函数(Systick时钟源=21MHz) * @param nms 待延时时间(单位:ms),无上限(通过循环分段实现) * @retval None 无返回值 * @note 基于delay_us函数实现,1ms=1000μs; * 延时存在微小误差(因循环开销),适用于对精度要求不高的场景。 */voiddelay_ms(uint32_tnms){while(nms--){delay_us(1000);// 每次延时1000μs(1ms)}}