STM32定时器RCR与单脉冲模式实现步进电机精确脉冲控制

📅 2026/7/31 16:47:24
STM32定时器RCR与单脉冲模式实现步进电机精确脉冲控制
1. 项目背景与核心需求最近在做一个桌面小装置需要控制一个42步进电机精确旋转固定的角度比如让一个转盘每次转动90度。手头正好有块STM32F103的开发板第一反应就是用PWM输出脉冲来驱动步进电机驱动器。听起来很简单对吧不就是配置个定时器输出PWM嘛。但实际一上手就发现标准库或者HAL库里那些HAL_TIM_PWM_Start函数一启动就是连续输出根本停不下来。我需要的是输出固定数量的脉冲比如7200个脉冲后自动停止这样电机才能停在精确的位置上。这个问题困扰了不少从Arduino转向STM32的开发者。在Arduino里有现成的Stepper库或者AccelStepper库设置好目标步数就行。但在STM32的HAL库环境下你会发现它并没有直接提供一个“发送N个脉冲然后停止”的API。网上的资料要么讲得很散要么直接上高级的定时器编码器模式或者用中断手动翻转IO对于刚接触CubeMX和HAL库的朋友来说理解成本有点高。其实STM32的定时器功能非常强大完全可以通过其内置的“重复计数器”和“单脉冲模式”来实现这个需求而且几乎不占用CPU资源。这次我就以最常用的STM32F103C8T6为例使用CubeMX进行图形化配置手把手带你实现用PWM输出固定脉冲数来控制步进电机。这个方法稳定可靠是很多实际产品中的基础方案。2. 硬件连接与核心原理剖析在写代码之前得先搞清楚硬件怎么接以及STM32定时器是如何“数”脉冲的。2.1 硬件连接方案我使用的电机是常见的42步进电机型号如42BYGH驱动芯片是TB6600。这是一种非常经典的组合。连接方式如下STM32 GPIO-TB6600 PUL (脉冲信号输入)这是本次配置的核心我们将用一个定时器的PWM输出通道连接到这个引脚。STM32 GND-TB6600 PUL- (脉冲信号地)一定要共地确保信号基准一致。TB6600-42步进电机驱动器的A A- B B-连接到电机的四根线。细分、电流等参数根据电机铭牌在驱动器上设置好例如我设置为16细分电流1.5A。电源为TB6600提供合适的直流电源例如24V/36V注意电机和驱动器的功率匹配。这里的关键是PUL脉冲信号。TB6600每接收到一个脉冲的上升沿或下降沿可设置就会驱动电机转动一个细分步。所以控制发送脉冲的数量就等于控制了电机转动的角度。2.2 定时器如何输出“有限个”PWM脉冲STM32的通用定时器如TIM1, TIM2, TIM3, TIM4有一个叫做**重复计数器Repetition Counter, RCR**的寄存器。这是实现有限脉冲输出的关键。普通PWM输出可以理解为定时器根据你设置的频率ARR寄存器和占空比CCR寄存器在通道引脚上循环输出高低电平。这个循环是无限的。而重复计数器的作用是让这个“输出循环”只执行(RCR1)次。当计数器从0向上计数到ARR值算一个周期输出一个完整的PWM脉冲。重复计数器会记录这个周期数当达到了设定的次数后定时器便自动停止或者产生一个更新事件Update Event后停止。举个例子假设ARR设置为999那么PWM频率 定时器时钟 / (999 1)。设置RCR 7199。那么定时器会输出 (7199 1) 7200 个完整的PWM周期后停止。这就正好输出了我们需要的7200个脉冲。另一个需要搭配的功能是单脉冲模式One Pulse Mode, OPM。当使能OPM后定时器在发生一次更新事件比如RCR计数满后会自动停止计数而不会重新开始。这确保了我们的脉冲序列只发送一轮非常干净利落。所以我们的方案核心就是利用定时器的PWM输出模式配合重复计数器RCR设定脉冲总数并启用单脉冲模式OPM。配置好后只需要启动一次定时器它就会自动发送指定数量的脉冲然后停下。3. CubeMX工程配置详解我们打开STM32CubeMX新建一个工程选择你的芯片型号我的是STM32F103C8T6。3.1 时钟树配置首先配置系统时钟。在RCC设置中将HSE外部高速时钟设置为“Crystal/Ceramic Resonator”。然后进入Clock Configuration标签页。 对于STM32F103一个常见的配置是HSE输入8MHz通过PLL倍频到72MHz作为系统时钟SYSCLK。APB1总线时钟定时器时钟主要来源最高36MHzAPB2总线时钟最高72MHz。确保你的定时器挂载在APB2上如TIM1这样可以得到72MHz的时钟源提供更高的PWM频率分辨率。3.2 定时器与PWM输出配置假设我们使用TIM1的通道1对应PA8引脚来输出PWM脉冲。在Pinout视图找到PA8将其功能设置为“TIM1_CH1”。打开TIM1配置在左侧Categories中找到“TIM1”进入其配置。选择时钟源在“Clock Source”中选择“Internal Clock”。参数配置切换到“Parameter Settings”标签页。Prescaler (分频系数) 设置为71。计算逻辑定时器时钟为72MHz分频后得到72MHz / (711) 1MHz的计数频率。这样计数器每微秒计数一次方便计算。Counter Mode (计数模式) “Up” (向上计数)。Counter Period (自动重载值 ARR) 设置为999。这意味着每个PWM周期有1000个计数点 (0~999)。结合上面的分频PWM频率 1MHz / 1000 1kHz。这个频率对于大多数步进电机驱动器如TB6600 接收脉冲最高可达200kHz来说非常安全且常用。Auto-reload preload (自动重载预装载) 务必选择“Enable”。这能确保你对ARR、RCR的修改在下次更新事件时才生效避免产生中间状态的错误脉冲。重复计数器配置Repetition Counter (RCR) 这是我们控制脉冲总数的关键。我们先填一个值比如99。这代表会输出991100个脉冲。这个值我们后续在代码中动态修改。注意这个选项可能被折叠需要点击“Parameter Settings”旁边的“Show Advanced Parameters”才会出现。PWM输出通道配置在下方找到“Channel 1”模式选择“PWM Generation CH1”。Pulse (占空比 CCR1) 设置为500。占空比 500 / 1000 50%。对于步进驱动器通常需要20%-50%的占空比方波50%是一个很稳妥的值。CH Polarity (通道极性) 保持“High”即有效电平为高。这表示在计数值小于CCR1时输出高电平。关键模式使能One Pulse Mode (单脉冲模式) 在“Master/Slave Mode”或者“Trigger Options”附近找到“One Pulse Mode”将其设置为 “Enable”。在CubeMX的不同版本中这个选项的位置可能略有不同仔细找一下。刹车和死区 对于TIM1这种高级定时器如果不需要刹车功能在“Break and Dead Time”配置中将“Break Function”设为“Disable”。配置完成后你可以在右侧的引脚图上看到PA8已经被标记为TIM1_CH1。生成的初始化代码会自动将这个引脚配置为复用推挽输出。3.3 生成工程代码点击“Project Manager”设置好工程名称、路径、IDEMDK-ARM V5。在“Code Generator”中建议选择“Copy only necessary library files”以保持工程简洁并勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”这样定时器的代码会独立成文件结构清晰。最后点击“GENERATE CODE”。第一次生成会询问是否打开工程选择打开即可进入Keil MDK。4. 代码实现与逻辑解析打开工程后我们主要关注两个文件tim.c和main.c。4.1 生成的定时器初始化代码分析在tim.c中找到MX_TIM1_Init函数。CubeMX已经帮我们生成了完整的配置包括时钟使能、时基单元设置、输出比较通道配置和单脉冲模式使能。你需要检查一下关键参数是否和我们设定的一致htim1.Instance TIM1; htim1.Init.Prescaler 71; htim1.Init.CounterMode TIM_COUNTERMODE_UP; htim1.Init.Period 999; // ARR htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 99; // RCR初始值 htim1.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE;在通道配置部分可以看到占空比设置sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 500; // CCR1 sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCNPolarity TIM_OCNPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; sConfigOC.OCIdleState TIM_OCIDLESTATE_RESET; sConfigOC.OCNIdleState TIM_OCNIDLESTATE_RESET;以及单脉冲模式的使能if (HAL_TIM_OnePulse_Init(htim1, TIM_OPMODE_SINGLE) ! HAL_OK) { Error_Handler(); }这里的TIM_OPMODE_SINGLE是单脉冲模式。注意这个函数HAL_TIM_OnePulse_Init内部也会调用HAL_TIM_PWM_Init所以PWM通道的初始化也包含在内了。4.2 主程序逻辑与控制函数现在来到main.c。我们首先需要定义一个函数用来设置脉冲数量并启动定时器。步骤一编写脉冲发送函数在/* USER CODE BEGIN 4 */区域添加以下函数/** * brief 发送指定数量的PWM脉冲 * param htim: 定时器句柄指针 * param pulses: 需要发送的脉冲数量 * retval HAL状态 */ HAL_StatusTypeDef PWM_Start_Pulses(TIM_HandleTypeDef *htim, uint32_t pulses) { /* 1. 检查参数有效性 */ if (pulses 0 || pulses 0xFFFF) { // RCR是16位寄存器最大值65535 return HAL_ERROR; } /* 2. 停止定时器确保从停止状态开始 */ HAL_TIM_PWM_Stop(htim, TIM_CHANNEL_1); /* 3. 设置重复计数器值RCR pulses - 1 */ __HAL_TIM_SET_REPETITIONCOUNTER(htim, pulses - 1); /* 4. 清除可能存在的更新中断标志 */ __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_UPDATE); /* 5. 启动PWM输出单脉冲模式已使能输出完指定脉冲数后会自动停止 */ return HAL_TIM_PWM_Start(htim, TIM_CHANNEL_1); }代码逻辑解析参数检查RCR寄存器是16位的所以理论最大脉冲数是65536。实际使用中脉冲数受限于电机和驱动器的性能通常几千到几万。停止定时器这是一个好习惯。确保定时器处于停止状态再修改其参数防止产生混乱的输出。设置RCR这是核心。我们使用HAL库的宏__HAL_TIM_SET_REPETITIONCOUNTER来设置RCR值。因为定时器输出(RCR1)个脉冲所以我们要传入pulses - 1。清除更新标志在启动前清除更新中断标志位避免一启动就误触发更新事件。启动PWM调用HAL_TIM_PWM_Start。由于我们在CubeMX中使能了单脉冲模式定时器会在输出完(RCR1)个脉冲后自动停止无需我们手动干预。步骤二在主循环中调用在main函数的while (1)循环中我们可以这样测试/* USER CODE BEGIN 2 */ // 发送7200个脉冲电机旋转一圈假设驱动器设置为16细分电机为200步/圈则7200脉冲 7200/16/200 2.25圈 if (PWM_Start_Pulses(htim1, 7200) ! HAL_OK) { // 错误处理例如点亮一个LED HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); } // 等待脉冲发送完成。可以通过查询定时器使能状态或者使用延时。 // 方法1简单延时。计算发送7200个1kHz脉冲所需时间7200 / 1000Hz 7.2秒 HAL_Delay(8000); // 给予充足余量 // 方法2查询定时器状态更精确 // while (__HAL_TIM_GET_FLAG(htim1, TIM_FLAG_UPDATE) RESET) {} // 等待更新标志置位 // __HAL_TIM_CLEAR_FLAG(htim1, TIM_FLAG_UPDATE); // 然后可以发送下一组脉冲... // PWM_Start_Pulses(htim1, 3600); // 旋转半圈 /* USER CODE END 2 */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ // 主循环可以处理其他任务比如按键扫描、通信等 // 脉冲发送是硬件自动完成的不阻塞CPU。 }4.3 使用中断获取发送完成状态上面的例子用了延时或轮询标志位来等待发送完成这在实际项目中可能不够高效。更好的方法是使用更新中断Update Interrupt。当重复计数器溢出即脉冲发送完毕时会产生一个更新事件如果中断使能就会进入中断服务函数。配置步骤CubeMX中使能中断回到TIM1配置在“NVIC Settings”中勾选“TIM1 update interrupt”并设置合适的优先级。重新生成代码CubeMX会在tim.c中生成中断使能代码HAL_TIM_Base_Start_IT(htim1)但我们需要的是PWM模式下的更新中断。更简单的做法是我们自己在代码中启动中断。修改启动函数HAL_StatusTypeDef PWM_Start_Pulses_IT(TIM_HandleTypeDef *htim, uint32_t pulses) { if (pulses 0 || pulses 0xFFFF) { return HAL_ERROR; } HAL_TIM_PWM_Stop(htim, TIM_CHANNEL_1); __HAL_TIM_SET_REPETITIONCOUNTER(htim, pulses - 1); __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_UPDATE); // 先开启更新中断再启动PWM __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE); return HAL_TIM_PWM_Start(htim, TIM_CHANNEL_1); }编写中断回调函数在stm32f1xx_it.c中找到TIM1_UP_IRQHandler函数它内部会调用HAL_TIM_IRQHandler。HAL库的中断处理逻辑最终会调用一个弱定义的回调函数。我们在main.c中重写这个回调函数/* USER CODE BEGIN 4 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM1) { // 脉冲发送完成 __HAL_TIM_DISABLE_IT(htim, TIM_IT_UPDATE); // 关闭更新中断防止重复进入 // 可以设置一个标志位通知主程序或执行下一步动作 // 例如motor_pulses_done 1; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 翻转LED指示 } } /* USER CODE END 4 */这样主程序调用PWM_Start_Pulses_IT(htim1, 7200)后就可以立即去执行其他任务。7.2秒后中断函数会自动被调用我们在中断里处理完成后的动作如切换任务状态、点亮指示灯等效率高得多。5. 实测中的关键问题与调试技巧理论配置完成下载程序到板子用示波器或者逻辑分析仪探头连接到PA8引脚你可能会遇到一些意想不到的情况。5.1 第一个脉冲宽度异常问题现象使用单脉冲模式OPM时你可能会发现第一个PWM脉冲的宽度高电平时间异常不是设定的50%500个计数点而是一个很短或很长的脉冲。根因分析这个问题非常经典。当定时器从停止状态启动时计数器CNT从0开始计数。但输出比较通道的生效需要时间并且和计数器当前值、比较寄存器CCR的值关系复杂。在单脉冲模式下定时器硬件为了确保只输出一个“脉冲包”包含RCR1个周期对第一个周期的处理逻辑有些特殊可能导致第一个脉冲的占空比失真。解决方案在启动PWM输出前将计数器的值手动设置为一个非零值以避开那个特殊的初始状态。通常设置为Period - 1即ARR值是一个有效的方法。修改我们的启动函数HAL_StatusTypeDef PWM_Start_Pulses(TIM_HandleTypeDef *htim, uint32_t pulses) { /* ... 参数检查、停止定时器 ... */ /* 关键修复设置计数器初始值 */ __HAL_TIM_SET_COUNTER(htim, htim-Init.Period); // 设置为ARR值 __HAL_TIM_SET_REPETITIONCOUNTER(htim, pulses - 1); __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_UPDATE); return HAL_TIM_PWM_Start(htim, TIM_CHANNEL_1); }实测表明这个操作能有效稳定第一个脉冲的宽度使其与后续脉冲一致。5.2 脉冲数量总是多一个或少一个现象设置发送100个脉冲实际用逻辑分析仪计数可能是101个或99个。排查思路检查RCR公式确认是RCR pulses - 1。如果你不小心设置成RCR pulses就会多输出一个脉冲。检查边沿计数TB6600驱动器可以设置是上升沿还是下降沿触发。我们的PWM是50%占空比的方波上升沿和下降沿数量一样。但如果你的占空比不是50%或者驱动器只对其中一种边沿敏感就需要确认你数的“脉冲”和驱动器识别的“有效沿”是否一致。通常驱动器默认为上升沿触发。确保你用逻辑分析仪计数的也是上升沿。检查单脉冲模式确认OPM确实被正确使能。如果没有使能OPM定时器在RCR计数满后会产生一个更新事件然后重新装载RCR和ARR继续下一轮输出这就变成了循环输出固定数量的脉冲而不是只输出一次。这会导致你看到无数组脉冲序列计数就乱了。5.3 如何动态改变脉冲频率有时我们需要改变电机转速即改变PWM脉冲的频率。这需要修改定时器的ARR值因为预分频器PSC通常固定以获得较好的计时分辨率。重要原则在定时器停止时修改ARR或PSC。同时由于我们使用了自动重载预装载AutoReloadPreload修改ARR后新的值会在下一次更新事件时才生效这保证了波形切换的平滑性。可以写一个函数来改变频率void PWM_Set_Frequency(TIM_HandleTypeDef *htim, uint32_t freq_hz) { // 假设定时器时钟为72MHz预分频器PSC已固定为71得到1MHz计数时钟 uint32_t timer_clock 72000000 / (71 1); // 计算当前计数频率 1MHz uint32_t arr_value (timer_clock / freq_hz) - 1; // 停止定时器 HAL_TIM_PWM_Stop(htim, TIM_CHANNEL_1); // 修改ARR __HAL_TIM_SET_AUTORELOAD(htim, arr_value); // 为了保证占空比不变可能需要同步修改CCR。假设保持50%占空比 __HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, arr_value / 2); // 重新设置计数器初始值规避第一个脉冲问题 __HAL_TIM_SET_COUNTER(htim, arr_value); }注意改变频率后发送固定脉冲数所需的时间也会变。例如从1kHz变为2kHz发送7200个脉冲的时间就从7.2秒缩短到3.6秒。5.4 没有示波器/逻辑分析仪如何调试如果没有硬件调试工具可以借助一个简单的“软件脉冲计数”方法来验证。在更新中断回调函数里对一个全局变量进行累加并通过串口打印出来。volatile uint32_t pulse_counter 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM1) { pulse_counter; // 每发送完一组脉冲RCR1个会进入一次中断 // 如果你设置RCR99这里pulse_counter加到1代表100个脉冲发完了。 if(pulse_counter 1) { // 根据你的需求判断 __HAL_TIM_DISABLE_IT(htim, TIM_IT_UPDATE); // 通过串口打印信息 printf(Pulses sent completed.\r\n); pulse_counter 0; } } }同时在主函数里启动定时器更新中断HAL_TIM_Base_Start_IT(htim1)和PWM输出HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1)但不开启单脉冲模式并将RCR设为一个较小的值如9。这样定时器每输出10个脉冲就会进入一次中断并打印你可以通过观察串口输出的频率来间接验证脉冲输出是否正常。注意这只是调试手段最终产品代码要改回单脉冲模式。6. 方案对比与进阶思考6.1 与其他实现方式的对比纯软件模拟延时翻转IO在循环里用HAL_Delay和HAL_GPIO_TogglePin产生脉冲。这是最入门但最差的方法CPU被完全阻塞无法处理其他任务且定时精度受中断和系统负载影响极大不适用于任何严肃的场合。定时器中断翻转IO配置一个定时器在更新中断里手动翻转脉冲引脚。相比纯软件模拟定时更准但每个脉冲都要进一次中断当脉冲频率高时如10kHz中断频率也是10kHz会消耗大量CPU资源导致系统响应变慢。PWM 外部中断计数用一路PWM连续输出再用另一个IO的外部中断对脉冲上升沿计数计满后关闭PWM。这种方法需要占用两个硬件资源一个定时器通道和一个外部中断并且关闭PWM的时机可能滞后导致多输出一两个脉冲。本文方案PWM RCR OPM硬件全自动完成。CPU只需要发起一次“开始”命令硬件定时器就会精确地输出指定数量的脉冲然后自动停止。整个过程零CPU干预精度极高资源占用最小仅一个定时器通道。这是最优的解决方案。6.2 扩展到多轴控制与加减速在实际的CNC或3D打印机项目中需要多轴X, Y, Z同步控制并且电机需要平滑的加减速S曲线或梯形曲线以避免失步和抖动。多轴控制可以为每个步进电机分配一个独立的定时器如TIM1, TIM2, TIM3, TIM4每个定时器独立配置其RCR和频率。通过协调启动多个定时器可以实现多轴联动。更复杂的轨迹规划则需要上位机或更强大的算法。加减速控制本文的固定频率输出适用于匀速运动。要实现加减速就需要动态地改变PWM的频率。这可以在定时器的更新中断中完成。例如建立一个“速度曲线表”中断服务函数根据当前时间查表得到目标频率然后动态修改定时器的ARR值。由于修改ARR会影响脉冲总数因此需要更复杂的算法来同时管理速度曲线和位置规划总脉冲数。通常会使用专门的步进电机驱动芯片如TMC2209, DRV8825或集成运动控制库如SPI接口的步进驱动来简化这部分工作STM32则作为主控发送方向和步进脉冲。6.3 关于高级定时器与通用定时器本例使用了TIM1它是一个高级定时器具有重复计数器RCR。STM32F103的通用定时器TIM2, TIM3, TIM4没有重复计数器寄存器。那么通用定时器如何实现有限脉冲输出呢对于通用定时器常用的方法是使用主从定时器门控模式或DMA传输。主从模式将一个定时器TIM_A配置为PWM输出模式作为“从”定时器。将另一个定时器TIM_B配置为单脉冲输出模式其输出作为TIM_A的触发源和门控信号。通过控制TIM_B的输出脉冲宽度即高电平时间来控制TIM_A输出PWM的时长间接控制脉冲数。这种方法更复杂但可行。DMA模式将PWM脉冲的占空比数据如一组0和1交替的数组通过DMA循环发送到定时器的捕获/比较寄存器。通过控制DMA传输的数据量来控制脉冲数量。这种方法灵活性高但需要仔细配置DMA并且会占用一定的内存和DMA资源。因此如果项目需要控制多个步进电机且脉冲数固定优先选择具有RCR的高级定时器如TIM1, TIM8或者型号更丰富的定时器如STM32F4系列其通用定时器也有RCR。