Transfer BASIC programs and machine-language code to/from a Sharp PC-1500 /
PC-1500A / PC-1600 pocket computer over serial, read and write files on
Calc-U-1600 CE-1600F floppy images, read and
write cassette-tape WAV files (and play them to a CE-150 / CE-1600P through the sound
output), and tokenize/de-tokenize BASIC listings offline — no Java runtime, a single
self-contained binary. This is the
Rust reimplementation of the Java
SharpDataExchangeJava, with
byte-identical convert output to the Java tool on the checked-in fixtures.
Embedding this in your own application (C / C++ / Swift / Rust)? See
library.md: it covers tokenizing / de-tokenizing, file kinds, files on
a floppy side and cassette WAVs. Serial get/put, audio playback and config are
CLI-only.
- Install
- Hardware Setup
- Quick Start
- Concepts
- Command Reference
- Pocket Computer Commands
- Data Formats
- Common Workflows
- Troubleshooting
- Scope / Known Limitations
- Development
- License
Download for your platform from the Releases page. The
.tar.gz / .zip archives hold bin/sde plus the C library and header (see
library.md), the license, and this document — unpack and put bin/sde on your
PATH.
| download | platform |
|---|---|
SharpDataExchange-<version>.pkg |
macOS 15.8+ (Apple Silicon) — graphical installer |
sharpdx-macos-arm64.tar.gz |
macOS 15.8+ (Apple Silicon) — archive |
sharpdx-linux-x86_64.tar.gz |
Linux x86-64 (glibc 2.35+) |
sharpdx-linux-aarch64.tar.gz |
Linux arm64 (glibc 2.35+) |
sharpdx-windows-x86_64.zip |
Windows 10+ x64 |
sharpdx-windows-aarch64.zip |
Windows 11 arm64 |
Easiest: double-click SharpDataExchange-<version>.pkg. It is signed and
notarized (no Gatekeeper warning) and asks whether to install:
- for all users →
/usr/local/bin/sde(already onPATH); or - for me only →
~/.local/bin/sde, creating that directory and adding it to~/.zshrcwhen needed — open a new terminal afterwards.
Prefer to place the binary yourself: use sharpdx-macos-arm64.tar.gz. Its sde
and libsharpdx.dylib are Developer-ID-signed and notarized (Gatekeeper verifies
online on first run); the archive itself carries no stapled ticket.
cargo build --release
target/release/sde is the CLI; it links only the system C library and the system's
audio library (for put -f wav). On Linux, building needs the ALSA development
package (libasound2-dev on Debian/Ubuntu, alsa-lib-devel on Fedora). The library
alone (cargo build --release --lib --no-default-features) needs neither.
Photos and build notes (adapter board, wiring, CE-158X) are in
docs/HardwareNotes.md.
The PC-1500 and PC-1500A do not have a built-in serial port. The original Sharp
CE-158 add-on provided serial and parallel ports, but it is not supported: its port
is RS-232 only and would most likely need a lower baud rate than sde uses (see
docs/HardwareNotes.md). Use the modern
CE-158X by Jeff Birt (available at soigeneris.com).
Connect the CE-158X to the PC via its USB port (labeled U1). No driver configuration is necessary on modern systems; the device appears as a standard USB serial adapter.
The PC-1600 has a built-in 5V TTL serial port via a 15-pin connector. Connect it to the PC using an FTDI USB/UART adapter (e.g. TTL-232R-5V-WE). The adapter signals must be inverted before use — a one-time operation using FTDI's configuration utility on a Windows machine.
Wiring (pin 1 is the rightmost pin of the PC-1600's 15-pin connector):
| PC-1600 pin | Signal | Adapter wire |
|---|---|---|
| 2 | TX | RX (yellow) |
| 3 | RX | TX (orange) |
| 4 | RTS | CTS (brown) |
| 5 | CTS | RTS (green) |
| 7 | Ground | Ground (black) |
Do not connect the red (5V) wire of the adapter.
sde automatically detects the serial port for pc1500, pc1500a, and
pc1600:
- macOS: looks for
cu.usb* - Linux: looks for
ttyACM*orttyUSB* - Windows: not yet implemented — always pass
--portexplicitly
If more than one matching port is present, or auto-detection isn't supported on
your platform, use -p/--port to specify the port explicitly.
To exchange data with a PC-1600 emulator instead of real hardware, use
--device pc1600emul. The emulator is reached over a host pseudo-terminal, which
the Calc-U-1600 emulator creates while it runs as a file always named
calcu1600-rs232c.serial inside some directory. Pseudo-terminal paths are never
auto-detected, so sde needs to know that directory — either given
per-invocation with --port <full-path-to-calcu1600-rs232c.serial>, or configured
once as a default directory (see Config file):
sde config set pc1600emul.port /tmp/my-emulator-dir
sde put myprogram.bas --device pc1600emul # uses /tmp/my-emulator-dir/calcu1600-rs232c.serial
sde config set pc1600emul.port <dir> stores the directory, not the full
port path — sde always appends the fixed filename calcu1600-rs232c.serial itself.
If you never set this, the default directory is /tmp, so
--device pc1600emul with no --port and no config resolves to
/tmp/calcu1600-rs232c.serial out of the box.
pc1600emul sends the same data as pc1600 and, like pc1600 by default, uses
no RTS/CTS hardware flow control and paces the transfer like the PC-1500. A
pseudo-terminal has no handshake lines, so --flowcontrol (see below) is only a
best-effort attempt there and most likely has no effect.
sde convert myprogram.bas # -> myprogram.bbas (CE-158 header + tokens)
sde info game.wav # what's on the tape (PC-1500 or PC-1600, every file)
sde get game.wav # -> GAME.bas (de-tokenized listing) / GAME.bin
sde put myprogram.bas -f wav # play it as a tape; type CLOAD on the pocket computer
On the PC-1500: SETDEV U1,CI,CO then CSAVE.
On the PC, run sde get before triggering CSAVE (the device doesn't
buffer — see Who goes first):
sde get
The program is saved as readable ASCII BASIC; sde derives the filename from
the received header and de-tokenizes the binary automatically.
On the PC-1500: SETDEV U1,CI,CO, then issue CLOAD before running put:
sde put myprogram.bas
sde get myprogram.bas --device pc1600
sde put myprogram.bas --device pc1600
See Pocket Computer Commands for the PC-1600's one-time-per-session serial setup commands.
get and put describe what the PC does: get takes a file from somewhere and
saves it on the PC, put takes a file on the PC and sends or stores it. On the
Pocket Computer, get pairs with a save command (CSAVE, SAVE) and put with a
load command (CLOAD, LOAD).
flowchart LR
host["Files on the PC<br/>.bas .bbas .bin …"]
pc["Pocket computer<br/>(serial)"]
floppy["Calc-U-1600 floppy image<br/>disk.floppy.yaml:A:"]
folder["Folder as PC-1600 disk<br/>(Calc-U-1600 S3:)"]
wav["Cassette WAV"]
audio["Audio output →<br/>CE-150 / CE-1600P"]
host -- put --> pc
pc -- get --> host
host -- put --> floppy
floppy -- get --> host
host -- put --> folder
wav -- "get, convert" --> host
host -- "convert -f wav" --> wav
host -- "put -f wav" --> audio
convertworks on the PC alone: tokenizes or de-tokenizes BASIC, adds or strips a machine-code header, unpacks or writes a cassette WAV.infodescribes what a file holds.diranddellist and delete files on a floppy image.
get and put can also take a file off, or store one on, a CE-1600F floppy image
of the Calc-U-1600 emulator
(<name>.floppy.yaml) instead of talking to the serial port. dir lists such an
image and del deletes files from it. A file on the image is addressed as
<image>.floppy.yaml:<side>:<NAME.EXT>
<side>isAorB: the CE-1600F drive is single-sided and the disk is turned over by hand, so each side is a separate volume with its own files.- Names are 8.3 and upper-case on the disk (
prog.basis stored asPROG.BAS). getanddelaccept*and?wildcards (DOS rules:*.BAS,G*.*,*). Quote them so the shell doesn't expand them:'disk.floppy.yaml:A:*.BAS'.- The floppy is PC-1600 media, so everything is PC-1600: BASIC is tokenized with the PC-1600 keyword table, and files with a PC-1500 (CE-158) header are refused.
- The image is always rewritten whole and atomically, and only if every file of the
command succeeded — a failing
putof three files stores none of them.
Eject the disk in Calc-U-1600 first (or quit the emulator). The emulator keeps an inserted disk in memory and rewrites its file shortly after every disk access, which would silently undo sde's changes.
The container format is specified in Calc-U-1600's docs/Floppy-Image-Format.md; the
filesystem inside a side is Sharp's (see the Sharp1500-1600-Ref corpus,
PC-1600-Filesystem.md §5).
The CE-150 (PC-1500 / PC-1500A) and the CE-1600P (PC-1600, MODE 0) save programs to
cassette; a recording of such a tape is a WAV file. sde reads one wherever it reads
a file, recognized by content (any .wav name, or none):
sde info tape.wavnames the tape format — PC-1500 (CE-150) or PC-1600 (CE-1600P) — and every file on it, with its type, name, addresses and content.sde get tape.wav [output]andsde convert tape.wav [output]write each file on the tape asgetwrites a received one (BASIC as a listing, machine code with its header).tape.wav:NAMEpicks one file of several.sde put tape.wavsends the file on the tape over serial (or onto a floppy image,sde put tape.wav disk.floppy.yaml:A:).
A WAV is written only with -f wav; no default changes:
sde convert prog.bas -f wav/sde get -f wavwrite a WAV file,sde put prog.bas -f wavplays the tape through the computer's default audio output — for loading programs into a pocket computer that has a CE-150 or CE-1600P (with a cable from the headphone jack to the recorder input) but no CE-158 serial interface.
The tape format follows the file's header (CE-158 → PC-1500, PC-1600 header →
PC-1600), else --device.
Reading is tolerant, but safe. Recordings may be at any sample rate from 5 kHz, 8 to
32 bits or float, mono or stereo (the louder channel is used), quiet (down to about
−40 dBFS), noisy, polarity-inverted, played back up to 25 % too fast or too slow, and
with several % of wow and flutter. A file is only accepted when every checksum on the
tape matches; a damaged file is reported (warning: with its position on the tape),
never half-read.
Writing is clean. Phase-continuous tones at the nominal frequencies, the ROMs' own
framing, 16-bit mono at 48 kHz (--sample-rate). The ROMs write a very long lead-in
tone (PC-1500 about 8 s); sde writes about 2 s (PC-1500) or 3 s (PC-1600) by default,
enough for CLOAD with the recorder on the remote. --leader <seconds> sets any
length, --leader rom the original one.
Supported: BASIC programs and machine code on both, plus the PC-1500's Reserve Area.
PC-1500 DAT / DEF files and PC-1600 ASCII (CSAVE ,A) and data files are
recognized and reported, but not converted.
For get: start sde get first, then issue the save command on the Pocket
Computer. For put: issue the load command on the Pocket Computer first, then
run sde put. The device does not buffer outgoing data, so the ordering matters.
sde identifies data type from its content, not the file name or extension —
.bas, .bin, or any other extension can be used freely for get/put/convert
input files. A .bas file holding tokenized BASIC (as a PC-1600 disk stores it) is
fine; -v mentions it.
When sde names a file it writes, it uses one convention everywhere (serial, disk image,
tape, convert):
| Content | ASCII form | Binary form |
|---|---|---|
| BASIC | .bas |
.bbas |
| Reserve Area | .sdar |
.bsdar |
| Variables | .sdav |
.bsdav |
| Machine code | — | .bin |
| Text | .txt |
— |
| Unrecognized data | — | .bin |
The exception is a PC-1600 disk (a floppy image or a host folder):
there BASIC is NAME.BAS in both forms, because that is how the PC-1600 itself saves
it.
get/put handle BASIC programs, machine-language (assembly) programs, text,
Reserve Area, and Variables. Reserve Area and Variables are PC-1500/1500A-only and
are not handled by convert — see Data Formats for their layout.
By default, every command runs quietly — only the essential result is printed
(the saved filename, a warning that matters regardless of verbosity, or an
error). -v/--verbose narrates every non-obvious decision made along the way
(which device was inferred and from where, whether a header was added/stripped,
which content type was detected, etc.) to stderr. -q/--quiet is not a
lower verbosity than the default — it exists only to force narration off for
one invocation when a config-file default has turned it on. Precedence:
-v > -q > config file verbose setting > off.
--dry-run reports what would happen and writes nothing:
get --dry-runover serial still performs a real receive (it opens the port and waits for data) and runs detection, but doesn't write the output file. From a disk image or WAV it only reads.put --dry-runnever opens the serial port, the audio device, or changes a disk image or folder.del --dry-runlists what would be deleted.
convert has no --dry-run.
sde reads optional defaults from ~/.sderc (%USERPROFILE%\.sderc on
Windows) — a plain key = value text file, modeled on git config --global
but simpler (no sections, no nesting). It's entirely optional; nothing breaks
if it's absent. Manage it with sde config get/set rather than hand-editing,
though the format is simple enough to edit directly if you prefer.
Keys:
| key | meaning |
|---|---|
pc1600emul.port |
directory holding the pc1600emul pseudo-terminal socket, whose filename is always calcu1600-rs232c.serial. Defaults to /tmp when unset — i.e. /tmp/calcu1600-rs232c.serial — if --port is also not given. |
verbose |
default verbosity (true/false) when neither -v nor -q is given |
An explicit --port/-v/-q on the command line always overrides the config
file.
sde config set pc1600emul.port /tmp/my-emulator-dir
sde config set verbose true
sde config get verbose
sde config get pc1600emul.port
sde get [options] [<output-file>]
Waits for the Pocket Computer to send data, then writes it to <output-file>.
If <output-file> is omitted, the filename is derived from the serial header
sent by the Pocket Computer; if the header also lacks a filename, unnamed is
used (a warning is printed in both fallback cases). If a filename is given but
lacks an extension, one is appended based on the data type and --format, per the
extension table (.bas for a listing, .bbas
for tokenized BASIC, .bin for machine language).
| Option | Description |
|---|---|
-d, --device <device> |
Target device: pc1500 (default), pc1500a, pc1600, pc1600emul. Configures baud rate and flow control before data arrives; decoding uses the device recorded in the received header when one is found, so an incorrect --device doesn't corrupt the output content — only the transport timing. |
-p, --port <port> |
Serial port name (auto-detected if omitted; see Serial Port Auto-Detection). |
--flowcontrol |
Enable RTS/CTS hardware flow control (pc1600 / pc1600emul only; RTS is used while receiving). Off by default, which is the safe choice: the host is fast enough that nothing is lost without it. See Sharp PC-1600. |
-f, --format <format> |
Output format: ascii (default), binary. ascii on machine-language content is rejected — machine language can't be de-tokenized, so pass --format binary for it. |
--eol <eol> |
Line ending of a de-tokenized listing or text file: auto (default: CRLF on Windows, LF elsewhere), lf, crlf, cr. |
--skip-header |
Omit the serial header from the saved binary file (--format binary only). The resulting file can't be auto-identified or reloaded by sde without it — a warning is printed. |
--raw |
Dump the received bytes verbatim, with no header/content detection at all. Requires an output file. See below. |
--dry-run |
Perform the real receive, run detection, but skip the file write — reports what would have been written. |
-v, --verbose / -q, --quiet |
See Verbosity. |
-h, --help |
Print help. (--version/-V is top-level only: sde --version, not sde get --version.) |
--raw: for capturing an arbitrary byte stream that carries no serial
header at all — for example, a hand-written assembly program that just writes
bytes to the port. --device still selects baud rate and flow control, and the
per-device idle timeout (500 ms for PC-1600 family, 5000 ms for PC-1500 family)
that normally serves as a fallback end-of-transfer detector becomes the only
way the transfer is considered complete — no header/length parsing is
attempted, so header-shaped bytes inside the stream are never misinterpreted.
If a header matching the same device family as --device is found, it's
stripped from the saved bytes (verbose: "Stripping detected header"); a header
for the wrong family is left in place (verbose: "Not stripping <flavor>
header, as device is <device>"). After receiving, sde prints the byte count
and a 16-bit sum (mod 65536) of the saved bytes, for a manual cross-check
against a checksum the sender computes independently — printed even under
--dry-run, since it describes what was received, not whether it was saved.
sde put [options] <input-file>
Reads <input-file> and sends it to the Pocket Computer. Content is always
detected from the file's bytes, never its name.
| Option | Description |
|---|---|
-d, --device <device> |
Target device. Optional if the file already carries a recognized header — the device is then inferred from it. A PC-1600 header always means pc1600 (or pc1600emul, if given); a PC-1500-family --device is overridden with a warning. A CE-158 header with a PC-1600-family --device is an error. Without a header and without --device, defaults to pc1500. |
-p, --port <port> |
Serial port name (auto-detected if omitted). |
--flowcontrol |
Enable RTS/CTS hardware flow control (pc1600 / pc1600emul only; CTS is used while sending). Off by default: a pc1600 transfer is then paced byte-by-byte like pc1600emul. With it, a real pc1600 is sent unpaced and the handshake throttles the transfer. Only try this if put fails with ERROR 142, and set RCVSTAT "COM1:",24 on the PC-1600 to match; see Sharp PC-1600. |
-f, --format <format> |
For headerless ASCII BASIC input: binary (default when omitted) tokenizes it before sending; ascii sends it line-by-line, untokenized (slower; mirrors the device's CLOADa/ASCII load). On input detected as text, binary forces it to be tokenized as a BASIC listing — the override when a listing is not recognized as BASIC. Has no effect on machine language (always sent as raw binary) or on input that already has a header (always sent as-is). |
--start-address <hex> |
Load address for a headerless machine-language input (e.g. 38C5 or 0x38C5). Required to send headerless machine code — see below. |
--run-address <hex> |
Auto-run address for a headerless machine-language input; requires --start-address. Defaults to 0xFFFF (no auto-run) if --start-address is given without it. |
--raw |
Send a headerless machine-language file exactly as read, unmodified — even without --start-address. Overrides the error below. |
--force |
Disk images and folders only: replace an existing file of the same name. |
--dry-run |
Report what would be sent (header present/added/omitted, size, addresses), without opening the serial port. |
-v, --verbose / -q, --quiet |
See Verbosity. |
-h, --help |
Print help. (--version/-V is top-level only: sde --version, not sde put --version.) |
Header handling:
- A file that already carries a recognized CE-158/PC-1600 header is sent as-is.
- ASCII BASIC input is tokenized and wrapped in a freshly built header — unless
--format asciiis given explicitly, in which case it's sent line-by-line, untokenized, with no header at all (see-f/--formatabove). - Headerless machine-language input needs
--start-address—sdebuilds a header from it (and--run-address, default0xFFFF). Without a header,--start-address, or--raw,putrefuses rather than guessing what the bytes are. - Plain text (see Text) is converted to the Sharp character set with CRLF
line ends and a trailing
1Aand sent without a header — PC-1600 family only. For the PC-1500, use--raw.
sde dir <image>.floppy.yaml[:<side>[:<pattern>]]
sde get [options] <image>.floppy.yaml:<side>:<NAME.EXT|pattern> [<output-file-or-dir>]
sde put [options] <file>... <image>.floppy.yaml:<side>:[<NAME.EXT>]
sde put [options] <file>... <directory>
sde del [--force] [--dry-run] <image>.floppy.yaml:<side>:<NAME.EXT|pattern>...
See Disk images for the addressing and the "eject first"
rule. dir lists name, size, date and time, attributes (P write-protected, H
hidden, I unknown) and the file's type, found from its content (the directory does
not record it), plus the free space. Without a side it lists both.
What put stores and get returns, by what the file contains:
| Content | put stores (default) |
get writes (default) |
Options |
|---|---|---|---|
| BASIC listing | tokenized, 16-byte header, NAME.BAS |
de-tokenized listing, NAME.bas |
put -f ascii: store the listing as an ASCII program (what SAVE "…",A writes). get -f binary: keep it tokenized with its header (NAME.bbas); add --skip-header to drop the header. |
| BASIC saved as ASCII | — | the listing, NAME.bas |
--raw for the stored bytes |
Text (.ASM, .CFG, …) |
Sharp character set, CRLF, trailing 1A |
UTF-8, --eol line ends, 1A removed; name kept |
put -f binary: tokenize it as BASIC after all (see below). get -f binary / --raw: the stored bytes |
| File with a PC-1600 header | unchanged | machine code: unchanged (header kept, .bin if no extension); BASIC: see above |
get --skip-header: without the header. put --start-address with a header is an error. |
| Machine code without a header | error — give --start-address (24-bit: bank in the top byte, e.g. 1C000 = bank 1, C000) and optionally --run-address (default: no auto-start), which adds a header; or --raw |
— | |
| File with a PC-1500 header, SDAR, SDAV | error (PC-1500 data) | — | put --raw stores it anyway |
| Anything else | error, as for headerless machine code | unchanged, with a warning |
BASIC or text? A headerless file counts as a BASIC listing only if its lines look
like listing lines: at most 4 spaces, a line number, exactly one space or tab, then the
statement. The test is strict on purpose. Text taken for BASIC would be tokenized and
ruined — a data file such as a game's high-score list ( 50 1 MARTIN)
must stay text — while a listing taken for text still works: it is stored as an ASCII
program, which the PC-1600 LOADs. If a real listing is taken for text, put -f binary
forces tokenizing; it fails if the file has no numbered lines. Line numbers need not
increase (#SEGMENT restarts them).
put names the file on the disk after the host file (upper-cased; BAS for BASIC,
otherwise the host extension). A name after the side renames it (one input file only):
sde put hello.bas disk.floppy.yaml:A:HI.BAS. An existing file is only replaced with
--force. With a wildcard, get writes into a directory (default: the current one);
--raw writes every file exactly as stored. --dry-run never changes the image.
--port, --flowcontrol and --device pc1500/pc1500a don't apply to images.
A folder as the disk. With an existing directory as the last argument, put
stores the files there exactly as on a floppy side: same conversions, 8.3 names, BASIC
tokenized as NAME.BAS, -f ascii for an ASCII program, --force to replace,
--dry-run. New files are created upper-case; an existing file is matched ignoring
case and keeps its host spelling. A host file whose name isn't 8.3 is refused (rename
it first). This prepares a folder for Calc-U-1600's host drive, see
below. get, dir and del don't take a folder: the
files are ordinary host files, so read one with sde convert DIR/PROG.BAS.
sde get [options] <tape.wav>[:<NAME>] [<output-file-or-dir>]
sde convert [options] <tape.wav> [<output-file-or-dir>]
sde put [options] <tape.wav>[:<NAME>] [<image>.floppy.yaml:<side>:[<NAME.EXT>]]
sde convert [options] <input> [<output.wav>] -f wav
sde get [options] -f wav [<output.wav>]
sde get [options] -f wav <image>.floppy.yaml:<side>:<NAME.EXT|pattern> [<output-or-dir>]
sde put [options] <input> -f wav
See Cassette WAV files. A WAV source is found by content;
-f wav makes a WAV the target. Reading a WAV takes get's --format,
--skip-header, --eol and --dry-run; files are named after the file on the tape
(LANDER.bas). The input to -f wav is anything put could send: a BASIC listing
(tokenized for --device, default pc1500), tokenized BASIC or machine code with a
header, headerless machine code with --start-address, a Reserve Area (SDAR), or
another WAV (re-encoded clean).
Option (with -f wav) |
Description |
|---|---|
--name <NAME> |
File name on the tape (at most 16 characters). Default: the name in the header, else the input file's name, upper-cased. CLOAD "NAME" looks for it. |
--leader <SECONDS|rom> |
Lead-in tone before each file. Default about 2 s (PC-1500) / 3 s (PC-1600); rom: the ROMs' own length (about 8 s / 3.3 s). |
--sample-rate <HZ> |
Sample rate of a written WAV file, 16000–192000 (default 48000; below 16 kHz the PC-1600's CLOAD fails). Playback (put) uses the audio device's rate. |
--clean |
put -f wav of a WAV: decode it and play a freshly encoded tape instead of the recording. |
-y, --yes |
put -f wav: start at once instead of waiting for Enter. |
Playing to the pocket computer (put -f wav). Connect the computer's headphone
output to the cassette interface's tape input: the plug the CE-150 or CE-1600P
normally puts into the recorder's EAR socket. Turn the volume up (start high and
lower it if loading fails). sde prints the format, length and what to type,
then waits for Enter: type CLOAD (CLOAD M for machine code) on the pocket computer
first, then press Enter. A WAV input is played as recorded (resampled to the device);
--clean plays a re-encoded copy instead, which helps with a noisy recording.
--dry-run reports the length without touching the audio device.
sde convert [options] <infile> [<outfile>]
No Pocket Computer or serial connection involved. Direction is chosen from file content:
- ASCII BASIC in → tokenized BASIC out (wrapped in a CE-158 or PC-1600 header,
chosen by
--device), so the result can be sent later withputor read back withconvert. - Tokenized BASIC in → ASCII BASIC out. The input must carry a CE-158 or PC-1600 header; a headerless tokenized payload is rejected.
- Machine code with a CE-158 or PC-1600 header in → the bare machine code out,
header stripped (
prog.bin→prog.pure.bin). The load and run addresses are printed, since the output no longer records them. Bytes after the length the header records are dropped. - Headerless input with
--start-address→ machine code behind a new MACHINE header, CE-158 or PC-1600 per--device(prog.bin→prog.ce158.bin/prog.pc1600.bin).--start-addressis what marks the input as machine code, so it is required: headerless input that is not a BASIC listing is an error without it. An input that already has a header is an error too (strip it first).
Reserve Area and Variables are rejected.
.bas is the extension for ASCII listings, .bbas for tokenized BASIC, .bin for
machine code (see the extension table). The
input's content decides what happens, never its extension; with -v, an extension that
disagrees with the content is mentioned. Tokenized BASIC named PROG.bas (as a PC-1600
disk names it) whose listing would get its own name is renamed to PROG.bbas first,
and the listing is written to PROG.bas; the summary line says so, -v says why. If
PROG.bbas exists, that is an error — give an <outfile>. A machine-code
output name drops a .pure/.ce158/.pc1600 tag already in the input name
(prog.ce158.bin → prog.pure.bin, not prog.ce158.pure.bin); the CE-158 header's
filename field is that base name, upper-cased. An explicit <outfile> without an
extension gets .bin. convert never overwrites its input file: if the output name
would be the input (other than the rename above), it stops with an error — give an
<outfile>.
With no <infile> and data on stdin, sde reads stdin and writes the converted bytes
to stdout (in that mode, direction is always ASCII→tokenized, and --start-address is
not accepted).
| Option | Description |
|---|---|
-d, --device <device> |
pc1500 (default), pc1500a, pc1600, pc1600emul. Selects the keyword table + header flavor when tokenizing, and the header flavor for --start-address. Ignored when de-tokenizing or stripping (device comes from the input header). |
--eol <eol> |
Line ending for a de-tokenized listing: auto (default; CRLF on Windows, LF elsewhere), lf, crlf, cr. Ignored when tokenizing — CR and CRLF input are always accepted. |
--start-address <hex> |
Add a machine-code header with this load address to a headerless input (e.g. 38C5 or 0x38C5). PC-1500: 16-bit. PC-1600: 24-bit, bank in the top byte (e.g. 1C000 = bank 1, C000). |
--run-address <hex> |
Auto-run address for the added header; requires --start-address. Default: no auto-start (FFFF; on the PC-1600 in the load address's bank, as BSAVE writes it). |
-v, --verbose |
Explain each step on stderr: bytes read, detected content, keyword table / header used, the header found or added (flavor, size, filename, load/run address, payload length), dropped trailing bytes, bytes written. |
sde convert myprogram.bas
→ myprogram.bbas (CE-158 header + tokens)
sde convert -d pc1600 myprogram.bas out.bbas
→ out.bbas (PC-1600 header + tokens)
sde convert myprogram.bbas
→ myprogram.bas (readable listing)
sde convert S3/GAME.BAS
→ GAME.BAS renamed to GAME.bbas, listing in GAME.bas
cat myprogram.bas | sde convert > myprogram.bbas
sde convert prog.bin --start-address 38C5
→ prog.ce158.bin (CE-158 MACHINE header, load 38C5, no auto-start)
sde convert -d pc1600 prog.bin --start-address 1C000 --run-address 1C000
→ prog.pc1600.bin (PC-1600 MACHINE header, bank 1)
sde convert prog.ce158.bin
→ prog.pure.bin (header stripped)
With -v:
$ sde convert -v prog.bin --start-address 38C5
Read 7 bytes from prog.bin
Detected content: unrecognized content
No header; treating the input as machine code (--start-address)
Adding CE-158 MACHINE header (27 bytes): name=PROG load=0x38C5 run=0xFFFF (no auto-start), payload 7 bytes
Wrote 34 bytes to prog.ce158.bin
Converted prog.bin -> prog.ce158.bin (CE-158 header added, load=0x38C5)
sde info [-v] <file>
Reads a local file and prints what it holds: a one-line verdict, then details. Nothing is changed and no Pocket Computer is involved.
$ sde info prog.ce158.bin
LH5801 machine code, CE-158 header
kind: ml-lh5801
file size: 34 bytes
header: CE-158, 27 bytes at offset 0
name: PROG
load address: 0x38C5
end address: 0x38CB
run address: none (0xFFFF, no auto-start)
payload: 7 bytes
| Verdict | Details |
|---|---|
PC-1500 tokenized BASIC program, CE-158 header / PC-1600 tokenized BASIC program, PC-1600 header |
header, name (CE-158), payload size, line count, line-number range |
LH5801 machine code, CE-158 header / Z80 (SC7852) machine code, PC-1600 header |
header, name (CE-158), load / end / run address (PC-1600 with its bank), payload size |
ASCII BASIC listing |
line count, line-number range, line endings, 1A end mark, which keyword tables (PC-1500, PC-1600) tokenize it |
Reserve Area, CE-158 header / Reserve Area, SDAR text |
name, number of assigned keys |
Variables, CE-158 header / Variables, SDAV text |
name, number of variables |
Plain text |
line count, line endings, 1A end mark |
probably LH5801 machine code, no header (heuristic) / probably Z80 (SC7852) machine code, no header (heuristic) / binary data, no header; CPU not recognized |
size |
PC-1500 (CE-150) cassette WAV: … / PC-1600 (CE-1600P, MODE 0) cassette WAV: … |
sample rate, bits, channels, length, level; for one file on the tape its tape speed, save date (PC-1600) and the details of its content as above; for several, one line per file. Damaged or unsupported files appear as warning: lines with their position on the tape. |
WAV file, no PC-1500 / PC-1600 tape signal found |
the audio facts |
Problems appear as warning: lines: a payload shorter than the header says, trailing
bytes after it, 00 noise before the header, a header cut short, a BASIC payload that
does not de-tokenize.
Which CPU. Headers don't record the CPU. A CE-158 header is reported as LH5801 code
(PC-1500 family), and a PC-1600 header as Z80 code, since the PC-1600's BSAVE runs on
its Z80. If the payload clearly looks like the other CPU, a code looks like: line
says so. For a headerless binary the CPU is a guess, and is labelled as one. The
bytes are decoded as both instruction sets, looking for undocumented opcodes,
relative branches that land on instruction boundaries, and typical instructions,
compared with what random bytes give. Files of 1 KB or more are judged in 512-byte
windows, so data tables don't drown out code. Tested on 35 Z80 programs and slices of
the PC-1500 ROM, no program got the wrong CPU; files under 16 bytes, and code too
short or too mixed with data, are reported as not recognized. -v prints the numbers
behind the guess on stderr.
The kind: line is the same one-word token the library returns from
sde_file_kind / sde_file_info (ml-lh5801, basic-pc1600, raw-z80, …) — see
File kind (program loaders).
sde config get <key>
sde config set <key> <value>
See Config file for the keys and the precedence rules.
The PC-1500 uses the CE-158X's USB port (U1). Before each session, redirect
serial I/O to that port (must be re-entered after each power cycle or NEW):
SETDEV U1,CI,CO
Issue the load command on the PC-1500 before starting put:
| To receive... | Command |
|---|---|
| Tokenized BASIC (binary, default) | CLOAD |
| ASCII BASIC | CLOADa |
| Machine language | CLOADM |
sde sends BASIC as tokenized binary by default for the fastest, most reliable
transfer. Machine language is always binary and always requires CLOADM —
CLOAD/CLOADa only load BASIC programs.
Start sde get first, then issue the save command on the PC-1500:
| To send... | Command |
|---|---|
| Tokenized BASIC (binary, default) | CSAVE or CSAVE"filename" |
| ASCII BASIC | CSAVEa |
| Machine language | CSAVE M start,end[,run] (or CSAVEM) |
Machine language is always saved with CSAVE M, giving the start/end addresses
of the memory block to send. See CSAVE M in the CE-150
reference
for the full syntax.
SETCOM "COM1:",9600,8,N,1,N,N
INIT "COM1:",4096
OUTSTAT "COM1:"
RCVSTAT "COM1:",28
For sending from the PC-1600 to the PC, additionally enter:
SNDSTAT "COM1:",28
These configure the port at 9600 baud, 8 data bits, no parity, 1 stop bit, with
RTS/CTS flow control disabled, and a 4096-byte buffer. This is the default:
sde paces the transfer itself.
Hardware handshaking is optional. To use it, pass --flowcontrol to sde get/
sde put and use 24 instead of 28 in RCVSTAT (for put) or SNDSTAT
(for get). The settings must match: a PC-1600 set to 24 while sde runs
without --flowcontrol can end in ERROR 142.
Issue the load command before starting put. The PC-1600 auto-detects
ASCII vs. binary:
LOAD "COM1:" ' or LOAD "COM1:",R to auto-run after loading
Start sde get first, then on the PC-1600:
SAVE "COM1:"
| Command | Purpose |
|---|---|
SETCOM "COM1:",<baud>,<bits>,<parity>,<stop>,<xon>,<shift> |
Configure baud rate and protocol. |
INIT "COM1:",<buffer-size> |
Set receive buffer size (default after power-on is 40 bytes). |
OUTSTAT "COM1:" |
Enable dynamic RTS/DTR flow control. |
RCVSTAT "COM1:",<protocol>[,<timeout>] |
Receive handshake/timeout; 28 disables flow control (default for sde), 24 enables RTS/CTS (needs --flowcontrol). |
SNDSTAT "COM1:",<protocol>[,<timeout>] |
Send handshake/timeout; 28 disables flow control (default for sde), 24 enables RTS/CTS (needs --flowcontrol). |
SETDEV "COM1:"[,KI][,PO] |
Redirect INPUT/LPRINT/LLIST to the serial port. |
PCONSOLE "COM1:",<line-length>,<eol> |
Line length and EOL (0=CR, 1=LF, 2=CR/LF) for serial output. |
Standard Sharp BASIC source code, one line per line number. De-tokenized listings
always spell keywords out in full; when tokenizing for the PC-1500, the dotted
abbreviations the PC-1500 accepts (P. for PRINT, GOS. for GOSUB) are expanded:
10 FOR I=1 TO 10
20 PRINT I
30 NEXT I
This is get's default output for any BASIC program, whether the device sent
it as binary or ASCII.
Raw binary format as used by the Pocket Computer's cassette/serial interface. The PC-1500 family uses a 27-byte CE-158 header; the PC-1600 uses a 16-byte header. Both carry a type field (BASIC or machine language), a payload length, and — for machine language — load/run addresses.
Use --format binary with get to save the file in binary form (header
included by default, so the file can be sent straight back with put
unmodified). Machine language can only be saved in binary form: over serial,
where get defaults to --format ascii, give --format binary; from a disk image
or tape the format follows the content.
Any other UTF-8 text whose characters all exist in the Sharp character set (treated
as IBM code page 437) — assembler source, configuration files and the like. On the
PC-1600 (and on its disks) such a file is an "ASCII file": no header, CRLF line ends,
a final 1A byte. sde converts in both directions; a character with no equivalent
in the Sharp set is an error naming its line and column.
PC-1500/1500A only (CSAVEr/CLOADr); not reachable through convert, only
get/put. The binary payload behind a CE-158 header is a fixed 188 bytes: three
26-byte key-layer labels, followed by a 110-byte key-content pool. The pool is a
sequence of [key code][content bytes...] entries terminated by 0x00; each of the
three layers has six keys, with key codes 0x01-0x06 (layer 1), 0x11-0x16
(layer 2), and 0x09-0x0E (layer 3). A key with no assigned content has no entry.
Content bytes mix literal characters with 2-byte BASIC keyword tokens.
get --format ascii (the default) renders this as SDAR text:
; sde-reserve:1.0 pc1500
; Filename: MYAPP
[layer 1]
label: MENU
key 1: PRINT
key 2:
key 3:
key 4:
key 5:
key 6:
[layer 2]
...
[layer 3]
...
All three [layer N] sections and all six key N: lines are always present (content
may be empty). put accepts this text back and tokenizes it, erroring if the encoded
pool would exceed the 110-byte hardware limit. .sdar is the extension for this ASCII
form; get -f binary writes the tokenized form as .bsdar.
PC-1500/1500A only; not reachable through convert, only get/put. The binary
payload behind a CE-158 header is a sequence of records with no fixed total length —
each record is a 0x00 separator, a 4-byte prefix (record length, array dimension,
reserved, type), then data. A 0x88 type byte marks numeric data (8-byte
PC-1500-hardware BCD floats — see src/bcd.rs's doc comment for the exact byte
layout); any other type byte is a string element's length in bytes. A 0 dimension
byte marks a scalar; a nonzero value N marks an array of N + 1 elements (a
DIM'd variable).
get --format ascii (the default) renders this as SDAV text:
; sde-variables:1.0 pc1500
; Count: 3
; Filename: MYAPP
42
"Hi there!"
DIM (2)
1
2
3
; Count: is the number of top-level records (one per scalar or whole array); put
recomputes it from the parsed text and errors on a mismatch. A numeric array is a
DIM (<dimMax>) header followed by dimMax + 1 decimal lines; a string array is a
DIM $(<dimMax>)*<maxLen> header followed by dimMax + 1 double-quoted lines
(\, ", and non-printable bytes escaped as \\, \", \xHH). .sdav is the
extension for this ASCII form; get -f binary writes the binary form as .bsdav.
Whether the PC-1600 protocol has equivalent header types for either format is
unresearched — sde recognizes Reserve Area/Variables only behind a CE-158 (PC-1500)
header.
sde get myprogram.bas
On the PC-1500: SETDEV U1,CI,CO then CSAVE — sde de-tokenizes the binary
and writes a readable ASCII listing.
sde put myprogram.bas
On the PC-1500: SETDEV U1,CI,CO then CLOAD.
sde put --format ascii myprogram.bas
On the PC-1500: SETDEV U1,CI,CO then CLOADa.
sde put --start-address 38C5 --run-address 38C5 program.bin
On the PC-1500: SETDEV U1,CI,CO then CLOADM.
Start sde get first, then on the PC-1500, e.g. CSAVE M &38C5,&3A00:
sde get program.bin --format binary
Eject the disk in Calc-U-1600 first, then:
sde dir Progs.floppy.yaml
sde put hello.bas notes.txt Progs.floppy.yaml:A:
sde put monitor.bin Progs.floppy.yaml:A:MON.BIN --start-address C0C5
sde get 'Progs.floppy.yaml:A:*.BAS' listings/
sde del Progs.floppy.yaml:A:OLD.BAS
On the PC-1600: LOAD "X:HELLO.BAS", OPEN "X:NOTES.TXT" FOR INPUT AS #1,
BLOAD "X:MON.BIN".
Calc-U-1600's File ▸ Mount Directory… makes a folder the PC-1600 drive S3:, used
byte for byte like a RAM disk: LOAD expects tokenized BASIC named NAME.BAS. An
ASCII listing loads too, but only within the ROM's line-length limit, which many
programs from the internet exceed. Fill the folder with tokenized copies:
sde put -v *.bas ~/pc1600/s3/
Each listing is tokenized for the PC-1600 and stored as NAME.BAS; -v notes that
the file is binary despite the .BAS. To compare an ASCII load with a binary one,
store the listing as well, under a second name:
cp game.bas gamea.bas && sde put -f ascii gamea.bas ~/pc1600/s3/. Names must be 8.3.
On the PC-1600: LOAD "S3:GAME.BAS".
On the PC-1500 with CE-150: connect the computer's headphone output to the CE-150's
EAR plug (the one that normally goes into the recorder) and turn the volume up. Then:
sde put lander.bas -f wav # prints what to type, waits for Enter
Type CLOAD on the PC-1500, then press Enter on the computer. For machine code,
sde put mc.bin -f wav --start-address 7C01 and CLOAD M. For a PC-1600 with
CE-1600P, add -d pc1600 for a listing.
Record the tape with any audio program (mono or stereo, 44.1 or 48 kHz), then:
sde info tape.wav # which files survived, and how fast the tape ran
sde get tape.wav programs/ # every file on it
sde convert tape.wav clean.wav -f wav # a clean copy for playing back later
sde config set pc1600emul.port /tmp/my-emulator-dir
sde put myprogram.bas --device pc1600emul # uses /tmp/my-emulator-dir/calcu1600-rs232c.serial
sde get --device pc1600emul
Or rely on the built-in /tmp default and skip config set entirely, if your
emulator happens to create /tmp/calcu1600-rs232c.serial.
sde put --dry-run --start-address 38C5 program.bin
sde get myprogram.bas --port /dev/cu.usbserial-A50285BI
sde exits immediately with a port error (get/put)
The serial port wasn't found or auto-detection was ambiguous/unsupported on
your platform (see Serial Port Auto-Detection).
Use --port to specify it explicitly.
ERROR 67 on the PC-1500 when loading ASCII
A line in the program exceeds 80 characters. Load as binary instead (omit a
from CLOADa).
ERROR 61 on the PC-1500 when loading binary
The binary file lacks a CE-158 header. sde adds the header automatically when
sending a recognized ASCII/machine-language input; this shouldn't occur with
files produced by sde get. For a third-party binary, supply --start-address
or check it already has a valid header.
Machine-language program loads but reports a garbled/wrong header, or won't run
CLOAD only loads BASIC programs — it doesn't understand a machine-language
payload even with a correct header. Use CLOADM on the PC-1500. Likewise, use
CSAVE M start,end[,run] (not plain CSAVE) to send machine language to get.
put refuses a machine-language file with "needs --start-address"
The input has no recognizable header and no --start-address was given. Either
supply --start-address (and optionally --run-address), or pass --raw if
the bytes are intentionally headerless and should be sent exactly as-is.
get refuses with "machine language cannot be converted to ASCII"
The received content is machine language, and --format was ascii (the serial
default). Machine language can't be de-tokenized — pass --format binary.
Data corruption or incomplete transfer (PC-1500)
The PC-1500 requires paced transmission; sde applies a 1 ms delay between
bytes and a 300 ms pause after the header automatically. If problems persist,
check the USB cable and CE-158X connection.
The PC-1600 serial port stops responding Re-enter the full setup sequence on the PC-1600:
SETCOM "COM1:",9600,8,N,1,N,N
INIT "COM1:",4096
OUTSTAT "COM1:"
RCVSTAT "COM1:",28
ERROR 142 on the PC-1600
This is an RTS/CTS handshake error, typically on put. By default sde uses no
flow control, which means RCVSTAT "COM1:",28 (and SNDSTAT "COM1:",28) on the
PC-1600. If you still get ERROR 142 on put, you can try enabling flow
control: pass --flowcontrol to sde put and switch RCVSTAT on the PC-1600 to
RCVSTAT "COM1:",24. This depends on your USB adapter and wiring and may not
work.
Implemented: convert (offline tokenize/de-tokenize), get/put (serial
transfer) for BASIC programs, machine-language programs, text, Reserve Area, and
Variables (the latter two PC-1500/1500A-only, get/put-only), dir/get/put/
del on Calc-U-1600 floppy images, put to a folder used as a PC-1600 disk,
cassette WAV files (PC-1500 + CE-150, PC-1600 +
CE-1600P in MODE 0: read, write, play), and config (per-user defaults).
Won't implement (permanent decisions, not just currently-undone work):
terminalmode (interactive passive serial session) — the serial link isn't full-duplex-capable (only input or output can be redirected at a time), so an interactive terminal has little value here.--add-utils(serial utility BASIC sub-program injection onput).convert --dry-run.- Windows serial-port auto-detection — Windows machines routinely enumerate many COM
ports (Bluetooth, virtual devices, etc.), making auto-detection unreliable; always
pass
--portthere.
Not implemented (may change):
- Cassette WAVs: PC-1500
DAT/DEFfiles and PC-1600 ASCII and data files are recognized but not converted; a PC-1600 in MODE 1 writes no tapes of its own (it reads PC-1500 tapes, which sde writes with-d pc1500). Big-endian (RIFX), RF64 and compressed WAV files are not read. Other Sharp pocket computers' tapes (PC-1261, PC-1401, …) are not recognized. - Disk images: only CE-1600F floppies (
.floppy.yaml) and folders (putonly). PC-1600 RAM-disk card images, raw.imgdumps, and formatting a side (INIT) are not supported; real 2.5″ disks can't be read at all. - Whether the PC-1600 protocol has header types equivalent to Reserve Area/Variables
at all is unresearched —
sdedoes not recognize any PC-1600 header type byte beyond BASIC/machine language. - Dotted keyword abbreviations (
P.) are expanded for the PC-1500 only; the PC-1600's are not known yet. - PC-1600-only BASIC token values are limited to what the Java
Pc1600Keywords.javalists (no PC-1600 ROM source exists); unknown>= 0xE0byte pairs pass through opaquely duringconvert.
cargo test # byte-parity, round-trip, unit, and C-ABI tests
cargo clippy --all-targets -- -D warnings
The tree is not kept rustfmt-clean (CI's cargo fmt --check is advisory); format only
the code you change, not the whole tree.
The byte-parity test compares convert output with the Java tool's fixtures. The
CE-158 header's filename field is the one difference (sde upper-cases it) and is
compared as "don't care"; payloads match exactly.
src/keywords.rs is generated from the Java device/*Keywords.java sources by
tools/extract_keywords.py:
python3 tools/extract_keywords.py # rewrite src/keywords.rs
python3 tools/extract_keywords.py --check # CI drift check (also a cargo test)
get/put's serial transport, config file, and pacing/receive logic are unit
tested against an in-memory transport double (no real hardware needed to run
cargo test). Manually verified: real serial transfers against a real PC-1600 on
macOS, and port auto-detection on macOS. Still untested: real PC-1500/CE-158X
transfers, and port auto-detection on Windows and Linux.
Releases are cut with bin/release; see the comment header in
that script and CHANGELOG.md.
Polyform Noncommercial License 1.0.0 — free for any noncommercial purpose. Copyright 2026 Martin Erzberger.