Creating custom keyboards with KMK
Beta version of my own design, based on the Corne V4.

Creating custom keyboards with KMK

Hello!

For some time I've been working on an idea that I plan to eventually turn into a business: custom keyboards. While I've focused a lot to bring a robust product at very competitive price point, this post is not about a business, but about developing embedded software using KMK and CircuitPython. I expect you to already be familiar with circuitpython and keyboard design, and I will only mention the optimizations that I've been doing to run the KMK keyboard-framework on a NRF52840 board.

Note: I'm publishing all the code of my keyboard in this repo: https://github.com/RaulHuertas/NutyKMK

Why KMK/CircuitPython?

During my initial tests I tried with QMK and ZMK. They are both written in native language and have a better prospect for long battery life and performance. However, each one had features that made it not appropiate for MY design:

QMK: It has a pletora of features and great configuration utilities like VIA and VIAL,but no bluetooth support.

ZMK: Remapping the keyboard still involves handling source code, which is something I don't want for the users of my keyboard. Also, it doesn't support wired split mode.

What do I want in my keyboard?

  • Touch-Typing oriented
  • Split, wired or wireless
  • I want it to work in both USB and BLE. Using one or the other has to be done with a slide switch
  • Functional RGB, for layer indication
  • Provide a lot of typing techniques to the user, like hold tap, tap dance, macro recording
  • Simple key-remapping done by the user. It should be just a configuration file, not a new firmware

Most of the features are availables in ZMK an QMK, but not the last one. I really don't want my final users to have to deal with terms like 'firmware', 'github account', or 'compiling source code'. Layers and remapping is already too much.

KMK, by using scripts, solve this. All the source code is a configuration file. It can be generated by an external tool and then just simply 'copied and pasted' to the keyboard, no 'reflashing'.

First test with KMK

My first tests were all with an RP2040 board. I designed a Corne V3 clone with a home PCB and used it first with QMK. I got very familiar with the space potato. But not long after that I tried KMK, and woah!, development time was sped up. I could try all my iterations instantly. Adding encoders, trackball kits and making changes to the PCB was all quick. I tried my own key layout with 72 buttons, but the corne layout standed out as simple. I managed to add keys, media, mouse, OLED displays, encoders, RGB and MIDI support just following the tutorials and pressing Ctrl+S. All with an USB-C connection. Everything worked perfectly. I got to practice with all the typing techniques it has available, and chose my favorite ones. I managed to get used to the corne layout, and remap it many times. I took it to work and soon started recording my credentials on it, to launching them with a single key. I even managed to start using neovim and i3 with it.


My first corne clone, it was ugly, but it was functional. It was the first one that I started using on a regular basis

Until this point, all my tests were with the RP2040 and usb only. I though I could finish this project in just a few weeks. But then...

First roadblocks

You see, one of the design goals for my keyboard is that it needs to have a competitive local price. Here in Peru there are a few stores that sell Corne Keyboards, but they are just re-selling what other stores produce abroad, which makes them very expensive here. But having a low price wouldn't be enough. I need this to have more features, and one of them is bluetooth. Not just bluetooth-only like other designs have, but it should be just a mode of operation, along with USB.

For this, I first tried the ESP32-S3 microcontroller. Its abundant memory, computing power and low price made it worth to try. I tried first sending characters, check. Mouse, check. MIDI, check. And then I tried to activate characters and MIDI... it just wouldn't work. I couldn't understand why. Then I tried it's native SDK. Characters, check. Mouse, check. MIDI, check. Characters+MIDI... nothing!. Then I read a small line in the documentation "USB Composite Device (MSC + CDC)". Being the only composite mode it mentions, and after some visits to the forums, seems like I can't combine all the USB modes simultaneously. This implies that for remapping the keys, the user would have to put the keyboard in MSC+CDC mode, get another(!) keyboard, and then restart the keyboard. That look like a terrible workflow, so I discarted the S3. Nor the C6 or C3, which I also had available, supported native USB so I promptly discarted the ESP32 for this project, but not KMK. The espressif tools seem very powerful though.

NRF to the rescue!

Now I put my focus in the XIAO-BLE module. It uses the NRF52840 microcontroller. Although it requires it's own programmer for flashin, circuitpython solves this issue. The XIAO-BLE module even has an integrated antenna. The XIAO-ESP32 has an external-antennna. It might have more reach, but it was going to be something else to manage during the design of the case.

First the good news: In USB mode, I could had a composite device with keyboard, mouse, and midi running on it. Awesome! I also tried the split ble mode and it worked! I was very excited. If i had chosen this microcontroller from the beginning It would had save me many months of night-coding. But this one is also the most expensive. As long as you don't need wireless or USB-composite devices, the RP2040 and ESP32 are very good choices.

First struggles with KMK

While I have already settled for a hardware design around the XIAO-BLE, now I had to work on polishing the firmware. Despite having the same amount of memory as the RP2040, it's circuitpython implementation provides less heap-memory to the final application. I presume that this is because of its BLE stack. If I added more than 20 key functions, it just simply crashed inmediatly after starting the KMK loop.

However, I'm still commited to the KMK/Circuitpython stack. I really think they are the best option if you want something easily programable for your final users. Fortunately I found this talk done by one of the developers of MicroPython, which is the base of CircuitPython https://www.youtube.com/watch?v=hHec4qL00x0. It has proven to be invaluable to my project. I have found many optimizations that have been useful to complete my USB stack with four layers, and all the previously mentioned functions. Here is the list:

Optimizations And Recommendations

Global and local scopes

In python, when you reference a variable, the runtime searchs for it first in the local scope of the current function, and then in the global scope. This means that of course, finding a local variable is faster than finding a global variable. However, in the talk is mentioned that, for implementation reasons , this time is even more drastic than it seems. I applied this technique in my code to assing the keys.

It was initally like this:

#KC it's a global variable
    layer0Asignations[0] =  KC.TAB
    layer0Asignations[1] =  KC.Q
    layer0Asignations[2] =  KC.W
    layer0Asignations[3] =  KC.FD(0)
...
     layer1Asignations[0] =  KC.ENTER
    layer1Asignations[1] =  KC.Z
    layer1Asignations[2] =  KC.Y
    layer1Asignations[3] =  KC.FD(0)        

And now it looks like this:

  #KC it's a global variable
#kc it's a local variable
    kc = KC
    home = kc.FD(0)
    layer0Asignations[0] =  kc .TAB
    layer0Asignations[1] =  kc .Q
    layer0Asignations[2] =  kc .W
    layer0Asignations[3] = home 
...
     layer1Asignations[0] =  kc .ENTER
    layer1Asignations[1] =  kc .Z
    layer1Asignations[2] =  kc .Y
    layer1Asignations[3] =  home 
...        

Not only this code will be faster, but by avoiding creating multiple copies of the same Key/KC object, my program has more heap memory available.

Optimize imports

Never use '*' when importing. I was initially using the split module in this way:

from kmk.modules.Split import *        

now I do this:

from kmk.modules.Split import Split, SplitSide, SplitType        

Also, take into account that CircuitPython removes non used references as soon as they go out of scope. Instead of using the imports in the global scope, I have now put the keyboard setup code inside a function, so they are no longer available after it returns. The 'del' operator is also very useful

def initKB():
    
        from kmk.extensions.media_keys import MediaKeys
        from kmk.modules.mouse_keys import MouseKeys
        ...
        del MediaKeys
        ...
kb =initKB()
kb.go()        

Remove unnecessary keys

All modules in the kmk repo provide plenty of functions for its own usage. For example, the RGB module provides keys to modify the brightness, change animations, modes. reset lights, etc... However, most of the projects don't need all of this simultaneously. Since I've focused my design in touch-typing, I don't need all of this rgb-effects. I promptly proceed to remove all the make_key() lines that I could find, and when possible, even removing the methods that each one triggers. Here is a list of keys that you may want to check if they are necessary in your project:

MediaKeys: Do you really need the 'MEDIA_EJECT'?

MIDI: MIDI_CC is only for potentiometers. If you don't have those, then you don't need this.

RGB: Do you really need all the effects? Comment the effects you are not using.

Also, I deleted all of the keypad keys, and mos of the international keycodes.


Write your own layer class.

The KMK documentation, to make your lights change according to your current layer, recommends creating a extension derived from the RGB class, and a module derived from the Layers class. This works but it also consumes a lot of memory.

What worked for me was to stop using the RGB extension all together, and instead calling the neopixel library inside the module derived from 'Layers'. Since my design only has 6 RGB LEDS, I wrote my own effects inside of it. You can check the class 'RGBLayers' in the NUTYR/code.py of the repo.

Don't use multipurposes modules

Both key kmk_keyboard class and Split module load BLE code, even if you are not using BLE. I created other classes from them, but without the BLE references. They are the NUTYR/kmk/usbkb.py, NUTYR/kmk/hid.py and NUTYR/kmk/modules/spuart.py . They are both functional and you can used them now.

Integrate modules and extensions functionality in your own keyboard class

Now that I had my own keyboard class, I decided to start adding more functionality to it. The first extension I 'absorbed' was the Media_Keys. It only consists of 'make_key()' calls, so I put them in the initialization function of usbkb.

The road ahead

With all these changes, I have around 13KB of memory left. I don't think I can add ble here. I plan to write parallel clases for BLE, and I might have to drop MIDI. I will replace my code.py for one that check whether it has to load USB or BLE classes, and run them. But I think my progress so far wil be useful for other people working in the KMK/CircuitPython combo. Both are awesome projects. Thanks to both teams!.


12 healthy KB available to add more functionality in USB mode






















Micah Alpern

Design & Product. Prev Sr Director Design @Instacart, Prior: Head of Design @Venmo

1 周

Really great write up, thank you for sharing. How do you sell your keyboards in Peru?

回复
叶言庭

The 1% Club学生

1 个月

You can try RMK. Although RMK is not yet fully mature, it supports Vial and Bluetooth, as well as the nice!nano. The tutorials are also very simple.

要查看或添加评论,请登录

社区洞察

其他会员也浏览了