Jump to content
IGNORED

C02 Compiler


CurtisP

Recommended Posts

This is something that had been bouncing around in my head for at least ten years. I originally started it about five years ago, but then got distracted and finally picked it up again a couple months ago.

The language and compiler are still in the alpha stage of development. The code is currently a bit of a mess, and I'm still wrestling with design decisions.

Here is some explanation from the beginning of the documentation that I have written:

INTRODUCTION

C02 is a simple C-syntax language designed to generate highly optimized
code for the 6502 microprocessor. The C02 specification is a highly
specific subset of the C standard with some modifications and extensions

PURPOSE

Why create a whole new language, particularly one with severe restrictions,
when there are already full-featured C compilers available? It can be
argued that standard C is a poor fit for processors like the 6502. The C 
was language designed to translate directly to machine language instructions 
whenever possible. This works well on 32-bit processors, but requires either 
a byte-code interpreter or the generation of complex code on a typical
8-bit processor. C02, on the other hand, has been designed to translate 
directly to 6502 machine language instructions.
 
The C02 language and compiler were designed with two goals in mind.

The first goal is the ability to target machines with low memory: a few 
kilobytes of RAM (assuming the generated object code is to be loaded into 
and ran from RAM), or as little as 128 bytes of RAM and 2 kilobytes of ROM
(assuming the object code is to be run from a ROM or PROM). 

The compiler is agnostic with regard to system calls and library functions. 
Calculations and comparisons are done with 8 bit precision. Intermediate 
results, array indexing, and function calls use the 6502 internal registers.
While this results in compiled code with virtually no overhead, it severely 
restricts the syntax of the language.

The second goal is to port the compiler to C02 code so that it may be
compiled by itself and run on any 6502 based machine with sufficient memory
and appropriate peripherals. This slightly restricts the implementation of
code structures.

 

  • Like 3
Link to comment
Share on other sites

A lot of 2600 coding involves instructions occurring at specific cycles on a scanline. How does C02 handle that? If it doesn't, then you can't position a player, missile, or ball accurately.

 

For instance, this is one of the commonly used routines for positioning objects:

;===============================================================================
; PosObject
;----------
; subroutine for setting the X position of any TIA object
; when called, set the following registers:
;   A - holds the X position of the object
;   X - holds which object to position
;       0 = player0
;       1 = player1
;       2 = missile0
;       3 = missile1
;       4 = ball
; the routine will set the coarse X position of the object, as well as the
; fine-tune register that will be used when HMOVE is used.
;
; Note: The X position differs based on the object, for player0 and player1
;       0 is the leftmost pixel while for missile0, missile1 and ball 1 is
;       the leftmost pixel:
;           players     - X range is 0-159
;           missiles    - X range is 1-160
;           ball        - X range is 1-160
; Note: Setting players to double or quad size will affect the position of
;       the players.
;===============================================================================
PosObject:
        sec
        sta WSYNC
DivideLoop
        sbc #15        ; 2  2 - each time thru this loop takes 5 cycles, which is 
        bcs DivideLoop ; 2  4 - the same amount of time it takes to draw 15 pixels
        eor #7         ; 2  6 - The EOR & ASL statements convert the remainder
        asl            ; 2  8 - of position/15 to the value needed to fine tune
        asl            ; 2 10 - the X position
        asl            ; 2 12
        asl            ; 2 14
        sta.wx HMP0,X  ; 5 19 - store fine tuning of X
        sta RESP0,X    ; 4 23 - set coarse X position of object
        rts            ; 6 29


 

The STA RESP0,X must occur at 23, 28, 33, etc (ever 5 cycles). HMP0 is on zero page, so the STA HMP0,x would normally be compiled as a 4 cycle Zero Page instruction, which would cause the STA RESP0,x to occur at the wrong time. So we force the compiler to use a word address (16-bit) instead of Zero Page address for that extra cycle of time, which is what makes this routine possible.

Link to comment
Share on other sites

The compiler is efficient enough that I can write most VCS code directly in C02.

Here is an example:

//Atari VCS Color Bars Program

#pragma origin $F800    //2k Cartridge
#include <vcshead.h02>  //TIA and RIOT Registers

/* Generate Vertical Sync Signal */
void vtsync() {
  A=2;     //Set Bit 2 (D1)
  WSYNC=A; //Wait for end of Scanline
  VSYNC=A; //Turn On Vertical Sync
  WSYNC=A; //Wait 2 More Scanlines
  WSYNC=A;
  A=0;     //Clear Bit 2 (D1)
  WSYNC=A; //Wait for End of 3rd Scanline
  VSYNC=A; //Turn On Vertical Sync
  return;
}

