Note: This is a very old personal project I made back when I was learning AVR assembly. Expect rough edges.
Conway's Game of Life running on an ATmega328P, displayed over a composite video signal (PAL, monochrome).
The simulation runs at 120×96 cells and is rendered directly to a TV or monitor via a composite video output generated entirely in software/hardware on the ATmega328P. Timer 1 produces the horizontal sync pulses via its PWM output, while pixel data is bit-banged out on a separate GPIO pin in the Timer 1 overflow ISR.
| Pin | Direction | Function |
|---|---|---|
| PB1 (OC1A) | Output | Sync signal (H/V sync via Timer 1 PWM) |
| PD7 | Output | Pixel data (bit-banged) |
| PD3 | Input (pull-up) | Button — regenerate (randomise VRAM) |
| PD4 | Input (pull-up) | Button — start / stop simulation |
Both buttons are active-LOW (connect to GND when pressed). PB1 and PD7 should be combined through an appropriate resistor network to produce a standard 1 Vpp composite signal.
MCU: ATmega328P @ 16 MHz (e.g. Arduino Uno/Nano)
| Symbol | Address range | Size | Purpose |
|---|---|---|---|
VRAM |
0x0100–0x06A0 |
1440 B | Video RAM — 96 rows × 15 bytes (120 pixels/row, 1 bit/pixel) |
PREV_BUF |
0x06A0–0x06AF |
15 B | Previous row snapshot used during GoL neighbour counting |
RES_BUF |
0x06AF–0x06BE |
15 B | Computed result for the current row |
CUR_BUF |
0x06BE–0x06CD |
15 B | Current row snapshot before it is overwritten |
An additional 64-byte stack region and an 18-byte LUT region are reserved after CUR_BUF.
The output targets the PAL line standard (312 lines per frame).
| Parameter | Value | Description |
|---|---|---|
FRAME_LINES |
312 | Total raster lines per frame |
FIRST_RENDER_LINE |
54 | First raster line with pixel output |
| Last render line | 245 | Pixel output ends (192 active raster lines) |
VSYNC_LENGTH |
75 | Timer 1 OCR1A value during vertical sync |
HSYNC_LENGTH |
942 | Timer 1 OCR1A value during active video / horizontal sync |
Each VRAM row is rendered on two consecutive raster lines, giving 96 unique rows × 2 = 192 displayed lines. The Y pointer rewinds every odd line so the same data is output again.
Pixel timing: each bit is output in ~4 CPU cycles (250 ns at 16 MHz), yielding ~30 µs of active video per line.
Initialises GPIO direction and pull-ups, zeroes working registers, configures Timer 1 in Fast PWM mode (ICR1 = 0x03FF as top), enables the Timer 1 overflow interrupt, seeds VRAM with random data, then falls through to LOOP.
Main polling loop. Pressing PD3 calls REGENERATE to randomise VRAM. Pressing PD4 enters LOOP_GAME, which repeatedly calls GAME to advance the simulation one generation at a time. Pressing PD4 again while in game mode returns to the idle LOOP.
Fires once per raster line. Responsibilities:
- Increments the raster line counter (
LINE_CNT). - Switches
OCR1Ato the H-sync value at line 305, and back to the V-sync value at line 312 (also resets the counter and the VRAM read pointer). - Sets/clears the
DRAWflag to mark the active video window (lines 54–245). - When
DRAWis set, synchronises to the timer, then streams 15 bytes × 8 bits from VRAM to PD7 usingBST/BLD/OUTsequences withNOPpadding for exact pixel timing.
Fills VRAM (0x0100–0x06A0) with pseudo-random bytes by calling PRNG for every byte, then resets the VRAM read pointer.
Generates one pseudo-random byte by:
- Triggering an ADC conversion on an unconnected pin to sample thermal noise.
- XOR-mixing the ADC result with the low byte of Timer 1's counter.
- Multiplying against an incrementing seed register (
PRNGSN).
Advances the entire 120×96 grid by one generation using the following per-row pipeline:
- Snapshot the current row into
CUR_BUF. - Clear
RES_BUF. - For each of the 15 bytes in the row, count the live neighbours for all 8 cells in the byte. Neighbours are accumulated into registers
CELL_0–CELL_7. The current cell state is stored in bit 4 of eachCELLregister; the neighbour count occupies bits 3:0. - After counting, call
CALC_RESULTfor each cell and pack the 8 results into a byte that is stored inRES_BUF. - Copy
CUR_BUF→PREV_BUF, flushRES_BUFback into VRAM, then load the next VRAM row intoCUR_BUF. - Repeat for all 96 rows.
Applies the standard Conway rules using the packed state<<4 | count encoding:
| Input value | Meaning | Next state |
|---|---|---|
0x03 |
Dead cell, 3 live neighbours | Born (alive) |
0x12 |
Live cell, 2 live neighbours | Survives |
0x13 |
Live cell, 3 live neighbours | Survives |
| anything else | — | Dead |
Returns the next state as the T bit (for use with BLD into the result byte).
Computes X = VRAM + LINE_NUM * 15 + BYTE_NUM, pointing into VRAM at the start of the current cell's byte.
Triple-nested busy-wait loop (~0xFF × 0xFF × 0x0F iterations) used to throttle the simulation speed.
Assemble with avra or avr-as targeting the ATmega328P:
avra -I /usr/share/avra main.asmFlash with avrdude:
avrdude -c arduino -p m328p -P /dev/ttyUSB0 -b 115200 -U flash:w:main.hex