Skip to main content

Writing Custom Keyboard Firmware with QMK

·2116 words·10 mins·
Coding
Table of Contents
This project has an associated GitHub repository.

Intro
#

The best part about building your own keyboard is being able to customize anything and everything to your liking. On keyboards that support QMK firmware that customization doesn’t stop at just the physical keyboard.

With QMK you can write your own custom firmware using C code, tweaking keyboard functions and adding features like custom keymaps, RGB lighting, macros, and more. Since this is firmware-level customization it requires no software on your computer, and stays with your keyboard wherever you take it.

In this post I’ll be using QMK to make my own firmware for my GMMK Pro and flashing it onto my keyboard.

Setting up a QMK environment
#

Since I’m using Windows, I first installed QMK MSYS. If you’re using a different OS, the QMK docs have instructions for setting up a build environment on MacOS, Linux, and FreeBSD.

Next I forked the qmk_firmware github repository and cloned my new repository with git clone --recurse-submodules. After that it just took a few basic commands to get QMK ready:

qmk setup
qmk list-keyboards | grep gmmk
qmk config user.keyboard=gmmk/pro/rev1/ansi
qmk config user.keymap=coreybraun

The first command performs initial setup of QMK, finding or creating your qmk home folder with the QMK file structure. By default QMK will search for/create the qmk folder at qmk_firmware/ within your user’s home, but if you want to use a different location one can be specified with -H <path>. The second command lists all QMK keyboards, which I pipe to grep to find the name of my keyboard, “gmmk/pro/rev1/ansi”, which is also the path to its keymaps in qmk_firmware/keyboards/. The last two commands set some defaults that will save me having to specify a keyboard and keymap name with -kb <keyboard> and -km <name> arguments every time when I create, compile, and flash my firmware.

Creating my keymap
#

To start off I can create my own keymap using qmk new-keymap, which creates my new keymap at qmk_firmware/keyboards/<my keyboard>/keymaps/coreybraun, copying the default keymap for my keyboard (.../keymaps/default) to use as a base.

Within this folder there should be three files:

  • keymap.c, which contains the main C code for your keyboard
  • config.h, a C header file which can be used to set keyboard variables
  • rules.mk, a makefile used to define other variables, most notably enabling (or disabling) QMK features at the cost of increased firmware size

If either of the latter two files are missing you can create them; Since they are optional configuration files they may not be included in the default keymap. In my case I had to create my own config.h, as well as include it with a line near the top of my keymap.c:

#include "config.h"

By editing these three files you can customize your keyboard to your liking. The files are pretty empty to start, relying on QMK’s defaults for most settings and functions, as well as default files for your specific keyboard model. Thanks to QMK’s focus on customization it is usually pretty easy to add the features you want by using existing functions or changing configuration variables within your personal keymap.

Changing the Keyboard Layout
#

Near the top of keymap.c is a definition of one or more (up to 16) matrices which set the mapping of keys on each layer of the keyboard, typically with accompanying comment lines to give an idea of what the default keymap translates to on the keyboard. The keycodes within these matrices can be changed to any of the valid QMK Keycodes, but it is important to have QK_BOOT accessible somewhere, as without it you cannot put the keyboard in bootloader mode to flash it again (unless the keyboard has a hardware bootloader button). Here is the final keymap I ended up deciding on:

