MicrOS boots successfully on QEMU 🎉


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 🛠️

  1. Configure and build MicrOS: rm -rf build/ cmake -S . -B build/ -DMICROS_BOARD=lm/lm3s6965evb -DMICROS_SAMPLE=hello_world cmake --build build/
  2. Check binary size: arm-none-eabi-size build/samples/hello_world/hello_world Example output: text data bss dec hex filename 6805 160 12616 19581 4c7d build/samples/hello_world/hello_world
  3. Inspect vector table: arm-none-eabi-objdump -s -j .isr_vector build/samples/hello_world/hello_world | head Make sure the first word is an SRAM address (SP), second is FLASH address (Reset_Handler).
  4. Run in QEMU: qemu-system-arm -M lm3s6965evb -nographic \ -kernel build/samples/hello_world/hello_world You should see the firmware boot without crashing.
    (Output from printf() will appear once _write() is hooked to UART0.)

Next steps

  1. Implement _write() for UART0 on LM3S → printf("Hello, MicrOS!\n") visible in the QEMU terminal.
  2. Add the first task scheduler and context switching.
  3. 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 🚀.


Grzegorz Grzęda
Grzegorz Grzęda

At G2Labs, Grzegorz Grzęda develops IoT and embedded platforms while building MicrOS in public and sharing hands-on tutorials.

Articles: 10

Newsletter Updates

Enter your email address below and subscribe to our newsletter