Skip to content

ARINC 429 LRU Acceptance Test

Automate a dual-bus avionics LRU acceptance test with two 28V supplies, an ARINC 429 analyzer, and a DMM, and trend the results to cut No-Fault-Found.

TofuPilotComponent TestPythonTofuPilot FrameworkGitHub
ARINC 429 LRU Acceptance Test

Introduction

The LRU and the ARINC 429 Bus

An LRU (line-replaceable unit) is a self-contained box in the avionics bay, swapped as a whole during maintenance and sent to a component shop for repair. Many LRUs sit on the aircraft's DC power distribution and report their status over ARINC 429, the standard digital databus for commercial avionics. A power distribution or bus control unit is a good example: it takes redundant 28 V DC feeds and transmits fault and status words to the flight computers and the central maintenance system.

Close-up of an avionics line-replaceable unit: finned aluminium housing, two circular multi-pin connectors on the front face, and the twisted shielded pairs of the ARINC 429 buses plugged in.

A typical LRU: sealed aluminium housing, circular connectors carrying both 28 V buses and the ARINC 429 pairs, swapped as one part on the aircraft and tested as one part in the shop.

Two properties of this class of unit shape the whole test. First, it has two segregated power inputs, a primary and a backup bus, electrically and mechanically separated so the loss of one supply cannot take the unit down. Aircraft DC power is nominally 28 V DC, produced by transformer-rectifier units from the 115 V AC buses. Second, it talks over ARINC 429, so the acceptance test has to speak that protocol to stimulate the unit and read it back.

ARINC 429 is a unidirectional, point-to-point databus: one transmitter drives up to twenty receivers on a twisted shielded pair, and a bidirectional link needs two buses. It runs at either low speed (12.5 kbit/s) or high speed (100 kbit/s), and the two rates cannot share a bus. Each message is a 32-bit word with five fields:

BitsFieldPurpose
1–8LabelOctal code identifying the data type
9–10SDISource/destination identifier
11–29DataThe payload (BNR, BCD, or discrete)
30–31SSMSign/status matrix (normal, failure warning, no-computed-data, functional test)
32ParityOdd parity over the word

The differential signal swings ±10 V between the two legs, with defined high, null, and low bands and a specified rise/fall time (1.5 ± 0.5 µs at high speed, 10 ± 5 µs at low speed). These are real, measurable pass/fail quantities on the bench.

One ARINC 429 word captured on the bus: bipolar return-to-zero differential voltage swinging between +10 V, null and -10 V, 32 bits over 320 µs at 100 kbit/s, with the label, SDI, data, SSM and parity fields shaded.

Label 206 as the mock analyzer captures it: bipolar return-to-zero, every bit spends its first half at +10 V or -10 V and its second half at null, 32 bits in 320 µs at high speed, odd parity in bit 32.

Purpose of the Test

When a removed unit arrives at the shop, the acceptance test per the OEM's CMM (component maintenance manual) has to prove three things before the unit is released on an EASA Form 1: it draws the right current on each bus, it transmits every expected ARINC 429 label correctly, and it fails over from one power bus to the other without dropping its output. This template builds that test as a structured, automated procedure so that every measured value, not just the pass/fail verdict, is recorded against the unit's serial number. The per-unit fingerprint:

  • Bus A and bus B current at matched 28.0 V, and their mismatch
  • Label word error rate and response latency on the ARINC 429 output
  • Bus waveform: one captured word with its rise time and HI level
  • Output accuracy of the decoded engineering-unit value
  • Failover: word error rate and bus B current with bus A cut
  • Power-down state of both buses, recorded by the teardown

The reason to keep the values matters as much as the test itself. A large share of avionics LRUs that come back to the shop test No Fault Found: field data puts avionics NFF as high as 20 percent of removals, and at the repair-bench level figures upward of 70 percent have been reported for some unit types. An NFF unit consumes a full bench cycle and returns to stock having taught the shop nothing, unless the measurements behind the pass are trended over time. ARINC 672 exists specifically as an industry guideline for reducing NFF, and structured test data is the raw material for it.

Beyond the avionics content, this template demonstrates four framework mechanics: one driver class with one thin subclass per instrument address for the two identical supplies, a multi-dimensional measurement with custom aggregations computed in Python and validated in YAML, a derived measurement combining two plugs, and a teardown stage that powers the unit down whatever happened before.

Equipment & Setup

To run a dual-bus ARINC 429 acceptance test you will need:

  • Two identical programmable DC supplies, one per power bus, each set to 28.0 V.
  • An ARINC 429 bus analyzer with transmit ports (to stimulate the unit's receive labels), receive ports (to capture and time-tag the unit's transmit labels), and a waveform capture on the pair.
  • A bench digital multimeter (DMM) for output voltage and insulation/dielectric checks.
  • The device under test (DUT): the LRU, wired to both supplies and to the analyzer's bus pairs.
  • A TofuPilot Framework procedure to power the unit, drive the analyzer, evaluate every measurement against its CMM limits, and provide a global pass/fail status.
  • The TofuPilot Dashboard to store results for traceability and analytics.

Avionics acceptance test bench: the line-replaceable unit on the bench mat with its circular connector, two identical 28 V supplies, a bench multimeter, the ARINC 429 analyzer with twisted shielded pairs, and the production test computer.

The bench: two identical supplies feeding bus A and bus B, the analyzer on the twisted shielded pairs, the DMM, and the test computer running the procedure.

Why Two Identical Supplies

The unit under test has two power inputs by design, for redundancy, so the bench must feed each one from its own independent supply. Using two supplies of the same model, held at a matched 28.0 V, means any difference in current draw between bus A and bus B is a property of the unit, not an artifact of two mismatched instruments. It also lets the test prove the failover directly: cut supply A, and confirm the unit rides through on supply B without interrupting its ARINC 429 output.

A common bench choice is a pair of 3-channel programmable supplies (30 V / 3 A per channel, sub-0.1 percent programming accuracy, SCPI-controllable over LAN, USB, or RS-232), for example two Rigol DP832-class units. The important point for the software is not the model but that there are two of them, identical, each at a different address.

ARINC 429 Analyzer

Production benches use a PCI/PXI or USB ARINC 429 interface with several transmit and receive channels (Astronics Ballard, AIM, or Holt evaluation boards). Word error rate and latency come from the channel's receive FIFO with hardware time tags; the waveform comes from an oscilloscope probe across the pair, or from the analyzer's own capture on interfaces that provide it. The template's mock synthesizes both from the same 32-bit encoder so the procedure runs without hardware.

Test Procedure

Overview

After connecting the unit to both supplies and to the analyzer pairs, the procedure runs:

  1. Power both buses to 28.0 V and record per-bus current and their mismatch.
  2. Measure the label 206 word error rate and response latency, capture one word and compute its rise time and HI level.
  3. Decode an engineering-unit output and compare it to the commanded value.
  4. Cut bus A, confirm the label error rate holds at zero on bus B alone, record bus B current.
  5. Teardown: both supplies to 0 V, confirm both buses read below 0.5 V.
  6. Validate every metric against its declared limits and stream the run to TofuPilot.

Why TofuPilot Framework

TofuPilot Framework is a YAML + Python test framework built for hardware. You describe what the test does in a procedure.yaml file and how in small Python phase and plug files. The framework handles Python environment management, the operator UI, measurement validation and live charts, and process isolation between phases and each piece of equipment.

Project Structure

procedure.yaml
phases
power_up.py
bus_integrity.py
output_accuracy.py
bus_failover.py
power_down.py
plugs
psu.py
arinc.py
dmm.py
utils
arinc_word.py
waveform.py
README.md
pyproject.toml

The plugs are mocks: the supplies return fixed currents with noise, the analyzer synthesizes a word on the bus. 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. It identifies the unit, declares the four instruments as plugs, lays out four main phases with their measurements and CMM limits, and a teardown phase:

