DJI RoboMaster M3508P19とC620をArduinoからCAN通信で動かす

教育用のロボット部品として販売されている,RoboMaster用モーターですが,仕事面でもかなり使えそうということで,とある案件のために試験的に購入してみました.
価格はモーターとコントローラ合わせて2万円くらいで,ちょっとお高めですが,このサイズ+パワーかつ,コントローラも扱いやすいとなると,かなりお買い得感があります.
実際,サイズが結構小さいので,スペック上は要求スペックを満たしていてもパワーが足りず,扱いづらいんじゃないかと思っていたのですが,パワフルすぎて逆にびっくりしています.
その分,発熱も結構あります.

Arduinoから動かす

このモーターの難点というか少し面倒くさいところとして,プロトコルがCANとPWM(PPM)しか無いということです.
回転数や角度のフィードバックが欲しいならCANは必須です.
しかし,ArduinoにはCANポートはありません.
STマイコンやESPだとついているんですけど...
それで若干避けていたという部分もあったのですが,MCP2515というCANをSPIに変換するモジュールを使用すると,30分くらいですんなり動かせました.
ただ,実際に動かしてみたって記事が少ないので,今回こうして記事にかいているわけですが,もしかしたら簡単すぎて誰も記事に書いていないだけかもしれません.

本日のレシピ

材料

XT60コネクタはいっぱい持っていますが,XT30は持っていなかったので近くのラジコンショップに駆け込んだらありました.

配線

こんな感じに配線してください.
わかりづらい画像ですみません.


今回はモーターとArduinoは1対1で接続していますので,お互い終端抵抗をつけます.
MCP2515はジャンパピンをつけることで終端抵抗がONになります.
コントローラはスライドスイッチをONにするだけです.

プロトコル

普通のCAN(CAN HS)です.
通信スピードは1MbpsでDLCは8バイトです.

送信

モーターにはそれぞれ1ー8の中でIDを割り振ることができ,最大8個まで同一ネットワークで動作させることができます.
モーターを動かすには,電流値指定して送信する必要があります.
一つのCANIDで4つまでモーターを動かすことができるので,効率的にモーターを動かすことができます.
最大最小は-20A〜20Aとなっており,-16384〜0〜16384の範囲に変換し,2バイトに分けて送ります.

ID:0x200 モータID データ
Data[0] モーター1 上位バイト
Data[1] モーター1 下位バイト
Data[2] モーター2 上位バイト
Data[3] モーター2 下位バイト
Data[4] モーター3 上位バイト
Data[5] モーター3 下位バイト
Data[6] モーター4 上位バイト
Data[7] モーター4 下位バイト
ID:0x1FF モータID データ
Data[0] モーター5 上位バイト
Data[1] モーター5 下位バイト
Data[2] モーター6 上位バイト
Data[3] モーター6 下位バイト
Data[4] モーター7 上位バイト
Data[5] モーター7 下位バイト
Data[6] モーター8 上位バイト
Data[7] モーター8 下位バイト

受信

受信はそれぞれのモーターからデータが送られてきます.
受信するCANIDが20Xとなっており,Xには設定したモーターIDが割り当てられています.
例えばモーターコントローラのIDを1にしたら,CANID:0x201でフィードバック情報が送られてきます.

角度は0~360の範囲が0-8192で送られてきます.
rpmはそのままの数値です.
電流(トルク)はおそらく-16384〜0〜16384の範囲です.
温度はそのままの数値です.

ID:0x20X モータID データ
Data[0] 角度 上位バイト
Data[1] 角度 下位バイト
Data[2] rpm 上位バイト
Data[3] rpm 下位バイト
Data[4] 電流(トルク) 上位バイト
Data[5] 電流(トルク) 下位バイト
Data[6] 温度 1バイト
Data[7] 割り当てなし 割り当てなし

プログラム

Arduino Megaのスケッチの参考例を載せます.
今回はmcp_can(https://github.com/coryjfowler/MCP_CAN_lib)
というライブラリを使用します.
MCP2515自体は,SPI通信でArduinoと繋がっています.

motor_ouput_current_Aの変数に電流値を入れることで制御できるようになっています.
最大最小は-20A〜20Aですのでその範囲内を指定してください.
パワーがすごいので最初は1Aくらいから試してみると良いと思います.
シリアルモニタを開くとフィードバック値を表示するようにしています.

#include <mcp_can.h>
#include <SPI.h>

long unsigned int rxId;
unsigned char len = 0;
unsigned char rxBuf[8];
byte txBuf[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
long Pre_millis;
MCP_CAN CAN0(53);

int16_t fmap(double x, double in_min, double in_max, int16_t out_min, int16_t out_max);

void setup()
{
  Serial.begin(115200);
  if (CAN0.begin(MCP_ANY, CAN_1000KBPS, MCP_8MHZ) == CAN_OK)
  {
    Serial.println("CAN0: Init OK!");
    CAN0.setMode(MCP_NORMAL);
  }
  else
  {
    Serial.println("CAN0: Init Fail!");
  }

  Pre_millis = millis();
}

void loop()
{
  double motor_ouput_current_A = 0.0;
  int16_t motor_ouput_current_Byte = fmap(motor_ouput_current_A, 0, 20, 0, 16384);//2バイトに変換
  txBuf[0] = (motor_ouput_current_Byte >> 8) & 0xFF;//上位バイト
  txBuf[1] = motor_ouput_current_Byte & 0xFF;//下位バイト

  //Send
  if (millis() - Pre_millis > 20) { // Period: 20ms
    CAN0.sendMsgBuf(0x200, 0, 8, txBuf);
    Pre_millis = millis();
  }

  //Receive
  if (CAN0.checkReceive() == CAN_MSGAVAIL)
  {
    CAN0.readMsgBuf(&rxId, &len, rxBuf);
    Serial.print("Recive ID: ");
    Serial.print(rxId, HEX);
    Serial.print(" Data: ");

    int16_t angle = rxBuf[0] << 8 | rxBuf[1];
    int16_t rpm = rxBuf[2] << 8 | rxBuf[3];
    int16_t amp = rxBuf[4] << 8 | rxBuf[5];
    int8_t  temp = rxBuf[6];

    Serial.print(angle);
    Serial.print(",");
    Serial.print(rpm);
    Serial.print(",");
    Serial.print(amp);
    Serial.print(",");
    Serial.println(temp);
  }
}

int16_t fmap(double x, double in_min, double in_max, int16_t out_min, int16_t out_max) {
  return (x - in_min) * ((double)(out_max - out_min)) / (in_max - in_min) + out_min;
}

動作の様子