Initial commit

This commit is contained in:
Joey Yakimowich-Payne 2025-11-20 11:20:19 -07:00
commit bb5e51330a
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
14 changed files with 1764 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
GP2040-CE
build
!.vscode/*

22
.vscode/c_cpp_properties.json vendored Normal file
View file

@ -0,0 +1,22 @@
{
"configurations": [
{
"name": "Pico",
"includePath": [
"${workspaceFolder}/**",
"${userHome}/.pico-sdk/sdk/2.2.0/**"
],
"forcedInclude": [
"${workspaceFolder}/build/generated/pico_base/pico/config_autogen.h",
"${userHome}/.pico-sdk/sdk/2.2.0/src/common/pico_base_headers/include/pico.h"
],
"defines": [],
"compilerPath": "${userHome}/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gcc",
"compileCommands": "${workspaceFolder}/build/compile_commands.json",
"cStandard": "c17",
"cppStandard": "c++14",
"intelliSenseMode": "linux-gcc-arm"
}
],
"version": 4
}

15
.vscode/cmake-kits.json vendored Normal file
View file

@ -0,0 +1,15 @@
[
{
"name": "Pico",
"compilers": {
"C": "${command:raspberry-pi-pico.getCompilerPath}",
"CXX": "${command:raspberry-pi-pico.getCxxCompilerPath}"
},
"environmentVariables": {
"PATH": "${command:raspberry-pi-pico.getEnvPath};${env:PATH}"
},
"cmakeSettings": {
"Python3_EXECUTABLE": "${command:raspberry-pi-pico.getPythonPath}"
}
}
]

9
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,9 @@
{
"recommendations": [
"marus25.cortex-debug",
"ms-vscode.cpptools",
"ms-vscode.cpptools-extension-pack",
"ms-vscode.vscode-serial-monitor",
"raspberry-pi.raspberry-pi-pico"
]
}

50
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,50 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Pico Debug (Cortex-Debug)",
"cwd": "${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
"executable": "${command:raspberry-pi-pico.launchTargetPath}",
"request": "launch",
"type": "cortex-debug",
"servertype": "openocd",
"serverpath": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
"gdbPath": "${command:raspberry-pi-pico.getGDBPath}",
"device": "${command:raspberry-pi-pico.getChipUppercase}",
"configFiles": [
"interface/cmsis-dap.cfg",
"target/${command:raspberry-pi-pico.getTarget}.cfg"
],
"svdFile": "${userHome}/.pico-sdk/sdk/2.2.0/src/${command:raspberry-pi-pico.getChip}/hardware_regs/${command:raspberry-pi-pico.getChipUppercase}.svd",
"runToEntryPoint": "main",
// Fix for no_flash binaries, where monitor reset halt doesn't do what is expected
// Also works fine for flash binaries
"overrideLaunchCommands": [
"monitor reset init",
"load \"${command:raspberry-pi-pico.launchTargetPath}\""
],
"openOCDLaunchCommands": [
"adapter speed 5000"
]
},
{
"name": "Pico Debug (Cortex-Debug with external OpenOCD)",
"cwd": "${workspaceRoot}",
"executable": "${command:raspberry-pi-pico.launchTargetPath}",
"request": "launch",
"type": "cortex-debug",
"servertype": "external",
"gdbTarget": "localhost:3333",
"gdbPath": "${command:raspberry-pi-pico.getGDBPath}",
"device": "${command:raspberry-pi-pico.getChipUppercase}",
"svdFile": "${userHome}/.pico-sdk/sdk/2.2.0/src/${command:raspberry-pi-pico.getChip}/hardware_regs/${command:raspberry-pi-pico.getChipUppercase}.svd",
"runToEntryPoint": "main",
// Fix for no_flash binaries, where monitor reset halt doesn't do what is expected
// Also works fine for flash binaries
"overrideLaunchCommands": [
"monitor reset init",
"load \"${command:raspberry-pi-pico.launchTargetPath}\""
]
},
]
}

40
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,40 @@
{
"cmake.showSystemKits": false,
"cmake.options.statusBarVisibility": "hidden",
"cmake.options.advanced": {
"build": {
"statusBarVisibility": "hidden"
},
"launch": {
"statusBarVisibility": "hidden"
},
"debug": {
"statusBarVisibility": "hidden"
}
},
"cmake.configureOnEdit": true,
"cmake.automaticReconfigure": true,
"cmake.configureOnOpen": true,
"cmake.generator": "Ninja",
"cmake.cmakePath": "${userHome}/.pico-sdk/cmake/v3.31.5/bin/cmake",
"C_Cpp.debugShortcut": false,
"terminal.integrated.env.windows": {
"PICO_SDK_PATH": "${env:USERPROFILE}/.pico-sdk/sdk/2.2.0",
"PICO_TOOLCHAIN_PATH": "${env:USERPROFILE}/.pico-sdk/toolchain/14_2_Rel1",
"Path": "${env:USERPROFILE}/.pico-sdk/toolchain/14_2_Rel1/bin;${env:USERPROFILE}/.pico-sdk/picotool/2.2.0-a4/picotool;${env:USERPROFILE}/.pico-sdk/cmake/v3.31.5/bin;${env:USERPROFILE}/.pico-sdk/ninja/v1.12.1;${env:PATH}"
},
"terminal.integrated.env.osx": {
"PICO_SDK_PATH": "${env:HOME}/.pico-sdk/sdk/2.2.0",
"PICO_TOOLCHAIN_PATH": "${env:HOME}/.pico-sdk/toolchain/14_2_Rel1",
"PATH": "${env:HOME}/.pico-sdk/toolchain/14_2_Rel1/bin:${env:HOME}/.pico-sdk/picotool/2.2.0-a4/picotool:${env:HOME}/.pico-sdk/cmake/v3.31.5/bin:${env:HOME}/.pico-sdk/ninja/v1.12.1:${env:PATH}"
},
"terminal.integrated.env.linux": {
"PICO_SDK_PATH": "${env:HOME}/.pico-sdk/sdk/2.2.0",
"PICO_TOOLCHAIN_PATH": "${env:HOME}/.pico-sdk/toolchain/14_2_Rel1",
"PATH": "${env:HOME}/.pico-sdk/toolchain/14_2_Rel1/bin:${env:HOME}/.pico-sdk/picotool/2.2.0-a4/picotool:${env:HOME}/.pico-sdk/cmake/v3.31.5/bin:${env:HOME}/.pico-sdk/ninja/v1.12.1:${env:PATH}"
},
"raspberry-pi-pico.cmakeAutoConfigure": false,
"raspberry-pi-pico.useCmakeTools": true,
"raspberry-pi-pico.cmakePath": "${HOME}/.pico-sdk/cmake/v3.31.5/bin/cmake",
"raspberry-pi-pico.ninjaPath": "${HOME}/.pico-sdk/ninja/v1.12.1/ninja"
}

102
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,102 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Compile Project",
"type": "process",
"isBuildCommand": true,
"command": "${userHome}/.pico-sdk/ninja/v1.12.1/ninja",
"args": ["-C", "${workspaceFolder}/build"],
"group": "build",
"presentation": {
"reveal": "always",
"panel": "dedicated"
},
"problemMatcher": "$gcc",
"windows": {
"command": "${env:USERPROFILE}/.pico-sdk/ninja/v1.12.1/ninja.exe"
}
},
{
"label": "Run Project",
"type": "process",
"command": "${env:HOME}/.pico-sdk/picotool/2.2.0-a4/picotool/picotool",
"args": [
"load",
"${command:raspberry-pi-pico.launchTargetPath}",
"-fx"
],
"presentation": {
"reveal": "always",
"panel": "dedicated"
},
"problemMatcher": [],
"windows": {
"command": "${env:USERPROFILE}/.pico-sdk/picotool/2.2.0-a4/picotool/picotool.exe"
}
},
{
"label": "Flash",
"type": "process",
"command": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
"args": [
"-s",
"${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
"-f",
"interface/cmsis-dap.cfg",
"-f",
"target/${command:raspberry-pi-pico.getTarget}.cfg",
"-c",
"adapter speed 5000; program \"${command:raspberry-pi-pico.launchTargetPath}\" verify reset exit"
],
"problemMatcher": [],
"windows": {
"command": "${env:USERPROFILE}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
}
},
{
"label": "Rescue Reset",
"type": "process",
"command": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
"args": [
"-s",
"${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
"-f",
"interface/cmsis-dap.cfg",
"-f",
"target/${command:raspberry-pi-pico.getChip}-rescue.cfg",
"-c",
"adapter speed 5000; reset halt; exit"
],
"problemMatcher": [],
"windows": {
"command": "${env:USERPROFILE}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
}
},
{
"label": "RISC-V Reset (RP2350)",
"type": "process",
"command": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
"args": [
"-s",
"${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
"-c",
"set USE_CORE { rv0 rv1 cm0 cm1 }",
"-f",
"interface/cmsis-dap.cfg",
"-f",
"target/rp2350.cfg",
"-c",
"adapter speed 5000; init;",
"-c",
"write_memory 0x40120158 8 { 0x3 }; echo [format \"Info : ARCHSEL 0x%02x\" [read_memory 0x40120158 8 1]];",
"-c",
"reset halt; targets rp2350.rv0; echo [format \"Info : ARCHSEL_STATUS 0x%02x\" [read_memory 0x4012015C 8 1]]; exit"
],
"problemMatcher": [],
"windows": {
"command": "${env:USERPROFILE}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
}
}
]
}

64
CMakeLists.txt Normal file
View file

@ -0,0 +1,64 @@
# Generated Cmake Pico project file
cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Initialise pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work ==
if(WIN32)
set(USERHOME $ENV{USERPROFILE})
else()
set(USERHOME $ENV{HOME})
endif()
set(sdkVersion 2.2.0)
set(toolchainVersion 14_2_Rel1)
set(picotoolVersion 2.2.0-a4)
set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake)
if (EXISTS ${picoVscode})
include(${picoVscode})
endif()
# ====================================================================================
set(PICO_BOARD pico CACHE STRING "Board type")
# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)
project(switch-pico C CXX ASM)
# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()
# Add executable. Default name is the project name, version 0.1
add_executable(switch-pico
switch-pico.cpp
switch_pro_driver.cpp
)
pico_set_program_name(switch-pico "switch-pico")
pico_set_program_version(switch-pico "0.1")
# Modify the below lines to enable/disable output over UART/USB
pico_enable_stdio_uart(switch-pico 0)
pico_enable_stdio_usb(switch-pico 0)
# Add the standard library to the build
target_link_libraries(switch-pico
pico_stdlib
tinyusb_device
tinyusb_board
hardware_uart
pico_rand
)
# Add the standard include files to the build
target_include_directories(switch-pico PRIVATE
${CMAKE_CURRENT_LIST_DIR}
)
pico_add_extra_outputs(switch-pico)

121
pico_sdk_import.cmake Normal file
View file

@ -0,0 +1,121 @@
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
# This can be dropped into an external project to help locate this SDK
# It should be include()ed prior to project()
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
endif ()
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
endif()
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
if (NOT PICO_SDK_PATH)
if (PICO_SDK_FETCH_FROM_GIT)
include(FetchContent)
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
if (PICO_SDK_FETCH_FROM_GIT_PATH)
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
endif ()
FetchContent_Declare(
pico_sdk
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
)
if (NOT pico_sdk)
message("Downloading Raspberry Pi Pico SDK")
# GIT_SUBMODULES_RECURSE was added in 3.17
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
GIT_SUBMODULES_RECURSE FALSE
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
else ()
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
endif ()
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
endif ()
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
else ()
message(FATAL_ERROR
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
)
endif ()
endif ()
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
if (NOT EXISTS ${PICO_SDK_PATH})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
endif ()
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
endif ()
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
include(${PICO_SDK_INIT_CMAKE_FILE})

76
switch-pico.cpp Normal file
View file

@ -0,0 +1,76 @@
#include <stdio.h>
#include "bsp/board.h"
#include "hardware/uart.h"
#include "pico/stdlib.h"
#include "tusb.h"
#include "switch_pro_driver.h"
// UART1 is reserved for external input frames from the host PC.
#define UART_ID uart1
#define BAUD_RATE 115200
#define UART_TX_PIN 4
#define UART_RX_PIN 5
static void init_uart_input() {
uart_init(UART_ID, BAUD_RATE);
gpio_set_function(UART_TX_PIN, GPIO_FUNC_UART);
gpio_set_function(UART_RX_PIN, GPIO_FUNC_UART);
uart_set_format(UART_ID, 8, 1, UART_PARITY_NONE);
}
static SwitchInputState neutral_input() {
SwitchInputState state{};
state.lx = SWITCH_PRO_JOYSTICK_MID;
state.ly = SWITCH_PRO_JOYSTICK_MID;
state.rx = SWITCH_PRO_JOYSTICK_MID;
state.ry = SWITCH_PRO_JOYSTICK_MID;
return state;
}
// Consume UART bytes and forward complete frames to the Switch Pro driver.
static void poll_uart_frames() {
static uint8_t buffer[8];
static uint8_t index = 0;
static absolute_time_t last_byte_time = {0};
static bool has_last_byte = false;
while (uart_is_readable(UART_ID)) {
uint8_t byte = uart_getc(UART_ID);
uint64_t now = to_ms_since_boot(get_absolute_time());
if (has_last_byte && (now - to_ms_since_boot(last_byte_time)) > 20) {
index = 0; // stale data, restart frame
}
last_byte_time = get_absolute_time();
has_last_byte = true;
if (index == 0) {
if (byte != 0xAA) {
continue; // wait for start-of-frame marker
}
}
buffer[index++] = byte;
if (index >= sizeof(buffer)) {
switch_pro_apply_uart_packet(buffer, sizeof(buffer));
index = 0;
}
}
}
int main() {
board_init();
stdio_init_all();
init_uart_input();
tusb_init();
switch_pro_init();
switch_pro_set_input(neutral_input());
while (true) {
tud_task(); // USB device tasks
poll_uart_frames(); // Pull controller state from UART1
switch_pro_task(); // Push state to the Switch host
}
}

511
switch_pro_descriptors.h Normal file
View file

@ -0,0 +1,511 @@
/*
* Switch Pro controller descriptor and data definitions.
* Copied from the GP2040-CE project to mirror the behaviour of its
* SwitchProDriver without pulling in the rest of the codebase.
*/
#pragma once
#include <stdint.h>
#define SWITCH_PRO_ENDPOINT_SIZE 64
// HAT report (4 bits)
#define SWITCH_PRO_HAT_UP 0x00
#define SWITCH_PRO_HAT_UPRIGHT 0x01
#define SWITCH_PRO_HAT_RIGHT 0x02
#define SWITCH_PRO_HAT_DOWNRIGHT 0x03
#define SWITCH_PRO_HAT_DOWN 0x04
#define SWITCH_PRO_HAT_DOWNLEFT 0x05
#define SWITCH_PRO_HAT_LEFT 0x06
#define SWITCH_PRO_HAT_UPLEFT 0x07
#define SWITCH_PRO_HAT_NOTHING 0x08
#define SWITCH_PRO_MASK_ZR (1U << 7)
#define SWITCH_PRO_MASK_R (1U << 6)
#define SWITCH_PRO_MASK_A (1U << 3)
#define SWITCH_PRO_MASK_B (1U << 2)
#define SWITCH_PRO_MASK_X (1U << 1)
#define SWITCH_PRO_MASK_Y 1U
#define SWITCH_PRO_MASK_CAPTURE (1U << 5)
#define SWITCH_PRO_MASK_HOME (1U << 4)
#define SWITCH_PRO_MASK_L3 (1U << 3)
#define SWITCH_PRO_MASK_R3 (1U << 2)
#define SWITCH_PRO_MASK_PLUS (1U << 1)
#define SWITCH_PRO_MASK_MINUS 1U
#define SWITCH_PRO_MASK_ZL (1U << 7)
#define SWITCH_PRO_MASK_L (1U << 6)
#define SWITCH_PRO_JOYSTICK_MIN 0x0000
#define SWITCH_PRO_JOYSTICK_MID 0x7FFF
#define SWITCH_PRO_JOYSTICK_MAX 0xFFFF
typedef enum {
REPORT_OUTPUT_00 = 0x00,
REPORT_FEATURE = 0x01,
REPORT_OUTPUT_10 = 0x10,
REPORT_OUTPUT_21 = 0x21,
REPORT_OUTPUT_30 = 0x30,
REPORT_CONFIGURATION = 0x80,
REPORT_USB_INPUT_81 = 0x81,
} SwitchReportID;
typedef enum {
IDENTIFY = 0x01,
HANDSHAKE,
BAUD_RATE,
DISABLE_USB_TIMEOUT,
ENABLE_USB_TIMEOUT
} SwitchOutputSubtypes;
typedef enum {
GET_CONTROLLER_STATE = 0x00,
BLUETOOTH_PAIR_REQUEST = 0x01,
REQUEST_DEVICE_INFO = 0x02,
SET_MODE = 0x03,
TRIGGER_BUTTONS = 0x04,
SET_SHIPMENT = 0x08,
SPI_READ = 0x10,
SET_NFC_IR_CONFIG = 0x21,
SET_NFC_IR_STATE = 0x22,
SET_PLAYER_LIGHTS = 0x30,
GET_PLAYER_LIGHTS = 0x31,
COMMAND_UNKNOWN_33 = 0x33,
SET_HOME_LIGHT = 0x38,
TOGGLE_IMU = 0x40,
IMU_SENSITIVITY = 0x41,
READ_IMU = 0x43,
ENABLE_VIBRATION = 0x48,
GET_VOLTAGE = 0x50,
} SwitchCommands;
typedef struct {
uint8_t data[3];
void setX(uint16_t x) {
data[0] = x & 0xFF;
data[1] = (data[1] & 0xF0) | ((x >> 8) & 0x0F);
}
void setY(uint16_t y) {
data[1] = (data[1] & 0x0F) | ((y & 0x0F) << 4);
data[2] = (y >> 4) & 0xFF;
}
uint16_t getX() {
return static_cast<uint16_t>(data[0]) | ((data[1] & 0x0F) << 8);
}
uint16_t getY() {
return static_cast<uint16_t>((data[1] >> 4)) | (data[2] << 4);
}
} SwitchAnalog;
// left and right calibration are stored differently for some reason, so two structs
typedef struct {
uint8_t data[9];
void getMin(uint16_t& x, uint16_t& y) const { packCalib(6, x, y); }
void getCenter(uint16_t& x, uint16_t& y) const { packCalib(3, x, y); }
void getMax(uint16_t& x, uint16_t& y) const { packCalib(0, x, y); }
void getRealMin(uint16_t& x, uint16_t& y) const {
uint16_t minX, minY;
uint16_t cenX, cenY;
getMin(minX, minY);
getCenter(cenX, cenY);
x = cenX - minX;
y = cenY - minY;
}
void getRealMax(uint16_t& x, uint16_t& y) const {
uint16_t maxX, maxY;
uint16_t cenX, cenY;
getMax(maxX, maxY);
getCenter(cenX, cenY);
x = cenX + maxX;
y = cenY + maxY;
}
void packCalib(uint8_t offset, uint16_t& x, uint16_t& y) const {
x = static_cast<uint16_t>(data[offset]) | ((data[offset + 1] & 0x0F) << 8);
y = static_cast<uint16_t>(data[offset + 2] << 4) | (data[offset + 1] >> 4);
}
} SwitchLeftCalibration;
typedef struct {
uint8_t data[9];
void getMin(uint16_t& x, uint16_t& y) const { packCalib(3, x, y); }
void getCenter(uint16_t& x, uint16_t& y) const { packCalib(0, x, y); }
void getMax(uint16_t& x, uint16_t& y) const { packCalib(6, x, y); }
void getRealMin(uint16_t& x, uint16_t& y) const {
uint16_t minX, minY;
uint16_t cenX, cenY;
getMin(minX, minY);
getCenter(cenX, cenY);
x = cenX - minX;
y = cenY - minY;
}
void getRealMax(uint16_t& x, uint16_t& y) const {
uint16_t maxX, maxY;
uint16_t cenX, cenY;
getMax(maxX, maxY);
getCenter(cenX, cenY);
x = cenX + maxX;
y = cenY + maxY;
}
void packCalib(uint8_t offset, uint16_t& x, uint16_t& y) const {
x = static_cast<uint16_t>(data[offset]) | ((data[offset + 1] & 0x0F) << 8);
y = static_cast<uint16_t>(data[offset + 2] << 4) | (data[offset + 1] >> 4);
}
} SwitchRightCalibration;
typedef struct
{
uint8_t red;
uint8_t green;
uint8_t blue;
} SwitchColorDefinition;
typedef struct __attribute((packed, aligned(1)))
{
uint8_t serialNumber[16];
uint8_t unknown00[2];
uint8_t deviceType;
uint8_t unknown01; // usually 0xA0
uint8_t unknown02[7];
uint8_t colorInfo; // 0 = default colors
uint8_t unknown03[4];
uint8_t motionCalibration[24];
uint8_t unknown04[5];
SwitchLeftCalibration leftStickCalibration;
SwitchRightCalibration rightStickCalibration;
uint8_t unknown08;
SwitchColorDefinition bodyColor;
SwitchColorDefinition buttonColor;
SwitchColorDefinition leftGripColor;
SwitchColorDefinition rightGripColor;
uint8_t unknown06[37];
uint8_t motionHorizontalOffsets[6];
uint8_t stickParams1[17];
uint8_t stickParams2[17];
uint8_t unknown07[0xE57];
} SwitchFactoryConfig;
typedef struct __attribute((packed, aligned(1)))
{
uint8_t unknown00[16];
uint8_t leftCalibrationMagic[2];
SwitchLeftCalibration leftCalibration;
uint8_t rightCalibrationMagic[2];
SwitchRightCalibration rightCalibration;
uint8_t motionCalibrationMagic[2];
uint8_t motionCalibration[24];
} SwitchUserCalibration;
typedef struct __attribute((packed, aligned(1)))
{
uint8_t connectionInfo : 4;
uint8_t batteryLevel : 4;
// byte 00
uint8_t buttonY : 1;
uint8_t buttonX : 1;
uint8_t buttonB : 1;
uint8_t buttonA : 1;
uint8_t buttonRightSR : 1;
uint8_t buttonRightSL : 1;
uint8_t buttonR : 1;
uint8_t buttonZR : 1;
// byte 01
uint8_t buttonMinus : 1;
uint8_t buttonPlus : 1;
uint8_t buttonThumbR : 1;
uint8_t buttonThumbL : 1;
uint8_t buttonHome : 1;
uint8_t buttonCapture : 1;
uint8_t dummy : 1;
uint8_t chargingGrip : 1;
// byte 02
uint8_t dpadDown : 1;
uint8_t dpadUp : 1;
uint8_t dpadRight : 1;
uint8_t dpadLeft : 1;
uint8_t buttonLeftSL : 1;
uint8_t buttonLeftSR : 1;
uint8_t buttonL : 1;
uint8_t buttonZL : 1;
SwitchAnalog leftStick;
SwitchAnalog rightStick;
} SwitchInputReport;
typedef struct __attribute((packed, aligned(1)))
{
uint8_t reportID;
uint8_t timestamp;
SwitchInputReport inputs;
uint8_t rumbleReport;
uint8_t imuData[36];
uint8_t padding[15];
} SwitchProReport;
typedef enum {
SWITCH_TYPE_LEFT_JOYCON = 0x01,
SWITCH_TYPE_RIGHT_JOYCON,
SWITCH_TYPE_PRO_CONTROLLER,
SWITCH_TYPE_FAMICOM_LEFT_JOYCON = 0x07,
SWITCH_TYPE_FAMICOM_RIGHT_JOYCON = 0x08,
SWITCH_TYPE_NES_LEFT_JOYCON = 0x09,
SWITCH_TYPE_NES_RIGHT_JOYCON = 0x0A,
SWITCH_TYPE_SNES = 0x0B,
SWITCH_TYPE_N64 = 0x0C,
} SwitchControllerType;
typedef struct {
uint8_t majorVersion;
uint8_t minorVersion;
uint8_t controllerType;
uint8_t unknown00;
uint8_t macAddress[6];
uint8_t unknown01;
uint8_t storedColors;
} SwitchDeviceInfo;
typedef struct
{
uint16_t buttons;
uint8_t hat;
uint8_t lx;
uint8_t ly;
uint8_t rx;
uint8_t ry;
} SwitchProOutReport;
static const uint8_t switch_pro_string_language[] = { 0x09, 0x04 };
static const uint8_t switch_pro_string_manufacturer[] = "Open Stick Community";
static const uint8_t switch_pro_string_product[] = "GP2040-CE (Pro Controller)";
static const uint8_t switch_pro_string_version[] = "000000000001";
static const uint8_t *switch_pro_string_descriptors[] __attribute__((unused)) =
{
switch_pro_string_language,
switch_pro_string_manufacturer,
switch_pro_string_product,
switch_pro_string_version
};
static const uint8_t switch_pro_device_descriptor[] =
{
0x12, // bLength
0x01, // bDescriptorType (Device)
0x00, 0x02, // bcdUSB 2.00
0x00, // bDeviceClass (Use class information in the Interface Descriptors)
0x00, // bDeviceSubClass
0x00, // bDeviceProtocol
0x40, // bMaxPacketSize0 64
0x7E, 0x05, // idVendor 0x057E
0x09, 0x20, // idProduct 0x2009
0x10, 0x02, // bcdDevice 4.10
0x01, // iManufacturer (String Index)
0x02, // iProduct (String Index)
0x03, // iSerialNumber (String Index)
0x01, // bNumConfigurations 1
};
static const uint8_t switch_pro_hid_descriptor[] =
{
0x09, // bLength
0x21, // bDescriptorType (HID)
0x11, 0x01, // bcdHID 1.11
0x00, // bCountryCode
0x01, // bNumDescriptors
0x22, // bDescriptorType[0] (HID)
0xCB, 0x00, // wDescriptorLength[0] 86
};
static const uint8_t switch_pro_configuration_descriptor[] =
{
0x09, // bLength
0x02, // bDescriptorType (Configuration)
0x29, 0x00, // wTotalLength 41
0x01, // bNumInterfaces 1
0x01, // bConfigurationValue
0x00, // iConfiguration (String Index)
0xA0, // bmAttributes Remote Wakeup
0xFA, // bMaxPower 500mA
0x09, // bLength
0x04, // bDescriptorType (Interface)
0x00, // bInterfaceNumber 0
0x00, // bAlternateSetting
0x02, // bNumEndpoints 2
0x03, // bInterfaceClass
0x00, // bInterfaceSubClass
0x00, // bInterfaceProtocol
0x00, // iInterface (String Index)
0x09, // bLength
0x21, // bDescriptorType (HID)
0x11, 0x01, // bcdHID 1.11
0x00, // bCountryCode
0x01, // bNumDescriptors
0x22, // bDescriptorType[0] (HID)
0xCB, 0x00, // wDescriptorLength[0] 203
0x07, // bLength
0x05, // bDescriptorType (Endpoint)
0x05, // bEndpointAddress (IN/D2H)
0x03, // bmAttributes (Interrupt)
0x40, 0x00, // wMaxPacketSize 64
0x08, // bInterval 8 (unit depends on device speed)
0x07, // bLength
0x05, // bDescriptorType (Endpoint)
0x01, // bEndpointAddress (OUT/H2D)
0x03, // bmAttributes (Interrupt)
0x40, 0x00, // wMaxPacketSize 64
0x08, // bInterval 8 (unit depends on device speed)
};
static const uint8_t switch_pro_report_descriptor[] =
{
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x15, 0x00, // Logical Minimum (0)
0x09, 0x04, // Usage (Joystick)
0xA1, 0x01, // Collection (Application)
0x85, 0x30, // Report ID (48)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x0A, // Usage Maximum (0x0A)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x0A, // Report Count (10)
0x55, 0x00, // Unit Exponent (0)
0x65, 0x00, // Unit (None)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x09, // Usage Page (Button)
0x19, 0x0B, // Usage Minimum (0x0B)
0x29, 0x0E, // Usage Maximum (0x0E)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x04, // Report Count (4)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x01, // Report Size (1)
0x95, 0x02, // Report Count (2)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x0B, 0x01, 0x00, 0x01, 0x00, // Usage (0x010001)
0xA1, 0x00, // Collection (Physical)
0x0B, 0x30, 0x00, 0x01, 0x00, // Usage (0x010030)
0x0B, 0x31, 0x00, 0x01, 0x00, // Usage (0x010031)
0x0B, 0x32, 0x00, 0x01, 0x00, // Usage (0x010032)
0x0B, 0x35, 0x00, 0x01, 0x00, // Usage (0x010035)
0x15, 0x00, // Logical Minimum (0)
0x27, 0xFF, 0xFF, 0x00, 0x00, // Logical Maximum (65534)
0x75, 0x10, // Report Size (16)
0x95, 0x04, // Report Count (4)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
0x0B, 0x39, 0x00, 0x01, 0x00, // Usage (0x010039)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x07, // Logical Maximum (7)
0x35, 0x00, // Physical Minimum (0)
0x46, 0x3B, 0x01, // Physical Maximum (315)
0x65, 0x14, // Unit (System: English Rotation, Length: Centimeter)
0x75, 0x04, // Report Size (4)
0x95, 0x01, // Report Count (1)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x09, // Usage Page (Button)
0x19, 0x0F, // Usage Minimum (0x0F)
0x29, 0x12, // Usage Maximum (0x12)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x04, // Report Count (4)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x08, // Report Size (8)
0x95, 0x34, // Report Count (52)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00)
0x85, 0x21, // Report ID (33)
0x09, 0x01, // Usage (0x01)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x85, 0x81, // Report ID (-127)
0x09, 0x02, // Usage (0x02)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x85, 0x01, // Report ID (1)
0x09, 0x03, // Usage (0x03)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x91, 0x83, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
0x85, 0x10, // Report ID (16)
0x09, 0x04, // Usage (0x04)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x91, 0x83, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
0x85, 0x80, // Report ID (-128)
0x09, 0x05, // Usage (0x05)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x91, 0x83, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
0x85, 0x82, // Report ID (-126)
0x09, 0x06, // Usage (0x06)
0x75, 0x08, // Report Size (8)
0x95, 0x3F, // Report Count (63)
0x91, 0x83, // Output (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Volatile)
0xC0, // End Collection
};

663
switch_pro_driver.cpp Normal file
View file

@ -0,0 +1,663 @@
#include "switch_pro_driver.h"
#include <algorithm>
#include <cstring>
#include <map>
#include "pico/rand.h"
#include "pico/time.h"
#include "tusb.h"
// force a report to be sent every X ms
#define SWITCH_PRO_KEEPALIVE_TIMER 5
static SwitchInputState g_input_state{
false, false, false, false,
false, false, false, false, false, false, false, false,
false, false, false, false, false, false,
SWITCH_PRO_JOYSTICK_MID, SWITCH_PRO_JOYSTICK_MID,
SWITCH_PRO_JOYSTICK_MID, SWITCH_PRO_JOYSTICK_MID};
static uint8_t report_buffer[SWITCH_PRO_ENDPOINT_SIZE] = {};
static uint8_t last_report[SWITCH_PRO_ENDPOINT_SIZE] = {};
static SwitchProReport switch_report{};
static uint8_t last_report_counter = 0;
static uint32_t last_report_timer = 0;
static bool is_ready = false;
static bool is_initialized = false;
static bool is_report_queued = false;
static bool report_sent = false;
static uint8_t queued_report_id = 0;
static uint8_t handshake_counter = 0;
static SwitchDeviceInfo device_info{};
static uint8_t player_id = 0;
static uint8_t input_mode = 0x30;
static bool is_imu_enabled = false;
static bool is_vibration_enabled = false;
static uint16_t leftMinX, leftMinY;
static uint16_t leftCenX, leftCenY;
static uint16_t leftMaxX, leftMaxY;
static uint16_t rightMinX, rightMinY;
static uint16_t rightCenX, rightCenY;
static uint16_t rightMaxX, rightMaxY;
static const uint8_t factory_config_data[0xEFF] = {
// serial number
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
// device type
SWITCH_TYPE_PRO_CONTROLLER,
// unknown
0xA0,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
// color options
0x02,
0xFF, 0xFF, 0xFF, 0xFF,
// config & calibration 1
0xE3, 0xFF, 0x39, 0xFF, 0xED, 0x01, 0x00, 0x40,
0x00, 0x40, 0x00, 0x40, 0x09, 0x00, 0xEA, 0xFF,
0xA1, 0xFF, 0x3B, 0x34, 0x3B, 0x34, 0x3B, 0x34,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
// config & calibration 2
// left stick
0xa4, 0x46, 0x6a, 0x00, 0x08, 0x80, 0xa4, 0x46,
0x6a,
// right stick
0x00, 0x08, 0x80, 0xa4, 0x46, 0x6a, 0xa4, 0x46,
0x6a,
0xFF,
// body color
0x1B, 0x1B, 0x1D,
// button color
0xFF, 0xFF, 0xFF,
// left grip color
0xEC, 0x00, 0x8C,
// right grip color
0xEC, 0x00, 0x8C,
0x01,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF,
0x50, 0xFD, 0x00, 0x00, 0xC6, 0x0F,
0x0F, 0x30, 0x61, 0xAE, 0x90, 0xD9, 0xD4, 0x14,
0x54, 0x41, 0x15, 0x54, 0xC7, 0x79, 0x9C, 0x33,
0x36, 0x63,
0x0F, 0x30, 0x61, 0xAE, 0x90, 0xD9, 0xD4, 0x14,
0x54, 0x41, 0x15, 0x54,
0xC7,
0x79,
0x9C,
0x33,
0x36,
0x63, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF
};
static const uint8_t user_calibration_data[0x3F] = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
// Left Stick
0xB2, 0xA1, 0xa4, 0x46, 0x6a, 0x00, 0x08, 0x80,
0xa4, 0x46, 0x6a,
// Right Stick
0xB2, 0xA1, 0x00, 0x08, 0x80, 0xa4, 0x46, 0x6a,
0xa4, 0x46, 0x6a,
// Motion
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};
static const SwitchFactoryConfig* factory_config = reinterpret_cast<const SwitchFactoryConfig*>(factory_config_data);
static const SwitchUserCalibration* user_calibration [[maybe_unused]] = reinterpret_cast<const SwitchUserCalibration*>(user_calibration_data);
static std::map<uint32_t, const uint8_t*> spi_flash_data = {
{0x6000, factory_config_data},
{0x8000, user_calibration_data}
};
static inline uint16_t scale16To12(uint16_t pos) { return pos >> 4; }
static SwitchInputState make_neutral_state() {
SwitchInputState s{};
s.lx = SWITCH_PRO_JOYSTICK_MID;
s.ly = SWITCH_PRO_JOYSTICK_MID;
s.rx = SWITCH_PRO_JOYSTICK_MID;
s.ry = SWITCH_PRO_JOYSTICK_MID;
return s;
}
static void send_identify() {
memset(report_buffer, 0x00, sizeof(report_buffer));
report_buffer[0] = REPORT_USB_INPUT_81;
report_buffer[1] = IDENTIFY;
report_buffer[2] = 0x00;
report_buffer[3] = device_info.controllerType;
for (uint8_t i = 0; i < 6; i++) {
report_buffer[4 + i] = device_info.macAddress[5 - i];
}
}
static bool send_report(uint8_t reportID, const void* reportData, uint16_t reportLength) {
bool result = tud_hid_report(reportID, reportData, reportLength);
if (last_report_counter < 255) {
last_report_counter++;
} else {
last_report_counter = 0;
}
return result;
}
static void read_spi_flash(uint8_t* dest, uint32_t address, uint8_t size) {
uint32_t addressBank = address & 0xFFFFFF00;
uint32_t addressOffset = address & 0x000000FF;
auto it = spi_flash_data.find(addressBank);
if (it != spi_flash_data.end()) {
const uint8_t* data = it->second;
memcpy(dest, data + addressOffset, size);
} else {
memset(dest, 0xFF, size);
}
}
static void handle_config_report(uint8_t switchReportID, uint8_t switchReportSubID, const uint8_t *reportData, uint16_t reportLength) {
bool canSend = false;
switch (switchReportSubID) {
case IDENTIFY:
send_identify();
canSend = true;
break;
case HANDSHAKE:
report_buffer[0] = REPORT_USB_INPUT_81;
report_buffer[1] = HANDSHAKE;
canSend = true;
break;
case BAUD_RATE:
report_buffer[0] = REPORT_USB_INPUT_81;
report_buffer[1] = BAUD_RATE;
canSend = true;
break;
case DISABLE_USB_TIMEOUT:
report_buffer[0] = REPORT_OUTPUT_30;
report_buffer[1] = switchReportSubID;
//if (handshakeCounter < 4) {
// handshakeCounter++;
//} else {
is_ready = true;
//}
canSend = true;
break;
case ENABLE_USB_TIMEOUT:
report_buffer[0] = REPORT_OUTPUT_30;
report_buffer[1] = switchReportSubID;
canSend = true;
break;
default:
report_buffer[0] = REPORT_OUTPUT_30;
report_buffer[1] = switchReportSubID;
canSend = true;
break;
}
if (canSend) is_report_queued = true;
}
static void handle_feature_report(uint8_t switchReportID, uint8_t switchReportSubID, const uint8_t *reportData, uint16_t reportLength) {
uint8_t commandID = reportData[10];
uint32_t spiReadAddress = 0;
uint8_t spiReadSize = 0;
bool canSend = false;
report_buffer[0] = REPORT_OUTPUT_21;
report_buffer[1] = last_report_counter;
memcpy(report_buffer + 2, &switch_report.inputs, sizeof(SwitchInputReport));
switch (commandID) {
case GET_CONTROLLER_STATE:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
report_buffer[15] = 0x03;
canSend = true;
break;
case BLUETOOTH_PAIR_REQUEST:
report_buffer[13] = 0x81;
report_buffer[14] = commandID;
report_buffer[15] = 0x03;
canSend = true;
break;
case REQUEST_DEVICE_INFO:
report_buffer[13] = 0x82;
report_buffer[14] = 0x02;
memcpy(&report_buffer[15], &device_info, sizeof(device_info));
canSend = true;
break;
case SET_MODE:
input_mode = reportData[11];
report_buffer[13] = 0x80;
report_buffer[14] = 0x03;
report_buffer[15] = input_mode;
canSend = true;
break;
case TRIGGER_BUTTONS:
report_buffer[13] = 0x83;
report_buffer[14] = 0x04;
canSend = true;
break;
case SET_SHIPMENT:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
canSend = true;
break;
case SPI_READ:
spiReadAddress = (reportData[14] << 24) | (reportData[13] << 16) | (reportData[12] << 8) | (reportData[11]);
spiReadSize = reportData[15];
report_buffer[13] = 0x90;
report_buffer[14] = reportData[10];
report_buffer[15] = reportData[11];
report_buffer[16] = reportData[12];
report_buffer[17] = reportData[13];
report_buffer[18] = reportData[14];
report_buffer[19] = reportData[15];
read_spi_flash(&report_buffer[20], spiReadAddress, spiReadSize);
canSend = true;
break;
case SET_NFC_IR_CONFIG:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
canSend = true;
break;
case SET_NFC_IR_STATE:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
canSend = true;
break;
case SET_PLAYER_LIGHTS:
player_id = reportData[11];
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
canSend = true;
break;
case GET_PLAYER_LIGHTS:
player_id = reportData[11];
report_buffer[13] = 0xB0;
report_buffer[14] = commandID;
report_buffer[15] = player_id;
canSend = true;
break;
case COMMAND_UNKNOWN_33:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
report_buffer[15] = 0x03;
canSend = true;
break;
case SET_HOME_LIGHT:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
report_buffer[15] = 0x00;
canSend = true;
break;
case TOGGLE_IMU:
is_imu_enabled = reportData[11];
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
report_buffer[15] = 0x00;
canSend = true;
break;
case IMU_SENSITIVITY:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
canSend = true;
break;
case ENABLE_VIBRATION:
is_vibration_enabled = reportData[11];
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
report_buffer[15] = 0x00;
canSend = true;
break;
case READ_IMU:
report_buffer[13] = 0xC0;
report_buffer[14] = commandID;
report_buffer[15] = reportData[11];
report_buffer[16] = reportData[12];
canSend = true;
break;
case GET_VOLTAGE:
report_buffer[13] = 0xD0;
report_buffer[14] = 0x50;
report_buffer[15] = 0x83;
report_buffer[16] = 0x06;
canSend = true;
break;
default:
report_buffer[13] = 0x80;
report_buffer[14] = commandID;
report_buffer[15] = 0x03;
canSend = true;
break;
}
if (canSend) is_report_queued = true;
}
static void update_switch_report_from_state() {
switch_report.inputs.dpadUp = g_input_state.dpad_up;
switch_report.inputs.dpadDown = g_input_state.dpad_down;
switch_report.inputs.dpadLeft = g_input_state.dpad_left;
switch_report.inputs.dpadRight = g_input_state.dpad_right;
switch_report.inputs.chargingGrip = 1;
switch_report.inputs.buttonY = g_input_state.button_y;
switch_report.inputs.buttonX = g_input_state.button_x;
switch_report.inputs.buttonB = g_input_state.button_b;
switch_report.inputs.buttonA = g_input_state.button_a;
switch_report.inputs.buttonRightSR = 0;
switch_report.inputs.buttonRightSL = 0;
switch_report.inputs.buttonR = g_input_state.button_r;
switch_report.inputs.buttonZR = g_input_state.button_zr;
switch_report.inputs.buttonMinus = g_input_state.button_minus;
switch_report.inputs.buttonPlus = g_input_state.button_plus;
switch_report.inputs.buttonThumbR = g_input_state.button_r3;
switch_report.inputs.buttonThumbL = g_input_state.button_l3;
switch_report.inputs.buttonHome = g_input_state.button_home;
switch_report.inputs.buttonCapture = g_input_state.button_capture;
switch_report.inputs.buttonLeftSR = 0;
switch_report.inputs.buttonLeftSL = 0;
switch_report.inputs.buttonL = g_input_state.button_l;
switch_report.inputs.buttonZL = g_input_state.button_zl;
uint16_t scaleLeftStickX = scale16To12(g_input_state.lx);
uint16_t scaleLeftStickY = scale16To12(g_input_state.ly);
uint16_t scaleRightStickX = scale16To12(g_input_state.rx);
uint16_t scaleRightStickY = scale16To12(g_input_state.ry);
switch_report.inputs.leftStick.setX(std::min(std::max(scaleLeftStickX,leftMinX), leftMaxX));
switch_report.inputs.leftStick.setY(-std::min(std::max(scaleLeftStickY,leftMinY), leftMaxY));
switch_report.inputs.rightStick.setX(std::min(std::max(scaleRightStickX,rightMinX), rightMaxX));
switch_report.inputs.rightStick.setY(-std::min(std::max(scaleRightStickY,rightMinY), rightMaxY));
switch_report.rumbleReport = 0x09;
}
void switch_pro_init() {
player_id = 0;
last_report_counter = 0;
handshake_counter = 0;
is_ready = false;
is_initialized = false;
is_report_queued = false;
report_sent = false;
device_info = {
.majorVersion = 0x04,
.minorVersion = 0x91,
.controllerType = SWITCH_TYPE_PRO_CONTROLLER,
.unknown00 = 0x02,
.macAddress = {0x7c, 0xbb, 0x8a, static_cast<uint8_t>(get_rand_32() % 0xff), static_cast<uint8_t>(get_rand_32() % 0xff), static_cast<uint8_t>(get_rand_32() % 0xff)},
.unknown01 = 0x01,
.storedColors = 0x02,
};
switch_report = {
.reportID = 0x30,
.timestamp = 0,
.inputs {
.connectionInfo = 0,
.batteryLevel = 0x08,
.buttonY = 0,
.buttonX = 0,
.buttonB = 0,
.buttonA = 0,
.buttonRightSR = 0,
.buttonRightSL = 0,
.buttonR = 0,
.buttonZR = 0,
.buttonMinus = 0,
.buttonPlus = 0,
.buttonThumbR = 0,
.buttonThumbL = 0,
.buttonHome = 0,
.buttonCapture = 0,
.dummy = 0,
.chargingGrip = 0,
.dpadDown = 0,
.dpadUp = 0,
.dpadRight = 0,
.dpadLeft = 0,
.buttonLeftSL = 0,
.buttonLeftSR = 0,
.buttonL = 0,
.buttonZL = 0,
.leftStick = {0xFF, 0xF7, 0x7F},
.rightStick = {0xFF, 0xF7, 0x7F},
},
.rumbleReport = 0,
.imuData = {0x00},
.padding = {0x00}
};
last_report_timer = to_ms_since_boot(get_absolute_time());
factory_config->leftStickCalibration.getRealMin(leftMinX, leftMinY);
factory_config->leftStickCalibration.getCenter(leftCenX, leftCenY);
factory_config->leftStickCalibration.getRealMax(leftMaxX, leftMaxY);
factory_config->rightStickCalibration.getRealMin(rightMinX, rightMinY);
factory_config->rightStickCalibration.getCenter(rightCenX, rightCenY);
factory_config->rightStickCalibration.getRealMax(rightMaxX, rightMaxY);
}
void switch_pro_set_input(const SwitchInputState& state) {
g_input_state = state;
}
void switch_pro_task() {
uint32_t now = to_ms_since_boot(get_absolute_time());
report_sent = false;
update_switch_report_from_state();
if (tud_suspended()) {
tud_remote_wakeup();
}
if (is_report_queued) {
if ((now - last_report_timer) > SWITCH_PRO_KEEPALIVE_TIMER) {
if (tud_hid_ready() && send_report(queued_report_id, report_buffer, 64) == true ) {
}
is_report_queued = false;
last_report_timer = now;
}
report_sent = true;
}
if (is_ready && !report_sent) {
if ((now - last_report_timer) > SWITCH_PRO_KEEPALIVE_TIMER) {
switch_report.timestamp = last_report_counter;
void * inputReport = &switch_report;
uint16_t report_size = sizeof(switch_report);
if (memcmp(last_report, inputReport, report_size) != 0) {
if (tud_hid_ready() && send_report(0, inputReport, report_size) == true ) {
memcpy(last_report, inputReport, report_size);
report_sent = true;
}
last_report_timer = now;
}
}
} else {
if (!is_initialized) {
send_identify();
if (tud_hid_ready() && tud_hid_report(0, report_buffer, 64) == true) {
is_initialized = true;
report_sent = true;
}
last_report_timer = now;
}
}
}
bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length) {
// Packet format: 0xAA, buttons(2 LE), hat, lx, ly, rx, ry
if (length < 8 || packet[0] != 0xAA) {
return false;
}
SwitchProOutReport out{};
out.buttons = static_cast<uint16_t>(packet[1]) | (static_cast<uint16_t>(packet[2]) << 8);
out.hat = packet[3];
out.lx = packet[4];
out.ly = packet[5];
out.rx = packet[6];
out.ry = packet[7];
auto expand_axis = [](uint8_t v) -> uint16_t {
return static_cast<uint16_t>(v) << 8 | v;
};
SwitchInputState state = make_neutral_state();
switch (out.hat) {
case SWITCH_PRO_HAT_UP: state.dpad_up = true; break;
case SWITCH_PRO_HAT_UPRIGHT: state.dpad_up = true; state.dpad_right = true; break;
case SWITCH_PRO_HAT_RIGHT: state.dpad_right = true; break;
case SWITCH_PRO_HAT_DOWNRIGHT: state.dpad_down = true; state.dpad_right = true; break;
case SWITCH_PRO_HAT_DOWN: state.dpad_down = true; break;
case SWITCH_PRO_HAT_DOWNLEFT: state.dpad_down = true; state.dpad_left = true; break;
case SWITCH_PRO_HAT_LEFT: state.dpad_left = true; break;
case SWITCH_PRO_HAT_UPLEFT: state.dpad_up = true; state.dpad_left = true; break;
default: break;
}
state.button_y = out.buttons & SWITCH_PRO_MASK_Y;
state.button_x = out.buttons & SWITCH_PRO_MASK_X;
state.button_b = out.buttons & SWITCH_PRO_MASK_B;
state.button_a = out.buttons & SWITCH_PRO_MASK_A;
state.button_r = out.buttons & SWITCH_PRO_MASK_R;
state.button_zr = out.buttons & SWITCH_PRO_MASK_ZR;
state.button_plus = out.buttons & SWITCH_PRO_MASK_PLUS;
state.button_minus = out.buttons & SWITCH_PRO_MASK_MINUS;
state.button_r3 = out.buttons & SWITCH_PRO_MASK_R3;
state.button_l3 = out.buttons & SWITCH_PRO_MASK_L3;
state.button_home = out.buttons & SWITCH_PRO_MASK_HOME;
state.button_capture = out.buttons & SWITCH_PRO_MASK_CAPTURE;
state.button_zl = out.buttons & SWITCH_PRO_MASK_ZL;
state.button_l = out.buttons & SWITCH_PRO_MASK_L;
state.lx = expand_axis(out.lx);
state.ly = expand_axis(out.ly);
state.rx = expand_axis(out.rx);
state.ry = expand_axis(out.ry);
switch_pro_set_input(state);
return true;
}
// HID callbacks
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) {
(void)instance;
(void)report_id;
(void)report_type;
(void)buffer;
(void)reqlen;
return 0;
}
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) {
(void)instance;
if (report_type != HID_REPORT_TYPE_OUTPUT) return;
memset(report_buffer, 0x00, bufsize);
uint8_t switchReportID = buffer[0];
uint8_t switchReportSubID = buffer[1];
if (switchReportID == REPORT_OUTPUT_00) {
} else if (switchReportID == REPORT_FEATURE) {
queued_report_id = report_id;
handle_feature_report(switchReportID, switchReportSubID, buffer, bufsize);
} else if (switchReportID == REPORT_CONFIGURATION) {
queued_report_id = report_id;
handle_config_report(switchReportID, switchReportSubID, buffer, bufsize);
} else {
}
}
uint8_t const * tud_hid_descriptor_report_cb(uint8_t itf) {
(void)itf;
return switch_pro_report_descriptor;
}
uint8_t const * tud_descriptor_device_cb(void) {
return switch_pro_device_descriptor;
}
uint8_t const * tud_descriptor_configuration_cb(uint8_t index) {
(void)index;
return switch_pro_configuration_descriptor;
}
static uint16_t desc_str[32];
uint16_t const * tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
(void)langid;
uint8_t chr_count;
if ( index == 0 ) {
memcpy(&desc_str[1], switch_pro_string_language, 2);
chr_count = 1;
} else {
if ( index >= sizeof(switch_pro_string_descriptors)/sizeof(switch_pro_string_descriptors[0]) ) return nullptr;
const uint8_t *str = switch_pro_string_descriptors[index];
chr_count = 0;
while ( str[chr_count] ) chr_count++;
if ( chr_count > 31 ) chr_count = 31;
for(uint8_t i=0; i<chr_count; i++) {
desc_str[1+i] = str[i];
}
}
desc_str[0] = (uint16_t) ((0x03 << 8 ) | (2*chr_count + 2));
return desc_str;
}

