Multithreading on the TI-83 Plus

Published August 03, 2006
Advertisement
I'm not really clued up on the way these new-fangled modern operating systems handle running multiple threads, so I apologise in advance if this isn't really proper multithreading.

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:

Quote:-> 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:

Main	.include "Multithread/Multithread.asm"	; Kick things into action.	call Multithread.Init		; 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.	ld b,8	call ionRandom	ld (curCol),a	ld b,8	call ionRandom	ld (curRow),a	ld b,0	call ionRandom	bcall(_PutMap)	bcall(_getCSC)	cp skClear	jr nz,{-}		call Multithread.End	ret	; ---------------------------------------------------------	SecondaryThread		di ; Don't want any other thread writing to the LCD!				ld b,64	call ionRandom	add a,$80	push af ; Save for later...		out ($10),a				ld b,48		call ionRandom		add a,48		ld b,a		srl a		srl a		srl a		add a,$20		out ($10),a			ld a,b		and 7		ld c,%10000000		jr z,{+}		ld b,a	-	srl c		djnz {-}	+			in a,($11)		call LcdBusy		in a,($11)		xor c		ld c,a		call LcdBusy	pop af	out ($10),a	call LcdBusy	ld a,c	out ($11),a						ei		jr SecondaryThread	LcdBusy	push af	inc hl	dec hl	pop af	ret



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]
Previous Entry Map Editing
Next Entry Stuff and nonsense
0 likes 5 comments

Comments

Ravuya
With a cursory look over your code, it looks like you are doing a very primitive form of time slicing. This is a good implementation (some DOS tools used to use it back in the day), and if you take a modern operating systems course you can find out all about it. It has been superceded by advancements in technology, but is probably the best you can hope for on the Z80.

It's a shame you're not using the 89 as the 68000 has a built in task switching unit which Commodore and Atari exploited to get multitasking speedups and beat Apple's original 68000 Macintosh.

I don't think the Z80 really has much in the way of complex program logic.
August 03, 2006 08:53 AM
benryves
Quote:Original post by Ravuya
With a cursory look over your code, it looks like you are doing a very primitive form of time slicing.
Hence the phrase "time slice". It's not especially elegant - but it's simple, and works.

Quote:I don't think the Z80 really has much in the way of complex program logic.
Not really. The smallish number of registers (I'm not even considering the shadow registers) that need to be backed up helps things a lot, as does an easily relocatable stack.

Memory allocation is an ugly beast on the TI, though. I think my best bet is to create a new temporary program for each new thread. The documentation says that some of the memory functions "clean up temporary variables", which is a bit scary if it starts rearranging programs in RAM. I'll have to see.
August 03, 2006 10:39 AM
paulecoyote
Man, your blog is always inspiring. What you can squeeze out that hardware is amazing.
August 03, 2006 11:21 AM
EDI
If you want to use a shared resource (those function calls you mentioned), you can implement a mutual exclusion synchronization system.

Wherein a thread is only allowed to use a resource of it's mutex flag says it is not in use. When a thread starts to use a resource it flags the mutex to in use. when it is done flags it to unused. the second thread can then wait and continue to query the mutex flag until it is free (where it can then aquire it), this of course destroys parralel processing, but neccissary when you have somthing that cannot run in parralel.

September 22, 2006 10:20 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement