refactor: migrate firmware from Arduino to native ESP-IDF implementation with CMake configuration

This commit is contained in:
2026-06-12 13:48:35 +01:00
parent 619ec20ca1
commit f5ec189dc4
6 changed files with 254 additions and 94 deletions
+3
View File
@@ -0,0 +1,3 @@
build/
sdkconfig
sdkconfig.old
+3
View File
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp32-aldl)
+95 -21
View File
@@ -1,20 +1,36 @@
# ESP32 ALDL Wireless Bridge
**ESP32 firmware for reading GM 160-baud PWM ALDL data from a 19861988 Pontiac Fiero 2.8L V6 (1227170 ECM) and streaming it wirelessly over Bluetooth SPP.**
**ESP32 native C firmware using ESP-IDF for reading GM 160-baud PWM ALDL data from a 19861988 Pontiac Fiero 2.8L V6 (1227170 ECM) and streaming it wirelessly over Bluetooth SPP.**
This project turns an ESP32 into a reliable, low-latency wireless ALDL interface. It decodes the raw 160-baud PWM signal in real time and forwards clean 25-byte data frames over Bluetooth to the companion Android app ([esp32-aldl-android](https://git.i3omb.com/gronod/esp32-aldl-android)).
It is implemented as a native ESP-IDF application in C, removing the overhead of the Arduino framework.
---
## Features
- **Real-time 160-baud PWM ALDL decoding** — Custom bit-banged decoder running in a high-priority FreeRTOS task
- **Native C ESP-IDF Implementation** — Built directly on ESP-IDF v5.2+ for maximum performance and low memory footprint
- **Real-time 160-baud PWM ALDL decoding** — Custom bit-banged decoder running in a high-priority FreeRTOS task (`aldlDecodeTask`)
- **Robust pulse handling** — Handles glitches, merged pulses, and idle gaps gracefully
- **Hard frame synchronization** — Prepends `0xAA 0x55` header for reliable packet alignment on the receiving side
- **Bluetooth SPP (Serial Port Profile)** — Appears as a classic Bluetooth serial device named `ESP32-ALDL`
- **Decoupled architecture** — ISR Ring buffer Decoder task Bluetooth transmit queue
- **Status reporting** — Periodic status output over USB serial
- **Low CPU usage** on the decoder core while maintaining timing accuracy
- **Bluedroid Bluetooth Classic SPP** — Connects directly via Serial Port Profile (SPP) using "Just Works" Secure Simple Pairing (SSP) configuration
- **Decoupled RTOS Architecture** — ISR -> Ring Buffer -> Decoder task -> Bluetooth transmit queue -> BT TX task
- **Periodic status reporting** — Diagnostics reported over ESP-IDF log outputs (`ESP_LOGI`)
---
## Project Structure
```
esp32-aldl/
├── CMakeLists.txt # Top-level CMake build configuration
├── sdkconfig.defaults # Kconfig options (enabling BT, SPP, HZ=1000)
├── README.md # Project documentation
└── main/
├── CMakeLists.txt # Main component build script
└── main.c # C application logic (decoder and Bluetooth SPP)
```
---
@@ -22,34 +38,92 @@ This project turns an ESP32 into a reliable, low-latency wireless ALDL interface
- **ECM**: GM 1227170 (19861988 Pontiac Fiero 2.8L V6)
- **ALDL Mode**: `$24` / `$24A` mask (25-byte continuous broadcast frames)
- **Microcontroller**: ESP32 (any dev board with sufficient GPIO — tested on ESP32-WROOM-32 and ESP32-S3)
- **Bluetooth**: Classic Bluetooth (BR/EDR) — works with most Android devices
- **Microcontroller**: ESP32 (Classic ESP32 with Classic Bluetooth capability)
- **Bluetooth**: Classic Bluetooth (BR/EDR)
---
## Pin Connections
| Signal | ESP32 Pin | Notes |
|--------------|---------------|--------------------------------------------|
| ALDL Data In | **GPIO 4** | Connect to ALDL pin **M** (data line) |
| GND | GND | Must share ground with the car/ECU |
| Signal | ESP32 Pin | Notes |
|--------------|---------------|-----------------------------------------------|
| ALDL Data In | **GPIO 4** | Connect to ALDL pin **M** (data line) |
| GND | GND | Must share ground with the car/ECU |
| 5V / 3.3V | — | **Do not** power the ESP32 from the ALDL line |
> **Important**: The ALDL line is a 5V PWM signal. The ESP32 GPIO is 3.3V tolerant for input in most cases, but a simple voltage divider or level shifter (e.g., 1kΩ + 2kΩ) is recommended for long-term reliability.
> [!WARNING]
> The ALDL line is a 5V PWM signal. Since ESP32 GPIO pins are not 5V-tolerant, a simple voltage divider (e.g., 1kΩ + 2kΩ) or a bidirectional level shifter is required to step down the signal to 3.3V for long-term hardware reliability.
---
## Prerequisites & Installation
To build and compile the firmware, you need the official **ESP-IDF v5.2.1** toolchain installed.
1. Clone ESP-IDF (v5.2.1 stable release):
```bash
mkdir -p ~/esp
git clone -b v5.2.1 --recursive https://github.com/espressif/esp-idf.git ~/esp/esp-idf
```
2. Run the toolchain installation:
```bash
cd ~/esp/esp-idf
./install.sh esp32
```
3. Initialize the environment:
```bash
source ~/esp/esp-idf/export.sh
```
4. *(Optional)* If `cmake` or `ninja` are missing in your environment, install them inside the active Python virtual environment:
```bash
pip install cmake ninja
```
---
## Building and Flashing
Once the toolchain environment is loaded, execute the following commands in the `esp32-aldl` root workspace folder:
### 1. Set Build Target
```bash
idf.py set-target esp32
```
This command generates the build directories and loads settings from `sdkconfig.defaults` (enabling Bluetooth SPP and setting the FreeRTOS clock frequency to 1000 Hz).
### 2. Compile Project
```bash
idf.py build
```
This builds the bootloader, partition table, ESP-IDF drivers, Bluetooth stack, and the main application code, producing `build/esp32-aldl.bin`.
### 3. Flash & Monitor
Flash the firmware to the ESP32 and open the terminal console to view serial output (replace `/dev/ttyUSB0` with your target serial port):
```bash
idf.py -p /dev/ttyUSB0 flash monitor
```
To exit the serial monitor, press `Ctrl + ]`.
---
## How It Works
### Signal Decoding
The firmware uses an interrupt service routine (ISR) triggered on any edge of GPIO 4. Pulse widths are measured using the ESP32 high-resolution timer and pushed into a lock-free ring buffer.
The firmware registers an Interrupt Service Routine (ISR) triggered on any edge (both rising and falling) of **GPIO 4**. Pulse widths are calculated in microseconds using `esp_timer_get_time()` and pushed to a volatile ring buffer.
A dedicated decoder task (`aldlDecodeTask`) processes the pulse stream:
- Classifies pulses as `0`, `1`, glitch, merged pulse, or idle gap
- Reconstructs bytes using the standard 160-baud ALDL PWM encoding
- Assembles complete 25-byte frames
- Validates and enqueues valid frames
The high-priority `aldlDecodeTask` pops pulses from the ring buffer:
- Glitches under `300us` are discarded.
- Idle gaps over `13500us` reset the decoder.
- Pulses are classified into Logical `0` (approx. `1.11ms`), Logical `1` (approx. `4.16ms`), or Merged pulses.
- Decoded bits are reconstructed into 25-byte frames.
### Bluetooth Transmission
A separate task (`btTransmitTask`) pulls decoded frames from a FreeRTOS queue and transmits them over Bluetooth SPP with a 2-byte hard sync header:
When a 25-byte frame is fully received, it is pushed to `bt_queue`.
The `btTransmitTask` waits on the queue:
- Prepend the 2-byte hard-sync header (`0xAA 0x55`).
- Sends the packet (`27 bytes` total) over the Bluetooth Classic Serial Port Profile connection via the `esp_spp_write()` API.
- The device advertises itself under the classic Bluetooth name `ESP32-ALDL`.
+2
View File
@@ -0,0 +1,2 @@
idf_component_register(SRCS "main.c"
INCLUDE_DIRS ".")
+147 -73
View File
@@ -1,16 +1,18 @@
/**
* =============================================================================
* ESP32 ALDL Wireless Bridge GM 1227170 / Fiero 2.8L V6
* 160-baud PWM ALDL decoder + BluetoothSerial bridge
*
* REVISION 7 0xAA 0x55 Hard Sync Header
* =============================================================================
*/
#include <Arduino.h>
#include <BluetoothSerial.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/gpio.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"
#include "esp_spp_api.h"
#define ALDL_PIN GPIO_NUM_4
@@ -26,7 +28,6 @@
#define BT_DEVICE_NAME "ESP32-ALDL"
#define BT_QUEUE_DEPTH 4u
#define DEBUG_LEVEL 2
#define PC_GLITCH ((uint8_t)0)
#define PC_LOGIC_0 ((uint8_t)1)
@@ -38,6 +39,8 @@
#define DS_AWAIT_START ((uint8_t)1)
#define DS_READ_BITS ((uint8_t)2)
static const char *TAG = "ALDL";
struct BtFrame {
uint8_t data[PAYLOAD_BYTES];
uint8_t len;
@@ -62,10 +65,12 @@ struct RingBuffer {
volatile uint16_t tail;
};
static RingBuffer rb;
static DecoderContext ctx;
static BluetoothSerial SerialBT;
static QueueHandle_t bt_queue = nullptr;
static struct RingBuffer rb;
static struct DecoderContext ctx;
static QueueHandle_t bt_queue = NULL;
static uint32_t spp_handle = 0;
static bool bt_connected = false;
#define RB_MASK ((uint16_t)255u)
@@ -77,21 +82,17 @@ static inline void IRAM_ATTR rb_push(uint32_t v) {
rb.head = next;
}
static inline bool rb_pop(uint32_t &out) {
static inline bool rb_pop(uint32_t *out) {
if (rb.tail == rb.head) return false;
out = rb.data[rb.tail];
*out = rb.data[rb.tail];
__asm__ __volatile__("" ::: "memory");
rb.tail = (rb.tail + 1u) & RB_MASK;
return true;
}
static inline uint16_t rb_available() {
return (rb.head - rb.tail) & RB_MASK;
}
static volatile uint64_t isr_fall_us = 0;
static void IRAM_ATTR aldl_gpio_isr(void* /*arg*/) {
static void IRAM_ATTR aldl_gpio_isr(void* arg) {
uint64_t now = (uint64_t)esp_timer_get_time();
if (gpio_get_level((gpio_num_t)ALDL_PIN) == 0) {
isr_fall_us = now;
@@ -111,7 +112,7 @@ static uint8_t classify_pulse(uint32_t us) {
return PC_LOGIC_1;
}
static void reset_decoder() {
static void reset_decoder(void) {
ctx.state = DS_HUNT_SYNC;
ctx.sync_count = 0;
ctx.bit_count = 0;
@@ -121,25 +122,22 @@ static void reset_decoder() {
ctx.bytes_this_frame = 0;
}
static void enqueue_frame() {
BtFrame f;
static void enqueue_frame(void) {
struct BtFrame f;
memcpy(f.data, ctx.frame, PAYLOAD_BYTES);
f.len = PAYLOAD_BYTES;
if (xQueueSend(bt_queue, &f, 0) != pdTRUE) {
if (DEBUG_LEVEL >= 1) Serial.println(F("[WARN] BT queue full"));
ESP_LOGW(TAG, "BT queue full");
}
}
static void print_frame() {
Serial.print(F("[FRAME #"));
Serial.print(ctx.frames_decoded);
Serial.print(F("] "));
static void print_frame(void) {
char hex_str[ PAYLOAD_BYTES * 3 + 1 ];
int offset = 0;
for (uint8_t i = 0; i < PAYLOAD_BYTES; i++) {
if (ctx.frame[i] < 0x10) Serial.print('0');
Serial.print(ctx.frame[i], HEX);
if (i < PAYLOAD_BYTES - 1) Serial.print(' ');
offset += sprintf(hex_str + offset, "%02X ", ctx.frame[i]);
}
Serial.println();
ESP_LOGI(TAG, "[FRAME #%lu] %s", ctx.frames_decoded, hex_str);
}
static void feed_bit(uint8_t pc) {
@@ -188,7 +186,7 @@ static void feed_bit(uint8_t pc) {
if (ctx.byte_count >= PAYLOAD_BYTES) {
ctx.frames_decoded++;
if (DEBUG_LEVEL >= 1) print_frame();
print_frame();
enqueue_frame();
reset_decoder();
} else {
@@ -224,12 +222,8 @@ static void process_pulse(uint32_t pulse_us) {
feed_bit(pc);
}
// ---------------------------------------------------------------------------
// ── BT transmit task (Core 0) ────────────────────────────────────────────────
// ---------------------------------------------------------------------------
static void btTransmitTask(void* /*pvParameters*/) {
BtFrame f;
static void btTransmitTask(void* pvParameters) {
struct BtFrame f;
// The 2-byte hard-sync header that ALDLDroid will lock onto
uint8_t tx_buffer[PAYLOAD_BYTES + 2];
@@ -238,21 +232,21 @@ static void btTransmitTask(void* /*pvParameters*/) {
for (;;) {
if (xQueueReceive(bt_queue, &f, portMAX_DELAY) == pdTRUE) {
if (SerialBT.connected()) {
if (bt_connected && spp_handle != 0) {
// Copy the 25 decoded bytes immediately after the header
memcpy(&tx_buffer[2], f.data, f.len);
// Transmit the 27-byte locked packet
SerialBT.write(tx_buffer, f.len + 2);
esp_spp_write(spp_handle, f.len + 2, tx_buffer);
}
}
}
}
static void aldlDecodeTask(void* /*pvParameters*/) {
static void aldlDecodeTask(void* pvParameters) {
uint32_t pulse_us = 0;
for (;;) {
bool did_work = false;
while (rb_pop(pulse_us)) {
while (rb_pop(&pulse_us)) {
process_pulse(pulse_us);
did_work = true;
}
@@ -260,24 +254,114 @@ static void aldlDecodeTask(void* /*pvParameters*/) {
}
}
static void statusTask(void* /*pvParameters*/) {
static void statusTask(void* pvParameters) {
for (;;) {
vTaskDelay(pdMS_TO_TICKS(5000));
Serial.print(F("[STATUS] frames="));
Serial.print(ctx.frames_decoded);
Serial.print(F(" bt="));
Serial.println(SerialBT.connected() ? F("UP") : F("waiting"));
ESP_LOGI(TAG, "[STATUS] frames=%lu bt=%s",
ctx.frames_decoded,
bt_connected ? "UP" : "waiting");
}
}
void setup() {
Serial.begin(115200);
delay(500);
static void esp_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) {
switch (event) {
case ESP_SPP_INIT_EVT:
ESP_LOGI(TAG, "ESP_SPP_INIT_EVT");
esp_spp_start_srv(ESP_SPP_SEC_NONE, ESP_SPP_ROLE_SLAVE, 0, "SPP_SERVER");
break;
case ESP_SPP_DISCOVERY_COMP_EVT:
ESP_LOGI(TAG, "ESP_SPP_DISCOVERY_COMP_EVT");
break;
case ESP_SPP_OPEN_EVT:
ESP_LOGI(TAG, "ESP_SPP_OPEN_EVT");
break;
case ESP_SPP_CLOSE_EVT:
ESP_LOGI(TAG, "ESP_SPP_CLOSE_EVT");
spp_handle = 0;
bt_connected = false;
break;
case ESP_SPP_START_EVT:
ESP_LOGI(TAG, "ESP_SPP_START_EVT");
esp_bt_dev_set_device_name(BT_DEVICE_NAME);
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
break;
case ESP_SPP_CL_INIT_EVT:
ESP_LOGI(TAG, "ESP_SPP_CL_INIT_EVT");
break;
case ESP_SPP_DATA_IND_EVT:
ESP_LOGI(TAG, "ESP_SPP_DATA_IND_EVT len=%d, handle=%lu",
param->data_ind.len, (unsigned long)param->data_ind.handle);
break;
case ESP_SPP_CONG_EVT:
ESP_LOGI(TAG, "ESP_SPP_CONG_EVT");
break;
case ESP_SPP_WRITE_EVT:
break;
case ESP_SPP_SRV_OPEN_EVT:
ESP_LOGI(TAG, "ESP_SPP_SRV_OPEN_EVT");
spp_handle = param->srv_open.handle;
bt_connected = true;
break;
case ESP_SPP_SRV_STOP_EVT:
ESP_LOGI(TAG, "ESP_SPP_SRV_STOP_EVT");
break;
Serial.println(F("============================================"));
Serial.println(F(" ESP32 ALDL Bridge — GM 1227170 Fiero 2.8 "));
Serial.println(F(" 160-baud PWM — AA55 Hard Sync Active "));
Serial.println(F("============================================"));
default:
break;
}
}
void app_main(void) {
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
if ((ret = esp_bt_controller_init(&bt_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "%s initialize controller failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
if ((ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT)) != ESP_OK) {
ESP_LOGE(TAG, "%s enable controller failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
esp_bluedroid_config_t bluedroid_cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT();
if ((ret = esp_bluedroid_init_with_cfg(&bluedroid_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "%s initialize bluedroid failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
if ((ret = esp_bluedroid_enable()) != ESP_OK) {
ESP_LOGE(TAG, "%s enable bluedroid failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
if ((ret = esp_spp_register_callback(esp_spp_cb)) != ESP_OK) {
ESP_LOGE(TAG, "%s spp register failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
esp_spp_cfg_t spp_cfg = {
.mode = ESP_SPP_MODE_CB,
.enable_l2cap_ertm = true,
.tx_buffer_size = 0,
};
if ((ret = esp_spp_enhanced_init(&spp_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "%s spp init failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
ESP_LOGI(TAG, "============================================");
ESP_LOGI(TAG, " ESP32 ALDL Bridge — GM 1227170 Fiero 2.8 ");
ESP_LOGI(TAG, " 160-baud PWM — AA55 Hard Sync Active ");
ESP_LOGI(TAG, "============================================");
memset(&rb, 0, sizeof(rb));
memset(&ctx, 0, sizeof(ctx));
@@ -292,21 +376,11 @@ void setup() {
gpio_config(&io);
gpio_install_isr_service(ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3);
gpio_isr_handler_add((gpio_num_t)ALDL_PIN, aldl_gpio_isr, nullptr);
if (!SerialBT.begin(BT_DEVICE_NAME)) {
for (;;) delay(1000);
}
Serial.print(F("[BT] Advertising as: "));
Serial.println(F(BT_DEVICE_NAME));
gpio_isr_handler_add((gpio_num_t)ALDL_PIN, aldl_gpio_isr, NULL);
bt_queue = xQueueCreate(BT_QUEUE_DEPTH, sizeof(BtFrame));
bt_queue = xQueueCreate(BT_QUEUE_DEPTH, sizeof(struct BtFrame));
xTaskCreatePinnedToCore(aldlDecodeTask, "aldlDecode", 4096, nullptr, 3, nullptr, 0);
xTaskCreatePinnedToCore(btTransmitTask, "btTx", 4096, nullptr, 2, nullptr, 0);
xTaskCreatePinnedToCore(statusTask, "status", 2048, nullptr, 1, nullptr, 0);
}
void loop() {
vTaskDelay(pdMS_TO_TICKS(100));
xTaskCreatePinnedToCore(aldlDecodeTask, "aldlDecode", 4096, NULL, 3, NULL, 0);
xTaskCreatePinnedToCore(btTransmitTask, "btTx", 4096, NULL, 2, NULL, 0);
xTaskCreatePinnedToCore(statusTask, "status", 2048, NULL, 1, NULL, 0);
}
+4
View File
@@ -0,0 +1,4 @@
CONFIG_BT_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=y
CONFIG_BT_SPP_ENABLED=y
CONFIG_FREERTOS_HZ=1000