SixtyPical/eg/c64/ribos/ribos.p65

294 lines
10 KiB
Plaintext

; ribos.p65 - p65 assembly source for RIBOS:
; Demonstration of the VIC-II raster interrupt on the Commodore 64:
; Alter the border colour in the middle part of the screen only.
; Original (hardware IRQ vector) version.
; SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
; For more information, please refer to <https://unlicense.org/>
; SPDX-License-Identifier: Unlicense
; ----- BEGIN ribos.p65 -----
; This source file is intented to be assembled to produce a PRG file
; which can be loaded into the C64's memory from a peripheral device.
; All C64 PRG files start with a 16-bit word which represents the
; location in memory to which they will be loaded. We can provide this
; using p65 directives as follows:
.org 0
.word $C000
; Now the actual assembly starts (at memory location 49152.)
.org $C000
; ----- Constants -----
; We first define some symbolic constants to add some clarity to our
; references to hardware registers and other locations in memory.
; Descriptions of the registers follow those given in the 'Commodore 64
; Programmer's Reference Guide'.
; The CIA #1 chip.
.alias cia1 $dc00 ; pp. 328-331
.alias intr_ctrl cia1+$d ; "CIA Interrupt Control Register
; (Read IRQs/Write Mask)"
; The VIC-II chip.
.alias vic $d000 ; Appendix G:
.alias vic_ctrl vic+$11 ; "Y SCROLL MODE"
.alias vic_raster vic+$12 ; "RASTER"
.alias vic_intr vic+$19 ; "Interrupt Request's" (sic)
.alias vic_intr_enable vic+$1a ; "Interrupt Request MASKS"
.alias border_color vic+$20 ; "BORDER COLOR"
; The address at which the IRQ vector is stored.
.alias irq_vec $fffe ; p. 411
; The zero-page address of the 6510's I/O register.
.alias io_ctrl $01 ; p. 310, "6510 On-Chip 8-Bit
; Input/Output Register"
; KERNAL and BASIC ROMs, p. 320
.alias kernal $e000
.alias basic $a000
; Zero-page addresses that the memory-copy routine uses for
; scratch space: "FREKZP", p. 316
.alias zp $fb
.alias stop_at $fd
; ----- Main Routine -----
; This routine is intended to be called by the user (by, e.g., SYS 49152).
; It installs the raster interrupt handler and returns to the caller.
; Key to installing the interrupt handler is altering the IRQ service
; vector. However, under normal circumstances, the address at which
; this vector is stored ($ffffe) maps to the C64's KERNAL ROM, which
; cannot be changed. So, in order to alter the vector, we must enable
; the RAM that underlies the ROM (i.e. the RAM that maps to the same
; address space as the KERNAL ROM.) If we were writing a bare-metal
; game, and didn't need any KERNAL routines or support, we could just
; switch it off. But for this demo, we'd like the raster effect to
; occur in the background as we use BASIC and whatnot, so we need to
; continue to have access to the KERNAL ROM. So what we do is copy the
; KERNAL ROM to the underlying RAM, then switch the RAM for the ROM.
jsr copy_rom_to_ram
; Interrupts can occur at any time. If one were to occur while we were
; changing the interrupt vector - for example, after we have stored the
; low byte of the address but before we have stored the high byte -
; unpredictable behaviour would result. To be safe, we disable interrupts
; with the 'sei' instruction before changing anything.
sei
; We obtain the address of the current IRQ service routine and save it
; in the variable 'saved_irq_vec'.
lda irq_vec ; save low byte
sta saved_irq_vec
lda irq_vec+1 ; save high byte
sta saved_irq_vec+1
; We then store the address of our IRQ service routine in its place.
lda #<our_service_routine
sta irq_vec
lda #>our_service_routine
sta irq_vec+1
; Now we must specify the raster line at which the interrupt gets called.
lda scanline
sta vic_raster
; Note that the position of the raster line is given by a 9-bit value,
; and we can't just assume that, because the raster line we want is less
; than 256, that the high bit will automatically be set to zero, because
; it won't. We have to explicitly set it high or low. It is found as
; the most significant bit of a VIC-II control register which has many
; different functions, so we must be careful to preserve all other bits.
lda vic_ctrl
and #%01111111
sta vic_ctrl
; Then we enable the raster interrupt on the VIC-II chip.
lda #$01
sta vic_intr_enable
; The article at everything2 suggests that we read the interrupt control
; port of the CIA #1 chip, presumably to acknowledge any pending IRQ and
; avoid the problem of having some sort of lockup due to a spurious IRQ.
; I've tested leaving this out, and the interrupt handler still seems get
; installed alright. But, I haven't tested it very demandingly, and it's
; likely to open up a race condition that I just haven't encountered (much
; like if we were to forget to execute the 'sei' instruction, above.)
; So, to play it safe, we read the port here.
lda intr_ctrl
; We re-enable interrupts to resume normal operation - normal, that is,
; except that our raster interrupt service routine will be called the next
; time the raster reaches the line stored in the 'vic_raster' register of
; the VIC-II chip.
cli
; Finally, we return to the caller.
rts
; ----- Raster Interrupt Service Routine ------
our_service_routine:
; This is an interrupt service routine (a.k.a. interrupt handler,) and as
; such, it can be called from anywhere. Since the code that was interrupted
; likely cares deeply about the values in its registers, we must be careful
; to save any that we change, and restore them before switching back to it.
; In this case, we only affect the processor flags and the accumulator, so
; we push them onto the stack.
php
pha
; The interrupt service routine on the Commodore 64 is very general-purpose,
; and may be invoked by any number of different kinds of interrupts. We,
; however, only care about a certain kind - the VIC-II's raster interrupt.
; We check to see if the current interrupt was caused by the raster by
; looking at the low bit of the VIC-II interrupt register. Note that we
; immediately store back the value found there before testing it. This is
; to acknowledge to the VIC-II chip that we got the interrupt. If we don't
; do this, it won't send us another interrupt next time.
lda vic_intr
sta vic_intr
and #$01
cmp #$01
beq we_handle_it
; If the interrupt was not caused by the raster, we restore the values
; of the registers from the stack, and continue execution as normal with
; the existing interrupt service routine.
pla
plp
jmp (saved_irq_vec)
we_handle_it:
; If we got here, the interrupt _was_ caused by the raster. So, we get
; to do our thing. To keep things simple, we just invert the border colour.
lda border_color
eor #$ff
sta border_color
; Now, we make the interrupt trigger on a different scan line so that we'll
; invert the colour back to normal lower down on the screen.
lda scanline
eor #$ff
sta scanline
sta vic_raster
; Restore the registers that we saved at the beginning of this routine.
pla
plp
; Return to normal operation. Note that we must issue an 'rti' instruction
; here, not 'rts', as we are returning from an interrupt.
rti
; ----- Utility Routine: copy KERNAL ROM to underlying RAM -----
copy_rom_to_ram:
; This is somewhat more involved than I let on above. The memory mapping
; facilities of the C64 are a bit convoluted. The Programmer's Reference
; Guide states on page 261 that the way to map out the KERNAL ROM, and
; map in the RAM underlying it, is to set the HIRAM signal on the 6510's
; I/O line (which is memory-mapped to address $0001) to 0. This is true.
; However, it is not the whole story: setting HIRAM to 0 *also* maps out
; BASIC ROM and maps in the RAM underlying *it*. I suppose this makes
; sense from a design point of view; after all, BASIC uses the KERNAL, so
; there wouldn't be much sense leaving it mapped when the KERNAL is mapped
; out. Anyway, what this means for us is that we must copy both of these
; ROMs to their respective underlying RAMs if we want to survive returning
; to BASIC.
ldx #>basic
ldy #$c0
jsr copy_block
ldx #>kernal
ldy #$00
jsr copy_block
; To actually substitute the RAM for the ROM in the memory map, we
; set HIRAM (the second least significant bit) to 0.
lda io_ctrl
and #%11111101
sta io_ctrl
rts
; ----- Utility Routine: copy a ROM memory block to the underlying RAM -----
; Input: x register = high byte of start address (low byte = #$00)
; y register = high byte of end address (stops at address $yy00 - 1)
; This subroutine is a fairly straightforward memory copy loop. A somewhat
; counter-intuitive feature is that we immediately store each byte in the
; same location where we just read it from. We can do this because, even
; when the KERNAL or BASIC ROM is mapped in, writes to those locations still
; go to the underlying RAM.
copy_block: stx zp+1
sty stop_at
ldy #$00
sty zp
copy_loop: lda (zp), y
sta (zp), y
iny
cpy #$00
bne copy_loop
ldx zp+1
inx
stx zp+1
cpx stop_at
bne copy_loop
rts
; ----- Variables -----
; 'scanline' stores the raster line that we want the interrupt to trigger
; on; it gets loaded into the VIC-II's 'vic_raster' register.
scanline: .byte %01010101
; We also reserve space to store the address of the interrupt service
; routine that we are replacing in the IRQ vector, so that we can transfer
; control to it at the end of our routine.
.space saved_irq_vec 2
; ----- END of ribos.p65 -----