Amiga Machine Code Letter IX - Interrupts

Amiga Machine Code - Letter IX

We have reached Letter IX of the Amiga Machine Code course. As always, make sure to read the letter, since I won’t go through all the details.

In this post, we are going to take a closer look at interrupts. If you haven’t heard about them before, then it only shows how amazingly well abstractions work on a computer. You can spend your whole programming career in a high-level language, without ever having to look beneath the surface, to discover how a computer works at it’s most basic level. Interrupts are one of such concepts, that most programmers never need to deal with, yet they are quite vital.

Interrupts are raised, when we type away on keyboards, or when e.g. communicating with external devices. Interrupts are incredibly usefull, and keeps the processor free to do other things, knowing that it will get “interrupted” if anything important happens.

At the end of the post, we are going write an interrupt handler, that makes the Amiga power LED turn on and off, each time F10 is pressed. The program is called mc0901 in Letter IX.

The Exception Vector Table

When an interrupt happens, the system handles it by calling it’s interrupt handler. The handler is placed in memory and it’s memory location is added to the exception vector table, by assigning it to a given interrupt vector.

Already now, the terminology seems a bit odd. Why is it called an exception vector table, when we deal with interrupts? What’s the difference between the two?

The overall gist, is that interrupts are asynchronous events, triggered by e.g. external devices like a keypress on a keyboard, and are not in sync with processor instruction execution. These are also called hardware interrupts.

Exceptions, on the other hand, are synchronous events, that are in sync with processor instruction execution, and occours when the processor detects a failure mode, while executing an instruction. A failure mode could be e.g. division by zero.

The Amiga CPU, the Motorola 68000, has 256 thirty-two bit interrupt vectors. The table occupies 256 * 4 bytes = 1024 bytes in memory, from the address $\$000000$ to $\$0003FF$.

According to the 68k Reference Manual the table consists of 64 vectors defined by the processor, and 192 vectors that are user defined.

Below is a list of some of the vectors, that we are going to use in the interrupt handler for the keyboard.

Vector Address Exception
1 $\$000004$ RESET-Initial PC
25 $\$000064$ Level 1 interrupt autovector
26 $\$000068$ Level 2 interrupt autovector
27 $\$00006C$ Level 3 interrupt autovector
28 $\$000070$ Level 4 interrupt autovector
29 $\$000074$ Level 5 interrupt autovector
30 $\$000078$ Level 6 interrupt autovector
31 $\$00007C$ Level 7 interrupt autovector
255 $\$0003FF$ User definded vectors (192)

The interrupts are grouped in seven levels, where level 1 is the lowest priority interrupt. A higher level priority interrupt, can always “interrupt” a lower level interrupt handler, and invoke it’s own handler. The lower level interrupt handler then has to wait until the higher level interrupt handler has finished executing.

Using library functions

Calling library functions from machine code, is almost the same as calling subroutines. When calling a subroutine, we use the BSR instruction (branch to subroutine), which generates a realtive offset to a label, that is then added to the program counter at runtime, hence making a jump within the program.

When calling a library function, we use the JSR instruction (jump to subroutine), that takes an absolute address as input. At runtime the program counter will be set to the given address. This allows us to make a call to a function outside our program.

Amiga libraries are accessed through their base pointer. Below the base pointer are pointers to the various library functions, that are defined above the base pointer. It’s important to always use those pointers, since function implementations can change between versions of the library, while the pointers are guaranteed to stay the same.

library

The library base pointer, should, by convention, always be accessed through a6, since library functions also calls other library functions. Read more about it here.

The Amiga has a special library, called the exec.library, that provides functions for system related stuff, like memory managment, among others. The special thing about this library, is that it´s base pointer, called ExecBase, always can be found at the fixed address $\$000004$. All other libraries, by comparison, has base pointers stored at arbitrary memory locations. In other words, the exec.library acts as an entry point to all Amiga libraries, through it’s OpenLibrary and CloseLibrary functions.

Allocating memory

We have not looked at memory allocation before. Usually we just include allocations for assets like bitplanes and sound in the program, by defining labels and using the K-Seka pseudo-op’s dc and blk. The drawback of this strategy is that it produces rather large executables. This can be avoided completely, by allocating memory at run time.

