2025年9月23日火曜日

モータドライバに通信タイムアウトを入れてみた。

 OdriveライクなCAN通信でSimpleFOCのモータドライバと通信しているんだけど、電圧低下などでESP32がフリーズしたりするとモータが前回値でそのまま動いてしまうので通信切断検知を入れてみた。

この前のCAN対応ファームウェアに追加で通信がない状態が1000ms続いたらモータをフリーにするようにしてみた。坂道だとフリーなので走ってしまうかもしれないけど…

アクロバティックな走行をしていると途中でバッテリの電圧降下で回り続けるようになってしまったり。とりあえず通信切断検知を入れたらこの状態で止まってくれた。

とりあえず簡単に実装してみたやつでも動いてそう。

#define WATCHDOG 1000
unsigned long lastUpdate = 0;
boolean watchDogEnable = true;

void inputWatchdog_reset() {
#ifdef WATCHDOG
  lastUpdate = millis();
  motor.enable();
#endif
}

とりあえず追加した変数と関数はこんな感じで通信の場所でinputWatchdog_reset()を読み続ければ通信切断は検知しない感じに。

#ifdef WATCHDOG
  unsigned long watchDogMs = millis();
  if (watchDogMs - lastUpdate > WATCHDOG && watchDogEnable == true) {
    motor.disable();
  }
#endif

切断検知はloop()関数内にこんな感じで追加。

        case 0x00C:  //Set Input Pos
          if (CAN_RX_msg.type == DATA_FRAME) {
            FLOAT_BYTE_UNION input;
            memcpy(input.uint, CAN_RX_msg.data, 8);
            motor.controller = MotionControlType::angle;
            target_angle = input.value[0];
          }
          inputWatchdog_reset();
          break;
        case 0x00D:  //Set Input Vel
          if (CAN_RX_msg.type == DATA_FRAME) {
            FLOAT_BYTE_UNION input;
            memcpy(input.uint, CAN_RX_msg.data, 8);
            motor.controller = MotionControlType::velocity;
            target_angle = input.value[0];
          }
          inputWatchdog_reset();
          break;
        case 0x00E:  //Set Input Torque
          if (CAN_RX_msg.type == DATA_FRAME) {
            FLOAT_BYTE_UNION input;
            memcpy(input.uint, CAN_RX_msg.data, 8);
            motor.controller = MotionControlType::torque;
            target_angle = input.value[0];
          }
          inputWatchdog_reset();
          break;

あとは前回同様CAN通信のswitch case分の中のモータを動かしそうな関数にinputWatchdog_reset()を追加していけばOK。これを追加するとSimpleFOC StudioからいじれなくなるのでそのときはwatchDogEnableをfalseにする関数をコマンドに追加してやろうかなと言う感じ。

使用してるジンバルモータが220kvなので1Sだと90rad/sぐらい回せそうなんだけども48rad/s制限で回してみると結構早い。頑張ればウィリーできるぞ…

電源もブレッドボードからとりあえず取ってるのでウィリーしてリアのケーブルを動かすと接触不良で通信が止まってしまうんだけど、ちゃんとモータが止まるようになった。
ウィリーした瞬間に通信が止まってモータが回り続けているけど1秒ぐらいで停止している感じ。
モータドライバのLEDが付いてるのでモータドライバは動いてそう。通信してるとブレーキが掛かるんだけど、通信していないとフリーになるので通信切断検知が動いてるのかわかりやすい。

結構モータが強いのでキャスターをどうにかしてもうちょっと走行性能を上げたいな。

2025年9月22日月曜日

SimpleFOCの速度PIDゲイン調整してみた。

 とりあえずBluetoothラジコンとしてPS3コンで動くようになったのは良いんだけども、モータがカクカクするのでPIDのゲインを調整してみた。負荷がかかるとハンチングしてる感じ。

SimpleFOCのPIDゲイン調整をしたときにAngleでFOCのゲインを調整をしていたのにもかかわらず、そもそものAngleとVelocityのゲインは未調整だった…

  // velocity loop PID
  motor.PID_velocity.P = 0.2;
  motor.PID_velocity.I = 20.0;
  motor.PID_velocity.D = 0.0;
  motor.PID_velocity.output_ramp = 1000.0;
  motor.PID_velocity.limit = 8.0;
  // Low pass filtering time constant
  motor.LPF_velocity.Tf = 0.01;
  // angle loop PID
  motor.P_angle.P = 20.0;
  motor.P_angle.I = 0.0;
  motor.P_angle.D = 0.0;
  motor.P_angle.output_ramp = 0.0;
  motor.P_angle.limit = 40.0;
  // Low pass filtering time constant
  motor.LPF_angle.Tf = 0.0;

この前のスケッチを見るとデフォルト値?でこんな感じになっていた。

コネクタがこの通り抜き差ししにくいので、とりあえずモータを取り外してシリアルでPCに繋いでSimpleFOC Studioで追加で調整してみた。まずは速度ゲインの調整を。Angleはとりあえずは使っていないので。
コネクタの向きはモータドライバステーを改良すれば行けそうな気がするな…

IとDをゼロにしてPを上げて行くとすぐハンチングしてしまう。LPFを0.05にしてみたらPが1.5ぐらいまで行けそうだったのでとりあえずはPを1.5にしてIを上げていってみた。Iを結構上げても大丈夫そうなんだけど負荷のかかり具合によってまたカクカクしそうなのでとりあえず10ぐらいにしておいた。

  // velocity loop PID
  motor.PID_velocity.P = 1.5;
  motor.PID_velocity.I = 10.0;
  motor.PID_velocity.D = 0.0;
  motor.PID_velocity.output_ramp = 1000.0;
  motor.PID_velocity.limit = 8.0;
  // Low pass filtering time constant
  motor.LPF_velocity.Tf = 0.05;

まずはこんな感じにセットしてみた。

左右のモータドライバに書き込んで動かしてみるとカクカクが治っていい感じ。もう少し調整できそうな気はするけども…

ということでSimpleFOC Studioを使わなくてもCANから変更できるようにCANの部分を少し改良。オリジナルのOdriveでもそういうコマンドが合ったのでとりあえずPとIを変更できるようにしてみた。RTRを送ると現在の値が見れる。

        case 0x01B:  //Set Vel Gains
          if (CAN_RX_msg.type == DATA_FRAME) {
            FLOAT_BYTE_UNION input;
            memcpy(input.uint, CAN_RX_msg.data, 8);
            motor.PID_velocity.P = input.value[0];
            motor.PID_velocity.I = input.value[1];
          }else{
            FLOAT_BYTE_UNION output;
            output.value[0] = motor.PID_velocity.P;
            output.value[1] = motor.PID_velocity.I;
            memcpy(CAN_TX_msg.data, output.uint, 8);
            CAN_TX_msg.len = 8;
            CANSend(&CAN_TX_msg);
          }
          break;

こんな感じで0x01Bに対応した部分をswitch case分の中に追加しただけ。とりあえず簡単なツールをPythonで作成して走行しながら調整してみよう。ESP32の方にもリアルタイムで調整できるようにWebServerでも追加しようかな。

うまく動くようになったのでスピードも少し上げてみた。もうちょっと早くできそう。
最大スピードもリアルタイムで変更できるようにしたいな。スピード上げると直線走行時にまたハンチングが少し出てきているような気が…
あとキャスターが左右にゆらゆらするので、そのせいで車体もゆらゆらしてしまうなぁ
ボールキャスターも試してみようかな。ちなみにモータが静かすぎてキャスターの回る音のほうが大きいのでキャスターを変えたらもっと静かになりそう。メカナムラジコンのときのギアの音とは大違いだ…

2025年9月21日日曜日

DDモータのラジコンを走行テストしてみた。

 前回タミヤのユニバーサルプレートにダイレクトドライブモータとキャスターを取り付けられたので試しにESP32でラジコン化して動かしてみた。

ダイレクトドライブモータとして使用するジンバルモータには自作モータドライバを取り付けてOdriveのようなプロトコルでCANから制御できるようになっている。ESP32にCANトランシーバを取り付けて、Bluetoothで接続したPS3コントローラをつかってラジコン化してみる。

DDアクチュエータ

