Willem's Fizzy Logic

Customize your Keychron Q3 Keyboard QMK Firmware

Customize your Keychron Q3 Keyboard QMK Firmware

Full admission here, I am a keyboard nerd. Sorry, I can’t help myself. I love tinkering with custom keyboards. And to be fair, I spent too much money on them in the past.

One of the keyboards I own is a Keychron Q3 mechanical keyboard. I have two of them, one in midnight blue, and one in classic black. The black one is pretty old, the other I got recently for office use.

The Q3 was the second mechanical keyboard I owned and at first I didn’t know you could modify its firmware. But when I looked into getting a more classically shaped mechanical keyboard for in the office, I noticed that this keyboard runs QMK firmware.

So when my vacation started, I decided it was time to tinker with it!

In this post, I’ll show you how to change the firmware for this keyboard and what awesome and whacky things you can do with this plain looking keyboard.

Let’s get started setting up QMK first though.

Setting up QMK

Working with QMK requires a bit of work. You need to configure QMK itself and generate a few things before you can start tinkering.

Installing the QMK tool chain

Before you can customize a QMK based keyboard like the Keychron Q3, you need to make sure you have the required tools on your system. The requirements are listed on the QMK Website, but I’ll include them here as well:

You can obtain Python through the package manager on your machine. I used pacman -S python3-pip to get Python installed on Arch Linux. A similar command exists for Ubuntu apt install -y python3-pip.

If you’re running another operating system like Mac or Windows, you should definitely check out the QMK website. They have installation guides for those operating systems as well. Windows is a lot more wonky than the others, but should work. Although I recommend using WSL2 if you’re on Windows.

As for the build tools, I used pacman -S base-devel to get the right tools configured for my machine. For Ubuntu you can use apt install -y build-essential to install a decent compiler tool chain.

Configuring QMK on Linux isn’t too hard. You can install QMK through pip by issuing the following command:

pip install --upgrade qmk

This installs a commandline utility that you’ll need to run many of the QMK related actions to customize your keyboard. Once you’ve installed QMK on your machine, you can configure it for your keyboard.

Configuring QMK for your keyboard

Configuring QMK starts by executing the command qmk setup. This will clone the GIT repository containing the firmware source files to your system in ~/qmk_firmware. It takes a while to clone and configure the repository, so this is a great time to refill that glass or cup.

After cloning the repository and setting things up, you should receive confirmation that your system is fully configured to work on the QMK firmware code. If there are any missing dependencies, the QMK utility will help you set them up correctly. Unless something is seriously broken in your system, you should get this part right within 5 to 10 minutes at the most.

With the setup completed, we can configure the keyboard. First, we need to setup two configuration variables. These depend on the keyboard you’re using. I have a Keychron Q3 with the Ansi layout. I can customize this keyboard by setting the following setting:

qmk config user.keyboard=keychron/q3/ansi

The second configuration variable is the keymap that you want to work on. This setting is usually configured with your username like so:

qmk config user.keymap=wmeints

After setting up the necessary configuration variables, you can create the actual keymap using the following command:

qmk new-keymap

This command creates a keymap in the location ~/qmk_firmware/keyboards/keychron/q3/ansi/keymaps/wmeints/keymap.c. The location corresponds with the keyboard you configured and the name of the keymap you configured.

Let’s look at the keymap that was generated to understand how you can customize it to your liking.

Exploring the keymap

The keymap.c file has a rather interesting layout thanks to the ASCII art showing the layout of your keyboard. Be careful with formatting this file, because the ASCII art is going to help you locate key mappings quickly.

The keymap for my keyboard looks like this right now:

#include QMK_KEYBOARD_H

