xlink

data transfer and control system for the commodore 64/128

xlink allows connecting a Commodore 64 or 128 to a PC using a custom build USB adapter or a simple parallel port cable. A command-line client is used on the PC to transfer data to and from the remote machine memory, run programs on the remote machine or to initiate a hardware reset.

An interrupt-driven server on the remote machine listens to and executes the commands send by the client. The server can be temporarily loaded on the remote machine, or it can be permanently installed using a customized kernal rom. The latter has the advantage of being instantly available after power-up or reset, which makes the xlink system well suited for fast and easy cross development using a PC and a real Commodore machine.

The implementation of the underlying functionality is distributed as a shared library, making the functionality provided by xlink readily available for use in other programs.

The xlink client software and library is supported under Linux, MacOSX and Windows.

Downloads

Source

The source distribution includes the sources for the client, server, kernal and firmware as well as schematics and pcb layouts in KiCad format for building the hardware.

Latest stable is xlink-1.3.tar.gz.

All releases can be found under /download/xlink

Latest developments are available via github:

git clone https://github.com/hbekel/xlink

Binaries

Installing binary packages

Windows

Simply run the xlink-1.3.msi installer package and follow the on-screen instructions. All required libraries will be installed and the xlink command line client will be added to your PATH.

In order to access the parallel port on Windows XP or later you need to run xlink.exe as Administrator once to install the necessary drivers.

Now proceed to Installing the firmware.

MacOSX

Open a terminal, change to the directory containing the package file and run the following command:

$ sudo tar vxC / -f xlink-1.2-macosx.tar.gz

You will also need libusb. Use the homebrew package manager to install it:

$ brew install libusb

Now proceed to Installing the firmware.

Building and installing the Client from source

Linux & MacOSX

KickAssembler 3 and libusb must be installed.

On MacOSX, you can use the homebrew package manager to install libusb:

$ brew install libusb

On Linux you should use the package manager of your distribution to install libusb. If your distribution provides separate development packages, make sure you install the development package for libusb as well.

Also download KickAssembler 3 and copy the KickAss.jar file to a convenient location.

Extract the source tarball and check/adjust the following settings in the Makefile:

PREFIX=/usr/local
SYSCONFDIR=/etc
KASM?=java -jar /usr/share/kickassembler/KickAss.jar
  • PREFIX: The installation prefix, which defaults to /usr/local. Leave this unchanged on MacOSX.

  • SYSCONFDIR: This is the global system configuration directory, which defaults to /etc. For any PREFIX other than /usr this will result in $(PREFIX)/$(SYSCONFDIR).

  • KASM: The command to run KickAssembler. Adjust the path to KickAss.jar accordingly.

Now run make to build the client and library.

If all went well you can now run make install as root. This will result in:

$(PREFIX)/bin/xlink
$(PREFIX)/lib/libxlink.so
$(PREFIX)/include/xlink.h
$(SYSCONFDIR)/bash_completion.d/xlink

On Linux, the following udev rule will also be installed:

$(SYSCONFDIR)/udev/rules.d/10-xlink.rules

The standard DESTDIR variable can be used for a staged install.

Windows

The windows version can be cross-compiled with mingw32 under Linux or Cygwin. Use your package manager to install the mingw32 toolchain and libusb (including development packages if your distribution provides them).

Download KickAssembler 3 and copy the KickAss.jar file to a convenient location.

You will also need the wget utility to be installed.

Now check/adjust the following settings in the Makefile:

KASM?=java -jar /usr/share/kickassembler/KickAss.jar
MINGW32?=i686-w64-mingw32
  • KASM: The command to run KickAssembler. Adjust the path to KickAss.jar accordingly.

  • MINGW32: The prefix used for the mingw32 binaries, e.g. if gcc was installed as i686-w64-mingw32-gcc, the prefix is i686-w64-mingw32. This value depends on your distribution/architecture.

To install on Windows you will need to copy xlink.dll and inpout32.dll to your 32-bit system directory. On 32-bit systems this is C:\Windows\system32 while on 64-bit systems this is C:\Windows\SysWOW64.

You will also need to install libusb. Download the “Latest Windows Binaries” package from the libusb website, extract the archive and copy the MS32/dll/libusb-1.0.dll to your 32-bit system directory.

In order to access the parallel port on Windows XP or later you need to run xlink.exe as Administrator once to install the necessary drivers.

Building the Hardware

USB adapter

The USB adapter is implemented as a full-speed USB 2.0 device using Dean Cameras LUFA library on an Atmel at90usb162 chip. Unfortunately this chip is only available in SMD packaging (TQFP-32), and thus building the adapter requires a proper pcb and some soldering skill.

Schematics and pcb layout in KiCad format are included in the source distribution

List of parts

Reference Type Value Package/RM
C1 Ceramic capacitor 18pF 2.5mm
C2 Ceramic capacitor 18pF 2.5mm
C3 Tantalum capacitor 1uF 2.5mm
C4 Ceramic capacitor 100nF 2.5mm
C5 Ceramic capacitor 4.7nF 2.5mm
IC1 Microprocessor at90usb162 TQFP-32
J1 Userport connector 2x12 3.96mm
P1 USB Socket USB-A Print
P2 Pin Header, 2 Jumpers 2x2 2.54mm
JP1 Pin Header, 1 Jumper 1x2 2.54mm
R1 Precision Resistor 22Ω 6.5mm, ∅ 2.5mm
R2 Precision Resistor 22Ω 6.5mm, ∅ 2.5mm
R3 Resistor 1 MΩ 6.5mm, ∅ 2.5mm
R4 Precision Resistor 1.8 KΩ 6.5mm, ∅ 2.5mm
R5 Precision Resistor 1.8 KΩ 6.5mm, ∅ 2.5mm
R6 Precision Resistor 1.8 KΩ 6.5mm, ∅ 2.5mm
R7 Precision Resistor 1.8 KΩ 6.5mm, ∅ 2.5mm
R8 Precision Resistor 1.8 KΩ 6.5mm, ∅ 2.5mm
R9 Precision Resistor 1.8 KΩ 6.5mm, ∅ 2.5mm
R10 Precision Resistor 1.8 KΩ 6.5mm, ∅ 2.5mm
R11 Precision Resistor 1.8 KΩ 6.5mm, ∅ 2.5mm
R12 Precision Resistor 820 Ω 6.5mm, ∅ 2.5mm
R13 Precision Resistor 1.8 KΩ 6.5mm, ∅ 2.5mm
X1 Quartz Crystal 8Mhz HC49/U-S

