Jump to content
IGNORED

Attemping to code the Atari 2600 in C with CC65


Fabrizio Caruso

Recommended Posts

  • 4 months later...

Has anyone tried to code for the Atari 2600 in C wirh CC65?

 

I am trying to figure pout how to code the display.

 

I understand I need to control the raster.

 

I am looking for a simple "Hello World" example.

 

 

Did you find a good graphics / audio / input example with the CC65 compiler?

Like an example game where you can see optimized solutions working.

Edited by zezba9000
Link to comment
Share on other sites

There is no high level construct in CC65 for the Atari2600.
Unfortunately CC65 only supports Assembly or "C as Assembly", i.e., you can write your code in C but you have to follow the beam as you do in Assembly.

CC65 does not provide anything close to Batari BASIC.

 

I may write a version of my hame if I understand how to do simple graphics.

Let us say I have a buffer with a 32x16 grid of bits (or less), how can I draw this buffer as 32x16 full blocks on the screen in C?

Link to comment
Share on other sites

Why do I need to write in Assembler? Anyway were can I find a simple example to just write blocks from a grid of bits.
Given the limited ram, I am assuming just a 32x16 or 32x12 grid.
The beam has to be on or off depending on the value of the bit in the grid.
If the main loop is fast enough, maybe I don't even need interrupts. At this stage I wouldn't mind if there were some flickering.
I do not want to use player/missile/ball. Just big blocks with one single screen-wide color.

Link to comment
Share on other sites

You need display kernel which has to be written in assembler.

 

In this example: https://github.com/cc65/cc65/blob/master/samples/atari2600hello.c I'm able to compile and see the screen flashing colors as expected.

I see TIA being used to set the background color.

Does this expose the features needed?

 

If not where is a good example of using the asm to write colored blocks or lines using the graphics unit I could invoke from C?

From reading it seems the graphics unit supports block and horizontal lines. I'll have to find that article again.

 

Example compiled like so: "..\bin\cl65 -t atari2600 .\atari2600hello.c -o atari2600hello.a26"

Edited by zezba9000
Link to comment
Share on other sites

You really have to understand the basics of the 2600 graphics. There is no frame buffer which you can address. Instead you have to "race the beam", updating several graphics registers in sync with it. That way you construct line by line of the display. And this requires assembler and cycle counting.

 

A simple assembler kernel can be found here: How to Draw a Playfield, but you would have to modify it significantly to control it from outside.

Link to comment
Share on other sites

You really have to understand the basics of the 2600 graphics. There is no frame buffer which you can address. Instead you have to "race the beam", updating several graphics registers in sync with it. That way you construct line by line of the display. And this requires assembler and cycle counting.

 

A simple assembler kernel can be found here: How to Draw a Playfield, but you would have to modify it significantly to control it from outside.

 

So got playfields working with this example: http://www.randomterrain.com/atari-2600-memories-tutorial-andrew-davie-13.html

However I'm a little confused on what to use to get the rainbow effect correctly.

 

Here is my C code so far.

/*****************************************************************************/
/* */
/* Atari VCS 2600 sample C program */
/* */
/* Florent Flament (contact@florentflament.com), 2017 */
/* */
/*****************************************************************************/

#include <atari2600.h>

// PAL Timings
// Roughly computed based on Stella Programmer's guide (Steve Wright)
// scanlines count per section.
#define VBLANK_TIM64 51 // 45 lines * 76 cycles/line / 64 cycles/tick
#define KERNAL_T1024 17 // 228 lines * 76 cycles/line / 1024 cycles/tick
#define OVERSCAN_TIM64 42 // 36 lines * 76 cycles/line / 64 cycles/tick

// test
unsigned char flip = 0;

void main(void)
{
unsigned char color = 0x79; // Stack variable
TIA.colubk = 0xFF;// background color
TIA.colupf = 0x00;// playfield color
TIA.pf0 = 0x55;// playfield pattern 1 (01010101)
TIA.pf1 = 0xAA;// playfield pattern 2 (10101010)
TIA.pf2 = 0x55;// playfield pattern 3 (01010101)

while (1)
{
// Vertical Sync signal
TIA.vsync = 0x02;
TIA.wsync = 0x00;
TIA.wsync = 0x00;
TIA.wsync = 0x00;
TIA.vsync = 0x00;

// Vertical Blank timer setting
RIOT.tim64t = VBLANK_TIM64;

// Doing frame computation during blank
// TODO

// Wait for end of Vertical Blank
while (RIOT.timint == 0)
{
// Can I change values here?
}
TIA.wsync = 0x00;
TIA.vblank = 0x00; // Turn on beam

// Display frame
RIOT.t1024t = KERNAL_T1024;
while (RIOT.timint == 0) {}
TIA.wsync = 0x00;
TIA.vblank = 0x02; // Turn off beam

// Overscan
RIOT.tim64t = OVERSCAN_TIM64;
while (RIOT.timint == 0) {}
}
}

 

