Adding support for interrupts in your programs
![Picture](/uploads/6/0/2/5/6025013/3336831.jpg)
When I got back to CPC development in 2008, I wondered at the time how could I achieve to create a slow graphical effect (taking several VBLs) without caring about the music needing to be played 50 times per second. My first attempt was to split the effect along all the VBLs but it quickly became quite complex to maintain.
The solution of course for this problem was to use interrupts.
The solution of course for this problem was to use interrupts.
Download
The source of this article can be directly downloaded here :
![](http://www.weebly.com/weebly/images/file_icons/file.png)
interrupts.asm | |
File Size: | 1 kb |
File Type: | asm |
Disclaimer
I'm still learning. In none of the cases I consider myself as an expert wih low-level or system programming on the Amstrad CPC. If you encounter mistakes in this article, please report them to me I will be glad to fix it.
While this topic is actually a complex one, the aim is to keep it as accessible as possible to everyone. I won't enter details but go instead directly to the info.
While this topic is actually a complex one, the aim is to keep it as accessible as possible to everyone. I won't enter details but go instead directly to the info.
What are interrupts ?
When Z80 execute code, it basically treats evaluate current instruction, execute it, then go to next line of the program, evalute current instruction, execute it,... and does that indefinitively.
Z80 has a mechanism to temporarily pause the "go to next line of the program" process. Instead of going next line, it can actually go to a previously-specified line of the program. Once that secondary block of lines to be executed is treated, then the Z80 will execute back the program from interrupted position and execution will continue as if nothing actually occured. That secondary block of lines being executed is called an interrupt.
Z80 has a mechanism to temporarily pause the "go to next line of the program" process. Instead of going next line, it can actually go to a previously-specified line of the program. Once that secondary block of lines to be executed is treated, then the Z80 will execute back the program from interrupted position and execution will continue as if nothing actually occured. That secondary block of lines being executed is called an interrupt.
How interrupts works on Amstrad CPC
On Amstrad CPC, an interrupt occurs 300 times per second (allowing 6 interrupts per VBL).
By default, interrupts are enabled. 300 times per second, the Z80 will internally execute the following instructions :
di
call &38
When Amstrad CPC is started up, we see that at &38 adress there is this instruction :
jp &B941
&B941 adress is part of Amstrad CPC firmware code, that will do internal management of the CPC.
To implement our own interrupts, the idea is to redirect the jump from &B941 adress to our own code !
Really, it's as simple as that :
di
ld hl, interrupt_callback
ld ( &39 ), hl
ei
jp $ ; infinite loop
interrupt_callback:
ei
ret
The function interrupt_callback will get called 6 times per VBL.
By default, interrupts are enabled. 300 times per second, the Z80 will internally execute the following instructions :
di
call &38
When Amstrad CPC is started up, we see that at &38 adress there is this instruction :
jp &B941
&B941 adress is part of Amstrad CPC firmware code, that will do internal management of the CPC.
To implement our own interrupts, the idea is to redirect the jump from &B941 adress to our own code !
Really, it's as simple as that :
di
ld hl, interrupt_callback
ld ( &39 ), hl
ei
jp $ ; infinite loop
interrupt_callback:
ei
ret
The function interrupt_callback will get called 6 times per VBL.
Digging around the interrupt : disabling/enabling interrupts
di/ei instructions are used to disable/enable interrupts. If an interrupt has to be executed but the Z80 is currently inside a di/ei block, no wonders : the interrupt's execution will be delayed till the ei instruction will be executed.
Digging around the interrupt : preserving state
As mentioned before, to execute an interrupt, the Z80 will actually pause currently executed code, then execute the interrupt, then get the program back on track. Unfortunately, if interrupt's code change values of registers, when leaving the interrupt the registers will be kept as is and that will probably make the program crashes once getting back to interrupted code.
A possible solution for that is to keep registers values using stack (don't forget secondary register set too) :
interrupt_callback:
push af
push bc
push de
push hl
push ix
push iy
exx
ex af, af'
push af
push bc
push de
push hl
; here we do custom code..
pop hl
pop de
pop bc
pop af
ex af, af'
exx
pop iy
pop ix
pop hl
pop de
pop bc
pop af
ei
ret
A possible solution for that is to keep registers values using stack (don't forget secondary register set too) :
interrupt_callback:
push af
push bc
push de
push hl
push ix
push iy
exx
ex af, af'
push af
push bc
push de
push hl
; here we do custom code..
pop hl
pop de
pop bc
pop af
ex af, af'
exx
pop iy
pop ix
pop hl
pop de
pop bc
pop af
ei
ret
Digging around the interrupt : stack considerations
Be aware in previous example values are stored on stack ; but also, the "internal call" from Z80 to interrupt will make the return adress being pushed to stack (keep in mind that RET is equals to POP HL/JP (HL) - with "HL" preserved).
Basically, if your main program being interrupted also makes use of stack it could lead to a conflict. If your program really needs stack usage, simply use DI/EI to disable/enable interrupts for this part of code (which has to try to be as small as possible).
That said, because of these considerations about stack usage, it's generally a good idea to let the interrupt code runs its own stack. Example :
interrupt_callback:
ld ( previous_stack + 1 ), sp
ld sp, interrupt_stack_start
; the pushes..
; here we do custom code..
; the pops..
previous_stack:
ld sp, 0
ei
ret
interrupt_stack:
ds 256
interrupt_stack_start:
Basically, if your main program being interrupted also makes use of stack it could lead to a conflict. If your program really needs stack usage, simply use DI/EI to disable/enable interrupts for this part of code (which has to try to be as small as possible).
That said, because of these considerations about stack usage, it's generally a good idea to let the interrupt code runs its own stack. Example :
interrupt_callback:
ld ( previous_stack + 1 ), sp
ld sp, interrupt_stack_start
; the pushes..
; here we do custom code..
; the pops..
previous_stack:
ld sp, 0
ei
ret
interrupt_stack:
ds 256
interrupt_stack_start:
Digging around the interrupt : managing 6 interrupts
It's nice to get code called at 300Hz, but it could be more practical to use 50Hz-based code (for a music player, by example). For that, the trick is to implement 6 distincts interrupt code. When the main interrupt code will be called, we will actually increment a small counter, that will be used to jump to the right interrupt code to be executed. That counter will be reseted to zero each 6 times.
This is a possible implementation for that, our different interrupt implementation will actually change background/border colors :
interrupt_callback:
; push state..
ld a, ( interrupt_index )
inc a
cp 6
jp nz, no_interrupt_reset
xor a
no_interrupt_reset:
ld ( interrupt_index ), a
add a, a
ld c, a
ld b, 0
ld hl, interrupts
add hl, bc
ld a, ( hl )
inc hl
ld h, ( hl )
ld l, a
ld ( interrupt_call + 1 ), hl
interrupt_call:
call 0
; pop state..
ei
ret
interrupt_index:
db 0
interrupts:
dw interrupt_0
dw interrupt_1
dw interrupt_2
dw interrupt_3
dw interrupt_4
dw interrupt_5
COLOR0 equ &54 ; color BASIC 0
COLOR1 equ &44 ; color BASIC 1
COLOR2 equ &55 ; color BASIC 2
COLOR3 equ &5C ; color BASIC 3
COLOR4 equ &58 ; color BASIC 4
COLOR5 equ &5D ; color BASIC 5
setColor:
ld bc, &7f00
out (c), c
out (c), a
ld bc, &7f10
out (c), c
out (c), a
ret
interrupt_0:
ld a, COLOR0
jp setColor
interrupt_1:
ld a, COLOR1
jp setColor
interrupt_2:
ld a, COLOR2
jp setColor
interrupt_3:
ld a, COLOR3
jp setColor
interrupt_4:
ld a, COLOR4
jp setColor
interrupt_5:
ld a, COLOR5
jp setColor
This is a possible implementation for that, our different interrupt implementation will actually change background/border colors :
interrupt_callback:
; push state..
ld a, ( interrupt_index )
inc a
cp 6
jp nz, no_interrupt_reset
xor a
no_interrupt_reset:
ld ( interrupt_index ), a
add a, a
ld c, a
ld b, 0
ld hl, interrupts
add hl, bc
ld a, ( hl )
inc hl
ld h, ( hl )
ld l, a
ld ( interrupt_call + 1 ), hl
interrupt_call:
call 0
; pop state..
ei
ret
interrupt_index:
db 0
interrupts:
dw interrupt_0
dw interrupt_1
dw interrupt_2
dw interrupt_3
dw interrupt_4
dw interrupt_5
COLOR0 equ &54 ; color BASIC 0
COLOR1 equ &44 ; color BASIC 1
COLOR2 equ &55 ; color BASIC 2
COLOR3 equ &5C ; color BASIC 3
COLOR4 equ &58 ; color BASIC 4
COLOR5 equ &5D ; color BASIC 5
setColor:
ld bc, &7f00
out (c), c
out (c), a
ld bc, &7f10
out (c), c
out (c), a
ret
interrupt_0:
ld a, COLOR0
jp setColor
interrupt_1:
ld a, COLOR1
jp setColor
interrupt_2:
ld a, COLOR2
jp setColor
interrupt_3:
ld a, COLOR3
jp setColor
interrupt_4:
ld a, COLOR4
jp setColor
interrupt_5:
ld a, COLOR5
jp setColor
Digging around the interrupt : correct setup
As a reminder, we setup the interrupts like that :
di
ld hl, interrupt_callback
ld ( &39 ), hl
ei
This tells that our first interrupt will be starting from next 300Hz interrupt code. It's more convenient from a developer perspective to always consider interrupt_0 to be called as first interrupt, interrupt_1 as second one, etc.
For that, I added to the main interrupt code a small "VBL" timer to make sure interrupt_0 is always executed at right position. This can be accomplished like that :
ld b,#f5
antiVBL:
in a,(c)
rra
jr c,antiVBL
VBL:
in a,(c)
rra
jr nc,VBL
ld b, &f5
in a, ( c )
rrca
jr nc, skipInitFirst
; here this is 1st interrupt of VBL
skipInitFirst:
; this is not 1st interrupt of VBL
di
ld hl, interrupt_callback
ld ( &39 ), hl
ei
This tells that our first interrupt will be starting from next 300Hz interrupt code. It's more convenient from a developer perspective to always consider interrupt_0 to be called as first interrupt, interrupt_1 as second one, etc.
For that, I added to the main interrupt code a small "VBL" timer to make sure interrupt_0 is always executed at right position. This can be accomplished like that :
ld b,#f5
antiVBL:
in a,(c)
rra
jr c,antiVBL
VBL:
in a,(c)
rra
jr nc,VBL
ld b, &f5
in a, ( c )
rrca
jr nc, skipInitFirst
; here this is 1st interrupt of VBL
skipInitFirst:
; this is not 1st interrupt of VBL
Wrapping up everything : the source
The source is Maxam-compatible, you can directly copy/paste this code in WinAPE emulator if you want (this source is also available as a file at the beginning of this article) :
nolist
org &4000
call install_interrupts
jp $
ret
install_interrupts:
di
ld hl, interrupt_callback
ld ( &39 ), hl
ei
ret
interrupt_notReady equ -2
interrupt_firstValue equ -1
interrupt_callback:
ld ( interrupt_previous_stack ), sp
ld sp, interrupt_stack_start
push af
push bc
push de
push hl
push ix
push iy
exx
ex af, af'
push af
push bc
push de
push hl
ld b, &f5
in a, ( c )
rrca
jr nc, skipInitFirst
ld a, interrupt_firstValue
ld ( interrupt_index ), a
skipInitFirst:
ld a, ( interrupt_index )
cp interrupt_notReady
jp z, skipInterrupt
inc a
cp 6
jp nz, no_interrupt_reset
xor a
no_interrupt_reset:
ld ( interrupt_index ), a
add a, a
ld c, a
ld b, 0
ld hl, interrupts
add hl, bc
ld a, ( hl )
inc hl
ld h, ( hl )
ld l, a
ld ( interrupt_call + 1 ), hl
interrupt_call:
call 0
skipInterrupt:
pop hl
pop de
pop bc
pop af
ex af, af'
exx
pop iy
pop ix
pop hl
pop de
pop bc
pop af
ld sp, ( interrupt_previous_stack )
ei
ret
interrupt_previous_stack:
dw 0
interrupt_stack:
ds 256
interrupt_stack_start:
interrupt_index:
db interrupt_notReady
interrupts:
dw interrupt_0
dw interrupt_1
dw interrupt_2
dw interrupt_3
dw interrupt_4
dw interrupt_5
COLOR0 equ &54
COLOR1 equ &44
COLOR2 equ &55
COLOR3 equ &5C
COLOR4 equ &58
COLOR5 equ &5D
COLOR6 equ &4C
setColor:
ld bc, &7f00
out (c), c
out (c), a
ld bc, &7f10
out (c), c
out (c), a
ret
interrupt_0:
ld a, COLOR0
jp setColor
interrupt_1:
ld a, COLOR1
jp setColor
interrupt_2:
ld a, COLOR2
jp setColor
interrupt_3:
ld a, COLOR3
jp setColor
interrupt_4:
ld a, COLOR4
jp setColor
interrupt_5:
ld a, COLOR5
jp setColor
nolist
org &4000
call install_interrupts
jp $
ret
install_interrupts:
di
ld hl, interrupt_callback
ld ( &39 ), hl
ei
ret
interrupt_notReady equ -2
interrupt_firstValue equ -1
interrupt_callback:
ld ( interrupt_previous_stack ), sp
ld sp, interrupt_stack_start
push af
push bc
push de
push hl
push ix
push iy
exx
ex af, af'
push af
push bc
push de
push hl
ld b, &f5
in a, ( c )
rrca
jr nc, skipInitFirst
ld a, interrupt_firstValue
ld ( interrupt_index ), a
skipInitFirst:
ld a, ( interrupt_index )
cp interrupt_notReady
jp z, skipInterrupt
inc a
cp 6
jp nz, no_interrupt_reset
xor a
no_interrupt_reset:
ld ( interrupt_index ), a
add a, a
ld c, a
ld b, 0
ld hl, interrupts
add hl, bc
ld a, ( hl )
inc hl
ld h, ( hl )
ld l, a
ld ( interrupt_call + 1 ), hl
interrupt_call:
call 0
skipInterrupt:
pop hl
pop de
pop bc
pop af
ex af, af'
exx
pop iy
pop ix
pop hl
pop de
pop bc
pop af
ld sp, ( interrupt_previous_stack )
ei
ret
interrupt_previous_stack:
dw 0
interrupt_stack:
ds 256
interrupt_stack_start:
interrupt_index:
db interrupt_notReady
interrupts:
dw interrupt_0
dw interrupt_1
dw interrupt_2
dw interrupt_3
dw interrupt_4
dw interrupt_5
COLOR0 equ &54
COLOR1 equ &44
COLOR2 equ &55
COLOR3 equ &5C
COLOR4 equ &58
COLOR5 equ &5D
COLOR6 equ &4C
setColor:
ld bc, &7f00
out (c), c
out (c), a
ld bc, &7f10
out (c), c
out (c), a
ret
interrupt_0:
ld a, COLOR0
jp setColor
interrupt_1:
ld a, COLOR1
jp setColor
interrupt_2:
ld a, COLOR2
jp setColor
interrupt_3:
ld a, COLOR3
jp setColor
interrupt_4:
ld a, COLOR4
jp setColor
interrupt_5:
ld a, COLOR5
jp setColor
Conclusion
This is the end of this very technical article. There are still rooms for improvements (especially the setup thing to "sync" with the VBL) but the aim of this article was to present you the basics. With that, you are now able to use interrupts in your own programs the easy way !