简介:一套基于STM32F103与STM32F405单片机的4轴步进电机加减速控制工程源码包,面向嵌入式开发者、电机控制学习者和智能车、3D打印机、小型数控平台等运动控制项目。工程包含两个独立版本完整软件,分别适配STM32F103与STM32F405平台,可同时控制4路步进电机进行加减速,便于对照同一套算法在不同内核芯片上的移植与调度实现。压缩包共320个文件、约11.5MB,以C源文件、H头文件、O目标文件及uvproj/uvopt工程文件为主体,还包含hex可执行文件、pdf说明、txt辅助文档、map/axf映射信息等,覆盖源码、编译配置、调试烧录文件与参考文档,打开工程即可阅读和验证。代码对4轴脉冲引脚、定时器以及加减速状态控制有模块化处理,可重点研读加减速曲线规划、多轴同步控制和中断驱动方法,对设计步进电机运动控制系统具有直接参考价值。目前已有1017人学习浏览,是一份工程结构完整、适合边读边练的多轴电机控制参考资源。
1. 从双芯片选型看4轴步进电机加减速控制的工程全貌
拿到基于STM32F103_STM32F405单片机的4轴步进电机加减速控制工程软件源码.zip这样的工程包,第一反应不是解压看代码,而是先想清楚一个问题:为什么一个 4 轴步进电机控制工程要同时出现 STM32F103 和 STM32F405 两代芯片?这直接决定了工程的架构是「共用一套代码」还是「各有分工、协同工作」。从实际工程经验看,F103 通常承担脉冲生成、IO 逻辑、通讯交互这类实时性要求中等的工作,而 F405 凭借更高的主频和更强的浮点能力,更多被用在加减速曲线计算、前瞻规划、多轴联动插补这类计算密集型任务上。4 轴控制的核心难点从来不是「让电机转」,而是「让 4 个轴在加减速过程中不丢步、不过冲、协同到位」。本文围绕这个工程标题,把选型、加减速算法、代码组织、参数整定和排错路径完整过一遍。适合正在做步进电机控制、运动控制卡替代方案、3D 打印机固件移植或小型数控机床电控的嵌入式工程师。
2. STM32F103 与 STM32F405 分工背后的引脚与定时器资源规划
2.1 为什么 4 轴步进电机控制必须仔细分配定时器
步进电机控制最常规的方案是「脉冲方向」模式:每给一个脉冲,电机走一步。因此 4 轴控制至少需要 4 路脉冲输出和 4 路方向输出。STM32 的定时器虽然多,但不是每个定时器的每个通道都能直接映射到任意引脚,必须查数据手册里的 AFIO 重映射表和引脚定义表。F103 的 TIM1、TIM2、TIM3、TIM4 是 4 个 16 位定时器,每个定时器有 4 个通道,理论上能输出 4 路 PWM,但如果 4 个轴都要独立频率、独立加速度,使用 4 个不同的定时器更省心,因为同一定时器的 4 个通道虽然可以输出不同占空比,却共享同一个时基。做 4 轴独立运动时,每个轴的脉冲频率完全独立变化,共同时基会让实现变得绕远路。
F405 的优势在定时器资源更丰富,TIM1 到 TIM14 一共 14 个定时器,其中还有 32 位定时器 TIM2 和 TIM5。如果一个轴需要很宽的调速范围,32 位定时器能用更大的预分频和重装载值覆盖低频到高频,不会因为 16 位定时器溢出而被迫中断频繁重装。工程里同时出现 F103 和 F405,常见做法是:F103 负责 2 轴或 4 轴的硬实时脉冲输出,用定时器 PWM 模式硬件产生方波,CPU 只在需要改变频率时干预;F405 负责梯形或 S 形曲线实时计算,把每个时间片的脉冲周期算好再下发给 F103。这样分工的好处是计算和输出解耦,加减速曲线再复杂也不会让脉冲波形抖动。
2.2 定时器输出脉冲的初始化代码
先看 F103 主定时器初始化。使用定时器 2 的通道 1 输出一路脉冲,方向引脚用普通 GPIO。
void TIM2_Pulse_Init(uint16_t arr, uint16_t psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); 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); TIM_TimeBaseStructure.TIM_Period = arr; TIM_TimeBaseStructure.TIM_Prescaler = psc; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_Toggle; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OC1Init(TIM2, &TIM_OCInitStructure); TIM_Cmd(TIM2, ENABLE); }这段代码把 TIM2_CH1 配置成翻转模式,每次计数器上溢自动翻转一次引脚电平,因此脉冲频率等于 TIM2 溢出频率。TIM_Period和TIM_Prescaler共同决定溢出频率,计算公式为:
freq = 72MHz / ((psc + 1) * (arr + 1))实际工程中很少直接写死这两个值,因为加减速过程要求频率连续变化。常见做法是只修改TIM_Period,保持预分频不变,这样频率与周期成反比,方便加速度积分运算。多轴时复制这套初始化代码,改用 TIM3、TIM4,并注意引脚冲突。F103 的 PA0 和 PA1 分别对应 TIM2_CH1 和 CH2,如果同时启用 PA0 和 PA1,没有问题;但若把 TIM2_CH2 和 TIM4_CH1 都映射到 PB3,就需要查重映射表了。
2.3 F405 的定时器配置差异
F405 的定时器基本结构同 F103 一致,但如果工程把 F405 用作主计算芯片,定时器配置思路就变了。F405 主频 168MHz,用 TIM5 这类 32 位定时器时,即使预分频设为 1,重装载值也能覆盖极大的频率范围。F405 常见用法是定时器中断产生固定时间基准,例如 100us 中断一次,在中断里计算当前速度、位置和下一步的定时器重装值,然后写入对应轴的控制结构体。F405 的 FPU 在sinf、sqrtf这类浮点运算上比 F103 快得多,所以 S 形曲线的实时计算通常放 F405。注意 F405 的定时器时钟源可能来自 APB1 或 APB2 总线,而 APB1 定时器时钟默认是主频的一半,需要先查RCC_ClocksTypeDef确认,否则脉冲频率会整体算错。
3. 4轴步进电机加减速控制的核心参数设计与梯形/S形曲线实现
3.1 梯形加减速与S形加减速的适用边界
加减速控制是步进电机工程真正拉开差距的地方。不做加减速直接给目标频率,步进电机会因为瞬间频率变化过大而失步,严重时堵转。梯形加减速实现最简:匀加速、匀速、匀减速三段。加速段位移公式为S = 1/2 * (V_start + V_max) * T_acc,速度从起点频f0线性增长到最高频fmax,每步周期递减。S 形加减速在此基础上再加一段加加速度变化过程,速度曲线呈 S 形,启停瞬间 jerk(加加速度)不为无穷大,机械冲击更小,适合 4 轴雕刻机这类对末端轨迹精度有要求的场景。
工程源码里一般会预计算一张加减速查找表,而不是在中断里实时算对数函数或浮点除法。常见做法是:把加速过程按步数分成 N 段,每段对应一个频率档位,存成uint32_t speed_table[TABLE_SIZE],中断里用查表加插值得到当前周期。查表方式为主,实时计算做修正。对于 F103 这种不带 FPU 的芯片,避免在中断里写float除法是关键优化点。
3.2 梯形加减速步进周期计算公式
梯形加减速每个步进周期的计算,常用的公式来自 Atmel AVR446 应用笔记。加速阶段每一步的时间间隔C[n]计算如下:
C[n] = C[n-1] - (2 * C[n-1]) / (4 * n + 1)其中C[n]是第n步的定时器计数值。这个递推公式避免了每步都做浮点开方,代价是在极低速度时误差略大,但通过预分频调整可以接受。实际工程步骤是:先算加速度a,再按定时器频率把a换算成每步周期减小的增量,最后用递推公式生成整张表。
看一段直接使用整数运算的脉冲周期更新函数:
uint32_t step_delay; // 当前步的定时器重装载值 uint32_t rest_steps; // 剩余步数 void timer_calc_next_delay(void) { step_delay -= (2 * step_delay) / ((4 * step_count) + 1); step_count++; TIM_SetAutoreload(TIM2, step_delay); }调用这个函数时,step_count代表已经发出的步数。第一次进入前把step_delay设为起始速度对应的定时器周期。这个递推式只有在加速段使用,到达目标速度后切换为匀速,step_delay固定;减速段反向使用类似递推。参数调整时最需要注意的是分母(4 * step_count + 1)在step_count很小的时候变化快,容易让周期骤减,因此工程上第一档周期不要设得太小,否则启动瞬间频率太高。
3.3 S 形加减速系数表与查表参数说明
S 形加减速真正要调的是 4 个参数:起始速度v0、最大速度vmax、加速度a、加加速度j。4 轴联动的场景里,每个轴可以独立拥有这套参数,也可以共享一组运动规划参数。工程中常见结构体定义如下:
typedef struct { uint32_t v0; // 起始速度,单位:脉冲/s uint32_t vmax; // 最大速度,单位:脉冲/s uint32_t accel; // 加速度,单位:脉冲/s^2 uint32_t jerk; // 加加速度,单位:脉冲/s^3 int32_t position; // 当前逻辑位置,单位:脉冲数 int32_t target_pos; // 目标位置 uint8_t state; // 0:停止 1:加速 2:匀速 3:减速 } AxisParam;查表法 S 形曲线实现步骤:启动前根据v0、vmax、accel、jerk离线计算一张表,表里存的是从起始速度到最大速度之间每步的周期值。运行时状态机判断当前处于哪个阶段,然后从表中取周期、写入定时器。F405 上浮点能力强时,可改为在线计算,但查表仍然是保底方案。实测中jerk太大会让 S 形退化成梯形,太小则延长加减速时间。一般建议jerk = accel * 4起步,再根据机械共振情况微调。
3.4 4轴联动时的速度规划同步与插补后处理
4 个轴同时运动,如果各算各的加减速曲线,会出现一个轴先到、另一个轴后到的不同步问题。同步办法是把 4 轴抽象成一条合成轨迹:选取移动距离最长、时间需求最大的轴作为主运动轴,以其加速时间、匀速时间、减速时间作为统一时间基准,其他轴的每一步按比例折算。这一比例系数就是脉冲当量比。
假设 X 轴走 1000 脉冲,Y 轴走 500 脉冲,最高速度限制分别为 vmax_x 和 vmax_y。若各自独立加速到最高速,X 轴到达时间大于 Y 轴,但联动轨迹要求两轴同时完成,解决方式是让 Y 轴最高速度限制为 X 轴速度的一半。工程里通常用一个ratio数组:ratio[i] = distance[i] / distance[max_axis],每步根据主轴的增量乘以 ratio 得到从轴增量。注意这里存在取整误差,累计误差会随时间漂移,因此需要误差补偿:把每步的余数累加,超过 1 个脉冲时补发一个脉冲。
uint32_t accum_error[4]; void stepper_plan_pulse(uint32_t axis, int32_t delta_steps) { for (int i = 0; i < 4; i++) { accum_error[i] += table_ratio[i]; int32_t step_output = accum_error[i] / RATIO_DIVISOR; accum_error[i] %= RATIO_DIVISOR; if (step_output != 0) { generate_pulse(axis, i, step_output); } } }accum_error数组的本质是把除法余数搬到了时间轴上,避免每次都ceil造成累计位移偏大。参数RATIO_DIVISOR是比例精度,通常设为 1000 或 10000,精度越高计算量越大。F103 主频 72MHz 时建议用 1000,F405 用 10000 也能承受。
4. 源码工程中的速度规划、状态机与中断控制的协同机制
4.1 运动状态机划分与中断优先级分配
4 轴运动控制最忌在多个中断里同时修改同一个轴的参数。工程里通常把运动控制分成三层:应用层、规划层、输出层。应用层负责接收指令,例如move_x(1000, 2000)表示 X 轴移动 1000 步、最大速度 2000 步/s。规划层由定时器周期中断驱动,每 1ms 执行一次速度规划和位移累加。输出层是每轴独立的脉冲定时器中断,只负责根据当前周期翻转引脚。规划中断的优先级要高于普通外部中断,但低于系统心跳;脉冲输出中断追求的是稳定频率,不能被规划中断长时间阻塞。
推荐的中断设计方案:使用 1ms 基本定时器作为运动规划节拍,优先级设置为 2;4 个轴的脉冲定时器使用同一优先级,设置为 1(数字越小优先级越高在 Cortex-M 中)。如果规划中断和脉冲中断共享同一个轴的数据,必须用临界区保护。ARM Cortex-M 上最轻量的保护方式是__disable_irq()/__enable_irq(),但要注意临界区只能保护极短的操作,绝不能在里面做除法。
4.2 状态机实现步骤与代码
一个轴在任意时刻只处于停止、加速、匀速、减速四种状态之一。用switch实现步骤分解如下。
void axis_process(AxisParam *axis) { switch (axis->state) { case STATE_STOP: if (axis->target_pos != axis->position) { axis->state = STATE_ACCEL; axis->accel_step = 0; } break; case STATE_ACCEL: axis->speed += axis->accel_calc; if (axis->speed >= axis->vmax) { axis->speed = axis->vmax; axis->state = STATE_RUN; } axis->delta_steps = axis->speed / FREQ_DIVISOR; break; case STATE_RUN: axis->delta_steps = axis->vmax / FREQ_DIVISOR; if (remaining_distance <= decel_distance) { axis->state = STATE_DECEL; } break; case STATE_DECEL: axis->speed -= axis->decel_calc; if (axis->speed <= axis->v0 || axis->decel_remaining <= 0) { axis->speed = axis->v0; axis->state = STATE_STOP; axis->target_pos = axis->position; } axis->delta_steps = axis->speed / FREQ_DIVISOR; break; } }注意axis->speed的单位是「脉冲/s」,而delta_steps是每个规划周期内需要发出的脉冲数增量。FREQ_DIVISOR是规划周期换算因子。若规划周期为 1ms,则FREQ_DIVISOR=1000。若为 1ns 基准计时则有更精细的变换。加油速阶段accel_calc要先换算成每个周期的速度增量。减速段的进入条件不是简单的剩余距离小于多少,而是用减速位移公式decel_distance = (speed^2 - v0^2) / (2 * decel)判断,否则会出现减速过晚导致停不准。
4.3 中断里如何安全更新定时器重装值
实际写中断时,最常见的 bug 是 TIM_SetAutoreload 在 PWM 输出模式下,修改预装载值并不会立即生效,直到更新事件出现才装载。这意味着中断里如果先改重装值再等下一次周期,会出现一拍延迟。工程上的修正方案是:本周期中断进来,先计算下一拍的重装值并写入寄存器,当前周期的剩余时间不做修改。这样脉冲频率的每次调整都延迟一个完整周期,但曲线平滑且逻辑清晰。
void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); pulse_count++; if (axis_x.running == 1) { step_delay_next = calc_next_delay(&axis_x); TIM_SetAutoreload(TIM2, (uint16_t)step_delay_next); } if (pulse_count >= axis_x.total_steps) { axis_x.running = 0; TIM_Cmd(TIM2, DISABLE); } } }逻辑说明:中断每次进入代表一个脉冲已经发出。此时先判断是否走完所有步数,若未走完则计算下一步的定时器周期并装载。注意step_delay_next是作为结构体成员传入,因为中断里调用calc_next_delay时要访问上一次的周期值,普通局部变量在中断与主循环间共享会互相覆盖。多轴时对应每个轴有自己的中断函数和结构体,不能共用一个全局变量保存延迟值。
4.4 代码仓库里常见的文件分层与宏开关
工程源码包打开后通常会有以下文件分层:bsp板级驱动目录里每个芯片一个初始化文件;motion目录放加减速表和状态机;protocol目录放上位机串口指令解析;main.c只做初始化加主循环。宏开关通常有USE_F103和USE_F405两组,编译时通过#define决定使用哪组定时器资源。这里最容易踩坑的是两个芯片的中断向量名不同,F103 叫TIM2_IRQHandler,F405 也是这个名字,但如果同时在一个工程中适配两芯片,中断服务函数里调的寄存器地址会因为芯片型号不同而编译失败。工程做法是单独维护两个工程文件,或使用#ifdef包住整段寄存器配置,而不是在同一个 c 文件里同时出现两套硬件操作。
5. STM32CubeMX 配置下的引脚分配与代码生成策略
5.1 CubeMX 快速生成 4 路 PWM 的最小配置
很多工程师拿到工程源码后第一件事是看别人怎么配置 CubeMX。用 STM32CubeMX 配置 4 轴脉冲输出的核心步骤就三条:选择定时器时钟源,开启 PWM 输出通道,配置 GPIO 为复用模式。以 F103 为例,在 Pinout 视图选中 PE9、PE11、PE13、PE14,分别对应 TIM1_CH1、TIM1_CH2、TIM1_CH3、TIM1_CH4,一个定时器就能覆盖 4 个通道,适合 4 轴共用同一时基的场景。若需要每个轴独立频率,改选 4 个不同的定时器,例如 TIM1、TIM2、TIM3、TIM4,并在 Clock Configuration 里检查 APB1 和 APB2 定时器时钟是否一致。
CubeMX 生成代码后需要手动在while(1)循环外做三件事:启动定时器、使能输出比较通道、设置方向引脚初始电平。很多人忘记调用HAL_TIM_PWM_Start,导致引脚不变。用 HAL 库时定时器重装载值修改函数是__HAL_TIM_SET_AUTORELOAD(&htim, value),此函数只写 ARR 寄存器但不会使能更新事件,如果之前配置了TIM_IT_UPDATE中断,需要确保正常触发。推荐在 CubeMX 里把 PWM 生成模式配置为Toggle Mode,把AutoReloadPreload设为 Enable,这样每次修改 ARR 后会在下一个更新事件自动装载。
5.2 方向引脚与限位引脚冲突检查表
4 轴控制至少涉及 12 个引脚:4 脉冲、4 方向、4 使能(可选)。若带限位开关则再加 8 个输入引脚。下表是 4 轴配置最常见的引脚分配参考,实际以手头板子丝印为准。
| 功能 | 建议 F103 引脚 | 建议 F405 引脚 | 说明 |
|---|---|---|---|
| X 轴脉冲 | PA0 / TIM2_CH1 | PE9 / TIM1_CH1 | 高电平触发边沿 |
| X 轴方向 | PA1 | PE10 | 空闲时固定为低 |
| Y 轴脉冲 | PA6 / TIM3_CH1 | PE11 / TIM1_CH2 | 不与 JTAG 冲突 |
| Y 轴方向 | PA7 | PE12 | 方向切换前先停 5us |
| Z 轴脉冲 | PB0 / TIM3_CH3 | PE13 / TIM1_CH3 | 注意 PB0 是 ADC 复用 |
| Z 轴方向 | PB1 | PE14 | 避免和 boot 冲突 |
注意列表中 PE9 到 PE14 同时对应 TIM1 的 PWM 输出通道,启用后 TIM1 还需要配置TIM_CtrlPWMOutputs才能输出,HAL 库中HAL_TIM_PWM_Start已经内含此操作,标准库版本必须手动调用。方向引脚属于普通推挽输出,切换方向与脉冲之间必须间隔一定时间,通常用指令__NOP()延时即可。
5.3 源码编译前预处理宏的自检方法
拿到一个 STM32 工程,编译失败的第一原因往往不是代码逻辑而是芯片型号选择错误。Keil 里魔术棒选项卡 Device 选了 F103,但代码里定义了STM32F405xx,启动文件选择错了也会直接编译报错。常见做法是使用#define STM32F10X_HD或STM32F405xx作为全局宏,并在 stm32f10x.h 或 stm32f4xx.h 头部通过#ifdef判断该宏来包含正确寄存器定义。自检命令是检查以下三处是否一致:C / C++ 选项卡里的 Define 宏、Device 选项卡里的芯片型号、启动文件startup_stm32f10x_hd.s还是startup_stm32f40xx.s。三者不一致时,即使编译通过,烧录后程序也可能直接跑飞。
6. 4轴同步误差补偿与回原点脉冲丢失排查
6.1 脉冲方向信号的时序要求与换向抖动
换向产生丢步是最隐蔽的故障。方向引脚变化必须在脉冲信号稳定为无效电平后再执行,同时脉冲引脚不能和方向引脚同时翻转。常见做法是:发出最后一个正方向脉冲后,等待至少一个定时器周期,再拉高方向引脚,再等待 5 微秒,才允许第一个反方向脉冲产生。F103 标准库中可用TIM_GetCounter读取当前计数值来实现定时等待,或者用delay_us死延时。死延时的长度以驱动器的方向建立时间为准,典型值 2~5us,最长不要超过 50us,否则高速换向时会影响平均速度。调试时用示波器同时测量方向引脚和脉冲引脚,方向引脚建立时间不够会看到脉冲边沿处方向尚未稳定,这通常是驱动器数据手册里「方向保持时间」指标的违反。
6.2 回原点时序与原点信号的去抖参数
4 轴设备每次开机需要回原点,回原点的顺序也很关键。常见顺序是先用低速寻原点开关,找到开关后才切换到高速,离开原点开关后再用低速回找精准原点。这个过程在两个方向上都可能产生丢步,因为碰开关瞬间机械振动会让开关抖动,程序若在第一次检测到边沿就立即停,后续可能因抖动误判。推荐做法是让轴继续以当前速度运行一小段距离并连续采样多次,例如要求在连续 10 次读取中至少 8 次为有效电平才确认到位。这个去抖参数用时间窗口来控制,而不是简单的delay_ms(10),这样不同速度下行为一致。
6.3 验证 4 轴同步精度的三种手段
写完代码最想做的是证明 4 轴确实同步了。最直接的手段是示波器观察 4 路脉冲的总数,这个对同步性没有直接帮助。更有效的方法是打印每个轴到达目标后的实际位置偏差,可以用串口定期输出 4 轴当前坐标,跑一段斜线轨迹,观察末端坐标是否落在理论斜线上。第二种方法是把 4 路脉冲分别输入到计数模块,比较到达目标时四个计数值与目标之比是否一致。第三种方法是让 4 个轴分别画正方形,每个角等待 IO 同步信号,通过千分表测量角点位置的笛卡尔误差。工程上 I 类误差(单轴重复定位误差)通常在 2 个脉冲以内,II 类误差(多轴联动轮廓误差)可以控制在 5 个脉冲以内。若同步误差超过 10 个脉冲,回查前文提到的误差累计补偿数组accum_error是否溢出或被主循环清空。
6.4 从源码包中提取与复用加减速曲线参数的技巧
最后给一个实际跳坑的建议:工程包里通常有一张加减速曲线参数表,表中会写max_speed、accel、decel之类的宏。不要直接照搬整张表,先确认单位是「脉冲每秒」还是「转每分」。若是转每分,要乘以细分数和齿轮比换算回脉冲。例如电机细分为 16,丝杆导程 5mm,速度 300rpm,脉冲速度为300 / 60 * 16 * 200 = 16000 steps/s,系数乘反会让加减速时间差一个数量级。确认单位后,再按机械负载修改jerk值直到噪声明显下降。这个参数整定过程没有捷径,但有一条验算规则:若电机在加速中段听到明显啸叫,大概率是加速度过大进入共振区,优先降低加速度而不是改细分。
本文还有配套的精品资源,点击获取