Compare commits

...

8 Commits

Author SHA1 Message Date
Jacob Dahl 41fd62500e make format 2026-02-17 14:07:20 -09:00
Jacob Dahl f37df48fa2 Merge branch 'main' into jake/am32_flasher 2026-02-17 13:44:54 -09:00
Jacob Dahl ceba801573 claude first attempt at converting https://github.com/PX4/PX4-Autopilot/pull/25503 2026-02-17 13:42:51 -09:00
Matt Kowalczyk 4d58f00bf8 Revert settings.json changes. 2025-08-29 13:19:58 -04:00
MattKow-Firefly 8eb753068a Added all changes to DShot driver, needed to work with ESC Flasher module. 2025-08-29 13:16:17 -04:00
MattKow-Firefly a7e93d1145 Add last new file EscFlasherVersions.msg, forgot it in previous commit. 2025-08-29 13:05:55 -04:00
MattKow-Firefly 0f687cdbb4 Add ESC Flasher module files, msg files.
Enable ESC Flasher module in fmu-v6x. It still needs changes to DShot before it works.
2025-08-29 11:34:09 -04:00
MattKow-Firefly d773946f76 Disable some items so fmu-v6x will build. 2025-08-29 11:05:28 -04:00
11 changed files with 1577 additions and 1 deletions
+280
View File
@@ -0,0 +1,280 @@
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<EscFlasher> + 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
2. 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/<target>.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)
6. Write firmware tag section
7. CMD_RUN → ESC reboots to application
8. Move to next ESC
3. 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);
}
4. 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
)
5. src/modules/esc_flasher/Kconfig
menuconfig MODULES_ESC_FLASHER
bool "esc_flasher"
default n
---help---
AM32 ESC firmware update module
6. 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
7. 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)
8. 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.
9. 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.
10. src/modules/mavlink/mavlink_messages.cpp
Register the new stream.
11. 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
4. 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.
+2 -1
View File
@@ -44,12 +44,13 @@ CONFIG_DRIVERS_UAVCAN=y
CONFIG_BOARD_UAVCAN_TIMER_OVERRIDE=2
CONFIG_MODULES_AIRSPEED_SELECTOR=y
CONFIG_MODULES_BATTERY_STATUS=y
CONFIG_MODULES_CAMERA_FEEDBACK=y
CONFIG_MODULES_CAMERA_FEEDBACK=n
CONFIG_MODULES_COMMANDER=y
CONFIG_MODULES_CONTROL_ALLOCATOR=y
CONFIG_MODULES_DATAMAN=y
CONFIG_MODULES_EKF2=y
CONFIG_MODULES_ESC_BATTERY=y
CONFIG_MODULES_ESC_FLASHER=y
CONFIG_MODULES_EVENTS=y
CONFIG_MODULES_FLIGHT_MODE_MANAGER=y
CONFIG_MODULES_FW_ATT_CONTROL=y
+1
View File
@@ -69,6 +69,7 @@ set(msg_files
DistanceSensorModeChangeRequest.msg
DronecanNodeStatus.msg
Ekf2Timestamps.msg
EscFirmwareUpdateStatus.msg
EscReport.msg
EscStatus.msg
EstimatorAidSource1d.msg
+29
View File
@@ -0,0 +1,29 @@
uint64 timestamp # time since system start (microseconds)
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
+1
View File
@@ -114,6 +114,7 @@ uint16 VEHICLE_CMD_EXTERNAL_WIND_ESTIMATE = 43004
uint32 VEHICLE_CMD_PX4_INTERNAL_START = 65537 # Start of PX4 internal only vehicle commands (> UINT16_MAX).
uint32 VEHICLE_CMD_SET_GPS_GLOBAL_ORIGIN = 100000 # Sets the GPS coordinates of the vehicle local origin (0,0,0) position. |Unused|Unused|Unused|Unused|Latitude (WGS-84)|Longitude (WGS-84)|[m] Altitude (AMSL from GNSS, positive above ground)|
uint32 VEHICLE_CMD_SET_NAV_STATE = 100001 # Change mode by specifying nav_state directly. |nav_state|Unused|Unused|Unused|Unused|Unused|Unused|
uint32 VEHICLE_CMD_ESC_FIRMWARE_UPDATE = 100002 # Trigger AM32 ESC firmware update from SD card. |ESC index (0-based, 255=all)|ESC type (0=AM32)|Unused|Unused|Unused|Unused|Unused|
uint8 VEHICLE_MOUNT_MODE_RETRACT = 0 # Load and keep safe position (Roll,Pitch,Yaw) from permanent memory and stop stabilization.
uint8 VEHICLE_MOUNT_MODE_NEUTRAL = 1 # Load and keep neutral position (Roll,Pitch,Yaw) from permanent memory.
+42
View File
@@ -0,0 +1,42 @@
############################################################################
#
# Copyright (c) 2024 PX4 Development Team. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
# 3. Neither the name PX4 nor the names of its contributors may be
# used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
############################################################################
px4_add_module(
MODULE modules__esc_flasher
MAIN esc_flasher
SRCS
EscFlasher.cpp
am32_protocol.cpp
DEPENDS
px4_work_queue
)
+595
View File
@@ -0,0 +1,595 @@
/****************************************************************************
*
* Copyright (c) 2024 PX4 Development Team. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name PX4 nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
/**
* @file EscFlasher.cpp
*
* AM32 ESC firmware update module.
*/
#include "EscFlasher.hpp"
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <px4_platform_common/px4_config.h>
#ifdef __PX4_NUTTX
#include <px4_arch/io_timer.h>
#endif
using namespace time_literals;
EscFlasher::EscFlasher() :
ModuleParams(nullptr)
{
}
EscFlasher::~EscFlasher()
{
cleanup();
}
// ───────────────────────────── Module lifecycle ─────────────────────────────
int EscFlasher::task_spawn(int argc, char *argv[])
{
_task_id = px4_task_spawn_cmd("esc_flasher",
SCHED_DEFAULT,
SCHED_PRIORITY_DEFAULT,
PX4_STACK_ADJUSTED(4096),
(px4_main_t)&run_trampoline,
(char *const *)argv);
if (_task_id < 0) {
_task_id = -1;
return -errno;
}
return 0;
}
EscFlasher *EscFlasher::instantiate(int argc, char *argv[])
{
EscFlasher *instance = new EscFlasher();
if (instance == nullptr) {
PX4_ERR("alloc failed");
}
return instance;
}
void EscFlasher::run()
{
while (!should_exit()) {
switch (_state) {
case State::Idle:
handle_vehicle_command();
break;
case State::StoppingDShot:
state_stopping_dshot();
break;
case State::InitGPIOs:
state_init_gpios();
break;
case State::ConnectESC:
state_connect_esc();
break;
case State::ReadInfo:
state_read_info();
break;
case State::LoadFirmware:
state_load_firmware();
break;
case State::Flashing:
state_flash_chunk();
break;
case State::RunApp:
state_run_app();
break;
case State::Complete:
state_complete();
break;
case State::Failed:
state_failed();
break;
}
// Idle → slow poll; active → no extra sleep (protocol
// bit-bang already takes wall-clock time per chunk)
if (_state == State::Idle) {
px4_usleep(IDLE_INTERVAL_US);
}
}
}
// ───────────────────────────── Vehicle command ──────────────────────────────
void EscFlasher::handle_vehicle_command()
{
vehicle_command_s cmd;
while (_vehicle_cmd_sub.update(&cmd)) {
if (cmd.command != vehicle_command_s::VEHICLE_CMD_ESC_FIRMWARE_UPDATE) {
continue;
}
// Must be disarmed
vehicle_status_s vs{};
_vehicle_status_sub.copy(&vs);
if (vs.arming_state != vehicle_status_s::ARMING_STATE_DISARMED) {
PX4_ERR("cannot flash ESCs while armed");
ack_vehicle_command(cmd, vehicle_command_ack_s::VEHICLE_CMD_RESULT_DENIED);
_status.error = esc_firmware_update_status_s::ERROR_NOT_DISARMED;
publish_status();
continue;
}
ack_vehicle_command(cmd, vehicle_command_ack_s::VEHICLE_CMD_RESULT_ACCEPTED);
uint8_t esc_index = (uint8_t)cmd.param1; // 255 = all
begin_update(esc_index);
return; // exit the poll loop — we are now active
}
}
void EscFlasher::ack_vehicle_command(const vehicle_command_s &cmd, uint8_t result)
{
vehicle_command_ack_s ack{};
ack.command = cmd.command;
ack.result = result;
ack.target_system = cmd.source_system;
ack.target_component = cmd.source_component;
ack.timestamp = hrt_absolute_time();
_cmd_ack_pub.publish(ack);
}
void EscFlasher::begin_update(uint8_t esc_index)
{
_target_esc = esc_index;
_current_esc = 0;
_esc_count = 0;
_flash_started = hrt_absolute_time();
memset(&_status, 0, sizeof(_status));
_status.status = esc_firmware_update_status_s::STATUS_CONNECTING;
publish_status();
mavlink_log_info(&_mavlink_log_pub, "ESC firmware update starting");
transition(State::StoppingDShot);
}
// ───────────────────────────── State: StoppingDShot ─────────────────────────
void EscFlasher::state_stopping_dshot()
{
PX4_INFO("stopping dshot driver");
int ret = system("dshot stop");
if (ret != 0) {
PX4_WARN("dshot stop returned %d (may already be stopped)", ret);
}
// Give DShot destructor time to release DMA/timers
px4_usleep(200000); // 200 ms
transition(State::InitGPIOs);
}
// ───────────────────────────── State: InitGPIOs ─────────────────────────────
void EscFlasher::state_init_gpios()
{
#ifdef __PX4_NUTTX
// Discover motor GPIO pins from the io_timer subsystem
_esc_count = 0;
for (unsigned ch = 0; ch < MAX_ESCS; ch++) {
uint32_t gpio = io_timer_channel_get_gpio_output(ch);
if (gpio != 0) {
_gpio_pins[_esc_count] = gpio & (GPIO_PORT_MASK | GPIO_PIN_MASK);
_esc_count++;
}
}
if (_esc_count == 0) {
PX4_ERR("no motor GPIOs found");
fail(esc_firmware_update_status_s::ERROR_DSHOT_STOP_FAILED);
return;
}
PX4_INFO("found %u motor GPIO(s)", _esc_count);
// If targeting a specific ESC, validate index
if (_target_esc != 255 && _target_esc >= _esc_count) {
PX4_ERR("ESC index %u out of range (have %u)", _target_esc, _esc_count);
fail(esc_firmware_update_status_s::ERROR_UNSUPPORTED_HARDWARE);
return;
}
// Drive all target pins HIGH to force ESCs into bootloader on next reset
for (uint8_t i = 0; i < _esc_count; i++) {
bool target = (_target_esc == 255) || (i == _target_esc);
if (target) {
uint32_t gpio_out = _gpio_pins[i]
| GPIO_OUTPUT | GPIO_OUTPUT_SET | GPIO_PULLUP;
px4_arch_configgpio(gpio_out);
}
}
PX4_INFO("GPIOs HIGH — waiting %u ms for ESC bootloader entry",
(unsigned)(GPIO_SETTLE_TIME_US / 1000));
_status.esc_count = (_target_esc == 255) ? _esc_count : 1;
publish_status();
// Wait for ESCs to detect HIGH and enter bootloader
px4_usleep(GPIO_SETTLE_TIME_US);
// Set starting ESC
if (_target_esc != 255) {
_current_esc = _target_esc;
} else {
_current_esc = 0;
}
transition(State::ConnectESC);
#else
PX4_ERR("GPIO operations not supported on this platform");
fail(esc_firmware_update_status_s::ERROR_DSHOT_STOP_FAILED);
#endif
}
// ───────────────────────────── State: ConnectESC ────────────────────────────
void EscFlasher::state_connect_esc()
{
PX4_INFO("connecting to ESC %u", _current_esc);
_status.esc_index = _current_esc;
_status.status = esc_firmware_update_status_s::STATUS_CONNECTING;
_status.error = esc_firmware_update_status_s::ERROR_NONE;
publish_status();
int ret = am32::handshake(_gpio_pins[_current_esc], _device_info);
if (ret != am32::OK) {
PX4_ERR("bootloader handshake failed on ESC %u (err %d)", _current_esc, ret);
fail(esc_firmware_update_status_s::ERROR_COMMS_TIMEOUT);
return;
}
PX4_INFO("ESC %u bootloader: %02x %02x %02x %02x %02x %02x %02x %02x %02x",
_current_esc,
_device_info[0], _device_info[1], _device_info[2],
_device_info[3], _device_info[4], _device_info[5],
_device_info[6], _device_info[7], _device_info[8]);
transition(State::ReadInfo);
}
// ───────────────────────────── State: ReadInfo ──────────────────────────────
void EscFlasher::state_read_info()
{
_status.status = esc_firmware_update_status_s::STATUS_READING_INFO;
publish_status();
// Device info already obtained during handshake.
// Future: read firmware tag to auto-select firmware file.
transition(State::LoadFirmware);
}
// ───────────────────────────── State: LoadFirmware ──────────────────────────
void EscFlasher::state_load_firmware()
{
// Close any previously open file (when looping over ESCs)
if (_fw_fd >= 0) {
close(_fw_fd);
_fw_fd = -1;
}
_fw_fd = open(FIRMWARE_PATH, O_RDONLY);
if (_fw_fd < 0) {
PX4_ERR("cannot open %s", FIRMWARE_PATH);
mavlink_log_critical(&_mavlink_log_pub, "No firmware file on SD card");
fail(esc_firmware_update_status_s::ERROR_NO_FIRMWARE_FILE);
return;
}
struct stat st;
if (fstat(_fw_fd, &st) != 0 || st.st_size == 0) {
PX4_ERR("cannot stat firmware file");
close(_fw_fd);
_fw_fd = -1;
fail(esc_firmware_update_status_s::ERROR_NO_FIRMWARE_FILE);
return;
}
_fw_size = (uint32_t)st.st_size;
_fw_written = 0;
PX4_INFO("firmware %s: %lu bytes, %lu chunks",
FIRMWARE_PATH, (unsigned long)_fw_size,
(unsigned long)((_fw_size + am32::MAX_CHUNK_SIZE - 1) / am32::MAX_CHUNK_SIZE));
transition(State::Flashing);
}
// ───────────────────────────── State: Flashing ──────────────────────────────
void EscFlasher::state_flash_chunk()
{
_status.status = esc_firmware_update_status_s::STATUS_WRITING;
uint32_t remaining = _fw_size - _fw_written;
uint16_t chunk_len = (remaining > am32::MAX_CHUNK_SIZE)
? am32::MAX_CHUNK_SIZE : (uint16_t)remaining;
uint8_t buf[am32::MAX_CHUNK_SIZE];
ssize_t nread = read(_fw_fd, buf, chunk_len);
if (nread != (ssize_t)chunk_len) {
PX4_ERR("firmware read error at offset %lu", (unsigned long)_fw_written);
fail(esc_firmware_update_status_s::ERROR_NO_FIRMWARE_FILE);
return;
}
// Set flash address (16-bit, relative to STM32 flash base)
uint16_t address = (uint16_t)((am32::FIRMWARE_ADDR & 0xFFFF) + _fw_written);
int ret = am32::set_address(_gpio_pins[_current_esc], address);
if (ret != am32::OK) {
PX4_ERR("set_address failed on ESC %u (err %d)", _current_esc, ret);
fail(esc_firmware_update_status_s::ERROR_COMMS_TIMEOUT);
return;
}
ret = am32::write_chunk(_gpio_pins[_current_esc], buf, chunk_len);
if (ret != am32::OK) {
PX4_ERR("write_chunk failed on ESC %u at 0x%04x (err %d)",
_current_esc, address, ret);
fail(esc_firmware_update_status_s::ERROR_CRC_MISMATCH);
return;
}
_fw_written += chunk_len;
// Progress for current ESC
_status.progress_pct = (uint8_t)((_fw_written * 100) / _fw_size);
// Overall progress across all ESCs
uint8_t esc_total = (_target_esc == 255) ? _esc_count : 1;
uint8_t esc_done;
if (_target_esc == 255) {
esc_done = _current_esc;
} else {
esc_done = 0;
}
_status.overall_progress_pct = (uint8_t)(
((esc_done * 100) + _status.progress_pct) / esc_total);
publish_status();
uint32_t total_chunks = (_fw_size + am32::MAX_CHUNK_SIZE - 1) / am32::MAX_CHUNK_SIZE;
uint32_t current_chunk = (_fw_written + am32::MAX_CHUNK_SIZE - 1) / am32::MAX_CHUNK_SIZE;
PX4_INFO("ESC %u: chunk %lu/%lu (%u%%)",
_current_esc, (unsigned long)current_chunk,
(unsigned long)total_chunks, _status.progress_pct);
if (_fw_written >= _fw_size) {
transition(State::RunApp);
}
}
// ───────────────────────────── State: RunApp ────────────────────────────────
void EscFlasher::state_run_app()
{
PX4_INFO("sending RUN_APP to ESC %u", _current_esc);
am32::run_app(_gpio_pins[_current_esc]);
// Close firmware file so it can be re-opened for the next ESC
if (_fw_fd >= 0) {
close(_fw_fd);
_fw_fd = -1;
}
advance_to_next_esc();
}
void EscFlasher::advance_to_next_esc()
{
if (_target_esc != 255) {
// Single-ESC mode — done
transition(State::Complete);
return;
}
_current_esc++;
if (_current_esc >= _esc_count) {
transition(State::Complete);
} else {
// Next ESC
transition(State::ConnectESC);
}
}
// ───────────────────────────── State: Complete ──────────────────────────────
void EscFlasher::state_complete()
{
hrt_abstime elapsed = hrt_absolute_time() - _flash_started;
unsigned seconds = (unsigned)(elapsed / 1000000);
PX4_INFO("ESC firmware update complete in %u seconds", seconds);
mavlink_log_info(&_mavlink_log_pub, "ESC flash done in %us — reboot required", seconds);
_status.status = esc_firmware_update_status_s::STATUS_REBOOT_REQUIRED;
_status.progress_pct = 100;
_status.overall_progress_pct = 100;
_status.error = esc_firmware_update_status_s::ERROR_NONE;
publish_status();
cleanup();
transition(State::Idle);
}
// ───────────────────────────── State: Failed ────────────────────────────────
void EscFlasher::state_failed()
{
PX4_ERR("ESC firmware update failed (error %u)", _status.error);
mavlink_log_critical(&_mavlink_log_pub, "ESC flash failed (error %u)", _status.error);
_status.status = esc_firmware_update_status_s::STATUS_FAILED;
publish_status();
cleanup();
transition(State::Idle);
}
// ───────────────────────────── Helpers ──────────────────────────────────────
void EscFlasher::transition(State s)
{
_state = s;
_state_entered = hrt_absolute_time();
}
void EscFlasher::fail(uint8_t error_code)
{
_status.error = error_code;
transition(State::Failed);
}
void EscFlasher::cleanup()
{
if (_fw_fd >= 0) {
close(_fw_fd);
_fw_fd = -1;
}
_fw_size = 0;
_fw_written = 0;
}
void EscFlasher::publish_status()
{
_status.timestamp = hrt_absolute_time();
_status_pub.publish(_status);
}
// ───────────────────────────── ModuleBase boilerplate ────────────────────────
int EscFlasher::print_status()
{
static const char *state_names[] = {
"Idle", "StoppingDShot", "InitGPIOs", "ConnectESC",
"ReadInfo", "LoadFirmware", "Flashing", "RunApp",
"Complete", "Failed"
};
PX4_INFO("state: %s", state_names[(int)_state]);
PX4_INFO("ESC %u / %u, firmware %lu / %lu bytes",
_current_esc, _esc_count,
(unsigned long)_fw_written, (unsigned long)_fw_size);
return 0;
}
int EscFlasher::print_usage(const char *reason)
{
if (reason) {
PX4_ERR("%s", reason);
}
PRINT_MODULE_DESCRIPTION(
R"DESCR_STR(
### Description
AM32 ESC firmware update module.
Reads firmware from SD card, stops DShot, and flashes ESCs
via bit-banged bootloader protocol. Triggered by the
VEHICLE_CMD_ESC_FIRMWARE_UPDATE vehicle command.
A reboot is required after flashing to restore normal operation.
)DESCR_STR");
PRINT_MODULE_USAGE_NAME("esc_flasher", "system");
PRINT_MODULE_USAGE_COMMAND_DESCR("start", "Start the module (auto-started on supported boards)");
PRINT_MODULE_USAGE_DEFAULT_COMMANDS();
return 0;
}
int EscFlasher::custom_command(int argc, char *argv[])
{
return print_usage("unrecognised command");
}
extern "C" __EXPORT int esc_flasher_main(int argc, char *argv[])
{
return EscFlasher::main(argc, argv);
}
+160
View File
@@ -0,0 +1,160 @@
/****************************************************************************
*
* Copyright (c) 2024 PX4 Development Team. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name PX4 nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
/**
* @file EscFlasher.hpp
*
* AM32 ESC firmware update module.
*
* Triggered by MAV_CMD_ESC_FIRMWARE_UPDATE via vehicle_command.
* Reads firmware from SD card, stops DShot, takes over motor GPIOs,
* and flashes each ESC via bit-banged AM32 bootloader protocol.
* Requires reboot to restore normal DShot operation.
*/
#pragma once
#include <px4_platform_common/module.h>
#include <px4_platform_common/module_params.h>
#include <px4_platform_common/posix.h>
#include <drivers/drv_hrt.h>
#include <systemlib/mavlink_log.h>
#include <uORB/Publication.hpp>
#include <uORB/Subscription.hpp>
#include <uORB/topics/esc_firmware_update_status.h>
#include <uORB/topics/vehicle_command.h>
#include <uORB/topics/vehicle_command_ack.h>
#include <uORB/topics/vehicle_status.h>
#include "am32_protocol.hpp"
class EscFlasher : public ModuleBase<EscFlasher>, public ModuleParams
{
public:
EscFlasher();
~EscFlasher() override;
EscFlasher(const EscFlasher &) = delete;
EscFlasher &operator=(const EscFlasher &) = delete;
/** @see ModuleBase */
static int task_spawn(int argc, char *argv[]);
/** @see ModuleBase */
static EscFlasher *instantiate(int argc, char *argv[]);
/** @see ModuleBase::run() */
void run() override;
/** @see ModuleBase */
static int custom_command(int argc, char *argv[]);
/** @see ModuleBase */
static int print_usage(const char *reason = nullptr);
/** @see ModuleBase::print_status() */
int print_status() override;
private:
static constexpr uint8_t MAX_ESCS = 8;
static constexpr const char *FIRMWARE_PATH = "/fs/microsd/am32/firmware.bin";
static constexpr uint32_t DSHOT_STOP_TIMEOUT_US = 2000000; // 2 s
static constexpr uint32_t GPIO_SETTLE_TIME_US = 600000; // 600 ms
static constexpr uint32_t IDLE_INTERVAL_US = 1000000; // 1 s
enum class State : uint8_t {
Idle,
StoppingDShot,
InitGPIOs,
ConnectESC,
ReadInfo,
LoadFirmware,
Flashing,
RunApp,
Complete,
Failed
};
// --- state machine helpers ---
void handle_vehicle_command();
void ack_vehicle_command(const vehicle_command_s &cmd, uint8_t result);
void begin_update(uint8_t esc_index);
void advance_to_next_esc();
void transition(State s);
void fail(uint8_t error_code);
void cleanup();
void publish_status();
// --- state handlers (called from run loop) ---
void state_stopping_dshot();
void state_init_gpios();
void state_connect_esc();
void state_read_info();
void state_load_firmware();
void state_flash_chunk();
void state_run_app();
void state_complete();
void state_failed();
// --- current state ---
State _state{State::Idle};
hrt_abstime _state_entered{0};
hrt_abstime _flash_started{0};
// --- ESC tracking ---
uint8_t _target_esc{255}; // from vehicle_command param1
uint8_t _current_esc{0}; // index into _gpio_pins[]
uint8_t _esc_count{0};
uint32_t _gpio_pins[MAX_ESCS] {};
uint8_t _device_info[am32::DEVICE_INFO_SIZE] {};
// --- firmware file ---
int _fw_fd{-1};
uint32_t _fw_size{0};
uint32_t _fw_written{0};
// --- published status ---
esc_firmware_update_status_s _status{};
// --- publications ---
uORB::Publication<esc_firmware_update_status_s> _status_pub{ORB_ID(esc_firmware_update_status)};
uORB::Publication<vehicle_command_ack_s> _cmd_ack_pub{ORB_ID(vehicle_command_ack)};
// --- subscriptions ---
uORB::Subscription _vehicle_cmd_sub{ORB_ID(vehicle_command)};
uORB::Subscription _vehicle_status_sub{ORB_ID(vehicle_status)};
// --- mavlink log ---
orb_advert_t _mavlink_log_pub{nullptr};
};
+5
View File
@@ -0,0 +1,5 @@
menuconfig MODULES_ESC_FLASHER
bool "esc_flasher"
default n
---help---
Enable support for ESC Flasher module
+305
View File
@@ -0,0 +1,305 @@
/****************************************************************************
*
* Copyright (c) 2024 PX4 Development Team. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name PX4 nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
/**
* @file am32_protocol.cpp
*
* AM32 ESC bootloader protocol — bit-banged UART implementation.
*/
#include "am32_protocol.hpp"
#include <string.h>
#include <px4_platform_common/px4_config.h>
#include <px4_platform_common/micro_hal.h>
#include <drivers/drv_hrt.h>
namespace am32
{
// Bootloader handshake packet (8 zero bytes + 0x0D + "BLHeli" + CRC)
static const uint8_t boot_init[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0D, 'B', 'L', 'H', 'e', 'l', 'i', 0xF4, 0x7D
};
uint16_t crc16(const uint8_t *buf, uint16_t len)
{
uint16_t crc = 0;
for (uint16_t i = 0; i < len; i++) {
uint8_t xb = buf[i];
for (uint8_t j = 0; j < 8; j++) {
if (((xb & 0x01) ^ (crc & 0x0001)) != 0) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
xb >>= 1;
}
}
return crc;
}
int send_packet(uint32_t gpio, const uint8_t *tx_data, uint16_t tx_len,
uint8_t *rx_data, uint8_t rx_len)
{
// Build GPIO configurations from the port+pin value
uint32_t gpio_out = (gpio & (GPIO_PORT_MASK | GPIO_PIN_MASK))
| GPIO_OUTPUT | GPIO_OUTPUT_SET | GPIO_PULLUP;
uint32_t gpio_in = (gpio & (GPIO_PORT_MASK | GPIO_PIN_MASK))
| GPIO_INPUT | GPIO_PULLUP;
// Start as output HIGH
px4_arch_configgpio(gpio_out);
// --- critical section: interrupts off for timing accuracy ---
irqstate_t irq_state = px4_enter_critical_section();
// Transmit bytes (19200 8N1, LSB-first)
for (uint16_t k = 0; k < tx_len; k++) {
uint8_t byte = tx_data[k];
// Start bit (LOW)
px4_arch_gpiowrite(gpio_out, 0);
hrt_abstime t = hrt_absolute_time();
while (hrt_absolute_time() - t < BIT_TIME_US) {}
// 8 data bits, LSB first
for (uint8_t i = 0; i < 8; i++) {
px4_arch_gpiowrite(gpio_out, (byte >> i) & 1);
t = hrt_absolute_time();
while (hrt_absolute_time() - t < BIT_TIME_US) {}
}
// Stop bit (HIGH)
px4_arch_gpiowrite(gpio_out, 1);
t = hrt_absolute_time();
while (hrt_absolute_time() - t < BIT_TIME_US) {}
}
// Switch to input for receiving
px4_arch_configgpio(gpio_in);
// Receive response bytes
uint8_t read_count = 0;
if (rx_len > 0 && rx_data != nullptr) {
for (uint8_t i = 0; i < rx_len; i++) {
// Wait for start bit (line goes LOW)
hrt_abstime t = hrt_absolute_time();
while (px4_arch_gpioread(gpio_in)) {
if (hrt_absolute_time() - t > RX_TIMEOUT_US) {
// Timeout — restore output HIGH and bail
px4_arch_configgpio(gpio_out);
px4_leave_critical_section(irq_state);
return ERR_TIMEOUT;
}
}
// Half-bit delay to centre sampling window
t = hrt_absolute_time();
while (hrt_absolute_time() - t < HALF_BIT_TIME_US) {}
// Read 8 data bits
uint8_t byte = 0;
for (uint8_t bit = 0; bit < 8; bit++) {
t = hrt_absolute_time();
while (hrt_absolute_time() - t < BIT_TIME_US) {}
if (px4_arch_gpioread(gpio_in)) {
byte |= (1 << bit);
}
}
rx_data[read_count++] = byte;
// Wait through stop bit
t = hrt_absolute_time();
while (hrt_absolute_time() - t < BIT_TIME_US) {}
}
}
// Restore pin to output HIGH
px4_arch_configgpio(gpio_out);
// Inter-packet gap (8 bit-times)
hrt_abstime t = hrt_absolute_time();
while (hrt_absolute_time() - t < BIT_TIME_US * 8) {}
px4_leave_critical_section(irq_state);
return read_count;
}
int handshake(uint32_t gpio, uint8_t device_info[DEVICE_INFO_SIZE])
{
int ret = send_packet(gpio, boot_init, sizeof(boot_init),
device_info, DEVICE_INFO_SIZE);
if (ret == DEVICE_INFO_SIZE) {
return OK;
}
return (ret < 0) ? ret : ERR_NACK;
}
int set_address(uint32_t gpio, uint16_t address)
{
uint8_t cmd[6];
cmd[0] = CMD_SET_ADDRESS;
cmd[1] = 0x00;
cmd[2] = (address >> 8) & 0xFF; // high byte
cmd[3] = address & 0xFF; // low byte
uint16_t crc = crc16(cmd, 4);
cmd[4] = crc & 0xFF;
cmd[5] = (crc >> 8) & 0xFF;
uint8_t response;
int ret = send_packet(gpio, cmd, sizeof(cmd), &response, 1);
if (ret == 1 && response == ACK) {
return OK;
}
return (ret < 0) ? ret : ERR_NACK;
}
int write_chunk(uint32_t gpio, const uint8_t *data, uint16_t len)
{
if (len == 0 || len > MAX_CHUNK_SIZE || data == nullptr) {
return ERR_INVALID;
}
// 1) CMD_SET_BUFFER — tell bootloader how many bytes follow
uint8_t size_cmd[6];
size_cmd[0] = CMD_SET_BUFFER;
size_cmd[1] = 0x00;
if (len == MAX_CHUNK_SIZE) {
size_cmd[2] = 1; // 256 encoded as (1, 0)
size_cmd[3] = 0;
} else {
size_cmd[2] = 0;
size_cmd[3] = (uint8_t)len;
}
uint16_t crc = crc16(size_cmd, 4);
size_cmd[4] = crc & 0xFF;
size_cmd[5] = (crc >> 8) & 0xFF;
// SET_BUFFER has no response
send_packet(gpio, size_cmd, sizeof(size_cmd), nullptr, 0);
// 2) Send data + CRC — bootloader ACKs after receiving
uint8_t buf[MAX_CHUNK_SIZE + 2];
memcpy(buf, data, len);
crc = crc16(data, len);
buf[len] = crc & 0xFF;
buf[len + 1] = (crc >> 8) & 0xFF;
uint8_t response;
int ret = send_packet(gpio, buf, len + 2, &response, 1);
if (ret != 1 || response != ACK) {
return (ret < 0) ? ret : ERR_NACK;
}
// 3) CMD_PROG_FLASH — commit buffer to flash
uint8_t prog_cmd[4];
prog_cmd[0] = CMD_PROG_FLASH;
prog_cmd[1] = 0x00;
crc = crc16(prog_cmd, 2);
prog_cmd[2] = crc & 0xFF;
prog_cmd[3] = (crc >> 8) & 0xFF;
ret = send_packet(gpio, prog_cmd, sizeof(prog_cmd), &response, 1);
if (ret == 1 && response == ACK) {
return OK;
}
return (ret < 0) ? ret : ERR_NACK;
}
int read_flash(uint32_t gpio, uint8_t *buf, uint16_t len)
{
if (len == 0 || buf == nullptr) {
return ERR_INVALID;
}
// Address must already be set via set_address()
uint8_t cmd[4];
cmd[0] = CMD_READ_FLASH;
cmd[1] = (uint8_t)len;
uint16_t crc = crc16(cmd, 2);
cmd[2] = crc & 0xFF;
cmd[3] = (crc >> 8) & 0xFF;
int ret = send_packet(gpio, cmd, sizeof(cmd), buf, (uint8_t)len);
if (ret == (int)len) {
return OK;
}
return (ret < 0) ? ret : ERR_NACK;
}
int run_app(uint32_t gpio)
{
static const uint8_t cmd[] = {0x00, 0x00, 0x00, 0x00};
// No response expected
send_packet(gpio, cmd, sizeof(cmd), nullptr, 0);
return OK;
}
} // namespace am32
+157
View File
@@ -0,0 +1,157 @@
/****************************************************************************
*
* Copyright (c) 2024 PX4 Development Team. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name PX4 nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
/**
* @file am32_protocol.hpp
*
* AM32 ESC bootloader protocol — bit-banged UART at 19200 baud.
*
* Extracted from Matt's proof-of-concept and cleaned up into a
* function-based API for reuse by the esc_flasher module.
*/
#pragma once
#include <stdint.h>
namespace am32
{
// AM32 bootloader command bytes
static constexpr uint8_t CMD_RUN = 0x00;
static constexpr uint8_t CMD_PROG_FLASH = 0x01;
static constexpr uint8_t CMD_READ_FLASH = 0x03;
static constexpr uint8_t CMD_SET_BUFFER = 0xFE;
static constexpr uint8_t CMD_SET_ADDRESS = 0xFF;
// Bootloader ACK byte
static constexpr uint8_t ACK = 0x30;
// AM32 firmware memory layout (STM32 flash addresses)
static constexpr uint32_t FIRMWARE_ADDR = 0x08001000;
static constexpr uint32_t FIRMWARE_TAG_ADDR = 0x08007BE0;
static constexpr uint16_t FIRMWARE_TAG_SIZE = 16;
// Protocol constants
static constexpr uint16_t MAX_CHUNK_SIZE = 256;
static constexpr uint32_t BIT_TIME_US = 52; // ~19200 baud
static constexpr uint32_t HALF_BIT_TIME_US = 26;
static constexpr uint32_t RX_TIMEOUT_US = 100000; // 100 ms
// Device info from handshake response
static constexpr uint8_t DEVICE_INFO_SIZE = 9;
// Return codes
static constexpr int OK = 0;
static constexpr int ERR_TIMEOUT = -1;
static constexpr int ERR_NACK = -2;
static constexpr int ERR_CRC = -3;
static constexpr int ERR_INVALID = -4;
/**
* CRC-16 with polynomial 0xA001 (AM32 bootloader).
*/
uint16_t crc16(const uint8_t *buf, uint16_t len);
/**
* Bit-banged UART send/receive on a GPIO pin.
*
* Enters a critical section (interrupts disabled) for the
* duration of the transfer to maintain 19200-baud timing.
*
* @param gpio GPIO configuration value (port + pin bits)
* @param tx_data Bytes to transmit
* @param tx_len Number of bytes to transmit
* @param rx_data Buffer for response (may be nullptr if rx_len == 0)
* @param rx_len Expected number of response bytes
* @return Number of bytes received, or negative error code
*/
int send_packet(uint32_t gpio, const uint8_t *tx_data, uint16_t tx_len,
uint8_t *rx_data, uint8_t rx_len);
/**
* Perform the bootloader handshake.
*
* Sends the AM32 boot-init sequence and reads back 9 bytes
* of device information.
*
* @param gpio Target ESC GPIO
* @param device_info Output buffer for 9-byte device info
* @return OK or negative error code
*/
int handshake(uint32_t gpio, uint8_t device_info[DEVICE_INFO_SIZE]);
/**
* Set the flash write/read address.
*
* @param gpio Target ESC GPIO
* @param address 16-bit flash address
* @return OK or negative error code
*/
int set_address(uint32_t gpio, uint16_t address);
/**
* Write a chunk to flash.
*
* Sends CMD_SET_BUFFER + size, then data + CRC, then
* CMD_PROG_FLASH and waits for ACK. Caller must call
* set_address() first.
*
* @param gpio Target ESC GPIO
* @param data Data to write (max MAX_CHUNK_SIZE bytes)
* @param len Byte count
* @return OK or negative error code
*/
int write_chunk(uint32_t gpio, const uint8_t *data, uint16_t len);
/**
* Read flash memory.
*
* Caller must call set_address() first.
*
* @param gpio Target ESC GPIO
* @param buf Output buffer
* @param len Number of bytes to read
* @return OK or negative error code
*/
int read_flash(uint32_t gpio, uint8_t *buf, uint16_t len);
/**
* Send the RUN_APP command so the ESC boots its application.
*
* @param gpio Target ESC GPIO
* @return OK (always; no response expected)
*/
int run_app(uint32_t gpio);
} // namespace am32