GRBL源码详解步进电机的算法学习笔记(STM32)一篇带你学完GRBL算法初版

慈云数据 2024-03-15 技术支持 78 0

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

GRBL源码中步进电机的算法学习笔记(STM32)


前言

GRBL源码中算法部分的学习是我在公司研发激光切割机(三轴)期间研究的一套关于步进电机驱动控制的常见算法,以下内容都以激光切割机来举例,话不多说直接上干货。(以下内容皆为个人理解,如有错误可以在评论区揪出,希望大家一同讨论,共同进步)


一、简看GRBL的工作流程

对于一台激光切割机来说,当你想要雕刻一个圆

第一步:(将指令或图片G代码通过串口发送给MCU)

首先从上位机LaserGRBL/lightburn软件解析想要雕刻的圆,然后上位机将解析后的数据通过串口发送给MCU,MCU判断串口收到的数据是G代码还是“$”指令(该部分对应的函数为: protocol_execute_line)。

在本次举例中,圆会被解析成G代码,所以MCU通过源码中的Gcode解释器(Gcode解释器在源码中对应的函数为:gc_execute_line),但如果是指令则通过源码system_execute_line这个函数进行处理

第二部:(将接受到的G代码进行分析,从而调用对应的算法函数进行处理)

注:本文暂时不讨论指令如何处理和G代码如何分析只讨论算法

当Gcode解释器解释到数据是圆的时候,将会先计算圆心的坐标,然后再调用mc_arc函数将圆拆分成一段段小的线段(算法部分也就是从mc_arc这个函数开始)

{

算法部分

1、mc_arc() —> 将圆拆分成小线段,并计算线段的起点与终点的坐标

2、mc_line() —> 将线段与线段之间的切换速度进行计算

3、planner_recalculate() —> 基于切换速度和前瞻算法,设定线段的最佳起始速度和结束速度

4、st_prep_buffer()—> 梯形算法,将速度换算成每一步需要的时间,最后将时间数据通过定时器,改变输出频率作用到步进电机

}

二、算法部分(所有算法解释内容目前只放在代码块中体现)

1.mc_arc

代码如下(示例):

  void mc_arc(float *position, float *target, float *offset, float radius, float feed_rate,
    uint8_t invert_feed_rate, uint8_t axis_0, uint8_t axis_1, uint8_t axis_linear, uint8_t is_clockwise_arc)//计算圆弧被拆分后,每一个坐标的位置并传送给mc_line()
#endif
{
	float center_axis0 = position[axis_0] + offset[axis_0];//position为当前位置坐标,offset为圆心相对于当前位置的偏移量
	float center_axis1 = position[axis_1] + offset[axis_1];
	float r_axis0 = -offset[axis_0]; //假设圆心为0时,当前位置的坐标 // Radius vector from center to current location
	float r_axis1 = -offset[axis_1];
	float rt_axis0 = target[axis_0] - center_axis0;//假设圆心为0时,终点位置的坐标
	float rt_axis1 = target[axis_1] - center_axis1;
	uint16_t segments;
	float theta_per_segment;
	float linear_per_segment;
	float cos_T,sin_T;
	float sin_Ti;
	float cos_Ti;
	float r_axisi;
	uint16_t i;
	uint8_t count = 0;
  
	// CCW angle between position and target from circle center. Only one atan2() trig computation required.
	// 计算圆弧的终点向量∠B - 起始向量∠A的夹角对应的夹角弧度数,参考函数arctan(y/x) = ∠A  和 tan(∠B - ∠A)= tan∠B - tan∠A / 1 + tan∠A * tan∠B的算法
	//angular_travel = arctan(∠B - ∠A) = (Yb/Xb - Ya/Xa) / (1 + Ya/Xa * Yb/Xb) -- 上下同时 *XaXb
	//angular_travel = (Yb*Xa - Ya*Xb) / (Xa*Xb + Ya*Yb)
	//夹角弧度数 = 同心角大小映射到pi上
	float angular_travel = atan2(r_axis0*rt_axis1-r_axis1*rt_axis0, r_axis0*rt_axis0+r_axis1*rt_axis1);//atan2的返回值是弧度
if (is_clockwise_arc)//顺时针 //判断是顺时针还是逆时针,①终点向量与x轴的弧度制减去起始向量与x轴的弧度制的差为angular_travel
	{ 
		//由①可知,如果圆弧顺时针移动,角度应该是负值,如果计算出的角度为正值,需要在计算出的角度基础上减去2*pi(pi为圆周率)
		if (angular_travel >= -ARC_ANGULAR_TRAVEL_EPSILON) { angular_travel -= 2*M_PI; }
	} 
	else//逆时针
	{
		//由①可知,如果圆弧逆时针移动,角度应该是正值,如果计算出的角度为负值,需要在计算出的角度基础上加上2*pi(pi为圆周率)
		if (angular_travel  angular_travel += 2*M_PI; }
	}
	// NOTE: Segment end points are on the arc, which can lead to the arc diameter being smaller by up to
	// (2x) settings.arc_tolerance. For 99% of users, this is just fine. If a different arc segment fit
	// is desired, i.e. least-squares, midpoint on arc, just change the mm_per_arc_segment calculation.
	// For the intended uses of Grbl, this value shouldn't exceed 2000 for the strictest of cases.
	//settings.arc_tolerance这个参数是用于计算每一个线段的长度。arc_tolerance越小分出来的线段越多,插补越精确
	//segments = (弧长) / (每一小段的长度);判断可以分成多少份。//本行的详细解释在代码块下方的②处
	segments = floor(fabs(0.5*angular_travel*radius)/
						  sqrt(settings.arc_tolerance*(2*radius - settings.arc_tolerance)) );
	if (segments) { 
		// Multiply inverse feed_rate to compensate for the fact that this movement is approximated
		// by a number of discrete segments. The inverse feed_rate should be correct for the sum of 
		// all segments.
		if (invert_feed_rate) { feed_rate *= segments; }
		theta_per_segment = angular_travel/segments;//细分后,每一段对应的弧度制的大小
		linear_per_segment = (target[axis_linear] - position[axis_linear])/segments; //axis_linear,除了圆弧平面之外的第三个轴,即与圆弧平面垂直的轴
		// 三角函数的泰勒级数Computes: cos(theta_per_segment) = 1 - theta_per_segment^2/2, \
										 sin(theta_per_segment) = theta_per_segment - theta_per_segment^3/6)
		cos_T = 2.0 - theta_per_segment*theta_per_segment;//每份弧度数对应的2*cos pi/x
		sin_T = theta_per_segment*0.16666667*(cos_T + 4.0);//每份弧度数对应的sin pi/x
		cos_T *= 0.5;//每份弧度数对应的cosθ
		//cosθ = 1 - θ^2 / 2
		//sinθ = θ - θ^3 / 6 
		for (i = 1; i // Increment (segments-1).
			//下方为计算旋转 ②单位角弧度 后的终点坐标原理:
				//r_axis0、r_axis1分别是圆点到起始坐标的x、y轴的偏移量
			//所以 设“偏移前”的起始坐标为(r_axis0,r_axis1),且假设C为当前角度,T为②单位弧度,半径为r那么
			//r_axis0 = r * cosC
			//r_axis1 = r * sinC
			//那么 设“偏移T°后”的起始坐标为(r_axis0',r_axis1')
			//r_axis0' = r * cos(C + T) = r*cosC*cosT - r*sinC*sinT = r_axis0*cosT - r_axis1*sinT
			//r_axis1' = r * sin(C + T) = r*sinC*cosT + r*cosC*sinT = r_axis1*cosT + r_axis0*sinT = r_axis0*sin_T + r_axis1*cos_T
			if (count millimeters = 0;//当前节点的剩余执行距离
  block->direction_bits = 0;//轴运动方向
  block->acceleration = SOME_LARGE_VALUE;//稍后缩小至最大加速度 // Scaled down to maximum acceleration later
  #ifdef USE_LINE_NUMBERS
    block->line_number = line_number;
  #endif
  // Compute and store initial move distance data.
  // TODO: After this for-loop, we don't touch the stepper algorithm data. Might be a good idea
  // to try to keep these types of things completely separate from the planner for portability.
  #ifdef COREX//settings.steps_per_mm每毫米行走的步数,即x step/mm
    target_steps[A_MOTOR] = (target[A_MOTOR]*settings.steps_per_mm[A_MOTOR])>0?(int32_t)(target[A_MOTOR]*settings.steps_per_mm[A_MOTOR]+0.5):(int32_t)(target[A_MOTOR]*settings.steps_per_mm[A_MOTOR]-0.5);
	//x轴电机要运行的步数,并四舍五入
    target_steps[B_MOTOR] = (target[B_MOTOR]*settings.steps_per_mm[B_MOTOR])>0?(int32_t)(target[B_MOTOR]*settings.steps_per_mm[B_MOTOR]+0.5):(int32_t)(target[B_MOTOR]*settings.steps_per_mm[B_MOTOR]-0.5);
	//y轴电机要运行的步数,并四舍五入
    block->steps[A_MOTOR] = labs((target_steps[X_AXIS]-pl.position[X_AXIS]) + (target_steps[Y_AXIS]-pl.position[Y_AXIS]));
    block->steps[B_MOTOR] = labs((target_steps[X_AXIS]-pl.position[X_AXIS]) - (target_steps[Y_AXIS]-pl.position[Y_AXIS]));
  #endif
//N_AXIS即xyz三轴
  for (idx=0; idx//①
	  //target,为激光头期望移动到的坐标,坐标的单位为mm
	  //feed_rate,线段的最大运行速度
	  //steps_per_mm值,也就是每毫米代表的轴移动步数per/mm
    target_steps[idx] = (target[idx]*settings.steps_per_mm[idx])  0 ? (int32_t)(target[idx]*settings.steps_per_mm[idx]+0.5) : (int32_t)(target[idx]*settings.steps_per_mm[idx]-0.5);
	  //电机从原点到目标点的各轴要执行的步数
	  
    block->steps[idx] = labs(target_steps[idx] - pl.position[idx]);//各个轴从当前坐标移动到期望坐标的步数
    block->step_event_count = max(block->step_event_count, block->steps[idx]);//各个轴需要的移动步数中,取最高的放置在参数block->step_event_count中
    delta_mm = (target_steps[idx] - pl.position[idx])/settings.steps_per_mm[idx];//从当前坐标移动到期望坐标所需要移动的距离(单位: mm)
   	unit_vec[idx] = delta_mm; // Store unit vector numerator. Denominator computed later.
        
    // Set direction bits. Bit enabled always means direction is negative.
    if (delta_mm direction_bits |= get_direction_pin_mask(idx);
	}
    
    // Incrementally compute total move distance by Euclidean norm. First add square of each term.
    block->millimeters += delta_mm*delta_mm;//每个轴平方的累加
  }//①
  
  
  block->millimeters = sqrt(block->millimeters); // Complete millimeters calculation with sqrt()
  //将x、y轴要运行的距离的平方和再开根号,即三角行的第三边计算公式
  
  // Bail if this is a zero-length block. Highly unlikely to occur.
  if (block->step_event_count == 0) { return; } //如果最大要执行的步数为0,那么就直接退出函数
  
  // Adjust feed_rate value to mm/min depending on type of rate input (normal, inverse time, or rapids)
  // TODO: Need to distinguish a rapids vs feed move for overrides. Some flag of some sort.
  if (feed_rate millimeters; }//暂无
  if (feed_rate millimeters;  // 取第三边的倒数Inverse millimeters to remove multiple float divides	
  junction_cos_theta = 0;
  
  for (idx=0; idx
	  /*主要公式 acc[x] = Acc * (x / (√x^2 + y^2))
				 acc[y] = Acc * (y / (√x^2 + y^2))
	  所以		 Acc0 = acc[x] / (x / (√x^2 + y^2)) 、 Acc1 = acc[y] / (y / (√x^2 + y^2))
				 Acc = min(Acc0 ,Acc1);
	  */
    if (unit_vec[idx] != 0) {  // Avoid divide by zero.
      unit_vec[idx] *= inverse_millimeters;  // Complete unit vector calculation  //unit_vec[x] = x / (√x^2 + y^2)
      inverse_unit_vec_value = fabs(1.0/unit_vec[idx]); // Inverse to remove multiple float divides. // inverse_unit_vec_value[x] = (√x^2 + y^2) / x
      // Check and limit feed rate against max individual axis velocities and accelerations
      feed_rate = min(feed_rate, settings.max_rate[idx]*inverse_unit_vec_value);//通过分量计算速度
      block-acceleration = min(block->acceleration, settings.acceleration[idx]*inverse_unit_vec_value);//通过分量计算加速度
      // Incrementally compute cosine of angle between previous and current path. Cos(theta) of the junction
      // between the current move and the previous move is simply the dot product of the two unit vectors, 
      // where prev_unit_vec is negative. Used later to compute maximum junction speed.
      junction_cos_theta -= pl.previous_unit_vec[idx] * unit_vec[idx];
    }
  }//计算加速度Acc ,计算速度rate
  
  // TODO: Need to check this method handling zero junction speeds when starting from rest.
  if (block_buffer_head == block_buffer_tail) {
  
    // Initialize block entry speed as zero. Assume it will be starting from rest. Planner will correct this later.
    block->entry_speed_sqr = 0.0;
    block->max_junction_speed_sqr = 0.0; // Starting from rest. Enforce start from zero velocity.
  
  } else {
    // NOTE: Computed without any expensive trig, sin() or acos(), by trig half angle identity of cos(theta).
    if (junction_cos_theta > 0.999999) {
      //  For a 0 degree acute junction, just set minimum junction speed. 
      block->max_junction_speed_sqr = MINIMUM_JUNCTION_SPEED*MINIMUM_JUNCTION_SPEED;
    } else {
      junction_cos_theta = max(junction_cos_theta,-0.999999); // Check for numerical round-off to avoid divide by zero.
      sin_theta_d2 = sqrt(0.5*(1.0-junction_cos_theta)); // Trig half angle identity. Always positive.
      // TODO: Technically, the acceleration used in calculation needs to be limited by the minimum of the
      // two junctions. However, this shouldn't be a significant problem except in extreme circumstances.
      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) );
    }//计算出拐角速度的平方
  }
  // Store block nominal speed
  block->nominal_speed_sqr = feed_rate*feed_rate; // 上位机传过来的期望速度的平方(mm/min). Always > 0
  
  // Compute the junction maximum entry based on the minimum of the junction speed and neighboring nominal speeds.
  block->max_entry_speed_sqr = min(block->max_junction_speed_sqr, 
                                   min(block->nominal_speed_sqr, pl.previous_nominal_speed_sqr));//求出理论值的最大拐角速度
  
  // Update previous path unit_vector and nominal speed (squared)
  memcpy(pl.previous_unit_vec, unit_vec, sizeof(unit_vec)); // 更新分量速度在pl.previous_unit_vec[],pl.previous_unit_vec[] = unit_vec[]
  pl.previous_nominal_speed_sqr = block->nominal_speed_sqr;//更新标称速度,用来给下一条线段进行对比
    
  // Update planner position  更新激光头的坐标
  memcpy(pl.position, target_steps, sizeof(target_steps)); // pl.position[] = target_steps[]
  // New block is all set. Update buffer head and next buffer head indices.
  //运行以下函数之前,假设当前节点block_buffer_head为head_node
  block_buffer_head = next_buffer_head;  //buffer_head指向当前节点head_node的下一个节点
  next_buffer_head = plan_next_block_index(block_buffer_head);//buffer_next指向当前节点head_node的下一个节点的下一个节点
  //以下函数planner_recalculate()会用到关于上诉节点改变后的节点,用来追溯最开始的head_node
  
  // Finish up by recalculating the plan with the new block.
  planner_recalculate();

3.planner_recalculate

代码如下(示例):

static void planner_recalculate(void)//运动规划-》前瞻算法,和后顾算法
{   
	// Initialize block index to the last block in the planner buffer.
	//在plan_buffer_line这个函数中提及到了block_buffer_head这个节点已经变为了自己的下一个节点
	uint8_t block_index = plan_prev_block_index(block_buffer_head);//将block_index变成当前处理的节点的坐标
		
	// Bail. Can't do anything with one only one plan-able block.
	float entry_speed_sqr;
	plan_block_t *next;
	plan_block_t *current = &block_buffer[block_index];//current为当前节点
	if (block_index == block_buffer_planned) { return; }
	  
	// Reverse Pass: Coarsely maximize all possible deceleration curves back-planning from the last
	// block in buffer. Cease planning when the last optimal planned or tail pointer is reached.
	// NOTE: Forward pass will later refine and correct the reverse pass to create an optimal plan.
	// Calculate maximum entry speed for last block in buffer, where the exit speed is always zero.
	//current即是当前链表中最后一个处理的线段(也是最新处理的线段)现在设退出时的速度(即末速度)v1为0,那么v1^2 - v0^2 = 2ax可以推出进入是的初速度为2ax
	current->entry_speed_sqr = min( current->max_entry_speed_sqr, 2*current->acceleration*current->millimeters);
	block_index = plan_prev_block_index(block_index);//将block_index变成当前线段的上一个线段的坐标
	if (block_index == block_buffer_planned)// 当【Only two plannable blocks in buffer】. Reverse pass complete.
	{ 
		// Check if the first block is the tail. If so, notify stepper to update its current parameters.
		if (block_index == block_buffer_tail) { st_update_plan_block_parameters(); }
	} 
	else // 当【Three or more plan-able blocks】
	{ 
		while (block_index != block_buffer_planned)				//无t公式要理解清楚方向的定义!!!
		{//这段代码的含义是从当前线段往前推,直到所有的线段都优化过退出循环,\
		   即每条线段的初速度不能超过线段设置的最大初速度的限制
			next = current;
			current = &block_buffer[block_index];
			block_index = plan_prev_block_index(block_index);
			// Check if next block is the tail block(=planned block). If so, update current stepper parameters.
			if (block_index == block_buffer_tail) { st_update_plan_block_parameters(); } 
			// Compute maximum entry speed decelerating over the current block from its exit speed.
			if (current->entry_speed_sqr != current->max_entry_speed_sqr)
			{
				entry_speed_sqr = next->entry_speed_sqr + 2*current->acceleration*current->millimeters;//从最后一个块的退出速度当作初速度往前推(且最后一块的退出速度为0)
				
				if (entry_speed_sqr max_entry_speed_sqr)
					current->entry_speed_sqr = entry_speed_sqr;
				else
					current->entry_speed_sqr = current->max_entry_speed_sqr;
			}
		}
	}    
	// Forward Pass: Forward plan the acceleration curve from the planned pointer onward.
	// Also scans for optimal plan breakpoints and appropriately updates the planned pointer.
	next = &block_buffer[block_buffer_planned]; // Begin at buffer planned pointer
	block_index = plan_next_block_index(block_buffer_planned); 
	
	while (block_index != block_buffer_head)
	{ //这段代码的含义是从第一个没有优化过的线段往前,直到到达当前线段时退出循环,\
	    即每条线段的末速度不能超过下一条线段的初速度,这样多条线段才能保持连续的速度运行
		current = next;
		next = &block_buffer[block_index];
		// Any acceleration detected in the forward pass automatically moves the optimal planned
		// pointer forward, since everything before this is all optimal. In other words, nothing
		// can improve the plan from the buffer tail to the planned pointer by logic.
		if (current->entry_speed_sqr entry_speed_sqr)
		{
		  entry_speed_sqr = current->entry_speed_sqr + 2*current->acceleration*current->millimeters;
		  // If true, current block is full-acceleration and we can move the planned pointer forward.
			if (entry_speed_sqr entry_speed_sqr)
			{
				next->entry_speed_sqr = entry_speed_sqr; // Always 
微信扫一扫加客服

微信扫一扫加客服

点击启动AI问答
Draggable Icon