Skip to content
Closed
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
57 changes: 55 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 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

Expand Down Expand Up @@ -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.
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,

/* 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,
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,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;
Expand All @@ -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];
Expand All @@ -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
Expand All @@ -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
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
21 changes: 21 additions & 0 deletions include/hardware/rgb_api.h
Original file line number Diff line number Diff line change
@@ -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
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