- Arduinoを使用したCAN通信で複数データの送受信はどうやって行うのだろう?
- 一度に複数のデータが送られてきたときに、受信側はどうやって受け取る?
- 単発データならまだしも、複数データだと取りこぼしが発生してしまうのでは?
今回はそんな疑問にお答えします。
CAN通信で単発のデータの送受信ができるようになっても複数データとなるとどうやって取得したらいいか分からなかったりしますよね。送信側によるデータ送信周期が遅ければなんとなくデータの取りこぼしも発生しないような気がしますが、同時多発的に送られてきたときにすべてのデータを漏らすことなく受信する方法が分からない。
答えは"割り込み"といって、CANを受信したら都度データを取得する関数を発動させる仕組みを使って取りこぼしを防止します。この記事ではCANのハード的なお話から割り込みの仕組みまで丁寧に解説していきます。
前回記事にてArduinoとMCP2515との接続方法や、動作に必要なライブラリのインストール方法を掲載しておりますので、必ず参照をお願いします。
-
-
CANを使ってArduino2台で簡単にデータ送受信を行う方法
Arduinoを2台使ってデータ通信を行いたいけど、どうすればいいんだろ? マイコンの通信方法はI2CやSPIがあるけど、やり方がよく分からないし・・・ 大容量のデータ通信じゃなくてとりあえず1バイト ...
続きを見る
まずはCANのハード的な解説
ArduinoでCAN通信を行うためにはMCP2515という外部モジュールが必要になります。
ArduinoからMCP2515に対してフォーマットに従ったデータ(電気信号)を送れば、あとはMCP2515側が勝手に送信をしてくれます。そして相手側からデータが来た場合も自動的に受信処理を行ってくれます。
CAN通信は設定次第では送信も受信も同時に行うことが可能ですが、今回の図では便宜的に左側を送信専用、右側を受信専用としました。
MCP2515にはRX0とRX1という2つの8Byteバッファがありまして、CANでデータを受信した場合そこに自動的にデータを格納します。ArduinoからCAN.readMsgBufという関数を使用するとMCP2515に格納されているデータを取得可能になり、またバッファをクリアしてあげることができます。
そうすることでMCP2515はまた新たなデータをバッファに格納できるのですが、RX0/RX1両方埋まった状態で新たなデータが来た場合はオーバーフローとなり、それ以上はデータの受信ができなくなってしまいます。
なのでArduino側はCANでデータを受信するたびに割り込みを発生させて、都度CAN.readMsgBufを行いバッファのデータ取得&クリアを行います。Arduinoに限らずどのマイコンにも割り込みコントローラーというものがあり、設定次第では特定のピンに電気信号が送られたらそれをCPUに通知する機能があります。
ArduinoのCAN通信では2番ピンが割り込みコントローラーに接続されているので、MCP2515からはデータを受信したら2番ピンに電気信号を送らせ、割り込みが発生したらバッファを読み込みに行く処理にします。
サンプルコード
今回は3種類のCAN IDのデータを数秒おきに送り、受信側はそれをシリアルモニタに表示するだけの単純なプログラムを作成したいと思います。
CAN ID(16進数) | データ |
0x100 | int intData = 1234 |
0x101 | float floatData = 56.78 |
0x102 | char charData = 'A' |
送信側
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#include <SPI.h> // SPI通信ライブラリをインクルード #include "mcp_can.h" // MCP_CANライブラリをインクルード const int SPI_CS_PIN = 10; // MCP2515のCSピン番号を定義 MCP_CAN CAN(SPI_CS_PIN); // MCP_CANオブジェクトを作成(CSピンを指定) void setup() { Serial.begin(115200); // シリアル通信を115200bpsで開始 // CANコントローラ初期化(正常に初期化できるまで繰り返す) while (CAN_OK != CAN.begin(MCP_ANY, CAN_500KBPS, MCP_8MHZ)) { Serial.println("CAN BUS init failed, retrying..."); // 初期化失敗メッセージを出力 delay(100); // 100ms待機して再試行 } CAN.setMode(MCP_NORMAL); // CANコントローラを通常モードに設定 Serial.println("CAN BUS Sender Ready"); // 初期化完了メッセージ } void loop() { int intData = 1234; // 送信するint型データを定義 float floatData = 56.78; // 送信するfloat型データを定義 char charData = 'A'; // 送信するchar型データを定義 // --- ID 0x100: intデータ送信 --- CAN.sendMsgBuf(0x100, 0, sizeof(intData), (uint8_t*)&intData); // intデータを送信 Serial.println("Sent int data: 1234"); // シリアルモニタに送信内容を表示 delay(3000); // 3000ms待機 // --- ID 0x101: floatデータ送信 --- CAN.sendMsgBuf(0x101, 0, sizeof(floatData), (uint8_t*)&floatData); // floatデータを送信 Serial.println("Sent float data: 56.78"); // シリアルモニタに送信内容を表示 delay(3000); // 3000ms待機 // --- ID 0x102: charデータ送信 --- CAN.sendMsgBuf(0x102, 0, sizeof(charData), (uint8_t*)&charData); // charデータを送信 Serial.println("Sent char data: 'A'"); // シリアルモニタに送信内容を表示 delay(5000); // 5秒待機(次のループまでの間隔) } |
受信側
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
#include <SPI.h> // SPI通信ライブラリをインクルード #include "mcp_can.h" // MCP_CANライブラリをインクルード const int SPI_CS_PIN = 10; // MCP2515のCSピン番号を定義 const int CAN_INT_PIN = 2; // MCP2515のINTピン番号を定義 MCP_CAN CAN(SPI_CS_PIN); // MCP_CANオブジェクトを作成(CSピンを指定) volatile bool canInterrupt = false; // 割り込みフラグを定義(割り込みハンドラで設定) // 割り込みハンドラ:MCP2515から割り込み信号を受けた時に呼ばれる void handleCANInterrupt() { canInterrupt = true; // 割り込みフラグをONにする } void setup() { Serial.begin(115200); // シリアル通信を115200bpsで開始 // CANコントローラ初期化(正常に初期化できるまで繰り返す) while (CAN_OK != CAN.begin(MCP_ANY, CAN_500KBPS, MCP_8MHZ)) { Serial.println("CAN BUS init failed, retrying..."); // 初期化失敗メッセージを出力 delay(100); // 100ms待機して再試行 } CAN.setMode(MCP_NORMAL); // CANコントローラを通常モードに設定 attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), handleCANInterrupt, FALLING); // INTピンに割り込みを設定 Serial.println("CAN BUS Receiver Ready"); // 初期化完了メッセージ } void loop() { // 割り込みフラグが立っている場合のみ処理 if (canInterrupt) { canInterrupt = false; // 割り込みフラグをクリア long unsigned int rxId; // 受信したCAN ID格納用変数 unsigned char len = 0; // 受信データ長格納用変数 unsigned char rxBuf[8]; // 受信データ格納用バッファ // CANメッセージを読み込み(成功した場合のみ処理) if (CAN.readMsgBuf(&rxId, &len, rxBuf) == CAN_OK) { Serial.print("Received ID: 0x"); // 受信したCAN IDを表示 Serial.println(rxId, HEX); // 16進数でIDを出力 // 受信したIDによって処理を分岐(switch文) switch (rxId) { case 0x100: { // ID 0x100の場合(intデータ) int receivedInt; // 受信intデータ格納用 memcpy(&receivedInt, rxBuf, sizeof(int)); // バイト配列からintへコピー Serial.print("Received int: "); // intデータを表示 Serial.println(receivedInt); break; } case 0x101: { // ID 0x101の場合(floatデータ) float receivedFloat; // 受信floatデータ格納用 memcpy(&receivedFloat, rxBuf, sizeof(float)); // バイト配列からfloatへコピー Serial.print("Received float: "); // floatデータを表示 Serial.println(receivedFloat, 2); // 小数点以下2桁表示 break; } case 0x102: { // ID 0x102の場合(charデータ) char receivedChar; // 受信charデータ格納用 memcpy(&receivedChar, rxBuf, sizeof(char)); // バイト配列からcharへコピー Serial.print("Received char: "); // charデータを表示 Serial.println(receivedChar); break; } default: // 未知のIDの場合 Serial.println("Unknown CAN ID"); // 未知IDメッセージを表示 break; } } } } |
解説
送信側は特に問題ないと思いますので、受信側を解説します。
まずはCAN受信の仕組みを理解するためにものすごく単純なプログラムを作成してみました。
CAN受信が発生(2番ピンに電気信号が送られる)したら割り込み関数を発動してhandleCANInterrupt関数が呼ばれるように設定します。そしてhandleCANInterrupt関数の中でcanInterrupt(割り込みフラグ)をONにします。
void loop()の中でcanInterrupt(割り込みフラグ)を判定し、フラグがONであればCAN.readMsgBufでバッファのデータを取得します。
CAN.readMsgBufの関数だけでなく
- CAN0.readMsgBuf
- CAN1.readMsgBuf
という関数もあります。こちらを使うとRX0/RX1を指定してバッファからの読み出しが可能になります。
とはいえCAN.readMsgBufはバッファが埋まっている方を読み出しに行くので、基本的にはこの関数を使えばよいでしょう
このプログラムの問題点
割り込みによって「割り込みフラグ」をONにしにいって、loopの中で実際にバッファデータの取得を行うわけですが、割り込みが行われてloop関数が発動するまでに連続でCANデータが送られた場合、取りこぼしが発生してしまいます。
なのでこの問題を解決するために以下の方法を採りたいと思います。
- Arduino側でCANデータを格納するためのバッファを用意しておく
- 構造体配列でCANID、データ長、データが保存できるようにする
- データはとりあえず8Byte分配列用意しておいて、そこにデータを入れておく
- データは使う際に、必要な部分だけ取り出す
- 割り込みが発生するたびにMCP2515バッファ→Arduinoバッファにデータを退避させる
- void loop()の方で、退避させたArduinoバッファデータを使ってシリアルモニタ表示を行う
- データの格納/取り出し方法は以前このブログでも取り上げたFIFO方式とする
改良版プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
#include <SPI.h> // SPI通信ライブラリをインクルード #include "mcp_can.h" // MCP_CANライブラリをインクルード const int SPI_CS_PIN = 10; // MCP2515のCSピン番号を定義 const int CAN_INT_PIN = 2; // MCP2515のINTピン番号を定義 MCP_CAN CAN(SPI_CS_PIN); // MCP_CANオブジェクトを作成(CSピンを指定) #define SIZE 10 // 割り込みで使用するバッファ構造体配列の要素数を定義(とりあえず10に設定) // 割り込みで使用するバッファの構造体を定義 typedef struct { long unsigned int rxId; // 受信したIDを格納するための変数 unsigned char len; // 受信したフレームのデータ長を格納するための変数 unsigned char rxBuf[8]; // 受信したデータを格納するための変数 } ReceiveBuf; // バッファの構造体変数配列を宣言 static ReceiveBuf RecBuf[SIZE]; // バッファ構造体を操作するための変数を宣言 static int front; // キューの先頭を指すインデックス(取り出し側) static int rear; // キューの末尾を指すインデックス(追加側) static int count; // キュー内の要素数を管理する変数 // 割り込みハンドラ:MCP2515から割り込み信号を受けた時に呼ばれる void handleCANInterrupt() { if (count == SIZE) { // バッファの要素数が最大サイズと等しい場合、満杯と判定 Serial.println("RxBuf is full"); //バッファがいっぱいであることを表示 return -1; //関数を異常終了させる } else { if (CAN.readMsgBuf(&RecBuf[rear].rxId, &RecBuf[rear].len, RecBuf[rear].rxBuf) == CAN_OK) { // バッファからID,データ長,データを取得 count++; // 要素数を増やす rear = (rear + 1) % SIZE; // rear を次の位置に移動(循環バッファ対応) return 0; // 関数を正常終了させる } else { // バッファからデータを取得できなかった場合 return -1; //関数を異常終了させる } } } void setup() { Serial.begin(115200); // シリアル通信を115200bpsで開始 // CANコントローラ初期化(正常に初期化できるまで繰り返す) while (CAN_OK != CAN.begin(MCP_ANY, CAN_500KBPS, MCP_8MHZ)) { Serial.println("CAN BUS init failed, retrying..."); // 初期化失敗メッセージを出力 delay(100); // 100ms待機して再試行 } CAN.setMode(MCP_NORMAL); // CANコントローラを通常モードに設定 attachInterrupt(digitalPinToInterrupt(CAN_INT_PIN), handleCANInterrupt, FALLING); // INTピンに割り込みを設定 Serial.println("CAN BUS Receiver Ready"); // 初期化完了メッセージ } void loop() { // バッファにデータがある場合のみ処理 while (count > 0) { count--; // 要素数を減らす long unsigned int t_rxId; // 受信したIDを格納するためのテンポラリ変数 unsigned char t_len; // 受信したフレームのデータ長を格納するためのテンポラリ変数 unsigned char t_rxBuf[8]; // 受信したデータを格納するためのテンポラリ変数 t_rxId = RecBuf[front].rxId; // CANIDをバッファからテンポラリ変数に格納 t_len = RecBuf[front].len; // データ長をバッファからテンポラリ変数に格納 memcpy(&t_rxBuf, RecBuf[front].rxBuf, 8); // データをバッファからテンポラリ変数に格納 front = (front + 1) % SIZE; // 次のデータに移動(循環バッファ対応) // バッファのデータを読み込み Serial.print("Received ID: 0x"); // 受信したCAN IDを表示 Serial.println(t_rxId, HEX); // 16進数でIDを出力 // 受信したIDによって処理を分岐(switch文) switch (t_rxId) { case 0x100: { // ID 0x100の場合(intデータ) int receivedInt; // 受信intデータ格納用 memcpy(&receivedInt, t_rxBuf, sizeof(int)); // バイト配列からintへコピー Serial.print("Received int: "); // intデータを表示 Serial.println(receivedInt); break; } case 0x101: { // ID 0x101の場合(floatデータ) float receivedFloat; // 受信floatデータ格納用 memcpy(&receivedFloat, t_rxBuf, sizeof(float)); // バイト配列からfloatへコピー Serial.print("Received float: "); // floatデータを表示 Serial.println(receivedFloat, 2); // 小数点以下2桁表示 break; } case 0x102: { // ID 0x102の場合(charデータ) char receivedChar; // 受信charデータ格納用 memcpy(&receivedChar, t_rxBuf, sizeof(char)); // バイト配列からcharへコピー Serial.print("Received char: "); // charデータを表示 Serial.println(receivedChar); break; } default: // 未知のIDの場合 Serial.println("Unknown CAN ID"); // 未知IDメッセージを表示 break; } } } |
解説
構造体配列RecBufを用意して、割り込み関数handleCANInterruptの中でFIFO方式で都度データを入れる方式とします。
そしてvoid loop()の中でRecBufからデータを取り出し、IDに応じてシリアルモニタで表示する方式とします。
バッファのデータ(rxBuf)は8Byte分確保してしまっているので、memcpyを使用してデータ型に応じて必要な部分だけ取り出します。
備考:バッファ構造体のイメージ図
以上がCANを使ってArduino2台で複数データの送受信を行う方法でした。
誰かに解説してもらえれば簡単に頭に入りますが、巷にはこういった解説が出ていないのが困りどころですよね。みなさんもこの記事を読んで、ぜひCANで複数データの送受信を行ってみましょう!