On BIOS calls for Z80 systems

Published: August 02, 2015
Tags: 8-bit homebrew computing retrocomputing z80

After a lot of quite successful and quite entertaining early playing around with my homebrew Z80 system, I recently decided to knuckle down and rewrite the ROM code "properly". By this I mean structuring it so that all direct interaction with the hardware happens through a small set of clearly defined and general purpose functions, and all other code works by using these functions. The functions will also be available to user programs, so they basically define a low-level programming interface for the machine. I call such functions "BIOS functions"; I think that, strictly speaking, BIOS is a term specific to the IBM PC, but it has become genericised and I don't know of a better term, so BIOS functions it is. This post will consider the smartest way to call these functions.

Despite the special role they play, BIOS functions are still just plain old functions, and it's perfectly cromulent to call them using the CALL instruction. All you need to do is set up an include file which uses your assembler's equ directive to define easy to remember names for the relevant entry addresses, and you can CALL away as needed. Each BIOS call will require three bytes of machine code and execute in 17 clock cycles. This is the simplest thing that could possibly work, but it's arguably too simple.

The main downside to this approach is that if you update your BIOS code and any function ends up being just one byte shorter or longer than it was before, the entry address for all subsequent functions gets shifted. This means software assembled for the old BIOS version will not work on the new version. For a long term, constantly changing, never-really-finished hobbyist project, this is a pretty massive inconvenience. It perhaps would not have been such a big problem for a home computer manufacturer in the 1980s, where the BIOS most likely would have been aggressively debugged by engineers for months and then released in millions of machines which would never receive firwmare updates. There'd still be a downside, though, in that it would be very difficult to write a backwards-compatible BIOS for your next machine.

The way around all of this inflexibility is to insert a layer of indirection by using a so-called "jump table" of JP instructions. To call a particular function, you might, say, CALL 0042h, and at address 0042 is the instruction JP 1234h. Address 1234 is where the actual implementation of the function is. If a future revision of the BIOS changes the entry address, all you need to do is change the jump instruction at 0042 to point to the new address, and existing software will continue to work without being reassembled because 0042 is still a valid entry point. The price for this convenience is that the jump table consumes three bytes of ROM space for every function, and after the initial 17 clock cycles to CALL into the table, a further 10 are used on the JP instruction, making the function call almost 60% slower than the naive. This is the design approach that CP/M used: the CP/M BIOS begins with a table of 16 jump instructions, which point to implementations of the 16 BIOS functions.

It's possible to get smarter yet, making use of the Z80's RST instruction. RST is a bit of an oddity; There are, in fact, 8 RST instructions: RST 00h, RST 08h, RST 10h, RST 18h, RST 20h, RST 28h, RST 30h and RST 38h. In terms of what they cause the processor to do, each one is absolutely identical to a CALL instruction with an argument of 0000h, 0008h, 0010h, 0018h, etc. The difference is that while a CALL requires three bytes of machine code and 17 clock cycles to complete, a RST instruction requires just one byte of machine code and completes in 11 cycles - just one cycle slower than a JP, which doesn't touch the stack at all. RST is basically a "small and fast" CALL to one of eight special memory locations. The reason this odd instruction exists comes from the Z80's compatibility with the Intel 8080. The 8080 had only a single interrupt mode, which involved reading and executing a single instruction off the data bus from the interrupting peripheral (similar to the Z80's interrupt mode 0). The RST instruction is naturally very useful for this, although the Z80's interrupt mode 2 is more powerful.

The MSX BIOS takes very careful and full advantage of the RST instruction. It features a jump table which begins at address 0000h. Rather than putting the 3 byte jump instructions immediately one after the other, ending up with entry address of 0000h, 0003h, 0006h, 0009h, etc., one byte of padding is placed between each jump. This means that the entry addresses are 0000h, 0004h, 0008h, 000Ch, etc. Notice that the 1st, 3rd, 5th, 7th, etc. addresses now line up the available RST addresses. This means that eight of the BIOS functions can be called using the appropriate RST, making those functions cheaper to call than the others. Compared to the CP/M approach, at first glance it seems that MSX uses more ROM space because the jump table is larger by one byte for every function due to the padding. However, RSTs are not possible beyond address 0038h, and indeed once this point has been passed the MSX BIOS stops including padding bytes so that JP instructions are spaced three bytes apart. This means the total cost for the approach is just an extra 16 bytes of ROM space. Furthermore, thanks to using this approach, eight BIOS functions can now be called with 1 instead of three bytes of machine code, saving you two bytes per call. If each of the RST-compatible functions is used exactly once in the ROM (and of course each one is, otherwise it wouldn't exist!) then you break even on space. It's almost certain that some functions are called more than once, so in reality the RST savings will easily save you more than the 16 padding bytes they cost. This approach is thus both faster and more compact than the CP/M approach: a rare engineering free lunch. The fact that the MSX BIOS uses this approach shows that it was carefully designed. I would be very interested to know if the functions chosen for RST-compatible addresses are exactly the most frequently called ones elsewhere in the ROM, so as to maximise the space saving. This is the approach I will take for my own homebrew Z80 system, as I only have 8kb of ROM to work with.

Another question which arises is how to pass arguments to these BIOS function calls. There's less room to be clever here. The choices are to either assign them to particular registers, or to push them in a particular order onto the stack. The stack approach has the advantages that you can pass as many arguments as you like (or rather, as your memory will allow, but this is not a practical limitation), and also that you can call BIOS functions from inside BIOS functions without any problem (this is why e.g. C compilers for the Z80 pass function arguments via the stack). However, this approach is not cheap: each PUSH of arguments takes 11 clock cycles, and each corresponding POP inside the function itself takes 10. At 22 clock cycles per argument, the 6 clock cycles per function call that are saved by using RST instead of CALL are quickly lost again. Passing via the registers is a lot faster: 7 clock cycles to LD an 8-bit value into a register (10 for a 16-bit value) before making the BIOS call, and no cost at all once you're inside. Unsurprisingly, both CP/M and MSX use this approach, and so will I.

Feeds
Archives
Top tags