For the resistors labelled “Precision resistor” use “metal film” or “precision” types.

For C3 an electrolytic 1uF capacitor should work as well.

Placement on board

placement

(microprocessor excluded)

The microprocessor is mounted on the bottom side. All other components are mounted on the top side.

Assembly Instructions

First solder the microprocessor to the bottom side. When the bottom side is facing up and the usb connector is at the top, pin one of the microprocessor (designated by the corner dot on the case) needs to be located in the top left corner.

Then solder the small components (resistors, capacitors and crystal).

Note that the tantalum capacitor C3 is polarized. There should be a small plus sign printed on the body, denoting the positive lead. It must face towards the microprocessor.

Now add the USB socket. Solder the contacts first, making sure the socket sits flat and smooth on the pcb. Now slightly bend the tongues extending through the larger holes. Make sure they don’t touch the ground plane. Then simply fill the large holes with solder.

Now you may optionally solder the pin header P2. This header is only required if you need to reprogram the microprocessor and the xlink bootloader command for some reason fails to enter the dfu bootloader. In this case, enter the bootloader manually.

The pin header JP1 configures the adapter for use in the C64 or the C128. If you only plan to use the adapter in a C64 you may simply bridge this jumper with a piece of wire.

Finally solder the userport-connector. It has proven practical to first bend all the solder tongues towards each other until they touch in the middle of the connector. Then you can gradually “wiggle” the pcb in between the tongues. The tongues now hold the pcb tightly, making a good physical connection before soldering. Although not all tongues need to be soldered, it is recommended to do so to increase stability.

Configuring the adapter for the C128

The JP1 header needs to remain open if the adapter is used in a C128. This is necessary because the reset line provided at the C128 userport only resets peripheral devices but not the C128 as a whole.

To enable hardware resets, an additional wire has to be run from the top pin of the header to the real reset line of the C128. This line can be accessed at PIN 11 of U57 (located inside the metal box containing the VIC) or at the right leg of R23 (located just below the reset switch).

If you want to use the first or second revision of the adapter in a C128 you will need to desolder or cut the trace connecting the userport reset line and instead connect this trace to the real reset line.

You’re now ready to build and install the firmware.

Manually entering the bootloader

If for some reason the xlink bootloader command fails to set the microprocessor in dfu mode, you can force it to enter the bootloader by setting jumpers on the pin header in the following sequence:

hwbe-sequence

After that the microproccessor should be in dfu mode.

Parallel Port Cable

This is a simple 8-bit parallel transfer cable connecting the PC Parallel Port to the Userport of the Commodore:

PC Parallel Port Commodore Userport
GND (18-25) GND (1, 12, A, N)
D0 (2) PB0 (C)
D1 (3) PB1 (D)
D2 (4) PB2 (E)
D3 (5) PB3 (F)
D4 (6) PB4 (H)
D5 (7) PB5 (J)
D6 (8) PB6 (K)
D7 (9) PB7 (L)
BUSY (11) PA2 (M)
STROBE (1) /FLAG2 (B)

A shielded cable is recommended. The cable shield should be connected to ground on both sides. A 0.1uF capacitor between shield and ground at the Userport side can also help to reduce noise. Improper shielding will most likely cause transfer errors.

Check that your parallel port is configured for bidirectional transfer in your BIOS. This is either called PS/2, EPP, ECP, EPP/ECP or simply bidirectional. If in doubt, anything but standard should do.

Please note that there is no electrical protection of any kind. The parallel port pins are directly connected to port B of CIA2. To prevent damage to the parallel port or CIA chip you should always power down both the PC and Commodore before inserting or removing the cable. Also turn off the Commodore during boot, reboot or shutdown of the PC.

Reset Circuit

This optional circuit is required for the parallel port cable in order to perform a hardware reset of the target machine.

reset circuit
Line Port Pin
INIT Parallel Port 16
RESET Userport 3
VCC Userport 2
GND Userport 1, 12, A, N

For use with a C128, the RESET signal needs to be connected to the real reset line on the C128 board (Pin 11 of U57 or right leg of R23).

This circuit holds the Commodore RESET line low as long as the parallel port INIT line is held low. The parallel port lines themselves may have been left in an unknown/random state after the PC wakes up from suspend or after the port has been accessed by another program. This means that if the INIT line happens to be permanently low, the Commodore will remain in a state of permanent reset and appear frozen or even dead (showing just a black screen after power up). In this case running xlink reset will bring the parallel port lines back to a defined state and the Commodore will resume operation.

Building the Firmware

You will need gcc-avr to build the firmware for the usb adapter.

Optionally you may want to export the XLINK_SERIAL environment variable before building. This will make the adapter report a specific USB serial number.

Adding a serial number is only useful if you’re planning to connect more than one Commodore machine to the same PC via USB.

Now run make firmware. This will create the file xlink.hex in the subdirectory driver/at90usb162/.

Installing the Firmware

Make sure the Commodore is turned off and insert the adapter into the userport. Connect it to the PC using the USB cable. Now power on the Commodore.

Linux & MacOSX

Listing USB devices

On Linux, the lsusb command lists the usb devices. On MacOsx, the command system_profiler SPUSBDataType can be used. Alternatively, go to Applications -> Utilities -> System profiler -> Hardware -> USB.

Programming the firmware

Make sure dfu-programmer is installed on your system.

List the usb devices to check that the adapter has been properly enumerated. It should be reported as Atmel Corp. at90usb162 DFU bootloader.

