qmk_userspace icon indicating copy to clipboard operation
qmk_userspace copied to clipboard

Personal QMK firmware user space

Summary

This is my personal userspace for QMK Firmware. It is setup as a self-contained folder that avoids placing keymap.c files into the QMK source folders.

  • Keyboard keymaps are saved as QMK Configurator JSON format
  • Shared source files build with rules.mk.

corneplanck

Building Userspace

This repository can be built as QMK's userspace with users folder by running qmk compile on the JSON files. Actions can also be leveraged to do likewise on a GitHub container with the build.yml workflow.

Features

  • Shared layout wrapper macros
  • Combos simplified with preprocessors
  • Tap-hold clipboard shortcuts
  • OLED indicators and animation
    • Bongocat with compressed RLE frames
    • Luna (and Felix) the dog
    • Soundmonster indicator icons
    • Katakana コルネ font file
  • RGB matrix lighting and effects
    • Custom "candy" matrix effect
    • Layer indicators of active keys
  • Word processing features
    • Caps Unlock to toggle caps lock following a word
    • Autocorrection for typos

Corne (CRKBD) OLED display

Corne keyboard can be build with few OLED display options. The -e OLED= option can select pet animation on primary OLED with status icons on secondary. Animation are key stroke driven by oled_tap_timer. To use WPM, add -e WPM_ENABLE=yes.

Bongocat or Luna and Felix

Bongocat animation will be build as the default pet. Use the following option to select Luna or Felix:

qmk compile -e OLED=LUNA corne.json
qmk compile -e OLED=FELIX corne.json

Logo file

Icons used to render keyboard state is stored in glcdfont.. Images in that file can be viewed and edited with:

Code Snippets

Light configured layers keys

if (get_highest_layer(layer_state); > COLEMAK) {
    uint8_t layer = get_highest_layer(layer_state);
    for (uint8_t row = 0; row < MATRIX_ROWS; ++row) {
        for (uint8_t col = 0; col < MATRIX_COLS; ++col) {
            if (g_led_config.matrix_co[row][col] != NO_LED &&
            keymap_key_to_keycode(layer, (keypos_t){col, row}) != KC_TRNS) {
                rgb_matrix_set_color(g_led_config.matrix_co[row][col], RGB_LAYER);
            }
        }
    }
}

Code loops through every row and column on a per-key RGB board, scanning for configured keys (not KC_TRANS) and lighting that index location. It is configured to activate on non-default layers. This can be further customised using layer switch condition inside the last if statement.

Tap hold macros

#define TH_W LT(0, KC_W)

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    switch (keycode) {
    case TH_W:
        // Unmatched return on tap
        if (record->tap.count) { return true; }
        // Send macro string on hold
        else if (record->event.pressed) { SEND_STRING(":wq"); }
        return false;
    }
    return true; // continue with unmatched keycodes
}

Tap hold shortcuts using layer tap (LT()) uses less firmware space than tap dance (~35 bytes per shortcut). Macro W_TH replaces KC_W on the key map (keymap[]), and the code will intercept hold function of LT() to send the macro string. There are more examples in QMK's Intercepting Mod Taps.

Combo helper macros

The QMK combo code file combos.c is modified from Jane Bernhardt's helper macros to simplify combo management. New shortcuts are added to combos.inc as one-liners and preprocessor macros will generate required QMK combo source codes at compile time.

Pro Micro RX/TX LEDs

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

#define LED_CAPS_LOCK_PIN B0
#define LED_PIN_ON_STATE 0

For advance usage, setup 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 LEDs with the *_INIT macro on startup inside matrix_scan_user(void). Subsequently, LEDs can be used as indicators with the *_ON and *_OFF macros that follows.

Tap Hold Configuration

Home row mods using mod tap is finicky with typing habits. They are a bit more usable with the use of a tap timer to reduce false positives while typing:

Tap timer

Setup a tap timer to detect recent key presses in process_record_user and a boolean macro for "typing interval" when the last key press is within 1.3 times TAPPING_TERM:

static uint16_t tap_timer = 0;
#define IS_TYPING() (timer_elapsed(tap_timer) < TAPPING_TERM * 1.3)

bool process_record_user(uint16_t const keycode, keyrecord_t *record) {
    if (record->event.pressed) {
        tap_timer = timer_read();
    }
    return true;
}

Ignore Mod Tap Interrupt

#define IGNORE_MOD_TAP_INTERRUPT

Allow rolling of tap hold keys as default.

Increase tapping term while typing

#define TAPPING_TERM 200
#define TAPPING_TERM_PER_KEY
#define IS_MOD_TAP(kc) (QK_MOD_TAP <= kc && kc <= QK_MOD_TAP_MAX)

uint16_t get_tapping_term(uint16_t keycode, keyrecord_t *record) {
    if (IS_MOD_TAP(keycode) && IS_TYPING()) {
      return TAPPING_TERM * 1.3;
    }
    return TAPPING_TERM;
}

Increases tapping term within typing interval to avoid accidentally trigger of mod taps while typing.

Permissive hold for thumb shift

#define PERMISSIVE_HOLD_PER_KEY
#define MODTAP_BIT(kc) ((kc >> 8) & 0x0f)

bool get_permissive_hold(uint16_t keycode, keyrecord_t *record) {
    if (MODTAP_BIT(keycode) & MOD_MASK_SHIFT && !IS_TYPING()) {
      return true;
    }
    return false;
}

Activate Shift mod tap with another nested key press when not within typing interval.

Hold on layer tap

#define HOLD_ON_OTHER_KEY_PRESS_PER_KEY
#define QK_LAYER_TAP_1 0x4100

bool get_hold_on_other_key_press(uint16_t keycode, keyrecord_t *record) {
    if (QK_LAYER_TAP_1 <= keycode && keycode <= QK_LAYER_TAP_MAX) {
      return true;
    }
    return false;
}

Trigger layer taps immediately with another key press.

Layout wrapper macros

Basic setup

A single key map layout is shared with multiple keyboards using C preprocessors macros. They are referenced in the keyboard JSON files and the build process will expand them into a transient keymap.c file during compile time.

The split_3x6_3 layout is used as the base and defined in layout.h file. This is an example of the number layer:

#define _NUMB \
    _______, _______, KC_1,    KC_2,    KC_3,    _______,     KC_HOME, KC_PGDN, KC_PGUP, KC_END,  KC_DQUO, _______, \
    _______, _______, KC_4,    KC_5,    KC_6,    _______,     KC_LEFT, KC_DOWN, KC_UP,   KC_RGHT, KC_QUOT, _______, \
    _______, _______, KC_7,    KC_8,    KC_9,    KC_0,        KC_INS,  _______, _______, _______, _______, _______, \
                               _______, MO(FNC), _______,     _______, _______, _______

Next, a wrapper aliases to the layout used by the keyboard is also defined in layout.h file, e.g. for CRKBD:

#define LAYOUT_corne_w(...) LAYOUT_split_3x6_3(__VA_ARGS__)

Both macros are referenced in the keyboard's JSON file with the following format:

{
    "keyboard": "crkbd/rev1",
    "keymap": "filterpaper",
    "layout": "LAYOUT_corne_w",
    "layers": [
        [ "_BASE" ],
        [ "_NUMB" ],
        [ "_SYMB" ],
        [ "_FUNC" ]
    ]
}

Append #include layout.h to config.h. The build process will construct a transient keymap.c from JSON and build it with config.h. C preprocessor will reference macros in layout.h to expand them into the real layout structure.

Wrapping home row modifiers

Home row mods can be wrapped over the layout macros. Order of home row modifiers are defined with these two macros:

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

Both of them are then placed within the HRM macro for the split_3x6_3 base:

