简介:本资源面向STM32嵌入式开发者与运动控制方向的学习者,聚焦F103与F407两款经典型号,解决步进电机驱动中精准脉冲个数输出的问题。内容围绕定时器主从模式展开,讲解主定时器提供时基、从定时器按触发事件计数的协同机制,并涉及TRGO触发、预分频器与比较寄存器配置等关键环节,帮助读者理解如何生成指定数量与频率的PWM脉冲。压缩包共264个文件,以c与h源码为主,辅以工程编译产物、map与lst映射文件、hex固件及uvprojx工程文件,整体约1.85MB,包含F103与F407两套独立示例工程,便于对照移植。目前已有8229人学习下载,适合希望掌握库函数版定时器主从配置、提升步进电机控制精度的开发者参考实践。
1. 定时器主从模式:让STM32吐出精确到个数的脉冲
做步进电机控制的朋友大概率遇到过这个场景:想让电机严格走 2000 步,一步不多一步不少,用软件循环计数吧,中断一多就丢步;用 PWM 持续输出吧,又不知道什么时候该停。STM32 的定时器主从模式就是冲着这个痛点来的——用一个定时器当"脉冲发生器",另一个定时器当"计数器",数够了自动关闸,全程硬件完成,CPU 只管发指令和收结果。
这套方案在 F103 和 F407 上都适用,核心思路是利用定时器的级联(Cascade)和门控(Gated)模式,把两个定时器串成一条流水线。F103 主频 72MHz,F407 主频 168MHz,时钟树不同但主从触发机制一致。适合正在做步进电机驱动、伺服脉冲控制、或者任何需要"定量脉冲输出"的嵌入式开发者。下面从工程结构开始,一步步拆到能跑、能改。
2. 工程结构与时钟配置:从 CubeMX 到寄存器
2.1 两个定时器怎么分工
主从模式的核心是"谁触发谁"。以 TIM1 为主、TIM2 为从为例:TIM1 配置为 PWM 输出模式,产生方波脉冲;TIM2 配置为从模式,时钟源选外部触发(ETR 或 ITR),每来一个脉冲计一次数。当 TIM2 的计数值达到预设的脉冲个数时,触发中断,在中断里关闭 TIM1 的 PWM 输出。
这里有个关键点:TIM1 的 PWM 输出频率决定了脉冲速度,TIM2 的计数上限决定了脉冲个数。两者独立配置,互不干扰。F103 上 TIM1 是高级定时器,TIM2 是通用定时器,通过 ITR 内部触发线连接;F407 上 TIM1 和 TIM2 同样支持这种级联。
常见做法是在 CubeMX 里先把两个定时器的时钟使能,然后手动配置从模式的触发源。CubeMX 对主从模式的图形化支持有限,很多时候还是得自己写寄存器。
2.2 时钟树与预分频计算
F103 的定时器时钟来自 APB2(TIM1)和 APB1(TIM2)。APB2 通常 72MHz,APB1 通常 36MHz 但定时器倍频后也是 72MHz。F407 则更复杂一些,APB2 定时器时钟 168MHz,APB1 定时器时钟 84MHz。
假设我们要输出 10kHz 的脉冲,F103 上 TIM1 的配置如下:
// TIM1 初始化:PWM 模式,10kHz 输出 // 时钟 72MHz,预分频 71,计数频率 1MHz // 自动重装载值 99,PWM 频率 = 1MHz / (99+1) = 10kHz TIM_HandleTypeDef htim1; htim1.Instance = TIM1; htim1.Init.Prescaler = 71; // 72MHz / 72 = 1MHz htim1.Init.CounterMode = TIM_COUNTERMODE_UP; htim1.Init.Period = 99; // 1MHz / 100 = 10kHz htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter = 0; htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(&htim1); // PWM 通道配置:占空比 50% TIM_OC_InitTypeDef sConfigOC; sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 50; // 50% 占空比 sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1);预分频值 71 意味着每 72 个时钟周期计一次数,得到 1MHz 的计数频率。自动重装载值 99 意味着计到 99 就翻转,一个完整周期 100 个计数,所以 PWM 频率是 10kHz。占空比 50% 对应比较值 50,这个值决定了脉冲的高电平持续时间。
F407 上如果主频 168MHz,预分频要改成 167,其他逻辑一样。这里容易翻车的地方是忘了使能定时器时钟,或者 APB 分频系数没搞对,导致实际输出频率差一倍。
2.3 从模式触发源的选择
TIM2 作为从定时器,需要选择触发源。STM32 提供了多种从模式:复位模式、门控模式、触发模式。我们要的是"每来一个脉冲计一次数",所以选外部时钟模式 1,触发源选 ITR0(对应 TIM1)。
// TIM2 从模式配置:外部时钟模式1,触发源 ITR0 TIM_SlaveConfigTypeDef sSlaveConfig; sSlaveConfig.SlaveMode = TIM_SLAVEMODE_EXTERNAL1; // 外部时钟模式1 sSlaveConfig.InputTrigger = TIM_TS_ITR0; // ITR0 对应 TIM1 sSlaveConfig.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING; sSlaveConfig.TriggerFilter = 0; // 不滤波 HAL_TIM_SlaveConfigSynchro(&htim2, &sSlaveConfig); // TIM2 计数上限设为 2000,即 2000 个脉冲后溢出 htim2.Instance = TIM2; htim2.Init.Prescaler = 0; // 不分频,每个脉冲计一次 htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 1999; // 2000 个脉冲 htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(&htim2);ITR0 在 F103 上对应 TIM1 的 TRGO 输出,在 F407 上对应关系需要查参考手册的触发连接表。触发极性选上升沿,意味着 TIM1 输出的每个上升沿都会让 TIM2 计一次数。滤波器设为 0 表示不滤波,如果信号有毛刺可以适当增加滤波系数。
TIM2 的预分频设为 0,保证每个脉冲都被计数。Period 设为 1999,计满 2000 个脉冲后产生更新事件。这个更新事件就是我们的"停止信号"。
3. 主从联动与中断处理:脉冲数到了怎么停
3.1 启动顺序与触发链路
主从模式的启动顺序很讲究。必须先启动从定时器(TIM2),再启动主定时器(TIM1)。如果反了,TIM1 已经开始发脉冲了,TIM2 还没准备好计数,前几个脉冲就丢了。
// 正确的启动顺序 HAL_TIM_Base_Start_IT(&htim2); // 先启动从定时器,开启中断 HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); // 再启动主定时器,开始发脉冲HAL_TIM_Base_Start_IT 会开启 TIM2 的更新中断,这样计数溢出时能进中断。HAL_TIM_PWM_Start 启动 TIM1 的 PWM 输出。两行代码的顺序不能颠倒,这是血泪经验。
触发链路是这样的:TIM1 的 PWM 信号一方面从 GPIO 引脚输出到外部,另一方面通过内部 TRGO 信号传给 TIM2 的 ITR0。TIM2 每收到一个上升沿就计一次数,计到 1999 后溢出,触发更新中断。
3.2 中断里关 PWM 的时机
中断服务函数里要做的事情很简单:关掉 TIM1 的 PWM 输出,停止 TIM2 计数,然后置一个标志位告诉主循环"脉冲发完了"。
volatile uint8_t pulse_done = 0; void TIM2_IRQHandler(void) { HAL_TIM_IRQHandler(&htim2); } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { // 脉冲数到了,立即关闭主定时器 PWM 输出 HAL_TIM_PWM_Stop(&htim1, TIM_CHANNEL_1); HAL_TIM_Base_Stop_IT(&htim2); pulse_done = 1; // 通知主循环 } }这里有个细节:HAL_TIM_PWM_Stop 会立即停止 PWM 输出,但当前周期可能还没走完。如果对最后一个脉冲的完整性有要求,可以改用 HAL_TIM_PWM_Stop_IT 或者在中断里先关输出使能再等一个周期。大多数步进电机应用对最后一个脉冲的宽度不敏感,直接关就行。
pulse_done 标志位用 volatile 修饰,防止编译器优化掉。主循环里轮询这个标志,或者用信号量通知任务。
3.3 动态修改脉冲个数的技巧
实际项目中经常需要动态改变脉冲个数,比如电机要加速、减速或者改变行程。直接改 TIM2 的 Period 寄存器是可以的,但要注意时机。
// 动态修改脉冲个数:先停再从定时器,改完再启动 void set_pulse_count(uint16_t count) { HAL_TIM_Base_Stop_IT(&htim2); // 先停 __HAL_TIM_SET_AUTORELOAD(&htim2, count - 1); // 改重装载值 __HAL_TIM_SET_COUNTER(&htim2, 0); // 计数器清零 pulse_done = 0; HAL_TIM_Base_Start_IT(&htim2); // 重新启动 }__HAL_TIM_SET_AUTORELOAD 是 HAL 库提供的宏,直接写 ARR 寄存器。改完之后必须把计数器清零,否则可能立即触发溢出。这个函数要在 TIM1 停止的时候调用,否则正在发脉冲时改个数会导致时序混乱。
如果需要在运行中平滑修改,可以用 DMA 或者预装载寄存器,但复杂度会高不少。一般应用场景下,停下来改再启动就够用了。
4. 避坑与常见问题排查
4.1 脉冲个数总是多一个或少一个
现象:设了 2000 个脉冲,实际输出 1999 或 2001 个。
原因:TIM2 的计数从 0 开始,Period 设为 1999 时,计数序列是 0 到 1999,共 2000 个值。但 TIM1 的 PWM 启动和 TIM2 的计数启动之间可能有半个周期的延迟,导致第一个脉冲没被计到,或者最后一个脉冲计数后 PWM 已经多输出了一个周期。
解决:在启动 TIM1 之前先把 TIM2 的计数器清零,确保从第一个脉冲开始计数。如果还差一个,把 Period 改成 1998 或 2000 试一下,具体取决于你的启动时序。用示波器抓一下 TIM1 输出和 TIM2 计数器的关系,一目了然。
4.2 从定时器不计数
现象:TIM1 正常输出 PWM,但 TIM2 的计数器一直是 0。
原因:触发源选错了。F103 和 F407 的 ITR 映射不一样,F103 的 ITR0 对应 TIM1,但 F407 上 ITR0 可能对应 TIM2 自己。另外 TIM1 的 TRGO 输出需要配置,默认是复位事件,要改成使能事件或更新事件。
解决:查对应型号的参考手册"定时器内部触发连接"表,确认 ITR 编号。TIM1 的 TRGO 配置用 HAL_TIMEx_MasterConfigSynchronization 设置,TriggerOutput 选 TIM_TRGO_UPDATE 或 TIM_TRGO_ENABLE。
4.3 中断进不去
现象:脉冲数到了但中断没触发,PWM 一直输出。
原因:NVIC 没使能,或者中断优先级配置有问题。HAL_TIM_Base_Start_IT 只开了定时器的更新中断使能位,NVIC 的使能需要单独做。
解决:在 CubeMX 里勾选 TIM2 的全局中断,或者在代码里手动调用 HAL_NVIC_EnableIRQ(TIM2_IRQn) 和 HAL_NVIC_SetPriority。F407 上还要注意中断分组设置。
4.4 高速脉冲下丢步
现象:低频时脉冲个数准确,频率一高就少几个。
原因:中断响应延迟。TIM2 溢出后进中断需要时间,在中断里关 PWM 之前,TIM1 可能已经又发出了几个脉冲。
解决:改用硬件自动关闭。把 TIM1 的刹车输入(BKIN)配置为由 TIM2 的溢出事件触发,这样不需要 CPU 介入就能立即关断。F103 的 TIM1 和 F407 的 TIM1 都支持刹车功能,具体配置查参考手册的"刹车和死区"章节。
4.5 两个定时器时钟不同步
现象:脉冲频率和预期不符,或者计数不准确。
原因:F103 的 TIM1 在 APB2,TIM2 在 APB1,如果 APB1 分频系数不是 1,TIM2 的时钟会是 APB1 的两倍。F407 类似但更复杂。
解决:用 RCC_GetClocksFreq 确认实际时钟频率,重新计算预分频值。或者干脆把两个定时器都放在同一个 APB 总线上,比如都用 TIM1 和 TIM8(都在 APB2)。
5. 二次开发:从定长脉冲到加减速控制
5.1 改造思路:用 DMA 更新频率
定长脉冲跑通之后,下一步往往是加减速。步进电机直接全速启动容易失步,需要梯形或 S 形加减速曲线。最直接的办法是用 DMA 把频率表搬到 TIM1 的 ARR 寄存器。
// 频率表:从低速到高速再到低速 uint16_t freq_table[100] = {999, 998, 997, /* ... */, 500, /* ... */, 997, 998, 999}; // DMA 配置:内存到外设,每次传输更新 TIM1->ARR HAL_DMA_Start(&hdma_tim1_up, (uint32_t)freq_table, (uint32_t)&TIM1->ARR, 100); __HAL_TIM_ENABLE_DMA(&htim1, TIM_DMA_UPDATE);每个 PWM 周期结束后,DMA 自动把下一个 ARR 值写入,实现频率的连续变化。100 个点的频率表对应 100 个脉冲的加减速过程。这个方案的好处是不占 CPU,缺点是频率表要提前算好。
5.2 验证方法:示波器加逻辑分析仪
改完之后怎么验证?示波器看波形频率和个数,逻辑分析仪抓时序关系。重点看三个东西:脉冲总数对不对、频率变化是否平滑、停止时有没有多余脉冲。
我一般会在中断里翻转一个 GPIO,用逻辑分析仪同时抓 PWM 输出和这个 GPIO,一眼就能看出中断响应延迟和实际停止位置。如果延迟超过一个脉冲周期,就得考虑上硬件刹车了。
5.3 一个容易忽略的细节:ARR 预装载
TIM1 的 ARR 寄存器有预装载功能,写新值后不会立即生效,要等当前周期结束。这个特性在 DMA 更新频率时非常有用,保证了频率变化的平滑性。但如果在中断里直接改 ARR 又没开预装载,可能会出现周期被截断的玄学问题。
// 确保 ARR 预装载使能 TIM1->CR1 |= TIM_CR1_ARPE;这行代码在 HAL_TIM_Base_Init 里其实已经做了,但如果你手动改寄存器,别忘了这一位。从那以后我每次调定时器主从模式,都先用示波器抓一遍启动和停止的时序,确认没有多余脉冲再往下做。希望帮到你。
本文还有配套的精品资源,点击获取