Chapter 156: Automotive Diagnostics with CAN (OBD-II)
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the fundamentals of the On-Board Diagnostics version II (OBD-II) standard and its relationship with the Controller Area Network (CAN) bus.
- Configure the ESP32‘s Two-Wire Automotive Interface (TWAI) controller to communicate with a vehicle’s OBD-II system.
- Implement requests for standard OBD-II Parameter IDs (PIDs) to retrieve live data from a vehicle.
- Parse and interpret common OBD-II responses, such as engine RPM, vehicle speed, and coolant temperature.
- Develop a basic ESP32 application to read and display vehicle diagnostic parameters.
- Recognize differences in implementing OBD-II solutions across various ESP32 variants, particularly those with and without built-in CAN controllers.
- Identify common issues and apply troubleshooting techniques when working with ESP32 and OBD-II.
Introduction
In the modern automotive world, access to vehicle data is crucial for diagnostics, performance monitoring, and custom vehicle integrations. The On-Board Diagnostics version II (OBD-II) standard provides a universal gateway to this information for most cars and light trucks manufactured since the mid-1990s. While several communication protocols were initially supported by OBD-II, the Controller Area Network (CAN) bus (specifically ISO 15765-4) has become the predominant standard.
As we learned in Chapters 151-155, many ESP32 variants are equipped with a built-in TWAI controller, which is compatible with the CAN protocol. This capability, combined with the ESP32’s processing power and connectivity options, makes it an excellent candidate for creating sophisticated, low-cost OBD-II diagnostic tools, data loggers, or custom vehicle display systems.
This chapter will guide you through the process of leveraging the ESP32’s TWAI peripheral to interact with a vehicle’s OBD-II system. We will cover the theoretical aspects of OBD-II communication over CAN and then dive into practical examples of how to request and interpret diagnostic data.
Theory
What is OBD-II?
OBD-II is a standard mandated by environmental protection agencies (like the EPA in the USA) primarily for monitoring and reporting on vehicle emissions control systems. However, its scope has expanded to provide access to a wealth of data from various Electronic Control Units (ECUs) within a vehicle.
Key Aspects of OBD-II:
- Purpose:
- Monitor the performance of emission-critical components and systems.
- Detect malfunctions and illuminate a Malfunction Indicator Lamp (MIL), commonly known as the “check engine” light.
- Store Diagnostic Trouble Codes (DTCs) to identify the nature of detected faults.
- Provide access to real-time sensor data (live data) and system status.
- Standardization:
- SAE J1979 / ISO 15031-5: Defines standard diagnostic test modes (services) and Parameter IDs (PIDs) for requesting data.
- ISO 15765-4: Specifies the use of CAN as a transport protocol for OBD-II diagnostics. This is the most common protocol in vehicles manufactured after 2008.
- OBD-II Connector (SAE J1962):
- A standardized 16-pin D-shaped connector, typically located within reach of the driver (e.g., under the dashboard).Provides pins for power, ground, and various communication protocols. For CAN-based OBD-II, the critical pins are:
- Pin 6: CAN High (CAN_H)Pin 14: CAN Low (CAN_L)Pin 4: Chassis GroundPin 5: Signal GroundPin 16: Battery Positive (+12V)
- A standardized 16-pin D-shaped connector, typically located within reach of the driver (e.g., under the dashboard).Provides pins for power, ground, and various communication protocols. For CAN-based OBD-II, the critical pins are:
Pin | Description | Notes for CAN-based OBD-II |
---|---|---|
1 | Manufacturer Discretion | Varies |
2 | SAE J1850 Bus+ (PWM/VPW) | Not typically used if CAN is primary |
3 | Manufacturer Discretion | Varies |
4 | Chassis Ground | Critical Ground Connection |
5 | Signal Ground | Critical Ground Connection |
6 | CAN High (ISO 15765-4) | Critical for CAN Communication |
7 | K-Line (ISO 9141-2 / ISO 14230-4) | Not typically used if CAN is primary |
8 | Manufacturer Discretion | Varies |
9 | Manufacturer Discretion | Varies |
10 | SAE J1850 Bus- (PWM only) | Not typically used if CAN is primary |
11 | Manufacturer Discretion | Varies |
12 | Manufacturer Discretion | Varies |
13 | Manufacturer Discretion | Varies |
14 | CAN Low (ISO 15765-4) | Critical for CAN Communication |
15 | L-Line (ISO 9141-2 / ISO 14230-4) | Not typically used if CAN is primary |
16 | Battery Positive (+12V) | Provides Power |
OBD-II Services (Modes)
OBD-II defines several “services” or “modes” to access different types of diagnostic information. These are requested by sending a message to the vehicle’s ECUs. Some key services include:
- Mode
$01
(Show current data): Requests real-time values from sensors and ECUs (e.g., engine RPM, vehicle speed, oxygen sensor voltage). This is the most commonly used mode for live data monitoring. - Mode
$02
(Show freeze frame data): Requests a snapshot of sensor data captured at the moment a DTC was stored. - Mode
$03
(Show stored Diagnostic Trouble Codes): Requests the list of active DTCs related to emissions. - Mode
$04
(Clear Diagnostic Trouble Codes and stored values): Clears DTCs and related freeze frame data. Use with extreme caution! - Mode
$07
(Show pending Diagnostic Trouble Codes): Requests DTCs detected during the current or last completed driving cycle but not yet confirmed enough to illuminate the MIL. - Mode
$09
(Request vehicle information): Requests static information about the vehicle, such as the Vehicle Identification Number (VIN), calibration IDs, etc. - Mode
$0A
(Show permanent Diagnostic Trouble Codes): Requests DTCs that cannot be cleared by Mode$04
and are typically only cleared by the ECU after a repair and verification cycle.
For our practical examples, we will primarily focus on Mode $01
.
Parameter IDs (PIDs)
When using Mode $01
(or other relevant modes), you specify the exact piece of data you want by including a Parameter ID (PID) in your request. Each PID corresponds to a specific sensor reading or calculated value.
- Request Structure: An OBD-II request using CAN typically involves sending a CAN frame with a specific CAN ID and data payload. For a Mode $01 request, the data payload usually looks like:[Number of additional data bytes (typically 0x02), Service Mode (e.g., 0x01), PID code, Padding (e.g., 0x00, 0x00, 0x00, 0x00, 0x00)]For example, to request Engine RPM (PID 0x0C) using Mode $01:Data: [0x02, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00]
- Response Structure: The ECU responds with a message. For a successful Mode $01 response, the data payload typically looks like:[Number of additional data bytes, Positive Response Mode (e.g., 0x41, which is Mode + 0x40), PID code, Value Byte A, Value Byte B, Value Byte C, Value Byte D, Padding]For example, a response for Engine RPM (PID 0x0C):Data: [0x04, 0x41, 0x0C, A, B, 0x00, 0x00, 0x00] (RPM uses 2 data bytes A and B)
- PID Interpretation: The meaning and formula for converting the data bytes (A, B, C, D) into a human-readable value are specific to each PID.
Common PIDs (Mode $01
):
PID Code | Description | Data Bytes Used (Example) | Formula (Example) | Units |
---|---|---|---|---|
0x00 | PIDs supported [01-20] | 4 (A,B,C,D) | Bitmask | – |
0x04 | Calculated Engine Load | 1 (A) | (A * 100) / 255 | % |
0x05 | Engine Coolant Temperature | 1 (A) | A – 40 | °C |
0x0C | Engine RPM | 2 (A,B) | ((A * 256) + B) / 4 | rpm |
0x0D | Vehicle Speed | 1 (A) | A | km/h |
0x0F | Intake Air Temperature | 1 (A) | A – 40 | °C |
0x10 | MAF Air Flow Rate | 2 (A,B) | ((A * 256) + B) / 100 | grams/sec |
0x11 | Throttle Position | 1 (A) | (A * 100) / 255 | % |
0x1C | OBD standards this vehicle conforms to | 1 (A) | Enum list (see standard) | – |
0x1F | Run time since engine start | 2 (A,B) | (A * 256) + B | seconds |
0x20 | PIDs supported [21-40] | 4 (A,B,C,D) | Bitmask | – |
0x2F | Fuel Tank Level Input | 1 (A) | (A * 100) / 255 | % |
0x31 | Distance traveled since codes cleared | 2 (A,B) | (A * 256) + B | km |
0x42 | Control module voltage | 2 (A,B) | ((A * 256) + B) / 1000 | V |
0x5E | Engine fuel rate | 2 (A,B) | ((A * 256) + B) * 0.05 | L/h |
graph TD A["ESP32: Prepare OBD-II Request <br> e.g., Mode $01, PID $0C (RPM)"] --> B{"Construct CAN Message <br> ID: 0x7DF (Functional Request) <br> Data: [0x02, 0x01, 0x0C, 00, 00, 00, 00, 00]"}; B --> C[ESP32: Transmit CAN Message <br> via TWAI Controller & Transceiver]; C --> D{{Vehicle CAN Bus}}; D --> E[Vehicle ECUs Receive Request]; E --> F{"ECU (e.g., Engine Control Module): <br> Is request valid and PID supported?"}; F -- Yes --> G[ECU: Prepare Response <br> e.g., Mode $41, PID $0C, Data A, B]; G --> H{"ECU: Construct CAN Message <br> ID: 0x7E8 (Physical Response) <br> Data: [0x04, 0x41, 0x0C, A, B, 00, 00, 00]"}; H --> I{{Vehicle CAN Bus}}; I --> J[ESP32: Receive CAN Message <br> via TWAI Controller & Transceiver]; J --> K{"ESP32: Filter for Response ID <br> (e.g., 0x7E8 - 0x7EF)"}; K -- Match --> L["ESP32: Parse Response Data <br> Extract PID and Values (A, B)"]; L --> M["ESP32: Calculate Human-Readable Value <br> e.g., RPM = ((A*256)+B)/4"]; M --> N["ESP32: Display/Use Data (e.g., Engine RPM)"]; F -- No --> O["ECU: Send Negative Response (NRC) <br> or No Response"]; O --> I; K -- No Match --> P[ESP32: Discard or Log Irrelevant Message]; classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef check fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; class A,C,J,L,M primary; class B,H,G process; class D,I process; class E process; class F,K decision; class N,P success; class O check;
CAN Protocol in OBD-II (ISO 15765-4)
ISO 15765-4 defines how OBD-II messages are transported over a CAN bus.
- CAN IDs:
- Standard (11-bit) IDs: Most commonly used for legislated OBD-II.
- Functional Request ID:
0x7DF
. A message sent to this ID is a broadcast to all ECUs capable of OBD-II responses. - Physical Response IDs:
0x7E8
to0x7EF
. Each ECU that responds to a functional request will use a unique ID from this range (e.g., Engine ECU might be0x7E8
, Transmission ECU0x7E9
).
- Functional Request ID:
- Extended (29-bit) IDs: Less common for standard OBD-II PIDs but may be used for manufacturer-specific diagnostics.
- Standard (11-bit) IDs: Most commonly used for legislated OBD-II.
- CAN Bus Speed:
- The most common speed is 500 kbps.
- Some vehicles, particularly older ones or specific systems, might use 250 kbps.
- It’s crucial to configure the ESP32’s TWAI controller to match the vehicle’s CAN bus speed.
- Message Length and Segmentation (ISO 15765-2 – ISO-TP):
- A standard CAN frame can carry up to 8 bytes of data.
- OBD-II responses can sometimes be longer than what fits in a single CAN frame (e.g., VIN request).
- ISO 15765-2 (ISO Transport Protocol) defines how to segment longer messages into multiple CAN frames and reassemble them. This involves:
- Single Frame (SF): For messages up to 7 data bytes (OBD-II data).
- First Frame (FF): Indicates the start of a multi-frame message and the total length.
- Consecutive Frame (CF): Carries subsequent segments of the message.
- Flow Control (FC): Sent by the receiver to manage the sender’s transmission rate and buffer availability.
- For many common PIDs (like RPM, speed, temp), the response fits in a single frame, simplifying implementation.
sequenceDiagram participant Sender as ECU participant Receiver as ESP32 Sender->>+Receiver: 1. First Frame (FF)<br/>[PCI: 1 | TotalLength (12 bits)]<br/>[Data Segment 1] Note right of Receiver: Receiver checks if it can handle message size Receiver-->>-Sender: 2. Flow Control (FC)<br/>[PCI: 30 | BlockSize | STmin]<br/>(e.g., ClearToSend) loop For each subsequent block Sender->>+Receiver: 3. Consecutive Frame (CF)<br/>[PCI: 2 | SeqNum (4 bits)]<br/>[Data Segment N] Note over Sender,Receiver: Sequence Number increments Sender->>+Receiver: ... more CFs ... Sender->>+Receiver: X. Last Consecutive Frame (CF)<br/>[PCI: 2 | SeqNum (4 bits)]<br/>[Final Data Segment] end Note right of Receiver: Receiver reassembles all segments<br/>to get the complete message.
ESP32 TWAI Controller for OBD-II
As detailed in Chapters 151-155, the ESP32’s TWAI peripheral can be configured to communicate on a CAN bus. For OBD-II applications, key considerations are:
- Driver Installation and Start: Use
twai_driver_install()
,twai_start()
. - Timing Configuration (
twai_timing_config_t
): Must be set for the correct baud rate (e.g.,TWAI_TIMING_CONFIG_500KBITS()
orTWAI_TIMING_CONFIG_250KBITS()
). You can also define custom timings if needed. - Acceptance Filter Configuration (
twai_filter_config_t
): This is critical. You need to configure the TWAI controller to accept messages only from the relevant OBD-II ECU response IDs (e.g.,0x7E8
through0x7EF
). Without proper filtering, the ESP32 might be overwhelmed by other CAN traffic on the vehicle bus or miss the responses. A common approach is to set a range filter or multiple single ID filters.- The filter is defined by an acceptance code and an acceptance mask. For a simple case of accepting a range like
0x7E8
to0x7EF
, you might need to set a mask that allows the lower bits to vary while matching the upper bits, or accept all messages and filter in software (less efficient). A more precise filter would be:- Accept Code:
0x7E8 << (21-8)
(for 11-bit ID, shifted into the filter register format) - Accept Mask:
~((0x7EF ^ 0x7E8) << (21-8))
(mask to allow IDs from 7E8 to 7EF). - Alternatively, for simpler scenarios, one might set multiple filters, one for each expected ECU ID (e.g., one for
0x7E8
, one for0x7E9
, etc.), or a broader filter likecode=0x700
,mask=~0x700
to accept all0x7xx
messages and then filter in software. For OBD-II, it’s common to accept0x7E8
to0x7EF
. A practical approach is to set a filter that accepts messages with IDs from0x7E8
up to0x7EF
. For example, a mask could be0xFF8
(shifted appropriately for the hardware filter format) and a code of0x7E8
. - A common filter setup for OBD-II is to accept IDs from
0x000
to0x7FF
if you want to catch all standard CAN traffic, and then filter programmatically. However, for specific OBD-II responses, filtering for0x7E8
to0x7EF
is more targeted. - For ESP-IDF v5.x,
twai_filter_config_t
usesacceptance_code
andacceptance_mask
. The mask bits determine which bits of the incoming ID must match theacceptance_code
. A0
in a mask bit means the corresponding ID bit must match the code bit. A1
means the ID bit is a “don’t care”. To accept a specific ID, the mask is typically all zeros. To accept a range is more complex with a single filter entry, often easier to allow a broader range and filter in software or use multiple hardware filter entries if the hardware supports it. For OBD-II, we are interested in IDs0x7E8
through0x7EF
. A simple filter could beacceptance_code = 0x000
,acceptance_mask = 0x000
(accept all standard IDs), and then software filtering. Or, more specifically, if we expect a response from0x7E8
:acceptance_code = (0x7E8 << 21)
,acceptance_mask = ~(0x7FF << 21)
for single ID, or a more complex mask for a narrow range. A simpler hardware filter might target the upper bits:acceptance_code = (0x7E0 << 21)
,acceptance_mask = ~(0x7F0 << 21)
to accept0x7E0-0x7EF
. - Let’s simplify: for OBD-II, the primary response IDs are
0x7E8
to0x7EF
. We can set the filter to accept all standard IDs and then check the ID in the received message:filter_config = TWAI_FILTER_CONFIG_ACCEPT_ALL()
and then checkrx_message.identifier
in the code. This is easier to manage for beginners.
- Accept Code:
- The filter is defined by an acceptance code and an acceptance mask. For a simple case of accepting a range like
- Sending Requests: Use
twai_transmit()
to send the formatted OBD-II request CAN message. - Receiving Responses: Use
twai_receive()
to get the ECU’s response. This is typically a blocking call with a timeout.
Practical Examples
Warning: Connecting electronic devices to your vehicle’s OBD-II port can be risky if not done correctly. Always ensure your wiring is correct and your CAN transceiver is functioning properly. Incorrect connections or faulty hardware could potentially interfere with vehicle systems or even cause damage. Proceed with caution and at your own risk. It is highly recommended to test your setup with an OBD-II simulator before connecting to a live vehicle. Never attempt to clear DTCs (Mode
$04
) unless you are a qualified technician and understand the implications. Our examples will focus on read-only operations.
Hardware Setup
- ESP32 Development Board: Any ESP32, ESP32-S2, ESP32-S3, ESP32-C6, or ESP32-H2 board with available GPIOs for TWAI.
- CAN Transceiver Module: A module based on a chip like MCP2551, TJA1050, or SN65HVD230. This is essential to convert the ESP32’s logic-level signals (TX/RX) to the differential CAN bus signals (CAN_H/CAN_L).
- Connect ESP32’s TWAI TX pin to the transceiver’s TXD (or equivalent) pin.
- Connect ESP32’s TWAI RX pin to the transceiver’s RXD (or equivalent) pin.
- Connect the transceiver’s CAN_H and CAN_L pins to the corresponding pins on the OBD-II connector (Pin 6 for CAN_H, Pin 14 for CAN_L).
- Power the transceiver (usually 3.3V or 5V, check its datasheet) and ensure a common ground with the ESP32.
- OBD-II Connector (J1962 Male): To plug into the vehicle’s port.
- Vehicle or OBD-II Simulator: For testing. An OBD-II simulator is highly recommended for development.
- Wiring:
- ESP32 TWAI_TX_GPIO -> CAN Transceiver TX input
- ESP32 TWAI_RX_GPIO -> CAN Transceiver RX output
- CAN Transceiver CAN_H -> OBD-II Connector Pin 6
- CAN Transceiver CAN_L -> OBD-II Connector Pin 14
- ESP32 GND -> CAN Transceiver GND -> OBD-II Connector Pin 4 (Chassis Ground) and/or Pin 5 (Signal Ground)
- Power for ESP32 and Transceiver (e.g., from OBD-II Pin 16 if allowed and regulated, or separate supply).
Project Setup (VS Code, ESP-IDF)
- Create a new ESP-IDF project in VS Code (e.g.,
esp32_obdii_reader
). - The TWAI driver is part of ESP-IDF by default.
- Configure TWAI GPIO pins in
menuconfig
orsdkconfig.defaults
. For example:- Open
sdkconfig.defaults
in your project’s root directory (create it if it doesn’t exist). - Add:
CONFIG_TWAI_TX_GPIO=21 CONFIG_TWAI_RX_GPIO=22
(Adjust GPIO numbers based on your board and wiring. Common defaults for ESP32 are GPIO21 (TX) and GPIO22 (RX), but always verify for your specific hardware). - Run
idf.py reconfigure
if you modifysdkconfig.defaults
or change settings inmenuconfig
(Component config -> TWAI Controller
).
- Open
Example 1: Basic OBD-II Connection and PID Request (Engine RPM)
This example initializes the TWAI driver, configures it for 500 kbps, sets a general acceptance filter, and then periodically requests and prints the engine RPM.
main/main.c
:
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/twai.h"
#include "esp_log.h"
static const char *TAG = "OBD2_RPM_READER";
// Default TWAI GPIOs - these can be configured via menuconfig
#define TWAI_TX_GPIO CONFIG_TWAI_TX_GPIO
#define TWAI_RX_GPIO CONFIG_TWAI_RX_GPIO
// OBD-II Defines
#define OBD2_CAN_REQUEST_ID 0x7DF // Functional addressing request ID
#define OBD2_SERVICE_CURRENT_DATA 0x01 // Service $01 - Show current data
#define OBD2_PID_ENGINE_RPM 0x0C // PID for Engine RPM
// Function to initialize TWAI driver
esp_err_t twai_init_driver(void) {
// Initialize TWAI general configuration
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TWAI_TX_GPIO, TWAI_RX_GPIO, TWAI_MODE_NORMAL);
// Initialize TWAI timing configuration for 500 kbit/s
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();
// Initialize TWAI filter configuration to accept all messages (software filtering will be done)
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
// Install TWAI driver
if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) {
ESP_LOGE(TAG, "Failed to install TWAI driver");
return ESP_FAIL;
}
ESP_LOGI(TAG, "TWAI driver installed");
// Start TWAI driver
if (twai_start() != ESP_OK) {
ESP_LOGE(TAG, "Failed to start TWAI driver");
return ESP_FAIL;
}
ESP_LOGI(TAG, "TWAI driver started");
return ESP_OK;
}
// Function to send an OBD-II PID request
esp_err_t send_obd2_request(uint8_t service, uint8_t pid) {
twai_message_t message;
message.identifier = OBD2_CAN_REQUEST_ID; // 11-bit ID
message.flags = TWAI_MSG_FLAG_NONE; // Or TWAI_MSG_FLAG_EXTD for 29-bit if needed
message.data_length_code = 8; // OBD-II requests are typically 8 bytes
message.data[0] = 0x02; // Number of additional data bytes in this request (Service + PID)
message.data[1] = service;
message.data[2] = pid;
message.data[3] = 0x00; // Padding / Don't care bytes
message.data[4] = 0x00;
message.data[5] = 0x00;
message.data[6] = 0x00;
message.data[7] = 0x00;
// Queue message for transmission
if (twai_transmit(&message, pdMS_TO_TICKS(1000)) == ESP_OK) {
ESP_LOGI(TAG, "Message queued for transmission: Service 0x%02X, PID 0x%02X", service, pid);
return ESP_OK;
} else {
ESP_LOGE(TAG, "Failed to queue message for transmission");
return ESP_FAIL;
}
}
// Function to receive and parse OBD-II response for RPM
void receive_and_parse_rpm_response(void) {
twai_message_t rx_message;
esp_err_t ret = twai_receive(&rx_message, pdMS_TO_TICKS(2000)); // Wait up to 2 seconds
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Message received: ID: 0x%03lX, DLC: %d", rx_message.identifier, rx_message.data_length_code);
// Log raw data bytes
char data_str[3 * TWAI_FRAME_MAX_DLC + 1] = {0};
for (int i = 0; i < rx_message.data_length_code; i++) {
sprintf(data_str + i * 3, "%02X ", rx_message.data[i]);
}
ESP_LOGI(TAG, "Data: %s", data_str);
// Basic check for OBD-II response (ID 0x7E8-0x7EF, Mode 0x41, requested PID)
// A more robust solution would check the full ID range. 0x7E8 is common for engine ECU.
if ((rx_message.identifier >= 0x7E8 && rx_message.identifier <= 0x7EF) &&
rx_message.data_length_code >= 4 && // Minimum length for RPM response
rx_message.data[1] == (OBD2_SERVICE_CURRENT_DATA + 0x40) && // Positive response for Service $01 is $41
rx_message.data[2] == OBD2_PID_ENGINE_RPM) {
uint8_t byte_a = rx_message.data[3];
uint8_t byte_b = rx_message.data[4];
float rpm = ((float)(byte_a * 256 + byte_b)) / 4.0f;
ESP_LOGI(TAG, "Engine RPM: %.2f rpm", rpm);
} else {
ESP_LOGW(TAG, "Received non-matching or unexpected OBD-II response.");
}
} else if (ret == ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "Timeout receiving OBD-II response.");
} else {
ESP_LOGE(TAG, "Failed to receive message: %s", esp_err_to_name(ret));
}
}
void obd2_task(void *pvParameters) {
if (twai_init_driver() != ESP_OK) {
ESP_LOGE(TAG, "TWAI initialization failed. Task stopping.");
vTaskDelete(NULL);
return;
}
while (1) {
ESP_LOGI(TAG, "Requesting Engine RPM (PID 0x%02X)...", OBD2_PID_ENGINE_RPM);
if (send_obd2_request(OBD2_SERVICE_CURRENT_DATA, OBD2_PID_ENGINE_RPM) == ESP_OK) {
// Wait a short moment for ECU to process and respond
vTaskDelay(pdMS_TO_TICKS(100)); // Adjust as needed, 100ms is a starting point
receive_and_parse_rpm_response();
} else {
ESP_LOGE(TAG, "Failed to send RPM request.");
}
vTaskDelay(pdMS_TO_TICKS(5000)); // Request RPM every 5 seconds
}
}
void app_main(void) {
ESP_LOGI(TAG, "Starting OBD-II RPM Reader Application");
xTaskCreate(obd2_task, "obd2_task", 4096, NULL, 5, NULL);
}
Build Instructions:
- Save the code as
main/main.c
in your project. - Ensure your
sdkconfig.defaults
ormenuconfig
settings for TWAI GPIOs are correct. - Open the ESP-IDF terminal in VS Code.
- Run
idf.py build
.
Run/Flash/Observe:
- Flash the built firmware to your ESP32:
idf.py -p /dev/ttyUSB0 flash
(replace/dev/ttyUSB0
with your ESP32’s serial port). - Connect your ESP32 (with CAN transceiver) to your vehicle’s OBD-II port or an OBD-II simulator.Tip: Ensure the vehicle’s ignition is in the “ON” position (engine can be off or running) for the ECUs to be powered and responsive. Some vehicles require the engine to be running for certain PIDs.
- Open the serial monitor:
idf.py -p /dev/ttyUSB0 monitor
. - You should see log messages indicating the TWAI driver starting, RPM requests being sent, and (if connected correctly and the vehicle responds) the received messages and calculated RPM values.Example Output:
I (XYZ) OBD2_RPM_READER: Starting OBD-II RPM Reader Application
I (XYZ) OBDI2_RPM_READER: TWAI driver installed
I (XYZ) OBDI2_RPM_READER: TWAI driver started
I (XYZ) OBD2_RPM_READER: Requesting Engine RPM (PID 0x0C)...
I (XYZ) OBD2_RPM_READER: Message queued for transmission: Service 0x01, PID 0x0C
I (XYZ) OBD2_RPM_READER: Message received: ID: 0x7E8, DLC: 8
I (XYZ) OBD2_RPM_READER: Data: 04 41 0C 0C F0 00 00 00
I (XYZ) OBD2_RPM_READER: Engine RPM: 828.00 rpm
Example 2: Reading Multiple PIDs (Speed and Coolant Temperature)
This example extends the previous one to request and parse Vehicle Speed (PID 0x0D
) and Engine Coolant Temperature (PID 0x05
).
// Add these defines near the top
#define OBD2_PID_VEHICLE_SPEED 0x0D
#define OBD2_PID_COOLANT_TEMP 0x05
// Modify/add these functions:
// Generic PID processing function
void process_obd2_response(twai_message_t *rx_message) {
ESP_LOGI(TAG, "Processing received message: ID: 0x%03lX, DLC: %d", rx_message->identifier, rx_message->data_length_code);
// Log raw data bytes
char data_str[3 * TWAI_FRAME_MAX_DLC + 1] = {0};
for (int i = 0; i < rx_message->data_length_code; i++) {
sprintf(data_str + i * 3, "%02X ", rx_message->data[i]);
}
ESP_LOGI(TAG, "Data: %s", data_str);
if (!((rx_message->identifier >= 0x7E8 && rx_message->identifier <= 0x7EF) &&
rx_message->data[1] == (OBD2_SERVICE_CURRENT_DATA + 0x40))) {
ESP_LOGW(TAG, "Received non-matching or unexpected OBD-II response header.");
return;
}
uint8_t responded_pid = rx_message->data[2];
uint8_t byte_a = rx_message->data[3];
uint8_t byte_b = rx_message->data[4]; // Used by some PIDs
switch (responded_pid) {
case OBD2_PID_ENGINE_RPM:
if (rx_message->data_length_code >= 5) { // Need at least 2 data bytes (A, B) + 3 header bytes
float rpm = ((float)(byte_a * 256 + byte_b)) / 4.0f;
ESP_LOGI(TAG, "Engine RPM: %.2f rpm", rpm);
} else {
ESP_LOGW(TAG, "RPM response too short.");
}
break;
case OBD2_PID_VEHICLE_SPEED:
if (rx_message->data_length_code >= 4) { // Need at least 1 data byte (A) + 3 header bytes
float speed = (float)byte_a;
ESP_LOGI(TAG, "Vehicle Speed: %.0f km/h", speed);
} else {
ESP_LOGW(TAG, "Speed response too short.");
}
break;
case OBD2_PID_COOLANT_TEMP:
if (rx_message->data_length_code >= 4) { // Need at least 1 data byte (A) + 3 header bytes
float temp = (float)byte_a - 40.0f;
ESP_LOGI(TAG, "Coolant Temperature: %.0f C", temp);
} else {
ESP_LOGW(TAG, "Coolant temp response too short.");
}
break;
default:
ESP_LOGW(TAG, "Received response for unhandled PID: 0x%02X", responded_pid);
break;
}
}
void obd2_multi_pid_task(void *pvParameters) {
if (twai_init_driver() != ESP_OK) {
ESP_LOGE(TAG, "TWAI initialization failed. Task stopping.");
vTaskDelete(NULL);
return;
}
uint8_t pids_to_request[] = {OBD2_PID_ENGINE_RPM, OBD2_PID_VEHICLE_SPEED, OBD2_PID_COOLANT_TEMP};
int num_pids = sizeof(pids_to_request) / sizeof(pids_to_request[0]);
int current_pid_index = 0;
while (1) {
uint8_t current_pid = pids_to_request[current_pid_index];
ESP_LOGI(TAG, "Requesting PID 0x%02X...", current_pid);
if (send_obd2_request(OBD2_SERVICE_CURRENT_DATA, current_pid) == ESP_OK) {
vTaskDelay(pdMS_TO_TICKS(100)); // Wait for ECU
twai_message_t rx_msg;
esp_err_t ret = twai_receive(&rx_msg, pdMS_TO_TICKS(1000)); // Shorter timeout per PID
if (ret == ESP_OK) {
process_obd2_response(&rx_msg);
} else if (ret == ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "Timeout receiving response for PID 0x%02X", current_pid);
} else {
ESP_LOGE(TAG, "Failed to receive for PID 0x%02X: %s", current_pid, esp_err_to_name(ret));
}
} else {
ESP_LOGE(TAG, "Failed to send request for PID 0x%02X.", current_pid);
}
current_pid_index = (current_pid_index + 1) % num_pids;
vTaskDelay(pdMS_TO_TICKS(2000)); // Delay between requesting different PIDs
}
}
// In app_main:
// xTaskCreate(obd2_task, "obd2_task", 4096, NULL, 5, NULL);
// Replace with:
// xTaskCreate(obd2_multi_pid_task, "obd2_multi_pid_task", 4096, NULL, 5, NULL);
Note: This example requests PIDs sequentially. A more advanced approach might involve asynchronous requests or checking which PIDs are supported by the vehicle first (using PID 0x00
, 0x20
, etc.).
Example 3: Reading Diagnostic Trouble Codes (DTCs) – Basic
This example demonstrates how to request Mode $03
to get the count and list of stored DTCs. Full DTC decoding is complex; this example will show the raw DTC bytes.
// Add these defines
#define OBD2_SERVICE_STORED_DTCS 0x03
// Function to request and process DTCs
void request_and_process_dtcs(void) {
ESP_LOGI(TAG, "Requesting Stored DTCs (Service 0x%02X)...", OBD2_SERVICE_STORED_DTCS);
// Mode 03 request typically doesn't need a PID, but the standard request format is often kept.
// Some ECUs might expect the PID field to be zero or absent (DLC adjusted).
// For simplicity, we send a standard 8-byte frame, PID field as 0.
// Data for Mode 03: [0x01 (Num bytes = 1, for service), 0x03 (Service), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
// More correctly, the first byte (length) for Mode 03 is 0x01 (only service byte follows)
// However, many implementations send 0x02, 0x03, 0x00...
// Let's try the common way:
twai_message_t message;
message.identifier = OBD2_CAN_REQUEST_ID;
message.flags = TWAI_MSG_FLAG_NONE;
message.data_length_code = 8;
message.data[0] = 0x01; // Number of data bytes following this one is 1 (just the service mode)
// Some OBD tools send 0x02, 0x03, 0x00... but 0x01, 0x03 is more aligned with some interpretations.
// Let's use the common structure seen in many tools:
// Byte 0: Number of meaningful bytes to follow (e.g., 1 for just service, or 2 if including a dummy PID)
// For Mode 03, it's typically just the service.
// A common request is [0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
// Let's use the simpler [DLC=2, Service=0x03]
message.data[0] = 0x02; // Number of data bytes for this request (Service + dummy PID/count)
message.data[1] = OBD2_SERVICE_STORED_DTCS; // Service $03
for(int i=2; i<8; ++i) message.data[i] = 0x00; // Pad with 0x00 or 0x55 (CAN fill pattern)
if (twai_transmit(&message, pdMS_TO_TICKS(1000)) != ESP_OK) {
ESP_LOGE(TAG, "Failed to queue DTC request message");
return;
}
vTaskDelay(pdMS_TO_TICKS(100)); // Wait for ECU
twai_message_t rx_message;
// DTC responses can be multi-frame. This basic example only handles single-frame.
// A full implementation requires ISO-TP handling.
esp_err_t ret = twai_receive(&rx_message, pdMS_TO_TICKS(2000));
if (ret == ESP_OK) {
ESP_LOGI(TAG, "DTC Response: ID: 0x%03lX, DLC: %d", rx_message.identifier, rx_message.data_length_code);
char data_str[3 * TWAI_FRAME_MAX_DLC + 1] = {0};
for (int i = 0; i < rx_message.data_length_code; i++) {
sprintf(data_str + i * 3, "%02X ", rx_message.data[i]);
}
ESP_LOGI(TAG, "Data: %s", data_str);
// Check for positive response to Mode 03 (0x43)
if ((rx_message.identifier >= 0x7E8 && rx_message.identifier <= 0x7EF) &&
rx_message.data[1] == (OBD2_SERVICE_STORED_DTCS + 0x40)) {
// Byte 2 (rx_message.data[2]) contains the number of DTCs.
// This count also includes MIL status in its bits.
// Actual number of DTCs is often just this byte if no MIL info is encoded,
// or it's the first byte of the DTC list itself.
// A typical response: [NumBytes, 0x43, NumDTCs, DTC1_Byte1, DTC1_Byte2, DTC2_Byte1, DTC2_Byte2, ...]
// The first byte of data (rx_message.data[0]) is the count of bytes that follow.
// The second byte (rx_message.data[1]) is the response mode (0x43).
// The third byte (rx_message.data[2]) is the number of DTCs.
// Each DTC is 2 bytes.
uint8_t num_dtcs_field = rx_message.data[2]; // This field contains the count of DTCs.
// The MSB might indicate MIL status.
// For simplicity, we'll treat it as raw count.
ESP_LOGI(TAG, "Number of DTCs field: %d (0x%02X)", num_dtcs_field, num_dtcs_field);
// Assuming the format is [DLC, 0x43, Count, DTC1_Hi, DTC1_Lo, DTC2_Hi, DTC2_Lo, ...]
// and the DLC in rx_message.data[0] tells us how many bytes follow (e.g. 0x06 for Count + 2 DTCs)
// rx_message.data[0] is the length of the OBD data (e.g., 1 for no DTCs, 3 for 1 DTC, 5 for 2 DTCs)
// rx_message.data[1] is 0x43
// rx_message.data[2] is the first DTC byte 1 or number of DTCs (interpretation varies)
// Let's assume rx_message.data[2] is the count of DTCs.
// And subsequent pairs are DTCs.
// The actual number of DTCs encoded is (rx_message.data[0] - 1) / 2,
// where rx_message.data[0] is the PCI byte from the ECU (e.g. 06 for 2 DTCs + count byte)
// and rx_message.data[2] is the actual count byte.
// A common format for Mode 03 response is:
// Byte 0: Number of bytes of data to follow (e.g., 0x07 for 1 DTC, count, plus two pairs of DTC bytes)
// Byte 1: 0x43 (Response for Mode 03)
// Byte 2: Number of DTCs (e.g., 0x01 for one DTC)
// Byte 3, 4: First DTC (e.g., P0123 -> 0x01, 0x23)
// Byte 5, 6: Second DTC
// ...
// This example assumes a single frame response. Multi-frame is common for many DTCs.
if (rx_message.data_length_code > 3 && rx_message.data[0] >= 1) { // data[0] is PCI length
// The actual number of DTCs is in data[2]
uint8_t reported_dtc_count = rx_message.data[2];
ESP_LOGI(TAG, "Reported DTC Count: %d", reported_dtc_count);
int bytes_for_dtcs = rx_message.data[0] - 1; // PCI byte - count byte
int actual_dtcs_in_frame = bytes_for_dtcs / 2;
ESP_LOGI(TAG, "DTCs found in this frame: %d", actual_dtcs_in_frame);
for (int i = 0; i < actual_dtcs_in_frame && (3 + i * 2 + 1) < rx_message.data_length_code; i++) {
uint8_t dtc_byte1 = rx_message.data[3 + i * 2];
uint8_t dtc_byte2 = rx_message.data[3 + i * 2 + 1];
// Decode first two bits of dtc_byte1 for P, C, B, U
char type = ' ';
switch (dtc_byte1 >> 6) {
case 0: type = 'P'; break; // Powertrain
case 1: type = 'C'; break; // Chassis
case 2: type = 'B'; break; // Body
case 3: type = 'U'; break; // Network
}
// Remaining bits form the number
uint16_t code = ((dtc_byte1 & 0x3F) << 8) | dtc_byte2;
ESP_LOGI(TAG, "DTC #%d: %c%04X (Raw: %02X %02X)", i + 1, type, code, dtc_byte1, dtc_byte2);
}
if (actual_dtcs_in_frame == 0 && reported_dtc_count == 0) {
ESP_LOGI(TAG, "No stored DTCs reported by this ECU.");
}
} else if (rx_message.data_length_code >=3 && rx_message.data[0] == 0x01 && rx_message.data[2] == 0x00) {
// This means PCI length is 1 (for the count byte), and count is 0.
ESP_LOGI(TAG, "No stored DTCs reported by this ECU (Count is 0).");
} else {
ESP_LOGW(TAG, "DTC response format not fully parsed or no DTCs.");
}
} else {
ESP_LOGW(TAG, "Received non-matching or unexpected OBD-II response for DTCs.");
}
} else if (ret == ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "Timeout receiving DTC response.");
} else {
ESP_LOGE(TAG, "Failed to receive DTC message: %s", esp_err_to_name(ret));
}
}
// In your main task or a dedicated diagnostic task, call:
// request_and_process_dtcs();
// Remember to call twai_init_driver() first.
// Example call in obd2_task (replace RPM logic or add as a new step):
// void obd2_diag_task(void *pvParameters) {
// if (twai_init_driver() != ESP_OK) { /* ... error handling ... */ }
// while(1) {
// request_and_process_dtcs();
// vTaskDelay(pdMS_TO_TICKS(30000)); // Request DTCs every 30 seconds
// }
// }
Tip: DTC responses can often be multi-frame if there are many DTCs. The example above only handles a single-frame response for simplicity. A robust DTC reader needs to implement ISO 15765-2 transport protocol handling.
Variant Notes
- ESP32, ESP32-S2, ESP32-S3, ESP32-C6, ESP32-H2:
- These variants all feature a built-in TWAI (CAN) controller. The examples provided in this chapter are directly applicable to them.
- The primary difference will be the specific GPIO pins available or chosen for TWAI_TX and TWAI_RX. Always configure these correctly using
menuconfig
(underComponent config -> TWAI Controller
) or by settingCONFIG_TWAI_TX_GPIO
andCONFIG_TWAI_RX_GPIO
insdkconfig.defaults
. - The TWAI driver API (
driver/twai.h
) is consistent across these variants in ESP-IDF v5.x.
- ESP32-C3:
- The ESP32-C3 does not have a built-in hardware TWAI/CAN controller.
- To implement OBD-II functionality on an ESP32-C3, you would need to use an external CAN controller IC (e.g., MCP2515 connected via SPI, or a UART-to-CAN module) and a corresponding software library for that external controller.
- The code examples in this chapter, which rely on the
driver/twai.h
API for the built-in peripheral, will not work directly on an ESP32-C3 without significant modifications to interface with an external CAN controller.
- General:
- Always use a CAN transceiver (e.g., MCP2551, TJA1050) between the ESP32’s TWAI GPIOs and the physical CAN bus (OBD-II connector). The ESP32 outputs logic-level signals, while CAN is a differential bus.
- Ensure the chosen GPIOs for TWAI are not conflicting with other peripherals or functions on your specific development board or custom hardware.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Incorrect CAN Baud Rate | No communication, twai_receive() times out, ESP_ERR_TIMEOUT errors. twai_transmit() might return ESP_OK but no ACKs received, leading to bus errors. | Verify vehicle’s CAN bus speed (typically 500 kbps for OBD-II on pins 6 & 14, some use 250 kbps). Adjust twai_timing_config_t (e.g., TWAI_TIMING_CONFIG_500KBITS() or TWAI_TIMING_CONFIG_250KBITS()). |
Missing/Incorrectly Wired CAN Transceiver | No communication. Potential damage to ESP32 GPIOs or vehicle ECU if ESP32 TX/RX directly connected. Swapped CAN_H/CAN_L lines prevent communication. | Always use a CAN transceiver (e.g., MCP2551, TJA1050). Double-check wiring: ESP32 TX GPIO → Transceiver TXD, ESP32 RX GPIO → Transceiver RXD, Transceiver CAN_H → OBD-II Pin 6, Transceiver CAN_L → OBD-II Pin 14. Ensure common ground. |
Improper Acceptance Filter | twai_receive() times out or receives no messages, or receives messages but not the expected OBD-II responses (e.g., from 0x7E8-0x7EF). | For basic OBD-II, ensure software checks rx_message.identifier for the range 0x7E8 to 0x7EF. If using hardware filters, configure them correctly. Start with TWAI_FILTER_CONFIG_ACCEPT_ALL() and add software checks for IDs during development. |
OBD-II Request Formatting Errors | ECU does not respond, or sends a Negative Response Code (NRC). No data or unexpected data. | Review SAE J1979 for Mode $01 request format (e.g., [0x02, 0x01, PID, 0x00, 0x00, 0x00, 0x00, 0x00]). Ensure message.data_length_code is 8. Verify correct service mode and PID. |
Power Issues or Vehicle State | No communication at all. ESP32 may be unstable or not running. | Ensure stable power to ESP32 and CAN transceiver. Verify vehicle’s ignition is ON (ECUs unpowered otherwise). Some PIDs require engine running. Check OBD-II port for +12V on Pin 16. |
Incorrect TWAI GPIO Configuration | twai_driver_install() or twai_start() fails. No signals on TX/RX pins. | Verify CONFIG_TWAI_TX_GPIO and CONFIG_TWAI_RX_GPIO in sdkconfig.defaults or menuconfig match your hardware wiring. Ensure pins are not used by other peripherals. |
No Response or Timeout on twai_receive() | Code hangs at twai_receive() or returns ESP_ERR_TIMEOUT. | Check all above points. Is vehicle OBD-II active? Is CAN bus speed correct? Is transceiver wired and powered? Are filters too restrictive? Is the request valid? Is the timeout for twai_receive() sufficient (e.g., 100-200ms for simple PIDs, longer for complex requests)? |
Interpreting Response Data Incorrectly | Received data seems valid but calculations result in nonsensical values (e.g., very high RPM, incorrect temperature). | Double-check the formula and number of data bytes (A, B, C, D) used for the specific PID from SAE J1979 or reliable sources. Pay attention to byte order (MSB/LSB) and scaling factors. |
Exercises
- Exercise 1 (Easy): Read and Display Throttle Position
- Modify Example 1 or 2 to request and display the Throttle Position (PID
0x11
). The formula is(A * 100) / 255
where A is the first data byte in the response. The result is a percentage.
- Modify Example 1 or 2 to request and display the Throttle Position (PID
- Exercise 2 (Medium): Create a PID Support Checker
- OBD-II PIDs
0x00
,0x20
,0x40
, etc., report which other PIDs are supported by the ECU in a bitmask format. - Request PID
0x00
(supports PIDs01-20
). The response will be 4 bytes (A, B, C, D). - Interpret these 4 bytes as a 32-bit bitmask. If bit
N
is set, then PIDN
(in hexadecimal, corresponding to that bit position) is supported. For example, if the most significant bit of byte A is set, PID0x01
is supported. If the least significant bit of byte A is set, PID0x08
is supported. - Write a function that requests PID
0x00
and prints a list of supported PIDs from0x01
to0x20
.
- OBD-II PIDs
- Exercise 3 (Medium): Implement a Basic VIN Reader (Single Frame Focus)
- Request Vehicle Information (Mode
$09
), specifically the Vehicle Identification Number (VIN) which is PID0x02
. - A VIN response is typically multi-frame. For this exercise, focus on receiving and printing the data from the first frame of the response. You’ll see a part of the VIN.
- The response format for Mode
$09
, PID0x02
starts with[NumBytes, 0x49, 0x02, VIN_Message_Counter_and_Type, VIN_Char1, VIN_Char2, VIN_Char3, ...]
. - This will give you an introduction to responses that are more complex than simple single-byte or two-byte values.
- Request Vehicle Information (Mode
- Exercise 4 (Advanced): Basic ISO-TP First Frame (FF) Detection for VIN
- Building on Exercise 3, when you receive a response for Mode
$09
, PID0x02
, check if it’s an ISO-TP First Frame (FF). - An FF for an 11-bit CAN ID typically has the first nibble of the first data byte as
0x1
(e.g.,0x1X
). The following 12 bits (lower nibble of first byte + second byte) indicate the total length of the multi-frame message. - If an FF is detected, print the total expected length of the VIN message.
- Challenge (Optional): Send a Flow Control (FC) frame in response to the FF. This is a significant step towards full multi-frame reception. An FC frame from the ESP32 would typically tell the ECU it’s clear to send (CTS) the remaining Consecutive Frames (CF). This part is complex and requires careful study of ISO 15765-2.
- Building on Exercise 3, when you receive a response for Mode
Summary
- OBD-II provides standardized access to vehicle diagnostics, with CAN (ISO 15765-4) being the common communication protocol in modern vehicles.
- ESP32 variants (ESP32, S2, S3, C6, H2) equipped with a TWAI controller can interface with OBD-II systems when paired with a CAN transceiver.
- OBD-II communication involves sending requests for specific Parameter IDs (PIDs) using defined Service Modes (e.g., Mode
$01
for current data, Mode$03
for DTCs). - The functional request CAN ID for OBD-II is typically
0x7DF
, with ECUs responding on IDs like0x7E8
(engine),0x7E9
(transmission), etc. - Correct TWAI configuration (GPIOs, baud rate – usually 500 kbps, acceptance filters) is essential for successful communication. Using
TWAI_FILTER_CONFIG_ACCEPT_ALL()
and software filtering of received IDs is a viable approach for beginners. - Interpreting OBD-II PID responses requires understanding the specific data byte encoding and conversion formulas for each PID.
- The ESP32-C3 lacks an integrated TWAI/CAN controller and requires an external CAN IC (e.g., MCP2515 via SPI) for OBD-II applications.
- Handling multi-frame messages (ISO-TP) is necessary for some OBD-II data like VIN or extensive DTC lists, adding complexity beyond single-frame PID requests.
Further Reading
- ESP-IDF TWAI Programming Guide: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/twai.html
- SAE J1979 Standard: “E/E Diagnostic Test Modes” – The definitive standard for OBD-II services and PIDs. (Typically requires purchase or library access).
- ISO 15765-4:2016: “Road vehicles — Diagnostic communication over Controller Area Network (DoCAN) — Part 4: Requirements for emissions-related systems.” (Definitive standard for OBD-II over CAN, typically requires purchase).
- ISO 15765-2:2016: “Road vehicles — Diagnostic communication over Controller Area Network (DoCAN) — Part 2: Transport protocol and network layer services.” (For multi-frame message handling).
- Wikipedia – OBD-II PIDs: https://en.wikipedia.org/wiki/OBD-II_PIDs (A good, free resource for a general list of PIDs and their interpretations).
- Community Forums and OBD-II Simulators: Websites like “SparkFun” or “Adafruit” often have tutorials or projects related to OBD-II, and various OBD-II simulator projects (hardware or software) can be found online for safer development.