I know the spectrum had the IM2 mode, but can I start multiple threads running simultaneously?
Like to play sounds, watch the keyboard, and other tasks who can be runned simultaneous.
Retrocomputing Stack Exchange is a question and answer site for vintage-computer hobbyists interested in restoring, preserving, and using the classic computer and gaming systems of yesteryear. It only takes a minute to sign up.
Sign up to join this communityI know the spectrum had the IM2 mode, but can I start multiple threads running simultaneously?
Like to play sounds, watch the keyboard, and other tasks who can be runned simultaneous.
Not simultaneously, as it only has one CPU, but it has a 50 Hz timer interrupt, so it can do time sharing processing, provided that programs are well behaved, as there is no memory protection and no supervisor mode in the CPU.
As a proof of concept, I've written a small task scheduler and tasks that are executed in a time sharing fashion (with a 20ms quantum)
; (c)2020 Miguel Angel Rodriguez Jodar, for Retrocomputing (Stack Exchange)
; Demonstration of a very simple scheduler and two tasks running on the ZX Spectrum
; Licensed under GPL 3.0
org 0c0e0h
Main:
halt ;let a whole interrupt happen so we have plenty of time until the next one
di
ld a,0c0h
ld i,a
im 2 ;set scheduler interrupt
ei
ld sp,StackTask1Top ;load task1
jp Task1 ;and execute it
org 0c0ffh
dw Scheduler
Scheduler:
push af ;save current process state it its stack
push bc
push de
push hl
exx
push af
push bc
push de
push hl
exx
push ix
push iy
ld hl,0
add hl,sp
push hl ;get current stack pointer (just interrupted task)
ld hl,(CurrentProc)
inc hl
pop de
ld (hl),e
inc hl
ld (hl),d ;and store it into process table
inc hl
ld a,(hl) ;check if end of processes in table
or a
jr nz,NotEndOfTasks
ld hl,TaskTable ;if so, start from the beginning of the table
NotEndOfTasks:
ld (CurrentProc),hl ;select next process
inc hl
ld e,(hl)
inc hl
ld d,(hl)
ex de,hl
ld sp,hl ;load its stack pointer
pop iy ;and jump to it, restoring its state
pop ix
exx
pop hl
pop de
pop bc
pop af
exx
pop hl
pop de
pop bc
pop af
ei
ret
;----------------------------------------------------------
;TASK1: CALCULATE AND PRINT 16-BIT PRIME NUMBERS
;----------------------------------------------------------
Task1:
ld ix,DataTask1 ;environment for this task (pointers for PRINT routine)
TryNextNumber:
ld bc,2 ;BC holds the current divisor to try with
TryDivisor:
ld hl,(NextCandidate)
ld d,b
ld e,c
KeepSub:
or a
sbc hl,de
jp m,NotDivisible
jp z,NotPrime
jp KeepSub
NotDivisible:
inc bc
or a
ld hl,(NextCandidate)
sbc hl,bc
sbc hl,bc
jp m,ItsPrime
jp TryDivisor
NotPrime:
ld hl,(NextCandidate)
inc hl ;HL is not prime, try the next odd number
inc hl
ld (NextCandidate),hl
jp TryNextNumber
ItsPrime:
ld hl,(NextCandidate)
ld (ix+SCRROW),6 ;print number at position 6,14
ld (ix+SCRCOL),14
call CalculateScrPos
call PrintNumberHL
jp NotPrime
NextCandidate:
dw 3 ;we begin with 3 to test if prime
;----------------------------------------------------------
;TASK2: print character set
;----------------------------------------------------------
Task2: ld ix,DataTask2
ld a,' '
AnotherChar:
call PrintCharA ;just print the character set, from ASCII 32
inc a
jp p,AnotherChar ;to ASCII 127 and start over again
ld a,' '
NotResetChar:
jp AnotherChar
;----------------------------------------------------------
PrintNumberHL:
ld bc,10000
call Div
ld a,d
add a,'0'
call PrintCharA
ld bc,1000
call Div
ld a,d
add a,'0'
call PrintCharA
ld bc,100
call Div
ld a,d
add a,'0'
call PrintCharA
ld bc,10
call Div
ld a,d
add a,'0'
call PrintCharA
ld a,l
add a,'0'
call PrintCharA
ret
Div:
ld d,0
KeepDiv:
or a
sbc hl,bc
jp m,Negative
push af
inc d
pop af
ret z
jp KeepDiv
Negative:
add hl,bc
ret
PrintCharA:
push af
push bc
push de
push hl
ld l,a
ld h,0
ld a,(ix+SCRPOS)
or (ix+SCRPOS+1)
call z,CalculateScrPos
ld e,(ix+SCRPOS)
ld d,(ix+SCRPOS+1)
push de
add hl,hl
add hl,hl
add hl,hl
ex de,hl
push hl
ld hl,(23606)
add hl,de
pop de
ld b,8
LoopPrintScan:
ld a,(hl)
ld (de),a
inc hl
inc d
djnz LoopPrintScan
pop hl
ld a,(ix+SCRCOL)
inc a
cp (ix+ENDCOL)
jp z,NotEndColumn
jp nc,EndColumn
NotEndColumn:
ld (ix+SCRCOL),a
inc hl
ld (ix+SCRPOS),l
ld (ix+SCRPOS+1),h
jp EndPrintA
EndColumn:
ld a,(ix+BGCOL)
ld (ix+SCRCOL),a
ld a,(ix+SCRROW)
inc a
cp (ix+ENDROW)
jp z,NotEndRow
jp nc,EndRow
NotEndRow:
ld (ix+SCRROW),a
jp CalcAndEnd
EndRow: ld a,(ix+BGROW)
ld (ix+SCRROW),a
CalcAndEnd:
call CalculateScrPos
EndPrintA:
pop hl
pop de
pop bc
pop af
ret
CalculateScrPos: ; HL = (row/8)<<11 + (row%8)<<5 + col
push af
push hl
ld a,(ix+SCRROW)
and 0f8h
or 40h
ld h,a
ld a,(ix+SCRROW)
and 7
sla a
sla a
sla a
sla a
sla a
or (ix+SCRCOL)
ld l,a
ld (ix+SCRPOS),l
ld (ix+SCRPOS+1),h
pop hl
pop af
ret
;Table used by the scheduler to get the stack pointer for
;the next task to execute.
;The end of the table is marked by a processor with ID 0
TaskTable:
db 1 ;ID
dw StackTask2Top-20 ;stack pointer
db 2 ;ID
dw StackTask2Top-20 ;stack pointer
db 0 ;ID (end of table)
dw 0 ;null stack
CurrentProc:
dw TaskTable ;current position within TaskTable
ds 64
StackTask1Top:
dw 0
ds 64
StackTask2Top:
dw Task2
BGROW equ 0
ENDROW equ 1
BGCOL equ 2
ENDCOL equ 3
SCRROW equ 4
SCRCOL equ 5
SCRPOS equ 6
DataTask1:
db 0,11,0,31 ;min row, max row, min column, max column. Window of (0,0,31,11)
db 0,0 ;current row,col position
dw 0 ;screen memory address of such position
DataTask2:
db 12,22,2,28 ;Text window (12,2,22,28)
db 12,2
dw 0
end Main
Interrupt Mode 2 is not a ZX Spectrum feature, it's a feature of the Zilog Z80 CPU itself.
Per Raffzahn@'s comment, the answer to your question kinda depends on what kind of threading you're talking about.
There are generally two levels of abstraction when it comes to multithreading, and two meanings of the term. First is the hardware level Simultaneous Multi-Threading (SMT) - a feature of the superscalar CPUs that allows multiple instructions to be executed in parallel. Intel calls its implementation of this feature in their CPUs like Pentium 4 HT and Core i7 'Hyper-Threading'. Second is the software level multi-threading - a model of concurrent execution without true parallelism, but that makes it appear to a user that things are executed at the same time by quickly switching back and forth between multiple threads of execution. This is what all multitasking operating systems implement, even on CPUs that do support SMT and have multiple cores, because generally there are way more processes and threads that need to be executed than there are execution units, CPU cores, and CPU sockets in a computer system. On the systems with only scalar CPUs the second variant of multi-threading is the only available option.
Now to go back to Z80 and Spectrum - since Z80 is a scalar CPU, the first kind of multi-threading, the true simultaneous one, with parallel execution of the instructions, is not possible. But the second kind, where threads execute concurrently, but not at the same time in parallel, is definitely possible. You don't necessarily need memory protection for this, all you need for pre-emptive multitasking is interrupt mechanism (the implication being even without interrupt mechanism it should still be possible to implement cooperative multitasking like in Windows 1.x/2.x/3.x and classic macOS). In fact, there is an operating system called SymbOS that supports preemptive multitasking on machines like Amstrad CPC and MSX2 family. Those machines use Z80 CPU and lack memory protection just like ZX Spectrum. The main problem with running SymbOS on original ZX Spectrum is that the OS needs at least 128KiB RAM and currently doesn't support all of Spectrum's hardware. NOTE: the difference between threads and processes is in memory sharing, not scheduling and execution, so for this question I use them somewhat interchangeably.
The specifics of interrupt mechanism, registers, etc., mostly determine not whether it is possible to implement multitasking/multithreading, but how easy or difficult it is. E.g., the register set and memory model of Z80 are limited enough to make C compiler implementation more difficult, and the resulting compiled code to be much less efficient than hand-written assembly, but it has been done, so it's definitely not impossible to do so. In general, you can find plenty examples of programmers achieving many great things on a very limited hardware, it's more a question of persistence and ingenuity than possibility. And a cost/benefit tradeoff.
All of the popular, early 8-bit CPU's support hardware and software interrupts. Therefore, they can all theoretically support preemptive multitasking, which I think is what this question is really asking.
The relevant article from Wikipedia states:
In simple terms: Preemptive multitasking involves the use of an interrupt mechanism which suspends the currently executing process and invokes a scheduler to determine which process should execute next. Therefore, all processes will get some amount of CPU time at any given time.
Of course, more advanced CPU's, such as the i8086/88 and the Motorola 68000, had additional architectural features to help OS developers to implement a practical multitasking environment. The solutions for 8-bit uP (there have been many valiant attempts) tend to be proof-of-concept novelties. The task switching performance and memory constraints usually make it too slow to be practical for real-world application usage.
A notable exception to the above is OS-9, which provides a quite functional Unix-like multitasking OS for the 6809 microprocessor. It can work with only 64KiB, but excels on a system with more memory. OS-9 is the main reason that Tandy Color Computer 3 users frequently expanded these 8-bit systems to 512KiB in the late 1980's.
Yes it is.
Any processor is capable of multithreading if software is written to implement multithreading.
Of course the Spectrum OS doesn't support it. You would have to replace the OS with a multithreading executive of your own. This is not as complex as you might think if you are only attempting to produce a small controller of some sort. You could treat this as a challenge to learn how multithreading works.
Note that multithreading is NOT the same as multitasking. So references to implementing multitasking on the Spectrum will not help you. Similarly, you only need a single processor. I suspect the Spectrum is a good machine to try this on. Although I've never tried it on the Spectrum myself, I've done it on other machines of that era.