Now install the firmware using the following commands:

$ dfu-programmer at90usb162 erase
$ dfu-programmer at90usb162 write xlink-firmware-1.1.hex
$ dfu-programmer at90usb162 reset

If you have build the firmware from source you can use make firmware-install.

If all went well, wait a few seconds and check the usb device list again. The adapter should now appear as 1d50:60c8 OpenMoko, Inc..

On Linux, given that you have already installed the client software, udev should now have created a symlink to the device at /dev/xlink. If not, install the client software and run the following commands as root:

udevadm control --reload-rules
udevadm trigger

This will make sure that the udev rules installed for the adapter take effect.

The adapter is now ready to use.

Windows

Things are a bit more complicated on windows. First make sure that you have properly installed libusb as described in Building and installing the Client. The windows installer package does this for you.

When you power up the adapter for the first time, windows will ask for a driver. Cancel the driver installation wizard. Instead, use Zadig to register the device for use with libusb-win32.

Now install the firmware over USB using Atmels FLIP tool.

Once the firmware is installed windows should ask for a driver again. (If not, power cycle the Commodore once to force re-enumeration). Use the Zadig tool again to register the device, but this time use the WinUSB driver.

The adapter is now ready to use.

Running the server on the target machine

RAM based server

Use the client to create a server.prg by using the server command:

xlink server server.prg

The resulting server.prg can be loaded on the C64 and started with RUN. See xlink help server for more information.

To create a server program for the C128, specify the machine type using the --machine option:

xlink server --machine=c128 server.prg

Manual bootstraping

If you have no initial means of transferring the server.prg from your PC to your target machine you can type in a short basic program (C64, C128). Just run it and follow the on-screen instructions.

ROM based server

Use the client to patch a kernal image to include the server:

xlink kernal kernal.rom xlink.rom

This will take the existing kernal.rom image and patch it to create a kernal image called xlink.rom that you can install in your C64.

To patch a C128 kernal image, use the --machine option to specify the target machine:

xlink kernal --machine=c128 kernal.rom xlink.rom

The patch will always be applied to the last 8192 bytes of the input file. This allows patching combined 16 or 32k rom images of the C64C or C128, which contain the kernal code in the upper 8k.

The kernal includes the server and runs it as part of the system irq.

Note that tape io has been removed in favor of the server. Attempts to load or save to tape in direct mode will display the error message “TAPE IO DISABLED” on both machines.

To speed up development cycles the reset routines on both machines have been modified.

On the C64, the startup memory check has been made optional and is skipped by default. To perform a memory check you will have to hold down the Commodore key during powerup/reset.

On the C128, autoboot from disk has been made optional and is skipped by default. To perform an autoboot you will have to hold down the Control key during powerup/reset.

Usage

For an overview of available commands run xlink --help

xlink 1.3 Copyright (C) 2015 Henning Bekel <h.bekel@googlemail.com>

Usage: xlink [<opts>] [<command> [<opts>] [<arguments>]]...

Options:
  -h, --help                    : show this help
  -q, --quiet                   : show errors only
  -v, --verbose                 : show verbose debug output
  -d, --device <path>           : transfer device (default: /dev/xlink)
  -M, --machine                 : machine type (default: C64)
  -m, --memory                  : C64/C128 memory config (default: 0x37/0x00)
  -b, --bank                    : C128 bank value (default: 15)
  -a, --address <start>[-<end>] : address/range (default: autodetect)
  -s, --skip <n>                : Skip n bytes of file

Commands:
  help  [<command>]            : show detailed help for command

  kernal <infile> <outfile>    : patch kernal image to include server code
  server [-a<addr>] <file>     : create server program and save to file
  relocate <addr>              : relocate currently running server

  reset                        : reset machine (requires hardware support)
  ready                        : try to make sure the server is ready
  ping                         : check if the server is available
  identify                     : identify remote server and machine type

  load  [<opts>] <file>        : load file into memory
  save  [<opts>] <file>        : save memory to file
  poke  [<opts>] <addr>,<val>  : poke value into memory
  peek  [<opts>] <addr>        : read value from memory
  fill  <range>  <val>         : fill memory range with value
  jump  [<opts>] <addr>        : jump to specified address
  run   [<opts>] [<file>]      : run program, optionally load it before
  <file>...                    : load file(s) and run last file

  benchmark [<opts>]           : test/measure transfer speed
  bootloader                   : enter dfu-bootloader (at90usb162)

To get detailed help for a specific command use xlink help <command>.

Device autodetection

When no device is specified, xlink will first try to connect via USB, using the product and vendor id for the adapter. If no usb adapter is found, it will try to use a parallel port instead. On Linux the default parallel port is /dev/parport0. On windows, the default parallel port is assumed to be available at io port 0x378.

Specifying the device

The --device option can be used to specify a device on the commandline. Alternatively, the environment variable XLINK_DEVICE can be used.

On Linux, both the usb adapter and the parallel port are accessed via their respective device nodes, i.e. /dev/xlink for usb or /dev/parport0 for the (first) parallel port.

On MacOSX, the usb device is specified by using the literal string usb (this is the default).

On Windows, the usb device is specified by using the literal string usb. The parallel port is specified via its port address, (usually) 0x378.

The shared memory driver can be selected by using the literal string shm. This driver implements a virtual port in shared memory that can be accessed by other programs to emulate the Commodore side (see Vice emulation).

Specifying the target machine

The --machine option can be used to specify the target machine. Alternatively, the environment variable XLINK_MACHINE can be used. Legal values are c64 or c128. The default value is c64.

Setting the machine type to C64 when connecting to a C128 will cause the reset and ready commands to change to C64 mode automatically.

Multiple usb devices

In order to use multiple usb adapters connected to the same PC, the adapters need to be programmed with different usb serial numbers (See Building the Firmware). On Linux you can then use udev rules to create device symlinks based on those serials. On windows, the desired serial number can be attached to the device specification, e.g. to use the device with the serial number 12345 you’d use usb:12345.

Vice emulation

The win32 vice build is based on vice 2.4.19 and includes xlink emulation support.

To build this version yourself, apply the corresponding patch to revision 29590 of the vice svn tree:

$ svn checkout svn://svn.code.sf.net/p/vice-emu/code/trunk@29590 vice
$ cd vice
$ patch -p0 -i vice-2.4.19-r29590-xlink-1.2.patch

The resulting source tree will likely not build under systems other than win32 or posix.

Add the following line to the vice config file:

UserportXlink=1

Alternatively pass the command line option -xlink. Note that if you save your settings in vice, the current xlink configuration will be saved as well, even though it does not appear anywhere in the menus.

Now load the server program in vice or install an image of the xlink kernal. To tell xlink to use the shared memory driver pass --device shm or export XLINK_DEVICE=shm.

Note that the transfer speed will depend on the performance of your machine and the vice emulation speed. On modern systems, this should be on par with the speed obatained on real hardware. On older systems the transfer speed might be somewhat decreased. For example, on my trusty old 1Ghz single-core box the transfer speed is limited to about 13kb/s.

Commands in general

Multiple commands can be specified in sequence. For example, the load and run commands can be combined like this:

$ xlink load myprog.prg run

This is equivalent to

$ xlink load myprog.prg
$ xlink run

Execution is aborted when one of these commands fails, so that subsequent commands are not executed.

Handling of file arguments

Specifying a single file as the only argument will automatically ready the server, load the file and run it. Thus the following command

$ xlink myprog.prg

is equivalent to

$ xlink ready run myprog.prg

Specifying multiple files in sequence will automatically ready the server, load all files into memory and the run the last file. Thus the following command

$ xlink charset.prg sprites.prg main.prg

is equivalent to

$ xlink ready load charset.prg load sprites.prg run main.prg

Numeric arguments

Numeric arguments may be specified in decimal or hexadecimal notation, where hexadecimal arguments must be prefixed with 0x.

For example, --address 0xC000 is equivalent to --address 49152.

Memory configuration

The effective memory configuration used on the remote machine prior to reading and/or writing values into memory can be controlled via the --memory and --bank options.

On the C64, the value specified by the --memory option is applied to the processor port at $01. The --bank value is ignored.

On the C128, the value specified by the --memory option is applied to the MMU control register at $ff00. Alternatively, the --bank option can be used to specify one of the fifteen predefined memory configurations of the C128. If both options are present, --memory takes precedence over --bank.

Memory ranges

Memory ranges are specified with the start address inclusive and the end address exclusive. This means that when using a command like

$ xlink save -a 0xC000-0xD000 file.bin

the last value saved will be the content of the memory location $CFFF.

To include the very last memory location use 0x10000 as the end address.

Transfer Commands

Load