HYYuoyS.png

Edited by zezba9000
Link to comment
Share on other sites

OK, I now see what you are trying (code always helps icon_smile.gif).

 

Yes, you can change values in the first timer loop. But you should do your changes before and use that loop for syncing only. But that area is outside the visible area. Instead you have to change your TIA registers in the display frame area.

 

Here is a simple kernel, which updates the playfield color:

// RIOT.t1024t = KERNAL_T1024;
line = 228;
do 
{
  TIA.colupf = line; // change color every (other) line
  TIA.wsync = 0x00;  // start next scanline (any other value will do too, WSYNC doesn't care)  
} while (--line);
// while (RIOT.timint == 0) {}
// TIA.wsync = 0x00;
I hope this helps. Edited by Thomas Jentzsch
Link to comment
Share on other sites

You'll need a variable to hold the color byte and possibly a scanline, but uses 5 cycle to inc compare to 2/3 cycle using x or y register. In the loop below, I'm using the y register preloaded with value of 192.

There's 76 cycles per scanline.

The kernel loop in asm would be,

loop:
ldx Color
inx
stx COLUPF
stx Color
iny (counting scanline)
cpy 
bne loop

You'll have to make a function in C with this asm to work.
IncreaseVariable(Color)
Something like this might able to replace the C code with the code above.

        While(RegisterY!=0)
        WSYNC();
	IncreasePFColorUsingVariable(Color);
        Donedrawingonthisline();}

Some kernal uses the y register to keep track of scanlines so it can fall through and go in the overscan area. Some time use timer register to fall through. I experienced course scanline count that the timer may have pushes my game to 263 scanline, still trying to understand that part.

The key problem is doing C is that you have to design a scanline kernal. Placing Sprite anywhere on screen cost more cycle than using a fix y position using tables to load the data to the sprite registers. It's hard to explain since you have 76 cycle a scanline. You can't update everything on a line. If you're over 76 cycle, it automatically start drawing the next line. You have 22 cycle of Hblank to update your register before the beam enter from the right. There's a lot of information for newcomer to understand about the Atari 2600 like how to place sprite horizontal because there's no x coordinate. Or understand what the motion registers are for the 5 objects. Or the usage of HMOVE in the middle of the screen to reposition the sprites.

C should be possible, but have to understand there's under scan for your calculation, the 192 line display kernel loop, overscan usually to check the sprites/PF for overlaps. Display Kernel is the difficult part to understand.

Source code:

	seg Code
        org $f000

Start
	CLEAN_START

NextFrame
        lsr SWCHB	; test Game Reset switch
        bcc Start	; reset?

; 1 + 3 lines of VSYNC
	VERTICAL_SYNC
; 37 lines of underscan
	TIMER_SETUP 37
        lda #$AA
	sta PF1
	sta COLUPF
	sta Color
	ldy #192
        TIMER_WAIT
; 192 lines of frame
loop
        STA WSYNC
	ldx Color
	inx 
	stx COLUPF
	stx Color
	dey
        cpy #0
        bne loop
; 29 lines of overscan
	TIMER_SETUP 29
        TIMER_WAIT
; total = 262 lines, go to next frame
        jmp NextFrame
Edited by Kiwi
Link to comment
Share on other sites

OK, I now see what you are trying (code always helps icon_smile.gif).

 

Yes, you can change values in the first timer loop. But you should do your changes before and use that loop for syncing only. But that area is outside the visible area. Instead you have to change your TIA registers in the display frame area.

 

Here is a simple kernel, which updates the playfield color:

// RIOT.t1024t = KERNAL_T1024;
line = 228;
do 
{
  TIA.colupf = line; // change color every (other) line
  TIA.wsync = 0x00;  // start next scanline (any other value will do too, WSYNC doesn't care)  
} while (--line);
// while (RIOT.timint == 0) {}
// TIA.wsync = 0x00;
I hope this helps.

 

 

NOTE: Ok so I should ask if anyone knows what the NTSC timings are as I'm in the US and wont be able to run this code on my UnoCart.

// PAL Timings
// Roughly computed based on Stella Programmer's guide (Steve Wright)
// scanlines count per section.
#define VBLANK_TIM64 51 // 45 lines * 76 cycles/line / 64 cycles/tick
#define KERNAL_T1024 17 // 228 lines * 76 cycles/line / 1024 cycles/tick
#define OVERSCAN_TIM64 42 // 36 lines * 76 cycles/line / 64 cycles/tick

 

Using your suggestion I modified it to wait every other line as I think some asm instructions from C are taking to long so things can't keep up.

#include <atari2600.h>

// PAL Timings
// Roughly computed based on Stella Programmer's guide (Steve Wright)
// scanlines count per section.
#define VBLANK_TIM64 51 // 45 lines * 76 cycles/line / 64 cycles/tick
#define KERNAL_T1024 17 // 228 lines * 76 cycles/line / 1024 cycles/tick
#define OVERSCAN_TIM64 42 // 36 lines * 76 cycles/line / 64 cycles/tick

// test variables
unsigned char flip = 0;
//unsigned char buffer[80 * 96];// UnoCart back-buffer test

void main(void)
{
unsigned char line = 0;
//TIA.colubk = 0xFF;// background color

//TIA.colupf = 0x00;// playfield color
//TIA.pf0 = 0x55;// playfield pattern 1 (01010101)
//TIA.pf1 = 0xAA;// playfield pattern 2 (10101010)
//TIA.pf2 = 0x55;// playfield pattern 3 (01010101)

while (1)
{
TIA.vblank = 0x00;// enable vblank

// Vertical Sync signal
TIA.vsync = 0x02;
TIA.wsync = 0x00;
TIA.wsync = 0x00;
TIA.wsync = 0x00;
TIA.vsync = 0x00;

// write every two lines as one color
TIA.wsync = 0x00;// not sure why I need to wait here?
do
{
if (flip) TIA.wsync = 0x00;// wait for end of vblank
else TIA.colubk = line;// change color
flip = !flip;
} while (--line);

TIA.wsync = 0x00;// wait for last color to finish writing

// disable vblack
TIA.vblank = 0x02;
//TIA.vblank = 42;// is this the correct value?

// set timer to overscan
RIOT.tim64t = OVERSCAN_TIM64;

// prep next frame while waiting (main game logic goes here)
flip = 0;
line = 192;
TIA.colubk = line;

// wait for overscan to finish
while (RIOT.timint == 0) {}
}
}

 

 

I now can make a color pattern in C however I need to learn how to do this with the CA65 assembly compiler.

HZ6Za2R.png

 

Now I have 3 major things I would like to figure out if you or anyone has any tips.

1) How to render sprites (What timing helpers do I use for this).

2) Double-Buffering using the UnoCart with 32k of ram. How do I compile a .3e rom that allows for this.

3) CA65 how to compile an asm method I can invoke from C? (Having a hard time finding info here [maybe searching for the wrong thing])

Edited by zezba9000
Link to comment
Share on other sites

Using your suggestion I modified it to wait every other line as I think some asm instructions from C are taking to long so things can't keep up.

If you cannot do a simple color change within on scanline (hard to believe), then you will not get far at all using C for the kernel.

 

Some math: With assembler, such a simple change takes 3-8 cycles (out of 76 per scanline). Updating just the playfield takes at least ~28 cycles (only PF1 and PF2) up to ~48 cycles. So this update, which (for an asymmetrical playfield) has to happen every scanline, will require multiple scanlines.

 

You should look at the generated assembler code and identify why your simple kernel loop takes so long and try to fix that first.

Edited by Thomas Jentzsch
Link to comment
Share on other sites

If you cannot do a simple color change within on scanline (hard to believe), then you will not get far at all using C for the kernel.

 

Some math: With assembler, such a simple change takes 3-8 cycles (out of 76 per scanline). Updating just the playfield takes at least ~28 cycles (only PF1 and PF2) up to ~48 cycles. So this update, which (for an asymmetrical playfield) has to happen every scanline, will require multiple scanlines.

 

You should look at the generated assembler code and identify why your simple kernel loop takes so long and try to fix that first.

I might be wrong about the assembly will have to look. I'm updating the background not playfield in my last example. How many cycles does that take?

 

Also I wasn't compiling with optimizations (oops). So problem now is fixed and I can use the code below.

// write every line as new color (seems to effect every two though)
do
{
TIA.wsync = 0x00;// wait for end of vblank
TIA.colubk = line;// change color
} while (--line);
Edited by zezba9000
Link to comment
Share on other sites

Assuming you have to load line each time (instead of keeping it in a register) and it is a zeropage address, it should take 6 cycles. If the variable is optimized into a register, then it should take 3 cycles.

 

You can use Stella's debugger to look at the generated code and cycles consumed in every detail.

Link to comment
Share on other sites

C is basically just a macro assembler anyway. Doing this stuff in C is the same as doing it in assembler for the most part. You will spend a lot of time driving the chips and registers with stuff like

TIA.somereg = somevalue

which is pretty much the same as

LDA somevalue

STA somereg

 

You won't be able to use much of the C library, no printf's or fopens or anything like that. You won't gain anything in particular from using C anyways, other than the syntax for bit twiddling and testing if you know it better in C and some extra slowness.

Link to comment
Share on other sites

I'm late to respond here, but my game Robo-Ninja Climb has the main logic written in C in cc65, with the kernel and atari-specific stuff written in ca65 asm. (the game logic is the same across this version and my NES version of the game) C is fairly slow, but it worked out fine. Source code is available at bitbucket.

 

 

If the compiler is smart, it might help you optimizing register usage. Probably better than a noob would do but worse than an experienced coder.

 

It's not smart. Register usage in cc65 is pretty inefficient, and ends up being relatively slow.

Edited by gauauu
  • Like 1
Link to comment
Share on other sites

I'm late to respond here, but my game Robo-Ninja Climb has the main logic written in C in cc65, with the kernel and atari-specific stuff written in ca65 asm. (the game logic is the same across this version and my NES version of the game) C is fairly slow, but it worked out fine. Source code is available at bitbucket.

 

 

 

It's not smart. Register usage in cc65 is pretty inefficient, and ends up being relatively slow.

Thanks! I've been looking for a demo like this. You're using CA65 for the assembly and CC65 for the C correct? I'll download after work.

 

Also anyone know how to compile out a .3e rom with the CC65 toolkit so I get 32k of ram?

EDIT: Rather anyone know how to store memory in the 32k location in CA65 assembly? So I can hold a back-buffer there. Then I can just write the kernel parts in asm.

I'm newer to using assembly as most my work is in C/C++ or C#.

Edited by zezba9000
Link to comment
Share on other sites

Thanks! I've been looking for a demo like this. You're using CA65 for the assembly and CC65 for the C correct? I'll download after work.

 

Also anyone know how to compile out a .3e rom with the CC65 toolkit so I get 32k of ram?

 

Yeah, ca65 for assembly, cc65 for C.

 

I don't know enough about the rom layout for .3e, but you need to adjust the ld65 linker config file to tell it where ram lives, and the difference between zero page and other ram. Something like this:

MEMORY {
  ZP:      start = $0080, size = $0080, type = rw;
  RAM:     start = $0200, size = $0600, type = rw; #I don't know where ram really goes?

  #these are your various banks. adjust per the size of the bank and start address
  ROM0:    start = $F000, size = $1000,    type = ro, file = %O, fill=yes, fillval=$FF;
  ROM1:    start = $F000, size = $1000,    type = ro, file = %O, fill=yes, fillval=$FF;
}

SEGMENTS {
  ZEROPAGE:  load = ZP, type = zp;
  BSS:       load = RAM, type = bss, define = yes;

  #this assumes you have all these segments. adjust as needed.
  CODE0:     load = ROM0, type = ro; 
  RODATA0:   load = ROM0, type = ro;
  VECTORS0:  load = ROM0, type = ro, start=$FFFC;

  CODE1:     load = ROM1, type = ro; 
  RODATA1:   load = ROM1, type = ro;
  VECTORS1:  load = ROM1, type = ro, start=$FFFC;


}
Link to comment
Share on other sites

 

 

Yeah, ca65 for assembly, cc65 for C.

 

I don't know enough about the rom layout for .3e, but you need to adjust the ld65 linker config file to tell it where ram lives, and the difference between zero page and other ram. Something like this:

MEMORY {
  ZP:      start = $0080, size = $0080, type = rw;
  RAM:     start = $0200, size = $0600, type = rw; #I don't know where ram really goes?

  #these are your various banks. adjust per the size of the bank and start address
  ROM0:    start = $F000, size = $1000,    type = ro, file = %O, fill=yes, fillval=$FF;
  ROM1:    start = $F000, size = $1000,    type = ro, file = %O, fill=yes, fillval=$FF;
}

SEGMENTS {
  ZEROPAGE:  load = ZP, type = zp;
  BSS:       load = RAM, type = bss, define = yes;

  #this assumes you have all these segments. adjust as needed.
  CODE0:     load = ROM0, type = ro; 
  RODATA0:   load = ROM0, type = ro;
  VECTORS0:  load = ROM0, type = ro, start=$FFFC;

  CODE1:     load = ROM1, type = ro; 
  RODATA1:   load = ROM1, type = ro;
  VECTORS1:  load = ROM1, type = ro, start=$FFFC;


}

 

Looks like this doc says where it goes: http://kevtris.org/files/sizes.txt

"F4 - 'Standard' 32K; uses 1FF4 to 1FFB"

 

Because the UnoCart uses the same spec as Stella for 32k .3e roms that might work.

Link to comment
Share on other sites

  • 1 year later...
  • 3 years later...

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...