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!