ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32 TIM1高级定时器输出指定个数PWM中断实现详解

STM32 TIM1高级定时器输出指定个数PWM中断实现详解 简介这是一套围绕意法半导体STM32系列中TIM1高级定时器输出指定个数PWM信号、并通过中断方式实现控制的工程资源主要面向嵌入式系统开发者、电子竞赛参赛者以及正在系统学习STM32定时器原理的工程师。资源包共374个文件压缩后约为8.34MB内部除核心C语言源文件与头文件外还包含工程配置文件、汇编启动文件、链接脚本、编译过程生成的中间文件以及调试输出文件等目录结构清晰完整便于按模块查阅也方便直接导入IDE中检查运行效果。内容覆盖了TIM1定时器的工作原理、PWM模式初始化流程、各通道占空比配置方法、更新中断与捕获比较中断的服务函数写法、脉冲计数逻辑以及达到指定脉冲数量后自动关闭输出或切换状态的处理策略针对该方案在脉冲数量上可能存在的硬件限制作者也提供了使用其他定时器联动或优化中断响应等思路以保证输出实时性。已有654人浏览学习对于希望获得可直接运行的参考样例、理解高级定时器中断式PWM控制细节的开发者来说具有较高的实用价值。 做步进电机控制或者需要精确脉冲输出的兄弟大概率都碰到过一个需求让单片机对外输出固定数量的PWM发完立刻停下来最好还能给主程序一个中断通知。STM32 的定时器里TIM1 和 TIM8 这两个高级定时器天生适合干这个活因为它们的重复计数器RCR可以指定更新中断要隔多少个计数周期才触发一次。这篇文章就围绕 STM32 TIM1 高级定时器输出指定个数 PWM 中断方式把原理、寄存器配置、标准库和 HAL 库的写法以及各种坑一次讲清楚。这个需求在工业控制里太常见了步进电机要走固定角度、激光振镜要打固定点数、变送器要输出固定个数的脉冲信号本质上都是在问“怎么让硬件替我精确数脉冲数够了喊我一声”。如果你还在用普通的延时翻 IO 实现那频率稍微上去就全是问题如果你用通用定时器的更新中断去计数那 CPU 会被高频中断拖死。TIM1 的重复计数器就是为了解决这类问题设计的下面直接切入正题。1. 项目整体设计与 TIM1 选型分析1.1 为什么是 TIM1 而不是通用定时器STM32 的定时器家族里TIM1 和 TIM8 是高级控制定时器TIM2/TIM3/TIM4/TIM5 是通用定时器TIM6/TIM7 是基本定时器。很多人觉得 PWM 输出用通用定时器就够了确实如果只是持续输出可调占空比的波形TIM3 完全顶得住。但如果要“输出固定个数 PWM 然后停下来”通用定时器就有点尴尬了。通用定时器的更新事件是每个计数周期都产生的比如 ARR 设成 999CNT 从 0 数到 999 就会产生一次更新事件。你要是想在输出 100 个脉冲之后中断一次就得在更新中断里做软件计数每进一次中断加 1加到 100 再关输出。这样 CPU 被中断按住了打频率高一点就腾不出手干别的。TIM1 就不一样。它内部多了一个重复计数器 RCR作用是“把更新事件攒够 N1 次再真正触发一次更新中断”。这相当于把“数脉冲”这个活从 CPU 搬到了硬件里CPU 只需要在中断里收尾就行。TIM1 常被拿来产生多路带有死区保护的互补 PWM但 RCR 这个特性让它成了精确脉冲控制的首选。1.2 中断方式的核心思路拆解“输出指定个数 PWM 中断方式”这句话拆开其实是三个动作输出 PWM、数够指定个数、发中断。三个动作里只有“发中断”是 CPU 参与的事前两个完全可以交给硬件完成。实现路径是这样的TIM1 工作在 PWM1 模式计数方式设为向上计数。CNT 每数到 ARR 就认为完成一个 PWM 周期理论上每周期产生一次更新事件但因为 RCR 的存在硬件会先把更新事件扣住等扣满了 RCR1 次才真正向上层报一次更新中断。所以把 RCR 设为 N-1就能在输出第 N 个 PWM 周期时触发一次更新中断。中断服务函数里你要做的事通常是关闭 TIM1 输出、记录状态、置一个标志位通知主循环然后根据业务逻辑决定下一步是改占空比接着发还是切到其他执行机构。整个过程中 CPU 没有持续干预 PWM 的翻转实际输出的频率和个数完全由定时器硬件保证。2. 核心原理拆解PWM 周期和中断的对应关系2.1 从 ARR/CCR 到 PWM 波形的生成过程PWM 频率由预分频器 PSC 和自动重载值 ARR 共同决定。假设系统时钟是 72MHzAPB2 定时器时钟也是 72MHz如果 PSC 设为 71定时器计数频率就变成 72MHz / (711) 1MHz也就是每 1us 计数器加 1。如果 ARR 设为 999CNT 从 0 数到 999 需要 1000us也就是 1ms 一个周期对应 PWM 频率 1kHz。占空比由比较寄存器 CCR 决定。在 PWM1 模式、向上计数、输出极性为高的情况下CNT 小于 CCR 时输出高电平CNT 大于等于 CCR 时输出低电平。CCR 设为 499占空比就是 50%CCR 设为 999占空比就是 100%CCR 设为 0占空比是 0%。这里有个小知识点CCR 等于 ARR 时部分芯片可能存在 100% 占空比输出异常的坑后面我会单独说。一次完整的 PWM 周期就是 CNT 从 0 走到 ARR 再清零重新开始的过程。每一个这样的周期硬件会产生一个更新事件只是这个更新事件不一定能上升到中断层。2.2 重复计数器 RCR 到底干了一件什么事RCR全称 Repetition Counter Register重复计数器寄存器。它的机制可以用一个生活例子理解你去食堂打饭每打一碗饭阿姨在记录板上加一个道道。如果只有一道横线她打一碗加一个道道就擦掉重来如果她设定的是一排五个道道那就要打满五碗饭才停下来擦一次记录板。只要这个间隔足够稳定RCR 就是那个“记录板”更新中断就是“擦记录板的时机”。具体到寄存器操作RCR 的 16 位数值范围很大但实际常用到的就是 0 到几千。关键公式是实际触发中断需要的更新事件次数 RCR 1。也就是说RCR0 时每个 PWM 周期都触发中断RCR99 时每 100 个 PWM 周期触发一次。所以“输出 N 个 PWM 后进中断”这个目标操作上就是把 RCR 赋值为 N-1。这里容易踩的坑就是把 RCR 直接赋成 N最后发现中断要么提前触发要么延后触发脉冲个数总是对不上。我刷到过不少同学在论坛问“为什么我设了 100 个实际只出来 99 个”十有八九是忘了减这个 1。3. 标准外设库实现一个可直接复制的例程3.1 硬件连接与时钟配置注意点我用的是 STM32F103ZET6TIM1_CH1 默认映射在 PA8。如果用到互补输出CH1N 默认在 PB13。很多例程喜欢用重映射把 TIM1 挪到 PE9 这些脚上但我的建议是新手尽量先用默认引脚等熟悉流程再折腾重映射不然 GPIO_Remap 和 AFIO 时钟忘开一个查半天查不出来。时钟这块有个容易忽略的细节TIM1 挂在 APB2 总线上和 USART1、ADC1 共用一条总线时钟。标准库初始化里一定要单独把 RCC_APB2PeriphClockCmd 里加上 RCC_APB2Periph_AFIO因为 TIM1 的引脚复用需要开启 AFIO 时钟。虽然某些默认复用情况下不开启也能跑但规范做法是开启避免后续上重映射时莫名其妙的问题。3.2 从 GPIO 到 TIM1 的完整配置代码下面这段是用标准外设库写的完整初始化我直接在工程里验证过只要主频匹配粘贴改一下参数就能用。void TIM1_PWM_NPulse_Init(uint16_t freq_pulse_num, uint16_t pulse_cycle, uint16_t pulse_width) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 1. 开启 TIM1 和 GPIOA 以及 AFIO 时钟均挂在 APB2 上 RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // 2. 配置 PA8 为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 配置定时器时基参数 // 假设系统时钟 72MHz这里 PSC 设为 71计数频率 1MHz TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period pulse_cycle - 1; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_RepetitionCounter freq_pulse_num - 1; // 关键N 个脉冲中断一次 TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); // 4. 配置 PWM 输出通道 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse pulse_width - 1; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); // 5. 预装载使能防止运行中修改参数导致波形毛刺 TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); // 6. 开启更新中断并配置 NVIC TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM1_UP_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); // 7. 高级定时器必须有这一步打开主输出使能 TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }有个地方要格外注意步骤 7 的TIM_CtrlPWMOutputs只对高级定时器有效通用定时器根本没有这个函数。这是因为高级定时器内部有个 MOE 主输出使能位必须置 1 才能把 PWM 信号送到引脚上。我见过很多移植例程的人拿着通用定时器的代码往 TIM1 上搬忘记这个函数折腾半天发现引脚就是没有波形。3.3 中断服务函数与后续动作中断服务函数名称宏定义在头文件里查一下启动文件就能确认。F103 系列 TIM1 的更新中断服务函数名是TIM1_UP_IRQHandler。volatile uint8_t g_pwm_finished_flag 0; void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); // 到达指定个数关闭输出 TIM_Cmd(TIM1, DISABLE); TIM_CtrlPWMOutputs(TIM1, DISABLE); g_pwm_finished_flag 1; } }中断里做完这三件事清标志、关输出、置标志位。关输出用两条语句一条是关计数器TIM_Cmd一条是关主输出使能TIM_CtrlPWMOutputs两个都关才保证引脚彻底没有输出波形。置标志位是给主循环一个信号主循环发现这个标志置 1就知道输出结束了可以继续下一步动作。这里有一点大家要理解中断触发的时间点是第 N 个 PWM 周期结束也就是 CNT 从 ARR 回到 0 的那一刻而真正执行到关闭输出的代码还需要几个时钟周期。所以在示波器上数波形可能看到输出完美跳变也可能看到最后一个脉冲比前面稍微短了一点尾巴这个取决于中断响应延迟。对绝大多数应用来说可以忽略但对时序要求极其苛刻的场合要么把关闭动作提前一个周期要么改用 DMA 方式这个我放在后面扩展讲。4. 基于 CubeMX 和 HAL 库的快速实现4.1 CubeMX 配置里容易被忽略的 RepetitionCounterHAL 库搞这套逻辑更省事CubeMX 图形化配置直接把好多底层细节屏蔽掉了。新建工程后在 Timers 里选 TIM1Mode 配置成 PWM Generation CH1Parameter Settings 里把 Prescaler、Counter Period、Pulse 填好。关键参数就是 Counter Period 是 ARRPulse 是 CCR这俩决定了 PWM 频率和占空比。但是很多人翻遍 CubeMX 界面都找不到 RCR 在哪。它藏在 TIM1 的 Configuration 页面仔细找一下有个 Repetition Counter 输入框默认是 0。你要输出 200 个脉冲就在这里填 199。填完以后CubeMX 生成的代码里htim1.Init.RepetitionCounter就已经是 199 了。如果这一步漏掉后面你用再多的回调函数也只会得到持续输出的波形因为每个周期都在进中断。NVIC 设置里记得勾选 TIM1 update interrupt这个不开的话中断永远不会执行。4.2 HAL 库启动与回调函数用法HAL 库的启动方式比标准库抽象了一层启动 PWM 输出并开启中断是这样写的HAL_TIM_PWM_Start_IT(htim1, TIM_CHANNEL_1);这个函数内部会配置 CH1 输出并使能更新中断。当 RCR 计数完成也就是指定个数的脉冲输出完毕HAL 库会从更新中断里调用回调函数void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM1) { HAL_TIM_PWM_Stop_IT(htim1, TIM_CHANNEL_1); // 这里可以继续做你的业务逻辑 } }需要提醒的是HAL_TIM_PWM_PulseFinishedCallback这个名字容易让人误以为每个脉冲都会回调一次。实际上它对应的是更新事件只有更新中断被触发才会调用。如果你没改 RCR那确实每个 PWM 周期都回调一次改了 RCR 之后就会变成多个周期回调一次。HAL 库对标志位的清除和中断入口管理都封装好了你只管在回调里写业务逻辑就行人为出错率比标准库低了不少。HAL 库的坑也有。比如你在回调里直接调用HAL_TIM_PWM_Stop_IT需要保证当前没有竞争条件。更稳妥的做法是只置一个标志位回到主循环再停止定时器避免在中断上下文里做太多操作。我在实际项目里一般只在回调里翻转一个 GPIO 和置标志位剩下的放 while 主循环里处理。5. 常见问题与排查技巧实录5.1 输出个数不对比设定值多一个或者少一个这是最高频的问题几乎每个月都能在论坛看到新帖。绝大多数情况都是 RCR 的值填错了。RCR N-1这件事我再强调一遍都不为过。填 N 就多一个周期才中断填 N-1 还没到第 N 个就进中断了。另一种情况是 PWM 计数起始位置。上电后计数器从 0 开始第一段高电平的起点是 CNT 复位后立刻输出高电平这个过程如果从示波器上看第一个脉冲的开始时间和后续脉冲的周期并不是完全对齐的。但实际测试下来影响极小除非你做高精度相位测量否则基本不用管。5.2 引脚完全没有波形输出一直是低电平这时候先别怀疑定时器配置先查两件事。第一TIM_CtrlPWMOutputs有没有执行高级定时器不开 MOE 位哪怕寄存器里所有值都对引脚也是死的。第二GPIO 模式是不是复用推挽默认的 GPIO_Mode_AF_PP 有没有写成 GPIO_Mode_Out_PP。这两个问题占了 80% 的没有波形的情况。还有一类隐蔽问题某些开发板的 PA8 引脚被板上其他外设占用了或者跳线帽没接。我给不少学生排查过代码怎么看都对最后发现是示波器探头接错了位置。先拿万用表量一下引脚电平变化能省很多时间。5.3 中断一直在进停不下来如果你发现程序一直卡在中断服务函数里先看是不是中断标志没清除。标准库要调用TIM_ClearITPendingBitHAL 库在回调返回后会自动清理两种库的维护方式不同但原理一致更新事件的标志位如果不及时清零中断会反复触发。另外注意区分两个标志更新中断标志TIM_IT_Update和捕获比较中断标志TIM_IT_CC1。很多人配置 PWM 的时候顺手把 CC 中断开了结果明明是 PWM 输出却不断进 CC 中断。排查时可以在中断服务函数里打断点看它进的是TIM1_UP_IRQHandler还是TIM1_CC_IRQHandler进去一看就明白了。我把常见问题整理成了一张表方便对照排查。现象可能原因解决办法输出0个脉冲引脚低电平未调用TIM_CtrlPWMOutputs标准库调用TIM_CtrlPWMOutputs(TIM1, ENABLE)脉冲数比设定少1RCR设置错误RCR赋值N-1脉冲数比设定多1RCR设置错误RCR赋值N-1中断不断触发更新标志未清除中断里调用TIM_ClearITPendingBit或等HAL清理占空比100%却无输出CCRARR时部分场景异常CCR设置为ARR-1或单独验证频率比预期高一倍或对不上PSC/ARR未减1计数频率和周期都补上那个15.4 100% 占空比下的输出异常现象最后一个坑和指定个数无关但使用 PWM 模式时容易一起踩到。有的型号在主频较高、CCR 等于 ARR 的场景下输出 100% 占空比会出现异常比如偶尔丢一个脉冲或者高电平维持不住。我的经验是如果确实需要 100% 占空比可以把 CCR 设成 ARR-1然后让输出极性反过来配合或者直接切到强制输出高电平模式。这个属于应用层 workaround不影响 PWM 计数逻辑。6. 实际应用场景与个人经验扩展TIM1 输出指定 PWM 中断方式最典型的场景就是步进电机脉冲控制。步进电机驱动器接收的是脉冲方向信号一个脉冲对应一个步进角度想要精确控制电机转多少圈本质上就是精确控制脉冲个数。以前用软件延时发脉冲电机低速还好高速时 CPU 全被占满整个系统其他任务全都卡死。换成 TIM1 加 RCR 之后电机的脉冲发放在后台自动完成CPU 可以在等待期间处理显示、通讯、按键这些事情性能提升非常明显。另外一个我个人觉得很有用的扩展是配合 DMA 实现更精确的关闭时机。前面提到中断方式存在毫秒级以下的响应延迟如果要求极高的脉冲精度可以在 PWM 发送到倒数第二个周期时利用 DMA 把关闭寄存器值的序列放到内存里由 DMA 在下一个更新事件时自动写入寄存器实现零延迟关闭。这个玩法比较复杂一般用的不多但如果你做激光振镜这类设备这个思路值得研究。调试工具这块我建议手头准备一个逻辑分析仪几十块钱的就行比示波器更容易抓到长时间的多路波形。用逻辑分析仪观察 TIM1_CH1 和 CH1N 两路输出可以直观看到 PWM 个数和死区时间是否符合预期。很多时候代码没 bug是示波器触发放置不对导致看不到完整波形逻辑分析仪的连续记录功能能让你轻松数出每一个脉冲。根据我自己的项目经验还有一个小细节中断服务函数里尽量只做标志位操作和必要的状态机切换不要在里面初始化外设、执行耗时算法或者调用打印函数。中断里做打印会瞬间把系统拖到崩溃边缘因为串口输出一个字节的时间足够定时器产生好几次溢出了。把这个习惯养成后面做更复杂的多定时器协同项目时会少踩很多坑。本文还有配套的精品资源点击获取
返回列表