资讯动态

STM32驱动10路步进电机:定时器+DMA脉冲方案与避坑指南

发布时间:2026/10/17 4:44:25 来源:尧图企业网站定制
简介这份资源面向学习STM32嵌入式开发、需要多路电机协同控制的工程师与学生提供一套可运行的十路步进电机驱动方案。工程通过串口发送指令即可控制每个电机的正反转、旋转角度与运行速度并引入闭环控制思路适合用来搭建多轴运动平台、机械臂或自动化设备的原型验证。压缩包共232个文件约7.94MB以41个c源文件与43个h头文件构成核心驱动与STM32标准外设库另有42个o、41个crf等编译中间文件以及uvprojx、uvoptx等Keil工程配置和hex、axf可执行产物目录结构完整可直接导入编译。目前已有1782人学习下载。读者可从中获取串口指令解析、定时器PWM调速、多电机分时调度与闭环反馈的完整代码框架并借助现成工程快速复现与二次开发省去从零搭建底层驱动的成本。1. 十个步进电机同时转起来STM32 到底扛不扛得住很多人第一次听到「STM32 驱动十个步进电机」脑子里第一反应是「IO 口够不够」。真上手才发现IO 从来不是瓶颈瓶颈是脉冲时序和CPU 被中断吃干净。我最早做的一个模拟项目用某款常见的 STM32F103 去带 10 个 42 步进电机做点位运动单轴跑得好好的十轴一起发脉冲第三个轴就开始丢步示波器一抓脉冲周期抖得像心电图。问题不在电机也不在驱动器在于我用软件延时在主循环里轮流翻转 IO十个轴互相抢 CPU谁都发不准。这个标题真正要解决的问题是在有限的定时器资源下让 10 路步进脉冲各自独立、频率可调、加减速可控同时 CPU 还有余量去处理串口指令和限位信号。适合两类人一类是做多轴小型运动平台的比如点胶、贴片、多工位送料另一类是学生做毕设或竞赛需要多电机协同但预算和板子都有限。核心思路只有一句话——把脉冲生成这件事从 CPU 手里拿走交给定时器和 DMA。下面我按选型、配置、代码、避坑的顺序把这条路走一遍。2. 先算清楚资源账定时器、通道和脉冲从哪来2.1 十个电机需要几路脉冲STM32 到底有几个定时器步进电机的驱动器常见的是细分型驱动器只需要两个信号PUL脉冲和 DIR方向有些还有 ENA使能。所以 10 个电机 10 路脉冲 10 路方向 可选 10 路使能。方向脚用普通 GPIO 就够慢速翻转无所谓脉冲脚才是关键必须由定时器产生。以常见的 STM32F103 系列为例它有 TIM1~TIM4 四个通用/高级定时器每个定时器有 4 个独立通道。理论上 4×416 路 PWM够 10 路用。但这里有个坑同一个定时器的所有通道共用同一个计数器 CNT 和预分频 PSC也就是说同一个定时器出来的 4 路脉冲频率必须一样只有占空比能各自调。如果你要求 10 个电机跑不同速度就不能简单地把它们塞进 4 个定时器的通道里。常见做法有两种方案脉冲来源能独立调速的轴数CPU 占用适用场景方案 A每个定时器 1 路 PWM配 DMA 改 ARR4 轴低轴数少、要求高方案 B定时器更新中断里软件翻转 IO任意极高轴数少、频率低方案 C定时器 PWM 外部脉冲扩展芯片可扩展低轴数多、成本敏感10 轴独立调速F103 的 4 个定时器不够用。我一般会选定时器资源更多的型号比如带 TIM1~TIM8 的 STM32F4 系列或者用「定时器 移位寄存器/专用脉冲芯片」的组合。如果非要用 F103退而求其次的做法是把 10 个轴按速度分组同组共用定时器组内用 DIR 和使能区分——但这只适合速度要求不高的场合。2.2 脉冲频率和细分怎么换算成定时器参数步进电机的转速公式要先搞清楚。设电机步距角为 1.8°即 200 步/圈驱动器细分设为 N则转一圈需要的脉冲数脉冲数/圈 200 × N如果要求转速为 R 转/秒则脉冲频率f 200 × N × R比如细分 16转速 2 转/秒f 200×16×2 6400 Hz。这个频率决定了定时器的 ARR 和 PSC。STM32 定时器输出 PWM 频率公式f_pwm f_clk / ((PSC1) × (ARR1))其中 f_clk 是定时器时钟F103 主频 72MHz 时APB1 定时器时钟 72MHzAPB2 也是 72MHz具体看时钟树配置。要得到 6400Hz可以取 PSC0ARR 72e6/6400 - 1 11249。ARR 越大频率分辨率越高调速越平滑但 ARR 太大时改频率的响应会变慢。我一般让 ARR 保持在 1000~60000 之间兼顾分辨率和响应。提示细分不是越高越好。细分 32 以上时脉冲频率成倍上升普通光耦驱动器可能跟不上脉冲边沿会变缓导致丢步。16 细分对大多数小型平台够用。2.3 为什么必须上 DMA而不是在中断里改 ARR如果只用 PWM频率是固定的没法加减速。要调速就得在运行中改 ARR 或 PSC。最朴素的做法是在定时器更新中断里改 ARR但 10 个轴、每个轴每秒几千次中断CPU 直接被打满。我实测过F103 在 72MHz 下10 轴各 5kHz 中断光进中断出中断就吃掉 60% 以上的 CPU主循环里串口指令响应延迟肉眼可见。正确做法是用 DMA 把预先算好的 ARR 序列搬运到定时器的 ARR 寄存器。定时器每次更新事件触发一次 DMA 请求DMA 把下一个 ARR 值写进去CPU 完全不参与。这样加减速曲线可以提前算好运行时只是 DMA 在搬数据。代价是每个轴要占一个 DMA 通道10 个轴就是 10 个 DMA 通道——F103 只有 7 个 DMA 通道DMA1不够F4 系列 DMA 通道多能撑住。3. 用定时器 DMA 把 10 路脉冲跑起来3.1 硬件连接和驱动器接线确认先确认硬件。每个步进电机配一个细分驱动器驱动器输入端接 STM32 的 PUL、DIR、ENA输出端接电机四线。PUL 和 DIR 通常用光耦隔离所以 STM32 的 IO 要能提供足够的灌电流一般 8mA 以上。共阳接法下PUL 有效是低电平这点在配置 PWM 极性时要对应上。我一般这样分配引脚PUL接定时器通道输出如 TIM1_CH1、TIM2_CH2 等DIR普通 GPIO 推挽输出ENA普通 GPIO 推挽输出或直接接固定电平接线完成后先不写代码用示波器或逻辑分析仪确认每个 PUL 脚在空载时能输出干净方波。这一步能省掉后面大量「以为是代码问题其实是接线问题」的排查时间。3.2 定时器 PWM DMA 的最小配置代码下面以 STM32F4 的 HAL 库为例给出一路脉冲的配置骨架。10 路就是把这个过程重复 10 次换不同的定时器和通道。// 以 TIM1 CH1 为例输出脉冲DMA 搬运 ARR TIM_HandleTypeDef htim1; DMA_HandleTypeDef hdma_tim1_up; // 预先算好的 ARR 序列长度根据加减速曲线决定 #define ARR_BUF_LEN 200 uint32_t arr_buf[ARR_BUF_LEN]; void MX_TIM1_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig {0}; TIM_MasterConfigTypeDef sMasterConfig {0}; TIM_OC_InitTypeDef sConfigOC {0}; htim1.Instance TIM1; htim1.Init.Prescaler 0; // 不分频72MHz 计数 htim1.Init.CounterMode TIM_COUNTERMODE_UP; htim1.Init.Period 11249; // 初始 ARR对应 6400Hz htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 0; htim1.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(htim1); sClockSourceConfig.ClockSource TIM_CLOCKSOURCE_INTERNAL; HAL_TIM_ConfigClockSource(htim1, sClockSourceConfig); HAL_TIM_PWM_Init(htim1); sMasterConfig.MasterOutputTrigger TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim1, sMasterConfig); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 5625; // 占空比 50% sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); // 配置 DMA定时器更新事件触发搬运 ARR __HAL_RCC_DMA2_CLK_ENABLE(); hdma_tim1_up.Instance DMA2_Stream5; hdma_tim1_up.Init.Channel DMA_CHANNEL_6; hdma_tim1_up.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_tim1_up.Init.PeriphInc DMA_PINC_DISABLE; hdma_tim1_up.Init.MemInc DMA_MINC_ENABLE; hdma_tim1_up.Init.PeriphDataAlignment DMA_PDATAALIGN_WORD; hdma_tim1_up.Init.MemDataAlignment DMA_MDATAALIGN_WORD; hdma_tim1_up.Init.Mode DMA_NORMAL; // 单次模式搬完停 hdma_tim1_up.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_tim1_up); __HAL_LINKDMA(htim1, hdma[TIM_DMA_ID_UPDATE], hdma_tim1_up); HAL_NVIC_SetPriority(DMA2_Stream5_IRQn, 1, 0); HAL_NVIC_EnableIRQ(DMA2_Stream5_IRQn); }这段代码做了三件事把 TIM1 配成 72MHz 计数、初始 6400Hz 的 PWM把 DMA2 Stream5 挂到 TIM1 的更新事件上设置 DMA 为内存到外设、内存地址递增、单次模式。关键参数是PeriodARR和PulseCCR前者决定频率后者决定占空比。步进驱动器一般要求 50% 占空比所以 Pulse 取 Period 的一半。3.3 加减速曲线怎么算进 ARR 数组DMA 搬运的是 ARR 序列这个序列就是加减速曲线。最简单的是梯形加减速加速段频率线性上升匀速段频率不变减速段线性下降。把每个时刻的频率换算成 ARR填进数组。// 生成梯形加减速的 ARR 序列 // f_start: 起始频率, f_max: 最高频率, accel_steps: 加速段脉冲数 void build_trapezoid_arr(uint32_t *buf, uint32_t f_start, uint32_t f_max, uint32_t accel_steps, uint32_t total_steps) { uint32_t i; float df (float)(f_max - f_start) / accel_steps; for (i 0; i total_steps; i) { float f; if (i accel_steps) { f f_start df * i; // 加速段 } else if (i total_steps - accel_steps) { f f_max; // 匀速段 } else { uint32_t k total_steps - i; f f_start df * k; // 减速段 } if (f 100.0f) f 100.0f; // 频率下限保护 buf[i] (uint32_t)(72000000.0f / f) - 1; // 换算成 ARR } }逻辑说明f_start是启动频率不能太低也不能太高太低电机响应慢太高会堵转一般取 500~2000Hzf_max是匀速频率由目标转速决定accel_steps是加速段占用的脉冲数越大加速越平缓。buf[i]存的是 ARR 值DMA 每次更新事件取一个定时器频率就按这个序列变化。注意最后要加频率下限保护防止 ARR 溢出。参数怎么定加速段脉冲数一般取总脉冲数的 10%~20%。如果电机带载大加速段要拉长否则会丢步。我一般先用保守参数跑通再逐步缩短加速段直到电机刚好不丢步为止。3.4 十路脉冲的启动、停止和方向控制10 路配置好之后启动逻辑要统一管理。我一般用一个结构体数组描述每个轴的状态typedef struct { TIM_HandleTypeDef *htim; uint32_t channel; GPIO_TypeDef *dir_port; uint16_t dir_pin; uint32_t *arr_buf; uint32_t total_steps; volatile uint8_t running; } Axis_t; Axis_t axes[10]; void axis_start(uint8_t id, uint8_t dir, uint32_t steps) { Axis_t *ax axes[id]; // 设置方向 HAL_GPIO_WritePin(ax-dir_port, ax-dir_pin, dir ? GPIO_PIN_SET : GPIO_PIN_RESET); // 生成 ARR 序列 build_trapezoid_arr(ax-arr_buf, 1000, 6400, steps/10, steps); ax-total_steps steps; ax-running 1; // 启动 PWM 和 DMA HAL_TIM_PWM_Start(ax-htim, ax-channel); HAL_DMA_Start_IT(ax-htim-hdma[TIM_DMA_ID_UPDATE], (uint32_t)ax-arr_buf, (uint32_t)ax-htim-Instance-ARR, steps); } void axis_stop(uint8_t id) { Axis_t *ax axes[id]; HAL_TIM_PWM_Stop(ax-htim, ax-channel); HAL_DMA_Abort(ax-htim-hdma[TIM_DMA_ID_UPDATE]); ax-running 0; }方向控制就是提前把 DIR 脚拉高或拉低注意 DIR 变化要在脉冲停止时进行运行中改方向会丢步甚至堵转。停止时先停 PWM 再停 DMA顺序反了可能残留一个脉冲。每个轴的 DMA 传输完成中断里把running清零主循环轮询这个标志就知道哪个轴跑完了。4. 十个轴一起跑最容易翻车的几个地方4.1 现象某个轴偶尔丢步其他轴正常原因通常有三种一是该轴的 DMA 通道和别的外设冲突比如和 ADC 共用同一个 DMA Stream二是该轴的 ARR 序列里有异常值比如除零或溢出三是该轴的脉冲频率超过了驱动器光耦的响应上限。排查方法先用示波器单独抓这个轴的脉冲看频率和占空比是否正常再把它的 ARR 序列打印出来检查有没有突变。如果是 DMA 冲突换一个 Stream 或调整优先级。4.2 现象十轴同时启动时电源电压被拉低电机啸叫这是电源功率不够。10 个步进电机同时加速瞬时电流可能是额定电流的 2~3 倍。我遇到过 10 个 42 电机共用一路 24V/5A 电源同时启动时电压掉到 18V驱动器欠压保护。解决办法要么加大电源功率要么让各轴错峰启动间隔 50~100ms。错峰启动在代码里很好实现启动循环里加个延时就行。4.3 现象DMA 传输完成后电机还在转停不下来原因是 DMA 传输完成中断里只清了标志没有停 PWM。DMA 搬完最后一个 ARR 后定时器还在用最后一个 ARR 继续计数PWM 不会自动停。正确做法是在 DMA 完成回调里调用HAL_TIM_PWM_Stop。另外要注意DMA 单次模式搬完后要重新配置才能再次启动不能直接重启。4.4 现象改了 ARR 但频率没变TIM 的 ARR 有预装载功能如果AutoReloadPreload没使能写 ARR 会立即生效可能造成当前周期被截断如果使能了要等下一个更新事件才生效。DMA 搬运 ARR 时一般要使能预装载让新值在更新事件时统一生效。如果发现频率没变先检查TIM_AUTORELOAD_PRELOAD_ENABLE有没有配上。4.5 现象串口指令响应变慢甚至丢指令这是 CPU 被中断占满的典型表现。检查是不是在定时器中断里做了太多事或者 DMA 完成中断里执行了耗时操作。原则是中断里只置标志复杂逻辑放主循环。另外10 个轴的 DMA 完成中断如果同时触发会形成中断风暴可以考虑合并处理或者用轮询代替中断。5. 把 10 轴方案做稳的两个进阶技巧第一个技巧是用定时器的从模式做脉冲计数。步进电机丢步的本质是「发了 N 个脉冲但电机没走 N 步」如果能在硬件层面统计实际发出的脉冲数就能和理论值对比发现丢步。做法是把脉冲输出脚同时接到另一个定时器的外部时钟输入脚用那个定时器计数。这样不占 CPU精度也高。我一般会留一个定时器专门做这个统计10 个轴分时复用或者用多个定时器。第二个技巧是把加减速曲线做成 S 形而不是梯形。梯形加减速在加速段和匀速段的拐点处加速度突变电机会有轻微冲击高速时容易丢步。S 形曲线在拐点处平滑过渡代价是计算量稍大。实现上不用复杂公式用分段线性逼近就行把加速段再分成「加加速」和「减加速」两段每段频率变化率不同。下面是一个简化版// S 形加减速加速段分两半前半加速率递增后半递减 void build_s_curve_arr(uint32_t *buf, uint32_t f_start, uint32_t f_max, uint32_t accel_steps, uint32_t total_steps) { uint32_t i; uint32_t half accel_steps / 2; for (i 0; i total_steps; i) { float f; if (i half) { // 加加速段二次曲线上升 float t (float)i / half; f f_start (f_max - f_start) * t * t; } else if (i accel_steps) { // 减加速段二次曲线逼近 float t (float)(accel_steps - i) / half; f f_max - (f_max - f_start) * t * t; } else if (i total_steps - accel_steps) { f f_max; } else { uint32_t k total_steps - i; float t (float)k / accel_steps; f f_start (f_max - f_start) * t; } if (f 100.0f) f 100.0f; buf[i] (uint32_t)(72000000.0f / f) - 1; } }这个版本只是示意实际调参时我会先用示波器看脉冲频率曲线确认没有突变点再上电机测试。S 形曲线的加速段通常要比梯形长 20%~30%才能达到同样的最高频率而不丢步。验证方法上我习惯做两步第一步空载跑用逻辑分析仪抓 10 路脉冲确认每路的频率序列和理论值一致第二步带载跑在电机轴上贴个标记跑 1000 步后用肉眼或编码器确认实际转角和理论值偏差在允许范围内。偏差超过 1% 就要查加速段参数或电源。最后说个血泪经验别在没确认电源和接线之前调代码。我最早那版方案调了三天丢步问题最后发现是一个驱动器的 PUL 线虚焊。从那以后我每次都是先示波器看波形再看代码。这个习惯帮我省下的时间比任何优化技巧都多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