From bb5e51330a33b069fe069ca0da4703199eff11c5 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 20 Nov 2025 11:20:19 -0700 Subject: [PATCH] Initial commit --- .gitignore | 3 + .vscode/c_cpp_properties.json | 22 ++ .vscode/cmake-kits.json | 15 + .vscode/extensions.json | 9 + .vscode/launch.json | 50 +++ .vscode/settings.json | 40 ++ .vscode/tasks.json | 102 ++++++ CMakeLists.txt | 64 ++++ pico_sdk_import.cmake | 121 +++++++ switch-pico.cpp | 76 ++++ switch_pro_descriptors.h | 511 ++++++++++++++++++++++++++ switch_pro_driver.cpp | 663 ++++++++++++++++++++++++++++++++++ switch_pro_driver.h | 50 +++ tusb_config.h | 38 ++ 14 files changed, 1764 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/cmake-kits.json create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 CMakeLists.txt create mode 100644 pico_sdk_import.cmake create mode 100644 switch-pico.cpp create mode 100644 switch_pro_descriptors.h create mode 100644 switch_pro_driver.cpp create mode 100644 switch_pro_driver.h create mode 100644 tusb_config.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ed84ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +GP2040-CE +build +!.vscode/* diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..0a6cd5a --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -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 +} diff --git a/.vscode/cmake-kits.json b/.vscode/cmake-kits.json new file mode 100644 index 0000000..b0f3815 --- /dev/null +++ b/.vscode/cmake-kits.json @@ -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}" + } + } +] \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..a940d7c --- /dev/null +++ b/.vscode/extensions.json @@ -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" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..424aa71 --- /dev/null +++ b/.vscode/launch.json @@ -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}\"" + ] + }, + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cd4187a --- /dev/null +++ b/.vscode/settings.json @@ -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" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..d1b3193 --- /dev/null +++ b/.vscode/tasks.json @@ -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", + } + } + ] +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7ff57c6 --- /dev/null +++ b/CMakeLists.txt @@ -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) diff --git a/pico_sdk_import.cmake b/pico_sdk_import.cmake new file mode 100644 index 0000000..d493cc2 --- /dev/null +++ b/pico_sdk_import.cmake @@ -0,0 +1,121 @@ +# This is a copy of /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}) diff --git a/switch-pico.cpp b/switch-pico.cpp new file mode 100644 index 0000000..88f9fe6 --- /dev/null +++ b/switch-pico.cpp @@ -0,0 +1,76 @@ +#include +#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 + } +} diff --git a/switch_pro_descriptors.h b/switch_pro_descriptors.h new file mode 100644 index 0000000..be1a667 --- /dev/null +++ b/switch_pro_descriptors.h @@ -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 + +#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(data[0]) | ((data[1] & 0x0F) << 8); + } + + uint16_t getY() { + return static_cast((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(data[offset]) | ((data[offset + 1] & 0x0F) << 8); + y = static_cast(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(data[offset]) | ((data[offset + 1] & 0x0F) << 8); + y = static_cast(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 +}; diff --git a/switch_pro_driver.cpp b/switch_pro_driver.cpp new file mode 100644 index 0000000..f2e5df2 --- /dev/null +++ b/switch_pro_driver.cpp @@ -0,0 +1,663 @@ +#include "switch_pro_driver.h" + +#include +#include +#include +#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(factory_config_data); +static const SwitchUserCalibration* user_calibration [[maybe_unused]] = reinterpret_cast(user_calibration_data); + +static std::map 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(get_rand_32() % 0xff), static_cast(get_rand_32() % 0xff), static_cast(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(packet[1]) | (static_cast(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(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 +#include +#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); diff --git a/tusb_config.h b/tusb_config.h new file mode 100644 index 0000000..ff97f2e --- /dev/null +++ b/tusb_config.h @@ -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_