Skip to content

dreamsourcelab-dslogic: stream+RLE long captures + chunk_loop (follow-up to #293)#294

Open
huehuehuehueing wants to merge 13 commits into
sigrokproject:masterfrom
huehuehuehueing:dslogic-plus-0034-pr-c
Open

dreamsourcelab-dslogic: stream+RLE long captures + chunk_loop (follow-up to #293)#294
huehuehuehueing wants to merge 13 commits into
sigrokproject:masterfrom
huehuehuehueing:dslogic-plus-0034-pr-c

Conversation

@huehuehuehueing

Copy link
Copy Markdown

Summary

Extends V2 captures so a single --time budget can span many trigger events instead of just the first. Four commits, ordered foundation -> feature:

  1. V2 stream+RLE bandwidth bypass — let stream+RLE skip the per-channel-mode max_samplerate cap. Each mode's cap reflects raw bandwidth; with RLE the FPGA emits compressed pairs over USB, so sparse traffic (e.g. intermittent SPI bursts) fits 50 MB/s easily even at rates the raw mode would refuse. Lets 100 MHz × 4 ch stream continuously when the bus is sparse.
  2. wall-clock cutoff for stream+RLE — in stream+RLE the host-side sent_samples counter (computed from raw transfer bytes interpreted as uncompressed samples) tracks at the compression-ratio-dependent rate, decoupled from wall-clock. The --time 50s -> 5e9-sample budget therefore never trips; sigrok-cli sits waiting forever after the FPGA finishes its real 50 s of capture. Fix: arm a wall-clock deadline of limit_samples / samplerate * 1.1 on the first non-empty transfer and abort on cross.
  3. emit DF_HEADER once per sessionstart_transfers re-emitted DF_HEADER every time it ran. Harmless in single-shot mode, but the chunk_loop introduced next re-enters start_transfers per chunk; re-emitted DF_HEADER makes sigrok-cli re-init its output module, which makes srzip g_unlink the .sr file on the next DF_LOGIC and start a fresh archive — losing all prior chunks' data. Move DF_HEADER to dslogic_acquisition_start (called exactly once per session).
  4. chunk_loop for back-to-back captures — new SR_CONF_CHUNK_LOOP boolean (registered in the libsigrok config table). When on, the FPGA cnt register is set per-chunk (default samplerate/2, ~500 ms) instead of session-wide; on each chunk's wall-clock deadline the driver immediately re-arms (re-runs acquisition_stop + fpga_config + acquisition_start + new trigger-position transfer) without ending the libsigrok session. Re-arm is deferred to the libsigrok event-loop tick via rearm_pending, since synchronous control transfers from inside a libusb callback return LIBUSB_ERROR_BUSY. Chunk boundaries marked with SR_DF_TRIGGER. Total session bounded by total_deadline_us from --time × samplerate × 1.1.

The FPGA's trigger is one-shot per arm: after a trigger fires it captures up to cnt samples and halts. For bursty buses (SPI, I2C, anything sparse), this means only the first burst gets captured. With chunk_loop, each chunk's post-trigger window is small (~500 ms) and the driver re-arms within ~15-30 ms, so the next bus burst is caught.

Stacked on #293 (and transitively #292, #291). Diff narrows to 4 commits once the bases land.

Public API addition

This PR adds SR_CONF_CHUNK_LOOP to the libsigrok config-key enum (and registers it in hwdriver.c as SR_T_BOOL, key string "chunk_loop"). It's a generic "the driver re-arms the device per chunk under the user's --time budget" knob, useful for any device whose trigger is one-shot per arm. For now only the dreamsourcelab-dslogic V2 path implements it; other drivers can opt in by setting devc->chunk_loop from SR_CONF_CHUNK_LOOP in their config_set.

Verification

End-to-end on a sparse SPI bus (SX1276 polling cadence ~once per few seconds):

run duration archive chunks raw data on-disk SPI annotations
single-shot, no chunk_loop 100 s 24 ~96 MB 18 MB ~20
stream+RLE + chunk_loop 100 s 1126 ~4.7 GB 4.8 MB ~18 500

~925× more bus events captured per wall-clock second, same .sr output workflow. Same comparison at 10 s yielded 28 vs 20 annotations decoded directly (no file write).

Test plan

  • make builds clean
  • Per-mode samplerate clamp warning is suppressed when stream + RLE is enabled; sigrok-cli -d dreamsourcelab-dslogic -C 0,1,2,3 -c samplerate=100000000 -c continuous=on -c rle=on -t '3=f' --time 20s -O bits runs at 100 MHz across DSL_STREAM50x6 (4 channels, RLE compresses)
  • Stream captures exit cleanly at the requested --time rather than hanging on stale sample-budget
  • .sr files from sessions in single-shot mode (no chunk_loop) decode identically to before this PR
  • sigrok-cli ... -c chunk_loop=on -t '3=f' --time 100s -o long.sr produces a multi-chunk archive that decodes with many trigger events
  • SR_CONF_CHUNK_LOOP reads back what was written (-c chunk_loop=on then config-get returns true)

huehuehuehueing and others added 13 commits May 30, 2026 18:52
…0034)

Adds support for the DSLogic Plus hardware revision that enumerates
as USB ID 2a0e:0034 ("USB-based DSL Instrument v2") and ships with a
Pango FPGA bitstream. This revision speaks a newer envelope-style
control protocol than the existing DSLogic family; the driver gains a
parallel V2 path while leaving the V1 (flat-opcode) path untouched.

Architecture
------------

- struct dslogic_protocol_ops vtable in protocol.h with one slot per
  operation whose wire format differs between protocol versions
  (fpga_firmware_upload, fpga_config, acquisition_start/stop,
  set_samplerate/voltage_threshold/trigger/external_clock/clock_edge,
  security_check).
- enum dslogic_protocol_version per profile (DSL_PROTO_V1 or
  DSL_PROTO_V2). All existing profiles tagged V1; PID 0x0034 tagged V2.
- dev_context gains a cached ops pointer resolved at dev_open.
- protocol_v1.c (new): thin facades over the existing V1 functions,
  bound into dslogic_v1_ops. Pure refactor with no wire changes.
- protocol_v2.{c,h} (new): the envelope-protocol implementation.

Wire format (V2)
----------------

- Three control opcodes only: CMD_CTL_WR=0xb0, CMD_CTL_RD_PRE=0xb1,
  CMD_CTL_RD=0xb2. The packed struct ctl_header (dest, offset, size)
  carries the real destination via a DSL_CTL_* enum
  (HW_STATUS, INTRDY, WORDWIDE, START, STOP, BULK_WR, NVM, I2C_REG,
  I2C_STATUS, PROG_B, LED, FW_VERSION).
- command_ctl_rd_v2 is two-phase: PRE (OUT) writes the header, 10 ms
  sleep, then RD (IN) collects the requested bytes.
- Register R/W (dsl_wr_reg_v2 / dsl_rd_reg_v2) wraps the envelope with
  I2C_REG (write) / I2C_STATUS (read) destination and a 1-byte address
  in offset. NVM reads use DSL_CTL_NVM.

Bitstream upload (V2)
---------------------

Mirrors DSView's dsl_fpga_config sequence:
PROG_B low -> LED off -> PROG_B high -> wait FPGA_INIT_B -> INTRDY
low -> DSL_CTL_BULK_WR with 3-byte filesize -> bulk transfer bitstream
on ep2 OUT -> INTRDY high -> wait GPIF_DONE -> INTRDY low -> wait
FPGA_DONE -> LED green -> WORDWIDE high.

Security challenge (V2)
-----------------------

The 0x0034 firmware gates capture on an 8-step challenge-response over
an I2C register block (SEC_CTRL_ADDR=0x73, SEC_DATA_ADDR=0x75). The
encryption blob is read from device NVM at SECU_EEP_ADDR=0x3C00 via
DSL_CTL_NVM. Implemented as v2_security_check; called from dev_open
after the FPGA bitstream is loaded. Mirrors DSView's dsl_secuCheck.

FPGA arm (V2)
-------------

WORDWIDE -> DSL_CTL_BULK_WR (3-byte word count = sizeof(setting)/2 =
186) -> poll bmSYS_CLR -> bulk transfer struct DSL_setting (372 bytes)
on ep2 OUT -> DSL_CTL_INTRDY -> read HW_STATUS once and check
bmGPIF_DONE. The DSL_setting layout (with its (register_index << 8) |
word_count header encoding) and the samplerate divider math
(hw_max=500 MHz, pre_div=5 for DSLogic Plus pgl12) mirror DSView's
dsl_fpga_arm. Sample count is shifted right by 4 because the FPGA's
minimum capture unit is 16 samples.

V2 acquisition stop is two-stage like DSView's: write CTR0_ADDR :=
bmFORCE_RDY first (soft FPGA abort that releases the GPIF capture
engine and resets the green LED to solid), then DSL_CTL_STOP.

dev_open changes
----------------

- Extends has_firmware product-string probe to recognise "USB-based
  DSL Instrument v2" so the V2 device skips the legacy FX2 firmware
  upload (which would renumerate it to 0x0020 with the V1 firmware).
- V2 path performs DSL_CTL_FW_VERSION + DSL_CTL_HW_STATUS reads at the
  start of dev_open (matches DSView's hw_dev_open + dsl_dev_open
  initialisation). The pre-existing V1 firmware version probe at
  bRequest 0xb0 collides with V2's CMD_CTL_WR opcode and is now
  V1-gated.
- After bitstream upload + security check, an initial voltage threshold
  is written to VTH_ADDR via the new vtable slot.

Tested on real PID 0x2a0e:0x0034 hardware: scan, FPGA bitstream upload,
security challenge, FPGA arm, sample capture (100 samples / 16 channels
at 1 MHz returns 1959 bytes), and clean stop with idle-LED state.

Co-authored-by: Larry Hernandez <l.gr@dartmouth.edu>
After a close/reopen cycle (e.g. pulseview Stop then Run again) the
kernel-side endpoint state can take a few ms to settle. The next
dev_open's libusb_claim_interface then returns LIBUSB_ERROR_BUSY even
though no other process holds the interface.

Retry up to 10 times at 50 ms intervals (500 ms worst case) before
giving up with the original error message.

Independently testable: cycle PulseView's Stop and Run buttons
repeatedly on the same device without replugging; the BUSY error
should no longer surface.
…al open

Two related fixes for the PulseView Stop->Run cycle:

1. v2_fpga_firmware_upload now reads HW_STATUS first and skips the
   PROG_B/bitstream sequence if bmFPGA_DONE is already set. In that
   case it just writes CTR0_ADDR=0 ("dessert clear"), mirroring
   DSView's behaviour in dsl_dev_open's already-configured branch
   (dsl.c). Re-running the full PROG_B cycle on a live FPGA
   wedges the post-INTRDY FPGA_DONE poll because the previous capture
   engine has not been torn down on the host side. Symptom was:

     sr: dreamsourcelab-dslogic: Timeout waiting for HW_STATUS bit 0x40

2. dev_open now releases + closes the USB handle on any post-claim
   failure (fpga_firmware_upload or security_check returning non-OK).
   libsigrok does not call dev_close on a failed dev_open, so without
   this unwind the kernel still saw the interface as claimed by us;
   the next dev_open's libusb_claim_interface then returned
   LIBUSB_ERROR_BUSY ("Another program or driver has already claimed
   it") even with the retry loop added in 2ee3757. The retry loop
   stays as a belt-and-braces for genuinely transient cases.

Together these turn the "Stop, then Run again" PulseView cycle into a
reliable no-op (the device is already configured, we skip the upload,
arm runs against the warm FPGA).
DSLogic Plus exposes seven distinct channel-count/samplerate presets in
DSView (DSL_BUFFER100x16 / DSL_BUFFER200x8 / DSL_BUFFER400x4 plus four
stream modes from 20 to 100 MHz). Each preset has its own FPGA base
clock and pre-divider, so the samplerate divider math has to key off
the active preset. This commit introduces the table and uses the
profile's default preset (DSL_BUFFER100x16: 16 channels buffered, max
100 MHz, hw_max=100 MHz, pre_div=1).

Foundation for a follow-up commit that exposes SR_CONF_CHANNEL_MODE
so users can pick a different preset (more channels at lower rates,
or fewer channels at higher rates / streaming).

This also fixes a latent bug: the previous implementation hardcoded
hw_max=500 MHz and pre_div=5 (values that belong to the _3DN2 mode
variants the DSLogic Plus does not enable), producing a div_h value
that almost certainly resulted in a sample rate different from the
one the user requested. After this change, the divider matches
DSView's computation for the active channel mode.

V1 hardware is untouched: the ch_mode_id field on dev_context is V2
only, and the V1 path does not look at it.

Adds the relevant DS_MODE_* bit positions to protocol_v2.h and sets
DS_MODE_STREAM_MODE_BIT in setting.mode for stream channel modes.
ch_en mask is derived from the active mode's num_channels (16-bit only;
the DSLogic Plus has no channels above 15).
Wire dslogic_plus_auto_pick_mode_id into the V2 set_samplerate path
and re-pick at arm time. The auto-pick chooses the smallest-channel
mode whose max_samplerate covers the requested rate (minimising USB
bandwidth so high samplerates can stream cleanly).

sigrok-cli orders -c flags independently of -C, so the enabled-
channel mask may still be stale at config_set time; the arm-time
re-pick in v2_build_default_setting sees the FINAL mask just before
the FPGA arm.

When no mode covers (requested samplerate x enabled channels), clamp
cur_samplerate down to the picked mode's max_samplerate and warn so
the user knows to drop a channel or switch off continuous mode.
Avoids a silent "Device only sent N samples" USB-bandwidth abort
mid-acquisition.
…wer mode

V2 captures at 50/100/200 MHz buffered with all 16 channels enabled
were sending ~100-200 MB/s on the USB IN endpoint - twice USB 2.0 HS's
~50 MB/s ceiling. The FPGA-side buffer filled with pre-arm idle data
and real bursts never made it across.

Two interlocking changes:

1. v2_build_default_setting derives setting.ch_en_l from the sigrok
   enabled_channel_mask intersected with the active mode's capability
   cap, instead of unconditionally enabling channels 0..num_channels-1.
   With sigrok-cli's -C 0,1,2,3 (or PulseView's channel checkboxes),
   the FPGA now only captures those channels, dropping the IN-rate.

2. dslogic_plus_auto_pick_mode_id now takes need_channels =
   max_enabled_index+1 and picks the smallest num_channels mode that
   satisfies BOTH the requested samplerate AND need_channels. At
   50 MHz with -C 0,1,2,3 this picks DSL_BUFFER400x4 (4-channel mode,
   max 400 MHz) which streams at 25 MB/s instead of 100 MB/s.

Promotes enabled_channel_count and enabled_channel_mask from file-
static to SR_PRIV so V2 code can use them.

Independently testable:
  sigrok-cli -d dreamsourcelab-dslogic -C 0,1,2,3 \
      -c samplerate=50000000 --samples 100000000 -O bits
captures real SPI activity (verified against DSView CSV reference of
the same DUT: SCK on ch0, MOSI on ch1, MISO on ch2, CS on ch3 - all
four lines show the expected DUT-reset burst pattern).
Wire three V2-only configuration knobs through the libsigrok config
API and into the DSL_setting blob's mode bitfield at arm time:

- SR_CONF_RLE        -> mode bit 3 (RLE_MODE)
- SR_CONF_FILTER     -> mode bit 8 (FILTER)
- SR_CONF_EXTERNAL_CLOCK -> mode bit 1 (CLK_TYPE)

These give applications access to the DSLogic Plus FPGA's run-length
encoding (compresses idle samples to fit USB 2.0 HS bandwidth at
high samplerates), the 1T glitch filter (suppresses single-sample
spikes in noisy environments), and external clocking.

SR_CONF_FILTER is wired as SR_T_BOOL (matches its registration in
libsigrok's config table), not a string enum; passing a string would
make PulseView's libsigrokcxx bindings throw std::bad_cast when the
runtime type didn't match the declared boolean type.
The FPGA's trigger-position header reports remain_cnt in two halves
(remain_cnt_l/h, 64-bit total). In BUFFERED mode with RLE this is
the count of samples the FPGA was short of limit_samples (the
compressed buffer ran out of room before reaching the requested
capture length); without honoring it, the acquisition-stop check
never trips, sent_samples never reaches limit_samples, and the
capture hangs until the empty-transfer timeout aborts.

Stash the FPGA-actual count in dev_context.actual_samples and use
it (when non-zero) as the acquisition-stop budget. Falls back to
limit_samples for non-RLE captures.

Gate the shortening to !continuous_mode. In streaming mode
remain_cnt is an in-flight "samples remaining to send" counter
that updates continuously as the FPGA streams; subtracting it
from limit_samples gives a meaningless small number that would
end streaming captures almost immediately on the first trigger
header. In streaming mode let limit_samples remain the budget.
Wire libsigrok's sr_session_trigger to the V2 DSL_setting trig_*
fields and enable the FPGA's trigger comparator. Before this patch
the V2 driver had a no-op v2_set_trigger stub and hardcoded
"always-true" trig defaults, so -t flags were silently dropped and
captures started immediately on arm.

Encoding mirrors DSView's SIMPLE_TRIGGER path:

- v2_encode_trigger walks the session trigger stages and packs
  per-channel ZERO/ONE/RISING/FALLING/EDGE matches into
  trig_mask0/value0/edge0[0] (mirrored to the *1 halves), with
  unused stages left as mask=0xffff/logic=2 ("always true").

- trig_glb gets the enabled-channel count in its upper 5 bits and
  (num_stages - 1) in the low byte.

- trig_pos is computed from capture_ratio (percentage of
  limit_samples that should sit before the trigger), clamped to
  10% in streaming mode and 90% in buffered mode and aligned to
  the FPGA's 64-sample atomic unit.

- mode bit 0 (TRIG_EN) is set when at least one stage has matches.
  Without this bit the FPGA ignores the trig_* fields entirely;
  this was the missing piece that made the first iteration look
  like the encoding was wrong.

Verified end-to-end: 100 MHz buffered RLE on channels 0..3 with
-t '3=f' (CS-falling) captures the SX1276's actual SPI traffic,
sigrok_pd:chip=sx1276 decodes 21 register transactions including
Burst R/W RegOpmode and FIFO reads in a 0.1s window.
When stream and RLE are both enabled, treat each channel mode's
max_samplerate as the RAW-bandwidth cap and let the requested rate
through anyway. The FPGA emits RLE-compressed pairs over USB so
sparse traffic (e.g. intermittent SPI bursts) fits the ~50 MB/s
USB 2.0 HS ceiling even when raw samplerate x channel-count would
not - the actual USB throughput is bounded by signal density, not
the nominal samplerate.

Changes:
- dslogic_plus_auto_pick_mode_id takes a new rle parameter. When
  rle && continuous, skip the per-mode samplerate-fits check; pick
  the smallest-channel mode that holds need_channels and let the
  divider drive the requested rate.
- v2_build_default_setting reclassifies the samplerate-over-max
  case: in stream+RLE it's an sr_info note ("USB throughput is
  bounded by signal density"); otherwise still the loud sr_warn
  with the clamp. Adds RLE to the warning's recovery hints.
- Demoted the arm-time mode-pick announcement from sr_info to
  sr_dbg now that it's no longer needed for triage.

Verified end-to-end: -C 0,1,2,3 -c samplerate=100000000
-c continuous=on -c rle=on -t '3=f' --time 20s now picks
DSL_STREAM50x6 (smallest 4+ channel stream mode), runs at full
100 MHz, captures 1994 USB transfers across 20s with the trigger
gating, and decodes SX1276 register transactions via sigrok_pd.
In stream+RLE the FPGA emits RLE-compressed pairs over USB, so the
host-side sent_samples counter (computed from raw transfer bytes
interpreted as uncompressed samples) grows at the compression-
ratio-dependent rate, decoupled from wall-clock. The sample-budget
stop check therefore never trips at the user-requested --time:
sigrok-cli translates --time 50s into a 5e9 sample budget at
100 MHz, but RLE compression of sparse SPI bursts means only ~5e8
worth of raw-byte-equivalent samples arrive across the full 50s of
real-world FPGA capture - the budget would require ~500s of wall-
clock at this density.

The FPGA itself stops when its cnt register reaches zero (50s of
wall-clock capture), so the green LED stops flashing on time, but
the host then sits waiting for sample-budget that will never come
until the empty-transfer timeout (6.4s) trips.

Fix: arm a wall-clock deadline of limit_samples/samplerate * 1.1
(10% drain grace) on the first non-empty transfer, and abort the
moment we cross it. Only active when both continuous_mode and
rle_mode are set; sample-budget gating remains correct for raw
streaming and for buffered captures.

Also adds detail to the empty-transfer abort log so future stalls
can be diagnosed without rebuilding.

Verified: --time 50s now exits in ~55s with clean "wall-clock
deadline reached" log, writes the full .sr file, no empty-transfer
abort.
start_transfers used to send std_session_send_df_header at the
bottom of its setup. For single-shot captures that's harmless
(start_transfers runs once), but with chunk_loop start_transfers
is re-entered on every re-arm via trigger_receive, so each chunk
re-emitted DF_HEADER.

Downstream effect: sigrok-cli's session loop responds to
DF_HEADER by calling setup_output_format, which constructs a
fresh srzip output context with zip_created=FALSE. The next
DF_LOGIC for that chunk then triggers srzip's zip_create, which
unlinks the .sr file and starts a brand-new archive. Net result:
only the last chunk's data ever survived in the .sr file
(or roughly one chunk's worth, ~16% of total samples captured).

Move the DF_HEADER emission to dslogic_acquisition_start, called
exactly once per session, so all chunks share a single output
module context and srzip appends across the whole session.

Verified: 10 s chunk_loop capture with -o file.sr now produces
a 93-chunk archive (~390 MB raw data, 398 KB on disk after RLE
+ zip compression) with 1548 SPI annotations across the file,
vs 1 chunk (~100 KB on disk, ~20 annotations) before the fix.
The FPGA's trigger model is one-shot: after a trigger fires it
captures up to cnt samples and then halts. For triggered captures
on a bursty bus (e.g. SX1276 SPI traffic), this means only the
first burst gets captured during the whole session - subsequent
bus events fire triggers the FPGA isn't armed to see.

Add a chunk_loop mode that arms the FPGA with a small per-chunk
sample budget (default samplerate/2 = 500ms), and on each chunk's
completion immediately re-arms for the next trigger. The session
stays alive across chunks; chunk boundaries are marked with
SR_DF_TRIGGER. Total wall-clock is bounded by the user's --time
(or LIMIT_SAMPLES) via a new total_deadline_us tracked from acq
start; once the deadline is crossed during a drain, the next
free_transfer path skips re-arm and ends the session normally.

Protocol-agnostic: the re-arm uses sr_session_trigger_get's
existing trigger condition (whatever the user passed via -t).
Works with any trigger spec, not just edge-on-a-channel.

Mechanics:

- SR_CONF_CHUNK_LOOP (new boolean config, registered in
  hwdriver.c, off by default; -c chunk_loop=on enables).
- dev_context gains chunk_loop, chunk_samples, total_deadline_us,
  rearm_pending.
- v2_build_default_setting uses chunk_samples instead of
  limit_samples for the FPGA's cnt register when chunk_loop is on,
  so the FPGA stops at each chunk boundary.
- receive_transfer sets rearm_pending when the per-chunk budget
  or wallclock_deadline_us trips, then calls abort_acquisition;
  the prior chunk's transfers drain through free_transfer.
- receive_data (libusb event tick) sees rearm_pending +
  submitted_transfers == 0 and calls rearm_chunk_in_place on a
  clean stack. Doing the re-arm from inside the libusb callback
  collides with libusb's internal transfer queue and returns
  LIBUSB_ERROR_BUSY, so we defer.
- rearm_chunk_in_place sends an SR_DF_TRIGGER chunk-boundary
  marker, resets per-chunk state, runs ops->acquisition_stop +
  fpga_config + acquisition_start, and submits a fresh
  trigger-position transfer. Typical re-arm latency: 15-30 ms.

Verified: 10 s capture with -c chunk_loop=on -t '3=f' on a sparse
SX1276 SPI bus catches ~28 transactions decoded by the plain spi
decoder, vs 20 transactions in a 100 s single-shot run on the
same bus - chunk_loop catches roughly 5x more events per unit
wall-clock by re-arming the trigger immediately after each
chunk's post-trigger window.

Known limitation: srzip output (-o file.sr) drops data across
chunk boundaries; only the first chunk's worth of samples lands
in the .sr file. Direct decode (-P ... -A ...) or text output
(-O bits) handle chunked sessions correctly. Fixing srzip is
out of scope for this commit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant