PX4-Autopilot/CLAUDE.MD

12 KiB

AM32 ESC Flasher Module - Implementation Plan

Context

PX4 has no upstream support for AM32 ESC firmware updates. Matt's branch (jake/matt_firefly_am32_flasher) proves the concept works but has architectural issues: firmware baked into the build as a C array, ~100-state machine with poor error recovery, tight coupling to DShot via a 4-message uORB handshake, no GCS progress reporting, and ~826 lines of flashing logic injected into the DShot driver.

Goal: Clean, upstream-quality esc_flasher module that reads firmware from SD card, is triggered via MAVLink command from QGC, reports per-ESC progress via a custom MAVLink message, and requires a reboot to resume normal DShot operation. Structured to allow future USB passthrough (4-way interface) extension.

Architecture Overview

QGC PX4 │ │ ├─COMMAND_LONG──────────────►│ vehicle_command uORB │ MAV_CMD_ESC_FW_UPDATE │ │ │ │ esc_flasher module │ │ (dormant at boot, wakes on cmd) │ │ │ │ │ 1. Validate (disarmed, files exist) │ │ 2. ACK command │ │ 3. Stop DShot module │ │ 4. Take over motor GPIOs │ │ 5. For each ESC: │ │ - Bootloader handshake │ │ - Read device info → pick FW file │◄─ESC_FW_UPDATE_STATUS──────│ - Flash 256-byte chunks │ (per-ESC progress) │ - Send RUN_APP │ │ 6. Publish REBOOT_REQUIRED │◄─COMMAND_ACK (ACCEPTED)────│ 7. Go idle (reboot needed)

New Files

  1. src/modules/esc_flasher/EscFlasher.hpp

Module class inheriting ModuleBase + ModuleParams + ScheduledWorkItem.

Key members:

  • _vehicle_command_sub — polls for MAV_CMD trigger
  • _esc_fw_update_status_pub — publishes per-ESC progress
  • _command_ack_pub — ACKs the initial command
  • State enum: Idle, StoppingDShot, InitGPIOs, ConnectingESC, ReadingInfo, Flashing, RunApp, Complete, Failed
  • Per-ESC state: _current_esc_index, _esc_count, _bytes_written, _firmware_size
  • GPIO pin array: uint32_t _gpio_pins[MAX_MOTORS]
  • File handle for current firmware
  1. src/modules/esc_flasher/EscFlasher.cpp

Main module implementation (~600-800 lines, much simpler than Matt's ~1,900).

Module lifecycle:

  • Auto-started at boot on supported boards (Kconfig opt-in)
  • Runs at 1Hz when idle (vehicle_command poll only — negligible overhead)
  • Transitions to 100Hz active polling during flashing
  • Returns to idle after completion (reboot required to restore DShot)

DShot shutdown:

  • Calls system("dshot stop") via NuttX shell — DShot destructor calls up_dshot_arm(false)
  • Waits up to 2s for DShot to exit, then proceeds
  • Simple, no coupling — DShot module has no knowledge of the flasher

GPIO takeover:

  • After DShot stops, configures motor pins as GPIO outputs via px4_arch_configgpio(io_timer_channel_get_gpio_output(ch))
  • Drives all pins HIGH for 600ms to force ESCs into bootloader mode on reboot
  • ESC power cycle not required — AM32 bootloader detects signal-high at startup

Flashing loop (per ESC):

  1. Select ESC (drive target pin, keep others HIGH)
  2. Send bootloader handshake → get 9-byte device info
  3. Read firmware tag (CMD_READ_FLASH at tag address) → extract hardware target string
  4. Open /fs/microsd/am32/.bin (e.g., AM32_G071_48K.bin)
  5. For each 256-byte chunk:
  • CMD_SET_ADDRESS
  • CMD_SET_BUFFER + data + CRC
  • CMD_PROG_FLASH → wait ACK
  • Publish progress (chunk_index / total_chunks * 100)
  1. Write firmware tag section

  2. CMD_RUN → ESC reboots to application

  3. Move to next ESC

  4. src/modules/esc_flasher/am32_protocol.hpp / .cpp

Extracted and cleaned AM32 bootloader protocol implementation.

Reused from Matt's code (protocol is correct):

  • crc16_ccitt() — CRC-16 with polynomial 0xA001
  • bitbang_send_packet() — bit-banged UART TX/RX at 19200 baud
    • 52µs bit time, 26µs half-bit for sampling
    • px4_enter_critical_section() for timing accuracy
    • hrt_absolute_time() busy-wait loops
  • Bootloader handshake packet: {0x00 * 8, 0x0D, 'B','L','H','e','l','i', CRC}
  • Command encoding: SET_ADDRESS(0xFF), SET_BUFFER(0xFE), PROG_FLASH(0x01), RUN(0x00), READ_FLASH(0x03)

Cleaned up vs Matt's code:

  • Function-based API instead of inline state machine code
  • Proper error return codes instead of magic numbers
  • Configurable timeouts instead of hardcoded values
  • Separated from module state machine logic

Public API: namespace am32 { int handshake(uint32_t gpio, uint8_t device_info[9]); int set_address(uint32_t gpio, uint16_t address); int write_chunk(uint32_t gpio, const uint8_t *data, uint16_t len); int read_flash(uint32_t gpio, uint16_t address, uint8_t *buf, uint16_t len); int run_app(uint32_t gpio); uint16_t crc16(const uint8_t *buf, uint16_t len); }

  1. src/modules/esc_flasher/CMakeLists.txt

px4_add_module( MODULE modules__esc_flasher MAIN esc_flasher SRCS EscFlasher.cpp am32_protocol.cpp DEPENDS px4_work_queue )

  1. src/modules/esc_flasher/Kconfig

menuconfig MODULES_ESC_FLASHER bool "esc_flasher" default n ---help--- AM32 ESC firmware update module

  1. msg/EscFirmwareUpdateStatus.msg

uint64 timestamp

uint8 esc_index # Current ESC being flashed (0-based) uint8 esc_count # Total ESCs being flashed uint8 progress_pct # 0-100 for current ESC uint8 overall_progress_pct # 0-100 for entire operation

uint8 status uint8 STATUS_IDLE = 0 uint8 STATUS_CONNECTING = 1 uint8 STATUS_READING_INFO = 2 uint8 STATUS_ERASING = 3 uint8 STATUS_WRITING = 4 uint8 STATUS_COMPLETE = 5 uint8 STATUS_FAILED = 6 uint8 STATUS_REBOOT_REQUIRED = 7

uint8 error uint8 ERROR_NONE = 0 uint8 ERROR_NOT_DISARMED = 1 uint8 ERROR_NO_FIRMWARE_FILE = 2 uint8 ERROR_COMMS_TIMEOUT = 3 uint8 ERROR_CRC_MISMATCH = 4 uint8 ERROR_VERIFICATION_FAILED = 5 uint8 ERROR_UNSUPPORTED_HARDWARE = 6 uint8 ERROR_DSHOT_STOP_FAILED = 7

uint32 current_firmware_version # Currently installed version uint32 new_firmware_version # Version being written

Modified Files

  1. msg/versioned/VehicleCommand.msg

Add command constant in PX4-internal range (for development; upstream MAVLink later):

uint32 VEHICLE_CMD_ESC_FIRMWARE_UPDATE = 100002

  • param1: ESC index (0-based, 255 = all ESCs)
  • param2: ESC type (0 = AM32)
  1. MAVLink message definition (development.xml in mavlink submodule)

Add ESC_FIRMWARE_UPDATE_STATUS message definition. This is in the MAVLink submodule — we'll fork/PR it separately. For initial development, stream the uORB message as a generic STATUSTEXT fallback.

  1. src/modules/mavlink/streams/ESC_FIRMWARE_UPDATE_STATUS.hpp

New MAVLink stream class that subscribes to esc_firmware_update_status uORB and sends the MAVLink message. Follow the pattern of existing streams like ESC_STATUS.hpp.

  1. src/modules/mavlink/mavlink_messages.cpp

Register the new stream.

  1. Board configs

Enable MODULES_ESC_FLASHER on boards that use AM32 ESCs (e.g., ARK boards). Add esc_flasher start to the board's startup script (rc.board_defaults or similar).

Key Design Decisions

Why stop DShot entirely (not just pause)?

  • DShot uses DMA + timer hardware that conflicts with GPIO bit-bang
  • Clean shutdown via destructor guarantees all DMA released, timers freed
  • Reboot to restore is simple and safe — avoids complex reinit edge cases
  • ESCs need power cycle anyway after reflash

Why system("dshot stop") instead of uORB handshake?

  • Zero coupling — DShot module doesn't know flasher exists
  • Works with any output driver (PWM, OneShot, future drivers)
  • DShot's existing ModuleBase::stop() + destructor already handles cleanup
  • Matt's 4-message uORB handshake added 826 lines to DShot for this

Why auto-start + idle instead of on-demand spawn?

  • Module is already loaded and subscribed to vehicle_command — instant response
  • 1Hz idle poll is negligible overhead
  • Avoids complexity of commander spawning external modules
  • Kconfig opt-in means boards that don't need it pay zero cost

Why separate am32_protocol from module?

  • Clean separation of protocol logic from module orchestration
  • Protocol code can be reused by future 4-way passthrough implementation
  • Easier to test protocol functions in isolation

Firmware File Convention

Files in /fs/microsd/am32/:

  • Named by AM32 hardware target: AM32_G071_48K.bin, AM32_F051_32K.bin, etc.
  • The flasher connects to each ESC bootloader, reads the firmware tag area to determine hardware target
  • Maps tag → filename, opens file, flashes
  • If no matching file found, reports ERROR_NO_FIRMWARE_FILE for that ESC and moves on

State Machine (simplified)

Idle ──(MAV_CMD)──► StoppingDShot ──(dshot stopped)──► InitGPIOs │ (pins HIGH, 600ms) │ ┌──► ConnectESC │ │ │ (handshake OK) │ │ │ ReadInfo ──► LoadFirmware ──► Flash │ │ │ (all chunks done) │ │ │ RunApp ◄──────────────────────┘ │ │ │ (next ESC?) │ │ └──YES─┘ │ NO ▼ Complete (REBOOT_REQUIRED)

Error from any flashing state → Failed (with error code), skip to next ESC or abort.

Implementation Order

  1. am32_protocol.cpp/hpp — Extract and clean up Matt's protocol code (CRC, bitbang, commands)
  2. EscFirmwareUpdateStatus.msg — uORB message definition
  3. VehicleCommand.msg — Add command constant
  4. EscFlasher.hpp/cpp — Module skeleton with state machine
  5. CMakeLists.txt + Kconfig — Build integration
  6. MAVLink stream — ESC_FIRMWARE_UPDATE_STATUS.hpp + register in mavlink_messages.cpp
  7. Board config — Enable on a test board
  8. Test — SITL smoke test (state machine logic), then hardware test

Verification

  1. Build: make ark_fmu-v6x_default (or target board) — verify clean compile
  2. SITL smoke test: Module starts, responds to vehicle command, reports error (no GPIOs in SITL)
  3. Hardware test:
  • Place AM32 firmware on SD card
  • Send MAV_CMD_ESC_FIRMWARE_UPDATE via commander shell or MAVLink
  • Verify DShot stops, ESCs enter bootloader, firmware flashes, progress reported
  • Reboot, verify ESCs running new firmware
  1. QGC integration: Verify COMMAND_ACK and status messages appear in MAVLink inspector

Future Extensions (not in this PR)

  • 4-way interface passthrough: Add USB serial passthrough mode using the same am32_protocol infrastructure. The flasher module would proxy between USB serial (4-way protocol) and GPIO bit-bang.
  • Runtime DShot restart: Instead of requiring reboot, add dshot start after flashing. Needs careful timer/DMA reinit validation.
  • ESC settings read/write: Extend am32_protocol with EEPROM read/write commands.
  • Orchestrator module: If hot-switching between DShot and passthrough becomes needed, add a motor_output_mgr similar to CDC ACM.