CAN通信の基本的な使用方法を、Arduinoのプログラムを使用して、データ(フレーム)の送信方法や受信データの確認方法、どのようなデータが送受信されているか等をサンプルプログラムを使用して実際に動作確認しながら詳しく紹介します。
データの送受信には専用の「CANユニット」または「汎用のトランシーバ基板(MCP2551)」を使用した方法で紹介しています。
「AtomS3」の使い方や初期設定、開発環境「ArduinoIDE」の使い方については、以下のリンクで詳しく紹介しています。


1.CAN通信とは
・主な特徴
・通信の基本
・データフレームの構造詳細(標準と拡張の違い)
2.使用するCAN通信ユニット、トランシーバーについて
3.動作紹介
4.配線図
5.開発環境、ライブラリについて
・開発環境 ArduinoIDE
・ライブラリ ESP32-TWAI-CAN
6.サンプルプログラム
7.サンプルプログラムの詳細
・CANデータフレームの送信方法
・CANデータフレームの受信方法
8.まとめ
1.CAN通信とは
「CAN(Controller Area Network)通信」とは、主に自動車内の電子制御ユニット(ECU:Electronic Control Unit)同士を接続するために開発された、信頼性の高いシリアル通信プロトコルです。
下画像のように、複数のノード(ECU)がバス(信号線:2本+GND[0V])を共有しながら、「メッセージID」によって優先度を管理(IDの数値が小さい程優先度が高い)されながら通信が行われるため、効率的で信頼性の高いデータ通信が可能です。

各ノード(ECU)が、それぞれの役割に応じて情報をやり取りすることで、システム全体の連携動作を実現しています。
・主な特徴
主な特徴は以下のようになります。
- マルチマスター方式 : ネットワーク上の、どのノード(ECU:Electronic Control Unit)からでも、バスが空いていればデータの送信を開始できます。
- メッセージベース : 各ノードはID(メッセージ識別子)を付加したメッセージを送信し、受信ノードは必要なIDのメッセージのみを受信します。
- 優先度制御 : メッセージIDによって優先度が決定(IDの数値が小さい程優先度が高い)され、複数のノードが同時に送信を開始した場合でも、優先度の高いメッセージが先に送信されます (CSMA/CR方式)。
これにより、複数のメッセージが送信待ちの状態でも、エンジンやブレーキ等の重要なメッセージが最優先に送信されて処理することができます。 - 高速データ通信:通信速度は一般的に1Mbps(約40m)と高速で、他にも500kbps(約100m)、250kbps(約250m)、125kbps(約500m)があり、通信距離に応じて使い分けます。
さらに通信速度の早い「CAN FD」の場合最大8Mbps(数m:システム設計に依存)での通信も可能です。 - 耐ノイズ性 : 2本の信号線 (CAN High, CAN Low) の電圧差を利用して通信を行うため、外部ノイズの影響を受けにくい。
- エラー検出機能 : CRC (巡回冗長検査) など複数のエラー検出機構を備えており、高い信頼性を確保しています。
エラーを検出したノードは、他のすべてのノードにエラーフレームを送信して通知します。 - 柔軟なシステム構築 : ノードの追加や削除が比較的容易です。
- 低コスト: シンプルなハードウェア構成で実装できます。
・通信の基本
CANバス(信号線:2本+GND[0V])上で、信号線「CAN H」と「CAN L」から送信されるデータの状態(電位差)は、下画像のようにドミナント (優勢:電位差あり 2V、論理0) と レセシブ (劣勢:電位差なし 0V、論理1) の2つの状態によって表されます。

優れたノイズ耐性
2本の信号線の電位差によってデータを送信するため、非常に優れたノイズ耐性を持ちます。

上画像のように車載用途等では配線経路に電子機器やインバーター、各種モーターがあり、厳しいノイズ環境にさらされています。

2本の信号線(ツイストペア)の電位差を利用することで、上画像のように信号線に同じノイズによるサージ電圧が印加されても、2本線の電位差に変化はなく、レセシブ/ドミナントの状態を確実に送信することができます。
データフレームの構造詳細(標準と拡張の違い)
実際に送信するデータを含む「データフレーム」の構造は下図のようになります。

