aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorspicyjpeg <88942473+spicyjpeg@users.noreply.github.com>2021-11-07 18:57:48 +0100
committerspicyjpeg <88942473+spicyjpeg@users.noreply.github.com>2021-11-07 18:57:48 +0100
commitb3ef4c5874f579c968f2d26843d0db420e9ed40e (patch)
tree85f60b460901bfe14b1cb6b30eeedadd6398eee3
parentc377ec612422ac82afe034d92eedb45fb8a17a39 (diff)
downloadpsn00bsdk-b3ef4c5874f579c968f2d26843d0db420e9ed40e.tar.gz
Added io/pads example, psxpad.h definitions, bugfixes
-rw-r--r--doc/dev notes.txt38
-rw-r--r--examples/demos/n00bdemo/main.c8
-rw-r--r--examples/io/pads/CMakeLists.txt22
-rw-r--r--examples/io/pads/main.c259
-rw-r--r--examples/io/pads/spi.c216
-rw-r--r--examples/io/pads/spi.h60
-rw-r--r--indev/README.md4
-rw-r--r--libpsn00b/include/psxpad.h203
-rw-r--r--libpsn00b/libc/start.c6
9 files changed, 759 insertions, 57 deletions
diff --git a/doc/dev notes.txt b/doc/dev notes.txt
index 1bc9ccd..d825ee7 100644
--- a/doc/dev notes.txt
+++ b/doc/dev notes.txt
@@ -53,6 +53,44 @@ IRQs are only triggered on transfer completion.
Additional notes by spicyjpeg:
+* The controller/memory card SPI interface is poorly implemented in most
+emulators, making custom controller polling code insanely hard to write and
+debug. The only emulator that comes close to real hardware is no$psx, which
+seems to correctly implement all features of the SPI port (even those not used
+by the BIOS pad driver, such as TX/RX interrupts). DuckStation only emulates
+the bare minimum required by the BIOS and Sony libraries, and pcsx-redux has
+major bugs that break most custom polling implementations. This pretty much
+means TX/RX IRQs and many flags in the JOY_* registers should not be used
+unless you are willing to break compatibility with emulators.
+
+* As if communicating with controllers wasn't difficult enough already,
+DualShock pads also have a built-in watchdog timer that gets enabled when first
+putting them in configuration mode (and is NOT disabled after exiting config
+mode). If no polling commands are sent to the controller for about 1 second,
+vibration motors are switched off and analog mode is disabled; the same happens
+if the analog button is pressed while in analog mode. In order to always keep
+the pad in analog mode you must:
+
+ * Poll both controller ports at least once per frame by sending command 42h.
+ Polling at a higher rate might be desirable in some cases (such as rhythm
+ games) to increase timing accuracy.
+ * If a digital pad response (type = 4) is received from a port that hasn't
+ previously been flagged as digital-only, attempt to put the pad into config
+ mode using command 43h.
+ * If the pad doesn't recognize the config command, flag it as digital-only
+ and treat all further digital pad responses from it as valid.
+ * If the pad recognizes the command, it will reply by identifying as an
+ analog pad in config mode (type = 15). Send command 44h immediately
+ (without sending 42h first, otherwise the pad will exit config mode) to
+ enable analog mode and turn on the LED.
+ * Pressing the analog button will result in the controller identifying as
+ digital even though it is not flagged as such, thus re-triggering the
+ configuration process and putting it back into analog mode.
+ * All analog pad responses (type = 7) can be treated as valid, as they will
+ come from controllers that have already been configured.
+ * If no valid response is received, assume no controller is connected and
+ reset the port's digital-only flag.
+
* The SDK provides no support yet for replacing the BIOS exception handler with
a custom one, however it can be done (if you are ok with losing all BIOS
controller, memory card and file functionality). In order not to break anything
diff --git a/examples/demos/n00bdemo/main.c b/examples/demos/n00bdemo/main.c
index ba21d88..e9dd336 100644
--- a/examples/demos/n00bdemo/main.c
+++ b/examples/demos/n00bdemo/main.c
@@ -617,7 +617,13 @@ void transition() {
if( comp >= 16 )
break;
-
+
+ // FIXME: for some reason this loop glitches out and hangs indefinitely
+ // in no$psx, *unless* there's a function somewhere that gets called
+ // with a pointer/string as first argument... wtf. It works fine in
+ // other emulators. If you are reading this, please help and enlighten
+ // me. -- spicyjpeg
+ puts(".");
}
DrawSync(0);
diff --git a/examples/io/pads/CMakeLists.txt b/examples/io/pads/CMakeLists.txt
new file mode 100644
index 0000000..124d62b
--- /dev/null
+++ b/examples/io/pads/CMakeLists.txt
@@ -0,0 +1,22 @@
+# PSn00bSDK example CMake script
+# (C) 2021 spicyjpeg - MPL licensed
+
+cmake_minimum_required(VERSION 3.21)
+
+if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS})
+ set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake)
+endif()
+
+project(
+ pads
+ LANGUAGES C ASM
+ VERSION 1.0.0
+ DESCRIPTION "PSn00bSDK controller polling example"
+ HOMEPAGE_URL "http://lameguy64.net/?page=psn00bsdk"
+)
+
+file(GLOB _sources *.c *.s)
+psn00bsdk_add_executable(pads STATIC ${_sources})
+#psn00bsdk_add_cd_image(pads_iso pads iso.xml DEPENDS pads)
+
+install(FILES ${PROJECT_BINARY_DIR}/pads.exe DESTINATION .)
diff --git a/examples/io/pads/main.c b/examples/io/pads/main.c
new file mode 100644
index 0000000..bb56a27
--- /dev/null
+++ b/examples/io/pads/main.c
@@ -0,0 +1,259 @@
+/*
+ * PSn00bSDK controller polling example
+ * (C) 2021 spicyjpeg - MPL licensed
+ *
+ * This example shows how to poll controllers at high speeds (250 Hz) by using
+ * timer interrupts and communicating with devices on the SPI controller bus
+ * manually rather than relying on the BIOS pad driver, which is limited to
+ * 50/60 Hz and does not support custom commands. The example also demonstrates
+ * using configuration mode commands to force DualShock pads into analog mode
+ * and enable button pressure sensing on DualShock 2 (PS2) controllers.
+ *
+ * See spi.c for details on how the low-level SPI communication driver works.
+ * The DualShock handshaking logic is implemented here (see poll_cb() and
+ * dualshock_init_cb()). There is no support for memory cards in this example,
+ * but the code in spi.c can be used to read/write sectors on a memory card and
+ * combined with a higher-level filesystem driver for full support.
+ *
+ * IMPORTANT: this example doesn't work on pcsx-redux, and hasn't yet been
+ * tested on real hardware and/or with unofficial controllers. It works on
+ * DuckStation and no$psx though.
+ */
+
+#include <sys/types.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <psxetc.h>
+#include <psxgpu.h>
+#include <psxpad.h>
+
+#include "spi.h"
+
+static const char *const PAD_TYPEIDS[] = {
+ "[UNKNOWN]",
+ "MOUSE",
+ "NEGCON",
+ "IRQ10_GUN",
+ "DIGITAL",
+ "ANALOG_STICK",
+ "GUNCON",
+ "ANALOG",
+ "MULTITAP",
+ "[UNKNOWN]",
+ "[UNKNOWN]",
+ "[UNKNOWN]",
+ "[UNKNOWN]",
+ "[UNKNOWN]",
+ "JOGCON",
+ "CONFIG_MODE"
+};
+
+/* Display/GPU context utilities */
+
+#define SCREEN_XRES 320
+#define SCREEN_YRES 240
+
+typedef struct {
+ DISPENV disp;
+ DRAWENV draw;
+} DB;
+
+typedef struct {
+ DB db[2];
+ uint32_t db_active;
+} CONTEXT;
+
+void init_context(CONTEXT *ctx) {
+ DB *db;
+
+ ResetGraph(0);
+ ctx->db_active = 0;
+
+ db = &(ctx->db[0]);
+ SetDefDispEnv(&(db->disp), 0, 0, SCREEN_XRES, SCREEN_YRES);
+ SetDefDrawEnv(&(db->draw), SCREEN_XRES, 0, SCREEN_XRES, SCREEN_YRES);
+ setRGB0(&(db->draw), 63, 0, 127);
+ db->draw.isbg = 1;
+ db->draw.dtd = 1;
+
+ db = &(ctx->db[1]);
+ SetDefDispEnv(&(db->disp), SCREEN_XRES, 0, SCREEN_XRES, SCREEN_YRES);
+ SetDefDrawEnv(&(db->draw), 0, 0, SCREEN_XRES, SCREEN_YRES);
+ setRGB0(&(db->draw), 63, 0, 127);
+ db->draw.isbg = 1;
+ db->draw.dtd = 1;
+
+ PutDrawEnv(&(db->draw));
+ //PutDispEnv(&(db->disp));
+
+ // Create a text stream at the top of the screen.
+ FntLoad(960, 0);
+ FntOpen(8, 16, 304, 208, 2, 512);
+}
+
+void display(CONTEXT *ctx) {
+ DB *db;
+
+ DrawSync(0);
+ VSync(0);
+ ctx->db_active ^= 1;
+
+ db = &(ctx->db[ctx->db_active]);
+ PutDrawEnv(&(db->draw));
+ PutDispEnv(&(db->disp));
+ SetDispMask(1);
+}
+
+/* Pad buffers and callback */
+
+static volatile uint8_t pad_buff[2][34];
+static volatile size_t pad_buff_len[2];
+static volatile uint32_t pad_digital_only[2] = { 0, 0 };
+
+// Just a wrapper around spi_new_request(). This does not send the command
+// immediately but adds it to the driver's request queue.
+void send_pad_cmd(
+ uint32_t port,
+ PAD_COMMAND cmd,
+ uint8_t arg1,
+ uint8_t arg2,
+ SPICALLBACK callback
+) {
+ SPIREQUEST *req = spi_new_request();
+
+ req->len = 9;
+ req->port = port;
+ req->callback = callback;
+ req->pad_req.addr = 0x01;
+ req->pad_req.cmd = cmd;
+ req->pad_req.tap_mode = 0x00;
+ req->pad_req.motor_r = arg1;
+ req->pad_req.motor_l = arg2;
+
+ // The padding bytes must be 0xff when unlocking vibration motors.
+ memset(
+ req->pad_req.dummy,
+ (cmd == PAD_CMD_REQUEST_CONFIG) ? 0xff : 0x00,
+ 4
+ );
+}
+
+// This callback determines whether a pad that identifies as digital is
+// actually a DualShock in digital mode by checking how it replied to the
+// CONFIG_MODE command.
+void dualshock_init_cb(uint32_t port, const volatile uint8_t *buff, size_t rx_len) {
+ // Flag the pad as digital-only if it didn't reply.
+ // FIXME: rx_len should be 8 for digital pads, but sometimes it's only 4
+ if (rx_len < 4) {
+ printf("no, pad is digital-only (len = %d)\n", rx_len);
+
+ pad_digital_only[port] = 1;
+ return;
+ }
+
+ printf("yes, forcing analog mode (len = %d)\n", rx_len);
+
+ // Issue further commands to finish configuring the controller.
+ // https://gist.github.com/scanlime/5042071
+ send_pad_cmd(port, PAD_CMD_SET_ANALOG, 0x01, 0x02, 0); // 0x03 disables analog button...?
+ send_pad_cmd(port, PAD_CMD_INIT_PRESSURE, 0x00, 0x00, 0); // Ignored by DualShock 1
+ send_pad_cmd(port, PAD_CMD_REQUEST_CONFIG, 0x00, 0x01, 0);
+ send_pad_cmd(port, PAD_CMD_RESPONSE_CONFIG, 0xff, 0xff, 0); // Ignored by DualShock 1
+ send_pad_cmd(port, PAD_CMD_CONFIG_MODE, 0x00, 0x00, 0);
+}
+
+// This function is called by the pad timer ISR each time a pad is polled and a
+// response (even an invalid/incomplete one) is received.
+void poll_cb(uint32_t port, const volatile uint8_t *buff, size_t rx_len) {
+ // Copy the response to a persistent buffer so it can be accessed from the
+ // main loop and displayed on screen.
+ pad_buff_len[port] = rx_len;
+ if (rx_len)
+ memcpy((void *) pad_buff[port], (void *) buff, rx_len);
+
+ PADTYPE *pad = (PADTYPE *) buff;
+
+ // If this pad identifies as a digital pad and hasn't been flagged as a
+ // digital-only pad already, attempt to put it into analog mode by entering
+ // configuration mode. It this fails, it will be flagged as digital-only.
+ // The digital-only flag is reset when the controller is unplugged or stops
+ // returning digital pad responses.
+ if ((pad->raw.prefix == 0x5a) && (pad->raw.type == PAD_ID_DIGITAL)) {
+ if (!pad_digital_only[port]) {
+ printf("Detecting if pad %d supports config mode... ", port + 1);
+
+ send_pad_cmd(
+ port,
+ PAD_CMD_CONFIG_MODE,
+ 0x01,
+ 0x00,
+ &dualshock_init_cb
+ );
+ }
+
+ } else if (pad_digital_only[port]) {
+ printf("Clearing digital-only flag for pad %d\n", port + 1);
+
+ pad_digital_only[port] = 0;
+ }
+}
+
+/* Main */
+
+static CONTEXT ctx;
+
+int main(int argc, const char* argv[]) {
+ init_context(&ctx);
+ spi_init(&poll_cb);
+
+ uint32_t counter = 0;
+
+ while (1) {
+ FntPrint(-1, "COUNTER=%d", counter++);
+
+ for (uint32_t port = 0; port < 2; port++) {
+ PADTYPE *pad = (PADTYPE *) pad_buff[port];
+
+ // According to nocash docs, there is a hardware bug in DualShock
+ // controllers that causes the prefix byte (normally 0x5a) to turn
+ // into 0x00 if the analog button is pressed after configuration
+ // commands have been used.
+ //if (!pad->raw.prefix != 0x5a) {
+ if (!pad_buff_len[port]) {
+ FntPrint(-1, "\n\nPORT %d: NO DEVICE FOUND\n", port + 1);
+ if ((counter % 64) < 32)
+ FntPrint(-1, " CONNECT PAD NOW...");
+
+ continue;
+ }
+
+ uint32_t type = pad->raw.type;
+ uint32_t len = pad->raw.len * 2;
+ if (type == PAD_ID_MULTITAP)
+ len = 32;
+
+ FntPrint(
+ -1,
+ "\n\nPORT %d: %s\n TYPE=%d LEN=%d",
+ port + 1,
+ PAD_TYPEIDS[type],
+ type,
+ len
+ );
+
+ // Print a hexdump of the payload returned by the pad.
+ for (uint32_t i = 0; i < len; i++)
+ FntPrint(
+ -1,
+ (i % 8) ? " %02x" : "\n %02x",
+ pad_buff[port][i + 2]
+ );
+ }
+
+ FntFlush(-1);
+ display(&ctx);
+ }
+
+ return 0;
+}
diff --git a/examples/io/pads/spi.c b/examples/io/pads/spi.c
new file mode 100644
index 0000000..4728626
--- /dev/null
+++ b/examples/io/pads/spi.c
@@ -0,0 +1,216 @@
+/*
+ * PSn00bSDK controller polling example (SPI driver)
+ * (C) 2021 spicyjpeg - MPL licensed
+ *
+ * This is a fairly complete timer driven high-speed SPI driver, with support
+ * for sending custom commands (including memory card reads/writes), in about
+ * 200 lines of code. Feel free to copypaste and adapt it.
+ *
+ * The way this works is by maintaining a queue of requests to send, each with
+ * its own payload and callback. Timer 2 is configured to trigger an IRQ at
+ * regular intervals. On each tick, the next request in the queue (or a poll
+ * command if no request is pending) is prepared and the first byte is
+ * sent; if the controller asks for more data by pulling /ACK low, the next
+ * byte is sent and the received byte is placed into a buffer. This goes on
+ * until the last byte is exchanged and the controller stops asserting /ACK.
+ *
+ * On the next tick, the response buffer is passed to the request's callback
+ * and reset, and the next request in the queue is sent. This blindly assumes
+ * it only takes one tick for a request/response to be sent, which is the case
+ * for controllers' very small packets but not for memory cards. It is
+ * advisable to call spi_set_poll_rate() to temporarily reduce poll rate while
+ * accessing memory cards.
+ *
+ * Note that this driver completely takes over the SPI bus, so you won't be
+ * able to use any BIOS functions that rely on SPI access (i.e. pad and memory
+ * card APIs) alongside it.
+ */
+
+#include <sys/types.h>
+#include <string.h>
+#include <malloc.h>
+#include <psxetc.h>
+#include <psxapi.h>
+#include <psxpad.h>
+
+#include "spi.h"
+
+/* Register definitions */
+
+#define F_CPU 33868800UL
+
+#define TIM_VALUE(N) *((volatile uint32_t *) 0x1f801100 + 4 * (N))
+#define TIM_CTRL(N) *((volatile uint32_t *) 0x1f801104 + 4 * (N))
+#define TIM_RELOAD(N) *((volatile uint32_t *) 0x1f801108 + 4 * (N))
+#define JOY_TXRX *((volatile uint32_t *) 0x1f801040)
+#define JOY_STAT *((volatile uint32_t *) 0x1f801044)
+#define JOY_MODE *((volatile uint16_t *) 0x1f801048)
+#define JOY_CTRL *((volatile uint16_t *) 0x1f80104a)
+#define JOY_BAUD *((volatile uint16_t *) 0x1f80104e)
+
+/* Internal structures and globals */
+
+typedef struct _SPICONTEXT {
+ uint8_t tx_buff[SPI_BUFF_LEN];
+ uint8_t rx_buff[SPI_BUFF_LEN];
+ uint32_t tx_len, rx_len, port;
+ SPICALLBACK callback;
+} SPICONTEXT;
+
+static volatile SPICONTEXT ctx;
+static volatile SPIREQUEST volatile *current_req;
+static SPICALLBACK default_cb;
+
+/* Request queue management */
+
+static void prepare_poll_req(void) {
+ PADREQUEST *req = (PADREQUEST *) ctx.tx_buff;
+
+ req->addr = 0x01;
+ req->cmd = PAD_CMD_READ;
+ req->tap_mode = 0x00; // 0x01 to enable extended multitap response
+ req->motor_l = 0x00;
+ req->motor_r = 0x00;
+
+ ctx.tx_len = 4;
+ ctx.rx_len = 0;
+ ctx.port ^= 1;
+ ctx.callback = default_cb;
+}
+
+static void prepare_next_req(void) {
+ // Copy the contents of the first request in the queue into the TX buffer.
+ memcpy((void *) ctx.tx_buff, (void *) current_req->data, current_req->len);
+
+ ctx.tx_len = current_req->len;
+ ctx.rx_len = 0;
+ ctx.port = current_req->port;
+ ctx.callback = current_req->callback;
+
+ // Pop the first request from the queue by deallocating it and adjusting
+ // the pointer to the first queue item.
+ SPIREQUEST *next = current_req->next;
+
+ free((void *) current_req);
+ current_req = next;
+}
+
+/* Interrupt handlers */
+
+static void poll_timer_tick(void) {
+ // Fetch the last response byte, which wasn't followed by a pulse on /ACK,
+ // from the RX FIFO.
+ if (JOY_STAT & 0x0002)
+ ctx.rx_buff[ctx.rx_len - 1] = (uint8_t) JOY_TXRX;
+
+ if (ctx.callback)
+ ctx.callback(ctx.port, ctx.rx_buff, ctx.rx_len);
+
+ // If the request queue is empty, create a pad polling request.
+ if (current_req)
+ prepare_next_req();
+ else
+ prepare_poll_req();
+
+ // Prepare the SPI port by clearing any pending IRQ, pulling /CS high and
+ // enabling the /ACK IRQ. In order to communicate with controllers, /CS has
+ // to be driven low again for about 20 us before sending the first byte.
+ JOY_CTRL = 0x0010;
+ for (uint32_t i = 0; i < 50; i++)
+ __asm__("nop");
+
+ JOY_CTRL = 0x1003 | (ctx.port << 13);
+ for (uint32_t i = 0; i < 500; i++)
+ __asm__("nop");
+
+ // Send the first byte indicating which device to address. If the matching
+ // device is connected, it will reply by triggering the /ACK IRQ.
+ JOY_TXRX = ctx.tx_buff[0];
+}
+
+static void spi_ack_handler(void) {
+ // Wait until /ACK is pulled up by the controller before sending the next
+ // byte. According to nocash docs, this has to be done before resetting the
+ // IRQ.
+ while (JOY_STAT & 0x0080)
+ __asm__("nop");
+
+ // Keep /CS pulled low and acknowledge the IRQ (bit 4) to ensure it can be
+ // triggered again.
+ JOY_CTRL = 0x1013 | (ctx.port << 13);
+
+ if (!ctx.rx_len) {
+ // We just sent the first address byte. Obviously the response we
+ // received was read from an open bus, so the SPI port's internal FIFO
+ // must be flushed to ensure we are only going to read valid data from
+ // now on.
+ volatile uint32_t dummy;
+ while (JOY_STAT & 0x0002)
+ dummy = JOY_TXRX;
+
+ } else if (ctx.rx_len <= SPI_BUFF_LEN) {
+ // If this is not the first byte, put it in the RX buffer.
+ ctx.rx_buff[ctx.rx_len - 1] = (uint8_t) JOY_TXRX;
+ }
+
+ // Send the next byte, or a null byte if there is no more data to send and
+ // we're just reading a response.
+ ctx.rx_len++;
+ if (ctx.rx_len < ctx.tx_len)
+ JOY_TXRX = (uint32_t) ctx.tx_buff[ctx.rx_len];
+ else
+ JOY_TXRX = 0x00;
+}
+
+/* Public API */
+
+SPIREQUEST *spi_new_request(void) {
+ SPIREQUEST *req = malloc(sizeof(SPIREQUEST));
+
+ req->len = 0;
+ req->port = 0;
+ req->callback = 0;
+ req->next = 0;
+
+ // Find the last queued request by traversing the linked list and append a
+ // pointer to the new request.
+ if (!current_req) {
+ current_req = req;
+ } else {
+ volatile SPIREQUEST *volatile last = current_req;
+ while (last->next)
+ last = last->next;
+
+ last->next = req;
+ }
+
+ return req;
+}
+
+void spi_set_poll_rate(uint32_t value) {
+ TIM_CTRL(2) = 0x0258; // CLK/8 input, IRQ on reload, disable one-shot IRQ
+
+ if (value < 65)
+ TIM_RELOAD(2) = 0xffff;
+ else
+ TIM_RELOAD(2) = (F_CPU / 8) / value;
+}
+
+void spi_init(SPICALLBACK callback) {
+ // Disable the BIOS timer handler (which for some stupid reason is enabled
+ // by default, even though it does nothing) and set up custom interrupt
+ // handlers.
+ EnterCriticalSection();
+ ChangeClearRCnt(2, 0);
+ InterruptCallback(6, &poll_timer_tick);
+ InterruptCallback(7, &spi_ack_handler);
+ ExitCriticalSection();
+
+ JOY_CTRL = 0x0040; // Reset all registers
+ JOY_MODE = 0x000d; // 1x multiplier, 8 data bits, no parity
+ JOY_BAUD = 0x0088; // 250000 bps
+
+ spi_set_poll_rate(250);
+ current_req = 0;
+ default_cb = callback;
+}
diff --git a/examples/io/pads/spi.h b/examples/io/pads/spi.h
new file mode 100644
index 0000000..09223e2
--- /dev/null
+++ b/examples/io/pads/spi.h
@@ -0,0 +1,60 @@
+/*
+ * PSn00bSDK controller polling example (SPI driver)
+ * (C) 2021 spicyjpeg - MPL licensed
+ */
+
+#ifndef __SPI_H
+#define __SPI_H
+
+#include <psxpad.h>
+
+//#define SPI_BUFF_LEN 34
+#define SPI_BUFF_LEN 140
+
+/* Request structures */
+
+typedef void (*SPICALLBACK)(uint32_t port, const volatile uint8_t *buff, size_t rx_len);
+
+typedef struct _SPIREQUEST {
+ union {
+ uint8_t data[SPI_BUFF_LEN];
+ PADREQUEST pad_req;
+ MCDREQUEST mcd_req;
+ };
+ uint32_t len, port;
+ SPICALLBACK callback;
+ struct _SPIREQUEST *next;
+} SPIREQUEST;
+
+/* Public API */
+
+/**
+ * @brief Allocates a new request object and adds it to the request queue. The
+ * object must be populated afterwards by setting the length, callback and
+ * filling in the TX data buffer.
+ */
+SPIREQUEST *spi_new_request(void);
+
+/**
+ * @brief Changes the controller polling rate. The lowest supported rate is 65
+ * Hz (requests sent every 1/65th of a second, each port polled at 32.5 Hz when
+ * no request is pending).
+ *
+ * @param value
+ */
+void spi_set_poll_rate(uint32_t value);
+
+/**
+ * @brief Installs the SPI and timer 2 interrupt handlers and starts the poll
+ * timer. By default the polling rate is set to 250 Hz (125 Hz per port),
+ * however it can be changed at any time by calling spi_set_poll_rate().
+ *
+ * The provided callback (if any) is called to report the result of poll
+ * requests, which are issued automatically when no other request is in the
+ * queue. Passing NULL as callback does not disable auto-polling.
+ *
+ * @param callback
+ */
+void spi_init(SPICALLBACK callback);
+
+#endif
diff --git a/indev/README.md b/indev/README.md
index 2064a36..8263487 100644
--- a/indev/README.md
+++ b/indev/README.md
@@ -23,6 +23,10 @@ also go into this directory.
The Github release of this work-in-progress component includes delay
corrections for PAL consoles.
+ **NOTE**: the `io/pads` example also shows how to poll controllers manually
+ in a slightly different way (using a timer), and includes a reusable
+ low-level pad driver.
+
Work-in-progress components such as psxcd, interlace-exp, xptest and partest
are not included, as the former was completed while the remaining latter are
merely scrap test programs. \ No newline at end of file
diff --git a/libpsn00b/include/psxpad.h b/libpsn00b/include/psxpad.h
index 01aff06..d152896 100644
--- a/libpsn00b/include/psxpad.h
+++ b/libpsn00b/include/psxpad.h
@@ -3,10 +3,12 @@
* 2019 Lameguy64 / Meido-Tek Productions
*
* Currently only provides a bunch of definitions and a few structs but no
- * handling functions yet. Use the code in pad.s in one of the sample
- * programs for the meantime instead.
+ * handling functions yet. See the io/pads example for a simple manual pollling
+ * implementation (with support for DualShock 2 controllers).
*
* Work in progress, subject to change significantly in future releases.
+ *
+ * Reference: https://gist.github.com/scanlime/5042071
*/
#ifndef _PSXPAD_H
@@ -50,58 +52,153 @@
#define GCON_TRIGGER 8192
#define GCON_B 16384
-// Struct for digital, joystick, dual analog and Dualshock controllers
-typedef struct {
- unsigned char stat; // Status
- unsigned char len:4; // Data length (in halfwords)
- unsigned char type:4; // Device type:
- // 0x4 - digital pad
- // 0x5 - analog joystick
- // 0x7 - dual analog & Dualshock
- unsigned short btn; // Button states
- unsigned char rs_x,rs_y; // Right stick coordinates
- unsigned char ls_x,ls_y; // Left stick coordinates
+typedef enum {
+ PAD_ID_MOUSE = 0x1, // Sony PS1 mouse
+ PAD_ID_NEGCON = 0x2, // Namco neGcon
+ PAD_ID_IRQ10_GUN = 0x3, // "Konami" lightgun without composite video passthrough
+ PAD_ID_DIGITAL = 0x4, // Digital pad or Dual Analog/DualShock in digital mode
+ PAD_ID_ANALOG_STICK = 0x5, // Flight stick or Dual Analog in green LED mode
+ PAD_ID_GUNCON = 0x6, // Namco Guncon (lightgun with composite video passthrough)
+ PAD_ID_ANALOG = 0x7, // Dual Analog/DualShock in analog (red LED) mode
+ PAD_ID_MULTITAP = 0x8, // Multitap adapter (when tap_mode == 1)
+ PAD_ID_JOGCON = 0xe, // Namco Jogcon
+ PAD_ID_CONFIG_MODE = 0xf, // Dual Analog/DualShock in config mode (if len == 0x3)
+ PAD_ID_NONE = 0xf // No pad connected (if len == 0xf)
+} PAD_TYPEID;
+
+// Controller command definitions
+typedef enum {
+ PAD_CMD_INIT_PRESSURE = '@', // Initialize DS2 button pressure sensors (in config mode)
+ PAD_CMD_READ = 'B', // Read pad state and set rumble
+ PAD_CMD_CONFIG_MODE = 'C', // Toggle DualShock configuration mode
+ PAD_CMD_SET_ANALOG = 'D', // Set analog mode/LED state (in config mode)
+ PAD_CMD_GET_ANALOG = 'E', // Get analog mode/LED state (in config mode)
+ PAD_CMD_REQUEST_CONFIG = 'M', // Configure request/unlock vibration (in config mode)
+ PAD_CMD_RESPONSE_CONFIG = 'O' // Configure response/unlock DS2 pressure (in config mode)
+} PAD_COMMAND;
+
+// Memory card command/response definitions
+typedef enum {
+ MCD_CMD_READ = 'R', // Read sector
+ MCD_CMD_IDENTIFY = 'S', // Retrieve ID and card size information
+ MCD_CMD_WRITE = 'W' // Write sector
+} MCD_COMMAND;
+
+typedef enum {
+ MCD_STAT_OK = 'G',
+ MCD_STAT_BAD_CHECKSUM = 'N',
+ MCD_STAT_BAD_SECTOR = 0xff
+} MCD_STATUS;
+
+#define MCD_CMD_READ_LEN 139
+#define MCD_CMD_IDENTIFY_LEN 9
+#define MCD_CMD_WRITE_LEN 137
+
+// Memory card status flags
+#define MCD_FLAG_WRITE_ERROR 4 // Last write command failed
+#define MCD_FLAG_NOT_WRITTEN 8 // No writes have been issued yet
+#define MCD_FLAG_UNKNOWN 16 // Might be set on third-party cards
+
+// Struct for data returned by controllers
+typedef struct _PADTYPE {
+ union { // Header:
+ struct __attribute__((packed)) { // When parsing data returned by BIOS:
+ unsigned char stat; // Status
+ unsigned char len:4; // Payload length / 2, 0 for multitap
+ unsigned char type:4; // Device type (PAD_TYPEID)
+ };
+ struct __attribute__((packed)) { // When parsing raw controller response:
+ unsigned char len:4; // Payload length / 2, 0 for multitap
+ unsigned char type:4; // Device type (PAD_TYPEID)
+ unsigned char prefix; // Must be 0x5a
+ } raw;
+ };
+ struct { // Payload:
+ unsigned short btn; // Button states
+ union {
+ struct { // Analog controller:
+ unsigned char rs_x,rs_y; // Right stick coordinates
+ unsigned char ls_x,ls_y; // Left stick coordinates
+ unsigned char press[12]; // Button pressure (DualShock 2 only)
+ };
+ struct { // Mouse:
+ char x_mov; // X movement of mouse
+ char y_mov; // Y movement of mouse
+ };
+ struct { // neGcon:
+ unsigned char twist; // Controller twist
+ unsigned char btn_i; // I button value
+ unsigned char btn_ii; // II button value
+ unsigned char trg_l; // L trigger value
+ };
+ struct { // Jogcon:
+ unsigned short jog_rot; // Jog rotation
+ };
+ struct { // Guncon:
+ unsigned short gun_x; // Gun X position in dotclocks
+ unsigned short gun_y; // Gun Y position in scanlines
+ };
+ };
+ };
} PADTYPE;
-// Struct for a mouse controller
-typedef struct {
- unsigned char stat;
- unsigned char len:4;
- unsigned char type:4; // Device type (0x1)
- unsigned short btn;
- char x_mov; // X movement of mouse
- char y_mov; // Y movement of mouse
-} MOUSETYPE;
-
-// Struct for a neGcon controller (for Namco neGcon)
-typedef struct {
- unsigned char stat;
- unsigned char len:4;
- unsigned char type:4; // (0x2)
- unsigned short btn;
- unsigned char twist; // Controller twist
- unsigned char btn_i; // I button value
- unsigned char btn_ii; // II button value
- unsigned char trg_l; // L trigger value
-} NCONTYPE;
-
-// Struct for a Jogcon controller (for Namco Jogcon)
-typedef struct {
- unsigned char stat;
- unsigned char len:4;
- unsigned char type:4; // (0xE)
- unsigned short btn;
- unsigned short jog_rot; // Jog rotation
-} JCONTYPE;
-
-// Struct for a Gun-Con controller (for Namco Gun-Con)
-typedef struct {
- unsigned char status;
- unsigned char len:4;
- unsigned char type:4; // (0x6)
- unsigned short btn;
- unsigned short gun_x; // Gun X position in dotclocks
- unsigned short gun_y; // Gun Y position in scanlines
-} GCONTYPE;
+typedef struct _MCDRESPONSE {
+ unsigned char flags; // Status flags
+ unsigned char type1; // Must be 0x5a
+ unsigned char type2; // Must be 0x5d
+ union {
+ struct { // MCD_CMD_READ response:
+ unsigned char dummy[2];
+ unsigned char ack1; // Must be 0x5c
+ unsigned char ack2; // Must be 0x5d
+ unsigned char lba_h;
+ unsigned char lba_l;
+ unsigned char data[128];
+ unsigned char checksum; // = lba_h ^ lba_l ^ data
+ unsigned char stat; // Status (MCD_STATUS)
+ } read;
+ struct { // MCD_CMD_IDENTIFY response:
+ unsigned char ack1; // Must be 0x5c
+ unsigned char ack2; // Must be 0x5d
+ unsigned char size_h; // Card capacity bits 8-15 (0x04 = 128KB)
+ unsigned char size_l; // Card capacity bits 0-7 (0x00 = 128KB)
+ unsigned char blksize_h; // Sector size bits 8-15 (must be 0x00)
+ unsigned char blksize_l; // Sector size bits 0-7 (must be 0x80)
+ } identify;
+ struct { // MCD_CMD_WRITE response:
+ unsigned char dummy[131];
+ unsigned char ack1; // Must be 0x5c
+ unsigned char ack2; // Must be 0x5d
+ unsigned char stat; // Status (MCD_STATUS)
+ } write;
+ };
+} MCDRESPONSE;
+
+//typedef PADTYPE MOUSETYPE;
+//typedef PADTYPE NCONTYPE;
+//typedef PADTYPE JCONTYPE;
+//typedef PADTYPE GCONTYPE;
+
+// Structs for raw controller request
+typedef struct _PADREQUEST {
+ unsigned char addr; // Must be 0x01 (or 02/03/04 for multitap pads)
+ unsigned char cmd; // Command (PAD_COMMAND)
+ unsigned char tap_mode; // 0x01 to enable multitap response
+ unsigned char motor_r; // Right motor control (on/off)
+ unsigned char motor_l; // Left motor control (PWM)
+ unsigned char dummy[4];
+} PADREQUEST;
+
+// Structs for raw memory card request
+typedef struct _MCDREQUEST {
+ unsigned char addr; // Must be 0x81 (or 02/03/04 for multitap cards)
+ unsigned char cmd; // Command (MCD_COMMAND)
+ unsigned char dummy[2];
+ unsigned char lba_h; // Sector address bits 8-15 (dummy for CMD_IDENTIFY)
+ unsigned char lba_l; // Sector address bits 0-7 (dummy for CMD_IDENTIFY)
+ unsigned char data[128]; // Sector payload (dummy for CMD_READ/CMD_IDENTIFY)
+ unsigned char checksum; // = lba_h ^ lba_l ^ data (CMD_WRITE only)
+ unsigned char dummy2[3];
+} MCDREQUEST;
#endif \ No newline at end of file
diff --git a/libpsn00b/libc/start.c b/libpsn00b/libc/start.c
index c5872df..c7dbfe5 100644
--- a/libpsn00b/libc/start.c
+++ b/libpsn00b/libc/start.c
@@ -88,9 +88,9 @@ void _start(int32_t override_argc, const char **override_argv) {
// Calculate how much RAM is available after the loaded executable and
// initialize heap accordingly.
- void *exe_end = _end + 4;
- unsigned int exe_size = (unsigned int) exe_end - (unsigned int) __text_start;
- InitHeap(exe_end, 0x200000 - (exe_size + STACK_MAX_SIZE));
+ void *exe_end = _end + 4;
+ size_t exe_size = (size_t) exe_end - (size_t) __text_start;
+ InitHeap(exe_end, 0x1f0000 - (exe_size + STACK_MAX_SIZE) & 0xfffffffc);
if (override_argv) {
__argc = override_argc;