BENCH ONLINE · 13.7°C · 24V RAIL STABLE REV 2.0 · BUILD 2026.05 · NODE/LUNDGREN-01
← /bench REVIEW · CAN · BENCH-VERIFIED · 84,059 FRAMES EACH
BENCH-TESTED 250 KBIT/S CAN BUS COMPARISON ~ 12 MIN READ

I ran the same 50-second saturation flood through a $50 Pi CAN HAT and a $500 Kvaser. Both captured every frame.

Side-by-side bench comparison of a Waveshare 2-Channel Isolated CAN HAT against a Kvaser Leaf Light v2 on the same 250 kbit/s twisted pair. Same DBC, same injector, same flood — 84,059 frames captured by each interface, zero drops. The real differences show up where you'd expect: timestamp precision and host CPU load.

PUBLISHED2026-05-18 REV AInitial bench session AUTHORJ. LUNDGREN
[ 00 ]

The bench

Both interfaces sharing one twisted pair at 250 kbit/s. Same bus, same flood, same fixture. Every data point in this article was captured by the two devices in the photo below, running off the wires between them.

Bench overview shot on a black self-healing cutting mat: Kvaser Leaf Light v2 USB dongle at top (PWR LED lit green) with a short DB9 pigtail, and a Waveshare 2-Channel Isolated CAN HAT mounted on a Raspberry Pi in a red case at bottom-right. Red, green, and brown jumpers carry CAN-H, CAN-L, and ground between them — the entire physical bus.
// BENCH · OVERVIEW The whole comparison on one cutting mat: Kvaser Leaf Light v2 (top), Waveshare 2-Channel CAN HAT on a Pi 3A+ (bottom-right), three twisted jumpers between them carrying CAN-H, CAN-L, and ground. 120 Ω terminators at each end, both PWR LEDs green.

The $50 side, up close — two MCP2515 controllers, an isolated SPI digital isolator (NS787 / ISO1042 in the middle), a B0505LS-1WR3 isolated DC-DC for the bus side, and two Phoenix-style terminal blocks labelled CAN0 / CAN1. The yellow jumpers at the corners are the on-board 120 Ω terminator-enable shunts.

Macro top-down shot of the Waveshare 2-Channel Isolated CAN HAT board. Two MCP2515 SPI-CAN controllers visible, a center ISO1042-class digital isolator, two transceiver ICs below it, the B0505LS isolated DC-DC module on the left edge, and green Phoenix terminal blocks at the bottom labelled CAN0 (with a green jumper plugged in) and CAN1. Yellow 120R termination jumpers on each side.
// BENCH · CAN HAT SILICON + PINOUT The $50 of silicon doing the work on the Pi side. MCP2515 ×2, ISO1042-class digital isolator, B0505LS isolated DC-DC, on-board 120 Ω termination jumpers. Two electrically-isolated CAN channels for a list price under $30 of parts. GPIO header pin labels visible along the right edge — 5V, GND, MISO, MOSI, SCK, CS0, CS1, INT0, INT1 — click the image for a readable pinout view.

And the other end of why this matters — the Kvaser's DB9, the marine-side M12 connector family it has to play nicely with, and a discrete 120 Ω terminator across H/L on the Kvaser DB9 since the Leaf Light v2 doesn't have a switchable on-dongle terminator the way the HAT does.

Kvaser Leaf Light v2 dongle (bottom-left, PWR LED green) with its DB9 connector terminator adapter, and an M12 right-angle elbow + straight M12 field connector at top-right — the NMEA 2000 / DeviceNet bus-side connectors a CAN logger on a boat eventually has to land on.
// BENCH · KVASER + MARINE BUS-SIDE Kvaser with its DB9 → 120 Ω terminator adapter, and a pair of M12 NMEA 2000 / DeviceNet connectors (right-angle elbow + straight) for scale. The lab bench above is a stand-in for the boat bench: same 250 kbit/s, same arbitration, different physical layer at the end of the wire.
> WHY THIS SETUP Two interfaces, one shared twisted pair, ground bonded between the two host computers. That's the whole rig. Everything that follows — the rate sweep, the flood, the jitter histogram, the CPU plot — comes from this fixture, with each dongle reading the same wire at the same time.
[ 01 ]

Test rig

