Skip to content

Instantly share code, notes, and snippets.

@scintill
Last active November 20, 2020 22:21
Show Gist options
  • Save scintill/e265a1596b0e1ba38ea7404a3f142831 to your computer and use it in GitHub Desktop.
Save scintill/e265a1596b0e1ba38ea7404a3f142831 to your computer and use it in GitHub Desktop.
EC EN 427 USB gamepad

USB gamepad support

I had some fun adding USB gamepad support to my Space Invaders game and thought others might enjoy it. I used built-in Linux drivers. I tested with a Nintendo Switch USB controller (D-pad to move, any button to fire.) Some of the code might need to be adjusted for other controllers; there are some comments in that section that might help.

The basic idea is to hackily extend the intc module to be able to also check the gamepad device and return a fake interrupt flag for it, though Linux is itself handling whatever real interrupts are associated with USB. When that flag is set, I read from the device to see what the input is, and pass it to the rest of the game.

Here are some snippets in diff format from git, which probably won't fit your files exactly, but should give an idea of what I changed.

diff --git a/userspace/apps/space_invaders/CMakeLists.txt b/userspace/apps/space_invaders/CMakeLists.txt
index 4c1b546..320b407 100644
--- a/userspace/apps/space_invaders/CMakeLists.txt
+++ b/userspace/apps/space_invaders/CMakeLists.txt
@@ -15,2 +15,3 @@ add_executable(space_invaders
 	audio/audio.c
+	usbinput/usbinput.c
 )
diff --git a/userspace/apps/space_invaders/space_invaders.c b/userspace/apps/space_invaders/space_invaders.c
index b78ed73..5cb4c5b 100644
--- a/userspace/apps/space_invaders/space_invaders.c
+++ b/userspace/apps/space_invaders/space_invaders.c
@@ -16,2 +16,3 @@
 #include "system.h"
+#include "usbinput/usbinput.h"
 
@@ -99,2 +100,7 @@ int main() {
   }
+  err = usbinput_init();
+  if (err) {
+    printf("usbinput_init failed\n");
+    return EXIT_ERROR;
+  }
 
