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.


Clicky sends input to DeMon and drives local lighting and feedback

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 clicky ships 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 = 0 produces 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 for CCMPH = 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 INVEN on the three pins, or write 255 - 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

Ant64 concept render — macro view of the cyan illuminated power button

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 OFF pin; 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 documented INSEL0 = SPI0 MOSI CCL 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, a could 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 a gives a, a slightly longer hold gives A.
  • Caps Word — a smart one-word caps lock that capitalises until you type a space or punctuation, then switches itself off — made for CONSTANT_NAMES without leaving caps lock on.
  • Key Overrides — redefine what a modifier-plus-key combination emits (say Shift + 1 producing 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 $FEFE half-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's IN $FEFE handling reads this directly. For 56-key modern keyboards in Speccy mode, only the lower 5 rows participate in the IN $FEFE dump — 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 speccy profile for retro-compat work on the eZX, a code profile for AntOS shell sessions, a game profile 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 clicky command
  • 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:

  1. 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
  2. TCD antiphase timing — confirm PA4 and PA5 are complementary with no overlap at the top and bottom of the frequency range
  3. 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 clicky CLI 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 clicky CLI 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.

Important: The Ant64 family of home computers are at early design/prototype stage, everything you see here is subject to change.