TeleVideo Systems MmmOST Protocol
Methods
Reverse engineering required quite a bit of patience, and some trial-and-error. I was able to start out with schematics, a copy of MmmOST in an ImageDisk image, and along the way found a copy of the TeleVideo client source code archived online.
I still don't have access to a working Service Processor, so I haven't been able to verify that the behavior matches the original hardware, but I have been able to re-implement most of it so that the client software seems to run correctly. I'll have a set of blog articles about that in the future, but some of the hardware that I produced for that was necessary for verifying how the protocol worked.
If you haven't read the first article with an overview of what MmmOST is and how it works, it'd be best to read through that first.
I still don't have access to a working Service Processor, so I haven't been able to verify that the behavior matches the original hardware, but I have been able to re-implement most of it so that the client software seems to run correctly. I'll have a set of blog articles about that in the future, but some of the hardware that I produced for that was necessary for verifying how the protocol worked.
If you haven't read the first article with an overview of what MmmOST is and how it works, it'd be best to read through that first.
Hardware information
The systems use a Zilog Z80A SIO/2 (Z8442) chip in SDLC (aka HDLC) synchronous serial mode, with an 800KHz transmit clock derived from the 4MHz CPU clock. This is (per specs) the fastest data rate that can be used in this mode on the SIO chip. The system transmits over an RS-422 channel (using +/- 5V drivers into a 100ohm impedance), sending a Transmit Data (TxD), Transmit Clock (TxC), and Request to Send (RTS) signal. It receives from the other end a Receive Data (RxD), Receive Clock (RxC), and Clear to Send (CTS) signal. These are generated from the matching signals on the other end.
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|
GND | TxD+ | RxD+ | RTS+ | CTS+ | TxC- | RxC- | GND |
TxD- | RxD- | RTS- | CTS- | TxC+ | RxC+ | Test | |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
The basic handshaking of the protocol goes like this (client and server can be reversed depending on data direction):
- Client raises RTS
- Server acknowledges and sends CTS to client
- Client sends data using SDLC encoding to the global address, with standard SDLC CRC-16 checksum at the end
- Client and server drop RTS/CTS
There are delays that are needed between 2 and 3, and after 4, to ensure that the other end is ready to receive data, and has cleared any junk out of receive buffers, and to ensure that both ends have dropped RTS after the request. These were mostly determined by trial and error.
Boot loader dump
The bootloader provided the first look at what data is sent. Requests are sent from the client to the server as a 10-byte SDLC frame, generally with the first byte (called SOR, or Start of Request) set to 01h. The bootloader received by the firmware in EPROM as one 128-byte record, which assuming the CRC is checked out as good by the SIO chip, is then executed. The particular machine design determines where this is loaded into RAM, but generally, it's loaded near the lowest address which is always RAM, regardless of the state of the EPROM enable latch / RAM bank switch. On early machines, this is address 4000h, and on later machines, it's a bit above C000h.
I was able to find a copy of the boot loader in a MmmOST install floppy image, as an Intel HEX formatted file named XPD?BOOT.HEX - where the "?" varies based on machine type.
The system sends out a message, including the following:
I was able to find a copy of the boot loader in a MmmOST install floppy image, as an Intel HEX formatted file named XPD?BOOT.HEX - where the "?" varies based on machine type.
The system sends out a message, including the following:
- Command - IPL is ASCII 'L' or 4Ch
- USER - the hardware type -- 0 for the 801/802/800A, and other values for later machines
- CBOOT - set to 1 since it's a cold boot; this is set to 0 if CP/M is doing a warm boot
- RECS - the number of records that are expected for the OS image; +/- a few records will be accepted as well
- SREC - start record number; I haven't seen this set to anything but 0
- Signature bytes - when loading the OS image, these are always 0's
SOR | IPL | USER | CBOOT | RECS | SREC | 6 | 7 | 8 | 9 | |
---|---|---|---|---|---|---|---|---|---|---|
801 | 01h | 4Ch | 00h | 01h | 58h | 00h | 00h | 00h | 00h | 00h |
802H | 01h | 4Ch | 01h | 01h | 68h | 00h | 00h | 00h | 00h | 00h |
803 | 01h | 4Ch | 04h | 01h | 6Eh | 00h | 00h | 00h | 00h | 00h |
803H | 01h | 4Ch | 05h | 01h | 6Eh | 00h | 00h | 00h | 00h | 00h |
TPC-I | 01h | 4Ch | 07h | 01h | 6Eh | 00h | 00h | 00h | 00h | 00h |
Hardware capture
To get more data, I had built hardware using an ARM board (the Next Thing Co CHIP) with GPIOs interfacing to a Z85C30 serial chip. Initially, I ran this at a 666KHz clock to avoid dropping characters, but I was able to bump it up to 800KHz with some hardware and software redesign. Any speed lower than 666KHz (which was picked as it was a divider of the clock for the Z85C30) resulted in the client system timing out.
Using this, I was able to capture that request that the EPROM was sending out for the boot loader. It was the same 10-byte format which was used by the bootloader. One thing that I ended up having to sort out, is that the request for the boot loader was very similar to the request for the whole OS. The only consistent difference was that the signature bytes at the end of the request were 4,5,6,7 in the request from the EPROM (which only would process one 128-byte record) versus all zeros in the request from the bootloader, which was actually asking for the whole OS image. The boot loader also is not a part of the OS image, so keying off of these signature bytes was critical to select what data to send to the client.
The format is the same as the above request for the OS from the boot loader.
Using this, I was able to capture that request that the EPROM was sending out for the boot loader. It was the same 10-byte format which was used by the bootloader. One thing that I ended up having to sort out, is that the request for the boot loader was very similar to the request for the whole OS. The only consistent difference was that the signature bytes at the end of the request were 4,5,6,7 in the request from the EPROM (which only would process one 128-byte record) versus all zeros in the request from the bootloader, which was actually asking for the whole OS image. The boot loader also is not a part of the OS image, so keying off of these signature bytes was critical to select what data to send to the client.
The format is the same as the above request for the OS from the boot loader.
SOR | IPL | USER | CBOOT | RECS | SREC | 4 | 5 | 6 | 7 | |
---|---|---|---|---|---|---|---|---|---|---|
801 | 01h | 4Ch | 00h | 01h | 37h | 00h | 04h | 05h | 06h | 07h |
803 | 01h | 4Ch | 04h | 01h | FFh | 00h | 04h | 05h | 06h | 07h |
803H | 01h | 4Ch | 05h | 01h | FFh | 00h | 04h | 05h | 06h | 07h |
TPC-I | 01h | 4Ch | 07h | 01h | FFh | 00h | 04h | 05h | 06h | 07h |
CBIOS Source Code
The next thing I was able to look at was a copy of the CP/M CBIOS source code, in 8080 and Z80 assembly code (the mix of the two is a pain to read - I've also discovered that Zilog Z80 mnemonics seem to make more sense and are more readable than the Intel 8080 ones). TeleVideo actually distributed this code with machines at the time, and some of it has been preserved on Bitsavers. The source code which I was looking at covered the TS-803 and TPC-I (which have very similar hardware) and could generate either the standalone CP/M or "USER" version for the service processor to send to clients.
Service processor requests
There are a few functions that send a request to the service processor for some non-disk related action. I call these "Checks", as they use ASCII 'C' as their function code in the request
During boot and afterward there are a few requests sent:
- Get print spool drive
- Start a new print spool (print the old one)
- Get Processor ID (service processor port number) number and if autologin command should be run
- Get MmmOST "GENREV" version
- Directory hijack request (locking for asking to read a Public drive directory section)
Each of these is sent with a 128-byte additional record, which is unused for these requests. The client expects a 4-byte reply, which contains the response for the request/command.
SOR | REQ | DRV | SUBREQ | X | X | X | X | X | X |
---|---|---|---|---|---|---|---|---|---|
01h | 43h | x | y | 0 | 0 | 0 | 0 | 0 | 0 |
SOR | REQ | DRV | SUBREQ | X | X | X | X | X | X |
---|---|---|---|---|---|---|---|---|---|
01h | 4Eh | 01h | 52h | 0 | 0 | 0 | 0 | 0 | 0 |
Disk direct read/write requests
There are two functions, used to read/write individual 128-byte CP/M records on the disk. The blocks may be bigger than this, but the service processor handles blocking/deblocking the data to/from disk. These are primarily used on Private and Public-only disks. On Public disks (with MmmOST managing shared files), applications can only read the directory blocks and are not allowed to write blocks directly.
Reading:
- 10-byte IPC request to the server
- 4-byte IPC response to the client
- If no error was returned, 128-byte record to the client
Writing:
- 10-byte IPC request to the server
- 128-byte record to the server
- 4-byte IPC response to the client
The request looks like this:
SOR | REQ | DISK | TK8 | RECL | RECH | TK16L | TK16H | WRTYPE | SEL |
---|---|---|---|---|---|---|---|---|---|
01h | 'R'/'W' | 0-11 | X | RL | RH | TL | TH | WT | 0/1 |
- The Disk is 0-11, corresponding to A through N
- TK8 is an 8-bit value for the track number on the disk
- RECH/RECL are two halves of the 16-bit value for the 128-byte record number of the track being accessed
- TK16H/TK16L are two halves of the 16-bit value for the disk track (aka cylinder) being accessed
- WRTYPE tells the service processor what kind of write to do - Async write for a data record (0), Sync write for a directory record (1), or (2) if it's the first write to the block, and the service processor doesn't need to preserve the data in the block's other records.
- SEL is 0 or 1, depending on if the disk has been selected previously
Note that blocks are typically bigger than records, which are fixed at 128 bytes. Generally, blocks are a few KB in size - TeleVideo uses 2KB for floppies and 4KB for hard disks.
The response looks like this:
RET | X | ERRNO | PRNT |
---|---|---|---|
0-FFh | 0 | x | y |
- Retcode is the CP/M 2.2 compatible return code, that would be returned in the A register for the CP/M function call. 0 indicates no error, and 1 indicates an irrecoverable error
- X appears to be unused
- ERRNO is the error code returned to the caller, usually one from the following table
- PRNT is set to 1 to print the error returned
Error condition | Value |
---|---|
Error selecting drive | 0 |
Error reading drive | 1 |
Error writing drive | 2 |
BDOS function handling
The CP/M functions that work with files (using File Control Blocks, or FCBs) are intercepted for shared "Public" drives. Note that Public-only drives are different, and do not support shared file handling and locking, and Private drives can only be used by one user station at once, so those are both handled directly by the user station's CBIOS as a sort of remote block store.
The functions that are intercepted include:
The functions that are intercepted include:
Number | Function call |
---|---|
15 | Open File |
16 | Close File |
17 | Search for first match |
19 | Delete file |
20 | Read file, sequential |
21 | Write file, sequential |
22 | Make (create) new file |
23 | Rename file |
30 | Set file attributes |
33 | Read file, Random |
34 | Write file, Random |
35 | Compute file size |
36 | Set random record from seq. position |
40 | Write random, zero fill block |
All BDOS functions that are intercepted for handling by MmmOST on the Public drive send a request IPC and FCB, and receive an IPC response and modified FCB back from the service processor. Depending on the function (and if there was an error), they will also send or receive a 128-byte record with the actual data in it as well. This is only used for the 5 functions that read or write to a file.
The sequence looks like this:
- 10 byte IPC request to the server
- 36 byte FCB to the server
- For Write/Write random/Write Zero fill, send 128-byte data record to the server
- 4 byte IPC response to the client
- 36 byte modified FCB to the client
- If Read/Read random, and if there was no read error, send 128-byte data record to the client
The BDOS IPC request looks like this:
SOR | REQ | LOGDRV | FUNC | USER | FNUML | FNUMH | CBD | CBF | X |
---|---|---|---|---|---|---|---|---|---|
1 | 46h | 0-11 | x | x | fl | fh | x | x | x |
- REQ is ASCII 'F' for a file request
- LOGDRV is the currently logged on drive on CP/M
- FUNC is the CP/M BDOS function call number
- FNUMH/FNUML are 8-bit parts of the 16-bit file number used by MmmOST to keep track of the file
- CBD is the "default" drive for file requests
- CBF is "Current BDOS Function", which is probably the same as FUNC
The response looks like this:
FNUML | FNUMH | RETCODE | ERR |
---|---|---|---|
FL | FH | 0-FFh | 0-FFh |
- FNUMH/FNUML are the MmmOST file number
- RETCODE is the return code returned to the caller, typically 0xFF on error
- ERR is the error code which CP/M can use to determine the fault
ERR is divided up into 3 fields. Bit 7 is set to 1 to print an extra second line of error, used for BIOS returned messages. Bits 6-4 are used for errors from the BIOS (problems reading/writing). Bits 3-0 are used for error from the data passed to the BDOS function. The list of error codes are:
Value | Error |
---|---|
0 | OK |
1 | Command fault |
2 | Write Protect |
3 | Illegal Call |
4 | Bad File for request |
5 | Drive type bad for request |
6 | Transfer out error |
7 | Transfer in error |
8 | Genrev does not match |
9 | No space on drive |
90h | Bad sector |
98h | Read only |
C0h | Drive selection error |
Other programs
LOGON.COM is the most useful other program. It send a request to the server to switch your private directory to a different password-protected (or back to the default) directory.
The request follows the same 10-byte / 128-byte / 4-byte sequence from "Check" requests earlier. One major difference is that the SOR byte is 00h instead of 01h. The 128-byte data field contains the password as the first 8 bytes (and the remainder should be ignored). The 4 byte response will have the first byte as 0 if the request was successful.
The Logon request looks like this, with DRV set to the CP/M drive number to attempt to log-on with the password:
The request follows the same 10-byte / 128-byte / 4-byte sequence from "Check" requests earlier. One major difference is that the SOR byte is 00h instead of 01h. The 128-byte data field contains the password as the first 8 bytes (and the remainder should be ignored). The 4 byte response will have the first byte as 0 if the request was successful.
The Logon request looks like this, with DRV set to the CP/M drive number to attempt to log-on with the password:
SOR | REQ | DRV | SUBREQ | x | x | x | x | x | x |
---|---|---|---|---|---|---|---|---|---|
00h | 43h | x | 4Ch | 0 | 0 | 0 | 0 | 0 | 0 |
Timing concerns
Testing this on TS-801s and TS-803s, they both use somewhat different time outs and have different timing requirements. I have observed that a 45uS sleep after requests are serviced before checking for CTS, a 1-5ms sleep before sending each boot loader block, and a 100uS sleep before sending data back to the client in the file handler was optimal to avoid times or sending data before the client was ready for it.
In other words, timing race conditions seem to be something that needs to be carefully handled in working with these systems. The Z80 code mostly uses busy loops as delays or for timeouts, which are very clock speed sensitive.
In other words, timing race conditions seem to be something that needs to be carefully handled in working with these systems. The Z80 code mostly uses busy loops as delays or for timeouts, which are very clock speed sensitive.
Future work needed
There's more MmmOST software that needs to be reverse engineered, including print queue handling.
It'd be nice to also look at TurboDOS as another example of how this was done by a different CP/M compatible OS.
Also, I'd like to work on modifying SimH to simulate TeleVideo systems. The RS-422 connection could be emulated with a network (or local) socket between two copies of the simulator.
A future blog will document my creation of a replacement for MmmOST, which runs on modern hardware.
It'd be nice to also look at TurboDOS as another example of how this was done by a different CP/M compatible OS.
Also, I'd like to work on modifying SimH to simulate TeleVideo systems. The RS-422 connection could be emulated with a network (or local) socket between two copies of the simulator.
A future blog will document my creation of a replacement for MmmOST, which runs on modern hardware.
References
Bitsavers's "bits" collection has the TeleVideo source code and some disk images. Dave Dunfield's collection of images has Imagedisk images for MmmOST on a TS-806, along with the software needed to read them. Online CP/M manuals such as this one have been very important in understanding how CP/M works. Datasheets for Zilog parts such as the Z80 SIO and other peripherals are valuable for understanding the code, along with the schematics in the TeleVideo service manuals on Bitsavers.
No comments:
Post a Comment