The last months went by in a flash. The 6502MMU is closer to being in a beta version, but a few features remains.
One of the features of this MMU is to enable multitasking and multibank use. I therefore wanted to do this in a way that makes it possible to run several tasks on the 65C02. Using memory banking has enabled us to switch between different stacks. Currently up to 256 different stacks and zero pages (and probably up to 65536 in the future). What is not so obvious it how to implement it so that nothing breaks. I need interrrupts to work and each task within its own bank (or several banks).
After some thinking I believe I have cooked it down to two ways to actually make it work. Both requires a "Supervisor level" and a "User level". The MMU can set a memory bank to being in Supervisor mode and thus not accessible from User level. It also sets a key to each bank that is in user level so that you can't jump into a bank from a different bank, unless you are in Supervisor mode. The idea is that the task manager (system) runs in Supervisor level, while the actual tasks runs in User level. Interrupts happen and the interrupt handler also runs in supervisor mode, but this is were things get complicated.
Once the task handler jumps into a task (and to User level) this happens with a JSR instruction into a memory bank that has its protection key set. Once in this bank, the 65C02 can't exit that bank using JMP or JSR (the MMU won't change the bank). There is only two ways to exit the bank (with the User level task) which is through an interrupt or through an RTS. The RTS return address in not accessible in the User level since it resides in a Supervisor level stack, so the "system" is protected from whatever happens inside a task. The task simply has no way to access the memory that the task manager and other system processes runs in.
I can solve the interrupt handling by using one of the following two methods:
1) The interrupt handler is invoked after a certain time. The reason for this is that the task is allowed a certain amount of time before it needs to stop so that the task handler can run the next task (in the task handler queue). The interrupt code runs in Supervisor level and needs to return to the task it interrupted at some point. This can be done immediately if there is code (in the task) that exits the task on the request from the interrupt handler. From a programming perspective, this makes the interrupt handler easier to do since it always returns to the last interrupt request via a RTI, and two interrupts won't happen nested inside each other. It is also the most demanding with respect to making tasks, so my guess it that its not a good idea.
2) The interrupt handler is invoked after a certain time as for (1), but the interrupt handler doesn't return to the current task at hand but invokes the next task in the task manager queue. At some point we need to return to the interrupted task, but the idea is that this happens in the next time slot (that we have for that task) and not before. In order for that to work we need to do two things:
i) Store the return address, stack pointer and processor status.
ii) Store the bank number in which the interrupt happened.
Let me show this in a semigraphical-way to simplify:
Method 1)
Code:
Supervisor level:
JSR task1 --> JSR task 2 --> Interrupt handler -> (code) -> RTI
| / | / V
| | | | |
| | | | |
User level: V | V | |
Task1 | Task2 | |
CLI | CLI / |
(some) | (Interrupt) >-- /
(code) | (code) <---------------------------------------------
SEI / SEI
RTS---- RTS-----> (back to supervisor task handler)
Method 2)
Code:
Supervisor level:
JSR task1 --> JSR task 2 --> Interrupt handler -> (code) -> JSR task3
| / | / |
| | | | |
| | | | |
User level: V | V | V
Task1 | Task2 | Task3
CLI | CLI / CLI
(some) | (Interrupt) >-- (some)
(code) | (code) (code)
SEI / SEI SEI
RTS---- RTS -----> (task manager) RTS -----> (task manager)
and so on... As you may understand, the second method has an interrupt handler that is more complicated and interweaved with the task handler. Storing the bank number is also not so easy to do since the MMU doesn't have a way to report back on the active bank (at least at the moment). Still we know which bank we jumped into (from the task manager) and as long as we don't spread a task over several banks, we don't need to read the bank number.
There are probably many consequences that I can't see at this point, and there may be other ways to do it (which I haven't seen). Passing parameters between the task and the system is one bottleneck which requires some thinking, but all-in-all I haven't seen anything that prevents this method from working. A task that doesn't use CLI and therefore prevents an IRQ, can be handled with an NMI (timer driven) to prevent it from halting the system.
If anyone has some input to improved ways (or questions on why it is implemented this way), I am happy to hear you.