简介:面向STM32嵌入式初学者与电机控制实践者的步进电机工程包,围绕GPIO连接步进电机驱动器、双按键正反转、步进序列与定时器调速等基础环节展开,可用于课程实验、毕业设计前期验证和外设入门练习。压缩包共156个文件,约4.16MB,含38个h头文件、37个c源文件及d、o、crf等编译中间文件,并保留uvprojx工程、hex可执行文件、sct与map等配置产物,结构接近完整Keil工程,便于对照源码与编译结果。已有3505人学习,说明其在步进电机入门场景中具有一定参考价值。读者可据此理解四相电机接口设计、按键中断处理、正反转序列组织及HAL/LL库配置思路,观察两个电机如何随按键同步执行正反转,并借助现成工程快速搭建实验平台,进一步扩展到双电机同步、精确位置控制和速度调节等应用。
1. 从一块只会抖的步进电机说起:stm32 步进电机控制真正的分水岭在哪
车间里最常见的场景是这样:STM32 板子焊好,驱动模块指示灯亮着,烧进一个 GPIO 翻转的 demo,电机嗡一声转起来,看着挺顺,可一旦挂上丝杆、加个负载,位置就开始偏——跑十次回原点九次对得上,一次差半毫米。多数人此时的第一反应是换更大电流的驱动、换更贵的电机,其实问题基本都出在脉冲时序和加减速上。用 stm32 步进电机做定位,硬件链路只是入场券,能不能每次都稳稳停在同一个脉冲数上,拼的是定时器分频怎么写、速度曲线怎么切、限位和回零怎么设计。这份资源围绕的正是这条链路:能直接编译的 HAL 工程、定时器脉冲输出、梯形加减速、回零状态机,适合刚拿到驱动模块不知道从哪下手的新手,也适合电机已经转起来、位置精度始终差一口气的老手。
2. 硬件链路定下来:脉冲接口、细分与电流的三条线
STM32 到步进电机之间隔着一级驱动器,这一级把弱电脉冲放大成绕组电流。整条链路真正要操心的信号只有三个:STEP(每来一个脉冲走一步)、DIR(电平决定正反转)、EN(使能,多数模块低有效)。剩下的事情全在参数里:细分决定一个脉冲对应多少机械角度,电流决定电机能不能带动负载,而脉冲由谁产生决定了时序抖动有多大。这三件事没定死之前就去写代码,后面基本是在给自己埋雷。
2.1 为什么脉冲必须交给定时器,而不是 GPIO 翻转
先用 GPIO 翻转拉脉冲是最常见的起步方式,for循环里置高、延时、置低、再延时,逻辑一看就懂。问题在于这个循环的时间精度由中断延迟和编译优化决定,72MHz 的片子在裸机状态下抖动能到几百纳秒,一旦开了串口接收中断或者 SysTick 抢占,抖动直接上到微秒级。对步进电机来说,每个脉冲的宽度变化会直接体现在绕组的电流建立时间上,低速时看不出来,频率一上到 10kHz 以上,就容易出现丢步、啸叫甚至闷停。
定时器输出比较是硬件行为,计数器与比较寄存器在时钟驱动下比对,翻转时刻由硬件决定,抖动只跟时钟源有关,基本可以忽略。配置分两步:先把定时器初始化成 PWM 模式,用自动重装载值(ARR)决定周期,用比较值(CCR)决定占空比;再写一个改频率的函数,每次改频率只动 ARR 和 CCR,不动通道配置,避免重新初始化时多出半个脉冲。
#include "stm32f1xx_hal.h" TIM_HandleTypeDef htim3; /* 注意:72MHz 是 TIM3 挂在 APB1、且 APB1 预分频不为 1 时的定时器时钟, 换型号或改时钟树之前先核对参考手册里的 TIMxCLK,别照抄 */ void step_tim_init(uint32_t start_freq_hz) { __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /* STEP 脚:PA6 复用为 TIM3_CH1 输出,直接接驱动器 STEP 输入 */ GPIO_InitTypeDef gpio = {0}; gpio.Pin = GPIO_PIN_6; gpio.Mode = GPIO_MODE_AF_PP; /* 复用推挽,交给定时器驱动 */ gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &gpio); htim3.Instance = TIM3; htim3.Init.Prescaler = 0; /* 不分频:计数频率 = 72MHz */ htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 0; /* 占位,由频率函数重算 */ htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim3); TIM_OC_InitTypeDef oc = {0}; oc.OCMode = TIM_OCMODE_PWM1; oc.Pulse = 0; oc.OCPolarity = TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(&htim3, &oc, TIM_CHANNEL_1); step_set_freq(start_freq_hz); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); } /* 按目标频率重算 ARR 与 CCR,占空比固定 50% */ void step_set_freq(uint32_t freq_hz) { if (freq_hz == 0U) { __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0); return; } uint32_t arr = (72000000UL / freq_hz) - 1U; if (arr < 100U) arr = 100U; /* 上限约 720kHz,别超过驱动器光耦的接收能力 */ __HAL_TIM_SET_AUTORELOAD(&htim3, arr); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, arr / 2U); __HAL_TIM_SET_COUNTER(&htim3, 0U); /* 清零计数器,避免切换瞬间多出半个脉冲 */ }这段代码里Prescaler = 0是关键,一预分频就把频率分辨率拉粗了,改速度时容易出现「算出来 12kHz、示波器量出来 11.3kHz」的偏差。arr < 100的保护不能省,频率算高了驱动器直接收不到,表现为电机不转但定时器还在跑。HAL_TIM_SET_COUNTER清零这一步看似多余,实际在高频段换速时会明显减少多走一步的概率。
2.2 细分与电流:先把脉冲当量算清楚再动手
细分这个参数最容易被忽略。1.8° 步距角的两相电机整步走一圈是 200 个脉冲,细分 16 就是 3200 个脉冲一圈。细分不是为了「更准」,它是用绕组电流的中间状态把整步之间的角度插出来,本质是用力矩换平滑度:细分越高,每个脉冲对应的电流变化越小,输出力矩下降越明显,高细分下带不动额定负载是常有的事。常见做法是先上 8 或 16,把运动和定位逻辑跑通,再根据噪声和定位指标往高调。
| 细分数 | 每圈脉冲(1.8°) | 导程 5mm 丝杆每毫米脉冲 | 典型场景 |
|---|---|---|---|
| 1 | 200 | 40 | 只要转起来,噪声大 |
| 8 | 1600 | 320 | 轻载、追求速度 |
| 16 | 3200 | 640 | 定位平台最常用 |
| 32 | 6400 | 1280 | 低噪声低速场景 |
| 64 | 12800 | 2560 | 分辨率优先,力矩显著下降 |
这张表按丝杆直连、无减速比计算,实际脉冲当量 = 每圈脉冲 ÷ 丝杆导程 ÷ 减速比。写代码之前先把这一行算出来,后面所有「走 10mm 需要多少个脉冲」的问题都能直接套,不用每次翻计算器。电流设定看驱动器上的 Vref 点,常见做法是按采样电阻换算,用万用表顶在 Vref 和 GND 之间慢慢拧电位器,先按电机额定电流的 70% 起步,跑一段时间摸电机外壳温度,烫手就再降一档,一直降到温热但不烫为止。
2.3 接线、电平与共地:这三件事错一件就白干
接线阶段按顺序过一遍,比事后拿示波器抓信号省事得多:
- 确认驱动器 STEP/DIR 输入是光耦还是直接电平输入,光耦输入一般要 5~10mA 的驱动电流,STM32 的 3.3V 推挽口带得动但余量不大,串 220Ω 限流比较稳妥;
- 驱动器逻辑地和 MCU 地必须共地,不共地时 STEP 信号没有参考点,表现是时转时不转;
- EN 脚确认有效电平,多数模块低有效,上电默认拉低会让电机在没收到指令时也保持锁轴发热;
- 电机绕组用万用表量通断确认两两一组,接错一组会出现「转是转,但是没劲」的典型症状;
- 驱动电源与 MCU 电源分开,中间加共模电感和 TVS,电机启停的反电动势是后面死机复位的主要原因。
3. 定时器方案落地:从固定频率到梯形加减速
固定频率跑定位在低速、轻载时能用,一旦速度上去,启动瞬间就是一次「硬拽」,电机要么失步要么直接堵转。步进电机的输出力矩随转速上升而下降,加减速曲线的作用是在低速段留出建立力矩的时间,让电机始终工作在力矩够用的区间。这一节把脉冲生成从「一个固定 ARR」推进到「每一步都换一次 ARR」。
3.1 定时器中断里数脉冲,主循环只负责下指令
PWM 硬件负责时序,软件负责计数,两者分工清楚。每产生一个脉冲,定时器的更新中断进一次,把已走步数加一,走完就停 PWM。这种方式的好处是中断里只做加法和一次比较,开销极小,坏处是频率越高中断越密,20kHz 时每 50 微秒进一次,中断服务函数里绝对不能再放浮点运算和串口打印。
volatile uint32_t g_pulses_done = 0; volatile uint32_t g_pulses_total = 0; volatile uint8_t g_motion_busy = 0; /* TIM3 更新中断:每来一个脉冲进入一次 */ void TIM3_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(&htim3, TIM_FLAG_UPDATE) && __HAL_TIM_GET_IT_SOURCE(&htim3, TIM_IT_UPDATE)) { __HAL_TIM_CLEAR_IT(&htim3, TIM_IT_UPDATE); if (g_motion_busy) { g_pulses_done++; if (g_pulses_done >= g_pulses_total) { HAL_TIM_PWM_Stop_IT(&htim3, TIM_CHANNEL_1); g_motion_busy = 0; /* 这里只置标志,回零状态机的处理放到主循环 */ g_motion_done_flag = 1; } else { motion_on_step(); /* 见 3.2,更新下一步频率 */ } } } } /* 主循环下发一次运动指令 */ void motion_start(uint32_t pulses, uint8_t dir) { if (g_motion_busy) return; /* 忙的时候直接丢,别排队 */ set_dir(dir); g_pulses_done = 0; g_pulses_total = pulses; g_motion_busy = 1; step_set_freq(STEP_FREQ_MIN); HAL_TIM_PWM_Start_IT(&htim3, TIM_CHANNEL_1); }g_motion_busy这个标志是整个运动模块的总闸,回零状态机、手动点动、上位机指令都要先看它一眼。中断里只做计数和停止判定,g_motion_done_flag交给主循环处理,是因为回零流程里还有延时、方向切换、二次确认这些需要「慢」的操作,放进中断会把时序彻底搅乱。
3.2 梯形曲线:用 v² 递推把加速段写对
加减速的数学基础是匀加速运动,离散到每个脉冲上,就是「每走一步,速度平方加上 2a」。这个递推式不需要知道时间,每一步直接算出下一步该用多大频率,比按时间片查表更贴合脉冲计数的实际执行方式。
#include <math.h> #define STEP_FREQ_MIN 800U /* 起步频率,低于这个值电机只抖不转 */ #define STEP_FREQ_MAX 20000U /* 匀速段上限,按电机力矩曲线留余量 */ #define STEP_ACCEL 40000.0f /* 加速度,单位:脉冲/秒² */ static uint32_t g_cur_freq = STEP_FREQ_MIN; /* 每个脉冲结束后调用:更新下一步频率 */ void motion_on_step(void) { uint32_t remain = g_pulses_total - g_pulses_done; /* 从当前速度刹到起步速度需要的步数,用来决定何时开始减速 */ float brake_f = ((float)g_cur_freq * g_cur_freq - (float)STEP_FREQ_MIN * STEP_FREQ_MIN) / (2.0f * STEP_ACCEL); uint32_t brake_steps = (uint32_t)brake_f; if (remain > brake_steps && g_cur_freq < STEP_FREQ_MAX) { /* 加速:f_next = sqrt(f² + 2a) */ float f = (float)g_cur_freq * g_cur_freq + 2.0f * STEP_ACCEL; g_cur_freq = (uint32_t)sqrtf(f); if (g_cur_freq > STEP_FREQ_MAX) g_cur_freq = STEP_FREQ_MAX; } else if (remain <= brake_steps && g_cur_freq > STEP_FREQ_MIN) { /* 减速:对称地把频率降回去 */ float f = (float)g_cur_freq * g_cur_freq - 2.0f * STEP_ACCEL; g_cur_freq = (f > (float)STEP_FREQ_MIN * STEP_FREQ_MIN) ? (uint32_t)sqrtf(f) : STEP_FREQ_MIN; } /* 匀速段两个分支都不进,频率保持不变 */ step_set_freq(g_cur_freq); }三个参数的关系是:STEP_FREQ_MIN定得越低越平滑,但低到 200Hz 以下时电机绕组电流还没建立起来就被下一拍打断,声音会变得又闷又拖;STEP_FREQ_MAX要对照电机的力矩-频率曲线取,42 电机带丝杆在 20kHz 以上基本没有可用力矩;STEP_ACCEL越大加速越猛,起步电流也越大,驱动电源功率不够时会直接触发过流保护。调的时候从小的加速度开始,逐步往上加,加到启动瞬间能听到轻微的「哒」但位置不丢,就是这台机器的极限。
brake_steps的计算是整个算法的核心,它保证电机不会「冲过头才发现要刹车」。很多丢步案例都是加速段写对了、减速段用固定步数一刀切,结果负载一变就刹不住。
3.3 多轴联动的 Bresenham 分配
两个轴要走直线的时候,总不能一个轴走完另一个轴再走,那画出来就是台阶。常见做法是用 Bresenham 算法把主轴和副轴的脉冲按比例插进同一个时基里,每个定时器周期判断这一拍该推哪个轴。
typedef struct { uint32_t ax_remain; /* 主轴剩余脉冲 */ uint32_t ay_remain; /* 副轴剩余脉冲 */ uint32_t n; /* 主轴总脉冲,作为累加基准 */ int32_t err; /* 累加器 */ uint32_t dy; } bres_t; static bres_t br; /* 在定时器更新中断里调用,替代单轴的 motion_on_step */ void bres_on_tick(void) { if (br.ax_remain) { STEP_TOGGLE_X(); br.ax_remain--; } br.err += br.dy; if (br.err >= (int32_t)br.n) { if (br.ay_remain) { STEP_TOGGLE_Y(); br.ay_remain--; } br.err -= (int32_t)br.n; } if (!br.ax_remain && !br.ay_remain) { HAL_TIM_PWM_Stop_IT(&htim3, TIM_CHANNEL_1); g_motion_busy = 0; } }br.n取两个轴脉冲数里的大值,br.dy取小值,累加器err每次加上dy,超过n就让副轴走一步再减回去。这样副轴的每一步都均匀分布在主轴脉冲之间,走出来是直线而不是阶梯。方向由主轴和副轴各自的 DIR 引脚决定,插补本身不关心方向,只关心比例。如果两轴的速度要求差别很大,比如一个轴走 10000 脉冲另一个只走 50 脉冲,这一版会退化成「副轴几乎不动、主轴匀速」,属于算法本身的边界,需要改用带速度规划的版本,不是调参数能解决的。
4. 回零、限位与状态机:让开环系统具备自检能力
步进电机是开环执行器,控制器不知道电机实际转到了哪,只知道「我发了多少个脉冲」。回零的作用就是给这个开环系统一个绝对参考点,限位的作用是在参考点失效时兜底。这两块做不好,前面调得再顺,连续跑几十次之后位置一样会漂。
4.1 回零流程:原点开关、限位与 Z 相信号的三段式
回零不是「撞一下开关就完事」。常见做法分三段:先快速朝原点方向走,碰到开关立刻停;再反向退出开关一段固定距离,让开关恢复;最后以低速再靠近一次,取开关下降沿的那一刻作为零点。中间那一段退出距离很关键,开关的释放点比触发点滞后,不退出来直接找零点,每次都会差上零点几毫米。
typedef enum { HOME_IDLE = 0, HOME_FAST, HOME_BACKOFF, HOME_SLOW, HOME_DONE, HOME_ERR } home_state_t; static home_state_t hs = HOME_IDLE; static uint32_t home_retry = 0; void homing_task(void) /* 主循环里每 1ms 调一次 */ { switch (hs) { case HOME_IDLE: home_retry = 0; hs = HOME_FAST; motion_start_continuous(HOME_DIR_FAST); /* 连续走,靠开关打断 */ break; case HOME_FAST: if (limit_active()) { motion_stop_immediate(); hs = HOME_BACKOFF; motion_start(HOME_BACKOFF_STEPS, !HOME_DIR_FAST); /* 退固定距离 */ } else if (limit_opposite_active()) { /* 走到反向限位还没找到原点 */ motion_stop_immediate(); hs = HOME_ERR; } break; case HOME_BACKOFF: if (g_motion_done_flag) { g_motion_done_flag = 0; hs = HOME_SLOW; motion_start_continuous(HOME_DIR_SLOW); } break; case HOME_SLOW: if (limit_falling_edge()) { /* 找下降沿,不是电平 */ motion_stop_immediate(); g_position_pulses = 0; /* 当前位置即原点 */ hs = HOME_DONE; } break; case HOME_ERR: if (++home_retry < 3U) hs = HOME_IDLE; /* 三次都失败就真报错 */ break; default: break; } }状态机的好处是每一步的进入条件和退出条件都是显式的,出问题时能直接打印当前处在哪个状态,比在几层if里瞎猜快得多。HOME_SLOW里用下降沿而不是电平,是因为开关在慢速靠近时可能已经抖了好几次,电平判断会把抖动的第一下当成零点。HOME_ERR里给了三次重试,是因为偶尔一次回零失败多半是开关粘滞或者机械卡点,硬报错反而误事。
4.2 限位开关消抖与硬件级急停
机械开关的抖动时间典型值在 5~20ms,直接在中断里判断电平,一次按下会被识别成十几次。软件消抖用定时器采样窗口的方式最稳:每 1ms 采一次,连续 20 次都是同一个状态才认账。
#define DEBOUNCE_CNT 20U static uint8_t limit_stable = 0; static uint8_t limit_cnt = 0; static uint8_t limit_last = 0; /* 1ms 调用一次 */ void limit_scan(void) { uint8_t now = (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET); if (now != limit_last) { limit_cnt = 0; limit_last = now; } else if (limit_cnt < DEBOUNCE_CNT) { limit_cnt++; if (limit_cnt == DEBOUNCE_CNT) { limit_stable = now; if (now) hw_emergency_stop(); /* 真的碰到限位,立刻断 EN */ } } }hw_emergency_stop()里直接拉高使能脚、关掉 PWM 输出,不经过状态机,这是硬件级的最后一道保险。软件消抖之外,限位开关的输入端最好再加一级 RC 低通(10kΩ + 100nF),把高频干扰挡在引脚外面。两条路并行做,才能既快又不误触发。
4.3 丢步判定:编码器和堵转电流两条路
开环系统本身判断不了丢步,只能靠外部信号反推。两条路最常见:一是电机尾部加增量编码器,回零后每次运动结束后比对「应到脉冲数」和「编码器实际跳变数」,差值超过阈值就重新回零;二是在驱动电源回路上串一个采样电阻,通过堵转时的电流突增判断电机被卡住,及时停下并报警,虽然定位不准但至少不会把机械撞坏。
编码器这条路成本高一些,但对症;电流检测成本低,只能防事故不能提精度。实际项目里两者经常一起用:电流检测做保护,编码器做补偿。补偿逻辑不复杂,每次运动结束后做一次位置比对,超过阈值就标记「位置不可信」,下一次运行前强制回零,比花力气做闭环插补实际得多。
5. 避坑与常见问题:五条现场排查记录
调试栈进电机的时候,现象和原因往往隔着好几层,光看现象容易往错的方向修。下面这五条是这几年在车间和实验室里反复遇到、每次都要重新回忆一遍的,按现象、原因、解决三段写,遇到对得上的直接照着查。
5.1 电机在转,但一转就知道不对劲
现象一:低速能转,速度一超过 5kHz 就开始丢步,声音变得又尖又碎。原因多半出在加速度上,频率爬升太快,电机绕组电流来不及建立,实际力矩跟不上指令。解决是把STEP_ACCEL砍一半,观察丢步是否消失;如果还是丢,就把STEP_FREQ_MAX降下来,让电机在力矩还够的区间内跑,别指望靠参数把物理极限拉过去。
现象二:电机转起来有明显的「一格一格」顿挫,位置走多了还能听到咯噔声。第一反应往往是细分不够,其实更常见的是脉冲频率落在了电机的低频共振区,通常在一两百赫兹附近。解决办法是把起步频率直接抬到 800Hz 以上,跳过共振区间;对共振特别明显的机架,把加减速曲线从梯形改成 S 形,让加速度在段首段尾平滑过渡,顿挫感会明显减弱。
5.2 位置漂移、回零失败:开环系统的典型症状
现象三:手动点动定位很准,连续跑三四十个来回后原点开始偏,越跑越偏。这种单向累积的漂移基本可以确定是丢步,而且大多发生在加速段。先量一下驱动器 Vref 是否和电机额定电流对得上,再看细分数有没有设得过高——细分 64 加轻负载的组合下,力矩下降得非常明显。把细分降到 16、按采样电阻重新算电流,再跑一边看漂移是否收敛。如果还漂,就在每段运动结束后比对应到与实际编码器的差值,超阈值强制回零,这是最省事的兜底方案。
现象四:回零时好时坏,十次里有两次撞到限位才停。原因是原点开关的触发点没有做消抖,或者在慢速靠近阶段用了电平判断而不是边沿判断。机械开关在慢速靠近时会有几次弹跳,电平判断会把第一次弹跳当成触发,实际位置就偏了。解决是把HOME_SLOW阶段的判断改成下降沿捕获,同时在limit_scan()里加上 20ms 的采样窗口;另外把回零速度降到 500Hz 以下,慢到开关一次按下只产生一次有效边沿,稳定性会好很多。
5.3 一上电就复位、限位乱触发:电气与信号层面的坑
现象五:电机一启动,MCU 就复位或者串口突然乱码,把电机线拔了又一切正常。这是血泪经验里最典型的一条:电机启停瞬间的反电动势通过共地路径串进了 MCU 电源。先确认驱动电源和 MCU 电源是不是各自独立,共地只在地线上单点汇合;再在电机绕组两端并 TVS 管或 RC 吸收网络,把尖峰压下去;最后检查 STEP/DIR 信号线有没有和电机线捆在一起走,两者平行走线超过 10cm 就等着收干扰。改完之后先用示波器看一下 MCU 供电轨上的纹波,峰峰值压到 200mV 以内再继续往下调。
6. 再往前一步:把频率计算挪出中断,用查表加 DMA 收尾
3.2 那版motion_on_step()在每个脉冲中断里都做一次sqrtf,跑 20kHz 时等于每秒 2 万次浮点开方。72MHz 的 M3 不带 FPU,这个开销会吃掉中断响应余量,表现是高频段偶尔多走一步、波形上能看到周期性的毛刺。后来我把速度曲线改成上电时预计算一张表,中断里只做查表和计数器自增,波形立刻就规整了。
表的结构很简单:每一项记录一个 ARR 值,索引按「已走步数」推进。加速段和减速段共用同一张表,减速时倒着索引就行。
| 表项 | ARR 值(72MHz) | 对应频率 | 累计步数 |
|---|---|---|---|
| 0 | 89999 | 800Hz | 0 |
| 1 | 63920 | 1126Hz | 1 |
| … | … | … | … |
| 199 | 3599 | 20000Hz | 199 |
表的长度按「从起步频率加速到最高频率所需步数」算,通常几百项就够。生成逻辑就是 3.2 里那段f² + 2a递推,只是挪到main()初始化阶段跑一遍。中断里改成:
/* 中断里只查表:table_idx 在加速段自增,在减速段自减 */ static void step_isr_fast(void) { if (g_motion_busy) { g_pulses_done++; if (g_pulses_done >= g_pulses_total) { HAL_TIM_PWM_Stop_IT(&htim3, TIM_CHANNEL_1); g_motion_busy = 0; return; } uint32_t arrive = g_pulses_done; uint32_t idx = (arrive < g_accel_len) ? arrive : (g_pulses_total - arrive < g_accel_len ? g_pulses_total - arrive : g_accel_len); __HAL_TIM_SET_AUTORELOAD(&htim3, g_freq_table[idx]); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, g_freq_table[idx] / 2U); } }更进一步可以用定时器的单脉冲模式配合 DMA,把整张表搬到 CCR 寄存器,CPU 完全不参与,只在中途改一次方向或者发停止指令。这条路适合脉冲频率需要稳定在几十 kHz 以上、同时 CPU 还要跑通信和上层逻辑的场景。
验证习惯我是这么养成的:每次改完定时器参数,先用示波器量 STEP 脚的脉冲宽度和周期,确认实际频率和算出来的对得上;再跑一百次回零,记录每次的偏差,看偏差是随机分布还是单向累积,前者是干扰,后者是丢步。这两个动作加起来不超过十分钟,能挡掉后面大半的返工。从那以后我每次改完频率相关代码,都会强制走一遍这套验证,不敢再凭「听着声音挺顺」就往下走。希望帮到你。
本文还有配套的精品资源,点击获取