Close
0%
0%

SPAM-1 - 8 Bit CPU

8 Bit CPU with simulator and toolchain and a hardware build to follow

Similar projects worth following
SPAM-1 : Simple Programmable And Massive - v1/v2
"Massive" in the sense that it's going to be built on breadboard.

8 Bit CPU using mostly HCT chips and fully simulated in verilog with realistic chip timings.

The current design is a heck of a lot more complex than the original POC but also a lot more capable. Unlike the POC it won't be relying on google sheets as the development effort of programming in that environment is too high, and also because I want to be able to use Icarus verilog and other such tools.

---

The prototype learning exercise was simulated in Logisim Evolution with an assembler written in Google Sheets - I've not seen this done before - is it a first? My intention was to make the tooling as accessible as possible and more visual.

However, once that initial effort and sim was completed I decided that this was too trivial to actually build as hardware. So I decided to bite off a lot more.

V2 objective

I got v1 working in the sim but then decided I wasn't going to build it in hardware as these very simple CPU's aren't that capable or complex and I wanted more of a challenge; something that would force me to learn more.

So this has changed out of all recognition.

Along the way I got distrated by a hardware build of a testing device for all the chip's Ive bought for this CPU project. The testing device project can be found in my project list. That was interesting and a throw back to my Uni days in the 80's.

Now, I'm firmly back on the CPU task and nearing the end of the design and sim phase....

UPDATE: After a few more months I've finally settled on the capabilities and design and have arrived at an approach that appeals to my  sense of symmetry. I've adopted some features of ARM; specifically conditional instructions and status flag control.

Check out the project logs !



V1 objective 

⭐️ I wanted to do things a little differently to some of the other efforts on the internet! 

I wanted the assembler and any other code I write to to be more readily accessible and instantly usable to others (like you) without installing python or perl or whatever first, so I've written the assembler in google sheets!I want to be able to run at least the typical demo programs like Fibonacci

  • I would like to extend it to play some kind of basic game (tbd)
  • It will have an assembly language and assembler for it
  • I might port C to it
  • I want to simulate it first
  • I want to build it physically, or a more likely a derivative

The Assembler is in Google Sheets ....

However, on completion of this phase of the project I made these observations (sorry about the font size)...

So I changed tack ... thus V2 above.

Graphics Interchange Format - 2.18 MB - 08/03/2019 at 19:59

Preview
Download

  • 1 × Logism Evolution
  • 1 × Google Sheets

  • Github Design Documentation Updated

    John Lonergan2 days ago 0 comments

    At long last I've updated github to match the actual state of the project https://github.com/Johnlon/spam-1/tree/master

    Still some details to fill in but all the prior design stuff is pushed out of the way now and lots of up to date stuff added. 

  • Chip8 means I need a SPAM-1 "C" Compiler after all

    John Lonergan01/03/2021 at 22:22 0 comments

    It is with some regret that I have to admit that a cross compilation from Chip 8 is tricky.

    Looks like I have to write an interpreter / emulator after all. This is necessary because of the need to mess around with direct memory addresses in Chip-8. 

    I'm going to write interpreter in SPAM "C"  and what this means is that I'm back working on the SPAM "C" compiler. The big change is that I need to upgrade the SPAM "C" compiler from 8 bit to 16 bit Integers. I spent age's trying to decide whether and how to support 8 and 16 bit ints but in the end decided to switch entirely to 16 bits and then work out how to add 8 buts back into the necessary ops. The shorter int would be used most of the time in programs like Chip8 emulation however 16 bits it also needed as the Chip8 program counter is 12 bits and the Chip8 Index register is also 12 bits. So if I do everthing as 16 bits to start with then I can optimise 8 bits back into the compiler. For now I'm declaring everything as "uint16".

    It's a bit of a bummer as the ALU can do all 8 bit maths ops in a single clock cycle but even a 16 bit shift takes many more cycles and I haven't yet worked out a 16 bit multiply or divide.

    I will add maths ops to SPAM 'C' as the needs of the Chip8 emulator dictate.

    In retrospec I'l have to review whether I should have trier retargeting GCC at SPAM-1. I did read around this but the documentation I found suggested that it was a hard thing to do. On the other hand building SPAM 'C' isn't a walk in the park either. It's been educational but also a lot of head scratching and late nights.

    ----

    Later ...

    I spent a while reading up on retargeting a compiler to SPAM-1. One page ...

    https://stackoverflow.com/questions/8696229/how-to-create-a-c-compiler-for-custom-cpu

    led me here ...

    http://www.compilers.de/vbcc.html

    I will check this out too.

    Decisions decisions.

  • SPAM-1 goes CHIP-8

    John Lonergan12/24/2020 at 15:54 0 comments

    So having gotten my SpamCC compiler working I then realised that perhaps it would be a better idea to start with a CHIP-8 compiler or cross-compiler.

    chip8.pngThe benefit of going after CHIP-8 is the availability of existing software, ie retro-games, and CHIP-8 is really simple with only 35 opcodes.

    So I spent a few hours writing a CHIP-8 emulator to learn more about CHIP-8 and you can see the results in my github repo

    The upshot is that I think a compiler is actually pretty feasible. 

    A cross-compiler from CHIP-8 bytecode where I dont have the source is also pretty easy most of the time though there will be complications for a few games because of the way instructions are aligned in them. In CHIP-8 every instruction is 2 bytes wide and most of the time the instructions are aligned at an even memory address so it's easy to disassemble the code passing across it linearly. However, the occasional program doesn't align to even addresses so for these one would need to recursively disassemble the code by walking each of the jump operations to see where one ends up. This is feasible except for the Bxxx operation which can jump to a calculated address and this is a bit of a show stopper I think. Fortunately, I haven't  seen use of Bxxx and there are plenty of games that do align to even addresses. Also, if I can get the source for a game then that overcomes the disassembly & alignment issue entirely. 

    Anyway - next step will be to map this into my Verilog simulation to see the simulated SPAM-1 running a game (slowly no doubt) and then get back to the hardware build...


    PONG ...

    PONG

    BREAKOUT ...

  • Further Compiler Enhancements

    John Lonergan12/05/2020 at 06:23 0 comments

    I need to be able to return values from functions. So I had to make a change.

    I could have used a "return" statement, but decided instead to extend the function arguments to include "inout" parameters instead.

    This allows me to return zero or more value from a function call by marking the relevant params as "out". Technically are "inout" when marked in this manner.

    Below I pass 'A' into the call tree and the value is incremented in the function 'depth2' changing it to a 'B'; which we expect to be printed once control returns to the main function.

    fun depth2(b1 out) {
     var b1 = b1 + 1;
    }
    
    fun depth1(a1 out) {
     depth2(a1)
    }
    
    fun main() {
     var arg1 = 'A';
     depth1(arg1)
     putchar(arg1)
    }


    The Assembler is shown below. 

    Everything wth a ';' is a comment so there are 36 instructions to implement the above program.

    The "EQU" block defined constants for the addresses in RAM for temporary values and for function arguments and also variables used in the program.

    The names of these constants map to the static path in the program where the variable can be found.

    No stack is used at the moment so all calls and return addresses are recorded in RAM variables allocated to each function. What this aproach means is that the execution is fast at the expense of not being able to safely have functions called recursuvely; ie no "reentrant" functions. 

    If I retain this calling convention as the default and call it the "variable" calling conventions, then I can introduce a second calling convention called "stack" for those few cases where reentrant functions are needed. The function declaration would signal when it wants one or the other.     

           root_function_depth2___VAR_RETURN_HI: EQU 0
           root_function_depth2___VAR_RETURN_LO: EQU 1
           root_function_depth2___VAR_b1: EQU 2
           root_function_depth1___VAR_RETURN_HI: EQU 3
           root_function_depth1___VAR_RETURN_LO: EQU 4
           root_function_depth1___VAR_a1: EQU 5
           root_function_depth1___VAR_blkExprs2: EQU 6
           root_function_main___VAR_RETURN_HI: EQU 7
           root_function_main___VAR_RETURN_LO: EQU 8
           root_function_main___VAR_arg1: EQU 9
           root_function_main___VAR_blkExprs2: EQU 10
        0  PCHITMP = < :ROOT________main_start
        1  PC = > :ROOT________main_start
           ; (0) ENTER root_function_depth2 @ function
                 root_function_depth2___LABEL_START:
                 ; (1)  ENTER root_function_depth2 @ statementEqVarOpConst
        2               REGA = [:root_function_depth2___VAR_b1]
        3               REGA = REGA + 1
        4               [:root_function_depth2___VAR_b1] = REGA
                 ; (1)  EXIT  root_function_depth2 @ statementEqVarOpConst
        5        PCHITMP = [:root_function_depth2___VAR_RETURN_HI]
        6        PC = [:root_function_depth2___VAR_RETURN_LO]
           ; (0) EXIT  root_function_depth2 @ function
           ; (0) ENTER root_function_depth1 @ function
                 root_function_depth1___LABEL_START:
                 ; (1)  ENTER root_function_depth1 @ functionCall
                        ; (2)   ENTER root_function_depth1 @ blkExprs
                                ; (3)    ENTER root_function_depth1 @ blkVar
        7                                REGA = [:root_function_depth1___VAR_a1]
                                ; (3)    EXIT  root_function_depth1 @ blkVar
                                ; assign clause 1 result to [:root_function_depth1___VAR_blkExprs2] = a1 
        8                       [:root_function_depth1___VAR_blkExprs2] = REGA
                                ; assigning result back to REGA
        9                       REGA = [:root_function_depth1___VAR_blkExprs2]
                        ; (2)   EXIT  root_function_depth1 @ blkExprs
       10               [:root_function_depth2___VAR_b1] = REGA
                        ; set return address variables
       11               [:root_function_depth2___VAR_RETURN_HI] = < :root_function_depth1___LABEL_RETURN_1
       12               [:root_function_depth2___VAR_RETURN_LO] = > :root_function_depth1___LABEL_RETURN_1
                        ; do jump to function 'depth2''
       13               PCHITMP = < :root_function_depth2___LABEL_START
       14               PC = > :root_function_depth2___LABEL_START
                        ; return location
                        root_function_depth1___LABEL_RETURN_1:
       15               REGA = [:root_function_depth2___VAR_b1]
       16               [:root_function_depth1___VAR_a1] = REGA
                 ; (1)  EXIT  root_function_depth1 @ functionCall
       17        PCHITMP = [:root_function_depth1___VAR_RETURN_HI]
       18        PC = [:root_function_depth1___VAR_RETURN_LO]
           ; (0) EXIT  root_function_depth1 @ function
           ; (0) ENTER root_function_main @ function...
    Read more »

  • SPAM-1 'C' Compiler - good progress

    John Lonergan11/30/2020 at 19:25 0 comments

    Hot on the heels of writing an assembler I decided to take my learning and have a go at a higher level language with C-family syntax. 

    To be honest the syntax so far looks more like groovy (or even Kotlin) , because of the lack of typing and the use of the "var" keyword and the "main()" function approach looks like Kotlin.

    This is being written in Scala because it's a great language for this kind of thing due to its pattern matching and also it's Parser Combinator library which makes parsing a breeze (mostly).

    An example of a countdown program is shown  below.

    The steps in the automated tests are 

    - compile the "SPAM-C" code into Asm files

    - assemble the generated Asm file into ROM images

    - run the ROM images in the verilog simulator

    These steps happen automatically in the test and this has helped spot loads of potential issues along the way.

    SPAM-C code .....

    def main() {
         var a=10;
         while(a>0) {
             var a=a-1;
             putchar(a)
         }
     }

    The compiler spits out this assembler which is then assembled and the verilog simulator invoked so I get to see the program running too.

    root_function_main_a: EQU 0
    ; (0) ENTER root_function_main @ function
          ; (1)  ENTER root_function_main @ statementVar
                 [:root_function_main_a] = 10
          ; (1)  EXIT  root_function_main @ statementVar
          ; (1)  ENTER root_function_main_whileCond1 @ whileCond
                 root_function_main_whileCond1__2__check:
                 ; (2)   ENTER root_function_main_whileCond1 @ condition
                         REGA = [:root_function_main_a]
                         REGA = REGA PASS_A 0 _S
                 ; (2)   EXIT  root_function_main_whileCond1 @ condition
                 PCHITMP = <:root_function_main_whileCond1__2__top
                 PC = >:root_function_main_whileCond1__2__top _GT
                 PCHITMP = <:root_function_main_whileCond1__2__bot
                 PC = >:root_function_main_whileCond1__2__bot
                 root_function_main_whileCond1__2__top:
                 ; (2)   ENTER root_function_main_whileCond1 @ statementEqVarOpConst
                         REGA = [:root_function_main_a]
                         REGA = REGA - 1
                         [:root_function_main_a] = REGA
                 ; (2)   EXIT  root_function_main_whileCond1 @ statementEqVarOpConst
                 ; (2)   ENTER root_function_main_whileCond1_putcharN @ statementPutcharName
                         root_function_main_whileCond1_putcharN__wait_3:
                         PCHITMP = <:root_function_main_whileCond1_putcharN__transmit_4
                         PC = >:root_function_main_whileCond1_putcharN__transmit_4 _DO
                         PCHITMP = <:root_function_main_whileCond1_putcharN__wait_3
                         PC = <:root_function_main_whileCond1_putcharN__wait_3
                         root_function_main_whileCond1_putcharN__transmit_4:
         ...
    Read more »

  • SPAM-1 Assembler

    John Lonergan11/14/2020 at 19:54 0 comments

    Decided to get on with writing an assembler for SPAM-1 and use this as an opprtunity to learn about writing grammars.

    My previous effort at writing an Assembler was way back when I was experimenting with using Google sheets for the assembler for my Logisim model. At that time I was shy of speaking on camera so I recorded a silent video of using that software. See "Prequel 1" below. Attempting to use Google sheets for the toolchain is quite limiting obviously. I turned to using Icarus Verilog to te idea of using Google sheets for anything more was bust. So I've reverted to proper dev tools and Linux.

    My first attempt recently was using Antlr4 to write a grammar and parser and I spent a couple of days on this. But I became quickly frustrated with the approach. I found it pretty difficult to get the software to do what I wanted and while there's a lot of documentation but it didn't help me much when I had problems. Also I didn't particularly like the approach of writing a text file grammar and then having to compile it into code. I spent quite a few years programming in Scala and I was aware of better alternatives.

    So, I switched to using scala and scala's "parser combinators" to build an assembler. Parser combinators are just a library built into the SDK that provide some fancy Scala operator syntax to allow the building of Lexer/Parsers really easily, and without all the hassle of Antlr and the entire thing is done in the scala language itself and not two different modes like Antlr. This aproach shortened the dev cycle and the strong type system in scala helped to direct the work and avoid subtle type related bugs. I also found a bunch of useful parser combinator stuff to use for inspiration such as a 6502 parser that I could learn from.

    Every operation in SPAM-1 is an assignment like REGA=REGB+REGC and the assembly language looks like this ...

    ; define a constant called SOMEVAR using a constant expression
    ; forward references to label addresses are permitted
    
    SOMEVAR:    EQU ($0100 + %1010 + $f + 1+2+(:LABEL2+33)) ; some arbitrarily complicated constant expression
    
    ; grab the top and bottom bytes of the constant SOMEVAR into two constants
    
    TOPBYTE:    EQU <:SOMEVAR                        ; top 8 bits into TOPBYTE
    BOTBYTE:    EQU >:SOMEVAR                        ; bottom 8 bits into BOTBYTE
    
    ; demo setting registers to constants
    
                REGA = $ff                          ; set A to the constant hex ff but do not set processor status flags
                REGB = $ff    _S                    ; set A to the constant hex ff and set the processor status flags
    
    ; registers can be set to results of ALU operations
    
    LABEL1:     REGA = REGB   _C_S                  ; if Carry is set then update A to value of B and set the flags
                REGA = REGB
                REGA = REGA A_PLUS_B REGC           ; set A = A + B but do not set flags
                REGA = REGA + REGC                  ; set A = A + B but do not set flags            REGA = REGB A_MINUS_B [:SOMEVAR]    ; set B to the data at RAM location :SOMEVAR
    LABEL2:
                REGA = :TOPBYTE                     ; set A to the constant
    
    ; unconditional jump to whatever SOMEVAR was pointing to
    
                PCHITMP = :TOPBYTE                  ; prepare the top PC register for a jump
                PC      = :BOTBYTE                  ; execute the jump to the location defined by {TOPBYTE:PCHITMP}
    
    END
    

    You can see the code for the assembler here .. https://github.com/Johnlon/spam-1/tree/master/verilog/assembler 

    The grammar is pulled together by the Parser class https://github.com/Johnlon/spam-1/blob/master/verilog/assembler/src/main/scala/Parser.scala

    Next revision will be to have it write the ROM files for the Verilog simulation so I can double check the solution. I've written some automated scalatest tests so I'm hopeful.

    Have fun.

    ===

    Below is my eariest effort at a YT video :)

  • Triple Port Register Patterns

    John Lonergan10/29/2020 at 05:53 0 comments

    Short video covering the register file pattersn I've used in the CPU.

    Extension of the previous video ...

  • 4x8 Register File video

    John Lonergan10/28/2020 at 08:50 0 comments
  • Fried Chips Flavoured Register File

    John Lonergan10/26/2020 at 04:43 0 comments

    Precursor to a video about SPAM-1's register file component.

    This video looks at the 74HCT670.

  • Feature Envy Video - Conditional Execution

    John Lonergan10/21/2020 at 18:21 0 comments

    CircuitVerse sim of the new control logic https://circuitverse.org/users/7507/projects/spam-1-conditional-instruction-logic

    Bonus ! Heres a video on Instructrion Set Arch that I thought was really good https://youtu.be/myJCfYSj9jk (past of a series)

View all 38 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates