# Using SPI as You Would GPIB
This thought experiment is inspired, in part, by my work on the ForthBox:20AE project and by the Commodore 8-bit family's use of GPIB to talk to their storage devices.
Basically, I need a reliable and easy to implement RPC mechanism between the ForthBox computer and an arbitrary storage device.
I could always tie the ForthBox to a specific technology (e.g., like I did with my Kestrel-2 family, which were hard-wired to understand SD-cards and that's it; not even SDHC or SDXC were supported);
however, I see this as self-limiting.
I want something more.
So, one day, I had an idea come to me where I conceived a GPIB-like protocol that ran over plain-ol' SPI. Here's my attempt to describe it informally.
Does this mean I'll be using a GPIB-like protocol with ForthBox? Not necessarily!
As I write this blog article, I'm still researching and trying to estimate implementation difficulty of this method and many others.
But, it's good to document ideas as I get them; they may prove useful in the future.
If not for me, then for someone else!
(Just give credit, please!)
## Assumptions
Traditional SPI uses terms like "master" and "slave", which has been a cause of some consternation for some of my online associates.
For this reason, I decided to use the actual GPIB terms for various roles instead.
The host PC is connected to a device via a SPI interconnect.
The SPI interconnect consists of the following pins on the PC:
- CODI -- Controller Output, Device Input (basically, MOSI)
- CIDO -- Controller Input, Device Output (basically, MISO)
- CCLK -- Controller Clock
- DS# -- Device Select (active low, as is customary for SPI devices)
The host PC is always the controller. The attached device may become a listener or a talker at any time the controller desires, but only upon the behest of the controller.
We desire an OPEN/READ/WRITE/CLOSE protocol similar to Commodore's adaptation of GPIB, but corrected to allow all 31 channels and devices, instead of 15.
## Sending Data To the Device
Host asserts DS#, thus grabbing the attention of the device.
At this point, the device has no idea what to do.
The host then sends a LISTEN ($40) byte, followed by one of the following:
- $FF meaning no secondary address
- $60+n meaning secondary address n (0-30)
- $80+n meaning OPEN channel n (0-30)
- $A0+n meaning CLOSE channel n (0-30)
At this point, an optional block of data can be sent to the device, typically containing a filename for the OPEN bytes.
The data is encoded in a standard format, like so:
Length[2] ...data....
The length field is formatted as follows:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
,-----------------------------------------------------------------------------------------------.
| EOI | 0 | 0 | 0 | LENGTH |
`-----------------------------------------------------------------------------------------------'
Up to 4095 bytes of data can be sent as a contiguous block like this. The EOI flag is set if the last byte of this block is the last byte of the data stream.
For an OPEN command, the data which follows (up to the end of the data stream) indicates the filename to open.
Any data sent after the end of the filename will be ignored; this preserves the rule of one transaction per device select assertion.
Neither SECONDARY nor CLOSE accept a filename.
The device unlistens when its device select line is negated.
All the while this is happening, the device is throttling the host PC via a sequence of status bytes.
Bit 7 of each byte indicates whether the device is ready to receive more data (0) or not (1).
Another interpretation of this bit is the "retry" bit;
if set, the host PC must resend the most recently sent byte, as the device did not have the opportunity to receive it yet.
So, for example, if we wanted to write the string HELLO WORLD to a file named GREETINGS,
we can do so like so (note the throttling that happens between opening and writing the data payload):
| Time | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|-------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| Host | $40 | $81 | $09 | $80 | 'G' | 'R' | 'E' | 'E' | 'T' | 'I' | 'N' | 'G' | 'S' |
| Device | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 |
Transaction #1: the OPEN command.
| Time | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
|-------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| Host | $40 | $61 | $0C | $0C | $0C | $0C | $0C | $80 | 'H' | 'E' | 'L' | 'L' | 'O' | ' ' | 'W' | 'O' | 'R' | 'L' | 'D' | $0A |
| Device | $00 | $00 | $80 | $80 | $80 | $80 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 |
Transaction #2: the WRITE command.
| Time | 33 | 34 |
|-------:|:---:|:---:|
| Host | $40 | $A1 |
| Device | $00 | $00 |
Transaction #3: the CLOSE command.
The host PC asserts the device select, and then starts issuing commands to the device.
During time slots 0 to 1 inclusive,
the device is in "attention" mode, basically listening for an instruction of what to do (OPEN channel 1).
Since the device select remains asserted, from time slots 2 on, it's listening for a filename,
which it happily accepts until the end of time slot 12.
We know byte 12 is the last byte of the filename, because the EOI bit is set in its length header.
Also, because the host PC negates the device select, thus completing the OPEN transaction.
To commence with the WRITE operation, the host PC once again asserts the device select signal.
At time slot 13, we tell the device once again to LISTEN, only this time addressing an already open channel (1).
Starting with time slot 15, the host tries to send a length header for the payload to write to the stream, but cannot.
The drive is still in the process of opening the GREETINGS file/channel from the first transaction.
So, the host PC must wait.
This is indicated by the device via bit 7 of its status bytes.
It isn't until byte 19 that the device is finally able to react to the payload.
Finally, closing the file happens when we issue a CLOSE command transaction,
which is illustrated in the third table.
Note that neither CLOSE nor SECONDARY accept a filename.
## Receiving Data From the Device
What happens if we want to read back the contents of the GREETINGS file?
We first have to re-open it, which looks exactly like it did before.
| Time | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|-------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| Host | $40 | $81 | $09 | $80 | 'G' | 'R' | 'E' | 'E' | 'T' | 'I' | 'N' | 'G' | 'S' |
| Device | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 |
Transaction #1: the OPEN command.
Only this time, we now must tell this device to TALK instead of to LISTEN.
But, wait, how does the host PC throttle the device?
Simple: since the host PC is always the controller anyway, it simply halts the clock.
But, it might not know by how much to halt the clock if the device itself is the one who isn't ready.
Consider the following:
| Time | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
|-------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| Host | $20 | $61 | $FF | $FF | $FF | $FF | $FF | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $FF |
| Device | $00 | $00 | $80 | $80 | $80 | $80 | $40 | $0C | $80 | 'H' | 'E' | 'L' | 'L' | 'O' | ' ' | 'W' | 'O' | 'R' | 'L' | 'D' | $0A | $A0 |
Transaction #2: the READ command.
After bytes 13 and 14, the drive will be placed into TALK/SECONDARY(1) mode.
Since the OPEN is probably still going on, it will not be able to send useful data right away.
The host PC sends garbage bytes (completely ignored by the drive), just so it can react to the drive's status bytes.
As we can see, from bytes 15 to 18, the drive is responding with a retry status, telling the host PC that it is not yet ready to deliver data.
When it is, the drive responds with a new status code: $40,
which tells the host PC that it should swap roles (e.g., the host becomes the listener while the drive actually talks).
When this happens, we see from bytes 20 through 32 that we receive the contents of the GREETINGS file.
We know the last byte is the last byte of the file because the EOI flag is set in the length count;
and we know that because it was that way when we wrote to the file (see above).
After the data has been read,
the device automatically swaps roles again.
As we see in byte 33,
we can send more garbage bytes to the drive only for it to react with a $A0 status code.
Bit 7 is set, so it's clearly not ready to deal with the data byte. But,
bit 5 is also set, indicating that the byte does not conform with the protocol illustrated here.
What would happen if the read operation were interrupted in the middle?
For example, what if the data being read spans across multiple sectors and there's a time penalty for having to read the next sector into a buffer first?
| Time | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
|-------:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| Host | $20 | $61 | $FF | $FF | $FF | $FF | $FF | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $FF | $FF | $00 | $00 | $00 | $00 | $00 | $00 | $00 | $FF |
| Device | $00 | $00 | $80 | $80 | $80 | $80 | $40 | $06 | $00 | 'H' | 'E' | 'L' | 'L' | 'O' | ' ' | $80 | $40 | $05 | $80 | 'W' | 'O' | 'R' | 'L' | 'D' | $A0 |
| |
Wait, I'm reading more data for you! --------------' |
OK, I can continue now. --------------------'
Transaction #3: the interrupted READ command.
In this case, the data will be broken up into two chunks.
Each chunk starts out the same: $40 to indicate role reversal, followed by the usual length header and raw data.
Note how the PC returns to its role as a talker after receiving "HELLO ".
It knows that more data must exist because in this case the EOI flag in byte 20 is not set.
Once the second chunk of data arrives, we see that it is five characters long, and truely is the last five bytes in the file (since EOI is set in byte 30).
Also note that just because a read transaction says there are, say, 2048 bytes of data in a chunk,
you do not need to read all of them.
The host PC is free to terminate the read in the middle of a transaction at any time by negating the device select signal.
It can always pick up where it left off by re-asserting device select and re-issuing a TALK/SECONDARY command.
Closing the device channel works the same way for reads as it does for writes.
| Time | 33 | 34 |
|-------:|:---:|:---:|
| Host | $40 | $A1 |
| Device | $00 | $00 |
Transaction #4: the CLOSE command.
## Error Reporting
Most devices will have a dedicated command channel through which higher-level commands can be issued.
This channel is often used as well to report errors.
For example, on Commodore 1541-1581 disk drives, channel 15 is used not only for high-level DOS commands, like format or change directory,
but if you tell the drive to TALK/SECONDARY(15), it will report the most recent error string.
By convention, the string takes the form of "NN,Error Message,TT,SS", where NN, TT, and SS are numbers representing the error code, and the track and sector where the error occurred;
however, this is merely a convention, and is not obligated by the GPIB or Commodore Serial bus itself.
Likewise, I do not presuppose an error format in this document. I do presuppose at least one channel by which the latest error can be queried, however.
Which channel that is, however, is device specific.
## Random Access
Most contemporary storage devices support some form of random access.
However, the GPIB-like protocol is strongly optimized for sequential accesses.
Using the command channel, however, random access can be implemented.
For example, the Commodore DOS devices use "relative" type files, and they support a set of commands for selecting arbitrary records just fine.
(Of course, the record itself is transferred sequentially, as the above examples would go.)