14. 步进电机直线插补实现¶
在前面的章节中,我们已经详细介绍并讲解了各种常见的步进电机的控制方法,从最基础的IO口模拟脉冲控制步进电机旋转, 到步进电机的梯形、S型加减速以及PID速度环位置环控制等,这些都是控制单个步进电机的方法,在众多的步进电机应用中, 更多的是双轴甚至多轴的联动控制。如何控制步进电机进行双轴联合运动?这就是我们接下来要讲解内容:步进电机的双轴插补运动。
14.1. 插补运动简介¶
14.1.1. 概念¶
插补这个概念最初源自于数值分析数学中的插值,它是一类在离散的已知数据点范围内构造新数据点的方法。 现在这类方法被广泛应用在数控系统和各种相关行业中,所以接下来关于插补的介绍讲解将会直接引用数控系统对其的定义, 包括可能出现的一些数控系统的名词。
数控机床的加工过程中,在理论上刀具的运动轨迹应该十分精准的沿着被加工工件的轮廓,同时满足对加工对象的精度要求。 但是真正加工的工件轮廓可能是各种形状,有一些简单的直线段和圆弧,还有一些复杂曲线。直接生成复杂曲线的运动轨迹会耗费大量的计算资源, 因此在实际应用中通常使用简单的线型去拟合复杂曲线,同时采用一系列微小的直线段去逼近直线和圆弧线型,以满足加工精度的要求。
在数控机床的加工程序中,一般仅提供描述线性所必须的参数,比如直线段只提供起点和终点在加工系统中的坐标,圆弧则会提供圆心、起点和终点的坐标, 以及圆弧的方向。一般数控机床的刀具运动轨迹是由X、Y两个方向的运动合成的,本身并不能非常严格的按照理论曲线运动,只知道这么一些线段参数无法精准的完成加工任务, 需要一种方法能把已知点中间所有微小直线段的坐标点全都计算出来,从而形成符合精度要求的刀具运动轨迹,这种计算方法就叫插补(interpolation),也称为“数据点的密化”。
插补算法法所要解决的就是根据加工速度的要求,在给定的数据点坐标之间,连续计算出若干中间点的坐标值。 而这些中间点的坐标值以一定的精度逼近理论的轨迹。由于计算每个中间点所消耗的时间直接影响数控系统的控制速度,坐标值的计算精度又影响数控系统的控制精度, 所以插补算法是整个数控系统的控制核心。
常见插补方法简介 ————————–e:git_work_spacereleaseebf_motor_ra_tutorialdocimprove_partstep_motor_linear_interpolation.rst e:git_work_spacereleaseebf_motor_ra_tutorialdocimprove_partstep_motor_linear_interpolation.rst 严格来说,插补应该是一类计算方法,而不只是某一种。目前,根据不同的条件给出的插补方法实现有很多种,在这里我们简单的介绍几种常用的插补方法。 根据输出信号的方式不同,插补算法大致可以分为脉冲增量插补和数字增量插补两大类。
脉冲增量插补。也称作基准脉冲插补,这类插补算法的特点是每次插补运算只产生一个行程增量,用一个个脉冲的方式输出到电机以驱动机床刀具或工作台运动。 每发出一个脉冲,刀具或工作台向X或Y轴方向移动一个脉冲当量。脉冲增量插补的实现方法相对比较简单,通常只需加法、乘法和移位即可完成插补。 这类算法一般适合中等精度和中等速度的数控系统中,插补误差不超过一个脉冲当量,不过输出脉冲的速率主要受插补计算的时间限制。
数字采样插补。这类插补算法的特点是把插补运算分成了粗插补和精插补两个部分。第一步先粗插补,把给定曲线分割成若干等长的首尾相接的微小直线段, 直线段的长度与插补周期有关,求得每个微小直线段起点终点的增量坐标。粗插补生成的直线段对于系统精度来说还是比较大,所以还需要第二部分, 第二部分是精插补,这一部分实际上是把粗插补部分生成的直线段再做插补,相当于对直线使用脉冲增量插补。
上述两类插补算法中,数字采样插补输出的信号不是一个脉冲当量,而是与各坐标轴位置增量相对应的几个数字量,并且需要数控系统的反馈回路跟踪轨迹误差, 然后根据误差修正下一次计算结果,所以这类算法基本上都用在以伺服电机为驱动装置的闭环数控系统。脉冲增量插补算法就非常适合以步进电机为驱动装置的开环数控系统, 脉冲增量插补在计算过程中不断向X、Y各个坐标轴发出进给脉冲,以驱动坐标轴所在的步进电机运动。 目前已经有很多属于脉冲增量插补算法的具体算法被用到实际的生产应用中,在这里我们简单介绍几种比较常见的。
逐点比较法。逐点比较法最开始被称为区域判别法,又称代数运算法或醉步式近似法。是一种逐点计算、判别偏差并修正逼近理论轨迹的方法。 逐点比较法的基本思想就是在刀具按理论轨迹运动加工工件轮廓的时候,不断比较刀具与工件轮廓之间的相对位置, 并根据比较结果决定下一步的进给方向,使刀具向减小误差的方向移动。
数字积分法。数字积分法又称数字微分分析法DDA(Digital Differential Analyzer),简称积分器。这种算法是在数字积分器的基础上建立起来的一种插补算法, 可以较为方便的实现一次、二次曲线的插补。具有运算速度快、脉冲分配均匀、易于实现多坐标联动及描绘平面各种函数曲线的特点,应用比较广泛。
Bresenham算法。这种算法本来是计算机图形学里的一种用来快速画直线段和圆的绘图算法,由于算法非常成熟并且十分高效,目前也逐渐被引入到数控系统中, 通常作为直线插补算法或者多轴联动算法使用。例如著名的开源3D打印机固件Marlin,其内部就应用了Bresenham算法。
在普通的开环数控系统中,逐点比较法和数字积分法应用最为广泛,其中逐点比较法计算简单直观容易理解,并且输出为步进式的脉冲信号,尤其适合步进电机驱动的开环数控系统。 所以本章及下一章主要讲解逐点比较法的原理和实现过程。
14.2. 逐点比较法直线插补原理¶
逐点比较法的基本原理其实在上一节介绍的时候已有简单提及,这种算法能逐点计算和比较加工偏差,以控制刀具的下一步进给。一句话概括;逐点比较法是以阶梯折现来逼近直线段和圆弧等曲线。 每完成一次进给都需要以下4个步骤:
偏差判别:判断当前加工点和理论加工图形之间的相对位置,决定下一步X、Y轴的运动方向;
坐标进给:根据得到的偏差,控制指定坐标轴进给(移动)一步,逼近理论图形,减小误差;
偏差计算:计算新的加工点与理论加工图像间的偏差大小,作为下一步判别的依据;
终点判别:判断是否到达加工终点,如果到达终点则停止插补,如过没有到达终点,则回到第一个步骤, 不断重复整个过程,直到到达轨迹终点。
逐点比较法可以插补直线和圆弧,并且都存在上述4个步骤,在这一章中我们先来讲解逐点比较法如何实现直线插补,圆弧插补放到下一章。 为了降低复杂度,我们首先讲解在坐标轴第一象限的直线的插补原理。
14.2.1. 偏差判别¶
假设现在有一条如下图所示的第一象限直线OE,取O点为坐标原点,直线终点为E点且坐标已知,图中有一动点P为加工点。
根据上图信息使用直线的两点式方程可得直线OE的方程:
设Fi为加工动点P与直线OE间的偏差,将Fi和点P坐标代入上式可得:
从上面等式中可以分析出,Fi的符号能够反映动点P和直线OE的位置偏差情况。
若Fi= 0,表示动点P在直线OE内;
若Fi> 0,表示动点P在直线OE的上方;
若Fi< 0,表示动点P在直线OE的下方。
综上,这个等式便是逐点比较法直线插补的偏差判别方程。
14.2.2. 坐标进给¶
逐点比较法的坐标进给有两个原则:一是减小加工动点相对于理论轨迹的位置偏差,二是进给方向总是平行与某个坐标轴。 根据这个原则以及上个步骤偏差判别的信息,可以得出直线插补的坐标进给方法:
当Fi= 0时,动点P在直线内,可向+X方向进给一步,也可向+Y方向进给一步,通常规定向+X方向进给;
当Fi> 0时,动点P在直线上方,应该向+X方向进给一步;
当Fi< 0时,动点P在直线下方,应该向+Y方向进给一步;
开始坐标进给前,刀具总是位于直线轨迹的起点处,此时Fi= 0,整个坐标进给的轨迹效果可以用一张图演示出来,如下图所示。
当然,图中只做为直线插补中刀具运动轨迹的演示,实际应用中的运动轨迹并不会像图里这么夸张,进给一步的长度是一个脉冲当量,以保证加工精度。
14.2.3. 偏差计算¶
在坐标进给之后得到新的动点坐标值,此时需要计算新的动点和理论轨迹之间的偏差值。从前面的讨论中我们知道了偏差值Fi的计算公式, 可以通过公式直接求出Fi。虽然现在的各种控制器基本可以轻松的做乘法运算,但是为了追求更高的运行效率,我们把当前的偏差计算公式做一点小小的优化, 将其变为递推公式,即设法找到相邻两个加工动点偏差值间的关系。
假设当Fi> 0时,加工动点向+X方向进给一步,生成一个新的动点Pi+1,坐标是(Xi+1, Yi+1), 则新动点的偏差值Fi+1计算公式为:
又因为动点Pi+1的坐标可由P点表示:
所以将由P点表示的Pi+1坐标代入Fi+1式中,可得:
最后得出的这个公式便是逐点比较法的第一象限直线插补偏差计算的递推公式,从式中可以看出,偏差Fi+1的计算只跟上一步进给的偏差Fi和终点坐标值有关, 且只有加法运算,比原始公式更简单快速。
同理可得,当Fi< 0,加工动点向+Y方向进给一步后的新偏差值递推公式:
14.2.4. 终点判别¶
常用的终点判别方法有三种,终点坐标法、投影法和总步长法。
终点坐标法。在启动插补之前,先定义X、Y两个方向的步长计数器,分别记录终点坐标在X、Y两个方向上的值。开始插补后当X、Y方向每进给一步, 就在相应的计数器中减1,直到两个计数器的值都减为0时,刀具抵达终点,停止插补。
投影法。在插补前,先比较出终点坐标的X、Y值中较大的一个,然后以较大的数值作为计数器的值,当对应的轴有进给时,计数器减1,直到计数器为0。 相当于终点坐标向值较大的轴做投影,所以叫投影法。
总步长法,即插补前,将终点坐标的X、Y值求和,得到一个总步长计数器,开始插补后,无论哪个轴进给一步,总步长计数器都减1,直到计数器等于0,停止插补。
以上三种终点判别的方法,全部使用坐标的绝对值进行计算。
14.3. 第一象限直线插补实验¶
上一节讲解了逐点比较法的第一象限直线插补原理,接下来结合实验程序讲解具体实现。本实验讲解如何实现第一象限直线插补,学习本节内容时, 请打开配套的工程配合阅读。
14.3.1. 硬件设计¶
本实验用到的步进电机与之前的步进电机例程相同,所以硬件连接上也基本相同,不同的是使用了2个步进电机,分别接入开发板的步进电机接口1、接口2中, 如下图所示。接口1的步进电机对应X轴,接口2的步进电机对应Y轴。
14.3.2. 编程要点¶
步进电机基础控制
步进电机定时器比较中断配置
在定时器中完成直线插补的4个步骤
通过对直线参数的设置实现直线插补
14.3.3. 软件设计¶
14.3.3.1. 新建工程¶
本次第一象限直线插补例程是在步进电机串口控制例程的基础上编写的,直接在基础部分章节的 “300_Motor_Stepper_PWM_Control” 例程的基础上修改程序。
- 对于 e2 studio 开发环境:
拷贝一份我们之前的 e2s 工程 “300_Motor_Stepper_PWM_Control”, 然后将工程文件夹重命名为 “309_Motor_Stepper_Linear_interpolation_First_Quadrant”,最后再将它导入到我们的 e2 studio 工作空间中。
- 对于 Keil 开发环境:
拷贝一份我们之前的 Keil 工程 “300_Motor_Stepper_PWM_Control”, 然后将工程文件夹重命名为 “309_Motor_Stepper_Linear_interpolation_First_Quadrant”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。
工程新建好之后,在工程根目录的 “src->motor_controls” 文件夹下面新建源文件和头文件:“bsp_linear_interpolation.c” 和 “bsp_linear_interpolation.h”。 工程文件结构如下。
309_Motor_Stepper_Linear_interpolation_First_Quadrant
├─ ......
└─ src
├─ beep
│ ├─ bsp_beep.c
│ └─ bsp_beep.h
├─ debug_uart
│ ├─ bsp_debug_uart.c
│ └─ bsp_debug_uart.h
├─ key
│ ├─ bsp_key_irq.c
│ └─ bsp_key_irq.h
├─ led
│ ├─ bsp_led.c
│ └─ bsp_led.h
├─ motor_controls
│ ├─ bsp_linear_interpolation.c //新建文件
│ ├─ bsp_linear_interpolation.h //新建文件
│ ├─ bsp_motor_control.c
│ └─ bsp_motor_control.h
└─ hal_entry.c
本例程需要另一个电机,那么在FSP配置中需要配置,并将两个步进电机的模式改为比较输出模式,以及需要两个按键中断来进行试验, 关于这部分可以参考实际例程和之前的内容,这里不再赘述。
14.3.3.2. 电机宏定义¶
使用宏定义是为了简化代码编写和提高可读性,特别是在插补控制中,双电机需要同步运动。 通过将电机的使能、方向控制等操作封装为宏定义,可以方便地调用这些操作,同时便于管理和维护参数。 在本插补例程中,定义了两个电机的控制函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | /*********************X轴电机定义*******************/
/* 步进电机使能控制 */
#define X_STEP_ENABLED R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_13_PIN_12, BSP_IO_LEVEL_LOW) // 步进电机使能
#define X_STEP_DISABLED R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_13_PIN_12, BSP_IO_LEVEL_HIGH) // 步进电机禁用
/* 步进电机方向控制 */
#define X_STEP_CW R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_10, BSP_IO_LEVEL_HIGH) // 设置步进电机顺时针方向
#define X_STEP_CCW R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_10, BSP_IO_LEVEL_LOW) // 设置步进电机逆时针方向
/* 步进电机状态及方向反转 */
#define X_STEP_STATE_TOGGLE R_PORT13->PODR ^= 1 << (BSP_IO_PORT_13_PIN_12 & 0xFF) // 切换步进电机的使能状态
#define X_STEP_DIRECTION_TOGGLE R_PORT14->PODR ^= 1 << (BSP_IO_PORT_14_PIN_10 & 0xFF) // 切换步进电机的方向
/*********************Y轴电机定义*******************/
/* 步进电机使能控制 */
#define Y_STEP_ENABLED R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_12_PIN_08, BSP_IO_LEVEL_LOW) // 步进电机使能
#define Y_STEP_DISABLED R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_12_PIN_08, BSP_IO_LEVEL_HIGH) // 步进电机禁用
/* 步进电机方向控制 */
#define Y_STEP_CW R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_12, BSP_IO_LEVEL_HIGH) // 设置步进电机顺时针方向
#define Y_STEP_CCW R_IOPORT_PinWrite(&g_ioport_ctrl, BSP_IO_PORT_14_PIN_12, BSP_IO_LEVEL_LOW) // 设置步进电机逆时针方向
/* 步进电机状态及方向反转 */
#define Y_STEP_STATE_TOGGLE R_PORT13->PODR ^= 1 << (BSP_IO_PORT_12_PIN_08 & 0xFF) // 切换步进电机的使能状态
#define Y_STEP_DIRECTION_TOGGLE R_PORT14->PODR ^= 1 << (BSP_IO_PORT_14_PIN_12 & 0xFF) // 切换步进电机的方向
/* 定时器时钟频率 */
#define TIMER_CLK_FREQ 120000000 // 定时器时钟频率为 120 MHz
/* 计算目标频率对应的定时器周期值 */
#define Hz_Set(frequency_hz) ((TIMER_CLK_FREQ / (frequency_hz)) - 1) // 根据目标频率计算周期值
/* 电机单圈参数 */
#define STEP_ANGLE 1.8f // 步进电机步距角(单位:度)
#define FSPR (360.0f / STEP_ANGLE) // 电机一圈所需步数(无细分)
#define MICRO_STEP 32 // 细分数
#define SPR ((uint32_t)(FSPR * MICRO_STEP)) // 细分后电机一圈所需脉冲数
|
14.3.3.3. 电机控制函数¶
代码通过初始化 GPT 定时器实现比较输出功能,用于控制步进电机的启动、停止和速度调整。 函数中使用宏定义简化了电机的使能和方向设置, Motor_Control_SetSpeed 函数通过设置定时器的比较值和周期, 精确控制双电机的频率输出,为插补算法提供控制基础。
/* 电机初始化 */
void Motor_Control_Init(void)
{
fsp_err_t err = FSP_SUCCESS;
/* 初始化 GPT 模块 */
err = R_GPT_Open(&x_step_pwm_ctrl, &x_step_pwm_cfg);
assert(FSP_SUCCESS == err);
err = R_GPT_Enable(&x_step_pwm_ctrl);
assert(FSP_SUCCESS == err);
/* 初始化 GPT 模块 */
err = R_GPT_Open(&y_step_pwm_ctrl, &y_step_pwm_cfg);
assert(FSP_SUCCESS == err);
err = R_GPT_Enable(&y_step_pwm_ctrl);
assert(FSP_SUCCESS == err);
X_STEP_ENABLED; //设置 X 轴电机使能
Y_STEP_ENABLED; //设置 Y 轴电机使能
X_STEP_CW; // 设置 X 轴方向为正转
Y_STEP_CW; // 设置 Y 轴方向为正转
}
/**
* @brief 启动电机
*/
void X_Motor_Control_Start(void)
{
R_GPT_Start(&x_step_pwm_ctrl); // 使能定时器的比较输出
}
/**
* @brief 启动电机
*/
void Y_Motor_Control_Start(void)
{
R_GPT_Start(&y_step_pwm_ctrl); // 使能定时器的比较输出
}
/**
* @brief 停止电机
*/
void X_Motor_Control_Stop(void)
{
R_GPT_Stop(&x_step_pwm_ctrl); // 禁用定时器的比较输出
}
/**
* @brief 停止电机
*/
void Y_Motor_Control_Stop(void)
{
R_GPT_Stop(&y_step_pwm_ctrl); // 禁用定时器的比较输出
}
/**
* @brief 控制电机速度
* @param[in] speed_hz 目标速度,单位:Hz
* @retval 无
*/
void Motor_Control_SetSpeed(uint32_t x_speed_hz, uint32_t y_speed_hz)
{
// 使用 R_GPT_CompareMatchSet 和 R_GPT_PeriodSet 来设置定时器周期,从而控制 X 轴输出频率
R_GPT_CompareMatchSet(&x_step_pwm_ctrl, x_speed_hz * 2, TIMER_COMPARE_MATCH_B);
R_GPT_PeriodSet(&x_step_pwm_ctrl, Hz_Set(x_speed_hz));
// 使用 R_GPT_CompareMatchSet 和 R_GPT_PeriodSet 来设置定时器周期,从而控制 Y 轴输出频率
R_GPT_CompareMatchSet(&y_step_pwm_ctrl, y_speed_hz * 2, TIMER_COMPARE_MATCH_B);
R_GPT_PeriodSet(&y_step_pwm_ctrl, Hz_Set(y_speed_hz));
}
14.3.3.4. 直线插补相关参数¶
以下代码中分别定义了一个枚举型和结构体,用于管理第一象限直线插补的相关数据参数。LinearInterpolation_TypeDef结构体的成员中, endpoint_x和endpoint_y用于记录直线插补的终点坐标,endpoint_pulse记录运动到终点需要多少脉冲,active_axis记录当前活动的轴, 也就是当前进给的轴,deviation就是上面讲到的Fi,用来记录和计算偏差值。
/* 坐标轴枚举 */
typedef enum{
x_axis = 0U,
y_axis
}Axis_TypeDef;
/* 直线插补参数结构体 */
typedef struct{
__IO uint32_t endpoint_x; //终点坐标X
__IO uint32_t endpoint_y; //终点坐标Y
__IO uint32_t endpoint_pulse; //到达终点位置需要的脉冲数
__IO uint32_t active_axis; //当前运动的轴
__IO int32_t deviation; //偏差参数
}LinearInterpolation_TypeDef;
14.3.3.5. 直线插补算法实现¶
以下代码启动第一象限直线插补,首先将偏差Fi清零,设置终点坐标值,为什么没有起点坐标呢?因为逐点比较法属于脉冲增量插补算法, 参数坐标都是增量形式的相对坐标,也就是说默认直线的起点在坐标系原点(0,0)上,或以上一条直线的终点为新的起点。 把完成插补所需的脉冲数记录为终点的X、Y坐标值之和,用于插补终点的判断,这里使用的是总步长法。
由于是刚开始插补,此时加工点必定在预计的直线上,根据之前所讲内容,加工点在直线上时向X方向进给一步,也就是在上述代码中把进给的轴设置为X轴, 同时利用Fi的偏差公式,计算出进给之后的新偏差值。计算出偏差值后,设置两轴的进给速度,实际为定时器的比较输出频率, 使能主输出后进行判断,开启X轴的输出,开始处理后续数据和发送脉冲。
/**
* @brief 直线增量插补运动
* @param inc_x:终点坐标X的增量
* @param inc_y:终点坐标Y的增量
* @param speed:进给速度
* @retval 无
*/
void InterPolation_Move(uint32_t inc_x, uint32_t inc_y, uint32_t speed)
{
/* 偏差清零 */
interpolation_para.deviation = 0;
/* 设置终点坐标 */
interpolation_para.endpoint_x = inc_x;
interpolation_para.endpoint_y = inc_y;
/* 所需脉冲数为X、Y坐标增量之和 */
interpolation_para.endpoint_pulse = inc_x + inc_y;
/* 第一步进给的活动轴为X轴 */
interpolation_para.active_axis = x_axis;
/* 计算偏差 */
interpolation_para.deviation -= (int32_t)interpolation_para.endpoint_y;
// 设置电机的速度
Motor_Control_SetSpeed(speed, speed);
if(interpolation_para.active_axis == x_axis)
// 启动 X 轴电机控制
X_Motor_Control_Start();
else
// 启动 Y 轴电机控制
Y_Motor_Control_Start();
}
14.3.3.6. 定时器比较中断函数¶
该回调函数是两个定时器共用的函数,负责执行直线插补控制逻辑,按照一定的频率切换电机的活动轴,实现指定路径的步进。具体流程如下:
事件触发检测 检查定时器的中断事件是否为 TIMER_EVENT_CYCLE_END,确保函数执行时是因为定时器比较事件触发的。
保存上一次活动轴 在计算下一个活动轴之前,先记录当前活动轴 active_axis ,后续用来判断是否需要切换活动轴。
插补偏差计算 插补的核心在于偏差的计算:
若偏差 deviation ≥ 0,说明插补点位于目标直线的上方,需要进给 X轴,同时减去偏差值以调整方向。
若偏差 deviation < 0,说明插补点位于目标直线的下方,需要进给 Y轴,同时增加偏差值。
偏差的更新方式基于数字微分法的插补原理,确保电机按比例接近目标点。
判断活动轴切换 比较上一次的活动轴与当前计算出的活动轴是否一致:
如果一致,则无需切换轴,保持当前轴继续工作。
如果不一致,则需要停掉当前轴,启动新的活动轴以调整运动方向。
减小剩余步数 每次定时器事件触发,插补逻辑完成一次步进操作,因此需要将总的插补步数 endpoint_pulse 减 1,表示前进一步。
检查插补完成 判断剩余步数是否为零:
如果步数为零,说明插补已完成,则关闭所有电机,终止运动。
如果步数未完成,则等待下一次事件继续插补。
/**
* @brief 定时器比较中断回调函数
* @param htim:定时器句柄指针
* @note 无
* @retval 无
*/
void x_y_step_callback(timer_callback_args_t *p_args)
{
if(p_args -> event == TIMER_EVENT_CYCLE_END){
uint32_t last_axis = 0;
/* 记录上一步的进给活动轴 */
last_axis = interpolation_para.active_axis;
/* 根据上一步的偏差,判断的进给方向,并计算下一步的偏差 */
if(interpolation_para.deviation >= 0)
{
/* 偏差>0,在直线上方,进给X轴,计算偏差 */
interpolation_para.active_axis = x_axis;
interpolation_para.deviation -= (int32_t)interpolation_para.endpoint_y;
}
else
{
/* 偏差<0,在直线下方,进给Y轴,计算偏差 */
interpolation_para.active_axis = y_axis;
interpolation_para.deviation += (int32_t)interpolation_para.endpoint_x;
}
/* 下一步的活动轴与上一步的不一致时,需要换轴 */
if(last_axis != interpolation_para.active_axis)
{
// 判断活动轴是否为 X 轴
if (interpolation_para.active_axis == x_axis)
{
// 启动 X 轴电机,停止 Y 轴电机
X_Motor_Control_Start();
Y_Motor_Control_Stop();
}
else
{
// 启动 Y 轴电机,停止 X 轴电机
Y_Motor_Control_Start();
X_Motor_Control_Stop();
}
}
/* 进给总步数减1 */
interpolation_para.endpoint_pulse--;
/* 判断是否完成插补 */
if(interpolation_para.endpoint_pulse == 0)
{
/* 关闭定时器 */
X_Motor_Control_Stop();
Y_Motor_Control_Stop();
}
}
}
14.3.3.7. 主函数¶
主函数中主要就是一些外设的初始化,包括步进电机的定时器初始化。
在 sw2_irq_callback 中,X 和 Y 轴的目标位置均为 SPR(步进电机在32细分下转一圈所需要的脉冲),意味着按下按键2时,步进电机会根据 SPR 的值在 X 和 Y 轴上同时进行插补运动。
在 sw3_irq_callback 中,X 和 Y 轴的目标位置均为 SPR * 5,按下按键3时,步进电机会执行更大的插补运动,目标位置为 SPR 的5倍。
// 按键2中断回调函数
void sw2_irq_callback(external_irq_callback_args_t *p_args)
{
// 防止回调函数中没有使用形参的警告产生
FSP_PARAMETER_NOT_USED(p_args);
// 执行第一象限直线插补运动,传入的参数分别是 X、Y 轴的目标位置
InterPolation_Move(SPR, SPR, 6400);
}
// 按键3中断回调函数
void sw3_irq_callback(external_irq_callback_args_t *p_args)
{
// 防止回调函数中没有使用形参的警告产生
FSP_PARAMETER_NOT_USED(p_args);
// 执行第一象限直线插补运动,传入的参数分别是 X、Y 轴的目标位置
InterPolation_Move(SPR * 5, SPR * 5, 6400);
}
void hal_entry(void)
{
/* TODO: add your own code here */
LED_Init(); // LED 初始化
Debug_UART9_Init();//调试串口初始化
IRQ_Init(); //按键中断初始化
Motor_Control_Init(); //初始化电机定时器
MOTOR_PRINT("这是一个瑞萨RA,步进电机第一象限直线插补例程\r\n");
while(1)
{}
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
14.3.4. 下载验证¶
使用两轴丝杆滑台组成一个标准X-Y滑动平台,将步进电机连接好,下载程序到开发板后,按下开发板的按键,可以看到丝杆滑台上的滑块沿着程序设定的直线轨迹运动。
14.4. 任意象限直线插补¶
在前面的内容中,我们详细讲解了第一象限直线插补的原理和实现,不过实际应用中并不只有第一象限的直线,任意象限和方向的直线都可能会遇到, 所以下面的内容将会讲解如果实现任意象限的直线插补。
14.4.1. 任意象限直线插补原理¶
假设有一条起点坐标为原点,终点坐标为E‘的直线OE‘位于第二象限,如下图所示。 图中直线OE‘和直线OE关于Y轴对称。对OE进行直线插补的时候,如果把原本沿+X方向的进给变成沿-X方向, 也就是步进电机运动方向相反。那么实际插补出的就是第二象限的直线OE‘。
同理,如果是插补第三象限的直线,由于其和第一象限直线OE关于原点对称,所以依旧可以按照第一象限直线OE插补,唯一不同的只是进给方向。
综上可得,任意象限直线插补的方法都可用第一象限直线插补推出,偏差计算公式都相同,偏差值的计算使用坐标的绝对值。 设L1、L2、L3、L4分别表示第1、2、3、4象限的直线,则任意象限直线插补的X、Y轴进给方向如下图所示。
在上图中,靠近Y轴区域偏差F>0,靠近X轴区域偏差F<0。当F≥0时,进给都是沿X轴,动点坐标的X轴绝对值增大; 当F<0时,进给都沿Y轴,动点坐标的Y轴绝对值增大。把以上内容整理成表格,如下表所示。
至此,任意象限直线插补的原理已经讲解完毕,接下来开始讲解在程序中如何实现。
14.5. 任意象限直线插补实验¶
本实验讲解如何实现任意象限的直线插补。学习本节内容时,请打开配套的工程配合阅读。
14.5.1. 硬件设计¶
本实验的硬件设计部分与上一个实验完全相同,在此不再赘述。如有不清楚的地方,请查看第一象限直线插补实验。
14.5.2. 软件设计¶
14.5.2.1. 新建工程¶
本次任意象限直线插补例程是在第一象限直线插补例程的基础上编写的,直接在 “309_Motor_Stepper_Linear_interpolation_First_Quadrant” 例程的基础上修改程序。
- 对于 e2 studio 开发环境:
拷贝一份我们之前的 e2s 工程 “309_Motor_Stepper_Linear_interpolation_First_Quadrant”, 然后将工程文件夹重命名为 “310_Motor_Stepper_Linear_interpolation_Any_Quadrant”,最后再将它导入到我们的 e2 studio 工作空间中。
- 对于 Keil 开发环境:
拷贝一份我们之前的 Keil 工程 “309_Motor_Stepper_Linear_interpolation_First_Quadrant”, 然后将工程文件夹重命名为 “310_Motor_Stepper_Linear_interpolation_Any_Quadrant”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。
14.5.2.2. 直线插补相关参数¶
本实验中的步进电机相关宏定义以及控制部分与上一实验完全相同,不再赘述,这里重点详解任意象限的插补算法部分的实现。
以下代码是任意象限直线插补的相关数据参数,同样由一个枚举和一个结构体构成,其中结构体中相比第一象限的插补多了一个成员变量,用来管理整个插补运动的状态。
/* 坐标轴枚举 */
typedef enum{
x_axis = 0U,
y_axis
}Axis_TypeDef;
/* 直线插补参数结构体 */
typedef struct{
__IO uint32_t endpoint_x; //终点坐标X
__IO uint32_t endpoint_y; //终点坐标Y
__IO uint32_t endpoint_pulse; //到达终点位置需要的脉冲数
__IO uint32_t active_axis; //当前运动的轴
__IO int32_t deviation; //偏差参数
__IO uint8_t motionstatus : 1; //插补运动状态
}LinearInterpolation_TypeDef;
14.5.2.3. 直线插补算法实现¶
以下代码是实现任意象限直线插补的关键,Linear_Interpolation函数的入口参数是直线终点坐标值和进给速度,以下是控制逻辑。
判断终点坐标的X值是否大于0,如果X<0,X轴步进电机反转,如果X>0,X轴步进电机正转,同时计算出坐标绝对值;
判断终点坐标的Y值是否大于0,如果Y<0,Y轴步进电机反转,如果Y>0,Y轴步进电机正转,同时计算出坐标绝对值;
设置完X、Y轴步进电机的运动方向之后,根据计算出的坐标绝对值开始执行插补运动。
可以看到部分代码与第一象限直线插补实验完全相同,这是因为直线所在象限的判断和进给方向的处理已经在Linear_Interpolation函数中完成, 剩下的直接按照第一象限直线进行插补即可。
/**
* @brief 直线增量插补运动
* @param inc_x:终点坐标X的增量
* @param inc_y:终点坐标Y的增量
* @param speed:进给速度
* @retval 无
*/
void InterPolation_Move(uint32_t inc_x, uint32_t inc_y, uint32_t speed)
{
/* 偏差清零 */
interpolation_para.deviation = 0;
/* 设置终点坐标 */
interpolation_para.endpoint_x = inc_x;
interpolation_para.endpoint_y = inc_y;
/* 所需脉冲数为X、Y坐标增量之和 */
interpolation_para.endpoint_pulse = inc_x + inc_y;
/* 第一步进给的活动轴为X轴 */
interpolation_para.active_axis = x_axis;
/* 计算偏差 */
interpolation_para.deviation -= (int32_t)interpolation_para.endpoint_y;
// 设置电机的速度
Motor_Control_SetSpeed(speed, speed);
if(interpolation_para.active_axis == x_axis)
// 启动 X 轴电机控制
X_Motor_Control_Start();
else
// 启动 Y 轴电机控制
Y_Motor_Control_Start();
}
/**
* @brief 任意象限直线插补运动
* @param coordi_x:终点坐标X的增量
* @param coordi_y:终点坐标Y的增量
* @param speed:进给速度,定时器计数值
* @retval 无
*/
void Linear_Interpolation(int32_t coordi_x, int32_t coordi_y, uint32_t speed)
{
/* 判断当前是否正在做插补运动 */
if(interpolation_para.motionstatus != 0)
return;
/* 判断坐标正负,以此决定各轴的运动方向 */
if (coordi_x < 0)
{
X_STEP_CCW; // X 轴逆时针方向
coordi_x = -coordi_x; // 取 X 坐标的绝对值
}
else
{
X_STEP_CW; // X 轴顺时针方向
}
if (coordi_y < 0)
{
Y_STEP_CCW; // Y 轴逆时针方向
coordi_y = -coordi_y; // 取 Y 坐标的绝对值
}
else
{
Y_STEP_CW; // Y 轴顺时针方向
}
/* 开始插补运动 */
InterPolation_Move((uint32_t)coordi_x,(uint32_t) coordi_y, speed);
}
14.5.2.4. 定时器比较中断函数¶
该回调函数是两个定时器共用的函数,负责执行直线插补控制逻辑,按照一定的频率切换电机的活动轴,实现指定路径的步进,具体流程与上一例程完全相同。
/**
* @brief 定时器比较中断回调函数
* @param htim:定时器句柄指针
* @note 无
* @retval 无
*/
void x_y_step_callback(timer_callback_args_t *p_args)
{
if(p_args -> event == TIMER_EVENT_CYCLE_END){
uint32_t last_axis = 0;
/* 记录上一步的进给活动轴 */
last_axis = interpolation_para.active_axis;
/* 根据上一步的偏差,判断的进给方向,并计算下一步的偏差 */
if(interpolation_para.deviation >= 0)
{
/* 偏差>0,在直线上方,进给X轴,计算偏差 */
interpolation_para.active_axis = x_axis;
interpolation_para.deviation -= (int32_t)interpolation_para.endpoint_y;
}
else
{
/* 偏差<0,在直线下方,进给Y轴,计算偏差 */
interpolation_para.active_axis = y_axis;
interpolation_para.deviation += (int32_t)interpolation_para.endpoint_x;
}
/* 下一步的活动轴与上一步的不一致时,需要换轴 */
if(last_axis != interpolation_para.active_axis)
{
// 判断活动轴是否为 X 轴
if (interpolation_para.active_axis == x_axis)
{
// 启动 X 轴电机,停止 Y 轴电机
X_Motor_Control_Start();
Y_Motor_Control_Stop();
}
else
{
// 启动 Y 轴电机,停止 X 轴电机
Y_Motor_Control_Start();
X_Motor_Control_Stop();
}
}
/* 进给总步数减1 */
interpolation_para.endpoint_pulse--;
/* 判断是否完成插补 */
if(interpolation_para.endpoint_pulse == 0)
{
/* 关闭定时器 */
X_Motor_Control_Stop();
Y_Motor_Control_Stop();
// 重置插补参数中的运动状态
interpolation_para.motionstatus = 0;
}
}
}
14.5.2.5. 主函数¶
主函数中主要就是一些外设的初始化,包括步进电机的定时器初始化。
当按下按键时,程序会根据预设的目标位置启动步进电机进行插补运动。具体的运动要求如下:
按键2中断回调函数 (sw2_irq_callback):
按下按键2后,电机会执行 第二象限 的直线插补运动。
目标位置为: X轴 = -SPR * 5 , Y轴 = SPR * 5 。这里的 SPR 是步进电机在32细分下转一圈所需要的脉冲数。
按键3中断回调函数 (sw3_irq_callback):
按下按键3后,电机会执行 第三象限 的直线插补运动。
目标位置为: X轴 = -SPR * 5 , Y轴 = -SPR * 5 。
// 按键2中断回调函数
void sw2_irq_callback(external_irq_callback_args_t *p_args)
{
// 防止回调函数中没有使用形参的警告产生
FSP_PARAMETER_NOT_USED(p_args);
// 执行第二象限直线插补运动,传入 X、Y 轴的目标位置
Linear_Interpolation(-SPR * 5, SPR * 5, 6400);
}
// 按键3中断回调函数
void sw3_irq_callback(external_irq_callback_args_t *p_args)
{
// 防止回调函数中没有使用形参的警告产生
FSP_PARAMETER_NOT_USED(p_args);
// 执行第三象限直线插补运动,传入 X、Y 轴的目标位置
Linear_Interpolation(-SPR * 5, -SPR * 5, 6400);
}
void hal_entry(void)
{
/* TODO: add your own code here */
LED_Init(); // LED 初始化
Debug_UART9_Init();//调试串口初始化
IRQ_Init(); //按键中断初始化
Motor_Control_Init(); //初始化电机定时器
MOTOR_PRINT("这是一个瑞萨RA,步进电机任意象限直线插补例程\r\n");
while(1)
{}
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
14.5.3. 下载验证¶
使用两轴丝杆滑台组成一个标准X-Y滑动平台,将步进电机连接好,下载程序到开发板后,按下开发板的按键,可以看到丝杆滑台上的滑块沿着程序设定的直线轨迹运动。