[Project Log] Python on the 6502/C64, 8080, 6800, 6809 and AVR

Still no luck finding a way to run the Intel PL/M 8080 compiler, but one of the manuals had some sample code in it.

Consider this PL/M procedure:

PRINT$STRING: PROCEDURE(NAME,LENGTH);
    DECLARE NAME ADDRESS,
        (LENGTH,I,CHAR BASED NAME) BYTE;
        DO I = 0 to LENGTH-1;
        CALL PRINT$CHAR(CHAR(I));
        END;
    END PRINT$STRING;

Before we get to the code generated by the compiler, a few words about the compiler conventions.

Parameters to a procedure are allocated statically in memory unless the procedure is declared to be reentrant. Local variables are allocated next, contiguously.

If there are two or fewer parameters, they are passed in registers.

The first in register pair BC if ADDRESS and in register C if BYTE.

The second in register pair DE if ADDRESS and in register E if BYTE.

 00C8 21 02E2	     [10] 00078			lxi		H,2E2h	; Point to NAME
 00CB 71		      [7] 00079			mov		M,C		; Store NAME
 00CC 23		      [5] 00080			inx		H
 00CD 70		      [7] 00081			mov		M,B
 00CE 2C		      [5] 00082			inr		L		; Store LENGTH
 00CF 73		      [7] 00083			mov		M,E
 00D0 2C		      [5] 00084			inr		L		; Clear I
 00D1 36 00		     [10] 00085			mvi		M,0
 00D3 21 02E4	     [10] 00086			lxi		H,2E4h	; Point to LENGTH
 00D6 4E		      [7] 00087			mov		C,M
 00D7 0D		      [5] 00088			dcr		C		; LENGTH-1
 00D8 79		      [5] 00089			mov		A,C
 00D9 2C		      [5] 00090			inr		L
 00DA 96		      [7] 00091			sub		M		; Compare with I
 00DB DA 00F1	     [10] 00092			jc		0F1h	; End loop
 00DE 4E		      [7] 00093			mov		C,M		; Make index into NAME
 00DF 06 00		      [7] 00094			mvi		B,0
 00E1 2A 02E2	     [16] 00095			lhld	2E2h	; Add base of NAME
 00E4 09		     [10] 00096			dad		B
 00E5 7E		      [7] 00097			mov		A,M		; Get next CHAR
 00E6 4F		      [5] 00098			mov		C,A
 00E7 CD 00C0	     [17] 00099			call	0C0h	; call PRINT$CHAR
 00EA 21 02E5	     [10] 00100			lxi		H,2E5h	; Point to I
 00ED 34		     [10] 00101			inr		M		; Increment I
 00EE C3 00D3	     [10] 00102			jmp		0D3h	; Back to top of the loop
 00F1 C9		     [10] 00103			ret

This is not great code, but it is not bad. The compiler definitely knows about the strengths and weaknesses of the 8080.

Loading a byte from or storing a byte to an arbitrary location in memory takes three bytes of machine code and 13 clock cycles. And it has to go through the accumulator.

Contrast that with indirect access using an address in the HL register pair. The “memory” register M is an equal to the other single-byte registers except for slightly slower access.

This is what an assembly language programmer might write:

 00C8 21 02E2	     [10] 00107			lxi		H,2E2h	; Point to NAME
 00CB 71		      [7] 00108			mov		M,C		; Store NAME
 00CC 23		      [5] 00109			inx		H
 00CD 70		      [7] 00110			mov		M,B
 00CE 23		      [5] 00111			inx		H		; Store LENGTH
 00CF 73		      [7] 00112			mov		M,E
 00D0 AF		      [4] 00113			xra		A		; Check for LENGTH = 0
 00D1 BB		      [4] 00114			cmp		E
 00D2 C8		   [5/11] 00115			rz
 00D3 2A 02E2	     [16] 00116	Loop:	lhld	2E2h	; Get next character
 00D6 4E		      [7] 00117			mov		C,M
 00D7 23		      [5] 00118			inx		H		; Point to next character
 00D8 22 02E2	     [16] 00119			shld	2E2h	; Save for next time
 00DB CD 00C0	     [17] 00120			call	0C0h	; call PRINT$CHAR
 00DE 21 02E4	     [10] 00121			lxi		H,2E4h	; Point to LENGTH
 00E1 35		     [10] 00122			dcr		M		; Decrement LENGTH
 00E2 C2 00D3	     [10] 00123			jnz		Loop
 00E5 C9		     [10] 00124			ret

Now I am curious what the compiler would generate if I had written PL/M code to do it this way…

Success! The Intel PL/M compiler lives!

