dreamsourcelab-dslogic: stream+RLE long captures + chunk_loop (follow-up to #293)#294
Open
huehuehuehueing wants to merge 13 commits into
Open
dreamsourcelab-dslogic: stream+RLE long captures + chunk_loop (follow-up to #293)#294huehuehuehueing wants to merge 13 commits into
huehuehuehueing wants to merge 13 commits into
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extends V2 captures so a single
--timebudget can span many trigger events instead of just the first. Four commits, ordered foundation -> feature:max_sampleratecap. 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.sent_samplescounter (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-clisits waiting forever after the FPGA finishes its real 50 s of capture. Fix: arm a wall-clock deadline oflimit_samples / samplerate * 1.1on the first non-empty transfer and abort on cross.start_transfersre-emittedDF_HEADERevery time it ran. Harmless in single-shot mode, but the chunk_loop introduced next re-entersstart_transfersper chunk; re-emittedDF_HEADERmakessigrok-clire-init its output module, which makes srzipg_unlinkthe.srfile on the nextDF_LOGICand start a fresh archive — losing all prior chunks' data. MoveDF_HEADERtodslogic_acquisition_start(called exactly once per session).SR_CONF_CHUNK_LOOPboolean (registered in the libsigrok config table). When on, the FPGAcntregister is set per-chunk (defaultsamplerate/2, ~500 ms) instead of session-wide; on each chunk's wall-clock deadline the driver immediately re-arms (re-runsacquisition_stop+fpga_config+acquisition_start+ new trigger-position transfer) without ending the libsigrok session. Re-arm is deferred to the libsigrok event-loop tick viarearm_pending, since synchronous control transfers from inside a libusb callback returnLIBUSB_ERROR_BUSY. Chunk boundaries marked withSR_DF_TRIGGER. Total session bounded bytotal_deadline_usfrom--time× samplerate × 1.1.The FPGA's trigger is one-shot per arm: after a trigger fires it captures up to
cntsamples and halts. For bursty buses (SPI, I2C, anything sparse), this means only the first burst gets captured. Withchunk_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_LOOPto the libsigrok config-key enum (and registers it inhwdriver.casSR_T_BOOL, key string"chunk_loop"). It's a generic "the driver re-arms the device per chunk under the user's--timebudget" 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 settingdevc->chunk_loopfromSR_CONF_CHUNK_LOOPin theirconfig_set.Verification
End-to-end on a sparse SPI bus (SX1276 polling cadence ~once per few seconds):
~925× more bus events captured per wall-clock second, same
.sroutput workflow. Same comparison at 10 s yielded 28 vs 20 annotations decoded directly (no file write).Test plan
makebuilds cleanstream + RLEis 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 bitsruns at 100 MHz across DSL_STREAM50x6 (4 channels, RLE compresses)--timerather than hanging on stale sample-budget.srfiles from sessions in single-shot mode (no chunk_loop) decode identically to before this PRsigrok-cli ... -c chunk_loop=on -t '3=f' --time 100s -o long.srproduces a multi-chunk archive that decodes with many trigger eventsSR_CONF_CHUNK_LOOPreads back what was written (-c chunk_loop=onthen config-get returnstrue)