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 bindings 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,pll_calc.hh- typed option resolution, symbolic PLL presets, and calculated PLL profiles 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 CRC32 integrity checks
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 Value Change Dump (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. The C++ VCD export default is $timescale 10ns, matching the VCD import default of a 10 ns PulsePins output period.
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.
Serialization capability matrix:
| Format | C++ | Python | CLI |
|---|---|---|---|
| PulsePins text sequence format | import + export | import + export | export via ppread -save-text; import in selected workflows |
| Value Change Dump (VCD) | import + export | import + export | import via ppplay or ppvcd; 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 exposed in elements.hh:
ValueBitLoadBitSetBitClearBitFlipBitNotBitAndBitOrBitXorBitXnorBitSllBitSrlTriggerCondition
These helper types are lightweight tags and inspectors for authored elements. The el object
itself stores flattened raw control, count, and value fields.
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: el derives the effective element kind from the encoded control word. It 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(): includes extra control decorations such as thestoreslotregular_token()andsequence_record(): render the element into the PulsePins text sequence grammar
Preferred transformation helpers on el are immutable:
stored_in(int)with_control(control_t)with_count(count_t)with_counter(Counter)with_regular_value(Value)as_bitload_after(value_t)
Additional mutating helpers are available for direct in-place updates: set_control(), set_count(), set_value(), and store(). Prefer the immutable helpers above when constructing transformed elements.
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 accepts the same regular update modes implemented by the Value subclasses, non-final triggers, preprocessor operations (store, r, rt, pr), terminal explicit final terminators, and the f force-trigger flag. The accepted token grammar is documented inline next to the parser and mirrored in pptest - self-tests (source).
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.
PulsePins generates SPI transactions by turning clock, data, and chip-select transitions into ordinary output-sequence elements.
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 software-side control shadow (stop, internal trigger force, internal trigger enable, reset, internal trigger reset, qout select, stop-on-buffer-error)get_qout(): synchronized snapshot of the current value at the output (on the "device pins", if there is no postprocessing)get_qout_streamer(): synchronized snapshot of the streamer output before the optional override muxqout_select(): select whether the streamer output or override value is presented; the active streamer-clock shadow updates only while the streamer is idle or resetqout_set(): write the override value and select it; active output visibility follows the same idle/reset commit rule and readback snapshot latencymonitor_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
In practical terms, streamer_fifo is the simpler CPU-driven path for short sequences, while streamer_dma prepares a memory-backed transfer and lets the DMA engine move the sequence into the hardware path.
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 readout, and FIFO statisticsstreamer_fifo.hh- direct CPU-driven FIFO transport for short/simple sequence deliverystreamer_dma.hh- SDRAM + Intel/Altera Modular Scatter-Gather DMA 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: