For the last couple of days I’ve been battling with my custom embedded operating system — MicrOS (slogan: “simple, open, embedded”). The goal was straightforward: get a bare-metal “Hello, world” sample running under emulation, before moving to real hardware. The journey turned out to be more educational than I expected.
The challenge
My first target was the familiar STM32VLDiscovery board.
I wrote a minimal linker script and a startup file in C that set up the stack, copied .data, zeroed .bss, and jumped into main().
The linker defined the memory regions:
MEMORY {
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
and the startup provided the vector table:
__attribute__((section(".isr_vector")))
const void *vector_table[] = {
(void *)&__stack_top__, // initial SP
Reset_Handler, // entry point
NMI_Handler,
HardFault_Handler,
// ...
};
Everything looked fine… until I tried to run it:
qemu-system-arm -M stm32vldiscovery -nographic -serial mon:stdio \
-kernel build/samples/hello_world/hello_world
QEMU answered:
qemu: fatal: Lockup: can't escalate 3 to HardFault (current priority -1)
💥 A hard crash, every time.
The investigation
I assumed it was my fault (it usually is 😅).
I checked the ELF with nm and objdump:
arm-none-eabi-nm hello_world | grep __data
Output:
20000000 D __data_start__
20000060 D __data_end__
08001ad8 A __data_load__
This revealed the first bug:__data_load__ overlapped with .text, so my Reset_Handler was copying instructions as if they were data — instant fault!
After fixing the linker script with:
.data : {
__data_start__ = .;
*(.data*)
__data_end__ = .;
} > RAM AT > FLASH
__data_load__ = LOADADDR(.data);
the symbols looked healthy: .data in RAM, .data_load__ in FLASH.
But the crash persisted.
The discovery
The culprit wasn’t my code after all — it was QEMU’s STM32 emulation.
The stm32vldiscovery machine in QEMU is only partially implemented. Touching even basic RCC or FLASH registers triggers faults that can’t be recovered. That explained the mysterious HardFault loop.
The breakthrough
Instead of fighting STM32 emulation, I tried another board:
qemu-system-arm -M lm3s6965evb -nographic \
-kernel build/samples/hello_world/hello_world
And… success! 🎉
MicrOS booted cleanly, initialized .bss and .data, and reached main() without crashing.
To confirm, I inspected the vector table:
arm-none-eabi-objdump -s -j .isr_vector hello_world | head
Output:
08000000 a8310020 c5010008 bd010008 ...
Decoded as:
0x200031a8→ initial SP (in SRAM ✅)0x080001c5→ Reset_Handler (in FLASH ✅)
The system was now booting properly in QEMU.
What this means for MicrOS
- ✅ Toolchain and startup are solid.
- ✅ QEMU can run MicrOS reliably on LM3S6965EVB.
- ❌ QEMU’s STM32 machines are not practical (real hardware will be the true test).
Step-by-step: reproduce this yourself 🛠️
- Configure and build MicrOS:
rm -rf build/ cmake -S . -B build/ -DMICROS_BOARD=lm/lm3s6965evb -DMICROS_SAMPLE=hello_world cmake --build build/ - Check binary size:
arm-none-eabi-size build/samples/hello_world/hello_worldExample output:text data bss dec hex filename 6805 160 12616 19581 4c7d build/samples/hello_world/hello_world - Inspect vector table:
arm-none-eabi-objdump -s -j .isr_vector build/samples/hello_world/hello_world | headMake sure the first word is an SRAM address (SP), second is FLASH address (Reset_Handler). - Run in QEMU:
qemu-system-arm -M lm3s6965evb -nographic \ -kernel build/samples/hello_world/hello_worldYou should see the firmware boot without crashing.
(Output fromprintf()will appear once_write()is hooked to UART0.)
Next steps
- Implement
_write()for UART0 on LM3S →printf("Hello, MicrOS!\n")visible in the QEMU terminal. - Add the first task scheduler and context switching.
- Separate
arch/vs.board/directories to make LM3S (for emulation) and STM32 (for hardware) selectable in CMake.
Conclusion
This milestone marks the first time MicrOS boots and runs on real (emulated) silicon.
It’s a small step, but a foundational one — proving the toolchain, linker, and startup are working correctly.
The next blog post will hopefully show a clean "Hello, MicrOS!" coming out of QEMU’s UART, and then we can dive into scheduling and threads 🚀.