Skip to content

Board-Level Burn-In Stress Testing

Run powered-on temperature-stress burn-in on PCBAs to weed out infant mortality defects before shipment to consumer, industrial, or automotive customers.

TofuPilotBurn-InPythonTofuPilot FrameworkGitHub
Board-Level Burn-In Stress Testing

Introduction

Burn-In Overview

Electronic assemblies follow the bathtub curve: a steep "infant mortality" region with a decreasing hazard rate (Weibull shape β < 1), a flat useful-life plateau, and a rising wear-out tail. Burn-in deliberately runs the population through the infant-mortality slope before shipment, so customers only see units that have already survived the early failures. The Arrhenius acceleration factor (a typical activation energy around 0.7 eV) is what lets 48 h at 105 °C stand in for months of field life.

Bathtub curve of hazard rate over field time with the infant mortality, useful life and wear-out regions, and the burn-in screen consuming the decreasing-hazard region before shipment.

The region burn-in consumes: everything to the left of the plateau is a failure the customer would otherwise have found in the first weeks.

Defects exposed during burn-in: weak BGA solder balls, marginal silicon (threshold-voltage drift), electromigration onset in narrow traces, contaminated wire bonds, capacitor electrolyte loss, tin whisker initiation on lead-free finishes. None of these fail a cold electrical test; all of them fail in the field with the wrong probability distribution.

Test Purpose

The procedure produces a per-board burn-in record:

  • Pre-burn parametric snapshot: cold Icc, ADC offset, oscillator error, leakage
  • Chamber ramp to setpoint with the exerciser running at Vcc +5 %, overshoot and settled temperature
  • 48 h dwell with every heartbeat checked, the temperature and hot Icc logged every 30 minutes, and the exerciser's fault counters accumulated
  • Post-burn parametric snapshot with the same instruments at ambient
  • Deltas per parameter, validated against infant-mortality limits

The burn-in recipe as the run records it: 3 °C per minute ramp to 105 °C with 1.2 K overshoot, 48 hours at setpoint with 17 280 heartbeats, ramp back to ambient, and the hot Icc annealing down 3.6 percent over the dwell.

The whole recipe as one run records it on the mock bench: the chamber ramp and its overshoot, 48 hours at 105 °C with the hot Icc annealing down a few percent, the ramp back to ambient. A latent defect shows as the blue trace rising instead.

Burn-in eliminates an entire class of returns: "died after two weeks", "stopped working mid-flight", "dead out of the box". The cost is calendar time and chamber utilization; the return is a population whose field-failure rate is bounded.

Beyond the reliability content, this template demonstrates five framework mechanics: a setup snapshot used as the delta reference through previous-results injection into a main phase, an execution-scoped plug for the shared chamber, multi-dimensional measurements with custom aggregations on the ramp and the dwell log, a long-running phase with a progress component, and JSON measurements for the snapshots and a validated fault-counter object.

Equipment & Setup

To implement board-level burn-in on a production line, the following are required:

  • A temperature-controlled chamber with a programmable profile and a data interface
  • Burn-in boards (BIBs) carrying several DUTs with edge sockets, a power harness, and the exerciser heartbeat link
  • A programmable DC supply per BIB capable of Vcc margining
  • The Devices Under Test: populated, ICT / boundary scan / FCT-passed PCBAs with the exerciser firmware loaded
  • A TofuPilot Framework procedure to run pre, during and post measurements and validate every board
  • The TofuPilot Dashboard to log per-unit deltas and trend infant mortality

Burn-in station: environmental chamber with the door open on a loaded burn-in board, the power cable from the side port to the bench supply, and the production test computer showing the temperature profile.

The station: chamber, loaded burn-in board, supply through the side port, and the test computer watching the heartbeats and the profile.

Hardware Components

Burn-In Chamber

Production-grade options: Espec burn-in chambers built around BIB slots with high heat-load tolerance, Thermotron AST-class chambers with fast ramps (marketed for HALT and HASS, used for accelerated burn-in), Weiss and CSZ climatic chambers when humidity is part of the recipe. All expose a serial, Ethernet or SCPI-style interface for the profile and the readback. Typical usable window for industrial products: -40 to +125 °C, 24 to 168 h dwell at the hot extreme.

Burn-In Board

The BIB is the load board: a panel of edge sockets, distributed power and ground planes, decoupling, a harness to the chamber feedthrough, hardware reset and watchdog. The on-board exerciser firmware drives every DUT at worst-case power: full CPU load, every peripheral toggling, memory pattern walks, ADC and DAC loopback. Gold-plated sockets and solid copper planes are mandatory to survive thousands of insertions and the steady-state current.

Burn-in load board close-up: four boards upright in edge-card sockets on the cream carrier, the bulk capacitors along the edge and the power cable leaving one end.

A load board: DUTs upright in edge sockets, bulk capacitance along the carrier edge, and the cable that carries margined Vcc and the heartbeat lines through the chamber wall. The chamber's own thermocouples sit on a representative board, not on the carrier.

Stress Recipes

Common burn-in profiles by product class:

  • Consumer: 24 h at 85 °C, Vcc nominal, dynamic exercise
  • Industrial (this template): 48 h at 105 °C, Vcc +5 %
  • Automotive, medical, mil-aero: 72 to 168 h at 125 °C, Vcc +5 %
  • Power cycling: 1 min on / 1 min off layered on the soak to stress die attach and BGA joints
  • HAST: 110 or 130 °C at 85 % RH, biased, as a humidity accelerant (JESD22-A110)

HALT (design phase, stress to failure, no survivors expected), HASS (production screen at stresses above field but below the HALT limits, a few hours per unit) and burn-in (constant elevated temperature and voltage for tens to hundreds of hours, powered and exercised) target different failure populations; this template is the classical burn-in.

Custom Firmware

The DUT boots into a burn-in exerciser mode: full CPU load, every peripheral cycled, a heartbeat every 10 s over a side channel (UART, CAN, Ethernet) carrying the hot supply current and the internal fault counters (memory ECC errors, watchdog resets, peripheral timeouts). The exerciser is flashed at the programming station before burn-in; the production firmware replaces it afterwards.

Test Procedure

Overview

For each board on the burn-in board, the procedure runs:

  1. Setup: cold parametrics at ambient before the chamber moves.
  2. Vcc to +5 %, exerciser on, chamber ramp to 105 °C at 3 °C/min; overshoot and settled temperature checked.
  3. Dwell 48 h: every heartbeat checked, temperature and hot Icc logged every 30 min, fault counters accumulated.
  4. Vcc back to nominal, chamber ramp to ambient.
  5. Cold parametrics again, post minus pre per parameter against the limits.
  6. Teardown: slot supply off before the BIB comes out.

Why TofuPilot Framework?

TofuPilot Framework is a YAML + Python test framework built for hardware manufacturing. Instead of writing all your test logic, measurements, and limits inside Python code, you describe what the test does in a procedure.yaml file, and how in small Python phase files. The framework handles:

  • Automatic Python environment management (via uv)
  • Operator UI (no frontend code needed)
  • Measurement validation and live charts
  • Process isolation between phases and equipment plugs

Project Structure

procedure.yaml
phases
pre_burn.py
ramp_up.py
dwell.py
ramp_down.py
post_burn.py
power_off.py
plugs
chamber.py
bib.py
utils
recipe.py
README.md
pyproject.toml

The chamber and BIB plugs are mocks, time-scaled through one constant in utils/recipe.py so the 48 h recipe runs in under a minute; the mock board anneals a few percent like healthy silicon. Run it with tofupilot run . --no-tui --no-kiosk --json. You can find the full source on GitHub.

The Procedure File

This is the exact file the template ships, verified end to end with the CLI:

procedure.yaml
name: Board-Level Burn-Inversion: 0.1.0description: Industrial burn-in recipe, 48 h at 105 C with Vcc at +5 percent, with cold parametrics before and after and exerciser heartbeat monitoring throughout the dwell.unit:  auto_identify: true  serial_number:    description: "Scan the board data-matrix before it goes into the socket"    placeholder: "UAUT-00000"    pattern: "^UAUT-\\d{5}$"    default_value: "UAUT-04829"  part_number:    default_value: "PCB-MAIN-V2"plugs:  - name: Chamber    description: Environmental chamber, one instance per execution (mock, time-scaled)    python: plugs.chamber:Chamber    key: chamber    scope: execution  - name: BIB Slot    description: "One DUT socket on the burn-in board: parametrics, margined Vcc, exerciser heartbeat (mock)"    python: plugs.bib:BibSlot    key: bibsetup:  - name: Pre-Burn Snapshot    key: pre_burn    python: phases.pre_burn    measurements:      - name: Pre-Burn Snapshot        key: pre_snapshot        description: "Cold Icc, ADC offset, oscillator error and leakage at ambient; the reference for every delta."      - name: Pre-Burn Icc        key: pre_icc        unit: mA        validators:          - {operator: ">=", expected_value: 100.0}          - {operator: "<=", expected_value: 140.0}      - name: Pre-Burn Leakage        key: pre_leak        unit: µA        validators:          - {operator: "<=", expected_value: 3.0}main:  - name: Ramp Up    key: ramp_up    python: phases.ramp_up    timeout: 2h    measurements:      - name: Exerciser Started        key: exerciser_started        validators:          - {operator: "==", expected_value: true}      - name: Vcc Margined        key: vcc_margined        unit: V        validators:          - {operator: ">=", expected_value: 3.44}          - {operator: "<=", expected_value: 3.49}      - name: Ramp        key: ramp        title: Chamber ramp to setpoint        x_axis:          legend: Time          unit: min        y_axis:          - legend: Temperature            key: temperature            unit: °C            aggregations:              - type: overshoot_k                unit: K                validators:                  - {operator: "<=", expected_value: 3.0}              - type: settled_c                unit: °C                validators:                  - {operator: ">=", expected_value: 104.0}                  - {operator: "<=", expected_value: 106.0}  - name: Dwell    key: dwell    python: phases.dwell    depends_on: [ramp_up]    ui:      components:        - key: dwell_progress          type: progress          label: "Dwell at 105 °C"          description: "48 h, heartbeat every 10 s"          default_value: 0          max: 100    measurements:      - name: Dwell Log        key: dwell_log        title: Temperature and hot Icc over the dwell        x_axis:          legend: Elapsed          unit: h        y_axis:          - legend: Temperature            key: temperature            unit: °C            aggregations:              - type: mean_c                unit: °C                validators:                  - {operator: ">=", expected_value: 104.0}                  - {operator: "<=", expected_value: 106.0}          - legend: Hot Icc            key: icc_hot            unit: mA            aggregations:              - type: drift_pct                unit: "%"                validators:                  - {operator: ">=", expected_value: -8.0}                  - {operator: "<=", expected_value: 2.0}      - name: Heartbeats        key: heartbeats        validators:          - {operator: "==", expected_value: 17280}      - name: Missed Heartbeats        key: missed_heartbeats        validators:          - {operator: "==", expected_value: 0}      - name: Fault Counters        key: fault_counters        validators:          - operator: "=="            expected_value:              ecc_errors: 0              watchdog_resets: 0              peripheral_timeouts: 0      - name: Thermal Dose        key: thermal_dose        unit: °C·h        validators:          - {operator: ">=", expected_value: 5000.0}  - name: Ramp Down    key: ramp_down    python: phases.ramp_down    depends_on: [dwell]    timeout: 2h    measurements:      - name: Ambient Reached        key: ambient_reached        unit: °C        validators:          - {operator: "<=", expected_value: 25.0}  - name: Post-Burn Snapshot    key: post_burn    python: phases.post_burn    depends_on: [ramp_down]    measurements:      - name: Post-Burn Snapshot        key: post_snapshot      - name: Icc Delta        key: icc_delta_pct        unit: "%"        description: "Post minus pre, cold. Healthy silicon anneals a few percent down; a rise or a large drop is a latent defect."        validators:          - {operator: ">=", expected_value: -10.0}          - {operator: "<=", expected_value: 2.0}      - name: ADC Offset Delta        key: adc_offset_delta_lsb        unit: LSB        validators:          - {operator: ">=", expected_value: -1.0}          - {operator: "<=", expected_value: 1.0}      - name: Oscillator Drift        key: osc_drift_ppm        unit: ppm        validators:          - {operator: ">=", expected_value: -10.0}          - {operator: "<=", expected_value: 10.0}      - name: Leakage Ratio        key: leak_ratio        description: "Post over pre leakage; above 2x is a screened infant-mortality signature."        validators:          - {operator: "<=", expected_value: 2.0}teardown:  - name: Power Off    key: power_off    python: phases.power_off    measurements:      - name: Vcc Off        key: vcc_off        unit: V        validators:          - {operator: "==", expected_value: 0.0}

