Clicky — Keyboard Controller
Built on the AVR128DB64 — the same controller as Sticky — Clicky scans the keyboard, decodes each press to UTF-8 on-chip, and reports it to DeMon over UART, available long before USB is up. It also drives the per-key NeoPixels, handles the 8 status NeoPixels and the buzzer on DeMon's behalf, and (on Ant64) reads a jog dial. It also manages the power button and power-off, and on Ant64 hosts the NFC reader on its own SPI bus. The same Clicky firmware binary runs on Ant64 and eZX Spectrum; a board-strap pin selects the matrix-scan mode at boot. Fully reprogrammable over UPDI (by Pulse, under DeMon's orchestration) at any time.
Overview
| Feature | Detail |
|---|---|
| Chip | AVR128DB64 @ 24 MHz (same part as Sticky) |
| Supply | 5 V throughout — the chip runs at 5 V, so the matrix, WS2812 chain, RGB power light and buzzer are all driven directly. Level shifters are used only on the links to 3.3 V peers (see Interface Levels) |
| Firmware | Custom Clicky firmware (no QMK) — one binary, both products, branches on a board-strap pin at boot |
| Comms | UART2 ↔ DeMon — bidirectional (DeMon GP14 TX / GP15 RX) |
| Programming | UPDI via Pulse (serialUPDI over UART4) — update clicky |
Clicky replaces the earlier RP2350A / QMK controller. With that change there is no RP2350 anywhere in the Ant64.
Same Firmware, Two Products
Clicky ships one firmware binary that runs identically on the Ant64 and the eZX Spectrum. This mirrors how Sticky already works — the 4 joypad ports are wired the same on both products, so one Sticky firmware covers both. Clicky now has the same property.
The selector is a single GPIO — KPRODUCT on PF2 — read once at boot:
- Tied to 5 V on one product and GND on the other (polarity is firmware-defined; an internal weak pull-up means a floating pin during development falls back to a default mode)
- Read once during
setup()to choose the matrix-scan code path - Never read again — products don't change identity at runtime
KPRODUCT is now the machine's only board-type strap
It used to be one of two: demon also read the same fact from PB7 on the MCP23017. That is gone — KPRODUCT on PF2 is the single source, and AntOS asks Clicky for it over the I²C status interface at 0x28.
That is the right consolidation, and it makes the strap simpler rather than harder. A net shared between a 5 V AVR and a 3.3 V expander would have worked in neither direction — 3.3 V is below Clicky's ≈3.5 V V<sub>IH</sub>, and 5 V exceeds the MCP23017's VDD + 0.6 V absolute maximum — so a single reader means a plain tie to 5 V or GND with no translation at all.
Two things follow from being the only source:
- The polarity is now a contract, not a firmware choice. AntOS depends on it, so it is pinned here: 0 = eZX Spectrum, 1 = Ant64, matching the pinout. It is no longer free to be inverted for convenience.
- DeMon should cache it, because Clicky is not always answering. The expander was a dumb part that always responded; Clicky is a microcontroller running firmware, and during
update clicky— or after a bad flash — it answers nothing. A recovery boot that cannot tell an eZX from an Ant64 does not know its port map, panel presence or drive letters. So AntOS writes the value into a byte of the MCP79410's battery-backed SRAM on first successful read and falls back to that cache when Clicky is silent. Board type is immutable for the life of the machine, so the cache can never go stale.
Timing is not a concern in the other direction: Clicky is up in milliseconds on the switched 5 V rail and Linux takes on the order of a second, so the answer is always ready before AntOS asks. A read that somehow arrives early should retry rather than fail.
What the strap has to gate on the LED chain
The NeoPixel chain length differs by product, so the strap picks the frame size as well as the matrix: Clicky composes one contiguous frame of 8 + N LEDs, and sending too few leaves trailing pixels holding stale colour.
Two consequences worth stating. The eZX may have no per-key pixels at all — a Sinclair membrane keyboard has none, and most Spectrum Next-compatible boards do not either — in which case its chain is the 8 status LEDs and nothing else. That drops the power budget from the Ant64's ~5 A worst case to a few hundred milliamps, and it makes the status-LEDs-read-first property total rather than merely convenient.
And because a WS2812 chain cannot be probed for its length, the strap can only select a default. Where an eZX keyboard does have lighting, the real count has to come from configuration — which the existing model already covers, since DeMon is the authority and store and pushes config down once the UART link is up.
What branches on the strap:
- Matrix-scan loop — Ant64 scans the 75% mechanical layout and reads the jog-dial rotary encoder; eZX scans the 8×7 matrix via the Sinclair column + row ribbons and the Spectrum Next keyboard extender, with no encoder
- Default keymap and lighting profile — different physical layouts need different startup defaults
What does not branch — identical bytes executing on both products:
- UTF-8 decode and event composition
- All QMK-equivalent keymap logic — layers, Mod-Tap, Tap Dance, Combos, Leader, Macros, Caps Word, Key Overrides, Repeat / Alt-Repeat, Autocorrect, Grave Escape, Space Cadet, Swap Hands, Key Lock, Bootmagic / Command, configurable debounce
- UART protocol to DeMon (event message format, intent message format)
- Configuration save/load model (Clicky as engine, DeMon as authority and store — see below)
- WS2812 chain driver (status LEDs + per-key NeoPixels)
- Buzzer tone generation
- RGB power-light driver
- UPDI update path —
update clickyships one image; the strap reading on the target board decides which mode it runs in
One codebase, one CI pipeline, one test matrix, one set of features to track.
What Clicky Does
- Scans the keyboard matrix with Schottky-diode row/column sampling for fast, ghost-free reads. Matrix layout depends on the host product — see Keyboards below
- Decodes row/column → UTF-8 on-chip. Each key event sent to DeMon carries the event type, the raw row/column, and the decoded UTF-8 codepoint — so DeMon receives ready-to-use characters without a host-side keymap
- Reads a jog dial with push button (Ant64 only — the eZX has no rotary encoder), decoded entirely in hardware and never polled — see Jog Dial below
- Drives the per-key NeoPixels — one WS2812 under each key — and the 8 status NeoPixels on DeMon's status panel, all on the same WS2812 chain. See NeoPixel Chain below
- Drives the buzzer on DeMon's behalf. DeMon sends tone-and-duration intent over I²C and Clicky handles the timing, so DeMon never has to. See Buzzer below
- Drives the RGB power light — the case's single 4-pin RGB indicator, independent of the WS2812 chain so it works even when the chain is unpowered or mid-refresh. See RGB Power Light below
- Manages the power button and power-off. Clicky senses the soft power switch, reports presses to DeMon for graceful shutdown, and drives the power-off line — all on the always-alive 5 V controller. The ~10 s force-off is hardware, not Clicky. See Power Button & Power Control below
- Owns the NFC reader (Ant64). The PN532 sits on Clicky's SPI1 (master), reset and IRQ on Clicky. Clicky handles the whole low-level stack — card types, NDEF, validation — and hands DeMon a clean, validated card record over I²C, off DeMon's housekeeping bus. See NFC Reader below
- Comes alive well before USB enumerates, so DeMon can read the built-in keyboard during early boot, recovery, and configuration — long before a USB HID stack is up. External USB keyboards remain supported through DeMon's USB host port
Keyboards
Ant64 — 75% Custom Mechanical
The production Ant64 keyboard is a 75% custom mechanical design with per-key RGB (NeoPixel) lighting, a rotary jog dial with button, and — where a numeric keypad would normally sit — a vertical TFT touch screen (its display reset and I²C touch-controller reset are on DeMon's MCP23017, PA4 / PA5). Matrix scanning uses Schottky diodes for fast row-to-column sampling. Its layout, symbol layers, colour palette, and emulation-control keys (Select, Option, Start, the dial) are documented in keyboard — Clicky scans that matrix and decodes each press to UTF-8. (During development, before this board exists, the same layout runs on a USB AK820; Clicky is a later swap behind the same key meanings.)
The prototype uses an off-the-shelf AJAZZ AK820 — wired, 75% layout, 82 keys (83 including the rotary-knob button), per-key RGB, and a rotary control — connected to DeMon over USB; the production board moves the controller to Clicky.
Custom prototype key caps: https://yuzukeycaps.com/c/e47a4a66-6aed-4409-a54a-dcedb0a7a3aa
eZX Spectrum — Sinclair Ribbon + Spectrum Next Extender
The eZX Spectrum's keyboard interface follows the established Spectrum-community standards rather than ad-hoc pin headers, so existing Sinclair and Spectrum Next keyboards plug in directly with no adapter hardware:
| Connector | Pins | Purpose |
|---|---|---|
| Sinclair column ribbon | 8 | Standard Sinclair Spectrum column connector — 8 column return lines |
| Sinclair row ribbon | 5 | Standard Sinclair Spectrum row connector — 5 row drive lines (rows 0–4) |
| Spectrum Next keyboard extender | 2 | Additional 2 row drive lines (rows 5–6), following the Spectrum Next's keyboard extender pin standard |
Two keyboard generations are supported natively:
- An original Sinclair Spectrum membrane keyboard (the 48K rubber-key membrane or any aftermarket clone) plugs into the Sinclair column + row ribbon connectors. The Next extender stays empty; rows 5–6 are never driven. Get the original 40-key 5×8 Speccy layout
- A modern Spectrum Next-compatible 56-key keyboard uses all three connectors via the Next extender for the additional 2 rows. Get the extended 56-key 7×8 layout
Clicky autodetects which keyboard is connected by reading the row-drive lines at boot: if rows 5–6 produce no electrical response (extender unpopulated), Clicky assumes a 5-row Speccy keyboard and applies the 40-key default keymap. If rows 5–6 respond, the full 56-key matrix is scanned with the modern default keymap. Mid-session swap is unsupported — users plug their keyboard in before powering up.
The eZX keyboard has no rotary encoder — Spectrum-lineage keyboards have no integrated rotary control, and there's no separate jog-dial input device on the eZX (the Ant64's dedicated jog-dial controller, Cranky, is not present on the eZX). The keyboard handles all menu navigation.
Jog Dial
(Ant64 only.) The rotary jog dial is decoded entirely in hardware — Clicky's CPU never sees an edge, never takes an interrupt, and never polls. The dial keeps counting correctly through a full WS2812 refresh, a UART burst, or a matrix scan, because nothing in the decode path touches the core.
The chain is four peripherals deep and costs two pins:
| Stage | Hardware | Role |
|---|---|---|
| Input | PB0, PB1 with internal pull-ups | Quadrature A and B from the dial |
| Distribution | EVSYS CH0 (A), CH1 (B) | Pin-level events. PORTA/PORTB pin events are only available on channels 0 and 1, so this pairing is forced by silicon |
| Direction | CCL LUT0 + LUT1 driving Sequencer 0 as a D flip-flop | B sampled on A's rising edge — the classic quadrature direction detector. Both LUTs are pass-through (TRUTH = 0x02) taking EVENTA |
| Count | TCA0 in normal 16-bit mode | EVACTA = CNT_ANYEDGE on channel 0 counts both edges of A (2× resolution); EVACTB = UPDOWN on channel 8 takes the direction level from the sequencer |
TCA0.SINGLE.CNT is the dial position. The scan loop reads it, takes the signed delta against the last read, and sends that to DeMon as a jog-dial event. No debounce code, no state machine, no ISR.
Why not the Event System's own quadrature decoder: the XMEGA-era EVSYS QDEC block does not exist on the AVR Dx family. CCL plus TCA is Microchip's own replacement pattern for it (see AN2434).
TCA0 must stay in normal 16-bit mode. Split mode has no EVCTRL register at all, so event counting is simply unavailable there — this is why the RGB power light uses TCA1 rather than sharing TCA0.
Known limitation. The D flip-flop latches B on A's edge, so a dial stopped exactly on an edge can register a single count in the wrong direction. This is invisible on a volume or scroll control. If the dial is ever mapped to something absolute, the fix is the five-LUT CW/CCW variant feeding two TCBs, which costs three more LUTs and one more event channel — all of which are free.
The dial's push button is not part of this chain; it is read as COL15 on PD7 alongside the other off-keyboard buttons.
NeoPixel Chain
Clicky drives a single WS2812 data line that carries two logical groups of NeoPixels in one daisy-chained chain:
| Group | Count | Position in chain | Logical owner | Physical location |
|---|---|---|---|---|
| DeMon status LEDs | 8 | Head of chain (LEDs 0–7) | DeMon decides what each shows | DeMon's status panel on the case |
| Keyboard NeoPixels | per layout | Tail of chain (LEDs 8+) | DeMon or Clicky animations | Mounted under the keys |
Because WS2812 is a single-data-line daisy-chain protocol, one Clicky GPIO carries the data for the whole chain. Each LED reads the first 24 bits (or 32 bits for RGBW), then forwards the remainder downstream — so the 8 status LEDs read their values first, the keyboard LEDs read theirs after. Clicky composes both groups into a single contiguous frame per refresh.
Logical addressing. Clicky's firmware exposes the chain to DeMon as two logical address spaces — status[0..7] for the 8 status LEDs and key[row][col] (or key[index]) for the keyboard. DeMon doesn't care about the physical wire order; it addresses each LED by its logical name and Clicky maps that onto the right slot in the contiguous WS2812 frame.
Two refresh strategies share the same chain:
- DeMon-owned status LEDs update on subsystem state changes — power-on, SD insertion, network activity, MIDI traffic. DeMon sends "set status LED N to colour C" intent over I²C; Clicky updates that slot in the frame and triggers a chain redraw. Event-driven, not animated
- Keyboard NeoPixels can be either DeMon-driven (per-keystroke feedback flashes, keymap-layer indicators) or autonomously animated by Clicky (idle breathing patterns, modifier-shift indication, AntOS busy spinner, Velocikey). Clicky's 60 Hz frame loop composes any in-progress animation with the latest DeMon status-LED state into one outgoing frame
DeMon's "still controlling but not driving" model means the status LEDs behave exactly as if they were on DeMon's own GPIO, but the WS2812 timing and bus contention are entirely Clicky's problem. DeMon never bit-bangs a WS2812 frame; Clicky never decides what a status LED should mean.
How the waveform is generated
KLED is on PF3, which is CCL LUT3's output pin — and LUT3's only output pin, since LUT3 is the one LUT on the AVR128DB64 with no alternate position. That constraint is what shapes the rest of the design.
The WS2812 bitstream is not bit-banged. Cycle-counted assembly would hold interrupts off for roughly 30 µs per LED — around 2.6 ms per frame on a full keyboard, or 16% of wall-clock at 60 Hz — which is unacceptable next to a matrix scan and a live UART link. Instead a serial peripheral generates the waveform and the CCL routes it to PF3:
| Element | Setting | Reason |
|---|---|---|
| SPI0 | Host mode, PORTMUX = NONE, SSD set |
Runs the shift register as a pure internal signal source with no pin driven. The CCL's SPI input is valid in host mode only — which is precisely why the pinned SPI (the NFC bus) lives on SPI1 (see Peripheral Allocation) |
| SPI0 | PRESC = DIV16, CLK2X set |
24 MHz / 8 = 3 MHz shift clock |
| CCL LUT3 | INSEL0 = SPI0, TRUTH3 = 0x02 |
INSEL0 = 0x9 taps SPI0 MOSI. A plain pass-through buffer — SPI idles low, so no inversion is needed |
Bit encoding. Three SPI bits per WS2812 bit: 0b100 for a zero, 0b110 for a one. At 3 MHz each SPI bit is 333 ns, giving a 1.0 µs cell with T0H = 333 ns and T1H = 667 ns, against WS2812B windows of 400 ± 150 ns and 800 ± 150 ns. Both land in spec and this is the encoding almost every SPI-driven NeoPixel implementation uses, though T1H sits near the bottom of its window.
Buffering. 9 SPI bytes per LED — 88 LEDs is 792 bytes against 16 KB of SRAM. The frame is pre-expanded into a flat buffer and shifted out from the SPI0 DRE interrupt, which reduces the ISR to a load and a store. One byte every 2.67 µs, so the DRE vector must be assigned to CPUINT.LVL1VEC. Byte boundaries do not align with WS2812 cell boundaries, so a stall can leave MOSI parked high mid-cell and corrupt a bit — the buffer must never run dry, and a gap longer than about 50 µs latches the strip outright.
Exact-timing alternative, decided in firmware not hardware. USART0 is also free and its TXD is also a CCL input on the same LUT3 input index, so switching is a one-register change with no board impact. Running USART0 as 7N1 async double-speed with BAUD = 80 gives exactly 2.4 Mbaud at 24 MHz — zero error, T0H = 417 ns and T1H = 833 ns, dead centre of both windows. It costs a slightly awkward encoding: each 9-bit-time frame reads 1 !d0 !d1 | !d2 !d3 !d4 | !d5 !d6 0 once inverted, so fixing d1=1, d2=0, d4=1, d5=0 makes every group a valid 1 X 0 cell and leaves three payload bits per frame. Set TRUTH3 = 0x01 instead of 0x02, since a UART idles high and WS2812 idles low — the inversion is free in the truth table.
/* USART0 variant: one 7N1 frame carries three WS2812 bits */
static inline uint8_t frame3(uint8_t a, uint8_t b, uint8_t c) {
uint8_t f = 0x5B; /* all three bits = 0 */
if (a) f &= (uint8_t)~0x01;
if (b) f &= (uint8_t)~0x08;
if (c) f &= (uint8_t)~0x40;
return f;
}
Open verification item. The datasheet documents PORTMUX = NONE as a pin-routing selection and describes CCL peripheral inputs as a category distinct from pin inputs, which implies the tap sits upstream of the pin mux and survives NONE — but it never states this outright, for either SPI MOSI or USART TXD. Confirm on a Curiosity Nano before the layout is committed, because if the tap does not survive NONE then KLED cannot stay on PF3 at all: every alternate pin position of every CCL-reachable serial peripheral collides with the matrix, the crystals or the FireStorm link, so the fix would be a reshuffle rather than a fuse change.
Power. 330 Ω in series on PF3 at the MCU end, and 1000 µF across the strip's 5 V rail close to the first status pixel. With 88 LEDs at full white the chain approaches 5 A, so the rail should be budgeted against the brightness ceiling the firmware actually permits rather than the theoretical maximum.
Buzzer
A passive piezoelectric buzzer is driven directly from Clicky. Clicky generates the square wave, varying the frequency to produce different tones and the duration to produce different note lengths.
Hardware
KBUZZ is driven differentially from TCD0 at its default pin position: PA4 (WOA) and PA5 (WOB). TCD's waveform generator is natively a half-bridge — its cycle is structured as dead-time WOA, on-time WOA, dead-time WOB, on-time WOB — so the two outputs come out in antiphase with no extra logic. Across the piezo that is a 10 Vpp swing from the 5 V rail instead of 5 Vpp, roughly four times the acoustic power for one extra pin.
Using TCD's default position matters: erratum 2.14.2 makes CMPAEN control every waveform output on any non-default position, which would prevent enabling WOA and WOB independently on A4/A5 silicon. At the default position that erratum does not apply. WOC and WOD land on PA6 and PA7, which stay free as ordinary GPIO so long as CMPCEN and CMPDEN remain clear.
| Setting | Value | Effect |
|---|---|---|
| Clock | CLKSEL = CLKPER, CNTPRES = DIV32, SYNCPRES = DIV2 |
24 MHz / 64 = 375 kHz counter clock |
| Mode | WGMODE = ONERAMP |
CMPBCLR is the period register: f = 375000 / (CMPBCLR + 1) |
| Range | CMPBCLR = 3749 down to 74 |
100 Hz to 5 kHz in a single prescaler setting |
Compare registers are double-buffered, so a frequency change is written then committed with CTRLE = SYNCEOC. The tone queue runs from the TCD overflow interrupt, so a three-note boot chime costs the CPU almost nothing between notes.
Why TCD and not a TCB. A TCB has no prescaler of its own beyond /2 — it must borrow CLK_TCA from one of the TCAs. Both TCAs are committed (TCA0 to the jog dial, TCA1 to the power light), and borrowing TCA1's divider would chain the buzzer's frequency range to the power light's PWM refresh rate: a divider slow enough to reach 100 Hz makes the LED flicker at 91 Hz. TCD is the only remaining timer with an independent clock domain, and its 12-bit period register gives finer pitch resolution than a TCB's 8-bit one.
Pitch resolution. About 1.3% (23 cents) at the top of the range — fine for chimes, error tones and key clicks, coarse if a canned sound is ever meant to be melodic. Dropping to /32 and switching prescaler by octave is the fix if that becomes a requirement.
Protocol
DeMon sends short I²C messages describing the tone intent:
| Message form | Use |
|---|---|
| Play tone (F, D) | Frequency in Hz + duration in milliseconds — for arbitrary tones generated on-demand |
| Play canned sound (ID) | Pre-loaded sound patterns held in Clicky's flash — e.g., "boot" (power-on chime), "error" (two-tone beep), "key-click" (per-keystroke feedback, optional), "low-battery", "accessibility-prompt" |
| Stop | Silence the buzzer immediately, cutting off any in-progress tone or sequence |
| Define canned sound (ID, sequence) | Load a new sound pattern into Clicky's flash for later replay — used at configuration time, not in real-time playback |
Clicky maintains a small queue so DeMon can pre-load a short sequence ("boot chime" = 3 tones over 500 ms) with one I²C message and let it play out without further intervention. This keeps DeMon's control traffic low even during multi-note feedback sequences.
The buzzer is the system-level user-feedback channel — the audio path that works before the main audio stack is up (power-on chime), after AntOS has parked the audio stack (shutdown beep), and orthogonally to whatever the main audio system is doing (low-battery warning during music playback). After AntOS boot and the main audio stack spinning up, most audio output goes through the Tempest mixer to the WM8960/WM8962 codec; the buzzer is reserved for feedback that should be hearable even with headphones unplugged.
Clicky's firmware also uses the buzzer for keyboard-behaviour feedback: a key click on press (optional, off by default), a beep on layer or Caps-Word changes, a locked/error tone, alongside the tone-and-duration intents DeMon sends.
RGB Power Light
A single 4-pin RGB LED (common cathode or common anode) on the case, separate from the WS2812 chain. It is the one visual indicator that is guaranteed live: it comes up on the first instruction of Clicky's boot, works when the NeoPixel rail is unpowered, and cannot be affected by a stalled or mis-clocked WS2812 frame. That makes it the right place for power state and hard-fault indication.
Driven by TCA1 in split mode at its ALT3 (PORTG) pin position:
| Channel | Compare | Pin |
|---|---|---|
| Red | LCMP0 → WO0 |
PG0 |
| Green | LCMP1 → WO1 |
PG1 |
| Blue | LCMP2 → WO2 |
PG2 |
Split mode turns the 16-bit TCA into two independent 8-bit timers with three compare channels each. Only the low-byte half is used; the high-byte compares (HCMP0-2 → WO3–WO5 → PG3/PG4/PG5) are left disabled, so PG4 and PG5 remain available GPIO — a compare channel only overrides its pin when its enable bit is set.
LPER = 255,CLKSEL = DIV64→ 1.46 kHz PWM at 24 MHz, well clear of visible flicker- Writing
LCMPn = 0produces a genuine static low, stated explicitly in the datasheet for TCA. This is the reason TCA1 is used rather than a spare TCB: TCB's 8-bit PWM mode carries no equivalent guarantee forCCMPH = 0, and "not quite off" is visible on a power light in a dark room - Apply a gamma ≈ 2.2 lookup to the duty values; linear PWM makes fades look badly bunched at the bright end
- Common-anode parts: set
INVENon the three pins, or write255 - value
Why PORTG. TCA1 has only four pin positions on this part. PORTB starts at PB0 where the jog dial lives; PORTC is the COL0–7 bank; PORTE is the ROW bank. PORTG is the only position whose first three pins are free.
Series resistors at 5 V, sized per die because forward voltages differ: roughly 150 Ω for a 2.0 V red die and 91 Ω for 3.2 V green and blue, at 20 mA each. One resistor per colour, never a single resistor on the common leg — a shared resistor makes the colour shift with brightness.
Power Button & Power Control
Concept render of the Ant64 enclosure.
(Ant64 and eZX.) Clicky owns the soft power switch — both sensing the button and driving the power-off line — moved here from DeMon's MCP23017. Clicky is the right owner: it sits on the switched 5 V rail, comes alive in milliseconds, and is always running, so power control never depends on DeMon or the CM5 being up.
| Line | Pin | Direction | Role |
|---|---|---|---|
KPWR_BTN |
PG3 | in | Power-button sense from the Sparkfun Soft Power Switch Mk2 |
KPWR_OFF |
PG6 | out | Power-off / kill to the soft switch |
Power-on is hardware: pressing the button latches the soft switch on, powering the rails; Clicky then boots and takes control. Short press while running → Clicky raises KINT and flags the press in its I²C status; AntOS reads it, runs a graceful shutdown, and then commands Clicky over I²C to cut power (KPWR_OFF). This pairs naturally with the RGB power light Clicky already drives: power state and power control live on one always-alive chip.
The ~10 s force-off is hardware, and deliberately not Clicky's. Holding the button cuts power unconditionally in the switch itself, with no microcontroller in the path. That distinction is the whole point of the feature: the case it exists for is one where firmware cannot help — a wedged CM5, or Clicky itself halted mid-update clicky, when button sense, KPWR_OFF, keyboard, NeoPixels and power light are all gone at once. A Clicky-implemented hold would fail in exactly that case. Anyone later tempted to "simplify" it into firmware should read this paragraph first.
Design note. The referenced SparkFun Mk2 is a 74LVC1G175 flip-flop whose documented power-off is MCU-driven via its
OFFpin; its hardware overview describes no hold-to-kill timer. The unconditional 10 s cut is therefore something to add to that reference design — an RC into the flip-flop's reset, or a dedicated supervisor part — not something inherited from it. Cheap now, impossible to retrofit.
NFC Reader
(Ant64 only.) The on-board PN532 NFC reader sits on Clicky's own SPI bus (SPI1 as master, PB4–PB7), with reset and IRQ on Clicky (PA6 / PA7) — moved here from DeMon's shared housekeeping I²C and MCP23017. A dedicated bus keeps DeMon's RTC / secure-element / expander traffic clean, and lets the always-alive Clicky poll the reader continuously.
| Line | Pin | Direction | Role |
|---|---|---|---|
KNFC_MOSI / SCK / SS |
PB4, PB6, PB7 | Clicky → PN532 | NFC SPI (SPI1 master) to the PN532 — a dedicated bus. 5 V → 3.3 V |
KNFC_MISO |
PB5 | PN532 → Clicky | 3.3 V → 5 V |
KNFC_RST |
PA6 | Clicky → PN532 | Reset out to the PN532. 5 V → 3.3 V |
KNFC_IRQ |
PA7 | PN532 → Clicky | Card-present IRQ in from the PN532. 3.3 V → 5 V |
Clicky owns the whole low-level NFC stack — the PN532 command/response protocol, card-type handling (MIFARE, NTAG21x, ISO14443A…), NDEF parsing, URI extraction, validation and anti-repeat debounce — and hands DeMon a clean, validated card record over I²C (its slave interface on DeMon's bus): only good data crosses, e.g. { type, uid, uri }. DeMon never sees raw card bytes or card-type quirks — it just takes the record and runs the "a card is a URL" dispatch (scheme handling, DBFS switching, the game-manifest system in NFC). Writes are high-level too: DeMon says "write this URI" and Clicky does the NDEF encoding and PN532 write for the card's type. A tapped card raises the KINT doorbell; DeMon then reads the record over I²C.
The PN532 interface is 3.3 V, whatever VBAT is fed
The PN532 has two supplies, and only one of them takes 5 V. VBAT is 2.7–5.4 V, so the module can be powered from the 5 V rail — but the host interface runs off PVDD, specified 1.6–3.6 V with an absolute maximum of 4 V. Clicky's 5 V outputs would sit a volt above that absolute max continuously, so the NFC bus must be level-translated in both directions; it is not a 5 V-domain link. (The datasheet also forbids PVDD above VBAT, so raising PVDD is not an escape.)
The return direction needs translating for the mirror-image reason: MISO and KNFC_IRQ drive to PVDD, so at most 3.3–3.6 V against Clicky's 5 V V<sub>IH</sub> of ≈3.5 V — marginal at best, and not something to leave to tolerance.
Six lines, two directions, two parts:
| Direction | Lines | Part | Why |
|---|---|---|---|
| 5 V → 3.3 V | MOSI, SCK, SS, KNFC_RST |
74LVC125A powered at 3.3 V | Its inputs are 5 V-tolerant when the part runs at 3.3 V, so it takes Clicky's swing directly and outputs a clean PVDD-safe level. Four lines, one package |
| 3.3 V → 5 V | MISO, KNFC_IRQ |
74HCT125 powered at 5 V | HCT input thresholds (V<sub>IH</sub> ≈ 2.0 V at 5 V) accept 3.3 V as a valid high and re-drive it to a full 5 V, which is exactly the problem Clicky's 3.5 V V<sub>IH</sub> creates. Two lines, one package |
Fixed-direction buffers rather than an auto-direction translator (TXB-class) on purpose: the TXB parts drive weakly and are known to be marginal on push-pull SPI into any real capacitance, and there is no bidirectional line here to justify the risk. NFC traffic is slow — the PN532's SPI tops out around 5 MHz — so a pair of ordinary buffers is comfortably fast enough.
Worth confirming against the chosen module. Most PN532 breakouts regulate their own 3.3 V from a 5 V input pin, in which case the interface is 3.3 V regardless of what the "5 V" pin is fed, and the translation above is required either way. A bare-chip design on the carrier just sets PVDD = 3.3 V explicitly.
Peripheral Allocation
The four core-independent subsystems between them spend three timers, three CCL LUTs, one sequencer and three event channels. Everything else is free.
| Subsystem | Timer | CCL | EVSYS | Pins |
|---|---|---|---|---|
| Jog dial QDEC | TCA0 (normal 16-bit) | LUT0, LUT1, SEQ0 | CH0, CH1, CH8 | PB0, PB1 |
| RGB power light | TCA1 (split, low byte) | — | — | PG0, PG1, PG2 |
| WS2812 chain | — | LUT3 | — | PF3 |
| Buzzer | TCD0 (one-ramp) | — | — | PA4, PA5 |
Remaining after allocation:
| Resource | Free |
|---|---|
| Pins | PG4/PG5 free after removing the direct FPGA UART link — PA6/PA7 NFC reset/IRQ, PB4–PB7 NFC SPI (SPI1 master), PB2/PB3 FireStorm I²C slave (TWI1), PG3/PG6 power button/off, PG7 interrupt to DeMon; PA2/PA3 stay the DeMon-bus slave (TWI0). PB2/PB3 is the designated reserve — see below |
| CCL LUTs | LUT2, LUT4, LUT5 |
| CCL sequencers | SEQ1, SEQ2 |
| Timers | TCB0 – TCB4, all five |
| Event channels | CH2 – CH7, CH9 |
| Serial | USART0 (or SPI0, whichever KLED does not use), USART1, USART3, USART4 |
Every TCB is still available, which is what to reach for to time debounce windows, auto-repeat intervals or the tone queue without burning CPU. TWI0 stays Clicky's slave on DeMon's housekeeping bus (PA2/PA3) — DeMon reads Clicky's status / interrupt reason there. The NFC reader is now on SPI1 (PB4–PB7, master), and TWI1 (PB2/PB3) remains an I²C slave to FireStorm if that register-style side channel is retained. PG4/PG5 are free after removal of the direct FPGA UART link.
Why the NFC SPI is on SPI1
The NFC SPI is on SPI1 at ALT2 (PB4–PB7) rather than SPI0's default PA4–PA7. SPI1 is the master here; putting the pinned SPI on SPI1 rather than SPI0 buys three things:
- SPI0 is kept free to run as the internal host with
PORTMUX = NONE, which is what makes the documentedINSEL0 = SPI0 MOSICCL input available for KLED. That CCL tap works in host mode only, so SPI0 must stay the NeoPixel host — the pinned NFC bus therefore lives on SPI1 - The buzzer moves to TCD0's default position on PA4/PA5, sidestepping erratum 2.14.2 entirely
- PA6 and PA7 come free as ordinary GPIO
SPI1's ALT2 erratum (2.11.1) names 48-pin devices only — PB6 and PB7 do not exist on that package — so ALT2 is fully usable here on the 64-pin part.
Interface Levels
Clicky itself runs at 5 V, which is what lets it drive the key matrix, the WS2812 chain, the RGB power light and the piezo with no buffering:
- WS2812 DIN needs V<sub>IH</sub> ≈ 0.7 × 5 V = 3.5 V. A 5 V AVR output clears it directly — this is one of the main reasons for running the part at 5 V rather than 3.3 V
- RGB power light — green and blue dice have V<sub>f</sub> ≈ 3.2 V and would have almost no resistor headroom on a 3.3 V rail
- Key matrix — full 5 V swing through the Schottky diodes
Level shifters are required on every link to a 3.3 V peer:
| Link | Pins | Peer |
|---|---|---|
| UART2 to DeMon | PF4 / PF5 | DeMon |
| No direct FireStorm UART | PG4 / PG5 | Free GPIO; key events go via DeMon |
| UPDI | PF7 | Pulse (serialUPDI, UART4) |
| I2C0 slave (to DeMon) | PA2 / PA3 | Clicky's slave on DeMon's housekeeping bus. Open-drain — a bidirectional level translator or a correctly-referenced pull-up arrangement |
| I²C slave to FireStorm (TWI1) | PB2 / PB3 | To FireStorm (3.3 V) — the designated pin reserve; see below. Open-drain — bidirectional level translator or correctly-referenced pull-ups, as the DeMon bus |
| NFC SPI + RST (out) | PB4, PB6, PB7, PA6 | To the PN532 — 5 V → 3.3 V, 74LVC125A at 3.3 V |
| NFC MISO + IRQ (in) | PB5, PA7 | From the PN532 — 3.3 V → 5 V, 74HCT125 at 5 V |
Interrupt to DeMon (KINT) |
PG7 | To the MCP23017 (3.3 V) — 5 V→3.3 V |
The NFC bus is a 3.3 V link, not a 5 V one — the PN532's PVDD tops out at 3.6 V with a 4 V absolute maximum, so all six lines are translated. See NFC Reader for the part choice and the reasoning.
Clicky has no direct FPGA UART. Key events travel over Clicky's UART2 to DeMon; DeMon forwards them to FireStorm over its existing system path. TWI1 on PB2/PB3 remains available as a register-style FireStorm side channel if retained in the hardware design.
DeMon ↔ Clicky Interface
| Line | Direction | Carries |
|---|---|---|
| UART2 RX (DeMon GP15) | Clicky → DeMon | The immediate input stream — key events (type + row/col + UTF-8), jog-dial deltas (Ant64), button presses — sent the instant they happen, for minimum latency; plus config-save uploads and raw matrix dumps |
| UART2 TX (DeMon GP14) | DeMon → Clicky | Bulk keymap / configuration uploads, where UART throughput beats I²C |
| I2C0 (TWI0 slave) | DeMon → Clicky | Control: status-LED / key-NeoPixel colour intent, keyboard patterns, buzzer tone intent, power-light intent, power-off, profile switches, NFC write commands (Ant64) |
| I2C0 (TWI0 slave, read) | Clicky → DeMon | Structured reads: status / interrupt-reason, validated NFC card records (Ant64), power-button state, query responses |
INT (KINT → MCP23017 PB0) |
Clicky → DeMon | Doorbell — an NFC card, power-button, or status change is waiting on the I²C interface |
| UPDI (via Pulse, serialUPDI/UART4) | Pulse → Clicky | Firmware programming — update clicky |
The link splits by latency and role. The immediate input stream — keys, jog-dial, buttons — goes out on UART2 the instant it happens, for minimum latency; this is why keys are on UART and not the polled I²C bus. Everything DeMon drives or reads goes over the I²C slave, where DeMon is master: lighting and buzzer intent, power control, and NFC — small register-style writes and structured, on-demand reads. Clicky remains an I²C slave on DeMon's housekeeping bus (TWI0, PA2/PA3); the NFC reader is on a separate bus where Clicky is master (TWI1), so the two never contend. The KINT line into the MCP23017's PB0 is the doorbell (folding Clicky into the same INTB aggregation as Pulse and FireStorm): when a card is tapped or the power button pressed, KINT fires, DeMon reads Clicky's status over I²C to see what's waiting, then reads the record — already card-type/NDEF-validated — over the same interface.
DeMon-side pins are in demon.
Default baud rate is 115200. For high-throughput keymap or NeoPixel buffer uploads the link can negotiate up to ~1 Mbps on the AVR128DB64's UART; the default rate is sufficient for all routine traffic.
Flow control is protocol-level, not hardware. The AVR128DB64's USART has no RTS/CTS support, and USART2's ALT1 pin position (PF4/PF5) exposes only TxD and RxD — there is no XCK or XDIR there. Back-pressure during bulk uploads is handled by an ack/window scheme in the message protocol. If hardware flow control is ever genuinely needed, it costs two GPIOs driven as ordinary outputs and inputs.
Event message format
Each key event from Clicky to DeMon is a single message containing four fields:
| Field | Size | Description |
|---|---|---|
| Event type | 1 byte | press, release, or repeat (auto-repeat for held keys) |
| Row | 1 byte | Matrix row |
| Column | 1 byte | Matrix column |
| UTF-8 codepoint | 1–4 bytes | The decoded character for the active keymap and modifier state. Empty (zero-length) for pure modifier keys (Shift, Ctrl, Alt, CAPS SHIFT, SYMBOL SHIFT, …) and for keys that the active keymap leaves unmapped — only the row/column tells the host what was pressed |
The exact byte-level framing (binary with length prefix vs ASCII-escaped JSON vs SLIP-style framing) is an implementation detail of the firmware; the four fields above are the architectural contract.
Intent message forms (DeMon → Clicky)
| Intent | Carries |
|---|---|
| Set status LED | (index 0–7, RGB colour) — immediate, no animation |
| Set key NeoPixel | (row, col, RGB colour, optional fade duration) — instant or interpolated |
| Set keyboard pattern | (pattern ID, parameters) — kicks off a Clicky-side animation (breathing, rainbow, Velocikey, layer indicator) |
| Set power light | (RGB colour, optional fade duration) — the case RGB indicator, independent of the WS2812 chain |
| Play tone | (frequency, duration) |
| Play canned sound | (sound ID) |
| Define canned sound | (sound ID, sequence) |
| Stop buzzer | — |
| Power off | Cut power via the soft switch (after AntOS's graceful shutdown) |
| NFC command (Ant64) | (op, params) — configure the PN532, or write a card |
| Upload keymap | Full or partial keymap replacement |
| Switch profile | (profile ID) — swap to a previously-uploaded profile |
| Query state | (query type) — request matrix dump, modifier state, current profile, etc. |
Firmware Features
Clicky's firmware reproduces the keyboard-behaviour surface of QMK — everything about how a keyboard interprets presses — done entirely on-chip and reported to DeMon over UART2 rather than presented as USB-HID. It drops only the parts that were never Clicky's job (screens, wireless, multi-speaker audio).
Keymap & layer logic — handled on-chip, independent of the transport:
- Layers — the keymap is built from stacked overlays, like a generalised Fn key. Switching layer makes every physical key do something different, so a compact board reaches far more than its key count. Layers can be momentary (active only while a key is held), toggled on and off, one-shot (active for just the next keypress), entered by a tap-or-hold key, set as the default base, or combined so that holding two layers activates a third (tri-layer).
- Mod-Tap and tap-hold tuning — a dual-function key that types a normal character when tapped but acts as a modifier (Shift, Ctrl…) or layer when held, so a home-row key can double as Shift. The "tuning" is the set of timing rules that decide tap-versus-hold cleanly: the tapping term (how long a hold must last), permissive hold and hold-on-other-key-press (treat another keypress as the hold signal), quick tap, and retro tap — adjustable globally or per key.
- One-Shot keys and modifiers — "sticky" keys: tap Shift once and it applies to just the next key, then releases itself, so you never have to hold a modifier down.
- Tap Dance — one key that does different things depending on how many times you tap it (or tap then hold): e.g. one tap
(, two taps[, hold{. - Combos — pressing two or more keys at the same instant produces a separate action from any of them alone — chording, for symbols or shortcuts without a dedicated key.
- Leader key — press a dedicated leader key, then type a short sequence, and Clicky runs the action bound to it (the keyboard version of Vim's leader): leader, then
c,acould launch the calculator. - Macros and dynamic macros — a single key sends a whole string of keystrokes. Fixed macros are defined ahead of time; dynamic macros are recorded live on the keyboard itself and replayed on demand.
- Auto Shift — hold a key a fraction longer to get its shifted form instead of reaching for Shift: a quick tap of
agivesa, a slightly longer hold givesA. - Caps Word — a smart one-word caps lock that capitalises until you type a space or punctuation, then switches itself off — made for
CONSTANT_NAMESwithout leaving caps lock on. - Key Overrides — redefine what a modifier-plus-key combination emits (say Shift +
1producing something other than!) without disturbing the base key. - Repeat / Alternate-Repeat — a key that re-sends your last keypress, plus an "alternate" variant that sends its logical opposite (after
→it sends←). - Autocorrect — fixes common typos as you type, from a small on-board dictionary (e.g. "teh" → "the").
- Grave Escape — lets one key serve as both Esc and the grave/tilde key, choosing between them by modifier state — handy on compact boards with no room for both.
- Space Cadet — the Shift keys gain a second job: a quick tap of left Shift types
(and right Shift), while holding them still shifts normally. - Swap Hands — temporarily mirrors the layout left-to-right so the board can be typed one-handed while a key is held.
- Key Lock — press once to hold a key or modifier "down" until it's pressed again — useful for holding a game key or for one-finger operation.
- Bootmagic / Command — change settings without reflashing: hold chosen keys at power-on (Bootmagic), or a key combo at runtime (Command), to swap modifiers, toggle options, or drop into the bootloader.
- Configurable debounce — a mechanical switch "bounces" (makes and breaks several times in a millisecond); the debounce algorithm filters that so one physical press is one clean event, with the method and timing tunable. A spare TCB times the debounce window without occupying the scan loop.
Cleaner on UART than on USB — being a UART peer of DeMon rather than a USB-HID device improves three things outright:
- Full N-key rollover — every key registers independently no matter how many are already held, with no ghosting or blocking. USB's boot protocol caps this at six keys; the UART event stream has no such limit, so fast typing and gaming chords all come through. (On the eZX this matters especially for Spectrum games that read multi-key chords via
IN $FEFEhalf-rows in tight game loops.) - Native Unicode — Clicky already ships a real UTF-8 codepoint with each event, so accented and non-Latin characters just work, without the OS-specific input-method tricks QMK needs over USB.
- Live remap (VIA-style) — DeMon can change the keymap, build macros and combos, set the encoder map and tap-hold timings, and adjust lighting on the fly, with Clicky applying it immediately and no reflash — then saving it (below).
Lighting — on the WS2812 chain Clicky drives:
- Per-key RGB matrix — individually addressable lighting under every key, with animations and typing-reactive effects (keys can light as they're pressed); the same LEDs can also act as indicators for caps, active layer, and so on.
- Status pixels — the eight status LEDs at the head of the chain double as indicator lights, driven by Clicky on DeMon's behalf — DeMon sends colour intent for system states (power / SD activity / network / Wi-Fi / Bluetooth / MIDI / etc.) and Clicky composes them into the same outgoing frame as the keyboard lighting.
- Velocikey — the lighting animation speeds up or slows down with how fast you're actually typing.
- Power light — the case RGB indicator on PG0–PG2 is driven independently of the chain, so it still reports state if the NeoPixel rail is down.
Audio — buzzer only. No twin speakers, no MIDI, no music mode — just feedback tones on the single piezo: a key click, a beep on layer or Caps-Word changes, a locked/error tone, a startup chime — alongside the tone-and-duration intents DeMon already sends.
Encoder — the jog dial. (Ant64 only.) The rotary dial is mapped per layer, so turning it clockwise or anticlockwise can do different things on each layer (volume, scroll, brightness, zoom…), and the knob's push button is a fully configurable key, layer switch, or macro. Decoding is core-independent — see Jog Dial. The eZX has no integrated rotary control and no Cranky chip, so this feature is absent there — encoder-mapped layer behaviour on the eZX uses keyboard chords or modifier combinations instead.
Speccy compatibility mode. (eZX only.) The eZX-specific decoder mode for running Spectrum software that expects the original keyboard semantics:
- Standard key events are emitted as UTF-8 codepoints exactly like the modern mode
- Spectrum BASIC keyword chords — the CAPS SHIFT and SYMBOL SHIFT combinations that the original Spectrum uses to enter BASIC keywords (
PRINT,LET,GOTO, etc.) are resolved on Clicky and the keyword's Unicode equivalent is emitted as a single event. Applications that want the raw chord (modifier + base key) rather than the resolved keyword can switch to a "raw chord" sub-mode - Raw matrix dumps on request — Spectrum applications that read the keyboard via
IN $FEFE(and the floating-bus quirks around it) need bit-exact half-row matrix state, not UTF-8 events. Clicky exposes a polling endpoint that returns the current 5×8 matrix bitmap on demand; the running Classic-image core'sIN $FEFEhandling reads this directly. For 56-key modern keyboards in Speccy mode, only the lower 5 rows participate in theIN $FEFEdump — the extra 2 rows return idle, preserving bit-exact Speccy compatibility
Not included: screen drivers (OLED / LCD — Clicky drives no screen; the vertical touch TFT on Ant64 is DeMon's), Bluetooth (DeMon / C5's job), split-keyboard, PS/2 mouse, thermal printer, the pointing-device driver framework, mouse keys (moving the cursor from the keyboard), haptic feedback (a buzzing solenoid or motor in the case), and music-mode / MIDI audio. Stenography — a Plover-style chord mode for machine shorthand — is left as an optional future mode.
Configuration — Save / Load via DeMon
Clicky holds its active configuration — keymap and layers, macros, combos, tap-dance, leader sequences, encoder map (Ant64), tap-hold timings, lighting, power-light colours, Speccy-compat-mode flag (eZX) — in working memory, and DeMon owns the persistent copy. A small protocol over UART2 keeps the two in sync:
- Boot — Clicky comes up immediately on a built-in default held in its own EEPROM, so the keyboard works before DeMon is ready. The default is product-aware: 75% mechanical default on Ant64, Spectrum-style default on eZX (40-key or 56-key per autodetect). Once the UART link is up, DeMon loads the saved configuration down to Clicky, which applies it in place
- Save — when a config changes (a live remap, a recorded macro, a lighting tweak), Clicky sends the updated configuration up to DeMon, which writes it to persistent storage (DBFS). A user key combo can also force a save
- Load on demand — DeMon can push a different stored profile at any time — per-user or per-application keymaps, say — and Clicky swaps to it without a reset. Useful examples: a
speccyprofile for retro-compat work on the eZX, acodeprofile for AntOS shell sessions, agameprofile with tighter debounce and gaming-favourable layer assignments
DeMon is the authority and the store; Clicky is the engine. The same link that carries key events and lighting intents carries these config transfers, so no extra wiring is needed.
Programming
Clicky is UPDI-programmed by Pulse — the single UPDI programmer for all three AVRs — under DeMon's orchestration. Clicky's UPDI pin (PF7) runs to Pulse, wired as serialUPDI: Pulse's UART4 TX and RX are joined at PF7 through a series resistor (≈470 Ω, with the classic SerialUPDI 4.7 kΩ TX pull-up), so a hardware UART drives the single-wire protocol at full speed with no bit-banging.
The flow is the same as Sticky's and Cranky's:
- DeMon holds the firmware file in DBFS, parses it (Intel HEX / ELF), extracts the program bytes and fuse settings, and ships the payload to Pulse over the SPI peer link
- Pulse drives the UPDI session on Clicky — write flash and fuses, read back to verify — and reports the result to DeMon
- The user-facing entry point is the AntOS
update clickycommand - During the update the keyboard, NeoPixel chain, buzzer, power light and the power button are all unavailable — everything Clicky owns goes at once. AntOS warns the user before initiating the update if there are no other input methods configured (e.g., a USB keyboard on DeMon's hub)
- AntOS should sync and quiesce DBFS before starting. The only escape from a hung session is the hardware 10 s force-off, which is an ungraceful cut with AntOS running normally and D: live — so assume an impatient user reaches for it, and make that a clean restart rather than a filesystem casualty
UPDI is used for firmware update and possible debug only. Normal keyboard input flows over UART2 to DeMon; NeoPixel, buzzer and power-light control flow over the I²C link.
OCD-level debug (single-step, breakpoint, register inspection on the AVR core via pyupdi-style tooling) is the natural extension if ever needed, and the serialUPDI transport supports it — though a debug session holds Pulse's shared UART4, so the CI1303 link and the extra MIDI Thru pause while it is attached. Initial implementation is programming only.
Pin Naming
| Signal | Direction | Count | Notes |
|---|---|---|---|
| Column read lines | Clicky input | 8 | Pulled high by Clicky, read low when a depressed key in the active row pulls them down through its Schottky diode |
| Row drive lines | Clicky output | 5–7 | Ant64: depends on 75% layout. eZX: 5 Sinclair rows + 2 Next-extender rows. Only one row driven low at a time during scan |
| Encoder A/B | Clicky input | 2 | (Ant64 only) Quadrature signals from the rotary jog dial. Must be on PORTA or PORTB — pin events from those ports are the only ones reaching event channels 0 and 1 |
| Dial push button | Clicky input | 1 | (Ant64 only) Read as COL15, not as a dedicated pin |
KLED |
Clicky output | 1 | WS2812 data line — single chain carries the 8 status LEDs at the head + per-key NeoPixels downstream. Must be a CCL LUT output pin |
KBUZZ+, KBUZZ− |
Clicky output | 2 | Antiphase square-wave pair to the passive piezo (TCD0 WOA/WOB at the default pin position). A single-ended variant works on one pin at half the swing |
KPWR_R, KPWR_G, KPWR_B |
Clicky output | 3 | 8-bit PWM to the case RGB power light (TCA1 split mode). Must be three consecutive WO pins of one TCA1 position |
KPWR_BTN |
Clicky input | 1 | Power-button sense from the soft power switch |
KPWR_OFF |
Clicky output | 1 | Power-off / kill to the soft power switch |
KI2C_SDA, KI2C_SCL |
Clicky I²C slave | 2 | Slave on DeMon's housekeeping bus (TWI0) — DeMon reads status / interrupt reason. 3.3 V peer, level-shifted |
KNFC_MOSI, MISO, SCK, SS |
Clicky SPI master | 4 | (Ant64) Dedicated NFC SPI bus to the PN532 (SPI1 master). 3.3 V peer — translated: MOSI/SCK/SS down, MISO up |
KPANEL_PWM |
Clicky output | 1 | (Ant64) Backlight brightness PWM to the panel, on PB2. Unidirectional shift from 5 V down to the panel's IO rail, whatever it is — 1.8 V bare module, 3.3 V finished board. Replaces KFPGA_SDA; duty 0 is the power-on state, so this line is both the enable and the brightness control — see demon |
KNFC_RST |
Clicky output | 1 | (Ant64) NFC reset to the PN532. 5 V → 3.3 V, shares the outbound translator |
KNFC_IRQ |
Clicky input | 1 | (Ant64) NFC card-present IRQ from the PN532. 3.3 V → 5 V, shares the inbound translator |
KINT |
Clicky output | 1 | Interrupt / doorbell to DeMon, into the MCP23017 PB0 → INTB. 3.3 V peer, level-shifted |
KUART_TX, KUART_RX |
UART to DeMon | 2 | Standard UART signals, no hardware flow control |
K2UART_TX / K2UART_RX |
Free | 2 | No direct FireStorm UART; key stream is Clicky → DeMon |
KUPDI |
UPDI from Pulse | 1 | Firmware update only — serialUPDI over Pulse's UART4, level-shifted to 3.3 V |
KPRODUCT |
Clicky input (board strap) | 1 | Tied to GND or 5V depending on product (eZX vs Ant64). Read once at boot to select matrix-scan mode. Weak internal pull-up means a floating pin during development falls back to a default mode |
Pinouts
| Pin | Function | Description |
|---|---|---|
| PA0 | XTALHF1 | 24 MHz crystal |
| PA1 | XTALHF2 | |
| PA2 | I2C0_SDA — TWI0 slave (default) | Slave on DeMon's housekeeping bus — DeMon reads Clicky status / interrupt reason |
| PA3 | I2C0_SCL — TWI0 slave (default) | |
| PA4 | KBUZZ+ — TCD0 WOA (default) | Piezo, 100 Ω series |
| PA5 | KBUZZ− — TCD0 WOB (default) | Piezo other leg, antiphase |
| PA6 | KNFC_RST | (Ant64) NFC reset out to PN532, translated to 3.3 V. TCD0 WOC — leave CMPCEN clear |
| PA7 | KNFC_IRQ | (Ant64) NFC IRQ in from PN532, translated from 3.3 V. TCD0 WOD — leave CMPDEN clear |
| PB0 | Encoder A — EVSYS CH0 | Jog dial quadrature A. Internal pull-up |
| PB1 | Encoder B — EVSYS CH1 | Jog dial quadrature B. Internal pull-up |
| PB2 | KPANEL_PWM — TCA0 WO2 (PORTMUX → PORTB) | (Ant64) Backlight PWM to the panel. 5 V down to the panel's own IO rail — 1.8 V for the bare ST7102 module, 3.3 V for a finished Waveshare-class board. Was KFPGA_SDA |
| PB3 | free | Was KFPGA_SCL. Freed by dropping the Clicky↔FireStorm I²C |
| PB4 | SPI1_MOSI | (Ant64) NFC SPI — SPI1 master, ALT2 → PN532, via the 5 V→3.3 V translator |
| PB5 | SPI1_MISO | Return from the PN532, via the 3.3 V→5 V translator |
| PB6 | SPI1_SCK | |
| PB7 | SPI1_SS | |
| PC0 | COL0 | COL 0 to 14 used on Ant64, COL 0 to 6 used on eZX |
| PC1 | COL1 | |
| PC2 | COL2 | |
| PC3 | COL3 | |
| PC4 | COL4 | |
| PC5 | COL5 | |
| PC6 | COL6 | |
| PC7 | COL7 | |
| PD0 | COL8 | |
| PD1 | COL9 | |
| PD2 | COL10 | |
| PD3 | COL11 | |
| PD4 | COL12 | |
| PD5 | COL13 | |
| PD6 | COL14 | |
| PD7 | COL15 | Buttons not on keyboard, eg jogdial button. Also VREFA — only matters if an external ADC reference is ever needed |
| PE0 | ROW0 | ROW 0 to 5 used on Ant64, ROW 0 to 7 used on eZX |
| PE1 | ROW1 | |
| PE2 | ROW2 | |
| PE3 | ROW3 | |
| PE4 | ROW4 | |
| PE5 | ROW5 | |
| PE6 | ROW6 | |
| PE7 | ROW7 | |
| PF0 | XTAL32K1 | 32.768 kHz crystal |
| PF1 | XTAL32K2 | |
| PF2 | KPRODUCT input | 0 = eZX layout, 1 = Ant64 layout. Clear of USART2 because UART2 uses ALT1, which has no XCK |
| PF3 | KLED — CCL LUT3 output | NeoPixel chain data. LUT3's only output position; no alternate exists. 330 Ω series at the connector |
| PF4 | KUART_TX — USART2 ALT1 | To DeMon |
| PF5 | KUART_RX — USART2 ALT1 | From DeMon |
| PF6 | RESET | |
| PF7 | KUPDI | UPDI from Pulse (serialUPDI, UART4) |
| PG0 | KPWR_R — TCA1 WO0 / LCMP0 (ALT3) | RGB power light, red. ~150 Ω at 20 mA |
| PG1 | KPWR_G — TCA1 WO1 / LCMP1 (ALT3) | Green. ~91 Ω at 20 mA |
| PG2 | KPWR_B — TCA1 WO2 / LCMP2 (ALT3) | Blue. ~91 Ω at 20 mA |
| PG3 | KPWR_BTN | Power-button sense (in). TCA1 WO3 — leave HCMP0EN clear; also TCB4's only WO |
| PG4 | Free GPIO | Direct FPGA UART removed; USART5 disabled |
| PG5 | Free GPIO | Direct FPGA UART removed; USART5 disabled |
| PG6 | KPWR_OFF | Power-off / kill (out) to the soft power switch |
| PG7 | KINT | Interrupt / doorbell out to DeMon (MCP23017 PB0) |
Not routed to pins: SPI0 runs as a host with PORTMUX = NONE as the internal signal source for the NeoPixel waveform (see NeoPixel Chain); USART0 is held in reserve for the same role with PORTMUX = NONE if the exact-timing variant is preferred. TCA0 counts jog-dial events and drives no outputs.
Silicon Revision Notes
Errata references are DS80000915F rev. F, covering AVR128DB28/32/48/64.
| Erratum | Effect on Clicky | Revisions |
|---|---|---|
| 2.11.1 SPI1 ALT2 non-functional | Names 48-pin devices only — PB6/PB7 do not exist on that package. Does not affect this design, which is 64-pin. This is the erratum that would have blocked putting the NFC SPI on PB4–PB7 | A4, A5 — fixed in B0 |
2.14.2 TCD CMPAEN controls all WOx at non-default pin positions |
Does not apply — KBUZZ uses TCD0's default position, which is one of the reasons the buzzer sits on PA4/PA5 rather than PB4/PB5 | A4, A5 — fixed in B0 |
2.13.1 TCB CCMP/CNT act as 16-bit in 8-bit PWM mode |
No effect today — no TCB is in PWM8 mode. Relevant if a spare TCB is ever used for PWM: write the full 16-bit CCMP, not CCMPH alone |
A4, A5 — fixed in B0 |
| TCB4 WO alternate non-functional | TCB4's output is fixed at PG3. Note if PG3 is ever assigned to something else | All revisions |
| 2.12.1 TCA restart resets count direction in NORMAL/FRQ mode | TCA0 is in normal mode but is never restarted by command or event, so this does not apply. Avoid issuing RESTART to TCA0 |
A4, A5 — fixed in B0 |
Bring-up verification list:
- CCL peripheral tap with
PORTMUX = NONE— the one undocumented assumption in the design. Scope PF3 with SPI0 shifting before trusting the NeoPixel path. If it fails, retry with USART0 (same LUT3 input index, one register different) before concluding the tap is unavailable; if both fail, KLED cannot stay on PF3 and the pinout needs rework - TCD antiphase timing — confirm PA4 and PA5 are complementary with no overlap at the top and bottom of the frequency range
- WS2812 stall margin — confirm the SPI buffer never runs dry under worst-case interrupt load (a full matrix scan concurrent with a UART burst). Byte boundaries do not align with WS2812 cell boundaries, so a stall can park MOSI high mid-cell
Future Considerations
- Higher UART baud rates for fast keymap reloads or live-debug matrix streaming — the AVR128DB64's UART supports rates up to ~1 Mbps with care taken on clock accuracy; the default 115200 baud is sufficient for routine use
- Per-key haptic feedback — a small actuator under each key could give tactile confirmation of keypresses on a membrane-style keyboard. Not currently planned; all five TCBs are unallocated, though no spare GPIO remain after the NFC, power and interrupt lines
- 4× jog-dial resolution — the current decoder counts both edges of channel A for 2× resolution. Adding a third LUT as an XOR of A and B gives 4× at the cost of one more LUT and one more event channel, both available
- Programmable key remapping in EEPROM — Clicky's on-chip EEPROM (512 bytes) holds a small per-user keymap override applied at the scanner level before reporting to DeMon. Useful for keeping user remaps persistent across power cycles without DeMon involvement at every boot; the AntOS
clickyCLI exposes the editor - Stenography mode — Plover-style chord input for machine shorthand, the technique court reporters use to type at 200+ WPM. Rather than one key per character, the typist presses a whole group of keys at once — a stroke — which resolves through a steno dictionary to an entire word, syllable, or phrase, so a sentence takes a handful of strokes instead of dozens of keypresses. Clicky already has the two hard prerequisites: full N-key rollover (every key in a simultaneous stroke registers cleanly, no ghosting — see the modern-mode section above) and the chord-detection machinery that already resolves multi-key combos and Speccy BASIC keyword tokens. In steno mode Clicky captures each stroke as a key-set and hands it to DeMon, where a Plover-format dictionary in DBFS resolves it to a UTF-8 string — the same ready-to-use codepoint stream every other Clicky event produces, so nothing downstream changes. It's an optional keymap layer (and an accessibility win for anyone who finds single-key typing slow or painful); because resolution lives on DeMon, the dictionary is user-editable via the AntOS
clickyCLI and can be swapped or extended without reflashing Clicky
Clicky-to-FireStorm path — via DeMon
Clicky sends key events, dial deltas and button events to DeMon over UART2. DeMon forwards those events to FireStorm over its existing system path. There is no direct Clicky-to-FPGA UART, so PG4 and PG5 are free GPIO and the unused USART5 peripheral is disabled. No FPGA acknowledgements or commands are required on the Clicky link.