CAN通信のフレームにはいくつか種類がありますので、以下から詳しく紹介します。
主なフレームの種類
主なフレームの種類は以下の通りです。
- データフレーム : 実際に送信するデータを含むフレーム。メッセージIDが11bitの「標準データフレーム」と、29bitでより多くの種類のメッセージが扱える「拡張データフレーム」があります。
- リモートフレーム : 特定のデータの送信を要求するフレームでデータは含まれません。(データフレームの「DLC」と「DATA」部を除いたフレーム構造)
近年ではバスのデータ占有率を下げるために、各ノードへ定期的にデータフレームを送信する方式が一般的で、リモートフレームは使用されなくなってきています。
拡張フレームではID領域が29bitと長く、バスの競合が増える可能性が高くなるため、リモートフレーム自体が存在しません。 - エラーフレーム : 通信エラーが発生した際に送信されるフレーム。
- オーバーロードフレーム : ノードが次のメッセージを受信する準備ができていない(処理能力が低い、受信バッファに空きがない等)ことを示すために送信される特別なフレームです。
近年ではコントローラの高性能化で、実際に送信される頻度は非常に少なくなっていますが、古いコントローラや非常に負荷の高いネットワークでは使用される可能性がないわけではありません。
・データフレームの構造詳細(標準と拡張の違い)
実際に送信するデータが含まれる「データフレーム」には、標準データフレーム(識別ID 11bit)と拡張データフレーム(識別ID 29bit)があり、拡張フレームでは識別IDの桁数が増えたことで、より多くの種類のメッセージを扱うことができます。
以下から「標準フレーム」と「拡張フレーム」のデータ構造について詳しく紹介します。
標準データフレームの構造
「IDEフィールド」が「ドミナント 0」の場合「標準データフレーム」となります。

拡張データフレームの構造
標準データフレームの「RTR」が「SRR」に置き換わり、「IDE」が「レセシブ 1」の場合「拡張フレーム」となり、標準ID 11bitと組み合わせて使用する拡張ID 18bitが送信されます(11+18=29bit)。

各データ領域(フィールド)の詳細は下表のようになります。
領域(フィールド)名 | 用途 | bit数 |
---|---|---|
SOF(Start Of Frame) | ドミナント 0で、フレームの開始を示す | 1 |
ID(Identifier) | メッセージの識別や優先順位決定のためのIDを指定 ・標準フレーム:11bit ・拡張フレーム:異なるフィールドでベースID 11bitと、拡張ID 18bitを指定し、11bit + 18bitの合計29bitで指定可能 | 標準:11 拡張:29(11+18) |
RTR(Remote Transmission Request)標準フレームのみ | データフレームかリモートフレームかの識別用 ・データフレーム:ドミナント 0 ・リモートフレーム:レセシブ 1 | 1 |
SRR(Substitute Remote Request Bit)拡張フレームのみ | 拡張フォーマットで使用され、常にレセシブ 1 | 1 |
IDE(Identifier Extension) | 標準フォーマットか拡張フォーマットの識別用 ・標準フォーマット:ドミナント 0 ・拡張フォーマット:レセシブ 1 | 1 |
r0/r1(reserved bit) | 将来的な拡張機能のための予約ビットで現状特に意味を持ちませんが、通常はドミナント 0 として送信することになっています | 1 |
DLC(Data Length Code) | 送信するデータの長さをbyte単位で設定 | 4 |
DATA | 0~64bit(0~8byte)の可変長で、実際に送信するデータを設定 DLCフィールドで指定されたbyte数によってbit数が決まる | 0~64 (8byte) |
CRC(Cyclic Redundancy Check) | 15ビットの巡回冗長検査符号で、伝送エラーの検出に使われる15bitと、CRCシーケンスの終わりを示す(レセシブ 1)デリミタ1bitの合計16bitで構成 | 15 + 1(デリミタ) |
ACK(Acknowledge) | 受信ノードが正常にデータを受信したことを送信ノードに通知(正常受信でドミナント 0)するために使用される1bitと、ACKフィールドの終わりを示す(レセシブ 1)デリミタ1bitの合計2bitで構成 | 1 + 1(デリミタ) |
EOF(End Of Frame) | 7bitのレセシブビット(全て1)で、データフレームの終わりを示す | 7 |
ITM(Intermission) | 最小3bitのレセシブビット(全て1)で、連続するフレームを分離するために使用 | 3 |
2.使用するCAN通信ユニット、トランシーバーについて
今回動作確認に使用した「CAN通信ユニット」と「トランシーバー基板」について紹介します。
M5Stack社製のCANユニット
下画像は「M5Stack」社製のCANユニット「ATOMIC CANBus Base 」でCANコントローラとしては「CA-IS3050G」が搭載されています。


「AtomS3」や「AtomS3 Lite」「Atom Lite」を組み込んで使用することができ、電源供給(USB Type-C)と信号線(CAN H/CAN L/GND)の接続だけでCAN通信を行うことができます。
「AtomS3」の使い方や初期設定については、以下のリンクで詳しく紹介しています。

トランシーバー基板(MCP2551)
下画像はCANコントローラICの「MCP2551」を搭載した、安価なCAN通信トランシーバー基板です。


電源電圧が5Vのため、「AtomS3」のような制御電圧が3.3Vのコントローラと通信するには通信線の電圧レベルを3.3Vから5Vに変換する「双方向ロジックレベル変換基板」を使用する必要があります。
CANコントローラの駆動電源5Vと制御電源3.3Vを別々に供給できる、CANコントローラIC「TJA1051T/3」を搭載した以下のようなCANユニットもあります。
3.動作紹介
この下で公開している「サンプルプログラム」を使用した動作について紹介します。
「AtomS3」本体のボタンを押した回数をCAN通信で送信し、もう一方の「AtomS3」の液晶画面上で確認することができます。
受信したデータはシリアルモニタでも確認できるため、連続したデータを記録したい場合はシリルモニタを使用してください。
CANユニットでの動作確認

本体ボタンを押すと画面が青色になり、ボタンを押した回数がカウントされます。
カウント値はデータフィールドの1byteで送信され、送信先の液晶画面でも確認できます。
残り7byteのデータは「FF」としています。

もう一方の本体ボタンを押すと、同様にボタンのON回数が送信されてもう一方の液晶画面で確認できます。
CANトランシーバー基板での動作確認
動作は「CANユニット」同士の場合と同じです。

本体ボタンを押すと画面が青色になり、ボタンを押した回数がカウントされ送信されることで、もう一方の液晶画面でも同じデータが確認できます。

もう一方の本体ボタンを押した場合も同じ動作です。
4.配線図
配線図については今回使用した「CANユニット」同士の場合と「トランシーバ基板」同士の場合、「CANユニット」と「トランシーバ基板」を接続した場合とで紹介します。
CANユニット同士
「ATOMIC CANBus Base 」同士の接続は「AtomS3」に電源を供給して、「H/L/G」端子をそれぞれ接続するシンプルなものです。(付属の120Ωの抵抗器も必ず図の位置に取り付けてください。)

実際に配線した画像は以下のようになります。


「AtomS3」の使い方や初期設定については、以下のリンクで詳しく紹介しています。

トランシーバ基板同士
CANトランシーバー基板は5Vで動作しますが「AtomS3」の信号レベルは3.3Vのため、信号レベルを5Vに変換する「双方向ロジックレベル変換基板」が必要です。
配線は少々複雑ですが、UART通信の信号線(TX/RX)の間に「変換基板」を割り込ませているだけです。

実際に配線した画像は以下のようになります。


CANユニット + トランシーバ基板
下画像では、上記で使用した「CANユニット」と「トランシーバ基板」の「CAN HとH/CAN LとL/GNDとG」を接続しています。

実際に配線した画像は以下のようになります。


5.開発環境、ライブラリについて
今回使用した開発環境「ArduinoIDE」と「ライブラリ」について紹介します。
・開発環境 ArduinoIDE
開発環境「ArduinoIDE」のインストールやライブラリの準備方法は以下のリンクで詳しく紹介しています。

・ライブラリ ESP32-TWAI-CAN
使用するライブラリは以下表のようになります。
「ArduinoIDE」で事前にインストールしておいてください。
ライブラリ名 | 用途 | バージョン | 検索名 |
---|---|---|---|
M5AtomS3 | AtomS3制御用 | 1.0.2 | AtomS3 |
M5Unified | 〃 | 0.2.5 | M5Unif |
FastLED | 〃 | 3.9.16 | FastL |
M5GFX | 液晶画面制御用 | 0.2.6 | M5G |
ESP32-TWAI-CAN | CAN通信制御用 | 1.0.1 | ESP32-TWAI |
6.サンプルプログラム
サンプルプログラムは以下になります。
コピペで貼り付けて書き込んでください。コピーは下の黒塗り部右上のアイコンクリックでもできます。
#include <M5AtomS3.h>
#include <ESP32-TWAI-CAN.hpp>
#define CAN_TX 5 // 受信端子(トランシーバー側TXへ接続)
#define CAN_RX 6 // 送信端子(トランシーバー側RXへ接続)
uint8_t count = 0; // ボタンON回数カウント用
CanFrame txFrame; // 送信用データフレームを準備
CanFrame rxFrame; // 受信用データフレームを準備
/*********************** 受信データ表示処理 ***********************/
void dataDisp(CanFrame dataFrame) {
M5.Lcd.setCursor(0, 5); // 表示開始位置左上角(X,Y)
M5.Lcd.setTextColor(0xFFFF); // 文字色指定
// ID、データサイズ表示
M5.Lcd.printf("ID = 0x%03X DLC = %d\n", dataFrame.identifier, dataFrame.data_length_code);
Serial.printf("ID=0x%03X DLC=%d Data=", dataFrame.identifier, dataFrame.data_length_code);
// 受信データ表示
M5.Lcd.setTextColor(M5.Lcd.color565(0, 255, 255)); // 文字色指定
for (int i = 0; i < dataFrame.data_length_code; i++) { // 受信データ 液晶表示
M5.Lcd.printf("%02X ", dataFrame.data[i]);
if((i+1)%3 == 0) { M5.Lcd.print("\n"); } // 受信データ3要素ごとに改行
}
for (int i = 0; i < dataFrame.data_length_code; i++) { // 受信データ USBシリアル表示
Serial.printf("%02X ", dataFrame.data[i]);
}
Serial.println();
}
/*********************** データ送信処理 ***********************/
void sendDataFrame(uint32_t id) {
txFrame.identifier = id; // 送信先ID指定:値が少ない程優先度が高い(標準フォーマット11ビット、拡張29ビット)
txFrame.extd = 0; // 0:標準フォーマット 1:拡張フォーマット
txFrame.data_length_code = 8; // 送信データフィールドの長さを設定(標準:最大8バイト、拡張:最大64バイト)
txFrame.data[0] = count; // 送信データをセット
txFrame.data[1] = 0xFF;
txFrame.data[2] = 0xFF;
txFrame.data[3] = 0xFF;
txFrame.data[4] = 0xFF;
txFrame.data[5] = 0xFF;
txFrame.data[6] = 0xFF;
txFrame.data[7] = 0xFF;
ESP32Can.writeFrame(txFrame); // データ送信実行
}
// 初期設定 **************************************************
void setup() {
auto cfg = M5.config(); // 本体初期化
AtomS3.begin(cfg);
Serial.begin(115200); // USBシリアル通信初期化
Serial.println("AtomS3 CAN test start!"); // USBシリアル出力
// 液晶表示(起動画面)
M5.Lcd.setCursor(0, 25); // 表示開始位置左上角(X,Y)
M5.Lcd.fillScreen(0); // 表示クリア
M5.Lcd.setRotation(0); // 画面向き設定(USB位置基準 0:上/ 1:左/ 2:下/ 3:右)
M5.Lcd.setTextFont(4); // フォント
M5.Lcd.setTextColor(0xFFFF); // 文字色指定
M5.Lcd.println(" AtomS3\n CAN\n MONITOR"); // 液晶初期表示
// CAN通信初期化
ESP32Can.setPins(CAN_TX, CAN_RX); // 通信端子設定
ESP32Can.setRxQueueSize(5); // キュー(データ保持バッファ)のサイズ設定も可(5:デフォルト)
ESP32Can.setTxQueueSize(5);
ESP32Can.setSpeed(ESP32Can.convertSpeed(500)); // 通信速度設定(kbps) ※100,125,250,500,800,1000
if(ESP32Can.begin()) { // CAN通信の開始
Serial.println("CAN bus started!"); // 成功の場合
} else {
Serial.println("CAN bus failed!"); // 失敗の場合
}
}
// メイン ****************************************************
void loop() {
M5.update(); // ボタン状態更新
// 本体スイッチ処理
if (M5.BtnA.wasPressed()) { // ボタンが押されていれば
count++;
M5.Lcd.fillScreen(BLUE); // データ送信時は背景「青」
Serial.print("送信:");
sendDataFrame(0x7DF); // 送信先IDを指定して、データ送信処理
dataDisp(txFrame); // 送信データ表示処理
}
// 全ての受信データを処理
if (ESP32Can.readFrame(rxFrame, 10)) { // データを受信したら(タイムアウト10ms)
M5.Lcd.fillScreen(0); // データ受信時は背景「黒」
Serial.print("受信:");
dataDisp(rxFrame); // 受信データ表示処理
}
// 特定IDのデータのみ処理
// if (rxFrame.identifier == 0x7DF) { 特定IDのデータ処理内容をここに書く }
}
7.サンプルプログラムの詳細
サンプルプログラムからCANデータフレームの送受信部を抜粋して紹介します。
・CANデータフレームの送信方法
CANデータフレームの送信はサンプルプログラムの「33-47行目」で以下のように「ID」を指定して呼び出す関数としてまとめています。
/*********************** データ送信処理 ***********************/
void sendDataFrame(uint32_t id) {
txFrame.identifier = id; // 送信先ID指定:値が少ない程優先度が高い(標準フォーマット11ビット、拡張29ビット)
txFrame.extd = 0; // 0:標準フォーマット 1:拡張フォーマット
txFrame.data_length_code = 8; // 送信データフィールドの長さを設定(標準:最大8バイト、拡張:最大64バイト)
txFrame.data[0] = count; // 送信データをセット
txFrame.data[1] = 0xFF;
txFrame.data[2] = 0xFF;
txFrame.data[3] = 0xFF;
txFrame.data[4] = 0xFF;
txFrame.data[5] = 0xFF;
txFrame.data[6] = 0xFF;
txFrame.data[7] = 0xFF;
ESP32Can.writeFrame(txFrame); // データ送信実行
}
送信するデータフレームの「ID」「標準/拡張フォーマットの選択」「送信データ長(byte単位)」をして、配列で「送信したいデータ」を指定します。
データフレームが準備できたら「ESP32Can.writeFrame(txFrame)」を実行することで、送信することができます。
・CANデータフレームの受信方法
CANデータフレームの受信はサンプルプログラムの「89-96行目」で以下のように行っています。
// 全ての受信データを処理
if (ESP32Can.readFrame(rxFrame, 10)) { // データを受信したら(タイムアウト10ms)
M5.Lcd.fillScreen(0); // データ受信時は背景「黒」
Serial.print("受信:");
dataDisp(rxFrame); // 受信データ表示処理
}
// 特定IDのデータのみ処理
// if (rxFrame.identifier == 0x7DF) { 特定IDのデータ処理内容をここに書く }
全てのデータを受信する場合は受信データフレームの「rxFrame」と「タイムアウト時間(ここでは10ms)」を指定して「ESP32Can.readFrame」を実行することで「rxFrame」にデータが受信されます。
特定IDのデータのみ処理したい場合は「rxFrame.identifier」の値を確認して、一致した場合のみ処理を実行するようにします。(コメントアウトしているため必要に応じて使用してください)
8.まとめ
CAN通信の基本的な使用方法を詳しく紹介しました。CAN通信は、主に自動車の電子制御ユニット同士を接続するために開発された、信頼性の高いシリアル通信プロトコルです。
ネットワーク上の、どのノード(ECU:Electronic Control Unit)からでも、バスが空いていればデータ送信を開始でき、メッセージIDによって優先度の高いメッセージが先に送信されます。
これにより、エンジンやブレーキ等の重要なメッセージを最優先に処理することができます。
ツイストペアの信号線 (CAN High, CAN Low) の電圧差を利用して通信を行うことで、外部ノイズの影響を受けにくく、通信速度も1Mbps(CAN FDでは最大8Mbps)と高速で信頼性の高い通信が実現できるため、自動車に限らず産業オートメーション、医療機器、航空宇宙などの、幅広い分野で使用されています。
CAN通信のデータのやり取りはCANコントローラを使用した「データフレーム」の送受信が基本です。
CANコントローラは「マイコンボード」と「トランシーバー基板」の組み合わせで簡単に実現でき、「AtomS3」のような液晶表示付きのボードならデータの確認も容易に行うことができます。
シリアルモニタを使用すれば、受信データの連続記録も可能です。
今回は「C言語」による「Arduino」のライブラリを使用した方法を紹介しましたが、「Raspberry Pi Pico」を使用した「Python」での使い方も、また機会があれば紹介したいと思います。

コメント