展示HN:E80:一种结构化VHDL的8位CPU
Show HN: E80: an 8-bit CPU in structural VHDL

原始链接: https://github.com/Stokpan/E80

## E80:一种基于VHDL的构造主义CPU 该项目详细介绍了用VHDL实现的一个功能齐全的8位CPU的开发,旨在作为计算机体系结构教育的构造主义微世界。它追求**低门槛**,采用基于教科书的组件和一键式工具链安装程序进行仿真;**高上限**,支持堆栈操作和子程序调用等典型CPU指令;以及**宽阔的拓展性**,通过平台兼容性和完整的源代码访问实现。 该CPU具有8位架构,包含8位数据/地址总线和变长(1或2字)指令。它包括一个2R/1W多端口RAM、一个8x8位寄存器文件(6个通用寄存器、标志寄存器和堆栈指针),并支持加载/存储和寄存器-寄存器寻址。一个汇编器(ISO C99)可以将混合的ARM/x86风格的汇编语言翻译成机器码。 该设计使用GHDL/GTKWave和ModelSim进行仿真,并可综合到Tang Primer 25K、Altera Cyclone IV等FPGA上。提供了全面的文档,包括BNF语法和指令集。示例程序展示了功能和测试过程,例如内存访问、子程序调用以及与外部硬件(DIP开关、LED、操纵杆)的交互。该项目优先考虑易用性、真实的程序执行以及适应各种实验室和课堂场景的能力。

一位开发者在Hacker News分享了“E80”,一个完全结构化的8位CPU,使用VHDL从头开始构建。该项目有意避免软核中常见的高级抽象,旨在通过直接实现像进位加法器和触发器等基本组件来实现教学上的清晰度——即使以牺牲资源效率为代价。 该创作者还为CPU构建了一个简单的C99汇编器,并将完整的开发环境(Sci1、GHDL、GTKWave)打包到一个Windows安装程序中,以便于设置和波形可视化。 项目文件适用于Tang Primer 25K和Cyclone IV等FPGA,整个项目在GPL3许可下开源。值得注意的是,开发者明确表示该项目*没有*借助AI,因为其独特的6502/6800启发式逻辑可能会让当前的LLM感到困惑。
相关文章

原文

A simple CPU in VHDL, developed from scratch for my undergraduate thesis to provide all three characteristics of a Constructionist Microworld:

  • Low floor, as it depends purely on textbook-based, structural VHDL components, and offers a toolchain installer for one-click simulation.
  • High ceiling, as it supports all typical instructions found in Computer Architecture textbooks, including stack operations & subroutine calling.
  • Wide walls, as it was designed for compatibility with a variety of platforms, as seen below, and provides the complete source material (from the CFG grammar to EDA project files) for study and modification.

This makes it easy to use, capable of running pretty complex and realistic programs, and can be used in multiple lab or classroom scenarios.

Feature Description
Dependencies ieee.std_logic_1164 (no arithmetic libraries)
Execution Single-cycle
Word Size 8-bit
Buses 8-bit data, 8-bit address, 16-bit instruction
Instruction size Variable (1 or 2 words)
RAM Multiport (2R, 1R/W), addressable at 0x00-0xFE
Register file Multiport (1R/W, 1R, 1W), 8x8-bit
Registers 6 general purpose (R0-R5), Flags (R6), SP (R7)
Stack Full descending (Stack Pointer init = 0xFF)
Architecture Load/Store, register-register
Addressing Immediate, direct, register, register-indirect
Input 8-bit Memory Mapped at 0xFF (1x8 DIP switch)
Output Flags, Registers, PC, Clock (3x8 LEDs)
Assembly syntax Hybrid of ARM, x86, and textbook pseudocode
Assembler ISO C99 (standard library, stdin I/O)
Simulated on GHDL/GTKWave & ModelSim via one-click scripts
Editor SciTE, with syntax coloring & one-click run
Synthesized on Quartus Lite, Gowin Education, Vivado Standard
Tested on Tang Primer 25K, Altera Cyclone IV
n       : 8-bit immediate value or memory address.
r,r1,r2 : 3-bit register address (R0 to R7), eg. MOV R3,R5 translates to
          0b(00011000 0rrr0rrr) ≡ 0b(00011000 00110101) or 0x(18rr) ≡ 0x(1835).
[x]     : Memory at address x < 255, [255] = DIP input.
PC      : Program counter, initialized to 0 on reset.
SP      : Register R7, initialized to 255 on reset.
          --SP Decrease SP by 1, and then read it.
          SP++ Read SP, and then increase it by 1.
Flags   : Register R6 = [CZSVH---] (see ALU.vhd)
          C = Carry out (unsigned arithmetic) or shifted-out bit.
          Z = Zero, set to 1 when result is 0.
          S = Sign, set to the most significant bit of the result.
          V = Overflow (signed arithmetic), or sign bit flip in L/RSHIFT
          H = Halt flag, (freezes PC).

     +----------+----------+-------+---------------+-----------------------+-------+
     | Instr1   | Instr2   | Hex   | Mnemonic      | Description           | Flags |
+----+----------+----------+-------+---------------+-----------------------+-------+
| 1  | 00000000 |          | 00    | HLT           | PC ← PC               |     H |
| 2  | 00000001 |          | 01    | NOP           |                       |       |
| 3  | 00000010 | nnnnnnnn | 02 nn | JMP n         | PC ← n                |       |
| 4  | 00000011 | 00000rrr | 03 0r | JMP r         | PC ← r                |       |
| 5  | 00000100 | nnnnnnnn | 04 nn | JC n          | if C=1, PC ← n        |       |
| 6  | 00000101 | nnnnnnnn | 05 nn | JNC n         | if C=0, PC ← n        |       |
| 7  | 00000110 | nnnnnnnn | 06 nn | JZ n          | if Z=1, PC ← n        |       |
| 8  | 00000111 | nnnnnnnn | 07 nn | JNZ n         | if Z=0, PC ← n        |       |
| 9  | 00001010 | nnnnnnnn | 0A nn | JS n          | if S=1, PC ← n        |       |
| 10 | 00001011 | nnnnnnnn | 0B nn | JNS n         | if S=0, PC ← n        |       |
| 11 | 00001100 | nnnnnnnn | 0C nn | JV n          | if V=1, PC ← n        |       |
| 12 | 00001101 | nnnnnnnn | 0D nn | JNV n         | if V=0, PC ← n        |       |
| 13 | 00001110 | nnnnnnnn | 0E nn | CALL n        | PC+2 → [--SP]; PC ← n |       |
| 14 | 00001111 |          | 0F    | RETURN        | PC ← [SP++]           |       |
| 15 | 00010rrr | nnnnnnnn | 1r nn | MOV r,n       | r ← n                 |  ZS   |
| 16 | 00011000 | 0rrr0rrr | 18 rr | MOV r1,r2     | r1 ← r2               |  ZS   |
| 17 | 00100rrr | nnnnnnnn | 2r nn | ADD r,n       | r ← r+n               | CZSV  |
| 18 | 00101000 | 0rrr0rrr | 28 rr | ADD r1,r2     | r1 ← r1+r2            | CZSV  |
| 19 | 00110rrr | nnnnnnnn | 3r nn | SUB r,n       | r ← r+(~n)+1          | CZSV  |
| 20 | 00111000 | 0rrr0rrr | 38 rr | SUB r1,r2     | r1 ← r1+(~r2)+1       | CZSV  |
| 21 | 01000rrr | nnnnnnnn | 4r nn | AND r,n       | r ← r&n               |  ZS   |
| 22 | 01001000 | 0rrr0rrr | 48 rr | AND r1,r2     | r1 ← r1&r2            |  ZS   |
| 23 | 01010rrr | nnnnnnnn | 5r nn | OR r,n        | r ← r|n               |  ZS   |
| 24 | 01011000 | 0rrr0rrr | 58 rr | OR r1,r2      | r1 ← r1|r2            |  ZS   |
| 25 | 01100rrr | nnnnnnnn | 6r nn | XOR r,n       | r ← r^n               |  ZS   |
| 26 | 01101000 | 0rrr0rrr | 68 rr | XOR r1,r2     | r1 ← r1^r2            |  ZS   |
| 27 | 01110rrr | nnnnnnnn | 7r nn | ROR r,n       | r>>n (r<<8-n)         |  ZS   |
| 28 | 01111000 | 0rrr0rrr | 78 rr | ROR r1,r2     | r1>>r2 (r1<<8-r2)     |  ZS   |
| 29 | 10000rrr | nnnnnnnn | 8r nn | STORE r,[n]   | r → [n]               |       |
| 30 | 10001000 | 0rrr0rrr | 88 rr | STORE r1,[r2] | r1 → [r2]             |       |
| 31 | 10010rrr | nnnnnnnn | 9r nn | LOAD r,[n]    | r ← [n]               |  ZS   |
| 32 | 10011000 | 0rrr0rrr | 98 rr | LOAD r1,[r2]  | r1 ← [r2]             |  ZS   |
| 33 | 10100rrr |          | Ar    | LSHIFT r      | (C,r)<<1; V ← S flip  | CZSV  |
| 34 | 10110rrr | nnnnnnnn | Br nn | CMP r,n       | SUB, discard result   | CZSV  |
| 35 | 10111000 | 0rrr0rrr | B8 rr | CMP r1,r2     | SUB, discard result   | CZSV  |
| 36 | 11000rrr | nnnnnnnn | Cr nn | BIT r,n       | AND, discard result   |  ZS   |
| 37 | 11010rrr |          | Dr    | RSHIFT r      | (r,C)>>1; V ← S flip  | CZSV  |
| 38 | 11100rrr |          | Er    | PUSH r        | r → [--SP]            |       |
| 39 | 11110rrr |          | Fr    | POP r         | r ← [SP++]            |       |
+----+----------+----------+-------+---------------+-----------------------+-------+

Notes

  • ROR R1,R2 rotates R1 to the right by R2 bits. This is equivalent to left rotation by 8-R2 bits.
  • Carry and oVerflow flags are updated by arithmetic and shift instructions, except ROR.
  • Shift instructions are logical; Carry flag = shifted bit and the Overflow flag is set if the sign bit is changed.
  • The Sign and Zero flags are updated by CMP, BIT, and any instruction that modifies a register, except for stack-related instructions.
  • Explicit modifications of the FLAGS register take precedence over normal flag changes, eg. OR FLAGS, 0b01000000 sets Z=1 although the result is non-zero.
  • The HLT instruction sets the H flag and freezes the PC, thereby stopping execution in the current cycle. Setting the Halt flag by modifying the Flags (R6) register will stop execution on the next cycle.
  • Comparison of unsigned numbers via the Carry flag can be confusing because SUB R1,R2 is done via standard adder logic (R1 + ~R2 + 1). See the flags cheatsheet below.
           +------+-----------------------------+----------------------+
           | Flag | Signed                      | Unsigned             |
 +---------+------+-----------------------------+----------------------+
 | ADD a,b | C=1  |                             | a+b > 255 (overflow) |
 |         | C=0  |                             | a+b ≤ 255            |
 |         | V=1  | a+b ∉ [-128,127] (overflow) |                      |
 |         | V=0  | a+b ∈ [-128,127]            |                      |
 |         | S=1  | a+b < 0                     | a+b ≥ 128 (if C=0)   |
 |         | S=0  | a+b ≥ 0                     | a+b < 128 (if C=0)   |
 +---------+------+-----------------------------+----------------------+
 | SUB a,b | C=1  |                             | a ≥ b                |
 | or      | C=0  |                             | a < b (overflow)     |
 | CMP a,b | V=1  | a-b ∉ [-128,127] (overflow) |                      |
 |         | V=0  | a-b ∈ [-128,127]            |                      |
 |         | S=1  | a < b                       | a-b ≥ 128 (if C=1)   |
 |         | S=0  | a ≥ b                       | a-b < 128 (if C=1)   |
 +---------+------+-----------------------------+----------------------+
string  : ASCII with escaped quotes, eg. "a\"b\"c" is quoted a"b"c.
label   : Starts from a letter, may contain letters, numbers, underscores.
number  : 0-255 no leading zeroes, bin (eg. 0b0011), hex (eg. 0x0A).
val     : Number or label.
csv     : Comma-separated numbers and strings.
reg     : Register R0-R7 or FLAGS (alias of R6) or SP (alias of R7).
op1/op2 : Reg or val (flexible operand).
[op2]   : Memory at address op2 (or DIP input if op2=0xFF).

+----------------------+----------------------------------------------------+
| Directive            | Description                                        |
+----------------------+----------------------------------------------------+
| .TITLE "string"      | Sets VHDL output title to string                   |
| .LABEL label number  | Assigns a number to a label                        |
| .SIMDIP value        | Sets the DIP input to value (for simulation only)  |
| .DATA label csv      | Appends csv at label address after program space   |
| .FREQUENCY deciHertz | Set frequency to deciHertz (1-1000)                |
+----------------------+----------------------------------------------------+

+----------------------+----------------------------------------------------+
| Instruction          | Description                                        |
+----------------------+----------------------------------------------------+
| label:               | Marks the address of the next instruction          |
| HLT                  | Sets the H flag and halts execution                |
| NOP                  | No operation                                       |
| JMP op1              | Jump to op1 address                                |
| JC n                 | Jump if Carry (C=1)                                |
| JNC n                | Jump if Not Carry (C=0)                            |
| JZ n                 | Jump if Zero (Z=1)                                 |
| JNZ n                | Jump if Not Zero (Z=0)                             |
| JS n                 | Jump if Sign (S=1)                                 |
| JNS n                | Jump if Not Sign (S=0)                             |
| JV n                 | Jump if Overflow (V=1)                             |
| JNV n                | Jump if Not Overflow (V=0)                         |
| CALL n               | Call subroutine at n                               |
| RETURN               | Return from subroutine                             |
| MOV reg, op2         | Move op2 to reg                                    |
| ADD reg, op2         | Add op2 to reg                                     |
| SUB reg, op2         | In unsigned subtraction, C = reg ≥ op2             |
| ROR reg, op2         | Rotate right by op2 bits (left by 8-op2 bits)      |
| AND reg, op2         | Bitwise AND                                        |
| OR reg, op2          | Bitwise OR                                         |
| XOR reg, op2         | Bitwise XOR                                        |
| STORE reg, [op2]     | Store reg to op2 address, reg → [op2]              |
| LOAD reg, [op2]      | Load word at op2 address to reg, reg ← [op2]       |
| RSHIFT reg           | Right shift, C = shifted bit, V = sign change      |
| CMP reg, op2         | Compare with SUB, set flags and discard result     |
| LSHIFT reg           | Left shift, C = shifted bit, V = sign change       |
| BIT reg, n           | Bit test with AND, set flags and discard result    |
| PUSH reg             | Push reg to stack                                  |
| POP reg              | Pop reg from stack                                 |
+----------------------+----------------------------------------------------+

Notes

  • Directives must precede all instructions.
  • Labels are case sensitive, but directives and instructions are not.
  • .DATA sets a label after the last instruction and writes the csv data to it; consecutive .DATA directives append after each other.
  • Comments start with a semicolon.
  • The grammar is available in BNF notation at Piber's Testing suite. To test your input, first select BNF from the settings cogwheel on the top right. This syntax is a structural blueprint, not a full specification, and lacks features such as comments.
  • The .FREQUENCY directive doesn't affect simulation; it's used in the FPGA unit only.
  • Likewise, the .SIMDIP directive doesn't affect execution on FPGAs; it's used in simulation only.

Example 1 - One-click simulation with GHDL/GTKWave or ModelSim

The following program writes the null-terminated string `az{"0 to memory after the last instruction (notice the label under HLT) and converts the lowercase characters to uppercase, stopping when it hits the terminator:

.TITLE "Converts the lowercase characters of a given string to uppercase"
.LABEL char_a 97
.LABEL char_after_z 123     ; character after "z" is "{"
.LABEL case_difference 32
.DATA string "`az{\"0",0    ; null-terminated string under the last instruction
    MOV R0, string          ; R0 = address of the first character ("`")
loop:   
    LOAD R1, [R0]           ; Updates SZ flags (like 6800 & 6502)
    JZ finish               ; loop while [R0] != null
    CMP R1, char_a
    JNC next                ; if [R0] < "a" goto next
    CMP R1, char_after_z
    JC next                 ; else if [R0] ≥ "{" goto next
    SUB R1, case_difference ; [R0] ∈ ["a", "z"], so change to uppercase
    STORE R1, [R0]          ; write character back to RAM
next:
    ADD R0, 1               ; [R0]++
    JMP loop                ; end loop
finish:
    HLT                     ; stop execution & simulation

To simulate it, first install the E80 Toolchain package from the Releases, then open the E80 Editor and paste the code into it:

Assembly code as it appears in the editor

Notice that syntax highlighting for the E80 assembly language has been enabled by default for all code (except for VHDL files).

Press F5. The editor will automatically assemble the code, save the VHDL output, compile the entire design with GHDL, and launch a GTKWave instance. Subsequent simulations will be closing the previous GTKWave window to open a new one.

You should see the following waveform, in which the RAM has been expanded to show how the lowercase letters of the string have changed to uppercase:

GHDL waveform output in GTKWave. The highlighted RAM locations 25-31 have been initialized by the .DATA directive and modified by the program. These have been manually set to ASCII data format in GTKwave.

Notice that the HLT instruction has stopped the simulation in GHDL, allowing for the waveforms to be drawn for the runtime only. This useful feature is supported in ModelSim as well.

You can also press F7 to view the generated Firmware.vhd file, without simulation:

VHDL output of the assembler on the editor

Notice how the assembler formats the output into columns according to instruction size, and annotates each line to its respective disassembled instruction, ASCII character or number.

If you have installed ModelSim, you can press F8 to automatically open ModelSim and simulate into it. Subsequent simulations on ModelSim will update its existing window:

ModelSim simulation and waveform

The Memory Data tab next to the Wave tab contains the RAM in the end of simulation. The contents can also be displayed by hovering on the RAM in the Wave tab, but there's a catch: if the radix is set to ASCII and the data include a curly bracket, ModelSim will throw an error when trying to show the tooltip.

Example 2 - Testing on the Tang Primer 25K

First, install Gowin EDA Student Edition (Windows, Linux, MacOS).

Study the pin assignments in the Gowin\E80.cst file and apply them to your board. Use a five-direction navigation button/joystick with Left, Right, Up, Down, Set (for Pause), and Reset. All input pins must be active high with a 10kΩ pull-down resistor and the joystick's COM port must be connected to a 3.3V output. Below is a reference photo of the board setup:

Tang Primer 25K Board Setup for E80

The top module (FPGA.vhd) uses three 8-LED rows for display, and the joystick buttons for control:

LEDs

  • Row A (Status):
    • [7:4] (Flags): Carry, Zero, Sign, Overflow.
    • [3:1] (Register Selection): Binary index of the register currently displayed on Row B.
    • [0] (Clock): Pulses dimly during normal execution, pulses brightly during reset, and turns solid bright on HLT.
  • Row B (Data): Displays the value of the selected register. When Reset is held, it mirrors the DIP switch input instead.
  • Row C (PC): Displays the current Program Counter.

Buttons

  • Joystick Left/Right: Adjust clock speed; supports auto-repeat when held down.
  • Joystick Center: Reset clock speed.
  • Joystick Up/Down: Select which register is displayed on Row B; supports auto-repeat when held down.
  • Set: Pause execution.
  • Reset: CPU initialization and firmware reset; press until the Clock LED starts flashing brightly.

To run the test, paste the following program into the editor and press F7. This will automatically update Firmware.vhd with your new code.

.TITLE "256-ROR to test joystick control"
.SIMDIP 0b00000010     ; for simulation only, FPGA ignores this
	LOAD R0, [0xFF]    ; loads the DIP input word to R0
	MOV R1, 0
loop:
	ROR R0, 1
	ADD R1, 1
	JNC loop            ; stop after 256 RORs (32 full rotations)
	HLT

You can also simulate (F5 or F8) the program prior to running in the FPGA; in this case it's suggested to match the .SIMDIP value with the DIP switches on the board.

Open the Gowin\Gowin.gprj file in the Gowin IDE. Compile the project using Run All, wait for completion, connect your Tang Primer 25K board on your PC, and then use the Programmer function to upload the configuration.

When the upload is finished, press the Reset button to initialize the RAM with your firmware, and release it when the Clock LED stops pulsing. The Clock LED will be pulsing as the program runs. LED Row B defaults to R0, so you will see it rotating. Then the HLT instruction will be executed and the Clock LED will stop pulsing.

联系我们 contact @ memedata.com