Bare-metal STM32: A foundational approach to embedded system startup
This review examines lmilz's detailed guide on building an STM32 project from scratch, focusing on the vector table, linker script, and startup code. It clarifies the low-level components for…
This review examines lmilz's detailed guide on building an STM32 project from scratch, focusing on the vector table, linker script, and startup code. It clarifies the low-level components for embedded development.
TL;DR
Best for: Embedded developers seeking a deep, fundamental understanding of how STM32 microcontrollers initialize, or for educational purposes where control over every byte is paramount. Skip if: You prioritize rapid application development, rely on high-level abstraction layers like HAL, or require extensive peripheral support out of the box. This approach is not for quick prototyping. Bottom line: lmilz's method provides unparalleled insight and control over the STM32 startup process, demystifying the boot sequence, but it demands significant manual effort and a strong grasp of ARM architecture.
METHODOLOGY
This v0 review draws on the founder's published claims and detailed code examples at lmilz.dev/blog/2026/04/19/Embedded-Hello-World.html; independent benchmarks pending. Update cadence: re-tested when claims diverge from observed behavior. The "tool" under review is the methodology and code artifacts presented by lmilz for setting up a bare-metal STM32 project. This includes the specific C and assembly code, along with the linker script, designed to run on an STM32F446RE microcontroller. The review covers the explicit steps and explanations provided by lmilz regarding the vector table, linker script, and startup code. It does not cover independent performance measurements, long-term workflow integration, or edge cases beyond the scope of the blog post's minimal example. The focus is on the clarity and technical soundness of the described process.
WHAT IT DOES
The lmilz blog post details a multi-step process for initializing an STM32F446RE microcontroller without relying on vendor-supplied libraries or IDEs. The core components provided enable a developer to understand and control the very first instructions executed by the MCU.
Custom vector table definition
The process begins with defining a custom vector table in C. This table maps interrupt requests and exception handlers to specific memory addresses, crucial for the ARM Cortex-M architecture. lmilz outlines how to declare this table as an array of function pointers, ensuring it is placed at the correct memory location, typically the start of Flash memory. This explicit definition replaces the default tables provided by toolchains, offering direct control over exception handling.
Linker script definition
A custom linker script (stm32f446re.ld) is central to this bare-metal approach. lmilz meticulously explains how to define memory regions (Flash, RAM), specify the entry point for the program, and control the placement of various code and data sections. This script ensures that the vector table, startup code, and application code are correctly positioned in memory, enabling the MCU to boot and execute the program as intended. It defines the stack and heap, critical for runtime operations.
Startup assembly code
The guide provides a minimal startup assembly file (startup.s). This code is the first to run after the MCU resets. It performs essential tasks such as initializing the stack pointer, copying initialized data from Flash to RAM, and zeroing out the uninitialized data (BSS) section. Crucially, it then calls the SystemInit function (if defined) and finally jumps to the main function, transferring control to the C application code. This assembly layer is the bridge between hardware reset and high-level C execution.
Minimal C entry point
Finally, lmilz presents a minimal main.c file. This C code serves as the application's entry point, demonstrating a simple infinite loop. It relies on the preceding components to correctly initialize the system before any application logic executes. The example is kept intentionally simple to highlight the foundational setup, allowing developers to build complex applications on this bare-bones foundation.
WHAT'S INTERESTING / WHAT'S NOT
What's interesting about lmilz's approach is its uncompromising focus on fundamentals. In an era where abstraction layers like STM32CubeMX and HAL libraries dominate embedded development, this guide peels back every layer to expose the underlying mechanisms. For developers who have only ever used these high-level tools, this post is an invaluable educational resource, demystifying the "magic" behind MCU startup. The explicit code examples for the vector table, linker script, and startup assembly are clear and well-commented, providing a reproducible path to understanding. This level of control is essential for applications requiring extreme optimization, minimal footprint, or precise timing, where every instruction cycle matters. It also fosters a deeper understanding of potential debugging challenges related to memory layout or exception handling.
What's not interesting, from a product-feature perspective, is that this is not a tool to be adopted for rapid development. It is a methodology. The inherent complexity and manual effort involved in setting up a project this way mean it is not suitable for every team or project. While the blog post is excellent at explaining how to do it, it doesn't address the why not for typical commercial projects, which often prioritize development speed and maintainability over absolute control. For instance, managing multiple peripherals, complex communication protocols, or real-time operating systems would require significantly more manual effort to integrate into this bare-metal framework compared to using a vendor-supplied ecosystem. The trade-off is clear: maximum understanding and control at the cost of increased development time and boilerplate code.
PRICING
This is a blog post detailing a technical process, not a commercial product or service. There are no associated costs or pricing tiers. The information and code examples are freely available as of May 15, 2026.
VERDICT
lmilz's guide to bare-metal STM32 development is an essential resource for embedded engineers who need to understand the absolute lowest levels of microcontroller operation. It excels as an educational tool, providing the foundational knowledge necessary to debug complex boot issues or optimize for extreme resource constraints. For projects where every byte and cycle counts, or for those seeking to escape the black box of vendor-supplied HALs, this methodology offers unparalleled control. However, for most commercial projects that prioritize development velocity and leverage existing ecosystem support, the manual overhead of this approach is prohibitive. We recommend this for learning and highly specialized applications, but not as a default for general-purpose embedded development.
WHAT WE'D TEST NEXT
Our next steps would involve implementing lmilz's methodology on a physical STM32F446RE Nucleo board. We would compile the provided code using a standard ARM GCC toolchain and flash it to the device. Key benchmarks would include measuring the final binary size and comparing it against a functionally equivalent project generated by STM32CubeMX. We would also test basic peripheral initialization, such as blinking an LED or sending data over UART, to assess the effort required to extend this bare-metal foundation. Debugging capabilities with GDB and an ST-Link debugger would also be evaluated to ensure a practical development experience beyond the initial setup. Finally, we would explore the portability of this setup to other STM32 series, such as the F0 or L4, to understand the necessary architectural adaptations.
Pull quote: “lmilz's method provides unparalleled insight and control over the STM32 startup process, demystifying the boot sequence, but it demands significant manual effort and a strong grasp of ARM architecture.”
Every claim ties to a primary source. See our methodology.