算法理论

有三条曲线分别是红色、青色和蓝色,其中红色速度曲线、蓝色加速度曲线,青色为梯形加减速模型的加速部分曲线。

图中是梯形加速度部分(青色曲线)和 S 形加速部分(红色曲线)比较,梯形加减速是按照一个固定的斜率增加速度到达 Vt, 到达 Vt 后加速部分结束,开始进入匀速部分,梯形加减速由匀加速上升的趋势突然变成匀速,由于惯性会产生较大的冲击力和噪声;S 形加减速则很好的避免了这一问题,在加速到 Vt 后进入匀速阶段曲线上非常的顺滑,阶段衔接的相对完美。

基础公式推导

  • 其中 k 是图中加速度的斜率,t 为时间变化,所以有

  • 对加速度积分就可以得到速度,所以

  • 加速度 a 随着时间变化到最大值时速度 V=Vm, 并且初速度 V0=0,将这两个条件带入公式 4,可求得 斜率 k

  • 对速度积分就可以得到位移

其中:

  • A.对速度进行积分;
  • B.将 速度 v 的表达式带入;
  • C.求解速度积分的计算结果,并得到位移与时间的公式,即 公式 6;

根据 公式六可以很清楚的看到只要给定一个合适的 t 值,比如 1 秒内加速 1000 次,也就是t1=1/1000 那就可以得到一个比较平滑的速度曲线。

速度推导

步进电机的速度是由定时器脉冲的频率决定的,频率越快也就是步进电机的速度越快,所以可以得出 脉冲输出的频率 = 速度

计算出第一步的周期时间就可以计算出下一步的速度;并且根据时间的变化每计算一次就会累加一次时间变化量 Sumt

  • T 代表但脉冲周期
  • f 代表频率
  • speed 代表速度

每一步的速度都是在初始速度的基础上计算的,所以说 公式 4 计算的是当 初始速度为 0 的增量,具体公式如下:

  • t 时刻的速度等于初始速度与增量速度的和;
  • 求解增量速度,并且将累计时间变量带入 公式 4
  • 带入求解 t 时刻的速度;

软件设计

编程要点

  • (1) 通用 GPIO 配置
  • (2) 步进电机、定时器中断初始化
  • (3) 计算 S 加减速的速度
  • (4) 在定时器中对状态进行决策
  • (5) 通过对步进电机的初始速度、末速度和加速时间的设置来决定步进电机的运动

计算加减速速度表

/*

在这里采用的是数学匀变速的数学模型,但是速度也并

不是匀速变化的,所以准确的说是加速度的匀变数学模型。

a-t 曲线如下 (V-t曲线 请看 文档)

    | 
 a|   /|\             
    |  / | \
    | /  |  \            
    |/___|___\_t         
    0   t/2

以上就是加速度的匀变模型(默认初始的加速度为0)

那么有了这个模型就很容易可以算出一些不为人知的秘密!!!

首先有了以上的模型就可以算出每一时刻的加速度,所以先看 

0~t/2 这段 a=K*T ,其中K为加速度a的斜率

将加速度积分就是速度,所以 V = a dt = K*T dt

得: V = K*T^2/2

将速度积分就是位移,所以 S = V dt = K*T^2/2 dt 

得: S = K*T^3/6

*/

Speed_s Speed ;
uint8_t print_flag=0;


/**
  * @brief  计算加速表
  * @param  Vo  初始速度
  * @param  Vt    t时刻的速度
  * @param  T    加速完成时间
  *    @note         无
  * @retval 无
  */

void CalculateSpeedTab(int Vo, int Vt, float T)
{
    int32_t i = 0;

    int32_t Vm =0;      // 中间点速度
    float K = 0;           // 加加速度 加速度的斜率
    float Tn = 0;         // Tn时刻
    float DeltaV = 0;     // 每一时刻的速度
    float TimeDel = 0;    // 时间间隔

    Speed.Vo = CONVER(Vo);    // 起速:Step/s
    Speed.Vt = CONVER(Vt);    // 末速:Step/s

    T = T / 2;                        //加加速段的时间(加速度斜率>0的时间)

    Vm = (Speed.Vo + Speed.Vt) / 2;    //计算中点的速度

    K = ( ( 2 * ((Vm) - (Speed.Vo)) ) / pow((T),2) );// 根据中点速度计算加加速度

    Speed.AccelHalfStep  = (int)( ( (K) * pow( (T) ,3) ) / 6 );// 加加速需要的步数

    /* 申请内存空间存放速度表 */
    Speed.AccelHalfStep  = abs(Speed.AccelHalfStep ); // 减速计算的时候防止出现负数
    if( Speed.AccelHalfStep  % 2 != 0)     // 由于浮点型数据转换成整形数据带来了误差,所以这里加1
    {
        Speed.AccelHalfStep  += 1;
    }
    Speed.AccelTotalStep = Speed.AccelHalfStep * 2;           // 加速段的步数

    if(FORM_LEN<Speed.AccelTotalStep)
    {
        printf("FORM_LEN 缓存长度不足\r\n,请将 FORM_LEN 修改为 %d \r\n",Speed.AccelTotalStep);
        return ;
    }

    /* 目标的速度曲线是对时间的方程,将时间与步数对应,所以在此将时间分成
        与步数对应的份数,并且计算出 ,这样就可以计算出相应的速度*/
    TimeDel = T / Speed.AccelHalfStep;

    for(i = 0; i <= Speed.AccelHalfStep; i++)
    {
        Tn = i * TimeDel;                        // 第i个时刻的Tn
        DeltaV = 0.5f * K * pow(Tn,2);            // 在Tn时刻所对应的速度  dv = 1/2 * K * t^2;
        Speed.Form[i] = Speed.Vo + DeltaV;        // 得到每一时刻对应的速度,由于考虑到初速度不为0的情况,所以与Vo相加求和

        Speed.Form [ Speed.AccelTotalStep - i] = Speed.Vt - DeltaV ;        // 加加速过程与减加速是中心对称,可以直接求出后半段速度
                                                                            // 减加速过程对称点的速度
    }
    /* 输出速度表内容 */
    for(i = 0; i <= Speed.AccelTotalStep ; i++)
    {
        printf("i,%.3f;speed,%.3f\n",(float)i,Speed.Form[i]);    
    }
    /* 清空表 */
    memset(Speed.Form,0,FORM_LEN*sizeof(float));
}
/**
  * @brief  初始化状态并且设置第一步的速度
  * @param  无
    * @param  无
    *    @note         无
  * @retval 无
  */
void stepper_start_run()
{

    /*初始化结构体*/
    memset(&Stepper,0,sizeof(Stepper_Typedef));
    /*初始电机状态*/
    Stepper.status=ACCEL;
    /*初始电机位置*/
    Stepper.pos=0;

    /*计算第一次脉冲间隔*/
  if(Speed.Form[0] == 0)    //排除分母为0的情况
    Stepper.pluse_time = 0xFFFF;
  else                                        //分母不为0的情况
    Stepper.pluse_time  = (uint32_t)(T1_FREQ/Speed.Form[0]);

    /*获取当前计数值*/
    uint32_t temp=__HAL_TIM_GET_COUNTER(&TIM_TimeBaseStructure);
    /*在当前计数值基础上设置定时器比较值*/
    __HAL_TIM_SET_COMPARE(&TIM_TimeBaseStructure,MOTOR_PUL_CHANNEL_x,temp +Stepper.pluse_time); 
    /*开启中断输出比较*/
    HAL_TIM_OC_Start_IT(&TIM_TimeBaseStructure,MOTOR_PUL_CHANNEL_x);
    /*使能定时器通道*/
    TIM_CCxChannelCmd(MOTOR_PUL_TIM, MOTOR_PUL_CHANNEL_x, TIM_CCx_ENABLE);
}
  • 1.初始化 Stepper 结构体,初始化电机状态以及电机位置;
  • 2.计算第一次的脉冲时间间隔,也就是利用 定时器分频后的主频与Speed.Form[0] 做比值,但是这里需要考虑分母为 0 的情况如果分母为 0,那么直接将脉冲时间赋值为 0xffff;
  • 3.获取当前定时器的计数值并且与脉冲时间累加,然后作为参数传入__HAL_TIM_SET_COMPARE 设置下一次进入中断的时间。并开启中断比较输出。

计算加减速速度表

/*

在这里采用的是数学匀变速的数学模型,但是速度也并

不是匀速变化的,所以准确的说是加速度的匀变数学模型。

a-t 曲线如下 (V-t曲线 请看 文档)

    |    .
 a|   /|\             
    |  / | \
    | /  |  \            
    |/___|___\_t         
    0   t/2

以上就是加速度的匀变模型(默认初始的加速度为0)

那么有了这个模型就很容易可以算出一些不为人知的秘密!!!

首先有了以上的模型就可以算出每一时刻的加速度,所以先看 

0~t/2 这段 a=K*T ,其中K为加速度a的斜率

将加速度积分就是速度,所以 V = a dt = K*T dt

得: V = K*T^2/2

将速度积分就是位移,所以 S = V dt = K*T^2/2 dt 

得: S = K*T^3/6

*/
/*算法相关结构体变量定义*/
SpeedCalc_TypeDef Speed ;
Stepper_Typedef Stepper;

uint8_t print_flag=0;

void CalcSpeed(int32_t Vo, int32_t Vt, float T)
{

  uint8_t Is_Dec = FALSE;     
  int32_t i = 0;
  int32_t Vm =0;              // 中间点速度
  float K = 0;             // 加加速度
  float Ti = 0;               // 时间间隔 dt
  float Sumt = 0;             // 时间累加量
  float DeltaV = 0;           // 速度的增量dv  

    /***************************************************************************/
    /*判断初速度和末速度的关系,来决定加减速*/
  if(Vo > Vt )
    {                               
    Is_Dec = TRUE;
    Speed.Vo = CONVER(Vt);  
    Speed.Vt = CONVER(Vo); 
  }
  else
  {
    Is_Dec = FALSE;
    Speed.Vo = CONVER(Vo);    
    Speed.Vt = CONVER(Vt);    
  }
    /***************************************************************************/
    /*计算初始参数*/
    T = T / 2;                        //加加速段的时间(加速度斜率>0的时间)

  Vm = (Speed.Vo + Speed.Vt) / 2;    //计算中点的速度

  K = fabs(( 2 * ((Vm) - (Speed.Vo)) ) / pow((T),2));// 根据中点速度计算加加速度

  Speed.INC_AccelTotalStep = (int32_t)( ( (K) * pow( (T) ,3) ) / 6 );// 加加速需要的步数

  Speed.Dec_AccelTotalStep = (int32_t)(Speed.Vt * T - Speed.INC_AccelTotalStep);   // 减加速需要的步数 S = Vt * Time - S1

    /***************************************************************************/
    /*计算共需要的步数,并校检内存大小,申请内存空间存放速度表*/
  Speed.AccelTotalStep = Speed.Dec_AccelTotalStep + Speed.INC_AccelTotalStep;              // 加速需要的步数 
  if( Speed.AccelTotalStep  % 2 != 0)     // 由于浮点型数据转换成整形数据带来了误差,所以这里加1
    Speed.AccelTotalStep  += 1;

    /*判断内存长度*/
    if(FORM_LEN<Speed.AccelTotalStep)
    {
        printf("FORM_LEN 缓存长度不足\r\n,请将 FORM_LEN 修改为 %d \r\n",Speed.AccelTotalStep);
        return ;
    }

    /***************************************************************************/
    /* 计算第一步的时间 */

    /*根据第一步的时间计算,第一步的速度和脉冲时间间隔*/
    /*根据位移为0的时候的情况,计算时间的关系式 ->  根据位移和时间的公式S = 1/2 * K * Ti^3  可得 Ti=6 * 1 / K开1/3次方 */
  Ti = pow((6.0f * 1.0f / K),(1 / 3.0f) ); //开方求解 Ti 时间常数
  Sumt += Ti;//累计时间常数
    /*根据V=1/2*K*T^2,可以计算第一步的速度*/
  DeltaV = 0.5f * K * pow(Sumt,2);
    /*在初始速度的基础上增加速度*/
  Speed.Form[0] = Speed.Vo + DeltaV;

    /***************************************************************************/
    /*最小速度限幅*/
  if( Speed.Form[0] <= MIN_SPEED )//以当前定时器频率所能达到的最低速度
    Speed.Form[0] = MIN_SPEED;

  /***************************************************************************/
    /*计算S形速度表*/
  for(i = 1; i < Speed.AccelTotalStep; i++)
  {

        /*根据时间周期与频率成反比的关系,可以计算出Ti,在这里每次计算上一步时间,用于积累到当前时间*/
        Ti = 1.0f / Speed.Form[i-1];   
    /* 加加速度计算 */
    if( i < Speed.INC_AccelTotalStep)
    {
            /*累积时间*/
      Sumt += Ti;
            /*速度的变化量 dV = 1/2 * K * Ti^2 */
      DeltaV = 0.5f * K * pow(Sumt,2);
            /*根据初始速度和变化量求得速度表*/
      Speed.Form[i] = Speed.Vo + DeltaV;  
            /*为了保证在最后一步可以使得时间严谨的与预期计算的时间一致,在最后一步进行处理*/
      if(i == Speed.INC_AccelTotalStep - 1)
        Sumt  = fabs(Sumt - T );
    }
    /* 减加速度计算 */
    else
    {
            /*时间累积*/
      Sumt += Ti;                                       
            /*计算速度*/
      DeltaV = 0.5f * K * pow(fabs( T - Sumt),2); 
      Speed.Form[i] = Speed.Vt - DeltaV;          
    }
  }
    /***************************************************************************/
    /*减速运动,倒序排列*/
  if(Is_Dec == TRUE)
  {
    float tmp_Speed = 0;  
    /* 倒序排序 */
    for(i = 0; i< (Speed.AccelTotalStep / 2); i++)
    {
      tmp_Speed = Speed.Form[i];
      Speed.Form[i] = Speed.Form[Speed.AccelTotalStep-1 - i];
      Speed.Form[Speed.AccelTotalStep-1 - i] = tmp_Speed;
    }
  }
}
  • 第一: 根据传入的参数判断,是加速还是减速;如果初始速度小于末速度那么就是加速运动,如果初始速度大于末速度那么就是减速度运动,并将状态变量 Is_Dec 对应修改。
  • 第二: 计算初始算法相关的基础公式,其中 T 计算的是斜率大于 0 的时间;Vm是根据初末速度计算的中点速度;K 计算的是斜率;Speed.INC_AccelTotalStepSpeed.Dec_AccelTotalStep 计算的分别是加加速度时的所需步数和总步数;具体的公式推导过程可以参考 算法理论实现的基础公式推到部分。
  • 第三: 根据加加速运动和减加速运动可以计算出一共所需的步数,并判断数组是否可以装下这些数据,如果不可以,提示修改。
  • 第四: 计算 Speed.Form[0] :根据公式 6 可直接推导出时间 Ti 的数值,并且累计时间常数带入到 v-t 的关系式中,求得 △t 的数值。并根据初始量与变化量的关系即可求出Speed.Form[0]
  • 第五: 判断 Speed.Form[0] 最小速度的数值大小,不能小于 定时器的频率与最大计数值的比值
  • 第六: 根据前面的总步数和算出每一步的 Speed.Form[i] 数值
  • 第七: 由于中心对称的关系,如果为减速运动只需将之前计算的数值倒序排列即可。

