QMK Introduction: Running meishi Trackball Module with Custom Firmware
The created firmware code is available in this repository. Please check here if you have any questions.
By the way, the header image is a keyboard drawn by DALL-E, with many columns and rows, and grass growing on it.
Background
I bought this namecard-sized trackball, thinking, "Why not use it to move the mouse without leaving the keyboard?" It comes with two switches and a rotary encoder, allowing it to be used like a mouse. The trackball part can be used separately, making it a great product.
After assembling it with a Pro Micro microcontroller and struggling with the USB connection, I finally completed it. Although there is a detailed guide by the creator, I still managed to get the orientation wrong, which was a humbling experience.
Trackball Angle Problem
After successfully assembling the trackball and confirming that it works, I noticed that the trackball angle doesn't change smoothly. The ROT_R15
and ROT_L15
keycodes are supposed to change the angle by 15 degrees each time, but it seems to reset to around 90 degrees.
Fortunately, the firmware code is publicly available. After reading it, I found that the rotation angle is fixed, with a total range of 120 degrees. Although this is intentional, it seems that the code needs to be modified to set it to any angle.
#define COCOT_ROTATION_ANGLE { -60, -45, -30, -15, 0, 15, 30, 45, 60 }
QMK Compilation Error
The publicly available firmware can be modified to solve this issue. This firmware uses QMK.
QMK is a widely used firmware library for custom keyboards. It's not only easy to implement but also provides convenient features like browser-based keymap editing.
However, since QMK is actively developed, there are sometimes breaking changes. The publicly available code for the meishi Trackball Module is written for QMK 0.16, which causes compilation errors with QMK 0.22.
……So, what to do? Since I had to rewrite it anyway, I decided to implement it from scratch to support the new version.
Writing the Firmware
QMK has excellent documentation, so most issues can be resolved by referring to the official documentation.
From here, I'll only describe the points where I stumbled.
Scope of Implementation
For now, I'll focus on implementing the minimum required features: making the keys work, enabling scrolling with the rotary encoder, and making the trackball move.
Convenient Tools
- QMK Toolbox
- This tool burns the firmware. Although it can be done from the browser, it's reassuring to have console output.
- REMAP
- This is a firmware repository site. It's very convenient. The official meishi Trackball Module firmware can also be burned or downloaded from here. I'll use it mainly for testing.
Hardware Settings
These are the hardware settings, including the MCU type, pin settings, and more. I extracted them from the original code for reference.
- MCU: atmega32u4
- bootloader: atmel-dfu
- Trackball sensor: adns5050
- diode_direction: COL2ROW
- Pins
- B6: Key 1
- B3: Key 2 (rotary encoder click?)
- B2: Key 3
- D2: Rotary encoder rotation 1
- D3: Rotary encoder rotation 2
Setup
Since I'm using an M1 Mac, I installed QMK using brew install qmk/qmk/qmk
. However, it takes a ridiculously long time. It's essential to do this before starting to write code, or you'll lose motivation.
Moreover, since C projects are modularized for each keyboard, setup can be challenging. Even following the official guide, there are parts where IDE completion doesn't work. VS Code gets frustrated, but I'll persevere.
qmk setup -H ./qmk_firmware
clones the QMK repository. It's better to use the v0.22.0 tag, which is the latest version at the time of writing (2024/02).
info.json Settings
This is an important file, but it's unclear what settings are required. Although similar settings can be written in config.h
using macros, it's better to set them here for clarity.
Many custom keyboards use a matrix (row-column combination) for switch detection, but in this case, matrix_pins.direct
can be set. The layouts
specification is almost the same as for matrix keyboards.
Moreover, USB vendor IDs and other settings should not be taken from the original firmware. If you do, REMAP might try to match the existing keyboard definition, leading to a day of debugging.
{
"manufacturer": "aaaa",
"keyboard_name": "test_trackball",
"maintainer": "aaaa",
"bootloader": "atmel-dfu",
"diode_direction": "COL2ROW",
"features": {
"bootmagic": true,
"command": false,
"console": false,
"extrakey": true,
"mousekey": true,
"nkro": true
},
"processor": "atmega32u4",
"url": "",
"matrix_pins": {
"direct": [["B6", "B3", "B2"]]
},
"usb": {
"device_version": "1.0.0",
"pid": "0x000A",
"vid": "0x1720"
},
"layouts": {
"LAYOUT": {
"layout": [
{ "matrix": [0, 0], "label": "0,0", "x": 0, "y": 0 },
{ "matrix": [0, 1], "label": "0,1", "x": 1, "y": 0 },
{ "matrix": [0, 2], "label": "0,2", "x": 2, "y": 0 }
]
}
}
}
Receiving Key Input
In keymaps/default/keymap.c
, I wrote the processing for when the keys set in matrix_pins
and layouts
are pressed. The order is crucial. Using the names specified in info.json
under layouts
allows for compile-time checking.
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {[0] = LAYOUT(KC_MS_BTN1, KC_MS_BTN2, KC_MS_BTN3)};
Enabling the Rotary Encoder
This is described in the QMK documentation.
By specifying ENCODER_ENABLE = yes
in rules.mk
, the rotary encoder is enabled. The pin settings are specified in config.h
.
#define ENCODERS_PAD_A \
{ D2 }
#define ENCODERS_PAD_B \
{ D3 }
#define ENCODER_RESOLUTION 4
The keymap is specified in keymaps/default/rules.mk
and keymaps/default/keymap.c
.
#if defined(ENCODER_MAP_ENABLE)
const uint16_t PROGMEM encoder_map[][NUM_ENCODERS][NUM_DIRECTIONS] = {
[0] = {ENCODER_CCW_CW(KC_MS_WH_UP, KC_MS_WH_DOWN)},
};
#endif
Trackball Support
Finally, I reached the part where I want to change the trackball behavior. This is also described in the QMK documentation.
First, I enabled it in rules.mk
. Specifying the sensor driver is crucial.
POINTING_DEVICE_ENABLE = yes
POINTING_DEVICE_DRIVER = adns5050
In config.h
, I specified the pins. The original firmware code can be used as is.
#define ADNS5050_SCLK_PIN F7
#define ADNS5050_SDIO_PIN F6
#define ADNS5050_CS_PIN B1
Here, I implemented the processing in <keyboard>.c
(where <keyboard>
is the specified keyboard name). The callback function for processing the coordinate values is prepared, so I rewrote it. The angle
value is incremented or decremented by 1 each time a button is pressed.
#define ANGLE_UNIT 15
#define ANGLE_MAX (360 / ANGLE_UNIT)
uint8_t angle = 0;
report_mouse_t pointing_device_task_kb(report_mouse_t mouse_report) {
double rad = ANGLE_UNIT * angle * (M_PI / 180);
int8_t x_rev = +mouse_report.x * cos(rad) - mouse_report.y * sin(rad);
int8_t y_rev = +mouse_report.x * sin(rad) + mouse_report.y * cos(rad);
mouse_report.x = x_rev;
mouse_report.y = y_rev;
return pointing_device_task_user(mouse_report);
}
Custom Key Settings
Here, I implemented the processing to change the angle
value according to the custom keys ROT_R15
and ROT_L15
. Using printf
makes debugging easier.
enum my_keycodes { ROT_R15 = QK_KB_0, ROT_L15 };
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
# ifdef CONSOLE_ENABLE
uprintf("KL: kc: 0x%04X, col: %2u, row: %2u, pressed: %u, time: %5u, int: %u, count: %u\n", keycode, record->event.key.col, record->event.key.row, record->event.pressed, record->event.time, record->tap.interrupted, record->tap.count);
uprintf("%04X\n", ROT_R15);
# endif
switch (keycode) {
case ROT_R15:
angle = (angle + 1) % ANGLE_MAX;
return false;
case ROT_L15:
angle = (angle + ANGLE_MAX - 1) % ANGLE_MAX;
return false;
default:
return true;
}
}
VIA Support
To set keys in the browser, VIA support is necessary. REMAP also requires this.
The definition file is specified as follows. The vendorId
and productId
should be the same. The custom keycodes specified in customKeycodes
correspond to QK_KB_0
, QK_KB_1
, and so on.
{
"name": "test_trackball",
"vendorId": "0x1720",
"productId": "0x000A",
"lighting": "none",
"menus": [],
"keycodes": [],
"matrix": { "cols": 5, "rows": 1 },
"customKeycodes": [
{
"name": "ROT_R15",
"title": "Rotate sensor Y-axis by 15 degrees clockwise",
"shortName": "ROT_R15"
},
{
"name": "ROT_L15",
"title": "Rotate sensor Y-axis by 15 degrees counterclockwise",
"shortName": "ROT_L15"
}
],
"layouts": {
"keymap": [["0,0", "0,2\n\n\n\n\n\n\n\n\ne0", "0,1"]]
}
}
Completion!
If you can burn the firmware using a suitable method and assign custom keys using REMAP, you're done!
Please refer to the sample code for detailed explanations. It's also a good idea to debug using printf
.
Afterword
QMK is very well-organized and convenient. It's also great for custom keyboard development.
The meishi Trackball Module is an excellent product, with publicly available documentation and code. It's perfect for custom keyboard beginners.
Now that I've completed the QMK-based firmware, I'd like to explore keyboard separation and PCB design next.