RTM-Z80: Retro Tiny Multitasking System for Z80 Based Computers
github.com
github.com
Depending on how you count, the 8080 has a few dozen instructions: https://dercuano.github.io/notes/8080-opcode-map.html#addtoc...
So a new multitasking OS for the Z80 is pretty great!
It has a few big disadvantages. Although you can multibank, there's no way to directly address more than 64K, which is a real pain. Even the Z80 is not well suited to languages where functions are recursive by default, like C, and the 8080 is even worse at that. There's no memory protection, not even an MPU, so there's no fault isolation and no virtual memory. And the Z80 chips I've found are ridiculously power-hungry, like, 500 mA, and still run slower than an AVR.
The later chips in the 8-bit family are much more power efficient; I built an eZ80 board whose total power budget is 500mA including SD card and Ethernet module. It runs at 50MHz, but performance isn't really the reason we muck around with 8-bit CPUs, right? :)
Performance can substantially ease the difficulties involved with mucking around with 8-bit CPUs.
As I explained in https://news.ycombinator.com/item?id=30017810, my point of comparison is the Ambiq Apollo3, which purportedly gets about 100 Z80s of performance on about half a milliwatt.
It's likely a bit less than that over USB Vbus but it's just running a linear regulator down to 3v3 so mostly the difference just becomes heat.
The STM32 line is my preference for performant parts - there's an STM32L0 on the board doing USB to serial. I could likely emulate the eZ80 on that faster than the real thing runs, but I couldn't build an 8-bit bus computer with it.
I mean, it's a little goofy, because a lot of the Z80 design was a response to design constraints that no longer exist, right? You don't need to fit your CPU in 40 pins or 10000 transistors anymore. And even the MuP21 probably would have been a better design under those constraints.
The Z84C00 at 6Mhz will also "only" use around 30mA, down to 10µA in standby. At 15MHz the Z84C00 uses something more like 80mA, though, so the "turbo" mode in that calculator line probably chews through batteries.
I expect TI still uses the eZ80 CPU for the same reason other companies still use Z80-based processors - backwards compatibility. The Z80's got a very long life in industrial control systems, where you want to avoid rewriting the code if you reasonably can.
The eZ80 is about as modernised as you can take the Z80 core, but as you say a lot of the Z80 design is due to the constraints of the era. It was also designed to be code compatible with the Intel 8080, and probably would have just quietly fizzled on launch if it didn't have that.
CP/M's solution to this is to start the code at near-zero (0x100). This way forces the OS, drivers and hardware out of the way and permits binaries from one system to at least possibly run on another.
More interestingly, I never understood why code was at $0100 in CP/M land. I just got a Z80 card for my Apple 6502 based retro computer. I have never used CP/M and the availability of fairly sophisticated software interests me.
That sort of constrains the load address to be something in the range 0x41-0x200 or so. I'm not sure why they ended up at 0x100 instead of, say, 0x80 or 0xC0.
This setup was a big problem for the TRS-80, which mapped ROM at the bottom of memory.
The toolbox on the //GS may have also used handles.
The extreme case of "how many relocations you have" is zero, which is position-independent code. The 8080 inconveniently uses absolute addresses rather than relative addresses in its call and jump instructions and doesn't have a PC-relative data addressing mode (or really much in the way of addressing modes at all; the Z80 is slightly better here). Lacking a PC-relative mode for one or the other means some base pointer register somewhere has to tell the module where it lives. On the 8080 or Z80 this would make your code super slow and bloated, particularly since statically allocating addresses to your data is such an important optimization there.
An intermediate case is "the linker relocates a global offset table". Without any kind of base address register this requires each GOT entry to be at a fixed memory address, so it has to be globally allocated across all the modules you're ever going to have loaded: one entry for every subroutine or global variable. Every call and global variable reference has to indirect through the GOT. For global variables this is also super painful on the 8080 because of the lack of indexed addressing. A nice plus, though, is that it makes it easy to unload a module, as long as the module isn't on the call stack or otherwise has its addresses stored someplace. This is useful both for having more code available to run than you have memory for, and for dynamically reloading code as you modify it.
The other extreme is the usual case for LINK.COM or ld(1): you have one relocation per callsite or global variable reference, so linking takes a long time, but there's no per-variable-reference or per-callsite overhead.
There's a similar set of options for when the linking happens. You can allocate all the static addresses during compilation, which avoids the need for a separate linking stage but also makes separate compilation impossible, which is pretty much a nonstarter for self-hosted development on 8-bit systems. You can allocate the static addresses when you build an executable, which is the standard LINK.COM/ld(1) approach. You can allocate them when the program starts, which is the current ld.so approach, and requires the linker to run at program startup. (You might automatically cache the thus-relocated version of each module so that future loads at the same address could be instant, but the floppies on 8-bit micros were both too small and too slow for this to be practical.) Or you can allocate them when the first reference to them is made, by initially pointing all the GOT entries at a trampoline function that passes control to the dynamic linker to resolve just that single GOT entry, which requires the linker to run throughout the lifetime of your program. (In theory you could do this without a GOT on a per-callsite basis, but I've never heard of anyone doing that.)
A scheme more common than runtime linking on 8-bit computers (and on pre-IBM-360 mainframes) was "overlays," which allowed you to have more code in your program than would fit in RAM, without slowing it down. You partition your program into a central core, which was always loaded, and two or more overlays, only one of which was loaded at a time. The overlays couldn't call each other, but they could call within themselves and to the central core with no overhead. When the main program wanted to call an overlay, it would have to first make sure that the overlay it was calling was the one that was currently loaded, and if not, load it from disk, which typically took several seconds. And overlay schemes could get more elaborate, with the possibility of having multiple overlays loaded at once, direct calls between overlays, and so forth. If you had multibanked memory you could load different overlays into different memory banks, and switching between banks was a lot faster than loading from floppy, but it wasn't nearly as convenient as just having a bigger memory space, even in the shitty 8086 segment:offset fashion.
I've been thinking a lot about reviving some of these recondite techniques for low-power computing. The Ambiq Apollo3 ARM Cortex-M4F chip costs US$5 and has 1MB Flash and 384K SRAM, and uses 33 pJ/instruction plus 68 μA overhead at 3.3 V. So you could run a 10-DMIPS "workstation", a hundred times as fast as a Z80 and comparable to a SPARCStation 2 or a 386/40, on 0.6 milliwatts, which is about the same as a solar calculator, and you can burst to much higher speeds (48 MHz, about 60 DMIPS) or run there all the time when you have direct sunlight. Another 0.05-0.2 milliwatts pays for a 400×240 SHARP LS027B7DH01A memory LCD. But 384K of RAM is pretty limiting, so you need some kind of virtual memory or overlay scheme. It doesn't have an MMU, but I think its MPU avoids the need for a trusted compiler scheme like HotSpot to prevent different tasks from corrupting each other. And of course ARM is good at indexed addressing, has lots of registers, and has position-independent jumps and calls by default.
https://www.eembc.org/ulpmark/ulp-cp/scores.php
(click on the headers to sort..)
I think Apollo is the most general purpose of these chips, even though it no longer gets the top score. I assume the RSL10 is using its DSP, and the R7F0E01182CFP is Cortex-M0+ (no floating).
I used it for a project: the peripherals are really nice. For example, the I2C slave uses a shared memory design instead of waking up the CPU to receive anything.
Maybe https://www.eembc.org/ulpmark/ulp-cm/scores.php is the actual power consumption used for computation (or rather vice versa: "number of CoreMark iterations a device can execute per milli-Joule" according to), which doesn't seem to include the Ambiq chips yet. I have the impression that a CoreMark is typically about 2 Dhrystone MIPS, which is not dimensionally compatible with the above.
There have been a couple of experimental processors on which professors have published papers claiming an order of magnitude lower power consumption than Ambiq claims for their chips. Assuming that's true, I don't know how much of that is a question of process variability (e.g., maybe 5% of Ambiq's chips also get such low power consumption) and how much is a question of other engineering compromises (e.g., maybe ARM is a particularly power-hungry architecture, as the lower power consumption of the GD32VF chips suggests, or maybe the experimental chips only worked between 47° and 47.5° and even then they crashed once an hour).
By "really low" do you mean active power more like 1 microwatt, 10 microwatts, 100 microwatts, or a milliwatt?
What I liked is that all peripherals were "low power"- all of them could work when the CPU was off and all could wake the CPU (well it woke on any pending interrupt). So it means it's more likely that the CPU is asleep. With STM32L, only peripherals designated low power would work while the CPU was off.
But actually that number is totally in keeping with what I'd gotten from the Apollo3 datasheet: 33 pJ per Dhrystone "instruction" at 48 MHz and 1.25 DMIPS/MHz is 1.98 mW, and then there's 0.22 mW overhead, so we'd expect about 2.2 mW. 3 is close enough, especially for an earlier-generation chip. You might get an STM32L or an ATMega328 down to a milliwatt or two in run mode, but what (theoretically!) distinguishes the Ambiq chips is not their maximum power draw, but their efficiency per instruction and their deep power down load, which is what determines their power draw at a given computational load.
Limited low-power peripherals can be a huge headache, yeah.
Thank you very much for sharing your knowledge!
org 100h # ;)> My name is Ladislau Szilagyi, I'm 67 and I'm a big fan of Z80 retro computers.
> I just published on GitHub ( https://github.com/Laci1953/RTM-Z80 ) my RTM/Z80 project.
> RTM/Z80 is a multitasking kernel, built for Z80 based computers, written in Z80 assembly language, providing its users with an Application Programming Interface (API) accessible from programs written in the C language and the Z80 assembly language.
From the repo:
The user manual: https://raw.githubusercontent.com/Laci1953/RTM-Z80/main/RTM-...
> The RTM/Z80 project is intended to offer to the retro-computer hobbyists and to anyone willing to learn about multitasking systems the necessary resources needed to understand and learn the basics of this interesting but difficult area of software engineering.
The demo video: https://raw.githubusercontent.com/Laci1953/RTM-Z80/main/RTMD...