Skip to content

Conversation

@catharinejm
Copy link
Contributor

This adds support for connecting emulated peripherals to the user port via a dynamically linked library which exposes an x16_user_port_init function. read, write and step functions are also provided by the library, via a struct, and they are dispatched to by the via2_read, etc. functions. If the peripheral fails to load or initialize, an error is logged, and the X16 continues to boot without it.

Usage is mostly documented in user_pins.h, which must be included by a user peripheral lib. The shared lib is passed via a new -user-peripheral command-line flag.

I don't know if this kind of thing is wanted by the project, but there was a murmur of interest on discord, and I found it quite helpful for debugging a bitbang uart I'm running on my userport in hardware. The interface is not exactly user friendly, the implementation is by no means complete, and I've only really tested in my one case, and only on Linux... but it seems like a decent start.

This is a lot of commits for not a lot of code, it churned a lot. If you prefer I can come back with a single squashed commit. Or if you're not interested at all, I'll take no offense.

If you'd like an example of it being used, the emulated FTDI I wrote is here: https://git.sr.ht/~cjm/x16-ft232rl/tree/main/item/userport.c. It's not a very straightforward example perhaps, but it is the only one at the moment 😅

catharinejm added 18 commits May 6, 2025 10:17
sending only PORTA pins makes it look like PORTB output pins are
being pulled low, and vice versa. send all, regardless of which
register was written.

still no CA1/CA2/CB1/CB2 though
allowing the libraries to verify they were built against the same
version as the x16emu executable
in particular, if the user perhipheral connected pins don't include
CA1 or CB1, don't check them for level changes. Old logic would treat
unconnected as 'low' which could, in principle, override a value set
by other means. In practice, there is no code path for those values to
be set elsewhere, but no sense in leaving in a future bug.
Copy link
Collaborator

@Fulgen301 Fulgen301 left a comment

Choose a reason for hiding this comment

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

I'm a bit early with the review, so please wait for @mooinglemur 's opinion, but my two cents:

  • The API needs a cleanup function so that the shared object can clean itself up properly. This should be called before a reset and before the emulator exits.
  • Has this been tested with the WASM build? The JS wrapper has to specify which WASM plugin to load, if any.

And my main gripe:

  • This will result in people uploading precompiled binaries to somewhere and telling people to download it to try out new hardware in the emulator, binaries that can then do anything because they're native code like any other. (I'm not implying that hardware manufacturers are evil, just that the potential is there.) So at the minimum this needs to communicate to the user, in the README and the command line help, that only trusted plugins should be loaded.
    In fact, this plugin API would actually be a good candidate for WASM plugins. This would add a runtime as a dependency for all builds except the WASM one (and I'd volunteer for that part), but we could shield plugins from doing anything nefarious without the user's consent. I am aware that a plugin communicating with real hardware will need some sort of system access, but I'd rather see that opt-in than loading a shared object that may return an error and download a Bitcoin miner in another thread.

#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.

src/user_pins.h Outdated
user_pin_t (*read)();

// 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.

Comment on lines +70 to +73
// 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.
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.

src/via.c Outdated
Comment on lines 370 to 372
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.

src/user_pins.h Outdated
// 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.)

src/user_pins.h Outdated
// 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]

src/user_pins.h Outdated

// 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...

src/user_pins.h Outdated
// 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

Comment on lines +98 to +99
// Populates the provided [user_port_t *]. If any of [read], [write] or [step] is NULL, it
// will be ignored.
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.

README.md Outdated
* `-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).

easier peripheral state management
@catharinejm
Copy link
Contributor Author

I agree that it's a security hole, but then so is running any binary downloaded from the internet. I don't know much about WASM, I've played with it a little, but nothing too serious. I've only tested this as a regular linux shared library. A WASM plugin could be an interesting alternative, but I just don't know much about it.

called on reset and poweroff so peripherals can clean up state,
close files, etc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants