As mentioned in the intro of this little CP/M series, the CP/M hardware abstraction layer is provided in a “BIOS”. This is a system-specific section of code which interfaces the main part of CP/M with the actual hardware. It’s written in 8080 or Z80 assembly language.

Assembler

Assembly language is (almost) as close to the raw silicon of a CPU as you can get.

It’s not complicated, it’s just low-level. And you need to understand the CPU’s design.

Assembly language for the Z80 is considerably simpler to understand than for any of the modern CPU architectures. That’s just because the Z80 CPU design is so much simpler.

The Z80 CPU processor state is contained in the following register set (from Wikipedia):

All a CPU does, is to fetch “machine instructions” from memory, and perform the actions they describe. There are arithmetic instructions which operate on the above registers, for example. Others read or write memory locations. Yet others alter the Program Counter (PC), which points to where to fetch next instruction from. Everything a Z80 can do is done through sequences of these very basic steps. It’s all a matter of creating layer upon layer of functionality, until you end up with something meaningful, such as a word processor, a spreadheet, or a compiler. At the core it’s machine instructions. Always.

The BIOS

CP/M creates a foundation, providing “system calls” which all applications can use. Tasks such as sending or receiving text via the console, but also reading and writing files on a disk (which is a major abstraction in itself, of course). And it all starts with the CP/M BIOS.

The BIOS sits permanently in memory. It’s located at the top of the available 64K RAM. This code must have a strictly-defined layout: first a jump table, followed by the BIOS code and room for variables and buffers used by the BIOS. All the higher-level parts of CP/M care about is the location of that jump table. It defines the primitives in terms of which the core of CP/M can do its work: send/receive a console character, select a disk drive, read/write a specific disk sector, and a few more such functions.

The complete BIOS is available here. It’s written using the Z80 mnemonic conventions.

With assembler being such a low-level, it takes almost 200 lines of source code to fully implement this BIOS. Even though it does almost nothing - the resulting machine code (more on how to generate it later) is less than 300 bytes long. The BIOS is located at addresses 0xFE00 through 0xFFFF in the Z80’s emulated RAM address space.

CP/M’s memory use is dictated by the location of the BIOS. Since this starts at address 0xFE00, the core of CP/M will be placed just below there, i.e. starting at address 0xE800. The memory map for this first CP/M implementation will be as follows:

From To Contents
0x0000 0x00FF CP/M low-memory, BIOS/BDOS jumps, etc
0x0100 0xEFFF Transient Program Area (TPA)
0xE800 0xEFFF CP/M Command Processor (CCP)
0xF000 0xFDFF Basic Disk Operating System (BDOS)
0xFE00 0xFFFF Basic I/O System (BIOS)

There is some overlap: the CCP is not present when a user application is running.

It’s all virtual

With emulation, the “hardware” interface seen by the (virtual) Z80 CPU is fake: it’s never directly accessing real hardware (or an “outside world” for that matter). The Z80 “sees” what the emulator wants it to see - no more, no less. Every I/O machine instruction is turned into a C++ call to the systemCall() definition (in src/main.cpp).

This means that the emulator can implement the I/O model which is most convenient to keep the BIOS implementation simple. This has been used to great advantage here and leads to a truly minimal BIOS. For example: reading N disk sectors is one I/O instruction in our setup, whereas it would be a whole slew of Z80 I/O operations when dealing with a real floppy controller chip and its track seek timing, sector matching, and data transfers.

The z80emu emulator intercepts all “IN” machine instructions, and treats them as system calls. The code for this is in src/main.cpp, in systemCall() - here is an extract:

void systemCall (Context* ctx, int req) {
    Z80_STATE* state = &(ctx->state);
    switch (req) {
        case 0: // coninst
            A = console.readable() ? 0xFF : 0x00;
            break;
        case 1: // conin
            A = console.getc();
            break;
        case 2: // conout
            console.putc(C);
            break;
        case 3: // constr
            for (uint16_t i = DE; *mapMem(ctx, i) != 0; ++i)
                console.putc(*mapMem(ctx, i));
            break;
        case 4:  // read/write
            //  ld a,(sekdrv)
            //  ld b,1 ; +128 for write
            //  ld de,(seksat)
            //  ld hl,(dmaadr)
            //  in a,(4)
            //  ret
            [...]
            break;
        default:
            printf("syscall %d @ %04x ?\n", req, state->pc);
            while (1) {}
    }
}

When examining the BIOS source code, it should become clear how the requests made from the Z80 side end up in the systemCall() routine, and how each one is processed.

The irony being, of course, that all those primitive Z80 instructions are being processed by sophisticated C++ code on a modern 32-bit ARM µC. The crazy world of “virtual” retro!