Research-Stack/6-Documentation/FPGA_PROGRAMMING_GUIDE.md
Brandon Schneider 48896f8f91 docs: runbook, FPGA programming guide, disaster recovery, API docs
- RUNBOOK.md: k3s/FPGA/Tailscale/GPU/DNS ops procedures
- FPGA_PROGRAMMING_GUIDE.md: SUBLEQ format, memory map, 3 examples
- DISASTER_RECOVERY.md: backup/restore for all components
- API_DOCS.md: dashboard, credential, registry, jobs, blobs APIs
2026-05-29 01:26:41 -05:00

8.2 KiB
Raw Blame History

FPGA Programming Guide — SUBLEQ on the Blitter

Last updated: 2026-05-29 Board: Sipeed Tang Nano 9K (Gowin GW1NR-LV9) CPU: Blitter6502OISC (One Instruction Set Computer — SUBLEQ)


SUBLEQ Instruction Format

The Blitter implements a SUBLEQ (Subtract and Branch if Less-than-or-Equal to Zero) CPU.

Each instruction is 3 words (3 × 16-bit = 6 bytes):

[src] [dst] [next]

Semantics:

mem[dst] = mem[dst] - mem[src]
if mem[dst] <= 0:
    PC = next
else:
    PC = PC + 3

Special addresses:

  • If next == 0 (or next == PC): HALT
  • If src == 0: reads zero (constant source)
  • If dst == IO_ADDR: writes to I/O

Memory Map ($0000$FFFF)

The SUBLEQ address space is 16-bit (64K words). The memory map is divided into regions:

Address Range Size Function
$0000$0FFF 4K words Program + data (BRAM)
$1000$7FFF 28K words Extended data (if available)
$8000$800F 16 words Q16 LUT result registers
$8010 1 word Voltage controller mode
$8011 1 word Scale space parameter
$8020$8025 6 words HiGHS pivot registers
$FF00 1 word UART TX data register
$FF01 1 word UART TX status (bit 0 = busy)
$FF02 1 word UART RX data register
$FF03 1 word UART RX status (bit 0 = data available)
$FFF0 1 word LED output (bits 0-5 = led[0:5])
$FFF1 1 word Button input (bit 0 = user_btn)

Q16 LUT ($8000$8025)

The Q16 LUT is a hardware-accelerated fixed-point arithmetic unit. It operates on Q16.16 values (16-bit integer, 16-bit fraction; total 1.0 = 65536).

Q16 Operations

Write operands to the LUT registers, then read the result:

Address Register Function
$8000 OP_A (lo) Operand A, low word
$8001 OP_A (hi) Operand A, high word
$8002 OP_B (lo) Operand B, low word
$8003 OP_B (hi) Operand B, high word
$8004 OPCODE Operation selector (0-7)
$8008 RESULT (lo) Result, low word
$8009 RESULT (hi) Result, high word

Opcodes

Code Operation Latency
0 A + B 2 cycles (74ns @ 27MHz)
1 A - B 2 cycles
2 A × B 2 cycles
3 A ÷ B 2 cycles
4 √A 2 cycles
5 |A| 2 cycles
6 min(A, B) 2 cycles
7 max(A, B) 2 cycles

Q16.16 Encoding

value = integer_part × 65536 + fraction_part

Examples:
  1.0  = 65536    (0x00010000)
  0.5  = 32768    (0x00008000)
  3.14 = 205887   (0x000323D7)
  -1.0 = -65536   (0xFFFF0000)

Voltage Controller ($8010)

The voltage controller manages BRAM access modes:

Mode Value Description
STORE 0 Direct memory read/write
COMPUTE 1 Q16 LUT computation mode
APPROX 2 Approximate computation (fast)
MORPHIC 3 Morphic field mode
; Set voltage controller to COMPUTE mode
; Write 1 to address $8010

Scale Space ($8011)

The scale space parameter controls Gaussian kernel selection:

Value σ (sigma) Kernel Bank
0 0.25 Bank 0
1 0.50 Bank 1
2 0.75 Bank 2
3 1.00 Bank 3

HiGHS Pivot Registers ($8020$8025)

3-stage simplex pipeline interface for hardware-accelerated LP solving:

Address Register Function
$8020 PIVOT_ROW Row index
$8021 PIVOT_COL Column index
$8022 PIVOT_VAL (lo) Pivot value, low
$8023 PIVOT_VAL (hi) Pivot value, high
$8024 PIVOT_CTRL Control/status
$8025 PIVOT_RESULT Result/iteration count

Example Programs

Blink LED 0 in a loop:

; Program at address 0
; Toggle LED 0 by XOR with 1

; mem[100] = 1 (constant)
; mem[101] = LED address ($FFF0)
; mem[102] = current LED state
; mem[103] = 0 (zero constant)

; Instruction 0: sub 103 from 102, store in 102 (clear 102)
addr 0:  103  102  3     ; mem[102] = mem[102] - mem[103] = 0

