Playing on the Nintendo Switch with a generic USB-HID controller (specifically, a fake PS3 controller)

In a previous post, we did some warmups — playing MSX games using a fake PS3 controller. In this post, we’ll be using GP2040-CE to control a Nintendo Switch. (No analog stick support implemented at the moment, but it wouldn’t be hard.) At the time of writing, most of the code to do this is already there! And I’m sure there will be support for USB-HID controllers in no time, so this post will probably be outdated soon.
Update: Analog is implemented too, and the diff below has been updated.

Anyway, you just need to put GP2040-CE on your Pico and get into the web configuration. In add-ons, you enable keyboard support, and then set up the “Keyboard Host Configuration”, which looks like this:

GP2040-CE keyboard mapping and other configuration

Then you can connect a generic USB keyboard to the Raspberry Pi Pico, and connect the Pico to the Nintendo Switch. (For electrical reasons, I do not recommend setting a pin for 5V power here, and just putting the host USB +5 on the VBUS pin of the Pico.)

Things that could come in handy: Breadboard, Raspberry Pi Pico, USB port that can be connected to one of the Pico’s GPIO pins

If everything works and you can control Sonic using your keyboard, great, you can move on to the next step! If it didn’t work, it probably won’t magically get better from here on out, so make sure to check those connections. The green/white wires can be D+/D- or D-/D+!

Now we’ll perform a small modification to the existing code. I’m basing my work on commit 961c49d5b969ee749ae17bd4cbb2f0bad2380e71. Beware, this may or may not work with your controller. I’d recommend taking a look at the above-mentioned previous post where we modify a Pico-PIO-USB example and to check if your controller behaves the same way. I have only two controllers to test with, and I only tested with one! Anyway, here’s the diff:

diff --git a/headers/addons/keyboard_host.h b/headers/addons/keyboard_host.h
index af9c61b..74fb628 100644
--- a/headers/addons/keyboard_host.h
+++ b/headers/addons/keyboard_host.h
@@ -53,6 +53,7 @@ private:
 	bool _keyboard_host_enabled;
 	uint8_t getKeycodeFromModifier(uint8_t modifier);
 	void process_kbd_report(uint8_t dev_addr, hid_keyboard_report_t const *report);
+	void process_usb_gamepad_report(uint8_t dev_addr, const uint8_t *report);
 	GamepadState _keyboard_host_state;
 	KeyboardButtonMapping _keyboard_host_mapDpadUp;
 	KeyboardButtonMapping _keyboard_host_mapDpadDown;
@@ -74,4 +75,4 @@ private:
 	KeyboardButtonMapping _keyboard_host_mapButtonA2;
 };
 
-#endif  // _KeyboardHost_H_
\ No newline at end of file
+#endif  // _KeyboardHost_H_
diff --git a/src/addons/keyboard_host.cpp b/src/addons/keyboard_host.cpp
index a5294e9..8f59f4a 100644
--- a/src/addons/keyboard_host.cpp
+++ b/src/addons/keyboard_host.cpp
@@ -63,12 +63,15 @@ void KeyboardHostAddon::setup() {
 
 void KeyboardHostAddon::preprocess() {
   Gamepad *gamepad = Storage::getInstance().GetGamepad();
+  gamepad->setDpadMode(DpadMode::DPAD_MODE_DIGITAL);
+  gamepad->hasLeftAnalogStick = true;
+  gamepad->hasRightAnalogStick = true;
   gamepad->state.dpad     |= _keyboard_host_state.dpad;
   gamepad->state.buttons  |= _keyboard_host_state.buttons;
-  gamepad->state.lx       |= _keyboard_host_state.lx;
-  gamepad->state.ly       |= _keyboard_host_state.ly;
-  gamepad->state.rx       |= _keyboard_host_state.rx;
-  gamepad->state.ry       |= _keyboard_host_state.ry;
+  gamepad->state.lx       = _keyboard_host_state.lx;
+  gamepad->state.ly       = _keyboard_host_state.ly;
+  gamepad->state.rx       = _keyboard_host_state.rx;
+  gamepad->state.ry       = _keyboard_host_state.ry;
   gamepad->state.lt       |= _keyboard_host_state.lt;
   gamepad->state.rt       |= _keyboard_host_state.rt;
 }
@@ -89,10 +92,11 @@ void KeyboardHostAddon::report_received(uint8_t dev_addr, uint8_t instance, uint
   uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance);
 
   // tuh_hid_report_received_cb() will be invoked when report is available
-  if (itf_protocol != HID_ITF_PROTOCOL_KEYBOARD)
-    return;
-
-  process_kbd_report(dev_addr, (hid_keyboard_report_t const*) report );
+  if (itf_protocol == HID_ITF_PROTOCOL_KEYBOARD) {
+    process_kbd_report(dev_addr, (hid_keyboard_report_t const*) report );
+  } else {
+    process_usb_gamepad_report(dev_addr, report);
+  }
 }
 
 uint8_t KeyboardHostAddon::getKeycodeFromModifier(uint8_t modifier) {
@@ -161,4 +165,96 @@ void KeyboardHostAddon::process_kbd_report(uint8_t dev_addr, hid_keyboard_report
         _keyboard_host_state.rt = 0;
     }
   }