バッテリを共通化しようとしたけどESP32がうまく起動しなかったのでとりあえずESP32の電源はモバイルバッテリを使用することに。昇圧回路挟まないとだめかな。低速でテストするので適当においてあるだけ…

#include <Ps3Controller.h>
#include "driver/twai.h"

#define CAN_RX 35
#define CAN_TX 32
#define canRate 100  //モータステータスリクエスト周期
#define DEBUG
#define DEADZone 8

#define BTMAC "1a:2b:3c:01:01:01"

//コントローラーの値代入用
int lx = 0;
int ly = 0;
int rx = 0;
int ry = 0;
int lt = 0;
int rt = 0;
boolean BTconnected = false;

//モーター出力設定
float MotorGain = 0.5;
uint8_t MotorMode = 0;

//ポーリング用
unsigned long lastMotor = 0;
unsigned long lastupdate = 0;

//CANデータ処理用共用体
typedef union {
  int32_t value;
  byte bytes[4];
} INT32_BYTE_UNION;

typedef union {
  float value;
  byte bytes[4];
} FLOAT_BYTE_UNION;

void notify() {
  lx = Ps3.data.analog.stick.lx;
  ly = Ps3.data.analog.stick.ly;
  rx = Ps3.data.analog.stick.rx;
  ry = Ps3.data.analog.stick.ry;
  if (abs(lx) < DEADZone) {
    lx = 0;
  }
  if (abs(ly) < DEADZone) {
    ly = 0;
  }
  if (abs(rx) < DEADZone) {
    rx = 0;
  }
  if (abs(ry) < DEADZone) {
    ry = 0;
  }
  lt = Ps3.data.analog.button.l2;
  rt = Ps3.data.analog.button.r2;

  if (Ps3.event.button_up.r1) {
    MotorMode += 1;
    if (MotorMode > 4) {
      MotorMode = 4;
    }
    Ps3.setPlayer(MotorMode);
  } else if (Ps3.event.button_up.l1 && (MotorMode > 0)) {
    MotorMode -= 1;
    Ps3.setPlayer(MotorMode);
  }
}

void btdisconn() {
  BTconnected = false;
#ifdef DEBUG
  Serial.println("Controller Disconnected");
#endif
}

void btconn() {
  BTconnected = true;
#ifdef DEBUG
  Serial.println("Controller Connected");
#endif
}

void setup() {
#ifdef DEBUG
  Serial.begin(115200);
#endif

  //DUALSHOCK3初期化
  Ps3.attach(notify);
  Ps3.attachOnDisconnect(btdisconn);
  Ps3.attachOnConnect(btconn);
#ifdef BTMAC
  Ps3.begin(BTMAC);
#else
  Ps3.begin();
#endif

  // Initialize configuration structures using macro initializers
  twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t)CAN_TX, (gpio_num_t)CAN_RX, TWAI_MODE_NORMAL);
  twai_timing_config_t t_config = TWAI_TIMING_CONFIG_250KBITS();  //Look in the api-reference for other speed sets.
  twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();

  // Install TWAI driver
  if (twai_driver_install(&g_config, &t_config, &f_config) == ESP_OK) {
    Serial.println("Driver installed");
  } else {
    Serial.println("Failed to install driver");
    return;
  }

  // Start TWAI driver
  if (twai_start() == ESP_OK) {
    Serial.println("Driver started");
  } else {
    Serial.println("Failed to start driver");
    return;
  }

  // Reconfigure alerts to detect TX alerts and Bus-Off errors
  uint32_t alerts_to_enable = TWAI_ALERT_TX_IDLE | TWAI_ALERT_TX_SUCCESS | TWAI_ALERT_TX_FAILED | TWAI_ALERT_ERR_PASS | TWAI_ALERT_BUS_ERROR;
  if (twai_reconfigure_alerts(alerts_to_enable, NULL) == ESP_OK) {
    Serial.println("CAN Alerts reconfigured");
  } else {
    Serial.println("Failed to reconfigure alerts");
    return;
  }
}



void loop() {
  if (lastMotor + 50 <= millis()) {
    lastMotor = millis();  //次の実行のために時間更新
    if (BTconnected == true) {
      setspeed(ly / -127.00, rx / 127.00);
    } else {
      setspeed(0, 0);
    }
  }
}

void setspeed(float y, float x) {  // 前後左右回転それぞれを係数と掛けて足す

  switch (MotorMode) {
    case 0:
      MotorGain = 1.0;
      break;
    case 1:
      MotorGain = 6;
      break;
    case 2:
      MotorGain = 8;
      break;
    case 3:
      MotorGain = 10;
      break;
    case 4:
      MotorGain = 16;
      break;
  }
  float motor_l = (y + x) * MotorGain;
  float motor_r = (y - x) * MotorGain;
  setSpeed(0, motor_l);
  setSpeed(1, motor_r * -1);
}

//CAN通信関連
void canSend(int addr, byte* CanData) {

  twai_message_t message;
  message.identifier = addr;
  message.data_length_code = 8;
  for (int i = 0; i < 8; i++) {
    message.data[i] = CanData[i];
  }

  // Queue message for transmission
  if (twai_transmit(&message, pdMS_TO_TICKS(1000)) == ESP_OK) {
    //printf("Message queued for transmission\n");
  } else {
    //printf("Failed to queue message for transmission\n");
  }
}

void setSpeed(uint8_t mID, float speed) {  //Control motor by Speed (unit 0.01dps/LSB,1rpm = 6dps)
  FLOAT_BYTE_UNION speed_f;
  speed_f.value = speed;
  int addr = mID << 5 | 0x00D;
  byte data[8] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
  for (int i = 0; i <= 3; i++) {
    data[i] = speed_f.bytes[i];  //Little endian
  }
  canSend(addr, data);
}

ESP32側のソフトはこんな感じで、以前メカナムラジコンをESP32化したときに使ったスケッチをCAN対応にした感じ。モータドライバ側は前に作ったCAN対応バージョンをそのまま使用している。PIDとかのパラメータはそのまま。

とりあえず低速で動かしてみるとSimpleFOCのキャリブレーションが悪いのか、こんな感じでカクカクしている。無負荷だとスムーズに回っているのでキャリブレーションの問題かも。そういえばvelocity loop PIDはまだいじっていないのでデフォルトだった気も。
通信の負荷の可能性もあったので通信しないで走らせても同じ感じだった。

スピードを上げると少し良くなるのでやっぱりキャリブレーションを見直さないとな…
しかしさすがダイレクトドライブ、タミヤのギアボックスと比べ物にならないぐらい静か。

2025年9月20日土曜日

キャスターをユニバーサルプレートに装着するアダプタ

 前回ユニバーサルプレートにジンバルモータを装着したところ、車体が高すぎて前輪のキャスターがつけられなかったのでアダプタを3Dプリントしてもらってきた。

穴は全部2.7mmで貫通。

シームの位置はランダム

2.7mm設定ならM3ネジがそのままねじ込めた。タミヤのユニバーサルプレート付属のネジ(M3x8)でキャスターを固定。
ちなみに1枚入りのNo.98 ユニバーサルプレートセットにはM3ネジが付属してるんだけど、2枚入りのNo.157 ユニバーサルプレート 2枚セットだと樹脂のピンしか付属していないっぽい。
反対側は20mmピッチにしてあるのでそのままユニバーサルプレートに固定できる。キャスターにすると直進性に難が出たりするので、しっかり中心に固定できるのは良さそう。
タミヤのギアボックスよりも車高がだいぶ高い。このモータなら回転フィードバックがかけられるので直進性はだいぶ良くなると期待。

2025年9月14日日曜日

LiDARをユニバーサルプレートに載せられるようにしてみた。

 昔アリエクで購入したLDS-006をユニバーサルプレートに装着できるようにステーを設計してみた。LDS-006自体はEcovacsの何種類かの掃除ロボットで使われてるようで、前に購入したLDS-006ではいい感じのステーが一緒に装着されていたので平面が有れば置くだけで装着できたんだけど、ネジ穴の寸法が測定しにくいのでステーごと作り変えてみることに。

こちらがもともとついていたステー。左右非対称なので寸法が図りにくい。

