Running a Lua VM on a Microcontroller
Running a Lua virtual machine on a microcontroller was not an academic exercise, but part of a real system built under strict resource constraints.
This work is part of Alyn — a lightweight embedded project designed to operate on limited MCU-class hardware while still allowing flexibility at the application level.
Alyn was developed as an assistive system for children with special needs, intended to operate reliably in an underwater pool environment, where robustness, determinism, and low power consumption are critical.
The goal was to introduce controlled runtime configurability and behavior scripting without moving the system to Linux, while consciously accepting the trade-offs imposed by flat memory, lack of an MMU, and real-time constraints.
In this article you’ll learn:
- Why running Lua on an MCU is fundamentally different from Linux systems
- How heap fragmentation manifests without an MMU, even with garbage collection
- What Lua’s GC can and cannot guarantee under constrained memory conditions
- Practical lessons learned from integrating Lua with Zephyr in a real system
Why Zephyr?
Zephyr was selected for its small footprint, deterministic execution, and explicit heap management model.
Rather than hiding memory behavior behind complex allocators, Zephyr makes allocation patterns and fragmentation observable — which is essential when evaluating how a Lua VM behaves under true MCU-class constraints.
Lessons, Trade-offs, and Hard Limits
Lua was not designed to run on a bare-metal or MCU-class system.
It assumes a dynamic heap, a capable allocator, and a forgiving memory model.
And yet — in the right architecture — running a Lua VM on an MCU can be both practical and powerful.
This post summarizes the real challenges, the failures, and the patterns that actually work when integrating Lua into an MCU system running Zephyr RTOS.
❓ Why Even Consider Lua on an MCU?
In many embedded products, the real problem is not performance — it’s customization.
Typical pain points:
- Per-customer behavior differences
- Configuration explosion
- Feature flags, ifdefs, and brittle tables
- Long reflash cycles for small logic changes
These pressures often push teams from MCU-based systems to Linux — not because they need Linux, but because they need flexibility.
Lua provides:
- A compact scripting language
- A clean C API
- Fast iteration
- Clear separation between core and policy
⚠️ The Reality: Lua Is Dangerous on an MCU
Porting Lua is easy.
Running it safely over time is not.
The main challenges are:
1. Garbage Collection Is Not Deterministic
Lua’s GC:
- Is heuristic-based
- Is not realtime-safe
- Runs when Lua decides, not when you decide
This alone disqualifies Lua from hard realtime paths.
2. Heap Fragmentation Is the Silent Killer
On an MCU:
- There is no MMU
- No virtual memory
- No heap compaction
- No swapping
Zephyr’s sys_heap:
- Frees memory correctly
- Does not defragment
- Requires contiguous blocks
Over time:
- Small, variable allocations fragment the heap
- GC frees memory, but cannot merge distant blocks
- Eventually even a 16–32 byte allocation fails
This is not a bug — it is physics.
3. GC Cannot Save You From Fragmentation
A key realization:
Lua GC can free objects.
It cannot fix heap topology.
Even with a correct lua_free() implementation:
- Free memory may exist
- But no contiguous block may be available
At that point, the failure is no longer theoretical.
In my case, the system would run correctly for a long time and then suddenly fail on very small allocations.
The following log was captured during such a failure:
LUA OOM: alloc 16 failed
LUA OOM: alloc 32 failed
🧠 Why This Works on Linux but Not on MCUs
On Linux:
- Lua allocates virtual memory
- The kernel handles page mapping
- Physical fragmentation is hidden
- Allocators are sophisticated and compacting
On MCUs:
- Memory is flat and physical
- Fragmentation accumulates forever
- Allocator behavior is visible and unforgiving
This difference alone explains why Lua feels “safe” on Linux and “fragile” on bare metal.
✅ What Actually Works on an MCU
After multiple iterations and failures, only a few patterns proved stable.
1. No Dynamic Allocation After Initialization
The most important rule:
After system init, Lua must not allocate memory.
This means:
- No table creation
- No string formatting
- No dynamic userdata
- No implicit allocations in hot paths
Lua becomes a deterministic behavior engine, not a general runtime.
2. Pre-Allocated Objects and Ownership in C
- All heavy objects are allocated once
- Owned and managed by C
- Lua receives references only
- Lua never “creates” system objects
This prevents both leaks and fragmentation.
3. Clear VM Lifecycle
If a full reset is required:
- The Lua VM must be destroyed
- The heap must be reinitialized
- A new Lua state must be created
Partial resets inside Lua do not work.
4. PC Simulator With Identical Lua Code
A critical architectural win:
- The same Lua scripts run on:
This enables:
- Faster development
- Safer testing
- Debugging without hardware
The MCU becomes a deployment target — not the development bottleneck.
Lua should not be used when:
- Hard realtime guarantees are required
- Safety certification is involved
- RAM is extremely limited
- The team does not understand memory models
In those cases, a static FSM or DSL is a better choice.
🧩 When Lua on an MCU Is a Very Good Choice
Lua makes sense when:
- Behavior varies per customer
- Core logic is stable
- Power and boot time matter
- Linux is overkill
- Long-term maintainability matters more than raw performance
In these cases:
MCU + RTOS + scripting can outperform Linux in total system cost and complexity.
🧠 Final Thoughts
Running a Lua VM on an MCU is not a hack — but it is also not a default choice.
It requires:
- Deep understanding of memory behavior
- Discipline in API design
- Acceptance of hard constraints
- A clear separation between realtime core and dynamic policy
When done correctly, it enables:
- Highly customizable products
- Fast iteration
- Long-term maintainability
- Significant cost and power savings
✅ Conclusion
Running Lua on an MCU is not about being clever.
It is about choosing flexibility consciously — and paying its cost upfront.
When the limits are understood, Lua can be a powerful tool.
When they are ignored, the system will eventually fail.
Running Lua on MCU platforms without an MMU exposes memory fragmentation issues that are typically hidden on Linux systems.
This post demonstrated how Lua’s garbage collector behaves under constrained memory conditions on Zephyr OS, and why careful allocator design is required for long-running embedded workloads.
🔗 Code References
The following links provide concrete implementation examples referenced throughout this post.
Lua Game / Effect Script (User-Level Allocation Pressure)
This Lua script represents a long-running, stateful workload that continuously allocates and updates Lua-side structures.
Zephyr Heap Wrapper for Lua
A custom memory backend that routes Lua allocations (malloc, realloc, free) into Zephyr’s sys_heap, operating on a fixed-size, flat memory region.
Lua Memory Backend Adaptation
The Lua VM was modified to use the custom allocator correctly, following Lua 5.4’s memory management contract.
Lua memory allocator implementation:
memory.c
Lua auxiliary library (lauxlib.c) allocation path, where custom changes were made to adapt Lua for MCU execution on Zephyr OS:
lauxlib.c (allocation path)