-
A Year Later
09/16/2020 at 10:01 • 0 commentsI must say I'm a bit disappointed with this project. I was expecting it to become my default keyboard, but somehow it never did. It just always felt ever so slightly wrong. Initially I thought it was the minimally tighter key spacing, or the improvised stabilizers, and I think they do contribute to the weirdness a little bit, but after building the #Dorsch 40k Keyboard I can confidently say the main reason for this keyboard being uncomfortable a the switches.
Those sunken low-profile switches are very different from the regular (not sunken) chocolate switches — not only they are not nearly as clicky, but they also have this very annoying resistance — as if they didn't fit together very well.
And since the switches are really the bulk of the keyboard's cost, I don't see much chance for rescuing this project.
-
Details
12/17/2019 at 15:55 • 0 commentsI finished up a couple of details recently, so that I can say now the project is finished.
First, I added support for the CapsLock key to the firmware. It's still a hack for now, so the change is not merged upstream, but it works well enough for me, so I decided to keep it. I will work on getting that merged to CircuitPython with a proper API.
Second, I replaced the USB cable with a flat one that just arrived. Flat keyboard — flat cable. It also doesn't get tangled as much.
I also disabled all the USB endpoints except for HID keyboard, so that it's visible only as a regular keyboard when connected to a computer.
Finally, I stuck some nice padding to the back of the keyboard. Those are sticky pads for something called "fingerboard", whatever that is. They work well enough. I had to do two layers in the middle, to prevent it from wobbling, because it's a bit thinner there, with no stabilizers in that area.
-
Firmware
12/02/2019 at 23:23 • 0 commentsIn its simplest form, keyboard firmware is not rocket science. You basically need to do three things: scan the key matrix to see which keys are pressed, translate the matrix locations into key codes and modifiers, and send USB HID reports with lists of currently pressed keys. Easy.
So why is the KMK firmware so large? Well, it's written using an "enterprise" approach — everything is a class that has a hierarchy at least four levels deep, with abstract interfaces, pre- and post-event hooks, handlers, validators, layers of abstraction, flexibility and extensibility. Also RGB LEDs and Unicode emoji. Unfortunately, removing all the things I didn't need would require quite a bit of work, as despite having so many layers of abstraction, the code is actually pretty tangled. So instead I decided to just write the most naive code I could, and see if that works. I came up with this:
import board import digitalio import usb_hid COLS = (board._C1, board._C2, board._C3, board._C4, board._C5, board._C6, board._C7, board._C8, board._C9, board._C10, board._C11, board._C12, board._C14, board._C13, board._C15) ROWS = (board._R1, board._R2, board._R3, board._R4, board._R5) def run(cols, rows, matrix): report = bytearray(8) report_mod_keys = memoryview(report)[0:1] report_no_mod_keys = memoryview(report)[2:] for device in usb_hid.devices: if device.usage == 0x06 and device.usage_page == 0x01: break else: raise RuntimeError("no HID keyboard device") cols = [digitalio.DigitalInOut(pin) for pin in cols] rows = [digitalio.DigitalInOut(pin) for pin in rows] for col in cols: col.switch_to_output(value=0) for row in rows: row.switch_to_input(pull=digitalio.Pull.DOWN) last_state = bytearray(len(cols)) layer = 0 while True: changed = False for x, col in enumerate(cols): col.value = 1 bits = 0 for y, row in enumerate(rows): bit = row.value << y bits |= bit if row.value != bool(last_state[x] & (1 << y)): changed = True code = matrix[layer][y][x] if code == 0: pass elif code == 135: layer = int(row.value) elif code > 127: modifier = 1 << (code - 128) if row.value: report_mod_keys[0] |= modifier else: report_mod_keys[0] &= ~modifier else: if row.value: for i, value in enumerate(report_no_mod_keys): if value == 0x00: report_no_mod_keys[i] = code break else: for i, value in enumerate(report_no_mod_keys): if value in (matrix[0][y][x], matrix[1][y][x]): report_no_mod_keys[i] = 0x00 col.value = 0 last_state[x] = bits if changed: device.send_report(report)
Of course you need a main.py file to run this:
import flounder MATRIX = ( (b'\x1e\x1f !"#$%&\'-.\x00*I', b'+\x14\x1a\x08\x15\x17\x1c\x18\x0c\x12\x13/01L', b'5\x04\x16\x07\t\n\x0b\r\x0e\x0f34\x00(K', b')\x81\x1d\x1b\x06\x19\x05\x11\x10678\x85RN', b'\x809\x83\x82\x00,\x00\x00\x00\x86\x87\x84PQO'), (b':;<=>?@ABCDE\x00\x00\x00', b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'\x81\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x85K\x00', b'\x80\x00\x83\x82\x00\x00\x00\x00\x00\x86\x87\x84JNM') ) flounder.run(flounder.COLS, flounder.ROWS, MATRIX)
Yes, it's not very human-readable. The MATRIX actually defines the codes of all the keys in two layers. Of course I didn't write it like that. I generated it with this code:
from micropython import const import pprint _A = const(4) _B = const(5) _C = const(6) _D = const(7) _E = const(8) _F = const(9) _G = const(10) _H = const(11) _I = const(12) _J = const(13) _K = const(14) _L = const(15) _M = const(16) _N = const(17) _O = const(18) _P = const(19) _Q = const(20) _R = const(21) _S = const(22) _T = const(23) _U = const(24) _V = const(25) _W = const(26) _X = const(27) _Y = const(28) _Z = const(29) _1 = const(30) _2 = const(31) _3 = const(32) _4 = const(33) _5 = const(34) _6 = const(35) _7 = const(36) _8 = const(37) _9 = const(38) _0 = const(39) _ENT = const(40) # Enter _ESC = const(41) # Esc _BS = const(42) # Backspace _TAB = const(43) # Tab _SPC = const(44) # Space _MN = const(45) # Minus -/_ _EQ = const(46) # Equal =/+ _LB = const(47) # Left bracket [/{ _RB = const(48) # Right bracket ]/} _BSL = const(49) # Backslash \/| _SC = const(51) # Semicolon ;/: _QT = const(52) # Quote '/" _GR = const(53) # Grave `/~ _CM = const(54) # Comman ,/< _DT = const(55) # Dot ./> _SL = const(56) # Slash //? _CAPS = const(57) # Caps lock _F1 = const(58) _F2 = const(59) _F3 = const(60) _F4 = const(61) _F5 = const(62) _F6 = const(63) _F7 = const(64) _F8 = const(65) _F9 = const(66) _F10 = const(67) _F11 = const(68) _F12 = const(69) _PS = const(70) # Print screen _SCL = const(71) # Scroll lock _PA = const(72) # Pause _INS = const(73) # Insert _HOME = const(74) # Home _PGUP = const(75) # Page up _DEL = const(76) # Delete _END = const(77) # End _PGDN = const(78) # Page down _AR = const(79) # Arrow right _AL = const(80) # Arrow left _AD = const(81) # Arrow down _AU = const(82) # Arrow up _LCT = const(128) # Left control _LSH = const(129) # Left shift _LAL = const(130) # Left alternate _LSP = const(131) # Left super _RCT = const(132) # Right control _RSH = const(133) # Right shift _RAL = const(134) # Right alternate _FN = const(135) # Function MATRIX = ( (_1, _2, _3, _4, _5, _6, _7, _8, _9, _0, _MN, _EQ, 0, _BS, _INS), (_TAB, _Q, _W, _E, _R, _T, _Y, _U, _I, _O, _P, _LB, _RB, _BSL, _DEL), (_GR, _A, _S, _D, _F, _G, _H, _J, _K, _L, _SC, _QT, 0, _ENT, _PGUP), (_ESC, _LSH, _Z, _X, _C, _V, _B, _N, _M, _CM, _DT, _SL, _RSH, _AU, _PGDN), (_LCT, _CAPS, _LSP, _LAL, 0, _SPC, 0, 0, 0, _RAL, _FN, _RCT, _AL, _AD, _AR), ), ( (_F1, _F2, _F3, _F4, _F5, _F6, _F7, _F8, _F9, _F10, _F11, _F12, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), (0, _LSH, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, _RSH, _PGUP, 0), (_LCT, _CAPS, _LSP, _LAL, 0, 0, 0, 0, 0, _RAL, _FN, _RCT, _HOME, _PGDN, _END), ) matrix = tuple(tuple(bytes(row) for row in layer) for layer in MATRIX) pprint.pprint(matrix)
Actually initially I used that code directly, but then I decided to try and save a little bit more flash space (the code above takes almost no RAM, as it's all constants that are replaced in place with number literals).
In any case, despite a few quirks (the modifier keys have to be present in both layers in the same places, for example) and a complete lack of software debouncing, it seems to work well.
As a final touch, I compiled a version of CircuitPython with all but HID USB devices disabled, so that it's only visible as a keyboard, and not a million other devices.
-
A Moment of Truth
12/02/2019 at 23:05 • 3 commentsI finally started working on the firmware for this keyboard, starting with writing a board description and compiling CircuitPython for it. Unfortunately that enabled me to find a number of problem with the PCB.
First, I needed to figure out which pins go to which rows and columns, and let me say that the schematic view of Fritzing is not very helpful with that:
It just piled all the switches and diodes on top of each other, and I really don't have the energy to arrange them all properly. But no problem, we can use the PCB design view, and simply look where each trace goes. If we click the trace, it gets highlighted, together with all things that are connected to it, so that's not too bad. While doing that, I noticed a curious thing:
Apparently I wasted one of the pins, by connecting it directly to the backslash button, before the diode. This isn't a problem, since there are still enough pins (just barely), and there is another pin properly connected to the whole row after that diode, but it shows what my mental state was when I was designing it.
With the CircuitPython compiled and flashed, the next step is to try the KMK firmware. Unfortunately, it doesn't even fit on the 45KB flash disk my tiny SAMD21 has — not even compiled to byte-code. So I started trimming. Removed all the tap-dancing and leader-key code, all the debugging prints, all the RGB LED handling, Unicode keys, and so on. Finally I got it to a size that just barely fits, together with a layout definition. And of course I get MemoryError. Fine, I will write my own keyboard firmware in CircuitPython. With blackjack. And hookers.
The first step was to wite the matrix scanning code, of course. That went fast, but I noticed a weid quirk: whenever I pressed a key from row 3, the key from row 4 would also get pressed. And the other way around, pressing a key from row 4 would also press a key from row 3. Seems like there is a short between the two rows. I couldn't see any obvious problems, so I just touched up the QFN package a bit, to make sure there is no short underneath, but no dice. Took out my trusty ohmmeter, and noticed that the resistance is pretty much that of a solid trace — that doesn't look like some accidental short somewhere. To make sure, I took an unpopulated board, and checked there — sure enough, there is a short as well. That made me look closer at the traces, and sure enough:
The row 4 is dipping under the column traces here, but there is also a hard-to-see trace for the row 3 going right through it on the underside of the PCB. It's hard to see, because I rooted it along a silkscreen line, genius. Cutting that trace on both sides and connecting the two vias with a piece of wire solved this problem.
Next, Caps Lock and the A key seem to be the same key — no matter which one I press, I get the same matrix position. Looking closer, yes, of course I connected both to the same row and column. Fortunately, soldering the diode for Caps Lock up-side down, and then adding a wire leading to Left Ctrl on row 5 fixed the problem. It moved the key to row 5, where there is a free spot. Maybe the layout definition will look a bit confusing, but who cares.
Finally, five of the keys had the diodes soldered only on one side — re-touching them with the iron fixed it.
It all works now mechanically, even the Caps Lock led:
Next time, more about the firmware.
-
Wouldhavebeens
11/17/2019 at 09:04 • 0 commentsThe keyboard is now pretty much assembled mechanically, now I "only need to program it" (haha). But I have learned a bit with this, and I can list things that I would have done differently if I was doing it again:
- Standard key spacing. I made the keys denser to make the keyboard smaller, but that introduced a number of problems. The space turned out to be too big. The switches had to be soldered really carefully, to avoid the keys colliding. And also there is something in the muscle memory that expects the standard distances between keys — they keyboard takes a bit to get used to.
- Tighter holes. I left generous tolerances both in the sizes of the soldering holes, and the holes that take the body of the switch — to be safe with varying manufacturing processes and my own inaccuracies. Turns out that makes it hard to solder all the switches straight.
- Plated holes for the stabilizers. I didn't expect I will have to make my own stabilizers out of paperclips, but if I knew that, the holes for mounting the stabilizers would look completely different — the hooks would be soldered into the PCB.
- Pads for USB cable shield. So that I can solder it to the PCB for more robust mechanical connection.
- Maybe use a standard 32u4 chip, so that I don't have to write the firmware myself.
All in all, this might have gone much worse, so I'm quite happy about how it came out.
-
Paperclips!
11/17/2019 at 07:01 • 0 commentsSince the regular plastic key stabilizers were simply too big, no matter how much I whittled them down with a knife, I had to come up with something completely different. The inspiration came from an older stabilizer design that I saw in #Alpen Clack, where the key just had a kind of horizontal slit in which the wire of the stabilizer would move. So first of all, I needed a way to hold the stabilizer wire:
That was easily achieved by a simple hook made of the same paperclip wire. Next, I needed that horizontal channel — I thought can simply use the surface of the key cap, and add something to hold it from the bottom. Gluing a bit of wire in the right place seems to work well enough for this.
Now just bend the stabilizing wire at the ends to hook into those channels, and voila, it works!
I repeated the same process for the shift and enter keys. Enter was a bit tricky, because there wasn't enough room for the channel — I had to cut out the "+" sockets for the "proper" stabilizers to make space. I still need to do this for the backspace key, however that will wait for a better USB cable — right now the temporary cable I have is taking up all the space under the key, actually making it difficult to press, but I already have a thinner one on order.
-
My Space is Too Big
11/13/2019 at 08:55 • 0 commentsThe PCBs arrived today! They look great:
So of course I had to try and insert all the switches and keycaps to see how they fit. The fit is (intentionally) tight, as I made them a bit denser than on a normal keyboard, but they seem to all fit properly:
With one notable exception: the space bar. You see, since the caps were designed for a looser layout, the space includes a little more room for the gaps between keys, so it's longer than I anticipated. I now how two options: whittle it down a bit with a dremel to fit, or use the alternate, smaller, ALT keys from the keycap set:
For now I will go with the second option, as it's less destructive and can always be undone.
-
Low-profile Switches
10/27/2019 at 21:33 • 0 commentsThe first 50 switches have arrived, and I can finally compare them with my footprint and also see how they fit in the PCB. They are actually good 1.7mm lower than the "regular" kailh chocolate switches:
(Note that on this photo the switch slipped one step lower than it should be — it doesn't sink into the PCB that far, only as far as the part from witch the legs protrude.)
The footprints and the spacing both look correct, so I will be ordering the PCB in the coming days (I'm waiting for the new plate for #PewPew M4 to arrive, so that I can see if I will need a new PCB for it as well, then I can order them together and save on shipping.)
-
PCB
10/10/2019 at 17:42 • 0 commentsI decided to go with the stabilizers I currently have, and maybe cut them up a bit—we will see how that works. Having finally measured the key caps, and figured out where the stabilizers should go and whether the caps won't be too close together, all that is left to do is to actually design the PCB. Here it is:
As you can see, it's a tight fit. I will probably end up gluing the stabilizers to the board anyways (after cutting off the parts that go under the board and that normally hold them in place). I had to drop the USB port, and I will instead use a USB cable—there simply wasn't enough room for it at the top of the board, between the switches and the stabilizers. The 48-pin SAMD21 microcontroller is perfect—just the right number of pins for a 5×15 matrix, with an extra pin left for the NumLock LED (also re-used as a status LED).
I will let this design sit there for a while, as I'm sure I will find some errors in it. I might even get rid of all those right angles if I'm bored, but I decided to not care much for them.
-
Caps
10/10/2019 at 09:51 • 0 commentsThe key caps have arrived, so I finally can do the measurements for the placement of stabilizers, figure out where the USB socket and the MCU should go, and maybe finalize the PCB. Turns out the 1.5u keys don't have stabilizers, so that leaves me with just the 5.5u space, 2.5u shift, and 2u enter and backspace keys. I can also test spacing between the keys, to make sure the caps don't collide.
(Sorry for the hair, it's the shedding season.)
I was actually quite surprised that there are many alternate key sizes for some of the keys — specifically the enter, shift, alt and fn keys. That gives me a little bit more flexibility.