Seeed XIAO RP2350にいくつかのタクトスイッチとSSD1306のディスプレイを接続した基盤を作成したので、Nintendo Switchのコントローラーを作ってみました。
基板の作成
Seeed XIAO RP2350は画像の左上が0番ピン、左下が6番ピン、右下が7番ピン、右上が13番ピンとなっています。
0~3を左側のタクトスイッチの上、右、下、左と接続し、4番をSSD1306のSDA、5番をSCK(SCL)、6番を基盤の右上、7~10を右側のタクトスイッチの上、右、下、左と接続しました。11番はSSD1306のVDD、12番はGNDでSSD1306のGNDと全てのタクトスイッチの片方(右側)に接続しました。
13番ピンは5Vですがどこにも接続していません。
機能について
ボタンが9個しかないので、全部のNintendo Switch用のボタンに対応させるには到底足りません。その為、右上のタクトスイッチをファンクションキーとして使用し、この右上を押していないときは左側のタクトスイッチ4個を十字キー、右側のタクトスイッチをプロコン等と同様に上がX、右がA、下がB、左がYとなるようにし、ファンクションキーを押しているときは、左側の上をR、右がZR、下がZL、左がL、右側の上が「+」、右がHOME、下がキャプチャー、左が「ー」となるようにしました。
ソースコードの作成
Google AIモードを使用して作成しました。
概ね出力されたソースでそのまま動いたのですが、RP2350の場合「Adafruit_SSD1306」の初期化で「&Wire」ではなく「&Wire1」を使う必要があるので、指摘して「&Wire1」に変更してもらっています。また、ZLとL、ZRとRが入れ替わっている現象が起きていたのですが、ビットマスクの指定に誤りがあったらしく、「正しい割り当てに修正」という形で修正されています。他に「con」という余分な文字列が含まれていた部分を指摘し「不要なconを削除済み」というコメントが入っていたりします。
Google AIモードはかなり優秀ですが、ある程度は人間側でコードレビューできた方が良いように思いました。
#include "Adafruit_TinyUSB.h"
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// --- SSD1306 ディスプレイ設定 ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire1, OLED_RESET);
// --- ピン配置定義 ---
const int PIN_A = D8;
const int PIN_B = D9;
const int PIN_UP = D0;
const int PIN_DOWN = D2;
const int PIN_LEFT = D3;
const int PIN_RIGHT = D1;
const int PIN_Y = D10;
const int PIN_X = D7;
const int PIN_FN = D6; // Fn(同時押しトリガー)
// --- Switch コントローラーのデータ構造 ---
typedef struct {
uint16_t Button;
uint8_t Hat;
uint8_t LX;
uint8_t LY;
uint8_t RX;
uint8_t RY;
uint8_t Vendor;
} SwitchReport_t;
// --- 💡 Switch用ボタンビットマスク(正しい割り当てに修正) ---
#define SWITCH_Y 0x0001
#define SWITCH_B 0x0002
#define SWITCH_A 0x0004
#define SWITCH_X 0x0008
#define SWITCH_L 0x0010 // 👈 0x0010 が正しい L ボタン
#define SWITCH_R 0x0020 // 👈 0x0020 が正しい R ボタン
#define SWITCH_ZL 0x0040 // 👈 0x0040 が正しい ZL ボタン
#define SWITCH_ZR 0x0080 // 👈 0x0080 が正しい ZR ボタン
#define SWITCH_MINUS 0x0100
#define SWITCH_PLUS 0x0200
#define SWITCH_HOME 0x1000
#define SWITCH_CAPTURE 0x2000
// 十字キー(Hat)の値定義
#define HAT_UP 0x00
#define HAT_UP_RIGHT 0x01
#define HAT_RIGHT 0x02
#define HAT_DOWN_RIGHT 0x03
#define HAT_DOWN 0x04
#define HAT_DOWN_LEFT 0x05
#define HAT_LEFT 0x06
#define HAT_UP_LEFT 0x07
#define HAT_CENTER 0x08
uint8_t const desc_hid_report[] = {
0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x15, 0x00, 0x25, 0x01, 0x35, 0x00, 0x45, 0x01, 0x75, 0x01,
0x95, 0x10, 0x05, 0x09, 0x19, 0x01, 0x29, 0x10, 0x81, 0x02, 0x05, 0x01, 0x25, 0x07, 0x46, 0x3b,
0x01, 0x75, 0x04, 0x95, 0x01, 0x65, 0x14, 0x09, 0x39, 0x81, 0x42, 0x65, 0x00, 0x75, 0x04, 0x95,
0x01, 0x81, 0x01, 0x26, 0xff, 0x00, 0x46, 0xff, 0x00, 0x75, 0x08, 0x95, 0x04, 0x09, 0x30, 0x09,
0x31, 0x09, 0x32, 0x09, 0x35, 0x81, 0x02, 0x05, 0x0c, 0x75, 0x08, 0x95, 0x01, 0x09, 0x00, 0xb1,
0x03, 0xc0
};
Adafruit_USBD_HID usb_hid;
SwitchReport_t joyReport;
void reset_report() {
joyReport.Button = 0;
joyReport.Hat = HAT_CENTER;
joyReport.LX = 128;
joyReport.LY = 128;
joyReport.RX = 128;
joyReport.RY = 128;
joyReport.Vendor = 0;
}
void setup() {
int pins[] = {PIN_A, PIN_B, PIN_UP, PIN_DOWN, PIN_LEFT, PIN_RIGHT, PIN_Y, PIN_X, PIN_FN};
for (int i = 0; i < 9; i++) {
pinMode(pins[i], INPUT_PULLUP);
}
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
for(;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Switch Pad Init...");
display.display();
TinyUSBDevice.setID(0x0f0d, 0x00c1);
usb_hid.setPollInterval(2);
usb_hid.setReportDescriptor(desc_hid_report, sizeof(desc_hid_report));
usb_hid.begin();
reset_report();
}
void loop() {
if (!TinyUSBDevice.ready()) return;
reset_report();
String pressedButtons = "";
String dirText = "CENTER";
// 各物理ピンの生状態を取得 (LOW = 押されている)
bool p_a = (digitalRead(PIN_A) == LOW);
bool p_b = (digitalRead(PIN_B) == LOW);
bool p_up = (digitalRead(PIN_UP) == LOW);
bool p_down = (digitalRead(PIN_DOWN) == LOW);
bool p_left = (digitalRead(PIN_LEFT) == LOW);
bool p_right = (digitalRead(PIN_RIGHT) == LOW);
bool p_y = (digitalRead(PIN_Y) == LOW);
bool p_x = (digitalRead(PIN_X) == LOW);
bool p_fn = (digitalRead(PIN_FN) == LOW);
// --- 1. D6(Fn)が押されているときの特殊同時押し判定 ---
if (p_fn) {
// 💡 D6 + D3(LEFT) + D0(UP) = L + R 同時押し (競合を防ぐため一番最初に判定)
if (p_left && p_up) {
joyReport.Button |= (SWITCH_L | SWITCH_R);
pressedButtons += "L+R ";
}
// 💡 D6 + D3(LEFT) = L ボタン
else if (p_left) {
joyReport.Button |= SWITCH_L;
pressedButtons += "L ";
}
// 💡 D6 + D0(UP) = R ボタン
else if (p_up) {
joyReport.Button |= SWITCH_R;
pressedButtons += "R ";
}
// 💡 D6 + D2(DOWN) = ZL ボタン
if (p_down) {
joyReport.Button |= SWITCH_ZL;
pressedButtons += "ZL ";
}
// 💡 D6 + D1(RIGHT) = ZR ボタン
if (p_right) {
joyReport.Button |= SWITCH_ZR;
pressedButtons += "ZR ";
}
// D6 + D10(Y) = マイナス(-)
if (p_y) {
joyReport.Button |= SWITCH_MINUS;
pressedButtons += "- ";
}
// D6 + D7(X) = プラス(+)
if (p_x) {
joyReport.Button |= SWITCH_PLUS;
pressedButtons += "+ ";
}
// D6 + D8(A) = HOME
if (p_a) {
joyReport.Button |= SWITCH_HOME;
pressedButtons += "HOME ";
}
// D6 + D9(B) = CAPTURE
if (p_b) {
joyReport.Button |= SWITCH_CAPTURE;
pressedButtons += "CAPT ";
}
}
// --- 2. D6(Fn)が押されていないときの通常判定 ---
else {
// 通常のA, B, Y, Xボタン
if (p_a) { joyReport.Button |= SWITCH_A; pressedButtons += "A "; }
if (p_b) { joyReport.Button |= SWITCH_B; pressedButtons += "B "; }
if (p_y) { joyReport.Button |= SWITCH_Y; pressedButtons += "Y "; }
if (p_x) { joyReport.Button |= SWITCH_X; pressedButtons += "X "; }
// 通常の十字キー判定(不要なconを削除済み)
if (p_up && p_right) { joyReport.Hat = HAT_UP_RIGHT; dirText = "UP-RIGHT"; }
else if (p_down && p_right) { joyReport.Hat = HAT_DOWN_RIGHT; dirText = "DN-RIGHT"; }
else if (p_down && p_left) { joyReport.Hat = HAT_DOWN_LEFT; dirText = "DN-LEFT"; }
else if (p_up && p_left) { joyReport.Hat = HAT_UP_LEFT; dirText = "UP-LEFT"; }
else if (p_up) { joyReport.Hat = HAT_UP; dirText = "UP"; }
else if (p_down) { joyReport.Hat = HAT_DOWN; dirText = "DOWN"; }
else if (p_left) { joyReport.Hat = HAT_LEFT; dirText = "LEFT"; }
else if (p_right) { joyReport.Hat = HAT_RIGHT; dirText = "RIGHT"; }
}
// --- SSD1306 ディスプレイ表示処理 ---
display.clearDisplay();
display.setCursor(0, 0);
display.setTextSize(1);
display.println("[ XIAO RP2350 PAD ]");
display.println("---------------------");
display.print("Pressed: ");
display.setTextSize(2);
display.println(pressedButtons);
display.setTextSize(1);
display.print("Dir : ");
display.setTextSize(2);
display.println(dirText);
display.display();
// レポート送信
usb_hid.sendReport(0, &joyReport, sizeof(joyReport));
delay(5);
}
Arduino IDEでコンパイル、ビルドする方法
以下のサイト等を参考にArduino IDEをインストールし、ボードマネージャで「SEEED XIAO RP2350」を指定します。
https://wiki.seeedstudio.com/ja/xiao_rp2350_arduino/
①基本設定で以下のURLを追加する
https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
②「ボードを選択」で「Seeed XIAO RP2350」を選択する。
③「ツール」「USB Stack」で「Adafruit TinyUSB」を選択する。
④上記の①~③でコンパイル可能になると思います。
「スケッチ」→「コンパイル済みバイナリをエクスポート」を実行します。
⑤コンパイルが完了すると、ソースファイルの置いてあるフォルダに「build」フォルダが作成され、その中に「~.uf2」ファイルが作成されます。
Seeed XIAO RP2350へ書き込む方法
①「B」のボタンを押しながらUSBケーブルを接続します。
②下記のような形でUSBメモリーと同様にフォルダが開きます。ここに作成したuf2ファイルをドロップしてコピーすると書き込まれます。
動作確認
スーパーマリオブラザーズで動作確認しました。
十字キーの右とAボタンを押して、マリオがちょっとジャンプしています。
最後に
Seeed XIAO RP2350は背面にD11~D18の8個の拡張ピンのパッドがあるので、増やそうと思えばあと8つボタンを足すことができます。ただし、多数のボタンが必要な自作アケコン等をつくるのであれば、本家のRaspberry Pi Pico 2を使ったりした方がよさそうです。また、RP2350はアナログ入力が0~2の3個しかないので、アナログ入力を4軸以上使う場合は80ピン版のRP2350Bを搭載したボードを使用する必要があります。









