Multitasking On The Humble Z80 CPU

Multitasking is something we take for granted these days. Just about every computer we use, from our desktops to our phones, is capable of multitasking. It might sound silly to implement multitasking on lower-spec machines from many decades ago, given their limited resources, but it can be done, as [bchiha] demonstrates on a Z80-based machine.

[bchiha] has achieved pre-emptive multitasking on the TEC-1G Z80 computer, a modern reimagining of the classic Talking Electronics TEC-1 from the 1980s.  The proof of concept code allows running up to eight separate tasks at once. Task switching runs on interrupts, triggered at approximately 50 Hz. When an interrupt fires, the CPU registers are transferred onto that task’s stack, and the next task’s stack is swapped to the stack pointer to allow execution of the new task to proceed. There is an overhead, of course, with [bchiha] noting that the task swapping routine itself takes about 430 clock cycles to run in between tasks.

Multitasking took some time to appear on home computers for good reason—it’s not very useful unless you have a machine with enough power to practically run multiple tasks at once. While a Z80 machine like this can do multitasking, you’d better hope each task is pretty tiny to avoid each individual task taking forever to run.

[bchiha] has made the simple multitasking code available on Github for the curious. We’ve featured multitasking work on other unconventional platforms before, too, like the Arduino Uno. Video after the break.

[Thanks to Stephen Walters for the tip!]

32 thoughts on “Multitasking On The Humble Z80 CPU

  1. there is another significant reason for multitasking – many tasks have to wait for things, and it is often easier to do that in a full task than an interrupt – particularly if your processor is connected to a lot of different things (ie I often have every pin of my esp32 doing something…). So many tasks might be sleeping much of the time..

    Even for the z80 this would be true..

  2. Z80 is plenty capable of doing some task switching. Older machines (such as the PDP series) first did batch processing, but multi tasking kernels were quickly developed so many users could use the single (and expensive) computer at the same time. I guess that’s also much more efficient then having the hardware idling while a single person is feeding in stacks of punch cards from different users. The history of how these things evolved is quite interesting.

    And tasks for the Z80 do not have to be that tiny. In the example here, all tasks probably use software loop delays to set a certain speed, and that is very bad (arduino like) practice. One of the main parts to make multi tasking efficient, is to use an event driven system. Then the CPU is mostly idling in an empty loop (somewhere in the OS) and a user program only gets cpu cycles if it has something to do (such as refreshing a display, or scanning a keyboard). It is also common for an RTOS to deliver timing services to the applications.

    Spending 40s to use the solver of an HP27S to calculate an RC constant is also quite silly. (And so is entering a whole lot of zero’s instead of using an exponent) Maybe a video such as this can help with getting people interested in task switching, but overall there are much better tutorials “out there”. I quite liked the introduction of using an RTOS that was (is?) part of FreeRTOS, but it’s been years since I read it. Apparently ChibiOS is a very small and well written RTOS, but I never used it.

    I mostly did my programming for AVR’s, and they are a bit annoying for the task switching part, as they have many registers that have to be saved. Applications are usually also so small that there is not much advantage for using an RTOS. When using an uC such as the ARM Cortex-M series, there is more incentive for using an RTOS. It has much more resources, including more memory and a multi level ISR controller that can be put to good use in an RTOS.

    An RTOS can also be used to make code better readable, and re-usable. But there is also the trap of writing more complex “generic” code for the sake of being able to reuse it in another project, while simpler & faster code that just does what is needed for an application is often a better compromise.

    1. Excellent mini-history of the RTOS, and +1 for the warning against premature generification.
      If I had a dollar for every line of code I’ve written with the intent for it to be reused in all future projects, only for it to languish as an overly complex one-off….

    2. If you actually watch the video you’ll see he did NOT use software delay loops but instead a 555 to interrupt the CPU to switch tasks.

      1. I think what paulvdh was saying is that his individual tasks were being timed by software delay loops, which caused them to slow down because these loops were paused when other tasks were running. He even says so in the video at 15:01.

    3. In fact, it seems like the Z-80 was intended to be multi-tasked. Being able to swap registers with a second bank in two instructions is pretty bad-ass for a 1970s MCU, and the utter simplicity of the system means there aren’t a ton of things that have to be swapped, just the general registers, the stack pointer, and the program counter. Of course, the dual register banks on the Z-80 were intended mainly for switching between the main program and an interrupt handler, and in the Z-80 as soon as an interrupt is recognized, the first thing that happens is that interrupts are disabled, so that one interrupt handler won’t be interrupted by another. The extra step of having separate stacks for each task becomes necessary when you have more than just a main program and one-at-a-time interrupt handlers.

      What he describes here is almost identical to what I understand FreeRTOS does, but it’s nice to have it described in detail and with visual aids. FreeRTOS goes a little further though, in allowing some tasks to be preempted (by that periodic timer interrupt), and others to use the CPU until they complete a task.

    4. Um, actually, in Arduino, all of the MCUs supported by Arduino have built-in timers, and the delay(n) function doesn’t use a software loop; it sets an interrupt to go off n milliseconds from now, and puts the MCU to sleep as it waits for the interrupt. That function is there to a certain extent to discourage users from setting up software delays, since these just waste power in the MCU. As a side benefit, delays are consistent across different MCUs and different clock frequencies.

    1. On the one hand, the smaller number of registers make context switching much lighter-weight than a Z80. On the other hand, the small hardware stack is a problem (though using (Indirect),Y for a 16 bit software stack works fine). A 65c816 would make an interesting target for multi-tasking, and would be relatively fast for an 8 bit CPU.

      1. I don’t know if that’s just a typo, but the 65c816 is 16-bit (noted by the 16 at the end), not 8-bit. So yes it would be fast compared to 8-bit CPUs! WDC made it to be kind of a 16-bit version of the 6502.

    1. Exactly so. I was developing and selling multi-user stock control systems on Comart and Cromemco Z-80 hardware running MP/M from ’79 onwards.

      Both supported >64KB memory cards, with bank switching built in. This allowed multiple users to be running with private memory of up to 48KB each, with 16KB of shared memory for MP/M itself.

      On that technology we ran stock control for the Van Allen dress shop chain at their Leicester headquarters and warehouse, RAF Henlow, Geest Industrial and a load of other then-major companies.

    1. Because it’s cool, and new to many readers who may not be familiar with either the TEC-1G, the Z80, or 8 bit CPUs in general.
      It’s always better to contribute something that some people (like me) enjoy than to be a self-appointed critic.

    2. AvRs have no MMU (memory manage unit) therefore you cannot do real multitasking, only fake it with state machines and pre-inserted return statements in your code. I’ve done embedded for as long as I am in industry so if you don’t know what your posting then don’t post.

      1. Nope. You are asserting that memory protection is the single thing that makes multitasking possible. Not that I would do it this way, but another alternative is to code all processes in a high-level language, then have them executed by an interpreter that handles security. So “you cannot do this” does not apply. And then, “pre-inserted return statements”??? Have you never heard of interrupts? But you do you, and stay out of the way of those who find solutions.

  3. Fun stuff! I remember doing this about 40 years ago to implement a print spooler on the TRS-80 Model III, which had a clock that generated the interrupts necessary for preemptive multitasking.

  4. This is cool. While any CPU that can save its full context and resources to hold a few contexts can be made to multi-task, that just wasn’t the go-to solution for most problems in the 8 bit era. It’s cool to watch and see how it can be done.

  5. I did exactly this for the Z80 on a 128K spectrum last year. The interrupt also included paging in each task’s own private 16K RAM at C000h to run 4 tasks each if which would utilise a quarter of the screen using a shared graphics library. Programs can also be loaded into each of the 4 task slots.
    Great project and actually runs really well.

    1. Wait. A multi-user SCREEN? What’s next, mult-user keyboards and mice? I do remember seeing something like this featured here on HaD a while back. Was that you?

    1. The dual register set was there to allow interrupts to be serviced very quickly. For multi-tasking you need to save them on stacks.

  6. just for fun & curiosity in 1979, i wrote a 6800 pre-emptive multitasker that also implemented semaphores running on exorcisor boards. irq rate (context switching) was 1khz. serial i/o was all interrupt driven. dozens of different machine control systens used it. migrated to 6809 & then 68000, 68008, 68020, 68030, 68040, & 68060. 68881/68882/fpu supported. hundreds of manufacturing & testing machines ran it. cool memories.

  7. Back in 1978 I wrote a pre-emptive 6800 multi tasker that also implemented semaphores. the context switching irq was at 1khz & all serial i/o was interrupt driven. Dozens of different machine control systems used the homebrew rtos. An AMD 9511 was also supported. It was migrated to the 6809 and then 68000, 68008, 68020/68881, 68030/68882, 68040, & 68060. The design was the same throughout, tight & fast. Hundreds of manufacturing & testing machines ran that code. Fun stuff.

    1. When I came on board in ’86 we used VRTX as our real-time tasker for the the 68xxx line. At the utility I work at today we still use the 68332 cpu for substation automation. Those systems are slowing being fazed out, but still going strong. Z-80 boards were still in use up until recently, and were interrupt driven. Ie. loop the main code, but I/O boards would ‘interrupt’ and be processed with associated millisecond timestamp. I did fix a few things in that assembly code, but never ‘wrote’ it.

  8. Hypercom payment terminals from the 90’s and early 2000’s were z80 based, with memory banking up to 1.5MB and round-robin multitasking, support for yielding your time slice. No limits in the number of tasks other than the available memory. I was lucky enough to work with those beasts for almost 8 years.

Leave a Reply to yardcoolie2017Cancel reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.