[{"element_type":"text","content":"\n        \u003ch2\u003eUnicomp Mini M with CircuitPython\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://www.pckeyboard.com/\"\u003eUnicomp\u003c/a\u003e is the heir to the IBM Buckling Spring legacy. This style of keyboard has a distinctive sound \u0026amp; feel which some people just can't get enough of. I'm one of them, having used a crusty old Model M for at least a decade at work.\u003c/p\u003e\n\u003cp\u003eTheir \"\u003ca href=\"https://www.pckeyboard.com/page/product/MINI_M\"\u003eMini M\u003c/a\u003e\" is an 87-key (\"tenkeyless\") layout, and in late summer 2023 the controller was replaced with a board that has a Raspberry Pi Pico soldered on to it.\u003c/p\u003e\n\u003cp\u003eIt's too soon for me to have formed a strong opinion about this keyboard, though I will note that the cable seems to be in the category of \"incredibly cursed\": It is a USB A-A cable, which should not exist. If your computer is USB-C only you're entering dongle city, but, you're probably used to that.\u003c/p\u003e\n\u003cp\u003eIn this mini-guide, you'll learn how to replace the factory firmware with a fully customizable CircuitPython firmware. Of course, once you have the controller in UF2 bootloader mode, you could also use Arduino or Pico-SDK to program it with the software of your choice.\u003c/p\u003e\n\u003cp\u003eNote that this involves opening up the keyboard enclosure and there is always the possibility that you will damage the keyboard while doing so.\u003c/p\u003e\n      ","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/000/195/original/shark_justify-mike-smith_unicomp_2023.jpg?1695242586","metadata":{"caption":"Unicomp “Justify/Mike Smith” controller courtesy Admiral Sharks Keyboards"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eReverse Engineering\u003c/h2\u003e\n\u003cp\u003eI don't know who all to credit for the reverse engineering, but all the information I needed was in a fork of the open source keyboard controller \"\u003ca href=\"https://github.com/purdeaandrei/vial-qmk-mini-m\"\u003eVial-QMK\u003c/a\u003e\". Here are the basics:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eKeyboard is organized as a 16x12 matrix, with the columns controlled by a pair of 8-channel muxes. Together, pico pins GP0 through GP4 select one of 16 column channels, with just one of GP3 or GP4 selected by setting to a False pin value\u003c/li\u003e\n\u003cli\u003eThe 12 row inputs are GP11 through GP22\u003c/li\u003e\n\u003cli\u003eThe LEDs are on GP6 through 8, with a common anode on GP5 (so set GP5 True always, and GP6 through 8 to False to light the corresponding LED; GP6 is Num Lock)\u003c/li\u003e\n\u003cli\u003eThe matrix has no diodes, so anti-ghosting must be done in software\u003c/li\u003e\n\u003c/ul\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eInstalling CircuitPython \u0026amp; the keyboard code\u003c/h2\u003e\n\u003cp\u003eUndo the two screws on the bottom rear of the keyboard and carefully remove the top cover.\u003c/p\u003e\n\u003cp\u003eMove the keyboard assembly out of the way so you can access the controller PCB. Take care not to damage the fragile flexible PCBs in the process.\u003c/p\u003e\n\u003cp\u003eWhile holding down the BOOTSEL button on the Pi Pico, plug the USB cable into your computer.\u003c/p\u003e\n\u003cp\u003eIt will appear as an \u003cstrong\u003eRPI-RP2\u003c/strong\u003e drive. If the \u003cstrong\u003eRPI-RP2\u003c/strong\u003e drive is not recognized by your computer after a few seconds, unplug the keyboard and repeat the steps.\u003c/p\u003e\n\u003cp\u003eNeed more help getting started with CircuitPython or Raspberry Pi Pico? \u003ca href=\"https://learn.adafruit.com/getting-started-with-raspberry-pi-pico-circuitpython\"\u003eAdafruit has it covered in this guide\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eNow, make a back-up of your device's original firmware using \u003ca href=\"https://github.com/raspberrypi/picotool\"\u003epicotool\u003c/a\u003e:\u003c/p\u003e\n\u003cp style=\"padding-left: 40px;\"\u003e\u003ccode\u003epicotool save -a original_firmware_mini_m.uf2 -t uf2\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003eIf for any reason you don't want a custom firmware anymore, you can copy this back to the \u003cstrong\u003eRPI-RP2\u003c/strong\u003e bootloader drive.\u003c/p\u003e\n\u003cp\u003eNext, copy a CircuitPython UF2 to the device (I used 8.x for this project).\u003c/p\u003e\n\u003cp\u003eFrom the bundle grab the \u003cstrong\u003eadafruit_hid\u003c/strong\u003e library and copy it to \u003cstrong\u003eCIRCUITPY\\lib\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eGrab the raw version of \u003cstrong\u003ecode.py\u003c/strong\u003e from the gist below, copy it to \u003cstrong\u003eCIRCUITPY\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eAfter a few moments, the board will re-start with your code. You can type in the keyboard just like normal!\u003c/p\u003e\n\u003cp\u003eIf it doesn't work as expected, open a serial terminal to check for Python errors. Otherwise, congratulations! You now have a keyboard running CircuitPython!\u003c/p\u003e\n\u003cp\u003eIs your mind brimming with ideas about how to customize how your keyboard works? Read below for an overview of some key parts of the code.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"button","content":"https://gist.github.com/jepler/eec312a023001b5870bfa32cc08a5aab","metadata":{"class":"btn btn-large btn-block btn-primary","button_type":"primary","target":"_self","zip_folder":"false"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eScanning the matrix with CircuitPython\u003c/h2\u003e\n\u003cp\u003eBecause this kind of matrix column select is not supported by CircuitPython's KeyMatrix, I had to write a keyboard scanner in pure Python. This isn't the fastest but worked well for my purposes.\u003c/p\u003e\n\u003cp\u003eThe scanner is written as a generator; it will output a series of numbers, which combine the \u003ccode\u003ekey_number\u003c/code\u003e and an \u003ccode\u003eis_pressed\u003c/code\u003e flag. Each time it has scanned the whole matrix it will output the special value \u003ccode\u003eNone\u003c/code\u003e which is a cue to the main loop to do \"other things\", such as updating the LEDs.\u003c/p\u003e\n\u003cp\u003eOne step of the scanner is simply to capture the current state of every key into an array; this also tracks auxiliary information \"counts_by_row\" (the number of pressed keys in each row) and \"counts_by_column\" (the number of pressed key in each column), which will be used for ghost detection. This is done by the scan1 nested function.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"ROWS = const(12)\nCOLS = const(16)\nN = const(ROWS*COLS)\ndef scanner():\n    def scan1(state):\n        for row in range(ROWS):\n            counts_by_row[row] = 0\n\n        for column in range(COLS):\n            A0.value = column \u0026 1\n            A1.value = column \u0026 2\n            A2.value = column \u0026 4\n            if column \u0026 8:\n                EN_U2.value = False\n            else:\n                EN_U1.value = False\n\n            n = 0\n            for row in range(len(ROW)):\n                pin = ROW[row]\n                i = row + column * ROWS\n                # never fill a matrix position with no associated key\n                # this improves rollover for instance, A+F1+W cause a ghost\n                # at matrix location that is otherwise unused. By not\n                # recording a value at the ghost location, the ghost\n                # suppression code below won't activate.\n                if kc_unused[i]: continue\n                value = not pin.value\n                n += value\n                counts_by_row[row] += value\n                state[i] = value\n            counts_by_column[column] = n\n\n            if column \u0026 8:\n                EN_U2.value = True\n            else:\n                EN_U1.value = True","metadata":{"language":"python","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eContinuing in the scan function, \"scan1\" is performed repeatedly until the output matches. Then, ghost key presses (ones which come from a row with at least two keys AND a column with at least two keys) are eliminated. Finally, the changes are \"yield\"ed.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"def scanner():\n    def scan1(state):\n        ... # as above\n        \n    counts_by_column = [0] * COLS\n    counts_by_row = [0] * ROWS\n    state = [0] * N\n    latch = [0] * N\n    old_state = [0] * N\n\n    while True:\n        yield None # at the start of a scan\n\n        scan1(state)\n        scan1(latch)\n        while state != latch: # something changed\n            state, latch = latch, state\n            scan1(latch)\n\n        for column in range(COLS):\n            if counts_by_column[column] \u003c 2:\n                continue\n            for row in range(ROWS):\n                i = row + column * ROWS\n                if not state[i]:\n                    continue\n                if counts_by_row[row] \u003c 2:\n                    continue    \n                state[i] = old_state[i]\n                \n        for i in range(N):\n            if state[i] != old_state[i]:\n                yield i | (state[i] \u003c\u003c 15)\n\n        old_state, state = state, old_state","metadata":{"language":"python","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eA pair of long tables (the second for numlock mode) are used for converting key numbers into USB HID keycodes (the second table only has to include items that differ from the non-numlock mode):\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"keycodes = {\n    12: K.ESCAPE,\n    26: K.F1,\n    48: K.F2,\n    60: K.F3,\n...\n}\n\nkeycodes_l1 = {\n    89: K.KEYPAD_SEVEN,  \n    94: K.KEYPAD_EIGHT,\n    93: K.KEYPAD_NINE,\n...\n}","metadata":{"language":"python","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n        \u003cp\u003eThe program's forever loop just scans the keyboard, sends USB HID events, and reacts to keyboard LED changes. If the first LED is lit, then the \"layer 1\" (numlock) keycodes are used.\u003c/p\u003e\n\u003cp\u003eThe code is written so that after the initial setup no additional memory is ever allocated. This avoids the possibility of a long delay between scans of the key matrix. Avoiding memory allocations is why internal functions like \u003ccode\u003ekbd._add_keycode_to_report\u003c/code\u003e are called.\u003c/p\u003e\n\u003cp\u003eWhenever the code exits (e.g., because of an error or because code.py was edited), the finally block makes sure all keys are released so you aren't left with a key stubbornly still held down:\u003c/p\u003e\n      \n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"led = ~0\ngc.disable() # So we never get GC pauses\ntry:\n    for ev in scanner():\n        if ev is None: # complete scan, poll LEDs\n            leds = kbd.led_status\n            if leds and leds[0] != led:\n                led = leds[0]\n                for i in range(3):\n                    LEDS[i].value = not (led \u0026 (1 \u003c\u003c i))\n            continue\n\n        key_number = ev \u0026 0xfff\n        pressed = ev \u003e\u003e 15\n\n        keycode = keycodes.get(key_number)\n        if led \u0026 1:\n            keycode = keycodes_l1.get(key_number, keycode)\n\n        # the normal methods aren't used so that gc allocations can be avoided\n        if keycode is not None:\n            if pressed:\n                kbd._add_keycode_to_report(keycode)\n            else:\n                kbd._remove_keycode_from_report(keycode)\n            kbd._keyboard_device.send_report(kbd.report)\n\nfinally:\n    kbd.release_all()","metadata":{"language":"python","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n        \u003ch2\u003eMore on ghost detection\u003c/h2\u003e\n\u003cp\u003eThe algorithm used here for ghost detection is ad-hoc but seems to work well: It does not strictly limit the keyboard to 2KRO, though many combinations of 3 or more keys are not permitted. Exactly which ones work depend on the key matrix, which Unicomp says they have designed to effectively provide up to 10KRO.\u003c/p\u003e\n\u003cp\u003eOne example of a set of keys that cause ghosting is a+s+space. Without ghost detection, this causes a spurious down-arrow key to also be seen.\u003c/p\u003e\n\u003cp\u003eThis happens because of the locations of these keys in the matrix.\u0026nbsp; Key \"a\" is at (column, row) = (11, 2), key \"s\" is at (0, 2), key \"space\" is at (0, 10) , and \"cursor down\" is at (11, 10).\u003c/p\u003e\n\u003cp\u003eWhen the switches at (11, 2), (0, 2), and (0, 10) are all closed, this creates a path from column 11 to row 10 through the 3 closed switches, even though the switch at (11, 10) is open, and makes the cursor down key appear as a ghost.\u003c/p\u003e\n\u003cp\u003eHere's what the algorithm does to prevent ghosting: Suppose A and S are held and space is pressed. Now, columns 0 and 11 have 2 keys pressed, and rows 2 and 10 have 2 keys pressed. This causes ghost detection to trigger.\u003c/p\u003e\n\u003cp\u003eFor all 4 of those positions, the prior value at those positions is kept instead of being updated. This means that the result of the ghost detection is to continue pressing A and S, but to not start pressing space or cursor down. The ghost key (cursor down) is avoided, at the cost of not allowing the desired key (space) to be reported either.\u003c/p\u003e\n\u003cp\u003eIf either A or S is released before space is released, then ghost detection will end and the desired key (space) will be reported at that time. Otherwise, if space is released first, it will never be reported.\u003c/p\u003e\n\u003cp\u003eThere are also combinations of 3 keys where the 4th position on the matrix is unused. In this case, it's possible to treat the 3 keys unambiguously. The \u003ccode\u003ekc_unused\u003c/code\u003e variable allows efficiently checking for this. An example combination that is enabled by this check is A+F1+W.\u003c/p\u003e\n\u003cp\u003eFor sloppy typists like me, who let fingers linger on keys after pressing them, good ghost avoidance is key to a good keyboard experience.\u003c/p\u003e\n\u003cp\u003eFor more information, see \u003ca href=\"https://geekdroids.com/keyboard-ghosting-explained/\"\u003ethis article from geekdroids\u003c/a\u003e (the source for the below illustration). Admiral Shark \u003ca href=\"https://sharktastica.co.uk/sims/matrix?kb=minim\"\u003ehosts a key matrix emulator\u003c/a\u003e where you can see more about how various keys interact.\u003c/p\u003e\n      \n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/000/196/original/Figure5-4267085126.png?1695258392","metadata":{"caption":"Keyboard ghosting illustration from geekdroids"}},{"element_type":"text","content":"\n  \n  \n  \n        \u003ch2\u003eOverclocking\u003c/h2\u003e\n\u003cp\u003eWhat's better than measuring performance and deciding whether it's good enough? Turning every \"go faster\" knob you have available. That's why the firmware overclocks the RP2040 microcontroller to 240MHz. This is not officially supported by Raspberry Pi and might damage your keyboard, so I recommend for greatest safety that you comment out the following lines in code.py:\u003c/p\u003e\n      \n\n\n","metadata":{}},{"element_type":"code","content":"print(\"overclocking\")\nmicrocontroller.cpu.frequency = 240_000_000","metadata":{"language":"auto","linenums":false}},{"element_type":"text","content":"\n  \n        \u003ch2\u003eOther optimization possibilities\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eMake \u003ccode\u003ekeycodes\u003c/code\u003e \u0026amp; \u003ccode\u003ekeycodes_l1\u003c/code\u003e be lists instead of dicts, so that indexing is faster\u003c/li\u003e\n\u003cli\u003eImplement the key scanner in PIO so it can run without CPU intervention\u003c/li\u003e\n\u003c/ul\u003e\n      \n","metadata":{}}]