Skip to content
Instrument Control

Share Instruments Across Parallel Slots

Shared instruments cause interleaved commands and lost throughput. Learn how plug scope serializes calls and how to size the bottleneck.

JJulien Buteau
advanced10 min readSeptember 9, 2026

Share Instruments Across Parallel Slots

Parallel testing assumes each position has its own instruments. Real stations rarely do. One DMM behind a multiplexer serves eight positions, one thermal chamber holds the whole tray, one power supply feeds a rack.

Shared instruments are where parallel test stations lose the throughput they were built for, and where they produce measurements that are wrong in ways that pass. This guide covers how sharing works, how to size the benefit, and the failure modes worth designing against.

It assumes a multi-position fixture; for what a position is and how to declare one, see What Are Test Sockets and How to Use Them.

Why Sharing Is a Problem

An instrument is a single piece of hardware with state. Configure a DMM for a 10V DC range, and that is its range until something changes it.

Two slots measuring at once creates two failures that look nothing alike:

Interleaved commands. Slot 1 sets the range to 10V, slot 2 sets it to 100mV, then slot 1 triggers a reading. Slot 1 measures its unit on slot 2's range. The instrument reports no error, the measurement is a plausible number, and the unit either fails for no reason or passes when it should not.

Serialization. Two slots cannot physically read one DMM simultaneously. The calls queue whether or not you designed for it.

The first failure is the dangerous one, because it produces bad data rather than an error.

Scope Decides How Many Instances Exist

In the TofuPilot Framework, plug scope controls instance count and lifetime:

ScopeInstanceUse for
slot (default)One per slotPer-position PSU channel, per-position serial port
executionOne shared by all slotsA DMM behind a mux, a chamber, a rack supply
stationOne per station, held across runsSlow-to-open VISA or TCP sessions
procedure.yaml
execution:  slots: 8plugs:  - name: dut_serial    python: plugs.serial_port:DutSerial    scope: slot  - name: dmm    python: plugs.dmm:Keysight34461A    scope: execution    config:      address: "192.168.1.50"  - name: chamber    python: plugs.chamber:ThermalChamber    scope: station    config:      address: "192.168.1.60"

Declaring a shared instrument as execution or station is what makes it safe. A plug instance runs one method call at a time. When several slots call the same shared plug, their calls queue and execute one at a time, like commands on a SCPI socket. The interleaving failure above cannot happen through a shared plug, because there is one instance and one call in flight.

The corresponding mistake is declaring a shared instrument as slot scope. Eight slots then open eight sessions to one physical instrument, with no serialization at all, and you get exactly the interleaving problem — intermittently, under load, in production.

Keep the Critical Section Inside One Call

Serialization is per method call, not per phase. A shared measurement must therefore be one call that configures, triggers, and reads:

plugs/dmm.py
import pyvisaclass Keysight34461A:    def __init__(self, address: str):        self._rm = pyvisa.ResourceManager()        self._inst = self._rm.open_resource(f"TCPIP::{address}::INSTR")        self._inst.timeout = 5000    def measure_dc_volts(self, channel: int, expected_range: float) -> float:        """Select the channel, configure, and read as one indivisible call."""        self._inst.write(f"ROUT:CLOS (@{channel})")        self._inst.write(f"CONF:VOLT:DC {expected_range}")        return float(self._inst.query("READ?"))    def __del__(self):        self._inst.close()

Split across three calls, another slot's call can land between the configure and the read:

plugs/dmm_wrong.py
class DmmWrong:    def select_channel(self, channel: int):        self._inst.write(f"ROUT:CLOS (@{channel})")    def configure(self, expected_range: float):        self._inst.write(f"CONF:VOLT:DC {expected_range}")    def read(self) -> float:        # Another slot may have re-routed the mux by now.        return float(self._inst.query("READ?"))

The phase then passes its own channel, and the shared instance keeps every sequence intact. A phase reads its position from run.slot_id, the key declared in execution.slots:

phases/measure_rail.py
CHANNELS = {"s1": 101, "s2": 102, "s3": 103, "s4": 104}def measure_rail(measurements, run, dmm):    measurements.rail_voltage = dmm.measure_dc_volts(        channel=CHANNELS[run.slot_id],        expected_range=10.0,    )

Mapping slot keys to channels explicitly, rather than deriving a channel from a slot number, keeps the wiring readable and survives renumbering the fixture.

Keep Shared Calls Short

Queuing has a direct throughput cost, and it is the reason parallel stations underperform their spec.

A shared plug method that sleeps holds every other slot's calls to that plug for as long as it runs. Settling time is the usual culprit:

plugs/dmm_slow.py
import timeclass DmmSlow:    def measure_after_settle(self, channel: int) -> float:        self._inst.write(f"ROUT:CLOS (@{channel})")        time.sleep(2.0)          # blocks all eight slots        return float(self._inst.query("READ?"))

With eight slots, that two-second settle costs sixteen seconds of station time per cycle, and seven slots spend it idle. Put the wait in the phase, where slots wait concurrently:

phases/measure_rail.py
import timeCHANNELS = {"s1": 101, "s2": 102, "s3": 103, "s4": 104}def measure_rail(measurements, run, psu, dmm):    channel = CHANNELS[run.slot_id]    psu.enable(channel)    time.sleep(2.0)  # each slot waits in parallel    measurements.rail_voltage = dmm.measure_dc_volts(channel, 10.0)

The rule: shared plug methods should contain instrument I/O and nothing else. Waiting, computation, and retry loops belong in phases.

Sizing the Benefit Before Building

Whether sharing is acceptable is arithmetic, worth doing before the fixture is built.

Let N be slot count, S the shared instrument time per unit, and P the per-slot time that runs in parallel. Cycle time is roughly:

model.txt
cycle ≈ max(P, N × S)

Eight slots, 25s of parallel work, 1s of shared measurement:

good.txt
max(25, 8 × 1) = 25s     shared instrument is free

The same station with a 4-second shared measurement:

bad.txt
max(25, 8 × 4) = 32s     the DMM is now the bottleneck

Past N × S > P, adding slots buys nothing: the ninth slot waits on the DMM. That is the point to either buy a second instrument, shorten the shared measurement, or stop adding positions.

Use phase_first for Shared Instruments

With slots configured, execution strategy affects how shared work clusters:

procedure.yaml
execution:  strategy: phase_first  slots: 8

phase_first, the default, runs the same phase across all slots before advancing. Every slot's DMM measurement happens in the same window, so the instrument stays in one configuration for the group.

slot_first completes one slot before starting the next, which reconfigures the shared instrument on every switch. For a station whose sharing is expensive to reconfigure, phase_first is the better default.

Shared Setup and Teardown

Some shared hardware is not measured per unit at all: a chamber ramps once, a rack supply powers on once. Those are phases scoped to the execution:

procedure.yaml
setup:  - name: Ramp Chamber to 85C    python: phases.ramp_chamber    scope: executionmain:  - name: Measure Leakage    python: phases.measure_leakageteardown:  - name: Discharge This Position    python: phases.discharge  - name: Return Chamber to Ambient    python: phases.cool_chamber    scope: execution

Execution-scoped setup runs once before any slot starts; execution-scoped teardown runs once after every slot is done.

This scoping is a correctness requirement, not a convenience. A slot-scoped teardown can run while its neighbours are still testing. Return Chamber to Ambient as a slot-scoped phase would start cooling as soon as the first unit finished, and every remaining unit would be measured on a falling temperature ramp. Those units fail intermittently, the failures do not reproduce on the bench, and nothing in the data points at the chamber.

The rule follows directly: if a teardown touches something another slot can still be using, it must be scope: execution.

Failure Behavior

A shared plug that fails to initialize stops every slot, and so does a failing execution-scoped setup phase. That is correct — eight units testing without a chamber at temperature would produce eight meaningless passes — but it makes shared setup the highest-consequence code on the station.

Give those phases retries and error messages that name the instrument:

procedure.yaml
setup:  - name: Ramp Chamber to 85C    python: phases.ramp_chamber    scope: execution    timeout: 15m    retry:      limit: 2      delay: 30s

If the instrument connection drops mid-test, handle the reconnect inside the plug. The engine guarantees a long-lived plug process, not a live instrument session; it health-checks a held station plug before each reuse and respawns it if the process died, but a dead TCP session inside a living process is yours to notice.

Common Pitfalls

Declaring a shared instrument as slot scope. N sessions to one instrument, no serialization, interleaved commands. This is the failure that produces wrong numbers instead of errors.

Splitting a configure-and-read across several plug methods. Another slot can land in the gap.

Sleeping inside a shared plug method. Every other slot waits. Move the wait to the phase.

Cleaning up shared hardware in slot teardown. The first slot to finish disturbs the rest.

Sizing the fixture without the shared-instrument term. A 16-slot fixture with a 3-second shared measurement is a 48-second cycle no matter how fast the units are.

Key Points

  • Shared instruments fail two ways: interleaved commands producing wrong data, and queuing destroying throughput.
  • scope: execution or scope: station gives one instance whose calls serialize, which prevents interleaving by construction.
  • A shared plug method must be the whole critical section: configure, trigger, and read in one call.
  • Keep waits out of shared methods; a sleeping method blocks every slot.
  • Cycle time is roughly max(P, N × S) — past that crossover, more slots buy nothing.
  • Anything a finishing slot could disturb belongs in an execution-scoped teardown.

More Guides

Put this guide into practice