
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.

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 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

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.

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:
- Setup: cold parametrics at ambient before the chamber moves.
- Vcc to +5 %, exerciser on, chamber ramp to 105 °C at 3 °C/min; overshoot and settled temperature checked.
- Dwell 48 h: every heartbeat checked, temperature and hot Icc logged every 30 min, fault counters accumulated.
- Vcc back to nominal, chamber ramp to ambient.
- Cold parametrics again, post minus pre per parameter against the limits.
- 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
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:
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:
- Execution-scoped plug:
scope: executionon 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 ownbibplug while the chamber stays shared. - Setup as the delta reference: the pre-burn snapshot is a setup phase;
post_burnnames it as a parameter and readspre_burn.pre_snapshot, so the deltas are computed from the run's own record, not from a file on disk. - 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.
- JSON measurements: both snapshots are recorded as objects, and
fault_countersis validated as a whole object equal to all zeros, so any non-zero counter fails the phase with the full table on the run. - 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:
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:
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:
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_CA 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:
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
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:
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 sOn 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.