32-Channel Test Stand Telemetry DAQ
A ruggedized data acquisition board for propulsion and structural test benches — 32 analog channels, hardware timestamping, and a Python host stack.
Overview
Test-stand data is only as good as its time base. This board acquires 32 differential channels — bridge, thermocouple, and voltage — with a hardware time base disciplined to an external trigger, so pressure, thrust, and strain from separate boxes can actually be overlaid on the same axis afterwards.
| Year | 2024 |
| Role | Hardware and firmware design, host software |
| Disciplines | Embedded Firmware, PCB Design, Test & Verification, Mechanical & Enclosure CAD |
Signal chain
Each channel is a differential input with programmable gain, an anti-alias filter set for the channel's configured rate, and a 24-bit delta-sigma converter. Excitation for bridge channels is generated on-board and sensed ratiometrically so that excitation drift cancels out of the measurement instead of showing up as a slow strain signal.
Specifications
| Channels | 32 differential, individually configurable |
| Input types | ±10 V, bridge (350 Ω / 1 kΩ), type K/T/J thermocouple |
| Resolution | 24-bit ΔΣ, 50 kS/s per channel |
| Isolation | 1 kV channel-to-chassis |
| Time base | TCXO disciplined to external 1 PPS or trigger |
| Interface | Gigabit Ethernet, UDP streaming + TCP control |
| Storage | Local ring buffer to microSD on link loss |
| Power | 18 – 36 V DC, 11 W typical |
Select a numbered marker for the design rationale behind that region.
"""Host-side reader for the DAQ UDP stream.
Frames are fixed-size so that a dropped packet costs exactly one frame and
never desynchronises the decoder. Sequence numbers are used to detect gaps
and to reconstruct the time base without trusting host arrival time.
"""
import socket
import struct
import numpy as np
FRAME = struct.Struct("<IQH H 32i") # seq, t_ns, flags, crc, 32 samples
CHANNELS = 32
def stream(host: str = "0.0.0.0", port: int = 5005, seconds: float = 10.0):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((host, port))
sock.settimeout(1.0)
samples, stamps, expected, dropped = [], [], None, 0
while len(stamps) < int(seconds * 50_000):
seq, t_ns, flags, _crc, *chans = FRAME.unpack(sock.recv(FRAME.size))
if expected is not None and seq != expected:
dropped += (seq - expected) & 0xFFFFFFFF
expected = (seq + 1) & 0xFFFFFFFF
stamps.append(t_ns)
samples.append(chans)
data = np.asarray(samples, dtype=np.int32).reshape(-1, CHANNELS)
t = (np.asarray(stamps, dtype=np.uint64) - stamps[0]) * 1e-9
return t, data * (10.0 / (1 << 23)), dropped # volts, dropped frames
Verified performance
Questions about this design? I am happy to walk through the trade studies, the measurements, or anything I glossed over here — drakeajoseph@gmail.com.