Arduinoでエンコーダ付きDCモータの回転速度、回転角を制御する方法

この記事で学習できること

  • Arduino UNO R3にエンコーダ付きDCモータ(SGM25-370)を接続し、DCモータからのエンコーダ信号を使って、モータの回転速度、角度を計測する方法を学習します。また計測した値を使って目標回転速度、目標角度へPID制御する方法を学習します。
  • 通常のDCモータは電圧をかければ回転しますが、負荷の状態によって回転速度が変わってしまいます、また正確な位置で止めることも困難です。エンコーダはその課題を解決します。

エンコーダ付きDCモータ(SGM25-370)

  • エンコーダ付きDCモータ(SGM25-370)には磁気を感知するホールICを2個使っており、モータが回転するとホールICから2種類のエンコーダの信号(A相、B相)が出力されます。この信号を使って、モータの回転速度、モータの回転位置を計算できます。

SGM25-370の外観

● 全長は約74mm

SG25-370 外観
— SG25-370 外観 —

SGM25-370のコネクタピン配置

  • エンコーダ部に6ピンのコネクタが実装されており、モータ端子およびホールICに接続されています。
  • DCモータのシャフトにSNに着磁された磁石が取付られており、ホールICが磁界の変化を検出しエンコーダ信号を生成します。
エンコーダ部の外観
— エンコーダ部の外観 —
  • ホールICはホール効果による電圧変化を増幅して出力します。DCモータのシャフトに取り付けられた磁石はSNを交互に着磁してあり、回転に応じた磁界の変化を出力します。
  • 下記は、Honeywell社のホールICであるSS400シリーズの仕様書の一部です。
  • ホールICの出力はオープンコレクタになっています。エンコーダ部のPCBにはプルアップ抵抗が実装してあり、シャフトに取り付けられた磁石が回転すれば、コネクタからは方形波が出力されます。
ホールICの外観とブロック図
— ホールICの外観とブロック図 —

SGM25-370のエンコーダ信号

  • DCモータの回転すると角度に応じてA相、B相からエンコーダ信号が出力されます。
    • パルス数と時間を計測することで回転速度を計算できます。
  • DCモータの回転方向によってA相とB相の位相が変わります。
    • A相エンコーダ信号の立上がり、立下り時にB相エンコーダ信号がHiかLoかを計測すると回転方向が解ります。時計方向回転でエンコーダ信号を加算と反時計方向回転でエンコーダ信号を減算すればモータの回転位置が計算できます。
  • ホールICから出力されるA相とB相のエンコーダ信号をオシロスコープで観察しました。A相、B相ともに方形波で出力されています。また、DCモータを時計方向と反時計方向に回転した場合で、A相とB相の位相がずれているのが解ります。
  • A相とB相の出力の数から、DCモータの回転数が解ります。
  • A相の立上がりでB相がHiかLoで回転方向が解ります。
エンコーダー出力 DCモータ時計方向回転時
— エンコーダー信号 DCモータ時計方向回転時 —
エンコーダー出力 DCモータ反時計方向回転時
— エンコーダー信号 DCモータ反時計方向回転時 —

DCモータ(SGM25-370) DC:6V RPM: 130の仕様

諸元特性/仕様
定格電圧6V
回転方向時計方向/反時計方向
シャフト径φ4mm (D外形部 3.5mm)
シャフト長さ12mm (D外形部 8mm)
重量約 100g
ギヤ比 (減速比)45:1
エンコーダーホールエフェクト式の磁気エンコーダ2相(A相・B相)
エンコーダ出力モーター1回転あたり 11 パルス

● エンコーダ信号のパルス数はギア出力を通すと 11 × 減速比(45:1) ≒ 495 パルス/回転 となります。

DCモータ(SGM25-370)を駆動する部品

部品機能個数
Arduino UNO R3CPUボード1 個
SGM25-370エンコーダ付きDC モータ1 個
TB6612FNGのDIP化基板 モータードライバ1 個
7.4V バッテリーArduino、モータ用電源1 個
ブレッドボード、ジャンパ線配線用部材(必要数)
  • 回転の制御用に赤外線リモコンを使います。赤外線受信用のICを取り付けます。また、デバッグ用に表示用のLEDを1個取り付けます。
部品機能個数
VB1838B赤外線受光素子1 個
LED状態表示1 個
抵抗 (10KΩ)LED電流制限1 個

DCモータ(SGM25-370)動作確認用回路の部品接続

  • SGM25-370とArduino UNO R3、TB6612FNG(DIP化基板)の接続
SGM25-370接続先端子
モーター端子TB6612FNGAO1
GNDArduino UNO R3GND
ホール素子 A相Arduino UNO R3Digital 2ピン
ホール素子 B相Arduino UNO R3Digital 12ピン
VccArduino UNO R35V
モーター端子TB6612FNGAO2
  • TB6612FNGのDIP化基板、Arduino UNO R3、モーター用電池(6V)の接続
TB6612FNG(DIP化基板)接続先端子
PWMAArduino UNO R3Digital 7ピン
AIN1Arduino UNO R3Digital 5ピン
AIN2Arduino UNO R3Digital 6ピン
VCCArduino UNO R35V
GNDArduino UNO R3とドライバ電源GND
STBYArduino UNO R3Digital 11ピン
VMArduino UNO R3Vin
  • Arduino UNO R3と赤外線リモコン受光素子、状態表示用LEDの接続
部品接続先端子
VB1838B(赤外線受光素子)Arduino UNO R3Analog 5ピン
LED (アノード側)Arduino UNO R3Analog 4ピン

DCモータ(SGM25-370)動作確認用回路

  • Arduino UNO R3と使用する部品の接続図です。
— SGM25-370 駆動回路 —

エンコーダ信号を処理する関数

Arduinoの割り込み処理機能

  • Arduino UNO R3には、割込み関数を呼び出すトリガー信号を受けることのできる割込み信号用ピンがあります。このピンにエンコーダ信号を入力しパルス数を計数します。専用のポートはデジタルポートの、2または3ピンになります。(割り込みポートの0または1ピンになります。ピン番号が紛らわしいので注意が必要です。) スケッチに割込み関数を呼び出す設定が準備されており書式に従って記述すれば割込みで信号処理が可能です。
  • 一般的に割込み関数の中の処理は、メインの処理の中断を避けるためにできるだけ少なくすべきです。
  • 割り込みを有効にする関数は、attachInterrupt(digitalPinToInterrupt(pin), isr, mode) です。
  • digitalPinToInterrupt(pin),は割込み処理に使えるピン番号です。Arduino UNO R3ではデジタルポートの、2または3ピンになります。
  • isr,に割込み関数の名前を設定します。割込み関数はvoid型でなければなりません。
  • mode、で波形のどのような状態で割り込みをするかの設定になります。RISINGの場合は、信号が立ち上がった時に割り込みが発生します。CHANGEの場合は信号が変化したときに割り込みを発生させます。
attachInterrupt(0,isr,RISING)
  • detachInterrupt(digitalPinToInterrupt(pin))関数を使って、割り込みを無効にする設定をします。割り込みで取得した値を使って計算する期間は、割り込みが発生しても、割り込み処理をしないようにします。
detachInterrupt(digitalPinToInterrupt(0));
  • 割込み関数をstr()として定義した例です。割込み関数はvoid型でなければなりません。
void isr()     

割り込み関数を使ってエンコーダの波形を計数する具体例

  • エンコーダ出力を割込み関数を利用して計数する処理に係るスケッチコードの具体例です。
// ----- ピン定義 -----
const int PIN_ENCODER_A = 2;    // 右モータ エンコーダ A相 (INT0)
const int PIN_ENCODER_B = 12;   // 右モータ エンコーダ B相 (配線表の左モータ表記箇所)

// ----- 定数・変数定義 -----
volatile long encoderCount = 0; // パルスカウント用 (加減算対応)

// パルスカウント用割り込みサービスルーチン (ISR)
// A相の立ち上がり時にB相の状態を見て回転方向を判別
void countEncoder() {
  if (digitalRead(PIN_ENCODER_B) == LOW) {
    encoderCount++; // 時計方向(正転):加算
  } else {
    encoderCount--; // 反時計方向(逆転):減算
  }
}
  • エンコーダのA相を割込み機能を持つArduino UNO R3のデジタル2ピンに割り当てます。
  • エンコーダのB相をArduino UNO R3のデジタル12ピンに割り当てます。
  • attachInterrupt(),関数に、ピンを割り当て、割込み関数handleEncoderAを指定し、エンコーダ出力の立上がりで割込み関数を呼び出すように設定します。
  • 割込み関数countPulse()が呼ばれたら、A相、B相の位相を判定しencoderCountを加減算します。
  • この処理によってエンコーダA相の波形の立ち上がりごとに、handleEncoderA()が呼ばれて変数encoderCountのエンコーダ信号数を加減算します。
(参考) 割込み機能を使って回転数を測定した記事

割込み関数を使っての連続サーボモータの回転数を測定しました。記事 [転連続回転サーボモーターの回転速度を測定する方法]にまとめています。

回転速度と回転角度の演算方法

  • エンコ-ダ信号を検出できれば、DCモータの回転速度を計算することは難しくはありません。信号の数と、信号を数えた時間が解れば計算できます。
エンコ-ダ信号積算値から回転速度を計算する方法
— エンコ-ダ信号積算値から回転速度を計算する方法 —
  • エンコ-ダ信号積算値はDCモータの回転方向に合わせて増加若しくは減少します。初期の位置でエンコーダ信号の積算値を’0’に設定すれば、DC-モータが回転しても初期位置に戻れば、パルスの積算値は’0’になります。1回転のパルス数が解っていれば、パルスの積算値をその数で割った余りから回転位置が解ります。
エンコ-ダ信号積算値から回転角を計算する方法
— エンコ-ダ信号積算値から回転角を計算する方法 —
  • 例えば、DCモータの1回転のエンコーダ信号の数が495である場合にモータが回転してエンコーダ信号の積算値が1000になったとします。積算値が正なので、時計方向に回転していると解ります。1000を495で割ると、2余り10になりますから、2回転と10/360度回転していることが解ります。
  • DCモータが車輪に接続されていて、車輪の半径を30mmとすると、移動距離は、1周の距離は、2*半径*円周率なので、エンコーダ信号の積算値が1000の場合は、2*30*π (2 + 10/360) ≒ 382 mm となります。

回転速度と回転角度の演算例

回転速度の測定例

  • 時計方向と反時計方向でそれぞれPWMの設定を32ステップ刻みで変化させ10秒間エンコーダ信号数を計測し回転速度(RPM)を計算します。
// PWMを変化させPRMを測定する

// ----- ピン定義 -----
const int PIN_ENCODER_A = 2;    // 右モータ エンコーダ A相 (INT0)
const int PIN_ENCODER_B = 12;   // 右モータ エンコーダ B相 (配線表の左モータ表記箇所)
const int PIN_AIN1      = 4;    // TB6612 AIN1 (回転方向制御 1)
const int PIN_AIN2      = 5;    // TB6612 AIN2 (回転方向制御 2)
const int PIN_PWMA      = 6;    // TB6612 PWMA (速度制御 PWM)
const int PIN_STBY      = 11;   // TB6612 STBY (スタンバイ)
const int PIN_IR        = A0;   // IRセンサ (入力設定のみ)
const int PIN_LED       = A1;   // ステータス用 LED

// ----- 定数・変数定義 -----
const float PULSES_PER_REV = 495.0; // 1回転あたりのエンコーダパルス数
const unsigned long MEASURE_TIME_MS = 10000; // 測定時間 (10秒)

volatile long encoderCount = 0; // パルスカウント用 (加減算対応)

// パルスカウント用割り込みサービスルーチン (ISR)
// A相の立ち上がり時にB相の状態を見て回転方向を判別
void countEncoder() {
  if (digitalRead(PIN_ENCODER_B) == LOW) {
    encoderCount++; // 時計方向(正転):加算
  } else {
    encoderCount--; // 反時計方向(逆転):減算
  }
}

// モータ駆動ヘルパー関数
// speed: -255 〜 255 (正: 時計方向, 負: 反時計方向, 0: 停止)
void setMotorSpeed(int speed) {
  if (speed > 0) {
    digitalWrite(PIN_AIN1, HIGH);
    digitalWrite(PIN_AIN2, LOW);
    analogWrite(PIN_PWMA, speed);
  } else if (speed < 0) {
    digitalWrite(PIN_AIN1, LOW);
    digitalWrite(PIN_AIN2, HIGH);
    analogWrite(PIN_PWMA, -speed);
  } else {
    digitalWrite(PIN_AIN1, LOW);
    digitalWrite(PIN_AIN2, LOW);
    analogWrite(PIN_PWMA, 0);
  }
}

// 測定関数
void runTestSequence(const char* directionName, bool isClockwise) {
  Serial.print("=== テスト開始: ");
  Serial.print(directionName);
  Serial.println(" ===");
  Serial.println("PWM\tカウント値\tRPM");

  // 32ステップ刻みでPWMを変化 (0, 32, 64, 96, 128, 160, 192, 224, 255)
  for (int step = 0; step <= 8; step++) {
    int pwmVal = step * 32;
    if (pwmVal > 255) pwmVal = 255;

    // LED点灯 (測定中表示)
    digitalWrite(PIN_LED, HIGH);

    // エンコーダカウント初期化
    noInterrupts();
    encoderCount = 0;
    interrupts();

    // モータ回転開始
    int motorCmd = isClockwise ? pwmVal : -pwmVal;
    setMotorSpeed(motorCmd);

    // 10秒間計測
    delay(MEASURE_TIME_MS);

    // モータ一時停止 & カウント取得
    setMotorSpeed(0);
    noInterrupts();
    long currentCount = encoderCount;
    interrupts();

    // LED消灯
    digitalWrite(PIN_LED, LOW);

    // RPMの計算(カウント値の正負に対応するため abs を使用して絶対値換算)
    // (カウント数 / 495) = 回転数(10秒間)
    // 10秒間の回転数 * 6 = 1分あたりの回転数(RPM)
    float rpm = (float(labs(currentCount)) / PULSES_PER_REV) * 6.0;

    // シリアル出力 (カウント値は負の値のまま表示)
    Serial.print(pwmVal);
    Serial.print("\t");
    Serial.print(currentCount);
    Serial.print("\t\t");
    Serial.println(rpm, 2); // 小数点以下2桁表示

    // ステップ間のインターバル (1秒)
    delay(1000);
  }
  Serial.println();
}

void setup() {
  Serial.begin(115200);

  // ピンモード設定
  pinMode(PIN_ENCODER_A, INPUT_PULLUP);
  pinMode(PIN_ENCODER_B, INPUT_PULLUP);
  pinMode(PIN_AIN1, OUTPUT);
  pinMode(PIN_AIN2, OUTPUT);
  pinMode(PIN_PWMA, OUTPUT);
  pinMode(PIN_STBY, OUTPUT);
  pinMode(PIN_IR, INPUT);
  pinMode(PIN_LED, OUTPUT);

  // モータドライバを動作許可状態にする
  digitalWrite(PIN_STBY, HIGH);
  digitalWrite(PIN_LED, LOW);

  // 外部割り込みの有効化 (D2ピンの立ち上がりエッジで割込発生)
  attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_A), countEncoder, RISING);

  Serial.println("SGM25 Test スケッチが起動しました。");
  delay(2000);

  // --- 1) 時計方向 (CW) のテスト ---
  runTestSequence("時計方向 (CW)", true);

  // 方向転換前のインターバル (2秒)
  delay(2000);

  // --- 2) 反時計方向 (CCW) のテスト ---
  runTestSequence("反時計方向 (CCW)", false);

  Serial.println("すべてのテストが完了しました。");
}

void loop() {
  // テスト完了後は待機状態(LED低速点滅)
  digitalWrite(PIN_LED, HIGH);
  delay(500);
  digitalWrite(PIN_LED, LOW);
  delay(500);
}
  • シリアルモニタの出力例です。
  • シリアルモニタに、PWM値、エンコーダ信号積算数、そしてRPMを出力しています。モータの仕様は495パルスでシャフトが1回転します。測定は10秒間のカウント数ですから、このカウント数を6倍して495で割ればRPM値になります。
  • 時計方向と反時計方向で同じPWM値でも回転数に違いがあることが解ります。この場合、このモータに車輪を取り付けて同じPWM値で回転させても前方移動と後方移動では速度が異なります。
=== テスト開始: 時計方向 (CW) ===
PWM	カウント値	RPM
0	0		0.00
32	1374		16.65
64	2897		35.12
96	4429		53.68
128	5955		72.18
160	7505		90.97
192	8931		108.25
224	10507		127.36
255	11992		145.36

=== テスト開始: 反時計方向 (CCW) ===
PWM	カウント値	RPM
0	0		0.00
32	-1355		16.42
64	-2865		34.73
96	-4381		53.10
128	-5953		72.16
160	-7423		89.98
192	-8929		108.23
224	-10407		126.15
255	-11835		143.45

回転角度の計測例

  • PWMの設定を10秒周期sin波形状に変化させます。エンコーダ信号を計数し、それぞれの変化をシリアルプロッタに表示します。
// PWMをsin()変化させ回転角度を測定する

#include <Arduino.h>

// ----- ピン定義 -----
const int PIN_ENCODER_A = 2;    // 右モータ エンコーダ A相 (INT0)
const int PIN_ENCODER_B = 12;   // 右モータ エンコーダ B相
const int PIN_AIN1      = 4;    // TB6612 AIN1 (回転方向制御 1)
const int PIN_AIN2      = 5;    // TB6612 AIN2 (回転方向制御 2)
const int PIN_PWMA      = 6;    // TB6612 PWMA (速度制御 PWM)
const int PIN_STBY      = 11;   // TB6612 STBY (スタンバイ)
const int PIN_IR        = A0;   // IRセンサ (入力設定)
const int PIN_LED       = A1;   // ステータス用 LED

// ----- 定数・設定変数 -----
const float PULSES_PER_REV = 495.0; // 1回転あたりのエンコーダパルス数
const float SIN_PERIOD_MS  = 10000.0; // sin波の周期 (10秒 = 10000ms)
const int   MAX_PWM_AMPLITUDE = 128;   // PWMの最大振幅

// シリアルプロッタ出力の間隔 (ms) ※変数で変更可能
const unsigned long PLOT_INTERVAL_MS = 500; 

// ----- 内部変数 -----
volatile long encoderCount = 0; // パルスカウント用 (正転: +, 逆転: -)
unsigned long startTime = 0;
unsigned long lastPlotTime = 0;

// エンコーダパルスカウント用割り込みサービスルーチン (ISR)
void countEncoder() {
  if (digitalRead(PIN_ENCODER_B) == LOW) {
    encoderCount++; // 正転(時計方向):加算
  } else {
    encoderCount--; // 逆転(反時計方向):減算
  }
}

// モータ駆動関数
// speed: -128 〜 128 (正: 時計方向, 負: 反時計方向, 0: 停止)
void setMotorSpeed(int speed) {
  if (speed > 0) {
    digitalWrite(PIN_AIN1, HIGH);
    digitalWrite(PIN_AIN2, LOW);
    analogWrite(PIN_PWMA, speed);
  } else if (speed < 0) {
    digitalWrite(PIN_AIN1, LOW);
    digitalWrite(PIN_AIN2, HIGH);
    analogWrite(PIN_PWMA, -speed);
  } else {
    digitalWrite(PIN_AIN1, LOW);
    digitalWrite(PIN_AIN2, LOW);
    analogWrite(PIN_PWMA, 0);
  }
}