Framework features to notice:

  1. Execution-scoped plug: scope: execution on the chamber means one instance per pass of the procedure, which is what a chamber shared by every board on a burn-in board needs. On a multi-slot station each DUT gets its own bib plug while the chamber stays shared.
  2. Setup as the delta reference: the pre-burn snapshot is a setup phase; post_burn names it as a parameter and reads pre_burn.pre_snapshot, so the deltas are computed from the run's own record, not from a file on disk.
  3. Multi-dimensional measurements with custom aggregations: the ramp validates its overshoot and settled temperature; the dwell log validates the mean temperature and the hot Icc drift, all computed in the phase and checked in YAML.
  4. JSON measurements: both snapshots are recorded as objects, and fault_counters is validated as a whole object equal to all zeros, so any non-zero counter fails the phase with the full table on the run.
  5. Timeouts where they fit: the two ramps carry timeout: 2h; a phase timeout tops out at 24 h, so the 48 h dwell runs without one and relies on the heartbeat check and the chamber's own watchdog instead.

Pre-Burn Snapshot

Cold measurements at ambient before the chamber moves: quiescent Icc, ADC offset, oscillator error against the reference, leakage on the isolation rail. The whole snapshot is kept as JSON; the two values that have absolute limits are also recorded as numbers:

phases/pre_burn.py
def pre_burn(measurements, bib, log):    """Setup: cold parametrics at ambient before the chamber moves. The delta reference."""    snap = bib.snapshot()    log.info(f"Pre-burn: Icc {snap['icc_ma']} mA, ADC offset {snap['adc_offset_lsb']} LSB, osc {snap['osc_ppm']} ppm, leak {snap['leak_ua']} uA")    measurements.pre_snapshot = snap    measurements.pre_icc = snap["icc_ma"]    measurements.pre_leak = snap["leak_ua"]

Ramp Up

Vcc goes to +5 %, the exerciser starts, and the chamber ramps at 3 °C/min. The ramp is recorded as a series with two aggregations: the overshoot past the setpoint and the settled temperature over the last five minutes:

phases/ramp_up.py
import numpy as npfrom utils.recipe import SETPOINT_C, VCC_MARGIN, VCC_NOMINALdef ramp_up(measurements, chamber, bib, log):    bib.set_vcc(VCC_NOMINAL * VCC_MARGIN)    measurements.exerciser_started = bib.start_exerciser()    measurements.vcc_margined = bib.vcc()    minutes, temp = chamber.ramp_to(SETPOINT_C)    temp = np.asarray(temp)    settled = np.asarray(minutes) > (minutes[-1] - 5.0)    log.info(f"Ramp to {SETPOINT_C} C in {minutes[-1] - 10:.0f} min, overshoot {temp.max() - SETPOINT_C:.2f} K")    measurements.ramp.x_axis = minutes    measurements.ramp.y_axis.temperature = temp.tolist()    measurements.ramp.y_axis.temperature.aggregations.overshoot_k = float(temp.max() - SETPOINT_C)    measurements.ramp.y_axis.temperature.aggregations.settled_c = float(temp[settled].mean())

Dwell

The dwell is one phase that lasts 48 hours on a real bench. Every 10 s heartbeat is checked for the alive flag and its fault counters are summed; every 30 minutes the chamber temperature and the hot Icc are appended to the log and the progress bar advances:

phases/dwell.py
from utils.recipe import DWELL_HOURS, HEARTBEAT_S, SETPOINT_CLOG_EVERY_HOURS = 0.5def dwell(measurements, chamber, bib, ui, log):    """48 h at setpoint with the exerciser running; every heartbeat is checked, every 30 min logged."""    hours, temps, iccs = [], [], []    missed = 0    faults = {"ecc_errors": 0, "watchdog_resets": 0, "peripheral_timeouts": 0}    beats = int(DWELL_HOURS * 3600.0 / HEARTBEAT_S)    log_every = int(LOG_EVERY_HOURS * 3600.0 / HEARTBEAT_S)    for i in range(beats):        elapsed_h = (i + 1) * HEARTBEAT_S / 3600.0        hb = bib.heartbeat(elapsed_h)        if not hb["alive"]:            missed += 1        for key in faults:            faults[key] += hb[key]        if i % log_every == 0:            hours.append(round(elapsed_h, 2))            temps.append(chamber.temperature())            iccs.append(hb["icc_ma"])            ui.dwell_progress = int(100 * elapsed_h / DWELL_HOURS)            chamber.wait(LOG_EVERY_HOURS * 3600.0)    log.info(f"Dwell done: {beats} heartbeats, {missed} missed, faults {faults}")    measurements.dwell_log.x_axis = hours    measurements.dwell_log.y_axis.temperature = temps    measurements.dwell_log.y_axis.icc_hot = iccs    measurements.dwell_log.y_axis.temperature.aggregations.mean_c = sum(temps) / len(temps)    measurements.dwell_log.y_axis.icc_hot.aggregations.drift_pct = 100.0 * (iccs[-1] - iccs[0]) / iccs[0]    measurements.heartbeats = beats    measurements.missed_heartbeats = missed    measurements.fault_counters = faults    measurements.thermal_dose = DWELL_HOURS * SETPOINT_C

A missed heartbeat is not an exception: the loop keeps counting so the run records when and how often the board went silent, and the missed_heartbeats == 0 validator fails the phase at the end. On a real bench the heartbeat plug call blocks for the 10 s interval; in the mock it returns at once and the chamber's wait is time-scaled.

Post-Burn Snapshot

The same cold measurements, then the deltas. The pre-burn values arrive through the pre_burn parameter:

phases/post_burn.py
def post_burn(measurements, bib, pre_burn, log):    """Cold parametrics again, same instruments, then post minus pre per parameter."""    snap = bib.snapshot()    pre = pre_burn.pre_snapshot    measurements.post_snapshot = snap    icc_delta = 100.0 * (snap["icc_ma"] - pre["icc_ma"]) / pre["icc_ma"]    osc_drift = snap["osc_ppm"] - pre["osc_ppm"]    leak_ratio = snap["leak_ua"] / pre["leak_ua"]    log.info(f"Deltas: Icc {icc_delta:+.2f} %, osc {osc_drift:+.2f} ppm, leak x{leak_ratio:.2f}")    measurements.icc_delta_pct = icc_delta    measurements.adc_offset_delta_lsb = snap["adc_offset_lsb"] - pre["adc_offset_lsb"]    measurements.osc_drift_ppm = osc_drift    measurements.leak_ratio = leak_ratio

Post minus pre deltas on the mock board against their limits: Icc -3.23 percent inside -10 to +2, ADC offset 0.00 LSB inside ±1, oscillator +1.45 ppm inside ±10, leakage ratio 1.10 under 2.0.

The four deltas of the mock board against their limits: the small Icc drop and the slight leakage rise are the normal anneal, not a defect. A board with a weak joint or a marginal die shows an Icc rise, a leakage doubling, or an oscillator pull outside ±10 ppm.

One detail worth knowing: a value read back from measurements.<key> is a proxy, so the deltas are kept in local variables before being logged and assigned.

Mock Plugs

The chamber mock ramps at the recipe rate with a small first-order overshoot and reads back the setpoint with noise; the BIB mock carries a healthy board that anneals 3 % over the dwell and pulls its oscillator 1.5 ppm. Both are time-scaled by one constant:

utils/recipe.py
SETPOINT_C = 105.0AMBIENT_C = 22.0RAMP_C_PER_MIN = 3.0DWELL_HOURS = 48.0VCC_NOMINAL = 3.3VCC_MARGIN = 1.05HEARTBEAT_S = 10.0TIME_SCALE = 1.0 / 20000.0  # 48 h -> ~8.6 s

On a real bench, set TIME_SCALE = 1.0, map plugs/chamber.py to the chamber's protocol over pyvisa and plugs/bib.py to the load board's supply channel and heartbeat link; the phases, measurements, and limits stay exactly the same. Multi-slot execution, one bib plug per socket with the chamber shared, is the production form of this procedure; the framework's execution.slots block declares it, and the CLI's one-shot tofupilot run does not accept multi-slot procedures yet, so the template ships single-slot.

Run your first test in minutes