Summary
This RFD captures a notional interface for a Thruster Test Stand Controller implemented on a Digilent Arty A7-100T. The central design choice is that the FPGA, rather than a host operating system or microcontroller software loop, owns every time-critical output edge after an actuation command is accepted.
The host configures the sequence, asks for an operation, receives a structured acknowledgement, and records a loss-detectable 1 kHz status stream. The FPGA uses a local microsecond counter to schedule active-high 3.3 V logic outputs for Spark, Fuel, Oxidizer, three HitHold observations, and two heaters.
The proposed determination is:
- use one fixed, versioned UDP command and status contract;
- use CCSDS-style primary headers while keeping Project 14 time fields in the secondary status body;
- keep command values in milliseconds but execute edges from the FPGA's local microsecond counter;
- keep setup commands separate from actuation commands;
- reject ambiguous, overlapping, out-of-range, or replay-adjacent fire requests before any output changes;
- report physical outputs, controller state, diagnostics, operation identity, and queue overflow in every status packet;
- keep the interactive model in this RFD non-transmitting and allocate only waveform transition nodes rather than sampled traces.
This is a discussion artifact and prototype interface, not a safety-rated, flight-qualified, or propellant-test authorization.
Problem statement
A host can conveniently describe a fire sequence, but a general-purpose operating system cannot guarantee the relative timing of ignition and valve edges. Network latency also cannot be made deterministic merely by choosing Ethernet or UDP.
The interface therefore separates two timing questions:
- When does the FPGA receive and accept the request? This remains subject to host, network, parsing, and queue latency.
- What happens after the FPGA accepts it? This is scheduled by synchronous FPGA logic against one local counter, independent of later host scheduling.
The contract needs to make that boundary visible. It also needs enough diagnostic information for the host to distinguish acceptance, rejection, busy state, idempotent no-change behavior, telemetry loss, and queue overload.
Goals
The notional controller should:
- produce deterministic relative edge timing after command acceptance;
- expose a small command surface with exact binary layouts;
- preserve command identity with nonzero request IDs;
- return one structured acknowledgement for every addressed command that reaches Project 14 validation;
- stream status at 1 kHz without making status delivery part of the actuation timing loop;
- make overflow and packet loss observable;
- force every controlled output low on controller reset;
- keep heaters independent from fire sequencing and fire cutoff;
- provide Python methods for every command plus decoded and raw recording.
Non-goals
This RFD does not propose:
- DHCP, TCP, routing, DNS, or a general network stack;
- deterministic Ethernet delivery;
- a standardized CCSDS mission time code;
- cryptographic authentication or a complete durable replay window;
- direct valve, solenoid, heater, or igniter drive;
- isolation, flyback suppression, arm/permissive, emergency stop, or watchdog hardware;
- flight qualification or energized test approval;
- behavior for the still-undefined Boost signal.
System boundary
The wire diagrams begin at the UDP payload. Ethernet, IPv4, and UDP headers are outside the CCSDS-style packet. The FPGA uses fixed identity:
| Item | Value |
|---|---|
| Board MAC | 02:00:00:00:00:7A |
| Board IPv4 | 169.254.209.0/16 |
| Board UDP port | 24576 (0x6000) |
| Recommended host IPv4 | 169.254.209.1/16 |
| Command APID | 0x120 |
| Telemetry APID | 0x121 |
| Interface version | 0x01 |
| Status telemetry ID | 0x4101 |
The direct link has no gateway requirement. DNS is not implemented in the FPGA.
Concept of operations
A nominal non-energetic workflow is:
- Reset the controller and independently make every external load safe.
- Establish link and use ARP plus
time.getfor non-actuating identity checks. - Store a valid Fire Sequence, HitHold duration, and fire maximum duration.
- Configure independent heater maximums if heaters will be used.
- Start periodic telemetry and begin raw plus decoded recording.
- Send one actuation command with an explicit nonzero request ID and adequate delay for any negative edge offsets.
- Correlate the acknowledgement by request ID.
- Let the FPGA own every scheduled edge through normal completion, cutoff, or reset.
- Stop telemetry and preserve capture evidence.
The setup commands store values only. They do not establish time zero and do not
actuate an output. An accepted fire.single or fire.duty snapshots the stored
Fire Sequence, cutoff tail, HitHold duration, and fire maximum.
Timing model
For Single Fire and the first Duty Cycle:
The six edge offsets are signed milliseconds relative to t0. Each channel's
OFF offset must be strictly greater than its ON offset. An actuation request is
rejected if any calculated edge would precede command receipt.
For later Duty Cycle iterations:
The latest prior-cycle OFF edge must be strictly earlier than the next safe boundary. That boundary also accounts for a negative ON offset in the next cycle. Equality rejects rather than relying on event ordering within one tick.
Digital signal contract
All outputs are active-high 3.3 V logic. 0 means OFF and 1 means ON.
| PMOD pin | Signal | Behavior |
|---|---|---|
| JD1 | Spark | Independent Fire Sequence window |
| JD2 | Fuel | Independent Fire Sequence window |
| JD3 | Oxidizer | Independent Fire Sequence window and cutoff tail |
| JD4 | HitHoldShared | FuelHitHold OR OxidizerHitHold |
| JD7 | FuelHitHold | Pulse on each Fuel rising edge |
| JD8 | OxidizerHitHold | Pulse on each Oxidizer rising edge |
| JC1 | FuelHeater | Independent maximum-duration heater output |
| JC2 | OxidizerHeater | Independent maximum-duration heater output |
A Fuel rising edge starts FuelHitHold; an Oxidizer rising edge starts
OxidizerHitHold. A same-channel retrigger restarts its full configured
duration. The shared output is derived, not independently scheduled.
At the fire maximum-duration deadline:
- Spark and Fuel immediately turn OFF;
- both per-propellant HitHold outputs immediately turn OFF;
- later Spark, Fuel, and HitHold edges are suppressed;
- Oxidizer stays ON only if it was already ON;
- an already-high Oxidizer stays ON for the configured
max_duration_oxidizer_tail_ms, then turns OFF; - the cutoff never creates a new Oxidizer rising edge;
- remaining Duty Cycle iterations are canceled;
- heaters are unchanged.
Command interface
Every command has a six-byte CCSDS-style primary header, an eight-byte Project 14 command header, and an exact fixed-binary argument block. Multi-byte values are big-endian. Signed values use two's-complement representation.
| Command | ID | Arguments | Argument bytes |
|---|---|---|---|
fire.set_sequence | 0x3001 | six i32 edge offsets, u32 oxidizer tail | 28 |
hithold.set_duration | 0x3002 | u32 duration_ms | 4 |
fire.set_max_duration | 0x3003 | u32 duration_ms | 4 |
time.get | 0x3004 | none | 0 |
fire.single | 0x3010 | u32 delay_ms | 4 |
fire.duty | 0x3011 | u32 delay_ms, u32 off_duration_ms, u16 cycles | 10 |
heater.on | 0x3020 | u8 target | 1 |
heater.off | 0x3021 | u8 target | 1 |
heater.set_max | 0x3022 | u8 target, u32 duration_ms | 5 |
telemetry.start | 0x3030 | none | 0 |
telemetry.stop | 0x3031 | none | 0 |
The fixed command header contains:
| Bit offset | Bits | Field | Required value |
|---|---|---|---|
| 0 | 3 | CCSDS version | 0 |
| 3 | 1 | Packet type | 1 |
| 4 | 1 | Secondary-header flag | 1 |
| 5 | 11 | APID | 0x120 |
| 16 | 2 | Sequence flags | 0b11 |
| 18 | 14 | Sequence count | Host-selected |
| 32 | 16 | Packet length | Bytes after primary header minus one |
| 48 | 8 | Interface version | 0x01 |
| 56 | 8 | Message type | 0x01 |
| 64 | 16 | Command ID | Catalog value |
| 80 | 16 | Request ID | 1..65535 |
| 96 | 8 | Argument format | 0x01 |
| 104 | 8 | Argument length | Exact catalog length |
Request ID zero is reserved for unsolicited periodic telemetry. The FPGA rejects
a zero-ID command with a structured REQUEST_ID / FIELD_OUT_OF_RANGE
diagnostic. The host allocator wraps 65535 to 1, but allocator state is
process-local and not a durable replay policy.
Telemetry interface
Solicited acknowledgements and unsolicited periodic status use one fixed 54-byte packet. The status packet reports:
- command request ID or zero for periodic status;
- disposition (
ACCEPTED,BUSY,ACCEPTED_NO_CHANGE, orREJECTED); - all eight physical output states;
- pending, active, Duty Cycle, cutoff/tail, telemetry, and overflow flags;
- rejected field, error code, observed value, and reference value;
- 64-bit FPGA
time_usand retained/currentt0_us; - 32-bit
sample_sequence; - active request ID, cycle index, and cycle count;
- saturating RX and TX overflow counters.
The complete 54-byte UDP-payload layout is:
| Byte offset | Bytes | Field | Wire type | Value or meaning |
|---|---|---|---|---|
| 0 | 2 | Primary header word | bit-packed u16 | Version 0, telemetry type 0, secondary header 1, APID 0x121 |
| 2 | 2 | Sequence word | bit-packed u16 | Flags 0b11; low 14 bits mirror sample_sequence |
| 4 | 2 | Packet length | big-endian u16 | 47 (bytes after the primary header minus one) |
| 6 | 1 | Interface version | u8 | 0x01 |
| 7 | 1 | Message type | u8 | 0x02 status telemetry |
| 8 | 2 | Request ID | big-endian u16 | Correlated command ID; zero for periodic status |
| 10 | 2 | Telemetry ID | big-endian u16 | 0x4101 |
| 12 | 1 | Disposition | u8 | 0x80 accepted, 0x81 busy, 0x82 no change, 0xE0 rejected |
| 13 | 1 | Output flags | bit-packed u8 | Physical output state bitmap |
| 14 | 1 | Controller flags | bit-packed u8 | Sequencer, stream, and sticky-overflow bitmap |
| 15 | 1 | Rejected field | u8 | Field identifier, or zero |
| 16 | 2 | Error code | big-endian u16 | Diagnostic identifier, or zero |
| 18 | 4 | Observed value | big-endian i32 | Rejected or computed value |
| 22 | 4 | Reference value | big-endian i32 | Limit or comparison value |
| 26 | 8 | time_us | big-endian u64 | FPGA time since reset |
| 34 | 8 | t0_us | big-endian u64 | Retained/current cycle reference |
| 42 | 4 | sample_sequence | big-endian u32 | Loss-detection sequence |
| 46 | 2 | Active request ID | big-endian u16 | Zero while idle |
| 48 | 2 | Duty Cycle index | big-endian u16 | Current zero-based cycle |
| 50 | 2 | Duty Cycle count | big-endian u16 | Snapshotted requested count |
| 52 | 1 | RX overflow count | u8 | Saturating counter |
| 53 | 1 | TX overflow count | u8 | Saturating counter |
Output flag bits 0..7 are Spark, Fuel, Oxidizer, FuelHitHold,
OxidizerHitHold, HitHoldShared, FuelHeater, and OxidizerHeater. Controller flag
bits 0..6 are pending, active, Duty Cycle, cutoff active, sticky RX overflow,
sticky TX overflow, and telemetry streaming; bit 7 is reserved and transmitted
as zero.
The CCSDS primary header does not inherently include a timestamp. This interface places its local time-since-reset values in the Project 14 status body. They are not UTC and are not synchronized to an external clock.
The 32-bit sample sequence is the loss-detection authority. Its low 14 bits are also placed in the CCSDS sequence count. A recorder should classify missing, duplicate, reordered, and modulo-32-bit rollover behavior while preserving the validated 54-byte packets verbatim.
Interactive notional model
The explorer below is intentionally non-transmitting. It serializes the selected command in memory, shows every field and exact byte, applies the notional validation rules, and derives output transitions.
The waveform is not a sampled telemetry trace. Each active interval contributes only four point-line nodes: low to the rising edge, vertical high, horizontal to the falling edge, and vertical low. Flat signals need only domain endpoints. Duty Cycle rendering is capped at six cycles even when the packet requests more.
Validation and rejection behavior
The controller rejects:
- malformed Project 14 envelopes;
- unsupported APIDs or command IDs;
- bad argument format or length;
- scalar values outside their controller range;
- any channel whose OFF offset is not strictly later than ON;
- an actuation edge that would precede command receipt;
- a Duty Cycle whose prior output overlaps the next safe boundary;
- actuation before a valid Fire Sequence is stored;
- actuation while another fire operation owns the sequencer;
- a heater target other than Fuel or Oxidizer;
- a required nonzero maximum duration that is zero;
- reuse of the most recently accepted Fire actuation request ID.
The duplicate check is deliberately narrow. It does not retain a durable history, does not cover heater commands, and clears on reset.
Queueing and loss
Complete command, response, ARP, and periodic-stream records cross clock domains through bounded asynchronous queues. A reserved response slot prevents a dispatched command acknowledgement from being lost when the response queue is temporarily full.
Command responses have transmit priority over ARP and periodic status. Periodic status is best-effort and loss-detectable. A dropped stream sample increments a saturating TX-overflow counter. Response backpressure does not.
This distinction keeps actuation command outcome reporting stronger than periodic observation without pretending that UDP itself is lossless.
Safety boundary
This interface is suitable only for logic-level prototyping until an external safety architecture is defined. The FPGA outputs shall not directly drive an igniter, valve, solenoid, relay coil, or heater.
External circuitry must provide:
- galvanic isolation where required;
- logic buffering and load-current drive;
- inductive transient and flyback suppression;
- safe output bias while the FPGA is unconfigured;
- hardware arm/permissive and emergency-stop functions;
- watchdog and fault-response behavior;
- independent power removal appropriate to the hazard analysis.
Ethernet reachability, a valid acknowledgement, or a clean status stream is not an authorization to energize a fixture.
Alternatives considered
Host-timed output changes
The host could send one network command per edge. This is simple conceptually but makes relative timing depend on the host scheduler, network, parser, and queue state. It does not satisfy the main determinism goal.
Microcontroller-owned sequence
A microcontroller can provide good timing with a carefully designed hardware timer architecture. It remains a viable implementation, but the FPGA path provides explicit parallel edge comparators, clock-domain boundaries, and fixed-latency output state updates in the existing Arty Ethernet prototype.
Sampled waveform storage
The UI could allocate a value for every signal at every millisecond. That scales with duration and display resolution even though digital outputs change only at a small number of edges. Sparse point-lines preserve the exact ideal waveform with memory proportional to transitions.
Open questions
- What hardware arm/permissive, emergency-stop, and watchdog signals must gate all energetic outputs?
- Should command freshness use a larger persisted request space, time window, session nonce, or authenticated sequence?
- Should a future interface use a standardized CCSDS time code or externally synchronized execution time?
- What is the required response to link loss while a fire sequence or heater is active?
- Should Boost be defined, assigned a pin and command, or removed from the controller vocabulary?
- What Rigol probe mapping and electrical interface will become the approved timing-verification setup?
Determination
Proceed with the fixed, versioned command and status contract as a bench prototype. Keep deterministic edge ownership in the FPGA, keep host and network latency outside the accepted sequence, expose complete state and loss indicators, and preserve exact packet bytes in host recordings.
Do not treat the current replay check, reset behavior, PMOD outputs, or Ethernet observability as safety qualification. Energetic use remains blocked on an independent hardware safety design and explicit verification plan.