Skip to content
Open
Show file tree
Hide file tree
Changes from all 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|dylib}>` connects an emulated user peripheral built as a dynamically linked library. implies `-via2`
* `-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
17 changes: 16 additions & 1 deletion src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ bool testbench = false;
bool enable_midline = false;
bool ym2151_irq_support = false;
char *cartridge_path = NULL;
char *user_peripheral_plugin_path = NULL;

bool has_midi_card = false;
uint16_t midi_card_addr;
Expand Down Expand Up @@ -334,7 +335,7 @@ machine_reset()
vera_spi_init();
via1_init();
if (has_via2) {
via2_init();
via2_init(user_peripheral_plugin_path);
}
video_reset();
mouse_state_init();
Expand Down Expand Up @@ -514,6 +515,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_plugin_path = argv[0];
argc--;
argv++;
} else if (!strcmp(argv[0], "-version")){
printf("%s", VER_INFO);
#ifdef GIT_REV
Expand Down Expand Up @@ -1309,6 +1323,7 @@ void main_shutdown() {
cartridge_save_nvram();
cartridge_unload();
}
via2_shutdown();
files_shutdown();

#ifdef PERFSTAT
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
121 changes: 121 additions & 0 deletions src/user_pins.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#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
// called at runtime by the emulator during initialization of via2. [x16_user_port_init]
// should return 0 on success, and -1 on error.

#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
// [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 user_port_t {
// 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;

// Arbitrary user data which is passed to the various callbacks.
void *userdata;

// 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)(void *userdata);

// New pin values pushed from the via to the peripheral. Pins not in the
// [connected_pins] mask will be zeroes, but that does not imply those pins are low
void (*write)(user_pin_t pins, void *userdata);

// 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, void *userdata);

// Called on reset and shutdown. is_poweroff is true for final shutdown, to
// differentiate from a reset
void (*cleanup)(bool is_poweroff, void *userdata);
} user_port_t;

// Extensible init args struct. Includes api_version so perhipheral libraries can abort or
// downgrade, if necessary.
typedef struct __attribute__((aligned(sizeof(void *)))) user_port_init_args_t {
int api_version;

// Callback for setting an error message from the plugin
// Errors longer than 256 chars (including null) will be truncated
void (*set_error)(char const *);
} user_port_init_args_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 -1 on error.
typedef int (*user_port_init_t)(user_port_init_args_t *, user_port_t *);
Loading
Loading