{"id":1510,"date":"2025-09-12T13:12:08","date_gmt":"2025-09-12T11:12:08","guid":{"rendered":"https:\/\/redero.fr\/?p=1510"},"modified":"2025-09-22T19:49:12","modified_gmt":"2025-09-22T17:49:12","slug":"keyboard-from-trash-to-open-source","status":"publish","type":"post","link":"https:\/\/redero.fr\/?p=1510&lang=en","title":{"rendered":"Keyboard from trash to open source"},"content":{"rendered":"\n<p>A while ago, I picked up a keyboard from a trash pile in the street.<br>It appeared to be a quality product, and it was, except it was very dirty and missing lots of keycaps.<br>What also caught my eyes was the SWD debug header under the missing spacebar\u2026 I know the open-source keyboard community is very active, and after cleaning and adding some new keycaps, I could not resist trying to cook a new firmware for it.<\/p>\n\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;69e2bef93d3c9&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"69e2bef93d3c9\" class=\"wp-block-image size-large wp-duotone-unset-1 wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"768\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/2025-09-02-10-59-26-028-1024x768.jpg\" alt=\"\" class=\"wp-image-1512\" srcset=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/2025-09-02-10-59-26-028-1024x768.jpg 1024w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/2025-09-02-10-59-26-028-300x225.jpg 300w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/2025-09-02-10-59-26-028-768x576.jpg 768w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/2025-09-02-10-59-26-028-1536x1152.jpg 1536w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/2025-09-02-10-59-26-028-2048x1536.jpg 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\taria-label=\"Agrandir\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.imageButtonRight\"\n\t\t\tdata-wp-style--top=\"state.imageButtonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure>\n\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;69e2bef93d8bf&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"69e2bef93d8bf\" class=\"wp-block-image size-large wp-duotone-unset-2 wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"413\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/apexpromini_swd_zoom-1024x413.jpg\" alt=\"\" class=\"wp-image-1650\" srcset=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/apexpromini_swd_zoom-1024x413.jpg 1024w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/apexpromini_swd_zoom-300x121.jpg 300w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/apexpromini_swd_zoom-768x310.jpg 768w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/apexpromini_swd_zoom-1536x620.jpg 1536w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/apexpromini_swd_zoom-2048x826.jpg 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\taria-label=\"Agrandir\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.imageButtonRight\"\n\t\t\tdata-wp-style--top=\"state.imageButtonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure>\n\n\n\n<p>So let us start by analyzing the board and its architecture!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1. Reverse-engineering<\/h2>\n\n\n\n<p>This keyboard has 62 keys on 5 lines, and each one has its own individual RGB LED.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1.1 Chip references<br><\/h3>\n\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;69e2bef93dd7f&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"69e2bef93dd7f\" class=\"wp-block-image size-large wp-duotone-unset-3 wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"768\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/2025-09-02-11-29-19-063-1024x768.jpg\" alt=\"\" class=\"wp-image-1513\" srcset=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/2025-09-02-11-29-19-063-1024x768.jpg 1024w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/2025-09-02-11-29-19-063-300x225.jpg 300w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/2025-09-02-11-29-19-063-768x576.jpg 768w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/2025-09-02-11-29-19-063-1536x1152.jpg 1536w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/2025-09-02-11-29-19-063-2048x1536.jpg 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\taria-label=\"Agrandir\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.imageButtonRight\"\n\t\t\tdata-wp-style--top=\"state.imageButtonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure>\n\n\n\n<p>Getting my hands on a nice datasheet makes everything so much easier, so I spent a lot of time guessing the references.<\/p>\n\n\n\n<p>The SoC is an STM32L412RBT6, easy to find out since it is written on it. The flash reference was also easy to read.<\/p>\n\n\n\n<p>I struggled to find the LED controller reference. <br>This chip has pins accessible to probing, even if it is not very practical due to their tiny size. Probing could also short the pins, so you have to make sure everything is turned off before doing this kind of work.<\/p>\n\n\n\n<p>With my multimeter in continuity mode, aka beepy mode, I checked that the chip outputs connect to LED lines, and that it connects to the SoC via SPI &#8211; I know it is an SPI bus because the testpoints are labeled LED_MOSI, LED_MISO, LED_SCK and LED_CS1.<\/p>\n\n\n\n<p>The marking I read on the chip was \u00ab\u00a0W1833 S7430\u00a0\u00bb, but googling that gave no results.<\/p>\n\n\n\n<p>I gathered some information to run a more advanced search for my reference:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>LED driver (because what else could be connected to all the LEDs)<\/li>\n\n\n\n<li>Connected with SPI (written on testpoints)<\/li>\n\n\n\n<li>40 pins (4&#215;10) (counted)<\/li>\n\n\n\n<li>Size around 5mm x 5mm size (measured)<\/li>\n\n\n\n<li>QFN-type package (dunno if there is a pad under this)<\/li>\n<\/ul>\n\n\n\n<p>I started listing and filtering chips in catalogues distributors. I also tried feeding my chip description to ChatGPT, it answered a bunch of interesting references but not the one I was looking for. In order to verify if a references matches, the first thing I check is the pin disposition: for this driver, SPI and other non-LED pins are opposite to pin 1.<\/p>\n\n\n\n<p>I ended up noticing that a company named Lumissil seemed to have several references that almost matched my description. It specializes in LED drivers, so I opened their full catalogue. Most of their products are I2C, so I filtered out all non-SPI references, and ended up finding the reference IS31FL3743 A and B chips.<\/p>\n\n\n\n<p>They match my description\u2026 and then I understood I was misreading \u00ab\u00a0S7430\u00a0\u00bb: it was actually \u00ab\u00a03743B\u00a0\u00bb. This chip is a Lumissil IS31FL3743B, and I need to buy a <a href=\"https:\/\/andonstar.com\/soldering-microscope\/\">magnifier<\/a>.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-medium wp-duotone-unset-4\"><img loading=\"lazy\" decoding=\"async\" width=\"284\" height=\"300\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/andonstar-284x300.jpg\" alt=\"\" class=\"wp-image-1641\" srcset=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/andonstar-284x300.jpg 284w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/andonstar-971x1024.jpg 971w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/andonstar-768x810.jpg 768w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/andonstar.jpg 1422w\" sizes=\"auto, (max-width: 284px) 100vw, 284px\" \/><\/figure>\n\n\n\n<p class=\"has-text-align-center has-small-font-size\" style=\"margin-top:0;margin-bottom:0\">Lab shopping list item #1269: Soldering microscope<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1.2 Pin connections untangling<\/h3>\n\n\n\n<p>After this, I used the datasheet and the multimeter to find out which LED is controlled by which pin. It took me a while and lots of beeps, but I ended up with a nice spreadsheet with all the indexes. The PCB overlay marking were very helpful: since each keyboard key has its reference marked, I can use them to not confuse any of the 62 keys.<\/p>\n\n\n\n<p>For the keys pinout, it got a bit more complicated. First I read some keyboard schematics basics: they work in a matrix, with transistors, and most of the time diodes to prevent one keystroke to be detected as other keystrokes from the same line (or column). The little trick in my case is that the Schottky diodes are packaged by two, so there are only 3 diode chips per column, for 5 keys.<\/p>\n\n\n\n<p>In order to help me a little bit finding connections, I also probed the keyboard live, with the oscilloscope: the keyboard scan has a pretty recognizable signal of 3ms period and 110us active-high pulse.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full wp-duotone-unset-5\"><img loading=\"lazy\" decoding=\"async\" width=\"265\" height=\"225\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/key_signal_mosleft_cropped.png\" alt=\"\" class=\"wp-image-1727\"\/><\/figure>\n\n\n\n<p class=\"has-text-align-center has-small-font-size\" style=\"margin-top:0;margin-bottom:0\">Row outputs on manufacturer\u2019s firmware<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full wp-duotone-unset-6\"><img loading=\"lazy\" decoding=\"async\" width=\"265\" height=\"226\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/key_signal_two_rows.png\" alt=\"\" class=\"wp-image-1518\"\/><\/figure>\n\n\n\n<p class=\"has-text-align-center has-small-font-size\" style=\"margin-top:0;margin-bottom:0\">Superposition of 2 adjacent rows outputs on manufacturer\u2019s firmware<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full wp-duotone-unset-7\"><img loading=\"lazy\" decoding=\"async\" width=\"264\" height=\"225\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/key_signal_line_noise.png\" alt=\"\" class=\"wp-image-1519\"\/><\/figure>\n\n\n\n<p class=\"has-text-align-center has-small-font-size\" style=\"margin-top:0;margin-bottom:0\">Scan signal noise on column input lines, no key press<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-full wp-duotone-unset-8\"><img loading=\"lazy\" decoding=\"async\" width=\"264\" height=\"226\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/key_signal_pressed_plus_sync.png\" alt=\"\" class=\"wp-image-1725\"\/><\/figure>\n\n\n\n<p class=\"has-text-align-center has-small-font-size\" style=\"margin-top:0;margin-bottom:0\">Key press on column input line (yellow), with row signal superimposed (blue)<\/p>\n\n\n\n<p>After a good while I finally realized that each one of my rows was scanned by a signal, not directly from the STM32, but amplified beforehand, by a mystery component, maybe an op-amp. If someone recognizes this strange 6-pins package, with a bigger pin in the middle, please contact me.<\/p>\n\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;69e2bef93eb4e&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"69e2bef93eb4e\" class=\"wp-block-image size-large wp-duotone-unset-9 wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"345\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/apexpro_bottom_notes-1-1024x345.png\" alt=\"\" class=\"wp-image-1619\" srcset=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/apexpro_bottom_notes-1-1024x345.png 1024w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/apexpro_bottom_notes-1-300x101.png 300w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/apexpro_bottom_notes-1-768x259.png 768w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/apexpro_bottom_notes-1-1536x518.png 1536w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/apexpro_bottom_notes-1-2048x690.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\taria-label=\"Agrandir\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.imageButtonRight\"\n\t\t\tdata-wp-style--top=\"state.imageButtonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure>\n\n\n\n<p>The STM32 pins connect to the input of my amp thingies for rows, and to the output of my dual diodes thingies for columns. This STM32 package pins can be comfortably probed to find continuity, still with the beep mode. So I ended up finding the whole matrix for keys too, and it is different than the matrixing of LEDs.<\/p>\n\n\n\n<p>The full pinout is detailed <a href=\"https:\/\/github.com\/zephyrproject-rtos\/zephyr\/blob\/main\/boards\/steelseries\/apex_pro_mini\/doc\/index.rst\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1.3 Protocol recording<\/h3>\n\n\n\n<p>Since I was not sure to be able to copy the factory firmware, I decided to record the communication between SoC and LED driver, even if I have full documentation on them and do not need to reverse-engineer the protocol itself. There are still some parameters and fine-tuning to retrieve.<\/p>\n\n\n\n<p>Unfortunately, this means soldering on testpoints and I always damage stuff when I solder on testpoints.<br>I could not get the MISO line data, and my LEDs stopped lighting up. I damaged the poor LED controller in the process.<\/p>\n\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;69e2bef93f296&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"69e2bef93f296\" class=\"wp-block-image size-large wp-duotone-unset-10 wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"268\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/led_driver_startup-1024x268.png\" alt=\"\" class=\"wp-image-1516\" srcset=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/led_driver_startup-1024x268.png 1024w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/led_driver_startup-300x79.png 300w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/led_driver_startup-768x201.png 768w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/led_driver_startup.png 1519w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\taria-label=\"Agrandir\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.imageButtonRight\"\n\t\t\tdata-wp-style--top=\"state.imageButtonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure>\n\n\n\n<p>In case you are wondering how this should be done properly, the answer is probably <a href=\"https:\/\/sensepeek.com\/pcbite-20\">probe holders<\/a>.<\/p>\n\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;69e2bef93f728&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"69e2bef93f728\" class=\"wp-block-image size-full wp-duotone-unset-11 wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"http:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/pcb_ite-4.png\" alt=\"\" class=\"wp-image-1639\"\/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\taria-label=\"Agrandir\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.imageButtonRight\"\n\t\t\tdata-wp-style--top=\"state.imageButtonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure>\n\n\n\n<p class=\"has-text-align-center has-small-font-size\" style=\"margin-top:0;margin-bottom:0\">Lab shopping list item #1270: Probe holders<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1.4 Software drafting<\/h3>\n\n\n\n<p>In order to make sure I had all the information I wanted before erasing all firmware from the board, I started drafting some software builds. I cloned the ZMK projects and started writing my device tree, which also forced me to complete my spreadsheet with some missing elements: the SPI lines for the flash, the USB lines, the testpoints\u2026 and I also started writing some documentation and creating branches for the project.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. To brick or not to brick<\/h2>\n\n\n\n<p>Once I got all the useful information I wanted from the factory firmware, time to move on to the one-way operation: flashing a new firmware on the board. Or brick everything.<\/p>\n\n\n\n<p>On STM32 chips, there are some read protections to prevent miscreants from reading the manufacturer\u2019s firmware. Still, most of the time it is authorized to recover read\/write access to the flash if you blank it first. This is controlled by the RDP option byte: \u00ab\u00a0AA\u00a0\u00bb means complete read\/write access, and \u00ab\u00a0BB\u00a0\u00bb means no read\/write access. Switching from BB to AA automatically resets the flash and send the manufacturer\u2019s firmware to oblivion.<\/p>\n\n\n\n<p>I opened my board with an ST-Link gizmo and the STM32CubeProgrammer software. It was detect and I could read the Option Bytes register. The board was in BB mode, so I switched to AA, thus destroying the existing firmware.<br>Then, I realized I could not write anything in the flash. <\/p>\n\n\n\n<p>Oooops, I bricked it again?<\/p>\n\n\n\n<p>After more reading and tutorial videos and playing with the option bytes, I managed to recover the read\/write access. I was missing 2 things:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>For this chip family, there are read\/write protection sets for memory ranges. In order to be disabled, one needs to write an end address before a start address, for instance start=0xFF and end=0x00.<\/li>\n\n\n\n<li>For those memory-range protections to be removed, one needs to set the \u00ab\u00a0PCROP_RDB\u00a0\u00bb bit to 1, and then to operate a transition from RDP BB to AA.<\/li>\n<\/ul>\n\n\n\n<p>I had to switch my STM32 back to BB mode, change all the ranges without missing a field, check the PCROP box, and then switch again to AA mode. After several attempts (forgot to invert the ranges\u2026 forgot to tick the box\u2026) I finally managed. Which means I can flash my new firmware, yay!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Writing a new firmware<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">3.1 Debugging the debug port<\/h3>\n\n\n\n<p>Of course, my draft firmware absolutely did not work at first try.<\/p>\n\n\n\n<p>This board does not have connectors or headers for debug UART or even a nice debug LED on GPIOs, so I had to use the testpoints.<br>The first test I did was using Zephyr samples\/basic\/led to blink what actually was a testpoint, and probe it to verify the STM32 was alive and switching levels on the GPIO every second.<\/p>\n\n\n\n<p>I tried to connect a GDB session but it failed, and I tried to debug my debugger&#8230; I just managed to fix a reset option in OpenOCD, which was good enough to get the board to flash with \u00ab\u00a0west flash\u00a0\u00bb instead of STM32CubeProgrammer. It was all I could do. Maybe a proper debug probe should be on the lab shopping list too?<\/p>\n\n\n\n<p>I tried shell logging via USB but it failed, it needed to be debugged too. But in order to debug my USB logging I needed another form of logging.<\/p>\n\n\n\n<p>I ended up soldering cables on testpoints again. I found some cool ones with the right pinmux to get me a debug UART. <br>I still don\u2019t like this, I had to be careful while moving cables around, and I almost tore the testpoints off, but it was still worth it.<\/p>\n\n\n\n<p>I began investigating the non-working USB, because USB is the only connection: it is the main feature, and it is also supposed to be my debug port.<br>Why it was not working:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Clock. There is a special clock called HSI_48 to be enabled just for USB.<\/li>\n\n\n\n<li>Pincontrol. Since there is only one USB port, I did not think of adding a pincontrol, some models do not require you to. But since you could also use PA11 and PA12 as vanilla GPIOs it means that yes, the pincontrol is required.<\/li>\n<\/ul>\n\n\n\n<p>After this, there were not so much more stuff left for me to get wrong.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&amp;clk_hsi48 {\n    status = \"okay\";\n};\n\n&amp;usb_fs_phy {\n    status = \"okay\";\n};\n\nzephyr_udc0: &amp;usb {\n    status = \"okay\";\n    pinctrl-0 = &lt;&amp;usb_dm_pa11 &amp;usb_dp_pa12&gt;;\n    pinctrl-names = \"default\";\n\n    usbuart: cdc_acm_uart0 {  \n        compatible = \"zephyr,cdc-acm-uart\";\n    };\n};<\/code><\/pre>\n\n\n\n<p>After being corner-cased by the subsys\/usb\/testusb sample, which does not work on my platform in Zephyr version 4.2.99, I realized the other samples and the other versions were all working, and I ended up having a nice shell and log over USB.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3.2 Debugging the keyboard<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">3.2.1 First try<\/h4>\n\n\n\n<p>A first oscilloscope probing allowed me to find a first very obvious issue: my keystroke-probing signal was output on the column lines, which are supposed to be the inputs, and not on the row lines. Easy to fix by changing \u00ab\u00a0col2row\u00a0\u00bb to \u00ab\u00a0row2col\u00a0\u00bb in the device tree.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">3.2.2 Second try<\/h4>\n\n\n\n<p>A second issue arose. Since the signal is amplified by an unknown gizmo, there is some filtering happening, and the 20us pulse provided by the ZMK driver gets eaten out by the hardware filtering. There is no signal left on the key MOS pins. It worked with the factory firmware because the pulse was 110us wide. So I need to increase the pulse duration.<\/p>\n\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;69e2bef940113&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"69e2bef940113\" class=\"wp-block-image size-large wp-duotone-unset-12 wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"729\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/graphs_pulses-1024x729.png\" alt=\"\" class=\"wp-image-1682\" srcset=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/graphs_pulses-1024x729.png 1024w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/graphs_pulses-300x213.png 300w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/graphs_pulses-768x546.png 768w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/graphs_pulses.png 1255w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\taria-label=\"Agrandir\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.imageButtonRight\"\n\t\t\tdata-wp-style--top=\"state.imageButtonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure>\n\n\n\n<p class=\"has-text-align-center has-small-font-size\" style=\"margin-top:0;margin-bottom:0\">Comparing pulse sizes: why it does not works at 20us<\/p>\n\n\n\n<p>My first measurements were done with a hack:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>k_usleep(90); \/\/ Increase to 110 us pulse<\/code><\/pre>\n\n\n\n<p>Then I asked about it on the ZMK Discord forum to get the right way to change the pulse. There is an option to add more time, in ticks (which are microseconds in my case) to the existing pulse, so 90 makes a 20us + 90us pulse:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CONFIG_ZMK_KSCAN_MATRIX_WAIT_BEFORE_INPUTS=90<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">3.2.3 Third try<\/h4>\n\n\n\n<p>Once this was solved, I ran my firmware, pressed a key, and it crashed. Started again, pressed another key, crash. Started yet again, waiting for a while, played with the shell, it was OK, pressed a key, crash. Disabled all the nodes and drivers I could from the device tree. Verified I was in polling mode and not in interrupt mode. Verified no other GPIO was colliding with my GPIO pincontrols. Disabled USB-UART CDC ACM mode and tried again with my testpoint serial port. Crash, crash, crash.<\/p>\n\n\n\n<p>I tried to attach a GDB session to OpenOCD, again, and considered soldering a push-button to create a new reset button game like I do with all my STM32, where you have to find the exact moment to release the button otherwise GDB does not connect. Made a mental note to try to find a workaround for this STM32 reset button cult I have.<\/p>\n\n\n\n<p>And then it hit me. I forgot Zephyr #1 rule, as I do each time I have this bug, I have to get it tattooed or get it on a poster or something:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Mysterious crash? Add more stack!<\/p>\n<\/blockquote>\n\n\n\n<p>Actually it was not exactly as simple as that, I already added some more bytes to the application main stack, but the stack that provoked the crash here was not the usual stack, it was another stack:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048<\/code><\/pre>\n\n\n\n<p>There! No more crashes. I finally managed to get some keys working, but not all keys, and they were <em>definitely not<\/em> typing the right letter.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">3.2.4 Fourth try<\/h4>\n\n\n\n<p>First, the missing keys: I had a whole row missing, and could not understand why. I probed the signals, they looked fine. But I got tired and needed the win so I just added a fake row. Row number 0 is now an unused testpoint pin, and my actual keyboard rows start at one. It works.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">3.2.5 Fifth try<\/h4>\n\n\n\n<p>One final detail: keys are working but they need to output the right keycode value. There are quite a bit of arrays to provide to ZMK, which I converted from my spreadsheet to my device tree thanks to my VIM search and replace expertise.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full wp-duotone-unset-13\"><img loading=\"lazy\" decoding=\"async\" width=\"708\" height=\"270\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/ugly_spreadsheet.png\" alt=\"\" class=\"wp-image-1694\" srcset=\"https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/ugly_spreadsheet.png 708w, https:\/\/redero.fr\/wp-content\/uploads\/2025\/09\/ugly_spreadsheet-300x114.png 300w\" sizes=\"auto, (max-width: 708px) 100vw, 708px\" \/><\/figure>\n\n\n\n<p class=\"has-text-align-center has-small-font-size\" style=\"margin-top:0;margin-bottom:0\">An glimpse into the spreadsheet from Hell<\/p>\n\n\n\n<p>First you have a zmk,matrix-transform object that maps the hardware coordinates of the key to an index of a list that should match your mechanical layout. Since the list is 1D and not 2D you have to get a precise idea of which key belongs to which line. Also, I have a weird ESC key that is on the first line physically on the layout, but is on the 5th line on the electrical matrix: this is where it gets fixed. Then this new \u00ab\u00a0fake 2D\u00a0\u00bb array has to match a keymap file, of type zmk,keymap, with the same indexes, which describes which keyboard codes are sent.<\/p>\n\n\n\n<p>I am French and this keyboard is AZERTY, so I thought the keyboard codes would be AZERTY, but no. After I ran evtest on my own computer keyboard, I realized even the AZERTY keyboards do not send AZERTY keycodes: they send QWERTY and it is translated to AZERTY by the system. My \u00ab\u00a0M\u00a0\u00bb key is actually sending the SEMICOLON keycode. So I rewrote the keyboard codes in QWERTY, based on my own keyboard and a picture I have of the US version of the Apex Pro Mini, and got most of the matrix right. Except for my \u00ab\u00a0&gt;&lt;\u00a0\u00bb key, which is called KEY_102ND on Linux, as reported by evtest: it does not exist on QWERTY keyboards. The ZMK name for it is: NUBS (Non-US BackSlash), and Zephyr calls it INPUT_KEY_HOME as per input-event-codes.h.<\/p>\n\n\n\n<p>The ENTER key is 2 rows high, I had to choose a reference row for it. And choose the same in every file, because since the array is 1D, one mistake for a key position offsets all the other following keys. But a wrong keyboard matrix is great fun to debug.<\/p>\n\n\n\n<p>So finally, the keyboard works with ZMK powering it. You can find the ZMK source code <a href=\"https:\/\/github.com\/everedero\/zmk\/tree\/apex_pro_mini\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3.3 Additional fun with the SPI flash<\/h3>\n\n\n\n<p>I have an SPI flash, so let\u2019s try to talk to it with the samples\/drivers\/spi-flash sample code.<br>The sample could not initialize my flash. A quick multimeter check gave me a chip select pin staying at 3V3, so my chip was never selected\u2026 As an experiment I forced it to zero by declaring it as an LED and using gpio_pin_set_dt. After forcing my GPIO it worked. Why oh why do the CS pins never work with me?<\/p>\n\n\n\n<p>I think I finally figured out why. CS pins can be controlled by the SPI peripheral, which is why you get pin controls such as \u00ab\u00a0spi1_nss_pa15\u00a0\u00bb and \u00ab\u00a0spi2_nss_pb12\u00a0\u00bb, but in Zephyr the SPI drivers use them as vanilla GPIO, not invoking them through the SPI peripheral, which makes sense too. So if you get overzealous and invoke the CS pincontrols like I do, you are actually preventing Zephyr from using them as normal GPIOs. No pincontrols for CS, they just go in the cs-gpios field and the driver manages.<\/p>\n\n\n\n<p>Also, USB CDC ACM requires several Kconfig options to work, like CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT. There is a magic config invocation file to make it all work: boards\/common\/usb\/Kconfig.cdc_acm_serial.defconfig.<\/p>\n\n\n\n<p>So finally, my SPI flash works too!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Several years ago, I watched a video about installing OpenWRT on old routers, and its conclusion was: \u00ab\u00a0DIY electronics kits are cool, but the best thing is reusing hardware you already have\u00a0\u00bb. It really opened my mind. I have been meaning to achieve this kind of hacking and upcycling project for a long time, I am very happy about how it turned out!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I found a keyboard in trash and ended up cooking a new firmware for it.<\/p>\n","protected":false},"author":1,"featured_media":1723,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[49],"tags":[95,108,47],"class_list":["post-1510","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog_en","tag-hardware-en","tag-reverse","tag-zephyr-en"],"_links":{"self":[{"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/posts\/1510","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/redero.fr\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1510"}],"version-history":[{"count":150,"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/posts\/1510\/revisions"}],"predecessor-version":[{"id":1779,"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/posts\/1510\/revisions\/1779"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/media\/1723"}],"wp:attachment":[{"href":"https:\/\/redero.fr\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1510"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/redero.fr\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1510"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/redero.fr\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1510"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}