diff --git a/README.md b/README.md index 384e3e9..7866aff 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,56 @@ 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. + +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 +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..55fcd80 --- /dev/null +++ b/keyboards/kbhe-75he/keyboard.json @@ -0,0 +1,229 @@ +{ + "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], + "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.25 }, + { "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, "y": 0.25 }, + { "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, "x": 0.25 } + ], + [ + { "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, "x": 0.25, "w": 1.25, "h": 2 }, + { "key": 43, "x": 0.25 } + ], + [ + { "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, "x": 1.5 } + ], + [ + { "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": 1.75 } + ], + [ + { "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 }, + { "key": 77 }, + { "key": 78 } + ], + [ + { "key": 79, "x": 13.25, "y": -0.75 }, + { "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_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" + ], + [ + "_______", "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..69c1ba7 100644 --- a/scripts/make.py +++ b/scripts/make.py @@ -152,5 +152,34 @@ 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()}") + + # 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 a8f7b6f..4be9b39 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,47 @@ 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], + # 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 + ), } 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..0a5978f 100644 --- a/scripts/schema/keyboard.py +++ b/scripts/schema/keyboard.py @@ -12,7 +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): @@ -118,6 +130,55 @@ 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) + # 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 class Keyboard(BaseModel): name: str @@ -135,3 +196,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..8a3be9b 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}") @@ -69,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/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..83b0408 --- /dev/null +++ b/src/rgb.c @@ -0,0 +1,391 @@ +/* + * 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"); + +/* + * 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]; +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 + rgb_wave_offset(i)); + 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_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)) + 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/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; +} 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)