Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
*.bak
.DS_Store

# Python bytecode (tools/)
__pycache__/
*.pyc

# Git worktrees
.worktrees/

Expand Down
384 changes: 384 additions & 0 deletions KMBOX_NET_FIRMWARE_REQUIREMENTS.md

Large diffs are not rendered by default.

21 changes: 18 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@ else
$(error PROTOCOL must be 'hurra' or 'ferrum')
endif

DEFINES = -DARDUINO_TEENSY_MICROMOD -D__IMXRT1062__ -DF_CPU=816000000 \
# F_CPU: core clock. 600 MHz is the part's rated max; this board runs
# overclocked. 912 MHz needs ~1525 mV core (set_arm_clock interpolates it,
# capped at the 1575 mV silicon max). IPG = F_CPU/4 = 228 MHz stays a whole
# MHz so the GPT2 1 µs tick and LED scale (both F_CPU-derived) remain exact.
# Above ~864 MHz exceeds NXP's 1300 mV recommended limit — trades silicon
# lifetime for clock. Drop to 816000000 to back off. The tempmon monitor
# (main.c) flags overtemp via LED/telemetry but does NOT downclock — it warns,
# it does not protect; manage cooling accordingly.
F_CPU ?= 912000000
DEFINES = -DARDUINO_TEENSY_MICROMOD -D__IMXRT1062__ -DF_CPU=$(F_CPU) \
-DCMD_BAUD=$(CMD_BAUD) $(PROTO_DEF)

CFLAGS = $(MCU_FLAGS) $(DEFINES) \
Expand Down Expand Up @@ -57,8 +66,9 @@ $(TARGET).elf: $(OBJ)
$(TARGET).hex: $(TARGET).elf
$(OBJCOPY) -O ihex -R .eeprom $< $@

# Hot-path sources get -O2 instead of -Os
HOT_SRC = src/usb_host.o src/usb_device.o src/kmbox.o \
# Hot-path sources get -O2 instead of -Os. main.c is included because the
# central poll loop (PIT tick dispatch, EP polling, merge/send) lives there.
HOT_SRC = src/main.o src/usb_host.o src/usb_device.o src/kmbox.o \
src/humanize.o src/actions.o
ifeq ($(PROTOCOL),hurra)
HOT_SRC += src/hurra.o src/third_party/TinyFrame/TinyFrame.o
Expand All @@ -85,3 +95,8 @@ test:
cc -std=c11 -O2 -DHUMANIZE_HOSTTEST -Isrc -o /tmp/humanize_test \
test/humanize_test.c src/humanize.c -lm
/tmp/humanize_test
cc -std=c11 -O2 -Isrc -o /tmp/motion_test \
test/motion_test.c src/actions.c -lm
/tmp/motion_test
cc -std=c11 -O2 -Isrc -o /tmp/synth_cadence_test test/synth_cadence_test.c
/tmp/synth_cadence_test
120 changes: 79 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,42 @@
# hurra v2 - USB HS
# Hurra v2 USB HID man-in-the-middle

Bare-metal USB HID man-in-the-middle firmware for the **SparkFun MicroMod Teensy** (NXP i.MX RT1062). Enumerates a real USB HID device on the host port, replays it on the device port to the Mac/PC, and accepts **Hurra binary protocol** commands over UART (default) to inject mouse/keyboard input on top of the live HID stream. A **Ferrum ASCII** compatibility mode is available via `make PROTOCOL=ferrum`.
Bare-metal firmware for the **SparkFun MicroMod Teensy** (NXP i.MX RT1062) that sits between a USB HID device and your computer. It enumerates a real HID device (mouse, keyboard, controller) on its USB host port, replays that device to the host PC on its device port, and lets you **inject your own mouse and keyboard input** on top of the live HID stream over a serial link.

