Introduction
Recently, I attended SWEST28, a summer workshop focused on embedded systems technologies.
https://swest.toppers.jp/phx/event
At the event, I joined a hands-on embedded development workshop using UIAPduino, led by Yuuki Umeta, the developer of UIAP.
During the workshop, we used the UIAPduino Pro Micro CH32V003. I also received a UIAPduino Pro Micro CH32V006, which is still under development.
After SWEST28, I first built a small game using the CH32V003 and shared it with the UIAP and Elixir communities, as well as other SWEST participants.
During one of our casual conversations, someone said:
It would be interesting if AtomVM could run on UIAPduino.
I use Elixir and AtomVM regularly, but I am not deeply familiar with AtomVM internals or the low-level details of the CH32V006.
Still, I was curious, so I decided to experiment with it while asking ChatGPT and Codex to help with research and implementation.
After some trial and error, I was able to run Elixir code on the CH32V006 and blink an LED.
It is still experimental, but it worked.
What is UIAP?
UIAPduino is a small and inexpensive Arduino-compatible development board.
https://www.uiap.jp/uiapduino/
For comparison, here are two UIAPduino boards alongside the XIAO ESP32S3.
| Board | Flash | SRAM |
|---|---|---|
| UIAPduino Pro Micro CH32V003 | 16KB | 2KB |
| UIAPduino Pro Micro CH32V006 | 64KB | 8KB |
| XIAO ESP32S3 | 8MB | 512KB |
Looking at the numbers makes it clear that the CH32V006 is still a very small microcontroller.
However, it has more room than the CH32V003.
That made me wonder whether AtomVM might fit if we reduced its functionality to the bare minimum.
What is AtomVM?
AtomVM is a lightweight virtual machine designed to run Elixir and Erlang code on microcontrollers.
It can run in embedded environments such as ESP32 and is designed to work with limited Flash and RAM.
Making AtomVM Even Smaller for the CH32V006
The CH32V006 has only 64KB of Flash and 8KB of SRAM.
That is a very constrained environment for running AtomVM.
Instead of trying to run a normal AtomVM build unchanged, I created a configuration where the available functionality is reduced to the minimum required for this experiment.
The initial goal was simply:
Boot AtomVM
↓
Run Elixir code
↓
Blink an LED using GPIO
For this reason, the AtomVM build used in this experiment has many features disabled.
Developing Together with LLMs
Because I am not deeply familiar with AtomVM internals or the CH32V006, I worked with ChatGPT and Codex while investigating and implementing the port.
I repeatedly went through:
Research
↓
Implementation
↓
Build
↓
Measurement
↓
Hardware testing
Along the way, I also kept notes and ADRs describing what I tried and why I made certain decisions.
By checking each step individually, I was able to gradually make progress in an area that was initially unfamiliar to me.
Current Results
The current implementation is available on this working branch:
https://github.com/mnishiguchi/AtomVM/tree/feature/ch32v006/src/platforms/ch32v006
So far, I have confirmed the following on real hardware:
Boot AtomVM
↓
Run Elixir code
↓
Control GPIO
↓
Blink an LED
It is still experimental, but I have confirmed that a minimal AtomVM configuration can run on the CH32V006.
Blinking an LED
From here, I will walk through how I ran AtomVM on the CH32V006 and blinked an LED using Elixir.
The following steps assume a Linux environment.
1. Prepare the Development Environment
The main tools I used were:
Erlang/OTP 28
Elixir 1.19
GNU Make
RISC-V GCC
RISC-V binutils
newlib
On my machine, I checked them with:
erl -noshell \
-eval 'io:format("OTP ~s~n", [erlang:system_info(otp_release)]), halt().'
elixir --version
make --version
riscv64-unknown-elf-gcc --version
riscv64-unknown-elf-readelf --version
2. Prepare ch32fun and minichlink
For the CH32V006 platform layer, I use ch32fun.
For this experiment, I pinned the revision that I had tested successfully.
git clone https://github.com/cnlohr/ch32fun.git /tmp/ch32fun
git -C /tmp/ch32fun checkout \
618bba58c615ed29dc99e6ea92d869c914b6a8c0
I also built minichlink, which is used for flashing the CH32V006 and viewing terminal output.
make -C /tmp/ch32fun/minichlink
After connecting the UIAPduino over USB, it appeared like this on my machine:
lsusb | grep -i 1209:b806
ID 1209:b806 Generic CNLohr RV003 RVSWDIO Programmer Customized for UIAPduino
3. Build AtomVM First
Before running Elixir, I first built the minimal blink example included in the repository.
Run this from the root of the AtomVM repository:
make -C src/platforms/ch32v006 \
CH32FUN=/tmp/ch32fun/ch32fun \
clean image
The firmware build roughly follows this flow:
Erlang
↓
BEAM
↓
Ahead-of-time compilation for RISC-V
↓
Link with AtomVM
↓
Firmware
On my machine, the build produced:
Validated build/ch32v006_minimal.beam: 1096 bytes of rv32ec_zmmul code
The Flash usage was:
Verified atomvm_ch32v006.bin: 56200 / 63488 bytes (7288 bytes free)
Only about 62KB is available to the firmware, so this is already quite tight.
4. Flash the CH32V006
Next, flash the generated firmware to the board.
make -C src/platforms/ch32v006 \
CH32FUN=/tmp/ch32fun/ch32fun \
MINICHLINK=/tmp/ch32fun/minichlink \
FLASH_IMAGE=build/images/AtomVM-uiapduino-pro-micro-ch32v006.bin \
flash-uiap
When the write succeeds, the output includes:
Detected CH32V006
Writing image
Image written.
At this point, the orange LED on the board repeats this pattern:
100ms ON
900ms OFF
This confirmed that AtomVM itself was running on the physical CH32V006.
5. Create an Elixir Blink Application
Next, I created an actual Elixir application.
Create a Mix project outside the AtomVM repository.
mix new uiap_blink --module UIAPBlink
cd uiap_blink
Add ExAtomVM to mix.exs.
def project do
[
app: :uiap_blink,
version: "0.1.0",
elixir: "~> 1.19",
deps: deps(),
atomvm: [start: UIAPBlink]
]
end
defp deps do
[
{:exatomvm,
git: "https://github.com/atomvm/ExAtomVM",
branch: "main",
runtime: false}
]
end
I used the following lib/uiap_blink.ex.
defmodule UIAPBlink do
@compile {:no_warn_undefined, [:gpio, :ch32v006]}
@led 35
def start do
:gpio.init(@led)
:gpio.set_pin_mode(@led, :output)
loop(:high)
end
defp loop(level) do
:gpio.digital_write(@led, level)
:ch32v006.delay_ms(500)
loop(toggle(level))
end
defp toggle(:high), do: :low
defp toggle(:low), do: :high
end
The logic is simple.
Turn the LED on
↓
Wait 500ms
↓
Turn the LED off
↓
Wait 500ms
↓
Repeat
Compile the application.
mix deps.get
mix compile
This creates:
_build/dev/lib/uiap_blink/ebin/Elixir.UIAPBlink.beam
6. Build Firmware Containing the Elixir Application
For this CH32V006 port, I do not use mix atomvm.packbeam to create an .avm archive.
Instead, the BEAM produced by mix compile is embedded directly into the AtomVM firmware.
Elixir
↓
BEAM
↓
Ahead-of-time compilation for RISC-V
↓
Link with AtomVM
↓
Firmware
Return to the AtomVM repository and build the firmware while specifying the BEAM file.
make -C src/platforms/ch32v006 \
CH32FUN=/tmp/ch32fun/ch32fun \
START_BEAM_INPUT=/path/to/uiap_blink/_build/dev/lib/uiap_blink/ebin/Elixir.UIAPBlink.beam \
IMAGE_BASENAME=AtomVM-uiapduino-pro-micro-ch32v006-uiap-blink \
image
On my machine, this produced:
Validated build/Elixir.UIAPBlink.beam: 2824 bytes of rv32ec_zmmul code
The complete firmware size was:
Verified atomvm_ch32v006.bin: 58672 / 63488 bytes (4816 bytes free)
Here is the comparison.
| Application | Flash usage |
|---|---|
| Minimal Erlang blink | 56,200 bytes |
| Elixir blink | 58,672 bytes |
The Elixir version leaves only 4,816 bytes free.
It is a good reminder of just how small this environment is.
7. Flash the Elixir Firmware
Now flash the firmware containing the Elixir application.
make -C src/platforms/ch32v006 \
CH32FUN=/tmp/ch32fun/ch32fun \
MINICHLINK=/tmp/ch32fun/minichlink \
FLASH_IMAGE=build/images/AtomVM-uiapduino-pro-micro-ch32v006-uiap-blink.bin \
flash-uiap
After flashing, the LED pattern changed from:
100ms ON
900ms OFF
to:
500ms ON
500ms OFF
This confirmed that my Elixir code was actually running on the CH32V006.
8. Confirm That AtomVM Boots
Finally, I checked the AtomVM boot message over SWIO.
make -C src/platforms/ch32v006 \
CH32FUN=/tmp/ch32fun/ch32fun \
MINICHLINK=/tmp/ch32fun/minichlink \
monitor-uiap
The board printed:
Terminal started
AVM CH32V006 boot
At this point, the complete path had been verified on real hardware:
Elixir
↓
BEAM
↓
AtomVM
↓
Ahead-of-time compilation for RISC-V
↓
CH32V006
↓
GPIO
↓
Blinking LED
What I Learned
This experiment confirmed that even on a very small microcontroller such as the CH32V006, Elixir code can run if AtomVM is reduced to the minimum functionality required.
For me, the most interesting result was confirming this entire path on real hardware:
Elixir
↓
BEAM
↓
AtomVM
↓
RISC-V
↓
CH32V006
Of course, this is not yet the same experience as using a normal AtomVM environment.
Still, running AtomVM at all within only 64KB of Flash and 8KB of SRAM shows that there is room for further exploration.
Current Limitations
The CH32V006 AtomVM port is still experimental.
At the moment, there are several important limitations:
- Very little Flash or RAM remains available
- Many AtomVM features have been disabled
- The startup BEAM is embedded directly into the firmware
- Many features normally available in AtomVM are not yet supported
- The entire firmware must be rebuilt whenever the application changes
For this reason, it cannot yet be used in the same way as AtomVM on ESP32.
At the moment, I am mainly exploring how small AtomVM can become while still remaining useful on the CH32V006.
What's Next
Next, I would like to gradually test more basic hardware features beyond blinking an LED.
For example:
- GPIO input
- Buttons
- UART
- ADC
- I2C
- SPI
I am also interested in seeing how much Elixir-style code can realistically fit within such limited Flash and RAM.
Rather than thinking of the CH32V006 as simply a smaller ESP32, I would like to explore AtomVM use cases that make sense specifically because the environment is this small.
Conclusion
In this experiment, I was able to run a minimal AtomVM configuration on the UIAPduino Pro Micro CH32V006 and confirm an Elixir-based LED blink on real hardware.
I am not an expert in AtomVM internals or the CH32V006, but by working with LLMs and going through research, implementation, measurement, and hardware verification one step at a time, I was able to get this far.
The experiment started from encountering UIAPduino at SWEST28 and from a casual conversation afterward in the community.
That small idea eventually led to running Elixir on the CH32V006.
It is still experimental, but now that I know AtomVM can run on the CH32V006, I would like to keep exploring what is possible in this very small environment.
Top comments (0)