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.
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 timer is highly configurable and independent of system speed. It will work the same regardless of CPU speed or other code multitasking.
With extensions, many interrupt vectors could be handled. I don't have a use for this, but if anyone can think of a use case where this would add utility I'll add it to my plan.
Nothing is written in stone, so if you have thoughts on this now is the time to drop a comment. Any feedback graciously received.
Hardware programmable timers
Re: Hardware programmable timers
Fairly sure. It creates jitter of around 20ms or "more" in the audio. The jitter decreases as clock speed increases. The 50 Hz interrupt is also not guaranteed. It is 60 Hz on NTSC machines.
I am using a 30 MHz QL that changes speed to 7.5 MHz when accessing QL IO. The lack of predictability creates enough audio jitter to be easily distinguishable by even an untrained ear.
It seems for existing machines the benefits are modest. For future hardware it does make sense to have a much faster and programmable interrupt timer.
I am using a 30 MHz QL that changes speed to 7.5 MHz when accessing QL IO. The lack of predictability creates enough audio jitter to be easily distinguishable by even an untrained ear.
It seems for existing machines the benefits are modest. For future hardware it does make sense to have a much faster and programmable interrupt timer.
Re: Hardware programmable timers
I just looked into this a bit more to double check myself. Applications insert their code into the lower priority scheduler routine. The imprecision of the timing here is a large part of what makes serial ports unpredictable over 9600 baud. As the sound card has MIDI (and can buffer it) this compounds the problems of the interrupt being low speed and also low precision depending on what hardware servicing happens at a higher priority.
In my little test just now I measured +/- 70ms of jitter worst case, and typically 30-35ms.
The upside of the slow interrupt is that there are 150,000 clock cycles (roundly 40 to 50K instructions depending on instruction mix) per time slice. This is of course split up to hardware service, the polling routine, scheduling and the remainder passed to applications based on priority. Some of this time the CPU will also be paused waiting to access a contended address while screen readout occurs.
I'm super comfy on the hardware side, but the software element of handling interrupts gives me night sweats! For example, the QL uses IPL0 and IPL2 but ignores IPL1 which reduces the interrupt levels considerably. Does this mean the OS ignores those interrupts, or just has vacant handlers for them? Can I add them or can they never be enabled?
I'd like to adopt something that can work flexibly not just for *my* use case, but that is openly documented and available for anyone to use.
In my little test just now I measured +/- 70ms of jitter worst case, and typically 30-35ms.
The upside of the slow interrupt is that there are 150,000 clock cycles (roundly 40 to 50K instructions depending on instruction mix) per time slice. This is of course split up to hardware service, the polling routine, scheduling and the remainder passed to applications based on priority. Some of this time the CPU will also be paused waiting to access a contended address while screen readout occurs.
I'm super comfy on the hardware side, but the software element of handling interrupts gives me night sweats! For example, the QL uses IPL0 and IPL2 but ignores IPL1 which reduces the interrupt levels considerably. Does this mean the OS ignores those interrupts, or just has vacant handlers for them? Can I add them or can they never be enabled?
I'd like to adopt something that can work flexibly not just for *my* use case, but that is openly documented and available for anyone to use.
Re: Hardware programmable timers
The CPU simply has no way of ever seeing an interrupt with IPL 1, 3, 4, or 6, because it lacks an IPL2 pin (rather, IPL2 is internally always equal to IPL0). The arrangements in the exception table are still there for compatibility reasons (and you could, in theory, also install handlers on them - with not much use), but a '08 will never receive them, so, such an interrupt can never be triggered.Dave wrote: Mon Aug 18, 2025 9:42 pm I just looked into this a bit more to double check myself. Applications insert their code into the lower priority scheduler routine. The imprecision of the timing here is a large part of what makes serial ports unpredictable over 9600 baud. As the sound card has MIDI (and can buffer it) this compounds the problems of the interrupt being low speed and also low precision depending on what hardware servicing happens at a higher priority.
In my little test just now I measured +/- 70ms of jitter worst case, and typically 30-35ms.
The upside of the slow interrupt is that there are 150,000 clock cycles (roundly 40 to 50K instructions depending on instruction mix) per time slice. This is of course split up to hardware service, the polling routine, scheduling and the remainder passed to applications based on priority. Some of this time the CPU will also be paused waiting to access a contended address while screen readout occurs.
I'm super comfy on the hardware side, but the software element of handling interrupts gives me night sweats! For example, the QL uses IPL0 and IPL2 but ignores IPL1 which reduces the interrupt levels considerably. Does this mean the OS ignores those interrupts, or just has vacant handlers for them? Can I add them or can they never be enabled?
I'd like to adopt something that can work flexibly not just for *my* use case, but that is openly documented and available for anyone to use.
By using the polling list rather than the scheduler loop, you should be able to reduce the jitter significantly. It's much more accurate than the scheduler loop, which has more like a "best effort" timing.
The original QL uses shared interrupts - All interrupts are IPL2, and the OS goes through a list if handlers to cover all possible sources (so, not exactly precise on an expanded system and depending in what interfaces you use). The EXTINTL input on the expansion bus should be used to trigger interrupts, rather than fiddling with the IPL lines directly, I think. The EXTINT is (apart from the internal interrupt sources) also the only interrupt source that can be handled by software (All other IPLs are routed to no-ops in the ROM) on an unmodified ROM, as it is the only one where the OS allows you to install a handler. However, the ROMs themselves (including Minerva) don't use any interrupt other than the frame IRQ, to my knowledge (everything is polled).
With your approach (high-frequency interrupt), I'd be a bit scared by the possible background load, at least with an original CPU. So, you should absolutely foresee a mechanism to be able to switch off the ticker when not needed. Also, keep in mind that especially for sound, the data you want to spit out fast and equidistant, must be coming from somewhere. It's not enough to being able to spit out data reliably, you must also be able to produce it in parallel. The Q68, for example, is just barely able to sustain its sound queue to the DACs from SD card.
ʎɐqǝ ɯoɹɟ ǝq oʇ ƃuᴉoƃ ʇou sᴉ pɹɐoqʎǝʞ ʇxǝu ʎɯ 'ɹɐǝp ɥO
Re: Hardware programmable timers
I don't remember exactly, but the Q68 should be about 20 times faster than it needs to be for this task.tofro wrote: Tue Aug 19, 2025 7:37 am It's not enough to being able to spit out data reliably, you must also be able to produce it in parallel. The Q68, for example, is just barely able to sustain its sound queue to the DACs from SD card.
What we talk here is a software problem caused by SLAVE buffering.
With the QIMSI sound demo, I have shown that even the 68008 can feed a similar sound queue fast enough, if SLAVEing is "disabled".
Re: Hardware programmable timers
Well, the exact reason isn't really relevant here, in my opinion: My point still stands: If sound output eats more than 50% of your CPU, there's only 50% left to generate sound. And somehow it must be generated, that's what people tend to forget.Peter wrote: Tue Aug 19, 2025 11:00 amI don't remember exactly, but the Q68 should be about 20 times faster than it needs to be for this task.tofro wrote: Tue Aug 19, 2025 7:37 am It's not enough to being able to spit out data reliably, you must also be able to produce it in parallel. The Q68, for example, is just barely able to sustain its sound queue to the DACs from SD card.
What we talk here is a software problem caused by SLAVE buffering.
With the QIMSI sound demo, I have shown that even the 68008 can feed a similar sound queue fast enough, if SLAVEing is "disabled".
ʎɐqǝ ɯoɹɟ ǝq oʇ ƃuᴉoƃ ʇou sᴉ pɹɐoqʎǝʞ ʇxǝu ʎɯ 'ɹɐǝp ɥO
Re: Hardware programmable timers
Divided by 3 would be 10922.666...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 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...
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.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.
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...
Re: Hardware programmable timers
A programmable interrupt would be useful for this (I think that's how Atari ST sequencers used to work), but the 70 ms number you measured seems concerning... must be a software issue, and unless understood it could affect your interrupt, too, or worse with the increased CPU interrupt load.Dave wrote: Mon Aug 18, 2025 9:42 pm In my little test just now I measured +/- 70ms of jitter worst case, and typically 30-35ms.
Can't buffer MIDI for long, or you'll need to assign timestamps to each byte or MIDI event in the card.Dave wrote: Mon Aug 18, 2025 9:42 pm As the sound card has MIDI (and can buffer it) this compounds the problems of the interrupt being low speed and also low precision depending on what hardware servicing happens at a higher priority.
Re: Hardware programmable timers
The SAM chip is on a card with a 68SEC000. It has separate IPL0, IPL1 and IPL2. It has two clock modes: 7.5 MHz all the time, and 30 MHz *except* during access to QL IO or a write to video memory. For those cycles it clocks at 7.5 MHz. This is the minimum spec for anything that would come with the chip. The QL is able to issue interrupts to the external CPU as normal. If it makes sense to do so, I can translate the limited interrupt levels onto the card's expanded range of interrupts to remap them. I can see cases where this might be useful, but I personally don't have any.tofro wrote: Tue Aug 19, 2025 7:37 am The CPU simply has no way of ever seeing an interrupt with IPL 1, 3, 4, or 6, because it lacks an IPL2 pin (rather, IPL2 is internally always equal to IPL0). The arrangements in the exception table are still there for compatibility reasons (and you could, in theory, also install handlers on them - with not much use), but a '08 will never receive them, so, such an interrupt can never be triggered.
Run-on sentence warning: The CPU being able to operate at a variety of clock speeds, see all interrupt levels and access any vector in the table, the vector table being in SRAM starting at $00 once the OS has been initialized and the OS and main memory of the system being on the card with the CPU and properly isolated from the QL, we solve many of the challenges of the QL mainboard.
The system I pitched is one of two that are simple in use. The trigger can be a divider, or it can be a programmable counter. The divider is 8-bit. A counter would have to be 16-bit to provide enough range of delays. There could be more than one register and each could be mapped to a different interrupt level.
I can see how this would be more precise most of the time. Though I know programming languages, I do not describe myself as a programmer. Knowing how to operate a motor vehicle is different from knowing how to drive. I hold programmers in the highest regard. I ask questions to make sure I'm providing suitable tools to them that are fit for purpose, efficient, non-intrusive and easily implemented. Looking at it from a programmer's point of view, you'll naturally gravitate towards things you can understand and control. Your thoughts may be a perfectly adequate solution for many. My job here is to present the options from my side that might extend your toolkit.tofro wrote: By using the polling list rather than the scheduler loop, you should be able to reduce the jitter significantly. It's much more accurate than the scheduler loop, which has more like a "best effort" timing.
This is forefront in my mind and why I wanted the fast interrupt to be very configurable. It should be able to scale to fit the task at hand. I did not consider the case of multiple programs wanting conflicting or incompatible interrupts. I am laser focused on MIDI playback and sequencing, with capturing incoming MIDI on a timeline for 'recording' an external MIDI keyboard or etc.tofro wrote: With your approach (high-frequency interrupt), I'd be a bit scared by the possible background load, at least with an original CPU. So, you should absolutely foresee a mechanism to be able to switch off the ticker when not needed. Also, keep in mind that especially for sound, the data you want to spit out fast and equidistant, must be coming from somewhere. It's not enough to being able to spit out data reliably, you must also be able to produce it in parallel. The Q68, for example, is just barely able to sustain its sound queue to the DACs from SD card.
The sound chip doesn't natively support playback of sound through a D/A converter. If this is something people want, I would use large FIFOs like 2Kx9 so overhead would be low. I can see challenges if the source of the data is not RAM.