ステーを外すとLDS-006側は左右対称っぽい形状をしているので測定しやすかった。
とりあえず3Dデータを作成して、ここからユニバーサルプレートに合うように設計してみることに。
とりあえずネジ穴の寸法とステーを作るときに干渉しないようにしないといけないDCモータの部分をいい感じに3Dデータ化してみた。
そんでもってユニバーサルプレートに合うように5mmピッチの穴を開けたステーを設計。もともとついていたステーだとネジを止めた際にLiDARに鑑賞してしまいそうだったのでボスの高さを少し高くしておいた。

LDS-006自体は今回購入したバージョンはステーが付いていたけども、ステーは機種によって色んな種類があるみたいなのでどのLDS-006を購入してもこのステーなら取り付け可能かも。
まぁ今となってはもっと小さいLiDARが同じぐらいのお値段で購入できるようになってると思うのでアレ需要はほぼないかもしれないけど。

とりあえず3Dプリントの方をお願いしつつ、このステーを止められるような長いM3スタンドオフをアリエクで探すとしますか。

2025年9月13日土曜日

自作モータドライバを楽しい工作シリーズと組み合わせてみる

 この前、スリムタイヤを装着したことによりダイレクトドライブモータユニットが完成したのでこれをタミヤのユニバーサルプレートに装着してみることに。

最初はボールキャスターを使おうと思ったんだけど、床が傷だらけになりそうなので昔遊んでいた梵天丸をインスパイアしてキャスターにした。
J11 TPPA 025 S
キャスターはホームセンターで130円ぐらいのエラストマータイプにしてみた。これで床も傷つきにくいはず。このサイズのキャスター、高さが35mmなのでタミヤ系のギアボックスとかとの相性が良さそう。
ジンバルモータに自作モータドライバを装着するステーに5mmピッチのM3の穴を開けておいたのでそのまま装着できる。ネジはユニバーサルプレート付属のM3ネジをそのまま使った。ステーを小型化しすぎて15mmピッチのところで固定してるけどトー角が動きすぎてしまうので調整が難しいな…

モーターの関係で固定位置が48mmで車高がかなり高くなってしまったので35mmのキャスターはやっぱり延長してユニバーサルプレートを水平にしておきたい。
あとキャスターの穴ピッチも微妙なんだよね…穴が大きいのでギリギリ固定できそう。
というわけで3Dプリント部品を作って延長してやろうかと。
こんな感じでM3のネジで固定できるように2.7mmの固定穴を開けただけのアダプタ。ユニバーサルプレートに固定するネジピッチは楽しい工作シリーズのボールキャスターと同じピッチにした。(20x20)これでボールキャスターをつけたい場合はボールキャスターの延長にも使える。あとはユニバーサルプレートの中心に取り付けられるし。

市販品で購入したのがユニバーサルプレートとキャスターとOリングぐらいしかないので3D化してアセンブリしてみた。拡張部品作るときに干渉チェックとかに使えるかなとか思って。

配線も電源とCAN-BUSなのでパラレルでどんどん繋げられるようなコネクタにすればもうちょっと拡張性が良くなりそう。

2025年9月7日日曜日

Oリングをスリムタイヤにしてみた。

 ちょっと前にタミヤのスリムタイヤキットを自作してようとしてOリングをアリエクでポチってたんだけどもなかなか届かない…
1週間で届くってなってたんだけども返金申請がようやくできるようになったので返金申請してみた。最終的にアリエクが仲裁に入って返金された。

というわけでOリングをホームセンターで購入してきた。

大きなOリングも意外とホームセンターで扱っているのね…

そんでもって前回設計したホイール部分を3Dプリントしてもらった。Oリングに合わせた形状が細かすぎるとのことで0.2mmノズルを使ってもらった。A1用のアリエクの互換品だけどもなかなか良さそう。

剥がすときにこの部分やってしまうらしい。
表面はすごくきれい。さすが0.2mmノズル。一個1時間かかるらしいけど。
モータのネジ部はあまり深さがないので4mmぐらいの長さのM2のネジを探していたんだけどホームセンターではこれしか売ってなかった。
なかなかいい感じに固定できた。あとはこの状態でもう一回SimpleFOCのチューニングを確認してみる。

タミヤのユニバーサルプレートに固定できるような穴をモータドライバマウントにつけているのでこれでとりあえず対向二輪の車体を作ることができるかな。