简介面向嵌入式入门、课程设计与电机控制初学者的STM32F103ZET6实战工程围绕步进电机驱动与增量式编码器反馈展开。方案以定时器3输出PWM及互补信号调节步进电机转速与方向用定时器4输入捕获编码器A/B相脉冲以计算位置和速度再借助USART将反馈数据实时上传电脑构成控制与监测闭环。压缩包共206个文件、约4.96MB含37个c与38个h源码、Keil工程uvprojx/uvoptx、hex与axf可执行产物、map/lst/sct链接及分散加载配置并保留o、d、crf等编译中间文件便于直接编译、对照与二次开发。已有7350人学习下载。读者可从中理解定时器PWM、输入捕获与串口中断的协同用法复用外设初始化、中断服务与串口上传框架并借鉴上位机解析数据的思路对定时器配置、编码器计数方向判断和串口数据帧组织等关键点也可结合源码与map/lst文件交叉核对适合课程设计、毕业设计前期验证及运动控制入门练习。1. 从脉冲到角度STM32驱动编码器步进电机的真实控制链STM32驱动编码器步进电机的板子最常见的翻车点不是算法而是信号链没接对编码器A/B相抖动被当成位置变化步进驱动器收到脉冲却不转。这个资源要解决的是把增量式编码器读数、步进电机脉冲输出和简单闭环修正串成一条能复现的控制链适合已经能点灯、会配置定时器但一上电机就丢步或读数跳变的从业者。常见做法是先用定时器编码器接口把AB相读稳再用PWM通道发STEP脉冲最后用编码器差值修正步进目标。下面按硬件接线、读数测速、脉冲生成、避坑排查、闭环验证的顺序拆开讲。2. 硬件信号链落地ABZ编码器与STEP/DIR驱动器怎么接到STM32一套能跑起来的STM32步进电机编码器控制板信号链通常分两段编码器把机械角度变成A/B两路正交脉冲STM32定时器编码器接口负责计数步进驱动器把STEP脉冲和DIR电平变成绕组电流编码器再把这个动作反馈回来。很多人一上来就写闭环结果连开环脉冲都没数清楚最后只能对着电机听响。硬件这一层先把电平、共地、上拉、滤波和定时器资源定死后面代码才有意义。常见增量式编码器有A、B、Z三路A/B相差90度Z是每转一个零位。A/B可以直接进STM32定时器的CH1/CH2配置成编码器模式后硬件自动四倍频计数不需要外部中断里手动判相。步进驱动器一般是STEP、DIR、EN三路光耦输入常见共阳接法是把驱动器光耦正端接3.3V或5VSTM32用开漏或推挽拉低对应端子。这里有一个边界STM32的3.3V输出能否驱动驱动器光耦要看驱动器输入限流电阻和光耦阈值常见做法是查驱动器手册里的输入电流必要时加三极管或光耦隔离不要直接把电机侧电压引进MCU。2.1 编码器ABZ信号与定时器编码器接口编码器接口模式的核心是定时器把CH1和CH2的边沿组合成计数方向A相超前B相时加计数B相超前A相时减计数。以常见STM32F1/F4系列为例TIM2、TIM3、TIM4、TIM5里通常有带编码器接口的定时器具体看芯片数据手册的复用表。引脚分配我一般会优先选同一组端口方便布线和排查比如PA0/PA1给TIM2PA6给TIM3_CH1做STEPPA7做DIRPA8做EN。信号常见STM32引脚定时器/模式注意事项编码器APA0TIM2_CH1编码器输入上拉或外部上拉串联小电阻编码器BPA1TIM2_CH2编码器输入与A相同滤波避免只滤一路编码器ZPA2TIM5_CH1输入捕获只在回零校准用运行中别频繁清零STEPPA6TIM3_CH1PWM输出驱动器只看边沿占空比50%即可DIRPA7GPIO输出换向前先停脉冲留建立时间ENPA8GPIO输出使能有效电平看驱动器手册编码器供电和信号地要跟MCU地共在一起长线用屏蔽双绞屏蔽层单端接地。A/B线上拉电阻常见4.7k到10k如果编码器是差分输出不要直接接单端定时器先加差分接收器转成单端。滤波电容不要拍脑袋加太大常见100pF到1nF量级具体看线长和干扰电容太大时高速脉冲会被吃掉低速又看起来正常排查时很容易误判成代码问题。2.2 步进驱动器STEP/DIR/EN的电气接法步进驱动器输入侧通常是光耦STEP每来一个有效边沿走一个细分步DIR电平决定方向EN用来释放或锁定绕组。共阳接法下驱动器PUL、DIR、ENA接3.3V或5VPUL-、DIR-、ENA-分别接STM32引脚MCU输出低电平时光耦导通。共阴接法逻辑相反接线前必须确认驱动器手册里的“有效电平”否则会出现“代码发脉冲电机不动”或者“一上电电机就锁死”。接线项常见接法参数/检查点PUL / DIR / ENA接3.3V或5V与驱动器输入限流电阻匹配PUL-接TIM3_CH1脉冲频率先设1kHz到5kHz试转DIR-接GPIO PA7换向建立时间给几微秒到几十微秒ENA-接GPIO PA8确认高有效还是低有效驱动器电源地与MCU地共地电机电源和逻辑电源分开走线注意电机电源和逻辑电源不要共用一根细线从远处拉过来驱动器换向瞬间的电流尖峰会把MCU拉复位。常见做法是电机电源单独走粗线逻辑侧加TVS和电解电容STEP/DIR线远离电机相线。2.3 定时器资源分配与初始化代码下面这段是编码器接口和STEP PWM的初始化骨架基于常见HAL库写法。参数不是万能值但结构可以直接照着改。代码里把编码器滤波设成6是给中低速、线长一般的情况留余量如果现场干扰大先改滤波和布线不要先改控制算法。/* TIM2: 编码器接口PA0CH1, PA1CH2 */ static void Encoder_TIM2_Init(void) { GPIO_InitTypeDef gpio {0}; TIM_Encoder_InitTypeDef enc {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_TIM2_CLK_ENABLE(); gpio.Pin GPIO_PIN_0 | GPIO_PIN_1; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, gpio); enc.EncoderMode TIM_ENCODERMODE_TI12; // A/B两相都计数四倍频 enc.IC1Polarity TIM_ICPOLARITY_RISING; enc.IC2Polarity TIM_ICPOLARITY_RISING; enc.IC1Selection TIM_ICSELECTION_DIRECTTI; enc.IC2Selection TIM_ICSELECTION_DIRECTTI; enc.IC1Prescaler TIM_ICPSC_DIV1; enc.IC2Prescaler TIM_ICPSC_DIV1; enc.IC1Filter 6; // 0~15按噪声和转速折中 enc.IC2Filter 6; HAL_TIM_Encoder_Init(htim2, enc); __HAL_TIM_SET_COUNTER(htim2, 0); HAL_TIM_Encoder_Start(htim2, TIM_CHANNEL_ALL); } /* TIM3_CH1: PA6输出STEPPA7DIRPA8EN */ static void StepPulse_TIM3_Init(uint32_t psc, uint32_t arr) { GPIO_InitTypeDef gpio {0}; TIM_OC_InitTypeDef oc {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_TIM3_CLK_ENABLE(); gpio.Pin GPIO_PIN_6; // STEP gpio.Mode GPIO_MODE_AF_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, gpio); gpio.Pin GPIO_PIN_7 | GPIO_PIN_8; // DIR / EN gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, gpio); htim3.Instance TIM3; htim3.Init.Prescaler psc; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period arr; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim3); oc.OCMode TIM_OCMODE_PWM1; oc.Pulse arr / 2; // 50%占空比驱动器看边沿 oc.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim3, oc, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); }这段代码里有两个关键参数。EncoderMode TIM_ENCODERMODE_TI12表示A/B两相都参与计数常见叫四倍频如果只选TI1脉冲数会少一半位置换算会整体偏。IC1Filter和IC2Filter不是越大越好滤波越大抗干扰越强但高速时可能漏掉边沿低速抖动和高速丢数要分开排查。StepPulse_TIM3_Init里的psc和arr决定STEP频率常见先设1kHz到2kHz让电机空载转起来再进加减速表。DIR和EN先当普通GPIO确认有效电平后再封函数。3. 编码器读数与测速把CNT寄存器变成可用的转速和位置编码器接口配置好之后CNT寄存器会随A/B相边沿加减但直接读CNT有两个坑一是16位定时器会回绕二是方向符号和机械安装方向不一定一致。位置换算要先把“计数差”转成“脉冲数”再按编码器线数、四倍频、减速比换算成角度或转速。测速常见用M法固定周期采样计数差除以采样时间和每转脉冲数。低速时M法分辨率差可以加滑动平均或改成T法测周期但先别在没跑通开环的时候上复合算法。3.1 编码器模式初始化与CNT读取读取CNT时不要用无符号数直接相减16位定时器从65535回到0时无符号减法会得到一个巨大的正数速度曲线会突然冒尖。常见做法是转成16位有符号差值再累加到32位总位置。下面这个函数只处理计数差调用周期由外部定时器决定。static int32_t Encoder_ReadDelta(void) { static uint16_t last 0; uint16_t now (uint16_t)__HAL_TIM_GET_COUNTER(htim2); int16_t diff (int16_t)(now - last); // 利用16位回绕得到有符号差值 last now; return (int32_t)diff; }now - last先按16位截断再转int16_t这样正转越过65535时差值为正反转越过0时差值为负。如果机械方向和你定义的正方向相反不要改这段改后面的符号系数或者把A/B相接线对调。总位置用一个int32_t累加常见小型控制板短时间内不会溢出但长时间运行要自己做限幅或归零。3.2 M法测速固定周期采样与滑动平均测速周期常见取5ms到20ms。周期太短低速时计数差只有几个脉冲速度跳得厉害周期太长响应慢闭环修正会滞后。下面用10ms定时器中断采样一阶低通做平滑。ENC_CPR要按实际编码器线数、四倍频和减速比改代码里只是示例结构不要照抄数值。#define ENC_CPR (13 * 4 * 4) // 示例线数*四倍频*减速比按实物改 #define SAMPLE_MS 10 static float g_speed_rpm 0.0f; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM4) { // TIM4配置为10ms周期中断 int32_t d Encoder_ReadDelta(); float rpm (d * 60000.0f) / (ENC_CPR * SAMPLE_MS); g_speed_rpm g_speed_rpm * 0.8f rpm * 0.2f; } }d * 60000.0f是把“每10ms脉冲数”换算成“每分钟脉冲数”再除以每转脉冲数得到rpm。低通系数0.2不是固定值要跟得快就加大要曲线稳就减小。调试时先把g_speed_rpm通过串口打出来用手慢慢转电机轴看正反转符号和数值是否连续。如果正转时速度正负跳变先查A/B相是否接反、滤波是否过大、编码器电源是否和MCU共地。3.3 方向判定与Z信号回零方向判定不要只看DIR引脚闭环系统里DIR是“我想往哪走”编码器差值才是“实际往哪走”。常见做法是用计数差符号做方向观测DIR只作为前馈。Z信号每转一个脉冲适合上电回零或长时间运行后的零位校准不适合在高速运行中反复清零CNT否则位置会跳。static volatile uint8_t g_zero_flag 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM5 htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { g_zero_flag 1; // 只置标志主循环里做校准 __HAL_TIM_SET_COUNTER(htim2, 0); // 低速回零时可用运行中慎用 } }Z信号捕获后只置标志真正的位置校准放到主循环避免中断里做浮点运算或长流程。回零时先让电机低速找Z找到后把总位置清零再检查编码器计数是否在零附近小幅抖动。如果Z信号一直不进中断先查定时器输入捕获通道和引脚复用再查Z信号是集电极开路还是推挽输出需不需要上拉。4. 步进脉冲生成定时器PWM、加减速表和方向时序步进电机的STEP脉冲可以用定时器PWM持续发也可以用定时器更新中断翻转IO。PWM方式占用CPU少适合中高速中断翻转灵活适合低速和脉冲数精确计数。常见做法是PWM发STEP同时用更新中断或脉冲计数回调数脉冲达到目标脉冲数就停。这里的关键参数是每转脉冲数、最大频率、加减速时间和方向建立时间任何一个拍脑袋设现场都可能丢步。4.1 PWM频率与步距角换算步进电机走一圈的脉冲数等于360除以步距角再乘以驱动器细分。常见两相步进电机步距角1.8度整步200脉冲每转驱动器设16细分就是3200脉冲每转。如果机械减速比是10输出轴一圈就是32000脉冲。频率和转速的关系是脉冲频率等于每秒脉冲数转速rpm等于脉冲频率乘60除以每转脉冲数。参数示例值说明步距角1.8度整步200脉冲/转驱动器细分163200脉冲/转减速比10输出轴32000脉冲/转目标转速60rpm输出轴每秒1转需要的STEP频率32000Hz高速时注意驱动器输入上限提示STEP频率不要一上来就给到驱动器标称上限先按空载1kHz到5kHz试转确认方向、使能和细分都对再逐步升频。电机啸叫不代表丢步但堵转、异响和位置偏差同时出现时先降频。4.2 梯形加减速表与定时器更新梯形加减速的核心是让频率按加速度上升到最大频率保持接近目标脉冲数时再降。下面给一个简化版按1ms调用更新频率accel单位是Hz每ms。实际项目里可以把加减速段做成表也可以实时算先跑通实时算再优化。typedef struct { uint32_t step_target; uint32_t step_now; uint32_t f_max; uint32_t accel; uint32_t f_cur; } Motion_t; static Motion_t g_motion {3200, 0, 20000, 200, 1000}; static uint32_t g_freq 1000; void Motion_Update(Motion_t *m) { if (m-step_now m-step_target) { g_freq 0; HAL_TIM_PWM_Stop(htim3, TIM_CHANNEL_1); return; } if (m-f_cur m-f_max) { m-f_cur m-accel; // 每1ms增加accel Hz if (m-f_cur m-f_max) m-f_cur m-f_max; } uint32_t tim_clk 72000000U; // 按实际定时器时钟改 uint32_t arr tim_clk / m-f_cur - 1; __HAL_TIM_SET_AUTORELOAD(htim3, arr); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, arr / 2); }tim_clk必须和实际定时器时钟一致常见F1系列TIM3在72MHzF4系列可能不同改之前先看时钟树。arr算出来要检查是否小于65535频率太低时ARR会很大16位定时器装不下需要加预分频。f_cur起步值不要低于驱动器能响应的最低频率太低时电机可能只听到嗡嗡声不转。4.3 方向切换与使能时序方向切换最常见的错误是脉冲还在发的时候直接翻DIR驱动器可能丢一步或者反向冲击。常见顺序是先停PWM等几个微秒改DIR再等方向建立时间最后重新开PWM。使能EN也要注意有些驱动器EN有效时电机锁轴无效时自由上电默认状态要明确。static void Step_SetDir(uint8_t dir) { HAL_TIM_PWM_Stop(htim3, TIM_CHANNEL_1); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, dir ? GPIO_PIN_SET : GPIO_PIN_RESET); for (volatile uint32_t i 0; i 2000; i) { __NOP(); // 粗略建立时间按主频调整 } HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); } static void Step_Enable(uint8_t en) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, en ? GPIO_PIN_SET : GPIO_PIN_RESET); }Step_SetDir里的空循环不是精确延时只适合先跑通。要做严格时序就用定时器或DWT计数。Step_Enable的有效电平按驱动器手册改如果上电电机就锁死先查EN极性再查DIR和STEP是否接反。调试时把示波器或逻辑分析仪接在STEP和DIR上看换向时脉冲是否真的停了比盯着代码猜快得多。5. 避坑与排查编码器抖动、丢步、PWM异常、方向反转的现场记录这套东西最容易让人怀疑人生的是“代码明明没改昨天能跑今天不跑”。STM32驱动编码器步进电机的排查要按信号链分段编码器读数是否干净STEP脉冲是否真的到了驱动器驱动器使能和细分是否对电机电源和逻辑地是否稳。下面几条是现场最常见的记录按现象、原因、解决写照着顺序查能少走很多弯路。5.1 编码器计数跳变与方向反现象电机静止时编码器读数每秒跳几十个计数速度曲线在0附近来回抖或者正转时速度显示为负。原因A/B相没有上拉或上拉太弱线太长没有屏蔽滤波参数太小也可能是A/B相接反或者编码器电源地和MCU地没有共地。解决先断电查A/B对地电阻和上拉示波器看静止时是否有毛刺把定时器滤波从0逐步加到6到10看跳变是否收敛确认共地后仍然方向反就对调A/B或者把速度符号取反。不要用软件死区把低速全部滤掉那样低速闭环会失效。5.2 步进电机丢步与堵转现象空载转得挺顺一装负载就走不到位脉冲数发完了编码器差值对不上或者低速时电机嗡嗡响但不转。原因加减速太猛启动频率超过电机响应驱动器电流设得太小细分和每转脉冲数换算错机械负载本身有卡点。解决先把f_max降到一半、accel降到三分之一试确认能走准再往上加查驱动器拨码或软件设置的相电流别只看额定电流用编码器差值做失步检测连续几个周期误差超过阈值就停脉冲报警。丢步不是靠加大电流硬顶先排除机械卡滞和共振区。5.3 PWM无输出、驱动器不动作现象代码里PWM已经开始示波器量PA6没有波形或者PA6有波形驱动器STEP灯不亮电机不锁轴。原因定时器通道复用没开引脚被别的外设占用PWM启动函数只启动了通道没启动定时器驱动器共阳/共阴接法反了或者EN极性不对。解决先量MCU引脚是否有波形没有就查GPIO复用和定时器时钟有波形但驱动器不认查驱动器输入限流电阻和共阳/共阴接法必要时用万用表量光耦输入端电压。EN先强制到有效状态听电机是否锁轴再查STEP。5.4 上电复位、串口乱码与地线干扰现象电机一转MCU就复位或者串口打印乱码用手摸板子又好了。原因电机电源和逻辑电源共地走线太长驱动器换向尖峰通过地线串进MCU编码器屏蔽层两端接地形成地环流逻辑侧缺少去耦电容。解决电机电源单独走粗线逻辑侧加100nF加10uF去耦必要时加磁珠或TVS编码器屏蔽层单端接地STEP/DIR线远离电机相线。复位问题不要先怀疑代码先看电源和地。5.5 方向反转与回零位置漂移现象每次回零后零位都不一样或者换向之后位置整体偏一截。原因Z信号捕获时没有做低速稳定判断换向时DIR建立时间不够编码器计数在换向瞬间有抖动。解决回零只用低速找到Z后连续采样几次位置稳定后再清零换向先停脉冲给足建立时间编码器差值做短窗口滤波但不要滤到影响实时性。零位漂移如果每次方向一致查机械安装和联轴器如果随机查干扰和Z信号质量。6. 闭环修正与验证编码器反馈如何给步进电机兜底步进电机开环跑得再顺负载一变就可能丢步编码器反馈的价值不是把步进当伺服用而是做失步检测和小范围位置修正。常见做法是设定目标脉冲数读编码器实际位置误差超过阈值时补一两个脉冲连续超限就停机和报警。下面这段只是修正骨架阈值和步长要按机械刚性、编码器分辨率和负载调。int32_t pos_err target_pulse - encoder_pulse; if (pos_err 5) { step_target 1; // 小步补脉冲避免过冲 } else if (pos_err -5) { step_target - 1; } if (abs(pos_err) 200) { // 连续大误差视为失步 Step_Enable(0); fault_flag 1; }验证时我一般分三步先静态用手转编码器看计数和方向是否连续再空载发1000个脉冲读编码器差值是否在允差内最后带载低速走往返记录每次停止后的误差。表格里三个指标够用静态跳变计数、空载往返误差、带载连续运行温升和误差。只要静态跳变压不住闭环参数怎么调都是玄学。从那以后我每次上电前都会先确认共地、EN极性和A/B相上拉再发第一个脉冲希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?