Specimen Report
· Elixir
nerves-fun-lab
phiat/nerves-fun-lab
conway's game of life in elixir on an ESP32-C3 - atomvm, a 72×40 pixel OLED, pure elixir on bare metal
- Stars
- ★ 1
- Forks
- ⑂ 0
- Language
- Elixir
- Size
- 106 kB
- Last Push
- 2mo ago
- Forged
- 4mo ago
atomvmconways-game-of-lifeelixirembeddedesp32nervesoled
# JagCanal
Elixir running on ESP32-C3 via AtomVM - exploring embedded Elixir development with Conway's Game of Life on a 0.42" OLED display.

## Features
- **Conway's Game of Life** running at 2-8 FPS on ESP32-C3 @ 160MHz
- **Pure Elixir I2C driver** for SSD1306 OLED (72×40 pixels)
- **Integer-encoded positions** for 2x performance improvement
- **Adaptive frame delay** for smooth animation (targets 8.3 FPS)
- **Performance instrumentation** with real-time metrics via serial
## Quick Start
```bash
# 1. Activate OTP-27 (required for AtomVM)
eval "$(mise activate bash)"
# 2. Install dependencies
mix deps.get
# 3. Flash AtomVM firmware (one-time) - use web flasher:
# https://petermm.github.io/atomvm-web-tools/
# 4. Build and flash your app
mix flash
# 5. Monitor serial output
minicom -D /dev/ttyACM1 -b 115200
```
> **WSL Users**: Run `usbipd attach --wsl --busid <BUSID>` in PowerShell first.
> See [docs/networking.md](docs/networking.md) for WSL USB setup.
## Hardware
- **Board**: ESP32-C3 Supermini with integrated 0.42" OLED
- **MCU**: ESP32-C3 @ 160MHz (single-core RISC-V)
- **RAM**: ~320KB usable
- **Display**: SSD1306 72×40 pixels, I2C @ 400kHz
- **LED**: Built-in on GPIO 8
### Pinout
| Function | GPIO |
|----------|------|
| OLED SDA | 5 |
| OLED SCL | 6 |
| Built-in LED | 8 |
| I2C Address | 0x3C |
## Performance
**Current Performance** (32×16 resolution, integer encoding):
| Cells | step() | render | flush | total | **FPS** |
|-------|--------|--------|-------|-------|---------|
| 18 | 41ms | 7ms | 45ms | 93ms | **8.3** |
| 37 | 111ms | 18ms | 53ms | 182ms | **5.5** |
| 75 | 323ms | 48ms | 57ms | 428ms | **2.3** |
**Optimizations Applied:**
- Integer position encoding (`x + y*32` vs `{x,y}` tuples): **2x speedup**
- Map-based neighbor counting (vs merge sort): **2x speedup**
- Adaptive frame delay: **Smooth 8.3 FPS** when population < 30 cells
- Reduced resolution (32×16 vs 36×20): Less computation overhead
**Bottleneck:** `step()` is 65% of frame time (neighbor counting + rule application)
See [docs/oled.md](docs/oled.md) for detailed performance analysis.
## Development
### Prerequisites
**1. Erlang/Elixir with OTP-27**
AtomVM v0.6.6 requires OTP-27 (not OTP-28). Use mise to manage versions:
```bash
mise use erlang@27.3.4.6 elixir@1.19.5-otp-27
eval "$(mise activate bash)"
```
**2. esptool**
```bash
uv tool install esptool
```
**3. WSL USB Passthrough (Windows only)**
```powershell
# In Windows PowerShell (admin):
usbipd bind --busid <BUSID>
usbipd attach --wsl --busid <BUSID>
```
### Build & Flash
```bash
# Ensure OTP-27 is active (CRITICAL for AtomVM!)
eval "$(mise activate bash)"
mise use erlang@27
# Clean build and flash
mix clean && mix flash
# Or manual steps:
mix compile
mix atomvm.packbeam
esptool --chip esp32c3 --port /dev/ttyACM1 --baud 921600 \
write-flash 0x250000 jag_canal.avm
```
### Performance Testing
**Capture Real Device Metrics:**
```bash
# Capture serial output with timing data
minicom -b 115200 -D /dev/ttyACM1 -C perf_output.txt -o
# Parse and analyze (after 30+ seconds of capture)
./scripts/parse_device_perf.exs perf_output.txt
```
**⚠️ Important:** Do NOT use `mix test` for performance benchmarks - it runs on the host CPU (x86) and gives misleading results. Always use real device metrics from serial output.
### Serial Debugging
```bash
# Using minicom (recommended for capture)
minicom -b 115200 -D /dev/ttyACM1 -C output.txt -o
# Simple output (Ctrl-C to exit)
timeout 30 cat /dev/ttyACM1
# Using screen
screen /dev/ttyACM1 115200
```
**⚠️ Never use `stty`** - minicom handles all serial setup automatically.
Press RESET button on the board to see boot messages.
## Project Structure
```
jag-canal/
├── lib/
│ ├── jag_canal.ex # Main application entry point
│ ├── game_of_life.ex # Conway's Game of Life (32×16, integer encoding)
│ ├── ssd1306.ex # Pure Elixir I2C driver for SSD1306 OLED
│ └── mix/tasks/
│ ├── flash.ex # Mix task: flash to device
│ └── setup.ex # Mix task: setup script
├── scripts/
│ ├── parse_device_perf.exs # Parse ESP32-C3 serial metrics
│ └── analyze_perf.exs # CSV performance analysis
├── docs/
│ ├── oled.md # Display specs & performance analysis
│ ├── networking.md # ESP32-C3 networking setup
│ ├── flashing.md # Flashing guide
│ ├── game-of-life-optimization-research.md
│ ├── gol-32x16-optimizations.md
│ ├── gol-hotloop.md # Hot loop optimization notes
│ └── gol-nif.md # NIF optimization research
├── AGENTS.md # Claude Code agent instructions
├── mix.exs # Project config with AtomVM settings
└── mise.toml # Erlang/Elixir version pinning (OTP-27!)
```
## AtomVM Development Notes
### ⚠️ ALWAYS Verify with atomvm-guru
Many Elixir stdlib functions crash **silently** on AtomVM:
- ❌ `Enum.sort`, `Enum.take`, `:lists.sort`, `:lists.sublist`, `:lists.member`
- ✅ Use manual implementations or `:maps` module functions
**Before flashing new code**, always verify AtomVM compatibility:
```bash
# Use atomvm-guru agent in Claude Code
# Or manually check: https://doc.atomvm.org/main/apiguide.html
```
### Performance Characteristics
- **AtomVM interpreter overhead**: ~100-1000 cycles per operation
- **Prefer Map operations** (C implementation) over recursive sorts
- **Each function call** has pattern matching + dispatch overhead
- **:counters module** is NOT available (no mutable arrays)
- **Process dictionary** works but use sparingly
## Game of Life Implementation
**Current Algorithm:**
1. **Integer position encoding**: Each cell is `x + y*32` (single integer)
2. **Map-based neighbor counting**: Direct counting (no sorting)
3. **Precomputed wrapping**: Inline modulo for edge wrap
4. **Adaptive frame delay**: Targets 120ms per frame (8.3 FPS)
**Key Code Locations:**
- `lib/game_of_life.ex:64-124` - Core `step()` algorithm
- `lib/game_of_life.ex:145-195` - Main game loop with timing
- `lib/ssd1306.ex:288-307` - Fast 2×2 block rendering
## Troubleshooting
### "Code compiled with OTP-28 is not supported"
**CRITICAL:** AtomVM v0.6.6 only supports OTP-27.
```bash
eval "$(mise activate bash)"
mise use erlang@27
mix clean && mix flash
```
### Silent crashes / No serial output
Likely using unsupported stdlib function. Check:
1. Are you using `Enum.sort`, `Enum.take`, `:lists.sort`?
2. Did you verify with atomvm-guru agent?
3. Press RESET button to see boot messages
### "Failed to connect to ESP32-C3"
- Use **direct USB connection** (not through a hub)
- Re-attach USB in WSL: `usbipd attach --wsl --busid <BUSID>`
- Try `/dev/ttyACM1` instead of `/dev/ttyACM0`
- Check with `ls -la /dev/ttyACM*`
### Performance is slow or choppy
1. Capture real metrics: `minicom -b 115200 -D /dev/ttyACM1 -C output.txt -o`
2. Analyze: `./scripts/parse_device_perf.exs output.txt`
3. Check if step() time varies widely (population-dependent)
4. See remaining optimization tasks: `bd ready` (beads issue tracker)
## Resources
- [AtomVM Documentation](https://doc.atomvm.org/)
- [AtomVM GitHub](https://github.com/atomvm/AtomVM)
- [ExAtomVM](https://github.com/atomvm/exatomvm)
- [ESP32-C3 Supermini Pinout](https://www.espboards.dev/esp32/esp32-c3-oled-042/)
- [SSD1306 Datasheet](https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf)
## Contributing
This project uses **beads (bd)** for issue tracking:
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --status in_progress # Claim work
bd close <id> # Complete work
```
See [AGENTS.md](AGENTS.md) for Claude Code agent instructions and project-specific notes.
## License
MIT