From b9e92de717c92567c2e97fee2359f851b6db6229 Mon Sep 17 00:00:00 2001 From: Jesse Zhao Date: Wed, 11 Mar 2026 12:42:02 -0700 Subject: [PATCH 01/10] Add STM32 HAL and stm32pintester board config with bootloader offset Adds STM32/maple HAL layer (XInput, GPIO, serial, keyboard mode) and stm32pintester board configuration. App is linked at 0x08001000 with VECT_TAB_ADDR relocated to support a 4KB custom bootloader at 0x08000000. --- HAL/stm32/include/comms/XInputBackend.hpp | 20 + HAL/stm32/include/config_defaults.hpp | 121 +++++ HAL/stm32/include/core/KeyboardMode.hpp | 25 + HAL/stm32/include/gpio.hpp | 25 + HAL/stm32/include/keycodes.h | 180 +++++++ HAL/stm32/include/reboot.hpp | 7 + HAL/stm32/include/serial.hpp | 15 + HAL/stm32/include/stdlib.hpp | 8 + HAL/stm32/include/util/state_util.hpp | 479 ++++++++++++++++++ HAL/stm32/proto/config.options | 99 ++++ HAL/stm32/src/comms/XInputBackend.cpp | 62 +++ HAL/stm32/src/comms/backend_init.cpp | 161 ++++++ HAL/stm32/src/comms/console_detection.cpp | 11 + HAL/stm32/src/core/KeyboardMode.cpp | 16 + HAL/stm32/src/core/mode_selection.cpp | 111 ++++ HAL/stm32/src/gpio.cpp | 17 + HAL/stm32/src/reboot.cpp | 15 + HAL/stm32/src/serial.cpp | 29 ++ config/stm32pintester/config.cpp | 128 +++++ config/stm32pintester/env.ini | 13 + config/stm32pintester/override_vect.py | 19 + .../stm32pintester/stm32f103re_bootloader.ld | 18 + platformio.ini | 32 ++ src/util/analog_filters.cpp | 2 +- 24 files changed, 1612 insertions(+), 1 deletion(-) create mode 100644 HAL/stm32/include/comms/XInputBackend.hpp create mode 100644 HAL/stm32/include/config_defaults.hpp create mode 100644 HAL/stm32/include/core/KeyboardMode.hpp create mode 100644 HAL/stm32/include/gpio.hpp create mode 100644 HAL/stm32/include/keycodes.h create mode 100644 HAL/stm32/include/reboot.hpp create mode 100644 HAL/stm32/include/serial.hpp create mode 100644 HAL/stm32/include/stdlib.hpp create mode 100644 HAL/stm32/include/util/state_util.hpp create mode 100644 HAL/stm32/proto/config.options create mode 100644 HAL/stm32/src/comms/XInputBackend.cpp create mode 100644 HAL/stm32/src/comms/backend_init.cpp create mode 100644 HAL/stm32/src/comms/console_detection.cpp create mode 100644 HAL/stm32/src/core/KeyboardMode.cpp create mode 100644 HAL/stm32/src/core/mode_selection.cpp create mode 100644 HAL/stm32/src/gpio.cpp create mode 100644 HAL/stm32/src/reboot.cpp create mode 100644 HAL/stm32/src/serial.cpp create mode 100644 config/stm32pintester/config.cpp create mode 100644 config/stm32pintester/env.ini create mode 100644 config/stm32pintester/override_vect.py create mode 100644 config/stm32pintester/stm32f103re_bootloader.ld diff --git a/HAL/stm32/include/comms/XInputBackend.hpp b/HAL/stm32/include/comms/XInputBackend.hpp new file mode 100644 index 00000000..59a989b0 --- /dev/null +++ b/HAL/stm32/include/comms/XInputBackend.hpp @@ -0,0 +1,20 @@ +#ifndef _COMMS_XINPUTBACKEND_HPP +#define _COMMS_XINPUTBACKEND_HPP + +#include "core/CommunicationBackend.hpp" +#include "core/InputSource.hpp" +#include "stdlib.hpp" + +#include + +class XInputBackend : public CommunicationBackend { + public: + XInputBackend(InputState &inputs, InputSource **input_sources, size_t input_source_count); + CommunicationBackendId BackendId(); + void SendReport(); + + private: + USBXBox360 _xbox360; +}; + +#endif diff --git a/HAL/stm32/include/config_defaults.hpp b/HAL/stm32/include/config_defaults.hpp new file mode 100644 index 00000000..f45e6614 --- /dev/null +++ b/HAL/stm32/include/config_defaults.hpp @@ -0,0 +1,121 @@ +#ifndef _CONFIG_DEFAULTS_HPP +#define _CONFIG_DEFAULTS_HPP + +#include + +// clang-format off + +const Config default_config = { + .game_mode_configs_count = 4, + .game_mode_configs = new GameModeConfig[4] { + GameModeConfig { + .mode_id = MODE_MELEE, + .name = {}, + .socd_pairs_count = 4, + .socd_pairs = new SocdPair[4] { + SocdPair { .button_dir1 = BTN_LF3, .button_dir2 = BTN_LF1, .socd_type = SOCD_2IP_NO_REAC }, + SocdPair { .button_dir1 = BTN_LF2, .button_dir2 = BTN_RF4, .socd_type = SOCD_2IP_NO_REAC }, + SocdPair { .button_dir1 = BTN_RT3, .button_dir2 = BTN_RT5, .socd_type = SOCD_2IP_NO_REAC }, + SocdPair { .button_dir1 = BTN_RT2, .button_dir2 = BTN_RT4, .socd_type = SOCD_2IP_NO_REAC }, + }, + .button_remapping_count = 0, + .button_remapping = {}, + .activation_binding_count = 3, + .activation_binding = new Button[3] { BTN_LT1, BTN_MB1, BTN_LF4 }, + .custom_mode_config = 0, + .keyboard_mode_config = 0, + .rgb_config = 0, + }, + GameModeConfig { + .mode_id = MODE_PROJECT_M, + .name = {}, + .socd_pairs_count = 4, + .socd_pairs = new SocdPair[4] { + SocdPair { .button_dir1 = BTN_LF3, .button_dir2 = BTN_LF1, .socd_type = SOCD_2IP_NO_REAC }, + SocdPair { .button_dir1 = BTN_LF2, .button_dir2 = BTN_RF4, .socd_type = SOCD_2IP_NO_REAC }, + SocdPair { .button_dir1 = BTN_RT3, .button_dir2 = BTN_RT5, .socd_type = SOCD_2IP_NO_REAC }, + SocdPair { .button_dir1 = BTN_RT2, .button_dir2 = BTN_RT4, .socd_type = SOCD_2IP_NO_REAC }, + }, + .button_remapping_count = 0, + .button_remapping = {}, + .activation_binding_count = 3, + .activation_binding = new Button[3] { BTN_LT1, BTN_MB1, BTN_LF3 }, + .custom_mode_config = 0, + .keyboard_mode_config = 0, + .rgb_config = 0, + }, + GameModeConfig { + .mode_id = MODE_ULTIMATE, + .name = {}, + .socd_pairs_count = 4, + .socd_pairs = new SocdPair[4] { + SocdPair { .button_dir1 = BTN_LF3, .button_dir2 = BTN_LF1, .socd_type = SOCD_2IP }, + SocdPair { .button_dir1 = BTN_LF2, .button_dir2 = BTN_RF4, .socd_type = SOCD_2IP }, + SocdPair { .button_dir1 = BTN_RT3, .button_dir2 = BTN_RT5, .socd_type = SOCD_2IP }, + SocdPair { .button_dir1 = BTN_RT2, .button_dir2 = BTN_RT4, .socd_type = SOCD_2IP }, + }, + .button_remapping_count = 0, + .button_remapping = {}, + .activation_binding_count = 3, + .activation_binding = new Button[3] { BTN_LT1, BTN_MB1, BTN_LF2 }, + .custom_mode_config = 0, + .keyboard_mode_config = 0, + .rgb_config = 0, + }, + GameModeConfig { + .mode_id = MODE_FGC, + .name = {}, + .socd_pairs_count = 2, + .socd_pairs = new SocdPair[2] { + SocdPair { .button_dir1 = BTN_LF3, .button_dir2 = BTN_LF1, .socd_type = SOCD_NEUTRAL }, + SocdPair { .button_dir1 = BTN_LT1, .button_dir2 = BTN_RT4, .socd_type = SOCD_NEUTRAL }, + }, + .button_remapping_count = 1, + .button_remapping = new ButtonRemap[1] { + ButtonRemap { .physical_button = BTN_RT4, .activates = BTN_LT1 }, + }, + .activation_binding_count = 3, + .activation_binding = new Button[3] { BTN_LT1, BTN_MB1, BTN_LF1 }, + .custom_mode_config = 0, + .keyboard_mode_config = 0, + .rgb_config = 0, + }, + }, + .communication_backend_configs_count = 1, + .communication_backend_configs = new CommunicationBackendConfig[1] { + CommunicationBackendConfig { + .backend_id = COMMS_BACKEND_XINPUT, + .default_mode_config = 1, + .activation_binding_count = 0, + .activation_binding = {}, + .secondary_backends = {}, + }, + }, + .custom_modes_count = 0, + .custom_modes = {}, + .keyboard_modes_count = 0, + .keyboard_modes = {}, + .rgb_configs_count = 0, + .rgb_configs = {}, + .default_backend_config = 1, + .default_usb_backend_config = 1, + .rgb_brightness = 0, + .has_melee_options = true, + .melee_options = { + .crouch_walk_os = false, + .disable_ledgedash_socd_override = false, + .has_custom_airdodge = false, + .custom_airdodge = { .x = 0, .y = 0 }, + }, + .has_project_m_options = true, + .project_m_options = { + .true_z_press = true, + .disable_ledgedash_socd_override = false, + .has_custom_airdodge = false, + .custom_airdodge = { .x = 0, .y = 0 }, + }, +}; + +// clang-format on + +#endif diff --git a/HAL/stm32/include/core/KeyboardMode.hpp b/HAL/stm32/include/core/KeyboardMode.hpp new file mode 100644 index 00000000..974ad30f --- /dev/null +++ b/HAL/stm32/include/core/KeyboardMode.hpp @@ -0,0 +1,25 @@ +#ifndef _CORE_KEYBOARDMODE_HPP +#define _CORE_KEYBOARDMODE_HPP + +#include "core/InputMode.hpp" +#include "core/socd.hpp" +#include "core/state.hpp" + +#include + +class KeyboardMode : public InputMode { + public: + KeyboardMode(); + ~KeyboardMode(); + void SendReport(const InputState &inputs); + + void UpdateOutputs(const InputState &inputs, OutputState &outputs) {} + + protected: + void Press(uint8_t keycode, bool press); + + private: + virtual void UpdateKeys(const InputState &inputs) = 0; +}; + +#endif diff --git a/HAL/stm32/include/gpio.hpp b/HAL/stm32/include/gpio.hpp new file mode 100644 index 00000000..52a15e5b --- /dev/null +++ b/HAL/stm32/include/gpio.hpp @@ -0,0 +1,25 @@ +#ifndef _GPIO_HPP +#define _GPIO_HPP + +#include "stdlib.hpp" + +namespace gpio { + enum class GpioMode { + GPIO_OUTPUT, + GPIO_INPUT, + GPIO_INPUT_PULLUP, + GPIO_INPUT_PULLDOWN, + }; + + void init_pin(uint pin, GpioMode mode); + + inline bool read_digital(uint pin) { + return digitalRead(pin); + } + + inline void write_digital(uint pin, bool value) { + digitalWrite(pin, value); + } +} + +#endif diff --git a/HAL/stm32/include/keycodes.h b/HAL/stm32/include/keycodes.h new file mode 100644 index 00000000..21ebd1c2 --- /dev/null +++ b/HAL/stm32/include/keycodes.h @@ -0,0 +1,180 @@ +#ifndef _KEYCODES_H +#define _KEYCODES_H + +// From TinyUSB: https://github.com/hathach/tinyusb/blob/master/src/class/hid/hid.h +//--------------------------------------------------------------------+ +// HID KEYCODE +//--------------------------------------------------------------------+ +#define HID_KEY_NONE 0x00 +#define HID_KEY_A 0x04 +#define HID_KEY_B 0x05 +#define HID_KEY_C 0x06 +#define HID_KEY_D 0x07 +#define HID_KEY_E 0x08 +#define HID_KEY_F 0x09 +#define HID_KEY_G 0x0A +#define HID_KEY_H 0x0B +#define HID_KEY_I 0x0C +#define HID_KEY_J 0x0D +#define HID_KEY_K 0x0E +#define HID_KEY_L 0x0F +#define HID_KEY_M 0x10 +#define HID_KEY_N 0x11 +#define HID_KEY_O 0x12 +#define HID_KEY_P 0x13 +#define HID_KEY_Q 0x14 +#define HID_KEY_R 0x15 +#define HID_KEY_S 0x16 +#define HID_KEY_T 0x17 +#define HID_KEY_U 0x18 +#define HID_KEY_V 0x19 +#define HID_KEY_W 0x1A +#define HID_KEY_X 0x1B +#define HID_KEY_Y 0x1C +#define HID_KEY_Z 0x1D +#define HID_KEY_1 0x1E +#define HID_KEY_2 0x1F +#define HID_KEY_3 0x20 +#define HID_KEY_4 0x21 +#define HID_KEY_5 0x22 +#define HID_KEY_6 0x23 +#define HID_KEY_7 0x24 +#define HID_KEY_8 0x25 +#define HID_KEY_9 0x26 +#define HID_KEY_0 0x27 +#define HID_KEY_ENTER 0x28 +#define HID_KEY_ESCAPE 0x29 +#define HID_KEY_BACKSPACE 0x2A +#define HID_KEY_TAB 0x2B +#define HID_KEY_SPACE 0x2C +#define HID_KEY_MINUS 0x2D +#define HID_KEY_EQUAL 0x2E +#define HID_KEY_BRACKET_LEFT 0x2F +#define HID_KEY_BRACKET_RIGHT 0x30 +#define HID_KEY_BACKSLASH 0x31 +#define HID_KEY_EUROPE_1 0x32 +#define HID_KEY_SEMICOLON 0x33 +#define HID_KEY_APOSTROPHE 0x34 +#define HID_KEY_GRAVE 0x35 +#define HID_KEY_COMMA 0x36 +#define HID_KEY_PERIOD 0x37 +#define HID_KEY_SLASH 0x38 +#define HID_KEY_CAPS_LOCK 0x39 +#define HID_KEY_F1 0x3A +#define HID_KEY_F2 0x3B +#define HID_KEY_F3 0x3C +#define HID_KEY_F4 0x3D +#define HID_KEY_F5 0x3E +#define HID_KEY_F6 0x3F +#define HID_KEY_F7 0x40 +#define HID_KEY_F8 0x41 +#define HID_KEY_F9 0x42 +#define HID_KEY_F10 0x43 +#define HID_KEY_F11 0x44 +#define HID_KEY_F12 0x45 +#define HID_KEY_PRINT_SCREEN 0x46 +#define HID_KEY_SCROLL_LOCK 0x47 +#define HID_KEY_PAUSE 0x48 +#define HID_KEY_INSERT 0x49 +#define HID_KEY_HOME 0x4A +#define HID_KEY_PAGE_UP 0x4B +#define HID_KEY_DELETE 0x4C +#define HID_KEY_END 0x4D +#define HID_KEY_PAGE_DOWN 0x4E +#define HID_KEY_ARROW_RIGHT 0x4F +#define HID_KEY_ARROW_LEFT 0x50 +#define HID_KEY_ARROW_DOWN 0x51 +#define HID_KEY_ARROW_UP 0x52 +#define HID_KEY_NUM_LOCK 0x53 +#define HID_KEY_KEYPAD_DIVIDE 0x54 +#define HID_KEY_KEYPAD_MULTIPLY 0x55 +#define HID_KEY_KEYPAD_SUBTRACT 0x56 +#define HID_KEY_KEYPAD_ADD 0x57 +#define HID_KEY_KEYPAD_ENTER 0x58 +#define HID_KEY_KEYPAD_1 0x59 +#define HID_KEY_KEYPAD_2 0x5A +#define HID_KEY_KEYPAD_3 0x5B +#define HID_KEY_KEYPAD_4 0x5C +#define HID_KEY_KEYPAD_5 0x5D +#define HID_KEY_KEYPAD_6 0x5E +#define HID_KEY_KEYPAD_7 0x5F +#define HID_KEY_KEYPAD_8 0x60 +#define HID_KEY_KEYPAD_9 0x61 +#define HID_KEY_KEYPAD_0 0x62 +#define HID_KEY_KEYPAD_DECIMAL 0x63 +#define HID_KEY_EUROPE_2 0x64 +#define HID_KEY_APPLICATION 0x65 +#define HID_KEY_POWER 0x66 +#define HID_KEY_KEYPAD_EQUAL 0x67 +#define HID_KEY_F13 0x68 +#define HID_KEY_F14 0x69 +#define HID_KEY_F15 0x6A +#define HID_KEY_F16 0x6B +#define HID_KEY_F17 0x6C +#define HID_KEY_F18 0x6D +#define HID_KEY_F19 0x6E +#define HID_KEY_F20 0x6F +#define HID_KEY_F21 0x70 +#define HID_KEY_F22 0x71 +#define HID_KEY_F23 0x72 +#define HID_KEY_F24 0x73 +#define HID_KEY_EXECUTE 0x74 +#define HID_KEY_HELP 0x75 +#define HID_KEY_MENU 0x76 +#define HID_KEY_SELECT 0x77 +#define HID_KEY_STOP 0x78 +#define HID_KEY_AGAIN 0x79 +#define HID_KEY_UNDO 0x7A +#define HID_KEY_CUT 0x7B +#define HID_KEY_COPY 0x7C +#define HID_KEY_PASTE 0x7D +#define HID_KEY_FIND 0x7E +#define HID_KEY_MUTE 0x7F +#define HID_KEY_VOLUME_UP 0x80 +#define HID_KEY_VOLUME_DOWN 0x81 +#define HID_KEY_LOCKING_CAPS_LOCK 0x82 +#define HID_KEY_LOCKING_NUM_LOCK 0x83 +#define HID_KEY_LOCKING_SCROLL_LOCK 0x84 +#define HID_KEY_KEYPAD_COMMA 0x85 +#define HID_KEY_KEYPAD_EQUAL_SIGN 0x86 +#define HID_KEY_KANJI1 0x87 +#define HID_KEY_KANJI2 0x88 +#define HID_KEY_KANJI3 0x89 +#define HID_KEY_KANJI4 0x8A +#define HID_KEY_KANJI5 0x8B +#define HID_KEY_KANJI6 0x8C +#define HID_KEY_KANJI7 0x8D +#define HID_KEY_KANJI8 0x8E +#define HID_KEY_KANJI9 0x8F +#define HID_KEY_LANG1 0x90 +#define HID_KEY_LANG2 0x91 +#define HID_KEY_LANG3 0x92 +#define HID_KEY_LANG4 0x93 +#define HID_KEY_LANG5 0x94 +#define HID_KEY_LANG6 0x95 +#define HID_KEY_LANG7 0x96 +#define HID_KEY_LANG8 0x97 +#define HID_KEY_LANG9 0x98 +#define HID_KEY_ALTERNATE_ERASE 0x99 +#define HID_KEY_SYSREQ_ATTENTION 0x9A +#define HID_KEY_CANCEL 0x9B +#define HID_KEY_CLEAR 0x9C +#define HID_KEY_PRIOR 0x9D +#define HID_KEY_RETURN 0x9E +#define HID_KEY_SEPARATOR 0x9F +#define HID_KEY_OUT 0xA0 +#define HID_KEY_OPER 0xA1 +#define HID_KEY_CLEAR_AGAIN 0xA2 +#define HID_KEY_CRSEL_PROPS 0xA3 +#define HID_KEY_EXSEL 0xA4 +// RESERVED 0xA5-DF +#define HID_KEY_CONTROL_LEFT 0xE0 +#define HID_KEY_SHIFT_LEFT 0xE1 +#define HID_KEY_ALT_LEFT 0xE2 +#define HID_KEY_GUI_LEFT 0xE3 +#define HID_KEY_CONTROL_RIGHT 0xE4 +#define HID_KEY_SHIFT_RIGHT 0xE5 +#define HID_KEY_ALT_RIGHT 0xE6 +#define HID_KEY_GUI_RIGHT 0xE7 + +#endif \ No newline at end of file diff --git a/HAL/stm32/include/reboot.hpp b/HAL/stm32/include/reboot.hpp new file mode 100644 index 00000000..466f6cf3 --- /dev/null +++ b/HAL/stm32/include/reboot.hpp @@ -0,0 +1,7 @@ +#ifndef _REBOOT_HPP +#define _REBOOT_HPP + +void reboot_firmware(); +void reboot_bootloader(); + +#endif diff --git a/HAL/stm32/include/serial.hpp b/HAL/stm32/include/serial.hpp new file mode 100644 index 00000000..633edd07 --- /dev/null +++ b/HAL/stm32/include/serial.hpp @@ -0,0 +1,15 @@ +#ifndef _SERIAL_HPP +#define _SERIAL_HPP + +#include "stdlib.hpp" + +namespace serial { + void init(unsigned long baudrate); + void close(); + void print(const char *string); + void write(uint8_t byte); + void write(uint8_t *bytes, size_t len); + int available_for_write(); +} + +#endif diff --git a/HAL/stm32/include/stdlib.hpp b/HAL/stm32/include/stdlib.hpp new file mode 100644 index 00000000..98e6ba5d --- /dev/null +++ b/HAL/stm32/include/stdlib.hpp @@ -0,0 +1,8 @@ +#ifndef _HAL_STDLIB_HPP +#define _HAL_STDLIB_HPP + +#include + +typedef unsigned int uint; + +#endif diff --git a/HAL/stm32/include/util/state_util.hpp b/HAL/stm32/include/util/state_util.hpp new file mode 100644 index 00000000..65c01e0b --- /dev/null +++ b/HAL/stm32/include/util/state_util.hpp @@ -0,0 +1,479 @@ +#ifndef _UTIL_STATE_UTIL_HPP +#define _UTIL_STATE_UTIL_HPP + +#include "stdlib.hpp" + +#include + +typedef struct _ButtonState { + bool lf1 : 1; + bool lf2 : 1; + bool lf3 : 1; + bool lf4 : 1; + bool lf5 : 1; + bool lf6 : 1; + bool lf7 : 1; + bool lf8 : 1; + bool lf9 : 1; + bool lf10 : 1; + bool lf11 : 1; + bool lf12 : 1; + bool lf13 : 1; + bool lf14 : 1; + bool lf15 : 1; + bool lf16 : 1; + bool rf1 : 1; + bool rf2 : 1; + bool rf3 : 1; + bool rf4 : 1; + bool rf5 : 1; + bool rf6 : 1; + bool rf7 : 1; + bool rf8 : 1; + bool rf9 : 1; + bool rf10 : 1; + bool rf11 : 1; + bool rf12 : 1; + bool rf13 : 1; + bool rf14 : 1; + bool rf15 : 1; + bool rf16 : 1; + bool lt1 : 1; + bool lt2 : 1; + bool lt3 : 1; + bool lt4 : 1; + bool lt5 : 1; + bool lt6 : 1; + bool lt7 : 1; + bool lt8 : 1; + bool rt1 : 1; + bool rt2 : 1; + bool rt3 : 1; + bool rt4 : 1; + bool rt5 : 1; + bool rt6 : 1; + bool rt7 : 1; + bool rt8 : 1; + bool mb1 : 1; + bool mb2 : 1; + bool mb3 : 1; + bool mb4 : 1; + bool mb5 : 1; + bool mb6 : 1; + bool mb7 : 1; + bool mb8 : 1; + bool mb9 : 1; + bool mb10 : 1; + bool mb11 : 1; + bool mb12 : 1; +} ButtonState; + +inline void set_button(uint64_t &buttons, Button button_index, bool pressed) { + ButtonState &inputs = (ButtonState &)buttons; + if (button_index == BTN_UNSPECIFIED) { + return; + } + switch (button_index) { + case BTN_LF1: + inputs.lf1 = pressed; + break; + case BTN_LF2: + inputs.lf2 = pressed; + break; + case BTN_LF3: + inputs.lf3 = pressed; + break; + case BTN_LF4: + inputs.lf4 = pressed; + break; + case BTN_LF5: + inputs.lf5 = pressed; + break; + case BTN_LF6: + inputs.lf6 = pressed; + break; + case BTN_LF7: + inputs.lf7 = pressed; + break; + case BTN_LF8: + inputs.lf8 = pressed; + break; + case BTN_LF9: + inputs.lf9 = pressed; + break; + case BTN_LF10: + inputs.lf10 = pressed; + break; + case BTN_LF11: + inputs.lf11 = pressed; + break; + case BTN_LF12: + inputs.lf12 = pressed; + break; + case BTN_LF13: + inputs.lf13 = pressed; + break; + case BTN_LF14: + inputs.lf14 = pressed; + break; + case BTN_LF15: + inputs.lf15 = pressed; + break; + case BTN_LF16: + inputs.lf16 = pressed; + break; + case BTN_RF1: + inputs.rf1 = pressed; + break; + case BTN_RF2: + inputs.rf2 = pressed; + break; + case BTN_RF3: + inputs.rf3 = pressed; + break; + case BTN_RF4: + inputs.rf4 = pressed; + break; + case BTN_RF5: + inputs.rf5 = pressed; + break; + case BTN_RF6: + inputs.rf6 = pressed; + break; + case BTN_RF7: + inputs.rf7 = pressed; + break; + case BTN_RF8: + inputs.rf8 = pressed; + break; + case BTN_RF9: + inputs.rf9 = pressed; + break; + case BTN_RF10: + inputs.rf10 = pressed; + break; + case BTN_RF11: + inputs.rf11 = pressed; + break; + case BTN_RF12: + inputs.rf12 = pressed; + break; + case BTN_RF13: + inputs.rf13 = pressed; + break; + case BTN_RF14: + inputs.rf14 = pressed; + break; + case BTN_RF15: + inputs.rf15 = pressed; + break; + case BTN_RF16: + inputs.rf16 = pressed; + break; + case BTN_LT1: + inputs.lt1 = pressed; + break; + case BTN_LT2: + inputs.lt2 = pressed; + break; + case BTN_LT3: + inputs.lt3 = pressed; + break; + case BTN_LT4: + inputs.lt4 = pressed; + break; + case BTN_LT5: + inputs.lt5 = pressed; + break; + case BTN_LT6: + inputs.lt6 = pressed; + break; + case BTN_LT7: + inputs.lt7 = pressed; + break; + case BTN_LT8: + inputs.lt8 = pressed; + break; + case BTN_RT1: + inputs.rt1 = pressed; + break; + case BTN_RT2: + inputs.rt2 = pressed; + break; + case BTN_RT3: + inputs.rt3 = pressed; + break; + case BTN_RT4: + inputs.rt4 = pressed; + break; + case BTN_RT5: + inputs.rt5 = pressed; + break; + case BTN_RT6: + inputs.rt6 = pressed; + break; + case BTN_RT7: + inputs.rt7 = pressed; + break; + case BTN_RT8: + inputs.rt8 = pressed; + break; + case BTN_MB1: + inputs.mb1 = pressed; + break; + case BTN_MB2: + inputs.mb2 = pressed; + break; + case BTN_MB3: + inputs.mb3 = pressed; + break; + case BTN_MB4: + inputs.mb4 = pressed; + break; + case BTN_MB5: + inputs.mb5 = pressed; + break; + case BTN_MB6: + inputs.mb6 = pressed; + break; + case BTN_MB7: + inputs.mb7 = pressed; + break; + case BTN_MB8: + inputs.mb8 = pressed; + break; + case BTN_MB9: + inputs.mb9 = pressed; + break; + case BTN_MB10: + inputs.mb10 = pressed; + break; + case BTN_MB11: + inputs.mb11 = pressed; + break; + case BTN_MB12: + inputs.mb12 = pressed; + break; + default: + break; + } +} + +inline bool get_button(const uint64_t &buttons, Button button_index) { + ButtonState &inputs = (ButtonState &)buttons; + switch (button_index) { + case BTN_LF1: + return inputs.lf1; + case BTN_LF2: + return inputs.lf2; + case BTN_LF3: + return inputs.lf3; + case BTN_LF4: + return inputs.lf4; + case BTN_LF5: + return inputs.lf5; + case BTN_LF6: + return inputs.lf6; + case BTN_LF7: + return inputs.lf7; + case BTN_LF8: + return inputs.lf8; + case BTN_LF9: + return inputs.lf9; + case BTN_LF10: + return inputs.lf10; + case BTN_LF11: + return inputs.lf11; + case BTN_LF12: + return inputs.lf12; + case BTN_LF13: + return inputs.lf13; + case BTN_LF14: + return inputs.lf14; + case BTN_LF15: + return inputs.lf15; + case BTN_LF16: + return inputs.lf16; + case BTN_RF1: + return inputs.rf1; + case BTN_RF2: + return inputs.rf2; + case BTN_RF3: + return inputs.rf3; + case BTN_RF4: + return inputs.rf4; + case BTN_RF5: + return inputs.rf5; + case BTN_RF6: + return inputs.rf6; + case BTN_RF7: + return inputs.rf7; + case BTN_RF8: + return inputs.rf8; + case BTN_RF9: + return inputs.rf9; + case BTN_RF10: + return inputs.rf10; + case BTN_RF11: + return inputs.rf11; + case BTN_RF12: + return inputs.rf12; + case BTN_RF13: + return inputs.rf13; + case BTN_RF14: + return inputs.rf14; + case BTN_RF15: + return inputs.rf15; + case BTN_RF16: + return inputs.rf16; + case BTN_LT1: + return inputs.lt1; + case BTN_LT2: + return inputs.lt2; + case BTN_LT3: + return inputs.lt3; + case BTN_LT4: + return inputs.lt4; + case BTN_LT5: + return inputs.lt5; + case BTN_LT6: + return inputs.lt6; + case BTN_LT7: + return inputs.lt7; + case BTN_LT8: + return inputs.lt8; + case BTN_RT1: + return inputs.rt1; + case BTN_RT2: + return inputs.rt2; + case BTN_RT3: + return inputs.rt3; + case BTN_RT4: + return inputs.rt4; + case BTN_RT5: + return inputs.rt5; + case BTN_RT6: + return inputs.rt6; + case BTN_RT7: + return inputs.rt7; + case BTN_RT8: + return inputs.rt8; + case BTN_MB1: + return inputs.mb1; + case BTN_MB2: + return inputs.mb2; + case BTN_MB3: + return inputs.mb3; + case BTN_MB4: + return inputs.mb4; + case BTN_MB5: + return inputs.mb5; + case BTN_MB6: + return inputs.mb6; + case BTN_MB7: + return inputs.mb7; + case BTN_MB8: + return inputs.mb8; + case BTN_MB9: + return inputs.mb9; + case BTN_MB10: + return inputs.mb10; + case BTN_MB11: + return inputs.mb11; + case BTN_MB12: + return inputs.mb12; + default: + return false; + } +} + +inline uint64_t make_button_mask(const Button *buttons, size_t buttons_count) { + uint64_t button_mask = 0; + for (size_t j = 0; j < buttons_count; j++) { + button_mask |= (1ULL << (buttons[j] - 1)); + } + return button_mask; +} + +inline bool all_buttons_held(const uint64_t &buttons, uint64_t button_mask) { + return button_mask != 0 && (buttons & button_mask) == button_mask; +} + +inline bool any_button_held(const uint64_t &buttons, uint64_t button_mask) { + return button_mask != 0 && (buttons & button_mask); +} + +/* OutputState utils */ + +inline void set_output(uint32_t &buttons, DigitalOutput output_index, bool pressed) { + if (output_index == GP_UNSPECIFIED) { + return; + } + DigitalOutput output_index_adjusted = (DigitalOutput)(output_index - 1); + buttons = + (buttons & ~(1UL << output_index_adjusted)) | ((uint32_t)pressed << output_index_adjusted); +} + +inline uint8_t OutputState::*axis_pointer(AnalogAxis axis) { + switch (axis) { + case AXIS_LSTICK_X: + return &OutputState::leftStickX; + case AXIS_LSTICK_Y: + return &OutputState::leftStickY; + case AXIS_RSTICK_X: + return &OutputState::rightStickX; + case AXIS_RSTICK_Y: + return &OutputState::rightStickY; + case AXIS_LTRIGGER: + return &OutputState::triggerLAnalog; + case AXIS_RTRIGGER: + return &OutputState::triggerRAnalog; + default: + return nullptr; + } +} + +constexpr const char *digital_output_name(DigitalOutput output) { + switch (output) { + case GP_A: + return "A"; + case GP_B: + return "B"; + case GP_X: + return "X"; + case GP_Y: + return "Y"; + case GP_LB: + return "L1"; + case GP_RB: + return "R1"; + case GP_LT: + return "L2"; + case GP_RT: + return "R2"; + case GP_START: + return "Start"; + case GP_SELECT: + return "Select"; + case GP_HOME: + return "Home"; + case GP_CAPTURE: + return "Capture"; + case GP_DPAD_UP: + return "D-Pad Up"; + case GP_DPAD_DOWN: + return "D-Pad Down"; + case GP_DPAD_LEFT: + return "D-Pad Left"; + case GP_DPAD_RIGHT: + return "D-Pad Right"; + case GP_LSTICK_CLICK: + return "L3"; + case GP_RSTICK_CLICK: + return "R3"; + default: + return "Unknown"; + } +} + +#endif \ No newline at end of file diff --git a/HAL/stm32/proto/config.options b/HAL/stm32/proto/config.options new file mode 100644 index 00000000..de7a0a38 --- /dev/null +++ b/HAL/stm32/proto/config.options @@ -0,0 +1,99 @@ +ButtonRemap.physical_button int_size:IS_8 +ButtonRemap.activates int_size:IS_8 + +SocdPair.button_dir1 int_size:IS_8 +SocdPair.button_dir2 int_size:IS_8 +SocdPair.socd_type int_size:IS_8 + +AnalogTriggerMapping.button int_size:IS_8 +AnalogTriggerMapping.trigger int_size:IS_8 +AnalogTriggerMapping.value int_size:IS_8 + +AnalogModifier.buttons int_size:IS_8 +AnalogModifier.buttons max_count:3 +AnalogModifier.buttons type:FT_POINTER +AnalogModifier.axis int_size:IS_8 +AnalogModifier.combination_mode int_size:IS_8 + +ButtonComboMapping.buttons int_size:IS_8 +ButtonComboMapping.buttons max_count:3 +ButtonComboMapping.buttons type:FT_POINTER +ButtonComboMapping.digital_output int_size:IS_8 + +Coords.x int_size:IS_8 +Coords.y int_size:IS_8 + +ButtonToKeycodeMapping.button int_size:IS_8 +ButtonToKeycodeMapping.keycode int_size:IS_8 + +ButtonToColorMapping.button int_size:IS_8 + +GameModeConfig.mode_id int_size:IS_8 +GameModeConfig.name max_length:17 +GameModeConfig.name type:FT_POINTER +GameModeConfig.socd_pairs max_count:10 +GameModeConfig.socd_pairs type:FT_POINTER +GameModeConfig.button_remapping max_count:60 +GameModeConfig.button_remapping type:FT_POINTER +GameModeConfig.activation_binding max_count:4 +GameModeConfig.activation_binding type:FT_POINTER +GameModeConfig.custom_mode_config int_size:IS_8 +GameModeConfig.keyboard_mode_config int_size:IS_8 +GameModeConfig.rgb_config int_size:IS_8 + +CustomModeConfig.id int_size:IS_8 +CustomModeConfig.digital_button_mappings max_count:18 +CustomModeConfig.digital_button_mappings type:FT_POINTER +CustomModeConfig.stick_direction_mappings max_count:8 +CustomModeConfig.stick_direction_mappings type:FT_POINTER +CustomModeConfig.analog_trigger_mappings max_count:4 +CustomModeConfig.analog_trigger_mappings type:FT_POINTER +CustomModeConfig.button_combo_mappings max_count:5 +CustomModeConfig.button_combo_mappings type:FT_POINTER +CustomModeConfig.modifiers max_count:20 +CustomModeConfig.modifiers type:FT_POINTER +CustomModeConfig.stick_range int_size:IS_8 + +KeyboardModeConfig.id int_size:IS_8 +KeyboardModeConfig.buttons_to_keycodes max_count:60 +KeyboardModeConfig.buttons_to_keycodes type:FT_POINTER + +CommunicationBackendConfig.backend_id int_size:IS_8 +CommunicationBackendConfig.default_mode_config int_size:IS_8 +CommunicationBackendConfig.activation_binding max_count:2 +CommunicationBackendConfig.activation_binding type:FT_POINTER + +RgbConfig.button_colors max_count:60 +RgbConfig.button_colors type:FT_POINTER +RgbConfig.animation int_size:IS_8 +RgbConfig.speed int_size:IS_8 + +Config.game_mode_configs max_count:10 +Config.game_mode_configs type:FT_POINTER +Config.communication_backend_configs max_count:10 +Config.communication_backend_configs type:FT_POINTER +Config.custom_modes max_count:5 +Config.custom_modes type:FT_POINTER +Config.keyboard_modes max_count:5 +Config.keyboard_modes type:FT_POINTER +Config.rgb_configs max_count:10 +Config.rgb_configs type:FT_POINTER +Config.default_backend_config int_size:IS_8 +Config.default_usb_backend_config int_size:IS_8 +Config.rgb_brightness int_size:IS_8 + +DeviceInfo.firmware_name max_length:25 +DeviceInfo.firmware_version max_length:8 +DeviceInfo.device_name max_length:30 + +Command long_names:false +Button long_names:false +DigitalOutput long_names:false +StickDirectionButton long_names:false +AnalogAxis long_names:false +AnalogTrigger long_names:false +ModifierCombinationMode long_names:false +SocdType long_names:false +GameModeId long_names:false +CommunicationBackendId long_names:false +RgbAnimationId long_names:false \ No newline at end of file diff --git a/HAL/stm32/src/comms/XInputBackend.cpp b/HAL/stm32/src/comms/XInputBackend.cpp new file mode 100644 index 00000000..285d42e9 --- /dev/null +++ b/HAL/stm32/src/comms/XInputBackend.cpp @@ -0,0 +1,62 @@ +#include "comms/XInputBackend.hpp" + +#include "core/CommunicationBackend.hpp" +#include "core/state.hpp" + +#include + +XInputBackend::XInputBackend( + InputState &inputs, + InputSource **input_sources, + size_t input_source_count +) + : CommunicationBackend(inputs, input_sources, input_source_count), + _xbox360(0x0738, 0x4726) { + // Disconnect the maple core's default USB serial device first. + USBComposite.clear(); + _xbox360.registerComponent(); + USBComposite.begin(); + while (!USBComposite) + ; +} + +CommunicationBackendId XInputBackend::BackendId() { + return COMMS_BACKEND_XINPUT; +} + +void XInputBackend::SendReport() { + ScanInputs(InputScanSpeed::SLOW); + ScanInputs(InputScanSpeed::MEDIUM); + ScanInputs(InputScanSpeed::FAST); + + UpdateOutputs(); + + // Digital buttons + _xbox360.button(XBOX_A, _outputs.a); + _xbox360.button(XBOX_B, _outputs.b); + _xbox360.button(XBOX_X, _outputs.x); + _xbox360.button(XBOX_Y, _outputs.y); + _xbox360.button(XBOX_LSHOULDER, _outputs.buttonL); + _xbox360.button(XBOX_RSHOULDER, _outputs.buttonR); + _xbox360.button(XBOX_START, _outputs.start); + _xbox360.button(XBOX_BACK, _outputs.select); + _xbox360.button(XBOX_GUIDE, _outputs.home); + _xbox360.button(XBOX_DUP, _outputs.dpadUp); + _xbox360.button(XBOX_DDOWN, _outputs.dpadDown); + _xbox360.button(XBOX_DLEFT, _outputs.dpadLeft); + _xbox360.button(XBOX_DRIGHT, _outputs.dpadRight); + _xbox360.button(XBOX_L3, _outputs.leftStickClick); + _xbox360.button(XBOX_R3, _outputs.rightStickClick); + + // Triggers + _xbox360.sliderLeft(_outputs.triggerLDigital ? 255 : _outputs.triggerLAnalog); + _xbox360.sliderRight(_outputs.triggerRDigital ? 255 : _outputs.triggerRAnalog); + + // Analog sticks (convert 0-255 range to -32768..32767) + _xbox360.X((_outputs.leftStickX - 128) * 65535 / 255 + 128); + _xbox360.Y((_outputs.leftStickY - 128) * 65535 / 255 + 128); + _xbox360.XRight((_outputs.rightStickX - 128) * 65535 / 255 + 128); + _xbox360.YRight((_outputs.rightStickY - 128) * 65535 / 255 + 128); + + _xbox360.send(); +} diff --git a/HAL/stm32/src/comms/backend_init.cpp b/HAL/stm32/src/comms/backend_init.cpp new file mode 100644 index 00000000..15328d5c --- /dev/null +++ b/HAL/stm32/src/comms/backend_init.cpp @@ -0,0 +1,161 @@ +#include "comms/backend_init.hpp" + +#include "comms/XInputBackend.hpp" +#include "core/CommunicationBackend.hpp" +#include "core/mode_selection.hpp" +#include "core/pinout.hpp" +#include "util/config_util.hpp" + +#include + +size_t initialize_backends( + CommunicationBackend **&backends, + InputState &inputs, + InputSource **input_sources, + size_t input_source_count, + Config &config, + const Pinout &pinout, + backend_config_selector_t get_backend_config, + usb_backend_getter_t get_usb_backend_config, + detect_console_t detect_console, + secondary_backend_initializer_t init_secondary_backends, + primary_backend_initializer_t init_primary_backend +) { + if (get_backend_config == nullptr || get_usb_backend_config == nullptr || + init_primary_backend == nullptr || detect_console == nullptr) { + return 0; + } + + CommunicationBackendConfig backend_config = CommunicationBackendConfig_init_zero; + get_backend_config(backend_config, inputs, config); + + CommunicationBackend *primary_backend = nullptr; + + if (backend_config.backend_id == COMMS_BACKEND_UNSPECIFIED) { + CommunicationBackendConfig usb_backend_config; + get_usb_backend_config(usb_backend_config, config); + init_primary_backend( + primary_backend, + usb_backend_config.backend_id, + inputs, + input_sources, + input_source_count, + config, + pinout + ); + CommunicationBackendId detected_backend_id = detect_console(pinout); + if (detected_backend_id == COMMS_BACKEND_XINPUT) { + backend_config = usb_backend_config; + } else { + backend_config = backend_config_from_id( + detected_backend_id, + config.communication_backend_configs, + config.communication_backend_configs_count + ); + } + } + if (backend_config.backend_id == COMMS_BACKEND_UNSPECIFIED && + config.default_backend_config > 0) { + backend_config = config.communication_backend_configs[config.default_backend_config - 1]; + } + + init_primary_backend( + primary_backend, + backend_config.backend_id, + inputs, + input_sources, + input_source_count, + config, + pinout + ); + + size_t backend_count = 1; + if (init_secondary_backends != nullptr) { + backend_count = init_secondary_backends( + backends, + primary_backend, + backend_config.backend_id, + inputs, + input_sources, + input_source_count, + config, + pinout + ); + } + + if (backend_config.default_mode_config > 0) { + GameModeConfig &mode_config = + config.game_mode_configs[backend_config.default_mode_config - 1]; + for (size_t i = 0; i < backend_count; i++) { + set_mode(backends[i], mode_config, config); + } + } + + return backend_count; +} + +void init_primary_backend( + CommunicationBackend *&primary_backend, + CommunicationBackendId backend_id, + InputState &inputs, + InputSource **input_sources, + size_t input_source_count, + Config &config, + const Pinout &pinout +) { + switch (backend_id) { + case COMMS_BACKEND_UNSPECIFIED: + case COMMS_BACKEND_XINPUT: + default: + if (primary_backend == nullptr) { + primary_backend = + new XInputBackend(inputs, input_sources, input_source_count); + } + break; + } +} + +size_t init_secondary_backends( + CommunicationBackend **&backends, + CommunicationBackend *&primary_backend, + CommunicationBackendId backend_id, + InputState &inputs, + InputSource **input_sources, + size_t input_source_count, + Config &config, + const Pinout &pinout +) { + size_t backend_count = 1; + backends = new CommunicationBackend *[backend_count] { primary_backend }; + return backend_count; +} + +// clang-format off + +backend_config_selector_t get_backend_config_default = []( + CommunicationBackendConfig &backend_config, + const InputState &inputs, + Config &config +) { + backend_config = backend_config_from_buttons( + inputs, + config.communication_backend_configs, + config.communication_backend_configs_count + ); +}; + +usb_backend_getter_t get_usb_backend_config_default = []( + CommunicationBackendConfig &backend_config, + const Config &config +) { + if (config.default_usb_backend_config > 0 && + config.default_usb_backend_config <= config.communication_backend_configs_count) { + backend_config = + config.communication_backend_configs[config.default_usb_backend_config - 1]; + } +}; + +// clang-format on + +primary_backend_initializer_t init_primary_backend_default = &init_primary_backend; +secondary_backend_initializer_t init_secondary_backends_default = &init_secondary_backends; diff --git a/HAL/stm32/src/comms/console_detection.cpp b/HAL/stm32/src/comms/console_detection.cpp new file mode 100644 index 00000000..bc59615f --- /dev/null +++ b/HAL/stm32/src/comms/console_detection.cpp @@ -0,0 +1,11 @@ +#include "comms/console_detection.hpp" + +#include "core/pinout.hpp" +#include "stdlib.hpp" + +#include + +CommunicationBackendId detect_console(const Pinout &pinout) { + // STM32 is always USB — return XInput. + return COMMS_BACKEND_XINPUT; +} diff --git a/HAL/stm32/src/core/KeyboardMode.cpp b/HAL/stm32/src/core/KeyboardMode.cpp new file mode 100644 index 00000000..1e0045de --- /dev/null +++ b/HAL/stm32/src/core/KeyboardMode.cpp @@ -0,0 +1,16 @@ +#include "core/KeyboardMode.hpp" + +#include "core/InputMode.hpp" + +KeyboardMode::KeyboardMode() : InputMode() {} + +KeyboardMode::~KeyboardMode() {} + +void KeyboardMode::SendReport(const InputState &inputs) { + // Keyboard mode is not yet supported on STM32. + // TODO: Implement using USBComposite HID keyboard. +} + +void KeyboardMode::Press(uint8_t keycode, bool press) { + // Stub — no keyboard support yet. +} diff --git a/HAL/stm32/src/core/mode_selection.cpp b/HAL/stm32/src/core/mode_selection.cpp new file mode 100644 index 00000000..cd8af66f --- /dev/null +++ b/HAL/stm32/src/core/mode_selection.cpp @@ -0,0 +1,111 @@ +#include "core/mode_selection.hpp" + +#include "core/state.hpp" +#include "modes/CustomControllerMode.hpp" +#include "modes/CustomKeyboardMode.hpp" +#include "modes/FgcMode.hpp" +#include "modes/Melee20Button.hpp" +#include "modes/ProjectM.hpp" +#include "modes/RivalsOfAether.hpp" +#include "modes/Ultimate.hpp" +#include "util/state_util.hpp" + +#include + +Melee20Button melee_mode; +ProjectM projectm_mode; +Ultimate ultimate_mode; +FgcMode fgc_mode; +RivalsOfAether rivals_mode; +CustomKeyboardMode keyboard_mode; +CustomControllerMode custom_mode; + +uint64_t mode_activation_masks[10]; + +size_t current_mode_index = SIZE_MAX; + +void set_mode(CommunicationBackend *backend, ControllerMode *mode) { + current_kb_mode = nullptr; + backend->SetGameMode(mode); +} + +void set_mode(CommunicationBackend *backend, KeyboardMode *mode) { + // Keyboard mode not supported on STM32 XInput backend. + return; +} + +void set_mode(CommunicationBackend *backend, GameModeConfig &mode_config, Config &config) { + switch (mode_config.mode_id) { + case MODE_MELEE: + melee_mode.SetConfig(mode_config, config.melee_options); + set_mode(backend, &melee_mode); + break; + case MODE_PROJECT_M: + projectm_mode.SetConfig(mode_config, config.project_m_options); + set_mode(backend, &projectm_mode); + break; + case MODE_ULTIMATE: + ultimate_mode.SetConfig(mode_config); + set_mode(backend, &ultimate_mode); + break; + case MODE_FGC: + fgc_mode.SetConfig(mode_config); + set_mode(backend, &fgc_mode); + break; + case MODE_RIVALS_OF_AETHER: + rivals_mode.SetConfig(mode_config); + set_mode(backend, &rivals_mode); + break; + case MODE_KEYBOARD: + // Not supported on STM32. + break; + case MODE_CUSTOM: + if (mode_config.custom_mode_config < 1 || + mode_config.custom_mode_config > config.custom_modes_count) { + break; + } + custom_mode.SetConfig( + mode_config, + config.custom_modes[mode_config.custom_mode_config - 1] + ); + set_mode(backend, &custom_mode); + break; + case MODE_UNSPECIFIED: + default: + break; + } +} + +void set_mode(CommunicationBackend *backend, GameModeId mode_id, Config &config) { + for (size_t i = 0; i < config.game_mode_configs_count; i++) { + GameModeConfig &mode = config.game_mode_configs[i]; + if (mode.mode_id == mode_id) { + set_mode(backend, mode, config); + return; + } + } +} + +void select_mode(CommunicationBackend **backends, size_t backends_count, Config &config) { + InputState &inputs = backends[0]->GetInputs(); + + for (size_t i = 0; i < config.game_mode_configs_count; i++) { + GameModeConfig &mode_config = config.game_mode_configs[i]; + if (all_buttons_held(inputs.buttons, mode_activation_masks[i]) && i != current_mode_index) { + current_mode_index = i; + for (size_t i = 0; i < backends_count; i++) { + set_mode(backends[i], mode_config, config); + } + return; + } + } +} + +void setup_mode_activation_bindings(const GameModeConfig *mode_configs, size_t mode_configs_count) { + for (size_t i = 0; i < mode_configs_count; i++) { + mode_activation_masks[i] = make_button_mask( + mode_configs[i].activation_binding, + mode_configs[i].activation_binding_count + ); + } +} diff --git a/HAL/stm32/src/gpio.cpp b/HAL/stm32/src/gpio.cpp new file mode 100644 index 00000000..7534f120 --- /dev/null +++ b/HAL/stm32/src/gpio.cpp @@ -0,0 +1,17 @@ +#include "gpio.hpp" + +#include "stdlib.hpp" + +namespace gpio { + void init_pin(uint pin, GpioMode mode) { + if (mode == GpioMode::GPIO_OUTPUT) { + pinMode(pin, OUTPUT); + } else if (mode == GpioMode::GPIO_INPUT_PULLUP) { + pinMode(pin, INPUT_PULLUP); + } else if (mode == GpioMode::GPIO_INPUT_PULLDOWN) { + pinMode(pin, INPUT_PULLDOWN); + } else if (mode == GpioMode::GPIO_INPUT) { + pinMode(pin, INPUT); + } + } +} diff --git a/HAL/stm32/src/reboot.cpp b/HAL/stm32/src/reboot.cpp new file mode 100644 index 00000000..3c8ac532 --- /dev/null +++ b/HAL/stm32/src/reboot.cpp @@ -0,0 +1,15 @@ +#include "reboot.hpp" + +#include "stdlib.hpp" + +#include + +void reboot_firmware() { + nvic_sys_reset(); +} + +void reboot_bootloader() { + // TODO: Set magic word at end of RAM to enter system bootloader after reset. + // For now, just reset into firmware. + nvic_sys_reset(); +} diff --git a/HAL/stm32/src/serial.cpp b/HAL/stm32/src/serial.cpp new file mode 100644 index 00000000..0a9f81cc --- /dev/null +++ b/HAL/stm32/src/serial.cpp @@ -0,0 +1,29 @@ +#include "serial.hpp" + +#include "stdlib.hpp" + +namespace serial { + void init(unsigned long baudrate) { + Serial.begin(baudrate); + } + + void close() { + Serial.end(); + } + + void print(const char *string) { + Serial.print(string); + } + + void write(uint8_t byte) { + Serial.write(byte); + } + + void write(uint8_t *bytes, size_t len) { + Serial.write(bytes, len); + } + + int available_for_write() { + return Serial.availableForWrite(); + } +} diff --git a/config/stm32pintester/config.cpp b/config/stm32pintester/config.cpp new file mode 100644 index 00000000..20ed7a23 --- /dev/null +++ b/config/stm32pintester/config.cpp @@ -0,0 +1,128 @@ +#include "comms/backend_init.hpp" +#include "config_defaults.hpp" +#include "core/CommunicationBackend.hpp" +#include "core/KeyboardMode.hpp" +#include "core/mode_selection.hpp" +#include "core/pinout.hpp" +#include "core/state.hpp" +#include "input/GpioButtonInput.hpp" +#include "reboot.hpp" +#include "stdlib.hpp" + +#include + +Config config = default_config; + +// Button-to-pin mapping from stm32pintester Buttons.md. +// B0XX-style leverless layout — adjust after physical testing. +// +// Left hand (movement): +// B1(PC0) B2(PC2) B3(PB8) +// B4(PD2) B5(PB11) B6(PA2) +// B7(PA0) B8(PB15) B9(PB13) B10(PC5) +// B11(PB15) B12(PB11) +// +// Right hand (attacks): +// B13(PB0) B14(PC8) B15(PC9) B16(PC10) B17(PC11) +// B18(PB9) B19(PB7) B20(PB5) B21(PC3) +// B22(PC12) B23(PA15) B24(PC6) B25(PB14) +// B26(PC7) B27(PB4) +// +// Note: B5/B12 share PB11, B8/B11 share PB15 — only 25 unique pins for 27 buttons. +// Shared pins are mapped once (the first occurrence). + +const GpioButtonMapping button_mappings[] = { + // Left hand - movement + { BTN_LF4, PC0 }, // B1 - Left + { BTN_LF3, PA0 }, // B7 - Down + { BTN_LF2, PC2 }, // B2 - Up + { BTN_LF1, PB8 }, // B3 - Right + + // Left hand - thumbs + { BTN_LT1, PB13 }, // B9 - ModX + { BTN_LT2, PC5 }, // B10 - ModY + + // Left hand - extra + { BTN_MB3, PD2 }, // B4 + { BTN_MB1, PB11 }, // B5 (shared with B12) + { BTN_MB2, PA2 }, // B6 + + // Right hand - top row + { BTN_RT3, PC8 }, // B14 + { BTN_RT4, PC9 }, // B15 + { BTN_RT2, PC10 }, // B16 + { BTN_RT1, PC11 }, // B17 + { BTN_RT5, PB15 }, // B8 (shared with B11) + + // Right hand - attack buttons + { BTN_RF1, PB9 }, // B18 - A (B) + { BTN_RF2, PB7 }, // B19 - B (A) + { BTN_RF3, PB5 }, // B20 - X (Y) + { BTN_RF4, PC3 }, // B21 - Y (X) + + // Right hand - bottom rows + { BTN_RF5, PB0 }, // B13 + { BTN_RF6, PC12 }, // B22 + { BTN_RF7, PA15 }, // B23 + { BTN_RF8, PC6 }, // B24 + + // Remaining buttons + { BTN_RF9, PB14 }, // B25 + { BTN_RF10, PC7 }, // B26 + { BTN_RF11, PB4 }, // B27 +}; +const size_t button_count = sizeof(button_mappings) / sizeof(GpioButtonMapping); + +const Pinout pinout = { + .joybus_data = 0, + .nes_data = -1, + .nes_clock = -1, + .nes_latch = -1, + .mux = -1, + .nunchuk_detect = -1, + .nunchuk_sda = -1, + .nunchuk_scl = -1, +}; + +CommunicationBackend **backends = nullptr; +size_t backend_count; +KeyboardMode *current_kb_mode = nullptr; + +void setup() { + // Free PA15, PB3, PB4 from JTAG for GPIO use (keep SWD on PA13/PA14). + afio_cfg_debug_ports(AFIO_DEBUG_SW_ONLY); + + // PA8 is the matrix row driver — drive LOW to activate the diode-to-ground matrix. + pinMode(PA8, OUTPUT); + digitalWrite(PA8, LOW); + + static InputState inputs; + + static GpioButtonInput gpio_input(button_mappings, button_count); + gpio_input.UpdateInputs(inputs); + + // Check bootloader button hold as early as possible. + if (inputs.rt2) { + reboot_bootloader(); + } + + static InputSource *input_sources[] = { &gpio_input }; + size_t input_source_count = sizeof(input_sources) / sizeof(InputSource *); + + backend_count = + initialize_backends(backends, inputs, input_sources, input_source_count, config, pinout); + + setup_mode_activation_bindings(config.game_mode_configs, config.game_mode_configs_count); +} + +void loop() { + select_mode(backends, backend_count, config); + + for (size_t i = 0; i < backend_count; i++) { + backends[i]->SendReport(); + } + + if (current_kb_mode != nullptr) { + current_kb_mode->SendReport(backends[0]->GetInputs()); + } +} diff --git a/config/stm32pintester/env.ini b/config/stm32pintester/env.ini new file mode 100644 index 00000000..ade109d6 --- /dev/null +++ b/config/stm32pintester/env.ini @@ -0,0 +1,13 @@ +[env:stm32pintester] +extends = stm32_base +; Actual MCU is F103RFT6 (XL-density, 768KB), but maple core only supports up to +; F103RE (high-density, 512KB). Same 64-pin LQFP pinout; firmware fits in 512KB. +board = genericSTM32F103RE +board_upload.offset_address = 0x08001000 +board_build.ldscript = stm32f103re_bootloader.ld +extra_scripts = post:config/stm32pintester/override_vect.py +build_flags = + ${stm32_base.build_flags} +build_src_filter = + ${stm32_base.build_src_filter} + + diff --git a/config/stm32pintester/override_vect.py b/config/stm32pintester/override_vect.py new file mode 100644 index 00000000..5ebe00cc --- /dev/null +++ b/config/stm32pintester/override_vect.py @@ -0,0 +1,19 @@ +Import("env") +import os + +# Replace the default VECT_TAB_ADDR=0x8000000 (set by maple build script) +# with 0x08001000 for our custom bootloader offset. +cppdefines = env.get("CPPDEFINES", []) +new_defines = [] +for d in cppdefines: + if isinstance(d, tuple) and d[0] == "VECT_TAB_ADDR": + new_defines.append(("VECT_TAB_ADDR", "0x08001000")) + elif isinstance(d, list) and len(d) == 2 and d[0] == "VECT_TAB_ADDR": + new_defines.append(["VECT_TAB_ADDR", "0x08001000"]) + else: + new_defines.append(d) +env.Replace(CPPDEFINES=new_defines) + +# Add our config dir to linker search path for the custom .ld file +ld_dir = os.path.join(env["PROJECT_DIR"], "config", "stm32pintester") +env.Prepend(LIBPATH=[ld_dir]) diff --git a/config/stm32pintester/stm32f103re_bootloader.ld b/config/stm32pintester/stm32f103re_bootloader.ld new file mode 100644 index 00000000..b34ccd8d --- /dev/null +++ b/config/stm32pintester/stm32f103re_bootloader.ld @@ -0,0 +1,18 @@ +/* + * Linker script for STM32F103RE with 4KB custom bootloader. + * App starts at 0x08001000 (4KB offset from flash start). + */ +MEMORY +{ + ram (rwx) : ORIGIN = 0x20000000, LENGTH = 64K + rom (rx) : ORIGIN = 0x08001000, LENGTH = 508K +} + +/* Provide memory region aliases for common.inc */ +REGION_ALIAS("REGION_TEXT", rom); +REGION_ALIAS("REGION_DATA", ram); +REGION_ALIAS("REGION_BSS", ram); +REGION_ALIAS("REGION_RODATA", rom); + +/* Let common.inc handle the real work. */ +INCLUDE common.inc diff --git a/platformio.ini b/platformio.ini index 1b7f820f..1584b426 100644 --- a/platformio.ini +++ b/platformio.ini @@ -81,6 +81,38 @@ lib_deps = https://github.com/JonnyHaystack/ArduinoJoystickLibrary/archive/refs/tags/v0.0.1.zip https://github.com/JonnyHaystack/ArduinoKeyboard/archive/refs/tags/1.0.5.zip +[stm32_base] +platform = ststm32 +framework = arduino +board_build.core = maple +build_unflags = + -std=gnu++11 + -DSERIAL_USB + -DGENERIC_BOOTLOADER +build_flags = + ${env.build_flags} + -std=gnu++17 + -Os + -USERIAL_USB + -UGENERIC_BOOTLOADER + -fdata-sections + -ffunction-sections + -fno-sized-deallocation + -Wl,--gc-sections + -Wno-unused-variable + -I HAL/stm32/include +build_src_filter = + ${env.build_src_filter} + + + - + - +custom_nanopb_options = + ${env.custom_nanopb_options} + --options-file ../../../../HAL/stm32/proto/config.options +lib_deps = + ${env.lib_deps} + arpruss/USBComposite for STM32F1 + [arduino_pico_base] platform = https://github.com/maxgerhardt/platform-raspberrypi.git#5e87ae34ca025274df25b3303e9e9cb6c120123c framework = arduino diff --git a/src/util/analog_filters.cpp b/src/util/analog_filters.cpp index 74e619df..4108d919 100644 --- a/src/util/analog_filters.cpp +++ b/src/util/analog_filters.cpp @@ -14,7 +14,7 @@ uint8_t apply_deadzone(uint8_t value, uint8_t deadzone, bool scale) { // value is given on the rim. if (scale) { int8_t sign = SIGNUM(post_deadzone); - int8_t post_scaling = min(127, abs(post_deadzone) * 128.0 / (128 - deadzone)) * sign; + int8_t post_scaling = min(127, (int)(abs(post_deadzone) * 128.0 / (128 - deadzone))) * sign; return post_scaling + 128; } return post_deadzone + 128; From cef444a46cb87c221c24d093f70081146b576505 Mon Sep 17 00:00:00 2001 From: Jesse Zhao Date: Wed, 11 Mar 2026 15:26:54 -0700 Subject: [PATCH 02/10] Support DFU bootloader for stm32pintester Implement reboot_bootloader() via RAM magic (0xDEADBEEFCC00FFEE) for davidgfnet DFU bootloader. Fix RAM to 96KB with 8-byte reservation for reboot flags. Add dfu-util upload command to env.ini. --- HAL/stm32/src/reboot.cpp | 8 ++++++-- config/stm32pintester/env.ini | 4 ++++ config/stm32pintester/stm32f103re_bootloader.ld | 5 +++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/HAL/stm32/src/reboot.cpp b/HAL/stm32/src/reboot.cpp index 3c8ac532..4304f0c7 100644 --- a/HAL/stm32/src/reboot.cpp +++ b/HAL/stm32/src/reboot.cpp @@ -9,7 +9,11 @@ void reboot_firmware() { } void reboot_bootloader() { - // TODO: Set magic word at end of RAM to enter system bootloader after reset. - // For now, just reset into firmware. + // Write magic to top of RAM to signal DFU bootloader entry on reset. + // Linker reserves last 8 bytes (LENGTH = 96K - 8), so __msp_init points there. + // Compatible with davidgfnet/stm32-dfu-bootloader reboot protocol. + extern uint32_t __msp_init; + volatile uint64_t *magic = (volatile uint64_t *)&__msp_init; + *magic = 0xDEADBEEFCC00FFEEULL; nvic_sys_reset(); } diff --git a/config/stm32pintester/env.ini b/config/stm32pintester/env.ini index ade109d6..976a5441 100644 --- a/config/stm32pintester/env.ini +++ b/config/stm32pintester/env.ini @@ -6,6 +6,10 @@ board = genericSTM32F103RE board_upload.offset_address = 0x08001000 board_build.ldscript = stm32f103re_bootloader.ld extra_scripts = post:config/stm32pintester/override_vect.py +; DFU bootloader on PA11/PA12: hold B1 + reset to enter DFU mode, then: +; dfu-util -a 0 -s 0x08001000:leave -D .pio/build/stm32pintester/firmware.bin +upload_protocol = dfu +upload_command = dfu-util -a 0 -s 0x08001000:leave -D $SOURCE build_flags = ${stm32_base.build_flags} build_src_filter = diff --git a/config/stm32pintester/stm32f103re_bootloader.ld b/config/stm32pintester/stm32f103re_bootloader.ld index b34ccd8d..374fbc6f 100644 --- a/config/stm32pintester/stm32f103re_bootloader.ld +++ b/config/stm32pintester/stm32f103re_bootloader.ld @@ -1,10 +1,11 @@ /* - * Linker script for STM32F103RE with 4KB custom bootloader. + * Linker script for STM32F103RF with 4KB DFU bootloader. * App starts at 0x08001000 (4KB offset from flash start). + * RAM: 96KB, last 8 bytes reserved for bootloader reboot magic. */ MEMORY { - ram (rwx) : ORIGIN = 0x20000000, LENGTH = 64K + ram (rwx) : ORIGIN = 0x20000000, LENGTH = 96K - 8 rom (rx) : ORIGIN = 0x08001000, LENGTH = 508K } From 68449614588af9c47d8713ef508cd430b4e47c90 Mon Sep 17 00:00:00 2001 From: Jesse Zhao Date: Wed, 11 Mar 2026 15:30:33 -0700 Subject: [PATCH 03/10] Fix RAM size report to 96KB for F103RF --- config/stm32pintester/env.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/config/stm32pintester/env.ini b/config/stm32pintester/env.ini index 976a5441..272626ba 100644 --- a/config/stm32pintester/env.ini +++ b/config/stm32pintester/env.ini @@ -3,6 +3,7 @@ extends = stm32_base ; Actual MCU is F103RFT6 (XL-density, 768KB), but maple core only supports up to ; F103RE (high-density, 512KB). Same 64-pin LQFP pinout; firmware fits in 512KB. board = genericSTM32F103RE +board_upload.maximum_ram_size = 98304 board_upload.offset_address = 0x08001000 board_build.ldscript = stm32f103re_bootloader.ld extra_scripts = post:config/stm32pintester/override_vect.py From a0ec7d5d5e14a06f6a8b952f61004f8ff20baf8d Mon Sep 17 00:00:00 2001 From: Jesse Zhao Date: Thu, 12 Mar 2026 11:32:16 -0700 Subject: [PATCH 04/10] Rewrite jz-stm32-leverless with verified pin mappings and PC12 fix - Rename config from stm32pintester to jz-stm32-leverless - Rewrite button_mappings[] with 17 active pins verified via pintester (12 of 27 original pin assignments were wrong) - Fix PC12 (A button) stuck/unresponsive: maple framework was using PC12 as USB disconnect pin. Override with -DBOARD_USB_DISC_DEV=NULL - Add SOCD toggle combo (Start + Select + DPadUp cycles SOCD modes) - Add ButtonMappings.md with full mapping table - Set default mode to FGC (default_mode_config = 4) --- HAL/stm32/include/config_defaults.hpp | 2 +- config/jz-stm32-leverless/ButtonMappings.md | 66 ++++++++ config/jz-stm32-leverless/config.cpp | 146 ++++++++++++++++++ .../env.ini | 9 +- .../override_vect.py | 2 +- .../stm32f103re_bootloader.ld | 0 config/stm32pintester/config.cpp | 128 --------------- 7 files changed, 219 insertions(+), 134 deletions(-) create mode 100644 config/jz-stm32-leverless/ButtonMappings.md create mode 100644 config/jz-stm32-leverless/config.cpp rename config/{stm32pintester => jz-stm32-leverless}/env.ini (71%) rename config/{stm32pintester => jz-stm32-leverless}/override_vect.py (89%) rename config/{stm32pintester => jz-stm32-leverless}/stm32f103re_bootloader.ld (100%) delete mode 100644 config/stm32pintester/config.cpp diff --git a/HAL/stm32/include/config_defaults.hpp b/HAL/stm32/include/config_defaults.hpp index f45e6614..359d3f0d 100644 --- a/HAL/stm32/include/config_defaults.hpp +++ b/HAL/stm32/include/config_defaults.hpp @@ -85,7 +85,7 @@ const Config default_config = { .communication_backend_configs = new CommunicationBackendConfig[1] { CommunicationBackendConfig { .backend_id = COMMS_BACKEND_XINPUT, - .default_mode_config = 1, + .default_mode_config = 4, .activation_binding_count = 0, .activation_binding = {}, .secondary_backends = {}, diff --git a/config/jz-stm32-leverless/ButtonMappings.md b/config/jz-stm32-leverless/ButtonMappings.md new file mode 100644 index 00000000..7cdccffe --- /dev/null +++ b/config/jz-stm32-leverless/ButtonMappings.md @@ -0,0 +1,66 @@ +# Button Mappings (FGC Mode / XInput) + +Pin assignments verified via stm32pintester 2026-03-12. + +## Physical Layout + +Left hand (movement + utility): +``` + B1(PC0) B2(PC2) B3(PA4) + B4(PD2) B5(PA7) B6(PB7) + B7(PB9) B8(PB11) B9(PB1) B10(PC5) +``` + +Right hand (attacks): +``` + B13(PB12) B14(PC8) B15(PC9) B16(PC10) B17(PC11) + B18(PA5) B19(PA3) B20(PA1) B21(PC3) + B22(PC12) B23(PB13) B24(PC6) B25(PB14) + B26(PC7) B27(PA0) +``` + +Shared pins: B5/B12 share PA7 (26 unique pins, 27 buttons). +B8/B11 are NOT shared — B8=PB11, B11=PB15. + +## Button-to-XInput Mapping + +| Button | Pin | BTN_* | XInput | Notes | +|--------|------|----------|------------|--------------------------------| +| B1 | PC0 | BTN_MB1 | Start | | +| B2 | PC2 | BTN_RT3 | Select | | +| B3 | PA4 | BTN_RT2 | Home | Also bootloader entry | +| B4 | PD2 | — | — | Unmapped | +| B5 | PA7 | — | — | Unmapped (shared with B12) | +| B6 | PB7 | — | — | Unmapped | +| B7 | PB9 | BTN_LT2 | L3 | | +| B8 | PB11 | BTN_LF3 | DPad Left | | +| B9 | PB1 | BTN_LF2 | DPad Down | | +| B10 | PC5 | BTN_LF1 | DPad Right | | +| B11 | PB15 | — | — | Unmapped | +| B12 | PA7 | — | — | Shared pin with B5, both unmapped | +| B13 | PB12 | — | — | Unmapped | +| B14 | PC8 | — | — | Unmapped | +| B15 | PC9 | — | — | Unmapped | +| B16 | PC10 | — | — | Unmapped | +| B17 | PC11 | — | — | Unmapped | +| B18 | PA5 | BTN_RF5 | X | | +| B19 | PA3 | BTN_RF6 | Y | | +| B20 | PA1 | BTN_RF7 | RB | | +| B21 | PC3 | BTN_RF8 | LB | | +| B22 | PC12 | BTN_RF1 | A | | +| B23 | PB13 | BTN_RF2 | B | | +| B24 | PC6 | BTN_RF3 | RT | | +| B25 | PB14 | BTN_RF4 | LT | | +| B26 | PC7 | BTN_LT1 | DPad Up | | +| B27 | PA0 | BTN_RT1 | R3 | | + +## Unmapped Buttons (Available for Future Use) + +B4 (PD2), B5/B12 (PA7), B6 (PB7), B11 (PB15), B13 (PB12), B14 (PC8), B15 (PC9), B16 (PC10), B17 (PC11) + +These 9 unique pins (10 buttons) are not mapped to any BTN_* value. + +## SOCD Toggle + +Hold Start (B1) + Select (B2) + DPad Up (B26) to cycle SOCD modes: +SOCD_NEUTRAL → SOCD_2IP → SOCD_2IP_NO_REAC → SOCD_NEUTRAL diff --git a/config/jz-stm32-leverless/config.cpp b/config/jz-stm32-leverless/config.cpp new file mode 100644 index 00000000..8ea5c140 --- /dev/null +++ b/config/jz-stm32-leverless/config.cpp @@ -0,0 +1,146 @@ +#include "comms/backend_init.hpp" +#include "config_defaults.hpp" +#include "core/CommunicationBackend.hpp" +#include "core/KeyboardMode.hpp" +#include "core/mode_selection.hpp" +#include "core/pinout.hpp" +#include "core/state.hpp" +#include "input/GpioButtonInput.hpp" +#include "reboot.hpp" +#include "stdlib.hpp" + +#include + +Config config = default_config; + +// Button-to-pin mapping for jz-stm32-leverless (FGC mode). +// Pin assignments verified via stm32pintester 2026-03-12. +// +// Physical layout: +// Left hand: Right hand: +// B1(Start) B2(Sel) B3(Home) B18(X) B19(Y) B20(RB) B21(LB) +// B4 B5 B6 B22(A) B23(B) B24(RT) B25(LT) +// B7(L3) B8(Left) B9(Down) B10(Right) B26(Up) B27(R3) +// +// Shared pins: B5/B12→PA7 (26 unique pins, 27 buttons) + +const GpioButtonMapping button_mappings[] = { + // Top row — Start / Select / Home + { BTN_MB1, PC0 }, // B1 - Start + { BTN_RT3, PC2 }, // B2 - Select + { BTN_RT2, PA4 }, // B3 - Home (also bootloader entry) + + // Left hand — movement + { BTN_LT2, PB9 }, // B7 - L3 + { BTN_LF3, PB11 }, // B8 - DPad Left + { BTN_LF2, PB1 }, // B9 - DPad Down + { BTN_LF1, PC5 }, // B10 - DPad Right + + // Right hand — attack buttons + { BTN_RF5, PA5 }, // B18 - X + { BTN_RF6, PA3 }, // B19 - Y + { BTN_RF7, PA1 }, // B20 - RB + { BTN_RF8, PC3 }, // B21 - LB + { BTN_RF1, PC12 }, // B22 - A + { BTN_RF2, PB13 }, // B23 - B + { BTN_RF3, PC6 }, // B24 - RT + { BTN_RF4, PB14 }, // B25 - LT + + // Bottom row + { BTN_LT1, PC7 }, // B26 - DPad Up + { BTN_RT1, PA0 }, // B27 - R3 +}; +const size_t button_count = sizeof(button_mappings) / sizeof(GpioButtonMapping); + +const Pinout pinout = { + .joybus_data = 0, + .nes_data = -1, + .nes_clock = -1, + .nes_latch = -1, + .mux = -1, + .nunchuk_detect = -1, + .nunchuk_sda = -1, + .nunchuk_scl = -1, +}; + +CommunicationBackend **backends = nullptr; +size_t backend_count; +KeyboardMode *current_kb_mode = nullptr; + +void setup() { + // Free PA15, PB3, PB4 from JTAG for GPIO use (keep SWD on PA13/PA14). + afio_cfg_debug_ports(AFIO_DEBUG_SW_ONLY); + + // PA8 is the matrix row driver — drive LOW to activate the diode-to-ground matrix. + pinMode(PA8, OUTPUT); + digitalWrite(PA8, LOW); + + static InputState inputs; + + static GpioButtonInput gpio_input(button_mappings, button_count); + gpio_input.UpdateInputs(inputs); + + // Check bootloader button hold as early as possible. + if (inputs.rt2) { + reboot_bootloader(); + } + + static InputSource *input_sources[] = { &gpio_input }; + size_t input_source_count = sizeof(input_sources) / sizeof(InputSource *); + + backend_count = + initialize_backends(backends, inputs, input_sources, input_source_count, config, pinout); + + setup_mode_activation_bindings(config.game_mode_configs, config.game_mode_configs_count); +} + +GameModeConfig *find_fgc_config(Config &cfg) { + for (size_t i = 0; i < cfg.game_mode_configs_count; i++) { + if (cfg.game_mode_configs[i].mode_id == MODE_FGC) { + return &cfg.game_mode_configs[i]; + } + } + return nullptr; +} + +static SocdType next_socd(SocdType current) { + switch (current) { + case SOCD_NEUTRAL: return SOCD_2IP; + case SOCD_2IP: return SOCD_2IP_NO_REAC; + case SOCD_2IP_NO_REAC: return SOCD_NEUTRAL; + default: return SOCD_NEUTRAL; + } +} + +void loop() { + select_mode(backends, backend_count, config); + + // SOCD toggle: Start (BTN_MB1) + Select (BTN_RT3) + Up (BTN_LT1) + static bool combo_was_held = false; + InputState &inputs = backends[0]->GetInputs(); + bool combo_held = inputs.mb1 && inputs.rt3 && inputs.lt1; + + if (combo_held && !combo_was_held) { + GameModeConfig *fgc_cfg = find_fgc_config(config); + if (fgc_cfg != nullptr) { + SocdType new_socd = next_socd(fgc_cfg->socd_pairs[0].socd_type); + for (size_t i = 0; i < fgc_cfg->socd_pairs_count; i++) { + fgc_cfg->socd_pairs[i].socd_type = new_socd; + } + // Re-apply config if FGC mode is currently active. + InputMode *current_mode = backends[0]->CurrentGameMode(); + if (current_mode != nullptr) { + current_mode->SetConfig(*fgc_cfg); + } + } + } + combo_was_held = combo_held; + + for (size_t i = 0; i < backend_count; i++) { + backends[i]->SendReport(); + } + + if (current_kb_mode != nullptr) { + current_kb_mode->SendReport(backends[0]->GetInputs()); + } +} diff --git a/config/stm32pintester/env.ini b/config/jz-stm32-leverless/env.ini similarity index 71% rename from config/stm32pintester/env.ini rename to config/jz-stm32-leverless/env.ini index 272626ba..83b0999f 100644 --- a/config/stm32pintester/env.ini +++ b/config/jz-stm32-leverless/env.ini @@ -1,4 +1,4 @@ -[env:stm32pintester] +[env:jz-stm32-leverless] extends = stm32_base ; Actual MCU is F103RFT6 (XL-density, 768KB), but maple core only supports up to ; F103RE (high-density, 512KB). Same 64-pin LQFP pinout; firmware fits in 512KB. @@ -6,13 +6,14 @@ board = genericSTM32F103RE board_upload.maximum_ram_size = 98304 board_upload.offset_address = 0x08001000 board_build.ldscript = stm32f103re_bootloader.ld -extra_scripts = post:config/stm32pintester/override_vect.py +extra_scripts = post:config/jz-stm32-leverless/override_vect.py ; DFU bootloader on PA11/PA12: hold B1 + reset to enter DFU mode, then: -; dfu-util -a 0 -s 0x08001000:leave -D .pio/build/stm32pintester/firmware.bin +; dfu-util -a 0 -s 0x08001000:leave -D .pio/build/jz-stm32-leverless/firmware.bin upload_protocol = dfu upload_command = dfu-util -a 0 -s 0x08001000:leave -D $SOURCE build_flags = ${stm32_base.build_flags} + -DBOARD_USB_DISC_DEV=NULL build_src_filter = ${stm32_base.build_src_filter} - + + + diff --git a/config/stm32pintester/override_vect.py b/config/jz-stm32-leverless/override_vect.py similarity index 89% rename from config/stm32pintester/override_vect.py rename to config/jz-stm32-leverless/override_vect.py index 5ebe00cc..d63b9626 100644 --- a/config/stm32pintester/override_vect.py +++ b/config/jz-stm32-leverless/override_vect.py @@ -15,5 +15,5 @@ env.Replace(CPPDEFINES=new_defines) # Add our config dir to linker search path for the custom .ld file -ld_dir = os.path.join(env["PROJECT_DIR"], "config", "stm32pintester") +ld_dir = os.path.join(env["PROJECT_DIR"], "config", "jz-stm32-leverless") env.Prepend(LIBPATH=[ld_dir]) diff --git a/config/stm32pintester/stm32f103re_bootloader.ld b/config/jz-stm32-leverless/stm32f103re_bootloader.ld similarity index 100% rename from config/stm32pintester/stm32f103re_bootloader.ld rename to config/jz-stm32-leverless/stm32f103re_bootloader.ld diff --git a/config/stm32pintester/config.cpp b/config/stm32pintester/config.cpp deleted file mode 100644 index 20ed7a23..00000000 --- a/config/stm32pintester/config.cpp +++ /dev/null @@ -1,128 +0,0 @@ -#include "comms/backend_init.hpp" -#include "config_defaults.hpp" -#include "core/CommunicationBackend.hpp" -#include "core/KeyboardMode.hpp" -#include "core/mode_selection.hpp" -#include "core/pinout.hpp" -#include "core/state.hpp" -#include "input/GpioButtonInput.hpp" -#include "reboot.hpp" -#include "stdlib.hpp" - -#include - -Config config = default_config; - -// Button-to-pin mapping from stm32pintester Buttons.md. -// B0XX-style leverless layout — adjust after physical testing. -// -// Left hand (movement): -// B1(PC0) B2(PC2) B3(PB8) -// B4(PD2) B5(PB11) B6(PA2) -// B7(PA0) B8(PB15) B9(PB13) B10(PC5) -// B11(PB15) B12(PB11) -// -// Right hand (attacks): -// B13(PB0) B14(PC8) B15(PC9) B16(PC10) B17(PC11) -// B18(PB9) B19(PB7) B20(PB5) B21(PC3) -// B22(PC12) B23(PA15) B24(PC6) B25(PB14) -// B26(PC7) B27(PB4) -// -// Note: B5/B12 share PB11, B8/B11 share PB15 — only 25 unique pins for 27 buttons. -// Shared pins are mapped once (the first occurrence). - -const GpioButtonMapping button_mappings[] = { - // Left hand - movement - { BTN_LF4, PC0 }, // B1 - Left - { BTN_LF3, PA0 }, // B7 - Down - { BTN_LF2, PC2 }, // B2 - Up - { BTN_LF1, PB8 }, // B3 - Right - - // Left hand - thumbs - { BTN_LT1, PB13 }, // B9 - ModX - { BTN_LT2, PC5 }, // B10 - ModY - - // Left hand - extra - { BTN_MB3, PD2 }, // B4 - { BTN_MB1, PB11 }, // B5 (shared with B12) - { BTN_MB2, PA2 }, // B6 - - // Right hand - top row - { BTN_RT3, PC8 }, // B14 - { BTN_RT4, PC9 }, // B15 - { BTN_RT2, PC10 }, // B16 - { BTN_RT1, PC11 }, // B17 - { BTN_RT5, PB15 }, // B8 (shared with B11) - - // Right hand - attack buttons - { BTN_RF1, PB9 }, // B18 - A (B) - { BTN_RF2, PB7 }, // B19 - B (A) - { BTN_RF3, PB5 }, // B20 - X (Y) - { BTN_RF4, PC3 }, // B21 - Y (X) - - // Right hand - bottom rows - { BTN_RF5, PB0 }, // B13 - { BTN_RF6, PC12 }, // B22 - { BTN_RF7, PA15 }, // B23 - { BTN_RF8, PC6 }, // B24 - - // Remaining buttons - { BTN_RF9, PB14 }, // B25 - { BTN_RF10, PC7 }, // B26 - { BTN_RF11, PB4 }, // B27 -}; -const size_t button_count = sizeof(button_mappings) / sizeof(GpioButtonMapping); - -const Pinout pinout = { - .joybus_data = 0, - .nes_data = -1, - .nes_clock = -1, - .nes_latch = -1, - .mux = -1, - .nunchuk_detect = -1, - .nunchuk_sda = -1, - .nunchuk_scl = -1, -}; - -CommunicationBackend **backends = nullptr; -size_t backend_count; -KeyboardMode *current_kb_mode = nullptr; - -void setup() { - // Free PA15, PB3, PB4 from JTAG for GPIO use (keep SWD on PA13/PA14). - afio_cfg_debug_ports(AFIO_DEBUG_SW_ONLY); - - // PA8 is the matrix row driver — drive LOW to activate the diode-to-ground matrix. - pinMode(PA8, OUTPUT); - digitalWrite(PA8, LOW); - - static InputState inputs; - - static GpioButtonInput gpio_input(button_mappings, button_count); - gpio_input.UpdateInputs(inputs); - - // Check bootloader button hold as early as possible. - if (inputs.rt2) { - reboot_bootloader(); - } - - static InputSource *input_sources[] = { &gpio_input }; - size_t input_source_count = sizeof(input_sources) / sizeof(InputSource *); - - backend_count = - initialize_backends(backends, inputs, input_sources, input_source_count, config, pinout); - - setup_mode_activation_bindings(config.game_mode_configs, config.game_mode_configs_count); -} - -void loop() { - select_mode(backends, backend_count, config); - - for (size_t i = 0; i < backend_count; i++) { - backends[i]->SendReport(); - } - - if (current_kb_mode != nullptr) { - current_kb_mode->SendReport(backends[0]->GetInputs()); - } -} From 933e9a1bcfffb25e1107293193e2ceb90d03b5dd Mon Sep 17 00:00:00 2001 From: Jesse Zhao Date: Thu, 12 Mar 2026 11:48:45 -0700 Subject: [PATCH 05/10] Default FGC vertical SOCD to Up priority, toggle only vertical pair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change the default vertical SOCD from SOCD_NEUTRAL to SOCD_DIR1_PRIORITY (Up wins over Down). SOCD toggle combo now cycles only the vertical pair (index 1), leaving horizontal always SOCD_NEUTRAL. The cycle now includes DIR1_PRIORITY: DIR1 → NEUTRAL → 2IP → 2IP_NO_REAC → DIR1. --- config/jz-stm32-leverless/config.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/config/jz-stm32-leverless/config.cpp b/config/jz-stm32-leverless/config.cpp index 8ea5c140..64e2ed15 100644 --- a/config/jz-stm32-leverless/config.cpp +++ b/config/jz-stm32-leverless/config.cpp @@ -67,6 +67,8 @@ CommunicationBackend **backends = nullptr; size_t backend_count; KeyboardMode *current_kb_mode = nullptr; +GameModeConfig *find_fgc_config(Config &cfg); + void setup() { // Free PA15, PB3, PB4 from JTAG for GPIO use (keep SWD on PA13/PA14). afio_cfg_debug_ports(AFIO_DEBUG_SW_ONLY); @@ -88,6 +90,12 @@ void setup() { static InputSource *input_sources[] = { &gpio_input }; size_t input_source_count = sizeof(input_sources) / sizeof(InputSource *); + // Default FGC vertical SOCD to Up priority (Dir1 = Up wins over Down). + GameModeConfig *fgc = find_fgc_config(config); + if (fgc && fgc->socd_pairs_count >= 2) { + fgc->socd_pairs[1] = { BTN_LT1, BTN_LF2, SOCD_DIR1_PRIORITY }; + } + backend_count = initialize_backends(backends, inputs, input_sources, input_source_count, config, pinout); @@ -105,10 +113,11 @@ GameModeConfig *find_fgc_config(Config &cfg) { static SocdType next_socd(SocdType current) { switch (current) { - case SOCD_NEUTRAL: return SOCD_2IP; - case SOCD_2IP: return SOCD_2IP_NO_REAC; - case SOCD_2IP_NO_REAC: return SOCD_NEUTRAL; - default: return SOCD_NEUTRAL; + case SOCD_DIR1_PRIORITY: return SOCD_NEUTRAL; + case SOCD_NEUTRAL: return SOCD_2IP; + case SOCD_2IP: return SOCD_2IP_NO_REAC; + case SOCD_2IP_NO_REAC: return SOCD_DIR1_PRIORITY; + default: return SOCD_DIR1_PRIORITY; } } @@ -122,11 +131,8 @@ void loop() { if (combo_held && !combo_was_held) { GameModeConfig *fgc_cfg = find_fgc_config(config); - if (fgc_cfg != nullptr) { - SocdType new_socd = next_socd(fgc_cfg->socd_pairs[0].socd_type); - for (size_t i = 0; i < fgc_cfg->socd_pairs_count; i++) { - fgc_cfg->socd_pairs[i].socd_type = new_socd; - } + if (fgc_cfg != nullptr && fgc_cfg->socd_pairs_count >= 2) { + fgc_cfg->socd_pairs[1].socd_type = next_socd(fgc_cfg->socd_pairs[1].socd_type); // Re-apply config if FGC mode is currently active. InputMode *current_mode = backends[0]->CurrentGameMode(); if (current_mode != nullptr) { From ad45b22a9946ff1f7c01abf90a0883839bd27121 Mon Sep 17 00:00:00 2001 From: Jesse Zhao Date: Sun, 15 Mar 2026 09:14:17 -0700 Subject: [PATCH 06/10] Add B11/B12 to left-hand physical layout in ButtonMappings.md --- config/jz-stm32-leverless/ButtonMappings.md | 1 + 1 file changed, 1 insertion(+) diff --git a/config/jz-stm32-leverless/ButtonMappings.md b/config/jz-stm32-leverless/ButtonMappings.md index 7cdccffe..25da019c 100644 --- a/config/jz-stm32-leverless/ButtonMappings.md +++ b/config/jz-stm32-leverless/ButtonMappings.md @@ -9,6 +9,7 @@ Left hand (movement + utility): B1(PC0) B2(PC2) B3(PA4) B4(PD2) B5(PA7) B6(PB7) B7(PB9) B8(PB11) B9(PB1) B10(PC5) + B11(PB15) B12(PA7) ``` Right hand (attacks): From 875023012ae0c81ecbbda7ebc5046370eb4c0543 Mon Sep 17 00:00:00 2001 From: Jesse Zhao Date: Sun, 15 Mar 2026 09:29:33 -0700 Subject: [PATCH 07/10] =?UTF-8?q?Map=20additional=20buttons:=20B5/B12?= =?UTF-8?q?=E2=86=92Up,=20B11=E2=86=92L3,=20B13=E2=86=92R3,=20B14-B16?= =?UTF-8?q?=E2=86=92Start/Select/Home?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/jz-stm32-leverless/ButtonMappings.md | 20 ++++++++++---------- config/jz-stm32-leverless/config.cpp | 6 ++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/config/jz-stm32-leverless/ButtonMappings.md b/config/jz-stm32-leverless/ButtonMappings.md index 25da019c..17ec4ac7 100644 --- a/config/jz-stm32-leverless/ButtonMappings.md +++ b/config/jz-stm32-leverless/ButtonMappings.md @@ -20,7 +20,7 @@ Right hand (attacks): B26(PC7) B27(PA0) ``` -Shared pins: B5/B12 share PA7 (26 unique pins, 27 buttons). +Shared pins: B5/B12 share PA7 → both mapped to DPad Up (26 unique pins, 27 buttons). B8/B11 are NOT shared — B8=PB11, B11=PB15. ## Button-to-XInput Mapping @@ -31,18 +31,18 @@ B8/B11 are NOT shared — B8=PB11, B11=PB15. | B2 | PC2 | BTN_RT3 | Select | | | B3 | PA4 | BTN_RT2 | Home | Also bootloader entry | | B4 | PD2 | — | — | Unmapped | -| B5 | PA7 | — | — | Unmapped (shared with B12) | +| B5 | PA7 | BTN_LT1 | DPad Up | Shared pin with B12 | | B6 | PB7 | — | — | Unmapped | | B7 | PB9 | BTN_LT2 | L3 | | | B8 | PB11 | BTN_LF3 | DPad Left | | | B9 | PB1 | BTN_LF2 | DPad Down | | | B10 | PC5 | BTN_LF1 | DPad Right | | -| B11 | PB15 | — | — | Unmapped | -| B12 | PA7 | — | — | Shared pin with B5, both unmapped | -| B13 | PB12 | — | — | Unmapped | -| B14 | PC8 | — | — | Unmapped | -| B15 | PC9 | — | — | Unmapped | -| B16 | PC10 | — | — | Unmapped | +| B11 | PB15 | BTN_LT2 | L3 | Same as B7 | +| B12 | PA7 | BTN_LT1 | DPad Up | Shared pin with B5 | +| B13 | PB12 | BTN_RT1 | R3 | Same as B27 | +| B14 | PC8 | BTN_MB1 | Start | Same as B1 | +| B15 | PC9 | BTN_RT3 | Select | Same as B2 | +| B16 | PC10 | BTN_RT2 | Home | Same as B3 | | B17 | PC11 | — | — | Unmapped | | B18 | PA5 | BTN_RF5 | X | | | B19 | PA3 | BTN_RF6 | Y | | @@ -57,9 +57,9 @@ B8/B11 are NOT shared — B8=PB11, B11=PB15. ## Unmapped Buttons (Available for Future Use) -B4 (PD2), B5/B12 (PA7), B6 (PB7), B11 (PB15), B13 (PB12), B14 (PC8), B15 (PC9), B16 (PC10), B17 (PC11) +B4 (PD2), B6 (PB7), B17 (PC11) -These 9 unique pins (10 buttons) are not mapped to any BTN_* value. +These 3 unique pins (3 buttons) are not mapped to any BTN_* value. ## SOCD Toggle diff --git a/config/jz-stm32-leverless/config.cpp b/config/jz-stm32-leverless/config.cpp index 64e2ed15..c6fb9a9a 100644 --- a/config/jz-stm32-leverless/config.cpp +++ b/config/jz-stm32-leverless/config.cpp @@ -29,9 +29,14 @@ const GpioButtonMapping button_mappings[] = { { BTN_MB1, PC0 }, // B1 - Start { BTN_RT3, PC2 }, // B2 - Select { BTN_RT2, PA4 }, // B3 - Home (also bootloader entry) + { BTN_MB1, PC8 }, // B14 - Start (same as B1) + { BTN_RT3, PC9 }, // B15 - Select (same as B2) + { BTN_RT2, PC10 }, // B16 - Home (same as B3) // Left hand — movement + { BTN_LT1, PA7 }, // B5 - DPad Up (also PA7/B12) { BTN_LT2, PB9 }, // B7 - L3 + { BTN_LT2, PB15 }, // B11 - L3 (same as B7) { BTN_LF3, PB11 }, // B8 - DPad Left { BTN_LF2, PB1 }, // B9 - DPad Down { BTN_LF1, PC5 }, // B10 - DPad Right @@ -49,6 +54,7 @@ const GpioButtonMapping button_mappings[] = { // Bottom row { BTN_LT1, PC7 }, // B26 - DPad Up { BTN_RT1, PA0 }, // B27 - R3 + { BTN_RT1, PB12 }, // B13 - R3 (same as B27) }; const size_t button_count = sizeof(button_mappings) / sizeof(GpioButtonMapping); From 99c4cd334f6f36558ed264f1e1e948bbdbc9fce4 Mon Sep 17 00:00:00 2001 From: Jesse Zhao Date: Sun, 15 Mar 2026 17:29:25 -0700 Subject: [PATCH 08/10] Fix input source pre-clear pattern for multi-mapping Add pre-clear pass before GPIO reads so multiple pins mapped to the same BTN_* constant use OR semantics: any pressed pin sets the button true; no unpressed pin can overwrite it false. Without this, the last entry in button_mappings[] always wins, breaking secondary mappings. UpdateButtonState now only sets true; false is handled by the pre-clear. Same pattern applied to SwitchMatrixInput, DebouncedGpioButtonInput, DebouncedSwitchMatrixInput, and Pca9671Input for consistency. --- HAL/pico/include/input/DebouncedGpioButtonInput.hpp | 10 +++------- HAL/pico/include/input/DebouncedSwitchMatrixInput.hpp | 11 +++-------- HAL/pico/src/input/Pca9671Input.cpp | 8 +++++++- include/input/SwitchMatrixInput.hpp | 10 ++++++++-- src/input/GpioButtonInput.cpp | 7 ++++++- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/HAL/pico/include/input/DebouncedGpioButtonInput.hpp b/HAL/pico/include/input/DebouncedGpioButtonInput.hpp index 916c34d9..aa3dfa71 100644 --- a/HAL/pico/include/input/DebouncedGpioButtonInput.hpp +++ b/HAL/pico/include/input/DebouncedGpioButtonInput.hpp @@ -21,13 +21,9 @@ template class DebouncedGpioButtonInput : public GpioButto uint32_t _debounce_period_ms; void UpdateButtonState(InputState &inputs, size_t button_mapping_index, bool pressed) { - bool state_changed = update_debounce_state( - _debounce_state[button_mapping_index], - pressed, - _debounce_period_ms - ); - if (state_changed) { - set_button(inputs.buttons, _button_mappings[button_mapping_index].button, pressed); + update_debounce_state(_debounce_state[button_mapping_index], pressed, _debounce_period_ms); + if (_debounce_state[button_mapping_index].pressed) { + set_button(inputs.buttons, _button_mappings[button_mapping_index].button, true); } } }; diff --git a/HAL/pico/include/input/DebouncedSwitchMatrixInput.hpp b/HAL/pico/include/input/DebouncedSwitchMatrixInput.hpp index bd6d6dd8..290e0634 100644 --- a/HAL/pico/include/input/DebouncedSwitchMatrixInput.hpp +++ b/HAL/pico/include/input/DebouncedSwitchMatrixInput.hpp @@ -23,14 +23,9 @@ class DebouncedSwitchMatrixInput : public SwitchMatrixInput uint32_t _debounce_period_ms; void UpdateButtonState(InputState &inputs, size_t col_index, size_t row_index, bool pressed) { - bool state_changed = update_debounce_state( - _debounce_state[col_index][row_index], - pressed, - _debounce_period_ms - ); - if (state_changed) { - Button button = this->_matrix[col_index][row_index]; - set_button(inputs.buttons, button, pressed); + update_debounce_state(_debounce_state[col_index][row_index], pressed, _debounce_period_ms); + if (_debounce_state[col_index][row_index].pressed) { + set_button(inputs.buttons, this->_matrix[col_index][row_index], true); } }; }; diff --git a/HAL/pico/src/input/Pca9671Input.cpp b/HAL/pico/src/input/Pca9671Input.cpp index fd501853..666c656e 100644 --- a/HAL/pico/src/input/Pca9671Input.cpp +++ b/HAL/pico/src/input/Pca9671Input.cpp @@ -31,6 +31,10 @@ InputScanSpeed Pca9671Input::ScanSpeed() { } void Pca9671Input::UpdateInputs(InputState &inputs) { + for (size_t i = 0; i < _button_count; i++) { + set_button(inputs.buttons, _button_mappings[i].button, false); + } + uint16_t pin_values = _pcf.read16(); for (size_t i = 0; i < _button_count; i++) { @@ -43,5 +47,7 @@ void Pca9671Input::UpdateButtonState( size_t button_mapping_index, bool pressed ) { - set_button(inputs.buttons, _button_mappings[button_mapping_index].button, pressed); + if (pressed) { + set_button(inputs.buttons, _button_mappings[button_mapping_index].button, true); + } } \ No newline at end of file diff --git a/include/input/SwitchMatrixInput.hpp b/include/input/SwitchMatrixInput.hpp index d499de64..fa466605 100644 --- a/include/input/SwitchMatrixInput.hpp +++ b/include/input/SwitchMatrixInput.hpp @@ -59,6 +59,11 @@ template class SwitchMatrixInput : public Inp InputScanSpeed ScanSpeed() { return InputScanSpeed::FAST; } void UpdateInputs(InputState &inputs) { + for (size_t i = 0; i < num_rows; i++) { + for (size_t j = 0; j < num_cols; j++) { + set_button(inputs.buttons, _matrix[i][j], false); + } + } for (size_t i = 0; i < _num_outputs; i++) { // Activate the column/row. gpio::init_pin(_output_pins[i], gpio::GpioMode::GPIO_OUTPUT); @@ -94,8 +99,9 @@ template class SwitchMatrixInput : public Inp size_t row_index, bool pressed ) { - Button button = _matrix[col_index][row_index]; - set_button(inputs.buttons, button, pressed); + if (pressed) { + set_button(inputs.buttons, _matrix[col_index][row_index], true); + } }; }; diff --git a/src/input/GpioButtonInput.cpp b/src/input/GpioButtonInput.cpp index 69fc2ee1..0f62e654 100644 --- a/src/input/GpioButtonInput.cpp +++ b/src/input/GpioButtonInput.cpp @@ -19,6 +19,9 @@ InputScanSpeed GpioButtonInput::ScanSpeed() { } void GpioButtonInput::UpdateInputs(InputState &inputs) { + for (size_t i = 0; i < _button_count; i++) { + set_button(inputs.buttons, _button_mappings[i].button, false); + } for (size_t i = 0; i < _button_count; i++) { UpdateButtonState(inputs, i, !gpio::read_digital(_button_mappings[i].pin)); } @@ -29,5 +32,7 @@ void GpioButtonInput::UpdateButtonState( size_t button_mapping_index, bool pressed ) { - set_button(inputs.buttons, _button_mappings[button_mapping_index].button, pressed); + if (pressed) { + set_button(inputs.buttons, _button_mappings[button_mapping_index].button, true); + } } From db8d78ccef03245c69b03f8fa0f906a4fa73c081 Mon Sep 17 00:00:00 2001 From: Jesse Zhao Date: Sun, 15 Mar 2026 17:31:13 -0700 Subject: [PATCH 09/10] Fix XInput responsiveness: manual report mode and 1ms USB polling setManualReportMode(true) prevents safeSendReport() from firing a blocking USB send on every button/axis setter call. Previously each of the ~21 setters triggered a full sendData() with two USB wait cycles, capping the effective loop rate at ~47Hz. With manual mode, setters only update the in-memory report struct and a single send() at the end of SendReport() does one USB transfer per frame (~1000Hz). Also add patch_usb_polling.py pre-build script to set bInterval=1 in the USBComposite usb_x360w.c endpoint descriptor, raising the USB host polling rate from 250Hz (bInterval=4) to 1000Hz (bInterval=1). --- HAL/stm32/src/comms/XInputBackend.cpp | 2 ++ config/jz-stm32-leverless/env.ini | 4 +++- config/jz-stm32-leverless/patch_usb_polling.py | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 config/jz-stm32-leverless/patch_usb_polling.py diff --git a/HAL/stm32/src/comms/XInputBackend.cpp b/HAL/stm32/src/comms/XInputBackend.cpp index 285d42e9..942798bf 100644 --- a/HAL/stm32/src/comms/XInputBackend.cpp +++ b/HAL/stm32/src/comms/XInputBackend.cpp @@ -18,6 +18,8 @@ XInputBackend::XInputBackend( USBComposite.begin(); while (!USBComposite) ; + // Suppress auto-send on each setter call; we send once at the end of SendReport(). + _xbox360.setManualReportMode(true); } CommunicationBackendId XInputBackend::BackendId() { diff --git a/config/jz-stm32-leverless/env.ini b/config/jz-stm32-leverless/env.ini index 83b0999f..836c8187 100644 --- a/config/jz-stm32-leverless/env.ini +++ b/config/jz-stm32-leverless/env.ini @@ -6,7 +6,9 @@ board = genericSTM32F103RE board_upload.maximum_ram_size = 98304 board_upload.offset_address = 0x08001000 board_build.ldscript = stm32f103re_bootloader.ld -extra_scripts = post:config/jz-stm32-leverless/override_vect.py +extra_scripts = + pre:config/jz-stm32-leverless/patch_usb_polling.py + post:config/jz-stm32-leverless/override_vect.py ; DFU bootloader on PA11/PA12: hold B1 + reset to enter DFU mode, then: ; dfu-util -a 0 -s 0x08001000:leave -D .pio/build/jz-stm32-leverless/firmware.bin upload_protocol = dfu diff --git a/config/jz-stm32-leverless/patch_usb_polling.py b/config/jz-stm32-leverless/patch_usb_polling.py new file mode 100644 index 00000000..779f3de2 --- /dev/null +++ b/config/jz-stm32-leverless/patch_usb_polling.py @@ -0,0 +1,18 @@ +Import("env") +import os, re + +# Patch USBComposite usb_x360w.c to set bInterval=1 (1ms/1000Hz polling). +# The library default is 4ms (250Hz), which causes noticeable input latency. + +libdeps = env.subst("$PROJECT_LIBDEPS_DIR") +target_file = os.path.join(libdeps, env["PIOENV"], + "USBComposite for STM32F1", "usb_x360w.c") + +if os.path.isfile(target_file): + with open(target_file, "r") as f: + src = f.read() + patched = re.sub(r"(\.bInterval\s*=\s*)[48](,)", r"\g<1>1\2", src) + if patched != src: + with open(target_file, "w") as f: + f.write(patched) + print("Patched usb_x360w.c: bInterval → 1ms (1000Hz)") From 184a86fcbb952b3ac928dfcec0509cfbb6937009 Mon Sep 17 00:00:00 2001 From: Jesse Zhao Date: Mon, 16 Mar 2026 13:54:07 -0700 Subject: [PATCH 10/10] Deduplicate state_util.hpp and config.options from AVR HAL --- HAL/stm32/include/util/state_util.hpp | 480 +------------------------- HAL/stm32/proto/config.options | 99 ------ platformio.ini | 2 +- 3 files changed, 2 insertions(+), 579 deletions(-) delete mode 100644 HAL/stm32/proto/config.options diff --git a/HAL/stm32/include/util/state_util.hpp b/HAL/stm32/include/util/state_util.hpp index 65c01e0b..d5330ce2 100644 --- a/HAL/stm32/include/util/state_util.hpp +++ b/HAL/stm32/include/util/state_util.hpp @@ -1,479 +1 @@ -#ifndef _UTIL_STATE_UTIL_HPP -#define _UTIL_STATE_UTIL_HPP - -#include "stdlib.hpp" - -#include - -typedef struct _ButtonState { - bool lf1 : 1; - bool lf2 : 1; - bool lf3 : 1; - bool lf4 : 1; - bool lf5 : 1; - bool lf6 : 1; - bool lf7 : 1; - bool lf8 : 1; - bool lf9 : 1; - bool lf10 : 1; - bool lf11 : 1; - bool lf12 : 1; - bool lf13 : 1; - bool lf14 : 1; - bool lf15 : 1; - bool lf16 : 1; - bool rf1 : 1; - bool rf2 : 1; - bool rf3 : 1; - bool rf4 : 1; - bool rf5 : 1; - bool rf6 : 1; - bool rf7 : 1; - bool rf8 : 1; - bool rf9 : 1; - bool rf10 : 1; - bool rf11 : 1; - bool rf12 : 1; - bool rf13 : 1; - bool rf14 : 1; - bool rf15 : 1; - bool rf16 : 1; - bool lt1 : 1; - bool lt2 : 1; - bool lt3 : 1; - bool lt4 : 1; - bool lt5 : 1; - bool lt6 : 1; - bool lt7 : 1; - bool lt8 : 1; - bool rt1 : 1; - bool rt2 : 1; - bool rt3 : 1; - bool rt4 : 1; - bool rt5 : 1; - bool rt6 : 1; - bool rt7 : 1; - bool rt8 : 1; - bool mb1 : 1; - bool mb2 : 1; - bool mb3 : 1; - bool mb4 : 1; - bool mb5 : 1; - bool mb6 : 1; - bool mb7 : 1; - bool mb8 : 1; - bool mb9 : 1; - bool mb10 : 1; - bool mb11 : 1; - bool mb12 : 1; -} ButtonState; - -inline void set_button(uint64_t &buttons, Button button_index, bool pressed) { - ButtonState &inputs = (ButtonState &)buttons; - if (button_index == BTN_UNSPECIFIED) { - return; - } - switch (button_index) { - case BTN_LF1: - inputs.lf1 = pressed; - break; - case BTN_LF2: - inputs.lf2 = pressed; - break; - case BTN_LF3: - inputs.lf3 = pressed; - break; - case BTN_LF4: - inputs.lf4 = pressed; - break; - case BTN_LF5: - inputs.lf5 = pressed; - break; - case BTN_LF6: - inputs.lf6 = pressed; - break; - case BTN_LF7: - inputs.lf7 = pressed; - break; - case BTN_LF8: - inputs.lf8 = pressed; - break; - case BTN_LF9: - inputs.lf9 = pressed; - break; - case BTN_LF10: - inputs.lf10 = pressed; - break; - case BTN_LF11: - inputs.lf11 = pressed; - break; - case BTN_LF12: - inputs.lf12 = pressed; - break; - case BTN_LF13: - inputs.lf13 = pressed; - break; - case BTN_LF14: - inputs.lf14 = pressed; - break; - case BTN_LF15: - inputs.lf15 = pressed; - break; - case BTN_LF16: - inputs.lf16 = pressed; - break; - case BTN_RF1: - inputs.rf1 = pressed; - break; - case BTN_RF2: - inputs.rf2 = pressed; - break; - case BTN_RF3: - inputs.rf3 = pressed; - break; - case BTN_RF4: - inputs.rf4 = pressed; - break; - case BTN_RF5: - inputs.rf5 = pressed; - break; - case BTN_RF6: - inputs.rf6 = pressed; - break; - case BTN_RF7: - inputs.rf7 = pressed; - break; - case BTN_RF8: - inputs.rf8 = pressed; - break; - case BTN_RF9: - inputs.rf9 = pressed; - break; - case BTN_RF10: - inputs.rf10 = pressed; - break; - case BTN_RF11: - inputs.rf11 = pressed; - break; - case BTN_RF12: - inputs.rf12 = pressed; - break; - case BTN_RF13: - inputs.rf13 = pressed; - break; - case BTN_RF14: - inputs.rf14 = pressed; - break; - case BTN_RF15: - inputs.rf15 = pressed; - break; - case BTN_RF16: - inputs.rf16 = pressed; - break; - case BTN_LT1: - inputs.lt1 = pressed; - break; - case BTN_LT2: - inputs.lt2 = pressed; - break; - case BTN_LT3: - inputs.lt3 = pressed; - break; - case BTN_LT4: - inputs.lt4 = pressed; - break; - case BTN_LT5: - inputs.lt5 = pressed; - break; - case BTN_LT6: - inputs.lt6 = pressed; - break; - case BTN_LT7: - inputs.lt7 = pressed; - break; - case BTN_LT8: - inputs.lt8 = pressed; - break; - case BTN_RT1: - inputs.rt1 = pressed; - break; - case BTN_RT2: - inputs.rt2 = pressed; - break; - case BTN_RT3: - inputs.rt3 = pressed; - break; - case BTN_RT4: - inputs.rt4 = pressed; - break; - case BTN_RT5: - inputs.rt5 = pressed; - break; - case BTN_RT6: - inputs.rt6 = pressed; - break; - case BTN_RT7: - inputs.rt7 = pressed; - break; - case BTN_RT8: - inputs.rt8 = pressed; - break; - case BTN_MB1: - inputs.mb1 = pressed; - break; - case BTN_MB2: - inputs.mb2 = pressed; - break; - case BTN_MB3: - inputs.mb3 = pressed; - break; - case BTN_MB4: - inputs.mb4 = pressed; - break; - case BTN_MB5: - inputs.mb5 = pressed; - break; - case BTN_MB6: - inputs.mb6 = pressed; - break; - case BTN_MB7: - inputs.mb7 = pressed; - break; - case BTN_MB8: - inputs.mb8 = pressed; - break; - case BTN_MB9: - inputs.mb9 = pressed; - break; - case BTN_MB10: - inputs.mb10 = pressed; - break; - case BTN_MB11: - inputs.mb11 = pressed; - break; - case BTN_MB12: - inputs.mb12 = pressed; - break; - default: - break; - } -} - -inline bool get_button(const uint64_t &buttons, Button button_index) { - ButtonState &inputs = (ButtonState &)buttons; - switch (button_index) { - case BTN_LF1: - return inputs.lf1; - case BTN_LF2: - return inputs.lf2; - case BTN_LF3: - return inputs.lf3; - case BTN_LF4: - return inputs.lf4; - case BTN_LF5: - return inputs.lf5; - case BTN_LF6: - return inputs.lf6; - case BTN_LF7: - return inputs.lf7; - case BTN_LF8: - return inputs.lf8; - case BTN_LF9: - return inputs.lf9; - case BTN_LF10: - return inputs.lf10; - case BTN_LF11: - return inputs.lf11; - case BTN_LF12: - return inputs.lf12; - case BTN_LF13: - return inputs.lf13; - case BTN_LF14: - return inputs.lf14; - case BTN_LF15: - return inputs.lf15; - case BTN_LF16: - return inputs.lf16; - case BTN_RF1: - return inputs.rf1; - case BTN_RF2: - return inputs.rf2; - case BTN_RF3: - return inputs.rf3; - case BTN_RF4: - return inputs.rf4; - case BTN_RF5: - return inputs.rf5; - case BTN_RF6: - return inputs.rf6; - case BTN_RF7: - return inputs.rf7; - case BTN_RF8: - return inputs.rf8; - case BTN_RF9: - return inputs.rf9; - case BTN_RF10: - return inputs.rf10; - case BTN_RF11: - return inputs.rf11; - case BTN_RF12: - return inputs.rf12; - case BTN_RF13: - return inputs.rf13; - case BTN_RF14: - return inputs.rf14; - case BTN_RF15: - return inputs.rf15; - case BTN_RF16: - return inputs.rf16; - case BTN_LT1: - return inputs.lt1; - case BTN_LT2: - return inputs.lt2; - case BTN_LT3: - return inputs.lt3; - case BTN_LT4: - return inputs.lt4; - case BTN_LT5: - return inputs.lt5; - case BTN_LT6: - return inputs.lt6; - case BTN_LT7: - return inputs.lt7; - case BTN_LT8: - return inputs.lt8; - case BTN_RT1: - return inputs.rt1; - case BTN_RT2: - return inputs.rt2; - case BTN_RT3: - return inputs.rt3; - case BTN_RT4: - return inputs.rt4; - case BTN_RT5: - return inputs.rt5; - case BTN_RT6: - return inputs.rt6; - case BTN_RT7: - return inputs.rt7; - case BTN_RT8: - return inputs.rt8; - case BTN_MB1: - return inputs.mb1; - case BTN_MB2: - return inputs.mb2; - case BTN_MB3: - return inputs.mb3; - case BTN_MB4: - return inputs.mb4; - case BTN_MB5: - return inputs.mb5; - case BTN_MB6: - return inputs.mb6; - case BTN_MB7: - return inputs.mb7; - case BTN_MB8: - return inputs.mb8; - case BTN_MB9: - return inputs.mb9; - case BTN_MB10: - return inputs.mb10; - case BTN_MB11: - return inputs.mb11; - case BTN_MB12: - return inputs.mb12; - default: - return false; - } -} - -inline uint64_t make_button_mask(const Button *buttons, size_t buttons_count) { - uint64_t button_mask = 0; - for (size_t j = 0; j < buttons_count; j++) { - button_mask |= (1ULL << (buttons[j] - 1)); - } - return button_mask; -} - -inline bool all_buttons_held(const uint64_t &buttons, uint64_t button_mask) { - return button_mask != 0 && (buttons & button_mask) == button_mask; -} - -inline bool any_button_held(const uint64_t &buttons, uint64_t button_mask) { - return button_mask != 0 && (buttons & button_mask); -} - -/* OutputState utils */ - -inline void set_output(uint32_t &buttons, DigitalOutput output_index, bool pressed) { - if (output_index == GP_UNSPECIFIED) { - return; - } - DigitalOutput output_index_adjusted = (DigitalOutput)(output_index - 1); - buttons = - (buttons & ~(1UL << output_index_adjusted)) | ((uint32_t)pressed << output_index_adjusted); -} - -inline uint8_t OutputState::*axis_pointer(AnalogAxis axis) { - switch (axis) { - case AXIS_LSTICK_X: - return &OutputState::leftStickX; - case AXIS_LSTICK_Y: - return &OutputState::leftStickY; - case AXIS_RSTICK_X: - return &OutputState::rightStickX; - case AXIS_RSTICK_Y: - return &OutputState::rightStickY; - case AXIS_LTRIGGER: - return &OutputState::triggerLAnalog; - case AXIS_RTRIGGER: - return &OutputState::triggerRAnalog; - default: - return nullptr; - } -} - -constexpr const char *digital_output_name(DigitalOutput output) { - switch (output) { - case GP_A: - return "A"; - case GP_B: - return "B"; - case GP_X: - return "X"; - case GP_Y: - return "Y"; - case GP_LB: - return "L1"; - case GP_RB: - return "R1"; - case GP_LT: - return "L2"; - case GP_RT: - return "R2"; - case GP_START: - return "Start"; - case GP_SELECT: - return "Select"; - case GP_HOME: - return "Home"; - case GP_CAPTURE: - return "Capture"; - case GP_DPAD_UP: - return "D-Pad Up"; - case GP_DPAD_DOWN: - return "D-Pad Down"; - case GP_DPAD_LEFT: - return "D-Pad Left"; - case GP_DPAD_RIGHT: - return "D-Pad Right"; - case GP_LSTICK_CLICK: - return "L3"; - case GP_RSTICK_CLICK: - return "R3"; - default: - return "Unknown"; - } -} - -#endif \ No newline at end of file +#include "../../avr/include/util/state_util.hpp" diff --git a/HAL/stm32/proto/config.options b/HAL/stm32/proto/config.options deleted file mode 100644 index de7a0a38..00000000 --- a/HAL/stm32/proto/config.options +++ /dev/null @@ -1,99 +0,0 @@ -ButtonRemap.physical_button int_size:IS_8 -ButtonRemap.activates int_size:IS_8 - -SocdPair.button_dir1 int_size:IS_8 -SocdPair.button_dir2 int_size:IS_8 -SocdPair.socd_type int_size:IS_8 - -AnalogTriggerMapping.button int_size:IS_8 -AnalogTriggerMapping.trigger int_size:IS_8 -AnalogTriggerMapping.value int_size:IS_8 - -AnalogModifier.buttons int_size:IS_8 -AnalogModifier.buttons max_count:3 -AnalogModifier.buttons type:FT_POINTER -AnalogModifier.axis int_size:IS_8 -AnalogModifier.combination_mode int_size:IS_8 - -ButtonComboMapping.buttons int_size:IS_8 -ButtonComboMapping.buttons max_count:3 -ButtonComboMapping.buttons type:FT_POINTER -ButtonComboMapping.digital_output int_size:IS_8 - -Coords.x int_size:IS_8 -Coords.y int_size:IS_8 - -ButtonToKeycodeMapping.button int_size:IS_8 -ButtonToKeycodeMapping.keycode int_size:IS_8 - -ButtonToColorMapping.button int_size:IS_8 - -GameModeConfig.mode_id int_size:IS_8 -GameModeConfig.name max_length:17 -GameModeConfig.name type:FT_POINTER -GameModeConfig.socd_pairs max_count:10 -GameModeConfig.socd_pairs type:FT_POINTER -GameModeConfig.button_remapping max_count:60 -GameModeConfig.button_remapping type:FT_POINTER -GameModeConfig.activation_binding max_count:4 -GameModeConfig.activation_binding type:FT_POINTER -GameModeConfig.custom_mode_config int_size:IS_8 -GameModeConfig.keyboard_mode_config int_size:IS_8 -GameModeConfig.rgb_config int_size:IS_8 - -CustomModeConfig.id int_size:IS_8 -CustomModeConfig.digital_button_mappings max_count:18 -CustomModeConfig.digital_button_mappings type:FT_POINTER -CustomModeConfig.stick_direction_mappings max_count:8 -CustomModeConfig.stick_direction_mappings type:FT_POINTER -CustomModeConfig.analog_trigger_mappings max_count:4 -CustomModeConfig.analog_trigger_mappings type:FT_POINTER -CustomModeConfig.button_combo_mappings max_count:5 -CustomModeConfig.button_combo_mappings type:FT_POINTER -CustomModeConfig.modifiers max_count:20 -CustomModeConfig.modifiers type:FT_POINTER -CustomModeConfig.stick_range int_size:IS_8 - -KeyboardModeConfig.id int_size:IS_8 -KeyboardModeConfig.buttons_to_keycodes max_count:60 -KeyboardModeConfig.buttons_to_keycodes type:FT_POINTER - -CommunicationBackendConfig.backend_id int_size:IS_8 -CommunicationBackendConfig.default_mode_config int_size:IS_8 -CommunicationBackendConfig.activation_binding max_count:2 -CommunicationBackendConfig.activation_binding type:FT_POINTER - -RgbConfig.button_colors max_count:60 -RgbConfig.button_colors type:FT_POINTER -RgbConfig.animation int_size:IS_8 -RgbConfig.speed int_size:IS_8 - -Config.game_mode_configs max_count:10 -Config.game_mode_configs type:FT_POINTER -Config.communication_backend_configs max_count:10 -Config.communication_backend_configs type:FT_POINTER -Config.custom_modes max_count:5 -Config.custom_modes type:FT_POINTER -Config.keyboard_modes max_count:5 -Config.keyboard_modes type:FT_POINTER -Config.rgb_configs max_count:10 -Config.rgb_configs type:FT_POINTER -Config.default_backend_config int_size:IS_8 -Config.default_usb_backend_config int_size:IS_8 -Config.rgb_brightness int_size:IS_8 - -DeviceInfo.firmware_name max_length:25 -DeviceInfo.firmware_version max_length:8 -DeviceInfo.device_name max_length:30 - -Command long_names:false -Button long_names:false -DigitalOutput long_names:false -StickDirectionButton long_names:false -AnalogAxis long_names:false -AnalogTrigger long_names:false -ModifierCombinationMode long_names:false -SocdType long_names:false -GameModeId long_names:false -CommunicationBackendId long_names:false -RgbAnimationId long_names:false \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 1584b426..d90ed573 100644 --- a/platformio.ini +++ b/platformio.ini @@ -108,7 +108,7 @@ build_src_filter = - custom_nanopb_options = ${env.custom_nanopb_options} - --options-file ../../../../HAL/stm32/proto/config.options + --options-file ../../../../HAL/avr/proto/config.options lib_deps = ${env.lib_deps} arpruss/USBComposite for STM32F1