-
Notifications
You must be signed in to change notification settings - Fork 3
SIO Command Protocol
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.
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.
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.
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 |
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 |
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 |
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 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).
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 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 |