50
switch_pro_driver.h Normal file
View file

@ -0,0 +1,50 @@
/*
* Minimal Switch Pro controller emulation glue derived from the GP2040-CE
* SwitchProDriver. The driver keeps the same descriptors/handshake while
* exposing a simple API for feeding inputs.
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "switch_pro_descriptors.h"
typedef struct {
bool dpad_up;
bool dpad_down;
bool dpad_left;
bool dpad_right;
bool button_a;
bool button_b;
bool button_x;
bool button_y;
bool button_l;
bool button_r;
bool button_zl;
bool button_zr;
bool button_plus;
bool button_minus;
bool button_home;
bool button_capture;
bool button_l3;
bool button_r3;
uint16_t lx; // 0-65535
uint16_t ly;
uint16_t rx;
uint16_t ry;
} SwitchInputState;
// Initialize USB state and calibration before entering the main loop.
void switch_pro_init();
// Update the desired controller state for the next USB report.
void switch_pro_set_input(const SwitchInputState& state);
// Drive the Switch Pro USB state machine; call this frequently in the main loop.
void switch_pro_task();
// Convert a packed UART message into controller state (returns true if parsed).
bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length);

38
tusb_config.h Normal file
View file

@ -0,0 +1,38 @@
// TinyUSB configuration tailored for a single Switch Pro style HID interface.
// Data is derived from TinyUSB examples and tuned for a 64-byte HID endpoint.
#ifndef _TUSB_CONFIG_H_
#define _TUSB_CONFIG_H_
#ifdef __cplusplus
extern "C" {
#endif
#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED)
#ifndef CFG_TUSB_OS
#define CFG_TUSB_OS OPT_OS_NONE
#endif
#ifndef CFG_TUSB_MEM_SECTION
#define CFG_TUSB_MEM_SECTION
#endif
#ifndef CFG_TUSB_MEM_ALIGN
#define CFG_TUSB_MEM_ALIGN __attribute__((aligned(4)))
#endif
#define CFG_TUD_ENDPOINT0_SIZE 64
// Device class configuration
#define CFG_TUD_HID 1
#define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0
#define CFG_TUD_MIDI 0
#define CFG_TUD_VENDOR 0
#define CFG_TUD_HID_EP_BUFSIZE 64
#ifdef __cplusplus
}
#endif
#endif // _TUSB_CONFIG_H_