A soft-core FPGA replica of the Saab CK37 — the world's first airborne computer built with integrated circuits.
The CK37 was the central navigation and fire control computer of the Saab 37 Viggen, Sweden's Mach 2 multirole fighter. First flown in 1963, it was the first computer in the world to use first-generation integrated circuits in an airborne application. Nearly 200 units were delivered between 1970 and 1978. Some remained in active service until the early 2000s.
This project reconstructs the CK37 as a working FPGA soft-core, assembled from primary sources — including the official FMV maintenance manual Beskrivning CK37, Del 1 (1985-11-01) and Bengt Jiewertz's original design documentation. The goal is a functionally accurate replica: correct instruction set, correct number representation, correct I/O architecture — running original-class navigation software.
The end target is a fixed-wing drone with a CK37 soft-core as its navigation computer. The same algorithms that guided a Viggen at Mach 1.1 and 30 metres AGL are more than sufficient for autonomous flight.
| Component | Status |
|---|---|
| CPU core (Verilog) | All bugs fixed — 25/25 tests passing |
| Instruction encoding | Confirmed from FMV manual Bild 12/13 |
| Memory model | VM/PM/EEPROM/PROM regions |
| Interrupt system | 6-level, vectors from Tabell 3 |
| Clock interrupts | 2.864ms + 34.37ms generators |
| Analogue I/O bridge | Memory-mapped, channel table confirmed |
| Digital I/O | Synthesisable (packed arrays, no dynamic range selects) |
| Assembler | Octal, Q1.25, full 26-bit encoding |
| Navigation kernel | 198 words — IRQ4/IRQ5 handlers, PID nav loop |
| FPGA synthesis | Clean — 1,978 LUT4, 12/12 EBR, ECP5-25F |
| Place and route | 85 MHz max (10 MHz target) — 8.5× timing margin |
| Hardware bring-up | Pending OrangeCrab flash |
The CK37 is a 26-bit fixed-point fractional computer (Q1.25 format), single-accumulator, with a 15-bit address space spanning up to 18432 words across four memory regions.
| Parameter | Value | Source |
|---|---|---|
| Word length | 26 bits + 2 parity | FMV Del 1, Sida 13 |
| Number format | Q1.25 signed fraction, range [−1, +1) | Sida 12 |
| Number base | Octal (10 octal digits per word) | Sida 12 |
| Clock | 2.860 MHz | Appendix 2 |
| Instructions | 48 basic, 4 formats | Bild 11–14 |
| Interrupts | 6 priority levels | Tabell 3 |
| Analogue I/O | 32 in + 32 out, sampled every 17ms | Sida 16 |
| Digital I/O | 450 binary signals | Appendix 3 |
| Multiply time | 23.8 µs (two-shift method) | Appendix 2 |
| MTBF | >200 hours flight operation | Appendix 3 |
000000 – 003777 VM Variable Memory (RAM, read/write)
100000 – 177777 PM Permanent Memory (ROM, write-protected)
204000 – 314000 EEPROM
400000 – 404000 PROM (interrupt vectors)
In the 15-bit CPU address space: PM base = 15'h1000 (4096 decimal). PROM vectors at 0x4000 + level*2.
Four formats confirmed from FMV manual Bild 11–14:
Halvords enadress [13 bits] pos 00-12
[0] [opcode 4b] [address 8b]
Helords enadress [26 bits] pos 00-25
[opcode 13b, pos 00-12] [address 13b, pos 13-25]
Helordsorder med operand [26 bits]
[opcode 10b] [irrel 3b] [operand sign] [operand 12b]
Helords tvåadress [26 bits] — GOTOPR only
[opcode 3b] [L address 10b] [M address 13b]
Stores return address at L, jumps to M
Bit layout: position 00 = MSB = IR[25], position 25 = LSB = IR[0].
IR[25] = 1 distinguishes full-word (26-bit) from half-word (13-bit) instructions.
All addresses octal:
| Level | Type | Store address | Jump address |
|---|---|---|---|
| 0 | Power off | 000000 | 400000 |
| 1 | Power on | 000002 | 400002 |
| 2 | Parity / write error | 000004 | 400004 |
| 3 | Program load | 000006 | 400006 |
| 4 | Clock 2.864 ms (~350 Hz) | 000010 | 400010 |
| 5 | Clock 34.37 ms (~29 Hz) | 000012 | 400012 |
The 2.864ms clock interrupt (8191 cycles at 2.860MHz — exactly 2¹³−1) drives the fast navigation update loop. The 34.37ms interrupt drives position integration and display updates.
ck37-core/
├── rtl/
│ ├── ck37_cpu.v CPU core — 26-bit, Q1.25, 48-instruction ISA
│ ├── ck37_mem.v Memory — VM/PM/EEPROM/PROM, parity-guarded
│ ├── ck37_aio.v Analogue I/O bridge (32+32 channels, SPI)
│ ├── ck37_dio.v Digital I/O (450 bits, 35×13-bit words)
│ ├── ck37_clk_irq.v Clock interrupt generator (IRQ4 + IRQ5)
│ └── ck37_top.v Integration top-level (memory bus arbitration)
├── syn/
│ ├── ck37_syn_top.v Synthesis wrapper — OrangeCrab r0.2 (ECP5-25F)
│ ├── ck37_orangecrab.lpf Pin constraints (LED/UART)
│ └── ck37_orangecrab_unconstrained.lpf Timing-only variant
├── sim/
│ └── nav_tb.v Navigation kernel testbench — 25/25 passing
├── asm/
│ ├── ck37asm.py Assembler — octal, Q1.25, full 26-bit encoding
│ └── gen_labels.py Regenerates nav/nav_labels.vh from kernel
├── nav/
│ ├── nav_kernel.asm Navigation kernel (198 words, CK37 assembly)
│ ├── nav_kernel.hex Assembled ROM image ($readmemh format)
│ ├── nav_kernel.lst Listing with addresses and encoding
│ └── nav_labels.vh Auto-generated handler addresses (Verilog include)
├── docs/
│ ├── ISA_v2.md Full ISA specification with manual corrections
│ ├── confirmed_architecture.md
│ └── instruction_encoding.md
├── Makefile sim / labels / synth / pnr / bit / flash
├── README.md
└── RUNBOOK_NO_HARDWARE.md
# Requires iverilog
sudo apt install iverilog
make sim
# Assembles nav_kernel.asm → nav_kernel.hex, then runs nav_tb.v
# Expected: ╔══╗ RESULTS: 25 passed, 0 failed ╚══╝# Requires: yosys, nextpnr-ecp5, fpga-trellis, dfu-util
sudo apt install yosys nextpnr-ecp5 fpga-trellis dfu-util
make synth # → build/ck37.json
make pnr # → build/ck37_routed.config
make bit # → build/ck37.bit
make flash # DFU flash to OrangeCrab r0.2See RUNBOOK_NO_HARDWARE.md for step-by-step instructions with expected output at each stage.
python3 asm/ck37asm.py nav/nav_kernel.asm --memh -o nav/nav_kernel.hex
python3 asm/gen_labels.py # regenerates nav/nav_labels.vhTarget: LFE5U-25F-8MG285C (OrangeCrab r0.2)
Tool: Yosys 0.52 (ABC9) + nextpnr-ecp5 0.6
Resource utilisation:
LUT4: 1,978 / 24,288 ( 8%)
TRELLIS_FF: 386 / 24,288 ( 1%)
DP16KD: 12 / 12 (100% — exactly fits ECP5-25F)
CCU2C: 264 (carry chains)
Timing (post-route, speed grade 8):
ck37_clk max: 85.46 MHz PASS at 10.00 MHz (8.5× margin)
clk_48m max: 435.54 MHz PASS at 48.00 MHz
7 warnings, 0 errors
Clock divider: 48 MHz ÷ 17 ≈ 2.82 MHz (target: 2.860 MHz)
Note: integer divider gives 98.7% of target frequency.
A fractional PLL can hit 2.860 MHz exactly for production.
The BRAM fit is exact — 8192 × 26-bit data memory maps to exactly 12 × 18Kbit EBR blocks. Parity checking is active in simulation (ifndef SYNTHESIS) and stubbed to zero on silicon to free the 13th block.
1. Flash via DFU
# Hold BTN0 while plugging USB to enter DFU bootloader
sudo apt install dfu-util
make flash2. Verify boot on hardware
On power-up the CPU boots at PM_BASE (0x1000), loads gain constants, runs self-test, then enters the IRQ-driven nav loop:
led_g(green) heartbeats at ~1 Hz — CPU is runningled_r(red) stays off — not haltedled_b(blue) stays off — no parity errors
3. UART debug transmitter (optional)
uart_tx is currently held high (idle). Implement a 57600 8N1 transmitter in syn/ck37_syn_top.v to stream the AR register value each IRQ4 cycle.
4. Wire up ADC/DAC SPI
Connect ADS8688A (ADC) and DAC8568 (DAC) to the GPIO pins and restore the LPF constraints. The AIO module (rtl/ck37_aio.v) is already written; it just needs the SPI physical wiring.
All bugs found and fixed across the development cycle:
| # | Module | Bug |
|---|---|---|
| 1 | asm | .ORG 0100000 overflow → corrected to 010000 |
| 2 | asm | First-pass PC not incremented for .FLOAT/.WORD → all ROM constants resolved to same address |
| 3 | asm | Short-form (halvord) encoding placed bits in wrong positions → disabled, always full 26-bit |
| 4 | cpu | GOTOPR not detected — opcodes embedded in IR[25:23], masked by casez fall-through |
| 5 | cpu | SC+ halvord: value placed in AR[12:0] instead of AR[25:13] |
| 6 | cpu | S= halvord: symmetric placement error |
| 7 | cpu | AND/ANDOP/OR/OROP: operated on AR[12:0] instead of AR[25:13] |
| 8 | cpu | ABSOP: subtracted from lower half instead of upper half |
| 9 | cpu | C+OP/+OP/-OP/S+/S-/SIGNOP: same upper/lower swap |
| 10 | cpu | MUL: treated AR as unsigned — negative AR gave wrong product |
| 11 | cpu | INT_EN reset to 0 — interrupts never fired |
| 12 | cpu | INT_EN cleared in S_INTR — interrupts fired once then stopped |
| 13 | kernel | SELF_TEST and SAVE_BOOT at same address (000055) — INDGOTO jumped to PC=0 |
| 14 | kernel | SPD_TARGET never loaded from ROM at boot |
| 15 | kernel | ONE_HALF never loaded from ROM at boot |
| 16 | kernel | AU_THR init used SC+ before S= — SC+ overwrote AR |
| 17 | kernel | ERR_HDG/ERR_ALT only computed in IRQ5 — IRQ4 used stale values |
| 18 | rtl/dio | Unpacked array ports not synthesisable by Yosys → flattened to packed vectors |
| 19 | rtl/dio | For-loop reset with variable index on packed vector → replaced with direct zero assign |
| 20 | rtl/dio | bi01_shadow double sequential assignment → merged into single concatenation |
| 21 | rtl/dio | Double-slice bu_data[N:M][P:Q] → replaced with flat computed slice |
| 22 | rtl/mem | Parity logic not guarded → 13 EBR needed vs 12 available; guarded with `ifndef SYNTHESIS |
| 23 | syn | Async set+reset FFs (initial values + async reset) → removed initialisers, reset-only blocks |
The nav kernel (nav/nav_kernel.asm, 198 words at PM_BASE=0x1000) implements a full PID navigation loop:
IRQ4 handler (2.864ms fast loop):
- Reads sensors: PITCH, ROLL, YAW, AIRSPEED, BARO, BEARING
- Computes heading error (
ERR_HDG) and altitude error (ERR_ALT) fresh every cycle - Calls
ALT_CONTROL,HDG_CONTROL,SPEED_CONTROLviaGOTOPR - Writes actuator commands:
CMD_AIL,CMD_ELEV,CMD_RUD,CMD_THR
IRQ5 handler (34.37ms slow loop):
- Reads GPS velocity (VEL_N, VEL_E, VEL_D)
- Integrates position: POS_N, POS_E, POS_ALT += VEL × DT_SLOW
Handler addresses (current assembly):
| Label | Address (hex) | Octal |
|---|---|---|
| BOOT | 0x1000 | 010000 |
| IRQ4_HANDLER | 0x102F | 010057 |
| IRQ5_HANDLER | 0x104D | 010115 |
| ALT_CONTROL | 0x1069 | 010151 |
| HDG_CONTROL | 0x1079 | 010171 |
| SPEED_CONTROL | 0x1089 | 010211 |
-
FMV "Beskrivning CK37, Del 1" (1985-11-01, M7773-460011) — Official Swedish Air Force maintenance manual, Saab-Scania — Confirms: word length, number format, instruction encoding, memory map, interrupt vectors, I/O architecture, clock timings
-
Bengt Jiewertz, "Central Computer for aircraft Saab 37, Viggen" — Datasaab Vänner paper, written by the CK37's chief designer — Appendices 2 & 3: prototype specifications, instruction timing, I/O counts — Available: datasaab.se/Papers/Articles/Viggenck37.pdf
-
"BITS & BYTES — TEMA FLYG, ur Datasaabs historia" (1995, ISBN 91-972464-17) — Datasaabs Vänner, edited by Bengt Jiewertz — Chapters by original design team members
-
DCS World AJS37 Viggen — CK37 Input Codes cheatsheet (community document) — Reveals the pilot data-entry interface: 6-digit BCD keypad codes, mode structure, ~700 system variables across 30 program blocks
The CK37 was designed to replace a human navigator in a single-seat aircraft. Its entire function was sensor fusion, dead-reckoning navigation, and weapon delivery computation — which maps directly onto autonomous drone flight:
| Viggen system | Drone equivalent |
|---|---|
| INS platform (attitude) | BNO085 IMU |
| Air data (IAS, altitude) | Pitot + baro |
| Radio altimeter | Lidar/radar altimeter |
| Attack radar (range) | GPS range to waypoint |
| HUD output | Telemetry downlink |
| Weapon release | Payload trigger |
| Pilot data panel | GCS command uplink |
This project is part of Former Lab — a sovereignty-focused hardware and software lab building across embedded systems, RF, and autonomous platforms.
The immediate priorities are:
- Hardware bring-up — OrangeCrab flash, UART debug tap
- ADC/DAC wiring — connect ADS8688A + DAC8568 to the AIO module
- Del 3 schematics — the CEM circuit schematic volumes (Flik 4–5) for board-accurate replica
- Kanaldata pages — sida 35–40 for electrical I/O specifications
If you have access to CK37 programming documentation or Del 3 schematics, please open an issue.
- RTL, assembler, tools: CERN OHL-S v2
- Documentation: CC BY 4.0
Former Lab — Sweden