2026年9月6日日曜日

M5Stack PowerHubでGPS情報をCANロガーに送信

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ロガー、ついに通信成功。