From b9e92de717c92567c2e97fee2359f851b6db6229 Mon Sep 17 00:00:00 2001 From: Jesse Zhao Date: Wed, 11 Mar 2026 12:42:02 -0700 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] =?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/11] 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/11] 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/11] 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 From 4907f586465190e098b9242052708096565ccdc7 Mon Sep 17 00:00:00 2001 From: Jesse Zhao Date: Thu, 10 Sep 2026 20:13:49 -0700 Subject: [PATCH 11/11] Fix STM32 USB reporting and SOCD mode isolation Use non-blocking XInput reports with a 1 ms wired IN interval. Keep PC12 available for button input. Pin the STM32 build dependencies. Apply SOCD changes to the FGC configuration in place. Add descriptor regression tests and ELF verification. Record the Windows input checks. --- .gitignore | 2 + HAL/stm32/src/comms/XInputBackend.cpp | 5 +- config/jz-stm32-leverless/VALIDATION.md | 106 ++++++++++++++++++ config/jz-stm32-leverless/config.cpp | 5 - config/jz-stm32-leverless/env.ini | 4 +- .../jz-stm32-leverless/patch_usb_polling.py | 44 +++++--- config/jz-stm32-leverless/usb_disc_override.h | 8 ++ platformio.ini | 10 +- tests/requirements.txt | 1 + tests/test_stm32_usb_polling.py | 90 +++++++++++++++ tests/verify_stm32_firmware.py | 91 +++++++++++++++ 11 files changed, 340 insertions(+), 26 deletions(-) create mode 100644 config/jz-stm32-leverless/VALIDATION.md create mode 100644 config/jz-stm32-leverless/usb_disc_override.h create mode 100644 tests/requirements.txt create mode 100644 tests/test_stm32_usb_polling.py create mode 100644 tests/verify_stm32_firmware.py diff --git a/.gitignore b/.gitignore index 89cc49cb..072cd113 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .pio +compile_commands.json +__pycache__/ .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json .vscode/launch.json diff --git a/HAL/stm32/src/comms/XInputBackend.cpp b/HAL/stm32/src/comms/XInputBackend.cpp index 942798bf..96bea515 100644 --- a/HAL/stm32/src/comms/XInputBackend.cpp +++ b/HAL/stm32/src/comms/XInputBackend.cpp @@ -60,5 +60,8 @@ void XInputBackend::SendReport() { _xbox360.XRight((_outputs.rightStickX - 128) * 65535 / 255 + 128); _xbox360.YRight((_outputs.rightStickY - 128) * 65535 / 255 + 128); - _xbox360.send(); + // Use x360_tx to keep input scanning active when the USB endpoint is busy. + if (USBComposite) { + x360_tx(0, _xbox360.getReport(), _xbox360.getReportSize()); + } } diff --git a/config/jz-stm32-leverless/VALIDATION.md b/config/jz-stm32-leverless/VALIDATION.md new file mode 100644 index 00000000..0306d2f9 --- /dev/null +++ b/config/jz-stm32-leverless/VALIDATION.md @@ -0,0 +1,106 @@ +# STM32 USB validation + +## Build and checks + +Use a Python virtual environment with PlatformIO 6.1.19. +The STM32 environment pins its platform, compiler, framework, and libraries in `platformio.ini`. + +```sh +python -m pip install platformio==6.1.19 -r tests/requirements.txt +pio run -e jz-stm32-leverless +python -m unittest discover -s tests -p test_stm32_usb_polling.py +python tests/verify_stm32_firmware.py .pio/build/jz-stm32-leverless/firmware.elf \ + --objdump "${PLATFORMIO_CORE_DIR:-$HOME/.platformio}/packages/toolchain-gccarmnoneeabi/bin/arm-none-eabi-objdump" +``` + +Run the ELF checker with `--baseline` for the unchanged `184a86f` build. +The checker requires the pinned compiler's disassembler and an unstripped ELF file. +It checks linked descriptor intervals, report submission calls, the application address, and the initial stack pointer. +These checks do not measure physical input latency. + +Generate `compile_commands.json` with `pio run -e jz-stm32-leverless -t compiledb` for editor include paths. +Build again after this target before checking the ELF file. +The compilation database captures vector definitions before the existing post-build configuration script applies its override. +Use the linked startup instructions to verify the runtime vector address. + +## Verified comparison + +| Property | Rebuilt baseline `184a86f` | First USB candidate | +| --- | --- | --- | +| Linked wired IN interval | 4 ms | 1 ms | +| Linked wired OUT interval | 8 ms | 8 ms | +| Backend submission call | `USBXBox360Controller::send()` | `x360_tx()` | +| Flash usage, text plus data | 53,336 bytes | 53,364 bytes | +| Static RAM, data plus BSS | 6,176 bytes | 6,176 bytes | +| Application address | `0x08001000` | `0x08001000` | +| Initial stack pointer | `0x20017ff8` | `0x20017ff8` | + +All seven descriptor regression tests fail against the original patch script and pass against the replacement. +A build with fresh library dependencies passes. +The candidate's preprocessed USB source defines `BOARD_USB_DISC_DEV` as `NULL`. +The linked startup passes `0x08001000` to `nvic_init`. +Pyright passes with the verification environment selected. +For the first USB change, Lizard reports a maximum complexity of seven. + +## Device check + +Windows binds the controller to the built-in Xbox 360 driver through its per-device driver picker. +The DFU download writes 53,364 application bytes starting at `0x08001000`. +The update does not write the first 4 KiB bootloader region. +The device remains in DFU after the leave request during this test. +A normal power cycle starts the candidate and restores Windows XInput on slot zero. +B22/PC12 reports the A button as `0x1000` while held. +Releasing B22 restores the button mask to `0x0000`. + +The application readback attempt returns zero bytes. +No usable device backup exists from that attempt. +The rebuilt baseline is a source comparison artifact, not a byte-for-byte copy of the previous application. + +## SOCD mode-configuration correction + +The FGC toggle updates its configuration in place without rebinding the active mode. +The corrected image uses 53,340 bytes of flash and 6,176 bytes of static RAM. +Disassembly shows the loop's mode-rebinding call before the fix and its absence after the fix. +The compiled loop shrinks from 196 to 172 bytes. +Lizard reports a loop complexity of nine. +The seven descriptor tests and the ELF checks pass for the corrected build. + +The corrected application is flashed at `0x08001000` and starts after a normal power cycle. +The live regression sequence selects Ultimate, cycles FGC SOCD, then presses Left followed by Right while holding both. +XInput reports LX = 25,828, confirming Right wins instead of resolving to neutral at 128. +A subsequent power cycle restores the default FGC configuration before the input capture. + +## Windows A-button capture + +A 20-second capture samples XInput 79,006 times during ten requested slow taps. +It records ten presses and ten releases, with no additional A-button transitions. +The trace contains no XInput errors or other button inputs. +It begins and ends with all buttons released. +Reported holds range from 110.962 to 156.052 ms. +The shortest reported interval between a release and the next press is 789.208 ms. +The host sampler's largest polling gap is 0.4586 ms; its p99 gap is 0.2584 ms. +These gap values measure the host sampler's cadence. +Physical input latency and electrical contact bounce remain unmeasured. + +## Remaining acceptance checks + +- Exercise every mapped button, simultaneous directions, and independent releases of duplicate actions. +- Verify suspend/resume and a sustained input workload. +- Measure physical input-to-report timing with a correlated timebase. +- Measure mechanical contact bounce before selecting a debounce policy. +- Specify the minimum supported pulse width and test pulses between USB polls. +- Exercise every SOCD policy and its direction-release sequences. + +Input scanning continues while the endpoint is busy. +The next successful submission uses the latest sampled state. +A press and release entirely between successful reports can remain unreported. +The candidate does not add pulse latching or mechanical debounce. +Graphical configuration and multiple profiles remain follow-up work. + +## Bootloader follow-up + +Retain the current 4 KiB DFU design for this board. +Evaluate `ENABLE_DFU_UPLOAD` for backups and flash verification. +Reconcile the advertised 1 KiB erase regions with the erase code's 2 KiB page handling. +Evaluate image validation together with its required application metadata. +These changes require separate bootloader work and testing. diff --git a/config/jz-stm32-leverless/config.cpp b/config/jz-stm32-leverless/config.cpp index c6fb9a9a..057967c5 100644 --- a/config/jz-stm32-leverless/config.cpp +++ b/config/jz-stm32-leverless/config.cpp @@ -139,11 +139,6 @@ void loop() { GameModeConfig *fgc_cfg = find_fgc_config(config); 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) { - current_mode->SetConfig(*fgc_cfg); - } } } combo_was_held = combo_held; diff --git a/config/jz-stm32-leverless/env.ini b/config/jz-stm32-leverless/env.ini index 836c8187..246b0a18 100644 --- a/config/jz-stm32-leverless/env.ini +++ b/config/jz-stm32-leverless/env.ini @@ -7,7 +7,7 @@ board_upload.maximum_ram_size = 98304 board_upload.offset_address = 0x08001000 board_build.ldscript = stm32f103re_bootloader.ld extra_scripts = - pre:config/jz-stm32-leverless/patch_usb_polling.py + post: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 @@ -15,7 +15,7 @@ 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 + -include config/jz-stm32-leverless/usb_disc_override.h build_src_filter = ${stm32_base.build_src_filter} + diff --git a/config/jz-stm32-leverless/patch_usb_polling.py b/config/jz-stm32-leverless/patch_usb_polling.py index 779f3de2..49f76cdf 100644 --- a/config/jz-stm32-leverless/patch_usb_polling.py +++ b/config/jz-stm32-leverless/patch_usb_polling.py @@ -1,18 +1,32 @@ -Import("env") -import os, re +from pathlib import Path +import 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") +def interval_field(source, endpoint, allowed): + pattern = r"\." + endpoint + r"\s*=\s*\{[^{}]*?\.bInterval\s*=\s*(\d+)\s*," + matches = list(re.finditer(pattern, source)) + if len(matches) != 1: + raise RuntimeError(f"USBComposite: expected one {endpoint} interval") + match = matches[0] + if match.group(1) not in allowed: + raise RuntimeError(f"USBComposite: unexpected {endpoint} interval {match.group(1)}") + return match.span(1) -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)") + +globals()["Import"]("env") +env = globals()["env"] + +# USBXBox360 uses the wired descriptor; keep host output polling unchanged. +target = ( + Path(env.subst("$PROJECT_LIBDEPS_DIR")) + / env["PIOENV"] + / "USBComposite for STM32F1" + / "usb_multi_x360.c" +) +source = target.read_text() +start, end = interval_field(source, "DataInEndpoint", {"1", "4"}) +interval_field(source, "DataOutEndpoint", {"8"}) +patched = source[:start] + "1" + source[end:] +if patched != source: + target.write_text(patched) +print("Verified wired XInput polling: IN 1 ms, OUT 8 ms") diff --git a/config/jz-stm32-leverless/usb_disc_override.h b/config/jz-stm32-leverless/usb_disc_override.h new file mode 100644 index 00000000..d8a07b42 --- /dev/null +++ b/config/jz-stm32-leverless/usb_disc_override.h @@ -0,0 +1,8 @@ +#pragma once + +#ifndef __ASSEMBLER__ +#include +// PC12 belongs to B22, so USB initialization must not drive it. +#undef BOARD_USB_DISC_DEV +#define BOARD_USB_DISC_DEV NULL +#endif diff --git a/platformio.ini b/platformio.ini index d90ed573..30dd634c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -82,9 +82,12 @@ lib_deps = https://github.com/JonnyHaystack/ArduinoKeyboard/archive/refs/tags/1.0.5.zip [stm32_base] -platform = ststm32 +platform = ststm32@20.0.0 framework = arduino board_build.core = maple +platform_packages = + platformio/framework-arduinoststm32-maple@3.10000.201129 + platformio/toolchain-gccarmnoneeabi@1.70201.0 build_unflags = -std=gnu++11 -DSERIAL_USB @@ -110,8 +113,9 @@ custom_nanopb_options = ${env.custom_nanopb_options} --options-file ../../../../HAL/avr/proto/config.options lib_deps = - ${env.lib_deps} - arpruss/USBComposite for STM32F1 + nanopb/Nanopb@0.4.92 + https://github.com/JonnyHaystack/HayBox-proto/archive/refs/tags/v0.1.0.zip + arpruss/USBComposite for STM32F1@1.6.0 [arduino_pico_base] platform = https://github.com/maxgerhardt/platform-raspberrypi.git#5e87ae34ca025274df25b3303e9e9cb6c120123c diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..ab3c4870 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1 @@ +pyelftools==0.32 diff --git a/tests/test_stm32_usb_polling.py b/tests/test_stm32_usb_polling.py new file mode 100644 index 00000000..0419cabe --- /dev/null +++ b/tests/test_stm32_usb_polling.py @@ -0,0 +1,90 @@ +import contextlib +import io +from pathlib import Path +import runpy +import tempfile +import unittest + + +SCRIPT = ( + Path(__file__).resolve().parents[1] + / "config/jz-stm32-leverless/patch_usb_polling.py" +) +DESCRIPTOR = """\ +.DataInEndpoint = { + .bInterval = 4, +}, +.DataOutEndpoint = { + .bInterval = 8, +}, +""" + + +class BuildEnvironment(dict): + def subst(self, value): + return self[value] + + +class UsbPollingTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + root = Path(temporary.name) + self.env = BuildEnvironment( + PIOENV="jz-stm32-leverless", + **{"$PROJECT_LIBDEPS_DIR": str(root)}, + ) + package = root / self.env["PIOENV"] / "USBComposite for STM32F1" + package.mkdir(parents=True) + self.wired = package / "usb_multi_x360.c" + self.wireless = package / "usb_x360w.c" + self.wired.write_text(DESCRIPTOR) + self.wireless.write_text(DESCRIPTOR) + + def run_patch(self): + with contextlib.redirect_stdout(io.StringIO()): + runpy.run_path( + str(SCRIPT), + init_globals={"env": self.env, "Import": lambda name: None}, + ) + + def test_patches_only_wired_input_interval(self): + self.run_patch() + self.assertEqual( + self.wired.read_text(), DESCRIPTOR.replace("bInterval = 4", "bInterval = 1") + ) + self.assertEqual(self.wireless.read_text(), DESCRIPTOR) + + def test_repeated_build_preserves_patched_file(self): + self.run_patch() + modified = self.wired.stat().st_mtime_ns + self.run_patch() + self.assertEqual(self.wired.stat().st_mtime_ns, modified) + self.assertIn("bInterval = 1", self.wired.read_text()) + + def test_missing_wired_source_fails_build(self): + self.env["PIOENV"] = "missing-environment" + with self.assertRaises(FileNotFoundError): + self.run_patch() + + def assert_rejected(self, source): + self.wired.write_text(source) + with self.assertRaises(RuntimeError): + self.run_patch() + self.assertEqual(self.wired.read_text(), source) + + def test_missing_input_field_fails_build(self): + self.assert_rejected(DESCRIPTOR.replace("DataInEndpoint", "RenamedEndpoint")) + + def test_duplicate_input_field_fails_build(self): + self.assert_rejected(DESCRIPTOR + ".DataInEndpoint = { .bInterval = 4, },\n") + + def test_unexpected_input_interval_fails_build(self): + self.assert_rejected(DESCRIPTOR.replace("bInterval = 4", "bInterval = 2")) + + def test_unexpected_output_interval_fails_build(self): + self.assert_rejected(DESCRIPTOR.replace("bInterval = 8", "bInterval = 4")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/verify_stm32_firmware.py b/tests/verify_stm32_firmware.py new file mode 100644 index 00000000..05ad488a --- /dev/null +++ b/tests/verify_stm32_firmware.py @@ -0,0 +1,91 @@ +import argparse +import hashlib +import json +from pathlib import Path +import re +import struct +import subprocess + +from elftools.elf.elffile import ELFFile + + +def symbol_bytes(elf, name): + symbols = elf.get_section_by_name(".symtab").get_symbol_by_name(name) + if not symbols or len(symbols) != 1: + raise RuntimeError(f"Expected one ELF symbol: {name}") + symbol = symbols[0] + section = elf.get_section(symbol["st_shndx"]) + offset = symbol["st_value"] - section["sh_addr"] + return section.data()[offset : offset + symbol["st_size"]] + + +def endpoint_intervals(descriptor): + intervals = {} + offset = 0 + while offset < len(descriptor): + length, kind = descriptor[offset : offset + 2] + if length < 2 or offset + length > len(descriptor): + raise RuntimeError("Invalid linked USB descriptor length") + if kind == 5: + address = descriptor[offset + 2] + direction = "IN" if address & 0x80 else "OUT" + if direction in intervals: + raise RuntimeError(f"Duplicate {direction} endpoint") + intervals[direction] = descriptor[offset + 6] + offset += length + return intervals + + +def verify_report_path(elf, path, objdump, baseline): + symbols = elf.get_section_by_name(".symtab") + function = next( + symbol for symbol in symbols.iter_symbols() + if symbol.name == "_ZN13XInputBackend10SendReportEv" + ) + address = function["st_value"] & ~1 + assembly = subprocess.check_output( + [ + objdump, "-d", "-C", f"--start-address={address}", + f"--stop-address={address + function['st_size']}", str(path), + ], + text=True, + ) + targets = set(re.findall(r"\b(?:bl|b\.w)\s+[0-9a-f]+\s+<([^>]+)>", assembly)) + blocking = "USBXBox360Controller::send()" in targets + nonblocking = "x360_tx" in targets + if baseline: + assert blocking and not nonblocking, targets + else: + assert nonblocking and not blocking, targets + return "blocking" if blocking else "non-blocking" + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("elf", type=Path) + parser.add_argument("--objdump", default="arm-none-eabi-objdump") + parser.add_argument("--baseline", action="store_true") + args = parser.parse_args() + with args.elf.open("rb") as stream: + elf = ELFFile(stream) + intervals = endpoint_intervals(symbol_bytes(elf, "X360Descriptor_Config")) + assert intervals == {"IN": 4 if args.baseline else 1, "OUT": 8}, intervals + text = elf.get_section_by_name(".text") + assert text is not None, "ELF has no .text section" + assert text["sh_addr"] == 0x08001000, text["sh_addr"] + stack, reset = struct.unpack("