STM32定时器PWM控制步进电机:从分频到中断计数的完整实战
简介面向嵌入式初学者的STM32F1步进电机PWM脉冲控制工程基于ARM Cortex-M3内核微控制器解决步进电机角度、速度与位置控制中的定时器PWM及中断配置问题。资源共138个文件压缩包约1.76MB主要包含34个h头文件、33个c源程序、16个o及axf、hex等编译产物另有uvproj工程配置与bat批处理脚本可覆盖从定时器配置、PWM占空比调整到中断服务程序设计的完整代码链路便于对照学习KEIL工程结构与底层驱动写法。已有1783人学习下载。内容围绕PWM模式1、预分频因子、ARR与CCR寄存器的实际设置展开并给出定时器溢出中断与占空比实时更新的处理思路。读者可获得STM32F1定时器模块的完整示例代码、标准外设库工程模板以及步进电机控制中脉冲频率与转速、占空比与转角关系的调试参考适合用于自动化设备、小型精密定位等场景的入门实践与项目二次开发。1. 步进电机为什么需要PWM脉冲STM32F1怎么接这个活步进电机的控制本质上不是“给多少电压”的问题而是“给多少个脉冲、以多快的频率给”的问题。一个脉冲让转子走一个固定角度也就是步距角脉冲数量决定转过多少角度脉冲频率决定转速。这个特性决定了驱动器想要的输入信号就是连续脉冲串而STM32F1的定时器模块恰好是最廉价的脉冲发生器它通过内部计数器、比较寄存器和输出引脚在基本不占用CPU时间的情况下把PWM波形送到电机驱动器。这篇博文围绕一个实际的TIMER工程展开从STM32F1的APB时钟树、PSC与ARR分频、PWM模式1与边沿对齐输出到更新中断里做脉冲计数和动态调速最后把工程源码里那些Keil文件的作用和常见坑位过一遍。适合正在用STM32做运动控制、或者想彻底搞懂定时器PWM底层细节的开发者看完可以直接抄参数。2. STM32F1定时器PWM生成的底层机制从APB时钟到CCRx2.1 定时器时钟树与PSC/ARR分频逻辑STM32F1的定时器分布在不同总线上TIM1和TIM8挂在APB2上TIM2、TIM3、TIM4挂在APB1上。这里有一个容易翻车的细节如果APB1预分频系数不是1那么挂在这条总线上的定时器时钟会翻倍。以最常见的72MHz主频配置为例APB1总线本身被限制在36MHz因此APB1通常配置为2分频此时TIM2/TIM3/TIM4的时钟反而恢复为72MHz而不是APB1总线上的36MHz。若按总线时钟直接带入公式算出的PWM周期会差出一倍这在低速控制时表现得尤其明显。PSC预分频器和ARR自动重装载寄存器共同决定PWM周期。计数器时钟等于定时器时钟除以(PSC1)一个PWM周期需要的计数器时钟个数等于(ARR1)所以输出频率公式是fPWM TIMxCLK / ((PSC 1) * (ARR 1))预分频器可以理解为粗调决定计数器每秒钟走多少步ARR是细调决定走多少步算一个周期。两者配合就能在很宽的范围内得到目标频率。例如要在TIM2上输出1kHz的PWM72MHz时钟下可以用PSC71、ARR999也可以PSC719、ARR99。后者的占空比调节档位只有100级对于需要精细调整脉冲宽度的场景明显不够用。我一般会把ARR放在500到2000之间优先保证占空比分辨率再用PSC去凑频率。目标频率PSC写寄存器ARR写寄存器占空比档位1kHz7199910005kHz7119920010kHz7199100这个表格假设TIMxCLK为72MHz。如果实际时钟是36MHz要达到同样的频率PSC或ARR需要减半调试时可以先用调试器观察CNT计数速率反推时钟树配置是否正确。2.2 PWM模式1与模式2在边沿对齐下的波形差异定时器工作在边沿对齐向上计数模式时计数器从0递增到ARR然后回到0如此循环。PWM模式1和模式2的区别在于有效电平出现的位置模式1下当CNT小于CCRx时输出有效电平CNT大于等于CCRx时输出无效电平模式2恰好相反CNT大于等于CCRx时输出有效电平。绝大多数步进电机工程用的是模式1配合高电平极性得到的是前高后低的脉冲波形边缘陡峭便于驱动器光耦识别。占空比由CCRx决定占空比 CCRx / (ARR 1)ARR999时CCRx500就是50%占空比。但步进电机驱动器和直流电机驱动不一样占空比本身不直接决定力矩或转速它只需要保证脉冲电平的有效宽度大于驱动器要求的最小值。常见驱动器的光耦输入要求最小脉冲宽度不小于2.5微秒所以我在配置PWM时通常把占空比固定在50%不把它当作调速变量速度全部通过修改频率实现。如果PWM不是送给驱动器而是直接驱动H桥那就要额外关注死区时间防止上下桥臂同时导通。STM32高级定时器TIM1/TIM8自带死区插入功能普通定时器则没有这也是选型时需要考虑的一点。2.3 用标准外设库直接验证波形生成这个TIMER工程里出现的是stm32f10x_tim.c也就是标准外设库SPL的定时器驱动。SPL和HAL库的API风格差异很大但寄存器操作是一一对应的。用SPL初始化TIM2输出PWM的典型代码如下#include stm32f10x_tim.h #include stm32f10x_gpio.h #include stm32f10x_rcc.h void TIM2_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; /* 使能TIM2与GPIOA时钟 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* PA0复用为TIM2_CH1 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* 时基72MHz / 72 / 1000 1kHz */ TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 999; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); /* PWM模式1CCR500占空比50% */ TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_Cmd(TIM2, ENABLE); }这里的关键参数有三个TIM_Prescaler71写入PSC寄存器实际分频是72因为计数器从0开始对齐TIM_Period999写入ARR寄存器一个周期实际计数1000次TIM_Pulse500写入CCR1决定占空比。PA0之所以能输出PWM是因为GPIO被配置成了复用推挽输出GPIO_Mode_AF_PP并把复用功能映射到TIM2_CH1。整个流程缺一不可少了任何一步引脚上都量不到波形。注意如果改用TIM1或TIM8这类高级定时器SPL代码里还需要在TIM_Cmd之后调用TIM_CtrlPWMOutputsEnable(TIM1, ENABLE)否则主输出闭关引脚上始终是低电平。TIM2、TIM3、TIM4没有这个限制直接TIM_Cmd即可。3. 在HAL库下配置PWM脉冲输出与更新中断3.1 HAL库初始化流程与时钟使能差异HAL库的代码风格和SPL差别很大但底层原理一一对应。使用HAL库时要先定义TIM_HandleTypeDef结构体句柄再通过HAL_TIM_PWM_Init完成时基初始化。时钟使能从SPL的RCC_APB1PeriphClockCmd变成__HAL_RCC_TIM3_CLK_ENABLE宏GPIO初始化也变成了HAL_GPIO_Init。CubeMX生成的工程通常把时基初始化和PWM通道配置拆成两个函数看起来比SPL冗长但每一段只做一件事定位问题反而更方便。需要特别留意的是HAL_TIM_PWM_Init内部会调用HAL_TIM_Base_Init做时基配置但不会配置输出通道输出通道的PWM模式、占空比和极性是在HAL_TIM_PWM_ConfigChannel里设置的。如果漏掉第二步定时器在跑引脚上却看不到波形。这种问题在论坛里反复出现排查时先确认是否调用了ConfigChannel再怀疑硬件。3.2 配置PWM通道参数并启动输出下面这段代码用TIM3_CH1在PA6上输出PWM并开启更新中断作用是每到一个PWM周期就触发一次中断回调#include stm32f1xx_hal.h TIM_HandleTypeDef htim3; TIM_OC_InitTypeDef sConfigOC; void MX_TIM3_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /* PA6复用为TIM3_CH1推挽输出 */ GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); /* 时基72MHz / 72 / 2000 500Hz */ htim3.Instance TIM3; htim3.Init.Prescaler 71; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 1999; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.RepetitionCounter 0; HAL_TIM_PWM_Init(htim3); /* PWM模式1CCR1000占空比50% */ sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 1000; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); /* 启动PWM输出并开启更新中断 */ HAL_TIM_PWM_Start_IT(htim3, TIM_CHANNEL_1); }htim3.Init.Prescaler和htim3.Init.Period的含义与SPL完全相同都是“写入寄存器值1”才等于实际分频系数或重装载值。sConfigOC.Pulse对应CCR1占空比是1000/200050%PWM周期500Hz意味着每2毫秒产生一个脉冲。把Period改成199或把Prescaler改成7频率就会跳到5kHz甚至更高具体取值取决于目标转速和驱动器允许的输入频率。3.3 更新中断回调与脉冲计数步进电机做位置控制时要数清楚脉冲个数转90度需要多少个脉冲就发多少个脉冲然后停止。HAL库把定时器更新中断统一封装到一个弱函数HAL_TIM_PWM_PulseFinishedCallback里每个PWM周期结束都会进入一次。在其中对已发送脉冲计数达到目标值后关闭PWM输出uint32_t target_pulses 0; /* 目标脉冲数 */ volatile uint32_t sent_pulses 0; /* 已发送脉冲数 */ void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3) { sent_pulses; if (sent_pulses target_pulses) { HAL_TIM_PWM_Stop(htim3, TIM_CHANNEL_1); } } } void Move_Steps(uint32_t pulses) { target_pulses pulses; sent_pulses 0; HAL_TIM_PWM_Start_IT(htim3, TIM_CHANNEL_1); }target_pulses由上层函数在启动前写入sent_pulses在中断里递增。当已发送数量达到目标值调用HAL_TIM_PWM_Stop关闭输出电机停在目标位置。sent_pulses用volatile修饰是因为它在中断上下文被修改、在主循环里有可能被读取避免编译器把它优化到寄存器里导致读到陈旧值。这里有一个隐含前提更新中断频率等于PWM频率。如果PWM频率拉到20kHz以上逐脉冲进中断的CPU开销就不能忽略此时常见做法是改用定时器主从模式或DMA来转移计数压力中断里只做方向切换和保护逻辑。TIMER工程本身面向中小频率控制场景中断计数完全够用。3.4 中断服务函数、优先级与SPL/HAL对应关系HAL库要求用户自己编写定时器中断服务函数并在其中调用HAL_TIM_IRQHandler否则标志位不会被清除回调也不会执行void TIM3_IRQHandler(void) { HAL_TIM_IRQHandler(htim3); }优先级通过HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0)和HAL_NVIC_EnableIRQ(TIM3_IRQn)设置。多电机或带其他外设的项目里定时器中断优先级要高于普通外设但中断回调里不要做串口打印这类耗时操作只更新计数和标志由主循环消费这些状态。SPL与HAL库的接口对应关系可以这样对照功能SPL接口HAL库接口GPIO复用为定时器输出GPIO_Init GPIO_Mode_AF_PPHAL_GPIO_Init时基配置TIM_TimeBaseInitHAL_TIM_PWM_InitPWM通道参数TIM_OC1InitHAL_TIM_PWM_ConfigChannel启动PWM输出TIM_CmdHAL_TIM_PWM_Start更新中断启动TIM_ITConfig NVICHAL_TIM_PWM_Start_IT从这张表能看出SPL偏底层分散需要自己手动调NVICHAL库用Start_IT一步完成启动与中断使能但代价是代码编排更重。老工程换成HAL库时按照这个映射逐项迁移比对着固件库手册翻要快很多。4. 步进电机控制参数换算与工程实战4.1 脉冲频率、步距角、细分与转速的关系步进电机的步距角常见是1.8度和0.9度前者转一圈需要200个整步脉冲。驱动器设置细分倍数m后每个PWM脉冲对应的角度变为步距角除以m转一圈需要的脉冲数变为200乘以m。转速公式随之确定转速(rpm) 脉冲频率(Hz) * 60 / (200 * m)以42步进电机配合16细分驱动板为例脉冲当量是1.8/160.1125度转一圈需要3200个脉冲。要让电机转60rpmPWM频率需要60*3200/603200Hz。反过来如果直接给800Hz的脉冲串转速只有15rpm。调试时我先固定一个已知频率比如1kHz观察电机转向和转速是否与公式吻合再改成目标频率可以避免把参数错误混杂进排查过程。细分m脉冲当量(度)每转脉冲数1kHz对应转速(rpm)11.820030080.225160037.5160.1125320018.754.2 转角与脉冲数量中断里做位置控制位置控制的换算更直接所需脉冲数 目标角度 / (步距角 / 细分)1.8度步距角、8细分转90度需要90/(1.8/8)400个脉冲。第三章的Move_Steps函数里放入400中断到400就停机。脉冲当量这个说法在开环驱动器里很直观在EtherCAT总线的闭环步进驱动里含义稍有不同但本质仍然是“一个命令单位走多少角度”。代码里把步距角和细分定义成宏方便不同电机间切换#define STEP_ANGLE_DEG 1.8f /* 步距角单位度 */ #define MICRO_STEP 8 /* 驱动器细分 */ #define PULSE_PER_REV (360.0f / (STEP_ANGLE_DEG / MICRO_STEP))PULSE_PER_REV在初始化时被用来计算目标脉冲数例如Move_Steps(PULSE_PER_REV / 4)就是转90度。这样即使换了不同步距角或细分主逻辑不用动。4.3 高、低速边界与加减速的取舍PWM频率过低时电机会出现明显振动和噪声几个Hz到几十Hz区间整步运行几乎无法接受通常要靠细分来平滑。频率过高则受两个因素制约一是定时器ARR必须大于等于CCR否则占空比指令失真二是驱动器输入接口带宽普通光耦隔离输入一般不支持几万赫兹以上的连续脉冲。开环步进控制通常把最高频率限制在10kHz到100kHz具体以驱动器手册为准。加减速是容易被忽略的点。静止状态下直接给高频脉冲步进电机会因为跟不上脉冲而堵转丢步。常见做法是梯形加减速加速阶段频率线性升高匀速阶段保持减速阶段对称下降。TIMER工程没有专门做加减速但用更新中断扩展很容易在回调里对脉冲计数分段判断到达切换点后重写ARR就能实现近似梯形曲线。重写ARR前先关闭定时器改完再重新使能避免计数器运行到一半时周期突变导致输出一个异常宽度的脉冲。4.4 方向与使能信号的处理多数步进驱动器都有DIR方向引脚和ENA使能引脚。方向切换要在脉冲发送之前完成最好预留几微秒的建立时间。软件里先把方向GPIO拉到目标电平再调用Move_Steps启动PWM。使能信号一般低有效电机停止后可以关闭ENA让电机进入释放状态降低发热但如果系统要求停机后保持力矩就不能关只能让PWM继续输出但频率置零。这两种模式对应不同的机械场景夹紧机构用保持力矩传送带则可以释放。驱动器一般还会输出故障信号过流、过压或过热时拉低。把这个引脚接到STM32的外部中断输入中断里立刻HAL_TIM_PWM_Stop所有通道是最直观的PWM故障保护方式。处理这类信号时要注意故障信号的电平逻辑是推挽还是开漏开漏输出通常需要外部上拉电阻否则单片机读到的电平会不稳定。5. 从TIMER工程源码看Keil项目结构与排错技巧5.1 文件清单暴露的工程信息这个工程的文件列表很有代表性TIMER.uvgui.Administrator是Keil的窗口布局配置TIMER.uvproj.bak和TIMER_uvopt.bak是项目文件与选项配置的备份TIMER.axf是编译生成的带调试信息可执行文件keilkilll.bat是清理中间产物的批处理脚本。出现stm32f10x_tim.c说明定时器驱动来自标准外设库stm32f10x_flash.c则用于读取片内Flash中的配置参数比如目标转速和细分值。拿到这类工程先用keilkilll.bat清掉历史编译产物再重新编译能排除旧文件干扰。如果打开后编译报错找不到头文件优先检查Keil里C/C选项卡的Include Paths是否还指向本机实际路径路径变了是换电脑后最常见的错误。5.2 排错的三个入手点第一用调试器直接看寄存器。打开Keil的Peripherals菜单找到对应定时器实时观察CNT、PSC、ARR、CCR1。CNT不递增说明定时器没启动或时钟没配置CNT在跑但引脚没波形检查GPIO复用和AFIO时钟。换成HAL库后还要确认是否执行了HAL_TIM_PWM_Start。第二检查中断链路的每一环。TIM3_IRQHandler里有没有调HAL_TIM_IRQHandler更新中断是否在NVIC层被使能回调里如果走不到关闭PWM的逻辑是不是target_pulses写入了0。把sent_pulses打印出来对比实际脉冲数是最直接的判断方式。很多“丢步”其实不是机械问题而是中断被打断导致计数滞后。第三注意高级定时器TIM1/TIM8的主输出使能。SPL工程里漏写TIM_CtrlPWMOutputsEnable是最常见的无输出原因检查这个函数是否被调用。脉冲方向反了直接对调驱动器两相绕组比在软件里分析时序更快但前提是确认方向引脚电平变化正确。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →