M5Stack PowerHubを購入したので、以下を試してみました。
構成
・目的
・使用ハードウェア
・進捗の記録(エラーだらけの旅路)
・完成コード
・.dbcファイル
・CAN通信デバッグのポイント
・まとめ
1. 目的
M5Stack PowerHub(ESP32-S3)でGPSによる現在位置情報を取得し、OLEDで表示するプログラムがありました。これに CAN通信で車両用のCANロガーに送信する機能 を追加した、記録です。
使用ハードウェア
部品 備考
M5Stack PowerHub ESP32-S3-WROOM-1U-N16R8
M5Unit OLED 128×64 ドットディスプレイ
GPSモジュール Serial2(115200bps)接続
CANトランシーバ SIT1044QTK/3(PowerHub内蔵)
CANロガー 2ch、500kbps、車載OBD対応
CANピンアサイン(PowerHub回路図より)
MCU CAN TXD → GPIO 39
MCU CAN RXD → GPIO 40
2. 旅路(エラーの記録)
2-1. ライブラリが見つからない
#include <ESP32CAN.h>
fatal error: ESP32CAN.h: No such file or directory
原因: 外部ライブラリがインストールされていなかった。
学び: ESP32にはCAN機能が標準搭載(TWIドライバー)されているので、ライブラリに頼らず使う手がある。
2-2. クラス名が合わない
ESP32-TWAI-CAN.hpp をインストールして使ってみたが:
error: 'ESP32CAN' does not name a type
原因: ライブラリによってクラス名が異なる(ESP32CAN / TWAI_CAN など)。
学び: 外部ライブラリは「バージョンによる互換性問題」に頭を痛めさせる。標準ドライバーを使うのが安定。
2-3. TWAIドライバーの地獄(ここからが本番)
#include "driver/twai.h" を使うことにしたが、M5Stack S3のボードマネージャー(v3.x = ESP-IDF v5系) では、古いチュートリアルやネット記事の記述が全部合わない状況に陥った。
エラー 原因 解決
TWAI_GENERAL_CONFIG_DEFAULT requires 3 arguments v5系では引数が必要 (tx, rx, mode) を渡す
no member named 'tx_gpio_num' 変数名が tx_io に変更 型キャスト + 変数名変更
TWAI_TIMING_CONFIG_DEFAULT not declared マクロ名が変更 TWAI_TIMING_CONFIG_500KBITS() を使う
TWAI_FILTER_CONFIG_DEFAULT not declared マクロが削除 構造体を {} で初期化
no member named 'ext' 構造体から削除 extd に変更
学び: ESP32の公式ドライバは「バージョンによって構造体が変わる」。ネットの情報を鵜呑みにすると、必ずどこかで引っかかる。
2-4. 通信エラー(Status: Failed!)
コンパイルは通ったが、送信すると Failed! と表示された。
調べたこと
検証 結果
CAN_H / CAN_L 電圧 各2.5V(差分0V)→ アイドル正常
GNDを共通化 してもダメ
CAN_H/L を入れ替え ダメ
ロガー同士の通信(1ch→2ch) 成功 ← 物理層・ボーレートはOK
M5Stackから送信 Failed ← ACKが返らない
結論
ロガー同士は通信しているのにM5Stackだけが失敗する = M5Stackの送信信号がバスに乗っていない(あるいはACKが返ってこない)。
2-5. 正解にたどり着く
M5Stack公式ドキュメント(PowerHub CANページ)のコードを確認:
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(MCU_CAN_TXD, MCU_CAN_RXD, TWAI_MODE_NORMAL);
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
そして twai_message_t の拡張フラグは:
msg.extd = 0; // ← .ext ではなく .extd
この公式コードをそのまま踏襲したところ、ACKが返って通信成功!
3. 完成コード
#include <M5Unified.h>
#include <M5UnitOLED.h>
#include <TinyGPS++.h>
#include "driver/twai.h"
M5UnitOLED oled;
// --- GPS 設定 ---
const int GPS_RX = 2;
const int GPS_TX = 1;
const long GPS_BAUD = 115200;
// --- CAN (TWAI) 設定 ---
const gpio_num_t MCU_CAN_TXD = GPIO_NUM_39;
const gpio_num_t MCU_CAN_RXD = GPIO_NUM_40;
TinyGPSPlus gps;
bool isSearching = true;
unsigned long lastCANSendTime = 0;
const unsigned long CAN_SEND_INTERVAL = 1000; // 1秒ごと
void setup() {
M5.begin();
oled.begin();
oled.setRotation(1);
oled.setTextColor(WHITE, BLACK);
oled.setTextSize(1);
oled.fillScreen(BLACK);
Serial2.begin(GPS_BAUD, SERIAL_8N1, GPS_RX, GPS_TX);
// CAN初期化(公式ドキュメント準拠)
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(MCU_CAN_TXD, MCU_CAN_RXD, TWAI_MODE_NORMAL);
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
if (twai_driver_install(&g_config, &t_config, &f_config) == ESP_OK &&
twai_start() == ESP_OK) {
Serial.println("CAN ready.");
} else {
Serial.println("CAN init failed.");
while (1) delay(1000);
}
}
void loop() {
M5.update();
// GPS解析
while (Serial2.available() > 0) {
if (gps.encode(Serial2.read())) {
displayGPSInfo();
}
}
// 1秒ごとにCAN送信
if (millis() - lastCANSendTime > CAN_SEND_INTERVAL) {
sendGPSViaCAN();
lastCANSendTime = millis();
}
}
void sendGPSViaCAN() {
if (!gps.location.isValid()) return;
// --- 1. 座標 (ID: 0x123) ---
int32_t lat = (int32_t)(gps.location.lat() * 1000000);
int32_t lon = (int32_t)(gps.location.lng() * 1000000);
twai_message_t msgLoc = {};
msgLoc.extd = 0;
msgLoc.identifier = 0x123;
msgLoc.data_length_code = 8;
// Big Endian で格納
msgLoc.data[0] = (lat >> 24) & 0xFF;
msgLoc.data[1] = (lat >> 16) & 0xFF;
msgLoc.data[2] = (lat >> 8) & 0xFF;
msgLoc.data[3] = lat & 0xFF;
msgLoc.data[4] = (lon >> 24) & 0xFF;
msgLoc.data[5] = (lon >> 16) & 0xFF;
msgLoc.data[6] = (lon >> 8) & 0xFF;
msgLoc.data[7] = lon & 0xFF;
twai_transmit(&msgLoc, pdMS_TO_TICKS(100));
// --- 2. 時刻 (ID: 0x124) ---
int hour = gps.time.hour() + 9; // UTC → JST
int minute = gps.time.minute();
int second = gps.time.second();
int day = gps.date.day();
int month = gps.date.month();
int year = gps.date.year();
if (hour >= 24) { hour -= 24; day++; }
twai_message_t msgTime = {};
msgTime.extd = 0;
msgTime.identifier = 0x124;
msgTime.data_length_code = 7;
msgTime.data[0] = (year >> 8) & 0xFF;
msgTime.data[1] = year & 0xFF;
msgTime.data[2] = month;
msgTime.data[3] = day;
msgTime.data[4] = hour;
msgTime.data[5] = minute;
msgTime.data[6] = second;
twai_transmit(&msgTime, pdMS_TO_TICKS(100));
}
void displayGPSInfo() {
if (gps.location.isValid()) {
if (isSearching) { oled.fillScreen(BLACK); isSearching = false; }
int h = gps.time.hour() + 9;
int m = gps.time.minute();
int s = gps.time.second();
int d = gps.date.day();
int mo = gps.date.month();
int y = gps.date.year();
if (h >= 24) { h -= 24; d++; }
oled.setTextColor(WHITE, BLACK);
oled.setCursor(0, 0); oled.printf("Date: %04d/%02d/%02d", y, mo, d);
oled.setCursor(0, 12); oled.printf("Time: %02d:%02d:%02d", h, m, s);
oled.setCursor(0, 24); oled.printf("Lat: %.6f", gps.location.lat());
oled.setCursor(0, 36); oled.printf("Lon: %.6f", gps.location.lng());
oled.setCursor(0, 48); oled.printf("Sats: %d", gps.satellites.value());
} else {
if (!isSearching) { oled.fillScreen(BLACK); isSearching = true; }
oled.setCursor(0, 24);
oled.setTextColor(WHITE, BLACK);
oled.println("Status: Searching...");
}
}
4. .dbcファイル
VERSION ""
NS_ :
BS_:
BU_: M5Stack_S3
BO_ 291 GPS_Location: 8 M5Stack_S3
SG_ Latitude : 0|32|-1@0 1e-6 (0 180) "deg" M5Stack_S3
SG_ Longitude : 32|32|-1@0 1e-6 (-180 180) "deg" M5Stack_S3
BO_ 292 GPS_Time: 7 M5Stack_S3
SG_ Year : 0|16| 1@0 1 (0 2100) "year" M5Stack_S3
SG_ Month : 16| 8| 1@0 1 (1 12) "month" M5Stack_S3
SG_ Day : 24| 8| 1@0 1 (1 31) "day" M5Stack_S3
SG_ Hour : 32| 8| 1@0 1 (0 23) "hour" M5Stack_S3
SG_ Minute : 40| 8| 1@0 1 (0 59) "min" M5Stack_S3
SG_ Second : 48| 8| 1@0 1 (0 59) "sec" M5Stack_S3
@0 = Little Endian(Intel)/ @1 = Big Endian(Motorola)
今回のコードは Big Endian で送信しているため、ロガー側で Motorola (Big Endian) に設定する。
5. デバッグで学んだこと
CAN通信デバッグのチェックリスト
# チェック項目 確認方法
1 ボーレート一致 送信側・受信側・ロガーで同一
2 GND共通 送信側・受信側のGNDを接続
3 終端抵抗 120Ω 電源OFFで CAN_H–CAN_L 間を測定(約60Ω)
4 CAN_H / CAN_L 電圧 アイドル時 各2.5V / 差分0V
5 配線(H↔H, L↔L) 入れ替えて再確認
6 トランシーバ電源 VCCピンで5V確認
7 STBピン GND接続(動作時)
8 ACK取得 送信側で ESP_OK になるか
ACKとは
CAN通信では、送信ノードがデータを送信した後、バスの上にある他のノードが「ACK(確認応答)」ビットを返す ことで、正常な送信が確定します。
ACKが返らない → 送信失敗(Failed)
原因:相手不在、ボーレート不一致、物理断線、電位差
Big Endian / Little Endian の違い
形式 別名 バイト順 例(0x1234)
Big Endian Motorola MSB → LSB [12, 34]
Little Endian Intel LSB → MSB [34, 12]
今回のコードは Big Endian(Motorola)で送信しているため、ロガー側も Big Endian (Motorola) に設定 すれば正しく数値が復元されます。
6. まとめ
課題 解決策
ライブラリが見つからない ESP32標準の driver/twai.h を使用
構造体のメンバー名が変わる 公式ドキュメントのコードをベースに
送信がFailedになる GND共通化 + ACK取得を確認
数値がずれる ロガー側で Big Endian (Motorola) に設定
8バイト制限 座標と時刻を2つのIDに分割
今回の「教訓」
最新のESP32-S3環境(ボードマネージャーv3.x / ESP-IDF v5系)では、旧バージョン向けのコードがそのまま通用しない。
ネットの情報が「正しい」のか「古い」のかを見極める目と、公式ドキュメントを最終リファレンスにする ことが大切です。
2026年8月、M5Stack PowerHub × CANロガー、ついに通信成功。
