PCBA & Functional Testing

Catch Intermittent Contacts in Production

Learn how to catch intermittent solder joints, crimps and connectors by measuring resistance under flex and validating the spread, not only the mean.

JJulien Buteau
intermediate9 min readOctober 27, 2026

An intermittent contact measures normal at rest and opens only when the part moves. Learn how to catch it in production by measuring resistance repeatedly while the joint is flexed, recording the result as a time-series measurement in TofuPilot, and validating the spread of the readings, not only their mean.

Why a Static Measurement Is Not Enough

A cracked solder joint, a crimp with too few strands captured, or a connector contact with low normal force can all conduct perfectly when nothing moves. The crack surfaces touch, or the strands press against the barrel, and a single resistance reading falls inside the limits. Under vibration or a pull on the wire the contact area changes from one moment to the next, and the resistance jumps.

Example readings on a heavy-wire ESC joint, 4-wire, 4 A test current:

JointStatic readingUnder flex: minUnder flex: maxUnder flex: mean
Good0.50 mΩ0.49 mΩ0.52 mΩ0.50 mΩ
Cracked0.62 mΩ0.60 mΩ3.80 mΩ0.75 mΩ

These are illustrative values. The pattern is what matters: the cracked joint passes a static limit of 0.8 mΩ and also passes a limit on the mean under flex, because the high readings are short. Only the maximum and the spread (max minus min) separate it from the good joint.

The method applies to any contact that carries current through a mechanical interface:

ContactHow to move it during the measurement
Wire-to-pad solder jointFlex the wire through a fixed angle at the joint exit
CrimpFlex the wire at the back of the crimp barrel
ConnectorRock the mated connector, or vibrate the assembly
Through-hole joint on a heavy partTap or vibrate the board

How the Measurement Works

The fixture does three things at once for a fixed duration:

  1. Forces a constant current through the contact with Kelvin force contacts.
  2. Moves the contact with an actuator: a servo arm, a solenoid or a small shaker, at a fixed amplitude and frequency.
  3. Samples the sense voltage and the actual current at a fixed interval.

Each sample gives a resistance R = V / I. With the voltage in microvolts and the current in milliamps, the ratio is directly in milliohms. The series of samples is recorded as one multi-dimensional measurement: time on the x axis, resistance on the y axis.

The actuator must move the part the same way on every unit. A technician flexing a wire by hand gives a different force each time, and the result cannot be compared from unit to unit or between operators.

Prerequisites

  • The TofuPilot CLI installed on the test station
  • A Kelvin fixture as described in Screen Solder Joints Without Destroying Them, with an actuator added
  • Fixture firmware that captures a burst of samples during actuation and returns them in one reply

Step 1: Define the Capture Command

In this example the fixture microcontroller accepts one command per capture:

fixture-protocol.txt
FLEX <channel> <amps> <samples> <period_ms> <flex_hz>

The firmware selects the channel, measures the zero-current offset, turns on the current, starts the actuator, takes samples readings every period_ms, stops the actuator and the current, and replies with one JSON line:

fixture-reply.json
{"sense_uv": [2010.4, 2012.1, 2009.8], "current_ma": [4001.2, 4000.8, 4001.0]}

The offset is already subtracted from sense_uv. Timing stays in the firmware because a microcontroller samples at a regular interval, while a Python loop sending one serial query per sample does not.

Step 2: Write the Fixture Plug

plugs/flex_fixture.py
21 lines
import jsonimport serialclass FlexFixture:    def __init__(self, port, baudrate=115200):        self._ser = serial.Serial(port, baudrate, timeout=10)    def capture_under_flex(self, channel, amps, samples, period_ms, flex_hz):        command = f"FLEX {channel} {amps:.3f} {samples} {period_ms} {flex_hz:.2f}"        self._ser.write(command.encode())        reply = self._ser.readline().decode().strip()        if not reply or reply.startswith("ERR"):            raise RuntimeError(f"Flex capture on channel {channel} failed: {reply!r}")        # Lists of floats cross the plug boundary, which accepts JSON values only.        return json.loads(reply)    def __del__(self):        self._ser.close()

The serial timeout covers the whole capture: 500 samples at 2 ms take 1 s, plus the actuator start and stop.

Step 3: Write the Measurement Phase

phases/flex.py
49 lines
import csvimport io# Recorded in place of a sample where the current collapsed (open contact).R_CEILING_MOHM = 100.0def measure_under_flex(    flex_fixture,    measurements,    attach,    log,    channel,    current_a=4.0,    samples=500,    period_ms=2,    flex_hz=5.0,):    capture = flex_fixture.capture_under_flex(channel, current_a, samples, period_ms, flex_hz)    times_ms = [i * period_ms for i in range(len(capture["sense_uv"]))]    resistance = []    open_samples = 0    for uv, ma in zip(capture["sense_uv"], capture["current_ma"]):        # A current far below the set point means the source hit its        # compliance voltage: the contact was open during this sample.        if ma < 0.5 * current_a * 1000:            open_samples += 1            resistance.append(R_CEILING_MOHM)        else:            resistance.append(uv / ma)  # µV / mA = mΩ    m = measurements.flex_resistance    m.x_axis = times_ms    m.y_axis.resistance = resistance    aggs = m.y_axis.resistance.aggregations    aggs.mean = sum(resistance) / len(resistance)    aggs.max = max(resistance)    aggs.spread = max(resistance) - min(resistance)    aggs.open_samples = open_samples    buf = io.StringIO()    writer = csv.writer(buf)    writer.writerow(["time_ms", "sense_uv", "current_ma", "resistance_mohm"])    writer.writerows(zip(times_ms, capture["sense_uv"], capture["current_ma"], resistance))    attach.data(buf.getvalue().encode(), f"flex_channel_{channel}.csv")    log.info(f"Channel {channel}: spread {aggs.spread:.3f} mΩ, {open_samples} open samples")

