From 636f2eeb108dc399da8b511a6c7576c74864a381 Mon Sep 17 00:00:00 2001 From: Roman Kuzmitskii Date: Wed, 13 May 2026 02:28:23 +0700 Subject: [PATCH] ws2812: add vcc enable pin support opt in mechanism for boards that gate the WS2812 chain vcc rail through one or more gpio. ws2812 draw ~1ma each even when displaying black long chains dominate standby drain on battery powered keyboards. configured via ws2812.vcc_enable_pins (array) and ws2812.vcc_active_low (bool) in keyboard.json. drivers/led/ws2812.{c,h}: new ws2812_set_power(bool on), compiled only when WS2812_VCC_ENABLE_PINS is defined. polarity from WS2812_VCC_ACTIVE_LOW. pins driven together in lockstep. The driver dedupes consecutive identical calls and lazily flips pins to output (write-then-set-output to avoid an active-low glitch on the first call). quantum/rgb_matrix/rgb_matrix_drivers.{c,h} and quantum/rgblight/rgblight_drivers.{c,h}: optional set_power vtable entry. ws2812 driver wires it only when WS2812_VCC_ENABLE_PINS is defined; other drivers and unconfigured ws2812 builds leave NULL. callers branch on set_power != NULL so unconfigured boards keep their existing render-zero-and-flush behavior. quantum/rgb_matrix/rgb_matrix.c: drive set_power from rgb_task_start with the composite enable state - user toggle, usb suspend (via rgb_matrix_set_suspend_state), idle timeout. gate path runs independently of RGB_MATRIX_SLEEP. quantum/rgblight/rgblight.c: drive set_power from rgblight_init, the rgblight_enable variants, and rgblight_set (which also catches the rgblight_disable variants via the enable=false branch). rgblight_suspend and rgblight_wakeup compile unconditionally; they take the gate path when set_power is wired and otherwise fall back to the legacy RGBLIGHT_SLEEP code or early-return. quantum/quantum.c: drop RGBLIGHT_SLEEP from the suspend and wakeup guards so gate-only boards receive the hooks. matches the existing rgb_matrix_set_suspend_state call pattern. data/schemas/keyboard.jsonschema and data/mappings/info_config.hjson: schema entries and macro mappings for the two new fields. docs/drivers/ws2812.md: new "VCC Enable Pin" section. Signed-off-by: Roman Kuzmitskii --- data/mappings/info_config.hjson | 2 ++ data/schemas/keyboard.jsonschema | 8 +++++- docs/drivers/ws2812.md | 19 ++++++++++++++ drivers/led/ws2812.c | 35 +++++++++++++++++++++++++ drivers/led/ws2812.h | 6 +++++ quantum/quantum.c | 4 +-- quantum/rgb_matrix/rgb_matrix.c | 34 ++++++++++++++++++++---- quantum/rgb_matrix/rgb_matrix_drivers.c | 3 +++ quantum/rgb_matrix/rgb_matrix_drivers.h | 3 +++ quantum/rgblight/rgblight.c | 34 +++++++++++++++++++++--- quantum/rgblight/rgblight_drivers.c | 3 +++ quantum/rgblight/rgblight_drivers.h | 3 +++ 12 files changed, 142 insertions(+), 12 deletions(-) diff --git a/data/mappings/info_config.hjson b/data/mappings/info_config.hjson index cd4a127844fc..673def9e5738 100644 --- a/data/mappings/info_config.hjson +++ b/data/mappings/info_config.hjson @@ -241,6 +241,8 @@ "WS2812_I2C_ADDRESS": {"info_key": "ws2812.i2c_address", "value_type": "hex"}, "WS2812_I2C_TIMEOUT": {"info_key": "ws2812.i2c_timeout", "value_type": "int"}, "WS2812_RGBW": {"info_key": "ws2812.rgbw", "value_type": "flag"}, + "WS2812_VCC_ENABLE_PINS": {"info_key": "ws2812.vcc_enable_pins", "value_type": "array"}, + "WS2812_VCC_ACTIVE_LOW": {"info_key": "ws2812.vcc_active_low", "value_type": "flag"}, "LAYOUTS": {"info_key": "layout_aliases", "value_type": "mapping"}, diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index d3a26923b541..ebe1d1df5313 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -1075,7 +1075,13 @@ "pin": {"$ref": "./definitions.jsonschema#/mcu_pin"}, "rgbw": {"type": "boolean"}, "i2c_address": {"$ref": "./definitions.jsonschema#/hex_number_2d"}, - "i2c_timeout": {"$ref": "./definitions.jsonschema#/unsigned_int"} + "i2c_timeout": {"$ref": "./definitions.jsonschema#/unsigned_int"}, + "vcc_enable_pins": { + "type": "array", + "items": {"$ref": "./definitions.jsonschema#/mcu_pin"}, + "minItems": 1 + }, + "vcc_active_low": {"type": "boolean"} } } } diff --git a/docs/drivers/ws2812.md b/docs/drivers/ws2812.md index 1d701138970b..eaa039c707ff 100644 --- a/docs/drivers/ws2812.md +++ b/docs/drivers/ws2812.md @@ -80,6 +80,25 @@ To enable RGBW conversion, add the following to your `config.h`: #define WS2812_RGBW ``` +### VCC Enable Pin {#vcc-enable-pin} + +WS2812 chips draw idle current (~1 mA per LED) even when displaying black. At long chain lengths this dominates standby drain on wireless keyboards. If your board has a GPIO that gates the chain's 5 V rail (e.g. through a load switch or FET), declare the pin(s) and QMK will toggle the rail with the RGB Matrix / RGBLight enable state, USB suspend, and (for RGB Matrix) the idle timeout. + +In `keyboard.json`: + +```json +"ws2812": { + "vcc_enable_pins": ["A8"] +} +``` + +|Key |Required|Description | +|------------------|--------|---------------------------------------------------------------------------------------------| +|`vcc_enable_pins` |Yes |One or more pins driven together. Use a list when multiple stages need to toggle in lockstep (e.g., main rail + boost converter).| +|`vcc_active_low` |No |Set to `true` when the pin is asserted low to enable. Defaults to active-high; omit otherwise.| + +The pins are driven as outputs lazily on the first transition; boards needing an explicit initial direction can `gpio_set_pin_output` in `keyboard_pre_init_kb`. The corresponding C-side macros are `WS2812_VCC_ENABLE_PINS` and `WS2812_VCC_ACTIVE_LOW`. + ## Driver Configuration {#driver-configuration} Driver selection can be configured in `rules.mk` as `WS2812_DRIVER`, or in `info.json` as `ws2812.driver`. Valid values are `bitbang` (default), `i2c`, `spi`, `pwm`, `vendor`, or `custom`. See below for information on individual drivers. diff --git a/drivers/led/ws2812.c b/drivers/led/ws2812.c index bf234c6f7dc6..a45c7ab8875d 100644 --- a/drivers/led/ws2812.c +++ b/drivers/led/ws2812.c @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "ws2812.h" +#include "gpio.h" #if defined(WS2812_RGBW) void ws2812_rgb_to_rgbw(ws2812_led_t *led) { @@ -13,3 +14,37 @@ void ws2812_rgb_to_rgbw(ws2812_led_t *led) { led->b -= led->w; } #endif + +#if defined(WS2812_VCC_ENABLE_PINS) + +static const pin_t ws2812_vcc_enable_pins[] = WS2812_VCC_ENABLE_PINS; + +void ws2812_set_power(bool on) { + static bool initialized = false; + static bool last_on = false; + + if (initialized && on == last_on) { + return; + } + + bool pin_value = on; +#if defined(WS2812_VCC_ACTIVE_LOW) + pin_value = !pin_value; +#endif + + for (uint8_t i = 0; i < ARRAY_SIZE(ws2812_vcc_enable_pins); i++) { + gpio_write_pin(ws2812_vcc_enable_pins[i], pin_value); + } + + if (!initialized) { + /* Latch-then-output order avoids active-low glitch on first call. */ + for (uint8_t i = 0; i < ARRAY_SIZE(ws2812_vcc_enable_pins); i++) { + gpio_set_pin_output(ws2812_vcc_enable_pins[i]); + } + initialized = true; + } + + last_on = on; +} + +#endif diff --git a/drivers/led/ws2812.h b/drivers/led/ws2812.h index 8013e5bd2dd4..11d403656dfd 100644 --- a/drivers/led/ws2812.h +++ b/drivers/led/ws2812.h @@ -15,6 +15,8 @@ #pragma once +#include + #include "util.h" /* @@ -95,3 +97,7 @@ void ws2812_set_color_all(uint8_t red, uint8_t green, uint8_t blue); void ws2812_flush(void); void ws2812_rgb_to_rgbw(ws2812_led_t *led); + +#if defined(WS2812_VCC_ENABLE_PINS) +void ws2812_set_power(bool on); +#endif diff --git a/quantum/quantum.c b/quantum/quantum.c index 1a57548fd287..bf5857734d4d 100644 --- a/quantum/quantum.c +++ b/quantum/quantum.c @@ -503,7 +503,7 @@ void suspend_power_down_quantum(void) { # endif // Turn off underglow -# if defined(RGBLIGHT_SLEEP) && defined(RGBLIGHT_ENABLE) +# if defined(RGBLIGHT_ENABLE) rgblight_suspend(); # endif @@ -541,7 +541,7 @@ __attribute__((weak)) void suspend_wakeup_init_quantum(void) { led_wakeup(); // Wake up underglow -#if defined(RGBLIGHT_SLEEP) && defined(RGBLIGHT_ENABLE) +#if defined(RGBLIGHT_ENABLE) rgblight_wakeup(); #endif diff --git a/quantum/rgb_matrix/rgb_matrix.c b/quantum/rgb_matrix/rgb_matrix.c index e2b268efa7c2..4cff140125cf 100644 --- a/quantum/rgb_matrix/rgb_matrix.c +++ b/quantum/rgb_matrix/rgb_matrix.c @@ -319,9 +319,18 @@ static void rgb_task_start(void) { (last_input_activity_elapsed() > (uint32_t)RGB_MATRIX_TIMEOUT) || #endif // RGB_MATRIX_TIMEOUT > 0 false; + bool off_state = suspend_backlight || !rgb_matrix_config.enable; + + if (rgb_matrix_driver.set_power != NULL) { + rgb_matrix_driver.set_power(!off_state); + if (off_state) { + rgb_task_state = SYNCING; + return; + } + } // Set effect to be renedered - rgb_current_effect = suspend_backlight || !rgb_matrix_config.enable ? 0 : rgb_matrix_config.mode; + rgb_current_effect = off_state ? 0 : rgb_matrix_config.mode; // next task rgb_task_state = RENDERING; @@ -524,16 +533,31 @@ void rgb_matrix_init(void) { eeconfig_update_rgb_matrix_default(); } eeconfig_debug_rgb_matrix(); // display current eeprom values + + if (rgb_matrix_driver.set_power != NULL) { + rgb_matrix_driver.set_power(rgb_matrix_config.enable); + } } void rgb_matrix_set_suspend_state(bool state) { -#ifdef RGB_MATRIX_SLEEP + bool gate_available = (rgb_matrix_driver.set_power != NULL); + +#ifndef RGB_MATRIX_SLEEP + /* Gate opts in independently of RGB_MATRIX_SLEEP. */ + if (!gate_available) { + return; + } +#endif + if (state && !suspend_state) { // only run if turning off, and only once - rgb_task_render(0); // turn off all LEDs when suspending - rgb_task_flush(0); // and actually flash led state to LEDs + if (gate_available) { + rgb_matrix_driver.set_power(false); + } else { + rgb_task_render(0); // turn off all LEDs when suspending + rgb_task_flush(0); // and actually flash led state to LEDs + } } suspend_state = state; -#endif } bool rgb_matrix_get_suspend_state(void) { diff --git a/quantum/rgb_matrix/rgb_matrix_drivers.c b/quantum/rgb_matrix/rgb_matrix_drivers.c index 3b45e82cb919..f7cba4faf324 100644 --- a/quantum/rgb_matrix/rgb_matrix_drivers.c +++ b/quantum/rgb_matrix/rgb_matrix_drivers.c @@ -151,6 +151,9 @@ const rgb_matrix_driver_t rgb_matrix_driver = { .flush = ws2812_flush, .set_color = ws2812_set_color, .set_color_all = ws2812_set_color_all, +#if defined(WS2812_VCC_ENABLE_PINS) + .set_power = ws2812_set_power, +#endif }; #endif diff --git a/quantum/rgb_matrix/rgb_matrix_drivers.h b/quantum/rgb_matrix/rgb_matrix_drivers.h index 1ea5f0817d31..9f6104332ae8 100644 --- a/quantum/rgb_matrix/rgb_matrix_drivers.h +++ b/quantum/rgb_matrix/rgb_matrix_drivers.h @@ -3,6 +3,7 @@ #pragma once +#include #include #if defined(RGB_MATRIX_AW20216S) @@ -46,6 +47,8 @@ typedef struct { void (*set_color_all)(uint8_t r, uint8_t g, uint8_t b); /* Flush any buffered changes to the hardware. */ void (*flush)(void); + /* Optional: toggle the chain's VCC-enable rail. NULL when not supported. */ + void (*set_power)(bool on); } rgb_matrix_driver_t; extern const rgb_matrix_driver_t rgb_matrix_driver; diff --git a/quantum/rgblight/rgblight.c b/quantum/rgblight/rgblight.c index 3c4a05844d92..232a57d81e4c 100644 --- a/quantum/rgblight/rgblight.c +++ b/quantum/rgblight/rgblight.c @@ -104,6 +104,12 @@ rgblight_config_t rgblight_config; rgblight_status_t rgblight_status = {.timer_enabled = false}; bool is_rgblight_initialized = false; +static inline void rgblight_set_power(bool on) { + if (rgblight_driver.set_power != NULL) { + rgblight_driver.set_power(on); + } +} + #ifdef RGBLIGHT_SLEEP static bool is_suspended; static bool pre_suspend_enabled; @@ -225,6 +231,7 @@ void rgblight_init(void) { rgblight_timer_init(); // setup the timer rgblight_driver.init(); + rgblight_set_power(rgblight_config.enable); if (rgblight_config.enable) { rgblight_mode_noeeprom(rgblight_config.mode); @@ -371,12 +378,14 @@ void rgblight_enable(void) { // No need to update EEPROM here. rgblight_mode() will do that, actually // eeconfig_update_rgblight(&rgblight_config); dprintf("rgblight enable [EEPROM]: rgblight_config.enable = %u\n", rgblight_config.enable); + rgblight_set_power(true); rgblight_mode(rgblight_config.mode); } void rgblight_enable_noeeprom(void) { rgblight_config.enable = 1; dprintf("rgblight enable [NOEEPROM]: rgblight_config.enable = %u\n", rgblight_config.enable); + rgblight_set_power(true); rgblight_mode_noeeprom(rgblight_config.mode); } @@ -822,9 +831,15 @@ void rgblight_blink_layer_repeat_helper(void) { #endif -#ifdef RGBLIGHT_SLEEP - void rgblight_suspend(void) { +#ifndef RGBLIGHT_SLEEP + /* Gate opts in independently of RGBLIGHT_SLEEP. */ + if (rgblight_driver.set_power == NULL) { + return; + } + rgblight_set_power(false); + return; +#else rgblight_timer_disable(); if (!is_suspended) { is_suspended = true; @@ -838,9 +853,17 @@ void rgblight_suspend(void) { rgblight_disable_noeeprom(); } +#endif } void rgblight_wakeup(void) { +#ifndef RGBLIGHT_SLEEP + if (rgblight_driver.set_power == NULL) { + return; + } + rgblight_set_power(rgblight_config.enable); + return; +#else is_suspended = false; if (pre_suspend_enabled) { @@ -854,12 +877,15 @@ void rgblight_wakeup(void) { # endif rgblight_timer_enable(); -} - #endif +} void rgblight_set(void) { if (!rgblight_config.enable) { + if (rgblight_driver.set_power != NULL) { + rgblight_set_power(false); + return; + } for (uint8_t i = rgblight_ranges.effect_start_pos; i < rgblight_ranges.effect_end_pos; i++) { rgblight_driver.set_color(rgblight_led_index(i), 0, 0, 0); } diff --git a/quantum/rgblight/rgblight_drivers.c b/quantum/rgblight/rgblight_drivers.c index ef986ee13c55..178ad93b9fa2 100644 --- a/quantum/rgblight/rgblight_drivers.c +++ b/quantum/rgblight/rgblight_drivers.c @@ -11,6 +11,9 @@ const rgblight_driver_t rgblight_driver = { .set_color = ws2812_set_color, .set_color_all = ws2812_set_color_all, .flush = ws2812_flush, +#if defined(WS2812_VCC_ENABLE_PINS) + .set_power = ws2812_set_power, +#endif }; #elif defined(RGBLIGHT_APA102) diff --git a/quantum/rgblight/rgblight_drivers.h b/quantum/rgblight/rgblight_drivers.h index 16fb4cebd638..88828bf3e07a 100644 --- a/quantum/rgblight/rgblight_drivers.h +++ b/quantum/rgblight/rgblight_drivers.h @@ -3,6 +3,7 @@ #pragma once +#include #include typedef struct { @@ -10,6 +11,8 @@ typedef struct { void (*set_color)(int index, uint8_t red, uint8_t green, uint8_t blue); void (*set_color_all)(uint8_t red, uint8_t green, uint8_t blue); void (*flush)(void); + /* Optional: toggle the chain's VCC-enable rail. NULL when not supported. */ + void (*set_power)(bool on); } rgblight_driver_t; extern const rgblight_driver_t rgblight_driver;