[0] = LAYOUT(
        KC_ESC,  KC_F1,   KC_F2,   KC_F3,   KC_F4,   KC_F5,   KC_F6,   KC_F7,   KC_F8,   KC_F9,   KC_F10,  KC_F11,  KC_F12,  KC_DEL,           KC_MPLY,
        KC_GRV,  KC_1,    KC_2,    KC_3,    KC_4,    KC_5,    KC_6,    KC_7,    KC_8,    KC_9,    KC_0,    KC_MINS, KC_EQL,  KC_BSPC,          KC_HOME,
        KC_TAB,  KC_Q,    KC_W,    KC_E,    KC_R,    KC_T,    KC_Y,    KC_U,    KC_I,    KC_O,    KC_P,    KC_LBRC, KC_RBRC, KC_BSLS,          KC_PGUP,
        KC_CAPS, KC_A,    KC_S,    KC_D,    KC_F,    KC_G,    KC_H,    KC_J,    KC_K,    KC_L,    KC_SCLN, KC_QUOT,          KC_ENT,           KC_PGDN,
        KC_LSFT,          KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,    KC_N,    KC_M,    KC_COMM, KC_DOT,  KC_SLSH,          KC_RSFT, KC_UP,   KC_END,
        KC_LCTL, KC_LGUI, KC_LALT,                            KC_SPC,                             KC_RALT, MO(1),   KC_RCTL, KC_LEFT, KC_DOWN, KC_RGHT
    ),

    [1] = LAYOUT(
        _______, UNSAFE,  _______, _______, SPAM_M1, _______, _______, RGB_MOD, RGB_VAD, KC_VOLD, KC_VOLU, KC_MPRV, KC_MNXT, KC_PSCR,          _______,
        _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,          KC_INS,
        _______, _______, _______, _______, RGB_TOG, _______, _______, _______, _______, _______, _______, _______, _______, QK_BOOT,          _______,
        QK_LOCK, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,          _______,          _______,
        _______,          _______, _______, _______, _______, _______, NK_TOGG, _______, _______, _______, _______,          TG(2),   KC_PGUP, _______,
        _______, _______, _______,                            _______,                            _______, _______, _______, KC_HOME, KC_PGDN, KC_END
    ),

	[2] = LAYOUT(
        _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,          _______,
        _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,          _______,
        _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,          KC_WH_U,
        QK_LOCK, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______,          _______,          KC_WH_D,
        _______,          _______, _______, _______, _______, _______, _______, _______, KC_BTN1, KC_BTN2, KC_BTN3,          TG(2),   KC_MS_U, KC_BTN2,
        _______, _______, _______,                            _______,                            _______, _______, KC_BTN1, KC_MS_L, KC_MS_D, KC_MS_R
    ),

For the most part I put normal keycodes on layer 0, then my lesser-used keys like insert and printscreen on layer 1 along with some special QMK keycodes, custom macros, and controls for RGB and media. Layer 2, which can be toggled on/off via right shift on layer 1 or 2, is primarily used for mouse controls.

Other Keyboard Functions
#

Key Lock
#

Key Lock is a niche but useful feature in QMK, setting the next keycode you input after key lock to stay in a down state until pressed again, essentially holding down a key for you. To use key lock you first have to enable it by adding a line with KEY_LOCK_ENABLE = yes in rules.mk, then you can set a key in your keymap to QK_LOCK, the key lock keycode. In my case I set the caps lock key to QK_LOCK on layer 1 and 2.

Mouse Keys
#

Mouse Keys allow you to control your cursor as well as send mouse button presses from your keyboard. After adding MOUSEKEY_ENABLE = yes to rules.mk, you can use mouse control keycodes in your keymap. For my keymap I use layer 2 for mouse keys, with arrow keys mapped to moving the cursor, page up and page down for scroll wheel, and mouse button presses mapped in two locations for the option of controlling the mouse one-handed or two-handed. I also set the layer to be toggled on and off so I don’t have to hold a button to keep the layer active while I’m controlling the mouse via my keyboard.

There are also several configuration values you can define in to override the defaults. These are mostly down to personal preference, but these are the lines I ended up adding to my config.h:

#define MOUSEKEY_INTERVAL 7
#define MOUSEKEY_DELAY 0
#define MOUSEKEY_TIME_TO_MAX 60
#define MOUSEKEY_MAX_SPEED 8
#define MOUSEKEY_MOVE_DELTA 1
#define MOUSEKEY_WHEEL_DELAY 0
#define MOUSEKEY_WHEEL_INTERVAL 76

RGB Indicators
#

One of the most important goals I had was setting certain RGB LEDs in my keyboard to a different colors to indicate caps lock status or which layer is currently active. Fortunately QMK defines functions to use for setting your own RGB indicators, with these functions running after other RGB lighting effects to ensure your indicators take precedence. Within these functions you can set LEDs’ colors with RGB_MATRIX_INDICATOR_SET_COLOR(index, red, green, blue), where the first value is the index number of the LED and the latter 3 are 8-bit values for the brightness of each color.

I started out pretty simple, using the indicator function in my keymap.c to set one LED to red when caps lock was on, green while layer 1 was active, and blue while layer 2 was active. The variables I use for the RGB colors here are defined in a QMK C header file.

bool rgb_matrix_indicators_advanced_user(uint8_t led_min, uint8_t led_max) {
    if (host_keyboard_led_state().caps_lock) RGB_MATRIX_INDICATOR_SET_COLOR(3, RGB_RED);
    switch(get_highest_layer(layer_state|default_layer_state)) {
		case 1:
		RGB_MATRIX_INDICATOR_SET_COLOR(3, RGB_GREEN);
		break;
		case 2:
		RGB_MATRIX_INDICATOR_SET_COLOR(3, RGB_BLUE);
		break;
    }
    return false;
}

After this first basic iteration worked I wanted to change the lighting to be on the left and right lightbars as well as the top row of the keyboard. Since I wanted to set the color of about 30 LEDs to indicate caps lock or different layers I created a function to set the color of all these LEDs at once by iterating over an array of the LEDs’ index numbers:

uint8_t outer_led_array[] = {0, 6, 12, 18, 23, 28, 34, 39, 44, 50, 56, 61, 66, 69, 67, 68, 70, 71, 73, 74, 76, 77, 80, 81, 83, 84, 87, 88, 91, 92};
void set_outer_leds(uint8_t redValue, uint8_t greenValue, uint8_t blueValue) {
	for (uint8_t i=0; i < (sizeof(outer_led_array) / sizeof(outer_led_array[0])); i++) {
		RGB_MATRIX_INDICATOR_SET_COLOR((outer_led_array[i]), redValue, greenValue, blueValue);
	}
};

After that I just had to change the caps lock and layer conditionals to call this function instead, passing it the desired color values.

Macros
#

There are a couple macros I want on my keyboard. The first should just type “thisisunsafe” when pressed. Typing this string into chromium browsers bypasses the warning for an invalid SSL cert, a frequently-seen warning when using HTTPS to access web interfaces for internal services that use a self-signed cert. For the second macro I want pressing it to toggle spam pressing mouse1. I also want to be able to easily disable spamming mouse1 at any time by pressing escape or pressing the key again.

I started by defining keycodes for these two macros, as well as a boolean value for whether mouse1 should be getting spam pressed, with a default of false. These definitions are in my keymap.c, located above my keyboard layout since they need to be defined before referencing them in the keymap arrays.

enum custom_keycodes {
    UNSAFE = SAFE_RANGE,
    SPAM_M1,
};
bool spamming_m1 = false;

Assigning the first keycode to SAFE_RANGE ensures the keycodes will get a unique ID without requiring me to manually specify one.

Next I defined what should happen on key presses:

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    switch (keycode) {
    case UNSAFE:
        if (record->event.pressed) SEND_STRING("thisisunsafe");
        break;
    case SPAM_M1:
        if (record->event.pressed) spamming_m1 = !spamming_m1; 
        break;
    case KC_ESC:
        if (record->event.pressed) if (spamming_m1) spamming_m1 = false;
        break;
    }
    return true;
};

process_record_user is a QMK function called any time a key is pressed or released, by using it here we can modify key behaviors. Using if (record->event.pressed) causes these actions to happen on the key press and not release. For the first macro we just send a string, and for the second we toggle the state of the boolean value we created earlier. We also want escape to set the boolean to false if it’s true, so we can do that here as well. Since this returns true it doesn’t interupt the key’s normal function.

Finally I needed to make the boolean for spamming mouse1 actually do something:

void matrix_scan_user(void) {
    if (spamming_m1) {
        tap_code(KC_BTN1);
    }
};

matrix_scan_user is a function that gets run every matrix scan, so by setting it to tap (send down then up) mouse1 every cycle when the boolean is true it achieves my goal of spam pressing mouse1. The QMK docs say the function will get run as often as your keyboard’s processor can handle, and with my keyboard I was getting around 90 clicks per second.

Compiling and Flashing firmware
#

A keymap can be compiled by running:

qmk compile

Assuming the firmware compiles without errors you can put your keyboard into bootloader mode and flash your firmware onto it.

The method to enter bootloader mode varies, but for keyboards already running QMK you need to input the key mapped to keycode QK_BOOT (default Fn+\), or, if enabled, hold the bootmagic lite key (default escape) while plugging in the keyboard. Using the stock Glorious firmware on my keyboard I had to plug in the keyboard while holding space and B to enter bootloader mode.

Once in bootloader mode your keyboard will no longer work until it is either flashed and reboots or is rebooted by unplugging then plugging it back in. It should go without saying, but don’t unplug the keyboard while firmware is being written to it.

While in bootloader mode, you can flash firmware onto your keyboard by running:

qmk flash

Alteratively, you can run the command and then enter bootloader mode as it may be difficult to type the command without a usable keyboard.