目录
梯形速度规划介绍与问题引出
在进行了速度前瞻之后,就可以进行速度规划了,mks-dlc32中采用时间片分割方法实现速度规划。我对其他速度规划方法知之甚少,主要谈谈开源的GRBL中采用的梯型速度规划。
首先我们要明确一个概念,在运动控制中一个脉冲,步进电机只会走一个步进,倘若步进电机细分已经确定,那么一个步进的距离是确定的。在mks-dlc32中,步进电机采用16细分,一个步进所走的距离为12.5um,即0.0125mm,80个脉冲走一毫米。
取一个较为标准的运动为例,G1 X100 Y0 F200,在这行G代码中进入速度、退出速度都为0,标称速度为200mm/s。如果80个脉冲走1mm,那么X轴运动100mm即为对驱动X轴的电机驱动芯片发送100*80个脉冲,梯形速度规划的作用就是让这8000个脉冲在时间尺度上呈现一个梯形的形状。
图一
在经过速度前瞻之后,可以将一段运动简单的分成匀加速段、匀速段、匀减速段这三种状态,其他的运动都可以看作是缺少上面三种状态某一种或两种的情况。在程序编写上可以参考状态机的思想,速度规划执行当前段时只需要考虑下一个段的状态,这样问题就会变得非常简单。
图二
以下的问题是我们编写梯形速度规划程序时必须考虑的问题:
一、怎么判断状态转换
二、速度达不到标称速度如何解决
三、路程走完未减速到退出速度
四、速度到退出速度,要走的距离未走完
五、如何处理一个段所发的脉冲为一个小数
六、如何处理最后一个段小于一个时间片的时间
如何解决状态转换
在进行了速度前瞻之后,可能会出现的运动情况。由于mks-dlc32中存在第一个状态为减速覆盖的情况,即第一个状态为匀减速,所以需要讨论状态转换会稍微复杂一些。
图三
进行了前瞻之后的运动数据块一定会符合以上其中情况的一种,不符合的会进行转化,后面会有分析。
在梯形速度规划中讨论状态转化时我们已知条件只有Vs(进入速度)、Ve(退出速度)、Vn(标称速度)、X(这一个块运动的位移量)、a(恒定的加速度)、S(已经走完的路程)。
在开始时我们需要对一个运动块进行判断,判断它属于上面其中情况中的哪一类。首先先判断它是否存在匀速运动状态,我们可以先利用公式计算出Sac(进入速度匀加速到标称的位移)、Sde(标称匀减速到退出速度的位移)。
若第一段为减速覆盖,则Sac加上绝对值即可,计算出Sac与Sde后,若X > (Sac + Sde),则存在匀速段,能达到标称速度,为图三中的情况(2)、(3)、(4)、(5)、(6);若X < (Sac + Sde),则不存在匀速段,不能达到标称速度,为图三中的剩下情况。
对于(2)、(3)、(4)、(5)、(6)只需要再利用Vs、Vn、Ve即可将所有情况分类。
Vs < Vn、Vn > Ve为情况(2);
Vs > Vn、Vn > Ve为情况(3);
Vs < Vn、Vn = Ve为情况(4);
Vs = Vn、Vn > Ve为情况(5);
Vs = Vn、Vn = Ve为情况(6)。
对于情况(1)、(7)、(8)的判断只需要再次利用X与Sac、Sde之间的关系。
X <= Sac、Vs > Ve为情况(1);
X > Sac、X > Sde为情况(7);
X < Sde、Vs < Ve为情况(8)。
在对每一个运动块进行了情况划分后,就正式进行步进段状态转化判断了。一个新的运动块开始运动规划时,所处状态只有两种情况:减速覆盖、匀加速。
在处于减速覆盖的状态时,下一个时间片的步进段的运动状态可能为减速覆盖或者匀速状态,可以用Sac与Vs、Vn间的关系综合考量。mks-dlc32中利用了Sac进行状态转换判定,若已经走完的路程S > Sac,则下一个状态为匀速状态,否则继续减速覆盖状态。
在处于匀加速状态时,下一个时间片的步进段的运动状态可能为匀加速、匀速或者匀减速状态,在分好情况后只需要用Sac与Vs、Vn间的关系综合考量即可。在情况(7)中,达不到标称速度,会利用二分法计算出一个Vm(此段能够到达的最大速度),在情况(8)中,会利用二分法求出一个Ve,然后可利用求出Vm、Ve再计算Sac。若已经走完的路程S < Sac,则下一段的运动状态仍然为匀加速;若已经走完的路程S > Sac且为情况(2)、(4)则下一段的运动状态为匀速;若已经走完的路程S > Sac且为情况(7)则下一段的运动状态为匀减速。
在处于匀速段时,下一个时间片的可能运动状态只有匀速和匀减速两种情况,只需判定剩余未走的路程S与Sde间的关系即可,若剩余未走的路程S >Sde,则下一段仍然为匀速段,否则为匀减速段。
匀减速段的次态只有匀减速,只需判断此运动块是否走完,对于运动块位移是否走完讨论稍微复杂,本文分了几个可能出现的情况讨论。
速度达不到标称速度如何解决
当路程过小时会发生速度达不到给定的进给速率,这时我们可以采取二分法,在进入速度、退出速度中的最大值与给定的进给速率之间二分,求出一个这个运动块能到达的最大速率,以这个最大速率进行运动规划。
路程走完未减速到退出速度
当路程走完还未降到退出速度时,mks-dlc32中梯形加减速采取的措施为骤降,降到退出速度,在进行完速度前瞻之后,除非执行运动赞停的指令,执行运动暂停后,必须在这个运动块末尾将速度降至0,此时就可能发生路程走完未减速到退出速度的情况,因为原本的规划退出速度不一定为0。
如何处理速度到达退出速度,要走的距离未走完
一般这种情况只会发生在减速状态,可以在减速状态加一些条件判断,如果下一段速度已经为退出速度,但路程未走完,我们可以不进行速度变化,依然以这一段的速度走下一段,直到将路程走完。
如何处理一个段所发的脉冲为一个小数
进行向下取整,将小数部分抛到下一个段执行,若此段为最后一个段的话,可以采用四舍五入,这样便能得到整数个脉冲,通过这种向下取整、四舍五入可以确保我们每个步进段都是整数个脉冲。
如何处理最后一个段小于一个时间片的时间
步进段数据存放了脉冲个数,速度(脉冲间隔),以恒定间隔发送完指定的脉冲数后,此段便算执行完毕,进入下一个步进段的执行,时间片只是一个中间数据,用于设置脉冲间隔,脉冲间隔 x 脉冲个数即为这一段的执行时间,这算时变相地处理了时间片缩短问题。
完整代码演示
下面的代码是我在理解梯形速度规划后,对于单轴运动的速度规划模块单独复现。这与mks-dlc32中的速度规划稍有差别,舍弃了减速覆盖的情况,也就是不会出现情况(3),不过大体思路一致。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
/************************全局变量*************************/
#define STEP_PER_MM 80 // 步进电机每毫米的步数
#define STEP_EQUAL_MM 0.0125 // 步进电机每步的长度,单位:mm
#define MAX_ACCELERATION 200.0 // 系统的最大加速度mm/s^2
#define DT_SEGMENT 0.01 // 每个段的最小时间片为10ms
#define DATA_FILE_NAME "./T_data.txt" // 数据文件名
FILE *DATA_FILE_HANDLE; // 文件句柄指针
static float ALLOW_PRECISION = 0.0001; // 允许的误差
enum Situation
{
ACCEL = 0,
DECEL = 1,
CONST = 2,
};
typedef struct
{
float start_velocity; // 起始速度 mm/s -> 80 * start_velocity
float end_velocity; // 结束速度 mm/s -> 80 * end_velocity
float nominal_velocity; // 标称速度 mm/s -> 80 * nominal_velocity
float acceleration; // 这一块的加速度 mm/s^2
float distance; // 总运动距离 mm
float steps; // 总运动距离需要执行的步数 个 -> distance * STEP_PER_MM
} S_Curve_Params;
S_Curve_Params block;
// 段准备数据结构。包含计算新段所需的所有信息,基于当前执行的规划块。
typedef struct
{
float dt_remainder; // 上一个段剩余的时间(用于时间计算)
float mm_remaining; // 当前段剩余的步数
uint8_t ramp_type; // 当前段的加减速状态(加速、匀速、减速)
float mm_complete; // 当前规划块结束时的已走的距离(单位:毫米)
// 注意:这个值在转换时必须是整数步数(没有小数部分)
float current_speed; // 当前段结束时的速度(单位:毫米/秒)
float maximum_speed; // 执行块的最大速度(单位:毫米/秒),不一定是标称速度
float exit_speed; // 当前块的退出速度(单位:毫米/秒)
float entry_speed; // 当前块的进入速度(单位:毫米/秒)
float accelerate_until; // 从块结束处开始的加速段的步进个数(单位:个)
float decelerate_after; // 从块结束处开始的减速段的步进个数(单位:个)
} st_prep_t;
static st_prep_t prep; // 用于存储段准备数据的结构体实例
typedef struct
{
int number; // 此个插补周期在这段G代码中的序号
int steps; // 每个插补周期的脉冲个数
float velocity; // 每个插补周期的速度 mm/s -> 80 * velocity
int isrPeriod; // 每个插补周期的中断周期,也就是电机运动速度
Situation situation; // 情况划分
} Step_segment;
Step_segment segment;
/********************************************************/
/************************函数声明*************************/
inline int Clear_document(); // 清空文档
void Value_input(float Vs, float Ve, float Vth, float S); // 将block赋值
void Segment_Divide(); // 将插补周期划分
/********************************************************/
/************************程序入口*************************/
int main()
{
if (Clear_document() == 0)
{
DATA_FILE_HANDLE = fopen(DATA_FILE_NAME, "w");
if (DATA_FILE_HANDLE == NULL)
{
cout << "FILE_OPEN_ERROR" << endl;
return -1;
}
}
//输入进入速度、出去速度、标称速度、要走的距离,运行程序获取七段S型运动规划
float Vs = 0.0, Ve = 0.0, Vth = 200.0, S = 100.0;
Value_input(Vs, Ve, Vth, S);
Segment_Divide();
return 0;
}
/********************************************************/
/************************函数定义*************************/
inline int Clear_document()
{
DATA_FILE_HANDLE = fopen(DATA_FILE_NAME, "w");
if (DATA_FILE_HANDLE == NULL)
{
cout << "FILE_OPEN_ERROR" << endl;
return -1;
}
fclose(DATA_FILE_HANDLE);
return 0;
}
void Value_input(float Vs, float Ve, float Vth, float S)
{
S_Curve_Params *p = █ // 指针指向block
p->start_velocity = Vs; // 起始速度赋值
p->end_velocity = Ve; // 结束速度赋值
p->nominal_velocity = Vth; // 标称速度赋值
p->distance = S; // 总运动距离赋值
p->steps = S * STEP_PER_MM; // 总步数赋值
p->acceleration = MAX_ACCELERATION; // 加速度赋值
p = NULL; // 销毁指针
}
void Segment_Divide()
{
//step 1:计算第一段的状态
S_Curve_Params *p = █ // 指针指向block
st_prep_t *pr = &prep; // 指针指向prep结构体
Step_segment *seg = &segment; // 指针指向segment结构体
float S = 0.5 * fabs(pow(p->end_velocity,2) - pow(p->start_velocity,2)) / p->acceleration;//初速度到末速度最小位移
float Sac = 0.5 * (pow(p->nominal_velocity,2) - pow(p->start_velocity,2)) / p->acceleration;
float Sde = 0.5 * (pow(p->nominal_velocity,2) - pow(p->end_velocity,2)) / p->acceleration;
if(p->distance <= (S + ALLOW_PRECISION)){
if(p->start_velocity > p->end_velocity){//纯减速段
pr->entry_speed = p->start_velocity;
pr->maximum_speed = p->start_velocity;
pr->exit_speed = sqrt(pow(p->start_velocity,2) - 2 * p->acceleration * p->distance);
seg->situation = DECEL;
pr->accelerate_until = pr->decelerate_after = p->distance;
}else{//纯加速段
pr->entry_speed = p->start_velocity;
pr->maximum_speed = sqrt(pow(p->end_velocity,2) - 2 * p->acceleration * p->distance);
pr->exit_speed = pr->maximum_speed;
seg->situation = ACCEL;
pr->accelerate_until = pr->decelerate_after = 0.0;
}
}else if(p->distance <=(Sac + Sde)){//三角形状况
//在标称速度与进入速度、退出速度的最大值之间二分
float Vmax = p->nominal_velocity;
float Vmin = ((p->start_velocity > p->end_velocity) ? p->start_velocity : p->end_velocity);
float dV = Vmax - Vmin;
while(1){
Sac = 0.5 * (pow(Vmax,2) - pow(p->start_velocity,2)) / p->acceleration;
Sde = 0.5 * (pow(Vmax,2) - pow(p->end_velocity,2)) / p->acceleration;
dV = Vmax - Vmin;
cout<<"Sac = "<<Sac<<endl;
cout<<"Sde = "<<Sde<<endl;
if(((p->distance - Sac - Sde) <= ALLOW_PRECISION) && ((p->distance - Sac - Sde) >= -ALLOW_PRECISION)){
pr->maximum_speed = Vmax;
break;
}else if((p->distance - Sac - Sde) > ALLOW_PRECISION){
Vmin = Vmax;
Vmax = Vmax + 0.5 * dV;
}else{
Vmax = Vmax - 0.5 * dV;
Vmin = Vmin;
}
}
pr->entry_speed = p->start_velocity;
pr->exit_speed = p->end_velocity;
seg->situation = ACCEL;
pr->accelerate_until = pr->decelerate_after = (0.5 * (pow(pr->maximum_speed,2) - pow(pr->exit_speed,2)) / p->acceleration) * STEP_PER_MM;
}else{//梯形状况
pr->entry_speed = p->start_velocity;
pr->exit_speed = p->end_velocity;
pr->maximum_speed = p->nominal_velocity;
seg->situation = ACCEL;
pr->accelerate_until = (p->distance - 0.5 * (pow(pr->maximum_speed,2) - pow(pr->entry_speed,2)) / p->acceleration) * STEP_PER_MM;
pr->decelerate_after = (0.5 * (pow(pr->maximum_speed,2) - pow(pr->exit_speed,2)) / p->acceleration) * STEP_PER_MM;
}
//step 2:开始分段
pr->current_speed = pr->entry_speed; // 当前速度
pr->mm_remaining = 0.0; // 段中剩余距离
float block_steps_remaining = round(p->distance * STEP_PER_MM); // 块中的剩余步数脉冲数
while(block_steps_remaining > ALLOW_PRECISION)//有剩余距离时需要执行循环
{
float time_var = DT_SEGMENT;//时间变化量
float speed_var = 0.0; //速度变化量
float mm_var = 0.0; //距离变化量
char buf[50] = {0};
switch(seg->situation)
{
case ACCEL:
speed_var = p->acceleration * time_var;
mm_var = pr->mm_remaining + (pr->current_speed + 0.5 * speed_var) * time_var;
//更新pr结构体
pr->mm_remaining = fmod(mm_var, STEP_EQUAL_MM);
pr->mm_complete += (mm_var - pr->mm_remaining);
if((pr->current_speed += speed_var) >= pr->maximum_speed)
{
pr->current_speed = pr->maximum_speed;
}
//更新seg结构体
seg->number++;
//判断纯加速到末尾时,脉冲是否足够
if(block_steps_remaining <= (round((mm_var - pr->mm_remaining) / STEP_EQUAL_MM)))
{
float dt = 0.0;
seg->steps = round(block_steps_remaining);
//纯加速段时剩余脉冲数不足,以上一段速度继续运行,但时间片缩小
dt = (seg->steps * STEP_EQUAL_MM) / seg->velocity;
}
else
{
seg->steps = round((mm_var - pr->mm_remaining) / STEP_EQUAL_MM);
seg->velocity = seg->steps * 100 * STEP_EQUAL_MM;
}
//将此段剩余步数更新
block_steps_remaining -= seg->steps;
//状态转换判断
if(block_steps_remaining <= pr->accelerate_until)
{
if((pr->accelerate_until - pr->decelerate_after) < ALLOW_PRECISION)//判断是否有匀速段
{
seg->situation = DECEL;
}
else
{
seg->situation = CONST;
}
}
else
{
seg->situation = ACCEL;
}
cout<<"jiasu "<<",number: "<<seg->number<<",velocity: "<<seg->velocity<<",steps:"<<seg->steps<<endl;
//将段数据写入文档
sprintf(buf, "[%d,%.2f,%d,%f]\n", seg->number, seg->velocity, seg->steps,block_steps_remaining);
fwrite(buf,strlen(buf),1,DATA_FILE_HANDLE);
break;
case CONST:
mm_var = pr->mm_remaining + pr->current_speed * time_var;
//更新pr结构体
pr->mm_remaining = fmod(mm_var, STEP_EQUAL_MM);
pr->mm_complete += (mm_var - pr->mm_remaining); //更新已完成距离
pr->current_speed = pr->maximum_speed;
//更新seg结构体
seg->number++;
seg->steps = round((mm_var - pr->mm_remaining) / STEP_EQUAL_MM);
seg->velocity = seg->steps * 100 * STEP_EQUAL_MM;
//将此段剩余步数更新
block_steps_remaining -= seg->steps;
//状态转换判断
if(block_steps_remaining <= pr->decelerate_after)
{
seg->situation = DECEL;
}
else
{
seg->situation = CONST;
}
cout<<"yunsu "<<",number: "<<seg->number<<",velocity: "<<seg->velocity<<",steps:"<<seg->steps<<endl;
//将段数据写入文档
sprintf(buf, "[%d,%.2f,%d,%f]\n", seg->number, seg->velocity, seg->steps,block_steps_remaining);
fwrite(buf,strlen(buf),1,DATA_FILE_HANDLE);
break;
case DECEL:
speed_var = p->acceleration * time_var;
//判断速度够不够减
if((pr->current_speed - speed_var) < ALLOW_PRECISION)
{
speed_var = pr->current_speed;
}
mm_var = pr->mm_remaining + (pr->current_speed - 0.5 * speed_var) * time_var;
//更新pr结构体
pr->mm_remaining = fmod(mm_var, STEP_EQUAL_MM);
pr->mm_complete += (mm_var - pr->mm_remaining);
pr->current_speed -= speed_var;
//更新seg结构体
seg->number++;
//判断剩余的路程是否足够
if(block_steps_remaining <= (round((mm_var - pr->mm_remaining) / STEP_EQUAL_MM)))
{
float dt = 0.0;
seg->steps = round(block_steps_remaining);
dt = (seg->steps * STEP_EQUAL_MM) / seg->velocity;
}
else
{
seg->steps = round((mm_var - pr->mm_remaining) / STEP_EQUAL_MM);
seg->velocity = seg->steps * 100 * STEP_EQUAL_MM;
}
//将此段剩余步数更新
block_steps_remaining -= seg->steps;
cout<<"jiansu "<<",number: "<<seg->number<<",velocity: "<<seg->velocity<<",steps:"<<seg->steps<<endl;
//将段数据写入文档
sprintf(buf, "[%d,%.2f,%d,%f]\n", seg->number, seg->velocity, seg->steps,block_steps_remaining);
fwrite(buf,strlen(buf),1,DATA_FILE_HANDLE);
break;
}
}
seg = NULL;
p = NULL;
pr = NULL;
}
/********************************************************/
python对运动速度数据进行可视化:
得到的运动数据:为上个程序运行完时生成的txt文档,保存了进行梯形速度规划时的段数据,一行即为一个步进段,包含了这段的行号、速度、要走的脉冲数、剩余脉冲数。
import matplotlib.pyplot as plt # type: ignore
##############
FILE_NAME = "T_data.txt"
##############
#定义数据
X_data = []
V_data = []
S_data = []
R_data = []
with open(FILE_NAME, "r") as f:
for line in f:
line_data = str(line)
line_data = line_data.strip()
lines = eval(line_data)
X_data.append(lines[0])
V_data.append(lines[1])
S_data.append(lines[2])
R_data.append(lines[3])
V_list = [V_data]#[V_data,S_data]
S_list = [S_data]
R_list = [R_data]
y_marker = ["o"]#['*',"o"]
# 创建画布
plt.figure(figsize=(9,6), dpi=100)
# 循环绘制每一条折线图
plt.subplot(2,2,1)
for i in range(len(V_list)):
plt.plot(X_data, V_list[i],marker=y_marker[i])
plt.subplot(2,2,2)
for i in range(len(S_list)):
plt.plot(X_data, S_list[i],marker=y_marker[i])
plt.subplot(2,2,3)
for i in range(len(R_list)):
plt.plot(X_data, R_list[i],marker=y_marker[i])
# 显示图形
plt.show()
执行上面的python程序得到的可视化运动数据图像: