ESP32でCAN通信(データ衝突の回避)

ESP32でCAN通信(データ衝突の回避)

Tags
Software Development
Arduino
通信
Published
July 30, 2022
前回に引き続きCAN通信に関する記事です

CANのメリットといえばマルチマスター通信

ESP32でCAN通信を1:1でする記事はいくらでも出てきました。
しかしマルチマスターをする記事は見つかりませんでした。
今回はマルチマスターをする上で必要になるデータ衝突の回避を頑張ってやってみたので備忘録がてらまとめようと思います。
参考になったらTwitterなどで役立ちました等ツイートしていただけると私が報われます。お願いします。

CAN通信はデータ衝突が起こる

CANはマルチマスターです。
ですのでどの機器からでも自由にデータを送信できます。
なので複数から同時にデータが送信されると、通信ラインはぶっ壊れます
notion image
しかし、CAN通信(というよりドライバ)はとても優秀です。
なんと、データの衝突を検知できます。
notion image
データ衝突(受信エラー)を検知すると自動的に再送信してくれます。
しかし、残念ながら複数が同時に再送信を始めてしまうと、またまたデータ衝突が起こってしまいます。
これだとコリジョンループが発生してしまいます。どうしましょう。
notion image

どうやって回避する?

衝突を回避する方法を調べてみましたが、全体的にまだ模索中みたいな回答がほとんどでした。画期的な方法は今のところなさそうです。

見つけられた方法

  • プライオリティをつける
  • CSMA/CD
  • IDアドレッシング
あと有線LANもCANと同じくネットワークなのでLAN周りでも調べてみました。
データ衝突のことをコリジョンと言うそうです。このワードで調べてみると有線LANなどの話が出てくるので一度目を通してみると勉強になると思います。

CSMA/CDを使った回避プログラム

これは何かというと、送信エラーを検知したら「デバイスごとにランダムに数ミリ秒待ち、それぞれがバラバラのタイミング再送信をする」という手段です。
notion image
notion image
CANと同じく、物理ワイヤーでネットワークを構成する有線LANではこのような方法がとられていました(が最近は半二重ではないので使われていないらしいです)
CSMA/CDなら実装するのはそこまで難しくないと思いました。

CSMA/CDを使った実装実例を探した

Evan Sebranekさんが書いているこの記事では6台のArduinoがCANバス上に接続されており、CSMA/CD方式を使ってランダムに空き時間を作ることでデータ衝突を回避しています。
これによるデメリットはもちろんあります。時間を置いてから再送するので時間がかかります。
リアルタイム性の求められて、5msすら勿体無いロボカップジュニアのサッカーロボットなどには流石に厳しいと思います。

IDアドレッシングで回避する

デバイスが固有のIDを個々に持ち、「自分のIDに対して送信リクエストが飛んできたら送り返す」といったものです。
notion image
I2CやSPI通信と同じです。注文をしたら欲しいお寿司が流れてくる回転寿司🍣
今回はこれに取り組んでみました。

IDアドレッシングでデータ衝突回避

  • 今回はちゃんとコメントも書きました。偉すぎます。
  • リクエストにはRTR(Remote Transmission Request)を使いました。
  • このPDFがCAN通信についてかなり詳しくまとまっていてよかったです。
📌
マルチマスターなCAN通信に置いてSlave/Masterという表記は間違っているかもしれませんが、I2Cなどで一般的なSlave/Masterの構図をCAN通信で再現してみました。

データを送ってくれとリクエストする側(Master)

// Master #include <CAN.h> #include <Ticker.h> const int LED_PIN = 2; //生存確認用LED Ticker tick; //生存確認用Ticker void onReceive(int packetSize); // CAN受信時に呼び出される関数(プロトタイプ宣言) void setup() { // 生存確認のタイマー割り込み設定 pinMode(LED_PIN, OUTPUT); tick.attach_ms(1000, []() { digitalWrite(LED_PIN, !digitalRead(LED_PIN)); }); // シリアル初期化 Serial.begin(2000000); while (!Serial) ; Serial.println("CAN Receiver Callback"); // CAN通信を初期化 CAN.setPins(26, 25); // CAN_RX, CAN_TX if (!CAN.begin(500E3)) { // 500kbpsで初期化 Serial.println("Starting CAN failed!"); while (1) ; } // CAN受信割り込みコールバック関数を設定 CAN.onReceive(onReceive); } // CAN受信時に呼び出される関数 void onReceive(int packetSize) { Serial.print("Received "); if (CAN.packetExtended()) { Serial.print(" extended "); } if (CAN.packetRtr()) { // Remote transmission request, packet contains no data Serial.print("RTR "); } Serial.print("packet with id 0x"); Serial.print(CAN.packetId(), HEX); if (CAN.packetRtr()) { Serial.print(" and requested length "); Serial.println(CAN.packetDlc()); } else { Serial.print(" and length "); Serial.println(packetSize); // only print packet data for non-RTR packets while (CAN.available()) { Serial.print((char)CAN.read()); } Serial.println(); } Serial.println(); } void loop() { CAN.beginPacket(0x100, 5, true); // Slaveにデータ送信のリクエスト(RTR設定) CAN.endPacket(); // Slaveにデータ送信のリクエスト(送信) delay(1000); }

リクエストが来たらデータを送る側(Slave)

// Slave #include <CAN.h> #include <Ticker.h> const int MODULE_ID = 0x100; // Slaveモジュール(このESP32)のユニークID const int LED_PIN = 2; //生存確認用LED Ticker tick; //生存確認用Ticker void onReceive(int packetSize); // CAN受信時に呼び出される関数(プロトタイプ宣言) void setup() { // 生存確認のタイマー割り込み設定 pinMode(LED_PIN, OUTPUT); tick.attach_ms(1000, []() { digitalWrite(LED_PIN, !digitalRead(LED_PIN)); }); // シリアル初期化 Serial.begin(2000000); while (!Serial) ; Serial.println("CAN Receiver Callback"); // CAN通信を初期化 CAN.setPins(26, 25); // CAN_RX, CAN_TX if (!CAN.begin(500E3)) { // 500kbpsで初期化 Serial.println("Starting CAN failed!"); while (1) ; } //↓なぜか受信割り込み中にCAN.endPacket()するとメモリエラーを起こすので使わないことにしています。 // CAN.onReceive(onReceive); } // CAN受信時に呼び出される関数 void onReceive(int packetSize) { Serial.print("Received "); Serial.print("filter:"); Serial.print(CAN.filter(MODULE_ID)); // フィルター設定 if (CAN.packetExtended()) { Serial.print(" extended "); } if (CAN.packetRtr()) { // Remote transmission request, packet contains no data Serial.print("RTR "); } Serial.print("packet with id 0x"); Serial.print(CAN.packetId(), HEX); if (CAN.packetRtr()) { // RTRパケットの場合(送信リクエストが来た場合) Serial.print(" and requested length "); Serial.println(CAN.packetDlc()); //送信依頼の来ているデータのバイト数の表示 if (CAN.packetId() == MODULE_ID) { //パケットIDが自分のIDに一致した場合 Serial.println("Received packet is correct"); CAN.beginPacket(0x44); // パケットを送信するためにbeginPacket()を呼び出す CAN.write('H'); //データ(1byte目) CAN.write('E'); //データ(2byte目) CAN.write('L'); //データ(3byte目) CAN.write('L'); //データ(4byte目) CAN.write('O'); //データ(5byte目) CAN.endPacket(); // パケットを送信 Serial.println("sent packet"); } else { Serial.println("Received packet is incorrect"); } } else { Serial.print(" and length "); Serial.println(packetSize); // only print packet data for non-RTR packets while (CAN.available()) { Serial.print((char)CAN.read()); } Serial.println(); } Serial.println(); } void loop() { int packetSize = CAN.parsePacket(); //パケットサイズの確認 if (packetSize) { // CANバスからデータを受信したら onReceive(packetSize); //受信時に呼び出される関数を呼び出す } }
 

通信に成功するとこうなります(Master側)

notion image

ぶつかった壁

今回はCAN受信割り込みを使ってみました。
CAN.onReceive(onReceive)で設定できます
これがちゃんと使えるとすごく便利なのですが、割り込み関数内でCAN.endPacket()をするとメモリエラーを起こしてしまいまして、Slave側ではポーリング処理に置き換えました。

メモリエラーを引き起こすプログラムとコンソール

InstrFetchProhibitedエラーを引き起こしています。
ポーリング側は空なので変なエラーは出ないと思うのですが、なんででしょうか。
これの治し方わかる人がいらっしゃれば教えてください。
コメントかTwitterのリプライでお願いします。
notion image
// Slave #include <CAN.h> #include <Ticker.h> const int MODULE_ID = 0x100; // Slaveモジュール(このESP32)のユニークID const int LED_PIN = 2; //生存確認用LED Ticker tick; //生存確認用Ticker void onReceive(int packetSize); // CAN受信時に呼び出される関数(プロトタイプ宣言) void setup() { // 生存確認のタイマー割り込み設定 pinMode(LED_PIN, OUTPUT); tick.attach_ms(1000, []() { digitalWrite(LED_PIN, !digitalRead(LED_PIN)); }); // シリアル初期化 Serial.begin(2000000); while (!Serial) ; Serial.println("CAN Receiver Callback"); // CAN通信を初期化 CAN.setPins(26, 25); // CAN_RX, CAN_TX if (!CAN.begin(500E3)) { // 500kbpsで初期化 Serial.println("Starting CAN failed!"); while (1) ; } //↓なぜか受信割り込み中にCAN.endPacket()するとメモリエラーを起こすので使わないことにしています。 CAN.onReceive(onReceive); } // CAN受信時に呼び出される関数 void onReceive(int packetSize) { Serial.print("Received "); Serial.print("filter:"); Serial.print(CAN.filter(MODULE_ID)); // フィルター設定 if (CAN.packetExtended()) { Serial.print(" extended "); } if (CAN.packetRtr()) { // Remote transmission request, packet contains no data Serial.print("RTR "); } Serial.print("packet with id 0x"); Serial.print(CAN.packetId(), HEX); if (CAN.packetRtr()) { // RTRパケットの場合(送信リクエストが来た場合) Serial.print(" and requested length "); Serial.println(CAN.packetDlc()); //送信依頼の来ているデータのバイト数の表示 if (CAN.packetId() == MODULE_ID) { //パケットIDが自分のIDに一致した場合 Serial.println("Received packet is correct"); CAN.beginPacket(0x44); // パケットを送信するためにbeginPacket()を呼び出す CAN.write('H'); //データ(1byte目) CAN.write('E'); //データ(2byte目) CAN.write('L'); //データ(3byte目) CAN.write('L'); //データ(4byte目) CAN.write('O'); //データ(5byte目) CAN.endPacket(); // パケットを送信 Serial.println("sent packet"); } else { Serial.println("Received packet is incorrect"); } } else { Serial.print(" and length "); Serial.println(packetSize); // only print packet data for non-RTR packets while (CAN.available()) { Serial.print((char)CAN.read()); } Serial.println(); } Serial.println(); } void loop() { }
 
ajinori3からのコメント
ajinori3からのコメント