R.COM, W.COM and z80sim

R.COM and W.COM are two very useful programs that Peter Schorn wrote for his AltairZ80 version of the SIMH computer simulator. They allow you to transfer files across the virtual boundary between the simulated computer and its host. Simply put, you can copy Windows files in and out of a CP/M computer; right from inside the CP/M computer.

It looks like this:

As you see, you can specify a pathname that the host will use. The CP/M name is the basename of the file. It’s just: “r [path\]file” or “w [path\]file”. R.COM reads the file into the CP/M system and W.COM writes one out. The simplicity of it is what makes it so impressive. There’s no fiddling around with, “what format is the CP/M disk I’m copying from?”. There’s no need to exit and later return to the simulation. It’s just easy.

How Does It Work?

Crossing the virtual boundary is always interesting. A CP/M computer from long ago had no idea that Windows would ever exist. The CP/M computer has access to its own disk drives and its own 64KB (or less) of RAM. The world outside is unimaginably big and it has no way of addressing it. CP/M 2.2 can’t write to memory beyond 0FFFFH, and everything below that is used. Some computers use an invalid instruction to trigger a switch to supervisor mode. Cromix uses something similar to do system calls. Somewhere, something needs to cross the boundary between executing simulated z80 code and running within the native (host) program.

Peter’s solution was simple and elegant. He’s emulating hardware and that runs in native code – so he added a pseudo device “SIMH” which feeds commands into the simulator from the simulation. It appears on IO Port 0xFE so he can use sequences of “OUT (0FEH),A” and / or “IN A,(0FEH)” to talk to the host environment.

The SIMH pseudo device is at the bottom of altairz80_sio.c (the version I’m looking at is dated 20 Apr 2009). It supports a number of one-byte commands and zero or more parameter bytes following that (how many depends on the command). Some commands also return result bytes back through port 0xFE. Again, which ones do, and how many bytes are involved, depends on the command that was sent.

He has set it up to do a range of useful functions such as getting the host time to set the CP/M clock; not just the ability to transfer files across the boundary.

Can it be used with other emulators?

If other emulators were to match Peter’s interface, there’s a good chance you could use Peter’s existing R.COM or W.COM program in other emulators.

There are some things to watch for though.

Files aren’t actually transferred through the SIMH pseudo device. There’s a mechanism to setup for the transfer, and one to close it again afterwards; but the bit in the middle is missing.

