简介:面向STM32运动控制学习者与自动化、机器人方向开发者的双轴联动直线插补工程包,围绕定时器PWM输出实现两轴同步运动,涉及预分频与计数器配置、占空比调节、梯形或S曲线加减速及位置步进分解,适合在具备HAL库基础后进阶理解实时控制与轨迹规划。压缩包共660个文件、约7.14MB,以368个C源文件和143个头文件为主体,辅以启动汇编、链接脚本、批处理及hex、axf等构建产物,并包含多种IDE工程配置,便于直接编译、对照与移植。已有252人学习下载。读者可从中获得一套可运行的双轴插补参考实现,结合定时器同步、插补算法与编码器闭环思路,快速搭建实验平台并排查运动不同步、加减速冲击等问题。
1. 两轴联动的第一道坎:单轴跑得再准,拼起来也会走歪
两根轴分开调的时候都挺听话,点动、回零、单轴定位一个不差;可一旦让它们同时起步去画一条斜线,轨迹走到中途开始偏,末端两轴的到达时间差出十几毫秒,直线硬生生变成一道弧。这是两轴同时运动项目里最典型的第一道坎——难点从来不在单个电机能不能转,而在于两路脉冲在时间轴上怎么对齐,每个时刻两轴各该分到多少步。这套基于 STM32 定时器与直线插补的双轴运动方案拆的就是这件事:定时器做统一时基,插补算法把一条直线拆成两轴的步数分配,HAL 层把外设配置固化下来,让双轴在同一条时间线上各自走完自己的路径。适合正在做运动控制课设、毕设,或者要给小型雕刻机、点胶机、写字机搭一套联动底盘的人,拿去当骨架改。
2. 总体架构:三层分工把「同时」这件事拆开
2.1 主时基为什么必须交给定时器
主循环轮询是新手最容易踩的坑。用while(1)里读标志位、算插补、翻 IO 的写法,单轴低速跑看着没问题,一旦把速度提到 5 kHz 以上,串口打印、按键扫描、LCD 刷新随便哪个函数多花几微秒,脉冲周期就抖一下。步进电机对周期抖动极其敏感,表现出来就是啸叫、丢步、走位偏移,排查半天找不到原因,因为逻辑本身没错,错在时基。
改成定时器更新中断做节拍源之后,节拍由硬件产生,只要中断服务函数写得足够短,周期抖动可以压到几十纳秒量级。这里的关键取舍是:主循环只负责「决定去哪」,定时器中断只负责「这一拍怎么走」。所有带浮点、带打印、带分支判断的路径规划逻辑,一律放在主循环里算完,把结果压缩成「每拍走不走、走几步」这种极简形式交给中断。
我一般会把插补节拍频率定在 10 kHz 到 20 kHz 之间。低于 10 kHz,高细分下电机低速段会有明显颗粒感;高于 20 kHz,中断里那点固定开销开始吃 CPU,尤其在 72 MHz 主频的片子上,20 kHz 意味着每 50 微秒就要完整执行一遍中断进出、插补判断、GPIO 翻转,留给主循环的时间已经很紧。
2.2 三层职责划分:指令层、插补层、脉冲输出层
整套方案按依赖方向切成三层,层与层之间只用结构体传递数据,不互相调用业务函数:
| 层级 | 职责 | 运行位置 | 典型输出 |
|---|---|---|---|
| 指令层 | 解析目标坐标、限速、加减速参数,维护运动状态机 | 主循环 | 一条待执行的直线段 |
| 插补层 | 把直线段拆成每拍的步进分配,管理速度曲线 | 主循环算 + 中断取用 | 每拍step_x/step_y |
| 脉冲输出层 | 按节拍翻转方向脚和脉冲脚,更新绝对位置 | 定时器中断 | 两路 STEP 脉冲 |
这样分层的好处是调试时可以逐层替换。比如怀疑是插补算错,就把脉冲输出层换成「只累加位置、不真正出脉冲」的空跑模式,让程序跑完一整条线段后打印终点坐标,跟目标坐标一比就知道是算法问题还是硬件问题。这种空跑开关建议一开始就留好,后面验收环节会反复用到。
层与层之间的耦合点只有一个Axis_t数组和一份运动命令结构体,任何一层都不直接读全局变量。
2.3 核心数据结构与工程文件划分
单轴的状态用一个结构体描述,把外设句柄、方向引脚、位置计数、限速参数全收进去,避免后面出现一堆posX、posY之类的平行变量:
/* axis.h —— 单轴描述,双轴就是两个这样的实例 */ typedef struct { TIM_HandleTypeDef *htim; /* 该轴脉冲定时器(PWM 或时基) */ uint32_t channel; /* 定时器通道 */ GPIO_TypeDef *dir_port; /* 方向信号端口 */ uint16_t dir_pin; /* 方向信号引脚 */ int32_t pos; /* 当前绝对位置,单位:脉冲 */ int32_t target; /* 目标绝对位置,单位:脉冲 */ uint32_t max_freq; /* 该轴允许的最高脉冲频率 Hz */ uint8_t dir_level; /* 正方向对应的电平,用于适配不同驱动器 */ } Axis_t; /* 运动状态机,主循环据此决定还能不能接收新指令 */ typedef enum { MOTION_IDLE = 0, MOTION_RUNNING, MOTION_DECEL, MOTION_FAULT } MotionState_t;pos用int32_t而不是uint16_t,是因为很多人在行程稍长的机架上跑几十万脉冲后溢出,位置突然跳变,表现为「走到一半电机反向猛冲」,这类事故在调试阶段很吓人。按 3200 脉冲/圈算,int32_t能覆盖上百万圈,足够用。
工程文件建议这样切:
motor_cfg.h:引脚定义、细分、丝杠导程、软限位,全部是宏,改机型只改这一个文件axis.c/h:单轴初始化、方向设置、位置读写interp.c/h:直线插补器,纯算法,不碰硬件,可以拿到 PC 上单独验证planner.c/h:速度规划,输出当前节拍应该用的定时器周期app_motion.c:状态机、命令接收、错误处理
把interp.c做成不依赖 HAL 的纯 C,是这套结构里最值钱的一条经验。它意味着你可以写个几十行的小测试程序,在电脑上枚举几千条不同斜率的线段,检查插补器输出的脉冲数是否和理论值完全一致,一次把算法层面的 bug 清干净,不用每次都烧板子。
3. 直线插补实现:脉冲怎么在两轴之间分
3.1 DDA 与 Bresenham 的误差和代价对比
两轴走直线的本质是:在总步数N个节拍里,X 轴要出dx个脉冲,Y 轴要出dy个脉冲,且任意时刻两轴走过的步数比例尽量贴近dx:dy。两种经典做法各有取舍。
数字积分法(DDA)每拍给每轴累加一个增量dx/N、dy/N,累计超过 1 就出一步。它的优点是合速度恒定,不管斜率多少,两轴合成的进给速度都一样;缺点是要么用浮点(中断里开销大),要么用定点小数(要选好放大倍数,否则低速时累积误差明显)。
Bresenham用整数加减和一个误差项决定这一拍哪轴出步。它没有除法、没有浮点、误差严格不超过半步,是单片机上的首选。但有个容易被忽略的性质:它保证的是「每拍最多走一步」,长轴每一拍都走,短轴按比例走。也就是说,如果节拍频率固定,长轴的脉冲频率就是节拍频率,而合速度会随斜率变化,45 度时合速度比单轴时高出约 41%。
这个性质带来一个实际问题:画正方形时四条边的合速度一样(因为长轴相同),但画一条接近 45 度的长线和一条接近水平的短线,手感完全不同。对雕刻、点胶这类对进给速度一致性有要求的场景,得在速度规划层补偿,后面 3.3 会说怎么补。
结论:用 Bresenham 定步数分配,用规划层定节拍频率。算法负责「比例对」,规划负责「快慢对」,两件事分开管,代码也好写。
3.2 插补器的纯 C 实现
下面这份实现不依赖任何 HAL,可以直接拿到 PC 上跑单元测试:
/* interp.c —— 纯整数 Bresenham 直线插补器 */ typedef struct { int32_t dx, dy; /* 两轴总步数,已取绝对值 */ int32_t sx, sy; /* 两轴方向:+1 或 -1 */ int32_t err; /* 累积误差项 */ int32_t total; /* 已走节拍数 */ int32_t total_steps; /* 总节拍数 = max(dx, dy) */ } LineInterp_t; /* 用起点终点初始化,坐标单位是脉冲 */ void LineInterp_Init(LineInterp_t *li, int32_t x0, int32_t y0, int32_t x1, int32_t y1) { int32_t dx = x1 - x0; int32_t dy = y1 - y0; li->sx = (dx >= 0) ? 1 : -1; li->sy = (dy >= 0) ? 1 : -1; li->dx = (dx >= 0) ? dx : -dx; li->dy = (dy >= 0) ? dy : -dy; li->err = li->dx - li->dy; /* 经典 Bresenham 初值 */ li->total = 0; li->total_steps = (li->dx > li->dy) ? li->dx : li->dy; } /* 每个节拍调用一次;返回 1 表示还有步要走,0 表示整段结束 */ uint8_t LineInterp_Next(LineInterp_t *li, uint8_t *step_x, uint8_t *step_y) { *step_x = 0; *step_y = 0; if (li->total >= li->total_steps) { return 0; /* 线段走完 */ } int32_t e2 = li->err << 1; /* 左移代替乘 2,省一个乘法 */ if (e2 > -li->dy) { /* X 轴该出一步 */ li->err -= li->dy; *step_x = 1; } if (e2 < li->dx) { /* Y 轴该出一步 */ li->err += li->dx; *step_y = 1; } li->total++; /* 节拍计数前进 */ return 1; }这段代码有三个要点值得说清楚。
第一,total_steps取两轴步数的最大值,意味着长轴每拍都出步,短轴按误差项决定出不出。因此节拍频率就等于长轴的脉冲频率,这一点直接决定了后面定时器周期的计算方式。
第二,e2 > -dy和e2 < dx两个判断是并列的,不是二选一,所以当斜率为 1 时两轴会在同一拍同时出步,这就是「两轴同时运动」在 45 度方向上的天然体现。若写成if/else,45 度会退化成交替出步,合速度直接掉一半,画出来的斜线明显比其它角度慢。
第三,整个函数没有除法、没有浮点,最坏路径也就几十个周期。放在 20 kHz 的中断里完全没有压力。这是选 Bresenham 而不是 DDA 的直接理由。
配套的位置更新放在脉冲输出层做,插补器本身只回答「走不走」,不改pos,职责清晰。这样同一份插补代码也能被圆弧扩展复用。
3.3 速度规划接进插补的时机
定好每拍走多少,接下来定「一拍多长」。最朴素的做法是全段用同一个定时器周期,即恒速运行。低速小线段勉强能用,一旦线段变长、速度变高,起步瞬间就是一次阶跃——电机直接堵转或者丢步,这是新手最常见的翻车点。
合理做法是梯形速度曲线:起步频率f_start、目标频率f_max、加速度a(单位脉冲/秒²),分加速段、匀速段、减速段三部分。实现上不必每拍都重算,按固定拍数分批更新就行:
/* planner.c —— 每 16 拍更新一次定时器周期,近似匀加速 */ #define SPEED_UPDATE_TICKS 16 static void Planner_Update(const LineInterp_t *li, uint32_t *freq) { uint32_t remain = li->total_steps - li->total; /* 剩余步数少于减速段所需步数时,进入减速 */ if (remain < g_planner.decel_steps) { *freq -= g_planner.freq_step; /* 逐步降频 */ if (*freq < g_planner.f_start) { *freq = g_planner.f_start; } } else if (*freq < g_planner.f_max) { *freq += g_planner.freq_step; /* 逐步升频 */ if (*freq > g_planner.f_max) { *freq = g_planner.f_max; } } /* 匀速段什么都不做,保持 f_max */ }freq_step由加速度换算而来:每SPEED_UPDATE_TICKS拍经过的时间是16/f秒,在这么长的时间里速度提升a × 16/f,约等于freq_step。工程上更省事的做法是先算好一张加速表,运行时查表递增,避免每拍做乘除。表长取 256 或 512 项,用f_max/f_start的比例做对数分布,加速段的手感会明显更顺。
回到 3.1 提到的合速度问题:如果要求不管什么斜率合速度都恒定,就在算出f_max之后乘一个系数dx / sqrt(dx² + dy²)(长轴为 X 时),把长轴速度按斜率缩一缩。这个开方在主循环里算一次就够,不进中断。是否要做取决于场景——雕刻直线纹理时感知明显,写字机上基本看不出来,按需加。
最后提醒一点:f_start不要设得太低。常见的步进驱动器在 200 Hz 以下会有明显共振区,起步频率压在共振带里,每次启动都「嗡」一声并且丢几步。我一般把起始频率定在 600 Hz 到 1 kHz 之间,配 16 细分基本能平稳起步。
4. 定时器与脉冲输出:让两路脉冲走在同一条时间线上
4.1 定时器周期的换算与限幅
节拍频率定了之后,要落到定时器的 ARR 上。公式很简单:
f_pulse = f_tim / ((PSC + 1) × (ARR + 1))其中f_tim是定时器挂载总线的时钟,具体数值取决于芯片型号和时钟树配置,常见的是 72 MHz、84 MHz 或 170 MHz 量级,配置前务必在时钟树文件里确认,不要凭印象写。PSC 一般固定不动(取一个能让 ARR 落在合理区间的值),运行时只改 ARR。
/* axis.c —— 运行时只改 ARR,PSC 保持不变 */ static void Axis_SetPulseFreq(TIM_HandleTypeDef *htim, uint32_t f_pulse) { const uint32_t tim_clk = 84000000U; /* 按实际定时器时钟修改 */ uint32_t psc = htim->Instance->PSC; /* 已配置好,读出来即可 */ if (f_pulse < 100U) { f_pulse = 100U; /* 下限保护,防止除零和超长周期 */ } uint32_t arr = tim_clk / ((psc + 1U) * f_pulse); if (arr < 2U) { arr = 2U; /* 上限保护,防止频率超规格 */ } __HAL_TIM_SET_AUTORELOAD(htim, arr - 1U); htim->Instance->EGR = TIM_EGR_UG; /* 立即载入新周期,避免等一个旧周期 */ }arr - 1是因为计数器从 0 计到 ARR 共 ARR+1 个数。EGR写UG位的作用是产生一次更新事件,把预装载寄存器里的新值立刻搬到影子寄存器,否则要等当前周期跑完才生效,在加减速时会落后一两拍,表现为速度曲线末端不够平滑。
arr的下限保护尤其重要。当f_pulse算出来接近定时器时钟的一半时,arr会小到 2 甚至 1,此时计数器几乎没有分辨率,实际输出频率和设定值偏差很大,还可能触发定时器自身的更新事件异常。真正的高频段应该改 PSC 而不是硬压 ARR,但双轴联动时节拍是共享的,改 PSC 会影响另一轴,所以通常的做法是把 PSC 一次性配到「最高目标频率时 ARR 不小于 20」的位置,之后只动 ARR。
4.2 双轴同时运动的四种输出方案
「两轴同时」在硬件上有好几种实现层次,差别很大,选错了后面全是麻烦:
| 方案 | 做法 | 同步性 | 适用场景 |
|---|---|---|---|
| 双定时器独立 | 两轴各用一个定时器出 PWM,软件先后启动 | 差,启动有微秒级偏差 | 两轴各自独立定位,不需要插补 |
| 单定时器双通道 | 一个定时器的 CH1/CH2 输出,共用 ARR | 好,同频严格同步 | 只适合两轴速度相同的场合,如固定 45 度 |
| 单定时器中断 + GPIO | 一个定时器做节拍,中断里翻 IO | 好,两轴在同一中断里出步 | 通用插补,本方案采用 |
| 定时器中断 + PWM 门控 | 中断决定走不走,PWM 硬件出脉冲 | 最好,脉宽稳定 | 高脉冲频率场合,实现复杂 |
通用直线插补必须让两轴在同一拍里按需独立出步,前两种做不到,第四种实现成本高。所以主流做法是第三种:一个定时器负责节拍,中断里同时处理两轴。这样做的好处是节拍天然对齐,两轴的每一步都发生在同一个时间基准上,不存在「X 轴已经走了三步,Y 轴才走两步」的相位漂移。
代价是脉冲宽度不受硬件控制,只能靠中断里的代码执行时间来定。这就引出下一节。
4.3 中断服务函数与脉冲宽度控制
中断里的活必须干得又少又稳。核心流程就三步:取插补结果、输出脉冲、更新位置。
/* app_motion.c —— 定时器更新中断,20 kHz 节拍 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance != MOTION_TIM) { return; /* 只处理节拍定时器 */ } uint8_t sx = 0, sy = 0; if (!LineInterp_Next(&g_line, &sx, &sy)) { Axis_StopAll(); /* 整段走完,关脉冲 */ g_motion.state = MOTION_IDLE; return; } /* 两轴同时置高,保证脉冲前沿对齐 */ if (sx) HAL_GPIO_WritePin(STEP_X_PORT, STEP_X_PIN, GPIO_PIN_SET); if (sy) HAL_GPIO_WritePin(STEP_Y_PORT, STEP_Y_PIN, GPIO_PIN_SET); DWT_DelayUs(3); /* 定宽 3us,驱动器识别用 */ /* 统一拉低,脉宽一致 */ HAL_GPIO_WritePin(STEP_X_PORT, STEP_X_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(STEP_Y_PORT, STEP_Y_PIN, GPIO_PIN_RESET); if (sx) g_axis[0].pos += g_line.sx; if (sy) g_axis[1].pos += g_line.sy; if (++g_tick_cnt >= SPEED_UPDATE_TICKS) { /* 到点更新速度 */ g_tick_cnt = 0; Planner_Update(&g_line, &g_freq); Axis_SetPulseFreq(MOTION_TIM_H, g_freq); } }几个设计点逐个说明。
先置高再拉低,中间不要插别的操作。两轴同时置高、同时拉低的写法,保证了脉冲前沿严格对齐,这对「两轴同时到达」的判定很关键。如果写成「X 置高、延时、X 拉低、Y 置高、延时、Y 拉低」,两轴脉冲前沿差出一个脉宽,虽然只有几微秒,但在高脉冲数下会累积成明显的末端不同步。
脉宽不能用HAL_Delay。那个函数基于 SysTick 毫秒计数,最小分辨率 1 ms,在中断里调用直接卡死整个节拍。用 DWT 周期计数器做微秒级延时,或者干脆写一个已知周期的空循环(__NOP()序列),后者要关中断防止被更高优先级打断。3 微秒这个值来自常见步进驱动器的要求,多数型号要求脉宽不小于 2.5 微秒,留点余量。
脉宽决定了节拍频率的上限。20 kHz 时一个周期 50 微秒,3 微秒脉宽占 6%,可以接受。想跑到 50 kHz,周期只有 20 微秒,脉宽占比升到 15%,中断里再干点别的就会挤爆。这时候就得换成 4.2 表格里的第四种方案,让硬件 PWM 去出脉冲。
位置更新放在脉冲之后。顺序反了不会出错,但调试时如果在中途打断点,位置会比实际脉冲多走一步,查起来很费劲。
还有一个容易忽略的细节:LineInterp_Next返回 0 的那一拍,脉冲已经不该出了,但定时器还在跑。所以Axis_StopAll()里要么关掉定时器中断,要么把 ARR 设成一个极大值让它空转,前者更干净。注意关中断要在中断函数内部做,不能直接HAL_TIM_Base_Stop_IT,那会在中断上下文里操作寄存器,虽然多数情况下能用,但更稳妥的方式是置一个标志位,让主循环去关。
5. 常见问题排查:双轴联动最容易翻车的五处
5.1 轨迹类异常:走歪、末端不齐
现象一:斜线走到末端,一轴先停,另一轴还在补步,收尾处出现一个小折角。
原因通常不在插补算法,而在两轴的总步数没有对齐。Bresenham 保证的是「在total_steps拍内两轴分别出够dx和dy步」,但如果初始化时用的是「起点到终点的增量」而位置结构体里存的是「上一段的终点」,中间有一次手动回零或软限位修正,两边就对不上了。表现出来就是末端多走或少走几个脉冲。
解决方式是在每段运动开始前做一次一致性检查:把pos与target的差值算出来,和LineInterp_Init里用的dx/dy比对,不一致就报MOTION_FAULT并拒绝执行。这个检查只有几行代码,但能挡住后续所有因为位置漂移导致的神奇现象。
现象二:正方形四个角是圆的,或者闭合时回不到原点。
这是加减速段没算准的典型表现。减速距离decel_steps如果按f_max算,但实际运行中因为限幅只跑到了 0.8 倍的f_max,减速段就会提前结束,最后几拍以匀速冲过终点,拐角处来不及减速,机械上表现为过冲。反过来,减速距离算长了,会在到达终点前就降到最低频,最后一段慢得像爬。
稳妥的做法是减速判断不要用「固定步数」,改成「按当前频率实时算剩余所需步数」:v²/(2a)换算成脉冲数,每拍更新一次。多一次除法,但拐角精度立刻正常。
5.2 脉冲与节拍类异常:丢步、抖动、堵转
现象三:低速正常,一提速就丢步,回零位置每次都差一点。
先别怀疑算法,先量脉宽。用示波器抓 STEP 引脚,如果脉宽只有几百纳秒,驱动器根本来不及响应,尤其是光耦隔离输入的驱动器,上升沿本身就慢。原因多半是中断里置高之后紧接着就拉低,中间没有确定的延时。按 4.3 的写法补上 3 微秒定宽,问题基本消失。
如果脉宽正常还丢步,就看加速度。步进电机的矩频特性决定了高速段扭矩衰减很快,加速度设得比电机能力大,每次启动都丢几步,且丢的步数随机,回零位置自然每次不同。把freq_step减半再试,能稳定跑完就说明是加速度的问题,不是代码的问题。
现象四:电机静止时能听到规律的「嗒嗒」声,手摸有轻微抖动。
这是节拍中断里在空转时仍然翻转 IO 导致的。有些实现在LineInterp_Next返回 0 之后没有真正停止定时器,中断还在以 20 kHz 触发,虽然不置高脉冲脚,但方向脚或使能脚被反复写,或者中断本身干扰了驱动器。解决很简单:整段结束后关掉定时器中断,等下一段开始时再开。
还有一种可能是细分设置和节拍频率不匹配。细分 32、节拍 20 kHz 时,电机每转需要 6400 个脉冲,理论上限转速只有 3 转/秒左右,再高就跟不上。这种「跑不快」不是 bug,是参数没配平,把细分降到 16 或者提高节拍频率即可。
现象五:两轴单独跑都正常,一联动就随机性死机或复位。
优先查中断优先级。如果节拍定时器中断和串口接收、SysTick 的优先级配置不当,嵌套过深会耗尽栈空间。20 kHz 的中断里再被更高优先级打断几次,栈很快就会见底,表现为随机复位。把节拍定时器设成最高优先级(数值最小),其它外设中断一律降级,并且确保中断里不调用任何可能阻塞的 HAL 函数。我吃过这个亏,一开始以为是电源干扰,查了两天发现是栈溢出。
6. 验收方法:怎么证明两轴真的「同时」到了
方案跑通不等于能交付。答辩或者验收时,评委最爱问的一句话是「你怎么知道两轴是同时到的」,光说「我看画出来是直的」压不住场。下面这套验收流程我在几个项目上都走过,可以直接照搬。
第一层,逻辑空跑。把脉冲输出关掉,只让插补器跑,跑完打印两轴的累计步数,和理论值比对。枚举 200 条不同斜率的线段(含水平、垂直、45 度、接近 0 度和接近 90 度的边界情况),要求误差严格为 0。这一层能把算法问题全部挡在硬件之外。
第二层,示波器测同步性。抓两轴 STEP 信号的最后一拍,测量两个下降沿的时间差。判据是差值不超过一个节拍周期。20 kHz 节拍下就是 50 微秒以内。这个数字是可以写进报告的硬指标。
第三层,几何闭合测试。装好机械,跑一个 100 mm × 100 mm 的正方形,连续 20 圈,回到起点后用千分表测偏差。判据是单圈闭合误差小于 0.05 mm,20 圈后不累积。累积说明有系统性丢步,不累积但有随机偏差说明是加减速过冲。
| 验收项 | 方法 | 判据 |
|---|---|---|
| 插补正确性 | 逻辑空跑 200 条线段 | 步数误差为 0 |
| 两轴同步性 | 示波器测末端脉冲沿差值 | 小于 1 个节拍周期 |
| 轨迹精度 | 正方形闭合 20 圈 | 单圈误差 < 0.05 mm,无累积 |
| 速度一致性 | 同一段长分别用不同斜率跑 | 合速度偏差 < 5% |
| 长时间稳定性 | 连续运行 2 小时 | 无丢步、无复位、温升正常 |
答辩时的表达技巧。别一上来讲代码,先讲那张对比表:为什么双定时器独立启动不行,为什么单定时器双通道只适合 45 度,把「同步性」这个抽象指标落到「脉冲前沿时间差」这个可测量的量上。评委听到可测量的判据,追问的欲望会明显下降。然后拿出示波器截图和闭合测试的数据,两页 PPT 就够。
从那以后,我每次做联动项目都强制走一遍「空跑 → 示波器 → 闭合」这三步,再急的活也不跳。跳过空跑直接上机械,后面排查问题时花的每一分钟,都是在给前面省下的十分钟还债。希望帮到你。
本文还有配套的精品资源,点击获取