xlink load [--address <start>[-<end>] [--memory <mem>] [--skip <n>] <file>

Load the specified file into memory

If no start address is given it is assumed that the file is a PRG file and that its first two bytes contain the start address in little-endian order.

Otherwise, if a start address is given it is assumed that the file is a plain binary file that does not contain a start address. In this case the entire file is loaded to the specified address. The --skip option may be used to skip an arbitrary amount of bytes at the beginning of the file. Thus you can load a file already containing a start address to a different location by specifying --skip 2 in addition to --address.

If an additional end address is specified, transfer will end as soon as the end address or the end of the file is reached, whichever comes first.

Optional memory and bank configuration values can be specified that will be applied on the remote machine before writing the transferred values to their destination.

If no memory or bank values are specified, values will be written using the default memory configuration for the respective machine. The only exception to this rule is that if the destination range overlaps with the io area, values will be written to the ram residing below the io area. This is a safety measure that prevents possible damage to the involved hardware io ports. In order to load data directly into the io area the memory configuration needs to be set explicitly.

On the C64, the value of --memory is applied via the processor port at $01. The bank value has no effect.

On the C128, the value of --memory is applied via the MMU configuration register at $ff00. Alternatively, the --bank value can be used to chose one of the fifteen predefined bank configurations of the C128. If both options are specified, --memory will take precedence over --bank.

If the server on the remote machine is running from RAM and is located in the same memory area as the data to be loaded then an attempt is made to relocate the server to a different location beforehand.

Save

xlink save [--address <start>-<end>] [--memory <mem>] [--bank <bank>] file

Saves the specified C64 memory area to file.

If the destination filename ends with .prg then the destination file will be prefixed with the supplied start address. If no address range is specified, then the basic program currently residing in C64 memory will be saved.

For a description of the –memory and –bank options see the load command.

Peek

xlink peek [--memory <mem>] [--bank <bank>] <address>

Read the byte at the specified C64 memory address and print it on standard output.

Poke

xlink poke [--memory <mem>] [--bank <bank>] <address>,<byte>

Poke the specified byte to the specified address.

If no memory or bank option is specified, then the default memory config for the respective machine will be used, so that values poked to the io area will have the expected effect.

Fill

xlink fill --address <start>-<end> <value>

Fill the specified memory area with <value>. The end address will default to 0x10000 unless explicitly specified.

Control Commands

Jump

xlink jump [--memory <mem>] [--bank <bank>] <address>

Jump to the specified address in memory. The stack pointer, processor flags and registers will be reset prior to jumping.

The --memory and --bank options can be used to configure the memory setup of the remote machine prior to jumping.

Run

xlink run [<file>]

Without argument, RUN the currently loaded basic program. With a file argument specified, load the file beforehand. If the file loads to the respective machines’ default basic start address, then assume its a basic program and RUN it, else assume it’s an ml program and jump to the files load address.

Reset

xlink reset

Reset the remote machine. Works without the server actually running on the remote side.

###Ping

xlink ping

Ping the server, exit successfully if the server responds.

Ready

xlink ready [<commands>...]

Makes sure that the server is ready. First the server is pinged. If it doesn’t respond immediately, the remote machine is reset. If the server responds to another ping within three seconds, then the remaining commands (if any) are executed.

This command requires the server to be installed permanently so that it is available after reset.

Maintenance Commands

Server

xlink server [--machine c64|c128] [--address <address>] <file>

Write a ram-based server program to <file>. Use address to specify the start address for the server code. If the address corresponds to the machines default basic start address, then the server can be started with RUN. This is the default if no address is specified.

Kernal

xlink kernal [--machine c64|c128] <infile> <outfile>

Patch the kernal image supplied via <infile> to include an xlink server and write the results to <outfile>. Note that the resulting kernal will no longer support tape IO.

The patch will always be applied to the last 8192 bytes of the input file. This allows patching the combined 16 or 32k roms of the C128, which contain the kernal code in the upmost 8k.

The reset procedures of the respective machines have been modified to speed up development cycles. On the C64, the memory check on startup has been made optional and is skipped by default unless the commodore key is held down during reset. On the C128, automatic boot from disk has been made optional and is skipped by default unless the control key is held down during reset.

Benchmark

xlink benchmark [--address <start>[-<end>] [--memory <mem>] [--bank <bank>]

Write random data into memory, then read it back and compare it to the original data while measuring the achieved transfer rates.

If no address range is specified, a default range of freely usable ram in the standard memory configuration is chosen for the respective machine.

If an address range is specified that overlaps with rom or io, the data received will differ from the data send, and the comparison will fail. Use the --memory and --bank options to disable rom and/or io for such ranges.

Bootloader

xlink bootloader

Prepare USB device for firmware updates. Enters the atmel dfu-bootloader.

API Documentation

Hello World

#include <stdio.h>
#include <xlink.h>

int main(int argc, char **argv) {

  // memory config to use
  uchar memory = 0x37;

  // bank value (reserverd, always use 0x00)
  uchar bank = 0x00;

  // start of screen ram
  unsigned short address = 0x0400;
    
  // "HELLO WORLD!" in C64 Screencode  
  char data[12] = { 0x08, 0x05, 0x0C, 0x0C, 0x0F, 0x20,
                    0x17, 0x0F, 0x12, 0x0C, 0x04, 0x21 };

  xlink_load(memory, bank, address, data, sizeof(data));
  return 0;
}

Compile and link against xlink:

$ gcc -o hello hello.c -lxlink

This program prints the message “HELLO WORLD!” into the upper left corner of the C64 screen by using xlink_load() to load the corresponding screencodes to the start of the screen memory area at $0400.

Since we didn’t specify the device to use beforehand, the device is autodetected as described device autodetection.

To find out which device has been detected, use xlink_get_device(). To find out whether a device has been detected at all, use xlink_has_device(). To set the device explicitly, use xlink_set_device().

Note that this program will fail silently if an error occurs. The next section describes how to handle errors.

Error handling

The global variable xlink_error points to a struct of type xlink_error_t which contains details about the last error that occurred.

Most xlink functions return true on success and false on failure. In the case of failure, an error code and a human readable error message can be obtained from xlink_error. In case of success, xlink_error is reset.

typedef struct {
  int code;
  char message[512];
} xlink_error_t;

code contains a code denoting the general type of error, and can be one of:

#define XLINK_SUCCESS          0x00
#define XLINK_ERROR_DEVICE     0x01
#define XLINK_ERROR_LIBUSB     0x02
#define XLINK_ERROR_PARPORT    0x03
#define XLINK_ERROR_SERVER     0x04

message contains a human readable error message.

For example, errors could be reported to the user using the following scheme:

if(!xlink_load(memory, bank, address, data, size)) {
  fprintf(stderr, "error: %s\n", xlink_error->message);
}

Debug messages

If debugging is enabled, detailed debug messages will be written to stdout.

void xlink_set_debug(bool enabled)

Enable or disable debug messages.

Versioning

uchar xlink_version(void)

Returns the library version as an unsigned char, where the high nibble contains the major version and the low nibble contains the minor version.

The API will remain consistent across major versions. Minor version bumps will only occur if new functions are added. Should functions be removed or other breaking changes become necessary this will be reflected by a bump of the major version.

Device handling

bool xlink_has_device(void)  

Returns true if a device has been sucessfully initialized.

char* xlink_get_device(void)

Returns a pointer to a string containing the device specification for the device currently in use. If no device has been initialized yet, the string will be empty. Do not modify the returned string.

bool xlink_set_device(char* path)

Sets the device specification to path and tries to initialize the device. Returns true if the device has been successfully initialized.

Server detection and control

bool xlink_ping(void)

Sends a strobe signal to the server and returns true if the server responds with an ack signal within 250ms.

Note that this only checks whether something on the C64 side has acknowledged the strobe. This might be an xlink server ready to receive commands, but it might also be an xlink server gone astray (hung in a previous transfer), or something else altogether.

To identify the server and its state reliably use xlink_identify().

bool xlink_identify(xlink_server_info_t* server)

Requests identification data from the server and populates the the xlink_server_info_t structure pointed to by server with the received results.

  typedef struct {
    char id[16];    // server identification
    uchar version;  // high byte major, low byte minor
    uchar machine;  // XLINK_MACHINE_C64
    uchar type;     // XLINK_SERVER_TYPE_{RAM|ROM}
    ushort start;   // server start address
    ushort end;     // server end address
    ushort length;  // server code length
    ushort memtop;  // current top of (lower) memory (0xa000 or 0x8000)
  } xlink_server_info_t;
  • id contains an identification string of up to 15 characters. The xlink server reports XLINK here.

  • version contains the server version, where the high nibble contains the major version and the low nibble contains the minor version. Currently the xlink server will respond with 0x10.

  • machine contains the remote machine type. Possible values are XLINK_MACHINE_C64 or XLINK_MACHINE_C128.

  • type contains the server type, either XLINK_SERVER_TYPE_RAM or XLINK_SERVER_TYPE_ROM

  • start contains the start address of the server code.

  • end contains the end address of the server code.

  • length contains the length of the server code.

  • memtop contains the top of the lower memory area.

bool xlink_reset(void)

Resets the C64 by pulling its RESET line low for about 10ms. This works whether or not a server is running on the C64.

If a parallel port cable is used, additional circuity is required for this to work.

bool xlink_ready(void)

Try to make sure that the server is ready. First, the server is pinged. If it doesn’t respond, the C64 is reset. If the server responds to another ping within the next 3 seconds this function returns true.

If the initial ping succeeds and a basic program is found running on the C64, a basic warmstart (equivalent to pressing runstop-restore) is performed prior to returning successfully.

bool xlink_relocate(ushort address)

Loads a ram-based server to the given address and passes control to it, effectively disabling the currently running server. Note that no additional checks are performed. You have to make sure that the server doesn’t end up below ROM or IO areas.

You can use xlink_identify() beforehand to determine whether relocation is required and what the size of the server code will be.

Memory transfers

Common arguments

Address

The address argument specifies the source or destination address or address range. For transfers involving more than one byte of data, wrapping may occur if the size of the transfer exceeds the machines address space. This means that if the last memory address $FFFF has been read or written to and there is still more data to transfer, the transfer continues from address $0000 onwards.

Memory and Bank

The memory and bank arguments allow adjusting the target machines memory configuration before reading/writing values to/from memory. These arguments are equivalent to the memory configuration options described above.

In addition, the most significant bit of the memory value controls whether screen blanking should occur during transfers. Set this bit to prevent screen blanking for the load, save and fill operations (i.e. binary OR the value with 0x80).

bool xlink_load(uchar memory, uchar bank, ushort address, uchar* data, uint size)

Load size bytes of data obtained from the memory area pointed to by data to address in the target machine memory.

bool xlink_save(uchar memory, uchar bank, ushort address, uchar* data, uint size)

Read size bytes of data beginning from address in the target machine memory and store the result in the memory area pointed to by data. The caller has to make sure that enough memory is allocated for data beforehand.

bool xlink_peek(uchar memory, uchar bank, ushort address, uchar* value)

Read the byte at address from the target machine memory and store it in the memory location pointed to by value.

bool xlink_poke(uchar memory, uchar bank, ushort address, uchar value)

Write value to address in the target machine memory.

bool xlink_fill(uchar memory, uchar bank, ushort address, uchar value, uint size)

Fill the target machine memory with size bytes of the constant value value beginning at address.

Program execution

bool xlink_jump(uchar memory, uchar bank, ushort address)

Make the target machine jump to the specified address. Prior to jumping, the stack pointer is reset and the address of the basic REPL (Read-Eval-Print-Loop) is pushed on the stack, followed by the actual jump address. Then the supplied memory config is applied, clean processor flags and registers are pushed onto the stack and the jump is performed via RTI, leaving the current invocation of the IRQ routine servicing the request.

When the code performs a final RTS, the target machine (should) return to the basic prompt.

See xlink_inject() for an alternative way of running code on the target machine.

bool xlink_run(void);

Runs the currently loaded basic program. The currently running server is uninstalled beforehand.

This is equivalent to issuing RUN on the target machine.

Low level API

Overview

The low level API provides a way to implement custom commands or communication schemes in your own programs without requiring changes to the server code running on the users remote machine.

Instead, the code required to serve a custom command on the C64 can be injected into the context of the currently running server using xlink_inject(). This code is then responsible for performing the subsequent low level communication with the client. The client in turn can use the functions of the low level API to communicate with the server.

To begin a communication session, the client calls xlink_begin(). It then uses the communication primitives xlink_send() and xlink_receive() (and their variants) to transfer data back and forth between the two machines. It finally calls xlink_end() to signal the end of the communication session. This will be explained in greater detail later in this document.

Technical background

In order to implement the server side code serving custom commands it is necessary to understand the underlying technical implementation. In this section I will describe the implementation and its implications while offering a simple set of macros that can be used to implement a custom server on the C64. The section is concluded with a simple example for a custom server and a corresponding client.

Handshaking

Two distinct lines are used to implement the necessary handshaking between the two machines:

The first line is used to send a handshake signal from the PC to the C64. On the C64, this line is connected to the userport FLAG line. The occurrence of a falling edge on the FLAG line is reflected by bit 4 of the interrupt control and status register of CIA2 at $dd0d. This bit is set when a falling edge occurs, and is cleared when the register is read. Thus to send a handshake signal to the C64, the PC generates a falling edge on the flag line. To detect a handshake from the PC, the C64 reads $dd0d and tests whether bit 4 is set.

On the C64, we can wait for the occurrence of a handshake signal using the following code:

wait:	lda $dd0d
	and #$10
	beq wait

We define a macro called wait containing this code.

The second line is used to send a handshake signal from the C64 to the PC. At the C64, this line is connected to bit 2 of Port A of CIA2 at $dd00. To send a signal to the PC, the C64 simply flips the value of this bit. The PC can thus detect an incoming signal from the C64 by listening for changes on this line.

On the C64, we can send a handshake signal to the PC using the following code:

signal:
	lda $dd00
	eor #$04
	sta $dd00

We define two macros called strobe and ack containing this code.

This way our code will document our intent when sending a signal to the PC: we’ll use the strobe macro when we initially tell the PC that some condition has occurred, and we’ll use the ack macro if we merely acknowledge a previous signal we received from the PC.

Data port and direction

xlink uses Port B of CIA2 at $dd01 for 8-bit wide bidirectional data transfers.

On the C64, we can set the port direction of the data port to input using:

set_input:
	lda #$00 
	sta $dd03

We can set the port direction to output using:

set_output:
	lda #$ff
	sta $dd03

We define to macros for this, called set_input and set_output respectively.

Note that you should never use set_output blindly without having negotiated a change of transfer direction with the PC first. See negotiating direction for details.

On the client side the port is always kept in input mode unless the port is actually used to send data to the server. On the server side, we should always make sure that the port remains in input mode unless output mode is actually required. Thus, the code we inject() should always initialize the port and our handshake lines correctly first:

init:
        :set_input()
  
	lda $dd02 // set CIA2 Port A bit 2 to output 
        ora #$04  // this is our handshaking line to the PC
	sta $dd02
	

We define a macro called init for this.

Negotiating direction

The transfer direction of the data port must be negotiated with care between the two machines. If both ports were set to output, either port may be damaged by short circuits, depending on the actual levels the individual port lines are driven to. The logic needed for negotiation of transfer direction is abstracted away by the low level functions on the client side. On the server side, code must follow the assumptions made by the low level functions on the client side.

In order to negotiate the transfer direction and to switch the direction during a communication session the handshake lines are used for synchronization. When the direction should change, the currently receiving side may switch its port to output (and thus become the sender) only after having received a signal from the current sender acknowledging that is has already set its port to input (and thus has become the receiver).

So on the C64 we’ll have to wait for a signal from the PC before switching to output:

output:
   :wait()
   :set_output()	

We define a macro called output for this.

Likewise, if the C64 is currently sending, it must set its port to input first and then acknowledge the switch to the PC:

input:
   :set_input()
   :strobe()

We define a macro called input for this.

Transferring data

In order to transfer data between the two machines the following communication scheme is used:

  • The sender writes a byte of data on the port
  • The sender sends a signal to the receiver (it “strobes” the receiver)
  • The sender waits until the receiver acknowledges the receipt of the data

  • The receiver waits for a signal from the sender (it is “strobed” by the sender)
  • The receiver reads the data from the port
  • The receiver sends a signal to acknowledge receipt (it “acks” to the sender)

If the C64 takes the role of the sender, we can use the following code to send one byte of data from the accumulator:

send:
	sta $dd01
	:strobe()
	:wait()

If the C64 takes the role of the receiver, we can use the following code to receive one byte of data and store it into the X register:

receive:
	:wait()
	ldx $dd01
	:ack()

We define macros called send and receive for this.

Implementing a custom server and client

We now have the necessary macros to implement a custom server on the C64: init, input, output, send and receive.

For the sake of the example we’ll implement a simple injectable server that understands only two commands:

  • identify will send the short identification string “CUSTOM”
  • quit will quit the server and return control to the xlink server

The code uses Kickassembler 3 syntax.

.pc = $c000
	
.macro wait()       { loop: lda $dd0d and #$10 beq loop }
.macro ack()        { lda $dd00 eor #$04 sta $dd00 }
.macro strobe()     { :ack() }	
.macro send()       { sta $dd01 :strobe() :wait() }	
.macro receive()    { :wait() ldx $dd01 :ack() }
.macro set_input()  { lda #$00 sta $dd03 }
.macro set_output() { lda #$ff sta $dd03 }	
.macro input()      { :set_input() :strobe() }	
.macro output()     { :wait() :set_output() }
.macro init()       { :set_input() lda $dd02 ora #$04 sta $dd02 }

.var cmd_id   = $01
.var cmd_quit = $02

listen: {
	:init()         // always set the port direction to input
                        // and configure our handshake line for output

        :receive()      // receive a command byte and store into X

check_identify:	     
	cpx #cmd_id     // is it the identify command?
	bne check_quit  // if not, check if it is the quit command...
	jmp identify    // else serve identify command

check_quit:	        
	cpx #cmd_quit   // is it the quit command?
	bne listen      // if not, unknown command, back to listen
	rts             // else exit injection, return to previous server
}
	
identify: {
	:output()       // negotiate output direction

	lda len         // tell the client how many bytes to receive
	:send() 

	ldx #00         // send len bytes...
loop:	lda id,x      
	:send()
	inx
	cpx len
	bne loop

	jmp listen      // done, back to listen (switching back to input)

id:  .byte 'C', 'U', 'S', 'T', 'O', 'M' 
len: .byte $06
}

The corresponding client will first inject the above code using xlink_inject(). It will send an identify command and output the results, followed by a quit command to quit the injected server again. It then uses xlink_identify to verify that the custom server has actually quit by checking that the xlink server is active again.

#include <stdlib.h>
#include <stdio.h>
#include <xlink.h>

void identify(void);
void quit(void);

int main(int argc, char** argv) {

  uchar code[115] = {169,0,141,3,221,173,2,221,9,4,141,
		     2,221,173,13,221,41,16,240,249,174,
		     1,221,173,0,221,73,4,141,0,221,224,1,
		     208,3,76,43,192,224,2,208,214,96,173,
		     13,221,41,16,240,249,169,255,141,3,
		     221,173,114,192,141,1,221,173,0,221,
		     73,4,141,0,221,173,13,221,41,16,240,
		     249,162,0,189,108,192,141,1,221,173,0,
		     221,73,4,141,0,221,173,13,221,41,16,
		     240,249,232,236,114,192,208,229,76,0,
		     192,67,85,83,84,79,77,6,};
    
  printf("Injecting custom server code...\n");
  
  if(xlink_inject(0xc000, code, sizeof(code))) {
    printf("Custom server code running...\n");

    identify();
    quit();

    printf("Checking if custom server has quit...\n");

    xlink_server_info_t info;
    if(xlink_identify(&info)) {      
      if(strncmp(info.id, "XLINK", 5) == 0) {
        printf("Custom server has quit, xlink server is active again\n");
      }
    }
  }
  
  if(xlink_error->code != XLINK_SUCCESS) {
    fprintf(stderr, "xlink error: %s\n", xlink_error->message);
    return 1;
  }
  return 0;
}

void identify() {
  uchar cmd_id = 0x01;
  uchar len = 0;
  char *data;
  
  xlink_begin();

  printf("Sending identify command 0x%02X...\n", cmd_id);
  xlink_send(&cmd_id, 1);

  printf("Receiving length of identification string...\n");
  xlink_receive(&len, 1);
  
  printf("Received length: %d\n", len);

  printf("Receiving %d bytes...\n", len);
  data = (char*) calloc(len+1, sizeof(char));
  
  xlink_receive((uchar*) data, len);

  xlink_end();

  printf("Received \"%s\"\n", data);
  free(data);
}

void quit() {
  uchar cmd_quit = 0x02;

  xlink_begin();
  
  printf("Sending quit command 0x%02X...\n", cmd_quit);
  xlink_send(&cmd_quit, 1);

  xlink_end();  
}

Low level functions

Overview and rationale

The low level functions are based on a concept of communication sessions. The functions xlink_begin() and xlink_end() signal the begin and end of a single session to the library. This is necessary because at the begin of a session, the roles of the PC and the C64 are not defined yet: while xlink is based on the assumption that usually the PC is a client that sends commands that are served by the C64, a reversal of these roles is possible just as well: in this case the PC could take the role of the server, listening for and servicing commands send by the C64.

To make this possible, the initial setup of transfer direction does not require handshaking. Instead, it is assumed that both sides are in input mode at the beginning of a session, and that either side may initiate communication by simply setting its port to output and sending a command to the other side. After this initial transfer has been send though, any subsequent change of transfer direction requires proper negotiation through the exchange of handshake signals as described above. The low level send and receive functions automatically detect whether the transfer direction changes and perform the necessary negotiation.

Note that in order for the PC to become the server, it will need to keep listening for a command sent by the remote machine for a possibly indefinite amount of time. Although you could use xlink_receive_with_timeout() with a timeout value of zero for this, it is not recommended to do so. If a parallel port cable was used, this would be no problem, since the user could simply abort the server program on the PC. If the usb adapter is used though, a timeout value of zero will make the microprocessor on the adapter enter a possibly endless loop while waiting for a signal from the remote machine. If no signal ever occurs, the adapter will need a power-cycle to break the endless loop.

Thus it is recommended implement a listening loop on the PC like this:

do { xlink_begin() } while(!xlink_receive(&cmd, 1));

// command received, process...

xlink_end();
bool xlink_inject(ushort address, uchar* code, uint size)

Loads size bytes of code to address and executes the code in the context of the current invocation of the IRQ routine servicing the request. An RTS from the code will return control back to the xlink server. Since the code runs within the IRQ service routine it should not preform a CLI instruction.

void xlink_begin(void);

Signals the begin of a communication session with the remote machine and assures that the next call to either xlink_send() or xlink_receive() (or their variants) will not perform an additional handshake required to negotiate transfer direction. This assumes that both communication ports are set to input when the first call to these functions occurs. On the client side, this is assured by the implementation of these functions. On the server side, the servicing code is responsible for keeping the port in input mode unless output mode is strictly required.

bool xlink_send(uchar* data, uint size)

Sends size bytes of data to the server.

If this call implies a change of transfer direction within the current communication session (i.e. if the previous call was to either xlink_receive() or xlink_receive_with_timeout()), then this function first waits for a signal acknowledging that the other side has switched its port to input and then switches the port to output.

If no change of transfer direction is implied by this call (i.e. the previous call was to either xlink_begin(), xlink_send() or xlink_send_with_timeout()), then no additional handshake is expected before switching the port to output.

If the server fails to acknowledge a previous strobe performed during the execution of this function within one second, this function will return false and xlink_error will be set appropriately. Use xlink_send_with_timeout() to specify a larger timeout.

bool xlink_send_with_timeout(uchar* data, uint size, uint timeout)

This function is equivalent to xlink_send() except that the timeout for the transfer may be set explicitly. The timeout is given in seconds. A zero timeout value means no timeout at all.

bool xlink_receive(uchar *data, uint size)

Receives size bytes of data from the server. The caller has to make sure that sufficient memory is allocated for data beforehand.

If this call implies a change of transfer direction within the current communication session (i.e. if the previous call was to either xlink_send() or xlink_send_with_timeout()), then this function will first switch the port to input and then acknowledge this to the other side.

If no change of transfer direction is implied by this call (i.e. the previous call was to either xlink_begin(), xlink_receive() or xlink_receive_with_timeout()), then no additional handshake is expected before switching the port to input.

If the server fails to acknowledge a previous strobe performed during the execution of this function within one second, this function will return false and xlink_error will be set appropriately. Use xlink_receive_with_timeout() to specify a larger timeout.

bool xlink_receive_with_timeout(uchar* data, uint size, uint timeout)

This function is equivalent to xlink_receive() except that the timeout for the transfer may be set explicitly. The timeout is given in seconds. A zero timeout value means no timeout at all.

void xlink_end(void)

Signals the end of a communication session with the remote machine. Although this call is functionally equivalent to xlink_begin() it is provided as a means to clarify the code.

Feedback

Please report bugs, issues, feature request etc. on the github issue tracker.

If you have problems building, installing or using this software, feel free to drop me a line at h.bekel@googlemail.com.

License

	XLINK Hardware, Firmware, Client, Server and Library
      Copyright (c) 2015, Henning Bekel <h.bekel@googlemail.com>
	   
		      All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the
   distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
                           LUFA Library
                 Copyright (C) Dean Camera, 2013.

              dean [at] fourwalledcubicle [dot] com
                         www.lufa-lib.org

Permission to use, copy, modify, and distribute this software and its
documentation for any purpose is hereby granted without fee, provided
that the above copyright notice appear in all copies and that both
that the copyright notice and this permission notice and warranty
disclaimer appear in supporting documentation, and that the name of
the author not be used in advertising or publicity pertaining to
distribution of the software without specific, written prior
permission.

The author disclaims all warranties with regard to this software,
including all implied warranties of merchantability and fitness.  In
no event shall the author be liable for any special, indirect or
consequential damages or any damages whatsoever resulting from loss of
use, data or profits, whether in an action of contract, negligence or
other tortious action, arising out of or in connection with the use or
performance of this software.
                   InpOut32 Library and Drivers

                       Copyright Logix4U &
   Phillip Gibbons [Highresolution Enterprises] (for the x64 port)

             http://www.highrez.co.uk/Downloads/InpOut32/
	     
The Author makes no guarantee that this software is free from bugs and
will not harm your system.  However, the author actively runs this
software and all downloads have been checked for known viruses.  This
product is released as open source (Freeware).