-}
\ No newline at end of file
+}
+
+// 0x1: L2
+// 0x2: R2
+// 0x4: L1
+// 0x8: R1
+// 0x10: Triangle
+// 0x20: Circle
+// 0x40: X
+// 0x80: Square?
+// 0x100: ?
+// 0x200: ?
+// 0x400: R3
+// 0x800: Start
+// 0x1000: Up
+// 0x2000: Right
+// 0x4000: Down?
+// 0x8000: Left?
+
+#define BUTTON_L2 0x1
+#define BUTTON_R2 0x2
+#define BUTTON_L1 0x4
+#define BUTTON_R1 0x8
+#define BUTTON_SQUARE 0x10
+#define BUTTON_CROSS 0x20
+#define BUTTON_CIRCLE 0x40
+#define BUTTON_TRIANGLE 0x80
+
+#define BUTTON_SELECT 0x100 // ?
+#define BUTTON_L3 0x200 // ?
+#define BUTTON_R3 0x400 // ?
+
+#define BUTTON_START 0x800
+#define BUTTON_UP 0x1000
+#define BUTTON_RIGHT 0x2000
+#define BUTTON_DOWN 0x4000
+#define BUTTON_LEFT 0x8000
+
+void KeyboardHostAddon::process_usb_gamepad_report(uint8_t dev_addr, const uint8_t *report)
+{
+  _keyboard_host_state.dpad = 0;
+  _keyboard_host_state.buttons = 0;
+  _keyboard_host_state.lx = GAMEPAD_JOYSTICK_MID;
+  _keyboard_host_state.ly = GAMEPAD_JOYSTICK_MID;
+  _keyboard_host_state.rx = GAMEPAD_JOYSTICK_MID;
+  _keyboard_host_state.ry = GAMEPAD_JOYSTICK_MID;
+  _keyboard_host_state.lt = 0;
+  _keyboard_host_state.rt = 0;
+
+  uint16_t button_state = report[2] << 8 | report[3];
+  uint8_t left_analog_x = report[6];
+  uint8_t left_analog_y = report[7];
+  uint8_t right_analog_x = report[8];
+  uint8_t right_analog_y = report[9];
+
+  const GamepadOptions& gamepadOptions = Storage::getInstance().getGamepadOptions();
+
+  _keyboard_host_state.dpad |=
+            ((button_state & BUTTON_UP)    ? (gamepadOptions.invertYAxis ? _keyboard_host_mapDpadDown.buttonMask : _keyboard_host_mapDpadUp.buttonMask) : _keyboard_host_state.dpad)
+          | ((button_state & BUTTON_DOWN)  ? (gamepadOptions.invertYAxis ? _keyboard_host_mapDpadUp.buttonMask : _keyboard_host_mapDpadDown.buttonMask) : _keyboard_host_state.dpad)
+          | ((button_state & BUTTON_LEFT)  ? _keyboard_host_mapDpadLeft.buttonMask  : _keyboard_host_state.dpad)
+          | ((button_state & BUTTON_RIGHT) ? _keyboard_host_mapDpadRight.buttonMask : _keyboard_host_state.dpad)
+        ;
+
+  _keyboard_host_state.buttons |=
+      ((button_state & BUTTON_CROSS)  ? _keyboard_host_mapButtonB1.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_CIRCLE)  ? _keyboard_host_mapButtonB2.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_SQUARE)  ? _keyboard_host_mapButtonB3.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_TRIANGLE)  ? _keyboard_host_mapButtonB4.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_L1)  ? _keyboard_host_mapButtonL1.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_R1)  ? _keyboard_host_mapButtonR1.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_L2)  ? _keyboard_host_mapButtonL2.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_R2)  ? _keyboard_host_mapButtonR2.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_SELECT)  ? _keyboard_host_mapButtonS1.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_START)  ? _keyboard_host_mapButtonS2.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_L3)  ? _keyboard_host_mapButtonL3.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_R3)  ? _keyboard_host_mapButtonR3.buttonMask  : _keyboard_host_state.buttons)
+  ;
+
+  /*
+   * #define GAMEPAD_JOYSTICK_MIN 0
+   * #define GAMEPAD_JOYSTICK_MID 0x7FFF
+   * #define GAMEPAD_JOYSTICK_MAX 0xFFFF
+   * lx, ly, rx, ry are 16-bit values, but our joystick produces 8-bit values
+   * our joystick's middle is at 0x80, so it would probably be slightly better to adjust that.
+   */
+  _keyboard_host_state.lx = (left_analog_x == 0x80) ? GAMEPAD_JOYSTICK_MID : (left_analog_x << 8 | left_analog_x);
+  _keyboard_host_state.ly = (left_analog_y == 0x80) ? GAMEPAD_JOYSTICK_MID : (left_analog_y << 8 | left_analog_y);
+  _keyboard_host_state.rx = (right_analog_x == 0x80) ? GAMEPAD_JOYSTICK_MID : (right_analog_x << 8 | right_analog_x);
+  _keyboard_host_state.ry = (right_analog_y == 0x80) ? GAMEPAD_JOYSTICK_MID : (right_analog_y << 8 | right_analog_y);
+  _keyboard_host_state.lt = 0;
+  _keyboard_host_state.rt = 0;
+}

Good luck. Miraculously, everything worked perfectly for me. The keyboard worked immediately, the above code modification worked immediately without having to do any debugging, and I’ve gotta say, my fake PS3 controller feels quite okay! (Note that you will have to press the PlayStation button after connecting your PS3 controller.)

The red part just hides some clutter.

PS3 controller repair log

Symptoms on real PS3: probably “crazy behavior”, I didn’t actually test on a real PS3.
Symptoms when connected to a computer with a program open that displays the gamepad status: random button presses, button “flickering”, buttons going on and off randomly, possibly depending on how the controller is held.

Cause in my case: rubber cushion is worn out, and/or physical damage to the controller’s case and/or loss of one of the screws. The rubber cushion sits on a piece of plastic, and a flex cable is sandwiched between the rubber cushion and the PCB. If the rubber cushion loses some of its original height, for example due to wear, or if one of the controller’s screws are lost and the PCB isn’t pressed as hard against the rubber cushion as it used to, buttons will randomly appear pressed or unpressed. (When there is absolutely no connection between the flex cable and the PCB, all buttons will appear pressed. When the connection is flaky, buttons may appear pressed all the time, or go on and off.)

Fix: adding a little height to the cushion fixed the problem for me.

In this pic, I’m holding the flex cable with one of my fingers. The tube (it’s a piece of heat shrink, actually) is what I added to improve contact between the flex cable and PCB. The original rubber is still there.

「DETEKER」の「電子工作でよく使う電子部品セット」の トランジスターについて

アマゾンで売られている「電子工作でよく使う電子部品セット」に18種のトランジスタが入っています。これらのトランジスターはどう違うのか、初心者は気になることでしょう。

トランジスターには色々な特性があります。例えば、電圧の限界値、許容電流、周波数の限界、ノイズ、ゲイン。これらの特性によって、そのトランジスターの用途が決まってきます。これらの特性は、トランジスターの「データシート」で簡単に調べられます。ネットで、例えば「2n2222 datasheet」のような検索をすればすぐに出てきます。

DETEKER のキットに含まれているトランジスターは18種もありますが、残念ながらすべての用途がカバーできるわけではないです。キットに含まれているすべてのトランジスターを調べた結果、これらのカテゴリーのトランジスターがありました。

  • オーディオ
  • 汎用
  • 電波
  • 高電圧

また、NPN トランジスターと同じ特性を持った PNP 版のトランジスターも多く含まれています。

個人的には残念だと思うのは、モーターの駆動に使えるトランジスターがないこと。(小型モーターなら、2n2222 あたりでいけなくはないかもしれませんが。)

さて、表を作ったのでご確認ください。載せていない属性もたくさんあります。データシートも、用途によっては、ノイズ値などはあったりなかったりします。(PC でご覧の場合は Shift+マウスホイールなどで横にスクロールができます。)

The following table shows several important properties of the transistors that come in DETEKER’s “Basic Electronic Components Kit” that is available on Amazon. (Use Shift+mouse wheel to scroll horizontally)

トランジスター名C-E 耐電圧C 電流ゲイン (最低は)
低電流時 (1 mA)
ゲイン (最大)
低電流時 (1 mA)
ゲイン (標準値)
低電流時 (1 mA)
ゲイン (最低は)
高電流時 (50 mA)
周波数特性種類データシートに書いてある用途カテゴリーデータシートの URL
Transistor nameMax C-E VoltageC currentGain (min)
at low current
(e.g. 1 mA)
Gain (max)
at low current
(e.g. 1 mA)
Gain (typical)
at low current
(e.g. 1 mA)
Gain (min)
(at high current)
FrequencyTypeDatasheets explicitly mentionAuthor’s interpretationDatasheet URL
S805020700 mA10040011040NPNClass B push-pull audio amplifier
General purpose
Complementary to S8550
Audio (オーディオ)
General purpose (汎用)
https://pdf1.alldatasheet.jp/datasheet-pdf/view/172696/UTC/S8050.html
S855020700 mA10040011040PNPSee S8050Audio
General purpose
https://pdf1.alldatasheet.jp/datasheet-pdf/view/172698/UTC/S8550.html
S901240500 mA6440040150 MHzPNPAudiohttps://pdf1.alldatasheet.jp/datasheet-pdf/view/433169/MCC/S9012-I.html
S9013NPNDesigned for use in 1W output amplifier of portable radios in class B push-pull operation
Complementary to S9012
Audiohttps://pdf1.alldatasheet.jp/datasheet-pdf/view/447547/TGS/S9013.html
S901445100 mA601000280NPNPre-amplifier, low level & low noise
Complementary to S9015
Audiohttps://pdf1.alldatasheet.jp/datasheet-pdf/view/54742/FAIRCHILD/S9014.html
S9015PNPSee S9014Audio
S90183050 mA28198100GBWP
700 MHz min, 1100 typ
NPNAM/FM amplifier, local oscillator of FM/VHF tunerRF (電波)https://pdf1.alldatasheet.jp/datasheet-pdf/view/54744/FAIRCHILD/S9018.html
A1015-50-150 mA7040025
(at -150 mA)
PNPLow-frequency amplifier
Complementary to C1815
Audiohttps://pdf1.alldatasheet.net/datasheet-pdf/view-marking/1161111/ONSEMI/KSA1015GRTA.html
C1815NPNSee A1015Audio
A42305200 mA2540
(at 30 mA)
50 MHz
20V, 10 mA
NPNHigh voltageHigh voltage (高電圧)https://pdf1.alldatasheet.com/datasheet-pdf/view/1312096/LUGUANG/A42.html
A92-305-200 mA2540
(at 30 mA)
50 MHz
20V, 10 mA
PNPHigh voltageHigh voltagehttps://pdf1.alldatasheet.com/datasheet-pdf/view/1312098/LUGUANG/A92.html
A733-50-100 mA90600200Current Gain Bandwidth
100 MHz min, 180 typ
PNPAF output amplifierAudiohttps://pdf1.alldatasheet.jp/datasheet-pdf/view/143737/STANSON/A733.html
C94550100 mA90600200Current Gain Bandwidth
100 MHz min, 180 typ
NPNAF output amplifierAudiohttps://pdf1.alldatasheet.jp/datasheet-pdf/view/143746/STANSON/C945.html
2N222230800 mA50300250 MHzNPNGeneral purposehttps://pdf1.alldatasheet.jp/datasheet-pdf/view/21676/STMICROELECTRONICS/2N2221-2N2222.html
2N3906-40200 mA80300250 MHzPNPSwitching and amplifier
Complementary to 2N3904
General purposehttps://pdf1.alldatasheet.jp/datasheet-pdf/view/61873/GE/2N3906.html
2N3904NPNHigh-speed switching
Complementary to 2N3906
General purposehttps://pdf1.alldatasheet.jp/datasheet-pdf/view/15077/PHILIPS/2N3904.html
2N5401150200 mA50240Current gain bandwidth product
100 MHz min, 300 max
PNPGeneral purposeGeneral purposehttps://pdf1.alldatasheet.jp/datasheet-pdf/view/50038/FAIRCHILD/2N5401.html
2N5551160600 mA8030
(at 50 mA)
NPNHigh voltagehttps://pdf1.alldatasheet.jp/datasheet-pdf/view/11488/ONSEMI/2N5551.html