diff --git a/Inc/targets.h b/Inc/targets.h new file mode 100644 index 00000000..bc2864be --- /dev/null +++ b/Inc/targets.h @@ -0,0 +1,214 @@ +/* + 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) + 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 + 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/ + 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 + 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). +*/ + +/* + 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 +#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 +#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 + +// 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 + +// 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 + +#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 + +#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 diff --git a/Makefile b/Makefile index 906f5e3a..128d5fb4 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)) @@ -47,8 +54,11 @@ 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 CFLAGS_COMMON := $(CFLAGS_BASE) @@ -62,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 @@ -137,6 +150,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 +184,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 +198,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 +293,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/Mcu/f051/Inc/blutil.h b/Mcu/f051/Inc/blutil.h index 20f1a618..4b3c04ec 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) { diff --git a/Mcu/g431/Inc/blutil.h b/Mcu/g431/Inc/blutil.h index dab80293..fe945868 100644 --- a/Mcu/g431/Inc/blutil.h +++ b/Mcu/g431/Inc/blutil.h @@ -175,6 +175,95 @@ 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. + */ +#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/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); 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(); } diff --git a/bootloader/DroneCAN/DroneCAN.c b/bootloader/DroneCAN/DroneCAN.c index 216f6768..0221a99e 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 @@ -52,6 +65,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 */ @@ -63,7 +85,22 @@ 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; + +// 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; +} // some convenience macros #define MIN(a,b) ((a)<(b)?(a):(b)) @@ -117,14 +154,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) { @@ -135,11 +171,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; } /* @@ -169,7 +206,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; @@ -189,6 +230,405 @@ 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 + +#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. + 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 bl_param { + const char *name; + 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/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 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_bytes(uint16_t offset, const uint8_t *data, uint16_t len) +{ + static uint8_t buf[EEPROM_PRESERVE_SIZE]; + if ((uint32_t)offset + len > sizeof(buf)) { + return false; + } + memcpy(buf, (const void *)EEPROM_START_ADD, sizeof(buf)); + if (memcmp(buf + offset, data, len) == 0) { + // nothing to do; avoid an unnecessary erase cycle. + return true; + } + 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) 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 bool bl_param_read_numeric(uint8_t p_idx, uint16_t *out_val) +{ + const uint8_t *eeprom = (const uint8_t *)EEPROM_START_ADD; + 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; + } + *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. 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) +{ + 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. 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_EMPTY && + eeprom[0] == 0x01) { + 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; + } + } + } + + // Build response. + struct uavcan_protocol_param_GetSetResponse pkt; + memset(&pkt, 0, sizeof(pkt)); + + if (p_idx >= 0) { + 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); + } + + 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 @@ -208,6 +648,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; } @@ -224,6 +668,7 @@ static void handle_param_ExecuteOpcode(CanardInstance* ins, CanardRxTransfer* tr &buffer[0], total_size); } +#endif // DRONECAN_PARAM_SUPPORT_ENABLED /* handle RestartNode request @@ -245,7 +690,8 @@ static void handle_GetNodeInfo(CanardInstance *ins, CanardRxTransfer *transfer) memset(&pkt, 0, sizeof(pkt)); - node_status.uptime_sec = micros64() / 1000000ULL; + // 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 @@ -254,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); @@ -534,10 +984,16 @@ 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; } + case UAVCAN_PROTOCOL_PARAM_GETSET_ID: { + handle_param_GetSet(ins, transfer); + break; + } +#endif } } if (transfer->transfer_type == CanardTransferTypeResponse) { @@ -589,10 +1045,16 @@ 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; } + case UAVCAN_PROTOCOL_PARAM_GETSET_ID: { + *out_data_type_signature = UAVCAN_PROTOCOL_PARAM_GETSET_SIGNATURE; + return true; + } +#endif } } if (transfer_type == CanardTransferTypeResponse) { @@ -628,7 +1090,8 @@ static void send_NodeStatus(void) { uint8_t buffer[UAVCAN_PROTOCOL_GETNODEINFO_RESPONSE_MAX_SIZE]; - node_status.uptime_sec = micros64() / 1000000ULL; + // 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; @@ -661,17 +1124,27 @@ 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 + 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++; } /* @@ -681,7 +1154,8 @@ void DroneCAN_receiveFrame(void) { CanardCANFrame rx_frame = {0}; while (sys_can_receive(&rx_frame) > 0) { - canardHandleRxFrame(&canard, &rx_frame, micros64()); + can_seen = true; + canardHandleRxFrame(&canard, &rx_frame, (uint64_t)micros32()); } } @@ -690,7 +1164,8 @@ void DroneCAN_receiveFrame(void) */ void DroneCAN_handleFrame(CanardCANFrame *frame) { - canardHandleRxFrame(&canard, frame, micros64()); + can_seen = true; + canardHandleRxFrame(&canard, frame, (uint64_t)micros32()); } /* @@ -753,6 +1228,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"); @@ -768,7 +1263,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; @@ -779,6 +1274,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 @@ -789,10 +1311,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); } @@ -831,10 +1356,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); } @@ -885,8 +1411,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; } 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/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..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; @@ -256,5 +261,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..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; @@ -455,5 +474,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 3e7e7368..0b302a3a 100644 --- a/bootloader/DroneCAN/sys_can_stm32_CANFD.c +++ b/bootloader/DroneCAN/sys_can_stm32_CANFD.c @@ -132,52 +132,60 @@ 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) { - // 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) @@ -280,9 +288,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 +390,46 @@ static void can_init(void) void sys_can_init(void) { // Setup CAN RX and TX pins - // assumes PA11/PA12 for FDCAN1 + // 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 +#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(); @@ -416,4 +452,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) diff --git a/bootloader/main.c b/bootloader/main.c index f7b07a95..4a997bbd 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 @@ -127,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" @@ -185,8 +230,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 +246,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 +299,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 @@ -245,6 +329,39 @@ static bool messagereceived; static int cmd; static int received; static bool initialized; +/* + 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. 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; @@ -327,7 +444,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; @@ -348,12 +468,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 } @@ -411,15 +535,23 @@ 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() { 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() @@ -438,6 +570,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 + mark_serial_active(); } static bool checkAddressWritable(uint32_t address) @@ -560,6 +695,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(); @@ -584,6 +724,7 @@ static void decodeInput() return; } + mark_serial_active(); // no ack with command set buffer; if (rxBuffer[2] == 0x01) { @@ -605,6 +746,7 @@ static void decodeInput() return; } + mark_serial_active(); serialwriteOneChar(0xC1); // bad command message. return; @@ -639,6 +781,7 @@ static void decodeInput() return; } + mark_serial_active(); if (address == 0) { // must send SET_ADDRESS first @@ -715,23 +858,49 @@ 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 - if (DroneCAN_update()) { + // 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 (!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(); + } #endif - if (messagereceived) { - // we've been waiting too long, don't allow for long gaps - // between bytes -#ifdef SERIAL_STATS - stats.no_start++; +#ifdef USE_RGB_LED + bl_led_update(); #endif - return false; - } - } } + // 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 +909,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,12 +932,23 @@ 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; +#if DRONECAN_SUPPORT + bl_serial_idle_count = 0; +#endif receiveByte = rxbyte; #ifdef SERIAL_STATS stats.good++; @@ -808,6 +991,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 +1001,9 @@ static void sendString(const uint8_t *data, int len) delayMicroseconds(BITTIME); } setReceive(); +#if DRONECAN_SUPPORT + sys_can_enable_IRQ(); +#endif } static void receiveBuffer() @@ -1227,8 +1416,9 @@ 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 @@ -1242,6 +1432,41 @@ 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. + + 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 = 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); + } + } + if (has_pin_signal) { + 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(); gpio_mode_set_input(input_pin, GPIO_PULL_UP); @@ -1274,11 +1499,16 @@ 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 - if (DroneCAN_update()) { + if (!bl_serial_active && DroneCAN_update()) { jump(); } #endif 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() 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;