Skip to content

Flashing keyboard firmware

This page describes how a stock keyboard image is written to a half: the way NayaCore does it, and the way it was measured on real hardware in both directions (upgrade and downgrade), with timings, the two risk windows, how to verify the result, what a flash keeps, the failure modes, and what has and has not been tested. Flashing uses the bootloader's SMP serial recovery, not the configuration protocol. The thing to know first: every stock upload arms a permanent swap, so there is no trial run and no automatic way back.

You can damage a half. Read the safety notes first.

Everything here was measured on generation-A halves, flashing between 3.35.4 and 3.41.0 in both directions, on a Windows host (owner's board, 2026-09-20 to 2026-09-22). Nothing has been measured on a generation-B half, on other firmware versions, or with a power loss during a flash. Make first attempts on a donor board, not a daily driver. Module firmware is flashed differently: see Module firmware.

Correction: no trial boot, no automatic revert

On the Create the upload itself arms a permanent swap (every stock resource carries a pre-written trailer), and the bootloader refuses the image state write that could request a test swap (rc 8). A signed image that boots badly stays; the way back is another upload through serial recovery STATIC MEASURED34. This differs from naya-create-kb, which says a new image boots on trial and is reverted automatically if it fails, so that firmware experiments would be safe on a daily driver13. See There is no test mode.

At a glance

  • Send ee/10ae, find the port that answers SMP, upload the whole 663 552-byte resource to image: 2 in 512-byte chunks, wait out the silent swap, send os reset, confirm with fe/1002.
  • About 1 to 3.5 minutes of upload per half; about 100 s of silent swap; four to ten minutes for both halves.
  • Flash the left (central) half first. Halves on different firmware still type.
  • A flash keeps keymaps, LED maps, layers, module slots and Bluetooth bonds; it resets the live lighting.

What you need

  • The stock image that matches the half's side and flash generation. The four current images and how to tell them apart are on Images; all stock images are in create-legacy-firmware.
  • An SMP (mcumgr) client for the serial port. The framing and its two silent-failure pitfalls are on Bootloader.
  • The vendor's rules for its own updater (NayaFlow 1.25.1): no modules docked on either half; both halves switched on; two direct USB-C to USB-C cables (Y-cables "are known to cause issues" for updates and pairing); allow up to 3 minutes and wait for the LEDs to come back; update at most two halves at a time (connecting "four or more Create halves" may overload it) DOC8.
  • Manual v1.1.0: at first setup, connect by USB (3.0) and update the firmware with Naya Flow; if that fails, use a standalone USB 3.0 cable and update the halves one by one DOC9.
  • One program per port: quit NayaFlow (its NayaCore holds the ports) and any other tool first.

How it works

Keyboard firmware is flashed over MCUboot serial recovery with the SMP (mcumgr) image upload command. The configuration protocol is used only to send the half into its bootloader, with ee/10ae MEASURED STATIC (owner's board, six whole-slot writes, 2026-09-20 to 2026-09-22)2.

NayaCore's sequence (NayaCore 6.11.0): enumerate the serial ports; read the USB product id to derive side and flash generation; pick the matching embedded image, refusing an unknown generation; open the port; then MCUBootWorker::doStart uploads the whole resource to image: 2 (uploadImageToCreateSlot) and resets the half (restartDevice, SMP os reset). It never writes image state STATIC13.

The upload request's keys are image, off, len, sha and data; hash and confirm (the image state write) sit beside them in NayaCore's binary STATIC2.

On the device side, MCUboot's bs_upload erases the target slot on the first chunk, writes the bytes verbatim, does not parse sha, and accepts a len equal to the slot size DOC INFERRED53.

The measured sequence

Firmware write: can brick a half

Safety level HIGH. TESTED by us on both halves, 3.35.4 to 3.41.0 and back, generation A, 2026-09-20 (by hand) and 2026-09-22 (from OpenFlow's interface). Untested on generation B and on any other firmware. Do not unplug or power off a half at any point between step 5 and step 9.

The sequence as measured MEASURED4:

Step What you send What the half does Typical time What "stuck" looks like
1 ee/10ae MCU_BOOT_RESET, params 00, on the half's own port (left: aa 00 50 00 ee 03 10 ae 00 be 04) leaves the application; lights go out immediate nothing: the half is supposed to go dark
2 watch USB for the MCUboot PID (0x006F left, 0x00D3 right) re-enumerates with two CDC ports a few seconds no bootloader PID: the command did not reach the half
3 os echo on each of the two ports one port answers (data), the other swallows frames (log) seconds neither answers: see Bootloader
4 image state read reports each slot's hash; name the running image 4 to 69 s through OpenFlow's retries, reopening the port per request (right about 25 s, left about 65 s); about 20 s on the left with both ports held open (Bootloader) a long silence here is normal (cause open)
5 image upload, the whole 663 552-byte resource to image: 2, 512-byte chunks erases the secondary slot on the first chunk, then stores the chunks 57.6 to 77.7 s by hand; 124 to 207 s in the interface runs a pause after the first chunk is the erase
6 (nothing) the trailer in the last chunk arms a permanent swap at the last chunk
7 wait MCUboot swaps the slots and decrypts; the port throws or times out, then re-enumerates about 100 s silence is the swap; do not unplug
8 os reset (group 0, id 5) on the port that answers SMP; re-send every 30 s boots the new image about 8 s once it takes a dark half: the reset did not take, re-send it
9 fe/1002 on the half reports the new version the old version: see Verifying

Chunking and the upload request

NayaCore sends min(remaining, 512) bytes of data per request (uploadImageToSlot, x86_64 address 0x100115e95), so a 663 552-byte resource is 1 296 chunks. 512 bytes is the one chunk size this bootloader is known to take. The first request carries len (the total) and sha; later requests carry off. Follow the off the device echoes back: trusting a local counter risks a silently corrupt image after a dropped frame STATIC MEASURED3.

Timings

The first chunk blocks while the bootloader erases the whole 648 KiB secondary slot: about 6 s (left) and 17 s (right) by hand; in one interface run the first 5 % took 55 s, with one pause right after the erase. The erase is not finished when the first chunk is acknowledged, so a chunk shortly after it can block for more than 5 s MEASURED4.

By hand, the whole image took 57.6 s (left) and 77.7 s (right), 8.5 to 11.5 KB/s. Supervised interface runs took 124 to 207 s including a handful of stalls: the large left image had 11 stalls over 1 s, about 59 s in total; the right had 4, about 17 s. The median and 90th-percentile chunk time was 40 ms, and the steady rate 12.9 KB/s (each 5 % in 2.6 s). The chunk loop runs at wire speed MEASURED4.

After the last chunk the bootloader is silent for about 100 s while it carries out the swap; the port throws an error (on Windows ClearCommError failed) or times out, and the half re-enumerates MEASURED4.

os reset returns the half to its application in about 8 s once it takes. Confirming the new version (reboot plus version report) took about 122 s (left) and 72 to 74 s (right) in supervised runs, because a reset sent right after an upload often does not take and must be re-sent MEASURED4.

A whole two-half run, from backup to verdict, took about 10 minutes; supervised runs take four to ten minutes MEASURED (owner's board, 2026-09-22).

Phase Left half Right half
First chunk (slot erase), by hand about 6 s about 17 s
Whole upload, by hand 57.6 s 77.7 s
Whole upload, interface runs (with stalls; range over all runs) 124 to 207 s 124 to 207 s
Silent swap after the last chunk about 100 s about 100 s
os reset to the application about 8 s about 8 s
Version confirmed, interface runs about 122 s 72 to 74 s

Timeouts that work

These timeouts completed every measured flash; the shorter values each failed a flash that was in fact perfect MEASURED (owner's board, 2026-09-20 and 2026-09-22; OpenFlow's flasher)12:

Wait Value Why Earlier value that failed
first chunk 90 s the slot erase 5 s (the post-erase stall)
later chunks 20 s occasional multi-second stalls
after the last chunk 300 s the silent swap 90 s
reset re-send every 30 s, up to 300 s a reset right after an upload often does not take
both halves re-linked 90 s the central reboots when the peer returns
version confirm (within the above) 45 s

The two risk windows

  1. During the chunked upload (about 1 to 3.5 minutes): MCUboot's design leaves the primary slot untouched while the secondary is written, so an interruption is expected to leave the half as it was INFERRED5. Expected, not measured.
  2. During the silent swap after the last chunk (about 100 s): MCUboot is moving flash sectors. This is the one unverified window to treat as a brick risk INFERRED.

Do not unplug or power off a half in either window. Nobody we know of has tested a power loss in either one (open question).

The swap is armed before any host-side check can run (the trailer is inside the uploaded bytes). What refuses a corrupt or foreign image is MCUboot's own signature check before it swaps. Verify the running version afterwards either way STATIC INFERRED3 (Images).

Verifying the result

The swap can complete before the flasher reads back. On both halves the new image was in slot 0 and the image it replaced in slot 1 (left 479e89ba... over 036059b2..., right 2abb2695... over 959fbae1...), so a check of slot 1 alone "finds" the old image and calls a perfect flash corrupt. Recognize completion as: the new hash in slot 0 and the previously running hash in slot 1, when the two differ MEASURED4. Then read fe/1002 in the application.

Nothing about the flash is central-only: the right half's bootloader behaves like the left's MEASURED4.

Two halves

Flash one half at a time, the central (left) half first, so the board only passes through measured states (a newer central with an older peripheral) MEASURED INFERRED. Between the two flashes the unflashed half answers the handshake on its own port and then returns empty payloads, so do not read it there: read its version through the central's port at dst 0x51. When the second half comes back on matching firmware the halves re-link, and the central reboots through its bootloader (about 2 s off USB, lights out) MEASURED4 (Bootloader).

Halves on different firmware still type on both hands (left 3.41.0 with right 3.35.4). A mismatch breaks two things, and both resolve themselves once the halves match: the peripheral's LEDs go dark (the LED payload between the halves changed in 3.41), and the peripheral's own USB port returns empty payloads. The peripheral stays reachable through the central's port at dst 0x51 for the version read: the left port read the right half's version 3.35.4 while the right half's own port returned empty payloads MEASURED4. Relaying is partial, though: Bluetooth identity reads sent to 0x51 on the left port answer for the left half (Transport). OpenFlow never built an automatic retry through that route; the route itself is measured. NayaCore also refuses its pairing flow with "Devices have different firmware versions": a policy, not a dead link STATIC7.

Done on the owner's hardware: halves deliberately put on different firmware and then matched again MEASURED4.

Downgrades

Every stock image is signed by the key the bootloader trusts, so a downgrade is an ordinary flash (Images). Measured: the left half went from 3.41.0 to 3.35.4, whole resource, 59.8 s upload. Safety HIGH. TESTED (us, 2026-09-20) MEASURED.

Mind the configuration protocol afterwards: on 3.28.7 any configuration write that needs three CDC frames wedges the half until it is unplugged. That is a limit of the configuration protocol on that firmware, not of the flash MEASURED (owner's board, 3.28.7, 2026-09-19); see Recovery R8.

What a flash keeps

A firmware flash does not touch the data store. Keymap records (both banks), LED maps, layer identities, module configuration slots and Bluetooth bond tables were byte-identical before and after two slot erases, writes and swaps per half MEASURED4 (bonds; the rest measured on the owner's board, 2026-09-20 and 2026-09-22).

The keymap record format did not change between 3.35.4 and 3.41.0: 82 records times 3 layers, zero differing positions, and the layer identities were identical after two flashes MEASURED.

Pairing survives a flash. A pairing repair is a remedy for an already broken board, never a routine step after an update MEASURED4.

What a flash does not keep is the live lighting state. A half comes back from the bootloader white or a darker amber while its stored LED maps are byte-identical. Writing the layer list back unchanged to the left half (30/1002 with the bytes just read by 30/1001) returns both halves to their stored lighting in one frame. Brightness came back low after the 3.41.0 upgrade; OpenFlow's restore step re-sends the brightness ceiling (ed/1013 with the saved value, else 100: params 00 00 64). Whether the flash changed the stored ceiling or only the live level was not recorded. The owner has done this restore on hardware after flashes MEASURED STATIC (Recovery R5).

3.35.4 does not report the second binding bank (its layer read stops at position 0x51); 3.41.0 returns the whole bank, padded with NONE records. A double-tap key at 0x89 was on the board through an upgrade: invisible before, present after. Cross-version comparisons must treat "absent" as NONE MEASURED (owner's board, 2026-09-22).

The LED map reads differently across 3.35.4 and 3.41.0 (the payload changed in 3.41), so a cross-version LED difference is an advisory, not a failed flash MEASURED4.

Item After a firmware flash
Keymap records, both banks kept, byte-identical
LED maps kept, byte-identical (reads differ across 3.35.4 and 3.41.0)
Layer list and layer identities kept
Module configuration slots kept
Bluetooth bonds and the pairing between the halves kept
Live lighting not kept: rewrite the layer list
Brightness came back low after the 3.41.0 upgrade; re-send the ceiling

The same table for every other event (power cycles, resets, the vendor's repair buttons, 30/10ca) is on Flash layout.

Failure modes

Symptom Cause What to do
Stuck at 0 to 5 % the first chunk waits for the 648 KiB slot erase wait (90 s timeout)
Silent after 100 % MCUboot is swapping wait up to 300 s; never unplug
Half dark after the flash still in its bootloader: the reset did not take re-send os reset to the port that answers SMP (Recovery R1)
Slot 1 holds the old image the swap already happened check slot 0 for the new hash
Halves on different firmware mid-run expected until the second flash flash the other half
image state write refused (rc 8) not implemented on this bootloader nothing; do not depend on it
Log port says the primary image is not valid see Recovery R2 untested

There is no test mode

The bootloader answers an image state write with rc 8 (not supported). So the route "upload a bare image, verify it, then arm it through image state" cannot work on the Create, and an image already in the secondary slot cannot be booted by marking it: it must be uploaded again. NayaCore's testImage and confirmImage exist in its binary and are never called, for the same reason MEASURED34.

So the advice that firmware trials are safe on a daily driver, because the bootloader boots new images on trial and reverts automatically, does not hold here MEASURED STATIC (see the correction at the top of this page).

Never upload to image 0 or 1: that writes the primary slot directly. The first chunk erases the whole running image, and with no swap there is no copy to fall back on, so an interruption, or a decryption the bootloader does not perform, leaves the half with an invalid primary INFERRED. MCUboot can be built to decrypt an encrypted image uploaded straight to the primary slot (boot_handle_enc_fw, in upstream since 2023); whether the vendor's build includes that is unknown DOC OPEN6. Stock images go to image: 2, as NayaCore sends them. (A third party's failed rescue attempts had gone to the primary slot; that report is private and not cited.)

Custom or unsigned firmware cannot be flashed this way: the signature check refuses it STATIC (Images).

Client notes

Three client pitfalls together cost a 140-fold slowdown (1 h 50 min for an image NayaCore writes in about 2 minutes): a fixed read(256) for a 31-byte reply waited out the full timeout on every request; a line check that insisted on base64 = padding rejected complete replies; and reopening the port for every request added about 1 s. Reading what is available and holding one port open per transfer gives 36 ms round trips MEASURED (owner's board, 2026-09-20).

NayaCore tells the data port from the log port with a "data port connection test" whose content is unknown STATIC7. Trying os echo on both ports works.

OpenFlow's flasher

OpenFlow implements this procedure.

  • Interlocks before any write: the half is in recovery and answers image state; the running image is in the stock-image table (identified by hash); side and generation agree from both the USB PID and the running image's table entry; the chosen image is marked flashable; the caller passes an arming token equal to the hash the device just reported; a downgrade needs an explicit allow. Flashing ships switched off STATIC12.
  • It ships only a firmware catalog, fetches a cataloged image on demand (by default from create-legacy-firmware) and refuses the bytes unless they hash to the catalog value STATIC12.
  • Around the upload: a full backup first; an append-only log flushed per line; the central first; the device lock held for the whole run; brightness and lighting restored afterwards; a comparison against the backup (bindings, bonds and versions strict; LED and second-bank differences across a version change as advisories) STATIC12.

The stock updater (NayaFlow)

NayaFlow hazards: in its Hardware Manager, Ctrl+D (Cmd+D on macOS) starts update_create_fw on both halves without a confirmation dialog when no warnings are showing. update_create_fw with two empty frames flashes the bundled image, and with a file path flashes that file; NayaCore requires 0 or 2 target devices ("need 0 or 2 HWIDs") STATIC78.

NayaCore 5.8.1 (NayaFlow 1.15.0) made keyboard flashing "up to 2.5x faster"; NayaCore 6.11.0 added generation-B ("64-bit") updates DOC1011.

Tested and untested

Not yet done by anyone we know of: a generation-B flash; a flash onto a half whose primary image is invalid (Recovery R2); a power loss during either risk window MEASURED (absence; status 2026-09-23). Done: both halves in both directions between 3.35.4 and 3.41.0, a deliberate mismatch and re-match, the lighting restore afterwards, and module-bundle updates on the left half (3.41.0, from 2026-09-23; see Module firmware).

Open questions

  • OPEN NayaCore's own keyboard-image upload timing; only our flasher was timed uploading keyboard images (details).
  • OPEN Why the bootloader takes so long to name the running image after entry: about 20 s on the left half with both ports held open, 4 to 69 s when the port is reopened for each request (details).
  • OPEN How a generation-B half behaves, including image slot info (details).
  • OPEN What a power loss does in either window (details).
  • OPEN Whether the Create application would confirm a test swap by itself; moot while none can be requested.

Sources


  1. create-legacy-firmware, FLASHING-PROCEDURE.md, "The current procedure (v1.25.1)". ↩

  2. create-legacy-firmware, FLASHING-PROCEDURE.md, "Slot ids, vendor-exact". ↩↩

  3. create-legacy-firmware, FLASHING-PROCEDURE.md, "The resource is a whole slot, trailer included" (flow corrected in commit 6ef80e2: upload the whole resource, then reset; no mark step). ↩↩↩↩↩↩

  4. create-legacy-firmware, FLASHING-PROCEDURE.md, "Measured on hardware, both halves (2026-09-20)" (commit cdd897c). ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  5. MCUboot, boot_serial.c (bs_upload) and the image encryption design (images are decrypted while being swapped from the secondary slot). ↩↩

  6. MCUboot at commit 439930aee115 (2024-10-25): boot/boot_serial/src/boot_serial.c (a completed upload to the primary slot calls boot_handle_enc_fw when image encryption is built in) and boot_serial_encryption.c. ↩

  7. NayaFlow 1.25.1, NayaCore 6.11.0 strings (pairing refusal, "data port connection test", "need 0 or 2 HWIDs"). ↩↩↩

  8. NayaFlow 1.25.1, renderer strings (Hardware Manager update texts and the Ctrl/Cmd+D shortcut). ↩↩

  9. Naya Create User Manual v1.1.0, p3; see Manuals. ↩

  10. create-legacy-firmware, CHANGELOG.md L340 (NayaCore 5.8.1). ↩

  11. create-legacy-firmware, CHANGELOG.md L113 (NayaCore 6.11.0). ↩

  12. OpenFlow: its flasher and firmware catalog. ↩↩↩↩

  13. naya-create-kb, firmware/signing. ↩