Import dual CAN+DShot protocol and DroneCAN bootloader params (ARK_G431_CAN) - #2
Open
AlexKlimaj wants to merge 25 commits into
Open
Import dual CAN+DShot protocol and DroneCAN bootloader params (ARK_G431_CAN)#2AlexKlimaj wants to merge 25 commits into
AlexKlimaj wants to merge 25 commits into
Conversation
5 tasks
AlexKlimaj
force-pushed
the
feat/ark-g431-can-dual-protocol
branch
from
August 3, 2026 23:17
20ebac2 to
086755b
Compare
5 tasks
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.
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.
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.
Each board is a "#ifdef <BOARDNAME>" 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).
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.
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.
… 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<pin> 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.
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.
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.
…50_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.
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.
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.
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.
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.
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.
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).
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.
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.
… 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).
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.
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.
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
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
AlexKlimaj
force-pushed
the
feat/ark-g431-can-dual-protocol
branch
from
August 4, 2026 00:16
086755b to
29b746c
Compare
d986bd3 accidentally left an extra preprocessor close and brace after bl_gate_driver_off(), which broke every F051 bootloader build in CI.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bring am32-firmware/AM32-bootloader#68 (
tridge:pr-dual-protocol) and #69 (tridge:pr-bl-parameters) into the ARK fork for the ARK 12S CAN ESC.This supersedes the closed original board PR am32-firmware/AM32-bootloader#60 (AlexKlimaj:ark-g4-bootloader), which tridge closed as replaced by am32-firmware#68. Same hardware goals as am32-firmware#60, with the data-driven
Inc/targets.happroach instead of a separateARKG4MCU type.Pairs with firmware ARK-Electronics/ARK32#36.
ARK_G431_CAN (matches PR am32-firmware#60 / firmware PR am32-firmware#36)
AM32_G431_BOOTLOADER_ARKG4_CANAlso included
ADDRESS_MAGIC_DEVINFODETECT_SERIAL_CLIENT=0Not from PR am32-firmware#60
Test plan
make AM32_G431_BOOTLOADER_ARKG4_CAN— links (~88% of 16K flash)make AM32_G431_BOOTLOADER_PB4— 4K non-CAN still fits (~85%)make AM32_G431_BOOTLOADER_PB4_CANmake sitl_test— 32/32 pass.binin ARK32 factory image