Hacking the NuPhy Air60 keyboard: part 3
It's been a long time coming... so this one's going to be a bigger one.
This is another update on the long-running project of putting my own firmware on a NuPhy Air60. If you're interested in the previous parts, they're here: part 1 and part 2.
So what happened since? Quite a lot, honestly.
- Timeline so far
- SMK grew up
- sinowealth-kb-tool becomes sinowisp
- From SinoLink to sinodude
- smk.carlossless.io: flashing from a browser
- A new base for the keyboard, with the debug bus on the outside
- Why now, though?
- What's next
Timeline so far§
- April 2023 - sinowealth-kb-tool gets its first release.
- December 2023 - USB lands on top of the matrix scanning from October, so SMK works as an actual USB keyboard for the first time.
- 2024 into 2025 - a steady stream of new devices gets added to sinowealth-kb-tool, and it hits 1.0.0 in May 2025. SMK picks up wireless (over the BK3632) and NKRO around the turn of the year.
- January 2026 - reverse engineering the remaining bits of the SinoLink, enough to build sinodude, my own programmer around an Arduino Nano.
- June 2026 - hidra gets written, the browser flasher at smk.carlossless.io quietly goes live, and I start designing a new base for the keyboard in FreeCAD.
- July 2026 - everything gets wired together. sinowealth-kb-tool becomes sinowisp and drops hidapi for hidra.
- August 2026 - the LED effect engine, sleep and settings land in SMK, along with the split between the platform layer and keyboard policy.
- September 2026 - ports onto more keyboards and more MCUs (the SH68F881 and SH68F89, a Genesis Thor 300 and a CIY X77), and, thanks to the multiple backends, sinowisp gets to the point where it can talk to every documented ISP bootloader on Windows, macOS and Linux.
Anyway, on to the actual stuff...
SMK grew up§
Part 2 of this series ended with a minimal firmware that booted, jumped back into the bootloader without bricking itself, and not a lot else. That scratchpad (it was called sh68f90a-experiments back then) is now SMK, a working keyboard firmware, somewhat stable even. Stable enough that the Air60 running it is one of the two keyboards I use day to day.
It didn't all happen at once. Matrix scanning and USB came first, which is the point where it's a keyboard you can actually type on. A port to an E-YOOSO Z11 came next, and that was what forced the Air60-specific assumptions out of the way, taking the first steps into making this a more general keyboard firmware. Then media keys, NKRO, the LED effect engine, sleep, settings persistence, and fixing a pile of nasty timing bugs (USB-enumeration freezes, the LED scan and matrix scan fighting over the same pins), along with a bunch of cases where the stack would overflow or an interrupt would write into the wrong page. The 8051 isn't a forgiving architecture: the stack shares its 256 bytes of internal RAM with everything else, and nothing anywhere will fault on you, so mistakes like that don't crash, they just quietly corrupt something you notice a few keystrokes later.
Wireless was its own thing. The Air60 talks 2.4G and Bluetooth through a Beken BK3632, and the protocol between the MCU and that radio wasn't documented anywhere, so reproducing it in SMK meant reverse engineering the stock firmware and sitting a logic analyzer on the bus between the two to watch what they actually said to each other. That took a while to get right, but it works now, and it works well enough that I don't think about it anymore.
SMK isn't tied to the Air60 anymore either, which is what let it spread onto the Z11, a Genesis Thor 300 and a CIY X77, across a few different Sinowealth parts. Every one of those cheap BYKxxx and BYxxxx labelled boards is a potential target.
Testing all of that is the hard part, which is also why I've been holding off on accepting contributions so far. I don't own most of these keyboards, and with the amount of refactoring I've been doing, I'd have no way to tell whether a board I can't plug in still works afterwards. I have been experimenting with testing some of the more basic things in a modified build of ucsim, so I think I might be able to start accepting contributions soon.
I should also mention that I started SMK as sort of a QMK clone for 8051, borrowing its keycodes and the general shape of things. But I never got much actual parity with QMK, and since then I've largely diverged from it anyway, so compatibility with QMK isn't the aim anymore.
sinowealth-kb-tool becomes sinowisp§
The little CLI from part 1 ended up carrying this whole project. Over time it picked up support for a lot more keyboards than I expected: somewhere around fifty of them now, plus a couple of mice.
Most of those entries didn't come from keyboards I own. Once the tool was out there, people started showing up with their own boards, filing device reports and dumps, and sometimes pull requests. I was surprised how common a platform this is for relatively inexpensive keyboards.
For a long time, it talked to keyboards through hidapi, a C library. It works and it's on every platform, but having a C dependency in the mix got particularly annoying whenever I wanted to cross compile (might just be me). On top of that, I wanted something that could talk WebHID, and something that could give me several different backends on the same platform, so I could switch between them depending on what state the keyboard was in.
So I wrote my own cross-platform HID library in Rust.
hidra talks to the same platform APIs hidapi does: hidraw on Linux, hid.dll and SetupAPI on Windows, and IOKit's IOHIDManager on macOS. On top of those it adds WebHID, so the same code runs in a browser, which is the whole reason the browser flasher later in this post exists at all. It's all one async API.
The part that ended up mattering most, though, is that it can carry several backends at once and switch between them at runtime. A keyboard shows up as a perfectly normal HID device, but the moment you send it into ISP mode its descriptors change, potentially into something the OS doesn't consider a valid HID device at all, so the native HID stack can't talk to it anymore. To get through you have to drop down to raw USB, which hidra does through nusb (a pure-Rust USB library, and the thing that made writing hidra feasible in the first place). So it carries both and picks: HID while the OS still sees a HID device, raw USB once ISP mode has left it undetectable as one.
That fixed some long-standing annoyances. Before, certain devices in ISP mode just couldn't be reached at all on some platforms, macOS especially.
The swap came with a restructure. The device-poking logic (read, write, erase, verify, all the bootloader quirks) now lives in a reusable library, with the CLI sitting on top as a thin shell. Pulling those apart, on top of hidra, is what lets the library build with no CLI dependencies at all, so it can be compiled for the web.
At that point it wasn't really a kb-tool anymore, which was a good excuse to drop a working title I'd never liked: it's now sinowisp.
From SinoLink to sinodude§
Time to talk about bricking again...
Part 2 covered how you brick one of these: flash firmware that can't jump back into the bootloader and the USB path is gone, leaving a programmer wired straight to the chip as the only way back in. When you're iterating on firmware, which involves writing a lot of broken firmware, you end up doing it more than you would expect.
That's why I bought the official option back in part 1: a Sinowealth SinoLink programmer, the probe the vendor's own ProWriter software drives. It works! But it's a proprietary black box with a Windows-only host tool, and I really don't like having one of those in the middle of an otherwise fully open project.
So, naturally, I took it apart, and spent quite a while with its firmware in Ghidra.
The bit I actually cared about is how it programs the 8051 parts (Sinowealth makes ARM ones too). It does that over ICP, on the four JTAG lines. The annoying part is that these programming headers don't expose a reset line, so the only way into ICP is to switch the target's power off and back on, and start talking to it at the right moment. The read side of that was already mapped out by gashtaan in his sinowealth-8051-dumper, so what I was after was the rest of it: page-write, mass-erase, the custom regions, and managing the security bits. And once I'd mapped that out, the obvious thought: if I understand the wire protocol, I don't need the dongle at all.
So I released sinodude: an open-source programmer that runs on a plain Arduino Nano (though it would take little to get it onto any compatible AVR) and can read and write most of the Sinowealth 8051 chips.
The Nano wasn't a random pick. The SinoLink is relatively expensive and, depending on where you are, hard to source, while a Nano is a couple of euros and you can get one anywhere, and anyone willing to crack open their keyboard has probably got one in a drawer already. gashtaan's dumper already runs on one too (it's the exact setup that I started with back in part 1), so there was a well-trodden path onto that hardware.
It also just made my life a lot better. I really disliked being stuck in the Windows-only ProWriter, and reverse-engineering how ProWriter itself talked to the SinoLink turned out to be harder than I expected. (I got a semi-working version of that going, and maybe I'll finish it off one day and add SinoLink support back into sinodude. We'll see.) Being able to run this programmer from a CLI, on Linux and macOS, was worth all of it.
The circuit is minimal: the four JTAG lines, ground, and one more pin driving a MOSFET that gates power to the target, so the programmer can time the power cycle these chips need itself. What it doesn't have is the circuitry the SinoLink puts around all this: a 3V3 supply alongside the 5V one, level translation between the probe and the target, and draining circuitry on the target's rails, without which the power cycle isn't always consistent on boards with more capacitance. But it's stable enough for me.
On the host side it's the usual read / write / erase / verify, with a database of about 370 Sinowealth parts behind it, so it can read the JTAG ID off a target and suggest which parts match it, and with support for the EEPROM and all the custom regions these parts have (customer ID, option bytes, the security key, and so on).
One thing I haven't touched yet: the SinoLink can also do on-chip debugging (OCD) on these 8051 targets, not just flashing. That's something I'd like to reverse engineer some day. What I don't know yet is how hard it'd be to wire that up to something standard like probe-rs or OpenOCD, or whether these parts are too far off the beaten path for either to care. A problem for future me.
smk.carlossless.io: flashing from a browser§
This was a fun detour.
Remember how I split the device logic into a sinowisp library, and wrote hidra so it could target WebHID? This is what that was all for. smk.carlossless.io is a browser-based flasher for these keyboards. You plug in a HID device with one of these ISP bootloaders, click a button, and read or write its firmware straight from a web page. No CLI, no install, no cables beyond the USB one.
Under the hood it's exactly the pieces from above: sinowisp compiled to WebAssembly, talking to the keyboard over WebHID through hidra.
It works largely the same as sinowisp, but:
- Chromium only. Firefox still doesn't ship WebHID, so it won't work there.
- It doesn't work on Linux. This one annoys me, given Linux is the platform I primarily use. The ISP bootloader's HID interface has no endpoints, so usbhid never binds it, and without a hidraw node there's nothing for WebHID to grab. The obvious fallback would be WebUSB, except Chrome refuses to hand over any interface that claims to be HID. So the device can't be reached over HID because the kernel won't treat it as one, and can't be reached over USB because Chrome insists that it is one.
A new base for the keyboard, with the debug bus on the outside§
Which brings me back to the keyboard itself.
Getting this far meant JTAG leads soldered onto the Air60 (and a few other keyboards), and a whole logic-analyzer harness onto the bit-banged SPI bus to the BK3632. Doing that off flying leads on a half-open keyboard is as annoying as it sounds, especially when you want to be pressing buttons on the other side of it.
All that opening and closing had also wrecked the stock base, with a few of its standoffs disintegrating from being screwed and unscrewed so many times (it wasn't that many times...). So I fixed both at once.
First, a combined adapter PCB that lives inside the keyboard permanently, so all that access stays soldered down and I don't have to redo it every time.
Then a replacement base designed around it in FreeCAD, with the mounting, standoffs and cutouts worked out against the real PCB, and split into left and right halves so it fits on my printer.
The adapter breaks out:
- the MCU's JTAG - TCK / TDI / TDO / TMS, plus +5V and GND
- the MCU-to-radio bus - the SPI lines MOSI / MISO / SCK / CS and the MOT and ACK handshake lines, plus +3V3 and GND
All of it comes out to headers you can reach with the keyboard fully assembled and running.
Here's the base (drag to spin it):
Why now, though?§
A big reason I came back to this project at all is that LLM agents made the leftover parts finishable in the relatively little free time I have these days. What was left wasn't hard so much as tedious, and that's exactly the kind of work they're good at.
Reverse engineering is the obvious example, the SinoLink's firmware above as much as the keyboard's own stock firmware. A lot of it is just grinding: stepping through the disassembly, working out what a routine does, annotating it, and moving on to the next one. That's the part it's great at, and having it do that groundwork with me meant an hour in the evening was enough to get somewhere, instead of just enough to remember where I'd left off.
What's next§
"I don't like the stock software" turned into a bootloader flasher, a keyboard firmware, an in-circuit programmer for when that bootloader can't save you, a cross-platform HID library, the same flasher again in a browser, and a printed base that hands you the keyboard's own debug bus on a header. This is probably the deepest and most branching rabbit hole I've been down, and also one of the most rewarding.
There's still plenty I want to do:
- An open-source ISP bootloader. The stock one is what bricks you, and it's the reason the browser flasher can't touch Linux. I'd like to replace it with one that presents itself as a proper HID device, which would kill the Linux limitation, and one that's actually recoverable: hold a key combination while plugging the keyboard in and you're in the bootloader, no programmer needed.
- An open-hardware replacement for the SinoLink. The Arduino Nano is the only programmer sinodude supports right now, and it's bare-bones. I want a proper little board around an RP2xxx, with the 5V / 3V3 muxing, level shifting and draining circuitry it's missing, and with a lot of the bit-banging living in the PIO instead of being timed by hand. And additionally, one day, hopefully, sinodude talking to a SinoLink directly as well.
- A lot more SMK. More refactoring, more boards, and (fingers crossed) more actual features.
If you're interested in any of this, SMK is on GitHub and there's a small community on Discord if you want to come say hi.
Many thanks again to @gashtaan for releasing his dumper, which enabled me to take the first steps in this project, and to @swiftgeek for his reverse engineering work around these chips!
See you in the next part!