enum layers{
    MAC_BASE,
    MAC_FN,
    WIN_BASE,
    WIN_FN
};

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [MAC_BASE] = LAYOUT_tkl_ansi(
        KC_ESC,             KC_BRID,  KC_BRIU,  KC_NO,    KC_NO,    RM_VALD,  RM_VALU,  KC_MPRV,  KC_MPLY,  KC_MNXT,  KC_MUTE,  KC_VOLD,    KC_VOLU,  KC_NO,    KC_NO,    RM_NEXT,
        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_INS,   KC_HOME,  KC_PGUP,
        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_DEL,   KC_END,   KC_PGDN,
        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_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_LCTL,  KC_LOPT,  KC_LCMD,                                KC_SPC,                                 KC_RCMD,  KC_ROPT,  MO(MAC_FN), KC_RCTL,  KC_LEFT,  KC_DOWN,  KC_RGHT),

    [MAC_FN] = LAYOUT_tkl_ansi(
        _______,            KC_F1,    KC_F2,    KC_F3,    KC_F4,    KC_F5,    KC_F6,    KC_F7,    KC_F8,    KC_F9,    KC_F10,   KC_F11,     KC_F12,   _______,  _______,  RM_TOGG,
        _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,  _______,  _______,
        RM_TOGG,  RM_NEXT,  RM_VALU,  RM_HUEU,  RM_SATU,  RM_SPDU,  _______,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,  _______,  _______,
        _______,  RM_PREV,  RM_VALD,  RM_HUED,  RM_SATD,  RM_SPDD,  _______,  _______,  _______,  _______,  _______,  _______,              _______,
        _______,            _______,  _______,  _______,  _______,  _______,  NK_TOGG,  _______,  _______,  _______,  _______,              _______,            _______,
        _______,  _______,  _______,                                _______,                                _______,  _______,  _______,    _______,  _______,  _______,  _______),

    [WIN_BASE] = LAYOUT_tkl_ansi(
        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_PSCR,  _______,  _______,
        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_INS,   KC_HOME,  KC_PGUP,
        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_DEL,   KC_END,   KC_PGDN,
        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_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_LCTL,  KC_LWIN,  KC_LALT,                                KC_SPC,                                 KC_RALT,  KC_RWIN,  MO(WIN_FN), KC_RCTL,  KC_LEFT,  KC_DOWN,  KC_RGHT),

    [WIN_FN] = LAYOUT_tkl_ansi(
        _______,            KC_BRID,  KC_BRIU,  _______,  _______,  _______,  _______,  KC_MPRV,  KC_MPLY,  KC_MNXT,  KC_MUTE,  KC_VOLD,    KC_VOLU,  _______,  _______,  _______,
        _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,  _______,  _______,
        _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,  _______,  _______,
        _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,              _______,
        _______,            _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,              _______,            _______,
        _______,  _______,  _______,                                _______,                                _______,  _______,  _______,    _______,  _______,  _______,  _______),
};

The keymap for the Keychron Q3 has four layers by default but it’s not limited to these 4 layers. The first layer is the base layer if you have the switch on the back of the keyboard set to Mac. The third layer is the base layer if you have the keyboard set to Windows mode. Beyond these two specific layers you can configure up to 28 more layers for a total of 32 layers.

The top of the file includes the base definitions for the keyboard. This file is generated and you shouldn’t try to modify it. Below the include, you can find an enumeration for the layers. This enumeration helps with readability, but otherwise doesn’t have any significance.

The rest of the file defines the key mappings for all the layers in the keyboard. It’s important to understand that the base layer will define how the keyboard works without any modifiers. The MAC_FN and WIN_FN layers are overlays over the other layers. Any key not mapped, indicated by _______ passes through to the layer below it.

QMK allows you to activate multiple layers at once. Layers with a higher index have priority over the layers below. This is an interesting construction because it allows you to map areas of your keyboard in separate layers while keeping the rest as-is.

Before we dive into configuring colors, key functions, and additional layers, let’s look at how to load the keymap into the keyboard.

Flashing the keymap to your keyboard

When you’ve modified the keymap.c file you’re going to need to compile the keymap. You can do this from the command line with the command:

qmk compile

This command takes in extra optional arguments -kb <keyboard> and -km <map>. The first argument -kb sets the keyboard we’re compiling. The second argument -km sets the keymap that we want to compile. Since we already configured the user.keyboard setting and user.keymap setting we can omit these settings.

After compiling the keymap, you can flash it to your keyboard with the following command:

qmk flash

The flashing utility will ask you to put your keyboard in bootloader mode. This is different for each brand of keyboard. For the Keychron Q3, disconnect the keyboard, hold the Escape key, and put the cable back in.

The flashing utility will automatically detect the bootloader and flash the keyboard. After it’s flashed, it will go back to its normal operating mode.

Modifying the colors on the keys

The Keychron keyboards come with a rather cheesy RGB mode out-of-the-box. There’s a button on the keyboard to change the mode and there are key mappings for controlling the colors and brightness. The QMK firmware allows you to change the colors of the leds in your keyboard, by adding a special function to the keymap.c file.

bool rgb_matrix_indicators_user(void) {

}

