Yet Another Minimalistic One Instruction CPU
SUBLEQ means SUBtraction jump on Less than or EQual to zero.
To make the experience fit your profile, pick a username and tell us what interests you.
We found and based on your interests.
I build a simple compiler tool train for Subleq but the results are quite disappointing. Subleq is very memory inefficient:
So the minimum address space is 12 bits (16 bit would be more useful).
No chance of using a simple front panel for this CPU.
And it will be slow. Amazingly slow!
I have done some PCB and strip-board designs but input and output will need to be via an Arduino.
So overall a sad situation.
So I have decide to suspend this project.
AlanX
Coding directly in SUBLEQ is necessary initially but is very tedious. Macros are the way to go.
There are a couple of software packages that are suitable:
and others.
I did not like the syntax of M4 and NASM. Here is a NASM example:
/* macro TRUE_BRANCH: correctly handles underflow */ %define TRUE_BRANCH(b,GEZ,LTZ) \ T T ? \ b T TEST_GEZ \ T T LTZ \ TEST_GEZ: P T GEZ \ P T LTZ // Examples: TRUE_BRANCH(x,xGEZ,xLTZ)Which produces:
%line 1+1 macros.nasm /* macro TRUE_BRANCH: correctly handles underflow */ %line 8+1 macros.nasm // Examples: T T ? x T TEST_GEZ T T xLTZ TEST_GEZ: P T xGEZ P T xLTZ
Okay lets try the %macro:
/* macro TRUE_BRANCH: correctly handles underflow */ %macro TRUE_BRANCH 3 T T ? %1 T TEST_GEZ T T %3 TEST_GEZ: T T %2 T T %3 %endmacro // Examples: TRUE_BRANCH(x,xGEZ,xLTZ)
Which produces:
%line 1+1 macros.nasm /* macro TRUE_BRANCH: correctly handles underflow */ %line 9+1 macros.nasm // Examples: T T ? %line 11+0 macros.nasm x T TEST_GEZ T T xLTZ TEST_GEZ: T T xGEZ T T xLTZNot pretty and not easy to suppress the %Line comments (i.e. .nolist ?).
The C pre-processors cpp, mcpp and gpp are very similar, but gpp stands out as it honours new lines in its standard mode and emits less commentary.
Here is gpp:
/* macro TRUE_BRANCH: correctly handles underflow */ #define TRUE_BRANCH(b,GEZ,LTZ) \ T T ? \ b T TEST_GEZ \ T T LTZ \ TEST_GEZ: P T GEZ \ P T LTZ
For:
TRUE_BRANCH(x,xGEZ,xLTZ)
Which gets expanded to this:
T T ? x T TEST_GEZ T T xLTZ TEST_GEZ: P T xGEZ P T xLTZThe problem with cpp and mcpp is that they do not honour the newline, this is what you get:
#line 1 "C:/AlanX/subleq/Macros/macros.inc"
#line 10 "C:/AlanX/subleq/Macros/macros.inc"
T T ? x T TEST_GEZ T T xLTZ TEST_GEZ: P T xGEZ P T xLTZ
I have to add a new line marker ("nl") and then use an AWK script to convert it to a real new line:
/* macro TRUE_BRANCH: correctly handles underflow */ #define TRUE_BRANCH(b,GEZ,LTZ) nl\ T T ? nl\ b T TEST_GEZ nl\ T T LTZ nl\ TEST_GEZ: P T GEZ nl\ P T LTZ // Examples: TRUE_BRANCH(x,xGEZ,xLTZ)Now I get:
#line 1 "C:/AlanX/subleq/Macros/macros.inc" #line 10 "C:/AlanX/subleq/Macros/macros.inc" T T ? x T TEST_GEZ T T xLTZ TEST_GEZ: P T xGEZ P T xLTZWhich is pretty close.
Here is the AWK script:
awk "{ gsub(/nl/,\"\n\"); print }" macros.tmp >macros.sub
It was an absolute headache working out how to get AWK to work with Windows for this one liner!
The purpose for this log is to record my macros:
( SUBLEQ MACROS ) ( System defaults ) #define system() \ ( DEFAULTS ) \ @INPUT -1 \ @OUTPUT -2 \ @HALT 0 \ @Z -9 \ @T -10 \ @P -11 \ @N -12 \ @SP -13 \Read more »
Z Z ? ; Z=0 \ T T ? ; T=0 \ -1 P ? ; P=1 \ 1 N ? ; N=-1 \ 32 SP ? ; SP=-32 \ ( FREE VARIABLES ) \ @a -14 \ @b -15 \ @c -16 \ @d -17 \ @e -18 \ @f -19 \ @g -20 \
( MACRO VARIABLES ) \ @t -21 ; Macro temporary variable \ @w -22 ; Word width test \ @TOP_OF_FREE_RAM -33 \ ( POINTER TO POINTER COPY EQUATES ) \ @PPC -32 \ @NDATA -23 \ @SRC -32 \ @DST -28 \ @RTN -24 ( Load Pointer to Pointer Copy to RAM ) #define LoadPPC() \ ( POINTER TO POINTER COPY EQUATES ) \ @NDATA -23 \ @SRC -32 \ @DST -28 \ @RTN -24 \ ( POINTER TO POINTER COPY ROUTINE LINES ) \ @PPC_L1 -32 ; SRC \ @PPC_L2 -31 ; NDATA \ @PPC_L3 -30 ; ? (=PPC_L4) \ @PPC_L4 -29 ; NDATA \ @PPC_L5 -28 ; DST \ @PPC_L6 -27 ; ? (=PPC_L7) \ @PPC_L7 -26 ; T \ @PPC_L8 -25 ; T \ @PPC_L9 -24 ; RTN \ ( COPY ROM CODE TO RAM - THE HARD WAY ) \ PPC_L1 PPC_L1 ? \ T T ? \ PPC_D1 T ? \ T PPC_L1 ? \ PPC_L2 PPC_L2 ? \ T T ? \ PPC_D2 T ? \ T PPC_L2 ? \ PPC_L3 PPC_L3 ? \ T T ? \ PPC_D3 T ? \ T PPC_L3 ? \ PPC_L4 PPC_L4 ? \ T T ? \ PPC_D4 T ? \ T PPC_L4 ? \ PPC_L5 PPC_L5 ? \ T T ? \ PPC_D5 T ? \ T PPC_L5 ? \ PPC_L6 PPC_L6 ? \ T T ? \ PPC_D6 T ? \ T PPC_L6 ? \ PPC_L7 PPC_L7 ? \ T T ? \ PPC_D7 T ? \ T...
TTA (Transport Triggered Architecture) and SUBLEQ can use pointer but it requires the use self modifying code. Reading data from a memory location to a variable called DATA from an address stored in (pointed to by) a variable called ADDR is an example. We can write it as:
Not to be confused with copy or move:
DATA = [ADDR] can be coded as:
T T ? ; T=0
PTR PTR ? ; PTR=0
ADDR T ? ; T=-ADDR
T PTR ? ; PTR=ADDR
T T ? ; T=0
PTR: T T ? ; T=-PTR (self modifying code)
DATA DATA ? ; DATA=0
T DATA ? ; DATA=[ADDR]
Although the assembler thinks the first T on the PTR: line is real and initially set with the address of "T", the code above overwrites the address with the value held in ADDR. The only problem is that code cannot reside in ROM (as it is self modifying).
A more general form would be:
This would be coded as:
SRC_PTR SCR_PTR ? ; Copy SRC to SRC_PTR T T ? SRC T ? T SRC_PTR ? DST_PTR DST_PTR ? ; Copy DST to DST_PTR T T ? DST T ? T DST_PTR ? NDATA NDATA ? ; COPY [SRC] TO NDATA TO [DST] SRC_PTR: SRC_PTR NDATA ? ; SELF MODIFYING CODE NDATA DST_PTR: DST_PTR ? ; SELF MODIFYING CODE T T RTN ; RETURN
Getting code in ROM to RAM without self modifying code is tricky, but first an assembler update.
A space is required between tokens.
SUBLEQ defined variables:
SUBLEQ defined constants (value should not be changed):
Comments:
Address Labels:
Constants and Literals:
Examples of code (note: line numbers are not part of the assembler code):
Example assembler output (note: line numbers are not part of the assembler output):
Note that the ".label" command exports variable values one per line while the "label:" command exports as a triplet (i.e. a SUBLEQ instruction).
The following code fragments sets up a pointer [ADDR1] to pointer [ADDR2] copy that returns the DATA copied:
( SET SYSTEM DEFAULTS)
@INPUT -1
@OUTPUT -2
@HALT 0
@Z -9
@T -10
@SP -11
( VARIABLES )
@A -12
@B -13
@C -14
@D -15
@E -16
@F -17
@L -18
@H -19
@I -20
@J -21
@K -22
@TOP_OF_FREE_RAM -33
( POINTER TO POINTER COPY EQUATES )
@PPC -32
@NDATA -23
@SRC -32
@DST -28
@RTN -24
( POINTER TO POINTER COPY ROUTINE LINES )
@PPC_L1 -32 ; SRC
@PPC_L2 -31 ; NDATA
@PPC_L3 -30 ; ? (=PPC_L4)
@PPC_L4 -29 ; NDATA
@PPC_L5 -28 ; DST
@PPC_L6 -27 ; ? (=PPC_L7)
@PPC_L7 -26 ; T
@PPC_L8 -25 ; T
@PPC_L9 -24 ; RTN
( COPY ROM CODE TO RAM - THE HARD WAY )
PPC_L1 PPC_L1 ? T T ? PPC_D1 T ? T PPC_L1 ?
PPC_L2 PPC_L2 ? T T ? PPC_D2 T ? T PPC_L2 ?
PPC_L3 PPC_L3 ? T T ? PPC_D3 T ? T PPC_L3 ?
PPC_L4 PPC_L4 ? T T ? PPC_D4 T ? T PPC_L4 ?
PPC_L5 PPC_L5 ? T T ? PPC_D5 T ? T PPC_L5 ?
PPC_L6 PPC_L6 ? T T ? PPC_D6 T ? T PPC_L6 ?
PPC_L7 PPC_L7 ? T T ? PPC_D7 T ? T PPC_L7 ?
PPC_L8 PPC_L8 ? T T ? PPC_D8 T ? T PPC_L8 ?
PPC_L9 PPC_L9 ? T T ? PPC_D9 T ? T PPC_L9 ?
T T JMP
( POINTER TO POINTER COPY CODE TO BE MOVED )
.PPC_D1...
Read more »
Input/Output (IO) in SUBLEQ is not as straight forward as you would expect.
Output
Output (i.e. copy A to [0xff] or (-1)):
where:
If we just did:
then export value would be:
where B is the value read.
if B = -1 then the exported value is -1 - A or ~B, then all that needs to be done is to invert the output.
So the following would work:
As I pull the address and data busses up with 22k resistors then the 74LS373 is redundant and can be removed (providing nothing else uses its address space). To save the output inverters we could use:
For TTL and LEDs the following would work (note: the pull up resistors supply most of the current for the LEDs):
If we are writing to LEDs then all we really need is:
And:
Input
Input is similar:
And here is a switch design:
If the switches are inverted logic then
And here is a switch design:
It would be useful to have a NAND in hardware. As the inputs to the NAND are inverted and the output is inverted than a NOR gate will result in a NAND function. Why NAND? All the other functions can be generated and the XOR can be generated as shown below but in software:
In summary:
For output:
For Input:
AlanX
I added an 8 bit front panel to the schematic:
The Front Panel maps 128 bytes of PROM, 120 bytes of RAM and 8 bytes of IO. So I thought I better do the Monitor Program before I went any further as the PROM for the Weird CPU was only just enough.
The first problem was that writing to the IO port involves a read first and I had not anticipated that. Two fixes, make the port a register or map the port to a RAM address.
The second problem is that as I use momentary contact switches, I need an XOR to toggle the bits in the Data and Addr variables. As SUBLEQ has no hardware bit operations I wrote some C code with pseudo SUBLEQ code:
char xor(char a, char b) { char c=0; char w=0; loop1: // c=c+c subleq(c,&z); subleq(z,&c); subleq(z,&z); /* TEST 1 A<0 AND B>=0 */ // Test A<0 subleq(t,&t); if (subleq(a,&t)) goto TEST1A_GEZ; // TEST_GEZ if (subleq(t,&t)) goto TEST1B; // TRUE_LTZ TEST1A_GEZ: if (subleq(1,&t)) goto TEST2; // TRUE_GEZ // TRUE_LTZ (-128 found) TEST1B: // Test B>=0 subleq(t,&t); if (subleq(b,&t)) goto TEST1B_GEZ; // TEST_GEZ if (subleq(t,&t)) goto TEST2; // TRUE_LTZ TEST1B_GEZ: if (subleq(1,&t)) goto SETBIT; // TRUE_GEZ if (subleq(t,&t)) goto NEXT; // TRUE_LTZ (-128 found) TEST2: // A>=0 AND B<0 // Test B<0 subleq(t,&t); if (subleq(b,&t)) goto TEST2B_GEZ; // TEST_GEZ if (subleq(t,&t)) goto TEST2A; // TRUE_LTZ TEST2B_GEZ: if (subleq(1,&t)) goto NEXT; // TRUE_GEZ // TRUE_LTZ (-128 found) TEST2A: // Test A>=0 subleq(t,&t); if (subleq(a,&t)) goto TEST2A_GEZ; // TEST_GEZ if (subleq(t,&t)) goto NEXT; // TRUE_LTZ; TEST2A_GEZ: if (subleq(1,&t)) goto SETBIT; // TRUE_GEZ; if (subleq(t,&t)) goto NEXT; // TRUE_LTZ (-128 found) SETBIT: subleq(-1,&c); NEXT: // a=a+a subleq(a,&z); subleq(z,&a); subleq(z,&z); // b=b+b subleq(b,&z); subleq(z,&b); subleq(z,&z); // w=w+w+1 - Avoid -128 bug subleq(t,&t); subleq(w,&t); subleq(t,&w); subleq(-1,&w); subleq(t,&t); if (subleq(w,&t)) goto loop1; return c; }I had lots of problems with underflow (i.e. the value -128) which took time to solve. The problem is that -(-128) == -128! The code is efficient and could be adapted to other bit hardware (i.e. AND, NAND, OR, NOR and SHR). The code works out the word width by itself.
Unfortunately the code is about +34 instructions or +102 bytes long! I will need to add a hardware XOR to the schematic if I want to stay with 8 bits.
This test must be popular for SUBLEQ coders as it is only two instructions and A is not modified:
Here is a true A >= 0:
Does it really matter that we should always use the true A >+ 0 version? Consider the C code for loop:
The compiler will not mind (quite legal) but after counting from -128 to 127 it repeats infinitely.
It appears as if it does not matter that the comparison in the for loop fails.
In the above pseudo code I have used the z (=zero) convention of ensuring it is set to 0 after use. Where I can not do that I use t (=temp) where it must the zeroed before use.
Two other coding conventions would "n" for -1 and "p" for +1.
I found a great site that helped with the general structure for XOR subroutine. Worth a look:
http://bisqwit.iki.fi/story/howto/bitmath/
Oh well, I am forced to use PCB mount slide switches:
So the Monitor pseudo code would be:
Today I looked at Mazonka's SUBLEQ wehpage (http://mazonka.com/subleq/) and his Higher SUBLEQ webpage (http://mazonka.com/subleq/hsq.html). Higher SUBLEQ is actually a C Compiler for SUBLEQ. His site also has an on-line C Compiler, Assembler and Interpreter (http://mazonka.com/subleq/online/hsqjs.cgi). The problem with Mazonka's work is it is a bit too advanced for me at the moment.
What I was really looking for was some code that I could have a look at. That I can manually step through so that I could get an idea how the language works. I came across Sandro Maffiodo "OI" webpage (http://www.assezeta.com/sandromaffiodo/oi/) which had what I wanted. His assembler syntax was very simple and yet does the job. Here is his "Hello World!":
(OI Assembler by Sandro Maffiodo
This example write hello world)
H -2 .
E -2 .
L -2 .
L -2 .
O -2 .
BLANK -2 .
W -2 .
O -2 .
R -2 .
L -2 .
D -2 .
BANG -2 -1
(ASCII characters)
.H 72
.E 69
.L 76
.O 79
.BLANK 32
.W 87
.R 82
.D 68
.BANG 33
So Let Have A LookFirst comments start with a "(" and end with a ")".
Next the lines are for the form "A B C" (except for data) which are constants.
The SUBLEQ microcode is:
On the first code line, the "A" address is called the letter H and is a label for an address that the compiler has to work out. The actual value for H is stored at ".H" and no surprise, is the value 72 or ASCII "H".
The "-2" (or "B" address) is a special symbol for the address of the ASCII input/output port.
The "." is the jump address for the next instruction that the compiler has to work out.
Step Through
Move the value 72 or "H" (stored at at address H) to the ASCII output port (-2) then jump to the next instruction. Repeat for the remaining letter of "HELLO WORLD!".
The last letter "!" however, jumps to address "-1" which is understood to mean "halt".
Now was that so hard?
Maffiodo's webpage has an assembler and an interpreter for download. He even codes a SUBLEQ interpreter in "SUBLEQ". Then he gets a compiled "C Code" SUBLEQ interpreter to run (interpret) a the "SUBLEQ" SUBLEQ interpreter that runs another "SUBLEQ" SUBLEQ interpreter that runs another "SUBLEQ" SUBLEQ interpreter that runs the "SUBLEQ" program "Hello".
To do this the "SUBLEQ" interpreter has to relocate the code each time in memory. Pretty smart! The SUBLEQ SUBLEQ interpreter only uses 348 memory locations!
Anyway I downloaded his assembler program and rebuilt it. My version is longer as Maffiodo uses many coding tricks to reduce his code size. I have never actually see code that uses the "," operator! My coding background is Pascal so I code with very tight typing. Maffiodo is very fluid in this regard. The program is actually quite simple:
I want to add an inline macro section for macros like:
Anyway here is my current version of Maffiodo's assembler code:
// SUBLEQ ASSEMBLER // ================= // // SYNTAX // ------ // // A space is required between tokens. // // SUBLEQ defined variables: // Z ( Zero, if used must be cleared after use ) // T ( Temp, must be cleared before use ) // // SUBLEQ defined constants (value should not be changed): // N ( Negative or -1 ) // P ( Positive or +1 ) // // Comments: // ( begin in-line comment // ) end in-line comment // ; end of line comment // // Address Labels: // . next address // ? next address // label label for address reference or variable // .label variable (location) for label (compiler sets address) // label: address reference (goto location) for label (compiler sets address) // @label absolute (fixed) address for label (no code generated) // // Constants or Literals: // Digits constant (example "1") // -? constant (example...Read more »
Had a sleep on the schematic issues and decided to make few changes.
Here is the updated Reset/Clock/Decoder:
And the Timing Signals:
Here is the Memory Address Register and Instruction Pointer:
Unfortunately the 74LS373 does not seem to be working in TinaTI so I can not test the circuit(?). In theory the simulation is an infinite loop (Subleq 0 0 0). And finally, here is the ALU:
More revisions:
Here is the updated ALU:
As far as I can work out the Carry (i.e. Borrow) is all I need!
Bus conflicts are not usually a problem with simple systems as they do no damage. The reason is that TTL signals cannot supply much current (100s of uA). But an Arduino can supply ~100 mA so a bad bus conflict can do damage. I blew up my 4 Bit CPU RAM through careless testing of the memory (I have since modified the write cycle to solve this problem).
Here is the problem write cycle (1), the memory decoder fix for the 4 Bit CPU (2) and the CPU fix (3):
In case (3) the write cycle is less than ideal for latching registers so I have added an IO_STB signal o the decoder schematic (which can be inverted for IO_CLK in necessary).
The problem with PCBs are:
So what is PCB/bus planning is about:
Here my first pass at PCB/bus planning:
You can see that I have:
Here is the updated schematic with provision for expansion:
Well I can say that it is not often a project get simpler as you progress!
Working
I swapped back to 74LS374s were I could and got the 74LS373s working (just!). I am really pushing my luck with TinaTI! I have got the simulation to execute the infinite loop which pretty well says I got it. The address bus and to a lesser extent the data bus is a glitch mess. I will have to analysis the decoder signals more closely, to find out why. It may just be TinaTI warming me it is close to falling over again. Here is the latest schematic:
I have swapped out the schmitt trigger relaxation oscillator for a gated crystal oscillator and the three 74LS74 D-Latches for a 74LS161.
The signals look okay with an 8 MHz crystal. The crystal oscillator is a little unusual in using a schmitt trigger for the active component but it works well.
I have swapped the strobe signals back to clock signals.
Some tweaks with the ALU carry in and carry out.
I have great expectations for this CPU so I will go to 16 bit up front (two 8 bit stacks).
I need to...
Read more »Wikipedia presets the following SUBLEQ pseudocode:
int memory[], program_counter, a, b, c program_counter = 0 while (program_counter >= 0): a = memory[program_counter] b = memory[program_counter+1] c = memory[program_counter+2] if (a < 0 or b < 0): program_counter = -1 else: memory[b] = memory[b] - memory[a] if (memory[b] > 0): program_counter += 3 else: program_counter = c
In my case signed integers and a stopping condition are not required:
int memory[], program_counter, a, b, c program_counter = 0 while (true): a = memory[program_counter] b = memory[program_counter+1] c = memory[program_counter+2] if (memory[b] > memory[a]): memory[b] = memory[b] - memory[a] program_counter += 3 else: memory[b] = memory[b] - memory[a] program_counter = c
The schematic considers the scaling up to any word width (i.e. 8, 16, 14 or 32 bit).
The first step is the Reset, Clock and Decoder logic.
Initially I used Logic Friday to develop a schematic but the schematic when modelled in Tina TI was badly glitched. I did fix it with D-latches but the chip count was highe than I have achieved using manual methods.
Here is a schematic for my decoder:
And here are the signals:
I have been using a particular static RAM timing cycle that up until I blew up a RAM chip I thought was okay (i.e. after the !WE pulse the RAM chip did't care less with regard to memory conflicts - Wrong!).
So after carefully reviewing the RAM datasheet I now use the above !DATA_OUT, !OE (not used on my RAM chips) and !WE. !DATA_OUT waits until !WE disables the RAM output.
The instruction pointer uses an ADDER to increase the base memory address rather than a COUNTER. The DECODER provides the address increments (signal IP0 and IP1):
If "!JMP" is low then the Address C replaces IP (the 374) upon the LDC signal. Else the IP is incremented by 3 (via the ADDER and the Carry In).
The Memory Access Registers (MAR) hold the fetched memory address that is then used to load the pointed values. (much like my Weird CPU):
Its not much, it just subtracts A from B and tests for a JMP (i.e. A >= B):
The schematics need review and I have not considered the Memory/Input/Output decoder or the Front Panel yet. I am also consider changing the signal logic to use 373's rather than 374's.
The number in the labels of each drawing is the number of chips so the CPU will require 29 chips.
AlanX
Create an account to leave a comment. Already have an account? Log In.
Hi Paul,
Sorry I missed your comment.
If you get started let me know. I am happy to join your project and do some of the work.
When I started this project I wanted something that would make a good demo, it should be field programmable (like my TTA8), that is you could enter a short program to demonstrate it working as a general programmable computer. SubLEq is not really suited because the coding is difficult to understand and lengthy.
Perhaps I have set too high a standard. Perhaps a preprogrammed binary counter is good enough.
When you use an Arduino make sure you put 1k resistors inline. The problem is that during (normal) memory conflicts, standard logic chips cannot supply enough current to damage the memory. The Arduino can supply 40mA per pin versus 4mA for 74HCXXX.
I tried coding in Forth but I just could not work out how to follow the stack using pencil and paper. I am too old to keep it in my head.
Regards Alan
Good luck with this project, i hope you was able to reach some results. If you search for some application to run on that CPU, would like to suggest you my project (Dawn operating system) https://hackaday.io/project/158329-dawn-the-subleq-operating-system-by-geri
Become a member to follow this project and never miss any updates
Hi, I'm taking up your project. I'm interested in building a 16-bit word version of this machine, 32k of ROM and 32k of RAM. In the space of home-brew CPUs this one is fairly unique, and it would be a challenge to get it up and running.
Your assessment of the number of instructions to implement the C code is not encouraging, but I'm not planning to use C. I want to port E-Forth to this machine, as I'm interested in Forth, and I think it would suit this resource-poor machine well. There are 30-odd primitive words in E-Forth, which form the foundation of the language, and these nees to be implemented in assembly language for the target machine. Most are quite simple, and once they are implemented, the rest of the Forth system, and indeed user code, occupies very little space.
I'm not worried by using an Arduino to communicate with the machine, it would need some terminal anyway for communication, and the Arduino can handle this I/O nicely.
Would you re-open the project, or should I start another?
Paul