procedure.yaml
name: ARINC 429 LRU Acceptance Testversion: 0.1.0description: Functional acceptance test for a dual-bus avionics LRU, using two identical 28V supplies, an ARINC 429 bus analyzer, and a DMM.unit:  auto_identify: true  serial_number:    description: "Serial from the LRU nameplate"    placeholder: "LRU000000"    pattern: "^LRU\\d{6}$"    default_value: "LRU000042"  part_number:    default_value: "622-1234-001"plugs:  - name: PSU Bus A    description: 28V DC supply for redundant bus A (mock Rigol DP832-class, VISA 192.168.1.50)    python: plugs.psu:PsuBusA    key: psu_a  - name: PSU Bus B    description: 28V DC supply for redundant bus B (mock Rigol DP832-class, VISA 192.168.1.51)    python: plugs.psu:PsuBusB    key: psu_b  - name: ARINC 429 Analyzer    description: ARINC 429 bus analyzer for label capture, latency and waveform (mock)    python: plugs.arinc:Arinc429Analyzer    key: arinc  - name: DMM    description: Digital multimeter for output accuracy (mock)    python: plugs.dmm:Dmm    key: dmmmain:  - name: Power Up    key: power_up    python: phases.power_up    measurements:      - name: Bus A Current        key: bus_a_current        unit: A        validators:          - {operator: "<=", expected_value: 1.8}      - name: Bus B Current        key: bus_b_current        unit: A        validators:          - {operator: "<=", expected_value: 1.8}      - name: Bus Current Mismatch        key: bus_current_mismatch        unit: A        description: Absolute difference between the two bus currents at matched 28.0 V. Two identical supplies make this a property of the unit.        validators:          - {operator: "<=", expected_value: 0.2}  - name: Bus Integrity    key: bus_integrity    python: phases.bus_integrity    depends_on: [power_up]    measurements:      - name: Label Word Error Rate        key: word_error_rate        description: Fraction of label 206 words received corrupted or missing over the capture window.        validators:          - {operator: "<=", expected_value: 0.00001}      - name: Response Latency        key: response_latency        unit: ms        validators:          - {operator: "<=", expected_value: 50}      - name: Label 206 Waveform        key: label_waveform        title: ARINC 429 word on the bus (label 206, high speed)        description: Differential voltage of one captured 32-bit word, bipolar return-to-zero at 100 kbit/s.        x_axis:          legend: Time          unit: µs        y_axis:          - legend: Differential voltage            key: v_diff            unit: V            aggregations:              - type: rise_time_us                unit: µs                validators:                  - {operator: ">=", expected_value: 1.0}                  - {operator: "<=", expected_value: 2.0}              - type: high_level_v                unit: V                validators:                  - {operator: ">=", expected_value: 9.0}                  - {operator: "<=", expected_value: 11.0}  - name: Output Accuracy    key: output_accuracy    python: phases.output_accuracy    depends_on: [bus_integrity]    measurements:      - name: Output Error        key: output_error        unit: "%"        validators:          - {operator: ">=", expected_value: -0.5}          - {operator: "<=", expected_value: 0.5}  - name: Bus Failover    key: bus_failover    python: phases.bus_failover    depends_on: [output_accuracy]    measurements:      - name: Failover Word Error Rate        key: failover_word_error_rate        description: Label 206 error rate while bus A is cut and the unit rides on bus B alone.        validators:          - {operator: "<=", expected_value: 0.00001}      - name: Bus B Current On Failover        key: bus_b_failover_current        unit: A        description: Bus B carries the whole unit once bus A is gone.        validators:          - {operator: "<=", expected_value: 3.0}teardown:  - name: Power Down    key: power_down    python: phases.power_down    measurements:      - name: Bus A Off Voltage        key: bus_a_off_voltage        unit: V        validators:          - {operator: "<=", expected_value: 0.5}      - name: Bus B Off Voltage        key: bus_b_off_voltage        unit: V        validators:          - {operator: "<=", expected_value: 0.5}

The limits shown are illustrative; on a real bench they come from the unit's CMM. Framework features to notice:

  1. Two plugs, one driver: psu_a and psu_b point at two subclasses of the same driver class; each phase that needs both names them as two parameters.
  2. Multi-dimensional measurement with custom aggregations: label_waveform stores the captured word as an x/y series; rise_time_us and high_level_v are free-named aggregations that Python computes on the series and the YAML validates against the bus spec.
  3. Derived measurement: bus_current_mismatch is computed in the phase from two plug readings and validated like any other value.
  4. Teardown stage: power_down runs after the main phases whatever their outcome, so a failed unit is never left energized on the bench.
  5. Unit serial pattern: the nameplate serial is validated against ^LRU\d{6}$ before the run starts.

Controlling Two Identical Instruments

Here is the question that trips up most people setting up a multi-instrument bench: how do you drive two supplies of the same model without duplicating the driver code, and where do the addresses live?

The framework instantiates a plug by calling the class with no arguments, so the cleanest pattern is one shared driver class plus a thin subclass per address. The driver logic is written once; each subclass just fixes which physical instrument it is:

plugs/psu.py
import randomclass ProgrammablePsu:    """Shared driver for a programmable DC supply at a given VISA address."""    def __init__(self, address, idle_current_a):        self.address = address        self._idle_current_a = idle_current_a        self._volts = 0.0        self._extra_load_a = 0.0        # self.inst = pyvisa.ResourceManager().open_resource(address)        print(f"Connected PSU at {self.address}")    def set_voltage(self, volts):        # self.inst.write(f":SOUR:VOLT {volts}")        self._volts = float(volts)    def measure_voltage(self):        # return float(self.inst.query(":MEAS:VOLT?"))        return self._volts + random.gauss(0.0, 0.01)    def measure_current(self):        # return float(self.inst.query(":MEAS:CURR?"))        if self._volts < 18.0:            return 0.0        return self._idle_current_a + self._extra_load_a + random.gauss(0.0, 0.01)    def take_over_load(self, amps):        """Mock only: the other bus dropped, this supply now carries its share."""        self._extra_load_a = float(amps)class PsuBusA(ProgrammablePsu):    def __init__(self):        super().__init__("TCPIP0::192.168.1.50::INSTR", idle_current_a=1.42)class PsuBusB(ProgrammablePsu):    def __init__(self):        super().__init__("TCPIP0::192.168.1.51::INSTR", idle_current_a=1.39)

Each subclass is referenced as its own plug in the YAML, with its own key. The framework runs each plug in its own process and passes both into a phase by matching the parameter name to the plug key. So psu_a and psu_b are two independent connections sharing every line of driver code, and adding a third supply is three more lines.

Power Up

Both buses go to 28.0 V and the phase records what each one draws. Two identical supplies at matched voltage make the per-bus comparison meaningful, so the mismatch is a measurement in its own right:

phases/power_up.py
def power_up(psu_a, psu_b, measurements, log):    log.info("Powering redundant buses A and B to 28.0 V")    psu_a.set_voltage(28.0)    psu_b.set_voltage(28.0)    i_a = psu_a.measure_current()    i_b = psu_b.measure_current()    log.info(f"Bus A {i_a:.2f} A, bus B {i_b:.2f} A")    measurements.bus_a_current = i_a    measurements.bus_b_current = i_b    measurements.bus_current_mismatch = abs(i_a - i_b)

Bus Integrity

The analyzer reports the label 206 error rate and the response latency, then captures one word on the pair. The phase pushes the raw samples into the multi-dimensional measurement and computes the two aggregations the YAML validates:

phases/bus_integrity.py
from utils.waveform import high_level, rise_timeLABEL = 0o206def bus_integrity(arinc, measurements, log):    log.info("Checking ARINC 429 label integrity and latency")    measurements.word_error_rate = arinc.measure_word_error_rate(LABEL)    measurements.response_latency = arinc.measure_latency_ms()    t_us, v_diff = arinc.capture_word(LABEL)    measurements.label_waveform.x_axis = t_us    measurements.label_waveform.y_axis.v_diff = v_diff    measurements.label_waveform.y_axis.v_diff.aggregations.rise_time_us = rise_time(t_us, v_diff)    measurements.label_waveform.y_axis.v_diff.aggregations.high_level_v = high_level(t_us, v_diff)    log.info(f"Rise time {rise_time(t_us, v_diff):.2f} us, HI level {high_level(t_us, v_diff):.2f} V")

The edge metrics live in a small utility module: the HI level is the mean of the settled top samples, the rise time is the 10-90 % crossing of the first null-to-HI edge:

utils/waveform.py
import numpy as npdef high_level(t_us, v_diff):    """Mean of the settled HI half-bits (top 10 % of samples, noise-tolerant)."""    v = np.asarray(v_diff)    top = np.sort(v)[int(0.9 * v.size) :]    return float(top.mean())def rise_time(t_us, v_diff):    """10-90 % rise time of the first null -> HI transition, in microseconds."""    t = np.asarray(t_us)    v = np.asarray(v_diff)    high = high_level(t_us, v_diff)    lo, hi = 0.1 * high, 0.9 * high    i_lo = int(np.argmax(v >= lo))    i_hi = int(np.argmax(v >= hi))    return float(t[i_hi] - t[i_lo])

Zoom on the first two bits of the captured word: the 10 to 90 percent rise time of 1.40 µs against the 1.5 ± 0.5 µs high-speed spec, and the 9.78 V HI level against the 9 to 11 V transmitter output limit.

The two aggregations on the captured word: 1.40 µs rise time inside the 1 to 2 µs window, 9.78 V HI level inside the 9 to 11 V transmitter output band. A driver with a failing output stage shows up here as a slow edge or a sagging HI level long before the label stops decoding.

Output Accuracy

An engineering-unit output is decoded and compared to the commanded value:

phases/output_accuracy.py
def output_accuracy(dmm, measurements, log):    log.info("Measuring output accuracy against reference")    measurements.output_error = dmm.measure_output_error_pct()

Bus Failover

The redundancy check is what a single-supply bench cannot do. With two independent supplies under software control, the phase drops bus A and confirms the unit holds its ARINC 429 output on bus B, and records how much current bus B carries alone:

phases/bus_failover.py
LABEL = 0o206def bus_failover(psu_a, psu_b, arinc, measurements, log):    log.info("Dropping bus A, confirming output holds on bus B")    psu_a.set_voltage(0.0)    psu_b.take_over_load(1.35)  # mock: bus B now carries the unit alone    measurements.failover_word_error_rate = arinc.measure_word_error_rate(LABEL)    i_b = psu_b.measure_current()    log.info(f"Bus B {i_b:.2f} A with bus A cut")    measurements.bus_b_failover_current = i_b

Bar chart of supply currents: 1.42 A on bus A and 1.39 A on bus B at power up under the 1.8 A per-bus limit, then 0 A on bus A and 2.74 A on bus B during failover under the 3.0 A limit.

Currents through the run: matched draw at power up (0.05 A mismatch against a 0.2 A limit), then the whole unit on bus B once bus A is cut. Because each supply is its own plug, cutting one and watching the other is a two-line phase, not a bench rewire.

Power Down

The teardown always runs, even when a main phase failed, so no unit is left energized on the bench:

phases/power_down.py
def power_down(psu_a, psu_b, measurements, log):    """Teardown: always runs, so the unit is never left powered on the bench."""    log.info("Powering down both buses")    psu_a.set_voltage(0.0)    psu_b.set_voltage(0.0)    measurements.bus_a_off_voltage = abs(psu_a.measure_voltage())    measurements.bus_b_off_voltage = abs(psu_b.measure_voltage())

Mock Plugs

The analyzer mock synthesizes the word it "captures": the 32 bits come from a small encoder that builds the label, SDI, data, SSM and odd parity in transmission order, then the bipolar return-to-zero waveform is shaped with first-order edges at 20 MS/s so the rise-time aggregation has something real to measure:

utils/arinc_word.py
def encode_word(label_octal, sdi, data, ssm):    """Return the 32 bits of a word in transmission order (bit 1 first).    Bits 1-8 label, transmitted MSB first (the label octal's high digit goes out    first), bits 9-10 SDI, bits 11-29 data (LSB first), bits 30-31 SSM, bit 32    odd parity over bits 1-31.    """    label = int(label_octal) & 0xFF    bits = [(label >> (7 - i)) & 1 for i in range(8)]    bits += [(sdi >> i) & 1 for i in range(2)]    bits += [(data >> i) & 1 for i in range(19)]    bits += [(ssm >> i) & 1 for i in range(2)]    bits.append(0 if sum(bits) % 2 else 1)    assert len(bits) == 32    return bits

Running the procedure from its directory executes the five phases in order and validates every measurement:

text
→ Phase: Power Up→ Bus A 1.43 A, bus B 1.38 A✓ Phase Power Up: PASS→ Phase: Bus Integrity→ Rise time 1.40 us, HI level 9.78 V✓ Phase Bus Integrity: PASS→ Phase: Output Accuracy✓ Phase Output Accuracy: PASS→ Phase: Bus Failover→ Bus B 2.74 A with bus A cut✓ Phase Bus Failover: PASS→ Phase: Power Down✓ Phase Power Down: PASS✓ Run complete: PASS

To use it on a real bench, uncomment the pyvisa calls in plugs/psu.py and replace the analyzer mock with your interface's API; the phases, measurements, and limits stay exactly the same.

Run your first test in minutes