I started this project with a fairly simple question.
Embedded engineers often debate when Linux is justified, when an RTOS is a better fit, and when a simpler system would have been enough.
Rather than compare different development boards, I wanted to keep the hardware fixed and change only the operating-system model.
The platform I chose was the Sipeed Lichee RV / 86 Panel, based on the Allwinner D1 and single-core XuanTie C906 RISC-V processor.
The experiment eventually grew well beyond benchmarking. It involved RT-Smart BSP work, a local Apache NuttX D1 port, watchdog and timer debugging, measurement validation, LCD and touchscreen bring-up, Wi-Fi, and finally a native 720×720 graphical interface.
The hardwareThe test platform uses:
Allwinner D1 / sun20i-d1
- Allwinner D1 / sun20i-d1
XuanTie C906 RV64 processor
- XuanTie C906 RV64 processor
Single core, approximately 1.008 GHz maximum tested frequency
- Single core, approximately 1.008 GHz maximum tested frequency
512 MiB DDR3
- 512 MiB DDR3
24 MHz RISC-V TIME counter
- 24 MHz RISC-V TIME counter
microSD boot
- microSD boot
720×720 LCD
- 720×720 LCD
Capacitive touchscreen
- Capacitive touchscreen
XR829 Wi-Fi
- XR829 Wi-Fi
UART console
- UART console
Sipeed Lichee RV / 86 Panel carrier
- Sipeed Lichee RV / 86 Panel carrier
Using one physical platform meant the CPU, memory subsystem, board layout, oscillator sources, serial path and boot media remained constant throughout the OS comparison.
Three operating-system modelsI tested three substantially different environments.
Ubuntu LinuxUbuntu Linux provided the mature protected-process baseline.
It gives you the environment most embedded Linux developers expect:
protected processes
- protected processes
virtual memory
- virtual memory
mature filesystem support
- mature filesystem support
networking
- networking
POSIX APIs
- POSIX APIs
broad tooling and software compatibility
- broad tooling and software compatibility
I used RT-Thread RT-Smart 5.0.2 as the middle ground.
RT-Smart provides MMU-backed userspace and process isolation in a much smaller embedded operating system.
Getting it running correctly on the D1 required BSP work around MMU setup, memory mapping, heap initialization, serial configuration, SD handling and runtime support.
The D1 bring-up work was submitted upstream as:
https://github.com/RT-Thread/rt-thread/pull/11714
Apache NuttXFor Apache NuttX I created a local D1 board port.
The initial target was deliberately FLAT S-mode.
That means the benchmarked NuttX configuration used a shared address space rather than Linux-style protected processes.
That distinction is essential when looking at the benchmark results.
The purpose of the first NuttX stage was to establish a stable D1 hardware port before attempting a more complex protected/MMU configuration.
The first NuttX boot was not actually a successful bring-upThe first local NuttX image reached the nsh> shell surprisingly quickly.
At first this looked like success.
Then the board reset roughly 20 seconds later.
The cause turned out to be inherited hardware state.
U-Boot had left the Allwinner D1 watchdog enabled with an approximately 16-second timeout.
NuttX had successfully started, but it had inherited a guaranteed reset condition from the bootloader.
Disabling the watchdog early in NuttX startup fixed the reset.
That became one of the first important lessons from the project:
A shell prompt is not proof that board bring-up is complete.
Timed blocking, interrupts and sustained uptime also have to work.
SBI TIME said success, but the interrupt never arrivedOnce the watchdog issue was fixed, another problem appeared.
sleep 1 would not wake correctly.
The free-running time source was advancing, but timed waits were not expiring.
The diagnostics showed that:
OpenSBI reported SBI specification 1.0
- OpenSBI reported SBI specification 1.0
the TIME extension was advertised
- the TIME extension was advertised
set_timer returned success
set_timerreturned success
STIE was enabled
- STIE was enabled
a valid deadline was programmed
- a valid deadline was programmed
But after the deadline passed, the expected supervisor timer pending state never appeared.
STIP remained clear.
Rather than continue modifying the firmware path, I moved NuttX onto the native Allwinner D1 Timer1 peripheral.
Timer1 runs from OSC24M and delivers its interrupt through the PLIC.
That gave NuttX a working scheduler tick independent of the broken supervisor timer path.
Then I found a one-count timer errorThis was probably the most interesting debugging result in the project.
The D1 timer runs from a 24 MHz source.
For a 1 ms scheduler tick, the obvious interval appears to be:
24,000
That is what I initially programmed.
The benchmark then showed a small but consistent clock-domain drift.
Over approximately 20 seconds, the difference accumulated to nearly 0.84 ms.
The error was almost exactly one input clock per scheduler period.
The reason was the timer's countdown behaviour.
Programming an interval value of 24, 000 produced an effective period of 24, 001 clocks.
Changing the register value to:
23,999
produced the intended 24, 000 input clocks per scheduler tick.
After that correction, the measured drift was effectively eliminated.
The numerical error was only around 41.7 ppm.
That sounds insignificant.
But when you are attempting to make sub-microsecond latency measurements, it is enough to invalidate the experiment.
The measurement code also affected the resultAnother issue came from the benchmark itself.
An earlier version printed diagnostic information over UART at 115200 baud during the measurement interval.
That output consumed enough time to disturb scheduler behaviour and create millisecond-scale apparent clock drift.
The final latency benchmark therefore ran silently during the measurement period.
This reinforced another basic principle:
The instrumentation used to observe a real-time system can become part of the system you are measuring.
Benchmark resultsOnce the timer path and measurement methodology were corrected, I could retain defensible results.
Some examples are shown below.
Contended mutex p50Linux: 21.666 µs
- Linux: 21.666 µs
RT-Smart: 6.833 µs
- RT-Smart: 6.833 µs
NuttX FLAT: 1.042 µs
- NuttX FLAT: 1.042 µs
Linux: 21.208 µs
- Linux: 21.208 µs
RT-Smart: 13.250 µs
- RT-Smart: 13.250 µs
NuttX FLAT: 1.167 µs
- NuttX FLAT: 1.167 µs
Linux: 265.333 µs
- Linux: 265.333 µs
RT-Smart: 1063.958 µs
- RT-Smart: 1063.958 µs
NuttX FLAT: 10.000 µs
- NuttX FLAT: 10.000 µs
The loaded NuttX 1 ms periodic test produced:
p50: 0.542 µs
- p50: 0.542 µs
p99: 0.583 µs
- p99: 0.583 µs
p99.9: 0.708 µs
- p99.9: 0.708 µs
max: 0.708 µs
- max: 0.708 µs
missed periods: 0 / 20, 000
- missed periods: 0 / 20, 000
The periodic task ran at FIFO priority 120 with a continuous lower-priority FIFO90 compute worker on the single C906 core.
There is an important measurement caveat.
The phase-alignment midpoint uncertainty was approximately ±0.375 µs.
So the defensible conclusion is not that the physical scheduler latency is known to exactly 0.542 µs.
The useful conclusion is that the observed wake-up behaviour was at the sub-microsecond measurement floor, with extremely tight tails and zero missed periods.
NuttX did not simply "beat Linux"The raw numbers need context.
The tested NuttX build was FLAT shared-address-space S-mode.
Linux was simultaneously providing:
protected processes
- protected processes
virtual memory
- virtual memory
filesystem infrastructure
- filesystem infrastructure
networking
- networking
process semantics
- process semantics
a much larger userspace environment
- a much larger userspace environment
broad third-party software compatibility
- broad third-party software compatibility
RT-Smart provided another architecture again, with MMU-backed userspace and process isolation in a much smaller embedded environment.
So the project did not produce a simple:
RTOS is faster than Linux
result.
Linux was actually faster in some uncontended userspace paths.
The operating systems were optimized for different cost structures.
The meaningful comparison is not simply which bar on a benchmark chart is shortest.
It is what functionality and protection model you are paying for.
Turning the benchmark platform into a touchscreen deviceOnce the OS experiment was complete, I continued working on the physical 86 Panel.
The Linux/Tina/MaixLinux configuration now has:
stable 720×720 LCD output
- stable 720×720 LCD output
approximately 58.1 fps
- approximately 58.1 fps
correct default_lcd panel profile
- correct
default_lcdpanel profile
framebuffer RGB output verified
- framebuffer RGB output verified
PWM7 backlight at 20 kHz
- PWM7 backlight at 20 kHz
working fts_ts capacitive touchscreen
- working
fts_tscapacitive touchscreen
live X/Y touch events
- live X/Y touch events
XR829 Wi-Fi
- XR829 Wi-Fi
IP networking
- IP networking
DNS
- DNS
Internet connectivity
- Internet connectivity
I then built a lightweight native touchscreen application.
Rather than using Chromium, Electron or a desktop environment, the interface is designed specifically for this constrained embedded platform.
RunTime interfaceThe first screen is a live RunTime system dashboard.
It displays information such as:
CPU frequency and load
- CPU frequency and load
temperature
- temperature
memory usage
- memory usage
Linux/system status
- Linux/system status
Wi-Fi and network information
- Wi-Fi and network information
IP address
- IP address
display information
- display information
live touch coordinates
- live touch coordinates
It turns the 86 Panel into a compact embedded system monitor.
Home interfaceI also built a second interface inspired by a smart-home control panel.
The Home screen contains:
climate controls
- climate controls
lighting toggles
- lighting toggles
security status
- security status
energy information
- energy information
room status
- room status
The bottom navigation allows the user to switch directly between:
RunTime
and
Home
The Home interface is currently primarily a local UI demonstration. It does not require a Home Assistant server.
The architecture is deliberately lightweight enough that real Home Assistant REST or WebSocket integration could be added later.
Video demonstrationsI recorded a full video explaining why I ran the OS experiment, showing the hardware and discussing some of the results:
I also recorded a separate demonstration of the 720×720 touchscreen and graphical interface:
Source codeThe complete public project repository is here:
https://github.com/lanceharvie/allwinner-d1-os-benchmark
The repository includes the RunTime Panel source, Makefile, documentation, images, font generation tools and the v1.0.0 software release.
The source release is archived on Zenodo:
https://doi.org/10.5281/zenodo.22054828
Full engineering reportI documented the complete experiment, including failed paths and methodology, in a 30-page technical report:
Allwinner D1 Operating-System Bring-Up and Benchmark Experiment
https://doi.org/10.5281/zenodo.22054539
The report contains:
Linux baseline results
- Linux baseline results
RT-Smart bring-up
- RT-Smart bring-up
NuttX D1 port creation
- NuttX D1 port creation
watchdog diagnosis
- watchdog diagnosis
SBI timer diagnosis
- SBI timer diagnosis
native Timer1 implementation
- native Timer1 implementation
timer off-by-one analysis
- timer off-by-one analysis
benchmark methodology
- benchmark methodology
measurement validation
- measurement validation
cross-OS results
- cross-OS results
limitations
- limitations
reproducibility information
- reproducibility information
LCD, touch, Wi-Fi and RunTime Panel continuation work
- LCD, touch, Wi-Fi and RunTime Panel continuation work
The most useful result from the project was not identifying an operating-system winner.
It was seeing how easily a benchmark can produce the wrong conclusion.
The bootloader can leave hardware in an unexpected state.
A timer can be wrong by one count.
UART diagnostics can disturb the measurement.
Two operating systems can expose similar APIs while providing completely different protection and scheduling models underneath them.
Architecture determines what a benchmark number actually means.
The next experiment I would like to run is PREEMPT_RT Linux on exactly the same Allwinner D1 hardware.
That should make the real-time comparison considerably more interesting.







Comments