本文最后更新于 2025-06-10,学习久了要注意休息哟

第一章 RTC 实时时钟

1.1 RTC基础概述

1.1.1 RTC 实时时钟

1、RTC的基本概念

RTC(Real-Time Clock,实时时钟)是一种能够 独立计时 的模块,即使在主处理器关机或休眠时,也能持续记录时间

它通常以 秒、分钟、小时、日、月、年 等标准格式进行计时,可提供精准的当前时间信息。

RTC模块为各类电子系统提供了 准确、连续、不间断的计时服务,是实现低功耗系统定时、事件触发、时间管理的重要基础单元。

2、RTC的主要作用

  • 系统时钟来源:为操作系统或应用程序提供标准时间。
  • 定时唤醒功能:可以在特定时间点触发唤醒或中断操作(如定时开机)。
  • 低功耗计时:即使主MCU休眠,RTC依然低功耗运行,保证时间不中断。
  • 报警提醒功能:通过设置闹钟事件,实现定时任务提醒。
  • 时间戳记录:在特定事件发生时,记录准确的发生时间(如黑匣子功能)。

3、RTC常见应用场景

应用场景描述
智能手机提供锁屏时间显示,定时闹钟唤醒
智能手表实现长时间准确计时,支持多闹钟提醒
数据记录仪精确记录各类传感器数据的时间戳
安防监控设备录像时打上精准的时间戳
工业控制系统定时启动、定时采集、历史数据追踪
医疗设备定时药物提醒、生命体征监测记录

1.1.2 RTC工作框图

1、整体认识

STM32F407的RTC模块,是一个独立运行的实时时钟单元,支持从多种低速时钟源(如LSE、LSI、HSE/128)获取时基信号,经过内部分频处理后,最终实现1Hz的秒脉冲信号,用来进行时间计数。

RTC内部主要包含预分频器计数器时间/日期寄存器闹钟子模块等。

2、RTC工作流程概览

整个RTC模块可以分为以下几个主要部分(对应框图):

1. 时钟源选择
  • 支持LSE(32.768kHz)、LSI(约32kHz)或HSE除128频率。
  • 通过选择不同来源,为RTC模块提供输入时钟。
2. 预分频器(RTC_PRER)
  • 异步预分频器(PREDIV_A)
    • 先把高频时钟(比如32.768kHz)降低到较低频率(比如256Hz)。
  • 同步预分频器(PREDIV_S)
    • 继续细分,最终得到1Hz(即1秒一次的节拍)。

通俗理解
"两级分频器,就像两个齿轮,配合把快频率慢下来,变成1秒1次。"

3. 时间计数器和寄存器
  • 核心是计数器(CNT),每秒累加一次。
  • 时间信息(小时、分钟、秒钟)存储在时间寄存器(TR)
  • 日期信息(年、月、日、星期)存储在日期寄存器(DR)
  • 有影子寄存器保证读取操作时的一致性。
4. 闹钟模块
  • 支持设置两个独立的闹钟:闹钟A(ALRM_A)闹钟B(ALRM_B)
  • 到达设定时间时,自动生成闹钟中断。
5. 唤醒定时器
  • 内部提供一个16位的自动唤醒定时器(WUTR)
  • 支持周期性唤醒MCU,例如每2秒、4秒、8秒唤醒一次。
6. 备用寄存器
  • RTC模块提供20个32位的备份寄存器
  • 用来在掉电后保持少量关键数据(配合电池VBAT供电)。

3、RTC最小工作流程

可简单归纳为:

  • RTC内部通过多级分频器精确生成标准的1秒节拍;
  • 配合时间寄存器,可以精准维护年月日时分秒;
  • 支持低功耗模式运行,适合电池供电长时间保持时间;
  • 支持闹钟中断周期性唤醒功能,扩展性强。

1.1.3 RTC模块的基本功能

1、标准时间计时功能

RTC模块能够连续、准确地记录标准时间,支持计时单位包括:

  • 秒(Seconds)
  • 分(Minutes)
  • 小时(Hours)
  • 星期(Weekday)
  • 日期(Date)
  • 月份(Month)
  • 年份(Year)

👉 即使在主芯片掉电、休眠时,只要RTC区域有独立电源(VBAT脚供电),
计时功能也可以持续不中断。

2、时间和日期管理

  • 时间设置:可以通过编程方式设定当前时间(小时、分钟、秒)。
  • 日期设置:可以设定年、月、日、星期。
  • 时间读取:软件可以随时读取当前的实时时间与日期。
  • 时间校准:支持细粒度时间校准(比如微调到更准确的时钟频率)。

3、闹钟功能(Alarm)

RTC内部集成了两个独立闹钟单元:

  • 闹钟A(ALARM A)
  • 闹钟B(ALARM B)

可以设置在指定的时间点触发中断,用于:

  • 唤醒MCU
  • 定时执行任务
  • 产生提醒或报警信号

(例如每天8:00自动唤醒系统,或者每小时整点提醒。)

4、周期性唤醒功能(Wakeup Timer)

  • RTC提供一个16位唤醒定时器(WUTR)
  • 可以设置周期性定时,比如每2秒、4秒、8秒自动产生一次中断。
  • 适合低功耗应用,比如MCU进入睡眠后,周期性被RTC唤醒,进行短时任务处理。

5、时间戳功能(TimeStamp)

  • 当外部触发信号(比如按下按钮)到来时,RTC模块可以记录触发时刻的时间信息
  • 保存触发时的日期、时间,供后续查询。

👉 例如在黑匣子、报警器系统中,记录异常事件的发生时间。

6、备用寄存器功能(Backup Register)

  • STM32 RTC模块提供20个32位的备用寄存器
  • 即使主电源断电,只要VBAT供电,数据依然保存。
  • 通常用于保存:
    • 上次关机时间
    • 用户设置参数
    • 系统运行状态标志

7、低功耗支持

  • RTC模块能够在停机模式(Standby)低功耗运行模式(VBAT模式) 下独立工作。
  • 超低功耗,维持长时间计时,极大延长电池寿命。

1.1.4 常见RTC方案

在嵌入式系统设计中,根据对实时时钟(RTC)的要求不同,
通常有两种主流实现方案:

1、芯片内置RTC方案(片上RTC)

1. 结构特点
  • RTC模块直接集成在主芯片内部(如 STM32F407 内置RTC)。
  • 通过芯片内部电源管理系统供电(支持独立 VBAT 引脚)。
2. 优点
  • 硬件简单:无需外加独立RTC芯片,减少外围元件数量。
  • 功耗低:在低功耗模式下,RTC模块仍能独立运行。
  • 数据访问快:通过内部总线,快速读写时间信息。
  • 成本低:节省外部器件成本,减少PCB面积。
3. 缺点
  • 精度受限:依赖主板的LSE振荡器质量,抗干扰能力相对较差。
  • 功能相对固定:难以扩展更复杂的时间管理功能。
4. 典型应用
  • 中高端MCU应用(如STM32系列、NXP系列)
  • 低功耗物联网设备
  • 智能穿戴设备

2、独立RTC芯片方案(外置RTC)

1. 结构特点
  • 选用外部专用RTC芯片(如DS1307、PCF8563等)。
  • 独立晶振、独立电源,专门负责时间管理。
  • 通过I2C、SPI等总线与主芯片通讯。
2. 优点
  • 高精度:专用RTC芯片带温度补偿,走时精度更高。
  • 抗干扰能力强:独立电源、独立晶振,稳定性好。
  • 功能丰富:部分RTC芯片内置存储器、温度检测、日历功能。
3. 缺点
  • 硬件复杂:增加了芯片、晶振、供电等外围电路。
  • 占用资源:需要占用I2C/SPI总线资源。
  • 成本增加:外加芯片和外围电路提高整体成本。
4. 典型应用
  • 高精度要求场合(如工业自动化、医疗设备)
  • 长时间记录型设备(如数据记录仪、黑匣子)
  • 高可靠性需求的系统(如服务器、卫星通信设备)

3、小结对比

项目芯片内置RTC方案外置独立RTC方案
硬件复杂度
成本
计时精度一般(依赖LSE)高(带补偿)
抗干扰能力较弱很强
适合场景通用低功耗应用高精度/高可靠性应用

1.2 RTC标准库讲解

1.2.1 RTC相关库函数

1、RTC时钟初始化相关

// 启动LSE(32.768kHz外部晶振)
RCC_LSEConfig(RCC_LSE_ON);

// 选择LSE作为RTC时钟源
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);

// 使能RTC时钟模块
RCC_RTCCLKCmd(ENABLE);

// 等待RTC寄存器与APB总线同步
RTC_WaitForSynchro();
  • RCC_LSEConfig():打开或关闭LSE振荡器。

  • RCC_RTCCLKConfig():配置RTC的时钟源。

  • RCC_RTCCLKCmd():使能RTC模块。

  • RTC_WaitForSynchro():等待同步完成,确保配置生效。

2、RTC基本初始化配置

// 初始化RTC的分频系数和小时制
RTC_InitTypeDef RTC_InitStruct;
RTC_InitStruct.RTC_AsynchPrediv = 127;  // 异步预分频器
RTC_InitStruct.RTC_SynchPrediv  = 255;  // 同步预分频器
RTC_InitStruct.RTC_HourFormat   = RTC_HourFormat_24; // 24小时制
RTC_Init(&RTC_InitStruct);
  • RTC_Init():配置RTC模块基本运行参数(分频、小时制)。

3、RTC时间和日期设置与读取

// 设置时间
RTC_TimeTypeDef RTC_TimeStruct;
RTC_TimeStruct.RTC_Hours   = 12;
RTC_TimeStruct.RTC_Minutes = 0;
RTC_TimeStruct.RTC_Seconds = 0;
RTC_SetTime(RTC_Format_BIN, &RTC_TimeStruct);

// 读取时间
RTC_TimeTypeDef RTC_ReadTimeStruct;
RTC_GetTime(RTC_Format_BIN, &RTC_ReadTimeStruct);

// 设置日期
RTC_DateTypeDef RTC_DateStruct;
RTC_DateStruct.RTC_Year    = 25;   // 表示2025年
RTC_DateStruct.RTC_Month   = 4;
RTC_DateStruct.RTC_Date    = 26;
RTC_DateStruct.RTC_WeekDay = RTC_Weekday_Saturday;
RTC_SetDate(RTC_Format_BIN, &RTC_DateStruct);

// 读取日期
RTC_DateTypeDef RTC_ReadDateStruct;
RTC_GetDate(RTC_Format_BIN, &RTC_ReadDateStruct);
  • RTC_SetTime():设置当前时间(小时、分钟、秒)。

  • RTC_GetTime():读取当前时间。

  • RTC_SetDate():设置当前日期(年、月、日、星期)。

  • RTC_GetDate():读取当前日期。

4、RTC闹钟设置相关

// 初始化闹钟结构体(默认清零)
RTC_AlarmTypeDef RTC_AlarmStruct;
RTC_AlarmStructInit(&RTC_AlarmStruct);

// 设置闹钟时间
RTC_AlarmStruct.RTC_AlarmTime.RTC_Hours   = 12;
RTC_AlarmStruct.RTC_AlarmTime.RTC_Minutes = 1;
RTC_AlarmStruct.RTC_AlarmTime.RTC_Seconds = 0;
RTC_SetAlarm(RTC_Format_BIN, RTC_Alarm_A, &RTC_AlarmStruct);

