; 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 ; 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+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 -----