/* Execute Vertical Blank Code */
void vtblnk() {
  X=37; //Delay 37 Scanlines
  do {
    WSYNC=A;  //Wait for end of Scanline
    X--;
  } while (X);
  return;
}

/* Execute Kernel Code */
void kernel() {
  A=0; //Clear All Bits
  WSYNC=A;  //Wait for end of Scanline
  VBLANK=A; //Turn Off Vertical Blank
  X=0; //Draw 192 Scanlines (256-64)
  do {
    if (X & 3) {
      WSYNC = A;  //Wait for end of Scanline
      COLUBK = X; //Set Background Color
    }
    X--;
  } while (X);
  return;
}

/* Execute Overscan Code */
void ovrscn() {
  WSYNC=A;  //Wait for end of Scanline
  A=2;      //Set Bit 2 (D1)
  VBLANK=A; //Turn On Vertical Blank
  X=27; //Delay 27 Scanlines
  do {
    WSYNC=X;  //Wait for end of Scanline
    X--;
  } while(X);
  return;
}

start:
  asm("","SEI","");
  asm("","CLD","");
  A = 0;
  X = A;
  Y = A;
  do {
    X--;
    asm("","TXS","");
    push 0;
  } while (X);
irqbrk: //Code to Execute when BRK Instruction is Encountered
main:  //Start of Program Code
  vtsync(); //Generate Vertical Sync Signal  
  vtblnk(); //Generate Vertical Blank Signal  
  kernel(); //Execute Kernal Code
  ovrscn(); //Execute Overscan Code
  goto main;

#include <vcsfoot.h02>  //Finalization Code

And here is the Assembly Language that's created:

        PROCESSOR 6502         
        ORG $F800        
; ======== Assembler File  include/vcshead.a02  =======
VSYNC   EQU $00  ;0000 00x0   Vertical Sync Set-Clear
VBLANK  EQU $01  ;xx00 00x0   Vertical Blank Set-Clear
WSYNC   EQU $02  ;---- ----   Wait for Horizontal Blank
COLUBK  EQU $09  ;xxxx xxx0   Color-Luminance Background
; ==========================================
VTSYNC:                  ;VOID VTSYNC() {
        LDA #2           ;  A = 2
        STA WSYNC        ;  WSYNC = A;
        STA VSYNC        ;  VSYNC = A;
        STA WSYNC        ;  WSYNC = A;
        STA WSYNC        ;  WSYNC = A;
        LDA #0           ;  A = 0;
        STA WSYNC        ;  WSYNC = A;
        STA VSYNC        ;  VSYNC = A;
        RTS              ;  RETURN; }
VTBLNK:                  ;VOID VTBLNK() {
        LDA #37          ;  X = 37;
        TAX              ;
L_0001:                  ;  DO {
        STA WSYNC        ;    WSYNC = A;
        DEX              ;    X--;
        TXA              ;  } WHILE(X);
        BEQ L_0000       
        JMP L_0001       ;
L_0000: RTS              ;  RETURN; }
KERNEL:                  ;VOID KERNEL() {
        LDA #0           ;  A = 0;
        STA WSYNC        ;  WSYNC = A;
        STA VBLANK       ;  VBLANK = A;
        LDA #0           ;  X = 0 ;
        TAX              ;  
L_0003: TXA              ;  DO {  
        AND #3           ;    IF ( X & 3 ) {
        BEQ L_0004       ;
        STA WSYNC        ;      WSYNC= A
        TXA              ;      COLUBK = X;
        STA COLUBK       ;    }
L_0004: DEX              ;    X--;
        TXA              ;  } WHILE (X);
        BEQ L_0002       ;
        JMP L_0003       ;
L_0002: RTS              ;  RETURN; }
OVRSCN:                  ;VOID OVRSCN() {
        STA WSYNC        ;  WSYNC= A;
        LDA #2           ;  A = 2;
        STA VBLANK       ;  VBLANK= A;
        LDA #27          ;  X = 27;
        TAX              ;
L_0006: TXA              ;  DO { 
        STA WSYNC        ;  WSYNC= X;
        DEX              ;  X--;
        TXA              ;  } WHILE (X);
        BEQ L_0005       ;
        JMP L_0006       ;
L_0005: RTS              ;  RETURN; }
START:                   ;START:
        SEI              ;  ASM("","SEI","");
        CLD              ;  ASM("","CLD","");
        LDA #0           ;  A = 0;
        TAX              ;  X = A;
        TAY              ;  Y = A;
L_0008:                  ;  DO {
        DEX              ;    X--;
        TXS              ;  ASM("","TXS","");
        LDA #0           ;  PUSH 0;
        PHA              ;
        TXA              ;  } WHILE (X);
        BEQ L_0007       ;
        JMP L_0008       ;
L_0007:                  ;
IRQBRK:                  ;IRQBRK:
MAIN:                    ;MAIN:
        JSR VTSYNC       ;  VTSYNC();
        JSR VTBLNK       ;  VTBLNK();
        JSR KERNEL       ;  KERNEL(
        JSR OVRSCN       ;  OVRSCN(
        JMP MAIN         ;  GOTO MAIN;
; ======== Assembler File  include/vcsfoot.a02  =======
        ORG  $FFF8   ;Control and Interrupt Registers
SCCTL   DC.B $00     ;$FFF8 Supercharger Control Register
SCAUD   DC.B $00     ;$FFF9 Supercharger Audio Data
        DC.W $0000   ;$FFFA Non-Maskable Interrupt Vector (Unused)
        DC.W START   ;$FFFC Reset Vector
        DC.W IRQBRK  ;$FFFE Interrupt Vector
; ==========================================
  • Like 4
Link to comment
Share on other sites

A lot of 2600 coding involves instructions occurring at specific cycles on a scanline. How does C02 handle that? If it doesn't, then you can't position a player, missile, or ball accurately.

 

This is handled through function calls that are coded in pure assembly language.

 

I started to type in an example, then hit backspace at the wrong time and lost it. I'll post it shortly.

 

Link to comment
Share on other sites

Neat! I'd suggest a minor change to the strobe registers(WSYNC, RESP0, HMOVE, etc):

void vtsync() {
  A=2;     //Set Bit 2 (D1)
  WSYNC;   //Wait for end of Scanline
  VSYNC=A; //Turn On Vertical Sync
  WSYNC;   //Wait 2 More Scanlines
  WSYNC;  
  A=0;     //Clear Bit 2 (D1)
  WSYNC;   //Wait for End of 3rd Scanline
  VSYNC=A; //Turn On Vertical Sync
  return;
}

Makes it cleaner, less typing, and thus less chance of a mistake.

 

Even better would be to abstract the register usage when feasible:

void vtsync() {
  WSYNC;   //Wait for end of Scanline
  VSYNC=2; //Turn On Vertical Sync, Set Bit 2 (D1)
  WSYNC;   //Wait 2 More Scanlines
  WSYNC;  
  WSYNC;   //Wait for End of 3rd Scanline
  VSYNC=0; //Turn On Vertical Sync, Clear Bit 2 (D1)
  return;
}
Link to comment
Share on other sites

Neat! I'd suggest a minor change to the strobe registers(WSYNC, RESP0, HMOVE, etc):

void vtsync() {
  A=2;     //Set Bit 2 (D1)
  WSYNC;   //Wait for end of Scanline
  VSYNC=A; //Turn On Vertical Sync
  WSYNC;   //Wait 2 More Scanlines
  WSYNC;  
  A=0;     //Clear Bit 2 (D1)
  WSYNC;   //Wait for End of 3rd Scanline
  VSYNC=A; //Turn On Vertical Sync
  return;
}

 

A variable by itself as statement doesn't have any meaning in C-like languages. If I implemented C-like macros then I could possibly do something like this.

#define WSYNC = WSYNC = A

WSYNC;

However, almost all code that uses strobe registers is going to be in a function that's coded in assembly language anyway, so it should not be necessary.

  • Like 1
Link to comment
Share on other sites

Even better would be to abstract the register usage when feasible:

void vtsync() {
  WSYNC;   //Wait for end of Scanline
  VSYNC=2; //Turn On Vertical Sync, Set Bit 2 (D1)
  WSYNC;   //Wait 2 More Scanlines
  WSYNC;  
  WSYNC;   //Wait for End of 3rd Scanline
  VSYNC=0; //Turn On Vertical Sync, Clear Bit 2 (D1)
  return;
}

 

Registers are already abstracted. So the code

VSYNC=2;

Compiles to

LDA #2
STA VSYNC

I allowed the use of a register as a variable as an advanced feature so redundant LDA's can be avoided.

  • Like 1
Link to comment
Share on other sites

This looks awesome! :) Looking foward to seeing games and demo's in this tiny-c.

 

There is anothe similar project Atac-c that may be helpful, and you're also welcome to use the Flashback BASIC runtime which has a bit-blitter and other features that may give you some ideas.

Link to comment
Share on other sites

This looks awesome! :) Looking foward to seeing games and demo's in this tiny-c.

 

There is anothe similar project Atac-c that may be helpful, and you're also welcome to use the Flashback BASIC runtime which has a bit-blitter and other features that may give you some ideas.

 

Do you have a link to Atac-c. When I Googled it, I got too many unrelated hits.

Link to comment
Share on other sites

Neat! I'd suggest a minor change to the strobe registers(WSYNC, RESP0, HMOVE, etc):

void vtsync() {
  A=2;     //Set Bit 2 (D1)
  WSYNC;   //Wait for end of Scanline
  VSYNC=A; //Turn On Vertical Sync
  WSYNC;   //Wait 2 More Scanlines
  WSYNC;  
  A=0;     //Clear Bit 2 (D1)
  WSYNC;   //Wait for End of 3rd Scanline
  VSYNC=A; //Turn On Vertical Sync
  return;
}
Makes it cleaner, less typing, and thus less chance of a mistake.

 

There are other 6502 based systems that use strobe registers so, I modified the compiler to allow implicit assignments.

IMPLICIT ASSIGNMENTS

A statement consisting of only a simple variable is treated as an 
implicit assignment of the A register to the variable in question.

This is useful on systems that use memory locations as strobe registers.

Examples:
  HMOVE;  //Move Objects (Atari VCS)
  S80VID; //Enable 80-Column Video (Apple II)

Note: An implicit assignment generates an STA opcode with the variable
as the operand.

Thanks for the idea.

  • Like 1
Link to comment
Share on other sites

Do you plan on allowing inline assembly code?

 

Already implemented. You can use either

#include "filename.asm"

which simply inserts the contents of the specified file, or

asm("label", "opcode", "operand");

which generates a single line of assembly language.

 

The latter is used in my example program in the second comment.

Link to comment
Share on other sites

 

Already implemented. You can use either

#include "filename.asm"

which simply inserts the contents of the specified file, or

asm("label", "opcode", "operand");

which generates a single line of assembly language.

 

The latter is used in my example program in the second comment.

 

Ah. I missed that the first time thru. Nice to see easy inclusion of an assembly language file... But what about a block of inline assembly code? I'll admit, that I was spoiled by Turbo Pascal in that aspect. I don't think I've ever come across a C compiler that has done it as nicely as Turbo Pascal (version 6+).

 

Here is an example of a full procedure:

Procedure ClearMem(MSeg,size:word); assembler;
asm
	mov ax,MSeg
	mov es,ax
	xor di,di
	xor ax,ax
	mov cx,size
	shr cx,1
	stosw
end;

And an inline block;

procedure PrintError(s:string);
begin
	asm
		mov ax,03h
		int 10h
	end;
	writeln(s);
end;

That would be my feature request. :)

Link to comment
Share on other sites

 

Ah. I missed that the first time thru. Nice to see easy inclusion of an assembly language file... But what about a block of inline assembly code? I'll admit, that I was spoiled by Turbo Pascal in that aspect. I don't think I've ever come across a C compiler that has done it as nicely as Turbo Pascal (version 6+).

 

Here is an example of a full procedure:

Procedure ClearMem(MSeg,size:word); assembler;
asm
	mov ax,MSeg
	mov es,ax
	xor di,di
	xor ax,ax
	mov cx,size
	shr cx,1
	stosw
end;

And an inline block;

procedure PrintError(s:string);
begin
	asm
		mov ax,03h
		int 10h
	end;
	writeln(s);
end;

That would be my feature request. :)

 

Tiny C uses the non-standard directives #asm and #asmend.

 

I should probably do something similar, where it takes the inline assembly and converts is to the form expected by the target assembler. This would allow me to embed the assembly language right in the header files instead of having a separate assembly file.

Link to comment
Share on other sites

I've been working on a 2-line kernel library based on Random Terrain's Spiceware's Collect Tutorial.

 

Here's a simple program that uses the kernel to display both players:

#pragma origin $F800    //4k Cartridge
#include <vcshead.h02>  //TIA and RIOT Registers
#include <vcsstub.h02>  //Initialize VCS
#include <vcslib.h02>   //VCS Library Routines
#include <k2line.h02>   //Two Line Game Kernel

#define KNLLNS = 96     //Kernal Lines (Scanlines/2)
#define P0HGHT = 10     //Player 0 Height
#define P1HGHT = 10     //Player 1 Height

//Color Table
char colors = {$86, $C6, $46, $00, $0E, $06, $0A, $00};

//Human Shaped Player Graphics
char human = {%00011100, %00011000, %00011000, %00011000, %01011010,
              %01011010, %00111100, %00000000, %00011000, %00011000};

/* Setup Code */
void setup() {
  setclr(&colors); //Set Color Table
}

/* Vertical Blank Code */
void vrtblk() {
  posobj(0,0);          //Set P0 X-Position
  p0prep(96, &human);   //Set P0 Y-Position & Graphics Pointer
  posobj(8,1);          //Set P1 X-Position
  p1prep(96, &human);   //Set P1 Y-Position & Graphics Pointer
}

/* Execute Kernel Code */
void kernel() {	
  dsplns();      //Display Playfield and Objects
}

/* Execute Overscan Code */
void ovrscn() {
}

#include <digits.h02>   //Digit Graphics
#include <vcsfoot.h02>  //Finalization Code
Edited by CurtisP
Link to comment
Share on other sites

Cool idea! :thumbsup:

 

However, at the moment the generated assembler code seems still pretty inefficient. E.g.

        LDA #37          ;  X = 37;
        TAX              ;                
L_0001:                  ;  DO {
        STA WSYNC        ;    WSYNC = A;
        DEX              ;    X--;
        TXA              ;  } WHILE(X);   
        BEQ L_0000       ;                
        JMP L_0001       ;
L_0000: RTS              ;  RETURN; }
Compared to this:

        LDX #37          ;  X = 37;         
L_0001:                  ;  DO {
        STA WSYNC        ;    WSYNC = A;
        DEX              ;    X--;        
        BNE L_0001       ;  } WHILE(X); 
        RTS              ;  RETURN; }
Link to comment
Share on other sites

If that's what it is generating, then I think you have spotted a bug: the generated assembly does not do "WSYNC = A". Instead it does "WSYNC = X" in a somewhat convoluted way :) It will work anyway as WSYNC is a strobe, but still...

Edited by DirtyHairy
Link to comment
Share on other sites

Cool idea! :thumbsup:

 

However, at the moment the generated assembler code seems still pretty inefficient. E.g.

        LDA #37          ;  X = 37;
        TAX              ;                
L_0001:                  ;  DO {
        STA WSYNC        ;    WSYNC = A;
        DEX              ;    X--;
        TXA              ;  } WHILE(X);   
        BEQ L_0000       ;                
        JMP L_0001       ;
L_0000: RTS              ;  RETURN; }
Compared to this:

        LDX #37          ;  X = 37;         
L_0001:                  ;  DO {
        STA WSYNC        ;    WSYNC = A;
        DEX              ;    X--;        
        BNE L_0001       ;  } WHILE(X); 
        RTS              ;  RETURN; }

 

I'm making a lot of design compromises. Originally, the compiler didn't even recognize registers. I added them in because I thought it might be useful.

 

Now compiling

X=3

into

LDX #3

is easy enough, but what about

X=TEMP+3

That would have to be

LDA TEMP
CLC
ADC #3
TXA

So my choices are to use the expression parser (which uses the Accumulator) followed by a TAX, not allow expressions when assigning values to X or Y, or write some extra complicated code to figure out what is after the equals sign.

 

At this point, I've decided that I want all assignments to work the same way and have predictable side effects. In general, user programs won't be using the registers anyway, because they can be overwritten by the generated code.

Link to comment
Share on other sites

You're tiny C compiler is very efficient, tremendously so for C, but you may still end up deciding it's better to have a pure Assembly Framework with a kernel and a pure Assembly runtime library for your tiny C to call.

 

It's really interesting to see the kernel created in a high level language like you are doing, you can definitely do it but to get optimum performance for games and demos it might be better to have just the gameloop in tiny C and call the runtime.

 

Neither has been done before with C as far as I'm aware using just the real hardware. Cool whichever approach you take - even cooler if you do both! :)

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...