Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ch32-tapioca

A sub-€1 USB logic analyzer & protocol driver for 1- and 2-wire buses, built on the CH32X035 PIOC.

Taps a logic-side bus signal and decodes it live in the browser. Tested on CAN, DMX & MDIO. Throughput supports ~1 Mbps (non-clocked) / ~3 MHz (clocked). A separate MDIO master build target can actively drive Clause-22 and MMD register reads/writes over the same USB-CDC plumbing.

CAN live IDs dashboard view
CAN live ID view
CAN trace dashboard view
CAN trace view
MDIO dashboard view
MDIO register/MMD view
DMX512 dashboard view
DMX512 universe view

Disclaimer: this is an experimental / research project. The goal is to be genuinely useable, but treat it as such.

Why

The whole thing rides on one cheap part: the WCH CH32X035, a 48 MHz RISC-V MCU:

  • Dirt cheap (≤ €0.25)
  • Needs almost no external components (built-in oscillator)
  • Tiny QFN 3x3 package
  • Native USB 2.0 FS (12 Mbps)
  • And unusually at this price, a PIOC: a minimal coprocessor that can follow a bus edge-by-edge while the CPU does the housekeeping and data transmission.

It started as a need: an ultra low-cost embedded USB↔MDIO bridge to monitor/configure the Ethernet PHYs in an SPE media converter (two PHYs back-to-back). The PIOC turned out to be the perfect peripheral to passively follow an external clock, drive one, and capture/send data frames with tight timing, so the question became: can it be turned into a general-purpose logic tool?

The PIOC is comparable to the RPi Pico's PIO, but:

  • it's a full little RISC8 core (more capable per instruction)…
  • …and also more constrained: 48 MHz, a single core (no parallel state machines), only 2 IO pins, and ~30 bytes of register file shared with the CPU.
  • it's programmed in its own assembly, with sparse docs and a Windows-only vendor toolchain (this repo ships a small Python assembler so you don't strictly need it).

So this is mostly a real-time engineering exercise: how much useful capture can we wring out of that?

Reference dev board used for testing: WeAct Studio CH32X035 Core Board (< €2). The project builds with PlatformIO using the ch32v platform / noneos-sdk framework.

Related project: ch32-tapioca-probe, using the same approach to build an embedded debug/flashing probe (JTAG, ARM & CH32).

Sniffer

Working principle

  bus line
     │
     ▼
    PIOC    sample the line at a high, repeatable rate;
  (coproc)  pack the result into a shared FIFO
     │
     ▼
    CPU     drain the FIFO into a RAM ring → stream out
     │
     ▼
  USB-CDC   one binary stream (up to ~500 KB/s)
     │
     ▼
    host    browser decodes live; Python decodes saved
            captures, recovering the clock from the data

Two capture modes, picked at runtime (the host sends !mode rle|clocked):

mode for what the PIOC streams
clocked buses with a clock line (MDIO, SPI…) the data line sampled on each clock edge → logic levels
rle unclocked / asynchronous NRZ buses (CAN, DMX, LIN, UART…) RLE = run-length encoding: how many ticks the line stayed at a level (host recovers the clock from the data)

All protocol decoding happens host-side, so the same capture is protocol-agnostic. There's no kernel driver, decoding is a userspace lib in JS (browser) and Python, plus the shared C++ codec headers.

Numbers:

  • Host throughput caps at ~500 KB/s (CPU-bound on the drain↔USB path).
  • Holds classic CAN @ 1 Mbps (real bus): ~8000 frames/s of typical traffic, and 6000 frames/s worst case (a 0101… payload : transition every bit, shortest bit duration).
  • Timing resolution ±100 ns.
  • Clocked capture is clean end-to-end to ~3 MHz; a real MDIO bus runs ~1.5 MHz. (The clocked PIOC blob has passed isolated SPI-loopback tests at 6 MHz; ~3 MHz is the conservative product ceiling once CPU drain, USB framing and host decoding are included.)

Setup

  • Board: genericCH32X035F8U6 (QFN20). CH32X035 = 48 MHz RISC-V, USB-FS device.
  • Tap pins (logic-level side of the bus, not the differential CAN/RS485 pair):
    • data → PC19 (PIOC IO1, used in both modes)
    • clock → PC18 (PIOC IO0, clocked mode only)
    • common ground
    • for CAN/DMX, put the usual transceiver in front and tap the RXD/RO logic output
  • USB-CDC is on PC16/PC17. LED on PB12 (heartbeat).
  • Flashing: USB bootloader only. The PIOC uses PC18/PC19, which are also the chip's SWD/SDI debug pins (DIO/CLK), so SWD debug/flash are unavailable. Flash over the USB bootloader instead:
    1. plug the board in while holding the BOOT button, and it enumerates as the WCH bootloader
    2. pio run -t upload (upload_protocol = isp) detects and flashes it, no debug probe

