Reboot
Sometimes a project gets "long in the tooth" with no clear exit point.
In many respects I am at this point. Progress is very slow. The exit point is far away.
To reboot you need a new way?
I was looking at my SubLEq compiler and I realised that of the two huge faults: low code density and low speed, I could have fixed low code density by using a library rather than direct SubLEq code. That is just substituting OpCodes with SubLEq code.
With that in mind looking at my PL/0 compiler, it produces P-Code (27 primitives) that is converted into OpCodes (35 primitives), it struck me that 35 primitives for the TTA8 is quite doable.
PL/0?
PL/0 is a Pascal like language written by Niklaus Wirth. I have ported his code to C and extended the capability of the original code.
Here is an example of PL/0:
{ Program Test String } (* Program Test String *) /* Program Test String */ // Program Test String const true=1,false=0; const min=-2147483648; var Str[12],Msg[12]; procedure ReadStr; var i,test; { Read a Pascal string, uses Str } begin i:=1; test:=true; while test=true do begin getc Str[i]; if Str[i]<32 then begin Str:=i-1; { Str == Str[0] } test:=false end else begin if i=12 then begin Str:=i end else begin i:=i+1 end end end end; procedure WriteStr; var i; { Write a string, uses Str } begin i:=1; while i<=Str do begin putc Str[i]; i:=i+1 end end; procedure WriteMsg; var i; { Write Msg } begin i:=1; while i<=Msg do begin putc Msg[i]; i:=i+1 end end; begin Msg:="String Test"; call WriteMsg; putc 10; { new line } Msg:="Enter str: "; call WriteMsg; call ReadStr; Msg:="You wrote: "; call WriteMsg; call WriteStr; putc 10; { new line } putc 10; { new line } end. // End Program Test String
And the run:
Begin PL/0: String Test Enter str: Hello World You wrote: Hello World End PL/0.
Why PL/0? Really it could be any simple P-Code compiler. I just have this one on hand. I would most likely upgrade to a C like compiler in the future.
So what are the OpCode primitives:
// CPU model 16 bit// OpCodes
- int Ax; // Primary Register
- int Bx; // Secondary Register
- int Tx; // Temporary Register
- int PC; // Program counter
- int SP; // Stack pointer
- int BP; // Base pointer (base of active stack)
// System
- pushAx() {SP--;stack[SP]=Ax;}
- popAx() {Ax=stack[SP];SP++;}
- popBx() {Bx=stack[SP];SP++;}
- subAxBx() {Ax=Ax-Bx;}
- addAxBx() {Ax=Ax+Bx;}
- mulAxBx() {Ax=Ax*Bx;}
- divAxBx() {if (Bx!=0) {Ax=Ax/Bx;} else {Ax=min;}}
- modAxBx() {if (Bx!=0) {Ax=Ax%Bx;} else {Ax=min;}}
- oddAx() {Ax=Ax&1;}
- eqAxBx() {Ax=(Ax==Bx)?1:0;}
- ltAxBx() {Ax=(Ax<Bx)?1:0;}
- leAxBx() {Ax=(Ax<=Bx)?1:0;}
- neAxBx() {Ax=(Ax!=Bx)?1:0;}
- gtAxBx() {Ax=(Ax>Bx)?1:0;}
- geAxBx() {Ax=(Ax>=Bx)?1:0;}
- movAxBx() {Ax=Bx;}
- movTxAx() {Tx=Ax;}
- movBxTx() {Bx=Tx;}
- movBxImm() {Bx=Operand[PC-1];}
- jmp() {PC=Operand[PC-1];}
- jz() {if (Ax==0) PC=Operand[PC-1];}
- movPCAx() {PC=Ax;}
- movAxPC() {Ax=PC;}
- movAxSP() {Ax=SP;}
- movSPAx() {SP=Ax;}
- movAxBP() {Ax=BP;}
- movBPAx() {BP=Ax;}
- decAx() {Ax--;}
- incAx() {Ax++;}
- movAxIndirectAx() {Ax=stack[Ax];}
- movIndirectAxBx() {stack[Ax]=Bx;}
- write() {write(Ax);} // Put int
- putC() {putc(Ax);} // Put char
- read() {read(Ax);} // Read int
- getC() {Ax=(int)ch;} // Get char
- // Hidden Primitives
- atoi()
- itoa()
16 Bit
Something I have to do is code the primitives as 16 bit words and linearise the address space. This will require a 16 bit PC, SP and CPU registers. These will need to be accessable to the MicroCode as AL and AH for the AX register.
I also have to code 16 bit imult and idiv in micro code.
Coding the Virtual Machine
Spent a day or so micro-coding. With practice you get better, develop new tricks. Anyway, it looks like:
- ROM0 Setup/Initialisation
- ROM1 Fetch and Increment PC
- ROM2 OpCodes 0 to 15 Jump Table
- ROM3 OpCodes 16 to 31 Jump Table
- ROM4 OpCodes 32 to 47 Jump Table
- ROM5 OpCodes
- ROM6 OpCodes
- ROM7 OpCodes
- And more OpCodes!
The Fetch routine is 82 bytes of code and 12 constants, so a dedicated fetch for the PC, SP and AX is expensive. I also need a Deposit as well. So more work required.
Had looked at "idiv" and "imult", about two ROM pages each. Expect the same for "atoi" and "itoa".
Somewhere in the virtual machine (i.e. VM) I need to check interrupts and code the serial IO. Between ROM1 and ROM2 looks right.
Also looking to have a fixed but separate code for indirect read and write. Saves setting up the registers each time.
And I nearly forgot, need to check the front panel.
So the VM is a relatively big task compared to the Interpreter.
Update
I have been busy coding. So far I have coded the VM:
- DECODE: PC to PP:IP
- FETCH: DATA = [PP:IP]
- DEPOSIT: [PP:IP] = DATA
- INC PC
In order to use a linearised address space the PC address needs to be converted into a Page (PP or Page Pointer) and an Address (IP or Instruction Pointer). The register pair PP:IP is a general register pair (not reserved by the PC register pair). It is also used by the Stack Pointer (SP).
To finish off the VM I have coded three pages for Opcode Jump Addresses (capacity for 48 Opcodes). Currently only 35 OpCodes are required.
Coded OpCode
- Obviously I coded the easy ones first (need the practice):
- mov Ax,Bx
- mov Tx,Ax
- mov Bx,Tx
- mov PC,Ax
- mov Ax,PC
- mov Ax,SP
- mov SP,Ax
- mov Ax,BP
- mov BP,Ax
- dec Ax
- inc Ax
- mov Bx,Imm
- jmp
- jz
- odd Ax
- add Ax,Bx
- sub Ax,Bx
- eq Ax,Bx
- lt Ax,Bx
- le Ax,Bx
- ne Ax,Bx
- gt Ax,Bx
- ge Ax,Bx
The test OpCode (I.E. eq, lt, le, ne, gt and ge) were tricky. Problems with integer overflow.
Here is the C code that I used to model the micro-code:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
unsigned char AL=0;
unsigned char AH=0;
unsigned char BL=0;
unsigned char BH=0;
unsigned char TL=0;
unsigned char TH=0;
unsigned int AX;
unsigned int BX;
unsigned int T=0;
unsigned int Test=0;
for (AX=0;AX<=65535;AX++) {
for (BX=0;BX<=65535;BX++) {
AH=(AX>>8)&0xff;
AL=AX&0xff;
BH=(BX>>8)&0xff;
BL=BX&0xff;
// TX=-BX-1
TH=~BH;
TL=~BL;
// TX=AX+TX
T=AL+TL;
TL=T;
AL=1;
if (T>255) goto L1;
AL=0;
L1:
T=AH+TH+AL;
TH=T;
AL=1;
if (T>255) goto L2;
AL=0;
L2:
// TX=TX+1
T=TL+1;
TL=T;
AH=1;
if (T>255) goto L3;
AH=0;
L3:
T=TH+AH;
TH=T;
// AH=1;
// if (T>255) goto L4;
// AH=0;
// L4:
// Test results!
Test=0;
if ((AL==1)&&(AX >BX)) Test=1; // AX GT BX
if ((AL==0)&&(AX<=BX)) Test=1; // AX LE BX
if ((AH==1)&&(AX==BX)) Test=1; // AX EQ BX
if ((AH==0)&&(AX!=BX)) Test=1; // AX NE BX
if ((AL==0)&&(AH==0)&&(AX!=BX)) Test=1; // AX LT BX
if ((AL==1)||(AH==1)&&(AX!=BX)) Test=1; // AX GE BX
if (Test==0) {
printf("AX=%d > BX=%d GT=%s\n",AX,BX,AL?"true":"false");
}
// printf("%5d %5d: AX-BX-1 = %3d %3d",AX,BX,TH,TL); getchar();
// printf("AX=%d > BX=%d GT=%s",AX,BX,AL?"true":"false"); getchar();
}
}
return 0;
}
Okay, it looks very weird but it mimics the micro-code. If it works then I have confidence that the micro-code works. Here is the resulting micro-code:
The code return two flags:
- AL==1 if AX > BX
- AH==1 if AX == BX
Depending on the test, using these two codes, AX is set either to 0 (false) or 1 (true).
Remaining OpCode
The remaining OpCodes are:
- mulAxBx (imul)
- divAxBx (idiv)
- modAxBx (idiv)
- pushAx
- popAx
- popBx
- movAxIndirectAx (i.e. Ax = stack[Ax])
- movIndirectAxBx (i.e. stack[Ax] = Bx)
- write (itoa)
- read (atoi)
- putC
- getC
I have written the C code for imul, idiv, atoi, itoa, so they are ready for conversion to micro-code.
Only putC and getC (serial I/O of characters) which require interrupts need further thought.
So on balance I think I am 50% done.
Update II
A busy few days:
Coded the virtual machine.
- Coded a ring buffer for serial input.
- Coded a new serial output (that uses the ring buffer).
- Recoded the TTA8 emulator to model serial terminal.
- I want to get the virtual machine and serial working ASAP.
- Coded the IDIV OpCode.
Okay got the "Console/Serial Terminal" emulator working:
- Read and write to the serial terminal (using a 64 byte ring buffer).
- OpCodes are Fetched based on the PC (Program Counter).
- The address space have been linearised.
Next is to link up the OpCode Jump Table and finish off the OpCodes
Update III
I have been working on 16 bit signed multiplication and division. Unfortunately the code spans 3 ROM pages each. That is a bit much for having confidence that the code will work first time. I have elected to used 8 bit multiplication and division (only spans two ROM pages each) for testing purposes.
Another problem is that the timing interrupt (INTR7) is called every 256 micro-instructions (i.e. read/write pairs), and the multiplication/division will take about 7 timing interrupts. So two problems, a serial input character could be missed and timed delays will be "delayed".
I can fix the serial problem by porting the INTR6 signal to CTS to hold the USB-Serial converter. The missed timing interrupts will need the interrupt frequency reduced by a factor of 8 or I don't worry about it.
AlanX
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.