ESP32S3 A4988 ステッパモータ駆動
短時間でモータ指令値を変化させる用途のパルス生成クラス
タイマー割込みを利用してパルスを生成

2027.7.20 Coskx Lab  

1 はじめに

Xiao ESP32S3で小型ステッパモータ(ステッピングモータ)を制御します。
短時間でモータ指令値を変化させる用途のパルス生成クラスで制御するようにしました。
PWM信号の発生は20μsecごとに起動するタイマー割込みを利用しています。



ステッパモータドライバIC A4988を使用して,小型ステッパモータを駆動します。
A4988にのpulse端子にパルスを与えるだけでステッパモータは決められたステップ角度で回転します。
原理的な説明は次のWebページを参照してください。
≫モータドライバA4988でステッピングモータ制御モータドライバA4988でステッピングモータ制御

1/16マイクロステップ駆動を採用します。そのため,通常のモータ1ステップ動作は1.8度の回転ですが,ここでは0.1125度(=1.8度の1/16)回転になります。

参考 モータドライバIC A4988基板実装(秋月電子販売など)


参考 使用しているモータ
名称: 3DプリンターTitan Stepper Motor (AMAZON)
モーターサイズ(L * W * H):42 * 42 * 23 mm
フェーズ:2フェーズ
電圧:4.1V
電流:1A /相
抵抗値:4.1±10%Ω/相
インダクタンス:4.1±20%mH /相
保持トルク:13N.cm以上(18.5oz.in)
ディテントトッグ:2.0N.cm(2.85oz.in)
絶縁材クラス:B
ステップ角:1.8°±5%
重量:132g

2 使用環境

3 実験用配線

次のように配線しました。
DIR端子,STEP端子,ENABLE端子はそれぞれマイコンのD0,D1,D6に接続しました。
1/16マイクロステップ設定のためMS1,MS2,MS3は,3つともH(VDD)に接続します。
RESET端子,SLEEP端子は,それぞれH(VDD),H(VDD)に接続します。



ENABLE端子は通常Lに設定します。 ステッパモータは停まっているときも動いているときも常時電流が流れています。(停まっているときに保持トルクを発生しています。)
ステッパモータを操作していないときで,保持トルク不要の場合は,電流を流さないようにしたいです。そのときは,ENABLE端子にHを与えます。
マイコンの出力端子を一つ使って,プログラムでA4988のENABLE端子にL,H信号を与えるようしています。
なお,モータ用電源は12Vを使いました。

4 電流制限の設定

説明は次のWebページを参照してください。 ≫モータドライバA4988でステッピングモータ制御モータドライバA4988でステッピングモータ制御

5 モータ駆動デモプログラム

モータ駆動の細かなところはパルス生成クラスに任せているので,メイン側の記述は簡素になります。
ただし,タイマ割り込みはメイン側でしか記述できないので,おまじないのようなタイマ割り込みの仕組みもメイン側の記述に含まれてしまいました。(青字のところです)
メイン側では1000μsec間隔でモータ指令値をパルス生成クラス側に与えています。
モータ指令値は-1から1までが有効で,正の値の場合は正転,負の場合は逆転の意味です。
プログラム上では,モータ指令値を-0.1から0.1で変化させています。

モータ指令値はdrive()でパルス生成クラス側に伝えられます。
パルス生成クラス側drive()では20μsecから50000μsecの範囲で,指令値に合わせたパルス間隔を算出し,変数に保存します。
drive()はパルス間隔を変数に保存したら,timer割り込みルーチンに受け取るよう指示を出します。
timer割り込みルーチンは指示を受けたらパルス間隔を受け取り,パルスを生成します。
パルスは最低3回のtimer割り込みルーチンの起動で生成されるため,実際の最小パルス幅は60μsecとなります。

動作周期と最小パルス間隔の設定によりますが,指令値が非常に小さいとモータが回らないこともあります。

  ステッパモータの動作テスト用プログラム本体(メイン)

//steppermotorA4988withclass2_singleDEMO.ino
#include <StepperMotorESP32_2s.h>

StepperMotorESP32_2s stepper;

const uint8_t PinDIR  = D0; // 右モータdir出力に使用する PIN番号
const uint8_t PinSTEP = D1; // 右モータpulse出力に使用する PIN番号
const uint8_t PinEnable = D6; // 左右モータドライブEnable(府論理)に使用する PIN番号
const uint32_t Controlperiod = 50000; //[us] =50[ms]
const uint32_t MinPulseperiod = 50;  //[us]
const uint32_t InterruptPeriod = 20; //[us]

hw_timer_t * timer = NULL;

void ARDUINO_ISR_ATTR onTimer() {
  stepper.ISRhandler();
}

void initializeTimerInterrupt() {
  timer = timerBegin(1000000); //// Set timer frequency to 1Mhz
  timerAttachInterrupt(timer, &onTimer); // Attach onTimer function to our timer.
  timerAlarm(timer, InterruptPeriod, true, 0); //20カウントなので20μsがタイマー割り込み間隔
}

void setup() {
  Serial.begin(115200);
  //while (!Serial)
  //  delay(10); // will pause Zero, Leonardo, etc until serial console opens
  delay(100);

  stepper.initialize(PinDIR, PinSTEP, PinEnable);
  stepper.disableDriver();
  stepper.setPeriods(Controlperiod, MinPulseperiod, InterruptPeriod); //50[msec] 50[us] 20[us]
  stepper.enableDriver();
  initializeTimerInterrupt();
}

void loop() {
  const double Motor_Drive_dutyRatios[] = {-0.8, -0.6, -0.4, -0.2, 0., 0.2, 0.4, 0.6, 0.8};
  const static int nRatios = sizeof(Motor_Drive_dutyRatios)/sizeof(double);
  static int increment = 1; //デューティー比番号dutyRatioIndex変更用変数
  static int dutyRatioIndex = (nRatios>>1);
  String str;

  int result = stepper.getcount();
  str = "steps = " + String(result);
  Serial.println(str);

  double dutyRatio_Value = Motor_Drive_dutyRatios[dutyRatioIndex];
  stepper.drive(dutyRatio_Value);

  str = "motor duty = " + String(dutyRatio_Value, 2);
  Serial.println(str);

  dutyRatioIndex += increment;
  if (nRatios-1 <= dutyRatioIndex) increment = -1;
  if (dutyRatioIndex < 1) increment = 1;

  delay(1000); //[msec]
}

/* test
void loop() {
  static int n = 0;
  const int max = 4000;
  double dutyRatio_Value = 0.8*sin(2.*3.14159265359*n/max);
  stepper.drive(dutyRatio_Value);

  if (++n == max) n = 0;
  delay(5); //[msec]
}
*/

  パルス生成クラスを記述したヘッダファイル
(StepperMotorESP32_2s.hの名前でメインと同じフォルダに入れてください)

//stepperMotorESP32_2s.h

//The class StepperMotorESP32 drives a stepper motor using the A4988 driver module.
//smooth drive

//Pulse trains for stepping motors are generated by timer interrupts at short intervals
//without using the PWM generation function of the microcontroller.

//The timer interrupt is set on the main side, and the timer interrupt routine on the main side
//calls a handler of this class.
//This handler performs the actions that should be described in the interrupt routine.
//(The above configuration was adopted because interrupt routines cannot be defined as class members.)

#ifndef StepperMotorESP32_2s_h
#define StepperMotorESP32_2s_h

class StepperMotorESP32_2s {
  public:
    void initialize(uint8_t DirPin, uint8_t StepPin, uint8_t EnablePin);
    void setPeriods(uint32_t ControlPeriod, uint32_t MinPulsePeriod, uint32_t InterruptPeriod);//[microsec], [microsec], [microsec]
    void drive(double ControlValue); // -1.0 <= ControlValue <= 1.0
    int getcount();
    void enableDriver();
    void disableDriver();
    void ISRhandler(); //timer interrupt handler
    void clear(); //clear driver variables

  private:
    uint8_t dirpin, steppin, enablepin;
    uint32_t Controlperiod;
    uint32_t MinPulseperiod;
    uint32_t Interruptperiod;

    portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

    int minNCount; // 最小カウント数 20μsユニットで作るパルス長 3になる予定 すなわち60μs
    int maxNCount; // 最大カウント数 20μsユニットで作るパルス長 250になる予定 すなわち5000μs

    //前回getcount()が呼び出したときから現在までにステッピングモータに与えたパルス数
    volatile int PulseSUM = 0; //モータパルス数

    //drive()は引数dえ要求された指令値からパルス長と回転方向を算出し,タイマー割り込みルーチンに渡す必要がある
    //渡すべきlengthとdirを受け取ってもらうまで保存しておく変数
    volatile int length = 0; //モータパルス発生間隔(単位はユニット)
    volatile int dir = 0; //モータ回転方向 -1, 0 or 1;  0のときはパルスを発生しない

    //現在使用中か,あるいは直後のパルス長切り替え時に使用予定の発生パルスに関する値
    //タイマー割り込みルーチンはlengthとdirをlengthCRNTとdirCRNTに受け取る
    int lengthCRNT = 0; //モータパルス発生間隔(単位はユニット)
    int dirCRNT = 0; //モータ回転方向 -1, 0 or 1;  0のときはパルスを発生しない

    //drive()はタイマ割り込みルーチンに渡すべき値をlengthとdirにセットしたら,
    //checkrequestをtrueにセットする
    //タイマー割り込みルーチンはlengthとdirをlengthCRNTとdirCRNTに受け取ったら,
    //checkrequestをfalseにリセットする
    volatile bool checkrequest = false;

    int count = 0; //ユニットカウント
};
#endif

  パルス生成クラスを記述したC++ファイル
(StepperMotorESP32_2s.cppの名前でメインと同じフォルダに入れてください)

//stepperMotorESP32_2s.cpp
#include <Arduino.h>

#include "StepperMotorESP32_2s.h"

//タイマ割り込みルーチンのハンドラ(20μs間隔で起動)
void StepperMotorESP32_2s::ISRhandler() {
  if (checkrequest) {
    if (lengthCRNT <= count) {
      count = 0;
      dirCRNT = dir;
      lengthCRNT = length;
    } else if (dirCRNT == 0) {
      count = 0;
      dirCRNT = dir;
      lengthCRNT = length;
    } else if (count < length) {
      dirCRNT = dir;
      lengthCRNT = length;
    } else { //if (length <= count)
      count = 0;
      dirCRNT = dir;
      lengthCRNT = length;
    }
    checkrequest = false;
  }

  if (count == lengthCRNT) count = 0;
  if (count == 0) {
    if (0 <= dirCRNT) digitalWrite(dirpin, HIGH);
    else digitalWrite(dirpin, LOW);
    if (dirCRNT != 0) digitalWrite(steppin, HIGH);
  } else if (count == 1) {
    digitalWrite(steppin, LOW);
    portENTER_CRITICAL_ISR(&timerMux);
      if (dirCRNT == 1) PulseSUM++;
      else if (dirCRNT == -1) PulseSUM--;
    portEXIT_CRITICAL_ISR(&timerMux);
  }
  count++;
}

void StepperMotorESP32_2s::initialize(uint8_t DirPin, uint8_t StepPin, uint8_t EnablePin) {
  dirpin = DirPin;
  steppin = StepPin;
  enablepin = EnablePin;
  pinMode(dirpin, OUTPUT);
  pinMode(steppin, OUTPUT);
  pinMode(enablepin, OUTPUT);
  digitalWrite(enablepin, HIGH); //HIGH:disable; LOW:enable
  digitalWrite(DirPin, LOW);
  digitalWrite(steppin, LOW);
}

void StepperMotorESP32_2s::clear() {
  dir = 0;
  length = maxNCount;
  checkrequest = true;
  PulseSUM = 0;
}

void StepperMotorESP32_2s::setPeriods(uint32_t ControlPeriod, uint32_t MinPulsePeriod, uint32_t InterruptPeriod) {//[microsec], [microsec], [microsec]
  Controlperiod = ControlPeriod;
  MinPulseperiod = MinPulsePeriod;
  Interruptperiod = InterruptPeriod;
  minNCount = ceil((double)MinPulsePeriod / (double)Interruptperiod);
  maxNCount = ControlPeriod / Interruptperiod;
}

void StepperMotorESP32_2s::drive(double ControlValue) { // -1.0 <= ControlValue <= 1.0
    const double limit = 0.001;
    if (ControlValue < -limit) {
      dir = -1;
      length = -(int)(minNCount / ControlValue);
      if (maxNCount < length) {
        dir = 0;
        length = maxNCount;
      } else if (length < minNCount) {
        length = minNCount;
      }
    } else if (limit < ControlValue) {
      dir = 1;
      length = (int)(minNCount / ControlValue);
      if (maxNCount < length) {
        dir = 0;
        length = maxNCount;
      } else if (length < minNCount) {
        length = minNCount;
      }
    } else {
      dir = 0;
      length = maxNCount;
    }
    checkrequest = true;
}

int StepperMotorESP32_2s::getcount() {
  int sum;
  portENTER_CRITICAL(&timerMux);
  sum = PulseSUM;
  PulseSUM = 0;
  portEXIT_CRITICAL(&timerMux);
  return sum;
}


void StepperMotorESP32_2s::enableDriver()
{
  digitalWrite(enablepin, LOW);
}

void StepperMotorESP32_2s::disableDriver()
{
  digitalWrite(enablepin, HIGH);
}

6 実行の様子

個々での設定では指令値の絶対値は最大0.83程度になります。それ以上の指令値を与えてもそれ以上の短いパルスを供給で来ません。これは最小パルス長が30μsecであるためで,演算上の生ずる結果です。

motor duty = 0.00
steps = 0
motor duty = 0.20
steps = 3334
motor duty = 0.40
steps = 7143
motor duty = 0.60
steps = 10000
motor duty = 0.80
steps = 16666
motor duty = 0.60
steps = 10000
motor duty = 0.40
steps = 7143
motor duty = 0.20
steps = 3334
motor duty = 0.00
steps = 0
motor duty = -0.20
steps = -3334
motor duty = -0.40
steps = -7143
motor duty = -0.60
steps = -10000
motor duty = -0.80
steps = -16666

7 補足

タイマー割り込みでパルスを発生するため,パルス長決定の自由でが高くなりました。
しかし,指令値が0.83以上になると最小パルス長30μsecの壁のため,短いパルス周期のパルスが苦手です。(絶対値が1に近い指令値への対応が苦手です。)


8 まとめ

Xiao ESP32S3およびモータドライバA4988でステッパモータを駆動しました。
駆動では短い時間ごとにモータ指令値を変更することができるパルス生成クラスを用いて,プログラミングの負担を減らしています。
ここで使用しているパルス生成クラスでのPWM信号発生は,20μsecごとに起動するタイマ割り込みルーチンが生成します。
そのため,短いパルス間隔のパルス発生が苦手です。