The rgb_matrix_indicators_user function is a callback that’s invoked when the keyboard is updating the RGB leds in the keyboard. Since many keyboards support animation it’s important to keep in mind that this callback is invoked after the animation is rendered. This ensures that the colors you set here are displayed and don’t get overwritten by any animation logic.

You can do some very nice, and advanced, animation effects on your keyboard. But we’ll focus on setting the color of the leds first.

In the indicator function you can specify the color for a key by calling the function rgb_matrix_set_color(<key-index>, <red>, <green>,<blue>). You can also use rgb_matrix_set_color_all(<red>, <green>, <blue>) to set the color of all keys.

Not everyone is well-versed in RGB codes so the QMK firmware contains a few default colors which you can find in the ~/qmk_firmware/quantum/color.h file.

To give you an idea of what that looks like, you can add the following code to the function to make the keys light up purple:

rgb_matrix_set_color_all(RGB_PURPLE); // Make the key purple

Beyond this basic trick to set a color for all keys in a layer, you can do other interesting things like changing the color on a per-layer basis.

You can get the currently active layer using the code get_highest_layer(layer_state). This returns the index of the highest layer that’s currently active.

Based on the active layer you can change how leds are colored. For example, I wrote the following logic to set a different color per layer:

bool rgb_matrix_indicators_user(void) {
    switch(get_highest_layer(layer_state)) {
        case MAC_BASE:
        case WIN_BASE:  // Base layer - purple
            rgb_matrix_set_color_all(RGB_PURPLE);
            break;
        case MAC_FN:
        case WIN_FN: // Fn layer - goldenrod
            rgb_matrix_set_color_all(RGB_GOLDENROD);
            break;
    }

    return false;
}

You can extend beyond layer effects to set colors for specific keys. The function rgb_matrix_set_color(<index>, <red>, <green>, <blue>) requires the index of the led that you want to change. You can use the function rgb_matrix_map_row_column_to_led to map the row/column combination of a key in the keymap to it’s corresponding led index.

Using the rgb_matrix_map_row_column_to_led function you can color specific keys. For example, you can color the escape key red, using the following code:

uint8_t led_index;
rgb_matrix_map_row_column_to_led(0,0, &led_index);
rgb_matrix_set_color(led_index, RGB_RED);

Setting up colors can be useful if you want to make certain keys stand out or to highlight that you’re using keys from a specific layer in the keyboard.

You can take this one step further and modify how keys work.

Adding extra functions to keys

QMK is a very flexible piece of kit. There are many ways to customize how keys work.

One of the ways you can modify how keys work is by building tap dances into the keyboard. Tap dancing is a concept that’s pretty unique to how QMK works. You can, for example, build a sequence where you send the C key when the user taps a key once, and Ctrl+C when the user taps the C key twice within a set timeframe. Be careful though, if you’re a fast typer, you may end up getting into trouble, because you tap keys too fast and you’ll only get double taps because of the high speed.

You can, however, change how fast you need to double-tap to get the double-tap effect in a tap dance setup. Add config.h file to the directory where the keymap.c is located and place the following code in it.

#define TAPPING_TERM 200

Increase it if you’re a slow typer, and decrease it if you’re a fast typer.

Let’s take a look at configure a tap dance for C/Ctrl+C. First, we need to configure the keymap to support tap dancing. Add the following content to rules.mk next to the keymap.c file in the same folder:

TAP_DANCE_ENABLE = yes

After enabling the tap dance functionality, we can define tap dance keys in the keymap. This starts by adding an enumeration for tap dance keys:

enum {
    TD_C,
}

Next, you need to add an array of tap dance actions:

tap_dance_action_t tap_dance_actions[] = {
    [TD_C] = ACTION_TAP_DANCE_DOUBLE(KC_C, LCTL(KC_C))
};

The tap dance action array can contain multiple key mappings for tap dances. In this particular sample, we define a double tap dance key with the ACTION_TAP_DANCE_DOUBLE macro. This macro takes a key code that’s sent on the first tap, and a second key code for the second tap. The first key code is a plain KC_C sending the C key. The second key code here is a Left Control Key modifier combined with the key code for the letter C.

Finally, you can map the TD_C key in your keymap, replacing the original KC_C.

Make sure to compile and flash your keyboard after you’re done!

Adding extra layers

Adding tap dances increases the functionality of your keyboard a lot. You can bind quite a few functions to a single key if you do it right. You can easily bind 5 actions to a single key with the tap dance functions.

However, that’s not always a useful way to extend the power of your keyboard. Sometimes you want to switch the layout completely. This is where layers become useful.