The exec.library contains a function called AllocMem, that can allocate memory. Note that this function is obsolete, but that was not the case when letter IX was written, back in the late 80ties.

AllocMem
Description: allocates memory
Library:     exec.library
Offset:      -$C6 (-198)
Syntax:      memoryBlock = AllocMem(byteSize, attributes)
ML:          d0 = AllocMem(d0,d1)
Arguments:   byteSize = number of bytes required
             attributes = type of memory
             MEMF_ANY      ($00000000)
             MEMF_PUBLIC   ($00000001)
             MEMF_CHIP     ($00000002)
             MEMF_FAST     ($00000004)
             MEMF_LOCAL    ($00000008)
             MEMF_24BITDMA ($00000010)
             MEMF_CLEAR    ($00010000)
Result:      memoryBlock = allocated memory block

If we want to allocate 100 bytes of public memory, then we would need to put the number of bytes into d0 and the type of memory into d1. In machine code it would look like this:

moveq  #100,d0    ; byteSize
move.l #$1000,d1  ; attributes
move.l $4,a6      ; ExecBase of exec.library
jsr -198(a6)      ; jump to subroutine AllocMem

After running this code, d0 will hold a pointer to the allocated memory block.

After the jump, the program counter will run the AllocMem subroutine, but how will the program counter find it’s way back to the calling code?

The program counter is saved on the system stack on subroutine calls and is restored on returns. That’s why it’s important to call RTS, when the subroutine ends, otherwise the program counter will not be pointing back on the caller of the subroutine.

Let’s take a look at JSR, according to the Motorola 68000 Reference Manual.

Operation: SP - 4 => SP; PC => (SP)
           Destination Address => PC

This translates to: Push a new item onto the stack, and store the program counter in the address pointed to by the decremented stack pointer. Then set the program counter to the destination address. Note that the stack grows from higher to lower addresses.

Let’s look at the operation for RTS.

Operation: (SP) => PC; SP + 4 => SP

Which translates to: Set the program counter PC, to the value stored in the address of the stack pointer SP. Then increment the stack pointer, which is the same as popping the stack.

The program counter will now point back on the call site! 😃

Coding for speed

I found this link, which I will just leave here. It makes the point that the cost of pushing the return address on the stack is significant. So if we are in a thight loop, we might want to handle the program counter ourselves by using JMP, thus avoiding the stack completely.

However, this require that the subroutine does not call RTS, but instead calls JMP, so naturally we can’t use this for library functions, because conventions require them to return with RTS.

A Keyboard Interrupt Handler

On disk1 we can find the program mc0901, that registers an interrupt handler for the keyboard, that turns the power LED on and off, each time F10 is pressed.

The program starts by copying the address of the existing keyboard handler, into the new keyboard handler. In this way, we chain our own handler to the present handler, so that the keyboard still will function correctly.

We then allocate memory for the new keyboard interrupt handler, and then copies the handler code into the new memory location. Finally we set the entry for interrupt 2 in the Exception Vector Table, to point to our new handler.

Before assigning the handler to interrupt 2, we make sure that the interrupts are disabled. After assignment, we turn the interrupts back on again. This is followed by the program terminating.

However, the program might be terminated, but our keyboard interrupt handler is still alive and well. Each time we press F10, even outside K-Seka, the power LED toggles on and off.

lea.l	jump,a1          ; move address of jump into a1
move.l	$68,2(a1)        ; move value in address $68 (interupt 2) into memory pointed to by a1+2

moveq	#100,d0          ; move 100 into d0

moveq	#1,d1            ; move 1 into d1
swap	d1               ; swap words in d1, value is now $10000 
move.l	$4,a6            ; move value (ExecBase of exec.library) in address $4 into a6
jsr	-198(a6)         ; Jump to subroutine AllocMem in exec.library, d0 = AllocMem(d0, d1), allocate 100 bytes with type of memory MEMF_CLEAR.

move.l	d0,a1            ; put address of allocated memory stored in d0 into a1
move.l	d0,d7            ; put address of allocated memory stored in d0 into d7

lea.l	interrupt,a0     ; move address of interrupt into a0
moveq	#24,d0           ; set d0 to 24. Use d0 as a copyloop counter

copyloop:
move.l	(a0)+,(a1)+      ; copy value pointed to by a0 into address pointed to by a1. Increment both with 4 bytes (1 long word)
dbra	d0,copyloop      ; if d0 > -1 goto copyloop

move.w	#$4000,$dff09a   ; INTENA Interupt enable bits - disable all interrupts
move.l	d7,$68           ; move value in d7 that points to our allocated memory into $68 (interupt 2)
move.w	#$c000,$dff09a   ; INTENA Interupt enable bits - enable all interrupts

rts                      ; return from subroutine (main program)

interrupt:          ; begin interrupt handler routine
move.l	d0,-(a7)    ; push value in d0 onto the stack
move.b	$bfec01,d0  ; read a byte from CIAA serial data register connected to keyboard into d0
not.b	d0          ; negate a byte in d0
ror.b	#1,d0       ; rotate right 1 bit

cmp.b	#$59,d0     ; compare F10 key value with d0
bne.s	wrongkey    ; if not F10 pressed - goto wrongkey

bchg	#1,$bfe001  ; Test bit and change. Bit 1 is power LED

wrongkey:
move.l	(a7)+,d0    ; pop the stack and put value into d0 - reestablish d0 to it's previous value

jump:
jmp	$0	    ; the jump was previously set to the value in address $68 (interrupt 2) so this interupt function is linked together with the previous one

The interrupt program itself is only 32 bytes, so the 100 bytes we allocated is more than enough. Here is the hex dump from Seka.

iterrupt program

In WinUAE, the power LED toggle can be seen in the buttom status bar, were the test “Power” will toggle between black and gray text, each time F10 is pressed.

Note, if you run the mc0901 program twice, the power LED will stop toggling. That’s because the program daisy chains the handlers, so the new keyboard handler of the first run, will be called after the new keyboard handler of the second run. Effectively, they will both toggle the power LED, and cancel each other.

Some final thoughts

After publishing this post, I recieved some valuable feedback that I wish to highlight here.

On the face of it, the code looks a little strange for a keyboard handler. There is a jump to $\$0$ in the end, and there is also no handshake with the keyboard.

Our keyboard interrupt handler cannot stand alone, since there is no handshake. All in all, it’s very hackish 😮.

Regarding that jump to $\$0$ at the end of the code. It might look like the code jumps to $\$0$, but that’s not the case, because the program rewrites itself in the first two lines:

lea.l	jump,a1   ; move address of jump into a1
move.l	$68,2(a1) ; move value in address $68 into memory pointed to by a1+2

When the program counter reaches the last line of the code, The $\$0$ would have been overwritten by the program itself, with the value in address $\$68$.

jump:
jmp	$0

The address $\$68$ is an entry to interrupt 2 in the exception vector table. This jump, daisy chains our interrupt handler with the system keyboard interrupt handler. Our handler is just a relay that toggles the power LED and delegates everything else, including the handshake, to the system keyboard handler.

After the program rewrites itsef, it copies part of the program to an allocated space of memory, which is then hooked into the exception vector table.

At first, I thought that this was an interesting technique, and it surely is, but it’s very problematic. Here’s a quote from Mike Morton written in BYTE magazine, September 1986.

Self-modifying code is especially bad for 68000 programs that may someday run on the 68020, because the 68020’s instruction cache normally assumes that code is pure.

The 68020 introduced an L1 cache of 256 bytes, and I can imagine that any program that relies on self-modification, will risk running into big problems with memory and cache going out of sync.

The code example might not be the best, but I think the course authors choose to keep things simple and a little hackish. The code example is very simple to follow, and it would, without a doubt, have given a young reader a feeling of success, when the power LED turned on and off with each press on F10 👍.


Amiga Machine Code Course

Previous post: Amiga Machine Code Letter VIII - Wavetable Synthesis.

Next post: Amiga Machine Code Letter X - Memory.

Mark Wrobel
Mark Wrobel
Team Lead, developer and mortgage expert