#define HRM(k) HRM_TAPHOLD(k)
#define HRM_TAPHOLD( \
          k01, k02, k03, k04, k05, k06,    k07, k08, k09, k10, k11, k12, \
          k13, k14, k15, k16, k17, k18,    k19, k20, k21, k22, k23, k24, \
          k25, k26, k27, k28, k29, k30,    k31, k32, k33, k34, k35, k36, \
                         k37, k38, k39,    k40, k41, k42 \
) \
    k01,      k02, k03, k04, k05,  k06,    k07,      k08, k09, k10, k11,  k12, \
    k13, HRML(k14, k15, k16, k17), k18,    k19, HRMR(k20, k21, k22, k23), k24, \
    k25,      k26, k27, k28, k29,  k30,    k31,   TH(k32, k33, k34, k35), k36, \
                        k37, k38,  k39,    k40, k41, k42

They come together in the JSON file, by wrapping HRM() on the layers that require them, e.g.:

"layers": [
    [ "HRM(_BASE)" ],
    [ "HRM(_COLE)" ],
    [ "_NUMB" ],
    [ "_SYMB" ],
    [ "_FUNC" ]
],

Adapting layouts

The same base layout is shared and adapted for other split keyboards by trimming them with macros. Corne's 42-key 3x6_3 (6-column, 3-thumb) is reduced to a split 34-key 3x5_2 (5-column, 2-thumb) with the following wrapper macro to exclude outer columns and thumb keys:

#define LAYOUT_34key_w(...) LAYOUT(__VA_ARGS__)
// Corne to 34-key layout conversion
#define C_34(k) SPLIT_3x6_3_TO_3x5_2(k)
#define SPLIT_3x6_3_TO_3x5_2( \
    k01, k02, k03, k04, k05, k06, k07, k08, k09, k10, k11, k12, \
    k13, k14, k15, k16, k17, k18, k19, k20, k21, k22, k23, k24, \
    k25, k26, k27, k28, k29, k30, k31, k32, k33, k34, k35, k36, \
                   k37, k38, k39, k40, k41, k42 \
) \
         k02, k03, k04, k05, k06, k07, k08, k09, k10, k11, \
         k14, k15, k16, k17, k18, k19, k20, k21, k22, k23, \
         k26, k27, k28, k29, k30, k31, k32, k33, k34, k35, \
                        k38, k39, k40, k41

The JSON layout for 34-key Cradio keyboard uses that C_34() macro in the following format:

{
    "keyboard": "Cradio",
    "keymap": "filterpaper",
    "layout": "LAYOUT_34key_w",
    "layers": [
        [ "C_34(HRM(_BASE))" ],
        [ "C_34(HRM(_COLE))" ],
        [ "C_34(_NUMB)" ],
        [ "C_34(_SYMB)" ],
        [ "C_34(_FUNC)" ]
    ]
}

STeMcell notes

STM32F411 replacement controller with Pro micro footprint, v1.0.1. Runs on tinyuf2 bootloader.

  • Reset new STMC to stm-dfu:
    • Connect USB while holding button
    • Short RST and GND while holding button
  • Reset STMC with tinyuf2:
    • Double-short RST and GND
    • QK_BOOT keycode
    • Bootmagic lite

Bootloaders

To install the STeMcell tinyuf2 bootloader

dfu-util -a 0 -i 0 -s 0x08000000:leave -D tinyuf2-stemcell.bin

To wipe the entire STeMcell flash (wait up to 30s):

dfu-util -a 0 -i 0 -s 0x08000000:mass-erase:force

ISP Flashing Notes

Hardware

USBasp wiring

Connect the USBasp programmer to the target controller in this manner:

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

See the QMK ISP Flashing Guide. Replace the Pro Micro's default Caterina boot loader with Atmel-DFU using the following command for USBasp 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

Command Line Flashing

Simple bash and zsh shell function for flashing firmware (and optionally handedness) to Atmel DFU controller on MacOS. It requires dfu-programmer from Homebrew:

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