By default, the Keychron keyboards have 4 layers configured. Two for Mac, and two for Windows. You can however, map up to 32 layers in total. Here’s how.

First, you need to define another entry in the layers enum:

enum layers {
    MAC_BASE,
    MAC_FN,
    WIN_BASE,
    WIN_FN,
    WIN_VSCODE,
};

The WIN_VSCODE layer is going to map extra keys for working with VSCode. For example, we could map custom key combination when someone presses Q in the VSCode layer.

Before we can remap the Q key, we need to make sure there’s a layer connected for the VSCode keys.

Add the following code to the keymaps array in your keymap.c file:

[WIN_VSCODE] = LAYOUT_tkl_ansi(
        _______,            _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,  _______,  _______,
        _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,  _______,  _______,
        _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,  _______,  _______,
        _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,              _______,
        _______,            _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,              _______,            _______,
        _______,  _______,  _______,                                _______,                                _______,  _______,  _______,    _______,  _______,  _______,  _______),

This creates a completely transparent layer that we can customize for VSCode. You can now bind specific keys for VSCode to this layer. Next, we need to make sure that we can get to this layer. Replace the keymapping for the light bulb key (top right corner) with MO(WIN_VSCODE) to momentarily switch to the VSCode layer on your keyboard. The argument for the MO macro is the target layer to activate.

You can also switch to the VSCode layer until a key is pressed. This is called one-shot layer switching and can be configured by mapping OSL(WIN_VSCODE). Another great trick is to use TG(WIN_VSCODE) to toggle the VSCode layer. Pressing the same key again will deactivate the VSCode layer.

For the VSCode layer to be of any use, we need to map common shortcut keys to this layer. For example, you could bind Ctrl+Shift+E to the Q key. Here’s how to do that.

First, use the following code to create a macro for the Ctrl+Shift+E key combination.

#define VSC_TR LCTL(LSFT(KC_E)) // Ctrl+Shift+E to open the tree view

Next, bind it in the keymap like so:

[WIN_VSCODE] = LAYOUT_tkl_ansi(
        _______,            _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,  _______,  _______,
        _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,  _______,  _______,
        _______,  VSC_TR,   _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,  _______,  _______,
        _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,              _______,
        _______,            _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,  _______,              _______,            _______,
        _______,  _______,  _______,                                _______,                                _______,  _______,  _______,    _______,  _______,  _______,  _______),

Compile and flash your keymap, and you’re good to go!

Safely storing your keymaps

Over time you’re going to stack up quite a few customizations, and it would be very annoying to loose those customizations. By default, however, QMK will create your user keymap in the same source tree as the rest of the QMK firmware. You could of course submit the keymap as a pull request and have the QMK people save it for you. You could also fork the original repository, but that requires a lot of work, because you need to stay current with the original code.

There is a better way though, you can create a QMK external user space. Let’s create one so you can store your customizations separately from the main QMK repository.

First, fork the qmk/qmk_userspace repository. You can do this through the Github user interface.

Next, copy over the contents of ~/qmk_firmware/keyboards/keychron/q3/ansi/keymaps/<username> to the corresponding directory in ~/qmk_userspace/keyboards/keychron/q3/ansi/keymaps/<username>.

Finally, configure the userspace folder using the following command:

qmk config user.overlay_dir=$(realpath ~/qmk_userspace)

Make sure to undo any changes you made in the qmk_firmware directory so you know the code from your external user space is picked up.

Summary

In this post we covered how to use QMK to customize the Keychron Q3 keyboard changing the colors, adding extra functions to keys, and mapping extra layers.

There are lot more things you can customize though. I highly recommend looking at the RGB options offered through QMK, because they include some very nice animation tricks that I couldn’t cover here.

Overall, the Q3 is a bit of a sleeper keyboard. It looks like a regular sturdy mechanical keyboard, but packs quite a punch thanks to the QMK firmware inside it.

Let me know what keyboard you’re using and if you’ve ever customized it in any way.

About

Willem Meints is a Chief AI Architect at Aigency/Info Support, bringing a wealth of experience in artificial intelligence and software development. As a Microsoft AI Most Valuable Professional (MVP), he has been recognized for his significant contributions to the AI community.

In addition to his professional work, Willem is an author dedicated to advancing the field of AI. His latest work focuses on effective applications of large language models, providing valuable guidance for professionals and enthusiasts alike.

Willem is a fan of good BBQ, reflecting his appreciation for craftsmanship both in and out of the digital realm.

Contact