Using interrupts on Amstrad CPC with z88dk

Amstrad CPC and NC systems
Post Reply
cpitrat
New member
Posts: 1
Joined: Sat Nov 12, 2022 1:37 pm

Using interrupts on Amstrad CPC with z88dk

Post by cpitrat »

Hello,

I'm trying to write a program where I use interrupts using z88dk.

The idea would be to inject my interrupt callback, do whatever process I want to do (typically changing palette, switching mode, synchronizing things...) and then call the normal interrupt handler.

Reason: this is a very convenient way to handle multiple things, like playing some music while drawing something on the screen, having split screens with different modes/palettes, ...

In a "naked" environment, this would look something like that:

Code: Select all

 di
 // Save original interrupt callback
 ld hl, (0x39)
 ld original_interrupt_callback, hl
 // Replace by my own interrupt callback
 ld hl, interrupt_callback
 ld (0x39), hl
 ei
And then the code of interrupt_callback just calls original_interrupt_callback at the end.

However this doesn't work with z88dk because it's using exactly the same trick.

Looking at how it's done, I couldn't find a good way to inject my callback. There doesn't seem to be a hook planned for that.

I managed to get something that seem to work fine by exporting __fw_int_address__ in lib/target/cpc/classic/cpc_crt0.asm

I then setup my interrupts with code like this:

Code: Select all

void setup_interrupt()                                                                                                                                                                                                          
{                                                                                                                                                                                                                               
  #asm
  di
  // Save original interrupt handler address
  ld hl, (__fw_int_address__)
  ld (_original_int_address), hl
  // Add our own handler
  ld hl, _interrupt_callback
  ld (__fw_int_address__), hl
                                                                                                                                                                                                                                
  ei                                                                                                                                                                                                                            
  #endasm                                                                                                                                                                                                                       
}   
But I'd like to be able to do this kind of thing without having to modify z88dk library!

What do you think? Is there a way to do it? If not, would it make sense to introduce one?
User avatar
dom
Well known member
Posts: 2072
Joined: Sun Jul 15, 2007 10:01 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by dom »

I suspect the best thing to do, will be to make a change to the crt0 so that interposer_isr looks something like this:

Code: Select all


   EXTERN im1_vectors
   EXTENR asm_interrupt_handler
   
__interposer_isr__:

   call cpc_enable_fw_exx_set
   call 0x0038
   call cpc_enable_process_exx_set
   push    af
   push    hl
   ld      hl,im1_vectors
   call    asm_interrupt_handler
   pop     hl
   pop     af
   ei
   ret
   
   GLOBAL im1_init
   GLOBAL _im1_init
  im1_init:
  _im1_init:
    ret
 
And then you should be able to call add_raster_int() as per other targets and add/remove your own routines from the interrupt.

If you get it working, then a PR would be appreciated.
Timmy
Well known member
Posts: 392
Joined: Sat Mar 10, 2012 4:18 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by Timmy »

I was trying out CPC programming so I wrote my very first program.

This program also tries to hijack interposer_isr by hooking up my own isr routine. I also just realised it's probably better if I pushed all the registers I used in my interrupt routine.

It's not the best program and it uses too many magic numbers, so this might not work for you. (I've also only tested on 2.2 not on the nightly.)

Anyway, my suggestion is probably that if you add 1 line in interloper_isr, that might be enough. If you know how to add this asm_interrupt_handler, that is. (I don't know.) And you have to make sure that your own isr saves and restores the registers you are using.

But if you don't want to change this, it's probably fine too, because both ways seem to be unsatisfactory.

Code: Select all

__interposer_isr__:

   call cpc_enable_fw_exx_set
   call 0x0038
   di
   call cpc_enable_process_exx_set
   call asm_interrupt_handler   <-- new line
   ei
   ret

Code: Select all

// test 20221114 Timmy
// My first CPC program, so it's really bad :)

// I've built this with:
// zcc +cpc -lndos -lm -subtype=dsk -create-app -o 1 cpctest1.c

// run this in emulator with:
// run"1.cpc"

#include <stdio.h>

extern unsigned int fw_addr_ptr    @ 0x126a; // = 6122  =0x17ea
extern unsigned int isr_routine    @ 0x0039; // = 4741

char flip = 0x44;

void __FASTCALL__  isr_routine1() __naked
{
	#asm
	ld a, (_flip)
	xor 0x18
	ld (_flip), a
	ld bc, 0x7f00
	out (c), c
	out (c), a
	ld bc, 0x7f10
	out (c), c
	out (c), a
	ret
	#endasm
}

void __FASTCALL__ isr_routine_main() __naked
{
	#asm
	call 0x1240
	call 0x0038
	di
	call 0x1260
	call _isr_routine1
	di
	ld hl, _isr_routine_main
	ld (0x0039), hl
	ei
	ret
	#endasm
}

void setup_interrupts()
{
	#asm
	ld hl, _isr_routine_main
	ld (0x0039), hl
	#endasm
}

void main()
{
	printf("Hello World! %u\n", fw_addr_ptr);
	printf("isr_routine: %u\n", isr_routine);
	setup_interrupts();
	while (1);
}
Timmy
Well known member
Posts: 392
Joined: Sat Mar 10, 2012 4:18 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by Timmy »

Just a quick post here to say I've been playing with the Amstrad part of z88dk lately. Haven't got much to say yet, I hope to do a bit more writing about this later this week/next week.
Timmy
Well known member
Posts: 392
Joined: Sat Mar 10, 2012 4:18 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by Timmy »

Ok, so here is my CPC WYZ Player. It finally works now. :)

Code: Select all

// wyzplayer on the CPC 20230306 Timmy
// My second released CPC program :)

// I've built this with:
// zcc +cpc wyzcpc.c -o wyz.cpc.dsk quezesto.mus.asm -lndos -subtype=dsk -create-app

// run this in emulator with:
// run"wyz.cpc" (after loading it first)

// this program also requires the files quezesto.mus and quezesto.mus.asm from the wyzplayer demo songs

#include <stdio.h>
#include <intrinsic.h>
#include <interrupt.h>

#include <cpc.h>

#include <psg/wyz.h>
#include <stdlib.h>


extern wyz_song mysong;

char flip = 0x44;

void __FASTCALL__  isr_routine1() __naked
{
	#asm
	push af
	push bc
	ld a, (_flip)
	xor 0x18
	ld (_flip), a
	ld bc, 0x7f00
	out (c), c
	out (c), a
	ld bc, 0x7f10
	out (c), c
	out (c), a
	pop bc
	pop af
	ret
	#endasm
}

uchar wyz_60counter;

void __FASTCALL__ cpc_wyz_play() __naked
{
	#asm
	push af
	push bc
	push de
	push hl
	push ix
	push iy
	ld a, (_wyz_60counter)
	and a
	jr nz, m60cont
m60play:
	call ay_wyz_play
	ld a, 6
m60cont:
	dec a
	ld (_wyz_60counter), a
	pop iy
	pop ix
	pop hl
	pop de
	pop bc
	pop af
	ret
	#endasm
}

void __FASTCALL__ isr_routine_main() __naked
{
	#asm
	di
	call 0x1240
	call 0x0038
	di
	call 0x1260
	call _isr_routine1
	call _cpc_wyz_play
	push hl
	ld hl, _isr_routine_main
	ld (0x0039), hl
	pop hl
	ei
	ret
	#endasm
}

void setup_interrupts()
{
	#asm
	ld hl, _isr_routine_main
	ld (0x0039), hl
	#endasm
}

void main()
{
   uchar i;
   printf("%cWYZ Tracker example 2\n",12);
   
   // Load the tracker file
   ay_wyz_init(&mysong);
   // Play song 1 within  the  file
   ay_wyz_start(0);

   // Setup PSG  
   psg_init();
   // Setup interrupt
   setup_interrupts();

   // Just loop
   while  ( 1 ) {
	   msleep(200);
	   printf(".");
	   setup_interrupts();	   
   }
}
I had some problems with printf() interfering the interrupt. In the end (this afternoon) I found out the problem is that printf() took longer than an interrupt. So interrupt has to be setup again after a printf(). So I can finally declare this player working.
User avatar
dom
Well known member
Posts: 2072
Joined: Sun Jul 15, 2007 10:01 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by dom »

Thanks Timmy, that's given me a nudge to get it working.

The issue I've just fixed is that the crt restores the firmware interrupt handler when calling the firmware. Calling getk() in a tight loop means that the interrupt handler is only called roughly 1/6 of the time.

If you don't call getk() then the music ran far too fast! So adding in the 6 counter, and sleeping for a bit in the loop seems to make things work in both cases.

Commit: https://github.com/z88dk/z88dk/commit/5 ... a95d681724

Edit: I've added a driver for VortexTracker as well so that's working too.
Timmy
Well known member
Posts: 392
Joined: Sat Mar 10, 2012 4:18 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by Timmy »

Thanks for figuring out the problem. I would never have guessed that printf() would intercept firmware calls.

Just to explain a bit better: Interrupts on the CPC works at 300Hz. A song made for 50Hz needs to wait 5 times before it plays again.

Also what I understand from talking on WoS (link: https://worldofspectrum.org/forums/disc ... mstrad-cpc) is that most games on the CPC are not using the firmware, so I'll probably just skip the functions that do firmware calls while making games, at least. (The firmware is probably still useful for regular use.) (Not that I have any plans for making CPC games right now.)

This is such an important change (along with all those tracker updates), it might mean I have to update to the nightly now. ;)
Timmy
Well known member
Posts: 392
Joined: Sat Mar 10, 2012 4:18 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by Timmy »

Important Note: When I meant with "wait 5 times" in my previous post, I meant that a 50Hz song should only be played once every 6 interrupts.

Another question I just thought about: Is it possible now to call the functions you mentioned earlier, "cpc_enable_fw_exx_set" and "cpc_enable_process_exx_set" directly from inline assembly, and not like me calling hardcoded addresses 0x1240 and 0x1260 like I did in my wyzplayer above?
Timmy
Well known member
Posts: 392
Joined: Sat Mar 10, 2012 4:18 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by Timmy »

So it seems I was just too late to edit my previous posts...

I see you've added the "asm_interrupt_handler" there.

1) I have no idea how to use it, and
2) I wonder if it is possible to set up interrupt again in case some other random function interferes with the handler.
3) I would also like to say that I don't like the idea that the exposed "asm_interrupt_handler" is a 50Hz interrupt instead of a 300Hz one. On the CPC, the 300Hz is useful, and for example, can be used to change colours between different screen areas.
User avatar
dom
Well known member
Posts: 2072
Joined: Sun Jul 15, 2007 10:01 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by dom »

I see you've added the "asm_interrupt_handler" there.
I added it months ago when this topic first came up . Tasks are added to the 50Hz interrupt using add_raster_int() as per every other target (I hate the name, but it's stuck now) so there's no need for special code - again the wyz and vt2 examples show how.
On the CPC, the 300Hz is useful, and for example
I agree, I was going to add a handler there as well, but I couldn't think of an appropriate name that wasn't add_300Hz_isr(). On +sam, there's a true line interrupt which is hooked as detailed here: https://github.com/z88dk/z88dk/wiki/Pla ... interrupts - can you think of an appropriate name?
Another question I just thought about: Is it possible now to call the functions you mentioned earlier, "cpc_enable_fw_exx_set" and "cpc_enable_process_exx_set" directly from inline assembly, and not like me calling hardcoded addresses 0x1240 and 0x1260 like I did in my wyzplayer above?
See here: https://github.com/z88dk/z88dk/wiki/Pla ... e-firmware

I must say how I (personally) absolutely loathe working on the CPC. 'Use the firmware - it does everything' was what I was told years ago. Unfortunately using the firmware effectively turns the machine into an 8080 with ix and iy and means you need to have workarounds like this all around the place.

It would be good to write an inkey keyboard and joystick drivers and then we could be optionally free of the madness
Timmy
Well known member
Posts: 392
Joined: Sat Mar 10, 2012 4:18 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by Timmy »

dom wrote: Tue Mar 07, 2023 8:23 am It would be good to write an inkey keyboard and joystick drivers and then we could be optionally free of the madness
Is there anything specific you need for this?

Here's an example code for one row. I might have time to write the other rows later.
(Also prinf("%d", c) wasn't working here so the lines a re a bit more complex for now.)

Code: Select all

// test 20230307 Timmy

// I've built this with:
// zcc +cpc -lndos -lm -subtype=dsk -create-app -zorg=16384 -o 3 cpc-3.c

#include <cpc.h>
#include <stdio.h>
#include <strings.h>

int __FASTCALL__ getkeyrow(int hl) __naked
// function based on cpc_TestKeyboard.asm in a repository
{
	#asm
	ld a, l ; retrieve parameter
	
	di
	ld bc, $F782
	out (c), 0
	
	ld bc, $F40E
	out (c), c
	
	ld bc, $F6C0
	out (c), c
	
	out (c), 0
	
	ld bc, $F792
	out (c), c
	
	ld c, a
	ld b, $F6
	out (c), c
	
	ld b, $F4
	in a, (c)
	
	ld bc, $F782
	out (c), c
	dec b
	out (c), 0	

	cpl
	ld l, a
	ld h, 0
	ei
	ret
	#endasm
}

main(){
		uchar c;
		uchar *p;

	   cpc_DisableFirmware();  // optional

       cpc_SetMode(1);         // optional

	   while (1)
	   {
		   c = getkeyrow(0x44);

			if (c==1) printf("0");
			if (c==2) printf("9");
			if (c==4) printf("O");
			if (c==8) printf("I");
			if (c==16) printf("L");
			if (c==32) printf("K");
			if (c==64) printf("M");
			if (c==128) printf(",");
		   
		   
		   // a delay routine
			#asm
				ei
				ld b,12
				.ko
				halt
				djnz ko
				di
			#endasm
	   }
	   
}

User avatar
dom
Well known member
Posts: 2072
Joined: Sun Jul 15, 2007 10:01 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by dom »

Timmy wrote: Tue Mar 07, 2023 3:31 pmIs there anything specific you need for this?
As usual a nudge :)

I've added support for --hardware-keyboard with this commit: https://github.com/z88dk/z88dk/commit/b ... 64cb68096a

I still need to update the joystick handling so it's not dependent on the firmware and add a pragma to the crt to stop the interrupt handler calling into the firmware.
Timmy
Well known member
Posts: 392
Joined: Sat Mar 10, 2012 4:18 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by Timmy »

dom wrote: Tue Mar 07, 2023 9:57 pm I've added support for --hardware-keyboard with this commit: https://github.com/z88dk/z88dk/commit/b ... 64cb68096a

I still need to update the joystick handling so it's not dependent on the firmware and add a pragma to the crt to stop the interrupt handler calling into the firmware.
Thanks!

Don't worry about the joystick too much. If I read correctly, the joystick 1 on the CPC is just keyboard scan row 0x49, and joystick 2 is row 0x46. You basically already wrote it. :)

One small thing. These input routines reads from the PSG (and thats why the PSG is really annoying), so if you are using interrupts to play music, I'd suggest to listen to keys and joystick on one of the six interrupts per frame (and the music on another). I doubt (but unsure) if they can be mixed together. But that's not your concern, I guess. That's a concern for the game coders when they make a combined interrupt. For normal applications with no music, that's not a problem, I guess.

That's what the original poster had asked, as well.
User avatar
dom
Well known member
Posts: 2072
Joined: Sun Jul 15, 2007 10:01 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by dom »

Timmy wrote: Tue Mar 07, 2023 10:43 pmOne small thing. These input routines reads from the PSG (and thats why the PSG is really annoying), so if you are using interrupts to play music, I'd suggest to listen to keys and joystick on one of the six interrupts per frame (and the music on another). I doubt (but unsure) if they can be mixed together.
It's on register 14 of the AY so in theory it should be ok? I've run out of time for this evening, but I'll try with the tracker examples tomorrow to see if "user land" reading the keyboard interferes with the interrupt playing music.
User avatar
dom
Well known member
Posts: 2072
Joined: Sun Jul 15, 2007 10:01 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by dom »

Just for reference, there's no interaction between reading the keyboard and a tracker playing on the interrupt.

The joystick() function is now updated and doesn't use the firmware at all. As an added bonus the keyboard joysticks support multiple directions.

I've updated the CPC wiki page (https://github.com/z88dk/z88dk/wiki/Pla ... mstrad-CPC) with details of how to disable the firmware interrupt which Is probably recommended when writing something that takes over the machine. I've also added a cpc_add_fast_isr() which registers a 300Hz isr (naming inspired from the actual CPC terminology).

For cooperation with the firmware, I'd actually like to change the approach. It is possible to register ISRs with the firmware so it would make sense to advantage of that and remove the janky 6 counter. That way calling the firmware won't cause interrupts to be missed.
User avatar
dom
Well known member
Posts: 2072
Joined: Sun Jul 15, 2007 10:01 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by dom »

dom wrote: Wed Mar 08, 2023 7:22 pmFor cooperation with the firmware, I'd actually like to change the approach. It is possible to register ISRs with the firmware so it would make sense to advantage of that and remove the janky 6 counter.
I've now done just that so whichever mode you go for, vsync interrupts are fired at the right time.
That way calling the firmware won't cause interrupts to be missed.
Hahaha...that turns out to not be true - interrupts are missed even with the firmware interrupt handler being used.
Timmy
Well known member
Posts: 392
Joined: Sat Mar 10, 2012 4:18 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by Timmy »

dom wrote: Wed Mar 08, 2023 7:22 pm Just for reference, there's no interaction between reading the keyboard and a tracker playing on the interrupt.
So I've asked the people of WoS about this, and this is their answer (https://worldofspectrum.org/forums/disc ... nt_1004433):
But this isn't really a big deal. The Z80 is only ever doing one thing at a time, so you just set the PPI the way you want and do the necessary operations. The only time it could be an issue is if your main code is reading the keyboard and an interrupt routine is playing AY music (because there is a small chance the interrupt could occur when the PPI direction has been set, but before the keyboard is read) - all you need to do if that's the case is DI before scanning the keys and EI afterwards (or move key scanning into the interrupt routine if that works better for you).
(more info at the link)

So I guess, if I'm making a game, I'd do either:

1) I have an interrupt music routine at 50Hz -> I'll use a different interrupt to read inputs.
2) I'll do DI/EI before keyboard scanning.
User avatar
dom
Well known member
Posts: 2072
Joined: Sun Jul 15, 2007 10:01 pm

Re: Using interrupts on Amstrad CPC with z88dk

Post by dom »

That makes sense, I guess the di/ei we've got in the keyboard reading meant that any interaction wasn't observed.
Post Reply