From 57074ac3e790ca5b1446aaa3aaef5b5f60afbabe Mon Sep 17 00:00:00 2001 From: Fefedu973 <80718477+Fefedu973@users.noreply.github.com> Date: Sat, 22 Aug 2026 22:00:05 +0200 Subject: [PATCH 1/2] feat: add RGB and HID gamepad support --- README.md | 39 +++- hardware/stm32f723xx/board_def.h | 28 +++ include/commands.h | 17 ++ include/eeconfig.h | 68 +++++- include/hardware/rgb_api.h | 21 ++ include/rgb.h | 45 ++++ include/tusb_config.h | 4 +- include/usb_descriptors.h | 13 +- keyboards/kbhe-75he/keyboard.json | 193 ++++++++++++++++ scripts/drivers.py | 61 ++++++ scripts/make.py | 15 ++ scripts/metadata.py | 42 +++- scripts/schema/keyboard.py | 16 ++ scripts/utils.py | 2 + scripts/validate.py | 5 + src/commands.c | 156 ++++++++++++- src/eeconfig.c | 2 + src/hardware/stm32f723xx/analog.c | 255 ++++++++++++++++++++++ src/hardware/stm32f723xx/board.c | 158 ++++++++++++++ src/hardware/stm32f723xx/crc32.c | 31 +++ src/hardware/stm32f723xx/flash.c | 54 +++++ src/hardware/stm32f723xx/rgb.c | 189 ++++++++++++++++ src/hardware/stm32f723xx/timer.c | 14 ++ src/layout.c | 4 +- src/main.c | 12 + src/migration.c | 61 +++++- src/rgb.c | 352 ++++++++++++++++++++++++++++++ src/usb_descriptors.c | 122 ++++++++--- src/xinput.c | 175 ++++++++++++--- tools/rgb_core_test.c | 195 +++++++++++++++++ tools/test_include/eeconfig.h | 28 +++ 31 files changed, 2292 insertions(+), 85 deletions(-) create mode 100644 hardware/stm32f723xx/board_def.h create mode 100644 include/hardware/rgb_api.h create mode 100644 include/rgb.h create mode 100644 keyboards/kbhe-75he/keyboard.json create mode 100644 src/hardware/stm32f723xx/analog.c create mode 100644 src/hardware/stm32f723xx/board.c create mode 100644 src/hardware/stm32f723xx/crc32.c create mode 100644 src/hardware/stm32f723xx/flash.c create mode 100644 src/hardware/stm32f723xx/rgb.c create mode 100644 src/hardware/stm32f723xx/timer.c create mode 100644 src/rgb.c create mode 100644 tools/rgb_core_test.c create mode 100644 tools/test_include/eeconfig.h diff --git a/README.md b/README.md index 384e3e9..a00f1da 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,14 @@ This repository contains libraries for building a Hall-effect keyboard firmware. - [x] **Web Configurator**: Configure the firmware using [hmkconf](https://github.com/peppapighs/hmkconf) without needing to recompile the firmware. - [x] **Tick Rate**: Customizable tick rate for Tap-Hold and Dynamic Keystroke. - [x] **8kHz Polling Rate**: Support for 8kHz polling rate on some microcontrollers (e.g., AT32F405xx). -- [x] **Gamepad**: Support for XInput gamepad mode, allowing the keyboard to be used as a game controller. +- [x] **Gamepad**: Selectable XInput or standards-based HID gamepad output. +- [x] **RGB Lighting**: Optional non-blocking backend, basic effects, brightness/color persistence and host-controlled per-LED frames. ## Limitations -- **RGB Lighting**: The firmware does not support RGB lighting. +- **RGB Backends**: Addressable lighting is opt-in and each MCU family must + provide a non-blocking implementation of `include/hardware/rgb_api.h`. The + first backend supports WS2812 LEDs on STM32F723 TIM2 channel 1. ## Getting Started @@ -59,6 +62,38 @@ This repository contains libraries for building a Hall-effect keyboard firmware. 6. Flash the firmware to your keyboard using your preferred method (e.g., DFU, ISP). If your keyboard has a DFU bootloader, you can set `upload_protocol = dfu` in `platformio.ini` and use the command `pio run --target upload` or the PlatformIO IDE's "Upload" option while the keyboard is in DFU mode. If your browser supports WebUSB, you can also use [WebUSB DFU](https://devanlai.github.io/webdfu/dfu-util/) (Recommended method). +### RGB lighting + +Add an `rgb` object to `keyboard.json` to opt in. It defines the LED count, +data pin, backend, default brightness and default color. Non-RGB keyboards keep +the same source path and do not allocate frame buffers or schedule an RGB task. + +The portable core provides static, breathing, rainbow, rainbow-wave and live +effects. Live mode accepts atomic 60-byte RAW HID chunks: a new frame is shown +only after all chunks arrive, and live frames are never written to flash. The +wire protocol is documented by the command IDs in `include/commands.h`; hosts +must first negotiate `COMMAND_GET_RGB_CAPABILITIES` (`0x7F`). + +`LED_FILL` changes the persistent base color used by static and breathing +effects; rainbow effects keep their generated colors. Combined with the static +effect it produces an arbitrary persistent solid color. Only arbitrary per-LED +frames require live mode. + +Backends must copy a submitted logical RGB frame and return immediately. DMA or +another asynchronous peripheral mechanism should perform the physical transfer +so lighting cannot stall matrix processing. + +### Gamepad API + +The two low bits in `eeconfig_options_t` select one mutually exclusive gamepad +API: legacy XInput or a standard TinyUSB HID gamepad. HID is the portable choice +for operating systems that do not provide the XInput driver. Changing the API +changes USB descriptors and therefore takes effect after USB re-enumeration or +a reboot. Existing configurations remain compatible because HID uses a formerly +reserved option bit and XInput keeps priority if malformed legacy data sets both. +The v1.6 migration clears that bit's historical unused value, so upgrading an +existing device cannot enable HID accidentally. + ## Development The development branch is `dev`, which contains the latest features and bug fixes. The corresponding `dev` branch of [hmkconf](https://github.com/peppapighs/hmkconf/tree/dev) deployed at [https://dev.hmkconf.com](https://dev.hmkconf.com) is required to configure the `dev` branch of the firmware. To contribute, please create a pull request against the `dev` branch. diff --git a/hardware/stm32f723xx/board_def.h b/hardware/stm32f723xx/board_def.h new file mode 100644 index 0000000..9536319 --- /dev/null +++ b/hardware/stm32f723xx/board_def.h @@ -0,0 +1,28 @@ +/* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#pragma once + +/* TinyUSB root port 1 is the F723's DWC2 high-speed controller with its + * integrated PHY (PB14/PB15). */ +#define CFG_TUSB_RHPORT1_MODE (OPT_MODE_DEVICE | OPT_MODE_HIGH_SPEED) + +#if !defined(ADC_NUM_SAMPLE_CYCLES) +#define ADC_NUM_SAMPLE_CYCLES ADC_SAMPLETIME_3CYCLES +#endif + +#if ADC_RESOLUTION == 12 +#define ADC_RESOLUTION_HAL ADC_RESOLUTION_12B +#elif ADC_RESOLUTION == 10 +#define ADC_RESOLUTION_HAL ADC_RESOLUTION_10B +#elif ADC_RESOLUTION == 8 +#define ADC_RESOLUTION_HAL ADC_RESOLUTION_8B +#elif ADC_RESOLUTION == 6 +#define ADC_RESOLUTION_HAL ADC_RESOLUTION_6B +#else +#error "Unsupported ADC resolution" +#endif diff --git a/include/commands.h b/include/commands.h index 051fe54..0661ccd 100644 --- a/include/commands.h +++ b/include/commands.h @@ -41,6 +41,23 @@ typedef enum { COMMAND_GET_SERIAL, COMMAND_SAVE_CALIBRATION_THRESHOLD, + /* KBHE/libhmk RGB bridge v1. Requests and responses in this range use the + * KBHE layout: byte 0 command, byte 1 status/reserved, payload at byte 2. */ + COMMAND_GET_LED_ENABLED = 0x60, + COMMAND_SET_LED_ENABLED = 0x61, + COMMAND_GET_LED_BRIGHTNESS = 0x62, + COMMAND_SET_LED_BRIGHTNESS = 0x63, + COMMAND_GET_LED_PIXEL = 0x64, + COMMAND_SET_LED_PIXEL = 0x65, + COMMAND_GET_LED_ALL = 0x68, + COMMAND_SET_LED_ALL_CHUNK = 0x6A, + COMMAND_LED_CLEAR = 0x6B, + COMMAND_LED_FILL = 0x6C, + COMMAND_GET_LED_EFFECT = 0x6E, + COMMAND_SET_LED_EFFECT = 0x6F, + COMMAND_RESTORE_LED_EFFECT = 0x76, + COMMAND_GET_RGB_CAPABILITIES = 0x7F, + COMMAND_GET_KEYMAP = 128, COMMAND_SET_KEYMAP, COMMAND_GET_ACTUATION_MAP, diff --git a/include/eeconfig.h b/include/eeconfig.h index 47b443a..4bc107e 100644 --- a/include/eeconfig.h +++ b/include/eeconfig.h @@ -47,7 +47,10 @@ typedef union __attribute__((packed)) { struct __attribute__((packed)) { // Whether the XInput interface is enabled bool xinput_enabled : 1; - bool _unused0 : 1; + // Whether a standards-based HID gamepad interface is enabled. This is + // mutually exclusive with XInput and reuses a previously reserved bit. The + // v1.6 migration clears its historical value before enabling this meaning. + bool hid_gamepad_enabled : 1; // Whether 8kHz polling rate is enabled. Only applicable if USB HS is // enabled. If disabled, the 1kHz polling rate is used instead. bool high_polling_rate_enabled : 1; @@ -60,6 +63,41 @@ typedef union __attribute__((packed)) { _Static_assert(sizeof(eeconfig_options_t) == sizeof(uint16_t), "Invalid eeconfig_options_t size"); +typedef enum { + GAMEPAD_API_DISABLED = 0, + GAMEPAD_API_XINPUT, + GAMEPAD_API_HID, +} gamepad_api_t; + +static inline gamepad_api_t +eeconfig_get_gamepad_api(const eeconfig_options_t *options) { + if (options == NULL) + return GAMEPAD_API_DISABLED; + /* Preserve the legacy bit's priority if a malformed host sets both bits. */ + if (options->xinput_enabled) + return GAMEPAD_API_XINPUT; + return options->hid_gamepad_enabled ? GAMEPAD_API_HID + : GAMEPAD_API_DISABLED; +} + +static inline bool +eeconfig_set_gamepad_api(eeconfig_options_t *options, gamepad_api_t api) { + if (options == NULL || api > GAMEPAD_API_HID) + return false; + options->xinput_enabled = api == GAMEPAD_API_XINPUT; + options->hid_gamepad_enabled = api == GAMEPAD_API_HID; + return true; +} + +typedef struct __attribute__((packed)) { + uint8_t enabled; + uint8_t brightness; + uint8_t effect; + uint8_t color_r; + uint8_t color_g; + uint8_t color_b; +} eeconfig_rgb_t; + // Keyboard profile configuration typedef struct __attribute__((packed)) { uint8_t keymap[NUM_LAYERS][NUM_KEYS]; @@ -74,7 +112,7 @@ typedef struct __attribute__((packed)) { // Persistent configuration version. The size of the configuration must be // non-decreasing, so that the migration can assume that the new version is at // least as large as the previous version. -#define EECONFIG_VERSION 0x0105 +#define EECONFIG_VERSION 0x0106 // Keyboard configuration // Whenever there is a change in the configuration, `EECONFIG_VERSION` must be @@ -98,6 +136,10 @@ typedef struct __attribute__((packed)) { uint8_t current_profile; // Last non-default profile index, used for profile swapping uint8_t last_non_default_profile; + /* Board-wide RGB settings are kept in the common persistent layout so a + * configuration remains readable when firmware is rebuilt without an RGB + * backend. Live frames themselves intentionally remain in RAM. */ + eeconfig_rgb_t rgb; // End of global configurations // Profiles @@ -156,6 +198,28 @@ extern const eeconfig_t *eeconfig; #define DEFAULT_TICK_RATE 30 #endif +#if !defined(RGB_DEFAULT_BRIGHTNESS) +#define RGB_DEFAULT_BRIGHTNESS 50 +#endif +#if !defined(RGB_DEFAULT_R) +#define RGB_DEFAULT_R 255 +#endif +#if !defined(RGB_DEFAULT_G) +#define RGB_DEFAULT_G 255 +#endif +#if !defined(RGB_DEFAULT_B) +#define RGB_DEFAULT_B 255 +#endif +#define DEFAULT_RGB \ + { \ + .enabled = 1, \ + .brightness = RGB_DEFAULT_BRIGHTNESS, \ + .effect = 0, \ + .color_r = RGB_DEFAULT_R, \ + .color_g = RGB_DEFAULT_G, \ + .color_b = RGB_DEFAULT_B, \ + } + //--------------------------------------------------------------------+ // Persistent Configuration API //--------------------------------------------------------------------+ diff --git a/include/hardware/rgb_api.h b/include/hardware/rgb_api.h new file mode 100644 index 0000000..ede5228 --- /dev/null +++ b/include/hardware/rgb_api.h @@ -0,0 +1,21 @@ +/* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#pragma once + +#include "common.h" + +#if defined(RGB_ENABLE) + +/* Backends must return immediately. `rgb` remains valid only for the duration + * of this call, so a backend must copy it before returning true. */ +bool rgb_backend_init(void); +bool rgb_backend_submit(const uint8_t *rgb, uint16_t led_count); +bool rgb_backend_is_busy(void); +void rgb_backend_task(void); + +#endif diff --git a/include/rgb.h b/include/rgb.h new file mode 100644 index 0000000..ed63e06 --- /dev/null +++ b/include/rgb.h @@ -0,0 +1,45 @@ +/* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#pragma once + +#include "common.h" + +#if defined(RGB_ENABLE) + +#if !defined(RGB_LED_COUNT) +#error "RGB_LED_COUNT is not defined" +#endif + +#define RGB_BYTES_PER_PIXEL 3u +#define RGB_FRAME_BYTES ((uint16_t)(RGB_LED_COUNT * RGB_BYTES_PER_PIXEL)) +#define RGB_FRAME_CHUNK_BYTES 60u +#define RGB_EFFECT_STATIC 0u +#define RGB_EFFECT_BREATHING 1u +#define RGB_EFFECT_RAINBOW 2u +#define RGB_EFFECT_RAINBOW_WAVE 3u +#define RGB_EFFECT_LIVE 7u + +bool rgb_init(void); +void rgb_task(void); + +bool rgb_is_enabled(void); +bool rgb_set_enabled(bool enabled, bool persist); +uint8_t rgb_get_brightness(void); +bool rgb_set_brightness(uint8_t brightness, bool persist); +uint8_t rgb_get_effect(void); +bool rgb_set_effect(uint8_t effect, bool persist); +bool rgb_restore_effect(void); + +const uint8_t *rgb_get_frame(void); +bool rgb_get_pixel(uint8_t index, uint8_t *r, uint8_t *g, uint8_t *b); +bool rgb_set_pixel(uint8_t index, uint8_t r, uint8_t g, uint8_t b); +bool rgb_get_frame_chunk(uint16_t offset, uint8_t *dst, uint8_t len); +bool rgb_set_frame_chunk(uint16_t offset, const uint8_t *src, uint8_t len); +bool rgb_fill(uint8_t r, uint8_t g, uint8_t b, bool persist); + +#endif diff --git a/include/tusb_config.h b/include/tusb_config.h index 78d1259..33b4790 100644 --- a/include/tusb_config.h +++ b/include/tusb_config.h @@ -30,8 +30,8 @@ #define CFG_TUD_ENDPOINT0_SIZE 64 // Driver configuration -// Keyboard, generic, and raw HID interfaces -#define CFG_TUD_HID 3 +// Keyboard, generic, raw HID, and optional standards-based gamepad interfaces +#define CFG_TUD_HID 4 // HID buffer size. Must be at least the size of the largest reports (+1 for // interface with multiple reports) diff --git a/include/usb_descriptors.h b/include/usb_descriptors.h index e22d5a7..5f0016d 100644 --- a/include/usb_descriptors.h +++ b/include/usb_descriptors.h @@ -37,21 +37,24 @@ enum { USB_ITF_KEYBOARD = 0, USB_ITF_HID, USB_ITF_RAW_HID, - // We intentionally put the XInput interface last, so that if it is not - // enabled, we can subtract its size from the total configuration length - // without affecting the other interfaces. - USB_ITF_XINPUT, + // The optional gamepad is last so the configuration can expose either an + // XInput vendor interface, a standard HID gamepad, or neither. + USB_ITF_GAMEPAD, USB_ITF_COUNT, }; +#define USB_ITF_XINPUT USB_ITF_GAMEPAD + // In endpoint addresses enum { EP_IN_ADDR_KEYBOARD = 0x81, EP_IN_ADDR_HID, EP_IN_ADDR_RAW_HID, - EP_IN_ADDR_XINPUT, + EP_IN_ADDR_GAMEPAD, }; +#define EP_IN_ADDR_XINPUT EP_IN_ADDR_GAMEPAD + // Out endpoint addresses enum { EP_OUT_ADDR_RAW_HID = 0x01, diff --git a/keyboards/kbhe-75he/keyboard.json b/keyboards/kbhe-75he/keyboard.json new file mode 100644 index 0000000..54891e3 --- /dev/null +++ b/keyboards/kbhe-75he/keyboard.json @@ -0,0 +1,193 @@ +{ + "name": "KBHE 75HE (libhmk)", + "manufacturer": "KBHE", + "maintainer": "KBHE", + "usb": { + "vid": "0x9172", + "pid": "0x0004", + "port": "hs" + }, + "keyboard": { + "num_profiles": 4, + "num_layers": 4, + "num_keys": 82, + "num_advanced_keys": 32, + "num_dynamic_keystroke_max_bindings": 8, + "num_macro_nodes": 128 + }, + "hardware": { + "hse_value": 16000000, + "driver": "stm32f723xx" + }, + "analog": { + "invert_adc": false, + "delay": 1, + "mux": { + "select": ["D4", "D5", "D6"], + "input": ["A1", "A2", "A3", "A4", "A5", "A6", "A7", "B0", "B1", "C4", "C5"], + "matrix": [ + [6, 7, 8, 5, 22, 19, 21, 20], + [10, 11, 12, 9, 0, 23, 25, 24], + [0, 0, 14, 13, 29, 26, 28, 27], + [0, 0, 44, 42, 43, 57, 58, 0], + [39, 40, 41, 38, 56, 53, 55, 54], + [82, 81, 80, 72, 79, 71, 69, 70], + [65, 66, 67, 64, 68, 76, 78, 77], + [31, 32, 33, 30, 48, 45, 47, 46], + [2, 3, 4, 1, 18, 15, 17, 16], + [35, 36, 37, 34, 52, 49, 51, 50], + [60, 61, 62, 59, 63, 73, 75, 74] + ] + } + }, + "calibration": { + "initial_rest_value": 2195, + "initial_bottom_out_threshold": 550 + }, + "wear_leveling": { + "virtual_size": 16384, + "write_log_size": 65536 + }, + "actuation": { + "actuation_point": 128 + }, + "rgb": { + "num_leds": 82, + "data_pin": "A15", + "backend": "ws2812_tim2_ch1", + "default_brightness": 50, + "default_color": [255, 255, 255] + }, + "layout": { + "keymap": [ + [ + { "key": 0 }, + { "key": 1, "x": 0.5 }, + { "key": 2 }, + { "key": 3 }, + { "key": 4 }, + { "key": 5, "x": 0.25 }, + { "key": 6 }, + { "key": 7 }, + { "key": 8 }, + { "key": 9, "x": 0.25 }, + { "key": 10 }, + { "key": 11 }, + { "key": 12 }, + { "key": 13, "x": 0.25 } + ], + [ + { "key": 14 }, + { "key": 15 }, + { "key": 16 }, + { "key": 17 }, + { "key": 18 }, + { "key": 19 }, + { "key": 20 }, + { "key": 21 }, + { "key": 22 }, + { "key": 23 }, + { "key": 24 }, + { "key": 25 }, + { "key": 26 }, + { "key": 27, "w": 2 }, + { "key": 28 } + ], + [ + { "key": 29, "w": 1.5 }, + { "key": 30 }, + { "key": 31 }, + { "key": 32 }, + { "key": 33 }, + { "key": 34 }, + { "key": 35 }, + { "key": 36 }, + { "key": 37 }, + { "key": 38 }, + { "key": 39 }, + { "key": 40 }, + { "key": 41 }, + { "key": 42, "w": 1.5, "h": 2 }, + { "key": 43 } + ], + [ + { "key": 44, "w": 1.75 }, + { "key": 45 }, + { "key": 46 }, + { "key": 47 }, + { "key": 48 }, + { "key": 49 }, + { "key": 50 }, + { "key": 51 }, + { "key": 52 }, + { "key": 53 }, + { "key": 54 }, + { "key": 55 }, + { "key": 56 }, + { "key": 57 } + ], + [ + { "key": 58, "w": 1.25 }, + { "key": 59 }, + { "key": 60 }, + { "key": 61 }, + { "key": 62 }, + { "key": 63 }, + { "key": 64 }, + { "key": 65 }, + { "key": 66 }, + { "key": 67 }, + { "key": 68 }, + { "key": 69 }, + { "key": 70, "w": 2.75 }, + { "key": 71 } + ], + [ + { "key": 72, "w": 1.25 }, + { "key": 73, "w": 1.25 }, + { "key": 74, "w": 1.25 }, + { "key": 75, "w": 6.25 }, + { "key": 76, "w": 1.25 }, + { "key": 77, "w": 1.25 }, + { "key": 78, "w": 1.25 }, + { "key": 79 }, + { "key": 80 }, + { "key": 81 } + ] + ] + }, + "keymap": [ + [ + "KC_ESC", "KC_F1", "KC_F2", "KC_F3", "KC_F4", "KC_F5", "KC_F6", "KC_F7", "KC_F8", "KC_F9", "KC_F10", "KC_F11", "KC_F12", "KC_DEL", + "KC_GRV", "KC_1", "KC_2", "KC_3", "KC_4", "KC_5", "KC_6", "KC_7", "KC_8", "KC_9", "KC_0", "KC_MINS", "KC_EQL", "KC_BSPC", "KC_PGUP", + "KC_TAB", "KC_Q", "KC_W", "KC_E", "KC_R", "KC_T", "KC_Y", "KC_U", "KC_I", "KC_O", "KC_P", "KC_LBRC", "KC_RBRC", "KC_ENT", "KC_PGDN", + "KC_CAPS", "KC_A", "KC_S", "KC_D", "KC_F", "KC_G", "KC_H", "KC_J", "KC_K", "KC_L", "KC_SCLN", "KC_QUOT", "KC_NUHS", "KC_HOME", + "KC_LSFT", "KC_NUBS", "KC_Z", "KC_X", "KC_C", "KC_V", "KC_B", "KC_N", "KC_M", "KC_COMM", "KC_DOT", "KC_SLSH", "KC_RSFT", "KC_UP", + "KC_LCTL", "KC_LGUI", "KC_LALT", "KC_SPC", "KC_RALT", "MO(1)", "KC_RCTL", "KC_LEFT", "KC_DOWN", "KC_RGHT" + ], + [ + "_______", "KC_MPRV", "KC_MPLY", "KC_MNXT", "KC_MUTE", "KC_VOLD", "KC_VOLU", "_______", "_______", "_______", "PF(0)", "PF(1)", "PF(2)", "PF(3)", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "KC_HOME", + "KC_PSCR", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "KC_END", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "KC_PGUP", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "KC_HOME", "KC_PGDN", "KC_END" + ], + [ + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______" + ], + [ + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", + "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______", "_______" + ] + ] +} diff --git a/scripts/drivers.py b/scripts/drivers.py index 5fbdf50..6894976 100644 --- a/scripts/drivers.py +++ b/scripts/drivers.py @@ -191,6 +191,67 @@ class Driver: ), ) +STM32F723XX = Driver( + platformio=PlatformIO( + # PlatformIO's maintained F723 board definition supplies the correct + # Cortex-M7 startup/HAL package. The project linker still constrains the + # image and wear-leveling area to the KBHE VET6's 512 KiB flash. + board="disco_f723ie", + # The existing generic linker layout is architecture-independent and + # conservatively uses only the first 128 KiB of the F723's SRAM. + ldscript="stm32f446retx.ld", + framework="stm32cube", + platform="ststm32@19.7.1", + ), + tinyusb=TinyUSB(mcu="stm32f7"), + metadata=Metadata( + bootloader=Bootloader( + address=0x1FF00000, + magic=0xDEADBEEF, + ), + flash=Flash( + sector_sizes=NonUniformSectors( + sizes=[ + 16 * 1024, + 16 * 1024, + 16 * 1024, + 16 * 1024, + 64 * 1024, + 128 * 1024, + 128 * 1024, + 128 * 1024, + ] + ), + empty_value=0xFFFFFFFF, + ), + adc=ADC( + max_resolution=12, + input_pins=[ + "A0", + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "B0", + "B1", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + ], + to_gpio_array=lambda pins: ( + [f"GPIO{pin[0]}" for pin in pins], + [f"GPIO_PIN_{pin[1:]}" for pin in pins], + ), + ), + ), +) + AT32F405XX = Driver( platformio=PlatformIO( board="genericAT32F405RCT7", diff --git a/scripts/make.py b/scripts/make.py index aa5578b..0999750 100644 --- a/scripts/make.py +++ b/scripts/make.py @@ -152,5 +152,20 @@ if actuation.actuation_point is not None: build_flags.define("ACTUATION_POINT", actuation.actuation_point) +# Optional RGB matrix. The core stays out of non-RGB builds and the backend is +# selected explicitly so existing libhmk keyboards remain unchanged. +if kb_json.rgb is not None: + rgb = kb_json.rgb + ports, pin_nums = driver.metadata.adc.to_gpio_array([rgb.data_pin]) + build_flags.define("RGB_ENABLE") + build_flags.define("RGB_LED_COUNT", rgb.num_leds) + build_flags.define("RGB_DATA_GPIO_PORT", ports[0]) + build_flags.define("RGB_DATA_GPIO_PIN", pin_nums[0]) + build_flags.define("RGB_DEFAULT_BRIGHTNESS", rgb.default_brightness) + build_flags.define("RGB_DEFAULT_R", rgb.default_color[0]) + build_flags.define("RGB_DEFAULT_G", rgb.default_color[1]) + build_flags.define("RGB_DEFAULT_B", rgb.default_color[2]) + build_flags.define(f"RGB_BACKEND_{rgb.backend.upper()}") + # Add source build flags env.Append(BUILD_FLAGS=build_flags.get_flags()) diff --git a/scripts/metadata.py b/scripts/metadata.py index a8f7b6f..3eb75d4 100644 --- a/scripts/metadata.py +++ b/scripts/metadata.py @@ -16,7 +16,7 @@ import os import utils from schema.keyboard import KeyboardUSBPort -from uuid import uuid4 +from uuid import NAMESPACE_URL, uuid5 METADATA_TEMPLATE = """#pragma once @@ -41,7 +41,11 @@ def ms_os_20_guid_def(): - uuid = uuid4().hex.upper() + identity = ( + f"https://github.com/peppapighs/libhmk/" + f"{kb_json.usb.vid}/{kb_json.usb.pid}/xinput" + ) + uuid = uuid5(NAMESPACE_URL, identity).hex.upper() guid = f"{{{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}}}" guid_array = [f"U16_TO_U8S_LE('{x}')" for x in guid] + ["U16_TO_U8S_LE('\\0')"] * 2 @@ -61,22 +65,40 @@ def keyboard_metadata_def(): "numAdvancedKeys": kb_json.keyboard.num_advanced_keys, "numDynamicKeystrokeMaxBindings": kb_json.keyboard.num_dynamic_keystroke_max_bindings, "numMacroNodes": kb_json.keyboard.num_macro_nodes, + "gamepadApis": ["xinput", "hid"], "layout": kb_json.layout.model_dump(exclude_none=True), "defaultKeymaps": utils.resolve_default_keymaps(kb_json), + "rgb": ( + { + "numLeds": kb_json.rgb.num_leds, + "protocolMajor": 1, + "protocolMinor": 0, + "effects": [0, 1, 2, 3, 7], + } + if kb_json.rgb is not None + else None + ), } uncompressed = json.dumps(metadata).encode("utf-8") - compressed = gzip.compress(uncompressed) + compressed = gzip.compress(uncompressed, mtime=0) print(f"Uncompressed metadata size: {len(uncompressed)} bytes") print(f"Compressed metadata size: {len(compressed)} bytes") return utils.to_slice_def("KEYBOARD_METADATA", compressed) -with open(os.path.join("include", "metadata.h"), "w") as f: - f.write( - METADATA_TEMPLATE.format( - ms_os_20_guid=ms_os_20_guid_def(), - keyboard_metadata=keyboard_metadata_def(), - ) - ) +metadata_path = os.path.join("include", "metadata.h") +metadata_contents = METADATA_TEMPLATE.format( + ms_os_20_guid=ms_os_20_guid_def(), + keyboard_metadata=keyboard_metadata_def(), +) +try: + with open(metadata_path, "r", newline="") as f: + current_metadata = f.read() +except FileNotFoundError: + current_metadata = None + +if current_metadata != metadata_contents: + with open(metadata_path, "w", newline="") as f: + f.write(metadata_contents) diff --git a/scripts/schema/keyboard.py b/scripts/schema/keyboard.py index c688d03..31e3418 100644 --- a/scripts/schema/keyboard.py +++ b/scripts/schema/keyboard.py @@ -13,6 +13,10 @@ from enum import Enum from pydantic import BaseModel, Field, NonNegativeInt, PositiveFloat, PositiveInt +from typing import Annotated + + +RGBComponent = Annotated[int, Field(ge=0, le=255)] class KeyboardUSBPort(str, Enum): @@ -118,6 +122,17 @@ class KeyboardActuation(BaseModel): actuation_point: int = Field(ge=0, le=255) +class KeyboardRGB(BaseModel): + # Number of addressable RGB LEDs. The bridge protocol encodes this in one byte. + num_leds: int = Field(ge=1, le=255) + # GPIO pin used by the board-specific non-blocking backend. + data_pin: str = Field(pattern=r"^A(?:0|5|15)$") + # Currently supported by the STM32F723 driver used by KBHE. + backend: str = Field(pattern=r"^ws2812_tim2_ch1$") + default_brightness: int = Field(ge=0, le=255, default=50) + default_color: tuple[RGBComponent, RGBComponent, RGBComponent] = (255, 255, 255) + + # keyboard.json Schema class Keyboard(BaseModel): name: str @@ -135,3 +150,4 @@ class Keyboard(BaseModel): # Default keymaps for each profile. If not specified, the default keymap will be used for all profiles. keymaps: list[list[list[str]]] | None = None actuation: KeyboardActuation | None = None + rgb: KeyboardRGB | None = None diff --git a/scripts/utils.py b/scripts/utils.py index 30b6a98..f8fd735 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -60,6 +60,8 @@ def get_driver(keyboard: str): return STM32F446XX case "at32f405xx": return AT32F405XX + case "stm32f723xx": + return STM32F723XX case _: raise ValueError(f"Unsupported driver: {driver}") diff --git a/scripts/validate.py b/scripts/validate.py index 62cc44c..1e8cbc9 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -25,6 +25,11 @@ # Load JSON files and validate kb_json = utils.get_kb_json(keyboard) +if kb_json.rgb is not None and kb_json.hardware.driver != "stm32f723xx": + raise ValueError( + "The ws2812_tim2_ch1 RGB backend currently requires stm32f723xx" + ) + # Validate default keymaps default_keymaps = utils.resolve_default_keymaps(kb_json) if len(default_keymaps) != kb_json.keyboard.num_profiles: diff --git a/src/commands.c b/src/commands.c index f4d5e59..9ec0c8e 100644 --- a/src/commands.c +++ b/src/commands.c @@ -20,6 +20,9 @@ #include "layout.h" #include "matrix.h" #include "metadata.h" +#if defined(RGB_ENABLE) +#include "rgb.h" +#endif #include "tusb.h" // Helper macro to verify command parameters @@ -31,6 +34,26 @@ static const uint8_t keyboard_metadata[] = {KEYBOARD_METADATA}; +static bool gamepad_options_are_valid(const gamepad_options_t *options) { + if (options == NULL) + return false; + for (uint8_t i = 0u; i < 3u; i++) { + if (options->analog_curve[i][0] >= options->analog_curve[i + 1u][0]) + return false; + } + return true; +} + +static bool gamepad_buttons_are_valid(const uint8_t *buttons, uint8_t len) { + if (buttons == NULL) + return false; + for (uint8_t i = 0u; i < len; i++) { + if (buttons[i] > GP_BUTTON_RT) + return false; + } + return true; +} + // `volatile` to prevent compiler optimizations static volatile bool command_request_pending; static volatile bool command_response_pending; @@ -188,6 +211,10 @@ static void command_process(void) { const command_in_buffer_t *in = (const command_in_buffer_t *)in_buf; command_out_buffer_t *out = (command_out_buffer_t *)out_buf; + /* Responses are fixed-size RAW HID reports. Clear unused bytes so a short + * response cannot leak payload left behind by an earlier command. */ + memset(out_buf, 0, sizeof(out_buf)); + bool success = true; switch (in->command_id) { case COMMAND_FIRMWARE_VERSION: { @@ -242,7 +269,9 @@ static void command_process(void) { break; } case COMMAND_SET_OPTIONS: { - success = EECONFIG_WRITE(options, &in->options); + eeconfig_options_t options = in->options; + COMMAND_VERIFY(!(options.xinput_enabled && options.hid_gamepad_enabled)); + success = EECONFIG_WRITE(options, &options); break; } case COMMAND_RESET_PROFILE: { @@ -312,6 +341,127 @@ static void command_process(void) { success = EECONFIG_WRITE(bottom_out_threshold, bottom_out_threshold); break; } +#if defined(RGB_ENABLE) + case COMMAND_GET_LED_ENABLED: { + out_buf[1] = 0; + out_buf[2] = rgb_is_enabled() ? 1u : 0u; + break; + } + case COMMAND_SET_LED_ENABLED: { + if (in_buf[2] > 1u) + out_buf[1] = 3u; + else + out_buf[1] = rgb_set_enabled(in_buf[2] != 0u, true) ? 0u : 1u; + out_buf[2] = rgb_is_enabled() ? 1u : 0u; + break; + } + case COMMAND_GET_LED_BRIGHTNESS: { + out_buf[1] = 0; + out_buf[2] = rgb_get_brightness(); + break; + } + case COMMAND_SET_LED_BRIGHTNESS: { + out_buf[1] = rgb_set_brightness(in_buf[2], true) ? 0u : 1u; + out_buf[2] = rgb_get_brightness(); + break; + } + case COMMAND_GET_LED_PIXEL: { + uint8_t r = 0, g = 0, b = 0; + out_buf[2] = in_buf[2]; + out_buf[1] = rgb_get_pixel(in_buf[2], &r, &g, &b) ? 0u : 3u; + out_buf[3] = r; + out_buf[4] = g; + out_buf[5] = b; + break; + } + case COMMAND_SET_LED_PIXEL: { + out_buf[2] = in_buf[2]; + out_buf[3] = in_buf[3]; + out_buf[4] = in_buf[4]; + out_buf[5] = in_buf[5]; + out_buf[1] = rgb_set_pixel(in_buf[2], in_buf[3], in_buf[4], in_buf[5]) + ? 0u + : 3u; + break; + } + case COMMAND_GET_LED_ALL: { + const uint8_t chunk = in_buf[2]; + const uint16_t offset = (uint16_t)chunk * 60u; + const uint16_t remaining = offset < (uint16_t)RGB_FRAME_BYTES + ? (uint16_t)((uint16_t)RGB_FRAME_BYTES - + offset) + : 0u; + const uint8_t len = remaining > 60u ? 60u : (uint8_t)remaining; + out_buf[2] = chunk; + out_buf[3] = len; + out_buf[1] = rgb_get_frame_chunk(offset, out_buf + 4, len) ? 0u : 3u; + break; + } + case COMMAND_SET_LED_ALL_CHUNK: { + const uint8_t chunk = in_buf[2]; + const uint8_t len = in_buf[3]; + const uint16_t offset = (uint16_t)chunk * 60u; + out_buf[2] = chunk; + out_buf[3] = len; + out_buf[1] = (len <= 60u && rgb_set_frame_chunk(offset, in_buf + 4, len)) + ? 0u + : 3u; + break; + } + case COMMAND_LED_CLEAR: { + out_buf[1] = rgb_fill(0, 0, 0, rgb_get_effect() != RGB_EFFECT_LIVE) + ? 0u + : 1u; + break; + } + case COMMAND_LED_FILL: { + out_buf[1] = rgb_fill(in_buf[2], in_buf[3], in_buf[4], + rgb_get_effect() != RGB_EFFECT_LIVE) + ? 0u + : 1u; + out_buf[2] = in_buf[2]; + out_buf[3] = in_buf[3]; + out_buf[4] = in_buf[4]; + break; + } + case COMMAND_GET_LED_EFFECT: { + out_buf[1] = 0; + out_buf[2] = rgb_get_effect(); + break; + } + case COMMAND_SET_LED_EFFECT: { + const uint8_t effect = in_buf[2]; + const bool valid = effect == RGB_EFFECT_STATIC || + effect == RGB_EFFECT_BREATHING || + effect == RGB_EFFECT_RAINBOW || + effect == RGB_EFFECT_RAINBOW_WAVE || + effect == RGB_EFFECT_LIVE; + if (!valid) + out_buf[1] = 3u; + else + out_buf[1] = rgb_set_effect(effect, effect != RGB_EFFECT_LIVE) ? 0u : 1u; + out_buf[2] = rgb_get_effect(); + break; + } + case COMMAND_RESTORE_LED_EFFECT: { + out_buf[1] = rgb_restore_effect() ? 0u : 1u; + out_buf[2] = rgb_get_effect(); + break; + } + case COMMAND_GET_RGB_CAPABILITIES: { + out_buf[1] = 0; + out_buf[2] = 1; /* protocol major */ + out_buf[3] = 0; /* protocol minor */ + out_buf[4] = RGB_LED_COUNT; + out_buf[5] = RGB_BYTES_PER_PIXEL; + out_buf[6] = 60; + out_buf[7] = RGB_EFFECT_LIVE; + out_buf[8] = 0x7f; /* enabled..restore */ + out_buf[9] = 0; + out_buf[10] = 0; /* logical RGB */ + break; + } +#endif //--------------------------------------------------------------------+ // Per-profile commands //--------------------------------------------------------------------+ @@ -447,6 +597,7 @@ static void command_process(void) { COMMAND_VERIFY(p->offset < NUM_KEYS); COMMAND_VERIFY(p->len <= M_ARRAY_SIZE(p->gamepad_buttons) && p->len <= NUM_KEYS - p->offset); + COMMAND_VERIFY(gamepad_buttons_are_valid(p->gamepad_buttons, p->len)); success = EECONFIG_WRITE_N(profiles[p->profile].gamepad_buttons[p->offset], p->gamepad_buttons, sizeof(uint8_t) * p->len); @@ -464,6 +615,7 @@ static void command_process(void) { const command_in_gamepad_options_t *p = &in->gamepad_options; COMMAND_VERIFY(p->profile < NUM_PROFILES); + COMMAND_VERIFY(gamepad_options_are_valid(&p->gamepad_options)); success = EECONFIG_WRITE(profiles[p->profile].gamepad_options, &p->gamepad_options); @@ -491,4 +643,4 @@ void command_task(void) { tud_hid_n_report(USB_ITF_RAW_HID, 0, out_buf, RAW_HID_EP_SIZE)) // The command response has been sent, so clear the queue. command_response_pending = false; -} \ No newline at end of file +} diff --git a/src/eeconfig.c b/src/eeconfig.c index 3df61fa..3bc8979 100644 --- a/src/eeconfig.c +++ b/src/eeconfig.c @@ -23,6 +23,7 @@ const eeconfig_t *eeconfig; // Default configuration values static eeconfig_options_t default_options = DEFAULT_OPTIONS; static eeconfig_calibration_t default_calibration = DEFAULT_CALIBRATION; +static const eeconfig_rgb_t default_rgb = DEFAULT_RGB; static const uint8_t default_keymaps[NUM_PROFILES][NUM_LAYERS][NUM_KEYS] = DEFAULT_KEYMAPS; static const macro_node_t default_macro = { @@ -83,6 +84,7 @@ bool eeconfig_reset(void) { status &= EECONFIG_WRITE(options, &default_options); EECONFIG_WRITE_LOCAL(current_profile, 0); EECONFIG_WRITE_LOCAL(last_non_default_profile, M_MIN(1, NUM_PROFILES - 1)); + status &= EECONFIG_WRITE(rgb, &default_rgb); for (uint32_t i = 0; i < NUM_PROFILES; i++) status &= eeconfig_write_default_profile(i); EECONFIG_WRITE_LOCAL(magic_end, EECONFIG_MAGIC_END); diff --git a/src/hardware/stm32f723xx/analog.c b/src/hardware/stm32f723xx/analog.c new file mode 100644 index 0000000..826751a --- /dev/null +++ b/src/hardware/stm32f723xx/analog.c @@ -0,0 +1,255 @@ +/* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#include "hardware/hardware.h" + +#include "stm32f7xx_hal.h" + +static GPIO_TypeDef *channel_ports[] = { + GPIOA, GPIOA, GPIOA, GPIOA, GPIOA, GPIOA, GPIOA, GPIOA, + GPIOB, GPIOB, GPIOC, GPIOC, GPIOC, GPIOC, GPIOC, GPIOC, +}; +static const uint16_t channel_pins[] = { + GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_2, GPIO_PIN_3, GPIO_PIN_4, GPIO_PIN_5, + GPIO_PIN_6, GPIO_PIN_7, GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_0, GPIO_PIN_1, + GPIO_PIN_2, GPIO_PIN_3, GPIO_PIN_4, GPIO_PIN_5, +}; + +_Static_assert(M_ARRAY_SIZE(channel_ports) == ADC_NUM_CHANNELS, + "Invalid ADC channel port table"); +_Static_assert(M_ARRAY_SIZE(channel_pins) == ADC_NUM_CHANNELS, + "Invalid ADC channel pin table"); + +#if ADC_NUM_MUX_INPUTS > 0 +static const uint32_t mux_input_channels[] = ADC_MUX_INPUT_CHANNELS; +static GPIO_TypeDef *mux_select_ports[] = ADC_MUX_SELECT_PORTS; +static const uint16_t mux_select_pins[] = ADC_MUX_SELECT_PINS; +static const uint16_t mux_input_matrix[][ADC_NUM_MUX_INPUTS] = + ADC_MUX_INPUT_MATRIX; + +_Static_assert(M_ARRAY_SIZE(mux_input_channels) == ADC_NUM_MUX_INPUTS, + "Invalid ADC mux input table"); +_Static_assert(M_ARRAY_SIZE(mux_select_ports) == ADC_NUM_MUX_SELECT_PINS, + "Invalid mux select port table"); +_Static_assert(M_ARRAY_SIZE(mux_select_pins) == ADC_NUM_MUX_SELECT_PINS, + "Invalid mux select pin table"); +_Static_assert(M_ARRAY_SIZE(mux_input_matrix) == + (1u << ADC_NUM_MUX_SELECT_PINS), + "Invalid ADC mux matrix"); +#endif + +#if ADC_NUM_RAW_INPUTS > 0 +static const uint32_t raw_input_channels[] = ADC_RAW_INPUT_CHANNELS; +static const uint16_t raw_input_vector[] = ADC_RAW_INPUT_VECTOR; + +_Static_assert(M_ARRAY_SIZE(raw_input_channels) == ADC_NUM_RAW_INPUTS, + "Invalid raw ADC input table"); +_Static_assert(M_ARRAY_SIZE(raw_input_vector) == ADC_NUM_RAW_INPUTS, + "Invalid raw ADC mapping table"); +#endif + +static ADC_HandleTypeDef adc_handle; +static DMA_HandleTypeDef dma_handle; +#if ADC_NUM_MUX_INPUTS > 0 +static TIM_HandleTypeDef tim_handle; +#endif + +static volatile bool adc_initialized; +#define ADC_DMA_SAMPLES \ + (M_DIV_CEIL(ADC_NUM_MUX_INPUTS + ADC_NUM_RAW_INPUTS, 16u) * 16u) +__attribute__((aligned(32))) static volatile uint16_t + adc_buffer[ADC_DMA_SAMPLES]; +static volatile uint16_t adc_values[NUM_KEYS]; + +static void adc_buffer_invalidate(void) { + if ((SCB->CCR & SCB_CCR_DC_Msk) != 0u) { + const uintptr_t first = (uintptr_t)adc_buffer & ~(uintptr_t)31u; + const uintptr_t last = + ((uintptr_t)adc_buffer + sizeof(adc_buffer) + 31u) & ~(uintptr_t)31u; + SCB_InvalidateDCache_by_Addr((uint32_t *)first, (int32_t)(last - first)); + } +} + +void analog_init(void) { + ADC_ChannelConfTypeDef channel = {0}; + + __HAL_RCC_ADC1_CLK_ENABLE(); + __HAL_RCC_DMA2_CLK_ENABLE(); + __HAL_RCC_GPIOA_CLK_ENABLE(); + __HAL_RCC_GPIOB_CLK_ENABLE(); + __HAL_RCC_GPIOC_CLK_ENABLE(); + __HAL_RCC_GPIOD_CLK_ENABLE(); +#if ADC_NUM_MUX_INPUTS > 0 + __HAL_RCC_TIM10_CLK_ENABLE(); +#endif + + adc_handle.Instance = ADC1; + adc_handle.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; + adc_handle.Init.Resolution = ADC_RESOLUTION_HAL; + adc_handle.Init.ScanConvMode = ADC_SCAN_ENABLE; + adc_handle.Init.ContinuousConvMode = DISABLE; + adc_handle.Init.DiscontinuousConvMode = DISABLE; + adc_handle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; + adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; + adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; + adc_handle.Init.NbrOfConversion = ADC_NUM_MUX_INPUTS + ADC_NUM_RAW_INPUTS; + adc_handle.Init.DMAContinuousRequests = DISABLE; + adc_handle.Init.EOCSelection = ADC_EOC_SINGLE_CONV; + if (HAL_ADC_Init(&adc_handle) != HAL_OK) + board_error_handler(); + +#if ADC_NUM_MUX_INPUTS > 0 + for (uint32_t i = 0; i < ADC_NUM_MUX_INPUTS; i++) { + GPIO_InitTypeDef gpio = {0}; + channel.Channel = mux_input_channels[i]; + channel.Rank = i + 1u; + channel.SamplingTime = ADC_NUM_SAMPLE_CYCLES; + if (HAL_ADC_ConfigChannel(&adc_handle, &channel) != HAL_OK) + board_error_handler(); + + gpio.Pin = channel_pins[mux_input_channels[i]]; + gpio.Mode = GPIO_MODE_ANALOG; + gpio.Pull = GPIO_NOPULL; + HAL_GPIO_Init(channel_ports[mux_input_channels[i]], &gpio); + } + + for (uint32_t i = 0; i < ADC_NUM_MUX_SELECT_PINS; i++) { + GPIO_InitTypeDef gpio = {0}; + gpio.Pin = mux_select_pins[i]; + gpio.Mode = GPIO_MODE_OUTPUT_PP; + gpio.Pull = GPIO_NOPULL; + gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + HAL_GPIO_Init(mux_select_ports[i], &gpio); + HAL_GPIO_WritePin(mux_select_ports[i], mux_select_pins[i], GPIO_PIN_RESET); + } +#endif + +#if ADC_NUM_RAW_INPUTS > 0 + for (uint32_t i = 0; i < ADC_NUM_RAW_INPUTS; i++) { + GPIO_InitTypeDef gpio = {0}; + channel.Channel = raw_input_channels[i]; + channel.Rank = ADC_NUM_MUX_INPUTS + i + 1u; + channel.SamplingTime = ADC_NUM_SAMPLE_CYCLES; + if (HAL_ADC_ConfigChannel(&adc_handle, &channel) != HAL_OK) + board_error_handler(); + + gpio.Pin = channel_pins[raw_input_channels[i]]; + gpio.Mode = GPIO_MODE_ANALOG; + gpio.Pull = GPIO_NOPULL; + HAL_GPIO_Init(channel_ports[raw_input_channels[i]], &gpio); + } +#endif + + dma_handle.Instance = DMA2_Stream0; + dma_handle.Init.Channel = DMA_CHANNEL_0; + dma_handle.Init.Direction = DMA_PERIPH_TO_MEMORY; + dma_handle.Init.PeriphInc = DMA_PINC_DISABLE; + dma_handle.Init.MemInc = DMA_MINC_ENABLE; + dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; + dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; + dma_handle.Init.Mode = DMA_CIRCULAR; + dma_handle.Init.Priority = DMA_PRIORITY_VERY_HIGH; + dma_handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE; + if (HAL_DMA_Init(&dma_handle) != HAL_OK) + board_error_handler(); + __HAL_LINKDMA(&adc_handle, DMA_Handle, dma_handle); + +#if ADC_NUM_MUX_INPUTS > 0 + tim_handle.Instance = TIM10; + tim_handle.Init.Prescaler = 0; + tim_handle.Init.CounterMode = TIM_COUNTERMODE_UP; + tim_handle.Init.Period = + (F_CPU / 1000000u) * (uint32_t)ADC_SAMPLE_DELAY - 1u; + tim_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; + tim_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; + if (HAL_TIM_Base_Init(&tim_handle) != HAL_OK) + board_error_handler(); +#endif + + HAL_NVIC_SetPriority(ADC_IRQn, 1, 0); + HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 1, 0); + HAL_NVIC_EnableIRQ(ADC_IRQn); + HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn); +#if ADC_NUM_MUX_INPUTS > 0 + HAL_NVIC_SetPriority(TIM1_UP_TIM10_IRQn, 1, 0); + HAL_NVIC_EnableIRQ(TIM1_UP_TIM10_IRQn); +#endif + + adc_buffer_invalidate(); + if (HAL_ADC_Start_DMA(&adc_handle, (uint32_t *)adc_buffer, + ADC_NUM_MUX_INPUTS + ADC_NUM_RAW_INPUTS) != HAL_OK) + board_error_handler(); + while (!adc_initialized) + ; +} + +void analog_task(void) {} + +uint16_t analog_read(uint8_t key) { return adc_values[key]; } + +void ADC_IRQHandler(void) { HAL_ADC_IRQHandler(&adc_handle); } +void DMA2_Stream0_IRQHandler(void) { HAL_DMA_IRQHandler(&dma_handle); } +#if ADC_NUM_MUX_INPUTS > 0 +void TIM1_UP_TIM10_IRQHandler(void) { HAL_TIM_IRQHandler(&tim_handle); } +#endif + +void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { +#if ADC_NUM_MUX_INPUTS > 0 + static uint8_t mux_channel; +#endif + if (hadc != &adc_handle) + return; + + adc_buffer_invalidate(); +#if ADC_NUM_MUX_INPUTS > 0 + for (uint32_t i = 0; i < ADC_NUM_MUX_INPUTS; i++) { + const uint16_t key = mux_input_matrix[mux_channel][i]; + if (key != 0u) + adc_values[key - 1u] = adc_buffer[i]; + } +#endif +#if ADC_NUM_RAW_INPUTS > 0 + for (uint32_t i = 0; i < ADC_NUM_RAW_INPUTS; i++) { + const uint16_t key = raw_input_vector[i]; + if (key != 0u) + adc_values[key - 1u] = adc_buffer[ADC_NUM_MUX_INPUTS + i]; + } +#endif + +#if ADC_NUM_MUX_INPUTS > 0 + mux_channel = (uint8_t)((mux_channel + 1u) & + ((1u << ADC_NUM_MUX_SELECT_PINS) - 1u)); + adc_initialized |= mux_channel == 0u; + for (uint32_t i = 0; i < ADC_NUM_MUX_SELECT_PINS; i++) { + HAL_GPIO_WritePin(mux_select_ports[i], mux_select_pins[i], + ((((uint32_t)mux_channel) >> i) & 1u) != 0u + ? GPIO_PIN_SET + : GPIO_PIN_RESET); + } + if (HAL_TIM_Base_Start_IT(&tim_handle) != HAL_OK) + board_error_handler(); +#else + adc_initialized = true; + adc_buffer_invalidate(); + if (HAL_ADC_Start_DMA(&adc_handle, (uint32_t *)adc_buffer, + ADC_NUM_MUX_INPUTS + ADC_NUM_RAW_INPUTS) != HAL_OK) + board_error_handler(); +#endif +} + +#if ADC_NUM_MUX_INPUTS > 0 +void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { + if (htim != &tim_handle) + return; + if (HAL_TIM_Base_Stop_IT(&tim_handle) != HAL_OK) + board_error_handler(); + adc_buffer_invalidate(); + if (HAL_ADC_Start_DMA(&adc_handle, (uint32_t *)adc_buffer, + ADC_NUM_MUX_INPUTS + ADC_NUM_RAW_INPUTS) != HAL_OK) + board_error_handler(); +} +#endif diff --git a/src/hardware/stm32f723xx/board.c b/src/hardware/stm32f723xx/board.c new file mode 100644 index 0000000..2c080d0 --- /dev/null +++ b/src/hardware/stm32f723xx/board.c @@ -0,0 +1,158 @@ +/* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#include "hardware/hardware.h" + +#include "stm32f7xx_hal.h" +#include "tusb.h" + +static void board_clock_init(void) { + RCC_OscInitTypeDef osc = {0}; + RCC_ClkInitTypeDef clk = {0}; + + __HAL_RCC_PWR_CLK_ENABLE(); + __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); + + osc.OscillatorType = RCC_OSCILLATORTYPE_HSE; + osc.HSEState = RCC_HSE_ON; + osc.PLL.PLLState = RCC_PLL_ON; + osc.PLL.PLLSource = RCC_PLLSOURCE_HSE; + /* Keep the VCO input at 2 MHz. With the KBHE 16 MHz HSE this produces a + * 432 MHz VCO, a 216 MHz CPU clock and a valid 48 MHz PLLQ output. */ + osc.PLL.PLLM = BOARD_HSE_VALUE / 2000000u; + osc.PLL.PLLN = 216; + osc.PLL.PLLP = RCC_PLLP_DIV2; + osc.PLL.PLLQ = 9; + if (HAL_RCC_OscConfig(&osc) != HAL_OK) + board_error_handler(); + + if (HAL_PWREx_EnableOverDrive() != HAL_OK) + board_error_handler(); + + clk.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | + RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; + clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + clk.AHBCLKDivider = RCC_SYSCLK_DIV1; + clk.APB1CLKDivider = RCC_HCLK_DIV4; + clk.APB2CLKDivider = RCC_HCLK_DIV2; + if (HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_7) != HAL_OK) + board_error_handler(); +} + +static void board_usb_init(void) { + GPIO_InitTypeDef gpio = {0}; + +#if !defined(BOARD_USB_HS) +#error "The KBHE STM32F723 port requires the integrated high-speed PHY" +#endif + + __HAL_RCC_GPIOB_CLK_ENABLE(); + __HAL_RCC_OTGPHYC_CLK_ENABLE(); + __HAL_RCC_USB_OTG_HS_CLK_ENABLE(); + /* Required by the DWC2 core reset path even when the embedded PHY is used. */ + __HAL_RCC_USB_OTG_HS_ULPI_CLK_ENABLE(); + +#if defined(RCC_AHB1LPENR_OTGHSULPILPEN) + RCC->AHB1LPENR &= ~RCC_AHB1LPENR_OTGHSULPILPEN; +#endif + + gpio.Pin = GPIO_PIN_14 | GPIO_PIN_15; + gpio.Mode = GPIO_MODE_AF_PP; + gpio.Pull = GPIO_NOPULL; + gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + gpio.Alternate = GPIO_AF12_OTG_HS_FS; + HAL_GPIO_Init(GPIOB, &gpio); + + /* KBHE does not route the optional PB13 VBUS-sense input. Advertise a valid + * B-device session in software, as required by TinyUSB's STM32F7 BSP. */ + USB_OTG_HS->GCCFG &= ~USB_OTG_GCCFG_VBDEN; + USB_OTG_HS->GOTGCTL |= USB_OTG_GOTGCTL_BVALOEN; + USB_OTG_HS->GOTGCTL |= USB_OTG_GOTGCTL_BVALOVAL; + + HAL_NVIC_SetPriority(OTG_HS_IRQn, 0, 0); + HAL_NVIC_EnableIRQ(OTG_HS_IRQn); +} + +static void board_bootloader_jump(void) { + volatile const uint32_t *vector = + (volatile const uint32_t *)BOOTLOADER_ADDR; + const uint32_t sp = vector[0]; + const uint32_t entry = vector[1]; + + SysTick->CTRL = 0; + SysTick->LOAD = 0; + SysTick->VAL = 0; + for (uint32_t i = 0; i < M_ARRAY_SIZE(NVIC->ICER); i++) + NVIC->ICER[i] = UINT32_MAX; + + SCB->VTOR = (uint32_t)BOOTLOADER_ADDR; + __set_MSP(sp); + __set_PSP(sp); + ((void (*)(void))entry)(); + while (1) + ; +} + +extern uint32_t _board_bootloader_flag[]; +#define BOARD_BOOTLOADER_FLAG _board_bootloader_flag[0] + +void board_init(void) { + if (BOARD_BOOTLOADER_FLAG == BOOTLOADER_MAGIC) { + BOARD_BOOTLOADER_FLAG = 0; + board_bootloader_jump(); + } + + /* Match TinyUSB's STM32F7 policy: instruction cache materially reduces scan + * time, while data cache stays disabled so every current and future DMA + * buffer is coherent by default. Explicitly disable a cache that a chainload + * bootloader may have left enabled; CMSIS cleans it before switching it off. + * The ADC/RGB backends still perform aligned maintenance if a downstream + * board deliberately enables D-cache later. */ + if ((SCB->CCR & SCB_CCR_DC_Msk) != 0u) + SCB_DisableDCache(); + SCB_EnableICache(); + + HAL_Init(); + board_clock_init(); + board_usb_init(); + + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + DWT->CYCCNT = 0; +} + +void board_error_handler(void) { + __disable_irq(); + while (1) + ; +} + +void board_reset(void) { NVIC_SystemReset(); } + +void board_enter_bootloader(void) { + BOARD_BOOTLOADER_FLAG = BOOTLOADER_MAGIC; + NVIC_SystemReset(); +} + +uint32_t board_serial(char *buf) { + const volatile uint8_t *uid = (const volatile uint8_t *)UID_BASE; + for (uint32_t i = 0; i < 12; i++) { + buf[i * 2u] = M_HEX(uid[i] >> 4); + buf[i * 2u + 1u] = M_HEX(uid[i] & 0x0fu); + } + return 24; +} + +uint32_t board_cycle_count(void) { return DWT->CYCCNT; } + +uint32_t tusb_time_millis_api(void) { return HAL_GetTick(); } + +void tusb_time_delay_ms_api(uint32_t ms) { HAL_Delay(ms); } + +void SysTick_Handler(void) { HAL_IncTick(); } + +void OTG_HS_IRQHandler(void) { tud_int_handler(1); } diff --git a/src/hardware/stm32f723xx/crc32.c b/src/hardware/stm32f723xx/crc32.c new file mode 100644 index 0000000..07407a5 --- /dev/null +++ b/src/hardware/stm32f723xx/crc32.c @@ -0,0 +1,31 @@ +/* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#include "crc32.h" + +#include "hardware/hardware.h" +#include "stm32f7xx_hal.h" + +static CRC_HandleTypeDef crc_handle; + +void crc32_init(void) { + __HAL_RCC_CRC_CLK_ENABLE(); + crc_handle.Instance = CRC; + if (HAL_CRC_Init(&crc_handle) != HAL_OK) + board_error_handler(); +} +uint32_t crc32_compute(const void *buf, uint32_t len, uint32_t crc) { + const uint8_t *bytes = buf; + uint32_t tail = 0; + (void)HAL_CRC_Calculate(&crc_handle, &crc, 1); + crc = HAL_CRC_Accumulate(&crc_handle, (uint32_t *)bytes, len >> 2); + if ((len & 3u) != 0u) { + memcpy(&tail, bytes + (len & ~(uint32_t)3u), len & 3u); + crc = HAL_CRC_Accumulate(&crc_handle, &tail, 1); + } + return crc; +} diff --git a/src/hardware/stm32f723xx/flash.c b/src/hardware/stm32f723xx/flash.c new file mode 100644 index 0000000..310bacf --- /dev/null +++ b/src/hardware/stm32f723xx/flash.c @@ -0,0 +1,54 @@ +/* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#include "hardware/hardware.h" + +#include "stm32f7xx_hal.h" + +void flash_init(void) {} + +bool flash_erase(uint32_t sector) { + if (sector >= FLASH_NUM_SECTORS) + return false; + + FLASH_EraseInitTypeDef erase = {0}; + uint32_t error = 0; + erase.TypeErase = FLASH_TYPEERASE_SECTORS; + erase.Sector = sector; + erase.NbSectors = 1; + erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; + + HAL_FLASH_Unlock(); + const bool success = HAL_FLASHEx_Erase(&erase, &error) == HAL_OK; + HAL_FLASH_Lock(); + return success; +} + +bool flash_read(uint32_t addr, void *buf, uint32_t len) { + if (addr > FLASH_SIZE || len > (FLASH_SIZE - addr) / sizeof(uint32_t)) + return false; + memcpy(buf, (const void *)(FLASH_BASE + addr), len * sizeof(uint32_t)); + return true; +} + +bool flash_write(uint32_t addr, const void *buf, uint32_t len) { + if (addr > FLASH_SIZE || len > (FLASH_SIZE - addr) / sizeof(uint32_t)) + return false; + + const uint32_t *words = buf; + HAL_FLASH_Unlock(); + for (uint32_t i = 0; i < len; i++) { + if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, + FLASH_BASE + addr + i * sizeof(uint32_t), + words[i]) != HAL_OK) { + HAL_FLASH_Lock(); + return false; + } + } + HAL_FLASH_Lock(); + return true; +} diff --git a/src/hardware/stm32f723xx/rgb.c b/src/hardware/stm32f723xx/rgb.c new file mode 100644 index 0000000..9b50a43 --- /dev/null +++ b/src/hardware/stm32f723xx/rgb.c @@ -0,0 +1,189 @@ +/* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#include "hardware/rgb_api.h" + +#if defined(RGB_ENABLE) + +#include "hardware/hardware.h" +#include "rgb.h" +#include "stm32f7xx_hal.h" + +#if !defined(RGB_BACKEND_WS2812_TIM2_CH1) +#error "Unsupported RGB backend for STM32F723" +#endif +#define WS2812_BITS_PER_LED 24u +/* Refill eight LEDs per half-buffer. This lowers the worst-case interrupt rate + * from 33.3 kHz (one callback per LED) to 4.17 kHz while keeping the buffer + * small and every refill deadline a comfortable 240 us. */ +#define WS2812_LEDS_PER_HALF 8u +#define WS2812_HALF_WORDS (WS2812_BITS_PER_LED * WS2812_LEDS_PER_HALF) +#define WS2812_DMA_WORDS (WS2812_HALF_WORDS * 2u) +#define WS2812_PERIOD 134u +#define WS2812_ZERO 43u +#define WS2812_ONE 86u + +static TIM_HandleTypeDef tim_handle; +static DMA_HandleTypeDef dma_handle; +__attribute__((aligned(32))) static uint32_t dma_buffer[WS2812_DMA_WORDS]; +static uint8_t tx_frame[RGB_FRAME_BYTES]; +static volatile bool tx_busy; +static volatile bool tx_stop_pending; +static uint16_t tx_next_led; +static bool half_contains_data[2]; + +static void rgb_dma_clean(uint32_t *first, uint32_t words) { + if ((SCB->CCR & SCB_CCR_DC_Msk) != 0u) + SCB_CleanDCache_by_Addr(first, + (int32_t)(words * (uint32_t)sizeof(uint32_t))); +} + +static void encode_byte(uint8_t value, uint32_t **dst) { + for (uint8_t mask = 0x80u; mask != 0u; mask >>= 1u) { + **dst = (value & mask) != 0u ? WS2812_ONE : WS2812_ZERO; + (*dst)++; + } +} + +static void fill_half(uint8_t half) { + uint32_t *dst = dma_buffer + (uint32_t)half * WS2812_HALF_WORDS; + bool contains_data = false; + + memset(dst, 0, WS2812_HALF_WORDS * sizeof(uint32_t)); + for (uint8_t slot = 0u; + slot < WS2812_LEDS_PER_HALF && tx_next_led < RGB_LED_COUNT; slot++) { + const uint8_t *pixel = tx_frame + tx_next_led * RGB_BYTES_PER_PIXEL; + /* WS2812 is GRB on the wire; the bridge remains logical RGB. */ + encode_byte(pixel[1], &dst); + encode_byte(pixel[0], &dst); + encode_byte(pixel[2], &dst); + tx_next_led++; + contains_data = true; + } + half_contains_data[half] = contains_data; + rgb_dma_clean(dma_buffer + (uint32_t)half * WS2812_HALF_WORDS, + WS2812_HALF_WORDS); +} + +static void half_completed(uint8_t half) { + if (!tx_busy || tx_stop_pending) + return; + + if (!half_contains_data[half]) { + /* A completely-low half lasts 240 us, already exceeding the WS2812 latch + * requirement. Leave both circular halves low and defer HAL shutdown to + * the main loop: HAL_TIM_PWM_Stop_DMA() is not safe from inside its own + * completion callback because the DMA handle can remain in ABORT state. */ + tx_stop_pending = true; + return; + } + fill_half(half); +} + +bool rgb_backend_init(void) { + GPIO_InitTypeDef gpio = {0}; + TIM_OC_InitTypeDef output = {0}; + + __HAL_RCC_GPIOA_CLK_ENABLE(); + __HAL_RCC_DMA1_CLK_ENABLE(); + __HAL_RCC_TIM2_CLK_ENABLE(); + + gpio.Pin = RGB_DATA_GPIO_PIN; + gpio.Mode = GPIO_MODE_AF_PP; + gpio.Pull = GPIO_NOPULL; + gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + gpio.Alternate = GPIO_AF1_TIM2; + HAL_GPIO_Init(RGB_DATA_GPIO_PORT, &gpio); + + dma_handle.Instance = DMA1_Stream5; + dma_handle.Init.Channel = DMA_CHANNEL_3; + dma_handle.Init.Direction = DMA_MEMORY_TO_PERIPH; + dma_handle.Init.PeriphInc = DMA_PINC_DISABLE; + dma_handle.Init.MemInc = DMA_MINC_ENABLE; + dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; + dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; + dma_handle.Init.Mode = DMA_CIRCULAR; + dma_handle.Init.Priority = DMA_PRIORITY_HIGH; + dma_handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE; + if (HAL_DMA_Init(&dma_handle) != HAL_OK) + return false; + + tim_handle.Instance = TIM2; + tim_handle.Init.Prescaler = 0; + tim_handle.Init.CounterMode = TIM_COUNTERMODE_UP; + tim_handle.Init.Period = WS2812_PERIOD; + tim_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; + tim_handle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; + if (HAL_TIM_PWM_Init(&tim_handle) != HAL_OK) + return false; + + output.OCMode = TIM_OCMODE_PWM1; + output.Pulse = 0; + output.OCPolarity = TIM_OCPOLARITY_HIGH; + output.OCFastMode = TIM_OCFAST_DISABLE; + if (HAL_TIM_PWM_ConfigChannel(&tim_handle, &output, TIM_CHANNEL_1) != HAL_OK) + return false; + + __HAL_LINKDMA(&tim_handle, hdma[TIM_DMA_ID_CC1], dma_handle); + memset(dma_buffer, 0, sizeof(dma_buffer)); + rgb_dma_clean(dma_buffer, WS2812_DMA_WORDS); + + HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 2, 0); + HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn); + return true; +} + +bool rgb_backend_submit(const uint8_t *rgb, uint16_t led_count) { + if (rgb == NULL || led_count != RGB_LED_COUNT || tx_busy) + return false; + + memcpy(tx_frame, rgb, RGB_FRAME_BYTES); + tx_next_led = 0; + tx_stop_pending = false; + fill_half(0); + fill_half(1); + tx_busy = true; + + if (HAL_TIM_PWM_Start_DMA(&tim_handle, TIM_CHANNEL_1, dma_buffer, + WS2812_DMA_WORDS) != HAL_OK) { + tx_busy = false; + return false; + } + return true; +} + +bool rgb_backend_is_busy(void) { return tx_busy; } + +void rgb_backend_task(void) { + if (!tx_stop_pending) + return; + + /* Run the synchronous abort outside the DMA callback. The wire is already + * held low by both halves, so the few cycles spent disabling the peripheral + * cannot corrupt the latched LED frame. */ + __HAL_TIM_DISABLE_DMA(&tim_handle, TIM_DMA_CC1); + if (HAL_DMA_Abort(&dma_handle) != HAL_OK || + HAL_TIM_PWM_Stop(&tim_handle, TIM_CHANNEL_1) != HAL_OK) + board_error_handler(); + + tx_busy = false; + tx_stop_pending = false; +} + +void DMA1_Stream5_IRQHandler(void) { HAL_DMA_IRQHandler(&dma_handle); } + +void HAL_TIM_PWM_PulseFinishedHalfCpltCallback(TIM_HandleTypeDef *htim) { + if (htim == &tim_handle) + half_completed(0); +} + +void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) { + if (htim == &tim_handle) + half_completed(1); +} + +#endif diff --git a/src/hardware/stm32f723xx/timer.c b/src/hardware/stm32f723xx/timer.c new file mode 100644 index 0000000..1c8657f --- /dev/null +++ b/src/hardware/stm32f723xx/timer.c @@ -0,0 +1,14 @@ +/* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#include "hardware/hardware.h" + +#include "stm32f7xx_hal.h" + +void timer_init(void) {} + +uint32_t timer_read(void) { return HAL_GetTick(); } diff --git a/src/layout.c b/src/layout.c index fc7c1e3..5ec3ae5 100644 --- a/src/layout.c +++ b/src/layout.c @@ -141,7 +141,9 @@ void layout_task(void) { const key_state_t *k = &key_matrix[i]; const bool last_key_press = bitmap_get(key_press_states, i); - if ((current_layer == 0) & eeconfig->options.xinput_enabled) { + if ((current_layer == 0) & + (eeconfig_get_gamepad_api(&eeconfig->options) != + GAMEPAD_API_DISABLED)) { // XInput key only applies to layer 0. We process it first since the // subsequent key processing may be skipped due to the gamepad options. if (CURRENT_PROFILE.gamepad_buttons[i] != GP_BUTTON_NONE) { diff --git a/src/main.c b/src/main.c index 16c07ba..1cb4817 100644 --- a/src/main.c +++ b/src/main.c @@ -22,6 +22,9 @@ #include "hid.h" #include "layout.h" #include "matrix.h" +#if defined(RGB_ENABLE) +#include "rgb.h" +#endif #include "tusb.h" #include "wear_leveling.h" #include "xinput.h" @@ -46,6 +49,10 @@ int main(void) { xinput_init(); layout_init(); command_init(); +#if defined(RGB_ENABLE) + if (!rgb_init()) + board_error_handler(); +#endif tud_init(BOARD_TUD_RHPORT); @@ -57,6 +64,11 @@ int main(void) { matrix_scan(); layout_task(); xinput_task(); +#if defined(RGB_ENABLE) + /* RGB copies/submits at most one complete frame and runs after the input + * path, so a host stream can never delay an analog scan. */ + rgb_task(); +#endif } return 0; diff --git a/src/migration.c b/src/migration.c index 80ed65b..ad09e25 100644 --- a/src/migration.c +++ b/src/migration.c @@ -68,6 +68,13 @@ static bool v1_5_profile_config_func(uint8_t profile, uint8_t *dst, NUM_ADVANCED_KEYS * MIGRATION_V1_5_ADVANCED_KEY_SIZE + \ NUM_MACRO_NODES * MIGRATION_V1_5_MACRO_NODE_SIZE + NUM_KEYS + 9 + 1) +static bool v1_6_global_config_func(uint8_t *dst, const uint8_t *src); +static bool v1_6_profile_config_func(uint8_t profile, uint8_t *dst, + const uint8_t *src); +#define MIGRATION_V1_6_GLOBAL_CONFIG_SIZE \ + (MIGRATION_V1_5_GLOBAL_CONFIG_SIZE + sizeof(eeconfig_rgb_t)) +#define MIGRATION_V1_6_PROFILE_CONFIG_SIZE MIGRATION_V1_5_PROFILE_CONFIG_SIZE + // Migration metadata for each configuration version. The first entry is // reserved for the initial version (v1.0) which does not require migration. static const migration_t migrations[] = { @@ -111,13 +118,23 @@ static const migration_t migrations[] = { .global_config_func = v1_5_global_config_func, .profile_config_func = v1_5_profile_config_func, }, + { + .version = 0x0106, + .global_config_size = MIGRATION_V1_6_GLOBAL_CONFIG_SIZE, + .profile_config_size = MIGRATION_V1_6_PROFILE_CONFIG_SIZE, + .global_config_func = v1_6_global_config_func, + .profile_config_func = v1_6_profile_config_func, + }, }; // An assertion to remind us to bump the persistent configuration version, and // implement a migration function if there is a change to the configuration // type. Update the assertion when a new version is added. -_Static_assert(MIGRATION_V1_5_GLOBAL_CONFIG_SIZE + - NUM_PROFILES * MIGRATION_V1_5_PROFILE_CONFIG_SIZE == +#define MIGRATION_CURRENT_GLOBAL_CONFIG_SIZE MIGRATION_V1_6_GLOBAL_CONFIG_SIZE +#define MIGRATION_CURRENT_PROFILE_CONFIG_SIZE MIGRATION_V1_6_PROFILE_CONFIG_SIZE + +_Static_assert(MIGRATION_CURRENT_GLOBAL_CONFIG_SIZE + + NUM_PROFILES * MIGRATION_CURRENT_PROFILE_CONFIG_SIZE == offsetof(eeconfig_t, magic_end), "Invalid configuration size"); @@ -132,6 +149,14 @@ bool migration_try_migrate(void) { return false; const uint16_t config_version = eeconfig->version; + bool version_is_supported = false; + for (uint32_t i = 0; i < M_ARRAY_SIZE(migrations); i++) + version_is_supported |= migrations[i].version == config_version; + /* Never reinterpret an unknown/future layout as the current one. Also do + * not merely repair magic_end on an otherwise corrupt current layout. */ + if (!version_is_supported || config_version >= EECONFIG_VERSION) + return false; + // We alternate between two buffers to save the memory. uint8_t current_buf = 0; uint8_t bufs[2][sizeof(eeconfig_t)]; @@ -390,4 +415,34 @@ bool v1_5_profile_config_func(uint8_t profile, uint8_t *dst, migration_memcpy(&dst, &src, NUM_KEYS + 9 + 1); return true; -} \ No newline at end of file +} + +//--------------------------------------------------------------------+ +// v1.5 -> v1.6 Migration (stable optional-RGB layout) +//--------------------------------------------------------------------+ + +static bool v1_6_global_config_func(uint8_t *dst, const uint8_t *src) { + if (((const eeconfig_t *)src)->version != 0x0105) + return false; + + uint8_t *const global_config = dst; + migration_memcpy(&dst, &src, MIGRATION_V1_5_GLOBAL_CONFIG_SIZE); + /* Bit 1 was named `_unused0` in v1.5, but historical v1.2 -> v1.3 + * migrations deliberately set it. It must not silently enable the new HID + * gamepad API on existing devices. */ + ((eeconfig_t *)global_config)->options.hid_gamepad_enabled = false; + migration_assign_uint8_t(&dst, 1); + migration_assign_uint8_t(&dst, RGB_DEFAULT_BRIGHTNESS); + migration_assign_uint8_t(&dst, 0); + migration_assign_uint8_t(&dst, RGB_DEFAULT_R); + migration_assign_uint8_t(&dst, RGB_DEFAULT_G); + migration_assign_uint8_t(&dst, RGB_DEFAULT_B); + return true; +} + +static bool v1_6_profile_config_func(uint8_t profile, uint8_t *dst, + const uint8_t *src) { + (void)profile; + migration_memcpy(&dst, &src, MIGRATION_V1_6_PROFILE_CONFIG_SIZE); + return true; +} diff --git a/src/rgb.c b/src/rgb.c new file mode 100644 index 0000000..8f63cb8 --- /dev/null +++ b/src/rgb.c @@ -0,0 +1,352 @@ +/* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#include "rgb.h" + +#if defined(RGB_ENABLE) + +#include "eeconfig.h" +#include "hardware/rgb_api.h" +#include "hardware/timer_api.h" + +_Static_assert(RGB_LED_COUNT <= 255, "RGB bridge encodes LED count in one byte"); +_Static_assert((RGB_FRAME_BYTES + RGB_FRAME_CHUNK_BYTES - 1u) / + RGB_FRAME_CHUNK_BYTES <= + 16u, + "RGB live-frame chunk bitmap is too small"); + +static eeconfig_rgb_t rgb_config; +static uint8_t rgb_frame[RGB_FRAME_BYTES]; +static uint8_t rgb_live_staging[RGB_FRAME_BYTES]; +static uint8_t rgb_output[RGB_FRAME_BYTES]; +static uint8_t previous_effect; +static uint8_t effect_phase; +static uint16_t live_chunks_received; +static uint32_t last_effect_ms; +static bool frame_dirty; + +static bool rgb_effect_is_valid(uint8_t effect) { + return effect == RGB_EFFECT_STATIC || effect == RGB_EFFECT_BREATHING || + effect == RGB_EFFECT_RAINBOW || + effect == RGB_EFFECT_RAINBOW_WAVE || effect == RGB_EFFECT_LIVE; +} + +static void rgb_write_pixel(uint8_t *frame, uint16_t index, uint8_t r, + uint8_t g, uint8_t b) { + frame[index * RGB_BYTES_PER_PIXEL] = r; + frame[index * RGB_BYTES_PER_PIXEL + 1u] = g; + frame[index * RGB_BYTES_PER_PIXEL + 2u] = b; +} + +static void rgb_fill_frame(uint8_t *frame, uint8_t r, uint8_t g, uint8_t b) { + for (uint16_t i = 0; i < RGB_LED_COUNT; i++) + rgb_write_pixel(frame, i, r, g, b); +} + +/* Compact integer HSV conversion for portable effects. Saturation is fixed at + * 100%; callers control value through the global brightness stage. */ +static void rgb_hue_to_rgb(uint8_t hue, uint8_t *r, uint8_t *g, uint8_t *b) { + const uint8_t region = hue / 43u; + const uint8_t offset = (uint8_t)((hue - region * 43u) * 6u); + const uint8_t rising = offset; + const uint8_t falling = (uint8_t)(255u - offset); + + switch (region) { + case 0: + *r = 255u; + *g = rising; + *b = 0u; + break; + case 1: + *r = falling; + *g = 255u; + *b = 0u; + break; + case 2: + *r = 0u; + *g = 255u; + *b = rising; + break; + case 3: + *r = 0u; + *g = falling; + *b = 255u; + break; + case 4: + *r = rising; + *g = 0u; + *b = 255u; + break; + default: + *r = 255u; + *g = 0u; + *b = falling; + break; + } +} + +static void rgb_render_effect(void) { + uint8_t r; + uint8_t g; + uint8_t b; + + switch (rgb_config.effect) { + case RGB_EFFECT_STATIC: + rgb_fill_frame(rgb_frame, rgb_config.color_r, rgb_config.color_g, + rgb_config.color_b); + break; + case RGB_EFFECT_BREATHING: { + const uint8_t value = effect_phase < 128u + ? (uint8_t)(effect_phase * 2u) + : (uint8_t)((255u - effect_phase) * 2u); + r = (uint8_t)(((uint32_t)rgb_config.color_r * (uint32_t)value + 127u) / + 255u); + g = (uint8_t)(((uint32_t)rgb_config.color_g * (uint32_t)value + 127u) / + 255u); + b = (uint8_t)(((uint32_t)rgb_config.color_b * (uint32_t)value + 127u) / + 255u); + rgb_fill_frame(rgb_frame, r, g, b); + break; + } + case RGB_EFFECT_RAINBOW: + rgb_hue_to_rgb(effect_phase, &r, &g, &b); + rgb_fill_frame(rgb_frame, r, g, b); + break; + case RGB_EFFECT_RAINBOW_WAVE: + for (uint16_t i = 0; i < RGB_LED_COUNT; i++) { + const uint8_t hue = + (uint8_t)(effect_phase + ((uint32_t)i * 256u) / RGB_LED_COUNT); + rgb_hue_to_rgb(hue, &r, &g, &b); + rgb_write_pixel(rgb_frame, i, r, g, b); + } + break; + default: + return; + } + frame_dirty = true; +} + +static bool rgb_store_config(const eeconfig_rgb_t *next) { + eeconfig_rgb_t stored = *next; + + /* Live frames are a volatile host-owned stream. Persist the last autonomous + * effect when another live-mode setting (brightness, enabled, ...) is saved, + * without forcing the running stream out of live mode. */ + if (stored.effect == RGB_EFFECT_LIVE) { + stored.effect = previous_effect == RGB_EFFECT_LIVE ? RGB_EFFECT_STATIC + : previous_effect; + } + return EECONFIG_WRITE(rgb, &stored); +} + +bool rgb_init(void) { + rgb_config = eeconfig->rgb; + if (!rgb_effect_is_valid(rgb_config.effect) || + rgb_config.effect == RGB_EFFECT_LIVE) + rgb_config.effect = RGB_EFFECT_STATIC; + previous_effect = rgb_config.effect; + effect_phase = 0u; + live_chunks_received = 0u; + last_effect_ms = timer_read(); + rgb_render_effect(); + memcpy(rgb_live_staging, rgb_frame, sizeof(rgb_live_staging)); + return rgb_backend_init(); +} + +void rgb_task(void) { + rgb_backend_task(); + + /* Once an off/zero-brightness frame has been submitted, freeze autonomous + * animation until output can be visible again. This avoids blank WS2812 DMA + * transfers and their interrupts every 20 ms while lighting is disabled. */ + if (rgb_config.enabled != 0u && rgb_config.brightness != 0u && + rgb_config.effect != RGB_EFFECT_STATIC && + rgb_config.effect != RGB_EFFECT_LIVE && + timer_elapsed(last_effect_ms) >= 20u) { + last_effect_ms = timer_read(); + effect_phase++; + rgb_render_effect(); + } + + if (!frame_dirty || rgb_backend_is_busy()) + return; + + for (uint16_t i = 0; i < RGB_FRAME_BYTES; i++) { + const uint32_t scaled = (uint32_t)rgb_frame[i] * + (uint32_t)rgb_config.brightness + + 127u; + rgb_output[i] = + rgb_config.enabled ? (uint8_t)(scaled / 255u) : (uint8_t)0u; + } + + if (rgb_backend_submit(rgb_output, RGB_LED_COUNT)) + frame_dirty = false; +} + +bool rgb_is_enabled(void) { return rgb_config.enabled != 0u; } + +bool rgb_set_enabled(bool enabled, bool persist) { + eeconfig_rgb_t next = rgb_config; + next.enabled = enabled ? 1u : 0u; + if (persist && !rgb_store_config(&next)) + return false; + rgb_config = next; + frame_dirty = true; + return true; +} + +uint8_t rgb_get_brightness(void) { return rgb_config.brightness; } + +bool rgb_set_brightness(uint8_t brightness, bool persist) { + eeconfig_rgb_t next = rgb_config; + next.brightness = brightness; + if (persist && !rgb_store_config(&next)) + return false; + rgb_config = next; + frame_dirty = true; + return true; +} + +uint8_t rgb_get_effect(void) { return rgb_config.effect; } + +bool rgb_set_effect(uint8_t effect, bool persist) { + if (!rgb_effect_is_valid(effect)) + return false; + + eeconfig_rgb_t next = rgb_config; + if (effect == RGB_EFFECT_LIVE && rgb_config.effect != RGB_EFFECT_LIVE) + previous_effect = rgb_config.effect; + next.effect = effect; + + /* Live streaming is deliberately runtime-only: selecting it can never + * generate flash traffic at frame rate or strand the board in live mode. */ + if (persist && effect != RGB_EFFECT_LIVE && !rgb_store_config(&next)) + return false; + rgb_config = next; + if (effect == RGB_EFFECT_LIVE) { + /* Entering (or explicitly restarting) live mode begins a fresh atomic + * transaction. An incomplete upload from an earlier live session must + * never be completed by a late chunk from a newer session. */ + live_chunks_received = 0u; + memcpy(rgb_live_staging, rgb_frame, sizeof(rgb_live_staging)); + } else { + effect_phase = 0u; + last_effect_ms = timer_read(); + rgb_render_effect(); + } + frame_dirty = true; + return true; +} + +bool rgb_restore_effect(void) { + uint8_t effect = previous_effect; + if (effect == RGB_EFFECT_LIVE) + effect = RGB_EFFECT_STATIC; + return rgb_set_effect(effect, false); +} + +const uint8_t *rgb_get_frame(void) { return rgb_frame; } + +bool rgb_get_pixel(uint8_t index, uint8_t *r, uint8_t *g, uint8_t *b) { + if (index >= RGB_LED_COUNT || r == NULL || g == NULL || b == NULL) + return false; + *r = rgb_frame[(uint16_t)index * 3u]; + *g = rgb_frame[(uint16_t)index * 3u + 1u]; + *b = rgb_frame[(uint16_t)index * 3u + 2u]; + return true; +} + +bool rgb_set_pixel(uint8_t index, uint8_t r, uint8_t g, uint8_t b) { + if (rgb_config.effect != RGB_EFFECT_LIVE || index >= RGB_LED_COUNT) + return false; + /* A direct pixel edit is its own publication and invalidates any incomplete + * chunked upload, otherwise later chunks could publish a mixed frame. */ + live_chunks_received = 0u; + rgb_write_pixel(rgb_frame, index, r, g, b); + rgb_write_pixel(rgb_live_staging, index, r, g, b); + frame_dirty = true; + return true; +} + +bool rgb_get_frame_chunk(uint16_t offset, uint8_t *dst, uint8_t len) { + if (dst == NULL || len == 0u || offset >= RGB_FRAME_BYTES || + (uint32_t)offset + len > RGB_FRAME_BYTES) + return false; + memcpy(dst, rgb_frame + offset, len); + return true; +} + +bool rgb_set_frame_chunk(uint16_t offset, const uint8_t *src, uint8_t len) { + uint16_t chunk; + uint16_t remaining; + uint8_t expected_len; + const uint16_t chunk_count = + (RGB_FRAME_BYTES + RGB_FRAME_CHUNK_BYTES - 1u) / + RGB_FRAME_CHUNK_BYTES; + const uint16_t required_chunks = + (uint16_t)((1u << chunk_count) - 1u); + + if (rgb_config.effect != RGB_EFFECT_LIVE || src == NULL || len == 0u || + offset >= RGB_FRAME_BYTES || + offset % RGB_FRAME_CHUNK_BYTES != 0u) + return false; + chunk = offset / RGB_FRAME_CHUNK_BYTES; + remaining = RGB_FRAME_BYTES - offset; + expected_len = remaining > RGB_FRAME_CHUNK_BYTES + ? RGB_FRAME_CHUNK_BYTES + : (uint8_t)remaining; + if (len != expected_len) + return false; + + /* Chunk zero starts a new transaction. The displayed frame changes only + * after every chunk has arrived, so a dropped/out-of-order upload never + * exposes a mixture of old and new pixels to DMA. */ + if (chunk == 0u) + live_chunks_received = 0u; + memcpy(rgb_live_staging + offset, src, len); + live_chunks_received |= (uint16_t)(1u << chunk); + if (live_chunks_received == required_chunks) { + memcpy(rgb_frame, rgb_live_staging, sizeof(rgb_frame)); + /* Close the transaction before accepting another chunk. In particular, a + * late non-zero chunk must not republish a mixture of two host frames. */ + live_chunks_received = 0u; + frame_dirty = true; + } + return true; +} + +bool rgb_fill(uint8_t r, uint8_t g, uint8_t b, bool persist) { + eeconfig_rgb_t next = rgb_config; + + if (rgb_config.effect != RGB_EFFECT_LIVE) { + next.color_r = r; + next.color_g = g; + next.color_b = b; + if (persist && !rgb_store_config(&next)) + return false; + rgb_config = next; + + /* In autonomous modes FILL updates the effect's base color. Re-render the + * selected effect instead of flashing a full-color frame until its next + * animation tick (rainbow effects intentionally ignore the base color). */ + rgb_render_effect(); + } else { + /* A direct fill is its own publication and cancels a partial chunked + * transaction for the same reason as a direct pixel edit. */ + live_chunks_received = 0u; + rgb_fill_frame(rgb_frame, r, g, b); + } + + /* Publish the visual change only after its optional persistent transaction + * succeeds. A failed flash write therefore leaves runtime and storage in the + * same state. */ + memcpy(rgb_live_staging, rgb_frame, sizeof(rgb_live_staging)); + frame_dirty = true; + return true; +} + +#endif diff --git a/src/usb_descriptors.c b/src/usb_descriptors.c index 752f306..26d4a26 100644 --- a/src/usb_descriptors.c +++ b/src/usb_descriptors.c @@ -22,7 +22,7 @@ #include "xinput.h" // Device descriptor -static const tusb_desc_device_t desc_device = { +static tusb_desc_device_t desc_device = { .bLength = sizeof(tusb_desc_device_t), .bDescriptorType = TUSB_DESC_DEVICE, .bcdUSB = USB_BCD_VERSION, @@ -104,16 +104,22 @@ static const uint8_t desc_raw_hid_report[] = { }; -#define CONFIG_TOTAL_LEN \ - (TUD_CONFIG_DESC_LEN + 2 * TUD_HID_DESC_LEN + TUD_HID_INOUT_DESC_LEN + \ - XINPUT_DESC_LEN) +// Standard HID gamepad report used as the portable alternative to XInput. +static const uint8_t desc_gamepad_report[] = {TUD_HID_REPORT_DESC_GAMEPAD()}; + +#define CONFIG_BASE_LEN \ + (TUD_CONFIG_DESC_LEN + 2 * TUD_HID_DESC_LEN + TUD_HID_INOUT_DESC_LEN) +#define CONFIG_TOTAL_LEN (CONFIG_BASE_LEN + XINPUT_DESC_LEN) + +_Static_assert(XINPUT_DESC_LEN >= TUD_HID_DESC_LEN, + "configuration buffer must fit either gamepad descriptor"); // Configuration descriptor static uint8_t desc_configuration[CONFIG_TOTAL_LEN]; #if defined(BOARD_USB_HS) // Device qualifier descriptor for USB HS -static const tusb_desc_device_qualifier_t desc_device_qualifier = { +static tusb_desc_device_qualifier_t desc_device_qualifier = { .bLength = sizeof(tusb_desc_device_qualifier_t), .bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER, .bcdUSB = USB_BCD_VERSION, @@ -237,25 +243,31 @@ _Static_assert(M_ARRAY_SIZE(desc_ms_os_20) == MS_OS_20_DESC_LEN, * * @return None */ -static void generate_desc_configuration(uint8_t *dst) { - uint8_t num_interfaces = USB_ITF_COUNT; - uint16_t total_length = CONFIG_TOTAL_LEN; - if (!eeconfig->options.xinput_enabled) { - // If XInput is not enabled, subtract the XInput descriptor length - // from the total configuration length. - num_interfaces--; - total_length -= XINPUT_DESC_LEN; - } +static void generate_desc_configuration(uint8_t *dst, tusb_speed_t speed) { + const gamepad_api_t gamepad_api = + eeconfig_get_gamepad_api(&eeconfig->options); + const uint16_t gamepad_desc_len = gamepad_api == GAMEPAD_API_XINPUT + ? XINPUT_DESC_LEN + : gamepad_api == GAMEPAD_API_HID + ? TUD_HID_DESC_LEN + : 0u; + const uint8_t num_interfaces = + gamepad_api == GAMEPAD_API_DISABLED ? USB_ITF_COUNT - 1u + : USB_ITF_COUNT; + const uint16_t total_length = CONFIG_BASE_LEN + gamepad_desc_len; uint8_t polling_interval = 1; #if defined(BOARD_USB_HS) - if (!eeconfig->options.high_polling_rate_enabled) - // If high polling rate is not enabled, use 1kHz polling rate = 8 frames for - // USB HS instead. - polling_interval = 8; + if (speed == TUSB_SPEED_HIGH && + !eeconfig->options.high_polling_rate_enabled) + /* At high speed bInterval is exponential: 4 means 2^(4-1) = 8 + * microframes, i.e. 1 ms. A raw value of 8 would mean 16 ms (62.5 Hz). */ + polling_interval = 4; +#else + (void)speed; #endif - const uint8_t src[] = { + const uint8_t base[] = { // Configuration descriptor header. Request maximum 500mA for the device TUD_CONFIG_DESCRIPTOR(1, num_interfaces, 0, total_length, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 500), @@ -272,18 +284,42 @@ static void generate_desc_configuration(uint8_t *dst) { sizeof(desc_raw_hid_report), EP_OUT_ADDR_RAW_HID, EP_IN_ADDR_RAW_HID, RAW_HID_EP_SIZE, polling_interval), - // XInput interface descriptor - XINPUT_DESCRIPTOR(USB_ITF_XINPUT, 0, EP_OUT_ADDR_XINPUT, - EP_IN_ADDR_XINPUT, polling_interval), }; - _Static_assert(sizeof(src) == CONFIG_TOTAL_LEN, - "Invalid configuration descriptor size"); - - memcpy(dst, src, sizeof(src)); + _Static_assert(sizeof(base) == CONFIG_BASE_LEN, + "Invalid base configuration descriptor size"); + + memset(dst, 0, CONFIG_TOTAL_LEN); + memcpy(dst, base, sizeof(base)); + + if (gamepad_api == GAMEPAD_API_XINPUT) { + const uint8_t xinput[] = { + XINPUT_DESCRIPTOR(USB_ITF_GAMEPAD, 0, EP_OUT_ADDR_XINPUT, + EP_IN_ADDR_GAMEPAD, polling_interval), + }; + _Static_assert(sizeof(xinput) == XINPUT_DESC_LEN, + "Invalid XInput descriptor size"); + memcpy(dst + sizeof(base), xinput, sizeof(xinput)); + } else if (gamepad_api == GAMEPAD_API_HID) { + const uint8_t hid_gamepad[] = { + TUD_HID_DESCRIPTOR(USB_ITF_GAMEPAD, 0, HID_ITF_PROTOCOL_NONE, + sizeof(desc_gamepad_report), EP_IN_ADDR_GAMEPAD, + CFG_TUD_HID_EP_BUFSIZE, polling_interval), + }; + _Static_assert(sizeof(hid_gamepad) == TUD_HID_DESC_LEN, + "Invalid HID gamepad descriptor size"); + memcpy(dst + sizeof(base), hid_gamepad, sizeof(hid_gamepad)); + } } const uint8_t *tud_descriptor_device_cb(void) { + /* Microsoft OS 2.0 discovery requires USB 2.1 for XInput. Standard HID and + * keyboard-only configurations advertise USB 2.0 and expose no BOS, so a + * host can never bind the XUSB compatible ID to the HID gamepad interface. */ + desc_device.bcdUSB = + eeconfig_get_gamepad_api(&eeconfig->options) == GAMEPAD_API_XINPUT + ? USB_BCD_VERSION + : 0x0200; return (const uint8_t *)&desc_device; } @@ -298,6 +334,11 @@ const uint8_t *tud_hid_descriptor_report_cb(uint8_t instance) { case USB_ITF_RAW_HID: return desc_raw_hid_report; + case USB_ITF_GAMEPAD: + return eeconfig_get_gamepad_api(&eeconfig->options) == GAMEPAD_API_HID + ? desc_gamepad_report + : NULL; + default: // Invalid interface, should be unreachable return NULL; @@ -306,17 +347,27 @@ const uint8_t *tud_hid_descriptor_report_cb(uint8_t instance) { const uint8_t *tud_descriptor_configuration_cb(uint8_t index) { // We only have one configuration so we don't need to check the index - generate_desc_configuration(desc_configuration); + generate_desc_configuration(desc_configuration, tud_speed_get()); return desc_configuration; } #if defined(BOARD_USB_HS) const uint8_t *tud_descriptor_device_qualifier_cb(void) { + /* The qualifier describes the same device at the other bus speed and must + * advertise the same USB revision as the active device descriptor. Compute + * it directly so correctness does not depend on callback ordering. */ + desc_device_qualifier.bcdUSB = + eeconfig_get_gamepad_api(&eeconfig->options) == GAMEPAD_API_XINPUT + ? USB_BCD_VERSION + : 0x0200; return (const uint8_t *)&desc_device_qualifier; } const uint8_t *tud_descriptor_other_speed_configuration_cb(uint8_t index) { - generate_desc_configuration(desc_other_speed_config); + const tusb_speed_t other_speed = tud_speed_get() == TUSB_SPEED_HIGH + ? TUSB_SPEED_FULL + : TUSB_SPEED_HIGH; + generate_desc_configuration(desc_other_speed_config, other_speed); desc_other_speed_config[1] = TUSB_DESC_OTHER_SPEED_CONFIG; return desc_other_speed_config; @@ -368,16 +419,25 @@ const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid) { return desc_str; } -const uint8_t *tud_descriptor_bos_cb(void) { return desc_bos; } +const uint8_t *tud_descriptor_bos_cb(void) { + return eeconfig_get_gamepad_api(&eeconfig->options) == GAMEPAD_API_XINPUT + ? desc_bos + : NULL; +} bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_request_t *request) { + if (eeconfig_get_gamepad_api(&eeconfig->options) != GAMEPAD_API_XINPUT) + return false; + if (stage != CONTROL_STAGE_SETUP) // Nothing to do for other stages return true; - if (request->bmRequestType_bit.type == TUSB_REQ_TYPE_VENDOR && - request->bRequest == MS_OS_20_VENDOR_CODE) { + if (request->bmRequestType_bit.direction == TUSB_DIR_IN && + request->bmRequestType_bit.type == TUSB_REQ_TYPE_VENDOR && + request->bmRequestType_bit.recipient == TUSB_REQ_RCPT_DEVICE && + request->bRequest == MS_OS_20_VENDOR_CODE && request->wValue == 0u) { switch (request->wIndex) { case 0x07: // Microsoft OS 2.0 descriptor request diff --git a/src/xinput.c b/src/xinput.c index f4420e6..9fe6ff2 100644 --- a/src/xinput.c +++ b/src/xinput.c @@ -17,7 +17,6 @@ #include "device/usbd_pvt.h" #include "eeconfig.h" -#include "lib/bitmap.h" #include "lib/usqrt.h" #include "matrix.h" #include "tusb.h" @@ -68,6 +67,13 @@ static uint8_t apply_analog_curve(uint8_t value, bool *is_key_end_deadzone) { const int16_t x1 = curve[i][0], y1 = curve[i][1]; const int16_t x2 = curve[i + 1][0], y2 = curve[i + 1][1]; + /* Treat a malformed persisted curve as linear input. Host-side validation + * prevents new invalid curves, but this guard also makes old/corrupt storage + * incapable of triggering division by zero here. */ + if (x2 <= x1) { + *is_key_end_deadzone = false; + return value; + } return y1 + (y2 - y1) * (value - x1) / (x2 - x1); } @@ -101,10 +107,14 @@ static const uint8_t joystick_axes[][2] = { // Endpoints for XInput communication static uint8_t endpoint_in; static uint8_t endpoint_out; - -// We track key press states independently of the layout module in case -// layout processing is disabled for some keys. -static bitmap_t key_press_states[] = MAKE_BITMAP(NUM_KEYS); +CFG_TUSB_MEM_SECTION CFG_TUSB_MEM_ALIGN +static uint8_t xinput_out_report[XINPUT_EP_SIZE]; + +/* Digital buttons are aggregated from scratch on every matrix scan. This + * keeps duplicate mappings pressed until their last physical key is released + * and cannot retain stale state across a profile/keymap change. */ +static uint16_t digital_buttons; +static bool gamepad_report_pending; // Track maximum analog values for analog buttons // (2 joysticks * 4 directions + 2 triggers) static uint16_t analog_states[10]; @@ -114,7 +124,82 @@ static uint16_t analog_states[10]; static xinput_report_t report = {.report_size = sizeof(xinput_report_t)}; -void xinput_init(void) {} +static int8_t hid_axis_from_xinput(int16_t value) { + return (int8_t)(value / 257); +} + +static uint8_t hid_hat_from_xinput(uint16_t buttons) { + const bool up = (buttons & XINPUT_BUTTON_UP) != 0u; + const bool down = (buttons & XINPUT_BUTTON_DOWN) != 0u; + const bool left = (buttons & XINPUT_BUTTON_LEFT) != 0u; + const bool right = (buttons & XINPUT_BUTTON_RIGHT) != 0u; + const bool effective_up = up && !down; + const bool effective_down = down && !up; + const bool effective_left = left && !right; + const bool effective_right = right && !left; + + if (effective_up && effective_right) + return GAMEPAD_HAT_UP_RIGHT; + if (effective_down && effective_right) + return GAMEPAD_HAT_DOWN_RIGHT; + if (effective_down && effective_left) + return GAMEPAD_HAT_DOWN_LEFT; + if (effective_up && effective_left) + return GAMEPAD_HAT_UP_LEFT; + if (effective_up) + return GAMEPAD_HAT_UP; + if (effective_right) + return GAMEPAD_HAT_RIGHT; + if (effective_down) + return GAMEPAD_HAT_DOWN; + if (effective_left) + return GAMEPAD_HAT_LEFT; + return GAMEPAD_HAT_CENTERED; +} + +static uint32_t hid_buttons_from_xinput(uint16_t buttons) { + uint32_t result = 0u; + + if ((buttons & XINPUT_BUTTON_A) != 0u) + result |= GAMEPAD_BUTTON_0; + if ((buttons & XINPUT_BUTTON_B) != 0u) + result |= GAMEPAD_BUTTON_1; + if ((buttons & XINPUT_BUTTON_X) != 0u) + result |= GAMEPAD_BUTTON_2; + if ((buttons & XINPUT_BUTTON_Y) != 0u) + result |= GAMEPAD_BUTTON_3; + if ((buttons & XINPUT_BUTTON_LB) != 0u) + result |= GAMEPAD_BUTTON_4; + if ((buttons & XINPUT_BUTTON_RB) != 0u) + result |= GAMEPAD_BUTTON_5; + if ((buttons & XINPUT_BUTTON_LS) != 0u) + result |= GAMEPAD_BUTTON_6; + if ((buttons & XINPUT_BUTTON_RS) != 0u) + result |= GAMEPAD_BUTTON_7; + if ((buttons & XINPUT_BUTTON_BACK) != 0u) + result |= GAMEPAD_BUTTON_8; + if ((buttons & XINPUT_BUTTON_START) != 0u) + result |= GAMEPAD_BUTTON_9; + if ((buttons & XINPUT_BUTTON_HOME) != 0u) + result |= GAMEPAD_BUTTON_10; + return result; +} + +static hid_gamepad_report_t hid_report_from_xinput(void) { + return (hid_gamepad_report_t){ + .x = hid_axis_from_xinput(report.joysticks[0]), + /* HID's conventional positive Y direction is down. */ + .y = (int8_t)-hid_axis_from_xinput(report.joysticks[1]), + .z = hid_axis_from_xinput(report.joysticks[2]), + .rz = (int8_t)-hid_axis_from_xinput(report.joysticks[3]), + .rx = (int8_t)(((uint16_t)report.lz * 254u) / 255u - 127), + .ry = (int8_t)(((uint16_t)report.rz * 254u) / 255u - 127), + .hat = hid_hat_from_xinput(report.buttons), + .buttons = hid_buttons_from_xinput(report.buttons), + }; +} + +void xinput_init(void) { gamepad_report_pending = true; } void xinput_process(uint8_t key) { const key_state_t *k = &key_matrix[key]; @@ -125,17 +210,8 @@ void xinput_process(uint8_t key) { switch (keycode) { case GP_BUTTON_A ... GP_BUTTON_RB: { - const bool last_key_press = bitmap_get(key_press_states, key); - - if (k->is_pressed & !last_key_press) - // Key press event - report.buttons |= keycode_to_bm[keycode]; - else if (!k->is_pressed & last_key_press) - // Key release event - report.buttons &= ~keycode_to_bm[keycode]; - - // Finally, update the key state - bitmap_set(key_press_states, key, k->is_pressed); + if (k->is_pressed) + digital_buttons |= keycode_to_bm[keycode]; break; } case GP_BUTTON_LS_UP ... GP_BUTTON_RT: { @@ -151,8 +227,23 @@ void xinput_process(uint8_t key) { void xinput_task(void) { static xinput_report_t last_report = {.report_size = sizeof(xinput_report_t)}; + static hid_gamepad_report_t last_hid_report = {0}; + static bool usb_was_ready; + const gamepad_api_t api = eeconfig_get_gamepad_api(&eeconfig->options); + + if (api == GAMEPAD_API_DISABLED) { + digital_buttons = 0u; + memset(analog_states, 0, sizeof(analog_states)); + return; + } + + const bool usb_ready = tud_ready(); + if (usb_ready && !usb_was_ready) + gamepad_report_pending = true; + usb_was_ready = usb_ready; bool is_key_end_deadzone = false; + report.buttons = digital_buttons; // Update trigger states in the report report.lz = apply_analog_curve(ANALOG_STATE(GP_BUTTON_LT), &is_key_end_deadzone); @@ -237,17 +328,30 @@ void xinput_task(void) { report.joysticks[i] = -joystick_state; } - if (tud_ready() && endpoint_in != 0 && !usbd_edpt_busy(0, endpoint_in) && - // Only send report if it has changed - memcmp(&report, &last_report, sizeof(xinput_report_t)) != 0) { - usbd_edpt_claim(0, endpoint_in); - usbd_edpt_xfer(0, endpoint_in, (uint8_t *)&report, sizeof(xinput_report_t)); - usbd_edpt_release(0, endpoint_in); - // Update the last report to the current one - memcpy(&last_report, &report, sizeof(xinput_report_t)); + if (api == GAMEPAD_API_XINPUT && usb_ready && endpoint_in != 0 && + !usbd_edpt_busy(0, endpoint_in) && + (gamepad_report_pending || + memcmp(&report, &last_report, sizeof(xinput_report_t)) != 0)) { + if (usbd_edpt_claim(0, endpoint_in) && + usbd_edpt_xfer(0, endpoint_in, (uint8_t *)&report, + sizeof(xinput_report_t))) { + memcpy(&last_report, &report, sizeof(xinput_report_t)); + gamepad_report_pending = false; + } + } else if (api == GAMEPAD_API_HID) { + const hid_gamepad_report_t hid_report = hid_report_from_xinput(); + if (usb_ready && tud_hid_n_ready(USB_ITF_GAMEPAD) && + (gamepad_report_pending || + memcmp(&hid_report, &last_hid_report, sizeof(hid_report)) != 0) && + tud_hid_n_report(USB_ITF_GAMEPAD, 0, &hid_report, + sizeof(hid_report))) { + last_hid_report = hid_report; + gamepad_report_pending = false; + } } // Reset analog states for the next scan + digital_buttons = 0u; memset(analog_states, 0, sizeof(analog_states)); } @@ -257,7 +361,11 @@ void xinput_task(void) { static void xinput_driver_init(void) {} -static void xinput_driver_reset(uint8_t rhport) {} +static void xinput_driver_reset(uint8_t rhport) { + endpoint_in = 0u; + endpoint_out = 0u; + gamepad_report_pending = true; +} static uint16_t xinput_driver_open(uint8_t rhport, const tusb_desc_interface_t *desc_intf, @@ -265,10 +373,16 @@ static uint16_t xinput_driver_open(uint8_t rhport, if (desc_intf->bInterfaceClass == TUSB_CLASS_VENDOR_SPECIFIC && desc_intf->bInterfaceSubClass == XINPUT_SUBCLASS_DEFAULT && desc_intf->bInterfaceProtocol == XINPUT_PROTOCOL_DEFAULT) { + TU_ASSERT(max_len >= XINPUT_DESC_LEN, 0); TU_ASSERT(usbd_open_edpt_pair(rhport, tu_desc_next(tu_desc_next(desc_intf)), desc_intf->bNumEndpoints, TUSB_XFER_INTERRUPT, &endpoint_out, &endpoint_in), 0); + /* Always consume the optional XInput OUT/rumble report. Leaving the OUT + * endpoint unarmed makes hosts retry it indefinitely. */ + TU_ASSERT(usbd_edpt_xfer(rhport, endpoint_out, xinput_out_report, + sizeof(xinput_out_report)), + 0); return XINPUT_DESC_LEN; } @@ -285,6 +399,9 @@ xinput_driver_control_xfer_cb(uint8_t rhport, uint8_t stage, static bool xinput_driver_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + if (ep_addr == endpoint_out) + return usbd_edpt_xfer(rhport, endpoint_out, xinput_out_report, + sizeof(xinput_out_report)); return true; } @@ -298,7 +415,11 @@ static const usbd_class_driver_t xinput_driver = { }; const usbd_class_driver_t *usbd_app_driver_get_cb(uint8_t *driver_count) { - *driver_count = 1; + if (eeconfig_get_gamepad_api(&eeconfig->options) != GAMEPAD_API_XINPUT) { + *driver_count = 0; + return NULL; + } + *driver_count = 1; return &xinput_driver; } diff --git a/tools/rgb_core_test.c b/tools/rgb_core_test.c new file mode 100644 index 0000000..5885b8d --- /dev/null +++ b/tools/rgb_core_test.c @@ -0,0 +1,195 @@ +/* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#include +#include + +#define NUM_PROFILES 1 +#define NUM_LAYERS 1 +#define NUM_KEYS 1 +#define NUM_ADVANCED_KEYS 1 +#define NUM_DYNAMIC_KEYSTROKE_MAX_BINDINGS 4 +#define NUM_MACRO_NODES 1 +#define RGB_ENABLE +#define RGB_LED_COUNT 21 + +#include "../src/rgb.c" + +const eeconfig_t *eeconfig; + +static eeconfig_t test_config; +static eeconfig_rgb_t last_persisted; +static uint8_t submitted_frame[RGB_FRAME_BYTES]; +static uint32_t now_ms; +static uint32_t submit_count; + +bool rgb_test_persist(const eeconfig_rgb_t *value) { + assert(value != NULL); + last_persisted = *value; + return true; +} + +uint32_t timer_read(void) { return now_ms; } + +bool rgb_backend_init(void) { return true; } + +bool rgb_backend_submit(const uint8_t *rgb, uint16_t led_count) { + assert(rgb != NULL); + assert(led_count == RGB_LED_COUNT); + memcpy(submitted_frame, rgb, sizeof(submitted_frame)); + submit_count++; + return true; +} + +bool rgb_backend_is_busy(void) { return false; } + +void rgb_backend_task(void) {} + +static void test_effects(void) { + uint8_t wave_before[RGB_FRAME_BYTES]; + + assert(rgb_set_effect(RGB_EFFECT_RAINBOW, false)); + assert(rgb_get_frame()[0] == 255u); + assert(rgb_get_frame()[1] == 0u); + assert(rgb_set_effect(RGB_EFFECT_RAINBOW_WAVE, false)); + assert(memcmp(rgb_get_frame(), rgb_get_frame() + RGB_BYTES_PER_PIXEL, + RGB_BYTES_PER_PIXEL) != 0); + assert(rgb_set_effect(RGB_EFFECT_BREATHING, false)); + assert(rgb_get_frame()[0] == 0u); + now_ms += 20u; + rgb_task(); + assert(rgb_get_frame()[0] != 0u); + assert(!rgb_set_effect(4u, false)); + + assert(rgb_set_effect(RGB_EFFECT_RAINBOW_WAVE, false)); + memcpy(wave_before, rgb_get_frame(), sizeof(wave_before)); + assert(rgb_fill(11u, 22u, 33u, true)); + assert(memcmp(wave_before, rgb_get_frame(), sizeof(wave_before)) == 0); + assert(last_persisted.color_r == 11u); + assert(last_persisted.color_g == 22u); + assert(last_persisted.color_b == 33u); + + assert(rgb_set_effect(RGB_EFFECT_STATIC, false)); + assert(rgb_fill(7u, 8u, 9u, false)); + assert(rgb_get_frame()[0] == 7u); + assert(rgb_get_frame()[1] == 8u); + assert(rgb_get_frame()[2] == 9u); +} + +static void test_hidden_animation_is_idle(void) { + uint32_t idle_submit_count; + + assert(rgb_set_effect(RGB_EFFECT_BREATHING, false)); + rgb_task(); + assert(rgb_set_enabled(false, false)); + rgb_task(); + idle_submit_count = submit_count; + now_ms += 100u; + rgb_task(); + assert(submit_count == idle_submit_count); + + assert(rgb_set_enabled(true, false)); + assert(rgb_set_brightness(0u, false)); + rgb_task(); + idle_submit_count = submit_count; + now_ms += 100u; + rgb_task(); + assert(submit_count == idle_submit_count); + assert(rgb_set_brightness(255u, false)); +} + +static void test_atomic_live_frame(void) { + uint8_t frame[RGB_FRAME_BYTES]; + uint8_t late_chunk[RGB_FRAME_BYTES - RGB_FRAME_CHUNK_BYTES]; + uint8_t previous[RGB_FRAME_BYTES]; + + for (uint32_t i = 0u; i < sizeof(frame); i++) + frame[i] = (uint8_t)(i * 3u + 1u); + memcpy(previous, rgb_get_frame(), sizeof(previous)); + + assert(rgb_set_effect(RGB_EFFECT_RAINBOW_WAVE, false)); + assert(!rgb_set_frame_chunk(0u, frame, RGB_FRAME_CHUNK_BYTES)); + assert(!rgb_set_pixel(0u, 1u, 2u, 3u)); + assert(rgb_set_effect(RGB_EFFECT_LIVE, false)); + memcpy(previous, rgb_get_frame(), sizeof(previous)); + assert(rgb_set_frame_chunk(0u, frame, RGB_FRAME_CHUNK_BYTES)); + assert(memcmp(rgb_get_frame(), previous, sizeof(previous)) == 0); + + /* A direct live edit cancels an incomplete chunk transaction. */ + assert(rgb_set_pixel(0u, 9u, 8u, 7u)); + memcpy(previous, rgb_get_frame(), sizeof(previous)); + assert(rgb_set_frame_chunk(RGB_FRAME_CHUNK_BYTES, + frame + RGB_FRAME_CHUNK_BYTES, + RGB_FRAME_BYTES - RGB_FRAME_CHUNK_BYTES)); + assert(memcmp(rgb_get_frame(), previous, sizeof(previous)) == 0); + + assert(rgb_set_frame_chunk(0u, frame, RGB_FRAME_CHUNK_BYTES)); + assert(rgb_set_frame_chunk(RGB_FRAME_CHUNK_BYTES, + frame + RGB_FRAME_CHUNK_BYTES, + RGB_FRAME_BYTES - RGB_FRAME_CHUNK_BYTES)); + assert(memcmp(rgb_get_frame(), frame, sizeof(frame)) == 0); + rgb_task(); + const uint32_t completed_publish_count = submit_count; + + /* Publication closes the chunk transaction. A delayed non-zero chunk from + * another frame cannot modify or republish the displayed frame. */ + memset(late_chunk, 0xa5, sizeof(late_chunk)); + assert(rgb_set_frame_chunk(RGB_FRAME_CHUNK_BYTES, late_chunk, + sizeof(late_chunk))); + assert(memcmp(rgb_get_frame(), frame, sizeof(frame)) == 0); + rgb_task(); + assert(submit_count == completed_publish_count); + + /* A direct fill likewise cancels a partial upload. */ + assert(rgb_set_frame_chunk(0u, frame, RGB_FRAME_CHUNK_BYTES)); + assert(rgb_fill(4u, 5u, 6u, false)); + memcpy(previous, rgb_get_frame(), sizeof(previous)); + assert(rgb_set_frame_chunk(RGB_FRAME_CHUNK_BYTES, + frame + RGB_FRAME_CHUNK_BYTES, + RGB_FRAME_BYTES - RGB_FRAME_CHUNK_BYTES)); + assert(memcmp(rgb_get_frame(), previous, sizeof(previous)) == 0); + + /* Leaving and re-entering live mode also discards an incomplete upload. */ + assert(rgb_restore_effect()); + assert(rgb_set_effect(RGB_EFFECT_LIVE, false)); + memcpy(previous, rgb_get_frame(), sizeof(previous)); + assert(rgb_set_frame_chunk(RGB_FRAME_CHUNK_BYTES, late_chunk, + sizeof(late_chunk))); + assert(memcmp(rgb_get_frame(), previous, sizeof(previous)) == 0); + assert(!rgb_set_frame_chunk(1u, frame, RGB_FRAME_CHUNK_BYTES)); + + assert(rgb_set_brightness(128u, true)); + assert(last_persisted.effect == RGB_EFFECT_RAINBOW_WAVE); + assert(rgb_get_effect() == RGB_EFFECT_LIVE); + assert(rgb_restore_effect()); + assert(rgb_get_effect() == RGB_EFFECT_RAINBOW_WAVE); +} + +int main(void) { + test_config.rgb = (eeconfig_rgb_t){ + .enabled = 1u, + .brightness = 255u, + .effect = RGB_EFFECT_STATIC, + .color_r = 120u, + .color_g = 40u, + .color_b = 10u, + }; + eeconfig = &test_config; + + assert(rgb_init()); + rgb_task(); + assert(submit_count == 1u); + assert(submitted_frame[0] == 120u); + assert(submitted_frame[1] == 40u); + assert(submitted_frame[2] == 10u); + + test_effects(); + test_hidden_animation_is_idle(); + test_atomic_live_frame(); + puts("rgb_core_test: ok"); + return 0; +} diff --git a/tools/test_include/eeconfig.h b/tools/test_include/eeconfig.h new file mode 100644 index 0000000..cbc9711 --- /dev/null +++ b/tools/test_include/eeconfig.h @@ -0,0 +1,28 @@ +/* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#pragma once + +#include "common.h" + +typedef struct { + uint8_t enabled; + uint8_t brightness; + uint8_t effect; + uint8_t color_r; + uint8_t color_g; + uint8_t color_b; +} eeconfig_rgb_t; + +typedef struct { + eeconfig_rgb_t rgb; +} eeconfig_t; + +extern const eeconfig_t *eeconfig; +bool rgb_test_persist(const eeconfig_rgb_t *value); + +#define EECONFIG_WRITE(field, value) rgb_test_persist(value) From 498ff15084389e94e2179ab647ade1d0e487d91f Mon Sep 17 00:00:00 2001 From: Fefedu973 <80718477+Fefedu973@users.noreply.github.com> Date: Sun, 23 Aug 2026 12:19:36 +0200 Subject: [PATCH 2/2] fix(rgb): address LEDs by board position instead of chain order The WS2812 strip on KBHE 75HE snakes back on every other row, so the rainbow wave ran backwards on half the board and host frames landed on the wrong keys. Keyboards can now declare led_index_map, led_position and key_to_led. The core, the effects and the RAW HID bridge address LEDs in one stable logical order and only the buffer handed to the backend is written in chain order; the rainbow wave spreads its phase along the board's X axis. Boards that declare none of the three keep the previous behaviour. --- README.md | 18 ++++ keyboards/kbhe-75he/keyboard.json | 70 +++++++++++---- scripts/make.py | 14 +++ scripts/metadata.py | 7 ++ scripts/schema/keyboard.py | 48 +++++++++- scripts/utils.py | 11 +++ src/rgb.c | 55 ++++++++++-- tools/rgb_topology_test.c | 143 ++++++++++++++++++++++++++++++ 8 files changed, 340 insertions(+), 26 deletions(-) create mode 100644 tools/rgb_topology_test.c diff --git a/README.md b/README.md index a00f1da..7866aff 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,24 @@ Backends must copy a submitted logical RGB frame and return immediately. DMA or another asynchronous peripheral mechanism should perform the physical transfer so lighting cannot stall matrix processing. +Strips are rarely wired in the order a user perceives. Three optional fields +describe the board so nothing above the output stage has to know its wiring: + +- `led_index_map` gives, for each logical LED, its position in the chain. The + core, the effects and the host protocol then all address LEDs in one stable + logical order, and only the buffer handed to the backend is written in chain + order. +- `led_position` gives each logical LED an `[x, y]` coordinate in any + consistent integer unit. The rainbow wave sweeps along X, so on a serpentine + board it stays a vertical band travelling sideways instead of snaking back on + every other row. +- `key_to_led` states which LED lights each key. Hosts must not infer it from + matching key and LED counts, so it is advertised in the keyboard metadata + only when the keyboard declares it. + +A board that omits them keeps the previous behaviour: the strip is treated as a +bare line of pixels in wiring order. + ### Gamepad API The two low bits in `eeconfig_options_t` select one mutually exclusive gamepad diff --git a/keyboards/kbhe-75he/keyboard.json b/keyboards/kbhe-75he/keyboard.json index 54891e3..55fcd80 100644 --- a/keyboards/kbhe-75he/keyboard.json +++ b/keyboards/kbhe-75he/keyboard.json @@ -56,13 +56,45 @@ "data_pin": "A15", "backend": "ws2812_tim2_ch1", "default_brightness": 50, - "default_color": [255, 255, 255] + "default_color": [255, 255, 255], + "led_index_map": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, + 14, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 42, 43, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, + 45, 44, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72 + ], + "led_position": [ + [0, 0], [238, 0], [429, 0], [619, 0], [810, 0], [1048, 0], + [1238, 0], [1429, 0], [1619, 0], [1857, 0], [2048, 0], [2238, 0], + [2429, 0], [2667, 0], [0, 238], [190, 238], [381, 238], [572, 238], + [762, 238], [952, 238], [1143, 238], [1334, 238], [1524, 238], [1715, 238], + [1905, 238], [2096, 238], [2286, 238], [2572, 238], [2905, 238], [48, 429], + [286, 429], [476, 429], [667, 429], [857, 429], [1048, 429], [1238, 429], + [1429, 429], [1619, 429], [1810, 429], [2000, 429], [2191, 429], [2381, 429], + [2643, 524], [2905, 429], [71, 619], [333, 619], [524, 619], [714, 619], + [905, 619], [1095, 619], [1286, 619], [1476, 619], [1667, 619], [1857, 619], + [2048, 619], [2238, 619], [2429, 619], [2905, 619], [24, 810], [238, 810], + [428, 810], [619, 810], [810, 810], [1000, 810], [1191, 810], [1381, 810], + [1572, 810], [1762, 810], [1953, 810], [2143, 810], [2405, 810], [2715, 857], + [24, 1000], [262, 1000], [500, 1000], [1214, 1000], [1905, 1000], [2096, 1000], + [2286, 1000], [2524, 1048], [2715, 1048], [2905, 1048] + ], + "key_to_led": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, + 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81 + ] }, "layout": { "keymap": [ [ { "key": 0 }, - { "key": 1, "x": 0.5 }, + { "key": 1, "x": 0.25 }, { "key": 2 }, { "key": 3 }, { "key": 4 }, @@ -77,7 +109,7 @@ { "key": 13, "x": 0.25 } ], [ - { "key": 14 }, + { "key": 14, "y": 0.25 }, { "key": 15 }, { "key": 16 }, { "key": 17 }, @@ -91,7 +123,7 @@ { "key": 25 }, { "key": 26 }, { "key": 27, "w": 2 }, - { "key": 28 } + { "key": 28, "x": 0.25 } ], [ { "key": 29, "w": 1.5 }, @@ -107,8 +139,8 @@ { "key": 39 }, { "key": 40 }, { "key": 41 }, - { "key": 42, "w": 1.5, "h": 2 }, - { "key": 43 } + { "key": 42, "x": 0.25, "w": 1.25, "h": 2 }, + { "key": 43, "x": 0.25 } ], [ { "key": 44, "w": 1.75 }, @@ -124,7 +156,7 @@ { "key": 54 }, { "key": 55 }, { "key": 56 }, - { "key": 57 } + { "key": 57, "x": 1.5 } ], [ { "key": 58, "w": 1.25 }, @@ -139,18 +171,22 @@ { "key": 67 }, { "key": 68 }, { "key": 69 }, - { "key": 70, "w": 2.75 }, - { "key": 71 } + { "key": 70, "w": 1.75 } ], [ - { "key": 72, "w": 1.25 }, + { "key": 71, "x": 14.25, "y": -0.75 } + ], + [ + { "key": 72, "y": -0.25, "w": 1.25 }, { "key": 73, "w": 1.25 }, { "key": 74, "w": 1.25 }, { "key": 75, "w": 6.25 }, - { "key": 76, "w": 1.25 }, - { "key": 77, "w": 1.25 }, - { "key": 78, "w": 1.25 }, - { "key": 79 }, + { "key": 76 }, + { "key": 77 }, + { "key": 78 } + ], + [ + { "key": 79, "x": 13.25, "y": -0.75 }, { "key": 80 }, { "key": 81 } ] @@ -159,9 +195,9 @@ "keymap": [ [ "KC_ESC", "KC_F1", "KC_F2", "KC_F3", "KC_F4", "KC_F5", "KC_F6", "KC_F7", "KC_F8", "KC_F9", "KC_F10", "KC_F11", "KC_F12", "KC_DEL", - "KC_GRV", "KC_1", "KC_2", "KC_3", "KC_4", "KC_5", "KC_6", "KC_7", "KC_8", "KC_9", "KC_0", "KC_MINS", "KC_EQL", "KC_BSPC", "KC_PGUP", - "KC_TAB", "KC_Q", "KC_W", "KC_E", "KC_R", "KC_T", "KC_Y", "KC_U", "KC_I", "KC_O", "KC_P", "KC_LBRC", "KC_RBRC", "KC_ENT", "KC_PGDN", - "KC_CAPS", "KC_A", "KC_S", "KC_D", "KC_F", "KC_G", "KC_H", "KC_J", "KC_K", "KC_L", "KC_SCLN", "KC_QUOT", "KC_NUHS", "KC_HOME", + "KC_GRV", "KC_1", "KC_2", "KC_3", "KC_4", "KC_5", "KC_6", "KC_7", "KC_8", "KC_9", "KC_0", "KC_MINS", "KC_EQL", "KC_BSPC", "KC_HOME", + "KC_TAB", "KC_Q", "KC_W", "KC_E", "KC_R", "KC_T", "KC_Y", "KC_U", "KC_I", "KC_O", "KC_P", "KC_LBRC", "KC_RBRC", "KC_ENT", "KC_PGUP", + "KC_CAPS", "KC_A", "KC_S", "KC_D", "KC_F", "KC_G", "KC_H", "KC_J", "KC_K", "KC_L", "KC_SCLN", "KC_QUOT", "KC_NUHS", "KC_PGDN", "KC_LSFT", "KC_NUBS", "KC_Z", "KC_X", "KC_C", "KC_V", "KC_B", "KC_N", "KC_M", "KC_COMM", "KC_DOT", "KC_SLSH", "KC_RSFT", "KC_UP", "KC_LCTL", "KC_LGUI", "KC_LALT", "KC_SPC", "KC_RALT", "MO(1)", "KC_RCTL", "KC_LEFT", "KC_DOWN", "KC_RGHT" ], diff --git a/scripts/make.py b/scripts/make.py index 0999750..69c1ba7 100644 --- a/scripts/make.py +++ b/scripts/make.py @@ -167,5 +167,19 @@ build_flags.define("RGB_DEFAULT_B", rgb.default_color[2]) build_flags.define(f"RGB_BACKEND_{rgb.backend.upper()}") + # Wiring order. Everything above the backend - effects, pixel commands and + # host frames - then addresses LEDs in logical order, and only the DMA + # buffer is written in chain order. + if rgb.led_index_map is not None: + build_flags.define("RGB_LED_INDEX_MAP", utils.to_c_array(rgb.led_index_map)) + + # Physical placement, normalized to 0-255 so position-aware effects stay + # integer-only on the MCU. Only the X axis has a consumer today, so the Y + # column stays in `keyboard.json` rather than in the firmware image. + if rgb.led_position is not None: + build_flags.define( + "RGB_LED_POS_X", utils.to_c_array(utils.normalize_axis(rgb.led_position, 0)) + ) + # Add source build flags env.Append(BUILD_FLAGS=build_flags.get_flags()) diff --git a/scripts/metadata.py b/scripts/metadata.py index 3eb75d4..4be9b39 100644 --- a/scripts/metadata.py +++ b/scripts/metadata.py @@ -74,6 +74,13 @@ def keyboard_metadata_def(): "protocolMajor": 1, "protocolMinor": 0, "effects": [0, 1, 2, 3, 7], + # Only advertised when the keyboard states it. A host must not + # infer the relationship from matching key and LED counts. + **( + {"keyToLed": kb_json.rgb.key_to_led} + if kb_json.rgb.key_to_led is not None + else {} + ), } if kb_json.rgb is not None else None diff --git a/scripts/schema/keyboard.py b/scripts/schema/keyboard.py index 31e3418..0a5978f 100644 --- a/scripts/schema/keyboard.py +++ b/scripts/schema/keyboard.py @@ -12,11 +12,19 @@ # this program. If not, see . from enum import Enum -from pydantic import BaseModel, Field, NonNegativeInt, PositiveFloat, PositiveInt +from pydantic import ( + BaseModel, + Field, + NonNegativeInt, + PositiveFloat, + PositiveInt, + model_validator, +) from typing import Annotated RGBComponent = Annotated[int, Field(ge=0, le=255)] +LEDIndex = Annotated[int, Field(ge=0, le=254)] class KeyboardUSBPort(str, Enum): @@ -131,6 +139,44 @@ class KeyboardRGB(BaseModel): backend: str = Field(pattern=r"^ws2812_tim2_ch1$") default_brightness: int = Field(ge=0, le=255, default=50) default_color: tuple[RGBComponent, RGBComponent, RGBComponent] = (255, 255, 255) + # Wiring order: `led_index_map[logical] = position in the LED chain`. Boards + # whose strip snakes across the PCB declare it so the core, the effects and + # the host protocol all address LEDs in one stable logical order. Omitting + # it means the chain order is the logical order. + led_index_map: list[LEDIndex] | None = None + # Physical `[x, y]` of each logical LED in any consistent integer unit + # (KBHE uses 0.1 mm from the PCB placement). Position-aware effects such as + # the rainbow wave sweep along these axes instead of along the chain. + led_position: list[tuple[int, int]] | None = None + # Which LED lights each key, in logical key order. `null` marks a key with + # no LED. Hosts must not infer this from matching key and LED counts. + key_to_led: list[LEDIndex | None] | None = None + + @model_validator(mode="after") + def validate_topology(self): + if self.led_index_map is not None: + if len(self.led_index_map) != self.num_leds: + raise ValueError( + f"Expected led_index_map to have {self.num_leds} entries" + ) + if sorted(self.led_index_map) != list(range(self.num_leds)): + raise ValueError( + "led_index_map must be a permutation of the LED chain" + ) + if self.led_position is not None: + if len(self.led_position) != self.num_leds: + raise ValueError( + f"Expected led_position to have {self.num_leds} entries" + ) + if any(x < 0 or y < 0 for x, y in self.led_position): + raise ValueError("led_position coordinates must not be negative") + if self.key_to_led is not None and any( + led is not None and led >= self.num_leds for led in self.key_to_led + ): + raise ValueError( + f"key_to_led entries must be below {self.num_leds}" + ) + return self # keyboard.json Schema diff --git a/scripts/utils.py b/scripts/utils.py index f8fd735..8a3be9b 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -71,6 +71,17 @@ def to_c_array(arr: list | bytes): return f"{{{', '.join(to_c_array(x) if isinstance(x, list) else str(x) for x in arr)}}}" +# Scale one axis of a physical LED placement to 0-255. The unit used in +# `keyboard.json` is arbitrary as long as it is consistent, so only the span +# matters; a board with a single column collapses to zero rather than dividing. +def normalize_axis(positions: list[tuple[int, int]], axis: int): + values = [position[axis] for position in positions] + low, span = min(values), max(values) - min(values) + if span == 0: + return [0] * len(values) + return [round((value - low) * 255 / span) for value in values] + + # Convert a Python dictionary to a C struct initializer def to_c_struct(value: dict): return f"{{{', '.join(f'.{k} = {v}' for k, v in value.items())}}}" diff --git a/src/rgb.c b/src/rgb.c index 8f63cb8..83b0408 100644 --- a/src/rgb.c +++ b/src/rgb.c @@ -19,6 +19,41 @@ _Static_assert((RGB_FRAME_BYTES + RGB_FRAME_CHUNK_BYTES - 1u) / 16u, "RGB live-frame chunk bitmap is too small"); +/* + * Optional board topology. `RGB_LED_INDEX_MAP` maps a logical LED to its place + * in the physical chain, and `RGB_LED_POS_X` gives each logical LED a 0-255 + * coordinate along the board's width. A board that declares neither keeps the + * previous behaviour: the strip is a bare line of pixels in wiring order. + */ +#if defined(RGB_LED_INDEX_MAP) +static const uint8_t rgb_led_index_map[RGB_LED_COUNT] = RGB_LED_INDEX_MAP; +#endif +#if defined(RGB_LED_POS_X) +static const uint8_t rgb_led_pos_x[RGB_LED_COUNT] = RGB_LED_POS_X; +#endif + +static inline uint16_t rgb_physical_index(uint16_t index) { +#if defined(RGB_LED_INDEX_MAP) + return rgb_led_index_map[index]; +#else + return index; +#endif +} + +/* + * Phase offset of one LED in a travelling effect. Spreading the offset over the + * board's X axis makes the wave a vertical band sweeping horizontally. Without + * declared positions the offset falls back to the chain index, which on a + * serpentine strip looks like a snake rather than a wave. + */ +static inline uint8_t rgb_wave_offset(uint16_t index) { +#if defined(RGB_LED_POS_X) + return rgb_led_pos_x[index]; +#else + return (uint8_t)(((uint32_t)index * 256u) / RGB_LED_COUNT); +#endif +} + static eeconfig_rgb_t rgb_config; static uint8_t rgb_frame[RGB_FRAME_BYTES]; static uint8_t rgb_live_staging[RGB_FRAME_BYTES]; @@ -118,8 +153,7 @@ static void rgb_render_effect(void) { break; case RGB_EFFECT_RAINBOW_WAVE: for (uint16_t i = 0; i < RGB_LED_COUNT; i++) { - const uint8_t hue = - (uint8_t)(effect_phase + ((uint32_t)i * 256u) / RGB_LED_COUNT); + const uint8_t hue = (uint8_t)(effect_phase + rgb_wave_offset(i)); rgb_hue_to_rgb(hue, &r, &g, &b); rgb_write_pixel(rgb_frame, i, r, g, b); } @@ -175,12 +209,17 @@ void rgb_task(void) { if (!frame_dirty || rgb_backend_is_busy()) return; - for (uint16_t i = 0; i < RGB_FRAME_BYTES; i++) { - const uint32_t scaled = (uint32_t)rgb_frame[i] * - (uint32_t)rgb_config.brightness + - 127u; - rgb_output[i] = - rgb_config.enabled ? (uint8_t)(scaled / 255u) : (uint8_t)0u; + for (uint16_t i = 0; i < RGB_LED_COUNT; i++) { + const uint16_t src = (uint16_t)(i * RGB_BYTES_PER_PIXEL); + const uint16_t dst = + (uint16_t)(rgb_physical_index(i) * RGB_BYTES_PER_PIXEL); + for (uint8_t channel = 0; channel < RGB_BYTES_PER_PIXEL; channel++) { + const uint32_t scaled = (uint32_t)rgb_frame[src + channel] * + (uint32_t)rgb_config.brightness + + 127u; + rgb_output[dst + channel] = + rgb_config.enabled ? (uint8_t)(scaled / 255u) : (uint8_t)0u; + } } if (rgb_backend_submit(rgb_output, RGB_LED_COUNT)) diff --git a/tools/rgb_topology_test.c b/tools/rgb_topology_test.c new file mode 100644 index 0000000..942f48c --- /dev/null +++ b/tools/rgb_topology_test.c @@ -0,0 +1,143 @@ +/* + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + */ + +#include +#include + +#define NUM_PROFILES 1 +#define NUM_LAYERS 1 +#define NUM_KEYS 1 +#define NUM_ADVANCED_KEYS 1 +#define NUM_DYNAMIC_KEYSTROKE_MAX_BINDINGS 4 +#define NUM_MACRO_NODES 1 +#define RGB_ENABLE +#define RGB_LED_COUNT 8 + +/* + * Two rows of four LEDs wired as a serpentine: the first row runs left to + * right, the second runs back right to left. Logical order stays row major. + */ +#define RGB_LED_INDEX_MAP {0, 1, 2, 3, 7, 6, 5, 4} +#define RGB_LED_POS_X {0, 85, 170, 255, 0, 85, 170, 255} + +#include "../src/rgb.c" + +const eeconfig_t *eeconfig; + +static eeconfig_t test_config; +static uint8_t submitted_frame[RGB_FRAME_BYTES]; +static uint32_t now_ms; + +bool rgb_test_persist(const eeconfig_rgb_t *value) { + assert(value != NULL); + return true; +} + +uint32_t timer_read(void) { return now_ms; } + +bool rgb_backend_init(void) { return true; } + +bool rgb_backend_submit(const uint8_t *rgb, uint16_t led_count) { + assert(rgb != NULL); + assert(led_count == RGB_LED_COUNT); + memcpy(submitted_frame, rgb, sizeof(submitted_frame)); + return true; +} + +bool rgb_backend_is_busy(void) { return false; } + +void rgb_backend_task(void) {} + +static const uint8_t *logical_pixel(uint8_t index) { + return rgb_get_frame() + (uint16_t)index * RGB_BYTES_PER_PIXEL; +} + +static const uint8_t *chain_pixel(uint8_t index) { + return submitted_frame + (uint16_t)index * RGB_BYTES_PER_PIXEL; +} + +static bool same_pixel(const uint8_t *a, const uint8_t *b) { + return memcmp(a, b, RGB_BYTES_PER_PIXEL) == 0; +} + +/* A logical write must reach the chain position the board declares. */ +static void test_index_map_is_applied_at_the_output(void) { + assert(rgb_set_effect(RGB_EFFECT_LIVE, false)); + assert(rgb_set_brightness(255u, false)); + assert(rgb_set_enabled(true, false)); + + for (uint8_t i = 0; i < RGB_LED_COUNT; i++) + assert(rgb_set_pixel(i, (uint8_t)(i + 1u), 0u, 0u)); + rgb_task(); + + for (uint8_t i = 0; i < RGB_LED_COUNT; i++) { + const uint8_t physical = rgb_led_index_map[i]; + assert(chain_pixel(physical)[0] == (uint8_t)(i + 1u)); + } + /* Logical LED 4 opens the second row but is wired last. */ + assert(chain_pixel(7)[0] == 5u); + assert(chain_pixel(4)[0] == 8u); +} + +/* + * The regression this file exists for: with the wave driven by the chain index + * the second row runs backwards, so LEDs in the same column disagree. + */ +static void test_wave_follows_the_board_not_the_wiring(void) { + assert(rgb_set_effect(RGB_EFFECT_RAINBOW_WAVE, false)); + + for (uint8_t column = 0; column < 4u; column++) { + assert(same_pixel(logical_pixel(column), logical_pixel(column + 4u))); + } + /* Neighbouring columns still differ, so the board is not a flat fill. */ + for (uint8_t column = 0; column + 1u < 4u; column++) { + assert(!same_pixel(logical_pixel(column), logical_pixel(column + 1u))); + } + + /* Advancing the phase shifts the same colors one step along the X axis. */ + uint8_t before[RGB_FRAME_BYTES]; + memcpy(before, rgb_get_frame(), sizeof(before)); + now_ms += 20u; + rgb_task(); + assert(memcmp(before, rgb_get_frame(), sizeof(before)) != 0); + for (uint8_t column = 0; column < 4u; column++) { + assert(same_pixel(logical_pixel(column), logical_pixel(column + 4u))); + } +} + +/* Autonomous effects reach the strip through the same translation. */ +static void test_static_fill_survives_the_translation(void) { + assert(rgb_set_effect(RGB_EFFECT_STATIC, false)); + assert(rgb_fill(10u, 20u, 30u, false)); + rgb_task(); + + for (uint8_t i = 0; i < RGB_LED_COUNT; i++) { + assert(chain_pixel(i)[0] == 10u); + assert(chain_pixel(i)[1] == 20u); + assert(chain_pixel(i)[2] == 30u); + } +} + +int main(void) { + test_config.rgb = (eeconfig_rgb_t){ + .enabled = 1u, + .brightness = 255u, + .effect = RGB_EFFECT_STATIC, + .color_r = 255u, + .color_g = 255u, + .color_b = 255u, + }; + eeconfig = &test_config; + assert(rgb_init()); + + test_index_map_is_applied_at_the_output(); + test_wave_follows_the_board_not_the_wiring(); + test_static_fill_survives_the_translation(); + + printf("rgb topology tests passed\n"); + return 0; +}