CNCシールドで2つ以上のステッピングモーターを動かす

今回は、電子工作ネタです。
前回は、2つのステッピングモーターを同期して動かしてみました。
alasixosaka.hatenablog.com
今回は、同期せず、バラバラに2つのモーターを動かしてみようというものです。
CNCシールドでステッピングモーターを動かすには、STEPにつながっているピン(X軸の場合なら2番ピン)を周期的にHIGH、LOWにしてやるとモーターが動きます。モーターが一つだけの場合、HIGHとLOWの間にディレイを入れてやってモーターの回転速度を調整するのが普通です。しかし、ディレイ関数を使ってしまうとその間は他の操作ができなくなるので、2つのモーターを別々の速度でコントロールすることができなくなります。例えばモーターBとモーターAの2つのモーターがあり、モーターBをゆっくり、モーターAを早く動かそうとしたらどうするか。図に書くと下の図のようになります。

f:id:alasixOsaka:20210504211756j:plain
モーターA,Bのコントロール用シグナル

上の図の場合、モーターA用のシグナルのディレイが2msだったとすると、モーターコントロール用のピン、CNCシールドのX軸だったら2番ピンをHIGH、LOWと変化させる間にdelay(2)あるいはdelaymicorosecond(2000)を入れておいて、そのループが3回回ったタイミングでモーターB用のシグナル(Y軸だったら3番ピン)の極性を反転させれば良いことになります。プログラムで書くとこのようになります。

boolean B = LOW;
while(1){
  for (i=0; i<3; i++){
    digitalWrite(2, HIGH);
    delayMicrosecond(2000);
    digitalWrite(2, LOW);
    delayMicrosecond(2000);
  }
  digitalWrite(3, B);
  B=!B;
}

2つのモーターの速度が決まっている場合はこれでもよいでしょうが、モーターの速度を変更したい場合、例えばモーターBの場合は、ループ関数の値を調整すれば比較的簡単です。しかし、モーターAの速度を変えようと思ってディレイ関数の値を調整してしまうと、モーターAの速度も変わってしまいます。もちろんそれに合わせてループ関数の値を調整すれば良いのでしょうが、いちいち両方をいじくるのは何かと面倒です。それぞれのモーターの速度を簡単に制御する方法はないのか? というのが今回のお題です。
やり方ですが、タイマー割込みというのを使います。Arduino Uno(Atmega328P)にはTimer0、Timer1、Timer2という3つのタイマーがあります。このうち、Timer0は、ディレイ関数などタイミングをとる操作に使われているのでこれを使ってしまうとこれらの操作ができなくなるので、Timer1かTimer2を使うことになります。
そして、Timer割込みを使うにはレジスタ操作をする必要があるようです(できなくもないようなのですが、思い通りに動かすにはレジスタ操作の方がやり易いような感じです)。
ちょっと初心者にはハードルが高いですが、今回はこれにチャレンジです。タイマーの細かい使い方を知りたい方は参考サイトを見ていただくとして、今回は、決まったタイミングでTimer割込みを発生させてそのたびごとにピンのHIGH、LOWを切り替えるだけなのでそんなに難しくはないです。よくあるLチカと基本的には同じです。

レジスタの設定値

Timer1を使ってタイマー割込みを行うために使うレジスタは、TCCR1A、TCCR1B、OCR1A、OCR1B、TIMSK1の5つです。このうちTCCR1A、TCCR1B、TIMSK1の各レジスタはそれぞれ8bitで構成されており、それぞれのビットを1にするか0にするかでタイマーの動作を決定します。今回使用するTimer1は16ビットモードのタイマーで、最大のカウント数が65535となります。Arduino Unoの内部クロックは16MHzで動いているので、単純にこのクロックを1つづつカウントしていけば、最大で4.096msまでカウントすることができます。さきほど単純にと書きましたが、もっと長い時間をカウントする場合は、分周比を設定してしてやればもっと長くすることができます。分周比はTCCR1Bのビット0-2の3ビットCS10、CS11、CS12で指定することができます。例えば、もっと長く8倍にしたい場合は、CS10=0、CS11=1、CS12=0とすれば、タイマーを8倍に伸ばすことができ、先ほどの4.096msの8倍、すなわち、32.774msとすることができます。この3ビットの指定で最大で1024倍までカウント数を伸ばすことができます。
Arduino IDEでの書き方は、TCCR1B = 0; としてまずTCCR1Bをクリアしておいて、 TCCR1B |= (1 << CS11); として、CS11を1に変更します。
次にモードを設定します。今回の目的は周期的に割込みをかけるのが目的ですから、CTCモードというのを使います。このモードは、タイマーのカウント数がOCR1Aに一致したときに割込みを発生させて、カウント数を0にしてまたカウントを繰り返すというモードになります。正確には、OCR1Bに一致したときも割込みを発生することができますが今回は使いませんので詳しい説明は省略します。
CTCモードに設定するには、TCCR1BにあるWGM12ビットを1にする必要がありますので、先ほどの操作でTCCR1B |= (1 << CS11); としていたところを、TCCR1B |= (1 << CS11) |(1 << WGM12); としてやります。
そうしておいて、例えば、4msおきに割込みを発生させようと思った場合、先ほどの設定で分周比を8にしておいたとして、カウント数を8000にすれば丁度4msで割込みがかかることになります。したがって、OCR1A=8000; とします。
最後に割込みの設定です。TIMSK1のOCIE1Aビットを1にすることでOCR1Aに一致したタイミングで割込みを発生することができるので、TIMSK1 |= (1 << OCIE1A) ; としてやります。
これらのレジスタの設定をまとめると、

TCCR1A = 0; // 初期化
TCCR1B = 0; // 初期化
OCR1A = 8000;
TCCR1B |= (1 << CS11) | (1 << WGM12); //Prescaler=8, CTC mode on
TIMSK1 |= (1 << OCIE1A); //enable OCR1A Interrupt

今回TCCR1Aレジスタについては何も設定していませんが、初期化のために0にしています。
こうすることで4ms毎に割込みが発生するので、そのタイミングでモーターAの制御ピン(2番ピン)の状態を反転してやれば、最初の図と同じシグナルが出力されます。そして、そのタイミングはタイマー割込みによって決まっているのでモーターBの制御は通常のループ関数内でディレイ関数の値を制御するだけでコントロールできるようになります。図に書くと下のようになります。

f:id:alasixOsaka:20210504214101j:plain
モーターAをタイマー割込みでコントロールすることで、モーターBは独立にコントロールできる

図の矢印のタイミングで割込みが発生し、モーターA用のシグナルはそのタイミングで反転します。モーターBはそれに関係なくディレイ関数で速度をコントロールできます。図では、最初の図と同じタイミングになっていますが、モーターAと関係なく制御できるので、シグナル反転のタイミングをモーターAのシグナルがHIGHからLOWに変わるタイミングやその逆のタイミングでなく、HIGHの時とか、LOWの時など中途半端なタイミングでもOKです。
プログラムするとこんな感じになります。

#define PIN_EN 8
#define PIN_DIRZ 7
#define PIN_STEPZ 4
#define PIN_Zlimit 11
#define PIN_DIRY 6
#define PIN_STEPY 3
#define PIN_Ylimit 10

byte State = LOW;
byte DIR = LOW;
volatile int i;

void setup() {
  pinMode(PIN_EN, OUTPUT);
  pinMode(PIN_DIRZ, OUTPUT);
  pinMode(PIN_STEPZ, OUTPUT);
  pinMode(PIN_Zlimit, INPUT_PULLUP);
  pinMode(PIN_DIRY, OUTPUT);
  pinMode(PIN_STEPY, OUTPUT);
  pinMode(PIN_Ylimit, INPUT_PULLUP);

  //noInterrupts();
}

ISR (TIMER1_COMPA_vect) {
  State = !State;
  digitalWrite(PIN_STEPY, State);
  i++;
  if(i>2000){
    DIR = !DIR;
    digitalWrite(PIN_DIRY, DIR);
  }
}

void TimeStart(){
  TCCR1A  = 0;
  TCCR1B  = 0;
  OCR1A = 1200;
  TCCR1B |= (1 << WGM12) | (1 << CS11);  //CTCmode //prescaler to 8
  TIMSK1 |= (1 << OCIE1A); //enable OCR1A Interrupt
  digitalWrite(PIN_DIRY, DIR);
}

void loop() {
  // put your main code here, to run repeatedly:
  
  //interrupts();
  delay(2000);
  TimeStart();
  delay(2000);
  digitalWrite(PIN_DIRZ, HIGH);
  for (int j = 0; j<1000; j++){
    digitalWrite(PIN_STEPZ, HIGH);
    delayMicroseconds(50000);
    digitalWrite(PIN_STEPZ, LOW);
    delayMicroseconds(50000);
  }
  
  delay(2000);
  noInterrupts();
  while(1){
  }
}

始めの方はピンのアサインです。setupでデジタルピンをアウトプットに設定しています。ここで、リミットスイッチの入力をインプットに設定していますが今回のプログラムでは使っていません。
ISR (TIMER1_COMPA_vect) { 以下はタイマー割込みの処理ルーチンです。StateはSTEPに出力するための関数で割込みが発生するたびにHIGH⇔LOWを繰り返します。ループカウンタとしてiを設定しておいて、割込み処理ルーチンが呼び出されるたびに+1しています。ここではiが2000になったらDIRを反転して方向を変えてiをクリアしています。モーターの動く距離を調整する場合はここの2000を多くしたり、少なくしたりします。TimeStart()関数の中身は上の方で詳細に書いたので省略します。ここでは、STEPの間隔を0.6msとかなり早いタイミングにしています。
void loop() { 以下がメインルーチンでTimeStart();でタイマーをスタートします。ここからモーターB(Y軸)が動き始めます。2秒遅れてモーターA(Z軸)を動かします。モーターAのSTEP間隔は50msとゆっくり動かしています。モーターの動く距離はループ関数jで調整します。ここでは1000になっています。もちろん、ループ関数iやjを使う代わりにリミットスイッチを読み取って反転しても構いません。ただし、リミットスイッチは反転してすぐにクリアにならないので、リミットスイッチがクリアになるまでモーターを動かしてやる必要があるので少し手間がかかります(そうしないとリミットスイッチがONと判定されて反転を繰り返してモーターが動きません)。
割込み禁止、割込み許可のnointerrupt();、interrupts();は初めは使っていましたが、動作にはあまり関係ないようなのでコメントアウトしました。

参考にしたサイト
Arduino Unoでタイマー割り込みを使う方法 | 理系大学院生の知識の森
ArduinoのTimerを初心者が1からなんとなくわかるためのメモ - Qiita