@@ -132,3 +138,3 @@ int main() {
     // Call interrupt controller function to wait for interrupt
-    uint32_t interrupts = intc_wait_for_interrupt();
+    uint32_t interrupts = intc_wait_for_interrupt_or_input(usbinput_fd);
 

Integrate to the main game loop. usbinput_process_event() passes gamepad button-presses to the state machine(s) the same way the board's buttons are done.

@@ -150,2 +156,7 @@ int main() {
       isr_buttons();
+    // handle USB input
+    if (interrupts & SYSTEM_INTC_IRQ_LINUXINPUT_MASK) {
+      interrupts ^= SYSTEM_INTC_IRQ_LINUXINPUT_MASK; // XXX clear this bit since it's not a real intc interrupt
+      usbinput_process_event();
+    }
 

Here we read an input event struct from the Linux input device file and write it into the button variables checked by the rest of the game. This implementation only supports one button being pressed at a time, so firing a bullet stops your tank motion and you'll have to release and press to start moving again.

I just read one event at a time. The poll() call in the next iteration of the main game loop will let me know if there is more data to read, and I'll read the next event then.

diff --git a/userspace/apps/space_invaders/usbinput/usbinput.c b/userspace/apps/space_invaders/usbinput/usbinput.c
new file mode 100644
index 0000000..442e31a
--- /dev/null
+++ b/userspace/apps/space_invaders/usbinput/usbinput.c
@@ -0,0 +1,65 @@
+#include "usbinput/usbinput.h"
+#include <fcntl.h>
+#include <globals/globals.h>
+#include <linux/input-event-codes.h>
+#include <linux/input.h>
+#include <printf.h>
+#include <stdio.h>
+#include <unistd.h>
+
+int usbinput_fd;
+
+uint32_t usbinput_init(void) {
+  usbinput_fd = open("/dev/input/event0", O_RDONLY);
+  if (usbinput_fd < 0) {
+    return USBINPUT_ERROR;
+  }
+  return USBINPUT_SUCCESS;
+}
+
+void usbinput_process_event(void) {
+  struct input_event event;
+  if (read(usbinput_fd, &event, sizeof(event)) != sizeof(event)) {
+    printf("%s: error reading\n", __func__);
+    return;
+  }
+
+  // You can learn the values you want with `sudo evtest /dev/input/event0`
+  // (after `sudo apt install evtest`)
+
+  if (event.type == EV_ABS && event.code == ABS_HAT0X) {
+    if (event.value == -1) {
+      //printf("%s: left\n", __func__);
+      globals_button = GLOBALS_BTN0;
+      globals_buttonIsValid = true;
+    } else if (event.value == 1) {
+      //printf("%s: right\n", __func__);
+      globals_button = GLOBALS_BTN1;
+      globals_buttonIsValid = true;
+    } else {
+      //printf("%s: release direction\n", __func__);
+      globals_button = GLOBALS_BTN_NONE;
+      globals_buttonIsValid = true;
+    }
+  } else if (event.type == EV_KEY) {
+    if (event.value != 0) {
+      //printf("%s: fire\n", __func__);
+      globals_button = GLOBALS_BTN2;
+      globals_buttonIsValid = true;
+    } else {
+      //printf("%s: release fire\n", __func__);
+      globals_button = GLOBALS_BTN_NONE;
+      globals_buttonIsValid = true;
+    }
+  } else if (event.type == EV_SYN) {
+    // "EV_SYN:
+    //  - Used as markers to separate events. Events may be separated in time or in
+    //    space, such as with the multitouch protocol."
+    // We don't need to do anything.
+  } else if (event.type == EV_MSC && event.code == MSC_SCAN) {
+    // some kind of raw scancode? we don't need it
+  } else {
+    printf("%s: unknown event: type=%d code=%d value=%d\n", __func__, event.type, event.code,
+           event.value);
+  }
+}
diff --git a/userspace/apps/space_invaders/usbinput/usbinput.h b/userspace/apps/space_invaders/usbinput/usbinput.h
new file mode 100644
index 0000000..cacbf20
--- /dev/null
+++ b/userspace/apps/space_invaders/usbinput/usbinput.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <stdint.h>
+
+#define USBINPUT_SUCCESS 0
+#define USBINPUT_ERROR 1
+
+extern int usbinput_fd;
+
+uint32_t usbinput_init(void);
+void usbinput_process_event(void);

In order to wait for either the intc interrupt or a Linux input event from the gamepad, we can't use read() because waiting on one would block the other. I went with poll(). With a timeout of -1, it will wait indefinitely for one or both of the file descriptors to be ready for reading, and it will tell me which of them it is.

diff --git a/userspace/drivers/intc/intc.c b/userspace/drivers/intc/intc.c
index 7bb8010..958786d 100644
--- a/userspace/drivers/intc/intc.c
+++ b/userspace/drivers/intc/intc.c
@@ -2,2 +2,3 @@
 #include <fcntl.h>
+#include <poll.h>
 #include <stdint.h>
@@ -6,2 +7,3 @@
 #include <sys/mman.h>
+#include "system.h"
 #include <unistd.h>
@@ -65,2 +67,22 @@ void intc_exit() {
 
+// This function will block until an interrupt or Linux input event occurrs
+// Returns: Bitmask of activated interrupts
+uint32_t intc_wait_for_interrupt_or_input(int input_fd) {
+  struct pollfd fds[] = {
+      { .fd = intc_fd, .events = POLLIN },
+      { .fd = input_fd, .events = POLLIN },
+  };
+  uint32_t intmask = 0;
+  if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) < 0) {
+    printf("%s: error from poll()\n", __func__);
+  }
+  if (fds[0].revents & POLLIN) {
+    // Reuse the normal intc function; there should be no actual wait since we already know it's ready.
+    // It's important to read() the fd as that function does, or else poll() will instantly return it
+    // again next time, causing high CPU usage.
+    intmask |= intc_wait_for_interrupt();
+  }
+  if (fds[1].revents & POLLIN) {
+    intmask |= SYSTEM_INTC_IRQ_LINUXINPUT_MASK;
+  }
+  return intmask;
+}
+
 // This function will block until an interrupt occurrs
diff --git a/userspace/drivers/intc/intc.h b/userspace/drivers/intc/intc.h
index 59e7ad2..7505f25 100644
--- a/userspace/drivers/intc/intc.h
+++ b/userspace/drivers/intc/intc.h
@@ -21,2 +21,6 @@ void intc_exit();
 
+// This function will block until an interrupt or Linux input event occurrs
+// Returns: Bitmask of activated interrupts
+uint32_t intc_wait_for_interrupt_or_input(int input_fd);
+
 // This function will block until an interrupt occurrs
diff --git a/userspace/drivers/system.h b/userspace/drivers/system.h
index 0d2be11..9316cc0 100644
--- a/userspace/drivers/system.h
+++ b/userspace/drivers/system.h
@@ -4,2 +4,3 @@
 #define SYSTEM_INTC_IRQ_PIT_MASK 0x08
+#define SYSTEM_INTC_IRQ_LINUXINPUT_MASK 0x80 /* XXX not really attached to the intc */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment