15. 步进电机圆弧插补实现

15.1. 逐点比较法圆弧插补原理

逐点比较法的圆弧插补和直线插补一样,都分为偏差判别、坐标进给、偏差计算和终点判别四个步骤。逐点比较法圆弧插补中,一般以坐标原点为圆心,给出圆弧起点坐标和终点坐标, 以及圆弧的加工方向和所在的象限。为了降低复杂度,我们首先讲解位于第一象限的双向圆弧插补。

15.1.1. 偏差判别

设坐标轴中有一段逆时针方向的圆弧AB,圆形为坐标原点,起点坐标A(X0,Y0),终点坐标B(Xe,Ye),圆弧上有一加工动点P, 坐标P(Xi,Yi),如下图所示。

第一象限逆时针圆弧插补坐标图

设A点到圆心的距离为R,动点P到圆心的距离为RP,根据上图给出的信息,使用圆的标准方程可得圆弧AB所在圆的方程和动点P所在圆的方程:

第一象限逆时针圆弧插补公式1 第一象限逆时针圆弧插补公式2

设动点P所在圆的半径与圆弧AB所在圆的半径之间的差值为插补的偏差值,记为Fi,则有式子:

第一象限逆时针圆弧插补偏差公式

从上面的式子中可以分析出,Fi的正负能够反映动点P与圆弧AB的位置偏差情况。

  • 当Fi= 0时,RP= R,则动点P在圆弧AB上;

  • 当Fi> 0时,RP> R,则动点P在圆弧AB的外侧;

  • 当Fi< 0时,RP< R,则动点P在圆弧AB的内侧。

15.1.2. 坐标进给

逐点比较法的坐标进给有两个原则:一是减小加工动点相对于理论轨迹的位置偏差,二是进给方向总是平行与某个坐标轴。 根据这个原则以及上个步骤偏差判别的信息,可以得出第一象限逆时针圆弧插补的坐标进给方法:

  • Fi= 0,动点P在圆弧AB上,可向圆内进给一步,也可向圆外方向进给一步,通常规定向圆内即-X方向进给;

  • Fi> 0,动点P在圆弧AB的外侧,应该向圆内即-X方向进给一步;

  • Fi< 0,动点P在圆弧AB的内侧,应该向圆外即+Y方向进给一步;

  • 当动点P在X轴上时,为减小误差,通常直接向+Y方向进给一步。

整个坐标进给的轨迹效果可以用一张图演示出来,如下图所示。

第一象限逆时针圆弧插补轨迹图

当然,图中只作为第一象限逆时针圆弧插补中刀具运动轨迹的演示,实际应用中的运动轨迹并不会像图里这么夸张,进给一步的长度是一个脉冲当量,以保证加工精度。

15.1.3. 偏差计算

在坐标进给之后得到新的动点坐标值,此时需要计算新的动点和理论轨迹之间的偏差值。从前面的讨论中我们知道了偏差值Fi的计算公式, 可以通过公式直接求出Fi。不过公式中有4次乘方运算,也可以说是4次乘法运算,虽然现在的各种控制器基本可以轻松的做乘法运算, 但是为了追求更高的运行效率,我们把当前的偏差计算公式做一点小小的优化,将其变为递推公式。

假设当Fi> 0时,加工动点P向圆内进给一步,生成一个新的动点Pi+1,坐标是(Xi+1, Yi+1), 则新动点的偏差值Fi+1计算公式为:

第一象限逆时针圆弧插补偏差计算公式1

又因为动点Pi+1的坐标可由P点表示:

第一象限逆时针圆弧插补偏差计算公式2

所以将由P点表示的Pi+1坐标代入Fi+1式中,可得:

第一象限逆时针圆弧插补偏差计算公式3

上面最后得出的这个公式便是逐点比较法的第一象限逆时针圆弧插补偏差计算的递推公式,从式中可以看出, 偏差Fi+1的值只跟上一步进给的偏差Fi和上一步的动点坐标值Pi有关,且将四次乘方运算转为一次乘2运算,比原始公式更简单快速。

同理可得,当Fi< 0时,加工动点向+Y方向进给一步后的新偏差值递推公式:

第一象限逆时针圆弧插补偏差计算公式4

需要指出的是,以上两个圆弧插补偏差计算公式中包含了上一个加工动点的坐标Xi和Yi,由于加工动点是变化的, 因此在计算偏差Fi+1的同时,还要计算出新的动点坐标Xi+1和Yi+1,为新的偏差值计算做准备,这是直线插补所不需要的。

接下来看第一象限的顺时针圆弧插补。设坐标轴中有一段顺时针方向的圆弧AB,圆形为坐标原点,起点坐标A(X0,Y0), 终点坐标B(Xe,Ye),圆弧上有一加工动点P,坐标P(Xi,Yi),如下图所示。

第一象限顺时针圆弧插补坐标图

根据上面逆时针圆弧插补的思路,可以推出第一象限顺时针圆弧插补的偏差判别、偏差值计算公式和XY坐标进给方向。如下表所示。

第一象限顺时针圆弧插补参数计算表

综合这张计算表我们可以发现几个特点,如果把第一象限顺时针圆弧的X、Y轴插补方向对调,就变成了第一象限逆时针圆弧的插补方向, 这样顺时针圆弧的插补问题就变成了逆时针的插补问题,两个方向的偏差计算公式形式保持不变,只是符号和对应的X、Y轴变化。

15.1.4. 终点判别

常用的终点判别方法有三种,终点坐标法、投影法和总步长法。

  • 终点坐标法。在启动插补之前,先定义X、Y两个方向的步长计数器,分别记录终点坐标在X、Y两个方向上的值。开始插补后当X、Y方向每进给一步, 就在相应的计数器中减1,直到两个计数器的值都减为0时,刀具抵达终点,停止插补。

  • 投影法。在插补前,先比较出终点坐标的X、Y值中较大的一个,然后以较大的数值作为计数器的值,当对应的轴有进给时,计数器减1,直到计数器为0。 相当于终点坐标向值较大的轴做投影,所以叫投影法。

  • 总步长法,即插补前,将终点坐标的X、Y值求和,得到一个总步长计数器,开始插补后,无论哪个轴进给一步,总步长计数器都减1,直到计数器等于0,停止插补。

以上三种终点判别的方法,全部使用坐标的绝对值进行计算。

15.2. 第一象限双向圆弧插补实验

上一节讲解了逐点比较法的第一象限圆弧插补原理,接下来结合实验程序讲解具体实现。本实验讲解如何实现第一象限双向圆弧插补。 学习本节内容时,请打开配套的工程配合阅读。

15.2.1. 硬件设计

本实验用到的步进电机与上一章节的步进电机例程相同,所以硬件连接上也相同,接口1的步进电机对应X轴,接口2的步进电机对应Y轴。 如有不清楚的地方,请查看第一象限直线插补实验。

15.2.2. 编程要点

  1. 步进电机基础控制

  2. 步进电机定时器比较中断配置

  3. 在定时器中完成圆弧插补的4个步骤

  4. 通过对圆弧参数的设置实现第一象限圆弧插补

15.2.3. 软件设计

15.2.3.1. 新建工程

本次第一象限双向圆弧插补例程,直接在上一章节的 “309_Motor_Stepper_Linear_interpolation_First_Quadrant” 例程的基础上修改程序。

对于 e2 studio 开发环境:

拷贝一份我们之前的 e2s 工程 “309_Motor_Stepper_Linear_interpolation_First_Quadrant”, 然后将工程文件夹重命名为 “311_Motor_Stepper_Circular_interpolation_First_Quadrant”,最后再将它导入到我们的 e2 studio 工作空间中。

对于 Keil 开发环境:

拷贝一份我们之前的 Keil 工程 “309_Motor_Stepper_Linear_interpolation_First_Quadrant”, 然后将工程文件夹重命名为 “311_Motor_Stepper_Circular_interpolation_First_Quadrant”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。

工程新建好之后,在工程根目录的 “src->motor_controls” 文件夹下面源文件和头文件: “bsp_linear_interpolation.c” 和 “bsp_linear_interpolation.h”改为“bsp_circular_interpolation.c” 和 “bsp_circular_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_circular_interpolation.c  //新建文件
   │  ├─ bsp_circular_interpolation.h  //新建文件
   │  ├─ bsp_motor_control.c
   │  └─ bsp_motor_control.h
   └─ hal_entry.c

15.2.3.2. 圆弧插补相关参数

电机控制相关参数与之前的直线插补章节完全一样,这里不再赘述,主要讲解与圆弧插补实现有关的部分内容。

以下代码中分别定义了两个枚举和结构体,用于管理第一象限双向圆弧插补的相关数据参数。CircularInterpolation_TypeDef结构体的成员中, 定义了圆弧起点和终点的坐标,endpoint_pulse记录运动到终点需要多少脉冲,active_axis记录当前活动的轴, 也就是当前进给的轴,deviation就是上面讲到的Fi,用于偏差值的记录和计算。

bsp_circular_interpolation.h-宏定义
/* 坐标轴枚举 */
typedef enum{
  x_axis = 0U,
  y_axis
}Axis_TypeDef;

/* 方向枚举 */
typedef enum{
    CW = 0U,  // 正转 (顺时针 Clockwise)
    CCW       // 反转 (逆时针 Counter-Clockwise)
} Direction_TypeDef;


/* 圆弧插补参数结构体 */
typedef struct{
  __IO int32_t startpoint_x;         //起点坐标X
  __IO int32_t startpoint_y;         //起点坐标Y
  __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;      //插补运动状态
  __IO uint8_t dir_x : 1;             //X轴运动方向
  __IO uint8_t dir_y : 1;             //Y轴运动方向
}CircularInterpolation_TypeDef;

15.2.3.3. 圆弧插补算法实现

首先来看第一象限的顺时针圆弧插补算法,函数中首先检查是否已有进行中的插补运动,若有则返回,并通过公式验证起点与终点是否在同一圆上,否则返回。 接着清零偏差,并初始化插补参数,包括起点、终点坐标及总脉冲数(起点与终点的步数之和)。 根据圆弧方向设置 X 轴顺时针、Y 轴逆时针的运动方向,利用宏定义完成方向设置。 如果起点 X 坐标为零,优先选择 X 轴进给,减小误差;否则优先选择 Y 轴进给,同时计算初始偏差。 最后,通过 Motor_Control_SetSpeed 设置电机速度,并根据当前活动轴启动对应电机,标记插补运动开始。

bsp_circular_interpolation.c-顺时针圆弧插补算法
/**
  * @brief  第一象限顺圆插补运动
  * @param  start_x:圆弧起点相对于圆心的坐标X,增量
  * @param  inc_y:终点坐标Y的增量
  * @param  speed:进给速度
  * @retval 无
  */
void Circular_InterPolation_CW(int32_t start_x, int32_t start_y, int32_t stop_x, int32_t stop_y, uint32_t speed)
{
  /* 判断当前是否正在做插补运动 */
  if(circular_para.motionstatus != 0)
    return;

  /* 检查起点、终点坐标是否在同一个圆上 */
  if(((start_x * start_x) + (start_y * start_y)) != ((stop_x * stop_x) + (stop_y * stop_y)))
    return;

  /* 偏差清零 */
  circular_para.deviation = 0;

  /* 起点坐标 */
  circular_para.startpoint_x = start_x;
  circular_para.startpoint_y = start_y;
  /* 终点坐标 */
  circular_para.endpoint_x = (uint32_t)stop_x;
  circular_para.endpoint_y = (uint32_t)stop_y;
  /* 所需脉冲数是从起点到终点的脉冲数之和 */
  circular_para.endpoint_pulse = (uint32_t)abs(stop_x - start_x) + (uint32_t)abs(stop_y - start_y);

  /* 第一象限正圆,x轴正转,y轴逆转 */
  circular_para.dir_x = CW;
  circular_para.dir_y = CCW;
  X_STEP_CW;    // X 轴顺时针方向
  Y_STEP_CCW;   // Y 轴逆时针方向

  /* 起点坐标x=0,说明起点在y轴上,直接向x轴进给可减小误差 */
  if(circular_para.startpoint_x == 0)
  {
    /* 第一步活动轴为x轴 */
    circular_para.active_axis = x_axis;
    /* 计算偏差 */
    circular_para.deviation += (2 * circular_para.startpoint_x + 1);
  }
  else
  {
    /* 第一步活动轴为Y轴 */
    circular_para.active_axis = y_axis;
    /* 计算偏差 */
    circular_para.deviation -= (2 * circular_para.startpoint_y + 1);
  }

  // 设置电机的速度
  Motor_Control_SetSpeed(speed, speed);

  if(circular_para.active_axis == x_axis)
      // 启动 X 轴电机控制
      X_Motor_Control_Start();
  else
      // 启动 Y 轴电机控制
      Y_Motor_Control_Start();

  circular_para.motionstatus = 1;
}

接下来是第一象限的逆时针圆弧插补算法,该函数首先通过检查插补状态变量,确保当前没有进行中的插补运动,并验证起点和终点是否位于同一圆上,否则直接退出。 初始化插补参数,包括清零偏差、设置起点和终点坐标,以及计算起点到终点的总脉冲数(步数之和)。设置运动方向为 X 轴逆时针(CCW)、Y 轴顺时针(CW),并通过宏定义完成方向设定。 如果起点 Y 坐标为零,优先选择 Y 轴进给,计算初始偏差以减少误差;否则优先选择 X 轴进给并计算相应偏差。 调用 Motor_Control_SetSpeed 设置电机速度,并根据当前活动轴启动相应电机。最后,将插补状态变量置为活动状态,表示插补运动已开始。

bsp_circular_interpolation.c-逆时针圆弧插补算法
/**
  * @brief  第一象限逆圆插补运动
  * @param  start_x:圆弧起点相对于圆心的坐标X,增量
  * @param  inc_y:终点坐标Y的增量
  * @param  speed:进给速度
  * @retval 无
  */
void Circular_InterPolation_CCW(int32_t start_x, int32_t start_y, int32_t stop_x, int32_t stop_y, uint32_t speed)
{
  /* 判断当前是否正在做插补运动 */
  if(circular_para.motionstatus != 0)
    return;

  /* 检查起点、终点坐标是否在同一个圆上 */
  if(((start_x * start_x) + (start_y * start_y)) != ((stop_x * stop_x) + (stop_y * stop_y)))
    return;

  /* 偏差清零 */
  circular_para.deviation = 0;

  /* 起点坐标 */
  circular_para.startpoint_x = start_x;
  circular_para.startpoint_y = start_y;
  /* 终点坐标 */
  circular_para.endpoint_x = (uint32_t)stop_x;
  circular_para.endpoint_y = (uint32_t)stop_y;
  /* 所需脉冲数是从起点到终点的脉冲数之和 */
  circular_para.endpoint_pulse = (uint32_t)abs(stop_x - start_x) + (uint32_t)abs(stop_y - start_y);

  /* 第一象限逆圆,x轴逆转,y轴正转 */
  circular_para.dir_x = CCW;
  circular_para.dir_y = CW;
  X_STEP_CCW;    // X 轴逆时针方向
  Y_STEP_CW;   // Y 轴顺时针方向

  /* 起点坐标y=0,说明起点在x轴上,直接向y轴进给可减小误差 */
  if(circular_para.startpoint_y == 0)
  {
    /* 第一步活动轴为Y轴 */
    circular_para.active_axis = y_axis;
    /* 计算偏差 */
    circular_para.deviation += (2 * circular_para.startpoint_y + 1);
  }
  else
  {
    /* 第一步活动轴为X轴 */
    circular_para.active_axis = x_axis;
    /* 计算偏差 */
    circular_para.deviation -= (2 * circular_para.startpoint_x + 1);
  }

  // 设置电机的速度
  Motor_Control_SetSpeed(speed, speed);

  if(circular_para.active_axis == x_axis)
      // 启动 X 轴电机控制
      X_Motor_Control_Start();
  else
      // 启动 Y 轴电机控制
      Y_Motor_Control_Start();

  circular_para.motionstatus = 1;
}

15.2.3.4. 定时器比较中断函数

该回调函数是两个定时器共用的函数,负责执行圆弧插补控制逻辑,按照一定的频率切换电机的活动轴,实现指定路径的步进。具体流程如下:

  • 事件触发检测 检查定时器的中断事件是否为 TIMER_EVENT_CYCLE_END,确保函数执行是由定时器比较事件触发的。

  • 保存上一次活动轴 在计算下一个活动轴之前,记录当前活动轴 active_axis,以便后续判断是否需要切换活动轴并调整对应电机的运行状态。

  • 更新当前坐标 根据上一次的活动轴( last_axis)和其进给方向( dir_xdir_y),对起点坐标 startpoint_xstartpoint_y 进行更新:

    • 如果是 X 轴:

      • 顺时针(CW): startpoint_x++

      • 逆时针(CCW): startpoint_x–

    • 如果是 Y 轴:

      • 顺时针(CW): startpoint_y++

      • 逆时针(CCW): startpoint_y–

  • 插补偏差计算 通过数字微分法计算插补偏差 deviation

    • deviation ≥ 0:当前位置偏离圆弧外侧,应选择 Y 轴进给,并更新偏差为 deviation -= (2 * startpoint_y + 1)

    • deviation < 0:当前位置偏离圆弧内侧,应选择 X 轴进给,并更新偏差为 deviation += (2 * startpoint_x + 1)

  • 判断活动轴切换 比较上一次的活动轴与当前计算出的活动轴:

    • 如果活动轴不变,则保持当前轴运行。

    • 如果活动轴切换,则停止上一次的活动轴电机,启动新的活动轴电机:

      • 当前活动轴为 X 轴:启动 X 轴电机并停止 Y 轴电机。

      • 当前活动轴为 Y 轴:启动 Y 轴电机并停止 X 轴电机。

  • 减小剩余步数 每次事件触发表示完成了一次步进操作,将总步数 endpoint_pulse 减 1,用于记录当前插补进度。

  • 检查插补完成 判断剩余步数是否为 0:

    • 如果步数为 0,表示插补运动完成:

      • 停止 X 和 Y 轴电机。

      • 重置插补参数中的运动状态 motionstatus 为 0,结束插补流程。

    • 如果步数未完成,等待下一次中断继续执行插补。

bsp_circular_interpolation.c-比较中断函数
/**
* @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 = circular_para.active_axis;

  /* 根据进给方向刷新坐标 */
  switch(last_axis)
  {
    case x_axis:
      switch(circular_para.dir_x)
      {
        case CCW: circular_para.startpoint_x--; break;
        case CW:  circular_para.startpoint_x++; break;
      }
      break;
    case y_axis:
      switch(circular_para.dir_y)
      {
        case CCW: circular_para.startpoint_y--; break;
        case CW:  circular_para.startpoint_y++; break;
      }
      break;
  }

  /* 根据上一步的偏差,判断的进给方向,并计算下一步的偏差 */
  if(circular_para.deviation >= 0)
  {
    /* 偏差>=0,在圆弧外侧,应向圆内进给,计算偏差 */
    circular_para.active_axis = y_axis;
    circular_para.deviation -= (2 * circular_para.startpoint_y + 1);
  }
  else
  {
    /* 偏差<0,在圆弧内侧,应向圆外进给,计算偏差 */
    circular_para.active_axis = x_axis;
    circular_para.deviation += (2 * circular_para.startpoint_x + 1);
  }


  /* 下一步的活动轴与上一步的不一致时,需要换轴 */
  if(last_axis != circular_para.active_axis)
  {
      // 判断活动轴是否为 X 轴
      if (circular_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 */
  circular_para.endpoint_pulse--;

  /* 判断是否完成插补 */
  if(circular_para.endpoint_pulse == 0)
  {
    /* 关闭定时器 */
      X_Motor_Control_Stop();
      Y_Motor_Control_Stop();

      // 重置插补参数中的运动状态
      circular_para.motionstatus = 0;
   }
  }
}

15.2.3.5. 主函数

主函数中主要就是一些外设的初始化,包括步进电机的定时器初始化。

  • sw2_irq_callback 中,起点坐标为 (0, SPR * 5),目标坐标为 (SPR * 5, 0),表示圆弧的半径为 SPR * 5,意味着按下按键2时,步进电机会执行第一象限的顺时针圆弧插补运动,完成从 Y 轴正方向到 X 轴正方向的路径。

  • sw3_irq_callback 中,起点坐标为 (SPR * 5, 0),目标坐标为 (0, SPR * 5),表示圆弧的半径仍为 SPR * 5,意味着按下按键3时,步进电机会执行第一象限的逆时针圆弧插补运动,完成从 X 轴正方向到 Y 轴正方向的路径。

hal_entry.c-主函数
// 按键2中断回调函数
void sw2_irq_callback(external_irq_callback_args_t *p_args)
{
    // 防止回调函数中没有使用形参的警告产生
    FSP_PARAMETER_NOT_USED(p_args);

    // 执行第一象限顺时针圆形插补运动
    Circular_InterPolation_CW(0, SPR * 5, SPR * 5, 0, 6400);
}

// 按键3中断回调函数
void sw3_irq_callback(external_irq_callback_args_t *p_args)
{
    // 防止回调函数中没有使用形参的警告产生
    FSP_PARAMETER_NOT_USED(p_args);

    // 执行第一象限逆时针圆形插补运动
    Circular_InterPolation_CCW(SPR * 5, 0, 0, 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
}

15.2.4. 下载验证

使用两轴丝杆滑台组成一个标准X-Y滑动平台,将步进电机连接好,下载程序到开发板后,按下开发板的按键,可以看到丝杆滑台上的滑块沿着程序设定的圆弧轨迹运动。

15.3. 任意象限圆弧插补原理

在前面的内容中,我们详细讲解了第一象限双向圆弧插补的原理和实现,并推导出了偏差计算公式,不过实际应用中的圆弧插补并不只有第一象限,也有可能在第二、三、四象限。 所以下面的内容将会讲解如何实现任意象限的圆弧插补。

实际上和直线插补类似,逐点比较法的圆弧插补同样可以利用第一象限插补推出所有象限和方向的插补情况。 设SR1、SR2、SR3、SR4为第一、二、三、四象限的顺时针圆弧,NR1、NR2、NR3、NR4第一、二、三、四象限的逆时针圆弧,一共8种线形。 根据他们各自的偏差判别、偏差值计算公式和XY坐标进给方向,可以归纳为2组,这里由于篇幅限制,不展开推导每种线形的偏差计算公式, 而是分别用两张图表来说明,对此感兴趣的可以自己尝试推导。

首先第一组是NR1、NR3、SR2、SR4,这一组的共同点如下:

  • 当Fi≥ 0时,X轴进给,NR1、SR4走-X方向,SR2、NR3走+X方向;

  • 当Fi< 0时,Y轴进给,NR1、SR2走+Y方向,NR3、SR4走-Y方向。

设都从各自的圆弧起点开始插补,则第一组的圆弧插补方向在一张坐标系图中可得:

NR1、NR3、SR2、SR4圆弧插补方向图

第一组的偏差计算公式与第一象限逆时针圆弧插补相同,只是X、Y坐标值使用绝对值参与计算。 将这一组的偏差计算公式和进给方向总结为一张表格,如下表所示。

NR1、NR3、SR2、SR4圆弧插补偏差公式

然后第二组是SR1、SR3、NR2、NR4,这一组的共同点如下:

  • 当Fi≥ 0时,Y轴进给,SR1、NR2走-Y方向,SR3、NR4走+Y方向;

  • 当Fi< 0时,X轴进给,SR1、NR4走+X方向,NR2、SR3走-X方向。

同样,第二组的圆弧插补方向在坐标系图中表示为:

SR1、SR3、NR2、NR4圆弧插补方向图

第二组的偏差计算公式与第二组相反,并且X、Y坐标值使用绝对值参与计算。 第二组的偏差计算公式和进给方向总结为一张表格,如下表所示。

SR1、SR3、NR2、NR4圆弧插补偏差公式

15.4. 任意象限双向圆弧插补实验

上一节讲解了逐点比较法的任意象限双向圆弧插补原理,接下来结合实验程序讲解具体实现。 学习本节内容时,请打开配套的工程配合阅读。

15.4.1. 硬件设计

本实验的硬件设计部分与直线插补实验完全相同,在此不再赘述。如有不清楚的地方,请查看第一象限直线插补实验。

15.4.2. 软件设计

15.4.2.1. 新建工程

本次任意象限圆弧插补例程是在第一象限圆弧插补例程的基础上编写的,直接在 “310_Motor_Stepper_Linear_interpolation_Any_Quadrant” 例程的基础上修改程序。

对于 e2 studio 开发环境:

拷贝一份我们之前的 e2s 工程 “310_Motor_Stepper_Linear_interpolation_Any_Quadrant”, 然后将工程文件夹重命名为 “312_Motor_Stepper_Circular_interpolation_Any_Quadrant”,最后再将它导入到我们的 e2 studio 工作空间中。

对于 Keil 开发环境:

拷贝一份我们之前的 Keil 工程 “310_Motor_Stepper_Linear_interpolation_Any_Quadrant”, 然后将工程文件夹重命名为 “312_Motor_Stepper_Circular_interpolation_Any_Quadrant”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。

15.4.2.2. 圆弧插补相关参数

本实验中的步进电机相关宏定义以及控制部分与上一实验完全相同,不再赘述,这里重点详解任意象限的插补算法部分的实现。

这段代码定义了三个枚举类型和一个结构体,用于管理步进电机的运动控制。 Axis_TypeDef 表示坐标轴(X轴和Y轴), Quadrant_TypeDef 表示坐标轴所在的象限, Direction_TypeDef 表示电机的转动方向(顺时针或逆时针)。 CircularInterpolation_TypeDef 结构体用于存储圆弧插补的相关参数,如起终点坐标、脉冲数、运动方向等,以支持复杂的电机运动控制。

bsp_circular_interpolation.h-宏定义
/* 坐标轴枚举 */
typedef enum{
  x_axis = 0U,
  y_axis
}Axis_TypeDef;

/* 坐标轴象限枚举 */
typedef enum{
  quadrant_1st = 0U,
  quadrant_2nd,
  quadrant_3rd,
  quadrant_4th
}Quadrant_TypeDef;

/* 方向枚举 */
typedef enum{
    CW = 0U,  // 正转 (顺时针 Clockwise)
    CCW       // 反转 (逆时针 Counter-Clockwise)
} Direction_TypeDef;

/* 圆弧插补参数结构体 */
typedef struct{
  __IO int32_t startpoint[2];        //起点坐标X、Y
  __IO int32_t endpoint_x;           //终点坐标X
  __IO int32_t endpoint_y;           //终点坐标Y
  __IO uint32_t endpoint_pulse;      //到达终点位置需要的脉冲数
  __IO uint32_t active_axis;         //当前运动的轴
  __IO int32_t deviation;            //偏差参数F
  __IO int8_t devi_sign[2];          //偏差方程的运算符号,正负
  __IO uint8_t motionstatus : 1;     //插补运动状态
  __IO uint8_t dir_x : 1;            //X轴运动方向
  __IO uint8_t dir_y : 1;            //Y轴运动方向
  __IO uint8_t dir_interpo : 1;      //插补整体运动方向
  __IO uint8_t crood_pos : 2;        //起点坐标所在的象限
}CircularInterpolation_TypeDef;

15.4.2.3. 方向判断函数

以下函数 Set_Feed_DIR 用于设置步进电机的进给方向,并根据给定的坐标 (coord_x, coord_y) 确定电机的运动方向和所在的象限。根据方向枚举 CW (顺时针)或 CCW (逆时针),它将控制电机的运动方向,计算运动的坐标轴所在的象限,并更新相应的运动参数。

  • 根据 coord_xcoord_y 的值,判断当前点所在的象限(第一象限、第二象限、第三象限、第四象限)。

  • 根据坐标和方向,设置 circular_para 中的运动参数,如 dir_xdir_y (X轴和Y轴的运动方向), devi_sign (偏差的符号)以及步进信号控制( X_STEP_CWY_STEP_CCW 等)。

通过这个设置,电机可以根据不同的输入坐标和运动方向执行插补运动。

bsp_circular_interpolation.c-方向判断函数
/**
  * @brief  设置进给方向
  * @param  coord_x
  * @param  coord_y
  * @retval 无
  */
static void Set_Feed_DIR(int32_t coord_x, int32_t coord_y, uint8_t dir)
{
  /* 记录插补运动方向 */
  circular_para.dir_interpo = dir;

  if(dir == CW)
  {
    if(coord_x > 0)/* x正半轴 */
    {
      if(coord_y > 0)/* 第一象限 */
      {
        circular_para.crood_pos = quadrant_1st;
        circular_para.dir_x = CW;
        circular_para.dir_y = CCW;
        circular_para.devi_sign[x_axis] = 1;
        circular_para.devi_sign[y_axis] = -1;
        X_STEP_CW;
        Y_STEP_CCW;
      }
      else/* 第四象限 */
      {
        circular_para.crood_pos = quadrant_4th;
        circular_para.dir_x = CCW;
        circular_para.dir_y = CCW;
        circular_para.devi_sign[x_axis] = -1;
        circular_para.devi_sign[y_axis] = -1;
        X_STEP_CCW;
        Y_STEP_CCW;
      }
    }
    else if(coord_x < 0)/* x负半轴 */
    {
      if(coord_y >= 0)/* 第二象限 */
      {
        circular_para.crood_pos = quadrant_2nd;
        circular_para.dir_x = CW;
        circular_para.dir_y = CW;
        circular_para.devi_sign[x_axis] = 1;
        circular_para.devi_sign[y_axis] = 1;
        X_STEP_CW;
        Y_STEP_CW;
      }
      else/* 第三象限 */
      {
        circular_para.crood_pos = quadrant_3rd;
        circular_para.dir_x = CCW;
        circular_para.dir_y = CW;
        circular_para.devi_sign[x_axis] = -1;
        circular_para.devi_sign[y_axis] = 1;
        X_STEP_CCW;
        Y_STEP_CW;
      }
    }
    else if(coord_x == 0)/* x=0,当前点在Y轴上 */
    {
      if(coord_y > 0)/* 第一象限 */
      {
        circular_para.crood_pos = quadrant_1st;
        circular_para.dir_x = CW;
        circular_para.dir_y = CCW;
        circular_para.devi_sign[x_axis] = 1;
        circular_para.devi_sign[y_axis] = -1;
        X_STEP_CW;
        Y_STEP_CCW;
      }
      else if(coord_y < 0)/* 第三象限 */
      {
        circular_para.crood_pos = quadrant_3rd;
        circular_para.dir_x = CCW;
        circular_para.dir_y = CW;
        circular_para.devi_sign[x_axis] = -1;
        circular_para.devi_sign[y_axis] = 1;
        X_STEP_CCW;
        Y_STEP_CW;
      }
    }
  }
  else
  {
    if(coord_x > 0)/* x正半轴 */
    {
      if(coord_y >= 0)/* 第一象限 */
      {
        circular_para.crood_pos = quadrant_1st;
        circular_para.dir_x = CCW;
        circular_para.dir_y = CW;
        circular_para.devi_sign[x_axis] = -1;
        circular_para.devi_sign[y_axis] = 1;
        X_STEP_CCW;
        Y_STEP_CW;
      }
      else/* 第四象限 */
      {
        circular_para.crood_pos = quadrant_4th;
        circular_para.dir_x = CW;
        circular_para.dir_y = CW;
        circular_para.devi_sign[x_axis] = 1;
        circular_para.devi_sign[y_axis] = 1;
        X_STEP_CW;
        Y_STEP_CW;
      }
    }
    else if(coord_x < 0)/* x负半轴 */
    {
      if(coord_y > 0)/* 第二象限 */
      {
        circular_para.crood_pos = quadrant_2nd;
        circular_para.dir_x = CCW;
        circular_para.dir_y = CCW;
        circular_para.devi_sign[x_axis] = -1;
        circular_para.devi_sign[y_axis] = -1;
        X_STEP_CCW;
        Y_STEP_CCW;
      }
      else/* 第三象限 */
      {
        circular_para.crood_pos = quadrant_3rd;
        circular_para.dir_x = CW;
        circular_para.dir_y = CCW;
        circular_para.devi_sign[x_axis] = 1;
        circular_para.devi_sign[y_axis] = -1;
        X_STEP_CW;
        Y_STEP_CCW;
      }
    }
    else if(coord_x == 0)/* x=0,当前点在Y轴上 */
    {
      if(coord_y > 0)/* 第二象限 */
      {
        circular_para.crood_pos = quadrant_2nd;
        circular_para.dir_x = CCW;
        circular_para.dir_y = CCW;
        circular_para.devi_sign[x_axis] = -1;
        circular_para.devi_sign[y_axis] = -1;
        X_STEP_CCW;
        Y_STEP_CCW;
      }
      else if(coord_y < 0)/* 第四象限 */
      {
        circular_para.crood_pos = quadrant_4th;
        circular_para.dir_x = CW;
        circular_para.dir_y = CW;
        circular_para.devi_sign[x_axis] = 1;
        circular_para.devi_sign[y_axis] = 1;
        X_STEP_CW;
        Y_STEP_CW;
      }
    }
  }
}

15.4.2.4. 圆弧插补算法实现

该函数 Circular_InterPolation 实现了任意象限的顺圆插补运动。根据传入的起点和终点坐标以及进给速度,函数计算圆弧插补的所需脉冲数,并根据插补方向和坐标确定 X、Y 轴的运动方向。

主要功能:

  • 检查运动状态:如果当前正在进行插补运动,函数将直接返回,不进行新的插补。

  • 验证圆弧路径:判断起点和终点是否在同一个圆上,确保插补路径的合法性。

  • 计算所需脉冲数:通过计算起点和终点的坐标差值,得到运动所需的脉冲数。

  • 确定插补方向:通过调用 Set_Feed_DIR 函数,根据坐标信息和进给方向(顺时针或逆时针),设置电机的运动方向和坐标所在象限。

  • 设置偏差:根据起点坐标的 X 或 Y 值,设置偏差参数,用于调整插补路径的精度。

  • 设置速度和启动电机控制:根据传入的速度参数设置电机的运动速度,并根据当前活动轴(X 或 Y)启动相应的电机控制。

最终,函数通过更新 circular_para.motionstatus1,标记插补运动已开始,防止运动过程中重复执行。

bsp_circular_interpolation.c-圆弧插补算法实现
/**
  * @brief  任意象限顺圆插补运动
  * @param  start_x:圆弧起点坐标X
  * @param  start_y:圆弧起点坐标Y
  * @param  stop_x:圆弧终点坐标X
  * @param  stop_y:圆弧终点坐标Y
  * @param  speed:进给速度
  * @param  dir:进给方向
  * @retval 无
  */
void Circular_InterPolation(int32_t start_x, int32_t start_y, int32_t stop_x, int32_t stop_y, uint16_t speed, uint8_t dir)
{
  /* 判断当前是否正在做插补运动 */
  if(circular_para.motionstatus != 0)
    return;

  /* 检查起点、终点坐标是否在同一个圆上 */
  if(((start_x * start_x) + (start_y * start_y)) != ((stop_x * stop_x) + (stop_y * stop_y)))
    return;

  /* 偏差清零 */
  circular_para.deviation = 0;

  /* 起点坐标 */
  circular_para.startpoint[x_axis] = start_x;
  circular_para.startpoint[y_axis] = start_y;
  /* 终点坐标 */
  circular_para.endpoint_x = (int32_t)stop_x;
  circular_para.endpoint_y = (int32_t)stop_y;
  /* 所需脉冲数是从起点到终点的脉冲数之和 */
  circular_para.endpoint_pulse = (uint32_t)abs(stop_x - start_x) + (uint32_t)abs(stop_y - start_y);

  /* 根据坐标确定插补方向和X、Y运动方向 */
  Set_Feed_DIR(circular_para.startpoint[x_axis], circular_para.startpoint[y_axis], dir);

  /* 起点坐标x=0,说明起点在y轴上,直接向x轴进给可减小误差 */
  if(circular_para.startpoint[x_axis] == 0)
  {
    /* 偏差方程:F = F ± 2 * x + 1*/
    circular_para.active_axis = x_axis;
    circular_para.deviation += 2 * circular_para.devi_sign[x_axis]
                                * circular_para.startpoint[x_axis] + 1;
  }
  else
  {
    /* 偏差方程:F = F ± 2 * y + 1*/
    circular_para.active_axis = y_axis;
    circular_para.deviation += 2 * circular_para.devi_sign[y_axis]
                                * circular_para.startpoint[y_axis] + 1;
  }

  // 设置电机的速度
  Motor_Control_SetSpeed(speed, speed);

  if(circular_para.active_axis == x_axis)
      // 启动 X 轴电机控制
      X_Motor_Control_Start();
  else
      // 启动 Y 轴电机控制
      Y_Motor_Control_Start();

  circular_para.motionstatus = 1;
}

15.4.2.5. 定时器比较中断函数

该回调函数是两个定时器共用的函数,负责执行直线插补控制逻辑,按照一定的频率切换电机的活动轴,实现指定路径的步进。 根据定时器事件,函数会依照插补算法控制 X、Y 轴的运动步骤,并决定当前活跃的轴和进给方向。

主要功能:

  • 坐标更新:根据上一次的进给活动轴和进给方向(顺时针或逆时针),更新当前的起点坐标(X 或 Y)。

  • 计算新的活动轴:根据当前偏差值和插补方向,判断下一步需要活动的轴。通过判断偏差值的正负,选择继续沿当前轴运动,还是切换到另一个轴进行插补。

  • 计算新的偏差:根据当前轴的坐标和方向,更新插补的偏差值。偏差值的计算影响下一步的运动决策。

  • 切换活动轴:如果当前活动轴与上一步的轴不一致,则切换到新的活动轴,并启动新的电机控制,同时停止不再使用的电机。

  • 进给总步数减1:每次中断触发时,减去一步进给。

  • 判断插补完成:如果进给总步数为零,表示插补运动已完成,停止所有电机并重置插补状态。

整体流程是,定时器每次触发时更新插补进度,控制电机步进,直到完成整个圆弧插补路径。

bsp_circular_interpolation.c-比较中断函数
/**
  * @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 = circular_para.active_axis;

  /* 根据进给方向刷新坐标 */
  switch(last_axis)
  {
    case x_axis:
      switch(circular_para.dir_x)
      {
          case CCW: circular_para.startpoint[x_axis]--; break;
          case CW:  circular_para.startpoint[x_axis]++; break;
        }
        break;
      case y_axis:
        switch(circular_para.dir_y)
        {
          case CCW: circular_para.startpoint[y_axis]--; break;
          case CW:  circular_para.startpoint[y_axis]++; break;
      }
      break;
  }

  /* 根据上一次进给的偏差,判断新的进给活动轴 */
  if(circular_para.deviation >= 0)
  {
    switch(circular_para.dir_interpo)
    {
      case CW:/* 双向 */
        switch(circular_para.crood_pos)
        {
          case quadrant_1st:
          case quadrant_3rd:
            circular_para.active_axis = y_axis;
            break;
          case quadrant_2nd:
          case quadrant_4th:
            circular_para.active_axis = x_axis;
            break;
        }
        break;
      case CCW:/* 逆时针 */
        switch(circular_para.crood_pos)
        {
          case quadrant_1st:
          case quadrant_3rd:
            circular_para.active_axis = x_axis;
            break;
          case quadrant_2nd:
          case quadrant_4th:
            circular_para.active_axis = y_axis;
            break;
        }
        break;
    }
  }
  else /* 偏差小于0,向圆外进给 */
  {
    switch(circular_para.dir_interpo)
    {
      case CW:/* 双向 */
        switch(circular_para.crood_pos)
        {
          case quadrant_1st:
          case quadrant_3rd:
            circular_para.active_axis = x_axis;
            break;
          case quadrant_2nd:
          case quadrant_4th:
            circular_para.active_axis = y_axis;
            break;
        }
        break;
      case CCW:/* 逆时针 */
        switch(circular_para.crood_pos)
        {
          case quadrant_1st:
          case quadrant_3rd:
            circular_para.active_axis = y_axis;
            break;
          case quadrant_2nd:
          case quadrant_4th:
            circular_para.active_axis = x_axis;
            break;
        }
        break;
    }
  }

  /* 根据插补运动方向和进给方向计算出新的偏差 */
  circular_para.deviation += 2 * circular_para.devi_sign[circular_para.active_axis]
                              * circular_para.startpoint[circular_para.active_axis] + 1;

  /* 下一步的活动轴与上一步的不一致时,需要换轴 */
  if(last_axis != circular_para.active_axis)
  {
      // 判断活动轴是否为 X 轴
      if (circular_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 */
  circular_para.endpoint_pulse--;

  /* 判断是否完成插补 */
  if(circular_para.endpoint_pulse == 0)
  {
    /* 关闭定时器 */
      X_Motor_Control_Stop();
      Y_Motor_Control_Stop();

      // 重置插补参数中的运动状态
      circular_para.motionstatus = 0;

  }
}
}

15.4.2.6. 主函数

主函数中主要就是一些外设的初始化,包括步进电机的定时器初始化。 当按下按键时,程序会根据预设的目标位置启动步进电机进行插补运动。具体的运动要求如下:

  • sw2_irq_callback 中,起点坐标为 (SPR * 5, 0),目标坐标为 (0, -SPR * 5),表示圆弧的半径为 SPR * 5,意味着按下按键2时,步进电机会执行第四象限的顺时针圆弧插补运动,完成从 X 轴正方向到 Y 轴负方向的路径。

  • sw3_irq_callback 中,起点坐标为 (0, -SPR * 5),目标坐标为 (-SPR * 5, 0),表示圆弧的半径仍为 SPR * 5,意味着按下按键3时,步进电机会执行第三象限的逆时针圆弧插补运动,完成从 Y 轴负方向到 X 轴负方向的路径。

hal_entry.c-主函数
// 按键2中断回调函数
void sw2_irq_callback(external_irq_callback_args_t *p_args)
{
    // 防止回调函数中没有使用形参的警告产生
    FSP_PARAMETER_NOT_USED(p_args);

    // 执行第四象限顺时针圆弧插补运动
    Circular_InterPolation(SPR * 5, 0, 0, -SPR * 5, 6400, CW);
    while(circular_para.motionstatus);
}

// 按键3中断回调函数
void sw3_irq_callback(external_irq_callback_args_t *p_args)
{
    // 防止回调函数中没有使用形参的警告产生
    FSP_PARAMETER_NOT_USED(p_args);

    // 执行第三象限逆时针圆弧插补运动
    Circular_InterPolation(0, -SPR * 5, -SPR * 5, 0, 6400, CCW);
    while(circular_para.motionstatus);
}

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
}

15.4.3. 下载验证

使用两轴丝杆滑台组成一个标准X-Y滑动平台,将步进电机连接好,下载程序到开发板后,按下开发板的按键,可以看到丝杆滑台上的滑块沿着程序设定的直线轨迹运动。