2025年9月28日日曜日

タミヤのボールキャスターを試してみた。

 この前作った対向2輪型のラジコンなんだけど、高速で直進すると左右にゆらゆらするのでキャスターの制度が悪いからキャスターが振られてるんじゃないか?ということでボールキャスターに変更してみた。

金属製なので床が傷つくんじゃないかということで長らく放置していたタミヤのボールキャスター。
組み立ては超簡単。
キャスター用の高さ調整プレートはネジ穴を貫通にしてあるのでボールキャスターの高さ調整にも使用可能。ボールキャスターも35mmで組み立てたのでピッタリ。

実際に走らせてみるとやっぱりゆらゆらする。ということはSimpleFOCのPIDチューニングかもしれない。速度フィードバックで速度が一定になっていないのかなぁ。これはさらなるチューニングが必要かもしれない。
この前CAN経由でチューニングできるようにしたのでESP32側にちょっと変更できるようなしくみを入れようかな。

それはそうと、ボールキャスターになったことでフロントが軽くなってフロントが浮くようになってしまった。

重さはキャスターが36gでボールキャスターが12gだった。これはフロントが軽くなる。
キャスター変えただけでこんなに機敏に動くようになるなんて…
これジャイロつけたらずっとウィリーとかできそうな?

2025年9月27日土曜日

ESP32にLiDARを繋いでブラウザに表示してみた。

 前に購入した格安LiDARをESP32に繋いでブラウザでデータを表示するようにしてみた。購入したときにESP32に接続してデータ取得するところまではやってみたんだけど、今回はその取得できたデータをWebSocketでブラウザに送って可視化してみることに。

今回はM5AtomS3が余っていたのでこれに繋いでみることに。Groveコネクタは信号線が3.3Vだけど電源は5V取れるので好都合。GroveコネクタにLiDARが直結できるなんて便利かも。

色々試してみたんだけど、JSONでデータを送るとデータ量が多すぎてうまく360°分のデータが送れなかったので距離データだけをバイナリでWebSocketで送るようにしてみた。

今更気がついたんだけど、LDS-006って回転方向がCCWなのね…普通LiDARを手に持って回転させると画面の可視化データって反対回りに動くはずなんだけど同じ向きに回る。ということでそこもデータを取得するところで修正した。

#include <Arduino.h>
#include <WiFi.h>
#include <WebSocketsServer.h>
#include <WebServer.h>
#include <ESPmDNS.h>

#define RXD2 2
#define TXD2 1

// WiFi設定
const char* ssid = "SSID";
const char* password = "password";

WebServer server(80);  // Webサーバー port80
WebSocketsServer webSocket = WebSocketsServer(81);// WebSocketサーバー port81

// LDS-006 360点分の距離データ
volatile uint16_t lidar360[360];
volatile bool newData = false;

// LDS-006読み取りタスク
void taskLidarRead(void* pvParameters) {
  Serial2.begin(115200, SERIAL_8N1, RXD2, TXD2);
  delay(100);
  Serial2.print("startlds$");  // 起動コマンド

  uint8_t packet[22];
  int packetIndex = 0;

  while (1) {
    while (Serial2.available()) {
      uint8_t b = Serial2.read();
      if (packetIndex == 0 && b != 0xFA) continue;
      packet[packetIndex++] = b;
      if (packetIndex == 22) {
        if (packet[0] == 0xFA) {
          uint8_t block = packet[1];
          if (block >= 0xA0 && block <= 0xF9) {
            int baseAngle = (block - 0xA0) * 4;
            for (int i = 0; i < 4; i++) {
              int offset = 4 + i * 4;
              uint16_t dist = ((packet[offset + 1] & 0x3F) << 8) | packet[offset];
              int angle = baseAngle + i;
              if (angle >= 360) angle -= 360;
              int correctedAngle = (360 - angle) % 360;
              lidar360[correctedAngle] = dist;
            }
            newData = true;
          }
        }
        packetIndex = 0;
      }
    }
    vTaskDelay(1);
  }
}

// WebSocket送信タスク
void taskWebSocketSend(void* pvParameters) {
  while (1) {
    if (newData) {
      // 360点分の距離データを2バイトずつのバイナリで送信
      webSocket.broadcastBIN((uint8_t*)lidar360, 360 * 2);
      newData = false;
    }
    vTaskDelay(100);  // 100ms周期
  }
}

void taskHttpServer(void* pvParameters) {
  while (1) {
    server.handleClient();
    vTaskDelay(1);
  }
}

// WebSocketイベント
void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
  if (type == WStype_CONNECTED) {
    Serial.printf("WebSocket client #%u connected\n", num);
  }
}

#define index_html_gz_len 517
const uint8_t index_html_gz[] PROGMEM = {
  0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x55, 0x53, 0x5D, 0x6B, 0xDB, 0x30,
  0x14, 0x7D, 0x1F, 0xEC, 0x3F, 0xDC, 0x99, 0x41, 0xEC, 0x36, 0xB3, 0x9C, 0x8C, 0x85, 0x52, 0xDB,
  0x81, 0x2D, 0xED, 0x43, 0xA1, 0x65, 0x65, 0xE9, 0x18, 0x63, 0x0C, 0xAA, 0xD8, 0x4A, 0x2C, 0xA6,
  0x48, 0x45, 0xBA, 0x89, 0x63, 0x4A, 0x5E, 0xF6, 0x30, 0xF6, 0x97, 0xF6, 0x83, 0xC6, 0xFE, 0xC6,
  0x24, 0x7F, 0x24, 0xCE, 0x8B, 0xA5, 0xAB, 0x7B, 0xCE, 0xD5, 0x3D, 0x47, 0xBE, 0xC9, 0xAB, 0xAB,
  0x8F, 0xB3, 0x87, 0xAF, 0xF7, 0xD7, 0x50, 0xE0, 0x5A, 0x4C, 0x5F, 0xBE, 0x48, 0x0E, 0x2B, 0xA3,
  0xB9, 0x5D, 0x01, 0x12, 0xE4, 0x28, 0xD8, 0xF4, 0xF6, 0x6A, 0xFE, 0x26, 0x8A, 0x26, 0x70, 0xCB,
  0xB7, 0x2C, 0x21, 0xCD, 0x99, 0x85, 0x91, 0x16, 0x97, 0x2C, 0x54, 0x5E, 0xB9, 0x35, 0xA3, 0x72,
  0x4B, 0x0D, 0xF0, 0x3C, 0xF5, 0x04, 0xCF, 0xA9, 0x9E, 0xD5, 0xB1, 0x07, 0x25, 0xCF, 0xB1, 0x48,
  0xBD, 0x77, 0x51, 0xE4, 0x41, 0xC1, 0xF8, 0xAA, 0xC0, 0x26, 0x98, 0x26, 0xA4, 0xA1, 0x38, 0xB2,
  0xC9, 0x34, 0x7F, 0x42, 0xBB, 0xCB, 0x94, 0x34, 0x08, 0x6D, 0xAD, 0x14, 0x72, 0x95, 0x6D, 0xD6,
  0x4C, 0x62, 0xB8, 0x62, 0x78, 0x2D, 0x98, 0xDB, 0x7E, 0xA8, 0x6E, 0x72, 0x7F, 0xD0, 0xBB, 0x62,
  0x10, 0xC4, 0x07, 0x1E, 0xEE, 0x2C, 0xA9, 0x61, 0x3B, 0xCA, 0x4C, 0x49, 0x64, 0x3B, 0xF4, 0x07,
  0xE3, 0xBC, 0x87, 0x2A, 0x94, 0xFD, 0xA4, 0xB6, 0x31, 0x99, 0xAB, 0x32, 0x14, 0x2A, 0xA3, 0xC8,
  0x95, 0x0C, 0xDD, 0xB1, 0xA4, 0x6B, 0x76, 0xC0, 0x95, 0xAE, 0x03, 0xC9, 0x4A, 0xF8, 0xC2, 0x16,
  0x73, 0x95, 0xFD, 0x60, 0xE8, 0x3F, 0x96, 0xE6, 0x92, 0x90, 0xD7, 0xCF, 0x0E, 0xBB, 0xBF, 0xBC,
  0x18, 0x3D, 0xBA, 0xAA, 0xA5, 0x09, 0x17, 0x5C, 0x52, 0x5D, 0x3D, 0x54, 0x4F, 0xCC, 0x52, 0x3C,
  0xAA, 0x35, 0xAD, 0x16, 0x9B, 0xE5, 0x92, 0x69, 0xCF, 0xE6, 0x6B, 0x84, 0x92, 0x6B, 0x66, 0x0C,
  0x5D, 0x39, 0x80, 0xCF, 0xB6, 0x56, 0x48, 0x00, 0xE9, 0x14, 0x9E, 0x9D, 0xD3, 0xCD, 0x75, 0x0D,
  0xC1, 0xA6, 0xEB, 0x6C, 0x98, 0x53, 0xA4, 0xF1, 0x31, 0x9B, 0x73, 0x83, 0xEF, 0x5D, 0xDD, 0xB6,
  0xA7, 0xCF, 0x5C, 0xE2, 0x68, 0x52, 0x9F, 0xF8, 0x0D, 0x33, 0x88, 0x81, 0x10, 0x78, 0x3B, 0x89,
  0xFE, 0xFD, 0xFC, 0xF3, 0xF7, 0xF7, 0xAF, 0x9A, 0x8A, 0xBB, 0x30, 0x13, 0x8C, 0xEA, 0x4F, 0x2C,
  0x43, 0x3F, 0x1A, 0x46, 0xC3, 0xD6, 0x9B, 0xFA, 0x51, 0xBA, 0xA0, 0x79, 0x95, 0x20, 0xEE, 0x18,
  0x86, 0x6E, 0x99, 0x7F, 0x0C, 0x51, 0x53, 0x69, 0x04, 0x45, 0xE6, 0xF7, 0xC9, 0x64, 0x3C, 0x84,
  0x13, 0x3E, 0x19, 0x37, 0x94, 0xA5, 0xD2, 0xBE, 0x60, 0x08, 0x3C, 0x8D, 0x62, 0x9E, 0xD8, 0x76,
  0x62, 0x7E, 0x7E, 0x1E, 0xD4, 0x3A, 0x3B, 0x2D, 0x54, 0xAE, 0x84, 0xF3, 0x81, 0xC3, 0x19, 0xDC,
  0x51, 0x2C, 0xC2, 0xFB, 0x1B, 0x20, 0x30, 0xBA, 0x88, 0xE2, 0x3E, 0xCA, 0x59, 0x71, 0x50, 0xFD,
  0x8D, 0x7F, 0x3F, 0x8B, 0xC2, 0x51, 0x0C, 0x7D, 0x84, 0x7B, 0x6C, 0xDD, 0xD5, 0xC8, 0x94, 0xF1,
  0xEB, 0xC2, 0xC1, 0x49, 0x95, 0xAA, 0x8F, 0x31, 0x5C, 0x1E, 0x31, 0x2D, 0xCA, 0x2A, 0x5C, 0x72,
  0x21, 0xE6, 0x58, 0xD5, 0x3D, 0x79, 0x0B, 0xB1, 0x61, 0x5E, 0x7C, 0x9A, 0xAC, 0xED, 0xDB, 0x0D,
  0xA1, 0x1A, 0x82, 0x95, 0xDD, 0x0A, 0xDD, 0x77, 0x06, 0x69, 0x66, 0x50, 0xE9, 0xC6, 0xB2, 0x7D,
  0xEC, 0x66, 0xE3, 0xF0, 0x3B, 0x27, 0xA4, 0x1B, 0x0F, 0x52, 0xCF, 0xD7, 0x7F, 0x40, 0x2D, 0x47,
  0x49, 0x77, 0x03, 0x00, 0x00
};

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

  // WiFi接続
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("WiFi connected. IP: ");
  Serial.println(WiFi.localIP());
  MDNS.begin("esp32");

  // WebSocket
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);

  // タスク作成
  xTaskCreatePinnedToCore(taskLidarRead, "LidarRead", 4096, NULL, 2, NULL, 1);
  xTaskCreatePinnedToCore(taskWebSocketSend, "WebSocketSend", 4096, NULL, 1, NULL, 1);
  xTaskCreatePinnedToCore(taskHttpServer, "HttpServer", 4096, NULL, 1, NULL, 1);

  server.enableCORS();
  server.on("/", HTTP_GET, []() {
    server.sendHeader(F("Content-Encoding"), F("gzip"));
    server.send_P(200, "text/html", (const char*)index_html_gz, index_html_gz_len);
  });
  server.begin();
}

void loop() {
  webSocket.loop();  // WebSocketのループ
}

これでESP32を起動するとLiDARが回り始めて、http://esp.localにアクセスするとLiDARのデータがプロットされる。Webページのデータもスケッチにgzで圧縮して埋め込んでしまったんだけど、やっていることはWebSocketでデータを受け取ったやつをXY座標を三角関数で計算してCanvasでプロットしてるだけ。

こんな感じでiPhoneからアクセスしてもリアルタイムでデータが送られてくる。Androidの場合はmDNS非対応なのでIPアドレスでアクセスする必要があるかも。

中心線が無いと動きがわかりにくいので中心線とかグリッドとか入れておいたほうがいいかな?

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のチューニングを確認してみる。

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

2025年9月6日土曜日

シガソケUSB PD 65Wチャージャーを自作してみた。

 前回昇圧DCDCコンバータとUSB PD 65Wの降圧DCDCコンバータを組み合わせて12Vを無駄に22Vに昇圧してからUSB PDの出力を得ることに成功したので、流石にそのままだと使いにくいのでケースの3Dデータを作成してみた。
65Wまで対応なので、自動車の中でパソコンを充電したりタブレットも急速充電できたり。

こんな感じで2ピース構成でシンプルにしてみた。基板の3Dモデルを作ってそれに合わせてギリギリに作ってみた感じ。

そんでもって3Dプリンタを持っている方にプリントてもらった。USB PDの基板はパチンと止めるような爪を付けていたんだけど少し削る必要があった。昇圧DCコンバータのフィンの部分は寸法ギリギリで作っていたので両側0.8mmほど削って対応した。

上下のカバーの固定はM2ネジをそのままねじ込む形で固定。上部にはグラボ用のヒートシンクを貼り付けられるように穴を設けてU1Eの冷却を行う。そもそもPLAだと60度ぐらいで柔らかくなってしまうのでちょっと心配である。

とりあえずこのケースで熱のテストをしてみて上の蓋をアルミ版で作ったりとか色々試せればなと思う。ちなみに入力には7.5Aのヒューズを入れておいた。
早速ノートPCを充電してみたところ、電力が測定できるケーブルで測定すると30Wから70Wぐらいの電力が供給できていた。200円ぐらいのケーブルなので精度は怪しいんだけども負荷をかけると60~70Wぐらいまで行っていたのでちゃんと20Vで供給できてそう。
ヒートシンクはどちらも触れる程度に暖かかった。

今回はDCDCコンバータが余っていたので作ってみたけども、部品を揃えて作るよりも市販のシガソケ充電器を買ったほうが安上がりかもしれない。しかし最近マルチポートの充電器が多くて1つのUSB Type C端子からどれだけ電力を供給できるかはよく見ておかないとだめかも。トータル100W超えるような製品でも単体のポートは最大45Wまでとかあるみたいだし。

2025年8月31日日曜日

12VからUSB PDの65Wを出力してみた。

 少し前にDC電源入力のUSB PD Charger基板を購入してみたんだけど、降圧しかできない仕様だったので65Wを出すには入力電圧が20Vより大きくないといけなかったのでフル性能がなかなか使えなかった。

たまたま昔購入した昇圧DC-DCコンバータを発見したので車のシガーソケットでノートPCを充電できないかということでやってみた。

注文履歴を見ると2013年10月ってなっていてなにに使ったかよくわからないけど5ドルで2つ購入していたうちの一つを発見。端子のネジも一個欠損していた。
OSKJの150W対応のDC-DCコンバータっぽい。コントローラICにはUC3843ANが乗っていた。これで12Vから22Vぐらいまで昇圧できれば、CKCS U1E互換基板で65Wまで出力できるようになるかな。Amazonで同じものを見つけたんだけど、12Vバッテリで19V 3.42A(65W)のノートPCを駆動させると温度が45℃ぐらいになるということだったので用途的にはちょうど良さそう。

端子のネジが一つないし、そもそもこの端子で大電流流すのはアレなので端子を取り外してケーブルを直結してみた。DC5521バージョンを購入したのに電源端子を外してしまってこっちも直結。

XPM52Cのほうの発熱が気になるので裏側にヒートシンクを装着できるようにはんだ付けの際はフラットにしておいた。ヒートシンク無しで65W出力すると100℃超えるらしいし。

昇圧電圧を22Vになるようにセットして、とりあえず12V 7.5Aヒューズを入れて12Vバッテリに繋いでみたけどちゃんとノートPCをつなぐとType Cからは20V出てるっぽい(モジュールの出力コンデンサをテスターで測定)

とりあえずあり物で12Vから65W(20V 3.25A)が出せるようになったので良いけども、アリエクとかで65W出力可能なシガソケ充電器が12-24V対応とかで売っているのでそっちを購入したほうが安上がりかも。Essager 120W Car Charger(FSJ-009)とかだと1500円ぐらいで、昇圧対応でPDは100Wまで行けるみたいだし。何よりアルミ製で放熱も安心だし…

このままでは実際に使いにくいのでケースに入れないと。

2025年8月25日月曜日

PCファンをUSB扇風機として使えるように整流板を作ってみた。

 以前余ったファンをUSBで使えるように、5Vから12Vに昇圧してファンガードを両面につけてUSB扇風機として使うようにしてみたのだけれども、やっぱり風量が足りない。ポータブルファンとかと比べると回転数に対しての風量が少ない気がする。

PC用のファンなので実は風量よりも風向きがあまり良くないのではないかということで調べてみると、AINEXから風の直進性を高めるアイテムが出ていたので試してみたかったのだけれども販売終了していたので同じようなものを作ってみた。

整流板としてはとりあえず形状は何でも良さそうではあるけどもオリジナルを忠実に再現してヘキサゴン形状にした。大きさもスペックから同じぐらいの大きさにしてみた。ファンと嵌合する部分の長さがわからなかったけど予想でこんな感じに。Onshapeフリープランなのでfan boosterで検索すれば見つけられるかも。

というわけで例によって3Dプリンタ持っている人にお願いしてプリントしてもらった。ファンと合わせるくぼみのせいで余計なサポートが付いてしまったようで申し訳ないけど。

これ実は反対面をサポートにしたほうが良かったのでは?と思ったけどもまぁとりあえずこれでも問題なさそう。

ファンに装着するとこんな感じ。オリジナルはきつかったみたいだけどそれを考慮してゆとりをもたせたおかげでいい感じに装着できた。
長さ方向はこんな感じ。オリジナルと同じぐらいの長さになってると思う。

効果の程は結構絶大で、ネジ止めせずにつけたり外したりして試してもかなり風の直進性が高まってることが感じられる。自分に風を向けていると今まではファンの回転数調整はいらないと思っていたが少し下げられるように回転数を調整したいぐらいに涼しい。このファンだと2mぐらいでも風が感じられるぐらいには直進性が増していた。

次は風量を調整できるようにしないと…

もう少し小さいファンで持ち手をつけてハンディーファンでも作れそう。

2025年7月13日日曜日

ATGM336HなGPSモジュールを買ってみた

 アリエクで安いGPSモジュールがあったので購入してみた。ATGM336HっていうシリーズのGPS。580円だった。

とりあえずアンテナは小さいやつがついているのにしてみたけども、感度はどうなんだろう?

裏面にはホットスタート用のバッテリとEEPROMらしきものがついている。以前UBX-G7020のGPSモジュールを購入したときはEEPROMがついてなくて設定が保存できなかったので設定が保存できるのは便利そう。

とりあえず動作確認をしてみた。適当なシリアル変換アダプタに接続して、GnssToolKit3で動作確認ができる。室内だと窓際で1個補足できたぐらいでFIXすることはできなかった。

バイク用にバンク角もログれるGPSロガーがほしいという知り合いがいたのでとりあえずNMEAのGPSなのであとは独自のセンテンスを追加してやれば一緒にログれそうな気がする。

あとはツール側でそれを表示できるかどうか?とりあえずはGPXSeeっていうフリーソフトで試してみようかな。
まずは適当にログれるようなやつをM5 Atomあたりで作ってみようかな。

2025年6月28日土曜日

タミヤのスリムタイヤキットを自作してみる

 BLDCモータをベクトル制御できるようになったところで、やっぱり車体に搭載して動かしてみたいのでタイヤを作ってみることに。
タミヤのスリムタイヤキットはモータへの負担が小さく駆動率が高いということで、こういったダイレクトドライブモータとの相性が良いと思うんだけど、今回使用したモータに取り付けるのに加工しないといけないので、ホイールごとプリントしてもらえばいいかなと。

ホイールだけプリントしてスリムタイヤのゴムの部分だけ使っても良さそうだけど、実は大きなOリングをそのまま使えないかと言うことでOリングでやってみることに。(大学の時研究室でもOリングをタイヤにしていたし)
Oリングみたいなタイヤはモデル化のときとかに接地面が安定するので…

ということでOリングを使えるようなホイールの3Dモデルを作ってみた。スリムタイヤは横幅3.5mmっぽいのでOリングでも良い所ありそう。P-48だと外経も55mmっぽいしタミヤのスリムタイヤと同じ感じにできそう?
小さいほうがトルクが出そう?ということでP-46のOリングでホイールを設計してみた。

見た目がめっちゃプーリー。

モータとOリングを取り付けるとこんな見た目に。外経が52mmぐらいの予定。Oリングとぴったりサイズで作るとゆるゆるになったりするんじゃないかということで少し大きめに作ってOリングを引っ張るように調整しないといけないかも?

アリエクで線経が4mmで外径が52mmという少し太めのOリングを見つけてしまったのでとりあえず取り寄せてタイヤを作ってみようかな。250円で10本入りっぽいし。

ちなみにこのタイヤをユニバーサルプレートに装着して何かを作ろうとしてもキャスターのサイズが合わないのでキャスターとかも高さを調整する必要がありそう。まぁタミヤのギアボックス用にできてるもんな…

追記:
3Dプリントしてもらいました

2025年6月24日火曜日

BLDCモータドライバをCAN対応にしてみた。

 先日作ったFOC対応のBLDCモータドライバはCANトランシーバも積んでいるのでCANに対応させてみた。

こんな感じで電源とCANだけ接続してやればOK。I2Cみたいに束ねられるので配線が楽かも。マスター側のポートも1ポートで良いし。
基板設計時にもRS485かCANか迷ったけどとりあえずCANのほうがハードウェア側で色々やってくれるから楽なんじゃないかなということで。

とりあえずCANのプロトコルはOdrive風にしてみた。しかしOdriveとSimpleFOCだと単位が違うのが少し面倒。rpsとrad/sの変換を入れようとしてるけども、まずはSTM32F103でCAN通信をやりながらSimpleFOCでベクトル制御を動かせるかを試したいので値は変換しない状態でやってみようかと思う。

#include "SPI.h"
#include "SimpleFOC.h"
#include "SimpleFOCDrivers.h"
#include "encoders/MT6701/MagneticSensorMT6701SSI.h"
#include <EEPROM.h>
#include "stm32f103-can.h"

#define Protocol_Version 0x01
#define Hw_Version_Major 0x00
#define Hw_Version_Minor 0x00
#define Hw_Version_Variant 0x00
#define Fw_Version_Major 0x00
#define Fw_Version_Minor 0x00
#define Fw_Version_Revision 0x00
#define Fw_Version_Unreleased 0x00

#define NODE_ID_DEFAULT 0x000
#define NODE_BC 0x3F

uint16_t nodeID = 0x005;

#define MOTOR_POLE 7

#define CS1 PA15
#define SCK1 PB3
#define MISO1 PB4
#define MOSI1 PB5
#define DRV_EN PB12
#define TX3_SCL2 PB10
#define RX3_SDA2 PB11
#define CAN_RX PB8
#define CAN_TX PB9
#define V_CURR PA0
#define W_CURR PA1
#define U_CURR PA2
#define TEMP PA3
#define VOLTAGE PA4
#define LED_BUILTIN PC13

HardwareSerial Serial3(TX3_SCL2, RX3_SDA2);
SPIClass SPI_1(MOSI1, MISO1, SCK1);
MagneticSensorMT6701SSI sensor(CS1);
InlineCurrentSense current_sense = InlineCurrentSense(0.01, -50 * 4, W_CURR, V_CURR, U_CURR);
BLDCMotor motor = BLDCMotor(MOTOR_POLE);
BLDCDriver6PWM driver = BLDCDriver6PWM(PA8, PB13, PA9, PB14, PA10, PB15, DRV_EN);  //W V U EN

typedef union {  //IEEE 754 Float
  uint8_t uint[8];
  float value[2];
} FLOAT_BYTE_UNION;

uint8_t eepromRead(uint8_t reqID) {
  uint8_t lastData = 0xFF;
  for (uint16_t i = 0; i < EEPROM.length(); i += 2) {
    uint8_t dataID = EEPROM.read(i);
    uint8_t data = EEPROM.read(i + 1);
    //Serial1.println(dataID);
    if (dataID == reqID) {
      lastData = data;
      //Serial1.print("EEPROM Read:");
      //Serial1.println(i);
    }
    if (dataID == 0xFF | dataID == 0x00) {
      break;
    }
  }
  //Serial1.print("EEPROM Data:");
  //Serial1.println(lastData);
  return lastData;
}

void eepromWrite(uint8_t reqID, uint8_t data) {
  uint16_t lastID = 0xFFFF;
  for (uint16_t i = 0; i < EEPROM.length(); i += 2) {
    uint8_t eepromId = EEPROM.read(i);
    if (eepromId == 0xFF | eepromId == 0x00) {
      lastID = i;
      //Serial1.print("EEPROM Write:");
      //Serial1.println(i);
      break;
    }
  }
  if (lastID == EEPROM.length()) {
    lastID = 0;
  }
  EEPROM.write(lastID, reqID);
  EEPROM.write(lastID + 1, data);
}
void CANread() {
  if (CANMsgAvail()) {
    CAN_msg_t CAN_RX_msg;
    CANReceive(&CAN_RX_msg);
    uint16_t cmdID = uint16_t(CAN_RX_msg.id & 0x1F);
    uint16_t nodeIDrcv = CAN_RX_msg.id >> 5;
    CAN_msg_t CAN_TX_msg;
    memset(&CAN_TX_msg, 0, sizeof(CAN_msg_t));
    CAN_TX_msg.type = DATA_FRAME;
    CAN_TX_msg.format = STANDARD_FORMAT;
    CAN_TX_msg.id = ((nodeID << 5) | cmdID);
    if (nodeIDrcv == nodeID) {
      switch (cmdID) {
        // case 0x003:  //Get Motor Error
        //   break;
        // case 0x004:  //Get Encoder Error
        //   break;
        // case 0x005:  //Get Sensorless Error
        //   break;
        case 0x006:  //Set Axis Node ID
          if (CAN_RX_msg.type == DATA_FRAME) {
            uint32_t reqNodeID = CAN_RX_msg.data[0] | CAN_RX_msg.data[1] << 8 | CAN_RX_msg.data[2] << 16 | CAN_RX_msg.data[3] << 24;
            //Serial1.println(reqNodeID,HEX);
            if ((reqNodeID < 0x3F) && (reqNodeID != eepromRead(1))) {
              eepromWrite(1, reqNodeID);
              //Serial1.println("nodeID update");
            }
          }
          break;
        case 0x009:  //Get Encoder Estimates
          if (CAN_RX_msg.type == REMOTE_FRAME) {
            FLOAT_BYTE_UNION output;
            output.value[0] = motor.shaft_angle;
            output.value[1] = motor.shaft_velocity;
            memcpy(CAN_TX_msg.data, output.uint, 8);
            CAN_TX_msg.len = 8;
            CANSend(&CAN_TX_msg);
          }
          break;
        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];
          }
          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];
          }
          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];
          }
          break;
        // case 0x014:  //Get IQ
        //   break;
        case 0x015:  //Get Temperature(FET IEEE 754 Float, Motor IEEE 754 Float)
          if (CAN_RX_msg.type == REMOTE_FRAME) {
            FLOAT_BYTE_UNION output;
            output.value[0] = (1.43 - analogRead(ATEMP) * 3.3 / 4095) / 0.0043 + 25;
            output.value[1] = analogRead(AVREF);
            memcpy(CAN_TX_msg.data, output.uint, 8);
            CAN_TX_msg.len = 8;
            CANSend(&CAN_TX_msg);
          }
          break;
        case 0x017:  //Get Bus Voltage and Current
          if (CAN_RX_msg.type == REMOTE_FRAME) {
            FLOAT_BYTE_UNION output;
            output.value[0] = analogRead(VOLTAGE) * 3.3 / 4095.0 * 4.0;
            output.value[1] = motor.current.q;
            memcpy(CAN_TX_msg.data, output.uint, 8);
            CAN_TX_msg.len = 8;
            CANSend(&CAN_TX_msg);
          }
          break;
        // case 0x01c:  //Get ADC Voltage
        //   break;
        default:
          break;
      }
    }
    if (CAN_RX_msg.type == REMOTE_FRAME) {
      switch (cmdID) {
        case 0x000:  //Get Version
          CAN_TX_msg.data[0] = Protocol_Version;
          CAN_TX_msg.data[1] = Hw_Version_Major;
          CAN_TX_msg.data[2] = Hw_Version_Minor;
          CAN_TX_msg.data[3] = Hw_Version_Variant;
          CAN_TX_msg.data[4] = Fw_Version_Major;
          CAN_TX_msg.data[5] = Fw_Version_Minor;
          CAN_TX_msg.data[6] = Fw_Version_Revision;
          CAN_TX_msg.data[7] = Fw_Version_Unreleased;
          CAN_TX_msg.len = 8;
          CANSend(&CAN_TX_msg);
          break;
        case 0x001:  //Heartbeat Message
          CAN_TX_msg.data[1] = nodeID >> 8;
          CAN_TX_msg.data[0] = nodeID;
          CAN_TX_msg.len = 8;
          CANSend(&CAN_TX_msg);
          break;
        default:
          break;
      }
    }
  }
}
float target_angle = 0;
Commander command = Commander(Serial3);
void doTarget(char* cmd) {
  command.scalar(&target_angle, cmd);
}

void doMotor(char* cmd) {
  command.motor(&motor, cmd);
}

void doAnalog(char* cmd) {
  if (cmd[0] == 'T') Serial3.println(analogRead(TEMP));
  else if (cmd[0] == 'V') {
    Serial3.print("Voltage: ");
    Serial3.println(analogRead(VOLTAGE) * 0.0032);
  }
};

void setup() {
  analogReadResolution(12);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);
  Serial3.setTx(TX3_SCL2);
  Serial3.setRx(RX3_SDA2);
  Serial3.begin(115200);
  SimpleFOCDebug::enable(&Serial3);

  sensor.init(&SPI_1);
  motor.linkSensor(&sensor);
  driver.voltage_power_supply = 8;
  driver.init();
  motor.linkDriver(&driver);
  current_sense.linkDriver(&driver);
  // control loop type and torque mode
  motor.torque_controller = TorqueControlType::foc_current;
  motor.controller = MotionControlType::velocity;
  motor.motion_downsample = 0.0;

  // 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;
  // current q loop PID
  motor.PID_current_q.P = 1.5;
  motor.PID_current_q.I = 15.0;
  motor.PID_current_q.D = 0.0;
  motor.PID_current_q.output_ramp = 1000.0;
  motor.PID_current_q.limit = 5.0;
  // Low pass filtering time constant
  motor.LPF_current_q.Tf = 0.005;
  // current d loop PID
  motor.PID_current_d.P = 1.5;
  motor.PID_current_d.I = 15.0;
  motor.PID_current_d.D = 0.0;
  motor.PID_current_d.output_ramp = 1000.0;
  motor.PID_current_d.limit = 5.0;
  // Low pass filtering time constant
  motor.LPF_current_d.Tf = 0.02;
  // Limits
  motor.velocity_limit = 200.0;
  motor.voltage_limit = 5.0;
  motor.current_limit = 2.0;
  // sensor zero offset - home position
  motor.sensor_offset = 0.0;
  // general settings
  // pwm modulation settings
  motor.foc_modulation = FOCModulationType::SpaceVectorPWM;
  motor.modulation_centered = 1.0;

  motor.useMonitoring(Serial3);

  motor.linkCurrentSense(&current_sense);
  current_sense.init();

  motor.init();
  motor.sensor_direction = Direction::CW;
  motor.initFOC();

  command.add('T', doTarget, "target angle");
  command.add('M', doMotor, "motor");
  command.add('A', doAnalog, "analog read");
  motor.monitor_downsample = 0;
  //motor.monitor_variables = 0;

  Serial3.println(F("Motor ready."));
  Serial3.println(F("Set the target angle using serial terminal:"));

  bool ret = CANInit(CAN_250KBPS, 2);//PB8,PB9
  nodeID = eepromRead(1);
  if (nodeID >= 0x3F) {
    eepromWrite(1, NODE_ID_DEFAULT);
    nodeID = NODE_ID_DEFAULT;
  }
  CAN1->FMR |= 0x1UL;
  CAN1->FMR &= 0xFFFFC0FF;
  CAN1->FMR |= 0x1C << 8;
  CANSetFilter(0, 1, 0, 0, ((nodeID << 5) << 21), 0xFC000004);
  CANSetFilter(1, 1, 0, 0, ((NODE_BC << 5) << 21), 0xFC000004);
  CAN1->FMR &= ~(0x1UL);

  _delay(1000);
}

void loop() {
  motor.loopFOC();
  motor.move(target_angle);
  motor.monitor();
  command.run();
  CANread();
}

Odriveのほうもバージョンによってコマンドが変わっていたりするけどとりあえず使いそうなところだけ有効にしてみた。ノードIDはEEPROMに記録するようにした。同じファームを書いてあとからCANでノードIDを書き換えられる。ODriveではモードを切り替えないと現在のモード以外でのコマンドは受け付けないけど、モード切替なしにデータを受け取ったモードに自動で変更するようにしてみた。

CANのライブラリは以前フィルタを試したときに使わせてもらったスケッチと同じように元ネタのstm32f103.inoのsetupとloopを削除して"stm32f103-can.h"として別ファイルにして使用してみた。

いちいちCANでコマンドを入力するのが面倒なので簡易的なPythonツールを作成しようかなと思う。もしくはESP32で直接やってしまったほうが早いかなぁ?

あと通信が切れたときのためにモータの動作を止められるように通信Watchdogをつけておいたほうがいいかもしれないかなぁ。モータが回りっぱなしになってしまうし。

2025年6月22日日曜日

FOCモータドライバをチューニングしてみた。

 前回ようやく自作のFOCモータドライバでモータを回転させることができたけども、前回の設定だとSimpleFOCのVoltageモードの角度制御だったので、今回はモータのトルクを最大限使用できるようにベクトル制御で動かしてみる。インライン電流センサ3個積んでるのはこのため。(2個でもいけるけど念の為)

VoltageモードでもセンサレスESCに接続して動作確認したときはだいぶトルクが有る感じがした。ちなみにそのままFOC Currentに切り替えたらハンチングですごい振動していた。

スケッチの中でPIDの値をいじるのはちょっと面倒なので、SimpleFOCのチューニングをGUIでできるというSimpleFOCStudioを使ってみた。このツールはPythonで作られているっぽい。PythonはThonnyで実行していたのでThonnyの環境でも実行できるかどうか確認してみた。(Condaとかの環境インストール面倒だったし…

まずはGithubのSimpleFOCStudioから最新のソースコードをZIPでダウンロードしてくる。

解凍したらThonnyでsimpleFOCStudio.pyを開く。Thonnyからシステムシェルを開いて

pip install PyQt5 numpy pyqtgraph

のような感じで必要なライブラリをインストール。あとは実行するだけ。Thonny 4.1.7で普通に動いてしまった。

Windowが開いたら左上のアイコンからTreeViewで新しい窓を作って、Commandのところにスケッチ側で入れたコマンド(前回のコードだとM)を入れてConfigureでCOMポートの設定をすれば良い。

とりあえずモータドライバに5Vを接続してFOC Currentのチューニングをしてみることに。

とりあえずはPを少しずつ上げていって、ハンチングしないギリギリでIを上げてみたいな感じで少しずつ調整してみた。電流を流せるようにSpace Vector PWMにして、とりあえずAngleモードのFOC Current制御で手でモータを動かしたときに戻ってくるときにハンチングしないかどうかである程度決めたあとに角度を小さく降ったり大きく降ったり動かしてみながらハンチングしない所を探っていった。
このやり方が正しいかどうかはよくわからないけど…

チューニングができたらArduinoの設定値として生成してくれるのでこれをスケッチの中のモータ関連の設定値のあたりと置き換えればチューニングした値をデフォルト設定にできる。

ラジコン用ESCでこのモータの動作確認したときはトルクが少し弱かったので大丈夫かなぁと思っていたけどこれならベクトル制御で結構トルク出せそうですごい。

まだ電圧上げられそうなので電圧との関係も見ながらチューニングしてみようかな。