From 1e7ddd3fd726dba8e1f443ef9ae174aeed6c8c92 Mon Sep 17 00:00:00 2001 From: Parthiv Shah Date: Sat, 25 Apr 2026 19:55:16 -0500 Subject: [PATCH 01/12] stable flash no uart callback --- Makefile | 66 ++-- bootloader/Inc/bootloader_config.h | 46 +++ bootloader/Inc/bootloader_hal.h | 14 + bootloader/Inc/bootloader_indicator.h | 18 + bootloader/Inc/bootloader_runtime.h | 18 + bootloader/Src/bootloader_indicator.c | 80 ++++ bootloader/Src/bootloader_main.c | 505 ++++++++++++++++++++++++++ bootloader/Src/bootloader_runtime.c | 244 +++++++++++++ common/Src/stubs.c | 50 +++ docs/UartBootloader.md | 86 +++++ scripts/uart_bootloader_flash.py | 58 +++ test/Makefile | 6 + test/tests/blinky_bootloader_test.c | 57 +++ 13 files changed, 1222 insertions(+), 26 deletions(-) create mode 100644 bootloader/Inc/bootloader_config.h create mode 100644 bootloader/Inc/bootloader_hal.h create mode 100644 bootloader/Inc/bootloader_indicator.h create mode 100644 bootloader/Inc/bootloader_runtime.h create mode 100644 bootloader/Src/bootloader_indicator.c create mode 100644 bootloader/Src/bootloader_main.c create mode 100644 bootloader/Src/bootloader_runtime.c create mode 100644 docs/UartBootloader.md create mode 100755 scripts/uart_bootloader_flash.py create mode 100644 test/tests/blinky_bootloader_test.c diff --git a/Makefile b/Makefile index eaa83fa1..0641be01 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,8 @@ CLANG_INPUTS := $(filter-out $(IGNORED_CLANG_INPUTS), $(CLANG_INPUTS)) MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) BEAR_ENABLE ?= 1 +FIRMWARE_ROLE ?= app +BOOTLOADER_SIZE_KB ?= 64 ###################################### # target @@ -47,27 +49,9 @@ SERIES_GENERIC_CAP = STM32$(SERIES_CAP)xx SERIES_LINE = stm32$(SERIES)$(LINE) SERIES_LINE_CAP = STM32$(SERIES_CAP)$(LINE) -# Generate a list of STM32 variant patterns: -# 1. Remove the '.h' extension from filenames. -# 2. Replace 'x' with '.' for regex matching. -MCU_MATCHES = $(shell ls stm/$(SERIES_GENERIC)/CMSIS/Device/ST/$(SERIES_GENERIC_CAP)/Include \ - | sed 's/\.h//g; s/x/./g') - -# Find the generic STM32 series variant: -# 1. Iterate over MCU_MATCHES. -# 2. Check if the entry is exactly 11 characters long. -# 3. Match it against the pattern 'stm32$(SERIES_LINE)$(EXTRA_CUT)'. -# 4. Replace '.' with 'x' in the matched entry. -# 5. Break on the first valid match. -SERIES_LINE_GENERIC = $(shell \ - for match in $(MCU_MATCHES); do \ - if [ $${#match} -eq 11 ] && echo "stm32$(SERIES_LINE)$(EXTRA_CUT)" | grep -qE "$$match"; then \ - echo "$${match//./x}"; \ - break; \ - fi; \ - done) - -SERIES_LINE_GENERIC_CAP = $(shell echo $(SERIES_LINE_GENERIC) | sed 's/[^x]/\U&/g') +SERIES_LINE_GENERIC = $(SERIES_LINE)xx + +SERIES_LINE_GENERIC_CAP = $(shell python3 -c 's="$(SERIES_LINE_GENERIC)"; print(s[:-2].upper() + s[-2:])') ifeq ($(strip $(SERIES_LINE_GENERIC)),) $(error SERIES_LINE_GENERIC is not found in stm/$(SERIES_GENERIC)/CMSIS/Device/ST/$(SERIES_GENERIC_CAP)/Include. Please check the target configuration.) @@ -104,12 +88,14 @@ FATFS_PATH := middleware/FatFs # source ###################################### # C sources -C_SOURCES = \ -$(PROJECT_C_SOURCES) \ +COMMON_STM_SOURCES = \ $(filter-out %template.c, $(wildcard stm/$(SERIES_GENERIC)/$(SERIES_GENERIC_CAP)_HAL_Driver/Src/*.c)) \ stm/$(SERIES_GENERIC)/system_$(SERIES_GENERIC).c \ stm/$(SERIES_GENERIC)/$(SERIES_GENERIC)_hal_init.c \ -stm/$(SERIES_GENERIC)/$(SERIES_GENERIC)_hal_timebase_tim.c \ +stm/$(SERIES_GENERIC)/$(SERIES_GENERIC)_hal_timebase_tim.c + +APP_ROLE_SOURCES = \ +$(PROJECT_C_SOURCES) \ $(wildcard $(FREERTOS_PATH)/*.c) \ $(FREERTOS_PATH)/portable/GCC/ARM_CM4F/port.c \ $(wildcard common/Src/*.c) \ @@ -117,6 +103,16 @@ $(wildcard driver/Src/*.c) \ $(wildcard $(FATFS_PATH)/Src/*.c) \ $(filter-out $(addprefix bsp/Src/,$(addsuffix .c,$(BSP_DISABLE))),$(wildcard bsp/Src/*.c)) +BOOTLOADER_ROLE_SOURCES = \ +$(wildcard bootloader/Src/*.c) \ +common/Src/stubs.c + +ifeq ($(FIRMWARE_ROLE),bootloader) +C_SOURCES = $(BOOTLOADER_ROLE_SOURCES) $(COMMON_STM_SOURCES) +else +C_SOURCES = $(APP_ROLE_SOURCES) $(COMMON_STM_SOURCES) +endif + # ASM sources ASM_SOURCES = \ @@ -171,6 +167,10 @@ USE_HAL_DRIVER \ $(SERIES_LINE_GENERIC_CAP) \ $(SERIES_GENERIC_CAP) +ifeq ($(FIRMWARE_ROLE),bootloader) +C_DEFS += FIRMWARE_ROLE_BOOTLOADER +endif + C_DEFS := $(addprefix -D,$(C_DEFS)) # AS includes @@ -190,6 +190,10 @@ driver/Inc \ bsp/Inc \ middleware +ifeq ($(FIRMWARE_ROLE),bootloader) +C_INCLUDES += bootloader/Inc +endif + C_INCLUDES := $(addprefix -I,$(C_INCLUDES)) # compile gcc flags @@ -208,7 +212,13 @@ CFLAGS += -MMD -MP -MF"$(@:%.o=%.d)" # LDFLAGS ####################################### # link script -LDSCRIPT = stm/$(SERIES_GENERIC)/$(SERIES_LINE)/$(SERIES_LINE_CAP)$(EXTRA_CAP)x_FLASH.ld +ORIG_LDSCRIPT = stm/$(SERIES_GENERIC)/$(SERIES_LINE)/$(SERIES_LINE_CAP)$(EXTRA_CAP)x_FLASH.ld +GENERATED_LDSCRIPT = $(BUILD_DIR)/$(TARGET)_$(FIRMWARE_ROLE).ld +LDSCRIPT = $(ORIG_LDSCRIPT) + +ifeq ($(FIRMWARE_ROLE),app) +LDSCRIPT = $(GENERATED_LDSCRIPT) +endif # libraries LIBS = @@ -274,7 +284,7 @@ else @echo "AS $< -> $@" endif -$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile +$(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile $(LDSCRIPT) @if ls $(BUILD_DIR)/*.elf 1> /dev/null 2>&1; then \ rm -rf $(BUILD_DIR)/stm*.elf; \ fi @@ -320,6 +330,10 @@ endif $(BUILD_DIR): mkdir -p $@ +$(GENERATED_LDSCRIPT): $(ORIG_LDSCRIPT) Makefile | $(BUILD_DIR) + @python3 -c 'import pathlib; ld = pathlib.Path("$(ORIG_LDSCRIPT)").read_text(); size_kb = int("$(BOOTLOADER_SIZE_KB)"); flash_origin = 0x08000000 + size_kb * 1024; flash_length_kb = 512 - size_kb; ld = ld.replace("FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K", f"FLASH (rx) : ORIGIN = 0x{flash_origin:08x}, LENGTH = {flash_length_kb}K"); pathlib.Path("$(GENERATED_LDSCRIPT)").write_text(ld)' + @echo "LD_SCRIPT $(ORIG_LDSCRIPT) -> $(GENERATED_LDSCRIPT) ($(FIRMWARE_ROLE), FLASH=0x$$(printf '%x' $$((0x08000000 + $(BOOTLOADER_SIZE_KB)*1024))), LENGTH=$$((512-$(BOOTLOADER_SIZE_KB)))K)" + ####################################### # clean up ####################################### diff --git a/bootloader/Inc/bootloader_config.h b/bootloader/Inc/bootloader_config.h new file mode 100644 index 00000000..4f493ac6 --- /dev/null +++ b/bootloader/Inc/bootloader_config.h @@ -0,0 +1,46 @@ +#ifndef BOOTLOADER_CONFIG_H_ +#define BOOTLOADER_CONFIG_H_ + +#include "bootloader_hal.h" + +#ifndef BOOTLOADER_APP_BASE +#define BOOTLOADER_APP_BASE (0x08010000UL) +#endif + +#ifndef BOOTLOADER_APP_MAX_SIZE +#define BOOTLOADER_APP_MAX_SIZE (448UL * 1024UL) +#endif + +#ifndef BOOTLOADER_UART_BAUD +#define BOOTLOADER_UART_BAUD (115200U) +#endif + +#ifndef BOOTLOADER_UART_INSTANCE +#if defined(USART3) +#define BOOTLOADER_UART_INSTANCE USART3 +#elif defined(USART2) +#define BOOTLOADER_UART_INSTANCE USART2 +#elif defined(USART1) +#define BOOTLOADER_UART_INSTANCE USART1 +#else +#error "No supported UART instance available for bootloader." +#endif +#endif + +#ifndef BOOTLOADER_WRITE_CHUNK_MAX +#define BOOTLOADER_WRITE_CHUNK_MAX (128U) +#endif + +#ifndef BOOTLOADER_HANDSHAKE_TIMEOUT_MS +#define BOOTLOADER_HANDSHAKE_TIMEOUT_MS (0U) +#endif + +#ifndef BOOTLOADER_APP_STARTUP_WAIT_MS +#define BOOTLOADER_APP_STARTUP_WAIT_MS (3000U) +#endif + +#ifndef BOOTLOADER_POST_FLASH_BOOT_DELAY_MS +#define BOOTLOADER_POST_FLASH_BOOT_DELAY_MS (2000U) +#endif + +#endif diff --git a/bootloader/Inc/bootloader_hal.h b/bootloader/Inc/bootloader_hal.h new file mode 100644 index 00000000..9d929d9a --- /dev/null +++ b/bootloader/Inc/bootloader_hal.h @@ -0,0 +1,14 @@ +#ifndef BOOTLOADER_HAL_H_ +#define BOOTLOADER_HAL_H_ + +#if defined(STM32F4xx) +#include "stm32f4xx_hal.h" +#elif defined(STM32L4xx) +#include "stm32l4xx_hal.h" +#elif defined(STM32G4xx) +#include "stm32g4xx_hal.h" +#else +#error "Unsupported STM32 series for bootloader." +#endif + +#endif diff --git a/bootloader/Inc/bootloader_indicator.h b/bootloader/Inc/bootloader_indicator.h new file mode 100644 index 00000000..321f5252 --- /dev/null +++ b/bootloader/Inc/bootloader_indicator.h @@ -0,0 +1,18 @@ +#ifndef BOOTLOADER_INDICATOR_H_ +#define BOOTLOADER_INDICATOR_H_ + +#include + +typedef enum { + BOOTLOADER_INDICATOR_NO_APP = 0, + BOOTLOADER_INDICATOR_APP_PRESENT, + BOOTLOADER_INDICATOR_CONNECTED, + BOOTLOADER_INDICATOR_FLASHING, + BOOTLOADER_INDICATOR_ERROR, +} bootloader_indicator_mode_t; + +void bootloader_indicator_init(void); +void bootloader_indicator_set_mode(bootloader_indicator_mode_t mode); +void bootloader_indicator_update(uint32_t tick_ms); + +#endif diff --git a/bootloader/Inc/bootloader_runtime.h b/bootloader/Inc/bootloader_runtime.h new file mode 100644 index 00000000..7169985f --- /dev/null +++ b/bootloader/Inc/bootloader_runtime.h @@ -0,0 +1,18 @@ +#ifndef BOOTLOADER_RUNTIME_H_ +#define BOOTLOADER_RUNTIME_H_ + +#include +#include +#include + +void bootloader_runtime_init(void); +bool bootloader_runtime_wait_for_handshake(uint32_t timeout_ms); +bool bootloader_runtime_poll_sync(uint32_t timeout_ms); +bool bootloader_runtime_send_bytes(const uint8_t *data, uint16_t len); +bool bootloader_runtime_read_bytes(uint8_t *data, uint16_t len, uint32_t timeout_ms); +bool bootloader_runtime_erase_app(void); +bool bootloader_runtime_write_app(uint32_t app_offset, const uint8_t *data, size_t len); +bool bootloader_runtime_is_app_valid(void); +void bootloader_runtime_jump_to_app(void); + +#endif diff --git a/bootloader/Src/bootloader_indicator.c b/bootloader/Src/bootloader_indicator.c new file mode 100644 index 00000000..707e1817 --- /dev/null +++ b/bootloader/Src/bootloader_indicator.c @@ -0,0 +1,80 @@ +#include "bootloader_indicator.h" + +#include "bootloader_hal.h" + +#include + +#if defined(STM32L432xx) +#define LED_PIN GPIO_PIN_3 +#define LED_PORT GPIOB +#elif defined(STM32L431xx) +#define LED_PIN GPIO_PIN_11 +#define LED_PORT GPIOB +#elif defined(STM32G473xx) +#define LED_PIN GPIO_PIN_3 +#define LED_PORT GPIOC +#else +#define LED_PIN GPIO_PIN_5 +#define LED_PORT GPIOA +#endif + +static bootloader_indicator_mode_t s_mode = BOOTLOADER_INDICATOR_NO_APP; + +static void bootloader_indicator_enable_port_clock(void) { + if (LED_PORT == GPIOA) { + __HAL_RCC_GPIOA_CLK_ENABLE(); + } else if (LED_PORT == GPIOB) { + __HAL_RCC_GPIOB_CLK_ENABLE(); + } else if (LED_PORT == GPIOC) { + __HAL_RCC_GPIOC_CLK_ENABLE(); + } else if (LED_PORT == GPIOD) { + __HAL_RCC_GPIOD_CLK_ENABLE(); + } +} + +void bootloader_indicator_init(void) { + GPIO_InitTypeDef init = {0}; + bootloader_indicator_enable_port_clock(); + + init.Pin = LED_PIN; + init.Mode = GPIO_MODE_OUTPUT_PP; + init.Pull = GPIO_NOPULL; + init.Speed = GPIO_SPEED_FREQ_LOW; + HAL_GPIO_Init(LED_PORT, &init); + HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET); +} + +void bootloader_indicator_set_mode(bootloader_indicator_mode_t mode) { + s_mode = mode; + if (s_mode == BOOTLOADER_INDICATOR_CONNECTED) { + HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET); + } else { + HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET); + } +} + +void bootloader_indicator_update(uint32_t tick_ms) { + bool led_on = false; + + if (s_mode == BOOTLOADER_INDICATOR_CONNECTED) { + led_on = true; + } else if (s_mode == BOOTLOADER_INDICATOR_FLASHING) { + led_on = (tick_ms % 600U) < 300U; + } else if (s_mode == BOOTLOADER_INDICATOR_APP_PRESENT) { + uint32_t phase = tick_ms % 1000U; + led_on = (phase < 100U) || ((phase >= 200U) && (phase < 300U)); + } else if (s_mode == BOOTLOADER_INDICATOR_NO_APP) { + /* + * Software PWM fade. The update loop runs often while polling UART, so + * a 20 ms PWM frame is enough for a visible breathe without a timer. + */ + uint32_t breath = tick_ms % 2000U; + uint32_t level = (breath < 1000U) ? breath : (1999U - breath); + uint32_t pwm = tick_ms % 20U; + led_on = pwm < (level / 50U); + } else if (s_mode == BOOTLOADER_INDICATOR_ERROR) { + led_on = (tick_ms % 500U) < 250U; + } + + HAL_GPIO_WritePin(LED_PORT, LED_PIN, led_on ? GPIO_PIN_SET : GPIO_PIN_RESET); +} diff --git a/bootloader/Src/bootloader_main.c b/bootloader/Src/bootloader_main.c new file mode 100644 index 00000000..58bb4a66 --- /dev/null +++ b/bootloader/Src/bootloader_main.c @@ -0,0 +1,505 @@ +#include "bootloader_config.h" +#include "bootloader_indicator.h" +#include "bootloader_runtime.h" +#include "bootloader_hal.h" + +#include +#include +#include + +void SystemClock_Config(void); + +#define BL_SOF ((uint8_t)0x5AU) +#define BL_ACK ((uint8_t)0x79U) +#define BL_NACK ((uint8_t)0x1FU) + +/* AN3155 commands (subset needed for STM32CubeProgrammer UART flashing). */ +#define BL_CMD_GET ((uint8_t)0x00U) +#define BL_CMD_GET_VERSION ((uint8_t)0x01U) +#define BL_CMD_GET_ID ((uint8_t)0x02U) +#define BL_CMD_READ_MEMORY ((uint8_t)0x11U) +#define BL_CMD_GO ((uint8_t)0x21U) +#define BL_CMD_WRITE_MEMORY ((uint8_t)0x31U) +#define BL_CMD_ERASE ((uint8_t)0x43U) +#define BL_CMD_EXT_ERASE ((uint8_t)0x44U) + +#define BL_VERSION ((uint8_t)0x10U) +#define BL_DEVICE_ID ((uint16_t)0x0469U) + +#define BL_FLASH_BASE ((uint32_t)0x08000000UL) +#define BL_FLASH_PAGE_SIZE ((uint32_t)0x800UL) /* 2KB pages on STM32G473 */ +#define BL_OPTION_BYTES_BASE ((uint32_t)0x1FFF7800UL) +#define BL_OPTION_BYTES_SIZE ((uint32_t)0x80UL) + +static const uint8_t s_supported_cmds[] = { + BL_CMD_GET, + BL_CMD_GET_VERSION, + BL_CMD_GET_ID, + BL_CMD_READ_MEMORY, + BL_CMD_GO, + BL_CMD_WRITE_MEMORY, + BL_CMD_ERASE, + BL_CMD_EXT_ERASE, +}; + +static bool s_app_write_seen = false; +static uint32_t s_last_command_tick = 0U; + +static uint32_t bl_app_start(void) { + return BOOTLOADER_APP_BASE; +} + +static uint32_t bl_app_end_exclusive(void) { + return BOOTLOADER_APP_BASE + BOOTLOADER_APP_MAX_SIZE; +} + +static bool bl_addr_in_app(uint32_t addr) { + return addr >= bl_app_start() && addr < bl_app_end_exclusive(); +} + +static bool bl_range_in_app(uint32_t addr, uint32_t len) { + if (len == 0U) { + return false; + } + uint32_t end = addr + len; + if (end < addr) { + return false; + } + return addr >= bl_app_start() && end <= bl_app_end_exclusive(); +} + +static bool bl_range_in_option_bytes(uint32_t addr, uint32_t len) { + if (len == 0U) { + return false; + } + uint32_t end = addr + len; + if (end < addr) { + return false; + } + return addr >= BL_OPTION_BYTES_BASE && end <= (BL_OPTION_BYTES_BASE + BL_OPTION_BYTES_SIZE); +} + +static bool bl_xor_ok(const uint8_t *data, size_t len, uint8_t expected) { + uint8_t crc = 0U; + for (size_t i = 0; i < len; i++) { + crc ^= data[i]; + } + return crc == expected; +} + +static void bl_send_ack(void) { + uint8_t b = BL_ACK; + (void)bootloader_runtime_send_bytes(&b, 1U); +} + +static void bl_send_nack(void) { + uint8_t b = BL_NACK; + (void)bootloader_runtime_send_bytes(&b, 1U); +} + +static bool bl_read_cmd(uint8_t *cmd) { + uint8_t raw[2]; + if (!bootloader_runtime_read_bytes(raw, sizeof(raw), 1000U)) { + return false; + } + if ((uint8_t)(raw[0] ^ raw[1]) != 0xFFU) { + bl_send_nack(); + return false; + } + *cmd = raw[0]; + return true; +} + +static bool bl_read_addr(uint32_t *addr) { + uint8_t raw[5]; + if (!bootloader_runtime_read_bytes(raw, sizeof(raw), 1000U)) { + return false; + } + if (!bl_xor_ok(raw, 4U, raw[4])) { + bl_send_nack(); + return false; + } + *addr = ((uint32_t)raw[0] << 24) | ((uint32_t)raw[1] << 16) | + ((uint32_t)raw[2] << 8) | ((uint32_t)raw[3]); + return true; +} + +static bool bl_handle_get(void) { + bl_send_ack(); + uint8_t out[2 + sizeof(s_supported_cmds)]; + out[0] = (uint8_t)(sizeof(s_supported_cmds)); + out[1] = BL_VERSION; + for (size_t i = 0; i < sizeof(s_supported_cmds); i++) { + out[2 + i] = s_supported_cmds[i]; + } + if (!bootloader_runtime_send_bytes(out, sizeof(out))) { + return false; + } + bl_send_ack(); + return true; +} + +static bool bl_handle_get_version(void) { + bl_send_ack(); + uint8_t out[3] = {BL_VERSION, 0x00U, 0x00U}; + if (!bootloader_runtime_send_bytes(out, sizeof(out))) { + return false; + } + bl_send_ack(); + return true; +} + +static bool bl_handle_get_id(void) { + bl_send_ack(); + uint8_t out[3] = {0x01U, (uint8_t)(BL_DEVICE_ID >> 8), (uint8_t)BL_DEVICE_ID}; + if (!bootloader_runtime_send_bytes(out, sizeof(out))) { + return false; + } + bl_send_ack(); + return true; +} + +static bool bl_handle_read_memory(void) { + uint32_t addr = 0U; + bl_send_ack(); + if (!bl_read_addr(&addr)) { + return false; + } + bl_send_ack(); + + uint8_t count_pkt[2]; + if (!bootloader_runtime_read_bytes(count_pkt, sizeof(count_pkt), 1000U)) { + return false; + } + if ((uint8_t)(count_pkt[0] ^ count_pkt[1]) != 0xFFU) { + bl_send_nack(); + return false; + } + + uint32_t read_len = (uint32_t)count_pkt[0] + 1U; + uint32_t end = addr + read_len; + if (end < addr) { + bl_send_nack(); + return false; + } + + bl_send_ack(); + if (bl_range_in_app(addr, read_len)) { + return bootloader_runtime_send_bytes((const uint8_t *)addr, (uint16_t)read_len); + } + if (bl_range_in_option_bytes(addr, read_len)) { + return bootloader_runtime_send_bytes((const uint8_t *)addr, (uint16_t)read_len); + } + + /* + * Keep the session alive for other out-of-app probes while still denying + * access to arbitrary bootloader memory. + */ + uint8_t filler[256]; + for (uint32_t i = 0U; i < read_len; i++) { + filler[i] = 0xFFU; + } + return bootloader_runtime_send_bytes(filler, (uint16_t)read_len); +} + +static bool bl_handle_go(void) { + uint32_t addr = 0U; + bl_send_ack(); + if (!bl_read_addr(&addr)) { + return false; + } + + /* + * CubeProgrammer may issue GO with probe addresses; acknowledge the command + * to keep the session alive, and only actually jump when target is app base. + */ + bl_send_ack(); + if (addr == bl_app_start() && bootloader_runtime_is_app_valid()) { + HAL_Delay(20U); + bootloader_runtime_jump_to_app(); + } + return true; +} + +static bool bl_handle_write_memory(void) { + uint32_t addr = 0U; + bl_send_ack(); + if (!bl_read_addr(&addr)) { + return false; + } + if (!bl_addr_in_app(addr)) { + bl_send_nack(); + return false; + } + bl_send_ack(); + + uint8_t len_byte = 0U; + if (!bootloader_runtime_read_bytes(&len_byte, 1U, 1000U)) { + return false; + } + uint32_t data_len = (uint32_t)len_byte + 1U; + uint8_t data[256]; + if (!bootloader_runtime_read_bytes(data, (uint16_t)data_len, 1000U)) { + return false; + } + uint8_t checksum = 0U; + if (!bootloader_runtime_read_bytes(&checksum, 1U, 1000U)) { + return false; + } + + uint8_t crc = len_byte; + for (uint32_t i = 0; i < data_len; i++) { + crc ^= data[i]; + } + if (crc != checksum) { + bl_send_nack(); + return false; + } + if (!bl_range_in_app(addr, data_len)) { + bl_send_nack(); + return false; + } + + bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_FLASHING); + bootloader_indicator_update(HAL_GetTick()); + bool ok = bootloader_runtime_write_app(addr - bl_app_start(), data, data_len); + bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_CONNECTED); + if (!ok) { + bl_send_nack(); + return false; + } + s_app_write_seen = true; + bl_send_ack(); + return true; +} + +static bool bl_handle_erase(void) { + bl_send_ack(); + uint8_t n = 0U; + if (!bootloader_runtime_read_bytes(&n, 1U, 1000U)) { + return false; + } + + /* Only support global erase for app region. */ + if (n == 0xFFU) { + uint8_t chk = 0U; + if (!bootloader_runtime_read_bytes(&chk, 1U, 1000U)) { + return false; + } + if ((uint8_t)(n ^ chk) != 0x00U) { + bl_send_nack(); + return false; + } + bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_FLASHING); + bootloader_indicator_update(HAL_GetTick()); + bool ok = bootloader_runtime_erase_app(); + bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_CONNECTED); + if (!ok) { + bl_send_nack(); + return false; + } + bl_send_ack(); + return true; + } + + /* Read and validate page list then perform app erase. */ + uint16_t count = (uint16_t)n + 1U; + uint8_t page_buf[256]; + if (count > sizeof(page_buf)) { + bl_send_nack(); + return false; + } + if (!bootloader_runtime_read_bytes(page_buf, count, 1000U)) { + return false; + } + uint8_t chk = 0U; + if (!bootloader_runtime_read_bytes(&chk, 1U, 1000U)) { + return false; + } + uint8_t crc = n; + for (uint16_t i = 0; i < count; i++) { + crc ^= page_buf[i]; + } + if (crc != chk) { + bl_send_nack(); + return false; + } + + uint32_t app_first_page = (bl_app_start() - BL_FLASH_BASE) / BL_FLASH_PAGE_SIZE; + uint32_t app_last_page = (bl_app_end_exclusive() - 1U - BL_FLASH_BASE) / BL_FLASH_PAGE_SIZE; + for (uint16_t i = 0; i < count; i++) { + uint32_t p = page_buf[i]; + if (p < app_first_page || p > app_last_page) { + bl_send_nack(); + return false; + } + } + + bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_FLASHING); + bootloader_indicator_update(HAL_GetTick()); + bool ok = bootloader_runtime_erase_app(); + bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_CONNECTED); + if (!ok) { + bl_send_nack(); + return false; + } + bl_send_ack(); + return true; +} + +static bool bl_handle_ext_erase(void) { + bl_send_ack(); + uint8_t hdr[2]; + if (!bootloader_runtime_read_bytes(hdr, sizeof(hdr), 1000U)) { + return false; + } + uint16_t n = ((uint16_t)hdr[0] << 8) | hdr[1]; + + if ((n == 0xFFFFU) || (n == 0xFFFEU) || (n == 0xFFFDU)) { + uint8_t chk = 0U; + if (!bootloader_runtime_read_bytes(&chk, 1U, 1000U)) { + return false; + } + uint8_t crc = hdr[0] ^ hdr[1]; + if (crc != chk) { + bl_send_nack(); + return false; + } + /* + * AN3155 defines 0xFFFF global erase, 0xFFFE bank 1 erase, and + * 0xFFFD bank 2 erase. This bootloader constrains all special erase + * requests to the app region so the resident bootloader survives. + */ + bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_FLASHING); + bootloader_indicator_update(HAL_GetTick()); + bool ok = bootloader_runtime_erase_app(); + bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_CONNECTED); + if (!ok) { + bl_send_nack(); + return false; + } + bl_send_ack(); + return true; + } + + uint32_t count = (uint32_t)n + 1U; + uint32_t bytes = count * 2U; + if (bytes > 512U) { + bl_send_nack(); + return false; + } + + uint8_t pages[512]; + if (!bootloader_runtime_read_bytes(pages, (uint16_t)bytes, 1000U)) { + return false; + } + uint8_t chk = 0U; + if (!bootloader_runtime_read_bytes(&chk, 1U, 1000U)) { + return false; + } + uint8_t crc = hdr[0] ^ hdr[1]; + for (uint32_t i = 0U; i < bytes; i++) { + crc ^= pages[i]; + } + if (crc != chk) { + bl_send_nack(); + return false; + } + + uint32_t app_first_page = (bl_app_start() - BL_FLASH_BASE) / BL_FLASH_PAGE_SIZE; + uint32_t app_last_page = (bl_app_end_exclusive() - 1U - BL_FLASH_BASE) / BL_FLASH_PAGE_SIZE; + for (uint32_t i = 0U; i < count; i++) { + uint16_t p = ((uint16_t)pages[2U * i] << 8) | pages[(2U * i) + 1U]; + if (p < app_first_page || p > app_last_page) { + bl_send_nack(); + return false; + } + } + + bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_FLASHING); + bootloader_indicator_update(HAL_GetTick()); + bool ok = bootloader_runtime_erase_app(); + bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_CONNECTED); + if (!ok) { + bl_send_nack(); + return false; + } + bl_send_ack(); + return true; +} + +static bool bl_dispatch_cmd(uint8_t cmd) { + if (cmd == BL_CMD_GET) { + return bl_handle_get(); + } + if (cmd == BL_CMD_GET_VERSION) { + return bl_handle_get_version(); + } + if (cmd == BL_CMD_GET_ID) { + return bl_handle_get_id(); + } + if (cmd == BL_CMD_READ_MEMORY) { + return bl_handle_read_memory(); + } + if (cmd == BL_CMD_GO) { + return bl_handle_go(); + } + if (cmd == BL_CMD_WRITE_MEMORY) { + return bl_handle_write_memory(); + } + if (cmd == BL_CMD_ERASE) { + return bl_handle_erase(); + } + if (cmd == BL_CMD_EXT_ERASE) { + return bl_handle_ext_erase(); + } + + bl_send_nack(); + return false; +} + +static bool bl_wait_for_sync_with_indicator(uint32_t timeout_ms) { + uint32_t start = HAL_GetTick(); + while ((timeout_ms == 0U) || ((HAL_GetTick() - start) < timeout_ms)) { + bootloader_indicator_update(HAL_GetTick()); + + if (bootloader_runtime_poll_sync(5U)) { + bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_CONNECTED); + bl_send_ack(); + return true; + } + } + return false; +} + +int main(void) { + HAL_Init(); + SystemClock_Config(); + + bootloader_indicator_init(); + bootloader_runtime_init(); + + bool app_valid = bootloader_runtime_is_app_valid(); + bootloader_indicator_set_mode(app_valid ? BOOTLOADER_INDICATOR_APP_PRESENT : BOOTLOADER_INDICATOR_NO_APP); + + uint32_t startup_wait_ms = app_valid ? BOOTLOADER_APP_STARTUP_WAIT_MS : BOOTLOADER_HANDSHAKE_TIMEOUT_MS; + if (!bl_wait_for_sync_with_indicator(startup_wait_ms)) { + if (app_valid) { + bootloader_runtime_jump_to_app(); + } + } + + while (1) { + bootloader_indicator_update(HAL_GetTick()); + uint8_t cmd = 0U; + if (!bl_read_cmd(&cmd)) { + if (s_app_write_seen && bootloader_runtime_is_app_valid() && + ((HAL_GetTick() - s_last_command_tick) >= BOOTLOADER_POST_FLASH_BOOT_DELAY_MS)) { + bootloader_runtime_jump_to_app(); + } + continue; + } + s_last_command_tick = HAL_GetTick(); + (void)bl_dispatch_cmd(cmd); + } +} diff --git a/bootloader/Src/bootloader_runtime.c b/bootloader/Src/bootloader_runtime.c new file mode 100644 index 00000000..62341ea5 --- /dev/null +++ b/bootloader/Src/bootloader_runtime.c @@ -0,0 +1,244 @@ +#include "bootloader_runtime.h" + +#include "bootloader_config.h" +#include "bootloader_hal.h" + +static UART_HandleTypeDef s_huart = { + .Instance = BOOTLOADER_UART_INSTANCE, +}; + +static void bootloader_copy_bytes(uint8_t *dst, const uint8_t *src, size_t len) { + for (size_t i = 0; i < len; i++) { + dst[i] = src[i]; + } +} + +static void bootloader_uart_gpio_init(UART_HandleTypeDef *huart) { + GPIO_InitTypeDef init = {0}; + + init.Mode = GPIO_MODE_AF_PP; + init.Pull = GPIO_NOPULL; + init.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + +#if defined(USART1) + if (huart->Instance == USART1) { + __HAL_RCC_USART1_CLK_ENABLE(); + __HAL_RCC_GPIOA_CLK_ENABLE(); + init.Pin = GPIO_PIN_9 | GPIO_PIN_10; + init.Alternate = GPIO_AF7_USART1; + HAL_GPIO_Init(GPIOA, &init); + return; + } +#endif + +#if defined(USART2) + if (huart->Instance == USART2) { + __HAL_RCC_USART2_CLK_ENABLE(); +#if defined(GPIOD) + __HAL_RCC_GPIOD_CLK_ENABLE(); + init.Pin = GPIO_PIN_5 | GPIO_PIN_6; + init.Alternate = GPIO_AF7_USART2; + HAL_GPIO_Init(GPIOD, &init); +#else + __HAL_RCC_GPIOA_CLK_ENABLE(); + init.Pin = GPIO_PIN_2 | GPIO_PIN_3; + init.Alternate = GPIO_AF7_USART2; + HAL_GPIO_Init(GPIOA, &init); +#endif + return; + } +#endif + +#if defined(USART3) + if (huart->Instance == USART3) { + __HAL_RCC_USART3_CLK_ENABLE(); + __HAL_RCC_GPIOC_CLK_ENABLE(); + init.Pin = GPIO_PIN_10 | GPIO_PIN_11; + init.Alternate = GPIO_AF7_USART3; + HAL_GPIO_Init(GPIOC, &init); + return; + } +#endif +} + +static void bootloader_uart_init(UART_HandleTypeDef *huart) { + bootloader_uart_gpio_init(huart); + + huart->Init.BaudRate = BOOTLOADER_UART_BAUD; + /* AN3155 UART uses 8E1; HAL needs 9-bit word length when parity is enabled. */ + huart->Init.WordLength = UART_WORDLENGTH_9B; + huart->Init.StopBits = UART_STOPBITS_1; + huart->Init.Parity = UART_PARITY_EVEN; + huart->Init.Mode = UART_MODE_TX_RX; + huart->Init.HwFlowCtl = UART_HWCONTROL_NONE; + huart->Init.OverSampling = UART_OVERSAMPLING_16; + huart->Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; + huart->AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; + + (void)HAL_UART_Init(huart); +} + +void bootloader_runtime_init(void) { + bootloader_uart_init(&s_huart); +} + +bool bootloader_runtime_wait_for_handshake(uint32_t timeout_ms) { + uint8_t byte = 0U; + if (HAL_UART_Receive(&s_huart, &byte, 1U, timeout_ms) != HAL_OK) { + return false; + } + return byte == 0x7FU; +} + +bool bootloader_runtime_poll_sync(uint32_t timeout_ms) { + uint8_t byte = 0U; + return HAL_UART_Receive(&s_huart, &byte, 1U, timeout_ms) == HAL_OK && byte == 0x7FU; +} + +bool bootloader_runtime_send_bytes(const uint8_t *data, uint16_t len) { + return HAL_UART_Transmit(&s_huart, (uint8_t *)data, len, 1000U) == HAL_OK; +} + +bool bootloader_runtime_read_bytes(uint8_t *data, uint16_t len, uint32_t timeout_ms) { + return HAL_UART_Receive(&s_huart, data, len, timeout_ms) == HAL_OK; +} + +static uint32_t bootloader_app_end_addr(void) { + return BOOTLOADER_APP_BASE + BOOTLOADER_APP_MAX_SIZE; +} + +static bool bootloader_flash_is_dual_bank(void) { +#if defined(FLASH_OPTR_DBANK) + return READ_BIT(FLASH->OPTR, FLASH_OPTR_DBANK) != 0U; +#else + return false; +#endif +} + +static uint32_t bootloader_addr_to_bank(uint32_t addr) { +#if defined(FLASH_OPTR_DBANK) + if (bootloader_flash_is_dual_bank() && ((addr - FLASH_BASE) >= FLASH_BANK_SIZE)) { + return FLASH_BANK_2; + } +#else + (void)addr; +#endif + return FLASH_BANK_1; +} + +static uint32_t bootloader_addr_to_page(uint32_t addr) { +#if defined(FLASH_OPTR_DBANK) + if (bootloader_flash_is_dual_bank()) { + return ((addr - FLASH_BASE) % FLASH_BANK_SIZE) / FLASH_PAGE_SIZE; + } +#endif + return (addr - FLASH_BASE) / FLASH_PAGE_SIZE; +} + +bool bootloader_runtime_erase_app(void) { + uint32_t page_error = 0U; + uint32_t erase_addr = BOOTLOADER_APP_BASE; + uint32_t erase_end = bootloader_app_end_addr(); + + HAL_FLASH_Unlock(); + + bool ok = true; + while (erase_addr < erase_end) { + uint32_t bank = bootloader_addr_to_bank(erase_addr); + uint32_t segment_end = erase_end; + +#if defined(FLASH_OPTR_DBANK) + if (bootloader_flash_is_dual_bank() && (bank == FLASH_BANK_1)) { + uint32_t bank1_end = FLASH_BASE + FLASH_BANK_SIZE; + if (segment_end > bank1_end) { + segment_end = bank1_end; + } + } +#endif + + FLASH_EraseInitTypeDef erase = {0}; + uint32_t first_page = bootloader_addr_to_page(erase_addr); + uint32_t last_page = bootloader_addr_to_page(segment_end - 1U); + + erase.TypeErase = FLASH_TYPEERASE_PAGES; + erase.Page = first_page; + erase.NbPages = (last_page - first_page) + 1U; + erase.Banks = bank; + + if (HAL_FLASHEx_Erase(&erase, &page_error) != HAL_OK) { + ok = false; + break; + } + + erase_addr = segment_end; + } + + HAL_FLASH_Lock(); + return ok; +} + +bool bootloader_runtime_write_app(uint32_t app_offset, const uint8_t *data, size_t len) { + if ((app_offset + len) > BOOTLOADER_APP_MAX_SIZE) { + return false; + } + + uint32_t write_addr = BOOTLOADER_APP_BASE + app_offset; + HAL_FLASH_Unlock(); + + for (size_t i = 0; i < len; i += 8U) { + uint64_t dword = 0xFFFFFFFFFFFFFFFFULL; + size_t chunk = ((len - i) >= 8U) ? 8U : (len - i); + bootloader_copy_bytes((uint8_t *)&dword, &data[i], chunk); + if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, write_addr + i, dword) != HAL_OK) { + HAL_FLASH_Lock(); + return false; + } + } + + HAL_FLASH_Lock(); + return true; +} + +bool bootloader_runtime_is_app_valid(void) { + uint32_t stack = *(volatile uint32_t *)BOOTLOADER_APP_BASE; + uint32_t reset = *(volatile uint32_t *)(BOOTLOADER_APP_BASE + 4U); + + bool stack_valid = (stack & 0x2FF00000UL) == 0x20000000UL; + bool reset_valid = (reset >= BOOTLOADER_APP_BASE) && (reset < bootloader_app_end_addr()); + return stack_valid && reset_valid; +} + +static void bootloader_runtime_clear_interrupt_state(void) { + SysTick->CTRL = 0U; + SysTick->LOAD = 0U; + SysTick->VAL = 0U; + SCB->ICSR = SCB_ICSR_PENDSTCLR_Msk | SCB_ICSR_PENDSVCLR_Msk; + + for (uint32_t i = 0U; i < (sizeof(NVIC->ICER) / sizeof(NVIC->ICER[0])); i++) { + NVIC->ICER[i] = 0xFFFFFFFFUL; + NVIC->ICPR[i] = 0xFFFFFFFFUL; + } +} + +void bootloader_runtime_jump_to_app(void) { + if (!bootloader_runtime_is_app_valid()) { + return; + } + + uint32_t app_stack = *(volatile uint32_t *)BOOTLOADER_APP_BASE; + uint32_t app_reset = *(volatile uint32_t *)(BOOTLOADER_APP_BASE + 4U); + void (*app_reset_handler)(void) = (void (*)(void))app_reset; + + __disable_irq(); + HAL_DeInit(); + bootloader_runtime_clear_interrupt_state(); + + SCB->VTOR = BOOTLOADER_APP_BASE; + __set_CONTROL(0U); + __set_PSP(0U); + __set_MSP(app_stack); + __DSB(); + __ISB(); + __enable_irq(); + app_reset_handler(); +} diff --git a/common/Src/stubs.c b/common/Src/stubs.c index 7ac3740c..c255848f 100644 --- a/common/Src/stubs.c +++ b/common/Src/stubs.c @@ -1,11 +1,60 @@ #include +#if defined(FIRMWARE_ROLE_BOOTLOADER) +#if defined(STM32F4xx) +#include "stm32f4xx_hal.h" +#elif defined(STM32L4xx) +#include "stm32l4xx_hal.h" +#elif defined(STM32G4xx) +#include "stm32g4xx_hal.h" +#endif +#endif + // This is load bearing void _init(){} #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-variable" void HardFault_Handler(){ +#if defined(FIRMWARE_ROLE_BOOTLOADER) +#if defined(STM32L432xx) + #define HF_LED_PIN GPIO_PIN_3 + #define HF_LED_PORT GPIOB +#elif defined(STM32L431xx) + #define HF_LED_PIN GPIO_PIN_11 + #define HF_LED_PORT GPIOB +#elif defined(STM32G473xx) + #define HF_LED_PIN GPIO_PIN_3 + #define HF_LED_PORT GPIOC +#else + #define HF_LED_PIN GPIO_PIN_5 + #define HF_LED_PORT GPIOA +#endif + + if (HF_LED_PORT == GPIOA) { + __HAL_RCC_GPIOA_CLK_ENABLE(); + } else if (HF_LED_PORT == GPIOB) { + __HAL_RCC_GPIOB_CLK_ENABLE(); + } else if (HF_LED_PORT == GPIOC) { + __HAL_RCC_GPIOC_CLK_ENABLE(); + } else if (HF_LED_PORT == GPIOD) { + __HAL_RCC_GPIOD_CLK_ENABLE(); + } + + GPIO_InitTypeDef init = {0}; + init.Pin = HF_LED_PIN; + init.Mode = GPIO_MODE_OUTPUT_PP; + init.Pull = GPIO_NOPULL; + init.Speed = GPIO_SPEED_FREQ_LOW; + HAL_GPIO_Init(HF_LED_PORT, &init); + + while (1) { + HAL_GPIO_TogglePin(HF_LED_PORT, HF_LED_PIN); + for (volatile uint32_t i = 0U; i < 600000U; i++) { + __NOP(); + } + } +#else // Configurable Fault Status Register // Consists of MMSR, BFSR and UFSR volatile uint32_t _CFSR = (*((volatile unsigned long *)(0xE000ED28))) ; @@ -27,6 +76,7 @@ void HardFault_Handler(){ volatile uint32_t _BFAR = (*((volatile unsigned long *)(0xE000ED38))) ; __asm("BKPT #0\n") ; // Break into the debugger +#endif } #pragma GCC diagnostic pop diff --git a/docs/UartBootloader.md b/docs/UartBootloader.md new file mode 100644 index 00000000..59fce3bf --- /dev/null +++ b/docs/UartBootloader.md @@ -0,0 +1,86 @@ +# UART Bootloader + +This bootloader is designed for the shared Embedded Sharepoint repo so each firmware project can reuse the same UART flashing flow. + +## Behavior + +- Runs from flash start (`0x08000000` by default). +- Listens on the configured bootloader UART at `115200 8E1`. +- LED behavior in bootloader: + - Waiting for host connection: fade in/out. + - Flashing: rapid blink. +- If no handshake arrives before timeout and a valid app exists, it jumps to the app. +- Speaks the STM32 AN3155 UART bootloader protocol subset used by STM32CubeProgrammer. + +## Memory Map + +- Bootloader region: `0x08000000` .. `0x0800FFFF` (default 64 KB). +- App region: `0x08010000` .. end of flash. + +Override by build variables: + +- `BOOTLOADER_SIZE_KB` +- `BOOTLOADER_APP_BASE` (compile-time macro) +- `BOOTLOADER_APP_MAX_SIZE` (compile-time macro) + +## Build + +Bootloader image: + +```bash +make -C test clean TEST=main PROJECT_TARGET=stm32g473xx FIRMWARE_ROLE=bootloader BEAR_ENABLE=0 +make -C test TEST=main PROJECT_TARGET=stm32g473xx FIRMWARE_ROLE=bootloader BEAR_ENABLE=0 +``` + +App image (linked after bootloader): + +```bash +make -C test clean TEST=blinky_bootloader PROJECT_TARGET=stm32g473xx FIRMWARE_ROLE=app BOOTLOADER_SIZE_KB=64 BEAR_ENABLE=0 +make -C test TEST=blinky_bootloader PROJECT_TARGET=stm32g473xx FIRMWARE_ROLE=app BOOTLOADER_SIZE_KB=64 BEAR_ENABLE=0 +``` + +## Flash Flow + +### 1) Initial bootloader flash + +Use STM32 ROM boot mode (BOOT pin/switch) and flash bootloader at `0x08000000`. + +You can use existing helper: + +```bash +./flash-uart.sh build/stm32g473xx.bin 0x08000000 +``` + +### 2) Subsequent app updates through bootloader + +With board running bootloader (or reset into bootloader), run: + +```bash +python3 scripts/uart_bootloader_flash.py --port /dev/tty.usbserial-310 --bin build/app/stm32g473xx.bin --boot +``` + +## Wire Protocol (host <-> bootloader) + +- Handshake: + - Host sends `0x7F` + - Bootloader replies `ACK(0x79)` +- UART format: + - `115200 8E1` +- Command packets follow AN3155: + - `CMD`, then bitwise-complement command byte. + - Multi-byte addresses are sent MSB first. + - Checksums are XOR bytes, matching AN3155. + +Commands: + +- `0x00`: Get +- `0x01`: Get Version +- `0x02`: Get ID +- `0x11`: Read Memory +- `0x21`: Go +- `0x31`: Write Memory +- `0x43`: Erase +- `0x44`: Extended Erase + +The implementation constrains erase and write operations to the app region so the resident +bootloader is not erased by CubeProgrammer special erase requests. diff --git a/scripts/uart_bootloader_flash.py b/scripts/uart_bootloader_flash.py new file mode 100755 index 00000000..e8d0489e --- /dev/null +++ b/scripts/uart_bootloader_flash.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import argparse +import os +import sys +import subprocess + + +APP_BASE = "0x08010000" + + +def default_stm32prog() -> str: + if sys.platform == "darwin": + return ( + "/Applications/STMicroelectronics/STM32Cube/STM32CubeProgrammer/" + "STM32CubeProgrammer.app/Contents/Resources/bin/STM32_Programmer_CLI" + ) + return os.path.expanduser("~/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin/STM32_Programmer_CLI") + + +def run_stm32prog(stm32prog: str, args: list[str]) -> None: + cmd = [stm32prog] + args + print("+ " + " ".join(cmd)) + subprocess.run(cmd, check=True) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Flash app via ES_BLT AN3155-compatible UART bootloader") + parser.add_argument("--port", required=True, help="Serial device (ex: /dev/tty.usbserial-310)") + parser.add_argument("--baud", default=115200, type=int, help="UART baud rate") + parser.add_argument("--bin", required=True, help="App binary to flash") + parser.add_argument("--address", default=APP_BASE, help=f"Flash address (default: {APP_BASE})") + parser.add_argument("--boot", action="store_true", help="Jump to app after flashing") + parser.add_argument("--stm32prog", default=default_stm32prog(), help="Path to STM32_Programmer_CLI") + args = parser.parse_args() + + if not os.path.isfile(args.bin): + print(f"Binary does not exist: {args.bin}") + return 1 + if not os.path.isfile(args.stm32prog): + print(f"STM32_Programmer_CLI does not exist: {args.stm32prog}") + return 1 + if not os.access(args.stm32prog, os.X_OK): + print(f"STM32_Programmer_CLI is not executable: {args.stm32prog}") + return 1 + + conn = [f"port={args.port}", f"br={args.baud}"] + try: + run_stm32prog(args.stm32prog, ["-c", *conn, "-w", args.bin, args.address, "-v"]) + if args.boot: + run_stm32prog(args.stm32prog, ["-c", *conn, "-g", args.address]) + except subprocess.CalledProcessError as exc: + return exc.returncode + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/Makefile b/test/Makefile index 07ac9fa3..6a419332 100644 --- a/test/Makefile +++ b/test/Makefile @@ -12,7 +12,13 @@ _PROJECT_C_SOURCES = $(wildcard Src/*.c) _PROJECT_C_INCLUDES = Inc # build and driver directories +ifeq ($(FIRMWARE_ROLE),bootloader) +_PROJECT_BUILD_DIR ?= ../build/bootloader +else ifeq ($(FIRMWARE_ROLE),app) +_PROJECT_BUILD_DIR ?= ../build/app +else _PROJECT_BUILD_DIR ?= ../build +endif BUILD_MAKEFILE_DIR = ../ # ensure all paths are absolute diff --git a/test/tests/blinky_bootloader_test.c b/test/tests/blinky_bootloader_test.c new file mode 100644 index 00000000..06501d69 --- /dev/null +++ b/test/tests/blinky_bootloader_test.c @@ -0,0 +1,57 @@ +#include "stm32xx_hal.h" + +#if defined(STM32L432xx) +#define LED_PIN GPIO_PIN_3 +#define LED_PORT GPIOB +#elif defined(STM32L431xx) +#define LED_PIN GPIO_PIN_11 +#define LED_PORT GPIOB +#elif defined(STM32G473xx) +#define LED_PIN GPIO_PIN_3 +#define LED_PORT GPIOC +#else +#define LED_PIN GPIO_PIN_5 +#define LED_PORT GPIOA +#endif + +#ifndef APP_VECTOR_TABLE_BASE +#define APP_VECTOR_TABLE_BASE (0x08010000UL) +#endif + +static void heartbeat_clock_init(void) { + if (LED_PORT == GPIOA) { + __HAL_RCC_GPIOA_CLK_ENABLE(); + } else if (LED_PORT == GPIOB) { + __HAL_RCC_GPIOB_CLK_ENABLE(); + } else if (LED_PORT == GPIOC) { + __HAL_RCC_GPIOC_CLK_ENABLE(); + } else if (LED_PORT == GPIOD) { + __HAL_RCC_GPIOD_CLK_ENABLE(); + } +} + +static void vectortable_init(void) { + /* App is linked after bootloader, so move VTOR accordingly. */ + SCB->VTOR = APP_VECTOR_TABLE_BASE; + __DSB(); + __ISB(); +} + +int main(void) { + vectortable_init(); + HAL_Init(); + SystemClock_Config(); + + GPIO_InitTypeDef led_config = {0}; + led_config.Mode = GPIO_MODE_OUTPUT_PP; + led_config.Pull = GPIO_NOPULL; + led_config.Pin = LED_PIN; + + heartbeat_clock_init(); + HAL_GPIO_Init(LED_PORT, &led_config); + + while (1) { + HAL_GPIO_TogglePin(LED_PORT, LED_PIN); + HAL_Delay(1000); + } +} From 24149acdd5e6e82a632966363298761ceba53774 Mon Sep 17 00:00:00 2001 From: Parthiv Shah Date: Sat, 25 Apr 2026 20:12:15 -0500 Subject: [PATCH 02/12] uart command to go back to boot works --- Makefile | 1 + bootloader/Src/bootloader_main.c | 5 +- bsp/Inc/UART.h | 12 +++++ bsp/Src/UART.c | 14 +++++ common/Inc/uart_bootloader.h | 15 ++++++ common/Src/uart_bootloader.c | 78 +++++++++++++++++++++++++++ scripts/uart_bootloader_enter.py | 42 +++++++++++++++ test/tests/blinky_bootloader_test.c | 83 ++++++++++++++++++++++++++--- 8 files changed, 243 insertions(+), 7 deletions(-) create mode 100644 common/Inc/uart_bootloader.h create mode 100644 common/Src/uart_bootloader.c create mode 100755 scripts/uart_bootloader_enter.py diff --git a/Makefile b/Makefile index 0641be01..06e75264 100644 --- a/Makefile +++ b/Makefile @@ -105,6 +105,7 @@ $(filter-out $(addprefix bsp/Src/,$(addsuffix .c,$(BSP_DISABLE))),$(wildcard bsp BOOTLOADER_ROLE_SOURCES = \ $(wildcard bootloader/Src/*.c) \ +common/Src/uart_bootloader.c \ common/Src/stubs.c ifeq ($(FIRMWARE_ROLE),bootloader) diff --git a/bootloader/Src/bootloader_main.c b/bootloader/Src/bootloader_main.c index 58bb4a66..e431cc2d 100644 --- a/bootloader/Src/bootloader_main.c +++ b/bootloader/Src/bootloader_main.c @@ -2,6 +2,7 @@ #include "bootloader_indicator.h" #include "bootloader_runtime.h" #include "bootloader_hal.h" +#include "uart_bootloader.h" #include #include @@ -479,10 +480,12 @@ int main(void) { bootloader_indicator_init(); bootloader_runtime_init(); + bool forced_bootloader = uart_bootloader_consume_request(); bool app_valid = bootloader_runtime_is_app_valid(); bootloader_indicator_set_mode(app_valid ? BOOTLOADER_INDICATOR_APP_PRESENT : BOOTLOADER_INDICATOR_NO_APP); - uint32_t startup_wait_ms = app_valid ? BOOTLOADER_APP_STARTUP_WAIT_MS : BOOTLOADER_HANDSHAKE_TIMEOUT_MS; + uint32_t startup_wait_ms = (app_valid && !forced_bootloader) ? + BOOTLOADER_APP_STARTUP_WAIT_MS : BOOTLOADER_HANDSHAKE_TIMEOUT_MS; if (!bl_wait_for_sync_with_indicator(startup_wait_ms)) { if (app_valid) { bootloader_runtime_jump_to_app(); diff --git a/bsp/Inc/UART.h b/bsp/Inc/UART.h index d615dd11..a6d4395e 100644 --- a/bsp/Inc/UART.h +++ b/bsp/Inc/UART.h @@ -118,4 +118,16 @@ uart_status_t uart_send(UART_HandleTypeDef* handle, const uint8_t* data, uint16_ */ uart_status_t uart_recv(UART_HandleTypeDef* handle, uint8_t* data, uint16_t length, TickType_t delay_ticks); +/** + * @brief Services the UART bootloader command parser. + * + * Reads one byte from the UART RX queue and resets into the bootloader if the + * configured bootloader magic command is received. + * + * @param handle Pointer to the UART handle. + * @param delay_ticks Ticks to wait for one byte. + * @return uart_status_t UART_OK when a byte was serviced, UART_ERR otherwise. + */ +uart_status_t uart_bootloader_service(UART_HandleTypeDef* handle, TickType_t delay_ticks); + #endif /* UART_H_ */ diff --git a/bsp/Src/UART.c b/bsp/Src/UART.c index 28fff726..97f1f32e 100644 --- a/bsp/Src/UART.c +++ b/bsp/Src/UART.c @@ -2,6 +2,7 @@ #include #include #include "stm32xx_hal.h" +#include "uart_bootloader.h" // Define the size of the data to be transmitted // may need to be configured for support for packets less more than 8 bits @@ -648,6 +649,19 @@ uart_status_t uart_recv(UART_HandleTypeDef* handle, uint8_t* data, uint16_t leng return status; } +uart_status_t uart_bootloader_service(UART_HandleTypeDef* handle, TickType_t delay_ticks) { + uint8_t byte = 0U; + if (uart_recv(handle, &byte, 1U, delay_ticks) != UART_OK) { + return UART_ERR; + } + + if (uart_bootloader_feed_command_byte(byte)) { + uart_bootloader_request_reset(); + } + + return UART_OK; +} + // MUST BE CALLED FROM ISR OR CRIT SECTION static void uart_transmit(UART_HandleTypeDef *huart){ BaseType_t higherPriorityTaskWoken = pdFALSE; diff --git a/common/Inc/uart_bootloader.h b/common/Inc/uart_bootloader.h new file mode 100644 index 00000000..0cc212d1 --- /dev/null +++ b/common/Inc/uart_bootloader.h @@ -0,0 +1,15 @@ +#ifndef UART_BOOTLOADER_H_ +#define UART_BOOTLOADER_H_ + +#include +#include + +#define UART_BOOTLOADER_COMMAND "ESBLT_BOOT\n" +#define UART_BOOTLOADER_MAGIC_WORD (0x4553424CUL) + +bool uart_bootloader_feed_command_byte(uint8_t byte); +void uart_bootloader_command_reset(void); +void uart_bootloader_request_reset(void); +bool uart_bootloader_consume_request(void); + +#endif /* UART_BOOTLOADER_H_ */ diff --git a/common/Src/uart_bootloader.c b/common/Src/uart_bootloader.c new file mode 100644 index 00000000..97e77cc6 --- /dev/null +++ b/common/Src/uart_bootloader.c @@ -0,0 +1,78 @@ +#include "uart_bootloader.h" + +#if defined(FIRMWARE_ROLE_BOOTLOADER) +#include "bootloader_hal.h" +#else +#include "stm32xx_hal.h" +#endif + +#include + +static size_t s_command_match_len = 0U; + +static volatile uint32_t *uart_bootloader_magic_register(void) { +#if defined(TAMP) && defined(TAMP_BKP0R) + return &TAMP->BKP0R; +#elif defined(RTC) && defined(RTC_BKP0R) + return &RTC->BKP0R; +#else + return NULL; +#endif +} + +static void uart_bootloader_enable_backup_access(void) { +#if defined(__HAL_RCC_PWR_CLK_ENABLE) + __HAL_RCC_PWR_CLK_ENABLE(); +#endif + HAL_PWR_EnableBkUpAccess(); +#if defined(__HAL_RCC_RTCAPB_CLK_ENABLE) + __HAL_RCC_RTCAPB_CLK_ENABLE(); +#elif defined(__HAL_RCC_RTC_CLK_ENABLE) + __HAL_RCC_RTC_CLK_ENABLE(); +#endif +} + +bool uart_bootloader_feed_command_byte(uint8_t byte) { + const char command[] = UART_BOOTLOADER_COMMAND; + const size_t command_len = sizeof(command) - 1U; + + if (byte == (uint8_t)command[s_command_match_len]) { + s_command_match_len++; + if (s_command_match_len == command_len) { + s_command_match_len = 0U; + return true; + } + return false; + } + + s_command_match_len = (byte == (uint8_t)command[0]) ? 1U : 0U; + return false; +} + +void uart_bootloader_command_reset(void) { + s_command_match_len = 0U; +} + +void uart_bootloader_request_reset(void) { + uart_bootloader_enable_backup_access(); + + volatile uint32_t *magic = uart_bootloader_magic_register(); + if (magic != NULL) { + *magic = UART_BOOTLOADER_MAGIC_WORD; + } + + __DSB(); + NVIC_SystemReset(); +} + +bool uart_bootloader_consume_request(void) { + uart_bootloader_enable_backup_access(); + + volatile uint32_t *magic = uart_bootloader_magic_register(); + if ((magic == NULL) || (*magic != UART_BOOTLOADER_MAGIC_WORD)) { + return false; + } + + *magic = 0U; + return true; +} diff --git a/scripts/uart_bootloader_enter.py b/scripts/uart_bootloader_enter.py new file mode 100755 index 00000000..63c391bc --- /dev/null +++ b/scripts/uart_bootloader_enter.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import argparse +import sys +import time + + +BOOTLOADER_PACKET = b"ESBLT_BOOT\n" + + +def send_packet(port: str, baud: int, timeout: float, delay: float) -> None: + try: + import serial + except ImportError as exc: + raise RuntimeError("pyserial is required. Run this from `nix develop` or install requirements.txt.") from exc + + with serial.Serial(port=port, baudrate=baud, timeout=timeout) as uart: + if delay > 0: + time.sleep(delay) + uart.write(BOOTLOADER_PACKET) + uart.flush() + + +def main() -> int: + parser = argparse.ArgumentParser(description="Send the ES_BLT UART magic packet to reset app into bootloader") + parser.add_argument("--port", required=True, help="Serial device (ex: /dev/cu.usbserial-310)") + parser.add_argument("--baud", default=115200, type=int, help="UART baud rate (default: 115200)") + parser.add_argument("--timeout", default=1.0, type=float, help="Serial timeout in seconds") + parser.add_argument("--delay", default=0.1, type=float, help="Delay after opening port before sending") + args = parser.parse_args() + + try: + send_packet(args.port, args.baud, args.timeout, args.delay) + except Exception as exc: + print(f"Failed to send bootloader packet: {exc}", file=sys.stderr) + return 1 + + print(f"Sent bootloader packet to {args.port} at {args.baud} baud") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/tests/blinky_bootloader_test.c b/test/tests/blinky_bootloader_test.c index 06501d69..39b26038 100644 --- a/test/tests/blinky_bootloader_test.c +++ b/test/tests/blinky_bootloader_test.c @@ -1,4 +1,5 @@ #include "stm32xx_hal.h" +#include "UART.h" #if defined(STM32L432xx) #define LED_PIN GPIO_PIN_3 @@ -14,10 +15,25 @@ #define LED_PORT GPIOA #endif +#if defined(USART3) +#define BOOT_COMMAND_UART husart3 +#elif defined(USART2) +#define BOOT_COMMAND_UART husart2 +#elif defined(USART1) +#define BOOT_COMMAND_UART husart1 +#else +#error "No UART available for bootloader command test." +#endif + #ifndef APP_VECTOR_TABLE_BASE #define APP_VECTOR_TABLE_BASE (0x08010000UL) #endif +static StaticTask_t s_blinky_task_buffer; +static StaticTask_t s_uart_task_buffer; +static StackType_t s_blinky_task_stack[configMINIMAL_STACK_SIZE]; +static StackType_t s_uart_task_stack[configMINIMAL_STACK_SIZE]; + static void heartbeat_clock_init(void) { if (LED_PORT == GPIOA) { __HAL_RCC_GPIOA_CLK_ENABLE(); @@ -37,11 +53,7 @@ static void vectortable_init(void) { __ISB(); } -int main(void) { - vectortable_init(); - HAL_Init(); - SystemClock_Config(); - +static void led_init(void) { GPIO_InitTypeDef led_config = {0}; led_config.Mode = GPIO_MODE_OUTPUT_PP; led_config.Pull = GPIO_NOPULL; @@ -49,9 +61,68 @@ int main(void) { heartbeat_clock_init(); HAL_GPIO_Init(LED_PORT, &led_config); +} + +static void boot_command_uart_init(void) { + BOOT_COMMAND_UART->Init.BaudRate = 115200; + BOOT_COMMAND_UART->Init.WordLength = UART_WORDLENGTH_8B; + BOOT_COMMAND_UART->Init.StopBits = UART_STOPBITS_1; + BOOT_COMMAND_UART->Init.Parity = UART_PARITY_NONE; + BOOT_COMMAND_UART->Init.Mode = UART_MODE_TX_RX; + BOOT_COMMAND_UART->Init.HwFlowCtl = UART_HWCONTROL_NONE; + BOOT_COMMAND_UART->Init.OverSampling = UART_OVERSAMPLING_16; +#if defined(STM32L4xx) || defined(STM32G4xx) + BOOT_COMMAND_UART->Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; + BOOT_COMMAND_UART->AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; +#endif + + if (uart_init(BOOT_COMMAND_UART) != UART_OK) { + Error_Handler(); + } +} + +static void blinky_task(void *argument) { + (void)argument; while (1) { HAL_GPIO_TogglePin(LED_PORT, LED_PIN); - HAL_Delay(1000); + vTaskDelay(pdMS_TO_TICKS(200)); } } + +static void uart_task(void *argument) { + (void)argument; + + while (1) { + (void)uart_bootloader_service(BOOT_COMMAND_UART, portMAX_DELAY); + } +} + +int main(void) { + vectortable_init(); + HAL_Init(); + SystemClock_Config(); + + led_init(); + boot_command_uart_init(); + + xTaskCreateStatic(blinky_task, + "BLINK", + configMINIMAL_STACK_SIZE, + NULL, + tskIDLE_PRIORITY + 1, + s_blinky_task_stack, + &s_blinky_task_buffer); + + xTaskCreateStatic(uart_task, + "BOOTUART", + configMINIMAL_STACK_SIZE, + NULL, + tskIDLE_PRIORITY + 2, + s_uart_task_stack, + &s_uart_task_buffer); + + vTaskStartScheduler(); + + while (1) {} +} From 8d78dfafb95c90ab3f2f359497603310d0a4c8fe Mon Sep 17 00:00:00 2001 From: Parthiv Shah Date: Sat, 25 Apr 2026 23:21:34 -0500 Subject: [PATCH 03/12] parent project support --- Makefile | 86 ++++++++-- bootloader/Inc/bootloader_board.h | 32 ++++ bootloader/Src/bootloader_indicator.c | 41 +---- bsp/Src/UART.c | 2 +- common/Inc/uart_bootloader.h | 2 + common/Src/hardfault.c | 32 ++++ common/Src/stubs.c | 53 +----- common/Src/uart_bootloader.c | 9 ++ docs/UartBootloader.md | 221 ++++++++++++++++++++------ scripts/flash_bootloader.py | 74 +++++++++ scripts/uart_bootloader_enter.py | 25 ++- scripts/uart_bootloader_flash.py | 43 ++++- test/Makefile | 15 +- 13 files changed, 482 insertions(+), 153 deletions(-) create mode 100644 bootloader/Inc/bootloader_board.h create mode 100644 common/Src/hardfault.c create mode 100755 scripts/flash_bootloader.py diff --git a/Makefile b/Makefile index 06e75264..26c87781 100644 --- a/Makefile +++ b/Makefile @@ -30,8 +30,36 @@ CLANG_INPUTS := $(filter-out $(IGNORED_CLANG_INPUTS), $(CLANG_INPUTS)) MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) BEAR_ENABLE ?= 1 -FIRMWARE_ROLE ?= app -BOOTLOADER_SIZE_KB ?= 64 + +# Firmware type selects the default memory map and flashing behavior. +# firmware Standalone image at 0x08000000. This is the default. +# app App image linked after the resident bootloader. +# bootloader Resident bootloader image at 0x08000000. +ifeq ($(origin FIRMWARE_TYPE), undefined) +ifeq ($(origin FIRMWARE_ROLE), undefined) +FIRMWARE_TYPE := firmware +else +FIRMWARE_TYPE := $(FIRMWARE_ROLE) +endif +endif + +ifeq ($(filter $(FIRMWARE_TYPE),firmware app bootloader),) +$(error FIRMWARE_TYPE must be one of: firmware, app, bootloader) +endif + +# Keep legacy FIRMWARE_ROLE invocations working internally and for child makes. +FIRMWARE_ROLE := $(FIRMWARE_TYPE) + +ifeq ($(origin BOOTLOADER_SIZE_KB), undefined) +ifeq ($(FIRMWARE_TYPE),firmware) +BOOTLOADER_SIZE_KB := 0 +else +BOOTLOADER_SIZE_KB := 64 +endif +endif + +FLASH_BASE ?= 0x08000000 +BOOTLOADER_APP_BASE ?= $(shell python3 -c 'base=int("$(FLASH_BASE)", 0); size=int("$(BOOTLOADER_SIZE_KB)"); print("0x%08x" % (base + size * 1024))') ###################################### # target @@ -105,10 +133,11 @@ $(filter-out $(addprefix bsp/Src/,$(addsuffix .c,$(BSP_DISABLE))),$(wildcard bsp BOOTLOADER_ROLE_SOURCES = \ $(wildcard bootloader/Src/*.c) \ +common/Src/hardfault.c \ common/Src/uart_bootloader.c \ common/Src/stubs.c -ifeq ($(FIRMWARE_ROLE),bootloader) +ifeq ($(FIRMWARE_TYPE),bootloader) C_SOURCES = $(BOOTLOADER_ROLE_SOURCES) $(COMMON_STM_SOURCES) else C_SOURCES = $(APP_ROLE_SOURCES) $(COMMON_STM_SOURCES) @@ -168,10 +197,28 @@ USE_HAL_DRIVER \ $(SERIES_LINE_GENERIC_CAP) \ $(SERIES_GENERIC_CAP) -ifeq ($(FIRMWARE_ROLE),bootloader) +ifeq ($(FIRMWARE_TYPE),bootloader) C_DEFS += FIRMWARE_ROLE_BOOTLOADER endif +ifeq ($(FIRMWARE_TYPE),firmware) +C_DEFS += FIRMWARE_TYPE_FIRMWARE +else ifeq ($(FIRMWARE_TYPE),app) +C_DEFS += FIRMWARE_TYPE_APP +else ifeq ($(FIRMWARE_TYPE),bootloader) +C_DEFS += FIRMWARE_TYPE_BOOTLOADER +endif + +ifneq ($(filter $(FIRMWARE_TYPE),app bootloader),) +C_DEFS += BOOTLOADER_APP_BASE=$(BOOTLOADER_APP_BASE) +endif + +ifeq ($(FIRMWARE_TYPE),app) +ifneq ($(BOOTLOADER_SIZE_KB),0) +C_DEFS += FIRMWARE_USES_BOOTLOADER +endif +endif + C_DEFS := $(addprefix -D,$(C_DEFS)) # AS includes @@ -191,7 +238,7 @@ driver/Inc \ bsp/Inc \ middleware -ifeq ($(FIRMWARE_ROLE),bootloader) +ifneq ($(filter $(FIRMWARE_TYPE),bootloader),) C_INCLUDES += bootloader/Inc endif @@ -217,7 +264,7 @@ ORIG_LDSCRIPT = stm/$(SERIES_GENERIC)/$(SERIES_LINE)/$(SERIES_LINE_CAP)$(EXTRA_C GENERATED_LDSCRIPT = $(BUILD_DIR)/$(TARGET)_$(FIRMWARE_ROLE).ld LDSCRIPT = $(ORIG_LDSCRIPT) -ifeq ($(FIRMWARE_ROLE),app) +ifeq ($(FIRMWARE_TYPE),app) LDSCRIPT = $(GENERATED_LDSCRIPT) endif @@ -332,8 +379,8 @@ $(BUILD_DIR): mkdir -p $@ $(GENERATED_LDSCRIPT): $(ORIG_LDSCRIPT) Makefile | $(BUILD_DIR) - @python3 -c 'import pathlib; ld = pathlib.Path("$(ORIG_LDSCRIPT)").read_text(); size_kb = int("$(BOOTLOADER_SIZE_KB)"); flash_origin = 0x08000000 + size_kb * 1024; flash_length_kb = 512 - size_kb; ld = ld.replace("FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K", f"FLASH (rx) : ORIGIN = 0x{flash_origin:08x}, LENGTH = {flash_length_kb}K"); pathlib.Path("$(GENERATED_LDSCRIPT)").write_text(ld)' - @echo "LD_SCRIPT $(ORIG_LDSCRIPT) -> $(GENERATED_LDSCRIPT) ($(FIRMWARE_ROLE), FLASH=0x$$(printf '%x' $$((0x08000000 + $(BOOTLOADER_SIZE_KB)*1024))), LENGTH=$$((512-$(BOOTLOADER_SIZE_KB)))K)" + @python3 -c 'import pathlib; ld = pathlib.Path("$(ORIG_LDSCRIPT)").read_text(); size_kb = int("$(BOOTLOADER_SIZE_KB)"); flash_origin = int("$(FLASH_BASE)", 0) + size_kb * 1024; flash_length_kb = 512 - size_kb; ld = ld.replace("FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K", f"FLASH (rx) : ORIGIN = 0x{flash_origin:08x}, LENGTH = {flash_length_kb}K"); pathlib.Path("$(GENERATED_LDSCRIPT)").write_text(ld)' + @echo "LD_SCRIPT $(ORIG_LDSCRIPT) -> $(GENERATED_LDSCRIPT) ($(FIRMWARE_TYPE), FLASH=$(BOOTLOADER_APP_BASE), LENGTH=$$((512-$(BOOTLOADER_SIZE_KB)))K)" ####################################### # clean up @@ -345,8 +392,13 @@ clean: ####################################### # flash ####################################### -FLASH_ADDRESS ?= 0x8000000 -FLASH_FILE = $(shell find $(BUILD_DIR) -name 'stm*.bin' -exec basename {} \;) +ifeq ($(FIRMWARE_TYPE),app) +FLASH_ADDRESS ?= $(BOOTLOADER_APP_BASE) +else +FLASH_ADDRESS ?= $(FLASH_BASE) +endif + +FLASH_FILE = $(notdir $(firstword $(wildcard $(BUILD_DIR)/stm*.bin))) .PHONY: flash flash: @@ -355,7 +407,13 @@ flash: .PHONY: flash-uart flash-uart: +ifeq ($(FIRMWARE_TYPE),bootloader) + ./scripts/flash_bootloader.py --bin $(BUILD_DIR)/$(FLASH_FILE) --address $(FLASH_ADDRESS) +else ifeq ($(FIRMWARE_TYPE),app) + ./scripts/uart_bootloader_flash.py --bin $(BUILD_DIR)/$(FLASH_FILE) --address $(FLASH_ADDRESS) --boot +else ./flash-uart.sh $(BUILD_DIR)/$(FLASH_FILE) $(FLASH_ADDRESS) +endif ####################################### # format @@ -378,11 +436,17 @@ help: @echo "Available targets:" @echo " all - Build the project." @echo " clean - Remove build artifacts." - @echo " flash - Flash the target device." + @echo " flash - Flash with ST-Link at the default address for FIRMWARE_TYPE." + @echo " flash-uart - Flash over UART using the default workflow for FIRMWARE_TYPE." @echo " tidy - Run clang-tidy." @echo " tidy-fix - Run clang-tidy with fixes." @echo " format - Run clang-format." @echo " format-fix - Run clang-format and apply fixes." + @echo "" + @echo "Firmware types:" + @echo " FIRMWARE_TYPE=firmware Standalone image at 0x08000000 (default)." + @echo " FIRMWARE_TYPE=bootloader Bootloader image at 0x08000000." + @echo " FIRMWARE_TYPE=app App linked after bootloader." ####################################### diff --git a/bootloader/Inc/bootloader_board.h b/bootloader/Inc/bootloader_board.h new file mode 100644 index 00000000..8b75b186 --- /dev/null +++ b/bootloader/Inc/bootloader_board.h @@ -0,0 +1,32 @@ +#ifndef BOOTLOADER_BOARD_H_ +#define BOOTLOADER_BOARD_H_ + +#include "bootloader_hal.h" + +#if defined(STM32L432xx) +#define BOOTLOADER_LED_PIN GPIO_PIN_3 +#define BOOTLOADER_LED_PORT GPIOB +#elif defined(STM32L431xx) +#define BOOTLOADER_LED_PIN GPIO_PIN_11 +#define BOOTLOADER_LED_PORT GPIOB +#elif defined(STM32G473xx) +#define BOOTLOADER_LED_PIN GPIO_PIN_3 +#define BOOTLOADER_LED_PORT GPIOC +#else +#define BOOTLOADER_LED_PIN GPIO_PIN_5 +#define BOOTLOADER_LED_PORT GPIOA +#endif + +static inline void bootloader_board_enable_led_port_clock(void) { + if (BOOTLOADER_LED_PORT == GPIOA) { + __HAL_RCC_GPIOA_CLK_ENABLE(); + } else if (BOOTLOADER_LED_PORT == GPIOB) { + __HAL_RCC_GPIOB_CLK_ENABLE(); + } else if (BOOTLOADER_LED_PORT == GPIOC) { + __HAL_RCC_GPIOC_CLK_ENABLE(); + } else if (BOOTLOADER_LED_PORT == GPIOD) { + __HAL_RCC_GPIOD_CLK_ENABLE(); + } +} + +#endif /* BOOTLOADER_BOARD_H_ */ diff --git a/bootloader/Src/bootloader_indicator.c b/bootloader/Src/bootloader_indicator.c index 707e1817..e37be91d 100644 --- a/bootloader/Src/bootloader_indicator.c +++ b/bootloader/Src/bootloader_indicator.c @@ -1,55 +1,30 @@ #include "bootloader_indicator.h" +#include "bootloader_board.h" #include "bootloader_hal.h" #include -#if defined(STM32L432xx) -#define LED_PIN GPIO_PIN_3 -#define LED_PORT GPIOB -#elif defined(STM32L431xx) -#define LED_PIN GPIO_PIN_11 -#define LED_PORT GPIOB -#elif defined(STM32G473xx) -#define LED_PIN GPIO_PIN_3 -#define LED_PORT GPIOC -#else -#define LED_PIN GPIO_PIN_5 -#define LED_PORT GPIOA -#endif - static bootloader_indicator_mode_t s_mode = BOOTLOADER_INDICATOR_NO_APP; -static void bootloader_indicator_enable_port_clock(void) { - if (LED_PORT == GPIOA) { - __HAL_RCC_GPIOA_CLK_ENABLE(); - } else if (LED_PORT == GPIOB) { - __HAL_RCC_GPIOB_CLK_ENABLE(); - } else if (LED_PORT == GPIOC) { - __HAL_RCC_GPIOC_CLK_ENABLE(); - } else if (LED_PORT == GPIOD) { - __HAL_RCC_GPIOD_CLK_ENABLE(); - } -} - void bootloader_indicator_init(void) { GPIO_InitTypeDef init = {0}; - bootloader_indicator_enable_port_clock(); + bootloader_board_enable_led_port_clock(); - init.Pin = LED_PIN; + init.Pin = BOOTLOADER_LED_PIN; init.Mode = GPIO_MODE_OUTPUT_PP; init.Pull = GPIO_NOPULL; init.Speed = GPIO_SPEED_FREQ_LOW; - HAL_GPIO_Init(LED_PORT, &init); - HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET); + HAL_GPIO_Init(BOOTLOADER_LED_PORT, &init); + HAL_GPIO_WritePin(BOOTLOADER_LED_PORT, BOOTLOADER_LED_PIN, GPIO_PIN_RESET); } void bootloader_indicator_set_mode(bootloader_indicator_mode_t mode) { s_mode = mode; if (s_mode == BOOTLOADER_INDICATOR_CONNECTED) { - HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET); + HAL_GPIO_WritePin(BOOTLOADER_LED_PORT, BOOTLOADER_LED_PIN, GPIO_PIN_SET); } else { - HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET); + HAL_GPIO_WritePin(BOOTLOADER_LED_PORT, BOOTLOADER_LED_PIN, GPIO_PIN_RESET); } } @@ -76,5 +51,5 @@ void bootloader_indicator_update(uint32_t tick_ms) { led_on = (tick_ms % 500U) < 250U; } - HAL_GPIO_WritePin(LED_PORT, LED_PIN, led_on ? GPIO_PIN_SET : GPIO_PIN_RESET); + HAL_GPIO_WritePin(BOOTLOADER_LED_PORT, BOOTLOADER_LED_PIN, led_on ? GPIO_PIN_SET : GPIO_PIN_RESET); } diff --git a/bsp/Src/UART.c b/bsp/Src/UART.c index 97f1f32e..871cb4d0 100644 --- a/bsp/Src/UART.c +++ b/bsp/Src/UART.c @@ -655,7 +655,7 @@ uart_status_t uart_bootloader_service(UART_HandleTypeDef* handle, TickType_t del return UART_ERR; } - if (uart_bootloader_feed_command_byte(byte)) { + if (uart_bootloader_feed_command_byte(byte) && uart_bootloader_is_entry_allowed()) { uart_bootloader_request_reset(); } diff --git a/common/Inc/uart_bootloader.h b/common/Inc/uart_bootloader.h index 0cc212d1..1fff32d0 100644 --- a/common/Inc/uart_bootloader.h +++ b/common/Inc/uart_bootloader.h @@ -9,6 +9,8 @@ bool uart_bootloader_feed_command_byte(uint8_t byte); void uart_bootloader_command_reset(void); +void uart_bootloader_set_entry_allowed(bool allowed); +bool uart_bootloader_is_entry_allowed(void); void uart_bootloader_request_reset(void); bool uart_bootloader_consume_request(void); diff --git a/common/Src/hardfault.c b/common/Src/hardfault.c new file mode 100644 index 00000000..3169109d --- /dev/null +++ b/common/Src/hardfault.c @@ -0,0 +1,32 @@ +#include + +#if defined(FIRMWARE_ROLE_BOOTLOADER) +#include "bootloader_board.h" +#elif defined(FIRMWARE_USES_BOOTLOADER) +#include "uart_bootloader.h" +#endif + +#if defined(FIRMWARE_ROLE_BOOTLOADER) || defined(FIRMWARE_USES_BOOTLOADER) +void HardFault_Handler(void) { +#if defined(FIRMWARE_ROLE_BOOTLOADER) + GPIO_InitTypeDef init = {0}; + bootloader_board_enable_led_port_clock(); + + init.Pin = BOOTLOADER_LED_PIN; + init.Mode = GPIO_MODE_OUTPUT_PP; + init.Pull = GPIO_NOPULL; + init.Speed = GPIO_SPEED_FREQ_LOW; + HAL_GPIO_Init(BOOTLOADER_LED_PORT, &init); + + while (1) { + HAL_GPIO_TogglePin(BOOTLOADER_LED_PORT, BOOTLOADER_LED_PIN); + for (volatile uint32_t i = 0U; i < 600000U; i++) { + __NOP(); + } + } +#elif defined(FIRMWARE_USES_BOOTLOADER) + uart_bootloader_request_reset(); + while (1) {} +#endif +} +#endif diff --git a/common/Src/stubs.c b/common/Src/stubs.c index c255848f..98208a7c 100644 --- a/common/Src/stubs.c +++ b/common/Src/stubs.c @@ -1,60 +1,12 @@ #include -#if defined(FIRMWARE_ROLE_BOOTLOADER) -#if defined(STM32F4xx) -#include "stm32f4xx_hal.h" -#elif defined(STM32L4xx) -#include "stm32l4xx_hal.h" -#elif defined(STM32G4xx) -#include "stm32g4xx_hal.h" -#endif -#endif - // This is load bearing void _init(){} +#if !defined(FIRMWARE_ROLE_BOOTLOADER) && !defined(FIRMWARE_USES_BOOTLOADER) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-variable" void HardFault_Handler(){ -#if defined(FIRMWARE_ROLE_BOOTLOADER) -#if defined(STM32L432xx) - #define HF_LED_PIN GPIO_PIN_3 - #define HF_LED_PORT GPIOB -#elif defined(STM32L431xx) - #define HF_LED_PIN GPIO_PIN_11 - #define HF_LED_PORT GPIOB -#elif defined(STM32G473xx) - #define HF_LED_PIN GPIO_PIN_3 - #define HF_LED_PORT GPIOC -#else - #define HF_LED_PIN GPIO_PIN_5 - #define HF_LED_PORT GPIOA -#endif - - if (HF_LED_PORT == GPIOA) { - __HAL_RCC_GPIOA_CLK_ENABLE(); - } else if (HF_LED_PORT == GPIOB) { - __HAL_RCC_GPIOB_CLK_ENABLE(); - } else if (HF_LED_PORT == GPIOC) { - __HAL_RCC_GPIOC_CLK_ENABLE(); - } else if (HF_LED_PORT == GPIOD) { - __HAL_RCC_GPIOD_CLK_ENABLE(); - } - - GPIO_InitTypeDef init = {0}; - init.Pin = HF_LED_PIN; - init.Mode = GPIO_MODE_OUTPUT_PP; - init.Pull = GPIO_NOPULL; - init.Speed = GPIO_SPEED_FREQ_LOW; - HAL_GPIO_Init(HF_LED_PORT, &init); - - while (1) { - HAL_GPIO_TogglePin(HF_LED_PORT, HF_LED_PIN); - for (volatile uint32_t i = 0U; i < 600000U; i++) { - __NOP(); - } - } -#else // Configurable Fault Status Register // Consists of MMSR, BFSR and UFSR volatile uint32_t _CFSR = (*((volatile unsigned long *)(0xE000ED28))) ; @@ -76,7 +28,6 @@ void HardFault_Handler(){ volatile uint32_t _BFAR = (*((volatile unsigned long *)(0xE000ED38))) ; __asm("BKPT #0\n") ; // Break into the debugger -#endif } #pragma GCC diagnostic pop - +#endif diff --git a/common/Src/uart_bootloader.c b/common/Src/uart_bootloader.c index 97e77cc6..93a69c6e 100644 --- a/common/Src/uart_bootloader.c +++ b/common/Src/uart_bootloader.c @@ -9,6 +9,7 @@ #include static size_t s_command_match_len = 0U; +static volatile bool s_entry_allowed = true; static volatile uint32_t *uart_bootloader_magic_register(void) { #if defined(TAMP) && defined(TAMP_BKP0R) @@ -53,6 +54,14 @@ void uart_bootloader_command_reset(void) { s_command_match_len = 0U; } +void uart_bootloader_set_entry_allowed(bool allowed) { + s_entry_allowed = allowed; +} + +bool uart_bootloader_is_entry_allowed(void) { + return s_entry_allowed; +} + void uart_bootloader_request_reset(void) { uart_bootloader_enable_backup_access(); diff --git a/docs/UartBootloader.md b/docs/UartBootloader.md index 59fce3bf..99e786ee 100644 --- a/docs/UartBootloader.md +++ b/docs/UartBootloader.md @@ -1,86 +1,209 @@ # UART Bootloader -This bootloader is designed for the shared Embedded Sharepoint repo so each firmware project can reuse the same UART flashing flow. +This bootloader lets a board update its app over UART after the bootloader has +been installed once. Normal firmware builds still work the old way by default: +if you do not specify a firmware type, the image is linked and flashed at +`0x08000000`. -## Behavior +## Firmware Types -- Runs from flash start (`0x08000000` by default). -- Listens on the configured bootloader UART at `115200 8E1`. -- LED behavior in bootloader: - - Waiting for host connection: fade in/out. - - Flashing: rapid blink. -- If no handshake arrives before timeout and a valid app exists, it jumps to the app. -- Speaks the STM32 AN3155 UART bootloader protocol subset used by STM32CubeProgrammer. +Use `FIRMWARE_TYPE` to select the memory map and default flash address: -## Memory Map +- `firmware`: standalone application at `0x08000000`. This is the default. +- `bootloader`: resident bootloader at `0x08000000`, default size `64 KB`. +- `app`: app linked after the resident bootloader, default base `0x08010000`. -- Bootloader region: `0x08000000` .. `0x0800FFFF` (default 64 KB). -- App region: `0x08010000` .. end of flash. +Legacy `FIRMWARE_ROLE=app` and `FIRMWARE_ROLE=bootloader` invocations still work, +but new commands should use `FIRMWARE_TYPE`. -Override by build variables: +Defaults can be overridden: -- `BOOTLOADER_SIZE_KB` -- `BOOTLOADER_APP_BASE` (compile-time macro) -- `BOOTLOADER_APP_MAX_SIZE` (compile-time macro) +- `BOOTLOADER_SIZE_KB`: default `0` for `firmware`, `64` for `app` and `bootloader`. +- `BOOTLOADER_APP_BASE`: default `FLASH_BASE + BOOTLOADER_SIZE_KB * 1024`. +- `FLASH_ADDRESS`: default `0x08000000` for `firmware` and `bootloader`, + default `BOOTLOADER_APP_BASE` for `app`. +- `FLASH_BASE`: default `0x08000000`. ## Build +Standalone firmware, no bootloader: + +```bash +make -C test clean TEST=blinky PROJECT_TARGET=stm32g473xx BEAR_ENABLE=0 +make -C test TEST=blinky PROJECT_TARGET=stm32g473xx BEAR_ENABLE=0 +``` + Bootloader image: ```bash -make -C test clean TEST=main PROJECT_TARGET=stm32g473xx FIRMWARE_ROLE=bootloader BEAR_ENABLE=0 -make -C test TEST=main PROJECT_TARGET=stm32g473xx FIRMWARE_ROLE=bootloader BEAR_ENABLE=0 +make -C test clean TEST=main PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=bootloader BEAR_ENABLE=0 +make -C test TEST=main PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=bootloader BEAR_ENABLE=0 +``` + +App image for a board using the resident bootloader: + +```bash +make -C test clean TEST=blinky_bootloader PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=app BEAR_ENABLE=0 +make -C test TEST=blinky_bootloader PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=app BEAR_ENABLE=0 +``` + +## Parent Repo Integration + +Application repos usually live outside this shared embedded repo. The shared +Makefile is designed for that: the parent repo passes absolute app sources, +include paths, target, build directory, and firmware type into this repo. + +Required variables: + +- `PROJECT_TARGET`: MCU target, for example `stm32g473xx`. +- `PROJECT_C_SOURCES`: app source files from the parent repo. +- `PROJECT_C_INCLUDES`: app include directories from the parent repo. +- `PROJECT_BUILD_DIR`: output directory for objects, ELF, HEX, and BIN. +- `FIRMWARE_TYPE`: optional. Defaults to `firmware`; use `app` for a + bootloader-linked image. + +Example parent repo layout: + +```text +my-app/ + Makefile + Src/main.c + Inc/app_config.h + vendor/ES_BLT/ +``` + +Example parent `Makefile`: + +```make +ES_BLT_DIR := vendor/ES_BLT +PROJECT_TARGET ?= stm32g473xx +FIRMWARE_TYPE ?= firmware + +PROJECT_C_SOURCES := $(abspath Src/main.c) +PROJECT_C_INCLUDES := $(abspath Inc) + +ifeq ($(FIRMWARE_TYPE),app) +PROJECT_BUILD_DIR := $(abspath build/app) +else +PROJECT_BUILD_DIR := $(abspath build) +endif + +export PROJECT_TARGET +export PROJECT_C_SOURCES +export PROJECT_C_INCLUDES +export PROJECT_BUILD_DIR +export FIRMWARE_TYPE + +.PHONY: all clean flash flash-uart +all: + $(MAKE) -C $(ES_BLT_DIR) all + +clean: + $(MAKE) -C $(ES_BLT_DIR) clean + +flash: + $(MAKE) -C $(ES_BLT_DIR) flash + +flash-uart: + $(MAKE) -C $(ES_BLT_DIR) flash-uart +``` + +With that wrapper: + +```bash +# Old behavior: standalone app linked and flashed at 0x08000000. +make +make flash + +# Bootloader app: linked at 0x08010000 by default. +make clean FIRMWARE_TYPE=app +make FIRMWARE_TYPE=app +make flash-uart FIRMWARE_TYPE=app ``` -App image (linked after bootloader): +For a custom bootloader size, override only `BOOTLOADER_SIZE_KB`; the app base +and flash address follow automatically: ```bash -make -C test clean TEST=blinky_bootloader PROJECT_TARGET=stm32g473xx FIRMWARE_ROLE=app BOOTLOADER_SIZE_KB=64 BEAR_ENABLE=0 -make -C test TEST=blinky_bootloader PROJECT_TARGET=stm32g473xx FIRMWARE_ROLE=app BOOTLOADER_SIZE_KB=64 BEAR_ENABLE=0 +make FIRMWARE_TYPE=app BOOTLOADER_SIZE_KB=96 ``` -## Flash Flow +If a parent repo needs a fixed nonstandard app base, override +`BOOTLOADER_APP_BASE` directly. + +## Flash + +Standalone firmware uses the normal ST-Link flow and flashes to `0x08000000`: + +```bash +make -C test flash TEST=blinky PROJECT_TARGET=stm32g473xx BEAR_ENABLE=0 +``` -### 1) Initial bootloader flash +Install the resident bootloader once using STM32 ROM boot mode: -Use STM32 ROM boot mode (BOOT pin/switch) and flash bootloader at `0x08000000`. +```bash +./scripts/flash_bootloader.py +``` -You can use existing helper: +If autodetection picks the wrong UART, pass the port explicitly: ```bash -./flash-uart.sh build/stm32g473xx.bin 0x08000000 +./scripts/flash_bootloader.py --port /dev/cu.usbserial-310 ``` -### 2) Subsequent app updates through bootloader +Flash an app through the resident bootloader: -With board running bootloader (or reset into bootloader), run: +```bash +./scripts/uart_bootloader_flash.py --boot +``` + +If the app is already running and supports the ES_BLT magic packet, ask it to +reset into the bootloader first: ```bash -python3 scripts/uart_bootloader_flash.py --port /dev/tty.usbserial-310 --bin build/app/stm32g473xx.bin --boot +./scripts/uart_bootloader_flash.py --enter --boot ``` -## Wire Protocol (host <-> bootloader) +Only enter bootloader without flashing: -- Handshake: - - Host sends `0x7F` - - Bootloader replies `ACK(0x79)` -- UART format: - - `115200 8E1` -- Command packets follow AN3155: - - `CMD`, then bitwise-complement command byte. - - Multi-byte addresses are sent MSB first. - - Checksums are XOR bytes, matching AN3155. +```bash +./scripts/uart_bootloader_enter.py +``` -Commands: +All UART scripts accept `--port`, `--baud`, and path/address overrides. -- `0x00`: Get -- `0x01`: Get Version -- `0x02`: Get ID -- `0x11`: Read Memory -- `0x21`: Go -- `0x31`: Write Memory -- `0x43`: Erase -- `0x44`: Extended Erase +## Runtime Entry Policy -The implementation constrains erase and write operations to the app region so the resident +Apps can deny command-based bootloader entry while they are in an unsafe state: + +```c +uart_bootloader_set_entry_allowed(false); +``` + +Set it back to true when the app is safe to reset: + +```c +uart_bootloader_set_entry_allowed(true); +``` + +Hard faults still force bootloader entry for apps built with `FIRMWARE_TYPE=app`. +Standalone `FIRMWARE_TYPE=firmware` apps keep the normal debug hardfault handler. + +## Wire Protocol + +The resident bootloader speaks the STM32 AN3155 UART protocol subset used by +STM32CubeProgrammer: + +- UART format: `115200 8E1`. +- Handshake: host sends `0x7F`, bootloader replies `ACK(0x79)`. +- Supported commands: Get, Get Version, Get ID, Read Memory, Go, Write Memory, + Erase, and Extended Erase. + +Erase and write operations are constrained to the app region so the resident bootloader is not erased by CubeProgrammer special erase requests. + +Apps enter the bootloader through the common UART command packet: + +```text +ESBLT_BOOT\n +``` diff --git a/scripts/flash_bootloader.py b/scripts/flash_bootloader.py new file mode 100755 index 00000000..7273ffeb --- /dev/null +++ b/scripts/flash_bootloader.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +import argparse +import glob +import os +import subprocess +import sys + + +BOOTLOADER_ADDRESS = "0x08000000" +DEFAULT_BOOTLOADER_BIN = "build/bootloader/stm32g473xx.bin" + + +def default_stm32prog() -> str: + if sys.platform == "darwin": + return ( + "/Applications/STMicroelectronics/STM32Cube/STM32CubeProgrammer/" + "STM32CubeProgrammer.app/Contents/Resources/bin/STM32_Programmer_CLI" + ) + return os.path.expanduser("~/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin/STM32_Programmer_CLI") + + +def detect_port() -> str | None: + patterns = ( + "/dev/cu.usbserial*", + "/dev/cu.SLAB_USBtoUART*", + "/dev/ttyUSB*", + ) + for pattern in patterns: + ports = sorted(glob.glob(pattern)) + if ports: + return ports[0] + return None + + +def run_stm32prog(stm32prog: str, args: list[str]) -> None: + cmd = [stm32prog] + args + print("+ " + " ".join(cmd)) + subprocess.run(cmd, check=True) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Flash the resident ES_BLT bootloader over STM32 ROM UART") + parser.add_argument("--port", help="Serial device. If omitted, common CP210x paths are autodetected.") + parser.add_argument("--baud", default=115200, type=int, help="UART baud rate") + parser.add_argument("--bin", default=DEFAULT_BOOTLOADER_BIN, help=f"Bootloader binary (default: {DEFAULT_BOOTLOADER_BIN})") + parser.add_argument("--address", default=BOOTLOADER_ADDRESS, help=f"Flash address (default: {BOOTLOADER_ADDRESS})") + parser.add_argument("--stm32prog", default=default_stm32prog(), help="Path to STM32_Programmer_CLI") + args = parser.parse_args() + + port = args.port or detect_port() + if port is None: + print("No serial port found. Pass --port /dev/cu... or /dev/ttyUSB...", file=sys.stderr) + return 1 + if not os.path.isfile(args.bin): + print(f"Binary does not exist: {args.bin}", file=sys.stderr) + return 1 + if not os.path.isfile(args.stm32prog): + print(f"STM32_Programmer_CLI does not exist: {args.stm32prog}", file=sys.stderr) + return 1 + if not os.access(args.stm32prog, os.X_OK): + print(f"STM32_Programmer_CLI is not executable: {args.stm32prog}", file=sys.stderr) + return 1 + + conn = [f"port={port}", f"br={args.baud}"] + try: + run_stm32prog(args.stm32prog, ["-c", *conn, "-w", args.bin, args.address, "-v"]) + except subprocess.CalledProcessError as exc: + return exc.returncode + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/uart_bootloader_enter.py b/scripts/uart_bootloader_enter.py index 63c391bc..66ba79c9 100755 --- a/scripts/uart_bootloader_enter.py +++ b/scripts/uart_bootloader_enter.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import argparse +import glob import sys import time @@ -7,6 +8,19 @@ BOOTLOADER_PACKET = b"ESBLT_BOOT\n" +def detect_port() -> str | None: + patterns = ( + "/dev/cu.usbserial*", + "/dev/cu.SLAB_USBtoUART*", + "/dev/ttyUSB*", + ) + for pattern in patterns: + ports = sorted(glob.glob(pattern)) + if ports: + return ports[0] + return None + + def send_packet(port: str, baud: int, timeout: float, delay: float) -> None: try: import serial @@ -22,19 +36,24 @@ def send_packet(port: str, baud: int, timeout: float, delay: float) -> None: def main() -> int: parser = argparse.ArgumentParser(description="Send the ES_BLT UART magic packet to reset app into bootloader") - parser.add_argument("--port", required=True, help="Serial device (ex: /dev/cu.usbserial-310)") + parser.add_argument("--port", help="Serial device. If omitted, common CP210x paths are autodetected.") parser.add_argument("--baud", default=115200, type=int, help="UART baud rate (default: 115200)") parser.add_argument("--timeout", default=1.0, type=float, help="Serial timeout in seconds") parser.add_argument("--delay", default=0.1, type=float, help="Delay after opening port before sending") args = parser.parse_args() + port = args.port or detect_port() + if port is None: + print("No serial port found. Pass --port /dev/cu... or /dev/ttyUSB...", file=sys.stderr) + return 1 + try: - send_packet(args.port, args.baud, args.timeout, args.delay) + send_packet(port, args.baud, args.timeout, args.delay) except Exception as exc: print(f"Failed to send bootloader packet: {exc}", file=sys.stderr) return 1 - print(f"Sent bootloader packet to {args.port} at {args.baud} baud") + print(f"Sent bootloader packet to {port} at {args.baud} baud") return 0 diff --git a/scripts/uart_bootloader_flash.py b/scripts/uart_bootloader_flash.py index e8d0489e..5553a940 100755 --- a/scripts/uart_bootloader_flash.py +++ b/scripts/uart_bootloader_flash.py @@ -1,11 +1,15 @@ #!/usr/bin/env python3 import argparse +import glob import os import sys import subprocess +import time APP_BASE = "0x08010000" +DEFAULT_APP_BIN = "build/app/stm32g473xx.bin" +BOOTLOADER_PACKET = b"ESBLT_BOOT\n" def default_stm32prog() -> str: @@ -23,16 +27,46 @@ def run_stm32prog(stm32prog: str, args: list[str]) -> None: subprocess.run(cmd, check=True) +def detect_port() -> str | None: + patterns = ( + "/dev/cu.usbserial*", + "/dev/cu.SLAB_USBtoUART*", + "/dev/ttyUSB*", + ) + for pattern in patterns: + ports = sorted(glob.glob(pattern)) + if ports: + return ports[0] + return None + + +def request_bootloader(port: str, baud: int) -> None: + try: + import serial + except ImportError as exc: + raise RuntimeError("pyserial is required. Run from `nix develop` or install requirements.txt.") from exc + + with serial.Serial(port=port, baudrate=baud, timeout=1.0) as uart: + time.sleep(0.1) + uart.write(BOOTLOADER_PACKET) + uart.flush() + + def main() -> int: parser = argparse.ArgumentParser(description="Flash app via ES_BLT AN3155-compatible UART bootloader") - parser.add_argument("--port", required=True, help="Serial device (ex: /dev/tty.usbserial-310)") + parser.add_argument("--port", help="Serial device. If omitted, common CP210x paths are autodetected.") parser.add_argument("--baud", default=115200, type=int, help="UART baud rate") - parser.add_argument("--bin", required=True, help="App binary to flash") + parser.add_argument("--bin", default=DEFAULT_APP_BIN, help=f"App binary to flash (default: {DEFAULT_APP_BIN})") parser.add_argument("--address", default=APP_BASE, help=f"Flash address (default: {APP_BASE})") + parser.add_argument("--enter", action="store_true", help="Send ES_BLT magic packet before flashing") parser.add_argument("--boot", action="store_true", help="Jump to app after flashing") parser.add_argument("--stm32prog", default=default_stm32prog(), help="Path to STM32_Programmer_CLI") args = parser.parse_args() + port = args.port or detect_port() + if port is None: + print("No serial port found. Pass --port /dev/cu... or /dev/ttyUSB...", file=sys.stderr) + return 1 if not os.path.isfile(args.bin): print(f"Binary does not exist: {args.bin}") return 1 @@ -43,8 +77,11 @@ def main() -> int: print(f"STM32_Programmer_CLI is not executable: {args.stm32prog}") return 1 - conn = [f"port={args.port}", f"br={args.baud}"] + conn = [f"port={port}", f"br={args.baud}"] try: + if args.enter: + request_bootloader(port, args.baud) + time.sleep(0.5) run_stm32prog(args.stm32prog, ["-c", *conn, "-w", args.bin, args.address, "-v"]) if args.boot: run_stm32prog(args.stm32prog, ["-c", *conn, "-g", args.address]) diff --git a/test/Makefile b/test/Makefile index 6a419332..4f2c7240 100644 --- a/test/Makefile +++ b/test/Makefile @@ -7,14 +7,23 @@ PROJECT_TARGET ?= stm32g473xx BEAR_ENABLE ?= 1 VERBOSE ?= 0 +ifeq ($(origin FIRMWARE_TYPE), undefined) +ifeq ($(origin FIRMWARE_ROLE), undefined) +FIRMWARE_TYPE := firmware +else +FIRMWARE_TYPE := $(FIRMWARE_ROLE) +endif +endif +FIRMWARE_ROLE := $(FIRMWARE_TYPE) + # source and include directories _PROJECT_C_SOURCES = $(wildcard Src/*.c) _PROJECT_C_INCLUDES = Inc # build and driver directories -ifeq ($(FIRMWARE_ROLE),bootloader) +ifeq ($(FIRMWARE_TYPE),bootloader) _PROJECT_BUILD_DIR ?= ../build/bootloader -else ifeq ($(FIRMWARE_ROLE),app) +else ifeq ($(FIRMWARE_TYPE),app) _PROJECT_BUILD_DIR ?= ../build/app else _PROJECT_BUILD_DIR ?= ../build @@ -33,6 +42,8 @@ export PROJECT_C_INCLUDES export PROJECT_BUILD_DIR export BEAR_ENABLE export VERBOSE +export FIRMWARE_TYPE +export FIRMWARE_ROLE ifeq ($(BEAR_ENABLE), 1) COMPILE_COMMANDS_JSON := $(PROJECT_BUILD_DIR)/compile_commands.json From 05f93e4aa7c547c8addcbae5add18662c1f71dfd Mon Sep 17 00:00:00 2001 From: Parthiv Shah Date: Sun, 26 Apr 2026 01:19:06 -0500 Subject: [PATCH 04/12] VCU testing --- Makefile | 9 ++- bootloader/Src/bootloader_runtime.c | 2 + bsp/Inc/UART.h | 5 +- bsp/Src/UART.c | 12 ++- common/Inc/uart_bootloader.h | 1 + common/Src/uart_bootloader.c | 16 ++++ docs/Installation.md | 19 +++++ docs/README.md | 4 + docs/SharepointSubmodule.md | 27 +++++++ docs/UartBootloader.md | 91 +++++++++++++++++++++- driver/Src/slcanFormat.c | 2 + scripts/uart_bootloader_enter.py | 52 +++++++++++-- scripts/uart_bootloader_flash.py | 60 ++++++++++++-- stm/stm32f4xx/system_stm32f4xx.c | 4 +- stm/stm32g4xx/stm32g4xx_hal_timebase_tim.c | 26 +++++-- stm/stm32g4xx/system_stm32g4xx.c | 4 +- stm/stm32l4xx/system_stm32l4xx.c | 4 +- test/tests/blinky_bootloader_test.c | 14 +--- 18 files changed, 313 insertions(+), 39 deletions(-) diff --git a/Makefile b/Makefile index 26c87781..0533f5d5 100644 --- a/Makefile +++ b/Makefile @@ -219,6 +219,12 @@ C_DEFS += FIRMWARE_USES_BOOTLOADER endif endif +ifeq ($(FIRMWARE_TYPE),app) +ifeq ($(BOOTLOADER_SIZE_KB),0) +$(error FIRMWARE_TYPE=app requires BOOTLOADER_SIZE_KB to be nonzero) +endif +endif + C_DEFS := $(addprefix -D,$(C_DEFS)) # AS includes @@ -348,6 +354,7 @@ else @$(CC) $(OBJECTS) $(LDFLAGS) -o $@ @echo "LD $@" endif + @echo "FIRMWARE_TYPE=$(FIRMWARE_TYPE) BOOTLOADER_SIZE_KB=$(BOOTLOADER_SIZE_KB) BOOTLOADER_APP_BASE=$(BOOTLOADER_APP_BASE) FLASH_ADDRESS=$(FLASH_ADDRESS)" @$(SZ) $@ @echo "Finished compiling. Jolly good!" @@ -410,7 +417,7 @@ flash-uart: ifeq ($(FIRMWARE_TYPE),bootloader) ./scripts/flash_bootloader.py --bin $(BUILD_DIR)/$(FLASH_FILE) --address $(FLASH_ADDRESS) else ifeq ($(FIRMWARE_TYPE),app) - ./scripts/uart_bootloader_flash.py --bin $(BUILD_DIR)/$(FLASH_FILE) --address $(FLASH_ADDRESS) --boot + ./scripts/uart_bootloader_flash.py --bin $(BUILD_DIR)/$(FLASH_FILE) --address $(FLASH_ADDRESS) else ./flash-uart.sh $(BUILD_DIR)/$(FLASH_FILE) $(FLASH_ADDRESS) endif diff --git a/bootloader/Src/bootloader_runtime.c b/bootloader/Src/bootloader_runtime.c index 62341ea5..4690fe7e 100644 --- a/bootloader/Src/bootloader_runtime.c +++ b/bootloader/Src/bootloader_runtime.c @@ -230,7 +230,9 @@ void bootloader_runtime_jump_to_app(void) { void (*app_reset_handler)(void) = (void (*)(void))app_reset; __disable_irq(); + (void)HAL_UART_DeInit(&s_huart); HAL_DeInit(); + HAL_RCC_DeInit(); bootloader_runtime_clear_interrupt_state(); SCB->VTOR = BOOTLOADER_APP_BASE; diff --git a/bsp/Inc/UART.h b/bsp/Inc/UART.h index a6d4395e..3fb71fca 100644 --- a/bsp/Inc/UART.h +++ b/bsp/Inc/UART.h @@ -121,8 +121,9 @@ uart_status_t uart_recv(UART_HandleTypeDef* handle, uint8_t* data, uint16_t leng /** * @brief Services the UART bootloader command parser. * - * Reads one byte from the UART RX queue and resets into the bootloader if the - * configured bootloader magic command is received. + * Waits for one byte from the UART RX queue, then drains any queued bytes so + * burst-sent bootloader commands are parsed before the UART RX queue can fill. + * Resets into the bootloader if the configured magic command is received. * * @param handle Pointer to the UART handle. * @param delay_ticks Ticks to wait for one byte. diff --git a/bsp/Src/UART.c b/bsp/Src/UART.c index 871cb4d0..e143fc2b 100644 --- a/bsp/Src/UART.c +++ b/bsp/Src/UART.c @@ -649,14 +649,22 @@ uart_status_t uart_recv(UART_HandleTypeDef* handle, uint8_t* data, uint16_t leng return status; } +static void uart_bootloader_service_byte(uint8_t byte) { + if (uart_bootloader_feed_command_byte(byte) && uart_bootloader_is_entry_allowed()) { + uart_bootloader_request_reset(); + } +} + uart_status_t uart_bootloader_service(UART_HandleTypeDef* handle, TickType_t delay_ticks) { uint8_t byte = 0U; if (uart_recv(handle, &byte, 1U, delay_ticks) != UART_OK) { return UART_ERR; } - if (uart_bootloader_feed_command_byte(byte) && uart_bootloader_is_entry_allowed()) { - uart_bootloader_request_reset(); + uart_bootloader_service_byte(byte); + + while (uart_recv(handle, &byte, 1U, 0U) == UART_OK) { + uart_bootloader_service_byte(byte); } return UART_OK; diff --git a/common/Inc/uart_bootloader.h b/common/Inc/uart_bootloader.h index 1fff32d0..3d4b8ca6 100644 --- a/common/Inc/uart_bootloader.h +++ b/common/Inc/uart_bootloader.h @@ -11,6 +11,7 @@ bool uart_bootloader_feed_command_byte(uint8_t byte); void uart_bootloader_command_reset(void); void uart_bootloader_set_entry_allowed(bool allowed); bool uart_bootloader_is_entry_allowed(void); +void uart_bootloader_init_app_vector_table(void); void uart_bootloader_request_reset(void); bool uart_bootloader_consume_request(void); diff --git a/common/Src/uart_bootloader.c b/common/Src/uart_bootloader.c index 93a69c6e..c205bea1 100644 --- a/common/Src/uart_bootloader.c +++ b/common/Src/uart_bootloader.c @@ -8,6 +8,14 @@ #include +#if defined(FIRMWARE_TYPE_APP) && !defined(FIRMWARE_USES_BOOTLOADER) +#error "FIRMWARE_TYPE=app requires FIRMWARE_USES_BOOTLOADER. Check BOOTLOADER_SIZE_KB." +#endif + +#if defined(FIRMWARE_USES_BOOTLOADER) && !defined(BOOTLOADER_APP_BASE) +#error "FIRMWARE_USES_BOOTLOADER requires BOOTLOADER_APP_BASE." +#endif + static size_t s_command_match_len = 0U; static volatile bool s_entry_allowed = true; @@ -62,6 +70,14 @@ bool uart_bootloader_is_entry_allowed(void) { return s_entry_allowed; } +void uart_bootloader_init_app_vector_table(void) { +#if defined(FIRMWARE_USES_BOOTLOADER) && defined(BOOTLOADER_APP_BASE) + SCB->VTOR = BOOTLOADER_APP_BASE; + __DSB(); + __ISB(); +#endif +} + void uart_bootloader_request_reset(void) { uart_bootloader_enable_backup_access(); diff --git a/docs/Installation.md b/docs/Installation.md index cca60d57..821fa972 100644 --- a/docs/Installation.md +++ b/docs/Installation.md @@ -84,6 +84,25 @@ After that run ``lsusb`` and make sure you see an ST-Link Debug device is seen b make flash ``` +## Bootloader Build Check + +If your project will use the UART bootloader, also verify that both bootloader +build modes compile in the Nix shell: + +```sh +cd Embedded-Sharepoint +make -C test clean TEST=main PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=bootloader BEAR_ENABLE=0 +make -C test TEST=main PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=bootloader BEAR_ENABLE=0 + +make -C test clean TEST=blinky_bootloader PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=app BEAR_ENABLE=0 +make -C test TEST=blinky_bootloader PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=app BEAR_ENABLE=0 +``` + +Use the `PROJECT_TARGET` for your board. The `app` build should report a flash +origin after the bootloader, usually `0x08010000` for the default `64 KB` +bootloader region. See [UART Bootloader](./UartBootloader.md) for flashing, +parent Makefile setup, and bring-up procedure. + # Common Errors ## Could not open USB device if you're getting: diff --git a/docs/README.md b/docs/README.md index 948ccb24..a56ec7e2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -53,6 +53,10 @@ make flash ## Adding Embedded Sharepoint to your project Follow these [instructions](./SharepointSubmodule.md) on how to add Embedded Sharepoint to your project +## UART Bootloader +For resident UART bootloader setup, app offset builds, parent Makefile changes, +and generic board bring-up procedure, see [UART Bootloader](./UartBootloader.md). + ## Contributing See our [Issues](https://github.com/lhr-solar/Embedded-Sharepoint/issues) to see what you can work on! Once you're ready for a review please open a pull request to merge into main. [Pull Requests explained](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) diff --git a/docs/SharepointSubmodule.md b/docs/SharepointSubmodule.md index 674edc6e..95938e03 100644 --- a/docs/SharepointSubmodule.md +++ b/docs/SharepointSubmodule.md @@ -93,8 +93,17 @@ Below are several variables that the Embedded-Sharepoint Makefile uses to compil * `PROJECT_C_SOURCES`: List of your C source files * `PROJECT_C_INCLUDES`: List of your include directories * `PROJECT_BUILD_DIR`: Where to place build outputs +* `FIRMWARE_TYPE`: Optional firmware memory map. Defaults to `firmware`; use + `app` when building an application that runs behind the UART bootloader. +* `BOOTLOADER_SIZE_KB`: Optional bootloader size override. Defaults to `64` for + `FIRMWARE_TYPE=app` and `FIRMWARE_TYPE=bootloader`. * `BEAR_ENABLE` to make VSCode not mad at you (the red error squiggles). Bear is by default enabled, but you can set it to 0 to turn it off +Do not export empty bootloader override variables from your parent Makefile. +Only export `BOOTLOADER_APP_BASE`, `BOOTLOADER_SIZE_KB`, or `FLASH_ADDRESS` if +you assign a real value. Empty exported values override the defaults computed by +the Embedded-Sharepoint Makefile. + ### Creating a test folder It's good to be able to test independent portions of your code instead of your whole code base at once. For example, if I have a driver just for controlling lights I should have a test file that just runs some code for my lights driver instead of the whole codebase which could be running a lot of things other than lights. @@ -143,6 +152,24 @@ make ``` in the same directory as your new Makefile. +If your project uses the UART bootloader, compile the application with +`FIRMWARE_TYPE=app` so it links after the resident bootloader: + +```sh +make clean FIRMWARE_TYPE=app +make FIRMWARE_TYPE=app +``` + +Flash a bootloader-linked app through the resident bootloader with: + +```sh +make flash-uart FIRMWARE_TYPE=app +``` + +When switching between standalone firmware and bootloader-linked app builds, +always run `make clean` first because the linker script and flash address change. +For full bootloader setup and bring-up steps, see [UART Bootloader](./UartBootloader.md). + If you do not want to type that out everytime, you can create a bash script that runs the nix develop command and hardcodes the directory where the nix file is. See this example in [VCU](https://github.com/lhr-solar/PS-VehicleControlUnit/blob/main/Firmware/run_nix.sh) ## 6. Create a README diff --git a/docs/UartBootloader.md b/docs/UartBootloader.md index 99e786ee..b52e0a6a 100644 --- a/docs/UartBootloader.md +++ b/docs/UartBootloader.md @@ -26,6 +26,9 @@ Defaults can be overridden: ## Build +Run these commands from inside `Embedded-Sharepoint` or from a parent Makefile +that forwards the required project variables into `Embedded-Sharepoint/Makefile`. + Standalone firmware, no bootloader: ```bash @@ -47,6 +50,18 @@ make -C test clean TEST=blinky_bootloader PROJECT_TARGET=stm32g473xx FIRMWARE_TY make -C test TEST=blinky_bootloader PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=app BEAR_ENABLE=0 ``` +For parent repos, the same procedure is: + +```bash +make clean FIRMWARE_TYPE=app +make FIRMWARE_TYPE=app +make flash-uart FIRMWARE_TYPE=app +``` + +Always run `make clean` when switching between `firmware`, `bootloader`, and +`app`; each mode uses a different flash map and stale build outputs can flash +the wrong image. + ## Parent Repo Integration Application repos usually live outside this shared embedded repo. The shared @@ -62,6 +77,19 @@ Required variables: - `FIRMWARE_TYPE`: optional. Defaults to `firmware`; use `app` for a bootloader-linked image. +Bootloader-related overrides: + +- `BOOTLOADER_SIZE_KB`: default `64` for `app` and `bootloader`. +- `BOOTLOADER_APP_BASE`: normally derived from `FLASH_BASE` and + `BOOTLOADER_SIZE_KB`; only set this directly for a nonstandard memory map. +- `FLASH_ADDRESS`: normally derived from `FIRMWARE_TYPE`; only set this for a + custom flash operation. + +Parent Makefiles should not export empty bootloader override variables. For +example, do not export `BOOTLOADER_APP_BASE` unless you actually assign it, +because an empty exported value prevents the shared Makefile default from being +computed. + Example parent repo layout: ```text @@ -131,6 +159,64 @@ make FIRMWARE_TYPE=app BOOTLOADER_SIZE_KB=96 If a parent repo needs a fixed nonstandard app base, override `BOOTLOADER_APP_BASE` directly. +## App Requirements + +Apps that run behind the resident bootloader need three pieces: + +- Build with `FIRMWARE_TYPE=app` so the linker script starts flash at + `BOOTLOADER_APP_BASE`. +- Use the standard Embedded-Sharepoint startup files, which set `SCB->VTOR` to + `BOOTLOADER_APP_BASE` during `SystemInit()` for bootloader app builds. +- Optionally call `uart_bootloader_init_app_vector_table()` at the start of + `main()` as an explicit second set. +- Service the UART bootloader command parser on the same UART used by the + bootloader: + +```c +while (1) { + (void)uart_bootloader_service(husart3, portMAX_DELAY); +} +``` + +`uart_bootloader_service()` waits for one byte, then drains any bytes already +queued by the UART ISR. This keeps parent apps tolerant of host tools that send +the `ESBLT_BOOT\n` command as a short burst. + +If the app cannot safely reset at some point, gate command entry: + +```c +uart_bootloader_set_entry_allowed(false); +``` + +Hard faults still force bootloader entry for apps built with +`FIRMWARE_TYPE=app`; standalone apps keep the normal debug hardfault behavior. + +## Generic Bring-Up Procedure + +Before enabling a full vehicle or board application behind the bootloader, make +a minimal app in the parent repo and build it with `FIRMWARE_TYPE=app`. + +The bring-up app should: + +- Use the standard Embedded-Sharepoint startup files so `SystemInit()` sets the + app vector table before `main()`. +- Optionally call `uart_bootloader_init_app_vector_table()` before `HAL_Init()` + as an explicit second set. +- Initialize the same UART pins and instance used by the resident bootloader. +- Run `uart_bootloader_service()` in a task or main-loop path so + `uart_bootloader_flash.py --enter --boot` can return the board to update mode. +- Blink an LED or use another board-local indicator so you can confirm the app + jumped successfully. +- Keep only peripherals needed for bench validation. Gate or stub hardware + dependencies that can fault when sensors, contactors, transceivers, or other + board connections are absent. +- Add one low-risk peripheral heartbeat, such as a UART print or CAN frame, if + that peripheral is part of the bootloader update path or early board bring-up. + +After the minimal app can be flashed, reset, and re-enter the bootloader, move +the same `FIRMWARE_TYPE=app`, vector-table init, and UART service changes into +the normal application startup path. + ## Flash Standalone firmware uses the normal ST-Link flow and flashes to `0x08000000`: @@ -170,7 +256,10 @@ Only enter bootloader without flashing: ./scripts/uart_bootloader_enter.py ``` -All UART scripts accept `--port`, `--baud`, and path/address overrides. +All UART scripts accept `--port` and `--baud`; the flash script also accepts +path/address overrides. The entry scripts send `ESBLT_BOOT\n` byte-by-byte by +default to avoid overrunning simple app-side UART command paths. Use +`--byte-delay ` if a board needs a different spacing. ## Runtime Entry Policy diff --git a/driver/Src/slcanFormat.c b/driver/Src/slcanFormat.c index ccf057b3..3132f123 100644 --- a/driver/Src/slcanFormat.c +++ b/driver/Src/slcanFormat.c @@ -15,6 +15,8 @@ int can_to_slcan(uint16_t id, uint8_t out_size) { if (dlc > 8) return -1; + if (out == NULL) return -1; + if ((dlc > 0U) && (data == NULL)) return -1; uint8_t needed = 1 + 3 + 1 + (dlc * 2) + 1; if (out_size < needed) return -1; diff --git a/scripts/uart_bootloader_enter.py b/scripts/uart_bootloader_enter.py index 66ba79c9..4757a6be 100755 --- a/scripts/uart_bootloader_enter.py +++ b/scripts/uart_bootloader_enter.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 import argparse import glob +import os import sys import time BOOTLOADER_PACKET = b"ESBLT_BOOT\n" +BOOTLOADER_BYTE_DELAY_SECONDS = 0.005 def detect_port() -> str | None: @@ -21,16 +23,50 @@ def detect_port() -> str | None: return None -def send_packet(port: str, baud: int, timeout: float, delay: float) -> None: +def send_packet_termios(port: str, baud: int, delay: float, byte_delay: float) -> None: + import termios + + baud_attr = getattr(termios, f"B{baud}", None) + if baud_attr is None: + raise RuntimeError(f"Unsupported baud rate for termios fallback: {baud}") + + fd = os.open(port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK) + try: + attrs = termios.tcgetattr(fd) + attrs[0] = 0 + attrs[1] = 0 + attrs[2] = termios.CLOCAL | termios.CREAD | termios.CS8 + attrs[3] = 0 + attrs[4] = baud_attr + attrs[5] = baud_attr + attrs[6][termios.VMIN] = 0 + attrs[6][termios.VTIME] = 10 + + termios.tcsetattr(fd, termios.TCSANOW, attrs) + if delay > 0: + time.sleep(delay) + for byte in BOOTLOADER_PACKET: + os.write(fd, bytes((byte,))) + time.sleep(byte_delay) + termios.tcdrain(fd) + finally: + os.close(fd) + + +def send_packet(port: str, baud: int, timeout: float, delay: float, byte_delay: float) -> None: try: import serial - except ImportError as exc: - raise RuntimeError("pyserial is required. Run this from `nix develop` or install requirements.txt.") from exc + except ImportError: + send_packet_termios(port, baud, delay, byte_delay) + return with serial.Serial(port=port, baudrate=baud, timeout=timeout) as uart: if delay > 0: time.sleep(delay) - uart.write(BOOTLOADER_PACKET) + for byte in BOOTLOADER_PACKET: + uart.write(bytes((byte,))) + uart.flush() + time.sleep(byte_delay) uart.flush() @@ -40,6 +76,12 @@ def main() -> int: parser.add_argument("--baud", default=115200, type=int, help="UART baud rate (default: 115200)") parser.add_argument("--timeout", default=1.0, type=float, help="Serial timeout in seconds") parser.add_argument("--delay", default=0.1, type=float, help="Delay after opening port before sending") + parser.add_argument( + "--byte-delay", + default=BOOTLOADER_BYTE_DELAY_SECONDS, + type=float, + help=f"Delay between ES_BLT command bytes in seconds (default: {BOOTLOADER_BYTE_DELAY_SECONDS})", + ) args = parser.parse_args() port = args.port or detect_port() @@ -48,7 +90,7 @@ def main() -> int: return 1 try: - send_packet(port, args.baud, args.timeout, args.delay) + send_packet(port, args.baud, args.timeout, args.delay, args.byte_delay) except Exception as exc: print(f"Failed to send bootloader packet: {exc}", file=sys.stderr) return 1 diff --git a/scripts/uart_bootloader_flash.py b/scripts/uart_bootloader_flash.py index 5553a940..447a5be4 100755 --- a/scripts/uart_bootloader_flash.py +++ b/scripts/uart_bootloader_flash.py @@ -2,6 +2,7 @@ import argparse import glob import os +import shutil import sys import subprocess import time @@ -10,9 +11,18 @@ APP_BASE = "0x08010000" DEFAULT_APP_BIN = "build/app/stm32g473xx.bin" BOOTLOADER_PACKET = b"ESBLT_BOOT\n" +BOOTLOADER_BYTE_DELAY_SECONDS = 0.005 def default_stm32prog() -> str: + env_path = os.environ.get("STM32_PROGRAMMER_CLI") + if env_path: + return env_path + + path_match = shutil.which("STM32_Programmer_CLI") + if path_match: + return path_match + if sys.platform == "darwin": return ( "/Applications/STMicroelectronics/STM32Cube/STM32CubeProgrammer/" @@ -40,15 +50,49 @@ def detect_port() -> str | None: return None -def request_bootloader(port: str, baud: int) -> None: +def request_bootloader_termios(port: str, baud: int, byte_delay: float) -> None: + import termios + + baud_attr = getattr(termios, f"B{baud}", None) + if baud_attr is None: + raise RuntimeError(f"Unsupported baud rate for termios fallback: {baud}") + + fd = os.open(port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK) + try: + attrs = termios.tcgetattr(fd) + + attrs[0] = 0 # iflag + attrs[1] = 0 # oflag + attrs[2] = termios.CLOCAL | termios.CREAD | termios.CS8 + attrs[3] = 0 # lflag + attrs[4] = baud_attr + attrs[5] = baud_attr + attrs[6][termios.VMIN] = 0 + attrs[6][termios.VTIME] = 10 + + termios.tcsetattr(fd, termios.TCSANOW, attrs) + time.sleep(0.1) + for byte in BOOTLOADER_PACKET: + os.write(fd, bytes((byte,))) + time.sleep(byte_delay) + termios.tcdrain(fd) + finally: + os.close(fd) + + +def request_bootloader(port: str, baud: int, byte_delay: float) -> None: try: import serial - except ImportError as exc: - raise RuntimeError("pyserial is required. Run from `nix develop` or install requirements.txt.") from exc + except ImportError: + request_bootloader_termios(port, baud, byte_delay) + return with serial.Serial(port=port, baudrate=baud, timeout=1.0) as uart: time.sleep(0.1) - uart.write(BOOTLOADER_PACKET) + for byte in BOOTLOADER_PACKET: + uart.write(bytes((byte,))) + uart.flush() + time.sleep(byte_delay) uart.flush() @@ -60,6 +104,12 @@ def main() -> int: parser.add_argument("--address", default=APP_BASE, help=f"Flash address (default: {APP_BASE})") parser.add_argument("--enter", action="store_true", help="Send ES_BLT magic packet before flashing") parser.add_argument("--boot", action="store_true", help="Jump to app after flashing") + parser.add_argument( + "--byte-delay", + default=BOOTLOADER_BYTE_DELAY_SECONDS, + type=float, + help=f"Delay between ES_BLT command bytes in seconds (default: {BOOTLOADER_BYTE_DELAY_SECONDS})", + ) parser.add_argument("--stm32prog", default=default_stm32prog(), help="Path to STM32_Programmer_CLI") args = parser.parse_args() @@ -80,7 +130,7 @@ def main() -> int: conn = [f"port={port}", f"br={args.baud}"] try: if args.enter: - request_bootloader(port, args.baud) + request_bootloader(port, args.baud, args.byte_delay) time.sleep(0.5) run_stm32prog(args.stm32prog, ["-c", *conn, "-w", args.bin, args.address, "-v"]) if args.boot: diff --git a/stm/stm32f4xx/system_stm32f4xx.c b/stm/stm32f4xx/system_stm32f4xx.c index 3bd40f77..890cc46c 100644 --- a/stm/stm32f4xx/system_stm32f4xx.c +++ b/stm/stm32f4xx/system_stm32f4xx.c @@ -176,7 +176,9 @@ void SystemInit(void) #endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */ /* Configure the Vector Table location -------------------------------------*/ -#if defined(USER_VECT_TAB_ADDRESS) +#if defined(FIRMWARE_USES_BOOTLOADER) && defined(BOOTLOADER_APP_BASE) + SCB->VTOR = BOOTLOADER_APP_BASE; /* App vector table after resident bootloader */ +#elif defined(USER_VECT_TAB_ADDRESS) SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */ #endif /* USER_VECT_TAB_ADDRESS */ } diff --git a/stm/stm32g4xx/stm32g4xx_hal_timebase_tim.c b/stm/stm32g4xx/stm32g4xx_hal_timebase_tim.c index 8ea0de47..a3958953 100644 --- a/stm/stm32g4xx/stm32g4xx_hal_timebase_tim.c +++ b/stm/stm32g4xx/stm32g4xx_hal_timebase_tim.c @@ -75,11 +75,14 @@ HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority) uint32_t pFLatency; HAL_StatusTypeDef status; - /* Configure the TIM6 IRQ priority */ - HAL_NVIC_SetPriority(TIM6_DAC_IRQn, TickPriority, 0U); - - /* Enable the TIM6 global Interrupt */ - HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn); + /* + * A bootloader may leave a TIM6 interrupt pending before jumping to the app. + * Initialize the handle and clear stale IRQ state before enabling the NVIC + * line, otherwise the handler can run with an uninitialized TimHandle. + */ + TimHandle.Instance = TIM6; + HAL_NVIC_DisableIRQ(TIM6_DAC_IRQn); + HAL_NVIC_ClearPendingIRQ(TIM6_DAC_IRQn); /* Enable TIM6 clock */ __HAL_RCC_TIM6_CLK_ENABLE(); @@ -103,9 +106,6 @@ HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority) /* Compute the prescaler value to have TIM6 counter clock equal to 1MHz */ uwPrescalerValue = (uint32_t)((uwTimclock / 1000000U) - 1U); - /* Initialize TIM6 */ - TimHandle.Instance = TIM6; - /* Initialize TIMx peripheral as follow: + Period = [(TIM6CLK/1000) - 1]. to have a (1/1000) s time base. + Prescaler = (uwTimclock/1000000 - 1) to have a 1MHz counter clock. @@ -119,6 +119,16 @@ HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority) status = HAL_TIM_Base_Init(&TimHandle); if (status == HAL_OK) { + /* Configure the TIM6 IRQ priority */ + HAL_NVIC_SetPriority(TIM6_DAC_IRQn, TickPriority, 0U); + + /* Clear any pending update interrupt before enabling the NVIC line. */ + __HAL_TIM_CLEAR_FLAG(&TimHandle, TIM_FLAG_UPDATE); + HAL_NVIC_ClearPendingIRQ(TIM6_DAC_IRQn); + + /* Enable the TIM6 global Interrupt */ + HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn); + /* Start the TIM time Base generation in interrupt mode */ status = HAL_TIM_Base_Start_IT(&TimHandle); if (status == HAL_OK) diff --git a/stm/stm32g4xx/system_stm32g4xx.c b/stm/stm32g4xx/system_stm32g4xx.c index 72f68e94..4155a316 100644 --- a/stm/stm32g4xx/system_stm32g4xx.c +++ b/stm/stm32g4xx/system_stm32g4xx.c @@ -166,7 +166,9 @@ void SystemInit(void) #endif /* Configure the Vector Table location add offset address ------------------*/ -#ifdef VECT_TAB_SRAM +#if defined(FIRMWARE_USES_BOOTLOADER) && defined(BOOTLOADER_APP_BASE) + SCB->VTOR = BOOTLOADER_APP_BASE; /* App vector table after resident bootloader */ +#elif defined(VECT_TAB_SRAM) SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */ #else SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */ diff --git a/stm/stm32l4xx/system_stm32l4xx.c b/stm/stm32l4xx/system_stm32l4xx.c index be9cfee2..cb0f5f58 100644 --- a/stm/stm32l4xx/system_stm32l4xx.c +++ b/stm/stm32l4xx/system_stm32l4xx.c @@ -196,7 +196,9 @@ void SystemInit(void) { -#if defined(USER_VECT_TAB_ADDRESS) +#if defined(FIRMWARE_USES_BOOTLOADER) && defined(BOOTLOADER_APP_BASE) + SCB->VTOR = BOOTLOADER_APP_BASE; /* App vector table after resident bootloader */ +#elif defined(USER_VECT_TAB_ADDRESS) /* Configure the Vector Table location -------------------------------------*/ SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; #endif diff --git a/test/tests/blinky_bootloader_test.c b/test/tests/blinky_bootloader_test.c index 39b26038..64ac7c87 100644 --- a/test/tests/blinky_bootloader_test.c +++ b/test/tests/blinky_bootloader_test.c @@ -1,5 +1,6 @@ #include "stm32xx_hal.h" #include "UART.h" +#include "uart_bootloader.h" #if defined(STM32L432xx) #define LED_PIN GPIO_PIN_3 @@ -25,10 +26,6 @@ #error "No UART available for bootloader command test." #endif -#ifndef APP_VECTOR_TABLE_BASE -#define APP_VECTOR_TABLE_BASE (0x08010000UL) -#endif - static StaticTask_t s_blinky_task_buffer; static StaticTask_t s_uart_task_buffer; static StackType_t s_blinky_task_stack[configMINIMAL_STACK_SIZE]; @@ -46,13 +43,6 @@ static void heartbeat_clock_init(void) { } } -static void vectortable_init(void) { - /* App is linked after bootloader, so move VTOR accordingly. */ - SCB->VTOR = APP_VECTOR_TABLE_BASE; - __DSB(); - __ISB(); -} - static void led_init(void) { GPIO_InitTypeDef led_config = {0}; led_config.Mode = GPIO_MODE_OUTPUT_PP; @@ -99,7 +89,7 @@ static void uart_task(void *argument) { } int main(void) { - vectortable_init(); + uart_bootloader_init_app_vector_table(); HAL_Init(); SystemClock_Config(); From a7ef3a963ef636603937ef86b8cc301830bfdcde Mon Sep 17 00:00:00 2001 From: Parthiv Shah Date: Wed, 29 Apr 2026 18:13:28 -0500 Subject: [PATCH 05/12] standardization across target and hosts --- Makefile | 18 +++- bootloader/Src/bootloader_main.c | 19 +++- bootloader/Src/bootloader_runtime.c | 5 +- docs/UartBootloader.md | 53 +++++++++- scripts/bootloader_script_utils.py | 146 ++++++++++++++++++++++++++++ scripts/flash_bootloader.py | 66 +++++-------- scripts/uart_bootloader_enter.py | 23 ++--- scripts/uart_bootloader_flash.py | 79 ++++++--------- 8 files changed, 286 insertions(+), 123 deletions(-) create mode 100644 scripts/bootloader_script_utils.py diff --git a/Makefile b/Makefile index 0533f5d5..1e878dbe 100644 --- a/Makefile +++ b/Makefile @@ -108,6 +108,17 @@ VERBOSE ?= 0 ####################################### # Build path BUILD_DIR = $(PROJECT_BUILD_DIR) +ORIG_LDSCRIPT = stm/$(SERIES_GENERIC)/$(SERIES_LINE)/$(SERIES_LINE_CAP)$(EXTRA_CAP)x_FLASH.ld +GENERATED_LDSCRIPT = $(BUILD_DIR)/$(TARGET)_$(FIRMWARE_ROLE).ld +FLASH_SIZE_KB ?= $(shell python3 -c 'import pathlib,re,sys; ld=pathlib.Path("$(ORIG_LDSCRIPT)").read_text(); m=re.search(r"FLASH\s*\(rx\)\s*:\s*ORIGIN\s*=\s*0x[0-9a-fA-F]+,\s*LENGTH\s*=\s*([0-9]+)K", ld); sys.exit(1) if m is None else print(m.group(1))') +BOOTLOADER_APP_MAX_SIZE ?= $(shell python3 -c 'flash=int("$(FLASH_SIZE_KB)"); boot=int("$(BOOTLOADER_SIZE_KB)"); print((flash - boot) * 1024)') + +ifneq ($(filter $(FIRMWARE_TYPE),app bootloader),) +ifeq ($(shell python3 -c 'print(int("$(BOOTLOADER_SIZE_KB)") < int("$(FLASH_SIZE_KB)"))'),False) +$(error BOOTLOADER_SIZE_KB ($(BOOTLOADER_SIZE_KB)) must be smaller than target flash size ($(FLASH_SIZE_KB) KB)) +endif +endif + # FreeRTOS path FREERTOS_PATH := middleware/FreeRTOS-Kernel FATFS_PATH := middleware/FatFs @@ -211,6 +222,7 @@ endif ifneq ($(filter $(FIRMWARE_TYPE),app bootloader),) C_DEFS += BOOTLOADER_APP_BASE=$(BOOTLOADER_APP_BASE) +C_DEFS += BOOTLOADER_APP_MAX_SIZE=$(BOOTLOADER_APP_MAX_SIZE) endif ifeq ($(FIRMWARE_TYPE),app) @@ -266,8 +278,6 @@ CFLAGS += -MMD -MP -MF"$(@:%.o=%.d)" # LDFLAGS ####################################### # link script -ORIG_LDSCRIPT = stm/$(SERIES_GENERIC)/$(SERIES_LINE)/$(SERIES_LINE_CAP)$(EXTRA_CAP)x_FLASH.ld -GENERATED_LDSCRIPT = $(BUILD_DIR)/$(TARGET)_$(FIRMWARE_ROLE).ld LDSCRIPT = $(ORIG_LDSCRIPT) ifeq ($(FIRMWARE_TYPE),app) @@ -386,8 +396,8 @@ $(BUILD_DIR): mkdir -p $@ $(GENERATED_LDSCRIPT): $(ORIG_LDSCRIPT) Makefile | $(BUILD_DIR) - @python3 -c 'import pathlib; ld = pathlib.Path("$(ORIG_LDSCRIPT)").read_text(); size_kb = int("$(BOOTLOADER_SIZE_KB)"); flash_origin = int("$(FLASH_BASE)", 0) + size_kb * 1024; flash_length_kb = 512 - size_kb; ld = ld.replace("FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K", f"FLASH (rx) : ORIGIN = 0x{flash_origin:08x}, LENGTH = {flash_length_kb}K"); pathlib.Path("$(GENERATED_LDSCRIPT)").write_text(ld)' - @echo "LD_SCRIPT $(ORIG_LDSCRIPT) -> $(GENERATED_LDSCRIPT) ($(FIRMWARE_TYPE), FLASH=$(BOOTLOADER_APP_BASE), LENGTH=$$((512-$(BOOTLOADER_SIZE_KB)))K)" + @python3 -c 'import pathlib,re,sys; ld_path=pathlib.Path("$(ORIG_LDSCRIPT)"); ld=ld_path.read_text(); size_kb=int("$(BOOTLOADER_SIZE_KB)"); flash_kb=int("$(FLASH_SIZE_KB)"); flash_origin=int("$(FLASH_BASE)", 0) + size_kb * 1024; flash_length_kb=flash_kb - size_kb; pattern=r"FLASH\s*\(rx\)\s*:\s*ORIGIN\s*=\s*0x[0-9a-fA-F]+,\s*LENGTH\s*=\s*[0-9]+K"; repl="FLASH (rx) : ORIGIN = 0x%08x, LENGTH = %dK" % (flash_origin, flash_length_kb); ld, count=re.subn(pattern, repl, ld, count=1); sys.exit("Could not find FLASH region in %s" % ld_path) if count != 1 else pathlib.Path("$(GENERATED_LDSCRIPT)").write_text(ld)' + @echo "LD_SCRIPT $(ORIG_LDSCRIPT) -> $(GENERATED_LDSCRIPT) ($(FIRMWARE_TYPE), FLASH=$(BOOTLOADER_APP_BASE), LENGTH=$$(( $(FLASH_SIZE_KB)-$(BOOTLOADER_SIZE_KB) ))K)" ####################################### # clean up diff --git a/bootloader/Src/bootloader_main.c b/bootloader/Src/bootloader_main.c index e431cc2d..7b98d5c4 100644 --- a/bootloader/Src/bootloader_main.c +++ b/bootloader/Src/bootloader_main.c @@ -25,10 +25,12 @@ void SystemClock_Config(void); #define BL_CMD_EXT_ERASE ((uint8_t)0x44U) #define BL_VERSION ((uint8_t)0x10U) -#define BL_DEVICE_ID ((uint16_t)0x0469U) - #define BL_FLASH_BASE ((uint32_t)0x08000000UL) -#define BL_FLASH_PAGE_SIZE ((uint32_t)0x800UL) /* 2KB pages on STM32G473 */ +#if defined(FLASH_PAGE_SIZE) +#define BL_FLASH_PAGE_SIZE ((uint32_t)FLASH_PAGE_SIZE) +#else +#define BL_FLASH_PAGE_SIZE ((uint32_t)0x800UL) +#endif #define BL_OPTION_BYTES_BASE ((uint32_t)0x1FFF7800UL) #define BL_OPTION_BYTES_SIZE ((uint32_t)0x80UL) @@ -88,6 +90,14 @@ static bool bl_xor_ok(const uint8_t *data, size_t len, uint8_t expected) { return crc == expected; } +static uint16_t bl_device_id(void) { +#if defined(DBGMCU) + return (uint16_t)(DBGMCU->IDCODE & 0x0FFFU); +#else + return 0U; +#endif +} + static void bl_send_ack(void) { uint8_t b = BL_ACK; (void)bootloader_runtime_send_bytes(&b, 1U); @@ -152,7 +162,8 @@ static bool bl_handle_get_version(void) { static bool bl_handle_get_id(void) { bl_send_ack(); - uint8_t out[3] = {0x01U, (uint8_t)(BL_DEVICE_ID >> 8), (uint8_t)BL_DEVICE_ID}; + uint16_t device_id = bl_device_id(); + uint8_t out[3] = {0x01U, (uint8_t)(device_id >> 8), (uint8_t)device_id}; if (!bootloader_runtime_send_bytes(out, sizeof(out))) { return false; } diff --git a/bootloader/Src/bootloader_runtime.c b/bootloader/Src/bootloader_runtime.c index 4690fe7e..ba695904 100644 --- a/bootloader/Src/bootloader_runtime.c +++ b/bootloader/Src/bootloader_runtime.c @@ -107,7 +107,7 @@ static uint32_t bootloader_app_end_addr(void) { return BOOTLOADER_APP_BASE + BOOTLOADER_APP_MAX_SIZE; } -static bool bootloader_flash_is_dual_bank(void) { +static bool __attribute__((unused)) bootloader_flash_is_dual_bank(void) { #if defined(FLASH_OPTR_DBANK) return READ_BIT(FLASH->OPTR, FLASH_OPTR_DBANK) != 0U; #else @@ -178,7 +178,8 @@ bool bootloader_runtime_erase_app(void) { } bool bootloader_runtime_write_app(uint32_t app_offset, const uint8_t *data, size_t len) { - if ((app_offset + len) > BOOTLOADER_APP_MAX_SIZE) { + if ((data == NULL) || (app_offset > BOOTLOADER_APP_MAX_SIZE) || + (len > (BOOTLOADER_APP_MAX_SIZE - app_offset))) { return false; } diff --git a/docs/UartBootloader.md b/docs/UartBootloader.md index b52e0a6a..a4d609d1 100644 --- a/docs/UartBootloader.md +++ b/docs/UartBootloader.md @@ -20,6 +20,8 @@ Defaults can be overridden: - `BOOTLOADER_SIZE_KB`: default `0` for `firmware`, `64` for `app` and `bootloader`. - `BOOTLOADER_APP_BASE`: default `FLASH_BASE + BOOTLOADER_SIZE_KB * 1024`. +- `BOOTLOADER_APP_MAX_SIZE`: default target flash size minus `BOOTLOADER_SIZE_KB`. + This is derived from the target linker script. - `FLASH_ADDRESS`: default `0x08000000` for `firmware` and `bootloader`, default `BOOTLOADER_APP_BASE` for `app`. - `FLASH_BASE`: default `0x08000000`. @@ -82,6 +84,7 @@ Bootloader-related overrides: - `BOOTLOADER_SIZE_KB`: default `64` for `app` and `bootloader`. - `BOOTLOADER_APP_BASE`: normally derived from `FLASH_BASE` and `BOOTLOADER_SIZE_KB`; only set this directly for a nonstandard memory map. +- `BOOTLOADER_APP_MAX_SIZE`: normally derived from the target linker script. - `FLASH_ADDRESS`: normally derived from `FIRMWARE_TYPE`; only set this for a custom flash operation. @@ -170,7 +173,9 @@ Apps that run behind the resident bootloader need three pieces: - Optionally call `uart_bootloader_init_app_vector_table()` at the start of `main()` as an explicit second set. - Service the UART bootloader command parser on the same UART used by the - bootloader: + bootloader. The resident bootloader chooses `USART3`, then `USART2`, then + `USART1` by default, but parent apps can override the resident UART with a + compile definition such as `BOOTLOADER_UART_INSTANCE=USART2`. ```c while (1) { @@ -182,6 +187,11 @@ while (1) { queued by the UART ISR. This keeps parent apps tolerant of host tools that send the `ESBLT_BOOT\n` command as a short burst. +The app-side command UART uses the normal BSP UART configuration chosen by the +app, commonly `115200 8N1`. After reset, the resident bootloader speaks the +STM32 AN3155 UART protocol at `115200 8E1`; the flashing scripts and +STM32CubeProgrammer handle that transition. + If the app cannot safely reset at some point, gate command entry: ```c @@ -237,6 +247,37 @@ If autodetection picks the wrong UART, pass the port explicitly: ./scripts/flash_bootloader.py --port /dev/cu.usbserial-310 ``` +The scripts are portable across macOS, Linux, WSL, and Windows. They use +`pyserial` port discovery when available, with fallback globs for common Unix +device names. Typical port arguments are: + +```bash +./scripts/flash_bootloader.py --port /dev/cu.usbmodem1101 # macOS +./scripts/flash_bootloader.py --port /dev/ttyACM0 # Linux +./scripts/flash_bootloader.py --port /dev/ttyUSB0 # WSL after usbipd attach +python scripts/flash_bootloader.py --port COM4 # Windows +``` + +Set `STM32_PROGRAMMER_CLI` if STM32CubeProgrammer is installed somewhere +non-standard. Otherwise the scripts check `PATH`, the default macOS app bundle, +common Linux install directories, and Windows `Program Files`. When running from +WSL, attach the USB-UART adapter to WSL with `usbipd` and use the Linux device +that appears inside WSL, typically `/dev/ttyUSB` or `/dev/ttyACM`. + +Typical WSL setup from an administrator PowerShell: + +```powershell +usbipd list +usbipd bind --busid +usbipd attach --wsl --busid +``` + +Then check the device from WSL: + +```bash +ls /dev/ttyUSB* /dev/ttyACM* +``` + Flash an app through the resident bootloader: ```bash @@ -257,9 +298,13 @@ Only enter bootloader without flashing: ``` All UART scripts accept `--port` and `--baud`; the flash script also accepts -path/address overrides. The entry scripts send `ESBLT_BOOT\n` byte-by-byte by -default to avoid overrunning simple app-side UART command paths. Use -`--byte-delay ` if a board needs a different spacing. +path/address overrides. By default, the flash scripts use the first matching +`build/bootloader/stm*.bin` or `build/app/stm*.bin`, so they work across targets +without editing the script. The entry scripts send `ESBLT_BOOT\n` byte-by-byte +by default to avoid overrunning simple app-side UART command paths. Use +`--byte-delay ` if a board needs a different spacing. On Windows, +install the Python dependencies from `requirements.txt`; `pyserial` is required +there because the Unix `termios` fallback is not available. ## Runtime Entry Policy diff --git a/scripts/bootloader_script_utils.py b/scripts/bootloader_script_utils.py new file mode 100644 index 00000000..4df304d4 --- /dev/null +++ b/scripts/bootloader_script_utils.py @@ -0,0 +1,146 @@ +import glob +import os +import platform +import shutil +import subprocess +import sys +from pathlib import Path + + +def is_wsl() -> bool: + if sys.platform != "linux": + return False + try: + version = Path("/proc/version").read_text(encoding="utf-8").lower() + except OSError: + return False + return "microsoft" in version or "wsl" in version + + +def first_existing_path(candidates: list[str]) -> str | None: + for candidate in candidates: + expanded = os.path.expandvars(os.path.expanduser(candidate)) + if os.path.isfile(expanded): + return expanded + return None + + +def default_stm32prog() -> str: + env_path = os.environ.get("STM32_PROGRAMMER_CLI") + if env_path: + return env_path + + for exe in ("STM32_Programmer_CLI", "STM32_Programmer_CLI.exe"): + path_match = shutil.which(exe) + if path_match: + return path_match + + candidates: list[str] = [] + if sys.platform == "darwin": + candidates.append( + "/Applications/STMicroelectronics/STM32Cube/STM32CubeProgrammer/" + "STM32CubeProgrammer.app/Contents/Resources/bin/STM32_Programmer_CLI" + ) + elif sys.platform == "win32": + candidates.extend( + [ + r"%ProgramFiles%\STMicroelectronics\STM32Cube\STM32CubeProgrammer\bin\STM32_Programmer_CLI.exe", + r"%ProgramFiles(x86)%\STMicroelectronics\STM32Cube\STM32CubeProgrammer\bin\STM32_Programmer_CLI.exe", + ] + ) + else: + candidates.extend( + [ + "~/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin/STM32_Programmer_CLI", + "/opt/stm32cubeprogrammer/bin/STM32_Programmer_CLI", + "/opt/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin/STM32_Programmer_CLI", + ] + ) + existing = first_existing_path(candidates) + if existing is not None: + return existing + return os.path.expandvars(os.path.expanduser(candidates[0])) if candidates else "STM32_Programmer_CLI" + + +def default_binary(pattern: str) -> str: + matches = sorted(glob.glob(pattern)) + return matches[0] if matches else pattern + + +def serial_port_sort_key(port) -> tuple[int, str]: + text = " ".join( + str(value or "") + for value in (getattr(port, "device", ""), getattr(port, "description", ""), getattr(port, "hwid", "")) + ).lower() + preferred_tokens = ("stm", "stlink", "st-link", "usbserial", "cp210", "silab", "usb") + score = 0 if any(token in text for token in preferred_tokens) else 1 + return (score, getattr(port, "device", "")) + + +def detect_port_with_pyserial() -> str | None: + try: + from serial.tools import list_ports + except ImportError: + return None + + ports = sorted(list_ports.comports(), key=serial_port_sort_key) + return ports[0].device if ports else None + + +def detect_port_with_glob() -> str | None: + patterns: tuple[str, ...] + if sys.platform == "darwin": + patterns = ( + "/dev/cu.usbmodem*", + "/dev/cu.usbserial*", + "/dev/cu.SLAB_USBtoUART*", + ) + elif sys.platform == "win32": + return None + else: + patterns = ("/dev/ttyACM*", "/dev/ttyUSB*", "/dev/serial/by-id/*") + + for pattern in patterns: + matches = sorted(glob.glob(pattern)) + if matches: + return matches[0] + return None + + +def detect_port() -> str | None: + return detect_port_with_pyserial() or detect_port_with_glob() + + +def normalize_port_for_stm32prog(port: str, stm32prog: str) -> str: + return port + + +def normalize_port_for_pyserial(port: str) -> str: + return port + + +def port_help() -> str: + system = platform.system() + if is_wsl(): + return "No serial port found. Attach the USB-UART into WSL with usbipd, then pass --port /dev/ttyACM or /dev/ttyUSB." + if system == "Windows": + return "No serial port found. Pass --port COM." + if system == "Darwin": + return "No serial port found. Pass --port /dev/cu.usbmodem..., /dev/cu.usbserial..., or /dev/cu.SLAB_USBtoUART..." + return "No serial port found. Pass --port /dev/ttyACM, /dev/ttyUSB, or /dev/serial/by-id/..." + + +def validate_file(path: str, description: str, executable: bool = False) -> bool: + if not os.path.isfile(path): + print(f"{description} does not exist: {path}", file=sys.stderr) + return False + if executable and sys.platform != "win32" and not os.access(path, os.X_OK): + print(f"{description} is not executable: {path}", file=sys.stderr) + return False + return True + + +def run_stm32prog(stm32prog: str, args: list[str]) -> None: + cmd = [stm32prog] + args + print("+ " + " ".join(f'"{part}"' if " " in part else part for part in cmd)) + subprocess.run(cmd, check=True) diff --git a/scripts/flash_bootloader.py b/scripts/flash_bootloader.py index 7273ffeb..0376e6c8 100755 --- a/scripts/flash_bootloader.py +++ b/scripts/flash_bootloader.py @@ -1,71 +1,53 @@ #!/usr/bin/env python3 import argparse -import glob -import os -import subprocess import sys +from bootloader_script_utils import ( + default_binary, + default_stm32prog, + detect_port, + normalize_port_for_stm32prog, + port_help, + run_stm32prog, + validate_file, +) BOOTLOADER_ADDRESS = "0x08000000" -DEFAULT_BOOTLOADER_BIN = "build/bootloader/stm32g473xx.bin" +DEFAULT_BOOTLOADER_BIN_GLOB = "build/bootloader/stm*.bin" -def default_stm32prog() -> str: - if sys.platform == "darwin": - return ( - "/Applications/STMicroelectronics/STM32Cube/STM32CubeProgrammer/" - "STM32CubeProgrammer.app/Contents/Resources/bin/STM32_Programmer_CLI" - ) - return os.path.expanduser("~/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin/STM32_Programmer_CLI") - - -def detect_port() -> str | None: - patterns = ( - "/dev/cu.usbserial*", - "/dev/cu.SLAB_USBtoUART*", - "/dev/ttyUSB*", - ) - for pattern in patterns: - ports = sorted(glob.glob(pattern)) - if ports: - return ports[0] - return None - - -def run_stm32prog(stm32prog: str, args: list[str]) -> None: - cmd = [stm32prog] + args - print("+ " + " ".join(cmd)) - subprocess.run(cmd, check=True) +def default_bootloader_bin() -> str: + return default_binary(DEFAULT_BOOTLOADER_BIN_GLOB) def main() -> int: parser = argparse.ArgumentParser(description="Flash the resident ES_BLT bootloader over STM32 ROM UART") parser.add_argument("--port", help="Serial device. If omitted, common CP210x paths are autodetected.") parser.add_argument("--baud", default=115200, type=int, help="UART baud rate") - parser.add_argument("--bin", default=DEFAULT_BOOTLOADER_BIN, help=f"Bootloader binary (default: {DEFAULT_BOOTLOADER_BIN})") + parser.add_argument("--bin", default=default_bootloader_bin(), help="Bootloader binary") parser.add_argument("--address", default=BOOTLOADER_ADDRESS, help=f"Flash address (default: {BOOTLOADER_ADDRESS})") parser.add_argument("--stm32prog", default=default_stm32prog(), help="Path to STM32_Programmer_CLI") args = parser.parse_args() port = args.port or detect_port() if port is None: - print("No serial port found. Pass --port /dev/cu... or /dev/ttyUSB...", file=sys.stderr) - return 1 - if not os.path.isfile(args.bin): - print(f"Binary does not exist: {args.bin}", file=sys.stderr) + print(port_help(), file=sys.stderr) return 1 - if not os.path.isfile(args.stm32prog): - print(f"STM32_Programmer_CLI does not exist: {args.stm32prog}", file=sys.stderr) + if not validate_file(args.bin, "Binary"): return 1 - if not os.access(args.stm32prog, os.X_OK): - print(f"STM32_Programmer_CLI is not executable: {args.stm32prog}", file=sys.stderr) + if not validate_file(args.stm32prog, "STM32_Programmer_CLI", executable=True): return 1 - conn = [f"port={port}", f"br={args.baud}"] + stm32prog_port = normalize_port_for_stm32prog(port, args.stm32prog) + conn = [f"port={stm32prog_port}", f"br={args.baud}"] try: run_stm32prog(args.stm32prog, ["-c", *conn, "-w", args.bin, args.address, "-v"]) - except subprocess.CalledProcessError as exc: - return exc.returncode + except Exception as exc: + returncode = getattr(exc, "returncode", None) + if isinstance(returncode, int): + return returncode + print(f"STM32_Programmer_CLI failed: {exc}", file=sys.stderr) + return 1 return 0 diff --git a/scripts/uart_bootloader_enter.py b/scripts/uart_bootloader_enter.py index 4757a6be..efd64b9a 100755 --- a/scripts/uart_bootloader_enter.py +++ b/scripts/uart_bootloader_enter.py @@ -1,28 +1,16 @@ #!/usr/bin/env python3 import argparse -import glob import os import sys import time +from bootloader_script_utils import detect_port, normalize_port_for_pyserial, port_help + BOOTLOADER_PACKET = b"ESBLT_BOOT\n" BOOTLOADER_BYTE_DELAY_SECONDS = 0.005 -def detect_port() -> str | None: - patterns = ( - "/dev/cu.usbserial*", - "/dev/cu.SLAB_USBtoUART*", - "/dev/ttyUSB*", - ) - for pattern in patterns: - ports = sorted(glob.glob(pattern)) - if ports: - return ports[0] - return None - - def send_packet_termios(port: str, baud: int, delay: float, byte_delay: float) -> None: import termios @@ -57,6 +45,8 @@ def send_packet(port: str, baud: int, timeout: float, delay: float, byte_delay: try: import serial except ImportError: + if sys.platform == "win32": + raise RuntimeError("pyserial is required to send the bootloader packet on Windows") send_packet_termios(port, baud, delay, byte_delay) return @@ -86,11 +76,12 @@ def main() -> int: port = args.port or detect_port() if port is None: - print("No serial port found. Pass --port /dev/cu... or /dev/ttyUSB...", file=sys.stderr) + print(port_help(), file=sys.stderr) return 1 try: - send_packet(port, args.baud, args.timeout, args.delay, args.byte_delay) + serial_port = normalize_port_for_pyserial(port) + send_packet(serial_port, args.baud, args.timeout, args.delay, args.byte_delay) except Exception as exc: print(f"Failed to send bootloader packet: {exc}", file=sys.stderr) return 1 diff --git a/scripts/uart_bootloader_flash.py b/scripts/uart_bootloader_flash.py index 447a5be4..295c41ad 100755 --- a/scripts/uart_bootloader_flash.py +++ b/scripts/uart_bootloader_flash.py @@ -1,53 +1,28 @@ #!/usr/bin/env python3 import argparse -import glob import os -import shutil import sys -import subprocess import time +from bootloader_script_utils import ( + default_binary, + default_stm32prog, + detect_port, + normalize_port_for_pyserial, + normalize_port_for_stm32prog, + port_help, + run_stm32prog, + validate_file, +) APP_BASE = "0x08010000" -DEFAULT_APP_BIN = "build/app/stm32g473xx.bin" +DEFAULT_APP_BIN_GLOB = "build/app/stm*.bin" BOOTLOADER_PACKET = b"ESBLT_BOOT\n" BOOTLOADER_BYTE_DELAY_SECONDS = 0.005 -def default_stm32prog() -> str: - env_path = os.environ.get("STM32_PROGRAMMER_CLI") - if env_path: - return env_path - - path_match = shutil.which("STM32_Programmer_CLI") - if path_match: - return path_match - - if sys.platform == "darwin": - return ( - "/Applications/STMicroelectronics/STM32Cube/STM32CubeProgrammer/" - "STM32CubeProgrammer.app/Contents/Resources/bin/STM32_Programmer_CLI" - ) - return os.path.expanduser("~/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin/STM32_Programmer_CLI") - - -def run_stm32prog(stm32prog: str, args: list[str]) -> None: - cmd = [stm32prog] + args - print("+ " + " ".join(cmd)) - subprocess.run(cmd, check=True) - - -def detect_port() -> str | None: - patterns = ( - "/dev/cu.usbserial*", - "/dev/cu.SLAB_USBtoUART*", - "/dev/ttyUSB*", - ) - for pattern in patterns: - ports = sorted(glob.glob(pattern)) - if ports: - return ports[0] - return None +def default_app_bin() -> str: + return default_binary(DEFAULT_APP_BIN_GLOB) def request_bootloader_termios(port: str, baud: int, byte_delay: float) -> None: @@ -84,6 +59,8 @@ def request_bootloader(port: str, baud: int, byte_delay: float) -> None: try: import serial except ImportError: + if sys.platform == "win32": + raise RuntimeError("pyserial is required to send the bootloader packet on Windows") request_bootloader_termios(port, baud, byte_delay) return @@ -100,7 +77,7 @@ def main() -> int: parser = argparse.ArgumentParser(description="Flash app via ES_BLT AN3155-compatible UART bootloader") parser.add_argument("--port", help="Serial device. If omitted, common CP210x paths are autodetected.") parser.add_argument("--baud", default=115200, type=int, help="UART baud rate") - parser.add_argument("--bin", default=DEFAULT_APP_BIN, help=f"App binary to flash (default: {DEFAULT_APP_BIN})") + parser.add_argument("--bin", default=default_app_bin(), help="App binary to flash") parser.add_argument("--address", default=APP_BASE, help=f"Flash address (default: {APP_BASE})") parser.add_argument("--enter", action="store_true", help="Send ES_BLT magic packet before flashing") parser.add_argument("--boot", action="store_true", help="Jump to app after flashing") @@ -115,28 +92,28 @@ def main() -> int: port = args.port or detect_port() if port is None: - print("No serial port found. Pass --port /dev/cu... or /dev/ttyUSB...", file=sys.stderr) + print(port_help(), file=sys.stderr) return 1 - if not os.path.isfile(args.bin): - print(f"Binary does not exist: {args.bin}") + if not validate_file(args.bin, "Binary"): return 1 - if not os.path.isfile(args.stm32prog): - print(f"STM32_Programmer_CLI does not exist: {args.stm32prog}") - return 1 - if not os.access(args.stm32prog, os.X_OK): - print(f"STM32_Programmer_CLI is not executable: {args.stm32prog}") + if not validate_file(args.stm32prog, "STM32_Programmer_CLI", executable=True): return 1 - conn = [f"port={port}", f"br={args.baud}"] + stm32prog_port = normalize_port_for_stm32prog(port, args.stm32prog) + conn = [f"port={stm32prog_port}", f"br={args.baud}"] try: if args.enter: - request_bootloader(port, args.baud, args.byte_delay) + request_bootloader(normalize_port_for_pyserial(port), args.baud, args.byte_delay) time.sleep(0.5) run_stm32prog(args.stm32prog, ["-c", *conn, "-w", args.bin, args.address, "-v"]) if args.boot: run_stm32prog(args.stm32prog, ["-c", *conn, "-g", args.address]) - except subprocess.CalledProcessError as exc: - return exc.returncode + except Exception as exc: + returncode = getattr(exc, "returncode", None) + if isinstance(returncode, int): + return returncode + print(f"UART bootloader flash failed: {exc}", file=sys.stderr) + return 1 return 0 From f7b6ff66f480042f1e0b7a239e88994ca076e170 Mon Sep 17 00:00:00 2001 From: Parthiv Shah Date: Wed, 29 Apr 2026 19:28:32 -0500 Subject: [PATCH 06/12] psom lsom verified --- bootloader/Inc/bootloader_config.h | 7 +- bootloader/Src/bootloader_indicator.c | 2 +- bootloader/Src/bootloader_main.c | 39 ++ bootloader/Src/bootloader_runtime.c | 3 + test/tests/bootloader_test.c | 597 ++++++++++++++++++++++++++ 5 files changed, 645 insertions(+), 3 deletions(-) create mode 100644 test/tests/bootloader_test.c diff --git a/bootloader/Inc/bootloader_config.h b/bootloader/Inc/bootloader_config.h index 4f493ac6..793a7b62 100644 --- a/bootloader/Inc/bootloader_config.h +++ b/bootloader/Inc/bootloader_config.h @@ -16,7 +16,10 @@ #endif #ifndef BOOTLOADER_UART_INSTANCE -#if defined(USART3) +/* PSOM L431: same console USART as test UART apps (husart1 / PA9, PA10). */ +#if defined(STM32L431xx) && defined(USART1) +#define BOOTLOADER_UART_INSTANCE USART1 +#elif defined(USART3) #define BOOTLOADER_UART_INSTANCE USART3 #elif defined(USART2) #define BOOTLOADER_UART_INSTANCE USART2 @@ -36,7 +39,7 @@ #endif #ifndef BOOTLOADER_APP_STARTUP_WAIT_MS -#define BOOTLOADER_APP_STARTUP_WAIT_MS (3000U) +#define BOOTLOADER_APP_STARTUP_WAIT_MS (0U) #endif #ifndef BOOTLOADER_POST_FLASH_BOOT_DELAY_MS diff --git a/bootloader/Src/bootloader_indicator.c b/bootloader/Src/bootloader_indicator.c index e37be91d..2215baa9 100644 --- a/bootloader/Src/bootloader_indicator.c +++ b/bootloader/Src/bootloader_indicator.c @@ -34,7 +34,7 @@ void bootloader_indicator_update(uint32_t tick_ms) { if (s_mode == BOOTLOADER_INDICATOR_CONNECTED) { led_on = true; } else if (s_mode == BOOTLOADER_INDICATOR_FLASHING) { - led_on = (tick_ms % 600U) < 300U; + led_on = (tick_ms % 200U) < 100U; } else if (s_mode == BOOTLOADER_INDICATOR_APP_PRESENT) { uint32_t phase = tick_ms % 1000U; led_on = (phase < 100U) || ((phase >= 200U) && (phase < 300U)); diff --git a/bootloader/Src/bootloader_main.c b/bootloader/Src/bootloader_main.c index 7b98d5c4..5cce7264 100644 --- a/bootloader/Src/bootloader_main.c +++ b/bootloader/Src/bootloader_main.c @@ -517,3 +517,42 @@ int main(void) { (void)bl_dispatch_cmd(cmd); } } + +#if defined(STM32L431xx) +/* + * PSOM L431 only (validated board): HSE 8 MHz -> 80 MHz; same tree as test/tests/can_test.c. + * Overrides weak SystemClock_Config in stm32l4xx_hal_init.c for this image. + */ +void SystemClock_Config(void) { + RCC_OscInitTypeDef RCC_OscInitStruct = {0}; + RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; + + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) { + Error_Handler(); + } + + RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; + RCC_OscInitStruct.HSEState = RCC_HSE_ON; + RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; + RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; + RCC_OscInitStruct.PLL.PLLM = 1; + RCC_OscInitStruct.PLL.PLLN = 20; + RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; + RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; + RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; + if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { + Error_Handler(); + } + + RCC_ClkInitStruct.ClockType = + RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; + RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; + RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; + RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; + + if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) { + Error_Handler(); + } +} +#endif diff --git a/bootloader/Src/bootloader_runtime.c b/bootloader/Src/bootloader_runtime.c index ba695904..495733ad 100644 --- a/bootloader/Src/bootloader_runtime.c +++ b/bootloader/Src/bootloader_runtime.c @@ -2,6 +2,7 @@ #include "bootloader_config.h" #include "bootloader_hal.h" +#include "bootloader_indicator.h" static UART_HandleTypeDef s_huart = { .Instance = BOOTLOADER_UART_INSTANCE, @@ -144,6 +145,7 @@ bool bootloader_runtime_erase_app(void) { bool ok = true; while (erase_addr < erase_end) { + bootloader_indicator_update(HAL_GetTick()); uint32_t bank = bootloader_addr_to_bank(erase_addr); uint32_t segment_end = erase_end; @@ -187,6 +189,7 @@ bool bootloader_runtime_write_app(uint32_t app_offset, const uint8_t *data, size HAL_FLASH_Unlock(); for (size_t i = 0; i < len; i += 8U) { + bootloader_indicator_update(HAL_GetTick()); uint64_t dword = 0xFFFFFFFFFFFFFFFFULL; size_t chunk = ((len - i) >= 8U) ? 8U : (len - i); bootloader_copy_bytes((uint8_t *)&dword, &data[i], chunk); diff --git a/test/tests/bootloader_test.c b/test/tests/bootloader_test.c new file mode 100644 index 00000000..bf1c6ed4 --- /dev/null +++ b/test/tests/bootloader_test.c @@ -0,0 +1,597 @@ +/* + * Resident-bootloader integration test: soft LED breathe, periodic UART line, + * and classic CAN / CAN-FD (classic frame) on standard ID 0. UART task still + * accepts the ES_BLT magic command to reboot into the bootloader. + */ +#include "stm32xx_hal.h" +#include "UART.h" +#include "uart_bootloader.h" + +#if defined(FDCAN1) +#include "CAN_FD.h" +#elif defined(CAN1) +#include "CAN.h" +#endif + +#include +#include + +#if defined(FDCAN1) || defined(FDCAN2) || defined(FDCAN3) +#define FDCAN_NVIC_PRIO (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY + 3) +#endif + +#if defined(STM32L432xx) +#define LED_PIN GPIO_PIN_3 +#define LED_PORT GPIOB +#elif defined(STM32L431xx) +#define LED_PIN GPIO_PIN_11 +#define LED_PORT GPIOB +#elif defined(STM32G473xx) +#define LED_PIN GPIO_PIN_3 +#define LED_PORT GPIOC +#else +#define LED_PIN GPIO_PIN_5 +#define LED_PORT GPIOA +#endif + +#if defined(STM32L431xx) && defined(USART1) +#define BOOT_COMMAND_UART husart1 +#elif defined(USART3) +#define BOOT_COMMAND_UART husart3 +#elif defined(USART2) +#define BOOT_COMMAND_UART husart2 +#elif defined(USART1) +#define BOOT_COMMAND_UART husart1 +#else +#error "No UART available for bootloader command test." +#endif + +#define SPAM_PERIOD_MS (400U) + +#ifndef BOOTLOADER_TEST_UART_SPAM_LINESIZE +#define BOOTLOADER_TEST_UART_SPAM_LINESIZE (72U) +#endif + +static StaticTask_t s_breathe_task_buffer; +static StaticTask_t s_spam_task_buffer; +static StaticTask_t s_uart_task_buffer; +static StackType_t s_breathe_stack[configMINIMAL_STACK_SIZE + 24U]; +static StackType_t s_spam_stack[configMINIMAL_STACK_SIZE + 48U]; +static StackType_t s_uart_stack[configMINIMAL_STACK_SIZE]; + +#if defined(STM32G474xx) +/* Same clock tree as test/tests/can_fd_test.c (80 MHz class, HSI PLL). */ +static void G474_SystemClockConfig(void) { + RCC_OscInitTypeDef RCC_OscInitStruct = {0}; + RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; + + HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1); + + RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; + RCC_OscInitStruct.HSIState = RCC_HSI_ON; + RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; + RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; + RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI; + RCC_OscInitStruct.PLL.PLLM = RCC_PLLM_DIV4; + RCC_OscInitStruct.PLL.PLLN = 40; + RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; + RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; + RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; + if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { + Error_Handler(); + } + + RCC_ClkInitStruct.ClockType = + RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; + RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; + RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; + RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; + + if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) { + Error_Handler(); + } +} +#endif + +#if defined(STM32G473xx) +static void G473_SystemClockConfig(void) { + RCC_OscInitTypeDef RCC_OscInitStruct = {0}; + RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; + + HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1); + + RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; + RCC_OscInitStruct.HSIState = RCC_HSI_ON; + RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; + RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; + RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI; + RCC_OscInitStruct.PLL.PLLM = RCC_PLLM_DIV1; + RCC_OscInitStruct.PLL.PLLN = 10; + RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; + RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; + RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; + if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { + Error_Handler(); + } + + RCC_ClkInitStruct.ClockType = + RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; + RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; + RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; + RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; + + if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) { + Error_Handler(); + } +} +#endif + +static void heartbeat_clock_init(void) { + switch ((uint32_t)LED_PORT) { + case (uint32_t)GPIOA: + __HAL_RCC_GPIOA_CLK_ENABLE(); + break; + case (uint32_t)GPIOB: + __HAL_RCC_GPIOB_CLK_ENABLE(); + break; + case (uint32_t)GPIOC: + __HAL_RCC_GPIOC_CLK_ENABLE(); + break; + case (uint32_t)GPIOD: + __HAL_RCC_GPIOD_CLK_ENABLE(); + break; + default: + break; + } +} + +static void led_init(void) { + GPIO_InitTypeDef led_config = { + .Mode = GPIO_MODE_OUTPUT_PP, + .Pull = GPIO_NOPULL, + .Pin = LED_PIN, + }; + + heartbeat_clock_init(); + HAL_GPIO_Init(LED_PORT, &led_config); +} + +static void boot_command_uart_init(void) { + BOOT_COMMAND_UART->Init.BaudRate = 115200; + BOOT_COMMAND_UART->Init.WordLength = UART_WORDLENGTH_8B; + BOOT_COMMAND_UART->Init.StopBits = UART_STOPBITS_1; + BOOT_COMMAND_UART->Init.Parity = UART_PARITY_NONE; + BOOT_COMMAND_UART->Init.Mode = UART_MODE_TX_RX; + BOOT_COMMAND_UART->Init.HwFlowCtl = UART_HWCONTROL_NONE; + BOOT_COMMAND_UART->Init.OverSampling = UART_OVERSAMPLING_16; +#if defined(STM32L4xx) || defined(STM32G4xx) + BOOT_COMMAND_UART->Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; +#endif + BOOT_COMMAND_UART->AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; + + if (uart_init(BOOT_COMMAND_UART) != UART_OK) { + Error_Handler(); + } +} + +/* Slow triangle PWM using GPIO and FreeRTOS delays (~2.5 s per full cycle). */ +static void led_breathe_task(void *argument) { + (void)argument; + + for (;;) { + for (uint32_t t = 0; t < 100U; t++) { + uint32_t br = (t < 50U) ? t : (99U - t); + uint32_t on_ms = (br * 18U) / 50U + 1U; + HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET); + vTaskDelay(pdMS_TO_TICKS(on_ms)); + HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET); + uint32_t off_ms = (21U > on_ms) ? (21U - on_ms) : 1U; + vTaskDelay(pdMS_TO_TICKS(off_ms)); + } + } +} + +#if defined(FDCAN1) +static can_status_t boottest_fdcan_init(void) { + hfdcan1->Instance = FDCAN1; + hfdcan1->Init.ClockDivider = FDCAN_CLOCK_DIV1; + hfdcan1->Init.FrameFormat = FDCAN_FRAME_CLASSIC; + hfdcan1->Init.Mode = FDCAN_MODE_NORMAL; + hfdcan1->Init.AutoRetransmission = ENABLE; + hfdcan1->Init.TransmitPause = DISABLE; + hfdcan1->Init.ProtocolException = DISABLE; + hfdcan1->Init.NominalPrescaler = 20; + hfdcan1->Init.NominalSyncJumpWidth = 1; + hfdcan1->Init.NominalTimeSeg1 = 13; + hfdcan1->Init.NominalTimeSeg2 = 2; + hfdcan1->Init.DataPrescaler = 1; + hfdcan1->Init.DataSyncJumpWidth = 1; + hfdcan1->Init.DataTimeSeg1 = 1; + hfdcan1->Init.DataTimeSeg2 = 1; + hfdcan1->Init.StdFiltersNbr = 1; + hfdcan1->Init.ExtFiltersNbr = 0; + hfdcan1->Init.TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION; + + FDCAN_FilterTypeDef sFilterConfig = {0}; + sFilterConfig.IdType = FDCAN_STANDARD_ID; + sFilterConfig.FilterIndex = 0; + sFilterConfig.FilterType = FDCAN_FILTER_MASK; + sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0; + sFilterConfig.FilterID1 = 0x000; + sFilterConfig.FilterID2 = 0x000; + + if (can_fd_init(hfdcan1, &sFilterConfig) != CAN_OK) { + return CAN_ERR; + } + return can_fd_start(hfdcan1); +} +#endif + +#if defined(CAN1) +/* Filter + bit timing match test/tests/can_test.c (PSOM / L431); NORMAL for bus traffic. */ +static can_status_t boottest_can_init(void) { + CAN_FilterTypeDef sFilterConfig = {0}; + sFilterConfig.FilterBank = 0; + sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; + sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; + sFilterConfig.FilterIdHigh = 0x0000; + sFilterConfig.FilterIdLow = 0x0000; + sFilterConfig.FilterMaskIdHigh = 0x0000; + sFilterConfig.FilterMaskIdLow = 0x0000; + sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; + sFilterConfig.FilterActivation = ENABLE; + sFilterConfig.SlaveStartFilterBank = 14; + + hcan1->Init.Prescaler = 20; + hcan1->Init.SyncJumpWidth = CAN_SJW_1TQ; + hcan1->Init.TimeSeg1 = CAN_BS1_13TQ; + hcan1->Init.TimeSeg2 = CAN_BS2_2TQ; + hcan1->Init.Mode = CAN_MODE_NORMAL; + hcan1->Init.TimeTriggeredMode = DISABLE; + hcan1->Init.AutoBusOff = ENABLE; + hcan1->Init.AutoWakeUp = DISABLE; + hcan1->Init.AutoRetransmission = ENABLE; + hcan1->Init.ReceiveFifoLocked = DISABLE; + hcan1->Init.TransmitFifoPriority = ENABLE; + + if (can_init(hcan1, &sFilterConfig) != CAN_OK) { + return CAN_ERR; + } + return can_start(hcan1); +} +#endif + +static void spam_task(void *argument) { + (void)argument; + + uint32_t seq = 0U; + char line[BOOTLOADER_TEST_UART_SPAM_LINESIZE]; + + for (;;) { + int n = snprintf(line, sizeof(line), "ES_BLT_BOOTLOADER_TEST seq=%lu\r\n", (unsigned long)seq); + if (n > 0 && n < (int)sizeof(line)) { + (void)uart_send(BOOT_COMMAND_UART, (const uint8_t *)line, (uint16_t)n, pdMS_TO_TICKS(50)); + } + seq++; + +#if defined(FDCAN1) + { + uint8_t payload[8]; + memset(payload, 0, sizeof(payload)); + payload[0] = (uint8_t)(seq & 0xFFU); + payload[1] = (uint8_t)((seq >> 8) & 0xFFU); + + FDCAN_TxHeaderTypeDef tx_header = {0}; + tx_header.Identifier = 0U; + tx_header.IdType = FDCAN_STANDARD_ID; + tx_header.TxFrameType = FDCAN_DATA_FRAME; + tx_header.DataLength = FDCAN_DLC_BYTES_8; + tx_header.ErrorStateIndicator = FDCAN_ESI_ACTIVE; + tx_header.BitRateSwitch = FDCAN_BRS_OFF; + tx_header.FDFormat = FDCAN_CLASSIC_CAN; + tx_header.TxEventFifoControl = FDCAN_STORE_TX_EVENTS; + tx_header.MessageMarker = 0; + + (void)can_fd_send(hfdcan1, &tx_header, payload, pdMS_TO_TICKS(50)); + } +#elif defined(CAN1) + { + uint8_t payload[8]; + memset(payload, 0, sizeof(payload)); + payload[0] = (uint8_t)(seq & 0xFFU); + payload[1] = (uint8_t)((seq >> 8) & 0xFFU); + + CAN_TxHeaderTypeDef tx_header = {0}; + tx_header.StdId = 0U; + tx_header.RTR = CAN_RTR_DATA; + tx_header.IDE = CAN_ID_STD; + tx_header.DLC = 8U; + tx_header.TransmitGlobalTime = DISABLE; + + (void)can_send(hcan1, &tx_header, payload, pdMS_TO_TICKS(50)); + } +#endif + + vTaskDelay(pdMS_TO_TICKS(SPAM_PERIOD_MS)); + } +} + +static void uart_task(void *argument) { + (void)argument; + + for (;;) { + (void)uart_bootloader_service(BOOT_COMMAND_UART, portMAX_DELAY); + } +} + +int main(void) { + uart_bootloader_init_app_vector_table(); + HAL_Init(); + + __HAL_RCC_SYSCFG_CLK_ENABLE(); + __HAL_RCC_PWR_CLK_ENABLE(); + +#if defined(STM32G474xx) + G474_SystemClockConfig(); +#elif defined(STM32G473xx) + G473_SystemClockConfig(); +#else + /* L431: strong PSOM SystemClock_Config() below; other targets: weak stm32*xx_hal_init.c */ + SystemClock_Config(); +#endif + + led_init(); + boot_command_uart_init(); + +#if defined(FDCAN1) + if (boottest_fdcan_init() != CAN_OK) { + Error_Handler(); + } +#elif defined(CAN1) + if (boottest_can_init() != CAN_OK) { + Error_Handler(); + } +#endif + + xTaskCreateStatic(led_breathe_task, + "BREATHE", + sizeof(s_breathe_stack) / sizeof(StackType_t), + NULL, + tskIDLE_PRIORITY + 1, + s_breathe_stack, + &s_breathe_task_buffer); + + xTaskCreateStatic(spam_task, + "SPAM", + sizeof(s_spam_stack) / sizeof(StackType_t), + NULL, + tskIDLE_PRIORITY + 1, + s_spam_stack, + &s_spam_task_buffer); + + xTaskCreateStatic(uart_task, + "BOOTUART", + sizeof(s_uart_stack) / sizeof(StackType_t), + NULL, + tskIDLE_PRIORITY + 2, + s_uart_stack, + &s_uart_task_buffer); + + vTaskStartScheduler(); + + while (1) { + } +} + +#if defined(FDCAN1) || defined(FDCAN2) || defined(FDCAN3) + +static uint32_t HAL_RCC_FDCAN_CLK_ENABLED = 0; + +void HAL_FDCAN_MspInit(FDCAN_HandleTypeDef *fdcanHandle) { + GPIO_InitTypeDef GPIO_InitStruct = {0}; + RCC_PeriphCLKInitTypeDef PeriphClkInit = {0}; + + if (0) { + } +#ifdef FDCAN1 + else if (fdcanHandle->Instance == FDCAN1) { + PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_FDCAN; + PeriphClkInit.FdcanClockSelection = RCC_FDCANCLKSOURCE_PCLK1; + if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) { + Error_Handler(); + } + + HAL_RCC_FDCAN_CLK_ENABLED++; + if (HAL_RCC_FDCAN_CLK_ENABLED == 1) { + __HAL_RCC_FDCAN_CLK_ENABLE(); + } + + __HAL_RCC_GPIOA_CLK_ENABLE(); + GPIO_InitStruct.Pin = GPIO_PIN_11 | GPIO_PIN_12; + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; + GPIO_InitStruct.Pull = GPIO_NOPULL; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + GPIO_InitStruct.Alternate = GPIO_AF9_FDCAN1; + HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); + + HAL_NVIC_SetPriority(FDCAN1_IT0_IRQn, FDCAN_NVIC_PRIO, 0); + HAL_NVIC_EnableIRQ(FDCAN1_IT0_IRQn); + HAL_NVIC_SetPriority(FDCAN1_IT1_IRQn, FDCAN_NVIC_PRIO, 0); + HAL_NVIC_EnableIRQ(FDCAN1_IT1_IRQn); + } +#endif +#ifdef FDCAN2 + else if (fdcanHandle->Instance == FDCAN2) { + PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_FDCAN; + PeriphClkInit.FdcanClockSelection = RCC_FDCANCLKSOURCE_PCLK1; + if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) { + Error_Handler(); + } + + HAL_RCC_FDCAN_CLK_ENABLED++; + if (HAL_RCC_FDCAN_CLK_ENABLED == 1) { + __HAL_RCC_FDCAN_CLK_ENABLE(); + } + + __HAL_RCC_GPIOB_CLK_ENABLE(); + GPIO_InitStruct.Pin = GPIO_PIN_12 | GPIO_PIN_13; + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; + GPIO_InitStruct.Pull = GPIO_NOPULL; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + GPIO_InitStruct.Alternate = GPIO_AF9_FDCAN2; + HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); + + HAL_NVIC_SetPriority(FDCAN2_IT0_IRQn, FDCAN_NVIC_PRIO, 0); + HAL_NVIC_EnableIRQ(FDCAN2_IT0_IRQn); + HAL_NVIC_SetPriority(FDCAN2_IT1_IRQn, FDCAN_NVIC_PRIO, 0); + HAL_NVIC_EnableIRQ(FDCAN2_IT1_IRQn); + } +#endif +#ifdef FDCAN3 + else if (fdcanHandle->Instance == FDCAN3) { + PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_FDCAN; + PeriphClkInit.FdcanClockSelection = RCC_FDCANCLKSOURCE_PCLK1; + if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) { + Error_Handler(); + } + + HAL_RCC_FDCAN_CLK_ENABLED++; + if (HAL_RCC_FDCAN_CLK_ENABLED == 1) { + __HAL_RCC_FDCAN_CLK_ENABLE(); + } + + __HAL_RCC_GPIOA_CLK_ENABLE(); + GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_15; + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; + GPIO_InitStruct.Pull = GPIO_NOPULL; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + GPIO_InitStruct.Alternate = GPIO_AF11_FDCAN3; + HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); + + HAL_NVIC_SetPriority(FDCAN3_IT0_IRQn, FDCAN_NVIC_PRIO, 0); + HAL_NVIC_EnableIRQ(FDCAN3_IT0_IRQn); + HAL_NVIC_SetPriority(FDCAN3_IT1_IRQn, FDCAN_NVIC_PRIO, 0); + HAL_NVIC_EnableIRQ(FDCAN3_IT1_IRQn); + } +#endif +} + +void HAL_FDCAN_MspDeInit(FDCAN_HandleTypeDef *fdcanHandle) { +#ifdef FDCAN1 + if (fdcanHandle->Instance == FDCAN1) { + HAL_RCC_FDCAN_CLK_ENABLED--; + if (HAL_RCC_FDCAN_CLK_ENABLED == 0) { + __HAL_RCC_FDCAN_CLK_DISABLE(); + } + HAL_GPIO_DeInit(GPIOA, GPIO_PIN_11 | GPIO_PIN_12); + HAL_NVIC_DisableIRQ(FDCAN1_IT0_IRQn); + HAL_NVIC_DisableIRQ(FDCAN1_IT1_IRQn); + return; + } +#endif +#ifdef FDCAN2 + if (fdcanHandle->Instance == FDCAN2) { + HAL_RCC_FDCAN_CLK_ENABLED--; + if (HAL_RCC_FDCAN_CLK_ENABLED == 0) { + __HAL_RCC_FDCAN_CLK_DISABLE(); + } + HAL_GPIO_DeInit(GPIOB, GPIO_PIN_12 | GPIO_PIN_13); + HAL_NVIC_DisableIRQ(FDCAN2_IT0_IRQn); + HAL_NVIC_DisableIRQ(FDCAN2_IT1_IRQn); + return; + } +#endif +#ifdef FDCAN3 + if (fdcanHandle->Instance == FDCAN3) { + HAL_RCC_FDCAN_CLK_ENABLED--; + if (HAL_RCC_FDCAN_CLK_ENABLED == 0) { + __HAL_RCC_FDCAN_CLK_DISABLE(); + } + HAL_GPIO_DeInit(GPIOA, GPIO_PIN_8 | GPIO_PIN_15); + HAL_NVIC_DisableIRQ(FDCAN3_IT0_IRQn); + HAL_NVIC_DisableIRQ(FDCAN3_IT1_IRQn); + } +#endif +} + +void can_fd_error_callback_hook(FDCAN_HandleTypeDef *hfdcan, uint32_t ErrorStatusITs) { + (void)hfdcan; + (void)ErrorStatusITs; + HAL_GPIO_TogglePin(LED_PORT, LED_PIN); +} + +#endif /* FDCAN1 || FDCAN2 || FDCAN3 */ + +#if defined(STM32L431xx) && defined(CAN1) + +void HAL_CAN_MspInit(CAN_HandleTypeDef *hcan) { + GPIO_InitTypeDef GPIO_InitStruct = {0}; + if (hcan->Instance == CAN1) { + __HAL_RCC_CAN1_CLK_ENABLE(); + + __HAL_RCC_GPIOB_CLK_ENABLE(); + GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9; + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; + GPIO_InitStruct.Pull = GPIO_NOPULL; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + GPIO_InitStruct.Alternate = GPIO_AF9_CAN1; + HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); + + HAL_NVIC_SetPriority(CAN1_TX_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY, 0); + HAL_NVIC_EnableIRQ(CAN1_TX_IRQn); + HAL_NVIC_SetPriority(CAN1_RX0_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY, 0); + HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn); + } +} + +void HAL_CAN_MspDeInit(CAN_HandleTypeDef *hcan) { + if (hcan->Instance == CAN1) { + __HAL_RCC_CAN1_CLK_DISABLE(); + HAL_GPIO_DeInit(GPIOB, GPIO_PIN_8 | GPIO_PIN_9); + HAL_NVIC_DisableIRQ(CAN1_TX_IRQn); + HAL_NVIC_DisableIRQ(CAN1_RX0_IRQn); + } +} + +#endif /* STM32L431xx && CAN1 */ + +#if defined(STM32L431xx) +/* + * Peripheral SOM L431: HSE -> 80 MHz (matches test/tests/can_test.c). + * Strong definition overrides weak SystemClock_Config in stm32l4xx_hal_init.c for this image. + */ +void SystemClock_Config(void) { + RCC_OscInitTypeDef RCC_OscInitStruct = {0}; + RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; + + if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) { + Error_Handler(); + } + + RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; + RCC_OscInitStruct.HSEState = RCC_HSE_ON; + RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; + RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; + RCC_OscInitStruct.PLL.PLLM = 1; + RCC_OscInitStruct.PLL.PLLN = 20; + RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; + RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; + RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; + if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { + Error_Handler(); + } + + RCC_ClkInitStruct.ClockType = + RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; + RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; + RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; + RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; + + if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) { + Error_Handler(); + } +} +#endif From 1f96f2f046e7f326a1176e9556ace01ef1d2b735 Mon Sep 17 00:00:00 2001 From: Parthiv Shah Date: Wed, 29 Apr 2026 19:34:33 -0500 Subject: [PATCH 07/12] gemini fixes untested --- bootloader/Src/bootloader_main.c | 14 ++++- bootloader/Src/bootloader_runtime.c | 97 ++++++++++++++++++++++++++++- docs/UartBootloader.md | 10 +++ test/tests/blinky_bootloader_test.c | 3 + test/tests/bootloader_test.c | 3 + 5 files changed, 125 insertions(+), 2 deletions(-) diff --git a/bootloader/Src/bootloader_main.c b/bootloader/Src/bootloader_main.c index 5cce7264..be4a317a 100644 --- a/bootloader/Src/bootloader_main.c +++ b/bootloader/Src/bootloader_main.c @@ -9,6 +9,15 @@ #include void SystemClock_Config(void); +void Error_Handler(void); + +/* AN3155 command bytes: short poll while idle (keeps indicator smooth), longer wait for 2nd byte. */ +#ifndef BL_CMD_POLL_IDLE_TIMEOUT_MS +#define BL_CMD_POLL_IDLE_TIMEOUT_MS (20U) +#endif +#ifndef BL_CMD_FOLLOW_BYTE_TIMEOUT_MS +#define BL_CMD_FOLLOW_BYTE_TIMEOUT_MS (1000U) +#endif #define BL_SOF ((uint8_t)0x5AU) #define BL_ACK ((uint8_t)0x79U) @@ -110,7 +119,10 @@ static void bl_send_nack(void) { static bool bl_read_cmd(uint8_t *cmd) { uint8_t raw[2]; - if (!bootloader_runtime_read_bytes(raw, sizeof(raw), 1000U)) { + if (!bootloader_runtime_read_bytes(&raw[0], 1U, BL_CMD_POLL_IDLE_TIMEOUT_MS)) { + return false; + } + if (!bootloader_runtime_read_bytes(&raw[1], 1U, BL_CMD_FOLLOW_BYTE_TIMEOUT_MS)) { return false; } if ((uint8_t)(raw[0] ^ raw[1]) != 0xFFU) { diff --git a/bootloader/Src/bootloader_runtime.c b/bootloader/Src/bootloader_runtime.c index 495733ad..39180c89 100644 --- a/bootloader/Src/bootloader_runtime.c +++ b/bootloader/Src/bootloader_runtime.c @@ -108,6 +108,90 @@ static uint32_t bootloader_app_end_addr(void) { return BOOTLOADER_APP_BASE + BOOTLOADER_APP_MAX_SIZE; } +#if defined(STM32F446xx) || (defined(STM32F401xx) && (FLASH_END == 0x0807FFFFUL)) + +/* 512 KiB single-bank layout: 4×16 KiB + 64 KiB + 3×128 KiB (STM32F446xx, STM32F401xE). */ + +static uint32_t bootloader_f4_sector_of_addr(uint32_t addr) { + uint32_t off = addr - FLASH_BASE; + if (off < 0x10000U) { + return off >> 14; + } + if (off < 0x20000U) { + return (uint32_t)FLASH_SECTOR_4; + } + off -= 0x20000U; + return (uint32_t)FLASH_SECTOR_5 + (off >> 17); +} + +static uint32_t bootloader_f4_next_addr_after_sector(uint32_t sector) { + if (sector <= (uint32_t)FLASH_SECTOR_3) { + return FLASH_BASE + ((sector + 1U) << 14); + } + if (sector == (uint32_t)FLASH_SECTOR_4) { + return FLASH_BASE + 0x20000U; + } + return FLASH_BASE + 0x20000U + ((sector - (uint32_t)FLASH_SECTOR_5 + 1U) << 17); +} + +bool bootloader_runtime_erase_app(void) { + uint32_t erase_addr = BOOTLOADER_APP_BASE; + uint32_t erase_end = bootloader_app_end_addr(); + + HAL_FLASH_Unlock(); + bool ok = true; + while (erase_addr < erase_end) { + bootloader_indicator_update(HAL_GetTick()); + uint32_t sec = bootloader_f4_sector_of_addr(erase_addr); + FLASH_EraseInitTypeDef erase = {0}; + erase.TypeErase = FLASH_TYPEERASE_SECTORS; + erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; + erase.Sector = sec; + erase.NbSectors = 1U; + erase.Banks = FLASH_BANK_1; + uint32_t sector_error = 0U; + if (HAL_FLASHEx_Erase(&erase, §or_error) != HAL_OK) { + ok = false; + break; + } + erase_addr = bootloader_f4_next_addr_after_sector(sec); + } + HAL_FLASH_Lock(); + return ok; +} + +bool bootloader_runtime_write_app(uint32_t app_offset, const uint8_t *data, size_t len) { + if ((data == NULL) || (app_offset > BOOTLOADER_APP_MAX_SIZE) || + (len > (BOOTLOADER_APP_MAX_SIZE - app_offset))) { + return false; + } + + uint32_t write_addr = BOOTLOADER_APP_BASE + app_offset; + if ((write_addr & 7U) != 0U) { + return false; + } + + HAL_FLASH_Unlock(); + for (size_t i = 0; i < len; i += 8U) { + bootloader_indicator_update(HAL_GetTick()); + uint64_t dword = 0xFFFFFFFFFFFFFFFFULL; + size_t chunk = ((len - i) >= 8U) ? 8U : (len - i); + bootloader_copy_bytes((uint8_t *)&dword, &data[i], chunk); + if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, write_addr + i, dword) != HAL_OK) { + HAL_FLASH_Lock(); + return false; + } + } + HAL_FLASH_Lock(); + return true; +} + +#elif defined(STM32F4xx) + +#error "Resident bootloader flash erase is only implemented for STM32F446xx and STM32F401xE (512 KiB). Add a sector map in bootloader_runtime.c for this F4 part." + +#elif defined(STM32L4xx) || defined(STM32G4xx) + static bool __attribute__((unused)) bootloader_flash_is_dual_bank(void) { #if defined(FLASH_OPTR_DBANK) return READ_BIT(FLASH->OPTR, FLASH_OPTR_DBANK) != 0U; @@ -186,6 +270,10 @@ bool bootloader_runtime_write_app(uint32_t app_offset, const uint8_t *data, size } uint32_t write_addr = BOOTLOADER_APP_BASE + app_offset; + if ((write_addr & 7U) != 0U) { + return false; + } + HAL_FLASH_Unlock(); for (size_t i = 0; i < len; i += 8U) { @@ -203,6 +291,12 @@ bool bootloader_runtime_write_app(uint32_t app_offset, const uint8_t *data, size return true; } +#else + +#error "Unsupported STM32 series for resident bootloader flash (see bootloader_runtime.c)." + +#endif + bool bootloader_runtime_is_app_valid(void) { uint32_t stack = *(volatile uint32_t *)BOOTLOADER_APP_BASE; uint32_t reset = *(volatile uint32_t *)(BOOTLOADER_APP_BASE + 4U); @@ -245,6 +339,7 @@ void bootloader_runtime_jump_to_app(void) { __set_MSP(app_stack); __DSB(); __ISB(); - __enable_irq(); + /* Leave interrupts disabled; the app's startup (as with a cold boot from ROM) + * enables them when SystemInit / HAL / RTOS is ready. */ app_reset_handler(); } diff --git a/docs/UartBootloader.md b/docs/UartBootloader.md index a4d609d1..0a2d2719 100644 --- a/docs/UartBootloader.md +++ b/docs/UartBootloader.md @@ -172,6 +172,12 @@ Apps that run behind the resident bootloader need three pieces: `BOOTLOADER_APP_BASE` during `SystemInit()` for bootloader app builds. - Optionally call `uart_bootloader_init_app_vector_table()` at the start of `main()` as an explicit second set. +- After the resident bootloader jumps to your app, **the CPU is still in the + same state as a ROM boot with interrupts globally disabled** (`PRIMASK` set). + Standard `startup_*.s` / `SystemInit` / `HAL_Init` / RTOS startup will enable + interrupts when your app is ready—same as a normal power-on reset. Do not + assume IRQs are on before that path runs (if you have custom startup, call + `__enable_irq()` when appropriate). - Service the UART bootloader command parser on the same UART used by the bootloader. The resident bootloader chooses `USART3`, then `USART2`, then `USART1` by default, but parent apps can override the resident UART with a @@ -229,6 +235,10 @@ the normal application startup path. ## Flash +Programming uses **64-bit (doubleword) writes** on STM32L4/STM32G4 and the +supported STM32F4 configuration. The host **must** use write addresses that are +**8-byte aligned** to `BOOTLOADER_APP_BASE`; unaligned addresses are rejected. + Standalone firmware uses the normal ST-Link flow and flashes to `0x08000000`: ```bash diff --git a/test/tests/blinky_bootloader_test.c b/test/tests/blinky_bootloader_test.c index 64ac7c87..7363e0d4 100644 --- a/test/tests/blinky_bootloader_test.c +++ b/test/tests/blinky_bootloader_test.c @@ -93,6 +93,9 @@ int main(void) { HAL_Init(); SystemClock_Config(); + /* Resident bootloader jumps with IRQs globally masked; enable before UART / FreeRTOS. */ + __enable_irq(); + led_init(); boot_command_uart_init(); diff --git a/test/tests/bootloader_test.c b/test/tests/bootloader_test.c index bf1c6ed4..bbae82b4 100644 --- a/test/tests/bootloader_test.c +++ b/test/tests/bootloader_test.c @@ -342,6 +342,9 @@ int main(void) { SystemClock_Config(); #endif + /* Resident bootloader jumps with IRQs globally masked; turn them on before UART/CAN ISRs. */ + __enable_irq(); + led_init(); boot_command_uart_init(); From 4189adda1d5fe9386fd203e8cc3c0761cb4e09e0 Mon Sep 17 00:00:00 2001 From: Lakshay983 <56173382+Lakshay983@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:42:40 -0500 Subject: [PATCH 08/12] fix big shah build --- common/Inc/uart_bootloader.h | 5 +---- test/tests/blinky_bootloader_test.c | 18 ++++++++++++++---- test/tests/bootloader_test.c | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/common/Inc/uart_bootloader.h b/common/Inc/uart_bootloader.h index 3d4b8ca6..664b8546 100644 --- a/common/Inc/uart_bootloader.h +++ b/common/Inc/uart_bootloader.h @@ -1,5 +1,4 @@ -#ifndef UART_BOOTLOADER_H_ -#define UART_BOOTLOADER_H_ +#pragma once #include #include @@ -14,5 +13,3 @@ bool uart_bootloader_is_entry_allowed(void); void uart_bootloader_init_app_vector_table(void); void uart_bootloader_request_reset(void); bool uart_bootloader_consume_request(void); - -#endif /* UART_BOOTLOADER_H_ */ diff --git a/test/tests/blinky_bootloader_test.c b/test/tests/blinky_bootloader_test.c index 7363e0d4..8a64bdec 100644 --- a/test/tests/blinky_bootloader_test.c +++ b/test/tests/blinky_bootloader_test.c @@ -32,15 +32,25 @@ static StackType_t s_blinky_task_stack[configMINIMAL_STACK_SIZE]; static StackType_t s_uart_task_stack[configMINIMAL_STACK_SIZE]; static void heartbeat_clock_init(void) { - if (LED_PORT == GPIOA) { + if(0){ + + } + else if (LED_PORT == GPIOA) { __HAL_RCC_GPIOA_CLK_ENABLE(); - } else if (LED_PORT == GPIOB) { + } + + else if (LED_PORT == GPIOB) { __HAL_RCC_GPIOB_CLK_ENABLE(); - } else if (LED_PORT == GPIOC) { + } + else if (LED_PORT == GPIOC) { __HAL_RCC_GPIOC_CLK_ENABLE(); - } else if (LED_PORT == GPIOD) { + } +#ifdef GPIOD + else if (LED_PORT == GPIOD) { __HAL_RCC_GPIOD_CLK_ENABLE(); } +#endif + } static void led_init(void) { diff --git a/test/tests/bootloader_test.c b/test/tests/bootloader_test.c index bbae82b4..6797b5aa 100644 --- a/test/tests/bootloader_test.c +++ b/test/tests/bootloader_test.c @@ -168,8 +168,8 @@ static void boot_command_uart_init(void) { BOOT_COMMAND_UART->Init.OverSampling = UART_OVERSAMPLING_16; #if defined(STM32L4xx) || defined(STM32G4xx) BOOT_COMMAND_UART->Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; -#endif BOOT_COMMAND_UART->AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; +#endif if (uart_init(BOOT_COMMAND_UART) != UART_OK) { Error_Handler(); From 86680ac107b22433bf5cce80eab70398c607169d Mon Sep 17 00:00:00 2001 From: Parthiv Shah Date: Sat, 2 May 2026 12:30:57 -0500 Subject: [PATCH 09/12] gemini fixes working --- bootloader/Inc/bootloader_config.h | 1 + bootloader/Inc/bootloader_indicator.h | 1 - bootloader/Src/bootloader_indicator.c | 2 - bootloader/Src/bootloader_main.c | 40 ++++++------- bootloader/Src/bootloader_runtime.c | 5 -- common/Inc/bootloader_command.h | 54 ++++++++++++++++++ docs/UartBootloader.md | 81 +++++++++++++++++++++++++++ scripts/flash_bootloader.py | 32 ++++++++++- 8 files changed, 187 insertions(+), 29 deletions(-) create mode 100644 common/Inc/bootloader_command.h diff --git a/bootloader/Inc/bootloader_config.h b/bootloader/Inc/bootloader_config.h index 793a7b62..2d906cfd 100644 --- a/bootloader/Inc/bootloader_config.h +++ b/bootloader/Inc/bootloader_config.h @@ -38,6 +38,7 @@ #define BOOTLOADER_HANDSHAKE_TIMEOUT_MS (0U) #endif +/* 0 = skip UART listen and boot app immediately when valid (see bootloader_main). */ #ifndef BOOTLOADER_APP_STARTUP_WAIT_MS #define BOOTLOADER_APP_STARTUP_WAIT_MS (0U) #endif diff --git a/bootloader/Inc/bootloader_indicator.h b/bootloader/Inc/bootloader_indicator.h index 321f5252..841af0e6 100644 --- a/bootloader/Inc/bootloader_indicator.h +++ b/bootloader/Inc/bootloader_indicator.h @@ -7,7 +7,6 @@ typedef enum { BOOTLOADER_INDICATOR_NO_APP = 0, BOOTLOADER_INDICATOR_APP_PRESENT, BOOTLOADER_INDICATOR_CONNECTED, - BOOTLOADER_INDICATOR_FLASHING, BOOTLOADER_INDICATOR_ERROR, } bootloader_indicator_mode_t; diff --git a/bootloader/Src/bootloader_indicator.c b/bootloader/Src/bootloader_indicator.c index 2215baa9..3c51303e 100644 --- a/bootloader/Src/bootloader_indicator.c +++ b/bootloader/Src/bootloader_indicator.c @@ -33,8 +33,6 @@ void bootloader_indicator_update(uint32_t tick_ms) { if (s_mode == BOOTLOADER_INDICATOR_CONNECTED) { led_on = true; - } else if (s_mode == BOOTLOADER_INDICATOR_FLASHING) { - led_on = (tick_ms % 200U) < 100U; } else if (s_mode == BOOTLOADER_INDICATOR_APP_PRESENT) { uint32_t phase = tick_ms % 1000U; led_on = (phase < 100U) || ((phase >= 200U) && (phase < 300U)); diff --git a/bootloader/Src/bootloader_main.c b/bootloader/Src/bootloader_main.c index be4a317a..69e58f4c 100644 --- a/bootloader/Src/bootloader_main.c +++ b/bootloader/Src/bootloader_main.c @@ -284,10 +284,8 @@ static bool bl_handle_write_memory(void) { return false; } - bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_FLASHING); - bootloader_indicator_update(HAL_GetTick()); - bool ok = bootloader_runtime_write_app(addr - bl_app_start(), data, data_len); bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_CONNECTED); + bool ok = bootloader_runtime_write_app(addr - bl_app_start(), data, data_len); if (!ok) { bl_send_nack(); return false; @@ -314,10 +312,8 @@ static bool bl_handle_erase(void) { bl_send_nack(); return false; } - bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_FLASHING); - bootloader_indicator_update(HAL_GetTick()); - bool ok = bootloader_runtime_erase_app(); bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_CONNECTED); + bool ok = bootloader_runtime_erase_app(); if (!ok) { bl_send_nack(); return false; @@ -359,10 +355,8 @@ static bool bl_handle_erase(void) { } } - bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_FLASHING); - bootloader_indicator_update(HAL_GetTick()); - bool ok = bootloader_runtime_erase_app(); bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_CONNECTED); + bool ok = bootloader_runtime_erase_app(); if (!ok) { bl_send_nack(); return false; @@ -394,10 +388,8 @@ static bool bl_handle_ext_erase(void) { * 0xFFFD bank 2 erase. This bootloader constrains all special erase * requests to the app region so the resident bootloader survives. */ - bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_FLASHING); - bootloader_indicator_update(HAL_GetTick()); - bool ok = bootloader_runtime_erase_app(); bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_CONNECTED); + bool ok = bootloader_runtime_erase_app(); if (!ok) { bl_send_nack(); return false; @@ -440,10 +432,8 @@ static bool bl_handle_ext_erase(void) { } } - bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_FLASHING); - bootloader_indicator_update(HAL_GetTick()); - bool ok = bootloader_runtime_erase_app(); bootloader_indicator_set_mode(BOOTLOADER_INDICATOR_CONNECTED); + bool ok = bootloader_runtime_erase_app(); if (!ok) { bl_send_nack(); return false; @@ -507,12 +497,22 @@ int main(void) { bool app_valid = bootloader_runtime_is_app_valid(); bootloader_indicator_set_mode(app_valid ? BOOTLOADER_INDICATOR_APP_PRESENT : BOOTLOADER_INDICATOR_NO_APP); - uint32_t startup_wait_ms = (app_valid && !forced_bootloader) ? - BOOTLOADER_APP_STARTUP_WAIT_MS : BOOTLOADER_HANDSHAKE_TIMEOUT_MS; - if (!bl_wait_for_sync_with_indicator(startup_wait_ms)) { - if (app_valid) { - bootloader_runtime_jump_to_app(); + /* + * poll_sync uses timeout==0 to mean "wait forever". For a valid app that is + * not forced into the bootloader, BOOTLOADER_APP_STARTUP_WAIT_MS == 0 must + * mean "no initial handshake window" so we fall through and jump; + * otherwise reset never reaches jump (only the post-flash main-loop path would). + */ + bool host_sync = false; + if (app_valid && !forced_bootloader) { + if (BOOTLOADER_APP_STARTUP_WAIT_MS != 0U) { + host_sync = bl_wait_for_sync_with_indicator(BOOTLOADER_APP_STARTUP_WAIT_MS); } + } else { + host_sync = bl_wait_for_sync_with_indicator(BOOTLOADER_HANDSHAKE_TIMEOUT_MS); + } + if (!host_sync && app_valid) { + bootloader_runtime_jump_to_app(); } while (1) { diff --git a/bootloader/Src/bootloader_runtime.c b/bootloader/Src/bootloader_runtime.c index 39180c89..dde115b1 100644 --- a/bootloader/Src/bootloader_runtime.c +++ b/bootloader/Src/bootloader_runtime.c @@ -2,7 +2,6 @@ #include "bootloader_config.h" #include "bootloader_hal.h" -#include "bootloader_indicator.h" static UART_HandleTypeDef s_huart = { .Instance = BOOTLOADER_UART_INSTANCE, @@ -141,7 +140,6 @@ bool bootloader_runtime_erase_app(void) { HAL_FLASH_Unlock(); bool ok = true; while (erase_addr < erase_end) { - bootloader_indicator_update(HAL_GetTick()); uint32_t sec = bootloader_f4_sector_of_addr(erase_addr); FLASH_EraseInitTypeDef erase = {0}; erase.TypeErase = FLASH_TYPEERASE_SECTORS; @@ -173,7 +171,6 @@ bool bootloader_runtime_write_app(uint32_t app_offset, const uint8_t *data, size HAL_FLASH_Unlock(); for (size_t i = 0; i < len; i += 8U) { - bootloader_indicator_update(HAL_GetTick()); uint64_t dword = 0xFFFFFFFFFFFFFFFFULL; size_t chunk = ((len - i) >= 8U) ? 8U : (len - i); bootloader_copy_bytes((uint8_t *)&dword, &data[i], chunk); @@ -229,7 +226,6 @@ bool bootloader_runtime_erase_app(void) { bool ok = true; while (erase_addr < erase_end) { - bootloader_indicator_update(HAL_GetTick()); uint32_t bank = bootloader_addr_to_bank(erase_addr); uint32_t segment_end = erase_end; @@ -277,7 +273,6 @@ bool bootloader_runtime_write_app(uint32_t app_offset, const uint8_t *data, size HAL_FLASH_Unlock(); for (size_t i = 0; i < len; i += 8U) { - bootloader_indicator_update(HAL_GetTick()); uint64_t dword = 0xFFFFFFFFFFFFFFFFULL; size_t chunk = ((len - i) >= 8U) ? 8U : (len - i); bootloader_copy_bytes((uint8_t *)&dword, &data[i], chunk); diff --git a/common/Inc/bootloader_command.h b/common/Inc/bootloader_command.h new file mode 100644 index 00000000..96c1569c --- /dev/null +++ b/common/Inc/bootloader_command.h @@ -0,0 +1,54 @@ +#ifndef BOOTLOADER_COMMAND_H_ +#define BOOTLOADER_COMMAND_H_ + +#include +#include +#include + +#define BOOTLOADER_UART_COMMAND "ESBLT_BOOT\n" +#define BOOTLOADER_MAGIC_WORD (0x4553424CUL) +#define BOOTLOADER_HOLD_MAGIC_WORD (0x45534248UL) + +#ifndef BOOTLOADER_CAN_COMMAND_ID +#define BOOTLOADER_CAN_COMMAND_ID (0x6F0U) +#endif + +#ifndef BOOTLOADER_CAN_NODE_ID +#define BOOTLOADER_CAN_NODE_ID (0xFFU) +#endif + +#ifndef BOOTLOADER_CAN_HANDSHAKE_ID +#define BOOTLOADER_CAN_HANDSHAKE_ID (BOOTLOADER_CAN_COMMAND_ID + 1U) +#endif + +#ifndef BOOTLOADER_CAN_HANDSHAKE_COUNT +#define BOOTLOADER_CAN_HANDSHAKE_COUNT (50U) +#endif + +#ifndef BOOTLOADER_CAN_HANDSHAKE_INTERVAL_MS +#define BOOTLOADER_CAN_HANDSHAKE_INTERVAL_MS (10U) +#endif + +typedef enum { + BOOTLOADER_COMMAND_REQUEST_NONE = 0, + BOOTLOADER_COMMAND_REQUEST_HOLD, + BOOTLOADER_COMMAND_REQUEST_FLASH, +} bootloader_command_request_t; + +typedef bool (*bootloader_command_can_send_t)(uint32_t id, const uint8_t *data, size_t len, void *context); + +bool bootloader_command_feed_uart_byte(uint8_t byte); +void bootloader_command_reset_uart_parser(void); +bool bootloader_command_is_can_reset_message(uint32_t id, const uint8_t *data, size_t len); +bool bootloader_command_handle_can_message(uint32_t id, const uint8_t *data, size_t len); +bool bootloader_command_can_handshake_pending(void); +bool bootloader_command_can_handshake_service(bootloader_command_can_send_t send, void *context); +void bootloader_command_set_entry_allowed(bool allowed); +bool bootloader_command_is_entry_allowed(void); +void bootloader_command_init_app_vector_table(void); +void bootloader_command_request_reset(void); +void bootloader_command_request_hold_reset(void); +void bootloader_command_clear_request(void); +bootloader_command_request_t bootloader_command_consume_request(void); + +#endif /* BOOTLOADER_COMMAND_H_ */ diff --git a/docs/UartBootloader.md b/docs/UartBootloader.md index 0a2d2719..e4701ef6 100644 --- a/docs/UartBootloader.md +++ b/docs/UartBootloader.md @@ -64,6 +64,87 @@ Always run `make clean` when switching between `firmware`, `bootloader`, and `app`; each mode uses a different flash map and stale build outputs can flash the wrong image. +## STM32G4: resident bootloader and `bootloader_test` + +Supported targets: `PROJECT_TARGET=stm32g473xx` or `stm32g474xx`. Replace one +with the other in the commands below if your board uses the other G4 part. + +All commands assume the **repository root** is `ES_BLT` (this tree) and use +`BEAR_ENABLE=0` only to skip `compile_commands` generation; omit it if you +use the default. + +### Build the resident bootloader (G4) + +From repo root: + +```bash +make -C test clean TEST=main PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=bootloader BEAR_ENABLE=0 +make -C test TEST=main PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=bootloader BEAR_ENABLE=0 +``` + +Artifact: `build/bootloader/stm32g473xx.bin` (ELF/HEX beside it). The name +matches `PROJECT_TARGET`. + +### Flash the resident bootloader (G4) + +**One-time install via STM32 ROM UART** (board in UART bootloader / `BOOT0`, +per ST AN2606). From repo root; set your serial device: + +```bash +./scripts/flash_bootloader.py --port /dev/cu.YOURPORT --bin build/bootloader/stm32g473xx.bin +``` + +(Default `--address` is `0x08000000`.) + +**ST-Link** (writes the same image to flash start): + +```bash +make -C test flash TEST=main PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=bootloader BEAR_ENABLE=0 +``` + +(`Makefile` uses `st-flash` and `FLASH_ADDRESS=0x08000000` for +`FIRMWARE_TYPE=bootloader`.) + +### Build `bootloader_test` app (G4) + +Integration test: breathe LED, UART spam, FDCAN classic frames, UART magic +reboot command. Link address is `BOOTLOADER_APP_BASE` (default `0x08010000`). + +```bash +make -C test clean TEST=bootloader PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=app BEAR_ENABLE=0 +make -C test TEST=bootloader PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=app BEAR_ENABLE=0 +``` + +Artifact: `build/app/stm32g473xx.bin`. + +### Flash `bootloader_test` (G4) + +**ST-Link** (app region after the bootloader): + +```bash +make -C test flash TEST=bootloader PROJECT_TARGET=stm32g473xx FIRMWARE_TYPE=app BEAR_ENABLE=0 +``` + +(`FLASH_ADDRESS` defaults to `BOOTLOADER_APP_BASE`, e.g. `0x08010000`.) + +**Over UART through the resident bootloader** (STM32CubeProgrammer / +`uart_bootloader_flash.py`). With the board already in the resident bootloader +or after `--enter` if the running app handles the magic packet: + +```bash +./scripts/uart_bootloader_flash.py --port /dev/cu.YOURPORT --bin build/app/stm32g473xx.bin --address 0x08010000 --boot +``` + +Shorter form if the default app glob picks up this binary (typical single-target +tree): + +```bash +./scripts/uart_bootloader_flash.py --port /dev/cu.YOURPORT --enter --boot +``` + +If you use **`nix develop`**, run the same `make` and script commands inside +that shell so the toolchain and Python env match CI. + ## Parent Repo Integration Application repos usually live outside this shared embedded repo. The shared diff --git a/scripts/flash_bootloader.py b/scripts/flash_bootloader.py index 0376e6c8..1d9fb996 100755 --- a/scripts/flash_bootloader.py +++ b/scripts/flash_bootloader.py @@ -21,18 +21,48 @@ def default_bootloader_bin() -> str: def main() -> int: - parser = argparse.ArgumentParser(description="Flash the resident ES_BLT bootloader over STM32 ROM UART") + parser = argparse.ArgumentParser( + description=( + "Flash the resident ES_BLT bootloader image at the start of internal flash. " + "This uses STM32CubeProgrammer over UART and expects the STM32 ROM UART " + "bootloader (BOOT0 high per application note), not the running resident bootloader." + ) + ) parser.add_argument("--port", help="Serial device. If omitted, common CP210x paths are autodetected.") parser.add_argument("--baud", default=115200, type=int, help="UART baud rate") parser.add_argument("--bin", default=default_bootloader_bin(), help="Bootloader binary") parser.add_argument("--address", default=BOOTLOADER_ADDRESS, help=f"Flash address (default: {BOOTLOADER_ADDRESS})") parser.add_argument("--stm32prog", default=default_stm32prog(), help="Path to STM32_Programmer_CLI") + parser.add_argument( + "--assume-rom-bootloader", + action="store_true", + help="Do not print the BOOT0 / resident-bootloader note (use when chip is already in ROM UART boot).", + ) args = parser.parse_args() port = args.port or detect_port() if port is None: print(port_help(), file=sys.stderr) return 1 + erase_hint_printed = False + if (not args.assume_rom_bootloader) and args.address: + try: + addr_i = int(args.address, 0) + except ValueError: + addr_i = -1 + if addr_i == int(BOOTLOADER_ADDRESS, 0): + print( + "\n" + "flash_bootloader.py programs 0x08000000, so STM32CubeProgrammer must erase that flash.\n" + "• Use the STM32 factory ROM UART bootloader: set BOOT0 for System Memory, reset, then run this\n" + " script (do not run uart_bootloader_enter.py first—the resident bootloader executes from that\n" + " same flash and erase will fail, e.g. \"failed to erase memory\" / sectors [0 N]).\n" + "• Or use ST-Link: make -C test flash ... FIRMWARE_TYPE=bootloader\n" + "Pass --assume-rom-bootloader to hide this message.\n", + file=sys.stderr, + ) + erase_hint_printed = True + if not validate_file(args.bin, "Binary"): return 1 if not validate_file(args.stm32prog, "STM32_Programmer_CLI", executable=True): From 1b072c104570a7597845bbeb10569b0fe58a7840 Mon Sep 17 00:00:00 2001 From: Parthiv Shah Date: Sat, 2 May 2026 12:33:44 -0500 Subject: [PATCH 10/12] blinky_bootloader_test: drop dead if(0) in heartbeat_clock_init Co-authored-by: Cursor --- test/tests/blinky_bootloader_test.c | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/test/tests/blinky_bootloader_test.c b/test/tests/blinky_bootloader_test.c index 8a64bdec..2b4e1071 100644 --- a/test/tests/blinky_bootloader_test.c +++ b/test/tests/blinky_bootloader_test.c @@ -32,25 +32,18 @@ static StackType_t s_blinky_task_stack[configMINIMAL_STACK_SIZE]; static StackType_t s_uart_task_stack[configMINIMAL_STACK_SIZE]; static void heartbeat_clock_init(void) { - if(0){ - - } - else if (LED_PORT == GPIOA) { + if (LED_PORT == GPIOA) { __HAL_RCC_GPIOA_CLK_ENABLE(); - } - - else if (LED_PORT == GPIOB) { + } else if (LED_PORT == GPIOB) { __HAL_RCC_GPIOB_CLK_ENABLE(); - } - else if (LED_PORT == GPIOC) { + } else if (LED_PORT == GPIOC) { __HAL_RCC_GPIOC_CLK_ENABLE(); - } + } #ifdef GPIOD else if (LED_PORT == GPIOD) { __HAL_RCC_GPIOD_CLK_ENABLE(); } #endif - } static void led_init(void) { From f8f8e3ec438650532707035d1198964ed9b3f364 Mon Sep 17 00:00:00 2001 From: Parthiv Shah Date: Sat, 2 May 2026 12:51:41 -0500 Subject: [PATCH 11/12] enter bootlaoder when flash --- scripts/uart_bootloader_flash.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/uart_bootloader_flash.py b/scripts/uart_bootloader_flash.py index 295c41ad..458281d1 100755 --- a/scripts/uart_bootloader_flash.py +++ b/scripts/uart_bootloader_flash.py @@ -19,6 +19,8 @@ DEFAULT_APP_BIN_GLOB = "build/app/stm*.bin" BOOTLOADER_PACKET = b"ESBLT_BOOT\n" BOOTLOADER_BYTE_DELAY_SECONDS = 0.005 +# Time to wait after ESBLT_BOOT so the MCU can reset into the resident bootloader before STM32_Programmer connects. +ENTER_TO_FLASH_DELAY_S = 0.2 def default_app_bin() -> str: @@ -104,7 +106,7 @@ def main() -> int: try: if args.enter: request_bootloader(normalize_port_for_pyserial(port), args.baud, args.byte_delay) - time.sleep(0.5) + time.sleep(ENTER_TO_FLASH_DELAY_S) run_stm32prog(args.stm32prog, ["-c", *conn, "-w", args.bin, args.address, "-v"]) if args.boot: run_stm32prog(args.stm32prog, ["-c", *conn, "-g", args.address]) From b76d5456be9faa5de58f85108d1b46c35b486462 Mon Sep 17 00:00:00 2001 From: Parthiv Shah Date: Sat, 2 May 2026 12:56:32 -0500 Subject: [PATCH 12/12] enter bootlaoder pt 2 --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1e878dbe..b47b2858 100644 --- a/Makefile +++ b/Makefile @@ -422,12 +422,15 @@ flash: @echo "🔦 Flashing $(FLASH_FILE) to $(FLASH_ADDRESS)" st-flash write $(BUILD_DIR)/$(FLASH_FILE) $(FLASH_ADDRESS) +# Optional extra args for uart_bootloader_flash.py (e.g. --enter, --boot). Parent makefiles may set this. +UART_BOOTLOADER_FLASH_FLAGS ?= + .PHONY: flash-uart flash-uart: ifeq ($(FIRMWARE_TYPE),bootloader) ./scripts/flash_bootloader.py --bin $(BUILD_DIR)/$(FLASH_FILE) --address $(FLASH_ADDRESS) else ifeq ($(FIRMWARE_TYPE),app) - ./scripts/uart_bootloader_flash.py --bin $(BUILD_DIR)/$(FLASH_FILE) --address $(FLASH_ADDRESS) + ./scripts/uart_bootloader_flash.py $(UART_BOOTLOADER_FLASH_FLAGS) --bin $(BUILD_DIR)/$(FLASH_FILE) --address $(FLASH_ADDRESS) else ./flash-uart.sh $(BUILD_DIR)/$(FLASH_FILE) $(FLASH_ADDRESS) endif