The last piece of the puzzle is to make all the parts line up and work together. This turns out to be quite involved. A large part is due to the “virtualness” of this whole CP/M setup:
- we need a way to make the Z80 do something meaningful on (virtual) power-up
- we need to get a system boot loader onto the (virtual) floppy disk, i.e. flash
- the CP/M code (including our freshly-minted BIOS) needs to be on the floppy disk
- the floppy disk needs to have the proper structure for a CP/M startup disk
- ideally, some basic CP/M utilites should also be present on this (virtual) disk
Fortunately, this problem is not new. Every “system implementor” who has built a CP/M system from scratch in the past, has had to deal with the same issue. Another good thing, is that there is a very detailed “CP/M Operating System Manual” with information about all the steps needed to “bring up” CP/M (a quick search should bring up several hits).
The standard layout for CP/M disks on 8" SSSD media, is to reserve the first (i.e. outer) two tracks for “The System”. Then comes the disk directory, then the actual file storage. These last two are fully managed by CP/M, but they still have to be initialised correctly on all fresh floppy disks (including virtual ones).
With 26 sectors per track and 128 bytes per sector, two tracks can hold 6.5 KB of data. The CP/M core (i.e. CCP+BDOS) is 5.5 KB. Our (tiny) BIOS is 0.5 KB. That leaves us at most 0.5 KB, i.e. 512 bytes (4 sectors) for storing bootstrap code to bring everything up.
There are different ways to implement the bootstrap mechanism. I’m going to use a very traditional 2-level approach:
- On power-up, the first sector of the first track of the first disk drive is loaded into memory at address zero, and control is passed to whatever code that contains.
- This code, which cannot exceed 128 bytes (1 disk sector), then loads all remaining sectors from the two system tracks into high memory and jumps to the BIOS start.
At this point, all of CP/M will be in high memory (CCP, BDOS, and BIOS), exactly where it should be, and the normal CP/M “cold boot” starts. This concludes the bootstrap process.
This is the very first code executed by the (virtual) Z80 after power-up. The question is: where does it come from and where should it be stored?
On a “real” Z80, the answer is: in ROM, starting at address 0x0000. Because that’s where the Z80 starts fetching its very first instructions. As part of the bootstrap, there must then be a way to disable the ROM and map low memory onto RAM.
On a virtual Z80, things are much simpler. We could copy some constant bytes into RAM before starting the emulation. But that’d be silly, as the code would then use a Z80 I/O instruction to load disk 0 / track 0 / sector 0 into low memory. We can sidestep all that and directly load the first (virtual) disk sector into RAM, in C++. Or, to be precise:
FlashWear disk; disk.init(); disk.read(0, memPtr(&context, 0x0000));
When the emulator starts, it will run the system bootstrap we just loaded from the disk.
So this is a second bootstrap. It needs to be very small since it must fit entirely within one disk sector. Furthermore, it cannot rely on any other code being present in RAM. That’s the point of bootstrapping: to load fresh code into RAM which takes control of the system.
Here is a simple system bootstrap in Z80 assembly language:
; Cold bootstrap for z80 emulator CCP equ 0E800h BIOS equ CCP + 1600h org 0000h boot: ld de,greet in a,(3) ; display greeting ld a,0 ; first drive ld b,48 ; read CCP + BDOS + BIOS into memory ld de,1 ; skip first sector on first track ld hl,CCP ; starting load address in a,(4) ; disk read request jp BIOS ; jump to bios cold start greet: db 13,10,'[JeeLabs Retro Z80]',13,10,0 ds 007Fh-$ db 0 ; so generated code is exactly 128 bytes end
Since the emulator provides a convenient multi-sector read system call via the
in a,(4) instruction, the size of this bootstrap code is only a few dozen
bytes. And because there’s plenty of room anyway, this bootstrap also prints a
little startup greeting on the console.
The startup disk
The proper layout of an 8" SSSD CP/M startup disk is as follows:
|track 0, sector 0||the above system bootstrap|
|track 0, sectors 1 .. 25||first part of the CP/M system|
|track 1, sectors 0 .. 22||second part of the CP/M system|
|track 1, sectors 23 .. 25||currently unused|
The disk must also have a valid “disk directory” area, even if there are no files on it yet:
|track 2, sectors 0 .. 15||filled with 0xE5 bytes|
The rest of the disk content is ignored. It will normally get overwritten before use.
All the parts have now been identified. We need a disk with a system bootstrap in the first sector, followed by the CCP, BDOS, and BIOS, and the disk must be set up to contain a valid directory. What has not been described yet, though, is how to generate these parts.
This is where some form of external help is required. We need to be able to run an assembler which takes the Z80 assembly code and turns it into binary code.
There are several ways to do this, such as:
- find a working CP/M setup, build your new system there (as well as the separate system loader), and figure out a way to transfer the resulting binary files back out
- use an existing CP/M emulator (such as simh or yaze, and build the files there
- get hold of one of many available “cross assemblers”, i.e. an assembler for Z80 mnemonics, which runs on a different system (DOS, Windows, MacOS, Linux, etc)
- assemble the code by hand, looking up all the codes (tedious, but it can be done)
- write your own assembler in your favourite language (it’s not as hard as it sounds)
I’ve used various approaches in the past, but usually 2) or 3) above are the easiest to do.
The last part of this hurdle is to get the data onto our virtual disk, i.e. into the proper location of the F407’s internal flash memory. This is not very different from getting the compiled emulator firmware itself into flash memory. We just need to figure out what goes where, and what the corresponding addresses are in flash.