C++ application programming interface
The C++ code in c++/ serves two closely related roles:
- it is the host-side control plane for the PulsePins hardware
- it is also the implementation substrate for the
pptoolcommand family
The architecture is intentionally layered so command-line tools, tests, and future bindings can share the same sequence and hardware-control model.
Host-side architecture
The main entry point is c++/pptool.cc.
At startup it:
- builds a shared
HostRuntimefromhost_runtime.hh - that runtime parses common options, enables the shared runtime policy, constructs the single
FPGAobject, and performs the startup frequency-meter report - dispatches to a command handler based on the executable name
That dispatch model is why one compiled binary can appear as multiple tools such as pptool, ppfg, ppcounter, ppdelay, and ppplay.
The most important host-side layers are:
fpga.hh- top-level ownership of memory maps, PLL helpers, trigger monitors, and GPIO-backed control pathsstartup.hh- common process bootstrap and default FPGA startup policyhost_runtime.hh- shared bootstrap/runtime object used by the main host-side executablesoptions.hh,pll_rules.hh- typed option resolution and symbolic PLL presets shared by multiple toolspptool_streaming.cc,pptool_measurement.cc- user-facing command implementationsppworkflow.hh- shared streaming workflow used by commands that send sequences, arm/force triggers, and optionally validate readbackelements.hh,sequence.hh- host-side representation of pulse programs and trigger programs- subsystem wrappers such as
streamer.hh,readback.hh,counter.hh,timestamp.hh,freq_meter.hh, and the trigger/combiner helpers intrigger.hh,trigger_int.hh, andtrigger_ext.hh
For a maintainer-oriented walkthrough of the directory, see c++/README.md.
Common execution flow
For streamer-oriented tools, the typical host-side path is:
- build or parse a
Sequence - construct a transport such as a FIFO-backed streamer or DMA-backed streamer
- transmit the sequence to the FPGA
- enable or force the trigger
- optionally validate the readback stream
- wait for completion and inspect final status, counters, FIFO statistics, and CRCs
That common pattern is centralized in send_and_trig(...) in ppworkflow.hh so that behavior stays consistent across multiple tools.
The startup path follows the same philosophy: HostRuntime centralizes executable bootstrap, CLI and environment inputs are normalized by options.hh, then startup.hh applies the resulting clock-selection and PLL policy through the FPGA wrapper.
Trigger configuration follows a similar split:
options.hhresolves trigger-related CLI switches into aTriggerOptionspolicy objecttrigger.hhapplies that policy to the trigger combinertrigger_int.hhandtrigger_ext.hhexpose the direct software-driven and observed trigger PIO paths
Data types for sequence representation
The host-side sequence model mirrors the encoded data consumed by the streamer core. The core types are defined in elements.hh and sequence.hh.
Besides the text sequence format used by several tools, Sequence also supports VCD import and deterministic waveform export back to VCD. The export path targets sequences that can be reduced to a regular effective output waveform; control-flow and random elements are intentionally rejected.
For exact, lossless interchange, Sequence also supports a self-describing binary format that preserves the full internal sequence representation, including control-flow elements and the force-trigger flag.
In practice this also makes PulsePins useful as a simple logic-analyzer backend for deterministic readback waveforms.
Current serialization capability matrix:
| Format | C++ | Python | CLI |
|---|---|---|---|
| PulsePins text sequence format | import + export | import + export | export via ppread -save-text; import in selected workflows |
| VCD | import + export | import + export | import via ppplay (ppvcd compatibility alias); export via ppread -save-vcd |
| PulsePins binary sequence format | import + export | import + export | import via ppplay, export via ppread -save-binary |
Class hierarchy for counter and value helper objects:
-
Counter(defined inelements.hh): lightweight wrappers that carry the repetition count and the strobe policy for regular elementsStrobe: regular elements that assert the valid/strobe outputNoStrobe: regular elements that update qout without asserting the valid/strobe output
Interface of Counter objects:
count(): returns the count valuecount_str(): the count value as a string in hexadecimal and decimal form for inspection and troubleshootingcontrol_bits(): returns the strobe-related bits to be or'd into the control worddesc(): descriptive string for the strobe policy
Update types (d is the input value, q_prev is the previous output value, q is the new streamed value):
LOAD:q = dSET:q = q_prev | dCLEAR:q = q_prev & ~dFLIP:q = q_prev ^ dNOT:q = ~q_prevAND:q = q_prev & dOR:q = q_prev | dXOR:q = q_prev ^ dXNOR:q = ~(q_prev ^ d)SLL:q = q_prev << dSRL:q = q_prev >> d
The corresponding Value helper hierarchy is still exposed in elements.hh:
ValueBitLoadBitSetBitClearBitFlipBitNotBitAndBitOrBitXorBitXnorBitSllBitSrlTriggerCondition
These helper types are now lightweight tags and inspectors for authored elements. The el object itself stores flattened raw control, count, and value fields; it no longer owns std::shared_ptr wrappers internally.
Interface of Value objects:
value(): returns the stored valuevalue_str(): value in hexadecimal and decimal form for inspection and troubleshootingresult(value_t v_prev): returns the updated value for a given previous output statemode_bits(): returns the update-mode bits to be or'd into the control worddesc(): descriptive string for the update mode
TriggerCondition remains a specialized Value subclass with a constructor that takes pattern, mask, and final. Its string formatting decomposes the packed trigger value back into pattern and mask fields.
Class el (defined in elements.hh) represents one encoded sequence element. Supported construction paths are:
el(): sequence terminator with the default final output valueel(value_t): sequence terminator with an explicit final output valueel(count_t, value_t): regular element,BitLoadupdateel(Counter, value_t): regular element with explicit strobe policy,BitLoadupdateel(Counter, Value): regular element with explicit strobe and update modeel(trigger_t, trigger_t, bool): trigger condition elementel(Replay, count_t, value_t): replay a stored subsequenceel(Retrig, value_t): stop streaming and wait for a retrigger eventel(PseudoRandom, count_t): emit pseudo-random values for the selected count
Important implementation detail: the effective element kind is now derived from the encoded control word rather than stored separately. el keeps small authored metadata only where the raw wire format is ambiguous for regular elements (for example, to preserve descriptive strings around plain Value vs authored BitLoad construction).
Useful el methods:
control(),count(),value(): raw integer representation sent to the streamer/RLE decoderkind(): returns the derivedel_typemode(): returns the regular-mode bits from the control wordno_strobe(),is_stored(),store_slot(): inspect regular-element control metadatatrigger_pattern(),trigger_mask(),trigger_is_final(): inspect trigger elementsupdated_value(value_t): compute the effective streamed output for a previous output statedesc(): detailed debug descriptiondecode(): currently exposes extra control decorations such as thestoreslotregular_token()andsequence_record(): render the element into the PulsePins text sequence grammar
Preferred transformation helpers on el are now immutable:
stored_in(int)with_control(control_t)with_count(count_t)with_counter(Counter)with_regular_value(Value)as_bitload_after(value_t)
The older mutating helpers set_control(), set_count(), set_value(), and store() are still present for compatibility, but new code should prefer the immutable helpers above.
elements.hh also centralizes reconstruction helpers that are used by binary and text I/O:
el::classify_control(...)el::from_raw_triplet(...)el::from_regular_token(...)el::is_regular_token(...)
el objects can be compared and sent to an std::ostream.
Class Sequence (defined in sequence.hh) represents a sequence of elements. Internally, this is an overloaded std::deque of el objects.
Public member functions are:
size(): total number of elementsdata_size(): number of regular elementslength(): total duration of the sequence in units of clock periods (length)dump(): print out the entire sequenceconvert_to_BitLoad(): return a sequence where all regular (data) elements are converted to BITLOAD updatesmerge(): return a sequence where neighbouring data elements with the same value are merged together
Two sequences can be compared using function compare() and using operator==.
sequence.hh also provides parse_sequence_from_stream(std::istream&) for the text-based sequence format used by pptest test 42 and by the SCPI SEQ command. That parser now exposes the same regular update modes implemented by the Value subclasses, non-final triggers, preprocessor operations (store, r, rt, pr), explicit final terminators, and the f force-trigger flag. The accepted token grammar is documented inline next to the parser and mirrored in docs/docs/pptest.md.
The same header also provides write_sequence_to_stream(...) and write_sequence_to_file(...) for emitting that text format from an in-memory Sequence. These helpers are intended for round-tripping sequences through files or for generating sequence files programmatically instead of hand-writing token streams.
SPI sequence generation
SPI.hh provides a host-side SPI sequence generator that emits PulsePins Sequence objects directly, without going through the old standalone tools/spi_payload utility.
Generic SPI support lives in the spi namespace:
spi::Config: decoder clock, requested SPI clock, SPI mode, bit order, pin mapping, chip-select polarity, and select/deselect timingspi::SequenceBuilder: stateful encoder that emits RLESequenceelements for SPI idle, select, clock, MOSI, and auxiliary-pin changes
The builder quantizes the requested SPI clock to the nearest half-period measured in decoder-clock ticks. Use half_period_ticks() and achieved_spi_clock_hz() to inspect the resulting timing.
Typical usage:
spi::Config cfg;
cfg.decoder_clock_hz = 100e6;
cfg.spi_clock_hz = 12e6;
spi::SequenceBuilder spi_builder(cfg);
spi_builder.write_transaction({0x9a, 0xbc});
const Sequence &seq = spi_builder.sequence();
write_sequence_to_file(seq, "spi_sequence.txt");
Device-specific helpers stay separate from the generic SPI layer. PMODDA3.hh provides PMOD DA3 support in the pmod_da3 namespace:
pmod_da3::default_spi_config(): PMOD pin mapping and timing defaults matching the existing PMOD DA3 examplepmod_da3::code_from_voltage(): convert output voltage to the 16-bit DAC codepmod_da3::transaction_for_code()andpmod_da3::transaction_for_voltage(): build the SPI transaction as aspi::SequenceBuilder
Example:
auto dac = pmod_da3::transaction_for_voltage(1.25);
std::cout << dac.achieved_spi_clock_hz() << std::endl;
write_sequence_to_stream(dac.sequence(), std::cout, true);
Streamer control interface
The C++ streamer wrappers are thin, typed views of the hardware control/status registers rather than a separate software simulation of the datapath.
This is important for maintainability: the intent is that the code stays close to the actual FPGA programming model.
Class streamer_control (defined in streamer_control.hh and re-exported by streamer.hh) is the high-level interface for controlling the RLE-decoder core. Member functions:
status(): read the status register (buffer error, done, triggered, armed); these are outputs from the coreget_control(): returns the control register (stop, internal trigger force, internal trigger enable, reset, internal trigger reset); these are inputs to the coreget_qout(): current value at the output (on the "device pins", if there is no postprocessing)get_qout_streamer(): current value at the streamer output (which may be overridden, see below)qout_select(): determine what data is presented at the output (streamer output or override value)qout_set(): override the output with the chosen valuemonitor_ext_trig(): debugging tool for external trigger signalsset_initial_value(): set the value that is present at the streamer data output before triggeringbuffer_error(): buffer error detecteddone(): sequence decoding completed successfullystatus_report(): dump the status in textual representationreset_streamer(): reset the streamer and erase data in all FIFO buffersreset(): reset the streamer core (set control register to zero and callsreset_streamer)trigger_enable(): enable the trigger circuit; the trigger signals are ignored until this member function is called (or using an external trigger enable signal)trigger_force(): assert the internal trigger force signaltrigger_reset(): assert the internal trigger reset signalgating(): control gating (control of the streaming using an external output-enable signal)
Class streamer_fifo (defined in streamer_fifo.hh and re-exported by streamer.hh) is the high-level interface for spooling the sequence to the RLE-decoder core. Member functions:
out(): send one elementsend_sequence(): send entire sequencecheck_fill_status(): check FIFO status
Class streamer_dma (defined in streamer_dma.hh and re-exported by streamer.hh) is the high-level interface for transmitting the sequence to the
RLE-decoder code using direct memory access (DMA). Member functions:
write_element(): write an element at given memory locationprepare(): write an entire sequence in the memory bufferverify(): verify the correctness of the sequence stored in memorytransfer(): perform the transfersend_sequence(): high-level function for transmitting a sequence to the streamer via DMA
System-level interfaces
FPGA (defined in fpga.hh) is the top-level ARM-side container for memory-mapped FPGA access.
It owns the main memory maps, clock-selection helpers, trigger monitor helpers, PLL control helpers, and the output-enable GPIO path.
The constructor enforces single-owner semantics for this top-level hardware view. Shared startup actions such as clock selection, PLL setup, and the bring-up LED blink are applied by startup.hh, not by the constructor itself.
Clocking-related responsibilities are split deliberately:
FPGA::set_clk()performs top-level streamer-clock source switching with the required reset hold/release sequencepll_core_clkandpll_int_clkinpll_clk.hhprogram the two reconfigurable PLLsFPGA::set_streamer_clk()stores the measured active streaming frequency for later timing-aware host operations
Streamer classes (defined in basic_multi_dma.hh):
basic_streamer: one streamer instance with its control interface and FIFO transport; the minimal building block for higher-level streamer helpersstreamer: single-core helper using the FIFO transport and the Avalon-ST mux default path; it also applies the usual bring-up sequence (initial value, output enable, reset)dma_streamer: single-core helper that reuses the same streamer bring-up but switches the transport to DMA via the ST muxmultistreamer: container for four independentbasic_streamerinstances with coordinated bring-up of the four streamer cores
The host-side transport split is:
streamer_control.hh- register-level lifecycle control, status, gating, CRC, and FIFO statisticsstreamer_fifo.hh- direct CPU-driven FIFO transport for short/simple sequence deliverystreamer_dma.hh- SDRAM + MSGDMA transport for longer or repeated transfers
combiner (defined in combiner.hh) is the interface for advanced multiplexers.
combiner_qout, qout (defined in qout.hh)
readback (defined in readback.hh)
st_mux (defined in st_mux.hh)
trigger (defined in trigger.hh)
counter (defined in counter.hh) exposes the integrated statistics/measurement subsystem.
timestamp (defined in timestamp.hh) exposes the dual-path timestamp FIFOs and source-selection controls.
freq_meter, pp_freq_meter (defined in freq_meter.hh) expose the on-board frequency meter and its higher-level PulsePins wrapper.
Customization points
Common extension paths are:
- add a new
pp...command inpptool.ccand declare it inpptool_commands.hh - extend the sequence model in
elements.hh/sequence.hh - add a new typed wrapper for a hardware block and expose it through a tool or higher-level API
- adjust shared streaming policy in
ppworkflow.hh - update startup defaults in
startup.hh - change clock/PLL option semantics in
options.hhandpll_rules.hh
The preferred approach is to preserve the top-level command names and high-level wrapper types while evolving the implementation behind them.
For subsystem-level details, see also:
counter.mdcombiner.mdtimestamp.mdfreq_meter.mdst_mux.mdbuild.md