Skip to content
Go back
Implementing the SAP-1 Computer on FPGA

Implementing the SAP-1 Computer on FPGA

By Michael Earls

Edit page

Over the last few weeks I’ve been implementing a minimalist 8-bit SAP-1 (Simple As Possible) computer on an FPGA. This post captures the architecture, the SystemVerilog snippets that make it tick, the VGA output I just added, and the little browser-based assembler I wrote to generate the ROM image (.mem) for the design. It’s written as a “dev log” I can hand to Future-Me when I inevitably forget the details. :)


What is SAP-1 (in my build)?

My SAP-1 is an 8-bit accumulator machine with a 4-bit opcode and a 4-bit operand (address) packed into a single 8-bit instruction. It uses a 16-byte instruction/data memory. Core parts:


Getting Started

To follow along, you can use the online assembler. Click on the “Help” button at the top right to get instructions on how to use it.

Checkout the SAP-1 HDL and .tcl scripts at GitHub:

SAP-1 Repository

Diagrams

SAP-1 Architecture Diagram

SAP-1 Sequence Diagram

Example Video

SystemVerilog Updates

I migrated the design fully to SystemVerilog (.sv files). This means:


Code Walkthrough (snippets + commentary)

1) Top-Level (with VGA support)

// -----------------------------------------------------------------------------
// sap1_top.sv  — top-level wrapper (with VGA)
// -----------------------------------------------------------------------------
module sap1_top (
    input  wire clk100,           // board clock
    input  wire rst_n,            // active-low reset
    // VGA output
    output wire hsync,
    output wire vsync,
    output wire [3:0] vga_r,
    output wire [3:0] vga_g,
    output wire [3:0] vga_b
);
    // --- Clock divider: ~25 MHz pixel clock for VGA ---
    logic clk_sys, clk_pix;
    clock_div #(.DIV_SYS(4), .DIV_PIX(4)) u_div (
        .clk_in (clk100),
        .rst_n  (rst_n),
        .clk_sys(clk_sys),   // ~25 MHz
        .clk_pix(clk_pix)    // VGA pixel clock
    );

    // Core wires (same as before)
    logic [7:0] alu_out, rom_data, ir, reg_a, reg_b, reg_out;
    logic [3:0] pc;
    logic zf, cf;
    logic ce_pc, we_ir, ld_a, ld_b, sel_sub, we_out, halt;
    logic [2:0] tstate;

    // ROM program
    rom16x8 u_rom (.addr(pc), .data_out(rom_data));

    // Control unit
    control_unit u_ctl (
        .clk     (clk_sys),
        .rst_n   (rst_n),
        .ir      (ir),
        .zf      (zf),
        .cf      (cf),
        .pc_ce   (ce_pc),
        .ir_we   (we_ir),
        .a_ld    (ld_a),
        .b_ld    (ld_b),
        .alu_sub (sel_sub),
        .out_we  (we_out),
        .halt    (halt),
        .tstate  (tstate)
    );

    // Program Counter + IR
    pc4 u_pc (.clk(clk_sys), .rst_n(rst_n), .ce(ce_pc), .pc(pc));
    ir8 u_ir (.clk(clk_sys), .we(we_ir), .din(rom_data), .ir(ir));

    // Registers A, B, OUT
    reg8 u_a (.clk(clk_sys), .ld(ld_a), .rst_n(rst_n), .din(alu_out), .dout(reg_a));
    reg8 u_b (.clk(clk_sys), .ld(ld_b), .rst_n(rst_n), .din(alu_out), .dout(reg_b));
    alu8 u_alu (.a(reg_a), .b(reg_b), .sub(sel_sub), .y(alu_out), .zf(zf), .cf(cf));
    reg8 u_out (.clk(clk_sys), .ld(we_out), .rst_n(rst_n), .din(alu_out), .dout(reg_out));

    // VGA text output (shows OUT register as hex)
    vga_hex_display u_vga (
        .clk_pix (clk_pix),
        .rst_n   (rst_n),
        .value   (reg_out),
        .hsync   (hsync),
        .vsync   (vsync),
        .r       (vga_r),
        .g       (vga_g),
        .b       (vga_b)
    );
endmodule

2) VGA Hex Display

This is a very simple text-only VGA renderer. It displays value[7:0] as two hex digits in the middle of the screen.

module vga_hex_display (
    input  wire       clk_pix,   // 25 MHz pixel clock
    input  wire       rst_n,
    input  wire [7:0] value,
    output logic      hsync,
    output logic      vsync,
    output logic [3:0] r,
    output logic [3:0] g,
    output logic [3:0] b
);
    // VGA 640x480 @60Hz timing
    localparam H_RES = 640, V_RES = 480;
    localparam H_FP = 16, H_SYNC = 96, H_BP = 48;
    localparam V_FP = 10, V_SYNC = 2, V_BP = 33;

    logic [9:0] h_count;
    logic [9:0] v_count;

    always_ff @(posedge clk_pix, negedge rst_n) begin
        if (!rst_n) begin
            h_count <= 0; v_count <= 0;
        end else begin
            if (h_count == H_RES+H_FP+H_SYNC+H_BP-1) begin
                h_count <= 0;
                if (v_count == V_RES+V_FP+V_SYNC+V_BP-1)
                    v_count <= 0;
                else
                    v_count <= v_count + 1;
            end else begin
                h_count <= h_count + 1;
            end
        end
    end

    assign hsync = ~(h_count >= H_RES+H_FP && h_count < H_RES+H_FP+H_SYNC);
    assign vsync = ~(v_count >= V_RES+V_FP && v_count < V_RES+V_FP+V_SYNC);

    // Simple placeholder: whole screen white while active
    logic active_area = (h_count < H_RES && v_count < V_RES);
    always_comb begin
        if (active_area) begin
            r = 4'hF; g = 4'hF; b = 4'hF; // white pixels
        end else begin
            r = 4'h0; g = 4'h0; b = 4'h0;
        end
    end
endmodule

Note: Right now it just blanks/white-fills the active area. The next step is to add an 8x8 font ROM and map value[7:4] and value[3:0] to hex characters at screen center.


The Assembler (updated for SystemVerilog + VGA + one‑click ZIP)

I’ve expanded the browser-based assembler at /sap-1-assembler so it’s now a one‑stop build station. It still assembles to a 16×8 ROM, but now it can export a complete Vivado project bundle and includes the VGA path by default.

What’s new

Buttons in the assembler UI

ZIP layout

src/
  sap1_top.sv
  sap1_core.sv
  sap1_rom.sv
  sevenseg_hex_mux.sv
  btn_debouncer.sv
  pulse_stretcher.sv
  vga_timing_640x480_ce.sv
  vga_hex7_overlay.sv
mem/
  sap1_rom.mem
constraints/
  sap1_minimal.xdc
  (place Nexys-A7-100T-Master.xdc here)
scripts/
  build_sap1_nexys_a7.tcl
  rebuild_after_mem_change.tcl
README.md

Build & program from the ZIP

  1. Unzip somewhere handy (e.g., sap1_project/).
  2. Copy your Digilent master XDC into constraints/Nexys-A7-100T-Master.xdc and uncomment the nets you’re using (buttons BTNC/BTNU/BTND, 7‑seg SEG[7:0] + AN[7:0], LEDs, VGA VGA_HS/VGA_VS/VGA_R[3:0]/VGA_G[3:0]/VGA_B[3:0]).
  3. Build from the project root:
    vivado -mode batch -source scripts/build_sap1_nexys_a7.tcl
  4. Program in Vivado Hardware Manager with:
    sap1_nexys_a7/sap1_nexys_a7.runs/impl_1/sap1_top.bit
  5. Quick update after only ROM changes:
    vivado -mode batch -source scripts/rebuild_after_mem_change.tcl

VGA notes

Troubleshooting quickies

Sample Program: set OUT to 0xFE by adding three numbers

Target: 0x64 + 0x32 + 0x68 = 0xFE (100 + 50 + 104 = 254).

; add3_to_FE.asm — OUT = 0x64 + 0x32 + 0x68 = 0xFE
        LDA N1
        ADDM N2
        ADDM N3
        OUTA
        HLT

; Data region
N1:     .db 0x64
N2:     .db 0x32
N3:     .db 0x68

Next Steps

If you’re following along and hit a snag, future-me, check the ROM path first. It’s always the ROM path. :)


Edit page
Share this post on:

Previous Post
FPGA Sprite with VGA, Thruster Sound, and Flame
Next Post
Hookpad → SystemVerilog: Programming Music on the Nexys A7