}
四、多线段速度规划前瞻算法
连续执行多条线段插补的时候,为了加快轴的移动速度,执行完一条直线指令后不能停下来,然后重新
启动执行下一条直线指令。而是需要保持一定的速度去执行下一条直线插补,但是由于相邻两条直线之间
有一定的夹角,导致转弯的时候,轴的速度不能过快,还要考虑两条直线执行的最大速度限制和直线头尾
速度衔接等问题,这些问题的处理方法就是前瞻算法。
GRBL中使用了环形队列的方式存储每一条直线段的信息,这个队列的名称是
block_buffer[BLOCK_BUFFER_SIZE],这个结构体数组里存放的是线段的初速度、最大初速度限制、最大
转角速度限制、正常运行速度、加速度、线段长度信息。当G代码解析出一条线段指令或者圆弧拆分出线段
后,调用void mc_line(float *target, float feed_rate, uint8_t invert_feed_rate)函数,把当前线段的信息存入
block_buffer队列中,然后把当前线段和队列里前一条线段结合在一起,用前瞻算法修正队列里前一条线段
的最大运行速度,以便保证在前一条线段执行结束时的速度与当前线段的初速度一致。另外,根据两条线
段的夹角确定最大转角速度,用于修正前一条线段的结束速度和当前线段的初速度不能超过最大转角速
度。下面对mc_line函数进行详细分析:
target,线段移动到的最终位置,单位是mm,也就是说当前线段移动的长度是target值减去之前的所有线段
的移动长度;
feed_rate,线段的最大运行速度,梯形加减速值是提前设定好保存在eeprom里的;
invert_feed_rate,线段运行速度含义标志位,feed_rate有多种含义,这里我们只了解feed_rate的单位是
min/mm即可;
void mc_line(float *target, float feed_rate, uint8_t invert_feed_rate)
{
//如果限位使能,就检查target值是否超出了轴能到达的最远位置,如果超出了就告警限位错误,并复位系
统
if (bit_istrue(settings.flags,BITFLAG_SOFT_LIMIT_ENABLE)) { limits_soft_check(target); }
......
do {
//这个函数的功能很多,有很多地方都会调用它,这里调用的目的是判断有没有系统异常发生,比如系统
告警或复位,如果有异常,这个函数里处理异常的代码就会执行,没有就退出函数,继续运行
protocol_execute_runtime(); // Check for any run-time commands
if (sys.abort) { return; } // Bail, if system abort.
//检查block_buffer是否满,如果满就执行尝试打开线段插补执行使能开关。如果系统配置里开启了auto-
cycle功能,就可以自动开始执行线段插补,block_buffer里的线段会被系统执行插补操作而空出一些空间,
这样队列就不满了,也就退出下面的do-while循环继续执行代码
if ( plan_check_full_buffer() ) { protocol_auto_cycle_start(); } // Auto-cycle start when buffer is full.
else { break; }
} while (1);
//把当前线段的的信息添加到block_buffer队列里,这个函数里包含了前瞻算法的处理过程,在下面会详细
介绍
plan_buffer_line(target, feed_rate, invert_feed_rate);
......
}
void plan_buffer_line(float *target, float feed_rate, uint8_t invert_feed_rate)
{
......
for (idx=0; idx
target_steps[idx] = lround(target[idx]*settings.steps_per_mm[idx]);
//target表示轴从原点移动到终点的总距离,所以当前线段的移动步数需要用target减去之前所有线段移动
的总步数
block->steps[idx] = labs(target_steps[idx]-pl.position[idx]);
//获得三个轴里移动距离最远的轴移动的距离,后面DDA直线插补时会用到这个值。关于DDA插补算法的
方法后面会介绍
block->step_event_count = max(block->step_event_count, block->steps[idx]);
//根据步数换算出真实移动的距离,保存在unit_vec中,后面计算两条线段夹角时会用到
delta_mm = (target_steps[idx] - pl.position[idx])/settings.steps_per_mm[idx];
unit_vec[idx] = delta_mm;
// 这个值小于零,说明这个轴需要向与原来方向相反的方向移动
if (delta_mm < 0 ) { block->direction_bits |= get_direction_pin_mask(idx); }
//三个轴是正交的,知道了每个轴移动的距离,那么线段在空间里移动的真实距离是s*s=x*x+y*y+z*z,这个值在后面也
会用到
block->millimeters += delta_mm*delta_mm;
}
//开平方求出线段空间里移动的距离
block->millimeters = sqrt(block->millimeters);
......
for (idx=0; idxacceleration = min(block->acceleration,settings.acceleration[idx]*inverse_unit_vec_value);
//计算两条线段的夹角余弦值,夹角余弦公式cosa=(x1*x2+y1*y2+z1*z2)/(s1*s2),因为两条线段是首尾相连,那么用
两条线段的向量坐标计算出来的夹角其实是它的补角,夹角和它补角的余弦值刚好取负值即可,所以下面计算夹角余弦
的方法里多了一个负号
junction_cos_theta -= pl.previous_unit_vec[idx] * unit_vec[idx];
}
}
//这个用半角公式sin(a/2)=sqrt((1-cosa)/2)直接运算
float sin_theta_d2 = sqrt(0.5*(1.0-junction_cos_theta));
// 计算转角最大速度v,有圆弧加速度公式可知,v*v=a*r,其中a是圆弧向心加速度,这里近似值为block-
>acceleration,r是圆弧的半径。settings.junction_deviation是两条线段内切圆弧到两条线段交点的距离,这里
设为h,如下图所示,角EAD即为上面的a/2,内切圆的半径为r,那么AD的长度即为r+h,那么
sin_theta_d2=r/(r+h),已知h的值,那么r=h*sin_theta_d2/(1-sin_theta_d2),那么套入v*v=a*r即可求得v*v,即
block->max_junction_speed_sqr的值。
,
block->max_junction_speed_sqr = max( MINIMUM_JUNCTION_SPEED*MINIMUM_JUNCTION_SPEED,
(block->acceleration * settings.junction_deviation * sin_theta_d2)/(1.0-sin_theta_d2) );
......
//对没有优化过的线段进行优化
planner_recalculate();
}
static void planner_recalculate()
{
......
//如果所有的线段都已经优化过了,直接退出函数
if (block_index == block_buffer_planned) { return; }
......
//当前线段的起始速度取最大起始限制速度与末速度为零反推的最大起始速度的最小值
current->entry_speed_sqr = min( current->max_entry_speed_sqr, 2*current->acceleration*current->millimeters);
......
//这段代码的含义是从当前线段往前推,直到所有的线段都优化过退出循环,即每条线段的初速度不能超
过线段设置的最大初速度的限制
while (block_index != block_buffer_planned) { ......}
......
//这段代码的含义是从第一个没有优化过的线段往前,直到到达当前线段时退出循环,即每条线段的末速
度不能超过下一条线段的初速度,这样多条线段才能保持连续的速度运行
while (block_index != block_buffer_head) {}
}
到这里多线段速度前瞻规划已经完成了,下面分析GRBL中怎么把带有加减速的线段转化成用定时器输出脉
冲的过程。
五、线段转化成不同频率的输出脉冲
第四节里block_buffer队列里存放的就是每条线段的详细信息,根据线段的初速度、末速度、加速度和线
段距离信息,计算出这条线段运行时轴需要移动的总步数。假定把这条线段总的运行时间截取成多个微小
的时间段DT,即可求出每个DT时间段内的平均速度,同时可以求出DT时间段内轴移动的步数n,这样就可
以求出DT内每步需要的时间dt=DT/n,把dt设定为定时器定时间隔,直到中断计数次数到达n结束。由于线
段总步数选取的是线段向量坐标(x,y,z)里的最大值,所以定时器中断里不能每次都输出脉冲,而是需要用
DDA插补算法运算出每次中断哪个轴需要输出脉冲。
这里介绍一下GRBL中用到的DDA算法的实现过程,假设线段向量坐标a(x,y,z),选取x,y,z绝对值最大的作
为累加溢出值c=|max(x,y,z)|,假定累加初值b=c/2,那么三个轴输出脉冲的DDA算法如下:
m=l=k=b;
for(i=0;i=c)
{
x轴输出一个脉冲;
m-=c;
}
if(l>=c)
{
y轴输出一个脉冲;
l-=c;
}
if(k>=c)
{
z轴输出一个脉冲;
k-=c;
}
}
下面开始进行源码分析,第四节里对每个线段进行预处理之后,进入主循环里protocol_auto_cycle_start()函数
和protocol_execute_runtime()函数,我们从protocol_auto_cycle_start()函数开始分析。
//当GRBL默认配置里使能了auto_start功能,就把系统执行标志sys.execute设置为EXEC_CYCLE_START,也
就是系统可以自动执行线段输出脉冲,如果没有使能auto_start,在系统运行过程中可以用串口命令手动开启
线段输出脉冲
void protocol_auto_cycle_start() { if (sys.auto_start) { bit_true_atomic(sys.execute, EXEC_CYCLE_START); } }
void protocol_execute_runtime()
{
//省略了一些系统告警标志位的处理
......
//当系统没有使能线段输出脉冲功能时,只调用st_prep_buffer
if (rt_exec & EXEC_FEED_HOLD)
{
.....
st_prep_buffer();
......
}
if (rt_exec & EXEC_CYCLE_START)
{
......
//修改系统状态为STATE_CYCLE,这样启动定时器后下次循环就不会在进入这里重复启动定时器了
sys.state = STATE_CYCLE;
//把线段换算成定时器输出脉冲频率和脉冲个数
st_prep_buffer();
//启动定时器开始输出脉冲
st_wake_up();
......
}
......
//当定时器已经启动后,以后的循环就是不断的把线段换算成定时器输出脉冲
if (sys.state & (STATE_CYCLE | STATE_HOLD | STATE_HOMING)) { st_prep_buffer(); }
}
void st_prep_buffer()
{
//线段拆分成多个DT时间片,每个时间片轴运行的总步数和每步需要的时间存放在segment_buffer队里
中,while循环判断这个队列是否满,如果满了就退出循环,没满继续把线段拆分的时间片存入队列
while (segment_buffer_tail != segment_next_head) {
//判断当前线段拆分时间片是否完成,如果没有完成,pl_block不为空,if里的语句不会被执行。如果
pl_block为空,说明当前线段时间片拆分完成,执行if里的语句,开始把下一条线段的信息读出来进行时间片
拆分
if (pl_block == NULL) {
//从block_buffer队列中获取一条新的线段
pl_block = plan_get_current_block();
......
//开辟新的队列st_block_buffer存放新线段拆分时间片计算过程数据
st_prep_block = &st_block_buffer[prep.st_block_index];
//记录线段每个轴的运行方向
st_prep_block->direction_bits = pl_block->direction_bits;
//AMASS功能用于平滑脉冲频率太慢的线段,如果AMASS功能使能,线段步数放大
MAX_AMASS_LEVEL倍,但是定时器定时间隔将会缩短,相当于定时器中断加快了,更多次的中断累积才
输出一个脉冲,这样输出脉冲变得更平滑了
#ifndef ADAPTIVE_MULTI_AXIS_STEP_SMOOTHING
st_prep_block->steps[X_AXIS] = pl_block->steps[X_AXIS];
st_prep_block->steps[Y_AXIS] = pl_block->steps[Y_AXIS];
st_prep_block->steps[Z_AXIS] = pl_block->steps[Z_AXIS];
st_prep_block->step_event_count = pl_block->step_event_count;
#else
st_prep_block->steps[X_AXIS] = pl_block->steps[X_AXIS] << MAX_AMASS_LEVEL;
st_prep_block->steps[Y_AXIS] = pl_block->steps[Y_AXIS] << MAX_AMASS_LEVEL;
st_prep_block->steps[Z_AXIS] = pl_block->steps[Z_AXIS] << MAX_AMASS_LEVEL;
st_prep_block->step_event_count = pl_block->step_event_count << MAX_AMASS_LEVEL;
#endif
//inv_2_accel 是加速度a的过程值,inv_2_accel=1/2a
float inv_2_accel = 0.5/pl_block->acceleration;
if (sys.state == STATE_HOLD) {
//STATE_HOLD就是线段没有开始输出脉冲,这样线段的末速度为零,整个线段都是减速过程,
后面分析了线段输出脉冲的过程,再回头看这里就很简单了
......
}
else