Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ on:
- "linker/**"
- "scripts/**"
- "src/**"
- "tools/**"
- ".github/workflows/build.yml"
- ".github/workflows/ci_define_matrix.py"
push:
Expand All @@ -26,6 +27,7 @@ on:
- "linker/**"
- "scripts/**"
- "src/**"
- "tools/**"
- ".github/workflows/build.yml"
- ".github/workflows/ci_define_matrix.py"
tags: ["v*"]
Expand All @@ -34,6 +36,20 @@ permissions:
contents: write

jobs:
rgb-host-tests:
name: RGB Host Tests
runs-on: ubuntu-latest
steps:
- name: Checkout libhmk
uses: actions/checkout@v6

- name: Compile and run portable RGB tests
run: |
cc -std=c11 -Wall -Wextra -Werror -Itools/test_include -Iinclude tools/rgb_core_test.c -o /tmp/rgb_core_test
/tmp/rgb_core_test
cc -std=c11 -Wall -Wextra -Werror -Itools/test_include -Iinclude tools/rgb_topology_test.c -o /tmp/rgb_topology_test
/tmp/rgb_topology_test

define-matrix:
name: Define Action Matrix
runs-on: ubuntu-latest
Expand Down Expand Up @@ -96,7 +112,7 @@ jobs:
release:
name: Create Releast Draft
if: github.ref_type == 'tag'
needs: build
needs: [build, rgb-host-tests]
runs-on: ubuntu-latest
steps:
- name: Download Firmware Artifacts
Expand Down
62 changes: 60 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 backends, 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

Expand Down Expand Up @@ -59,6 +62,61 @@ 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 do
not compile the renderer, allocate frame buffers, schedule an RGB task, or
install a hardware backend.

The portable core provides static, breathing, rainbow, rainbow-wave and live
effects. Live mode accepts atomic 60-byte RAW HID chunks: a frame is published
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 receive a frame that the core has already translated into physical
chain order. They must copy it and return immediately; DMA or another
asynchronous peripheral mechanism should perform the transfer so lighting does
not bit-bang pixels in the matrix loop.

Brightness defaults are board-owned and should be chosen for the board's LED
type and power path (KBHE starts at 50/255). The core also publishes a black
frame and pauses animation while USB is suspended; it restores the selected
effect after resume.

Strips are rarely wired in the order a user perceives. Three optional fields
describe the board without leaking physical wiring into the host protocol:

- `led_index_map` gives, for each logical LED, its position in the chain. The
core, effects and host protocol use logical order; only the buffer handed to
the backend is translated to 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 instead of reversing on alternate rows.
- `key_to_led` states which logical LED lights each key. `null` can represent a
key without an LED. Hosts must not infer this relationship from equal counts.

A board that omits these fields keeps the simple linear-strip behaviour.

### Gamepad API

The two low bits in `eeconfig_options_t` select one mutually exclusive gamepad
API: legacy XInput, a standard TinyUSB HID gamepad, or no gamepad interface.
HID is portable to operating systems without an XInput driver. Changing the
selection changes the USB descriptors and takes effect after USB
re-enumeration or reboot.

The v1.6 configuration migration clears the formerly reserved HID bit before
giving it this meaning, so existing devices cannot enable HID accidentally.
If malformed storage or a non-conforming host sets both API bits, XInput keeps
legacy priority; new raw-HID writes that request both are rejected.

## 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.
Expand Down
28 changes: 28 additions & 0 deletions hardware/stm32f723xx/board_def.h
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions include/commands.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ typedef enum {
COMMAND_GET_SERIAL,
COMMAND_SAVE_CALIBRATION_THRESHOLD,

/* Optional RGB bridge v1. Requests and responses use byte 0 for the command,
* byte 1 for status/reserved, and payload from 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,
Expand Down
68 changes: 66 additions & 2 deletions include/eeconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ 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. It is
// mutually exclusive with XInput and reuses a previously reserved bit.
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;
Expand All @@ -60,6 +62,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 corrupted data 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];
Expand All @@ -74,7 +111,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 0x0107

// Keyboard configuration
// Whenever there is a change in the configuration, `EECONFIG_VERSION` must be
Expand All @@ -98,6 +135,10 @@ typedef struct __attribute__((packed)) {
uint8_t current_profile;
// Last non-default profile index, used for profile swapping
uint8_t last_non_default_profile;
/* Keep board-wide RGB settings in the common persistent layout even when
* RGB is not compiled, so firmware variants never shift profile offsets.
* Live pixels themselves remain runtime-only. */
eeconfig_rgb_t rgb;
// End of global configurations

// Profiles
Expand Down Expand Up @@ -128,6 +169,7 @@ extern const eeconfig_t *eeconfig;
#define DEFAULT_OPTIONS \
{ \
.xinput_enabled = false, \
.hid_gamepad_enabled = false, \
.high_polling_rate_enabled = true, \
}
#endif
Expand Down Expand Up @@ -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
//--------------------------------------------------------------------+
Expand Down
22 changes: 22 additions & 0 deletions include/hardware/rgb_api.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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)

/* `rgb` is already translated from host-visible logical order to physical
* chain order. Backends must return immediately; the pointer remains valid
* only for 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
45 changes: 45 additions & 0 deletions include/rgb.h
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions include/tusb_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 8 additions & 5 deletions include/usb_descriptors.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading