From 173bf6dda5cb7239fbc700850218e9c8f617898a Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 01/25] sys_can: configurable FDCAN pins, bounded init wait, drain RX FIFO Generalise the hardcoded PA11/PA12 FDCAN1 setup to FDCAN_RX/TX_PORT/_PIN/_AF macros (default PA11/PA12 AF9) and enable GPIOB so boards can route CAN elsewhere. Bound waitForBitState() with a finite try count so init can't spin forever. Drain all queued RX frames per interrupt: the FDCAN new-message IRQ is not re-asserted for frames already in the FIFO, so reading one per IRQ delayed queued DNA/GetSet/firmware-update traffic. --- bootloader/DroneCAN/sys_can_stm32_CANFD.c | 113 +++++++++++++--------- 1 file changed, 66 insertions(+), 47 deletions(-) diff --git a/bootloader/DroneCAN/sys_can_stm32_CANFD.c b/bootloader/DroneCAN/sys_can_stm32_CANFD.c index 3e7e7368..2074824c 100644 --- a/bootloader/DroneCAN/sys_can_stm32_CANFD.c +++ b/bootloader/DroneCAN/sys_can_stm32_CANFD.c @@ -132,47 +132,47 @@ static void handleRxInterrupt(uint8_t fifo_index) uint32_t get_index_mask = (fifo_index == 0) ? FDCAN_RXF0S_F0GI : FDCAN_RXF1S_F1GI; uint32_t get_index_shift = (fifo_index == 0) ? FDCAN_RXF0S_F0GI_SHIFT : FDCAN_RXF1S_F1GI_SHIFT; - // Check if FIFO has messages - if ((*fifo_status_reg & fifo_level_mask) == 0) { - return; - } - - // Get the get index - uint32_t get_index = (*fifo_status_reg & get_index_mask) >> get_index_shift; - - // Calculate address in message RAM - uint32_t rx_fifo_addr = (fifo_index == 0) ? MessageRam_RxFIFO0SA : MessageRam_RxFIFO1SA; - volatile RxMessageRAM *rx_mailbox = (volatile RxMessageRAM *)(rx_fifo_addr + (get_index * FDCAN_FRAME_BUFFER_SIZE * 4)); - - // Read the frame - CanardCANFrame frame = {}; - - uint32_t id_flags = rx_mailbox->id_flags; - if (id_flags & (1U << 30)) { - frame.id = (id_flags & MaskExtID) | CANARD_CAN_FRAME_EFF; - } else { - frame.id = (id_flags >> 18) & MaskStdID; - } - - if (id_flags & (1U << 29)) { - frame.id |= CANARD_CAN_FRAME_RTR; - } - - // Get DLC - uint32_t dlc = (rx_mailbox->dlc_timestamp >> 16) & 0xF; - frame.data_len = dlc; - - // Copy data - uint32_t *data_ptr = (uint32_t *)frame.data; - for (int i = 0; i < 2; i++) { - data_ptr[i] = rx_mailbox->data[i]; + // Drain every queued frame. The FIFO new-message interrupt is not re-asserted + // for frames already queued, so reading only one per IRQ would delay the rest + // until the next frame arrives. + while ((*fifo_status_reg & fifo_level_mask) != 0) { + // Get the get index + uint32_t get_index = (*fifo_status_reg & get_index_mask) >> get_index_shift; + + // Calculate address in message RAM + uint32_t rx_fifo_addr = (fifo_index == 0) ? MessageRam_RxFIFO0SA : MessageRam_RxFIFO1SA; + volatile RxMessageRAM *rx_mailbox = (volatile RxMessageRAM *)(rx_fifo_addr + (get_index * FDCAN_FRAME_BUFFER_SIZE * 4)); + + // Read the frame + CanardCANFrame frame = {}; + + uint32_t id_flags = rx_mailbox->id_flags; + if (id_flags & (1U << 30)) { + frame.id = (id_flags & MaskExtID) | CANARD_CAN_FRAME_EFF; + } else { + frame.id = (id_flags >> 18) & MaskStdID; + } + + if (id_flags & (1U << 29)) { + frame.id |= CANARD_CAN_FRAME_RTR; + } + + // Get DLC + uint32_t dlc = (rx_mailbox->dlc_timestamp >> 16) & 0xF; + frame.data_len = dlc; + + // Copy data + uint32_t *data_ptr = (uint32_t *)frame.data; + for (int i = 0; i < 2; i++) { + data_ptr[i] = rx_mailbox->data[i]; + } + + // Acknowledge the read + *fifo_ack_reg = get_index; + + // Process the frame + DroneCAN_handleFrame(&frame); } - - // Acknowledge the read - *fifo_ack_reg = get_index; - - // Process the frame - DroneCAN_handleFrame(&frame); } static void handleTxCompleteInterrupt(void) @@ -280,9 +280,8 @@ void sys_can_enable_IRQ(void) */ static bool waitForBitState(volatile uint32_t *reg, uint32_t mask, bool target_state) { - while (true) { - bool current_state = ((*reg) & mask) != 0; - if (current_state == target_state) { + for (volatile uint32_t tries = 0; tries < 1000000; tries++) { + if ((((*reg) & mask) != 0) == target_state) { return true; } } @@ -383,17 +382,37 @@ static void can_init(void) void sys_can_init(void) { // Setup CAN RX and TX pins - // assumes PA11/PA12 for FDCAN1 +#ifndef FDCAN_RX_PORT +#define FDCAN_RX_PORT GPIOA +#define FDCAN_RX_PIN LL_GPIO_PIN_11 +#endif +#ifndef FDCAN_TX_PORT +#define FDCAN_TX_PORT GPIOA +#define FDCAN_TX_PIN LL_GPIO_PIN_12 +#endif +#ifndef FDCAN_RX_AF +#define FDCAN_RX_AF LL_GPIO_AF_9 +#endif +#ifndef FDCAN_TX_AF +#define FDCAN_TX_AF LL_GPIO_AF_9 +#endif + LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOA); + LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOB); LL_GPIO_InitTypeDef GPIO_InitStruct = {0}; - GPIO_InitStruct.Pin = LL_GPIO_PIN_11 | LL_GPIO_PIN_12; GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE; GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL; GPIO_InitStruct.Pull = LL_GPIO_PULL_NO; GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH; - GPIO_InitStruct.Alternate = LL_GPIO_AF_9; // AF9 for FDCAN1 - LL_GPIO_Init(GPIOA, &GPIO_InitStruct); + + GPIO_InitStruct.Pin = FDCAN_RX_PIN; + GPIO_InitStruct.Alternate = FDCAN_RX_AF; + LL_GPIO_Init(FDCAN_RX_PORT, &GPIO_InitStruct); + + GPIO_InitStruct.Pin = FDCAN_TX_PIN; + GPIO_InitStruct.Alternate = FDCAN_TX_AF; + LL_GPIO_Init(FDCAN_TX_PORT, &GPIO_InitStruct); can_init(); From 16661d3a9b1ecade52fafa172490424df0520783 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 02/25] bootloader: run DroneCAN concurrently with bit-banged serial Let a CAN bootloader service DroneCAN while still accepting 4-way serial, so a board driven by DShot or DroneCAN can be flashed over either transport. DroneCAN_boot_ok() does a multi-ms memmem + crc32 scan of the firmware; run from the serial-wait loop it deafened the bit-banged reads and corrupted 4-way transfers. Once a config client connects (sendDeviceInfo) we set bl_serial_active and stop polling DroneCAN for the session (which always ends in a reset); otherwise we poll only every ~50ms in the start-bit wait, with CAN IRQs disabled around each bit-banged read/write. At boot we sample the signal pin and call DroneCAN_set_have_signal() if it's driven (DShot) so boot_ok() won't block on a CAN RawCommand. Also ack ExecuteOpcode SAVE (GetSet writes straight to flash, so it's a no-op) since DroneCAN parameter clients expect SAVE to succeed. --- bootloader/DroneCAN/DroneCAN.c | 13 ++++- bootloader/DroneCAN/DroneCAN.h | 1 + bootloader/main.c | 89 +++++++++++++++++++++++++++++----- 3 files changed, 91 insertions(+), 12 deletions(-) diff --git a/bootloader/DroneCAN/DroneCAN.c b/bootloader/DroneCAN/DroneCAN.c index 216f6768..f62325dc 100644 --- a/bootloader/DroneCAN/DroneCAN.c +++ b/bootloader/DroneCAN/DroneCAN.c @@ -63,7 +63,14 @@ static struct { uint32_t offset; } fwupdate; -static bool have_raw_command; +// set from the CAN RX ISR (RawCommand) and read in the boot decision, so +// volatile (especially with -flto cross-TU optimisation) +static volatile bool have_raw_command; + +void DroneCAN_set_have_signal(void) +{ + have_raw_command = true; +} // some convenience macros #define MIN(a,b) ((a)<(b)?(a):(b)) @@ -208,6 +215,10 @@ static void handle_param_ExecuteOpcode(CanardInstance* ins, CanardRxTransfer* tr can_print("resetting to defaults"); save_flash_nolib(default_settings, sizeof(default_settings), EEPROM_START_ADD); pkt.ok = true; + } else if (req.opcode == UAVCAN_PROTOCOL_PARAM_EXECUTEOPCODE_REQUEST_OPCODE_SAVE) { + // GetSet writes parameters straight to flash, so SAVE is a no-op; ack it + // so DroneCAN parameter clients that send SAVE see success. + pkt.ok = true; } diff --git a/bootloader/DroneCAN/DroneCAN.h b/bootloader/DroneCAN/DroneCAN.h index 2e865bf2..ce28aa08 100644 --- a/bootloader/DroneCAN/DroneCAN.h +++ b/bootloader/DroneCAN/DroneCAN.h @@ -6,5 +6,6 @@ void DroneCAN_Init(void); bool DroneCAN_update(); bool DroneCAN_boot_ok(void); +void DroneCAN_set_have_signal(void); #endif // DRONECAN_SUPPORT diff --git a/bootloader/main.c b/bootloader/main.c index f7b07a95..e746dc45 100644 --- a/bootloader/main.c +++ b/bootloader/main.c @@ -245,6 +245,13 @@ static bool messagereceived; static int cmd; static int received; static bool initialized; +/* + set once a configuration client has connected (asked for deviceInfo). While + set we stop polling DroneCAN, because DroneCAN_boot_ok() does a multi-ms + crc32 over the whole firmware that blocks the bit-banged serial and corrupts + 4-way reads. A config session always ends in a reset, which clears this. + */ +static bool bl_serial_active; static uint8_t rxBuffer[258]; static uint8_t payLoadBuffer[256]; static uint8_t rxbyte; @@ -411,9 +418,15 @@ static void setTransmit() static void serialwriteOneChar(uint8_t c) { +#if DRONECAN_SUPPORT + sys_can_disable_IRQ(); +#endif setTransmit(); serialwriteChar(c); setReceive(); +#if DRONECAN_SUPPORT + sys_can_enable_IRQ(); +#endif } static void send_ACK() @@ -438,6 +451,9 @@ static void sendDeviceInfo() { sendString(devinfo.deviceInfo,sizeof(devinfo.deviceInfo)); initialized = true; + // a config client is connected; stop DroneCAN polling so its firmware-CRC + // scan can't block the bit-banged serial during this session + bl_serial_active = true; } static bool checkAddressWritable(uint32_t address) @@ -715,23 +731,33 @@ static bool serialreadChar() // now we need to wait for the start bit leading edge, which is low bl_timer_reset(); while (gpio_read(input_pin)) { - if (bl_timer_elapsed() > 5*BITTIME) { + uint16_t elapsed = bl_timer_elapsed(); + if (messagereceived && elapsed > 5*BITTIME) { + // we've been waiting too long, don't allow for long gaps + // between bytes +#ifdef SERIAL_STATS + stats.no_start++; +#endif + return false; + } #if DRONECAN_SUPPORT + // Check DroneCAN every ~50ms when waiting for first byte. + // DroneCAN_boot_ok() scans flash (~2-5ms per call), so we + // rate-limit to avoid blocking serial start-bit detection. + if (!bl_serial_active && !messagereceived && elapsed > 50000) { if (DroneCAN_update()) { jump(); } -#endif - if (messagereceived) { - // we've been waiting too long, don't allow for long gaps - // between bytes -#ifdef SERIAL_STATS - stats.no_start++; -#endif - return false; - } + bl_timer_reset(); } +#endif } + // start bit detected - disable CAN IRQs to protect bit-banged timing +#if DRONECAN_SUPPORT + sys_can_disable_IRQ(); +#endif + // wait to get the center of bit time. We want to sample at the // middle of each bit delayMicroseconds(HALFBITTIME); @@ -740,6 +766,9 @@ static bool serialreadChar() // which should still be low #ifdef SERIAL_STATS stats.bad_start++; +#endif +#if DRONECAN_SUPPORT + sys_can_enable_IRQ(); #endif return false; } @@ -760,10 +789,18 @@ static bool serialreadChar() // bad framing, stop bit should be high #ifdef SERIAL_STATS stats.bad_stop++; +#endif +#if DRONECAN_SUPPORT + sys_can_enable_IRQ(); #endif return false; } + // re-enable CAN IRQs after byte is complete +#if DRONECAN_SUPPORT + sys_can_enable_IRQ(); +#endif + // we got a good byte messagereceived = true; receiveByte = rxbyte; @@ -808,6 +845,9 @@ static void serialwriteChar(uint8_t data) static void sendString(const uint8_t *data, int len) { +#if DRONECAN_SUPPORT + sys_can_disable_IRQ(); +#endif setTransmit(); for (int i = 0; i < len; i++) { serialwriteChar(data[i]); @@ -815,6 +855,9 @@ static void sendString(const uint8_t *data, int len) delayMicroseconds(BITTIME); } setReceive(); +#if DRONECAN_SUPPORT + sys_can_enable_IRQ(); +#endif } static void receiveBuffer() @@ -1242,6 +1285,30 @@ int main(void) test_rtc_backup(); #endif +#if DRONECAN_SUPPORT + /* + If the signal pin is driven (e.g. DShot), tell DroneCAN so boot_ok() accepts + the jump. Must run before checkForSignal(), whose float+low path calls jump() + unconditionally - otherwise jump() is rejected with have_raw_command false + and a DShot-only boot bounces. + */ + { + gpio_mode_set_input(input_pin, GPIO_PULL_UP); + delayMicroseconds(500); + bool has_pin_signal = false; + for (int i = 0; i < 500; i++) { + if (!gpio_read(input_pin)) { + has_pin_signal = true; + break; + } + delayMicroseconds(10); + } + if (has_pin_signal) { + DroneCAN_set_have_signal(); + } + } +#endif + checkForSignal(); gpio_mode_set_input(input_pin, GPIO_PULL_UP); @@ -1278,7 +1345,7 @@ int main(void) jump(); } #if DRONECAN_SUPPORT - if (DroneCAN_update()) { + if (!bl_serial_active && DroneCAN_update()) { jump(); } #endif From 983b1f85046d81cd666a4b45831bc78f15c07931 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 03/25] bootloader: v3 deviceInfo with devinfo magic address Add bootloader protocol version 3. The 9-byte deviceInfo from sendDeviceInfo() is unchanged, so pre-v3 clients see no wire change. v3 data lives in a packed self-describing struct read via ADDRESS_MAGIC_DEVINFO (0x23, a SET_ADDRESS magic), so a client can read it even over a 4-way passthrough. It returns magic1/magic2, the 9-byte deviceInfo, then length, address_shift and the firmware/filename/eeprom/tune region addresses (stored >> address_shift). Also fix FIRMWARE_RELATIVE_START to test DRONECAN_SUPPORT by value, not defined() - it is always defined 0/1, so non-CAN builds were wrongly getting 0x4000. --- bootloader/main.c | 52 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/bootloader/main.c b/bootloader/main.c index e746dc45..14153126 100644 --- a/bootloader/main.c +++ b/bootloader/main.c @@ -73,6 +73,8 @@ #endif #ifndef FIRMWARE_RELATIVE_START +// note: DRONECAN_SUPPORT is always defined (0 or 1) by the build, so test its +// value rather than defined(), to match the convention used elsewhere #if defined(MCXA153) || DRONECAN_SUPPORT #define FIRMWARE_RELATIVE_START 0x4000 #else @@ -185,8 +187,11 @@ static uint16_t invalid_command; a bootloader protocol version, sent as byte 8 in the deviceInfo this should change when the configurator applications need to know about a changed feature set in the bootloader + + v2: magic flash addresses (ADDRESS_MAGIC_EEPROM, ADDRESS_MAGIC_FILE_NAME) supported + v3: supports ADDRESS_MAGIC_DEVINFO */ -#define BOOTLOADER_PROTOCOL_VERSION 2 +#define BOOTLOADER_PROTOCOL_VERSION 3 /* the devinfo structure tells the configuration client our pin code, @@ -198,14 +203,40 @@ static uint16_t invalid_command; #define DEVINFO_MAGIC1 0x5925e3da #define DEVINFO_MAGIC2 0x4eb863d9 -static const struct { +static const struct __attribute__((packed)) { uint32_t magic1; uint32_t magic2; + /* + deviceInfo bytes: '4','7','1', pin code, flash size code, 0x06, 0x06, + protocol version, 0x30 + */ const uint8_t deviceInfo[9]; + /* + for (protocol version >= 3) we have additional information which can be fetched via ADDRESS_MAGIC_DEVINFO + */ + uint8_t length; + uint8_t address_shift; // this is 2 on some MCUs + /* + the following uint16_t start addresses are the addresses that need + to be passed to CMD_SET_ADDRESS to get each of the respective + areas. Note that these are shifted addresses if address_shift is + non-zero. This keeps the values within the limitation of the 16 + bit address in the protocol + */ + uint16_t firmware_start; + uint16_t filename_start; + uint16_t eeprom_start; + uint16_t tune_start; } devinfo __attribute__((section(".devinfo"))) = { .magic1 = DEVINFO_MAGIC1, .magic2 = DEVINFO_MAGIC2, - .deviceInfo = {'4','7','1',PIN_CODE,FLASH_SIZE_CODE,0x06,0x06,BOOTLOADER_PROTOCOL_VERSION,0x30} + .deviceInfo = {'4','7','1',PIN_CODE,FLASH_SIZE_CODE,0x06,0x06,BOOTLOADER_PROTOCOL_VERSION,0x30}, + sizeof(devinfo), + ADDRESS_SHIFT, + (uint16_t)(FIRMWARE_RELATIVE_START >> ADDRESS_SHIFT), // firmware_start + (uint16_t)((EEPROM_START_ADD - 32) >> ADDRESS_SHIFT), // filename_start + (uint16_t)(EEPROM_START_ADD >> ADDRESS_SHIFT), // eeprom_start + (uint16_t)((EEPROM_START_ADD + 48U) >> ADDRESS_SHIFT) // tune_start }; typedef void (*pFunction)(void); @@ -225,6 +256,16 @@ typedef void (*pFunction)(void); // magic address for continue transfer from last read #define ADDRESS_MAGIC_CONTINUE 0x22 +/* + magic address that maps to the devinfo structure in flash, so a + configuration client can READ the full deviceInfo (including the protocol + version and firmware start) even over a 4-way passthrough that only forwards + a short signature in the InitFlash reply. The read returns magic1, magic2 + then the deviceInfo bytes, so the client can confirm support via the magic + values. Supported for BOOTLOADER_PROTOCOL_VERSION 3 and later. + */ +#define ADDRESS_MAGIC_DEVINFO 0x23 + #define CMD_RUN 0x00 #define CMD_PROG_FLASH 0x01 @@ -576,6 +617,11 @@ static void decodeInput() } else if (address == ADDRESS_MAGIC_CONTINUE) { // allow easy continue from last address, for breaking up eeprom into multiple small reads address = continue_address; + } else if (address == ADDRESS_MAGIC_DEVINFO) { + // config app has requested the devinfo structure (magic1, magic2, + // deviceInfo). Lets the client read the protocol version and firmware + // start over a 4-way link that doesn't forward the full deviceInfo. + address = (uint32_t)(uintptr_t)&devinfo; } else if (address < 1024) { // other addresses below 1024 are reserved for future magic values send_BAD_ACK(); From 6b78cab7730f40cf8b024f2492157092f941ddf1 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 04/25] build: data-driven custom board targets from Inc/targets.h Each board is a "#ifdef " block (a subset of the main firmware's targets.h). make/parse_targets.py scans the file and emits MCU, _CAN flag and TARGET_TAG per block; the Makefile feeds these to CREATE_BOOTLOADER_TARGET, so ARK_G431_CAN generates AM32_G431_BOOTLOADER_ARKG4_CAN. targets.h is force-included and inert with no board define, so generic targets are unchanged. First board: the ARK G4 (FDCAN TX on PB9, RGB LED, 8MHz HSE, 112KB RAM). --- Inc/targets.h | 64 +++++++++++++++++++++++++ Makefile | 16 ++++++- make/parse_targets.py | 109 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 Inc/targets.h create mode 100644 make/parse_targets.py diff --git a/Inc/targets.h b/Inc/targets.h new file mode 100644 index 00000000..c8c7f868 --- /dev/null +++ b/Inc/targets.h @@ -0,0 +1,64 @@ +/* + per-board bootloader configuration. + + Each board is a "#ifdef ... #endif" block, named and formatted as + a subset of the matching board block in the main AM32 firmware repo + (../AM32/Inc/targets.h). The build system (make/parse_targets.py) scans this + file and creates one bootloader target per board: + + AM32__BOOTLOADER__CAN + + e.g. the ARK_G431_CAN block below generates AM32_G431_BOOTLOADER_ARKG4_CAN. + Building that target passes -D (here -DARK_G431_CAN), which + activates this board's block. targets.h is force-included into every + translation unit, so with no board define every block expands to nothing and + generic builds are unaffected. + + Required fields per board: + FILE_NAME - quoted board name; the MCU is the token matching a known MCU + family (G431, L431, ...) and a trailing _CAN marks a CAN build + TARGET_TAG - short tag placed in the build-target name (the "pin" slot) + USE_P - the bit-banged comms pin (e.g. USE_PB4) + + Optional fields (see the main firmware repo for the full set): + CAN_RX_PORT/CAN_RX_PIN, CAN_TX_PORT/CAN_TX_PIN - FDCAN1 pins (default PA11/PA12) + USE_RGB_LED + RED_PORT/RED_PIN, GREEN_PORT/GREEN_PIN, BLUE_PORT/BLUE_PIN + USE_HSE / HSE_VALUE / USE_HSE_BYPASS - external oscillator + RAM_LIMIT_KB - max app stack-pointer RAM (default 64) + + BOARD_FLASH_SIZE=128, DRONECAN_SUPPORT=1 and AM32_MCU come automatically from + the generated _CAN build, so they need not be repeated here. + + To disable a board without deleting it, comment out its FILE_NAME line with // + or add DISABLE_BUILD to it (matching the main firmware repo convention). +*/ + +#ifdef ARK_G431_CAN +#define FILE_NAME "ARK_G431_CAN" // parser: MCU=G431, CAN build +#define TARGET_TAG ARKG4 // -> AM32_G431_BOOTLOADER_ARKG4_CAN +#define USE_PB4 // bit-banged comms pin + +// FDCAN1 pins: RX PA11, TX PB9 (AF9) +#define CAN_RX_PORT GPIOA +#define CAN_RX_PIN LL_GPIO_PIN_11 +#define CAN_TX_PORT GPIOB +#define CAN_TX_PIN LL_GPIO_PIN_9 + +// RGB LED on PC6 (red), PC7 (green), PC8 (blue), active low +#define USE_RGB_LED +#define RED_PORT GPIOC +#define RED_PIN LL_GPIO_PIN_6 +#define GREEN_PORT GPIOC +#define GREEN_PIN LL_GPIO_PIN_7 +#define BLUE_PORT GPIOC +#define BLUE_PIN LL_GPIO_PIN_8 + +// 8MHz external oscillator (bypass mode) +#define USE_HSE +#undef HSE_VALUE +#define HSE_VALUE 8000000 +#define USE_HSE_BYPASS 0 + +// G491 has 112KB RAM; allow app stack pointer above the 64KB default +#define RAM_LIMIT_KB 112 +#endif diff --git a/Makefile b/Makefile index 906f5e3a..567d2a90 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,13 @@ endef MCU_TYPES := $(sort $(foreach mcu,$(MCU_BUILDS),$(call base_mcu,$(mcu)))) +# custom per-board targets defined in Inc/targets.h, parsed by make/parse_targets.py. +# BOARD_TARGETS entries are "BUILD|TAG|BOARDDEFINE", e.g. G431_CAN|ARKG4|ARK_G431_CAN +BOARD_TARGETS := $(shell python3 make/parse_targets.py builds) +BOARD_MCUS := $(shell python3 make/parse_targets.py mcus) +# make sure each custom board's per-MCU makefile gets included below +MCU_TYPES := $(sort $(MCU_TYPES) $(BOARD_MCUS)) + # Function to include makefile for each MCU type define INCLUDE_MCU_MAKEFILES $(foreach MCU_TYPE,$(MCU_TYPES),$(eval include $(call lc,$(MCU_TYPE))makefile.mk)) @@ -49,6 +56,8 @@ LIBS := -lnosys CFLAGS_BASE := -fsingle-precision-constant -fomit-frame-pointer -ffast-math --specs=nosys.specs CFLAGS_BASE += -I$(MAIN_INC_DIR) -g3 -Os -ffunction-sections -funsigned-char CFLAGS_BASE += -Wall -Wextra -Wundef -Werror -Wno-unused-parameter +# force-include per-board config; inert for builds with no board define +CFLAGS_BASE += -include $(MAIN_INC_DIR)/targets.h CFLAGS_COMMON := $(CFLAGS_BASE) @@ -137,6 +146,7 @@ BLU_BUILDS := define CREATE_BOOTLOADER_TARGET $(eval BUILD := $(1)) $(eval PIN := $(2)) +$(eval BOARD := $(3)) $(eval MCU := $$(call base_mcu,$$(1))) $(eval EXTRA_CFLAGS := $(call get_cflags,$(1))) $(eval ELF_FILE := $(BIN_DIR)/$(call BOOTLOADER_BASENAME_VER,$(BUILD),$(PIN)).elf) @@ -170,7 +180,7 @@ $(eval SRC_DRONECAN := $(if $(call has_can_suffix,$(1)),$(SRC_DRONECAN_$(MCU)))) -include $(DEP_FILE) -include $(BLU_DEP_FILE) -$(ELF_FILE): CFLAGS_BL := $$(MCU_$(MCU)) $$(CFLAGS_$(MCU)) $$(CFLAGS_BASE) -DBOOTLOADER -DUSE_$(PIN) $(EXTRA_CFLAGS) -DAM32_MCU=\"$(MCU)\" $$(CFLAGS_DRONECAN) $(xCFLAGS_ARM) $(xCFLAGS_4K) +$(ELF_FILE): CFLAGS_BL := $$(MCU_$(MCU)) $$(CFLAGS_$(MCU)) $$(CFLAGS_BASE) -DBOOTLOADER -DUSE_$(PIN) $(if $(BOARD),-D$(BOARD)) $(EXTRA_CFLAGS) -DAM32_MCU=\"$(MCU)\" $$(CFLAGS_DRONECAN) $(xCFLAGS_ARM) $(xCFLAGS_4K) $(ELF_FILE): LDFLAGS_BL := $$(LDFLAGS_COMMON) $$(LDFLAGS_$(MCU)) -T$(xLDSCRIPT) $(ELF_FILE): $$(SRC_$(MCU)_BL) $$(SRC_BL) $$(SRC_DRONECAN) $$(QUIET)echo building bootloader for $(BUILD) with pin $(PIN) @@ -184,7 +194,7 @@ $(ELF_FILE): $$(SRC_$(MCU)_BL) $$(SRC_BL) $$(SRC_DRONECAN) $(H_FILE): $(BIN_FILE) $$(QUIET)python3 bl_update/make_binheader.py $(BIN_FILE) $(H_FILE) -$(BLU_ELF_FILE): CFLAGS_BLU := -DAM32_MCU=\"$(MCU)\" $$(MCU_$(MCU)) $$(CFLAGS_$(MCU)) $$(CFLAGS_BASE) -DBOOTLOADER -DUSE_$(PIN) $(EXTRA_CFLAGS) -Wno-unused-variable -Wno-unused-function $(xCFLAGS_ARM) +$(BLU_ELF_FILE): CFLAGS_BLU := -DAM32_MCU=\"$(MCU)\" $$(MCU_$(MCU)) $$(CFLAGS_$(MCU)) $$(CFLAGS_BASE) -DBOOTLOADER -DUSE_$(PIN) $(if $(BOARD),-D$(BOARD)) $(EXTRA_CFLAGS) -Wno-unused-variable -Wno-unused-function $(xCFLAGS_ARM) $(BLU_ELF_FILE): LDFLAGS_BLU := $$(LDFLAGS_COMMON) $$(LDFLAGS_$(MCU)) -T$(xBLU_LDSCRIPT) $(BLU_ELF_FILE): $$(SRC_$(MCU)_BL) $$(SRC_BLU) $(H_FILE) $$(QUIET)echo building bootloader updater for $(BUILD) with pin $(PIN) @@ -279,6 +289,8 @@ endef # ARK 4IN1 F051: PB4 signal (same as generic PB4), PA15 = DRV8328 nSLEEP held low $(eval $(call CREATE_BOARD_BOOTLOADER_TARGET,F051,ARK4IN1,PB4,-DGATE_DRIVER_OFF_PORT=GPIOA -DGATE_DRIVER_OFF_PIN_NUM=15)) +# custom per-board targets from Inc/targets.h (BUILD|TAG|BOARDDEFINE) +$(foreach B,$(BOARD_TARGETS),$(eval $(call CREATE_BOOTLOADER_TARGET,$(word 1,$(subst |, ,$(B))),$(word 2,$(subst |, ,$(B))),$(word 3,$(subst |, ,$(B)))))) bootloaders: $(ALL_BUILDS) diff --git a/make/parse_targets.py b/make/parse_targets.py new file mode 100644 index 00000000..2a20ddc0 --- /dev/null +++ b/make/parse_targets.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +''' +parse Inc/targets.h and emit the custom board build targets for the Makefile. + +Each board is a "#ifdef ... #endif" block containing at least: + #define FILE_NAME "" the MCU is the name token matching a known MCU + family, a trailing _CAN marks a CAN build + #define TARGET_TAG short tag used in the generated target name + +For the ARK_G431_CAN block this produces the build target +AM32_G431_BOOTLOADER_ARKG4_CAN. + +Usage: + parse_targets.py builds -> one line per board: BUILD|TAG|BOARDDEFINE + e.g. G431_CAN|ARKG4|ARK_G431_CAN + parse_targets.py mcus -> unique MCU families used by custom boards +''' + +import os +import re +import sys + +# AM32 MCU families the bootloader knows about (matches Makefile MCU_BUILDS) +KNOWN_MCUS = ['E230', 'F031', 'F051', 'F415', 'F421', 'G071', 'G431', + 'L431', 'V203', 'A153'] + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +TARGETS_H = os.path.join(REPO_ROOT, 'Inc', 'targets.h') + + +def is_commented(line, marker): + '''true if marker appears only after a // comment on this line''' + idx = line.find(marker) + pre = line[:idx] + return '//' in pre + + +def parse_boards(path): + '''return list of dicts {guard, file_name, tag} for each board block''' + boards = [] + cur = None + depth = 0 + with open(path) as f: + for line in f: + s = line.strip() + if s.startswith('#ifdef') or s.startswith('#ifndef') or s.startswith('#if'): + depth += 1 + if depth == 1 and s.startswith('#ifdef'): + parts = s.split() + cur = {'guard': parts[1], 'file_name': None, 'tag': None} + continue + if s.startswith('#endif'): + if depth == 1 and cur is not None: + if cur['file_name']: + boards.append(cur) + cur = None + depth = max(0, depth - 1) + continue + if cur is None: + continue + if '#define' in line and 'FILE_NAME' in line: + if is_commented(line, '#define') or 'DISABLE_BUILD' in line: + continue + m = re.search(r'"([^"]+)"', line) + if m: + cur['file_name'] = m.group(1) + elif '#define' in line and 'TARGET_TAG' in line: + if is_commented(line, '#define'): + continue + m = re.search(r'#define\s+TARGET_TAG\s+(\S+)', line) + if m: + cur['tag'] = m.group(1) + return boards + + +def board_build(board): + '''return (build, tag, boarddefine) or None if the MCU is unrecognised''' + tokens = board['file_name'].split('_') + mcu = next((t for t in tokens if t in KNOWN_MCUS), None) + if mcu is None: + sys.stderr.write( + "parse_targets.py: no known MCU in FILE_NAME '%s', skipping\n" + % board['file_name']) + return None + is_can = 'CAN' in tokens + build = mcu + ('_CAN' if is_can else '') + tag = board['tag'] if board['tag'] else board['file_name'] + return (build, tag, board['guard']) + + +def main(): + mode = sys.argv[1] if len(sys.argv) > 1 else 'builds' + if not os.path.exists(TARGETS_H): + return + builds = [b for b in (board_build(x) for x in parse_boards(TARGETS_H)) if b] + if mode == 'mcus': + mcus = [] + for build, _, _ in builds: + mcu = build.split('_')[0] + if mcu not in mcus: + mcus.append(mcu) + print(' '.join(mcus)) + else: # builds + for build, tag, define in builds: + print('%s|%s|%s' % (build, tag, define)) + + +if __name__ == '__main__': + main() From f2c0d7c273367e64ac4ef143f465de8b347338b4 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 05/25] bootloader: consume per-board CAN pins, RGB LED and RAM limit Wire the bootloader to the per-board macros from Inc/targets.h: - sys_can_stm32_CANFD.c: map CAN_RX/TX_PORT/_PIN onto the FDCAN_* names so a board can use non-default pins (ARK uses TX on PB9). - Mcu/g431/Inc/blutil.h: RGB LED support (open drain, active low) keyed on RED/GREEN/BLUE_PORT/_PIN. - main.c: LED hooks (no-op when !USE_RGB_LED) and generalise the jump() RAM check to RAM_LIMIT_KB (default 64) so the ARK's 112KB accepts an app whose stack sits above 64KB. --- Mcu/g431/Inc/blutil.h | 63 +++++++++++++++++++++++ bootloader/DroneCAN/sys_can_stm32_CANFD.c | 9 ++++ bootloader/main.c | 56 +++++++++++++++++++- 3 files changed, 127 insertions(+), 1 deletion(-) diff --git a/Mcu/g431/Inc/blutil.h b/Mcu/g431/Inc/blutil.h index dab80293..2a5fd6fd 100644 --- a/Mcu/g431/Inc/blutil.h +++ b/Mcu/g431/Inc/blutil.h @@ -175,6 +175,69 @@ static inline void bl_gpio_init(void) LL_GPIO_Init(input_port, &GPIO_InitStruct); } +/* + RGB LED support, driven by per-board RED/GREEN/BLUE_PORT/_PIN from + Inc/targets.h (active low, open drain). Pins are LL_GPIO_PIN_x masks. + */ +#ifdef USE_RGB_LED +static inline void bl_led_port_clock(GPIO_TypeDef *port) +{ +#ifdef GPIOA + if (port == GPIOA) { LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOA); return; } +#endif +#ifdef GPIOB + if (port == GPIOB) { LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOB); return; } +#endif +#ifdef GPIOC + if (port == GPIOC) { LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOC); return; } +#endif +#ifdef GPIOD + if (port == GPIOD) { LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOD); return; } +#endif +#ifdef GPIOF + if (port == GPIOF) { LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOF); return; } +#endif +} + +static inline void bl_led_init(void) +{ + bl_led_port_clock(RED_PORT); + bl_led_port_clock(GREEN_PORT); + bl_led_port_clock(BLUE_PORT); + LL_GPIO_SetPinMode(RED_PORT, RED_PIN, LL_GPIO_MODE_OUTPUT); + LL_GPIO_SetPinOutputType(RED_PORT, RED_PIN, LL_GPIO_OUTPUT_OPENDRAIN); + LL_GPIO_SetPinMode(GREEN_PORT, GREEN_PIN, LL_GPIO_MODE_OUTPUT); + LL_GPIO_SetPinOutputType(GREEN_PORT, GREEN_PIN, LL_GPIO_OUTPUT_OPENDRAIN); + LL_GPIO_SetPinMode(BLUE_PORT, BLUE_PIN, LL_GPIO_MODE_OUTPUT); + LL_GPIO_SetPinOutputType(BLUE_PORT, BLUE_PIN, LL_GPIO_OUTPUT_OPENDRAIN); + // LEDs off (open drain high = hi-Z = off) + LL_GPIO_SetOutputPin(RED_PORT, RED_PIN); + LL_GPIO_SetOutputPin(GREEN_PORT, GREEN_PIN); + LL_GPIO_SetOutputPin(BLUE_PORT, BLUE_PIN); +} + +static inline void bl_led_on(void) +{ + LL_GPIO_ResetOutputPin(RED_PORT, RED_PIN); + LL_GPIO_ResetOutputPin(GREEN_PORT, GREEN_PIN); + LL_GPIO_ResetOutputPin(BLUE_PORT, BLUE_PIN); +} + +static inline void bl_led_off(void) +{ + LL_GPIO_SetOutputPin(RED_PORT, RED_PIN); + LL_GPIO_SetOutputPin(GREEN_PORT, GREEN_PIN); + LL_GPIO_SetOutputPin(BLUE_PORT, BLUE_PIN); +} + +static inline void bl_led_red_on(void) +{ + LL_GPIO_ResetOutputPin(RED_PORT, RED_PIN); + LL_GPIO_SetOutputPin(GREEN_PORT, GREEN_PIN); + LL_GPIO_SetOutputPin(BLUE_PORT, BLUE_PIN); +} +#endif // USE_RGB_LED + /* return true if the MCU booted under a software reset */ diff --git a/bootloader/DroneCAN/sys_can_stm32_CANFD.c b/bootloader/DroneCAN/sys_can_stm32_CANFD.c index 2074824c..e4a60c2e 100644 --- a/bootloader/DroneCAN/sys_can_stm32_CANFD.c +++ b/bootloader/DroneCAN/sys_can_stm32_CANFD.c @@ -382,6 +382,15 @@ static void can_init(void) void sys_can_init(void) { // Setup CAN RX and TX pins + // map main-firmware-style CAN pin names (from Inc/targets.h) onto FDCAN_* +#if defined(CAN_RX_PORT) && !defined(FDCAN_RX_PORT) +#define FDCAN_RX_PORT CAN_RX_PORT +#define FDCAN_RX_PIN CAN_RX_PIN +#endif +#if defined(CAN_TX_PORT) && !defined(FDCAN_TX_PORT) +#define FDCAN_TX_PORT CAN_TX_PORT +#define FDCAN_TX_PIN CAN_TX_PIN +#endif #ifndef FDCAN_RX_PORT #define FDCAN_RX_PORT GPIOA #define FDCAN_RX_PIN LL_GPIO_PIN_11 diff --git a/bootloader/main.c b/bootloader/main.c index 14153126..911f7913 100644 --- a/bootloader/main.c +++ b/bootloader/main.c @@ -129,6 +129,49 @@ static uint16_t invalid_command; #include +// default no-op LED functions if not provided by blutil.h (USE_RGB_LED) +#ifndef USE_RGB_LED +static inline void bl_led_init(void) {} +static inline void bl_led_on(void) {} +static inline void bl_led_off(void) {} +static inline void bl_led_red_on(void) {} +#endif + +#ifdef USE_RGB_LED +/* + blink RGB LEDs while stuck in the bootloader + - normal: blink all LEDs at ~2.5Hz + - error: blink red only + */ +static uint16_t led_timer_start; +static uint8_t led_blink_counter; +static bool led_error_mode; + +static void __attribute__((unused)) bl_led_set_error(bool error) +{ + led_error_mode = error; +} + +static void bl_led_update(void) +{ + const uint16_t now = bl_timer_us(); + if ((uint16_t)(now - led_timer_start) < 25000U) { + return; + } + led_timer_start = now; + led_blink_counter++; + if (led_blink_counter & 0x08) { + if (led_error_mode) { + bl_led_red_on(); + } else { + bl_led_on(); + } + } else { + bl_led_off(); + } +} +#endif // USE_RGB_LED + #if DRONECAN_SUPPORT #include "DroneCAN/DroneCAN.h" #include "DroneCAN/sys_can.h" @@ -375,7 +418,10 @@ static void jump() */ const uint32_t *app = (uint32_t*)(MCU_FLASH_START + FIRMWARE_RELATIVE_START); const uint32_t ram_start = 0x20000000; - const uint32_t ram_limit_kb = 64; +#ifndef RAM_LIMIT_KB +#define RAM_LIMIT_KB 64 +#endif + const uint32_t ram_limit_kb = RAM_LIMIT_KB; const uint32_t ram_end = ram_start+ram_limit_kb*1024; if (app[0] < ram_start || app[0] > ram_end) { invalid_command = 0; @@ -396,12 +442,16 @@ static void jump() #if DRONECAN_SUPPORT if (!DroneCAN_boot_ok()) { invalid_command = 0; +#ifdef USE_RGB_LED + bl_led_set_error(true); +#endif return; } sys_can_disable_IRQ(); #endif + bl_led_off(); jump_to_application(); #endif } @@ -796,6 +846,9 @@ static bool serialreadChar() } bl_timer_reset(); } +#endif +#ifdef USE_RGB_LED + bl_led_update(); #endif } @@ -1320,6 +1373,7 @@ int main(void) /* DRV8328 nSLEEP (etc.): keep the gate driver asleep while in the BL */ bl_gate_driver_off(); #endif + bl_led_init(); #ifdef BOOTLOADER_TEST_CLOCK test_clock(); From 7fe25af1f6a96fe685b416ca964f93fed7409e93 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Wed, 27 May 2026 13:54:29 +1000 Subject: [PATCH 06/25] bootloader: clear bl_serial_active ~10s after last good 4-way command A config client talking over the bit-banged 4-way serial sets bl_serial_active to suppress DroneCAN polling, whose multi-ms firmware-CRC scan would otherwise corrupt the serial reads. Mark it on any validated (good-CRC, known) 4-way command - not just the deviceInfo probe - so a client that skips the handshake is protected from its first command, and CMD_KEEP_ALIVE refreshes it so a client can hold the session open. Recover after BL_SERIAL_IDLE_THRESHOLD (~10s, was ~5min) of silence so DroneCAN polling resumes quickly when the client goes away, with no reset required. --- bootloader/main.c | 57 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/bootloader/main.c b/bootloader/main.c index 911f7913..0587e6f4 100644 --- a/bootloader/main.c +++ b/bootloader/main.c @@ -330,12 +330,38 @@ static int cmd; static int received; static bool initialized; /* - set once a configuration client has connected (asked for deviceInfo). While + set whenever a validated (good-CRC, known) 4-way command is processed. While set we stop polling DroneCAN, because DroneCAN_boot_ok() does a multi-ms crc32 over the whole firmware that blocks the bit-banged serial and corrupts - 4-way reads. A config session always ends in a reset, which clears this. + 4-way reads. Marking on every good command (not just the deviceInfo probe) + protects even a client that skips the deviceInfo handshake from the first + command onward. The start-bit-wait loop clears this after + BL_SERIAL_IDLE_THRESHOLD ticks (~10s) without a good command, so DroneCAN + polling resumes quickly once the client goes away - no reset required. See + serialreadChar(), mark_serial_active() and BL_SERIAL_IDLE_THRESHOLD. */ static bool bl_serial_active; +#if DRONECAN_SUPPORT +// counter of consecutive ~50ms idle ticks while bl_serial_active is set. +// Zeroed by mark_serial_active() on every good 4-way command and by any +// received byte, so it only climbs during true post-session silence. +static uint16_t bl_serial_idle_count; +// ~10 seconds of silence after the last good 4-way command before we assume the +// client has gone and resume DroneCAN polling. Short enough for fast recovery, +// long enough to bridge the gaps between commands in an active session. +// 200 ticks * 50ms = 10s. +#define BL_SERIAL_IDLE_THRESHOLD 200U +#endif + +// mark that a validated 4-way command was just handled: suppress DroneCAN +// polling for this config session and restart the idle timer. +static void mark_serial_active(void) +{ + bl_serial_active = true; +#if DRONECAN_SUPPORT + bl_serial_idle_count = 0; +#endif +} static uint8_t rxBuffer[258]; static uint8_t payLoadBuffer[256]; static uint8_t rxbyte; @@ -524,6 +550,8 @@ static void send_ACK() { serialwriteOneChar(0x30); // good ack! invalid_command = 0; + // an ACK is only sent for a validated command; keep DroneCAN suppressed + mark_serial_active(); } static void send_BAD_ACK() @@ -544,7 +572,7 @@ static void sendDeviceInfo() initialized = true; // a config client is connected; stop DroneCAN polling so its firmware-CRC // scan can't block the bit-banged serial during this session - bl_serial_active = true; + mark_serial_active(); } static bool checkAddressWritable(uint32_t address) @@ -696,6 +724,7 @@ static void decodeInput() return; } + mark_serial_active(); // no ack with command set buffer; if (rxBuffer[2] == 0x01) { @@ -717,6 +746,7 @@ static void decodeInput() return; } + mark_serial_active(); serialwriteOneChar(0xC1); // bad command message. return; @@ -751,6 +781,7 @@ static void decodeInput() return; } + mark_serial_active(); if (address == 0) { // must send SET_ADDRESS first @@ -840,8 +871,21 @@ static bool serialreadChar() // Check DroneCAN every ~50ms when waiting for first byte. // DroneCAN_boot_ok() scans flash (~2-5ms per call), so we // rate-limit to avoid blocking serial start-bit detection. - if (!bl_serial_active && !messagereceived && elapsed > 50000) { - if (DroneCAN_update()) { + if (!messagereceived && elapsed > 50000) { + if (bl_serial_active) { + /* + a config client is mid-session: a good 4-way command set + bl_serial_active (see mark_serial_active()) so DroneCAN polling + stays suppressed. The idle counter is zeroed on every good command + and any received byte, so it only climbs during real silence. After + BL_SERIAL_IDLE_THRESHOLD ticks (~10s) without a command we assume the + client is gone and resume polling - no reset required. + */ + if (++bl_serial_idle_count >= BL_SERIAL_IDLE_THRESHOLD) { + bl_serial_active = false; + bl_serial_idle_count = 0; + } + } else if (DroneCAN_update()) { jump(); } bl_timer_reset(); @@ -902,6 +946,9 @@ static bool serialreadChar() // we got a good byte messagereceived = true; +#if DRONECAN_SUPPORT + bl_serial_idle_count = 0; +#endif receiveByte = rxbyte; #ifdef SERIAL_STATS stats.good++; From bbfcae9740a3b1a8f3717ce8c5204224cf302ef2 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 07/25] targets: add SEQURE_G431_CAN, TBS_12S/16S_L431_CAN, TBS_12S_F415_CAN, TBS_F415_CAN, and ARK CAN_TERM_PIN Add five custom-board blocks mirroring the like-named blocks in the main AM32 firmware, back-fill ARK_G431_CAN with CAN_TERM_PIN, and document the new oscillator/termination fields. Each block carries only what the bootloader consumes: FILE_NAME/TARGET_TAG, the USE_P comms pin, CAN_TERM_PIN/POLARITY (ARK + the three TBS boards), and oscillator selection (SEQURE 8MHz HSE; TBS_12S LSE crystal, TBS_16S LSE bypass). Motor-control/ADC/telemetry fields and the default PA11/PA12 CAN pins are omitted. --- Inc/targets.h | 102 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/Inc/targets.h b/Inc/targets.h index c8c7f868..316073d3 100644 --- a/Inc/targets.h +++ b/Inc/targets.h @@ -22,8 +22,23 @@ Optional fields (see the main firmware repo for the full set): CAN_RX_PORT/CAN_RX_PIN, CAN_TX_PORT/CAN_TX_PIN - FDCAN1 pins (default PA11/PA12) + CAN_TERM_PIN / CAN_TERM_POLARITY - CAN termination pin, driven + from EEPROM byte 183 + (eepromBuffer.can.term_enable) + when the bootloader starts + DroneCAN. Encode the pin with + GPIO_PORT_PIN(portnum, pinnum) + where portnum is 0=A,1=B,2=C. USE_RGB_LED + RED_PORT/RED_PIN, GREEN_PORT/GREEN_PIN, BLUE_PORT/BLUE_PIN - USE_HSE / HSE_VALUE / USE_HSE_BYPASS - external oscillator + USE_HSE / HSE_VALUE / USE_HSE_BYPASS - external high-speed + oscillator (see the + per-MCU Mcu//Inc/ + blutil.h bl_clock_config() + for which MCUs honour it) + USE_LSE / USE_LSE_BYPASS - external low-speed + 32.768 kHz crystal (L431) + USE_MSI - free-running MSI clock + (L431, no LSE/HSE) RAM_LIMIT_KB - max app stack-pointer RAM (default 64) BOARD_FLASH_SIZE=128, DRONECAN_SUPPORT=1 and AM32_MCU come automatically from @@ -33,6 +48,13 @@ or add DISABLE_BUILD to it (matching the main firmware repo convention). */ +/* + encode a (port, pin) pair as a single 16-bit value, used by CAN_TERM_PIN. + Matches the same-named macro in the main firmware's Inc/targets.h so per-board + blocks here can be byte-identical to their main-firmware counterparts. + */ +#define GPIO_PORT_PIN(portnum, pinnum) ((portnum)<<8|(pinnum)) + #ifdef ARK_G431_CAN #define FILE_NAME "ARK_G431_CAN" // parser: MCU=G431, CAN build #define TARGET_TAG ARKG4 // -> AM32_G431_BOOTLOADER_ARKG4_CAN @@ -53,6 +75,10 @@ #define BLUE_PORT GPIOC #define BLUE_PIN LL_GPIO_PIN_8 +// CAN termination pin on PC12, active high +#define CAN_TERM_PIN GPIO_PORT_PIN(2, 12) // PC12 +#define CAN_TERM_POLARITY 1 + // 8MHz external oscillator (bypass mode) #define USE_HSE #undef HSE_VALUE @@ -62,3 +88,77 @@ // G491 has 112KB RAM; allow app stack pointer above the 64KB default #define RAM_LIMIT_KB 112 #endif + +#ifdef SEQURE_G431_CAN +#define FILE_NAME "SEQURE_G431_CAN" // parser: MCU=G431, CAN build +#define TARGET_TAG SEQUREG4 // -> AM32_G431_BOOTLOADER_SEQUREG4_CAN +#define USE_PA2 // from HARDWARE_GROUP_G4_D (INPUT_PIN PA2) + +// FDCAN1 pins default to PA11/PA12; SEQURE uses the defaults, so no CAN_* +// overrides are needed here. + +// CAN termination pin on PB7, active high. PB7 -> AN of the ISOM8610 (U2), a +// normally-open opto-emulator switch across the 120R between CAN_H/CAN_L: PB7 +// high drives forward current AN->CAT, closing the switch (termination on). +#define CAN_TERM_PIN GPIO_PORT_PIN(1, 7) // PB7 +#define CAN_TERM_POLARITY 1 + +// 8MHz external HSE crystal (USE_HSE_BYPASS 0 = crystal, not oscillator) +#define USE_HSE +#undef HSE_VALUE +#define HSE_VALUE 8000000 +#define USE_HSE_BYPASS 0 +#endif + +#ifdef TBS_12S_L431_CAN +#define FILE_NAME "TBS_12S_L431_CAN" // parser: MCU=L431, CAN build +#define TARGET_TAG TBS12SL4 // -> AM32_L431_BOOTLOADER_TBS12SL4_CAN +#define USE_PA2 // from HARDWARE_GROUP_L4_C (INPUT_PIN PA2) + +// bxCAN pins are fixed at PA11/PA12 in the L431 bootloader CAN driver. + +// CAN termination pin on PB3, active high (see commit applying CAN_TERM) +#define CAN_TERM_PIN GPIO_PORT_PIN(1, 3) // PB3 +#define CAN_TERM_POLARITY 1 + +// LSE-disciplined MSI clock (32.768 kHz crystal, not bypass) +#define USE_LSE +#define USE_LSE_BYPASS 0 +#endif + +#ifdef TBS_16S_L431_CAN +#define FILE_NAME "TBS_16S_L431_CAN" // parser: MCU=L431, CAN build +#define TARGET_TAG TBS16SL4 // -> AM32_L431_BOOTLOADER_TBS16SL4_CAN +#define USE_PA2 // from HARDWARE_GROUP_L4_A (INPUT_PIN PA2) + +// bxCAN pins are fixed at PA11/PA12 in the L431 bootloader CAN driver. + +// CAN termination pin on PB3, active high +#define CAN_TERM_PIN GPIO_PORT_PIN(1, 3) // PB3 +#define CAN_TERM_POLARITY 1 + +// LSE-disciplined MSI clock (external 32.768 kHz clock, bypass mode) +#define USE_LSE +#define USE_LSE_BYPASS 1 +#endif + +#ifdef TBS_12S_F415_CAN +#define FILE_NAME "TBS_12S_F415_CAN" // parser: MCU=F415, CAN build +#define TARGET_TAG TBS12SF4 // -> AM32_F415_BOOTLOADER_TBS12SF4_CAN +#define USE_PB4 // from HARDWARE_GROUP_AT_D (INPUT_PIN PB4) + +// AT32 CAN pins are fixed at PA11/PA12 (CAN1_GMUX_0000) in the F415 driver. + +// CAN termination pin on PB3, active high +#define CAN_TERM_PIN GPIO_PORT_PIN(1, 3) // PB3 +#define CAN_TERM_POLARITY 1 +#endif + +#ifdef TBS_F415_CAN +#define FILE_NAME "TBS_F415_CAN" // parser: MCU=F415, CAN build +#define TARGET_TAG TBSF4 // -> AM32_F415_BOOTLOADER_TBSF4_CAN +#define USE_PA2 // from HARDWARE_GROUP_AT_H (INPUT_PIN PA2) + +// AT32 CAN pins are fixed at PA11/PA12 (CAN1_GMUX_0000) in the F415 driver. +// Main firmware block has no CAN_TERM_PIN, so we don't set one here either. +#endif From aded5adc96cadc0ab9b88b87159e95c2c0158131 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 08/25] bootloader: drive CAN_TERM_PIN from EEPROM byte 183 on DroneCAN start Boards that switch the CAN termination resistor through a GPIO left the pin floating during the bootloader window. Port setup_portpin() from the main firmware into the three CAN drivers (sys_can_stm32.c, sys_can_stm32_CANFD.c, sys_can_at32.c) and, in DroneCAN_Startup() after sys_can_init(), drive CAN_TERM_PIN from EEPROM byte 183 (eepromBuffer.can.term_enable). An out-of-[0,1]-range byte (0xff on a defaults-seeded EEPROM) falls back to disabled, matching the main firmware. Wrapped in #ifdef CAN_TERM_PIN, so boards without the pin pay nothing. --- bootloader/DroneCAN/DroneCAN.c | 20 +++++++++++++ bootloader/DroneCAN/sys_can.h | 8 ++++++ bootloader/DroneCAN/sys_can_at32.c | 28 +++++++++++++++++++ bootloader/DroneCAN/sys_can_stm32.c | 27 ++++++++++++++++++ bootloader/DroneCAN/sys_can_stm32_CANFD.c | 34 +++++++++++++++++++++++ 5 files changed, 117 insertions(+) diff --git a/bootloader/DroneCAN/DroneCAN.c b/bootloader/DroneCAN/DroneCAN.c index f62325dc..cf935486 100644 --- a/bootloader/DroneCAN/DroneCAN.c +++ b/bootloader/DroneCAN/DroneCAN.c @@ -764,6 +764,26 @@ static void DroneCAN_Startup(void) // initialise low level CAN peripheral hardware sys_can_init(); +#ifdef CAN_TERM_PIN + /* + apply the CAN bus termination setting from the EEPROM. Byte 183 is + eepromBuffer.can.term_enable (declared in ../AM32/Inc/eeprom.h). Mirror the + main firmware's load_settings(): an out-of-[0,1]-range byte falls back to the + default. default_settings[] only covers the first 48 bytes, so on a + defaults-seeded or erased EEPROM byte 183 reads 0xff -> default 0 (disabled), + matching the main firmware's default. + */ + { + const uint8_t *eeprom = (const uint8_t *)EEPROM_START_ADD; + uint8_t term = eeprom[183]; + if (term > 1) { + term = 0; // out of range -> default (disabled) + } + const bool term_enable = (eeprom[0] == 1) && (term != 0); + setup_portpin(CAN_TERM_PIN, term_enable ? CAN_TERM_POLARITY : !CAN_TERM_POLARITY); + } +#endif + #if 0 if (fwupdate.node_id != 0) { can_print("fwupdate startup"); diff --git a/bootloader/DroneCAN/sys_can.h b/bootloader/DroneCAN/sys_can.h index a6676144..d7aab5e3 100644 --- a/bootloader/DroneCAN/sys_can.h +++ b/bootloader/DroneCAN/sys_can.h @@ -43,6 +43,14 @@ void sys_can_getUniqueID(uint8_t id[16]); */ void sys_can_init(void); +/* + drive a static GPIO output (used for CAN bus termination). The portpin is + encoded with GPIO_PORT_PIN(portnum, pinnum) where portnum is 0=A,1=B,2=C. + Only defined for CAN builds; matches the same-named helper in the main AM32 + firmware so the bootloader and firmware drive CAN_TERM_PIN identically. + */ +void setup_portpin(uint16_t portpin, bool enable); + /* called from CAN IRQ indicating we may have a free TX slot */ diff --git a/bootloader/DroneCAN/sys_can_at32.c b/bootloader/DroneCAN/sys_can_at32.c index 6efba795..f1aed772 100644 --- a/bootloader/DroneCAN/sys_can_at32.c +++ b/bootloader/DroneCAN/sys_can_at32.c @@ -256,5 +256,33 @@ void set_rtc_backup_register(uint8_t idx, uint32_t value) ertc_bpr_data_write((ertc_dt_type)idx, value); } +/* + drive a static port/pin as an output, used for CAN bus termination. + Ported from ../AM32/Src/DroneCAN/sys_can_at32.c. AT32 uses its own + gpio API (gpio_init / scr / clr) so this can't share code with the + STM32 bxCAN driver. + */ +void setup_portpin(uint16_t portpin, bool enable) +{ + const uint8_t port = portpin >> 8; + const uint8_t pin = portpin & 0xff; + const uint32_t pinshift = 1U << pin; + gpio_type *pport = port == 0 ? GPIOA : GPIOB; + + if (enable) { + pport->scr = pinshift; + } else { + pport->clr = pinshift; + } + + gpio_init_type gpio_init_struct; + gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER; + gpio_init_struct.gpio_out_type = GPIO_OUTPUT_PUSH_PULL; + gpio_init_struct.gpio_mode = GPIO_MODE_OUTPUT; + gpio_init_struct.gpio_pins = pinshift; + gpio_init_struct.gpio_pull = GPIO_PULL_NONE; + gpio_init(pport, &gpio_init_struct); +} + #endif // DRONECAN_SUPPORT && defined(ARTERY) diff --git a/bootloader/DroneCAN/sys_can_stm32.c b/bootloader/DroneCAN/sys_can_stm32.c index 8482c19e..71e3eb3d 100644 --- a/bootloader/DroneCAN/sys_can_stm32.c +++ b/bootloader/DroneCAN/sys_can_stm32.c @@ -455,5 +455,32 @@ void set_rtc_backup_register(uint8_t idx, uint32_t value) bkp[idx] = value; } +/* + drive a static port/pin as an output, used for CAN bus termination. + Ported from ../AM32/Src/DroneCAN/sys_can_stm32.c. The L431 only needs + GPIOA / GPIOB for the boards currently shipping CAN_TERM_PIN; other + ports drop through silently to match the main firmware's helper. + */ +void setup_portpin(uint16_t portpin, bool enable) +{ + const uint8_t port = portpin >> 8; + const uint8_t pin = portpin & 0xff; + const uint32_t pinshift = 1U << pin; + GPIO_TypeDef *pport = port == 0 ? GPIOA : GPIOB; + + if (enable) { + LL_GPIO_SetOutputPin(pport, pinshift); + } else { + LL_GPIO_ResetOutputPin(pport, pinshift); + } + + LL_GPIO_InitTypeDef GPIO_InitStruct = {0}; + GPIO_InitStruct.Pin = pinshift; + GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT; + GPIO_InitStruct.Pull = LL_GPIO_PULL_NO; + GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_LOW; + LL_GPIO_Init(pport, &GPIO_InitStruct); +} + #endif // DRONECAN_SUPPORT && defined(MCU_L431) diff --git a/bootloader/DroneCAN/sys_can_stm32_CANFD.c b/bootloader/DroneCAN/sys_can_stm32_CANFD.c index e4a60c2e..4fd4e7be 100644 --- a/bootloader/DroneCAN/sys_can_stm32_CANFD.c +++ b/bootloader/DroneCAN/sys_can_stm32_CANFD.c @@ -444,4 +444,38 @@ void set_rtc_backup_register(uint8_t idx, uint32_t value) bkp[idx] = value; } +/* + drive a static port/pin as an output, used for CAN bus termination. + portpin is encoded as (portnum << 8) | pinnum via GPIO_PORT_PIN(). + Ported from ../AM32/Src/DroneCAN/sys_can_stm32_CANFD.c so the bootloader + applies CAN_TERM_PIN identically to the main firmware. + */ +void setup_portpin(uint16_t portpin, bool enable) +{ + const uint8_t port = portpin >> 8; + const uint8_t pin = portpin & 0xff; + const uint32_t pinshift = 1U << pin; + GPIO_TypeDef *const ports[] = { GPIOA, GPIOB, GPIOC }; + if (port >= sizeof(ports)/sizeof(ports[0])) { + return; + } + GPIO_TypeDef *pport = ports[port]; + + LL_AHB2_GRP1_EnableClock(1U << port); + + if (enable) { + LL_GPIO_SetOutputPin(pport, pinshift); + } else { + LL_GPIO_ResetOutputPin(pport, pinshift); + } + + LL_GPIO_InitTypeDef GPIO_InitStruct = {0}; + GPIO_InitStruct.Pin = pinshift; + GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT; + GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_LOW; + GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL; + GPIO_InitStruct.Pull = LL_GPIO_PULL_NO; + LL_GPIO_Init(pport, &GPIO_InitStruct); +} + #endif // DRONECAN_SUPPORT && defined(MCU_G431) From 0057fed222a110cd60c15f3e32af89b6bbdac0bf Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 09/25] bootloader: honour USE_HSE / USE_LSE / USE_MSI on L431 Mirror the oscillator selection from the main firmware's l431 peripherals.c so per-board #defines drive bootloader and app identically; previously bl_clock_config() ignored them and always brought up HSI16. Now: USE_HSE (crystal or bypass, 8/16/24MHz), USE_LSE (MSI disciplined by 32.768kHz LSE, RTC from LSE), USE_MSI, or default HSI16 - all converging on the 80MHz PLL. Non-LSE paths still set up LSI so the RTC/backup registers (used for the DroneCAN->app handoff) stay clocked. Also enable backup-domain access here. Non-CAN L431 is -32 bytes. --- Mcu/l431/Inc/blutil.h | 123 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 106 insertions(+), 17 deletions(-) diff --git a/Mcu/l431/Inc/blutil.h b/Mcu/l431/Inc/blutil.h index 6de6c6f8..4e542bfb 100644 --- a/Mcu/l431/Inc/blutil.h +++ b/Mcu/l431/Inc/blutil.h @@ -93,41 +93,130 @@ static inline uint16_t bl_timer_us(void) } /* - initialise clocks + initialise clocks. Mirrors the oscillator choice in the main AM32 firmware's + Mcu/l431/Src/peripherals.c so per-board #define blocks (USE_HSE/USE_LSE/USE_MSI) + drive the bootloader and the application identically: + + USE_HSE external high-speed crystal/oscillator. HSE_VALUE must be + 8 / 16 / 24 MHz. USE_HSE_BYPASS=0 selects crystal, otherwise + external oscillator. + USE_LSE MSI clock disciplined by an external 32.768 kHz LSE; RTC + also runs from LSE. USE_LSE_BYPASS=1 selects an external + clock source instead of a crystal. + USE_MSI free-running MSI (no LSE discipline). + (default) HSI16, the no-external-oscillator path. + + All branches converge on an 80 MHz PLL output. */ static inline void bl_clock_config(void) { LL_FLASH_SetLatency(LL_FLASH_LATENCY_4); - while (LL_FLASH_GetLatency()!= LL_FLASH_LATENCY_4) ; + while (LL_FLASH_GetLatency() != LL_FLASH_LATENCY_4) ; LL_PWR_SetRegulVoltageScaling(LL_PWR_REGU_VOLTAGE_SCALE1); - while (LL_PWR_IsActiveFlag_VOS() != 0) ; - LL_RCC_HSI_Enable(); - LL_RCC_LSI_Enable(); - /* Wait till MSI and LSI are ready */ - while (LL_RCC_LSI_IsReady() != 1) ; - while (LL_RCC_HSI_IsReady() != 1) ; + // backup domain access is needed both to change the RTC clock source and + // to use the BKP registers for the bootloader <-> firmware handoff. + LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR); + LL_PWR_EnableBkUpAccess(); - LL_RCC_SetRTCClockSource(LL_RCC_RTC_CLKSOURCE_LSI); - LL_RCC_EnableRTC(); +#ifdef USE_HSE + /* + External high-speed crystal / oscillator. USE_HSE_BYPASS=0 selects + crystal mode; anything else selects bypass-from-external-oscillator. + */ +#if defined(USE_HSE_BYPASS) && (USE_HSE_BYPASS == 0) + LL_RCC_HSE_DisableBypass(); +#else + LL_RCC_HSE_EnableBypass(); +#endif + LL_RCC_HSE_Enable(); + while (LL_RCC_HSE_IsReady() != 1) ; +#if HSE_VALUE == 24000000 + LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSE, LL_RCC_PLLM_DIV_3, 20, LL_RCC_PLLR_DIV_2); +#elif HSE_VALUE == 16000000 + LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSE, LL_RCC_PLLM_DIV_2, 20, LL_RCC_PLLR_DIV_2); +#elif HSE_VALUE == 8000000 + LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSE, LL_RCC_PLLM_DIV_1, 20, LL_RCC_PLLR_DIV_2); +#else +#error "Unsupported HSE_VALUE" +#endif - LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSI, LL_RCC_PLLM_DIV_2, 20, LL_RCC_PLLR_DIV_2); - LL_RCC_PLL_EnableDomain_SYS(); - LL_RCC_PLL_Enable(); +#elif defined(USE_LSE) + /* + LSE-disciplined MSI: a 32.768 kHz LSE drives the MSI's PLL-mode trim so + the MSI is precise enough for clock-sensitive peripherals. RTC also runs + from LSE. The PLL still sources from MSI (not from LSE directly). + + The backup-domain reset and full LSE re-configuration are only safe on + cold boot. On warm boot (after the app has invoked NVIC_SystemReset for + a DroneCAN firmware-update handoff) the LSE is already running AND the + BKP registers carry the handoff magic + path that DroneCAN.c reads in + DroneCAN_Startup. Resetting the backup domain wipes that state and + breaks the OTA flow. Skip the reset/reconfigure path when LSE is + already ready: the app and bootloader want the same LSE configuration, + and we want to preserve whatever the app wrote to the BKP registers. + */ + LL_RCC_MSI_Enable(); + while (LL_RCC_MSI_IsReady() != 1) ; + LL_RCC_MSI_DisablePLLMode(); + if (LL_RCC_LSE_IsReady() != 1) { + LL_RCC_ForceBackupDomainReset(); + LL_RCC_ReleaseBackupDomainReset(); + LL_RCC_LSE_SetDriveCapability(LL_RCC_LSEDRIVE_HIGH); + #if defined(USE_LSE_BYPASS) && USE_LSE_BYPASS + LL_RCC_LSE_EnableBypass(); + #else + LL_RCC_LSE_DisableBypass(); + #endif + LL_RCC_LSE_Enable(); + while (LL_RCC_LSE_IsReady() != 1) ; + LL_RCC_SetRTCClockSource(LL_RCC_RTC_CLKSOURCE_LSE); + LL_RCC_EnableRTC(); + } + LL_RCC_MSI_EnablePLLMode(); + LL_RCC_MSI_EnableRangeSelection(); + LL_RCC_MSI_SetRange(LL_RCC_MSIRANGE_6); + LL_RCC_MSI_SetCalibTrimming(0); + LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_MSI, LL_RCC_PLLM_DIV_1, 40, LL_RCC_PLLR_DIV_2); +#elif defined(USE_MSI) + /* + Free-running MSI (no LSE discipline). Useful when neither LSE nor HSE + is wired but the MSI accuracy is acceptable. + */ + LL_RCC_MSI_Enable(); + while (LL_RCC_MSI_IsReady() != 1) ; LL_RCC_MSI_EnableRangeSelection(); LL_RCC_MSI_SetRange(LL_RCC_MSIRANGE_6); LL_RCC_MSI_SetCalibTrimming(0); LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_MSI, LL_RCC_PLLM_DIV_1, 40, LL_RCC_PLLR_DIV_2); + +#else + /* + Default: HSI16. The PLL produces 80 MHz from the internal 16 MHz HSI. + */ + LL_RCC_HSI_Enable(); + while (LL_RCC_HSI_IsReady() != 1) ; + LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSI, LL_RCC_PLLM_DIV_2, 20, LL_RCC_PLLR_DIV_2); +#endif + +#ifndef USE_LSE + /* + Non-LSE paths still need the RTC running so the DroneCAN bootloader can + hand off via the BKP registers. LSI is the natural source there. Cheap + enough to enable unconditionally for non-CAN builds too. + */ + LL_RCC_LSI_Enable(); + while (LL_RCC_LSI_IsReady() != 1) ; + LL_RCC_SetRTCClockSource(LL_RCC_RTC_CLKSOURCE_LSI); + LL_RCC_EnableRTC(); +#endif + LL_RCC_PLL_EnableDomain_SYS(); LL_RCC_PLL_Enable(); - - /* Wait till PLL is ready */ while (LL_RCC_PLL_IsReady() != 1) ; LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL); - - /* Wait till System clock is ready */ while (LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_PLL) ; LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1); From 670cfd06577ead1b81aaabeb382d6422fec56845 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 10/25] targets: add VIMDRONES_L431_CAN, VIMDRONES_NANO_L431_CAN, VIMDRONES_S50_L431_CAN Mirror the three Vimdrones CAN board blocks from the main firmware. All L431 / PA2 signal pin; NANO and S50 use a 24MHz external HSE (now honoured by the bootloader). None define CAN_TERM_PIN, so the termination GPIO code stays dead-code-eliminated for them. --- Inc/targets.h | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Inc/targets.h b/Inc/targets.h index 316073d3..c7a545fb 100644 --- a/Inc/targets.h +++ b/Inc/targets.h @@ -162,3 +162,41 @@ // AT32 CAN pins are fixed at PA11/PA12 (CAN1_GMUX_0000) in the F415 driver. // Main firmware block has no CAN_TERM_PIN, so we don't set one here either. #endif + +#ifdef VIMDRONES_L431_CAN +#define FILE_NAME "VIMDRONES_L431_CAN" // parser: MCU=L431, CAN build +#define TARGET_TAG VIML4 // -> AM32_L431_BOOTLOADER_VIML4_CAN +#define USE_PA2 // from HARDWARE_GROUP_L4_B (INPUT_PIN PA2) + +// bxCAN pins are fixed at PA11/PA12 in the L431 bootloader CAN driver. +// Main firmware block has no oscillator or CAN_TERM_PIN settings; the +// bootloader defaults to HSI16 (the main firmware does too via the +// fall-through path in Mcu/l431/Src/peripherals.c). +#endif + +#ifdef VIMDRONES_NANO_L431_CAN +#define FILE_NAME "VIMDRONES_NANO_L431_CAN" // parser: MCU=L431, CAN build +#define TARGET_TAG VIMNANO // -> AM32_L431_BOOTLOADER_VIMNANO_CAN +#define USE_PA2 // from HARDWARE_GROUP_L4_B (INPUT_PIN PA2) + +// bxCAN pins are fixed at PA11/PA12 in the L431 bootloader CAN driver. + +// 24 MHz external HSE clock (main firmware doesn't set USE_HSE_BYPASS, so +// the bootloader default applies = bypass mode = external clock source). +#define USE_HSE +#undef HSE_VALUE +#define HSE_VALUE 24000000 +#endif + +#ifdef VIMDRONES_S50_L431_CAN +#define FILE_NAME "VIMDRONES_S50_L431_CAN" // parser: MCU=L431, CAN build +#define TARGET_TAG VIMS50 // -> AM32_L431_BOOTLOADER_VIMS50_CAN +#define USE_PA2 // from HARDWARE_GROUP_L4_B (INPUT_PIN PA2) + +// bxCAN pins are fixed at PA11/PA12 in the L431 bootloader CAN driver. + +// 24 MHz external HSE clock (same as VIMDRONES_NANO, see above). +#define USE_HSE +#undef HSE_VALUE +#define HSE_VALUE 24000000 +#endif From 6bace55449e996420e60f1e773f376eb24f0763a Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 11/25] bl_update: disable interrupts for the flash_bootloader() sequence bl_update erases+reprograms the bootloader in bank 1 over its own AHB path. On dual-bank STM32G4 (G491/G4Axx, DBANK=1 default) bank-1 reads stall during any bank-1 write, including the instruction fetch when an interrupt arrives mid erase/program - the handler fetch comes from a half-erased bank and HardFaults, wedging the chip with a corrupt bootloader (SWD reflash only). Observed on ARK_G431_CAN. __disable_irq() before flash_bootloader() closes the race; no re-enable needed since NVIC_SystemReset() follows. Harmless on L431. --- bl_update/main.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bl_update/main.c b/bl_update/main.c index ea9a672f..d08f78bc 100644 --- a/bl_update/main.c +++ b/bl_update/main.c @@ -116,6 +116,19 @@ int main(void) // give 1.5s for debugger to attach delayMicroseconds(1500000); + /* + disable interrupts for the whole flash sequence. The updater runs from + the same flash bank that we are about to erase + reprogram; on dual-bank + STM32G4 (e.g. STM32G491 / G4Axx in DBANK=1 mode) the bootloader live in + bank 1 just like the updater code, and bank-1 reads stall during any + bank-1 write. If an interrupt fires during a write, the CPU may try to + fetch the handler from the half-erased bank and HardFault, leaving the + chip stuck with a partially-written bootloader. Disabling interrupts + avoids the race; we NVIC_SystemReset below so interrupt state is + naturally restored on the next boot. + */ + __disable_irq(); + // do the flash flash_bootloader(); } From de01d0ba0865d1afc5a81c7151d11687c758b43b Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 12/25] bootloader: switch DroneCAN time tracking from uint64_t to uint32_t The uint64_t monotonic clock pulled libgcc's __udivmoddi4 (720B) on every divide. uint32_t microseconds wraps at ~71 min, far beyond the bootloader lifetime (the bl_serial_active idle timeout caps a stuck session at ~5 min). Rename micros64()->micros32(); millis32() uses the single-cycle UDIV; 1Hz scheduling uses a wrap-safe signed-diff; libcanard still gets uint64 via a cast at each call site (it only needs monotonicity over ~2s). ~800B saved per CAN target. --- bootloader/DroneCAN/DroneCAN.c | 50 +++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/bootloader/DroneCAN/DroneCAN.c b/bootloader/DroneCAN/DroneCAN.c index cf935486..0d6a38d3 100644 --- a/bootloader/DroneCAN/DroneCAN.c +++ b/bootloader/DroneCAN/DroneCAN.c @@ -124,14 +124,13 @@ static uint16_t get_random16(void) } /* - get a 64 bit monotonic timestamp in microseconds since start. This - is platform specific - - NOTE: this should be in functions.c + monotonic microseconds since start. uint32 wraps at ~71min, far beyond the + bootloader lifetime, and avoids the libgcc 64-bit divide. libcanard takes + uint64 timestamps - cast at the call sites. */ -static uint64_t micros64(void) +static uint32_t micros32(void) { - static uint64_t base_us; + static uint32_t base_us; static uint16_t last_cnt; uint16_t cnt = bl_timer_us(); if (cnt < last_cnt) { @@ -142,11 +141,12 @@ static uint64_t micros64(void) } /* - get monotonic time in milliseconds since startup + get monotonic time in milliseconds since startup. 32-bit divide compiles + to a single UDIV on cortex-m4/m33 -- no libgcc helper needed. */ static uint32_t millis32(void) { - return micros64() / 1000ULL; + return micros32() / 1000U; } /* @@ -256,7 +256,7 @@ static void handle_GetNodeInfo(CanardInstance *ins, CanardRxTransfer *transfer) memset(&pkt, 0, sizeof(pkt)); - node_status.uptime_sec = micros64() / 1000000ULL; + node_status.uptime_sec = micros32() / 1000000U; pkt.status = node_status; // fill in your major and minor firmware version @@ -639,7 +639,7 @@ static void send_NodeStatus(void) { uint8_t buffer[UAVCAN_PROTOCOL_GETNODEINFO_RESPONSE_MAX_SIZE]; - node_status.uptime_sec = micros64() / 1000000ULL; + node_status.uptime_sec = micros32() / 1000000U; node_status.health = UAVCAN_PROTOCOL_NODESTATUS_HEALTH_OK; node_status.mode = UAVCAN_PROTOCOL_NODESTATUS_MODE_MAINTENANCE; node_status.sub_mode = 0; @@ -672,12 +672,14 @@ static void send_NodeStatus(void) /* This function is called at 1 Hz rate from the main loop. */ -static void process1HzTasks(uint64_t timestamp_usec) +static void process1HzTasks(uint32_t timestamp_usec) { /* - Purge transfers that are no longer transmitted. This can free up some memory + Purge transfers that are no longer transmitted. This can free up some memory. + Canard's API is uint64_t; zero-extend here. Stale-transfer timeout is + CANARD_TRANSFER_TIMEOUT_USEC (~2s), well below the uint32 wrap window. */ - canardCleanupStaleTransfers(&canard, timestamp_usec); + canardCleanupStaleTransfers(&canard, (uint64_t)timestamp_usec); /* Transmit the node status message @@ -692,7 +694,7 @@ void DroneCAN_receiveFrame(void) { CanardCANFrame rx_frame = {0}; while (sys_can_receive(&rx_frame) > 0) { - canardHandleRxFrame(&canard, &rx_frame, micros64()); + canardHandleRxFrame(&canard, &rx_frame, (uint64_t)micros32()); } } @@ -701,7 +703,7 @@ void DroneCAN_receiveFrame(void) */ void DroneCAN_handleFrame(CanardCANFrame *frame) { - canardHandleRxFrame(&canard, frame, micros64()); + canardHandleRxFrame(&canard, frame, (uint64_t)micros32()); } /* @@ -799,7 +801,7 @@ static void DroneCAN_Startup(void) */ bool DroneCAN_update() { - static uint64_t next_1hz_service_at; + static uint32_t next_1hz_service_at; static bool done_startup; if (!done_startup) { done_startup = true; @@ -820,10 +822,13 @@ bool DroneCAN_update() return false; } - const uint64_t ts = micros64(); + const uint32_t ts = micros32(); - if (ts >= next_1hz_service_at) { - next_1hz_service_at += 1000000ULL; + /* + 1Hz tick + */ + if ((int32_t)(ts - next_1hz_service_at) >= 0) { + next_1hz_service_at = ts + 1000000U; process1HzTasks(ts); } @@ -862,10 +867,11 @@ static void *bl_memmem(const void *haystack, size_t haystacklen, const void *nee static void set_reason(enum boot_code code, const char *reason) { - static uint64_t last_msg_us; + static uint32_t last_msg_us; node_status.vendor_specific_status_code = code; - const uint64_t now_us = micros64(); - if (now_us - last_msg_us > 5000000UL) { + // unsigned subtraction handles uint32 wrap + const uint32_t now_us = micros32(); + if (now_us - last_msg_us > 5000000U) { last_msg_us = now_us; can_print(reason); } From 7b50501546fd7b90a421caeb9d35397d62b2cfaa Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 13/25] bootloader: gate can_print() behind DRONECAN_DEBUG DRONECAN_DEBUG existed (default 0) but never gated anything, so can_print(), LogMessage_encode and ~12 debug strings were always linked. Wrap the body in #if DRONECAN_DEBUG with a no-op inline stub in the default build; call sites are unchanged and the linker GCs the rest. ~636B saved per CAN target; a debug build flips the flag to get the messages back. --- bootloader/DroneCAN/DroneCAN.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bootloader/DroneCAN/DroneCAN.c b/bootloader/DroneCAN/DroneCAN.c index 0d6a38d3..093f42b3 100644 --- a/bootloader/DroneCAN/DroneCAN.c +++ b/bootloader/DroneCAN/DroneCAN.c @@ -176,7 +176,11 @@ static uint32_t crc32(const uint8_t *buf, uint32_t size) } #endif -// print to CAN LogMessage for debugging +/* + print to CAN LogMessage for debugging. Compiled out unless DRONECAN_DEBUG; + the no-op inline stub lets the linker drop LogMessage_encode and the strings. + */ +#if DRONECAN_DEBUG static void can_print(const char *s) { struct uavcan_protocol_debug_LogMessage pkt; @@ -196,6 +200,9 @@ static void can_print(const char *s) CANARD_TRANSFER_PRIORITY_LOW, buffer, len); } +#else +static inline void can_print(const char *s) { (void)s; } +#endif /* handle parameter executeopcode request From 8d22fcab635bd2aa9b176c0a1a2652771dbdf5d6 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 14/25] sys_can_at32: initialise filter_mode in can_filter_init_struct sys_can_init()'s local can_filter_init_struct never set filter_mode before passing it to the AT32 SDK's can_filter_init(), which reads it to choose mask vs list mode - an uninitialised stack byte. It happened to land on mask mode often enough to work, but the -flto that follows exposes the maybe-uninitialised read as -Werror. Assign CAN_FILTER_MODE_ID_MASK (id=0/mask=0, accept-all) explicitly. --- bootloader/DroneCAN/sys_can_at32.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bootloader/DroneCAN/sys_can_at32.c b/bootloader/DroneCAN/sys_can_at32.c index f1aed772..66f3bc1d 100644 --- a/bootloader/DroneCAN/sys_can_at32.c +++ b/bootloader/DroneCAN/sys_can_at32.c @@ -95,9 +95,14 @@ void sys_can_init(void) can_baudrate_struct.bts2_size = CAN_BTS2_3TQ; can_baudrate_set(CAN1, &can_baudrate_struct); - /* can filter init */ + /* can filter init. + filter_mode must be initialised explicitly; can_filter_init() reads it + in a switch with no guaranteed default. Mask mode with id=0/mask=0 + means "accept all". Without LTO the uninitialised read was invisible; + LTO inlining exposes it as -Werror=maybe-uninitialized. */ can_filter_init_type can_filter_init_struct; can_filter_init_struct.filter_activate_enable = TRUE; + can_filter_init_struct.filter_mode = CAN_FILTER_MODE_ID_MASK; can_filter_init_struct.filter_fifo = CAN_FILTER_FIFO0; can_filter_init_struct.filter_number = 0; can_filter_init_struct.filter_bit = CAN_FILTER_32BIT; From 57bf83b3f5054a5ac759a42046747c879d39eb5b Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 15/25] build: enable -flto and -fdata-sections in the bootloader build The build already used -Os -ffunction-sections + --gc-sections. Add -fdata-sections (GC unused globals) and -flto (cross-TU inlining/DCE). Saves ~0.9-1.0KB per STM32 CAN target and ~2.3KB on F415 (the AT32 SDK wrappers benefit a lot); the tight non-CAN G431 build drops from 96% to 81% flash. Build time +~10-20% for the LTO link. --- Makefile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 567d2a90..128d5fb4 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,8 @@ LIBS := -lnosys # Compiler options CFLAGS_BASE := -fsingle-precision-constant -fomit-frame-pointer -ffast-math --specs=nosys.specs -CFLAGS_BASE += -I$(MAIN_INC_DIR) -g3 -Os -ffunction-sections -funsigned-char +CFLAGS_BASE += -I$(MAIN_INC_DIR) -g3 -Os -ffunction-sections -fdata-sections -funsigned-char +CFLAGS_BASE += -flto CFLAGS_BASE += -Wall -Wextra -Wundef -Werror -Wno-unused-parameter # force-include per-board config; inert for builds with no board define CFLAGS_BASE += -include $(MAIN_INC_DIR)/targets.h @@ -71,7 +72,10 @@ CFLAGS_COMMON := $(CFLAGS_BASE) CFLAGS_ARM_ONLY := -fno-optimize-crc # Linker options -LDFLAGS_COMMON := -specs=nano.specs $(LIBS) -Wl,--gc-sections -Wl,--print-memory-usage +# -fdata-sections lets --gc-sections drop unused globals (companion to -ffunction-sections). +# -flto enables link-time optimisation (cross-TU inlining + DCE); pass it on both +# the compile and link command lines so the linker gets the IR not raw object code. +LDFLAGS_COMMON := -specs=nano.specs $(LIBS) -Wl,--gc-sections -Wl,--print-memory-usage -flto # configure some directories that are relative to wherever ROOT_DIR is located OBJ := obj From 78b1a3b2daefe0f4a45d509ae621d54d0fd5ccb6 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 16/25] bootloader: accumulate uptime from 1Hz ticks to survive micros32 wrap After the 32-bit time switch, node_status.uptime_sec was micros32()/1000000, which wraps to 0 at ~71 min (and the divide on a wrapping source can be briefly non-monotonic) - a regression for a long-lived bootloader in DroneCAN logs. Accumulate uptime_sec from the 1Hz tick instead (saturating), incrementing after send_NodeStatus so the first broadcast reports 0. Drops the divide (~20B per CAN target). --- bootloader/DroneCAN/DroneCAN.c | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/bootloader/DroneCAN/DroneCAN.c b/bootloader/DroneCAN/DroneCAN.c index 093f42b3..75fbd637 100644 --- a/bootloader/DroneCAN/DroneCAN.c +++ b/bootloader/DroneCAN/DroneCAN.c @@ -263,7 +263,8 @@ static void handle_GetNodeInfo(CanardInstance *ins, CanardRxTransfer *transfer) memset(&pkt, 0, sizeof(pkt)); - node_status.uptime_sec = micros32() / 1000000U; + // uptime_sec is maintained by process1HzTasks via a 1Hz tick counter, + // not derived from the 71-minute-wrapping microsecond timer. pkt.status = node_status; // fill in your major and minor firmware version @@ -646,7 +647,8 @@ static void send_NodeStatus(void) { uint8_t buffer[UAVCAN_PROTOCOL_GETNODEINFO_RESPONSE_MAX_SIZE]; - node_status.uptime_sec = micros32() / 1000000U; + // uptime_sec is maintained by process1HzTasks (1Hz tick counter), not + // derived from the wrapping microsecond timer. node_status.health = UAVCAN_PROTOCOL_NODESTATUS_HEALTH_OK; node_status.mode = UAVCAN_PROTOCOL_NODESTATUS_MODE_MAINTENANCE; node_status.sub_mode = 0; @@ -689,9 +691,17 @@ static void process1HzTasks(uint32_t timestamp_usec) canardCleanupStaleTransfers(&canard, (uint64_t)timestamp_usec); /* - Transmit the node status message + Transmit the node status message. uptime_sec is bumped AFTER the + broadcast so the first NodeStatus (which fires almost immediately + after boot) reports uptime=0, matching the old micros64()-based + semantics. */ send_NodeStatus(); + + /* + wraps at 136 years + */ + node_status.uptime_sec++; } /* From 334060fdeb829f859fa3dbb848725d898cfc39f4 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 17/25] bootloader: drain CAN TX queue from interrupt, enable bxCAN TXFP Pipelined DroneCAN parameter fetches were timing out (~12s with many retries on L431, ~6s on G431). Two fixes: 1. The TX-complete IRQ was a no-op on L431 bxCAN (#if 0) and G431 FDCAN (stub), so the canard TX queue was only drained by the main loop at ~20Hz, capping multi-frame throughput and filling the memory pool. Drain it from the TX-complete IRQ on both, matching sys_can_at32.c. Safe: main-loop drains run under disable/enable_IRQ, and RX/TX IRQs share NVIC priority so they don't preempt each other on the pool. 2. bxCAN (L431) transmits by CAN-ID priority then lowest mailbox, so within a multi-frame transfer (same ID) a refilled mailbox could send out of order and the receiver dropped the transfer. Set MCR_TXFP=1 for chronological order. FDCAN (TXBC=0 FIFO) and AT32 already do this. Result: param fetch 12s->0.7s (L431), 6s->0.6s (G431), zero retries. Flash +20B L431, +4B G431. --- bootloader/DroneCAN/sys_can_stm32.c | 29 +++++++++++++++++++---- bootloader/DroneCAN/sys_can_stm32_CANFD.c | 10 +++++++- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/bootloader/DroneCAN/sys_can_stm32.c b/bootloader/DroneCAN/sys_can_stm32.c index 71e3eb3d..0aff1c2f 100644 --- a/bootloader/DroneCAN/sys_can_stm32.c +++ b/bootloader/DroneCAN/sys_can_stm32.c @@ -61,6 +61,7 @@ typedef struct { /* CAN master control register */ static uint32_t MCR_INRQ = (1U << 0); /* Bit 0: Initialization Request */ static uint32_t MCR_SLEEP = (1U << 1); /* Bit 1: Sleep Mode Request */ +static uint32_t MCR_TXFP = (1U << 2); /* Bit 2: Transmit FIFO Priority */ static uint32_t MCR_AWUM = (1U << 5); /* Bit 5: Automatic Wakeup Mode */ static uint32_t MCR_ABOM = (1U << 6); /* Bit 6: Automatic Bus-Off Management */ @@ -176,11 +177,16 @@ static bool can_send(const CanardCANFrame *frame) static void handleTxMailboxInterrupt(uint8_t mailbox_index, bool txok) { -#if 0 - // in the bootloader we don't do interrupt driven transmit, send - // happens from main loop only + (void)mailbox_index; + (void)txok; + /* + drain the next pending frame as a mailbox empties. Without this the canard + TX queue is only serviced from the main loop at ~20Hz, throttling multi-frame + responses and filling the memory pool. Safe wrt the main loop (its drains run + under disable/enable_IRQ) and wrt RX (RX/TX share NVIC priority, so they run + back-to-back, not preempting on the pool). + */ DroneCAN_processTxQueue(); -#endif } static void pollErrorFlagsFromISR() @@ -371,7 +377,20 @@ static void can_init(void) /* * Hardware initialization (the hardware has already confirmed initialization mode, see above) */ - BXCAN->MCR = MCR_ABOM | MCR_AWUM | MCR_INRQ; // RM page 648 + /* + TXFP=1 puts the 3 TX mailboxes into FIFO order: hardware transmits + in the order frames were enqueued, regardless of CAN ID. Without + this, frames with equal IDs (everything inside a single multi-frame + DroneCAN transfer) would arbitrate by mailbox number, so a TX-IRQ + refilling mailbox 0 with the next-in-line frame could cause it to + transmit before the still-pending frames in mailboxes 1/2 -- which + corrupted the DroneCAN toggle bit and made multi-frame responses + unreadable to the GUI. canard_stm32 (used by the main firmware) + works around this with priority-inversion checks in its transmit + path, but the bootloader's can_send() doesn't have that, so we use + the hardware FIFO mode instead. RM page 648. + */ + BXCAN->MCR = MCR_ABOM | MCR_AWUM | MCR_INRQ | MCR_TXFP; // timings assuming 80MHz clock const uint8_t sjw = 0; diff --git a/bootloader/DroneCAN/sys_can_stm32_CANFD.c b/bootloader/DroneCAN/sys_can_stm32_CANFD.c index 4fd4e7be..0b302a3a 100644 --- a/bootloader/DroneCAN/sys_can_stm32_CANFD.c +++ b/bootloader/DroneCAN/sys_can_stm32_CANFD.c @@ -177,7 +177,15 @@ static void handleRxInterrupt(uint8_t fifo_index) static void handleTxCompleteInterrupt(void) { - // Nothing to do in simple bootloader mode + /* + Drain the next pending frame as soon as a TX buffer empties; see + sys_can_stm32.c::handleTxMailboxInterrupt() for the rationale. + Without this the canard TX queue is only serviced from the main + loop's DroneCAN_update() at ~20 Hz, which throttles multi-frame + responses badly enough that pipelined GetSet fetches from + dronecan_gui_tool drop responses and time out. + */ + DroneCAN_processTxQueue(); } static void pollErrorFlagsFromISR(void) From bb0f264e6eda37449998414908396920f2563267 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:04:40 +1000 Subject: [PATCH 18/25] bootloader: fall back to non-CAN boot when no CAN cable present A CAN build with no cable never booted the app: the only boot gate is have_raw_command (a startup pin-low or a received RawCommand), so DShot-only users were stuck in the bootloader. Add a no-CAN fallback: - can_seen (RX-only) is set in both canardHandleRxFrame call sites (DroneCAN_handleFrame for bxCAN/FDCAN, DroneCAN_receiveFrame for AT32). - In DroneCAN_update(), after NONCAN_FALLBACK_MS (250ms) with no raw command and no CAN seen, arm noncan_fallback - unless EEPROM INPUT_SIGNAL_TYPE (byte 46) is DroneCAN. A blank EEPROM falls back (jump() still refuses a blank board). - DroneCAN_boot_ok() waives the raw-command requirement only while (noncan_fallback && !can_seen), so a later frame re-blocks the boot. All signature/CRC/length/board checks are kept. Also fixes the no-signal reject to use FAIL_REASON_NO_SIGNAL. The existing 50ms idle poll in serialreadChar() drives the fallback boot; it is suppressed during a 4-way session, so config sessions boot after disconnect. --- bootloader/DroneCAN/DroneCAN.c | 44 ++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/bootloader/DroneCAN/DroneCAN.c b/bootloader/DroneCAN/DroneCAN.c index 75fbd637..82084ed1 100644 --- a/bootloader/DroneCAN/DroneCAN.c +++ b/bootloader/DroneCAN/DroneCAN.c @@ -67,6 +67,14 @@ static struct { // volatile (especially with -flto cross-TU optimisation) static volatile bool have_raw_command; +// no-CAN-cable fallback: behave like a non-CAN build when no CAN frame is +// seen within NONCAN_FALLBACK_MS. can_seen is set RX-only; noncan_fallback +// relaxes the boot gate once the deadline passes with no traffic. +static volatile bool can_seen; +static bool noncan_fallback; +#define NONCAN_FALLBACK_MS 250 +#define DRONECAN_INPUT_TYPE 5 // mirrors DRONECAN_IN in AM32 Inc/common.h (EEPROM byte 46) + void DroneCAN_set_have_signal(void) { have_raw_command = true; @@ -711,6 +719,7 @@ void DroneCAN_receiveFrame(void) { CanardCANFrame rx_frame = {0}; while (sys_can_receive(&rx_frame) > 0) { + can_seen = true; canardHandleRxFrame(&canard, &rx_frame, (uint64_t)micros32()); } } @@ -720,6 +729,7 @@ void DroneCAN_receiveFrame(void) */ void DroneCAN_handleFrame(CanardCANFrame *frame) { + can_seen = true; canardHandleRxFrame(&canard, frame, (uint64_t)micros32()); } @@ -829,6 +839,33 @@ bool DroneCAN_update() DroneCAN_processTxQueue(); + /* + arm the no-CAN fallback. Must run before the DNA early-return below (with no + cable the node never gets an ID, so that returns false forever). After + NONCAN_FALLBACK_MS with no raw command and no CAN seen, relax the boot gate + unless INPUT_SIGNAL_TYPE is DroneCAN. A blank EEPROM arms, but the + short-circuit below still refuses to boot it. + */ + if (!have_raw_command && !noncan_fallback && !can_seen && + millis32() > NONCAN_FALLBACK_MS) { + const uint8_t *ee = (const uint8_t *)EEPROM_START_ADD; + const bool wait_can = (ee[0] == 0x01 && ee[46] == DRONECAN_INPUT_TYPE); + if (!wait_can) { + noncan_fallback = true; + } + } + if (noncan_fallback && !can_seen) { + // fallen back and still no CAN: boot like a non-CAN build. Like the non-CAN + // build, refuse a blank/unprogrammed EEPROM (byte0 != 0x01) rather than let + // DroneCAN_boot_ok() seed defaults and boot it. If a CAN frame arrives + // later, can_seen vetoes this and DNA/CAN handling resumes below. + sys_can_enable_IRQ(); + if (*(const uint8_t *)EEPROM_START_ADD != 0x01) { + return false; + } + return DroneCAN_boot_ok(); + } + // see if we are still doing DNA if (canardGetLocalNodeID(&canard) == CANARD_BROADCAST_NODE_ID) { // we're still waiting for a DNA allocation of our node ID @@ -939,8 +976,11 @@ bool DroneCAN_boot_ok(void) } #endif - if (!have_raw_command) { - set_reason(FAIL_REASON_BAD_CRC, "no signal"); + // waive the raw-command requirement under no-CAN fallback. The !can_seen + // re-check closes the late-frame race: a frame arriving after fallback armed + // re-blocks the boot and reverts to CAN behaviour. + if (!have_raw_command && !(noncan_fallback && !can_seen)) { + set_reason(FAIL_REASON_NO_SIGNAL, "no signal"); return false; } From fe0311e8161f302227f50ad2f7af01488457d335 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Fri, 26 Jun 2026 12:19:55 +1000 Subject: [PATCH 19/25] bootloader: init CAN early so the no-CAN fallback window sees the bus from boot Call DroneCAN_update() once before checkForSignal() (under DRONECAN_SUPPORT) so sys_can_init() runs and RX is live for the whole NONCAN_FALLBACK_MS window. Without this, CAN first goes live at the ~50ms serial idle poll, leaving a 0-50ms blind window at power-on. Return value ignored (false this early). --- bootloader/main.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bootloader/main.c b/bootloader/main.c index 0587e6f4..892788c0 100644 --- a/bootloader/main.c +++ b/bootloader/main.c @@ -1454,6 +1454,11 @@ int main(void) DroneCAN_set_have_signal(); } } + + // bring CAN RX live before checkForSignal() so the no-CAN fallback window + // (NONCAN_FALLBACK_MS) observes the bus from boot. Return value ignored: it + // is false this early (no raw command, deadline not yet reached). + (void)DroneCAN_update(); #endif checkForSignal(); From f1ed9efc3fb5ff24ae51c7d997d03d1a9025617e Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:10:14 +1000 Subject: [PATCH 20/25] bootloader: implement DroneCAN parameter GetSet for CAN_NODE / ESC_INDEX The bootloader implemented ExecuteOpcode but not GetSet, so the CAN binding parameters couldn't be set or queried without first booting the application. Expose CAN_NODE (uint8 0-127) and ESC_INDEX (uint8 0-32) from the main firmware's table, backed by the same EEPROM offsets (176/177). Reads go through bl_param_get(), mirroring the main firmware's load_settings(): an unset EEPROM magic or an out-of-[min,max] raw byte (0xff) returns default_value, so the GUI sees the value the app would use. Set is honoured only for a named request with an integer value and a programmed EEPROM; the preserve buffer is 1024 B (EEPROM_MAX_SIZE, static to keep it off the CAN stack) so writes to offsets 176/177 land in flash with surrounding settings intact. The response carries value, name and min/max/default. Verified over pydronecan against ARK G431_CAN and vimdrones L431_CAN: get returns the right ranges/defaults; set CAN_NODE=42 reads back 42 with surrounding EEPROM preserved. ~+1KB flash per CAN target, +1024 B BSS. --- bootloader/DroneCAN/DroneCAN.c | 169 +++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/bootloader/DroneCAN/DroneCAN.c b/bootloader/DroneCAN/DroneCAN.c index 82084ed1..1d2fe3b3 100644 --- a/bootloader/DroneCAN/DroneCAN.c +++ b/bootloader/DroneCAN/DroneCAN.c @@ -52,6 +52,15 @@ static uint8_t canard_memory_pool[CANARD_POOL_SIZE]; #error "Only 128K flash size supported for DroneCAN" #endif +/* + the EEPROM region we preserve on a single-byte update. This must cover + the highest parameter offset we touch (currently ESC_INDEX at byte 177) + and matches the main firmware's EEPROM_MAX_SIZE so we don't truncate + state the application has stored above the default_settings block. + Must be a multiple of the flash word granularity (8 on L431/G431). + */ +#define EEPROM_PRESERVE_SIZE 1024 + /* keep the state for firmware update */ @@ -212,6 +221,158 @@ static void can_print(const char *s) static inline void can_print(const char *s) { (void)s; } #endif +/* + parameters the bootloader exposes via uavcan.protocol.param.GetSet. We + expose just the two CAN-binding parameters from the main firmware so a + user can rebind an ESC's node id or motor index without first booting + the application. The values live at the same EEPROM offsets the main + firmware writes them to (see ../AM32/Inc/eeprom.h::eepromBuffer.can). + + default_value is what we report back when the raw EEPROM byte is + uninitialised (0xFF) or out of range; matches the main firmware's + defaults from load_settings() in ../AM32/Src/DroneCAN/DroneCAN.c. + */ +static const struct { + const char *name; + uint8_t min_value; + uint8_t max_value; + uint8_t default_value; + uint8_t eeprom_offset; +} bl_parameters[] = { + { "CAN_NODE", 0, 127, 0, 176 }, + { "ESC_INDEX", 0, 32, 0, 177 }, +}; + +#define NUM_BL_PARAMS (sizeof(bl_parameters)/sizeof(bl_parameters[0])) + +/* + patch a single byte in the EEPROM page while preserving the rest. + STM32 flash is page-erase-then-program: save_flash_nolib() erases the + whole 2 KB page at EEPROM_START_ADD and writes back only the bytes we + hand it, so we have to copy out enough of the live page to cover every + application setting, modify the one byte, and write the lot back. + EEPROM_PRESERVE_SIZE matches the main firmware's EEPROM_MAX_SIZE. + Returns true on success. + */ +static bool set_eeprom_byte(uint16_t offset, uint8_t value) +{ + static uint8_t buf[EEPROM_PRESERVE_SIZE]; + if (offset >= sizeof(buf)) { + return false; + } + memcpy(buf, (const void *)EEPROM_START_ADD, sizeof(buf)); + if (buf[offset] == value) { + // nothing to do; avoid an unnecessary erase cycle. + return true; + } + buf[offset] = value; + return save_flash_nolib(buf, sizeof(buf), EEPROM_START_ADD); +} + +/* + read the live (effective) value of a parameter from EEPROM, applying + the same default-and-clamp rules the main firmware's load_settings() + uses: an out-of-range raw byte (typically 0xFF on uninitialised EEPROM) + or an unset EEPROM magic produces the parameter's default_value, not + the raw 0xFF that confused the DroneCAN GUI tools. + */ +static uint8_t bl_param_get(uint8_t p_idx) +{ + const uint8_t *eeprom = (const uint8_t *)EEPROM_START_ADD; + const uint8_t raw = eeprom[bl_parameters[p_idx].eeprom_offset]; + if (eeprom[0] != 0x01 || + raw < bl_parameters[p_idx].min_value || + raw > bl_parameters[p_idx].max_value) { + return bl_parameters[p_idx].default_value; + } + return raw; +} + +/* + handle uavcan.protocol.param.GetSet request. Supports lookup by name + or by index; set is only honoured when the request carries a non-empty + name AND a non-empty integer value, matching the main firmware's + convention. Response carries the current value plus the parameter name, + default_value, min_value and max_value so the DroneCAN GUI tool can + validate user input. +*/ +static void handle_param_GetSet(CanardInstance* ins, CanardRxTransfer* transfer) +{ + struct uavcan_protocol_param_GetSetRequest req; + if (uavcan_protocol_param_GetSetRequest_decode(transfer, &req)) { + return; + } + + const uint8_t *eeprom = (const uint8_t *)EEPROM_START_ADD; + int p_idx = -1; + + if (req.name.len != 0) { + for (uint8_t i = 0; i < NUM_BL_PARAMS; i++) { + const char *pname = bl_parameters[i].name; + const uint32_t plen = strlen(pname); + if (req.name.len == plen && + memcmp(req.name.data, pname, plen) == 0) { + p_idx = (int)i; + break; + } + } + } else if (req.index < NUM_BL_PARAMS) { + p_idx = req.index; + } + + // set path: only when caller passed a name and a non-empty integer value. + // Refuse if the EEPROM magic isn't set; we'd otherwise be writing into a + // page full of 0xFF and the application would treat the result as + // uninitialised on next boot anyway. + if (p_idx >= 0 && req.name.len != 0 && + req.value.union_tag == UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE && + eeprom[0] == 0x01) { + int64_t v = req.value.integer_value; + if (v < (int64_t)bl_parameters[p_idx].min_value) { + v = bl_parameters[p_idx].min_value; + } + if (v > (int64_t)bl_parameters[p_idx].max_value) { + v = bl_parameters[p_idx].max_value; + } + set_eeprom_byte(bl_parameters[p_idx].eeprom_offset, (uint8_t)v); + } + + // build the response (current value, name, default/min/max). + struct uavcan_protocol_param_GetSetResponse pkt; + memset(&pkt, 0, sizeof(pkt)); + + if (p_idx >= 0) { + pkt.value.union_tag = UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE; + pkt.value.integer_value = bl_param_get((uint8_t)p_idx); + + pkt.default_value.union_tag = UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE; + pkt.default_value.integer_value = bl_parameters[p_idx].default_value; + + pkt.min_value.union_tag = UAVCAN_PROTOCOL_PARAM_NUMERICVALUE_INTEGER_VALUE; + pkt.min_value.integer_value = bl_parameters[p_idx].min_value; + + pkt.max_value.union_tag = UAVCAN_PROTOCOL_PARAM_NUMERICVALUE_INTEGER_VALUE; + pkt.max_value.integer_value = bl_parameters[p_idx].max_value; + + const char *pname = bl_parameters[p_idx].name; + pkt.name.len = strlen(pname); + memcpy(pkt.name.data, pname, pkt.name.len); + } + + uint8_t buffer[UAVCAN_PROTOCOL_PARAM_GETSET_RESPONSE_MAX_SIZE]; + uint16_t total_size = uavcan_protocol_param_GetSetResponse_encode(&pkt, buffer); + + canardRequestOrRespond(ins, + transfer->source_node_id, + UAVCAN_PROTOCOL_PARAM_GETSET_SIGNATURE, + UAVCAN_PROTOCOL_PARAM_GETSET_ID, + &transfer->transfer_id, + transfer->priority, + CanardResponse, + &buffer[0], + total_size); +} + /* handle parameter executeopcode request */ @@ -565,6 +726,10 @@ static void onTransferReceived(CanardInstance *ins, CanardRxTransfer *transfer) handle_param_ExecuteOpcode(ins, transfer); break; } + case UAVCAN_PROTOCOL_PARAM_GETSET_ID: { + handle_param_GetSet(ins, transfer); + break; + } } } if (transfer->transfer_type == CanardTransferTypeResponse) { @@ -620,6 +785,10 @@ static bool shouldAcceptTransfer(const CanardInstance *ins, *out_data_type_signature = UAVCAN_PROTOCOL_PARAM_EXECUTEOPCODE_SIGNATURE; return true; } + case UAVCAN_PROTOCOL_PARAM_GETSET_ID: { + *out_data_type_signature = UAVCAN_PROTOCOL_PARAM_GETSET_SIGNATURE; + return true; + } } } if (transfer_type == CanardTransferTypeResponse) { From fff7693aa7ecf6ed4924199e18f83eda46db76dd Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 27 Jun 2026 16:10:14 +1000 Subject: [PATCH 21/25] bootloader: expose full DroneCAN parameter list (T_UINT8/16/BOOL/STRING) Extend GetSet from the two CAN-binding parameters to the full ~32-entry set the main firmware advertises, mirroring its names/vtypes/ranges/defaults/scaling so a DroneCAN GUI sees the same controls against the bootloader or the app. Adds: - BL_T_UINT8 with the firmware's special scalings (CURRENT_LIMIT wire=eeprom*2; ADVANCE_LEVEL stored +10, legacy v3 remap applied on read). - BL_T_BOOL (Value.boolean_value). - BL_T_UINT16 stored as one byte, scaled on the wire (MOTOR_KV=eeprom*40+20, CELL_VOLTAGE_THR=eeprom+250). - BL_T_STRING for STARTUP_TUNE (offset 48..175), padding past the request length with 0xff. For parameters inside default_settings[] (0..47) the response default comes from default_settings, matching the firmware's "reset to default". set_eeprom_byte() becomes set_eeprom_bytes() (1..N bytes through the 1024 B preserve buffer) so the 128-byte tune write reuses the 1-byte path. DRONECAN_PARAM_SUPPORT_ENABLED (default 1) gates the whole interface; =0 drops it (~-2.9KB). Non-CAN targets unaffected. ~13.5-14.5KB per CAN target. Verified over pydronecan against ARK G431_CAN (32 params incl CAN_TERM_ENABLE) and vimdrones L431_CAN (31 params): round-trip set/get works for all vtypes including the scaled cases. --- bootloader/DroneCAN/DroneCAN.c | 394 +++++++++++++++++++++++++++------ 1 file changed, 328 insertions(+), 66 deletions(-) diff --git a/bootloader/DroneCAN/DroneCAN.c b/bootloader/DroneCAN/DroneCAN.c index 1d2fe3b3..13734589 100644 --- a/bootloader/DroneCAN/DroneCAN.c +++ b/bootloader/DroneCAN/DroneCAN.c @@ -32,6 +32,19 @@ #define DRONECAN_DEBUG 0 #endif +/* + DRONECAN_PARAM_SUPPORT_ENABLED gates the whole + uavcan.protocol.param.{GetSet,ExecuteOpcode} interface in the + bootloader: the parameter table, the handlers, the dispatch in + onTransferReceived(), and the shouldAcceptTransfer() entries. + Default on; a custom bootloader that needs the ~1 KB of flash for + something else can build with -DDRONECAN_PARAM_SUPPORT_ENABLED=0 + and the DroneCAN GUI will just see an empty parameter list. + */ +#ifndef DRONECAN_PARAM_SUPPORT_ENABLED +#define DRONECAN_PARAM_SUPPORT_ENABLED 1 +#endif + #ifndef DRONECAN_CHECK_SIGNATURE #define DRONECAN_CHECK_SIGNATURE 1 #endif @@ -221,80 +234,305 @@ static void can_print(const char *s) static inline void can_print(const char *s) { (void)s; } #endif +#if DRONECAN_PARAM_SUPPORT_ENABLED +/* + parameter type tags. These mirror what the main firmware exposes over + the wire via uavcan.protocol.param.Value so a DroneCAN GUI sees the + same value type (and therefore the same widget) for a given parameter + whether it's talking to the bootloader or the application. + */ +enum bl_param_type { + BL_T_UINT8 = 0, + BL_T_BOOL, + BL_T_UINT16, // stored as a single EEPROM byte, scaled on the wire + BL_T_STRING, // STARTUP_TUNE only, at eepromBuffer.tune (offset 48..175) +}; + +/* + EEPROM byte offsets that need a non-trivial uint8 <-> wire mapping. + Kept as named constants here so the special-case branches in + bl_param_*_value() and the set path read sensibly. + These match ../AM32/Inc/eeprom.h::eepromBuffer member offsets. + */ +#define EEPROM_OFS_ADVANCE_LEVEL 23 +#define EEPROM_OFS_MOTOR_KV 26 +#define EEPROM_OFS_LOW_CELL_VOLT_CUTOFF 37 +#define EEPROM_OFS_LIMITS_CURRENT 44 +#define EEPROM_OFS_TUNE 48 +#define EEPROM_TUNE_LEN 128 + +/* + remap table for legacy pre-v3 EEPROM advance_level encodings. Matches + ../AM32/Src/DroneCAN/DroneCAN.c::advance_level_v3_remap so a DroneCAN + client reading ADVANCE_LEVEL via the bootloader sees the same value + it would see via the application on an ESC that hasn't been rewritten + since the v3 migration. + */ +static const uint8_t advance_level_v3_remap[] = { 0x00, 0x08, 0x10, 0x16 }; + /* - parameters the bootloader exposes via uavcan.protocol.param.GetSet. We - expose just the two CAN-binding parameters from the main firmware so a - user can rebind an ESC's node id or motor index without first booting - the application. The values live at the same EEPROM offsets the main - firmware writes them to (see ../AM32/Inc/eeprom.h::eepromBuffer.can). - - default_value is what we report back when the raw EEPROM byte is - uninitialised (0xFF) or out of range; matches the main firmware's - defaults from load_settings() in ../AM32/Src/DroneCAN/DroneCAN.c. + parameters the bootloader exposes via uavcan.protocol.param.GetSet. + Name / vtype / min / max / default mirror the main firmware's + parameters[] table in ../AM32/Src/DroneCAN/DroneCAN.c so a DroneCAN GUI + shows the same values and ranges whether it talks to the application + or to the bootloader. + + default_value is what we report on the wire when the raw EEPROM byte + is uninitialised (0xFF) or out of range. For parameters whose EEPROM + offset is inside the default_settings[] block (offsets 0..47) the + response's default_value is taken from default_settings instead, to + match the main firmware's behaviour exactly. */ -static const struct { +static const struct bl_param { const char *name; - uint8_t min_value; - uint8_t max_value; - uint8_t default_value; + uint16_t min_value; + uint16_t max_value; + uint16_t default_value; + uint8_t vtype; // enum bl_param_type uint8_t eeprom_offset; } bl_parameters[] = { - { "CAN_NODE", 0, 127, 0, 176 }, - { "ESC_INDEX", 0, 32, 0, 177 }, + // CAN/DroneCAN block (offsets 176..183) — affects the bootloader's own + // CAN bus identity, so it has to be settable without booting the app. + { "CAN_NODE", 0, 127, 0, BL_T_UINT8, 176 }, + { "ESC_INDEX", 0, 32, 0, BL_T_UINT8, 177 }, + { "TELEM_RATE", 0, 200, 25, BL_T_UINT8, 179 }, + { "DEBUG_RATE", 0, 200, 0, BL_T_UINT8, 182 }, + { "REQUIRE_ARMING", 0, 1, 1, BL_T_BOOL, 178 }, + { "REQUIRE_ZERO_THROTTLE", 0, 1, 1, BL_T_BOOL, 180 }, + + // ESC behaviour (offsets within the first 48 B). Setting these from + // the bootloader lets a user reconfigure an ESC without first running + // the application; the main firmware applies them at next boot. + { "MOTOR_KV", 20, 10220, 2000, BL_T_UINT16, EEPROM_OFS_MOTOR_KV }, + { "MOTOR_POLES", 2, 64, 14, BL_T_UINT8, 27 }, + { "DIR_REVERSED", 0, 1, 0, BL_T_BOOL, 17 }, + { "BI_DIRECTIONAL", 0, 1, 0, BL_T_BOOL, 18 }, + { "BEEP_VOLUME", 0, 11, 5, BL_T_UINT8, 30 }, + { "VARIABLE_PWM", 0, 2, 1, BL_T_UINT8, 21 }, + { "PWM_FREQUENCY", 8, 144, 24, BL_T_UINT8, 24 }, + { "MAX_RAMP", 1, 200, 160, BL_T_UINT8, 5 }, + { "MIN_DUTY_CYCLE", 0, 50, 4, BL_T_UINT8, 6 }, + { "USE_SIN_START", 0, 1, 0, BL_T_BOOL, 19 }, + { "COMP_PWM", 0, 1, 1, BL_T_BOOL, 20 }, + { "STUCK_ROTOR_PROTECTION", 0, 1, 1, BL_T_BOOL, 22 }, + { "ADVANCE_LEVEL", 0, 30, 26, BL_T_UINT8, EEPROM_OFS_ADVANCE_LEVEL }, + { "AUTO_ADVANCE", 0, 1, 0, BL_T_BOOL, 47 }, + { "STARTUP_POWER", 50, 150, 10, BL_T_UINT8, 25 }, + { "CURRENT_LIMIT", 0, 200, 0, BL_T_UINT8, EEPROM_OFS_LIMITS_CURRENT }, + { "TEMPERATURE_LIMIT", 70, 255, 255, BL_T_UINT8, 43 }, + { "LOW_VOLTAGE_CUTOFF", 0, 1, 0, BL_T_BOOL, 36 }, + { "CELL_VOLTAGE_THRESHOLD", 250, 350, 300, BL_T_UINT16, EEPROM_OFS_LOW_CELL_VOLT_CUTOFF }, + { "BRAKE_ON_STOP", 0, 1, 1, BL_T_BOOL, 28 }, + { "DRIVING_BRAKE_STRENGTH", 1, 10, 10, BL_T_UINT8, 42 }, + { "DRAG_BRAKE_STRENGTH", 1, 10, 10, BL_T_UINT8, 41 }, + { "INPUT_SIGNAL_TYPE", 0, 5, 5, BL_T_UINT8, 46 }, + { "INPUT_FILTER_HZ", 0, 100, 0, BL_T_UINT8, 181 }, +#ifdef CAN_TERM_PIN + { "CAN_TERM_ENABLE", 0, 1, 0, BL_T_BOOL, 183 }, +#endif + { "STARTUP_TUNE", 0, 4, 0, BL_T_STRING, EEPROM_OFS_TUNE }, }; #define NUM_BL_PARAMS (sizeof(bl_parameters)/sizeof(bl_parameters[0])) /* - patch a single byte in the EEPROM page while preserving the rest. - STM32 flash is page-erase-then-program: save_flash_nolib() erases the - whole 2 KB page at EEPROM_START_ADD and writes back only the bytes we - hand it, so we have to copy out enough of the live page to cover every - application setting, modify the one byte, and write the lot back. - EEPROM_PRESERVE_SIZE matches the main firmware's EEPROM_MAX_SIZE. + patch a contiguous run of bytes in the EEPROM page while preserving + the rest. STM32 flash is page-erase-then-program: save_flash_nolib() + erases the whole 2 KB page at EEPROM_START_ADD and writes back only + the bytes we hand it, so we copy out enough of the live page to cover + every application setting, splice in the new bytes, and write the lot + back. EEPROM_PRESERVE_SIZE matches the main firmware's EEPROM_MAX_SIZE. Returns true on success. */ -static bool set_eeprom_byte(uint16_t offset, uint8_t value) +static bool set_eeprom_bytes(uint16_t offset, const uint8_t *data, uint16_t len) { static uint8_t buf[EEPROM_PRESERVE_SIZE]; - if (offset >= sizeof(buf)) { + if ((uint32_t)offset + len > sizeof(buf)) { return false; } memcpy(buf, (const void *)EEPROM_START_ADD, sizeof(buf)); - if (buf[offset] == value) { + if (memcmp(buf + offset, data, len) == 0) { // nothing to do; avoid an unnecessary erase cycle. return true; } - buf[offset] = value; + memcpy(buf + offset, data, len); return save_flash_nolib(buf, sizeof(buf), EEPROM_START_ADD); } +static bool set_eeprom_byte(uint16_t offset, uint8_t value) +{ + return set_eeprom_bytes(offset, &value, 1); +} + /* - read the live (effective) value of a parameter from EEPROM, applying - the same default-and-clamp rules the main firmware's load_settings() - uses: an out-of-range raw byte (typically 0xFF on uninitialised EEPROM) - or an unset EEPROM magic produces the parameter's default_value, not - the raw 0xFF that confused the DroneCAN GUI tools. + read the live (effective) wire value of a numeric parameter from + EEPROM, applying the same default-and-clamp rules and scaling the + main firmware uses. Sets *out_val and returns true on success; + returns false if the parameter is BL_T_STRING (caller must handle + strings separately) or if no parameter exists at p_idx. */ -static uint8_t bl_param_get(uint8_t p_idx) +static bool bl_param_read_numeric(uint8_t p_idx, uint16_t *out_val) { const uint8_t *eeprom = (const uint8_t *)EEPROM_START_ADD; - const uint8_t raw = eeprom[bl_parameters[p_idx].eeprom_offset]; - if (eeprom[0] != 0x01 || - raw < bl_parameters[p_idx].min_value || - raw > bl_parameters[p_idx].max_value) { - return bl_parameters[p_idx].default_value; + const struct bl_param *p = &bl_parameters[p_idx]; + if (p->vtype == BL_T_STRING) { + return false; + } + if (eeprom[0] != 0x01) { + *out_val = p->default_value; + return true; + } + const uint8_t raw = eeprom[p->eeprom_offset]; + uint16_t v; + switch (p->vtype) { + case BL_T_UINT16: + if (p->eeprom_offset == EEPROM_OFS_MOTOR_KV) { + v = (uint16_t)raw * 40U + 20U; + } else if (p->eeprom_offset == EEPROM_OFS_LOW_CELL_VOLT_CUTOFF) { + v = (uint16_t)raw + 250U; + } else { + v = raw; + } + break; + case BL_T_UINT8: + case BL_T_BOOL: + default: + v = raw; + if (p->eeprom_offset == EEPROM_OFS_LIMITS_CURRENT) { + v = (uint16_t)(v * 2U); + } else if (p->eeprom_offset == EEPROM_OFS_ADVANCE_LEVEL) { + if (v < sizeof(advance_level_v3_remap)) { + v = advance_level_v3_remap[v]; + } + if (v >= 10) { + v -= 10; + } + } + break; + } + // Clamp to range; an out-of-range raw byte (typically 0xFF on + // uninitialised EEPROM) maps to the parameter's default_value. + if (v < p->min_value || v > p->max_value) { + v = p->default_value; } - return raw; + *out_val = v; + return true; +} + +/* + write the wire value of a numeric parameter through to EEPROM, applying + the inverse of the scaling bl_param_read_numeric() applies. v is + pre-clamped to [min_value, max_value]; returns the result of the + flash write. + */ +static bool bl_param_write_numeric(uint8_t p_idx, uint16_t v) +{ + const struct bl_param *p = &bl_parameters[p_idx]; + if (v < p->min_value) v = p->min_value; + if (v > p->max_value) v = p->max_value; + uint8_t raw; + switch (p->vtype) { + case BL_T_UINT16: + if (p->eeprom_offset == EEPROM_OFS_MOTOR_KV) { + raw = (uint8_t)((v - 20U) / 40U); + } else if (p->eeprom_offset == EEPROM_OFS_LOW_CELL_VOLT_CUTOFF) { + raw = (uint8_t)(v - 250U); + } else { + raw = (uint8_t)v; + } + break; + case BL_T_UINT8: + case BL_T_BOOL: + default: + if (p->eeprom_offset == EEPROM_OFS_LIMITS_CURRENT) { + raw = (uint8_t)(v / 2U); + } else if (p->eeprom_offset == EEPROM_OFS_ADVANCE_LEVEL) { + raw = (uint8_t)(v + 10U); + } else { + raw = (uint8_t)v; + } + break; + } + return set_eeprom_byte(p->eeprom_offset, raw); +} + +/* + populate pkt.value / pkt.default_value / pkt.min_value / pkt.max_value + for parameter p_idx. For BL_T_UINT8/BL_T_BOOL parameters that live in + the default_settings[] region (offsets 0..47), the response's + default_value comes from default_settings (matching the main firmware); + for everything else we report the parameter table's default_value. + */ +static void bl_param_fill_response(struct uavcan_protocol_param_GetSetResponse *pkt, uint8_t p_idx) +{ + const struct bl_param *p = &bl_parameters[p_idx]; + const uint8_t off = p->eeprom_offset; + uint16_t cur_val = 0; + const bool have_numeric = bl_param_read_numeric(p_idx, &cur_val); + + switch (p->vtype) { + case BL_T_UINT8: { + pkt->value.union_tag = UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE; + pkt->value.integer_value = cur_val; + pkt->default_value.union_tag = UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE; + if (off < sizeof(default_settings)) { + uint16_t d = default_settings[off]; + if (off == EEPROM_OFS_LIMITS_CURRENT) d = (uint16_t)(d * 2U); + pkt->default_value.integer_value = d; + } else { + pkt->default_value.integer_value = p->default_value; + } + pkt->min_value.union_tag = UAVCAN_PROTOCOL_PARAM_NUMERICVALUE_INTEGER_VALUE; + pkt->min_value.integer_value = p->min_value; + pkt->max_value.union_tag = UAVCAN_PROTOCOL_PARAM_NUMERICVALUE_INTEGER_VALUE; + pkt->max_value.integer_value = p->max_value; + break; + } + case BL_T_BOOL: { + pkt->value.union_tag = UAVCAN_PROTOCOL_PARAM_VALUE_BOOLEAN_VALUE; + pkt->value.boolean_value = cur_val ? 1 : 0; + pkt->default_value.union_tag = UAVCAN_PROTOCOL_PARAM_VALUE_BOOLEAN_VALUE; + if (off < sizeof(default_settings)) { + pkt->default_value.boolean_value = default_settings[off] ? 1 : 0; + } else { + pkt->default_value.boolean_value = p->default_value ? 1 : 0; + } + break; + } + case BL_T_UINT16: { + pkt->value.union_tag = UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE; + pkt->value.integer_value = cur_val; + pkt->default_value.union_tag = UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE; + pkt->default_value.integer_value = p->default_value; + pkt->min_value.union_tag = UAVCAN_PROTOCOL_PARAM_NUMERICVALUE_INTEGER_VALUE; + pkt->min_value.integer_value = p->min_value; + pkt->max_value.union_tag = UAVCAN_PROTOCOL_PARAM_NUMERICVALUE_INTEGER_VALUE; + pkt->max_value.integer_value = p->max_value; + break; + } + case BL_T_STRING: { + pkt->value.union_tag = UAVCAN_PROTOCOL_PARAM_VALUE_STRING_VALUE; + const uint8_t *eeprom = (const uint8_t *)EEPROM_START_ADD; + const uint16_t maxlen = sizeof(pkt->value.string_value.data); + uint16_t slen = EEPROM_TUNE_LEN; + if (slen > maxlen) slen = maxlen; + pkt->value.string_value.len = slen; + memcpy(pkt->value.string_value.data, &eeprom[off], slen); + break; + } + } + (void)have_numeric; } /* handle uavcan.protocol.param.GetSet request. Supports lookup by name - or by index; set is only honoured when the request carries a non-empty - name AND a non-empty integer value, matching the main firmware's - convention. Response carries the current value plus the parameter name, - default_value, min_value and max_value so the DroneCAN GUI tool can - validate user input. + or by index. The set path is taken when the caller passes a name AND + a non-empty Value union; integers/booleans go through the scaling + helpers, strings (STARTUP_TUNE) go straight to EEPROM. + + Set is refused when the EEPROM magic isn't set: we'd otherwise be + writing into a page full of 0xFF, and the application would treat + the result as uninitialised on next boot. */ static void handle_param_GetSet(CanardInstance* ins, CanardRxTransfer* transfer) { @@ -320,40 +558,59 @@ static void handle_param_GetSet(CanardInstance* ins, CanardRxTransfer* transfer) p_idx = req.index; } - // set path: only when caller passed a name and a non-empty integer value. - // Refuse if the EEPROM magic isn't set; we'd otherwise be writing into a - // page full of 0xFF and the application would treat the result as - // uninitialised on next boot anyway. + // Set path. Refuse if EEPROM is unprovisioned; refuse the EMPTY union + // (that's how a pure Get-by-name is signalled). if (p_idx >= 0 && req.name.len != 0 && - req.value.union_tag == UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE && + req.value.union_tag != UAVCAN_PROTOCOL_PARAM_VALUE_EMPTY && eeprom[0] == 0x01) { - int64_t v = req.value.integer_value; - if (v < (int64_t)bl_parameters[p_idx].min_value) { - v = bl_parameters[p_idx].min_value; + const struct bl_param *p = &bl_parameters[p_idx]; + switch (p->vtype) { + case BL_T_BOOL: { + uint16_t v; + if (req.value.union_tag == UAVCAN_PROTOCOL_PARAM_VALUE_BOOLEAN_VALUE) { + v = req.value.boolean_value ? 1 : 0; + } else if (req.value.union_tag == UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE) { + v = req.value.integer_value ? 1 : 0; + } else { + break; + } + bl_param_write_numeric((uint8_t)p_idx, v); + break; + } + case BL_T_UINT8: + case BL_T_UINT16: { + if (req.value.union_tag != UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE) { + break; + } + int64_t v = req.value.integer_value; + if (v < 0) v = 0; + if (v > 0xFFFF) v = 0xFFFF; + bl_param_write_numeric((uint8_t)p_idx, (uint16_t)v); + break; + } + case BL_T_STRING: { + if (req.value.union_tag != UAVCAN_PROTOCOL_PARAM_VALUE_STRING_VALUE) { + break; + } + // STARTUP_TUNE: 128-byte slot at offset 48. Bytes beyond the + // request length are padded to 0xFF, matching the main firmware. + uint8_t tune[EEPROM_TUNE_LEN]; + const uint16_t slen = req.value.string_value.len; + for (uint16_t i = 0; i < sizeof(tune); i++) { + tune[i] = (i < slen) ? req.value.string_value.data[i] : 0xFF; + } + set_eeprom_bytes(EEPROM_OFS_TUNE, tune, sizeof(tune)); + break; } - if (v > (int64_t)bl_parameters[p_idx].max_value) { - v = bl_parameters[p_idx].max_value; } - set_eeprom_byte(bl_parameters[p_idx].eeprom_offset, (uint8_t)v); } - // build the response (current value, name, default/min/max). + // Build response. struct uavcan_protocol_param_GetSetResponse pkt; memset(&pkt, 0, sizeof(pkt)); if (p_idx >= 0) { - pkt.value.union_tag = UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE; - pkt.value.integer_value = bl_param_get((uint8_t)p_idx); - - pkt.default_value.union_tag = UAVCAN_PROTOCOL_PARAM_VALUE_INTEGER_VALUE; - pkt.default_value.integer_value = bl_parameters[p_idx].default_value; - - pkt.min_value.union_tag = UAVCAN_PROTOCOL_PARAM_NUMERICVALUE_INTEGER_VALUE; - pkt.min_value.integer_value = bl_parameters[p_idx].min_value; - - pkt.max_value.union_tag = UAVCAN_PROTOCOL_PARAM_NUMERICVALUE_INTEGER_VALUE; - pkt.max_value.integer_value = bl_parameters[p_idx].max_value; - + bl_param_fill_response(&pkt, (uint8_t)p_idx); const char *pname = bl_parameters[p_idx].name; pkt.name.len = strlen(pname); memcpy(pkt.name.data, pname, pkt.name.len); @@ -411,6 +668,7 @@ static void handle_param_ExecuteOpcode(CanardInstance* ins, CanardRxTransfer* tr &buffer[0], total_size); } +#endif // DRONECAN_PARAM_SUPPORT_ENABLED /* handle RestartNode request @@ -722,6 +980,7 @@ static void onTransferReceived(CanardInstance *ins, CanardRxTransfer *transfer) handle_begin_firmware_update(ins, transfer); break; } +#if DRONECAN_PARAM_SUPPORT_ENABLED case UAVCAN_PROTOCOL_PARAM_EXECUTEOPCODE_ID: { handle_param_ExecuteOpcode(ins, transfer); break; @@ -730,6 +989,7 @@ static void onTransferReceived(CanardInstance *ins, CanardRxTransfer *transfer) handle_param_GetSet(ins, transfer); break; } +#endif } } if (transfer->transfer_type == CanardTransferTypeResponse) { @@ -781,6 +1041,7 @@ static bool shouldAcceptTransfer(const CanardInstance *ins, *out_data_type_signature = UAVCAN_PROTOCOL_FILE_BEGINFIRMWAREUPDATE_SIGNATURE; return true; } +#if DRONECAN_PARAM_SUPPORT_ENABLED case UAVCAN_PROTOCOL_PARAM_EXECUTEOPCODE_ID: { *out_data_type_signature = UAVCAN_PROTOCOL_PARAM_EXECUTEOPCODE_SIGNATURE; return true; @@ -789,6 +1050,7 @@ static bool shouldAcceptTransfer(const CanardInstance *ins, *out_data_type_signature = UAVCAN_PROTOCOL_PARAM_GETSET_SIGNATURE; return true; } +#endif } } if (transfer_type == CanardTransferTypeResponse) { From d986bd3e93ffc4e71ec227b3e74eea73d3981d6f Mon Sep 17 00:00:00 2001 From: alexklimaj Date: Mon, 3 Aug 2026 17:12:57 -0600 Subject: [PATCH 22/25] feat: hold gate-driver ENABLE/nSLEEP low in bootloader Drive the smart gate-driver run pin low for the whole bootloader stay so DRV8350 (ARK 12S CAN) and DRV8328 (ARK 4IN1) remain in sleep: - ARK_G431_CAN: PC9 ENABLE via GATE_DRIVER_OFF_* - ARK_4IN1_F051: new board target (PB4 + PA15 nSLEEP low) - bl_gate_driver_off() on F051/G431; called from main after GPIO init --- Inc/targets.h | 9 +++++++++ Mcu/f051/Inc/blutil.h | 15 +++++++++++---- Mcu/g431/Inc/blutil.h | 26 ++++++++++++++++++++++++++ bootloader/main.c | 6 +++--- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/Inc/targets.h b/Inc/targets.h index c7a545fb..679891c4 100644 --- a/Inc/targets.h +++ b/Inc/targets.h @@ -30,6 +30,11 @@ GPIO_PORT_PIN(portnum, pinnum) where portnum is 0=A,1=B,2=C. USE_RGB_LED + RED_PORT/RED_PIN, GREEN_PORT/GREEN_PIN, BLUE_PORT/BLUE_PIN + GATE_DRIVER_OFF_PORT / GATE_DRIVER_OFF_PIN - smart gate-driver run pin + (DRV8350 ENABLE, DRV8328 + nSLEEP). Driven low for the + whole bootloader so the + driver stays in sleep. USE_HSE / HSE_VALUE / USE_HSE_BYPASS - external high-speed oscillator (see the per-MCU Mcu//Inc/ @@ -75,6 +80,10 @@ #define BLUE_PORT GPIOC #define BLUE_PIN LL_GPIO_PIN_8 +// DRV8350H ENABLE (PC9): hold low so the gate driver stays in sleep in BL +#define GATE_DRIVER_OFF_PORT GPIOC +#define GATE_DRIVER_OFF_PIN LL_GPIO_PIN_9 + // CAN termination pin on PC12, active high #define CAN_TERM_PIN GPIO_PORT_PIN(2, 12) // PC12 #define CAN_TERM_POLARITY 1 diff --git a/Mcu/f051/Inc/blutil.h b/Mcu/f051/Inc/blutil.h index 20f1a618..03985e21 100644 --- a/Mcu/f051/Inc/blutil.h +++ b/Mcu/f051/Inc/blutil.h @@ -146,15 +146,19 @@ static inline void bl_gpio_init(void) /* Hold the smart gate-driver run pin low for the whole bootloader stay - (e.g. DRV8328 nSLEEP on ARK 4IN1). Optional board CFLAGS: - -DGATE_DRIVER_OFF_PORT=GPIOA -DGATE_DRIVER_OFF_PIN_NUM=15 - (see Makefile ARK4IN1 product). + (e.g. DRV8328 nSLEEP on ARK 4IN1). Supports either: + -DGATE_DRIVER_OFF_PORT=GPIOA -DGATE_DRIVER_OFF_PIN_NUM=15 (Makefile product) + or GATE_DRIVER_OFF_PORT / GATE_DRIVER_OFF_PIN (targets.h LL pin mask) */ static inline void bl_gate_driver_off(void) { -#if defined(GATE_DRIVER_OFF_PORT) && defined(GATE_DRIVER_OFF_PIN_NUM) +#if defined(GATE_DRIVER_OFF_PORT) && (defined(GATE_DRIVER_OFF_PIN) || defined(GATE_DRIVER_OFF_PIN_NUM)) LL_GPIO_InitTypeDef s = {0}; +#if defined(GATE_DRIVER_OFF_PIN) + const uint32_t pin = GATE_DRIVER_OFF_PIN; +#else const uint32_t pin = (1U << (GATE_DRIVER_OFF_PIN_NUM)); +#endif if (GATE_DRIVER_OFF_PORT == GPIOA) { LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA); } else if (GATE_DRIVER_OFF_PORT == GPIOB) { @@ -170,6 +174,9 @@ static inline void bl_gate_driver_off(void) #endif } +#endif +} + /* return true if the MCU booted under a software reset */ diff --git a/Mcu/g431/Inc/blutil.h b/Mcu/g431/Inc/blutil.h index 2a5fd6fd..fe945868 100644 --- a/Mcu/g431/Inc/blutil.h +++ b/Mcu/g431/Inc/blutil.h @@ -175,6 +175,32 @@ static inline void bl_gpio_init(void) LL_GPIO_Init(input_port, &GPIO_InitStruct); } +/* + Hold the smart gate-driver run pin low (DRV8350 ENABLE, etc.) for the + whole bootloader so the charge pump / gate rails stay in sleep. + */ +static inline void bl_gate_driver_off(void) +{ +#ifdef GATE_DRIVER_OFF_PORT + LL_GPIO_InitTypeDef s = {0}; + /* Port may be C (ENABLE) while bl_gpio_init only clocks A/B. */ + if (GATE_DRIVER_OFF_PORT == GPIOA) { + LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOA); + } else if (GATE_DRIVER_OFF_PORT == GPIOB) { + LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOB); + } else if (GATE_DRIVER_OFF_PORT == GPIOC) { + LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOC); + } + s.Pin = GATE_DRIVER_OFF_PIN; + s.Mode = LL_GPIO_MODE_OUTPUT; + s.Speed = LL_GPIO_SPEED_FREQ_LOW; + s.OutputType = LL_GPIO_OUTPUT_PUSHPULL; + s.Pull = LL_GPIO_PULL_NO; + LL_GPIO_Init(GATE_DRIVER_OFF_PORT, &s); + LL_GPIO_ResetOutputPin(GATE_DRIVER_OFF_PORT, GATE_DRIVER_OFF_PIN); +#endif +} + /* RGB LED support, driven by per-board RED/GREEN/BLUE_PORT/_PIN from Inc/targets.h (active low, open drain). Pins are LL_GPIO_PIN_x masks. diff --git a/bootloader/main.c b/bootloader/main.c index 892788c0..7dc9a6fd 100644 --- a/bootloader/main.c +++ b/bootloader/main.c @@ -1416,11 +1416,11 @@ int main(void) bl_clock_config(); bl_timer_init(); bl_gpio_init(); -#if defined(GATE_DRIVER_OFF_PORT) && defined(GATE_DRIVER_OFF_PIN_NUM) - /* DRV8328 nSLEEP (etc.): keep the gate driver asleep while in the BL */ + bl_led_init(); +#if defined(GATE_DRIVER_OFF_PORT) && (defined(GATE_DRIVER_OFF_PIN) || defined(GATE_DRIVER_OFF_PIN_NUM)) + /* DRV8350 ENABLE / DRV8328 nSLEEP — keep gate driver asleep in BL */ bl_gate_driver_off(); #endif - bl_led_init(); #ifdef BOOTLOADER_TEST_CLOCK test_clock(); From 29b746c0a04feee0c2fb9282c6ad60ed3a15ece8 Mon Sep 17 00:00:00 2001 From: alexklimaj Date: Mon, 3 Aug 2026 18:16:13 -0600 Subject: [PATCH 23/25] bootloader: restore DShot/dual-protocol merge resolutions after rebase The dual-protocol import was a merge commit; linear rebase onto master dropped the conflict resolutions that: - prefer detect_fast_input_signal() for DroneCAN have_signal (bidir DShot) - skip the multi-ms DShot sample while a 4-way serial client is active - expect deviceInfo protocol version 3 in the SITL tests --- bootloader/main.c | 25 ++++++++++++++++++------- sitl/test_input_signal.c | 7 ++++++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/bootloader/main.c b/bootloader/main.c index 7dc9a6fd..4a997bbd 100644 --- a/bootloader/main.c +++ b/bootloader/main.c @@ -1438,17 +1438,23 @@ int main(void) the jump. Must run before checkForSignal(), whose float+low path calls jump() unconditionally - otherwise jump() is rejected with have_raw_command false and a DShot-only boot bounces. + + Prefer detect_fast_input_signal() so bidirectional DShot (idle high) is + recognised; fall back to a coarse "any low within ~5ms" sample for a + line held low without fast edges. */ { gpio_mode_set_input(input_pin, GPIO_PULL_UP); delayMicroseconds(500); - bool has_pin_signal = false; - for (int i = 0; i < 500; i++) { - if (!gpio_read(input_pin)) { - has_pin_signal = true; - break; + bool has_pin_signal = detect_fast_input_signal(); + if (!has_pin_signal) { + for (int i = 0; i < 500; i++) { + if (!gpio_read(input_pin)) { + has_pin_signal = true; + break; + } + delayMicroseconds(10); } - delayMicroseconds(10); } if (has_pin_signal) { DroneCAN_set_have_signal(); @@ -1493,7 +1499,12 @@ int main(void) turn one error into several. A real flight controller drives the pin continuously and trips this within a few bytes regardless. */ - if (invalid_command > 2 && detect_fast_input_signal()) { + /* + Skip the multi-ms DShot sample while a 4-way serial client is + active - same reason we pause DroneCAN: staring at the pin would + drop bytes mid-transfer. + */ + if (!bl_serial_active && invalid_command > 2 && detect_fast_input_signal()) { jump(); } #if DRONECAN_SUPPORT diff --git a/sitl/test_input_signal.c b/sitl/test_input_signal.c index 187745be..fa9eb219 100644 --- a/sitl/test_input_signal.c +++ b/sitl/test_input_signal.c @@ -41,9 +41,14 @@ device info the bootloader answers a configurator with, for the F051 / PB4 target this is built for: '4','7','1', pin code, flash size code, 0x06, 0x06, protocol version, 0x30 + + Protocol version 3 adds ADDRESS_MAGIC_DEVINFO (self-describing region + map) but the 9-byte deviceInfo reply is unchanged aside from the + version byte. Keep this in sync with BOOTLOADER_PROTOCOL_VERSION in + bootloader/main.c. */ static const uint8_t expected_devinfo[9] = {'4', '7', '1', 0x14, 0x1F, - 0x06, 0x06, 0x02, 0x30}; + 0x06, 0x06, 0x03, 0x30}; typedef struct { const char *name; From 0a764a20d5174d40ef8ebc5288e512169a115d84 Mon Sep 17 00:00:00 2001 From: alexklimaj Date: Mon, 3 Aug 2026 21:43:44 -0600 Subject: [PATCH 24/25] fix(F051): remove stray #endif/} after bl_gate_driver_off d986bd3 accidentally left an extra preprocessor close and brace after bl_gate_driver_off(), which broke every F051 bootloader build in CI. --- Mcu/f051/Inc/blutil.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/Mcu/f051/Inc/blutil.h b/Mcu/f051/Inc/blutil.h index 03985e21..4b3c04ec 100644 --- a/Mcu/f051/Inc/blutil.h +++ b/Mcu/f051/Inc/blutil.h @@ -174,9 +174,6 @@ static inline void bl_gate_driver_off(void) #endif } -#endif -} - /* return true if the MCU booted under a software reset */ From 060c417b9dca9abf381bd10cb97346961e51d6e2 Mon Sep 17 00:00:00 2001 From: alexklimaj Date: Thu, 13 Aug 2026 22:30:29 -0600 Subject: [PATCH 25/25] feat(ARK_G431_CAN): report UAVCAN hardware 0.71 (board_id 71) Match ARK32 GetNodeInfo so PX4 SD-card recovery while the ESC is in the bootloader looks for /ufw/71.bin, not stock AM32 2.3/515. Other CAN bootloaders keep the upstream 2.3 default. --- Inc/targets.h | 3 +++ bootloader/DroneCAN/DroneCAN.c | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Inc/targets.h b/Inc/targets.h index 679891c4..bc2864be 100644 --- a/Inc/targets.h +++ b/Inc/targets.h @@ -64,6 +64,9 @@ #define FILE_NAME "ARK_G431_CAN" // parser: MCU=G431, CAN build #define TARGET_TAG ARKG4 // -> AM32_G431_BOOTLOADER_ARKG4_CAN #define USE_PB4 // bit-banged comms pin +/* Must match ARK32 Inc/targets.h. PX4 board_id = (major << 8) | minor = 71. */ +#define DRONECAN_HW_VERSION_MAJOR 0 +#define DRONECAN_HW_VERSION_MINOR 71 // FDCAN1 pins: RX PA11, TX PB9 (AF9) #define CAN_RX_PORT GPIOA diff --git a/bootloader/DroneCAN/DroneCAN.c b/bootloader/DroneCAN/DroneCAN.c index 13734589..0221a99e 100644 --- a/bootloader/DroneCAN/DroneCAN.c +++ b/bootloader/DroneCAN/DroneCAN.c @@ -700,9 +700,13 @@ static void handle_GetNodeInfo(CanardInstance *ins, CanardRxTransfer *transfer) pkt.software_version.optional_field_flags = 0; pkt.software_version.vcs_commit = 0; // should put git hash in here - // should fill in hardware version +#ifdef DRONECAN_HW_VERSION_MAJOR + pkt.hardware_version.major = DRONECAN_HW_VERSION_MAJOR; + pkt.hardware_version.minor = DRONECAN_HW_VERSION_MINOR; +#else pkt.hardware_version.major = 2; pkt.hardware_version.minor = 3; +#endif sys_can_getUniqueID(pkt.hardware_version.unique_id);