The chart in the run shows the waveform, the aggregations carry the pass/fail decision, and the CSV keeps the raw voltage and current for failure analysis.

Step 4: Declare the Measurement and Its Limits

Axis validators on a multi-dimensional measurement only accept == and != against a full array, so range limits go on aggregations. The aggregation type is a free string, which allows spread and open_samples next to mean and max.

procedure.yaml
49 lines
name: ESC Power Joint Flex Screeningversion: 1.0.0execution:  workers: 1plugs:  - name: Flex Fixture    key: flex_fixture    python: plugs.flex_fixture:FlexFixture    scope: station    with:      port: "COM5"main:  - name: Battery Positive Joint Under Flex    key: flex_bat_pos    python: phases.flex:measure_under_flex    with:      channel: 0    measurements:      - name: Resistance Under Flex        key: flex_resistance        title: Battery positive joint, 5 Hz flex        x_axis:          legend: Time          key: time          unit: ms        y_axis:          - legend: Resistance            key: resistance            unit: mΩ            aggregations:              - type: mean                validators:                  - operator: "<="                    expected_value: 0.8              - type: max                validators:                  - operator: "<="                    expected_value: 1.2              - type: spread                validators:                  - operator: "<="                    expected_value: 0.3              - type: open_samples                validators:                  - operator: "=="                    expected_value: 0

The limit values are examples for a heavy-wire joint at 4 A. Derive yours from known-good units, as for the static measurement: measure the spread on 30 or more good units under the same actuation, and set the limit above that population with margin for fixture noise. A gauge R&R study on the spread tells you whether the fixture noise is small enough for the limit you need.

AggregationWhat it catches
meanA joint that is resistive all the time, already visible statically
maxShort resistance peaks while the crack opens
spreadA contact whose resistance moves with the actuator
open_samplesSamples where the contact opened completely

Step 5: Choose the Sample Rate and Duration

Two parameters decide what the measurement can see:

  • Sample interval. An opening shorter than the interval between two samples can fall between them. At 2 ms per sample, a 0.5 ms opening is detected only if a sample lands inside it. Shorter intervals reduce the averaging per sample and raise the noise, so the spread limit must account for it. Dedicated event detectors exist for sub-millisecond discontinuities on connectors; a sampled resistance measurement does not replace them.
  • Duration and flex cycles. Record several full actuator cycles: 500 samples at 2 ms cover 1 s, which is 5 cycles at 5 Hz. One cycle may not hit the position where the crack opens.

Keep the number of points per measurement moderate: a few hundred to a few thousand. The CSV attachment holds the full capture if a longer acquisition is needed for analysis.

Step 6: Review the Results

In the TofuPilot dashboard, each run shows the resistance chart of every joint next to its aggregations. Across runs:

ViewUse
Failure Pareto per measurementShows which joint or connector fails under flex most often
Control chart and Cpk of each aggregationShows a spread that grows over weeks, often a worn actuator or fixture
Yield per operator and per stationPoints to the build step or the test station responsible

For the process causes of cracked joints and the corrective actions, see Why ESC Solder Joints Fail in Flight.

Common Pitfalls

Validating only the mean. A crack that opens for a few percent of the time barely moves the mean. Put limits on max, spread and open_samples.

Flexing by hand. The force and angle change with each operator. Use an actuator with fixed amplitude and frequency.

Sampling from a Python loop over serial. The interval varies with USB and operating system latency. Let the firmware time the samples and return the burst.

Computing resistance with the set-point current. During an opening the source cannot hold its current. Use the measured current per sample, and count samples where it collapsed.

Two-wire probing. The probe contacts also move under flex, and their resistance changes. Kelvin contacts remove them from the reading.

Forgetting workers: 1. Phases without depends_on start in parallel. Two phases driving one actuator and one multiplexer at the same time give meaningless data.

Flexing too hard. The test must reveal defects, not create them. Set the amplitude well below what the joint sees in service, and confirm on good units that repeated testing does not change their readings.

Key Points

  • An intermittent contact passes a static resistance measurement and opens only under motion.
  • Measure resistance repeatedly while a fixed-amplitude actuator flexes the joint, crimp or connector.
  • Record the series as a multi-dimensional measurement, resistance in milliohms against time.
  • Validate max, spread and open_samples aggregations, not only the mean.
  • Let the fixture firmware time the samples, and attach the raw capture as CSV.
  • Set limits from known-good units under the same actuation, and check fixture noise with a gauge R&R study.

More Guides

Put this guide into practice