ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32定时器实战:从定时中断到PWM与输入捕获的嵌入式开发指南

STM32定时器实战:从定时中断到PWM与输入捕获的嵌入式开发指南 1. 从“嘀嗒”到“交响乐”理解STM32定时器的核心价值如果你刚开始接触STM32可能会觉得定时器Timer不过就是个“嘀嗒嘀嗒”计数的东西用来做个延时或者定时中断。但当你真正深入项目比如想用PWM驱动一个电机让它平滑转动或者想精准测量一个外部信号的频率和脉宽时你就会发现这个看似简单的“嘀嗒”声其实是整个微控制器系统里最复杂、最强大、也最考验功力的“交响乐指挥家”。我最初用定时器也就是在main函数里写个HAL_Delay(1000)让LED灯闪一下。后来做四轴飞行器需要同时控制四个电机的转速PWM、读取遥控器接收机的信号输入捕获、还要以固定频率运行姿态解算算法定时中断那一刻我才真正体会到把定时器玩明白是把STM32从“单片机”升级到“微控制器”的关键一步。它不再是孤立的模块而是协调整个系统时序、实现复杂控制逻辑的核心枢纽。STM32的定时器家族庞大从基本定时器TIM6, TIM7、通用定时器TIM2-TIM5, TIM9-TIM14到高级定时器TIM1, TIM8功能逐级增强。今天我们就围绕定时中断、PWM输出和输入捕获这三个最核心、最常用的功能抛开枯燥的寄存器手册从实际应用的角度把它们彻底讲透。我会结合我踩过的无数个坑告诉你配置时的关键细节、原理背后的“为什么”以及如何让它们协同工作。无论你是正在做智能小车、机械臂、还是任何需要精确定时和信号处理的项目这篇内容都能让你少走弯路。2. 定时中断系统节拍器的精准配置与避坑指南定时中断是定时器最基础的功能相当于给系统安装了一个高精度的“节拍器”。它的核心思想很简单让定时器以一个固定的频率比如1kHz自动计数每次计满或计到某个特定值就产生一个中断我们在中断服务函数里执行一些需要周期性处理的任务比如扫描按键、更新显示、运行控制算法。2.1 定时器时钟源与分频一切精度的起点配置定时中断第一步不是去写代码而是先搞清楚你的定时器“心跳”有多快。这里最容易出错的就是时钟源和分频系数的计算。以STM32F103系列常见的72MHz系统时钟为例。假设我们使用通用定时器TIM2它的时钟源通常是APB1总线时钟。在标准库或HAL库的默认初始化后如果APB1预分频系数不为1定时器时钟会倍频。对于F103APB1时钟最高36MHz但定时器会得到72MHz的时钟。这一点至关重要很多人的定时不准根源就在这里。假设我们需要一个1ms1kHz的定时中断。计算步骤如下确定定时器时钟CK_CNT 确认是72MHz。确定计数模式 通常使用向上计数模式。设定自动重装载值ARR 这是我们希望计数的最大值。设定预分频器值PSC 用于对CK_CNT进行分频得到实际的计数频率。计算公式为定时周期 (PSC 1) * (ARR 1) / CK_CNT我们的目标是定时周期 0.001秒。CK_CNT 72,000,000 Hz。这里有两个变量PSC和ARR我们可以先固定其中一个来简化。通常为了让ARR的值范围更直观比如设定为1000就代表1ms我们会先设定PSC。让(PSC 1) / CK_CNT 1us即PSC 1 CK_CNT / 1,000,000 72。所以PSC 71。此时计数器的计数频率 CK_CNT / (PSC1) 72MHz / 72 1MHz即每1us计一个数。那么要产生1ms的中断就需要计数1000次。所以ARR 1000 - 1 999。因为从0开始计数注意 在HAL库或标准库中我们填入的参数就是PSC和ARR的寄存器值即71和999。(PSC1)和(ARR1)是公式中的概念对应寄存器写入的值就是PSC和ARR。这一点理解偏差会导致定时时间差一倍务必小心。2.2 HAL库与CubeMX配置实战使用STM32CubeMX可以极大简化配置过程但理解其生成的代码同样重要。在CubeMX中 选择TIM2设置为“Internal Clock”。在Parameter Settings页Prescaler (PSC - 16 bits value): 71Counter Mode: UpCounter Period (AutoReload Register - 16 bits value): 999auto-reload preload: Enable (推荐可避免更新ARR时的毛刺)在NVIC Settings中勾选“TIM2 global interrupt”使能中断。生成的代码分析 CubeMX会在main.c中调用HAL_TIM_Base_Init()和HAL_TIM_Base_Start_IT(htim2)。重点在于中断服务函数和回调函数。中断向量TIM2_IRQHandler在stm32f1xx_it.c中它内部调用了HAL_TIM_IRQHandler(htim2)。我们用户需要重写的是定时器周期更新中断的回调函数HAL_TIM_PeriodElapsedCallback()。这个函数是一个弱定义__weak在HAL库中我们需要在main.c或自己的用户文件中重新实现它。/* 用户代码实现回调函数 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) // 判断是哪个定时器产生的中断 { // 在这里执行你的1ms周期任务 // 例如翻转一个测试引脚电平或者执行任务调度 HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 假设PA5接LED } }2.3 避坑心得中断响应性与系统负载中断服务函数要短 切记不要在HAL_TIM_PeriodElapsedCallback或任何中断服务函数中执行耗时操作如HAL_Delay、复杂的浮点运算、等待式通信。这会导致其他中断被延迟响应系统实时性变差。正确的做法是在中断里只设置标志位在主循环或由RTOS任务根据标志位去处理具体业务。多个定时器中断的优先级 如果系统中有多个定时器中断或其他中断如串口需要在CubeMX的NVIC配置中合理设置它们的“Preemption Priority”和“SubPriority”。优先级高的中断可以打断优先级低的中断。对于系统关键节拍如电机控制的PWM更新中断应赋予较高的抢占优先级。滴答定时器SysTick与通用定时器 SysTick是Cortex-M内核自带的简易定时器通常用于操作系统的心跳节拍如FreeRTOS的vTaskDelay。它使用方便但功能单一仅基本中断。对于复杂的、需要PWM或捕获功能的定时任务必须使用片上外设定时器TIMx。两者可以共存互不干扰。定时精度验证 配置好后最直接的验证方法就是用上述代码翻转一个GPIO然后用逻辑分析仪或示波器测量方波的周期。理论是1ms实际测量可能是1.0001ms或0.999ms这取决于外部晶振的精度和时钟树的配置。对于大多数应用这已足够。如果对精度要求极高如高精度计时需要考虑使用外部高精度有源晶振并校准RTC时钟。3. PWM输出从原理到实战驱动电机与调光的艺术PWMPulse Width Modulation脉冲宽度调制是定时器输出比较功能最典型的应用。它通过调节一个周期固定方波的“高电平时间”脉宽占整个周期的比例占空比来等效地模拟一个连续变化的模拟量。比如用50%占空比的3.3V PWM驱动一个LED其平均电压就是1.65V亮度介于全亮和全灭之间。3.1 PWM生成的底层原理比较与匹配通用定时器生成PWM通常使用其输出比较模式。核心是三个寄存器计数器寄存器CNT、自动重装载寄存器ARR和捕获/比较寄存器CCRx。ARR 决定了PWM的周期。计数器CNT从0开始向上计数达到ARR值后清零重新开始形成一个周期。CCRx 这是我们控制脉宽的关键。定时器硬件会实时比较CNT和CCRx的值。工作流程 假设我们设置PWM模式为“向上计数CNTCCRx时输出有效高电平”。CNT从0开始递增。当CNT CCRx时对应的输出通道如CH1保持高电平。当CNT CCRx时输出翻转为低电平。CNT继续递增到ARR然后清零输出又重新变为高电平开始下一个周期。因此PWM的周期由ARR和时钟频率决定而占空比则由CCRx的值决定。占空比 CCRx / (ARR 1)。3.2 使用CubeMX配置PWM的详细步骤与参数解读我们以TIM3的通道1CH1输出一个1kHz占空比可调的PWM为例。时钟与模式选择 在CubeMX中启用TIM3时钟源选择“Internal Clock”。在“Combined Channels”下拉菜单中选择“PWM Generation CH1”。参数配置Parameter SettingsPrescaler (PSC): 分频系数。假设系统时钟72MHz我们希望计数器时钟为1MHz这样每个计数是1us。则PSC 72 - 1 71。Counter Mode: Up向上计数。Counter Period (ARR): PWM周期。1kHz对应周期1ms。计数器时钟1MHz1ms需要计数1000次。所以ARR 1000 - 1 999。Pulse (CCR1): 初始占空比对应的比较值。例如想要初始占空比50%则CCR1 500。CH Polarity: High。表示“有效电平”为高。即CNTCCR1时输出高否则输出低。你也可以选择Low逻辑相反。GPIO设置 CubeMX会自动将TIM3_CH1映射到的GPIO如PA6配置为复用推挽输出模式。你可以在Pinout视图确认。生成代码与API调用 生成代码后在main.c的用户代码区需要启动PWM输出并学会动态修改占空比。/* 启动TIM3的通道1输出PWM */ HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); /* 在程序运行中动态修改占空比为75% */ __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 750); // CCR1 0.75 * (9991)__HAL_TIM_SET_COMPARE这个宏是直接操作寄存器速度极快适合在需要快速响应的控制循环如PID计算后更新电机PWM中使用。3.3 高级话题互补PWM与死区插入当你需要驱动一个全桥电路如直流无刷电机、步进电机驱动器时同一桥臂的上下两个开关管不能同时导通否则会造成短路直通烧毁器件。这就需要用到定时器的互补输出和死区插入功能这通常是高级定时器TIM1, TIM8的专属。互补输出 你可以配置CH1输出主波形同时CH1N输出一个完全反相互补的波形。死区插入 在互补波形切换的瞬间一个从高变低另一个从低变高硬件会自动插入一段两者都为低电平的“死区时间”确保上下管绝对没有同时导通的可能。在CubeMX中配置高级定时器的互补PWM时会多出一个“Dead Time”的参数单位可以是纳秒或时钟周期需要根据你使用的功率管MOSFET/IGBT的开关特性来谨慎设置。死区时间太短起不到保护作用太长则会降低有效电压利用率导致波形畸变。3.4 实战避坑电机控制中的PWM常见问题频率选择 驱动有刷直流电机PWM频率通常在1kHz到20kHz之间。频率太低如几百Hz电机会有可闻的啸叫声频率太高开关损耗会增大且可能超出驱动电路的响应能力。驱动舵机Servo则是固定的50Hz周期20ms通过脉宽0.5ms-2.5ms来控制角度。占空比与电机启动 很多直流电机有启动死区即占空比低于某个值如10%时产生的电压不足以克服静摩擦力电机不转。此时如果持续施加小占空比电机线圈相当于短路容易发热烧毁。解决方案是软件设定一个最小启动占空比或者采用开环速度斜坡启动。PWM引脚的重映射 有时候默认的引脚被其他功能占用需要查看芯片数据手册的“Alternate function mapping”表格使用重映射功能AFIO将定时器通道输出到其他引脚上。CubeMX的Pinout视图可以直观地完成这个操作。测量PWM输出 务必使用示波器验证你输出的PWM波形。确认频率、占空比、幅值应该是3.3V或5V是否符合预期。这是硬件调试中最直接有效的手段。4. 输入捕获精准测量频率与脉宽的关键技术输入捕获是定时器另一个强大的功能用于测量外部信号的频率或脉冲宽度。其核心原理是当检测到指定引脚上的特定边沿上升沿或下降沿时硬件会自动将当前计数器CNT的值锁存到捕获/比较寄存器CCRx中并可以产生中断。通过记录两次捕获的CNT值之差结合计数器的时钟周期就能计算出时间间隔。4.1 测量脉宽的标准方法双沿捕获要测量一个高电平脉冲的宽度最常用的方法是上升沿触发捕获一次下降沿触发再捕获一次。初始化配置将定时器通道配置为输入捕获模式通常先设置为上升沿捕获。开启捕获中断。设置一个足够大的ARR值如0xFFFF让计数器在测量期间不会溢出或处理好溢出情况。启动定时器。中断服务程序逻辑第一次中断上升沿 记录下CCRx的值存入变量Capture_Val1。然后立刻将捕获边沿改为下降沿通过修改CCER寄存器的CCxP位或调用HAL库函数。第二次中断下降沿 记录下CCRx的值存入变量Capture_Val2。然后立刻将捕获边沿改回上升沿为下一次测量做准备。计算脉宽 高电平时间PulseWidth (Capture_Val2 - Capture_Val1) * Timer_CLK_Period。需要考虑计数器溢出的情况如果Capture_Val2 Capture_Val1则PulseWidth (ARR 1 Capture_Val2 - Capture_Val1) * Timer_CLK_Period。4.2 使用HAL库实现输入捕获的代码框架HAL库提供了输入捕获的API但流程需要我们自己组织。以下是一个测量高电平脉宽的简化框架uint32_t uwIC2Value1 0, uwIC2Value2 0; uint32_t uwDiffCapture 0; uint8_t IsFirstCaptured 0; // 是否是第一次捕获标志 // 启动输入捕获中断 HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1); // 输入捕获中断回调函数 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) // 判断通道 { if(IsFirstCaptured 0) // 第一次捕获是上升沿 { uwIC2Value1 HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 读取CCR1 IsFirstCaptured 1; // 设置标志 // 改变触发边沿为下降沿 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); } else // 第二次捕获是下降沿 { uwIC2Value2 HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 计算差值注意处理溢出 if (uwIC2Value2 uwIC2Value1) { uwDiffCapture uwIC2Value2 - uwIC2Value1; } else { uwDiffCapture (0xFFFF - uwIC2Value1) uwIC2Value2 1; // 假设ARR0xFFFF } // 这里可以根据时钟频率计算出实际时间 // uint32_t pulse_width_ns uwDiffCapture * (1e9 / Timer_Clock_Freq); // 重置准备下一次测量 IsFirstCaptured 0; // 改变触发边沿为上升沿 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); } } }4.3 测量频率的方法测周法与测频法测周法 适用于低频信号。即测量一个完整周期的时间T频率 f 1 / T。可以用上述双沿捕获测量一个周期两次上升沿之间的时间来实现。精度高但信号频率越低测量所需时间越长。测频法 适用于高频信号。在固定的闸门时间比如1秒内用外部信号触发计数器计数定时器可以配置为外部时钟模式计数值即为频率。这种方法在闸门时间内完成测量速度固定但低频时可能计数太少精度下降。对于STM32更通用和精确的方法是测周法配合定时器的溢出中断来处理长周期信号。4.4 输入捕获的进阶技巧与避坑滤波与分频 如果输入信号有毛刺可以在定时器输入通道配置数字滤波器Input Filter和分频器Prescaler。例如设置分频为2则每2个有效边沿才触发一次捕获可以抑制高频噪声。但要注意这也会降低对快速边沿的响应能力。处理计数器溢出 当信号周期很长计数器CNT在两次捕获之间可能已经溢出多次。为了准确测量必须在定时器的更新中断溢出中断中对一个溢出计数器进行累加。最终时间 溢出次数 * (ARR1) 两次捕获的CNT差值 * 时钟周期。这是输入捕获测量长周期时的必备逻辑。引脚输入模式 输入捕获的GPIO必须配置为浮空输入或上拉/下拉输入模式具体取决于外部电路。如果信号源是开漏输出MCU这边通常需要使能内部上拉。多通道同时捕获 有些定时器支持多个通道独立捕获。例如可以用两个通道分别测量PWM信号的高电平和低电平时间效率更高。需要仔细阅读参考手册配置对应的从模式触发Slave Mode和触发选择TS。性能考量 输入捕获中断是硬件触发的对于极高频率的信号接近定时器时钟频率的一半频繁的中断可能消耗大量CPU资源。此时可以考虑使用DMA将捕获到的CCRx值直接搬运到内存数组中再进行批量处理解放CPU。5. 项目集成定时器综合应用案例——简易数字示波器前端为了将定时中断、PWM和输入捕获串联起来我们设想一个简单的项目一个基于STM32的简易数字示波器前端。它需要完成以下功能信号发生 用PWM产生一个可调频率和占空比的方波作为自测试信号源。信号测量 用输入捕获测量外部输入信号的频率和占空比。系统调度 用定时中断产生1ms的系统节拍用于按键扫描、测量结果刷新显示等。5.1 系统架构与资源分配TIM1高级定时器 用于产生高精度、带死区的PWM信号如果驱动需要这里我们仅用其CH1输出可调测试方波。TIM2通用定时器 配置为输入捕获模式CH1通道用于测量外部信号的频率和脉宽。开启更新中断和捕获中断以处理长周期测量和溢出。TIM3通用定时器 配置为1ms定时中断作为系统时基。TIM4通用定时器 备用或用于产生另一路PWM。5.2 核心代码逻辑与协同在main.c的初始化部分依次启动各个定时器// 启动系统节拍定时器中断 HAL_TIM_Base_Start_IT(htim3); // 启动PWM输出默认输出1kHz50%占空比 HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); // 启动输入捕获中断 HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1); HAL_TIM_Base_Start(htim2); // 也需要启动定时器基础计数在1ms定时中断TIM3的回调函数中执行非实时性要求高的任务void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { static uint16_t ms_cnt 0; if (htim-Instance TIM3) { // 每1ms执行一次 Key_Scan(); // 按键扫描用于调整PWM参数 if (ms_cnt 500) { ms_cnt 0; // 每500ms更新一次显示显示测量到的频率和占空比 Update_Display(measured_freq, measured_duty); } } }在输入捕获中断TIM2的回调函数中实现之前提到的双沿捕获逻辑并加入溢出处理。计算出的脉宽和周期值存入全局变量measured_pulse_width和measured_period供显示函数使用。在主循环中响应按键事件动态修改TIM1的ARR改频率和CCR1改占空比while (1) { if (key_event KEY_FREQ_UP) { current_arr 10; __HAL_TIM_SET_AUTORELOAD(htim1, current_arr); // 注意改变ARR后为了保持占空比不变CCR也应等比例调整 current_ccr (current_ccr * (current_arr1)) / (old_arr1); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, current_ccr); } // ... 处理其他按键事件 HAL_Delay(10); // 主循环小延时 }5.3 调试与优化经验中断冲突与优先级 TIM2的输入捕获中断测量信号是随机触发的实时性要求高应赋予较高的抢占优先级。TIM3的1ms系统中断优先级可以设低一些。TIM1的PWM更新中断如果需要优先级设置视应用而定。测量精度与范围 输入捕获的精度直接取决于定时器的时钟频率。72MHz下每个计数约13.9ns测量精度很高。测量范围受ARR值限制。为了兼顾宽范围和高精度可以采用“自动量程”思想先用一个较快的时钟不分频测量如果测得的计数值太小精度不够则动态切换到一个分频后的慢时钟重新测量。信号完整性 作为示波器前端输入信号的调理电路衰减、放大、限幅至关重要必须确保进入STM32引脚的电平在0-3.3V之间且边沿干净。可以直接在捕获引脚并联一个几十皮法的小电容来滤除高频毛刺。使用DMA减轻CPU负担 如果测量频率非常高输入捕获中断会极其频繁。可以考虑使用定时器的DMA请求功能将每次捕获的CCR值通过DMA循环存储到一个数组中。主程序只需要定期去检查这个数组的数据就能计算出频率和占空比从而将CPU从频繁的中断中解放出来。这是实现高性能测量的关键技巧。通过这个综合案例你可以看到定时器的三大功能是如何在一个实际项目中各司其职又相互配合的。PWM负责“输出”控制信号输入捕获负责“感知”外部世界定时中断则为整个系统提供稳定的“心跳”节奏。掌握它们你就掌握了STM32实时控制的精髓。
返回列表