之前在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固件中步进电机算法的合理和经典。
(完)