diff --git a/README.md b/README.md index fc4495b..68d7381 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # Pico-Game-Controller -WIP code for a rhythm game controller using a Raspberry Pi Pico. Intended for SDVX or IIDX but is capable of handling 11 buttons, 11 LEDs, and 2 encoders. +WIP code for a rhythm game controller using a Raspberry Pi Pico. Intended for SDVX or IIDX but is capable of handling 11 buttons, 10 LEDs, 1 WS2812B RGB strip, and 2 encoders. Currently working/fixed: - Gamepad mode - default boot mode - NKRO Keyboard & Mouse Mode - hold first button to enter kb mode - HID LEDs with Reactive LED fallback +- ws2812b rgb on second core +- 2 ws2812b hid descriptor zones - sdvx/iidx spoof - Tested on EAC - 1000hz polling - Reversable Encoders with debouncing @@ -17,9 +19,7 @@ TODO: - store configuration settings in a text file? consider implementing littlefs https://github.com/littlefs-project/littlefs https://www.raspberrypi.org/forums/viewtopic.php?t=313009 https://www.raspberrypi.org/forums/viewtopic.php?p=1894014#p1894014 - Store last mode in flash memory https://www.raspberrypi.org/forums/viewtopic.php?t=305570 -- debounce -- ws2812b rgb on second core? -- split input updating into pi pico's second core? +- debounce on switches How to Use: @@ -39,4 +39,4 @@ Thanks to: - https://github.com/Drewol/rp2040-gamecon for usb gamepad descriptor info. - https://github.com/veroxzik/arduino-konami-spoof for konami spoof usb descriptor info. - https://github.com/veroxzik/roxy-firmware for nkro descriptor and logic info. -- KyubiFox for bringing clkdiv to my attention for encoder debouncing \ No newline at end of file +- KyubiFox for bringing clkdiv to my attention for encoder debouncing diff --git a/build_uf2/Pico_Game_Controller.uf2 b/build_uf2/Pico_Game_Controller.uf2 index 87b216c..7b69800 100644 Binary files a/build_uf2/Pico_Game_Controller.uf2 and b/build_uf2/Pico_Game_Controller.uf2 differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c5cc490..14091d5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,9 +5,11 @@ add_executable(Pico_Game_Controller target_include_directories(Pico_Game_Controller PRIVATE ${CMAKE_CURRENT_LIST_DIR}) pico_generate_pio_header(Pico_Game_Controller ${CMAKE_CURRENT_LIST_DIR}/encoders.pio) +pico_generate_pio_header(Pico_Game_Controller ${CMAKE_CURRENT_LIST_DIR}/ws2812.pio) target_sources(Pico_Game_Controller PRIVATE pico_game_controller.c) target_link_libraries(Pico_Game_Controller PRIVATE + pico_multicore pico_stdlib tinyusb_device tinyusb_board diff --git a/src/pico_game_controller.c b/src/pico_game_controller.c index aa6848c..6ecc322 100644 --- a/src/pico_game_controller.c +++ b/src/pico_game_controller.c @@ -9,21 +9,28 @@ #include "bsp/board.h" #include "encoders.pio.h" +#include "hardware/clocks.h" #include "hardware/dma.h" #include "hardware/irq.h" #include "hardware/pio.h" +#include "pico/multicore.h" #include "pico/stdlib.h" #include "tusb.h" #include "usb_descriptors.h" +#include "ws2812.pio.h" -#define SW_GPIO_SIZE 11 // Number of switches -#define ENC_GPIO_SIZE 2 // Number of encoders -#define ENC_PPR 600 // Encoder PPR -#define ENC_DEBOUNCE true // Encoder Debouncing - +#define SW_GPIO_SIZE 11 // Number of switches +#define LED_GPIO_SIZE 10 // Number of switches +#define ENC_GPIO_SIZE 2 // Number of encoders +#define ENC_PPR 600 // Encoder PPR +#define ENC_DEBOUNCE true // Encoder Debouncing #define ENC_PULSE (ENC_PPR * 4) // 4 pulses per PPR #define ENC_ROLLOVER (ENC_PULSE * 2) // Delta Rollover threshold #define REACTIVE_TIMEOUT_MAX 100000 // Cycles before HID falls back to reactive +#define WS2812B_LED_SIZE 10 // Number of WS2812B LEDs +#define WS2812B_LED_ZONES 2 // Number of WS2812B LED Zones +#define WS2812B_LEDS_PER_ZONE \ + WS2812B_LED_SIZE / WS2812B_LED_ZONES // Number of LEDs per zone // MODIFY KEYBINDS HERE, MAKE SURE LENGTHS MATCH SW_GPIO_SIZE const uint8_t SW_KEYCODE[] = {HID_KEY_D, HID_KEY_F, HID_KEY_J, HID_KEY_K, @@ -33,12 +40,13 @@ const uint8_t SW_GPIO[] = { 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 27, }; const uint8_t LED_GPIO[] = { - 5, 7, 9, 11, 13, 15, 17, 19, 21, 26, 28, + 5, 7, 9, 11, 13, 15, 17, 19, 21, 26, }; const uint8_t ENC_GPIO[] = {0, 2}; // L_ENC(0, 1); R_ENC(2, 3) const bool ENC_REV[] = {false, false}; // Reverse Encoders +const uint8_t WS2812B_GPIO = 28; -PIO pio; +PIO pio, pio_1; uint32_t enc_val[ENC_GPIO_SIZE]; uint32_t prev_enc_val[ENC_GPIO_SIZE]; int cur_enc_val[ENC_GPIO_SIZE]; @@ -53,10 +61,76 @@ unsigned long reactive_timeout_count = REACTIVE_TIMEOUT_MAX; void (*loop_mode)(); -struct lights_report { - uint8_t buttons[11]; +typedef struct { + uint8_t r, g, b; +} RGB_t; + +union { + struct { + uint8_t buttons[LED_GPIO_SIZE]; + RGB_t rgb[WS2812B_LED_ZONES]; + } lights; + uint8_t raw[LED_GPIO_SIZE + WS2812B_LED_ZONES * 3]; } lights_report; +/** + * WS2812B RGB Assignment + * @param pixel_grb The pixel color to set + **/ +static inline void put_pixel(uint32_t pixel_grb) { + pio_sm_put_blocking(pio1, ENC_GPIO_SIZE, pixel_grb << 8u); +} + +/** + * WS2812B RGB Format Helper + **/ +static inline uint32_t urgb_u32(uint8_t r, uint8_t g, uint8_t b) { + return ((uint32_t)(r) << 8) | ((uint32_t)(g) << 16) | (uint32_t)(b); +} + +/** + * Color Wheel Picker + * @param wheel_pos Color value from 0-255, r->g->b->r... + **/ +uint32_t color_wheel(uint8_t wheel_pos) { + if (wheel_pos < 85) { + return urgb_u32(wheel_pos * 3, 255 - wheel_pos * 3, 0); + } else if (wheel_pos < 170) { + wheel_pos -= 85; + return urgb_u32(255 - wheel_pos * 3, 0, wheel_pos * 3); + } else { + wheel_pos -= 170; + return urgb_u32(0, wheel_pos * 3, 255 - wheel_pos * 3); + } +} + +/** + * Color cycle effect + **/ +void ws2812b_color_cycle(uint32_t counter) { + for (int i = 0; i < WS2812B_LED_SIZE; ++i) { + put_pixel(color_wheel((counter + i * (int)(255 / WS2812B_LED_SIZE)) % 256)); + } +} + +/** + * WS2812B Lighting + * @param counter Current number of WS2812B cycles + **/ +void ws2812b_update(uint32_t counter) { + if (reactive_timeout_count >= REACTIVE_TIMEOUT_MAX) { + ws2812b_color_cycle(counter); + } else { + for (int i = 0; i < WS2812B_LED_ZONES; i++) { + for (int j = 0; j < WS2812B_LEDS_PER_ZONE; j++) { + put_pixel(urgb_u32(lights_report.lights.rgb[i].r, + lights_report.lights.rgb[i].g, + lights_report.lights.rgb[i].b)); + } + } + } +} + /** * HID/Reactive Lights **/ @@ -65,7 +139,7 @@ void update_lights() { reactive_timeout_count++; } if (leds_changed) { - for (int i = 0; i < SW_GPIO_SIZE; i++) { + for (int i = 0; i < LED_GPIO_SIZE; i++) { if (reactive_timeout_count >= REACTIVE_TIMEOUT_MAX) { if (sw_val[i]) { gpio_put(LED_GPIO[i], 1); @@ -73,7 +147,7 @@ void update_lights() { gpio_put(LED_GPIO[i], 0); } } else { - if (lights_report.buttons[i] == 0) { + if (lights_report.lights.buttons[i] == 0) { gpio_put(LED_GPIO[i], 0); } else { gpio_put(LED_GPIO[i], 1); @@ -267,6 +341,7 @@ void init() { // Set up the state machine for encoders pio = pio0; uint offset = pio_add_program(pio, &encoders_program); + // Setup Encoders for (int i = 0; i < ENC_GPIO_SIZE; i++) { enc_val[i] = 0; @@ -290,6 +365,12 @@ void init() { dma_channel_set_irq0_enabled(i, true); } + // Set up WS2812B + pio_1 = pio1; + uint offset2 = pio_add_program(pio_1, &ws2812_program); + ws2812_program_init(pio_1, ENC_GPIO_SIZE, offset2, WS2812B_GPIO, 800000, + false); + // Setup Button GPIO for (int i = 0; i < SW_GPIO_SIZE; i++) { sw_val[i] = false; @@ -301,7 +382,7 @@ void init() { } // Setup LED GPIO - for (int i = 0; i < SW_GPIO_SIZE; i++) { + for (int i = 0; i < LED_GPIO_SIZE; i++) { gpio_init(LED_GPIO[i]); gpio_set_dir(LED_GPIO[i], GPIO_OUT); } @@ -319,6 +400,17 @@ void init() { } } +/** + * Second Core Runnable + **/ +void core1_entry() { + uint32_t counter = 0; + while (1) { + ws2812b_update(++counter); + sleep_ms(10); + } +} + /** * Main Loop Function **/ @@ -327,6 +419,8 @@ int main(void) { tusb_init(); init(); + multicore_launch_core1(core1_entry); + while (1) { tud_task(); // tinyusb device task update_inputs(); @@ -364,7 +458,7 @@ void tud_hid_set_report_cb(uint8_t itf, uint8_t report_id, { size_t i = 0; for (i; i < sizeof(lights_report); i++) { - lights_report.buttons[i] = buffer[i + 1]; + lights_report.raw[i] = buffer[i + 1]; } reactive_timeout_count = 0; leds_changed = true; diff --git a/src/usb_descriptors.h b/src/usb_descriptors.h index 7b65e53..112196e 100644 --- a/src/usb_descriptors.h +++ b/src/usb_descriptors.h @@ -35,11 +35,11 @@ enum { #define GAMECON_REPORT_DESC_LIGHTS(...) \ HID_USAGE_PAGE(HID_USAGE_PAGE_DESKTOP), HID_USAGE(0x00), \ HID_COLLECTION(HID_COLLECTION_APPLICATION), \ - __VA_ARGS__ HID_REPORT_COUNT(11), /*11 button lights*/ \ + __VA_ARGS__ HID_REPORT_COUNT(16), /*10 button lights + 2 RGB Sets*/ \ HID_REPORT_SIZE(8), HID_LOGICAL_MIN(0x00), HID_LOGICAL_MAX_N(0x00ff, 2), \ HID_USAGE_PAGE(HID_USAGE_PAGE_ORDINAL), HID_USAGE_MIN(1), \ - HID_USAGE_MAX(11), HID_OUTPUT(HID_DATA | HID_VARIABLE | HID_ABSOLUTE), \ - HID_REPORT_COUNT(1), HID_REPORT_SIZE(40), /*Padding*/ \ + HID_USAGE_MAX(16), HID_OUTPUT(HID_DATA | HID_VARIABLE | HID_ABSOLUTE), \ + HID_REPORT_COUNT(1), HID_REPORT_SIZE(8), /*Padding*/ \ HID_INPUT(HID_CONSTANT | HID_VARIABLE | HID_ABSOLUTE), \ HID_COLLECTION_END diff --git a/src/ws2812.pio b/src/ws2812.pio new file mode 100644 index 0000000..3c31fd6 --- /dev/null +++ b/src/ws2812.pio @@ -0,0 +1,85 @@ +; +; Copyright (c) 2020 Raspberry Pi (Trading) Ltd. +; +; SPDX-License-Identifier: BSD-3-Clause +; + +.program ws2812 +.side_set 1 + +.define public T1 2 +.define public T2 5 +.define public T3 3 + +.lang_opt python sideset_init = pico.PIO.OUT_HIGH +.lang_opt python out_init = pico.PIO.OUT_HIGH +.lang_opt python out_shiftdir = 1 + +.wrap_target +bitloop: + out x, 1 side 0 [T3 - 1] ; Side-set still takes place when instruction stalls + jmp !x do_zero side 1 [T1 - 1] ; Branch on the bit we shifted out. Positive pulse +do_one: + jmp bitloop side 1 [T2 - 1] ; Continue driving high, for a long pulse +do_zero: + nop side 0 [T2 - 1] ; Or drive low, for a short pulse +.wrap + +% c-sdk { +#include "hardware/clocks.h" + +static inline void ws2812_program_init(PIO pio, uint sm, uint offset, uint pin, float freq, bool rgbw) { + + pio_gpio_init(pio, pin); + pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); + + pio_sm_config c = ws2812_program_get_default_config(offset); + sm_config_set_sideset_pins(&c, pin); + sm_config_set_out_shift(&c, false, true, rgbw ? 32 : 24); + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); + + int cycles_per_bit = ws2812_T1 + ws2812_T2 + ws2812_T3; + float div = clock_get_hz(clk_sys) / (freq * cycles_per_bit); + sm_config_set_clkdiv(&c, div); + + pio_sm_init(pio, sm, offset, &c); + pio_sm_set_enabled(pio, sm, true); +} +%} + +.program ws2812_parallel + +.define public T1 2 +.define public T2 5 +.define public T3 3 + +.wrap_target + out x, 32 + mov pins, !null [T1-1] + mov pins, x [T2-1] + mov pins, null [T3-2] +.wrap + +% c-sdk { +#include "hardware/clocks.h" + +static inline void ws2812_parallel_program_init(PIO pio, uint sm, uint offset, uint pin_base, uint pin_count, float freq) { + for(uint i=pin_base; i