ppscpi
ppscpi is a standalone network server for remote control of a PulsePins device using
the Standard Commands for Programmable Instruments
(SCPI) protocol. It provides a minimal SCPI
interface for simple sequence streaming.
The IVI Foundation maintains the SCPI standard and hosts the SCPI-99 specification; for a quick non-normative overview, see the SCPI overview.
ppscpi is also convenient for controlling PulsePins from remote computers through Python/Jupyter notebooks (or any programming language/environment that can talk to socket interfaces).
The implementation is in c++/ppscpi.cc and the SCPI session/server helpers are in c++/scpi_server.hh.
Transport and startup
ppscpi listens on standard SCPI TCP port 5025.
On startup it:
- configures realtime scheduling and locks memory pages
- creates the shared
HostRuntimeand top-levelFPGAwrapper - applies the shared clock/PLL startup policy; the FPGA reset-manager pulse runs only if
-reset_FPGAorPP_RESET_FPGAis requested - reports the measured clocks using the frequency-meter block
- accepts SCPI-style commands over the network
The transport is line-oriented. A single line may contain multiple commands separated by
semicolons (;); ppscpi trims and dispatches each segment in order, with responses emitted
in the same order. Empty segments are ignored. Each segment is parsed as a complete command
path, so relative SCPI path continuation is not implemented.
Session model
Each client connection gets its own SCPI session object.
Session state includes:
- one
streamer,readback, andcounterwrapper set bound to the sharedFPGA - the loaded
Sequence - whether readback checking is enabled
- whether streaming should use forced triggering
The session does not persist across reconnects.
Supported commands
Standard commands:
*IDN?- identify the instrument*RST- clear loaded sequence/session state and reset the hardware run path (streamer, readback, counters, mux, output combiner, and trigger combiner)*CLS- clear status and error queue*OPC/*OPC?- operation complete flag/query*WAI- no-op;STREAMitself is synchronous*ESR?- standard event status register*STB?- status byteSYST:ERR?- query and drain the error queue
PulsePins-specific commands:
TEST1- run a built-in short self-test sequenceSEQ <data>- parse and load a sequence from textual representation, includingf,final, and control-flow records supported byparse_sequence_from_stream(...)CHECK <bool>- enable or disable readback checking duringSTREAMCHECK?- query the check settingCLOCK:STREAMER?- query the measured streamer clock frequency in HzSTREAM- send the loaded sequence and trigger executionDISCONNECT- close the client session; theppscpiserver keeps runningTERMINATE- stop theppscpiserver process
Typical flow
- Connect to TCP port
5025 - Send
*RST - Send
SEQ ...with the sequence payload - Optionally send
CHECK ON - Send
STREAM - Query
SYST:ERR?if needed
Host-side Python and Jupyter
Jupyter should normally run on the host computer, not on the DE10-Nano. The board only needs to run ppscpi; the notebook talks to it over Ethernet.
The lightweight host-side Python client lives in python/pulsepins/ and uses only the Python standard library. From a checkout, make that directory importable first:
export PYTHONPATH=/path/to/PulsePins/python
For notebooks, an editable host-side install is often more convenient:
python3 -m pip install -e /path/to/PulsePins/python
That install also provides example commands such as pulsepins-ppscpi-check, pulsepins-ppscpi-hello, pulsepins-notebook-workflow, pulsepins-timeline-preview, pulsepins-timeline-stream, and pulsepins-timeline-sweep. Add --self-test to pulsepins-ppscpi-check to run the built-in TEST1 hardware smoke path after connecting.
Then a notebook or script can drive the board with:
from pulsepins import PulsePins
with PulsePins("de10nano") as pp:
print(pp.idn())
print(pp.streamer_clock_hz())
pp.reset()
pp.load_sequence("""
d 10 0xff
d 5 0x00
d 2 0b0101
f
""")
pp.check(False)
pp.stream()
load_sequence(...) accepts normal multiline PulsePins text sequence input and flattens it into the single-line SEQ ... command that ppscpi expects. Because the SCPI transport is line-oriented, one uploaded sequence command must fit within the server's 64 KiB line limit and must not contain semicolons.
If SEQ or STREAM returns an error response, the Python client drains SYST:ERR? and raises PulsePinsCommandError with the queued server-side diagnostic text.
For notebook-oriented pulse construction, the same package provides Timeline:
from pulsepins import PulsePins
with PulsePins("de10nano") as pp:
timeline = pp.timeline(unit="us")
timeline.channel("laser", bit=0)
timeline.channel("camera", bit=1)
timeline.pulse("laser", start=10, duration=5)
timeline.pulse("camera", start=20, duration=10)
pp.reset()
pp.run(timeline, force_trigger=True, include_final=True)
Timeline.to_sequence(...) returns the generated text sequence, Timeline.to_csv() writes browser-compatible Timeline CSV, Timeline.to_draft_json() writes browser-compatible draft JSON, Timeline.to_vcd(...) writes a scalar waveform trace, and notebooks render a lightweight SVG preview when the timeline object is evaluated. Use include_final=True when streaming finite Timeline pulses so owned channels return to their resting value after the last pulse.
The same workflow is available as runnable examples:
PYTHONPATH=python python3 python/examples/timeline_preview.py --svg timeline.svg --csv timeline.csv --draft timeline.json --vcd timeline.vcd
PYTHONPATH=python python3 python/examples/timeline_stream.py de10nano --print-sequence
PYTHONPATH=python python3 python/examples/timeline_sweep.py de10nano --delays-us 0 5 10
With the editable install, use the installed command names instead:
pulsepins-ppscpi-check de10nano --self-test
pulsepins-notebook-workflow de10nano --output-dir previews --run
pulsepins-timeline-preview --svg timeline.svg --csv timeline.csv --draft timeline.json --vcd timeline.vcd
pulsepins-timeline-stream de10nano --print-sequence
pulsepins-timeline-sweep de10nano --delays-us 0 5 10
Notes
STREAMuses the same send/trigger path as the local tools, including optional readback verification.*RSTrestores a clean remote-control run state; it does not reprogram clocks or PLLs.SEQstores the parsed sequence in memory; nothing is transmitted to the streamer untilSTREAMis issued.- Semicolons separate SCPI commands before command parsing, so they are not valid inside a
SEQpayload. - If the loaded sequence does not end with terminal
final Vand the server was not started with-t,-random_final, orPP_RANDOM_FINAL,STREAMappends a no-modify final terminator so outputs remain at the last sequence value. Thefrecord only requests forced triggering. - Repeated
STREAMcommands reuse the stored sequence exactly as parsed; the cached session sequence is not rewritten by readback checking or final-output preparation. - Before each hardware-touching run (
TEST1andSTREAM),ppscpiresets the streamer core, readback encoder, and counters so repeated commands in the same process/session start from a clean hardware state. - If
TEST1orSTREAMreturnsFAILURE,ppscpialso pushes an execution-error record intoSYST:ERR?with the aggregated PulsePins return-code bits so remote clients can distinguish timeout, readback, CRC, buffer, and overflow failures. - When
CHECK ONis active and no explicit startup-timeoutor-hard-timeoutwas supplied, the shared workflow uses a conservative default readback timeout: 2s for the first readback element and 2s for later idle gaps. Startppscpiwith-timeout 0to disable idle-timeout protection,-timeout <value>to set the idle-gap timeout, or-hard-timeout <value>to set an absolute readback timeout from command start. - Finite
STREAMruns also inherit the internal 10 s streamer-completion timeout from the shared playback path. When that timeout fires, the user-facing message istimed out waiting for streamer completion (10 s internal limit). - Hardware-touching commands are serialized across sessions through the shared FPGA lock, so multiple clients can stay connected without racing each other on streamer/reset state.
- The server is intended for remote orchestration, not for high-throughput binary bulk transfer.
- Command-handler exceptions are converted into SCPI error/status state instead of tearing down the whole server process.
- After
DISCONNECT, clients can reconnect and start a fresh independent session. TERMINATEis the explicit server-shutdown command; it closes the client session and stops the process.