void setup() {
  // ボーレート 115200 bps
  Serial.begin(115200);

  // ピンモード設定
  pinMode(PIN_ENCODER_A, INPUT_PULLUP);
  pinMode(PIN_ENCODER_B, INPUT_PULLUP);
  pinMode(PIN_AIN1, OUTPUT);
  pinMode(PIN_AIN2, OUTPUT);
  pinMode(PIN_PWMA, OUTPUT);
  pinMode(PIN_STBY, OUTPUT);
  pinMode(PIN_IR, INPUT);
  pinMode(PIN_LED, OUTPUT);

  // モータドライバ有効化 & ステータスLED点灯
  digitalWrite(PIN_STBY, HIGH);
  digitalWrite(PIN_LED, HIGH);

  // 外部割り込み有効化 (D2ピンの立ち上がりエッジで割込発生)
  attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_A), countEncoder, RISING);

  // シリアルプロッタ用ラベル出力 (必要に応じて自動スケール用)
  Serial.println("PWM_Cmd,Encoder_Count,Angle_Deg");

  startTime = millis();
}

void loop() {
  unsigned long currentTime = millis();
  unsigned long elapsedTime = currentTime - startTime;

  // 1) 10秒周期の sin波で PWM指令値を計算 (振幅: -128 〜 +128)
  // 経過時間 (ms) から角度 rad を求める: (elapsedTime / 10000.0) * 2 * PI
  float radiansVal = (float)elapsedTime / SIN_PERIOD_MS * 2.0 * PI;
  int pwmCmd = -round(sin(radiansVal) * MAX_PWM_AMPLITUDE);

  // モータに出力
  setMotorSpeed(pwmCmd);

  // 2) 指定間隔 (PLOT_INTERVAL_MS) ごとにシリアルプロッタ/モニタに出力
  if (currentTime - lastPlotTime >= PLOT_INTERVAL_MS) {
    lastPlotTime = currentTime;

    // 割込保護してカウント値を取得
    noInterrupts();
    long currentCount = encoderCount;
    interrupts();

    // 回転角度の計算 (+/- 表示)
    float angleDeg = (float)currentCount / PULSES_PER_REV * 360.0;

// シリアルプロッタ用出力フォーマット (ラベル名:値)
    Serial.print("PWM_Cmd:");
    Serial.print(pwmCmd);
    Serial.print(",");
    Serial.print("Encoder_Count:");
    Serial.print(currentCount);
    Serial.print(",");
    Serial.print("Angle_Deg:");
    Serial.println(angleDeg, 2);
  }
}
  • シリアルプロッタの出力例です。
  • PWM値、エンコーダ信号の積算値、エンコーダの積算値を角度に変換した値(2回転した場合は、360*2 = 720のような表示にしています。)を表示しています。
  • PWM値の変化から遅れてエンコーダ信号積算値と回転角度が変動しているのが解ります。
  • エンコーダ信号積算値と角度が時間と共にプラス側にシフトしています。DCモータのバラツキ(回転の原点のズレ、時計方向と反時計方向の回転の差)による現象です。
— PWM 値とエンコーダ積算値の変化 —

回転速度と回転角の制御方法

  • DCモータをPWM駆動で回転させた時の回転速度と角度はエンコーダ信号から計算できます。しかしDCモータをある回転速度や角度で制御したい場合は、目標となる値と現在の値を比較して、その誤差が小さくなるようにPWM出力変化させる手順が必要です。このような目標値と実際の値の差を使った制御はフィードバック制御と呼ばれ、エアコンの温度制御や自動車のクルーズコントロールの速度制御などで使われます。フィードバック制御にはオンオフ制御(2位置制御)やPID制御などがあります。フィードバック制御をWebで検索すると多くの記事が見つかります。
  • DCモータの回転数や回転角を目標値としてPID制御を行い回転数や回転角がどのように変化するかを測定します。

回転速度と回転角の制御例

回転速度の制御例

  • 回転速度(RPM)を目標値としてPID制御します。
  • 目標値の回転速度(RPM)は赤外線リモコンで設定します。’2’を押すと10ずつ値を上げます。’8’を押すと10ずつ値を下げます。’5’を押すと目標値を’0’とし回転を停止します。
// RPM値を目標設定しPID制御で収束させる 

#include <Arduino.h>
#include <IRremote.hpp> // IRremoteライブラリ (v3.0以降対応)

// ----- ピン定義 -----
const int PIN_ENCODER_A = 2;    // 右モータ エンコーダ A相 (INT0)
const int PIN_ENCODER_B = 12;   // 右モータ エンコーダ B相
const int PIN_AIN1      = 4;    // TB6612 AIN1 (回転方向制御 1)
const int PIN_AIN2      = 5;    // TB6612 AIN2 (回転方向制御 2)
const int PIN_PWMA      = 6;    // TB6612 PWMA (速度制御 PWM)
const int PIN_STBY      = 11;   // TB6612 STBY (スタンバイ)
const int PIN_IR        = A0;   // IRセンサ
const int PIN_LED       = A1;   // ステータス用 LED

// ----- 赤外線リモコンのキーコード定義 -----
#define IR_KEY_2 0x18  // RPM +10
#define IR_KEY_8 0x52  // RPM -10
#define IR_KEY_5 0x1C  // RPM 0 (即時停止)

// ----- エンコーダ & 物理定数 -----
const float PULSES_PER_REV = 495.0; // 立ち上がりのみ(RISING)割込用

// ----- PID制御ゲインパラメータ -----
float Kp = 1.2;   // 比例ゲイン
float Ki = 0.8;   // 積分ゲイン
float Kd = 0.0;  // 微分ゲイン

// ----- 制御用変数 -----
volatile long encoderCount = 0; // 累積パルスカウント
long lastEncoderCount = 0;

float targetRPM = 0.0;   // 目標RPM (-80 〜 +80)
float currentRPM = 0.0;  // 現在の測定RPM (回転速度測定値)
int   currentPWM = 0;    // モータへの出力PWM値 (-255 〜 255)

float errorSum = 0.0;
float lastError = 0.0;

// ----- 基準時間変数 -----
unsigned long pidLastTime = 0;
unsigned long plotLastTime = 0;
unsigned long pwmSetTime = 0; 

// 出力間隔 (ms)
const unsigned long PLOT_INTERVAL_MS = 100;
const unsigned long PID_INTERVAL_MS = 50;

// エンコーダパルスカウント用割り込みサービスルーチン (ISR: 立ち上がりのみ)
void countEncoder() {
  if (digitalRead(PIN_ENCODER_B) == LOW) {
    encoderCount++; // 正転
  } else {
    encoderCount--; // 逆転
  }
}

// モータ駆動ヘルパー関数
void setMotorSpeed(int speed) {
  speed = constrain(speed, -255, 255);
  
  if (speed > 0) {
    digitalWrite(PIN_AIN1, HIGH);
    digitalWrite(PIN_AIN2, LOW);
    analogWrite(PIN_PWMA, speed);
  } else if (speed < 0) {
    digitalWrite(PIN_AIN1, LOW);
    digitalWrite(PIN_AIN2, HIGH);
    analogWrite(PIN_PWMA, -speed);
  } else {
    // 停止(両ピンLOW & PWM 0)
    digitalWrite(PIN_AIN1, LOW);
    digitalWrite(PIN_AIN2, LOW);
    analogWrite(PIN_PWMA, 0);
  }
}

void setup() {
  Serial.begin(115200);

  // ピン設定
  pinMode(PIN_ENCODER_A, INPUT_PULLUP);
  pinMode(PIN_ENCODER_B, INPUT_PULLUP);
  pinMode(PIN_AIN1, OUTPUT);
  pinMode(PIN_AIN2, OUTPUT);
  pinMode(PIN_PWMA, OUTPUT);
  pinMode(PIN_STBY, OUTPUT);
  pinMode(PIN_LED, OUTPUT);

  // モータドライバ有効化
  digitalWrite(PIN_STBY, HIGH);
  digitalWrite(PIN_LED, HIGH);

  // 赤外線受信開始
  IrReceiver.begin(PIN_IR, DISABLE_LED_FEEDBACK);

  // 外部割り込み有効化 (RISING)
  attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_A), countEncoder, RISING);

  pwmSetTime = millis();
  pidLastTime = millis();
  plotLastTime = millis();
}

void loop() {
  unsigned long now = millis();

  // --------------------------------------------------
  // 1) 赤外線リモコン受信処理
  // --------------------------------------------------
  if (IrReceiver.decode()) {
    if (!(IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT)) {
      uint16_t command = IrReceiver.decodedIRData.command;

      if (command == IR_KEY_2) {
        targetRPM += 10.0;
        if (targetRPM > 80.0) targetRPM = 80.0;
        pwmSetTime = now;
      } else if (command == IR_KEY_8) {
        targetRPM -= 10.0;
        if (targetRPM < -80.0) targetRPM = -80.0;
        pwmSetTime = now;
      } else if (command == IR_KEY_5) {
        // ボタン5:回転即時停止処理
        targetRPM = 0.0;
        currentPWM = 0;
        errorSum = 0.0;    // 積分項のリセット
        lastError = 0.0;   // 微分項のリセット
        setMotorSpeed(0);  // 即時停止
        pwmSetTime = now;
      }
    }
    IrReceiver.resume();
  }

// --------------------------------------------------
  // 2) PID制御演算 (周期: PID_INTERVAL_MS)
  // --------------------------------------------------
  if (now - pidLastTime >= PID_INTERVAL_MS) {
    float dt = (now - pidLastTime) / 1000.0; // 秒単位
    pidLastTime = now;

    // 現在のカウントを取得 (割込保護)
    noInterrupts();
    long currentCount = encoderCount;
    interrupts();

    // 周期内でのパルス変化量から RPM を計算
    long deltaCount = currentCount - lastEncoderCount;
    lastEncoderCount = currentCount;

    // 生のRPM計算
    float rawRPM = ((float)deltaCount / PULSES_PER_REV) * (60.0 / dt);
    
    // 一次ローパスフィルタ (前回70% + 今回30%)
    currentRPM = (currentRPM * 0.7) + (rawRPM * 0.3);

    if (targetRPM == 0.0) {
      currentPWM = 0;
      errorSum = 0.0;
      lastError = 0.0;
      setMotorSpeed(0);
    } else {
      // 偏差の計算
      float error = targetRPM - currentRPM;

      // 積分項の加算 (Ki を 0.5〜1.0 に上げて速やかに追従させる)
      errorSum += error * dt;

      // errorSum 自体ではなく、積分出力(Ki * errorSum)の段階で制限をかける
      float pTerm = Kp * error;
      float iTerm = Ki * errorSum;
      iTerm = constrain(iTerm, -255.0, 255.0); // 積分出力をPWM全域許可
      
      float dError = (error - lastError) / dt;
      float dTerm = Kd * dError;
      lastError = error;

      // PID出力計算
      float output = pTerm + iTerm + dTerm;
      
      // 出力PWMの制限 (-255 〜 255)
      currentPWM = constrain(round(output), -255, 255);

      // モータにPWM出力
      setMotorSpeed(currentPWM);
    }
  }

  // --------------------------------------------------
  // 3) シリアルプロッタ / モニタ出力 (間引き出力)
  // --------------------------------------------------
  if (now - plotLastTime >= PLOT_INTERVAL_MS) {
    plotLastTime = now;

    unsigned long elapsedTime = now - pwmSetTime;

    // シリアルプロッタ用KeyValueフォーマット出力
    Serial.print("ElapsedTime_ms:");
    Serial.print(elapsedTime);
    Serial.print(",");
    Serial.print("Target_RPM:");
    Serial.print(targetRPM);
    Serial.print(",");
    Serial.print("PWM_Cmd:");
    Serial.print(currentPWM);
    Serial.print(",");
    Serial.print("Measured_RPM:");
    Serial.println(currentRPM, 2); // 回転角度のかわりに回転速度測定値を表示
  }
}
  • シリアルモニタの出力例です。
  • 赤外線リモコンの’2’のボタンを押して目標角度を50度にした時のRPMの実測値とPWM値のです。DCモータ1回転(365度)でエンコーダ信号数は495個ですから、1パルスの誤差が +/- 360/495 ≒ +/- 0.73 度になります。
  • 回転速度(RPM)の目標値が50RPMの時のRPMの実測値です。目標値の回転数付近で制御されています。
ElapsedTime_ms:20942,Target_RPM:50.00,PWM_Cmd:88,Measured_RPM:50.16
ElapsedTime_ms:21042,Target_RPM:50.00,PWM_Cmd:88,Measured_RPM:50.03
ElapsedTime_ms:21142,Target_RPM:50.00,PWM_Cmd:88,Measured_RPM:49.97
ElapsedTime_ms:21242,Target_RPM:50.00,PWM_Cmd:89,Measured_RPM:49.44
ElapsedTime_ms:21342,Target_RPM:50.00,PWM_Cmd:88,Measured_RPM:50.19
ElapsedTime_ms:21442,Target_RPM:50.00,PWM_Cmd:88,Measured_RPM:49.83
ElapsedTime_ms:21542,Target_RPM:50.00,PWM_Cmd:88,Measured_RPM:49.65
ElapsedTime_ms:21642,Target_RPM:50.00,PWM_Cmd:88,Measured_RPM:50.29
ElapsedTime_ms:21742,Target_RPM:50.00,PWM_Cmd:88,Measured_RPM:50.10
ElapsedTime_ms:21842,Target_RPM:50.00,PWM_Cmd:88,Measured_RPM:49.78
ElapsedTime_ms:21942,Target_RPM:50.00,PWM_Cmd:88,Measured_RPM:49.63
ElapsedTime_ms:22042,Target_RPM:50.00,PWM_Cmd:88,Measured_RPM:50.28
ElapsedTime_ms:22142,Target_RPM:50.00,PWM_Cmd:88,Measured_RPM:49.87

回転角の制御例#1

  • 回転角(度)を目標値としてPID制御します。
  • 目標値の回転角(度)は赤外線リモコンで設定します。’2’を押すと30度ずつ値を上げます。’8’を押すと30度ずつ値を下げます。’5’を押すと目標値を0度とし回転を停止します。

// 回転角度を目標設定しPID制御で収束させる

#include <Arduino.h>
#include <IRremote.hpp> // IRremoteライブラリ (v3.0以降対応)

// ----- ピン定義 -----
const int PIN_ENCODER_A = 2;    // 右モータ エンコーダ A相 (INT0)
const int PIN_ENCODER_B = 12;   // 右モータ エンコーダ B相
const int PIN_AIN1      = 4;    // TB6612 AIN1 (回転方向制御 1)
const int PIN_AIN2      = 5;    // TB6612 AIN2 (回転方向制御 2)
const int PIN_PWMA      = 6;    // TB6612 PWMA (速度制御 PWM)
const int PIN_STBY      = 11;   // TB6612 STBY (スタンバイ)
const int PIN_IR        = A0;   // IRセンサ
const int PIN_LED       = A1;   // ステータス用 LED

// ----- 赤外線リモコンのキーコード定義 (お使いのリモコンのコードに差し替えてください) -----
#define IR_KEY_2 0x18  // 目標角度 +30 deg
#define IR_KEY_8 0x52  // 目標角度 -30 deg
#define IR_KEY_5 0x1C  // 目標角度 0 deg (即時停止)

// ----- エンコーダ & 物理定数 -----
// 立ち上がりのみ(RISING)割込のため 495 パルス/回転
const float PULSES_PER_REV = 495.0; 

// ----- PID制御ゲインパラメータ (位置制御用調整値) -----
float Kp = 3.5;   // 比例ゲイン(角度差に応じて強く回す)
float Ki = 0.05;  // 積分ゲイン(静止前の微妙な隙間・摩擦を補正)
float Kd = 0.0;   // 微分ゲイン(目標直前でブレーキをかけてオーバーシュートを防止)

// ----- 制御用変数 -----
volatile long encoderCount = 0; // 累積パルスカウント

float targetAngle = 0.0;   // 目標回転角度 (-360.0 〜 +360.0 deg)
float currentAngle = 0.0;  // 現在の測定回転角度 (deg)
int   currentPWM = 0;    // モータへの出力PWM値 (-255 〜 255)

float errorSum = 0.0;
float lastError = 0.0;

// ----- 基準時間変数 -----
unsigned long pidLastTime = 0;
unsigned long plotLastTime = 0;
unsigned long pwmSetTime = 0; // 目標角度が変更されてからの経過時間計算用

// シリアルプロッタ出力の間隔 (ms)
const unsigned long PLOT_INTERVAL_MS = 100;
// PID演算の割り込み周期 (ms) - 位置制御のため20ms周期で高速演算
const unsigned long PID_INTERVAL_MS = 20;

// エンコーダパルスカウント用割り込みサービスルーチン (ISR: 立ち上がりのみ)
void countEncoder() {
  if (digitalRead(PIN_ENCODER_B) == LOW) {
    encoderCount++; // 正転(時計方向):加算
  } else {
    encoderCount--; // 逆転(反時計方向):減算
  }
}

// モータ駆動ヘルパー関数
void setMotorSpeed(int speed) {
  speed = constrain(speed, -255, 255);
  
  if (speed > 0) {
    digitalWrite(PIN_AIN1, HIGH);
    digitalWrite(PIN_AIN2, LOW);
    analogWrite(PIN_PWMA, speed);
  } else if (speed < 0) {
    digitalWrite(PIN_AIN1, LOW);
    digitalWrite(PIN_AIN2, HIGH);
    analogWrite(PIN_PWMA, -speed);
  } else {
    // 強制停止(両ピンLOW & PWM 0 でブレーキ)
    digitalWrite(PIN_AIN1, LOW);
    digitalWrite(PIN_AIN2, LOW);
    analogWrite(PIN_PWMA, 0);
  }
}

void setup() {
  Serial.begin(115200);

  // ピン設定
  pinMode(PIN_ENCODER_A, INPUT_PULLUP);
  pinMode(PIN_ENCODER_B, INPUT_PULLUP);
  pinMode(PIN_AIN1, OUTPUT);
  pinMode(PIN_AIN2, OUTPUT);
  pinMode(PIN_PWMA, OUTPUT);
  pinMode(PIN_STBY, OUTPUT);
  pinMode(PIN_LED, OUTPUT);

  // モータドライバ有効化
  digitalWrite(PIN_STBY, HIGH);
  digitalWrite(PIN_LED, HIGH);

  // 赤外線受信開始
  IrReceiver.begin(PIN_IR, DISABLE_LED_FEEDBACK);

  // 外部割り込み有効化 (立ち上がりのみ: RISING)
  attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_A), countEncoder, RISING);

  pwmSetTime = millis();
  pidLastTime = millis();
  plotLastTime = millis();
}

