资讯动态

STM32 PWM精确输出脉冲数控制电机:硬件定时器方案与避坑指南

发布时间:2026/10/16 8:59:10 来源:尧图企业网站定制
简介这份资源面向STM32嵌入式开发者与电机控制初学者聚焦用PWM实现精确脉冲个数输出解决步进电机步进次数与舵机转角难以精准控制的问题。包内共196个文件以h头文件、c源文件、o目标文件、axf可执行文件、hex烧录文件、uvproj/uvprojx工程文件及map、lst编译列表为主另有少量rar压缩包整体约952KB属于可直接编译下载的完整工程集合。内容围绕定时器配置、比较匹配与脉冲计数展开涉及TIM1至TIM8等通用与高级定时器的PWM输出设置并给出步进电机按1.8度步距、舵机按50Hz信号控制角度的实践思路。已有1798人学习下载适合希望掌握精确脉冲定位控制、对照工程代码理解中断服务与PWM初始化流程的读者参考。1. 从一次丢步说起为什么PWM输出脉冲数必须精确去年帮一个做自动化设备的朋友调一台步进电机滑台他反馈说每次走完1000个脉冲实际位移总差那么几个微米反复跑几十次之后误差累积到肉眼可见。一开始怀疑是机械间隙拆了重装还是老样子后来用逻辑分析仪抓PWM波形才发现问题出在脉冲计数上——定时器中断里做计数中断响应延迟导致最后一个脉冲被截断或者多发了。这个场景在STM32控制步进电机、伺服电机、甚至某些需要定长脉冲输出的场合非常典型PWM本身好生成难的是“发完N个脉冲就干净利落地停”。这份资源围绕的就是这个痛点在STM32上用PWM实现精确输出脉冲数控制电机。它适合正在做运动控制、步进电机驱动、定长脉冲输出相关开发的工程师也适合刚接触STM32定时器、想搞明白主从模式、单脉冲模式、重复计数器的朋友。核心思路不是靠中断里手动计数而是让定时器硬件自己完成“发够N个就停”把CPU解放出来也把精度从“软件抖动”提升到“硬件确定性”。2. 硬件定时器怎么替CPU数脉冲三种方案对比与选型2.1 为什么中断计数不靠谱先看最常见的做法用一个定时器输出PWM同时开更新中断在中断里累加计数到了目标值就关PWM。代码大概长这样// 中断计数方案不推荐用于高精度场景 volatile uint32_t pulse_count 0; uint32_t target_pulses 1000; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); pulse_count; if (pulse_count target_pulses) { TIM_Cmd(TIM3, DISABLE); // 关定时器 pulse_count 0; } } }这段代码逻辑上没问题但实际跑起来有几个坑中断响应有延迟尤其当系统里还有别的中断比如串口、SysTick时延迟可能达到几微秒如果PWM频率较高比如100kHz一个周期才10微秒中断还没进完下一个脉冲就来了直接丢中断。结果就是发1000个脉冲实际可能只发了998个或者发了1002个。对于步进电机丢一个脉冲就是丢一步累积起来位置就偏了。2.2 方案一单脉冲模式 中断重装STM32的定时器支持单脉冲模式One Pulse Mode, OPM。配置好后定时器收到一个触发信号就输出一个脉冲然后自动停止。如果要发N个脉冲就在中断里重新启动N次。这种方式比纯中断计数准因为脉冲宽度由硬件保证但中断次数还是N次高频率下依然有压力。配置的关键寄存器是TIMx_CR1的OPM位以及TIMx_CNT和TIMx_ARR。常见做法是用一个主定时器做触发源从定时器配成单脉冲模式主定时器每发一个触发从定时器出一个脉冲。中断里只需要重新使能从定时器。// 单脉冲模式配置片段以TIM2为主TIM3为从 void OPM_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 主定时器TIM2产生触发信号 TIM_TimeBaseStructure.TIM_Period 1000 - 1; // 触发间隔 TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // 1MHz计数 TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGO_UPDATE); // 更新事件作为触发 // 从定时器TIM3单脉冲模式 TIM_TimeBaseStructure.TIM_Period 500 - 1; // 脉冲宽度 TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_SelectInputTrigger(TIM3, TIM_TS_ITR1); // 触发源选TIM2 TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Trigger); TIM_SelectOnePulseMode(TIM3, TIM_OPMode_Single); // 单脉冲模式 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM2; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 250; TIM_OC2Init(TIM3, TIM_OCInitStructure); TIM_Cmd(TIM3, ENABLE); }这种方案适合脉冲数不多几百到几千、频率不高的场景。缺点是每个脉冲都要进一次中断如果发10万个脉冲CPU基本被中断占满了。2.3 方案二重复计数器RCR——硬件自动计数STM32的高级定时器TIM1、TIM8和部分通用定时器如TIM15、TIM16、TIM17带重复计数器Repetition Counter, RCR。这个计数器的作用是每次更新事件发生时RCR减1减到0才产生真正的更新中断或DMA请求。换句话说你可以让定时器发N1个脉冲后才进一次中断。配置很简单设置TIMx_RCR寄存器即可// 使用重复计数器实现N个脉冲后中断一次 void RCR_Config(uint16_t pulse_num) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_TimeBaseStructure.TIM_Period 1000 - 1; // PWM周期 TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // 1MHz TIM_TimeBaseStructure.TIM_RepetitionCounter pulse_num - 1; // 关键N-1 TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; // 占空比50% TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_Cmd(TIM1, ENABLE); }注意RCR是16位寄存器最大65535所以单次最多发65536个脉冲。如果需要更多可以在中断里重新设置RCR继续发。这个方案比单脉冲模式效率高得多中断次数从N次降到1次或几次。但有个限制不是所有STM32型号的定时器都有RCR选型时要查参考手册。2.4 方案三主从门控 DMA——大批量脉冲的首选如果要发几万甚至几十万个脉冲且对CPU占用敏感主从门控配合DMA是更优解。思路是用一个定时器主控制脉冲频率另一个定时器从工作在门控模式主定时器的某个通道输出作为从定时器的门控信号。从定时器只在门控信号有效时计数计到设定值后自动停止。更常见的做法是用DMA来搬运脉冲数把目标脉冲数写入DMA传输数量定时器每次更新事件触发DMA请求DMA减到0时产生中断在中断里关定时器。这样CPU只在最后进一次中断。// DMA方式定时器更新事件触发DMA传输完成中断里停止 void DMA_Pulse_Config(uint32_t pulse_num) { DMA_InitTypeDef DMA_InitStructure; // 配置DMA源地址为固定值目的地址为定时器ARR或虚拟寄存器 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)TIM4-ARR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)dummy_value; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize pulse_num; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Disable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel7, DMA_InitStructure); TIM_DMACmd(TIM4, TIM_DMA_Update, ENABLE); DMA_ITConfig(DMA1_Channel7, DMA_IT_TC, ENABLE); DMA_Cmd(DMA1_Channel7, ENABLE); TIM_Cmd(TIM4, ENABLE); } // DMA传输完成中断 void DMA1_Channel7_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC7)) { DMA_ClearITPendingBit(DMA1_IT_TC7); TIM_Cmd(TIM4, DISABLE); // 脉冲发完停定时器 } }这个方案的精髓在于DMA的传输数量就是脉冲数硬件自动递减减到0触发中断。CPU全程不参与计数精度只取决于定时器时钟和DMA响应实测在72MHz主频下发10万个脉冲的误差可以控制在0个脉冲只要时钟准确。三种方案怎么选看下面这张表方案适用脉冲数CPU占用精度硬件要求中断计数500高差任意定时器单脉冲模式2000中好带OPM的定时器重复计数器65536低好带RCR的定时器主从DMA任意极低极好带DMA的定时器我一般会优先看芯片有没有RCR和DMA有DMA就直接上DMA方案省心。3. 从零跑通工程搭建与关键配置步骤3.1 时钟与定时器初始化不管用哪种方案时钟配置是第一步。以常见的STM32F103为例系统时钟72MHz定时器时钟也是72MHzAPB1分频系数为1时。如果APB1分频系数不为1定时器时钟会倍频这个细节容易翻车。// 系统时钟配置标准库 void RCC_Configuration(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); } // GPIO配置PWM输出引脚 void GPIO_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; // TIM4_CH1 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); }这里有个常见坑PWM输出引脚必须配成复用推挽输出AF_PP配成普通推挽输出的话引脚不受定时器控制示波器上什么都看不到。另外如果用的是高级定时器TIM1或TIM8还需要额外使能PWM输出TIM_CtrlPWMOutputs否则引脚也没波形。3.2 PWM频率与占空比计算PWM频率由定时器时钟、预分频器PSC和自动重装载值ARR共同决定PWM频率 定时器时钟 / ((PSC 1) * (ARR 1))比如72MHz时钟PSC71ARR999那么PWM频率 72,000,000 / (72 * 1000) 1000Hz。占空比由捕获比较寄存器CCR决定占空比 CCR / (ARR 1)CCR500时占空比50%。步进电机驱动器一般要求脉冲频率在几百Hz到几十kHz之间具体看驱动器手册。频率太低电机会抖动频率太高驱动器可能响应不过来。// 定时器基础配置1kHz PWM50%占空比 void TIM4_PWM_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_TimeBaseStructure.TIM_Period 1000 - 1; // ARR TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // PSC TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM4, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; // CCR TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM4, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM4, ENABLE); }注意TIM_OCPreload_Enable和TIM_ARRPreloadConfig这两个预装载配置它们保证在更新事件发生时CCR和ARR的值才生效避免PWM波形出现毛刺。调试时如果发现波形有异常窄脉冲先检查这两个配置。3.3 脉冲数控制逻辑的接入点把脉冲数控制逻辑接进去关键是找到“发完最后一个脉冲”的时刻。用DMA方案时DMA传输完成中断就是那个时刻用RCR方案时更新中断就是那个时刻。在中断里做两件事关定时器、置完成标志。// 脉冲输出完成标志 volatile uint8_t pulse_done 0; // DMA传输完成中断脉冲发完 void DMA1_Channel7_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC7) ! RESET) { DMA_ClearITPendingBit(DMA1_IT_TC7); TIM_Cmd(TIM4, DISABLE); // 立即停止PWM输出 pulse_done 1; // 通知主循环 } } // 启动一次定长脉冲输出 void Start_Pulse_Output(uint32_t num) { pulse_done 0; DMA_Cmd(DMA1_Channel7, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel7, num); // 设置脉冲数 DMA_Cmd(DMA1_Channel7, ENABLE); TIM_SetCounter(TIM4, 0); // 计数器清零 TIM_Cmd(TIM4, ENABLE); }这里有个细节关定时器之前最好先把计数器清零否则下次启动时计数器从上次的值开始第一个脉冲的宽度可能不对。另外如果电机驱动器对脉冲的下降沿敏感关定时器时引脚电平可能停在任意状态最好在关定时器后把引脚拉低确保最后一个脉冲完整。4. 避坑与排查脉冲数对不上时先查这五处4.1 现象实际脉冲数比设定值多1或少1原因定时器启动和停止的时机与计数器状态不同步。比如先使能定时器再设置DMA传输数量第一个脉冲可能已经发出去了但没被计数或者关定时器时正好在脉冲中间导致最后一个脉冲不完整。解决启动顺序固定为“先配置DMA传输数量 → 清零计数器 → 使能定时器”。停止时在中断里先关定时器再清标志。如果对边沿敏感可以在停止后手动拉低引脚。4.2 现象低频时正常高频时丢脉冲原因中断响应不过来或者DMA带宽不够。中断计数方案在高频下必然丢脉冲DMA方案如果和别的外设抢总线也可能丢。解决换用DMA方案或RCR方案把中断次数降到最低。检查DMA优先级把脉冲控制的DMA通道设为最高优先级。如果还是丢降低PWM频率试试确认是带宽问题还是逻辑问题。4.3 现象脉冲数对了但电机丢步原因脉冲频率超过了驱动器的最大响应频率或者加减速没处理好。步进电机启动时如果直接给高频脉冲会因为惯性丢步。解决加加减速曲线用定时器动态改ARR来实现频率渐变。常见做法是梯形加减速或S形加减速启动频率从几百Hz开始逐步升到目标频率。这个项目里如果没带加减速二次开发时可以补上。4.4 现象换了个STM32型号代码跑不通原因不同型号的定时器资源不一样。比如F103的TIM4没有RCRF4的TIM4有F1的DMA请求映射和F4不同。解决查对应型号的参考手册确认定时器是否支持RCR、DMA请求映射表是否一致。移植时重点检查TIM_DMACmd的请求源和DMA通道号。4.5 现象示波器上看波形有毛刺原因CCR或ARR在计数过程中被修改导致占空比突变。或者预装载没使能。解决使能TIM_OCPreload和TIM_ARRPreload确保修改在更新事件时生效。如果需要在运行中改频率用TIM_SetAutoreload和TIM_SetComparex它们会自动处理预装载。5. 二次开发把定长脉冲封装成可复用的电机控制接口5.1 封装成独立模块跑通之后下一步是把这套逻辑封装成干净的接口方便在多个项目里复用。我一般会建一个motor_pulse.c和motor_pulse.h对外只暴露三个函数初始化、启动、查询状态。// motor_pulse.h #ifndef __MOTOR_PULSE_H #define __MOTOR_PULSE_H #include stm32f10x.h void MotorPulse_Init(void); void MotorPulse_Start(uint32_t pulses); uint8_t MotorPulse_IsDone(void); void MotorPulse_Stop(void); #endif实现里把定时器、DMA、中断的细节都藏起来上层应用只关心“发多少个脉冲”和“发完没有”。这样换芯片时只需要改这一个文件不用动业务代码。5.2 加入加减速曲线定长脉冲的进阶需求是加减速。思路是在脉冲输出过程中动态调整ARR让频率按预设曲线变化。简单做法是用一个数组存频率表每次更新中断里取下一个频率值写入ARR。// 梯形加减速加速段、匀速段、减速段 const uint16_t accel_table[] {1000, 900, 800, 700, 600, 500}; // ARR值递减频率递增 const uint16_t decel_table[] {500, 600, 700, 800, 900, 1000}; void TIM4_IRQHandler(void) { static uint32_t step 0; if (TIM_GetITStatus(TIM4, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM4, TIM_IT_Update); step; if (step ACCEL_STEPS) { TIM_SetAutoreload(TIM4, accel_table[step]); } else if (step total_steps - DECEL_STEPS) { TIM_SetAutoreload(TIM4, decel_table[total_steps - step]); } } }这个表可以根据实际电机的矩频特性来调加速段太陡会丢步太缓则效率低。我一般先用保守参数跑通再用示波器看波形逐步优化。5.3 多轴联动的扩展思路如果项目需要控制多个电机每个电机用一个独立的定时器和DMA通道互不干扰。启动时按顺序使能或者用主定时器同步触发。注意DMA通道不能冲突查手册分配好。中断优先级也要规划脉冲完成中断优先级可以设高一点保证及时停止。5.4 验证方法用逻辑分析仪抓波形改完代码怎么验证最直接的办法是用逻辑分析仪或者示波器抓PWM引脚波形数脉冲个数。没有逻辑分析仪的话可以用另一个定时器做输入捕获数上升沿个数和设定值对比。我习惯在代码里加一个调试变量记录实际发出的脉冲数用DMA传输剩余量反推和设定值一起打印出来跑个几百次看有没有偏差。从那以后我每次做脉冲控制都会先跑一个“发1000个脉冲循环100次”的测试用输入捕获数实际脉冲数确认零误差再往下做。这个习惯帮我省了很多返工的时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