按键检测的原理与应用
基本概念
按键是单片机系统中核心的人机交互元件,通过机械接触或电容感应将用户操作转化为电信号,为单片机提供输入控制。常见类型包括:
- 机械按键:实体按压式,结构简单,成本低,适用于多数场景;
- 薄膜按键:轻触式,体积小、寿命长,常用于家电面板;
- 电容式按键:非接触感应,无机械磨损,适用于防水、防尘场景。
轻触按键因体积小、质量轻、操作便捷,广泛应用于电视机、键盘、显示器、照明设备等家用电器中。
工作原理
轻触按键的核心是内部金属弹片,按下时弹片变形使触点闭合(电路导通),松开时弹片复位使触点断开(电路截止)。由于机械触点的弹性特性,按键闭合和断开瞬间会产生 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输入初始化函数
c
/**
* @brief 按键GPIO端口初始化(配置PA0为输入模式)
* @param None 无参数
* @retval None 无返回值
* @note 初始化步骤:1.开启GPIOA时钟;2.配置引脚为输入模式;3.选择上拉/下拉模式;4.初始化GPIO端口
*/
void KEY_Config(void)
{
// 定义GPIO初始化结构体(存储GPIO配置参数)
GPIO_InitTypeDef GPIO_InitStructure;
// 开启GPIOA端口时钟(AHB1总线外设,必须先使能时钟才能操作GPIO)
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
// 配置要初始化的引脚:PA0
GPIO_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位保留。

c
// 读取PA0引脚电平(判断是否为高电平)
if (GPIOA->IDR & (1 << 0))
{
// PA0为高电平,按键未按下
}
else
{
// PA0为低电平,按键可能按下(需消抖确认)
}
消抖处理
硬件消抖
通过RS触发器或RC积分电路实现,原理是利用电容的充放电特性过滤抖动信号。优点是无需占用CPU资源,缺点是增加硬件复杂度和成本,适用于对实时性要求极高的场景。
软件消抖
核心思路:检测到IO口电平变化后,延时一段时间(5ms-10ms),待机械抖动稳定后再读取电平,确认按键状态。
① 软件延时消抖(循环实现)
c
/**
* @brief 10ms延时函数(用于按键消抖)
* @param None 无参数
* @retval None 无返回值
* @note 延时时间通过循环递减实现,需根据CPU主频调整循环次数;
* 10ms是兼顾消抖效果和程序实时性的最优值,过短无法消抖,过长影响响应速度。
*/
void delay_10ms(void)
{
// 循环次数根据168MHz主频校准(135000次≈10ms)
unsigned int cnt = 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_t KEY_Scan(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
// 第一次检测到按键按下(低电平)
if (!GPIO_ReadInputDataBit(GPIOx, GPIO_Pin))
{
delay_10ms(); // 消抖延时
// 第二次检测,确认按键按下
if (!GPIO_ReadInputDataBit(GPIOx, GPIO_Pin))
{
// 等待按键松开
while (!GPIO_ReadInputDataBit(GPIOx, GPIO_Pin));
return 1; // 按键按下
}
}
return 0; // 按键未按下
}
② 定时器消抖
利用单片机定时器产生固定周期中断(如1ms中断),在中断服务函数中计数按键电平稳定的时间,达到阈值(如5ms)则确认按键状态。优点是不阻塞主程序,实时性强;缺点是占用定时器资源,代码复杂度略高。
状态机按键检测
状态机通过定义按键的不同状态(空闲、按下、长按、双击等),根据电平变化和时间判断状态转换,可实现单击、双击、长按、短按等复杂按键操作,是实际项目中常用的高级方案。
按键状态机流程图

实现
c
#include "stm32f4xx.h"
#include "led.h"
/************************** 宏定义 **************************/
// 按键硬件相关宏定义 - PA0引脚作为按键输入
#define KEY_GPIO_PORT GPIOA // 按键GPIO端口
#define KEY_GPIO_PIN GPIO_Pin_0 // 按键GPIO引脚
#define KEY_EXTI_LINE EXTI_Line0 // 按键对应外部中断线
#define KEY_EXTI_PORT EXTI_PortSourceGPIOA// 外部中断端口源
#define KEY_EXTI_PIN GPIO_PinSource0 // 外部中断引脚源
// 按键时间阈值宏定义(单位:ms)
#define LONG_PRESS_THRESHOLD 1000 // 长按判定阈值:按下持续1000ms判定为长按
#define DOUBLE_CLICK_THRESHOLD 500 // 双击间隔阈值:两次按下间隔≤500ms判定为双击
#define KEY_DEBOUNCE_TIME 5 // 按键消抖时间:连续5ms电平稳定才判定有效
/************************** 枚举定义 **************************/
// 按键状态机枚举 - 定义按键的所有工作状态
typedef enum
{
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;
// 按键事件枚举 - 定义对外暴露的按键事件类型
typedef enum
{
KEY_EVENT_NONE = 0, // 无事件:默认值
KEY_EVENT_CLICK = 1, // 单击事件:单次短按后松开,且未触发双击
KEY_EVENT_LONG_PRESS = 2, // 长按事件:第一次按下持续时间≥1000ms
KEY_EVENT_DOUBLE_CLICK = 3 // 双击事件:两次按下间隔≤500ms
} Key_EventTypeDef;
/************************** 静态变量 **************************/
static Key_StateTypeDef key_state = KEY_STATE_IDLE; // 按键状态机当前状态(初始为空闲)
static uint16_t key_time = 0; // 时间计数器(单位:ms,由TIM6 1ms中断驱动)
static uint8_t click_cnt = 0; // 单击次数计数器(1=第一次按下,2=第二次按下)
static Key_EventTypeDef key_event = KEY_EVENT_NONE; // 按键事件缓存(供外部读取)
static uint8_t key_level = 1; // 按键当前电平(1=高电平/松开,0=低电平/按下)
static uint8_t key_debounce_cnt = 0; // 消抖计数器(累计电平不一致的时间)
/************************** 函数声明 **************************/
void KEY_GPIO_Config(void); // 按键GPIO初始化(上拉输入)
void KEY_EXTI_Config(void); // 按键外部中断初始化(双边沿触发)
void TIM6_Config(void); // 定时器6初始化(1ms中断,驱动状态机)
void KEY_StateMachine_Process(void);// 按键状态机核心处理函数(1ms执行一次)
Key_EventTypeDef KEY_GetEvent(void); // 获取按键事件(读取后清空缓存)
uint8_t KEY_Get_Level(void); // 获取消抖后的按键电平
/************************** 函数实现 **************************/
/**
* @brief 按键GPIO配置函数
* @note PA0引脚配置为上拉输入模式:
* - 按键未按下时:上拉电阻使引脚为高电平(1)
* - 按键按下时:引脚接地,为低电平(0)
*/
void KEY_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),双边沿触发:
* - 按下(下降沿)和松开(上升沿)都会触发中断
* - 仅清中断标志,不处理逻辑(逻辑在定时器中)
*/
void KEY_EXTI_Config(void)
{
EXTI_InitTypeDef EXTI_InitStruct; // 外部中断初始化结构体
NVIC_InitTypeDef NVIC_InitStruct; // 中断优先级配置结构体
// 1. 使能SYSCFG时钟(APB2总线,用于EXTI引脚映射)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
// 2. 将EXTI0映射到GPIOA的Pin0
SYSCFG_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中断一次)
* - 用于驱动按键状态机和消抖计时
*/
void TIM6_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; // 子优先级0
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; // 使能中断
NVIC_Init(&NVIC_InitStruct);
// 5. 启动TIM6计数器
TIM_Cmd(TIM6, ENABLE);
}
/**
* @brief 获取消抖后的按键电平
* @note 消抖逻辑:连续5ms电平一致才判定为稳定电平,避免机械按键抖动
* @retval 1:按键松开(高电平);0:按键按下(低电平)
*/
uint8_t KEY_Get_Level(void)
{
static uint8_t stable_level = 1; // 保存稳定的按键电平(初始为松开)
// 读取当前GPIO引脚的原始电平
uint8_t current_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; // 重置计数器
}
}
return stable_level; // 返回消抖后的稳定电平
}
/**
* @brief 按键状态机核心处理函数
* @note 由TIM6 1ms中断调用,核心改进点:
* 1. 仅第一次按下时检测长按
* 2. 第二次按下(双击)完全禁止长按判定
* 3. 单击事件仅在双击等待超时后触发
*/
void KEY_StateMachine_Process(void)
{
// 第一步:获取5ms消抖后的按键电平(确保电平稳定)
key_level = KEY_Get_Level();
// 第二步:根据当前状态执行对应逻辑(状态机核心)
switch (key_state)
{
case KEY_STATE_IDLE: // 空闲态:等待第一次按键按下
if (key_level == 0) // 检测到按键按下(低电平)
{
key_state = KEY_STATE_FIRST_PRESSED; // 进入第一次按下态
key_time = 0; // 重置长按计时
click_cnt = 1; // 标记第一次按下
}
break;
case KEY_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;
case KEY_STATE_LONG_PRESS: // 长按态:等待按键松开
if (key_level == 1) // 检测到按键松开(高电平)
{
key_state = KEY_STATE_IDLE; // 回到空闲态
click_cnt = 0; // 清空计数
}
break;
case KEY_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;
case KEY_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_EventTypeDef KEY_GetEvent(void)
{
Key_EventTypeDef event = key_event; // 读取当前事件
key_event = KEY_EVENT_NONE; // 清空事件缓存
return event; // 返回事件
}
/************************** 中断服务函数 **************************/
/**
* @brief EXTI0中断服务函数(PA0按键中断)
* @note 仅清空中断标志,不处理按键逻辑(逻辑在TIM6中断中)
*/
void EXTI0_IRQHandler(void)
{
// 检查EXTI0中断标志是否置位
if (EXTI_GetITStatus(KEY_EXTI_LINE) != RESET)
{
EXTI_ClearITPendingBit(KEY_EXTI_LINE); // 清空中断挂起位(必须)
// 注意:此处不处理按键逻辑,避免中断嵌套和抖动问题
}
}
/**
* @brief TIM6/DAC中断服务函数
* @note 1ms中断一次,驱动按键状态机和消抖逻辑
*/
void TIM6_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(用于测试)
*/
void KEY_Init(void)
{
KEY_GPIO_Config(); // 初始化按键GPIO
KEY_EXTI_Config(); // 初始化按键外部中断
TIM6_Config(); // 初始化1ms定时器
LED_Config(); // 初始化LED(用于按键事件反馈)
}
/************************** 主函数示例 **************************/
int main(void)
{
// 配置NVIC优先级分组为4(仅抢占优先级,0-15)
NVIC_SetPriorityGrouping(NVIC_PriorityGroup_4);
// 初始化按键模块
KEY_Init();
// 主循环
while (1)
{
// 获取按键事件(非阻塞)
Key_EventTypeDef event = KEY_GetEvent();
// 根据事件执行对应操作
switch (event)
{
case KEY_EVENT_CLICK:
GPIO_ResetBits(GPIOF, GPIO_Pin_9); // 单击:点亮LED0
break;
case KEY_EVENT_LONG_PRESS:
GPIO_SetBits(GPIOF, GPIO_Pin_9); // 长按:熄灭LED0
break;
case KEY_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)
c
/**
* @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阻塞,适用于短时间延时场景。
*/
void delay_us(uint32_t nus)
{
uint32_t reload_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;
}
毫秒延时函数(基于微秒延时)
c
/**
* @brief 毫秒级延时函数(Systick时钟源=21MHz)
* @param nms 待延时时间(单位:ms),无上限(通过循环分段实现)
* @retval None 无返回值
* @note 基于delay_us函数实现,1ms=1000μs;
* 延时存在微小误差(因循环开销),适用于对精度要求不高的场景。
*/
void delay_ms(uint32_t nms)
{
while (nms--)
{
delay_us(1000); // 每次延时1000μs(1ms)
}
}