So, a bit more on interrupts:
Again, some notes on how the 68k interrupt system works:
When a non-masked interrupt level is recognized on the ILP lines, the 68k will start exception processing (exceptions being a much wider category than interrupts, but in this case interrupts are what we are discussing).
An interrupt can be looked at as a 'jump to subroutine' caused by an external event, so the CPU has to do:
1) Acknowledge the interrupt and through that mechanism figure out what device caused it, so it knows to what 'subroutine' to jump to, that is expected to handle the interrupt
2) Store the current CPU context (i.e. remember where it stopped normal program execution)
3) Jump to the interrupt handler 'subroutine'
4) On completion of the interrupt handler, restore the CPU context (i.e. go back to where it was interrupted).
For this particular point in the discussion, let's look closely at point (1):
To acknowledge an interrupt, the CPU executes a special read cycle, with a special encoding on the FC and address lines, the encoding also contains the interrupt level that is acknowledged. The interrupting device is expected to respond in one of two ways:
a) with a 1-byte exception vector, the value of which is multiplied by 4, and this number is taken as the address from which to fetch the address of the interrupt servicing routine.
b) with an 'autovector signal' (the implementation of which varies on different 68k CPU family members), which forces the CPU to calculate the vector from the interrupt level being serviced, as 24+interrupt level. This number is again multiplied by 4 and the result is used as an address from which the address of the servicing routine is read
There are many subtleties to the way the 68k handles exceptions in general, which are also filter down to how interrupts are handled. For instance, what if no device responds, so no vector byte nor autovector signal? There are provisions to re-run the vector request or, default to vector 24. Also, there is a particular quirk of the 68k that although vector numbers 64 to 255 are defined as user interrupt vectors, an interrupting device can pass any interrupt vector, even vector 0 which is actually the initial address of the supervisor stack pointer at reset. This 'feature' is however used by dedicated 68k peripheral chips, which normally contain a register for the interrupt vector to e loaded into, so when the chip causes an interrupt, this will be used for interrupt acknowledge - on devices that implement this, it is required that the initial value of the register is 15, so if a device causes an interrupt out of turn while not being fully or properly initialized, there is a default vector which an OS can 'catch' and signal a programming error.
It should be noted that since multiple levels of interrupt exist, not just one maskable and one unmaskable as on older CPUs, it is permitted that recognizing a higher than currently processed interrupt level, will result with the current interrupt being interrupted by a higher level interrupt, unless the current interrupt servicing routine has masked higher level interrupts. This may turn to be a problem when adding higher level interrupt support on the QL.
All of this means it is not exactly possible to make a dead simple 'Sinclair style' interrupt system on the 68k - external devices are expected to encode the interrupt level and in case several devices are using the same level, and using vectors, external hardware has to decide which one of the possible devices causing the interrupt should respond with a vector when the interrupt is acknowledged, as only one can do so. This system can implement very complex interrupt structures.
However, since we are talking about a Sinclair product, it is simplified as far as it could have been. Any interrupt acknowledge is automatically made to request auto-vectoring. BTW this is possible even if devices capable of vectored interrupts are used to cause interrupts - it's just that detecting which one did it has to be done in software. Also, as I said in a previous post, since no interrupt priority encoder has been implemented, there is really just one interrupt level that can be used.
So, how are multiple interrupt sources handled in this maximally simplified system? This is basically the same as on any older system - when an interrupt occurs, it is auto-vectored to an address defined in the vector table, in this case vector 26, which on the QL is a fixed address residing in ROM. This jumps to the interrupt handler. It is up to this piece of software to go through all sources of interrupt and detect why the interrupt happened, and decide from that what to do about it.
On the bare QL this happens by reading the interrupt register, physically this is inside the 8302 ULA. There are several bits that will be set by the hardware requesting an interrupt, the handler has to read the register and then depending on which bits are set, act on each of them in turn based on priority hardcoded in the actual code, i.e. in which order does it check them and act on them. Also, once it has acted on a bit being set, it has to reset that bit according to a protocol depending on the actual hardware, to signal that it has handled that particular interrupt source.
As long as any of the interrupt bits are set, the interrupt pin on the 8302 remains active, so if some are not handled and the handler returns from interrupt processing, the remaining set bits will just cause another interrupt and get right back to interrupt processing. This last bit would be quite inefficient as it means all the 4 steps above will happen again, which requires time. The time that passes between hardware causing an interrupt and the interrupt being handled is usually referred to as interrupt latency and is a very important and contentious issue, especially for a real time OS - which is appropriate since QDOS/SMSQ has a lot of characteristics of a RTOS.
What is not so obvious is that the simpler interrupt system described above can be faster than a vectored interrupt system, and in general simpler to manage. One reason is that it skips the inefficiency of CPU context switching every time an interrupt vector has to be resolved - for instance if many devices are using the same interrupt level but supply a different vector, even with external hardware priority arbitration, the only way for the CPU to get the next vector from a device that has an interrupt pending, is to return from exception processing and then let itself be interrupted again.
In a simple system, the steps above are usually done as 1, 2 (where the common interrupt handler is invoked), and then as many steps 3 as are needed to service all interrupts, by software maintaining a table or linked list or simply hard coded order of servicing routines, and then finally step 4. Some further efficiency can be gained by storing all but a small number of registers with important pointers, saving the context of the CPU at point of interrupt, then do as many interrupt servicing routines that can consider all other registers free to use, then when all of them are done, restore them and end exception processing.
In a multi-vectored system each routine would have to store as many registers as it needs, then restore them before finishing, possibly only for the next interrupt to store them again, etc. On a slow CPU this is quite important, especially since we are talking about 4-byte wide registers on a CPU with an 8-bit bus.
On a faster more advanced 68k members this may actually be even ore important as there is more of the CPU context to be stored on the stack, and also, internal operations will be much faster than external reads and writes, the latter will rely on caching. While the faster CPU may well reduce latency in terms of time, it may NOT reduce it in terms of CPU cycles performed.
Yes, it is possible to do the same in a vector based system, but then one is exactly emulating a non-vectored system, by reading the vector register and calculating the vector manually, fetching the address and jumping to the next handler in turn.
There is abetter way:
I mentioned latency, and from there we quickly get to the concept of interrupt priority management. In case of a hardware based solution, unless it's quite complex hardware, you have what you have. In case of a simpler system implemented with linked lists, the list is handled in order of linking, first links get handled first. In other words, the linking order defines the priority and largely (* <- remember this!) latency. The linking order can therefore be used to define priority in software.
The way this is implemented in general, is that (in QL's case) an autovectored interrupt jumps to a sort of skeleton handler, which implements a 'preamble' of sorts that any interrupt handler would do before actually handling the interrupt, which is basically step 2 from the beginning of this post, presenting each of the linked handlers the same 'software interface', which it then fills in by going through the linked list of handlers, normally providing a pointer to the handlers private data (stored in the list). Each handler in turn is called as a subroutine (that would be step(s) 3 above), and can basically do whatever it needs, with the restriction that it has to keep the list pointer intact. When the list is exhausted, there is a 'postamble' which then implements step 4 and returns from exception handling.
It is up to each sub-handler to know what it has to do with it's hardware, to properly acknowledge, handle, and clear the relevant interrupt, including from multiple source(s).
SO, finally we come to the (*) above.
One of the hallmarks of a real time OS is that interrupt latency and processing times are well defined (at least as a range of best to worst case timings). The above is all assumed not to have been interrupted by a higher level interrupt. In that case, if the linked list is properly ordered and the handlers themselves are properly done with respect to the actual hardware, there is very good predictability.
The 68k will automatically rise the internal interrupt level mask one above the interrupt level currently being processed, so a higher level interrupt can interrupt lower level interrupt processing. That being said, once any exception processing is started (including interrupts) the CPU will automatically be in supervisor mode and there is nothing preventing an interrupt handler from altering the interrupt level mask to prevent itself from being interrupted while doing time-critical operations, or operations that must be atomic (usually some sort of handshake procedure required for proper interrupting hardware operation). The problem is, if higher level interrupts are implemented, assuming higher priority and less latency in their handling might not be a given, if every lower level handler decides the first thing it does is rises the interrupt mask to level 7.
There is really no other way to get completely around this, especially as legacy software can actually do this.
Basically, any interrupt structure can be mishandled, and handlers can misbehave, there is no bullet proof way around it.
What can be done is prescribe and expect some discipline in how handling of interrupts is done depending on level. While higher levels will be prioritized, it is expected also that the actual handling of said interrupts is as quick as possible. This should really be something along the lines of 'check status register, if needed move (X amount of) data from X to Y, clear interrupt, return'.
So, back to system timers:
If there is a hardware timer that generates a periodic interrupt on the system level, in order to prevent the actual timer read from introducing extra latency, there is a software interrupt count. Incrementing the count is handled in the 'preamble' (step 2 above) of the main handler, BEFORE anything else, to insure the count is accurate. Sub-handlers can then read the count and compare with what the read the last time around in their local data space, see how many interrupts have passed, and based on that decide if any actual handling is needed - if not, return and do not waste any more time. Local counters can be kept as well.
There are also hardware assisted solutions:
Said hardware timer from above also has a hardware counter, which can then be (only) read by anyone, to see how much time has elapsed since last the handler was called. This is to a large extent independent of the actual interrupt system, because the counter counts regardless of any interrupt being handled or masked - tough if used, has to be taken into account when writing and linking the handler.
Also, a hardware timer can be a part of the hardware that needs it to be properly serviced, and thus private to the interrupt handler for that hardware. How it is used, as a counter and/or interrupt timer, is then up to the designer, but it is not generally available to other programs, and cannot be meddled with by other code.
Oh, and I keep forgetting:
Interrupt vectors use up 3/4 of a k od data in the ROM. I've known a certain Tony Tebby that wrote a rather complex piece of code in that size of a space and called it bloatware

so back to Sinclar ways... the OS was barely squeezed in the available space.