diff --git a/HISTORY.md b/HISTORY.md index b3aad96..50cdde1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -8,6 +8,8 @@ History of SixtyPical be used in most places where literal values can be used. * Specifying multiple SixtyPical source files will produce a single compiled result from their combination. +* Rudimentary support for Atari 2600 prelude in a 4K cartridge image, + and start of an example program in `eg/atari2600` directory. 0.14 ---- diff --git a/bin/sixtypical b/bin/sixtypical index 299ba47..b296f94 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -81,6 +81,12 @@ def process_input_files(filenames, options): start_addr = 0x1001 prelude = [0x0b, 0x10, 0xc9, 0x07, 0x9e, 0x34, 0x31, 0x30, 0x39, 0x00, 0x00, 0x00] + elif options.prelude == 'atari2600': + output_format = 'crtbb' + start_addr = 0xf000 + prelude = [0x78, 0xd8, 0xa2, 0xff, 0x9a, 0xa9, + 0x00,0x95, 0x00, 0xca, 0xd0, 0xfb] + elif options.prelude: raise NotImplementedError("Unknown prelude: {}".format(options.prelude)) @@ -94,6 +100,14 @@ def process_input_files(filenames, options): emitter.emit(Byte(byte)) compiler = Compiler(emitter) compiler.compile_program(program) + + # If we are outputting a cartridge with boot and BRK address + # at the end, pad to ROM size minus 4 bytes, and emit addresses. + if output_format == 'crtbb': + emitter.pad_to_size(4096 - 4) + emitter.emit(Word(start_addr)) + emitter.emit(Word(start_addr)) + if options.debug: pprint(emitter.accum) else: @@ -119,15 +133,15 @@ if __name__ == '__main__': ) argparser.add_argument( "--output-format", type=str, default='prg', - help="Executable format to produce. Options are: prg (.PRG file " - "for Commodore 8-bit). Default: prg." + help="Executable format to produce. Options are: prg, crtbb. " + "Default: prg." ) argparser.add_argument( "--prelude", type=str, - help="Insert a snippet before the compiled program " - "so that it can be LOADed and RUN on a certain platforms. " + help="Insert a snippet of code before the compiled program so that " + "it can be booted automatically on a particular platform. " "Also sets the origin and format. " - "Options are: c64 or vic20." + "Options are: c64, vic20, atari2600." ) argparser.add_argument( "--debug", diff --git a/eg/README.md b/eg/README.md index 79c0d91..a805a60 100644 --- a/eg/README.md +++ b/eg/README.md @@ -38,4 +38,11 @@ In the [vic20](vic20/) directory are programs that run on the Commodore VIC-20. The directory itself contains some simple demos, for example [hearts.60p](vic20/hearts.60p). +### atari2600 + +In the [vic20](vic20/) directory are programs that run on the +Atari 2600 (4K cartridge). The directory itself contains a simple +demo, [atari-2600-example.60p](atari2600/atari-2600-example.60p). +(Doesn't work yet.) + [Ophis]: http://michaelcmartin.github.io/Ophis/ diff --git a/eg/atari2600/atari-2600-example.60p b/eg/atari2600/atari-2600-example.60p new file mode 100644 index 0000000..04b94a0 --- /dev/null +++ b/eg/atari2600/atari-2600-example.60p @@ -0,0 +1,168 @@ +// atari-2600-example.60p - SixtyPical translation of atari-2600-example.oph + +byte VSYNC @ $00 +byte VBLANK @ $01 +byte WSYNC @ $02 +byte NUSIZ0 @ $04 +byte NUSIZ1 @ $05 +byte COLUPF @ $08 +byte COLUBK @ $09 +byte PF0 @ $0D +byte PF1 @ $0E +byte PF2 @ $0F +byte SWCHA @ $280 +byte INTIM @ $284 +byte TIM64T @ $296 +byte CTRLPF @ $0A +byte COLUP0 @ $06 +byte COLUP1 @ $07 +byte GP0 @ $1B +byte GP1 @ $1C +byte HMOVE @ $2a +byte RESP0 @ $10 +byte RESP1 @ $11 + +byte colour @ $80 +byte luminosity @ $81 +byte joystick_delay @ $82 + +byte table[8] image_data : "ZZZZUUUU" + // %01111110 + // %10000001 + // %10011001 + // %10100101 + // %10000001 + // %10100101 + // %10000001 + // %01111110 + + +define vertical_blank routine + outputs VSYNC, WSYNC, TIM64T + trashes a, x, z, n +{ + ld x, $00 + ld a, $02 + st a, WSYNC + st a, WSYNC + st a, WSYNC + st a, VSYNC + st a, WSYNC + st a, WSYNC + ld a, $2C + st a, TIM64T + ld a, $00 + st a, WSYNC + st a, VSYNC +} + +define display_frame routine + inputs INTIM, image_data + outputs WSYNC, HMOVE, VBLANK, RESP0, GP0, PF0, PF1, PF2, COLUPF, COLUBK + trashes a, x, y, z, n +{ + repeat { + ld a, INTIM + } until z + + //; (After that loop finishes, we know the accumulator must contain 0.) + + st a, WSYNC + st a, HMOVE + st a, VBLANK + + //; + //; Wait for $3f (plus one?) scan lines to pass, by waiting for + //; WSYNC that many times. + //; + + ld x, $3F + repeat { + st a, WSYNC + dec x + } until z // FIXME orig loop used "bpl _wsync_loop" + st a, WSYNC + + //; + //; Delay while the raster scans across the screen. The more + //; we delay here, the more to the right the player will be when + //; we draw it. + //; + + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + + //; + //; OK, *now* display the player. + //; + + st a, RESP0 + + //; + //; Loop over the rows of the sprite data, drawing each to the screen + //; over four scan lines. + //; + //; TODO understand this better and describe it! + //; + + ld y, $07 + for y down to 0 { + ld a, image_data + y + st a, GP0 + + st a, WSYNC + st a, WSYNC + st a, WSYNC + st a, WSYNC + } // FIXME original was "dec y; bpl _image_loop" + + ld a, $00 + st a, GP0 + + //; + //; Turn off screen display and clear display registers. + //; + + ld a, $02 + st a, WSYNC + st a, VBLANK + ld a, $00 + st a, PF0 + st a, PF1 + st a, PF2 + st a, COLUPF + st a, COLUBK +} + +define main routine + inputs image_data, INTIM + outputs CTRLPF, colour, luminosity, NUSIZ0, VSYNC, WSYNC, TIM64T, HMOVE, VBLANK, RESP0, GP0, PF0, PF1, PF2, COLUPF, COLUBK + trashes a, x, y, z, n +{ + ld a, $00 + st a, CTRLPF + ld a, $0c + st a, colour + ld a, $0a + st a, luminosity + ld a, $00 + st a, NUSIZ0 + repeat { + call vertical_blank + call display_frame + // call read_joystick + } forever +} diff --git a/eg/atari2600/atari-2600-example.oph b/eg/atari2600/atari-2600-example.oph new file mode 100644 index 0000000..836a64a --- /dev/null +++ b/eg/atari2600/atari-2600-example.oph @@ -0,0 +1,340 @@ +; +; atari-2600-example.oph +; Skeleton code for an Atari 2600 ROM, +; plus an example of reading the joystick. +; By Chris Pressey, November 2, 2012. +; +; This work is in the public domain. See the file UNLICENSE for more info. +; +; Based on Chris Cracknell's Atari 2600 clock (also in the public domain): +; http://everything2.com/title/An+example+of+Atari+2600+source+code +; +; to build and run in Stella: +; ophis atari-2600-example.oph -o example.bin +; stella example.bin +; +; More useful information can be found in the Stella Programmer's Guide: +; http://alienbill.com/2600/101/docs/stella.html +; + +; +; Useful system addresses (TODO: briefly describe each of these.) +; + +.alias VSYNC $00 +.alias VBLANK $01 +.alias WSYNC $02 +.alias NUSIZ0 $04 +.alias NUSIZ1 $05 +.alias COLUPF $08 +.alias COLUBK $09 +.alias PF0 $0D +.alias PF1 $0E +.alias PF2 $0F +.alias SWCHA $280 +.alias INTIM $284 +.alias TIM64T $296 +.alias CTRLPF $0A +.alias COLUP0 $06 +.alias COLUP1 $07 +.alias GP0 $1B +.alias GP1 $1C +.alias HMOVE $2a +.alias RESP0 $10 +.alias RESP1 $11 + +; +; Cartridge ROM occupies the top 4K of memory ($F000-$FFFF). +; Thus, typically, the program will occupy all that space too. +; +; Zero-page RAM we can use with impunity starts at $80 and goes +; upward (at least until $99, but probably further.) +; + +.alias colour $80 +.alias luminosity $81 +.alias joystick_delay $82 + +.org $F000 + +; +; Standard prelude for Atari 2600 cartridge code. +; +; Get various parts of the machine into a known state: +; +; - Disable interrupts +; - Clear the Decimal flag +; - Initialize the Stack Pointer +; - Zero all bytes in Zero Page memory +; + +start: + sei + cld + ldx #$FF + txs + lda #$00 + +zero_loop: + sta $00, x + dex + bne zero_loop + + ; and fall through to... + +; +; Initialization. +; +; - Clear the Playfield Control register. +; - Set the player (sprite) colour to light green (write to COLUP0.) +; - Set the player (sprite) size/repetion to normal (write to NUSIZ0.) +; + + lda #$00 + sta CTRLPF + lda #$0c + sta colour + lda #$0a + sta luminosity + lda #$00 + sta NUSIZ0 + + ; and fall through to... + +; +; Main loop. +; +; A typical main loop consists of: +; - Waiting for the frame to start (vertical blank period) +; - Displaying stuff on the screen (the _display kernel_) +; - Doing any processing you like (reading joysticks, updating program state, +; etc.), as long as you get it all done before the next frame starts! +; + +main: + jsr vertical_blank + jsr display_frame + jsr read_joystick + jmp main + +; +; Vertical blank routine. +; +; In brief: wait until it is time for the next frame of video. +; TODO: describe this in more detail. +; + +vertical_blank: + ldx #$00 + lda #$02 + sta WSYNC + sta WSYNC + sta WSYNC + sta VSYNC + sta WSYNC + sta WSYNC + lda #$2C + sta TIM64T + lda #$00 + sta WSYNC + sta VSYNC + rts + +; +; Display kernal. +; +; First, wait until it's time to display the frame. +; + +.scope +display_frame: + lda INTIM + bne display_frame + +; +; (After that loop finishes, we know the accumulator must contain 0.) +; Wait for the next scanline, zero HMOVE (for some reason; TODO discover +; this), then turn on the screen. +; + + sta WSYNC + sta HMOVE + sta VBLANK + +; +; Actual work in the display kernal is done here. +; +; This is a pathological approach to writing a display kernal. +; This wouldn't be how you'd do things in a game. So be it. +; One day I may improve it. For now, be happy that it displays +; anything at all! +; + +; +; Wait for $3f (plus one?) scan lines to pass, by waiting for +; WSYNC that many times. +; + + ldx #$3F +_wsync_loop: + sta WSYNC + dex + bpl _wsync_loop + sta WSYNC + +; +; Delay while the raster scans across the screen. The more +; we delay here, the more to the right the player will be when +; we draw it. +; + + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + +; +; OK, *now* display the player. +; + + sta RESP0 + +; +; Loop over the rows of the sprite data, drawing each to the screen +; over four scan lines. +; +; TODO understand this better and describe it! +; + + ldy #$07 +_image_loop: + lda image_data, y + sta GP0 + + sta WSYNC + sta WSYNC + sta WSYNC + sta WSYNC + dey + bpl _image_loop + + lda #$00 + sta GP0 + +; +; Turn off screen display and clear display registers. +; + + lda #$02 + sta WSYNC + sta VBLANK + lda #$00 + sta PF0 + sta PF1 + sta PF2 + sta COLUPF + sta COLUBK + + rts +.scend + + +; +; Read the joystick and use it to modify the colour and luminosity +; of the player. +; + +.scope +read_joystick: + lda joystick_delay + beq _continue + + dec joystick_delay + rts + +_continue: + lda SWCHA + and #$f0 + cmp #$e0 + beq _up + cmp #$d0 + beq _down + cmp #$b0 + beq _left + cmp #$70 + beq _right + jmp _tail + +_up: + inc luminosity + jmp _tail +_down: + dec luminosity + jmp _tail +_left: + dec colour + jmp _tail +_right: + inc colour + ;jmp _tail + +_tail: + lda colour + and #$0f + sta colour + + lda luminosity + and #$0f + sta luminosity + + lda colour + clc + rol + rol + rol + rol + ora luminosity + sta COLUP0 + + lda #$06 + sta joystick_delay + + rts +.scend + +; +; Player (sprite) data. +; +; Because we loop over these bytes with the Y register counting *down*, +; this image is stored "upside-down". +; + +image_data: + .byte %01111110 + .byte %10000001 + .byte %10011001 + .byte %10100101 + .byte %10000001 + .byte %10100101 + .byte %10000001 + .byte %01111110 + +; +; Standard postlude for Atari 2600 cartridge code. +; Give BRK and boot vectors that point to the start of the code. +; + +.advance $FFFC + .word start + .word start diff --git a/loadngo.sh b/loadngo.sh index b3d0e1d..ca9f198 100755 --- a/loadngo.sh +++ b/loadngo.sh @@ -1,6 +1,6 @@ #!/bin/sh -usage="Usage: loadngo.sh (c64|vic20) [--dry-run] " +usage="Usage: loadngo.sh (c64|vic20|atari2600) [--dry-run] " arch="$1" shift 1 @@ -18,6 +18,9 @@ elif [ "X$arch" = "Xvic20" ]; then else emu="xvic" fi +elif [ "X$arch" = "Xatari2600" ]; then + prelude='atari2600' + emu='stella' else echo $usage && exit 1 fi diff --git a/src/sixtypical/emitter.py b/src/sixtypical/emitter.py index aa14a59..78dbf95 100644 --- a/src/sixtypical/emitter.py +++ b/src/sixtypical/emitter.py @@ -186,3 +186,14 @@ class Emitter(object): advance the address for the next label, but don't emit anything.""" self.resolve_label(label) self.addr += label.length + + def size(self): + return sum(emittable.size() for emittable in self.accum) + + def pad_to_size(self, size): + self_size = self.size() + if self_size > size: + raise IndexError("Emitter size {} exceeds pad size {}".format(self_size, size)) + num_bytes = size - self_size + if num_bytes > 0: + self.accum.extend([Byte(0)] * num_bytes)