// 使能闹钟A
RTC_AlarmCmd(RTC_Alarm_A, ENABLE);
  • RTC_AlarmStructInit():初始化闹钟结构体为默认值。

  • RTC_SetAlarm():设置闹钟触发时间。

  • RTC_AlarmCmd():使能闹钟功能。

5、RTC中断配置与管理

// 配置闹钟中断
RTC_ITConfig(RTC_IT_ALRA, ENABLE);

// 在中断服务函数中清除闹钟中断标志
RTC_ClearITPendingBit(RTC_IT_ALRA);
  • RTC_ITConfig():允许RTC闹钟产生中断。

  • RTC_ClearITPendingBit():清除RTC的中断标志位,避免重复进入中断。

1.2.2 RTC结构体说明

1、RTC初始化结构体:RTC_InitTypeDef

typedef struct
{
    uint32_t RTC_HourFormat;    // 小时制选择(12小时制 / 24小时制)
    uint32_t RTC_AsynchPrediv;  // 异步预分频值(一般配合LSE配置为127)
    uint32_t RTC_SynchPrediv;   // 同步预分频值(一般配合LSE配置为255)
} RTC_InitTypeDef;
  • RTC_HourFormat :设定时间计数方式,常用24小时制。

  • RTC_AsynchPrediv :异步分频器值,降低APB总线负担。

  • RTC_SynchPrediv :同步分频器值,最终形成1Hz秒脉冲。

✅ 【应用场景】初始化RTC时,需要配置此结构体,然后调用 RTC_Init() 函数。

2、RTC时间结构体:RTC_TimeTypeDef

typedef struct
{
    uint8_t RTC_Hours;    // 小时(0~23)
    uint8_t RTC_Minutes;  // 分钟(0~59)
    uint8_t RTC_Seconds;  // 秒钟(0~59)
} RTC_TimeTypeDef;
  • RTC_Hours :当前小时。

  • RTC_Minutes :当前分钟。

  • RTC_Seconds :当前秒数。

✅ 【应用场景】用来设置或者读取当前的时间值,配合 RTC_SetTime()RTC_GetTime() 使用。

3、RTC日期结构体:RTC_DateTypeDef

typedef struct
{
    uint8_t RTC_WeekDay;  // 星期几(1~7,对应周一~周日)
    uint8_t RTC_Month;    // 月份(1~12)
    uint8_t RTC_Date;     // 日期(1~31)
    uint8_t RTC_Year;     // 年份(00~99)
} RTC_DateTypeDef;
  • RTC_WeekDay :星期几(注意1代表星期一)。

  • RTC_Month :当前月份。

  • RTC_Date :当前日子。

  • RTC_Year :年份后两位,比如25代表2025年。

✅ 【应用场景】用来设置或读取当前日期,配合 RTC_SetDate()RTC_GetDate() 使用。

4、RTC闹钟结构体:RTC_AlarmTypeDef

typedef struct
{
    RTC_TimeTypeDef RTC_AlarmTime;       // 闹钟触发时间(小时/分钟/秒)
    uint32_t RTC_AlarmMask;               // 屏蔽哪些字段进行比较(可选择忽略日期、小时、分钟、秒)
    uint32_t RTC_AlarmDateWeekDaySel;     // 选择日期或星期作为比较标准
    uint8_t  RTC_AlarmDateWeekDay;        // 闹钟匹配的日期值或者星期值
} RTC_AlarmTypeDef;
  • RTC_AlarmTime :设定闹钟触发的具体时间。

  • RTC_AlarmMask :选择忽略哪些位进行闹钟匹配(如只按小时、分钟比较)。

  • RTC_AlarmDateWeekDaySel :选择按日期匹配还是星期匹配。

  • RTC_AlarmDateWeekDay :设定触发闹钟的日期或星期值。

✅ 【应用场景】用来配置闹钟功能,配合 RTC_SetAlarm()RTC_AlarmCmd() 使用。

结构体作用
RTC_InitTypeDef用于RTC模块初始化,配置分频和小时制
RTC_TimeTypeDef用于时间(小时、分钟、秒)设置与读取
RTC_DateTypeDef用于日期(年、月、日、星期)设置与读取
RTC_AlarmTypeDef用于闹钟时间与触发条件设置

1.2.3 RTC常用标志位

1、同步与配置相关标志位

// RTC寄存器同步标志位
RTC_FLAG_RSF

// RTC闹钟A写保护标志位
RTC_FLAG_ALRAWF

// RTC闹钟B写保护标志位
RTC_FLAG_ALRBWF
  • RTC_FLAG_RSF :寄存器同步完成标志。
    必须等待RSF为1,才能保证RTC配置正确同步。

  • RTC_FLAG_ALRAWF :闹钟A寄存器写保护解除标志。
    在配置闹钟A之前,需要确认此标志为1。

  • RTC_FLAG_ALRBWF :闹钟B寄存器写保护解除标志。

✅ 【应用场景】
RTC初始化、闹钟设置时,经常需要等待这些标志变为1,表示可以正常操作。

2、中断事件相关标志位

// RTC闹钟A事件标志
RTC_FLAG_ALRAF

// RTC唤醒事件标志
RTC_FLAG_WUTF

// RTC时间戳事件标志
RTC_FLAG_TSF
  • RTC_FLAG_ALRAF :闹钟A中断触发标志。
    当闹钟A时间到达时,该标志被置位,需要在中断服务函数中清除。

  • RTC_FLAG_WUTF :周期性唤醒定时器中断触发标志。
    每当设置的唤醒时间到达时,该标志被置位。

  • RTC_FLAG_TSF :时间戳触发标志。
    记录了外部事件发生时刻(本课程暂不重点使用)。

✅ 【应用场景】
中断处理函数中,通过检查这些标志来判断中断来源。

3、标志位操作函数

// 检查指定RTC标志位是否置位
FlagStatus RTC_GetFlagStatus(uint32_t RTC_FLAG);

// 清除指定RTC中断挂起位
void RTC_ClearFlag(uint32_t RTC_FLAG);
  • RTC_GetFlagStatus(RTC_FLAG) :检测某个标志是否被置位。

  • RTC_ClearFlag(RTC_FLAG) :清除对应的中断或事件标志位。

✅ 【应用场景】
读取RTC状态、确认操作完成、中断处理后清除对应标志。

标志位作用
RTC_FLAG_RSF确认RTC寄存器同步完成
RTC_FLAG_ALRAWF确认闹钟A寄存器可写
RTC_FLAG_ALRAF判断是否发生闹钟A中断
RTC_FLAG_WUTF判断是否发生唤醒定时器中断
RTC_FLAG_TSF时间戳功能触发标志

1.3 RTC核心寄存器讲解

寄存器主要作用
RTC_TR当前时间(小时、分钟、秒)存储
RTC_DR当前日期(年、月、日、星期)存储
RTC_ALRMAR / RTC_ALRMBR闹钟触发时间设定
RTC_PRER预分频器配置,生成1Hz时钟
RTC_CR控制RTC功能开关
RTC_ISR状态标志检测与初始化管理
RTC_WPR写保护管理,保证安全性

1、RTC时间和日期相关寄存器

// 时间寄存器
RTC_TR

// 日期寄存器
RTC_DR
  • RTC_TR(Time Register):存储当前时间(时、分、秒)。
    软件可以通过 RTC_GetTime()RTC_SetTime() 来访问。

  • RTC_DR(Date Register):存储当前日期(年、月、日、星期)。
    软件可以通过 RTC_GetDate()RTC_SetDate() 来访问。

✅ 【应用场景】
平时读取或设置当前时间和日期,都是基于这两个寄存器操作。

2、RTC闹钟相关寄存器

// 闹钟A寄存器
RTC_ALRMAR

// 闹钟B寄存器
RTC_ALRMBR
  • RTC_ALRMAR(Alarm A Register):设定闹钟A的触发时间。

  • RTC_ALRMBR(Alarm B Register):设定闹钟B的触发时间。

✅ 【应用场景】
在闹钟实验中,通过设置这些寄存器,实现定时中断功能。

3、RTC预分频器寄存器

// 预分频器寄存器
RTC_PRER
  • RTC_PRER(Prescaler Register):控制RTC内部的异步和同步预分频系数。
    通过设置异步分频(PREDIV_A)和同步分频(PREDIV_S),将时钟源分频为1Hz秒脉冲。

✅ 【应用场景】
RTC初始化时,需要正确设置此寄存器,确保计时准确。

4、RTC控制与状态寄存器

// 控制寄存器
RTC_CR

// 初始化与同步状态寄存器
RTC_ISR
  • RTC_CR(Control Register):
    控制RTC功能启用,比如闹钟使能、唤醒功能使能、中断配置等。

  • RTC_ISR(Initialization and Status Register):
    包含RTC初始化标志、同步状态标志、中断标志等。

✅ 【应用场景】

  • 初始化阶段检测或修改状态位;

  • 启用闹钟、唤醒中断;

  • 检查中断来源。

5、RTC写保护控制寄存器

// 写保护寄存器
RTC_WPR
  • RTC_WPR(Write Protection Register):
    RTC寄存器默认处于写保护状态。
    修改RTC配置前,必须先解锁写保护(写0xCA、0x53序列),
    修改完成后,再重新上锁。

✅ 【应用场景】
每次配置RTC参数(如设置时间、日期、闹钟)前,必须先取消写保护。

1.4 RTC实验:时间设置与读取

1.4.1 实验目的

  • 熟悉RTC模块的初始化流程。
  • 学会设置RTC的当前时间。
  • 能够正确读取并通过串口打印当前时间。
  • 理解备份寄存器的作用和RTC掉电保持功能。

1.4.2 配置流程

  1. 开启PWR模块时钟,允许访问备份域。
  2. 启动外部低速晶振(LSE),并等待其稳定。
  3. 选择LSE作为RTC模块的时钟源。
  4. 使能RTC模块。
  5. 等待RTC寄存器同步完成。
  6. 检查备份寄存器:
    • 如果未初始化,配置RTC参数(24小时制、异步预分频、同步预分频)。
    • 设置默认初始时间(如22:19:10)。
    • 写入初始化标志位到备份寄存器。
  7. 设置当前时间或读取当前时间,使用串口打印输出。

1.4.3 实例代码

1、RTC初始化

/**
 * @brief RTC初始化函数
 * 
 * 功能概述:
 * - 启动PWR模块时钟
 * - 允许访问备份域
 * - 启动并等待LSE外部晶振
 * - 配置RTC参数
 * - 防止重复初始化(通过备份寄存器判断)
 */
void RTC_Time_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
    PWR_BackupAccessCmd(ENABLE);

    RCC_LSEConfig(RCC_LSE_ON);
    while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);

    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
    RCC_RTCCLKCmd(ENABLE);

    RTC_WaitForSynchro();

    if (RTC_ReadBackupRegister(RTC_BKP_DR0) != RTC_BACKUP_FLAG)
    {
        RTC_InitTypeDef RTC_InitStruct;
        RTC_InitStruct.RTC_AsynchPrediv = 127;
        RTC_InitStruct.RTC_SynchPrediv  = 255;
        RTC_InitStruct.RTC_HourFormat   = RTC_HourFormat_24;
        RTC_Init(&RTC_InitStruct);

        RTC_Set_Time(INIT_HOUR, INIT_MINUTE, INIT_SECOND);

        RTC_WriteBackupRegister(RTC_BKP_DR0, RTC_BACKUP_FLAG);
    }
}

2、设置RTC当前时间

/**
 * @brief 设置RTC当前时间
 * @param hour 小时
 * @param min 分钟
 * @param sec 秒
 */
