multi-functional-controller/docs/PROJECT_HANDOFF.md

8.8 KiB

Project Handoff: Multi-Functional Controller (MFC) & RC Vehicle

1. Project Overview & Aim

Goal: Engineer a high-performance, bare-metal remote controller (MFC) from scratch using an ESP32-S3, paired with a custom 4WD RC vehicle. Secondary Objectives: Extend the controller's capabilities to include a cloud-connected AI voice assistant, local telemetry logging, and PC gamepad (USB HID/Bluetooth) support. Tech Stack: C/C++, ESP-IDF v5.x (FreeRTOS), Bare-metal register manipulation, SPI, I2C, Hardware PCNT, ADC Oneshot.

2. Current Situation

  • Controller Prototype: Currently prototyped on a 6x9cm perfboard. The ESP32-S3 successfully communicates over a shared SPI bus with an NRF24L01+ transceiver and a temporary 1.3" OLED display.
  • Firmware: A robust FreeRTOS state machine is running. It handles analog ADC joystick polling, hardware quadrature decoding (PCNT) for a rotary encoder menu UI, software button debouncing, and blind RF transmission on 2.4GHz.
  • Hardware Pivot: We recently discovered the 7-pin 1.3" OLED lacks a Chip Select (CS) pin, causing SPI collisions. We have decided to upgrade to a 1.8" ST7789 color TFT (which has a CS pin) to share the SPI bus safely.
  • Manufacturing: Paused cutting the rigid ABS plastic project box (DT-100) until next month when a proper rotary tool (Dremel/E108) and 2.35mm cutting wheels arrive to ensure clean, professional cutouts.

3. Hardware Architecture & Bill of Materials (BOM)

3.1 Handheld Controller (MFC)

  • MCU: Espressif ESP32-S3-N16R8 (16MB Flash, 8MB PSRAM).
  • RF Telemetry: Nordic nRF24L01+ 2.4GHz Module.
  • Display: 1.8" ST7789 TFT Color LCD (3.3V logic, SPI with CS pin).
  • Joysticks: 2x K-Silver (KS) PS5 Magnetic Hall-Effect modules (3.3V active sensors).
  • UI Input: KY-040 Rotary Encoder (CLK, DT, SW).
  • Voice AI: MAX4466 Electret Microphone Module (Analog output).
  • Data Logging: Arduino Micro SD Card Module (SPI interface, formatted FAT32).
  • Audio Output: 1x Passive Buzzer (Melody), 2x Active Buzzers (Percussive clicks) driven by 2N2222 NPN Transistors.
  • Power: Dual 18650 Li-ion cells, TP4056 Charger, MT3608 DC-DC Boost Converter (5V rail), 100µF Electrolytic Capacitor (for NRF24 burst smoothing), 100nF Ceramic Caps.

3.2 RC Vehicle (Receiver Node)

  • MCU: GD32F103C8T6 ARM Cortex-M3 Mini Board.
  • RF Telemetry: nRF24L01+ PA+LNA (with external SMA antenna for 1km range).
  • IMU / Navigation: MPU9250 9-DOF Sensor (Gyro, Accelerometer, Magnetometer for tilt-compensated heading).
  • Drivetrain: 4x JGB37-520 12V 333RPM Gear Motors (with built-in encoders for PID velocity control).
  • Chassis/Enclosure: DT-100 Project Box (190x137x59mm).
  • Power: 12V Battery, Heavy-duty Motor Drivers, 12-24V Waterproof Main Switch.

4. Pinout Definitions (ESP32-S3 Controller)

  • SPI Hub: MOSI (GPIO11), MISO (GPIO13), SCK (GPIO12)
  • NRF24: CSN (GPIO14), CE (GPIO21)
  • TFT Display (Planned): CS (GPIO10), DC (GPIO9), RES (GPIO8), BLK (3.3V)
  • Rotary Encoder: CLK (GPIO15), DT (GPIO16), SW (GPIO17 - Pull-up)
  • Joysticks: VRx (GPIO4), VRy (GPIO5) via ADC1. (Second joystick pins pending).

5. Master Codebase (Latest Working Version)

This code currently uses the monochrome OLED logic, but handles PCNT menu decoding and NRF transmission perfectly.

#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "driver/pulse_cnt.h"
#include "esp_system.h"
#include "esp_log.h"

// Hardware Pins
#define PIN_NUM_MOSI    11
#define PIN_NUM_MISO    13
#define PIN_NUM_CLK     12
#define NRF_PIN_CS      14
#define NRF_PIN_CE      21
#define OLED_PIN_CS     10
#define OLED_PIN_DC     9
#define OLED_PIN_RES    8
#define PIN_ENCODER_CLK 15
#define PIN_ENCODER_DT  16
#define PIN_ENCODER_SW  17

#define NUM_MENU_ITEMS  4

static const char *TAG = "MFC_CORE";
spi_device_handle_t oled_spi;
spi_device_handle_t nrf_spi;
int current_radio_channel = 76;

typedef enum { SCREEN_MAIN_MENU = 0, SCREEN_DRIVE, SCREEN_RADIO_CONFIG, SCREEN_CALIBRATE, SCREEN_SYSTEM_INFO } screen_state_t;

// [Font arrays omitted in handoff for brevity, but exist in master file: font_AZ, font_nums, etc.]
const uint8_t* get_font_data(char c) { /* ... */ return NULL; }

