Well, I redesigned the interrupts on the 65VM02 using ideas from all of you --- thanks for your input!
--- the new document is attached.
My new design allows IRQ and NMI to interrupt the low-priority ISRs (NMI can also interrupt an IRQ ISR). So, NMI would be used for super-high priority interrupts, IRQ for high priority interrupts, and the other interrupts for lower-priority interrupts (the low-priority interrupts can't interrupt any other ISR at all).
GARTHWILSON wrote:
It would be nice to have a preemptive multitasking OS where interrupts are not held up while a task switch is being performed. I don't know enough about preemptive multitasking systems to know if that's possible. I did a cooperative multitasking realtime system for work a few years ago on a PIC16 where interrupts could be processed even during a task switch (which consisted of nothing more than a return to the cyclic executive main loop and a call of the next task).
The NMI and IRQ interrupts would not be held up while a task-switch is being performed --- the low-priority interrupts would be held up though, because they can't interrupt each other.
If I were programming a 65c02, I would use a cooperative multi-tasking OS similarly to your PIC16 program. Forth traditionally did use a round-robin cooperative multi-tasker because that is what Charles Moore wrote way back in the 1970s for his Kitt Peak work (I think that was a PDP11). This is a reasonable design that many people have used successfully.
On the 65VM02 however, I have the EXA instruction that can be used for obtaining a semaphore. This allows a preemptive multi-tasker. The idea is that any shared resource has a semaphore associated with it. A task obtains the semaphore (it may have to wait until another task releases the semaphore). While it has the semaphore it can access the resource and it can be guaranteed that no other task will access the resource at the same time. If a task-switch occurs while the task is in the middle of accessing the resource, and there is some other task waiting to access the resource too, the other task will find the semaphore already set to 1 by the first task and it will wait until the semaphore gets set to 0 before it can obtain the semaphore and the resource.
I've never used a preemptive multi-tasker, so all of this is new to me. It is a pretty interesting subject though --- I want the 65VM02 to support this --- I think that, if the 65VM02 is going to be used in the real-world, it would be expected to support this.
GARTHWILSON wrote:
A high-priority interrupt could be like something I've done many times for generating or sampling an analog signal on a very exacting interval, something that more-complex machines usually do with DMA and a separate signal engine of some kind (although that process, and the delay from the buffers involved, might be a problem sometimes too). Even on my 5MHz '02 workbench computer, I've exceeded 140,000 interrupts per second, and sampled audio at 44ksps, interrupt-driven by a VIA timer. The delay to start the ISR must be very short, and just as importantly, consistent, since there's no buffer between the data converter and the bus. Uncertainty in the timing constitutes jitter which results in distortion.
This is what IRQ would be used for. In the new design, IRQ can interrupt the ISRs for the low-priority interrupts. IRQ tied to a timer with a fast frequency will execute on a strict schedule without any jitter --- an example would be reading an analog>digital converter.
GARTHWILSON wrote:
If a computer and OS are designed for a use that won't ever need such urgent interrupt service, that's understandable; but I would recommend making sure the processor itself can still do it if someone wants it to in a realtime design.
The 65VM02 is intended to be used for hard real-time micro-controller programs.
I wish I had thought of the 65VM02 in the early 1980s, but I was still in high-school at that time. The 65VM02 would have allowed computers such as the Commodore-64 to support a multi-tasking OS. How cool would that have been?
CP/M and MS-DOS would not have become popular --- who would want to use MS-DOS and its TSRs
when a real multi-tasking OS was available on the C64? --- the world would have been a better place!
Those days are long gone though.
Now the only realistic use for the 65VM02 would be in a low-cost low-power micro-controller --- a step up from the 80c320, but a step down from the ARM --- somewhat of a narrow niche, but I think it could find some use in the real-world (the STM8 is currently in that niche).
This is the rewritten part of the document discussing interrupts:
Code:
These new instructions support the MIRQ interrupts:
MRTI used to terminate MIRQ ISRs (similar to how RTI is used to terminate IRQ and NMI ISRs)
SEM sets the M-flag (this masks MIRQ interrupts, similar to SEI for IRQ)
CLM clears the M-flag (this allows MIRQ interrupts to occur, similar to CLI for IRQ)
These new instructions support the IRQ and NMI interupts:
ENTR push A X Y to the return-stack, then move D to X, then set A Y and D to zero
EXIT move X to D, then pull Y X A from the return-stack
Section 3.) the MIRQ interrupts
IRQ and NMI are the same as they were on the 65c02 --- they only push P and PC, and they end with RTI.
IRQ has a higher priority than MIRQ if they are both pending at the same time, and NMI is higher-priority than IRQ.
IRQ and NMI set the M-flag and the I-flag upon start --- so neither MIRQ nor IRQ can interrupt them.
There are four MIRQ interrupts in addition to the IRQ and NMI of the 65c02.
The lower MIRQx has a higher priority if there is more than one MIRQ pending at the same time.
MIRQ0 vector: $100
MIRQ1 vector: $102
MIRQ2 vector: $104 typically the UART that communicates with a desktop computer
MIRQ3 vector: $106 typically the heartbeat timer that does the task-switch
The MIRQ interrupts set A Y and D to zero so the direct-page is at $00 and the return-stack is at $100.
They then set the M-flag and push these registers:
PC the old PC value, not the new PC that points to the ISR
P the old P value, not the new P that has M-flag set
D the old D value, not the new D that is 0
A the old A value, not the new A that is 0
Y the old Y value, not the new Y that is 0
They then execute the ISR.
The MIRQ ISRs should end in MRTI rather than RTI to undo all of this (6 bytes, rather than just 3 bytes).
The MIRQ ISRs can use A and Y freely, but they can't use X because it isn't saved and restored automatically.
If the MIRQ ISRs need to use X, then can start with PHX and end with PLX to save and restore X manually.
Most ISRs don't need to use X. They typically use A to hold data, and Y as a pointer into a circular buffer.
It is possible for MIRQ ISRs to be written in Forth rather than assembly-language.
In this case, X and IP need to be saved and restored manually. X and IP are set for a new Forth system.
Forth is pretty slow compared to assembly-language though, so writing ISRs in Forth is not recommended.
An ISR is short and simple --- assembly-language is not onerous --- Forth is for the tasks because they are big.
MIRQ3 is typically the heartbeat timer, and this does a task-switch, setting X IP and D for the new task.
Rather than work with a circular buffer, an MIRQ could be used to read or write files between the alternate-bank.
The FLDA and FSTA instructions are provided to allow the alternate-bank to be used as a RAM-disk.
The system may send and/or receive files over a UART while communicating with a desktop computer.
Note that S is pointing somewhere in the middle of page-one, but this varies every time an MIRQ interrupt happens.
This is one reason why MIRQ shouldn't interrupt each other --- they might clobber each other's return-stack.
The M-flag is set upon entrance to any ISR and it should be left set so one MIRQ doesn't interrupt another.
MIRQ ISRs can be interrupted by IRQ and NMI though --- assuming that the I-flag is clear, which it generally is.
The IRQ and NMI ISRs don't use the page-one return-stack at all, so they don't clobber the MIRQ ISR's return-stack.
The M-flag masks MIRQ interrupts. This is set upon start-up. Use CLM to clear the M-flag and SEM to set it.
The MIRQ interrupt vectors are in RAM, so you have to be careful to set them before a CLM is done in the start-up.
They are in the bottom of page one, so they should hopefully not clash with legacy programs' memory usage.
It is assumed that the I/O ports are all in page zero. This is why D gets set to zero in MIRQ interrupts.
The multi-tasking OS uses D as zero for its own work, and gives the tasks other D values.
IRQ and NMI ISRs should start with ENTR and end with EXIT as shown below:
ENTR ; push A X Y to the return-stack, then move D to X, then set A Y and D to zero
... ; do the ISR --- use of the return-stack (JSR or PHA PHY PHX) is banned --- use of X is banned
EXIT ; move X to D, then pull Y X A from the return-stack
RTI
The primary limitation is that the ISR can't use the return-stack at all.
This is because the return-stack is page-one that belongs to the MIRQ ISRs and S is pointing into the middle of it.
If an IRQ or NMI interrupted an MIRQ ISR and then used the return-stack, it would likely clobber the MIRQ ISR's data.
IRQ and NMI ISRs should be pretty short and simple, so they don't need to call subroutines.
Another limitation is that the X register can't be used in the ISR because it is holding the old D value.
If an IRQ or NMI ISR needs the X register, the X register can be temporarily held in a direct-page variable like this.
ENTR ; push A X Y to the return-stack, then move D to X, then set A Y and D to zero
STX $BF
... ; do the ISR --- use of the return-stack (JSR or PHA PHY PHX) is banned --- use of X is okay
LDX $BF
EXIT ; move X to D, then pull Y X A from the return-stack
RTI
Note that PHX and PLX can't save and restore X because use of the return-stack is banned in IRQ and NMI ISRs.
Also note that the ISR can't be written in Forth because use of the return-stack is banned in IRQ and NMI ISRs.
IRQ and NMI are high-priority interrupts (they can interrupt MIRQ ISRs) so they should be in assembly-language for speed.
The STX and LDX would not generally be done because they waste time, and speed is critical in high-priority interrupts.
A typical use for IRQ would be a timer set at a high frequency, and the IRQ reads an analog>digital converter (ADC).
Reading the ADC has to be done on a strict schedule, so IRQ would be used because it can interrupt the MIRQ ISRs.
IRQ or NMI should not be used for the heartbeat timer because we don't want to do a task-switch in the middle of an MIRQ ISR.
Typically MIRQ3 should be the heartbeat timer because it is the lowest priority of the MIRQ interrupts.
The heartbeat is low priority because the tasks aren't expecting to be on a strict schedule.
MIRQ2 is typically the UART to the desktop computer because this is usually pretty slow, and it is not a high priority.
MIRQ1 and MIRQ0 are for high priority I/O. IRQ and NMI are for extremely high priority I/O (these can interrupt MIRQ ISRs).
Note that MIRQ ISRs start with A and Y set to zero, and IRQ or NMI ISRs also have A and Y set to zero if they use ENTR.
This is "defensive design." A possible bug is that the programmer forgets to initialize A or Y before using them.
By having them always start as zero, the program will do the same thing everytime it is run (possibly a bug though).
If there is a bug, you find out early --- you don't have a program that sporadically fails, which is very bad.
Note that D is set to zero on start-up. Legacy programs should work unchanged because this is the same configuration as the 65c02.
The M-flag gets set in IRQ and NMI interrupts, but this won't affect legacy programs because this bit in P was unused on the 65c02.