GPIB vs USB-TMC for High-Throughput Instrument Polling
When a rig must sustain thousands of FETC? transactions per second, the choice between GPIB/IEEE-488 and USB-TMC decides whether the aggregate poll rate is bounded by a shared 1 MB/s bus or by a per-device pipe running at 10–40 MB/s. This guide measures the two head to head — effective throughput, latency floor, multi-device scaling, and SRQ versus USB interrupt-in event notification — as a drill-down of Interface Selection and Concurrency inside the Serial, USB, and GPIB Communication Workflows area. It picks up exactly where that guide’s decision table stops: both transports clear the coarse selection filter, and the winner is now set by contention geometry, not by a ceiling in a spec sheet.
Where the Two Transports Actually Diverge
Both interfaces reach instruments through a VISA Resource Manager, speak IEEE-488.2 status semantics (the Status Byte, the Message Available bit), and present a nearly identical pyvisa surface — open_resource, query, read_stb. The divergence is physical. GPIB is a single shared 8-bit parallel bus: one controller-in-charge addresses one talker and one or more listeners at a time, and the three-wire handshake (DAV/NRFD/NDAC) means every byte moves at the speed of the slowest participant currently on the bus. USB-TMC is a switched star: each instrument owns a bulk-IN and bulk-OUT endpoint pair, and the host controller schedules micro-frames independently per device until an upstream hub becomes the shared segment.
That difference is decisive under sustained polling. On GPIB, adding a second active talker does not add bandwidth — it subtracts it, because the bus rate is divided across whoever holds it, and each addressing change costs a settling interval (T1, typically 350–1100 ns, longer with capacitive cable loading). On USB-TMC the second device runs on its own pipe at near-line-rate, and contention appears only when both devices sit behind one transaction translator. The comparison is therefore not “which is faster” but “how does effective per-device rate degrade as device count rises,” which is a scaling question the parent guide’s single-row-per-transport table cannot express.
Head-to-Head: Throughput, Latency, and Event Notification
The figures below are practical sustained values for typical mid-range instrument firmware (a benchtop DMM or a mid-tier scope), not bus-theoretical maxima. Read the last two rows carefully: they are where a design that looked fine on a single device falls apart on a rack.
| Property | GPIB / IEEE-488 (488.2, HS488) | USB-TMC |
|---|---|---|
| Raw bus rate | ~1 MB/s (488.1 handshake); HS488 up to ~8 MB/s if all devices support it | 10–40 MB/s (USB 2.0 High-Speed bulk) |
Effective single-device FETC? rate |
400–1500 txn/s (settling + handshake bound) | 1500–6000 txn/s (URB round-trip bound) |
| Latency floor per transaction | 0.5–5 ms incl. address settling T1 |
0.2–3 ms incl. one bulk-OUT + bulk-IN URB pair |
| Bandwidth model | Shared — divided across active talkers | Per-device — until a hub segment is shared |
| Devices per controller | 15 (electrical load limit) | 127 (address space); hub-limited in practice |
| Event notification | SRQ line asserted → serial-poll to find requester | Interrupt-IN READ_STATUS_BYTE / poll *STB? |
| Async recovery primitive | IFC (interface clear), DCL/SDC device-clear |
INITIATE_CLEAR / CHECK_CLEAR_STATUS on the endpoint |
| Scaling failure signature | One slow talker starves the whole bus | Hub bandwidth halves when two streams share a translator |
The single-device rows favour USB-TMC by roughly 3–4×, and for raw waveform streaming (megabyte records) the gap widens further because HS488 negotiation collapses to the slowest device’s capability. But GPIB’s SRQ model is genuinely superior for sparse event-driven acquisition: one hardware line signals “some device needs service” across all 15 instruments, and a serial-poll sweep locates the requester in a handful of transactions — no per-device polling of *STB? at all. USB-TMC’s interrupt-IN endpoint carries the same status byte but is polled per device by the host controller, so a large idle rack costs more baseline URB traffic. The transport that wins depends on whether your workload is dense continuous polling (USB-TMC) or sparse asynchronous notification across many instruments (GPIB SRQ).
Modelling Aggregate Throughput and the Contention Factor
The quantity that actually matters for a polling array is aggregate transaction throughput across the rig, and it behaves differently for the two topologies. For GPIB, the shared bus divides its effective rate across the number of active talkers in the poll cycle, and each addressing change adds settling overhead. Let R_bus be the effective bus rate, S the mean bytes moved per transaction, t_set the address-settling time, and k the number of active talkers sharing the bus:
The k in the denominator is the shared-bus contention factor: effective per-device rate is R_bus / k, so eight active talkers each see one-eighth of the bus, and the settling term R_bus·t_set becomes dominant once S is small (single-float FETC? replies). USB-TMC has no such divisor until a hub is saturated — each device runs its own pipe, so aggregate rate is the per-device rate summed over N devices, capped by the upstream segment bandwidth R_up:
Below the R_up knee the sum scales linearly with device count; above it the hub segment clamps every device to a shared slice — the USB analogue of GPIB’s k divisor, but reached only under waveform-scale payloads rather than on the first added meter. The design lesson is that GPIB is contention-bound from the second device while USB-TMC is contention-bound only past a payload threshold, and that threshold is what you must measure.
GPIB serializes every transaction on one shared rail so the effective per-device rate falls as 1/k; USB-TMC gives each instrument its own pipe and only contends at a shared hub segment under large payloads.
A Transport-Agnostic Benchmarking Poll Loop
Because both transports present the same pyvisa resource interface, one instrumented loop can benchmark either backend and report the effective Hz and per-transaction latency directly. The loop below issues a command repeatedly, records each round-trip with a monotonic clock, and supports two event-notification strategies: stb_poll reads the Status Byte after every transaction (the USB-TMC-friendly per-device path), while srq_wait blocks on the GPIB service-request line and serial-polls only when it fires (the sparse-event path). It never blocks unboundedly — every wait carries a VISA timeout so a wedged bus surfaces as a classifiable fault rather than a hang.
from __future__ import annotations
import statistics
import time
from dataclasses import dataclass
from enum import Enum
from typing import Callable
import pyvisa
from pyvisa.constants import StatusCode, VI_GPIB_REN_ADDRESS_GTL # noqa: F401
from pyvisa.errors import VisaIOError
class EventMode(str, Enum):
STB_POLL = "stb_poll" # read *STB? / status byte every cycle (USB-TMC)
SRQ_WAIT = "srq_wait" # block on SRQ line, serial-poll on assert (GPIB)
@dataclass(slots=True, frozen=True)
class PollStats:
"""Result of a benchmarking poll run."""
transactions: int
elapsed_s: float
mav_bit: int # 0x10 set = Message Available seen on last read
@property
def effective_hz(self) -> float:
return self.transactions / self.elapsed_s if self.elapsed_s else 0.0
def poll_loop(
resource: pyvisa.resources.MessageBasedResource,
command: str,
count: int,
mode: EventMode = EventMode.STB_POLL,
srq_timeout_ms: int = 200,
on_latency: Callable[[float], None] | None = None,
) -> PollStats:
"""Benchmark sustained polling of one instrument over GPIB or USB-TMC.
Issues ``command`` (e.g. ``"FETC?"`` or ``"*IDN?"``) ``count`` times,
measuring per-transaction latency and effective throughput. In STB_POLL
mode the Status Byte is read each cycle; in SRQ_WAIT mode the loop blocks
on the service-request line and serial-polls only when it asserts, which
is efficient for sparse GPIB event notification but wrong for dense polling.
Args:
resource: An opened VISA MessageBasedResource (GPIB or USB-TMC).
command: SCPI query to benchmark.
count: Number of transactions to run.
mode: Event-notification strategy to exercise.
srq_timeout_ms: Per-wait ceiling for SRQ_WAIT so a lost SRQ is bounded.
on_latency: Optional per-transaction latency sink (seconds).
Returns:
PollStats with transaction count, wall-clock elapsed time, and the
last-seen MAV bit for validation.
Raises:
VisaIOError: Propagated on transport faults for upstream classification.
"""
latencies: list[float] = []
last_stb = 0
resource.timeout = max(srq_timeout_ms, int(resource.timeout or 0), 1000)
if mode is EventMode.SRQ_WAIT:
resource.write("*SRE 16") # request service when MAV (bit 4) sets
start = time.monotonic()
for _ in range(count):
t0 = time.monotonic()
resource.write(command)
if mode is EventMode.SRQ_WAIT:
try:
resource.wait_on_event(
pyvisa.constants.EventType.service_request, srq_timeout_ms
)
except VisaIOError as exc:
if exc.error_code != StatusCode.error_timeout:
raise
# No SRQ within the window: fall through to a direct read so a
# stale/absent service request cannot stall the benchmark.
reply = resource.read() # noqa: F841 -- payload discarded; timing is the metric
last_stb = resource.read_stb()
dt = time.monotonic() - t0
latencies.append(dt)
if on_latency is not None:
on_latency(dt)
elapsed = time.monotonic() - start
if mode is EventMode.SRQ_WAIT:
resource.write("*SRE 0")
resource.discard_events(
pyvisa.constants.EventType.service_request,
pyvisa.constants.EventMechanism.queue,
)
if latencies:
p50 = statistics.median(latencies)
p99 = sorted(latencies)[min(len(latencies) - 1, int(0.99 * len(latencies)))]
print(f"latency p50={p50*1e3:.2f} ms p99={p99*1e3:.2f} ms")
return PollStats(transactions=count, elapsed_s=elapsed, mav_bit=last_stb & 0x10)
The loop is deliberately backend-blind: pass it a GPIB0::9::INSTR or a USB0::0x2A8D::...::INSTR resource and the only thing that changes is the measured Hz. That symmetry is what makes it a fair benchmark — the transport, not the code path, produces the difference. Feeding real workloads through it belongs behind an async command queue so the benchmark loop is not competing with production dispatch, and every VisaIOError it raises should be routed through Error Code Categorization rather than retried blindly.
Validating the Measured Rate on a Live Rig
Confirm the numbers before trusting them. Run the loop twice against the same instrument on each transport and compare the achieved effective_hz:
rm = pyvisa.ResourceManager()
for addr in ("GPIB0::9::INSTR", "USB0::0x2A8D::0x0101::MY5xxxxxxx::INSTR"):
with rm.open_resource(addr) as dev:
idn = poll_loop(dev, "*IDN?", count=2000, mode=EventMode.STB_POLL)
fetc = poll_loop(dev, "FETC?", count=2000, mode=EventMode.STB_POLL)
print(f"{addr}: *IDN? {idn.effective_hz:.0f} Hz FETC? {fetc.effective_hz:.0f} Hz")
assert fetc.mav_bit == 0x10, "MAV clear after read: response was truncated"
Three indicators tell you the run is honest. First, *IDN? (a firmware-local string, no measurement latency) isolates pure transport round-trip cost, so its Hz should land near the latency-floor rows of the table — GPIB in the 400–1500 range, USB-TMC in the 1500–6000 range; a GPIB result stuck at the low end points at cable settling or a slow co-resident talker, not the instrument. Second, FETC? adds the instrument’s conversion time, so its Hz is always lower than *IDN? on the same transport; if the two are equal you are re-reading a stale buffer, not triggering fresh conversions. Third, the asserted MAV bit (0x10) after each read confirms the reply was complete — a clear MAV means the read raced ahead of the instrument and returned a truncated frame, which downstream would look like a parse error, not a transport fault. Bounded retries around genuine timeouts belong in the Timeout Handling & Retry Logic policy rather than in the benchmark.
Failure Modes Specific to the GPIB–USB-TMC Comparison
These faults surface precisely because you chose one transport where the other’s geometry would have avoided them.
GPIB slow-talker starves the shared bus. Because bus bandwidth divides across active talkers, one instrument with a long readout (a slow integrating DMM at high resolution) holds the handshake and every other device’s poll cycle stretches to match. The signature is that removing one instrument raises everyone else’s Hz — impossible on a per-device transport. Diagnose by benchmarking each device solo versus in-rack; if solo Hz far exceeds in-rack Hz, the bus is contention-bound. Move the slow talker to its own controller, or migrate it to USB-TMC where its readout no longer taxes the others.
USB-TMC hub bandwidth halving. Two high-rate USB-TMC devices behind one USB 2.0 hub share a single transaction-translator budget, so each streams at roughly half its standalone rate even though the per-device table ceiling suggested full bandwidth. The signature is a throughput cliff that appears only with the second device plugged into the same hub and vanishes when it moves to a different root-hub port. Diagnose with lsusb -t to read the topology and confirm both devices share a parent; distribute high-rate instruments across distinct host controllers rather than daisy-chaining.
SRQ storm versus stale MAV. On GPIB, mixing srq_wait polling with instruments that assert SRQ aggressively floods the controller with service requests it must serial-poll to clear; conversely, a stb_poll loop that never re-reads after a device-clear can read a stale MAV bit and believe data is waiting when the buffer is empty. The signatures differ — a storm shows as the controller spending most of its time in serial-poll sweeps, a stale MAV shows as reads that return old data. Clear a storm by masking *SRE down to only the bits you service and draining the SRQ queue; clear a stale MAV with a device-clear (SDC/INITIATE_CLEAR) before resuming polling.
Controller-in-charge addressing errors. GPIB has exactly one controller-in-charge; if a second controller (a stray Prologix adapter, or a script that re-asserts IFC) contends for that role, addressing collapses and reads return the wrong device’s data or time out. USB-TMC has no equivalent — the host is unambiguously the master — so this fault is GPIB-exclusive. The signature is data that is valid but attributed to the wrong instrument. Enforce a single controller per bus, assert IFC only at initialization, and verify the primary-address map before every acquisition run.
Related
- Interface Selection and Concurrency — the coarse transport-and-concurrency decision this comparison refines once both candidates qualify.
- asyncio vs Threading for Serial I/O — the sibling concurrency question for the blocking-driver transports these two both are.
- VISA Resource Manager — how GPIB and USB-TMC resources are addressed and opened for either backend.
- Async Command Queuing Systems — draining the benchmark loop from a non-blocking dispatch layer in production.
- Error Code Categorization — classifying the
VisaIOErrors a saturated bus or halted endpoint raises.
← Back to Interface Selection and Concurrency
References
- pyvisa resource API —
read_stb,wait_on_event, and timeout semantics used by the poll loop. - USBTMC / USB488 subclass specification — bulk-endpoint transfers, device-clear, and the READ_STATUS_BYTE control request.