Automated test equipment is the combination of instruments, a fixture and software that tests a unit without an engineer taking readings by hand. The term covers everything from a bench with a multimeter and a script to a semiconductor tester costing millions. What they share is the same three layers.
The Three Layers
| Layer | What it is | Examples |
|---|---|---|
| Instruments | The things that stimulate and measure | DMM, power supply, oscilloscope, DAQ, RF analyser |
| Fixture | The mechanical and electrical interface to the unit | Pogo-pin bed, clamshell, cable harness, handler |
| Software | Sequencing, limits, records, operator interaction | The test executive and your test code |
Most ATE problems that look like software problems are fixture problems, and most that look like fixture problems are contact problems. That ordering is worth remembering when yield drops on one station and not another.
Types of ATE
| Type | Tests | Typical cost |
|---|---|---|
| Bench ATE | One board, one operator, low volume | Thousands |
| Functional test fixture (FCT) | Assembled PCBAs at end of line | Tens of thousands |
| In-circuit tester (ICT) | Components and nets on a bare-populated board | Hundreds of thousands |
| Flying probe | Same as ICT, no dedicated fixture, slower | Hundreds of thousands |
| Semiconductor ATE | Wafers and packaged die | Millions |
| System-level test | Fully assembled products | Varies |
What the Software Layer Has to Do
This is the part teams underestimate, because instruments and fixtures are visible and software is not.
| Responsibility | Why it matters |
|---|---|
| Sequencing | Steps run in a defined order, with dependencies |
| Limit evaluation | Pass/fail derived from the measurement, not hand-coded |
| Instrument lifecycle | Sessions opened once, shared, closed even on failure |
| Operator interaction | Serial scan, prompts, clear pass/fail indication |
| Result records | The measured value survives the shift |
| Traceability | Which unit, which station, which limits, which code |
| Recovery | A failure stops safely, not halfway through energised |
A test that prints PASS to a console has done the first two and none of the rest.
Building a Simple ATE
The three layers map directly onto a procedure: instruments are plugs, the sequence and limits are the procedure, and the test logic is phases.
class PowerSupply: """Instrument layer: programmable supply.""" def configure(self, volts: float, current_limit: float): pass # Replace with SCPI writes def output(self, on: bool): pass def measure_current_ma(self) -> float: return 41.3 def close(self): self.output(False)class Multimeter: def dc_voltage(self) -> float: return 3.31 # Replace with a PyVISA queryclass Fixture: """Fixture layer: interlock and contact sensing.""" def lid_closed(self) -> bool: return True def contact_ok(self) -> bool: return Truedef safety(measurements, fixture): """Nothing is energised until the fixture says it is safe.""" measurements.lid_closed = fixture.lid_closed() measurements.contact_ok = fixture.contact_ok()def power_up(measurements, psu, dmm): psu.configure(volts=5.0, current_limit=0.5) psu.output(True) measurements.rail_3v3 = dmm.dc_voltage() measurements.inrush_current = psu.measure_current_ma()procedure.yaml46 lines
name: Bench ATE Functional Testversion: 1.0.0unit: serial_number: default_value: "PCBA000001" part_number: default_value: "PCBA-100"plugs: - name: psu python: plugs.psu:PowerSupply - name: dmm python: plugs.dmm:Multimeter - name: fixture python: plugs.fixture:Fixturemain: - name: Safety python: phases.safety measurements: - name: Lid Closed validators: - operator: "==" expected_value: true - name: Contact Ok validators: - operator: "==" expected_value: true - name: Power Up python: phases.power_up depends_on: [safety] measurements: - name: Rail 3V3 unit: V validators: - operator: ">=" expected_value: 3.2 - operator: "<=" expected_value: 3.4 - name: Inrush Current unit: mA validators: - operator: "<=" expected_value: 450Run it with tofupilot run ./procedure.yaml.
Two structural points that make this ATE rather than a script:
Safety gates power. depends_on: [safety] means the supply is never enabled on an open fixture. That is a hard requirement of any ATE that energises anything.
Power-down lives in the plug. close() runs at teardown regardless of how the run ended, so a failure does not leave a board energised on the bench.
Contact Resistance Is the Most Common ATE Failure
Pogo pins wear, oxidise and collect flux. A worn fixture produces intermittent failures on measurements that have nothing to do with the board, and the usual response is to blame the product.
The defence is to measure the interface itself rather than inferring it:
def contact_check(measurements, dmm): """Four-wire resistance across a known short on the fixture.""" measurements.contact_resistance = dmm.four_wire_resistance()- name: Contact Check python: phases.contact_check depends_on: [safety] measurements: - name: Contact Resistance unit: "mΩ" description: Across the fixture's calibration short validators: - operator: "<=" expected_value: 50Trending that one measurement tells you when to service the fixture, before it starts failing good boards.
Scaling to Multiple Units
When the fixture holds several boards, declare slots. Each gets its own unit, measurements and outcome, and they run concurrently:
execution: slots: - name: Slot 1 - name: Slot 2 - name: Slot 3 - name: Slot 4A shared resource, such as a single rack supply feeding all slots, belongs in an execution-scoped setup and teardown rather than per-slot, otherwise one slot finishing will power down its neighbours.
Recording the Station
An ATE fleet needs to know which station produced a result:
def record_station(run): import socket run.metadata["station"] = socket.gethostname() run.metadata["fixture_id"] = "FIX-4417-02"Without it, a fixture-specific problem is indistinguishable from a product problem in the aggregate yield.
Buying vs Building
| Situation | Direction |
|---|---|
| Standard boards, moderate volume | Build a bench ATE; the cost is mostly your time |
| High volume, tight cycle time | Buy a fixture, build the software |
| Component-level coverage needed | Buy ICT or use flying probe |
| Regulated product with qualification burden | Buy, and budget for the qualification |
| Prototype and NPI | Build; requirements change weekly |
The software layer is worth building even when you buy the hardware, because it is where the traceability and the analytics live, and it is the part that has to outlast any individual fixture.
If You Already Run OpenHTF
ate.py27 lines
import openhtf as htffrom openhtf.plugs import BasePlugfrom openhtf.util import unitsfrom tofupilot.openhtf import uploadclass FixturePlug(BasePlug): def lid_closed(self) -> bool: return True@htf.measures(htf.Measurement("lid_closed").equals(True))@htf.plug(fixture=FixturePlug)def test_safety(test, fixture): test.measurements.lid_closed = fixture.lid_closed()@htf.measures( htf.Measurement("rail_3v3").in_range(3.2, 3.4).with_units(units.VOLT),)def test_power(test): test.measurements.rail_3v3 = 3.31test = htf.Test(test_safety, test_power, part_number="PCBA-100")test.add_output_callbacks(upload())test.execute(test_start=lambda: input("Scan serial: "))Install with pip install "tofupilot[openhtf]", or skip the callback and run it with tofupilot run.
Common Mistakes
| Mistake | Consequence | Fix |
|---|---|---|
| No interlock gating power | Fixture damage, safety incident | Gate energising phases on a safety phase |
| Power-down as the last phase | Board left energised after a failure | Put it in the plug's close() |
| No contact resistance measurement | Fixture wear looks like product failure | Measure and trend the interface |
| No station or fixture ID | Cannot separate a bad fixture from a bad lot | Record both on every run |
| Recording pass/fail only | No drift detection, no limit review | Record the measured values |
| Shared instrument in per-slot teardown | One slot powers down its neighbours | Use execution scope for shared resources |
