当初は、ELM327アダプタを使って、クルマから情報を取得する予定でしたが、クルマとの相性が悪く 購入したELM327アダプタではECUと接続できませんでした。
そこで、CAN通信で実現する方式に変更します。
1. CAN通信にてクルマから取得できる情報
CAN通信(OBD-II規格)から取得できる主な情報は次のとおり。
詳しくはこちらのWikiを参照。
(メーカーや車種によって、取得できない情報も含みます)
-
取得できる主な情報
$\small\textsf{ PID }$ 取得情報 意味・概要 0x0C RPM エンジン回転数 0x0D Speed 自動車の速度 0x05 WaterTemp エンジン冷却水温度 0x0F IA-Temp 吸入空気温度 0x11 Throttle スロットル開度 0x04 Load エンジン負荷率 0x2F Fuel 燃料残量レベル 0x0B MAP インテークマニホールド絶対圧 0x33 Baro 大気圧
-
どのPIDに対応しているか調べる
車種がどのPIDに対応しているか調べることができます。対象とするクルマに対し、これをリクエストすれば悩まずに済みます。$\small\textsf{ PID }$ 取得情報 返却バイト数 単位 0x00 PID 0x01 〜 0x20 の中でどれに対応しているか 4 A0〜D7 (32ビット)
各ビットが 1 なら「対応」、0 なら「非対応」を意味する0x20 PID 0x21 〜 0x40 の中でどれに対応しているか 4 同上 0x40 PID 0x41 〜 0x60 の中でどれに対応しているか 4 同上 0x60 PID 0x61 〜 0x80 の中でどれに対応しているか 4 同上
まずは、クルマがどのPIDをサポートしているのかを調べることを目的に、CAN通信手法を確立したいと思います。
2. 使用したハードウェア
2.1. ESP32-C3
以前にまとめ買いしていた小型のものを使用しました。
ESP32シリーズの多くは、CANコントローラーの機能が最初から内蔵されています。
2.2. CANトランシーバ
本物のSN65HVD230を搭載した CANトランシーバボード
クルマと接続する場合、(2Pヘッダーと並行に配置されている)120Ωの終端抵抗を外します。また、スペースの関係で ピンヘッダーとターミナルブロックも取り外しました。
2.3. DCDCコンバーター
ESP32-C3 と CANトランシーバ の電源を 作るため、DCDCコンバーターを使用します。小型の出力固定タイプを選びました。裏面のハンダジャンパーで3.3V固定化(可変タイプはポテンショメータが振動で動いて電圧が変化する恐れがある)。
当初、AM1117-3.3VのようなLDOを考えましたが、相当な発熱が想定されるため止めました。そもそもLDOは電圧差を熱に変えることで電圧を一定に保つ仕組みになっているので、5V→3.3V変換程度が妥当な用途です。
消費電流は、ESP32-C3でWifi使用時のピーク約335mA、SN65HVD230のCAN送信時最大約70mA、合計約400mAに達します。
・ 電圧差: 12V - 3.3V = 8.7V
・ 消費電流: 400mA
・ 発熱量: $\small 8.7\text{V}\times 400\text{mA}=\mathbf{3.48}\text{W}$
AM1117(一般的なSOT-223パッケージ)が、ヒートシンクなしの標準的な基板で安全に処理できる熱量は約0.5W程度、最大でも1W未満。3.48Wという熱量は許容量をはるかに超えており、耐えられません。
2.4. OBD2コネクタ
半透明のハウジング内に 上記のパーツを収納します。
このタイプも使いやすいかも
2.5. 配線図
各パーツをワイヤーでつなぐ
| ODB2コネクタ | DCDCコンバータ | CANトランシーバ | ESP32-C3 |
|---|---|---|---|
| 16(12V) | IN+ | ||
| 4(GND) | GND | GND | GND |
| VO+ | 3.3V | 3.3V | |
| TX | IO2 | ||
| RX | IO3 | ||
| 14(CANL) | CANL | ||
| 6(CANH) | CANH |
3. ハウジング内の配置
ユニバーサル基板を切り出して、そこにパーツを並べ裏面で配線しました。
まずは、ESP32-C3 SupreMini の USBからSerial出力でモニターします。この場合は、USBから電源を得られるため、DCDCコンバンターは不要ですが、いずれ無線化するため、あらかじめDCDCコンバンターを仕込んでおきました。
ユニバーサル基板を切ってハンダ付け
4. ソフトウエア
Arduino-IDE でプログラム(スケッチ)を作成しました。
特別なライブラリは不要で、CAN通信できます。
ESP32-C3 のUSB-UARTを使用するため、CDC on BootをEnableに設定してアップロードする必要があります。
// for ESP32-C3 and SN65HVD230 CAN receiver
#include "driver/twai.h"
#include <stdio.h>
// ESP32-C3のGPIOピン設定
#define TX_PIN GPIO_NUM_2
#define RX_PIN GPIO_NUM_3
// OBD2の標準的なCAN ID
#define OBD_REQUEST_ID 0x7DF
#define OBD_RESPONSE_ID 0x7E8
// 一括出力用のグローバル固定長バッファ
char outputBuffer[256];
// 各PIDのデータを格納する配列の構造体定義
struct ObdParameter {
uint8_t pid; // OBD2のPIDコード
const char* name; // 表示名
// 各パース関数:バッファ、書き込み可能残サイズ、最終フラグを受け取る仕様に変更
void (*parser)(uint8_t* data, const char* name, char* buf, size_t max_len, bool isLast);
};
// すべてのシリアル出力にタイムスタンプを自動付加する
void logWithTimestamp(const char* message, bool addNewLine = true) {
unsigned long currentMillis = millis();
unsigned long total_seconds = currentMillis / 1000;
unsigned long hours = total_seconds / 3600;
unsigned long minutes = (total_seconds % 3600) / 60;
unsigned long seconds = total_seconds % 60;
unsigned long ms = currentMillis % 1000;
char timestamp[32];
snprintf(timestamp, sizeof(timestamp), "[%lu:%02lu:%02lu.%03lu] ", hours, minutes, seconds, ms);
Serial.print(timestamp);
if (addNewLine) {
Serial.println(message);
} else {
Serial.print(message);
}
}
// 各パラメーターの解析関数
void parseRPM(uint8_t* data, const char* name, char* buf, size_t max_len, bool isLast) {
uint16_t rpm = ((data[3] << 8) | data[4]) / 4;
size_t current_len = strlen(buf);
snprintf(buf + current_len, max_len - current_len, "%s: %u rpm%s", name, rpm, isLast ? "" : " | ");
}
void parseSpeed(uint8_t* data, const char* name, char* buf, size_t max_len, bool isLast) {
uint8_t speed = data[3];
size_t current_len = strlen(buf);
snprintf(buf + current_len, max_len - current_len, "%s: %u km/h%s", name, speed, isLast ? "" : " | ");
}
void parseSupportedPids(uint8_t* data, const char* name, char* buf, size_t max_len, bool isLast) {
size_t current_len = strlen(buf);
snprintf(buf + current_len, max_len - current_len, "%s: %02X%02X%02X%02X%s",
name, data[3], data[4], data[5], data[6], isLast ? "" : " | ");
}
// 取得情報の配列
ObdParameter obd_pids[] = {
{ 0x0C, "RPM", parseRPM }, // エンジン回転数 (rpm)
{ 0x0D, "Speed", parseSpeed }, // 車速 (km/h)
{ 0x00, "PID00", parseSupportedPids }, // PIDs 01-20 のサポート状況
{ 0x20, "PID20", parseSupportedPids }, // PIDs 21-40 のサポート状況
{ 0x40, "PID40", parseSupportedPids }, // PIDs 41-60 のサポート状況
{ 0x60, "PID60", parseSupportedPids }, // PIDs 61-80 のサポート状況
{ 0x80, "PID80", parseSupportedPids }, // PIDs 81-A0 のサポート状況
};
const uint8_t total_pids = sizeof(obd_pids) / sizeof(obd_pids[0]);
unsigned long lastLoopTime = 0;
void setup() {
Serial.begin(115200);
while(!Serial) delay(10);
logWithTimestamp("OBD2 CAN Initializing...");
// 指定の GPIO2/GPIO3 に合わせて設定
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TX_PIN, RX_PIN, TWAI_MODE_NORMAL);
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();
// 0x7E8 のみを通すフィルターマスクの設定
twai_filter_config_t f_config;
f_config.acceptance_code = (OBD_RESPONSE_ID << 21);
f_config.acceptance_mask = 0x001FFFFF;
f_config.single_filter = true;
if (twai_driver_install(&g_config, &t_config, &f_config) == ESP_OK && twai_start() == ESP_OK) {
logWithTimestamp("TWAI driver started.");
} else {
logWithTimestamp("Failed to start TWAI driver.");
return;
}
}
void loop() {
unsigned long currentMillis = millis();
// 1秒に1回、まとめて情報を取りにいく
if (currentMillis - lastLoopTime >= 1000) {
lastLoopTime = currentMillis;
// char配列バッファを空文字列で初期化
outputBuffer[0] = '\0';
// 配列に入っているすべてのPIDを順番に処理
for (uint8_t i = 0; i < total_pids; i++) {
bool isLastItem = (i == total_pids - 1);
twai_message_t rx_msg;
// 送信前に受信バッファに残っている古いデータをすべて読み飛ばす(フラッシュ)
while (twai_receive(&rx_msg, 0) == ESP_OK) {
// 何もしない(バッファを空にするため)
}
// リクエストを送信
sendObdRequest(obd_pids[i].pid);
// 返信を待つ
unsigned long startTime = millis();
bool received = false;
while (millis() - startTime < 30) {
if (twai_receive(&rx_msg, pdMS_TO_TICKS(5)) == ESP_OK) {
if (rx_msg.identifier == OBD_RESPONSE_ID && rx_msg.data_length_code == 8) {
if (rx_msg.data[1] == 0x41 && rx_msg.data[2] == obd_pids[i].pid) {
// 登録された関数で出力バッファに書き込み
obd_pids[i].parser(rx_msg.data, obd_pids[i].name, outputBuffer, sizeof(outputBuffer), isLastItem);
received = true;
break;
}
}
}
}
if (!received) {
// タイムアウトした場合は N/A を出力
size_t current_len = strlen(outputBuffer);
snprintf(outputBuffer + current_len, sizeof(outputBuffer) - current_len, "%s: N/A%s", obd_pids[i].name, isLastItem ? "" : " | ");
}
// 車両ECUの負荷軽減のためのウェイト
delay(20);
}
// Serial出力
logWithTimestamp(outputBuffer);
}
}
// OBD2リクエスト送信関数
void sendObdRequest(uint8_t pid) {
twai_message_t tx_msg = {}; // all 0
tx_msg.identifier = OBD_REQUEST_ID;
tx_msg.data_length_code = 8;
tx_msg.data[0] = 2; // 追加データのバイト数
tx_msg.data[1] = 1; // モード01
tx_msg.data[2] = pid; // 対象のPID
twai_transmit(&tx_msg, pdMS_TO_TICKS(10));
}
- 事前テスト
実車でテストする前に、OBD2コネクタに12Vを給電して動作チェックエラーにはなっていないようです。[0:00:01.265] OBD2 CAN Initializing... [0:00:01.641] TWAI driver started. [0:00:02.646] RPM: N/A | Speed: N/A | PID00: N/A | PID20: N/A | PID40: N/A | PID60: N/A | PID80: N/A [0:00:03.646] RPM: N/A | Speed: N/A | PID00: N/A | PID20: N/A | PID40: N/A | PID60: N/A | PID80: N/A [0:00:04.646] RPM: N/A | Speed: N/A | PID00: N/A | PID20: N/A | PID40: N/A | PID60: N/A | PID80: N/A
5. 実車テスト
ACC電源オン状態では何も情報は得られませんが、エンジンをかけたところ 無事に取得できました。
[0:00:26.360] RPM: N/A | Speed: N/A | PID00: N/A | PID20: N/A | PID40: N/A | PID60: N/A | PID80: N/A
[0:00:27.360] RPM: N/A | Speed: N/A | PID00: N/A | PID20: N/A | PID40: N/A | PID60: N/A | PID80: N/A
[0:00:28.360] RPM: N/A | Speed: N/A | PID00: N/A | PID20: N/A | PID40: N/A | PID60: N/A | PID80: N/A
# エンジン始動
[0:01:03.270] RPM: 164 rpm | Speed: 0 km/h | PID00: 981B8013 | PID20: 8001A001 | PID40: CCC00001 | PID60: 00890141 | PID80: 21300000
[0:01:04.270] RPM: 804 rpm | Speed: 0 km/h | PID00: 981B8013 | PID20: 8001A001 | PID40: CCC00001 | PID60: 00890141 | PID80: 21300000
# 実走行
[0:01:37.272] RPM: 785 rpm | Speed: 2 km/h | PID00: 981B8013 | PID20: 8001A001 | PID40: CCC00001 | PID60: 00890141 | PID80: 21300000
[0:01:38.272] RPM: 826 rpm | Speed: 4 km/h | PID00: 981B8013 | PID20: 8001A001 | PID40: CCC00001 | PID60: 00890141 | PID80: 21300000
[0:01:40.271] RPM: 979 rpm | Speed: 8 km/h | PID00: 981B8013 | PID20: 8001A001 | PID40: CCC00001 | PID60: 00890141 | PID80: 21300000
このクルマの場合は、
PID00: 981B8013PID20: 8001A001PID40: CCC00001PID60: 00890141PID80: 21300000
でした。
6. サポートPIDを調査
6.1. Pythonで分解
PIDS = {0x00: 0x981B8013,
0x20: 0x8001A001,
0x40: 0xCCC00001,
0x60: 0x00890141,
0x80: 0x21300000}
for id in PIDS:
bits = PIDS[id]
print(f"0x{id:02x}:", end="")
for b in range(32):
if (bits << b) & 0x80000000:
print(f" 0x{id + b + 1:02X}", end="")
print()
0x00: 0x01 0x04 0x05 0x0C 0x0D 0x0F 0x10 0x11 0x1C 0x1F 0x20
0x20: 0x21 0x30 0x31 0x33 0x40
0x40: 0x41 0x42 0x45 0x46 0x49 0x4A 0x60
0x60: 0x69 0x6D 0x70 0x78 0x7A 0x80
0x80: 0x83 0x88 0x8B 0x8C
実に、29個(33個)の情報が取得できそうだと分かりました。以下にPIDの詳細を示します。
6.2. PID一覧・デコード仕様
ヘッダーを除いた、純粋なデータ本体の先頭から順に A, B, C, D... バイトと定義します。
(1) PID 0x01 〜 0x20(基本・動的パラメータ群)
| $\small\textsf{ PID }$ | 返却 バイト数 |
項目 | デコード計算式・物理値変換 |
|---|---|---|---|
| 0x01 | 4 | DTCクリア後のモニターステータス | ビットフラグ(MIL警告灯状態、各触媒診断状態) |
| 0x04 | 1 | 計算されたエンジン負荷率 | $A \times \frac{100}{255}$ [%] |
| 0x05 | 1 | エンジン冷却水温 | $A - 40$ [°C] |
| 0x0C | 2 | エンジン回転数(RPM) | $\frac{(A \times 256) + B}{4}$ [rpm] |
| 0x0D | 1 | 車速 | $A$ [km/h] |
| 0x0F | 1 | 吸入空気温度 | $A - 40$ [°C] |
| 0x10 | 2 | MAFエアフローセンサー流量 | $\frac{(A \times 256) + B}{100}$ [g/s] |
| 0x11 | 1 | 指令スロットル開度 | $A \times \frac{100}{255}$ [%] |
| 0x1C | 1 | 適合OBD規格 | 規格コード(例:0x06 = EOBDなど) |
| 0x1F | 2 | エンジン始動後の作動時間 | $(A \times 256) + B$ [秒] |
(2) PID 0x21 〜 0x40(環境・燃料制御系パラメータ群)
| $\small\textsf{ PID }$ | 返却 バイト数 |
項目 | デコード計算式・物理値変換 |
|---|---|---|---|
| 0x21 | 2 | MIL(警告灯)点灯後の走行距離 | $(A \times 256) + B$ [km] |
| 0x30 | 1 | DTCクリア後のウォームアップ回数 | $A$ [回] |
| 0x31 | 2 | DTCクリア後の走行距離 | $(A \times 256) + B$ [km] |
| 0x33 | 1 | 絶対大気圧 | $A$ [kPa] or $A \times 10$ [hPa] |
(3) PID 0x41 〜 0x60(電源・制御系パラメータ群)
| $\small\textsf{ PID }$ | 返却 バイト数 |
項目 | デコード計算式・物理値変換 |
|---|---|---|---|
| 0x41 | 4 | 今セッションの駆動系ステータス | ビットフラグ(今動かしてからの診断状態) |
| 0x42 | 2 | ECU入力・制御モジュール電圧 | $\frac{(A \times 256) + B}{1000}$ [V] |
| 0x45 | 1 | 相対スロットル位置 | $A \times \frac{100}{255}$ [%] |
| 0x46 | 1 | 外気温度 | $A - 40$ [°C] |
| 0x49 | 1 | アクセルペダル位置 D | $A \times \frac{100}{255}$ [%] |
| 0x4A | 1 | アクセルペダル位置 E | $A \times \frac{100}{255}$ [%] |
(4) PID 0x61 〜 0x80(拡張・エンジン特性制御群)
| $\small\textsf{ PID }$ | 返却 バイト数 |
項目 | デコード計算式・物理値変換 |
|---|---|---|---|
| 0x69 | 2 | 指令過給圧(ターボ目標圧) | $\frac{(A \times 256) + B}{32}$ [kPa](絶対圧) |
| 0x6D | 5 | コモンレール燃料圧力(RailP) | $((A \times 256) + B) \times 10$ [kPa] |
| 0x70 | 10 | ターボチャージャー過給圧特性 | 複数系統の過給圧目標値、実測値等 |
| 0x78 | 9 | 排気温度センサー(EGT) | マルチフレーム。各排気位置($A \sim I$)の温度:$value - 40$ [°C] |
| 0x7A | 5 | DPF差圧特性 | マルチフレーム。A, Bバイトが差圧等 |
(5) PID 0x81 〜 0xA0(拡張・ディーゼル排気制御群)
| $\small\textsf{ PID }$ | 返却 バイト数 |
項目 | デコード計算式・物理値変換 |
|---|---|---|---|
| 0x83 | 5 | NOxセンサーデータ |
センサー1 (ppm): $(A \times 256) + B - 200$ センサー2 (ppm): $(C \times 256) + D - 200$ |
| 0x88 | 5 | SCR / 尿素水(AdBlue)状態 | $A \sim E$ バイト:各動作状態フラグ、および残量レベル等 |
| 0x8B | 7 | DPF動作状態・堆積スート量 |
DPF差圧 (kPa): $\frac{(A \times 256) + B}{10} - 3276.8$ スート堆積量 (g): $\frac{(C \times 256) + D}{100}$ |
| 0x8C | 5 | DPF排気ガス温度特性 |
触媒前温度 (°C): $\frac{(A \times 256) + B}{10} - 40$ DPF前温度 (°C): $\frac{(C \times 256) + D}{10} - 40$ |
ディーゼル車だということがわかりますね。
⭐️返却データが5バイト以上のPIDは、マルチフレームで返却されます。
上記can_v1.inoはシングフレーム決め打ちになっています(CAN通信は1フレームの最大が8バイト)。
次回は、マルチフレームも含めて本格的な情報取得と、合わせて無線化をします。
7. マルチフレーム (2026.8.9 追加)
PIDの応答データの ほとんどは シングルフレームですが、返送データが4バイトを超える場合は、マルチフレームになります。
(1) シングルフレーム
-
単純な問合せ応答パターン
Sender Receiver Request (PID) → ← Response (Single Frame)
(2) マルチフレーム
-
レスポンスデータの先頭バイト(の先頭4ビット)をチェックすると、マルチフレーム(First Frame)だと分かります。Flow Control Frame を送って後続フレームを受信することができる
Sender Receiver Request (PID) → ← Response (First Frame)
(2バイト目にデータ長)
(データ 1〜4バイト目)Flow Control Frame → ← Response (Consecutive Frame)
(データ 5〜11バイト目)← Response (Consecutive Frame)
(以降、データ長まで繰り返し)
(3) フレーム種別
-
データの先頭バイトの先頭4ビット
先頭バイトの
先頭4ビット意味 0x0 Response (Single Frame) 0x1 Response (First Frame)
(2バイト目にデータ長)0x2 Response (Consecutive Frame) 0x3 Flow Control Frame
(2) へ続く
以上

