Skip to content

SIO Command Protocol

Peter Wilson edited this page Dec 6, 2023 · 8 revisions

In my own development I make good use of cross assemblers on a host computer (generally ZMAC on my Mac). To automate the process of downloading code during frequent edit/test cycles I make use of a simple data transfer protocol using port B on the Z80 SIO. In my case this connects to a Raspberry Pi that runs as a slave. The Z80 system operates as a master in this configuration and sends commands to the Pi for data. Data can be read and written using this protocol. Before I'd added SDCard support I used the Pi as a mass storage device and could emulate a disk.

My Raspberry Pi slave implementation is available in this repo.

My Z80 implementation of the core protocol is part of ZLib (libcmd.asm).

The protocol is not particularly sophisticated and the two sides can very occassionally get out of sync and need reset of the Z80. This happens infrequently enough that I've not looked into improving it.

Assuming the standard 14.74MHz system clock the SIO is configured to operate at 460,800bps (57KB/sec) so data transfer is very fast.

The Z80 side library blocks and polls for data from the Pi rather than use interrupts. In this mode I've not had to add any hardware flow control.

Protocol

Command from the Z80

Each transfer from the Z80 to the slave starts with a five byte header. The first two bytes are an introductory 0x55, 0xAA. The slave ignores all data until this sequence is seen.

The next byte is the command code - what the Z80 wants to do.

The final two bytes are the number of bytes in the body part of the payload (i.e. excluding the five bytes in the header and the checksum at the end). The body length can be zero.

If the body length in the header is non-zero then the header is followed by the number of bytes specified in the header. Finally a checksum byte is sent after the payload (if there is no payload then there is no checksum byte - the checksum only applies to the content of the body).

The checksum is very simply the sum of all the bytes in the payload, modulo 256.

Header Body
0x55 0xAA CMD Length LSB Length MSB ...Data... Checksum

Note: Even though the length is allowed to be 16 bits in the protocol, the commands currently implemented never transfer a payload of more than 128 bytes.

Response to Z80

The response expected by the Z80 is similar in structure to the request.

The header in this case comprises 6 bytes. In this case the first two 'sync' characters are 0x55, 0xCC.

The sync characters are followed by the command code. This must match the command code in the request. If it isn't then the Z80 will ignore the response and keep looking for another sync sequence.

The next byte is the response code to the request. Acceptable response codes are determined by the command requested however by convention a value of zero (0) indicates success and any other value is an application specific error code and the request failed.

The response is followed by the payload length, which is again two bytes, LSB first.

The payload structure is the same as in the request - a number of bytes as determined by the payload length followed by a checksum.

Header Body
0x55 0xCC CMD Response code* Length LSB Length MSB ...Payload... Checksum

Note [1]: Even though the length is allowed to be 16 bits in the protocol, the commands currently implemented never transfer a payload of more than 128 bytes.

Note [2]: A response code of zero (0) indicates success. Any other value is an application specific error code in which case the command failed.

Commands

The following commands are currently defined.

Command Name Description
0x10 Open File The payload is the name of the file to open. No data is transferred in this request.
0x11 Read Block Read the next block from the open file. Each block is a maximum of 128 bytes.
0x81 Read Sector Together these commands provide all the functionality required to implement a block storage device. This has been used to implement a drive suitable for installing and running CP/M. Implementing a block device suitable for running CP/M on the Z80 was the initial target of this protocol.
0x82 Set Write Sector
0x83 Write Sector

Reading file data

Open File (0x10)

The payload is the name of the file to open. It is up to the slave to determine where this file resides. Generally I Use a specific directory on the slave device to store downloadable files. The slave code implement paths and other enhancements, although I dont do this in my Raspberry Pi implementation.

Header Body
Sync CMD Length LSB Length MSB File name (not null terminated) Checksum
0x55 0xAA 0x10 0x08 0x00 t e s t . h e x XX

The slave attempts to open the named file. Only one file is ever open at a time and files are closed when either the last block is transferred or another file open request is received.

The file is transferred "as-is" - i.e. as a binary file. No modifications to the file are made.

The response to this request is a success/failure code with no payload.

Header
Sync CMD Result Length LSB Length MSB
0x55 0xCC 0x10 zero (0) success, !0 can't open 0x00 0x00

Read Block (0x11)

Having opened a file the Z80 will read data a block at a time until the file is exhausted. Each block is 128 bytes except the last block.

Header
Sync CMD 0x00 0x00
0x55 0xAA 0x11 0x00 0x00

The response to this request contains the next block from the currently open file. If there is no open file then a header only is returned with an error code (no payload). If there is a payload then the the response code is 0 to indicate there are more blocks to read after this one, or non-zero to indicate this is the last block.

Header Body
Sync CMD Response Length LSB Length MSB Data from the file Checksum
0x55 0xCC 0x11 0=OK, !0=EOF LL HH 0-128 bytes of data XX

File system emulation

The three command, 0x81-0x83, allow the slave system to provide access to a block level storage suitable for emulating a Z80 local 'disk'. This was the original inspiration and use of this protocol before I added SDCard support. I'm not currently making use of these commands anywhere in the ZLoader system although the libraries still support this mode of operation. It's something I'll likely add back at some point in the future.

Where/how the data is stored on the slave system is entirely up to the implementation on that slave. In my Raspberry Pi implementation (in JavaScript using Node.js) I use a file called "cpmdisk.img" for the storage.

In each of these commands a 'block' is 128 bytes, matching the standard for CP/M.

A block is addressed using three values:

Name Size Description
disk number 8 bits For emulating more than one disk.
track 16 bits Track number
sector 8 bits Sector number

The slave can interpret these values and the ranges in any way it wants. My implementation used 250 sectors per track and 160 tracks per disk to give a virtual disk size of 160KB.

In my node implementation persistent storage is provided in a file calls cpmdisk.img in the current working directory. This file will be as big as necessary. The first 160KB will contain disk 0, followed by disk 1 etc.

Read Sector (0x81)

Read a single 128 byte sector.

Header Body
Sync CMD Length LSB Length MSB Sector Address Checksum
0x55 0xAA 0x81 0x04 0x00 Disk Track LSB Track MSB Sector XX

The response to this request will return either an error if the address is invalid or the 128 bytes requested. The response format is:

Header Body
Sync CMD Response Length LSB Length MSB Data from the file Checksum
0x55 0xCC 0x81 0=OK, !0=Error LL HH 128 bytes of data XX

*Note: if the response code is not zero then the request failed and there will be no body (just the header).

Set Write Sector (0x82)

This request sends the address on the virtual disk to which the next write request (CMD: 0x83) should be written.

Header Body
Sync CMD Length LSB Length MSB Sector Address Checksum
0x55 0xAA 0x82 0x04 0x00 Disk Track LSB Track MSB Sector XX

The response is a simple acknowledgement, with optional error if the provided address is invalid.

Header
Sync CMD Response Length LSB Length MSB
0x55 0xCC 0x82 0=OK, !0=Error 0 0

Write Sector (0x83)

Write a 128 byte sector to the virtual disk at the address previously selected by a "Set Write Sector" operation (CMD: 0x82).

The request from the Z80 includes the 128 bytes to be written as the payload.

Header Body
Sync CMD Length LSB Length MSB Data Checksum
0x55 0xAA 0x83 0x80 0x00 128 bytes of data to be written to the virtual disk XX

The response contains no body and the response code indicates success (zero) or failure (non-zero) of the write request.

Header
Sync CMD Response Length LSB Length MSB
0x55 0xCC 0x83 0=OK, !0=Error 0 0