解码按键检测、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)
    }
}
相关推荐
无敌贵点大王4 小时前
RTThread学习记录11——RT-Thread 设备模型吃透:UART/ADC/PWM/PIN 到底有什么区别?
c语言·stm32·学习·链表
恶魔泡泡糖8 小时前
stm32F103C8T6标准库串口接收之中断方式2
stm32·单片机·嵌入式硬件
充哥单片机设计8 小时前
【STM32开源项目】智能小区充电桩
stm32·单片机·嵌入式硬件·毕业设计·小区充电桩·智能小区充电桩
充哥单片机设计10 小时前
【STM32开源项目】智能语音台灯
stm32·单片机·嵌入式硬件·毕业设计·单片机设计·智能语音台灯
Brilliantwxx12 小时前
【STM32】 SPI阻塞式通信与HAL源码 (源码分析)
stm32·单片机·嵌入式硬件
WWJA王文举13 小时前
Bootloader、App 和升级分区之间是什么关系?
stm32·freertos·嵌入式开发·在线升级
LCG元14 小时前
STM32F103 NTC 热敏电阻测温实战:分压网络设计、Steinhart-Hart 标定与 ADC 采样优化实测
stm32·单片机·嵌入式硬件
怀民民民15 小时前
嵌入式软件学习路线与项目实践记录:STM32、FreeRTOS、Linux 与 OpenMV
stm32·freertos·嵌入式软件·嵌入式linux·openmv
辰域电子16 小时前
STM32项目开源:实验室消防预警控制系统(代码 + 原理图 + 仿真)
stm32·单片机·嵌入式硬件·开源·proteus
ysu_03141 天前
TinyML 模型部署:从 Edge Impulse 到 STM32F103
驱动开发·stm32·嵌入式硬件·mcu·硬件架构·硬件工程·dsp开发