速度决策

/**
  * @brief  速度决策
    *    @note     在中断中使用,每进一次中断,决策一次
  * @retval 无
  */
void speed_decision(void)
{
    /*计数临时变量*/
  float temp_p = 0;
    /*脉冲计数*/
  static uint8_t i = 0;      

    if(__HAL_TIM_GET_IT_SOURCE(&TIM_TimeBaseStructure, MOTOR_TIM_IT_CCx) !=RESET)
    {
        /*清除定时器中断*/
        __HAL_TIM_CLEAR_IT(&TIM_TimeBaseStructure, MOTOR_TIM_IT_CCx);

        /******************************************************************/
        /*两次为一个脉冲周期*/
        i++; 
    if(i == 2)
    {
            /*脉冲周期完整后清零*/
      i = 0;   
            /*判断当前的状态,*/
      if(Stepper.status == ACCEL || Stepper.status == DECEL)
      {
                /*步数位置索引递增*/
        Stepper.pos++;
        if(Stepper.pos  < Speed.AccelTotalStep )
        { 
                    /*获取每一步的定时器计数值*/
          temp_p = T1_FREQ / Speed.Form[Stepper.pos];
          if((temp_p / 2) >= 0xFFFF)
            temp_p = 0xFFFF;
          Stepper.pluse_time = (uint16_t) (temp_p / 2);
        }
        else
        {
                    /*加速部分结束后接下来就是匀速状态或者停止状态*/
          if(Stepper.status == ACCEL)   
                    {
                      Stepper.status = AVESPEED;
                    }          
          else
          {
                        /*停止状态,清空速度表并且关闭通道*/
            Stepper.status = STOP; 
                        memset(Speed.Form,0,sizeof(float)*FORM_LEN);
            TIM_CCxChannelCmd(MOTOR_PUL_TIM, MOTOR_PUL_CHANNEL_x, TIM_CCx_DISABLE);// 使能定时器通道 

          }
        }
      }
    }
        /**********************************************************************/
        // 获取当前计数器数值
        uint32_t tim_count=__HAL_TIM_GET_COUNTER(&TIM_TimeBaseStructure);
        /*计算下一次时间*/
        uint32_t tmp = tim_count+Stepper.pluse_time;
        /*设置比较值*/
        __HAL_TIM_SET_COMPARE(&TIM_TimeBaseStructure,MOTOR_PUL_CHANNEL_x,tmp);

    }
}
  • 定义了脉冲计数以及临时变量;
  • 判断是否有新的中断到来,如果有便清楚定时器的中断标志;
  • 进入两次中断才能获得一个完整的脉冲周期,所以在这里进行一个脉冲偶数化清零;
  • 在 Speed.Form[] 表里按照顺序获取数值,并与定时器分频后的时钟主频做比值求得脉冲时间;
  • 加速结束后共有两个状态分别是匀速状态和停止状态,匀速状态即保持当前速度不变继续运行,停止状态即清空速度表并关闭通道;
  • 获取当前定时器的计数值并且与上文计算的脉冲时间累加,然后作为参数传入 __HAL_TIM_SET_COMPARE 设置下一次进入中断的时间。