void RTC_Set_Time(uint8_t hour, uint8_t min, uint8_t sec)
{
    RTC_TimeStruct.RTC_Hours   = hour;
    RTC_TimeStruct.RTC_Minutes = min;
    RTC_TimeStruct.RTC_Seconds = sec;
    RTC_SetTime(RTC_Format_BIN, &RTC_TimeStruct);
}

3、读取并打印当前时间

/**
 * @brief 读取RTC时间并通过串口打印
 */
void RTC_Read_Time(void)
{
    RTC_GetTime(RTC_Format_BIN, &RTC_TimeStruct);

    printf("当前时间:%02d:%02d:%02d\r\n",
           RTC_TimeStruct.RTC_Hours,
           RTC_TimeStruct.RTC_Minutes,
           RTC_TimeStruct.RTC_Seconds);
}

1.5 RTC实验:闹钟中断实验

1.5.1 实验目的

  • 掌握RTC闹钟功能的使用方法。
  • 能够设置指定时间触发闹钟中断。
  • 熟悉EXTI中断线路与RTC闹钟事件的关联配置。
  • 理解闹钟中断服务函数的基本编写方式。

1.5.2 配置流程

  1. 调用时间初始化函数,确保RTC正常运行。
  2. 配置RTC闹钟:
    • 读取当前时间。
    • 设定闹钟时间(当前秒数+5秒,或者指定具体时间)。
    • 配置闹钟掩码(通常不比较日期,只比较时间)。
    • 开启闹钟A功能。
  3. 配置RTC闹钟中断:
    • 开启闹钟中断。
    • 配置EXTI Line17连接RTC闹钟事件,触发方式为上升沿。
    • 配置NVIC,设置RTC闹钟中断优先级。
  4. 编写闹钟中断服务函数:
    • 判断中断标志。
    • 清除中断挂起位。
    • 执行LED点亮或串口打印提示。

1.5.3 实例代码

1、闹钟中断配置

/**
 * @brief 配置RTC闹钟中断
 * 
 * 功能概述:
 * - 开启RTC闹钟中断
 * - 配置EXTI Line17连接RTC闹钟
 * - 配置NVIC中断优先级
 */
void RTC_Alarm_Interrupt_Config(void)
{
    RTC_ITConfig(RTC_IT_ALRA, ENABLE);

    EXTI_ClearITPendingBit(EXTI_Line17);
    EXTI_InitTypeDef EXTI_InitStruct;
    EXTI_InitStruct.EXTI_Line = EXTI_Line17;
    EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising;
    EXTI_InitStruct.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStruct);

    NVIC_InitTypeDef NVIC_InitStruct;
    NVIC_InitStruct.NVIC_IRQChannel = RTC_Alarm_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);
}

2、设置闹钟(当前时间+5秒)

/**
 * @brief 设置RTC闹钟(当前时间+5秒)
 */
void RTC_Set_Alarm(void)
{
    RTC_AlarmTypeDef RTC_AlarmStruct;
    RTC_TimeTypeDef RTC_NowTimeStruct;

    RTC_GetTime(RTC_Format_BIN, &RTC_NowTimeStruct);

    RTC_AlarmStructInit(&RTC_AlarmStruct);
    RTC_AlarmStruct.RTC_AlarmTime.RTC_Hours   = RTC_NowTimeStruct.RTC_Hours;
    RTC_AlarmStruct.RTC_AlarmTime.RTC_Minutes = RTC_NowTimeStruct.RTC_Minutes;
    RTC_AlarmStruct.RTC_AlarmTime.RTC_Seconds = (RTC_NowTimeStruct.RTC_Seconds + 5) % 60;
    RTC_AlarmStruct.RTC_AlarmMask = RTC_AlarmMask_DateWeekDay;

    RTC_SetAlarm(RTC_Format_BIN, RTC_Alarm_A, &RTC_AlarmStruct);
    RTC_AlarmCmd(RTC_Alarm_A, ENABLE);
}

3、设置闹钟(指定时间)

/**
 * @brief 设定RTC闹钟到指定时间
 * @param hour 小时
 * @param min 分钟
 * @param sec 秒
 */
void RTC_Set_Alarm_Time(uint8_t hour, uint8_t min, uint8_t sec)
{
    RTC_AlarmTypeDef RTC_AlarmStruct;
    RTC_AlarmStructInit(&RTC_AlarmStruct);

    RTC_AlarmStruct.RTC_AlarmTime.RTC_Hours   = hour;
    RTC_AlarmStruct.RTC_AlarmTime.RTC_Minutes = min;
    RTC_AlarmStruct.RTC_AlarmTime.RTC_Seconds = sec;
    RTC_AlarmStruct.RTC_AlarmMask = RTC_AlarmMask_DateWeekDay;

    // 启动 闹钟
    RTC_AlarmCmd(RTC_Alarm_A, DISABLE); // 先关闭    不然下一次不会启动
    RTC_SetAlarm(RTC_Format_BIN, RTC_Alarm_A, &RTC_AlarmStruct);
    RTC_AlarmCmd(RTC_Alarm_A, ENABLE); // 重新打开
}

4、闹钟中断服务函数

/**
 * @brief RTC闹钟A中断处理函数
 */
void RTC_Alarm_IRQHandler(void)
{
    if (RTC_GetITStatus(RTC_IT_ALRA) != RESET)
    {
        RTC_ClearITPendingBit(RTC_IT_ALRA);
        EXTI_ClearITPendingBit(EXTI_Line17);

        LED_ON(2); // 点亮LED2
        printf("闹钟中断触发!\r\n");
    }
}

1.6 RTC实验:唤醒定时器秒中断实验

1.6.1 实验目的

  • 掌握使用RTC唤醒定时器产生周期性中断的方法。
  • 学会配置唤醒定时器实现每秒中断一次。
  • 理解唤醒中断(WUT)与普通RTC闹钟中断的区别。
  • 通过秒中断完成LED翻转、定时任务等简单功能。

1.6.2 配置流程

  1. 禁止唤醒定时器,以便重新配置。
  2. 配置唤醒定时器时钟源:
    • 选择 RTC 1Hz 分频后的时钟(RTC_WakeUpClock_RTCCLK_Div16)。
  3. 设置唤醒定时器计数值,使得中断周期为1秒。
  4. 开启RTC唤醒定时器中断功能。
  5. 配置EXTI Line22连接到唤醒事件:
    • 上升沿触发。
    • 使能EXTI线路。
  6. 配置NVIC,设置RTC唤醒定时器中断优先级。
  7. 重新使能RTC唤醒定时器,开始计时。
  8. 编写秒中断服务函数:
    • 判断中断标志。
    • 清除中断挂起位。
    • 翻转LED状态并串口打印提示。

1.6.3 配置流程

1、唤醒定时器配置(1秒)

/**
 * @brief 配置RTC唤醒定时器,产生1秒中断
 */
void RTC_Second_Interrupt_Config(void)
{
    RTC_WakeUpCmd(DISABLE);

    RTC_WakeUpClockConfig(RTC_WakeUpClock_RTCCLK_Div16);

    RTC_SetWakeUpCounter(0x7FF); // 配合LSE,设为1秒

    RTC_ITConfig(RTC_IT_WUT, ENABLE);

    EXTI_ClearITPendingBit(EXTI_Line22);
    EXTI_InitTypeDef EXTI_InitStruct;
    EXTI_InitStruct.EXTI_Line = EXTI_Line22;
    EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising;
    EXTI_InitStruct.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStruct);

    NVIC_InitTypeDef NVIC_InitStruct;
    NVIC_InitStruct.NVIC_IRQChannel = RTC_WKUP_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    RTC_WakeUpCmd(ENABLE);
}

2、唤醒中断服务函数

/**
 * @brief RTC唤醒(秒)中断服务函数
 */
void RTC_WKUP_IRQHandler(void)
{
    if (RTC_GetITStatus(RTC_IT_WUT) != RESET)
    {
        RTC_ClearITPendingBit(RTC_IT_WUT);
        EXTI_ClearITPendingBit(EXTI_Line22);

        LED_ToggleBits(1); // 翻转LED1
        printf("每秒中断触发!\r\n");
    }
}

第二章 TIM 定时器

2.1 定时器概述

2.1.1 什么是基本定时器

定时器(Timer)是单片机、微控制器内部非常重要的一个外设模块,
它能够按照一定的时钟频率进行计数,并在到达预定值时产生事件(如中断、输出信号等)。

通俗地说:

定时器就像一块小钟表,能够精准测量时间或触发周期性动作。

2.1.2 定时器的主要功能

定时器不仅可以用来计时,还可以扩展实现各种功能,包括但不限于:

  • 时间测量
    测量事件之间的时间间隔(如测速、定时)

  • 定时中断
    每隔一定时间产生中断,常用于系统心跳、周期任务管理。

  • 输出PWM波形
    产生可控占空比的PWM信号(用于电机调速、LED亮度调节等)。

  • 脉冲计数
    统计外部输入的脉冲数量,比如测速脉冲计数。

  • 编码器接口
    直接读取旋转编码器的转动角度或速度。

2.1.3 定时器在实际项目中的应用

在实际应用中,定时器的用途非常广泛,例如:

  • 秒表计时、闹钟提醒功能
  • 定时采集传感器数据
  • 定时刷新屏幕显示
  • 控制步进电机精准转动
  • 无人机飞控中的稳定姿态控制
  • 家电设备中周期性唤醒控制器休眠/运行

几乎每一个微控制器项目,都会不可避免地使用到定时器。

2.2 定时器分类

2.2.1 STM32定时器分类总览

在STM32微控制器中,定时器大致可以分为三类:

  • 常规定时器

    • 基本定时器(TIM6、TIM7)
    • 通用定时器(TIM2、TIM3、TIM4、TIM5、TIM9~TIM14)
    • 高级定时器(TIM1、TIM8)
  • 专用定时器

    • 独立看门狗、窗口看门狗、低功耗定时器等(本章不详细展开)
  • 内核定时器

    • SysTick滴答定时器(由Cortex-M内核提供)

本章重点讨论常规定时器(基本、通用、高级定时器)。

STM32定时器分类

2.2.2 常规定时器分类详细说明

定时器类型主要功能具体说明
基本定时器基本计时主要用于生成简单的时间基准信号,功能较为简单,通常用于周期性中断触发,不支持输入捕获、输出比较、PWM等功能
通用定时器计时、PWM输出、输入捕获、输出比较具备4种工作模式:计数模式、输入捕获模式、输出比较模式、PWM模式。常用于时间延迟、事件计数、PWM信号生成等应用场景。
高级定时器高分辨率PWM输出、死区时间控制、互补输出除了通用定时器功能外,额外支持死区时间生成、互补PWM输出、双边沿捕获等,适合用于电机控制、逆变器控制等高端应用场景。

2.2.3 常见定时器型号分类

定时器种类位宽支持计数模式支持DMA请求捕获/比较通道数互补输出主要应用场景
基本定时器 (TIM6, TIM7)16位向上计数支持不支持用于产生周期性中断、简单时间基准
通用定时器 (TIM2, TIM5)32位向上/向下/向上向下支持4通道不支持PWM输出、事件计时、脉冲测量等
通用定时器 (TIM3, TIM4)16位向上/向下/向上向下支持4通道不支持PWM输出、事件计时、脉冲测量等
通用定时器 (TIM9~TIM14)16位向上计数支持2通道不支持简单PWM输出、定时触发应用
高级定时器 (TIM1, TIM8)16位向上/向下/向上向下支持4通道支持高精度PWM控制、电机驱动控制

2.3 定时器原理

定时器的本质就是计数器。

定时器通过接收精准的时钟脉冲,按照设定的规则进行自动计数,当计数器达到预定值时,可以产生各种事件,如:

  • 中断
  • PWM输出
  • 捕获外部信号
  • 比较输出

简而言之:

定时器 = 时钟 + 计数器 + 控制逻辑

2.3.2 时钟源

时钟源是定时器正常工作的基础,它决定了定时器计数脉冲的速率和精度。

在 STM32F407 中,定时器的时钟源通常来自内部的时钟树(Clock Tree),主要有两种:

  • 系统时钟(SYSCLK)
    通过 APB1 或 APB2 总线分配给定时器模块。

  • 外部时钟输入(ETR)
    某些定时器支持使用外部信号作为时钟输入,实现更灵活或高精度的计时功能。

💡 注意事项:

  • 如果APB总线分频系数不为1,定时器时钟频率通常是APB时钟的2倍。
  • 不同定时器连接不同总线(APB1/APB2),对应不同的最大时钟频率。

2.3.1 计数器

CNT

计数器是定时器的核心组成部分。
在STM32F407中,常见定时器计数器位宽有:

  • 16位计数器(范围0~65535)
  • 32位计数器(范围0~4294967295,如TIM2、TIM5)

计数器的工作原理:

  • 接收预分频器输出的时钟脉冲。
  • 每来一个脉冲,计数器按模式递增或递减。
  • 当达到特定值(如ARR自动重载寄存器的值)时产生更新事件。

💡 计数器的工作模式有多种(后面章节详细讲解):

  • 向上计数模式
  • 向下计数模式
  • 中心对齐模式(先上升后下降)

2.3.3 分频器

定时器内部集成了一个预分频器(PSC)
用于将输入时钟信号进行分频,降低计数器的计数速度,从而调整定时器的定时周期。

预分频器基本原理:

  • 预分频器可以设定一个分频系数 N。
  • 定时器实际工作频率 = 输入时钟频率 ÷ (N+1)。

举例说明:

系统输入时钟预分频器设置值定时器计数频率
36 MHz351 MHz
72 MHz719910 KHz

通过设置预分频器,可以方便地控制计时器的计数周期,从而实现不同时间长度的定时任务

2.3.4 整体结构

定时器的基本整体结构可以理解为:
时钟源 ➔ 预分频器(PSC) ➔ 计数器(CNT) ➔ 自动重装载寄存器(ARR)

  • 时钟源(Ft)
    提供给定时器的基础脉冲信号。

  • 预分频器(PSC)
    将输入时钟Ft降低频率,输出更慢的时钟信号给计数器。

  • 计数器(CNT)
    按照分频后时钟脉冲递增或递减计数。

  • 自动重装载寄存器(ARR)
    设定计数器的最大值或最小值,计数到达后发生更新事件(溢出或下溢)。

2.3.5 时间计算公式

定时器产生一次更新事件(溢出/下溢)的时间周期,可以用下面的公式计算:

$$
T_{out} = \frac {(ARR + 1) \times (PSC + 1)} {{F_t}}
$$

  • $T_{out}$ —— 定时器溢出时间

  • $F_t$ —— 定时器的时钟源频率

  • $ARR$ —— 自动重装载寄存器的值

  • $PSC$ —— 预分频器的值

其中:

  • 计数器值:计数器从 0 到设定的值,单位为脉冲数。

  • 预分频器值:分频器的设定值。

  • 时钟频率:定时器的时钟频率,通常由系统时钟经过预分频器后得到。

举例说明

假设系统时钟 $F_t = 84 MHz$,
设置预分频器 $PSC = 8399$,自动重装载寄存器 (ARR = 9999),
那么定时器溢出时间为:

$$
T_{out} = \frac{(9999+1) \times (8399+1)}{84 \times 10^6} = 1,\text{秒}
$$

即:每1秒触发一次中断

2.3.6 影子寄存器

1、什么是影子寄存器?

在STM32的定时器中,影子寄存器(Shadow Register) 指的是:

  • 计数器、预分频器、重装载寄存器等关键寄存器的数据,并不是立即生效的。
  • 修改的新值先暂存到寄存器中,只有在特定时机才更新到影子寄存器

💡 例如:

  • 修改了ARR(自动重装载寄存器)后,新的值会保存在寄存器里。
  • 只有在下一次更新事件(UEV)发生时,新ARR值才真正加载到计数器逻辑。

2、为什么需要影子寄存器?

优点说明
保持一致性避免在计数器正在运行过程中突变,导致异常
提高可靠性保证参数切换在更新事件同步完成,稳定输出
支持复杂控制尤其在PWM、互补输出、死区控制等复杂应用中尤为重要

3、影子寄存器更新时机

通常,在以下情况下,影子寄存器的内容会更新到实际寄存器:

  • 发生更新事件(UEV)
  • 软件手动产生更新请求(设置UG位)
  • 自动模式下的周期性刷新

✅ 影子寄存器是为了保证计数稳定性切换安全性引入的一种机制。
✅ 修改定时器关键参数时,理解影子寄存器的工作机制非常重要!

2.4 定时器模式

2.4.1 定时器计数模式简介

STM32定时器支持多种计数模式,不同模式下,计数器(CNT)的变化规律不同。
不同计数模式对应不同的溢出条件,见下表:

计数器模式溢出条件
递增计数模式 向上计数CNT == ARR
递减计数模式CNT == 0
中心对齐计数模式CNT == ARR-1 或 CNT == 1
  • ARR:自动重装载寄存器的设定值
  • CNT:当前计数器的值
定时器模式图示

图中展示了三种不同计数方式下CNT的变化曲线。

2.4.2 递增计数模式(向上计数)

  • 计数器从0递增,每来一个时钟脉冲,CNT加1。
  • 当CNT计数到ARR值时,产生更新事件(UEV)。

参数示例:

  • 预分频器:PSC = 1
  • 自动重装载:ARR = 36
向上计数模式

更新事件发生后,计数器清零,重新开始计数。

2.4.3 递减计数模式(向下计数)

  • 计数器从ARR开始,每来一个时钟脉冲,CNT减1。
  • 当CNT计数到0时,产生更新事件(UEV)。

参数示例:

  • 预分频器:PSC = 1
  • 自动重装载:ARR = 36
向下计数模式

更新事件发生后,计数器重新加载ARR值,继续向下计数。

2.4.4 中心对齐模式(向上/向下计数)

  • 计数器从0递增到ARR-1,再反向递减到1。
  • 每次在ARR-1或1时,产生更新事件(UEV)。

参数示例:

  • 预分频器:PSC = 0
  • 自动重装载:ARR = 6
中心对齐计数模式

中心对齐模式广泛用于PWM波形的对称输出场景(如电机控制)。

2.4.5 影子寄存器作用

影子寄存器用于避免参数修改时,导致计数器行为异常或不连续。
特别是预分频器(PSC)和重载值(ARR)等重要寄存器,修改后不会立即生效
而是在下一个更新事件(UEV)时统一加载

示例:预分频器由1变为2时的计数器时序图

影子寄存器作用

💡 注意事项:

  • 修改预分频器、ARR等,需要等待更新事件才正式应用。
  • 可以通过手动设置UG位,强制立即产生更新事件,刷新影子寄存器。

第三章 基本定时器

3.1 基本定时器简介

3.1.1 什么是基本定时器

基本定时器(Base Timer)是 STM32 定时器家族中的一种,
它的主要作用是提供一个基本的定时功能或时间基准,用于产生定时中断。

与通用定时器、高级定时器相比,基本定时器结构更为简单:

  • 只有计数功能
  • 不支持输入捕获(IC)、输出比较(OC)和PWM输出
  • 通常用于定时中断、时间延迟管理等场景

💡 典型代表:TIM6TIM7

3.1.2 基本定时器的特性

特性说明
计数方式支持向上计数
位宽16位计数器(范围0~65535)
时钟源由APB1总线提供时钟
预分频器支持灵活设置,调整计数频率
中断功能支持更新事件(UEV)产生中断
DMA请求支持通过DMA传输数据(例如自动更新计数值)
无复杂功能不支持捕获、比较、PWM等高级功能

💡 小提醒:
TIM6、TIM7在STM32F4系列中只用于定时和基本中断,非常适合系统心跳、定时刷新任务等应用场景。

3.1.3 基本定时器的主要应用场景

基本定时器广泛应用于以下任务中:

  • 定时中断
    定时器溢出产生中断,用于周期性执行某些任务,如LED闪烁、传感器数据采集等。

  • 延时处理
    通过定时器精确产生微秒级或毫秒级的延时。

  • 系统心跳管理
    生成固定周期的系统心跳信号,监控系统运行状态。

  • 配合DMA进行周期性数据搬运
    通过定时器触发DMA搬运数据,减少CPU负担,提高效率。

  • 作为DAC的触发源
    TIM6在某些型号中,能够作为DAC(数模转换器)的触发源,实现定时数模输出。

3.2 基本定时器框图

3.2.1 框图概览

基本定时器(如 TIM6、TIM7)的内部结构主要包含:

  • 时钟源模块
  • 控制单元
  • 时基单元

整体框图如下:

基本定时器时钟树

注:基本定时器内部结构相对简单,主要实现基本计时和中断功能,不包含复杂捕获/比较/PWM单元。

3.2.2 时钟源模块

基本定时器的时钟来源于RCC模块,通过时钟树分发:

基本定时器总框图

具体过程:

时钟源示意
  • 外部晶振(或内部振荡源)提供基础时钟
  • 经过PLL锁相环倍频
  • 最终生成系统主时钟(SYSCLK)
  • SYSCLK再分配给APB1/APB2总线,驱动各类定时器

基本定时器(TIM6/TIM7)位于APB1总线上,时钟来源为APB1时钟

1、时钟示意图

项目内容
时钟来源APB1
默认频率42 MHz(若APB1=42MHz)

2、控制寄存器说明

  • CEN位(CR1寄存器):计数器使能位,置1启动定时器,置0停止。
  • UG位(EGR寄存器):软件触发一次更新事件(软复位),置1产生更新后自动清零。

⚡ CEN 需要手动清除,UG位是自动回弹(自清零)的。

3.2.3 控制单元模块

控制单元用于内部控制计数器的复位、启用、停止,以及产生外部触发信号(TRGO)。

控制单元
  • TRGO(Trigger Output)触发输出

    • 当计数器发生更新事件(UEV)时,可产生一个TRGO信号
    • 该信号可以用于触发外部模块,如ADC/DAC等
  • 内部控制逻辑

    • 复位:清零计数器
    • 使能:启动计数
    • 停止:暂停计数
    • 软件清零:通过UG位产生软复位

🎯 小提示:基本定时器常用于作为DAC模块的触发源。

3.2.4 时基单元模块

时基单元是定时器的核心模块,完成具体的计数功能。

时基单元

主要组成:

模块功能描述
预分频器(PSC)将定时器输入时钟进行分频,生成更低频率的计数时钟
计数器(CNT)实际的计数器,累加或递减
重装载寄存器(ARR)计数达到ARR值后产生溢出,触发更新事件
影子寄存器(Shadow Register)防止中途修改寄存器导致计数器异常跳变

1、影子寄存器机制

在定时器中,为了保证更新操作的稳定性,部分寄存器(如PSC、ARR)实际上有两个版本:

  • 用户缓冲区:程序修改的值
  • 影子寄存器:定时器实际工作的值

修改PSC/ARR后,不会立刻生效,必须等到下一次更新事件时才真正加载到影子寄存器中。

2、示例时序图

影子寄存器更新时序

要点:

  • 修改值后,等待下一个UEV,PSC/ARR新值才正式生效
  • 若希望立即更新,可以手动设置UG位触发一次更新

⚡ TIMx_CR1寄存器中的ARPE位用于控制是否启用ARR影子寄存器。

3.3 基本定时器寄存器

3.3.1 控制寄存器(TIMx_CR1)

名称功能描述
0CEN计数器使能(0=停止,1=启动)
1UDIS更新事件禁止(0=使能,1=禁止)
2URS更新请求源选择(0=全部源,1=仅溢出)
3OPM单脉冲模式(0=连续计数,1=单次停止)
7ARPE自动重载预装载使能(0=无缓冲,1=缓冲ARR)
  • CEN(计数器启动/停止)
    置1启动计数器,置0停止计数。

  • UDIS(禁止更新事件)
    置1禁止自动产生更新事件(UEV)。

  • URS(更新请求源选择)
    控制哪些事件能触发更新中断或DMA请求。

  • OPM(单脉冲模式)
    配置成只计数到一次溢出就停止。

  • ARPE(自动重载缓冲)
    使ARR寄存器的写入延迟到更新事件时生效,提高稳定性。

3.3.2 TIM6 和 TIM7 DMA/中断使能寄存器 (TIMx_DIER)

名称功能描述
8UDE更新DMA请求使能
0UIE更新中断使能
  • UIE(更新中断使能)
    置1后,当计数器溢出(或触发更新事件)时,产生中断。

  • UDE(更新DMA请求使能)
    置1后,更新事件可触发DMA搬运数据。

3.3.3 TIM6 和 TIM7 状态寄存器 (TIMx_SR)

名称功能描述
0UIF更新中断标志位

常用位简述

  • UIF(更新中断标志)
    • 1:计数器溢出或更新事件发生。
    • 0:无事件发生。

软件读取后需手动清除此位,或通过自动清除机制清除。

3.3.4 TIM6 和 TIM7 事件生成寄存器 (TIMx_EGR)

名称功能描述
0UG软件触发更新事件
  • UG(更新生成)
    写1时立即触发一次更新事件(强制重新载入PSC/ARR等)。

3.3.4 TIM6 和 TIM7 计数器 (TIMx_CNT)

名称功能描述
15:0CNT[15:0]当前计数器数值

常用简述

  • 用于实时读取当前计数值,或软件直接写入新值进行重置。

3.3.5 TIM6 和 TIM7 预分频器 (TIMx_PSC)

名称功能描述
15:0PSC[15:0]预分频器值
  • PSC=0,则不分频
  • PSC=N,则分频系数=N+1

计算公式:

$$
CK_CNT = \frac {f_{CK_PSC}} {(PSC[15:0] + 1)}
$$

3.3.6 TIM6 和 TIM7 自动重载寄存器 (TIMx_ARR)

名称功能描述
15:0ARR[15:0]自动重载值
  • 定义计数器计数终点(溢出值)。
  • CNT每次计到ARR值时触发更新事件。

3.4 基本定时器库函数

3.4.1 基本定时器库函数

// 1. 初始化定时器参数
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);

// 2. 使能或禁止定时器
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);

// 3. 使能或禁止更新中断
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);

// 4. 清除定时器中断标志位
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);

// 5. 设置计数器当前值
void TIM_SetCounter(TIM_TypeDef* TIMx, uint32_t Counter);

// 6. 设置自动重装载寄存器ARR值
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint32_t Autoreload);

3.4.2 基本定时器结构体

typedef struct
{
    uint16_t TIM_Prescaler;        // 预分频器   84Mhz / 8399
    uint16_t TIM_CounterMode;      // 计数器模式(向上计数/向下计数)
    uint32_t TIM_Period;           // 自动重装载值(ARR)    
    uint16_t TIM_ClockDivision;    // 时钟分频器(一般用默认)
    uint8_t  TIM_RepetitionCounter;// 重复计数器(TIM6/TIM7无效)
} TIM_TimeBaseInitTypeDef;
字段说明备注
TIM_Prescaler设置预分频器,将输入时钟降低。控制计数频率
TIM_CounterMode计数方向(向上/向下/中心对齐)。常用向上计数
TIM_Period自动重载值(ARR),计数到此值溢出。配合PSC共同决定定时周期
TIM_ClockDivision时钟分频(不重要,默认即可)。基本定时器用 TIM_CKD_DIV1
TIM_RepetitionCounter重复计数器(高级定时器用)。TIM6/TIM7无效,设0

3.5 基本定时器实验:定时器中断

3.5.1 配置流程

3.5.2 实例代码

1、定时器初始化

/**
 * @brief 配置基本定时器TIM6
 */
void TIM6_Init(uint16_t arr, uint16_t psc)
{
    // 开启 TIM6 时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);

    // 定时器基本配置
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
    TIM_TimeBaseInitStruct.TIM_Period = arr;              // 自动重装载值
    TIM_TimeBaseInitStruct.TIM_Prescaler = psc;            // 预分频系数
    TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频
    TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
    TIM_TimeBaseInit(TIM6, &TIM_TimeBaseInitStruct);

    // 使能定时器更新中断
    TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);

    // 配置 NVIC
    NVIC_InitTypeDef NVIC_InitStruct;
    NVIC_InitStruct.NVIC_IRQChannel = TIM6_DAC_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    // 启动定时器
    TIM_Cmd(TIM6, ENABLE);
}

2、中断服务函数

/**
 * @brief TIM6中断服务函数
 */
void TIM6_DAC_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIM6, TIM_IT_Update); // 清除更新中断标志

        // 翻转LED,作为中断触发的测试效果
        LED_D2_Toggle();
    }
}

3.6 基本定时器实验:桌面计时器-数码管显示

3.6.1 实验目的

  • 学习使用基本定时器(TIM6)周期性刷新数码管。

  • 掌握通过定时器+RTC实现桌面计时器功能。

  • 实现数码管动态刷新、数值显示和时间同步显示。

  • 理解多位数码管动态扫描的原理与实现技巧。

3.6.2 程序流程

  1. 初始化TIM6定时器,使其每2ms中断一次,用于刷新数码管。

  2. 定义数码管段码缓存区,并设计位切换机制(动态扫描)。

  3. 在TIM6中断服务函数中:

    • 如果选择时间显示模式,读取RTC当前时间。

    • 秒变化时,更新显示内容(分钟*100 + 秒)。

    • 每次中断刷新一位数码管,实现动态显示。

    • 如果选择数值模式,直接显示固定数字。

  4. 提供接口函数,支持外部切换显示数值或模式。

3.6.3 实例代码

1、数码管动态刷新函数

/**
 * @brief 数码管动态刷新函数(每次调用刷新一位)
 * @param num 要显示的4位数字(最大支持9999)
 */
void TIM_6_Nexie_Show(uint16_t num)
{
    static uint8_t seg_buf[4];        // 四位数码管段码缓存
    static uint8_t cur_idx = 0;        // 当前刷新位索引(0~3)
    static uint16_t last_num = 0xFFFF; // 上次显示的数字(初值为非法值)

    // 数值变化时重新拆分
    if (num != last_num)
    {
        last_num = num;

        // 限制最大值,防止数组访问越界
        if (num > 9999) num = 9999;

        // 拆分千位、百位、十位、个位
        seg_buf[0] = num / 1000;
        seg_buf[1] = (num / 100) % 10;
        seg_buf[2] = (num / 10) % 10;
        seg_buf[3] = num % 10;
    }

    // 刷新当前位数码管
    Nexie_Send(seg_buf[cur_idx], cur_idx);

    // 切换到下一位,循环刷新
    cur_idx++;
    if (cur_idx >= 4)
        cur_idx = 0;
}

2、全局变量声明

// 数码管显示的数值
static uint16_t tim_6_nexie_val = 0;

// 定时器计时相关变量
static uint8_t tim_6_mode = 1;           // 模式选择(1-显示时间,2-显示数字)
static uint32_t tim_6_ms_cnt = 0;         // 毫秒计数(暂未使用,可扩展)
static uint8_t tim_6_sec_cnt = 0;         // 秒计数(备用)
static uint8_t tim_6_min_cnt = 0;         // 分钟计数(备用)

// RTC时间缓存变量
static uint8_t rtc_tim_6_last_sec = 0;         // 上一次记录的秒数
static RTC_TimeTypeDef rtc_tim_6_now_time;     // 当前时间结构体

// 数码管模式定义
#define TIM_6_NEXIE_MODE_TIME    1   // 时间模式(显示分钟:秒)
#define TIM_6_NEXIE_MODE_NUMBER  2   // 数值模式(固定数字)

3、数码管显示数值设置函数

/**
 * @brief 设置数码管要显示的数值
 * @param num 要显示的数字(0~9999)
 */
void TIM_6_Nexie_Set(uint16_t num)
{
    tim_6_nexie_val = num;
}

4、TIM6中断服务函数

/**
 * @brief TIM6中断服务函数(刷新数码管+RTC时间同步)
 */
void TIM6_DAC_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIM6, TIM_IT_Update); // 清除中断标志

        if (tim_6_mode == TIM_6_NEXIE_MODE_TIME)
        {
            // 读取RTC当前时间
            RTC_GetTime(RTC_Format_BIN, &rtc_tim_6_now_time);

            // 秒数变化时更新显示内容
            if (rtc_tim_6_now_time.RTC_Seconds != rtc_tim_6_last_sec)
            {
                rtc_tim_6_last_sec = rtc_tim_6_now_time.RTC_Seconds;

                // 数码管显示分钟+秒钟(例如 19分20秒 -> 1920)
                tim_6_nexie_val = (rtc_tim_6_now_time.RTC_Minutes * 100) + rtc_tim_6_now_time.RTC_Seconds;
            }

            // 刷新一位数码管
            TIM_6_Nexie_Show(tim_6_nexie_val);
        }
        else if (tim_6_mode == TIM_6_NEXIE_MODE_NUMBER)
        {
            // 直接显示固定数值
            TIM_6_Nexie_Show(tim_6_nexie_val);
        }
    }
}

3.7 基本定时器实验:状态机按钮长短按识别

3.7.1 实验目的

  • 学习如何基于基本定时器扫描按键状态。

  • 理解按键抖动问题及消抖处理方式。

  • 掌握按钮状态机编程思想,实现按键短按、长按的区分识别。

  • 熟悉使用定时器中断定时扫描按键输入的机制。

3.7.3 状态机介绍

在实际按键检测中,由于存在按键抖动现象,仅依靠简单的电平检测容易出现误判。
为了解决这个问题,常使用状态机的思想来管理按键的行为变化。

所谓状态机,就是用一组明确的状态,配合清晰的状态跳转逻辑,来描述按键的全过程。
在本实验中,按键的状态划分如下:

状态名说明
KEY_IDLE松开状态,按键未被按下。
KEY_PRESS检测到按下,等待确认是否稳定(消抖)。
KEY_HOLD按住状态,按键稳定按下,开始计时。
KEY_RELEASE检测到松开,判断短按或长按。

通过定时器周期性地扫描按键电平,每10ms更新一次状态。
按键按下超过一定时间阈值(如200次10ms = 2秒)即认为是长按
否则为短按

优点总结

  • 有效避免按键抖动带来的误判
  • 清晰划分按键按下和松开过程
  • 便于后续扩展多按键、多功能管理

这种方法在实际开发中非常常用,尤其适用于:

  • 按钮数量较多
  • 需要区分短按、长按、连击等不同功能的应用场景。

3.7.3 配置流程

  1. 初始化基本定时器(TIM6):

    • 设置定时器周期,配置为10ms中断一次(用于定时扫描按键)。

    • 开启定时器更新中断功能。

    • 配置NVIC中断优先级,打开定时器中断。

  2. 配置按键GPIO输入引脚:

    • 设置按键对应的GPIO端口为输入模式。

    • 配置上拉(如有必要)以保证空闲时为高电平。

  3. 设计按键状态机逻辑:

    • 定义按键的4种状态:松开(KEY_IDLE)按下检测(KEY_PRESS)按住(KEY_HOLD)松开检测(KEY_RELEASE)

    • 在定时器中断服务函数中按周期扫描按键状态,并更新状态机状态。

  4. 编写短按/长按识别规则:

    • 短按:按下时间小于设定阈值(如2秒)。

    • 长按:按下时间大于设定阈值(如2秒)。

  5. 在状态机中加入打印提示:

    • 短按识别后串口打印“短按”。

    • 长按识别后串口打印“长按”。

3.6.4 示例代码

// 按键状态定义
#define KEY_IDLE    0   // 松开状态
#define KEY_PRESS   1   // 按下检测
#define KEY_HOLD    2   // 按住中
#define KEY_RELEASE 3   // 松开检测

static uint8_t key_state = KEY_IDLE; // 当前按键状态
static uint32_t key_time = 0;        // 按键按下持续时间(单位:10ms)

/**
 * @brief 单个按键状态机扫描函数(周期10ms调用)
 */
void Key_Scan(void)
{
    uint8_t key_input = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_9); // 读取按键电平

    switch (key_state)
    {
    case KEY_IDLE:
        if (key_input == 0) // 检测到按下
        {
            key_state = KEY_PRESS;
            key_time = 0;
        }
        break;

    case KEY_PRESS:
        if (key_input == 0)
        {
            key_time++;
            if (key_time > 2) // 消抖判断(约20ms)
            {
                key_state = KEY_HOLD;
                key_time = 0;
            }
        }
        else
        {
            key_state = KEY_IDLE; // 抖动回弹
            key_time = 0;
        }
        break;

    case KEY_HOLD:
        if (key_input == 0)
        {
            key_time++; // 按住计时
        }
        else
        {
            key_state = KEY_RELEASE;
        }
        break;

    case KEY_RELEASE:
        if (key_time >= 200) // 按下超过2秒,认定为长按
        {
            printf("长按识别!\r\n");
        }
        else
        {
            printf("短按识别!\r\n");
        }
        key_state = KEY_IDLE;
        key_time = 0;
        break;

    default:
        key_state = KEY_IDLE;
        key_time = 0;
        break;
    }
}

3.8 基本定时器实验:多按键状态机+蜂鸣器提示

3.8.1 实验目标

  • 使用 TIM6 基本定时器周期性扫描按键。

  • 使用状态机思想管理按键按下/按住/松开过程。

  • 正确区分短按(小于2秒)和长按(大于等于2秒)。

  • 按键确认按下和长按时,蜂鸣器各响一次提示。

  • 支持 多个按键(KEY1、KEY2、KEY3) 同时独立处理。

功能总结

  • 三个独立按键,互不影响,每个按键有自己的状态机。

  • 短按/长按精准识别,支持消抖处理,避免误判。

  • 蜂鸣器提示反馈,按下和长按时各响一次,用户体验好。

  • 基于定时器周期扫描,不会影响主程序,响应及时,结构清晰。

方法优点

优点说明
程序结构清晰每个按键独立状态管理,逻辑清晰易懂
抖动防护完善有效防止机械按键抖动造成误触发
扩展性强可以轻松增加新功能,如双击检测、组合键检测
蜂鸣器提示友好提升用户操作体验,动作有声音提示
多任务无阻塞基于定时器中断扫描,不影响主循环执行其他任务

3.8.2 程序流程

  1. 初始化定时器,配置为10ms中断一次,用于定时扫描按键。

  2. 定义按键状态机(IDLE、PRESS、HOLD、RELEASE)。

  3. 每次定时器中断时:

    • 扫描按键电平变化。

    • 按照状态机流程管理按键状态转移。

    • 确认按下后蜂鸣器短响提示。

    • 检测长按(2秒)时蜂鸣器再次长响提示。

    • 松开后根据按下时间判断是短按还是长按,并打印结果。

  4. 蜂鸣器响动时间自动管理,时间到后自动关闭。

3.8.3 实例代码

1、按键状态定义

// 按键状态定义
#define KEY_IDLE    0   // 松开状态
#define KEY_PRESS   1   // 按下检测状态
#define KEY_HOLD    2   // 按住中状态
#define KEY_RELEASE 3   // 松开检测状态

2、按键输入引脚宏定义

// 按键读取宏定义(0按下,1松开)
#define KEY1_READ()   GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_9)
#define KEY2_READ()   GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_8)
#define KEY3_READ()   GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_5)

3、蜂鸣器控制宏定义(高电平响)

// 蜂鸣器操作宏
#define BEEP_ON()     GPIO_SetBits(GPIOA, GPIO_Pin_15)
#define BEEP_OFF()    GPIO_ResetBits(GPIOA, GPIO_Pin_15)

4、按键状态结构体定义

// 每个按键的状态机结构体
typedef struct
{
    uint8_t state;    // 当前状态(IDLE/PRESS/HOLD/RELEASE)
    uint32_t time;    // 按住持续时间(单位:10ms)
} KEY_T;

5、全局变量声明

// 三个按键实例
static KEY_T key1 = {KEY_IDLE, 0};
static KEY_T key2 = {KEY_IDLE, 0};
static KEY_T key3 = {KEY_IDLE, 0};

// 蜂鸣器控制变量
static uint8_t beep_on = 0;      // 蜂鸣器开启标志
static uint32_t beep_time = 0;   // 蜂鸣器剩余响动时间(单位10ms)

6、按键扫描与状态机处理函数

/**
 * @brief 单个按键状态机扫描函数
 * @param key      指向对应按键的结构体
 * @param key_input 当前按键电平状态(0按下,1松开)
 * @param key_num   按键编号(用于打印)
 */
void Key_Scan(KEY_T* key, uint8_t key_input, uint8_t key_num)
{
    switch (key->state)
    {
    case KEY_IDLE:
        if (key_input == 0)
        {
            key->state = KEY_PRESS;
            key->time = 0;
        }
        break;

    case KEY_PRESS:
        if (key_input == 0)
        {
            key->time++;
            if (key->time > 2) // 消抖(大约20ms)
            {
                key->state = KEY_HOLD;
                key->time = 0;

                // 按下确认,蜂鸣器响一声
                BEEP_ON();
                beep_on = 1;
                beep_time = 5; // 响50ms
            }
        }
        else
        {
            key->state = KEY_IDLE;
            key->time = 0;
        }
        break;

    case KEY_HOLD:
        if (key_input == 0)
        {
            key->time++;
            if (key->time == 200) // 长按2秒
            {
                BEEP_ON();
                beep_on = 1;
                beep_time = 20; // 响200ms
            }
        }
        else
        {
            key->state = KEY_RELEASE;
        }
        break;

    case KEY_RELEASE:
        if (key->time >= 200)
        {
            printf("KEY%d 长按\n", key_num);
        }
        else
        {
            printf("KEY%d 短按\n", key_num);
        }
        key->state = KEY_IDLE;
        key->time = 0;
        break;

    default:
        key->state = KEY_IDLE;
        key->time = 0;
        break;
    }
}

7、TIM6中断服务函数

/**
 * @brief TIM6中断服务函数
 * @note 每10ms扫描一次所有按键,同时管理蜂鸣器响动时间
 */
void TIM6_DAC_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIM6, TIM_IT_Update);

        // 扫描三个按键
        Key_Scan(&key1, KEY1_READ(), 1);
        Key_Scan(&key2, KEY2_READ(), 2);
        Key_Scan(&key3, KEY3_READ(), 3);

        // 蜂鸣器响动管理
        if (beep_on)
        {
            if (beep_time > 0)
            {
                beep_time--;
                if (beep_time == 0)
                {
                    BEEP_OFF();
                    beep_on = 0;
                }
            }
        }
    }
}
通过长按 key1 进入数码管显示的应用

通过key1按钮短按 切换模式
一、显示时钟  时钟需要和实际时间相同
二、显示倒计时 
		第一次 长按key1按钮开始倒计时 
		第二次 长按key1按钮停止倒计时
	在倒计时未启动 或暂停情况下
		短按 key2 设置定时时间 增
		短按 key3 设置定时时间 减
	短按key1 不管 定时器是否启动 都可以切换模式
三、显示温度
四、显示湿度

长按 KEY USET 退出数码管显示



第四章 通用定时器

4.1 通用定时器概述

4.1.1 什么是通用定时器

通用定时器(TIMx,例如TIM2、TIM3、TIM4等)是STM32内部非常灵活强大的计时模块。
它不仅可以用于基本的定时和计数,还支持输入捕获输出比较PWM输出等多种功能。

简单来说,通用定时器是一个功能丰富应用广泛的计数器模块,既能单独工作,也能配合其他外设完成复杂任务。

4.1.2 通用定时器的特性

  • 支持多种计数模式

    • 向上计数、向下计数、中心对齐计数。
  • 支持输入捕获

    • 可以检测外部输入信号的上升沿、下降沿,测量脉宽、周期等。
  • 支持输出比较

    • 可以在特定时间输出一个高低电平跳变,用于事件同步、时间控制等。
  • 支持PWM输出

    • 可生成脉宽可调的PWM信号,用于电机控制、LED调光、舵机控制等。
  • 支持DMA传输

    • 可以结合DMA实现数据自动搬运,提高效率。
  • 支持中断事件

    • 可配置更新中断、捕获比较中断、触发中断等多种事件响应。
  • 支持主从模式同步

    • 多个定时器可以级联协同工作,实现复杂的同步控制场景。

STM32F407通用定时器功能对比表

定时器位宽通道数计数模式支持捕获比较支持PWM输出支持编码器模式特点或限制
TIM232位4通道向上/向下/中心对齐支持支持支持通用定时器中功能最全,32位宽
TIM316位4通道向上/向下/中心对齐支持支持支持功能丰富,常用
TIM416位4通道向上/向下/中心对齐支持支持支持常用
TIM532位4通道向上/向下/中心对齐支持支持支持32位宽,更适合长时间测量
TIM916位2通道仅支持向上计数支持支持不支持通道少,仅2个,简化版
TIM1016位1通道仅支持向上计数支持支持不支持资源少,适合小功能
TIM1116位1通道仅支持向上计数支持支持不支持用于简单定时
TIM1216位2通道仅支持向上计数支持支持不支持比较常用于简单PWM输出
TIM1316位1通道仅支持向上计数支持支持不支持简单应用
TIM1416位1通道仅支持向上计数支持支持不支持简单应用

通用定时器与基本定时器的区别

项目通用定时器(TIM2/3/4等)基本定时器(TIM6/7)
计数模式支持向上、向下、中心对齐仅支持向上计数
捕获功能支持输入捕获不支持
比较功能支持输出比较不支持
PWM输出支持不支持
通道数2~4个通道无通道
主从同步支持不支持
主要应用计时/捕获/PWM/控制简单定时、溢出中断

总结:
基本定时器只会简单计时;
通用定时器不仅能计时,还能“捕捉、比较、输出”,功能更强大!

4.1.3 通用定时器应用场景

  • 脉冲信号测量
    测量传感器输出脉冲宽度、频率,例如超声波测距模块。

  • PWM波形输出
    控制电机转速、LED亮度、舵机位置等。

  • 定时输出控制
    精确地在某个时间点产生信号,例如拍照触发、同步控制。

  • 事件同步与触发
    不同模块之间需要严格时间同步时,通用定时器可以作为主控触发器使用。

4.2 通用定时器框图

4.2.1 框图概述

在 STM32F407 中,通用定时器(如 TIM2、TIM3、TIM4、TIM5)的内部结构比基本定时器更加复杂和强大。
除了支持最基础的定时/计时功能外,还扩展支持:

  • 输入捕获(测量外部信号特性)

  • 输出比较(控制输出信号变化)

  • PWM生成(脉宽调制应用)

  • 编码器接口模式(读取旋转编码器位置信息)

相比基本定时器,通用定时器功能更加全面,应用范围更加广泛。

在 STM32F407 中,通用定时器(如 TIM2、TIM3、TIM4、TIM5)内部结构比基本定时器复杂得多。
它不仅支持普通的定时/计时功能,还支持输入捕获、输出比较、PWM生成、编码器接口等高级应用。

整张图可以清晰地分为 6大模块

模块编号模块名称主要功能简述
时钟源决定定时器内部计数的时钟来源,包括内部时钟、外部触发信号(ETR)、内部触发(ITRx)等
控制器管理定时器启动/停止、复位、同步、触发控制等核心功能
时基单元包括预分频器(PSC)+ 计数器(CNT)+ 自动重装载器(ARR),实现基本的时间基准计数
输入捕获捕捉外部信号的上升沿、下降沿,测量脉宽、频率等
捕获/比较(共享)负责将捕获的数据暂存,也用于比较定时器数值,产生输出事件
输出比较生成PWM波形、脉冲信号等,根据比较结果控制输出通道

4.2 时钟源

2.4.1 时钟源框图

4.2.2 时钟源简介

通用定时器的计数器时钟(CK_CNT)可以来自多种来源,主要包括:

编号时钟来源简要说明
内部时钟(CK_INT)来自外设总线(APB)提供的时钟信号,最常见的定时模式
内部触发输入(ITRx)用于与芯片内其他通用定时器、高级定时器进行级联触发,常用于同步控制
外部时钟模式1(TIx)通过输入捕获通道(通道1或通道2)接收外部信号作为计数时钟(例如脉冲计数)
外部时钟模式2(ETR)通过外部触发输入引脚(TIMx_ETR)接收外部时钟信号,用于更灵活的时钟控制场合

4.2.3 时钟源控制寄存器

时钟源的控制寄存器是使用从模式寄存器 TIMxSMCR

时钟源类型配置方法
内部时钟(CK_INT)设置 TIMx_SMCR 的 SMS = 000 且 ECE = 0
内部触发输入(ITRx)设置 TIMx_SMCR 的 TS 选择对应 ITRx
外部时钟模式1(外部引脚TIx)设置 TIMx_SMCR 的 SMS = 111
外部时钟模式2(外部触发ETR)设置 TIMx_SMCR 的 ECE = 1

1、内部时钟

如果要使用内部时钟,配置步骤如下:

  • SMS 位设为 000 —— 禁止从模式,使用内部时钟。

  • ECE 位设为 0 —— 禁止外部时钟模式2。

⚡ 这时计数器时钟由内部APB总线提供,常规的定时/计数应用就是这种模式。

如果要使用内部时钟,这需要将SMS 位 设置位 000 禁止从模式,然后需要将ECE位设置为0 禁止外部时钟模式2。

2、 外部时钟模式1(外部引脚TIx)

3、外部时钟模式2(外部触发ETR)

2.2.4 外部时钟模式1

1、配置流程

例如,要使递增计数器在 TI2 输入出现上升沿时计数,请执行以下步骤:

1.通过在 TIMx_CCMR1 寄存器中写入 CC2S=“01”来配置通道 2,使其能够检测 TI2 输入的上升沿。

2.通过在 TIMx_CCMR1 寄存器中写入 IC2F[3:0] 位来配置输入滤波时间(如果不需要任何滤波,请保持 IC2F=0000)。

注意: 由于捕获预分频器不用于触发操作,因此无需对其进行配置。

3.通过在 TIMx_CCER 寄存器中写入 CC2P=0 和 CC2NP=0 来选择上升沿极性。

4.通过在 TIMx_SMCR 寄存器中写入 SMS=111,使定时器在外部时钟模式 1 下工作。

5.通过在 TIMx_SMCR 寄存器中写入 TS=110 来选择 TI2 作为输入源。

6.通过在 TIMx_CR1 寄存器中写入 CEN=1 来使能计数器。

当 TI2 出现上升沿时,计数器便会计数一次并且 TIF 标志置 1。

TI2 的上升沿与实际计数器时钟之间的延迟是由于 TI2 输入的重新同步电路引起的。

2、输入捕获过滤寄存器

滤波器原理如下

滤波规则

  • 例如N=4

    • 必须连续4次采样结果一致,才认为一次有效跳变

    • 如果采样过程中出现了不一致,计数器会清零重来

滤波器示意图

在图中可以看到:

  • 第一次小抖动(只持续了两三个采样周期)——被滤掉,不算跳变。

  • 只有稳定连续超过4次采样结果一致的变化,才被视为真正跳变。

3、边沿检测方式

选择检测上升沿下降沿或者双边沿

4、触发方式选择

选择用于触发同步计数的源,常用TI1、TI2等外部输入。

5、从模式选择

从模式选择(SMS) 中设置:

2.2.5 外部时钟模式2

2.2.6 定时器级联

10s

TIMx_CR2寄存器 MMS位

内部触发选择表

4.3 通用定时器-输出比较

4.3.1 框图介绍

1、总框图

2、捕获/比较通道电路

3、输出比较

4.3.2 PWM原理

1、基本理解

想象一下,PWM(脉冲宽度调制)其实就是反复地"开关"一个引脚。
而我们要做的,就是控制开关多久开,多久关,形成有规律的脉冲信号。

这就是靠定时器里的两个寄存器来做到的:

  • ARR(自动重装载寄存器):控制多长时间算一圈

  • CCRx(比较寄存器):控制什么时候开始输出高电平

2、简单规则

定时器内部有个小计数器 CNT,从0开始不断加。

在计数过程中:

  • CNT < CCRx,IO输出 0(低电平)

  • CNT >= CCRx,IO输出 1(高电平)

计到 ARR,就重新归零,重新开始一轮。

3、通俗举例

假设:

  • ARR = 100,一圈数到100结束。

  • CCRx = 30,当数到30时IO引脚变成高电平。

那么:

  • 从 0 数到 29 —— IO引脚是 低电平。

  • 从 30 数到 100 —— IO引脚是 高电平。

  • 然后又从0重新开始,周而复始。

这样就形成了一个PWM波,
开一段时间,关一段时间,循环进行。

  • 周期时间ARR 决定(也就是一圈的长度)

  • 占空比(高电平占的比例)由 CCRx 决定

看一张图就明白了

  • 黑色线是CNT计数器的值。

  • 蓝色线是IO口的输出电平变化。

4.3.3 PWM模式

1、PWM模式介绍

在STM32定时器中,PWM模式控制着当计数器 CNT 与捕获比较寄存器 CCRx 比较时,输出引脚的电平变化。

根据模式不同,分为两种基本PWM输出模式:

模式条件IO输出逻辑
PWM模式1CNT < CCRx输出有效电平
CNT ≥ CCRx输出无效电平
PWM模式2CNT < CCRx输出无效电平
CNT ≥ CCRx输出有效电平

2、递增计数和递减计数下的行为

模式递增计数(CNT上升)递减计数(CNT下降)
PWM模式1CNT < CCRx:有效电平CNT ≥ CCRx:无效电平CNT > CCRx:无效电平CNT ≤ CCRx:有效电平
PWM模式2CNT < CCRx:无效电平CNT ≥ CCRx:有效电平CNT > CCRx:有效电平CNT ≤ CCRx:无效电平

3、有效电平与无效电平的定义

  • 有效/无效由 TIMx_CCER 寄存器中的 CCxP 位控制:

    • CCxP=0 :OC通道输出高电平为有效电平

    • CCxP=1 :OC通道输出低电平为有效电平

✅ 注意:有时为了兼容不同的负载特性(比如高电平点亮,低电平点亮)需要调整 CCxP。

4、图解:PWM模式波形

|PWM模式1|PWM模式2|
|:--|:--|:--|
|递增计数|从低电平跳到高电平|从高电平跳到低电平|
|递减计数|从高电平跳到低电平|从低电平跳到高电平|

通过控制 ARR 和 CCRx 的设置,可以灵活调整:

  • PWM周期(由 ARR 控制)

  • PWM占空比(由 CCRx 控制)

  • PWM模式1:适合普通脉冲输出,大部分应用场景。

  • PWM模式2:适合需要反相输出的场景。

  • 有效电平由CCxP位配置,要根据具体外设(比如LED、蜂鸣器)需求设置高/低电平有效。

PWM模式1:
递增:CNT < CCRx,输出有效电平
CNT >= CCRx,输出无效电平
递减:CNT > CCRx,输出无效电平
CNT <= CCRx,输出有效电平
PWM模式2:
递增:CNT < CCRx,输出无效电平
CNT >= CCRx,输出有效电平
递减:CNT > CCRx,输出有效电平
CNT <= CCRx,输出无效电平

有/无效状态由TIMx_CCER决定
CCxP=0:OCx高电平有效
CCxP=1:Ocx低电平有效

4.3.4 标准库

输出比较功能,STM32官方固件库提供了非常便捷的标准函数结构体,可以快速完成通道的初始化配置。

1、库函数

/**
 * @brief 初始化输出比较通道(PWM输出)
 * @param TIMx 要配置的定时器
 * @param TIM_OCInitStruct 配置结构体
 * @note 通常需要搭配通道使能函数 TIM_Cmd 或 TIM_OCxPreloadConfig 一起使用
 */
void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC3Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC4Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);

📖 注意:

  • TIM_OC1Init 对应 CH1 通道

  • TIM_OC2Init 对应 CH2 通道

  • TIM_OC3Init 对应 CH3 通道

  • TIM_OC4Init 对应 CH4 通道

其他常用辅助函数:

/**
 * @brief 使能/失能通道输出功能
 */
void TIM_CCxCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, FunctionalState NewState);

/**
 * @brief 使能/失能通道预装载功能(推荐开启)
 */
void TIM_OC1PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
void TIM_OC2PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
void TIM_OC3PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
void TIM_OC4PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);

2、结构体

标准库使用以下结构体来配置输出比较参数:

typedef struct
{
    uint16_t TIM_OCMode;      // OC通道模式选择(PWM模式1 / PWM模式2 / Timing / Toggle)
    uint16_t TIM_OutputState; // 输出使能(ENABLE / DISABLE)
    uint16_t TIM_Pulse;       // CCRx捕获比较寄存器的值(决定PWM占空比)
    uint16_t TIM_OCPolarity;  // 输出极性(高电平有效/低电平有效)
} TIM_OCInitTypeDef;

各字段含义简明解释:

字段作用常用设置示例
TIM_OCMode选择输出模式(PWM1/PWM2)TIM_OCMode_PWM1
TIM_OutputState使能或关闭通道输出ENABLE
TIM_Pulse设置比较寄存器初值1000(比如1ms高电平)
TIM_OCPolarity设置输出极性(高电平/低电平有效)TIM_OCPolarity_High

4.3.5 PWM频率与占空比总结

1. PWM频率计算公式

PWM信号频率由以下公式决定:

$$
f_{PWM} = \frac{f_{APB1 \times 2}}{(PSC + 1) \times (ARR + 1)}
$$

符号含义
$f_{PWM}$最终输出的PWM信号频率
$f_{APB1}$APB1总线时钟频率(定时器时钟为APB1频率×2)
$PSC$预分频器寄存器值
$ARR$自动重装载寄存器值

注意:

  • TIM2 ~ TIM5挂在APB1总线上,若APB1=42MHz,则TIM2定时器实际时钟频率是 84MHz。

  • 所以要使用APB1频率 × 2进行计算。

2. 占空比控制

PWM的占空比表示一个周期中高电平所占的比例。

占空比计算公式:
$$
D{uty}% = \frac{CCR}{ARR + 1} \times 100%
$$

符号含义
CCR捕获/比较寄存器值
ARR自动重装载寄存器值

3. PWM设置小技巧

要实现的目标设置思路
降低PWM频率增大PSC或者ARR
增加PWM频率减小PSC或者ARR
占空比增加增大CCR值
占空比减小减小CCR值

4. 示例快速参考表

目标频率APB1=42MHz条件下的配置示例
1KHzPSC=83,ARR=999
10KHzPSC=83,ARR=99
100HzPSC=839,ARR=999
50HzPSC=839,ARR=1999

✅ 计算方式示例(以1KHz为例):

$$
f_{PWM} = \frac{84,\text{MHz}}{(83+1)\times(999+1)} = 1,\text{KHz}
$$

4.4 通用定时器-输出比较

4.5 通用定时器实验:PWM控制蜂鸣器

4.5.1 实验目标

  • 使用通用定时器 TIM2_CH1(PA15) 输出 PWM波

  • 调整PWM占空比,实现蜂鸣器呼吸灯效果。

  • 熟悉TIM2定时器PWM输出的初始化流程。

4.5.2 硬件连接

硬件模块连接端口
蜂鸣器PA15(TIM2_CH1)

4.5.3 实验原理

  • TIM2定时器通过PWM模式输出控制IO电平变化。

  • 通过占空比的增减,实现蜂鸣器声音的由强到弱的循环变化,形成呼吸效果。

4.5.4 配置流程

  1. 开启 GPIOA 和 TIM2 时钟。

  2. 配置 PA15 引脚为复用功能(AF)。

  3. 配置 TIM2 的基本计数器参数(计数模式、分频、周期等)。

  4. 配置 TIM2 的 PWM输出参数(PWM模式、初始占空比等)。

  5. 使能 TIM2,开始输出PWM波。

4.5.5 代码实现

1、 Bsp_BeepPwm.h

#ifndef __BSP_BEEP_PWM_H
#define __BSP_BEEP_PWM_H

#include "stm32f4xx.h"

// 初始化PWM
void BEEP_PWM_Init(uint16_t arr, uint16_t psc);

// 设置占空比
void BEEP_PWM_SetDuty(uint16_t duty);

#endif

2、 Bsp_BeepPwm.c

#include "Bsp_BeepPwm.h"

/**
 * @brief 蜂鸣器PWM初始化(TIM2_CH1 -> PA15)
 * @param arr PWM周期自动重装载值
 * @param psc 预分频值
 */
void BEEP_PWM_Init(uint16_t arr, uint16_t psc)
{
    // 1. 定义结构体
    GPIO_InitTypeDef GPIO_InitStruct;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
    TIM_OCInitTypeDef TIM_OCInitStruct;

    // 2. 开启GPIOA和TIM2时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    // 3. 配置PA15为复用功能TIM2_CH1
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource15, GPIO_AF_TIM2);

    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_15;           // 选择PA15引脚
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;         // 设置为复用模式
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;   // 输出速度100MHz
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;       // 推挽输出
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;         // 上拉
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 4. 配置TIM2基本参数
    TIM_TimeBaseStruct.TIM_Period = arr;              // 自动重装载值(决定PWM周期)
    TIM_TimeBaseStruct.TIM_Prescaler = psc;           // 预分频器(决定计数频率)
    TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频,不分频
    TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStruct);

    // 5. 配置TIM2的PWM输出模式
    TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;    // PWM模式1
    TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; // 开启输出
    TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;     // 有效电平为高电平
    TIM_OCInitStruct.TIM_Pulse = 0;                   // 初始脉冲宽度(占空比0%)
    TIM_OC1Init(TIM2, &TIM_OCInitStruct);

    // 6. 允许捕获比较寄存器预装载
    TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);

    // 7. 允许ARR寄存器预装载
    TIM_ARRPreloadConfig(TIM2, ENABLE);

    // 8. 启动TIM2
    TIM_Cmd(TIM2, ENABLE);
}

/**
 * @brief 设置PWM占空比
 * @param duty 占空比值,范围 0~ARR
 */
void BEEP_PWM_SetDuty(uint16_t duty)
{
    TIM_SetCompare1(TIM2, duty);
}

3、 主程序 main.c

#include "stm32f4xx.h"
#include "Bsp_BeepPwm.h"
#include "Delay.h"  // 延时库

int main(void)
{
    uint16_t duty = 0;
    uint8_t dir = 0; // 0-增加,1-减少

    Delay_Init(168);                  // 延时初始化(168MHz系统时钟)
    BEEP_PWM_Init(999, 83);            // PWM初始化(1kHz频率)

    while (1)
    {
        Delay_ms(10); // 每10ms更新一次,占空比慢慢变化

        if (dir == 0)
        {
            duty++;
            if (duty >= 999)
                dir = 1; // 达到最大,占空比开始减少
        }
        else
        {
            duty--;
            if (duty == 0)
                dir = 0; // 回到0,占空比开始增加
        }

        BEEP_PWM_SetDuty(duty); // 更新PWM占空比
    }
}

注意事项

  • PA15默认复用为JTAG,需要关闭JTAG或配置好引脚复用,否则无法正常使用。

  • PWM频率=主频/((PSC+1)*(ARR+1)),可以通过调整ARR和PSC控制频率。

  • 呼吸效果的快慢可以通过Delay_ms调整。

⚡ 程序说明

  • 频率设置:TIM2计数频率 = 84MHz / (83+1) = 1MHz
    PWM频率 = 1MHz / (999+1) = 1kHz

  • 呼吸效果:占空比从0%逐渐增加到100%,再回落到0%。

  • 蜂鸣器效果:声音从弱到强,再从强到弱,循环。

4.6 通用定时器实验:舵机控制

4.6.1 实验目标

  • 掌握定时器输出PWM脉冲信号的方法。

  • 学会通过调整PWM脉宽控制舵机角度。

  • 实现通过程序控制舵机从0°到180°平滑转动。

4.6.2 硬件连接

信号线接线说明
PWM控制线连接到 STM32F407 的 PC7(TIM3通道2)
电源线(红色)连接到 +5V 电源
地线(黑色)连接到 GND

注意:舵机一般需要外接5V供电,确保供电电流足够,否则容易导致舵机抖动或复位。

4.6.3 舵机控制原理

1、什么是舵机

舵机(Servo)是一种可以精确控制转动角度的电机。
广泛应用于:

  • 机械臂

  • RC遥控模型

  • 智能机器人

舵机内部通常包含:

  • 小型电机

  • 减速齿轮组

  • 位置传感器(电位器)

  • 控制电路

2、舵机信号要求(PWM信号)

  • 舵机通过识别输入的PWM信号脉宽,来调整自身的角度位置。

  • 固定PWM周期20ms,脉冲宽度控制角度变化。

  • 在STM32中,使用定时器的PWM输出功能来产生精确的控制脉冲。

角度PWM脉冲宽度
0.5ms
90°1.5ms
180°2.5ms

20ms 占空比 高电平 0.5ms
简单理解

  • 脉冲越短 ➔ 舵机角度越小

  • 脉冲越长 ➔ 舵机角度越大

3、控制思路

我们通过STM32的定时器PWM功能来输出这种特殊的控制脉冲。

核心步骤:

  1. 配置一个定时器(比如TIM3)

  2. 设置定时器频率 = 50Hz(周期20ms)

  3. 通过修改比较寄存器(CCR)来调整PWM脉冲宽度(0.5ms~2.5ms)

  4. 舵机根据脉冲宽度,旋转到对应的角度

4、计算公式

假设APB1时钟 = 42MHz,TIM3时钟 = 84MHz:

  • 预分频器 Prescaler = 83

    • 84MHz ÷ (83+1) = 1MHz
  • 自动重装载 ARR = 19999

    • 1MHz ÷ (19999+1) = 50Hz
  • PWM脉冲宽度

    • 500us = 500计数

    • 1500us = 1500计数

    • 2500us = 2500计数

角度与脉冲宽度换算公式

$$
\text{Pulse} = 500 + \left( \frac{2000 \times \text{Angle}}{180} \right)
$$

其中:

  • Pulse:对应PWM脉冲宽度(单位:微秒)

  • Angle:要转动的角度(0°~180°)

4.6.4 示例代码

1、PWM初始化配置

/**
 * @brief TIM3通道2 PWM初始化(舵机控制用)
 */
void TIM3_CH2_PWM_Init(uint16_t arr, uint16_t psc)
{
    // 1. 开启时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);

    // 2. 配置PC7复用为TIM3_CH2
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOC, &GPIO_InitStruct);

    // 连接复用功能
    GPIO_PinAFConfig(GPIOC, GPIO_PinSource7, GPIO_AF_TIM3);

    // 3. 定时器基础配置
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
    TIM_TimeBaseStruct.TIM_Period = arr;
    TIM_TimeBaseStruct.TIM_Prescaler = psc;
    TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStruct);

    // 4. PWM输出配置
    TIM_OCInitTypeDef TIM_OCInitStruct;
    TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;    // PWM模式1
    TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStruct.TIM_Pulse = 1500;                // 默认脉宽1.5ms
    TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC2Init(TIM3, &TIM_OCInitStruct);

    // 5. 使能定时器
    TIM_Cmd(TIM3, ENABLE);
}

2、舵机角度控制函数

/**
 * @brief 设置舵机角度
 * @param angle 角度值(0°~180°)
 */
void Servo_Set_Angle(uint8_t angle)
{
    uint16_t pulse;

    if (angle > 180) angle = 180; // 限制最大角度
    pulse = 500 + (uint32_t)(2000 * angle) / 180;

    TIM_SetCompare2(TIM3, pulse); // 设置PWM脉宽
}

3、主函数测试

int main(void)
{
    // 初始化基础外设
    Main_Init();

    // 初始化舵机PWM
    TIM3_CH2_PWM_Init(19999, 83); // 50Hz,20ms周期

    while (1)
    {
        for (uint8_t angle = 0; angle <= 180; angle += 10)
        {
            Servo_Set_Angle(angle);
            Delay_ms(500); // 0.5秒移动一次
        }

        for (int8_t angle = 180; angle >= 0; angle -= 10)
        {
            Servo_Set_Angle(angle);
            Delay_ms(500); // 0.5秒移动一次
        }
    }
}

4.6 通用定时器实验:输入捕获

开关计数

编码器

第五章 高级定时器

第六章 独立看门狗

第七章 窗口看门狗