解码按键检测、Systick 定时器

按键检测的原理与应用

基本概念

按键是单片机系统中核心的人机交互元件,通过机械接触或电容感应将用户操作转化为电信号,为单片机提供输入控制。常见类型包括:

  • 机械按键:实体按压式,结构简单,成本低,适用于多数场景;
  • 薄膜按键:轻触式,体积小、寿命长,常用于家电面板;
  • 电容式按键:非接触感应,无机械磨损,适用于防水、防尘场景。
    轻触按键因体积小、质量轻、操作便捷,广泛应用于电视机、键盘、显示器、照明设备等家用电器中。

工作原理

轻触按键的核心是内部金属弹片,按下时弹片变形使触点闭合(电路导通),松开时弹片复位使触点断开(电路截止)。由于机械触点的弹性特性,按键闭合和断开瞬间会产生 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)
    }
}
相关推荐
ting_zh18 小时前
定时器输出PWM信号同步控制传感器开关与 ADC 采样
stm32·tim·pwm·adc
锻炼²1 天前
USB 设备/配置/接口/端点/描述符 和 HID类请求详解
stm32·usb·hid·全速传输·sof包·中断传输
小何code1 天前
STM32入门教程,第10课(下),Keil调试模式
stm32·单片机·嵌入式硬件
Y1rong1 天前
STM32之中断
stm32·单片机·嵌入式硬件
先知后行。1 天前
STM32F103的启动过程
stm32·单片机·嵌入式硬件
idcardwang1 天前
xl9555-IO拓展芯片
stm32·单片机·嵌入式硬件
Y1rong1 天前
STM32之EXTI
stm32·单片机·嵌入式硬件
兆龙电子单片机设计1 天前
【STM32项目开源】STM32单片机智能语音家居控制系统
stm32·单片机·嵌入式硬件·物联网·开源·自动化
意法半导体STM321 天前
【官方原创】SAU对NSC分区的影响 LAT1578
stm32·单片机·嵌入式硬件·mcu·信息安全·trustzone·stm32开发