Build & run

Firmware (PlatformIO):

pio run -e sniffer -t upload  # the product: both modes + runtime !mode switch

Then decode it in the browser, Chrome/Edge (Web Serial). The dashboard handles both capture modes and auto-detects the active one:

open app/web-sniffer/index.html  # then click "Connect" and pick the serial port

scripts/ holds offline Python decoders (capture a file, then decode it) + a throughput probe, for CLI/CI, not live (the browser app is the realtime decoder).

Main build environments (platformio.ini):

env what
sniffer (default) the product: both datapaths + runtime !mode rle|clocked
rle_sniffer RLE datapath only, with DIAG telemetry (re-validate a bus)
clocked_sniffer clocked datapath only, with DIAG telemetry

Host tests: bash app/test/run_all.sh - the codec/framing/mode tests need Node + a C++ compiler; the dashboard smoke test additionally needs Chrome (Web Serial / CDP). No hardware required.

Protocols tested

protocol kind notes / test setup
SPI clocked the dev loopback: SPI1 generates known waveforms (PA5/PA7 jumpered to the tap pins) to sanity-check capture without an external bus
MDIO sniffing clocked the original use case: watch Linux monitor/control Ethernet PHYs. Sniffed on a USB↔SPE (10BASE-T1L) dongle, full Clause-22 + MMD decode
CAN (classic, 1 Mbps) RLE a very cheap CAN analyzer. Tested against an ESP32 (TWAI) and a real bus with QDD motors through an SN65HVD230 CAN PHY
DMX (250 kbps) RLE handy DMX-line debugger. Tested against an ESP32 (EZDMX) and real lighting drivers over an RS485 transceiver

USB wire protocol and capture pipeline

Tapioca uses one USB-CDC link for both control and capture data. The host sends plain-text control commands such as !mode rle / !mode clocked; the device streams a compact binary format back to the host.

The short version:

mode stream shape used for
rle raw run-length bytes plus sentinels CAN, DMX, LIN/UART-style asynchronous buses
clocked COBS-0xFF framed sampled-burst records MDIO, SPI-style externally clocked buses

The firmware deliberately keeps protocol decode host-side. The device captures edges/samples, preserves timing, reports loss boundaries, and lets the browser or scripts decode CAN/DMX/MDIO/etc.

For the full byte-level format, examples, diagnostics, and the internal PIOC → RAM ring → USB pipeline, see docs/sniffer-usb-wire-api.md.

Engineering notes

Bring-up notes, real-time constraints and the less obvious PIOC/USB traps are collected in docs/sniffer-tips-and-gotchas.md.

MDIO driver

This project also features a separate MDIO master driver build target gated behind -D RUN_MDIO_MASTER. This is not a mode of the generic sniffer and it does not use the browser dashboard.

The split is deliberate: MDIO requires the firmware to drive the preamble/address, release data line exactly at turnaround, keep clock running, sample the PHY response at the right phase, and detect a missing PHY from the TA bit. That makes a protocol-specific driver the right shape.

The user interface is mdioctl, a small Python CLI modelled after phytool: same read / write / print style, without the Linux interface field because the selected USB-CDC device itself is the interface. But the driver talks plaintext ASCII so it's also useable standalone.

PlatformIO environment : mdio_master (mdio_master_stub allows testing the ASCII path without a PIOC) The same phytool-style syntax supports Clause-22 (phy/reg) and MMD indirect access (phy:mmd/reg), including mdioctl print --mmd <mmd>.

Details: docs/mdio-master.md.

Layout

path what
src/ firmware, one main_*.cpp per use case: main_sniffer, main_mdio, main_tick_test (each gated on its env's RUN_* flag, so exactly one defines main() per build)
src/common/ shared board layer: USB-CDC + LED instances, clock/timebase bring-up, printf-over-CDC (board.*)
src/sniffer/ the two datapaths (RleSniffer, ClockedSniffer) + wire helpers (mode_command, record_framer)
src/mdio/ active MDIO master (Mdio::Master) + USB ASCII bridge (Mdio::UsbBridge)
src/{usb,hal,util}/ USB-CDC, SDK + ISRs, ring/cobs/led/spi-gen
pioc/ PIOC assembly files & binary blobs + assemble.py (a small native assembler)
ldscript/ custom linker (reserves the top 4 KB of RAM for the PIOC program ROM)
app/web-sniffer/ the Web Serial browser dashboard (single-file index.html + its inline.mjs builder)
app/mdioctl/ phytool-style CLI for the MDIO master firmware
app/test/ the shared Node/C++ host test suite + codec sources of truth
scripts/ offline Python decoders (CLI/CI) + throughput probe + diag_monitor
docs/ detailed notes and screenshots used by the README

About

Ultra low-cost experimental USB logic analyzer up to 1 Mbps / 3 MHz & protocol driver based on CH32X035 (PIOC)

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages