
DDDDDDDD
DDDDDDDDDDDD
DDDDDDDDDDDDDDD
DDDDDDDDDDDDDDDDDD
DDDDDDDDDDDDDDDDDDDD
DDDDDDDDDDDDDDDDDDDDD
DDDDDDDDDDDDDDDDDDDDDD
DDDDDDDDDDDDDDDDDDDDDD
DDDDDDDDDDDDDDDDDDDDDD GGGGGGGGGGGGGGGGGGGGGG
DDDDDDDDDDDDDDDDDDDDDD GGGGGGGGGGGGGGGGGGGGGG
DDDDDDDDDDDDDDDDDDDDD GGGGGGGGGGGGGGGGGGGGG
DDDDDDDDDDDDDDDDDDDD GGGGGGGGGGGGGGGGGGGG
DDDDDDDDDDDDDDDDDD GGGGGGGGGGGGGGGGGG
DDDDDDDDDDDDDDD GGGGGGGGGGGGGGG
DDDDDDDDDDDD GGGGGGGGGGGG
DDDDDDDD GGGGGGGG X4
This is my first tapeout, and I decided to challenge myself with trying to build a single-tile 16-bit Data General Nova CPU in Verilog, based on the minicomputer architecture used by the Nova 1200. While fitting a 16-bit CPU on a single tapeout tile is ambitious, I believed it could be possible with some adjustments due to a 4-bit nibble-serial datapath used for the ALU.
A little bit of history: Data General was founded in 1968 by four engineers, three of whom were former employees of Digital Equipment Corporation (DEC), the company famous for minicomputers like the PDP-8. DEC had expressed no interest in moving from 12-bit to 16-bit architectures at the time, so three engineers left to start Data General and build the new design themselves. They went through years of commercial success for their 16-bit machines, then later developed the 16-bit (later 32-bit) Eclipse: the intense development process of the 32-bit Eclipse MV/8000 became the inspiration for the Pulitzer Prize-winning Tracy Kidder book, The Soul of a New Machine. They also helped define the future of portable laptop computers with the Data General/One in 1984.
A few modifications were necessary to fit the design on a single tile. The main difference from the original Nova is that AC0-AC3 were reduced to AC0-AC1, as the reality of four 16-bit accumulators plus multiplexing logic was simply too much for a single tile. This doesn't actually have much impact outside of some software compatibility, as the extra registers aren't used very often in simple programs and I was able to easily modify JSR to store PC in AC1 rather than AC3. The other architecture difference is the exclusion of the memory reference operations ISZ (Increment and Skip if Zero) and DSZ (Decrement and Skip if Zero). I otherwise tried to stay as close to the original Nova architecture as possible.

Tile Utilization: 88.614%
The Tiny Tapeout Nova CPU is fully programmable assuming a QSPI PMOD is being used with the demoboard. Full details on flashing the CPU and testing can be found below, in the "How to test" section. At a high level, the Nova CPU continuously fetches instructions from Flash, decodes them, and executes them:
AC0 and AC1). Temporary data and variables are stored in PSRAM. Much like the original Nova 1200, the Tiny Tapeout implementation of the CPU processes 16-bit math 4 bits at a time.ui_in[0] (RX) and uo_out[0] (TX). This allows for interactive terminal I/O, which the example program below demonstrates.uo_out[7:1]), using the demoboard's 7-segment display to show the CPU's major cycle, active instruction class, UART send/receive status, and halt status.Das Blinkenlights!
uo[7] uo[6],uo[5],uo[4] uo[3],uo[2],uo[1] uo[0]
----------------------------------------------------------------
| HALTED | IR_OP[2:0] | STATE[2:0] | UART_TX |
| (Halt) | (Instruction) | (Major Cycle) | (Serial) |
----------------------------------------------------------------

Nova programs are stored in Flash. Words are stored in big-endian format. I have included a Python script so it's possible to write Nova assembly, and the full Nova instruction set is given below. (I highly recommend sharing your Nova programs on Discord or GitHub if you create something cool! I haven't tried writing DOOM yet.)
You can assemble your Nova code using the self-contained make_rom.py script:
#!/usr/bin/env python3
"""
make_rom.py
DG Nova assembly -> big-endian binary (rom.bin)
Copyright (c) 2026 X4NTHA, x4ntha.com
SPDX-License-Identifier: Apache-2.0
"""
import re, sys
# demo assembly program: interactive UART echo
ASM_SOURCE = """
; interactive UART echo
START: SKPDN 010 ; check if char was received on UART RX (Dev 0o10)
JMP START ; wait for char
DIAC 0, 010 ; read char into AC0 (clears RX done flag)
WAIT: SKPBZ 011 ; wait until UART TX (Dev 0o11) is ready
JMP WAIT ; wait
DOAS 0, 011 ; echo char back out via UART TX (with start pulse)
JMP START ; loop indefinitely for next char
"""
def assemble_nova(source_text):
"""
assembles Nova asm into a list of 16-bit words
"""
lines = source_text.strip().split('\n')
rom = [0] * 16384 # 16K words, 32 KB
labels = {}
cleaned = []
pc = 0
def parse_string_bytes(s):
m = re.search(r'"([^"\\]*(?:\\.[^"\\]*)*)"', s)
if not m: return []
content = m.group(1).encode('utf-8').decode('unicode_escape')
byte_list = [ord(c) for c in content] + [0]
words = []
for i in range(0, len(byte_list), 2):
b_hi = byte_list[i]
b_lo = byte_list[i+1] if (i+1 < len(byte_list)) else 0
words.append((b_hi << 8) | b_lo)
return words
# pass 1: strip comments, extract labels, map PC
for raw in lines:
line = re.sub(r'[;#].*