; Instruction 3: sub 103 from LED, store in LED (clear LED)
addr 3:  103  2545  6    ; mem[$FFF0] = mem[$FFF0] - 0

; Instruction 6: sub 100 from LED, store in LED (set bit 0)
addr 6:  100  2545  9    ; mem[$FFF0] = mem[$FFF0] - 1

; Instruction 9: delay loop
addr 9:  104  104  12    ; mem[104] = mem[104] - 1
addr 12: 104  104  0     ; if mem[104] <= 0, jump to 0 (restart)

; Data
addr 100: 1              ; toggle mask
addr 101: 0              ; unused
addr 102: 0              ; LED state
addr 103: 0              ; zero
addr 104: 50000           ; delay counter

Assembled binary:

0064 0066 0003
0067 09F1 0006
0064 09F1 0009
0068 0068 000C
0068 0068 0000
0001 0000 0000 0000 C350

2. Q16 Addition

Add two Q16.16 values using the hardware LUT:

; Write operands to Q16 LUT, read result

; mem[200] = operand A = 3.14 (Q16: 205887 = 0x000323D7)
; mem[201] = operand B = 2.72 (Q16: 178258 = 0x0002B8F2)

; Write A low word to $8000
addr 0:  200  32768  3    ; mem[$8000] = mem[200] (A low)

; Write A high word to $8001
addr 3:  201  32769  6    ; mem[$8001] = 0 (A high)

; Write B low word to $8002
addr 6:  202  32770  9    ; mem[$8002] = mem[202] (B low)

; Write B high word to $8003
addr 9:  203  32771  12   ; mem[$8003] = 0 (B high)

; Set opcode to 0 (add)
addr 12: 204  32772  15   ; mem[$8004] = 0

; Read result low from $8008
addr 15: 204  32776  18   ; mem[$8008] -> read

; Store result to mem[210]
addr 18: 32776  210  21   ; mem[210] = result low

; HALT
addr 21: 0  0  0

; Data
addr 200: 23D7  ; A low (3.14)
addr 201: 0003  ; A high
addr 202: B8F2  ; B low (2.72)
addr 203: 0002  ; B high
addr 204: 0000  ; opcode 0 (add)

3. UART Send Character

Send 'A' (0x41) over UART:

; Wait for TX to be not busy, then send character

; mem[300] = 0x41 ('A')
; mem[301] = 0 (zero)
; mem[302] = UART TX status address ($FF01)
; mem[303] = UART TX data address ($FF00)

; Check TX status (poll loop)
addr 0:  302  304  3      ; mem[304] = mem[$FF01]
addr 3:  304  304  6      ; mem[304] -= mem[304] (test if zero)
addr 6:  304  304  9      ; if <= 0 (not busy), continue
addr 9:  301  304  0      ; else, reset and retry

; Send character
addr 12: 300  303  15     ; mem[$FF00] = 0x41

; HALT
addr 15: 0  0  0

; Data
addr 300: 0041   ; 'A'
addr 301: 0000   ; zero
addr 302: FF01   ; UART TX status
addr 303: FF00   ; UART TX data
addr 304: 0000   ; temp

Loading Programs via UART

Using Python

import serial
import struct

# Load assembled program (array of 16-bit words)
program = [0x0064, 0x0066, 0x0003, ...]  # assembled instructions

# Connect to FPGA UART
ser = serial.Serial('/dev/ttyUSB0', 115384, timeout=1)

# Send program: each word as 2 bytes, big-endian
for word in program:
    ser.write(struct.pack('>H', word))

ser.close()

Using openFPGALoader (SRAM load)

For quick iteration (non-persistent):

# Load bitstream to SRAM (lost on power cycle)
openFPGALoader -b tangnano9k --sram research_stack_top.fs

# Load to flash (persistent)
openFPGALoader -b tangnano9k research_stack_top.fs

Reading Results

Via UART

import serial

ser = serial.Serial('/dev/ttyUSB0', 115384, timeout=5)

# Read result bytes
data = ser.read(2)  # 1 word = 2 bytes
result = struct.unpack('>H', data)[0]
print(f"Result: {result} (0x{result:04X})")

ser.close()

Via LEDs

Read the 6 LEDs (pins 10-16) as a 6-bit value from led[0:5].

  • LED 0 = bit 0 (rightmost)

Via Memory Dump

After HALT, the UART TX beacon outputs the full memory contents. Connect a serial terminal and observe the dump.


Build Toolchain

# Synthesis
cd 4-Infrastructure/hardware && bash build_research_stack.sh

# Simulation
cd /tmp/fpga_sim_full && ./obj_dir/sim_top

# Flash
openFPGALoader -b tangnano9k research_stack_top.fs

Tool Versions

Tool Version
Yosys 0.64
nextpnr-himbaechel 0.10-75
gowin_pack latest
Verilator 5.048
openFPGALoader latest

Timing

  • Clock: 27 MHz (37.04 ns period)
  • Achieved Fmax: 195.92 MHz (7.2× margin)
  • Q16 LUT latency: 2 cycles (74 ns)
  • MAX_CYCLES: 1,000,000