void loop() {
  unsigned long now = millis();

  // --------------------------------------------------
  // 1) 赤外線リモコン受信処理
  // --------------------------------------------------
  if (IrReceiver.decode()) {
    if (!(IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT)) {
      uint16_t command = IrReceiver.decodedIRData.command;

      if (command == IR_KEY_2) {
        targetAngle += 30.0;
        if (targetAngle > 360.0) targetAngle = 360.0;
        pwmSetTime = now;
      } else if (command == IR_KEY_8) {
        targetAngle -= 30.0;
        if (targetAngle < -360.0) targetAngle = -360.0;
        pwmSetTime = now;
      } else if (command == IR_KEY_5) {
        // --- ボタン5:目標角度を0にし回転を強制停止 ---
        targetAngle = 0.0;
        currentPWM = 0;
        errorSum = 0.0;    // 積分値リセット
        lastError = 0.0;   // 微分値リセット
        setMotorSpeed(0);  // 即時ブレーキ出力
        pwmSetTime = now;
      }
    }
    IrReceiver.resume();
  }

  // --------------------------------------------------
  // 2) 位置PID制御演算 (周期: PID_INTERVAL_MS)
  // --------------------------------------------------
  if (now - pidLastTime >= PID_INTERVAL_MS) {
    float dt = (now - pidLastTime) / 1000.0; // 秒単位
    pidLastTime = now;

    // 現在のカウントを取得 (割込保護)
    noInterrupts();
    long currentCount = encoderCount;
    interrupts();

    // カウント数から現在の回転角度 (deg) を算出
    currentAngle = ((float)currentCount / PULSES_PER_REV) * 360.0;

    // 目標が0で静止指示が出ている場合
    if (targetAngle == 0.0 && abs(currentAngle) < 1.0) {
      currentPWM = 0;
      setMotorSpeed(0);
    } else {
      // 角度偏差の計算 (目標角度 - 現在角度)
      float error = targetAngle - currentAngle;

      // 積分項の計算 (静止間際の摩擦補正用)
      errorSum += error * dt;
      errorSum = constrain(errorSum, -500.0, 500.0);

      // 微分項の計算 (行き過ぎを抑えるダンパー効果)
      float dError = (error - lastError) / dt;
      lastError = error;

      // PID出力計算
      float pTerm = Kp * error;
      float iTerm = Ki * errorSum;
      float dTerm = Kd * dError;

      float output = pTerm + iTerm + dTerm;
      
      // PWM出力制限 (-255 〜 255)
      currentPWM = constrain(round(output), -255, 255);

      // モータにPWM出力
      setMotorSpeed(currentPWM);
    }
  }

  // --------------------------------------------------
  // 3) シリアルプロッタ / モニタ出力 (間引き出力: PLOT_INTERVAL_MS)
  // --------------------------------------------------
  if (now - plotLastTime >= PLOT_INTERVAL_MS) {
    plotLastTime = now;

    unsigned long elapsedTime = now - pwmSetTime;

    // シリアルプロッタ用 Output (KeyValueフォーマット)
    Serial.print("ElapsedTime_ms:");
    Serial.print(elapsedTime);
    Serial.print(",");
    Serial.print("Target_Angle:");
    Serial.print(targetAngle, 2);
    Serial.print(",");
    Serial.print("PWM_Cmd:");
    Serial.print(currentPWM);
    Serial.print(",");
    Serial.print("Measured_Angle:");
    Serial.println(currentAngle, 2);
  }
}

  • シリアルモニタの出力例です。
  • 赤外線リモコンの’2’のボタンを押して目標角度を30度にした時のRPMの実測値とPWM値のです。DCモータ1回転(365度)でエンコーダ信号数は495個ですから、1パルスの誤差が +/- 360/495 ≒ +/- 0.73 度になります。
  • 収束は約232msec以内に完了しています。
ElapsedTime_ms:3100,Target_Angle:0.00,PWM_Cmd:0,Measured_Angle:0.00
ElapsedTime_ms:3200,Target_Angle:0.00,PWM_Cmd:0,Measured_Angle:0.00
ElapsedTime_ms:3300,Target_Angle:0.00,PWM_Cmd:0,Measured_Angle:0.00
ElapsedTime_ms:32,Target_Angle:30.00,PWM_Cmd:95,Measured_Angle:2.91
ElapsedTime_ms:132,Target_Angle:30.00,PWM_Cmd:19,Measured_Angle:24.73
ElapsedTime_ms:232,Target_Angle:30.00,PWM_Cmd:3,Measured_Angle:29.09
ElapsedTime_ms:332,Target_Angle:30.00,PWM_Cmd:3,Measured_Angle:29.09
ElapsedTime_ms:432,Target_Angle:30.00,PWM_Cmd:3,Measured_Angle:29.09
ElapsedTime_ms:532,Target_Angle:30.00,PWM_Cmd:3,Measured_Angle:29.09
ElapsedTime_ms:632,Target_Angle:30.00,PWM_Cmd:3,Measured_Angle:29.09

回転角の制御例#2

  • 回転角(度)を目標値としてPID制御します。
  • 目標値の回転角(度)は10秒の周期で最大角度180度、最小角度-180度のsin波形で変化させます。
  • リモコンの2を押すと時計方向の回転から開始、リモコンの8を押すと反時計方向の回転から開始、リモコンの5を押すと回転を停止します。

// 目標回転角度をsin()で変化させPID制御で追従させる

#include <Arduino.h>
#include <IRremote.hpp> // IRremoteライブラリ (v3.0以降対応)

// ----- ピン定義 -----
const int PIN_ENCODER_A = 2;    // 右モータ エンコーダ A相 (INT0)
const int PIN_ENCODER_B = 12;   // 右モータ エンコーダ B相
const int PIN_AIN1      = 4;    // TB6612 AIN1 (回転方向制御 1)
const int PIN_AIN2      = 5;    // TB6612 AIN2 (回転方向制御 2)
const int PIN_PWMA      = 6;    // TB6612 PWMA (速度制御 PWM)
const int PIN_STBY      = 11;   // TB6612 STBY (スタンバイ)
const int PIN_IR        = A0;   // IRセンサ
const int PIN_LED       = A1;   // ステータス用 LED

// ----- 赤外線リモコンのキーコード定義 (お使いのリモコンに合わせて調整してください) -----
#define IR_KEY_2 0x18  // 時計方向側から sin 追従スタート
#define IR_KEY_8 0x52  // 反時計方向側から sin 追従スタート
#define IR_KEY_5 0x1C  // 目標角度 0deg で強制停止

// ----- エンコーダ & 物理定数 -----
// 立ち上がりのみ(RISING)割込のため 495 パルス/回転
const float PULSES_PER_REV = 495.0; 

// ----- PID制御ゲインパラメータ (正弦波追従用) -----
float Kp = 4.0;   // 比例ゲイン(動的追従のため少し高めに設定)
float Ki = 0.1;   // 積分ゲイン
float Kd = 0.0;   // 微分ゲイン(目標値の変化速度に対するブレーキ・応答性向上面)

// ----- 制御用変数 -----
volatile long encoderCount = 0; // 累積パルスカウント

bool isRunning = false;    // sin追従動作中フラグ
float phaseDirection = 1.0; // 1.0: 時計方向スタート, -1.0: 反時計方向スタート

float targetAngle = 0.0;   // 目標回転角度 (deg)
float currentAngle = 0.0;  // 現在の測定回転角度 (deg)
int   currentPWM = 0;    // モータへの出力PWM値 (-255 〜 255)

float errorSum = 0.0;
float lastError = 0.0;

// ----- 基準時間変数 -----
unsigned long pidLastTime = 0;
unsigned long plotLastTime = 0;
unsigned long pwmSetTime = 0; // 目標波形がスタートしてからの経過時間計算用

// シリアルプロッタ出力の間隔 (ms)
const unsigned long PLOT_INTERVAL_MS = 500;
// PID演算の割り込み周期 (ms) - 連続追従のため20ms周期
const unsigned long PID_INTERVAL_MS = 20;

// エンコーダパルスカウント用割り込みサービスルーチン (ISR: 立ち上がりのみ)
void countEncoder() {
  if (digitalRead(PIN_ENCODER_B) == LOW) {
    encoderCount++; // 正転(時計方向):加算
  } else {
    encoderCount--; // 逆転(反時計方向):減算
  }
}

// モータ駆動ヘルパー関数
void setMotorSpeed(int speed) {
  speed = constrain(speed, -255, 255);
  
  if (speed > 0) {
    digitalWrite(PIN_AIN1, HIGH);
    digitalWrite(PIN_AIN2, LOW);
    analogWrite(PIN_PWMA, speed);
  } else if (speed < 0) {
    digitalWrite(PIN_AIN1, LOW);
    digitalWrite(PIN_AIN2, HIGH);
    analogWrite(PIN_PWMA, -speed);
  } else {
    // 強制停止(両ピンLOW & PWM 0)
    digitalWrite(PIN_AIN1, LOW);
    digitalWrite(PIN_AIN2, LOW);
    analogWrite(PIN_PWMA, 0);
  }
}

void setup() {
  Serial.begin(115200);

  // ピン設定
  pinMode(PIN_ENCODER_A, INPUT_PULLUP);
  pinMode(PIN_ENCODER_B, INPUT_PULLUP);
  pinMode(PIN_AIN1, OUTPUT);
  pinMode(PIN_AIN2, OUTPUT);
  pinMode(PIN_PWMA, OUTPUT);
  pinMode(PIN_STBY, OUTPUT);
  pinMode(PIN_LED, OUTPUT);

  // モータドライバ有効化
  digitalWrite(PIN_STBY, HIGH);
  digitalWrite(PIN_LED, HIGH);

  // 赤外線受信開始
  IrReceiver.begin(PIN_IR, DISABLE_LED_FEEDBACK);

  // 外部割り込み有効化 (立ち上がりのみ: RISING)
  attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_A), countEncoder, RISING);

  pwmSetTime = millis();
  pidLastTime = millis();
  plotLastTime = millis();
}

void loop() {
  unsigned long now = millis();

  // --------------------------------------------------
  // 1) 赤外線リモコン受信処理
  // --------------------------------------------------
  if (IrReceiver.decode()) {
    if (!(IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT)) {
      uint16_t command = IrReceiver.decodedIRData.command;

      if (command == IR_KEY_2) {
        // 時計方向側から開始 (+180 deg に最初向かう sin 波)
        isRunning = true;
        phaseDirection = 1.0;
        pwmSetTime = now;
        errorSum = 0.0;
        lastError = 0.0;
      } else if (command == IR_KEY_8) {
        // 反時計方向側から開始 (-180 deg に最初向かう sin 波)
        isRunning = true;
        phaseDirection = -1.0;
        pwmSetTime = now;
        errorSum = 0.0;
        lastError = 0.0;
      } else if (command == IR_KEY_5) {
        // 強制停止処理
        isRunning = false;
        targetAngle = 0.0;
        currentPWM = 0;
        errorSum = 0.0;
        lastError = 0.0;
        setMotorSpeed(0);
        pwmSetTime = now;
      }
    }
    IrReceiver.resume();
  }

  // --------------------------------------------------
  // 2) 位置PID制御演算 (周期: PID_INTERVAL_MS)
  // --------------------------------------------------
  if (now - pidLastTime >= PID_INTERVAL_MS) {
    float dt = (now - pidLastTime) / 1000.0; // 秒単位
    pidLastTime = now;

    // 現在のカウントを取得 (割込保護)
    noInterrupts();
    long currentCount = encoderCount;
    interrupts();

    // カウント数から現在の回転角度 (deg) を算出
    currentAngle = ((float)currentCount / PULSES_PER_REV) * 360.0;

    if (isRunning) {
      // 経過時間 (秒)
      float elapsedSec = (now - pwmSetTime) / 1000.0;
      
      // 周期 10 秒の正弦波目標角度を生成 (±180度)
      // omega = 2 * PI / 10.0
      float omega = (2.0 * PI) / 10.0;
      targetAngle = phaseDirection * 180.0 * sin(omega * elapsedSec);

      // 角度偏差の計算 (目標角度 - 現在角度)
      float error = targetAngle - currentAngle;

      // 積分項
      errorSum += error * dt;
      errorSum = constrain(errorSum, -300.0, 300.0);

      // 微分項
      float dError = (error - lastError) / dt;
      lastError = error;

      // PID出力計算
      float pTerm = Kp * error;
      float iTerm = Ki * errorSum;
      float dTerm = Kd * dError;

      float output = pTerm + iTerm + dTerm;
      
      // PWM出力制限 (-255 〜 255)
      currentPWM = constrain(round(output), -255, 255);

      // モータにPWM出力
      setMotorSpeed(currentPWM);
    } else {
      // 停止モード時は PWM 0
      currentPWM = 0;
      setMotorSpeed(0);
    }
  }

  // --------------------------------------------------
  // 3) シリアルプロッタ / モニタ出力 (間引き出力: PLOT_INTERVAL_MS)
  // --------------------------------------------------
  if (now - plotLastTime >= PLOT_INTERVAL_MS) {
    plotLastTime = now;

    unsigned long elapsedTime = now - pwmSetTime;

    // シリアルプロッタ用 Output (KeyValueフォーマット)
    Serial.print("ElapsedTime_ms:");
    Serial.print(elapsedTime);
    Serial.print(",");
    Serial.print("Target_Angle:");
    Serial.print(targetAngle, 2);
    Serial.print(",");
    Serial.print("PWM_Cmd:");
    Serial.print(currentPWM);
    Serial.print(",");
    Serial.print("Measured_Angle:");
    Serial.println(currentAngle, 2);
  }
}

  • シリアルプロッタの出力例です。
  • 目標角度(度)、測定角度(度)、PWM出力値、エンコーダ出力値を500msec周期で描いています。
  • 目標角度(度)、測定角度(度)はほぼ重なっており、追従性は良いようです。
  • PWMをsin変化させていた時に見られたプラス側ドリフトしていた現象は発生していません。

よくあるトラブルと対処法

モーターが動かない配線の確認、駆動ICの制御ピンの操作を確認
逆回転しない駆動ICの制御ピンの操作を確認
エンコーダ値が取得できないモータへの配線の確認
電源が落ちる電源の配線を確認

まとめ

  • DCモータのエンコーダ出力を使って回転速度と回転角を計算できました。更に目標値を設定して回転速度と回転角の実測値をフィードバック制御(PID制御)することで目標値に収束できることが解りました。今回の記事で学んだことを、このモータを使ってロボットを作成する場合に活用したいと考えています。
  • 始めて赤外線リモコンを使って設定値を変える方法を使いました。個人的にはかなり利便性が上がる感じです。今までは変数を変えて再コンパイルしていました。今後も他の実験時にも使えそうです。

ご質問、誤植の指摘などありましたら。「問い合わせ 」のページからお願いします。