Time to boot it up! A continuation of the Z180 Retrocomputer project. Here, we look at the Z180 MMU behaviour, initialising the system and writing a small bootloader. See also Z180 Retrocomputer: Design for more about the design and construction of the system.
Most of these programs will be written in assembly. While any Z180 assembler would be ok, I’m going to use zasm. The macro/directive syntax might be zasm-specific. The full bootloader code is available on Github.
As the Z180 boots, it will begin executing code from physical address
0x0000 – which is the start of the mapping for the ROM. So we will need to write some code to the ROM that will be run as soon as the device boots.
The Z180 has a 1MB physical address space. In our system, ROM is mapped to the first 512kB (repeated every 32kB), and the RAM is located in the second 512kB. The CPU, however, only has a 64kB logical/virtual address space. This limitation is addressed (pun intended) through common and bank areas. The logical to physical translation can be congfigured to allow for different memory setups:
I don’t plan on using the ROM for anything besides this small bootloader. Eventually I would like unmap it, so the whole logical address space can be used as RAM. At this stage the bootloader is being executed from ROM. In stages, we can map the RAM in also, copy the bootloader to RAM, continue executing from RAM, and finally map the ROM out.
|Step||Bank||Virtual Address||Physical Address|
|Boot||Bank||0x0000-0xffff||0x0000-0xffff (32KB ROM, repeated)|
|Initialisation||Common 1||0x8000-0xffff||0x80000-0x87fff (first 32KB RAM)|
|Bank||0x0000-0x7fff||0x0000-0x7fff (32KB ROM)|
|Post-initialisation||Common 1||0x8000-0xffff||0x80000-0x87fff (first 32KB RAM)|
|Bank||0x0000-0x7fff||0x0000-0x7fff (first 32KB RAM)|
In both initialisation and post-initialisation, the second configuration in the diagram (bank + common area 1) is used. This gets the CPU into the ‘initialisation’ state:
init_mmu: ld a, 10000000b ; CBR1@0x8000, BANK@0x0000 out0 (CBAR), a ld a, 10000000b - 1000b ; CBR1 maps to second 512 out0 (CBR), a
To get the serial port working, the Asynchronous Serial Communication Interface (ASCI) needs to be configured. The USB-serial adaptor is connected to channel 0. In control register
CR0A, we enable the transmitter & receiver, and set the mode to 8 bit data, 1 stop bit, 0 parity. In
CR0B we set the general divide ratio to be φ ÷ 160. The clock speed is φ = 3.072MHz (half the crystal frequency) so the baudrate becomes 19200.
The stack pointer is initalised to a high part of RAM (stack grows downwards). Some test messages are printed useing subroutines to see if the stack is working. This also tests if RAM is mapped correctly to the stack area.
Leap of Faith
We can use the
ldir instruction to copy a chunk of memory containing this bootloader across to RAM:
; copy ourselves from ROM -> RAM ld bc, 04000h ; count ld hl, 00000h ; src ld de, 08000h ; dst ldir
In the initialisation and post-initialisation mappings, the logical addresses of the programs should be the same, despite the programs being stored physically in separate places. If we change the base of the bank section to the start of RAM, the program should ‘leap’ and execute any further instructions from RAM instead of ROM.
; leap of faith, currently executing from ROM ld a, 10000000b ; bank starts at second half of physical memory out0 (BBR), a ; write it to the bank base register ; anything hereafter is executed from RAM!
Running from ROM is a bit cumbersome, due to the need to physically replace and reprogram the IC with each update. This makes it pretty hard to debug too. Instead, we will write a small bootloader that loads a program via the serial port into memory that we can execute.
A simple format for representing binary data is the Intel hex format. It is widely used for programming microcontrollers and ROM memory, for example. What makes it particularly useful also is the data is just a stream of ASCII characters with a simple structure – allowing it to be easily parsed and transferred via a serial link. zasm can produce its output directly in Intel Hex format.
A file consists of multiple lines of text, each line corresponding to a record:
Structure :data byte countaddressrecord typedatachecksum\n
A record type of 00 indicates data, and 01 indicates end of tile as the final record. While there are others also, we will implement this subset.
As it’s a simple format, it’s not difficult to parse. The helper subroutines have been omitted here:
read_record: call in_char ; get a char cp ':' ; it should be a ':' jr nz, .error_reading ; otherwise it's an error call in_byte ; read the byte count ld b, a ; store byte count in b call in_short ; read the start address call in_byte ; read the record type cp 1 ; is it 1? jr z, .eof ; then it's the end of the file cp 0 ; is it NOT 0? jr nz, .error_reading ; then it's unsupported / error .data_record: ; it is 0, and a data record call in_byte ; get a byte ld (hl), a ; store byte inc hl ; inc out pointer djnz .data_record ; dec remaining bytes, loop while > 0 call in_byte ; consume checksum (is unchecked) call in_char ; get new line (assume single \n) jr read_record ; read next record .error_reading: ld hl, err_msg call puts jp read_intel_hex ; try again. .eof: call in_byte ; checksum (is unchecked) call in_char ; get new line (assume single \n) ld hl, done_msg call puts
We’ll assume that the test programs we write for now have an entry point at
0x1000. Once the program has finished loading, we can begin execution from there by calling
0x1000. If the program
rets nicely, we can try loading and running a new program immediately. This is useful for quickly making and testing new changes.
call dump_memory ; dump memory just loaded to debug call print_nl ld hl, hex_done_msg call puts call 0x1000 ; call the program just loaded jp read_intel_hex
Now we’re ready to load something!
In Part 3: We start writing and loading some programs for the system. Coming soon!
- change the post-initialisation mapping to use the full first 64kB of RAM
- this variant of the Z180 supports clock speeds at the same frequency as the crystal (instead of half). We can write to a control register to enable this.
- the baudrate is wrong in the message (19200 not 38400)
- Intel Hex bootloader: each line is allowed to end with a newline (\n), a carriage return (\r) or both. We only allow a single newline only here. zasm produces hex files with both (even under Linux) – so it fails to load it unless the endings are first convered. So would be helpful to update this sometime.