diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce66cbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +sdkconfig +sdkconfig.old diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d592e76 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.16) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(esp32-aldl) diff --git a/README.md b/README.md index 594a464..96eea18 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,36 @@ # ESP32 ALDL Wireless Bridge -**ESP32 firmware for reading GM 160-baud PWM ALDL data from a 1986–1988 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 1986–1988 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 (1986–1988 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: \ No newline at end of file +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`. \ No newline at end of file diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..8a9d914 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "main.c" + INCLUDE_DIRS ".") diff --git a/esp32-aldl.ino b/main/main.c similarity index 55% rename from esp32-aldl.ino rename to main/main.c index fa9fdff..c128057 100644 --- a/esp32-aldl.ino +++ b/main/main.c @@ -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 -#include +#include +#include +#include +#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); } diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..344a13f --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,4 @@ +CONFIG_BT_ENABLED=y +CONFIG_BT_CLASSIC_ENABLED=y +CONFIG_BT_SPP_ENABLED=y +CONFIG_FREERTOS_HZ=1000