Dave wrote: Mon Aug 18, 2025 7:23 pm
I created a wavetable sound card for the QL. Unfortunately, the absence of a fast interrupt timer on the BBQL means it's good for game sounds but no use for replaying MIDI files. Changes in CPU speed or any other code multitasking with it changes timing significantly for program-timed delays.
After careful thought and looking at how other systems accomplish this, I arrived at a workable solution. As I intend to add this to my 8301/8302 replacement logic, it seems only reasonable I publish my idea and get some feedback before implementing it.
My thought is to add a register in the $18000 area near the video registers. This register will default to 0b00000000.
Operation:
0b00000000 - off
0bNNNNNNNN (>0) - on
The binary number represented in the register is a divider of a 32,768 Hz clock. This allows the use of the generic clock oscillator circuit that will be present anyway.
00000001 = divide by one, or 32768 interrupts per second
00000010 = divide by two, or 16384 interrupts per second
00000011 = divide by three, or 8192 interrupts per second
.
.
11111110 = divide by 254, or 128 interrupts per second
11111111 = divide by 255, or 64 interrupts per second
Resetting the register to 00000000 turns off the interrupt.
Divided by 3 would be 10922.666...
Divided by 4 would be 8192
Divided by 5 would be 6553.6
etc...
The problem with such a system resource is, who gets do decide the correct divisor and what happens when it is changed and the internal division counter is in any given state - for instance if it is a count down type timer and someone changes the reset value (which is the divisor) from say 100 to 10, while it was at state 50, will it now count from 50 all the way to 255, loop back to 0 and then reset at the new value of 10?
System timers used for base timing are NEVER a 'public' resource, where any old job can change their frequency, rather they are always system only. That being said, any application specific hardware can implement their own timer, assuming there is an interrupt available to service it (although in theory it might be polled in a faster polling loop too...).
So this is where we get to interrupt latency... which is btw a potential problem with vectored interrupts, as vectors are still shared between the available interrupt levels, so multiple interrupting devices on the same level but with different vectors still need prioritizing. More on that later...
Any program wishing to use the programmable interrupt time can place its vector in the vector table and arrange handling code at the indexed address. In my case, this would be a MIDI player or sequencing software. You could have this flexible interrupt timer do anything for you. Any system that has this modification would also have SRAM mapped at $0000.
This does not work in a multitasking environment where multiple programs may want to use the timer. One taking over another without the other being aware of that would potentially result in instability. The way this is normally done, and in fact is under QDOS is that the (in this case auto-)vector is pointing to a linked list of interrupt service routines, which can be linked into by any program that wants to use the interrupt.
There is also the question of how exactly the interrupting hardware is serviced, which comes down to how the CPU implements interrupts. Again, more on that later. For now, the interrupt vector points to some code to properly address interrupt acknowledge, then jumps to the linked list of tasks the interrupt causes to be performed, which each are parts of code of other 'programs'.
From memory, new interrupt tasks are linked at the start of the list but since the list is in a known format any linking software can follow the list and link wherever. The order of course determines the priority of servicing.
And now back to the actual way interrupts are used on an original QL:
The 68008 has interrupt lines IPL02L and IPL1L (unless it's a FN 52-pin PLCC case version which has the usual 3, IPL0L, IPL1L, IPL2L).
It is important to remember that the interrupt level requested is ENCODED on the IPL lines and the CPU monitors how the code changes and depending on the interrupt mask, it can interrupt lower level interrupt code with higher level interrupt code.
Given the way the CPU pins are laid out, the possible interrupt codes are 0 (no interrupt), 2, 5 and 7 (non maskable interrupt), so altogether 3 levels, the top being non-maskable.
That being said, the way it connects on the motherboard, the only interrupt level that is usable is level 2, i.e. IPL1 being asserted on it's own. The actual interrupt 'controller' is the 8302 ULA, and it's single interrupt output is a result of several possible sources of interrupts, but in the end it connects to IPL1L. Astute readers will note that IPL1L as also connected to the IPC, pin P23, and in fact so is IPL02L, to pin P22, so in theory the IPC could request any available interrupt level. In THEORY.
So now one more important thing about 68k interrupts is that the CPU is level sensitive. What this means is, as long as a given IPL code is present, the CPU will attempt to service this interrupt. In other words, the interrupting hardware has to implement an interrupt acknowledge mechanism, implicitly or explicitly, which usually means a speciffic address has to be read or written to 'clear' the interrupt. The reason for this is simple - if a device needs service via interrupt, it keeps it's interrupt signal active as long as it is not serviced, in case other higher priority interrupts are being processed first, or code that must not be interrupted by that particular level of interrupt is being executed, so that level of interrupt is masked.
So imagine this scenario:
The IPC pulls low IPL02L, causing int 5 and then (since there is now ay for the 8302 to know the status of the IPL02L pin), the 8302 pulls IPL1L low, so now what was supposed to be int 5 with int 2 pending has just become int 7, which has interrupted the int 5 servicing routine 'somewhere', and the int 7 routine is now supposed to untangle who actually caused what level interrupt and emulate the proper response, i.e. this is actually a false non-maskable interrupt. What is worse, both int 5 and/or int 2 could have been masked because other important processing is being done, and all of a sudden you have a non-maskable interrupt, which WILL be processed.
Short version: only level 2 is usable as it stands now, DO NOT USE the IPC pins to cause an interrupt directly, or for that matter the IPL lines on the J1 bus, you are in for a world of hurt.
This could be fixed but it needs a proper interrupt level encoder, i.e. a bit of hardware with 3 inputs for int 2, 5, and 7 and 2 outputs, IPL02L and IPL1L. As it happens, this would be enough for almost any application - tie the 8302 to the int 2 input, which will amongst other things produce the basic 20ms polling interrupt, use a fast signal (2kHz or more) for devices that need to be serviced quickly, and also keep the int 7 input for what it's supposed to be used, like absolutely required stuff (more in a separate post) and debug.
The fast interrupt can also be used to implement a better resolution system timer, and with programs being able to use a linked list of tasks, each can actually divide the frequency of service as they need, again more on that in a separate post.
I even proposed some changes to the J1 bus along these lines - instead of exposing the IPL lines on the bus (which actually is NOT usable for the same reasons as explained above with the IPC scenario), re-use then as inputs to the interrupt level encoder.
One possibility would be:
INT 2 is available through the EXTINTL pin
IPL1L can become an int 5 request
IPL02L can become an int 7 request.
Till the next post...