From ae963b6403bb88f0db2d32a2263e82d18194e468 Mon Sep 17 00:00:00 2001 From: fredzo Date: Wed, 3 Dec 2025 18:58:43 +0100 Subject: [PATCH 01/33] First step to magnova driver. --- scopehal/CMakeLists.txt | 1 + scopehal/MagnovaOscilloscope.cpp | 3349 ++++++++++++++++++++++++++++++ scopehal/MagnovaOscilloscope.h | 357 ++++ scopehal/scopehal.cpp | 2 + 4 files changed, 3709 insertions(+) create mode 100644 scopehal/MagnovaOscilloscope.cpp create mode 100644 scopehal/MagnovaOscilloscope.h diff --git a/scopehal/CMakeLists.txt b/scopehal/CMakeLists.txt index cab9c298..509fd249 100644 --- a/scopehal/CMakeLists.txt +++ b/scopehal/CMakeLists.txt @@ -166,6 +166,7 @@ set(SCOPEHAL_SOURCES KeysightDCA.cpp LeCroyOscilloscope.cpp LeCroyFWPOscilloscope.cpp + MagnovaOscilloscope.cpp MockOscilloscope.cpp MultiLaneBERT.cpp NanoVNA.cpp diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp new file mode 100644 index 00000000..4cdb8fa1 --- /dev/null +++ b/scopehal/MagnovaOscilloscope.cpp @@ -0,0 +1,3349 @@ +/*********************************************************************************************************************** +* * +* libscopehal * +* * +* Copyright (c) 2012-2024 Andrew D. Zonenberg and contributors * +* All rights reserved. * +* * +* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * +* following conditions are met: * +* * +* * Redistributions of source code must retain the above copyright notice, this list of conditions, and the * +* following disclaimer. * +* * +* * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * +* following disclaimer in the documentation and/or other materials provided with the distribution. * +* * +* * Neither the name of the author nor the names of any contributors may be used to endorse or promote products * +* derived from this software without specific prior written permission. * +* * +* THIS SOFTWARE IS PROVIDED BY THE AUTHORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * +* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * +* THE AUTHORS BE HELD LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * +* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * +* POSSIBILITY OF SUCH DAMAGE. * +* * +***********************************************************************************************************************/ + +/* + * Generic Magnova scope driver. Currently supports Batronix BMO models. + */ + +#include "scopehal.h" +#include "MagnovaOscilloscope.h" +#include "base64.h" + +#include "DropoutTrigger.h" +#include "EdgeTrigger.h" +#include "PulseWidthTrigger.h" +#include "RuntTrigger.h" +#include "SlewRateTrigger.h" +#include "UartTrigger.h" +#include "WindowTrigger.h" + +#include +#include +#include +#include +#include +#include + +using namespace std; + +static const struct +{ + const char* name; + float val; +} c_sds2000xp_threshold_table[] = {{"TTL", 1.5F}, {"CMOS", 1.65F}, {"LVCMOS33", 1.65F}, {"LVCMOS25", 1.25F}, {NULL, 0}}; + +static const std::chrono::milliseconds c_trigger_delay(1000); // Delay required when forcing trigger +static const char* c_custom_thresh = "CUSTOM,"; // Prepend string for custom digital threshold +static const float c_thresh_thresh = 0.01f; // Zero equivalence threshold for fp comparisons + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Construction / destruction + +MagnovaOscilloscope::MagnovaOscilloscope(SCPITransport* transport) + : SCPIDevice(transport) + , SCPIInstrument(transport) + , m_digitalChannelCount(0) + , m_hasLA(false) + , m_hasDVM(false) + , m_hasFunctionGen(false) + , m_hasFastSampleRate(false) + , m_memoryDepthOption(0) + , m_hasI2cTrigger(false) + , m_hasSpiTrigger(false) + , m_hasUartTrigger(false) + , m_maxBandwidth(10000) + , m_triggerArmed(false) + , m_triggerOneShot(false) + , m_sampleRateValid(false) + , m_sampleRate(1) + , m_memoryDepthValid(false) + , m_memoryDepth(1) + , m_triggerOffsetValid(false) + , m_triggerOffset(0) +{ + //standard initialization + FlushConfigCache(); + IdentifyHardware(); + DetectBandwidth(); + DetectAnalogChannels(); + DetectOptions(); + SharedCtorInit(); + + //Figure out if scope is in low or high bit depth mode so we can download waveforms with the correct format + GetADCMode(0); +} + +string MagnovaOscilloscope::converse(const char* fmt, ...) +{ + string ret; + char opString[128]; + va_list va; + va_start(va, fmt); + vsnprintf(opString, sizeof(opString), fmt, va); + va_end(va); + + ret = m_transport->SendCommandQueuedWithReply(opString, false); + if(ret.length() == 0) + ret = m_transport->ReadReply(); // Sometimes the Magnova returns en empty string and then the actual reply + return ret; +} + +void MagnovaOscilloscope::sendOnly(const char* fmt, ...) +{ + char opString[128]; + va_list va; + + va_start(va, fmt); + vsnprintf(opString, sizeof(opString), fmt, va); + va_end(va); + + m_transport->SendCommandQueued(opString); +} + +void MagnovaOscilloscope::flush() +{ + m_transport->ReadReply(); +} + +void MagnovaOscilloscope::flushWaveformData() +{ // Read any pending waveform data until we get the double 0x0a end marker + uint8_t tmp; + while(true) + { + size_t avaiable = m_transport->ReadRawData(1, &tmp); + if(!avaiable) break; + if(tmp == 0x0a) + { + avaiable = m_transport->ReadRawData(1, &tmp); + if(!avaiable) break; + if(tmp == 0x0a) break; + } + } + m_transport->FlushRXBuffer(); +} + +void MagnovaOscilloscope::SharedCtorInit() +{ + //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); + + //Add the function generator output + if(m_hasFunctionGen) + { + //TODO: this is stupid, it shares the same name as our scope input! + //Is this going to break anything?? + m_awgChannel = new FunctionGeneratorChannel(this, "C1", "#808080", m_channels.size()); + m_channels.push_back(m_awgChannel); + m_awgChannel->SetDisplayName("AWG"); + } + else + m_awgChannel = nullptr; + + //Clear the state-change register to we get rid of any history we don't care about + PollTrigger(); + + //Enable deduplication for vertical axis commands once we know what we're dealing with + m_transport->DeduplicateCommand("OFFSET"); + m_transport->DeduplicateCommand("SCALE"); +} + +void MagnovaOscilloscope::ParseFirmwareVersion() +{ + //Check if version requires size workaround (1.3.9R6 and older) + m_fwMajorVersion = 0; + m_fwMinorVersion = 0; + m_fwPatchVersion = 0; + + sscanf(m_fwVersion.c_str(), "%d.%d.%d", + &m_fwMajorVersion, + &m_fwMinorVersion, + &m_fwPatchVersion); + LogDebug("Found version %d.%d.%d\n",m_fwMajorVersion,m_fwMinorVersion,m_fwPatchVersion); +} + +void MagnovaOscilloscope::IdentifyHardware() +{ + //Ask for the ID + string reply = converse("*IDN?"); + char vendor[128] = ""; + char model[128] = ""; + char serial[128] = ""; + char version[128] = ""; + if(4 != sscanf(reply.c_str(), "%127[^,],%127[^,],%127[^,],%127s", vendor, model, serial, version)) + { + LogError("Bad IDN response %s\n", reply.c_str()); + return; + } + m_vendor = vendor; + m_model = model; + m_serial = serial; + m_fwVersion = version; + + //Look up model info + m_modelid = MODEL_UNKNOWN; + + if(m_vendor.compare("Batronix") == 0) + { + if(m_model.compare("Magnova") == 0) + { + m_modelid = MODEL_MAGNOVA_BMO; + ParseFirmwareVersion(); + } + else + { + LogWarning("Model \"%s\" is unknown, available sample rates/memory depths may not be properly detected\n", + m_model.c_str()); + } + // TODO + // Only 5 ms for newer models + // m_transport->EnableRateLimiting(chrono::milliseconds(5)); + } + else + { + LogWarning("Vendor \"%s\" is unknown\n", m_vendor.c_str()); + } +} + +void MagnovaOscilloscope::DetectBandwidth() +{ + m_maxBandwidth = 0; + switch(m_modelid) + { + case MODEL_MAGNOVA_BMO: + m_maxBandwidth = 350; + break; + default: + LogWarning("No bandwidth detected for model \"%s\".\n", m_vendor.c_str()); + break; + } +} + +void MagnovaOscilloscope::DetectOptions() +{ // No OPT command for now on Magnova + // string options = converse("*OPT?"); + m_hasFunctionGen = true; + m_hasLA = true; +} + +/** + @brief Creates digital channels for the oscilloscope + */ + +void MagnovaOscilloscope::AddDigitalChannels(unsigned int count) +{ + m_digitalChannelCount = count; + m_analogAndDigitalChannelCount = m_analogChannelCount + m_digitalChannelCount; + m_digitalChannelBase = m_channels.size(); + + char chn[32]; + for(unsigned int i = 0; i < count; i++) + { + snprintf(chn, sizeof(chn), "D%u", i); + auto chan = new OscilloscopeChannel( + this, + chn, + GetDefaultChannelColor(m_channels.size()), + Unit(Unit::UNIT_FS), + Unit(Unit::UNIT_COUNTS), + Stream::STREAM_TYPE_DIGITAL, + m_channels.size()); + m_channels.push_back(chan); + m_digitalChannels.push_back(chan); + } +} + +/** + @brief Figures out how many analog channels we have, and add them to the device + + */ +void MagnovaOscilloscope::DetectAnalogChannels() +{ // 4 Channels on Magnova scopes + int nchans = 4; + for(int i = 0; i < nchans; i++) + { + //Hardware name of the channel + string chname = string("CH") + to_string(i+1); + + //Color the channels based on Magnova standard color sequence + //yellow-pink-cyan-green-lightgreen + string color = "#ffffff"; + switch(i % 4) + { + case 0: + color = "#fbff00ff"; + break; + + case 1: + color = "#f33404ff"; + break; + + case 2: + color = "#0077ffff"; + break; + + case 3: + color = "#04f810ff"; + break; + } + + //Create the channel + m_channels.push_back( + new OscilloscopeChannel( + this, + chname, + color, + Unit(Unit::UNIT_FS), + Unit(Unit::UNIT_VOLTS), + Stream::STREAM_TYPE_ANALOG, + i)); + } + m_analogChannelCount = nchans; + m_analogAndDigitalChannelCount = m_analogChannelCount + m_digitalChannelCount; +} + +MagnovaOscilloscope::~MagnovaOscilloscope() +{ +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Device information + +string MagnovaOscilloscope::GetDriverNameInternal() +{ + return "magnova"; +} + +OscilloscopeChannel* MagnovaOscilloscope::GetExternalTrigger() +{ + return m_extTrigChannel; +} + +void MagnovaOscilloscope::FlushConfigCache() +{ + lock_guard lock(m_cacheMutex); + + if(m_trigger) + delete m_trigger; + m_trigger = NULL; + + m_channelVoltageRanges.clear(); + m_channelOffsets.clear(); + m_channelsEnabled.clear(); + m_channelDeskew.clear(); + m_channelDigitalThresholds.clear(); + m_probeIsActive.clear(); + m_sampleRateValid = false; + m_memoryDepthValid = false; + m_triggerOffsetValid = false; + m_meterModeValid = false; + m_awgEnabled.clear(); + m_awgDutyCycle.clear(); + m_awgRange.clear(); + m_awgOffset.clear(); + m_awgFrequency.clear(); + m_awgShape.clear(); + m_awgImpedance.clear(); + m_adcModeValid = false; + + //Clear cached display name of all channels + for(auto c : m_channels) + { + if(GetInstrumentTypesForChannel(c->GetIndex()) & Instrument::INST_OSCILLOSCOPE) + c->ClearCachedDisplayName(); + } +} + +/** + @brief See what measurement capabilities we have + */ +unsigned int MagnovaOscilloscope::GetMeasurementTypes() +{ + unsigned int type = 0; + return type; +} + +/** + @brief See what features we have + */ +unsigned int MagnovaOscilloscope::GetInstrumentTypes() const +{ + unsigned int type = INST_OSCILLOSCOPE; + if(m_hasFunctionGen) + type |= INST_FUNCTION; + return type; +} + +uint32_t MagnovaOscilloscope::GetInstrumentTypesForChannel(size_t i) const +{ + if(m_awgChannel && (m_awgChannel->GetIndex() == i) ) + return Instrument::INST_FUNCTION; + + //If we get here, it's an oscilloscope channel + return Instrument::INST_OSCILLOSCOPE; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel configuration + +bool MagnovaOscilloscope::IsChannelEnabled(size_t i) +{ + //ext trigger should never be displayed + if(i == m_extTrigChannel->GetIndex()) + return false; + + //Early-out if status is in cache + { + lock_guard lock2(m_cacheMutex); + if(m_channelsEnabled.find(i) != m_channelsEnabled.end()) + return m_channelsEnabled[i]; + } + + //Analog + if(i < m_analogChannelCount) + { + //See if the channel is enabled, hide it if not + string reply; + + reply = converse(":CHAN%zu:STAT?", i + 1); + { + lock_guard lock2(m_cacheMutex); + m_channelsEnabled[i] = (reply.find("OFF") != 0); //may have a trailing newline, ignore that + } + } + else if(i < m_analogAndDigitalChannelCount) + { + //Digital + + //See if the channel is on (digital channel numbers are 0 based) + size_t nchan = i - m_analogChannelCount; + string str = converse(":DIG%zu:STAT?", nchan); + + lock_guard lock2(m_cacheMutex); + // OFF can bee "SUPPORT_OFF" if all digital channels are off + m_channelsEnabled[i] = (str == "ON") ? true : false; + } + + lock_guard lock2(m_cacheMutex); + return m_channelsEnabled[i]; +} + +void MagnovaOscilloscope::EnableChannel(size_t i) +{ + bool wasInterleaving = IsInterleaving(); + + //No need to lock the main mutex since sendOnly now pushes to the queue + + //If this is an analog channel, just toggle it + if(i < m_analogChannelCount) + { + sendOnly(":CHAN%zu:STAT ON", i + 1); + } + else if(i < m_analogAndDigitalChannelCount) + { + //Digital channel (digital channel numbers are 0 based) + sendOnly(":DIG%d:STAT ON", i - m_analogChannelCount); + } + else if(i == m_extTrigChannel->GetIndex()) + { + //Trigger can't be enabled + } + + lock_guard lock(m_cacheMutex); + m_channelsEnabled[i] = true; + + //Sample rate and memory depth can change if interleaving state changed + if(IsInterleaving() != wasInterleaving) + { + m_memoryDepthValid = false; + m_sampleRateValid = false; + } +} + +bool MagnovaOscilloscope::CanEnableChannel(size_t i) +{ + // Can enable all channels except trigger + return !(i == m_extTrigChannel->GetIndex()); +} + +void MagnovaOscilloscope::DisableChannel(size_t i) +{ + bool wasInterleaving = IsInterleaving(); + + { + lock_guard lock(m_cacheMutex); + m_channelsEnabled[i] = false; + } + + if(i < m_analogChannelCount) + { + sendOnly(":CHAN%zu:STAT OFF", i + 1); + } + else if(i < m_analogAndDigitalChannelCount) + { + //Digital channel + + //Disable this channel (digital channel numbers are 0 based) + sendOnly(":DIG%zu:STAT OFF", i - m_analogChannelCount); + } + else if(i == m_extTrigChannel->GetIndex()) + { + //Trigger can't be enabled + } + + //Sample rate and memory depth can change if interleaving state changed + if(IsInterleaving() != wasInterleaving) + { + m_memoryDepthValid = false; + m_sampleRateValid = false; + } +} + +vector MagnovaOscilloscope::GetAvailableCouplings(size_t /*i*/) +{ + vector ret; + + switch(m_modelid) + { + // -------------------------------------------------- + case MODEL_MAGNOVA_BMO: + ret.push_back(OscilloscopeChannel::COUPLE_DC_1M); + ret.push_back(OscilloscopeChannel::COUPLE_AC_1M); + ret.push_back(OscilloscopeChannel::COUPLE_DC_50); + ret.push_back(OscilloscopeChannel::COUPLE_AC_50); + ret.push_back(OscilloscopeChannel::COUPLE_GND); + break; + + // -------------------------------------------------- + default: + LogError("Unknown scope type\n"); + break; + // -------------------------------------------------- + } + return ret; +} + +OscilloscopeChannel::CouplingType MagnovaOscilloscope::GetChannelCoupling(size_t i) +{ + if(i >= m_analogChannelCount) + return OscilloscopeChannel::COUPLE_SYNTHETIC; + + string replyType; + string replyImp; + + m_probeIsActive[i] = false; + + replyType = Trim(converse(":CHAN%zu:COUP?", i + 1).substr(0, 2)); + replyImp = Trim(converse(":CHAN%zu:TERM?", i + 1).substr(0, 2)); + + if(replyType == "AC") + return (replyImp.find("ON") == 0) ? OscilloscopeChannel::COUPLE_AC_50 : OscilloscopeChannel::COUPLE_AC_1M; + else if(replyType == "DC") + return (replyImp.find("ON") == 0) ? OscilloscopeChannel::COUPLE_DC_50 : OscilloscopeChannel::COUPLE_DC_1M; + else if(replyType == "GN") + return OscilloscopeChannel::COUPLE_GND; + + //invalid + LogWarning("MagnovaOscilloscope::GetChannelCoupling got invalid coupling [%s] [%s]\n", + replyType.c_str(), + replyImp.c_str()); + return OscilloscopeChannel::COUPLE_SYNTHETIC; +} + +void MagnovaOscilloscope::SetChannelCoupling(size_t i, OscilloscopeChannel::CouplingType type) +{ + if(i >= m_analogChannelCount) + return; + + //Get the old coupling value first. + //This ensures that m_probeIsActive[i] is valid + GetChannelCoupling(i); + + //If we have an active probe, don't touch the hardware config + if(m_probeIsActive[i]) + return; + + switch(type) + { + case OscilloscopeChannel::COUPLE_AC_1M: + sendOnly(":CHAN%zu:COUP AC", i + 1); + sendOnly(":CHAN%zu:TERM OFF", i + 1); + break; + + case OscilloscopeChannel::COUPLE_DC_1M: + sendOnly(":CHAN%zu:COUP DC", i + 1); + sendOnly(":CHAN%zu:TERM OFF", i + 1); + break; + + case OscilloscopeChannel::COUPLE_DC_50: + sendOnly(":CHAN%zu:COUP DC", i + 1); + sendOnly(":CHAN%zu:TERM ON", i + 1); + break; + + case OscilloscopeChannel::COUPLE_AC_50: + sendOnly(":CHAN%zu:COUP AC", i + 1); + sendOnly(":CHAN%zu:TERM ON", i + 1); + break; + + //treat unrecognized as ground + case OscilloscopeChannel::COUPLE_GND: + default: + sendOnly(":CHAN%zu:COUP GND", i + 1); + break; + } +} + +double MagnovaOscilloscope::GetChannelAttenuation(size_t i) +{ + if(i >= m_analogChannelCount) + return 1; + + //TODO: support ext/10 + if(i == m_extTrigChannel->GetIndex()) + return 1; + + string reply; + + reply = converse(":CHAN%zu:DIV?", i + 1); + + int d; + sscanf(reply.c_str(), "%d", &d); + return 1/d; +} + +void MagnovaOscilloscope::SetChannelAttenuation(size_t i, double atten) +{ + if(i >= m_analogChannelCount) + return; + + if(atten <= 0) + return; + + //Get the old coupling value first. + //This ensures that m_probeIsActive[i] is valid + GetChannelCoupling(i); + + //Don't allow changing attenuation on active probes + { + lock_guard lock(m_cacheMutex); + if(m_probeIsActive[i]) + return; + } + + sendOnly(":CHAN%zu:DIV %d", i + 1, (int)(atten)); +} + +vector MagnovaOscilloscope::GetChannelBandwidthLimiters(size_t /*i*/) +{ + vector ret; + + switch(m_modelid) + { + case MODEL_MAGNOVA_BMO: + ret.push_back(0); + ret.push_back(20); + ret.push_back(50); + ret.push_back(100); + ret.push_back(200); + break; + // -------------------------------------------------- + default: + LogError("Unknown scope type\n"); + break; + // -------------------------------------------------- + } + + return ret; +} + +unsigned int MagnovaOscilloscope::GetChannelBandwidthLimit(size_t i) +{ + if(i >= m_analogChannelCount) + return 0; + + string reply; + + reply = converse(":CHAN%zu:FILT?", i + 1); + if(reply == "NONe") + return 0; + else if(reply == "AMPLitude") + return 0; + else if(reply == "20000000") + return 20; + else if(reply == "50000000") + return 50; + else if(reply == "100000000") + return 100; + else if(reply == "200000000") + return 200; + + LogWarning("MagnovaOscilloscope::GetChannelBandwidthLimit got invalid bwlimit %s\n", reply.c_str()); + return 0; +} + +void MagnovaOscilloscope::SetChannelBandwidthLimit(size_t i, unsigned int limit_mhz) +{ + switch(limit_mhz) + { + case 0: + sendOnly(":CHAN%zu:FILT NONe", i + 1); + break; + + case 20: + sendOnly(":CHAN%zu:FILT 20000000", i + 1); + break; + + case 50: + sendOnly(":CHAN%zu:FILT 50000000", i + 1); + break; + + case 100: + sendOnly(":CHAN%zu:FILT 100000000", i + 1); + break; + + case 200: + sendOnly(":CHAN%zu:FILT 200000000", i + 1); + break; + + default: + LogWarning("MagnovaOscilloscope::invalid bwlimit set request (%dMhz)\n", limit_mhz); + } +} + +bool MagnovaOscilloscope::CanInvert(size_t i) +{ + //All analog channels, and only analog channels, can be inverted + return (i < m_analogChannelCount); +} + +void MagnovaOscilloscope::Invert(size_t i, bool invert) +{ + if(i >= m_analogChannelCount) + return; + + sendOnly(":CHAN%zu:INV %s", i + 1, invert ? "ON" : "OFF"); +} + +bool MagnovaOscilloscope::IsInverted(size_t i) +{ + if(i >= m_analogChannelCount) + return false; + + string reply; + + reply = Trim(converse(":CHAN%zu:INV?", i + 1)); + return (reply == "ON"); +} + +void MagnovaOscilloscope::SetChannelDisplayName(size_t /* i */, string /* name */) +{ + // Not supported + return; +} + +string MagnovaOscilloscope::GetChannelDisplayName(size_t i) +{ + auto chan = GetOscilloscopeChannel(i); + if(!chan) + return ""; + // Not supported + return chan->GetHwname(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Triggering + +bool MagnovaOscilloscope::IsTriggerArmed() +{ + return m_triggerArmed; +} + +Oscilloscope::TriggerMode MagnovaOscilloscope::PollTrigger() +{ + //Read the Internal State Change Register + string sinr = ""; + + if(m_triggerForced) + { + // The force trigger completed, return the sample set + m_triggerForced = false; + m_triggerArmed = false; + return TRIGGER_MODE_TRIGGERED; + } + + sinr = converse(":STAT?"); + + //No waveform, but ready for one? + if((sinr == "WAITing")||(sinr == "RUNNing")) + { + m_triggerArmed = true; + return TRIGGER_MODE_RUN; + } + + if((sinr == "TRIGgered")) + { + return TRIGGER_MODE_TRIGGERED; + } + + //Stopped, no data available + if(sinr == "STOPped") + { + if(m_triggerArmed) + { + //Only mark the trigger as disarmed if this was a one-shot trigger. + //If this is a repeating trigger, we're still armed from the client's perspective, + //since AcquireData() will reset the trigger for the next acquisition. + if(m_triggerOneShot) + m_triggerArmed = false; + + return TRIGGER_MODE_TRIGGERED; + } + else + return TRIGGER_MODE_STOP; + } + return TRIGGER_MODE_RUN; +} + +std::optional MagnovaOscilloscope::parseMetadata( + const std::vector& data) { + + try { + Metadata metadata; + + // Helper function to read little-endian float + auto readFloat = [](const uint8_t* ptr) -> float { + uint32_t value = static_cast(ptr[0]) | + (static_cast(ptr[1]) << 8) | + (static_cast(ptr[2]) << 16) | + (static_cast(ptr[3]) << 24); + float result; + std::memcpy(&result, &value, sizeof(float)); + return result; + }; + + // Helper function to read little-endian uint32 + auto readUint32 = [](const uint8_t* ptr) -> uint32_t { + return static_cast(ptr[0]) | + (static_cast(ptr[1]) << 8) | + (static_cast(ptr[2]) << 16) | + (static_cast(ptr[3]) << 24); + }; + + // Read metadata in the correct order + const uint8_t* ptr = data.data(); + + // First three floats + float time_delta = readFloat(ptr); + ptr += sizeof(float); + float start_time = readFloat(ptr); + ptr += sizeof(float); + float end_time = readFloat(ptr); + ptr += sizeof(float); + + // Next two uint32s + uint32_t sample_start = readUint32(ptr); + ptr += sizeof(uint32_t); + uint32_t sample_length = readUint32(ptr); + ptr += sizeof(uint32_t); + + // Next two floats + float vertical_start = readFloat(ptr); + ptr += sizeof(float); + float vertical_step = readFloat(ptr); + ptr += sizeof(float); + + // Final uint32 + uint32_t sample_count = readUint32(ptr); + + // Assign all values + metadata.timeDelta = time_delta; + metadata.startTime = start_time; + metadata.endTime = end_time; + metadata.sampleStart = sample_start; + metadata.sampleLength = sample_length; + metadata.verticalStart = vertical_start; + metadata.verticalStep = vertical_step; + metadata.sampleCount = sample_count; + + return metadata; + } + catch (const std::exception& e) { + LogError("Error parsing metadata: %s.\n", e.what()); + } +} + +size_t MagnovaOscilloscope::ReadWaveformBlock(std::vector* data, std::function progress) +{ + //Read and discard data until we see the '#' + uint8_t tmp; + for(int i=0; i<20; i++) + { + m_transport->ReadRawData(1, &tmp); + if(tmp == '#') + break; + + //shouldn't ever get here + if(i == 19) + { + LogError("ReadWaveformBlock: threw away 20 bytes of data and never saw a '#'\n"); + // This is a protocol error, flush pending rx data + flush(); + // Stop aqcuisition after this protocol error + Stop(); + return 0; + } + } + + //Read length of the length field + m_transport->ReadRawData(1, &tmp); + int lengthOfLength = tmp - '0'; + + //Read the actual length field + char textlen[10] = {0}; + m_transport->ReadRawData(lengthOfLength, (unsigned char*)textlen); + uint32_t len = atoi(textlen); + + size_t readBytes = 0; + data->resize(len); + uint8_t* resultData = data->data(); + while(readBytes < len) + { + size_t newBytes = m_transport->ReadRawData(len-readBytes,resultData+readBytes,progress); + if(newBytes == 0) break; + readBytes += newBytes; + } + LogDebug("Got length %zu from scope, expected bytes = %" PRIu32 ".\n",readBytes, len); + + return readBytes; +} + +/** + @brief Optimized function for checking channel enable status en masse with less round trips to the scope + */ +void MagnovaOscilloscope::BulkCheckChannelEnableState() +{ + vector uncached; + + { + lock_guard lock(m_cacheMutex); + + //Check enable state in the cache. + for(unsigned int i = 0; i < m_analogAndDigitalChannelCount; i++) + { + if(m_channelsEnabled.find(i) == m_channelsEnabled.end()) + uncached.push_back(i); + } + } + + for(auto i : uncached) + { + string reply = (i < m_analogChannelCount) ? converse(":CHANNEL%d:SWITCH?", i + 1) : converse(":DIGITAL:D%d?", (i - m_analogChannelCount)); + // OFF can bee "SUPPORT_OFF" if all digital channels are off + m_channelsEnabled[i] = (reply == "ON") ? true : false; + } +} + +time_t MagnovaOscilloscope::ExtractTimestamp(const std::string& /* timeString */, double& basetime) +{ + /* + TIMESTAMP is shown as Reserved In Siglent data format. + This information is from LeCroy which uses the same wavedesc header. + Timestamp is a somewhat complex format that needs some shuffling around. + Timestamp starts at offset 296 bytes in the wavedesc + (296-303) double seconds + (304) byte minutes + (305) byte hours + (306) byte days + (307) byte months + (308-309) uint16 year + + TODO: during startup, query instrument for its current time zone + since the wavedesc reports instment local time + */ + //Yes, this cast is intentional. + //It assumes you're on a little endian system using IEEE754 64-bit float, but that applies to everything we support. + //cppcheck-suppress invalidPointerCast + // TODO + double fseconds = 0; /**reinterpret_cast(wavedesc + 296);*/ + uint8_t seconds = floor(fseconds); + basetime = fseconds - seconds; + time_t tnow = time(NULL); + struct tm tstruc; + +#ifdef _WIN32 + localtime_s(&tstruc, &tnow); +#else + localtime_r(&tnow, &tstruc); +#endif +/* + //Convert the instrument time to a string, then back to a tm + //Is there a better way to do this??? + //Naively poking "struct tm" fields gives incorrect results (scopehal-apps:#52) + //Maybe because tm_yday is inconsistent? + char tblock[64] = {0}; + snprintf(tblock, + sizeof(tblock), + "%d-%d-%d %d:%02d:%02d", + *reinterpret_cast(wavedesc + 308), + wavedesc[307], + wavedesc[306], + wavedesc[305], + wavedesc[304], + seconds); + locale cur_locale; + auto& tget = use_facet>(cur_locale); + istringstream stream(tblock); + ios::iostate state; + char format[] = "%F %T"; + tget.get(stream, time_get::iter_type(), stream, state, &tstruc, format, format + strlen(format));*/ + return mktime(&tstruc); +} + +/** + @brief Converts 16-bit ADC samples to floating point + */ +void MagnovaOscilloscope::Convert16BitSamples(float* pout, const uint16_t* pin, float gain, float offset, size_t count) +{ + //Divide large waveforms (>1M points) into blocks and multithread them + //TODO: tune split + if(count > 1000000) + { + //Round blocks to multiples of 64 samples for clean vectorization + size_t numblocks = omp_get_max_threads(); + size_t lastblock = numblocks - 1; + size_t blocksize = count / numblocks; + blocksize = blocksize - (blocksize % 64); + + #pragma omp parallel for + for(size_t i=0; i MagnovaOscilloscope::ProcessAnalogWaveform( + const std::vector& data, + size_t datalen, + uint32_t num_sequences, + time_t ttime, + double basetime, + double* wavetime, + int ch) +{ + vector ret; + + auto metadata = parseMetadata(data); + if(!metadata) + { + LogError("Could not parse metadta"); + return ret; + } + + // Get gain from vertical step + float v_gain = metadata->verticalStep/0xFFFF; + + // Get offset from vertical start + add channel offset + float v_off = (0 - metadata->verticalStart - GetChannelOffset(ch,0)); + + // Get interval from timedelta + float interval = metadata->timeDelta * FS_PER_SECOND; + + // Offset is null + double h_off = 0; + double h_off_frac = 0; + + + //Raw waveform data + size_t num_samples = metadata->sampleCount; + + size_t num_per_segment = num_samples / num_sequences; + + // Skip metadata + const uint8_t* ptr = data.data() + 32; + const uint16_t* wdata = reinterpret_cast(ptr); + + // float codes_per_div; + + LogDebug("\nV_Gain=%f, V_Off=%f, interval=%f, h_off=%f, h_off_frac=%f, datalen=%zu\n", + v_gain, + v_off, + interval, + h_off, + h_off_frac, + datalen); + + for(size_t j = 0; j < num_sequences; j++) + { + //Set up the capture we're going to store our data into + auto cap = new UniformAnalogWaveform; + cap->m_timescale = round(interval); + + cap->m_triggerPhase = h_off_frac; + cap->m_startTimestamp = ttime; + + //Parse the time + if(num_sequences > 1) + cap->m_startFemtoseconds = static_cast((basetime + wavetime[j * 2]) * FS_PER_SECOND); + else + cap->m_startFemtoseconds = static_cast(basetime * FS_PER_SECOND); + + cap->Resize(num_per_segment); + cap->PrepareForCpuAccess(); + + //Convert raw ADC samples to volts + Convert16BitSamples( + cap->m_samples.GetCpuPointer(), + (wdata + j * num_per_segment), + v_gain, + v_off, + num_per_segment); + + cap->MarkSamplesModifiedFromCpu(); + ret.push_back(cap); + } + + return ret; +} + +vector MagnovaOscilloscope::ProcessDigitalWaveform( + const std::vector& data, + size_t datalen, + uint32_t num_sequences, + time_t ttime, + double basetime, + double* wavetime, + int /*ch*/) +{ + vector ret; + + auto metadata = parseMetadata(data); + if(!metadata) + { + LogError("Could not parse metadta"); + return ret; + } + + + //cppcheck-suppress invalidPointerCast + float interval = metadata->timeDelta * FS_PER_SECOND; + + //Raw waveform data + size_t numSamples = datalen*8; + + //LogTrace("\nDigital, interval=%f, datalen=%zu\n", interval, datalen); + + //Get the client's local time. + //All we need from this is to know whether DST is active + tm now; + time_t tnow; + time(&tnow); + localtime_r(&tnow, &now); + + // Sample ratio between digital and analog + // TODO + int64_t digitalToAnalogSampleRatio = 1; //m_acqPoints / m_digitalAcqPoints; + + //We have each channel's data from start to finish before the next (no interleaving). + for(size_t numSeq = 0; numSeq < num_sequences; numSeq++) + { + SparseDigitalWaveform* cap = new SparseDigitalWaveform; + // Since the LA sample rate is a fraction of the sample rate of the analog channels, timescale needs to be updated accordingly + cap->m_timescale = round(interval)*digitalToAnalogSampleRatio; + cap->PrepareForCpuAccess(); + + //Capture timestamp + cap->m_startTimestamp = ttime; + //Parse the time + if(num_sequences > 1) + cap->m_startFemtoseconds = static_cast((basetime + wavetime[numSeq * 2]) * FS_PER_SECOND); + else + cap->m_startFemtoseconds = static_cast(basetime * FS_PER_SECOND); + + //Preallocate memory assuming no deduplication possible + cap->Resize(numSamples); + + size_t k = 0; + size_t sampleIndex = 0; + bool sampleValue = false; + bool lastSampleValue = false; + + + // Skip metadata + const uint8_t* ptr = data.data() + 32; + //Read and de-duplicate the other samples + const uint8_t* rawData = ptr; + for (size_t curByteIndex = 0; curByteIndex < datalen; curByteIndex++) + { + char samples = rawData[curByteIndex]; + for (int ii = 0; ii < 8; ii++, samples >>= 1) + { // Check if the current scope sample bit is set. + sampleValue = (samples & 0x1); + if((sampleIndex > 0) && (lastSampleValue == sampleValue) && ((sampleIndex + 3) < numSamples)) + { //Deduplicate consecutive samples with same value + cap->m_durations[k]++; + } + else + { //Nope, it toggled - store the new value + cap->m_offsets[k] = sampleIndex; + cap->m_durations[k] = 1; + cap->m_samples[k] = sampleValue; + lastSampleValue = sampleValue; + k++; + } + sampleIndex++; + } + } + + //Done, shrink any unused space + cap->Resize(k); + cap->m_offsets.shrink_to_fit(); + cap->m_durations.shrink_to_fit(); + cap->m_samples.shrink_to_fit(); + cap->MarkSamplesModifiedFromCpu(); + cap->MarkTimestampsModifiedFromCpu(); + + //See how much space we saved + //LogDebug("%zu samples deduplicated to %zu (%.1f %%)\n", numSamples, k, (k * 100.0f) / (numSamples)); + + //Done, save data and go on to next + ret.push_back(cap); + } + return ret; +} + +bool MagnovaOscilloscope::AcquireData() +{ + // Transfer buffers + std::vector* analogWaveformData[MAX_ANALOG] {nullptr}; + int analogWaveformDataSize[MAX_ANALOG] {0}; + std::vector* digitalWaveformDataBytes[MAX_DIGITAL] {nullptr}; + int digitalWaveformDataSize[MAX_DIGITAL] {0}; + std::string digitalWaveformData; + + //State for this acquisition (may be more than one waveform) + uint32_t num_sequences = 1; + map> pending_waveforms; + double start = GetTime(); + time_t ttime = 0; + double basetime = 0; + vector> waveforms; + vector> digitalWaveforms; + bool analogEnabled[MAX_ANALOG] = {false}; + bool digitalEnabled[MAX_DIGITAL] = {false}; + bool anyDigitalEnabled = false; + bool anyAnalogEnabled = true; + double* pwtime = NULL; + + //Acquire the data (but don't parse it) + + lock_guard lock(m_transport->GetMutex()); + start = GetTime(); + + // Get instrument time : format "23,35,11.280010" + string isntrumentTime = converse(":SYST:TIME?"); + + // Detect active channels + BulkCheckChannelEnableState(); + for(unsigned int i = 0; i < m_analogChannelCount; i++) + { // Check all analog channels + analogEnabled[i] = IsChannelEnabled(i); + anyAnalogEnabled |= analogEnabled[i]; + } + + for(unsigned int i = 0; i < m_digitalChannelCount; i++) + { // Check digital channels + // Not supported for now by Magnova firmware + /*digitalEnabled[i] = IsChannelEnabled(i+m_analogChannelCount); + anyDigitalEnabled |= digitalEnabled[i];*/ + digitalEnabled[i] = false; + } + + // Notify about download operation start + ChannelsDownloadStarted(); + + // Get time from instrument + ttime = ExtractTimestamp(isntrumentTime, basetime); + + //Read the data from each analog waveform + for(unsigned int i = 0; i < m_analogChannelCount; i++) + { + if(analogEnabled[i]) + { // Allocate buffer + analogWaveformData[i] = new std::vector; + // Run the same loop for paginated and unpagnated mode, if unpaginated we will run it only once + m_transport->SendCommand(":CHAN" + to_string(i + 1) + ":DATA:PACK? ALL,RAW"); + size_t readBytes = ReadWaveformBlock(analogWaveformData[i], [i, this] (float progress) { ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, progress); }); + analogWaveformDataSize[i] = readBytes; + LogDebug("Parsing metadata...\n"); + auto metadata = parseMetadata(*analogWaveformData[i]); + if(metadata) + { + LogDebug("Metadata parsed, starTime = %f\n",metadata->startTime); + } + ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_FINISHED, 1.0); + } + } + if(anyDigitalEnabled) + { + // uint64_t digitalAcqPoints = GetDigitalAcqPoints(); + // uint64_t acqDigitalBytes = ceil(digitalAcqPoints/8); // 8 points per byte on digital channels + // LogDebug("Digital acq : ratio = %lld, pages = %lld, page size = %lld , dig acq points = %lld, acq dig bytes = %lld.\n",(acqPoints / digitalAcqPoints),pages, pageSize,digitalAcqPoints, acqDigitalBytes); + //Read the data from each digital waveform + for(size_t i = 0; i < m_digitalChannelCount; i++) + { + if(digitalEnabled[i]) + { // Allocate buffer + digitalWaveformDataBytes[i] = new std::vector; + m_transport->SendCommand(":DIG" + to_string(i + 1) + ":DATA:PACK? ALL,RAW"); + size_t readBytes = ReadWaveformBlock(digitalWaveformDataBytes[i], [i, this] (float progress) { ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, progress); }); + digitalWaveformDataSize[i] = readBytes; + ChannelsDownloadStatusUpdate(i + m_analogChannelCount, InstrumentChannel::DownloadState::DOWNLOAD_FINISHED, 1.0); + } + } + } + + //At this point all data has been read so the scope is free to go do its thing while we crunch the results. + //Re-arm the trigger if not in one-shot mode + if(!m_triggerOneShot) + { + sendOnly(":SINGLE"); + m_triggerArmed = true; + } + + //Process analog waveforms + waveforms.resize(m_analogChannelCount); + for(unsigned int i = 0; i < m_analogChannelCount; i++) + { + if(analogEnabled[i]) + { + waveforms[i] = ProcessAnalogWaveform( + *analogWaveformData[i], + analogWaveformDataSize[i], + num_sequences, + ttime, + basetime, + pwtime, + i); + } + } + + //Save analog waveform data + for(unsigned int i = 0; i < m_analogChannelCount; i++) + { + if(!analogEnabled[i]) + continue; + + //Done, update the data + for(size_t j = 0; j < num_sequences; j++) + pending_waveforms[i].push_back(waveforms[i][j]); + } + + //Process digital waveforms + digitalWaveforms.resize(m_digitalChannelCount); + for(unsigned int i = 0; i < m_digitalChannelCount; i++) + { + if(digitalEnabled[i]) + { + digitalWaveforms[i] = ProcessDigitalWaveform( + *digitalWaveformDataBytes[i], + digitalWaveformDataSize[i], + num_sequences, + ttime, + basetime, + pwtime, + i); + } + } + + //Save digital waveform data + for(unsigned int i = 0; i < m_digitalChannelCount; i++) + { + if(!digitalEnabled[i]) + continue; + + //Done, update the data + for(size_t j = 0; j < num_sequences; j++) + pending_waveforms[i+m_analogChannelCount].push_back(digitalWaveforms[i][j]); + } + + // Tell the download monitor that waveform download has finished + ChannelsDownloadFinished(); + + //Now that we have all of the pending waveforms, save them in sets across all channels + m_pendingWaveformsMutex.lock(); + for(size_t i = 0; i < num_sequences; i++) + { + SequenceSet s; + for(size_t j = 0; j < m_analogAndDigitalChannelCount; j++) + { + if(pending_waveforms.find(j) != pending_waveforms.end()) + s[GetOscilloscopeChannel(j)] = pending_waveforms[j][i]; + } + m_pendingWaveforms.push_back(s); + } + m_pendingWaveformsMutex.unlock(); + + //Clean up + for(int i = 0; i < MAX_ANALOG; i++) + { + if(analogWaveformData[i] != nullptr) + delete analogWaveformData[i]; + } + for(int i = 0; i < MAX_DIGITAL; i++) + { + if(digitalWaveformDataBytes[i] != nullptr) + delete digitalWaveformDataBytes[i]; + } + + double dt = GetTime() - start; + LogTrace("Waveform download and processing took %.3f ms\n", dt * 1000); + return true; +} + +void MagnovaOscilloscope::PrepareAcquisition() +{ + m_triggerOffsetValid = false; + m_channelOffsets.clear(); + // m_transport->SendCommand(":WAVEFORM:START 0"); +} + +void MagnovaOscilloscope::Start() +{ + PrepareAcquisition(); + sendOnly(":STOP"); + sendOnly(":SINGLE"); //always do single captures, just re-trigger + + m_triggerArmed = true; + m_triggerOneShot = false; +} + +void MagnovaOscilloscope::StartSingleTrigger() +{ + //LogDebug("Start single trigger\n"); + + PrepareAcquisition(); + sendOnly(":STOP"); + sendOnly(":SINGLE"); + + m_triggerArmed = true; + m_triggerOneShot = true; +} + +void MagnovaOscilloscope::Stop() +{ + if(!m_triggerArmed) + return; + + m_transport->SendCommandImmediate(":STOP"); + + m_triggerArmed = false; + m_triggerOneShot = true; + + //Clear out any pending data (the user doesn't want it, and we don't want stale stuff hanging around) + ClearPendingWaveforms(); +} + +void MagnovaOscilloscope::ForceTrigger() +{ + // Don't allow more than one force at a time + if(m_triggerForced) + return; + + m_triggerForced = true; + + PrepareAcquisition(); + sendOnly(":SINGLE"); + if(!m_triggerArmed) + sendOnly(":SINGLE"); + + m_triggerArmed = true; + this_thread::sleep_for(c_trigger_delay); +} + +float MagnovaOscilloscope::GetChannelOffset(size_t i, size_t /*stream*/) +{ + //not meaningful for trigger or digital channels + if(i >= m_analogChannelCount) + return 0; + + { + lock_guard lock(m_cacheMutex); + + if(m_channelOffsets.find(i) != m_channelOffsets.end()) + return m_channelOffsets[i]; + } + + string reply; + + reply = converse(":CHANNEL%zu:OFFSET?", i + 1); + + float offset; + sscanf(reply.c_str(), "%f", &offset); + + lock_guard lock(m_cacheMutex); + m_channelOffsets[i] = offset; + return offset; +} + +void MagnovaOscilloscope::SetChannelOffset(size_t i, size_t /*stream*/, float offset) +{ + //not meaningful for trigger or digital channels + if(i >= m_analogChannelCount) + return; + + sendOnly(":CHANNEL%zu:OFFSET %1.2E", i + 1, offset); + + lock_guard lock(m_cacheMutex); + m_channelOffsets[i] = offset; +} + +float MagnovaOscilloscope::GetChannelVoltageRange(size_t i, size_t /*stream*/) +{ + //not meaningful for trigger or digital channels + if(i >= m_analogChannelCount) + return 1; + + { + lock_guard lock(m_cacheMutex); + if(m_channelVoltageRanges.find(i) != m_channelVoltageRanges.end()) + return m_channelVoltageRanges[i]; + } + + string reply; + + reply = converse(":CHANNEL%zu:SCALE?", i + 1); + + float volts_per_div; + sscanf(reply.c_str(), "%f", &volts_per_div); + + float v = volts_per_div * 8; //plot is 8 divisions high + lock_guard lock(m_cacheMutex); + m_channelVoltageRanges[i] = v; + return v; +} + +void MagnovaOscilloscope::SetChannelVoltageRange(size_t i, size_t /*stream*/, float range) +{ + // Only for analog channels + if(i >= m_analogChannelCount) + return; + + float vdiv = range / 8; + m_channelVoltageRanges[i] = range; + + sendOnly(":CHANNEL%zu:SCALE %.4f", i + 1, vdiv); +} + +vector MagnovaOscilloscope::GetSampleRatesNonInterleaved() +{ // Changing srate is not supported by Magnova scope + vector ret; + return ret; +} + +vector MagnovaOscilloscope::GetSampleRatesInterleaved() +{ + return GetSampleRatesNonInterleaved(); +} + +vector MagnovaOscilloscope::GetSampleDepthsNonInterleaved() +{ + vector ret; + // TODO + ret = {20 * 1000, 200 * 1000, 2000 * 1000, 20 * 1000 * 1000, 200 * 1000 * 1000}; + return ret; +} + +vector MagnovaOscilloscope::GetSampleDepthsInterleaved() +{ + return GetSampleDepthsNonInterleaved(); +} + +set MagnovaOscilloscope::GetInterleaveConflicts() +{ + set ret; + + //All scopes normally interleave channels 1/2 and 3/4. + //If both channels in either pair is in use, that's a problem. + ret.emplace(InterleaveConflict(GetOscilloscopeChannel(0), GetOscilloscopeChannel(1))); + if(m_analogChannelCount > 2) + ret.emplace(InterleaveConflict(GetOscilloscopeChannel(2), GetOscilloscopeChannel(3))); + + return ret; +} + +uint64_t MagnovaOscilloscope::GetSampleRate() +{ + double f; + if(!m_sampleRateValid) + { + string reply; + reply = converse(":ACQUIRE:SRATE?"); + + if(sscanf(reply.c_str(), "%lf", &f) != EOF) + { + m_sampleRate = static_cast(f); + m_sampleRateValid = true; + } + else + { + + } + } + return m_sampleRate; +} + +uint64_t MagnovaOscilloscope::GetSampleDepth() +{ + double f; + if(!m_memoryDepthValid) + { // Possible values are : AUTo, AFASt, Integer in pts + string reply = converse(":ACQUIRE:MDEPTH?"); + if(reply == "AUTo" || reply == "AFASt") + { + m_memoryDepth = 0; + m_memoryDepthValid = true; + } + else + { + f = Unit(Unit::UNIT_SAMPLEDEPTH).ParseString(reply); + m_memoryDepth = static_cast(f); + m_memoryDepthValid = true; + } + } + return m_memoryDepth; +} + +void MagnovaOscilloscope::SetSampleDepth(uint64_t depth) +{ + //Need to lock the mutex when setting depth because of the quirks around needing to change trigger mode too + lock_guard lock(m_transport->GetMutex()); + + //Save original sample rate (scope often changes sample rate when adjusting memory depth) + // uint64_t rate = GetSampleRate(); + // Get Trigger State to restore it after setting changing memory detph + // TriggerMode triggerMode = PollTrigger(); + + // we can not change memory size in Run/Stop mode + // sendOnly(":AUTO"); + switch(m_modelid) + { + case MODEL_MAGNOVA_BMO: + sendOnly("ACQUIRE:MDEPTH %" PRIu64 "",depth); + break; + // -------------------------------------------------- + default: + LogError("Unknown scope type\n"); + break; + // -------------------------------------------------- + } + /*if(IsTriggerArmed()) + { // restart trigger + sendOnly(":SINGLE"); + } + else + { // Restore previous trigger mode + sendOnly(":%s", ((triggerMode == TRIGGER_MODE_STOP) ? "STOP" : "AUTO")); + }*/ + + m_memoryDepthValid = false; + + //restore old sample rate + //SetSampleRate(rate); +} + +void MagnovaOscilloscope::SetSampleRate(uint64_t /*rate*/) +{ // Not supported by Magnova + return; +} + +void MagnovaOscilloscope::EnableTriggerOutput() +{ + sendOnly(":TRIG:AOUT ON"); +} + +void MagnovaOscilloscope::SetUseExternalRefclk(bool external) +{ + switch(m_modelid) + { + case MODEL_MAGNOVA_BMO: + sendOnly(":ACQuire:RCLock %s", external ? "EXT" : "INT"); + break; + + default: + LogError("Unknown scope type\n"); + break; + } + +} + +void MagnovaOscilloscope::SetTriggerOffset(int64_t offset) +{ + //Magnova's 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)); + + sendOnly(":TIMebase:OFFSet %1.2E", (offset - halfwidth) * SECONDS_PER_FS); + + //Don't update the cache because the scope is likely to round the offset we ask for. + //If we query the instrument later, the cache will be updated then. + lock_guard lock2(m_cacheMutex); + m_triggerOffsetValid = false; +} + +int64_t MagnovaOscilloscope::GetTriggerOffset() +{ + //Early out if the value is in cache + { + lock_guard lock(m_cacheMutex); + if(m_triggerOffsetValid) + return m_triggerOffset; + } + string reply; + reply = converse(":TIMebase:OFFSet?"); + + lock_guard lock(m_cacheMutex); + + //Result comes back in scientific notation + double sec; + sscanf(reply.c_str(), "%le", &sec); + m_triggerOffset = static_cast(round(sec * 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_triggerOffsetValid = true; + + return m_triggerOffset; +} + +void MagnovaOscilloscope::SetDeskewForChannel(size_t channel, int64_t skew) +{ + //Cannot deskew trigger channel + if(channel >= m_analogAndDigitalChannelCount) + return; + if(channel < m_analogChannelCount) + { + sendOnly(":CHAN%zu:DESK %1.2E", channel, skew * SECONDS_PER_FS); + } + else + { // Digital channels + sendOnly(":DIG:DESK%s %1.2E", ((channel - m_digitalChannelBase) < 8) ? "0to7" : "8to15", skew * SECONDS_PER_FS); + } + + //Update cache + lock_guard lock2(m_cacheMutex); + m_channelDeskew[channel] = skew; +} + +int64_t MagnovaOscilloscope::GetDeskewForChannel(size_t channel) +{ + //Cannot deskew trigger channel + if(channel >= m_analogAndDigitalChannelCount) + return 0; + + //Early out if the value is in cache + { + lock_guard lock(m_cacheMutex); + if(m_channelDeskew.find(channel) != m_channelDeskew.end()) + return m_channelDeskew[channel]; + } + + //Read the deskew + string reply; + if(channel < m_analogChannelCount) + { + reply = converse(":CHAN%zu:DESK?", channel + 1); + } + else + { // Digital channels + reply = converse(":DIG:DESK%s?", ((channel - m_digitalChannelBase) < 8) ? "0to7" : "8to15"); + } + + //Value comes back as floating point ps + float skew; + sscanf(reply.c_str(), "%f", &skew); + int64_t skew_ps = round(skew * FS_PER_SECOND); + + lock_guard lock2(m_cacheMutex); + m_channelDeskew[channel] = skew_ps; + + return skew_ps; +} + +bool MagnovaOscilloscope::IsInterleaving() +{ + switch(m_modelid) + { + case MODEL_MAGNOVA_BMO: + if((m_channelsEnabled[2] == true) || (m_channelsEnabled[3] == true)) + { // Interleaving if Channel 3 or 4 are active + return true; + } + return false; + // -------------------------------------------------- + default: + LogError("Unknown scope type\n"); + return false; + // -------------------------------------------------- + } +} + +bool MagnovaOscilloscope::SetInterleaving(bool /* combine*/) +{ + //Setting interleaving is not supported, it's always hardware managed + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Analog bank configuration + +bool MagnovaOscilloscope::IsADCModeConfigurable() +{ + return false; +} + +vector MagnovaOscilloscope::GetADCModeNames(size_t /*channel*/) +{ + vector v; + return v; +} + +size_t MagnovaOscilloscope::GetADCMode(size_t /*channel*/) +{ + return 0; +} + +void MagnovaOscilloscope::SetADCMode(size_t /*channel*/, size_t /* mode */) +{ + return; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Logic analyzer configuration + +vector MagnovaOscilloscope::GetDigitalBanks() +{ + vector banks; + + if(m_hasLA) + { + for(size_t n = 0; n < 2; n++) + { + DigitalBank bank; + + for(size_t i = 0; i < 8; i++) + bank.push_back(m_digitalChannels[i + n * 8]); + + banks.push_back(bank); + } + } + + return banks; +} + +Oscilloscope::DigitalBank MagnovaOscilloscope::GetDigitalBank(size_t channel) +{ + DigitalBank ret; + if(m_hasLA) + { + if(channel <= m_digitalChannels[7]->GetIndex()) + { + for(size_t i = 0; i < 8; i++) + ret.push_back(m_digitalChannels[i]); + } + else + { + for(size_t i = 0; i < 8; i++) + ret.push_back(m_digitalChannels[i + 8]); + } + } + return ret; +} + +bool MagnovaOscilloscope::IsDigitalHysteresisConfigurable() +{ + return false; +} + +bool MagnovaOscilloscope::IsDigitalThresholdConfigurable() +{ + return true; +} + +float MagnovaOscilloscope::GetDigitalHysteresis(size_t /*channel*/) +{ + return 0; +} + +float MagnovaOscilloscope::GetDigitalThreshold(size_t channel) +{ + if( (channel < m_digitalChannelBase) || (m_digitalChannelCount == 0) ) + return 0; + + channel -= m_analogChannelCount; + { + lock_guard lock(m_cacheMutex); + + if(m_channelDigitalThresholds.find(channel) != m_channelDigitalThresholds.end()) + return m_channelDigitalThresholds[channel]; + } + + float result = 0.0f; + + string r = converse(":DIGITAL:THRESHOLD%d?", (channel / 8) + 1).c_str(); + + // Look through the threshold table to see if theres a string match, return it if so + uint32_t i = 0; + while((c_sds2000xp_threshold_table[i].name) && + (strncmp(c_sds2000xp_threshold_table[i].name, r.c_str(), strlen(c_sds2000xp_threshold_table[i].name)))) + i++; + + if(c_sds2000xp_threshold_table[i].name) + { + result = c_sds2000xp_threshold_table[i].val; + } + else if(!strncmp(r.c_str(), c_custom_thresh, strlen(c_custom_thresh))) + { // Didn't match a standard, check for custom + result = strtof(&(r.c_str()[strlen(c_custom_thresh)]), NULL); + } + else + { + LogWarning("GetDigitalThreshold unrecognised value [%s]\n", r.c_str()); + } + + lock_guard lock(m_cacheMutex); + m_channelDigitalThresholds[channel] = result; + return result; +} + +void MagnovaOscilloscope::SetDigitalHysteresis(size_t /*channel*/, float /*level*/) +{ + LogWarning("SetDigitalHysteresis is not implemented\n"); +} + +void MagnovaOscilloscope::SetDigitalThreshold(size_t channel, float level) +{ + channel -= m_analogChannelCount; + + // Search through standard thresholds to see if one matches + uint32_t i = 0; + while(( + (c_sds2000xp_threshold_table[i].name) && (fabsf(level - c_sds2000xp_threshold_table[i].val)) > c_thresh_thresh)) + i++; + + if(c_sds2000xp_threshold_table[i].name) + sendOnly(":DIGITAL:THRESHOLD%zu %s", (channel / 8) + 1, (c_sds2000xp_threshold_table[i].name)); + else + { + do + { + sendOnly(":DIGITAL:THRESHOLD%zu CUSTOM,%1.2E", (channel / 8) + 1, level); + + } while(fabsf((GetDigitalThreshold(channel + m_analogChannelCount) - level)) > 0.1f); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Triggering + +void MagnovaOscilloscope::PullTrigger() +{ + std::string reply; + + bool isUart = false; + //Figure out what kind of trigger is active. + reply = Trim(converse(":TRIGGER:TYPE?")); + if(reply == "DROPout") + PullDropoutTrigger(); + else if(reply == "EDGe") + PullEdgeTrigger(); + else if(reply == "RUNT") + PullRuntTrigger(); + else if(reply == "SLOPe") + PullSlewRateTrigger(); + else if(reply == "DECode") + { + PullUartTrigger(); + isUart = true; + } + else if(reply == "INTerval") + PullPulseWidthTrigger(); + else if(reply == "WINDow") + PullWindowTrigger(); + // Note that PULSe, PATTern, QUALified, VIDeo, IIC, SPI, LIN, CAN, FLEXray, CANFd & IIS are not yet handled + //Unrecognized trigger type + else + { + LogWarning("Unknown trigger type \"%s\"\n", reply.c_str()); + m_trigger = NULL; + return; + } + + //Pull the source (same for all types of trigger) + PullTriggerSource(m_trigger, reply,isUart); +} + +/** + @brief Reads the source of a trigger from the instrument + */ +void MagnovaOscilloscope::PullTriggerSource(Trigger* trig, string triggerModeName, bool isUart) +{ + string reply = Trim(isUart ? converse(":TRIGGER:UART:RXS?") : converse(":TRIGGER:%s:SOURCE?", triggerModeName.c_str())); + // Returns CHANnel1 or DIGital1 + + // Get channel number + int i = reply.size() - 1; + while (i >= 0 && std::isdigit(reply[i])) { + i--; + } + std::string number = reply.substr(i + 1); + bool isAnalog = (reply[0] == 'C'); + + auto chan = GetOscilloscopeChannelByHwName((isAnalog ? "CH" : "D") + number); + trig->SetInput(0, StreamDescriptor(chan, 0), true); + if(!chan) + LogWarning("Unknown trigger source \"%s\"\n", reply.c_str()); +} + +/** + @brief Reads settings for a dropout trigger from the instrument + */ +void MagnovaOscilloscope::PullDropoutTrigger() +{ + //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 DropoutTrigger(this); + DropoutTrigger* dt = dynamic_cast(m_trigger); + + Unit fs(Unit::UNIT_FS); + + //Level + dt->SetLevel(stof(converse(":TRIGGER:DROPOUT:LEVEL?"))); + + //Dropout time + dt->SetDropoutTime(fs.ParseString(converse(":TRIGGER:DROPOUT:TIME?"))); + + //Edge type + if(Trim(converse(":TRIGGER:DROPOUT:SLOPE?")) == "RISING") + dt->SetType(DropoutTrigger::EDGE_RISING); + else + dt->SetType(DropoutTrigger::EDGE_FALLING); + + //Reset type + if(Trim(converse(":TRIGGER:DROPOUT:TYPE?")) == "EDGE") + dt->SetResetType(DropoutTrigger::RESET_OPPOSITE); + else + dt->SetResetType(DropoutTrigger::RESET_NONE); +} + +/** + @brief Reads settings for an edge trigger from the instrument + */ +void MagnovaOscilloscope::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); + + //Level + et->SetLevel(stof(converse(":TRIGGER:EDGE:LEVEL?"))); + + //TODO: OptimizeForHF (changes hysteresis for fast signals) + + //Slope + GetTriggerSlope(et, Trim(converse(":TRIGGER:EDGE:SLOPE?"))); +} + +/** + @brief Reads settings for an edge trigger from the instrument + */ +void MagnovaOscilloscope::PullPulseWidthTrigger() +{ + //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 PulseWidthTrigger(this); + auto pt = dynamic_cast(m_trigger); + Unit fs(Unit::UNIT_FS); + + //Level + pt->SetLevel(stof(converse(":TRIGGER:INTERVAL:LEVEL?"))); + + //Condition + pt->SetCondition(GetCondition(converse(":TRIGGER:INTERVAL:LIMIT?"))); + + //Min range + pt->SetLowerBound(fs.ParseString(converse(":TRIGGER:INTERVAL:TLOWER?"))); + + //Max range + pt->SetUpperBound(fs.ParseString(converse(":TRIGGER:INTERVAL:TUPPER?"))); + + //Slope + GetTriggerSlope(pt, Trim(converse(":TRIGGER:INTERVAL:SLOPE?"))); +} + +/** + @brief Reads settings for a runt-pulse trigger from the instrument + */ +void MagnovaOscilloscope::PullRuntTrigger() +{ + //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 RuntTrigger(this); + RuntTrigger* rt = dynamic_cast(m_trigger); + + Unit v(Unit::UNIT_VOLTS); + Unit fs(Unit::UNIT_FS); + string reply; + + //Lower bound + rt->SetLowerBound(v.ParseString(converse(":TRIGGER:RUNT:LLEVEL?"))); + + //Upper bound + rt->SetUpperBound(v.ParseString(converse(":TRIGGER:RUNT:HLEVEL?"))); + + //Lower bound + rt->SetLowerInterval(fs.ParseString(converse(":TRIGGER:RUNT:TLOWER?"))); + + //Upper interval + rt->SetUpperInterval(fs.ParseString(converse(":TRIGGER:RUNT:TUPPER?"))); + + //Slope + reply = Trim(converse(":TRIGGER:RUNT:POLARITY?")); + if(reply == "POSitive") + rt->SetSlope(RuntTrigger::EDGE_RISING); + else if(reply == "NEGative") + rt->SetSlope(RuntTrigger::EDGE_FALLING); + + //Condition + rt->SetCondition(GetCondition(converse(":TRIGGER:RUNT:LIMIT?"))); +} + +/** + @brief Reads settings for a slew rate trigger from the instrument + */ +void MagnovaOscilloscope::PullSlewRateTrigger() +{ + //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 SlewRateTrigger(this); + SlewRateTrigger* st = dynamic_cast(m_trigger); + + Unit v(Unit::UNIT_VOLTS); + Unit fs(Unit::UNIT_FS); + string reply ; + + //Lower bound + st->SetLowerBound(v.ParseString(converse(":TRIGGER:SLOPE:LLEVEL?"))); + + //Upper bound + st->SetUpperBound(v.ParseString(converse(":TRIGGER:SLOPE:HLEVEL?"))); + + //Lower interval + st->SetLowerInterval(fs.ParseString(converse(":TRIGGER:SLOPE:TLOWER?"))); + + //Upper interval + st->SetUpperInterval(fs.ParseString(converse(":TRIGGER:SLOPE:TUPPER?"))); + + //Slope + reply = Trim(converse("TRIGGER:SLOPE:SLOPE?")); + if(reply == "RISing") + st->SetSlope(SlewRateTrigger::EDGE_RISING); + else if(reply == "FALLing") + st->SetSlope(SlewRateTrigger::EDGE_FALLING); + else if(reply == "ALTernate") + st->SetSlope(SlewRateTrigger::EDGE_ANY); + + //Condition + st->SetCondition(GetCondition(converse("TRIGGER:SLOPE:LIMIT?"))); +} + +/** + @brief Reads settings for a UART trigger from the instrument + */ +void MagnovaOscilloscope::PullUartTrigger() +{ + //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 UartTrigger(this); + UartTrigger* ut = dynamic_cast(m_trigger); + + string reply; + string p1; + + + //Bit rate + ut->SetBitRate(stoi(converse(":TRIGGER:UART:BAUD?"))); + + //Level + ut->SetLevel(stof(converse(":TRIGGER:UART:RXT?"))); + + //Parity + reply = Trim(converse(":TRIGGER:UART:PARITY?")); + if(reply == "NONE") + ut->SetParityType(UartTrigger::PARITY_NONE); + else if(reply == "EVEN") + ut->SetParityType(UartTrigger::PARITY_EVEN); + else if(reply == "ODD") + ut->SetParityType(UartTrigger::PARITY_ODD); + else if(reply == "MARK") + ut->SetParityType(UartTrigger::PARITY_MARK); + else if(reply == "SPACe") + ut->SetParityType(UartTrigger::PARITY_SPACE); + + //Operator + //bool ignore_p2 = true; + + // It seems this scope only copes with equivalence + ut->SetCondition(Trigger::CONDITION_EQUAL); + + //Idle polarity + reply = Trim(converse(":TRIGGER:UART:IDLE?")); + if(reply == "HIGH") + ut->SetPolarity(UartTrigger::IDLE_HIGH); + else if(reply == "LOW") + ut->SetPolarity(UartTrigger::IDLE_LOW); + + //Stop bits + ut->SetStopBits(stof(Trim(converse(":TRIGGER:UART:STOP?")))); + + //Trigger type + reply = Trim(converse(":TRIGGER:UART:CONDITION?")); + if(reply == "STARt") + ut->SetMatchType(UartTrigger::TYPE_START); + else if(reply == "STOP") + ut->SetMatchType(UartTrigger::TYPE_STOP); + else if(reply == "ERRor") + ut->SetMatchType(UartTrigger::TYPE_PARITY_ERR); + else + ut->SetMatchType(UartTrigger::TYPE_DATA); + + // Data to match (there is no pattern2 on sds) + p1 = Trim(converse(":TRIGGER:UART:DATA?")); + ut->SetPatterns(p1, "", true); +} + +/** + @brief Reads settings for a window trigger from the instrument + */ +void MagnovaOscilloscope::PullWindowTrigger() +{ + //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 WindowTrigger(this); + WindowTrigger* wt = dynamic_cast(m_trigger); + + Unit v(Unit::UNIT_VOLTS); + + //Lower bound + wt->SetLowerBound(v.ParseString(converse(":TRIGGER:WINDOW:LLEVEL?"))); + + //Upper bound + wt->SetUpperBound(v.ParseString(converse(":TRIGGER:WINDOW:HLEVEL?"))); +} + +/** + @brief Processes the slope for an edge or edge-derived trigger + */ +void MagnovaOscilloscope::GetTriggerSlope(EdgeTrigger* trig, string reply) + +{ + reply = Trim(reply); + + if(reply == "RISing") + trig->SetType(EdgeTrigger::EDGE_RISING); + else if(reply == "FALLing") + trig->SetType(EdgeTrigger::EDGE_FALLING); + else if(reply == "ALTernate") + trig->SetType(EdgeTrigger::EDGE_ALTERNATING); + else if(reply == "BOTH") + trig->SetType(EdgeTrigger::EDGE_ANY); + else + LogWarning("Unknown trigger slope %s\n", reply.c_str()); +} + +/** + @brief Parses a trigger condition + */ +Trigger::Condition MagnovaOscilloscope::GetCondition(string reply) +{ + reply = Trim(reply); + + if(reply == "LESSthan") + return Trigger::CONDITION_LESS; + else if(reply == "GREATerthan") + return Trigger::CONDITION_GREATER; + else if(reply == "INNer") + return Trigger::CONDITION_BETWEEN; + else if(reply == "OUTer") + return Trigger::CONDITION_NOT_BETWEEN; + + //unknown + LogWarning("Unknown trigger condition [%s]\n", reply.c_str()); + return Trigger::CONDITION_LESS; +} + +void MagnovaOscilloscope::PushTrigger() +{ + auto dt = dynamic_cast(m_trigger); + auto et = dynamic_cast(m_trigger); + auto pt = dynamic_cast(m_trigger); + auto rt = dynamic_cast(m_trigger); + auto st = dynamic_cast(m_trigger); + auto ut = dynamic_cast(m_trigger); + auto wt = dynamic_cast(m_trigger); + + if(dt) + { + sendOnly(":TRIGGER:TYPE DROPOUT"); + sendOnly(":TRIGGER:DROPOUT:SOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); + PushDropoutTrigger(dt); + } + else if(pt) + { + sendOnly(":TRIGGER:TYPE INTERVAL"); + sendOnly(":TRIGGER:INTERVAL:SOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); + PushPulseWidthTrigger(pt); + } + else if(rt) + { + sendOnly(":TRIGGER:TYPE RUNT"); + sendOnly(":TRIGGER:RUNT:SOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); + PushRuntTrigger(rt); + } + else if(st) + { + sendOnly(":TRIGGER:TYPE SLOPE"); + sendOnly(":TRIGGER:SLOPE:SOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); + PushSlewRateTrigger(st); + } + else if(ut) + { + sendOnly(":TRIGGER:TYPE UART"); + // TODO: Validate these trigger allocations + sendOnly(":TRIGGER:UART:RXSOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); + sendOnly(":TRIGGER:UART:TXSOURCE %s", m_trigger->GetInput(1).m_channel->GetHwname().c_str()); + PushUartTrigger(ut); + } + else if(wt) + { + sendOnly(":TRIGGER:TYPE WINDOW"); + sendOnly(":TRIGGER:WINDOW:SOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); + PushWindowTrigger(wt); + } + + // TODO: Add in PULSE, VIDEO, PATTERN, QUALITFIED, SPI, IIC, CAN, LIN, FLEXRAY and CANFD Triggers + + else if(et) //must be last + { + sendOnly(":TRIGGER:TYPE EDGE"); + sendOnly(":TRIGGER:EDGE:SOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); + PushEdgeTrigger(et, "EDGE"); + } + + else + LogWarning("Unknown trigger type (not an edge)\n"); +} + +/** + @brief Pushes settings for a dropout trigger to the instrument + */ +void MagnovaOscilloscope::PushDropoutTrigger(DropoutTrigger* trig) +{ + + PushFloat(":TRIGGER:DROPOUT:LEVEL", trig->GetLevel()); + PushFloat(":TRIGGER:DROPOUT:TIME", trig->GetDropoutTime() * SECONDS_PER_FS); + sendOnly(":TRIGGER:DROPOUT:SLOPE %s", (trig->GetType() == DropoutTrigger::EDGE_RISING) ? "RISING" : "FALLING"); + sendOnly(":TRIGGER:DROPOUT:TYPE %s", (trig->GetResetType() == DropoutTrigger::RESET_OPPOSITE) ? "EDGE" : "STATE"); +} + +/** + @brief Pushes settings for an edge trigger to the instrument + */ +void MagnovaOscilloscope::PushEdgeTrigger(EdgeTrigger* trig, const std::string trigType) +{ + switch(trig->GetType()) + { + case EdgeTrigger::EDGE_RISING: + sendOnly(":TRIGGER:%s:SLOPE RISING", trigType.c_str()); + break; + + case EdgeTrigger::EDGE_FALLING: + sendOnly(":TRIGGER:%s:SLOPE FALLING", trigType.c_str()); + break; + + case EdgeTrigger::EDGE_ANY: + sendOnly(":TRIGGER:%s:SLOPE ALTERNATE", trigType.c_str()); + break; + + default: + LogWarning("Invalid trigger type %d\n", trig->GetType()); + break; + } + //Level + sendOnly(":TRIGGER:%s:LEVEL %1.2E", trigType.c_str(), trig->GetLevel()); +} + +/** + @brief Pushes settings for a pulse width trigger to the instrument + */ +void MagnovaOscilloscope::PushPulseWidthTrigger(PulseWidthTrigger* trig) +{ + PushEdgeTrigger(trig, "INTERVAL"); + PushCondition(":TRIGGER:INTERVAL", trig->GetCondition()); + PushFloat(":TRIGGER:INTERVAL:TUPPER", trig->GetUpperBound() * SECONDS_PER_FS); + PushFloat(":TRIGGER:INTERVAL:TLOWER", trig->GetLowerBound() * SECONDS_PER_FS); +} + +/** + @brief Pushes settings for a runt trigger to the instrument + */ +void MagnovaOscilloscope::PushRuntTrigger(RuntTrigger* trig) +{ + PushCondition(":TRIGGER:RUNT", trig->GetCondition()); + PushFloat(":TRIGGER:RUNT:TUPPER", trig->GetUpperInterval() * SECONDS_PER_FS); + PushFloat(":TRIGGER:RUNT:TLOWER", trig->GetLowerInterval() * SECONDS_PER_FS); + PushFloat(":TRIGGER:RUNT:LLEVEL", trig->GetLowerBound()); + PushFloat(":TRIGGER:RUNT:HLEVEL", trig->GetUpperBound()); + + sendOnly(":TRIGGER:RUNT:POLARITY %s", (trig->GetSlope() == RuntTrigger::EDGE_RISING) ? "POSITIVE" : "NEGATIVE"); +} + +/** + @brief Pushes settings for a slew rate trigger to the instrument + */ +void MagnovaOscilloscope::PushSlewRateTrigger(SlewRateTrigger* trig) +{ + PushCondition(":TRIGGER:SLOPE", trig->GetCondition()); + PushFloat(":TRIGGER:SLOPE:TUPPER", trig->GetUpperInterval() * SECONDS_PER_FS); + PushFloat(":TRIGGER:SLOPE:TLOWER", trig->GetLowerInterval() * SECONDS_PER_FS); + PushFloat(":TRIGGER:SLOPE:HLEVEL", trig->GetUpperBound()); + PushFloat(":TRIGGER:SLOPE:LLEVEL", trig->GetLowerBound()); + + sendOnly(":TRIGGER:SLOPE:SLOPE %s", + (trig->GetSlope() == SlewRateTrigger::EDGE_RISING) ? "RISING" : + (trig->GetSlope() == SlewRateTrigger::EDGE_FALLING) ? "FALLING" : + "ALTERNATE"); +} + +/** + @brief Pushes settings for a UART trigger to the instrument + */ +void MagnovaOscilloscope::PushUartTrigger(UartTrigger* trig) +{ + float nstop; + string pattern1; + //Special parameter for trigger level + PushFloat(":TRIGGER:UART:LIMIT", trig->GetLevel()); + + //AtPosition + //Bit9State + PushFloat(":TRIGGER:UART:BAUD", trig->GetBitRate()); + sendOnly(":TRIGGER:UART:BITORDER LSB"); + //DataBytesLenValue1 + //DataBytesLenValue2 + //DataCondition + //FrameDelimiter + //InterframeMinBits + //NeedDualLevels + //NeededSources + sendOnly(":TRIGGER:UART:DLENGTH 8"); + + switch(trig->GetParityType()) + { + case UartTrigger::PARITY_NONE: + sendOnly(":TRIGGER:UART:PARITY NONE"); + break; + + case UartTrigger::PARITY_ODD: + sendOnly(":TRIGGER:UART:PARITY ODD"); + break; + + case UartTrigger::PARITY_EVEN: + sendOnly(":TRIGGER:UART:PARITY EVEN"); + break; + + case UartTrigger::PARITY_MARK: + sendOnly(":TRIGGER:UART:PARITY MARK"); + break; + case UartTrigger::PARITY_SPACE: + sendOnly(":TRIGGER:UART:PARITY SPACE"); + break; + } + + //Pattern length depends on the current format. + //Note that the pattern length is in bytes, not bits, even though patterns are in binary. + pattern1 = trig->GetPattern1(); + sendOnly(":TRIGGER:UART:DLENGTH \"%d\"", (int)pattern1.length() / 8); + + PushCondition(":TRIGGER:UART", trig->GetCondition()); + + //Polarity + sendOnly(":TRIGGER:UART:IDLE %s", (trig->GetPolarity() == UartTrigger::IDLE_HIGH) ? "HIGH" : "LOW"); + + nstop = trig->GetStopBits(); + if(nstop == 1) + sendOnly(":TRIGGER:UART:STOP 1"); + else if(nstop == 2) + sendOnly(":TRIGGER:UART:STOP 2"); + else + sendOnly(":TRIGGER:UART:STOP 1.5"); + + //Match type + switch(trig->GetMatchType()) + { + case UartTrigger::TYPE_START: + sendOnly(":TRIGGER:UART:CONDITION START"); + break; + case UartTrigger::TYPE_STOP: + sendOnly(":TRIGGER:UART:CONDITION STOP"); + break; + case UartTrigger::TYPE_PARITY_ERR: + sendOnly(":TRIGGER:UART:CONDITION ERROR"); + break; + default: + case UartTrigger::TYPE_DATA: + sendOnly(":TRIGGER:UART:CONDITION DATA"); + break; + } +} + +/** + @brief Pushes settings for a window trigger to the instrument + */ +void MagnovaOscilloscope::PushWindowTrigger(WindowTrigger* trig) +{ + PushFloat(":TRIGGER:WINDOW:LLEVEL", trig->GetLowerBound()); + PushFloat(":TRIGGER:WINDOW:HLEVEL", trig->GetUpperBound()); +} + +/** + @brief Pushes settings for a trigger condition under a .Condition field + */ +void MagnovaOscilloscope::PushCondition(const string& path, Trigger::Condition cond) +{ + switch(cond) + { + case Trigger::CONDITION_LESS: + sendOnly("%s:LIMIT LESSTHAN", path.c_str()); + break; + + case Trigger::CONDITION_GREATER: + sendOnly("%s:LIMIT GREATERTHAN", path.c_str()); + break; + + case Trigger::CONDITION_BETWEEN: + sendOnly("%s:LIMIT INNER", path.c_str()); + break; + + case Trigger::CONDITION_NOT_BETWEEN: + sendOnly("%s:LIMIT OUTER", path.c_str()); + break; + + //Other values are not legal here, it seems + default: + break; + } +} + +void MagnovaOscilloscope::PushFloat(string path, float f) +{ + sendOnly("%s %1.2E", path.c_str(), f); +} + +vector MagnovaOscilloscope::GetTriggerTypes() +{ + vector ret; + ret.push_back(DropoutTrigger::GetTriggerName()); + ret.push_back(EdgeTrigger::GetTriggerName()); + ret.push_back(PulseWidthTrigger::GetTriggerName()); + ret.push_back(RuntTrigger::GetTriggerName()); + ret.push_back(SlewRateTrigger::GetTriggerName()); + if(m_hasUartTrigger) + ret.push_back(UartTrigger::GetTriggerName()); + ret.push_back(WindowTrigger::GetTriggerName()); + // TODO: Add in PULSE, VIDEO, PATTERN, QUALITFIED, SPI, IIC, CAN, LIN, FLEXRAY and CANFD Triggers + return ret; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Function generator mode + +//Per docs, this is almost the same API as the SDG series generators. +//But the SAG102I and integrated generator have only a single output. +//This code can likely be ported to work with SDG* fairly easily, though. + +vector MagnovaOscilloscope::GetAvailableWaveformShapes(int /*chan*/) +{ + vector ret; + ret.push_back(SHAPE_SINE); + ret.push_back(SHAPE_SQUARE); + ret.push_back(SHAPE_NOISE); + + //Docs say this is supported, but doesn't seem to work on SDS2104X+ + //Might be SDG only? + //ret.push_back(SHAPE_PRBS_NONSTANDARD); + + ret.push_back(SHAPE_DC); + ret.push_back(SHAPE_STAIRCASE_UP); + ret.push_back(SHAPE_STAIRCASE_DOWN); + ret.push_back(SHAPE_STAIRCASE_UP_DOWN); + ret.push_back(SHAPE_PULSE); + + //Docs say this is supported, but doesn't seem to work on SDS2104X+ + //Might be SDG only? + //ret.push_back(SHAPE_NEGATIVE_PULSE); + + //what's "trapezia"? + ret.push_back(SHAPE_SAWTOOTH_UP); + ret.push_back(SHAPE_SAWTOOTH_DOWN); + ret.push_back(SHAPE_EXPONENTIAL_DECAY); + ret.push_back(SHAPE_EXPONENTIAL_RISE); + ret.push_back(SHAPE_LOG_DECAY); + ret.push_back(SHAPE_LOG_RISE); + ret.push_back(SHAPE_SQUARE_ROOT); + ret.push_back(SHAPE_CUBE_ROOT); + ret.push_back(SHAPE_QUADRATIC); + ret.push_back(SHAPE_CUBIC); + ret.push_back(SHAPE_SINC); + ret.push_back(SHAPE_GAUSSIAN); + ret.push_back(SHAPE_DLORENTZ); + ret.push_back(SHAPE_HAVERSINE); + ret.push_back(SHAPE_LORENTZ); + ret.push_back(SHAPE_GAUSSIAN_PULSE); + //What's Gmonopuls? + //What's Tripuls? + ret.push_back(SHAPE_CARDIAC); + //What's quake? + //What's chirp? + //What's twotone? + //What's snr? + ret.push_back(SHAPE_HAMMING); + ret.push_back(SHAPE_HANNING); + ret.push_back(SHAPE_KAISER); + ret.push_back(SHAPE_BLACKMAN); + ret.push_back(SHAPE_GAUSSIAN_WINDOW); + ret.push_back(SHAPE_TRIANGLE); + ret.push_back(SHAPE_HARRIS); + ret.push_back(SHAPE_BARTLETT); + ret.push_back(SHAPE_TAN); + ret.push_back(SHAPE_COT); + ret.push_back(SHAPE_SEC); + ret.push_back(SHAPE_CSC); + ret.push_back(SHAPE_ASIN); + ret.push_back(SHAPE_ACOS); + ret.push_back(SHAPE_ATAN); + ret.push_back(SHAPE_ACOT); + + return ret; +} + +bool MagnovaOscilloscope::GetFunctionChannelActive(int chan) +{ + { + lock_guard lock(m_cacheMutex); + if(m_awgEnabled.find(chan) != m_awgEnabled.end()) + return m_awgEnabled[chan]; + } + + auto reply = m_transport->SendCommandQueuedWithReply(":FGEN:STAT?", false); + + { + lock_guard lock(m_cacheMutex); + + if(reply.find("OFF") != string::npos) + m_awgEnabled[chan] = false; + else + m_awgEnabled[chan] = true; + + return m_awgEnabled[chan]; + } + +} + +void MagnovaOscilloscope::SetFunctionChannelActive(int chan, bool on) +{ + string state; + if(on) + state = "ON"; + else + state = "OFF"; + + //Have to do this first, since it touches m_awgEnabled too + string imp; + if(GetFunctionChannelOutputImpedance(chan) == IMPEDANCE_50_OHM) + imp = "50"; + else + imp = "HZ"; + + m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":OUTP " + state + ",LOAD," + imp); + + lock_guard lock(m_cacheMutex); + m_awgEnabled[chan] = on; +} + +float MagnovaOscilloscope::GetFunctionChannelDutyCycle(int chan) +{ + { + lock_guard lock(m_cacheMutex); + if(m_awgDutyCycle.find(chan) != m_awgDutyCycle.end()) + return m_awgDutyCycle[chan]; + } + + //Get lots of config settings from the hardware, then return newly updated cache entry + GetFunctionChannelShape(chan); + + lock_guard lock(m_cacheMutex); + return m_awgDutyCycle[chan]; +} + +void MagnovaOscilloscope::SetFunctionChannelDutyCycle(int chan, float duty) +{ + m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":BSWV DUTY," + to_string(round(duty * 100))); + + lock_guard lock(m_cacheMutex); + m_awgDutyCycle[chan] = duty; +} + +float MagnovaOscilloscope::GetFunctionChannelAmplitude(int chan) +{ + { + lock_guard lock(m_cacheMutex); + if(m_awgRange.find(chan) != m_awgRange.end()) + return m_awgRange[chan]; + } + + //Get lots of config settings from the hardware, then return newly updated cache entry + GetFunctionChannelShape(chan); + + lock_guard lock(m_cacheMutex); + return m_awgRange[chan]; +} + +void MagnovaOscilloscope::SetFunctionChannelAmplitude(int chan, float amplitude) +{ + m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":BSWV AMP," + to_string(amplitude)); + + lock_guard lock(m_cacheMutex); + m_awgRange[chan] = amplitude; +} + +float MagnovaOscilloscope::GetFunctionChannelOffset(int chan) +{ + { + lock_guard lock(m_cacheMutex); + if(m_awgOffset.find(chan) != m_awgOffset.end()) + return m_awgOffset[chan]; + } + + //Get lots of config settings from the hardware, then return newly updated cache entry + GetFunctionChannelShape(chan); + + lock_guard lock(m_cacheMutex); + return m_awgOffset[chan]; +} + +void MagnovaOscilloscope::SetFunctionChannelOffset(int chan, float offset) +{ + m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":BSWV OFST," + to_string(offset)); + + lock_guard lock(m_cacheMutex); + m_awgOffset[chan] = offset; +} + +float MagnovaOscilloscope::GetFunctionChannelFrequency(int chan) +{ + { + lock_guard lock(m_cacheMutex); + if(m_awgFrequency.find(chan) != m_awgFrequency.end()) + return m_awgFrequency[chan]; + } + + //Get lots of config settings from the hardware, then return newly updated cache entry + GetFunctionChannelShape(chan); + + lock_guard lock(m_cacheMutex); + return m_awgFrequency[chan]; +} + +void MagnovaOscilloscope::SetFunctionChannelFrequency(int chan, float hz) +{ + m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":BSWV FRQ," + to_string(hz)); + + lock_guard lock(m_cacheMutex); + m_awgFrequency[chan] = hz; +} + +/** + @brief Parses a name-value set expressed as pairs of comma separated values + + Expected format: COMMAND? Name1, Value1, Name2, Value2 + + If forwardMap is true, returns name -> value. If false, returns value -> name. + */ +map MagnovaOscilloscope::ParseCommaSeparatedNameValueList(string str, bool forwardMap) +{ + str += ','; + size_t ispace = str.find(' '); + string tmpName; + string tmpVal; + bool firstHalf = true; + map ret; + for(size_t i=ispace+1; i lock(m_cacheMutex); + if(m_awgShape.find(chan) != m_awgShape.end()) + return m_awgShape[chan]; + } + + //Query the basic wave parameters + auto shape = m_transport->SendCommandQueuedWithReply(":FGEN:WAV:SHAP?", false); + // auto areply = m_transport->SendCommandQueuedWithReply(":FGEN:WAV:ASHAP?", false); + + //Crack the replies + { + lock_guard lock(m_cacheMutex); + + if(shape == "SINe") + m_awgShape[chan] = FunctionGenerator::SHAPE_SINE; + else if(shape == "SQUare") + m_awgShape[chan] = FunctionGenerator::SHAPE_SQUARE; + else if(shape == "RAMP") + { + LogWarning("wave type RAMP unimplemented\n"); + } + else if(shape == "PULSe") + m_awgShape[chan] = FunctionGenerator::SHAPE_PULSE; + else if(shape == "NOISe") + m_awgShape[chan] = FunctionGenerator::SHAPE_NOISE; + else if(shape == "DC") + m_awgShape[chan] = FunctionGenerator::SHAPE_DC; + else if(shape == "PRBS") + { + //TODO: LENGTH if type is PRBS? + //Might only be supported on SDGs + m_awgShape[chan] = FunctionGenerator::SHAPE_PRBS_NONSTANDARD; + } + else if(shape == "ARBitrary") + { + m_awgShape[chan] = FunctionGenerator::SHAPE_CARDIAC; + /*string name = areply.substr(areply.find("NAME,") + 5); + + if(name == "ExpFal") + m_awgShape[chan] = FunctionGenerator::SHAPE_EXPONENTIAL_DECAY; + else if(name == "ExpRise") + m_awgShape[chan] = FunctionGenerator::SHAPE_EXPONENTIAL_RISE; + else if(name == "LogFall") + m_awgShape[chan] = FunctionGenerator::SHAPE_LOG_DECAY; + else if(name == "LogRise") + m_awgShape[chan] = FunctionGenerator::SHAPE_LOG_RISE; + else if(name == "Sqrt") + m_awgShape[chan] = FunctionGenerator::SHAPE_SQUARE_ROOT; + else if(name == "Root3") + m_awgShape[chan] = FunctionGenerator::SHAPE_CUBE_ROOT; + else if(name == "X^2") + m_awgShape[chan] = FunctionGenerator::SHAPE_SQUARE; + else if(name == "X^3") + m_awgShape[chan] = FunctionGenerator::SHAPE_CUBIC; + else if(name == "Sinc") + m_awgShape[chan] = FunctionGenerator::SHAPE_SINC; + else if(name == "Gaussian") + m_awgShape[chan] = FunctionGenerator::SHAPE_GAUSSIAN; + else if(name == "StairUp") + m_awgShape[chan] = FunctionGenerator::SHAPE_STAIRCASE_UP; + //DLorentz + else if(name == "Haversine") + m_awgShape[chan] = FunctionGenerator::SHAPE_HAVERSINE; + else if(name == "Lorentz") + m_awgShape[chan] = FunctionGenerator::SHAPE_LORENTZ; + else if(name == "Gauspuls") + m_awgShape[chan] = FunctionGenerator::SHAPE_GAUSSIAN_PULSE; + //TODO: Gmonopuls + //TODO: Tripuls + else if(name == "Cardiac") + m_awgShape[chan] = FunctionGenerator::SHAPE_CARDIAC; + //TODO: Quake + //TODO: Chirp + //TODO: Twotone + else if(name == "StairDn") + m_awgShape[chan] = FunctionGenerator::SHAPE_STAIRCASE_DOWN; + //TODO: SNR + else if(name == "Hamming") + m_awgShape[chan] = FunctionGenerator::SHAPE_HAMMING; + else if(name == "Hanning") + m_awgShape[chan] = FunctionGenerator::SHAPE_HANNING; + else if(name == "kaiser") + m_awgShape[chan] = FunctionGenerator::SHAPE_KAISER; + else if(name == "Blackman") + m_awgShape[chan] = FunctionGenerator::SHAPE_BLACKMAN; + else if(name == "Gausswin") + m_awgShape[chan] = FunctionGenerator::SHAPE_GAUSSIAN_WINDOW; + else if(name == "Triangle") + m_awgShape[chan] = FunctionGenerator::SHAPE_TRIANGLE; + else if(name == "BlackmanH") + m_awgShape[chan] = FunctionGenerator::SHAPE_BLACKMAN; + else if(name == "Bartlett-Hann") + m_awgShape[chan] = FunctionGenerator::SHAPE_BARTLETT; + else if(name == "Tan") + m_awgShape[chan] = FunctionGenerator::SHAPE_TAN; + else if(name == "StairUD") + m_awgShape[chan] = FunctionGenerator::SHAPE_STAIRCASE_UP_DOWN; + else if(name == "Cot") + m_awgShape[chan] = FunctionGenerator::SHAPE_COT; + else if(name == "Sec") + m_awgShape[chan] = FunctionGenerator::SHAPE_SEC; + else if(name == "Csc") + m_awgShape[chan] = FunctionGenerator::SHAPE_CSC; + else if(name == "Asin") + m_awgShape[chan] = FunctionGenerator::SHAPE_ASIN; + else if(name == "Acos") + m_awgShape[chan] = FunctionGenerator::SHAPE_ACOS; + else if(name == "Atan") + m_awgShape[chan] = FunctionGenerator::SHAPE_ATAN; + else if(name == "Acot") + m_awgShape[chan] = FunctionGenerator::SHAPE_ACOT; + //TODO: Trapezia + else if(name == "Upramp") + m_awgShape[chan] = FunctionGenerator::SHAPE_SAWTOOTH_UP; + else if(name == "Dnramp") + m_awgShape[chan] = FunctionGenerator::SHAPE_SAWTOOTH_DOWN; + else + LogWarning("Arb shape %s unimplemented\n", name.c_str());*/ + } + else + LogWarning("wave type %s unimplemented\n", shape.c_str()); + + return m_awgShape[chan]; + } +} + +void MagnovaOscilloscope::SetFunctionChannelShape(int chan, FunctionGenerator::WaveShape shape) +{ + string basicType; + string arbType; + + switch(shape) + { + //Basic wave types + case SHAPE_SINE: + basicType = "SINE"; + break; + + case SHAPE_SQUARE: + basicType = "SQUARE"; + break; + + //TODO: "ramp" + + case SHAPE_PULSE: + basicType = "PULSE"; + break; + + case SHAPE_NOISE: + basicType = "NOISE"; + break; + + case SHAPE_PRBS_NONSTANDARD: + basicType = "PRBS"; + break; + + case SHAPE_DC: + basicType = "DC"; + break; + + //Arb wave types + case SHAPE_STAIRCASE_UP: + basicType = "ARB"; + arbType = "StairUp"; + break; + + case SHAPE_STAIRCASE_DOWN: + basicType = "ARB"; + arbType = "StairDn"; + break; + + case SHAPE_STAIRCASE_UP_DOWN: + basicType = "ARB"; + arbType = "StairUD"; + break; + + case SHAPE_SAWTOOTH_UP: + basicType = "ARB"; + arbType = "Upramp"; + break; + + case SHAPE_SAWTOOTH_DOWN: + basicType = "ARB"; + arbType = "Dnramp"; + break; + + case SHAPE_EXPONENTIAL_DECAY: + basicType = "ARB"; + arbType = "ExpFal"; + break; + + case SHAPE_EXPONENTIAL_RISE: + basicType = "ARB"; + arbType = "ExpRise"; + break; + + case SHAPE_LOG_DECAY: + basicType = "ARB"; + arbType = "LogFall"; + break; + + case SHAPE_LOG_RISE: + basicType = "ARB"; + arbType = "LogRise"; + break; + + case SHAPE_SQUARE_ROOT: + basicType = "ARB"; + arbType = "Sqrt"; + break; + + case SHAPE_CUBE_ROOT: + basicType = "ARB"; + arbType = "Root3"; + break; + + case SHAPE_QUADRATIC: + basicType = "ARB"; + arbType = "X^2"; + break; + + case SHAPE_CUBIC: + basicType = "ARB"; + arbType = "X^3"; + break; + + case SHAPE_SINC: + basicType = "ARB"; + arbType = "Sinc"; + break; + + case SHAPE_GAUSSIAN: + basicType = "ARB"; + arbType = "Gaussian"; + break; + + case SHAPE_DLORENTZ: + basicType = "ARB"; + arbType = "DLorentz"; + break; + + case SHAPE_HAVERSINE: + basicType = "ARB"; + arbType = "Haversine"; + break; + + case SHAPE_LORENTZ: + basicType = "ARB"; + arbType = "Lorentz"; + break; + + case SHAPE_GAUSSIAN_PULSE: + basicType = "ARB"; + arbType = "Gauspuls"; + break; + + case SHAPE_CARDIAC: + basicType = "ARB"; + arbType = "Cardiac"; + break; + + case SHAPE_HAMMING: + basicType = "ARB"; + arbType = "Hamming"; + break; + + case SHAPE_HANNING: + basicType = "ARB"; + arbType = "Hanning"; + break; + + case SHAPE_KAISER: + basicType = "ARB"; + arbType = "kaiser"; //yes, lowercase is intentional + break; + + case SHAPE_BLACKMAN: + basicType = "ARB"; + arbType = "Blackman"; + break; + + case SHAPE_GAUSSIAN_WINDOW: + basicType = "ARB"; + arbType = "Gausswin"; + break; + + case SHAPE_TRIANGLE: + basicType = "ARB"; + arbType = "Triangle"; + break; + + case SHAPE_HARRIS: + basicType = "ARB"; + arbType = "BlackmanH"; + break; + + case SHAPE_BARTLETT: + basicType = "ARB"; + arbType = "Bartlett-Hann"; + break; + + case SHAPE_TAN: + basicType = "ARB"; + arbType = "Tan"; + break; + + case SHAPE_COT: + basicType = "ARB"; + arbType = "Cot"; + break; + + case SHAPE_SEC: + basicType = "ARB"; + arbType = "Sec"; + break; + + case SHAPE_CSC: + basicType = "ARB"; + arbType = "Csc"; + break; + + case SHAPE_ASIN: + basicType = "ARB"; + arbType = "Asin"; + break; + + case SHAPE_ACOS: + basicType = "ARB"; + arbType = "Acos"; + break; + + case SHAPE_ATAN: + basicType = "ARB"; + arbType = "Atan"; + break; + + case SHAPE_ACOT: + basicType = "ARB"; + arbType = "Acot"; + break; + + //unsupported, ignore + default: + return; + } + + //Select type + m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":BSWV WVTP," + basicType); + if(basicType == "ARB") + { + //Returns map of memory slots ("M10") to waveform names + //Mapping is explicitly not stable, so we have to check for each instrument + //(but can be cached for a given session) + auto stl = m_transport->SendCommandQueuedWithReply("STL?"); + auto arbmap = ParseCommaSeparatedNameValueList(stl, false); + + m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":ARWV INDEX," + arbmap[arbType].substr(1)); + } + + //Update cache + lock_guard lock(m_cacheMutex); + m_awgShape[chan] = shape; +} + +bool MagnovaOscilloscope::HasFunctionRiseFallTimeControls(int /*chan*/) +{ + return false; +} + +FunctionGenerator::OutputImpedance MagnovaOscilloscope::GetFunctionChannelOutputImpedance(int chan) +{ + { + lock_guard lock(m_cacheMutex); + if(m_awgImpedance.find(chan) != m_awgImpedance.end()) + return m_awgImpedance[chan]; + } + + //Get output enable status and impedance from the hardware, then return newly updated cache entry + GetFunctionChannelActive(chan); + + lock_guard lock(m_cacheMutex); + return m_awgImpedance[chan]; +} + +void MagnovaOscilloscope::SetFunctionChannelOutputImpedance(int chan, FunctionGenerator::OutputImpedance z) +{ + //Have to do this first, since it touches m_awgImpedance + string state; + if(GetFunctionChannelActive(chan)) + state = "ON"; + else + state = "OFF"; + + string imp; + if(z == IMPEDANCE_50_OHM) + imp = "50"; + else + imp = "HZ"; + + m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":OUTP " + state + ",LOAD," + imp); + + lock_guard lock(m_cacheMutex); + m_awgImpedance[chan] = z; +} \ No newline at end of file diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h new file mode 100644 index 00000000..62d4e247 --- /dev/null +++ b/scopehal/MagnovaOscilloscope.h @@ -0,0 +1,357 @@ +/*********************************************************************************************************************** +* * +* libscopehal * +* * +* Copyright (c) 2012-2024 Andrew D. Zonenberg and contributors * +* All rights reserved. * +* * +* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * +* following conditions are met: * +* * +* * Redistributions of source code must retain the above copyright notice, this list of conditions, and the * +* following disclaimer. * +* * +* * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * +* following disclaimer in the documentation and/or other materials provided with the distribution. * +* * +* * Neither the name of the author nor the names of any contributors may be used to endorse or promote products * +* derived from this software without specific prior written permission. * +* * +* THIS SOFTWARE IS PROVIDED BY THE AUTHORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * +* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * +* THE AUTHORS BE HELD LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * +* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * +* POSSIBILITY OF SUCH DAMAGE. * +* * +***********************************************************************************************************************/ + +#ifndef MagnovaOscilloscope_h +#define MagnovaOscilloscope_h + +#include +#include + +class DropoutTrigger; +class EdgeTrigger; +class GlitchTrigger; +class PulseWidthTrigger; +class RuntTrigger; +class SlewRateTrigger; +class UartTrigger; +class WindowTrigger; + +/** + @brief Batronix's Magnova Oscilloscope + + */ + +#define MAX_ANALOG 4 +#define MAX_DIGITAL 16 + +#define c_digiChannelsPerBus 8 + +class MagnovaOscilloscope : public virtual SCPIOscilloscope + , public virtual SCPIFunctionGenerator +{ +public: + MagnovaOscilloscope(SCPITransport* transport); + virtual ~MagnovaOscilloscope(); + + //not copyable or assignable + MagnovaOscilloscope(const MagnovaOscilloscope& rhs) = delete; + MagnovaOscilloscope& operator=(const MagnovaOscilloscope& rhs) = delete; + +private: + std::string converse(const char* fmt, ...); + void sendOnly(const char* fmt, ...); + void flush(); + void flushWaveformData(); + +protected: + void IdentifyHardware(); + void DetectBandwidth(); + void SharedCtorInit(); + virtual void DetectAnalogChannels(); + void AddDigitalChannels(unsigned int count); + void DetectOptions(); + void ParseFirmwareVersion(); + +public: + // Specific 16 bit conversion methods for uint16_t + static void Convert16BitSamples(float* pout, const uint16_t* pin, float gain, float offset, size_t count); + static void Convert16BitSamplesGeneric(float* pout, const uint16_t* pin, float gain, float offset, size_t count); + + //Device information + virtual unsigned int GetInstrumentTypes() const override; + virtual unsigned int GetMeasurementTypes(); + virtual uint32_t GetInstrumentTypesForChannel(size_t i) const override; + + virtual void FlushConfigCache() override; + + //Channel configuration + virtual bool IsChannelEnabled(size_t i) override; + virtual void EnableChannel(size_t i) override; + virtual bool CanEnableChannel(size_t i) override; + virtual void DisableChannel(size_t i) override; + virtual OscilloscopeChannel::CouplingType GetChannelCoupling(size_t i) override; + virtual void SetChannelCoupling(size_t i, OscilloscopeChannel::CouplingType type) override; + virtual std::vector GetAvailableCouplings(size_t i) override; + virtual double GetChannelAttenuation(size_t i) override; + virtual void SetChannelAttenuation(size_t i, double atten) override; + virtual unsigned int GetChannelBandwidthLimit(size_t i) override; + virtual void SetChannelBandwidthLimit(size_t i, unsigned int limit_mhz) override; + virtual float GetChannelVoltageRange(size_t i, size_t stream) override; + virtual void SetChannelVoltageRange(size_t i, size_t stream, float range) override; + 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 std::string GetChannelDisplayName(size_t i) override; + virtual void SetChannelDisplayName(size_t i, std::string name) override; + virtual std::vector GetChannelBandwidthLimiters(size_t i) override; + virtual bool CanInvert(size_t i) override; + virtual void Invert(size_t i, bool invert) override; + virtual bool IsInverted(size_t i) override; + + //Triggering + virtual Oscilloscope::TriggerMode PollTrigger() override; + virtual bool AcquireData() override; + virtual void Start() override; + virtual void StartSingleTrigger() override; + virtual void Stop() override; + virtual void ForceTrigger() override; + virtual bool IsTriggerArmed() override; + virtual void PushTrigger() override; + virtual void PullTrigger() override; + virtual void EnableTriggerOutput() override; + virtual std::vector GetTriggerTypes() override; + + //Scope models. + //We only distinguish down to the series of scope, exact SKU is mostly irrelevant. + enum Model + { + MODEL_MAGNOVA_BMO, + MODEL_UNKNOWN + }; + + Model GetModelID() { return m_modelid; } + + //Timebase + virtual std::vector GetSampleRatesNonInterleaved() override; + virtual std::vector GetSampleRatesInterleaved() override; + virtual std::set GetInterleaveConflicts() override; + virtual std::vector GetSampleDepthsNonInterleaved() override; + virtual std::vector GetSampleDepthsInterleaved() override; + virtual uint64_t GetSampleRate() override; + virtual uint64_t GetSampleDepth() override; + virtual void SetSampleDepth(uint64_t depth) override; + virtual void SetSampleRate(uint64_t rate) override; + virtual void SetUseExternalRefclk(bool external) override; + virtual bool IsInterleaving() override; + virtual bool SetInterleaving(bool combine) override; + + virtual void SetTriggerOffset(int64_t offset) override; + virtual int64_t GetTriggerOffset() override; + virtual void SetDeskewForChannel(size_t channel, int64_t skew) override; + virtual int64_t GetDeskewForChannel(size_t channel) override; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Function generator + + virtual std::vector GetAvailableWaveformShapes(int chan) override; + + //Configuration + virtual bool GetFunctionChannelActive(int chan) override; + virtual void SetFunctionChannelActive(int chan, bool on) override; + + virtual float GetFunctionChannelDutyCycle(int chan) override; + virtual void SetFunctionChannelDutyCycle(int chan, float duty) override; + + virtual float GetFunctionChannelAmplitude(int chan) override; + virtual void SetFunctionChannelAmplitude(int chan, float amplitude) override; + + virtual float GetFunctionChannelOffset(int chan) override; + virtual void SetFunctionChannelOffset(int chan, float offset) override; + + virtual float GetFunctionChannelFrequency(int chan) override; + virtual void SetFunctionChannelFrequency(int chan, float hz) override; + + virtual WaveShape GetFunctionChannelShape(int chan) override; + virtual void SetFunctionChannelShape(int chan, WaveShape shape) override; + + virtual bool HasFunctionRiseFallTimeControls(int chan) override; + + virtual OutputImpedance GetFunctionChannelOutputImpedance(int chan) override; + virtual void SetFunctionChannelOutputImpedance(int chan, OutputImpedance z) override; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Logic analyzer configuration + + virtual std::vector GetDigitalBanks() override; + virtual DigitalBank GetDigitalBank(size_t channel) override; + virtual bool IsDigitalHysteresisConfigurable() override; + virtual bool IsDigitalThresholdConfigurable() override; + virtual float GetDigitalHysteresis(size_t channel) override; + virtual float GetDigitalThreshold(size_t channel) override; + virtual void SetDigitalHysteresis(size_t channel, float level) override; + virtual void SetDigitalThreshold(size_t channel, float level) override; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // ADC bit depth configuration + + //All currently supported Sig2 scopes have only one analog bank (same ADC config for all channels) + //so no need to override those + + enum ADCMode + { + ADC_MODE_8BIT = 0, + ADC_MODE_10BIT = 1 + }; + + virtual bool IsADCModeConfigurable() override; + virtual std::vector GetADCModeNames(size_t channel) override; + virtual size_t GetADCMode(size_t channel) override; + virtual void SetADCMode(size_t channel, size_t mode) override; + +protected: + struct Metadata { + float timeDelta; + float startTime; + float endTime; + uint32_t sampleCount; + uint32_t sampleStart; + uint32_t sampleLength; + float verticalStart; + float verticalStep; + }; + /** + * @brief Parse metadata from the oscilloscope data + * @param data Raw data from oscilloscope + * @param data_transfer_type Data transfer type + * @return Parsed metadata + */ + std::optional parseMetadata(const std::vector& data); + + + void PullDropoutTrigger(); + void PullEdgeTrigger(); + void PullPulseWidthTrigger(); + void PullRuntTrigger(); + void PullSlewRateTrigger(); + void PullUartTrigger(); + void PullWindowTrigger(); + void PullTriggerSource(Trigger* trig, std::string triggerModeName, bool isUart); + + void GetTriggerSlope(EdgeTrigger* trig, std::string reply); + Trigger::Condition GetCondition(std::string reply); + + void PushDropoutTrigger(DropoutTrigger* trig); + void PushEdgeTrigger(EdgeTrigger* trig, const std::string trigType); + void PushGlitchTrigger(GlitchTrigger* trig); + void PushCondition(const std::string& path, Trigger::Condition cond); + void PushPatternCondition(const std::string& path, Trigger::Condition cond); + void PushFloat(std::string path, float f); + void PushPulseWidthTrigger(PulseWidthTrigger* trig); + void PushRuntTrigger(RuntTrigger* trig); + void PushSlewRateTrigger(SlewRateTrigger* trig); + void PushUartTrigger(UartTrigger* trig); + void PushWindowTrigger(WindowTrigger* trig); + + void BulkCheckChannelEnableState(); + + void PrepareAcquisition(); + + std::string GetPossiblyEmptyString(const std::string& property); + + size_t ReadWaveformBlock(std::vector* data, std::function progress = nullptr); + + time_t ExtractTimestamp(const std::string& time, double& basetime); + + std::vector ProcessAnalogWaveform( + const std::vector& data, + size_t datalen, + uint32_t num_sequences, + time_t ttime, + double basetime, + double* wavetime, + int i); + + std::vector ProcessDigitalWaveform( + const std::vector& data, + size_t datalen, + uint32_t num_sequences, + time_t ttime, + double basetime, + double* wavetime, + int i); + + //hardware analog channel count, independent of LA option etc + unsigned int m_analogChannelCount; + unsigned int m_digitalChannelCount; + unsigned int m_analogAndDigitalChannelCount; + size_t m_digitalChannelBase; + + Model m_modelid; + + // Firmware version + int m_fwMajorVersion; + int m_fwMinorVersion; + int m_fwPatchVersion; + + //set of SW/HW options we have + bool m_hasLA; + bool m_hasDVM; + bool m_hasFunctionGen; + bool m_hasFastSampleRate; //-M models + int m_memoryDepthOption; //0 = base, after that number is max sample count in millions + bool m_hasI2cTrigger; + bool m_hasSpiTrigger; + bool m_hasUartTrigger; + + ///Maximum bandwidth we support, in MHz + unsigned int m_maxBandwidth; + + bool m_triggerArmed; + bool m_triggerOneShot; + bool m_triggerForced; + + //Cached configuration + std::map m_channelVoltageRanges; + std::map m_channelOffsets; + std::map m_channelDigitalThresholds; + std::map m_channelsEnabled; + bool m_sampleRateValid; + int64_t m_sampleRate; + bool m_memoryDepthValid; + int64_t m_memoryDepth; + bool m_triggerOffsetValid; + int64_t m_triggerOffset; + std::map m_channelDeskew; + Multimeter::MeasurementTypes m_meterMode; + bool m_meterModeValid; + std::map m_probeIsActive; + std::map m_awgEnabled; + std::map m_awgDutyCycle; + std::map m_awgRange; + std::map m_awgOffset; + std::map m_awgFrequency; + std::map m_awgShape; + std::map m_awgImpedance; + ADCMode m_adcMode; + bool m_adcModeValid; + + std::map ParseCommaSeparatedNameValueList(std::string str, bool forwardMap = true); + + int64_t m_timeDiv; + + //Other channels + OscilloscopeChannel* m_extTrigChannel; + FunctionGeneratorChannel* m_awgChannel; + std::vector m_digitalChannels; + +public: + static std::string GetDriverNameInternal(); + OSCILLOSCOPE_INITPROC(MagnovaOscilloscope) +}; +#endif diff --git a/scopehal/scopehal.cpp b/scopehal/scopehal.cpp index 81534a3b..19a9ba76 100644 --- a/scopehal/scopehal.cpp +++ b/scopehal/scopehal.cpp @@ -49,6 +49,7 @@ #include "KeysightDCA.h" #include "LeCroyOscilloscope.h" #include "LeCroyFWPOscilloscope.h" +#include "MagnovaOscilloscope.h" #include "PicoOscilloscope.h" #include "RigolOscilloscope.h" #include "RohdeSchwarzOscilloscope.h" @@ -249,6 +250,7 @@ void DriverStaticInit() AddDriverClass(RSRTO6Oscilloscope); AddDriverClass(LeCroyOscilloscope); AddDriverClass(LeCroyFWPOscilloscope); + AddDriverClass(MagnovaOscilloscope); AddDriverClass(SiglentSCPIOscilloscope); AddDriverClass(TektronixOscilloscope); AddDriverClass(TektronixHSIOscilloscope); From 02b2a3ffe236db70b892232757c6339c3247cebd Mon Sep 17 00:00:00 2001 From: fredzo Date: Thu, 4 Dec 2025 00:44:33 +0100 Subject: [PATCH 02/33] Fixed sample rate and depth setting. --- scopehal/MagnovaOscilloscope.cpp | 81 ++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 4cdb8fa1..8210ad8f 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -1537,7 +1537,7 @@ float MagnovaOscilloscope::GetChannelOffset(size_t i, size_t /*stream*/) string reply; - reply = converse(":CHANNEL%zu:OFFSET?", i + 1); + reply = converse(":CHAN%zu:OFFSET?", i + 1); float offset; sscanf(reply.c_str(), "%f", &offset); @@ -1553,7 +1553,7 @@ void MagnovaOscilloscope::SetChannelOffset(size_t i, size_t /*stream*/, float of if(i >= m_analogChannelCount) return; - sendOnly(":CHANNEL%zu:OFFSET %1.2E", i + 1, offset); + sendOnly(":CHAN%zu:OFFSET %1.2E", i + 1, offset); lock_guard lock(m_cacheMutex); m_channelOffsets[i] = offset; @@ -1573,7 +1573,7 @@ float MagnovaOscilloscope::GetChannelVoltageRange(size_t i, size_t /*stream*/) string reply; - reply = converse(":CHANNEL%zu:SCALE?", i + 1); + reply = converse(":CHAN%zu:SCALE?", i + 1); float volts_per_div; sscanf(reply.c_str(), "%f", &volts_per_div); @@ -1593,12 +1593,28 @@ void MagnovaOscilloscope::SetChannelVoltageRange(size_t i, size_t /*stream*/, fl float vdiv = range / 8; m_channelVoltageRanges[i] = range; - sendOnly(":CHANNEL%zu:SCALE %.4f", i + 1, vdiv); + sendOnly(":CHAN%zu:SCALE %.4f", i + 1, vdiv); } vector MagnovaOscilloscope::GetSampleRatesNonInterleaved() -{ // Changing srate is not supported by Magnova scope +{ + const uint64_t k = 1000; + const uint64_t m = k*k; + vector ret; + switch(m_modelid) + { + // -------------------------------------------------- + case MODEL_MAGNOVA_BMO: + ret = {1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 1*k, 2*k, 4*k, 5*k, 10*k, 20*k, 40*k, 50*k, 100*k, 200*k, 400*k, 500*k, 1*m, 2*m, 4*m, 5*m, 10*m, 20*m, 40*m, 50*m, 100*m, 200*m, 400*m, 800*m, 1600*m }; + break; + // -------------------------------------------------- + default: + LogError("Unknown scope type\n"); + break; + // -------------------------------------------------- + } + return ret; } @@ -1611,7 +1627,7 @@ vector MagnovaOscilloscope::GetSampleDepthsNonInterleaved() { vector ret; // TODO - ret = {20 * 1000, 200 * 1000, 2000 * 1000, 20 * 1000 * 1000, 200 * 1000 * 1000}; + ret = {20 * 1000, 50 * 1000, 100 * 1000, 200 * 1000, 500 * 1000, 1000 * 1000, 2000 * 1000, 5000 * 1000, 10 * 1000 * 1000, 20 * 1000 * 1000, 50 * 1000 * 1000, 200 * 1000 * 1000, 327151616}; return ret; } @@ -1677,16 +1693,9 @@ uint64_t MagnovaOscilloscope::GetSampleDepth() void MagnovaOscilloscope::SetSampleDepth(uint64_t depth) { - //Need to lock the mutex when setting depth because of the quirks around needing to change trigger mode too + //Need to lock the transport mutex when setting depth to prevent changing depth during an acquisition lock_guard lock(m_transport->GetMutex()); - //Save original sample rate (scope often changes sample rate when adjusting memory depth) - // uint64_t rate = GetSampleRate(); - // Get Trigger State to restore it after setting changing memory detph - // TriggerMode triggerMode = PollTrigger(); - - // we can not change memory size in Run/Stop mode - // sendOnly(":AUTO"); switch(m_modelid) { case MODEL_MAGNOVA_BMO: @@ -1698,24 +1707,38 @@ void MagnovaOscilloscope::SetSampleDepth(uint64_t depth) break; // -------------------------------------------------- } - /*if(IsTriggerArmed()) - { // restart trigger - sendOnly(":SINGLE"); - } - else - { // Restore previous trigger mode - sendOnly(":%s", ((triggerMode == TRIGGER_MODE_STOP) ? "STOP" : "AUTO")); - }*/ - m_memoryDepthValid = false; - - //restore old sample rate - //SetSampleRate(rate); + m_sampleRateValid = false; } -void MagnovaOscilloscope::SetSampleRate(uint64_t /*rate*/) -{ // Not supported by Magnova - return; +void MagnovaOscilloscope::SetSampleRate(uint64_t rate) +{ + //Need to lock the transport mutex when setting rate to prevent changing rate during an acquisition + lock_guard lock(m_transport->GetMutex()); + + double sampletime = GetSampleDepth() / (double)rate; + double scale = sampletime / 25; + + switch(m_modelid) + { + case MODEL_MAGNOVA_BMO: + { + char tmp[128]; + snprintf(tmp, sizeof(tmp), "%1.0E", scale); + if(tmp[0] == '3') + tmp[0] = '2'; + sendOnly(":TIMEBASE:SCALE %s", tmp); + } + break; + + // -------------------------------------------------- + default: + LogError("Unknown scope type\n"); + break; + // -------------------------------------------------- + } + m_sampleRateValid = false; + m_memoryDepthValid = false; } void MagnovaOscilloscope::EnableTriggerOutput() From 5caab4b19dce58d14ef42d7c0df6dd5526ed2681 Mon Sep 17 00:00:00 2001 From: fredzo Date: Thu, 4 Dec 2025 09:10:50 +0100 Subject: [PATCH 03/33] Added digital threshold support. --- scopehal/MagnovaOscilloscope.cpp | 59 +++++++------------------------- scopehal/MagnovaOscilloscope.h | 3 ++ 2 files changed, 15 insertions(+), 47 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 8210ad8f..644945a3 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -52,15 +52,7 @@ using namespace std; -static const struct -{ - const char* name; - float val; -} c_sds2000xp_threshold_table[] = {{"TTL", 1.5F}, {"CMOS", 1.65F}, {"LVCMOS33", 1.65F}, {"LVCMOS25", 1.25F}, {NULL, 0}}; - static const std::chrono::milliseconds c_trigger_delay(1000); // Delay required when forcing trigger -static const char* c_custom_thresh = "CUSTOM,"; // Prepend string for custom digital threshold -static const float c_thresh_thresh = 0.01f; // Zero equivalence threshold for fp comparisons //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Construction / destruction @@ -1817,7 +1809,7 @@ void MagnovaOscilloscope::SetDeskewForChannel(size_t channel, int64_t skew) } else { // Digital channels - sendOnly(":DIG:DESK%s %1.2E", ((channel - m_digitalChannelBase) < 8) ? "0to7" : "8to15", skew * SECONDS_PER_FS); + sendOnly(":DIG:DESK%s %1.2E", GetDigitalChannelBankName(channel), skew * SECONDS_PER_FS); } //Update cache @@ -1846,7 +1838,7 @@ int64_t MagnovaOscilloscope::GetDeskewForChannel(size_t channel) } else { // Digital channels - reply = converse(":DIG:DESK%s?", ((channel - m_digitalChannelBase) < 8) ? "0to7" : "8to15"); + reply = converse(":DIG:DESK%s?", GetDigitalChannelBankName(channel)); } //Value comes back as floating point ps @@ -1911,6 +1903,12 @@ void MagnovaOscilloscope::SetADCMode(size_t /*channel*/, size_t /* mode */) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Logic analyzer configuration +std::string MagnovaOscilloscope::GetDigitalChannelBankName(size_t channel) +{ + return ((channel - m_digitalChannelBase) < 8) ? "0to7" : "8to15"; +} + + vector MagnovaOscilloscope::GetDigitalBanks() { vector banks; @@ -1978,28 +1976,10 @@ float MagnovaOscilloscope::GetDigitalThreshold(size_t channel) return m_channelDigitalThresholds[channel]; } - float result = 0.0f; - - string r = converse(":DIGITAL:THRESHOLD%d?", (channel / 8) + 1).c_str(); - - // Look through the threshold table to see if theres a string match, return it if so - uint32_t i = 0; - while((c_sds2000xp_threshold_table[i].name) && - (strncmp(c_sds2000xp_threshold_table[i].name, r.c_str(), strlen(c_sds2000xp_threshold_table[i].name)))) - i++; + float result; - if(c_sds2000xp_threshold_table[i].name) - { - result = c_sds2000xp_threshold_table[i].val; - } - else if(!strncmp(r.c_str(), c_custom_thresh, strlen(c_custom_thresh))) - { // Didn't match a standard, check for custom - result = strtof(&(r.c_str()[strlen(c_custom_thresh)]), NULL); - } - else - { - LogWarning("GetDigitalThreshold unrecognised value [%s]\n", r.c_str()); - } + string reply = converse(":DIG:THRESHOLD%s?", GetDigitalChannelBankName(channel)); + sscanf(reply.c_str(), "%f", &result); lock_guard lock(m_cacheMutex); m_channelDigitalThresholds[channel] = result; @@ -2015,22 +1995,7 @@ void MagnovaOscilloscope::SetDigitalThreshold(size_t channel, float level) { channel -= m_analogChannelCount; - // Search through standard thresholds to see if one matches - uint32_t i = 0; - while(( - (c_sds2000xp_threshold_table[i].name) && (fabsf(level - c_sds2000xp_threshold_table[i].val)) > c_thresh_thresh)) - i++; - - if(c_sds2000xp_threshold_table[i].name) - sendOnly(":DIGITAL:THRESHOLD%zu %s", (channel / 8) + 1, (c_sds2000xp_threshold_table[i].name)); - else - { - do - { - sendOnly(":DIGITAL:THRESHOLD%zu CUSTOM,%1.2E", (channel / 8) + 1, level); - - } while(fabsf((GetDigitalThreshold(channel + m_analogChannelCount) - level)) > 0.1f); - } + sendOnly(":DIG:THRESHOLD%s %1.2E", GetDigitalChannelBankName(channel), level); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h index 62d4e247..8c1bf525 100644 --- a/scopehal/MagnovaOscilloscope.h +++ b/scopehal/MagnovaOscilloscope.h @@ -262,6 +262,9 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope void PrepareAcquisition(); + std::string GetDigitalChannelBankName(size_t channel); + + std::string GetPossiblyEmptyString(const std::string& property); size_t ReadWaveformBlock(std::vector* data, std::function progress = nullptr); From b3b86d0401777f1ce9c41acefeb5ae5a35a366f1 Mon Sep 17 00:00:00 2001 From: fredzo Date: Fri, 5 Dec 2025 15:47:37 +0100 Subject: [PATCH 04/33] Added sendWithAck() method for time sensitive commands. Added GetActiveChannelCount() method to dertermine memordy depth according to active channels and digital probe. Added digital channels. Try and fix memory leak. --- scopehal/MagnovaOscilloscope.cpp | 289 +++++++++++++++++++++---------- scopehal/MagnovaOscilloscope.h | 3 + 2 files changed, 200 insertions(+), 92 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 644945a3..c19b7385 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -118,6 +118,24 @@ void MagnovaOscilloscope::sendOnly(const char* fmt, ...) m_transport->SendCommandQueued(opString); } +bool MagnovaOscilloscope::sendWithAck(const char* fmt, ...) +{ + string ret; + char opString[128]; + va_list va; + va_start(va, fmt); + vsnprintf(opString, sizeof(opString), fmt, va); + va_end(va); + + std::string result(opString); + result += ";*OPC?"; + + ret = m_transport->SendCommandQueuedWithReply(result.c_str(), false); + if(ret.length() == 0) + ret = m_transport->ReadReply(); // Sometimes the Magnova returns en empty string and then the actual reply + return (ret == "1"); +} + void MagnovaOscilloscope::flush() { m_transport->ReadReply(); @@ -250,6 +268,7 @@ void MagnovaOscilloscope::DetectOptions() // string options = converse("*OPT?"); m_hasFunctionGen = true; m_hasLA = true; + AddDigitalChannels(16); } /** @@ -439,15 +458,20 @@ bool MagnovaOscilloscope::IsChannelEnabled(size_t i) } else if(i < m_analogAndDigitalChannelCount) { - //Digital + //Digital => first check digital module is ON + string module = converse(":DIG:STAT?"); + bool isOn = false; - //See if the channel is on (digital channel numbers are 0 based) - size_t nchan = i - m_analogChannelCount; - string str = converse(":DIG%zu:STAT?", nchan); + if(module == "ON") + { //See if the channel is on (digital channel numbers are 0 based) + size_t nchan = i - m_analogChannelCount; + string channel = converse(":DIG%zu:STAT?", nchan); + isOn = (channel == "ON"); + } lock_guard lock2(m_cacheMutex); // OFF can bee "SUPPORT_OFF" if all digital channels are off - m_channelsEnabled[i] = (str == "ON") ? true : false; + m_channelsEnabled[i] = isOn; } lock_guard lock2(m_cacheMutex); @@ -463,12 +487,12 @@ void MagnovaOscilloscope::EnableChannel(size_t i) //If this is an analog channel, just toggle it if(i < m_analogChannelCount) { - sendOnly(":CHAN%zu:STAT ON", i + 1); + sendWithAck(":CHAN%zu:STAT ON", i + 1); } else if(i < m_analogAndDigitalChannelCount) { //Digital channel (digital channel numbers are 0 based) - sendOnly(":DIG%d:STAT ON", i - m_analogChannelCount); + sendWithAck(":DIG%d:STAT ON", i - m_analogChannelCount); } else if(i == m_extTrigChannel->GetIndex()) { @@ -503,14 +527,13 @@ void MagnovaOscilloscope::DisableChannel(size_t i) if(i < m_analogChannelCount) { - sendOnly(":CHAN%zu:STAT OFF", i + 1); + sendWithAck(":CHAN%zu:STAT OFF", i + 1); } else if(i < m_analogAndDigitalChannelCount) { //Digital channel - //Disable this channel (digital channel numbers are 0 based) - sendOnly(":DIG%zu:STAT OFF", i - m_analogChannelCount); + sendWithAck(":DIG%zu:STAT OFF", i - m_analogChannelCount); } else if(i == m_extTrigChannel->GetIndex()) { @@ -520,6 +543,7 @@ void MagnovaOscilloscope::DisableChannel(size_t i) //Sample rate and memory depth can change if interleaving state changed if(IsInterleaving() != wasInterleaving) { + lock_guard lock2(m_cacheMutex); m_memoryDepthValid = false; m_sampleRateValid = false; } @@ -950,25 +974,67 @@ void MagnovaOscilloscope::BulkCheckChannelEnableState() { vector uncached; + bool hasUncachedDigital = false; { lock_guard lock(m_cacheMutex); - //Check enable state in the cache. for(unsigned int i = 0; i < m_analogAndDigitalChannelCount; i++) { if(m_channelsEnabled.find(i) == m_channelsEnabled.end()) + { uncached.push_back(i); + if(i >= m_analogChannelCount) hasUncachedDigital = true; + } } } + bool digitalModuleOn = false; + if(hasUncachedDigital) + { //Digital => first check digital module is ON + string module = converse(":DIG:STAT?"); + digitalModuleOn = (module == "ON"); + } for(auto i : uncached) { - string reply = (i < m_analogChannelCount) ? converse(":CHANNEL%d:SWITCH?", i + 1) : converse(":DIGITAL:D%d?", (i - m_analogChannelCount)); - // OFF can bee "SUPPORT_OFF" if all digital channels are off - m_channelsEnabled[i] = (reply == "ON") ? true : false; + if((i < m_analogChannelCount)) + { // Analog + m_channelsEnabled[i] = (converse(":CHAN%zu:STAT?", i + 1) == "ON"); + } + else + { // Digital + m_channelsEnabled[i] = digitalModuleOn && (converse(":DIG%zu:STAT?", (i - m_analogChannelCount)) == "ON"); + } + } +} + +/** + @brief Returns the number of active analog channels and digital probes to determine Memory Depth available per channel + */ +unsigned int MagnovaOscilloscope::GetActiveChannelsCount() +{ + BulkCheckChannelEnableState(); + unsigned int result = 0; + for(unsigned int i = 0; i < m_analogChannelCount; i++) + { // Check all analog channels + if(IsChannelEnabled(i)) result++; + } + bool probe0to7Active = false; + bool probe8to15Active = false; + unsigned int halfDigitalChannels = m_digitalChannelCount/2; + for(unsigned int i = 0; i < halfDigitalChannels; i++) + { // Check digital channels for bank1 + if(IsChannelEnabled(i+m_analogChannelCount)) probe0to7Active = true; } + for(unsigned int i = halfDigitalChannels; i < m_digitalChannelCount; i++) + { // Check digital channels for bank2 + if(IsChannelEnabled(i+m_analogChannelCount)) probe8to15Active = true; + } + if(probe0to7Active) result++; + if(probe8to15Active) result++; + return result; } + time_t MagnovaOscilloscope::ExtractTimestamp(const std::string& /* timeString */, double& basetime) { /* @@ -1268,9 +1334,9 @@ vector MagnovaOscilloscope::ProcessDigitalWaveform( bool MagnovaOscilloscope::AcquireData() { // Transfer buffers - std::vector* analogWaveformData[MAX_ANALOG] {nullptr}; + std::vector analogWaveformData[MAX_ANALOG]; int analogWaveformDataSize[MAX_ANALOG] {0}; - std::vector* digitalWaveformDataBytes[MAX_DIGITAL] {nullptr}; + std::vector digitalWaveformDataBytes[MAX_DIGITAL]; int digitalWaveformDataSize[MAX_DIGITAL] {0}; std::string digitalWaveformData; @@ -1323,13 +1389,12 @@ bool MagnovaOscilloscope::AcquireData() { if(analogEnabled[i]) { // Allocate buffer - analogWaveformData[i] = new std::vector; // Run the same loop for paginated and unpagnated mode, if unpaginated we will run it only once m_transport->SendCommand(":CHAN" + to_string(i + 1) + ":DATA:PACK? ALL,RAW"); - size_t readBytes = ReadWaveformBlock(analogWaveformData[i], [i, this] (float progress) { ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, progress); }); + size_t readBytes = ReadWaveformBlock(&analogWaveformData[i], [i, this] (float progress) { ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, progress); }); analogWaveformDataSize[i] = readBytes; LogDebug("Parsing metadata...\n"); - auto metadata = parseMetadata(*analogWaveformData[i]); + auto metadata = parseMetadata(analogWaveformData[i]); if(metadata) { LogDebug("Metadata parsed, starTime = %f\n",metadata->startTime); @@ -1347,9 +1412,8 @@ bool MagnovaOscilloscope::AcquireData() { if(digitalEnabled[i]) { // Allocate buffer - digitalWaveformDataBytes[i] = new std::vector; m_transport->SendCommand(":DIG" + to_string(i + 1) + ":DATA:PACK? ALL,RAW"); - size_t readBytes = ReadWaveformBlock(digitalWaveformDataBytes[i], [i, this] (float progress) { ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, progress); }); + size_t readBytes = ReadWaveformBlock(&digitalWaveformDataBytes[i], [i, this] (float progress) { ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, progress); }); digitalWaveformDataSize[i] = readBytes; ChannelsDownloadStatusUpdate(i + m_analogChannelCount, InstrumentChannel::DownloadState::DOWNLOAD_FINISHED, 1.0); } @@ -1363,6 +1427,12 @@ bool MagnovaOscilloscope::AcquireData() sendOnly(":SINGLE"); m_triggerArmed = true; } + // TODO => is this needed ? + /* + else + { // It was one shot acquisition, disarm trigger + m_triggerArmed = false; + }*/ //Process analog waveforms waveforms.resize(m_analogChannelCount); @@ -1371,7 +1441,7 @@ bool MagnovaOscilloscope::AcquireData() if(analogEnabled[i]) { waveforms[i] = ProcessAnalogWaveform( - *analogWaveformData[i], + analogWaveformData[i], analogWaveformDataSize[i], num_sequences, ttime, @@ -1399,7 +1469,7 @@ bool MagnovaOscilloscope::AcquireData() if(digitalEnabled[i]) { digitalWaveforms[i] = ProcessDigitalWaveform( - *digitalWaveformDataBytes[i], + digitalWaveformDataBytes[i], digitalWaveformDataSize[i], num_sequences, ttime, @@ -1437,18 +1507,6 @@ bool MagnovaOscilloscope::AcquireData() } m_pendingWaveformsMutex.unlock(); - //Clean up - for(int i = 0; i < MAX_ANALOG; i++) - { - if(analogWaveformData[i] != nullptr) - delete analogWaveformData[i]; - } - for(int i = 0; i < MAX_DIGITAL; i++) - { - if(digitalWaveformDataBytes[i] != nullptr) - delete digitalWaveformDataBytes[i]; - } - double dt = GetTime() - start; LogTrace("Waveform download and processing took %.3f ms\n", dt * 1000); return true; @@ -1545,7 +1603,7 @@ void MagnovaOscilloscope::SetChannelOffset(size_t i, size_t /*stream*/, float of if(i >= m_analogChannelCount) return; - sendOnly(":CHAN%zu:OFFSET %1.2E", i + 1, offset); + sendWithAck(":CHAN%zu:OFFSET %1.2E", i + 1, offset); lock_guard lock(m_cacheMutex); m_channelOffsets[i] = offset; @@ -1583,9 +1641,13 @@ void MagnovaOscilloscope::SetChannelVoltageRange(size_t i, size_t /*stream*/, fl return; float vdiv = range / 8; - m_channelVoltageRanges[i] = range; - sendOnly(":CHAN%zu:SCALE %.4f", i + 1, vdiv); + sendWithAck(":CHAN%zu:SCALE %.4f", i + 1, vdiv); + + //Don't update the cache because the scope is likely to round the value + //If we query the instrument later, the cache will be updated then. + lock_guard lock2(m_cacheMutex); + m_channelVoltageRanges.erase(i); } vector MagnovaOscilloscope::GetSampleRatesNonInterleaved() @@ -1598,7 +1660,7 @@ vector MagnovaOscilloscope::GetSampleRatesNonInterleaved() { // -------------------------------------------------- case MODEL_MAGNOVA_BMO: - ret = {1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 1*k, 2*k, 4*k, 5*k, 10*k, 20*k, 40*k, 50*k, 100*k, 200*k, 400*k, 500*k, 1*m, 2*m, 4*m, 5*m, 10*m, 20*m, 40*m, 50*m, 100*m, 200*m, 400*m, 800*m, 1600*m }; + ret = {1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 1*k, 2*k, 4*k, 5*k, 10*k, 20*k, 40*k, 50*k, 100*k, 200*k, 400*k, 500*k, 1*m, 2*m, 4*m, 5*m, 10*m, 20*m, 40*m, 50*m, 100*m, 200*m, 400*m, 500*m, 800*m, 1600*m }; break; // -------------------------------------------------- default: @@ -1618,8 +1680,28 @@ vector MagnovaOscilloscope::GetSampleRatesInterleaved() vector MagnovaOscilloscope::GetSampleDepthsNonInterleaved() { vector ret; - // TODO - ret = {20 * 1000, 50 * 1000, 100 * 1000, 200 * 1000, 500 * 1000, 1000 * 1000, 2000 * 1000, 5000 * 1000, 10 * 1000 * 1000, 20 * 1000 * 1000, 50 * 1000 * 1000, 200 * 1000 * 1000, 327151616}; + // Sample depths depend on the number of active analog channels and digital probes : + // 1 analog channel or digital probe: 327.2 Mpts + // 2 analog channels / digital probes: 163.6 Mpts per channel + // 3-4 analog channels / digital probes: 81.8 Mpts per channel + // ≥ 5 analog channels / digital probes: 40.9 Mpts per channel + unsigned int activeChannels = GetActiveChannelsCount(); + if(activeChannels <= 1) + { + ret = {20 * 1000, 50 * 1000, 100 * 1000, 200 * 1000, 500 * 1000, 1000 * 1000, 2000 * 1000, 5000 * 1000, 10 * 1000 * 1000, 20 * 1000 * 1000, 50 * 1000 * 1000, 200 * 1000 * 1000, 327151616}; + } + else if(activeChannels == 2) + { + ret = {10 * 1000, 25 * 1000, 50 * 1000, 100 * 1000, 250 * 1000, 500 * 1000, 1000 * 1000, 2500 * 1000, 5 * 1000 * 1000, 10 * 1000 * 1000, 25 * 1000 * 1000, 50 * 1000 * 1000, 100 * 1000 * 1000, 163575808}; + } + else if(activeChannels == 3 || activeChannels == 4) + { + ret = {5 * 1000, 12500 , 25 * 1000, 50 * 1000, 125 * 1000, 250 * 1000, 500 * 1000, 1250 * 1000, 2500 * 1000, 5 * 1000 * 1000, 12500 * 1000, 25 * 1000 * 1000, 50 * 1000 * 1000, 81787904}; + } + else + { + ret = {2500, 6250 , 12500, 25 * 1000, 62500, 125 * 1000, 250 * 1000, 625 * 1000, 1250 * 1000, 2500 * 1000, 6250 * 1000, 12500 * 1000, 25 * 1000 * 1000, 40893952}; + } return ret; } @@ -1632,11 +1714,21 @@ set MagnovaOscilloscope::GetInterleaveC { set ret; - //All scopes normally interleave channels 1/2 and 3/4. - //If both channels in either pair is in use, that's a problem. - ret.emplace(InterleaveConflict(GetOscilloscopeChannel(0), GetOscilloscopeChannel(1))); - if(m_analogChannelCount > 2) - ret.emplace(InterleaveConflict(GetOscilloscopeChannel(2), GetOscilloscopeChannel(3))); + switch(m_modelid) + { + // Magnova BMO interleaves if any of channel 3 or 4 is active + case MODEL_MAGNOVA_BMO: + ret.emplace(InterleaveConflict(GetOscilloscopeChannel(0), GetOscilloscopeChannel(2))); + ret.emplace(InterleaveConflict(GetOscilloscopeChannel(0), GetOscilloscopeChannel(3))); + ret.emplace(InterleaveConflict(GetOscilloscopeChannel(1), GetOscilloscopeChannel(2))); + ret.emplace(InterleaveConflict(GetOscilloscopeChannel(1), GetOscilloscopeChannel(3))); + break; + // -------------------------------------------------- + default: + LogError("Unknown scope type\n"); + break; + // -------------------------------------------------- + } return ret; } @@ -1656,7 +1748,7 @@ uint64_t MagnovaOscilloscope::GetSampleRate() } else { - + // TODO protocole error } } return m_sampleRate; @@ -1669,66 +1761,73 @@ uint64_t MagnovaOscilloscope::GetSampleDepth() { // Possible values are : AUTo, AFASt, Integer in pts string reply = converse(":ACQUIRE:MDEPTH?"); if(reply == "AUTo" || reply == "AFASt") - { - m_memoryDepth = 0; - m_memoryDepthValid = true; + { // Default to 10Ms + m_memoryDepth = 10000000; } else { f = Unit(Unit::UNIT_SAMPLEDEPTH).ParseString(reply); m_memoryDepth = static_cast(f); - m_memoryDepthValid = true; } + lock_guard lock2(m_cacheMutex); + m_memoryDepthValid = true; } return m_memoryDepth; } void MagnovaOscilloscope::SetSampleDepth(uint64_t depth) { - //Need to lock the transport mutex when setting depth to prevent changing depth during an acquisition - lock_guard lock(m_transport->GetMutex()); - - switch(m_modelid) - { - case MODEL_MAGNOVA_BMO: - sendOnly("ACQUIRE:MDEPTH %" PRIu64 "",depth); - break; - // -------------------------------------------------- - default: - LogError("Unknown scope type\n"); - break; + { //Need to lock the transport mutex when setting depth to prevent changing depth during an acquisition + lock_guard lock(m_transport->GetMutex()); + switch(m_modelid) + { + case MODEL_MAGNOVA_BMO: + sendWithAck("ACQUIRE:MDEPTH %" PRIu64 "",depth); + break; // -------------------------------------------------- + default: + LogError("Unknown scope type\n"); + break; + // -------------------------------------------------- + } } + //Don't update the cache because the scope is likely to round the value + //If we query the instrument later, the cache will be updated then. + lock_guard lock2(m_cacheMutex); m_memoryDepthValid = false; m_sampleRateValid = false; } void MagnovaOscilloscope::SetSampleRate(uint64_t rate) { - //Need to lock the transport mutex when setting rate to prevent changing rate during an acquisition - lock_guard lock(m_transport->GetMutex()); + { //Need to lock the transport mutex when setting rate to prevent changing rate during an acquisition + lock_guard lock(m_transport->GetMutex()); - double sampletime = GetSampleDepth() / (double)rate; - double scale = sampletime / 25; + double sampletime = GetSampleDepth() / (double)rate; + double scale = sampletime / 25; - switch(m_modelid) - { - case MODEL_MAGNOVA_BMO: - { - char tmp[128]; - snprintf(tmp, sizeof(tmp), "%1.0E", scale); - if(tmp[0] == '3') - tmp[0] = '2'; - sendOnly(":TIMEBASE:SCALE %s", tmp); - } - break; + switch(m_modelid) + { + case MODEL_MAGNOVA_BMO: + { + char tmp[128]; + snprintf(tmp, sizeof(tmp), "%1.0E", scale); + if(tmp[0] == '3') + tmp[0] = '2'; + sendWithAck(":TIMEBASE:SCALE %s", tmp); + } + break; - // -------------------------------------------------- - default: - LogError("Unknown scope type\n"); - break; // -------------------------------------------------- + default: + LogError("Unknown scope type\n"); + break; + // -------------------------------------------------- + } } + //Don't update the cache because the scope is likely to round the value + //If we query the instrument later, the cache will be updated then. + lock_guard lock2(m_cacheMutex); m_sampleRateValid = false; m_memoryDepthValid = false; } @@ -1761,7 +1860,7 @@ void MagnovaOscilloscope::SetTriggerOffset(int64_t offset) int64_t halfdepth = GetSampleDepth() / 2; int64_t halfwidth = static_cast(round(FS_PER_SECOND * halfdepth / rate)); - sendOnly(":TIMebase:OFFSet %1.2E", (offset - halfwidth) * SECONDS_PER_FS); + sendWithAck(":TIMebase:OFFSet %1.2E", (offset - halfwidth) * SECONDS_PER_FS); //Don't update the cache because the scope is likely to round the offset we ask for. //If we query the instrument later, the cache will be updated then. @@ -1995,7 +2094,13 @@ void MagnovaOscilloscope::SetDigitalThreshold(size_t channel, float level) { channel -= m_analogChannelCount; - sendOnly(":DIG:THRESHOLD%s %1.2E", GetDigitalChannelBankName(channel), level); + sendWithAck(":DIG:THRESHOLD%s %1.2E", GetDigitalChannelBankName(channel), level); + + //Don't update the cache because the scope is likely to round the offset we ask for. + //If we query the instrument later, the cache will be updated then. + lock_guard lock2(m_cacheMutex); + // TODO handel bank cache rather than channel + m_channelDigitalThresholds.erase(channel); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -2780,7 +2885,7 @@ void MagnovaOscilloscope::SetFunctionChannelActive(int chan, bool on) else imp = "HZ"; - m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":OUTP " + state + ",LOAD," + imp); + sendWithAck((m_channels[chan]->GetHwname() + ":OUTP " + state + ",LOAD," + imp).c_str()); lock_guard lock(m_cacheMutex); m_awgEnabled[chan] = on; @@ -2803,7 +2908,7 @@ float MagnovaOscilloscope::GetFunctionChannelDutyCycle(int chan) void MagnovaOscilloscope::SetFunctionChannelDutyCycle(int chan, float duty) { - m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":BSWV DUTY," + to_string(round(duty * 100))); + sendWithAck((m_channels[chan]->GetHwname() + ":BSWV DUTY," + to_string(round(duty * 100))).c_str()); lock_guard lock(m_cacheMutex); m_awgDutyCycle[chan] = duty; @@ -2826,7 +2931,7 @@ float MagnovaOscilloscope::GetFunctionChannelAmplitude(int chan) void MagnovaOscilloscope::SetFunctionChannelAmplitude(int chan, float amplitude) { - m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":BSWV AMP," + to_string(amplitude)); + sendWithAck((m_channels[chan]->GetHwname() + ":BSWV AMP," + to_string(amplitude)).c_str()); lock_guard lock(m_cacheMutex); m_awgRange[chan] = amplitude; @@ -2849,7 +2954,7 @@ float MagnovaOscilloscope::GetFunctionChannelOffset(int chan) void MagnovaOscilloscope::SetFunctionChannelOffset(int chan, float offset) { - m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":BSWV OFST," + to_string(offset)); + sendWithAck((m_channels[chan]->GetHwname() + ":BSWV OFST," + to_string(offset)).c_str()); lock_guard lock(m_cacheMutex); m_awgOffset[chan] = offset; @@ -2872,7 +2977,7 @@ float MagnovaOscilloscope::GetFunctionChannelFrequency(int chan) void MagnovaOscilloscope::SetFunctionChannelFrequency(int chan, float hz) { - m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":BSWV FRQ," + to_string(hz)); + sendWithAck((m_channels[chan]->GetHwname() + ":BSWV FRQ," + to_string(hz)).c_str()); lock_guard lock(m_cacheMutex); m_awgFrequency[chan] = hz; @@ -3278,7 +3383,7 @@ void MagnovaOscilloscope::SetFunctionChannelShape(int chan, FunctionGenerator::W } //Select type - m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":BSWV WVTP," + basicType); + sendWithAck((m_channels[chan]->GetHwname() + ":BSWV WVTP," + basicType).c_str()); if(basicType == "ARB") { //Returns map of memory slots ("M10") to waveform names @@ -3287,7 +3392,7 @@ void MagnovaOscilloscope::SetFunctionChannelShape(int chan, FunctionGenerator::W auto stl = m_transport->SendCommandQueuedWithReply("STL?"); auto arbmap = ParseCommaSeparatedNameValueList(stl, false); - m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":ARWV INDEX," + arbmap[arbType].substr(1)); + sendWithAck((m_channels[chan]->GetHwname() + ":ARWV INDEX," + arbmap[arbType].substr(1)).c_str()); } //Update cache @@ -3330,7 +3435,7 @@ void MagnovaOscilloscope::SetFunctionChannelOutputImpedance(int chan, FunctionGe else imp = "HZ"; - m_transport->SendCommandQueued(m_channels[chan]->GetHwname() + ":OUTP " + state + ",LOAD," + imp); + sendWithAck((m_channels[chan]->GetHwname() + ":OUTP " + state + ",LOAD," + imp).c_str()); lock_guard lock(m_cacheMutex); m_awgImpedance[chan] = z; diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h index 8c1bf525..0e58ec76 100644 --- a/scopehal/MagnovaOscilloscope.h +++ b/scopehal/MagnovaOscilloscope.h @@ -66,6 +66,7 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope private: std::string converse(const char* fmt, ...); void sendOnly(const char* fmt, ...); + bool sendWithAck(const char* fmt, ...); void flush(); void flushWaveformData(); @@ -260,6 +261,8 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope void BulkCheckChannelEnableState(); + unsigned int GetActiveChannelsCount(); + void PrepareAcquisition(); std::string GetDigitalChannelBankName(size_t channel); From e11f8dd81811740754ca18473812cbfc8aca5fef Mon Sep 17 00:00:00 2001 From: fredzo Date: Fri, 5 Dec 2025 21:18:35 +0100 Subject: [PATCH 05/33] Cleanup --- scopehal/MagnovaOscilloscope.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index c19b7385..21e96a9c 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -1195,7 +1195,7 @@ vector MagnovaOscilloscope::ProcessAnalogWaveform( for(size_t j = 0; j < num_sequences; j++) { //Set up the capture we're going to store our data into - auto cap = new UniformAnalogWaveform; + auto cap = AllocateAnalogWaveform(m_nickname + "." + GetChannel(ch)->GetHwname()); cap->m_timescale = round(interval); cap->m_triggerPhase = h_off_frac; @@ -1338,7 +1338,6 @@ bool MagnovaOscilloscope::AcquireData() int analogWaveformDataSize[MAX_ANALOG] {0}; std::vector digitalWaveformDataBytes[MAX_DIGITAL]; int digitalWaveformDataSize[MAX_DIGITAL] {0}; - std::string digitalWaveformData; //State for this acquisition (may be more than one waveform) uint32_t num_sequences = 1; @@ -1393,12 +1392,6 @@ bool MagnovaOscilloscope::AcquireData() m_transport->SendCommand(":CHAN" + to_string(i + 1) + ":DATA:PACK? ALL,RAW"); size_t readBytes = ReadWaveformBlock(&analogWaveformData[i], [i, this] (float progress) { ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, progress); }); analogWaveformDataSize[i] = readBytes; - LogDebug("Parsing metadata...\n"); - auto metadata = parseMetadata(analogWaveformData[i]); - if(metadata) - { - LogDebug("Metadata parsed, starTime = %f\n",metadata->startTime); - } ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_FINISHED, 1.0); } } @@ -1493,6 +1486,15 @@ bool MagnovaOscilloscope::AcquireData() // Tell the download monitor that waveform download has finished ChannelsDownloadFinished(); + for(int i=0; i Date: Fri, 5 Dec 2025 21:41:58 +0100 Subject: [PATCH 06/33] Fixed CI compilation. --- scopehal/MagnovaOscilloscope.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 21e96a9c..d6ed1f0a 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -1350,7 +1350,7 @@ bool MagnovaOscilloscope::AcquireData() bool analogEnabled[MAX_ANALOG] = {false}; bool digitalEnabled[MAX_DIGITAL] = {false}; bool anyDigitalEnabled = false; - bool anyAnalogEnabled = true; + bool anyAnalogEnabled = false; double* pwtime = NULL; //Acquire the data (but don't parse it) @@ -1910,7 +1910,7 @@ void MagnovaOscilloscope::SetDeskewForChannel(size_t channel, int64_t skew) } else { // Digital channels - sendOnly(":DIG:DESK%s %1.2E", GetDigitalChannelBankName(channel), skew * SECONDS_PER_FS); + sendOnly(":DIG:DESK%s %1.2E", GetDigitalChannelBankName(channel).c_str(), skew * SECONDS_PER_FS); } //Update cache @@ -1939,7 +1939,7 @@ int64_t MagnovaOscilloscope::GetDeskewForChannel(size_t channel) } else { // Digital channels - reply = converse(":DIG:DESK%s?", GetDigitalChannelBankName(channel)); + reply = converse(":DIG:DESK%s?", GetDigitalChannelBankName(channel).c_str()); } //Value comes back as floating point ps @@ -2079,7 +2079,7 @@ float MagnovaOscilloscope::GetDigitalThreshold(size_t channel) float result; - string reply = converse(":DIG:THRESHOLD%s?", GetDigitalChannelBankName(channel)); + string reply = converse(":DIG:THRESHOLD%s?", GetDigitalChannelBankName(channel).c_str()); sscanf(reply.c_str(), "%f", &result); lock_guard lock(m_cacheMutex); @@ -2096,7 +2096,7 @@ void MagnovaOscilloscope::SetDigitalThreshold(size_t channel, float level) { channel -= m_analogChannelCount; - sendWithAck(":DIG:THRESHOLD%s %1.2E", GetDigitalChannelBankName(channel), level); + sendWithAck(":DIG:THRESHOLD%s %1.2E", GetDigitalChannelBankName(channel).c_str(), level); //Don't update the cache because the scope is likely to round the offset we ask for. //If we query the instrument later, the cache will be updated then. From b62105ef58362767aee0e715e2d9c184e60045d9 Mon Sep 17 00:00:00 2001 From: fredzo Date: Fri, 5 Dec 2025 22:00:03 +0100 Subject: [PATCH 07/33] More CI build fix. --- scopehal/MagnovaOscilloscope.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index d6ed1f0a..b498147f 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -919,6 +919,7 @@ std::optional MagnovaOscilloscope::parseMetadata( } catch (const std::exception& e) { LogError("Error parsing metadata: %s.\n", e.what()); + return std::nullopt; } } @@ -1350,7 +1351,6 @@ bool MagnovaOscilloscope::AcquireData() bool analogEnabled[MAX_ANALOG] = {false}; bool digitalEnabled[MAX_DIGITAL] = {false}; bool anyDigitalEnabled = false; - bool anyAnalogEnabled = false; double* pwtime = NULL; //Acquire the data (but don't parse it) @@ -1363,11 +1363,6 @@ bool MagnovaOscilloscope::AcquireData() // Detect active channels BulkCheckChannelEnableState(); - for(unsigned int i = 0; i < m_analogChannelCount; i++) - { // Check all analog channels - analogEnabled[i] = IsChannelEnabled(i); - anyAnalogEnabled |= analogEnabled[i]; - } for(unsigned int i = 0; i < m_digitalChannelCount; i++) { // Check digital channels From 6ecb72a5e7218ca00ac7deffa76a0a5dcd9af72d Mon Sep 17 00:00:00 2001 From: fredzo Date: Fri, 5 Dec 2025 23:02:34 +0100 Subject: [PATCH 08/33] Fixed acquisition --- scopehal/MagnovaOscilloscope.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index b498147f..1e8b33d1 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -1363,6 +1363,10 @@ bool MagnovaOscilloscope::AcquireData() // Detect active channels BulkCheckChannelEnableState(); + for(unsigned int i = 0; i < m_analogChannelCount; i++) + { // Check all analog channels + analogEnabled[i] = IsChannelEnabled(i); + } for(unsigned int i = 0; i < m_digitalChannelCount; i++) { // Check digital channels From 8433287ca1ab3c2e0f65675c921d46e6d4b36a37 Mon Sep 17 00:00:00 2001 From: fredzo Date: Mon, 8 Dec 2025 11:11:25 +0100 Subject: [PATCH 09/33] Finally fixed memory leak. --- scopehal/MagnovaOscilloscope.cpp | 71 +++++++++++--------------------- 1 file changed, 25 insertions(+), 46 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 1e8b33d1..4e8ce22f 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -1036,30 +1036,22 @@ unsigned int MagnovaOscilloscope::GetActiveChannelsCount() } -time_t MagnovaOscilloscope::ExtractTimestamp(const std::string& /* timeString */, double& basetime) +time_t MagnovaOscilloscope::ExtractTimestamp(const std::string& timeString, double& basetime) { - /* - TIMESTAMP is shown as Reserved In Siglent data format. - This information is from LeCroy which uses the same wavedesc header. - Timestamp is a somewhat complex format that needs some shuffling around. - Timestamp starts at offset 296 bytes in the wavedesc - (296-303) double seconds - (304) byte minutes - (305) byte hours - (306) byte days - (307) byte months - (308-309) uint16 year - - TODO: during startup, query instrument for its current time zone - since the wavedesc reports instment local time - */ - //Yes, this cast is intentional. - //It assumes you're on a little endian system using IEEE754 64-bit float, but that applies to everything we support. - //cppcheck-suppress invalidPointerCast - // TODO - double fseconds = 0; /**reinterpret_cast(wavedesc + 296);*/ - uint8_t seconds = floor(fseconds); - basetime = fseconds - seconds; + //Timestamp returned by Magnova has the form 'hh,mm,ss.ssssss' + int hh, mm; + double ss; + + string input = timeString; + for (char &c : input) { + if (c == ',') c = ' '; + } + + std::stringstream ssin(input); + ssin >> hh >> mm >> ss; + + uint8_t seconds = floor(ss); + basetime = ss - seconds; time_t tnow = time(NULL); struct tm tstruc; @@ -1068,27 +1060,13 @@ time_t MagnovaOscilloscope::ExtractTimestamp(const std::string& /* timeString */ #else localtime_r(&tnow, &tstruc); #endif -/* - //Convert the instrument time to a string, then back to a tm - //Is there a better way to do this??? - //Naively poking "struct tm" fields gives incorrect results (scopehal-apps:#52) - //Maybe because tm_yday is inconsistent? - char tblock[64] = {0}; - snprintf(tblock, - sizeof(tblock), - "%d-%d-%d %d:%02d:%02d", - *reinterpret_cast(wavedesc + 308), - wavedesc[307], - wavedesc[306], - wavedesc[305], - wavedesc[304], - seconds); - locale cur_locale; - auto& tget = use_facet>(cur_locale); - istringstream stream(tblock); - ios::iostate state; - char format[] = "%F %T"; - tget.get(stream, time_get::iter_type(), stream, state, &tstruc, format, format + strlen(format));*/ + tstruc.tm_hour = hh; + tstruc.tm_min = mm; + tstruc.tm_sec = seconds; + + /* char buffer[64]; + strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tstruc); + LogDebug("Found time : %s\n",buffer);*/ return mktime(&tstruc); } @@ -1185,13 +1163,14 @@ vector MagnovaOscilloscope::ProcessAnalogWaveform( // float codes_per_div; - LogDebug("\nV_Gain=%f, V_Off=%f, interval=%f, h_off=%f, h_off_frac=%f, datalen=%zu\n", + LogDebug("\nV_Gain=%f, V_Off=%f, interval=%f, h_off=%f, h_off_frac=%f, datalen=%zu, basetime=%f\n", v_gain, v_off, interval, h_off, h_off_frac, - datalen); + datalen, + basetime); for(size_t j = 0; j < num_sequences; j++) { From d872fc61b874506d31965e79e7fdd89c305a36c0 Mon Sep 17 00:00:00 2001 From: fredzo Date: Mon, 8 Dec 2025 19:24:38 +0100 Subject: [PATCH 10/33] First step to AWG. --- scopehal/MagnovaOscilloscope.cpp | 201 ++++++++++++------------------- scopehal/MagnovaOscilloscope.h | 13 +- 2 files changed, 89 insertions(+), 125 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 4e8ce22f..1c988298 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -2093,7 +2093,7 @@ void MagnovaOscilloscope::PullTrigger() bool isUart = false; //Figure out what kind of trigger is active. reply = Trim(converse(":TRIGGER:TYPE?")); - if(reply == "DROPout") + if(reply == "TIMeout") PullDropoutTrigger(); else if(reply == "EDGe") PullEdgeTrigger(); @@ -2128,7 +2128,12 @@ void MagnovaOscilloscope::PullTrigger() */ void MagnovaOscilloscope::PullTriggerSource(Trigger* trig, string triggerModeName, bool isUart) { - string reply = Trim(isUart ? converse(":TRIGGER:UART:RXS?") : converse(":TRIGGER:%s:SOURCE?", triggerModeName.c_str())); + if(isUart) + { // No SCPI command on Magnova to get Trigget Group information for Decode Trigger + return; + } + + string reply = converse(":TRIGGER:%s:SOURCE?", triggerModeName.c_str()); // Returns CHANnel1 or DIGital1 // Get channel number @@ -2165,22 +2170,23 @@ void MagnovaOscilloscope::PullDropoutTrigger() Unit fs(Unit::UNIT_FS); //Level - dt->SetLevel(stof(converse(":TRIGGER:DROPOUT:LEVEL?"))); + dt->SetLevel(stof(converse(":TRIGGER:TIMeout:LEVEL?"))); //Dropout time - dt->SetDropoutTime(fs.ParseString(converse(":TRIGGER:DROPOUT:TIME?"))); + dt->SetDropoutTime(fs.ParseString(converse(":TRIGGER:TIMeout:TIME?"))); //Edge type - if(Trim(converse(":TRIGGER:DROPOUT:SLOPE?")) == "RISING") + if(Trim(converse(":TRIGGER:TIMeout:SLOPE?")) == "RISING") dt->SetType(DropoutTrigger::EDGE_RISING); else dt->SetType(DropoutTrigger::EDGE_FALLING); //Reset type - if(Trim(converse(":TRIGGER:DROPOUT:TYPE?")) == "EDGE") - dt->SetResetType(DropoutTrigger::RESET_OPPOSITE); - else - dt->SetResetType(DropoutTrigger::RESET_NONE); + dt->SetResetType(DropoutTrigger::RESET_NONE); + + // TODO => parse time + //if(Trim(converse(":TRIGGER:TIMeout:TIME?")) == "EDGE") + } /** @@ -2769,20 +2775,12 @@ vector MagnovaOscilloscope::GetAvailableWaveformSh ret.push_back(SHAPE_SQUARE); ret.push_back(SHAPE_NOISE); - //Docs say this is supported, but doesn't seem to work on SDS2104X+ - //Might be SDG only? - //ret.push_back(SHAPE_PRBS_NONSTANDARD); - ret.push_back(SHAPE_DC); ret.push_back(SHAPE_STAIRCASE_UP); ret.push_back(SHAPE_STAIRCASE_DOWN); ret.push_back(SHAPE_STAIRCASE_UP_DOWN); ret.push_back(SHAPE_PULSE); - //Docs say this is supported, but doesn't seem to work on SDS2104X+ - //Might be SDG only? - //ret.push_back(SHAPE_NEGATIVE_PULSE); - //what's "trapezia"? ret.push_back(SHAPE_SAWTOOTH_UP); ret.push_back(SHAPE_SAWTOOTH_DOWN); @@ -2852,20 +2850,7 @@ bool MagnovaOscilloscope::GetFunctionChannelActive(int chan) void MagnovaOscilloscope::SetFunctionChannelActive(int chan, bool on) { - string state; - if(on) - state = "ON"; - else - state = "OFF"; - - //Have to do this first, since it touches m_awgEnabled too - string imp; - if(GetFunctionChannelOutputImpedance(chan) == IMPEDANCE_50_OHM) - imp = "50"; - else - imp = "HZ"; - - sendWithAck((m_channels[chan]->GetHwname() + ":OUTP " + state + ",LOAD," + imp).c_str()); + sendWithAck(":FGEN:STAT %s",(on ? "ON" : "OFF")); lock_guard lock(m_cacheMutex); m_awgEnabled[chan] = on; @@ -2879,19 +2864,25 @@ float MagnovaOscilloscope::GetFunctionChannelDutyCycle(int chan) return m_awgDutyCycle[chan]; } - //Get lots of config settings from the hardware, then return newly updated cache entry - GetFunctionChannelShape(chan); + string type = GetFunctionChannelShape(chan) == SHAPE_SQUARE ? "SQU" : "PULS"; + + string duty = converse(":FGEN:WAV:%s:DUTY ?",type.c_str()); lock_guard lock(m_cacheMutex); + + float dutyf; + sscanf(duty.c_str(), "%f", &dutyf); + m_awgDutyCycle[chan] = (dutyf/100); return m_awgDutyCycle[chan]; } void MagnovaOscilloscope::SetFunctionChannelDutyCycle(int chan, float duty) { - sendWithAck((m_channels[chan]->GetHwname() + ":BSWV DUTY," + to_string(round(duty * 100))).c_str()); + string type = GetFunctionChannelShape(chan) == SHAPE_SQUARE ? "SQU" : "PULS"; + sendWithAck(":FGEN:WAV:%s:DUTY %.4f",type.c_str(),round(duty * 100)); lock_guard lock(m_cacheMutex); - m_awgDutyCycle[chan] = duty; + m_awgDutyCycle.erase(chan); } float MagnovaOscilloscope::GetFunctionChannelAmplitude(int chan) @@ -2902,19 +2893,23 @@ float MagnovaOscilloscope::GetFunctionChannelAmplitude(int chan) return m_awgRange[chan]; } - //Get lots of config settings from the hardware, then return newly updated cache entry - GetFunctionChannelShape(chan); + string amp = converse(":FGEN:WAV:AMPL ?"); lock_guard lock(m_cacheMutex); + + float ampf; + sscanf(amp.c_str(), "%f", &f); + m_awgRange[chan] = ampf; + return m_awgRange[chan]; } void MagnovaOscilloscope::SetFunctionChannelAmplitude(int chan, float amplitude) { - sendWithAck((m_channels[chan]->GetHwname() + ":BSWV AMP," + to_string(amplitude)).c_str()); + sendWithAck(":FGEN:WAV:AMPL %.4f",amplitude); lock_guard lock(m_cacheMutex); - m_awgRange[chan] = amplitude; + m_awgRange.erase(chan); } float MagnovaOscilloscope::GetFunctionChannelOffset(int chan) @@ -2925,19 +2920,21 @@ float MagnovaOscilloscope::GetFunctionChannelOffset(int chan) return m_awgOffset[chan]; } - //Get lots of config settings from the hardware, then return newly updated cache entry - GetFunctionChannelShape(chan); + string offset = converse(":FGEN:WAV:OFFS ?"); lock_guard lock(m_cacheMutex); + float offsetf; + sscanf(offset.c_str(), "%f", &offsetf); + m_awgOffset[chan] = offsetf; return m_awgOffset[chan]; } void MagnovaOscilloscope::SetFunctionChannelOffset(int chan, float offset) { - sendWithAck((m_channels[chan]->GetHwname() + ":BSWV OFST," + to_string(offset)).c_str()); + sendWithAck(":FGEN:WAV:OFFS %.4f",offset); lock_guard lock(m_cacheMutex); - m_awgOffset[chan] = offset; + m_awgOffset.erase(chan); } float MagnovaOscilloscope::GetFunctionChannelFrequency(int chan) @@ -2948,69 +2945,21 @@ float MagnovaOscilloscope::GetFunctionChannelFrequency(int chan) return m_awgFrequency[chan]; } - //Get lots of config settings from the hardware, then return newly updated cache entry - GetFunctionChannelShape(chan); + string freq = converse(":FGEN:WAV:FREQ ?"); lock_guard lock(m_cacheMutex); + float freqf; + sscanf(freq.c_str(), "%f", &freqf); + m_awgFrequency[chan] = freqf; return m_awgFrequency[chan]; } void MagnovaOscilloscope::SetFunctionChannelFrequency(int chan, float hz) { - sendWithAck((m_channels[chan]->GetHwname() + ":BSWV FRQ," + to_string(hz)).c_str()); + sendWithAck(":FGEN:WAV:FREQ %.4f",hz); lock_guard lock(m_cacheMutex); - m_awgFrequency[chan] = hz; -} - -/** - @brief Parses a name-value set expressed as pairs of comma separated values - - Expected format: COMMAND? Name1, Value1, Name2, Value2 - - If forwardMap is true, returns name -> value. If false, returns value -> name. - */ -map MagnovaOscilloscope::ParseCommaSeparatedNameValueList(string str, bool forwardMap) -{ - str += ','; - size_t ispace = str.find(' '); - string tmpName; - string tmpVal; - bool firstHalf = true; - map ret; - for(size_t i=ispace+1; iSendCommandQueuedWithReply(":FGEN:WAV:SHAP?", false); + // TODO Arb not available yet in SCPI commands // auto areply = m_transport->SendCommandQueuedWithReply(":FGEN:WAV:ASHAP?", false); //Crack the replies @@ -3363,26 +3313,22 @@ void MagnovaOscilloscope::SetFunctionChannelShape(int chan, FunctionGenerator::W } //Select type - sendWithAck((m_channels[chan]->GetHwname() + ":BSWV WVTP," + basicType).c_str()); + sendWithAck(":FGEN:WAV:SHAP %s", basicType.c_str()); if(basicType == "ARB") - { - //Returns map of memory slots ("M10") to waveform names - //Mapping is explicitly not stable, so we have to check for each instrument - //(but can be cached for a given session) - auto stl = m_transport->SendCommandQueuedWithReply("STL?"); - auto arbmap = ParseCommaSeparatedNameValueList(stl, false); - - sendWithAck((m_channels[chan]->GetHwname() + ":ARWV INDEX," + arbmap[arbType].substr(1)).c_str()); + { // TODO when available in Magnova firmware + //sendWithAck(":FGEN:WAV:SHAP %s", (arbmap[arbType].substr(1)).c_str()); } //Update cache lock_guard lock(m_cacheMutex); + // Duty cycle is reset when changing shape + m_awgDutyCycle.erase(chan); m_awgShape[chan] = shape; } bool MagnovaOscilloscope::HasFunctionRiseFallTimeControls(int /*chan*/) { - return false; + return true; } FunctionGenerator::OutputImpedance MagnovaOscilloscope::GetFunctionChannelOutputImpedance(int chan) @@ -3393,30 +3339,43 @@ FunctionGenerator::OutputImpedance MagnovaOscilloscope::GetFunctionChannelOutput return m_awgImpedance[chan]; } - //Get output enable status and impedance from the hardware, then return newly updated cache entry - GetFunctionChannelActive(chan); + string load = converse(":FGEN:LOAD ?"); + + FunctionGenerator::OutputImpedance imp = (load == "50") ? IMPEDANCE_50_OHM : IMPEDANCE_HIGH_Z; lock_guard lock(m_cacheMutex); + m_awgImpedance[chan] = imp; return m_awgImpedance[chan]; } void MagnovaOscilloscope::SetFunctionChannelOutputImpedance(int chan, FunctionGenerator::OutputImpedance z) { - //Have to do this first, since it touches m_awgImpedance - string state; - if(GetFunctionChannelActive(chan)) - state = "ON"; - else - state = "OFF"; - string imp; if(z == IMPEDANCE_50_OHM) - imp = "50"; + imp = "50OHM"; else - imp = "HZ"; + imp = "HIZ"; - sendWithAck((m_channels[chan]->GetHwname() + ":OUTP " + state + ",LOAD," + imp).c_str()); + sendWithAck(":FGEN:LOAD %s",imp.c_str()); lock_guard lock(m_cacheMutex); - m_awgImpedance[chan] = z; -} \ No newline at end of file + m_awgImpedance.erase(chan); +} + +float MagnovaOscilloscope::GetFunctionChannelRiseTime(int /*chan*/) +{ // TODO + return 0; +} + +void MagnovaOscilloscope::SetFunctionChannelRiseTime(int /*chan*/, float /*fs*/) +{ // TODO +} + +float MagnovaOscilloscope::GetFunctionChannelFallTime(int /*chan*/) +{ // TODO + return 0; +} + +void MagnovaOscilloscope::SetFunctionChannelFallTime(int /*chan*/, float /*fs*/) +{ // TODO +} diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h index 0e58ec76..0bb2d0c5 100644 --- a/scopehal/MagnovaOscilloscope.h +++ b/scopehal/MagnovaOscilloscope.h @@ -52,8 +52,7 @@ class WindowTrigger; #define c_digiChannelsPerBus 8 -class MagnovaOscilloscope : public virtual SCPIOscilloscope - , public virtual SCPIFunctionGenerator +class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCPIFunctionGenerator { public: MagnovaOscilloscope(SCPITransport* transport); @@ -186,6 +185,14 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope virtual OutputImpedance GetFunctionChannelOutputImpedance(int chan) override; virtual void SetFunctionChannelOutputImpedance(int chan, OutputImpedance z) override; + virtual float GetFunctionChannelRiseTime(int) override; + + virtual void SetFunctionChannelRiseTime(int, float) override; + + virtual void SetFunctionChannelFallTime(int, float) override; + + virtual float GetFunctionChannelFallTime(int chan) override; + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Logic analyzer configuration @@ -347,8 +354,6 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope ADCMode m_adcMode; bool m_adcModeValid; - std::map ParseCommaSeparatedNameValueList(std::string str, bool forwardMap = true); - int64_t m_timeDiv; //Other channels From 95c1e5e1433ccaeec88ad6907b32a4f686dbc530 Mon Sep 17 00:00:00 2001 From: Frederic Borry Date: Tue, 9 Dec 2025 21:23:44 +0100 Subject: [PATCH 11/33] Fixed digital channle threshold caching. Added AWG rise and fall time. --- scopehal/MagnovaOscilloscope.cpp | 93 ++++++++++++++++++++------------ scopehal/MagnovaOscilloscope.h | 4 +- 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 1c988298..f39a4e6e 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -387,6 +387,8 @@ void MagnovaOscilloscope::FlushConfigCache() m_awgRange.clear(); m_awgOffset.clear(); m_awgFrequency.clear(); + m_awgRiseTime.clear(); + m_awgFallTime.clear(); m_awgShape.clear(); m_awgImpedance.clear(); m_adcModeValid = false; @@ -648,7 +650,6 @@ double MagnovaOscilloscope::GetChannelAttenuation(size_t i) if(i >= m_analogChannelCount) return 1; - //TODO: support ext/10 if(i == m_extTrigChannel->GetIndex()) return 1; @@ -1076,7 +1077,6 @@ time_t MagnovaOscilloscope::ExtractTimestamp(const std::string& timeString, doub void MagnovaOscilloscope::Convert16BitSamples(float* pout, const uint16_t* pin, float gain, float offset, size_t count) { //Divide large waveforms (>1M points) into blocks and multithread them - //TODO: tune split if(count > 1000000) { //Round blocks to multiples of 64 samples for clean vectorization @@ -1398,12 +1398,6 @@ bool MagnovaOscilloscope::AcquireData() sendOnly(":SINGLE"); m_triggerArmed = true; } - // TODO => is this needed ? - /* - else - { // It was one shot acquisition, disarm trigger - m_triggerArmed = false; - }*/ //Process analog waveforms waveforms.resize(m_analogChannelCount); @@ -2047,21 +2041,20 @@ float MagnovaOscilloscope::GetDigitalThreshold(size_t channel) if( (channel < m_digitalChannelBase) || (m_digitalChannelCount == 0) ) return 0; - channel -= m_analogChannelCount; + string bank = GetDigitalChannelBankName(channel); { lock_guard lock(m_cacheMutex); - - if(m_channelDigitalThresholds.find(channel) != m_channelDigitalThresholds.end()) - return m_channelDigitalThresholds[channel]; + if(m_channelDigitalThresholds.find(bank) != m_channelDigitalThresholds.end()) + return m_channelDigitalThresholds[bank]; } float result; - string reply = converse(":DIG:THRESHOLD%s?", GetDigitalChannelBankName(channel).c_str()); + string reply = converse(":DIG:THRESHOLD%s?", bank.c_str()); sscanf(reply.c_str(), "%f", &result); lock_guard lock(m_cacheMutex); - m_channelDigitalThresholds[channel] = result; + m_channelDigitalThresholds[bank] = result; return result; } @@ -2072,15 +2065,14 @@ void MagnovaOscilloscope::SetDigitalHysteresis(size_t /*channel*/, float /*level void MagnovaOscilloscope::SetDigitalThreshold(size_t channel, float level) { - channel -= m_analogChannelCount; - - sendWithAck(":DIG:THRESHOLD%s %1.2E", GetDigitalChannelBankName(channel).c_str(), level); + string bank = GetDigitalChannelBankName(channel); + + sendWithAck(":DIG:THRESHOLD%s %1.2E", bank.c_str(), level); //Don't update the cache because the scope is likely to round the offset we ask for. //If we query the instrument later, the cache will be updated then. lock_guard lock2(m_cacheMutex); - // TODO handel bank cache rather than channel - m_channelDigitalThresholds.erase(channel); + m_channelDigitalThresholds.erase(bank); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -3326,11 +3318,6 @@ void MagnovaOscilloscope::SetFunctionChannelShape(int chan, FunctionGenerator::W m_awgShape[chan] = shape; } -bool MagnovaOscilloscope::HasFunctionRiseFallTimeControls(int /*chan*/) -{ - return true; -} - FunctionGenerator::OutputImpedance MagnovaOscilloscope::GetFunctionChannelOutputImpedance(int chan) { { @@ -3362,20 +3349,58 @@ void MagnovaOscilloscope::SetFunctionChannelOutputImpedance(int chan, FunctionGe m_awgImpedance.erase(chan); } -float MagnovaOscilloscope::GetFunctionChannelRiseTime(int /*chan*/) -{ // TODO - return 0; +bool MagnovaOscilloscope::HasFunctionRiseFallTimeControls(int /*chan*/) +{ + return true; +} + + +float MagnovaOscilloscope::GetFunctionChannelRiseTime(int chan) +{ + { + lock_guard lock(m_cacheMutex); + if(m_awgRiseTime.find(chan) != m_awgRiseTime.end()) + return m_awgRiseTime[chan]; + } + + string time = converse(":FGEN:WAV:PULS:RTIME ?"); + + lock_guard lock(m_cacheMutex); + float timef; + sscanf(time.c_str(), "%f", &timef); + m_awgRiseTime[chan] = timef * FS_PER_SECOND; + return m_awgRiseTime[chan]; } -void MagnovaOscilloscope::SetFunctionChannelRiseTime(int /*chan*/, float /*fs*/) -{ // TODO +void MagnovaOscilloscope::SetFunctionChannelRiseTime(int chan, float fs) +{ + sendWithAck(":FGEN:WAV:PULS:RTIME %.4f",fs * SECONDS_PER_FS); + + lock_guard lock(m_cacheMutex); + m_awgRiseTime.erase(chan); } -float MagnovaOscilloscope::GetFunctionChannelFallTime(int /*chan*/) -{ // TODO - return 0; +float MagnovaOscilloscope::GetFunctionChannelFallTime(int chan) +{ + { + lock_guard lock(m_cacheMutex); + if(m_awgFallTime.find(chan) != m_awgFallTime.end()) + return m_awgFallTime[chan]; + } + + string time = converse(":FGEN:WAV:PULS:FTIME ?"); + + lock_guard lock(m_cacheMutex); + float timef; + sscanf(time.c_str(), "%f", &timef); + m_awgFallTime[chan] = timef * FS_PER_SECOND; + return m_awgFallTime[chan]; } -void MagnovaOscilloscope::SetFunctionChannelFallTime(int /*chan*/, float /*fs*/) -{ // TODO +void MagnovaOscilloscope::SetFunctionChannelFallTime(int chan, float fs) +{ + sendWithAck(":FGEN:WAV:PULS:FTIME %.4f",fs * SECONDS_PER_FS); + + lock_guard lock(m_cacheMutex); + m_awgFallTime.erase(chan); } diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h index 0bb2d0c5..e9e87afa 100644 --- a/scopehal/MagnovaOscilloscope.h +++ b/scopehal/MagnovaOscilloscope.h @@ -332,7 +332,7 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP //Cached configuration std::map m_channelVoltageRanges; std::map m_channelOffsets; - std::map m_channelDigitalThresholds; + std::map m_channelDigitalThresholds; std::map m_channelsEnabled; bool m_sampleRateValid; int64_t m_sampleRate; @@ -349,6 +349,8 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP std::map m_awgRange; std::map m_awgOffset; std::map m_awgFrequency; + std::map m_awgRiseTime; + std::map m_awgFallTime; std::map m_awgShape; std::map m_awgImpedance; ADCMode m_adcMode; From e38b3b6dd561d5b63740b1ee239d13efb94fac66 Mon Sep 17 00:00:00 2001 From: Frederic Borry Date: Tue, 9 Dec 2025 23:15:56 +0100 Subject: [PATCH 12/33] Fixed rise/fall time commands. Fixed trigger channel update. --- scopehal/MagnovaOscilloscope.cpp | 33 ++++++++++++++++++++++---------- scopehal/MagnovaOscilloscope.h | 1 + 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index f39a4e6e..a4ce0869 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -1973,6 +1973,19 @@ void MagnovaOscilloscope::SetADCMode(size_t /*channel*/, size_t /* mode */) return; } +std::string MagnovaOscilloscope::GetChannelName(size_t channel) +{ + if(channel < m_digitalChannelBase) + { + return "CHAN" + (channel + 1); + } + else + { + return "DIG" + (channel - m_digitalChannelBase); + } +} + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Logic analyzer configuration @@ -2483,39 +2496,39 @@ void MagnovaOscilloscope::PushTrigger() if(dt) { sendOnly(":TRIGGER:TYPE DROPOUT"); - sendOnly(":TRIGGER:DROPOUT:SOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); + sendOnly(":TRIGGER:DROPOUT:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); PushDropoutTrigger(dt); } else if(pt) { sendOnly(":TRIGGER:TYPE INTERVAL"); - sendOnly(":TRIGGER:INTERVAL:SOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); + sendOnly(":TRIGGER:INTERVAL:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); PushPulseWidthTrigger(pt); } else if(rt) { sendOnly(":TRIGGER:TYPE RUNT"); - sendOnly(":TRIGGER:RUNT:SOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); + sendOnly(":TRIGGER:RUNT:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); PushRuntTrigger(rt); } else if(st) { sendOnly(":TRIGGER:TYPE SLOPE"); - sendOnly(":TRIGGER:SLOPE:SOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); + sendOnly(":TRIGGER:SLOPE:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); PushSlewRateTrigger(st); } else if(ut) { sendOnly(":TRIGGER:TYPE UART"); // TODO: Validate these trigger allocations - sendOnly(":TRIGGER:UART:RXSOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); - sendOnly(":TRIGGER:UART:TXSOURCE %s", m_trigger->GetInput(1).m_channel->GetHwname().c_str()); + sendOnly(":TRIGGER:UART:RXSOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + sendOnly(":TRIGGER:UART:TXSOURCE %s", GetChannelName(m_trigger->GetInput(1).m_channel->GetIndex()).c_str()); PushUartTrigger(ut); } else if(wt) { sendOnly(":TRIGGER:TYPE WINDOW"); - sendOnly(":TRIGGER:WINDOW:SOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); + sendOnly(":TRIGGER:WINDOW:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); PushWindowTrigger(wt); } @@ -2524,7 +2537,7 @@ void MagnovaOscilloscope::PushTrigger() else if(et) //must be last { sendOnly(":TRIGGER:TYPE EDGE"); - sendOnly(":TRIGGER:EDGE:SOURCE %s", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); + sendOnly(":TRIGGER:EDGE:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); PushEdgeTrigger(et, "EDGE"); } @@ -3374,7 +3387,7 @@ float MagnovaOscilloscope::GetFunctionChannelRiseTime(int chan) void MagnovaOscilloscope::SetFunctionChannelRiseTime(int chan, float fs) { - sendWithAck(":FGEN:WAV:PULS:RTIME %.4f",fs * SECONDS_PER_FS); + sendWithAck(":FGEN:WAV:PULS:RTIME %.10f",fs * SECONDS_PER_FS); lock_guard lock(m_cacheMutex); m_awgRiseTime.erase(chan); @@ -3399,7 +3412,7 @@ float MagnovaOscilloscope::GetFunctionChannelFallTime(int chan) void MagnovaOscilloscope::SetFunctionChannelFallTime(int chan, float fs) { - sendWithAck(":FGEN:WAV:PULS:FTIME %.4f",fs * SECONDS_PER_FS); + sendWithAck(":FGEN:WAV:PULS:FTIME %.10f",fs * SECONDS_PER_FS); lock_guard lock(m_cacheMutex); m_awgFallTime.erase(chan); diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h index e9e87afa..17a82cd9 100644 --- a/scopehal/MagnovaOscilloscope.h +++ b/scopehal/MagnovaOscilloscope.h @@ -274,6 +274,7 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP std::string GetDigitalChannelBankName(size_t channel); + std::string GetChannelName(size_t channel); std::string GetPossiblyEmptyString(const std::string& property); From e415888a36e5fef2bc24b72b02bd60b0b3599bf5 Mon Sep 17 00:00:00 2001 From: Frederic Borry Date: Tue, 9 Dec 2025 23:44:04 +0100 Subject: [PATCH 13/33] Prevent pull trigger from asking level on digital sources. --- scopehal/MagnovaOscilloscope.cpp | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index a4ce0869..1e4223f4 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -825,11 +825,11 @@ Oscilloscope::TriggerMode MagnovaOscilloscope::PollTrigger() sinr = converse(":STAT?"); //No waveform, but ready for one? - if((sinr == "WAITing")||(sinr == "RUNNing")) + /*if((sinr == "WAITing")||(sinr == "RUNNing")) { m_triggerArmed = true; return TRIGGER_MODE_RUN; - } + }*/ if((sinr == "TRIGgered")) { @@ -1977,11 +1977,11 @@ std::string MagnovaOscilloscope::GetChannelName(size_t channel) { if(channel < m_digitalChannelBase) { - return "CHAN" + (channel + 1); + return string("CHAN") + to_string(channel + 1); } else { - return "DIG" + (channel - m_digitalChannelBase); + return string("DIG") + to_string(channel - m_digitalChannelBase); } } @@ -2174,8 +2174,12 @@ void MagnovaOscilloscope::PullDropoutTrigger() Unit fs(Unit::UNIT_FS); - //Level - dt->SetLevel(stof(converse(":TRIGGER:TIMeout:LEVEL?"))); + // Check for digital source + string reply = converse(":TRIGGER:TIMeout:SOURCE?"); + if(reply[0] == 'C') + { // Level only for analog source + dt->SetLevel(stof(converse(":TRIGGER:TIMeout:LEVEL?"))); + } //Dropout time dt->SetDropoutTime(fs.ParseString(converse(":TRIGGER:TIMeout:TIME?"))); @@ -2211,8 +2215,12 @@ void MagnovaOscilloscope::PullEdgeTrigger() m_trigger = new EdgeTrigger(this); EdgeTrigger* et = dynamic_cast(m_trigger); - //Level - et->SetLevel(stof(converse(":TRIGGER:EDGE:LEVEL?"))); + // Check for digital source + string reply = converse(":TRIGGER:EDGE:SOURCE?"); + if(reply[0] == 'C') + { // Level only for analog source + et->SetLevel(stof(converse(":TRIGGER:EDGE:LEVEL?"))); + } //TODO: OptimizeForHF (changes hysteresis for fast signals) @@ -2238,8 +2246,12 @@ void MagnovaOscilloscope::PullPulseWidthTrigger() auto pt = dynamic_cast(m_trigger); Unit fs(Unit::UNIT_FS); - //Level - pt->SetLevel(stof(converse(":TRIGGER:INTERVAL:LEVEL?"))); + // Check for digital source + string reply = converse(":TRIGGER:INTERVAL:SOURCE?"); + if(reply[0] == 'C') + { // Level only for analog source + pt->SetLevel(stof(converse(":TRIGGER:INTERVAL:LEVEL?"))); + } //Condition pt->SetCondition(GetCondition(converse(":TRIGGER:INTERVAL:LIMIT?"))); From aebf2f515278db8ea1fa47b211e0f937e46e8fa9 Mon Sep 17 00:00:00 2001 From: Frederic Borry Date: Wed, 10 Dec 2025 00:41:35 +0100 Subject: [PATCH 14/33] Fixed transport lock for converse and sendwithack. Fixed vertical offset. --- scopehal/MagnovaOscilloscope.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 1e4223f4..f7a9c92e 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -100,6 +100,8 @@ string MagnovaOscilloscope::converse(const char* fmt, ...) vsnprintf(opString, sizeof(opString), fmt, va); va_end(va); + // Lock on transport at this level since magnova sometimes returns an \n before the actual reply + lock_guard lock(m_transport->GetMutex()); ret = m_transport->SendCommandQueuedWithReply(opString, false); if(ret.length() == 0) ret = m_transport->ReadReply(); // Sometimes the Magnova returns en empty string and then the actual reply @@ -130,6 +132,8 @@ bool MagnovaOscilloscope::sendWithAck(const char* fmt, ...) std::string result(opString); result += ";*OPC?"; + // Lock on transport at this level since magnova sometimes returns an \n before the actual reply + lock_guard lock(m_transport->GetMutex()); ret = m_transport->SendCommandQueuedWithReply(result.c_str(), false); if(ret.length() == 0) ret = m_transport->ReadReply(); // Sometimes the Magnova returns en empty string and then the actual reply @@ -1142,7 +1146,7 @@ vector MagnovaOscilloscope::ProcessAnalogWaveform( float v_gain = metadata->verticalStep/0xFFFF; // Get offset from vertical start + add channel offset - float v_off = (0 - metadata->verticalStart - GetChannelOffset(ch,0)); + float v_off = (0 - metadata->verticalStart/* - GetChannelOffset(ch,0)*/); // Get interval from timedelta float interval = metadata->timeDelta * FS_PER_SECOND; From b5b9a5687a8007bebecaf135caebaad340c2aa0e Mon Sep 17 00:00:00 2001 From: Frederic Borry Date: Wed, 10 Dec 2025 12:32:59 +0100 Subject: [PATCH 15/33] Fixed some caching issues. --- scopehal/MagnovaOscilloscope.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index f7a9c92e..37f37a0c 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -513,6 +513,7 @@ void MagnovaOscilloscope::EnableChannel(size_t i) { m_memoryDepthValid = false; m_sampleRateValid = false; + m_triggerOffsetValid = false; } } @@ -552,6 +553,7 @@ void MagnovaOscilloscope::DisableChannel(size_t i) lock_guard lock2(m_cacheMutex); m_memoryDepthValid = false; m_sampleRateValid = false; + m_triggerOffsetValid = false; } } @@ -1491,10 +1493,12 @@ bool MagnovaOscilloscope::AcquireData() } void MagnovaOscilloscope::PrepareAcquisition() -{ +{ // Make sure everything is up to date + lock_guard lock2(m_cacheMutex); + m_sampleRateValid = false; + m_memoryDepthValid = false; m_triggerOffsetValid = false; m_channelOffsets.clear(); - // m_transport->SendCommand(":WAVEFORM:START 0"); } void MagnovaOscilloscope::Start() @@ -1774,6 +1778,7 @@ void MagnovaOscilloscope::SetSampleDepth(uint64_t depth) lock_guard lock2(m_cacheMutex); m_memoryDepthValid = false; m_sampleRateValid = false; + m_triggerOffsetValid = false; } void MagnovaOscilloscope::SetSampleRate(uint64_t rate) @@ -1808,6 +1813,7 @@ void MagnovaOscilloscope::SetSampleRate(uint64_t rate) lock_guard lock2(m_cacheMutex); m_sampleRateValid = false; m_memoryDepthValid = false; + m_triggerOffsetValid = false; } void MagnovaOscilloscope::EnableTriggerOutput() @@ -1838,6 +1844,8 @@ void MagnovaOscilloscope::SetTriggerOffset(int64_t offset) int64_t halfdepth = GetSampleDepth() / 2; int64_t halfwidth = static_cast(round(FS_PER_SECOND * halfdepth / rate)); + //LogDebug("Set trigger offset to %f : rate = %" PRId64 ", depth = %" PRId64 " haldepth = %" PRId64 ", halfwidth = %" PRId64 ".\n",((float)offset)*SECONDS_PER_FS,rate,GetSampleDepth(),halfdepth,halfwidth); + sendWithAck(":TIMebase:OFFSet %1.2E", (offset - halfwidth) * SECONDS_PER_FS); //Don't update the cache because the scope is likely to round the offset we ask for. @@ -1870,6 +1878,8 @@ int64_t MagnovaOscilloscope::GetTriggerOffset() int64_t halfwidth = static_cast(round(FS_PER_SECOND * halfdepth / rate)); m_triggerOffset += halfwidth; + //LogDebug("Get trigger offset to %lf : rate = %" PRId64 ", depth = %" PRId64 " haldepth = %" PRId64 ", halfwidth = %" PRId64 ", result = %" PRId64 ".\n",sec,rate,GetSampleDepth(),halfdepth,halfwidth,m_triggerOffset); + m_triggerOffsetValid = true; return m_triggerOffset; From 28e303f841d3d47c02e1a004557f0a3d33f60269 Mon Sep 17 00:00:00 2001 From: Frederic Borry Date: Thu, 11 Dec 2025 23:35:30 +0100 Subject: [PATCH 16/33] Fixed AWG channel color. Added some triggers. Fixed single capture. --- scopehal/MagnovaOscilloscope.cpp | 127 ++++++++++++++++--------------- 1 file changed, 64 insertions(+), 63 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 37f37a0c..9e2ca2b3 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -179,9 +179,7 @@ void MagnovaOscilloscope::SharedCtorInit() //Add the function generator output if(m_hasFunctionGen) { - //TODO: this is stupid, it shares the same name as our scope input! - //Is this going to break anything?? - m_awgChannel = new FunctionGeneratorChannel(this, "C1", "#808080", m_channels.size()); + m_awgChannel = new FunctionGeneratorChannel(this, "AWG", "#ff00ffff", m_channels.size()); m_channels.push_back(m_awgChannel); m_awgChannel->SetDisplayName("AWG"); } @@ -1401,9 +1399,15 @@ bool MagnovaOscilloscope::AcquireData() //Re-arm the trigger if not in one-shot mode if(!m_triggerOneShot) { + //LogDebug("Arming trigger for next acquisition!\n"); sendOnly(":SINGLE"); m_triggerArmed = true; } + else + { + sendWithAck(":STOP"); + m_triggerArmed = false; + } //Process analog waveforms waveforms.resize(m_analogChannelCount); @@ -1504,9 +1508,9 @@ void MagnovaOscilloscope::PrepareAcquisition() void MagnovaOscilloscope::Start() { PrepareAcquisition(); - sendOnly(":STOP"); - sendOnly(":SINGLE"); //always do single captures, just re-trigger + sendOnly(":STOP;:SINGLE"); //always do single captures, just re-trigger + //LogDebug("Arming trigger for acquisition start!\n"); m_triggerArmed = true; m_triggerOneShot = false; } @@ -1516,9 +1520,9 @@ void MagnovaOscilloscope::StartSingleTrigger() //LogDebug("Start single trigger\n"); PrepareAcquisition(); - sendOnly(":STOP"); - sendOnly(":SINGLE"); + sendOnly(":STOP;:SINGLE"); + //LogDebug("Arming trigger for single acquisition!\n"); m_triggerArmed = true; m_triggerOneShot = true; } @@ -1550,6 +1554,7 @@ void MagnovaOscilloscope::ForceTrigger() if(!m_triggerArmed) sendOnly(":SINGLE"); + //LogDebug("Arming trigger for forced acquisition!\n"); m_triggerArmed = true; this_thread::sleep_for(c_trigger_delay); } @@ -2125,7 +2130,7 @@ void MagnovaOscilloscope::PullTrigger() PullUartTrigger(); isUart = true; } - else if(reply == "INTerval") + else if(reply == "PULSe") PullPulseWidthTrigger(); else if(reply == "WINDow") PullWindowTrigger(); @@ -2196,20 +2201,16 @@ void MagnovaOscilloscope::PullDropoutTrigger() } //Dropout time - dt->SetDropoutTime(fs.ParseString(converse(":TRIGGER:TIMeout:TIME?"))); + dt->SetDropoutTime(stof(converse(":TRIGGER:TIMeout:TIME?"))*FS_PER_SECOND); //Edge type - if(Trim(converse(":TRIGGER:TIMeout:SLOPE?")) == "RISING") + if(Trim(converse(":TRIGGER:TIMeout:SLOPE?")) == "RISing") dt->SetType(DropoutTrigger::EDGE_RISING); else dt->SetType(DropoutTrigger::EDGE_FALLING); //Reset type dt->SetResetType(DropoutTrigger::RESET_NONE); - - // TODO => parse time - //if(Trim(converse(":TRIGGER:TIMeout:TIME?")) == "EDGE") - } /** @@ -2236,8 +2237,6 @@ void MagnovaOscilloscope::PullEdgeTrigger() et->SetLevel(stof(converse(":TRIGGER:EDGE:LEVEL?"))); } - //TODO: OptimizeForHF (changes hysteresis for fast signals) - //Slope GetTriggerSlope(et, Trim(converse(":TRIGGER:EDGE:SLOPE?"))); } @@ -2261,23 +2260,28 @@ void MagnovaOscilloscope::PullPulseWidthTrigger() Unit fs(Unit::UNIT_FS); // Check for digital source - string reply = converse(":TRIGGER:INTERVAL:SOURCE?"); + string reply = converse(":TRIGGER:PULSe:SOURCE?"); if(reply[0] == 'C') { // Level only for analog source - pt->SetLevel(stof(converse(":TRIGGER:INTERVAL:LEVEL?"))); + pt->SetLevel(stof(converse(":TRIGGER:PULSe:LEVEL?"))); } //Condition - pt->SetCondition(GetCondition(converse(":TRIGGER:INTERVAL:LIMIT?"))); + pt->SetCondition(GetCondition(converse(":TRIGGER:PULSe:TIMing?"))); //Min range - pt->SetLowerBound(fs.ParseString(converse(":TRIGGER:INTERVAL:TLOWER?"))); + pt->SetLowerBound(fs.ParseString(converse(":TRIGGER:PULSe:DURation:LOWer?"))); //Max range - pt->SetUpperBound(fs.ParseString(converse(":TRIGGER:INTERVAL:TUPPER?"))); + pt->SetUpperBound(fs.ParseString(converse(":TRIGGER:PULSe:DURation:LOWer?"))); //Slope - GetTriggerSlope(pt, Trim(converse(":TRIGGER:INTERVAL:SLOPE?"))); + reply = Trim(converse(":TRIGGER:PULSe:POLarity?")); + if(reply == "POSitive") + pt->SetType(PulseWidthTrigger::EDGE_RISING); + else if(reply == "NEGative") + pt->SetType(PulseWidthTrigger::EDGE_FALLING); + } /** @@ -2302,16 +2306,16 @@ void MagnovaOscilloscope::PullRuntTrigger() string reply; //Lower bound - rt->SetLowerBound(v.ParseString(converse(":TRIGGER:RUNT:LLEVEL?"))); + rt->SetLowerBound(v.ParseString(converse(":TRIGGER:RUNT:LEVel1?"))); //Upper bound - rt->SetUpperBound(v.ParseString(converse(":TRIGGER:RUNT:HLEVEL?"))); + rt->SetUpperBound(v.ParseString(converse(":TRIGGER:RUNT:LEVel2?"))); //Lower bound - rt->SetLowerInterval(fs.ParseString(converse(":TRIGGER:RUNT:TLOWER?"))); + rt->SetLowerInterval(fs.ParseString(converse(":TRIGGER:RUNT:DURation:LOWer?"))); //Upper interval - rt->SetUpperInterval(fs.ParseString(converse(":TRIGGER:RUNT:TUPPER?"))); + rt->SetUpperInterval(fs.ParseString(converse(":TRIGGER:RUNT:DURation:UPPer?"))); //Slope reply = Trim(converse(":TRIGGER:RUNT:POLARITY?")); @@ -2321,7 +2325,7 @@ void MagnovaOscilloscope::PullRuntTrigger() rt->SetSlope(RuntTrigger::EDGE_FALLING); //Condition - rt->SetCondition(GetCondition(converse(":TRIGGER:RUNT:LIMIT?"))); + rt->SetCondition(GetCondition(converse(":TRIGGER:RUNT:TIMing?"))); } /** @@ -2346,16 +2350,16 @@ void MagnovaOscilloscope::PullSlewRateTrigger() string reply ; //Lower bound - st->SetLowerBound(v.ParseString(converse(":TRIGGER:SLOPE:LLEVEL?"))); + st->SetLowerBound(v.ParseString(converse(":TRIGGER:SLOPe:LEVel1?"))); //Upper bound - st->SetUpperBound(v.ParseString(converse(":TRIGGER:SLOPE:HLEVEL?"))); + st->SetUpperBound(v.ParseString(converse(":TRIGGER:SLOPe:LEVel2?"))); //Lower interval - st->SetLowerInterval(fs.ParseString(converse(":TRIGGER:SLOPE:TLOWER?"))); + st->SetLowerInterval(fs.ParseString(converse(":TRIGGER:SLOPE:DURation:LOWer?"))); //Upper interval - st->SetUpperInterval(fs.ParseString(converse(":TRIGGER:SLOPE:TUPPER?"))); + st->SetUpperInterval(fs.ParseString(converse(":TRIGGER:SLOPE:DURation:UPPer?"))); //Slope reply = Trim(converse("TRIGGER:SLOPE:SLOPE?")); @@ -2367,7 +2371,7 @@ void MagnovaOscilloscope::PullSlewRateTrigger() st->SetSlope(SlewRateTrigger::EDGE_ANY); //Condition - st->SetCondition(GetCondition(converse("TRIGGER:SLOPE:LIMIT?"))); + st->SetCondition(GetCondition(converse("TRIGGER:SLOPe:TIMing?"))); } /** @@ -2388,17 +2392,17 @@ void MagnovaOscilloscope::PullUartTrigger() UartTrigger* ut = dynamic_cast(m_trigger); string reply; - string p1; + string p1,p2; - //Bit rate - ut->SetBitRate(stoi(converse(":TRIGGER:UART:BAUD?"))); + //Bit rate : Not available in Magnova SCPI implemetnation for now + //ut->SetBitRate(stoi(converse(":TRIGGER:UART:BAUD?"))); - //Level - ut->SetLevel(stof(converse(":TRIGGER:UART:RXT?"))); + //Level : Not available in Magnova SCPI implemetnation for now + //ut->SetLevel(stof(converse(":TRIGGER:UART:RXT?"))); - //Parity - reply = Trim(converse(":TRIGGER:UART:PARITY?")); + //Parity : Not available in Magnova SCPI implemetnation for now + /*reply = Trim(converse(":TRIGGER:UART:PARITY?")); if(reply == "NONE") ut->SetParityType(UartTrigger::PARITY_NONE); else if(reply == "EVEN") @@ -2408,45 +2412,42 @@ void MagnovaOscilloscope::PullUartTrigger() else if(reply == "MARK") ut->SetParityType(UartTrigger::PARITY_MARK); else if(reply == "SPACe") - ut->SetParityType(UartTrigger::PARITY_SPACE); + ut->SetParityType(UartTrigger::PARITY_SPACE);*/ - //Operator - //bool ignore_p2 = true; - // It seems this scope only copes with equivalence - ut->SetCondition(Trigger::CONDITION_EQUAL); - - //Idle polarity - reply = Trim(converse(":TRIGGER:UART:IDLE?")); + //Idle polarity : Not available in Magnova SCPI implemetnation for now + /*reply = Trim(converse(":TRIGGER:UART:IDLE?")); if(reply == "HIGH") ut->SetPolarity(UartTrigger::IDLE_HIGH); else if(reply == "LOW") - ut->SetPolarity(UartTrigger::IDLE_LOW); + ut->SetPolarity(UartTrigger::IDLE_LOW);*/ - //Stop bits - ut->SetStopBits(stof(Trim(converse(":TRIGGER:UART:STOP?")))); + //Stop bits : Not available in Magnova SCPI implemetnation for now + //ut->SetStopBits(stof(Trim(converse(":TRIGGER:UART:STOP?")))); //Trigger type - reply = Trim(converse(":TRIGGER:UART:CONDITION?")); - if(reply == "STARt") + reply = Trim(converse(":TRIGGER:UART:EVENt?")); + if(reply == "FSTart") ut->SetMatchType(UartTrigger::TYPE_START); - else if(reply == "STOP") - ut->SetMatchType(UartTrigger::TYPE_STOP); - else if(reply == "ERRor") + else if(reply == "FPCHeck") ut->SetMatchType(UartTrigger::TYPE_PARITY_ERR); - else + else if(reply == "DATa") ut->SetMatchType(UartTrigger::TYPE_DATA); + else // "IFCompletion" (invalid frame completion) / "FPCHeck" (failed parity check) / "IFSTart" (invalid frame start) + LogWarning("Unsupported UART trigger condition '%s'", reply.c_str()); // Data to match (there is no pattern2 on sds) - p1 = Trim(converse(":TRIGGER:UART:DATA?")); - ut->SetPatterns(p1, "", true); + p1 = Trim(converse(":TRIGGER:UART:DATA:WORD0?")); + p2 = Trim(converse(":TRIGGER:UART:DATA:WORD1?")); + // TODO set ignorep2 according to p2 value + ut->SetPatterns(p1, p2, false); } /** @brief Reads settings for a window trigger from the instrument */ void MagnovaOscilloscope::PullWindowTrigger() -{ +{ // TODO //Clear out any triggers of the wrong type if((m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL)) { @@ -2495,13 +2496,13 @@ Trigger::Condition MagnovaOscilloscope::GetCondition(string reply) { reply = Trim(reply); - if(reply == "LESSthan") + if(reply == "LTHan") return Trigger::CONDITION_LESS; - else if(reply == "GREATerthan") + else if(reply == "GTHan") return Trigger::CONDITION_GREATER; - else if(reply == "INNer") + else if(reply == "INSide") return Trigger::CONDITION_BETWEEN; - else if(reply == "OUTer") + else if(reply == "OUTSide") return Trigger::CONDITION_NOT_BETWEEN; //unknown From 105ec874d5a60c22e8ee343d90105885f28b71bd Mon Sep 17 00:00:00 2001 From: Frederic Borry Date: Fri, 12 Dec 2025 00:22:16 +0100 Subject: [PATCH 17/33] Fixed low sample rate acquisition. --- scopehal/MagnovaOscilloscope.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 9e2ca2b3..591d1d18 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -836,8 +836,8 @@ Oscilloscope::TriggerMode MagnovaOscilloscope::PollTrigger() }*/ if((sinr == "TRIGgered")) - { - return TRIGGER_MODE_TRIGGERED; + { // Magnova returns TRIGgered status during Single acquisition, we need to wait for STOPped + return TRIGGER_MODE_RUN; } //Stopped, no data available From 0119fd07703ab81a94c4b5b3077a47a7a224317b Mon Sep 17 00:00:00 2001 From: fredzo Date: Fri, 12 Dec 2025 14:57:25 +0100 Subject: [PATCH 18/33] Code cleanup + added more trigger implementations. --- scopehal/MagnovaOscilloscope.cpp | 613 +++++++++++++++---------------- scopehal/MagnovaOscilloscope.h | 3 - 2 files changed, 306 insertions(+), 310 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 591d1d18..3994fdbe 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -64,11 +64,8 @@ MagnovaOscilloscope::MagnovaOscilloscope(SCPITransport* transport) , m_hasLA(false) , m_hasDVM(false) , m_hasFunctionGen(false) - , m_hasFastSampleRate(false) - , m_memoryDepthOption(0) , m_hasI2cTrigger(false) , m_hasSpiTrigger(false) - , m_hasUartTrigger(false) , m_maxBandwidth(10000) , m_triggerArmed(false) , m_triggerOneShot(false) @@ -2110,6 +2107,96 @@ void MagnovaOscilloscope::SetDigitalThreshold(size_t channel, float level) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Triggering + +/** + @brief Processes the slope for an edge or edge-derived trigger + */ +void MagnovaOscilloscope::GetTriggerSlope(EdgeTrigger* trig, string reply) + +{ + reply = Trim(reply); + + if(reply == "RISing") + trig->SetType(EdgeTrigger::EDGE_RISING); + else if(reply == "FALLing") + trig->SetType(EdgeTrigger::EDGE_FALLING); + else if(reply == "ALTernate") + trig->SetType(EdgeTrigger::EDGE_ALTERNATING); + else if(reply == "BOTH") + trig->SetType(EdgeTrigger::EDGE_ANY); + else + LogWarning("Unknown trigger slope %s\n", reply.c_str()); +} + +/** + @brief Parses a trigger condition + */ +Trigger::Condition MagnovaOscilloscope::GetCondition(string reply) +{ + reply = Trim(reply); + + if(reply == "LTHan") + return Trigger::CONDITION_LESS; + else if(reply == "GTHan") + return Trigger::CONDITION_GREATER; + else if(reply == "INSide") + return Trigger::CONDITION_BETWEEN; + else if(reply == "OUTSide") + return Trigger::CONDITION_NOT_BETWEEN; + + //unknown + LogWarning("Unknown trigger condition [%s]\n", reply.c_str()); + return Trigger::CONDITION_LESS; +} + +/** + @brief Pushes settings for a trigger condition under a .Condition field + */ +void MagnovaOscilloscope::PushCondition(const string& path, Trigger::Condition cond) +{ + switch(cond) + { + case Trigger::CONDITION_LESS: + sendOnly("%s LTHan", path.c_str()); + break; + + case Trigger::CONDITION_GREATER: + sendOnly("%s GTHan", path.c_str()); + break; + + case Trigger::CONDITION_BETWEEN: + sendOnly("%s INSide", path.c_str()); + break; + + case Trigger::CONDITION_NOT_BETWEEN: + sendOnly("%s OUTSide", path.c_str()); + break; + + //Other values are not legal here, it seems + default: + break; + } +} + +void MagnovaOscilloscope::PushFloat(string path, float f) +{ + sendOnly("%s %1.2E", path.c_str(), f); +} + +vector MagnovaOscilloscope::GetTriggerTypes() +{ + vector ret; + ret.push_back(DropoutTrigger::GetTriggerName()); + ret.push_back(EdgeTrigger::GetTriggerName()); + ret.push_back(PulseWidthTrigger::GetTriggerName()); + ret.push_back(RuntTrigger::GetTriggerName()); + ret.push_back(SlewRateTrigger::GetTriggerName()); + ret.push_back(UartTrigger::GetTriggerName()); + ret.push_back(WindowTrigger::GetTriggerName()); + // TODO: Add missing triggers (NEDGe, DELay, INTerval, SHOLd, PATTern + Decode-SPI/I2C/Parallel) + return ret; +} + void MagnovaOscilloscope::PullTrigger() { std::string reply; @@ -2134,7 +2221,7 @@ void MagnovaOscilloscope::PullTrigger() PullPulseWidthTrigger(); else if(reply == "WINDow") PullWindowTrigger(); - // Note that PULSe, PATTern, QUALified, VIDeo, IIC, SPI, LIN, CAN, FLEXray, CANFd & IIS are not yet handled + // Note that NEDGe, DELay, INTerval, SHOLd, PATTern + Decode-SPI/I2C/Parallel are not yet handled //Unrecognized trigger type else { @@ -2174,6 +2261,68 @@ void MagnovaOscilloscope::PullTriggerSource(Trigger* trig, string triggerModeNam LogWarning("Unknown trigger source \"%s\"\n", reply.c_str()); } +void MagnovaOscilloscope::PushTrigger() +{ + auto dt = dynamic_cast(m_trigger); + auto et = dynamic_cast(m_trigger); + auto pt = dynamic_cast(m_trigger); + auto rt = dynamic_cast(m_trigger); + auto st = dynamic_cast(m_trigger); + auto ut = dynamic_cast(m_trigger); + auto wt = dynamic_cast(m_trigger); + + if(dt) + { + sendOnly(":TRIGGER:TYPE TIMeout"); + sendOnly(":TRIGGER:DROPOUT:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + PushDropoutTrigger(dt); + } + else if(pt) + { + sendOnly(":TRIGGER:TYPE PULSe"); + sendOnly(":TRIGGER:INTERVAL:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + PushPulseWidthTrigger(pt); + } + else if(rt) + { + sendOnly(":TRIGGER:TYPE RUNT"); + sendOnly(":TRIGGER:RUNT:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + PushRuntTrigger(rt); + } + else if(st) + { + sendOnly(":TRIGGER:TYPE SLOPe"); + sendOnly(":TRIGGER:SLOPE:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + PushSlewRateTrigger(st); + } + else if(ut) + { + sendOnly(":TRIGGER:TYPE DECode"); + // Trigger group not accessible for now via SCPI + //sendOnly(":TRIGGER:UART:RXSOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + //sendOnly(":TRIGGER:UART:TXSOURCE %s", GetChannelName(m_trigger->GetInput(1).m_channel->GetIndex()).c_str()); + PushUartTrigger(ut); + } + else if(wt) + { + sendOnly(":TRIGGER:TYPE WINDow"); + sendOnly(":TRIGGER:WINDOW:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + PushWindowTrigger(wt); + } + + // TODO: Add missing triggers + + else if(et) //must be last + { + sendOnly(":TRIGGER:TYPE EDGe"); + sendOnly(":TRIGGER:EDGE:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + PushEdgeTrigger(et, "EDGe"); + } + + else + LogWarning("PushTrigger on an unimplemented trigger type.\n"); +} + /** @brief Reads settings for a dropout trigger from the instrument */ @@ -2213,6 +2362,16 @@ void MagnovaOscilloscope::PullDropoutTrigger() dt->SetResetType(DropoutTrigger::RESET_NONE); } +/** + @brief Pushes settings for a dropout trigger to the instrument + */ +void MagnovaOscilloscope::PushDropoutTrigger(DropoutTrigger* trig) +{ + PushFloat(":TRIGGER:TIMeout:LEVEL", trig->GetLevel()); + PushFloat(":TRIGGER:TIMeout:TIME", trig->GetDropoutTime() * SECONDS_PER_FS); + sendOnly(":TRIGGER:TIMeout:SLOPe %s", (trig->GetType() == DropoutTrigger::EDGE_RISING) ? "RISing" : "FALLing"); +} + /** @brief Reads settings for an edge trigger from the instrument */ @@ -2241,6 +2400,33 @@ void MagnovaOscilloscope::PullEdgeTrigger() GetTriggerSlope(et, Trim(converse(":TRIGGER:EDGE:SLOPE?"))); } +/** + @brief Pushes settings for an edge trigger to the instrument + */ +void MagnovaOscilloscope::PushEdgeTrigger(EdgeTrigger* trig, const std::string trigType) +{ + switch(trig->GetType()) + { + case EdgeTrigger::EDGE_RISING: + sendOnly(":TRIGGER:%s:SLOPE RISING", trigType.c_str()); + break; + + case EdgeTrigger::EDGE_FALLING: + sendOnly(":TRIGGER:%s:SLOPE FALLING", trigType.c_str()); + break; + + case EdgeTrigger::EDGE_ANY: + sendOnly(":TRIGGER:%s:SLOPE ALTERNATE", trigType.c_str()); + break; + + default: + LogWarning("Invalid trigger type %d\n", trig->GetType()); + break; + } + //Level + sendOnly(":TRIGGER:%s:LEVEL %1.2E", trigType.c_str(), trig->GetLevel()); +} + /** @brief Reads settings for an edge trigger from the instrument */ @@ -2273,7 +2459,7 @@ void MagnovaOscilloscope::PullPulseWidthTrigger() pt->SetLowerBound(fs.ParseString(converse(":TRIGGER:PULSe:DURation:LOWer?"))); //Max range - pt->SetUpperBound(fs.ParseString(converse(":TRIGGER:PULSe:DURation:LOWer?"))); + pt->SetUpperBound(fs.ParseString(converse(":TRIGGER:PULSe:DURation:UPPer?"))); //Slope reply = Trim(converse(":TRIGGER:PULSe:POLarity?")); @@ -2284,6 +2470,18 @@ void MagnovaOscilloscope::PullPulseWidthTrigger() } +/** + @brief Pushes settings for a pulse width trigger to the instrument + */ +void MagnovaOscilloscope::PushPulseWidthTrigger(PulseWidthTrigger* trig) +{ + PushFloat(":TRIGGER:PULSe:LEVEL", trig->GetLevel()); + PushCondition(":TRIGGER:PULSe:TIMing", trig->GetCondition()); + PushFloat(":TRIGGER:PULSe:DURation:LOWer", trig->GetUpperBound() * SECONDS_PER_FS); + PushFloat(":TRIGGER:PULSe:DURation:UPPer", trig->GetLowerBound() * SECONDS_PER_FS); + sendOnly(":TRIGGER:PULSe:POLarity %s", trig->GetType() != PulseWidthTrigger::EDGE_FALLING ? "POSitive" : "NEGative"); +} + /** @brief Reads settings for a runt-pulse trigger from the instrument */ @@ -2318,7 +2516,7 @@ void MagnovaOscilloscope::PullRuntTrigger() rt->SetUpperInterval(fs.ParseString(converse(":TRIGGER:RUNT:DURation:UPPer?"))); //Slope - reply = Trim(converse(":TRIGGER:RUNT:POLARITY?")); + reply = Trim(converse(":TRIGger:RUNT:POLarity?")); if(reply == "POSitive") rt->SetSlope(RuntTrigger::EDGE_RISING); else if(reply == "NEGative") @@ -2328,6 +2526,20 @@ void MagnovaOscilloscope::PullRuntTrigger() rt->SetCondition(GetCondition(converse(":TRIGGER:RUNT:TIMing?"))); } +/** + @brief Pushes settings for a runt trigger to the instrument + */ +void MagnovaOscilloscope::PushRuntTrigger(RuntTrigger* trig) +{ + PushFloat(":TRIGGER:RUNT:LEVel1", trig->GetLowerBound()); + PushFloat(":TRIGGER:RUNT:LEVel2", trig->GetUpperBound()); + PushFloat(":TRIGGER:RUNT:DURation:LOWer", trig->GetLowerInterval() * SECONDS_PER_FS); + PushFloat(":TRIGGER:RUNT:DURation:UPPer", trig->GetUpperInterval() * SECONDS_PER_FS); + + sendOnly(":TRIGger:RUNT:POLarity %s", (trig->GetSlope() != RuntTrigger::EDGE_FALLING) ? "POSitive" : "NEGative"); + PushCondition(":TRIGGER:RUNT:TIMing", trig->GetCondition()); +} + /** @brief Reads settings for a slew rate trigger from the instrument */ @@ -2356,24 +2568,35 @@ void MagnovaOscilloscope::PullSlewRateTrigger() st->SetUpperBound(v.ParseString(converse(":TRIGGER:SLOPe:LEVel2?"))); //Lower interval - st->SetLowerInterval(fs.ParseString(converse(":TRIGGER:SLOPE:DURation:LOWer?"))); + st->SetLowerInterval(fs.ParseString(converse(":TRIGGER:SLOPe:DURation:LOWer?"))); //Upper interval - st->SetUpperInterval(fs.ParseString(converse(":TRIGGER:SLOPE:DURation:UPPer?"))); + st->SetUpperInterval(fs.ParseString(converse(":TRIGGER:SLOPe:DURation:UPPer?"))); //Slope - reply = Trim(converse("TRIGGER:SLOPE:SLOPE?")); + reply = Trim(converse(":TRIGger:SLOPe:TYPE?")); if(reply == "RISing") st->SetSlope(SlewRateTrigger::EDGE_RISING); - else if(reply == "FALLing") + else st->SetSlope(SlewRateTrigger::EDGE_FALLING); - else if(reply == "ALTernate") - st->SetSlope(SlewRateTrigger::EDGE_ANY); //Condition st->SetCondition(GetCondition(converse("TRIGGER:SLOPe:TIMing?"))); } +/** + @brief Pushes settings for a slew rate trigger to the instrument + */ +void MagnovaOscilloscope::PushSlewRateTrigger(SlewRateTrigger* trig) +{ + PushCondition(":TRIGGER:SLOPE", trig->GetCondition()); + PushFloat(":TRIGGER:SLOPe:DURation:LOWer", trig->GetLowerInterval() * SECONDS_PER_FS); + PushFloat(":TRIGGER:SLOPe:DURation:UPPer", trig->GetUpperInterval() * SECONDS_PER_FS); + PushFloat(":TRIGGER:SLOPe:LEVel1", trig->GetLowerBound()); + PushFloat(":TRIGGER:SLOPe:LEVel2", trig->GetUpperBound()); + sendOnly(":TRIGger:SLOPe:TIMing %s", (trig->GetSlope() != SlewRateTrigger::EDGE_FALLING) ? "RISing" : "FALLing"); +} + /** @brief Reads settings for a UART trigger from the instrument */ @@ -2426,7 +2649,7 @@ void MagnovaOscilloscope::PullUartTrigger() //ut->SetStopBits(stof(Trim(converse(":TRIGGER:UART:STOP?")))); //Trigger type - reply = Trim(converse(":TRIGGER:UART:EVENt?")); + reply = Trim(converse(":TRIGger:DECode:UART:EVENt?")); if(reply == "FSTart") ut->SetMatchType(UartTrigger::TYPE_START); else if(reply == "FPCHeck") @@ -2437,246 +2660,29 @@ void MagnovaOscilloscope::PullUartTrigger() LogWarning("Unsupported UART trigger condition '%s'", reply.c_str()); // Data to match (there is no pattern2 on sds) - p1 = Trim(converse(":TRIGGER:UART:DATA:WORD0?")); - p2 = Trim(converse(":TRIGGER:UART:DATA:WORD1?")); + p1 = Trim(converse(":TRIGger:DECode:UART:DATA:WORD0?")); + p2 = Trim(converse(":TRIGger:DECode:UART:DATA:WORD1?")); // TODO set ignorep2 according to p2 value ut->SetPatterns(p1, p2, false); } -/** - @brief Reads settings for a window trigger from the instrument - */ -void MagnovaOscilloscope::PullWindowTrigger() -{ // TODO - //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 WindowTrigger(this); - WindowTrigger* wt = dynamic_cast(m_trigger); - - Unit v(Unit::UNIT_VOLTS); - - //Lower bound - wt->SetLowerBound(v.ParseString(converse(":TRIGGER:WINDOW:LLEVEL?"))); - - //Upper bound - wt->SetUpperBound(v.ParseString(converse(":TRIGGER:WINDOW:HLEVEL?"))); -} - -/** - @brief Processes the slope for an edge or edge-derived trigger - */ -void MagnovaOscilloscope::GetTriggerSlope(EdgeTrigger* trig, string reply) - -{ - reply = Trim(reply); - - if(reply == "RISing") - trig->SetType(EdgeTrigger::EDGE_RISING); - else if(reply == "FALLing") - trig->SetType(EdgeTrigger::EDGE_FALLING); - else if(reply == "ALTernate") - trig->SetType(EdgeTrigger::EDGE_ALTERNATING); - else if(reply == "BOTH") - trig->SetType(EdgeTrigger::EDGE_ANY); - else - LogWarning("Unknown trigger slope %s\n", reply.c_str()); -} - -/** - @brief Parses a trigger condition - */ -Trigger::Condition MagnovaOscilloscope::GetCondition(string reply) -{ - reply = Trim(reply); - - if(reply == "LTHan") - return Trigger::CONDITION_LESS; - else if(reply == "GTHan") - return Trigger::CONDITION_GREATER; - else if(reply == "INSide") - return Trigger::CONDITION_BETWEEN; - else if(reply == "OUTSide") - return Trigger::CONDITION_NOT_BETWEEN; - - //unknown - LogWarning("Unknown trigger condition [%s]\n", reply.c_str()); - return Trigger::CONDITION_LESS; -} - -void MagnovaOscilloscope::PushTrigger() -{ - auto dt = dynamic_cast(m_trigger); - auto et = dynamic_cast(m_trigger); - auto pt = dynamic_cast(m_trigger); - auto rt = dynamic_cast(m_trigger); - auto st = dynamic_cast(m_trigger); - auto ut = dynamic_cast(m_trigger); - auto wt = dynamic_cast(m_trigger); - - if(dt) - { - sendOnly(":TRIGGER:TYPE DROPOUT"); - sendOnly(":TRIGGER:DROPOUT:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); - PushDropoutTrigger(dt); - } - else if(pt) - { - sendOnly(":TRIGGER:TYPE INTERVAL"); - sendOnly(":TRIGGER:INTERVAL:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); - PushPulseWidthTrigger(pt); - } - else if(rt) - { - sendOnly(":TRIGGER:TYPE RUNT"); - sendOnly(":TRIGGER:RUNT:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); - PushRuntTrigger(rt); - } - else if(st) - { - sendOnly(":TRIGGER:TYPE SLOPE"); - sendOnly(":TRIGGER:SLOPE:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); - PushSlewRateTrigger(st); - } - else if(ut) - { - sendOnly(":TRIGGER:TYPE UART"); - // TODO: Validate these trigger allocations - sendOnly(":TRIGGER:UART:RXSOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); - sendOnly(":TRIGGER:UART:TXSOURCE %s", GetChannelName(m_trigger->GetInput(1).m_channel->GetIndex()).c_str()); - PushUartTrigger(ut); - } - else if(wt) - { - sendOnly(":TRIGGER:TYPE WINDOW"); - sendOnly(":TRIGGER:WINDOW:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); - PushWindowTrigger(wt); - } - - // TODO: Add in PULSE, VIDEO, PATTERN, QUALITFIED, SPI, IIC, CAN, LIN, FLEXRAY and CANFD Triggers - - else if(et) //must be last - { - sendOnly(":TRIGGER:TYPE EDGE"); - sendOnly(":TRIGGER:EDGE:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); - PushEdgeTrigger(et, "EDGE"); - } - - else - LogWarning("Unknown trigger type (not an edge)\n"); -} - -/** - @brief Pushes settings for a dropout trigger to the instrument - */ -void MagnovaOscilloscope::PushDropoutTrigger(DropoutTrigger* trig) -{ - - PushFloat(":TRIGGER:DROPOUT:LEVEL", trig->GetLevel()); - PushFloat(":TRIGGER:DROPOUT:TIME", trig->GetDropoutTime() * SECONDS_PER_FS); - sendOnly(":TRIGGER:DROPOUT:SLOPE %s", (trig->GetType() == DropoutTrigger::EDGE_RISING) ? "RISING" : "FALLING"); - sendOnly(":TRIGGER:DROPOUT:TYPE %s", (trig->GetResetType() == DropoutTrigger::RESET_OPPOSITE) ? "EDGE" : "STATE"); -} - -/** - @brief Pushes settings for an edge trigger to the instrument - */ -void MagnovaOscilloscope::PushEdgeTrigger(EdgeTrigger* trig, const std::string trigType) -{ - switch(trig->GetType()) - { - case EdgeTrigger::EDGE_RISING: - sendOnly(":TRIGGER:%s:SLOPE RISING", trigType.c_str()); - break; - - case EdgeTrigger::EDGE_FALLING: - sendOnly(":TRIGGER:%s:SLOPE FALLING", trigType.c_str()); - break; - - case EdgeTrigger::EDGE_ANY: - sendOnly(":TRIGGER:%s:SLOPE ALTERNATE", trigType.c_str()); - break; - - default: - LogWarning("Invalid trigger type %d\n", trig->GetType()); - break; - } - //Level - sendOnly(":TRIGGER:%s:LEVEL %1.2E", trigType.c_str(), trig->GetLevel()); -} - -/** - @brief Pushes settings for a pulse width trigger to the instrument - */ -void MagnovaOscilloscope::PushPulseWidthTrigger(PulseWidthTrigger* trig) -{ - PushEdgeTrigger(trig, "INTERVAL"); - PushCondition(":TRIGGER:INTERVAL", trig->GetCondition()); - PushFloat(":TRIGGER:INTERVAL:TUPPER", trig->GetUpperBound() * SECONDS_PER_FS); - PushFloat(":TRIGGER:INTERVAL:TLOWER", trig->GetLowerBound() * SECONDS_PER_FS); -} - -/** - @brief Pushes settings for a runt trigger to the instrument - */ -void MagnovaOscilloscope::PushRuntTrigger(RuntTrigger* trig) -{ - PushCondition(":TRIGGER:RUNT", trig->GetCondition()); - PushFloat(":TRIGGER:RUNT:TUPPER", trig->GetUpperInterval() * SECONDS_PER_FS); - PushFloat(":TRIGGER:RUNT:TLOWER", trig->GetLowerInterval() * SECONDS_PER_FS); - PushFloat(":TRIGGER:RUNT:LLEVEL", trig->GetLowerBound()); - PushFloat(":TRIGGER:RUNT:HLEVEL", trig->GetUpperBound()); - - sendOnly(":TRIGGER:RUNT:POLARITY %s", (trig->GetSlope() == RuntTrigger::EDGE_RISING) ? "POSITIVE" : "NEGATIVE"); -} - -/** - @brief Pushes settings for a slew rate trigger to the instrument - */ -void MagnovaOscilloscope::PushSlewRateTrigger(SlewRateTrigger* trig) -{ - PushCondition(":TRIGGER:SLOPE", trig->GetCondition()); - PushFloat(":TRIGGER:SLOPE:TUPPER", trig->GetUpperInterval() * SECONDS_PER_FS); - PushFloat(":TRIGGER:SLOPE:TLOWER", trig->GetLowerInterval() * SECONDS_PER_FS); - PushFloat(":TRIGGER:SLOPE:HLEVEL", trig->GetUpperBound()); - PushFloat(":TRIGGER:SLOPE:LLEVEL", trig->GetLowerBound()); - - sendOnly(":TRIGGER:SLOPE:SLOPE %s", - (trig->GetSlope() == SlewRateTrigger::EDGE_RISING) ? "RISING" : - (trig->GetSlope() == SlewRateTrigger::EDGE_FALLING) ? "FALLING" : - "ALTERNATE"); -} - /** @brief Pushes settings for a UART trigger to the instrument */ void MagnovaOscilloscope::PushUartTrigger(UartTrigger* trig) { - float nstop; - string pattern1; - //Special parameter for trigger level - PushFloat(":TRIGGER:UART:LIMIT", trig->GetLevel()); - - //AtPosition - //Bit9State - PushFloat(":TRIGGER:UART:BAUD", trig->GetBitRate()); - sendOnly(":TRIGGER:UART:BITORDER LSB"); - //DataBytesLenValue1 - //DataBytesLenValue2 - //DataCondition - //FrameDelimiter - //InterframeMinBits - //NeedDualLevels - //NeededSources - sendOnly(":TRIGGER:UART:DLENGTH 8"); - - switch(trig->GetParityType()) + string p1,p2; + // Level : Not available in Magnova SCPI implemetnation for now + // PushFloat(":TRIGGER:UART:LIMIT", trig->GetLevel()); + + //Bit9State : Not available in Magnova SCPI implemetnation for now + //PushFloat(":TRIGGER:UART:BAUD", trig->GetBitRate()); + //sendOnly(":TRIGGER:UART:BITORDER LSB"); + // Data length : : Not available in Magnova SCPI implemetnation for now + // sendOnly(":TRIGGER:UART:DLENGTH 8"); + + // Parity : Not available in Magnova SCPI implemetnation for now + /*switch(trig->GetParityType()) { case UartTrigger::PARITY_NONE: sendOnly(":TRIGGER:UART:PARITY NONE"); @@ -2696,101 +2702,94 @@ void MagnovaOscilloscope::PushUartTrigger(UartTrigger* trig) case UartTrigger::PARITY_SPACE: sendOnly(":TRIGGER:UART:PARITY SPACE"); break; - } - - //Pattern length depends on the current format. - //Note that the pattern length is in bytes, not bits, even though patterns are in binary. - pattern1 = trig->GetPattern1(); - sendOnly(":TRIGGER:UART:DLENGTH \"%d\"", (int)pattern1.length() / 8); - - PushCondition(":TRIGGER:UART", trig->GetCondition()); - - //Polarity - sendOnly(":TRIGGER:UART:IDLE %s", (trig->GetPolarity() == UartTrigger::IDLE_HIGH) ? "HIGH" : "LOW"); + }*/ - nstop = trig->GetStopBits(); + //Polarity : Not available in Magnova SCPI implemetnation for now + //sendOnly(":TRIGGER:UART:IDLE %s", (trig->GetPolarity() == UartTrigger::IDLE_HIGH) ? "HIGH" : "LOW"); + // Stops bits : Not available in Magnova SCPI implemetnation for now + /*float nstop = trig->GetStopBits(); if(nstop == 1) sendOnly(":TRIGGER:UART:STOP 1"); else if(nstop == 2) sendOnly(":TRIGGER:UART:STOP 2"); else - sendOnly(":TRIGGER:UART:STOP 1.5"); + sendOnly(":TRIGGER:UART:STOP 1.5");*/ + + //Pattern + p1 = trig->GetPattern1(); + p2 = trig->GetPattern2(); //Match type switch(trig->GetMatchType()) { case UartTrigger::TYPE_START: - sendOnly(":TRIGGER:UART:CONDITION START"); - break; - case UartTrigger::TYPE_STOP: - sendOnly(":TRIGGER:UART:CONDITION STOP"); + sendOnly(":TRIGger:DECode:UART:EVENt FSTart"); break; case UartTrigger::TYPE_PARITY_ERR: - sendOnly(":TRIGGER:UART:CONDITION ERROR"); + sendOnly(":TRIGger:DECode:UART:EVENt FPCHeck"); break; - default: case UartTrigger::TYPE_DATA: - sendOnly(":TRIGGER:UART:CONDITION DATA"); + sendOnly(":TRIGger:DECode:UART:EVENt DATA"); + break; + default: + case UartTrigger::TYPE_STOP: + LogWarning("Unsupported match type: %d\n",trig->GetMatchType()); break; } + sendOnly(":TRIGger:DECode:UART:DATA:WORD0 %s", p1.c_str()); + sendOnly(":TRIGger:DECode:UART:DATA:WORD2 %s", p2.c_str()); } /** - @brief Pushes settings for a window trigger to the instrument + @brief Reads settings for a window trigger from the instrument */ -void MagnovaOscilloscope::PushWindowTrigger(WindowTrigger* trig) -{ - PushFloat(":TRIGGER:WINDOW:LLEVEL", trig->GetLowerBound()); - PushFloat(":TRIGGER:WINDOW:HLEVEL", trig->GetUpperBound()); +void MagnovaOscilloscope::PullWindowTrigger() +{ // TODO + //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 WindowTrigger(this); + WindowTrigger* wt = dynamic_cast(m_trigger); + + Unit v(Unit::UNIT_VOLTS); + string type = converse(":TRIGger:WINDow:TYPE?"); + if(type == "ENTer") + wt->SetWindowType(WindowTrigger::WINDOW_ENTER); + else + wt->SetWindowType(WindowTrigger::WINDOW_EXIT); + + //Lower bound + wt->SetLowerBound(v.ParseString(converse(":TRIGger:WINDow:LEVel1?"))); + + //Upper bound + wt->SetUpperBound(v.ParseString(converse(":TRIGger:WINDow:LEVel2?"))); } /** - @brief Pushes settings for a trigger condition under a .Condition field + @brief Pushes settings for a window trigger to the instrument */ -void MagnovaOscilloscope::PushCondition(const string& path, Trigger::Condition cond) +void MagnovaOscilloscope::PushWindowTrigger(WindowTrigger* trig) { - switch(cond) + switch(trig->GetWindowType()) { - case Trigger::CONDITION_LESS: - sendOnly("%s:LIMIT LESSTHAN", path.c_str()); + case WindowTrigger::WINDOW_ENTER: + sendOnly(":TRIGger:WINDow:TYPE ENTer"); break; - - case Trigger::CONDITION_GREATER: - sendOnly("%s:LIMIT GREATERTHAN", path.c_str()); - break; - - case Trigger::CONDITION_BETWEEN: - sendOnly("%s:LIMIT INNER", path.c_str()); - break; - - case Trigger::CONDITION_NOT_BETWEEN: - sendOnly("%s:LIMIT OUTER", path.c_str()); + case WindowTrigger::WINDOW_EXIT: + sendOnly(":TRIGger:WINDow:TYPE LEAVe"); break; - - //Other values are not legal here, it seems default: + LogWarning("Unsupported window type: %d\n",trig->GetWindowType()); break; } -} - -void MagnovaOscilloscope::PushFloat(string path, float f) -{ - sendOnly("%s %1.2E", path.c_str(), f); -} - -vector MagnovaOscilloscope::GetTriggerTypes() -{ - vector ret; - ret.push_back(DropoutTrigger::GetTriggerName()); - ret.push_back(EdgeTrigger::GetTriggerName()); - ret.push_back(PulseWidthTrigger::GetTriggerName()); - ret.push_back(RuntTrigger::GetTriggerName()); - ret.push_back(SlewRateTrigger::GetTriggerName()); - if(m_hasUartTrigger) - ret.push_back(UartTrigger::GetTriggerName()); - ret.push_back(WindowTrigger::GetTriggerName()); - // TODO: Add in PULSE, VIDEO, PATTERN, QUALITFIED, SPI, IIC, CAN, LIN, FLEXRAY and CANFD Triggers - return ret; + PushFloat(":TRIGger:WINDow:LEVel1", trig->GetLowerBound()); + PushFloat(":TRIGger:WINDow:LEVel2", trig->GetUpperBound()); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h index 17a82cd9..399a26cd 100644 --- a/scopehal/MagnovaOscilloscope.h +++ b/scopehal/MagnovaOscilloscope.h @@ -317,11 +317,8 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP bool m_hasLA; bool m_hasDVM; bool m_hasFunctionGen; - bool m_hasFastSampleRate; //-M models - int m_memoryDepthOption; //0 = base, after that number is max sample count in millions bool m_hasI2cTrigger; bool m_hasSpiTrigger; - bool m_hasUartTrigger; ///Maximum bandwidth we support, in MHz unsigned int m_maxBandwidth; From 1afbf866e03b37a2a41c25a5c052d79ba48cd7dd Mon Sep 17 00:00:00 2001 From: fredzo Date: Fri, 12 Dec 2025 21:58:00 +0100 Subject: [PATCH 19/33] First step to protocol error reporting. --- scopehal/MagnovaOscilloscope.cpp | 49 ++++++++++++++++++-------------- scopehal/MagnovaOscilloscope.h | 4 ++- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 3994fdbe..63c4ac3b 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -142,23 +142,31 @@ void MagnovaOscilloscope::flush() m_transport->ReadReply(); } -void MagnovaOscilloscope::flushWaveformData() -{ // Read any pending waveform data until we get the double 0x0a end marker - uint8_t tmp; - while(true) - { - size_t avaiable = m_transport->ReadRawData(1, &tmp); - if(!avaiable) break; - if(tmp == 0x0a) - { - avaiable = m_transport->ReadRawData(1, &tmp); - if(!avaiable) break; - if(tmp == 0x0a) break; - } - } - m_transport->FlushRXBuffer(); +void MagnovaOscilloscope::protocolError(bool flush, const char* fmt, va_list ap) +{ + char opString[128]; + vsnprintf(opString, sizeof(opString), fmt, ap); + LogError("Protocol error%s: %s.\n", flush ? ", flushing read stream" : "", opString); + if(flush) m_transport->ReadReply(); +} + +void MagnovaOscilloscope::protocolError(const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + protocolError(false, fmt, ap); + va_end(ap); +} + +void MagnovaOscilloscope::protocolErrorWithFlush(const char* fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + protocolError(true, fmt, ap); + va_end(ap); } + void MagnovaOscilloscope::SharedCtorInit() { //Add the external trigger input @@ -238,9 +246,6 @@ void MagnovaOscilloscope::IdentifyHardware() LogWarning("Model \"%s\" is unknown, available sample rates/memory depths may not be properly detected\n", m_model.c_str()); } - // TODO - // Only 5 ms for newer models - // m_transport->EnableRateLimiting(chrono::milliseconds(5)); } else { @@ -273,7 +278,6 @@ void MagnovaOscilloscope::DetectOptions() /** @brief Creates digital channels for the oscilloscope */ - void MagnovaOscilloscope::AddDigitalChannels(unsigned int count) { m_digitalChannelCount = count; @@ -659,7 +663,10 @@ double MagnovaOscilloscope::GetChannelAttenuation(size_t i) reply = converse(":CHAN%zu:DIV?", i + 1); int d; - sscanf(reply.c_str(), "%d", &d); + if(sscanf(reply.c_str(), "%d", &d) != 1) + { + protocolError("invalid channel attenuation value '%S'",reply); + } return 1/d; } @@ -729,7 +736,7 @@ unsigned int MagnovaOscilloscope::GetChannelBandwidthLimit(size_t i) else if(reply == "200000000") return 200; - LogWarning("MagnovaOscilloscope::GetChannelBandwidthLimit got invalid bwlimit %s\n", reply.c_str()); + protocolError("MagnovaOscilloscope::GetChannelBandwidthLimit got invalid bwlimit %s\n", reply.c_str()); return 0; } diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h index 399a26cd..fb5e7506 100644 --- a/scopehal/MagnovaOscilloscope.h +++ b/scopehal/MagnovaOscilloscope.h @@ -67,7 +67,9 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP void sendOnly(const char* fmt, ...); bool sendWithAck(const char* fmt, ...); void flush(); - void flushWaveformData(); + void protocolError(bool flush, const char* fmt, va_list ap); + void protocolError(const char* fmt, ...); + void protocolErrorWithFlush(const char* fmt, ...); protected: void IdentifyHardware(); From cf16ab7f5cad03e5177ae1ad35d7b9b1fe0449dd Mon Sep 17 00:00:00 2001 From: fredzo Date: Sun, 14 Dec 2025 00:46:49 +0100 Subject: [PATCH 20/33] More triggering. --- scopehal/MagnovaOscilloscope.cpp | 59 +++++++++++++++++++++----------- scopehal/MagnovaOscilloscope.h | 2 +- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 63c4ac3b..d6839f57 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -2118,19 +2118,33 @@ void MagnovaOscilloscope::SetDigitalThreshold(size_t channel, float level) /** @brief Processes the slope for an edge or edge-derived trigger */ -void MagnovaOscilloscope::GetTriggerSlope(EdgeTrigger* trig, string reply) +void MagnovaOscilloscope::GetTriggerSlope(Trigger* trig, string reply) { + auto dt = dynamic_cast(trig); + auto et = dynamic_cast(trig); reply = Trim(reply); + if(reply == "RISing") - trig->SetType(EdgeTrigger::EDGE_RISING); + { + if(dt) dt->SetType(DropoutTrigger::EDGE_RISING); + if(et) et->SetType(EdgeTrigger::EDGE_RISING); + } else if(reply == "FALLing") - trig->SetType(EdgeTrigger::EDGE_FALLING); + { + if(dt) dt->SetType(DropoutTrigger::EDGE_FALLING); + if(et) et->SetType(EdgeTrigger::EDGE_FALLING); + } else if(reply == "ALTernate") - trig->SetType(EdgeTrigger::EDGE_ALTERNATING); + { + if(et) et->SetType(EdgeTrigger::EDGE_ALTERNATING); + } else if(reply == "BOTH") - trig->SetType(EdgeTrigger::EDGE_ANY); + { + if(dt) dt->SetType(DropoutTrigger::EDGE_ANY); + if(et) et->SetType(EdgeTrigger::EDGE_ANY); + } else LogWarning("Unknown trigger slope %s\n", reply.c_str()); } @@ -2187,7 +2201,7 @@ void MagnovaOscilloscope::PushCondition(const string& path, Trigger::Condition c void MagnovaOscilloscope::PushFloat(string path, float f) { - sendOnly("%s %1.2E", path.c_str(), f); + sendOnly("%s %1.5E", path.c_str(), f); } vector MagnovaOscilloscope::GetTriggerTypes() @@ -2281,13 +2295,13 @@ void MagnovaOscilloscope::PushTrigger() if(dt) { sendOnly(":TRIGGER:TYPE TIMeout"); - sendOnly(":TRIGGER:DROPOUT:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + sendOnly(":TRIGGER:TIMeout:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); PushDropoutTrigger(dt); } else if(pt) { sendOnly(":TRIGGER:TYPE PULSe"); - sendOnly(":TRIGGER:INTERVAL:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + sendOnly(":TRIGGER:PULSe:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); PushPulseWidthTrigger(pt); } else if(rt) @@ -2299,7 +2313,7 @@ void MagnovaOscilloscope::PushTrigger() else if(st) { sendOnly(":TRIGGER:TYPE SLOPe"); - sendOnly(":TRIGGER:SLOPE:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + sendOnly(":TRIGGER:SLOPe:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); PushSlewRateTrigger(st); } else if(ut) @@ -2313,7 +2327,7 @@ void MagnovaOscilloscope::PushTrigger() else if(wt) { sendOnly(":TRIGGER:TYPE WINDow"); - sendOnly(":TRIGGER:WINDOW:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + sendOnly(":TRIGGER:WINDow:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); PushWindowTrigger(wt); } @@ -2322,7 +2336,7 @@ void MagnovaOscilloscope::PushTrigger() else if(et) //must be last { sendOnly(":TRIGGER:TYPE EDGe"); - sendOnly(":TRIGGER:EDGE:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + sendOnly(":TRIGGER:EDGe:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); PushEdgeTrigger(et, "EDGe"); } @@ -2360,10 +2374,7 @@ void MagnovaOscilloscope::PullDropoutTrigger() dt->SetDropoutTime(stof(converse(":TRIGGER:TIMeout:TIME?"))*FS_PER_SECOND); //Edge type - if(Trim(converse(":TRIGGER:TIMeout:SLOPE?")) == "RISing") - dt->SetType(DropoutTrigger::EDGE_RISING); - else - dt->SetType(DropoutTrigger::EDGE_FALLING); + GetTriggerSlope(dt,converse(":TRIGGER:TIMeout:SLOPE?")); //Reset type dt->SetResetType(DropoutTrigger::RESET_NONE); @@ -2423,7 +2434,11 @@ void MagnovaOscilloscope::PushEdgeTrigger(EdgeTrigger* trig, const std::string t break; case EdgeTrigger::EDGE_ANY: - sendOnly(":TRIGGER:%s:SLOPE ALTERNATE", trigType.c_str()); + sendOnly(":TRIGGER:%s:SLOPE BOTH", trigType.c_str()); + break; + + case EdgeTrigger::EDGE_ALTERNATING: + sendOnly(":TRIGGER:%s:SLOPE ALTernate", trigType.c_str()); break; default: @@ -2462,11 +2477,13 @@ void MagnovaOscilloscope::PullPulseWidthTrigger() //Condition pt->SetCondition(GetCondition(converse(":TRIGGER:PULSe:TIMing?"))); + // Lower/upper not available on Magnova's pulse, only Threshod is available so let's map it lower bound + pt->SetLowerBound(stof((converse(":TRIGger:PULSe:THReshold?")))*FS_PER_SECOND); //Min range - pt->SetLowerBound(fs.ParseString(converse(":TRIGGER:PULSe:DURation:LOWer?"))); + //pt->SetLowerBound(fs.ParseString(converse(":TRIGGER:PULSe:DURation:LOWer?"))); //Max range - pt->SetUpperBound(fs.ParseString(converse(":TRIGGER:PULSe:DURation:UPPer?"))); + //pt->SetUpperBound(fs.ParseString(converse(":TRIGGER:PULSe:DURation:UPPer?"))); //Slope reply = Trim(converse(":TRIGGER:PULSe:POLarity?")); @@ -2484,8 +2501,10 @@ void MagnovaOscilloscope::PushPulseWidthTrigger(PulseWidthTrigger* trig) { PushFloat(":TRIGGER:PULSe:LEVEL", trig->GetLevel()); PushCondition(":TRIGGER:PULSe:TIMing", trig->GetCondition()); - PushFloat(":TRIGGER:PULSe:DURation:LOWer", trig->GetUpperBound() * SECONDS_PER_FS); - PushFloat(":TRIGGER:PULSe:DURation:UPPer", trig->GetLowerBound() * SECONDS_PER_FS); + // Lower/upper not available on Magnova's pulse, only Threshod is available so let's map it lower bound + PushFloat(":TRIGger:PULSe:THReshold", trig->GetUpperBound() * SECONDS_PER_FS); + //PushFloat(":TRIGGER:PULSe:DURation:LOWer", trig->GetUpperBound() * SECONDS_PER_FS); + //PushFloat(":TRIGGER:PULSe:DURation:UPPer", trig->GetLowerBound() * SECONDS_PER_FS); sendOnly(":TRIGGER:PULSe:POLarity %s", trig->GetType() != PulseWidthTrigger::EDGE_FALLING ? "POSitive" : "NEGative"); } diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h index fb5e7506..e24ff2a8 100644 --- a/scopehal/MagnovaOscilloscope.h +++ b/scopehal/MagnovaOscilloscope.h @@ -253,7 +253,7 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP void PullWindowTrigger(); void PullTriggerSource(Trigger* trig, std::string triggerModeName, bool isUart); - void GetTriggerSlope(EdgeTrigger* trig, std::string reply); + void GetTriggerSlope(Trigger* trig, std::string reply); Trigger::Condition GetCondition(std::string reply); void PushDropoutTrigger(DropoutTrigger* trig); From 0878fecca216184ba830bb1abe058f66165083a6 Mon Sep 17 00:00:00 2001 From: fredzo Date: Sun, 14 Dec 2025 00:48:43 +0100 Subject: [PATCH 21/33] Fixed triggering. --- scopehal/MagnovaOscilloscope.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index d6839f57..354dbe2c 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -2502,7 +2502,7 @@ void MagnovaOscilloscope::PushPulseWidthTrigger(PulseWidthTrigger* trig) PushFloat(":TRIGGER:PULSe:LEVEL", trig->GetLevel()); PushCondition(":TRIGGER:PULSe:TIMing", trig->GetCondition()); // Lower/upper not available on Magnova's pulse, only Threshod is available so let's map it lower bound - PushFloat(":TRIGger:PULSe:THReshold", trig->GetUpperBound() * SECONDS_PER_FS); + PushFloat(":TRIGger:PULSe:THReshold", trig->GetLowerBound() * SECONDS_PER_FS); //PushFloat(":TRIGGER:PULSe:DURation:LOWer", trig->GetUpperBound() * SECONDS_PER_FS); //PushFloat(":TRIGGER:PULSe:DURation:UPPer", trig->GetLowerBound() * SECONDS_PER_FS); sendOnly(":TRIGGER:PULSe:POLarity %s", trig->GetType() != PulseWidthTrigger::EDGE_FALLING ? "POSitive" : "NEGative"); From 7d5d561976c2cb3830b232fb9fe0699212e260d9 Mon Sep 17 00:00:00 2001 From: fredzo Date: Sun, 14 Dec 2025 00:56:57 +0100 Subject: [PATCH 22/33] Fixed trigger --- scopehal/MagnovaOscilloscope.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 354dbe2c..c7976da8 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -2620,7 +2620,8 @@ void MagnovaOscilloscope::PushSlewRateTrigger(SlewRateTrigger* trig) PushFloat(":TRIGGER:SLOPe:DURation:UPPer", trig->GetUpperInterval() * SECONDS_PER_FS); PushFloat(":TRIGGER:SLOPe:LEVel1", trig->GetLowerBound()); PushFloat(":TRIGGER:SLOPe:LEVel2", trig->GetUpperBound()); - sendOnly(":TRIGger:SLOPe:TIMing %s", (trig->GetSlope() != SlewRateTrigger::EDGE_FALLING) ? "RISing" : "FALLing"); + sendOnly(":TRIGger:SLOPe:TYPE %s", (trig->GetSlope() != SlewRateTrigger::EDGE_FALLING) ? "RISing" : "FALLing"); + PushCondition(":TRIGger:SLOPe:TIMing",trig->GetCondition()); } /** From 965f74ded451b8840ae1310fe5d7e2736203948f Mon Sep 17 00:00:00 2001 From: fredzo Date: Mon, 15 Dec 2025 01:28:13 +0100 Subject: [PATCH 23/33] Fixed logging and error reporting. Fixed uart trigger source. --- scopehal/MagnovaOscilloscope.cpp | 174 +++++++++++++++++++------------ 1 file changed, 110 insertions(+), 64 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index c7976da8..4fd2a2f0 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -601,7 +601,7 @@ OscilloscopeChannel::CouplingType MagnovaOscilloscope::GetChannelCoupling(size_t return OscilloscopeChannel::COUPLE_GND; //invalid - LogWarning("MagnovaOscilloscope::GetChannelCoupling got invalid coupling [%s] [%s]\n", + protocolError("MagnovaOscilloscope::GetChannelCoupling got invalid coupling [%s] [%s]\n", replyType.c_str(), replyImp.c_str()); return OscilloscopeChannel::COUPLE_SYNTHETIC; @@ -665,7 +665,7 @@ double MagnovaOscilloscope::GetChannelAttenuation(size_t i) int d; if(sscanf(reply.c_str(), "%d", &d) != 1) { - protocolError("invalid channel attenuation value '%S'",reply); + protocolError("invalid channel attenuation value '%s'",reply.c_str()); } return 1/d; } @@ -927,7 +927,7 @@ std::optional MagnovaOscilloscope::parseMetadata( return metadata; } catch (const std::exception& e) { - LogError("Error parsing metadata: %s.\n", e.what()); + protocolError("Error parsing metadata: %s.\n", e.what()); return std::nullopt; } } @@ -944,10 +944,8 @@ size_t MagnovaOscilloscope::ReadWaveformBlock(std::vector* data, std::f //shouldn't ever get here if(i == 19) - { - LogError("ReadWaveformBlock: threw away 20 bytes of data and never saw a '#'\n"); - // This is a protocol error, flush pending rx data - flush(); + { // This is a protocol error, flush pending rx data + protocolErrorWithFlush("ReadWaveformBlock: threw away 20 bytes of data and never saw a '#'\n"); // Stop aqcuisition after this protocol error Stop(); return 0; @@ -972,7 +970,7 @@ size_t MagnovaOscilloscope::ReadWaveformBlock(std::vector* data, std::f if(newBytes == 0) break; readBytes += newBytes; } - LogDebug("Got length %zu from scope, expected bytes = %" PRIu32 ".\n",readBytes, len); + //LogDebug("Got length %zu from scope, expected bytes = %" PRIu32 ".\n",readBytes, len); return readBytes; } @@ -1142,7 +1140,7 @@ vector MagnovaOscilloscope::ProcessAnalogWaveform( auto metadata = parseMetadata(data); if(!metadata) { - LogError("Could not parse metadta"); + LogError("Could not parse metadta.\n"); return ret; } @@ -1156,7 +1154,7 @@ vector MagnovaOscilloscope::ProcessAnalogWaveform( float interval = metadata->timeDelta * FS_PER_SECOND; // Offset is null - double h_off = 0; + //double h_off = 0; double h_off_frac = 0; @@ -1171,14 +1169,14 @@ vector MagnovaOscilloscope::ProcessAnalogWaveform( // float codes_per_div; - LogDebug("\nV_Gain=%f, V_Off=%f, interval=%f, h_off=%f, h_off_frac=%f, datalen=%zu, basetime=%f\n", + /*LogDebug("\nV_Gain=%f, V_Off=%f, interval=%f, h_off=%f, h_off_frac=%f, datalen=%zu, basetime=%f\n", v_gain, v_off, interval, h_off, h_off_frac, datalen, - basetime); + basetime);*/ for(size_t j = 0; j < num_sequences; j++) { @@ -1227,7 +1225,7 @@ vector MagnovaOscilloscope::ProcessDigitalWaveform( auto metadata = parseMetadata(data); if(!metadata) { - LogError("Could not parse metadta"); + LogError("Could not parse metadta.\n"); return ret; } @@ -1581,7 +1579,10 @@ float MagnovaOscilloscope::GetChannelOffset(size_t i, size_t /*stream*/) reply = converse(":CHAN%zu:OFFSET?", i + 1); float offset; - sscanf(reply.c_str(), "%f", &offset); + if(sscanf(reply.c_str(), "%f", &offset) != 1) + { + protocolError("invalid channel offset value '%s'",reply.c_str()); + } lock_guard lock(m_cacheMutex); m_channelOffsets[i] = offset; @@ -1617,7 +1618,10 @@ float MagnovaOscilloscope::GetChannelVoltageRange(size_t i, size_t /*stream*/) reply = converse(":CHAN%zu:SCALE?", i + 1); float volts_per_div; - sscanf(reply.c_str(), "%f", &volts_per_div); + if(sscanf(reply.c_str(), "%f", &volts_per_div)!=1) + { + protocolError("invalid channel vlotage range value '%s'",reply.c_str()); + } float v = volts_per_div * 8; //plot is 8 divisions high lock_guard lock(m_cacheMutex); @@ -1732,14 +1736,14 @@ uint64_t MagnovaOscilloscope::GetSampleRate() string reply; reply = converse(":ACQUIRE:SRATE?"); - if(sscanf(reply.c_str(), "%lf", &f) != EOF) + if(sscanf(reply.c_str(), "%lf", &f) == 1) { m_sampleRate = static_cast(f); m_sampleRateValid = true; } else { - // TODO protocole error + protocolError("invalid sample rate value '%s'",reply.c_str()); } } return m_sampleRate; @@ -1878,7 +1882,10 @@ int64_t MagnovaOscilloscope::GetTriggerOffset() //Result comes back in scientific notation double sec; - sscanf(reply.c_str(), "%le", &sec); + if(sscanf(reply.c_str(), "%le", &sec)!=1) + { + protocolError("invalid trigger offset value '%s'",reply.c_str()); + } m_triggerOffset = static_cast(round(sec * FS_PER_SECOND)); //Convert from midpoint to start point @@ -1939,7 +1946,10 @@ int64_t MagnovaOscilloscope::GetDeskewForChannel(size_t channel) //Value comes back as floating point ps float skew; - sscanf(reply.c_str(), "%f", &skew); + if(sscanf(reply.c_str(), "%f", &skew)!=1) + { + protocolError("invalid channel deskew value '%s'",reply.c_str()); + } int64_t skew_ps = round(skew * FS_PER_SECOND); lock_guard lock2(m_cacheMutex); @@ -2087,7 +2097,10 @@ float MagnovaOscilloscope::GetDigitalThreshold(size_t channel) float result; string reply = converse(":DIG:THRESHOLD%s?", bank.c_str()); - sscanf(reply.c_str(), "%f", &result); + if(sscanf(reply.c_str(), "%f", &result)!=1) + { + protocolError("invalid digital threshold offset value '%s'",reply.c_str()); + } lock_guard lock(m_cacheMutex); m_channelDigitalThresholds[bank] = result; @@ -2146,7 +2159,7 @@ void MagnovaOscilloscope::GetTriggerSlope(Trigger* trig, string reply) if(et) et->SetType(EdgeTrigger::EDGE_ANY); } else - LogWarning("Unknown trigger slope %s\n", reply.c_str()); + protocolError("Unknown trigger slope %s\n", reply.c_str()); } /** @@ -2166,7 +2179,7 @@ Trigger::Condition MagnovaOscilloscope::GetCondition(string reply) return Trigger::CONDITION_NOT_BETWEEN; //unknown - LogWarning("Unknown trigger condition [%s]\n", reply.c_str()); + protocolError("Unknown trigger condition [%s]\n", reply.c_str()); return Trigger::CONDITION_LESS; } @@ -2246,9 +2259,10 @@ void MagnovaOscilloscope::PullTrigger() //Unrecognized trigger type else { - LogWarning("Unknown trigger type \"%s\"\n", reply.c_str()); - m_trigger = NULL; - return; + LogWarning("Unsupported trigger type \"%s\", defaulting to Edge.\n", reply.c_str()); + reply = "EDGe"; + // Default to Edge + PullEdgeTrigger(); } //Pull the source (same for all types of trigger) @@ -2260,13 +2274,16 @@ void MagnovaOscilloscope::PullTrigger() */ void MagnovaOscilloscope::PullTriggerSource(Trigger* trig, string triggerModeName, bool isUart) { - if(isUart) - { // No SCPI command on Magnova to get Trigget Group information for Decode Trigger - return; - } - - string reply = converse(":TRIGGER:%s:SOURCE?", triggerModeName.c_str()); - // Returns CHANnel1 or DIGital1 + string reply; + if(!isUart) + { + reply = converse(":TRIGGER:%s:SOURCE?", triggerModeName.c_str()); + } + else + { // No SCPI command on Magnova to get Trigget Group information for Decode Trigger => default to edge trigger source + reply = converse(":TRIGGER:EDGe:SOURCE?"); + // Returns CHANnel1 or DIGital1 + } // Get channel number int i = reply.size() - 1; @@ -2279,7 +2296,7 @@ void MagnovaOscilloscope::PullTriggerSource(Trigger* trig, string triggerModeNam auto chan = GetOscilloscopeChannelByHwName((isAnalog ? "CH" : "D") + number); trig->SetInput(0, StreamDescriptor(chan, 0), true); if(!chan) - LogWarning("Unknown trigger source \"%s\"\n", reply.c_str()); + protocolError("Unknown trigger source \"%s\"\n", reply.c_str()); } void MagnovaOscilloscope::PushTrigger() @@ -2361,8 +2378,6 @@ void MagnovaOscilloscope::PullDropoutTrigger() m_trigger = new DropoutTrigger(this); DropoutTrigger* dt = dynamic_cast(m_trigger); - Unit fs(Unit::UNIT_FS); - // Check for digital source string reply = converse(":TRIGGER:TIMeout:SOURCE?"); if(reply[0] == 'C') @@ -2525,21 +2540,19 @@ void MagnovaOscilloscope::PullRuntTrigger() m_trigger = new RuntTrigger(this); RuntTrigger* rt = dynamic_cast(m_trigger); - Unit v(Unit::UNIT_VOLTS); - Unit fs(Unit::UNIT_FS); string reply; //Lower bound - rt->SetLowerBound(v.ParseString(converse(":TRIGGER:RUNT:LEVel1?"))); + rt->SetLowerBound(stof(converse(":TRIGGER:RUNT:LEVel1?"))); //Upper bound - rt->SetUpperBound(v.ParseString(converse(":TRIGGER:RUNT:LEVel2?"))); + rt->SetUpperBound(stof(converse(":TRIGGER:RUNT:LEVel2?"))); //Lower bound - rt->SetLowerInterval(fs.ParseString(converse(":TRIGGER:RUNT:DURation:LOWer?"))); + rt->SetLowerInterval(stof(converse(":TRIGGER:RUNT:DURation:LOWer?"))*FS_PER_SECOND); //Upper interval - rt->SetUpperInterval(fs.ParseString(converse(":TRIGGER:RUNT:DURation:UPPer?"))); + rt->SetUpperInterval(stof(converse(":TRIGGER:RUNT:DURation:UPPer?"))*FS_PER_SECOND); //Slope reply = Trim(converse(":TRIGger:RUNT:POLarity?")); @@ -2583,24 +2596,20 @@ void MagnovaOscilloscope::PullSlewRateTrigger() m_trigger = new SlewRateTrigger(this); SlewRateTrigger* st = dynamic_cast(m_trigger); - Unit v(Unit::UNIT_VOLTS); - Unit fs(Unit::UNIT_FS); - string reply ; - //Lower bound - st->SetLowerBound(v.ParseString(converse(":TRIGGER:SLOPe:LEVel1?"))); + st->SetLowerBound(stof(converse(":TRIGGER:SLOPe:LEVel1?"))); //Upper bound - st->SetUpperBound(v.ParseString(converse(":TRIGGER:SLOPe:LEVel2?"))); + st->SetUpperBound(stof(converse(":TRIGGER:SLOPe:LEVel2?"))); //Lower interval - st->SetLowerInterval(fs.ParseString(converse(":TRIGGER:SLOPe:DURation:LOWer?"))); + st->SetLowerInterval(stof(converse(":TRIGGER:SLOPe:DURation:LOWer?")) * FS_PER_SECOND); //Upper interval - st->SetUpperInterval(fs.ParseString(converse(":TRIGGER:SLOPe:DURation:UPPer?"))); + st->SetUpperInterval(stof(converse(":TRIGGER:SLOPe:DURation:UPPer?")) * FS_PER_SECOND); //Slope - reply = Trim(converse(":TRIGger:SLOPe:TYPE?")); + string reply = Trim(converse(":TRIGger:SLOPe:TYPE?")); if(reply == "RISing") st->SetSlope(SlewRateTrigger::EDGE_RISING); else @@ -2686,11 +2695,22 @@ void MagnovaOscilloscope::PullUartTrigger() else // "IFCompletion" (invalid frame completion) / "FPCHeck" (failed parity check) / "IFSTart" (invalid frame start) LogWarning("Unsupported UART trigger condition '%s'", reply.c_str()); + // Check data length + int length = stoi(converse(":TRIGger:DECode:UART:DATA:LENGth?")); + bool ignoreP2 = true; // Data to match (there is no pattern2 on sds) p1 = Trim(converse(":TRIGger:DECode:UART:DATA:WORD0?")); - p2 = Trim(converse(":TRIGger:DECode:UART:DATA:WORD1?")); + if(length >= 2) + { + p2 = Trim(converse(":TRIGger:DECode:UART:DATA:WORD1?")); + ignoreP2 = false; + } + else + { // SetPatterns() needs an patter of at least the same size as p1 + p2 = "XXXXXXXX"; + } // TODO set ignorep2 according to p2 value - ut->SetPatterns(p1, p2, false); + ut->SetPatterns(p1, p2, ignoreP2); } /** @@ -2743,8 +2763,16 @@ void MagnovaOscilloscope::PushUartTrigger(UartTrigger* trig) sendOnly(":TRIGGER:UART:STOP 1.5");*/ //Pattern - p1 = trig->GetPattern1(); - p2 = trig->GetPattern2(); + int dataLength = 1; + trig->SetRadix(SerialTrigger::RADIX_ASCII); + // No public access to unformated Pattern 1 and 2 => use GetParameter() instread since we want the unformated string value + p1 = trig->GetParameter("Pattern").ToString(); + p2 = trig->GetParameter("Pattern 2").ToString(); + if((p2 != "")) + { + dataLength++; + } + LogDebug("Found pattern1 = '%s' and pattern2 = '%s'.\n",p1.c_str(),p2.c_str()); //Match type switch(trig->GetMatchType()) @@ -2763,8 +2791,9 @@ void MagnovaOscilloscope::PushUartTrigger(UartTrigger* trig) LogWarning("Unsupported match type: %d\n",trig->GetMatchType()); break; } + sendOnly(":TRIGger:DECode:UART:DATA:LENGth %d",dataLength); sendOnly(":TRIGger:DECode:UART:DATA:WORD0 %s", p1.c_str()); - sendOnly(":TRIGger:DECode:UART:DATA:WORD2 %s", p2.c_str()); + sendOnly(":TRIGger:DECode:UART:DATA:WORD1 %s", p2.c_str()); } /** @@ -2784,7 +2813,6 @@ void MagnovaOscilloscope::PullWindowTrigger() m_trigger = new WindowTrigger(this); WindowTrigger* wt = dynamic_cast(m_trigger); - Unit v(Unit::UNIT_VOLTS); string type = converse(":TRIGger:WINDow:TYPE?"); if(type == "ENTer") wt->SetWindowType(WindowTrigger::WINDOW_ENTER); @@ -2792,10 +2820,10 @@ void MagnovaOscilloscope::PullWindowTrigger() wt->SetWindowType(WindowTrigger::WINDOW_EXIT); //Lower bound - wt->SetLowerBound(v.ParseString(converse(":TRIGger:WINDow:LEVel1?"))); + wt->SetLowerBound(stof(converse(":TRIGger:WINDow:LEVel1?"))); //Upper bound - wt->SetUpperBound(v.ParseString(converse(":TRIGger:WINDow:LEVel2?"))); + wt->SetUpperBound(stof(converse(":TRIGger:WINDow:LEVel2?"))); } /** @@ -2929,7 +2957,10 @@ float MagnovaOscilloscope::GetFunctionChannelDutyCycle(int chan) lock_guard lock(m_cacheMutex); float dutyf; - sscanf(duty.c_str(), "%f", &dutyf); + if(sscanf(duty.c_str(), "%f", &dutyf)!=1) + { + protocolError("invalid channel ducy cycle value '%s'",duty.c_str()); + } m_awgDutyCycle[chan] = (dutyf/100); return m_awgDutyCycle[chan]; } @@ -2956,7 +2987,10 @@ float MagnovaOscilloscope::GetFunctionChannelAmplitude(int chan) lock_guard lock(m_cacheMutex); float ampf; - sscanf(amp.c_str(), "%f", &f); + if(sscanf(amp.c_str(), "%f", &f)!=1) + { + protocolError("invalid channel amplitude value '%s'",amp.c_str()); + } m_awgRange[chan] = ampf; return m_awgRange[chan]; @@ -2982,7 +3016,10 @@ float MagnovaOscilloscope::GetFunctionChannelOffset(int chan) lock_guard lock(m_cacheMutex); float offsetf; - sscanf(offset.c_str(), "%f", &offsetf); + if(sscanf(offset.c_str(), "%f", &offsetf)!=1) + { + protocolError("invalid channel attenuation value '%s'",offset.c_str()); + } m_awgOffset[chan] = offsetf; return m_awgOffset[chan]; } @@ -3007,7 +3044,10 @@ float MagnovaOscilloscope::GetFunctionChannelFrequency(int chan) lock_guard lock(m_cacheMutex); float freqf; - sscanf(freq.c_str(), "%f", &freqf); + if(sscanf(freq.c_str(), "%f", &freqf)!=1) + { + protocolError("invalid channel frequency value '%s'",freq.c_str()); + } m_awgFrequency[chan] = freqf; return m_awgFrequency[chan]; } @@ -3433,7 +3473,10 @@ float MagnovaOscilloscope::GetFunctionChannelRiseTime(int chan) lock_guard lock(m_cacheMutex); float timef; - sscanf(time.c_str(), "%f", &timef); + if(sscanf(time.c_str(), "%f", &timef)!=1) + { + protocolError("invalid channel rise time value '%s'",time.c_str()); + } m_awgRiseTime[chan] = timef * FS_PER_SECOND; return m_awgRiseTime[chan]; } @@ -3458,7 +3501,10 @@ float MagnovaOscilloscope::GetFunctionChannelFallTime(int chan) lock_guard lock(m_cacheMutex); float timef; - sscanf(time.c_str(), "%f", &timef); + if(sscanf(time.c_str(), "%f", &timef)!=1) + { + protocolError("invalid channel fall time value '%s'",time.c_str()); + } m_awgFallTime[chan] = timef * FS_PER_SECOND; return m_awgFallTime[chan]; } From c2bb92aa28a0185f202bbc1d0e03961e2bfd7942 Mon Sep 17 00:00:00 2001 From: fredzo Date: Mon, 15 Dec 2025 12:22:49 +0100 Subject: [PATCH 24/33] Added more triggers. Added sanity check on analog data length. --- scopehal/MagnovaOscilloscope.cpp | 141 ++++++++++++++++++++++++++++++- scopehal/MagnovaOscilloscope.h | 4 + 2 files changed, 143 insertions(+), 2 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 4fd2a2f0..090c674e 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -42,6 +42,8 @@ #include "SlewRateTrigger.h" #include "UartTrigger.h" #include "WindowTrigger.h" +#include "GlitchTrigger.h" +#include "NthEdgeBurstTrigger.h" #include #include @@ -1161,6 +1163,14 @@ vector MagnovaOscilloscope::ProcessAnalogWaveform( //Raw waveform data size_t num_samples = metadata->sampleCount; + // Sanity check datalength consistency + size_t actual_num_samples = (datalen-32)/2; + if(num_samples != actual_num_samples) + { + protocolError("Invlaid sample count from metadata: found %zu, expected %zu.\n",num_samples,actual_num_samples); + num_samples = min(num_samples,actual_num_samples); + } + size_t num_per_segment = num_samples / num_sequences; // Skip metadata @@ -2136,6 +2146,7 @@ void MagnovaOscilloscope::GetTriggerSlope(Trigger* trig, string reply) { auto dt = dynamic_cast(trig); auto et = dynamic_cast(trig); + auto bt = dynamic_cast(m_trigger); reply = Trim(reply); @@ -2143,11 +2154,13 @@ void MagnovaOscilloscope::GetTriggerSlope(Trigger* trig, string reply) { if(dt) dt->SetType(DropoutTrigger::EDGE_RISING); if(et) et->SetType(EdgeTrigger::EDGE_RISING); + if(bt) bt->SetSlope(NthEdgeBurstTrigger::EDGE_RISING); } else if(reply == "FALLing") { if(dt) dt->SetType(DropoutTrigger::EDGE_FALLING); if(et) et->SetType(EdgeTrigger::EDGE_FALLING); + if(bt) bt->SetSlope(NthEdgeBurstTrigger::EDGE_FALLING); } else if(reply == "ALTernate") { @@ -2227,6 +2240,8 @@ vector MagnovaOscilloscope::GetTriggerTypes() ret.push_back(SlewRateTrigger::GetTriggerName()); ret.push_back(UartTrigger::GetTriggerName()); ret.push_back(WindowTrigger::GetTriggerName()); + ret.push_back(GlitchTrigger::GetTriggerName()); + ret.push_back(NthEdgeBurstTrigger::GetTriggerName()); // TODO: Add missing triggers (NEDGe, DELay, INTerval, SHOLd, PATTern + Decode-SPI/I2C/Parallel) return ret; } @@ -2255,6 +2270,10 @@ void MagnovaOscilloscope::PullTrigger() PullPulseWidthTrigger(); else if(reply == "WINDow") PullWindowTrigger(); + else if(reply == "INTerval") + PullGlitchTrigger(); + else if(reply == "NEDGe") + PullNthEdgeBurstTrigger(); // Note that NEDGe, DELay, INTerval, SHOLd, PATTern + Decode-SPI/I2C/Parallel are not yet handled //Unrecognized trigger type else @@ -2308,6 +2327,8 @@ void MagnovaOscilloscope::PushTrigger() auto st = dynamic_cast(m_trigger); auto ut = dynamic_cast(m_trigger); auto wt = dynamic_cast(m_trigger); + auto gt = dynamic_cast(m_trigger); + auto bt = dynamic_cast(m_trigger); if(dt) { @@ -2347,8 +2368,19 @@ void MagnovaOscilloscope::PushTrigger() sendOnly(":TRIGGER:WINDow:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); PushWindowTrigger(wt); } + else if(gt) + { + sendOnly(":TRIGGER:TYPE INTerval"); + sendOnly(":TRIGGER:INTerval:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + PushGlitchTrigger(gt); + } + else if(bt) + { + sendOnly(":TRIGGER:TYPE NEDGe"); + sendOnly(":TRIGGER:NEDGe:SOURCE %s", GetChannelName(m_trigger->GetInput(0).m_channel->GetIndex()).c_str()); + PushNthEdgeBurstTrigger(bt); + } - // TODO: Add missing triggers else if(et) //must be last { @@ -2800,7 +2832,7 @@ void MagnovaOscilloscope::PushUartTrigger(UartTrigger* trig) @brief Reads settings for a window trigger from the instrument */ void MagnovaOscilloscope::PullWindowTrigger() -{ // TODO +{ //Clear out any triggers of the wrong type if((m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL)) { @@ -2847,6 +2879,111 @@ void MagnovaOscilloscope::PushWindowTrigger(WindowTrigger* trig) PushFloat(":TRIGger:WINDow:LEVel2", trig->GetUpperBound()); } +/** + @brief Reads settings for a glitch trigger from the instrument + */ +void MagnovaOscilloscope::PullGlitchTrigger() +{ + //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 GlitchTrigger(this); + GlitchTrigger* gt = dynamic_cast(m_trigger); + + //Level + // Check for digital source + string reply = converse(":TRIGGER:INTerval:SOURCE?"); + if(reply[0] == 'C') + { // Level only for analog source + gt->SetLevel(stof(converse(":TRIGGER:INTerval:LEVEL?"))); + } + + //Slope + reply = Trim(converse(":TRIGGER:INTerval:POLarity?")); + if(reply == "POSitive") + gt->SetType(GlitchTrigger::EDGE_RISING); + else if(reply == "NEGative") + gt->SetType(GlitchTrigger::EDGE_FALLING); + + //Condition + gt->SetCondition(GetCondition(converse(":TRIGGER:INTerval:TIMing?"))); + + //Lower bound + gt->SetLowerBound(stof(converse(":TRIGger:INTerval:DURation:LOWer?"))*FS_PER_SECOND); + + //Upper interval + gt->SetUpperBound(stof(converse(":TRIGger:INTerval:DURation:UPPer?"))*FS_PER_SECOND); +} + +/** + @brief Pushes settings for a glitch trigger to the instrument + */ +void MagnovaOscilloscope::PushGlitchTrigger(GlitchTrigger* trig) +{ + PushFloat(":TRIGGER:INTerval:LEVEL", trig->GetLevel()); + sendOnly(":TRIGger:INTerval:POLarity %s", (trig->GetType() != GlitchTrigger::EDGE_FALLING) ? "POSitive" : "NEGative"); + PushCondition(":TRIGGER:INTerval:TIMing", trig->GetCondition()); + PushFloat(":TRIGGER:INTerval:DURation:LOWer", trig->GetLowerBound() * SECONDS_PER_FS); + PushFloat(":TRIGGER:INTerval:DURation:UPPer", trig->GetUpperBound() * SECONDS_PER_FS); +} + + +/** + @brief Reads settings for an Nth-edge-burst trigger from the instrument + */ +void MagnovaOscilloscope::PullNthEdgeBurstTrigger() +{ + //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 NthEdgeBurstTrigger(this); + auto bt = dynamic_cast(m_trigger); + + //Level + // Check for digital source + string reply = converse(":TRIGGER:NEDGe:SOURCE?"); + if(reply[0] == 'C') + { // Level only for analog source + bt->SetLevel(stof(converse(":TRIGGER:NEDGe:LEVEL?"))); + } + + //Slope + GetTriggerSlope(bt,converse(":TRIGger:NEDGe:SLOPe?")); + + //Idle time + bt->SetIdleTime(stof(converse(":TRIGger:NEDGe:IDLE?"))*FS_PER_SECOND); + + //Edge number + bt->SetEdgeNumber(stoi(converse(":TRIGger:NEDGe:COUNt?"))); +} + +/** + @brief Pushes settings for a Nth edge burst trigger to the instrument + + @param trig The trigger + */ +void MagnovaOscilloscope::PushNthEdgeBurstTrigger(NthEdgeBurstTrigger* trig) +{ + PushFloat(":TRIGGER:NEDGe:LEVEL", trig->GetLevel()); + sendOnly(":TRIGger:NEDGe:SLOPE %s", (trig->GetSlope() != NthEdgeBurstTrigger::EDGE_FALLING) ? "RISing" : "FALLing"); + PushFloat(":TRIGger:NEDGe:IDLE", trig->GetIdleTime() * SECONDS_PER_FS); + sendOnly(":TRIGger:NEDGe:COUNt %" PRIu64 "", trig->GetEdgeNumber()); +} + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Function generator mode diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h index e24ff2a8..93154405 100644 --- a/scopehal/MagnovaOscilloscope.h +++ b/scopehal/MagnovaOscilloscope.h @@ -32,6 +32,7 @@ #include #include +#include "NthEdgeBurstTrigger.h" class DropoutTrigger; class EdgeTrigger; @@ -251,6 +252,8 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP void PullSlewRateTrigger(); void PullUartTrigger(); void PullWindowTrigger(); + void PullGlitchTrigger(); + void PullNthEdgeBurstTrigger(); void PullTriggerSource(Trigger* trig, std::string triggerModeName, bool isUart); void GetTriggerSlope(Trigger* trig, std::string reply); @@ -267,6 +270,7 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP void PushSlewRateTrigger(SlewRateTrigger* trig); void PushUartTrigger(UartTrigger* trig); void PushWindowTrigger(WindowTrigger* trig); + void PushNthEdgeBurstTrigger(NthEdgeBurstTrigger* trig); void BulkCheckChannelEnableState(); From 84cb71d0827af772781c0b76cb1ab589dac8c5d7 Mon Sep 17 00:00:00 2001 From: fredzo Date: Mon, 15 Dec 2025 18:08:21 +0100 Subject: [PATCH 25/33] Fixed comments --- scopehal/MagnovaOscilloscope.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 090c674e..5b6c60f2 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -2242,7 +2242,7 @@ vector MagnovaOscilloscope::GetTriggerTypes() ret.push_back(WindowTrigger::GetTriggerName()); ret.push_back(GlitchTrigger::GetTriggerName()); ret.push_back(NthEdgeBurstTrigger::GetTriggerName()); - // TODO: Add missing triggers (NEDGe, DELay, INTerval, SHOLd, PATTern + Decode-SPI/I2C/Parallel) + // TODO: Add missing triggers (DELay, SHOLd, PATTern + Decode-SPI/I2C/Parallel) return ret; } @@ -2730,7 +2730,7 @@ void MagnovaOscilloscope::PullUartTrigger() // Check data length int length = stoi(converse(":TRIGger:DECode:UART:DATA:LENGth?")); bool ignoreP2 = true; - // Data to match (there is no pattern2 on sds) + // Data to match p1 = Trim(converse(":TRIGger:DECode:UART:DATA:WORD0?")); if(length >= 2) { @@ -2741,7 +2741,6 @@ void MagnovaOscilloscope::PullUartTrigger() { // SetPatterns() needs an patter of at least the same size as p1 p2 = "XXXXXXXX"; } - // TODO set ignorep2 according to p2 value ut->SetPatterns(p1, p2, ignoreP2); } @@ -3230,8 +3229,6 @@ FunctionGenerator::WaveShape MagnovaOscilloscope::GetFunctionChannelShape(int ch m_awgShape[chan] = FunctionGenerator::SHAPE_DC; else if(shape == "PRBS") { - //TODO: LENGTH if type is PRBS? - //Might only be supported on SDGs m_awgShape[chan] = FunctionGenerator::SHAPE_PRBS_NONSTANDARD; } else if(shape == "ARBitrary") From 7799929aff2c983102da0720f967b43ed17f2885 Mon Sep 17 00:00:00 2001 From: fredzo Date: Wed, 17 Dec 2025 00:12:38 +0100 Subject: [PATCH 26/33] Fixed potential deand locks. Added missing srate value. --- scopehal/MagnovaOscilloscope.cpp | 190 +++++++++++++++++-------------- 1 file changed, 103 insertions(+), 87 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 5b6c60f2..6c7ddfa8 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -1006,14 +1006,17 @@ void MagnovaOscilloscope::BulkCheckChannelEnableState() } for(auto i : uncached) { + bool enabled; if((i < m_analogChannelCount)) { // Analog - m_channelsEnabled[i] = (converse(":CHAN%zu:STAT?", i + 1) == "ON"); + enabled = (converse(":CHAN%zu:STAT?", i + 1) == "ON"); } else { // Digital - m_channelsEnabled[i] = digitalModuleOn && (converse(":DIG%zu:STAT?", (i - m_analogChannelCount)) == "ON"); + enabled = digitalModuleOn && (converse(":DIG%zu:STAT?", (i - m_analogChannelCount)) == "ON"); } + lock_guard lock(m_cacheMutex); + m_channelsEnabled[i] = enabled; } } @@ -1338,7 +1341,7 @@ bool MagnovaOscilloscope::AcquireData() //State for this acquisition (may be more than one waveform) uint32_t num_sequences = 1; map> pending_waveforms; - double start = GetTime(); + double start = 0; time_t ttime = 0; double basetime = 0; vector> waveforms; @@ -1350,9 +1353,6 @@ bool MagnovaOscilloscope::AcquireData() //Acquire the data (but don't parse it) - lock_guard lock(m_transport->GetMutex()); - start = GetTime(); - // Get instrument time : format "23,35,11.280010" string isntrumentTime = converse(":SYST:TIME?"); @@ -1362,7 +1362,6 @@ bool MagnovaOscilloscope::AcquireData() { // Check all analog channels analogEnabled[i] = IsChannelEnabled(i); } - for(unsigned int i = 0; i < m_digitalChannelCount; i++) { // Check digital channels // Not supported for now by Magnova firmware @@ -1374,51 +1373,57 @@ bool MagnovaOscilloscope::AcquireData() // Notify about download operation start ChannelsDownloadStarted(); - // Get time from instrument - ttime = ExtractTimestamp(isntrumentTime, basetime); + + { // Lock transport from now during all acquisition phase + lock_guard lock(m_transport->GetMutex()); + start = GetTime(); - //Read the data from each analog waveform - for(unsigned int i = 0; i < m_analogChannelCount; i++) - { - if(analogEnabled[i]) - { // Allocate buffer - // Run the same loop for paginated and unpagnated mode, if unpaginated we will run it only once - m_transport->SendCommand(":CHAN" + to_string(i + 1) + ":DATA:PACK? ALL,RAW"); - size_t readBytes = ReadWaveformBlock(&analogWaveformData[i], [i, this] (float progress) { ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, progress); }); - analogWaveformDataSize[i] = readBytes; - ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_FINISHED, 1.0); - } - } - if(anyDigitalEnabled) - { - // uint64_t digitalAcqPoints = GetDigitalAcqPoints(); - // uint64_t acqDigitalBytes = ceil(digitalAcqPoints/8); // 8 points per byte on digital channels - // LogDebug("Digital acq : ratio = %lld, pages = %lld, page size = %lld , dig acq points = %lld, acq dig bytes = %lld.\n",(acqPoints / digitalAcqPoints),pages, pageSize,digitalAcqPoints, acqDigitalBytes); - //Read the data from each digital waveform - for(size_t i = 0; i < m_digitalChannelCount; i++) + // Get time from instrument + ttime = ExtractTimestamp(isntrumentTime, basetime); + + //Read the data from each analog waveform + for(unsigned int i = 0; i < m_analogChannelCount; i++) { - if(digitalEnabled[i]) + if(analogEnabled[i]) { // Allocate buffer - m_transport->SendCommand(":DIG" + to_string(i + 1) + ":DATA:PACK? ALL,RAW"); - size_t readBytes = ReadWaveformBlock(&digitalWaveformDataBytes[i], [i, this] (float progress) { ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, progress); }); - digitalWaveformDataSize[i] = readBytes; - ChannelsDownloadStatusUpdate(i + m_analogChannelCount, InstrumentChannel::DownloadState::DOWNLOAD_FINISHED, 1.0); + // Run the same loop for paginated and unpagnated mode, if unpaginated we will run it only once + m_transport->SendCommand(":CHAN" + to_string(i + 1) + ":DATA:PACK? ALL,RAW"); + size_t readBytes = ReadWaveformBlock(&analogWaveformData[i], [i, this] (float progress) { ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, progress); }); + analogWaveformDataSize[i] = readBytes; + ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_FINISHED, 1.0); + } + } + if(anyDigitalEnabled) + { + // uint64_t digitalAcqPoints = GetDigitalAcqPoints(); + // uint64_t acqDigitalBytes = ceil(digitalAcqPoints/8); // 8 points per byte on digital channels + // LogDebug("Digital acq : ratio = %lld, pages = %lld, page size = %lld , dig acq points = %lld, acq dig bytes = %lld.\n",(acqPoints / digitalAcqPoints),pages, pageSize,digitalAcqPoints, acqDigitalBytes); + //Read the data from each digital waveform + for(size_t i = 0; i < m_digitalChannelCount; i++) + { + if(digitalEnabled[i]) + { // Allocate buffer + m_transport->SendCommand(":DIG" + to_string(i + 1) + ":DATA:PACK? ALL,RAW"); + size_t readBytes = ReadWaveformBlock(&digitalWaveformDataBytes[i], [i, this] (float progress) { ChannelsDownloadStatusUpdate(i, InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS, progress); }); + digitalWaveformDataSize[i] = readBytes; + ChannelsDownloadStatusUpdate(i + m_analogChannelCount, InstrumentChannel::DownloadState::DOWNLOAD_FINISHED, 1.0); + } } } - } - //At this point all data has been read so the scope is free to go do its thing while we crunch the results. - //Re-arm the trigger if not in one-shot mode - if(!m_triggerOneShot) - { - //LogDebug("Arming trigger for next acquisition!\n"); - sendOnly(":SINGLE"); - m_triggerArmed = true; - } - else - { - sendWithAck(":STOP"); - m_triggerArmed = false; + //At this point all data has been read so the scope is free to go do its thing while we crunch the results. + //Re-arm the trigger if not in one-shot mode + if(!m_triggerOneShot) + { + //LogDebug("Arming trigger for next acquisition!\n"); + sendOnly(":SINGLE"); + m_triggerArmed = true; + } + else + { + sendWithAck(":STOP"); + m_triggerArmed = false; + } } //Process analog waveforms @@ -1489,19 +1494,20 @@ bool MagnovaOscilloscope::AcquireData() digitalWaveformDataBytes[i] = {}; } - //Now that we have all of the pending waveforms, save them in sets across all channels - m_pendingWaveformsMutex.lock(); - for(size_t i = 0; i < num_sequences; i++) - { - SequenceSet s; - for(size_t j = 0; j < m_analogAndDigitalChannelCount; j++) + + { //Now that we have all of the pending waveforms, save them in sets across all channels + lock_guard lock(m_pendingWaveformsMutex); + for(size_t i = 0; i < num_sequences; i++) { - if(pending_waveforms.find(j) != pending_waveforms.end()) - s[GetOscilloscopeChannel(j)] = pending_waveforms[j][i]; + SequenceSet s; + for(size_t j = 0; j < m_analogAndDigitalChannelCount; j++) + { + if(pending_waveforms.find(j) != pending_waveforms.end()) + s[GetOscilloscopeChannel(j)] = pending_waveforms[j][i]; + } + m_pendingWaveforms.push_back(s); } - m_pendingWaveforms.push_back(s); } - m_pendingWaveformsMutex.unlock(); double dt = GetTime() - start; LogTrace("Waveform download and processing took %.3f ms\n", dt * 1000); @@ -1665,7 +1671,7 @@ vector MagnovaOscilloscope::GetSampleRatesNonInterleaved() { // -------------------------------------------------- case MODEL_MAGNOVA_BMO: - ret = {1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 1*k, 2*k, 4*k, 5*k, 10*k, 20*k, 40*k, 50*k, 100*k, 200*k, 400*k, 500*k, 1*m, 2*m, 4*m, 5*m, 10*m, 20*m, 40*m, 50*m, 100*m, 200*m, 400*m, 500*m, 800*m, 1600*m }; + ret = {1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 1*k, 2*k, 4*k, 5*k, 10*k, 20*k, 40*k, 50*k, 100*k, 200*k, 400*k, 500*k, 1*m, 2*m, 4*m, 5*m, 10*m, 20*m, 40*m, 50*m, 125*m, 100*m, 200*m, 400*m, 500*m, 800*m, 1000*m, 1600*m }; break; // -------------------------------------------------- default: @@ -1740,43 +1746,49 @@ set MagnovaOscilloscope::GetInterleaveC uint64_t MagnovaOscilloscope::GetSampleRate() { + { + lock_guard lock(m_cacheMutex); + if(m_sampleRateValid) + return m_sampleRate; + } double f; - if(!m_sampleRateValid) - { - string reply; - reply = converse(":ACQUIRE:SRATE?"); + string reply; + reply = converse(":ACQUIRE:SRATE?"); - if(sscanf(reply.c_str(), "%lf", &f) == 1) - { - m_sampleRate = static_cast(f); - m_sampleRateValid = true; - } - else - { - protocolError("invalid sample rate value '%s'",reply.c_str()); - } + lock_guard lock(m_cacheMutex); + if(sscanf(reply.c_str(), "%lf", &f) == 1) + { + m_sampleRate = static_cast(f); + m_sampleRateValid = true; + } + else + { + protocolError("invalid sample rate value '%s'",reply.c_str()); } return m_sampleRate; } uint64_t MagnovaOscilloscope::GetSampleDepth() { + { + lock_guard lock(m_cacheMutex); + if(m_memoryDepthValid) + return m_memoryDepth; + } double f; - if(!m_memoryDepthValid) - { // Possible values are : AUTo, AFASt, Integer in pts - string reply = converse(":ACQUIRE:MDEPTH?"); - if(reply == "AUTo" || reply == "AFASt") - { // Default to 10Ms - m_memoryDepth = 10000000; - } - else - { - f = Unit(Unit::UNIT_SAMPLEDEPTH).ParseString(reply); - m_memoryDepth = static_cast(f); - } - lock_guard lock2(m_cacheMutex); - m_memoryDepthValid = true; + // Possible values are : AUTo, AFASt, Integer in pts + string reply = converse(":ACQUIRE:MDEPTH?"); + lock_guard lock2(m_cacheMutex); + if(reply == "AUTo" || reply == "AFASt") + { // Default to 10Ms + m_memoryDepth = 10000000; + } + else + { + f = Unit(Unit::UNIT_SAMPLEDEPTH).ParseString(reply); + m_memoryDepth = static_cast(f); } + m_memoryDepthValid = true; return m_memoryDepth; } @@ -1888,20 +1900,23 @@ int64_t MagnovaOscilloscope::GetTriggerOffset() string reply; reply = converse(":TIMebase:OFFSet?"); - lock_guard lock(m_cacheMutex); - //Result comes back in scientific notation double sec; if(sscanf(reply.c_str(), "%le", &sec)!=1) { protocolError("invalid trigger offset value '%s'",reply.c_str()); } - m_triggerOffset = static_cast(round(sec * FS_PER_SECOND)); + { + lock_guard lock(m_cacheMutex); + m_triggerOffset = static_cast(round(sec * 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)); + + lock_guard lock(m_cacheMutex); m_triggerOffset += halfwidth; //LogDebug("Get trigger offset to %lf : rate = %" PRId64 ", depth = %" PRId64 " haldepth = %" PRId64 ", halfwidth = %" PRId64 ", result = %" PRId64 ".\n",sec,rate,GetSampleDepth(),halfdepth,halfwidth,m_triggerOffset); @@ -2929,6 +2944,7 @@ void MagnovaOscilloscope::PushGlitchTrigger(GlitchTrigger* trig) sendOnly(":TRIGger:INTerval:POLarity %s", (trig->GetType() != GlitchTrigger::EDGE_FALLING) ? "POSitive" : "NEGative"); PushCondition(":TRIGGER:INTerval:TIMing", trig->GetCondition()); PushFloat(":TRIGGER:INTerval:DURation:LOWer", trig->GetLowerBound() * SECONDS_PER_FS); + //LogError("Parameter Upper Bound = %s / %lld / %f\n",trig->GetParameter("Upper Bound").ToString().c_str(),trig->GetParameter("Upper Bound").GetIntVal(),trig->GetParameter("Upper Bound").GetIntVal() * SECONDS_PER_FS); PushFloat(":TRIGGER:INTerval:DURation:UPPer", trig->GetUpperBound() * SECONDS_PER_FS); } From d059fbe34bbe1b718b5ec500b33ee05103e9cd0d Mon Sep 17 00:00:00 2001 From: fredzo Date: Wed, 17 Dec 2025 10:03:24 +0100 Subject: [PATCH 27/33] Fixed Glitch Trigger. --- scopehal/GlitchTrigger.h | 6 +++--- scopehal/MagnovaOscilloscope.cpp | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/scopehal/GlitchTrigger.h b/scopehal/GlitchTrigger.h index 7fa59c4d..c515b78d 100644 --- a/scopehal/GlitchTrigger.h +++ b/scopehal/GlitchTrigger.h @@ -92,13 +92,13 @@ class GlitchTrigger : public EdgeTrigger protected: ///@brief Condition to look for - FilterParameter m_condition; + FilterParameter& m_condition; ///@brief Lower voltage level for glitch detector - FilterParameter m_lowerBound; + FilterParameter& m_lowerBound; ///@brief Upper voltage level for glitch detector - FilterParameter m_upperBound; + FilterParameter& m_upperBound; }; #endif diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 6c7ddfa8..bbbddbb7 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -2944,7 +2944,6 @@ void MagnovaOscilloscope::PushGlitchTrigger(GlitchTrigger* trig) sendOnly(":TRIGger:INTerval:POLarity %s", (trig->GetType() != GlitchTrigger::EDGE_FALLING) ? "POSitive" : "NEGative"); PushCondition(":TRIGGER:INTerval:TIMing", trig->GetCondition()); PushFloat(":TRIGGER:INTerval:DURation:LOWer", trig->GetLowerBound() * SECONDS_PER_FS); - //LogError("Parameter Upper Bound = %s / %lld / %f\n",trig->GetParameter("Upper Bound").ToString().c_str(),trig->GetParameter("Upper Bound").GetIntVal(),trig->GetParameter("Upper Bound").GetIntVal() * SECONDS_PER_FS); PushFloat(":TRIGGER:INTerval:DURation:UPPer", trig->GetUpperBound() * SECONDS_PER_FS); } From deb9dbc0a45a399a90892ec9ead0314780c9a3e4 Mon Sep 17 00:00:00 2001 From: fredzo Date: Thu, 18 Dec 2025 17:32:39 +0100 Subject: [PATCH 28/33] Fixed srate and mdepth value lists according to specifications provided by Batronix. Added workaround to get mdepth from srate and timebase when set to Auto/Afast. --- scopehal/MagnovaOscilloscope.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index bbbddbb7..e7f8dd07 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -1671,7 +1671,7 @@ vector MagnovaOscilloscope::GetSampleRatesNonInterleaved() { // -------------------------------------------------- case MODEL_MAGNOVA_BMO: - ret = {1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 1*k, 2*k, 4*k, 5*k, 10*k, 20*k, 40*k, 50*k, 100*k, 200*k, 400*k, 500*k, 1*m, 2*m, 4*m, 5*m, 10*m, 20*m, 40*m, 50*m, 125*m, 100*m, 200*m, 400*m, 500*m, 800*m, 1000*m, 1600*m }; + ret = {1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 1*k, 2*k, 4*k, 5*k, 10*k, 20*k, 40*k, 50*k, 100*k, 200*k, 400*k, 500*k, 1*m, 2*m, 4*m, 5*m, 10*m, 20*m, 40*m, 50*m, 125*m, 100*m, 125*m, 200*m, 250*m, 400*m, 500*m, 800*m, 1000*m, 1600*m }; break; // -------------------------------------------------- default: @@ -1699,7 +1699,7 @@ vector MagnovaOscilloscope::GetSampleDepthsNonInterleaved() unsigned int activeChannels = GetActiveChannelsCount(); if(activeChannels <= 1) { - ret = {20 * 1000, 50 * 1000, 100 * 1000, 200 * 1000, 500 * 1000, 1000 * 1000, 2000 * 1000, 5000 * 1000, 10 * 1000 * 1000, 20 * 1000 * 1000, 50 * 1000 * 1000, 200 * 1000 * 1000, 327151616}; + ret = {20 * 1000, 50 * 1000, 100 * 1000, 200 * 1000, 500 * 1000, 1000 * 1000, 2000 * 1000, 5000 * 1000, 10 * 1000 * 1000, 20 * 1000 * 1000, 50 * 1000 * 1000, 100 * 1000 * 1000, 200 * 1000 * 1000, 327151616}; } else if(activeChannels == 2) { @@ -1780,8 +1780,17 @@ uint64_t MagnovaOscilloscope::GetSampleDepth() string reply = converse(":ACQUIRE:MDEPTH?"); lock_guard lock2(m_cacheMutex); if(reply == "AUTo" || reply == "AFASt") - { // Default to 10Ms - m_memoryDepth = 10000000; + { // TODO :Get Sample depth based on srate and timebase + reply = converse(":TIMebase:SCALe?"); + double scale = stof(reply); + if(scale > 0) + { + m_memoryDepth = scale * 25 * GetSampleRate(); + } + else + { + m_memoryDepth = 10000000; + } } else { @@ -1822,7 +1831,7 @@ void MagnovaOscilloscope::SetSampleRate(uint64_t rate) lock_guard lock(m_transport->GetMutex()); double sampletime = GetSampleDepth() / (double)rate; - double scale = sampletime / 25; + double scale = sampletime / 25; // TODO: check that should be 12 or 24 (when in extended capture rate) ? switch(m_modelid) { From ff81f7fc2385086e1c991d418070158c2a002e62 Mon Sep 17 00:00:00 2001 From: fredzo Date: Sun, 21 Dec 2025 22:33:41 +0100 Subject: [PATCH 29/33] Added support for auto memory modes. --- scopehal/MagnovaOscilloscope.cpp | 254 +++++++++++++++++++++++++------ scopehal/MagnovaOscilloscope.h | 18 +++ 2 files changed, 223 insertions(+), 49 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index e7f8dd07..06e7f465 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -75,6 +75,8 @@ MagnovaOscilloscope::MagnovaOscilloscope(SCPITransport* transport) , m_sampleRate(1) , m_memoryDepthValid(false) , m_memoryDepth(1) + , m_timebaseScaleValid(false) + , m_timebaseScale(1) , m_triggerOffsetValid(false) , m_triggerOffset(0) { @@ -385,6 +387,7 @@ void MagnovaOscilloscope::FlushConfigCache() m_probeIsActive.clear(); m_sampleRateValid = false; m_memoryDepthValid = false; + m_timebaseScaleValid = false; m_triggerOffsetValid = false; m_meterModeValid = false; m_awgEnabled.clear(); @@ -513,6 +516,7 @@ void MagnovaOscilloscope::EnableChannel(size_t i) if(IsInterleaving() != wasInterleaving) { m_memoryDepthValid = false; + m_timebaseScaleValid = false; m_sampleRateValid = false; m_triggerOffsetValid = false; } @@ -553,6 +557,7 @@ void MagnovaOscilloscope::DisableChannel(size_t i) { lock_guard lock2(m_cacheMutex); m_memoryDepthValid = false; + m_timebaseScaleValid = false; m_sampleRateValid = false; m_triggerOffsetValid = false; } @@ -1047,6 +1052,64 @@ unsigned int MagnovaOscilloscope::GetActiveChannelsCount() return result; } +/** + @brief Returns true if the scope is in reduced sample rate + */ +bool MagnovaOscilloscope::IsReducedSampleRate() +{ + // ADC sample rate 1.6 GSa/s if + // - only channel 1 and/or 2 are active + // - only channel 1 or 2 and one digital probe are active + // - only one or two digital probes are active + // - only channel 1 and/or 2 are active plus one or two digital probes are active and time scale is ≤ 20 ns/div + + // ADC sample rate 1.0 GSa/s if + // - Channel 3 and/or 4 are active + // - The number of analog channels plus digital probes is 3 or more and time scale is > 20 ns/div. + + unsigned int activeChannels = GetActiveChannelsCount(); + if(IsChannelEnabled(2) || IsChannelEnabled(3)) + { // Reduced if channel 3 or 4 is active + return true; + } + else if(activeChannels >= 3) + { // Need to checm time scale + uint64_t nsPerDiv = llround(GetTimebaseScale()*FS_PER_SECOND)/FS_PER_NANOSECOND; + return nsPerDiv > 20; + } + return false; +} + + +/** + @brief Returns the max memory depth for auto mode + */ +uint64_t MagnovaOscilloscope::GetMaxAutoMemoryDepth() +{ + if(m_memodyDepthMode == MEMORY_DEPTH_AUTO_FAST) + { // In fast mode, depth is limited to 20 Mpts + return 20*1000*1000; + } + unsigned int activeChannels = GetActiveChannelsCount(); + if(activeChannels <= 1) + { + return 300*1000*1000; + } + else if(activeChannels == 2) + { + return 150*1000*1000; + } + else if(activeChannels == 3 || activeChannels == 4) + { + return 60*1000*1000; + } + else + { + return 30*1000*1000; + } +} + + time_t MagnovaOscilloscope::ExtractTimestamp(const std::string& timeString, double& basetime) { @@ -1519,6 +1582,7 @@ void MagnovaOscilloscope::PrepareAcquisition() lock_guard lock2(m_cacheMutex); m_sampleRateValid = false; m_memoryDepthValid = false; + m_timebaseScaleValid = false; m_triggerOffsetValid = false; m_channelOffsets.clear(); } @@ -1671,7 +1735,34 @@ vector MagnovaOscilloscope::GetSampleRatesNonInterleaved() { // -------------------------------------------------- case MODEL_MAGNOVA_BMO: - ret = {1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 1*k, 2*k, 4*k, 5*k, 10*k, 20*k, 40*k, 50*k, 100*k, 200*k, 400*k, 500*k, 1*m, 2*m, 4*m, 5*m, 10*m, 20*m, 40*m, 50*m, 125*m, 100*m, 125*m, 200*m, 250*m, 400*m, 500*m, 800*m, 1000*m, 1600*m }; + // Call GetSampleDepth to update Memody Depth Mode + GetSampleDepth(); + if(m_memodyDepthMode == MEMORY_DEPTH_AUTO_MAX) + { // In auto modes reduce possible values to the one that match sample depth / coarse time scale + if(IsReducedSampleRate()) + { + ret = {25, 50, 100, 250, 500, 1*k, 2500, 5*k, 10*k, 25*k, 50*k, 100*k, 250*k, 500*k, 1*m, 2500*k, 5*m, 10*m, 25*m, 50*m, 125*m, 250*m, 500*m, 1000*m }; + } + else + { + ret = {50, 100, 250, 500, 1*k, 2500, 5*k, 10*k, 25*k, 50*k, 100*k, 250*k, 500*k, 1*m, 2500*k, 5*m, 10*m, 25*m, 50*m, 100*m, 200*m, 400*m, 800*m, 1600*m }; + } + } + else if(m_memodyDepthMode == MEMORY_DEPTH_AUTO_FAST) + { // In auto modes reduce possible values to the one that match sample depth / coarse time scale + if(IsReducedSampleRate()) + { + ret = {2, 5, 10, 40, 50, 100, 400, 500, 1*k, 4*k, 5*k, 10*k, 40*k, 50*k, 100*k, 400*k, 500*k, 1*m, 2500*k, 5*m, 10*m, 25*m, 50*m, 125*m, 250*m, 500*m, 1000*m }; + } + else + { + ret = {2, 5, 10, 40, 50, 100, 400, 500, 1*k, 4*k, 5*k, 10*k, 40*k, 50*k, 100*k, 400*k, 500*k, 1*m, 4*m, 5*m, 10*m, 40*m, 50*m, 100*m, 400*m, 800*m, 1600*m }; + } + } + else + { // All possible values + ret = {1, 2, 4, 5, 10, 20, 25, 40, 50, 100, 200, 250, 400, 500, 1*k, 2*k, 2500, 4*k, 5*k, 10*k, 20*k, 25*k, 40*k, 50*k, 100*k, 200*k, 250*k, 400*k, 500*k, 1*m, 2*m, 2500*k, 4*m, 5*m, 10*m, 20*m, 25*m, 40*m, 50*m, 100*m, 125*m, 200*m, 250*m, 400*m, 500*m, 800*m, 1000*m, 1600*m }; + } break; // -------------------------------------------------- default: @@ -1691,27 +1782,49 @@ vector MagnovaOscilloscope::GetSampleRatesInterleaved() vector MagnovaOscilloscope::GetSampleDepthsNonInterleaved() { vector ret; - // Sample depths depend on the number of active analog channels and digital probes : - // 1 analog channel or digital probe: 327.2 Mpts - // 2 analog channels / digital probes: 163.6 Mpts per channel - // 3-4 analog channels / digital probes: 81.8 Mpts per channel - // ≥ 5 analog channels / digital probes: 40.9 Mpts per channel - unsigned int activeChannels = GetActiveChannelsCount(); - if(activeChannels <= 1) - { - ret = {20 * 1000, 50 * 1000, 100 * 1000, 200 * 1000, 500 * 1000, 1000 * 1000, 2000 * 1000, 5000 * 1000, 10 * 1000 * 1000, 20 * 1000 * 1000, 50 * 1000 * 1000, 100 * 1000 * 1000, 200 * 1000 * 1000, 327151616}; - } - else if(activeChannels == 2) - { - ret = {10 * 1000, 25 * 1000, 50 * 1000, 100 * 1000, 250 * 1000, 500 * 1000, 1000 * 1000, 2500 * 1000, 5 * 1000 * 1000, 10 * 1000 * 1000, 25 * 1000 * 1000, 50 * 1000 * 1000, 100 * 1000 * 1000, 163575808}; - } - else if(activeChannels == 3 || activeChannels == 4) - { - ret = {5 * 1000, 12500 , 25 * 1000, 50 * 1000, 125 * 1000, 250 * 1000, 500 * 1000, 1250 * 1000, 2500 * 1000, 5 * 1000 * 1000, 12500 * 1000, 25 * 1000 * 1000, 50 * 1000 * 1000, 81787904}; - } - else - { - ret = {2500, 6250 , 12500, 25 * 1000, 62500, 125 * 1000, 250 * 1000, 625 * 1000, 1250 * 1000, 2500 * 1000, 6250 * 1000, 12500 * 1000, 25 * 1000 * 1000, 40893952}; + // Memory depth can either be "Fixed" or "Auto" according to the scope's configuration + // Let's check mode by getting memory depth value + GetSampleDepth(); + switch(m_memodyDepthMode) + { + case MEMORY_DEPTH_AUTO_MAX: + case MEMORY_DEPTH_AUTO_FAST: + // In auto mode, memory depth can be (as tested on the scope, only for Extended Capture mode) : + if(IsReducedSampleRate()) + { + ret = {39, 42, 48, 60, 120, 240, 480, 1200, 2400, 4800, 12000, 24000, 48000, 120000, 240000, 480000, 1200000, 2400000, 4800000, 9600000, 12000000, 15000000, 19200000, 24000000, 30000000, 48000000, 60000000, 120000000, 150000000}; + } + else + { + ret = {40, 46, 56, 77, 192, 384, 768, 1920, 3840, 7680, 19200, 38400, 76800, 192000, 384000, 768000, 1920000, 3840000, 7680000, 12000000, 19200000, 30000000, 38400000, 60000000, 76800000, 120000000, 150000000, 192000000, 240000000, 300000000}; + } + break; + case MEMORY_DEPTH_FIXED: + default: + // In fixed mode, sample depths depend on the number of active analog channels and digital probes : + // 1 analog channel or digital probe: 327.2 Mpts + // 2 analog channels / digital probes: 163.6 Mpts per channel + // 3-4 analog channels / digital probes: 81.8 Mpts per channel + // ≥ 5 analog channels / digital probes: 40.9 Mpts per channel + unsigned int activeChannels = GetActiveChannelsCount(); + + if(activeChannels <= 1) + { + ret = {20 * 1000, 50 * 1000, 100 * 1000, 200 * 1000, 500 * 1000, 1000 * 1000, 2000 * 1000, 5000 * 1000, 10 * 1000 * 1000, 20 * 1000 * 1000, 50 * 1000 * 1000, 100 * 1000 * 1000, 200 * 1000 * 1000, 327151616}; + } + else if(activeChannels == 2) + { + ret = {10 * 1000, 25 * 1000, 50 * 1000, 100 * 1000, 250 * 1000, 500 * 1000, 1000 * 1000, 2500 * 1000, 5 * 1000 * 1000, 10 * 1000 * 1000, 25 * 1000 * 1000, 50 * 1000 * 1000, 100 * 1000 * 1000, 163575808}; + } + else if(activeChannels == 3 || activeChannels == 4) + { + ret = {5 * 1000, 12500 , 25 * 1000, 50 * 1000, 125 * 1000, 250 * 1000, 500 * 1000, 1250 * 1000, 2500 * 1000, 5 * 1000 * 1000, 12500 * 1000, 25 * 1000 * 1000, 50 * 1000 * 1000, 81787904}; + } + else + { + ret = {2500, 6250 , 12500, 25 * 1000, 62500, 125 * 1000, 250 * 1000, 625 * 1000, 1250 * 1000, 2500 * 1000, 6250 * 1000, 12500 * 1000, 25 * 1000 * 1000, 40893952}; + } + break; } return ret; } @@ -1768,6 +1881,24 @@ uint64_t MagnovaOscilloscope::GetSampleRate() return m_sampleRate; } +/** + * Returns the timebase scale in s + */ +double MagnovaOscilloscope::GetTimebaseScale() +{ + { + lock_guard lock(m_cacheMutex); + if(m_timebaseScaleValid) + return m_timebaseScale; + } + double scale = stod(converse(":TIMebase:SCALe?")); + lock_guard lock2(m_cacheMutex); + m_timebaseScale = scale; + m_timebaseScaleValid = true; + return m_timebaseScale; +} + + uint64_t MagnovaOscilloscope::GetSampleDepth() { { @@ -1778,25 +1909,48 @@ uint64_t MagnovaOscilloscope::GetSampleDepth() double f; // Possible values are : AUTo, AFASt, Integer in pts string reply = converse(":ACQUIRE:MDEPTH?"); - lock_guard lock2(m_cacheMutex); - if(reply == "AUTo" || reply == "AFASt") - { // TODO :Get Sample depth based on srate and timebase - reply = converse(":TIMebase:SCALe?"); - double scale = stof(reply); - if(scale > 0) - { - m_memoryDepth = scale * 25 * GetSampleRate(); - } - else - { - m_memoryDepth = 10000000; - } - } - else + MemoryDepthMode mode = (reply == "AUTo") ? MEMORY_DEPTH_AUTO_MAX : (reply == "AFASt") ? MEMORY_DEPTH_AUTO_FAST : MEMORY_DEPTH_FIXED; + uint64_t depth; + switch(mode) { - f = Unit(Unit::UNIT_SAMPLEDEPTH).ParseString(reply); - m_memoryDepth = static_cast(f); + case MEMORY_DEPTH_AUTO_MAX: + case MEMORY_DEPTH_AUTO_FAST: + { + // Get Sample depth based on srate and timebase + // Auto (Max): Memory length = recording time * sample rate. If the maximum memory is exceeded, the sample rate is halved until the memory length is <= maximum. + // TODO : Auto (Fast): Memory length = recording time * sample rate. If over 20 Mpts/CH, the sample rate is halved until the memory length is <= 20 Mpts. + double scale = GetTimebaseScale(); + depth = llround(scale * 24 * GetSampleRate()); + if(depth < 77) + { // Special handling of small values + if (depth == 48) depth = 60; + else if(depth == 38) depth = 56; + else if(depth == 24) depth = 48; + else if(depth == 19) depth = 46; + else if(depth == 12) depth = 42; + else if(depth == 8) depth = 40; + else if(depth == 5) depth = 39; + } + else + { + uint64_t maxDepth = GetMaxAutoMemoryDepth(); + if(depth > maxDepth) + { + depth = maxDepth; + } + } + LogDebug("Auto memory depth activated, calculating Mdepth based on time scale %f and sample rate %" PRIu64 ": mdepth = %" PRIu64 ".\n",scale,GetSampleRate(),depth); + } + break; + default: + case MEMORY_DEPTH_FIXED: + f = Unit(Unit::UNIT_SAMPLEDEPTH).ParseString(reply); + depth = static_cast(f); + break; } + lock_guard lock2(m_cacheMutex); + m_memoryDepth = depth; + m_memodyDepthMode = mode; m_memoryDepthValid = true; return m_memoryDepth; } @@ -1821,6 +1975,7 @@ void MagnovaOscilloscope::SetSampleDepth(uint64_t depth) //If we query the instrument later, the cache will be updated then. lock_guard lock2(m_cacheMutex); m_memoryDepthValid = false; + m_timebaseScaleValid= false; m_sampleRateValid = false; m_triggerOffsetValid = false; } @@ -1831,7 +1986,7 @@ void MagnovaOscilloscope::SetSampleRate(uint64_t rate) lock_guard lock(m_transport->GetMutex()); double sampletime = GetSampleDepth() / (double)rate; - double scale = sampletime / 25; // TODO: check that should be 12 or 24 (when in extended capture rate) ? + double scale = sampletime / 24; // TODO: check that should be 12 or 24 (when in extended capture rate) ? switch(m_modelid) { @@ -1857,6 +2012,7 @@ void MagnovaOscilloscope::SetSampleRate(uint64_t rate) lock_guard lock2(m_cacheMutex); m_sampleRateValid = false; m_memoryDepthValid = false; + m_timebaseScaleValid = false; m_triggerOffsetValid = false; } @@ -2442,7 +2598,7 @@ void MagnovaOscilloscope::PullDropoutTrigger() } //Dropout time - dt->SetDropoutTime(stof(converse(":TRIGGER:TIMeout:TIME?"))*FS_PER_SECOND); + dt->SetDropoutTime(llround(stod(converse(":TRIGGER:TIMeout:TIME?"))*FS_PER_SECOND)); //Edge type GetTriggerSlope(dt,converse(":TRIGGER:TIMeout:SLOPE?")); @@ -2549,7 +2705,7 @@ void MagnovaOscilloscope::PullPulseWidthTrigger() pt->SetCondition(GetCondition(converse(":TRIGGER:PULSe:TIMing?"))); // Lower/upper not available on Magnova's pulse, only Threshod is available so let's map it lower bound - pt->SetLowerBound(stof((converse(":TRIGger:PULSe:THReshold?")))*FS_PER_SECOND); + pt->SetLowerBound(llround(stod((converse(":TRIGger:PULSe:THReshold?")))*FS_PER_SECOND)); //Min range //pt->SetLowerBound(fs.ParseString(converse(":TRIGGER:PULSe:DURation:LOWer?"))); @@ -2605,10 +2761,10 @@ void MagnovaOscilloscope::PullRuntTrigger() rt->SetUpperBound(stof(converse(":TRIGGER:RUNT:LEVel2?"))); //Lower bound - rt->SetLowerInterval(stof(converse(":TRIGGER:RUNT:DURation:LOWer?"))*FS_PER_SECOND); + rt->SetLowerInterval(llround(stod(converse(":TRIGGER:RUNT:DURation:LOWer?"))*FS_PER_SECOND)); //Upper interval - rt->SetUpperInterval(stof(converse(":TRIGGER:RUNT:DURation:UPPer?"))*FS_PER_SECOND); + rt->SetUpperInterval(llround(stod(converse(":TRIGGER:RUNT:DURation:UPPer?"))*FS_PER_SECOND)); //Slope reply = Trim(converse(":TRIGger:RUNT:POLarity?")); @@ -2659,10 +2815,10 @@ void MagnovaOscilloscope::PullSlewRateTrigger() st->SetUpperBound(stof(converse(":TRIGGER:SLOPe:LEVel2?"))); //Lower interval - st->SetLowerInterval(stof(converse(":TRIGGER:SLOPe:DURation:LOWer?")) * FS_PER_SECOND); + st->SetLowerInterval(llround(stod(converse(":TRIGGER:SLOPe:DURation:LOWer?")) * FS_PER_SECOND)); //Upper interval - st->SetUpperInterval(stof(converse(":TRIGGER:SLOPe:DURation:UPPer?")) * FS_PER_SECOND); + st->SetUpperInterval(llround(stod(converse(":TRIGGER:SLOPe:DURation:UPPer?")) * FS_PER_SECOND)); //Slope string reply = Trim(converse(":TRIGger:SLOPe:TYPE?")); @@ -2938,10 +3094,10 @@ void MagnovaOscilloscope::PullGlitchTrigger() gt->SetCondition(GetCondition(converse(":TRIGGER:INTerval:TIMing?"))); //Lower bound - gt->SetLowerBound(stof(converse(":TRIGger:INTerval:DURation:LOWer?"))*FS_PER_SECOND); + gt->SetLowerBound(llround(stod(converse(":TRIGger:INTerval:DURation:LOWer?"))*FS_PER_SECOND)); //Upper interval - gt->SetUpperBound(stof(converse(":TRIGger:INTerval:DURation:UPPer?"))*FS_PER_SECOND); + gt->SetUpperBound(llround(stod(converse(":TRIGger:INTerval:DURation:UPPer?"))*FS_PER_SECOND)); } /** @@ -2986,7 +3142,7 @@ void MagnovaOscilloscope::PullNthEdgeBurstTrigger() GetTriggerSlope(bt,converse(":TRIGger:NEDGe:SLOPe?")); //Idle time - bt->SetIdleTime(stof(converse(":TRIGger:NEDGe:IDLE?"))*FS_PER_SECOND); + bt->SetIdleTime(llround(stod(converse(":TRIGger:NEDGe:IDLE?"))*FS_PER_SECOND)); //Edge number bt->SetEdgeNumber(stoi(converse(":TRIGger:NEDGe:COUNt?"))); diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h index 93154405..500edd06 100644 --- a/scopehal/MagnovaOscilloscope.h +++ b/scopehal/MagnovaOscilloscope.h @@ -138,6 +138,15 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP MODEL_UNKNOWN }; + // Memory depth mode + enum MemoryDepthMode + { + MEMORY_DEPTH_AUTO_FAST, + MEMORY_DEPTH_AUTO_MAX, + MEMORY_DEPTH_FIXED + }; + + Model GetModelID() { return m_modelid; } //Timebase @@ -276,6 +285,12 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP unsigned int GetActiveChannelsCount(); + double GetTimebaseScale(); + + bool IsReducedSampleRate(); + + uint64_t GetMaxAutoMemoryDepth(); + void PrepareAcquisition(); std::string GetDigitalChannelBankName(size_t channel); @@ -342,6 +357,9 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP int64_t m_sampleRate; bool m_memoryDepthValid; int64_t m_memoryDepth; + bool m_timebaseScaleValid; + double m_timebaseScale; + MemoryDepthMode m_memodyDepthMode; bool m_triggerOffsetValid; int64_t m_triggerOffset; std::map m_channelDeskew; From d0bd2d8a68d488d158a9cdc7a38422ea62427303 Mon Sep 17 00:00:00 2001 From: fredzo Date: Sun, 21 Dec 2025 23:59:01 +0100 Subject: [PATCH 30/33] Fixed set sample rate for auto mode. --- scopehal/MagnovaOscilloscope.cpp | 25 +++++++-- scopehal/MagnovaOscilloscope.h | 92 ++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 3 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 06e7f465..bf34fd45 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -1080,6 +1080,20 @@ bool MagnovaOscilloscope::IsReducedSampleRate() return false; } +/** + @brief Returns the memory depth for a given sample rate in auto mode + */ +uint64_t MagnovaOscilloscope::GetMemoryDepthForSrate(uint64_t srate) +{ + + const auto map = m_memodyDepthMode == MEMORY_DEPTH_AUTO_FAST ? &memoryDepthFastMap : IsReducedSampleRate() ? &memoryDepthMaxLowSrateMap : &memoryDepthMaxHighSrateMap; + const auto it = map->find(srate); + if (it != map->end()) + { + return it->second; + } + return m_memoryDepth; // Use current memory depth if not found +} /** @brief Returns the max memory depth for auto mode @@ -1796,7 +1810,7 @@ vector MagnovaOscilloscope::GetSampleDepthsNonInterleaved() } else { - ret = {40, 46, 56, 77, 192, 384, 768, 1920, 3840, 7680, 19200, 38400, 76800, 192000, 384000, 768000, 1920000, 3840000, 7680000, 12000000, 19200000, 30000000, 38400000, 60000000, 76800000, 120000000, 150000000, 192000000, 240000000, 300000000}; + ret = {40, 46, 56, 77, 192, 384, 768, 1920, 3840, 7680, 19200, 38400, 76800, 192000, 384000, 768000, 1920000, 3840000, 7680000, 9600000, 12000000, 19200000, 30000000, 38400000, 60000000, 76800000, 120000000, 150000000, 192000000, 240000000, 300000000}; } break; case MEMORY_DEPTH_FIXED: @@ -1985,8 +1999,13 @@ void MagnovaOscilloscope::SetSampleRate(uint64_t rate) { //Need to lock the transport mutex when setting rate to prevent changing rate during an acquisition lock_guard lock(m_transport->GetMutex()); - double sampletime = GetSampleDepth() / (double)rate; - double scale = sampletime / 24; // TODO: check that should be 12 or 24 (when in extended capture rate) ? + uint64_t sampleDepth = GetSampleDepth(); + if(m_memodyDepthMode == MEMORY_DEPTH_AUTO_MAX || m_memodyDepthMode == MEMORY_DEPTH_AUTO_FAST) + { // Sample depth is determined by the sample rate in auto mode + sampleDepth = GetMemoryDepthForSrate(rate); + } + double sampletime = sampleDepth / (double)rate; + double scale = sampletime / 24; // TODO: change 24 to 12 if acquire mode is not extended switch(m_modelid) { diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h index 500edd06..005c0695 100644 --- a/scopehal/MagnovaOscilloscope.h +++ b/scopehal/MagnovaOscilloscope.h @@ -289,6 +289,8 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP bool IsReducedSampleRate(); + uint64_t GetMemoryDepthForSrate(uint64_t srate); + uint64_t GetMaxAutoMemoryDepth(); void PrepareAcquisition(); @@ -385,6 +387,96 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP FunctionGeneratorChannel* m_awgChannel; std::vector m_digitalChannels; + // Maps for srate to memory depth for auto memory mode +private: + const std::map memoryDepthFastMap { + {2, 12000000}, + {5, 12000000}, + {10, 12000000}, + {40, 19200000}, + {50, 12000000}, + {100, 12000000}, + {400, 19200000}, + {500, 12000000}, + {1000, 12000000}, + {4000, 19200000}, + {5000, 12000000}, + {10000, 12000000}, + {40000, 19200000}, + {50000, 12000000}, + {100000, 12000000}, + {400000, 19200000}, + {500000, 12000000}, + {1000000, 12000000}, + {2500000, 12000000}, + {4000000, 19200000}, + {5000000, 12000000}, + {10000000, 12000000}, + {25000000, 12000000}, + {40000000, 19200000}, + {50000000, 12000000}, + {100000000, 12000000}, + {125000000, 15000000}, + {250000000, 12000000}, + {400000000, 19200000}, + {500000000, 12000000}, + {800000000, 19200000}, + {1000000000, 12000000}, + {1600000000, 19200000} + }; + const std::map memoryDepthMaxLowSrateMap { + {25, 120000000}, + {50, 120000000}, + {100, 120000000}, + {250, 120000000}, + {500, 120000000}, + {1000, 120000000}, + {2500, 120000000}, + {5000, 120000000}, + {10000, 120000000}, + {25000, 120000000}, + {50000, 120000000}, + {100000, 120000000}, + {250000, 120000000}, + {500000, 120000000}, + {1000000, 120000000}, + {2500000, 120000000}, + {5000000, 120000000}, + {10000000, 120000000}, + {25000000, 120000000}, + {50000000, 120000000}, + {125000000, 150000000}, + {250000000, 120000000}, + {500000000, 120000000}, + {1000000000, 120000000} + }; + const std::map memoryDepthMaxHighSrateMap { + {50, 240000000}, + {100, 240000000}, + {250, 300000000}, + {500, 240000000}, + {1000, 240000000}, + {2500, 300000000}, + {5000, 240000000}, + {10000, 240000000}, + {25000, 300000000}, + {50000, 240000000}, + {100000, 240000000}, + {250000, 300000000}, + {500000, 240000000}, + {1000000, 240000000}, + {2500000, 300000000}, + {5000000, 240000000}, + {10000000, 240000000}, + {25000000, 300000000}, + {50000000, 240000000}, + {100000000, 240000000}, + {200000000, 240000000}, + {400000000, 192000000}, + {800000000, 192000000}, + {1600000000, 192000000} + }; + public: static std::string GetDriverNameInternal(); OSCILLOSCOPE_INITPROC(MagnovaOscilloscope) From 9693c67d026e2ec30753f6c1e689db545b9055df Mon Sep 17 00:00:00 2001 From: fredzo Date: Mon, 22 Dec 2025 01:49:52 +0100 Subject: [PATCH 31/33] Fixed max memory depth calculation. --- scopehal/MagnovaOscilloscope.cpp | 26 ++++++++++++++------------ scopehal/MagnovaOscilloscope.h | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index bf34fd45..614ccc1c 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -1098,29 +1098,35 @@ uint64_t MagnovaOscilloscope::GetMemoryDepthForSrate(uint64_t srate) /** @brief Returns the max memory depth for auto mode */ -uint64_t MagnovaOscilloscope::GetMaxAutoMemoryDepth() +uint64_t MagnovaOscilloscope::GetMaxAutoMemoryDepth(uint64_t original) { if(m_memodyDepthMode == MEMORY_DEPTH_AUTO_FAST) { // In fast mode, depth is limited to 20 Mpts - return 20*1000*1000; + while(original > 20*1000*1000) + { + original/=2; + } + return original; } unsigned int activeChannels = GetActiveChannelsCount(); + uint64_t max; if(activeChannels <= 1) { - return 300*1000*1000; + max = 300*1000*1000; } else if(activeChannels == 2) { - return 150*1000*1000; + max = 150*1000*1000; } else if(activeChannels == 3 || activeChannels == 4) { - return 60*1000*1000; + max = 60*1000*1000; } else { - return 30*1000*1000; + max = 30*1000*1000; } + return min(original,max); } @@ -1947,11 +1953,7 @@ uint64_t MagnovaOscilloscope::GetSampleDepth() } else { - uint64_t maxDepth = GetMaxAutoMemoryDepth(); - if(depth > maxDepth) - { - depth = maxDepth; - } + depth = GetMaxAutoMemoryDepth(depth); } LogDebug("Auto memory depth activated, calculating Mdepth based on time scale %f and sample rate %" PRIu64 ": mdepth = %" PRIu64 ".\n",scale,GetSampleRate(),depth); } @@ -2172,7 +2174,7 @@ bool MagnovaOscilloscope::IsInterleaving() switch(m_modelid) { case MODEL_MAGNOVA_BMO: - if((m_channelsEnabled[2] == true) || (m_channelsEnabled[3] == true)) + if((IsChannelEnabled(2)) || (IsChannelEnabled(3) == true)) { // Interleaving if Channel 3 or 4 are active return true; } diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h index 005c0695..c19c95fa 100644 --- a/scopehal/MagnovaOscilloscope.h +++ b/scopehal/MagnovaOscilloscope.h @@ -291,7 +291,7 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP uint64_t GetMemoryDepthForSrate(uint64_t srate); - uint64_t GetMaxAutoMemoryDepth(); + uint64_t GetMaxAutoMemoryDepth(uint64_t original); void PrepareAcquisition(); From cbd5cc4b46f5c5e57c458f1837c0d42549673b5e Mon Sep 17 00:00:00 2001 From: fredzo Date: Mon, 22 Dec 2025 11:37:03 +0100 Subject: [PATCH 32/33] Fixed set srate in manual memory mode: detect screen div per capture according to srate. --- scopehal/MagnovaOscilloscope.cpp | 28 ++++++++++++++++++++++++++-- scopehal/MagnovaOscilloscope.h | 2 ++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 614ccc1c..1e2b7193 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -1095,6 +1095,30 @@ uint64_t MagnovaOscilloscope::GetMemoryDepthForSrate(uint64_t srate) return m_memoryDepth; // Use current memory depth if not found } +uint8_t MagnovaOscilloscope::GetCaptureScreenDivisions(MemoryDepthMode mode, uint64_t srate) +{ // TODO: change 24 to 12 if acquire mode is not extended + if(mode == MEMORY_DEPTH_AUTO_FAST || mode == MEMORY_DEPTH_AUTO_MAX) + { + return 24; + } + else + { // In manual mode the value can either be 25 or 40 depending on srate: if srate contains '1' or '5' digits, it's 40, otherwise it's 25 (as per empirical determination) + // for srate values > 1000000 + if(srate > 1000000) + { + string stringSrate = to_string(srate); + for (char c : stringSrate) + { + if (c == '1' || c == '5') + { + return 40; + } + } + } + return 25; + } +} + /** @brief Returns the max memory depth for auto mode */ @@ -1940,7 +1964,7 @@ uint64_t MagnovaOscilloscope::GetSampleDepth() // Auto (Max): Memory length = recording time * sample rate. If the maximum memory is exceeded, the sample rate is halved until the memory length is <= maximum. // TODO : Auto (Fast): Memory length = recording time * sample rate. If over 20 Mpts/CH, the sample rate is halved until the memory length is <= 20 Mpts. double scale = GetTimebaseScale(); - depth = llround(scale * 24 * GetSampleRate()); + depth = llround(scale * GetCaptureScreenDivisions(mode,0) * GetSampleRate()); if(depth < 77) { // Special handling of small values if (depth == 48) depth = 60; @@ -2007,7 +2031,7 @@ void MagnovaOscilloscope::SetSampleRate(uint64_t rate) sampleDepth = GetMemoryDepthForSrate(rate); } double sampletime = sampleDepth / (double)rate; - double scale = sampletime / 24; // TODO: change 24 to 12 if acquire mode is not extended + double scale = sampletime / GetCaptureScreenDivisions(m_memodyDepthMode,rate); switch(m_modelid) { diff --git a/scopehal/MagnovaOscilloscope.h b/scopehal/MagnovaOscilloscope.h index c19c95fa..0a75f7b9 100644 --- a/scopehal/MagnovaOscilloscope.h +++ b/scopehal/MagnovaOscilloscope.h @@ -293,6 +293,8 @@ class MagnovaOscilloscope : public virtual SCPIOscilloscope, public virtual SCP uint64_t GetMaxAutoMemoryDepth(uint64_t original); + static uint8_t GetCaptureScreenDivisions(MemoryDepthMode mode, uint64_t srate); + void PrepareAcquisition(); std::string GetDigitalChannelBankName(size_t channel); From 287d22de4744c2ac8aafedb30d37be54d60060a5 Mon Sep 17 00:00:00 2001 From: fredzo Date: Mon, 22 Dec 2025 11:43:59 +0100 Subject: [PATCH 33/33] Updated TODO --- scopehal/MagnovaOscilloscope.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scopehal/MagnovaOscilloscope.cpp b/scopehal/MagnovaOscilloscope.cpp index 1e2b7193..d7e6bb91 100644 --- a/scopehal/MagnovaOscilloscope.cpp +++ b/scopehal/MagnovaOscilloscope.cpp @@ -1962,7 +1962,7 @@ uint64_t MagnovaOscilloscope::GetSampleDepth() { // Get Sample depth based on srate and timebase // Auto (Max): Memory length = recording time * sample rate. If the maximum memory is exceeded, the sample rate is halved until the memory length is <= maximum. - // TODO : Auto (Fast): Memory length = recording time * sample rate. If over 20 Mpts/CH, the sample rate is halved until the memory length is <= 20 Mpts. + // Auto (Fast): Memory length = recording time * sample rate. If over 20 Mpts/CH, the sample rate is halved until the memory length is <= 20 Mpts. double scale = GetTimebaseScale(); depth = llround(scale * GetCaptureScreenDivisions(mode,0) * GetSampleRate()); if(depth < 77)