The host-side adapter is [`hurra-app`](https://github.com/VoltCyclone/hurra-app) (`hurra-bridge`), which also exposes a Ferrum-compatible virtual COM port for legacy tools.
Injected motion is passed through an always-on humanization filter (sub-pixel jitter, micro-correction, and dwell) so synthetic input blends with the real device stream.

## Hardware
Two command protocols are supported:

- **SparkFun MicroMod Teensy** on the **MicroMod ATP Carrier Board**.
- **WCH CH343** USB-UART bridge wired to Teensy `RX2`/`TX2` (D16/D17 -> LPUART3; ATP carrier UART_RX2/UART_TX2 headers). USB Full Speed, up to 6 Mbaud, 64-byte bulk MPS.
- A USB HID device (mouse, keyboard, controller) on the Teensy's USB host port.
- Teensy USB device port to the host PC.
- **Hurra binary** (default) — fast TinyFrame-based protocol, driven by the host app.
- **Ferrum ASCII** (`make PROTOCOL=ferrum`) — text protocol for compatibility with legacy tools.

The host-side companion app is [`hurra-app`](https://github.com/VoltCyclone/hurra-app) (`hurra-bridge`), which talks the Hurra protocol and also exposes a Ferrum-compatible virtual COM port for older tooling.

## How it works

```
USB HID device ──→ Teensy USB-host ──┐
│ (firmware proxies + injects)
Host PC USB ←── Teensy USB-device ───┘
km.* commands
injected input
Host PC USB ──→ CH343 ──→ Teensy LPUART3 (D16/D17)
```

## Wire protocol
The firmware forwards every real HID report through unchanged, merges in any input you inject over the serial link, and sends the combined stream to the host PC.

**Default — Hurra binary (TinyFrame):** SOF `0x68`, 1-byte ID/LEN/TYPE, CRC16. Little-endian payloads. Driven by `hurra-app`/`hurra-bridge`; see that repo for the host API. Targets >=8k commands/sec at 4 Mbps over the CH343 link. Boots at **4 Mbaud** (matching the bridge/hello default, so no `--baud` flag is needed); `km.baud(N)` bumps it and the firmware falls back to the 4 Mbaud boot default after extended RX idle.
## Hardware

- **SparkFun MicroMod Teensy** on the **MicroMod ATP Carrier Board**.
- **WCH CH343** USB-UART bridge wired to Teensy `RX2`/`TX2` (D16/D17 → LPUART3; ATP carrier `UART_RX2`/`UART_TX2` headers). USB Full Speed, up to 6 Mbaud, 64-byte bulk packets.
- A USB HID device (mouse, keyboard, controller) on the Teensy's USB **host** port.
- The Teensy USB **device** port connected to the host PC.

**Compatibility — Ferrum ASCII** (`make PROTOCOL=ferrum`): `\r\n`-terminated text, 115200 baud (resets to 115200 every power cycle). Reference: <https://ferrumllc.github.io/print.html>.
## Command protocols

**Hurra binary (default).** TinyFrame framing — SOF `0x68`, 1-byte ID/LEN/TYPE, CRC16, little-endian payloads. Driven by `hurra-app` / `hurra-bridge`; see that repo for the host API. Targets ≥8k commands/sec at 4 Mbps over the CH343 link. The firmware boots at **4 Mbaud** (matching the bridge's default, so no `--baud` flag is needed); `km.baud(N)` raises the rate, and the firmware falls back to the 4 Mbaud boot default after the link goes idle.

**Ferrum ASCII (`make PROTOCOL=ferrum`).** `\r\n`-terminated text commands at 115200 baud (reset to 115200 on every power cycle). Reference: <https://ferrumllc.github.io/print.html>.

```
TX: km.version()\r\n
Expand All @@ -37,51 +48,78 @@ TX: m(2, 0)\r\n # alias for km.move
## Build & flash

```sh
make # produces firmware.hex (Hurra binary protocol, default)
make PROTOCOL=ferrum # build with Ferrum ASCII protocol instead
make flash # flashes via teensy_loader_cli
make # build firmware.hex (Hurra binary protocol — default)
make PROTOCOL=ferrum # build with the Ferrum ASCII protocol instead
make flash # flash via teensy_loader_cli
make clean # remove objects and build artifacts
```

Requires:
- ARM GCC (the Teensyduino-bundled toolchain at `~/.platformio/packages/toolchain-gccarmnoneeabi-teensy/bin` by default — edit `Makefile` if yours lives elsewhere).
- `teensy_loader_cli` on `$PATH`.
Requirements:

- **ARM GCC** — the Teensyduino-bundled toolchain at `~/.platformio/packages/toolchain-gccarmnoneeabi-teensy/bin` by default. Edit the `Makefile` if yours lives elsewhere.
- **`teensy_loader_cli`** on your `$PATH`.

## Test

`ferrum_test.py` speaks Ferrum ASCII, so point it at a `PROTOCOL=ferrum` build's
serial port **or** at the `hurra-bridge` PTY symlink (`~/.hurra-bridge.tty`) when
running the default Hurra firmware — not directly at a Hurra build's port.
`tools/ferrum_test.py` speaks Ferrum ASCII. Point it at the serial port of a `PROTOCOL=ferrum` build, **or** at the `hurra-bridge` PTY symlink (`~/.hurra-bridge.tty`) when running the default Hurra firmware — not directly at a Hurra build's port.

```sh
pip install pyserial
tools/ferrum_test.py ~/.hurra-bridge.tty smoke # via the bridge (Hurra firmware)
tools/ferrum_test.py /dev/tty.usbserial-XXXX smoke # direct (PROTOCOL=ferrum build)

# Smoke test via the bridge (default Hurra firmware)
tools/ferrum_test.py ~/.hurra-bridge.tty smoke

# Smoke test direct (PROTOCOL=ferrum build)
tools/ferrum_test.py /dev/tty.usbserial-XXXX smoke
```

That handshakes `km.version()`, nudges the mouse, exercises buttons + wheel, and validates the read forms. For a closed-loop aim test that drives the cursor toward on-screen dots:
The smoke test handshakes `km.version()`, nudges the mouse, exercises the buttons and wheel, and validates the read forms.

**Closed-loop aim test** — drives the cursor toward on-screen dots:

```sh
pip install pyserial pynput
tools/ferrum_aim_test.py /dev/tty.usbserial-XXXX
tools/ferrum_aim_test.py ~/.hurra-bridge.tty
```

**Load test** — measures latency, throughput, and integrity of the command channel under sustained load:

```sh
tools/ferrum_load_test.py ~/.hurra-bridge.tty
```

**Humanization analyzer** — compares a captured motion trace against a real human baseline to check the kinematic signatures anti-cheat detectors look for:

```sh
tools/humanization_analyze.py trace.txt --baseline human.txt
```

A host-native unit test for the humanization filter also runs without hardware:

```sh
make test
```

## Layout

```
Makefile ARM GCC build, 816 MHz, -O2 hot path
core/ reset vector, MPU/cache setup, FlexSPI boot data
include/imxrt.h i.MX RT1062 register/peripheral header
src/main.c poll loop: USB host → merge → USB device send
src/usb_host.c/.h EHCI host (USB2)
src/usb_device.c/.h EHCI device (USB1)
src/desc_capture.* descriptor + HID report-layout capture
src/kmbox.c/.h LPUART3 DMA RX/TX ring + HID merge
src/hurra.c/.h Hurra binary parser (TinyFrame) — default protocol
src/ferrum.c/.h Ferrum ASCII parser (opt-in: PROTOCOL=ferrum)
src/proto.h compile-time protocol selector
src/actions.c/.h transport-agnostic injection helpers (act_*)
src/smooth.c/.h bezier-smoothed motion queue
src/humanize.c/.h sub-pixel jitter + dwell
tools/ferrum_test.py protocol smoke harness
tools/ferrum_aim_test.py closed-loop aim test against on-screen dots
Makefile ARM GCC build, 912 MHz default (F_CPU override), -O2 hot path
core/ reset vector, MPU/cache setup, FlexSPI boot data
include/imxrt.h i.MX RT1062 register/peripheral header
src/main.c poll loop: USB host → merge → USB device send
src/usb_host.c/.h EHCI host controller (USB2)
src/usb_device.c/.h EHCI device controller (USB1)
src/desc_capture.* descriptor + HID report-layout capture
src/kmbox.c/.h LPUART3 DMA RX/TX ring + HID report merge
src/hurra.c/.h Hurra binary parser (TinyFrame) — default protocol
src/ferrum.c/.h Ferrum ASCII parser (opt-in: PROTOCOL=ferrum)
src/proto.h compile-time protocol selector
src/actions.c/.h transport-agnostic injection helpers (act_*)
src/humanize.c/.h always-on humanization filter (jitter, micro-correction, dwell)
src/led.c/.h on-board LED status/heartbeat driver
src/third_party/TinyFrame/ TinyFrame framing library (Hurra protocol)
tools/ferrum_test.py protocol smoke harness
tools/ferrum_aim_test.py closed-loop aim test against on-screen dots
tools/ferrum_load_test.py command-channel latency/throughput/integrity load test
tools/humanization_analyze.py kinematic trace analyzer vs. a human baseline
```
8 changes: 8 additions & 0 deletions core/imxrt1062_mm.ld
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ SECTIONS
. = ALIGN(32);
} > RAM

/* USB EHCI relies on .dmabuffers being non-cacheable so a bare DSB (no
* cache clean/invalidate) is sufficient around qTD arming. core/startup.c
* marks only the first 64 KB of OCRAM (0x20200000) non-cacheable via MPU
* region 10. If .dmabuffers ever grows past 64 KB, the spillover becomes
* cacheable and DMA coherency silently breaks. Fail the build instead. */
ASSERT(SIZEOF(.bss.dma) <= 64K,
"ERROR: .dmabuffers exceeds the 64 KB non-cacheable MPU window (core/startup.c region 10). Widen the MPU region or shrink DMA buffers.")

.text.csf : {
FILL(0xFF)
. = ALIGN(1024);
Expand Down
38 changes: 32 additions & 6 deletions core/startup.c
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,39 @@ FLASHMEM uint32_t set_arm_clock(uint32_t frequency)
uint32_t cbcmr = CCM_CBCMR;

if (frequency > 528000000) {
// Step 1: Raise core voltage
// 600 MHz → TRG(15) ~1.25V; 816 MHz → TRG(19) ~1.30V
// Step 1: Raise core voltage BEFORE raising frequency.
//
// Voltage is interpolated the same way the Teensy core's clockspeed.c
// does it, instead of the old hand-tuned two-point table (TRG 15/19).
// That table under-volted everything above 600 MHz: it gave 816 MHz
// only ~1275 mV where the reference curve wants ~1425 mV, and at
// 912 MHz it would have produced ~1275 mV against a required ~1525 mV —
// a brownout hang under load. Formula:
// base 1250 mV at >528 MHz; above 600 MHz add 25 mV per 28 MHz step.
// Hard-capped at OC_MAX_VOLT_MV (1575 = silicon max, also the TRG-field
// ceiling) so no F_CPU can ever command an out-of-range TRG. Note NXP's
// *recommended* operating max is 1300 mV; steps above ~864 MHz exceed
// that and trade silicon lifetime for clock — intentional here.
// TRG encodes (mV-800)/25: 1250→18, 1425→25, 1525→29, 1575→31.
// Local consts (not #define) so these identifiers don't leak to file
// scope for the rest of startup.c — they're only used in this block.
const uint32_t oc_volt_step_hz = 28000000u;
const uint32_t oc_volt_step_mv = 25u;
const uint32_t oc_max_volt_mv = 1575u;
uint32_t voltage = 1250u; // >528 MHz base
if (frequency > 600000000u) {
voltage += ((frequency - 600000000u) / oc_volt_step_hz) * oc_volt_step_mv;
if (voltage > oc_max_volt_mv) voltage = oc_max_volt_mv;
}
uint32_t trg = (voltage - 800u) / 25u; // DCDC_REG3 TRG target step
uint32_t dcdc = DCDC_REG3;
dcdc &= ~DCDC_REG3_TRG_MASK;
dcdc |= DCDC_REG3_TRG(frequency > 600000000 ? 19 : 15);
DCDC_REG3 = dcdc;
while (!(DCDC_REG0 & DCDC_REG0_STS_DC_OK)) ;
// Only raise here; never lower (boot path goes low→high exactly once).
if ((dcdc & DCDC_REG3_TRG_MASK) < DCDC_REG3_TRG(trg)) {
dcdc &= ~DCDC_REG3_TRG_MASK;
dcdc |= DCDC_REG3_TRG(trg);
DCDC_REG3 = dcdc;
while (!(DCDC_REG0 & DCDC_REG0_STS_DC_OK)) ; // wait for rail to settle
}

// Step 2: Switch CPU to safe clock (PERIPH_CLK2) before touching ARM PLL.
// Use USB1 PLL (480 MHz) / 4 = 120 MHz as safe clock.
Expand Down
Loading
Loading