Though I cannot figure out how to set the program origin. Not necessary for this test since I am not actually trying to build a working program.

The rewritten PL/M program:

PRINT$STRING: PROCEDURE(NAME,LENGTH);
    DECLARE NAME ADDRESS,
        (LENGTH,CHAR BASED NAME) BYTE;
        DO WHILE LENGTH <> 0;
        CALL PRINT$CHAR(CHAR);
        NAME = NAME+1;
        LENGTH = LENGTH-1;
        END;
    END PRINT$STRING;

and the generated code:

 000B 21 0004	     [10] 00003		lxi		H,0004
 000E 73		      [7] 00004		mov		M,E
 000F 2B		      [5] 00005		dcx		H
 0010 70		      [7] 00006		mov		M,B
 0011 2B		      [5] 00007		dcx		H
 0012 71		      [7] 00008		mov		M,C
 0013 3A 0004	     [13] 00009		lda		0004
 0016 FE 00		      [7] 00010		cpi		00
 0018 CA 001E	     [10] 00011		jz		0030
 001B 2A 0002	     [16] 00012		lhld	0002
 001E 4E		      [7] 00013		mov		C,M
 001F CD 0000	     [17] 00014		call	0000
 0022 2A 0002	     [16] 00015		lhld	0002
 0025 23		      [5] 00016		inx		H
 0026 22 0002	     [16] 00017		shld	0002
 0029 21 0004	     [10] 00018		lxi		H,0004
 002C 35		     [10] 00019		dcr		M
 002D C3 000D	     [10] 00020		jmp		0013
 0030 C9		     [10] 00021		ret

Not quite as good as hand-written code, but much improved.

2 Likes

Just when you thought you have seen everything, this test program came with the compiler. Cheers!

/*
 * 99 bottles of beer in PL/M-80
 *
 * by John Durbetaki using AEDIT
 *
 */
Ninety$Nine: do;

$include(:f1:common.lit)
$include(:f1:isis.ext)

declare as              LITERALLY   'LITERALLY';
declare CRLF            as          '0Dh,0Ah';

declare Beers           BYTE;
declare Message1(*)     BYTE DATA(' of beer on the wall,',CRLF);
declare Message2(*)     BYTE DATA(' of beeeeer . . . . ,',CRLF);
declare Message3(*)     BYTE DATA('Take one down, pass it around,',CRLF);
declare Message4(*)     BYTE DATA(' of beer on the wall.',CRLF);
declare End$Message(*)  BYTE DATA(CRLF,'Time to buy more beer!',CRLF);
declare STATUS          ADDRESS;
declare How$Many(128)   BYTE;
declare How$Many$Count  BYTE;

Copy: PROCEDURE(Ap,Bp,Count);
    declare Ap              ADDRESS;
    declare A BASED Ap      BYTE;
    declare Bp              ADDRESS;
    declare B BASED Bp      BYTE;
    declare Count           BYTE;

    DO WHILE Count > 0;
        B=A;
        Ap=Ap+1;
        Bp=Bp+1;
        Count=Count-1;
        END;
    END;

Make$How$Many: PROCEDURE(Beers);
    declare Beers           BYTE;

    if Beers = 0 THEN DO;
	How$Many$Count=15;
        CALL Copy(.('No more bottles'),.How$Many(0),How$Many$Count);
        END;
    else if Beers = 1 THEN DO;
	How$Many$Count=15;
        CALL Copy(.('One more bottle'),.How$Many(0),How$Many$Count);
        END;
    else DO;
        if Beers >= 10 THEN DO;
            How$Many(0)='0'+(Beers/10);
            How$Many(1)='0'+(Beers MOD 10);
            CALL Copy(.(' bottles'),.How$Many(2),8);
            How$Many$Count=10;
            END;
        else DO;
            How$Many(0)='0'+Beers;
            CALL Copy(.(' bottles'),.How$Many(1),8);
            How$Many$Count=9;
            END;
        END;
    END;

Chug: PROCEDURE(Beers);
    declare Beers           BYTE;

    CALL Make$How$Many(Beers);
    CALL WRITE(0,.How$Many,How$Many$Count,.STATUS);
    CALL WRITE(0,.Message1,SIZE(Message1),.STATUS);
    CALL WRITE(0,.How$Many,How$Many$Count,.STATUS);
    CALL WRITE(0,.Message2,SIZE(Message2),.STATUS);
    CALL WRITE(0,.Message3,SIZE(Message3),.STATUS);
    CALL Make$How$Many(Beers-1);
    CALL WRITE(0,.How$Many,How$Many$Count,.STATUS);
    CALL WRITE(0,.Message4,SIZE(Message4),.STATUS);
    END;

    Beers = /*99*/ 9;
    DO WHILE Beers > 0;
        CALL Chug(Beers);
        Beers=Beers-1;
        END;
    CALL WRITE(0,.End$Message,SIZE(End$Message),.STATUS);
    call exit;
    END;
