Give Evk1029Source a single output port instead of N coupled ones

With role.RF_channels set to N > 1, Evk1029Source declared N separate
GNU-Radio-scheduler-coupled sync_block output ports and copied the same
decoded samples into each of them every work() call, so that N RF bands
sharing one raw capture (e.g. several Freq_Xlating_Fir_Filter instances
at different IFs) would see identical, sample-locked data.

That design has two costs. First, a gr::sync_block's work() cannot even
be called unless every declared output port currently has space -- so
one band's downstream chain running temporarily slower than the others
stalls production on every port at once, not just its own; if anything
downstream depends on one of the OTHER (unrelated) ports to keep making
progress, that coupling can cascade into the whole receiver stalling
with nothing left able to run. Second, N-1 extra full-buffer memcpy
calls happen on every work() call, at up to the raw capture's sample
rate (hundreds of MSps) with large per-call chunks -- real, continuous
CPU and memory-bandwidth cost for pure duplication.

Change Evk1029Source to always declare exactly one real output port.
Multiple RF bands sharing the capture now each get their own connect()
to that same port instead of separate ports -- gnss_flowgraph.cc's
existing generic signal-source-to-conditioner wiring already takes this
path automatically whenever output_signature()->max_streams() == 1,
using GNU Radio's native support for several independent readers on one
producer port. Each reader then advances at its own pace: a temporarily
slow band only delays itself, and the duplicate-copy cost is gone
entirely. This also now matches this project's older EVK10x9_source,
which used the same single-port-plus-independent-readers design.

.enable_throttle_control with RF_channels > 1 is left unsupported (a
single throttle block would itself need the same N-independent-readers
treatment, not implemented here) and continues to be ignored with a
warning; the warning message is updated to describe why.
This commit is contained in:
joebre
2026-09-17 12:05:55 +02:00
parent a6b63c77dc
commit 501a0117d6
4 changed files with 40 additions and 32 deletions
@@ -68,8 +68,8 @@ Evk1029SignalSource::Evk1029SignalSource(
if (enable_throttle_control_ && rf_channels_ > 1)
{
LOG(WARNING) << role << ".enable_throttle_control is not supported with RF_channels > 1 "
<< "(a single throttle can't sit across multiple shared-source output ports); ignoring it. "
<< "Backpressure from the slower downstream chain keeps the ports in lockstep instead.";
<< "(would need its own N-independent-readers wiring, same as Evk1029Source itself; "
<< "not yet implemented); ignoring it.";
std::cout << role << ": enable_throttle_control ignored with RF_channels=" << rf_channels_ << "\n";
enable_throttle_control_ = false;
}
@@ -8,17 +8,22 @@
* (dedicated file reader + optional throttle + optional dump), updated
* for the current EVK1029 capture format (see evk1029_source.h).
*
* Set role + ".RF_channels" to N > 1 (e.g. for a dual-band setup sharing a
* single IF capture) to get N identical, sample-locked output streams from
* ONE underlying Evk1029Source instance/read, instead of instantiating N
* separate SignalSourceN blocks each reading the file independently -- the
* latter has no shared clock forcing the two chains to progress at the same
* rate, and can drift apart over a long run if their downstream processing
* costs differ. In RF_channels > 1 mode, .enable_throttle_control is not
* supported (a single throttle block can't sit across N ports); it is
* ignored with a warning. Backpressure from the slower of the two
* downstream chains naturally keeps both output ports of the shared block
* in lockstep instead.
* Set role + ".RF_channels" to N > 1 (e.g. for a multi-band setup sharing a
* single IF capture) to get N independent readers of ONE underlying
* Evk1029Source instance/read, instead of instantiating N separate
* SignalSourceN blocks each reading the file independently -- the latter
* has no shared read position, so a long, slow file open/seek on one of
* them can leave it at a different point in the capture than the others.
* Evk1029Source itself only ever declares a single real output port (see
* its own header); gnss_flowgraph.cc's generic signal-source-to-conditioner
* wiring detects this (output_signature()->max_streams() == 1) and connects
* each of the N signal conditioners to that same port independently, using
* GNU Radio's native support for multiple readers on one producer port.
* Each reader then advances at its own pace -- one RF band's downstream
* chain running behind never blocks the others.
* In RF_channels > 1 mode, .enable_throttle_control is not supported (a
* single throttle block would itself need N independent readers wired the
* same way, not yet implemented); it is ignored with a warning.
*
* -----------------------------------------------------------------------------
*
@@ -19,7 +19,6 @@
#include <gnuradio/io_signature.h>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iostream>
#if USE_GLOG_AND_GFLAGS
@@ -71,13 +70,16 @@ Evk1029Source_sptr evk1029_make_source(const std::string& filename, int n_stream
Evk1029Source::Evk1029Source(const std::string& filename, int n_streams, Concurrent_Queue<pmt::pmt_t>* queue, double sampling_frequency, uint64_t file_offset_bytes)
: gr::sync_block("evk1029_source",
gr::io_signature::make(0, 0, 0),
gr::io_signature::make(n_streams, n_streams, sizeof(int8_t))),
gr::io_signature::make(1, 1, sizeof(int8_t))),
queue_(queue),
buffer_(kReadChunkBytes),
buffer_valid_(0),
buffer_pos_(0),
n_streams_(n_streams)
{
// Exactly one real output port, always -- see this class's header for
// why. n_streams_ (the RF_channels count) is kept only for logging;
// it no longer sizes the io_signature.
// Keep the scheduler from calling work() with a tiny noutput_items; each
// unit of work produces a pair of samples (one input byte -> 2 samples).
// This block runs at the raw, pre-decimation sample rate (up to ~180 Msps),
@@ -183,13 +185,5 @@ int Evk1029Source::work(int noutput_items,
return this->WORK_DONE;
}
// All output ports carry an identical copy of the same unpacked samples
// (single read, single unpack, fanned out here) so that RF bands fed
// from different ports of this one block can never drift apart.
for (int p = 1; p < n_streams_; ++p)
{
std::memcpy(output_items[p], out, static_cast<std::size_t>(produced) * sizeof(int8_t));
}
return produced;
}
@@ -11,13 +11,23 @@
* 4-bit OBA samples per byte (16 samples per 64-bit word, per the
* capture's .sdrx metadata: quantization=4, packedbits=64, encoding=OBA).
*
* Supports emitting multiple identical output streams (one read, N copies)
* from a single work() call, so that two RF bands fed from the same raw
* capture (e.g. two Freq_Xlating_Fir_Filter instances tuned to different
* IFs) can never drift apart sample-index-wise: GNU Radio computes
* noutput_items as the minimum available space across all declared output
* ports, so backpressure from whichever downstream chain is slower stalls
* production for every output port together, not just its own.
* Exactly ONE real output port, always (n_streams_ is kept only for
* logging -- it no longer sizes the io_signature). Multiple RF bands
* sharing this same raw capture (e.g. several Freq_Xlating_Fir_Filter
* instances tuned to different IFs) each get their OWN connect() to this
* one port instead (see gnss_flowgraph.cc's signal-source-to-conditioner
* wiring, which already takes this path whenever output_signature()'s
* max_streams() == 1): GNU Radio's buffer model natively supports several
* independent readers on one producer port, each with its own read
* pointer, so a downstream chain that temporarily falls behind only
* delays *itself* -- it can't force every port's own production to stall
* together the way N separate, GNU-Radio-scheduler-coupled sync_block
* output ports would (a single slow port failing GNU Radio's "space
* available on every port" check before work() is even called stalls
* production on every port at once). An earlier version of this block
* used N separate ports and copied the same decoded output into each of
* them; besides the tighter coupling above, that meant N-1 extra full-
* buffer memcpy calls per work() call at up to the raw capture rate.
*
* -----------------------------------------------------------------------------
*
@@ -68,8 +78,7 @@ Evk1029Source_sptr evk1029_make_source(const std::string& filename, int n_stream
/*!
* \brief Reads a continuous, header-less EVK1029 raw capture file and
* unpacks its OBA-encoded 4-bit samples (two per byte, low nibble first),
* writing the identical result to every declared output port.
* unpacks its OBA-encoded 4-bit samples (two per byte, low nibble first).
*/
class Evk1029Source : public gr::sync_block
{