简介面向嵌入式开发者的STM32步进电机串口控制程序能够接收上位机指令实现对电机速度与角度的精确调整。核心代码涵盖UART串口通信解析、GPIO推挽输出驱动、定时器中断产生脉冲以及计数器定位等模块并包含全步进、半步进、微步进三种控制模式适合正在学习STM32电机控制或需要快速搭建运动控制项目的工程师参考。资源包共216个文件以.c/.h源文件为主附带Keil工程配置文件、编译生成的.o/.crf中间文件、Hex固件及调试相关记录压缩包约3.41MB工程结构清晰可直接打开源码阅读或烧录到开发板验证。已有1245人学习下载。通过对这份程序的分析可以掌握串口通信协议定制、脉冲频率与速度换算、旋转角度计数设定以及过流、超速、失步等保护策略的具体实现方式对深入理解STM32定时器资源和中断机制也有明显帮助。1. 串口指令驱动的步进电机控制不重刷固件的调速调角方案做嵌入式设备调试时最怕的是改一个转速参数就要重新插拔烧录器。用STM32做步进电机控制把速度和角度指令通过串口交给固件去解释意味着上位机只发几个字节电机就能转起来。这个程序的核心里UART负责收指令定时器负责生成脉冲GPIO负责输出相序三部分串起来后一套滑台或者云台就能用串口调试助手直接操作。这套方案的另一个价值是可以把控制参数全部暴露给上位机连加减速曲线都可以运行时修改。对需要快速验证机械结构和自己写上位机的人来说这类工程是把电机控制快速落地的捷径。2. 硬件接线与UART串口指令协议先把物理层打通这套工程解压后是一份完整的Keil MDK项目里面的STM32-DEMO.uvproj双击就能打开编译axf文件是烧录用的镜像下载后USART1默认以115200波特率等待上位机指令。在讨论指令解析和脉冲生成之前先要把物理层接线和串口参数理清楚这两块出错时表现出来的现象都一样电机不动但排查路径完全不同。2.1 驱动器信号线与STM32 GPIO的接法市面上的步进电机驱动器比如TB6600和A4988控制侧一般只需要三根信号线脉冲PUL、方向DIR、使能ENA。STM32侧把这几个引脚配置为推挽输出给脉冲信号、方向电平和使能电平。我习惯让DIR和ENA默认带上拉避免上电瞬间电机误转一段。以STM32F103C8T6最小系统板为例PA0接PULPA1接DIRPA2接ENA驱动器如果是共阴接法公共端直接接GND。驱动器信号功能说明STM32引脚GPIO模式PUL脉冲输入每个上升沿走一步PA0推挽输出DIR方向控制高低电平决定正反转PA1推挽输出ENA使能控制低电平释放电机PA2推挽输出VCC/GND驱动器逻辑电源5V/GND-这里有个容易踩的坑驱动器的PUL端口内部一般带光耦隔离脉冲信号要有最小宽度通常是2到5微秒。如果程序写好之后电机不转先用示波器看PA0引脚波形确认脉冲宽度够不够再来怀疑定时器配置。STM32的GPIO输出速度建议设为最高档GPIO_SPEED_FREQ_HIGH否则在高速脉冲输出时引脚边沿会被拉宽接近半周期的脉冲可能直接让驱动器丢步。2.2 USART1串口初始化波特率与帧格式的选择串口通信的物理层配置是整个控制链路的地基。我一般把USART1设定为115200波特率、8个数据位、1个停止位、无校验这个组合在所有串口调试助手里都是默认值上位机不需要额外设置。使用HAL库时初始化代码集中在MX_USART1_UART_Init这一个函数里。void MX_USART1_UART_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 115200; /* 上位机必须一致 */ huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; HAL_UART_Init(huart1); }WordLength选了8位数据步进电机控制指令里一个字节恰好对应一个参数解析时无需做位拼接。HwFlowCtl必须设为NONE多数开发板没有引出硬件流控引脚一旦误开RTS/CTS串口助手发数据后模块根本不会响应。如果用的是CH340这类USB转串口模块接线时TX接STM32的RXPA10RX接STM32的TXPA9地线必须共地否则会出现只能发不能收的怪现象。2.3 指令帧格式从速度和角度需求倒推字段设计步进电机控制指令至少要携带目标速度和目标角度。为了让协议具备扩展性我习惯用固定帧头加命令字加参数的格式帧头是0xAA 0x55紧接着命令字0x01表示绝对角度旋转0x02表示连续旋转0x03表示立即停止。参数区固定4字节前两个字节是速度档位后两个字节是角度值乘以10后的整数这样能做到0.1度的分辨率又保持在单字节编码范围内。typedef struct { uint8_t head[2]; /* 0xAA 0x55 帧同步 */ uint8_t cmd; /* 命令字 0x01/0x02/0x03 */ uint8_t speed_h; /* 速度值高字节 */ uint8_t speed_l; /* 速度值低字节 */ uint8_t angle_h; /* 角度值高字节 */ uint8_t angle_l; /* 角度值低字节 */ uint8_t checksum; /* 累加和校验 */ } motor_cmd_t;速度值字段定义的是定时器的计数周期不是直接定义转速这个换算关系放到第三章展开。校验和取除帧头外所有字节累加的低8位能在接收时不费力地把损坏帧丢掉。在解析函数里维护一个头指针加环形缓冲区HAL库的接收回调只负责往缓冲区写字节主循环里做状态机匹配帧头这样上位机哪怕连发几十帧指令也不会出现帧错位。2.4 串口不通时的快速排障常见现象与检查点硬件调试阶段串口不通是消耗时间最多的问题。我整理过一张排查表基本覆盖了串口控制步进电机场景下的高频故障。现象常见原因检查点串口助手收不到数据CH340驱动未装或TX/RX接反设备管理器看COM口调换TX、RX收到乱码波特率不一致确认助手和固件都是115200指令发出电机没反应校验不过或PUL没波形逻辑分析仪量PA0核对帧格式下载程序失败BOOT0电平不对或串口被占用BOOT0接GND确认PA9/PA10悬空电机抖动不转驱动器电流限幅太小调驱动器的限流电位器串口烧写失败里还有一个隐蔽原因CH340驱动在高波特率下时序不稳定试着把下载工具的波特率降到38400成功率会明显提高。如果板子上的PA9、PA10被复用成定时器PWM输出下载工具握手时串口会被外设干扰这类问题在开发初期就要通过规划引脚功能避开。3. 定时器生成脉冲速度、角度与脉冲当量怎么换算3.1 定时器PSC和ARR的搭配逻辑步进电机的转速本质上取决于PUL脚上的脉冲频率。STM32用定时器输出PWM频率由预分频器PSC和自动重装载值ARR共同决定公式是72MHz除以(PSC1)再除以(ARR1)。以步距角1.8度的42步进电机为例每转一圈需要200个脉冲。要让转速达到60rpm也就是每秒1转每秒就需要200个脉冲PWM输出频率为200Hz。这里有两种配置方式都能得到200HzPSC3599、ARR99或者PSC71、ARR4999。目标转速(rpm)PWM频率(Hz)PSC值ARR值实际频率(Hz)301007199991006020035999920012040017999940024080089999800PSC和ARR的选择原则是优先把ARR调大让速度微调有更高分辨率。PSC过小会让定时器计数过快更新中断触发频繁CPU占用率明显上升PSC过大会让ARR一档对应的频率变化很粗想要微调1Hz都做不到。程序里用32位变量保存实时ARR值运行时通过__HAL_TIM_SET_AUTORELOAD直接修改不需要停止定时器加减速过程就不会出现脉冲毛刺。3.2 角度控制把目标角度换算成脉冲数角度控制的关键是把目标角度变成脉冲个数然后在定时器更新中断里递减计数。对于1.8度步距角旋转90度就是50个脉冲旋转360度就是200个脉冲。代码里维护一个volatile int32_t steps_remaining每次定时器更新中断自减减到0就关闭PWM输出电机停在目标位置。方向控制看目标角度的正负正值时DIR引脚输出高电平负值输出低电平。void start_rotation(int16_t angle_0_1deg, uint16_t speed) { int32_t steps; steps (int32_t)angle_0_1deg * 10 / STEP_ANGLE_1_8; /* 角度x10输入 */ if (steps 0) { HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, GPIO_PIN_RESET); steps -steps; } else { HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, GPIO_PIN_SET); } steps_remaining steps; set_pwm_frequency(speed); /* 根据速度换算ARR */ HAL_TIM_PWM_Start_IT(htim2, TIM_CHANNEL_1); }STEP_ANGLE_1_8在没有驱动器细分的场景下是1.8如果驱动器开了16细分要改成0.1125否则实际转角会放大16倍。set_pwm_frequency把串口收到的速度档位换算成ARR公式是ARR 72MHz / ((PSC1) * speed)。如果电机轴端带减速器还需要把减速比乘进去这个参数我单独用一个宏定义换减速比时只改一处。一个反复出现的问题是直接用HAL_Delay延时翻转GPIO来产生脉冲。低速时看起来没问题电机转速一提上来延时函数和中断互相阻塞串口数据来不及处理上位机指令就丢了。定时器方案的优点在于脉冲生成完全不占用主循环的CPU时间串口解析可以放在主循环里慢慢做两边互不干扰。3.3 加减速控制高速直启为什么会丢步步进电机转子有惯性静止状态下直接把脉冲频率拉高电机输出转矩不足以带动负载就会出现丢步严重时直接堵转。常见做法是梯形加减速启动频率从低位起步每走N个脉冲提升一档到达目标速度后匀速接近目标位置前再逐档降速。加减速每档的持续时间由电机的机械常数决定没有万能参数。static uint16_t current_arr; static uint16_t target_arr; static uint8_t accel_cursor; void timer_update_callback(void) { steps_remaining--; if (accel_cursor ACCEL_STEPS) { if (steps_remaining accel_cursor) { current_arr target_arr ACCEL_STEP_ARR * (ACCEL_STEPS - accel_cursor); __HAL_TIM_SET_AUTORELOAD(htim2, current_arr); accel_cursor; } } if (steps_remaining DECEL_THRESHOLD) { current_arr target_arr ACCEL_STEP_ARR * (DECEL_THRESHOLD - steps_remaining); __HAL_TIM_SET_AUTORELOAD(htim2, current_arr); } if (steps_remaining 0) { HAL_TIM_PWM_Stop_IT(htim2, TIM_CHANNEL_1); } }这段回调里ARR数值和频率成反比所以加速时current_arr从大往小调减速时从小往大调。ACCEL_STEP_ARR是每一档ARR的变化量这个值越小加减速越平缓但需要的加速距离也越长。从电磁特性看高频时绕组电感会限制电流建立速度电机输出转矩随转速升高而下降所以最高频率必须有上限否则电机在高速区会进入失步区。4. 步进模式与驱动器配置全步进、半步进和微步进怎么选4.1 全步进与半步进的相序逻辑两相四线步进电机在全步进双四拍模式下有四种通电状态分别是AB、B-A、A-B-、B-A-每切换一次电机转过1.8度。半步进则在整步之间插入单相通电状态一共八拍每拍走0.9度。STM32不用驱动板直接控制时GPIO按相序表翻转电平。const uint8_t full_step_table[4][4] { {1, 1, 0, 0}, /* A B */ {0, 1, 1, 0}, /* B A- */ {0, 0, 1, 1}, /* A- B- */ {1, 0, 0, 1} /* B- A */ }; void full_step_next(uint8_t *phase_idx) { HAL_GPIO_WritePin(A_PLUS_Pin, full_step_table[*phase_idx][0]); HAL_GPIO_WritePin(B_PLUS_Pin, full_step_table[*phase_idx][1]); HAL_GPIO_WritePin(A_MINUS_Pin, full_step_table[*phase_idx][2]); HAL_GPIO_WritePin(B_MINUS_Pin, full_step_table[*phase_idx][3]); *phase_idx (*phase_idx 1) 0x03; }这里只写了全步进的相序半步进表换成8x4数组索引时用 0x07。直接驱动绕组的方案下CPU每次进中断都要改写四个引脚速度高了以后中断负载很大而且没有电流斩波能力电机发热明显。因此我在实际项目中基本只用外部驱动器STM32只给PUL脉冲驱动器内部的H桥完成换相和电流控制CPU只需要控制PWM频率和脉冲个数。4.2 驱动器细分脉冲当量从1.8度降到0.1125度细分是驱动器内部对每个整步做电流插值让转子在相邻整步之间平滑过渡。开16细分后一个脉冲只让电机走0.1125度振动和噪音都有明显改善。细分倍数通过驱动器上的拨码开关设定TB6600的SW1到SW3组合出2、4、8、16、32等档位。每个脉冲对应的实际角度就是脉冲当量这个参数在带减速器的设备里经常与减速比一起计算。细分倍数每转脉冲数脉冲当量(度)典型场景12001.8高速粗定位816000.225常规平台1632000.1125精密云台3264000.05625低启停速度场合代码层面只需要把STEP_ANGLE_1_8这个宏改成对应细分下的脉冲当量比如16细分就是0.1125。不过角度换算处有个边界目标角度除以脉冲当量得到的必须化成整数否则累计误差会一直存在。我一般先把角度放大1000倍用整数运算最后四舍五入到整数脉冲避免在STM32F103这种没有FPU的芯片上引入浮点运算拖慢解析速度。驱动的电流设定同样影响位置精度。TB6600板载的限流电位器若是拧得太小高速时容易丢步拧得太大电机会发烫。电流按电机额定电流的70%起步加减速丢步就往上加发热超标就往下减。4.3 从开环到闭环编码器反馈与失步补偿开环控制下步进电机一旦过载丢步位置误差会一直累积。不少设备开始用闭环步进电机电机尾部集成编码器驱动器内部完成失步补偿STM32层的串口指令格式可以完全不变精度却提升一个数量级。这种情况下PUL还是那个PUL只是每一拍都经过编码器确认补偿逻辑由驱动器固件处理。如果继续用开环驱动器也可以在电机轴上加编码器把A相、B相接进STM32定时器的编码器模式。每执行完一段运动读一次计数器值与理论脉冲数对比。差值超过阈值就通过串口向上位机发一个异常帧上位机再发一个补偿角度。编码器模式自动对AB相计脉冲不占CPU但会占用一个定时器资源紧张时要先在引脚规划阶段定好。5. 串口状态机解析与在线调参调试的最后一块拼图5.1 状态机式帧解析粘包和半包都不怕串口收指令最常见的坑是粘包和半包。上位机连发两条指令时字节可能一次涌入缓冲区单条指令又被拆成两段到达。逐字节判断在这种场景下很容易错位所以我用状态机按当前状态决定下一个字节的期望值。状态机的核心价值是允许两个字节之间间隔任意长时间半包到达一半时状态保留在当前位置剩余字节到了接着走粘包则靠帧头重新对齐。case 0: if (data 0xAA) state 1; break; case 1: if (data ! 0x55) { state 0; break; } state 2; idx 0; break; case 2: frame.cmd data; state 3; break; case 3: ((uint8_t *)frame)[3 idx] data; if (idx 4) state 4; break; case 4: if (data calc_checksum(frame)) handle_cmd(frame); state 0; break;这段逻辑挂在HAL库的单字节接收回调后面每次收到一个字节就调一次。帧头匹配在case 0和case 1非法的首字节会直接丢弃并重新等待。我偶尔会在case 3之后判断一下角度字段是否超过允许范围提前丢掉非法帧减少运动函数的无效调用。5.2 在线调参让速度档位和加速度不用重编译速度档位、加速斜率、最高频率这些参数传统做法是改一个值就重新编译烧写一次调试要几分钟。在协议里预留参数写入命令后上位机发一帧参数包就能覆盖全局结构体正在运行的电机下一拍就按新参数运动。参数好不好立刻在机械轴上看到结果找到合适值再固化到程序里整个调参过程从小时级缩到分钟级。在线调参还有一个附带收益用来摸清电机的机械特性。发一串递进的速度档位观察哪个档位开始丢步记下这个频率作为程序里的启动上限再发不同加速度组合找出一组不丢步且耗时最短的数值。配合串口调试助手的定时发送功能把几十组角度数值循环发出去等全部走完再用状态查询命令核对位置漏了多少步一目了然这几组数据就是后续所有运动指令的边界。本文还有配套的精品资源点击获取