2 Likes

Through the FLEX Users Group, I have discovered that Programma International sold SPL/M, a PL/M lookalike, running on and generating 6800 code for the FLEX operating system.

1 Like

I’ll have to look more into this FLEX Operating system but did find some games for it:

http://tanrunomad.com/swtpc-flex-games/

1 Like

In transcribing some code from the 6800 to the 6502, this snippet had no obvious direct translation:

 1DB7 B6 11CA	      [4] 04064	         ldaa   DumpAddr+1 ; Now compare upper nybble of low byte
 1DBA 84 F0		      [2] 04065	         anda   #$F0
 1DBC F6 11BF	      [4] 04066	         ldab   DumpEndAddr+1
 1DBF C4 F0		      [2] 04067	         andb   #$F0
 1DC1 11		      [2] 04068	         cba
 1DC2 26 06 (1DCA)    [4] 04069	         bne    DumpLineContinue

The 6502 has a single accumulator and there are no operations other than transfer allowed between it and the index registers. Rethinking the properties of numbers yielded this:

 0AF0 AD 02BC	      [4] 01498		lda	DumpAddr	; Now compare upper nybble of low byte
 0AF3 4D 02B7	      [4] 01499		eor	DumpEndAddr
 0AF6 29 F0		      [2] 01500		and	#$F0
 0AF8 D0 08 (0B02)  [2/4] 01501		bne	DumpLineContinue

Exclusive oring a number to itself results in zero.

@bill looks like Adafruit is getting in the game with CircuitPython.

Seems to be built on top of micropython.

It has been a long and painful slog, but my emulator finally implements both the officially documented and undocumented behaviors of 6502 decimal mode.

It passes the test program at http://6502.org/tutorials/decimal_mode.html

The code is ugly and the adc and sbc instructions are substantially slower in decimal mode. Thankfully, they are not used often.

2 Likes

When I was talking transcribing 6800 code to the 6502, one of them is the TSC Space Voyage game. It is now running though there are some bugs to chase down…

SpaceVoyage65

2 Likes

For Happy Programmer's Day, I worked on transcribing the Space Voyage game to the 8080; it is about one-half done. Then I will do it for the AVR so that it will run on an Arduino.

4 Likes

Space Voyage for the 8080 is now feature complete and is playable.

Several days were needed to chase down a rather obscure bug with the short and long range scans reporting different numbers of stars in a quadrant.

.			  00844	* Put Objects in Sector Map
.			  00845
.05D3 CE 00DC     [3] 00846	PUTINM   ldx    #SECMAP   ; Point to map
.05D6 BD 106B     [9] 00847	         jsr    RANDOM
.05D9 84 0F	      [2] 00848	         anda   #$F       ; Gen random position
.05DB 97 85	      [4] 00849	         staa   TSAVE1
.05DD BD 031A     [9] 00850	         jsr    FIXXRG    ; Find in map
.05E0 E6 00	      [5] 00851	         ldab   0,X
.05E2 BD 106B     [9] 00852	         jsr    RANDOM    ; Gen random X
.05E5 84 03	      [2] 00853	         anda   #3
.05E7 97 81	      [4] 00854	         staa   ASAVE
.05E9 27 05 (05F0)[4] 00855	         beq    PUTIN2
.05EB 56	      [2] 00856	PUTIN1   rorb             ; Find X position
.05EC 56	      [2] 00857	         rorb       ; AAA
.05ED 4A	      [2] 00858	         deca
.05EE 26 FB (05EB)[4] 00859	         bne    PUTIN1
.05F0 C5 03	      [2] 00860	PUTIN2   bitb   #3        ; Is position empty?
.05F2 26 DF (05D3)[4] 00861	         bne    PUTINM    ; If not, repeat
.05F4 DA 7E	      [3] 00862	         orab   MASK
.05F6 96 81	      [3] 00863	         ldaa   ASAVE
.05F8 27 05 (05FF)[4] 00864	         beq    PUTIN4
.05FA 59	      [2] 00865	PUTIN3   rolb       ; BBB       ; Put object in map
.05FB 59	      [2] 00866	         rolb
.05FC 4A	      [2] 00867	         deca
.05FD 26 FB (05FA)[4] 00868	         bne    PUTIN3
.05FF E7 00	      [6] 00869	PUTIN4   stab   0,X       ; Save it

The original code kept an important bit in the carry flag between points AAA and BBB. The or instruction of the 8080 clears the carry flag, losing that bit.

Also, the 8080 cannot set its condition codes according to the contents of a memory location without loading the byte into the accumulator. Even then, an additional operation is needed such as a compare, and or or which also wipes out the carry flag.


Transcribing to the AVR has been easier. It has 32 registers, half of which has the full power of an accumulator. The only major drawback so far has been the lack of a decimal adjust instruction which the original code uses in several places. So instead of a single instruction, a call to a subroutine is needed.

 0003AC					  00998	DAA:
 0003AC E070	      [1] 00999		ldi		R23,0		; Initially no correction factor
 						  01000
 0003AD F408=0003AF [1/2] 01001		brcc	DAA_NotUCarry	; The add overflowed the upper nybble?
 						  01002
 0003AE 6670	      [1] 01003		ori		R23,$60
 						  01004
 0003AF					  01005	DAA_NotUCarry:
 0003AF F40D=0003B1 [1/2] 01006		brhc	DAA_NotLCarry	; The add overflowed the lower nybble?
 						  01007
 0003B0 6076	      [1] 01008		ori		R23,$06
 						  01009
 0003B1					  01010	DAA_NotLCarry:
 0003B1 EA90	      [1] 01011		ldi		R25,$A0		; Upper nybble needs correction if over 9
 						  01012
 0003B2 2F86	      [1] 01013		mov		R24,R22		; Make a scratch copy
 						  01014
 0003B3 708F	      [1] 01015		andi	R24,$0F
 						  01016
 0003B4 308A	      [1] 01017		cpi		R24,$0A		; Check if lower nybble is out of range
 0003B5 F010=0003B8 [1/2] 01018		brcs	DAA_CheckUpper
 						  01019
 0003B6 6076	      [1] 01020		ori		R23,$06		; Correct out of range lower nybble
 						  01021
 0003B7 E990	      [1] 01022		ldi		R25,$90		; Upper nybble needs correction if over 8
 						  01023					;   with carry from lower nybble
 						  01024
 0003B8					  01025	DAA_CheckUpper:
 0003B8 2F86	      [1] 01026		mov		R24,R22		; Make a scratch copy
 						  01027
 0003B9 1789	      [1] 01028		cp		R24,R25		; Check if upper nybble is out of range
 0003BA F008=0003BC [1/2] 01029		brcs	DAA_Add
 						  01030
 0003BB 6670	      [1] 01031		ori		R23,$60		; Correct out of range upper nybble
 						  01032
 0003BC					  01033	DAA_Add:
 0003BC 0F67	      [1] 01034		add		R22,R23		; Do decimal adjust
 						  01035
 0003BD 9508	      [2] 01036		ret
3 Likes

I would love to see a port of this to the Commander X16

Is his goal to run C64 binaries unmodified?

The creator is local. Maybe David @dave can twist his arm to give a talk on it…

It is not compatible with the C64. See the FAQ

From what little I read, the tools generate .PRG files and he claims to support the KERNAL API. So if I build something for the C64, it should run if I do not assume anything about the hardware other than a 6502 processor and RAM in the usual place…

It is actually the 65c02 not the 6502. There are some differences in timing and new opcodes.

As painful as that may be sometimes, I am limiting myself to the NMOS 6502 instructions because those work on all 6502 family machines. I do nothing timing dependent.

1 Like

I just reacquainted myself with one of the more aggravating limitations of the AVR instruction set. Many processors limit relative branches to between -128 and +127; the AVR limit is -64 and +63.

The AVR addresses program memory (16-bit) using word rather than byte addresses, so the range in bytes is the same as many others, but AVR instructions take up one or two words so that is half the number of instructions.

Brian posted a video SWTPC 6800/6809 Computer Review and History https://www.youtube.com/watch?v=SATjR-MWHDM

1 Like

Interesting twist. As the AVR version started coming up, it gave drastically different results than the others. The game uses pseudo-random numbers so comparing the versions is difficult.

So I rigged the number generator to start with the same seed for now and discovered to my horror that all four versions differed!

I have never worked at software from this angle before, “port” the code and compare the results to this level of detail.

Some of you may have taken a comparative anatomy or comparative religion class before, but I know of no comparative processors class which goes into much detail. The closest was a microprocessors class which covered the Motorola 6800 for several weeks, then the Zilog Z80 for another several. The lab exercises involved writing several simple programs on both.

An enlightening exercise would be to get a program moderate in size and complexity for one and write equivalent code for another processor.

2 Likes