Passive USB 1.1 packet sniffer for RP2040 — built entirely on the PIO coprocessor.
Captures Low-Speed and Full-Speed USB traffic without interfering with the bus.
The host and device never know the sniffer exists.
⚠️ Legal DisclaimerThis project is provided strictly for educational purposes, authorized security research, and legitimate hardware auditing. It is intended to help security professionals, researchers, and enthusiasts understand USB HID protocol internals, PIO-based signal capture, and embedded cryptography.
You are solely responsible for ensuring that your use of this software complies with all applicable laws in your jurisdiction. Intercepting, recording, or monitoring keyboard input from devices you do not own — or without the explicit, informed consent of the device owner — is illegal in most jurisdictions and may violate statutes including, but not limited to:
- Brazil — Lei 12.737/2012, Art. 154-A (invasão de dispositivo informático)
- United States — Computer Fraud and Abuse Act (18 U.S.C. § 1030), Wiretap Act (18 U.S.C. § 2511)
- European Union — GDPR (Art. 5, 6), national implementations of the Computer Misuse Directive
- United Kingdom — Computer Misuse Act 1990
The author and contributors do not condone, encourage, or support the use of this tool for unauthorized surveillance, data theft, or any form of illegal activity. By using, downloading, or distributing this software, you acknowledge that:
- You will only use it on devices you own or have written authorization to test
- You assume all legal liability arising from your use
- The MIT License governs copyright and redistribution only — it does not grant permission to violate any law
If you are unsure whether your intended use is lawful, consult a qualified legal professional before proceeding.
| Feature | Details |
|---|---|
| 100% passive | High-impedance tap — zero electrical interference |
| Low-Speed + Full-Speed | 1.5 Mbps and 12 Mbps USB 1.1 |
| 8× oversampling | Robust clock recovery via edge-triggered phase alignment |
| DMA ping-pong | Zero-copy manual ping-pong — no sample loss |
| Full protocol decode | NRZI → bit unstuffing → framing → CRC5/CRC16 |
| Callback API | Receive structured USBPacket with PID, address, endpoint, payload |
| Dual-core ready | Capture on Core 1, process on Core 0 |
| Minimal resources | 1 PIO state machine, 1 DMA channel, ~17 KB RAM |
┌───────────┐ ┌────────────────┐ ┌──────────────────┐
│ PIO SM │────►│ DMA Ping-Pong │────►│ USBPacketDecoder│
│ in pins,2│ │ 2 × 8 KB │ │ │
│ 12 MHz │ │ (manual swap) │ │ Clock Recovery │
│ │ │ │ │ NRZI Decode │
│ D+ ─┐ │ │ │ │ Bit Unstuffing │
│ D- ─┘ │ │ │ │ Packet Parsing │
└───────────┘ └────────────────┘ │ CRC Verification│
└────────┬─────────┘
│
callback(USBPacket)
- PIO program (1 instruction in a wrap-loop) samples D+ and D- every clock cycle. 16 two-bit samples are packed into a 32-bit word and autopushed to the RX FIFO. Clock divider sets the 8× oversampling rate.
- DMA ping-pong transfers words from the FIFO into two 8 KB RAM buffers. A single channel with manual restart avoids the RP2040
chain_toWRITE_ADDR race condition that corrupts memory. - Software decoder implements edge-triggered clock recovery, SYNC detection, NRZI decoding, bit-unstuffing, EOP framing, PID validation, and CRC5/CRC16 verification. An all-J fast-path optimization skips idle bus words in O(1).
USB Cable (Device ↔ Host) Raspberry Pi Pico
┌────────────────────┐ ┌──────────────────┐
│ VBUS (red) │ │ │
│ D- (white) │───[100Ω]────►│ GP3 │
│ D+ (green) │───[100Ω]────►│ GP2 │
│ GND (black) │─────────────►│ GND │
└────────────────────┘ └──────────────────┘
⚠ Important:
- Series resistors (100 Ω) are mandatory — they prevent the Pico's GPIO from loading the USB differential impedance (~90 Ω).
- Do NOT connect VBUS (5V) to the Pico — power the Pico from a separate USB port or other supply.
- D+ and D- must be consecutive GPIOs — the PIO reads both pins in a single instruction.
- Download the latest release as a
.zipfile - In Arduino IDE: Sketch → Include Library → Add .ZIP Library…
- Select the downloaded file
Search for USBSnifferPIO_RP2040 in the Library Manager (Tools → Manage Libraries...).
Add to platformio.ini:
lib_deps =
https://github.com/angeloINTJ/USBSnifferPIO_RP2040.git#include <USBSnifferPIO_RP2040.h>
USBSnifferPIO sniffer;
void onPacket(const USBPacket& pkt) {
if (pkt.isHIDKeyboardReport()) {
Serial.printf("Mods=0x%02X Key=0x%02X\n",
pkt.data[0], pkt.data[2]);
}
}
void setup() { Serial.begin(115200); }
void setup1() { sniffer.begin(2); sniffer.onPacket(onPacket); }
void loop() { }
void loop1() { sniffer.task(); }| Example | Description |
|---|---|
basic_sniffer |
Minimal capture — prints every packet to Serial |
keyboard_logger |
HID keyboard reports with modifier decoding |
packet_analyzer |
Full protocol analyzer with hex dump and serial commands |
hid_filter |
Auto-discovery + address/endpoint filtering + JSON output |
statistics_monitor |
Real-time bus health dashboard with error rates |
diagnostic |
Layer-by-layer hardware diagnostic (GPIO → PIO → DMA → Decoder) |
| Method | Description |
|---|---|
bool begin(pin_dp, speed, pio_instance) |
Start capture. Returns false if resources unavailable. |
void end() |
Stop capture and release all hardware resources. |
void task() |
Process pending DMA buffers. Call frequently. |
void onPacket(callback) |
Register packet callback. |
USBPacketDecoder& decoder() |
Access decoder for statistics. |
bool isRunning() |
Check if capture is active. |
| Field | Type | Description |
|---|---|---|
timestamp_us |
uint32_t |
Capture timestamp (microseconds) |
type |
USBPacketType |
TOKEN, DATA, HANDSHAKE, SPECIAL, UNKNOWN |
pid |
uint8_t |
Packet Identifier (lower 4 bits) |
addr |
uint8_t |
Device address (token packets only) |
endp |
uint8_t |
Endpoint number (token packets only) |
frame_number |
uint16_t |
Frame number (SOF only) |
data[64] |
uint8_t[] |
Payload bytes (data packets only) |
data_length |
uint8_t |
Payload size excluding CRC |
crc_valid |
bool |
CRC verification result |
isHIDKeyboardReport() |
bool |
Heuristic: DATA0/DATA1 with 8 bytes |
Access via sniffer.decoder():
| Counter | Description |
|---|---|
packets_decoded |
Successfully decoded packets |
crc_errors |
CRC5/CRC16 mismatches |
sync_errors |
Malformed SYNC patterns |
stuffing_errors |
Bit-stuffing violations |
overflow_errors |
Packets exceeding maximum length |
USBSnifferPIO_RP2040/
├── src/
│ ├── USBSnifferPIO_RP2040.h # Top-level class (PIO + DMA + Decoder)
│ ├── USBSnifferPIO_RP2040.cpp # PIO/DMA setup, processing loop
│ ├── USBPacketDecoder.h # Decoder: clock recovery → packet parse
│ ├── USBPacketDecoder.cpp # Full decoding pipeline implementation
│ ├── USBProtocol.h # PIDs, CRC, USBPacket struct, enums
│ ├── usb_sniffer.pio # PIO assembly source (1 instruction)
│ └── usb_sniffer.pio.h # Pre-compiled PIO header + SM init
├── examples/
│ ├── basic_sniffer/ # Minimal capture
│ ├── keyboard_logger/ # HID keyboard monitor
│ ├── packet_analyzer/ # Full protocol analyzer
│ ├── hid_filter/ # Address/endpoint filtering + JSON
│ ├── statistics_monitor/ # Bus health dashboard
│ └── diagnostic/ # Hardware diagnostic
├── library.properties # Arduino Library Manager metadata
├── library.json # PlatformIO metadata
├── keywords.txt # Arduino IDE syntax highlighting
├── CONTRIBUTING.md # Contribution guidelines
├── LICENSE # MIT License
├── .gitignore
└── README.md
| Resource | Usage |
|---|---|
| PIO state machines | 1 (auto-claimed) |
| PIO instruction memory | 1 slot (of 32 per block) |
| DMA channels | 1 (manual ping-pong) |
| RAM | ~17 KB (2 × 8 KB buffers + decoder) |
| Flash | ~6 KB (code) |
| CPU | 0% during idle bus (fast-path optimization) |
- Board: Raspberry Pi Pico or Pico W
- Arduino Core: Earle Philhower arduino-pico 3.x+
- PIO: One free state machine on pio0 or pio1
- DMA: One free channel
- USB 1.1 only (Low-Speed and Full-Speed). USB 2.0 High-Speed (480 Mbps) is beyond the RP2040's PIO sampling rate.
- Full-Speed (12 Mbps) with 8× oversampling requires 96 MHz PIO clock. The divider of 1.25 at 120 MHz leaves minimal margin — consider 125 or 133 MHz system clock.
- The decoder is not interrupt-safe. Call
task()from a single core only. - Maximum payload per packet: 64 bytes (Full-Speed bulk/interrupt endpoint limit).
Q: Can I use this with NeoPixels / WS2812?
A: Yes. NeoPixel libraries typically use pio0 SM0. USBSnifferPIO auto-claims the next free SM. If pio0 is full, pass pio_instance=1 to begin().
Q: Does this work on the Pico W?
A: Yes. The Pico W uses pio1 SM0 for the CYW43 Wi-Fi driver. Using pio_instance=1 will auto-claim SM1 (leaving SM0 for Wi-Fi). Or use pio_instance=0 to avoid pio1 entirely.
Q: Why not use the RP2040's native USB peripheral? A: The native USB peripheral can only act as a host or device — it cannot passively observe a bus it is not participating in. PIO-based sampling taps the raw differential lines without electrical interference.
Q: What about the Pico 2 (RP2350)? A: The RP2350 has PIO v2 with the same instruction set. This library should work without changes, but has not been tested yet.
RP2040 GPIO 2 (D+) ──── 100Ω ──── USB cable D+ (green)
RP2040 GPIO 3 (D-) ──── 100Ω ──── USB cable D- (white)
RP2040 GND ────────────────────── USB cable GND (black)
Do NOT connect VBUS. Power the Pico from its own USB port or an external supply.
Contributions are welcome! Please read CONTRIBUTING.md before submitting a pull request.
- Fork this repository
- Create a feature branch:
git checkout -b feature/my-improvement - Commit your changes:
git commit -m "Add: description of change" - Push to the branch:
git push origin feature/my-improvement - Open a Pull Request
MIT License — see LICENSE.
- Raspberry Pi Foundation for the RP2040 PIO architecture
- The Arduino-Pico community for the RP2040 Arduino core (Earle Philhower)
- The embedded community for feedback on PIO-based USB analysis techniques
- DHT22PIO_RP2040 — PIO-accelerated DHT22 library by the same author