From a93f5cb26fc474fdf624b76cd5eb2332d74a95b6 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 15 Nov 2025 12:34:29 +0100 Subject: [PATCH 01/59] RigolOscilloscope: major driver refactor Differentiate between scope families and then group their common behavior in switch statements. This is more flexible approach and allow more fine tuned, per family behavior than immediately grouping them by protocol. - use smart pointers where it makes sense - use vector/array rather than C arrays DS1000Z - implemented memory/samplerate configuration - improved download speed - improved trigger poll performance/robustness Other families should have same handling as before. --- scopehal/RigolOscilloscope.cpp | 2068 +++++++++++++++++++++----------- scopehal/RigolOscilloscope.h | 76 +- 2 files changed, 1443 insertions(+), 701 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 045b8212..e2fc0245 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -32,6 +32,9 @@ #include "EdgeTrigger.h" #include +#include +#include +#include #ifdef _WIN32 #include @@ -52,151 +55,19 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) , m_liveMode(false) , m_highDefinition(false) { - //Last digit of the model number is the number of channels - if(1 == sscanf(m_model.c_str(), "DS%d", &m_modelNumber)) - { - if(m_model.size() >= 7 && (m_model[6] == 'D' || m_model[6] == 'E')) - m_protocol = DS_OLD; - else - m_protocol = DS; - } - else if(1 == sscanf(m_model.c_str(), "MSO%d", &m_modelNumber)) - { - m_protocol = MSO5; - // Hacky workaround since :SYST:OPT:STAT doesn't work properly on some scopes - // Only enable chan 1 - m_transport->SendCommandQueued("CHAN1:DISP 1\n"); - m_transport->SendCommandQueued("CHAN2:DISP 0\n"); - if(m_modelNumber % 10 > 2) - { - m_transport->SendCommandQueued("CHAN3:DISP 0\n"); - m_transport->SendCommandQueued("CHAN4:DISP 0\n"); - } - // Set in run mode to be able to set memory depth - m_transport->SendCommandQueued("RUN\n"); - - m_transport->SendCommandQueued("ACQ:MDEP 200M\n"); - auto reply = Trim(m_transport->SendCommandQueuedWithReply("ACQ:MDEP?\n")); - m_opt200M = reply == "2.0000E+08" ? - true : - false; // Yes, it actually returns a stringified float, manual says "scientific notation" - - // Reset memory depth - m_transport->SendCommandQueued("ACQ:MDEP 1M\n"); - string originalBandwidthLimit = m_transport->SendCommandQueuedWithReply("CHAN1:BWL?"); - - // Figure out its actual bandwidth since :SYST:OPT:STAT is practically useless - m_transport->SendCommandQueued("CHAN1:BWL 200M\n"); - reply = Trim(m_transport->SendCommandQueuedWithReply("CHAN1:BWL?\n")); - - // A bit of a tree, maybe write more beautiful code - if(reply == "200M") - m_bandwidth = 350; - else - { - m_transport->SendCommandQueued("CHAN1:BWL 100M\n"); - reply = Trim(m_transport->SendCommandQueuedWithReply("CHAN1:BWL?\n")); - if(reply == "100M") - m_bandwidth = 200; - else - { - if(m_modelNumber % 1000 - m_modelNumber % 10 == 100) - m_bandwidth = 100; - else - m_bandwidth = 70; - } - } - - m_transport->SendCommandQueued("CHAN1:BWL " + originalBandwidthLimit); - } - else if(1 == sscanf(m_model.c_str(), "DHO%d", &m_modelNumber) && (m_modelNumber < 5000)) - { // Model numbers are : - // - DHO802 (70MHz), DHO804 (70Mhz), DHO812 (100MHz),DHO814 (100MHz) - // - DHO914/DHO914S (125MHz), DHO924/DHO924S (250MHz) - // - DHO1072 (70MHz), DHO1074 (70MHz), DHO1102 (100MHz), DHO1104 (100MHz), DHO1202 (200MHz), DHO1204 (200MHz) - // - DHO4204 (200MHz), DHO4404 (400 MHz), DHO4804 (800MHz) - m_protocol = DHO; - // Those are 12 bits (HD) models => default to high definition mode - // This can be overriden by driver 8bits setting - m_highDefinition = true; - - int model_multiplicator = 100; - int model_modulo = 100; - if(m_modelNumber > 1000) - { // DHO1000 and 4000 - model_multiplicator = 10; - model_modulo = 1000; - } - else if(m_modelNumber > 900) - { // special handling of DHO900 series - model_multiplicator = 125; - } - m_bandwidth = m_modelNumber % model_modulo / 10 * model_multiplicator; - if(m_bandwidth == 0) m_bandwidth = 70; // Fallback for DHO80x models - - m_opt200M = false; // does not exist in 800/900 series - m_lowSrate = false; - - if (m_modelNumber > 4000 && m_modelNumber < 5000) { - m_maxMdepth = 250*1000*1000; - m_maxSrate = 4*1000*1000*1000U; - /* probe for bandwidth upgrades and memory upgrades on DHO4000 series */ - auto reply = Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? RLU\n")); - if (reply == "1") - m_maxMdepth = 500*1000*1000; - - reply = Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW2T4\n")); - if (reply == "1") - m_bandwidth = 400; - - reply = Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW2T8\n")); - if (reply == "1") - m_bandwidth = 800; - - reply = Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW4T8\n")); - if (reply == "1") - m_bandwidth = 800; - } - else if (m_modelNumber > 1000 && m_modelNumber < 2000) { - m_maxMdepth = 50*1000*1000; - m_maxSrate = 2*1000*1000*1000; - /* probe for bandwidth upgrades and memory upgrades on DHO1000 series */ - auto reply = Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? RLU\n")); - if (reply == "1") - m_maxMdepth = 100*1000*1000; - - reply = Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW7T10\n")); - if (reply == "1") - m_bandwidth = 100; - - reply = Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW7T20\n")); - if (reply == "1") - m_bandwidth = 200; - reply = Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW10T20\n")); - if (reply == "1") - m_bandwidth = 200; - } - else - { // DHO800/900 (DHO800 also have 50M memory since firmware v00.01.03.00.04 2024/07/11) - m_maxMdepth = 50*1000*1000; - m_maxSrate = 1.25*1000*1000*1000; - m_lowSrate = true; - } - } - else + DecodeDeviceSeries(); + if(m_series == Series::UNKNOWN) { - LogError("Bad model number\n"); + LogError("device series not recognized nor supported\n"); return; } + LogVerbose("RigolOscilloscope: series: %d\n", int(m_series)); - // Maybe fix this in a similar manner to bandwidth - int nchans = m_modelNumber % 10; - - if((m_protocol != MSO5) && (m_protocol != DHO)) - m_bandwidth = m_modelNumber % 1000 - nchans; + AnalyzeDeviceCapabilities(); + UpdateDynamicCapabilities(); - for(int i = 0; i < nchans; i++) + for(auto i = 0U; i < m_analogChannelCount; i++) { //Hardware name of the channel string chname = string("CHAN") + to_string(i + 1); @@ -228,7 +99,6 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) m_channels.push_back(chan); chan->SetDefaultDisplayName(); } - m_analogChannelCount = nchans; //Add the external trigger input m_extTrigChannel = new OscilloscopeChannel( @@ -237,20 +107,56 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) m_extTrigChannel->SetDefaultDisplayName(); //Configure acquisition modes - if(m_protocol == DS_OLD) - m_transport->SendCommandQueued(":WAV:POIN:MODE RAW"); - else - { - m_transport->SendCommandQueued(string(":WAV:FORM ") + (m_highDefinition ? "WORD" : "BYTE")); - m_transport->SendCommandQueued(":WAV:MODE RAW"); + switch (m_series) { + case Series::DS1000: + m_transport->SendCommandQueued(":WAV:POIN:MODE RAW"); + break; + + case Series::DS1000Z: + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + m_transport->SendCommandQueued(string(":WAV:FORM ") + (m_highDefinition ? "WORD" : "BYTE")); + m_transport->SendCommandQueued(":WAV:MODE RAW"); + break; + + case Series::UNKNOWN: + break; } - if(m_protocol == MSO5 || m_protocol == DS_OLD || m_protocol == DHO) - { - for(size_t i = 0; i < m_analogChannelCount; i++) - m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":VERN ON"); + + switch (m_series) { + case Series::DS1000: + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + for(size_t i = 0; i < m_analogChannelCount; i++) + m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":VERN ON"); + break; + + case Series::DS1000Z: + case Series::UNKNOWN: + break; } - if(m_protocol == MSO5 || m_protocol == DS || m_protocol == DHO) - m_transport->SendCommandQueued(":TIM:VERN ON"); + + switch (m_series) { + case Series::DS1000Z: + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + m_transport->SendCommandQueued(":TIM:VERN ON"); + break; + + case Series::DS1000: + case Series::UNKNOWN: + break; + } + FlushConfigCache(); //make sure all setup commands finish before we proceed @@ -277,6 +183,440 @@ uint32_t RigolOscilloscope::GetInstrumentTypesForChannel(size_t /*i*/) const //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //Device interface functions +void RigolOscilloscope::DecodeDeviceSeries() +{ + //Last digit of the model number is the number of channels + m_series = [&]() -> Series + { + // scope name is always, no numeric prefix, followed by numeric model number optionally followed by alfanumeric suffix + auto cursor = m_model.begin(); + { + // extract + m_modelNew.prefix.clear(); + m_modelNew.prefix.resize(10); // preallocate space + int length; + if (1 != sscanf(cursor.base(), "%4[^0-9]%n", m_modelNew.prefix.data(), &length)) + { + LogError("could not parse scope series prefix, got more than maximum expected length 3\n"); + return Series::UNKNOWN; + } + m_modelNew.prefix.resize(length); + LogVerbose("parsed model prefix %s\n", m_modelNew.prefix.c_str()); + cursor += length; + } + + { + // parse model number + int length; + if (1 != sscanf(cursor.base(), "%5u%n", &m_modelNew.number, &length)) + { + LogError("could not parse scope model number\n"); + return Series::UNKNOWN; + } + LogVerbose("parsed model numer %d\n", m_modelNew.number); + cursor += length; + } + + // extract suffix - just a remainder after model number + m_modelNew.suffix.assign(cursor, m_model.end()); + + // decode into device family + { + switch(m_modelNew.number / 1000) + { + case 1: + if(strcmp(m_modelNew.prefix.c_str(), "DS") != 0) + break; + if(m_modelNew.suffix.size() < 1) + break; + if(m_modelNew.suffix[0] == 'D' || m_modelNew.suffix[0] == 'E') + return Series::DS1000; + else if(m_modelNew.suffix[0] == 'Z') + return Series::DS1000Z; + break; + + // case 2: + // if(strcmp(m_modelNew.prefix.c_str(), "DS") == 0 || strcmp(m_modelNew.prefix.c_str(), "MSO") == 0) + // return Series::MSODS2000; + // break; + + case 5: + if(strcmp(m_modelNew.prefix.c_str(), "MSO") == 0) + return Series::MSO5000; + break; + + // case 7: + // if(strcmp(m_modelNew.prefix.c_str(), "DS") == 0 || strcmp(m_modelNew.prefix.c_str(), "MSO") == 0) + // return Series::MSODS7000; + // break; + + // case 8: + // if(strcmp(m_modelNew.prefix.c_str(), "MSO") == 0) + // return Series::MSO8000; + // break; + + default: + break; + } + LogError("model %s was not recognized\n", m_model.c_str()); + return Series::UNKNOWN; + } + }(); +} + +void RigolOscilloscope::AnalyzeDeviceCapabilities() { + // Last digit of the model number is the number of channels + switch(m_series) { + + case Series::DS1000: + m_analogChannelCount = m_modelNew.number % 10; + m_bandwidth = m_modelNew.number % 1000 - m_analogChannelCount; + break; + + case Series::DS1000Z: + { + m_analogChannelCount = m_modelNew.number % 10; + m_bandwidth = m_modelNew.number % 1000 - m_analogChannelCount; + + { + // Probe 24M memory depth option. + // Hacky workaround since DS1000Z does not have a way how to query installed options + // Only enable chan 1 + m_transport->SendCommandQueued("CHAN1:DISP 1"); + m_transport->SendCommandQueued("CHAN2:DISP 0"); + if(m_analogChannelCount > 2) + { + m_transport->SendCommandQueued("CHAN3:DISP 0"); + m_transport->SendCommandQueued("CHAN4:DISP 0"); + } + + auto original_state = Trim(m_transport->SendCommandQueuedWithReply(":TRIG:STAT?")); + // Set in run mode to be able to set memory depth + m_transport->SendCommandQueued("RUN"); + auto originalMdepth = Trim(m_transport->SendCommandQueuedWithReply("ACQ:MDEP?")); + m_transport->SendCommandQueued("ACQ:MDEP 24000000"); + m_opt24M = Trim(m_transport->SendCommandQueuedWithReply("ACQ:MDEP?")) == "24000000"; + if (m_opt24M) + LogVerbose("this DS1000Z device has 24M option installed\n"); + + // Reset memory depth to original value + m_transport->SendCommandQueued("ACQ:MDEP " + originalMdepth); + if (original_state == "STOP") + m_transport->SendCommandQueued("STOP"); + } + + break; + } + + case Series::MSO5000: + { + m_analogChannelCount = m_modelNew.number % 10; + + // Hacky workaround since :SYST:OPT:STAT doesn't work properly on some scopes + // Only enable chan 1 + m_transport->SendCommandQueued("CHAN1:DISP 1"); + m_transport->SendCommandQueued("CHAN2:DISP 0"); + if(m_analogChannelCount > 2) + { + m_transport->SendCommandQueued("CHAN3:DISP 0"); + m_transport->SendCommandQueued("CHAN4:DISP 0"); + } + // Set in run mode to be able to set memory depth + m_transport->SendCommandQueued("RUN"); + + auto originalMdepth = Trim(m_transport->SendCommandQueuedWithReply("ACQ:MDEP?")); + m_transport->SendCommandQueued("ACQ:MDEP 200M"); + // Yes, it actually returns a stringified float, manual says "scientific notation" + m_opt200M = Trim(m_transport->SendCommandQueuedWithReply("ACQ:MDEP?")) == "2.0000E+08"; + + // Reset memory depth + m_transport->SendCommandQueued("ACQ:MDEP 1M"); + string originalBandwidthLimit = m_transport->SendCommandQueuedWithReply("CHAN1:BWL?"); + + // Figure out its actual bandwidth since :SYST:OPT:STAT is practically useless + m_transport->SendCommandQueued("CHAN1:BWL 200M"); + + + // A bit of a tree, maybe write more beautiful code + if(Trim(m_transport->SendCommandQueuedWithReply("CHAN1:BWL?")) == "200M") + m_bandwidth = 350; + else + { + m_transport->SendCommandQueued("CHAN1:BWL 100M"); + if(Trim(m_transport->SendCommandQueuedWithReply("CHAN1:BWL?")) == "100M") + m_bandwidth = 200; + else + { + if(m_modelNew.number % 1000 - m_modelNew.number % 10 == 100) + m_bandwidth = 100; + else + m_bandwidth = 70; + } + } + + m_transport->SendCommandQueued("CHAN1:BWL " + originalBandwidthLimit); + m_transport->SendCommandQueued("ACQ:MDEP " + originalMdepth); + break; + } + + //DHO are 12 bits (HD) models => default to high definition mode + // DHO only attrs: + // m_maxMdepth + // m_maxSrate + // m_lowSrate + + case Series::DHO800: + { + // - DHO802 (70MHz), DHO804 (70Mhz), DHO812 (100MHz),DHO814 (100MHz) + m_analogChannelCount = m_modelNew.number % 10; + m_bandwidth = m_modelNew.number % 100 / 10 * 100; // 814 -> 14 -> 1 -> 100 + if(m_bandwidth == 0) m_bandwidth = 70; // Fallback for DHO80x models + + m_highDefinition = true; + // DHO800/900 (DHO800 also have 50M memory since firmware v00.01.03.00.04 2024/07/11) + m_maxMdepth = 50*1000*1000; + m_maxSrate = 1.25*1000*1000*1000; + m_lowSrate = true; + + break; + } + + case Series::DHO900: + { + // - DHO914/DHO914S (125MHz), DHO924/DHO924S (250MHz) + m_analogChannelCount = m_modelNew.number % 10; + m_bandwidth = m_modelNew.number % 100 / 10 * 125; // 914 -> 24 -> 2 -> 250 + + m_highDefinition = true; + // DHO800/900 (DHO800 also have 50M memory since firmware v00.01.03.00.04 2024/07/11) + m_maxMdepth = 50*1000*1000; + m_maxSrate = 1.25*1000*1000*1000; + m_lowSrate = true; + + break; + } + + case Series::DHO1000: + { + // - DHO1072 (70MHz), DHO1074 (70MHz), DHO1102 (100MHz), DHO1104 (100MHz), DHO1202 (200MHz), DHO1204 (200MHz) + m_analogChannelCount = m_modelNew.number % 10; + m_bandwidth = m_modelNew.number % 1000 / 10 * 10; + + m_highDefinition = true; + + m_maxMdepth = 50*1000*1000; + m_maxSrate = 2*1000*1000*1000; + /* probe for bandwidth upgrades and memory upgrades on DHO1000 series */ + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? RLU\n")) == "1") + m_maxMdepth = 100*1000*1000; + + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW7T10\n")) == "1") + m_bandwidth = 100; + + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW7T20\n")) == "1") + m_bandwidth = 200; + + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW10T20\n")) == "1") + m_bandwidth = 200; + + break; + } + + case Series::DHO4000: + { + // - DHO1072 (70MHz), DHO1074 (70MHz), DHO1102 (100MHz), DHO1104 (100MHz), DHO1202 (200MHz), DHO1204 (200MHz) + m_analogChannelCount = m_modelNew.number % 10; + m_bandwidth = m_modelNew.number % 1000 / 10 * 10; + + m_highDefinition = true; + m_opt200M = false; // does not exist in DHO series + m_lowSrate = false; + + m_maxMdepth = 250*1000*1000; + m_maxSrate = 4*1000*1000*1000U; + /* probe for bandwidth upgrades and memory upgrades on DHO4000 series */ + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? RLU\n")) == "1") + m_maxMdepth = 500*1000*1000; + + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW2T4\n")) == "1") + m_bandwidth = 400; + + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW2T8\n")) == "1") + m_bandwidth = 800; + + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW4T8\n")) == "1") + m_bandwidth = 800; + break; + } + + case Series::UNKNOWN: { + LogError("RigolOscilloscope: unknown model, invalid state!\n"); + } + } +} + +static std::vector dhoSampleDepths { + // Available sample depths for DHO models, regardless of model type and activated channels + 1000, + 10 * 1000, + 100 * 1000, + 1 * 1000 * 1000, + 10 * 1000 * 1000, + 25 * 1000 * 1000, + 50 * 1000 * 1000, + 100 * 1000 * 1000, + 250 * 1000 * 1000, + 500 * 1000 * 1000, +}; + +static std::vector ds1000zSampleDepths { + // Available sample depths for ds1000z models, regardless of model type and activated channels + 12 * 1000, + 120 * 1000, + 1200 * 1000, + 12 * 1000 * 1000, + 24 * 1000 * 1000 // only as an option and only for 1 CH, presence checked at start.up +}; + +static std::vector mso5000SampleDepths { + 1000, + 10 * 1000, + 100 * 1000, + 1000 * 1000, + 10 * 1000 * 1000, + 25 * 1000 * 1000, + 50 * 1000 * 1000, + 100 * 1000 * 1000, // only for <= 2 CH + 200 * 1000 * 1000 // only for == 1 CH +}; + +void RigolOscilloscope::UpdateDynamicCapabilities() { + //FIXME + LogVerbose("updating dynamic capabilities"); + lock_guard lock(m_cacheMutex); + switch (m_series) + { + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + {// Mdepth depends on model (maxMemDepth) and number of enabled channels + uint64_t maxMemDepth = m_maxMdepth / GetEnabledChannelCount(); + vector depths; + for (auto curMemDepth : dhoSampleDepths) + { + if(curMemDepth<=maxMemDepth) + { + depths.push_back(curMemDepth); + } + else break; + } + m_depths = std::move(depths); + return; + } + case Series::MSO5000: + { + // 1k|10k|100k|1M|10M|25M|50M|100M|200M + // The maximum memory depth for the single channel is 200 M; the maximum memory + // depth for the half-channel is 100 M; and the maximum memory depth for the + // all-channel is 50 M. + // -> remove N highest + auto depths = mso5000SampleDepths; + auto channelsEnabled = GetEnabledChannelCount(); + if (channelsEnabled > 1) + depths.pop_back(); + if (channelsEnabled > 2) + depths.pop_back(); + m_depths = std::move(depths); + return; + } + + case Series::DS1000Z: + { + // For the analog channel: + // ― 1 CH: 12000|120000|1200000|12000000|24000000 + // ― 2 CH: 6000| 60000| 600000| 6000000|12000000 + // ― 3/4 CH: 3000| 30000| 300000| 3000000| 6000000 + // -> 1 CH values appropriately divided + auto divisor = GetChannelDivisor(); + vector depths; + for (auto &depth : ds1000zSampleDepths) + { + if (depth == 24'000'000 and (not m_opt24M or divisor != 1)) + continue; + + depths.emplace_back(depth/divisor); + } + m_depths = std::move(depths); + m_mdepthValid = false; + m_srateValid = false; + return; + } + + case Series::DS1000: + case Series::UNKNOWN: + LogError("RigolOscilloscope::GetSampleDepthsNonInterleaved not implemented for this model\n"); + break; + } +} + +std::size_t RigolOscilloscope::GetChannelDivisor() { + auto divisor = GetEnabledChannelCount(); + if (divisor <= 0) + divisor = 1; + else if (divisor >= 3) + divisor = 4; + return divisor; +} + +std::optional RigolOscilloscope::GetCapturePreamble() { + //This is basically the same function as a LeCroy WAVEDESC, but much less detailed + auto reply = Trim(m_transport->SendCommandQueuedWithReply("WAV:PRE?")); + LogDebug("Preamble = %s\n", reply.c_str()); + + CapturePreamble preamble {}; + int format; + int type; + + auto parsed_length = sscanf(reply.c_str(), + "%d,%d,%zu,%zu,%lf,%lf,%lf,%lf,%lf,%lf", + // is there a way of getting rid of reinterpret_cast without sacrificing typed enums and without helper variables? + &format, + &type, + &preamble.npoints, + &preamble.averages, + &preamble.sec_per_sample, + &preamble.xorigin, + &preamble.xreference, + &preamble.yincrement, + &preamble.yorigin, + &preamble.yreference); + + if (parsed_length != 10) { + LogError("Waveform data capture preamble parsing failed\n"); + return {}; + } + + // DHO QUIRK: models return page size instead of memory depth when paginating + switch (m_series) { + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + preamble.npoints = GetSampleDepth(); + break; + default: + break; + } + + // the other option was pointer reinterpret cast :/ + preamble.format = CaptureFormat(format); + preamble.type = CaptureType(type); + LogDebug("X: %ld points, %f origin, ref %f time/sample %lf\n", preamble.npoints, preamble.xorigin, preamble.xreference, preamble.sec_per_sample); + LogDebug("Y: %f inc, %f origin, %f ref\n", preamble.yincrement, preamble.yorigin, preamble.yreference); + return preamble; +} + string RigolOscilloscope::GetDriverNameInternal() { return "rigol"; @@ -337,6 +677,7 @@ void RigolOscilloscope::EnableChannel(size_t i) m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":DISP ON"); // invalidate channel enable cache until confirmed on next IsChannelEnabled m_channelsEnabled.erase(i); + UpdateDynamicCapabilities(); } void RigolOscilloscope::DisableChannel(size_t i) @@ -344,17 +685,18 @@ void RigolOscilloscope::DisableChannel(size_t i) m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":DISP OFF"); // invalidate channel enable cache until confirmed on next IsChannelEnabled m_channelsEnabled.erase(i); + UpdateDynamicCapabilities(); } vector RigolOscilloscope::GetAvailableCouplings(size_t /*i*/) { - vector ret; - ret.push_back(OscilloscopeChannel::COUPLE_DC_1M); - ret.push_back(OscilloscopeChannel::COUPLE_AC_1M); + vector couplings; + couplings.push_back(OscilloscopeChannel::COUPLE_DC_1M); + couplings.push_back(OscilloscopeChannel::COUPLE_AC_1M); //TODO: some higher end models do have 50 ohm inputs... which ones? //ret.push_back(OscilloscopeChannel::COUPLE_DC_50); - ret.push_back(OscilloscopeChannel::COUPLE_GND); - return ret; + couplings.push_back(OscilloscopeChannel::COUPLE_GND); + return couplings; } OscilloscopeChannel::CouplingType RigolOscilloscope::GetChannelCoupling(size_t i) @@ -418,6 +760,7 @@ double RigolOscilloscope::GetChannelAttenuation(size_t i) double atten; sscanf(reply.c_str(), "%lf", &atten); + //TODO: check sscanf return value for parsing errors lock_guard lock(m_cacheMutex); m_channelAttenuations[i] = atten; @@ -526,33 +869,36 @@ void RigolOscilloscope::SetChannelAttenuation(size_t i, double atten) vector RigolOscilloscope::GetChannelBandwidthLimiters(size_t /*i*/) { - vector ret; - - if(m_protocol == MSO5) + switch(m_series) { - switch(m_bandwidth) - { - case 70: - case 100: - ret = {20, 0}; - break; - case 200: - ret = {20, 100, 0}; - break; - case 350: - ret = {20, 100, 200, 0}; - break; - default: - LogError("Invalid model bandwidth\n"); - } - } - - //For now, all known DS series models only support 20 MHz or off - else if(m_protocol == DS || m_protocol == DHO) - { - ret = {20, 0}; + case Series::MSO5000: + switch(m_bandwidth) + { + case 70: + case 100: + return {20, 0}; + case 200: + return {20, 100, 0}; + case 350: + return {20, 100, 200, 0}; + default: + LogError("Invalid model bandwidth\n"); + } + break; + + case Series::DS1000: + case Series::DS1000Z: + case Series::DHO800: + case Series::DHO900: + case Series::DHO1000: + case Series::DHO4000: + return {20, 0}; + + case Series::UNKNOWN: + LogError("RigolOscilloscope: unknown model, invalid state!\n"); + break; } - return ret; + return {}; } unsigned int RigolOscilloscope::GetChannelBandwidthLimit(size_t i) @@ -579,57 +925,69 @@ unsigned int RigolOscilloscope::GetChannelBandwidthLimit(size_t i) void RigolOscilloscope::SetChannelBandwidthLimit(size_t i, unsigned int limit_mhz) { - bool valid = true; - - if(m_protocol == MSO5 || m_protocol == DHO) - { - switch(m_bandwidth) - { - case 70: - case 100: - case 125: - if((limit_mhz <= 20) & (limit_mhz != 0)) - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 20M"); - else - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL OFF"); - break; - case 200: - case 250: - if((limit_mhz <= 20) & (limit_mhz != 0)) - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 20M"); - else if((limit_mhz <= 100) & (limit_mhz != 0)) - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 100M"); - else - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL OFF"); - break; - case 350: - case 400: - case 800: + switch(m_series) { + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + + switch(m_bandwidth) + { + case 70: + case 100: + case 125: + if((limit_mhz <= 20) & (limit_mhz != 0)) + m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 20M"); + else + m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL OFF"); + break; + + case 200: + case 250: + if((limit_mhz <= 20) & (limit_mhz != 0)) + m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 20M"); + else if((limit_mhz <= 100) & (limit_mhz != 0)) + m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 100M"); + else + m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL OFF"); + break; + + case 350: + case 400: + case 800: + if((limit_mhz <= 20) & (limit_mhz != 0)) + m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 20M"); + else if((limit_mhz <= 100) & (limit_mhz != 0)) + m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 100M"); + else if((limit_mhz <= 200) & (limit_mhz != 0)) + m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 200M"); + else + m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL OFF"); + break; + + default: + LogError("Invalid model number\n"); + return; + } + break; + + case Series::DS1000: + case Series::DS1000Z: + { if((limit_mhz <= 20) & (limit_mhz != 0)) m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 20M"); - else if((limit_mhz <= 100) & (limit_mhz != 0)) - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 100M"); - else if((limit_mhz <= 200) & (limit_mhz != 0)) - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 200M"); else m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL OFF"); break; - default: - LogError("Invalid model number\n"); - valid = false; - } - } - else if(m_protocol == DS) - { - if((limit_mhz <= 20) & (limit_mhz != 0)) - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 20M"); - else - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL OFF"); + } + + case Series::UNKNOWN: + LogError("RigolOscilloscope: unknown model, invalid state!\n"); + return; + } - else - LogError("unimplemented SetChannelBandwidth for this model\n"); - if(valid) { lock_guard lock2(m_cacheMutex); if(limit_mhz == 0) @@ -656,21 +1014,50 @@ float RigolOscilloscope::GetChannelVoltageRange(size_t i, size_t /*stream*/) } string reply; - if(m_protocol == DS) - reply = Trim(m_transport->SendCommandQueuedWithReply(":" + m_channels[i]->GetHwname() + ":RANGE?")); - else if(m_protocol == MSO5 || m_protocol == DS_OLD || m_protocol == DHO) - reply = Trim(m_transport->SendCommandQueuedWithReply(":" + m_channels[i]->GetHwname() + ":SCALE?")); + switch (m_series) { + case Series::DS1000Z: + reply = Trim(m_transport->SendCommandQueuedWithReply(":" + m_channels[i]->GetHwname() + ":RANGE?")); + break; + + // we can probably use `default` here, as all modern (sane?) Rigol scopes use SCALE command + case Series::DS1000: + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + reply = Trim(m_transport->SendCommandQueuedWithReply(":" + m_channels[i]->GetHwname() + ":SCALE?")); + break; + + case Series::UNKNOWN: + return {}; + } float range; sscanf(reply.c_str(), "%f", &range); - lock_guard lock(m_cacheMutex); - if(m_protocol == MSO5 || m_protocol == DHO) - range = 8 * range; - if(m_protocol == DS_OLD) - range = 10 * range; - m_channelVoltageRanges[i] = range; + //TODO: check sscanf return value for parsing errors + { + lock_guard lock(m_cacheMutex); - return range; + switch (m_series) { + case Series::DS1000Z: + return m_channelVoltageRanges[i] = range; + + case Series::DS1000: + return m_channelVoltageRanges[i] = 10 * range; + + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + return m_channelVoltageRanges[i] = 8 * range; + + case Series::UNKNOWN: + break; + } + } + return {}; } void RigolOscilloscope::SetChannelVoltageRange(size_t i, size_t /*stream*/, float range) @@ -680,10 +1067,24 @@ void RigolOscilloscope::SetChannelVoltageRange(size_t i, size_t /*stream*/, floa m_channelVoltageRanges[i] = range; } - if(m_protocol == DS) - m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":RANGE " + to_string(range)); - else if(m_protocol == MSO5 || m_protocol == DS_OLD || m_protocol == DHO) - m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":SCALE " + to_string(range / 8)); + switch (m_series) { + case Series::DS1000Z: + m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":RANGE " + to_string(range)); + return; + + // we can probably use `default` here, as all modern (sane?) Rigol scopes use SCALE command + case Series::DS1000: + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":SCALE " + to_string(range / 8)); + return; + + case Series::UNKNOWN: + return; + } } OscilloscopeChannel* RigolOscilloscope::GetExternalTrigger() @@ -705,6 +1106,7 @@ float RigolOscilloscope::GetChannelOffset(size_t i, size_t /*stream*/) float offset; sscanf(reply.c_str(), "%f", &offset); + //TODO: check sscanf return value for parsing errors lock_guard lock(m_cacheMutex); m_channelOffsets[i] = offset; @@ -724,11 +1126,38 @@ Oscilloscope::TriggerMode RigolOscilloscope::PollTrigger() if(m_liveMode) return TRIGGER_MODE_TRIGGERED; - auto stat = Trim(m_transport->SendCommandQueuedWithReply(":TRIG:STAT?")); + if (m_series == Series::DS1000Z) { + // DS1000Z report trigger status in unreliable way. + // When triggered, it reports STOP for some time. + // Then it goes though RUN->WAIT->TRIG and ends up in STOP, + // but sometimes it completely skips transition to other states and + // remains in STOP mode for a whole time even though there are new data. + // As a workaround, we monitor output WAV:PREamble which also report sample count. + // Once it matches configured memory depth, we consider capture to be complete. + // This is also much faster than waiting for trigger states. + + // TODO: check if Protocol::MSO5000 procool could use this polling method + // TODO: consider to invalidate cached depth on trigger command + const auto sampleDepth = GetSampleDepth(); + LogVerbose("RigolOscilloscope:PollTrigger:DS1000Z: sdepth %" PRIu64 "\n", sampleDepth); + if (sampleDepth) { + const auto preamble = GetCapturePreamble(); + if (preamble.has_value() && preamble->npoints == sampleDepth) { + m_triggerArmed = false; + m_triggerWasLive = false; + return TRIGGER_MODE_TRIGGERED; + } + } + + } + auto stat = Trim(m_transport->SendCommandQueuedWithReply(":TRIG:STAT?")); + if(stat != "STOP") m_triggerWasLive = true; + LogVerbose("RigolOscilloscope:PollTrigger:DS1000Z: stat reply %s, m_triggerArmed %d, m_triggerWasLive %d, \n", stat.c_str(), m_triggerArmed, m_triggerWasLive); + if(stat == "TD") return TRIGGER_MODE_TRIGGERED; else if(stat == "RUN") @@ -742,7 +1171,8 @@ Oscilloscope::TriggerMode RigolOscilloscope::PollTrigger() //The "TD" state is not sticky on Rigol scopes, unlike the equivalent LeCroy status register bit. //The scope will go from "run" to "stop" state on trigger with only a momentary pass through "TD". //If we armed the trigger recently and we're now stopped, this means we must have triggered. - if(m_triggerArmed && (m_protocol != DS_OLD || m_triggerWasLive)) + // DS1000 QUIRK + if(m_triggerArmed && (m_series != Series::DS1000 || m_triggerWasLive)) { m_triggerArmed = false; m_triggerWasLive = false; @@ -756,7 +1186,7 @@ Oscilloscope::TriggerMode RigolOscilloscope::PollTrigger() bool RigolOscilloscope::AcquireData() { - //LogDebug("Acquiring data\n"); + LogTrace("Acquiring data\n"); lock_guard lock(m_transport->GetMutex()); LogIndenter li; @@ -768,82 +1198,109 @@ bool RigolOscilloscope::AcquireData() double now = GetTime(); //Grab the analog waveform data - int unused1; - int unused2; - size_t npoints; - int unused3; - double sec_per_sample; - double xorigin; - double xreference; - double yincrement; - double yorigin; - double yreference; - size_t maxpoints = 250 * 1000; - if(m_protocol == DS) - maxpoints = 250 * 1000; - else if(m_protocol == DS_OLD) - maxpoints = 8192; // FIXME - else if(m_protocol == MSO5) - maxpoints = GetSampleDepth(); //You can use 250E6 points too, but it is very slow - - unsigned char* temp_buf = new unsigned char[(m_highDefinition ? (maxpoints * 2) : maxpoints) + 1]; - map> pending_waveforms; - for(size_t i = 0; i < m_analogChannelCount; i++) + size_t npoints {}; + double sec_per_sample {}; + double yincrement {}; + double yorigin {}; + double yreference {}; + size_t maxpoints {}; + switch (m_series) { + case Series::DS1000: + maxpoints = 8192; // FIXME + break; + case Series::DS1000Z: + // manual specifies 250k as a maximum for bytes output + // maxpoints = 250 * 1000; + + // on my DS1054Z FW ... this larger chunk also works well and + // we get around ~20% speed-up when downloading a lot of data + maxpoints = 1024 * 1024; + break; + case Series::MSO5000: + maxpoints = GetSampleDepth(); //You can use 250E6 points too, but it is very slow + break; + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + maxpoints = 250 * 1000; + break; + case Series::UNKNOWN: + return false; + } + vector temp_buf; + temp_buf.resize((m_highDefinition ? (maxpoints * 2) : maxpoints) + 1); + map>> pending_waveforms; + for(auto channelIdx = 0U; channelIdx < m_analogChannelCount; channelIdx++) { - if(!IsChannelEnabled(i)) + if(!IsChannelEnabled(channelIdx)) + { + // ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_NONE, 0); + continue; + } //LogDebug("Channel %zu\n", i); int64_t fs_per_sample = 0; - if(m_protocol == DS_OLD) - { - yreference = 0; - npoints = maxpoints; - - yincrement = GetChannelVoltageRange(i, 0) / 256.0f; - yorigin = GetChannelOffset(i, 0); + // DS10000 QUIRK + switch (m_series) { + case Series::DS1000: + { + yreference = 0; + npoints = maxpoints; + + yincrement = GetChannelVoltageRange(channelIdx, 0) / 256.0f; + yorigin = GetChannelOffset(channelIdx, 0); + + auto reply = Trim(m_transport->SendCommandQueuedWithReply(":" + m_channels[channelIdx]->GetHwname() + ":OFFS?")); + sscanf(reply.c_str(), "%lf", &yorigin); + //TODO: check sscanf return value for parsing errors + + /* for these scopes, this is seconds per div */ + reply = Trim(m_transport->SendCommandQueuedWithReply(":TIM:SCAL?")); + sscanf(reply.c_str(), "%lf", &sec_per_sample); + //TODO: check sscanf return value for parsing errors + fs_per_sample = (sec_per_sample * 12 * FS_PER_SECOND) / npoints; + break; + } - auto reply = Trim(m_transport->SendCommandQueuedWithReply(":" + m_channels[i]->GetHwname() + ":OFFS?")); - sscanf(reply.c_str(), "%lf", &yorigin); + case Series::DS1000Z: + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + { + m_transport->SendCommandQueued(string("WAV:SOUR ") + m_channels[channelIdx]->GetHwname()); + + auto preamble = GetCapturePreamble(); + if (not preamble.has_value()) { + continue; + } - /* for these scopes, this is seconds per div */ - reply = Trim(m_transport->SendCommandQueuedWithReply(":TIM:SCAL?")); - sscanf(reply.c_str(), "%lf", &sec_per_sample); - fs_per_sample = (sec_per_sample * 12 * FS_PER_SECOND) / npoints; - } - else - { - m_transport->SendCommandQueued(string("WAV:SOUR ") + m_channels[i]->GetHwname()); - - //This is basically the same function as a LeCroy WAVEDESC, but much less detailed - auto reply = Trim(m_transport->SendCommandQueuedWithReply("WAV:PRE?")); - //LogDebug("Preamble = %s\n", reply.c_str()); - sscanf(reply.c_str(), - "%d,%d,%zu,%d,%lf,%lf,%lf,%lf,%lf,%lf", - &unused1, - &unused2, - &npoints, - &unused3, - &sec_per_sample, - &xorigin, - &xreference, - &yincrement, - &yorigin, - &yreference); - if(sec_per_sample == 0) - { // Sometimes the scope might return a null value for xincrement => ignore waveform to prenvent an Arithmetic exception in WaveformArea::RasterizeAnalogOrDigitalWaveform - LogWarning("Got null sec_per_sample value from the scope, ignoring this waveform.\n"); - continue; - } - fs_per_sample = round(sec_per_sample * FS_PER_SECOND); - if(m_protocol == DHO) - { // DHO models return page size instead of memory depth when paginating - npoints = GetSampleDepth(); + if(preamble->sec_per_sample == 0) + { // Sometimes the scope might return a null value for xincrement => ignore waveform to prenvent an Arithmetic exception in WaveformArea::RasterizeAnalogOrDigitalWaveform + LogWarning("Got null sec_per_sample value from the scope, ignoring this waveform.\n"); + continue; + } + fs_per_sample = round(preamble->sec_per_sample * FS_PER_SECOND); + + npoints = preamble->npoints; + sec_per_sample = preamble->sec_per_sample; + // xorigin = preamble->xorigin; + // xreference = preamble->xreference; + yincrement = preamble->yincrement; + yorigin = preamble->yorigin; + yreference = preamble->yreference; + //LogDebug("X: %d points, %f origin, ref %f fs/sample %ld\n", (int) npoints, xorigin, xreference, (long int) fs_per_sample); + //LogDebug("Y: %f inc, %f origin, %f ref\n", yincrement, yorigin, yreference); + break; } - //LogDebug("X: %d points, %f origin, ref %f fs/sample %ld\n", (int) npoints, xorigin, xreference, (long int) fs_per_sample); - //LogDebug("Y: %f inc, %f origin, %f ref\n", yincrement, yorigin, yreference); + + case Series::UNKNOWN: + return false; } //If we have zero points in the reply, skip reading data from this channel @@ -851,86 +1308,117 @@ bool RigolOscilloscope::AcquireData() continue; //Set up the capture we're going to store our data into - auto cap = AllocateAnalogWaveform(m_nickname + "." + GetChannel(i)->GetHwname()); - cap->Resize(0); + std::unique_ptr cap {AllocateAnalogWaveform(m_nickname + "." + GetChannel(channelIdx)->GetHwname())}; + cap->clear(); + cap->Reserve(npoints); cap->m_timescale = fs_per_sample; cap->m_triggerPhase = 0; cap->m_startTimestamp = floor(now); cap->m_startFemtoseconds = (now - floor(now)) * FS_PER_SECOND; - ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, 0.0); + ChannelsDownloadStatusUpdate(channelIdx, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, 0.0); //Downloading the waveform is a pain in the butt, because we can only pull 250K points at a time! (Unless you have a MSO5) + auto ts_start = chrono::steady_clock::now(); for(size_t npoint = 0; npoint < npoints;) { - if(m_protocol == MSO5) - { - //Ask for the data block - m_transport->SendCommandQueued("*WAI"); - m_transport->SendCommandQueued("WAV:DATA?"); - } - else if(m_protocol == DS_OLD) - { - m_transport->SendCommandQueued(string(":WAV:DATA? ") + m_channels[i]->GetHwname()); - } - else - { - //Ask for the data - m_transport->SendCommandQueued(string("WAV:STAR ") + to_string(npoint + 1)); //ONE based indexing WTF - size_t end = npoint + maxpoints; - if(end > npoints) - end = npoints; - m_transport->SendCommandQueued( - string("WAV:STOP ") + to_string(end)); //Here it is zero based, so it gets from 1-1000 - - //Ask for the data block - m_transport->SendCommandQueued("WAV:DATA?"); + + switch (m_series) { + case Series::DS1000: + m_transport->SendCommandQueued(string(":WAV:DATA? ") + m_channels[channelIdx]->GetHwname()); + break; + + case Series::DS1000Z: + { + // specify block sample range + m_transport->SendCommandQueued(string("WAV:STAR ") + to_string(npoint+1)); //ONE based indexing WTF + m_transport->SendCommandQueued(string("WAV:STOP ") + to_string(min(npoint + maxpoints, npoints))); //Here it is zero based, so it gets from 1-1000 + // m_transport->SendCommandQueued("*WAI"); // looks unnecessary + + //Ask for the data block + m_transport->SendCommandQueued("WAV:DATA?"); + } break; + + + case Series::MSO5000: + //Ask for the data block + m_transport->SendCommandQueued("*WAI"); + m_transport->SendCommandQueued("WAV:DATA?"); + break; + + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + { + //Ask for the data + m_transport->SendCommandQueued(string("WAV:STAR ") + to_string(npoint + 1)); //ONE based indexing WTF + size_t end = npoint + maxpoints; + if(end > npoints) + end = npoints; + m_transport->SendCommandQueued( + string("WAV:STOP ") + to_string(end)); //Here it is zero based, so it gets from 1-1000 + + //Ask for the data block + m_transport->SendCommandQueued("WAV:DATA?"); + break; + } + + case Series::UNKNOWN: + LogError("RigolOscilloscope: unknown model, invalid state!\n"); + return false; } + m_transport->FlushCommandQueue(); //Read block header, should be maximally 11 long on MSO5 scope with >= 100 MPoints - unsigned char header[12] = {0}; - + unsigned char header_size; - m_transport->ReadRawData(2, header); - //LogWarning("Time %f\n", (GetTime() - start)); - - sscanf((char*)header, "#%c", &header_size); - header_size = header_size - '0'; - - if(header_size > 12) { - header_size = 12; + array header_size_raw {}; + m_transport->ReadRawData(2, reinterpret_cast(header_size_raw.data())); + //LogWarning("Time %f\n", (GetTime() - start)); + + sscanf(header_size_raw.data(), "#%c", &header_size); + //TODO: check sscanf return value for parsing errors + header_size = header_size - '0'; + + if(header_size > 12) + { + header_size = 12; + } + } - - m_transport->ReadRawData(header_size, header); + + array header {0}; + m_transport->ReadRawData(header_size, reinterpret_cast(header.data())); //Look up the block size //size_t blocksize = end - npoints; //LogDebug("Block size = %zu\n", blocksize); - size_t header_blocksize; + // Block size is provided in bytes, not in points size_t header_blocksize_bytes; - sscanf((char*)header, "%zu", &header_blocksize); + sscanf(header.data(), "%zu", &header_blocksize_bytes); + //TODO: check sscanf return value for parsing errors //LogDebug("Header block size = %zu\n", header_blocksize); - if(header_blocksize == 0) + if(header_blocksize_bytes == 0) { LogWarning("Ran out of data after %zu points\n", npoint); - m_transport->ReadRawData(1, temp_buf); //discard the trailing newline + unsigned char sink; + m_transport->ReadRawData(1, &sink); //discard the trailing newline - //If this happened after zero samples, free the waveform so it doesn't leak + // If this happened after zero samples, free the waveform so it doesn't leak if(npoint == 0) { - AddWaveformToAnalogPool(cap); - cap = nullptr; + AddWaveformToAnalogPool(cap.release()); } break; } - // Block size is provided in bytes, not in points - header_blocksize_bytes = header_blocksize; + auto header_blocksize = header_blocksize_bytes; if(m_highDefinition) - header_blocksize = header_blocksize_bytes/2; + header_blocksize = header_blocksize_bytes/2; if(header_blocksize > maxpoints) { header_blocksize = maxpoints; @@ -940,66 +1428,77 @@ bool RigolOscilloscope::AcquireData() //Scale: (value - Yorigin - Yref) * Yinc size_t bytesToRead = header_blocksize_bytes + 1; //trailing newline after data block - auto downloadCallback = [i, this, npoint, npoints, bytesToRead] (float progress) { + auto downloadCallback = [channelIdx, this, npoint, npoints, bytesToRead] (float progress) { /* we get the percentage of this particular download; convert this into linear percentage across all chunks */ float bytes_progress = npoint * (m_highDefinition ? 2 : 1) + progress * bytesToRead; float bytes_total = npoints * (m_highDefinition ? 2 : 1); - ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, bytes_progress / bytes_total); + // LogVerbose("download progress %5.3f\n", bytes_progress / bytes_total); + ChannelsDownloadStatusUpdate(channelIdx, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, bytes_progress / bytes_total); }; - m_transport->ReadRawData(bytesToRead, temp_buf, downloadCallback); + m_transport->ReadRawData(bytesToRead, temp_buf.data(), downloadCallback); + downloadCallback(1); + // in case the transport did not call the progress callback (e.g. ScpiLxi), call it manually al least once after the transport finishes + double ydelta = yorigin + yreference; cap->Resize(cap->m_samples.size() + header_blocksize); cap->PrepareForCpuAccess(); - const uint16_t* temp_buf_int16 = m_highDefinition ? reinterpret_cast(temp_buf) : NULL; for(size_t j = 0; j < header_blocksize; j++) { // Handle 8bit / 16bit acquisition modes - float v; + auto &sample = cap->m_samples[npoint + j]; + uint16_t raw_sample {}; if(m_highDefinition) - { - v = (((static_cast(temp_buf_int16[j]))) - ydelta) * yincrement; - //LogDebug("V = %.3f, temp=%d, delta=%f, inc=%f\n", v, temp_buf_int16[j], ydelta, yincrement); - } + memcpy(&raw_sample, temp_buf.data() + (j * 2), sizeof(raw_sample)); else + raw_sample = temp_buf[j]; + + + switch(m_series) { - v = (static_cast(temp_buf[j]) - ydelta) * yincrement; - //LogDebug("V = %.3f, temp=%d, delta=%f, inc=%f\n", v, temp_buf[j], ydelta, yincrement); + case Series::DS1000: + sample = (128 - static_cast(raw_sample)) * yincrement - ydelta; + break; + + default: + sample = (static_cast(raw_sample) - ydelta) * yincrement; } - if(m_protocol == DS_OLD) - v = (128 - static_cast(temp_buf[j])) * yincrement - ydelta; - //LogDebug("V = %.3f, temp=%d, delta=%f, inc=%f\n", v, temp_buf[j], ydelta, yincrement); - cap->m_samples[npoint + j] = v; + //LogDebug("V = %.3f, raw=%d, delta=%f, inc=%f\n", v, raw_sample, ydelta, yincrement); } cap->MarkSamplesModifiedFromCpu(); npoint += header_blocksize; } + + auto ts_end = chrono::steady_clock::now(); + LogVerbose("download took %ld ms\n", chrono::duration_cast(ts_end - ts_start).count()); + // Notify about end of download for this channel - ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_FINISHED, 1.0); + ChannelsDownloadStatusUpdate(channelIdx, InstrumentChannel::DownloadState::DOWNLOAD_FINISHED, 1.0); //Done, update the data if(cap) - pending_waveforms[i].push_back(cap); + pending_waveforms[channelIdx].push_back(std::move(cap)); } //Now that we have all of the pending waveforms, save them in sets across all channels - m_pendingWaveformsMutex.lock(); - size_t num_pending = 1; //TODO: segmented capture support - for(size_t i = 0; i < num_pending; i++) { - SequenceSet s; - for(size_t j = 0; j < m_analogChannelCount; j++) + lock_guard lock2(m_pendingWaveformsMutex); + size_t num_pending = 1; //TODO: segmented capture support + for(size_t i = 0; i < num_pending; i++) { - if(pending_waveforms.count(j) > 0) - s[GetOscilloscopeChannel(j)] = pending_waveforms[j][i]; + SequenceSet s; + for(size_t j = 0; j < m_analogChannelCount; j++) + { + if(pending_waveforms.count(j) > 0) + s[GetOscilloscopeChannel(j)] = pending_waveforms[j][i].release(); + } + m_pendingWaveforms.push_back(s); } - m_pendingWaveforms.push_back(s); } - m_pendingWaveformsMutex.unlock(); //Clean up - delete[] temp_buf; + temp_buf = {}; // Tell the download monitor that waveform download has finished ChannelsDownloadFinished(); @@ -1009,19 +1508,29 @@ bool RigolOscilloscope::AcquireData() //Re-arm the trigger if not in one-shot mode if(!m_triggerOneShot) { - if(m_protocol == DS_OLD) - { - m_transport->SendCommandQueued(":STOP"); - m_transport->SendCommandQueued(":TRIG:EDGE:SWE SING"); - m_transport->SendCommandQueued(":RUN"); - } - else + switch (m_series) { - if(!m_liveMode) - { - m_transport->SendCommandQueued(":SING"); - m_transport->SendCommandQueued("*WAI"); - } + case Series::DS1000: + m_transport->SendCommandQueued(":STOP"); + m_transport->SendCommandQueued(":TRIG:EDGE:SWE SING"); + m_transport->SendCommandQueued(":RUN"); + break; + + case Series::DS1000Z: + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + if(!m_liveMode) + { + m_transport->SendCommandQueued(":SING"); + m_transport->SendCommandQueued("*WAI"); + } + break; + + case Series::UNKNOWN: + return false; } m_triggerArmed = true; } @@ -1032,72 +1541,106 @@ bool RigolOscilloscope::AcquireData() } void RigolOscilloscope::PrepareStart() -{ - if(m_protocol == DHO) +{ + switch (m_series) { - // DHO models need to set raw mode off and on again or vice versa to reset the number of points according to the current memory depth - if(m_liveMode) - { - m_transport->SendCommandQueued(":WAV:MODE RAW"); - m_transport->SendCommandQueued(":WAV:MODE NORM"); - } - else - { - m_transport->SendCommandQueued(":WAV:MODE NORM"); - m_transport->SendCommandQueued(":WAV:MODE RAW"); - } + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + // DHO models need to set raw mode off and on again or vice versa to reset the number of points according to the current memory depth + if(m_liveMode) + { + m_transport->SendCommandQueued(":WAV:MODE RAW"); + m_transport->SendCommandQueued(":WAV:MODE NORM"); + } + else + { + m_transport->SendCommandQueued(":WAV:MODE NORM"); + m_transport->SendCommandQueued(":WAV:MODE RAW"); + } + break; + + case Series::DS1000: + case Series::DS1000Z: + case Series::MSO5000: + case Series::UNKNOWN: + break; } } void RigolOscilloscope::Start() { - //LogDebug("Start single trigger\n"); - if(m_protocol == DS_OLD) + LogTrace("Start\n"); + switch (m_series) { - m_transport->SendCommandQueued(":TRIG:EDGE:SWE SING"); - m_transport->SendCommandQueued(":RUN"); - } - else if(m_protocol == DHO) - { // Check for memory depth : if it is 1k, switch to live mode for better performance - // Limit live mode to one channel setup to prevent grabbing waveforms from to different triggers on seperate channels - if(GetEnabledChannelCount()==1) - { - m_mdepthValid = false; - GetSampleDepth(); - m_liveMode = (m_mdepth == 1000); - } - else - { - m_liveMode = false; - } - PrepareStart(); - m_transport->SendCommandQueued(m_liveMode ? ":RUN" : ":SING"); - m_transport->SendCommandQueued("*WAI"); - } - else - { - m_transport->SendCommandQueued(":SING"); - m_transport->SendCommandQueued("*WAI"); + case Series::DS1000: + m_transport->SendCommandQueued(":TRIG:EDGE:SWE SING"); + m_transport->SendCommandQueued(":RUN"); + break; + + case Series::DS1000Z: + case Series::MSO5000: + m_transport->SendCommandQueued(":SING"); + m_transport->SendCommandQueued("*WAI"); + break; + + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + // Check for memory depth : if it is 1k, switch to live mode for better performance + // Limit live mode to one channel setup to prevent grabbing waveforms from to different triggers on seperate channels + if(GetEnabledChannelCount()==1) + { + m_mdepthValid = false; + GetSampleDepth(); + m_liveMode = (m_mdepth == 1000); + } + else + { + m_liveMode = false; + } + PrepareStart(); + m_transport->SendCommandQueued(m_liveMode ? ":RUN" : ":SING"); + m_transport->SendCommandQueued("*WAI"); + break; + + case Series::UNKNOWN: + break; } + m_triggerArmed = true; m_triggerOneShot = false; } void RigolOscilloscope::StartSingleTrigger() { + LogTrace("Start single trigger\n"); m_liveMode = false; m_mdepthValid = false; // Memory depth might have been changed on scope PrepareStart(); - if(m_protocol == DS_OLD) - { - m_transport->SendCommandQueued(":TRIG:EDGE:SWE SING"); - m_transport->SendCommandQueued(":RUN"); - } - else + switch (m_series) { - m_transport->SendCommandQueued(":SING"); - m_transport->SendCommandQueued("*WAI"); + case Series::DS1000: + m_transport->SendCommandQueued(":TRIG:EDGE:SWE SING"); + m_transport->SendCommandQueued(":RUN"); + break; + + case Series::DS1000Z: + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + m_transport->SendCommandQueued(":SING"); + m_transport->SendCommandQueued("*WAI"); + break; + + case Series::UNKNOWN: + break; } + m_triggerArmed = true; m_triggerOneShot = true; } @@ -1115,10 +1658,22 @@ void RigolOscilloscope::ForceTrigger() m_liveMode = false; m_mdepthValid = false; // Memory depth might have been changed on scope PrepareStart(); - if(m_protocol == DS || m_protocol == DHO) - m_transport->SendCommandQueued(":TFOR"); - else - LogError("RigolOscilloscope::ForceTrigger not implemented for this model\n"); + switch (m_series) + { + case Series::DS1000Z: + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + m_transport->SendCommandQueued(":TFOR"); + break; + + case Series::DS1000: + case Series::UNKNOWN: + LogError("RigolOscilloscope::ForceTrigger not implemented for this model\n"); + break; + } } bool RigolOscilloscope::IsTriggerArmed() @@ -1129,190 +1684,240 @@ bool RigolOscilloscope::IsTriggerArmed() static std::map dhoLowSampleRates { // Available sample rates for DHO 800/900 models, regardless of activated channels // Map each sample rate to its horizontal scale ratio (this ratio changes based on srate on DHO models) - { 200, 10}, - { 500, 10}, - { 1000, 10}, - { 2000, 10}, - { 5000, 10}, - { 10 * 1000, 10}, - { 20 * 1000, 10}, - { 50 * 1000, 10}, - { 100 * 1000, 10}, - { 125 * 1000, 16}, - { 250 * 1000, 20}, - { 500 * 1000, 20}, - { 1250 * 1000, 16}, - { 2500 * 1000, 20}, - { 6250 * 1000, 16}, - { 12500 * 1000, 16}, - { 31250 * 1000, 16}, - { 62500 * 1000, 16}, - { 156250 * 1000, 12.8}, - { 312500 * 1000, 16}, - { 625000 * 1000, 16}, - { 1250 * 1000 * 1000, 16}, + { 200, 10}, + { 500, 10}, + { 1000, 10}, + { 2000, 10}, + { 5000, 10}, + { 10 * 1000, 10}, + { 20 * 1000, 10}, + { 50 * 1000, 10}, + { 100 * 1000, 10}, + { 125 * 1000, 16}, + { 250 * 1000, 20}, + { 500 * 1000, 20}, + { 1250 * 1000, 16}, + { 2500 * 1000, 20}, + { 6250 * 1000, 16}, + { 12500 * 1000, 16}, + { 31250 * 1000, 16}, + { 62500 * 1000, 16}, + { 156250 * 1000, 12.8}, + { 312500 * 1000, 16}, + { 625000 * 1000, 16}, + { 1250 * 1000 * 1000, 16}, }; static std::map dhoHighSampleRates { // Available sample rates for DHO 1000/4000 models, regardless of activated channels // Map each sample rate to its horizontal scale ratio (this ratio changes based on srate on DHO models) - { 100, 10}, - { 200, 10}, - { 500, 10}, - { 1000, 10}, - { 2000, 10}, - { 5000, 10}, - { 10 * 1000, 10}, - { 20 * 1000, 10}, - { 50 * 1000, 10}, - { 100 * 1000, 10}, - { 200 * 1000, 10}, - { 500 * 1000, 10}, - { 1 * 1000 * 1000, 10}, - { 2 * 1000 * 1000, 10}, - { 5 * 1000 * 1000, 10}, - { 10 * 1000 * 1000, 10}, - { 20 * 1000 * 1000, 10}, - { 50 * 1000 * 1000, 10}, - { 100 * 1000 * 1000, 10}, - { 200 * 1000 * 1000, 10}, - { 500 * 1000 * 1000, 10}, + { 100, 10}, + { 200, 10}, + { 500, 10}, + { 1000, 10}, + { 2000, 10}, + { 5000, 10}, + { 10 * 1000, 10}, + { 20 * 1000, 10}, + { 50 * 1000, 10}, + { 100 * 1000, 10}, + { 200 * 1000, 10}, + { 500 * 1000, 10}, + { 1 * 1000 * 1000, 10}, + { 2 * 1000 * 1000, 10}, + { 5 * 1000 * 1000, 10}, + { 10 * 1000 * 1000, 10}, + { 20 * 1000 * 1000, 10}, + { 50 * 1000 * 1000, 10}, + { 100 * 1000 * 1000, 10}, + { 200 * 1000 * 1000, 10}, + { 500 * 1000 * 1000, 10}, { 1 * 1000 * 1000 * 1000, 10}, { 2 * 1000 * 1000 * 1000, 10}, { 4 * 1000 * 1000 * 1000U, 10}, }; +struct Ds1000zSrate { + uint64_t m_value; + uint8_t m_mdepthMask {}; + + bool supportsMdepth(uint64_t mdepth, size_t channelCount) const { + if (mdepth * channelCount == 24'000'000) { + //TODO: this is somewhat hacky approach + return m_mdepthMask & 16; + } + // as ds1000z samplerates are alway order of magnitude different, + // we can just count trailing zeros in decadic mode + // this way, we easily ignore rate division caused by channel count + size_t zeros {}; + while (mdepth != 0 and mdepth % 10 == 0) { + ++zeros; + mdepth /= 10; + } + return m_mdepthMask & (1 << (zeros - 3)); + } +}; + +static std::vector ds1000zSampleRates { + { 20, 1 }, // 12k + { 50, 1 }, // 12k + { 100, 1 }, // 12k + { 200, 1 | 2 }, // 12k 120k + { 500, 1 | 2 }, // 12k 120k + { 1000, 1 | 2 }, // 12k 120k + { 2000, 1 | 2 | 4 }, // 12k 120k 1M2 + { 5000, 1 | 2 | 4 }, // 12k 120k 1M2 + { 10 * 1000, 1 | 2 | 4 }, // 12k 120k 1M2 + { 20 * 1000, 1 | 2 | 4 }, // 12k 120k 1M2 12M + { 40 * 1000, 16}, // 24M + { 50 * 1000, 1 | 2 | 4 | 8 }, // 12k 120k 1M2 12M + { 100 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M + { 200 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M + { 400 * 1000, 16}, // 24M + { 500 * 1000, 1 | 2 | 4 | 8 }, // 12k 120k 1M2 12M + { 1 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M + { 2 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M + { 4 * 1000 * 1000, 16}, // 24M + { 5 * 1000 * 1000, 1 | 2 | 4 | 8 }, // 12k 120k 1M2 12M + { 10 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M + { 25 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M + { 50 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M + { 125 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M + { 250 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M + { 500 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M + {1000 * 1000 * 1000, 1 | 2 | 4 | 8 | 16} // 12k 120k 1M2 12M 24M +}; + vector RigolOscilloscope::GetSampleRatesNonInterleaved() { + LogTrace("GetSampleRatesNonInterleaved called"); + //FIXME - vector ret; - if(m_protocol == MSO5) + switch (m_series) { - ret = { - 100, - 200, - 500, - 1000, - 2000, - 5000, - 10 * 1000, - 20 * 1000, - 50 * 1000, - 100 * 1000, - 200 * 1000, - 500 * 1000, - 1 * 1000 * 1000, - 2 * 1000 * 1000, - 5 * 1000 * 1000, - 10 * 1000 * 1000, - 20 * 1000 * 1000, - 50 * 1000 * 1000, - 100 * 1000 * 1000, - 200 * 1000 * 1000, - 500 * 1000 * 1000, - 1 * 1000 * 1000 * 1000, - 2 * 1000 * 1000 * 1000, - }; - } - else if(m_protocol == DHO) - { // For DHO model, srates depend on high/low sample rate models, max srate and number of enabled channels - uint64_t maxSrate = m_maxSrate / GetEnabledChannelCount(); - uint64_t curSrate; - for (auto curSrateItem : (m_lowSrate ? dhoLowSampleRates : dhoHighSampleRates)) + case Series::DS1000Z: { - curSrate = curSrateItem.first; - if(curSrate<=maxSrate) + vector rates {}; + auto divisor = GetChannelDivisor(); + auto mdepth = GetSampleDepth(); + for (const auto& rate : ds1000zSampleRates) + if (rate.supportsMdepth(mdepth, divisor)) + rates.push_back(rate.m_value / divisor); + return rates; + } + + + case Series::MSO5000: + return { + 100, + 200, + 500, + 1000, + 2000, + 5000, + 10 * 1000, + 20 * 1000, + 50 * 1000, + 100 * 1000, + 200 * 1000, + 500 * 1000, + 1 * 1000 * 1000, + 2 * 1000 * 1000, + 5 * 1000 * 1000, + 10 * 1000 * 1000, + 20 * 1000 * 1000, + 50 * 1000 * 1000, + 100 * 1000 * 1000, + 200 * 1000 * 1000, + 500 * 1000 * 1000, + 1 * 1000 * 1000 * 1000, + 2 * 1000 * 1000 * 1000, + }; + break; + + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + { + + // For DHO model, srates depend on high/low sample rate models, max srate and number of enabled channels + uint64_t maxSrate = m_maxSrate / GetEnabledChannelCount(); + vector ret; + for (auto curSrateItem : (m_lowSrate ? dhoLowSampleRates : dhoHighSampleRates)) { - ret.push_back(curSrate); + auto curSrate = curSrateItem.first; + if(curSrate<=maxSrate) + { + ret.push_back(curSrate); + } + else break; } - else break; + return ret; } + + case Series::DS1000: + case Series::UNKNOWN: + LogError("RigolOscilloscope::GetSampleRatesNonInterleaved not implemented for this model\n"); + break; } - else - LogError("RigolOscilloscope::GetSampleRatesNonInterleaved not implemented for this model\n"); - return ret; + return {}; } vector RigolOscilloscope::GetSampleRatesInterleaved() { //FIXME - vector ret = {}; LogError("RigolOscilloscope::GetSampleRatesInterleaved not implemented for this model\n"); - return ret; + return {}; } set RigolOscilloscope::GetInterleaveConflicts() { - if(m_protocol == DHO) - { // No interleave conflicts possible on DHO models - return {}; + switch (m_series) + { + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + // No interleave conflicts possible on DHO models + return {}; + case Series::UNKNOWN: + case Series::DS1000: + case Series::DS1000Z: + case Series::MSO5000: + break; } //FIXME - set ret; LogError("RigolOscilloscope::GetInterleaveConflicts not implemented for this model\n"); - return ret; + return {}; } -static std::vector dhoSampleDepths { - { // Available sample depths for DHO models, regardless of model type and activated channels - 1000, - 10*1000, - 100*1000, - 1*1000*1000, - 10*1000*1000, - 25*1000*1000, - 50*1000*1000, - 100*1000*1000, - 250*1000*1000, - 500*1000*1000, - } -}; - vector RigolOscilloscope::GetSampleDepthsNonInterleaved() { - //FIXME - vector ret; - if(m_protocol == MSO5) - { - ret = { - 1000, - 10 * 1000, - 100 * 1000, - 1000 * 1000, - 10 * 1000 * 1000, - 25 * 1000 * 1000, - }; - } - else if(m_protocol == DHO) - { // Mdepth depends on model (maxMemDepth) and number of enabled channels - uint64_t maxMemDepth = m_maxMdepth / GetEnabledChannelCount(); - for (auto curMemDepth : dhoSampleDepths) - { - if(curMemDepth<=maxMemDepth) - { - ret.push_back(curMemDepth); - } - else break; - } - } - else - LogError("RigolOscilloscope::GetSampleDepthsNonInterleaved not implemented for this model\n"); - return ret; + return m_depths; } vector RigolOscilloscope::GetSampleDepthsInterleaved() { - if(m_protocol == DHO) - { // Sample Depths are dynamical (depending on the number of active channels) in DHO models - return GetSampleDepthsNonInterleaved(); - } - else + switch (m_series) { - //FIXME - vector ret; - LogError("RigolOscilloscope::GetSampleDepthsInterleaved not implemented for this model\n"); - return ret; + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + // Sample Depths are dynamical (depending on the number of active channels) in DHO models + return GetSampleDepthsNonInterleaved(); + + case Series::DS1000: + case Series::DS1000Z: + case Series::MSO5000: + case Series::UNKNOWN: + break; } + + //FIXME + LogError("RigolOscilloscope::GetSampleDepthsInterleaved not implemented for this model\n"); + return {}; } uint64_t RigolOscilloscope::GetSampleRate() @@ -1320,12 +1925,17 @@ uint64_t RigolOscilloscope::GetSampleRate() if(m_srateValid) return m_srate; + LogVerbose("smaplerate updating, m_srate %" PRIu64 "\n", m_srate); + // m_transport->SendCommandQueued("*WAI"); + auto ret = Trim(m_transport->SendCommandQueuedWithReply(":ACQ:SRAT?")); double rate; sscanf(ret.c_str(), "%lf", &rate); + //TODO: check sscanf return value for parsing errors m_srate = (uint64_t)rate; m_srateValid = true; + LogVerbose("smaplerate updated, m_srate %" PRIu64 "\n", m_srate); return rate; } @@ -1338,6 +1948,7 @@ uint64_t RigolOscilloscope::GetSampleDepth() double depth; sscanf(ret.c_str(), "%lf", &depth); + //TODO: check sscanf return value for parsing errors m_mdepth = (uint64_t)depth; m_mdepthValid = true; return m_mdepth; @@ -1345,99 +1956,129 @@ uint64_t RigolOscilloscope::GetSampleDepth() void RigolOscilloscope::SetSampleDepth(uint64_t depth) { - if(m_protocol == MSO5) + switch (m_series) { - // The MSO5 series will only process a sample depth setting if the oscilloscope is in auto or normal mode. - // It's frustrating, but to accommodate, we'll grab the current mode and status for restoration later, then stick the - // scope into auto mode - string trigger_sweep_mode = m_transport->SendCommandQueuedWithReply(":TRIG:SWE?"); - string trigger_status = m_transport->SendCommandQueuedWithReply(":TRIG:STAT?"); - m_transport->SendCommandQueued(":TRIG:SWE AUTO"); - m_transport->SendCommandQueued(":RUN"); - switch(depth) + case Series::MSO5000: { - case 1000: - m_transport->SendCommandQueued("ACQ:MDEP 1k"); - break; - case 10000: - m_transport->SendCommandQueued("ACQ:MDEP 10k"); - break; - case 100000: - m_transport->SendCommandQueued("ACQ:MDEP 100k"); - break; - case 1000000: - m_transport->SendCommandQueued("ACQ:MDEP 1M"); - break; - case 10000000: - m_transport->SendCommandQueued("ACQ:MDEP 10M"); - break; - case 25000000: - m_transport->SendCommandQueued("ACQ:MDEP 25M"); - break; - case 50000000: - if(m_opt200M) + // The MSO5 series will only process a sample depth setting if the oscilloscope is in auto or normal mode. + // It's frustrating, but to accommodate, we'll grab the current mode and status for restoration later, then stick the + // scope into auto mode + string trigger_sweep_mode = m_transport->SendCommandQueuedWithReply(":TRIG:SWE?"); + string trigger_status = m_transport->SendCommandQueuedWithReply(":TRIG:STAT?"); + m_transport->SendCommandQueued(":TRIG:SWE AUTO"); + m_transport->SendCommandQueued(":RUN"); + switch(depth) + { + case 1000: + m_transport->SendCommandQueued("ACQ:MDEP 1k"); + break; + case 10000: + m_transport->SendCommandQueued("ACQ:MDEP 10k"); + break; + case 100000: + m_transport->SendCommandQueued("ACQ:MDEP 100k"); + break; + case 1000000: + m_transport->SendCommandQueued("ACQ:MDEP 1M"); + break; + case 10000000: + m_transport->SendCommandQueued("ACQ:MDEP 10M"); + break; + case 25000000: + m_transport->SendCommandQueued("ACQ:MDEP 25M"); + break; + case 50000000: + if(m_opt200M) + m_transport->SendCommandQueued("ACQ:MDEP 50M"); + else + LogError("Invalid memory depth for channel: %" PRIu64 "\n", depth); + break; + case 100000000: + //m_transport->SendCommandQueued("ACQ:MDEP 100M"); + LogError("Invalid memory depth for channel: %" PRIu64 "\n", depth); + break; + case 200000000: + //m_transport->SendCommandQueued("ACQ:MDEP 200M"); + LogError("Invalid memory depth for channel: %" PRIu64 "\n", depth); + break; + default: + LogError("Invalid memory depth for channel: %" PRIu64 "\n", depth); + } + m_transport->SendCommandQueued(":TRIG:SWE " + trigger_sweep_mode); + // This is a little hairy - do we want to stop the instrument again if it was stopped previously? Probably? + if(trigger_status == "STOP") + m_transport->SendCommandQueued(":STOP"); + break; + } + + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + { // DHO models + switch(depth) + { + case 1000: + m_transport->SendCommandQueued("ACQ:MDEP 1k"); + break; + case 10000: + m_transport->SendCommandQueued("ACQ:MDEP 10k"); + break; + case 100000: + m_transport->SendCommandQueued("ACQ:MDEP 100k"); + break; + case 1000000: + m_transport->SendCommandQueued("ACQ:MDEP 1M"); + break; + case 10000000: + m_transport->SendCommandQueued("ACQ:MDEP 10M"); + break; + case 25000000: + m_transport->SendCommandQueued("ACQ:MDEP 25M"); + break; + case 50000000: m_transport->SendCommandQueued("ACQ:MDEP 50M"); - else + break; + case 100000000: + m_transport->SendCommandQueued("ACQ:MDEP 100M"); + break; + case 250000000: + m_transport->SendCommandQueued("ACQ:MDEP 250M"); + break; + case 500000000: + m_transport->SendCommandQueued("ACQ:MDEP 500M"); + break; + default: LogError("Invalid memory depth for channel: %" PRIu64 "\n", depth); - break; - case 100000000: - //m_transport->SendCommandQueued("ACQ:MDEP 100M"); - LogError("Invalid memory depth for channel: %" PRIu64 "\n", depth); - break; - case 200000000: - //m_transport->SendCommandQueued("ACQ:MDEP 200M"); - LogError("Invalid memory depth for channel: %" PRIu64 "\n", depth); - break; - default: - LogError("Invalid memory depth for channel: %" PRIu64 "\n", depth); + } + break; } - m_transport->SendCommandQueued(":TRIG:SWE " + trigger_sweep_mode); - // This is a little hairy - do we want to stop the instrument again if it was stopped previously? Probably? - if(trigger_status == "STOP") - m_transport->SendCommandQueued(":STOP"); - } - else if(m_protocol == DHO) - { // DHO models - switch(depth) + + case Series::DS1000Z: { - case 1000: - m_transport->SendCommandQueued("ACQ:MDEP 1k"); - break; - case 10000: - m_transport->SendCommandQueued("ACQ:MDEP 10k"); - break; - case 100000: - m_transport->SendCommandQueued("ACQ:MDEP 100k"); - break; - case 1000000: - m_transport->SendCommandQueued("ACQ:MDEP 1M"); - break; - case 10000000: - m_transport->SendCommandQueued("ACQ:MDEP 10M"); - break; - case 25000000: - m_transport->SendCommandQueued("ACQ:MDEP 25M"); - break; - case 50000000: - m_transport->SendCommandQueued("ACQ:MDEP 50M"); - break; - case 100000000: - m_transport->SendCommandQueued("ACQ:MDEP 100M"); - break; - case 250000000: - m_transport->SendCommandQueued("ACQ:MDEP 250M"); - break; - case 500000000: - m_transport->SendCommandQueued("ACQ:MDEP 500M"); - break; - default: - LogError("Invalid memory depth for channel: %" PRIu64 "\n", depth); + if (depth == 24'000'000 and not m_opt24M) { + LogError("This DS1000Z device does not have 24M option installed\n"); + return; + } + // memory depth is configurable only when scope is not stopped + string original_trigger_status = m_transport->SendCommandQueuedWithReply(":TRIG:STAT?"); + m_transport->SendCommandQueued(":RUN"); + m_transport->SendCommandQueued("ACQ:MDEP " + to_string(depth)); + //TODO: whould we also use switch to accept only valid values? + // m_transport->SendCommandQueued("ACQ:MDEP " + to_string(depth)); + if(original_trigger_status == "STOP") + m_transport->SendCommandQueued(":STOP"); + m_transport->FlushCommandQueue(); + m_srateValid = false; // changing depth and keeping timebase quite often results in chnage of srate + break; } + + case Series::DS1000: + case Series::UNKNOWN: + LogError("Memory depth setting not implemented for this series"); + break; } - else - { - LogError("Memory depth setting not implemented for this series"); - } + m_mdepthValid = false; } @@ -1446,19 +2087,52 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) //FIXME, you can set :TIMebase:SCALe m_mdepthValid = false; double sampletime = GetSampleDepth() / (double)rate; - double timeScaleFactor = 10; - if(m_protocol == DHO) - { // Scale factor is not constant across all sample rates for DHO models - std::map *srates = (m_lowSrate ? &dhoLowSampleRates : &dhoHighSampleRates); - auto d = srates->find(rate); - if (d != srates->end()) + + switch (m_series) + { + case Series::DHO800: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO900: + { // Scale factor is not constant across all sample rates for DHO models + double timeScaleFactor = 10; + auto& srates = (m_lowSrate ? dhoLowSampleRates : dhoHighSampleRates); + auto d = srates.find(rate); + if (d != srates.end()) + { + timeScaleFactor = d->second; + } + m_transport->SendCommandQueued(string(":TIM:SCAL ") + to_string(sampletime / timeScaleFactor)); + break; + } + + case Series::DS1000Z: { - timeScaleFactor = d->second; + // The following equation describes the relationship among memory depth, sample rate, and waveform length: + // Memory Depth = Sample Rate x Waveform Length + // Wherein, the Memory Depth can be set using the :ACQuire:MDEPth command, and + // the Waveform Length is the product of the horizontal timebase (set by + // the :TIMebase[:MAIN]:SCALe command) times the number of the horizontal scales + // (12 for DS1000Z). + // Mdepth = Srate * wlength + // Mdepth = Srate * Tscale * 12 + // Mdepth / (Srate * 12) = * Tscale + m_transport->SendCommandQueued(string(":TIM:SCAL ") + to_string(sampletime / 12 / 2)); // not sure why we need that / 2 + // Due to unknown reason, when we read SCAL right fter changing, we get the old value + // solution is to execute _any_ command with response (e.g. *IDN? works). + // Another requirement is that this read has to happen after the acquisition finishes which could take a long time on samplerates + // (this is visible even when you operate the scope manually) + // Workaround to both is to (re-)write memory depth, don't ask me why. + SetSampleDepth(GetSampleDepth()); + break; } + + default: + break; } - m_transport->SendCommandQueued(string(":TIM:SCAL ") + to_string(sampletime / timeScaleFactor)); + //TODO: should we also protect these with mutex? m_srateValid = false; m_mdepthValid = false; } @@ -1467,10 +2141,12 @@ void RigolOscilloscope::SetTriggerOffset(int64_t offset) { //Rigol standard has the offset being from the midpoint of the capture. //Scopehal has offset from the start. - int64_t rate = GetSampleRate(); - int64_t halfdepth = GetSampleDepth() / 2; - int64_t halfwidth = static_cast(round(FS_PER_SECOND * halfdepth / rate)); - m_transport->SendCommandQueued(string(":TIM:MAIN:OFFS ") + to_string((halfwidth - offset) * SECONDS_PER_FS)); + auto rate = GetSampleRate(); + auto depth = GetSampleDepth(); + auto width_fs = static_cast(round(FS_PER_SECOND * depth / rate)); + auto halfwidth_fs = width_fs /2; + m_transport->SendCommandQueued(string(":TIM:MAIN:OFFS ") + to_string((halfwidth_fs - offset) * SECONDS_PER_FS)); + } int64_t RigolOscilloscope::GetTriggerOffset() @@ -1614,9 +2290,23 @@ void RigolOscilloscope::PushEdgeTrigger(EdgeTrigger* trig) */ void RigolOscilloscope::ForceHDMode(bool mode) { - if((m_protocol == DHO) && mode != m_highDefinition) + switch (m_series) { - m_highDefinition = mode; - m_transport->SendCommandQueued(string(":WAV:FORM ") + (m_highDefinition ? "WORD" : "BYTE")); + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + if (mode == m_highDefinition) + break; + m_highDefinition = mode; + m_transport->SendCommandQueued(string(":WAV:FORM ") + (m_highDefinition ? "WORD" : "BYTE")); + break; + + case Series::UNKNOWN: + case Series::DS1000: + case Series::DS1000Z: + case Series::MSO5000: + //TODO: report/log invalidity of this + break; } } \ No newline at end of file diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index b18d7683..a63e2caf 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -90,24 +90,50 @@ class RigolOscilloscope : public virtual SCPIOscilloscope virtual void SetSampleRate(uint64_t rate) override; virtual void SetTriggerOffset(int64_t offset) override; virtual int64_t GetTriggerOffset() override; + virtual bool HasInterleavingControls() override; + virtual bool CanInterleave() override; virtual bool IsInterleaving() override; virtual bool SetInterleaving(bool combine) override; void ForceHDMode(bool mode); protected: - enum protocol_version + enum class Series { - MSO5, //MSO5000 series - DS, - DS_OLD, - DHO, //DHO800, DHO900, DHO1000 and DHO4000 series + UNKNOWN, + + DS1000, + DS1000Z, + // MSODS2000, + // MSO4000, + MSO5000, + // MSODS7000, + // MSO8000, + // DS70000, + + DHO1000, + DHO4000, + DHO800, + DHO900, + }; + + enum class CaptureFormat : int { + BYTE = 0, // a waveform point occupies one byte (namely 8 bits). + WORD = 1, // a waveform point occupies two bytes (namely 16 bits) in which the lower 8 bits are valid and the higher 8 bits are 0. + ASC = 2 // waveform points in character number. Waveform points are returned in scientific notation and separated by commas./ + }; + + enum class CaptureType : int { + NORMAL = 0, // NORMal: read the waveform data displayed on the screen. + MAXIMUM = 1, // MAXimum: read the waveform data displayed on the screen when the instrument is in the run state and the waveform data in the internal memory in the stop state. + RAW = 2 // RAW: read the waveform data in the internal memory. Note that the waveform data in the internal memory can only be read when the oscilloscope is in the stop state and the oscilloscope can not be operated. + // NOTE: If the MATH channel is selected, only the NORMal mode is valid. }; OscilloscopeChannel* m_extTrigChannel; //hardware analog channel count, independent of LA option etc - unsigned int m_analogChannelCount; + size_t m_analogChannelCount; //config cache std::map m_channelAttenuations; @@ -129,13 +155,20 @@ class RigolOscilloscope : public virtual SCPIOscilloscope bool m_liveMode; - int m_modelNumber; + struct Model { + std::string prefix; // e.g.: DS + unsigned int number; // e.g.: 1054 + std::string suffix; // e.g.: Z + } m_modelNew; + unsigned int m_bandwidth; - bool m_opt200M; - uint64_t m_maxMdepth; /* Maximum Memory depth for DHO model s*/ - uint64_t m_maxSrate; /* Maximum Sample rate for DHO models */ - bool m_lowSrate; /* True for DHO low sample rate models (DHO800/900) */ - protocol_version m_protocol; + bool m_opt200M {}; // 200M memory depth is MSO5000 specific option + bool m_opt24M {}; // 24M memory depth is DS1000Z specific option + uint64_t m_maxMdepth {}; // Maximum Memory depth for DHO models + uint64_t m_maxSrate {}; // Maximum Sample rate for DHO models + bool m_lowSrate {}; // True for DHO low sample rate models (DHO800/900) + Series m_series; + // protocol_version m_protocol; //True if we have >8 bit capture depth bool m_highDefinition; @@ -143,7 +176,26 @@ class RigolOscilloscope : public virtual SCPIOscilloscope void PushEdgeTrigger(EdgeTrigger* trig); void PullEdgeTrigger(); + struct CapturePreamble { + CaptureFormat format; + CaptureType type; + std::size_t npoints; // an integer between 1 and 12000000. + std::size_t averages; // the number of averages in the average sample mode and 1 in other modes. + double sec_per_sample; // the time difference between two neighboring points in the X direction. + double xorigin; // the time from the trigger point to the "Reference Time" in the X direction. + double xreference; // the reference time of the data point in the X direction. + double yincrement; // the waveform increment in the Y direction. + double yorigin; // the vertical offset relative to the "Vertical Reference Position" in the Y direction. + double yreference; // the vertical reference position in the Y direction. + }; + std::vector m_depths; + + std::optional GetCapturePreamble(); void PrepareStart(); + void DecodeDeviceSeries(); + void AnalyzeDeviceCapabilities(); + void UpdateDynamicCapabilities(); // capabilities dependent on enabled chanel count + std::size_t GetChannelDivisor(); // helper function to get memory depth/sample rate divisor base on current scope state (amount of enabled channels) public: static std::string GetDriverNameInternal(); From 99e83335a83e1ec6f01477e766fb89c960d2c6cb Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 15 Nov 2025 12:38:51 +0100 Subject: [PATCH 02/59] RigolOscilloscope: enforce all case handling in switch statements to make sure all scope families are handled everywhere --- scopehal/RigolOscilloscope.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index e2fc0245..895afc70 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -41,6 +41,11 @@ #include #endif +#pragma GCC diagnostic push +#pragma GCC diagnostic error "-Wswitch" +// #pragma GCC diagnostic warning "-Wswitch-enum" + + using namespace std; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -2309,4 +2314,6 @@ void RigolOscilloscope::ForceHDMode(bool mode) //TODO: report/log invalidity of this break; } -} \ No newline at end of file +} + +#pragma GCC diagnostic pop \ No newline at end of file From c88ba99abdc08a9bad889d20576422833ec25239 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 15 Nov 2025 12:40:28 +0100 Subject: [PATCH 03/59] RigolOscilloscope: clamp trigger offset into valid range When trigger offset was beyond memory range, at least DS1000Z never received trigger. --- scopehal/RigolOscilloscope.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 895afc70..a6e2414a 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -2150,6 +2150,8 @@ void RigolOscilloscope::SetTriggerOffset(int64_t offset) auto depth = GetSampleDepth(); auto width_fs = static_cast(round(FS_PER_SECOND * depth / rate)); auto halfwidth_fs = width_fs /2; + if (offset > width_fs) + offset = width_fs; // we want to ensure, the trigger is inside the capture range 0~mdepth m_transport->SendCommandQueued(string(":TIM:MAIN:OFFS ") + to_string((halfwidth_fs - offset) * SECONDS_PER_FS)); } From 94d4deb07e92c7eb6dc57745726315478546dffa Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 15 Nov 2025 12:41:10 +0100 Subject: [PATCH 04/59] RigolOscilloscope: mark trigger offset as invalid when new values is set --- scopehal/RigolOscilloscope.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index a6e2414a..5734a4d4 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -2154,6 +2154,11 @@ void RigolOscilloscope::SetTriggerOffset(int64_t offset) offset = width_fs; // we want to ensure, the trigger is inside the capture range 0~mdepth m_transport->SendCommandQueued(string(":TIM:MAIN:OFFS ") + to_string((halfwidth_fs - offset) * SECONDS_PER_FS)); + { + lock_guard lock(m_cacheMutex); + m_triggerOffsetValid = false; + } + } int64_t RigolOscilloscope::GetTriggerOffset() From 724a779153dfa4193b7418c4f5fb74aaa9cd9272 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 15 Nov 2025 12:44:16 +0100 Subject: [PATCH 05/59] RigolOscilloscope: update trigger offset on samplerate change This is necessary, because samplerate changes timebase. While TB changes, Rigol oscilloscopes keep the trigger position constant relative to the capture mid point. Because scopehal reference point is at start, the trigger has to be updated to keep it constant from scopehal's POV. --- scopehal/RigolOscilloscope.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 5734a4d4..ae7550c5 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -2140,6 +2140,12 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) //TODO: should we also protect these with mutex? m_srateValid = false; m_mdepthValid = false; + // To prevent trigger offset travelling on Srate change (timebase change), + // re-set trigger location, because difference (time) between + // our trigger reference point (start of sample buffer) and + // scope trigger reference point (mid of the sample buffer) changed. + SetTriggerOffset(m_triggerOffset); + } void RigolOscilloscope::SetTriggerOffset(int64_t offset) From 7b647c25872aa0d1860db9a5f904f586bcd87f82 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 15 Nov 2025 12:46:08 +0100 Subject: [PATCH 06/59] RigolOscilloscope: mark interleaving as not supported As interleaving done automatically by scope is not considered to be an interleaving feature, see #1014 --- scopehal/RigolOscilloscope.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index ae7550c5..41dd407b 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -2196,6 +2196,16 @@ int64_t RigolOscilloscope::GetTriggerOffset() return m_triggerOffset; } +bool RigolOscilloscope::HasInterleavingControls() +{ + return false; +} + +bool RigolOscilloscope::CanInterleave() +{ + return false; +} + bool RigolOscilloscope::IsInterleaving() { return false; From 43b6c78a79b97ee627f2684dfb1c0ecdd8b8ab63 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 15 Nov 2025 17:07:23 +0100 Subject: [PATCH 07/59] RigolOscilloscope: fix DS1000Z max download chunk size (maxpoints) Initial tests with 1 channel and 1 MB chunks worked fine. Later tests with 2 channels failed, but 500 kB chunk size worked fine, but failed for 3 or 4 channels. It looks like the chunk size (maxpoints), also depends on the amount of enabled channels. There may be still some margin, but it's not worth chaseing it. --- scopehal/RigolOscilloscope.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 41dd407b..1f99bb54 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -1217,9 +1217,11 @@ bool RigolOscilloscope::AcquireData() // manual specifies 250k as a maximum for bytes output // maxpoints = 250 * 1000; - // on my DS1054Z FW ... this larger chunk also works well and - // we get around ~20% speed-up when downloading a lot of data - maxpoints = 1024 * 1024; + // During experiments with my DS1054Z FW 00.04.05.SP2, + // 250kB limits applies when all channels are enabled. + // It is possible to use larger chunks with less channels. + // With single channel and 1 MB block, around ~20% speed-up is observable. + maxpoints = 1000 * 1000 / GetChannelDivisor(); break; case Series::MSO5000: maxpoints = GetSampleDepth(); //You can use 250E6 points too, but it is very slow From 8c9f46e364b086cfb81a680d53d8cc5099dcfd62 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 15 Nov 2025 22:04:59 +0100 Subject: [PATCH 08/59] RigolOscilloscope: refactor resource locking Proper resource locking was missing at multiple places. It may not be 100% complete yet. --- scopehal/RigolOscilloscope.cpp | 230 +++++++++++++++++++++------------ 1 file changed, 150 insertions(+), 80 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 1f99bb54..b03d0548 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -663,17 +663,19 @@ bool RigolOscilloscope::IsChannelEnabled(size_t i) } auto reply = Trim(m_transport->SendCommandQueuedWithReply(":" + m_channels[i]->GetHwname() + ":DISP?")); - if(reply == "0") - { - lock_guard lock(m_cacheMutex); - m_channelsEnabled[i] = false; - return false; - } - else + { lock_guard lock(m_cacheMutex); - m_channelsEnabled[i] = true; - return true; + if(reply == "0") + { + m_channelsEnabled[i] = false; + return false; + } + else + { + m_channelsEnabled[i] = true; + return true; + } } } @@ -681,7 +683,10 @@ void RigolOscilloscope::EnableChannel(size_t i) { m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":DISP ON"); // invalidate channel enable cache until confirmed on next IsChannelEnabled - m_channelsEnabled.erase(i); + { + lock_guard lock(m_cacheMutex); + m_channelsEnabled.erase(i); + } UpdateDynamicCapabilities(); } @@ -689,7 +694,10 @@ void RigolOscilloscope::DisableChannel(size_t i) { m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":DISP OFF"); // invalidate channel enable cache until confirmed on next IsChannelEnabled - m_channelsEnabled.erase(i); + { + lock_guard lock(m_cacheMutex); + m_channelsEnabled.erase(i); + } UpdateDynamicCapabilities(); } @@ -714,14 +722,16 @@ OscilloscopeChannel::CouplingType RigolOscilloscope::GetChannelCoupling(size_t i auto reply = Trim(m_transport->SendCommandQueuedWithReply(":" + m_channels[i]->GetHwname() + ":COUP?")); - lock_guard lock(m_cacheMutex); - if(reply == "AC") - m_channelCouplings[i] = OscilloscopeChannel::COUPLE_AC_1M; - else if(reply == "DC") - m_channelCouplings[i] = OscilloscopeChannel::COUPLE_DC_1M; - else /* if(reply == "GND") */ - m_channelCouplings[i] = OscilloscopeChannel::COUPLE_GND; - return m_channelCouplings[i]; + { + lock_guard lock(m_cacheMutex); + if(reply == "AC") + m_channelCouplings[i] = OscilloscopeChannel::COUPLE_AC_1M; + else if(reply == "DC") + m_channelCouplings[i] = OscilloscopeChannel::COUPLE_DC_1M; + else /* if(reply == "GND") */ + m_channelCouplings[i] = OscilloscopeChannel::COUPLE_GND; + return m_channelCouplings[i]; + } } void RigolOscilloscope::SetChannelCoupling(size_t i, OscilloscopeChannel::CouplingType type) @@ -767,9 +777,11 @@ double RigolOscilloscope::GetChannelAttenuation(size_t i) sscanf(reply.c_str(), "%lf", &atten); //TODO: check sscanf return value for parsing errors - lock_guard lock(m_cacheMutex); - m_channelAttenuations[i] = atten; - return atten; + { + lock_guard lock(m_cacheMutex); + m_channelAttenuations[i] = atten; + return atten; + } } void RigolOscilloscope::SetChannelAttenuation(size_t i, double atten) @@ -916,16 +928,18 @@ unsigned int RigolOscilloscope::GetChannelBandwidthLimit(size_t i) auto reply = Trim(m_transport->SendCommandQueuedWithReply(m_channels[i]->GetHwname() + ":BWL?")); - lock_guard lock2(m_cacheMutex); - if(reply == "20M") - m_channelBandwidthLimits[i] = 20; - if(reply == "100M") - m_channelBandwidthLimits[i] = 100; - if(reply == "200M") - m_channelBandwidthLimits[i] = 200; - else - m_channelBandwidthLimits[i] = m_bandwidth; - return m_channelBandwidthLimits[i]; + { + lock_guard lock2(m_cacheMutex); + if(reply == "20M") + m_channelBandwidthLimits[i] = 20; + if(reply == "100M") + m_channelBandwidthLimits[i] = 100; + if(reply == "200M") + m_channelBandwidthLimits[i] = 200; + else + m_channelBandwidthLimits[i] = m_bandwidth; + return m_channelBandwidthLimits[i]; + } } void RigolOscilloscope::SetChannelBandwidthLimit(size_t i, unsigned int limit_mhz) @@ -1113,8 +1127,10 @@ float RigolOscilloscope::GetChannelOffset(size_t i, size_t /*stream*/) sscanf(reply.c_str(), "%f", &offset); //TODO: check sscanf return value for parsing errors - lock_guard lock(m_cacheMutex); - m_channelOffsets[i] = offset; + { + lock_guard lock(m_cacheMutex); + m_channelOffsets[i] = offset; + } return offset; } @@ -1122,8 +1138,10 @@ void RigolOscilloscope::SetChannelOffset(size_t i, size_t /*stream*/, float offs { m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":OFFS " + to_string(offset)); - lock_guard lock(m_cacheMutex); - m_channelOffsets[i] = offset; + { + lock_guard lock(m_cacheMutex); + m_channelOffsets[i] = offset; + } } Oscilloscope::TriggerMode RigolOscilloscope::PollTrigger() @@ -1579,6 +1597,7 @@ void RigolOscilloscope::PrepareStart() void RigolOscilloscope::Start() { LogTrace("Start\n"); + //TODO: consider locking transport as it should not get interrupted by something else (or can it?) switch (m_series) { case Series::DS1000: @@ -1851,7 +1870,11 @@ vector RigolOscilloscope::GetSampleRatesNonInterleaved() // For DHO model, srates depend on high/low sample rate models, max srate and number of enabled channels uint64_t maxSrate = m_maxSrate / GetEnabledChannelCount(); vector ret; - for (auto curSrateItem : (m_lowSrate ? dhoLowSampleRates : dhoHighSampleRates)) + auto& srates = [&]() -> std::map& { + lock_guard lock(m_cacheMutex); + return (m_lowSrate ? dhoLowSampleRates : dhoHighSampleRates); + }(); + for (auto curSrateItem : srates) { auto curSrate = curSrateItem.first; if(curSrate<=maxSrate) @@ -1901,6 +1924,7 @@ set RigolOscilloscope::GetInterleaveConflicts( vector RigolOscilloscope::GetSampleDepthsNonInterleaved() { + lock_guard lock(m_cacheMutex); return m_depths; } @@ -1929,10 +1953,13 @@ vector RigolOscilloscope::GetSampleDepthsInterleaved() uint64_t RigolOscilloscope::GetSampleRate() { - if(m_srateValid) - return m_srate; - - LogVerbose("smaplerate updating, m_srate %" PRIu64 "\n", m_srate); + { + lock_guard lock(m_cacheMutex); + if(m_srateValid) + return m_srate; + + LogVerbose("smaplerate updating, m_srate %" PRIu64 "\n", m_srate); + } // m_transport->SendCommandQueued("*WAI"); auto ret = Trim(m_transport->SendCommandQueuedWithReply(":ACQ:SRAT?")); @@ -1940,25 +1967,38 @@ uint64_t RigolOscilloscope::GetSampleRate() double rate; sscanf(ret.c_str(), "%lf", &rate); //TODO: check sscanf return value for parsing errors - m_srate = (uint64_t)rate; - m_srateValid = true; - LogVerbose("smaplerate updated, m_srate %" PRIu64 "\n", m_srate); - return rate; + { + lock_guard lock(m_cacheMutex); + m_srate = (uint64_t)rate; + m_srateValid = true; + LogVerbose("smaplerate updated, m_srate %" PRIu64 "\n", m_srate); + return rate; + } } uint64_t RigolOscilloscope::GetSampleDepth() { - if(m_mdepthValid) - return m_mdepth; + { + lock_guard lock(m_cacheMutex); + if(m_mdepthValid) + return m_mdepth; + + LogVerbose("mem depth updating, m_mdepth %" PRIu64 "\n", m_mdepth); + } auto ret = Trim(m_transport->SendCommandQueuedWithReply(":ACQ:MDEP?")); double depth; sscanf(ret.c_str(), "%lf", &depth); //TODO: check sscanf return value for parsing errors - m_mdepth = (uint64_t)depth; - m_mdepthValid = true; - return m_mdepth; + + { + lock_guard lock(m_cacheMutex); + m_mdepth = (uint64_t)depth; + m_mdepthValid = true; + LogVerbose("mem depth updated, m_mdepth %" PRIu64 "\n", m_mdepth); + return m_mdepth; + } } void RigolOscilloscope::SetSampleDepth(uint64_t depth) @@ -1970,6 +2010,7 @@ void RigolOscilloscope::SetSampleDepth(uint64_t depth) // The MSO5 series will only process a sample depth setting if the oscilloscope is in auto or normal mode. // It's frustrating, but to accommodate, we'll grab the current mode and status for restoration later, then stick the // scope into auto mode + lock_guard lock(m_transport->GetMutex()); // this sequence may not be interrupted by others string trigger_sweep_mode = m_transport->SendCommandQueuedWithReply(":TRIG:SWE?"); string trigger_status = m_transport->SendCommandQueuedWithReply(":TRIG:STAT?"); m_transport->SendCommandQueued(":TRIG:SWE AUTO"); @@ -2068,15 +2109,21 @@ void RigolOscilloscope::SetSampleDepth(uint64_t depth) return; } // memory depth is configurable only when scope is not stopped - string original_trigger_status = m_transport->SendCommandQueuedWithReply(":TRIG:STAT?"); - m_transport->SendCommandQueued(":RUN"); - m_transport->SendCommandQueued("ACQ:MDEP " + to_string(depth)); - //TODO: whould we also use switch to accept only valid values? - // m_transport->SendCommandQueued("ACQ:MDEP " + to_string(depth)); - if(original_trigger_status == "STOP") - m_transport->SendCommandQueued(":STOP"); - m_transport->FlushCommandQueue(); - m_srateValid = false; // changing depth and keeping timebase quite often results in chnage of srate + { + string original_trigger_status = m_transport->SendCommandQueuedWithReply(":TRIG:STAT?"); + lock_guard lock(m_transport->GetMutex()); // this sequence may not be interrupted by others + m_transport->SendCommandQueued(":RUN"); + m_transport->SendCommandQueued("ACQ:MDEP " + to_string(depth)); + //TODO: whould we also use switch to accept only valid values? + // m_transport->SendCommandQueued("ACQ:MDEP " + to_string(depth)); + if(original_trigger_status == "STOP") + m_transport->SendCommandQueued(":STOP"); + m_transport->FlushCommandQueue(); + } + { + lock_guard lock(m_cacheMutex); + m_srateValid = false; // changing depth and keeping timebase quite often results in chnage of srate + } break; } @@ -2086,13 +2133,19 @@ void RigolOscilloscope::SetSampleDepth(uint64_t depth) break; } - m_mdepthValid = false; + { + lock_guard lock(m_cacheMutex); + m_mdepthValid = false; + } } void RigolOscilloscope::SetSampleRate(uint64_t rate) { //FIXME, you can set :TIMebase:SCALe - m_mdepthValid = false; + { + lock_guard lock(m_cacheMutex); + m_mdepthValid = false; + } double sampletime = GetSampleDepth() / (double)rate; switch (m_series) @@ -2103,7 +2156,12 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) case Series::DHO900: { // Scale factor is not constant across all sample rates for DHO models double timeScaleFactor = 10; - auto& srates = (m_lowSrate ? dhoLowSampleRates : dhoHighSampleRates); + + auto& srates = [&]() -> std::map& { + lock_guard lock(m_cacheMutex); + return (m_lowSrate ? dhoLowSampleRates : dhoHighSampleRates); + }(); + auto d = srates.find(rate); if (d != srates.end()) { @@ -2139,9 +2197,11 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) } - //TODO: should we also protect these with mutex? - m_srateValid = false; - m_mdepthValid = false; + { + lock_guard lock(m_cacheMutex); + m_srateValid = false; + m_mdepthValid = false; + } // To prevent trigger offset travelling on Srate change (timebase change), // re-set trigger location, because difference (time) between // our trigger reference point (start of sample buffer) and @@ -2240,17 +2300,20 @@ void RigolOscilloscope::PullTrigger() */ void RigolOscilloscope::PullEdgeTrigger() { - //Clear out any triggers of the wrong type - if((m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL)) - { - delete m_trigger; - m_trigger = NULL; - } - - //Create a new trigger if necessary - if(m_trigger == NULL) - m_trigger = new EdgeTrigger(this); - EdgeTrigger* et = dynamic_cast(m_trigger); + auto et = [&]() -> EdgeTrigger* { + lock_guard lock(m_cacheMutex); + //Clear out any triggers of the wrong type + if((m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL)) + { + delete m_trigger; + m_trigger = NULL; + } + + //Create a new trigger if necessary + if(m_trigger == NULL) + m_trigger = new EdgeTrigger(this); + return dynamic_cast(m_trigger); + }(); //Source auto reply = Trim(m_transport->SendCommandQueuedWithReply(":TRIG:EDGE:SOUR?")); @@ -2326,11 +2389,18 @@ void RigolOscilloscope::ForceHDMode(bool mode) case Series::DHO4000: case Series::DHO800: case Series::DHO900: - if (mode == m_highDefinition) - break; - m_highDefinition = mode; - m_transport->SendCommandQueued(string(":WAV:FORM ") + (m_highDefinition ? "WORD" : "BYTE")); + { + bool highDefinition {}; + { + lock_guard lock(m_cacheMutex); + if (mode == m_highDefinition) + break; + m_highDefinition = highDefinition = mode; + } + m_transport->SendCommandQueued(string(":WAV:FORM ") + (highDefinition ? "WORD" : "BYTE")); break; + } + case Series::UNKNOWN: case Series::DS1000: From d513007fd930340708b4fef084525866255f808d Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 15 Nov 2025 22:26:36 +0100 Subject: [PATCH 09/59] RigolOscilloscope: clean-up and extend logging - change verbose logs to trace - add more trace logs - print detected options - remove purely development logs --- scopehal/RigolOscilloscope.cpp | 77 ++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index b03d0548..bddb6d8d 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -67,7 +67,7 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) LogError("device series not recognized nor supported\n"); return; } - LogVerbose("RigolOscilloscope: series: %d\n", int(m_series)); + LogTrace("RigolOscilloscope: series: %d\n", int(m_series)); AnalyzeDeviceCapabilities(); UpdateDynamicCapabilities(); @@ -190,7 +190,11 @@ uint32_t RigolOscilloscope::GetInstrumentTypesForChannel(size_t /*i*/) const void RigolOscilloscope::DecodeDeviceSeries() { - //Last digit of the model number is the number of channels + LogTrace("Decoding device series\n"); + // Called once from the ctor so we don't lock any mutex as there is only one reference to this object + // and no concurrent access is possible at this time. + + // Last digit of the model number is the number of channels m_series = [&]() -> Series { // scope name is always, no numeric prefix, followed by numeric model number optionally followed by alfanumeric suffix @@ -206,7 +210,7 @@ void RigolOscilloscope::DecodeDeviceSeries() return Series::UNKNOWN; } m_modelNew.prefix.resize(length); - LogVerbose("parsed model prefix %s\n", m_modelNew.prefix.c_str()); + LogTrace("parsed model prefix %s\n", m_modelNew.prefix.c_str()); cursor += length; } @@ -218,7 +222,7 @@ void RigolOscilloscope::DecodeDeviceSeries() LogError("could not parse scope model number\n"); return Series::UNKNOWN; } - LogVerbose("parsed model numer %d\n", m_modelNew.number); + LogTrace("parsed model numer %d\n", m_modelNew.number); cursor += length; } @@ -270,6 +274,10 @@ void RigolOscilloscope::DecodeDeviceSeries() } void RigolOscilloscope::AnalyzeDeviceCapabilities() { + // Called once from the ctor so we don't lock any mutex as there is only one reference to this object + // and no concurrent access is possible at this time. + LogTrace("Analyzing scope capabilities\n"); + // Last digit of the model number is the number of channels switch(m_series) { @@ -302,7 +310,7 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { m_transport->SendCommandQueued("ACQ:MDEP 24000000"); m_opt24M = Trim(m_transport->SendCommandQueuedWithReply("ACQ:MDEP?")) == "24000000"; if (m_opt24M) - LogVerbose("this DS1000Z device has 24M option installed\n"); + LogTrace("DS1000Z: 24 Mpts memory option detected\n"); // Reset memory depth to original value m_transport->SendCommandQueued("ACQ:MDEP " + originalMdepth); @@ -333,6 +341,9 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { m_transport->SendCommandQueued("ACQ:MDEP 200M"); // Yes, it actually returns a stringified float, manual says "scientific notation" m_opt200M = Trim(m_transport->SendCommandQueuedWithReply("ACQ:MDEP?")) == "2.0000E+08"; + if (m_opt200M) { + LogTrace("MSO5000: 200 Mpts memory option detected\n"); + } // Reset memory depth m_transport->SendCommandQueued("ACQ:MDEP 1M"); @@ -412,17 +423,25 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { m_maxMdepth = 50*1000*1000; m_maxSrate = 2*1000*1000*1000; /* probe for bandwidth upgrades and memory upgrades on DHO1000 series */ - if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? RLU\n")) == "1") + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? RLU\n")) == "1") { m_maxMdepth = 100*1000*1000; + LogTrace("DHO1000: 100 Mpts memory option detected\n"); + } - if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW7T10\n")) == "1") + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW7T10\n")) == "1") { m_bandwidth = 100; + LogTrace("DHO1000: 100 MHz bandwidth option detected\n"); + } - if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW7T20\n")) == "1") + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW7T20\n")) == "1") { m_bandwidth = 200; + LogTrace("DHO1000: 200 MHz bandwidth option detected\n"); + } - if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW10T20\n")) == "1") + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW10T20\n")) == "1") { m_bandwidth = 200; + LogTrace("DHO1000: 200 MHz bandwidth option detected\n"); + } break; } @@ -440,17 +459,25 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { m_maxMdepth = 250*1000*1000; m_maxSrate = 4*1000*1000*1000U; /* probe for bandwidth upgrades and memory upgrades on DHO4000 series */ - if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? RLU\n")) == "1") + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? RLU\n")) == "1") { m_maxMdepth = 500*1000*1000; + LogTrace("DHO4000: 500 Mpts memory option detected\n"); + } - if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW2T4\n")) == "1") + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW2T4\n")) == "1") { m_bandwidth = 400; + LogTrace("DHO4000: 400 MHz bandwidth option detected\n"); + } - if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW2T8\n")) == "1") + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW2T8\n")) == "1") { m_bandwidth = 800; + LogTrace("DHO4000: 800 MHz bandwidth option detected\n"); + } - if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW4T8\n")) == "1") + if (Trim(m_transport->SendCommandQueuedWithReply(":SYST:OPT:STAT? BW4T8\n")) == "1") { m_bandwidth = 800; + LogTrace("DHO4000: 800 MHz bandwidth option detected\n"); + } break; } @@ -458,6 +485,8 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { LogError("RigolOscilloscope: unknown model, invalid state!\n"); } } + + LogTrace("Device bandwidth: %d MHz\n", m_bandwidth); } static std::vector dhoSampleDepths { @@ -496,8 +525,7 @@ static std::vector mso5000SampleDepths { }; void RigolOscilloscope::UpdateDynamicCapabilities() { - //FIXME - LogVerbose("updating dynamic capabilities"); + LogTrace("updating dynamic capabilities\n"); lock_guard lock(m_cacheMutex); switch (m_series) { @@ -1162,7 +1190,6 @@ Oscilloscope::TriggerMode RigolOscilloscope::PollTrigger() // TODO: check if Protocol::MSO5000 procool could use this polling method // TODO: consider to invalidate cached depth on trigger command const auto sampleDepth = GetSampleDepth(); - LogVerbose("RigolOscilloscope:PollTrigger:DS1000Z: sdepth %" PRIu64 "\n", sampleDepth); if (sampleDepth) { const auto preamble = GetCapturePreamble(); if (preamble.has_value() && preamble->npoints == sampleDepth) { @@ -1179,8 +1206,6 @@ Oscilloscope::TriggerMode RigolOscilloscope::PollTrigger() if(stat != "STOP") m_triggerWasLive = true; - LogVerbose("RigolOscilloscope:PollTrigger:DS1000Z: stat reply %s, m_triggerArmed %d, m_triggerWasLive %d, \n", stat.c_str(), m_triggerArmed, m_triggerWasLive); - if(stat == "TD") return TRIGGER_MODE_TRIGGERED; else if(stat == "RUN") @@ -1209,11 +1234,11 @@ Oscilloscope::TriggerMode RigolOscilloscope::PollTrigger() bool RigolOscilloscope::AcquireData() { - LogTrace("Acquiring data\n"); - lock_guard lock(m_transport->GetMutex()); LogIndenter li; + LogTrace("Acquiring data\n"); + // Notify about download operation start ChannelsDownloadStarted(); @@ -1457,7 +1482,7 @@ bool RigolOscilloscope::AcquireData() /* we get the percentage of this particular download; convert this into linear percentage across all chunks */ float bytes_progress = npoint * (m_highDefinition ? 2 : 1) + progress * bytesToRead; float bytes_total = npoints * (m_highDefinition ? 2 : 1); - // LogVerbose("download progress %5.3f\n", bytes_progress / bytes_total); + // LogTrace("download progress %5.3f\n", bytes_progress / bytes_total); ChannelsDownloadStatusUpdate(channelIdx, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, bytes_progress / bytes_total); }; m_transport->ReadRawData(bytesToRead, temp_buf.data(), downloadCallback); @@ -1496,7 +1521,7 @@ bool RigolOscilloscope::AcquireData() } auto ts_end = chrono::steady_clock::now(); - LogVerbose("download took %ld ms\n", chrono::duration_cast(ts_end - ts_start).count()); + LogTrace("download took %ld ms\n", chrono::duration_cast(ts_end - ts_start).count()); // Notify about end of download for this channel ChannelsDownloadStatusUpdate(channelIdx, InstrumentChannel::DownloadState::DOWNLOAD_FINISHED, 1.0); @@ -1958,7 +1983,7 @@ uint64_t RigolOscilloscope::GetSampleRate() if(m_srateValid) return m_srate; - LogVerbose("smaplerate updating, m_srate %" PRIu64 "\n", m_srate); + LogTrace("smaplerate updating, m_srate %" PRIu64 "\n", m_srate); } // m_transport->SendCommandQueued("*WAI"); @@ -1971,7 +1996,7 @@ uint64_t RigolOscilloscope::GetSampleRate() lock_guard lock(m_cacheMutex); m_srate = (uint64_t)rate; m_srateValid = true; - LogVerbose("smaplerate updated, m_srate %" PRIu64 "\n", m_srate); + LogTrace("smaplerate updated, m_srate %" PRIu64 "\n", m_srate); return rate; } } @@ -1983,7 +2008,7 @@ uint64_t RigolOscilloscope::GetSampleDepth() if(m_mdepthValid) return m_mdepth; - LogVerbose("mem depth updating, m_mdepth %" PRIu64 "\n", m_mdepth); + LogTrace("mem depth updating, m_mdepth %" PRIu64 "\n", m_mdepth); } auto ret = Trim(m_transport->SendCommandQueuedWithReply(":ACQ:MDEP?")); @@ -1996,7 +2021,7 @@ uint64_t RigolOscilloscope::GetSampleDepth() lock_guard lock(m_cacheMutex); m_mdepth = (uint64_t)depth; m_mdepthValid = true; - LogVerbose("mem depth updated, m_mdepth %" PRIu64 "\n", m_mdepth); + LogTrace("mem depth updated, m_mdepth %" PRIu64 "\n", m_mdepth); return m_mdepth; } } From 4462d808408a89d42e93efb540567761edb722d6 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 15 Nov 2025 22:57:37 +0100 Subject: [PATCH 10/59] RigolOscilloscope: fix `GetChannelBandwidthLimiters` for DHO series There are DHO4000 series scopes, which support 250 MHZ BW limit. --- scopehal/RigolOscilloscope.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index bddb6d8d..cda67586 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -931,12 +931,30 @@ vector RigolOscilloscope::GetChannelBandwidthLimiters(size_t /*i*/ } break; - case Series::DS1000: - case Series::DS1000Z: case Series::DHO800: case Series::DHO900: case Series::DHO1000: case Series::DHO4000: + switch(m_bandwidth) + { + + case 70: + case 100: + case 200: + // TODO: 20 MHZ BW limit is forced when scale is below 200 uV/div (DHO4000 user manual) + return {20, 0}; + case 400: + case 800: + // DHO4404/DHO480420 MHz, 250 MHz + // TODO: 250 MHZ BW limit is forced when scale is below 500 uV/div (DHO4000 user manual) + return {20, 250, 0}; + default: + LogError("Invalid model bandwidth\n"); + } + break; + + case Series::DS1000: + case Series::DS1000Z: return {20, 0}; case Series::UNKNOWN: From 6859667a32250865d096c95c5030669367e012da Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sun, 16 Nov 2025 00:07:41 +0100 Subject: [PATCH 11/59] RigolOscilloscope: simplify channel bw limit handling - all the series/model -> limiters logic is now in `GetChannelBandwidthLimiters` - `SetChannelBandwidthLimit` uses values from `GetChannelBandwidthLimiters` and uses nearest higher limit --- scopehal/RigolOscilloscope.cpp | 113 ++++++++++----------------------- 1 file changed, 35 insertions(+), 78 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index cda67586..9e1786ae 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -912,6 +912,7 @@ void RigolOscilloscope::SetChannelAttenuation(size_t i, double atten) } } +// Our requirements: has to be ordered, zero (full BW) position is not important vector RigolOscilloscope::GetChannelBandwidthLimiters(size_t /*i*/) { switch(m_series) @@ -974,99 +975,55 @@ unsigned int RigolOscilloscope::GetChannelBandwidthLimit(size_t i) auto reply = Trim(m_transport->SendCommandQueuedWithReply(m_channels[i]->GetHwname() + ":BWL?")); + unsigned int limit {}; + sscanf(reply.c_str(), "%uM", &limit); + // parsing will fail when reply is `OFF` and result in default value 0, which means full BW + { lock_guard lock2(m_cacheMutex); - if(reply == "20M") - m_channelBandwidthLimits[i] = 20; - if(reply == "100M") - m_channelBandwidthLimits[i] = 100; - if(reply == "200M") - m_channelBandwidthLimits[i] = 200; - else - m_channelBandwidthLimits[i] = m_bandwidth; - return m_channelBandwidthLimits[i]; + m_channelBandwidthLimits[i] = limit; } + LogTrace("Channel %zd, current BW limit: %u MHZ", i, limit); + return limit; } void RigolOscilloscope::SetChannelBandwidthLimit(size_t i, unsigned int limit_mhz) { - switch(m_series) { - case Series::MSO5000: - case Series::DHO1000: - case Series::DHO4000: - case Series::DHO800: - case Series::DHO900: + auto const available_limits = GetChannelBandwidthLimiters(i); - switch(m_bandwidth) - { - case 70: - case 100: - case 125: - if((limit_mhz <= 20) & (limit_mhz != 0)) - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 20M"); - else - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL OFF"); - break; - - case 200: - case 250: - if((limit_mhz <= 20) & (limit_mhz != 0)) - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 20M"); - else if((limit_mhz <= 100) & (limit_mhz != 0)) - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 100M"); - else - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL OFF"); - break; + // `available_limits` is vector of increasing limits (with 0 at the end representing full BW) + // Search for closest limit above `limit_mhz`, fall back to full rangeBW + auto const new_limit = [&]() -> unsigned int { + if (limit_mhz == 0) + return 0; - case 350: - case 400: - case 800: - if((limit_mhz <= 20) & (limit_mhz != 0)) - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 20M"); - else if((limit_mhz <= 100) & (limit_mhz != 0)) - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 100M"); - else if((limit_mhz <= 200) & (limit_mhz != 0)) - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 200M"); - else - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL OFF"); - break; + for (auto const limit : available_limits) // yeah, we also iterate over 0 (Full BW) at the end of the vector + { + if (limit_mhz <= limit) + return limit; + } + // fallback to 0 which means full BW + return 0; + }(); - default: - LogError("Invalid model number\n"); - return; - } - break; - - case Series::DS1000: - case Series::DS1000Z: - { - if((limit_mhz <= 20) & (limit_mhz != 0)) - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL 20M"); - else - m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL OFF"); - break; - } + // `new_limit` now holds value of limit suported by the device or 0 in case of full BW - case Series::UNKNOWN: - LogError("RigolOscilloscope: unknown model, invalid state!\n"); - return; + if (new_limit > 0) + m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL " + to_string(new_limit) + "M"); + else + m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL OFF"); - } + if (limit_mhz == 0) + LogTrace("requested channel %zd, set no limit\n", i); + else if (limit_mhz != new_limit) + LogWarning("RigolOscilloscope: requested channel %zd, BW limit %d MHZ, set %d MHz\n", i, limit_mhz, new_limit); + else + LogTrace("requested channel %zd, set BW limit %d MHZ\n", i, new_limit); { + //TODO: shouldn't we just rather invalidate cached value in `m_channelBandwidthLimits` and get a value by readback? lock_guard lock2(m_cacheMutex); - if(limit_mhz == 0) - m_channelBandwidthLimits[i] = m_bandwidth; // max - else if(limit_mhz <= 20) - m_channelBandwidthLimits[i] = 20; - else if(m_bandwidth == 70) - m_channelBandwidthLimits[i] = 70; - else if((limit_mhz <= 100) | (m_bandwidth == 100)) - m_channelBandwidthLimits[i] = 100; - else if((limit_mhz <= 200) | (m_bandwidth == 200)) - m_channelBandwidthLimits[i] = 200; - else - m_channelBandwidthLimits[i] = m_bandwidth; // 350 MHz + m_channelBandwidthLimits[i] = new_limit; } } From 88479912f07d2668e4456afa9064e697e985d0f5 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Mon, 17 Nov 2025 22:29:25 +0100 Subject: [PATCH 12/59] RigolOscilloscope: fix DS1000Z samplerate/timebase conversion rules it is more complicated than expected, no obvious rules --- scopehal/RigolOscilloscope.cpp | 79 +++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 9e1786ae..89fe2e4b 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -1785,33 +1785,35 @@ struct Ds1000zSrate { }; static std::vector ds1000zSampleRates { - { 20, 1 }, // 12k - { 50, 1 }, // 12k - { 100, 1 }, // 12k - { 200, 1 | 2 }, // 12k 120k - { 500, 1 | 2 }, // 12k 120k - { 1000, 1 | 2 }, // 12k 120k - { 2000, 1 | 2 | 4 }, // 12k 120k 1M2 - { 5000, 1 | 2 | 4 }, // 12k 120k 1M2 - { 10 * 1000, 1 | 2 | 4 }, // 12k 120k 1M2 - { 20 * 1000, 1 | 2 | 4 }, // 12k 120k 1M2 12M - { 40 * 1000, 16}, // 24M - { 50 * 1000, 1 | 2 | 4 | 8 }, // 12k 120k 1M2 12M - { 100 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M - { 200 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M - { 400 * 1000, 16}, // 24M - { 500 * 1000, 1 | 2 | 4 | 8 }, // 12k 120k 1M2 12M - { 1 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M - { 2 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M - { 4 * 1000 * 1000, 16}, // 24M - { 5 * 1000 * 1000, 1 | 2 | 4 | 8 }, // 12k 120k 1M2 12M - { 10 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M - { 25 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M - { 50 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M - { 125 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M - { 250 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M - { 500 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M - {1000 * 1000 * 1000, 1 | 2 | 4 | 8 | 16} // 12k 120k 1M2 12M 24M + // 1 CH sr 1CH mDepths 2 CH sr 3/4 CH sr display halt of memory? + { 20, 1 }, // 12k 10 5 N + { 50, 1 }, // 12k 25 12 ! N this special case looks to be caused by integer representation of the samplerate + { 100, 1 }, // 12k 50 25 N + { 200, 1 | 2 }, // 12k 120k 100 50 N + { 500, 1 | 2 }, // 12k 120k 250 125 N + { 1000, 1 | 2 }, // 12k 120k 500 250 N + { 2000, 1 | 2 | 4 }, // 12k 120k 1M2 1k 500 N + { 5000, 1 | 2 | 4 }, // 12k 120k 1M2 2k5 1k25 N + { 10 * 1000, 1 | 2 | 4 }, // 12k 120k 1M2 5k 2k5 N + { 20 * 1000, 1 | 2 | 4 }, // 12k 120k 1M2 12M 10k 5k N + { 40 * 1000, 16}, // 24M 20k 10k N + { 50 * 1000, 1 | 2 | 4 | 8 }, // 12k 120k 1M2 12M 25k 12k5 N + { 100 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M 50k 25k N + { 200 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M 100k 50k N + { 400 * 1000, 16}, // 24M 200k 100k N + { 500 * 1000, 1 | 2 | 4 | 8 }, // 12k 120k 1M2 12M 250k 125k N + { 1 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M 500k 250k N + { 2 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M 1M 500k N + { 4 * 1000 * 1000, 16}, // 24M 2M 1M N + { 5 * 1000 * 1000, 1 | 2 | 4 | 8 }, // 12k 120k 1M2 12M 2M5 1M25 N + { 10 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M 5M 2M5 N + // 10 Msps is magical threshold above which the regularity breaks + { 25 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M 10M ! 5M ! Y + { 50 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M 25M 12M5 Y + { 125 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M 50M ! 25M ! Y + { 250 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M 125M 50M ! Y + { 500 * 1000 * 1000, 1 | 2 | 4 | 8 | 16}, // 12k 120k 1M2 12M 24M 250M 125M Y + {1000 * 1000 * 1000, 1 | 2 | 4 | 8 | 16} // 12k 120k 1M2 12M 24M 500M 250M Y }; vector RigolOscilloscope::GetSampleRatesNonInterleaved() @@ -1828,7 +1830,19 @@ vector RigolOscilloscope::GetSampleRatesNonInterleaved() auto mdepth = GetSampleDepth(); for (const auto& rate : ds1000zSampleRates) if (rate.supportsMdepth(mdepth, divisor)) - rates.push_back(rate.m_value / divisor); + { + // 125M is never divided by 2 + // 125M /2 -> 50M /2 -> 25M + // 250M /2 -> 125M /2 -> 50M + if (rate.m_value == 125'000'000 and divisor != 1) + rates.push_back(100'000'000 / divisor); + else if (rate.m_value == 25'000'000 and divisor != 1) + rates.push_back(20'000'000 / divisor); + else if (rate.m_value == 250'000'000 and divisor == 4) + rates.push_back(200'000'000 / divisor); + else + rates.push_back(rate.m_value / divisor); + } return rates; } @@ -2182,7 +2196,14 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) // Mdepth = Srate * wlength // Mdepth = Srate * Tscale * 12 // Mdepth / (Srate * 12) = * Tscale - m_transport->SendCommandQueued(string(":TIM:SCAL ") + to_string(sampletime / 12 / 2)); // not sure why we need that / 2 + auto const divisor = GetChannelDivisor(); + LogTrace("setting target samplerate %lu, divisor %zu\n", rate, divisor); + auto const timescale = [&]() -> float { + if (divisor != 4 and (rate / divisor) >= 25'000'000) + return sampletime / 12 / 2; + return sampletime / 12; + }(); + m_transport->SendCommandQueued(string(":TIM:SCAL ") + to_string(timescale)); // Due to unknown reason, when we read SCAL right fter changing, we get the old value // solution is to execute _any_ command with response (e.g. *IDN? works). // Another requirement is that this read has to happen after the acquisition finishes which could take a long time on samplerates From 693f80b1dbf684be3ce279c7ab9cf74254882146 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Mon, 17 Nov 2025 23:07:06 +0100 Subject: [PATCH 13/59] RigolOscilloscope: DS1000Z: allow 24M point memory also for multiple channels --- scopehal/RigolOscilloscope.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 89fe2e4b..e9e6d22e 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -575,7 +575,7 @@ void RigolOscilloscope::UpdateDynamicCapabilities() { vector depths; for (auto &depth : ds1000zSampleDepths) { - if (depth == 24'000'000 and (not m_opt24M or divisor != 1)) + if (depth == 24'000'000 and (not m_opt24M)) continue; depths.emplace_back(depth/divisor); From 7b317b8500cc81a6abfe5ffc10838166c4cab0ba Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Mon, 17 Nov 2025 23:12:18 +0100 Subject: [PATCH 14/59] RigolOscilloscope: deduplicate pre/post start code --- scopehal/RigolOscilloscope.cpp | 37 +++++++++++++++++++++------------- scopehal/RigolOscilloscope.h | 2 ++ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index e9e6d22e..b2ed33e3 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -1565,8 +1565,10 @@ bool RigolOscilloscope::AcquireData() return true; } -void RigolOscilloscope::PrepareStart() -{ +void RigolOscilloscope::StartPre() +{ + m_liveMode = false; + m_mdepthValid = false; // Memory depth might have been changed on scope switch (m_series) { case Series::DHO1000: @@ -1594,10 +1596,19 @@ void RigolOscilloscope::PrepareStart() } } +void RigolOscilloscope::StartPost() +{ + m_triggerArmed = true; + +} + + void RigolOscilloscope::Start() { - LogTrace("Start\n"); //TODO: consider locking transport as it should not get interrupted by something else (or can it?) + LogTrace("Start\n"); + StartPre(); + m_triggerOneShot = false; switch (m_series) { case Series::DS1000: @@ -1627,7 +1638,6 @@ void RigolOscilloscope::Start() { m_liveMode = false; } - PrepareStart(); m_transport->SendCommandQueued(m_liveMode ? ":RUN" : ":SING"); m_transport->SendCommandQueued("*WAI"); break; @@ -1635,17 +1645,15 @@ void RigolOscilloscope::Start() case Series::UNKNOWN: break; } - - m_triggerArmed = true; - m_triggerOneShot = false; + StartPost(); } void RigolOscilloscope::StartSingleTrigger() { LogTrace("Start single trigger\n"); - m_liveMode = false; - m_mdepthValid = false; // Memory depth might have been changed on scope - PrepareStart(); + + StartPre(); + m_triggerOneShot = true; switch (m_series) { case Series::DS1000: @@ -1666,13 +1674,12 @@ void RigolOscilloscope::StartSingleTrigger() case Series::UNKNOWN: break; } - - m_triggerArmed = true; - m_triggerOneShot = true; + StartPost(); } void RigolOscilloscope::Stop() { + LogTrace("Explicit STOP requested"); m_transport->SendCommandQueued(":STOP"); m_liveMode = false; m_triggerArmed = false; @@ -1683,7 +1690,8 @@ void RigolOscilloscope::ForceTrigger() { m_liveMode = false; m_mdepthValid = false; // Memory depth might have been changed on scope - PrepareStart(); + m_triggerOneShot = true; + StartPre(); switch (m_series) { case Series::DS1000Z: @@ -1700,6 +1708,7 @@ void RigolOscilloscope::ForceTrigger() LogError("RigolOscilloscope::ForceTrigger not implemented for this model\n"); break; } + StartPost(); } bool RigolOscilloscope::IsTriggerArmed() diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index a63e2caf..7aa01de9 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -130,6 +130,8 @@ class RigolOscilloscope : public virtual SCPIOscilloscope // NOTE: If the MATH channel is selected, only the NORMal mode is valid. }; + void StartPre(); + void StartPost(); OscilloscopeChannel* m_extTrigChannel; //hardware analog channel count, independent of LA option etc From 37251c1057e15b1e989778691a24f3c21e8d73bd Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Mon, 17 Nov 2025 23:20:55 +0100 Subject: [PATCH 15/59] RigolOscilloscope: DS1000Z: improve trigger handling - deduce whole trigger state from data preamble rather than status - resilient against fast triggers skipping transitions - properly handles busy(wait)/armed(run) states --- scopehal/RigolOscilloscope.cpp | 76 ++++++++++++++++++++++++++++------ scopehal/RigolOscilloscope.h | 41 +++++++++--------- 2 files changed, 86 insertions(+), 31 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index b2ed33e3..a800acf2 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -605,14 +605,14 @@ std::size_t RigolOscilloscope::GetChannelDivisor() { std::optional RigolOscilloscope::GetCapturePreamble() { //This is basically the same function as a LeCroy WAVEDESC, but much less detailed auto reply = Trim(m_transport->SendCommandQueuedWithReply("WAV:PRE?")); - LogDebug("Preamble = %s\n", reply.c_str()); + // LogDebug("Preamble = %s\n", reply.c_str()); CapturePreamble preamble {}; int format; int type; auto parsed_length = sscanf(reply.c_str(), - "%d,%d,%zu,%zu,%lf,%lf,%lf,%lf,%lf,%lf", + "%d,%d,%" PRIuLEAST32 ",%" PRIuLEAST32 ",%lf,%lf,%lf,%lf,%lf,%lf", // is there a way of getting rid of reinterpret_cast without sacrificing typed enums and without helper variables? &format, &type, @@ -645,8 +645,8 @@ std::optional RigolOscilloscope::GetCaptureP // the other option was pointer reinterpret cast :/ preamble.format = CaptureFormat(format); preamble.type = CaptureType(type); - LogDebug("X: %ld points, %f origin, ref %f time/sample %lf\n", preamble.npoints, preamble.xorigin, preamble.xreference, preamble.sec_per_sample); - LogDebug("Y: %f inc, %f origin, %f ref\n", preamble.yincrement, preamble.yorigin, preamble.yreference); + LogTrace("X: %" PRIuLEAST32 " points, %f origin, ref %f time/sample %lf\n", preamble.npoints, preamble.xorigin, preamble.xreference, preamble.sec_per_sample); + LogTrace("Y: %f inc, %f origin, %f ref\n", preamble.yincrement, preamble.yorigin, preamble.yreference); return preamble; } @@ -1152,28 +1152,66 @@ Oscilloscope::TriggerMode RigolOscilloscope::PollTrigger() if(m_liveMode) return TRIGGER_MODE_TRIGGERED; + LogTrace("m_triggerArmed %d, m_triggerWasLive %d\n", m_triggerArmed, m_triggerWasLive); + if (m_series == Series::DS1000Z) { // DS1000Z report trigger status in unreliable way. // When triggered, it reports STOP for some time. // Then it goes though RUN->WAIT->TRIG and ends up in STOP, - // but sometimes it completely skips transition to other states and + // but sometimes it completely skips transition to other states (not quick enough poll) and // remains in STOP mode for a whole time even though there are new data. // As a workaround, we monitor output WAV:PREamble which also report sample count. // Once it matches configured memory depth, we consider capture to be complete. // This is also much faster than waiting for trigger states. - // TODO: check if Protocol::MSO5000 procool could use this polling method + // TODO: check if other series could use this method // TODO: consider to invalidate cached depth on trigger command - const auto sampleDepth = GetSampleDepth(); - if (sampleDepth) { + if (m_triggerArmed) + { + const auto sampleDepth = GetSampleDepth(); const auto preamble = GetCapturePreamble(); - if (preamble.has_value() && preamble->npoints == sampleDepth) { - m_triggerArmed = false; + if (sampleDepth and preamble.has_value()) + { + if (preamble->npoints == sampleDepth) { + // reached the target sample count + m_triggerArmed = false; + m_triggerWasLive = false; + return TRIGGER_MODE_TRIGGERED; + } + + if (m_triggerWasLive) // was live and sampling not finished + return TRIGGER_MODE_RUN; + + // was not live and no points were sampled yet or stale value from last acq + // acq have not started yet + if (preamble->npoints == 0 or preamble->npoints == m_pointsWhenStarted) + return TRIGGER_MODE_WAIT; + + // wa not live, but something was sampled -> switch to live + m_triggerWasLive = true; + return TRIGGER_MODE_RUN; + } + else + { + if (not sampleDepth) + LogError("failed to get sample depth\n"); + else if (not preamble) + LogError("failed to get preamble\n"); + } + } + else + { + if (m_triggerWasLive) + { + // after manually stopped acquisition + LogTrace("Last poll after manually stopped partial acquisition\n"); m_triggerWasLive = false; - return TRIGGER_MODE_TRIGGERED; + // return TRIGGER_MODE_ + // TRIGGERED; // returning triggered could result in (partial) data download + return TRIGGER_MODE_STOP; } + return TRIGGER_MODE_STOP; } - } auto stat = Trim(m_transport->SendCommandQueuedWithReply(":TRIG:STAT?")); @@ -1195,6 +1233,8 @@ Oscilloscope::TriggerMode RigolOscilloscope::PollTrigger() //The scope will go from "run" to "stop" state on trigger with only a momentary pass through "TD". //If we armed the trigger recently and we're now stopped, this means we must have triggered. // DS1000 QUIRK + + // It also takes sime time before the scope transitiona _from_ STOP to any other state if(m_triggerArmed && (m_series != Series::DS1000 || m_triggerWasLive)) { m_triggerArmed = false; @@ -1217,6 +1257,8 @@ bool RigolOscilloscope::AcquireData() // Notify about download operation start ChannelsDownloadStarted(); + m_triggerWasLive = false; // premature stop may have resulted in `m_triggerWasLive` staying true (did not reach triggered state) + // Rigol scopes do not have a capture time so we fake it double now = GetTime(); @@ -1600,6 +1642,16 @@ void RigolOscilloscope::StartPost() { m_triggerArmed = true; + { + auto preamble = GetCapturePreamble(); + if (preamble.has_value()) + { + m_pointsWhenStarted = preamble->npoints; + LogTrace("set m_pointsWhenStarted to %" PRIuLEAST32 "\n", m_pointsWhenStarted); + } + else + LogError("empty preable"); + } } diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index 7aa01de9..2bdd881c 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -130,8 +130,29 @@ class RigolOscilloscope : public virtual SCPIOscilloscope // NOTE: If the MATH channel is selected, only the NORMal mode is valid. }; + + struct CapturePreamble { + CaptureFormat format; + CaptureType type; + std::uint_least32_t npoints; // an integer between 1 and 12000000. + std::uint_least32_t averages; // the number of averages in the average sample mode and 1 in other modes. + double sec_per_sample; // the time difference between two neighboring points in the X direction. + double xorigin; // the time from the trigger point to the "Reference Time" in the X direction. + double xreference; // the reference time of the data point in the X direction. + double yincrement; // the waveform increment in the Y direction. + double yorigin; // the vertical offset relative to the "Vertical Reference Position" in the Y direction. + double yreference; // the vertical reference position in the Y direction. + }; + + std::optional GetCapturePreamble(); void StartPre(); void StartPost(); + void DecodeDeviceSeries(); + void AnalyzeDeviceCapabilities(); + void UpdateDynamicCapabilities(); // capabilities dependent on enabled chanel count + std::size_t GetChannelDivisor(); // helper function to get memory depth/sample rate divisor base on current scope state (amount of enabled channels) + +protected: OscilloscopeChannel* m_extTrigChannel; //hardware analog channel count, independent of LA option etc @@ -154,6 +175,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope bool m_triggerArmed; bool m_triggerWasLive; bool m_triggerOneShot; + std::uint_least32_t m_pointsWhenStarted; // used for some series as a part of trigger state detection workaround, sampled points reported right after arming bool m_liveMode; @@ -178,27 +200,8 @@ class RigolOscilloscope : public virtual SCPIOscilloscope void PushEdgeTrigger(EdgeTrigger* trig); void PullEdgeTrigger(); - struct CapturePreamble { - CaptureFormat format; - CaptureType type; - std::size_t npoints; // an integer between 1 and 12000000. - std::size_t averages; // the number of averages in the average sample mode and 1 in other modes. - double sec_per_sample; // the time difference between two neighboring points in the X direction. - double xorigin; // the time from the trigger point to the "Reference Time" in the X direction. - double xreference; // the reference time of the data point in the X direction. - double yincrement; // the waveform increment in the Y direction. - double yorigin; // the vertical offset relative to the "Vertical Reference Position" in the Y direction. - double yreference; // the vertical reference position in the Y direction. - }; std::vector m_depths; - std::optional GetCapturePreamble(); - void PrepareStart(); - void DecodeDeviceSeries(); - void AnalyzeDeviceCapabilities(); - void UpdateDynamicCapabilities(); // capabilities dependent on enabled chanel count - std::size_t GetChannelDivisor(); // helper function to get memory depth/sample rate divisor base on current scope state (amount of enabled channels) - public: static std::string GetDriverNameInternal(); OSCILLOSCOPE_INITPROC(RigolOscilloscope) From ddfe89e13e5ea137fd1eb1991b497508f416a23d Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Mon, 17 Nov 2025 23:22:35 +0100 Subject: [PATCH 16/59] RigolOscilloscope: minor cleanups --- scopehal/RigolOscilloscope.cpp | 1 - scopehal/RigolOscilloscope.h | 12 +++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index a800acf2..9d128e3f 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -2313,7 +2313,6 @@ void RigolOscilloscope::SetTriggerOffset(int64_t offset) int64_t RigolOscilloscope::GetTriggerOffset() { - //Early out if the value is in cache { lock_guard lock(m_cacheMutex); if(m_triggerOffsetValid) diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index 2bdd881c..a843803e 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -98,6 +98,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope void ForceHDMode(bool mode); protected: + // private/internal types and functions enum class Series { UNKNOWN, @@ -155,16 +156,18 @@ class RigolOscilloscope : public virtual SCPIOscilloscope protected: OscilloscopeChannel* m_extTrigChannel; - //hardware analog channel count, independent of LA option etc + // hardware analog channel count, independent of LA option etc size_t m_analogChannelCount; - //config cache + // config cache, values that can be updated whenever needed + // all access to these shall be exclusive using `m_cacheMutex` std::map m_channelAttenuations; std::map m_channelCouplings; std::map m_channelOffsets; std::map m_channelVoltageRanges; std::map m_channelBandwidthLimits; std::map m_channelsEnabled; + std::vector m_depths; bool m_srateValid; uint64_t m_srate; bool m_mdepthValid; @@ -172,6 +175,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope int64_t m_triggerOffset; bool m_triggerOffsetValid; + // state variables, may alter values during runtime bool m_triggerArmed; bool m_triggerWasLive; bool m_triggerOneShot; @@ -179,6 +183,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope bool m_liveMode; + // constants once the ctor finishes struct Model { std::string prefix; // e.g.: DS unsigned int number; // e.g.: 1054 @@ -192,7 +197,6 @@ class RigolOscilloscope : public virtual SCPIOscilloscope uint64_t m_maxSrate {}; // Maximum Sample rate for DHO models bool m_lowSrate {}; // True for DHO low sample rate models (DHO800/900) Series m_series; - // protocol_version m_protocol; //True if we have >8 bit capture depth bool m_highDefinition; @@ -200,8 +204,6 @@ class RigolOscilloscope : public virtual SCPIOscilloscope void PushEdgeTrigger(EdgeTrigger* trig); void PullEdgeTrigger(); - std::vector m_depths; - public: static std::string GetDriverNameInternal(); OSCILLOSCOPE_INITPROC(RigolOscilloscope) From 96a6f88bd1ef60b1694a4daf26a527ba6cee2701 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Thu, 27 Nov 2025 20:14:00 +0100 Subject: [PATCH 17/59] RigolOscilloscope: change `Series::DS1000Z` enumeration to change Series::MSODS1000Z as there are also MSO scopes --- scopehal/RigolOscilloscope.cpp | 50 +++++++++++++++++----------------- scopehal/RigolOscilloscope.h | 2 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 9d128e3f..91005067 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -117,7 +117,7 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) m_transport->SendCommandQueued(":WAV:POIN:MODE RAW"); break; - case Series::DS1000Z: + case Series::MSODS1000Z: case Series::MSO5000: case Series::DHO1000: case Series::DHO4000: @@ -142,13 +142,13 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":VERN ON"); break; - case Series::DS1000Z: + case Series::MSODS1000Z: case Series::UNKNOWN: break; } switch (m_series) { - case Series::DS1000Z: + case Series::MSODS1000Z: case Series::MSO5000: case Series::DHO1000: case Series::DHO4000: @@ -241,7 +241,7 @@ void RigolOscilloscope::DecodeDeviceSeries() if(m_modelNew.suffix[0] == 'D' || m_modelNew.suffix[0] == 'E') return Series::DS1000; else if(m_modelNew.suffix[0] == 'Z') - return Series::DS1000Z; + return Series::MSODS1000Z; break; // case 2: @@ -286,7 +286,7 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { m_bandwidth = m_modelNew.number % 1000 - m_analogChannelCount; break; - case Series::DS1000Z: + case Series::MSODS1000Z: { m_analogChannelCount = m_modelNew.number % 10; m_bandwidth = m_modelNew.number % 1000 - m_analogChannelCount; @@ -564,7 +564,7 @@ void RigolOscilloscope::UpdateDynamicCapabilities() { return; } - case Series::DS1000Z: + case Series::MSODS1000Z: { // For the analog channel: // ― 1 CH: 12000|120000|1200000|12000000|24000000 @@ -955,7 +955,7 @@ vector RigolOscilloscope::GetChannelBandwidthLimiters(size_t /*i*/ break; case Series::DS1000: - case Series::DS1000Z: + case Series::MSODS1000Z: return {20, 0}; case Series::UNKNOWN: @@ -1037,7 +1037,7 @@ float RigolOscilloscope::GetChannelVoltageRange(size_t i, size_t /*stream*/) string reply; switch (m_series) { - case Series::DS1000Z: + case Series::MSODS1000Z: reply = Trim(m_transport->SendCommandQueuedWithReply(":" + m_channels[i]->GetHwname() + ":RANGE?")); break; @@ -1062,7 +1062,7 @@ float RigolOscilloscope::GetChannelVoltageRange(size_t i, size_t /*stream*/) lock_guard lock(m_cacheMutex); switch (m_series) { - case Series::DS1000Z: + case Series::MSODS1000Z: return m_channelVoltageRanges[i] = range; case Series::DS1000: @@ -1090,7 +1090,7 @@ void RigolOscilloscope::SetChannelVoltageRange(size_t i, size_t /*stream*/, floa } switch (m_series) { - case Series::DS1000Z: + case Series::MSODS1000Z: m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":RANGE " + to_string(range)); return; @@ -1154,7 +1154,7 @@ Oscilloscope::TriggerMode RigolOscilloscope::PollTrigger() LogTrace("m_triggerArmed %d, m_triggerWasLive %d\n", m_triggerArmed, m_triggerWasLive); - if (m_series == Series::DS1000Z) { + if (m_series == Series::MSODS1000Z) { // DS1000Z report trigger status in unreliable way. // When triggered, it reports STOP for some time. // Then it goes though RUN->WAIT->TRIG and ends up in STOP, @@ -1273,7 +1273,7 @@ bool RigolOscilloscope::AcquireData() case Series::DS1000: maxpoints = 8192; // FIXME break; - case Series::DS1000Z: + case Series::MSODS1000Z: // manual specifies 250k as a maximum for bytes output // maxpoints = 250 * 1000; @@ -1333,7 +1333,7 @@ bool RigolOscilloscope::AcquireData() break; } - case Series::DS1000Z: + case Series::MSODS1000Z: case Series::MSO5000: case Series::DHO1000: case Series::DHO4000: @@ -1395,7 +1395,7 @@ bool RigolOscilloscope::AcquireData() m_transport->SendCommandQueued(string(":WAV:DATA? ") + m_channels[channelIdx]->GetHwname()); break; - case Series::DS1000Z: + case Series::MSODS1000Z: { // specify block sample range m_transport->SendCommandQueued(string("WAV:STAR ") + to_string(npoint+1)); //ONE based indexing WTF @@ -1583,7 +1583,7 @@ bool RigolOscilloscope::AcquireData() m_transport->SendCommandQueued(":RUN"); break; - case Series::DS1000Z: + case Series::MSODS1000Z: case Series::MSO5000: case Series::DHO1000: case Series::DHO4000: @@ -1631,7 +1631,7 @@ void RigolOscilloscope::StartPre() break; case Series::DS1000: - case Series::DS1000Z: + case Series::MSODS1000Z: case Series::MSO5000: case Series::UNKNOWN: break; @@ -1668,7 +1668,7 @@ void RigolOscilloscope::Start() m_transport->SendCommandQueued(":RUN"); break; - case Series::DS1000Z: + case Series::MSODS1000Z: case Series::MSO5000: m_transport->SendCommandQueued(":SING"); m_transport->SendCommandQueued("*WAI"); @@ -1713,7 +1713,7 @@ void RigolOscilloscope::StartSingleTrigger() m_transport->SendCommandQueued(":RUN"); break; - case Series::DS1000Z: + case Series::MSODS1000Z: case Series::MSO5000: case Series::DHO1000: case Series::DHO4000: @@ -1746,7 +1746,7 @@ void RigolOscilloscope::ForceTrigger() StartPre(); switch (m_series) { - case Series::DS1000Z: + case Series::MSODS1000Z: case Series::MSO5000: case Series::DHO1000: case Series::DHO4000: @@ -1884,7 +1884,7 @@ vector RigolOscilloscope::GetSampleRatesNonInterleaved() //FIXME switch (m_series) { - case Series::DS1000Z: + case Series::MSODS1000Z: { vector rates {}; auto divisor = GetChannelDivisor(); @@ -1988,7 +1988,7 @@ set RigolOscilloscope::GetInterleaveConflicts( return {}; case Series::UNKNOWN: case Series::DS1000: - case Series::DS1000Z: + case Series::MSODS1000Z: case Series::MSO5000: break; } @@ -2015,7 +2015,7 @@ vector RigolOscilloscope::GetSampleDepthsInterleaved() return GetSampleDepthsNonInterleaved(); case Series::DS1000: - case Series::DS1000Z: + case Series::MSODS1000Z: case Series::MSO5000: case Series::UNKNOWN: break; @@ -2177,7 +2177,7 @@ void RigolOscilloscope::SetSampleDepth(uint64_t depth) break; } - case Series::DS1000Z: + case Series::MSODS1000Z: { if (depth == 24'000'000 and not m_opt24M) { LogError("This DS1000Z device does not have 24M option installed\n"); @@ -2246,7 +2246,7 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) break; } - case Series::DS1000Z: + case Series::MSODS1000Z: { // The following equation describes the relationship among memory depth, sample rate, and waveform length: // Memory Depth = Sample Rate x Waveform Length @@ -2485,7 +2485,7 @@ void RigolOscilloscope::ForceHDMode(bool mode) case Series::UNKNOWN: case Series::DS1000: - case Series::DS1000Z: + case Series::MSODS1000Z: case Series::MSO5000: //TODO: report/log invalidity of this break; diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index a843803e..2d9360f0 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -104,7 +104,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope UNKNOWN, DS1000, - DS1000Z, + MSODS1000Z, // MSODS2000, // MSO4000, MSO5000, From f6e55b2f777ff8d18954f6ce88e0d451056a125b Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Thu, 27 Nov 2025 20:45:33 +0100 Subject: [PATCH 18/59] RigolOscilloscope: enable channel VERNier for all series MSODS1000Z was the only exception and it works there. --- scopehal/RigolOscilloscope.cpp | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 91005067..098c068e 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -130,22 +130,9 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) case Series::UNKNOWN: break; } - - switch (m_series) { - case Series::DS1000: - case Series::MSO5000: - case Series::DHO1000: - case Series::DHO4000: - case Series::DHO800: - case Series::DHO900: - for(size_t i = 0; i < m_analogChannelCount; i++) - m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":VERN ON"); - break; - - case Series::MSODS1000Z: - case Series::UNKNOWN: - break; - } + + for(size_t i = 0; i < m_analogChannelCount; i++) + m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":VERN ON"); switch (m_series) { case Series::MSODS1000Z: From ebd3d310e08faaf66f88495aecc06ade78c116d0 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Thu, 27 Nov 2025 20:47:35 +0100 Subject: [PATCH 19/59] RigolOscilloscope: fix serires detection - recognize MSO1000Z as MSODS1000Z (tested) - recognize DHO800/DHO900/DHO1000/DHO4000 (untested) --- scopehal/RigolOscilloscope.cpp | 65 +++++++++++++++++----------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 098c068e..c93e162a 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -217,46 +217,45 @@ void RigolOscilloscope::DecodeDeviceSeries() m_modelNew.suffix.assign(cursor, m_model.end()); // decode into device family + if(m_modelNew.prefix == "DS" or m_modelNew.prefix == "MSO") { switch(m_modelNew.number / 1000) { case 1: - if(strcmp(m_modelNew.prefix.c_str(), "DS") != 0) - break; - if(m_modelNew.suffix.size() < 1) - break; - if(m_modelNew.suffix[0] == 'D' || m_modelNew.suffix[0] == 'E') - return Series::DS1000; - else if(m_modelNew.suffix[0] == 'Z') - return Series::MSODS1000Z; - break; - - // case 2: - // if(strcmp(m_modelNew.prefix.c_str(), "DS") == 0 || strcmp(m_modelNew.prefix.c_str(), "MSO") == 0) - // return Series::MSODS2000; - // break; - - case 5: - if(strcmp(m_modelNew.prefix.c_str(), "MSO") == 0) - return Series::MSO5000; - break; - - // case 7: - // if(strcmp(m_modelNew.prefix.c_str(), "DS") == 0 || strcmp(m_modelNew.prefix.c_str(), "MSO") == 0) - // return Series::MSODS7000; - // break; - - // case 8: - // if(strcmp(m_modelNew.prefix.c_str(), "MSO") == 0) - // return Series::MSO8000; - // break; - - default: + if(m_modelNew.suffix.size() < 1) + break; + if(m_modelNew.suffix[0] == 'D' || m_modelNew.suffix[0] == 'E') + return Series::DS1000; + else if(m_modelNew.suffix[0] == 'Z') + return Series::MSODS1000Z; break; + case 5: return Series::MSO5000; + default: break; } - LogError("model %s was not recognized\n", m_model.c_str()); - return Series::UNKNOWN; } + else if (m_modelNew.prefix == "DHO") + { + if (m_modelNew.number < 1000) + { + switch (m_modelNew.number / 100) + { + case 8: return Series::DHO800; + case 9: return Series::DHO900; + default: break; + } + } + else + { + switch (m_modelNew.number / 1000) + { + case 1: return Series::DHO1000; + case 4: return Series::DHO4000; + default: break; + } + } + } + LogError("model %s was not recognized\n", m_model.c_str()); + return Series::UNKNOWN; }(); } From cff07fb197e5ae0b63afc497b3f07f9e951f4df6 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 29 Nov 2025 21:32:59 +0100 Subject: [PATCH 20/59] RigolOscilloscope: add response parsing hekpers only `double` for now, but templating allows easy addition of more --- scopehal/RigolOscilloscope.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index c93e162a..0ef412d5 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -48,6 +48,19 @@ using namespace std; +template +static T parseNumeric(string const &input, const string &fmt, T const &fallback_value = {}) { + T output {fallback_value}; + sscanf(input.c_str(), fmt.c_str(), &output); + return output; +} + +static auto parseDouble(string const &input, const string &fmt = "%lf", double const &fallback_value = numeric_limits::quiet_NaN()) +{ + return parseNumeric(input, fmt, fallback_value); +} + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //Construction / destruction From 5341b56a2f09887779801f1f98258dd3b15ed789 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 29 Nov 2025 21:34:38 +0100 Subject: [PATCH 21/59] RigolOscilloscope: add RAII "End Of Context" helper to make resource cleanups easier --- scopehal/RigolOscilloscope.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 0ef412d5..fb0b9e42 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -60,6 +60,15 @@ static auto parseDouble(string const &input, const string &fmt = "%lf", double c return parseNumeric(input, fmt, fallback_value); } +//TODO: this would make sens to move to some utility library +template +struct CallOnEOC { + T onExit; + CallOnEOC(T onExit_) : onExit(onExit_) {} + ~CallOnEOC() { + onExit(); + } +}; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //Construction / destruction From 920714a9baf98e6712f97bd0622e8bf2ab8a12a8 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 29 Nov 2025 21:36:35 +0100 Subject: [PATCH 22/59] RigolOscilloscope: move initial `UpdateDynamicCapabilities` after channel configuration - it may touch channels and it caused bugs when adding LA support --- scopehal/RigolOscilloscope.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index fb0b9e42..fa3cef2c 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -92,7 +92,6 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) LogTrace("RigolOscilloscope: series: %d\n", int(m_series)); AnalyzeDeviceCapabilities(); - UpdateDynamicCapabilities(); for(auto i = 0U; i < m_analogChannelCount; i++) { @@ -172,7 +171,8 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) } FlushConfigCache(); - + + UpdateDynamicCapabilities(); //make sure all setup commands finish before we proceed m_transport->FlushCommandQueue(); } From ece27e0ac1be13b3b141e57410d6aa03cdb6facf Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sun, 30 Nov 2025 11:56:34 +0100 Subject: [PATCH 23/59] RigolOscilloscope: reduce includes most of necessary includes are already included indirectly --- scopehal/RigolOscilloscope.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index fa3cef2c..f68bc617 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -32,9 +32,6 @@ #include "EdgeTrigger.h" #include -#include -#include -#include #ifdef _WIN32 #include From 311aa77c9270e8ea664cf617154d3263445739d7 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sun, 30 Nov 2025 12:46:29 +0100 Subject: [PATCH 24/59] RigolOscilloscope: add support for LA - developed with MSO1000Z series - MSO5000/DHO900 implemented, but not tested --- scopehal/RigolOscilloscope.cpp | 809 ++++++++++++++++++++++++++++++--- scopehal/RigolOscilloscope.h | 26 ++ 2 files changed, 766 insertions(+), 69 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index f68bc617..bed3c9a6 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -123,6 +123,28 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) chan->SetDefaultDisplayName(); } + if (m_digitalChannelCount != 0) + { + m_digitalBanks.push_back({}); + for(auto i = 0U; i < m_digitalChannelCount; i++) + { + //Hardware name of the channel + string chname = string("D") + to_string(i); + + //Color the channels based on Rigol's standard color sequence (yellow-cyan-red-blue) + string color = "#66ff00"; + + //Create the channel + auto chan = new OscilloscopeChannel( + this, chname, color, Unit(Unit::UNIT_FS), Unit(Unit::UNIT_VOLTS), Stream::STREAM_TYPE_DIGITAL, m_analogChannelCount + i); + m_channels.push_back(chan); + if (m_digitalBanks.back().size() == DIGITAL_BANK_SIZE) + m_digitalBanks.push_back({}); + m_digitalBanks.back().push_back(chan); + chan->SetDefaultDisplayName(); + } + } + //Add the external trigger input m_extTrigChannel = new OscilloscopeChannel( this, "EX", "", Unit(Unit::UNIT_FS), Unit(Unit::UNIT_VOLTS), Stream::STREAM_TYPE_TRIGGER, m_channels.size()); @@ -296,10 +318,13 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { m_analogChannelCount = m_modelNew.number % 10; m_bandwidth = m_modelNew.number % 1000 - m_analogChannelCount; + m_digitalChannelCount = m_modelNew.prefix == "MSO" ? 16 : 0; + { // Probe 24M memory depth option. // Hacky workaround since DS1000Z does not have a way how to query installed options // Only enable chan 1 + // TODO: what about LA channels on MSOs? m_transport->SendCommandQueued("CHAN1:DISP 1"); m_transport->SendCommandQueued("CHAN2:DISP 0"); if(m_analogChannelCount > 2) @@ -329,6 +354,7 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { case Series::MSO5000: { m_analogChannelCount = m_modelNew.number % 10; + m_digitalChannelCount = 16; // Hacky workaround since :SYST:OPT:STAT doesn't work properly on some scopes // Only enable chan 1 @@ -390,6 +416,7 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { { // - DHO802 (70MHz), DHO804 (70Mhz), DHO812 (100MHz),DHO814 (100MHz) m_analogChannelCount = m_modelNew.number % 10; + m_digitalChannelCount = 0; m_bandwidth = m_modelNew.number % 100 / 10 * 100; // 814 -> 14 -> 1 -> 100 if(m_bandwidth == 0) m_bandwidth = 70; // Fallback for DHO80x models @@ -406,6 +433,7 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { { // - DHO914/DHO914S (125MHz), DHO924/DHO924S (250MHz) m_analogChannelCount = m_modelNew.number % 10; + m_digitalChannelCount = 16; m_bandwidth = m_modelNew.number % 100 / 10 * 125; // 914 -> 24 -> 2 -> 250 m_highDefinition = true; @@ -421,6 +449,7 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { { // - DHO1072 (70MHz), DHO1074 (70MHz), DHO1102 (100MHz), DHO1104 (100MHz), DHO1202 (200MHz), DHO1204 (200MHz) m_analogChannelCount = m_modelNew.number % 10; + m_digitalChannelCount = 0; m_bandwidth = m_modelNew.number % 1000 / 10 * 10; m_highDefinition = true; @@ -455,6 +484,7 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { { // - DHO1072 (70MHz), DHO1074 (70MHz), DHO1102 (100MHz), DHO1104 (100MHz), DHO1202 (200MHz), DHO1204 (200MHz) m_analogChannelCount = m_modelNew.number % 10; + m_digitalChannelCount = 0; m_bandwidth = m_modelNew.number % 1000 / 10 * 10; m_highDefinition = true; @@ -599,7 +629,28 @@ void RigolOscilloscope::UpdateDynamicCapabilities() { } std::size_t RigolOscilloscope::GetChannelDivisor() { - auto divisor = GetEnabledChannelCount(); + auto divisor = GetEnabledAnalogChannelCount(); + + // MSO1000Z: + // any active D0 ~ D7 (POD 1) disables CHAN3 and takes the resources + // any active D8 ~ D15 (POD 2) disables CHAN4 and takes the resources + for (auto d = 0U; d < 8; ++d) + { + if (IsChannelEnabled(m_analogChannelCount + d)) + { + ++divisor; + break; + } + } + for (auto d = 0U; d < 8; ++d) + { + if (IsChannelEnabled(m_analogChannelCount + d + 8)) + { + ++divisor; + break; + } + } + if (divisor <= 0) divisor = 1; else if (divisor >= 3) @@ -607,6 +658,16 @@ std::size_t RigolOscilloscope::GetChannelDivisor() { return divisor; } +bool RigolOscilloscope::IsChannelAnalog(std::size_t i) +{ + return i < m_analogChannelCount; +} + +bool RigolOscilloscope::IsChannelDigital(std::size_t i) +{ + return m_analogChannelCount <= i and i < m_analogChannelCount + m_digitalChannelCount; +} + std::optional RigolOscilloscope::GetCapturePreamble() { //This is basically the same function as a LeCroy WAVEDESC, but much less detailed auto reply = Trim(m_transport->SendCommandQueuedWithReply("WAV:PRE?")); @@ -670,51 +731,301 @@ void RigolOscilloscope::FlushConfigCache() m_channelVoltageRanges.clear(); m_channelsEnabled.clear(); m_channelBandwidthLimits.clear(); + m_bankThresholds.clear(); m_srateValid = false; m_mdepthValid = false; m_triggerOffsetValid = false; + m_laEnabled.reset(); delete m_trigger; m_trigger = NULL; } +std::vector RigolOscilloscope::GetDigitalBanks() +{ + return m_digitalBanks; +} + +Oscilloscope::DigitalBank RigolOscilloscope::GetDigitalBank(std::size_t channel) +{ + for (auto &bank : m_digitalBanks) + for (auto &bankChannel : bank) + if (bankChannel->GetIndex() == channel) + return bank; + return {}; +} + +bool RigolOscilloscope::IsDigitalThresholdConfigurable() +{ + return m_digitalChannelCount > 0; +} + +float RigolOscilloscope::GetDigitalThreshold(size_t channel) +{ + if (not IsChannelDigital(channel)) + return 0; //// should we rathger return NaN? + + auto const bankIdx = (channel - m_analogChannelCount) / DIGITAL_BANK_SIZE; + + { + lock_guard lock(m_cacheMutex); + if(m_bankThresholds.find(bankIdx) != m_bankThresholds.end()) + return m_bankThresholds[bankIdx]; + } + + // pods index values start at 1 + auto const level = parseDouble(m_transport->SendCommandQueuedWithReply(string(":LA:POD") + to_string(bankIdx + 1) + ":THR?")); + + { + lock_guard lock(m_cacheMutex); + m_bankThresholds[bankIdx] = level; + } + + return level; +} + + +void RigolOscilloscope::SetDigitalThreshold(size_t channel, float level) +{ + auto const bankIdx = (channel - m_analogChannelCount) / DIGITAL_BANK_SIZE; + + // pods index values start at 1 + m_transport->SendCommandQueued(string(":LA:POD") + to_string(bankIdx + 1) + ":THR " + to_string(level)); + + { + lock_guard lock(m_cacheMutex); + m_bankThresholds[bankIdx] = level; + } +} + + +size_t RigolOscilloscope::GetEnabledAnalogChannelCount() +{ + auto result = 0U; + for(auto i = 0U; i < m_analogChannelCount; i++) + { + if(IsChannelEnabled(i)) + result++; + } + return result; +} + +void RigolOscilloscope::LogLaNotPresent() +{ + LogError("RigolOscilloscope: LA is not present or not supported (yet) in this device (%s%d%s)", m_modelNew.prefix.c_str(), m_modelNew.number, m_modelNew.suffix.c_str()); +} + +bool RigolOscilloscope::IsLaEnabled() { + { + lock_guard lock(m_cacheMutex); + if(m_laEnabled.has_value()) + return *m_laEnabled; + } + + bool enabled {}; + switch (m_series) + { + case Series::MSODS1000Z: + case Series::MSO5000: + enabled = Trim(m_transport->SendCommandQueuedWithReply(":LA:STAT?")) == "1"; + break; + case Series::DHO900: + enabled = Trim(m_transport->SendCommandQueuedWithReply(":LA:ENAB?")) == "1"; + break; + case Series::DS1000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::UNKNOWN: + LogLaNotPresent(); + return false; + } + + { + lock_guard lock(m_cacheMutex); + m_laEnabled = enabled; + } + return enabled; +} + +void RigolOscilloscope::LaEnable() +{ + if (IsLaEnabled()) + return; + + switch (m_series) + { + case Series::MSODS1000Z: + case Series::MSO5000: + m_transport->SendCommandQueued(":LA:STAT ON"); + break; + case Series::DHO900: + m_transport->SendCommandQueued(":LA:ENAB ON"); + break; + case Series::DS1000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::UNKNOWN: + LogLaNotPresent(); + break; + } + + { + lock_guard lock(m_cacheMutex); + // we force the cached value refresh in case something caused our request to be ignored + m_laEnabled.reset(); + } +} +void RigolOscilloscope::LaDisable() +{ + if (not IsLaEnabled()) + return; + + switch (m_series) + { + case Series::MSODS1000Z: + case Series::MSO5000: + m_transport->SendCommandQueued(":LA:STAT OFF"); + break; + case Series::DHO900: + m_transport->SendCommandQueued(":LA:ENAB OFF"); + break; + case Series::DS1000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::UNKNOWN: + LogLaNotPresent(); + break; + } + { + lock_guard lock(m_cacheMutex); + // we force the cached value refresh in case something caused our request to be ignored + m_laEnabled.reset(); + } +} + bool RigolOscilloscope::IsChannelEnabled(size_t i) { //ext trigger should never be displayed if(i == m_extTrigChannel->GetIndex()) return false; - //TODO: handle digital channels, for now just claim they're off - if(i >= m_analogChannelCount) - return false; - { lock_guard lock(m_cacheMutex); if(m_channelsEnabled.find(i) != m_channelsEnabled.end()) return m_channelsEnabled[i]; } - auto reply = Trim(m_transport->SendCommandQueuedWithReply(":" + m_channels[i]->GetHwname() + ":DISP?")); - + if (IsChannelAnalog(i)) { - lock_guard lock(m_cacheMutex); - if(reply == "0") + auto reply = Trim(m_transport->SendCommandQueuedWithReply(":" + m_channels[i]->GetHwname() + ":DISP?")); + { - m_channelsEnabled[i] = false; + lock_guard lock(m_cacheMutex); + if(reply == "0") + { + m_channelsEnabled[i] = false; + return false; + } + else + { + m_channelsEnabled[i] = true; + return true; + } + } + } + else if (IsChannelDigital(i)) + { + if (not IsLaEnabled()) return false; + + bool enabled {}; + switch (m_series) + { + case Series::MSODS1000Z: + case Series::MSO5000: + enabled = Trim(m_transport->SendCommandQueuedWithReply(string(":LA:DISP? ") + m_channels[i]->GetHwname())) == "1"; + break; + case Series::DHO900: + enabled = Trim(m_transport->SendCommandQueuedWithReply(string(":LA:DIG:ENAB? ") + m_channels[i]->GetHwname())) == "1"; + break; + case Series::DS1000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::UNKNOWN: + LogLaNotPresent(); + return false; } - else + { - m_channelsEnabled[i] = true; - return true; + lock_guard lock(m_cacheMutex); + m_channelsEnabled[i] = enabled; + return enabled; } } + else + { + return false; + } + LogError("Channel id %zd is unknown", i); + return false; } void RigolOscilloscope::EnableChannel(size_t i) { - m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":DISP ON"); + //TODO: ignore request when cached value says enabled? + if (IsChannelAnalog(i)) + { + if (m_series == Series::MSODS1000Z and m_digitalChannelCount != 0) { + // channel 3 can't be enabled when any digital channel from POD1 (D0 ~ D7) is enabled + // channel 4 can't be enabled when any digital channel from POD2 (D8 ~ D15) is enabled + if (i == 3 or i == 4) + { + bool bankActive = [&]() -> bool { + for (auto idx = m_analogChannelCount + 8 * (i - 3); idx < m_analogChannelCount + 8 * (i - 2); ++idx) + if (IsChannelEnabled(idx)) + return true; + return false; + }(); + if (bankActive) + { + LogWarning("Channel %zd (%s) can't be enabled, because some of colliding digital channels are enabled\n", i, GetChannel(i)->GetDisplayName().c_str()); + return; + } + } + } + m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":DISP ON"); + } + else if (IsChannelDigital(i)) + { + LaEnable(); + switch (m_series) + { + case Series::MSODS1000Z: + case Series::MSO5000: + m_transport->SendCommandQueued(":LA:DISP " + m_channels[i]->GetHwname() + ",ON"); + break; + case Series::DHO900: + m_transport->SendCommandQueued(":LA:DIG:ENAB " + m_channels[i]->GetHwname() + ",ON"); + break; + case Series::DS1000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::UNKNOWN: + LogLaNotPresent(); + return; + } + } + else + { + LogWarning("Channel with idx %zd not known or does no support enable/disable control\n", i); + return; + } // invalidate channel enable cache until confirmed on next IsChannelEnabled { lock_guard lock(m_cacheMutex); @@ -725,7 +1036,36 @@ void RigolOscilloscope::EnableChannel(size_t i) void RigolOscilloscope::DisableChannel(size_t i) { - m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":DISP OFF"); + //TODO: ignore request when cached value says enabled? + if (IsChannelAnalog(i)) + m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":DISP OFF"); + else if (IsChannelDigital(i)) + { + LaEnable(); + switch (m_series) + { + case Series::MSODS1000Z: + case Series::MSO5000: + m_transport->SendCommandQueued(":LA:DISP " + m_channels[i]->GetHwname() + ",OFF"); + break; + case Series::DHO900: + m_transport->SendCommandQueued(":LA:DIG:ENAB " + m_channels[i]->GetHwname() + ",OFF"); + break; + case Series::DS1000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::UNKNOWN: + LogLaNotPresent(); + return; + } + + } + else + { + LogError("Channel with idx %zd not known or does no support enable/disable control\n", i); + return; + } // invalidate channel enable cache until confirmed on next IsChannelEnabled { lock_guard lock(m_cacheMutex); @@ -747,6 +1087,9 @@ vector RigolOscilloscope::GetAvailableCouplin OscilloscopeChannel::CouplingType RigolOscilloscope::GetChannelCoupling(size_t i) { + if (not IsChannelAnalog(i)) + return OscilloscopeChannel::CouplingType::COUPLE_DC_1M; // TODO: what else should we return for DIGITAL non analog channels? + { lock_guard lock(m_cacheMutex); if(m_channelCouplings.find(i) != m_channelCouplings.end()) @@ -769,6 +1112,12 @@ OscilloscopeChannel::CouplingType RigolOscilloscope::GetChannelCoupling(size_t i void RigolOscilloscope::SetChannelCoupling(size_t i, OscilloscopeChannel::CouplingType type) { + if (not IsChannelAnalog(i)) + { + LogError("Channel with idx %zd is not analog channel, only those suport coupling configuration", i); + return; + } + bool valid = true; switch(type) { @@ -785,7 +1134,7 @@ void RigolOscilloscope::SetChannelCoupling(size_t i, OscilloscopeChannel::Coupli break; default: - LogError("Invalid coupling for channel\n"); + LogError("Invalid coupling %d for channel %zd (%s)\n", type, i, GetChannel(i)->GetDisplayName().c_str()); valid = false; } @@ -798,6 +1147,9 @@ void RigolOscilloscope::SetChannelCoupling(size_t i, OscilloscopeChannel::Coupli double RigolOscilloscope::GetChannelAttenuation(size_t i) { + if (not IsChannelAnalog(i)) + return 0; + { lock_guard lock(m_cacheMutex); if(m_channelAttenuations.find(i) != m_channelAttenuations.end()) @@ -819,10 +1171,16 @@ double RigolOscilloscope::GetChannelAttenuation(size_t i) void RigolOscilloscope::SetChannelAttenuation(size_t i, double atten) { + if (not IsChannelAnalog(i)) + { + LogError("Channel with idx %zd is not analog channel, only those suport attenuation configuration", i); + return; + } + + // TODO: refactor attenuation config to only check value presence in container and then use single command to push the value bool valid = true; - switch(( - int)(atten * 10000 + - 0.1)) //+ 0.1 in case atten is for example 0.049999 or so, to round it to 0.05 which turns to an int of 500 + //+ 0.1 in case atten is for example 0.049999 or so, to round it to 0.05 which turns to an int of 500 + switch((int)(atten * 10000 + 0.1)) { case 1: m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":PROB 0.0001"); @@ -906,7 +1264,7 @@ void RigolOscilloscope::SetChannelAttenuation(size_t i, double atten) m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":PROB 50000"); break; default: - LogError("Invalid attenuation for channel\n"); + LogError("Invalid attenuation %lf for channel %zd (%s)\n", atten, i, GetChannel(i)->GetDisplayName().c_str()); valid = false; } @@ -918,8 +1276,11 @@ void RigolOscilloscope::SetChannelAttenuation(size_t i, double atten) } // Our requirements: has to be ordered, zero (full BW) position is not important -vector RigolOscilloscope::GetChannelBandwidthLimiters(size_t /*i*/) +vector RigolOscilloscope::GetChannelBandwidthLimiters(size_t i) { + if (not IsChannelAnalog(i)) // we assume, only analog channels support BW limmitters + return {0}; + switch(m_series) { case Series::MSO5000: @@ -972,6 +1333,9 @@ vector RigolOscilloscope::GetChannelBandwidthLimiters(size_t /*i*/ unsigned int RigolOscilloscope::GetChannelBandwidthLimit(size_t i) { + if (not IsChannelAnalog(i)) + return 0; + { lock_guard lock(m_cacheMutex); if(m_channelBandwidthLimits.find(i) != m_channelBandwidthLimits.end()) @@ -994,6 +1358,9 @@ unsigned int RigolOscilloscope::GetChannelBandwidthLimit(size_t i) void RigolOscilloscope::SetChannelBandwidthLimit(size_t i, unsigned int limit_mhz) { + if (not IsChannelAnalog(i)) + return; + auto const available_limits = GetChannelBandwidthLimiters(i); // `available_limits` is vector of increasing limits (with 0 at the end representing full BW) @@ -1033,7 +1400,11 @@ void RigolOscilloscope::SetChannelBandwidthLimit(size_t i, unsigned int limit_mh } float RigolOscilloscope::GetChannelVoltageRange(size_t i, size_t /*stream*/) -{ +{ + // TODO: what should be the range of digital channel? + if (not IsChannelAnalog(i)) + return 1; + { lock_guard lock(m_cacheMutex); if(m_channelVoltageRanges.find(i) != m_channelVoltageRanges.end()) @@ -1089,6 +1460,10 @@ float RigolOscilloscope::GetChannelVoltageRange(size_t i, size_t /*stream*/) void RigolOscilloscope::SetChannelVoltageRange(size_t i, size_t /*stream*/, float range) { + // only analog channels have adjustable offset + if (not IsChannelAnalog(i)) + return; + { lock_guard lock2(m_cacheMutex); m_channelVoltageRanges[i] = range; @@ -1122,6 +1497,9 @@ OscilloscopeChannel* RigolOscilloscope::GetExternalTrigger() float RigolOscilloscope::GetChannelOffset(size_t i, size_t /*stream*/) { + if (not IsChannelAnalog(i)) + return 0; + { lock_guard lock(m_cacheMutex); @@ -1144,6 +1522,9 @@ float RigolOscilloscope::GetChannelOffset(size_t i, size_t /*stream*/) void RigolOscilloscope::SetChannelOffset(size_t i, size_t /*stream*/, float offset) { + if (not IsChannelAnalog(i)) + return; + m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":OFFS " + to_string(offset)); { @@ -1252,6 +1633,40 @@ Oscilloscope::TriggerMode RigolOscilloscope::PollTrigger() } } +uint64_t RigolOscilloscope::GetPendingWaveformBlockLength() +{ + unsigned char header_size; + { + array header_size_raw {}; + m_transport->ReadRawData(2, reinterpret_cast(header_size_raw.data())); + //LogWarning("Time %f\n", (GetTime() - start)); + + if (sscanf(header_size_raw.data(), "#%c", &header_size) != 1) + return 0; + + header_size = header_size - '0'; + + if(header_size > 12) + { + header_size = 12; + } + + } + + array header {0}; + m_transport->ReadRawData(header_size, reinterpret_cast(header.data())); + + //Look up the block size + //size_t blocksize = end - npoints; + //LogDebug("Block size = %zu\n", blocksize); + // Block size is provided in bytes, not in points + uint64_t header_blocksize_bytes; + sscanf(header.data(), "%" PRIu64, &header_blocksize_bytes); + LogTrace("Parsed waveform block length %" PRIu64 "\n", header_blocksize_bytes); + + return header_blocksize_bytes; +} + bool RigolOscilloscope::AcquireData() { lock_guard lock(m_transport->GetMutex()); @@ -1302,7 +1717,11 @@ bool RigolOscilloscope::AcquireData() } vector temp_buf; temp_buf.resize((m_highDefinition ? (maxpoints * 2) : maxpoints) + 1); - map>> pending_waveforms; + map> pending_waveforms; + + auto ts_download_start = chrono::steady_clock::now(); + + LogTrace("Downloading analog waveforms\n"); for(auto channelIdx = 0U; channelIdx < m_analogChannelCount; channelIdx++) { if(!IsChannelEnabled(channelIdx)) @@ -1380,7 +1799,7 @@ bool RigolOscilloscope::AcquireData() continue; //Set up the capture we're going to store our data into - std::unique_ptr cap {AllocateAnalogWaveform(m_nickname + "." + GetChannel(channelIdx)->GetHwname())}; + auto cap {AllocateAnalogWaveform(m_nickname + "." + GetChannel(channelIdx)->GetHwname())}; cap->clear(); cap->Reserve(npoints); cap->m_timescale = fs_per_sample; @@ -1388,10 +1807,19 @@ bool RigolOscilloscope::AcquireData() cap->m_startTimestamp = floor(now); cap->m_startFemtoseconds = (now - floor(now)) * FS_PER_SECOND; + // when waveforms is not transferred to pending_waveforms, move it back to the pool to prevent memory leak + auto waveformCleanup = CallOnEOC([&](){ + if (cap) + { + AddWaveformToAnalogPool(cap); + cap = nullptr; + } + }); + ChannelsDownloadStatusUpdate(channelIdx, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, 0.0); - //Downloading the waveform is a pain in the butt, because we can only pull 250K points at a time! (Unless you have a MSO5) - auto ts_start = chrono::steady_clock::now(); + // Downloading the waveform is a pain in the butt, because most series can download only limitted amount of points at a time! (Unless you have a MSO5) + auto ts_channel_download_start = chrono::steady_clock::now(); for(size_t npoint = 0; npoint < npoints;) { @@ -1445,46 +1873,13 @@ bool RigolOscilloscope::AcquireData() //Read block header, should be maximally 11 long on MSO5 scope with >= 100 MPoints - unsigned char header_size; - { - array header_size_raw {}; - m_transport->ReadRawData(2, reinterpret_cast(header_size_raw.data())); - //LogWarning("Time %f\n", (GetTime() - start)); - - sscanf(header_size_raw.data(), "#%c", &header_size); - //TODO: check sscanf return value for parsing errors - header_size = header_size - '0'; - - if(header_size > 12) - { - header_size = 12; - } - - } - - array header {0}; - m_transport->ReadRawData(header_size, reinterpret_cast(header.data())); - - //Look up the block size - //size_t blocksize = end - npoints; - //LogDebug("Block size = %zu\n", blocksize); - // Block size is provided in bytes, not in points - size_t header_blocksize_bytes; - sscanf(header.data(), "%zu", &header_blocksize_bytes); - //TODO: check sscanf return value for parsing errors - //LogDebug("Header block size = %zu\n", header_blocksize); + auto header_blocksize_bytes = GetPendingWaveformBlockLength(); if(header_blocksize_bytes == 0) { LogWarning("Ran out of data after %zu points\n", npoint); unsigned char sink; m_transport->ReadRawData(1, &sink); //discard the trailing newline - - // If this happened after zero samples, free the waveform so it doesn't leak - if(npoint == 0) - { - AddWaveformToAnalogPool(cap.release()); - } break; } @@ -1513,7 +1908,7 @@ bool RigolOscilloscope::AcquireData() double ydelta = yorigin + yreference; - cap->Resize(cap->m_samples.size() + header_blocksize); + cap->Resize(cap->size() + header_blocksize); cap->PrepareForCpuAccess(); for(size_t j = 0; j < header_blocksize; j++) @@ -1534,6 +1929,7 @@ bool RigolOscilloscope::AcquireData() default: sample = (static_cast(raw_sample) - ydelta) * yincrement; + // DS/MSO1000Z manual says (0x8E - YORigin - YREFerence) x YINCrement } //LogDebug("V = %.3f, raw=%d, delta=%f, inc=%f\n", v, raw_sample, ydelta, yincrement); } @@ -1542,17 +1938,284 @@ bool RigolOscilloscope::AcquireData() npoint += header_blocksize; } - auto ts_end = chrono::steady_clock::now(); - LogTrace("download took %ld ms\n", chrono::duration_cast(ts_end - ts_start).count()); + auto ts_channel_download_end = chrono::steady_clock::now(); + LogTrace("Channel %s download took %ld ms\n", GetChannel(channelIdx)->GetDisplayName().c_str(), chrono::duration_cast(ts_channel_download_end - ts_channel_download_start).count()); // Notify about end of download for this channel ChannelsDownloadStatusUpdate(channelIdx, InstrumentChannel::DownloadState::DOWNLOAD_FINISHED, 1.0); - //Done, update the data - if(cap) - pending_waveforms[channelIdx].push_back(std::move(cap)); + pending_waveforms[channelIdx].push_back(cap); + cap = nullptr; // we have to clean the ptr, otherwise the waveformCleanup will move it back to the pool } + // download digital channel data (if any) + LogTrace("Downloading digital waveforms\n"); + + // put active channels into their pods, makes the extration easier + struct DigitalChannelCapture { + InstrumentChannel* metadata; + SparseDigitalWaveform* capture; + + DigitalChannelCapture(InstrumentChannel* channel_, SparseDigitalWaveform *capture_) : + metadata(channel_), + capture{capture_} {} + }; + map> banksToDownload; + + for(auto channelIdx = 0U; channelIdx < m_digitalChannelCount; channelIdx++) + { + if(!IsChannelEnabled(m_analogChannelCount + channelIdx)) + continue; + auto const bankIdx = channelIdx / DIGITAL_BANK_SIZE; + auto capture {AllocateDigitalWaveform(m_nickname + "." + GetChannel(channelIdx)->GetHwname())}; + capture->clear(); + // capture->Reserve(npoints); // as we don't know the necessary size, the ubffer will grow on demand (still in blocks) + // capture->m_timescale = // will be udapted later + capture->m_triggerPhase = 0; + capture->m_startTimestamp = floor(now); + capture->m_startFemtoseconds = (now - floor(now)) * FS_PER_SECOND; + + if (auto bank = banksToDownload.find(bankIdx); bank != banksToDownload.end()) + bank->second.emplace_back(GetChannel(channelIdx + m_analogChannelCount), capture); + else + banksToDownload[bankIdx] = {{GetChannel(channelIdx + m_analogChannelCount), capture}}; + } + + // download data for each bank + + m_srateValid = false; + int64_t fs_per_sample = FS_PER_SECOND / GetSampleRate(); // MSO1000Z returns invalid increment value for digital channels, so we take it from here + for(auto bank = banksToDownload.begin(); bank != banksToDownload.end(); ++bank) + { + auto const bankIdx = bank->first; + auto & bankChannels = bank->second; + + // when some waveforms are not transferred to pending_waveforms, move them back to the pool to prevent memory leak + auto waveformCleanup = CallOnEOC([&](){ + for (auto& channel : bankChannels) + if (channel.capture) + { + AddWaveformToAnalogPool(channel.capture); + channel.capture = nullptr; + } + }); + + struct ReleaseWaveforms { + vector &bankChannels; + ~ReleaseWaveforms() { + + } + }; + + // we set the source to the lowest channel in the POD(bank), could be any + m_transport->SendCommandQueued(string("WAV:SOUR ") + m_channels[m_analogChannelCount + (bankIdx * DIGITAL_BANK_SIZE)]->GetHwname()); + + auto preamble = GetCapturePreamble(); + if (not preamble.has_value()) + continue; + + npoints = preamble->npoints; + //If we have zero points in the reply, skip reading data from this bank + if(npoints == 0) + continue; + + switch (m_series) { + case Series::MSODS1000Z: + // MSO1000Z return weirdly scaled xincrement values for digital channels (25 or 50 times higher) + break; + case Series::MSO5000: + case Series::DHO900: + { + //TODO: check that these series respond with meaningfule data in digital chanel preamble + if(preamble->sec_per_sample == 0) + { // Sometimes the scope might return a null value for xincrement => ignore waveform to prenvent an Arithmetic exception in WaveformArea::RasterizeAnalogOrDigitalWaveform + LogWarning("Got null preamble:sec_per_sample value from the scope, ignoring this waveform.\n"); + continue; + } + fs_per_sample = preamble->sec_per_sample; // prefer value from preamble rather than one obtained from GetSamplerate() + break; + } + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DS1000: + case Series::UNKNOWN: + LogLaNotPresent(); + return false; + } + + for (auto& channel : bankChannels) + { + channel.capture->m_timescale = fs_per_sample; + LogTrace("%s m_timescale %" PRIi64 "\n", channel.metadata->GetDisplayName().c_str(), channel.capture->m_timescale); + ChannelsDownloadStatusUpdate(channel.metadata->GetIndex(), InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, 0.0); + } + + // Downloading the waveform is a pain in the butt, because most series can download only limitted amount of points at a time! (Unless you have a MSO5) + auto ts_bank_download_start = chrono::steady_clock::now(); + for(size_t npoint = 0; npoint < npoints;) + { + switch (m_series) { + break; + + case Series::MSODS1000Z: + { + // specify block sample range + m_transport->SendCommandQueued(string("WAV:STAR ") + to_string(npoint+1)); //ONE based indexing WTF + m_transport->SendCommandQueued(string("WAV:STOP ") + to_string(min(npoint + maxpoints, npoints))); //Here it is zero based, so it gets from 1-1000 + // m_transport->SendCommandQueued("*WAI"); // looks unnecessary + + //Ask for the data block + m_transport->SendCommandQueued("WAV:DATA?"); + } break; + + + case Series::MSO5000: + //Ask for the data block + m_transport->SendCommandQueued("*WAI"); + m_transport->SendCommandQueued("WAV:DATA?"); + break; + + case Series::DHO900: + { + //Ask for the data + m_transport->SendCommandQueued(string("WAV:STAR ") + to_string(npoint + 1)); //ONE based indexing WTF + size_t end = npoint + maxpoints; + if(end > npoints) + end = npoints; + m_transport->SendCommandQueued( + string("WAV:STOP ") + to_string(end)); //Here it is zero based, so it gets from 1-1000 + + //Ask for the data block + m_transport->SendCommandQueued("WAV:DATA?"); + break; + } + + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DS1000: + case Series::UNKNOWN: + LogError("RigolOscilloscope: unknown model, invalid state!\n"); + return false; + } + + m_transport->FlushCommandQueue(); + + //Read block header, should be maximally 11 long on MSO5 scope with >= 100 MPoints + + auto header_blocksize_bytes = GetPendingWaveformBlockLength(); + + if(header_blocksize_bytes == 0) + { + LogWarning("Ran out of data after %zu points\n", npoint); + unsigned char sink; + m_transport->ReadRawData(1, &sink); //discard the trailing newline + + // If this happened after zero samples, free the waveform so it doesn't leak + if(npoint == 0) + for (auto& channel : bankChannels) + { + AddWaveformToAnalogPool(channel.capture); + channel.capture = nullptr; + } + break; + } + + auto header_blocksize = header_blocksize_bytes; + + size_t bytesToRead = header_blocksize_bytes + 1; //trailing newline after data block + auto downloadCallback = [&bankChannels, this, npoint, npoints, bytesToRead] (float progress) { + /* we get the percentage of this particular download; convert this into linear percentage across all chunks */ + // TODO: check taht DHO9000 also packs groups of 8 digital channels (pods) into bytes + float bytes_progress = npoint + progress * bytesToRead; + float bytes_total = npoints; + // LogTrace("download progress %5.3f\n", bytes_progress / bytes_total); + // Update all active channels in this bank + auto totalProgress = bytes_progress / bytes_total; + LogDebug("Updating digital channels download progress to %5.3f\n", totalProgress); + for (auto const& channel : bankChannels) + ChannelsDownloadStatusUpdate(channel.metadata->GetIndex(), InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, totalProgress); + }; + m_transport->ReadRawData(bytesToRead, temp_buf.data(), downloadCallback); + downloadCallback(1); + // in case the transport did not call the progress callback (e.g. ScpiLxi), call it manually al least once after the transport finishes + + // extract individual channels from the downloaded data + // raw data are samples with constant samplerate, but we use Sparse waveform for digital channels (to save resources) + // thus while extzracting the data, we only record level changes + for (auto const& channel : bankChannels) + { + auto& cap = channel.capture; + auto samplesStored = cap->size(); + cap->Resize(samplesStored + header_blocksize); // in case we have alternating 010101... sequence, otehrwise we will use less samples and shrink afterwards + cap->PrepareForCpuAccess(); + + uint8_t const mask = 1 << ((channel.metadata->GetIndex() - m_analogChannelCount) % DIGITAL_BANK_SIZE); + + if (samplesStored == 0) { + // not a single sample in the capture buffer yet + // we push the first one unconditionally, and we need to have there at leqst one for following deduplication process + cap->m_samples[samplesStored] = temp_buf[0] & mask; + cap->m_durations[samplesStored] = 1; + cap->m_offsets[samplesStored] = 0; + ++samplesStored; + } + + auto lastValue = cap->m_samples[samplesStored - 1]; + auto *lastDuration = &cap->m_durations[samplesStored - 1]; + + for(size_t j = 1U; j < header_blocksize; j++) + { + bool currentValue = temp_buf[j] & mask; + if (currentValue == lastValue) + ++(*lastDuration); // bit values stayed the same + else + { // bit value changed, create new samples with current value + cap->m_samples[samplesStored] = currentValue; + // lastOffset = cap->m_offsets[samplesStored] = lastOffset + *lastDuration; + cap->m_offsets[samplesStored] = npoint + j; + cap->m_durations[samplesStored] = 1; + lastDuration = &cap->m_durations[samplesStored]; + lastValue = currentValue; + ++samplesStored; + } + + } + cap->Resize(samplesStored); + cap->MarkSamplesModifiedFromCpu(); + } + + npoint += header_blocksize; + } + + auto ts_bank_download_end = chrono::steady_clock::now(); + LogTrace("Digital bank %zd download took %ld ms\n", bankIdx, chrono::duration_cast(ts_bank_download_end - ts_bank_download_start).count()); + + // download finished + for (auto& channel : bankChannels) + { + ChannelsDownloadStatusUpdate(channel.metadata->GetIndex(), InstrumentChannel::DownloadState::DOWNLOAD_FINISHED, 1.0); + { // workaround for https://github.com/ngscopeclient/scopehal-apps/issues/919 + auto samples = channel.capture->size(); + // add out dummy sample + channel.capture->Resize(++samples); + channel.capture->m_samples[samples - 1] = channel.capture->m_samples[samples - 2]; + channel.capture->m_offsets[samples - 1] = channel.capture->m_offsets[samples - 2] + channel.capture->m_durations[samples - 2]; + channel.capture->m_durations[samples - 1] = 0; + } + channel.capture->m_samples.shrink_to_fit(); + channel.capture->m_offsets.shrink_to_fit(); + channel.capture->m_durations.shrink_to_fit(); + channel.capture->MarkModifiedFromCpu(); + pending_waveforms[channel.metadata->GetIndex()].push_back(channel.capture); + channel.capture = nullptr; // we have to clean the ptr, otherwise the waveformCleanup will move it back to the pool + } + } + + auto ts_download_end = chrono::steady_clock::now(); + LogTrace("Whole download took %ld ms\n", chrono::duration_cast(ts_download_end - ts_download_start).count()); + //Now that we have all of the pending waveforms, save them in sets across all channels { lock_guard lock2(m_pendingWaveformsMutex); @@ -1560,13 +2223,21 @@ bool RigolOscilloscope::AcquireData() for(size_t i = 0; i < num_pending; i++) { SequenceSet s; - for(size_t j = 0; j < m_analogChannelCount; j++) + // move all captures to the set + for(size_t j = 0; j < m_analogChannelCount + m_digitalChannelCount; j++) { - if(pending_waveforms.count(j) > 0) - s[GetOscilloscopeChannel(j)] = pending_waveforms[j][i].release(); + if(auto waveform = pending_waveforms.find(j); waveform != pending_waveforms.end()) + { + s[GetOscilloscopeChannel(j)] = waveform->second.front(); + waveform->second.pop_front(); + if (waveform->second.empty()) + pending_waveforms.erase(waveform); + } } m_pendingWaveforms.push_back(s); } + if (not pending_waveforms.empty()) + LogError("pending_waveforms resource management errorl, pending_waveforms should have been amepty now!\n"); } //Clean up diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index 2d9360f0..2047cb1d 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -66,6 +66,11 @@ class RigolOscilloscope : public virtual SCPIOscilloscope virtual OscilloscopeChannel* GetExternalTrigger() override; virtual float GetChannelOffset(size_t i, size_t stream) override; virtual void SetChannelOffset(size_t i, size_t stream, float offset) override; + virtual bool IsDigitalThresholdConfigurable() override; + virtual float GetDigitalThreshold(size_t channel) override; + virtual void SetDigitalThreshold(size_t channel, float level) override; + virtual std::vector GetDigitalBanks() override; + virtual DigitalBank GetDigitalBank(std::size_t channel) override; //Triggering virtual Oscilloscope::TriggerMode PollTrigger() override; @@ -131,6 +136,18 @@ class RigolOscilloscope : public virtual SCPIOscilloscope // NOTE: If the MATH channel is selected, only the NORMal mode is valid. }; + // Rigol scopes organize digital channels into "PODs" of size 8, we call them banks + static constexpr auto DIGITAL_BANK_SIZE = 8; + + bool IsLaEnabled(); + void LaEnable(); + void LaDisable(); + + // bool IsLaPodEnabled(std::size_t pod); + // void LaPodEnable(std::size_t pod); + // void LaPodDisable(std::size_t pod); + + std::size_t GetEnabledAnalogChannelCount(); struct CapturePreamble { CaptureFormat format; @@ -152,6 +169,10 @@ class RigolOscilloscope : public virtual SCPIOscilloscope void AnalyzeDeviceCapabilities(); void UpdateDynamicCapabilities(); // capabilities dependent on enabled chanel count std::size_t GetChannelDivisor(); // helper function to get memory depth/sample rate divisor base on current scope state (amount of enabled channels) + bool IsChannelAnalog(std::size_t i); + bool IsChannelDigital(std::size_t i); + std::uint64_t GetPendingWaveformBlockLength(); // extratcs waveform block size from the TMC header at beginnign of each response to :WAV:DATA? command + void LogLaNotPresent(); protected: OscilloscopeChannel* m_extTrigChannel; @@ -159,6 +180,9 @@ class RigolOscilloscope : public virtual SCPIOscilloscope // hardware analog channel count, independent of LA option etc size_t m_analogChannelCount; + size_t m_digitalChannelCount; + std::vector m_digitalBanks; + // config cache, values that can be updated whenever needed // all access to these shall be exclusive using `m_cacheMutex` std::map m_channelAttenuations; @@ -166,6 +190,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope std::map m_channelOffsets; std::map m_channelVoltageRanges; std::map m_channelBandwidthLimits; + std::map m_bankThresholds; std::map m_channelsEnabled; std::vector m_depths; bool m_srateValid; @@ -174,6 +199,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope uint64_t m_mdepth; int64_t m_triggerOffset; bool m_triggerOffsetValid; + std::optional m_laEnabled; // state variables, may alter values during runtime bool m_triggerArmed; From 6079e58a9c7d8720908058e9b0f19b009960b5d4 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sun, 30 Nov 2025 15:11:48 +0100 Subject: [PATCH 25/59] RigolOscilloscope: change all cached bools (`value` + `ValueValid`) to `std::optional` --- scopehal/RigolOscilloscope.cpp | 73 +++++++++++++++++----------------- scopehal/RigolOscilloscope.h | 9 ++--- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index bed3c9a6..8ef09be6 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -616,8 +616,8 @@ void RigolOscilloscope::UpdateDynamicCapabilities() { depths.emplace_back(depth/divisor); } m_depths = std::move(depths); - m_mdepthValid = false; - m_srateValid = false; + m_mdepth.reset(); + m_srate.reset(); return; } @@ -733,9 +733,9 @@ void RigolOscilloscope::FlushConfigCache() m_channelBandwidthLimits.clear(); m_bankThresholds.clear(); - m_srateValid = false; - m_mdepthValid = false; - m_triggerOffsetValid = false; + m_srate.reset(); + m_mdepth.reset(); + m_triggerOffset.reset(); m_laEnabled.reset(); delete m_trigger; @@ -1983,7 +1983,7 @@ bool RigolOscilloscope::AcquireData() // download data for each bank - m_srateValid = false; + m_srate.reset(); int64_t fs_per_sample = FS_PER_SECOND / GetSampleRate(); // MSO1000Z returns invalid increment value for digital channels, so we take it from here for(auto bank = banksToDownload.begin(); bank != banksToDownload.end(); ++bank) { @@ -2286,7 +2286,7 @@ bool RigolOscilloscope::AcquireData() void RigolOscilloscope::StartPre() { m_liveMode = false; - m_mdepthValid = false; // Memory depth might have been changed on scope + m_mdepth.reset(); // Memory depth might have been changed on scope switch (m_series) { case Series::DHO1000: @@ -2358,7 +2358,7 @@ void RigolOscilloscope::Start() // Limit live mode to one channel setup to prevent grabbing waveforms from to different triggers on seperate channels if(GetEnabledChannelCount()==1) { - m_mdepthValid = false; + m_mdepth.reset(); GetSampleDepth(); m_liveMode = (m_mdepth == 1000); } @@ -2417,7 +2417,7 @@ void RigolOscilloscope::Stop() void RigolOscilloscope::ForceTrigger() { m_liveMode = false; - m_mdepthValid = false; // Memory depth might have been changed on scope + m_mdepth.reset(); // Memory depth might have been changed on scope m_triggerOneShot = true; StartPre(); switch (m_series) @@ -2706,10 +2706,10 @@ uint64_t RigolOscilloscope::GetSampleRate() { { lock_guard lock(m_cacheMutex); - if(m_srateValid) - return m_srate; + if(m_srate.has_value()) + return *m_srate; - LogTrace("smaplerate updating, m_srate %" PRIu64 "\n", m_srate); + LogTrace("smaplerate updating\n"); } // m_transport->SendCommandQueued("*WAI"); @@ -2721,8 +2721,7 @@ uint64_t RigolOscilloscope::GetSampleRate() { lock_guard lock(m_cacheMutex); m_srate = (uint64_t)rate; - m_srateValid = true; - LogTrace("smaplerate updated, m_srate %" PRIu64 "\n", m_srate); + LogTrace("smaplerate updated, m_srate %" PRIu64 "\n", *m_srate); return rate; } } @@ -2731,10 +2730,10 @@ uint64_t RigolOscilloscope::GetSampleDepth() { { lock_guard lock(m_cacheMutex); - if(m_mdepthValid) - return m_mdepth; + if(m_mdepth.has_value()) + return *m_mdepth; - LogTrace("mem depth updating, m_mdepth %" PRIu64 "\n", m_mdepth); + LogTrace("mem depth updating\n"); } auto ret = Trim(m_transport->SendCommandQueuedWithReply(":ACQ:MDEP?")); @@ -2746,9 +2745,8 @@ uint64_t RigolOscilloscope::GetSampleDepth() { lock_guard lock(m_cacheMutex); m_mdepth = (uint64_t)depth; - m_mdepthValid = true; - LogTrace("mem depth updated, m_mdepth %" PRIu64 "\n", m_mdepth); - return m_mdepth; + LogTrace("mem depth updated, m_mdepth %" PRIu64 "\n", *m_mdepth); + return *m_mdepth; } } @@ -2873,7 +2871,7 @@ void RigolOscilloscope::SetSampleDepth(uint64_t depth) } { lock_guard lock(m_cacheMutex); - m_srateValid = false; // changing depth and keeping timebase quite often results in chnage of srate + m_srate.reset(); // changing depth and keeping timebase quite often results in chnage of srate } break; } @@ -2886,18 +2884,21 @@ void RigolOscilloscope::SetSampleDepth(uint64_t depth) { lock_guard lock(m_cacheMutex); - m_mdepthValid = false; + m_mdepth.reset(); } } void RigolOscilloscope::SetSampleRate(uint64_t rate) { - //FIXME, you can set :TIMebase:SCALe + // Rigol scopes do not have samplerate controls. Only timebase can be adjusted :TIMebase:SCALe { lock_guard lock(m_cacheMutex); - m_mdepthValid = false; + m_mdepth.reset(); } double sampletime = GetSampleDepth() / (double)rate; + // locally cache current value before we change the timebase, + // so w can restore it after th timebase change + auto const triggerOffset = GetTriggerOffset(); switch (m_series) { @@ -2957,14 +2958,14 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) { lock_guard lock(m_cacheMutex); - m_srateValid = false; - m_mdepthValid = false; + m_srate.reset(); + m_mdepth.reset(); } // To prevent trigger offset travelling on Srate change (timebase change), // re-set trigger location, because difference (time) between // our trigger reference point (start of sample buffer) and // scope trigger reference point (mid of the sample buffer) changed. - SetTriggerOffset(m_triggerOffset); + SetTriggerOffset(triggerOffset); } @@ -2982,7 +2983,7 @@ void RigolOscilloscope::SetTriggerOffset(int64_t offset) { lock_guard lock(m_cacheMutex); - m_triggerOffsetValid = false; + m_triggerOffset.reset(); } } @@ -2991,28 +2992,28 @@ int64_t RigolOscilloscope::GetTriggerOffset() { { lock_guard lock(m_cacheMutex); - if(m_triggerOffsetValid) - return m_triggerOffset; + if(m_triggerOffset.has_value()) + return *m_triggerOffset; } auto reply = Trim(m_transport->SendCommandQueuedWithReply(":TIM:MAIN:OFFS?")); - lock_guard lock(m_cacheMutex); //Result comes back in scientific notation double offsetval; sscanf(reply.c_str(), "%lf", &offsetval); - m_triggerOffset = static_cast(round(offsetval * FS_PER_SECOND)); + auto offset = static_cast(round(offsetval * FS_PER_SECOND)); //Convert from midpoint to start point int64_t rate = GetSampleRate(); int64_t halfdepth = GetSampleDepth() / 2; int64_t halfwidth = static_cast(round(FS_PER_SECOND * halfdepth / rate)); - m_triggerOffset = halfwidth - m_triggerOffset; - - m_triggerOffsetValid = true; - return m_triggerOffset; + { + lock_guard lock(m_cacheMutex); + m_triggerOffset = halfwidth - offset; + return *m_triggerOffset; + } } bool RigolOscilloscope::HasInterleavingControls() diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index 2047cb1d..c712d529 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -193,12 +193,9 @@ class RigolOscilloscope : public virtual SCPIOscilloscope std::map m_bankThresholds; std::map m_channelsEnabled; std::vector m_depths; - bool m_srateValid; - uint64_t m_srate; - bool m_mdepthValid; - uint64_t m_mdepth; - int64_t m_triggerOffset; - bool m_triggerOffsetValid; + std::optional m_srate; + std::optional m_mdepth; + std::optional m_triggerOffset; std::optional m_laEnabled; // state variables, may alter values during runtime From 24e159f1b38a488c2a12e17407781bf3333fdff0 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 6 Dec 2025 11:19:31 +0100 Subject: [PATCH 26/59] RigolOscilloscope: implement `CanEnableChannel` and use it in EnableChannel - ATM only affects MSODS1000Z as there are collisions between digital banks and some analog channels --- scopehal/RigolOscilloscope.cpp | 162 +++++++++++++++++++++++++++++---- scopehal/RigolOscilloscope.h | 6 ++ 2 files changed, 151 insertions(+), 17 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 8ef09be6..8ce72955 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -907,6 +907,32 @@ void RigolOscilloscope::LaDisable() } } +std::size_t RigolOscilloscope::IdxToAnalogChannelNumber(std::size_t i) +{ + return i; +} + +std::size_t RigolOscilloscope::AnalogChannelNumberToIdx(std::size_t i) +{ + return i; +} + +std::size_t RigolOscilloscope::IdxToDigitalChannelNumber(std::size_t i) +{ + return i - m_analogChannelCount; +} + +std::size_t RigolOscilloscope::DigitalChannelNumberToIdx(std::size_t i) +{ + return i + m_analogChannelCount; +} + +std::size_t RigolOscilloscope::IdxToDigitalBankIdx(std::size_t i) +{ + return (i - m_analogChannelCount) / DIGITAL_BANK_SIZE; +} + + bool RigolOscilloscope::IsChannelEnabled(size_t i) { //ext trigger should never be displayed @@ -975,30 +1001,132 @@ bool RigolOscilloscope::IsChannelEnabled(size_t i) return false; } -void RigolOscilloscope::EnableChannel(size_t i) +bool RigolOscilloscope::CanEnableChannel(size_t i) { - //TODO: ignore request when cached value says enabled? - if (IsChannelAnalog(i)) + auto* channel = GetChannel(i); + if (channel == nullptr) + return false; + + switch (m_series) { - if (m_series == Series::MSODS1000Z and m_digitalChannelCount != 0) { - // channel 3 can't be enabled when any digital channel from POD1 (D0 ~ D7) is enabled - // channel 4 can't be enabled when any digital channel from POD2 (D8 ~ D15) is enabled - if (i == 3 or i == 4) + case Series::MSODS1000Z: + // CHAN3 collides with bank0 (D8 ~ D15) + // CHAN4 collides with bank1 (D0 ~ D7), yeah, feels swapped, but that's the reality + if (IsChannelAnalog(i)) { - bool bankActive = [&]() -> bool { - for (auto idx = m_analogChannelCount + 8 * (i - 3); idx < m_analogChannelCount + 8 * (i - 2); ++idx) - if (IsChannelEnabled(idx)) - return true; - return false; - }(); - if (bankActive) + if (m_digitalBanks.empty()) + return true; + + auto const analogChannelNumber = IdxToAnalogChannelNumber(i); + switch (analogChannelNumber) { - LogWarning("Channel %zd (%s) can't be enabled, because some of colliding digital channels are enabled\n", i, GetChannel(i)->GetDisplayName().c_str()); - return; + case 2: // attempting to enable CHAN3 + for (auto& dChannel : m_digitalBanks[1]) + if (IsChannelEnabled(dChannel->GetIndex())) + { + LogTrace("channel %zd (%s) can't be enabled, collision with (at least) channel %zd (%s) from bank %d\n", + i, GetChannel(i)->GetDisplayName().c_str(), + dChannel->GetIndex(), dChannel->GetDisplayName().c_str(), + 1U + ); + return false; + } + break; + case 3: // attempting to enable CHAN4 + for (auto& dChannel : m_digitalBanks[0]) + if (IsChannelEnabled(dChannel->GetIndex())) + { + LogTrace("channel %zd (%s) can't be enabled, collision with (at least) channel %zd (%s) from bank %d\n", + i, GetChannel(i)->GetDisplayName().c_str(), + dChannel->GetIndex(), dChannel->GetDisplayName().c_str(), + 0U + ); + return false; + } + break; + default: break; } + return true; } - } + + if (IsChannelDigital(i)) { + if (m_digitalBanks.empty()) + return true; + + auto const bankIdx = IdxToDigitalBankIdx(i); + switch (bankIdx) + { + case 0: // bank 0 + if (IsChannelEnabled(AnalogChannelNumberToIdx(2))) // CHAN3 + { + auto collidingCHannel = GetChannel(AnalogChannelNumberToIdx(2)); + LogTrace("channel %zd (%s) from bank %zd, can't be enabled, collision with analog channel %zd (%s)\n", + i, GetChannel(i)->GetDisplayName().c_str(), bankIdx, + collidingCHannel->GetIndex(), collidingCHannel->GetDisplayName().c_str() + ); + return false; + } + break; + case 1: // bank 1 + if (IsChannelEnabled(AnalogChannelNumberToIdx(3))) // CHAN4 + { + auto collidingCHannel = GetChannel(AnalogChannelNumberToIdx(3)); + LogTrace("channel %zd (%s) from bank %zd, can't be enabled, collision with analog channel %zd (%s)\n", + i, GetChannel(i)->GetDisplayName().c_str(), bankIdx, + collidingCHannel->GetIndex(), collidingCHannel->GetDisplayName().c_str() + ); + return false; + } + break; + default : return false; + } + return true; + } + break; + + case Series::DS1000: + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + if (IsChannelAnalog(i)) return true; + if (IsChannelDigital(i)) return true; + break; + case Series::UNKNOWN: + break; + } + return false; +} + +void RigolOscilloscope::EnableChannel(size_t i) +{ + if (not CanEnableChannel(i)) + { + LogWarning("can't enable channel %zd\n", i); + return; + } + //TODO: ignore request when cached value says enabled? + if (IsChannelAnalog(i)) + { m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":DISP ON"); + switch (m_series) + { + case Series::MSODS1000Z: + // impact of enabling analog channel takes effect (change of mem depth,...) only in RUN state + m_transport->SendCommandQueued(":SING"); + m_transport->SendCommandQueued(":TFOR"); + break; + //TODO: check of other scopes require this too + case Series::UNKNOWN: + case Series::DS1000: + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + break; + } } else if (IsChannelDigital(i)) { diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index c712d529..c3a8ded9 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -51,6 +51,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope //Channel configuration virtual bool IsChannelEnabled(size_t i) override; + virtual bool CanEnableChannel(size_t i) override; virtual void EnableChannel(size_t i) override; virtual void DisableChannel(size_t i) override; virtual OscilloscopeChannel::CouplingType GetChannelCoupling(size_t i) override; @@ -173,6 +174,11 @@ class RigolOscilloscope : public virtual SCPIOscilloscope bool IsChannelDigital(std::size_t i); std::uint64_t GetPendingWaveformBlockLength(); // extratcs waveform block size from the TMC header at beginnign of each response to :WAV:DATA? command void LogLaNotPresent(); + std::size_t IdxToAnalogChannelNumber(std::size_t i); // does not check for validity! + std::size_t AnalogChannelNumberToIdx(std::size_t i); // does not check for validity! + std::size_t IdxToDigitalChannelNumber(std::size_t i); // does not check for validity! + std::size_t DigitalChannelNumberToIdx(std::size_t i); // does not check for validity! + std::size_t IdxToDigitalBankIdx(std::size_t i); // does not check for validity! protected: OscilloscopeChannel* m_extTrigChannel; From 93e4f51b2485e34f01fddde227a3d36d35ff915e Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 6 Dec 2025 11:27:09 +0100 Subject: [PATCH 27/59] RigolOscilloscope: rename `GetChannelDivisor` -> `GetMsods1000ZChannelDivisor` because ATM this divisor behavior seems to be specific to MSODS1000Z only --- scopehal/RigolOscilloscope.cpp | 10 +++++----- scopehal/RigolOscilloscope.h | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 8ce72955..49526311 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -606,7 +606,7 @@ void RigolOscilloscope::UpdateDynamicCapabilities() { // ― 2 CH: 6000| 60000| 600000| 6000000|12000000 // ― 3/4 CH: 3000| 30000| 300000| 3000000| 6000000 // -> 1 CH values appropriately divided - auto divisor = GetChannelDivisor(); + auto divisor = GetMsods1000ZChannelDivisor(); vector depths; for (auto &depth : ds1000zSampleDepths) { @@ -628,7 +628,7 @@ void RigolOscilloscope::UpdateDynamicCapabilities() { } } -std::size_t RigolOscilloscope::GetChannelDivisor() { +std::size_t RigolOscilloscope::GetMsods1000ZChannelDivisor() { auto divisor = GetEnabledAnalogChannelCount(); // MSO1000Z: @@ -1829,7 +1829,7 @@ bool RigolOscilloscope::AcquireData() // 250kB limits applies when all channels are enabled. // It is possible to use larger chunks with less channels. // With single channel and 1 MB block, around ~20% speed-up is observable. - maxpoints = 1000 * 1000 / GetChannelDivisor(); + maxpoints = 1000 * 1000 / GetMsods1000ZChannelDivisor(); break; case Series::MSO5000: maxpoints = GetSampleDepth(); //You can use 250E6 points too, but it is very slow @@ -2691,7 +2691,7 @@ vector RigolOscilloscope::GetSampleRatesNonInterleaved() case Series::MSODS1000Z: { vector rates {}; - auto divisor = GetChannelDivisor(); + auto divisor = GetMsods1000ZChannelDivisor(); auto mdepth = GetSampleDepth(); for (const auto& rate : ds1000zSampleRates) if (rate.supportsMdepth(mdepth, divisor)) @@ -3062,7 +3062,7 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) // Mdepth = Srate * wlength // Mdepth = Srate * Tscale * 12 // Mdepth / (Srate * 12) = * Tscale - auto const divisor = GetChannelDivisor(); + auto const divisor = GetMsods1000ZChannelDivisor(); LogTrace("setting target samplerate %lu, divisor %zu\n", rate, divisor); auto const timescale = [&]() -> float { if (divisor != 4 and (rate / divisor) >= 25'000'000) diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index c3a8ded9..0cc1fd79 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -169,7 +169,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope void DecodeDeviceSeries(); void AnalyzeDeviceCapabilities(); void UpdateDynamicCapabilities(); // capabilities dependent on enabled chanel count - std::size_t GetChannelDivisor(); // helper function to get memory depth/sample rate divisor base on current scope state (amount of enabled channels) + std::size_t GetMsods1000ZChannelDivisor(); // helper function to get memory depth/sample rate divisor base on current scope state (amount of enabled channels) bool IsChannelAnalog(std::size_t i); bool IsChannelDigital(std::size_t i); std::uint64_t GetPendingWaveformBlockLength(); // extratcs waveform block size from the TMC header at beginnign of each response to :WAV:DATA? command From 9290c818fe7f338f23930fef20f3579899526e90 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 6 Dec 2025 12:05:39 +0100 Subject: [PATCH 28/59] RigolOscilloscope: MSODS1000Z: fix possible bad locking in `SetSampleDepth` - we have to ensure, the trigger status does not change after we query it so we can restore correct value in the end --- scopehal/RigolOscilloscope.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 49526311..3536991a 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -2987,8 +2987,8 @@ void RigolOscilloscope::SetSampleDepth(uint64_t depth) } // memory depth is configurable only when scope is not stopped { - string original_trigger_status = m_transport->SendCommandQueuedWithReply(":TRIG:STAT?"); lock_guard lock(m_transport->GetMutex()); // this sequence may not be interrupted by others + string original_trigger_status = m_transport->SendCommandQueuedWithReply(":TRIG:STAT?"); m_transport->SendCommandQueued(":RUN"); m_transport->SendCommandQueued("ACQ:MDEP " + to_string(depth)); //TODO: whould we also use switch to accept only valid values? From 5da83042fd4f4c24806cfdd2f29e23f5c0a44a24 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 6 Dec 2025 16:38:04 +0100 Subject: [PATCH 29/59] RigolOscilloscope: create external trigger on for series with EXT trig input - all supported except MSO5000 --- scopehal/RigolOscilloscope.cpp | 26 ++++++++++++++++++++------ scopehal/RigolOscilloscope.h | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 3536991a..c93e8ad4 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -145,11 +145,25 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) } } - //Add the external trigger input - m_extTrigChannel = new OscilloscopeChannel( - this, "EX", "", Unit(Unit::UNIT_FS), Unit(Unit::UNIT_VOLTS), Stream::STREAM_TYPE_TRIGGER, m_channels.size()); - m_channels.push_back(m_extTrigChannel); - m_extTrigChannel->SetDefaultDisplayName(); + // Add the external trigger input + switch (m_series) + { + case Series::DS1000: + case Series::MSODS1000Z: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + m_extTrigChannel = new OscilloscopeChannel( + this, "External", "#FFFFFF", Unit(Unit::UNIT_FS), Unit(Unit::UNIT_VOLTS), Stream::STREAM_TYPE_TRIGGER, m_channels.size()); + m_channels.push_back(m_extTrigChannel); + m_extTrigChannel->SetDefaultDisplayName(); + break; + + case Series::MSO5000: + case Series::UNKNOWN: + break; + } //Configure acquisition modes switch (m_series) { @@ -936,7 +950,7 @@ std::size_t RigolOscilloscope::IdxToDigitalBankIdx(std::size_t i) bool RigolOscilloscope::IsChannelEnabled(size_t i) { //ext trigger should never be displayed - if(i == m_extTrigChannel->GetIndex()) + if(m_extTrigChannel and i == m_extTrigChannel->GetIndex()) return false; { diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index 0cc1fd79..9e38b0dc 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -181,7 +181,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope std::size_t IdxToDigitalBankIdx(std::size_t i); // does not check for validity! protected: - OscilloscopeChannel* m_extTrigChannel; + OscilloscopeChannel* m_extTrigChannel {}; // hardware analog channel count, independent of LA option etc size_t m_analogChannelCount; From 07b1535b735bd1ad85bdd8c51c9c8e4e8f62301c Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 6 Dec 2025 16:48:51 +0100 Subject: [PATCH 30/59] RigolOscilloscope: add AC line (ACL) trigger, scopehal does not support it now, so it will not be listed in available trigger inputs --- scopehal/RigolOscilloscope.cpp | 24 ++++++++++++++++++++++++ scopehal/RigolOscilloscope.h | 1 + 2 files changed, 25 insertions(+) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index c93e8ad4..9ff405aa 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -165,6 +165,27 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) break; } + // Add AC line external trigger input + switch (m_series) + { + case Series::MSODS1000Z: + case Series::MSO5000: + case Series::DHO1000: + case Series::DHO4000: + case Series::DS1000: + m_aclTrigChannel = new OscilloscopeChannel( + this, "ACL", "#FF0000", Unit(Unit::UNIT_FS), Unit(Unit::UNIT_VOLTS), Stream::STREAM_TYPE_TRIGGER, m_channels.size()); + m_channels.push_back(m_aclTrigChannel); + m_aclTrigChannel->SetDisplayName("ACLine"); + m_aclTrigChannel->SetDefaultDisplayName(); + break; + + case Series::DHO800: + case Series::DHO900: + case Series::UNKNOWN: + break; + } + //Configure acquisition modes switch (m_series) { case Series::DS1000: @@ -953,6 +974,9 @@ bool RigolOscilloscope::IsChannelEnabled(size_t i) if(m_extTrigChannel and i == m_extTrigChannel->GetIndex()) return false; + if(m_aclTrigChannel and i == m_aclTrigChannel->GetIndex()) + return false; + { lock_guard lock(m_cacheMutex); if(m_channelsEnabled.find(i) != m_channelsEnabled.end()) diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index 9e38b0dc..c807b762 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -182,6 +182,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope protected: OscilloscopeChannel* m_extTrigChannel {}; + OscilloscopeChannel* m_aclTrigChannel {}; // TODO: scopehal does not handle AC "external triggers" ATM // hardware analog channel count, independent of LA option etc size_t m_analogChannelCount; From 70f3ce55d46726521914c12c6a9a3ac78250f8f1 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 6 Dec 2025 18:30:52 +0100 Subject: [PATCH 31/59] RigolOscilloscope: simplify `EnableChannel` immediate effect quirk, add MSO5000 as affected --- scopehal/RigolOscilloscope.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 9ff405aa..7cba05cc 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -1150,15 +1150,14 @@ void RigolOscilloscope::EnableChannel(size_t i) m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":DISP ON"); switch (m_series) { - case Series::MSODS1000Z: - // impact of enabling analog channel takes effect (change of mem depth,...) only in RUN state - m_transport->SendCommandQueued(":SING"); - m_transport->SendCommandQueued(":TFOR"); + // this is a quirk that changes to take effect immediately + case Series::MSODS1000Z: // applies the configuration + case Series::MSO5000: // ensures the srate readout reads correct value + SetSampleDepth(GetSampleDepth()); break; - //TODO: check of other scopes require this too + //TODO: check of other scopes require this too case Series::UNKNOWN: case Series::DS1000: - case Series::MSO5000: case Series::DHO1000: case Series::DHO4000: case Series::DHO800: From 48b3d67d7dcbd4cf931732f21914db05346a228d Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sat, 6 Dec 2025 18:33:36 +0100 Subject: [PATCH 32/59] RigolOscilloscope: discard cached srate value when`EnableChannel`/`DisableChannel` is called number of enabled channels may affect current samplerate, especially near the maximum rates --- scopehal/RigolOscilloscope.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 7cba05cc..3f71f8a2 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -1195,6 +1195,7 @@ void RigolOscilloscope::EnableChannel(size_t i) { lock_guard lock(m_cacheMutex); m_channelsEnabled.erase(i); + m_srate.reset(); } UpdateDynamicCapabilities(); } @@ -1235,6 +1236,7 @@ void RigolOscilloscope::DisableChannel(size_t i) { lock_guard lock(m_cacheMutex); m_channelsEnabled.erase(i); + m_srate.reset(); } UpdateDynamicCapabilities(); } From 56f13a1c71762b7a0a2d2061b7ff7b572decae47 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 9 Dec 2025 16:37:53 +0100 Subject: [PATCH 33/59] RigolOscilloscope: MSO5000: fix samplerate setting - it is dependent on how many analog channels are enabled --- scopehal/RigolOscilloscope.cpp | 149 +++++++++++++++++++++++++-------- 1 file changed, 115 insertions(+), 34 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 3f71f8a2..7ae2a380 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -590,8 +590,8 @@ static std::vector mso5000SampleDepths { 10 * 1000 * 1000, 25 * 1000 * 1000, 50 * 1000 * 1000, - 100 * 1000 * 1000, // only for <= 2 CH - 200 * 1000 * 1000 // only for == 1 CH + 100 * 1000 * 1000, // see GetMso5000AnalogBankUsage + 200 * 1000 * 1000 // see GetMso5000AnalogBankUsage }; void RigolOscilloscope::UpdateDynamicCapabilities() { @@ -625,11 +625,14 @@ void RigolOscilloscope::UpdateDynamicCapabilities() { // all-channel is 50 M. // -> remove N highest auto depths = mso5000SampleDepths; - auto channelsEnabled = GetEnabledChannelCount(); - if (channelsEnabled > 1) - depths.pop_back(); - if (channelsEnabled > 2) - depths.pop_back(); + auto const bankUsage = GetMso5000AnalogBankUsage(); + if (bankUsage[0] == 2 or bankUsage[1] == 2) + { + depths.pop_back(); // 200M + depths.pop_back(); // 100M + } + else if (not m_opt200M or (bankUsage[0] and bankUsage[1])) + depths.pop_back(); // 200M m_depths = std::move(depths); return; } @@ -693,6 +696,46 @@ std::size_t RigolOscilloscope::GetMsods1000ZChannelDivisor() { return divisor; } +// MSO5000 has it's own specific behavior of how enabled channels affect samplerate +// - (in this fcn) channel is considered enabled if it is enabled or if is trigger source +// - bank 0 are channels 1 dnd 2 +// - bank 1 are channels 3 dnd 4 +// if any bank has more than 1 channel enabled -> 4 and 8 Gsps rates are not available +// else if at one channel from each bank is enabled -> 8 Gsps are not available +// thus if e.g.: ch1 and ch2 are enabled (2 channels from single bank), only 2 Gsps max rate is available +// Sample applies for memory depth, but the highest depth is also only an option +std::array RigolOscilloscope::GetMso5000AnalogBankUsage() { + std::array bankUsage {}; + + for (auto idx = 0U; idx < m_analogChannelCount; ++idx) + { + bool enabled {}; + enabled |= IsChannelEnabled(idx); + + { + auto trigger = GetTrigger(); + auto input = trigger->GetInput(0); + enabled |= input.m_channel and input.m_channel->GetIndex() == idx; + } + if (not enabled) + continue; + switch (idx) + { + case 0: + case 1: + ++bankUsage[0]; + break; + case 2: + case 3: + ++bankUsage[1]; + break; + default: + break; + } + } + return bankUsage; +} + bool RigolOscilloscope::IsChannelAnalog(std::size_t i) { return i < m_analogChannelCount; @@ -2720,6 +2763,34 @@ static std::vector ds1000zSampleRates { {1000 * 1000 * 1000, 1 | 2 | 4 | 8 | 16} // 12k 120k 1M2 12M 24M 500M 250M Y }; +static std::vector mso5000SampleRates { + 100UL, + 200UL, + 500UL, + 1000UL, + 2000UL, + 5000UL, + 10 * 1000UL, + 20 * 1000UL, + 50 * 1000UL, + 100 * 1000UL, + 200 * 1000UL, + 500 * 1000UL, + 1 * 1000 * 1000UL, + 2 * 1000 * 1000UL, + 5 * 1000 * 1000UL, + 10 * 1000 * 1000UL, + 20 * 1000 * 1000UL, + 50 * 1000 * 1000UL, + 100 * 1000 * 1000UL, + 200 * 1000 * 1000UL, + 500 * 1000 * 1000UL, + 1 * 1000 * 1000 * 1000UL, + 2 * 1000 * 1000 * 1000UL, + 4 * 1000 * 1000 * 1000UL, // see GetMso5000AnalogBankUsage + 8 * 1000 * 1000 * 1000UL // see GetMso5000AnalogBankUsage +}; + vector RigolOscilloscope::GetSampleRatesNonInterleaved() { LogTrace("GetSampleRatesNonInterleaved called"); @@ -2752,33 +2823,18 @@ vector RigolOscilloscope::GetSampleRatesNonInterleaved() case Series::MSO5000: - return { - 100, - 200, - 500, - 1000, - 2000, - 5000, - 10 * 1000, - 20 * 1000, - 50 * 1000, - 100 * 1000, - 200 * 1000, - 500 * 1000, - 1 * 1000 * 1000, - 2 * 1000 * 1000, - 5 * 1000 * 1000, - 10 * 1000 * 1000, - 20 * 1000 * 1000, - 50 * 1000 * 1000, - 100 * 1000 * 1000, - 200 * 1000 * 1000, - 500 * 1000 * 1000, - 1 * 1000 * 1000 * 1000, - 2 * 1000 * 1000 * 1000, - }; - break; - + { + auto rates = mso5000SampleRates; + auto const bankUsage = GetMso5000AnalogBankUsage(); + if (bankUsage[0] == 2 or bankUsage[1] == 2) + { + rates.pop_back(); + rates.pop_back(); + } + else if (bankUsage[0] and bankUsage[1]) + rates.pop_back(); + return rates; + } case Series::DHO1000: case Series::DHO4000: case Series::DHO800: @@ -3090,6 +3146,31 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) break; } + case Series::MSO5000: + { + //TODO: consoder defining available time scales and do lookup of closes one (equal or lower) then we get from formula + LogTrace("setting target samplerate %lu\n", rate); + switch (rate) + { + // 4 Gpss result in 25 us, but the scope's native step is 20 us, anything above results in + case 4'000'000'000UL: + m_transport->SendCommandQueued(string(":TIM:SCAL ") + to_string(2e-5)); + break; + + // 8 Gpss result in 12.5 us, but the scope's native step is 10 us, anything above results in + case 8'000'000'000UL: + m_transport->SendCommandQueued(string(":TIM:SCAL ") + to_string(1e-5)); + break; + + default: + m_transport->SendCommandQueued(string(":TIM:SCAL ") + to_string(sampletime / 10)); + break; + + } + SetSampleDepth(GetSampleDepth()); // see note at MSODO1000Z + break; + } + case Series::MSODS1000Z: { // The following equation describes the relationship among memory depth, sample rate, and waveform length: From 2a37231160414e5fdb11344d9a89f8a590892fb5 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 9 Dec 2025 16:39:44 +0100 Subject: [PATCH 34/59] RigolOscilloscope: logging , comment/TODO and helper function usage cleanup --- scopehal/RigolOscilloscope.cpp | 28 ++++++++++++++++++---------- scopehal/RigolOscilloscope.h | 1 + 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 7ae2a380..a1ba18fd 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -661,7 +661,6 @@ void RigolOscilloscope::UpdateDynamicCapabilities() { case Series::DS1000: case Series::UNKNOWN: - LogError("RigolOscilloscope::GetSampleDepthsNonInterleaved not implemented for this model\n"); break; } } @@ -2008,6 +2007,8 @@ bool RigolOscilloscope::AcquireData() if(npoints == 0) continue; + LogTrace("Channel %u(%s) samplerate %s\n", channelIdx, GetChannel(channelIdx)->GetDisplayName().c_str(), Unit(Unit::UNIT_SAMPLERATE).PrettyPrint(FS_PER_SECOND/fs_per_sample).c_str()); + //Set up the capture we're going to store our data into auto cap {AllocateAnalogWaveform(m_nickname + "." + GetChannel(channelIdx)->GetHwname())}; cap->clear(); @@ -2112,10 +2113,13 @@ bool RigolOscilloscope::AcquireData() // LogTrace("download progress %5.3f\n", bytes_progress / bytes_total); ChannelsDownloadStatusUpdate(channelIdx, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, bytes_progress / bytes_total); }; - m_transport->ReadRawData(bytesToRead, temp_buf.data(), downloadCallback); + auto const bytesRead = m_transport->ReadRawData(bytesToRead, temp_buf.data(), downloadCallback); downloadCallback(1); // in case the transport did not call the progress callback (e.g. ScpiLxi), call it manually al least once after the transport finishes + if (bytesRead != bytesToRead) + LogWarning("requested %zd , got %zd B\n", bytesToRead, bytesRead); + double ydelta = yorigin + yreference; cap->Resize(cap->size() + header_blocksize); @@ -2174,7 +2178,7 @@ bool RigolOscilloscope::AcquireData() for(auto channelIdx = 0U; channelIdx < m_digitalChannelCount; channelIdx++) { - if(!IsChannelEnabled(m_analogChannelCount + channelIdx)) + if(!IsChannelEnabled(DigitalChannelNumberToIdx(channelIdx))) continue; auto const bankIdx = channelIdx / DIGITAL_BANK_SIZE; auto capture {AllocateDigitalWaveform(m_nickname + "." + GetChannel(channelIdx)->GetHwname())}; @@ -2186,9 +2190,9 @@ bool RigolOscilloscope::AcquireData() capture->m_startFemtoseconds = (now - floor(now)) * FS_PER_SECOND; if (auto bank = banksToDownload.find(bankIdx); bank != banksToDownload.end()) - bank->second.emplace_back(GetChannel(channelIdx + m_analogChannelCount), capture); + bank->second.emplace_back(GetChannel(DigitalChannelNumberToIdx(channelIdx)), capture); else - banksToDownload[bankIdx] = {{GetChannel(channelIdx + m_analogChannelCount), capture}}; + banksToDownload[bankIdx] = {{GetChannel(DigitalChannelNumberToIdx(channelIdx)), capture}}; } // download data for each bank @@ -2218,7 +2222,7 @@ bool RigolOscilloscope::AcquireData() }; // we set the source to the lowest channel in the POD(bank), could be any - m_transport->SendCommandQueued(string("WAV:SOUR ") + m_channels[m_analogChannelCount + (bankIdx * DIGITAL_BANK_SIZE)]->GetHwname()); + m_transport->SendCommandQueued(string("WAV:SOUR ") + GetChannel(DigitalChannelNumberToIdx(bankIdx * DIGITAL_BANK_SIZE))->GetHwname()); auto preamble = GetCapturePreamble(); if (not preamble.has_value()) @@ -2253,6 +2257,7 @@ bool RigolOscilloscope::AcquireData() LogLaNotPresent(); return false; } + LogTrace("Bank %zd samplerate %s\n", bankIdx, Unit(Unit::UNIT_SAMPLERATE).PrettyPrint(FS_PER_SECOND/fs_per_sample).c_str()); for (auto& channel : bankChannels) { @@ -2343,7 +2348,6 @@ bool RigolOscilloscope::AcquireData() // LogTrace("download progress %5.3f\n", bytes_progress / bytes_total); // Update all active channels in this bank auto totalProgress = bytes_progress / bytes_total; - LogDebug("Updating digital channels download progress to %5.3f\n", totalProgress); for (auto const& channel : bankChannels) ChannelsDownloadStatusUpdate(channel.metadata->GetIndex(), InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, totalProgress); }; @@ -2361,7 +2365,7 @@ bool RigolOscilloscope::AcquireData() cap->Resize(samplesStored + header_blocksize); // in case we have alternating 010101... sequence, otehrwise we will use less samples and shrink afterwards cap->PrepareForCpuAccess(); - uint8_t const mask = 1 << ((channel.metadata->GetIndex() - m_analogChannelCount) % DIGITAL_BANK_SIZE); + uint8_t const mask = 1 << (IdxToDigitalChannelNumber(channel.metadata->GetIndex()) % DIGITAL_BANK_SIZE); if (samplesStored == 0) { // not a single sample in the capture buffer yet @@ -2536,7 +2540,7 @@ void RigolOscilloscope::StartPost() LogTrace("set m_pointsWhenStarted to %" PRIuLEAST32 "\n", m_pointsWhenStarted); } else - LogError("empty preable"); + LogError("empty preable\n"); } } @@ -2795,6 +2799,8 @@ vector RigolOscilloscope::GetSampleRatesNonInterleaved() { LogTrace("GetSampleRatesNonInterleaved called"); + //TODO: cache samplerates + //FIXME switch (m_series) { @@ -3199,7 +3205,9 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) break; } - default: + case Series::UNKNOWN: + case Series::DS1000: + LogWarning("This scope does nto support samplerate settings (yet)"); break; } diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index c807b762..a2d95dd4 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -229,6 +229,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope Series m_series; //True if we have >8 bit capture depth + //TODO: switch to GetADCMode/SetADCMode API instead of having custom attribute switching DHOs to 12 bits bool m_highDefinition; void PushEdgeTrigger(EdgeTrigger* trig); From e72aed15d735e315ff6016882777593a20e7db41 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 9 Dec 2025 16:42:29 +0100 Subject: [PATCH 35/59] RigolOscilloscope: `PollTrigger`: add quirk for MSO5000 to not report `TRIGGER_MODE_TRIGGERED` when scope state == `TD`, because then the `ngscopeclient` immediately calls `AcquireData`, but data are available once the scope reaches `STOP` state, not `TD`. --- scopehal/RigolOscilloscope.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index a1ba18fd..162ded08 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -1815,7 +1815,18 @@ Oscilloscope::TriggerMode RigolOscilloscope::PollTrigger() m_triggerWasLive = true; if(stat == "TD") - return TRIGGER_MODE_TRIGGERED; + { + if (m_series == Series::MSO5000) + // MSO5000Z reports triggered right at the triggering moment, + // but that does not imply data availability (returning TRIGGER_MODE_TRIGGERED does). + // So we return TRIGGER_MODE_RUN to not indicate data availability + // and rely on the `STOP` to arrive afterwards. + // This can be easily caused by long sampling times (e.g.: 10 ksps and 100k mem. depth) + return TRIGGER_MODE_RUN; + else + // TODO: check if other families trigger TD state is also affected or not + return TRIGGER_MODE_TRIGGERED; + } else if(stat == "RUN") return TRIGGER_MODE_RUN; else if(stat == "WAIT") From 0491f6f6176651ce0a9a496d8bfe2afa03e09c7a Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 9 Dec 2025 16:42:55 +0100 Subject: [PATCH 36/59] RigolOscilloscope: make `AcquireData` more resistant to missing samplerate info in preamble --- scopehal/RigolOscilloscope.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 162ded08..7974765a 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -2253,11 +2253,9 @@ bool RigolOscilloscope::AcquireData() { //TODO: check that these series respond with meaningfule data in digital chanel preamble if(preamble->sec_per_sample == 0) - { // Sometimes the scope might return a null value for xincrement => ignore waveform to prenvent an Arithmetic exception in WaveformArea::RasterizeAnalogOrDigitalWaveform - LogWarning("Got null preamble:sec_per_sample value from the scope, ignoring this waveform.\n"); - continue; - } - fs_per_sample = preamble->sec_per_sample; // prefer value from preamble rather than one obtained from GetSamplerate() + break; // Sometimes the scope might return a null value for xincrement => do not update `fs_per_sample` + + fs_per_sample = preamble->sec_per_sample * FS_PER_SECOND; // prefer value from preamble rather than one obtained from GetSamplerate() break; } case Series::DHO1000: @@ -2268,6 +2266,13 @@ bool RigolOscilloscope::AcquireData() LogLaNotPresent(); return false; } + + if (fs_per_sample == 0) + { + LogWarning("Invalid fs_per_sample, ignoring waveform.\n"); + continue; + } + LogTrace("Bank %zd samplerate %s\n", bankIdx, Unit(Unit::UNIT_SAMPLERATE).PrettyPrint(FS_PER_SECOND/fs_per_sample).c_str()); for (auto& channel : bankChannels) From 8bd7bb29cace96292e10644df3bf62c2602d61b4 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 9 Dec 2025 16:43:36 +0100 Subject: [PATCH 37/59] RigolOscilloscope: `SetSampleRate`: ignore unsupported samplerate requests --- scopehal/RigolOscilloscope.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 7974765a..5dc5d293 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -3139,6 +3139,20 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) { lock_guard lock(m_cacheMutex); m_mdepth.reset(); + + bool requestedValueisValid {}; + auto rates = GetSampleRatesNonInterleaved(); + for (auto const& depth : rates) + if (depth == rate) + { + requestedValueisValid = true; + break; + } + if (not requestedValueisValid) + { + LogWarning("requested sample rate %" PRIu64 " is not any of %zd of suported by this device\n", rate, rates.size()); + return; + } } double sampletime = GetSampleDepth() / (double)rate; // locally cache current value before we change the timebase, From cc2d012b6572af34f8fe90a54744ad188f93c2be Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Sun, 14 Dec 2025 16:13:20 +0100 Subject: [PATCH 38/59] RigolOscilloscope: add missing `GetMso5000AnalogBankUsage` declaration --- scopehal/RigolOscilloscope.h | 1 + 1 file changed, 1 insertion(+) diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index a2d95dd4..0b310446 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -170,6 +170,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope void AnalyzeDeviceCapabilities(); void UpdateDynamicCapabilities(); // capabilities dependent on enabled chanel count std::size_t GetMsods1000ZChannelDivisor(); // helper function to get memory depth/sample rate divisor base on current scope state (amount of enabled channels) + std::array GetMso5000AnalogBankUsage(); bool IsChannelAnalog(std::size_t i); bool IsChannelDigital(std::size_t i); std::uint64_t GetPendingWaveformBlockLength(); // extratcs waveform block size from the TMC header at beginnign of each response to :WAV:DATA? command From ae3708444e2db4b6c8209d9bd329827bc5bb9f58 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 15:19:31 +0100 Subject: [PATCH 39/59] RigolOscilloscope: minor spelling/logging fixes --- scopehal/RigolOscilloscope.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 5dc5d293..bbf3d234 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -1587,7 +1587,7 @@ void RigolOscilloscope::SetChannelBandwidthLimit(size_t i, unsigned int limit_mh return 0; }(); - // `new_limit` now holds value of limit suported by the device or 0 in case of full BW + // `new_limit` now holds value of limit supported by the device or 0 in case of full BW if (new_limit > 0) m_transport->SendCommandQueued(m_channels[i]->GetHwname() + ":BWL " + to_string(new_limit) + "M"); @@ -2954,7 +2954,7 @@ uint64_t RigolOscilloscope::GetSampleRate() if(m_srate.has_value()) return *m_srate; - LogTrace("smaplerate updating\n"); + LogTrace("samplerate updating\n"); } // m_transport->SendCommandQueued("*WAI"); @@ -2966,7 +2966,7 @@ uint64_t RigolOscilloscope::GetSampleRate() { lock_guard lock(m_cacheMutex); m_srate = (uint64_t)rate; - LogTrace("smaplerate updated, m_srate %" PRIu64 "\n", *m_srate); + LogTrace("samplerate updated, m_srate %" PRIu64 "\n", *m_srate); return rate; } } @@ -3091,7 +3091,7 @@ void RigolOscilloscope::SetSampleDepth(uint64_t depth) m_transport->SendCommandQueued("ACQ:MDEP 500M"); break; default: - LogError("Invalid memory depth for channel: %" PRIu64 "\n", depth); + LogError("Invalid memory depth %" PRIu64 "\n", depth); } break; } From edb7073e0e9bd2f8acca2898c28f621da7e7b45f Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 15:21:25 +0100 Subject: [PATCH 40/59] RigolOscilloscope: only 2 ch. versions of DHO800 have EXT trigger --- scopehal/RigolOscilloscope.cpp | 42 +++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index bbf3d234..8c1ff5e8 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -144,25 +144,35 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) chan->SetDefaultDisplayName(); } } - // Add the external trigger input - switch (m_series) + auto const has_ext_trigger = [&]() -> bool { - case Series::DS1000: - case Series::MSODS1000Z: - case Series::DHO1000: - case Series::DHO4000: - case Series::DHO800: - case Series::DHO900: - m_extTrigChannel = new OscilloscopeChannel( - this, "External", "#FFFFFF", Unit(Unit::UNIT_FS), Unit(Unit::UNIT_VOLTS), Stream::STREAM_TYPE_TRIGGER, m_channels.size()); - m_channels.push_back(m_extTrigChannel); - m_extTrigChannel->SetDefaultDisplayName(); - break; + switch (m_series) + { + case Series::DHO800: + // from DHO800/900 prog. manual: "EXT is only available for DHO812 and DHO802" + // assuming it is based on analog channel count == 2 ... + return m_analogChannelCount == 2; - case Series::MSO5000: - case Series::UNKNOWN: - break; + case Series::DS1000: + case Series::MSODS1000Z: + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO900: + return true; + + case Series::MSO5000: + case Series::UNKNOWN: + return false; + } + return false; + }(); + if (has_ext_trigger) + { + m_extTrigChannel = new OscilloscopeChannel( + this, "External", "#FFFFFF", Unit(Unit::UNIT_FS), Unit(Unit::UNIT_VOLTS), Stream::STREAM_TYPE_TRIGGER, m_channels.size()); + m_channels.push_back(m_extTrigChannel); + m_extTrigChannel->SetDefaultDisplayName(); } // Add AC line external trigger input From dff319fda223980a3654bbaadcfeed79b400e169 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 15:23:00 +0100 Subject: [PATCH 41/59] RigolOscilloscope: DHO800 have max 25M memory depth note from release notes was misinterpretted 5M != 50M --- scopehal/RigolOscilloscope.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 8c1ff5e8..b1c9f09d 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -466,8 +466,7 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { if(m_bandwidth == 0) m_bandwidth = 70; // Fallback for DHO80x models m_highDefinition = true; - // DHO800/900 (DHO800 also have 50M memory since firmware v00.01.03.00.04 2024/07/11) - m_maxMdepth = 50*1000*1000; + m_maxMdepth = 25*1000*1000; m_maxSrate = 1.25*1000*1000*1000; m_lowSrate = true; @@ -482,7 +481,6 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { m_bandwidth = m_modelNew.number % 100 / 10 * 125; // 914 -> 24 -> 2 -> 250 m_highDefinition = true; - // DHO800/900 (DHO800 also have 50M memory since firmware v00.01.03.00.04 2024/07/11) m_maxMdepth = 50*1000*1000; m_maxSrate = 1.25*1000*1000*1000; m_lowSrate = true; From 4623b409b1f38f0ac9a0aeb56e21fa5be56dd4d8 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 15:26:25 +0100 Subject: [PATCH 42/59] RigolOscilloscope: `SetSampleDepth(...)` : ignore sample depths not in `GetSampleDepthsNonInterleaved()` (Rigol scopes do not support interleaved mode) --- scopehal/RigolOscilloscope.cpp | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index b1c9f09d..0bb57d9e 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -573,6 +573,7 @@ static std::vector dhoSampleDepths { 10 * 1000, 100 * 1000, 1 * 1000 * 1000, + 5 * 1000 * 1000, // only DHO800, since FW v00.01.03.00.04 10 * 1000 * 1000, 25 * 1000 * 1000, 50 * 1000 * 1000, @@ -616,11 +617,13 @@ void RigolOscilloscope::UpdateDynamicCapabilities() { vector depths; for (auto curMemDepth : dhoSampleDepths) { - if(curMemDepth<=maxMemDepth) - { - depths.push_back(curMemDepth); - } - else break; + if (curMemDepth>maxMemDepth) + break; + if (curMemDepth == 5'000'000 and m_series != Series::DHO800) + continue; + // v00.01.03.00.04 2024/07/11 + // 3. DHO800 adds 5M storage depth. + depths.push_back(curMemDepth); } m_depths = std::move(depths); return; @@ -3005,6 +3008,22 @@ uint64_t RigolOscilloscope::GetSampleDepth() void RigolOscilloscope::SetSampleDepth(uint64_t depth) { + { + auto depths = GetSampleDepthsNonInterleaved(); + auto requestedValueisValid = false; + for (auto const& depth_item : depths) + if (depth == depth_item) + { + requestedValueisValid = true; + break; + } + if (not requestedValueisValid) + { + LogWarning("requested sample depth %" PRIu64 " is not any of %zd of supported by this device\n", depth, depths.size()); + return; + } + } + switch (m_series) { case Series::MSO5000: @@ -3080,6 +3099,9 @@ void RigolOscilloscope::SetSampleDepth(uint64_t depth) case 1000000: m_transport->SendCommandQueued("ACQ:MDEP 1M"); break; + case 5000000: + m_transport->SendCommandQueued("ACQ:MDEP 5M"); + break; case 10000000: m_transport->SendCommandQueued("ACQ:MDEP 10M"); break; From 5ce9de80222567b45db840a8f394e92b4d7225fc Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 15:33:33 +0100 Subject: [PATCH 43/59] RigolOscilloscope: `SetSampleRate(...)`: clear `m_mdepth` only for relevant series and is is not clear, it is is even strictly necessary --- scopehal/RigolOscilloscope.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 0bb57d9e..7b6697b1 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -3167,20 +3167,17 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) { // Rigol scopes do not have samplerate controls. Only timebase can be adjusted :TIMebase:SCALe { - lock_guard lock(m_cacheMutex); - m_mdepth.reset(); - bool requestedValueisValid {}; auto rates = GetSampleRatesNonInterleaved(); - for (auto const& depth : rates) - if (depth == rate) + for (auto const& rate_item : rates) + if (rate_item == rate) { requestedValueisValid = true; break; } if (not requestedValueisValid) { - LogWarning("requested sample rate %" PRIu64 " is not any of %zd of suported by this device\n", rate, rates.size()); + LogWarning("requested sample rate %" PRIu64 " is not any of %zd of supported by this device\n", rate, rates.size()); return; } } @@ -3233,6 +3230,11 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) break; } + { + // TODO: is is really necessary to reset cached sample depth value? + lock_guard lock(m_cacheMutex); + m_mdepth.reset(); + } SetSampleDepth(GetSampleDepth()); // see note at MSODO1000Z break; } @@ -3261,6 +3263,11 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) // Another requirement is that this read has to happen after the acquisition finishes which could take a long time on samplerates // (this is visible even when you operate the scope manually) // Workaround to both is to (re-)write memory depth, don't ask me why. + { + // TODO: is is really necessary to reset cached sample depth value? + lock_guard lock(m_cacheMutex); + m_mdepth.reset(); + } SetSampleDepth(GetSampleDepth()); break; } From 9b01446f523f705f19ae4bf025cf759cd38e77e7 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 15:34:41 +0100 Subject: [PATCH 44/59] RigolOscilloscope: `SetSampleDepth(...)`: simplify for DHO series no need to match for supported rates as this is already checked at the beginning --- scopehal/RigolOscilloscope.cpp | 41 +--------------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 7b6697b1..2f0fe2bb 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -3084,47 +3084,8 @@ void RigolOscilloscope::SetSampleDepth(uint64_t depth) case Series::DHO4000: case Series::DHO800: case Series::DHO900: - { // DHO models - switch(depth) - { - case 1000: - m_transport->SendCommandQueued("ACQ:MDEP 1k"); - break; - case 10000: - m_transport->SendCommandQueued("ACQ:MDEP 10k"); - break; - case 100000: - m_transport->SendCommandQueued("ACQ:MDEP 100k"); - break; - case 1000000: - m_transport->SendCommandQueued("ACQ:MDEP 1M"); - break; - case 5000000: - m_transport->SendCommandQueued("ACQ:MDEP 5M"); - break; - case 10000000: - m_transport->SendCommandQueued("ACQ:MDEP 10M"); - break; - case 25000000: - m_transport->SendCommandQueued("ACQ:MDEP 25M"); - break; - case 50000000: - m_transport->SendCommandQueued("ACQ:MDEP 50M"); - break; - case 100000000: - m_transport->SendCommandQueued("ACQ:MDEP 100M"); - break; - case 250000000: - m_transport->SendCommandQueued("ACQ:MDEP 250M"); - break; - case 500000000: - m_transport->SendCommandQueued("ACQ:MDEP 500M"); - break; - default: - LogError("Invalid memory depth %" PRIu64 "\n", depth); - } + m_transport->SendCommandQueued("ACQ:MDEP " + to_string(depth)); break; - } case Series::MSODS1000Z: { From bb466ccda2e501cc1811a1a76e89497b05a915df Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 15:36:12 +0100 Subject: [PATCH 45/59] RigolOscilloscope: `SetSampleRate(...)`: add DHO quirk to immediately take effect At least on DHO800 series, the scope has to do at least one trigger after new samplerate (time base) is set, before it starts returning thenew value. RUN mode is not enough in this case. --- scopehal/RigolOscilloscope.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 2f0fe2bb..b57797fc 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -3167,6 +3167,18 @@ void RigolOscilloscope::SetSampleRate(uint64_t rate) timeScaleFactor = d->second; } m_transport->SendCommandQueued(string(":TIM:SCAL ") + to_string(sampletime / timeScaleFactor)); + // To ghet back the current samplerate, the scope has to trigger at least once! + // RUN mode is not enough and neither is fidlding with memory depth + auto singleshot = Trim(m_transport->SendCommandQueuedWithReply(":TRIG:SWE?")) == "SING"; + auto was_stopped = Trim(m_transport->SendCommandQueuedWithReply(":TRIG:STAT?")) == "STOP"; + if (was_stopped) + m_transport->SendCommandQueued("RUN"); + m_transport->SendCommandQueued("TFOR"); + if (was_stopped) + m_transport->SendCommandQueued("STOP"); + else if (singleshot) + m_transport->SendCommandQueued(":SING"); + break; } From a9e899a0300e9bd09588988dc1eec2e49f960a54 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 15:39:24 +0100 Subject: [PATCH 46/59] RigolOscilloscope: `SetTriggerOffset(...)`: DHO series: push new trigger time offset in RUN mode When in STOP, device limits the trigger offset changes to very limited range (at least DHO800) --- scopehal/RigolOscilloscope.cpp | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index b57797fc..f9705eff 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -3275,7 +3275,42 @@ void RigolOscilloscope::SetTriggerOffset(int64_t offset) auto halfwidth_fs = width_fs /2; if (offset > width_fs) offset = width_fs; // we want to ensure, the trigger is inside the capture range 0~mdepth + + // DHO800 allows very limited trigger offset range in stop mode + // let's assume this is behadior of all DHOs for now... + // TODO: check if it is necessary for other DHO series + + auto const resume_into_stop = [&]() -> bool + { + switch (m_series) + { + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + { + // Set in run mode to be able to set trigger offset in full range + if (Trim(m_transport->SendCommandQueuedWithReply(":TRIG:STAT?")) == "STOP") + { + m_transport->SendCommandQueued("RUN"); + return true; + } + return false; + } + + case Series::DS1000: + case Series::MSODS1000Z: + case Series::MSO5000: + case Series::UNKNOWN: + break; + } + return false; + }(); + m_transport->SendCommandQueued(string(":TIM:MAIN:OFFS ") + to_string((halfwidth_fs - offset) * SECONDS_PER_FS)); + + if (resume_into_stop) + m_transport->SendCommandQueued("STOP"); { lock_guard lock(m_cacheMutex); From 5f86684d34e56601659b780828bae0440b625f12 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 15:40:54 +0100 Subject: [PATCH 47/59] RigolOscilloscope: `GetExternalTrigger()`: return the m_extTrigChannel not sure why it always returned `nullptr` unconditionally --- scopehal/RigolOscilloscope.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index f9705eff..e8f18869 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -1711,8 +1711,7 @@ void RigolOscilloscope::SetChannelVoltageRange(size_t i, size_t /*stream*/, floa OscilloscopeChannel* RigolOscilloscope::GetExternalTrigger() { - //FIXME - return nullptr; + return m_extTrigChannel; } float RigolOscilloscope::GetChannelOffset(size_t i, size_t /*stream*/) From 29b5f863278967fe0294d8edc8415d0773a4a3a5 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 16:10:29 +0100 Subject: [PATCH 48/59] RigolOscilloscope: MSO5000 and DHO series: download waveform data all at once these scopes can send all at once making the transfer significantly faster (half the time) --- scopehal/RigolOscilloscope.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index e8f18869..6446be42 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -1934,19 +1934,16 @@ bool RigolOscilloscope::AcquireData() maxpoints = 1000 * 1000 / GetMsods1000ZChannelDivisor(); break; case Series::MSO5000: - maxpoints = GetSampleDepth(); //You can use 250E6 points too, but it is very slow - break; case Series::DHO1000: case Series::DHO4000: case Series::DHO800: case Series::DHO900: - maxpoints = 250 * 1000; + // keeping at 0, so it is updated to maximum from the value in preamble/memory depth break; case Series::UNKNOWN: return false; } vector temp_buf; - temp_buf.resize((m_highDefinition ? (maxpoints * 2) : maxpoints) + 1); map> pending_waveforms; auto ts_download_start = chrono::steady_clock::now(); @@ -2001,6 +1998,8 @@ bool RigolOscilloscope::AcquireData() continue; } + maxpoints = preamble->npoints; + if(preamble->sec_per_sample == 0) { // Sometimes the scope might return a null value for xincrement => ignore waveform to prenvent an Arithmetic exception in WaveformArea::RasterizeAnalogOrDigitalWaveform LogWarning("Got null sec_per_sample value from the scope, ignoring this waveform.\n"); @@ -2024,6 +2023,14 @@ bool RigolOscilloscope::AcquireData() return false; } + if (maxpoints == 0) + { + LogError("was not updated, this a bug! Ignoring waveform\n"); + continue; + } + + temp_buf.resize((m_highDefinition ? (maxpoints * 2) : maxpoints) + 1); + //If we have zero points in the reply, skip reading data from this channel if(npoints == 0) continue; From b39f95af998fddb162504cd405ee47abb9503619 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 16:20:08 +0100 Subject: [PATCH 49/59] RigolOscilloscope: unify data download for MSO5000/MSODS1000/DHO series the only difference are legacy DS1000 series --- scopehal/RigolOscilloscope.cpp | 64 +++++----------------------------- 1 file changed, 8 insertions(+), 56 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 6446be42..c8a1f377 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -2066,42 +2066,18 @@ bool RigolOscilloscope::AcquireData() case Series::DS1000: m_transport->SendCommandQueued(string(":WAV:DATA? ") + m_channels[channelIdx]->GetHwname()); break; - - case Series::MSODS1000Z: - { - // specify block sample range - m_transport->SendCommandQueued(string("WAV:STAR ") + to_string(npoint+1)); //ONE based indexing WTF - m_transport->SendCommandQueued(string("WAV:STOP ") + to_string(min(npoint + maxpoints, npoints))); //Here it is zero based, so it gets from 1-1000 - // m_transport->SendCommandQueued("*WAI"); // looks unnecessary - - //Ask for the data block - m_transport->SendCommandQueued("WAV:DATA?"); - } break; - + case Series::MSODS1000Z: case Series::MSO5000: - //Ask for the data block - m_transport->SendCommandQueued("*WAI"); - m_transport->SendCommandQueued("WAV:DATA?"); - break; - case Series::DHO1000: case Series::DHO4000: case Series::DHO800: case Series::DHO900: - { - //Ask for the data - m_transport->SendCommandQueued(string("WAV:STAR ") + to_string(npoint + 1)); //ONE based indexing WTF - size_t end = npoint + maxpoints; - if(end > npoints) - end = npoints; - m_transport->SendCommandQueued( - string("WAV:STOP ") + to_string(end)); //Here it is zero based, so it gets from 1-1000 - - //Ask for the data block + m_transport->SendCommandQueued("*WAI"); + m_transport->SendCommandQueued(string("WAV:STAR ") + to_string(npoint+1)); //ONE based indexing WTF + m_transport->SendCommandQueued(string("WAV:STOP ") + to_string(min(npoint + maxpoints, npoints))); m_transport->SendCommandQueued("WAV:DATA?"); break; - } case Series::UNKNOWN: LogError("RigolOscilloscope: unknown model, invalid state!\n"); @@ -2307,37 +2283,13 @@ bool RigolOscilloscope::AcquireData() break; case Series::MSODS1000Z: - { - // specify block sample range - m_transport->SendCommandQueued(string("WAV:STAR ") + to_string(npoint+1)); //ONE based indexing WTF - m_transport->SendCommandQueued(string("WAV:STOP ") + to_string(min(npoint + maxpoints, npoints))); //Here it is zero based, so it gets from 1-1000 - // m_transport->SendCommandQueued("*WAI"); // looks unnecessary - - //Ask for the data block - m_transport->SendCommandQueued("WAV:DATA?"); - } break; - - case Series::MSO5000: - //Ask for the data block - m_transport->SendCommandQueued("*WAI"); - m_transport->SendCommandQueued("WAV:DATA?"); - break; - case Series::DHO900: - { - //Ask for the data - m_transport->SendCommandQueued(string("WAV:STAR ") + to_string(npoint + 1)); //ONE based indexing WTF - size_t end = npoint + maxpoints; - if(end > npoints) - end = npoints; - m_transport->SendCommandQueued( - string("WAV:STOP ") + to_string(end)); //Here it is zero based, so it gets from 1-1000 - - //Ask for the data block + m_transport->SendCommandQueued("*WAI"); + m_transport->SendCommandQueued(string("WAV:STAR ") + to_string(npoint+1)); + m_transport->SendCommandQueued(string("WAV:STOP ") + to_string(min(npoint + maxpoints, npoints))); m_transport->SendCommandQueued("WAV:DATA?"); - break; - } + break; case Series::DHO1000: case Series::DHO4000: From ccbc2bb01abe0a12565c26087a58ad81253780b6 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 16:51:00 +0100 Subject: [PATCH 50/59] RigolOscilloscope: fix MSODS1000Z data download in recent commit, `maxpoints` got always updated from preamble, while it should happen only when it is zero --- scopehal/RigolOscilloscope.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index c8a1f377..2fab0fec 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -1998,7 +1998,8 @@ bool RigolOscilloscope::AcquireData() continue; } - maxpoints = preamble->npoints; + if (maxpoints == 0) + maxpoints = preamble->npoints; if(preamble->sec_per_sample == 0) { // Sometimes the scope might return a null value for xincrement => ignore waveform to prenvent an Arithmetic exception in WaveformArea::RasterizeAnalogOrDigitalWaveform From df869c70fe65803866c67fb6ba7fb01800bf2c01 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 16:56:25 +0100 Subject: [PATCH 51/59] RigolOscilloscope: update parsing heleprs to use `std::optional` this way, we can have invalid value even for integral types --- scopehal/RigolOscilloscope.cpp | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 2fab0fec..feacde25 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -46,15 +46,21 @@ using namespace std; template -static T parseNumeric(string const &input, const string &fmt, T const &fallback_value = {}) { - T output {fallback_value}; - sscanf(input.c_str(), fmt.c_str(), &output); +static optional parseNumeric(string const &input, const string &fmt) { + T output {}; + if (sscanf(input.c_str(), fmt.c_str(), &output) < 1) + return nullopt; return output; } -static auto parseDouble(string const &input, const string &fmt = "%lf", double const &fallback_value = numeric_limits::quiet_NaN()) +static auto parseDouble(string const &input, const string &fmt = "%lf") { - return parseNumeric(input, fmt, fallback_value); + return parseNumeric(input, fmt); +} + +static auto parseU64(string const &input, const string &fmt = "%" PRIu64) +{ + return parseNumeric(input, fmt); } //TODO: this would make sens to move to some utility library @@ -863,7 +869,14 @@ float RigolOscilloscope::GetDigitalThreshold(size_t channel) } // pods index values start at 1 - auto const level = parseDouble(m_transport->SendCommandQueuedWithReply(string(":LA:POD") + to_string(bankIdx + 1) + ":THR?")); + auto level = [&]() -> double { + auto const level_str = m_transport->SendCommandQueuedWithReply(string(":LA:POD") + to_string(bankIdx + 1) + ":THR?"); + auto const level = parseDouble(level_str); + if (level) + return *level; + LogError("could not parse channel %s threshold from %s\n", GetChannel(channel)->GetDisplayName().c_str(), level_str.c_str()); + return 0; + }(); { lock_guard lock(m_cacheMutex); From bee1f7dd57102b2826f20ff8ce6ac7c06b6238c1 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 16:57:47 +0100 Subject: [PATCH 52/59] RigolOscilloscope: when `GetSampleDepth()` fails to parse value, try to set lowest known mdepth and re-read this may happen if the scope memory depth i in AUTO mode and we can't work with that --- scopehal/RigolOscilloscope.cpp | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index feacde25..0a98000f 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -2964,11 +2964,21 @@ uint64_t RigolOscilloscope::GetSampleDepth() LogTrace("mem depth updating\n"); } - auto ret = Trim(m_transport->SendCommandQueuedWithReply(":ACQ:MDEP?")); - - double depth; - sscanf(ret.c_str(), "%lf", &depth); - //TODO: check sscanf return value for parsing errors + auto depth = [&]() -> uint64_t { + for(auto i = 2u; i; --i) + { + // it may fail in the first loop, but whould get valid value in the second + // this way we avoid recursion + auto const depth_str = m_transport->SendCommandQueuedWithReply(":ACQ:MDEP?"); + auto const depth = parseU64(depth_str); + if (depth) + return *depth; + LogError("could not sampled depth from %s. Falling back to lowest one\n", depth_str.c_str()); + + SetSampleDepth(GetSampleDepthsNonInterleaved()[0]); + } + return 0; + }(); { lock_guard lock(m_cacheMutex); From 68b3af8c0ea4c61cfb4f86101a755522d74ff080 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 17:38:00 +0100 Subject: [PATCH 53/59] RigolOscilloscope: default initialize all primitive member data for safety reasons --- scopehal/RigolOscilloscope.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index 0b310446..e4a14751 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -186,9 +186,9 @@ class RigolOscilloscope : public virtual SCPIOscilloscope OscilloscopeChannel* m_aclTrigChannel {}; // TODO: scopehal does not handle AC "external triggers" ATM // hardware analog channel count, independent of LA option etc - size_t m_analogChannelCount; + size_t m_analogChannelCount {}; - size_t m_digitalChannelCount; + size_t m_digitalChannelCount {}; std::vector m_digitalBanks; // config cache, values that can be updated whenever needed @@ -207,12 +207,12 @@ class RigolOscilloscope : public virtual SCPIOscilloscope std::optional m_laEnabled; // state variables, may alter values during runtime - bool m_triggerArmed; - bool m_triggerWasLive; - bool m_triggerOneShot; - std::uint_least32_t m_pointsWhenStarted; // used for some series as a part of trigger state detection workaround, sampled points reported right after arming + bool m_triggerArmed {}; + bool m_triggerWasLive {}; + bool m_triggerOneShot {}; + std::uint_least32_t m_pointsWhenStarted {}; // used for some series as a part of trigger state detection workaround, sampled points reported right after arming - bool m_liveMode; + bool m_liveMode {}; // constants once the ctor finishes struct Model { @@ -221,7 +221,7 @@ class RigolOscilloscope : public virtual SCPIOscilloscope std::string suffix; // e.g.: Z } m_modelNew; - unsigned int m_bandwidth; + unsigned int m_bandwidth {}; bool m_opt200M {}; // 200M memory depth is MSO5000 specific option bool m_opt24M {}; // 24M memory depth is DS1000Z specific option uint64_t m_maxMdepth {}; // Maximum Memory depth for DHO models From 268fc3ae40a5037c91b4147f6660edcdc796d779 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 17:52:38 +0100 Subject: [PATCH 54/59] RigolOscilloscope: some DS1000E notes, remains broken ATM --- scopehal/RigolOscilloscope.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 0a98000f..2d0304fe 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -205,7 +205,8 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) //Configure acquisition modes switch (m_series) { case Series::DS1000: - m_transport->SendCommandQueued(":WAV:POIN:MODE RAW"); + // m_transport->SendCommandQueued(":WAV:POIN:MODE RAW"); + // this command is not listed anywhere in the docs break; case Series::MSODS1000Z: @@ -362,6 +363,7 @@ void RigolOscilloscope::AnalyzeDeviceCapabilities() { case Series::DS1000: m_analogChannelCount = m_modelNew.number % 10; m_bandwidth = m_modelNew.number % 1000 - m_analogChannelCount; + //TODO: there are DS1000D devices with LA break; case Series::MSODS1000Z: @@ -1052,6 +1054,8 @@ bool RigolOscilloscope::IsChannelEnabled(size_t i) if (IsChannelAnalog(i)) { auto reply = Trim(m_transport->SendCommandQueuedWithReply(":" + m_channels[i]->GetHwname() + ":DISP?")); + // FIXME: DS1000E response times out, not sure why, prog. manual lists this command + // could be caused by some malformed command before { lock_guard lock(m_cacheMutex); From e5609198023861ed3f592d6be42484a049b91627 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 17:53:55 +0100 Subject: [PATCH 55/59] RigolOscilloscope: `GetSampleDepth` do not crash when mdepths are unknown --- scopehal/RigolOscilloscope.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 2d0304fe..b045c086 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -2977,8 +2977,13 @@ uint64_t RigolOscilloscope::GetSampleDepth() auto const depth = parseU64(depth_str); if (depth) return *depth; - LogError("could not sampled depth from %s. Falling back to lowest one\n", depth_str.c_str()); - + LogError("could not parse sampled depth from %s. Falling back to lowest one\n", depth_str.c_str()); + auto depths = GetSampleDepthsNonInterleaved(); + if (depths.empty()) + { + LogError("no known sampled depths known!\n"); + return 0; + } SetSampleDepth(GetSampleDepthsNonInterleaved()[0]); } return 0; From 716cdf0aec1de1084150d36e590bd6b5c837a116 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 18:23:44 +0100 Subject: [PATCH 56/59] RigolOscilloscope: fix: always parse sample memory depth as double --- scopehal/RigolOscilloscope.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index b045c086..7d3e7240 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -58,10 +58,10 @@ static auto parseDouble(string const &input, const string &fmt = "%lf") return parseNumeric(input, fmt); } -static auto parseU64(string const &input, const string &fmt = "%" PRIu64) -{ - return parseNumeric(input, fmt); -} +// static auto parseU64(string const &input, const string &fmt = "%" PRIu64) +// { +// return parseNumeric(input, fmt); +// } //TODO: this would make sens to move to some utility library template @@ -2974,7 +2974,7 @@ uint64_t RigolOscilloscope::GetSampleDepth() // it may fail in the first loop, but whould get valid value in the second // this way we avoid recursion auto const depth_str = m_transport->SendCommandQueuedWithReply(":ACQ:MDEP?"); - auto const depth = parseU64(depth_str); + auto const depth = parseDouble(depth_str); if (depth) return *depth; LogError("could not parse sampled depth from %s. Falling back to lowest one\n", depth_str.c_str()); From f621cd76878f79e8ca88aceafa7e7f350b7dc7eb Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 18:46:46 +0100 Subject: [PATCH 57/59] RigolOscilloscope: disable ato roll mode for DHOs --- scopehal/RigolOscilloscope.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 7d3e7240..88ea19d1 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -222,6 +222,23 @@ RigolOscilloscope::RigolOscilloscope(SCPITransport* transport) case Series::UNKNOWN: break; } + + // disable auto roll mode on DHOs + switch (m_series) { + case Series::DHO1000: + case Series::DHO4000: + case Series::DHO800: + case Series::DHO900: + m_transport->SendCommandQueued(":TIM:ROLL 0"); + break; + + case Series::DS1000: + case Series::MSODS1000Z: + case Series::MSO5000: + case Series::UNKNOWN: + break; + } + for(size_t i = 0; i < m_analogChannelCount; i++) m_transport->SendCommandQueued(":" + m_channels[i]->GetHwname() + ":VERN ON"); From fcbaf19216f77adeb582a69ac502494b52d04fc3 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 18:48:52 +0100 Subject: [PATCH 58/59] RigolOscilloscope: `PollTrigger()` enable `TD` state masking for all series because at least `ngscopeclient` starts the download when the scope enters triggered state, but the data are not ready yet. So we interpret `TD` as `TRIGGER_MODE_RUN` and flip to triggered/stop later --- scopehal/RigolOscilloscope.cpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/scopehal/RigolOscilloscope.cpp b/scopehal/RigolOscilloscope.cpp index 88ea19d1..b2014361 100644 --- a/scopehal/RigolOscilloscope.cpp +++ b/scopehal/RigolOscilloscope.cpp @@ -1860,16 +1860,13 @@ Oscilloscope::TriggerMode RigolOscilloscope::PollTrigger() if(stat == "TD") { - if (m_series == Series::MSO5000) - // MSO5000Z reports triggered right at the triggering moment, - // but that does not imply data availability (returning TRIGGER_MODE_TRIGGERED does). - // So we return TRIGGER_MODE_RUN to not indicate data availability - // and rely on the `STOP` to arrive afterwards. - // This can be easily caused by long sampling times (e.g.: 10 ksps and 100k mem. depth) - return TRIGGER_MODE_RUN; - else - // TODO: check if other families trigger TD state is also affected or not - return TRIGGER_MODE_TRIGGERED; + // MSO5000Z reports triggered right at the triggering moment, + // but that does not imply data availability (returning TRIGGER_MODE_TRIGGERED does). + // So we return TRIGGER_MODE_RUN to not indicate data availability + // and rely on the `STOP` to arrive afterwards. + // This can be easily caused by long sampling times (e.g.: 10 ksps and 100k mem. depth) + // DHO800 is also affected, so we mask the TD state for all + return TRIGGER_MODE_RUN; } else if(stat == "RUN") return TRIGGER_MODE_RUN; From 40a14db520b2f487a91f8d929b102f66ddfbd345 Mon Sep 17 00:00:00 2001 From: Patrik Bachan Date: Tue, 16 Dec 2025 18:50:15 +0100 Subject: [PATCH 59/59] RigolOscilloscope: default initialize all member data of `CapturePreamble` just in case so we don't read some garbage --- scopehal/RigolOscilloscope.h | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scopehal/RigolOscilloscope.h b/scopehal/RigolOscilloscope.h index e4a14751..993acc54 100644 --- a/scopehal/RigolOscilloscope.h +++ b/scopehal/RigolOscilloscope.h @@ -151,16 +151,16 @@ class RigolOscilloscope : public virtual SCPIOscilloscope std::size_t GetEnabledAnalogChannelCount(); struct CapturePreamble { - CaptureFormat format; - CaptureType type; - std::uint_least32_t npoints; // an integer between 1 and 12000000. - std::uint_least32_t averages; // the number of averages in the average sample mode and 1 in other modes. - double sec_per_sample; // the time difference between two neighboring points in the X direction. - double xorigin; // the time from the trigger point to the "Reference Time" in the X direction. - double xreference; // the reference time of the data point in the X direction. - double yincrement; // the waveform increment in the Y direction. - double yorigin; // the vertical offset relative to the "Vertical Reference Position" in the Y direction. - double yreference; // the vertical reference position in the Y direction. + CaptureFormat format {}; + CaptureType type {}; + std::uint_least32_t npoints {}; // an integer between 1 and 12000000. + std::uint_least32_t averages {}; // the number of averages in the average sample mode and 1 in other modes. + double sec_per_sample {}; // the time difference between two neighboring points in the X direction. + double xorigin {}; // the time from the trigger point to the "Reference Time" in the X direction. + double xreference {}; // the reference time of the data point in the X direction. + double yincrement {}; // the waveform increment in the Y direction. + double yorigin {}; // the vertical offset relative to the "Vertical Reference Position" in the Y direction. + double yreference {}; // the vertical reference position in the Y direction. }; std::optional GetCapturePreamble();