资讯详情

基于STM32定时器的双轴同步直线插补实现

📅 2026/10/26 3:39:22 | 华诺云谱 👁 阅读
基于STM32定时器的双轴同步直线插补实现
简介YSF4_HAL_MOTOR-219 双轴同时运动工程包面向STM32运动控制学习者与嵌入式开发者围绕定时器PWM输出、双轴同步和直线插补展开可用于机器人、自动化设备等场景的入门学习与方案参考。压缩包共660个文件、约7.14MB主体为368个C源文件、143个头文件、71个汇编文件及48个ICF链接脚本另含工程配置、HEX、PDF/DOCX说明和少量调试记录较完整地保留了源码、编译产物与目录结构。已有252人学习。读者可从中梳理HAL库下定时器配置、PWM调速、双轴同步及直线插补算法的代码组织结合工程文件理解编译链接、调试流程和参数整定思路。工程中还涉及梯形与S曲线加减速、编码器反馈、电机驱动设置及同步误差控制等考虑便于快速定位关键代码与验证思路适合作为课程设计、项目原型或排错对照的参考资料。1. 从“两个电机各自转”到“走一条直线”双轴联动的需求边界很多人第一次搭 XY 平台两个轴分开调都能跑单轴点动、回零、调速一切正常一旦要求两轴同时启动、同时到达画出来的线就变成斜一段、折一段甚至末端一轴先停、另一轴还在爬。问题通常不在驱动器也不在电机力矩而在于“谁来定两轴每一步的节奏”这件事没想清楚。这套 STM32 定时器驱动双轴同时运动的方案核心就是把直线插补和脉冲发生拆开一个节拍定时器统管两轴每次中断算一次“这一拍谁走、走几步”两轴的脉冲在同一个时间基准上发出。它适合正在做 XY 绘图仪、点胶平台、写字机、数控小台子这类毕设或课程设计的人也适合已经写过单轴步进代码、想补上联动这一块的工程师。下面从方案结构一路讲到怎么验收。2. 总体方案设计一个节拍定时器统管两轴脉冲先把最容易踩错的地方摆出来不少人给 X 轴配一个定时器、Y 轴配一个定时器各自算频率、各自跑中断。单轴看没问题两轴一起动就出问题——两个定时器的相位关系是随机的起步瞬间谁先谁后完全看运气频率一改更是各跑各的直线自然走不直。这套方案的选择是反过来的只用一个定时器做节拍源两轴的走步决策都在同一个中断里完成。2.1 职责划分插补层、规划层、执行层各管一段整个工程按三层切层与层之间只传结构体不互相摸对方的寄存器。层次负责的事不负责的事主要文件指令层解析 G01 X… Y… F… 这类运动指令压入指令队列不关心步数怎么算motion_cmd.c规划层由起点终点算两轴步数、方向、总步数、加减速参数不碰定时器和 GPIOinterpolate.c执行层定时器中断里做脉冲分配、翻转 GPIO、更新 ARR不知道直线还是圆弧motor_hw.c这么切的好处很实际调插补算法时不用连板子把Line_ISR()拿出来在 PC 上跑一遍把每拍的坐标打印出来画在纸上就知道直不直调硬件时也不用改算法只改 GPIO 宏定义。带过完整项目的人一般都会这么切因为一旦算法和硬件糊在一起改一个引脚都要重新编译验证整条链路血泪经验。规划层输出的结构体大致是这几个字段steps总步数取两轴步数的较大值、dx/dy两轴步数绝对值、sx/sy方向±1、v_target目标合成速度mm/s、acc加速度mm/s²。执行层只认这个结构体不认识“直线”这个概念将来要加圆弧插补只需要在规划层多写一个把圆弧离散成小线段的上层执行层一行不用改。2.2 定时器与引脚资源分配以常见的 STM32F4 为例主频 72MHz 到 168MHz 都行这套配置按 72MHz 写换芯片只需要重算 PSC。定时器分配如下资源用途关键配置TIM3节拍定时器产生插补中断PSC71计数频率 1MHzARR 动态改写GPIO PA0X 轴脉冲推挽输出默认低电平GPIO PA1Y 轴脉冲推挽输出默认低电平GPIO PA2 / PA3X / Y 方向推挽输出走步前先置方向GPIO PA4驱动器使能上电拉高急停拉低选 TIM3 是因为它是通用定时器、16 位、带更新中断够用且不占高级定时器的互补输出资源。PSC 取 71 把 72MHz 分成 1MHz也就是计数器每 1 微秒加一这样 ARR 的值直接对应“多少微秒一拍”改速度就是改一个整数心里有数。步进频率 5kHz 时 ARR19910kHz 时 ARR9920kHz 时 ARR49都在 16 位范围内不需要动 PSC。有一个取舍要说清楚这套方案在中断里直接把 GPIO 拉高、短延时、拉低脉冲宽度靠几个__NOP()撑到 2~3 微秒能满足大多数细分驱动器的要求。代价是步频上限受中断耗时限制实测在 72MHz 下跑到 15kHz 左右还算稳再往上就要换思路——把脉冲发生交给定时器的 PWM 通道或单脉冲模式中断只做插补决策和通道使能。毕设和课设的行程、速度要求一般在 10kHz 以内直接 GPIO 翻转是最省事也最好讲清楚的写法。2.3 实时性预算中断里到底能干多少活有人担心“中断里算插补会不会太慢”。算一笔账一次中断要做的事是读结构体、比较两个整数、决定走哪轴、翻转 GPIO、算一次 ARR、写寄存器编译出来大概 80~150 条指令在 72MHz 上 2~4 微秒。10kHz 步频时中断周期 100 微秒CPU 占用 2%~4%主循环还能跑串口、按键、屏幕刷新。真正会翻车的是在中断里塞了浮点开方、printf、或者 HAL 的阻塞延时这几样随便沾一个占用立刻翻几倍高步频时就丢拍。所以规划层的浮点运算要放在启动阶段一次算完中断里只做整数比较和一次简单的速度迭代具体见第 3 章的代码。这条边界划清楚整套方案的稳定性就有底了。3. 直线插补实现误差累积分配与梯形加减速的配合直线插补要回答两个问题这一拍两轴各走几步这一拍隔多久发。前一个问题是几何问题后一个问题是速度问题两者必须分开算否则改速度会牵动走步比例直线就走歪了。3.1 误差累积为什么主轴每拍必走一步两轴步距相同的前提下直线插补最常用的做法是以步数多的那根轴作为主轴主轴每一拍固定走一步从轴按累积误差决定这一拍走不走。这样总拍数就等于主轴步数两轴必然同时启动、同时收尾末端不会出现一轴先停的台阶。/* 直线插补上下文规划层算好执行层只读 */ typedef struct { int32_t dx, dy; /* 两轴步数绝对值 */ int32_t sx, sy; /* 方向1 或 -1 */ int32_t steps; /* 总拍数 max(dx, dy) */ int32_t err; /* 从轴误差累积器 */ int32_t cur; /* 已走拍数 */ uint8_t done; /* 1 本段结束 */ } LineCtx_t; /* 每次节拍中断调用一次产生一拍脉冲 */ void Line_ISR(LineCtx_t *L) { if (L-done) return; if (L-dx L-dy) { /* X 为主轴每拍必走 X累积够了再补一步 Y */ L-err L-dy; Step_X(L-sx); if (L-err L-dx) { L-err - L-dx; Step_Y(L-sy); } } else { /* Y 为主轴逻辑对称 */ L-err L-dx; Step_Y(L-sy); if (L-err L-dy) { L-err - L-dy; Step_X(L-sx); } } if (L-cur L-steps) L-done 1; }err的含义是“从轴欠了多少步”。主轴每走一步给err加上从轴步数一旦err超过主轴步数说明累积误差够一步了从轴补一步并把err减回去。整个过程只有整数加减和比较没有除法、没有浮点中断里跑起来很轻。判断主轴用而不是是为了在 45 度这种两轴步数相等的极限情况下让 X 稳定地当主轴避免每次编译优化不同导致主轴选择抖动。Step_X()/Step_Y()里做的事是按方向置好方向脚、拉高脉冲脚、短延时、拉低。方向脚只在方向变化时改不要每拍都写写多了在某些驱动器上会被当成噪声。这套写法的直线度误差不超过半个脉冲当量也就是一个步距以内肉眼和卡尺都看不出来。有人喜欢用 DDA 的浮点累加版本效果一样但中断里跑浮点要做现场保护F4 的 FPU 在中断里用要额外小心不如整数省心。3.2 速度换算把 mm/s 变成定时器 ARR速度这一层要处理两个换算。第一个是合成速度换成主轴步频。设脉冲当量step_mm毫米每步比如丝杆导程 4mm、1.8 度电机 16 细分step_mm 4 / (200 × 16) 0.00125即 800 步每毫米。合成长度用步数表示是hypot(dx, dy)主轴步数是steps那么主轴步频/* 启动前算一次结果存进上下文 */ float len_steps sqrtf((float)dx * dx (float)dy * dy); float f_main (v_target / step_mm) * ((float)steps / len_steps);steps / len_steps这一项就是主轴在合成方向上的投影比例45 度时是 0.707沿 X 轴走时是 1。少了这一项斜着走的速度会比直着走快画方框时四个角的速度不一致一眼就能看出来。第二个换算是步频换成 ARR。计数频率 1MHz 时/* f_step 单位 Hz返回写进 ARR 的值 */ static uint32_t FreqToARR(float f_step) { float arr 1000000.0f / f_step - 1.0f; if (arr 20.0f) arr 20.0f; /* 上限约 47kHz保护中断耗时 */ if (arr 60000.0f) arr 60000.0f; /* 下限约 16Hz防止停转 */ return (uint32_t)arr; }上限 20 对应约 47kHz实际跑不到那么高留这个值只是防止参数填错时 ARR 变成 0 导致定时器行为异常。下限 60000 是给极慢的手动点动留的余量低于这个频率电机听着就是一格一格地响不如直接停。写完 ARR 之后记得调一次__HAL_TIM_SET_AUTORELOAD()下一个周期才生效别去改计数器。3.3 梯形加减速与减速点预判恒速启动在轻载下能用负载稍大就丢步所以加减速要有。梯形曲线比 S 型简单参数只有加速度accmm/s²和目标速度v_target够应付绝大多数课设场景。加速段每拍更新一次速度用迭代而不是开方/* 每拍调用dt 是上一拍的周期单位秒 */ static void Speed_Update(void) { float dt 1.0f / cur_freq; switch (state) { case ST_ACC: v ACC * dt; /* 线性加速 */ if (v v_target) { v v_target; state ST_RUN; } break; case ST_RUN: /* 剩余步数进入减速区提前切减速 */ if (steps_left dec_steps) state ST_DEC; break; case ST_DEC: v - ACC * dt; if (v V_MIN) v V_MIN; /* 留一点末速度别减到 0 */ break; } cur_freq (v / step_mm) * proj_ratio; /* 合成速度 - 主轴步频 */ __HAL_TIM_SET_AUTORELOAD(htim3, FreqToARR(cur_freq)); }dec_steps是减速段需要多少拍启动时一次算好dec_steps v_target² / (2 × acc) / step_mm × proj_ratio。这里单位要统一到“拍”算错了会怎样算大了提前减速走一半就开始爬算小了到终点还在全速直接过冲撞限位。我的习惯是把dec_steps打出来跟总步数比一下超过总步数的一半就说明加速度设小了或者速度设高了这段直线根本加速不起来得先降速。V_MIN不要设成 0最后几拍速度太低会让 ARR 顶到下限最后一拍的时间被拉长末端看起来像“顿一下”。给个 5~10mm/s 的末速度走完直接停脉冲反而干净。4. HAL 落地定时器中断、脉冲输出与状态机的接线方式算法层写完之后落到 HAL 上的工作量其实不大关键是把初始化的顺序和中断里的边界处理好。4.1 定时器初始化PSC 定分辨率ARR 定速度/* TIM31MHz 计数ARR 由速度动态改写先给个安全值 */ static void MX_TIM3_Init(void) { TIM_ClockConfigTypeDef clk {0}; TIM_MasterConfigTypeDef mst {0}; htim3.Instance TIM3; htim3.Init.Prescaler 71; /* 72MHz / 72 1MHz */ htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 999; /* 初始 1kHz启动时会被覆盖 */ htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(htim3); clk.ClockSource TIM_CLOCKSOURCE_INTERNAL; HAL_TIM_ConfigClockSource(htim3, clk); mst.MasterOutputTrigger TIM_TRGO_RESET; mst.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim3, mst); }AutoReloadPreload必须打开。不打开的话中断里写 ARR 有可能正好撞上计数器溢出新值要等下一轮才生效表现出来就是速度曲线有一拍是乱的高速时听着电机“咔”一下。这个坑很隐蔽示波器上才能看见某一拍脉宽明显不对。启动和停止都封成函数别在主循环里直接操作寄存器void Motion_Start(LineCtx_t *L) { if (L-steps 0) return; cur_freq V_START / step_mm * proj_ratio; /* 起始速度 */ __HAL_TIM_SET_AUTORELOAD(htim3, FreqToARR(cur_freq)); __HAL_TIM_SET_COUNTER(htim3, 0); state ST_ACC; HAL_TIM_Base_Start_IT(htim3); } void Motion_Stop(void) { HAL_TIM_Base_Stop_IT(htim3); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); /* 关使能 */ state ST_IDLE; }Motion_Stop()里先停中断再关使能顺序反了的话关使能的瞬间可能还有一拍脉冲在路上电机发出一声异响。急停按钮直接调这个函数不要在里面加延时或打印。4.2 中断回调把插补结果落到 GPIOHAL 的更新中断最终走到HAL_TIM_PeriodElapsedCallback()这里只做一件事——调插补别的都别塞。void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance ! TIM3) return; if (line.done) { /* 本段走完立刻停不再产生脉冲 */ Motion_Stop(); cmd_queue_pop_and_start(); /* 取下一段没有就空转 */ return; } Line_ISR(line); /* 走一拍决策 发脉冲 */ Speed_Update(); /* 更新下一拍的 ARR */ }注意line.done的判断放在最前面。放在后面的话最后一步走完还会多产生一拍脉冲行程累积误差走一百条线段就偏出去一步。这种“多一拍”的问题在短线段多的图形上特别明显画个圆能看出来接缝处有毛刺。Step_X()内部大致是这样static inline void Step_X(int8_t dir) { if (dir ! last_dir_x) { /* 方向变了才写 */ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, dir 0 ? GPIO_PIN_SET : GPIO_PIN_RESET); last_dir_x dir; } HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); for (volatile int i 0; i 20; i) __NOP(); /* 约 2~3us 脉宽 */ HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); }那个for循环的圈数要按主频实测。72MHz 下 20 圈大约 2.5 微秒168MHz 下要加到 45 圈左右。脉宽不够驱动器认不到脉宽太长高步频时占空比过大下一拍还没拉低就来了。用示波器量一下最稳别拍脑袋。4.3 指令队列与运动状态机上层只往队列里塞目标点中断负责消费两边用一个头尾指针加__disable_irq()保护临界区队列入队出队各三五条指令比上 RTOS 轻得多。状态机的四个状态ST_IDLE / ST_ACC / ST_RUN / ST_DEC只在中断里改主循环只读用于界面显示。这样避免了主循环写状态、中断也写状态导致的竞争。要做暂停功能加一个ST_PAUSE中断里看到这个状态就直接 return 不发脉冲但定时器还在跑恢复时不用重配 ARR比反复启停定时器平滑。5. 常见问题排查脉冲丢步、直线走偏与启动抖动的四类根因这一章列的都是实际调试时最常撞上的按“现象 → 原因 → 解决”写对着查能省不少时间。5.1 脉冲层走不动、丢步、频率上不去现象一低速正常速度一提就丢步电机嗡嗡响但不转。原因多半是中断耗时超过了节拍周期。中断里如果有浮点开方、printf、HAL 的阻塞延时单次耗时可能到十几微秒节拍 100 微秒时看着还能跑节拍降到 50 微秒就顶不住了脉冲被吞掉。解决是把浮点运算全部挪到启动阶段中断里只留整数比较用示波器量一下中断服务函数的执行时间留出至少 50% 余量。另一个可能原因是驱动器要求的最小脉宽没满足for循环圈数按主频重新标定。现象二两轴分别测都正常一起动就有一轴明显滞后。原因是两轴的Step_X()/Step_Y()里各有一段忙等脉宽延时先调的那轴延时会挤占后调那轴的时间一拍的周期被拉长频率越高偏差越大。解决是把两轴的脉冲同时拉高、延时一次、再同时拉低让延时只付一次或者干脆改用定时器 PWM 通道输出脉冲中断只负责使能脉宽由硬件保证。我一般会把两轴的置位写在紧挨着的两行中间只隔几条指令同步误差能压到几十纳秒。现象三跑几分钟后突然停住复位又能跑。先查 ARR 是不是被算成了 0 或极小值。速度参数填错、proj_ratio算成 0、除零得到无穷大都会让FreqToARR()返回一个非法值定时器行为异常。加一句上下限钳位就能挡住。另一种情况是方向脚每拍都写某些驱动器把频繁的方向翻转当成干扰信号内部保护性停机改成方向变化时才写即可。5.2 插补与机械层走偏、过冲、起步抖动现象四理论上该走直线的两点实际走出来中间鼓一点或者末端拐一下。九成是主轴选择或误差累积写错。检查err的比较阈值用的是dx还是dy主轴分支里的累积量必须是“从轴步数”一旦写反走出来是阶梯状的折线。还有一种情况是steps用了dx dy这样总拍数偏多末端会多走一段直线末端出现一个小勾。正确做法是取max(dx, dy)。调试时把每拍的(x, y)打印出来在纸上点一遍一眼就能看出问题。现象五起步瞬间声音发闷、负载大时丢步或者到终点“哐”一下撞上去。丢步是起始速度设太高直接从 0 跳到目标速度惯量大的平台扛不住。把起始速度压到目标速度的 10%~20%加速段就有了缓冲。撞终点是dec_steps算小了减速点预判偏晚全速冲到末端才想起来减速。算完之后跟总步数比一比减速段超过总步数一半就说明这段直线的规划本身不合理要么降速要么降加速度。还有一种隐蔽情况短线段很多时每段都重新从起始速度开始加速速度永远上不去整体走起来一顿一顿这种要在规划层做前瞻把连续共线的短段合并属于进阶话题先把单段跑稳再考虑。6. 验收与答辩用三个测试把“同时运动”讲清楚方案做完了怎么证明它真的“同时”答辩现场最怕说不清。我的做法是准备三个能当场复现的测试每个都有量化指标。验收项测试方法合格标准直线度画一条 100mm 的 45 度线量起点到终点的连线与理论直线的最大偏差偏差 ≤ 2 个脉冲当量约 0.0025mm同步性用示波器双通道同时抓两轴脉冲看首拍和末拍的相对位置首拍时间差 5us末拍时间差 一拍周期速度一致性分别走 X 向、Y 向、45 度三条 100mm 线记录耗时三条线耗时偏差 3%加减速从 0 加速到 50mm/s 再停观察有无丢步连续 20 次往返位置误差不累积直线度这一项量不到 2 个脉冲当量也没关系只要说明用的是整数误差累积、理论误差上限就是一步比含糊地说“挺直的”强得多。同步性用示波器抓是最有说服力的把两轴的首个脉冲和最后一个脉冲截图放进答辩 PPT比讲十分钟算法管用。答辩时容易被问到的两个问题提前准备好答案一是“为什么不用两个定时器”答两轴相位不可控、改速度时容易不同步二是“为什么中断里发脉冲而不是 PWM”答频率要求不高、逻辑直观、便于观察每一步的决策过程同时说明高频场景的替代方案。这两个问题答上来评委基本就认可你想清楚了。还有一个我自己的习惯出图之前一定会先跑一遍“单段长直线 单段短直线 连续折线”这组最小用例用 A4 纸垫在笔下面画出来用眼睛先看一遍。纸上都歪了示波器上数据再漂亮也没用。从那以后我每次改插补代码都强制走一遍这组用例再看后面的指标。希望这套结构能帮到你少在同步问题上绕弯路。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