Three nodes share one twisted pair at 250 kbit/s. The Pi HAT's can1 channel acts as the synthetic injector, the HAT's can0 channel is one capturer (Linux SocketCAN, python-can), and the Kvaser Leaf Light v2 is the other capturer (Windows, python-can over Kvaser CANLIB). 120 Ω terminators at each physical end. Common ground jumpered between hosts.

ItemDetail
Bus rate250 kbit/s · standard 11-bit ID · 8-byte data frames
InjectorPi 3A+ · python-can over SocketCAN · single ID 0x123 · 32-bit seq in data[0:4]
Capturer APi 3A+ · SocketCAN · candump-equivalent loop
Capturer BWindows · python-can kvaser backend · CANLIB driver
Run length30 s per rate point · 7 s burst window · 50 s saturation flood
Frame count3,000 to 142,000 transmitted per point
> BENCH.NOTE: Every frame carries a 32-bit sequence number in the data field — drops are detectable as gaps in the seq counter, not just frame-count deltas.
[ 02 ]

Frame capture fidelity vs bus load

Percentage of injected frames captured by each interface across target injection rates from 100 to 8000 fps, log-x axis. Both interfaces hold 100% capture up to ~1000 fps; above that the bus saturates and the python injector's user-space queue becomes the limiter, not the dongles.
// PLOT · 01 · RATE CURVE Percentage of injected frames captured by each interface, by target injection rate (log-x). 100% means every transmitted frame appeared in the capture log.
> FINDING At realistic monitoring loads — up to ~1000 fps, roughly 50% of the bus — both interfaces capture 100% of injected frames. The cheap HAT is not silently dropping anything in the regime most NMEA 2000 / OBD-II applications operate in.

Above 1000 fps the bus itself saturates, so a "drop" measured here is really an injector-side queue limit, not a dongle limitation (see the throughput chart below).

[ 03 ]

Throughput plateau — where the bus runs out

Captured frames-per-second versus target injection rate for both interfaces, log-log axes, with the bus-saturation plateau visible around 1500-1700 fps. The injector's own TX rate is plotted for reference and tops out at the same plateau, confirming it's the bus running out of slots, not either capturer falling behind.
// PLOT · 05 · THROUGHPUT Captured frames per second on each interface, including the injector's actual TX rate. Plateau marks the 250 kbit/s bus saturation limit — neither interface dropped frames, the bus simply could not carry more.

At 250 kbit/s a standard 8-byte CAN frame takes ~440 µs on the wire (SOF + 11-bit ID + control + 64 data bits + 15-bit CRC + ACK + EOF). The ~1500 fps plateau is exactly what the bus math predicts under heavy back-to-back arbitration with normal inter-frame spacing.

[ 04 ]

Burst absorption — both at 100%

Grouped bar chart of percentage frames captured for back-to-back burst sizes of 32, 256, 1024, and 4096 frames. Both interfaces register 100% across every burst size.
// PLOT · 03 · BURST ABSORPTION Both interfaces captured 100% of frames that made it onto the wire during back-to-back bursts of 32 / 256 / 1024 / 4096 frames.
> NON-FINDING (KEPT FOR COMPLETENESS) Neither receive buffer was the bottleneck. The Python injector's user-space loop paced the bus too slowly to flood either dongle's input ring — both captured every frame that hit the wire. A flooded bus from a microcontroller or a Kvaser-side injector would be the next step here.
[ 05 ]

Sustained bus saturation — 50-second cangen flood

Per-second captured frames count for both interfaces during a 50-second cangen flood at 250 kbit/s. Both lines pin to the same ~1681 fps band from start of flood to end, with no drift, no buffer-overflow drops, and identical totals.
// PLOT · 06 · FLOOD STABILITY Per-second captured-frames count during a 50-second flood from cangen -g 0 -i on can1. The amber band marks the injector window. Bus is fully saturated at ~1681 fps for the entire duration.
> HEADLINE FINDING At true 250 kbit/s bus saturation, both interfaces captured exactly the same number of frames (84,059) — every single frame the bus carried. Neither receive buffer overflowed, neither dongle fell behind. The earlier "drops" in the rate-curve test were the Python user-space injector hitting its own queue limit, not a capturer limitation.

This is the clean answer to "can either dongle keep up with a busy bus": yes, both can. At 250 kbit/s, neither hardware path is the bottleneck.

[ 06 ]

Timestamp jitter — first place Kvaser pulls ahead

Log-y inter-arrival-time histogram for both interfaces when the injector emits a frame every 1.000 ms. Kvaser distribution clusters tightly around the 1.00 ms target with σ ≈ 0.374 ms. The Pi HAT distribution is visibly wider with σ ≈ 0.530 ms and a secondary mode near 0.6 ms attributable to the MCP2515 SPI polling cycle quantizing arrival timestamps in the kernel rather than at the controller.
// PLOT · 02 · JITTER Log-y histogram of measured inter-arrival times when the injector emits a frame every 1.000 ms. Tighter distribution around the 1.00 ms target means more precise per-frame timestamping.
> WHERE THE $500 EARNS ITS KEEP (PART 1) Kvaser timestamps cluster tightly around the 1.00 ms target (σ ≈ 0.374 ms, hardware-side timestamping on the USB ISO endpoint). The HAT shows a wider distribution (σ ≈ 0.530 ms) with a visible secondary mode near 0.6 ms — the MCP2515's SPI polling cycle quantizes arrival timestamps in the kernel, not at the controller.

For raw bus sniffing this doesn't matter. For timing-sensitive reconstruction — latency analysis, message-order forensics, gateway benchmarking — it does.

[ 07 ]

Host CPU — the headline differentiator

Median capturer-process CPU percentage versus capture rate on a log-x axis. Kvaser stays under 6% across the full sweep up to bus saturation. The Pi HAT's MCP2515-on-SPI driver climbs steeply, reaching ~51% of one Pi 3A+ core at 2 kfps.
// PLOT · 04 · CPU LOAD Median process CPU percentage of the capturing host (Python capture loop) at sustained capture rates.
> WHERE THE $500 EARNS ITS KEEP (PART 2) The headline divergence. The MCP2515-on-SPI driver consumes ~10× the host CPU of the Kvaser USB pipe at saturated bus rates — the HAT hits ~51% of one Pi core, the Kvaser sits under 6% on a Windows i-series. Each MCP2515 interrupt requires a full SPI burst to read the receive register, while Kvaser's USB ISO endpoint delivers batched frames in a single bulk transfer.

For a Pi running just a CAN sniffer this isn't a problem; for a Pi running anything else alongside, the headroom difference is real.

[ 08 ]

Verdict — where the differences actually appear

Factor Pi HAT $50 Kvaser Leaf Light v2 ~$500
Frame capture at realistic loads (≤ 1 kfps)100%100%
Frame capture at saturated busBus-limited (84,059 / 84,059)Bus-limited (84,059 / 84,059)
Timestamp jitter (σ at 1 ms periodic)0.530 ms0.374 ms
Host CPU at sustained 2 kfps~51% (one Pi 3A+ core)~5% (Windows i-series)
Channels2 isolated1
Galvanic isolationYes (opto + DC-DC)No
Driver stackLinux SocketCAN (in-tree)Vendor CANLIB (Win/Linux)
Listen-only modeYesYes

For an evening of bus sniffing on a properly-terminated 250 kbit/s backbone, the $50 HAT is genuinely competitive with a Kvaser Leaf Light v2: same captured frames, same percentage at every realistic load, and the HAT throws in galvanic isolation and a second channel that the Leaf Light doesn't have. The Kvaser pulls ahead on two things the bench can measure: tighter timestamps and ~10× less host CPU.

If either of those matters to your workflow — high-precision timing, or running other software alongside the sniffer on a constrained host — the price gap maps to real value. If neither does, the HAT is the more interesting hardware on the bench.

> BENCH.VERDICT: Buy the Pi HAT for $50. If your job involves timing-sensitive CAN analysis or you can't burn 50% of a host CPU on the sniffer, also buy the Kvaser. They cover different jobs at this price tier — not "cheap vs real," more "bench-friendly vs production-friendly."
[ 09 ]

Caveats — what this bench did not test

  • Bus rate fixed at 250 kbit/s. Both interfaces support 1 Mbit/s; the test rate matches the most common automotive / marine / industrial deployment but high-speed buses may differentiate the dongles more.
  • Hosts differ. Kvaser ran on a Windows i-series laptop, HAT on a Pi 3A+. CPU comparison is apples-to-oranges in absolute terms; the qualitative gap (one driver consumes orders of magnitude more host time per frame than the other) holds regardless.
  • Entry-level Kvaser only. USBcan Pro 2xHS v2 and equivalent pro-grade hardware were not tested. Leaf Light v2 specifically does not have galvanic isolation; higher-end Kvasers do.
  • Synthetic single-ID load. Real CAN traffic involves many IDs, varying priorities, and arbitration losses. Behaviour under multi-priority traffic was not benchmarked.
  • Python user-space injector limits. The rate-sweep "drops" above 1 kfps were Python user-space queue limits, not dongle limits. The saturation flood (cangen) is the authoritative high-load result.

Coming in Part 2: 12-hour clock-drift soak, multi-ID mixed traffic, 1 Mbit/s flood, listen-only ACK verification on a scope.

[ > ]

Try it yourself — Pi setup + starter scripts

Whole bench is two Python files and three ip link commands. The HAT itself is plug-and-go — drop it on the GPIO header, add the MCP2515 device-tree overlays, bring the interfaces up, run the scripts. No Pi-side driver install, no kernel rebuild. github.com/joelundgren/circuitessentials-can-bench has both scripts plus a README.

1. Enable SPI + MCP2515 overlays in /boot/firmware/config.txt (older Pi OS images use /boot/config.txt):

dtparam=spi=on
dtoverlay=mcp2515-can0,oscillator=16000000,interrupt=25
dtoverlay=mcp2515-can1,oscillator=16000000,interrupt=23

2. Reboot, then bring both interfaces up at 250 kbit/s:

sudo ip link set can0 up type can bitrate 250000
sudo ip link set can1 up type can bitrate 250000
ip -br link show can0 can1

The two yellow shunts on the HAT itself enable the on-board 120 Ω terminators (one per channel). Set both to ON when there are only two nodes on the bus — which is the case for this bench. Pinout for the GPIO header sits on the Amazon product page if you need to confirm which pin goes where; the overlays above match the stock Waveshare layout.

3. Capture every frame on can0 for 30 seconds:

python3 capture.py --backend socketcan --interface can0 --duration 30 --out cap.csv

4. From a second node (or the HAT's other channel), inject at a controlled rate:

python3 inject.py --interface can1 --rate 1000 --duration 30 --out tx.csv

Every frame carries a 32-bit sequence number in data[0:4] — diffing the seq counters in cap.csv against tx.csv is how the article above measured "did anything get dropped." For a real bus-saturation flood, use cangen from can-utils (sudo apt install can-utils) — its kernel-rate TX path beats anything the python-can user-space loop can drive:

cangen can1 -g 0 -I 123 -L 8 -D i -i

The same capture.py works against the Kvaser with --backend kvaser --interface 0 — channel index instead of a SocketCAN name, otherwise identical. CANLIB has to be installed on the host first.

> REPO: github.com/joelundgren/circuitessentials-can-bench — MIT-licensed, ~200 lines total, no framework. Clone, install python-can + psutil, run.
// FURTHER READING CAN BUS DEEPER

A bench session answers what one rig does. The references below answer why, and they pay dividends when something on the bus does not behave the way you expected. Affiliate links route through Bookshop.org.

A Comprehensible Guide to Controller Area Network

Wilfried Voss

CAN from arbitration through fault confinement, from a longtime industry consultant. Clearest practitioner-oriented intro I've seen for engineers coming from a SPI/I2C background.

Bookshop

High-Speed Digital Design: A Handbook of Black Magic

Howard Johnson & Martin Graham

CAN runs on a differential pair, and pairs misbehave the moment you treat them like logic. Johnson on return paths and terminations explains the bus-error counter spikes you see in the lab.

Bookshop

Making Embedded Systems

Elecia White

If you are wiring the CAN HAT into a real product rather than a bench rig, White's chapters on drivers, queues, and bring-up methodology save weeks of pain.

Bookshop

Full reading list across embedded, PCB, signal integrity, firmware, and protocols on the /library page.

// AFFILIATE DISCLOSURE

The Amazon link earns a small commission at no extra cost to you. The Kvaser link goes to a distributor (no commission). Book links above route through Bookshop.org, which supports independent bookstores and pays the site a slightly higher commission than Amazon. Both CAN interfaces are on this bench right now — the comparison data came from a single bench session with both running side-by-side. Link choices follow the evidence, not the kickback.


Want the full bench? See /bench →