Basically, I was pondering (as one does) whether it would be possible to run any number of threads on the TI-83 Plus. I daresay others have done this in the past, but not having access to the internet at the time to check I thought I'd do my own bit of experimentation.
The way I can see this working is to give each thread a small amount of time to run in (time slice). At the end of one thread running, I would save the calculator's state away, and load the state for the next thread. I would keep on swapping threads several times a second, giving each their own bit of time and preserving their state.
What I really needed to know was what needed to be preserved between executions. Really, there isn't a lot - just the registers. PC and SP clearly need to be preserved, so that when a thread is resumed it resumes running at the point it was left at. Keeping the other general-purpose registers (AF, BC, DE, HL, IX, IY) safe would be important as well.
Naturally, I'll need to also preserve the stack. Or rather, for each thread, I'll need to allocate some memory for a stack and point the new thread's SP register at the end of that new memory. To keep things simple, I'll just point my test thread's SP at $FFFF-200, as the TI is arranged to have 400 bytes of stack RAM, growing backwards from $FFFF.
To perform the swapping, I'll use a custom interrupt (run in IM 2). The timer hardware will trigger this interrupt many times a second, so it is ideal. At the end of my custom interrupt, I'll jump into the default handler provided by the TIOS so the TIOS functions that rely on it behave correctly.
The way I see it working is like this:
-> Enter interrupt handler.
Pop value off stack. This will be the program counter of the last running thread. (->A)
Save current stack pointer. (->B)
Set stack pointer to area in RAM where I can preserve registers for this thread. (<-C)
Push IY, IX, HL, DE, BC, AF to the stack.
Push old stack pointer (<-B) to stack.
Push old program counter (<-A) to stack.
Find the address of the RAM where we have stored the registers for the next thread.
Load it into the stack pointer.
Pop value off stack, store it (will be PC) (->D)
Pop value off stack, store it (will be SP) (->E)
Pop AF, BC, DE, HL, IX, IY off stack.
Save current stack pointer (end of RAM area used to store registers for new thread) as address of last thread for next interrupt. (->C).
Load value into SP (<-E)
Push new thead's PC to stack (<-D)
Jump into TIOS interrupt handler ->
Effectively, all this does is take the pointer stored when the current thread was resumed, dump all the registers into the memory it points to, hunts the next thread, reloads the registers and saves the pointer for next interrupt.
Does it work?
The above screenshot doesn't look too impressive, I'll give you that. There are only two threads running. The secondary thread is drawing all those random dots on the LCD. The other thread (which is the main program thread) just contains "bcall(_getkey)" - hence the 2nd/Alpha icons appearing,
I'll make the primary thread do something a bit more exotic - display random characters on the left side of the screen, the secondary work on the right side of the screen:
The code for this is simply:
; Kick things into action.
; For the moment, the secondary thread is hard-coded, and
; the 'multithreading' code ONLY switches back and forth between
; two hard-coded thread slots.
halt ; Slow things down a bit here.
di ; Don't want any other thread writing to the LCD!
push af ; Save for later...
- srl c
As you can see, there are two discrete threads running there.
Well, that's two threads up and running. What's needed to extend this to any number of threads?
- Thread management. Basically, the ability to add/remove threads at will, and have the handler be able to switch between as many threads as required.
- Allocation of new stack space. New threads will need more stack space, so I shall need to add some mechanism to steadily allocate it.
- Idle threads. One big problem is that as you add threads, each one gets progressively slower. If threads flag themselves as idle, they can be skipped so threads that do need CPU time get it. The easiest way to do this is to set a "sleep" counter which is checked when threads are switched around - if it's zero (when it runs out), the thread is given some time to run.
Off the top of my head, that's about it. There are some issues I have come across when working with this:
- TIOS routines are not thread-safe. This speaks for itself, pretty much - if you have two threads, both of which are calling bcall(_putC) to display a character, they interfere with eachother (for example - incorrectly setting the value of curCol or curRow as one thread changes it as the other one is reading it) which causes crashes.
- Variables can become an issue. Same reason as above - if two threads call a function which uses a set RAM location for a local variable, the variable can be changed half way through.
One possible way to get around thread-unsafety of functions is to disable interrupts before calling them and reenabling them afterwards, which prevents the thread switcher from swapping them. It's a pretty lousy workaround, though. [sad]