Click on the Table of Contents below to directly go the contents
- Project Overview
- Features
- Video Tutorials
- Final Circuits
- Architecture Components
- Control Unit (Hardwired)
- Instruction Set & Example
- Assembler or Compiler
- Fetch–Decode–Execute Cycle
- Run the CPU — Auto Mode
- Run the CPU — Manual Mode
- Future Improvements
This repository contains the Logisim Evolution implementation of a Simple-As-Possible (SAP-1) CPU. The SAP-1 is a foundational computer architecture used to teach the basic principles of CPU design.
This enhanced implementation features a fully functional hardwired control unit, which automates the fetch-decode-execute cycle. A key improvement is the addition of a ROM-based bootloader. This new feature allows machine code programs to be loaded into the CPU's RAM automatically, eliminating the tedious and error-prone process of manual data entry. The project culminates in successfully executing a simple addition program, loading two pre-defined 8-bit values, adding them, and storing the sum in memory.
- Load Data from Memory: Supports
LDAandLDBinstructions to load values into registers. - Arithmetic Operations: Performs addition (
ADD) and subtraction (SUB) between registers. - Store Results: Stores the contents of Register A into memory using the
STAinstruction. - Control Flow: Jumps to a specific memory address with the
JMPinstruction for loops and branching. - Program Execution: Executes instructions sequentially using the Program Counter.
- Instruction Handling: Fetches, decodes, and executes instructions automatically via the Instruction Register and Control Logic.
- Halt Execution: Stops program execution with the
HLTinstruction. - Debugging Support: Allows step-by-step manual control through dedicated pins in Logisim.
- Memory Access: Handles up to 16 memory addresses (4-bit address space).
- Data Processing: Processes 8-bit data values for arithmetic and storage operations.
- ROM Program Storage: Stores the instruction code in ROM for persistent programs.
- Bootloader / Instruction Loader: In debug mode, data can be loaded from ROM into RAM through a bootloader mechanism, enabling easy program initialization and testing.
I made videos explaining the SAP-1 CPU and how to simulate it.
- Auto Code Loading (
khalid_sap1_auto.circ): https://youtu.be/vwInhCTQctg - Manual Code Loading (
khalid_sap1_manual.circ): https://youtu.be/PrJcHA_dC8Q
The SAP-1 CPU is composed of several fundamental building blocks:
-
Program Counter (PC): A 4-bit counter that stores the memory address of the next instruction to be executed. It increments automatically after each instruction fetch.
-
Random Access Memory (RAM): An 8-bit wide memory unit used to store both machine code instructions and data. This implementation uses a 16-byte RAM.
-
Memory Address Register (MAR): A 4-bit register that holds the address of the memory location currently being accessed (for reading or writing).
-
Instruction Register (IR): An 8-bit register that temporarily holds the instruction fetched from RAM. It's split into a 4-bit opcode and a 4-bit operand (memory address).
-
Registers A & B (Accumulator & B-Register): 8-bit general-purpose registers. Register A (Accumulator) is typically used for arithmetic operations and storing results. Register B holds the second operand for ALU operations.
-
Arithmetic Logic Unit (ALU): An 8-bit unit capable of performing basic arithmetic (addition, subtraction) and logical operations on data from Registers A and B.
-
Instruction Loader: Loads code instructions from ROM to RAM with clock pulses.
-
Output Register: (Implicit in SAP-1, often just Register A or a direct output).
-
Control Unit: The "brain" of the CPU. It generates the necessary control signals (pin activations) at the correct time to sequence the micro-operations for fetching, decoding, and executing instructions.
The control unit is implemented using combinational logic (AND, OR, NOT gates) and a state counter (often called a ring counter in SAP-1 context). It orchestrates the entire CPU operation.
-
State Counter (RC): A 3-bit counter that cycles through T-states (T1, T2, T3, T4, T5, T6).
-
Opcode Decoder: A 4-to-16 decoder connected to the most significant 4 bits (opcode) of the Instruction Register. It generates a unique HIGH signal for each recognized instruction (e.g.,
isLDA,isADD,isHLT). -
Control Matrix (Logic Gates): The network of AND and OR gates that takes the T-state signals from the State Counter and the instruction signals from the Opcode Decoder as inputs. Its outputs are the various control pins that govern data flow and operations across the CPU.
The following Boolean equations define when each control pin is activated (goes HIGH). These are implemented directly using AND and OR gates in the Control Matrix. cpu_mode is NOT(debug), ensuring automated operation only when debug is OFF.
pc_out_final = T1 AND cpu_mode AND (NOT l2)mar_in_en_final = (T1 AND cpu_mode) OR ((T4 AND isLDA) AND cpu_mode) OR ((T4 AND isLDB) AND cpu_mode) OR ((T4 AND isSTA) AND cpu_mode) OR (l2 AND debug)sram_rd_final = (T2 AND cpu_mode) OR ((T5 AND isLDA) AND cpu_mode) OR ((T5 AND isLDB) AND cpu_mode AND (NOT l2))ins_reg_in_en_final = T2 AND cpu_mode AND (NOT l2)pc_en_final = T3 AND cpu_mode AND (NOT l2)ins_reg_out_en_final = ((T4 AND isLDA) AND cpu_mode) OR ((T4 AND isLDB) AND cpu_mode) OR ((T4 AND isSTA) AND cpu_mode) OR (T3 AND isJMP)a_in_final = ((T5 AND isLDA) AND cpu_mode) OR ((T4 AND isADD) AND cpu_mode AND (NOT l2))a_out_final = ((T4 AND isADD) AND cpu_mode) OR ((T5 AND isSTA) AND cpu_mode AND (NOT l2))b_in_final = (T5 AND isLDB) AND cpu_mode AND (NOT l2)b_out_final = (T4 AND isADD) AND cpu_mode AND (NOT l2)alu_out_final = (T4 AND isADD) AND cpu_mode AND (NOT l2)sram_wr_final = ((T5 AND isSTA) AND (NOT l2)) OR (l2 AND debug)alu_sub = (T4 AND isSUB) AND cpu_mode AND (NOT l2)hlt = T4 AND isHLT AND (NOT l2)(Used to stop the clock/reset the state counter)jmp_en = (T3 AND isJMP) AND cpu_mode AND (NOT l2)
pc_out_final = T1 AND cpu_modemar_in_en_final = (T1 AND cpu_mode) OR ((T4 AND isLDA) AND cpu_mode) OR ((T4 AND isLDB) AND cpu_mode) OR ((T4 AND isSTA) AND cpu_mode) OR (mar_in_en_manual AND debug)sram_rd_final = (T2 AND cpu_mode) OR ((T5 AND isLDA) AND cpu_mode) OR ((T5 AND isLDB) AND cpu_mode)ins_reg_in_en_final = T2 AND cpu_modepc_en_final = T3 AND cpu_modeins_reg_out_en_final = ((T4 AND isLDA) AND cpu_mode) OR ((T4 AND isLDB) AND cpu_mode) OR ((T4 AND isSTA) AND cpu_mode)a_in_final = ((T5 AND isLDA) AND cpu_mode) OR ((T4 AND isADD) AND cpu_mode)a_out_final = ((T4 AND isADD) AND cpu_mode) OR ((T5 AND isSTA) AND cpu_mode)b_in_final = (T5 AND isLDB) AND cpu_modeb_out_final = (T4 AND isADD) AND cpu_modealu_out_final = (T4 AND isADD) AND cpu_modealu_sub = 0(Always LOW for addition)cs_en = 1(Always HIGH)sram_wr_final = ((T5 AND isSTA) AND cpu_mode) OR (sram_wr_manual AND debug)hlt = T4 AND isHLT(Used to stop the clock/reset the state counter)
Note on Debug Mode: When the debug pin is HIGH, cpu_mode becomes LOW, disabling all _auto signals. The mar_in_en_final and sram_wr_final pins are then controlled by their respective _manual inputs, allowing direct RAM programming. All other bus outputs (from SRAM, Reg A, Reg B, ALU) are also disabled when debug is HIGH to prevent bus conflicts.
This program loads two 8-bit values (let's say 51 and 25), adds them, and stores the sum (76) in memory.
- Value 1 (Dec 51 & Hex
33) at:00001101(Decimal 13) - Value 2 (Dec 25 & Hex
19) at:00001110(Decimal 14) - Sum (Dec 76 & Hex
4C) stored at:00001111(Decimal 15)
| Address (Binary) | Instruction (Binary) | Hex | Mnemonic & Explanation |
|---|---|---|---|
00000000 |
0001 1101 |
1D |
LDA 13 (Load Register A with value from memory address 13) |
00000001 |
0010 1110 |
2E |
LDB 14 (Load Register B with value from memory address 14) |
00000010 |
0011 0000 |
30 |
ADD (Add B to A, store in A. Operand bits are unused) |
00000010 |
0100 0000 |
40 |
SUB (Sub A to B, store in A. Operand bits are unused) |
00000011 |
0101 1111 |
5F |
STA 15 (Store content of Register A to memory address 15) |
00000100 |
0110 0101 |
65 |
JMP 5 (Jump to memory address 5) |
00000101 |
1111 0000 |
F0 |
HLT (Halt program execution. Operand bits are unused) |
| Address (Binary) | Data (Binary) | Decimal | Hex |
|---|---|---|---|
00001101 |
00110011 |
51 | 33 |
00001110 |
00011001 |
25 | 19 |
Go to the link and write your SAP-1 assembly code and The assembler will convert it into a hex string for your Logisim ROM.
Compiler Link: sap1-compiler.vercel.app
Example:
Assembly Code For ADD: (LDA 13 LDA 14 ADD STA 15 HLT ORG 13 DEC 51 DEC 25)
Hex Code: 1D 2E 30 5F F0 00 00 00 00 00 00 00 00 33 19 00
Assembly Code For JMP & ADD: (LDA 13 LDA 14 JMP 5 ORG 5 ADD STA 15 HLT ORG 13 DEC 51 DEC 25)
Hex Code: 1D 2E 65 00 00 30 5F F0 00 00 00 00 00 33 19 00
The CPU operates in a continuous cycle, driven by the clock:
- T1: The Program Counter (PC) places its address onto the address bus. This address is loaded into the Memory Address Register (MAR).
- T2: The RAM reads the instruction at the address in MAR and places it onto the data bus. The Instruction Register (IR) loads this instruction.
- T3: The PC increments, preparing for the next instruction.
The Instruction Register's opcode portion is sent to the Opcode Decoder, which activates a specific instruction line (e.g., isLDA). This decoded instruction, along with the current T-state from the State Counter, determines which control signals will be activated in the Execute phase.
The control unit activates the necessary control pins to perform the micro-operations defined by the instruction. The number of T-states in this phase varies per instruction (e.g., LDA takes 2 T-states, ADD takes 2 T-states, HLT takes 1 T-state).
This cycle repeats automatically for each instruction until a HLT instruction is encountered, which stops the clock.
Follow these steps to load your ROM-based program and run the automated simulation:
Download and Open Logisim Evolution: If you don't have it, download Logisim Evolution.
- Open the
khalid_sap1_auto.circfile containing your SAP-1 CPU design in Logisim. - Ensure the
debugpin is OFF (LOW). - Ensure the main
clk(clock) component is OFF. - Pulse the
pc_resetpin once to reset the Program Counter to0000.
- Right-click the ROM component and select
Edit Contents.... - Enter the hex values for the program directly into the ROM's memory, as shown in the "Example Program" section.
- Type the code:
1D 2E 30 5F F0 00 00 00 00 00 00 00 00 33 19 00(For ADD) or1D 2E 40 5F F0 00 00 00 00 00 00 00 00 33 19 00(For SUB) - Or, you can upload the code to the ROM by loading the provided
instruction_code_addorinstruction_code_subfile.
- Turn ON the
debugpin (HIGH). The Code Loading Mode LED will turn ON. - With subsequent
clkpulses, the CPU will automatically load the instructions from the ROM into the SRAM. It takes two clock pulses per instruction/data value to complete the write cycle. - Allow the CPU to cycle through all necessary addresses and load all instructions and data.
- You can see the MAR address and the Data Bus in the 7-segment displays.
- Turn OFF the
debugpin (LOW). - Pulse the main
clkbutton once to ensure the bootloader process completely stops.
- Pulse the
pc_resetpin again to ensure the Program Counter is at0000for program start. - Repeatedly click the
clkbutton (or enable a continuous clock source) to watch the CPU execute the program automatically. - For each click, observe the changes in the PC, MAR, IR, Registers A and B in the 7-segment display.
- Follow the Fetch–Decode–Execute cycle for each instruction as detailed above.
- If you have a continuous clock source, enable it to watch the CPU run at speed.
You can follow the video below:
-
After the CPU executes the
HLTinstruction and stops, check the contents of RAM address00001111(decimal 15). -
If you do ADD then It should contain
01001100(decimal 76, Hex4C). Register A should also show4Con the 7-segment display.
Follow these steps to load your circuit, program the RAM, and run the automated simulation:
-
Download and Open Logisim Evolution: If you don't have it, download Logisim Evolution.
-
Load the Circuit: Open the
khalid_sap1_manual.circfile containing your SAP-1 CPU design in Logisim. -
Initial Setup:
- Ensure the
debugpin is OFF (LOW). This enables the automated control. - Pulse the
pc_resetpin once to reset the Program Counter to0000. - Ensure the main
clk(clock) component is OFF. For step-by-step testing, you'll use a manual button for the clock. - Ensure the
cs_enpin is ON (HIGH). This enables the circuit.
- Ensure the
-
Program the RAM (Debug Mode):
- Turn ON the
debugpin (HIGH). This enables manual control for RAM programming and disables automatic bus drivers. - For each instruction and data value (see Example Program above):
- Set Address: Use
debug_datato set the 8-bit memory address (e.g.,00000000for the first instruction). - Load Address to MAR: Pulse
mar_in_en_manualonce. - Set Data/Instruction: Use
debug_datato set the 8-bit instruction or data value (e.g.,00011101forLDA 13). - Write to RAM: Pulse
sram_wr_manualonce.
- Set Address: Use
- After loading all instructions and data, turn OFF the
debugpin (LOW). - Pulse
pc_resetagain to ensure the PC is at0000for program start.
- Turn ON the
-
Run the Program:
- Use the main
clkbutton (or enable the continuous clock):- Manual Stepping (recommended): Repeatedly click the
clkbutton and observe PC, MAR, IR, Registers A/B, and RAM contents following the Fetch–Decode–Execute cycle. - Continuous Run: If you have a continuous clock source, enable it to watch the CPU run at speed.
- Manual Stepping (recommended): Repeatedly click the
- Use the main
-
Observe
HLT: When the CPU reachesHLT, the clock should stop, or the state counter should halt, indicating the program has finished. -
Verify Result: Check RAM address
00001111(decimal 15). It should contain01001100(decimal 76).
- Expand Instruction Set: Add more instructions like
OUT,JZ, etc. - Microprogrammed Control: Replace the hardwired control unit with a microprogrammed one using a ROM for greater flexibility.
- Input/Output: Implement a simple input device (e.g., keyboard) and output display.















