3月接到一项开发任务:独自编写一套3D打印机的控制固件。对于FDM打印机,固件中最关键的控制对象是步进电机。我非常愿意向大家分享整个曲折的开发过程。
故事一开始,我要向Marlin项目致敬:以Marlin为代表的开源3D打印固件,孕育了桌面级3D打印的普及和繁荣;

  这次开发任务不同于Marlin固件在于,在中断响应中动态规划步进电机的速度。路径规划器退化成一个更新末尾速度和管理队列的模块。这样做的理由来自于产品经理深信,采用32位控制器后,中断能够有效的处理速度的实时计算。我对这个观点报怀疑态度,不过这不影响我做这件事情。

算法实现轻松愉快

首先,利用arduino的串口用作后面的移植,我们可以很快实现逻辑。arduino的1.6.7版本甚至自带了串口绘图器:只要在代码中调用println函数输出速度变量,就能够直接绘制出速度曲线。初版代码如下(待验证):

#define ledPin 13
#define stepPin 8
#define dirPin 9 
#define gratingPin 10


/*
 * Each of the timers has a counter that is incremented on each tick of the timer's clock.
 * CTC timer interrupts are triggered when the counter reaches a specified value stored in the compare match register. 
*/

/*
 * timer1 is 16 bit, which means it can store a maximum counter value of 65535.
 * periods = prescaler*(1+31250)/16M=0.5s. when using, calculate OCR1A instead
*/
void setup()
{
  pinMode(ledPin, OUTPUT);
  pinMode(stepPin, OUTPUT);
  pinMode(dirPin, OUTPUT);
  pinMode(gratingPin,INPUT);
  digitalWrite(dirPin, 1);
  
  Serial.begin(9600);
  // initialize timer1 
  noInterrupts();           // disable all interrupts
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;

  int rpm=120;//  280 rpm should be the fastest
  int MircoN=8;
/*
 * FOR Microstepping is Full step
 * this stepper is 1'8 per step. then a rotation hass 360/1.8=200 step per revolution  
 * f_pulse=N*f_step=N*f_revolution*200=200N*rpm/60
 * OCR=16m/f_timer/prescaler=16m/f_pulse/2/prescaler=1.2M/N/rpm/prescaler
 */
  
  OCR1A = 1200000/MircoN/rpm/256;            // compare match register 31250=16MHz/256/2Hz
  Serial.println(OCR1A);
  TCCR1B |= (1 << WGM12);   // CTC mode ( Clear Timer on Compare Match)
  TCCR1B |= (1 << CS12);    // 256 prescaler 
  TIMSK1 |= (1 << OCIE1A);  // enable timer compare interrupt
  interrupts();             // enable all interrupts
}

ISR(TIMER1_COMPA_vect)          // timer compare interrupt service routine
{
 
  static float v_set,v,acc,s,s_fore,s_set;
  v_set=30;
  s_set=6000;
  acc=2;


  if((s+s_fore)=0) { 
    v=v-1*acc;
    s=1*v+s;
  }
  delay(1);
  Serial.println(v);

}

void loop()
{
  // your program here...
}

脉冲信号vs方波信号 

   连接电路:步进电机需要额外的驱动模块,这里采用的A4988,将驱动信号通过H桥转化成为两组高频正弦波来产生磁场驱动电机。在这里需要提醒的是,步进电机的驱动信号为周期脉冲信号,而不是方波信号。实验中我用方波信号驱动带动了步进电机,但是噪声振动都非常大。具体的说明资料如下:(待完善)
   硬件方面,Arduino提供了快速解决方案,可以不用在乎电源,调试器,管教配置和编译工具链,实现控制代码原型。最终的连接方法如下。我们采用arduino自带的步进电机驱动模块,来验证硬件连接。在整个验证过程中,建议使用示波器测量A4988的输出管教,排除已毁坏的A4988模块的干扰。(本次实验,我使用的前两个模块都是坏的。如果不遵照流程,后面浪费的调试时间将不可想象。)
   硬件的连接如下:

A4988 步进电机加速控制算法的尝试(上)——arduino实现篇_第1张图片

和底层关联,思维误区。

       接下来的任务,很自然就是将速度的信号和底层的驱动连接起来。由于低层驱动位周期性脉冲信号,arduino很容易调用IO口拉高拉低就能实现。但是脉冲信号的频率,如何随中断更新的变量v联系起来。我们首先想到的,就是每次中断得到新的速度值,对应的就是下一次新的中断响应时间;事实上Marlin固件就是如此实现调整驱动速度。在实际实验中,我经历了很有趣的一次开发过程。
   原因在于我很希望能明确中断的频率和步进电机速度之间的关系。这里,我选择了步进电机的转速(rps)来代表其速度。我们的步进电机的转角是1.8度,也就是说每转的分辨率(resolution)为200。由于A4988配置为16分频细分,也就是驱动频率应该是200x16=3200倍的转速频率。由于后面都是以1r/s为典型值,3.2kHZ这个值后来成为了整个实验成功的关键目标。
   所以在我们的算法中,我们定义一个值,每次中断都累加一段位移,当达到一个定值时,这个值清除(减去这个定值,而不是清零,感谢ZLY指点),同时输出一个脉冲。我们可以这么理解,如果速度值比较大,累计的快,脉冲就密;反之,累计的慢,脉冲就疏。
   剩下的问题是这个值如何确定。我们假定速度处于稳定阶段,则该值为速度除以方波频率。而频率为3200倍的速度,所以速度抵消掉,这个定值无论速度为何,都是1/3200。
   实验证明,这种算法把速度变量和驱动速度更新有效的结合了起来。项目设计到这里,我也和大家一样,虽然相信这个算法正确,但是很困惑正确的原因。后来终于明白,其实道理非常简单:每当累计的位移达到每个细分位移时,机械结构要求输出一个脉冲。这种解释合情合理,也说明了1/3200这个定值的物理意义,为每个细分的位移(单位为转)。    

对不上实际值...

   高兴没有持续多久,验证时很快就发现了问题:示波器显示,转速并非代码设定的1r/s.这是非常让人沮丧的事情。项目到这时候陷入了很长时间的僵局,我不停实验,不停怀疑之前的算法有某个细节是错误的,但是这个错误始终找不出来,这时候我已经开始烦躁。
    转速无论怎么设定,都比设定的值低8倍左右。示波器很准确的捕捉到了这个看上去像是分频的现象。迟迟找不出算法的漏洞在哪里,或者代码的错误在哪里,我终于屈服于困难。然而硬件调试和软件调试的不同,就是从互联网上很难得到直接的指导:硬件调试出错的可能性太多,不具有典型性。更何况是新的控制算法T^T.幸好我有ZLY这么优秀的同事,在这个时候给了至关重要的指导。
      “中断响应程序中不可以出现浮点运算,否则会拖慢中断响应周期。”
 所以在没有指导的环境中开发硬件系统是令人沮丧的事情。在没有志同道合的玩伴或者硬件大神指导之前,我会慎重选择进行硬件开发。另一方面,硬件开发的书籍对于缩短开发周期将显得弥足珍贵。

浮点运算,数据类型溢出。

找到了问题,就立刻行动。浮点运算来源于两个方面:一是每个周期时间太短(秒);还有就是位移和速度单位太大(转)。所以解决方法是,每个周期的单位时间设为1。同时给所有长度单位乘以系数。
 这个系数应该多大呢?应该非常大。原因是为了保证加速度的整型值大于1,同事单位累计时间单位为1,位移的单位就必须至少乘以为中断频率的二次方;而中断频率必须大于3.2khz,才能实现1r/s的驱动,这是物理层决定的。所以这个系数至少都在1M级别。

 系数过大带来的最大问题是整型的溢出。arduino为8位机,unsigned int型为uint16_t 只有2个字节长度。表示范围0~65535,所以要用4个字节的uint32_t型(unsigned long int):


最终的代码如下:

#define ledPin 13
#define stepPin 8
#define dirPin 9 
#define OSCILLATOR 16000000
#define MICROSTEP 8
#define REVOLUTION 200
#define NANO 10000000000
#define VSET 4 //rotation speed rps
#define ACCSET 1//rotation per square second
#define SSET 100*NANO


#define DELTAT 256.0/OSCILLATOR


static unsigned long int block,s,s_fore,v,VSET_S,ACCSET_S;


void setup()
{
  pinMode(ledPin, OUTPUT);
  pinMode(stepPin, OUTPUT);
  pinMode(dirPin, OUTPUT);
  digitalWrite(dirPin, 1);
  
  Serial.begin(9600);
  // initialize timer1 
  noInterrupts();           // disable all interrupts
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;
  
  VSET_S=VSET*NANO*DELTAT;
  ACCSET_S=VSET*NANO*DELTAT*DELTAT;
  Serial.println(VSET_S);

  OCR1A=1;   

  
  TCCR1B |= (1 << WGM12);   // CTC mode ( Clear Timer on Compare Match)
  TCCR1B |= (1 << CS12);    // 256 prescaler, thus every "tick" takes  256/16M s 
  TIMSK1 |= (1 << OCIE1A);  // enable timer compare interrupt
  interrupts();             // enable all interrupts
}

ISR(TIMER1_COMPA_vect)          // timer compare interrupt service routine
{
  
digitalWrite(stepPin, 0);
  
 if((s+s_fore)0)
{
  s+=1*v;
  block+=v*1;
  v=v-1*ACCSET_S;
}
else if(v<=0)
{
  v=0;
  s_fore=0;
  s=0;
  delay(2000);      
 }  

if (block>NANO/MICROSTEP/REVOLUTION)  // reality meaning ,when 1 step made, output this step
{
  digitalWrite(stepPin, 1);
  block-=NANO/MICROSTEP/REVOLUTION;
}    
}
 

void loop()
{
  // your program here...
}

示波器终于显示出3.2kHz波形。新的步进电机控制算法在arduino的实现就比想象中来的困难。在中断中进行复杂运算应该尽量予以避免,而是养成“空间换时间”的设计习惯。这次开发到此为止,用掉了2周左右。硬件开发的时间还是存在很大的风险。然而并没有昨完。后面这个算法还要移植到32位机上。要完成路径管理器函数的封装和测试,最终要成为新固件的核心模块。