
09_STM32_定时篇
本文最后更新于 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 配置流程
- 开启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_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 配置流程
- 禁止唤醒定时器,以便重新配置。
- 配置唤醒定时器时钟源:
- 选择 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 计数器
CNT
计数器是定时器的核心组成部分。
在STM32F407中,常见定时器计数器位宽有:
- 16位计数器(范围0~65535)
- 32位计数器(范围0~4294967295,如TIM2、TIM5)
计数器的工作原理:
- 接收预分频器输出的时钟脉冲。
- 每来一个脉冲,计数器按模式递增或递减。
- 当达到特定值(如ARR自动重载寄存器的值)时产生更新事件。
💡 计数器的工作模式有多种(后面章节详细讲解):
- 向上计数模式
- 向下计数模式
- 中心对齐模式(先上升后下降)
2.3.3 分频器
定时器内部集成了一个预分频器(PSC),
用于将输入时钟信号进行分频,降低计数器的计数速度,从而调整定时器的定时周期。
预分频器基本原理:
- 预分频器可以设定一个分频系数 N。
- 定时器实际工作频率 = 输入时钟频率 ÷ (N+1)。
举例说明:
系统输入时钟 | 预分频器设置值 | 定时器计数频率 |
---|---|---|
36 MHz | 35 | 1 MHz |
72 MHz | 7199 | 10 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输出
- 通常用于定时中断、时间延迟管理等场景
💡 典型代表:TIM6 和 TIM7
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)
位 | 名称 | 功能描述 |
---|---|---|
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; // 预分频器 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 程序流程
-
初始化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;
}
}
}
}
}
通过长按 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输出 | 支持编码器模式 | 特点或限制 |
---|---|---|---|---|---|---|---|
TIM2 | 32位 | 4通道 | 向上/向下/中心对齐 | 支持 | 支持 | 支持 | 通用定时器中功能最全,32位宽 |
TIM3 | 16位 | 4通道 | 向上/向下/中心对齐 | 支持 | 支持 | 支持 | 功能丰富,常用 |
TIM4 | 16位 | 4通道 | 向上/向下/中心对齐 | 支持 | 支持 | 支持 | 常用 |
TIM5 | 32位 | 4通道 | 向上/向下/中心对齐 | 支持 | 支持 | 支持 | 32位宽,更适合长时间测量 |
TIM9 | 16位 | 2通道 | 仅支持向上计数 | 支持 | 支持 | 不支持 | 通道少,仅2个,简化版 |
TIM10 | 16位 | 1通道 | 仅支持向上计数 | 支持 | 支持 | 不支持 | 资源少,适合小功能 |
TIM11 | 16位 | 1通道 | 仅支持向上计数 | 支持 | 支持 | 不支持 | 用于简单定时 |
TIM12 | 16位 | 2通道 | 仅支持向上计数 | 支持 | 支持 | 不支持 | 比较常用于简单PWM输出 |
TIM13 | 16位 | 1通道 | 仅支持向上计数 | 支持 | 支持 | 不支持 | 简单应用 |
TIM14 | 16位 | 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模式1 | CNT < CCRx | 输出有效电平 |
CNT ≥ CCRx | 输出无效电平 | |
PWM模式2 | CNT < CCRx | 输出无效电平 |
CNT ≥ CCRx | 输出有效电平 |
2、递增计数和递减计数下的行为
模式 | 递增计数(CNT上升) | 递减计数(CNT下降) |
---|---|---|
PWM模式1 | CNT < CCRx:有效电平CNT ≥ CCRx:无效电平 | CNT > CCRx:无效电平CNT ≤ CCRx:有效电平 |
PWM模式2 | CNT < 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条件下的配置示例 |
---|---|
1KHz | PSC=83,ARR=999 |
10KHz | PSC=83,ARR=99 |
100Hz | PSC=839,ARR=999 |
50Hz | PSC=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 配置流程
-
开启 GPIOA 和 TIM2 时钟。
-
配置 PA15 引脚为复用功能(AF)。
-
配置 TIM2 的基本计数器参数(计数模式、分频、周期等)。
-
配置 TIM2 的 PWM输出参数(PWM模式、初始占空比等)。
-
使能 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° | 0.5ms |
90° | 1.5ms |
180° | 2.5ms |
20ms 占空比 高电平 0.5ms
简单理解:
-
脉冲越短 ➔ 舵机角度越小
-
脉冲越长 ➔ 舵机角度越大
3、控制思路
我们通过STM32的定时器PWM功能来输出这种特殊的控制脉冲。
核心步骤:
-
配置一个定时器(比如TIM3)
-
设置定时器频率 = 50Hz(周期20ms)
-
通过修改比较寄存器(CCR)来调整PWM脉冲宽度(0.5ms~2.5ms)
-
舵机根据脉冲宽度,旋转到对应的角度
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 通用定时器实验:输入捕获
开关计数
编码器
第五章 高级定时器
第六章 独立看门狗
第七章 窗口看门狗
- 感谢你赐予我前进的力量