Hello world on STM32F4 Discovery with FreeRTOS — no hardware

Short version: you can build a FreeRTOS firmware image for the STM32F4 Discovery with plain
make and arm-none-eabi-gcc, hand the ELF to Chiplab, and read the UART output back, with no
board, no ST-LINK, and no USB cable. Below is the example from our own repo, the exact stdout
from a run I minted today, and an honest list of what that run does not prove.
This is the boring demo. I mean that as a compliment. It's the one every embedded person recognizes on sight, which makes it the right place to check whether the claim "hardware is becoming an API" survives contact with a real toolchain.
The example
It lives in the Chiplab repo at examples/freertos/stm32f4-discovery. Nothing clever in it.
A single FreeRTOS task gets created, and that task prints Hello world! over USART2, then sleeps
on vTaskDelay, forever. That's the whole application. The interesting part isn't the task, it's
everything the task drags in behind it: the FreeRTOS kernel is a real dependency here, not a
stub. tasks.c, list.c and queue.c all get compiled in, memory comes from the heap_4
allocator, and scheduling runs through the Cortex-M4F port layer. So the firmware that boots is
doing the actual work an RTOS does: vector table, clock setup, SysTick driving the scheduler
tick, context switches through the port's assembly, and a hardware peripheral being written to
from inside a task context.
The kernel version is pinned. The Makefile does a shallow clone of FreeRTOS kernel V11.3.0 rather
than vendoring a copy or floating on main, which means the example doesn't quietly change
behavior six months from now because upstream refactored something. If you build it in a year you
get the same kernel I got today.
Link flags are the usual embedded diet: --gc-sections to drop everything the linker can prove
unreachable, and nano.specs plus nosys.specs so newlib doesn't try to be a hosted C library
on a chip with no operating system under it. Standard. Which is the point. There is no Chiplab
SDK to link against, no shim, no instrumentation build. It's the same ELF you'd flash.

Building it
make. That's the setup section, and I'm not padding it out to look like more work than it is.
You need arm-none-eabi-gcc on your path and git for the kernel clone. The Makefile handles
the rest: pull the pinned kernel, compile the application plus the four kernel sources plus the
port layer, link against the board's linker script, emit an ELF. No IDE, no vendor project file,
no code generator, no CubeMX round trip. This matters more than it sounds like it should, because
"can an agent do this unattended" and "does this need a GUI" are the same question wearing
different clothes.
The output is a single ELF. That file is the entire interface to the next step.
Running it without a board
Chiplab takes the ELF and a board_id, runs it, and gives you back the artifacts. Here the
board is stm32f4_discovery. The run I'm quoting was minted today from exactly the ELF that
Makefile produces.
Unedited stdout:
08:04:34.7331 [INFO] cpu: Guessing VectorTableOffset value to be 0x8000000.
08:04:34.9054 [INFO] machine-0: Machine started.
08:04:34.9793 [INFO] usart2: [host: 0.21s (+0.21s)|virt: 0.12ms (+0.12ms)] Hello world!
08:04:35.3618 [INFO] usart2: [host: 0.59s (+0.38s)|virt: 0.46s (+0.45s)] Hello world!
... (task repeats on its own vTaskDelay period; 21 total prints over ~5.3 virtual seconds before the run was stopped) ...
08:04:39.8055 [INFO] usart2: [host: 5.03s (+0.22s)|virt: 4.9s (+0.22s)] Hello world!
08:04:39.9122 [INFO] machine-0: Machine resumed.
Renode is quitting
I trimmed the middle. The task kept printing on its own period the entire time it was allowed to
run, 21 prints total, and the lines in between look exactly like the lines you can see. I cut
them because a wall of identical Hello world! rows is not evidence of anything the first three
don't already establish. The last two lines are Renode's own bookkeeping as the run winds down,
and I'm not going to read meaning into them that isn't there.
What's worth actually looking at:
The first line is a guess, and it says so. Guessing VectorTableOffset value to be 0x8000000 is
the simulator working out where the vector table lives because the ELF didn't hand it that on a
plate. It guessed right, which is what you want, and it told you it was guessing, which is what
you need. Compare that to a hardware bring-up where the same ambiguity shows up as a hang.
The two columns on each UART line are host time and virtual time, with deltas. That's the
distinction that makes simulation output readable: virt: 0.12ms on the first print means the
firmware got from reset to its first UART write in a tenth of a millisecond of simulated time,
while host: 0.21s is how long my machine spent producing that. By the last print, virtual time
is at 4.9 seconds and host time at 5.03. The two tracked each other closely across the run, which
is a nice property to have but isn't a timing guarantee, and I'll come back to why.
And the scheduler works. vTaskDelay is not a busy loop. For that call to return, SysTick has to
fire, the port's handler has to run, the tick has to increment, the task has to come off the
delayed list and back onto a ready list, and a context switch has to land the CPU back in the
task's stack frame. Twenty-one times, on schedule. That's the RTOS being exercised, not
skipped.
The part I want to be honest about
There are no warnings in that transcript. None. Not one line about an unhandled register, an unimplemented peripheral, or an access to memory that shouldn't exist.
That's real, and it's also the least surprising result in this batch of posts. FreeRTOS on the F4 Discovery is the single most-exercised path we have. It's the combination people reach for first, so it's the combination that got fixed first, and the transcript reflects that: clean boot, clean tick, clean UART, nothing to explain away.
Take it as a statement about this path, not about simulation in general. Push toward less traveled boards and the logs get chattier. When I put the Blue Pill through the same treatment, warnings showed up. When an agent went from datasheet to firmware in one prompt on an L073 Nucleo, warnings showed up there too. A silent log means the board model has had attention, and that's all it means.
Also, and this should be obvious but the internet is the internet: this did not run on physical silicon. There was no board on my desk today. The claim is "the firmware executed on a model of this MCU and produced this output", and it stops precisely there.

