Skip to content

Import dual CAN+DShot protocol and DroneCAN bootloader params (ARK_G431_CAN) - #2

Open
AlexKlimaj wants to merge 25 commits into
masterfrom
feat/ark-g431-can-dual-protocol
Open

Import dual CAN+DShot protocol and DroneCAN bootloader params (ARK_G431_CAN)#2
AlexKlimaj wants to merge 25 commits into
masterfrom
feat/ark-g431-can-dual-protocol

Conversation

@AlexKlimaj

Copy link
Copy Markdown
Member

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.h approach instead of a separate ARKG4 MCU type.

Pairs with firmware ARK-Electronics/ARK32#36.

ARK_G431_CAN (matches PR am32-firmware#60 / firmware PR am32-firmware#36)

Item Value
Signal pin PB4
FDCAN1 RX PA11, TX PB9
RGB LED PC6/7/8 active-low OD
CAN_TERM PC12 (from EEPROM byte 183)
Clock 8 MHz HSE
RAM limit 112 KB (G491)
Build AM32_G431_BOOTLOADER_ARKG4_CAN

Also included

Not 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_CAN
  • make sitl_test — 32/32 pass
  • Hardware: DroneCAN enum + FW update on ARK 12S CAN
  • Hardware: 4-way serial / configurator with CAN present
  • Hardware: DShot on PB4 boots app; RGB + CAN_TERM in BL
  • Follow-up (PR2): embed .bin in ARK32 factory image

tridge and others added 23 commits August 3, 2026 18:12
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
AlexKlimaj force-pushed the feat/ark-g431-can-dual-protocol branch from 086755b to 29b746c Compare August 4, 2026 00:16
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants