A4988 步进电机加速控制算法的尝试(下)——MKE06K128实现篇
更新:HHH   时间:2023-1-7


  之前在arduino上的实现的新的步进电机算法,需要移植到32位MKE06K128上。这个任务听上去事实而非,整个Marlin固件还涉及别的部分,比如PID加热控制模块,舵机模块,串口指令读取代码。但是我们cocky的Teamleader很反感移植Marlin。没办法,这一步我的理解,只要能实现plan_buffer_line就算完成任务。进一步简化,只要实现一个轴的plan_buffer_line就算完成任务。


硬件连接部分:


  我们cocky的Teamleader是个能干的人,很快画出并焊好了MKE06K128的电路板供我当作开发板使用。Freescale有基于eclipse的专用开发环境Kinetis Design Studio,简称KDS,KDS既集成了MKE的交叉编译器,而且以插件的形式提供了“专家系统”,能够可视化的配置硬件管脚,并且生成示意图方便review(如下图),看上去很酷。


  这个专家工具在调试硬件和快速实现阶段也非常方便。我在电路板上接上Jlink,并且连接上A4988和步进电机,在排除掉A4988的损坏之后。编写一个简答的定时器程序,并把周期写成3.2KHz(请参考arduino上的实现中关于3.2K的来历),步进电机就应该以1rad/s的速度转,如果不是,就要用示波器和万用表排除芯片,电路板,电源和Jlink的故障可能。如果是,就为下一步编写固件代码打下了坚实的基础。最终调试好的硬件连接图如下:


使用jlink时注意,

  • 在系统上电的情况下,需要去掉jlink上供电的跳线帽,否则无法进行刷写工作。

  • 即使脉冲宽度很窄,也可以驱动电机,而且窄脉冲的噪音很小。

  • A4988的EN路上拉到电路5V,需要软件将其拉低,步进电机才能正常驱动,否则驱动电路不使能。


软件实现部分:


  首先实现plan_buffer_line()函数,需要编写%Planner和%Stepper两个模块。前者管理队列,后者管理步进电机的中断响应驱动。和Marlin固件的区别在于,这里的%Planner不必计算执行的各个速度节点,而是只需要设置稳定后的速度值,%Stepper会动态的计算出瞬时速度。

  队列和队列指针都在%Planner模块中定义为全局变量,由于声明了external 关键字,当其他项目包含planner.h时就会引入该变量,即等效于该全局变量为全项目全局变量。

%Planner.cpp中:

block_t block_buffer[BLOCK_BUFFER_SIZE];
volatile unsigned char block_buffer_head; volatile unsigned char block_buffer_tail;

  另外设置了几个强制内联函数:

 FORCE_INLINE block_t *plan_get_current_block();//读取当前block函数
 FORCE_INLINE bool blocks_queued() { return (block_buffer_head != block_buffer_tail); }//队列是否非空

  在这里:

#define FORCE_INLINE attribute((always_inline)) inline

  告诉编译器,设置为强制内联型;对于此,Cpp的语法解释是:

inline关键字仅仅是建议编译器做内联展开处理,而不是强制。在gcc编译器中,如果编译优化设置为-O0,即使是inline函数也不会被内联展开,除非设置了强制内联(attribute((always_inline)))属性。

  关于内联函数,补充一点基础知识:

在内联函数内不允许用循环语句和开关语句。否则会被编译器当作普通函数。

  在%stepper.cpp中定义了:

 block_t *current_block  //当前运动实例

  由于没有在stepper.h中定义相应的extern 类型,所以该变量为模块内的private全局变量。

  即使是使用32位单片机,也不应该在中断响应函数中进行浮点运算,否则中断频率会被大大拖慢。所以%stepper函数的最终结果为:

void ST_PULSE_TI_OnInterrupt(void) {
	/* Write your code here ... */
#ifndef HARDWARE_DEBUG_MODE

	if (!current_block) {
		current_block = plan_get_current_block();
	}

#define DISTANCE_COUNT_RESET current_block->rounds_count_per_mstep-=current_block->one_micro_step_mm
#define DISTANCE_IS_ONESTEP  current_block->rounds_count_per_mstep>=current_block->one_micro_step_mm
#define DRIVE_PULSE E0_STE_SetVal();	\
		E0_STE_ClrVal()
	/*==========generate a pulse when a step accumulated===========*/

	if (DISTANCE_IS_ONESTEP) {
		DRIVE_PULSE;
		DISTANCE_COUNT_RESET;
	}


	/*update the new speed and  ... */

	if (current_block) {
		if (current_block->rounds_behind + current_block->rounds_ahead
				< current_block->rounds) {
			// accumulate the rounds and the microstep_count_for_rounds

			current_block->rounds_behind += current_block->instance_rate;
			current_block->rounds_count_per_mstep +=current_block->instance_rate;

			//when speed climbing up case
			if (current_block->instance_rate < current_block->nominal_rate) {
				current_block->instance_rate += current_block->acceleration;
			}

			//when hold the nominal_rate case
			else {
				//make sure the rate remains nominal_rate
				current_block->instance_rate = current_block->nominal_rate;
			}

			// update the rounds left for all the three case.
			current_block->rounds_ahead=current_block->instance_rate/2/current_block->acceleration*current_block->instance_rate;
		}
		//when speed slipping down case

		else if (current_block->instance_rate > current_block->exit_rate) {
			current_block->rounds_behind+= current_block->instance_rate;
			current_block->rounds_count_per_mstep +=current_block->instance_rate;
			current_block->instance_rate -= current_block->acceleration;
		}

		// at the end of the block

		else if (current_block->instance_rate <= current_block->exit_rate) {
			current_block->instance_rate = 0;
			current_block->nominal_rate = 0;
			current_block->rounds_ahead = 0;
			current_block->acceleration = 0;
			current_block->rounds = 0;
			current_block->rounds_count_per_mstep = 0;
			//ST_PULSE_TI_Disable();//current_block should update
			current_block = NULL;
		}

	}

#endif

}

  浮点数主要来自两方面,一是每个中断响应函数中的单位时间delta t;另一个是转每秒这个单位中,转往往是小数。为了消灭浮点数,我们对单位进行了转换:

void plan_buffer_line(const float e, float feed_rate) {
//void plan_buffer_line(const float x, const float y, const float z, const float e, float feed_rate, const uint8_t extruder){
// e is unit of round
// feedrate is unit of round per second
// push a block into pipeline
	block_t *block = &block_buffer[block_buffer_head];
	//update the ini value of the block
	block->nominal_rate =(unsigned int)(feed_rate*ST_PULSE_FREQ);
	block->rounds = (unsigned int)(e - position[E_AXIS])*FLOAT_FACTOR;
	block->entry_rate = 0;
	block->exit_rate = 0;
	block->acceleration = (unsigned int)(DEFAULT_ACCELERATION);
	block->direction_bits = e > position[E_AXIS] ? 1 : 0;
	block->one_micro_step_mm=(unsigned int)FLOAT_FACTOR/MICROSTEP/RESOLUTION;
	ST_PULSE_TI_Enable();
	//Enable the Interruption for stepper control and plannerS
	// Move buffer head
	block_buffer_head = next_block_index(block_buffer_head);
	//block_buffer_head=block_buffer_head+1;
	// Update position
	position[E_AXIS] = e;

 }

  其中,在configuration.h中定义了转换宏:

#define ST_PULSE_FREQ 10000

#define FLOAT_FACTOR (ST_PULSE_FREQ*ST_PULSE_FREQ)

  由此可见,为了在计时器中断响应函数中不出现浮点数,必须要给位移,速度和加速度乘以一个因子。而为了防止程序中出现变量超出整型大小而溢出,计时器频率不能太高。具体的范围,又受限于打印机的位移范围(往往是0~200mm)。这种算法,数据结构和处理器浮点运算性能局限性的共同作用,产生了最终的代码,非常的经典。

正是由于Team leader质疑修改步进电机算法,我才能发现这么多隐藏在理所当然中的深刻限制和工程智慧,感谢他的偏执。同时我更加深刻理解了Marlin固件中步进电机算法的合理和经典。

(完)

返回开发技术教程...