recipes/trs80: new recipe (WIP)
This commit is contained in:
parent
e1e0676191
commit
a74ee81822
144
recipes/trs80/README.md
Normal file
144
recipes/trs80/README.md
Normal file
@ -0,0 +1,144 @@
|
||||
# TRS-80 Model 4p
|
||||
|
||||
The TRS-80 (models 1, 3 and 4) are among the most popular z80 machines. They're
|
||||
very nicely designed and I got my hands on a 4p with two floppy disk drives and
|
||||
a RS-232 port. In this recipe, we're going to get Collapse OS running on it.
|
||||
|
||||
**This is a work in progress. Collapse OS doesn't run on it yet.**
|
||||
|
||||
## Floppy or RS-232?
|
||||
|
||||
There are many ways to get Collapse OS to run on it. One would involve writing
|
||||
it to a floppy. I bought myself old floppy drives for that purpose, but I happen
|
||||
to not have any functional computer with a floppy port on it. I still have the
|
||||
motherboard of my old pentium, but I don't seem to have a video card for it any
|
||||
more.
|
||||
|
||||
Because my 4p has a RS-232 port and because I have equipment to do serial
|
||||
communication from modern machines (I didn't have a DB-9 to DB-25 adapter
|
||||
though, I had to buy one), I chose that route.
|
||||
|
||||
## Gathering parts
|
||||
|
||||
* A TRS-80 model 4p with a RS-232 port
|
||||
* A TRSDOS 6.x disk
|
||||
* A means to do serial communication. In my case, that meant:
|
||||
* A USB-to-serial device
|
||||
* A null modem cable
|
||||
* A DB-9 gender changer
|
||||
* A DB-9 to DB-25 adapter
|
||||
|
||||
## Overview
|
||||
|
||||
We need to send sizeable binary programs through the RS-232 port and then run
|
||||
it. The big challenge here is ensuring data integrity. Sure, serial
|
||||
communication has parity check, but it has no helpful way of dealing with
|
||||
parity errors. When parity check is enabled and that a parity error occurs, the
|
||||
byte is simply dropped on the receiving side. Also, a double bit error could be
|
||||
missed by those checks.
|
||||
|
||||
What we'll do here is to ping back every received byte back and have the sender
|
||||
do the comparison and report mismatched data.
|
||||
|
||||
Another problem is ASCII control characters. When those are sent across serial
|
||||
communication channels, all hell breaks lose. When sending binary data, those
|
||||
characters have to be avoided. We use `tools/ttysafe` for that.
|
||||
|
||||
Does TRSDOS have a way to receive this binary inside these constraints? Not to
|
||||
my knowledge. As far as I know, the COMM program doesn't allow this.
|
||||
|
||||
What are we going to do? We're going to punch in a binary program to handle that
|
||||
kind of reception! You're gonna feel real badass about it too...
|
||||
|
||||
## Testing serial communication
|
||||
|
||||
The first step here is ensuring that you have bi-directional serial
|
||||
communication. To do this, first prepare your TRS-80:
|
||||
|
||||
set *cl to com
|
||||
setcomm (word=8, parity=no)
|
||||
|
||||
The first line loads the communication driver from the `COM/DRV` file on the
|
||||
TRSDOS disk and binds it to `*cl`, the name generally used for serial
|
||||
communication devices. The second line sets communication parameters in line
|
||||
with what is generally the default on modern machine. Note that I left the
|
||||
default of 300 bauds as-is.
|
||||
|
||||
Then, you can run `COMM *cl` to start a serial communication console.
|
||||
|
||||
Then, on the modern side, use your favorite serial communication program and set
|
||||
the tty to 300 baud with option "raw". Make sure you have `-parenb`.
|
||||
|
||||
If your line is good, then what you type on either side should echo on the
|
||||
other side. If it does not, something's wrong. Debug.
|
||||
|
||||
## Punching in the goodie
|
||||
|
||||
As stated in the overview, we need a program on the TRS-80 that:
|
||||
|
||||
1. Listens to `*cl`
|
||||
2. Echoes each character back to `*cl`
|
||||
3. Adjusts `ttysafe` escapes
|
||||
4. Stores received bytes in memory
|
||||
|
||||
That program has already been written, it's in `recv.asm` in this folder. You
|
||||
can get the binary with `zasm < recv.asm | xxd`.
|
||||
|
||||
How will you punch that in? The `debug` program! This very useful piece of
|
||||
software is supplied in TRSDOS. To invoke it, first run `debug (on)` and then
|
||||
press the `BREAK` key. You'll get the debug interface which allows you to punch
|
||||
in any data in any memory address. Let's use `0x3000` which is the offset for
|
||||
user apps.
|
||||
|
||||
First, display the `0x3000-0x303f` range with the `d3000<space>` command (I
|
||||
always press Enter by mistake, but it's space you need to press). Then, you can
|
||||
begin punching in with `h3000<space>`. This will bring up a visual indicator of
|
||||
the address being edited. Punch in the stuff with a space in between each byte
|
||||
and end the edit session with `x`.
|
||||
|
||||
But wait, it's not that easy! You see those `0xffff` addresses? They're
|
||||
placeholders. You need to replace those values with your DCB handle for `*cl`.
|
||||
See below.
|
||||
|
||||
## Getting your DCB address
|
||||
|
||||
In the previous step, you need to replace the `0xffff` placeholders in
|
||||
`recv.asm` with your "DCB" address for `*cl`. That address is your driver
|
||||
"handle". To get it, first get the address where the driver is loaded in
|
||||
memory. You can get this by running `device (b=y)`. That address you see next
|
||||
to `*cl`? that's it. But that's not our DCB.
|
||||
|
||||
To get your DBC, go explore that memory area. Right after the part where there's
|
||||
the `*cl` string, there's the DCB address (little endian). On my setup, the
|
||||
driver was loaded in `0x0ff4` and the DCB address was 8 bytes after that, with
|
||||
a value of `0x0238`.
|
||||
|
||||
## Sending data through the RS-232 port
|
||||
|
||||
Once you're finished punching your program in memory, you can run it with
|
||||
`g3000<enter>` (not space). Because it's an infinite loop, your screen will
|
||||
freeze. You can start sending your data.
|
||||
|
||||
To that end, there's the `tools/pingpong` program. It takes a device and a
|
||||
filename to send. As a test, send anything, but make it go through
|
||||
`tools/ttysafe` first (which just takes input from stdin and spits tty-safe
|
||||
content to stdout).
|
||||
|
||||
On OpenBSD, the invocation can look like:
|
||||
|
||||
doas ./pingpong /dev/ttyU0 mystuff.ttysafe
|
||||
|
||||
You will be prompted for a key before the contents is sent. This is because on
|
||||
OpenBSD, TTY configuration is lost as soon as the TTY is closed, which means
|
||||
that you can't just run `stty` before running `pingpong`. So, what you'll do is,
|
||||
before you press your key, run `doas stty -f /dev/ttyU0 300 raw` and then press
|
||||
any key on the `pingpong` invocation.
|
||||
|
||||
If everything goes well, the program will send your contents, verifying every
|
||||
byte echoed back, and then send a null char to indicate to the receiving end
|
||||
that it's finished sending. This will end the infinite loop on the TRS-80 side
|
||||
and return. That should bring you back to a refreshed debug display and you
|
||||
should see your sent content in memory, at the specified address (`0x3040` if
|
||||
you didn't change it).
|
||||
|
||||
**WIP: that's where we are for now...**
|
36
recipes/trs80/recv.asm
Normal file
36
recipes/trs80/recv.asm
Normal file
@ -0,0 +1,36 @@
|
||||
ld hl, 0x3040 ; memory address where to put contents.
|
||||
loop:
|
||||
ld a, 0x03 ; @GET
|
||||
ld de, 0xffff ; replace with *CL's DCB addr
|
||||
rst 0x28
|
||||
jr nz, maybeerror
|
||||
or a
|
||||
ret z ; Sending a straight NULL ends the comm.
|
||||
; @PUT that char back
|
||||
ld c, a
|
||||
ld a, 0x04 ; @PUT
|
||||
ld de, 0xffff ; replace with *CL's DCB addr
|
||||
rst 0x28
|
||||
jr nz, error
|
||||
ld a, c
|
||||
cp 0x20
|
||||
jr z, adjust
|
||||
write:
|
||||
ld (hl), a
|
||||
inc hl
|
||||
jr loop
|
||||
adjust:
|
||||
dec hl
|
||||
ld a, (hl)
|
||||
and 0x7f
|
||||
jr write
|
||||
maybeerror:
|
||||
; was it an error?
|
||||
or a
|
||||
jr z, loop ; not an error, just loop
|
||||
; error
|
||||
error:
|
||||
ld c, a ; Error code from @GET/@PUT
|
||||
ld a, 0x1a ; @ERROR
|
||||
rst 0x28
|
||||
ret
|
2
tools/.gitignore
vendored
2
tools/.gitignore
vendored
@ -2,3 +2,5 @@
|
||||
/blkdump
|
||||
/upload
|
||||
/fontcompile
|
||||
/ttysafe
|
||||
/pingpong
|
||||
|
@ -2,7 +2,10 @@ MEMDUMP_TGT = memdump
|
||||
BLKDUMP_TGT = blkdump
|
||||
UPLOAD_TGT = upload
|
||||
FONTCOMPILE_TGT = fontcompile
|
||||
TARGETS = $(MEMDUMP_TGT) $(BLKDUMP_TGT) $(UPLOAD_TGT) $(FONTCOMPILE_TGT)
|
||||
TTYSAFE_TGT = ttysafe
|
||||
PINGPONG_TGT = pingpong
|
||||
TARGETS = $(MEMDUMP_TGT) $(BLKDUMP_TGT) $(UPLOAD_TGT) $(FONTCOMPILE_TGT) \
|
||||
$(TTYSAFE_TGT) $(PINGPONG_TGT)
|
||||
OBJS = common.o
|
||||
|
||||
all: $(TARGETS)
|
||||
|
56
tools/pingpong.c
Normal file
56
tools/pingpong.c
Normal file
@ -0,0 +1,56 @@
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "common.h"
|
||||
|
||||
/* Sends the contents of `fname` to `device`, expecting every sent byte to be
|
||||
* echoed back verbatim. Compare every echoed byte with the one sent and bail
|
||||
* out if a mismatch is detected. When the whole file is sent, push a null char
|
||||
* to indicate EOF to the receiving end.
|
||||
*
|
||||
* It is recommended that you send contents that has gone through `ttysafe`.
|
||||
*/
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc != 3) {
|
||||
fprintf(stderr, "Usage: ./pingpong device fname\n");
|
||||
return 1;
|
||||
}
|
||||
FILE *fp = fopen(argv[2], "r");
|
||||
if (!fp) {
|
||||
fprintf(stderr, "Can't open %s.\n", argv[2]);
|
||||
return 1;
|
||||
}
|
||||
int fd = open(argv[1], O_RDWR|O_NOCTTY|O_NONBLOCK);
|
||||
printf("Press a key...\n");
|
||||
getchar();
|
||||
unsigned char c;
|
||||
// empty the recv buffer
|
||||
while (read(fd, &c, 1) == 1) usleep(1000);
|
||||
int returncode = 0;
|
||||
while (fread(&c, 1, 1, fp)) {
|
||||
putchar('.');
|
||||
fflush(stdout);
|
||||
write(fd, &c, 1);
|
||||
usleep(1000); // let it breathe
|
||||
unsigned char c2;
|
||||
while (read(fd, &c2, 1) != 1); // read echo
|
||||
if (c != c2) {
|
||||
// mismatch!
|
||||
unsigned int pos = ftell(fp);
|
||||
fprintf(stderr, "Mismatch at byte %d! %d != %d.\n", pos, c, c2);
|
||||
returncode = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// To close the receiving loop on the other side, we send a straight null
|
||||
c = 0;
|
||||
write(fd, &c, 1);
|
||||
printf("Done!\n");
|
||||
fclose(fp);
|
||||
return returncode;
|
||||
}
|
||||
|
||||
|
26
tools/ttysafe.c
Normal file
26
tools/ttysafe.c
Normal file
@ -0,0 +1,26 @@
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* Converts stdin to a content that is "tty safe", that is, that it doesn't
|
||||
* contain ASCII control characters that can mess up serial communication.
|
||||
* How it works is that it leaves any char > 0x20 intact, but any char <= 0x20
|
||||
* is replaced by two chars: char|0x80, 0x20. A 0x20 char always indicate "take
|
||||
* the char you've just received and unset the 7th bit from it".
|
||||
*/
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int c = getchar();
|
||||
while (c != EOF) {
|
||||
if (c <= 0x20) {
|
||||
putchar(c|0x80);
|
||||
putchar(0x20);
|
||||
} else {
|
||||
putchar(c&0xff);
|
||||
}
|
||||
c = getchar();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user