Support dual CAN + DShot protocols#68
Open
tridge wants to merge 11 commits into
Open
Conversation
This was referenced May 27, 2026
Generalise the hardcoded PA11/PA12 FDCAN1 pin setup to FDCAN_RX/TX_PORT, _PIN and _AF macros (defaulting to the original PA11/PA12 AF9), and enable GPIOB so boards can route CAN to other ports. Also bound waitForBitState with a finite try count so CAN init can no longer spin forever if the peripheral never reaches the expected state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets a CAN-enabled bootloader keep servicing DroneCAN while still accepting 4-way serial, so a board driven by either DShot or DroneCAN can be flashed over whichever transport the host uses. DroneCAN_boot_ok() does a multi-millisecond memmem + two crc32s over the whole firmware. Called from the serial-wait loop and the main loop, that scan deafened the bit-banged serial and corrupted 4-way reads, so settings and flash failed with ACK_D_GENERAL_ERROR whenever DroneCAN was active. Once a configuration client connects (sendDeviceInfo()) we set bl_serial_active and stop polling DroneCAN for the rest of the session; a config session always ends in a reset, and normal flight - where no client connects - is unaffected. While no client is connected we still poll DroneCAN, but only every ~50ms in the start-bit wait loop so the scan can't swallow a start bit. CAN IRQs are disabled around every bit-banged read and write to protect the timing. At boot we sample the signal pin: if it is being driven (e.g. DShot) we call DroneCAN_set_have_signal() so DroneCAN_boot_ok() won't block waiting for a CAN ESCRawCommand before booting the app. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add bootloader protocol version 3. The 9-byte deviceInfo returned by
sendDeviceInfo() is unchanged, so pre-v3 clients see no wire change. All v3
data lives in a packed, self-describing struct read via ADDRESS_MAGIC_DEVINFO
(0x23) - a SET_ADDRESS magic that maps to the devinfo struct - so a config
client can read it even over a 4-way passthrough whose InitFlash reply only
forwards a short signature. The read returns magic1, magic2 (to confirm
support) then the 9-byte deviceInfo and the v3 fields:
length - sizeof(devinfo), for forward-compatible parsing
address_shift - the CMD_SET_ADDRESS shift (2 on 128k parts)
firmware_start / filename_start / eeprom_start / tune_start
- the values to pass to CMD_SET_ADDRESS for each region,
stored >> address_shift so they fit the 16-bit address;
(address << address_shift) + MCU_FLASH_START reconstructs
the real flash address, matching decodeInput().
Also fix FIRMWARE_RELATIVE_START to test DRONECAN_SUPPORT by value
(#if DRONECAN_SUPPORT) rather than defined(): the build always defines it as
0 or 1, so every non-CAN build was wrongly getting 0x4000.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a mechanism for board-specific bootloader builds, modelled on the main AM32 firmware repo's Inc/targets.h. Each board is a "#ifdef <BOARDNAME>" block holding a subset of the firmware repo's lines for that ESC. make/parse_targets.py scans Inc/targets.h and, for each board block with a FILE_NAME, emits the MCU (from the name), the _CAN flag, and the TARGET_TAG. The Makefile feeds these to the existing CREATE_BOOTLOADER_TARGET via the "pin" slot, so the ARK_G431_CAN block generates AM32_G431_BOOTLOADER_ARKG4_CAN with -DARK_G431_CAN. targets.h is force-included into every TU (via CFLAGS_BASE) and is inert for builds with no board define, so generic targets are unchanged. This first board is the ARK G4 ESC: FDCAN1 TX on PB9, RGB LED on PC6/7/8, 8MHz HSE and a 112KB RAM limit - none of which the generic G431_CAN target can express. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire the bootloader source to the per-board macros set by Inc/targets.h: - sys_can_stm32_CANFD.c: map the main-firmware-style CAN_RX/TX_PORT/_PIN names onto the bootloader's FDCAN_* names (AF still defaults to AF9), so a board can route FDCAN to non-default pins (ARK uses TX on PB9). - Mcu/g431/Inc/blutil.h: add RGB LED support keyed on RED/GREEN/BLUE_PORT/_PIN (open drain, active low) with a small per-port clock enable. No debug UART. - main.c: re-add the LED hooks (no-op stubs when !USE_RGB_LED, plus a blink state machine that shows error/normal while stuck in the bootloader), and generalize the jump() RAM check to RAM_LIMIT_KB (default 64) so the ARK's 112KB lets the bootloader accept an app whose stack sits above 64KB. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If a config client probed deviceInfo (which sets bl_serial_active to suppress DroneCAN polling during the bit-banged serial session) and then disconnected without resetting the ESC, the bootloader could livelock inside serialreadChar()'s start-bit-wait: bl_serial_active blocks the DroneCAN_update() branch and messagereceived is cleared at the top of each receiveBuffer() call, so the 5*BITTIME exit doesn't fire either. Track consecutive ~50ms idle ticks while bl_serial_active is set and clear it after BL_SERIAL_IDLE_THRESHOLD ticks (~5 min of *complete* serial silence). 5 minutes is well above any realistic inter-command pause inside an active configurator session — reading a settings page, typing a value, taking a phone call — but any received byte resets the counter, so an interactive session stays alive indefinitely. Abandoned bootloaders eventually recover. A future protocol revision could add an explicit "session ended" command so cooperative clients recover instantly on disconnect. Guarded by DRONECAN_SUPPORT so non-CAN builds are unchanged. Reported by codex in PR review; threshold tightened from initial 1s to 5 min over two codex follow-up rounds to ensure paused-GUI sessions remain unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closed
… TBS_F415_CAN, and ARK CAN_TERM_PIN
Add five new custom-board blocks matching the like-named blocks in the
main AM32 firmware repo (../AM32/Inc/targets.h), back-fill the
ARK_G431_CAN block with CAN_TERM_PIN (mirrored from the main firmware),
and extend the optional-fields documentation to cover the new
oscillator and termination knobs the bootloader is about to start
honouring.
Each new board block carries the subset that the bootloader actually
consumes:
- FILE_NAME / TARGET_TAG generate the build target name via
make/parse_targets.py
- USE_P<port><n> the bit-banged comms pin, taken from
the matching HARDWARE_GROUP_*'s
INPUT_PIN / INPUT_PIN_PORT
- CAN_TERM_PIN /
CAN_TERM_POLARITY CAN bus termination pin (ARK + the
three TBS boards). Encoded with the
GPIO_PORT_PIN macro mirrored from the
main firmware. The bootloader starts
respecting these in the follow-up
commit that wires up setup_portpin()
from DroneCAN_Startup().
- USE_HSE / HSE_VALUE /
USE_HSE_BYPASS SEQURE: 8MHz external crystal
- USE_LSE / USE_LSE_BYPASS TBS_12S_L431_CAN (crystal),
TBS_16S_L431_CAN (external clock).
The L431 bootloader will start
honouring these in the follow-up
oscillator commit.
Generated build targets:
AM32_G431_BOOTLOADER_SEQUREG4_CAN
AM32_L431_BOOTLOADER_TBS12SL4_CAN
AM32_L431_BOOTLOADER_TBS16SL4_CAN
AM32_F415_BOOTLOADER_TBS12SF4_CAN
AM32_F415_BOOTLOADER_TBSF4_CAN
Fields deliberately omitted from these blocks (present in the main
firmware but unused by the bootloader): motor-control parameters
(DEAD_TIME, TARGET_VOLTAGE_DIVIDER, MILLIVOLT_PER_AMP, RAMP_SPEED_*,
LOOP_FREQUENCY_HZ), ADC pin/channel selections, USE_SERIAL_TELEMETRY,
USE_LED_STRIP. CAN_RX/TX pins are also omitted because all five boards
use the PA11/PA12 default that's hardcoded in the L431 / F415 CAN
drivers and defaulted in the G431 FDCAN driver.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Boards that route a CAN bus termination resistor through an MCU GPIO
(e.g. TBS_12S_L431_CAN, TBS_16S_L431_CAN, TBS_12S_F415_CAN) currently
have that pin floating during the bootloader window because the
bootloader never reads the termination setting from the EEPROM.
Port setup_portpin() from ../AM32/Src/DroneCAN/sys_can_*.c into each of
the three bootloader CAN drivers (sys_can_stm32.c for L431 bxCAN,
sys_can_stm32_CANFD.c for G431 FDCAN, sys_can_at32.c for F415) and
declare it in sys_can.h. The implementations mirror the main firmware
byte-for-byte so the same CAN_TERM_PIN encoding (GPIO_PORT_PIN(port,pin)
with portnum 0=A,1=B,2=C) drives the line the same way under both.
In DroneCAN_Startup(), after sys_can_init(), read EEPROM byte 183
(eepromBuffer.can.term_enable per ../AM32/Inc/eeprom.h) and call
setup_portpin(CAN_TERM_PIN,
term_enable ? CAN_TERM_POLARITY : !CAN_TERM_POLARITY).
This matches the call in ../AM32/Src/DroneCAN/DroneCAN.c:1030. If the
EEPROM magic byte is not 0x01 (uninitialised) we default term_enable
to 0, also matching the main firmware default for that parameter.
The whole call is wrapped in #ifdef CAN_TERM_PIN, and setup_portpin
itself is only compiled in CAN builds (the existing
DRONECAN_SUPPORT/MCU_* guards in sys_can_*.c), so boards without a
termination pin pay nothing — verified: ARK_G491_CAN and TBS_F415_CAN
sizes are unchanged (setup_portpin is DCE'd by --gc-sections), boards
with the new functionality grow by ~100 bytes for the call site and
GPIO setup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the oscillator-selection logic from
../AM32/Mcu/l431/Src/peripherals.c so per-board #defines drive the
bootloader and the application identically. Previously
bl_clock_config() ignored the USE_* defines and always brought up
HSI16 (with a buggy duplicate-PLL-config sequence that re-configured
the PLL source after enabling it — harmless because the second config
silently failed against an enabled PLL, but confusing).
Now bl_clock_config() supports:
USE_HSE external HSE crystal/oscillator; HSE_VALUE 8/16/24 MHz.
USE_HSE_BYPASS=0 selects crystal mode, otherwise external
clock-source bypass.
USE_LSE MSI clock disciplined by a 32.768 kHz LSE (USE_LSE_BYPASS
selects external clock vs crystal). RTC also runs from LSE.
USE_MSI free-running MSI.
(default) HSI16, unchanged behaviour from before.
All branches converge on an 80 MHz PLL output (verified by inspection).
For non-LSE paths an LSI source is still set up so the RTC stays
clocked and the BKP registers (used by sys_can_*.c for the DroneCAN
handoff to the application) remain functional. Non-CAN builds compile
fine but the LSI/RTC overhead is the same as before — net non-CAN
L431 image size is -32 bytes, a small win from removing the duplicate
PLL config.
Also enable backup-domain access in bl_clock_config() (was previously
deferred to bl_gpio_init); needed to change the RTC clock source under
USE_LSE.
The two TBS L431 board blocks are hooked up to USE_LSE in the preceding
targets commit (TBS_12S uses crystal mode, TBS_16S uses external clock
bypass), mirroring the main firmware.
The same oscillator handles will work for non-CAN custom boards added
in future — the code path is independent of DRONECAN_SUPPORT.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…50_L431_CAN
Mirror the three Vimdrones CAN board blocks from the main AM32 firmware
repo (../AM32/Inc/targets.h). All three are L431 with HARDWARE_GROUP_L4_B
(PA2 signal pin); NANO and S50 carry 24 MHz external HSE which the
L431 bootloader now honours via the per-board oscillator support
landed earlier today.
- VIMDRONES_L431_CAN -> AM32_L431_BOOTLOADER_VIML4_CAN
(no oscillator override -> HSI16)
- VIMDRONES_NANO_L431_CAN -> AM32_L431_BOOTLOADER_VIMNANO_CAN
(24 MHz HSE external clock)
- VIMDRONES_S50_L431_CAN -> AM32_L431_BOOTLOADER_VIMS50_CAN
(24 MHz HSE external clock, same as NANO)
None of the three define CAN_TERM_PIN in the main firmware, so the
bootloader's termination GPIO machinery stays dead-code-eliminated for
these targets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bl_update firmware runs from the app slot in bank 1 and erases + reprograms the bootloader (also in bank 1) over its own AHB path. On dual-bank STM32G4 parts (verified on STM32G491 / G4Axx with DBANK=1, which is the default factory option byte) reads from bank 1 stall during any bank-1 write, including the implicit instruction fetches the cortex-m core does when an interrupt arrives. If an interrupt fires while a flash page is being erased or programmed, the CPU attempts to fetch the handler from a half-erased bank and the fetch returns either undefined data or a stall that the EXCRETURN logic cannot recover from cleanly, taking the CPU into HardFault. The HardFault handler in the partially-written bootloader is now a `b .` infinite loop, so the chip is wedged with a corrupted bootloader and a soft reset can't recover it - SWD reflash is the only way out. Observed on ARK_G431_CAN (G491): the updater erased and programmed the first three 2 KB pages of bank 1 (0x08000000 - 0x08001800) correctly, then HardFaulted during the erase or program of page 3 (0x08001800), leaving the bootloader 1.5 KB written and 2.5 KB erased. `__disable_irq()` before `flash_bootloader()` removes the race - no interrupts can fire during the flash sequence. We don't bother re-enabling because `NVIC_SystemReset()` immediately follows; the reset restores PRIMASK. STM32L431 (vimdrones) didn't observe the failure mode on bench but the same race window exists; the fix is harmless there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
This adds support for a single bootloader for ESCs to support both CAN and DShot. Firmware updates and settings can all be done via:
The key change is adding a new magic address to fetch a v3 devinfo structure which gives all the addresses the client will need to use to talk to the ESC via the 4-way protocol. This supersedes the old single byte pin code while still being backwards compatible with old configuration tools and ESCs.
This PR also allows for custom bootloaders as needed for particular boards. In this PR several custom bootloaders are added, with specific pins for CAN and LED defines as well as oscillator settings. This sets us up for other custom bootloaders as they are needed. This will be particularly good for CAN ESCs with external oscillators, at the moment they are only used in the main firmware, which means a reset when temperature is a long way off (eg. reset when very high and cold) could leave us stuck in the bootloader if the CAN peripheral is driven too far off spec.
this replaces PR #60 - thanks to Alex for the work this PR is based on
NOTE: I've been doing some more testing and I think there are still issues with this, hold off on merging