Thursday, April 3, 2014

Hello World 2600

For comparison purposes, this week I am showing an assembly language version of Hello World, but created for the Atari 2600. This is using DASM as the assembler due to it's popularity with the 2600 home-brew crowd. This assembler supports a number of different chips so we start off with a declaration of the processor type. This is followed by setting up the constants that we use. In this case, the constants are the names of the TIA registers which are mapped to memory. While it may seem strange that many of these ports are mapped to precious zero page addresses, the 2600 only has 128 bytes of RAM so all RAM is in zero page anyway.

processor 6502

; Set up TIA registers as constants

VSYNC  = $00
VBLANK = $01
WSYNC  = $02
COLUPF = $08
COLUBK = $09
PF0    = $0D
PF1    = $0E
PF2    = $0F
INTIM  = $284
TIM64T = $296

As with the NES, things start out with basic housekeeping. Memory and TIA registers are zeroed out. The colours for the background and playfield are then set.

org $F000
Start
; Typical starting houskeeping
SEI ; Disable  Interrupts
CLD ; Clear BCD mode.
LDX #$FF ; Set ...
TXS ; ... stack pointer

; lets take advantage of X to wipe memory and TIA registers
LDA #0
ClearMemory
STA 0,X
DEX
BNE ClearMemory

; set up background and playfield colors
LDA #$CE ; A light greenish color
STA COLUBK
LDA #$60 ; Purple!
STA COLUPF

Next we start the main loop. This is where things get really different. The 2600 does not have any type of graphics memory. Instead, the display is drawn by the program as the television is actually drawing the display. This means that the program has to manage the television display. The first thing for doing that is to perform a Vertical sync which synchronizes the television signal with the frame that we are sending. This is done by telling the TIA chip we are syncing then waiting for 3 scanlines to be drawn. The WSYNC register will halt the processor until the horizontal blank (when the TV's Cathode ray is turned off and moved to the left for the next scanline) starts. This is followed by a 37 scan line  blank period before we start drawing the display. By setting a timer, we can do some work while we wait. The timer is set for 2752 cycles before it goes off. We have nothing to do, so we will waste this time.

MainLoop
; Vertical sync
; bit D1 of VSYNC needs to be set to turn on vsync
LDA #2
STA VSYNC
; now wait for 3 scanlines
STA WSYNC
STA WSYNC
STA WSYNC

; Vertical Blank
; set timer so we know when vertical blank nearly over
LDA  #43 ;load 43 (decimal) in the accumulator
STA  TIM64T ;and store that in the timer
; end VSync
LDA #0 ; Zero out bit 2 of VSYNC
STA  VSYNC ; to indicate sync time is over

; some game logic can go here while vertical blank happening
; as long as less than 2752 cycles

WaitForEndOfVBlank
LDA INTIM ; load remaining time
BNE WaitForEndOfVBlank ; wait till timer done

TAX ; set x to 0 for holding current scanline
TAY ; set y to 0 for offset data
; end vblank period
STA WSYNC
STA VBLANK  

Now we are ready to start drawing the message. The 2600 does not have any type of text capability so we are going to create the message using playfield graphics. This is only 40 blocks per scan line so rather rough looking but it is what we have. The thing is, the playfield registers only support 20 blocks with the other half of the display either copied or mirrored. In order to have an asymmetrical display like we need, the playfield registers need to be changed in the middle of drawing the line. Each line consists of 22 2/3 cycles in the horizontal blank period followed by 53 1/3 cycles of actual drawing time for a total of 76 cycles per scan line. These are 6502 cycles, as the TIA uses something it calls color-clocks which happen at 3 times the rate of processor cycles. This means that we need to set up the playfield registers by loading in our data from a data segment at the end of the program.  Then we do other stuff until the beam is far enough along. As I am repeating the playfield data for 16 lines per data set, the checking if time to move to next line of data is done in the middle of the scan to let the beam catch up with us. Finally, we set up the other half of the display then finish our end-of-scan-line logic.

ScanLoop
; fill left playfield data from table
LDA PlayfieldData,Y
STA PF0
LDA PlayfieldData+1,Y
STA PF1
LDA PlayfieldData+2,Y
STA PF2

; end of scanline logic done here so beam is far enough to reload 
; playfields. We are simply checking if on a line evenly divisible by
; 16 since when 16 (32,48,...) the lower bits will be zeros.
INX
TXA
AND #15
PHA

; replace playfield data with right side data from table
LDA PlayfieldData+3,Y
STA PF0
LDA PlayfieldData+4,Y
STA PF1
LDA PlayfieldData+5,Y
STA PF2

; durring mid line we calculated if time for next set of playfield bytes
; and pushed it onto stack. Pull results and check
PLA
BNE EndOfLine
; if time for new playfield data, increment y index by 6
TYA
CLC
ADC #6
TAY

EndOfLine
STA WSYNC
; are we finished rendering?
CPX #191
BNE ScanLoop

Once we have finished drawing the display, it is time for the over scan. This lasts 30 scan lines. Again, a timer is set up. Other things can be done while the timer is running, but we don't have anything to do so this will be wasted as well. Obviously, in a game this is where some of the game logic would be handled, and all the buttons handled. Did I mention that the buttons on the console, including the reset button, are the responsibility of the program? Thankfully we really don't need to worry about that with this program.

; Overscan
LDA #2 ; Set D1 bit for the VBLANK...
STA VBLANK ; Make TIA output invisible for the overscan, 
; set timer for overscan
LDA #35
STA TIM64T

; could put more game logic here if we had any
; 2240 cycles set on timer

WaitForOverscan
LDA INTIM ; load remaining time
BNE WaitForOverscan ; wait till timer done
STA WSYNC

JMP  MainLoop      ; Loop forver!

And now we are done. Now the playfield data. Notice that the playfield registers are not logically set up. PF0 is only half a byte, with bits 4 through 7 used drawn in that order (backwards from a human perspective). PF1 is written from bits 7 to 0 so is logical from a human perspective. PF2 is written from bits 0 to 7 so again is backwards from a human perspective. This strangeness was most likely done to make the TIA chip easier and cheaper to produce. Still, it is simply a matter of making sure the data is in the correct order, so here is the display data.

; Game Data
PlayfieldData ; pf0-4..7  pf1-7..0  pf2 0..7   pf0-4..7   pf1-7..0    pf2-0..7
.byte 000000, 000000, 000000, 000000, 000000, 000000 
.byte %01000000, %01011110, 100001, %10000000, %10000000, 000000 
.byte %01000000, %01010000, 100001, %01000000, %01000000, 000000 
.byte %11000000, %11011100, 100001, %01000000, %01000000, 000000 
.byte %01000000, %01010000, 100001, %01000000, %01000000, 000000 
.byte %01000000, %01011110, %11101111, %10010000, %10000000, 000000 
.byte 000000, 000000, 000000, 000000, 000000, 000000 
.byte 000000, 000010, %01100100, %11100000, 100001, 100011 
.byte 000000, 000010, %10010100, 100000, %10100001, 100100 
.byte 000000, 000010, %10010101, %11100000, 100001, 100100 
.byte 000000, 000011, %10010110, 100000, %10100001, 000100 
.byte 000000, 000010, %01100100, 100000, %10111101, 100011 
.byte 000000, 000000, 000000, 000000, 000000, 000000 

; Set pointers hardware uses to find start of program
org $FFFC
.word Start
.word Start

And we are finished. Clearly the NES is a nicer system but it is a lot newer. Considering what is involved in creating a 2600 game, you have to be impressed with many of the games that were created for the platform.

No comments: