Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7e746fb
Add basic support for providing user perhipherals via shared library
catharinejm May 5, 2025
de038af
ensure all output pins are sent to user_port on via2_write
catharinejm May 5, 2025
e88430a
clear any flag when writing to via2's IFR
catharinejm May 5, 2025
0f5a281
pass X16_USER_PORT_API_VERSION to user_port_init
catharinejm May 7, 2025
a2a07ff
minor readability tweaks
catharinejm May 16, 2025
306fbb6
Handle CA1 & CB1 interrupts from user pins
catharinejm Jun 22, 2025
5a5bd77
comment out via2 ifr state logging
catharinejm Jun 23, 2025
1a53e35
remove ifr debugging statements
catharinejm Jun 30, 2025
cd6b1a9
Merge branch 'master' into userport
catharinejm Aug 2, 2025
d0c6a02
via2: remove special case for setting IFR, it's handled elsewhere
catharinejm Aug 12, 2025
81134a6
via2: clean up CA1/CB1 interrupt handling
catharinejm Aug 12, 2025
93e5de7
via2 userport: handle api version mismatches in emulator
catharinejm Aug 12, 2025
050b3e6
userport: extract dylib macros from midi.c for userport handler
catharinejm Aug 12, 2025
7f5efe3
userport: log errors to stderr
catharinejm Aug 12, 2025
2eb9604
userport: rename expected init function to x16_user_port_init
catharinejm Aug 12, 2025
a073871
userport: comment user_pins.h
catharinejm Aug 12, 2025
6056aac
userport: add [-user-perhipheral] flag to README
catharinejm Aug 12, 2025
f015c22
userport: spell 'peripheral' correctly...
catharinejm Aug 12, 2025
e6f857d
userport: Add 'dylib' extension to README
catharinejm Aug 16, 2025
41e42cd
userport: rename user_peripheral_path -> user_peripheral_plugin_path
catharinejm Aug 16, 2025
efe08b5
userport: express tighter value guarantees in user_port.h comments
catharinejm Aug 16, 2025
f93da6b
userport: add extensible user port init args
catharinejm Aug 16, 2025
7e8be97
userport: explicit empty arglist in user_port.read()
catharinejm Aug 16, 2025
f492c4a
userport: one more user_perhipheral_plugin_path
catharinejm Aug 16, 2025
ebbabaa
userport: one more return -1 comment
catharinejm Aug 16, 2025
f5ffd64
userport: allow plugins to set an error message on init
catharinejm Aug 16, 2025
78c2a92
userport: thread userdata through callbacks
catharinejm Aug 16, 2025
0e0b09c
userport: add cleanup function to user peripheral
catharinejm Aug 17, 2025
7bb9768
Merge branch 'master' into userport
catharinejm Dec 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ When starting `x16emu` without arguments, it will pick up the system ROM (`rom.b
* `-sound <device>` can be used to specify the output sound device. If 'none', no audio is generated.
* `-abufs` can be used to specify the number of audio buffers (defaults to 8 when using the SD card, 32 when using HostFS). If you're experiencing stuttering in the audio, try increasing this number. This will result in additional audio latency though.
* `-via2` installs the second VIA chip expansion at $9F10.
* `-user-peripheral <my_peripheral.{so|dll}>` connects an emulated user peripheral built as a dynamically linked library. implies `-via2`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

macOS uses .dylib. The extension also isn't included in main.c (and I'm not sure whether it should be).

* `-midline-effects` enables mid-scanline raster effects at the cost of vastly increased host CPU usage.
* `-mhz <integer>` sets the emulated CPU's speed. Range is from 1-40. This option is mainly for testing and benchmarking.
* `-enable-ym2151-irq` connects the YM2151's IRQ pin to the system's IRQ line with a modest increase in host CPU usage.
Expand Down
17 changes: 17 additions & 0 deletions src/dylib.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once

#ifdef _WIN32
#include <windows.h>
#define LIBRARY_TYPE HMODULE
#define LOAD_LIBRARY(name) LoadLibrary(name)
#define GET_FUNCTION(lib, name) (void *)GetProcAddress(lib, name)
#define CLOSE_LIBRARY(lib) FreeLibrary(lib)
#define LIBRARY_ERROR() "[ unknown error ]"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetLastError + FormatMessage would be the way to get the error message; if you don't want to do the FormatMessage wrapping in this PR, I'd go with just printing GetLastError() and switching the format specifier between %u and %s depending on the platform.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add that but I can't test it. I just pulled these macros out of midi.c, there just wasn't one for errors.

#else
#include <dlfcn.h>
#define LIBRARY_TYPE void*
#define LOAD_LIBRARY(name) dlopen(name, RTLD_LAZY)
#define GET_FUNCTION(lib, name) dlsym(lib, name)
#define CLOSE_LIBRARY(lib) dlclose(lib)
#define LIBRARY_ERROR() dlerror()
#endif
16 changes: 15 additions & 1 deletion src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ bool testbench = false;
bool enable_midline = false;
bool ym2151_irq_support = false;
char *cartridge_path = NULL;
char *user_peripheral_path = NULL;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
char *user_peripheral_path = NULL;
char *user_peripheral_plugin_path = NULL;


bool has_midi_card = false;
uint16_t midi_card_addr;
Expand Down Expand Up @@ -333,7 +334,7 @@ machine_reset()
vera_spi_init();
via1_init();
if (has_via2) {
via2_init();
via2_init(user_peripheral_path);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
via2_init(user_peripheral_path);
via2_init(user_peripheral_plugin_path);

}
video_reset();
mouse_state_init();
Expand Down Expand Up @@ -513,6 +514,9 @@ usage()
printf("\tSet the real-time-clock to the current system time and date.\n");
printf("-via2\n");
printf("\tInstall the second VIA chip expansion at $9F10\n");
printf("-user-peripheral <peripheral lib>\n");
printf("\tUse the provided shared library to drive user peripherals.\n");
printf("\tImplies -via2\n");
printf("-testbench\n");
printf("\tHeadless mode for unit testing with an external test runner\n");
printf("-mhz <integer>\n");
Expand Down Expand Up @@ -1088,6 +1092,16 @@ main(int argc, char **argv)
argc--;
argv++;
has_via2 = true;
} else if (!strcmp(argv[0], "-user-peripheral")) {
argc--;
argv++;
if (!argc || argv[0][0] == '-') {
usage();
}
has_via2 = true;
user_peripheral_path = argv[0];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
user_peripheral_path = argv[0];
user_peripheral_plugin_path = argv[0];

argc--;
argv++;
} else if (!strcmp(argv[0], "-version")){
printf("%s", VER_INFO);
#ifdef GIT_REV
Expand Down
15 changes: 1 addition & 14 deletions src/midi.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,7 @@
#include "midi.h"
#include "audio.h"
#include "endian.h"

#ifdef _WIN32
#include <windows.h>
#define LIBRARY_TYPE HMODULE
#define LOAD_LIBRARY(name) LoadLibrary(name)
#define GET_FUNCTION(lib, name) (void *)GetProcAddress(lib, name)
#define CLOSE_LIBRARY(lib) FreeLibrary(lib)
#else
#include <dlfcn.h>
#define LIBRARY_TYPE void*
#define LOAD_LIBRARY(name) dlopen(name, RTLD_LAZY)
#define GET_FUNCTION(lib, name) dlsym(lib, name)
#define CLOSE_LIBRARY(lib) dlclose(lib)
#endif
#include "dylib.h"

#define ASSIGN_FUNCTION(lib, var, name) {\
var = GET_FUNCTION(lib, name);\
Expand Down
104 changes: 104 additions & 0 deletions src/user_pins.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#pragma once

// Include this header in a user peripheral library.
//
// A user peripheral library must export a [user_port_init_t x16_user_port_init] to be
// consumed at runtime by the emulator during initialization of via2. [x16_user_port_init]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// consumed at runtime by the emulator during initialization of via2. [x16_user_port_init]
// called at runtime by the emulator during initialization of via2. [x16_user_port_init]

// should return 0 on success, and <0 on error.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true / false or 0 / -1 would be better than allowing any negative number.


#include <stdint.h>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed in here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is? user_pins_t is uint32_t.


// This must be assigned to the [api_version] field of [user_port_t] by the peripheral
// library so that version mismatches can be detected.
#define X16_USER_PORT_API_VERSION 1

// Bit assignments for each 65c22 pin exposed to the user port, for use with
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Bit assignments for each 65c22 pin exposed to the user port, for use with
// Bit assignments for each 65C22 pin exposed to the user port, for use with

// [user_pin_t]. For convenience, all Port A pins are in the low byte, in bit order, and
// Port B pins are likewise in the second byte.

#define PA0_PIN (1 << 0)
#define PA1_PIN (1 << 1)
#define PA2_PIN (1 << 2)
#define PA3_PIN (1 << 3)
#define PA4_PIN (1 << 4)
#define PA5_PIN (1 << 5)
#define PA6_PIN (1 << 6)
#define PA7_PIN (1 << 7)
#define PB0_PIN (1 << 8)
#define PB1_PIN (1 << 9)
#define PB2_PIN (1 << 10)
#define PB3_PIN (1 << 11)
#define PB4_PIN (1 << 12)
#define PB5_PIN (1 << 13)
#define PB6_PIN (1 << 14)
#define PB7_PIN (1 << 15)
#define CA1_PIN (1 << 16)
#define CA2_PIN PB3_PIN
#define CB1_PIN PB6_PIN
#define CB2_PIN PB7_PIN

// USER_PINn macros map the 65C22 pins (above) to the exposed pins on the X16's user port.

// Left column
#define USER_PIN1 PB0_PIN
#define USER_PIN3 PA0_PIN
#define USER_PIN5 PA1_PIN
#define USER_PIN7 PA2_PIN
#define USER_PIN9 PA3_PIN
#define USER_PIN11 PA4_PIN
#define USER_PIN13 PA5_PIN
#define USER_PIN15 PA6_PIN
#define USER_PIN17 PA7_PIN
#define USER_PIN19 CA1_PIN
#define USER_PIN21 PB1_PIN
#define USER_PIN23 PB2_PIN
#define USER_PIN25 PB3_PIN

// Right Column
#define USER_PIN2 PB4_PIN
#define USER_PIN4 PB5_PIN
#define USER_PIN6 PB6_PIN
#define USER_PIN8 PB7_PIN
// 10-24 (even) are GND, 26 is VCC

// [user_pin_t] is a bitmask of pin values. Port A is the first byte, Port B is the second
// byte, and CA1 is bit 16. Bits above 16 must always be 0. The above *_PIN macros define
// the appropriate bits to use with [user_pin_t].
//
// Note that in this implementation, all pins are either high (1) or low (0). If your
// device uses pull-up/down, you'll need to factor that into [read] or [step] pin values
// manually. However, unless the data direction of the pins on the via stays constant it
// will be quite difficult to keep the via's and peripheral's states in sync, since there
// is no signal that a via pin has switched from being actively driven to hi-Z, or vice
// versa.
Comment on lines +70 to +73
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this relevant to implementers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's easy enough to overlook that I think it's worth calling out, especially since the emulator does actually do I2C, but it's a special case. That said, I think I could add pull-up/pull-down masks to the user_port initialization, and treat output->input changes in the ddrs as a 'write' of the pulled value, and input->output as a write of the register value.

typedef uint32_t user_pin_t;

typedef struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a void *userdata - global state in plugins is a pain.

// Must be set to X16_USER_PORT_API_VERSION
int api_version;

// A mask of pins actually connected to the user peripheral.
user_pin_t connected_pins;

// Return the values of the connected pins based on the peripheral's internal
// state. Any pin values not in [connected_pins] will be ignored.
user_pin_t (*read)();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
user_pin_t (*read)();
user_pin_t (*read)(void);

I forgot which C standard dropped () declarations meaning "variable number of arguments", but I don't want to find out the hard way...


// New pin values pushed from the via to the peripheral. Pins not in the
// [connected_pins] mask do not contain meaningful values (but should be zeroes).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should just guarantee that they're zero here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As implemented that is guaranteed, I could make the comment stronger. Mostly I meant to imply that a zero in a bit not connected does not mean the pin is low, it doesn't mean anything.

void (*write)(user_pin_t pins);

// Step the state machine of the connected peripheral. [nanos] is the number of
// nanoseconds which has passed since the last step. Returns the pin state so that any
// interrupts based on CA1 and CB1 can be triggered. CA2 and CB2 are not presently
// implemented in the via code.
user_pin_t (*step)(double nanos);
} user_port_t;

// Populates the provided [user_port_t *]. If any of [read], [write] or [step] is NULL, it
// will be ignored.
Comment on lines +115 to +116
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check isn't implemented.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is actually, via2_read, via2_write and via2_step check that user_port.whatever != NULL before invoking it. I figure it could be valid for a device not to need some subset of the functions.

//
// A peripheral library's exposed [user_port_init_t] function MUST be named "x16_user_port_init".
//
// Returns 0 on success and <0 on error.
typedef int (*user_port_init_t)(user_port_t *);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You made the struct API extensible, but not the init function - and the plugin has no idea what version you want from it. I'd recommend adding another struct:

typedef struct __attribute__((aligned(void *))) user_port_init_args {
    int api_version;
} user_port_init_args;

and modifying user_port_init to take a pointer to that struct. That way the plugin can determine whether it actually supports the given API version, and we can pass additional arguments in the future by extending the struct. (Force pointer alignment so that adding a pointer later doesn't cause headaches.)

121 changes: 118 additions & 3 deletions src/via.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
//XXX
#include "glue.h"
#include "joystick.h"
#include "dylib.h"
#include "user_pins.h"

#define CA1 (1 << 0)
#define CA2 (1 << 1)
#define CB1 (1 << 2)
#define CB2 (1 << 3)

typedef struct {
unsigned timer_count[2];
Expand All @@ -21,6 +28,7 @@ typedef struct {
bool timer1_m1;
bool timer_running[2];
bool pb7_output;
uint8_t cacb;
} via_t;

static via_t via[2];
Expand All @@ -38,6 +46,7 @@ via_init(via_t *via)
via->timer_running[1] = false;
via->timer1_m1 = false;
via->pb7_output = true;
via->cacb = 0;
}

static void
Expand Down Expand Up @@ -331,28 +340,134 @@ via1_irq()
// for now, just assume that all user ports are not connected
// and reads return output register (open bus behavior)


static bool attempt_peripheral_load = true;
static LIBRARY_TYPE user_peripheral_dl = NULL;
static user_port_init_t user_port_init = NULL;

static user_port_t user_port;

void
via2_init()
via2_init(char const *user_peripheral_path)
{
via_init(&via[1]);
if (attempt_peripheral_load && user_peripheral_path) {
attempt_peripheral_load = false;
user_peripheral_dl = LOAD_LIBRARY(user_peripheral_path);
if (user_peripheral_dl) {
user_port_init = GET_FUNCTION(user_peripheral_dl, "x16_user_port_init");
}
if (user_peripheral_dl == NULL || user_port_init == NULL) {
fprintf(stderr, "failed to load user peripheral %s:\n\t%s\n",
user_peripheral_path, LIBRARY_ERROR());
fprintf(stderr, "continuing with empty user port.\n");
if (user_peripheral_dl) CLOSE_LIBRARY(user_peripheral_dl);
}
}
// TODO Do we really want to reset the user peripherals every time?
if (user_port_init) {
bool error = false;
if (user_port_init(&user_port) < 0) {
fprintf(stderr, "error initializing user peripheral\n");
error = true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should return a proper error message. e.g. via const char *user_port_error() that is valid until the next call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean that the peripheral should retain its error, or the via code should retain userport errors, or both?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a set_error callback to user_port_init_args_t which a peripheral can call to pass an error string back to the emulator, which retains a copy.

}
else if (user_port.api_version != X16_USER_PORT_API_VERSION) {
fprintf(stderr, "user peripheral version mismatch: expected: %d, got: %d\n",
X16_USER_PORT_API_VERSION, user_port.api_version);
error = true;
}
if (error) {
user_port_init = NULL;
memset(&user_port, 0, sizeof(user_port));
}
}
}

uint8_t
via2_read(uint8_t reg, bool debug)
{
return via_read(&via[1], reg, debug);
uint8_t regval = via_read(&via[1], reg, debug);
if (user_port.read) {
switch (reg) {
case 0: {
uint8_t mask = user_port.connected_pins >> 8; // PB is 2nd byte
if (mask) {
uint8_t user_pins = user_port.read() >> 8;
// PB only returns pin values on inputs
mask &= ~via[1].registers[2];
return (regval & ~mask) | (user_pins & mask);
}
break;
}
case 1:
case 15: {
uint8_t mask = user_port.connected_pins; // PA is 1st byte
if (mask) {
uint8_t user_pins = user_port.read();
// Port A always returns pin values on a read, even on output pins
return (regval & ~mask) | (user_pins & mask);
}
break;
}
}
}
return regval;
}

void
via2_write(uint8_t reg, uint8_t value)
{
via_write(&via[1], reg, value);
switch (reg) {
case 0:
case 1:
case 2:
case 3:
case 15: {
if (user_port.write) {
user_pin_t pa = via[1].registers[1] & via[1].registers[3];
user_pin_t pb = via[1].registers[0] & via[1].registers[2];
user_pin_t pins = (pa | (pb << 8)) & user_port.connected_pins;
user_port.write(pins);
}
break;
}
}
}

void
via2_step(unsigned clocks)
{
via_step(&via[1], clocks);
via_step(&via[1], clocks);
if (user_port.step) {
user_pin_t pins = user_port.step((double)clocks * 1000.0 / MHZ);
// TODO CA2/CB2
if (user_port.connected_pins & CA1_PIN) {
uint8_t new_ca1 = pins & CA1_PIN ? CA1 : 0;
if (new_ca1 != (via[1].cacb & CA1)) {
bool ca1_positive_active_edge = via[1].registers[12] & 0x01;
if ((ca1_positive_active_edge && new_ca1) || (!ca1_positive_active_edge && !new_ca1))
via[1].registers[13] |= 0x02;
}
via[1].cacb &= ~CA1;
via[1].cacb |= new_ca1;
}
// CB1 shares a user pin with PB6, so only check if it's an input
if ((user_port.connected_pins & CB1_PIN) && !(via[1].registers[2] & 0x40)) {
uint8_t new_cb1 = pins & CB1_PIN ? CB1 : 0;
if (new_cb1 != (via[1].cacb & CB1)) {
bool cb1_positive_active_edge = via[1].registers[12] & 0x10;
if ((cb1_positive_active_edge && new_cb1) || (!cb1_positive_active_edge && !new_cb1))
via[1].registers[13] |= 0x10;
}
via[1].cacb &= ~CB1;
via[1].cacb |= new_cb1;
}

// We could update input pin values here, but it doesn't really matter unless they
// fire interrupts. The state of the pins is invisible until the cpu invokes
// [via2_read].
}
}

bool
Expand Down
2 changes: 1 addition & 1 deletion src/via.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ void via1_write(uint8_t reg, uint8_t value);
void via1_step(unsigned clocks);
bool via1_irq();

void via2_init();
void via2_init(char const *user_peripheral_so);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void via2_init(char const *user_peripheral_so);
void via2_init(char const *user_peripheral_plugin);

uint8_t via2_read(uint8_t reg, bool debug);
void via2_write(uint8_t reg, uint8_t value);
void via2_step(unsigned clocks);
Expand Down