The source code for R.COM and W.COM is a little hard to find, but it does exist. It is included inside the app.dsk disk image that comes with Peter’s CP/M download (https://schorn.ch/cpm/zip/cpm2.zip). Both are written in an unusual (to me at least) language called “SPL”, possibly for Simple Programming Language or Structured Programming Language. R.COM comes from READ.SPL. W.COM comes from WRITE.SPL. There is also an SPL.TXT file on the disk that explains the language (but not its name).

Reading through the source code shows that the actual transfer takes place using the Paper Tape Punch (PTP or PUN) or Paper Tape Reader (PTR or RDR) device. This is hard coded as using ports 12H for status and 13H for data.
If “IN A,(12H)”, “AND A,1” is 0 then no more input data exists.
If “IN A,(12H)”, “AND A,2” is non-zero you can output to the punch.

Attaching to the punch/reader via the SIMH pseudo device associates the device with a host filename.

Detaching from the punch/reader via the SIMH pseudo device closes the host file.

Whilst it would be possible, in many cases, to adjust the hardware within other simulators to match these ports and the status bits that are being used; I ended up modifying R.COM and W.COM. Clearly, having a single copy of R.COM and W.COM for all is preferable so it was with some reluctance that I did so. The Cromemco hardware that I use in my Windows version of z80sim, doesn’t conflict with the ports that Peter is using. However, the Cromemco CP/M that exists (probably for actual hardware) uses ports 10H and 11H and flag bits 40H and 80H. My preference was to use the Cromemco CP/M but this wasn’t the decider.

z80sim is a pretty pure emulation of a CPU and some hardware. The hardware can be changed (not easily; but with recompilation). The software it runs can be changed. It is not just a CP/M emulator or just a CP/M and derivatives emulator. I didn’t write the original but I do appreciate the the thought and effort that went into it being usable for non-CP/M devices (GameBoy perhaps? or video arcade consoles, microcontrollers, etc). The SIMH pseudo device interface requires the emulator to read bytes from 0080H. This is an “understood” agreement that the command line is at that location in memory. Matching the interface requires building that address into the emulator.

In Peter’s case, 0080H is perfectly reasonable. The CP/M command line is there. That was true for CP/M 1 and CP/M 2. It is probably also true for MP/M and CP/M 3. It is true for CDOS (the Cromemco lookalike of CP/M) and even of Cromix (a multi-user, multi-tasking successor of CDOS). It is a sound assumption for CP/M and CP/M related things. But, it probably isn’t true for other Z80 based systems.

I would have preferred it if the address of the command line (ie almost always 0080H) had been passed to the SIMH pseudo device. That would have left a bit more flexibility in the mechanism and I could have avoided hard-coding that in.

Making Changes

Given my reluctance to build 0080H into the emulator and my preference for using standard Cromemco CP/M, hardware, I/O ports and flag bits; I modified R.COM and W.COM.

The app.dsk image also contains an SPL compiler and, with the Microsoft Linker in Peter’s provided A drive, there is enough to rebuild the two CP/M programs.

Given that I was changing the CP/M side of the interface anyway, I also modified the protocol used, a little. I found I could simplify the z80sim version of the SIMH pseudo device by always following a command byte with a null terminated string. In its simplest form, a single command byte is now always followed by a 00H byte. That terminates the command sequence and triggers the command’s action. A command could be followed by one or more bytes; but they will end once a 00H byte is seen. This also provided a simple means of passing the command line to the pseudo device without hard-coding an address inside it – each byte is sent in sequence as the parameter for the command. The “attach” command gets told the filename instead of having to look it up. It doesn’t care where it came from and doesn’t need to know where it came from. This works even in a mythical “NEW-P/M” where the command line is at 200H.

In some cases, Peter’s interface returns values. This is the case where wildcards are used for the filename and you are reading files from the host file system. The wildcards need to refer to host files and a CP/M program has no idea what files the host has. The interface has to return a series of names so that CP/M can create files with those names on the CP/M disk.

The null-terminated string idea worked well for returning values too. Every byte command sets a return value, though this might just be 00H. If you read the SIMH interface after issuing a command you can read the return value. The return value always ends once you get a 00H byte. You don’t have to read the return value (eg if it’s a command that doesn’t return more than 00H, or if you don’t need the return value). A return value gets overwritten when you issue the next command.

Peter’s wildcard expansion process returned all of the filenames in one go. These are in the commonly used “each value with a null terminator and an extra null at the end” format. It worked out easier to swap to FindFirst / FindNext commands for the host and a filename (or “”) as the return value each time.

z80sim Changes

I added the following into cromemco-iosim.c

/// "Simulator Control" Device

static int  simctl_cmd = 0;
static int  simctl_state = 0;
static int  simctl_ptr=0;
static char simctl_val[80+1];
static FILE *hfp[8];

// cmd str
static void simctl_out(BYTE data) {
    int n;     

//printf("simctl out=%02x\n",data);
    // state 0 : command byte
    if (simctl_state==0) { simctl_cmd=data; simctl_state=1; simctl_ptr=0; return; }

    // state 1 : arg(s) (eg "n:path\file", "a,b,c" or ""), terminated by \0
    if (simctl_ptr<80) simctl_val[simctl_ptr++]= data;
    if (data!='\0') return;
    
//printf("simctl cmd=%02x, arg=%s ",simctl_cmd,simctl_val);
    // state 2 : arg(s) complete. act on it.
    switch (simctl_cmd) {
    case 0x01: config_io();              // 2018-06-28. was 0x01
               simctl_val[0]='\0'; break;
    case 0xFF: cpu_state = STOPPED;
               simctl_val[0]='\0'; break;
    case 0x10: n=atoi(simctl_val);
               hfp[n]=fopen(simctl_val+2,"rb");
               strcpy(simctl_val,(hfp[n]==NULL)?"N":"Y"); break;
    case 0x11: n=atoi(simctl_val);
               hfp[n]=fopen(simctl_val+2,"wb");
               strcpy(simctl_val,(hfp[n]==NULL)?"N":"Y"); break;
    case 0x12: n=atoi(simctl_val);
               hfp[n]=fopen(simctl_val+2,"ab");
               strcpy(simctl_val,(hfp[n]==NULL)?"N":"Y"); break;
    case 0x18: n=atoi(simctl_val); fclose(hfp[n]); hfp[n]=NULL;
               simctl_val[0]='\0'; break;
    case 0x19: n=atoi(simctl_val); rewind(hfp[n]);
               simctl_val[0]='\0'; break;
    case 0x20: OS_FindFirst(80,simctl_val); break;
    case 0x21: OS_FindNext( 80,simctl_val); break;
    }
//printf("result=[%s]\n",simctl_val);
    simctl_state=0;
    simctl_ptr=0;
}

BYTE simctl_in(void) {
    int result;

    if (simctl_ptr >= 80) return 0;
    result= simctl_val[simctl_ptr];
    if (result != 0) simctl_ptr++;
    return (BYTE) result;
}

// 0=lst, 1=rdr, 2=pun, ...

// PaperTape device (RDR,PUN)
// port_in[ 0x10] 40H=rda 80H=tbe, see Cromemco CP/M BIOS
static int pt_rda=0;
static int pt_dat;
BYTE pt_stat(void) {
    int stat;

    stat=0;
    if (!pt_rda && hfp[1]!=NULL && !feof(hfp[1]))
        if ((pt_dat=fgetc(hfp[1]))!=EOF)
            pt_rda=1;
    if (pt_rda      ) stat|=0x40;
    if (hfp[2]!=NULL) stat|=0x80;
//printf("pt_stat=%02x\n",stat);
    return (BYTE) stat;
}
void pt_baud(BYTE data) { data=data; }     // set baudrate. ignored.
BYTE pt_rd(void) {
    if ((pt_stat()&0x40) == 0) return (BYTE) 0x1a;
//printf("pt_rd\n");
    pt_rda=0;
    return (BYTE) pt_dat;
}
void pt_wr(BYTE data) {
    if ((pt_stat()&0x80) == 0) return;     // don't wait, won't happen
    fputc(data,hfp[2]);
}


It’s less than 100 lines to enable the mechanism.

Trying it out

For convenience during development and mainly to avoid clobbering the originals, I named the z80sim versions R2.COM and W2.COM. It’s an extra character; but I haven’t found that to slow me down much. I’ve left the new ones with those names. You can see them inside the latest (2018-07-01) version of run-cpm22.zip.

The source code, including the SPL files, is in z80sim-0.12.zip.

I’m assuming the source for R.COM and W.COM is also covered by the GPL that covers SIMH. If that’s not the case, I’ll rewrite them in C. I’m pretty sure they were intended to be used.

This is part of the CP/M topic.

It's only fair to share...Share on FacebookTweet about this on TwitterShare on Google+Share on LinkedInShare on StumbleUponDigg thisPin on PinterestEmail this to someone

Leave a Reply

Your email address will not be published. Required fields are marked *