Skip to content

Personal QMK firmware user space

License

Notifications You must be signed in to change notification settings

filterpaper/qmk_userspace

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Summary

This is my personal self-contained QMK keymap repository that can be built in the userspace folder or using GitHub Actions with its workflow.

kb

Custom Features

 
 

Contextual Mod-Taps

Home row mods are very useful on small split keyboards and they can be refined through contextual configuration. By considering both preceding and subsequent keys, trigger accuracy will be significantly improved by enhancing QMK's Tap-Hold Configuration functions.

Decision macros

Setup the following boolean macros to make the mod-tap decision functions more concise and easier to read:

// Matches home rows on a 3x5_2 split keyboard
#define IS_HOMEROW(r) (r->event.key.row == 1 || r->event.key.row == 5)

// Matches home row Ctrl,Alt or GUI modifiers
#define IS_HOMEROW_CAG(k, r) ( \
    (IS_HOMEROW(r)) && (IS_QK_MOD_TAP(k) && (k) & (QK_LCTL|QK_LALT|QK_LGUI)))

// Home row mod-tap and the subsequent key are on the same side of the keyboard
// It compares the `keyrecord_t *record` values with the incoming `inter_record`
#define IS_UNILATERAL(r, i) ( \
    (r->event.key.row == 1 && 0 <= i.event.key.row && i.event.key.row <= 2) || \
    (r->event.key.row == 5 && 4 <= i.event.key.row && i.event.key.row <= 6) )

// The previous alphabetical keycode is within QUICK_TAP_TERM
// Interval can be adjusted to match typing speed
#define IS_TYPING(k) ( \
    ((uint8_t)(k) <= KC_Z || (uint8_t)(k) == KC_SPC) && \
    (last_input_activity_elapsed() < QUICK_TAP_TERM) )

The home row macros should be adjusted to match the right rows in the keyboard layout.

Default tap

To avoid input delays and accidental modifier activation while typing, the tap-hold key will default to its tap keycode when preceded by alphabetical keystrokes within QUICK_TAP_TERM. This is implemented in the pre_process_record_user function:

static bool        is_pressed[UINT8_MAX];
static uint16_t    inter_keycode;
static keyrecord_t inter_record;

bool pre_process_record_user(uint16_t keycode, keyrecord_t *record) {
    uint8_t const tap_keycode = QK_MOD_TAP_GET_TAP_KEYCODE(keycode);

    if (record->event.pressed) {
        // Press the tap keycode if the homerow mod-tap follows the previous key swiftly
        if (IS_HOMEROW_CAG(keycode, record) && IS_TYPING(inter_keycode)) {
            is_pressed[tap_keycode] = true;
            record->keycode         = tap_keycode;
        }
        // Cache incoming input for in-progress and subsequent tap-hold decisions
        inter_keycode = keycode;
        inter_record  = *record;
    }
    // Release the tap keycode if pressed
    else if (is_pressed[tap_keycode]) {
        is_pressed[tap_keycode] = false;
        record->keycode         = tap_keycode;
    }
    return true;
}

Shift is excluded from the mod-tap match to favour quicker capitalization.

Stringent unilateral tap

Modifiers should not be triggered when a mod-tap key is pressed together with another key on the same hand. To accomplish this, the mod-tap key is resolved as tap using the get_hold_on_other_key_press function when the subsequent input is on the same side of the keyboard:

#ifdef HOLD_ON_OTHER_KEY_PRESS_PER_KEY
bool get_hold_on_other_key_press(uint16_t keycode, keyrecord_t *record) {
    // Tap keycode with an overlapping key press on the same hand
    if (IS_UNILATERAL(record, inter_record)) {
        is_pressed[QK_MOD_TAP_GET_TAP_KEYCODE(keycode)] = true;
        record->tap.interrupted                         = false;
        record->tap.count++;
        process_record(record);
    }
    return false;
}
#endif

The unilateral conditional statement can be tweaked to allow activation of two or more same hand modifiers.

Permissive bilateral hold

With unilateral tap configured, all other overlapping hold-tap combination will be considered opposite hand or bilateral. Applying permissive hold will allow them to be triggered easily:

#ifdef PERMISSIVE_HOLD_PER_KEY
bool get_permissive_hold(uint16_t keycode, keyrecord_t *record) {
    // Hold modifier with a nested bilateral tap on the opposite hand
    return true;
}
#endif

Permissive hold can be tailored to match specific modifiers for frequent use-cases like Shift or exclude destructive ones like Ctrl.

Integration Summary

The contextual implementation uses the keycode container in the keyrecord_t structure which requires either REPEAT_KEY_ENABLE or COMBO_ENABLE feature. These functions have no effect after TAPPING_TERM. The output experience will be similar to ZMK's require-prior-idle-ms option and positional hold tap feature.

 
 

Layout Wrapper Macros

A single keymap layout can be shared with multiple keyboards by using C preprocessor macros. These macros are referenced in the keyboard JSON files, and the build process will expand them into a transient keymap.c file during compile time.

Basic setup

The split_3x5_2 layout is used as the base, with layers defined in layout.h. The following is an example of a default layer:

#define BASE \
    KC_Q,    KC_W,    KC_E,    KC_R,    KC_T,      KC_Y,    KC_U,    KC_I,    KC_O,    KC_P,    \
    KC_A,    KC_S,    KC_D,    KC_F,    KC_G,      KC_H,    KC_J,    KC_K,    KC_L,    KC_QUOT, \
    KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,      KC_N,    KC_M,    KC_COMM, KC_DOT,  KC_SLSH, \
                  LT(SYM,KC_TAB), LCA_T(KC_ENT),   RSFT_T(KC_SPC), LT(NUM,KC_BSPC)

Next, a wrapper alias to the layout used by the keyboard is also defined in the layout.h file. For example, the following defines a wrapper alias for the Cradio layout:

#define LAYOUT_34key_w(...) LAYOUT_split_3x5_2(__VA_ARGS__)

Macros are not replaced recursively in a single step. Wrapper alias is required for the compiler to expand them on different iterations.

Both layout and layer macros are referenced in the keyboard JSON file (cradio.json) as follows:

{
    "keyboard": "cradio",
    "keymap": "filterpaper",
    "layout": "LAYOUT_34key_w",
    "layers": [
        [ "BASE" ],
        [ "NUMB" ],
        [ "SYMB" ],
        [ "FUNC" ]
    ]
}

To include the layout macros in the layout.h file, add the following line into the config.h file:

#ifndef __ASSEMBLER__
#    include layout.h
#endif

The assembler definition will prevent that file from being assembled in any build process where C opcodes are not valid.

Running qmk compile cradio.json will cause the build process to construct a transient keymap.c using the wrapper macros for compilation.

Wrapping home row modifiers

Home row mods can be added to the layout macros in the same manner. The order of the home row modifiers is defined by these two macros:

#define HRML(k1,k2,k3,k4)  LCTL_T(k1), LALT_T(k2), LGUI_T(k3), LSFT_T(k4)
#define HRMR(k1,k2,k3,k4)  RSFT_T(k1), RGUI_T(k2), RALT_T(k3), RCTL_T(k4)

Both are then used to transform the home row elements in the following HRM wrapper macro for the split_3x5_2 layout:

#define HRM(k) HRM_TAPHOLD(k)
#define HRM_TAPHOLD( \
      l01, l02, l03, l04, l05,    r01, r02, r03, r04, r05,       \
      l06, l07, l08, l09, l10,    r06, r07, r08, r09, r10,       \
      l11, l12, l13, l14, l15,    r11, r12, r13, r14, r15,       \
                     l16, l17,    r16, r17                       \
) \
      l01, l02, l03, l04, l05,    r01, r02, r03, r04, r05,       \
HRML(l06, l07, l08, l09), l10,    r06, HRMR(r07, r08, r09, r10), \
      l11, l12, l13, l14, l15,    r11, r12, r13, r14, r15,       \
                     l16, l17,    r16, r17

The HRM() macro can now be used in the JSON file to add home row modifiers for layers that require them. For example:

"layers": [
    [ "HRM(BASE)" ],
    [ "HRM(COLE)" ],
    [ "NUMB" ],
    [ "SYMB" ],
    [ "FUNC" ]
],

When setup this way, the home row modifier order can be easily edited in the HRML and HRMR macros.

Adapting for a different layout

The base layout can be adapted for other split keyboards by expanding it with macros. The following example expands the split_3x5_2 layout to Corne's 42-key 3x6_3 layout (6 columns, 3 thumb keys) using the following wrapper to add additional keys to the outer columns:

#define LAYOUT_corne_w(...) LAYOUT_split_3x6_3(__VA_ARGS__)
// 3x5_2 to 42-key conversion
#define C_42(k) CONV_42(k)
#define CONV_42( \
         l01, l02, l03, l04, l05,    r01, r02, r03, r04, r05,          \
         l06, l07, l08, l09, l10,    r06, r07, r08, r09, r10,          \
         l11, l12, l13, l14, l15,    r11, r12, r13, r14, r15,          \
                        l16, l17,    r16, r17                          \
) \
KC_TAB,  l01, l02, l03, l04, l05,    r01, r02, r03, r04, r05, KC_BSPC, \
QK_GESC, l06, l07, l08, l09, l10,    r06, r07, r08, r09, r10, KC_SCLN, \
KC_LSFT, l11, l12, l13, l14, l15,    r11, r12, r13, r14, r15, KC_ENT,  \
         RSA_T(KC_ESC), l16, l17,    r16, r17, RAG_T(KC_DEL)

The JSON file for Corne (corne.json) will use the conversion and HRM macro in the following format:

{
    "keyboard": "crkbd/rev1",
    "keymap": "filterpaper",
    "layout": "LAYOUT_corne_w",
    "layers": [
        [ "C_42(HRM(BASE))" ],
        [ "C_42(NUMB)" ],
        [ "C_42(SYMB)" ],
        [ "C_42(FUNC)" ]
    ]
}

 
 

Code Snippets

Light configured layers keys

bool rgb_matrix_indicators_user(void) {
    if (get_highest_layer(layer_state) > 0) {
        uint8_t const layer = get_highest_layer(layer_state);
        for (uint8_t row = 0; row < MATRIX_ROWS; ++row) {
            for (uint8_t col = 0; col < MATRIX_COLS; ++col) {
                uint_fast8_t  const led = g_led_config.matrix_co[row][col];
                uint_fast16_t const key = keymap_key_to_keycode(layer, (keypos_t){col, row});
                if (led != NO_LED && key != KC_TRNS) {
                    rgb_matrix_set_color(g_led_config.matrix_co[row][col], RGB_BLUE);
                }
            }
        }
    }
    return false;
}

This code iterates over every row and column on a per-key RGB keyboard, searching for keys on the layer that have been configured (not KC_TRANS) and lighting the corresponding index location. It is set to activate on layers other than the default.

KB2040 NeoPixel

The controller's NeoPixel LED can be enabled for RGB Matrix with the following settings:

rules.mk

RGB_MATRIX_ENABLE = yes
RGB_MATRIX_DRIVER = WS2812

config.h

#define RGBW
#define WS2812_DI_PIN 17U
// Additional directives for a pair on a split keyboard:
#define RGB_MATRIX_LED_COUNT 2
#define RGB_MATRIX_SPLIT {1, 1}
#define SPLIT_TRANSPORT_MIRROR

g_led_config structure that matches the host PCB:

// An example for 3x5_2 split
led_config_t g_led_config = { {
    { 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0 },
    { 1, 1, 1, 1, 1 }, { 1, 1, 1, 1, 1 },
    { 1, 1, 1, 1, 1 }, { 1, 1, 1, 1, 1 }
}, {
    {109, 48}, {115, 48}
}, {
    0x0f, 0xf0
} };

 
 

Pro Micro Notes

RX/TX LEDs

The data LEDs on an Atmega32u4 Pro Micro can be used as indicators. They are located on pins B0 (RX) and D5 (TX) of the microcontroller. To use them with QMK's LED Indicators, flag the pin in the config.h file:

#define LED_CAPS_LOCK_PIN B0
#define LED_PIN_ON_STATE 0

For advance usage, set up the following macros to call both pins with GPIO functions:

// Pro Micro data LED pins
#define RXLED B0
#define TXLED D5
// GPIO control macros
#define RXLED_INIT setPinOutput(RXLED)
#define TXLED_INIT setPinOutput(TXLED)
#define RXLED_ON   writePinLow(RXLED)
#define TXLED_ON   writePinLow(TXLED)
#define RXLED_OFF  writePinHigh(RXLED)
#define TXLED_OFF  writePinHigh(TXLED)

Initialise both LEDs with the *_INIT macro on startup in the matrix_init_user(void) function. They can then be used as indicators with the *_ON and *_OFF macros.

ISP Flashing

Hardware

Wiring

To wire the USBasp programmer to the target controller, use the following connections:

USBasp GND  <-> Pro Micro GND
USBasp RST  <-> Pro Micro RST
USBasp VCC  <-> Pro Micro VCC
USBasp SCLK <-> Pro Micro 15/B1 (SCLK)
USBasp MISO <-> Pro Micro 14/B3 (MISO)
USBasp MOSI <-> Pro Micro 16/B2 (MOSI)

Atmel DFU bootloader

To replace the Pro Micro's default Caterina bootloader with Atmel-DFU, use the following USBasp command and fuses parameter:

avrdude -c usbasp -P usb -p atmega32u4 \
-U flash:w:bootloader_atmega32u4_1.0.0.hex:i \
-U lfuse:w:0x5E:m -U hfuse:w:0xD9:m -U efuse:w:0xF3:m

See the QMK ISP Flashing Guide for more details.

Command line flashing

To flash firmware to an AVR controller with Atmel DFU bootloader on macOS, use the following bash or zsh shell alias. It requires dfu-programmer from Homebrew to be installed:

dfu-flash() {
  if [ ! -f $1 ] || [ -z $1 ]; then
    echo "Usage: dfu-flash <firmware.hex> [left|right]"
    return 1
  fi
  until [ -n "$(ioreg -p IOUSB | grep ATm32U4DFU)" ]; do
    echo "Waiting for ATm32U4DFU bootloader..."; sleep 3
  done
  dfu-programmer atmega32u4 erase --force
  if [ $2 = "left" ]; then
    echo -e "\nFlashing left EEPROM" && \
    echo -e ':0F000000000000000000000000000000000001F0\n:00000001FF' | \
    dfu-programmer atmega32u4 flash --force --suppress-validation --eeprom STDIN
  elif [ $2 = "right" ]; then
    echo -e "\nFlashing right EEPROM" && \
    echo -e ':0F000000000000000000000000000000000000F1\n:00000001FF' | \
    dfu-programmer atmega32u4 flash --force --suppress-validation --eeprom STDIN
  fi
  echo -e "\nFlashing $1" && dfu-programmer atmega32u4 flash --force $1
  dfu-programmer atmega32u4 reset
}

 
 

Useful Links

Hardware Parts