
09_STM32_定时篇
第一章 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 配置流程
- 开启PWR模块时钟,允许访问备份域。
- 启动外部低速晶振(LSE),并等待其稳定。
- 选择LSE作为RTC模块的时钟源。
- 使能RTC模块。
- 等待RTC寄存器同步完成。
- 检查备份寄存器:
- 如果未初始化,配置RTC参数(24小时制、异步预分频、同步预分频)。
- 设置默认初始时间(如22:19:10)。
- 写入初始化标志位到备份寄存器。
- 设置当前时间或读取当前时间,使用串口打印输出。
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 配置流程
- 调用时间初始化函数,确保RTC正常运行。
- 配置RTC闹钟:
- 读取当前时间。
- 设定闹钟时间(当前秒数+5秒,或者指定具体时间)。
- 配置闹钟掩码(通常不比较日期,只比较时间)。
- 开启闹钟A功能。
- 配置RTC闹钟中断:
- 开启闹钟中断。
- 配置EXTI Line17连接RTC闹钟事件,触发方式为上升沿。
- 配置NVIC,设置RTC闹钟中断优先级。
- 编写闹钟中断服务函数:
- 判断中断标志。
- 清除中断挂起位。
- 执行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_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 配置流程
- 禁止唤醒定时器,以便重新配置。
- 配置唤醒定时器时钟源:
- 选择 RTC 1Hz 分频后的时钟(RTC_WakeUpClock_RTCCLK_Div16)。
- 设置唤醒定时器计数值,使得中断周期为1秒。
- 开启RTC唤醒定时器中断功能。
- 配置EXTI Line22连接到唤醒事件:
- 上升沿触发。
- 使能EXTI线路。
- 配置NVIC,设置RTC唤醒定时器中断优先级。
- 重新使能RTC唤醒定时器,开始计时。
- 编写秒中断服务函数:
- 判断中断标志。
- 清除中断挂起位。
- 翻转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内核提供)
本章重点讨论常规定时器(基本、通用、高级定时器)。

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 计数器
定时器内部集成了一个预分频器(PSC),
用于将输入时钟信号进行分频,降低计数器的计数速度,从而调整定时器的定时周期。
预分频器基本原理:
- 预分频器可以设定一个分频系数 N。
- 定时器实际工作频率 = 输入时钟频率 ÷ (N+1)。
举例说明:
系统输入时钟 | 预分频器设置值 | 定时器计数频率 |
---|---|---|
36 MHz | 35 | 1 MHz |
72 MHz | 7199 | 10 KHz |
通过设置预分频器,可以方便地控制计时器的计数周期,从而实现不同时间长度的定时任务。
2.3.3 分频器
计数器是定时器的核心组成部分。
在STM32F407中,常见定时器计数器位宽有:
- 16位计数器(范围0~65535)
- 32位计数器(范围0~4294967295,如TIM2、TIM5)
计数器的工作原理:
- 接收预分频器输出的时钟脉冲。
- 每来一个脉冲,计数器按模式递增或递减。
- 当达到特定值(如ARR自动重载寄存器的值)时产生更新事件。
💡 计数器的工作模式有多种(后面章节详细讲解):
- 向上计数模式
- 向下计数模式
- 中心对齐模式(先上升后下降)
2.3.4 整体结构
定时器的基本整体结构可以理解为:
时钟源 ➔ 预分频器(PSC) ➔ 计数器(CNT) ➔ 自动重装载寄存器(ARR)。
-
时钟源(Ft)
提供给定时器的基础脉冲信号。 -
预分频器(PSC)
将输入时钟Ft降低频率,输出更慢的时钟信号给计数器。 -
计数器(CNT)
按照分频后时钟脉冲递增或递减计数。 -
自动重装载寄存器(ARR)
设定计数器的最大值或最小值,计数到达后发生更新事件(溢出或下溢)。
2.3.5 时间计算公式
定时器产生一次更新事件(溢出/下溢)的时间周期,可以用下面的公式计算:
$$
FtT_{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输出
- 通常用于定时中断、时间延迟管理等场景
💡 典型代表:TIM6 和 TIM7
3.1.2 基本定时器的特性
特性 | 说明 |
---|---|
计数方式 | 支持向上计数、向下计数 |
位宽 | 16位计数器(范围0~65535) |
时钟源 | 由APB1总线或APB2总线提供时钟 |
预分频器 | 支持灵活设置,调整计数频率 |
中断功能 | 支持更新事件(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)
位 | 名称 | 功能描述 |
---|---|---|
0 | CEN | 计数器使能(0=停止,1=启动) |
1 | UDIS | 更新事件禁止(0=使能,1=禁止) |
2 | URS | 更新请求源选择(0=全部源,1=仅溢出) |
3 | OPM | 单脉冲模式(0=连续计数,1=单次停止) |
7 | ARPE | 自动重载预装载使能(0=无缓冲,1=缓冲ARR) |
-
CEN(计数器启动/停止)
置1启动计数器,置0停止计数。 -
UDIS(禁止更新事件)
置1禁止自动产生更新事件(UEV)。 -
URS(更新请求源选择)
控制哪些事件能触发更新中断或DMA请求。 -
OPM(单脉冲模式)
配置成只计数到一次溢出就停止。 -
ARPE(自动重载缓冲)
使ARR寄存器的写入延迟到更新事件时生效,提高稳定性。
3.3.2 TIM6 和 TIM7 DMA/中断使能寄存器 (TIMx_DIER)
位 | 名称 | 功能描述 |
---|---|---|
8 | UDE | 更新DMA请求使能 |
0 | UIE | 更新中断使能 |
-
UIE(更新中断使能)
置1后,当计数器溢出(或触发更新事件)时,产生中断。 -
UDE(更新DMA请求使能)
置1后,更新事件可触发DMA搬运数据。
3.3.3 TIM6 和 TIM7 状态寄存器 (TIMx_SR)
位 | 名称 | 功能描述 |
---|---|---|
0 | UIF | 更新中断标志位 |
常用位简述
- UIF(更新中断标志)
- 1:计数器溢出或更新事件发生。
- 0:无事件发生。
软件读取后需手动清除此位,或通过自动清除机制清除。
3.3.4 TIM6 和 TIM7 事件生成寄存器 (TIMx_EGR)
位 | 名称 | 功能描述 |
---|---|---|
0 | UG | 软件触发更新事件 |
- UG(更新生成)
写1时立即触发一次更新事件(强制重新载入PSC/ARR等)。
3.3.4 TIM6 和 TIM7 计数器 (TIMx_CNT)
位 | 名称 | 功能描述 |
---|---|---|
15:0 | CNT[15:0] | 当前计数器数值 |
常用简述
- 用于实时读取当前计数值,或软件直接写入新值进行重置。
3.3.5 TIM6 和 TIM7 预分频器 (TIMx_PSC)
位 | 名称 | 功能描述 |
---|---|---|
15:0 | PSC[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:0 | ARR[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; // 预分频器
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 程序流程
-
初始化TIM6定时器,使其每2ms中断一次,用于刷新数码管。
-
定义数码管段码缓存区,并设计位切换机制(动态扫描)。
-
在TIM6中断服务函数中:
-
如果选择时间显示模式,读取RTC当前时间。
-
秒变化时,更新显示内容(分钟*100 + 秒)。
-
每次中断刷新一位数码管,实现动态显示。
-
如果选择数值模式,直接显示固定数字。
-
-
提供接口函数,支持外部切换显示数值或模式。
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 配置流程
-
初始化基本定时器(TIM6):
-
设置定时器周期,配置为10ms中断一次(用于定时扫描按键)。
-
开启定时器更新中断功能。
-
配置NVIC中断优先级,打开定时器中断。
-
-
配置按键GPIO输入引脚:
-
设置按键对应的GPIO端口为输入模式。
-
配置上拉(如有必要)以保证空闲时为高电平。
-
-
设计按键状态机逻辑:
-
定义按键的4种状态:
松开(KEY_IDLE)
、按下检测(KEY_PRESS)
、按住(KEY_HOLD)
、松开检测(KEY_RELEASE)
。 -
在定时器中断服务函数中按周期扫描按键状态,并更新状态机状态。
-
-
编写短按/长按识别规则:
-
短按:按下时间小于设定阈值(如2秒)。
-
长按:按下时间大于设定阈值(如2秒)。
-
-
在状态机中加入打印提示:
-
短按识别后串口打印“短按”。
-
长按识别后串口打印“长按”。
-
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 程序流程
-
初始化定时器,配置为10ms中断一次,用于定时扫描按键。
-
定义按键状态机(IDLE、PRESS、HOLD、RELEASE)。
-
每次定时器中断时:
-
扫描按键电平变化。
-
按照状态机流程管理按键状态转移。
-
确认按下后蜂鸣器短响提示。
-
检测长按(2秒)时蜂鸣器再次长响提示。
-
松开后根据按下时间判断是短按还是长按,并打印结果。
-
-
蜂鸣器响动时间自动管理,时间到后自动关闭。
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;
}
}
}
}
}
第四章 通用定时器
4.1 通用定时器概述
4.1.1 什么是通用定时器
4.1.2 通用定时器的特性
4.1.3 通用定时器与基本定时器的区别
4.1.4 通用定时器应用场景
4.2 通用定时器结构框图
4.2.1 总体框图解析
4.2.2 各功能单元作用
1、时基单元
2、输入捕获单元
3、输出比较单元
4、PWM输出单元
5、主从同步单元
6、触发与中断管理单元
4.3 通用定时器核心原理
4.3.1 时钟源与分频
4.3.2 计数器与计数模式
4.3.3 捕获与比较功能
4.3.4 PWM模式与输出波形
4.3.5 通用定时器与外部触发(ETR)
4.3.6 中断和事件产生机制
4.4 通用定时器标准库
4.4.1 通用定时器初始化函数
4.4.2 输入捕获配置函数
4.4.3 输出比较配置函数
4.4.4 PWM生成函数
4.4.5 常用结构体介绍
(如 TIM_OCInitTypeDef
、TIM_ICInitTypeDef
)
4.4.6 常用宏与中断标志位
4.5 通用定时器实验
4.5.1 基础实验:通用定时器中断实验
实现一个简单定时中断翻转LED
4.5.2 捕获实验:输入捕获脉宽测量
测量外部脉冲信号的周期与高电平宽度
4.5.3 输出比较实验:定时输出高低电平
实现指定时间点输出变化
4.5.4 PWM输出实验:可调占空比PWM波形生成
控制LED亮度或电机转速
第五章 高级定时器(暂时不讲)
第六章 独立看门狗
第七章 窗口看门狗
- 感谢你赐予我前进的力量