CurtisP Posted June 25, 2017 Share Posted June 25, 2017 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. 3 Quote Link to comment Share on other sites More sharing options...
+SpiceWare Posted June 25, 2017 Share Posted June 25, 2017 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. Quote Link to comment Share on other sites More sharing options...
CurtisP Posted June 25, 2017 Author Share Posted June 25, 2017 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 ; ========================================== 4 Quote Link to comment Share on other sites More sharing options...
CurtisP Posted June 25, 2017 Author Share Posted June 25, 2017 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. Quote Link to comment Share on other sites More sharing options...
+SpiceWare Posted June 25, 2017 Share Posted June 25, 2017 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; } Quote Link to comment Share on other sites More sharing options...
CurtisP Posted June 25, 2017 Author Share Posted June 25, 2017 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. 1 Quote Link to comment Share on other sites More sharing options...
CurtisP Posted June 25, 2017 Author Share Posted June 25, 2017 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. 1 Quote Link to comment Share on other sites More sharing options...
Mr SQL Posted June 25, 2017 Share Posted June 25, 2017 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. Quote Link to comment Share on other sites More sharing options...
CurtisP Posted June 25, 2017 Author Share Posted June 25, 2017 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. Quote Link to comment Share on other sites More sharing options...
CurtisP Posted June 25, 2017 Author Share Posted June 25, 2017 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. 1 Quote Link to comment Share on other sites More sharing options...
+splendidnut Posted June 25, 2017 Share Posted June 25, 2017 Ata-C by Gip-Gip: https://github.com/Gip-Gip/AtaC 1 Quote Link to comment Share on other sites More sharing options...
+splendidnut Posted June 25, 2017 Share Posted June 25, 2017 Do you plan on allowing inline assembly code? Quote Link to comment Share on other sites More sharing options...
CurtisP Posted June 25, 2017 Author Share Posted June 25, 2017 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. Quote Link to comment Share on other sites More sharing options...
tschak909 Posted June 25, 2017 Share Posted June 25, 2017 This is pretty nice. -Thom Quote Link to comment Share on other sites More sharing options...
+splendidnut Posted June 25, 2017 Share Posted June 25, 2017 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. Quote Link to comment Share on other sites More sharing options...
+splendidnut Posted June 25, 2017 Share Posted June 25, 2017 I just realized that this seems very similar to C--. Quote Link to comment Share on other sites More sharing options...
CurtisP Posted June 26, 2017 Author Share Posted June 26, 2017 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. Quote Link to comment Share on other sites More sharing options...
Kylearan Posted June 26, 2017 Share Posted June 26, 2017 Neat project! It reminds me a bit of macross or k65. I'm looking forward to see how it fares in a "real-world" project (small game or demo). Quote Link to comment Share on other sites More sharing options...
CurtisP Posted June 30, 2017 Author Share Posted June 30, 2017 (edited) 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 July 1, 2017 by CurtisP Quote Link to comment Share on other sites More sharing options...
Thomas Jentzsch Posted June 30, 2017 Share Posted June 30, 2017 Cool idea! 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; } Quote Link to comment Share on other sites More sharing options...
DirtyHairy Posted June 30, 2017 Share Posted June 30, 2017 (edited) 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 July 2, 2017 by DirtyHairy Quote Link to comment Share on other sites More sharing options...
+SpiceWare Posted June 30, 2017 Share Posted June 30, 2017 I've been working on a 2-line kernel library based on Random Terrain's Collect Tutorial.That's my tutorial, RT reformatted it for his site (with my permission). Quote Link to comment Share on other sites More sharing options...
CurtisP Posted July 1, 2017 Author Share Posted July 1, 2017 (edited) That's my tutorial, RT reformatted it for his site (with my permission). Sorry about that. I'm getting names all mixed up, especially late at night. By the way. I've found it quite useful. Edited July 1, 2017 by CurtisP Quote Link to comment Share on other sites More sharing options...
CurtisP Posted July 1, 2017 Author Share Posted July 1, 2017 Cool idea! 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. Quote Link to comment Share on other sites More sharing options...
Mr SQL Posted July 3, 2017 Share Posted July 3, 2017 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! Quote Link to comment Share on other sites More sharing options...
Recommended Posts
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.