Design for testability is the set of decisions made during layout that determine how much a board costs to test for the rest of its life. Most of them cost nothing at design time and are impossible to add later.
Why It Is a Layout-Time Decision
A board that reaches production without test access leaves three options, all bad: build an expensive fixture that probes awkward places, skip coverage and ship risk, or respin the PCB. The cost curve is steep and one-directional, which is why DFT reviews belong before layout is released, not after the first prototypes fail on the bench.
Test Points
| Practice | Recommendation |
|---|---|
| Diameter | 0.9-1.0 mm pad, 0.6 mm minimum for fine-pitch probes |
| Spacing | 2.54 mm preferred, 1.27 mm minimum between adjacent points |
| Edge clearance | At least 3 mm from the board edge for fixture tooling |
| Side | All on one side if possible; double-sided fixtures roughly double cost |
| Finish | Solderable finish, not solder-masked |
| Labelling | Silkscreen the net name; a bench technician will thank you |
| Tooling holes | Two non-plated holes, diagonally opposite, for fixture alignment |
What to bring out as a test point:
- Every power rail, on the load side of any filtering
- Ground, several times, close to the rails they return
- Reset, boot mode and strap pins
- I2C/SPI bus lines that a fixture may need to drive or observe
- Crystal or clock output, if a frequency check is planned
- Any net the test plan intends to measure
The last line is the actual rule. DFT works when the test plan exists before layout is released, even in draft.
Power Rail Access
Rails are the highest-value test points because nearly every failure mode shows up there first. Bring out each rail separately rather than relying on a single point downstream of all of them, so a measurement can localise the fault.
Consider a way to isolate rails from their loads, such as a zero-ohm link or a solder jumper, to distinguish a shorted regulator from a shorted load. A board that fails with a collapsed rail is otherwise a diagnostic dead end.
Programming and Debug Access
| Interface | What to expose |
|---|---|
| SWD/JTAG | Clock, data, reset, ground, and target voltage reference |
| UART console | TX, RX, ground, at a documented baud rate |
| Boot mode | A strap or test point that can force the bootloader |
| Reset | A test point the fixture can drive, not only a button |
Programming at end of line is normal, so the fixture needs to flash firmware, set the boot mode and reset the target without human intervention. If the boot strap is only a physical jumper, someone is moving jumpers on every board.
Boundary Scan
For dense BGA designs where physical probing cannot reach, IEEE 1149.1 boundary scan tests interconnects through the device itself.
| Requirement | Note |
|---|---|
| TCK, TMS, TDI, TDO, TRST | Bring out to test points or a connector |
| Chain order | Document it; the tooling needs it |
| Non-scan devices | Break the chain; note what is not covered |
| Pull-ups | Per the standard, so the chain is deterministic at power-up |
Firmware Hooks Are Part of DFT
The most under-used testability feature is firmware that cooperates with the test.
| Hook | Value in production test |
|---|---|
| Self-test command | Exercises peripherals the fixture cannot reach |
| Version query | Confirms the right image is installed |
| ADC raw readback | Verifies the analog front end without external stimulus |
| GPIO set/get | Enables loopback testing without extra hardware |
| Calibration write | Stores coefficients computed by the test |
| Fault injection | Verifies protection circuits deliberately |
A selftest command returning structured output turns a whole class of expensive fixture hardware into a serial query. It costs a day of firmware work and saves fixture cost on every station.
Design the output to be machine-readable from the start:
import jsonclass Dut: """Serial console on the board under test.""" def selftest(self) -> dict: """Firmware returns JSON, so the test does not parse prose.""" raw = self._query("selftest --json") return json.loads(raw) def _query(self, cmd: str) -> str: return '{"adc_counts": 2048, "gpio_ok": true, "temp_c": 41.2}'def selftest(measurements, dut): result = dut.selftest() measurements.adc_counts = result["adc_counts"] measurements.gpio_ok = result["gpio_ok"] measurements.die_temperature = result["temp_c"]procedure.yaml19 lines
main: - name: Selftest python: phases.selftest measurements: - name: ADC Counts validators: - operator: ">=" expected_value: 2000 - operator: "<=" expected_value: 2096 - name: GPIO Ok validators: - operator: "==" expected_value: true - name: Die Temperature unit: "°C" validators: - operator: "<=" expected_value: 85Structured output matters more than it looks. A firmware self-test that prints human prose forces every test script to parse it, and every firmware wording change breaks production.
Fixture Considerations
| Practice | Why |
|---|---|
| Keep tall components away from probe areas | Probes need clearance |
| Avoid test points under connectors or shields | Unreachable once assembled |
| Provide a ground plane reference near high-speed probes | Otherwise the measurement is the fixture |
| Add a fixture calibration short | Lets the test measure its own contact resistance |
| Standardise tooling hole positions across products | One fixture base serves many boards |
The calibration short is worth singling out. A known short across two pins lets the test measure contact resistance directly, so fixture wear is detected as a trend rather than as mysterious intermittent failures on good boards.
Panel and Multi-Up Considerations
If boards are tested in panel, decide early whether the fixture tests all positions at once. Testing four boards in parallel needs four sets of probes and four instrument paths, or a switching matrix. Both affect the layout, and retrofitting either is expensive.
The DFT Review
Before layout release, walk the test plan against the layout:
- Every measurement in the plan has an accessible test point.
- Every rail can be measured, and ideally isolated.
- Programming and reset are driveable by the fixture.
- Boot mode can be forced without human intervention.
- Test points are one side, correctly spaced, clear of the edge.
- Tooling holes exist and are documented.
- Firmware exposes the hooks the plan depends on.
- Dense areas have boundary scan or accept documented coverage gaps.
An hour of this review is routinely worth more than a week of fixture engineering later.
If You Already Run OpenHTF
selftest.py21 lines
import openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures( htf.Measurement("adc_counts").in_range(2000, 2096), htf.Measurement("gpio_ok").equals(True), htf.Measurement("die_temperature") .in_range(maximum=85) .with_units(units.DEGREE_CELSIUS),)def phase_selftest(test): test.measurements.adc_counts = 2048 test.measurements.gpio_ok = True test.measurements.die_temperature = 41.2test = htf.Test(phase_selftest, part_number="PCBA-100")test.add_output_callbacks(upload())test.execute(test_start=lambda: input("Scan serial: "))Install with pip install "tofupilot[openhtf]", or run it with tofupilot run.
Common Mistakes
| Mistake | Consequence | Fix |
|---|---|---|
| Test points added after layout | Awkward fixture, or a respin | DFT review before layout release |
| Test points on both sides | Fixture cost roughly doubles | Consolidate to one side |
| Solder-masked test pads | Probes cannot make contact | Specify a solderable finish |
| Boot strap only as a physical jumper | Manual intervention per board | Make it fixture-driveable |
| Firmware self-test printing prose | Every wording change breaks production | Structured output, JSON |
| No fixture calibration short | Fixture wear looks like product failure | Add one and trend contact resistance |
| No tooling holes | Alignment by hand, inconsistent contact | Two non-plated holes, diagonal |
