ARTICLE DETAIL

资讯详情

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

STM32 TIM1高级定时器输出指定个数PWM脉冲的完整实现

STM32 TIM1高级定时器输出指定个数PWM脉冲的完整实现 简介面向使用STM32进行嵌入式控制的开发者这是一份演示TIM1高级定时器以中断方式输出指定个数PWM脉冲的完整工程资源。内容围绕定时器初始化、PWM通道配置、更新/捕获中断处理及脉冲计数控制展开适合需要精确控制脉冲数量或学习高级定时器中断机制的工程师参考。压缩包内共374个文件以C源码、H头文件为主并包含汇编启动文件、编译生成的o/d/crf等中间文件及工程配置文件整体约8.34MB可直接用于Keil项目学习。资源已有654人学习下载。通过学习该工程读者可掌握寄存器级中断标志处理、防数据竞争设计以及用中断计数实现指定脉冲输出的思路同时也能了解多文件工程的组织方式便于迁移到电机控制、电源管理等实际场景。 做ST芯片的工程师我想大家迟早会撞上一个需求让定时器输出一串指定个数的PWM脉冲比如步进电机走N步、激光测距模块发射N个脉冲、计量仪表输出精确脉宽序列又或者驱动某些通信协议需要精确脉冲个数。用通用定时器硬数波形个数逻辑上能做但代码写起来总有点“不够优雅”到了重要场合还会担心计数漂移。我在实际项目里被这个需求折腾过好几次最后稳定的方案是落在TIM1这个高级定时器上配合中断方式实现简单直接现场也很好调。这篇就把它拆开讲透为什么选TIM1中断方式和其它方案比好在哪CubeMX怎么配代码怎么写以及那些“文档里不写但实际会踩”的坑。1. 为什么选TIM1高级定时器和通用定时器的本质区别1.1 高级定时器比通用定时器多了什么STM32里定时器分三类基本定时器TIM6/7、通用定时器TIM2/3/4/5、高级定时器TIM1/TIM8。很多人写PWM第一反应是拿TIM3因为例程多、用着顺。但“输出指定个数PWM”这个需求TIM3能用TIM1更合适原因在于硬件设计上的差异。高级定时器有三个通用定时器没有的关键能力第一是重复计数器RCR。这是我认为最核心的一点RCR的存在让硬件可以“数周期”。每一次计数器上溢不一定触发更新事件而是RCR先减一减到0才产生一次更新事件。这意味着可以硬件控制每隔几个PWM周期才产生一次中断把软件从高频中断里解放出来。第二是互补输出通道和死区控制。这个主要面向电机控制、逆变器场景虽然做脉冲输出不一定要用但如果项目后续要扩展半桥驱动TIM1直接有现成硬件支持。第三是刹车输入BRK和故障保护。高级定时器可以在异常信号到来时硬件立刻封锁PWM输出不需要CPU介入。这对工业现场很重要PWM故障保护的关键就在这里。1.2 输出指定个数PWM的三种常规思路围绕“指定个数”这个核心需求我见过三种主流做法各有各的适用场景第一种是主从定时器级联。TIM1做主输出PWMTIM2做从模式对PWM脉冲计数计满后输出一个事件再触发TIM1停止。这个方案纯硬件计数精度最高CPU完全不参与但配置复杂主从模式的关系调起来相对费神适合PWM频率高、脉冲数量大、计数精度要求变态的场景。第二种是用PWM输入捕获模式测自己的输出。外接一根线把输出反馈回输入捕获通道边沿计数。这个思路在某些特殊情况下能用但白白占用一个捕获通道还要处理“自己测自己”的延时问题一般我不推荐。第三种就是本文的主角更新中断方式软件计数。核心思路是开启PWM输出每次计数器上溢产生更新中断在中断里进行脉冲个数统计达到目标数量后由软件关闭输出。逻辑简单、代码直接、调试方便中低速PWM场景下足够稳定可靠。对于绝大多数项目的数量级需求“中断记个数”完全够用。2. 中断方式方案选型为什么够用边界在哪2.1 中断方式的工作原理PWM输出时计数器从0一直加到ARR到顶后回零重新开始这个过程就是一次PWM周期。计数器回零的那一瞬间会产生更新事件如果使能了更新中断ISR就会被执行。所以每次进入更新中断就意味着完整输出了一个PWM周期。我们要做的只是在中断里做一个自增计数当累计值与目标值相等时停止PWM输出。思路非常直白本质上是一个“软件计数器”。这里有一个细节很多人初次会弄混PWM的输出频率由ARR和PSC决定但PWM的占空比由CCR寄存器决定。更新中断响应的频率只跟PWM频率有关与占空比无关。也就是说占空比怎么变脉冲个数计数都不受影响。2.2 什么时候该用中断、什么时候该换DMA中断方式有个天然短板中断响应需要CPU时间。如果PWM频率是10kHz那就是每秒进入中断10000次每次ISR需要完成“自增比较可能关停”的动作一个简单ISR十几个周期能跑完对72MHz的F103来说占用不到2%的CPU资源完全扛得住。但如果你把PWM开到1MHz每秒100万次中断CPU大部分时间都在跑中断上下文切换系统就快被拖垮了。这时候更适合用带RCR的“硬件介入方式”把RCR设置为N-1这样每输出N个脉冲才产生一次更新中断中断频率直接降到原来的N分之一。如果你连这最后一次中断都想省掉那就用DMA传输完成中断或者干脆主从定时器级联让硬件自己做收尾。所以我把选型边界归纳得很简单1kHz到50kHz之间的常规PWM输出中断方式软计数是首选代码简单可靠超过100kHz建议用RCR收拢中断次数再往上或者脉冲个数极大比如上万请走主从级联。这个边界是我实际项目中试出来的经验值可以直接参考。3. 基于CubeMXHAL库的完整实现配置、代码、步骤3.1 CubeMX配置的关键参数我用STM32CubeMX配置工程这里以STM32F103系列为例芯片不同但配置思路一致。废话不多说直接上关键配置项。TIM1的Clock Source选择Internal Clock即可。Channel1改成PWM Generation CH1。如果你的需求要两路同步脉冲把Channel2也配成PWM Generation CH2。关键参数是这几项Prescaler (PSC) 72 - 1 Counter Mode Up Counter Period (ARR) 999 Auto-reload Preload Enable Repetition Counter (RCR) 0 CH1 Output Mode PWM Mode 1 CH1 Pulse (CCR1) 500 CH1 Output Polarity High CH1 Idle State Reset如果你用的STM32主频是72MHzPSC设为71后定时器计数频率是1MHzARR设为999PWM频率就是1MHz除以1000正好1kHz。这里有个习惯性问题我想特别提醒一下CubeMX里PSC和ARR是包含分频系数和重装值的真实数值不要手动减一。比如你想分频72倍PSC直接填71但CubeMX的输入框里填的就是71本身它会自动处理成PSC寄存器需要的值。不少新手在这里容易加倍分频导致频率差很多。Pulse就是CCR值决定占空比。我填500对应50%占空比。占空比的具体数值对你这个任务没有影响核心只决定每个脉冲的高电平宽度。Idle State是设置刹车或空闲状态下的输出电平一般保持Reset不配置为高否则可能一初始化就直接输出意外电平。3.2 HAL库的中断回调代码实现CubeMX配置完成后生成工程接下来在用户代码区里实现核心逻辑。全局定义两个变量一个是目标脉冲个数一个是当前脉冲计数volatile uint32_t pwm_pulse_target 0; volatile uint32_t pwm_pulse_current 0;启动函数很简单void start_target_pwm(uint32_t pulse_num) { pwm_pulse_target pulse_num; pwm_pulse_current 0; __HAL_TIM_CLEAR_FLAG(htim1, TIM_FLAG_UPDATE); HAL_TIM_PWM_Start_IT(htim1, TIM_CHANNEL_1); }然后是中断回调。HAL库把所有定时器事件都汇总到统一的回调函数里分发给具体定时器void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM1) { pwm_pulse_current; if (pwm_pulse_current pwm_pulse_target) { HAL_TIM_PWM_Stop_IT(htim1, TIM_CHANNEL_1); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 0); } } }这段代码有几个细节值得说明。第一为什么用HAL_TIM_PWM_PulseFinishedCallback而不是HAL_TIM_PeriodElapsedCallback。从底层实现看PWM模式下使能更新中断后中断里HAL库会先判断PWM模式再调用PulseFinished回调。用这个名字的回调比PeriodElapsed更精确地表达语义代码阅读性也好。第二关闭输出时我调用了HAL_TIM_PWM_Stop_IT。这个函数会把定时器的更新中断禁用同时关闭对应PWM输出通道。但注意它只是清零CCER里的对应bit不会动计数器状态也不会影响ARR、CCR这些寄存器。这时候如果你紧接着检测定时器备注会发现配置还在但输出已经断了。第三关闭输出后我又调用了set compare并置0。很多人不理解为什么关都关了还要把占空比清零。这里是为了防止下一次复用通道时产生“上次的余电平”。当PWM通道从停止状态恢复如果CCR还停留在500芯片会立即按当前配置输出波形也就是一启动就是50%占空比而如果你希望下一次是等状态准备好再输出先把比较值清0可以保证通道关闭期间输出确定的低电平。3.3 使用RCR降低中断频次如果你觉得中断频率太高心里悬可以启用重复计数器。这个功能在通用定时器上没有千万别小看它。把CubeMX里的Repetition Counter填成N-1比如你想让每200个脉冲进一次中断就填199。这样TIM1每计数200个PWM周期才产生一次更新事件中断频率降到原来的200分之一。中断回调的逻辑同步修改void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM1) { pwm_pulse_current 200; if (pwm_pulse_current pwm_pulse_target) { HAL_TIM_PWM_Stop_IT(htim1, TIM_CHANNEL_1); } } }这里注意pwm_pulse_current每次加的数值必须与RCR1严格一致否则累计计数会不准。实际开发中我建议把这个步进值定义成宏保持单一来源避免手改一处忘改另一处。3.4 用寄存器方式实现精简版如果你在用标准外设库或者不想依赖HAL可以直接在寄存器层操作最精简的实现长这样void TIM1_PWM_N_Pulses(uint16_t n) { TIM1-CR1 ~TIM_CR1_CEN; TIM1-CNT 0; TIM1-RCR 0; TIM1-SR 0; TIM1-DIER | TIM_DIER_UIE; TIM1-CCER | TIM_CCER_CC1E; TIM1-CR1 | TIM_CR1_CEN; }对应的中断服务函数void TIM1_UP_IRQHandler(void) { if (TIM1-SR TIM_SR_UIF) { TIM1-SR ~TIM_SR_UIF; pulse_count; if (pulse_count pulse_target) { TIM1-CCER ~TIM_CCER_CC1E; TIM1-DIER ~TIM_DIER_UIE; } } }寄存器版的好处是执行路径最短中断服务函数只有三四条指令响应极快。坏处是你要自己管理PSC、ARR这些寄存器另外还要记得在工程里打开TIM1全局中断NVIC。如果你的项目对时序要求非常苛刻寄存器版本值得保留。4. 常见问题与避坑实录4.1 问题速查表我把实际调试中遇到的典型问题整理成一张表方便直接对照排查现象可能原因解决方案输出一直不停更新中断未配置或NVIC未使能检查CubeMX中TIM1全局中断是否勾选NVIC是否Enable启动时提前出现意外电平CCR在启动前非0启动前将CCR置0开启后立即写目标比较值实际脉冲数比设定值少1关闭输出时正在输出第N个脉冲的高电平调整关闭时机或先关闭更新中断、延迟几微秒再关闭输出通道HAL_TIM_PWM_Stop_IT调用后立即查询UIF还置1中断标志未清除关闭输出前手动清标志高频率PWM下CPU占用过高中断频率太高启用RCR降低中断频次或改为主从定时器100%占空比时波形异常CCR设置为大于等于ARR确认CCR值在有效比较范围内注意边界情况4.2 实际调试中踩过的三个坑第一个坑是更新中断标志残留导致的提前关闭。有几回我在启动函数里调用HAL_TIM_PWM_Start_IT之前没有专门清除更新标志。结果定时器使能中断后第一次更新事件还没发生错的标志却先触发了一次中断导致脉冲计数提前1最终输出的脉冲数比目标值少一个。其实很好解决启动前手动清一次标志。第二个坑是占空比100%时的边界异常。如果你的CCR值设得比ARR大或者相等输出确实能保持高电平但这种情况下部分STM32型号的更新事件行为会跟正常占空比时不一样。有人认为PWM占空比100%时立即恒高就行但其实有些芯片内部影不影响更新标志说不准。建议把占空比上限控制在99%左右实验稳定后确定边界。第三个坑是关闭输出通道的时间点。中断关闭时当前PWM周期可能正在输出高电平这时关闭通道会直接把引脚电平拉低导致最后一个脉冲的宽度变短。某些对波形完整性有要求的场景肉眼可能看不出但示波器上很清晰。我的处理办法是中断回调里先关闭更新中断让计数器继续跑完当前周期然后再关闭输出通道这样最后一个脉冲能保持完整波形。判断当前波形是否处于高电平状态可以直接读输出比较状态寄存器。4.3 调试工具的使用心得这类问题用逻辑分析仪比示波器更方便。示波器看一两个波形没问题但要看“总数对不对”逻辑分析仪可以长时间连续采样数脉冲个数更可靠。我平时调试时直接把一个GPIO翻转信号拉高同时配合PWM输出测试在逻辑分析仪上确认次数符合预期。另外工业现场调试PWM故障保护时用示波器看故障信号与PWM输出的时序关系也很关键时间轴调细一些可以观察刹车闭锁延时。5. 扩展思路与总结心得5.1 继续扩展的方向这个中断计数方案其实只是一个基础骨架很多功能可以在它上面平滑扩展。第一是变成“带间隔的脉冲组序列”。每输出N个脉冲后进入一段空闲时间再输出下一组这在步进电机规划里很常见。你只需要在关闭输出通道后再启动另一个定时器做间隔延时延时到了再调用启动函数即可。第二是联动PWM的占空比动态调节。比如输出线性渐变的PWM也就是上一段热词里提到的呼吸灯场景可以通过在每个更新中断或每隔几个中断里修改CCR实现渐变。第三是加入故障保护联动。TIM1本身有BRK刹车引脚如果现场需要外部信号紧急封锁脉冲输出不需要修改代码逻辑硬件自己就能完成。这也是我选TIM1而不是通用定时器的另一个原因后续如果接功率板这个保护能力直接就有。5.2 一点个人体会我在多次调试后发现一个很多人忽略的道理做这种偏底层的控制逻辑方案不在于高端而在于可控。中断计数的方案谈不上多么新颖但它的优势是每一行逻辑都在自己手里出任何问题都能从代码层面追到底。还是那句话项目稳定压倒一切花哨的方案留着写文章可以守在生产线上跑的代码越直白越可靠。如果后续你在自己的板子上也踩到别的坑欢迎在评论区把现象贴出来一起讨论。本文还有配套的精品资源点击获取
返回列表