1. 从“闹钟”到“心脏”:为什么STM32定时器是嵌入式开发的基石
如果你刚开始接触STM32,可能会觉得定时器(Timer)这个概念有点抽象,不就是个“闹钟”吗?设置一个时间,时间到了就提醒一下。但当你真正深入项目,无论是想用PWM驱动一个电机让它平稳转动,还是想精确测量一个脉冲的宽度,或者只是想让一个LED灯以固定的频率闪烁,你都会发现,那个看似简单的“闹钟”,其实是整个系统协调运行的“心脏”。它远不止于计时,更是实现精准控制、信号生成和事件同步的核心硬件单元。
我最初做项目时,就曾掉进过一个“定时器坑”。当时想用中断做一个非阻塞的延时,结果LED闪烁的频率总是不对,时快时慢。排查了半天,最后发现是定时器的时钟源没有配置正确,系统时钟(SYSCLK)经过分频后,给到定时器的实际时钟频率和我计算时假设的完全不一样。这个经历让我深刻体会到,不理解定时器,就谈不上精通STM32。它不像GPIO点灯那样直观,但其重要性在所有复杂外设中绝对排在前列。
网络上围绕STM32定时器的搜索热词非常集中:stm32定时器捕获测频率、stm32cubemx 定时器配置pwm、stm32 定时器精确定时、定时器从模式……这些恰恰是开发者们在实际工作中最常遇到的需求场景。无论是电机控制、电源管理还是通信同步,都离不开对定时器的深度驾驭。本文将从一个资深开发者的视角,彻底拆解STM32的定时器体系。我不会仅仅罗列寄存器,而是结合这些高频需求,带你理解其工作原理、设计逻辑,并分享那些数据手册上不会写的配置技巧和避坑指南。无论你是正在学习标准库还是HAL库,这篇文章都将为你构建一个清晰且实用的定时器知识框架。
2. 庖丁解牛:深入理解STM32定时器的家族图谱与核心框图
STM32的定时器并非单一外设,而是一个功能强大、种类繁多的家族。盲目地直接配置寄存器或使用CubeMX生成代码,很容易“用牛刀杀鸡”或“用小刀锯大树”。首先,我们必须像认识工具一样,了解手头有哪些“刀具”,以及它们各自最擅长的“工种”。
2.1 定时器家族分类:找到你的“那把刀”
STM32的定时器主要分为以下几类,其复杂度和功能依次增强:
基本定时器(Basic Timers):如TIM6, TIM7。这是功能最简单的定时器,只具备最基础的定时功能,即向上计数,产生更新事件(溢出中断)。它没有外部IO,不能用于输入捕获或输出比较。它的核心用途就是提供纯粹的时基,例如为DAC(数模转换器)提供触发信号,或者为操作系统提供系统节拍。如果你只需要一个简单的、周期性的中断源,基本定时器是最轻量、最节省资源的选择。
通用定时器(General-purpose Timers):如TIM2, TIM3, TIM4, TIM5(在部分系列中功能更强)。这是STM32定时器家族的“主力军”,也是我们最常打交道的类型。它包含了基本定时器的所有功能,并大幅扩展:
- 双向计数:支持向上、向下、中央对齐(向上向下)计数模式。
- 输入捕获:可以捕获外部引脚上信号的边沿,并记录下发生时的计数器值,用于测量脉冲宽度、频率等。
- 输出比较:可以比较计数器值与预设值,匹配时产生相应动作(如翻转引脚、产生脉冲、产生中断),这是生成PWM波的基础。
- PWM生成:通过输出比较模式,配合自动重载寄存器,可以轻松生成频率和占空比可调的PWM信号。
- 编码器接口:部分通用定时器可以直接连接正交编码器,自动解析电机的位置和速度信号。
高级控制定时器(Advanced-control Timers):如TIM1, TIM8。这是功能最强大的定时器,在通用定时器的基础上,增加了用于电机控制和数字电源的“高级货”:
- 互补输出:可以输出带死区插入的互补PWM对,这是驱动三相全桥电路(如BLDC电机、逆变器)的必备功能,能有效防止上下桥臂直通短路。
- 刹车输入:紧急情况下,通过一个外部引脚信号可以快速关闭所有PWM输出,确保系统安全。
- 更灵活的输出模式。
选择定时器的原则是:用最合适的,而不是最强大的。驱动一个LED呼吸灯,用通用定时器的PWM功能足矣;而为BLDC电机设计FOC算法,则必须使用高级控制定时器来生成互补PWM。
2.2 核心框图解读:数据手册里的“藏宝图”
定时器的参考手册里都会有一张复杂的框图,很多新手会直接跳过。但在我看来,这张图是理解定时器如何工作的“藏宝图”。我们以通用定时器为例,拆解几个最关键的部分:
时钟源:这是定时器计数的“心跳”。它可以是内部时钟(CK_INT),即来自APB总线(如APB1、APB2)的时钟。这里有一个关键细节:当APB预分频器不为1时,定时器时钟会是APB时钟的2倍。例如,AHB=72MHz,APB1预分频器设为2,则APB1时钟为36MHz,但连接到APB1的定时器(如TIM2-4)的时钟源会是72MHz。这是很多初学者计算定时不准的第一个坑。
- 其他时钟源还包括外部时钟模式1(从TIx引脚输入)、外部时钟模式2(ETR引脚)、内部触发输入(ITRx,用于定时器级联)。
时基单元:这是定时器的“核心引擎”,由预分频器(PSC)、计数器(CNT)和自动重载寄存器(ARR)构成。
- PSC:对时钟源进行分频,得到计数器实际递增的时钟频率
CK_CNT = CK_PSC / (PSC + 1)。PSC是一个16位寄存器,决定了定时器的分辨率。 - CNT:根据计数模式(向上、向下、中央对齐)在
0到ARR(或ARR到0)之间循环计数。 - ARR:决定了计数器的周期。当计数器达到这个值(向上计数)或回到0(向下计数)时,会产生更新事件(UEV),计数器重置,并可以触发更新中断。
定时周期
T = (ARR + 1) * (PSC + 1) / T_clk。这里的+1是因为寄存器值是从0开始计数的。例如,要产生1ms的中断(系统时钟72MHz),可以设PSC=7199,ARR=9。计算:T = (9+1)*(7199+1)/72,000,000 = 10*7200/72,000,000 = 0.001s。- PSC:对时钟源进行分频,得到计数器实际递增的时钟频率
输入捕获/输出比较通道:这是定时器与外部世界交互的“手脚”。
- 输出比较:通道寄存器(CCRx)存放一个比较值。计数器CNT不断与CCRx比较,根据设定的模式(冻结、匹配时置高、匹配时置低、翻转)来操作对应的输出引脚(OCx),这是产生PWM的硬件基础。
- 输入捕获:当对应的输入引脚(ICx)检测到设定的边沿(上升沿、下降沿)时,会将当前CNT的值锁存到捕获/比较寄存器(CCRx)中,并可以产生中断。通过记录两次捕获的值,就能算出脉冲宽度。
理解了这个框图,你就再也不会觉得配置定时器是一堆魔幻数字了。每一个配置项,都能在框图中找到它的位置和作用。
3. 实战精讲:三大核心应用场景的配置与避坑指南
理解了原理,我们进入实战。下面我将围绕三个最高频的搜索热词场景,详细讲解配置步骤、代码逻辑,并分享我踩过的坑。
3.1 场景一:精确的定时中断——系统的心跳
需求:不使用HAL_Delay这类阻塞函数,而是创建一个1ms的定时器中断,用于执行周期性的任务(如扫描按键、更新传感器数据)。
选型:使用一个通用定时器(如TIM2)即可。更简单的系统心跳可以使用SysTick(滴答定时器),但SysTick通常被HAL库或操作系统占用,且功能单一。使用独立定时器更灵活。
配置核心(以HAL库为例):
- 时钟配置:确保定时器所在APB总线时钟正确。通过
RCC相关函数或CubeMX配置。 - 时基初始化:
TIM_HandleTypeDef htim2; htim2.Instance = TIM2; htim2.Init.Prescaler = 7200 - 1; // 72MHz / 7200 = 10kHz htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 10 - 1; // 10kHz / 10 = 1kHz (1ms) htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; // 建议使能,防止更新时出现毛刺 HAL_TIM_Base_Init(&htim2); - 中断配置:
HAL_TIM_Base_Start_IT(&htim2); // 启动定时器并开启更新中断 - 编写中断回调函数:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { // 你的1ms任务在这里执行 // 例如:按键扫描、状态机推进 } }
避坑指南与心得:
- 坑1:中断服务函数(ISR)过长。1ms中断非常频繁,ISR必须短小精悍。只做标记(设置标志位),把实际处理逻辑放到主循环中。否则会阻塞其他同级或低级中断,导致系统响应异常。
- 坑2:
AutoReloadPreload的作用。这个配置项非常关键。如果不使能(DISABLE),当你修改ARR值时,新值会立即生效。如果此时计数器值刚好在旧ARR和新ARR之间,可能会立即触发一次意外的更新事件。如果使能(ENABLE),则新ARR值会先写入预装载寄存器,只在下次更新事件时才生效,保证了时序的稳定性。在需要动态改变定时间隔(如改变PWM频率)时,务必使能此功能。 - 心得:计算值的验证。配置完PSC和ARR后,不要完全相信自己的计算。可以用调试器读取
TIMx->CNT寄存器的变化速度,或者用PWM输出一个波形,用逻辑分析仪测量实际周期,进行双重验证。
3.2 场景二:PWM信号生成——驱动世界的脉搏
需求:在PA8引脚上生成一个频率为1kHz,占空比为30%的PWM波,用于控制LED亮度或电机速度。
选型:查看数据手册,找到TIM1的通道1对应PA8。我们使用高级控制定时器TIM1来演示,其配置对通用定时器同样具有参考价值。
配置核心:
- 时基配置:决定PWM的频率。
Fpwm = Fclk / ((PSC+1)*(ARR+1))。设Fclk=72MHz,要得到1kHz,我们可以令(PSC+1)*(ARR+1) = 72000。为了得到较好的占空比调节分辨率,我们让ARR尽量大一些。设PSC=0(不分频),则ARR = 72000-1 = 71999。但ARR是16位寄存器,最大值65535,此方案不行。重新设计:设PSC=72-1=71,则Fcnt = 72MHz / 72 = 1MHz。再令ARR = 1000-1 = 999,则Fpwm = 1MHz / 1000 = 1kHz。ARR为999,占空比分辨率约为0.1%。 - 输出比较配置:
TIM_OC_InitTypeDef sConfigOC = {0}; sConfigOC.OCMode = TIM_OCMODE_PWM1; // PWM模式1:CNT<CCR时有效电平 sConfigOC.Pulse = 300; // 占空比:CCR值。占空比 = Pulse / (ARR+1) = 300/1000 = 30% sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; // 有效电平为高 sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET; HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1); - 启动PWM:
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
动态调节占空比:
// 在程序运行中,可以通过修改CCR1寄存器来改变占空比 __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, new_pulse_value); // 或者使用HAL库函数 TIM_OC_InitTypeDef sConfigOC; sConfigOC.Pulse = new_pulse_value; HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);避坑指南与心得:
- 坑1:引脚复用功能未开启。除了配置定时器,必须确保GPIO引脚被正确初始化为复用推挽输出模式,并映射到对应的定时器通道上。在CubeMX中勾选对应功能会自动完成,但手写代码时容易遗漏。
GPIO_InitStruct.Pin = GPIO_PIN_8; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽输出 GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate = GPIO_AF1_TIM1; // 关键!指定复用功能为TIM1 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); - 坑2:高级控制定时器的“主输出使能”。对于TIM1/TIM8,配置好PWM后,还需要使能“刹车和死区寄存器(BDTR)”中的主输出使能(MOE)位,否则PWM输出会被禁止。这在驱动互补输出时是必须的,但在普通单通道输出时也建议开启。
__HAL_TIM_MOE_ENABLE(&htim1); // 或使用HAL库函数 HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1); // 对于互补通道 // 对于普通通道,在HAL_TIM_PWM_Start内部可能会处理,但最好显式调用以下函数确保 HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig); - 心得:PWM频率与分辨率权衡。PWM频率越高,电机或LED的响应越快,噪音可能越小(对于电机驱动,频率需高于人耳可闻范围,通常>20kHz)。但频率越高,在系统时钟固定的情况下,ARR值就越小,导致占空比调节的分辨率越低。例如,72MHz时钟,要产生20kHz的PWM,ARR最大只能为
72M/20k -1 = 3599,分辨率只有1/3600。你需要根据实际应用在频率和分辨率之间做出取舍。
3.3 场景三:输入捕获测量频率与脉宽——感知信号的脉搏
需求:测量输入到某个引脚上的方波信号的频率或高电平脉宽。
选型:使用通用定时器的输入捕获功能。例如,用TIM3的通道1(可能对应PA6)。
配置核心(以测量脉宽为例): 测量脉宽通常需要捕获上升沿和下降沿。有两种常用方法:
- 方法A:单通道两次捕获。将通道配置为在上升沿和下降沿都触发捕获。在上升沿中断中,记录捕获值并改变极性为下降沿;在下降沿中断中,再次记录捕获值,差值即为脉宽。此法节省定时器资源,但软件逻辑稍复杂,且高频率下可能丢失中断。
- 方法B:双通道联合捕获(推荐)。使用同一个定时器的两个通道(如CH1和CH2),分别设置为上升沿触发和下降沿触发,并配置为从模式:复位模式,将CH1设为触发源。这样,上升沿时计数器清零并开始计数,下降沿时捕获当前计数值,该值即为高电平脉宽。此法硬件自动完成,精度高,不占用过多CPU。
配置步骤(方法B,HAL库):
- 时基配置:根据信号频率范围设置合适的PSC,使计数器在最大脉宽内不会溢出。例如,测1ms内脉宽,时钟1MHz,则ARR需大于1000。
- 输入捕获通道1(上升沿):
TIM_IC_InitTypeDef sConfigIC; sConfigIC.ICPolarity = TIM_ICPOLARITY_RISING; // 上升沿 sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; // 直接映射到TI1 sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; // 不分频,每个边沿都捕获 sConfigIC.ICFilter = 0; // 滤波器值,根据信号噪声情况设置 HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1); - 输入捕获通道2(下降沿):
sConfigIC.ICPolarity = TIM_ICPOLARITY_FALLING; // 下降沿 sConfigIC.ICSelection = TIM_ICSELECTION_INDIRECTTI; // 间接映射,从TI1输入 HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_2); - 从模式配置:
TIM_SlaveConfigTypeDef sSlaveConfig; sSlaveConfig.SlaveMode = TIM_SLAVEMODE_RESET; // 复位模式 sSlaveConfig.InputTrigger = TIM_TS_TI1FP1; // 触发源为TI1的滤波后信号 sSlaveConfig.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING; // 上升沿触发复位 sSlaveConfig.TriggerFilter = 0; HAL_TIM_SlaveConfigSynchro(&htim3, &sSlaveConfig); - 启动捕获:
HAL_TIM_IC_Start(&htim3, TIM_CHANNEL_1); // 启动通道1捕获(用于触发复位) HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_2); // 启动通道2捕获并开中断 - 中断回调函数中读取脉宽:
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) { uint32_t pulse_width = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2); // pulse_width 即为高电平期间的计数值,根据时钟频率可换算为时间 // 注意:如果脉宽可能超过ARR,需要处理溢出,或使用捕获溢出中断进行计数扩展。 } }
避坑指南与心得:
- 坑1:信号抖动与滤波器。如果被测信号有毛刺,会导致误触发。定时器的输入捕获单元内置了数字滤波器,通过
ICFilter参数配置。它的原理是连续采样N次,只有连续N次电平一致才认为有效边沿。N值越大,抗干扰能力越强,但会引入微小延迟。需要根据信号质量和系统要求折中设置。 - 坑2:脉宽超过ARR(溢出)。如果高电平时间太长,计数器从0开始计数,在到达下降沿之前就已经溢出归零,那么捕获到的值就不是真实的脉宽了。解决方法有两种:一是降低定时器时钟频率(增大PSC),让计数器计得更慢;二是开启定时器的更新中断,在中断里对一个全局变量进行累加,记录溢出次数。最终脉宽 = 溢出次数 * (ARR+1) + 本次捕获值。
- 心得:测量频率的简便方法。除了用脉宽换算,更常用的测频方法是输入捕获+从模式触发。配置一个通道为上升沿捕获,并设置为从模式:复位模式,触发源为自己。这样,每个上升沿计数器复位。同时,使能另一个定时器的更新中断,固定时间(如1秒)后,读取该通道的捕获值(即两次上升沿间的计数值),这个值就是1秒内的脉冲个数,直接得到频率。这种方法硬件自动完成计数,软件只需在固定时间点读取,非常高效准确。
4. 进阶与排错:应对复杂需求与诡异问题
掌握了基础应用,我们来看看更复杂的场景和那些让人头疼的调试问题。
4.1 定时器的级联与同步:构建复杂时序系统
当单个定时器不够用,或者需要创建非常长的定时周期、需要精确同步多个定时事件时,就需要用到定时器的级联(或称主从模式)。
典型应用:用一个定时器(TIM2)的更新事件,去触发另一个定时器(TIM3)的启动。这样TIM3的计数周期就是TIM2周期的整数倍,可以实现超长定时。或者,用TIM1作为主定时器产生PWM,用TIM2作为从定时器,由TIM1的更新事件触发其开始ADC采样,实现PWM中心点与ADC采样的严格同步(在电机FOC控制中至关重要)。
配置逻辑:
- 配置主定时器(Master):正常配置其时基和更新事件。在
TIMx->CR2寄存器中,设置MMS(主模式选择)为010(更新事件作为触发输出)。 - 配置从定时器(Slave):正常配置其时基。在从定时器的
TIMx->SMCR寄存器中:- 设置
TS(触发选择)为对应的ITRx(内部触发)。例如,TIM2作为TIM3的从机,则TIM3的TS应选择ITR1(具体对应关系需查数据手册)。 - 设置
SMS(从模式选择)为触发模式或门控模式等。例如,设为触发模式,则主定时器的更新事件一来,从定时器就开始计数。
- 设置
避坑点:级联时,时钟源必须同步。通常所有定时器使用相同的APB总线时钟。确保主定时器先启动,从定时器处于等待触发状态。
4.2 那些年我踩过的“定时器坑”与排查思路
即使理解了原理,调试时依然会遇到各种诡异现象。下面分享几个典型案例:
问题一:定时器中断一次都不进。
- 排查链:
- 时钟树:首先确认该定时器的外设时钟是否使能?
__HAL_RCC_TIMx_CLK_ENABLE()调用了吗? - NVIC配置:中断向量表里配置了该定时器的中断服务函数吗?NVIC的优先级和使能位设置了吗?在CubeMX生成的代码中,有时会漏掉
HAL_NVIC_EnableIRQ(TIMx_IRQn)这一行。 - 中断标志:在调试器中,查看定时器的
SR寄存器,更新中断标志UIF是否置位?如果置位了但没进中断,问题大概率在NVIC;如果没置位,问题在定时器本身配置。 - 计数器状态:
CNT寄存器在动吗?如果不动,检查CEN位是否置1(定时器是否启动),再回溯检查时钟源配置。
- 时钟树:首先确认该定时器的外设时钟是否使能?
- 排查链:
问题二:PWM没有输出,或者输出固定电平。
- 排查链:
- GPIO复用:这是最高频的错误。确认引脚模式是
AF_PP(复用推挽输出)吗?Alternate功能号选对了吗?用示波器或万用表测一下引脚,如果是高阻态,肯定是GPIO配置问题。 - 高级定时器的MOE位:如果是TIM1/TIM8,检查
BDTR寄存器的MOE位是否为1。 - 输出比较模式:确认
CCMR寄存器中通道的模式是PWM模式1或2吗? - CCR值:
CCR寄存器的值是否在0和ARR之间?如果为0,输出可能一直是无效电平。 - 引脚冲突:该引脚是否被其他外设(如串口、SPI)占用了?
- GPIO复用:这是最高频的错误。确认引脚模式是
- 排查链:
问题三:输入捕获值跳动很大,不准。
- 排查链:
- 信号质量:首先用示波器看被测信号是否干净?是否有振铃或毛刺?这是硬件问题。
- 滤波器设置:如果信号有噪声,尝试增大
ICFilter值。 - 边沿检测:确认设置的捕获极性(上升/下降沿)与实际信号是否一致?
- 中断处理延迟:如果使用单通道两次捕获的中断方式,在高频下,CPU可能来不及处理中断,导致丢失边沿。考虑使用双通道硬件捕获法,或者降低信号频率。
- 计数器溢出:测量长脉宽时,是否处理了计数器溢出?如前所述,需要结合更新中断来扩展计数范围。
- 排查链:
一个实用的调试技巧:当你怀疑定时器配置有问题时,可以暂时将其配置为PWM输出模式,并设置一个固定的占空比。用示波器测量输出波形。如果波形频率、占空比符合预期,证明时基单元和输出比较单元的配置是正确的,问题可能出在中断、DMA或特定模式(如输入捕获)的配置上。这是一种“功能验证法”,能快速定位问题模块。
5. 超越数据手册:项目实战中的经验沉淀
最后,分享一些在真实项目中积累的、超越官方文档的实用经验。
5.1 动态重载PSC与ARR:实现“无感”的频率切换
在电机控制、音频生成等场景,需要动态改变PWM频率。粗暴地直接修改ARR或PSC寄存器可能导致当前周期被截断,产生非对称的畸形波形。
优雅的做法:利用预装载寄存器和更新事件。
- 确保
TIMx->CR1寄存器中的ARPE位使能(Auto-reload preload enable)。 - 在需要修改频率时,计算新的
PSC和ARR值,写入TIMx->PSC和TIMx->ARR寄存器。此时,新值只是写入了预装载寄存器,并未生效。 - 通过软件设置
TIMx->EGR寄存器的UG位(Update generation),或等待下一次硬件更新事件到来。在更新事件发生时,预装载寄存器的值才会被传递到影子寄存器,从而真正生效。这样可以保证频率切换发生在完整的PWM周期边界,波形连续平滑。
在HAL库中,可以使用__HAL_TIM_SET_AUTORELOAD()和__HAL_TIM_SET_PRESCALER()来设置值,然后调用__HAL_TIM_GENERATE_EVENT(htim, TIM_EVENTSOURCE_UPDATE)来生成更新事件。
5.2 定时器与DMA的联姻:解放CPU的高效数据搬运
定时器触发DMA是STM32外设联动的经典应用,能极大减轻CPU负担。常见场景有:
- DAC输出波形:定时器更新事件触发DMA,将波形数据表(如正弦波数组)逐个搬运到DAC数据寄存器,实现高速、精确的模拟信号合成。
- ADC规则扫描:定时器触发ADC开始一组通道的扫描转换,转换完成后由DMA将结果搬运到内存数组。这是实现固定采样率数据采集的黄金组合。
- PWM控制序列:通过DMA动态更新多个通道的CCR值,可以实现复杂、预定义的PWM波形序列。
配置关键点:
- 配置DMA流(或通道),设置外设地址为定时器的
CCR寄存器地址(对于PWM)或ARR寄存器地址(对于改变频率),内存地址为你的数据数组。 - 在DMA配置中,选择触发源为对应的定时器事件(如
TIMx_UP更新事件,TIMx_CH1比较事件等)。 - 使能定时器的相应DMA请求(如
TIMx->DIER寄存器中的CCxDE或UDE位)。 - 启动DMA,然后启动定时器。定时器事件会像节拍器一样,自动驱动DMA进行数据搬运。
5.3 低功耗模式下的定时器生存之道
在电池供电设备中,MCU常需进入低功耗模式(如Stop模式)。但很多定时器在Stop模式下会停止工作,因为其时钟源(HSI/HSE)被关闭了。
解决方案:使用低功耗定时器(LPTIM)。LPTIM是STM32中专门为低功耗场景设计的定时器,它可以在多种低功耗模式下运行,时钟源可以是LSI(内部低速)、LSE(外部低速)甚至系统时钟。虽然功能比通用定时器简单(通常只支持单次或连续计数、比较、PWM输出),但用于唤醒、基本计时绰绰有余。
设计思路:在进入深度睡眠前,配置LPTIM在指定时间后产生中断或唤醒事件。然后MCU进入Stop模式。LPTIM依靠LSI/LSE继续计数,时间一到即唤醒MCU。这样既实现了定时功能,又保证了极低的功耗。
从基本定时到PWM生成,从信号捕获到复杂联动,STM32的定时器外设展现了一个由简入繁、深度集成的硬件世界。理解它,不仅仅是记住几个API函数,更是要读懂数据手册中的框图,理解每个寄存器位背后的硬件逻辑。我个人的体会是,定时器是区分“单片机程序员”和“嵌入式系统工程师”的一道坎。跨过去,你就能设计出时序精准、响应及时、资源高效的系统。下次当你面对电机不转、采样不准、通信不同步的问题时,不妨先从定时器的配置和时钟树查起,或许就能找到那把关键的钥匙。