What this run covers, and what it doesn't
Covered: does it build, does it boot, does the vector table land, does the scheduler tick, do context switches work, does the task reach the peripheral, does the peripheral emit the bytes you expected. That's the functional loop. For an agent writing firmware, it's most of what you need, because most of what an agent gets wrong is in that list.
Not covered, and worth knowing before you lean on it:
Timing is not cycle-accurate. Chiplab runs on Renode, which is MIT-licensed and has no cycle-accurate mode. Virtual time tracking host time in the transcript above is a scheduling artifact, not a measurement. If you need to know how many cycles your critical section takes, the number is not in this log and cannot be derived from it. I wrote up the full version of this in what cycle-accurate actually means rather than re-deriving it here.
The memory map is not the datasheet's. This specific board model gives you 256K of
contiguous SRAM at 0x20000000, and no CCM at 0x10000000. Real F407 silicon has 128K there,
with 0x20020000 and up reserved, plus a separate CCM block the model doesn't provide. Both
directions bite. Firmware that overruns past 128K runs fine in simulation and faults on the
chip. Firmware that touches CCM works on the chip and reads as nothing here. Same post covers
the mechanism.
Anything outside the chip. No power rails, no crystal that fails to start, no signal integrity, no sensor on the other end of a bus doing something the datasheet didn't mention. If your bug is analog, it isn't in this transcript.
The useful framing is that this run is a compiler for behavior. It catches the class of mistake that stops the firmware from working at all, fast and repeatably, and it says nothing about the class that only shows up when electrons are involved. You still need the board. You need it much later, and much less often, and that's the whole economic argument.
Why a hello world is the interesting example
Because of who's running it.
A human doing this once doesn't care that it's scriptable. An agent does. The loop that makes coding agents useful is write, run, read the failure, fix it, and embedded breaks that loop at the "run" step: the target is a physical object, attached to one desk, that one person can use at a time. Everything downstream of that constraint gets slow. The code takes minutes; proving it takes months.
What the transcript above actually demonstrates is that the loop closes. ELF in, output out, no
human in the middle, no cable, no queue for the shared board. That's a plain make plus one API
call, which means it can happen a hundred times in an afternoon while nobody watches. I've mapped
where this piece sits relative to the rest of the agent tooling stack in the agent
infrastructure map.
Start here, then go break something more interesting. The example is in the repo, and the parts I can't model are listed above rather than left for you to discover at 2am.
Questions people actually ask
Can I really run STM32F4 FreeRTOS with no hardware? Yes, for functional behavior. Build the
ELF with arm-none-eabi-gcc, run it against the stm32f4_discovery board model, read the UART
back. Timing and anything analog are out of scope.
Do I need to modify the firmware to run it in simulation? No. The ELF in this example is
built with the same Makefile, the same kernel, the same linker script and the same nano.specs /
nosys.specs flags you'd use for a real flash. There's nothing Chiplab-specific linked in.
Does the FreeRTOS scheduler actually run, or is it faked? It runs. SysTick drives the tick,
the Cortex-M4F port handles the context switches, and vTaskDelay returns because the kernel
moved the task between its lists. A faked scheduler would not produce 21 evenly spaced prints.
Why does the log say it's guessing the vector table offset? Because the ELF doesn't state it
explicitly, so the simulator infers 0x8000000 from the image layout. It's correct for this
board, and it tells you it inferred it, which is more than a hanging board would.
Can I trust the simulated memory map? Not as a substitute for the datasheet. This model
provides 256K contiguous SRAM at 0x20000000 and no CCM, which differs from real F407 silicon in
both directions. Check allocations against the part you're shipping.
