For a product I’m developing, I’m using the Acconeer XM125 module, which uses an STM32 L4 series MCU to control the radar IC. Using the XM125 module in production requires custom firmware (or at least modify one of the examples). My past experience with remote sensing has taught me that, wherever possible, firmware should be updatable. Given the need to write custom firmware for a complex system like a radar sensor, this was a non-negotiable.

Since the STM32 already has a built-in bootloader, using this was the path of least resistance, or so I thought. I learned a few things along the way to getting this work reliably. In this blog post I will share these learnings.

Entering the bootloader — STM32 application

There are two ways of entering the system bootloader of the STM32. Option 1 is to use the Boot0 pin, and Option 2 is to enter the bootloader directly from the main application. Given my situation, I only have UART to communicate with the sensor, which means that accessing the Boot0 pin is out of the question (although, if you are using the Boot0 pin, be sure to add the required resistors — don’t just hook it up to a GPIO line on another MCU, or you will have trouble with reliability).

There is no built-in function in the STM32 SDK to enter the bootloader from software; instead, you have to poke the stack to get the MCU to jump to the bootloader memory location. This simply changes the next set of instructions the MCU will execute.

typedef void (*pFunction)(void);pFunction JumpToApplication;uint32_t JumpAddress;JumpAddress = *(__IO uint32_t*) (BOOTLOADER_ADDRESS + 4);JumpToApplication = (pFunction) JumpAddress;JumpToApplication();

There are a few things to watch out for however.

1. The BOOTLOADER_ADDRESS is different for every MCU Family
Take a look at this blog post for the BootAddr that corresponds to your MCU type https://community.st.com/t5/stm32-mcus/how-to-jump-to-system-bootloader-from-application-code-on-stm32/ta-p/49424

2. The bootloader expects the MCU state to be pristine, as if the MCU has freshly rebooted. No initialised peripherals, no running UART, etc. Timings are very important in the bootloader, and if anything interrupts the timings, it won’t work.

Getting the MCU from its application state, which will have a bunch of peripherals running, back to an initial state is a challenge — it may be unclear what is actually running and therefore what needs to be de-initialised. Also, having to manually disable peripherals is error-prone. It is easy to forget to de-init something in a future update, and essentially prevent yourself from ever getting into the bootloader. If the bootloader is not in a pristine state, the bootloader may or may not work, or may work erratically.

The better way to enter the bootloader is to reboot and have the application enter the bootloader before anything is initialised. To do this, we need to utilize a “no init” section of memory. No-init memory is memory that is not automatically erased when the MCU reboots — it allows you to persist data between reboots (this does not apply to power cycling or brownouts).

We start by declaring a variable in the application's global space

__attribute__((section(".noinit"))) uint32_t bootloader_magic;

Then check to see if this variable is set as the first action in main(), and if so, enter the bootloader before the MCU has initialised anything.

#define BOOTLOADER_MAGIC 0xDEADBEEFint main(void) {  if (bootloader_magic == BOOTLOADER_MAGIC) {    typedef void (*pFunction)(void);    pFunction JumpToApplication;    uint32_t JumpAddress;    JumpAddress = *(__IO uint32_t*) (BOOTLOADER_ADDRESS + 4);    JumpToApplication = (pFunction) JumpAddress;    JumpToApplication();  }}

Then, in your application code, you need a way to trigger the entry of the bootloader. Such as sending a command through UART, which sets the bootloader_magic variable and then resets the MCU.

void jump_to_bootloader(void) {  bootloader_magic = BOOTLOADER_MAGIC;  NVIC_SystemReset();}

Entering the Bootloader — Main MCU (Arduino)

The SMT32 bootloader uses UART at any speed from 9600 to 115200 — it auto-detects the baud rate based on the first byte. The bootloader also uses Serial with 8 bits, even parity, and one stop bit (which is different from most serial protocols, which do not use stop bits). I found that a baud rate of 19200 worked well.

void setup() {  Serial.begin(115200);   // Initializing Application  Serial1.setRX(17);  Serial1.setTX(16);  Serial1.begin(115200);  delay(1000); // Brief delay for hardware to stabilize   sendEnterBootloader(); // Send command which will trigger jump_to_bootloader on the STM32  delay(500); // Allow the mcu to reboot and enter the bootloader   // Restart the serial for bootloader  Serial1.end();  delay(500);  // Starting STM32 Bootloader…  Serial1.begin(19200, SERIAL_8E1);  Serial1.flush();   // https://www.st.com/resource/en/application_note/an3155-usart-protocol-used-in-the-stm32-bootloader-stmicroelectronics.pdf  checkSTM(); // Perform the sync to set the baud rate, read the MCU device info, version, and ID  eraseSTM(); // Erase at least as many pages as your firmware will use (Each page is 2K)  writeFlash(); // Write the new firmware  verify(); // Verify the new firmware  endBootloader(); // Reset the MCU}

Resetting the MCU

There is an NRST pin on the MCU that can be used for resetting the MCU. Accessing this pin, however, is out of reach of a simple UART implementation. This will also not reset the rest of the sensor. A safer option is to toggle the power line back at the main MCU to perform a cold reset. This also allows the sensor to be hard reset at any time.

Final Notes

Implementation of checkSTM(), eraseSTM(), writeFlash(), verify(), and endBootloader() is outside the scope of this document.

Bootloading this way is still dangerous. If the bootloading process fails mid-way through writing, then the device could well be bricked, unless you can access the boot0 pin or reprogram using JTAG. The safer way is to make a stub bootloader — a small program that sits in protected memory and runs first, validates the application, and then decides to boot into the application or the bootloader.