Skip to content

Instantly share code, notes, and snippets.

@cbmeeks
Created October 30, 2017 19:01
Show Gist options
  • Save cbmeeks/1350c07bde298733d9a9f195de7d613b to your computer and use it in GitHub Desktop.
Save cbmeeks/1350c07bde298733d9a9f195de7d613b to your computer and use it in GitHub Desktop.
Replace Default IRQ Handler

Replace Default IRQ Handler

We are used to inserting small routines at the beginning of the default interrupt handler as an IRQ wedge. This has an advantage of being easy and not requiring any modifications to the part of memory occupied by the ROM.

But the default handler is responsible for cursor blinking, handling the screen editor and few other things. As we don't usually need them in our sprite animation, the default handler's code is just a waste of cycles and memory for us.

We have a simple routine wedged into the interrupt handler. It changes the border and the background color, executes a hundred nops and changes the colors back.

This allows us to see how much raster time it takes to run this routine.

If we change colors in the main loop, we will be able to inspect the time needed by the default handler to run until it returns control to the main routine.

  :mov #GREEN; border_color
  :mov #LIGHT_GREEN; background_color

It seems that it takes slightly more time than our routine, which means it wastes a bit more than 200 hundred cycles.

But as soon as we press a key on the keyboard, the default handler executes an additional routine which wastes even more time.

That's a quite a lot of cycles to save if we get rid of that additional code.

But what about the part that runs before our handler starts. To figure it out, we need to remind ourselves how the interrupt process works.

When the IRQ signal occurs, the CPU reads the address at the last two bytes of the memory and jumps to it.

By default, the address points to a routine somewhere in the KERNAL code, which stores register values on the stack, checks whether the IRQ or BRK signal caused the interrupt and then jumps indirectly to the appropriate handler through the jump-table in the RAM.

The last part is what allows inserting IRQ wedges into the handler, but it also wastes few cycles. Let's see how many exactly.

We'll be modifying the address in the last two bytes of the memory. This area is occupied by KERNAL ROM, so we'll need to copy and disable ROM's entirely.

  :copy_rom_pages($a000, $bfff) 
  :copy_rom_pages($e000, $ffff) 
  lda $01
  and #%11111101
  sta $01

Now we need to create a small routine that will be executed before the IRQ handler. It will just change border and background colors and jump to the actual handler.

before_irq_handler:
  :mov #LIGHT_GRAY; border_color
  :mov #GRAY; background_color
  jmp $0000

As we don't know the jump's address yet, we'll mark it with a label.

.label jump_destination = * + 1

Now, we will copy the address from the end of the memory, and replace it with the address of our routine while interrupts are disabled.

  :mov16 $fffe; jump_destination
  :mov16 #before_irq_handler; $fffe

With that in place, we can see that the initialization code takes almost one whole raster line. The gap in the background color change at the beginning is caused by a few cycles it took to execute lda and sta instructions.

Getting rid of the interrupt handler code is now trivial.

Instead of injecting the address of our routine indirectly into the irq_vector, we can put it at the last two bytes of the memory.

  :mov16 #irq1; $fffe

We can also remove unnecessary timing related code.

  :mov16 $fffe; jump_destination
  :mov16 #before_irq_handler; $fffe
before_irq_handler:
  :mov #LIGHT_GRAY; border_color
  :mov #GRAY; background_color
.label jump_destination = * + 1
  jmp $0000

And instead of jumping into the default handler, we will execute the rti instruction to return from the interrupt.

  rti

It works, and we saved a lot of cycles. Sadly, in most cases, we will need to add a bit more code to the beginning and the end of an interrupt.

As we remember, only the program counter and the status register are saved and recovered automatically.

Accumulator, X, and Y registers are not. If we don't save and restore them in an interrupt, we are going to overwrite them and cause unexpected bugs in the main code.

Consider this situation. In the main code, we will load index registers with colors.

  ldx #GREEN
  ldy #LIGHT_GREEN

Then in the loop, we will store their values into the border and background color registers.

  stx border_color
  sty background_color

If we change their values in the interrupt handler and run the program, we can see that they have been overwritten.

  ldx #GRAY
  ldy #LIGHT_GRAY

In general, we need to make sure that our interrupt code saves and restores values of any registers that are changed within.

One way to do that is to save them on the stack. First, we save the accumulator, then both X and Y in any order.

  pha
  tya
  pha
  txa
  pha

Now at the end of the routine, we need to restore them starting from the one that was saved last.

  pla
  tax
  pla
  tay
  pla

But this is not the only way to save the state.

We can also save all values in temporary addresses in the memory.

  sta atemp
  stx xtemp
  sty ytemp

Now we can load them in any order.

  lda atemp
  ldy ytemp
  ldx xtemp
  rti

atemp: .byte $00
xtemp: .byte $00
ytemp: .byte $00

This solution is slightly slower unless our temporary addresses lie in zero page.

But if we can make the code self-modifying we can make the restoring part even faster by changing lda instructions into an immediate mode and pointing temporary addresses right into their arguments.

.label atemp = * + 1
  lda #$00
.label xtemp = * + 1
  ldx #$00
.label ytemp = * + 1
  ldy #$00

When we have multiple interrupt routines, we need to inject their addresses right at the end of the address space instead of the irq_vector address used when inserting it as a wedge.

  :mov16 #irq2; $fffe
  :set_raster(180)  
irq2:
  sta atemp
  stx xtemp
  sty ytemp
  :mov #BROWN; border_color
  :mov #ORANGE; background_color
  ldx #GRAY
  ldy #LIGHT_GRAY
  .for (var i = 0; i < 100; i++) {
    nop
  }
  :mov #LIGHT_BLUE; border_color
  :mov #BLUE; background_color

  :mov16 #irq1; $fffe
  :set_raster(140)  

  lda #%00000001
  sta vic2_interrupt_status_register
.label atemp = * + 1
  lda #$00
.label xtemp = * + 1
  ldx #$00
.label ytemp = * + 1
  ldy #$00
  rti

When dealing with multiple interrupt handlers, it might be useful to extract the last part of the code into a subroutine and just jump to it.

This way we will add three cycles to every call, but we will also save four bytes on each routine.

Those numbers will add up and can potentially make a huge difference. So it's pretty useful to know these tricks.

See you soon!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment