简介:一份面向单片机初学者与步进电机应用开发者的源码示例,演示如何用51单片机对三个步进电机进行同步脉冲控制,实现三轴联动。资源聚焦C语言程序实现,涵盖I/O口配置、脉冲序列生成、定时器与中断同步机制等关键环节,适合机器人、3D打印机、自动化定位等场景的入门参考。压缩包内仅1个.c文件,体积约945B,代码量精简,便于直接阅读与移植;文件虽少,但包含初始化、脉冲生成、电机控制逻辑与中断处理等核心模块。已有2811人学习下载。通过这份代码,可理解步进电机的步距角与脉冲频率换算,掌握多轴同步控制中主定时器调度思路,以及死区时间、延时函数等精度优化细节。对于正在学习51单片机或准备搭建简易三轴运动控制系统的开发者,是一份实用的小型范例。
1. 51单片机为什么能扛起三轴联动:先搞懂你要解决的问题
“51单片机控制步进电机三轴联动”这句话,放在熟悉运动控制的人眼里,一下就变成了“用一颗低速8位单片机让三个电机协同走一条空间直线”。这套方案常见于低成本3D打印机、桌面级雕刻机、演示机械臂和课程设计,它能做,靠的并不是把三个电机各接一个引脚然后分别驱动,而是在同一个定时器中断里按同一份插补参数分配脉冲。我的结论先说在前面:先别急着追速度,先把“每个中断周期内三轴的步进次序”搞对,后面调速度、开细分才有意义。
如果三个轴各自按自己的定时器跑,走出来的往往是一条折线或曲线;如果先用最死板的方式把X轴走完再走Y轴,就谈不上“联动”。所以这篇文章会按一条实际做项目的顺序来讲:先定硬件接线,再讲DDA直线插补,再给可直接改的定时器中断代码,然后把最容易翻车的四个坑单列出来。适合刚入门、想亲手让三轴同时走出斜线的读者。
2. 硬件选型和接线:先定IO、驱动器和共阴接法
三轴联动看着是软件问题,其实一大半坑埋在接线阶段。我在给某课程设计搭这套系统时,第一反应是找三个现成的步进电机驱动板,结果发现每个驱动器的接口定义不一样,接错一个引脚就可能冒烟。所以这一章先把选型和接线定死,后面写代码时就不用再改硬件。
2.1 步进电机选型:两相四线是成本、扭矩和接线难度的交集
步进电机选什么,直接影响驱动器成本和接线复杂度。三轴联动最常见的是两相四线步进电机,步距角1.8度,每转需要200个整步脉冲。它的绕组简单,两根线一组,驱动器只需要输出两相H桥信号,接口清晰。两相电机扭矩虽然不如五相电机平稳,但对小型三轴平台来说已经够用,最关键的是驱动模块便宜、资料多。
五相步进电机扭矩纹波小、运行更平稳,但驱动器价格高,接口也复杂,一般只有追求精度的工业设备才用。对初学者和中小型桌面设备,我一般建议先选两相四线,等把联动逻辑跑通再考虑升级。
电机尺寸和电流也要看应用场景。小型平台可以用便宜的小尺寸电机,额定电流大约0.5A到1A;如果Z轴要带更大负载,就选扭矩更大的规格,但这时对驱动器电流和电源容量要求也更高。实际选型时先看驱动器的最大电流能不能覆盖电机额定电流,最好留出20%余量。否则电机扭矩不足,三轴联动时一旦加减速就会丢步。
驱动器的细分是我反复强调的参数。细分可以让每转脉冲数提高,例如8细分下每转就是1600个脉冲,电机运行更平滑,低转速共振也小。但细分不是越大越好,51单片机的中断频率有上限,目标速度不变时细分变大意味着每秒要发出的脉冲数变多,这很容易超出单片机能承受的范围。综合下来,8细分是性价比很高的点。
2.2 驱动器接口:脉冲、方向、使能是三个必须接明白的信号
市面上常见的步进驱动器模块,输入侧大多是“脉冲、方向、使能”三组信号,每组有两个接口,通常标成“+”“-”两个端子。很多初学者以为只要把PUL和DIR接两根线就行,实际上使能这组信号也很关键。如果使能信号不对,电机可能处于失能状态,手都能掰动,更别提三轴联动。
我常用的是共阴接法,也就是把所有信号的正端“+”接到一个公共电源正极,负端“-”接到单片机引脚。对51单片机来说,P1和P2口输出高电平能力有限,而共阴接法正好让单片机引脚负责下拉,比较稳妥。驱动器内部通常是光耦隔离,信号从一个引脚输入,到另一个引脚输出,光电耦的导通电流由外部上拉或内部电阻决定。因此接法要以购买模块的手册为准,宁可不厌其烦确认一次,也不要凭感觉接。
有一点必须注意:很多驱动模块是“下降沿触发”步进,也就是PUL-端从高电平跳变到低电平时才走一步。所以在初始化时,我会先把所有STEP引脚设为高电平,需要走一步时把它拉低,再拉回高电平。这样保证复位和程序启动瞬间不会产生多余脉冲。
使能信号通常是低有效,也就是ENA-引脚下拉时驱动器输出打开,电机锁止;但不同厂家模块逻辑不同。有的模块为了方便,把ENA默认悬空就认为是失能,所以上电后电机完全没力。对于这类模块,我一般直接不接ENA,让驱动器通过自身跳线保持使能,或者按手册明确接好后再上电。
2.3 最小接线表与IO分配:三步把九个信号接完
下面给出一份可以直接照搬的IO分配,使用单片机的P1口和P2口,正好满足三个轴共九个控制信号,还留出了外设扩展空间。
| 功能 | 单片机引脚 | 驱动器端子 | 默认电平 |
|---|---|---|---|
| X轴步进 | P1.0 | PUL- | 高 |
| X轴方向 | P1.1 | DIR- | 自由设置 |
| X轴使能 | P1.2 | ENA- | 低 |
| Y轴步进 | P1.3 | PUL- | 高 |
| Y轴方向 | P1.4 | DIR- | 自由设置 |
| Y轴使能 | P1.5 | ENA- | 低 |
| Z轴步进 | P1.6 | PUL- | 高 |
| Z轴方向 | P2.0 | DIR- | 自由设置 |
| Z轴使能 | P2.1 | ENA- | 低 |
接线步骤我习惯按这个顺序做:
- 先把驱动器电源接好,通常用12V或24V直流电源,容量按三个电机的总电流再加余量来选。千万不要试图用单片机开发板的5V去驱动电机,电流不够,电机一转就会把电压拉崩。
- 把所有驱动器的“+端子”并接到同一个公共正极,公共负极接回单片机GND,同时把驱动器电源地也接到这个GND上,形成统一参考地。
- 把三个轴的STEP引脚接到上表的PUL-接口,方向引脚接到DIR-接口,使能引脚接到ENA-接口。
- 上电前用万用表确认驱动器供电电压正常,再单独给单片机供电。先只给一个轴上电,手动发一个脉冲测试电机是否转动,再扩展到三轴。
这里有个容易忽略的点:如果要接限位开关,最好把限位信号也规划好。三轴联动时,限位开关经常放在X、Y、Z的负向,用来回原点。限位开关一般是常开或常闭触点,接入外部中断引脚,例如P3.2、P3.3、P3.4。每次启动前先回原点,能够让后续坐标计算准确定位,否则插补步数再多也只是相对当前位置动。
硬件部分最后再强调一次接地。共阴接法下,如果单片机GND与驱动器GND不共地,脉冲信号没有统一参考点,驱动器的光耦输入就可能因为电压差而无法导通或抖动。我遇到过用笔记本电脑USB供电时,驱动器偶尔丢步,后来把驱动器和单片机共地,问题才消失。这个现象可以当作排查手册里的一条备用经验。
3. 三轴联动的核心:脉冲输出与DDA插补策略
硬件接好以后,很多人的第一反应是写三个函数,比如“转X一圈、转Y一圈”。但真正的三轴联动要求三个电机同时运动,并且每个轴的脉冲数按比例分配,这样刀具或末端才能走一条空间斜线。这部分的算法基础,是运动控制里的DDA数字微分分析器,也叫三轴直线插补。
3.1 定时器中断是“节拍器”,不是每个轴一个定时器
51单片机基本上只有两个定时器,想给三个轴分别分配定时器本来就不现实。即使有三个定时器,也不能保证它们之间的相位严格同步,三个轴的步进时刻一旦错开,就会被机械结构放大成曲线。所以我一般只用定时器0,把它当成全系统的“节拍器”,每隔固定时间进入一次中断,在中断里统一决定:这一拍三个轴分别要不要走,要走哪一个轴。
把这个节拍器定在多少频率,直接影响联动速度和脉冲精度。以11.0592MHz晶振、定时器0方式1为例,机器周期是12/11.0592MHz,约1.085微秒。如果我希望中断频率为10kHz,那定时器初值就是65636减去10kHz对应计数。写成公式就是:
重装值 = 65536 - 晶振频率 / 12 / 中断频率
晶振频率用Hz,中断频率也用Hz。10kHz中断下,重装值大约为65536-921=64615。实际使用中,定时器中断一旦进入,执行完中断代码后硬件自动把初值再装载进TH0和TL0,保证下一拍的时间偏差不超过指令数量带来的几微秒。这个误差对步进电机来说可以接受。
中断频率也不是越高越好。频率越高,两次中断之间的时间越短,单片机在中断里要做的事就越多。如果中断代码超过了周期时间,下一次中断到来时会来不及响应,丢步就发生了。我通常会把中断代码执行时间控制在周期的70%以内,然后用示波器测量实际脉冲间隔做验证。
3.2 DDA直线插补:三轴位置的“公交调度”
DDA的思路其实很直观。假设我要从当前位置走到目标位置,三轴分别需要走Nx、Ny、Nz步。找出其中最大的数记为Nmax,然后用Nmax作为“总调度次数”。每次进入定时器中断,就把目标步数累加到对应累加器里,当累加器超过Nmax,就认为这个轴在这个拍里应该走一步。
我画过一张调度表:X需要走3步,Y需要走2步,Z需要走1步,Nmax为3。然后按比例分配,第一拍可能X走,第二拍X和Y同时走,第三拍Y走。三轴并不总是在每一拍都动,而是按各自比例均匀贡献,这样从宏观来看,三轴是同时到达终点的。
下面是最小版DDA伪代码,我用C语言直接写进定时器中断:
// 三轴DDA插补,每进入一次定时器中断调用一次 void dda_tick(void) { acc_x += target_x; // target_x是X轴目标步数 if (acc_x >= target_max) { // target_max是最大步数 X_STEP = 0; // 拉低输出,产生下降沿 _nop_(); _nop_(); // 保证脉冲宽度满足驱动器要求 X_STEP = 1; // 恢复高电平 acc_x -= target_max; current_x++; } // Y轴、Z轴的处理逻辑完全相同 }参数含义:target_x越大,累加器增长越快,X轴被分配到的步进机会越多;target_max是三个轴目标步数中的最大值,它决定了整段直线总共要经历多少个中断周期。acc_x是累加器,初始值一般设为target_max/2,这样可以让每个轴第一条脉冲的出现时间更居中,避免起点误差。
这里要特别注意,X_STEP = 0后必须停几个机器周期再恢复为1,否则脉冲宽度太窄,驱动器光耦来不及反应,就会丢掉这个脉冲。具体停几个周期,要看驱动器手册上写的最小脉冲宽度,通常大于1微秒即可。在定时器中断里我很少用delay函数,因为delay会浪费中断时间,而是插入几个_nop_()空指令来凑脉宽。
3.3 加减速:直接调速时最容易破坏插补的一条铁律
步进电机有启动频率,超过启动频率直接给高频脉冲就会失步。三轴联动要想不打滑,通常要加梯形加减速:起步时速度低,加速到目标速度,然后减速到零。问题在于,加减速本质上是改变脉冲间隔。
很多人会本能地想让定时器周期动态变化,然后发现直线插补变弯了。原因在于,加减速过程中若只修改中断频率,却不小心改动了DDA的target_x、target_y和target_z比例,就等于把直线的夹角改了。我有一条铁律:调速只能改变定时器初值,也就是改变每拍之间的时间间隔,不能改变累加器的比例和Nmax。只要比例不变,即使某一拍间隔变长或变短,路径仍然落在同一条直线上,只是走完的时间变了。
一个常见的加速实现,是在中断里给TH0和TL0重新赋值:
// 假设加速阶段还需要100拍,reload_step表示每次递减量 if (accelerate_steps_remaining > 0) { reload_value -= reload_step; if (reload_value < RELOAD_MIN) reload_value = RELOAD_MIN; TH0 = reload_value >> 8; TL0 = reload_value & 0xff; accelerate_steps_remaining--; }reload_value越小,定时器计数到溢出的时间越短,中断频率越高,速度越快。RELOAD_MIN要按目标速度上限算出来,避免设置成不可接受的极端值。这个机制在单向直线插补中好用,只要做到“修改节奏,不动比例”,三轴联动路径就不会走形。
要注意的是,加减速过程需要提前规划“剩余加速步数”和“剩余减速步数”。如果路径总长很短,加速还没到目标速度就要开始减速,相当于只经历了一个三角形速度曲线。这时候需要重新计算最顶点速度,不要让电机硬冲。
4. 代码实现:从定时器中断到串口发送三轴联动命令
理论讲了,这一章直接给代码。以C语言为例,编译器用常见的C51环境,寄存器定义以reg51.h为准。以下代码是可运行的最小工程骨架,重点展示三轴DDA插补、定时器中断和串口命令触发三轴联动。
4.1 全局变量和位引脚定义:先把硬件映射到代码
代码开头把上一章分配好的IO映射到位变量,同时定义插补需要的全局变量。这些变量的作用,我会在代码后面逐条说明。
#include <reg51.h> typedef unsigned char u8; typedef unsigned int u16; sbit X_STEP = P1^0; sbit X_DIR = P1^1; sbit X_ENA = P1^2; sbit Y_STEP = P1^3; sbit Y_DIR = P1^4; sbit Y_ENA = P1^5; sbit Z_STEP = P1^6; sbit Z_DIR = P2^0; sbit Z_ENA = P2^1; #define RELOAD 64614 // 10kHz中断初值,实测可小幅修正 volatile u16 target_x, target_y, target_z; // 目标步数 volatile u16 target_max; // 最大目标步数 volatile u16 acc_x, acc_y, acc_z; // 累加器 volatile u16 current_x, current_y, current_z; // 已走步数 volatile u8 run_flag; // 联动运行标志 void timer0_init(void); void uart_init(void); void send_string(char *s);RELOWD这个值每块板子因为晶振误差会有几十微秒偏差,我在正式跑之前会用示波器校准。target_x那一组变量必须声明为volatile,因为它们在主循环和定时器中断里会被同时访问,不写volatile可能会被编译器优化成寄存器副本,导致中断读到的数据不一致。
sbit定义的是绝对位地址,不同开发板的引脚对应关系必须和上一章的硬件接线表一致。如果自己改过接线,只需要改这几行,剩下的逻辑不用动。X_ENA、Y_ENA、Z_ENA三个引脚在初始化时默认拉低,让电机上电保持锁止。
4.2 定时器中断和DDA插补实现:每一拍只做一个比例决策
下面这段是核心中断服务程序。它做的事有两个:重新装载定时器初值,让中断周期稳定在10kHz;然后按照DDA比例,分别为三个轴决定是否输出一个步进脉冲。
void timer0_isr(void) interrupt 1 using 1 { TH0 = RELOAD >> 8; TL0 = RELOAD & 0xff; if (!run_flag) return; // X轴插补 acc_x += target_x; if (acc_x >= target_max) { X_STEP = 0; _nop_(); _nop_(); _nop_(); // 约3us脉宽 X_STEP = 1; acc_x -= target_max; current_x++; } // Y轴插补 acc_y += target_y; if (acc_y >= target_max) { Y_STEP = 0; _nop_(); _nop_(); _nop_(); Y_STEP = 1; acc_y -= target_max; current_y++; } // Z轴插补 acc_z += target_z; if (acc_z >= target_max) { Z_STEP = 0; _nop_(); _nop_(); _nop_(); Z_STEP = 1; acc_z -= target_max; current_z++; } }acc_x += target_x这个加法可能会造成累加器溢出,但不需要担心,无符号整数的溢出会自然回绕,只要最终减去target_max,结果仍然正确。target_max在设计上应保持大于等于每个轴的target值,这样每次都只会把一个轴对应的累加器清零或减少,不会出现负值。
脉冲宽度这里只插了三个_nop_(),在11.0592MHz下大约3微秒。如果你的驱动器要求更宽脉冲,可以多插几个空操作。要注意的是,这个脉宽和中断周期是两回事,脉宽太宽会导致一个中断周期内来不及处理其他轴,所以尽量按驱动器允许的下限取。中断代码本身执行时间要控制在中断周期的70%以内,也就是70微秒内,上面的逻辑满足这个条件。
run_flag是一个运行标志。当它为零时,中断只做定时器重装,不输出任何脉冲。这样可以在主循环里准备好所有参数后再启动联动,避免脉冲在参数没准备好时乱发。
4.3 启动三轴联动:主循环和串口的配合
有了DDA中断后,主循环只需要处理命令接收和参数计算。我常用串口下发一行文本命令,格式为M100,200,300,分别代表X、Y、Z三个轴的目标步数,步数前的正负由方向位决定。这一节给出最简实现。
volatile u8 cmd_buf[16]; volatile u8 cmd_len = 0; volatile u8 rx_finish = 0; void uart_init(void) { SCON = 0x50; // 串口方式1 TMOD &= 0x0F; TMOD |= 0x20; // 定时器1方式2 TH1 = 0xFD; // 9600bps @ 11.0592MHz TL1 = 0xFD; TR1 = 1; ES = 1; } void uart_isr(void) interrupt 4 { if (RI) { RI = 0; if (cmd_len < 15) { cmd_buf[cmd_len++] = SBUF; if (cmd_buf[cmd_len-1] == '\n') rx_finish = 1; } } } void move_all(u16 nx, u16 ny, u16 nz) { target_max = nx; if (ny > target_max) target_max = ny; if (nz > target_max) target_max = nz; if (target_max == 0) return; target_x = nx; target_y = ny; target_z = nz; // 把累加器初始化到一半,让首条脉冲更居中 acc_x = acc_y = acc_z = target_max >> 1; current_x = current_y = current_z = 0; run_flag = 1; }move_all函数中,target_max = target_max >> 1这句不是必须的,但建议保留。DDA误差居中后,直线两端误差更对称,起步时三个轴不会挤在同一拍发出第一条脉冲,对减小机械冲击有实际意义。
主循环里接收命令后要做的是解析、设置方向、再调用move_all。方向设置我单独写一段,因为方向引脚必须在脉冲产生之前稳定,否则电机第一个脉冲可能往反方向走。
void set_direction(u8 dir_x, u8 dir_y, u8 dir_z) { X_DIR = dir_x ? 1 : 0; Y_DIR = dir_y ? 1 : 0; Z_DIR = dir_z ? 1 : 0; // 方向稳定时间 for (u16 i = 0; i < 200; i++) ; }串口中断只能负责接收字符,不要在其中做解析和方向设置,否则一旦数据断流就会阻塞中断。我在主循环里检查rx_finish,再调用解析函数。如果命令内容不合法,就直接丢弃,避免误动作。
void main(void) { timer0_init(); uart_init(); send_string("ready\r\n"); while (1) { if (rx_finish) { rx_finish = 0; // 简单解析:M100,200,300 parse_command(); // 这里用固定方向示例,正数走正向,负数走反向 set_direction(cmd_x >= 0, cmd_y >= 0, cmd_z >= 0); move_all((u16)(cmd_x < 0 ? -cmd_x : cmd_x), (u16)(cmd_y < 0 ? -cmd_y : cmd_y), (u16)(cmd_z < 0 ? -cmd_z : cmd_z)); } if (run_flag && current_x >= target_x && current_y >= target_y && current_z >= target_z) { run_flag = 0; send_string("done\r\n"); } } }parse_command的完整数值解析不展开,实际工程里可以用库函数或者自己逐字符扫描并累加成整数。需要注意,move_all中的target_x如果是0,那么DDA里它的累加器不会增加,判断完成时current_x也始终是0,所以不会误报。真正要小心的是target_max不能为0,否则中断里acc_x恒大于等于0,会一直触发步进。
这套代码完成后的实际效果,是串口收到命令后,三个电机按比例同时转动,最后通过串口回发“done”。我在调试时一般会把这个“done”当作一次闭环信号,结合后续的机械位置去判断是否丢步。
5. 三轴联动避坑指南:四个常见故障、原因与解决
三轴联动调试过程容易让人怀疑人生,很多故障看起来是硬件玄学,其实都能从代码和接线里找到原因。这一章写四个我自己踩过、也看别人反复踩的坑。
5.1 现象:单独走轴正常,三轴联动就丢步
三轴分别点动都正常,一启用三轴联动,某个轴偶尔少走几十步。肉眼检查代码,逻辑似乎没有错,但次数多了位置误差累积,最后模型会歪掉。
原因大概率不是电机或驱动器坏了,而是定时器中断代码总执行时间贴近甚至超过了中断周期。三轴联动时,每个中断周期要处理三个轴的分支判断和脉冲输出,单轴运动时中断很快跑完,三轴时可能来不及在下一轮定时中断前退出中断,导致中断被无意义地延迟。另一个常见原因,是STEP引脚默认电平不对,启动瞬间掉了一个脉冲。
解决方法是把中断频率从10kHz降到5kHz甚至2kHz,再测试丢步是否消失。这很反直觉,但效果立竿见影。同时检查STEP引脚在初始化时为高电平,仅在需要步进时拉低。可以用示波器测某轴STEP引脚,理论上是等间隔脉冲,如果间隔抖动超过5%,就是中断过载,优先优化中断。
5.2 现象:两轴同时动就啸叫、振动,位置还偏
三轴中任意两个轴同时低速转动,驱动器或电机就发出明显啸叫,整机振动变大,运动结束位置比预想的偏。
原因一般是启动速度太高,超过了步进电机该细分下的启动极限。步进电机转子质量决定它没法瞬间跟随极高频率脉冲,超频就会丢步。另一个原因是驱动器电流设置过高,电机发热严重,轴在高速时反而扭矩下降。
解决方法是先把速度上限降一半,再观察啸叫。然后打开梯形加减速,让启动速度控制在目标速度的40%左右。驱动器电流调节旋钮按电机额定电流的80%来调,不要拧到最大。如果电机低速共振厉害,适当提高细分,例如从4细分升到8细分,也能减少啸叫。
5.3 现象:走的直线变成“先X后Y”的折线
目标是从点(0,0)走到点(100,50),观察机械轨迹时,发现X轴先快速走了几十步,Y轴才开始动,整体运动像一条折线而不是斜线。
这个现象说明DDA插补没有真正生效,更像是每个轴在独立运行。检查代码后,常见原因是启动时序错误:在主循环里先启动了run_flag,但target_x、target_y还没有赋值完成,中断里读到的是半旧半新的数据。也可能是DDA累加器初始化成了0,导致第一个中断周期里只有大比例的轴被选中。
解决方法是严格遵循“先设置参数,再置位run_flag”的顺序。建议在move_all函数末尾再打开run_flag,并在赋值语句之间插入一条内存屏障思想上的防优化操作。DDA累加器初始化为target_max/2,也会让起点附近三轴贡献更均衡。
5.4 现象:一上电电机锁死,但一动就咔咔响,或Z轴明显顶不住
电机上电瞬间“咔”一声后锁住,但用手轻轻转轴能感觉到阻力很小,运动过程中Z轴滑块甚至会滑落。这看上去像电机没力,实际是使能信号没接对。
原因是ENA引脚默认状态和驱动器不一致,或者驱动器本身的使能逻辑是低有效,但单片机复位瞬间引脚是高电平,等于让驱动器在失能状态下等待命令。另一方面,方向信号在第一次脉冲前没有稳定,电机向反方向袭动。
解决方法是先确认驱动器手册上的使能有效电平。如果是低有效,就让ENA引脚在代码里设置为0,并在main函数最开始就执行。如果不需要软使能,可以干脆不接ENA,让驱动器通过跳线保持常使能。方向稳定时间需要在脉冲前留出至少几毫秒,我用一个延时循环处理,效果没有问题。
6. 进阶:用定时器1测量三轴联动同步性,验证插补是否丢步
三轴联动最怕的并不是转得慢,而是“看起来在动,实际路径偏离”。所以我的进阶习惯是,不上电机,先把三轴的单片机输出引脚分别接三个LED,用眼睛确认每个轴的脉冲在不同节拍里按比例亮灭。这种干跑测试能最快暴露DDA逻辑错误。
具体做法是把定时器1配置成1毫秒节拍,在主循环里累计毫秒数,这样就能得到一个软件时钟。在中断里可以记录某个轴最后一次步进的时刻,再在主循环里把各轴的理论步数和时间数据通过串口发出来,做同步性验证。定时器1本来是用在串口波特率上的,所以为了测试,我先临时把波特率生成改成定时器1的另一个分频方式,或者直接换用标准9600波特率配合定时器1方式2,留下一点余量。
下面是验证思路的简化代码,只保留关键逻辑:
volatile u16 ms_count = 0; void timer1_isr(void) interrupt 3 { ms_count++; } u16 now_ms(void) { return ms_count; }在move_all开始时记录start_ms,在中断里当current_x达到target_x时记录finish_x_ms。理论上X轴完成时间应当和目标步数除以平均步进频率一致。如果三个轴完成时间相差超过几个毫秒,说明中途有丢步或中断被阻塞。
调试至少三次不同比例的目标步数,比如(300,200,100)、(400,300,200)、(600,600,100)。每一次都记录三个轴完成时的时间戳,只要比例对应,三条轴的完成时间应当非常接近。如果某次Z轴明明目标步数最少,却最晚完成,那基本可以判定Z轴丢步或者中断处理优先级出了问题。
我以前调一台小型三轴平台时,就在这一步抓到了一个被中断嵌套拖慢的Bug。当时只在示波器上看到脉冲少,却不知道少在哪个轴。加入时间验证后才发现,Z轴因为中断里多了一步手写的软件延时,在高速运动时被反复挤掉。后来把这一步延时挪到主循环处理,Z轴速度立刻恢复正常。
从那以后,我养成了一个习惯:任何三轴联动的代码,先做干跑验证,再接电机。这个习惯帮我省了很多烧驱动器的钱,也让我敢在调试时大胆调速度。希望帮到你。
本文还有配套的精品资源,点击获取