case 06 · Personal · 2025

UART bootloader

A custom bootloader for the STM32F446RE. Receives a firmware image over UART, writes it to flash, hands control to the application.

C STM32F446RE UART flash vector table
github.com/Prateek2174/uart_bootloader

Why build it

Once you've flashed a board a dozen times with ST-Link, the next obvious thing is to teach it how to update itself. A UART bootloader is a small enough project to write end-to-end but it touches everything that's easy to skip in higher-level work: the linker script, the vector table, flash erase/write timing, and the jump from one program into another.

How it works

  • Bootloader lives in the first sector of flash, application starts further up.
  • On reset the bootloader waits a moment on UART. If it gets the trigger byte, it enters update mode; otherwise it jumps to the application.
  • Update mode receives the new image in fixed-size chunks over UART, with a small framing layer.
  • Each chunk is written to flash one page at a time after the destination sector is erased.
  • When the transfer completes, the bootloader reconfigures the vector table address and jumps to the application's reset handler.

What was tricky

The jump from bootloader to application has to happen with the CPU in a clean state — deinit your peripherals, disable interrupts, set the new vector table base, set the stack pointer from the application's vector table, then branch. Forget any one of those and the application either crashes or behaves mysteriously a few hundred cycles in.

The flash programming side is unforgiving in a different way: writes only go 0 → 1 → 0 (you erase to all-ones, then program zeros), and you have to wait for the busy flag every time. Get the polling wrong and you'll corrupt the page you just wrote.

Core/Src/main.c C
#define BOOTLOADER_SIZE        (0x8000U)
#define MAIN_APP_START_ADDRESS (FLASH_BASE + BOOTLOADER_SIZE)


static void jump_to_main(void){

	typedef void (*void_fun)(void);

	uint32_t* reset_vector_location = (uint32_t*)(MAIN_APP_START_ADDRESS + 4U); //Since the first vector is the Stack pointer;
	uint32_t* reset_vector = (uint32_t*)(*reset_vector_location);
	void_fun jump_fun = (void_fun)reset_vector;

	jump_fun();
}
MCU
STM32F446RE
Comms
USART2 (default), 115200 baud
Flash
Sector-erase + word-program via FLASH peripheral
Layout
Bootloader at 0x0800_0000; application offset configurable

What's next

  • CRC integrity check — verify the received firmware with a CRC32 before committing it to flash; a corrupted transfer currently goes undetected until the application crashes at runtime
  • Host-side flashing script — write a Python companion script to drive the protocol from a PC: chunk the binary, send it, and report progress cleanly
  • Baud rate negotiation — auto-baud detect or a handshake sequence so the host doesn't have to be pre-configured to 115200
  • Software-triggered entry — let the running application jump into the bootloader via a magic value in backup RAM, eliminating the need for a physical reset-and-hold sequence
  • Encrypted firmware — add AES decryption in the bootloader so the binary on the wire is ciphertext; useful for protecting IP on production hardware