void oled_cmd(uint8_t cmd) {
    gpio_set_level(OLED_PIN_DC, 0);
    spi_transaction_t t = { .length = 8, .tx_buffer = &cmd };
    spi_device_polling_transmit(oled_spi, &t); 
}
void oled_data(uint8_t data) {
    gpio_set_level(OLED_PIN_DC, 1);
    spi_transaction_t t = { .length = 8, .tx_buffer = &data };
    spi_device_polling_transmit(oled_spi, &t);
}
void oled_draw_string(uint8_t page, uint8_t col, const char* str) {
    oled_cmd(0xB0 + page); 
    uint8_t phys_col = col + 2; 
    oled_cmd(0x00 | (phys_col & 0x0F)); 
    oled_cmd(0x10 | ((phys_col >> 4) & 0x0F));
    while (*str) {
        const uint8_t* pixels = get_font_data(*str++);
        for (int i = 0; i < 5; i++) { oled_data(pixels[i]); }
        oled_data(0x00); 
    }
}
void oled_clear() {
    for (uint8_t p = 0; p < 8; p++) {
        oled_cmd(0xB0 + p); oled_cmd(0x02); oled_cmd(0x10);
        for (uint8_t c = 0; c < 128; c++) { oled_data(0x00); }
    }
}
void oled_init() {
    gpio_set_level(OLED_PIN_RES, 0); vTaskDelay(pdMS_TO_TICKS(50));
    gpio_set_level(OLED_PIN_RES, 1); vTaskDelay(pdMS_TO_TICKS(50));
    uint8_t init_cmds[] = {0xAE,0xD5,0x80,0xA8,0x3F,0xD3,0x00,0x40,0x8D,0x14,0xA1,0xC8,0xDA,0x12,0x81,0xCF,0xD9,0xF1,0xDB,0x40,0xA4,0xA6,0xAF};
    for(int i = 0; i < sizeof(init_cmds); i++) { oled_cmd(init_cmds[i]); }
}

bool nrf_check_connection() {
    uint8_t tx[2] = { 0x03, 0xFF }; 
    uint8_t rx[2] = { 0 };
    spi_transaction_t t = { .length = 16, .tx_buffer = tx, .rx_buffer = rx };
    spi_device_polling_transmit(nrf_spi, &t);
    return (rx[1] == 0x03);
}
void set_nrf_channel(uint8_t channel) {
    uint8_t tx[2] = { 0x25, channel }; 
    spi_transaction_t t = { .length = 16, .tx_buffer = tx };
    spi_device_polling_transmit(nrf_spi, &t);
}

void draw_menu(int selected_index) { /* Renders Main Menu items */ }
void draw_system_info() { /* Renders ESP32 Free Heap */ }
void draw_radio_config_screen(int channel) { /* Interactive channel tuning UI */ }
void draw_placeholder_screen(const char* title) { /* Basic placeholder */ }

void app_main(void) {
    // 1. SPI Init
    spi_bus_config_t buscfg = { .miso_io_num = PIN_NUM_MISO, .mosi_io_num = PIN_NUM_MOSI, .sclk_io_num = PIN_NUM_CLK, .max_transfer_sz = 4096 };
    spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);

    // 2. Devices (OLED & NRF)
    spi_device_interface_config_t oled_cfg = { .clock_speed_hz = 10000000, .mode = 0, .spics_io_num = OLED_PIN_CS, .queue_size = 1 };
    spi_bus_add_device(SPI2_HOST, &oled_cfg, &oled_spi);
    
    spi_device_interface_config_t nrf_cfg = { .clock_speed_hz = 1000000, .mode = 0, .spics_io_num = NRF_PIN_CS, .queue_size = 1 };
    spi_bus_add_device(SPI2_HOST, &nrf_cfg, &nrf_spi);

    // 3. PCNT Setup for Rotary Encoder
    // [PCNT Init logic omitted for brevity in handoff, but fully functional in master]

    // 4. FreeRTOS State Machine Loop
    int count = 0, last_menu_index = -1, last_sw_state = 1;
    TickType_t last_button_press = 0;
    screen_state_t current_screen = SCREEN_MAIN_MENU;
    bool screen_needs_redraw = true;
    int radio_start_count = 0, last_tuned_channel = current_radio_channel;

    while (1) {
        // Core polling loop handling screen state, PCNT steps, debouncing, and NRF register updating.
        vTaskDelay(pdMS_TO_TICKS(30)); 
    }
}

6. Immediate Next Steps (For the Assisting Agent)

  1. ST7789 TFT Migration: Rewrite the display initialization and frame buffer logic using ESP-IDF's esp_lcd component to replace the custom 5x8 monochrome OLED code.
  2. SD Card SPI Setup: Configure the SD card module on the shared SPI bus (or a new SPI bus) and initialize the FATFS VFS so we can read/write data logs.
  3. NVS Calibration: Write the calibration routine for the upcoming K-Silver Hall-effect joysticks. We need to measure resting, min, and max voltages and save them permanently to the ESP32's flash memory.
  4. GD32 Vehicle Receiver Code: Begin structuring the C code for the ARM Cortex-M3 GD32 to poll the NRF24 PA+LNA module and read the MPU9250 I2C data.

7. Known Context & Hardware Rules

  • Do NOT use standard 16000 RPM tools on the plastic case; we are waiting for a 3-speed drill with 2.35mm diamond cutting discs.
  • Do NOT wire the Hall-effect joystick VCC backward; it is an active component and will burn out.
  • Do NOT wire the HC-SR04 ECHO pin directly to the GD32; it requires a resistor voltage divider. (Though we are considering the VL53L0X I2C ToF instead).
  • Must use a 100µF capacitor on the NRF24 power lines to prevent brown-outs during PTX bursts.