diff --git a/scopehal/LeCroyOscilloscope.cpp b/scopehal/LeCroyOscilloscope.cpp index c666912f..f100bfec 100644 --- a/scopehal/LeCroyOscilloscope.cpp +++ b/scopehal/LeCroyOscilloscope.cpp @@ -86,14 +86,8 @@ void LeCroyOscilloscope::SharedCtorInit() m_digitalChannelCount = 0; //Add the external trigger input - m_extTrigChannel = new OscilloscopeChannel( - this, - "Ext", - OscilloscopeChannel::CHANNEL_TYPE_TRIGGER, - "", - 1, - m_channels.size(), - true); + m_extTrigChannel = + new OscilloscopeChannel(this, "Ext", OscilloscopeChannel::CHANNEL_TYPE_TRIGGER, "", 1, m_channels.size(), true); m_channels.push_back(m_extTrigChannel); //Desired format for waveform data @@ -148,12 +142,12 @@ void LeCroyOscilloscope::IdentifyHardware() m_modelid = MODEL_DDA_5K; m_maxBandwidth = 5000; } - else if( (m_model.find("HDO4") == 0) && (m_model.find("A") != string::npos) ) + else if((m_model.find("HDO4") == 0) && (m_model.find("A") != string::npos)) { m_modelid = MODEL_HDO_4KA; m_maxBandwidth = stoi(m_model.substr(4, 2)) * 100; } - else if( (m_model.find("HDO6") == 0) && (m_model.find("A") != string::npos) ) + else if((m_model.find("HDO6") == 0) && (m_model.find("A") != string::npos)) { m_modelid = MODEL_HDO_6KA; m_maxBandwidth = stoi(m_model.substr(4, 2)) * 100; @@ -224,17 +218,6 @@ void LeCroyOscilloscope::IdentifyHardware() m_modelid = MODEL_WAVESURFER_3K; m_maxBandwidth = stoi(m_model.substr(3, 2)) * 100; } - else if (m_vendor.compare("SIGLENT") == 0) - { - // TODO: if LeCroy and Siglent classes get split, then this should obviously - // move to the Siglent class. - if (m_model.compare(0, 4, "SDS2") == 0 && m_model.back() == 'X') - m_modelid = MODEL_SIGLENT_SDS2000X; - - //FIXME - m_maxBandwidth = 200; - } - else { LogWarning("Model \"%s\" is unknown, available sample rates/memory depths may not be properly detected\n", @@ -262,7 +245,7 @@ void LeCroyOscilloscope::DetectOptions() //Read options until we hit a null vector options; string opt; - for(unsigned int i=0; iSendCommand(tmp); - string reply = m_transport->ReadReply(); + m_transport->SendCommand(tmp); + string reply = m_transport->ReadReply(); - //All good - if(Trim(reply) == "-1") - nchans = i; + //All good + if(Trim(reply) == "-1") + nchans = i; - //Anything else is probably an error: - //Object doesn't support this property or method: 'app.Acquisition.C5' - else - break; - } + //Anything else is probably an error: + //Object doesn't support this property or method: 'app.Acquisition.C5' + else + break; } - break; + } + break; //General model format is family, number, suffix. Not all are always present. default: + { + //Trim off alphabetic characters from the start of the model number + size_t pos; + for(pos = 0; pos < m_model.length(); pos++) { - //Trim off alphabetic characters from the start of the model number - size_t pos; - for(pos=0; pos < m_model.length(); pos++) + if(isalpha(m_model[pos])) + continue; + else if(isdigit(m_model[pos])) + break; + else { - if(isalpha(m_model[pos])) - continue; - else if(isdigit(m_model[pos])) - break; - else - { - LogError("Unrecognized character (not alphanumeric) in model number %s\n", m_model.c_str()); - return; - } + LogError("Unrecognized character (not alphanumeric) in model number %s\n", m_model.c_str()); + return; } + } - //Now we should be able to read the model number - int modelNum = atoi(m_model.c_str() + pos); + //Now we should be able to read the model number + int modelNum = atoi(m_model.c_str() + pos); - //Last digit of the model number is normally the number of channels (WAVESURFER3022, HDO8108) - nchans = modelNum % 10; - } - break; + //Last digit of the model number is normally the number of channels (WAVESURFER3022, HDO8108) + nchans = modelNum % 10; + } + break; } - for(int i=0; iGetHwname() + ":TRACE?"; m_transport->SendCommand(cmd); string reply = m_transport->ReadReply(); - if(reply.find("OFF") == 0) //may have a trailing newline, ignore that + if(reply.find("OFF") == 0) //may have a trailing newline, ignore that m_channelsEnabled[i] = false; else m_channelsEnabled[i] = true; @@ -1010,7 +982,7 @@ bool LeCroyOscilloscope::IsChannelEnabled(size_t i) { //See if the channel is on //Note that GetHwname() returns Dn, as used by triggers, not Digitaln, as used here - size_t nchan = i - (m_analogChannelCount+1); + size_t nchan = i - (m_analogChannelCount + 1); m_transport->SendCommand(string("VBS? 'return = app.LogicAnalyzer.Digital1.Digital") + to_string(nchan) + "'"); string str = m_transport->ReadReply(); if(str == "0") @@ -1038,8 +1010,7 @@ void LeCroyOscilloscope::EnableChannel(size_t i) auto conflicts = GetInterleaveConflicts(); for(auto c : conflicts) { - if( (c.first->IsEnabled() || (c.first == chan) ) && - (c.second->IsEnabled() || (c.second == chan) ) ) + if((c.first->IsEnabled() || (c.first == chan)) && (c.second->IsEnabled() || (c.second == chan))) { SetInterleaving(false); break; @@ -1074,7 +1045,7 @@ void LeCroyOscilloscope::EnableChannel(size_t i) //Enable this channel on the hardware //Note that GetHwname() returns Dn, as used by triggers, not Digitaln, as used here - size_t nchan = i - (m_analogChannelCount+1); + size_t nchan = i - (m_analogChannelCount + 1); m_transport->SendCommand(string("VBS 'app.LogicAnalyzer.Digital1.Digital") + to_string(nchan) + " = 1'"); char tmp[128]; size_t nbit = (i - m_digitalChannels[0]->GetIndex()); @@ -1099,14 +1070,13 @@ bool LeCroyOscilloscope::CanEnableChannel(size_t i) case MODEL_SDA_3K: case MODEL_HDO_4KA: case MODEL_WAVERUNNER_8K: - case MODEL_WAVERUNNER_8K_HD: //TODO: seems like multiple levels of interleaving possible + case MODEL_WAVERUNNER_8K_HD: //TODO: seems like multiple levels of interleaving possible case MODEL_WAVEMASTER_8ZI_B: case MODEL_WAVEPRO_HD: case MODEL_WAVERUNNER_9K: - case MODEL_SIGLENT_SDS2000X: return (i == 1) || (i == 2) || (i > m_analogChannelCount); - case MODEL_WAVESURFER_3K: //TODO: can use ch1 if not 2, and ch3 if not 4 + case MODEL_WAVESURFER_3K: //TODO: can use ch1 if not 2, and ch3 if not 4 return (i == 1) || (i == 2) || (i > m_analogChannelCount); //No interleaving possible, ignore @@ -1153,7 +1123,7 @@ void LeCroyOscilloscope::DisableChannel(size_t i) m_transport->SendCommand("VBS 'app.LogicAnalyzer.Digital1.UseGrid=\"NotOnGrid\"'"); //Disable this channel - size_t nchan = i - (m_analogChannelCount+1); + size_t nchan = i - (m_analogChannelCount + 1); m_transport->SendCommand(string("VBS 'app.LogicAnalyzer.Digital1.Digital") + to_string(nchan) + " = 0'"); } } @@ -1167,7 +1137,7 @@ OscilloscopeChannel::CouplingType LeCroyOscilloscope::GetChannelCoupling(size_t { lock_guard lock(m_mutex); m_transport->SendCommand(m_channels[i]->GetHwname() + ":COUPLING?"); - reply = Trim(m_transport->ReadReply().substr(0,3)); + reply = Trim(m_transport->ReadReply().substr(0, 3)); } lock_guard lock2(m_cacheMutex); @@ -1372,7 +1342,6 @@ vector LeCroyOscilloscope::GetChannelBandwidthLimiters(size_t /*i* //Only the default 20/200 case MODEL_HDO_4KA: case MODEL_HDO_6KA: - case MODEL_SIGLENT_SDS2000X: default: break; } @@ -1396,13 +1365,13 @@ int LeCroyOscilloscope::GetChannelBandwidthLimit(size_t i) return 0; char chbw[16]; - sscanf(reply.c_str() + index + 3, "%15[^,\n]", chbw); //offset 3 for "Cn," + sscanf(reply.c_str() + index + 3, "%15[^,\n]", chbw); //offset 3 for "Cn," string sbw(chbw); if(sbw == "OFF") return 0; - else if(sbw == "ON") //apparently "on" means lowest possible B/W? - return 20; //this isn't documented anywhere in the MAUI remote control manual + else if(sbw == "ON") //apparently "on" means lowest possible B/W? + return 20; //this isn't documented anywhere in the MAUI remote control manual else if(sbw == "20MHZ") return 20; else if(sbw == "200MHZ") @@ -1432,7 +1401,7 @@ void LeCroyOscilloscope::SetChannelBandwidthLimit(size_t i, unsigned int limit_m if(limit_mhz == 0) snprintf(cmd, sizeof(cmd), "BANDWIDTH_LIMIT %s,OFF", m_channels[i]->GetHwname().c_str()); else if(limit_mhz >= 1000) - snprintf(cmd, sizeof(cmd), "BANDWIDTH_LIMIT %s,%uGHZ", m_channels[i]->GetHwname().c_str(), limit_mhz/1000); + snprintf(cmd, sizeof(cmd), "BANDWIDTH_LIMIT %s,%uGHZ", m_channels[i]->GetHwname().c_str(), limit_mhz / 1000); else snprintf(cmd, sizeof(cmd), "BANDWIDTH_LIMIT %s,%uMHZ", m_channels[i]->GetHwname().c_str(), limit_mhz); @@ -1491,7 +1460,7 @@ void LeCroyOscilloscope::SetChannelDisplayName(size_t i, string name) else { m_transport->SendCommand(string("VBS 'app.LogicAnalyzer.Digital1.CustomBitName") + - to_string(i - m_digitalChannelBase) + " = \"" + name + "\""); + to_string(i - m_digitalChannelBase) + " = \"" + name + "\""); } } @@ -1641,18 +1610,17 @@ int LeCroyOscilloscope::GetCurrentMeterChannel() string str = m_transport->ReadReply(); int i; sscanf(str.c_str(), "C%d", &i); - return i - 1; //scope channels are 1 based + return i - 1; //scope channels are 1 based } void LeCroyOscilloscope::SetCurrentMeterChannel(int chan) { lock_guard lock(m_mutex); char cmd[128]; - snprintf( - cmd, + snprintf(cmd, sizeof(cmd), "VBS 'app.acquisition.DVM.DvmSource = \"C%d\"", - chan + 1); //scope channels are 1 based + chan + 1); //scope channels are 1 based m_transport->SendCommand(cmd); } @@ -1666,7 +1634,7 @@ Multimeter::MeasurementTypes LeCroyOscilloscope::GetMeterMode() string str = m_transport->ReadReply(); //trim off trailing whitespace - while(isspace(str[str.length()-1])) + while(isspace(str[str.length() - 1])) str.resize(str.length() - 1); if(str == "DC") @@ -1717,7 +1685,6 @@ void LeCroyOscilloscope::SetMeterMode(Multimeter::MeasurementTypes type) case Multimeter::TEMPERATURE: LogWarning("unsupported multimeter mode\n"); return; - } lock_guard lock(m_mutex); @@ -1915,7 +1882,7 @@ void LeCroyOscilloscope::BulkCheckChannelEnableState() //Check enable state in the cache. vector uncached; - for(unsigned int i=0; i& wavedescs, - bool* enabled, - unsigned int& firstEnabledChannel, - bool& any_enabled) + vector& wavedescs, bool* enabled, unsigned int& firstEnabledChannel, bool& any_enabled) { //(Note: with VICP framing we cannot use semicolons to separate commands) BulkCheckChannelEnableState(); - for(unsigned int i=0; iSendCommand(m_channels[i]->GetHwname() + ":WF? DESC"); } } - for(unsigned int i=0; i 1) && !sent_wavetime) + if((num_sequences > 1) && !sent_wavetime) { m_transport->SendCommand(m_channels[i]->GetHwname() + ":WF? TIME"); sent_wavetime = true; @@ -2083,7 +2047,9 @@ time_t LeCroyOscilloscope::ExtractTimestamp(unsigned char* wavedesc, double& bas //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", + snprintf(tblock, + sizeof(tblock), + "%d-%d-%d %d:%02d:%02d", *reinterpret_cast(wavedesc + 308), wavedesc[307], wavedesc[306], @@ -2091,16 +2057,15 @@ time_t LeCroyOscilloscope::ExtractTimestamp(unsigned char* wavedesc, double& bas wavedesc[304], seconds); locale cur_locale; - auto& tget = use_facet< time_get >(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)); + tget.get(stream, time_get::iter_type(), stream, state, &tstruc, format, format + strlen(format)); return mktime(&tstruc); } -vector LeCroyOscilloscope::ProcessAnalogWaveform( - const char* data, +vector LeCroyOscilloscope::ProcessAnalogWaveform(const char* data, size_t datalen, string& wavedesc, uint32_t num_sequences, @@ -2125,23 +2090,23 @@ vector LeCroyOscilloscope::ProcessAnalogWaveform( float interval = *reinterpret_cast(pdesc + 176) * FS_PER_SECOND; //cppcheck-suppress invalidPointerCast - double h_off = *reinterpret_cast(pdesc + 180) * FS_PER_SECOND; //fs from start of waveform to trigger + double h_off = *reinterpret_cast(pdesc + 180) * FS_PER_SECOND; //fs from start of waveform to trigger - double h_off_frac = fmodf(h_off, interval); //fractional sample position, in fs + double h_off_frac = fmodf(h_off, interval); //fractional sample position, in fs if(h_off_frac < 0) - h_off_frac = interval + h_off_frac; //double h_unit = *reinterpret_cast(pdesc + 244); + h_off_frac = interval + h_off_frac; //double h_unit = *reinterpret_cast(pdesc + 244); //Raw waveform data size_t num_samples; if(m_highDefinition) - num_samples = datalen/2; + num_samples = datalen / 2; else num_samples = datalen; size_t num_per_segment = num_samples / num_sequences; int16_t* wdata = (int16_t*)&data[0]; int8_t* bdata = (int8_t*)&data[0]; - for(size_t j=0; j LeCroyOscilloscope::ProcessAnalogWaveform( //Parse the time if(num_sequences > 1) - cap->m_startFemtoseconds = static_cast( (basetime + wavetime[j*2]) * FS_PER_SECOND ); + cap->m_startFemtoseconds = static_cast((basetime + wavetime[j * 2]) * FS_PER_SECOND); else cap->m_startFemtoseconds = static_cast(basetime * FS_PER_SECOND); @@ -2164,9 +2129,9 @@ vector LeCroyOscilloscope::ProcessAnalogWaveform( float* samps = reinterpret_cast(&cap->m_samples[0]); if(m_highDefinition) { - int16_t* base = wdata + j*num_per_segment; + int16_t* base = wdata + j * num_per_segment; - for(unsigned int k=0; km_offsets[k] = k; cap->m_durations[k] = 1; @@ -2187,34 +2152,32 @@ vector LeCroyOscilloscope::ProcessAnalogWaveform( size_t blocksize = num_per_segment / numblocks; blocksize = blocksize - (blocksize % 32); - #pragma omp parallel for - for(size_t i=0; im_offsets[i*blocksize], - (int64_t*)&cap->m_durations[i*blocksize], - samps + i*blocksize, - bdata + j*num_per_segment + i*blocksize, + Convert8BitSamplesAVX2((int64_t*)&cap->m_offsets[i * blocksize], + (int64_t*)&cap->m_durations[i * blocksize], + samps + i * blocksize, + bdata + j * num_per_segment + i * blocksize, v_gain, v_off, nsamp, - i*blocksize); + i * blocksize); } } //Small waveforms get done single threaded to avoid overhead else { - Convert8BitSamplesAVX2( - (int64_t*)&cap->m_offsets[0], + Convert8BitSamplesAVX2((int64_t*)&cap->m_offsets[0], (int64_t*)&cap->m_durations[0], samps, - bdata + j*num_per_segment, + bdata + j * num_per_segment, v_gain, v_off, num_per_segment, @@ -2223,11 +2186,10 @@ vector LeCroyOscilloscope::ProcessAnalogWaveform( } else { - Convert8BitSamples( - (int64_t*)&cap->m_offsets[0], + Convert8BitSamples((int64_t*)&cap->m_offsets[0], (int64_t*)&cap->m_durations[0], samps, - bdata + j*num_per_segment, + bdata + j * num_per_segment, v_gain, v_off, num_per_segment, @@ -2247,7 +2209,7 @@ vector LeCroyOscilloscope::ProcessAnalogWaveform( void LeCroyOscilloscope::Convert8BitSamples( int64_t* offs, int64_t* durs, float* pout, int8_t* pin, float gain, float offset, size_t count, int64_t ibase) { - for(unsigned int k=0; k(ones_x4)); __m256i all_fours = _mm256_load_si256(reinterpret_cast<__m256i*>(fours_x4)); __m256i counts = _mm256_load_si256(reinterpret_cast<__m256i*>(count_x4)); - __m256 gains = { gain, gain, gain, gain, gain, gain, gain, gain }; - __m256 offsets = { offset, offset, offset, offset, offset, offset, offset, offset }; + __m256 gains = {gain, gain, gain, gain, gain, gain, gain, gain}; + __m256 offsets = {offset, offset, offset, offset, offset, offset, offset, offset}; - for(unsigned int k=0; k LeCroyOscilloscope::ProcessDigitalWaveform(string& da string tmp = data.substr(data.find("SelectedLines=") + 14); tmp = tmp.substr(0, 16); bool enabledChannels[16]; - for(int i=0; i<16; i++) + for(int i = 0; i < 16; i++) enabledChannels[i] = (tmp[i] == '1'); //Quick and dirty string searching. We only care about a small fraction of the XML @@ -2411,7 +2366,7 @@ map LeCroyOscilloscope::ProcessDigitalWaveform(string& da epoch.tm_mday = 1; epoch.tm_mon = 0; epoch.tm_year = 100; - epoch.tm_wday = 6; //Jan 1 2000 was a Saturday + epoch.tm_wday = 6; //Jan 1 2000 was a Saturday epoch.tm_yday = 0; epoch.tm_isdst = now.tm_isdst; time_t epoch_stamp = mktime(&epoch); @@ -2430,13 +2385,13 @@ map LeCroyOscilloscope::ProcessDigitalWaveform(string& da //Decode the base64 base64_decodestate bstate; base64_init_decodestate(&bstate); - unsigned char* block = new unsigned char[tmp.length()]; //base64 is smaller than plaintext, leave room + unsigned char* block = new unsigned char[tmp.length()]; //base64 is smaller than plaintext, leave room base64_decode_block(tmp.c_str(), tmp.length(), (char*)block, &bstate); //We have each channel's data from start to finish before the next (no interleaving). //TODO: Multithread across waveforms unsigned int icapchan = 0; - for(unsigned int i=0; i LeCroyOscilloscope::ProcessDigitalWaveform(string& da cap->Resize(num_samples); //Save the first sample (can't merge with sample -1 because that doesn't exist) - size_t base = icapchan*num_samples; + size_t base = icapchan * num_samples; size_t k = 0; cap->m_offsets[0] = 0; cap->m_durations[0] = 1; @@ -2461,15 +2416,15 @@ map LeCroyOscilloscope::ProcessDigitalWaveform(string& da //Read and de-duplicate the other samples //TODO: can we vectorize this somehow? bool last = block[base]; - for(size_t j=1; jm_durations[k] ++; + if((last == sample) && ((j + 3) < num_samples)) + cap->m_durations[k]++; //Nope, it toggled - store the new value else @@ -2480,7 +2435,6 @@ map LeCroyOscilloscope::ProcessDigitalWaveform(string& da cap->m_samples[k] = sample; last = sample; } - } //Done, shrink any unused space @@ -2500,7 +2454,7 @@ map LeCroyOscilloscope::ProcessDigitalWaveform(string& da //Done, save data and go on to next ret[m_digitalChannels[i]->GetIndex()] = cap; - icapchan ++; + icapchan++; } //No data here for us! @@ -2515,7 +2469,7 @@ bool LeCroyOscilloscope::AcquireData() { //State for this acquisition (may be more than one waveform) uint32_t num_sequences = 1; - map > pending_waveforms; + map> pending_waveforms; double start = GetTime(); time_t ttime = 0; double basetime = 0; @@ -2539,9 +2493,9 @@ bool LeCroyOscilloscope::AcquireData() //Grab the WAVEDESC from the first enabled channel unsigned char* pdesc = NULL; - for(unsigned int i=0; i 0) { m_cacheMutex.lock(); - for(size_t i=0; iGetIndex()]) { @@ -2594,10 +2548,10 @@ bool LeCroyOscilloscope::AcquireData() ttime = ExtractTimestamp(pdesc, basetime); if(num_sequences > 1) wavetime = m_transport->ReadReply(); - pwtime = reinterpret_cast(&wavetime[16]); //skip 16-byte SCPI header + pwtime = reinterpret_cast(&wavetime[16]); //skip 16-byte SCPI header //Read the data from each analog waveform - for(unsigned int i=0; iReadReply(); @@ -2625,31 +2579,31 @@ bool LeCroyOscilloscope::AcquireData() } //Process analog waveforms - vector< vector > waveforms; + vector> waveforms; waveforms.resize(m_analogChannelCount); - for(unsigned int i=0; i lock(m_mutex); //m_transport->SendCommand("TRIG_MODE NORM"); - m_transport->SendCommand("TRIG_MODE SINGLE"); //always do single captures, just re-trigger + m_transport->SendCommand("TRIG_MODE SINGLE"); //always do single captures, just re-trigger m_triggerArmed = true; m_triggerOneShot = false; } @@ -2780,7 +2734,7 @@ double LeCroyOscilloscope::GetChannelVoltageRange(size_t i) double volts_per_div; sscanf(reply.c_str(), "%lf", &volts_per_div); - double v = volts_per_div * 8; //plot is 8 divisions high on all MAUI scopes + double v = volts_per_div * 8; //plot is 8 divisions high on all MAUI scopes lock_guard lock(m_cacheMutex); m_channelVoltageRanges[i] = v; return v; @@ -2808,8 +2762,8 @@ vector LeCroyOscilloscope::GetSampleRatesNonInterleaved() ret.push_back(1000); const int64_t k = 1000; - const int64_t m = k*k; - const int64_t g = k*m; + const int64_t m = k * k; + const int64_t g = k * m; //These rates are supported by all known scopes ret.push_back(2 * k); @@ -2822,7 +2776,7 @@ vector LeCroyOscilloscope::GetSampleRatesNonInterleaved() ret.push_back(500 * k); ret.push_back(1 * m); - if(m_modelid == MODEL_HDO_9K) //... with one exception + if(m_modelid == MODEL_HDO_9K) //... with one exception ret.push_back(2500 * k); else ret.push_back(2 * m); @@ -2879,11 +2833,12 @@ vector LeCroyOscilloscope::GetSampleRatesNonInterleaved() ret.push_back(2 * g); ret.push_back(5 * g); ret.push_back(10 * g); - ret.push_back(20 * g); //FIXME: 20 and 40 Gsps give garbage data in the MAUI Studio simulator. - ret.push_back(40 * g); //Data looks wrong in MAUI as well as glscopeclient so doesn't seem to be something - //that we did. Looks like bits and pieces of waveform with gaps or overlap. - //Unclear if sim bug or actual issue, no testing on actual LabMaster hardware - //has been performed to date. + ret.push_back(20 * g); //FIXME: 20 and 40 Gsps give garbage data in the MAUI Studio simulator. + ret.push_back( + 40 * g); //Data looks wrong in MAUI as well as glscopeclient so doesn't seem to be something + //that we did. Looks like bits and pieces of waveform with gaps or overlap. + //Unclear if sim bug or actual issue, no testing on actual LabMaster hardware + //has been performed to date. ret.push_back(80 * g); //TODO: exact sample rates may depend on the acquisition module(s) connected break; @@ -2971,7 +2926,7 @@ vector LeCroyOscilloscope::GetSampleRatesInterleaved() //Same as non-interleaved, plus double, for all other known scopes default: - ret.push_back(ret[ret.size()-1] * 2); + ret.push_back(ret[ret.size() - 1] * 2); break; } @@ -2981,7 +2936,7 @@ vector LeCroyOscilloscope::GetSampleRatesInterleaved() vector LeCroyOscilloscope::GetSampleDepthsNonInterleaved() { const int64_t k = 1000; - const int64_t m = k*k; + const int64_t m = k * k; vector ret; @@ -2993,8 +2948,8 @@ vector LeCroyOscilloscope::GetSampleDepthsNonInterleaved() ret.push_back(5 * k); ret.push_back(10 * k); ret.push_back(20 * k); - ret.push_back(40 * k); //20/40 Gsps scopes can use values other than 1/2/5. - //TODO: figure out which models allow this + ret.push_back(40 * k); //20/40 Gsps scopes can use values other than 1/2/5. + //TODO: figure out which models allow this ret.push_back(50 * k); ret.push_back(80 * k); ret.push_back(100 * k); @@ -3109,7 +3064,7 @@ vector LeCroyOscilloscope::GetSampleDepthsInterleaved() //Default to doubling the non-interleaved depths vector ret; for(auto rate : base) - ret.push_back(rate*2); + ret.push_back(rate * 2); switch(m_modelid) { @@ -3213,9 +3168,7 @@ void LeCroyOscilloscope::SetSampleDepth(uint64_t depth) float sec_per_acquisition = fs_per_acquisition * SECONDS_PER_FS; float sec_per_div = sec_per_acquisition / 10; - m_transport->SendCommand( - string("VBS? 'app.Acquisition.Horizontal.HorScale = ") + - to_string_sci(sec_per_div) + "'"); + m_transport->SendCommand(string("VBS? 'app.Acquisition.Horizontal.HorScale = ") + to_string_sci(sec_per_div) + "'"); //Sometimes the scope won't set the exact depth we ask for. //Flush the cache to force a read so we know the actual depth we got. @@ -3310,10 +3263,11 @@ void LeCroyOscilloscope::SetDeskewForChannel(size_t channel, int64_t skew) lock_guard lock(m_mutex); char tmp[128]; - snprintf(tmp, sizeof(tmp), "VBS? 'app.Acquisition.%s.Deskew=%e'", + snprintf(tmp, + sizeof(tmp), + "VBS? 'app.Acquisition.%s.Deskew=%e'", m_channels[channel]->GetHwname().c_str(), - skew * SECONDS_PER_FS - ); + skew * SECONDS_PER_FS); m_transport->SendCommand(tmp); //Update cache @@ -3462,10 +3416,10 @@ void LeCroyOscilloscope::SetADCMode(size_t /*channel*/, size_t mode) m_transport->SendCommand("VBS 'app.Acquisition.Horizontal.HiResolutionModeActive = \"HDOff\"'"); //Disable all interpolation - for(size_t i=0; iSendCommand(string("VBS 'app.Acquisition.") + m_channels[i]->GetHwname() + - ".Interpolation = \"NONE\"'"); + m_transport->SendCommand( + string("VBS 'app.Acquisition.") + m_channels[i]->GetHwname() + ".Interpolation = \"NONE\"'"); } } } @@ -3479,12 +3433,12 @@ vector LeCroyOscilloscope::GetDigitalBanks() if(m_hasLA) { - for(size_t n=0; n<2; n++) + 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]); + for(size_t i = 0; i < 8; i++) + bank.push_back(m_digitalChannels[i + n * 8]); banks.push_back(bank); } @@ -3498,15 +3452,15 @@ Oscilloscope::DigitalBank LeCroyOscilloscope::GetDigitalBank(size_t channel) DigitalBank ret; if(m_hasLA) { - if(channel <= m_digitalChannels[7]->GetIndex() ) + if(channel <= m_digitalChannels[7]->GetIndex()) { - for(size_t i=0; i<8; i++) + 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]); + for(size_t i = 0; i < 8; i++) + ret.push_back(m_digitalChannels[i + 8]); } } return ret; @@ -3526,7 +3480,7 @@ float LeCroyOscilloscope::GetDigitalHysteresis(size_t channel) { lock_guard lock(m_mutex); - if(channel <= m_digitalChannels[7]->GetIndex() ) + if(channel <= m_digitalChannels[7]->GetIndex()) m_transport->SendCommand("VBS? 'return = app.LogicAnalyzer.MSxxHysteresis0'"); else m_transport->SendCommand("VBS? 'return = app.LogicAnalyzer.MSxxHysteresis1'"); @@ -3538,7 +3492,7 @@ float LeCroyOscilloscope::GetDigitalThreshold(size_t channel) { lock_guard lock(m_mutex); - if(channel <= m_digitalChannels[7]->GetIndex() ) + if(channel <= m_digitalChannels[7]->GetIndex()) m_transport->SendCommand("VBS? 'return = app.LogicAnalyzer.MSxxThreshold0'"); else m_transport->SendCommand("VBS? 'return = app.LogicAnalyzer.MSxxThreshold1'"); @@ -3551,7 +3505,7 @@ void LeCroyOscilloscope::SetDigitalHysteresis(size_t channel, float level) lock_guard lock(m_mutex); char tmp[128]; - if(channel <= m_digitalChannels[7]->GetIndex() ) + if(channel <= m_digitalChannels[7]->GetIndex()) snprintf(tmp, sizeof(tmp), "VBS? 'app.LogicAnalyzer.MSxxHysteresis0 = %e'", level); else snprintf(tmp, sizeof(tmp), "VBS? 'app.LogicAnalyzer.MSxxHysteresis1 = %e'", level); @@ -3563,7 +3517,7 @@ void LeCroyOscilloscope::SetDigitalThreshold(size_t channel, float level) lock_guard lock(m_mutex); char tmp[128]; - if(channel <= m_digitalChannels[7]->GetIndex() ) + if(channel <= m_digitalChannels[7]->GetIndex()) snprintf(tmp, sizeof(tmp), "VBS? 'app.LogicAnalyzer.MSxxThreshold0 = %e'", level); else snprintf(tmp, sizeof(tmp), "VBS? 'app.LogicAnalyzer.MSxxThreshold1 = %e'", level); @@ -3580,21 +3534,21 @@ void LeCroyOscilloscope::PullTrigger() //Figure out what kind of trigger is active. m_transport->SendCommand("VBS? 'return = app.Acquisition.Trigger.Type'"); string reply = Trim(m_transport->ReadReply()); - if (reply == "Dropout") + if(reply == "Dropout") PullDropoutTrigger(); - else if (reply == "Edge") + else if(reply == "Edge") PullEdgeTrigger(); - else if (reply == "Glitch") + else if(reply == "Glitch") PullGlitchTrigger(); - else if (reply == "Runt") + else if(reply == "Runt") PullRuntTrigger(); - else if (reply == "SlewRate") + else if(reply == "SlewRate") PullSlewRateTrigger(); - else if (reply == "UART") + else if(reply == "UART") PullUartTrigger(); - else if (reply == "Width") + else if(reply == "Width") PullPulseWidthTrigger(); - else if (reply == "Window") + else if(reply == "Window") PullWindowTrigger(); //Unrecognized trigger type @@ -3616,7 +3570,7 @@ void LeCroyOscilloscope::PullTrigger() */ void LeCroyOscilloscope::PullTriggerSource(Trigger* trig) { - m_transport->SendCommand("VBS? 'return = app.Acquisition.Trigger.Source'"); //not visible in XStream Browser? + m_transport->SendCommand("VBS? 'return = app.Acquisition.Trigger.Source'"); //not visible in XStream Browser? string reply = Trim(m_transport->ReadReply()); auto chan = GetChannelByHwName(reply); trig->SetInput(0, StreamDescriptor(chan, 0), true); @@ -3630,7 +3584,7 @@ void LeCroyOscilloscope::PullTriggerSource(Trigger* trig) void LeCroyOscilloscope::PullDropoutTrigger() { //Clear out any triggers of the wrong type - if( (m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL) ) + if((m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL)) { delete m_trigger; m_trigger = NULL; @@ -3671,7 +3625,7 @@ void LeCroyOscilloscope::PullDropoutTrigger() void LeCroyOscilloscope::PullEdgeTrigger() { //Clear out any triggers of the wrong type - if( (m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL) ) + if((m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL)) { delete m_trigger; m_trigger = NULL; @@ -3699,7 +3653,7 @@ void LeCroyOscilloscope::PullEdgeTrigger() void LeCroyOscilloscope::PullGlitchTrigger() { //Clear out any triggers of the wrong type - if( (m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL) ) + if((m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL)) { delete m_trigger; m_trigger = NULL; @@ -3738,7 +3692,7 @@ void LeCroyOscilloscope::PullGlitchTrigger() void LeCroyOscilloscope::PullPulseWidthTrigger() { //Clear out any triggers of the wrong type - if( (m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL) ) + if((m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL)) { delete m_trigger; m_trigger = NULL; @@ -3777,7 +3731,7 @@ void LeCroyOscilloscope::PullPulseWidthTrigger() void LeCroyOscilloscope::PullRuntTrigger() { //Clear out any triggers of the wrong type - if( (m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL) ) + if((m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL)) { delete m_trigger; m_trigger = NULL; @@ -3825,7 +3779,7 @@ void LeCroyOscilloscope::PullRuntTrigger() void LeCroyOscilloscope::PullSlewRateTrigger() { //Clear out any triggers of the wrong type - if( (m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL) ) + if((m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL)) { delete m_trigger; m_trigger = NULL; @@ -3873,7 +3827,7 @@ void LeCroyOscilloscope::PullSlewRateTrigger() void LeCroyOscilloscope::PullUartTrigger() { //Clear out any triggers of the wrong type - if( (m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL) ) + if((m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL)) { delete m_trigger; m_trigger = NULL; @@ -3986,7 +3940,7 @@ void LeCroyOscilloscope::PullUartTrigger() void LeCroyOscilloscope::PullWindowTrigger() { //Clear out any triggers of the wrong type - if( (m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL) ) + if((m_trigger != NULL) && (dynamic_cast(m_trigger) != NULL)) { delete m_trigger; m_trigger = NULL; @@ -4050,8 +4004,7 @@ void LeCroyOscilloscope::PushTrigger() //Source is the same for every channel char tmp[128]; - snprintf( - tmp, + snprintf(tmp, sizeof(tmp), "VBS? 'app.Acquisition.Trigger.Source = \"%s\"'", m_trigger->GetInput(0).m_channel->GetHwname().c_str()); @@ -4101,7 +4054,7 @@ void LeCroyOscilloscope::PushTrigger() m_transport->SendCommand("VBS? 'app.Acquisition.Trigger.Type = \"Window\""); PushWindowTrigger(wt); } - else if(et) //must be last + else if(et) //must be last { m_transport->SendCommand("VBS? 'app.Acquisition.Trigger.Type = \"Edge\""); PushEdgeTrigger(et, "app.Acquisition.Trigger.Edge"); @@ -4249,13 +4202,18 @@ void LeCroyOscilloscope::PushUartTrigger(UartTrigger* trig) case UartTrigger::PARITY_EVEN: m_transport->SendCommand("VBS? 'app.Acquisition.Trigger.Serial.UART.ParityType = \"Even\""); break; + + default: + LogError("Unrecognised Parity set request\n"); + break; } //Pattern length depends on the current format. //Note that the pattern length is in bytes, not bits, even though patterns are in binary. auto pattern1 = trig->GetPattern1(); char tmp[256]; - snprintf(tmp, sizeof(tmp), + snprintf(tmp, + sizeof(tmp), "VBS? 'app.Acquisition.Trigger.Serial.UART.PatternLength = \"%d\"", (int)pattern1.length() / 8); m_transport->SendCommand(tmp); @@ -4264,8 +4222,7 @@ void LeCroyOscilloscope::PushUartTrigger(UartTrigger* trig) //PatternPosition - m_transport->SendCommand( - string("VBS? 'app.Acquisition.Trigger.Serial.UART.PatternValue = \"") + pattern1 + " \"'"); + m_transport->SendCommand(string("VBS? 'app.Acquisition.Trigger.Serial.UART.PatternValue = \"") + pattern1 + " \"'"); //PatternValue2 only for Between/NotBetween switch(trig->GetCondition()) @@ -4394,12 +4351,7 @@ void LeCroyOscilloscope::PushPatternCondition(const string& path, Trigger::Condi void LeCroyOscilloscope::PushFloat(string path, float f) { char tmp[128]; - snprintf( - tmp, - sizeof(tmp), - "VBS? '%s = %e'", - path.c_str(), - f); + snprintf(tmp, sizeof(tmp), "VBS? '%s = %e'", path.c_str(), f); m_transport->SendCommand(tmp); } diff --git a/scopehal/LeCroyOscilloscope.h b/scopehal/LeCroyOscilloscope.h index 85aa5c46..28c2beee 100644 --- a/scopehal/LeCroyOscilloscope.h +++ b/scopehal/LeCroyOscilloscope.h @@ -56,8 +56,8 @@ class LeCroyOscilloscope virtual ~LeCroyOscilloscope(); //not copyable or assignable - LeCroyOscilloscope(const LeCroyOscilloscope& rhs) =delete; - LeCroyOscilloscope& operator=(const LeCroyOscilloscope& rhs) =delete; + LeCroyOscilloscope(const LeCroyOscilloscope& rhs) = delete; + LeCroyOscilloscope& operator=(const LeCroyOscilloscope& rhs) = delete; protected: void IdentifyHardware(); @@ -174,13 +174,10 @@ class LeCroyOscilloscope MODEL_WAVESURFER_3K, - MODEL_SIGLENT_SDS2000X, - MODEL_UNKNOWN }; - Model GetModelID() - { return m_modelid; } + Model GetModelID() { return m_modelid; } //Timebase virtual std::vector GetSampleRatesNonInterleaved(); @@ -256,21 +253,16 @@ class LeCroyOscilloscope bool ReadWaveformBlock(std::string& data); bool ReadWavedescs( - std::vector& wavedescs, - bool* enabled, - unsigned int& firstEnabledChannel, - bool& any_enabled); + std::vector& wavedescs, bool* enabled, unsigned int& firstEnabledChannel, bool& any_enabled); void RequestWaveforms(bool* enabled, uint32_t num_sequences, bool denabled); time_t ExtractTimestamp(unsigned char* wavedesc, double& basetime); - std::vector ProcessAnalogWaveform( - const char* data, + std::vector ProcessAnalogWaveform(const char* data, size_t datalen, std::string& wavedesc, uint32_t num_sequences, time_t ttime, double basetime, - double* wavetime - ); + double* wavetime); std::map ProcessDigitalWaveform(std::string& data); void Convert8BitSamples( @@ -289,8 +281,8 @@ class LeCroyOscilloscope 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_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; diff --git a/scopehal/OscilloscopeChannel.h b/scopehal/OscilloscopeChannel.h index c2075a24..5ea30b6c 100644 --- a/scopehal/OscilloscopeChannel.h +++ b/scopehal/OscilloscopeChannel.h @@ -150,6 +150,7 @@ class OscilloscopeChannel COUPLE_DC_1M, //1M ohm, DC coupled COUPLE_AC_1M, //1M ohm, AC coupled COUPLE_DC_50, //50 ohm, DC coupled + COUPLE_AC_50, //50 ohm, AC coupled COUPLE_GND, //tie to ground COUPLE_SYNTHETIC //channel is math, digital, or otherwise not a direct voltage measurement }; diff --git a/scopehal/SCPISocketTransport.cpp b/scopehal/SCPISocketTransport.cpp index e58edab7..e084a68b 100644 --- a/scopehal/SCPISocketTransport.cpp +++ b/scopehal/SCPISocketTransport.cpp @@ -145,6 +145,12 @@ string SCPISocketTransport::ReadReply(bool endOnSemicolon) return ret; } +void SCPISocketTransport::FlushRXBuffer(void) + +{ + m_socket.FlushRxBuffer(); +} + void SCPISocketTransport::SendRawData(size_t len, const unsigned char* buf) { m_socket.SendLooped(buf, len); diff --git a/scopehal/SCPISocketTransport.h b/scopehal/SCPISocketTransport.h index 591da99d..702fe0c5 100644 --- a/scopehal/SCPISocketTransport.h +++ b/scopehal/SCPISocketTransport.h @@ -51,6 +51,7 @@ class SCPISocketTransport : public SCPITransport virtual std::string GetConnectionString(); static std::string GetTransportName(); + virtual void FlushRXBuffer(void); virtual bool SendCommand(const std::string& cmd); virtual std::string ReadReply(bool endOnSemicolon = true); virtual size_t ReadRawData(size_t len, unsigned char* buf); diff --git a/scopehal/SCPITransport.cpp b/scopehal/SCPITransport.cpp index 2ad2fbf0..e50b9997 100644 --- a/scopehal/SCPITransport.cpp +++ b/scopehal/SCPITransport.cpp @@ -162,3 +162,9 @@ void* SCPITransport::SendCommandImmediateWithRawBlockReply(string cmd, size_t& l len = ReadRawData(len, buf); return buf; } + +void SCPITransport::FlushRXBuffer(void) + +{ + LogError("SCPITransport::FlushRXBuffer is unimplemented"); +} diff --git a/scopehal/SCPITransport.h b/scopehal/SCPITransport.h index 8638c051..5a6fa730 100644 --- a/scopehal/SCPITransport.h +++ b/scopehal/SCPITransport.h @@ -61,6 +61,7 @@ class SCPITransport { return m_netMutex; } //Immediate command API + virtual void FlushRXBuffer(void); virtual bool SendCommand(const std::string& cmd) =0; virtual std::string ReadReply(bool endOnSemicolon = true) =0; virtual size_t ReadRawData(size_t len, unsigned char* buf) =0; diff --git a/scopehal/SiglentSCPIOscilloscope.cpp b/scopehal/SiglentSCPIOscilloscope.cpp index 96142091..461023be 100644 --- a/scopehal/SiglentSCPIOscilloscope.cpp +++ b/scopehal/SiglentSCPIOscilloscope.cpp @@ -2,7 +2,14 @@ * * * ANTIKERNEL v0.1 * * * -* Copyright (c) 2012-2020 Andrew D. Zonenberg, Galen Schretlen * +* Copyright (c) 2012-2020 Andrew D. Zonenberg * +* SDS2000/5000/6000 port (c) 2021 Dave Marples. Note that this is only tested on SDS2000X+. If someone wants to loan * +* an SDS5000/6000 for testing that can be integrated. This file is * +* derrived from the LeCroy driver. * +* * +* Note that this port replaces the previous Siglent driver, which was non-functional. That is available in the git * +* archive if needed. * +* * * All rights reserved. * * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * @@ -27,9 +34,33 @@ * * ***********************************************************************************************************************/ +/* Current State + * ============= + * + * - Basic functionality for analog channels works. + * - There is no feature detection because the scope does not support *IDN? (Request made) + * - Digital channels are not implemented (code in here is leftover from LeCroy) + * - Triggers are untested. + * - Sampling lengths up to 10KSamples are supported. 50K and 100K need to be batched and will be + * horribly slow. + * + */ + #include "scopehal.h" #include "SiglentSCPIOscilloscope.h" #include "base64.h" +#include +#include +#include +#include + +#include "DropoutTrigger.h" +#include "EdgeTrigger.h" +#include "PulseWidthTrigger.h" +#include "RuntTrigger.h" +#include "SlewRateTrigger.h" +#include "UartTrigger.h" +#include "WindowTrigger.h" using namespace std; @@ -37,352 +68,2633 @@ using namespace std; // Construction / destruction SiglentSCPIOscilloscope::SiglentSCPIOscilloscope(SCPITransport* transport) - : LeCroyOscilloscope(transport) - , m_acquiredDataIsSigned(false) - , m_hasVdivAttnBug(true) + : SCPIOscilloscope(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) + , m_sampleRateValid(false) + , m_sampleRate(1) + , m_memoryDepthValid(false) + , m_memoryDepth(1) + , m_triggerOffsetValid(false) + , m_triggerOffset(0) + , m_interleaving(false) + , m_interleavingValid(false) + , m_highDefinition(false) +{ + //standard initialization + FlushConfigCache(); + IdentifyHardware(); + DetectAnalogChannels(); + SharedCtorInit(); + DetectOptions(); +} + +string SiglentSCPIOscilloscope::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); + + m_transport->FlushRXBuffer(); + m_transport->SendCommand(opString); + ret = m_transport->ReadReply(); + return ret; +} + +void SiglentSCPIOscilloscope::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->FlushRXBuffer(); + m_transport->SendCommand(opString); +#ifdef SHOW_TRANSACTIONS + printf("[%s] NoReturn\n", opString); +#endif +} + +void SiglentSCPIOscilloscope::SharedCtorInit() +{ + m_digitalChannelCount = 0; + + //Add the external trigger input + m_extTrigChannel = + new OscilloscopeChannel(this, "Ext", OscilloscopeChannel::CHANNEL_TYPE_TRIGGER, "", 1, m_channels.size(), true); + m_channels.push_back(m_extTrigChannel); + + //Desired format for waveform data + //Only use increased bit depth if the scope actually puts content there! + sendOnly(":WAVEFORM:WIDTH %s", m_highDefinition ? "WORD" : "BYTE"); + + //Clear the state-change register to we get rid of any history we don't care about + PollTrigger(); +} + +void SiglentSCPIOscilloscope::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; + m_maxBandwidth = 0; + + if(m_vendor.compare("Siglent Technologies") == 0) + { + if(m_model.compare(0, 4, "SDS2") == 0 && m_model.back() == 's') + { + m_modelid = MODEL_SIGLENT_SDS2000XP; + + m_maxBandwidth = 100; + if(m_model.compare(4, 1, "2") == 0) + m_maxBandwidth = 200; + else if(m_model.compare(4, 1, "3") == 0) + m_maxBandwidth = 350; + if(m_model.compare(4, 1, "5") == 0) + m_maxBandwidth = 500; + } + else if(m_model.compare(0, 4, "SDS5") == 0) + { + m_modelid = MODEL_SIGLENT_SDS5000X; + + m_maxBandwidth = 350; + if(m_model.compare(5, 1, "5") == 0) + m_maxBandwidth = 500; + if(m_model.compare(5, 1, "0") == 0) + m_maxBandwidth = 1000; + } + } + else + { + LogWarning("Model \"%s\" is unknown, available sample rates/memory depths may not be properly detected\n", + m_model.c_str()); + } +} + +void SiglentSCPIOscilloscope::DetectOptions() +{ + //AddDigitalChannels(16); + + /* SDS2000+ has no capability to find the options :-( */ + return; +} + +/** + @brief Creates digital channels for the oscilloscope + */ +void SiglentSCPIOscilloscope::AddDigitalChannels(unsigned int count) +{ + LogWarning("Digital channels not implemented\n"); + // Old code from LeCroy implementation + // m_hasLA = true; + // LogIndenter li; + + // m_digitalChannelCount = count; + // 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, + // OscilloscopeChannel::CHANNEL_TYPE_DIGITAL, + // GetDefaultChannelColor(m_channels.size()), + // 1, + // m_channels.size(), + // true); + // 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 SiglentSCPIOscilloscope::DetectAnalogChannels() { - if (m_modelid == MODEL_SIGLENT_SDS2000X) + int nchans = 1; + + // Char 7 of the model name is the number of channels + if(m_model.length() > 7) + { + switch(m_model[6]) + { + case '2': + nchans = 2; + break; + case '4': + nchans = 4; + break; + } + } + + for(int i = 0; i < nchans; i++) { - m_acquiredDataIsSigned = true; + //Hardware name of the channel + string chname = string("C1"); + chname[1] += i; + + //Color the channels based on Siglents standard color sequence + //yellow-pink-cyan-green-lightgreen + string color = "#ffffff"; + switch(i % 4) + { + case 0: + color = "#ffff00"; + break; + + case 1: + color = "#ff6abc"; + break; + + case 2: + color = "#00ffff"; + break; + + case 3: + color = "#00c100"; + break; + } + + //Create the channel + m_channels.push_back( + new OscilloscopeChannel(this, chname, OscilloscopeChannel::CHANNEL_TYPE_ANALOG, color, 1, i, true)); } + m_analogChannelCount = nchans; } SiglentSCPIOscilloscope::~SiglentSCPIOscilloscope() { - } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// SCPI protocol logic +// Device information string SiglentSCPIOscilloscope::GetDriverNameInternal() { return "siglent"; } -void SiglentSCPIOscilloscope::SetChannelVoltageRange(size_t i, double range) +OscilloscopeChannel* SiglentSCPIOscilloscope::GetExternalTrigger() { - lock_guard lock(m_mutex); + return m_extTrigChannel; +} - // FIXME: this assumes there are 8 vertical DIVs on the screen. Should this become a per-SKU parameter? - double wantedVdiv = range / 8; - m_channelVoltageRanges[i] = range; +void SiglentSCPIOscilloscope::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_channelDisplayNames.clear(); + m_probeIsActive.clear(); + m_sampleRateValid = false; + m_memoryDepthValid = false; + m_triggerOffsetValid = false; + m_interleavingValid = false; + m_meterModeValid = false; +} - // A Siglent SDS2304X has the 2 firmware bugs (FW 1.2.2.2 R19) - // - // When you program a VOLT_DIV of x, it actually sets a VOLT_DIV of x * probe_attenuation. - // That's the value that will show up to the scope UI and also the value that gets read back - // for VOLT_DIV?. - // So the bug only happens when sending VOLT_DIV, but not when reading it. - // - // The other bug is that, sometimes, programming VOLT_DIV just doesn't work: the value - // gets ignored. However, when you do a VOLT_DIV? immediately after a VOLT_DIV, then - // it always seems to work. - // - // It's unclear which SKUs and FW version have this bug. - // - // The following work around should be work for all scopes, whether they have the bug or not: - // 1. Program the desired value - // 2. Read it back the actual value - // 3. Program the value again, but this time adjusted by the ratio between desired and actual value. - // 4. Read back the value again to make sure it held - // - // The only disadvantage to this is that UI on the scope will update twice. - // - // A potential improvement would be to check at the start if the scope exhibits this bug... - - char cmd[128]; - snprintf(cmd, sizeof(cmd), "%s:VOLT_DIV %.4f", m_channels[i]->GetHwname().c_str(), wantedVdiv); - m_transport->SendCommand(cmd); - - snprintf(cmd, sizeof(cmd), "%s:VOLT_DIV?", m_channels[i]->GetHwname().c_str()); - m_transport->SendCommand(cmd); - - string resultStr = m_transport->ReadReply(); - double actVdiv; - sscanf(resultStr.c_str(), "%lf", &actVdiv); - - if (!m_hasVdivAttnBug) - return; +/** + @brief See what measurement capabilities we have + */ +unsigned int SiglentSCPIOscilloscope::GetMeasurementTypes() +{ + unsigned int type = 0; + return type; +} + +/** + @brief See what features we have + */ +unsigned int SiglentSCPIOscilloscope::GetInstrumentTypes() +{ + unsigned int type = INST_OSCILLOSCOPE; + if(m_hasDVM) + type |= INST_DMM; + if(m_hasFunctionGen) + type |= INST_FUNCTION; + return type; +} + +string SiglentSCPIOscilloscope::GetName() +{ + return m_model; +} + +string SiglentSCPIOscilloscope::GetVendor() +{ + return m_vendor; +} - double adjustVdiv = wantedVdiv / actVdiv; +string SiglentSCPIOscilloscope::GetSerial() +{ + return m_serial; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Channel configuration - snprintf(cmd, sizeof(cmd), "%s:VOLT_DIV %.4f", m_channels[i]->GetHwname().c_str(), wantedVdiv * adjustVdiv); - m_transport->SendCommand(cmd); +bool SiglentSCPIOscilloscope::IsChannelEnabled(size_t i) +{ + //ext trigger should never be displayed + if(i == m_extTrigChannel->GetIndex()) + return false; - snprintf(cmd, sizeof(cmd), "%s:VOLT_DIV?", m_channels[i]->GetHwname().c_str()); - m_transport->SendCommand(cmd); + //Early-out if status is in cache + { + lock_guard lock2(m_cacheMutex); + if(m_channelsEnabled.find(i) != m_channelsEnabled.end()) + return m_channelsEnabled[i]; + } - resultStr = m_transport->ReadReply(); - sscanf(resultStr.c_str(), "%lf", &actVdiv); + //Need to lock the main mutex first to prevent deadlocks + lock_guard lock(m_mutex); + lock_guard lock2(m_cacheMutex); - adjustVdiv = wantedVdiv / actVdiv; + //Analog + if(i < m_analogChannelCount) + { + //See if the channel is enabled, hide it if not + string reply = converse(":CHANNEL%d:SWITCH?", i + 1); + m_channelsEnabled[i] = (reply.find("OFF") == 0); //may have a trailing newline, ignore that + } - LogDebug("Wanted VOLT_DIV: %lf, Actual VOLT_DIV: %lf, ratio: %lf \n", wantedVdiv, actVdiv, adjustVdiv); + //Digital + // else + // { + // //See if the channel is on + // size_t nchan = i - (m_analogChannelCount + 1); + // string str = converse(":DIG:D%d?", nchan); + // if(str == "OFF") + // m_channelsEnabled[i] = false; + // else + // m_channelsEnabled[i] = true; + // } + + return m_channelsEnabled[i]; } -// Somewhat arbitrary. No header has been seen that's larger than 17... -static const int maxWaveHeaderSize = 40; +void SiglentSCPIOscilloscope::EnableChannel(size_t i) +{ + lock_guard lock(m_mutex); + + //If this is an analog channel, just toggle it + if(i < m_analogChannelCount) + { + sendOnly(":CHANNEL%d:SWITCH ON", i + 1); + } + + //Trigger can't be enabled + else if(i == m_extTrigChannel->GetIndex()) + { + } + + //Digital channel + // else + // { + // //If we have NO digital channels enabled, enable the first digital bus + // bool anyDigitalEnabled = false; + // for(auto c : m_digitalChannels) + // { + // if(m_channelsEnabled[c->GetIndex()]) + // { + // anyDigitalEnabled = true; + // break; + // } + // } + + // if(!anyDigitalEnabled) + // sendOnly(":DIGITAL:BUS1:DISP ON"); + + // //Enable this channel on the hardware + // //Note that GetHwname() returns Dn, as used by triggers, not Digitaln, as used here + // sendOnly(":DIGD%d ON", i - (m_analogChannelCount + 1)); + // } + + m_channelsEnabled[i] = true; +} -// "WF?" commands return data that starts with a header. -// On a Siglent SDS2304X, the header of "C0: WF? DESC looks like this: "ALL,#9000000346" -// On other Siglent scopes, a header may look like this: "C1:WF ALL,#9000000070" -// So the size of the header is unknown due to the variable lenghth prefix. +bool SiglentSCPIOscilloscope::CanEnableChannel(size_t /* i */) +{ + return true; +} -// Returns -1 if no valid header was seen. -// Otherwise, it returns the size of the data chunk that follows the header. -int SiglentSCPIOscilloscope::ReadWaveHeader(char *header) +void SiglentSCPIOscilloscope::DisableChannel(size_t i) { - int i = 0; + lock_guard lock(m_mutex); - // Scan the prefix until ',' is seen. - // We don't want to overfetch, so just get stuff one byte at time... - bool comma_seen = false; - while(!comma_seen && iGetIndex()) { - m_transport->ReadRawData(1, (unsigned char *)(header+i)); - comma_seen = (header[i] == ','); - ++i; } - header[i] = '\0'; - if (!comma_seen) + //Digital channel + // else + // { + // //If we have NO digital channels enabled, disable the first digital bus + // bool anyDigitalEnabled = false; + // for(auto c : m_digitalChannels) + // { + // if(m_channelsEnabled[c->GetIndex()]) + // { + // anyDigitalEnabled = true; + // break; + // } + // } + // if(!anyDigitalEnabled) + // sendOnly(":DIGITAL:BUS1:DISP OFF"); + + // //Disable this channel + // sendOnly(":DIGITAL:D%d OFF", i - (m_analogChannelCount + 1)); + // } +} + +OscilloscopeChannel::CouplingType SiglentSCPIOscilloscope::GetChannelCoupling(size_t i) +{ + if(i >= m_analogChannelCount) + return OscilloscopeChannel::COUPLE_SYNTHETIC; + + string replyType; + string replyImp; + + lock_guard lock(m_mutex); + + replyType = Trim(converse(":CHANNEL%d:COUPLING?", i + 1).substr(0, 2)); + replyImp = Trim(converse(":CHANNEL%d:IMPEDANCE?", i + 1).substr(0, 3)); + + lock_guard lock2(m_cacheMutex); + m_probeIsActive[i] = false; + + if(replyType == "AC") + return (replyImp == "FIFT") ? OscilloscopeChannel::COUPLE_AC_50 : OscilloscopeChannel::COUPLE_AC_1M; + else if(replyType == "DC") + return (replyImp == "FIFT") ? OscilloscopeChannel::COUPLE_DC_50 : OscilloscopeChannel::COUPLE_DC_1M; + else if(replyType == "GN") + return OscilloscopeChannel::COUPLE_GND; + + //invalid + LogWarning("SiglentSCPIOscilloscope::GetChannelCoupling got invalid coupling [%s] [%s]\n", + replyType.c_str(), + replyImp.c_str()); + return OscilloscopeChannel::COUPLE_SYNTHETIC; +} + +void SiglentSCPIOscilloscope::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; + + lock_guard lock(m_mutex); + switch(type) { - LogError("WaveHeader: no end of prefix seen in header (%s)\n", header); - return -1; + case OscilloscopeChannel::COUPLE_AC_1M: + sendOnly(":CHANNEL%d:COUPLING AC", i + 1); + sendOnly(":CHANNEL%d:IMPEDANCE ONEMEG", i + 1); + break; + + case OscilloscopeChannel::COUPLE_DC_1M: + sendOnly(":CHANNEL%d:COUPLING DC", i + 1); + sendOnly(":CHANNEL%d:IMPEDANCE ONEMEG", i + 1); + break; + + case OscilloscopeChannel::COUPLE_DC_50: + sendOnly(":CHANNEL%d:COUPLING DC", i + 1); + sendOnly(":CHANNEL%d:IMPEDANCE FIFTY", i + 1); + break; + + case OscilloscopeChannel::COUPLE_AC_50: + sendOnly(":CHANNEL%d:COUPLING AC", i + 1); + sendOnly(":CHANNEL%d:IMPEDANCE FIFTY", i + 1); + break; + + //treat unrecognized as ground + case OscilloscopeChannel::COUPLE_GND: + default: + sendOnly(":CHANNEL%d:COUPLING GND", i + 1); + break; } +} + +double SiglentSCPIOscilloscope::GetChannelAttenuation(size_t i) +{ + if(i > m_analogChannelCount) + return 1; - // We now expect "#9nnnnnnnnn" (11 characters), where 'n' is a digit. - int start_of_size = i; + //TODO: support ext/10 + if(i == m_extTrigChannel->GetIndex()) + return 1; - m_transport->ReadRawData(11, (unsigned char *)(header+start_of_size)); - header[start_of_size+11] = '\0'; + lock_guard lock(m_mutex); + + string reply = converse(":CHANNEL%d:PROBE?", i + 1); + + double d; + sscanf(reply.c_str(), "%lf", &d); + return d; +} - bool header_conformant = true; - header_conformant &= (header[start_of_size] == '#'); - header_conformant &= (header[start_of_size+1] == '9'); - for(i=2; i<11;++i) - header_conformant &= isdigit(header[start_of_size+i]); +void SiglentSCPIOscilloscope::SetChannelAttenuation(size_t i, double atten) +{ + if(i >= m_analogChannelCount) + return; - header[start_of_size+11] = '\0'; + //Get the old coupling value first. + //This ensures that m_probeIsActive[i] is valid + GetChannelCoupling(i); - if (!header_conformant) + //Don't allow changing attenuation on active probes { - LogError("WaveHeader: header non-conformant (%s)\n", header); - return -1; + lock_guard lock(m_cacheMutex); + if(m_probeIsActive[i]) + return; } - int data_chunk_size = atoi(&header[start_of_size+2]); + lock_guard lock(m_mutex); + sendOnly(":CHANNEL%d:PROBE %lf", i + 1, atten); +} + +vector SiglentSCPIOscilloscope::GetChannelBandwidthLimiters(size_t /*i*/) +{ + vector ret; + + //"no limit" + ret.push_back(0); + + //Supported by all models + ret.push_back(20); - LogDebug("WaveHeader: size = %d (%s)\n", data_chunk_size, header); - return data_chunk_size; + if(m_maxBandwidth > 200) + ret.push_back(200); - m_transport->ReadRawData(15, (unsigned char*)header); - header[15] = 0; + return ret; } -void SiglentSCPIOscilloscope::ReadWaveDescriptorBlock(SiglentWaveformDesc_t *descriptor, unsigned int /*channel*/) +int SiglentSCPIOscilloscope::GetChannelBandwidthLimit(size_t i) { - char header[maxWaveHeaderSize] = {0}; - int headerLength = 0; + if(i > m_analogChannelCount) + return 0; - headerLength = ReadWaveHeader(header); - LogDebug("header length: %d\n", headerLength); + lock_guard lock(m_mutex); + string reply = converse(":CHANNEL%d:BWLIMIT?", i + 1); + if(reply == "FULL") + return 0; + else if(reply == "20M") + return 20; + else if(reply == "200M") + return 200; + + LogWarning("SiglentSCPIOscilloscope::GetChannelCoupling got invalid bwlimit %s\n", reply.c_str()); + return 0; +} - if(headerLength != sizeof(struct SiglentWaveformDesc_t)) +void SiglentSCPIOscilloscope::SetChannelBandwidthLimit(size_t i, unsigned int limit_mhz) +{ + lock_guard lock(m_mutex); + switch(limit_mhz) { - LogError("Unexpected header length: %d\n", headerLength); + case 0: + sendOnly(":CHANNEL%d:BWLIMIT FULL", i + 1); + break; + + case 20: + sendOnly(":CHANNEL%d:BWLIMIT 20M", i + 1); + break; + + case 200: + sendOnly(":CHANNEL%d:BWLIMIT 200M", i + 1); + break; + + default: + LogWarning("SiglentSCPIOscilloscope::invalid bwlimit set request (%dMhz)\n", limit_mhz); } +} - m_transport->ReadRawData(sizeof(struct SiglentWaveformDesc_t), (unsigned char*)descriptor); +bool SiglentSCPIOscilloscope::CanInvert(size_t i) +{ + //All analog channels, and only analog channels, can be inverted + return (i < m_analogChannelCount); +} + +void SiglentSCPIOscilloscope::Invert(size_t i, bool invert) +{ + if(i >= m_analogChannelCount) + return; - // grab the \n - m_transport->ReadReply(); + lock_guard lock(m_mutex); + sendOnly(":CHANNEL%d:INVERT %s", i + 1, invert ? "ON" : "OFF"); } -bool SiglentSCPIOscilloscope::AcquireData() +bool SiglentSCPIOscilloscope::IsInverted(size_t i) { + if(i >= m_analogChannelCount) + return false; - LogDebug("Acquire data\n"); + lock_guard lock(m_mutex); + auto reply = Trim(converse(":CHANNEL%d:INVERT?", i + 1)); + return (reply == "ON"); +} - double start = GetTime(); +void SiglentSCPIOscilloscope::SetChannelDisplayName(size_t i, string name) +{ + auto chan = m_channels[i]; - vector wavedescs; - bool enabled[4] = {false}; - map > pending_waveforms; + //External trigger cannot be renamed in hardware. + //TODO: allow clientside renaming? + if(chan == m_extTrigChannel) + return; + //Update cache { - lock_guard lock(m_mutex); + lock_guard lock(m_cacheMutex); + m_channelDisplayNames[m_channels[i]] = name; + } + + //Update in hardware + lock_guard lock(m_mutex); + if(i < m_analogChannelCount) + { + sendOnly(":CHANNEL%ld:LABEL:TEXT \"%s\"", i + 1, name.c_str()); + sendOnly(":CHANNEL%ld:LABEL ON", i + 1); + } + else + { + sendOnly(":DIGITAL%ld:LABEL \"%s\"", i, name.c_str()); + } +} + +string SiglentSCPIOscilloscope::GetChannelDisplayName(size_t i) +{ + auto chan = m_channels[i]; + + //External trigger cannot be renamed in hardware. + //TODO: allow clientside renaming? + if(chan == m_extTrigChannel) + return m_extTrigChannel->GetHwname(); + + //Check cache first + { + lock_guard lock(m_cacheMutex); + if(m_channelDisplayNames.find(chan) != m_channelDisplayNames.end()) + return m_channelDisplayNames[chan]; + } + + lock_guard lock(m_mutex); + + //Analog and digital channels use completely different namespaces, as usual. + //Because clean, orthogonal APIs are apparently for losers? + string name; + if(i < m_analogChannelCount) + { + name = converse(":CHANNEL%d:LABEL:TEXT?", i + 1); + + // Remove "'s around the name + if(name.length() > 2) + name = name.substr(1, name.length() - 2); + } + else + { + name = converse(":DIGITAL%d:LABEL?", i - m_analogChannelCount); + } + + //Default to using hwname if no alias defined + if(name == "") + name = chan->GetHwname(); + + lock_guard lock2(m_cacheMutex); + m_channelDisplayNames[chan] = name; + + return name; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Triggering + +bool SiglentSCPIOscilloscope::IsTriggerArmed() +{ + return m_triggerArmed; +} + +Oscilloscope::TriggerMode SiglentSCPIOscilloscope::PollTrigger() + +{ + //Read the Internal State Change Register + string sinr; + lock_guard lock(m_mutex); + sinr = converse(":TRIGGER:STATUS?"); - BulkCheckChannelEnableState(); - for(unsigned int i=0; iSendCommand(m_channels[i]->GetHwname() + ":WF? DESC"); - // TODO: a bunch of error checking... - ReadWaveDescriptorBlock(wavedescs[i], i); - LogDebug("name %s, number: %u\n",wavedescs[i]->InstrumentName, - wavedescs[i]->InstrumentNumber); - } + m_triggerArmed = false; + return TRIGGER_MODE_TRIGGERED; } + else + return TRIGGER_MODE_STOP; + } + return TRIGGER_MODE_RUN; +} - // Grab the actual waveforms +int SiglentSCPIOscilloscope::ReadWaveformBlock(uint32_t maxsize, char* data) - //TODO: WFSU in outer loop and WF in inner loop - unsigned int num_sequences = 1; - for(unsigned int chanNr=0; chanNrDescName).empty()) - continue; - - //Set up the capture we're going to store our data into - AnalogWaveform* cap = new AnalogWaveform; - - //TODO: get sequence count from wavedesc - //TODO: sequence mode should be multiple captures, one per sequence, with some kind of fifo or something? - - //Parse the wavedesc headers - LogDebug(" Wavedesc len: %d\n", wavedesc->WaveDescLen); - LogDebug(" Usertext len: %d\n", wavedesc->UserTextLen); - LogDebug(" Trigtime len: %d\n", wavedesc->TriggerTimeArrayLen); - - if(wavedesc->TriggerTimeArrayLen != 0) - num_sequences = wavedesc->TriggerTimeArrayLen; - float v_gain = wavedesc->VerticalGain; - float v_off = wavedesc->VerticalOffset; - float interval = wavedesc->HorizontalInterval * FS_PER_SECOND; - double h_off = wavedesc->HorizontalOffset * FS_PER_SECOND; //fs from start of waveform to trigger - double h_off_frac = fmodf(h_off, interval); //fractional sample position, in fs - if(h_off_frac < 0) - h_off_frac = interval + h_off_frac; - cap->m_triggerPhase = h_off_frac; //TODO: handle this properly in segmented mode? - //We might have multiple offsets - //double h_unit = *reinterpret_cast(pdesc + 244); - - //Timestamp is a somewhat complex format that needs some shuffling around. - double fseconds = wavedesc->Timestamp.Seconds; - uint8_t seconds = floor(wavedesc->Timestamp.Seconds); - cap->m_startFemtoseconds = static_cast( (fseconds - seconds) * FS_PER_SECOND ); - time_t tnow = time(NULL); - struct tm* now = localtime(&tnow); - struct tm tstruc; - tstruc.tm_sec = seconds; - tstruc.tm_min = wavedesc->Timestamp.Minutes; - tstruc.tm_hour = wavedesc->Timestamp.Hours; - tstruc.tm_mday = wavedesc->Timestamp.Days; - tstruc.tm_mon = wavedesc->Timestamp.Months; - tstruc.tm_year = wavedesc->Timestamp.Years; - tstruc.tm_wday = now->tm_wday; - tstruc.tm_yday = now->tm_yday; - tstruc.tm_isdst = now->tm_isdst; - cap->m_startTimestamp = mktime(&tstruc); - cap->m_timescale = round(interval); - for(unsigned int seqNr=0; seqNrGetHwname().c_str(), seqNr); +{ + char packetSizeSequence[17]; + uint32_t getLength; - //Ask for the segment of interest - //(segment number is ignored for non-segmented waveforms) - if(num_sequences > 1) - { - //segment 0 = "all", 1 = first part of capture - m_transport->SendCommand("WAVEFORM_SETUP SP,0,NP,0,FP,0,SN," + (seqNr+1)); - } + // Get size of this sequence + m_transport->ReadRawData(16, (unsigned char*)packetSizeSequence); + packetSizeSequence[16] = 0; + LogTrace("INITIAL PACKET [%s]\n", packetSizeSequence); + getLength = atoi(&packetSizeSequence[7]); - //Read the actual waveform data - m_transport->SendCommand(m_channels[chanNr]->GetHwname() + ":WF? DAT2"); - char header[maxWaveHeaderSize] = {0}; - size_t wavesize = ReadWaveHeader(header); - uint8_t *data = new uint8_t[wavesize]; - m_transport->ReadRawData(wavesize, data); - // two \n... - m_transport->ReadReply(); - m_transport->ReadReply(); - - double trigtime = 0; - if( (num_sequences > 1) && (seqNr > 0) ) - { - //If a multi-segment capture, ask for the trigger time data - m_transport->SendCommand(m_channels[chanNr]->GetHwname() + ":WF? TIME"); + // Now get the data + m_transport->ReadRawData((getLength > maxsize) ? maxsize : getLength, (unsigned char*)data); - trigtime = ReadWaveHeader(header); - // \n - m_transport->ReadReply(); - //double trigoff = ptrigtime[1]; //offset to point 0 from trigger time - } + return getLength; +} - int64_t trigtime_samples = trigtime * FS_PER_SECOND / interval; - //int64_t trigoff_samples = trigoff * FS_PER_SECOND / interval; - //LogDebug(" Trigger time: %.3f sec (%lu samples)\n", trigtime, trigtime_samples); - //LogDebug(" Trigger offset: %.3f sec (%lu samples)\n", trigoff, trigoff_samples); +/** + @brief Optimized function for checking channel enable status en masse with less round trips to the scope + */ +void SiglentSCPIOscilloscope::BulkCheckChannelEnableState() +{ + lock_guard lock(m_cacheMutex); - //If we have samples already in the capture, stretch the final one to our trigger offset - /* - if(cap->m_samples.size()) - { - auto& last_sample = cap->m_samples[cap->m_samples.size()-1]; - last_sample.m_duration = trigtime_samples - last_sample.m_offset; - } - */ + //Check enable state in the cache. + vector uncached; + for(unsigned int i = 0; i < m_analogChannelCount; i++) + { + if(m_channelsEnabled.find(i) == m_channelsEnabled.end()) + uncached.push_back(i); + } - //Decode the samples - unsigned int num_samples = wavesize; - LogDebug("Got %u samples\n", num_samples); - cap->Resize(num_samples); - for(unsigned int i=0; im_offsets[i] = i+trigtime_samples; - cap->m_durations[i] = 1; - if (m_acquiredDataIsSigned) - { - // See programming guide, page 267: https://siglentna.com/wp-content/uploads/2020/04/ProgrammingGuide_PG01-E02C.pdf - // voltage value (V) = code value * (vdiv /25) - voffset - cap->m_samples[i] = (int8_t)(data[i]) * (v_gain / 25.0) - v_off; - } - else - cap->m_samples[i] = data[i] * v_gain - v_off; - } - } + lock_guard lock2(m_mutex); + + for(auto i : uncached) + { + string reply = converse(":CHANNEL%d:SWITCH?", i + 1); + if(reply == "OFF") + m_channelsEnabled[i] = false; + else + m_channelsEnabled[i] = true; + } + + //Check digital status + //TODO: better per-lane queries + // m_transport->SendCommand("Digital1:TRACE?"); + + // string reply = m_transport->ReadReply(); + // if(reply == "OFF") + // { + // for(size_t i=0; iGetIndex()] = false; + // } + // else + // { + // for(size_t i=0; iGetIndex()] = true; + // } +} + +bool SiglentSCPIOscilloscope::ReadWavedescs( + char wavedescs[MAX_ANALOG][WAVEDESC_SIZE], bool* enabled, unsigned int& firstEnabledChannel, bool& any_enabled) +{ + //(Note: with VICP framing we cannot use semicolons to separate commands) + BulkCheckChannelEnableState(); + for(unsigned int i = 0; i < m_analogChannelCount; i++) + { + enabled[i] = IsChannelEnabled(i); + if(enabled[i]) + any_enabled = true; + } - //Done, update the data - pending_waveforms[chanNr].push_back(cap); + //#if 0 + for(unsigned int i = 0; i < m_analogChannelCount; i++) + { + //If NO channels are enabled, query channel 1's WAVEDESC. + //Per phone conversation w/ Honam @ LeCroy apps, this will be updated even if channel is turned off + if(enabled[i] || (!any_enabled && i == 0)) + { + if(firstEnabledChannel == UINT_MAX) + firstEnabledChannel = i; } + sendOnly(":WAVEFORM:SOURCE C%d", i + 1); + sendOnly(":WAVEFORM:PREAMBLE?"); } + //#endif - //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) + for(unsigned int i = 0; i < m_analogChannelCount; i++) { - lock_guard lock(m_mutex); + if(enabled[i] || (!any_enabled && i == 0)) + { + //sendOnly(":WAVEFORM:SOURCE C%d",i+1); + //sendOnly(":WAVEFORM:PREAMBLE?"); + if(WAVEDESC_SIZE != ReadWaveformBlock(WAVEDESC_SIZE, wavedescs[i])) + LogError("ReadWaveformBlock for wavedesc %u failed\n", i); - m_transport->SendCommand("TRIG_MODE SINGLE"); - m_triggerArmed = true; + // I have no idea why this is needed, but it certainly is + m_transport->ReadReply(); + } } - m_pendingWaveformsMutex.lock(); - size_t num_pending = 1; //TODO: segmented capture support - for(size_t i = 0; i < num_pending; ++i) + return true; +} + +void SiglentSCPIOscilloscope::RequestWaveforms(bool* enabled, uint32_t num_sequences, bool denabled) +{ + //Ask for all analog waveforms + // This routine does the asking, but doesn't catch the data as it comes back + bool sent_wavetime = false; + lock_guard lock(m_mutex); + + for(unsigned int i = 0; i < m_analogChannelCount; i++) { - SequenceSet s; - for(size_t j = 0; j < m_analogChannelCount; j++) + if(enabled[i]) { - if(enabled[j]) - s[m_channels[j]] = pending_waveforms[j][i]; + sendOnly(":WAVEFORM:SOURCE C%d", i + 1); + //If a multi-segment capture, ask for the trigger time data + if((num_sequences > 1) && !sent_wavetime) + { + sendOnly("%s:HISTORY TIME?", m_channels[i]->GetHwname()); + sent_wavetime = true; + } + + //Ask for the data + sendOnly(":WAVEFORM:DATA?"); } - m_pendingWaveforms.push_back(s); } - m_pendingWaveformsMutex.unlock(); - double dt = GetTime() - start; - LogTrace("Waveform download took %.3f ms\n", dt * 1000); + //Ask for the digital waveforms + // if(denabled) + // sendOnly("Digital1:WF?"); +} - return true; +time_t SiglentSCPIOscilloscope::ExtractTimestamp(unsigned char* wavedesc, 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 + double fseconds = *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); +} + +vector SiglentSCPIOscilloscope::ProcessAnalogWaveform(const char* data, + size_t datalen, + char* wavedesc, + uint32_t num_sequences, + time_t ttime, + double basetime, + double* wavetime) +{ + vector ret; + + //Parse the wavedesc headers + auto pdesc = wavedesc; //(unsigned char*)(&wavedesc[-1]); + + //uint32_t wavedesc_len = *reinterpret_cast(pdesc + 36); + //uint32_t usertext_len = *reinterpret_cast(pdesc + 40); + + //cppcheck-suppress invalidPointerCast + float v_gain = *reinterpret_cast(pdesc + 156); + + //cppcheck-suppress invalidPointerCast + float v_off = *reinterpret_cast(pdesc + 160); + + //cppcheck-suppress invalidPointerCast + float v_probefactor = *reinterpret_cast(pdesc + 328); + + //cppcheck-suppress invalidPointerCast + float interval = *reinterpret_cast(pdesc + 176) * FS_PER_SECOND; + + //cppcheck-suppress invalidPointerCast + double h_off = *reinterpret_cast(pdesc + 180) * FS_PER_SECOND; //fs from start of waveform to trigger + + double h_off_frac = fmodf(h_off, interval); //fractional sample position, in fs + if(h_off_frac < 0) + h_off_frac = interval + h_off_frac; //double h_unit = *reinterpret_cast(pdesc + 244); + + LogTrace("\nV_Gain=%f, V_Off=%f, interval=%f, h_off=%f, h_off_frac=%f, datalen=%ld\n", + v_gain, + v_off, + interval, + h_off, + h_off_frac, + datalen); + //Raw waveform data + size_t num_samples; + if(m_highDefinition) + num_samples = datalen / 2; + else + num_samples = datalen; + size_t num_per_segment = num_samples / num_sequences; + int16_t* wdata = (int16_t*)&data[0]; + int8_t* bdata = (int8_t*)&data[0]; + + // SDS2000X+ and SDS5000X have 30 codes per div. Todo; SDS6000X has 425. + // We also need to accomodate probe attenuation here. + v_gain = v_gain * v_probefactor / 30; + + for(size_t j = 0; j < num_sequences; j++) + { + //Set up the capture we're going to store our data into + AnalogWaveform* cap = new AnalogWaveform; + cap->m_timescale = round(interval); + + cap->m_triggerPhase = h_off_frac; + cap->m_startTimestamp = ttime; + cap->m_densePacked = true; + + //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); + + //Convert raw ADC samples to volts + //TODO: Optimized AVX conversion for 16-bit samples + float* samps = reinterpret_cast(&cap->m_samples[0]); + if(m_highDefinition) + { + int16_t* base = wdata + j * num_per_segment; + + for(unsigned int k = 0; k < num_per_segment; k++) + { + cap->m_offsets[k] = k; + cap->m_durations[k] = 1; + samps[k] = base[k] * v_gain - v_off; + } + } + else + { + if(g_hasAvx2) + { + //Divide large waveforms (>1M points) into blocks and multithread them + //TODO: tune split + if(num_per_segment > 1000000) + { + //Round blocks to multiples of 32 samples for clean vectorization + size_t numblocks = omp_get_max_threads(); + size_t lastblock = numblocks - 1; + size_t blocksize = num_per_segment / numblocks; + blocksize = blocksize - (blocksize % 32); +#pragma omp parallel for + for(size_t i = 0; i < numblocks; i++) + { + //Last block gets any extra that didn't divide evenly + size_t nsamp = blocksize; + if(i == lastblock) + nsamp = num_per_segment - i * blocksize; + + Convert8BitSamplesAVX2((int64_t*)&cap->m_offsets[i * blocksize], + (int64_t*)&cap->m_durations[i * blocksize], + samps + i * blocksize, + bdata + j * num_per_segment + i * blocksize, + v_gain, + v_off, + nsamp, + i * blocksize); + } + } + + //Small waveforms get done single threaded to avoid overhead + else + { + Convert8BitSamplesAVX2((int64_t*)&cap->m_offsets[0], + (int64_t*)&cap->m_durations[0], + samps, + bdata + j * num_per_segment, + v_gain, + v_off, + num_per_segment, + 0); + } + } + else + { + Convert8BitSamples((int64_t*)&cap->m_offsets[0], + (int64_t*)&cap->m_durations[0], + samps, + bdata + j * num_per_segment, + v_gain, + v_off, + num_per_segment, + 0); + } + } + + ret.push_back(cap); + } + + return ret; +} + +/** + @brief Converts 8-bit ADC samples to floating point + */ +void SiglentSCPIOscilloscope::Convert8BitSamples( + int64_t* offs, int64_t* durs, float* pout, int8_t* pin, float gain, float offset, size_t count, int64_t ibase) +{ + for(unsigned int k = 0; k < count; k++) + { + offs[k] = ibase + k; + durs[k] = 1; + pout[k] = pin[k] * gain - offset; + } +} + +/** + @brief Optimized version of Convert8BitSamples() + */ +__attribute__((target("avx2"))) void SiglentSCPIOscilloscope::Convert8BitSamplesAVX2( + int64_t* offs, int64_t* durs, float* pout, int8_t* pin, float gain, float offset, size_t count, int64_t ibase) +{ + unsigned int end = count - (count % 32); + + int64_t __attribute__((aligned(32))) ones_x4[] = {1, 1, 1, 1}; + int64_t __attribute__((aligned(32))) fours_x4[] = {4, 4, 4, 4}; + int64_t __attribute__((aligned(32))) count_x4[] = {ibase + 0, ibase + 1, ibase + 2, ibase + 3}; + + __m256i all_ones = _mm256_load_si256(reinterpret_cast<__m256i*>(ones_x4)); + __m256i all_fours = _mm256_load_si256(reinterpret_cast<__m256i*>(fours_x4)); + __m256i counts = _mm256_load_si256(reinterpret_cast<__m256i*>(count_x4)); + + __m256 gains = {gain, gain, gain, gain, gain, gain, gain, gain}; + __m256 offsets = {offset, offset, offset, offset, offset, offset, offset, offset}; + + for(unsigned int k = 0; k < end; k += 32) + { + //Load all 32 raw ADC samples, without assuming alignment + //(on most modern Intel processors, load and loadu have same latency/throughput) + __m256i raw_samples = _mm256_loadu_si256(reinterpret_cast<__m256i*>(pin + k)); + + //Fill duration + _mm256_store_si256(reinterpret_cast<__m256i*>(durs + k), all_ones); + _mm256_store_si256(reinterpret_cast<__m256i*>(durs + k + 4), all_ones); + _mm256_store_si256(reinterpret_cast<__m256i*>(durs + k + 8), all_ones); + _mm256_store_si256(reinterpret_cast<__m256i*>(durs + k + 12), all_ones); + _mm256_store_si256(reinterpret_cast<__m256i*>(durs + k + 16), all_ones); + _mm256_store_si256(reinterpret_cast<__m256i*>(durs + k + 20), all_ones); + _mm256_store_si256(reinterpret_cast<__m256i*>(durs + k + 24), all_ones); + _mm256_store_si256(reinterpret_cast<__m256i*>(durs + k + 28), all_ones); + + //Extract the low and high 16 samples from the block + __m128i block01_x8 = _mm256_extracti128_si256(raw_samples, 0); + __m128i block23_x8 = _mm256_extracti128_si256(raw_samples, 1); + + //Swap the low and high halves of these vectors + //Ugly casting needed because all permute instrinsics expect float/double datatypes + __m128i block10_x8 = _mm_castpd_si128(_mm_permute_pd(_mm_castsi128_pd(block01_x8), 1)); + __m128i block32_x8 = _mm_castpd_si128(_mm_permute_pd(_mm_castsi128_pd(block23_x8), 1)); + + //Divide into blocks of 8 samples and sign extend to 32 bit + __m256i block0_int = _mm256_cvtepi8_epi32(block01_x8); + __m256i block1_int = _mm256_cvtepi8_epi32(block10_x8); + __m256i block2_int = _mm256_cvtepi8_epi32(block23_x8); + __m256i block3_int = _mm256_cvtepi8_epi32(block32_x8); + + //Fill offset + _mm256_store_si256(reinterpret_cast<__m256i*>(offs + k), counts); + counts = _mm256_add_epi64(counts, all_fours); + _mm256_store_si256(reinterpret_cast<__m256i*>(offs + k + 4), counts); + counts = _mm256_add_epi64(counts, all_fours); + _mm256_store_si256(reinterpret_cast<__m256i*>(offs + k + 8), counts); + counts = _mm256_add_epi64(counts, all_fours); + _mm256_store_si256(reinterpret_cast<__m256i*>(offs + k + 12), counts); + counts = _mm256_add_epi64(counts, all_fours); + _mm256_store_si256(reinterpret_cast<__m256i*>(offs + k + 16), counts); + counts = _mm256_add_epi64(counts, all_fours); + _mm256_store_si256(reinterpret_cast<__m256i*>(offs + k + 20), counts); + counts = _mm256_add_epi64(counts, all_fours); + _mm256_store_si256(reinterpret_cast<__m256i*>(offs + k + 24), counts); + counts = _mm256_add_epi64(counts, all_fours); + _mm256_store_si256(reinterpret_cast<__m256i*>(offs + k + 28), counts); + counts = _mm256_add_epi64(counts, all_fours); + + //Convert the 32-bit int blocks to float. + //Apparently there's no direct epi8 to ps conversion instruction. + __m256 block0_float = _mm256_cvtepi32_ps(block0_int); + __m256 block1_float = _mm256_cvtepi32_ps(block1_int); + __m256 block2_float = _mm256_cvtepi32_ps(block2_int); + __m256 block3_float = _mm256_cvtepi32_ps(block3_int); + + //Woo! We've finally got floating point data. Now we can do the fun part. + block0_float = _mm256_mul_ps(block0_float, gains); + block1_float = _mm256_mul_ps(block1_float, gains); + block2_float = _mm256_mul_ps(block2_float, gains); + block3_float = _mm256_mul_ps(block3_float, gains); + + block0_float = _mm256_sub_ps(block0_float, offsets); + block1_float = _mm256_sub_ps(block1_float, offsets); + block2_float = _mm256_sub_ps(block2_float, offsets); + block3_float = _mm256_sub_ps(block3_float, offsets); + + //All done, store back to the output buffer + _mm256_store_ps(pout + k, block0_float); + _mm256_store_ps(pout + k + 8, block1_float); + _mm256_store_ps(pout + k + 16, block2_float); + _mm256_store_ps(pout + k + 24, block3_float); + } + + //Get any extras we didn't get in the SIMD loop + for(unsigned int k = end; k < count; k++) + { + offs[k] = ibase + k; + durs[k] = 1; + pout[k] = pin[k] * gain - offset; + } +} + +map SiglentSCPIOscilloscope::ProcessDigitalWaveform(string& data) +{ + map ret; + + // Digital channels not yet implemented + return ret; + + + //See what channels are enabled + string tmp = data.substr(data.find("SelectedLines=") + 14); + tmp = tmp.substr(0, 16); + bool enabledChannels[16]; + for(int i = 0; i < 16; i++) + enabledChannels[i] = (tmp[i] == '1'); + + //Quick and dirty string searching. We only care about a small fraction of the XML + //so no sense bringing in a full parser. + tmp = data.substr(data.find("") + 12); + tmp = tmp.substr(0, tmp.find("")); + float interval = atof(tmp.c_str()) * FS_PER_SECOND; + //LogDebug("Sample interval: %.2f fs\n", interval); + + tmp = data.substr(data.find("") + 12); + tmp = tmp.substr(0, tmp.find("")); + size_t num_samples = atoi(tmp.c_str()); + //LogDebug("Expecting %d samples\n", num_samples); + + //Extract the raw trigger timestamp (nanoseconds since Jan 1 2000) + tmp = data.substr(data.find("") + 16); + tmp = tmp.substr(0, tmp.find("")); + int64_t timestamp; + if(1 != sscanf(tmp.c_str(), "%ld", ×tamp)) + return ret; + + //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); + + //Convert Jan 1 2000 in the client's local time zone (assuming this is the same as instrument time) to Unix time. + //Note that the instrument time zone conversion seems to be broken and not handle DST offsets right. + //Move the epoch by an hour if we're currently in DST to compensate. + tm epoch; + epoch.tm_sec = 0; + epoch.tm_min = 0; + epoch.tm_hour = 0; + epoch.tm_mday = 1; + epoch.tm_mon = 0; + epoch.tm_year = 100; + epoch.tm_wday = 6; //Jan 1 2000 was a Saturday + epoch.tm_yday = 0; + epoch.tm_isdst = now.tm_isdst; + time_t epoch_stamp = mktime(&epoch); + + //Pull out nanoseconds from the timestamp and convert to femtoseconds since that's the scopehal fine time unit + const int64_t ns_per_sec = 1000000000; + int64_t start_ns = timestamp % ns_per_sec; + int64_t start_fs = 1000000 * start_ns; + int64_t start_sec = (timestamp - start_ns) / ns_per_sec; + time_t start_time = epoch_stamp + start_sec; + + //Pull out the actual binary data (Base64 coded) + tmp = data.substr(data.find("") + 12); + tmp = tmp.substr(0, tmp.find("")); + + //Decode the base64 + base64_decodestate bstate; + base64_init_decodestate(&bstate); + unsigned char* block = new unsigned char[tmp.length()]; //base64 is smaller than plaintext, leave room + base64_decode_block(tmp.c_str(), tmp.length(), (char*)block, &bstate); + + //We have each channel's data from start to finish before the next (no interleaving). + //TODO: Multithread across waveforms + unsigned int icapchan = 0; + for(unsigned int i = 0; i < m_digitalChannelCount; i++) + { + if(enabledChannels[i]) + { + DigitalWaveform* cap = new DigitalWaveform; + cap->m_timescale = interval; + cap->m_densePacked = true; + + //Capture timestamp + cap->m_startTimestamp = start_time; + cap->m_startFemtoseconds = start_fs; + + //Preallocate memory assuming no deduplication possible + cap->Resize(num_samples); + + //Save the first sample (can't merge with sample -1 because that doesn't exist) + size_t base = icapchan * num_samples; + size_t k = 0; + cap->m_offsets[0] = 0; + cap->m_durations[0] = 1; + cap->m_samples[0] = block[base]; + + //Read and de-duplicate the other samples + //TODO: can we vectorize this somehow? + bool last = block[base]; + for(size_t j = 1; j < num_samples; j++) + { + bool sample = block[base + j]; + + //Deduplicate consecutive samples with same value + //FIXME: temporary workaround for rendering bugs + //if(last == sample) + if((last == sample) && ((j + 3) < num_samples)) + cap->m_durations[k]++; + + //Nope, it toggled - store the new value + else + { + k++; + cap->m_offsets[k] = j; + cap->m_durations[k] = 1; + cap->m_samples[k] = sample; + last = sample; + } + } + + //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(); + + //See how much space we saved + /* + LogDebug("%s: %zu samples deduplicated to %zu (%.1f %%)\n", + m_digitalChannels[i]->GetDisplayName().c_str(), + num_samples, + k, + (k * 100.0f) / num_samples); + */ + + //Done, save data and go on to next + ret[m_digitalChannels[i]->GetIndex()] = cap; + icapchan++; + } + + //No data here for us! + else + ret[m_digitalChannels[i]->GetIndex()] = NULL; + } + delete[] block; + return ret; +} + +bool SiglentSCPIOscilloscope::AcquireData() +{ + //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; + bool denabled = false; + string wavetime; + bool enabled[8] = {false}; + double* pwtime = NULL; + char tmp[128]; + + //Acquire the data (but don't parse it) + { + lock_guard lock(m_mutex); + + //Get the wavedescs for all channels + unsigned int firstEnabledChannel = UINT_MAX; + bool any_enabled = true; + + if(!ReadWavedescs(m_wavedescs, enabled, firstEnabledChannel, any_enabled)) + return false; + + //Grab the WAVEDESC from the first enabled channel + unsigned char* pdesc = NULL; + for(unsigned int i = 0; i < m_analogChannelCount; i++) + { + if(enabled[i] || (!any_enabled && i == 0)) + { + pdesc = (unsigned char*)(&m_wavedescs[i][0]); + break; + } + } + + //See if any digital channels are enabled + if(m_digitalChannelCount > 0) + { + m_cacheMutex.lock(); + for(size_t i = 0; i < m_digitalChannels.size(); i++) + { + if(m_channelsEnabled[m_digitalChannels[i]->GetIndex()]) + { + denabled = true; + break; + } + } + m_cacheMutex.unlock(); + } + + //Pull sequence count out of the WAVEDESC if we have analog channels active + if(pdesc) + { + uint32_t trigtime_len = *reinterpret_cast(pdesc + 48); + if(trigtime_len > 0) + num_sequences = trigtime_len / 16; + } + + //No WAVEDESCs, look at digital channels + else + { + //TODO: support sequence capture of digital channels if the instrument supports this + //(need to look into it) + if(denabled) + num_sequences = 1; + + //no enabled channels. abort + else + return false; + } + + //Ask for every enabled channel up front, so the scope can send us the next while we parse the first + RequestWaveforms(enabled, num_sequences, denabled); + + if(pdesc) + { + // THIS SECTION IS UNTESTED + //Figure out when the first trigger happened. + //Read the timestamps if we're doing segmented capture + ttime = ExtractTimestamp(pdesc, basetime); + if(num_sequences > 1) + wavetime = m_transport->ReadReply(); + pwtime = reinterpret_cast(&wavetime[16]); //skip 16-byte SCPI header + + //Read the data from each analog waveform + for(unsigned int i = 0; i < m_analogChannelCount; i++) + { + // m_transport->SendCommand(":WAVEFORM:SOURCE "+to_string(i+1)); + // m_transport->SendCommand(":WAVEFORM:DATA?"); + if(enabled[i]) + { + m_analogWaveformDataSize[i] = ReadWaveformBlock(WAVEFORM_SIZE, m_analogWaveformData[i]); + // This is the 0x0a0a at the end + m_transport->ReadRawData(2, (unsigned char*)tmp); + } + } + } + + //Read the data from the digital waveforms, if enabled + if(denabled) + { + if(!ReadWaveformBlock(WAVEFORM_SIZE, m_digitalWaveformDataBytes)) + { + LogDebug("failed to download digital waveform\n"); + return 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) + { + // lock_guard lock(m_mutex); + sendOnly(":TRIGGER:MODE SINGLE"); + m_triggerArmed = true; + } + + //Process analog waveforms + vector> waveforms; + waveforms.resize(m_analogChannelCount); + for(unsigned int i = 0; i < m_analogChannelCount; i++) + { + if(enabled[i]) + { + waveforms[i] = ProcessAnalogWaveform(&m_analogWaveformData[i][0], + m_analogWaveformDataSize[i], + &m_wavedescs[i][0], + num_sequences, + ttime, + basetime, + pwtime); + } + } + + //Save analog waveform data + for(unsigned int i = 0; i < m_analogChannelCount; i++) + { + if(!enabled[i]) + continue; + + //Done, update the data + for(size_t j = 0; j < num_sequences; j++) + pending_waveforms[i].push_back(waveforms[i][j]); + } + + //TODO: proper support for sequenced capture when digital channels are active + // if(denabled) + // { + // //This is a weird XML-y format but I can't find any other way to get it :( + // map digwaves = ProcessDigitalWaveform(m_digitalWaveformData); + + // //Done, update the data + // for(auto it : digwaves) + // pending_waveforms[it.first].push_back(it.second); + // } + + //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_channels.size(); j++) + { + if(pending_waveforms.find(j) != pending_waveforms.end()) + s[m_channels[j]] = pending_waveforms[j][i]; + } + m_pendingWaveforms.push_back(s); + } + m_pendingWaveformsMutex.unlock(); + + double dt = GetTime() - start; + LogTrace("Waveform download and processing took %.3f ms\n", dt * 1000); + + return true; +} + +void SiglentSCPIOscilloscope::Start() +{ + lock_guard lock(m_mutex); + sendOnly(":TRIGGER:MODE STOP"); + sendOnly(":TRIGGER:MODE SINGLE"); //always do single captures, just re-trigger + m_triggerArmed = true; + m_triggerOneShot = false; +} + +void SiglentSCPIOscilloscope::StartSingleTrigger() +{ + lock_guard lock(m_mutex); + //LogDebug("Start single trigger\n"); + sendOnly(":TRIGGER:MODE STOP"); + sendOnly(":TRIGGER:MODE SINGLE"); + m_triggerArmed = true; + m_triggerOneShot = true; +} + +void SiglentSCPIOscilloscope::Stop() +{ + { + lock_guard lock(m_mutex); + sendOnly(":TRIGGER:MODE 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(); +} + +double SiglentSCPIOscilloscope::GetChannelOffset(size_t i) +{ + //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]; + } + + lock_guard lock2(m_mutex); + + string reply = converse(":CHANNEL%ld:OFFSET?", i + 1); + double offset; + sscanf(reply.c_str(), "%lf", &offset); + + lock_guard lock(m_cacheMutex); + m_channelOffsets[i] = offset; + return offset; +} + +void SiglentSCPIOscilloscope::SetChannelOffset(size_t i, double offset) +{ + //not meaningful for trigger or digital channels + if(i > m_analogChannelCount) + return; + + { + lock_guard lock2(m_mutex); + sendOnly(":CHANNEL%ld:OFFSET %e", i + 1, offset); + } + + lock_guard lock(m_cacheMutex); + m_channelOffsets[i] = offset; +} + +double SiglentSCPIOscilloscope::GetChannelVoltageRange(size_t i) +{ + //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]; + } + + lock_guard lock2(m_mutex); + + string reply = converse(":CHANNEL%d:SCALE?", i + 1); + double volts_per_div; + sscanf(reply.c_str(), "%lf", &volts_per_div); + + double v = volts_per_div * 8; //plot is 8 divisions high + lock_guard lock(m_cacheMutex); + m_channelVoltageRanges[i] = v; + return v; +} + +void SiglentSCPIOscilloscope::SetChannelVoltageRange(size_t i, double range) +{ + lock_guard lock(m_mutex); + + double vdiv = range / 8; + m_channelVoltageRanges[i] = range; + + sendOnly(":CHANNEL%ld:SCALE %.4f", i + 1, vdiv); +} + +vector SiglentSCPIOscilloscope::GetSampleRatesNonInterleaved() +{ + vector ret; + ret = {10 * 1000, + 20 * 1000, + 50 * 1000, + 100 * 1000, + 200 * 1000, + 500 * 1000, + 1 * 1000 * 1000, + 2 * 1000 * 1000, + 5 * 1000 * 1000, + 10 * 1000 * 1000, + 20 * 1000 * 1000, + 50 * 1000 * 1000, + 100 * 1000 * 1000, + 200 * 1000 * 1000, + 500 * 1000 * 1000, + 1 * 1000 * 1000 * 1000, + 2 * 1000 * 1000 * 1000}; + return ret; +} + +vector SiglentSCPIOscilloscope::GetSampleRatesInterleaved() +{ + vector ret = {}; + GetSampleRatesNonInterleaved(); + return ret; +} + +vector SiglentSCPIOscilloscope::GetSampleDepthsNonInterleaved() +{ + vector ret = {}; + return ret; +} + +vector SiglentSCPIOscilloscope::GetSampleDepthsInterleaved() +{ + vector ret = {}; + return ret; +} + +set SiglentSCPIOscilloscope::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(m_channels[0], m_channels[1])); + if(m_analogChannelCount > 2) + ret.emplace(InterleaveConflict(m_channels[2], m_channels[3])); + + return ret; +} + +uint64_t SiglentSCPIOscilloscope::GetSampleRate() +{ + if(!m_sampleRateValid) + { + lock_guard lock(m_mutex); + string reply = converse(":ACQUIRE:SRATE?"); + sscanf(reply.c_str(), "%ld", &m_sampleRate); + m_sampleRateValid = true; + } + + return m_sampleRate; +} + +uint64_t SiglentSCPIOscilloscope::GetSampleDepth() +{ + if(!m_memoryDepthValid) + { + //:AQUIRE:MDEPTH can sometimes return incorrect values! It returns the *cap* on memory depth, + //not the *actual* memory depth. + + //What you see below is the only observed method that seems to reliably get the *actual* memory depth. + lock_guard lock(m_mutex); + string reply = converse(":ACQUIRE:MDEPTH?"); + double capture_len_fs = Unit(Unit::UNIT_FS).ParseString(reply); + int64_t fs_per_sample = FS_PER_SECOND / GetSampleRate(); + + m_memoryDepth = (capture_len_fs + (fs_per_sample / 2)) / fs_per_sample; + m_memoryDepthValid = true; + } + + return m_memoryDepth; +} + +void SiglentSCPIOscilloscope::SetSampleDepth(uint64_t depth) +{ + lock_guard lock(m_mutex); + + switch(depth) + { + case 10000: + sendOnly("ACQUIRE:MDEPTH 10k"); + break; + case 20000: + sendOnly("ACQUIRE:MDEPTH 20k"); + break; + case 100000: + sendOnly("ACQUIRE:MDEPTH 100k"); + break; + case 200000: + sendOnly("ACQUIRE:MDEPTH 200k"); + break; + case 1000000: + sendOnly("ACQUIRE:MDEPTH 1M"); + break; + case 2000000: + sendOnly("ACQUIRE:MDEPTH 2M"); + break; + case 10000000: + sendOnly("ACQUIRE:MDEPTH 10M"); + break; + + // We don't yet support memory depths that need to be transferred in chunks + case 20000000: + //sendOnly("ACQUIRE:MDEPTH 20M"); + // break; + case 50000000: + // sendOnly("ACQUIRE:MDEPTH 50M"); + // break; + case 100000000: + // sendOnly("ACQUIRE:MDEPTH 100M"); + // break; + case 200000000: + // sendOnly("ACQUIRE:MDEPTH 200M"); + // break; + default: + LogError("Invalid memory depth for channel: %lu\n", depth); + } + + m_memoryDepthValid = false; +} + +void SiglentSCPIOscilloscope::SetSampleRate(uint64_t rate) +{ + lock_guard lock(m_mutex); + m_sampleRate = rate; + m_sampleRateValid = false; + + m_memoryDepthValid = false; + double sampletime = GetSampleDepth() / (double)rate; + sendOnly(":TIMEBASE:SCALE %e", sampletime / 10); + m_memoryDepthValid = false; +} + +void SiglentSCPIOscilloscope::EnableTriggerOutput() +{ + LogWarning("EnableTriggerOutput not implemented\n"); +} + +void SiglentSCPIOscilloscope::SetUseExternalRefclk(bool /*external*/) +{ + LogWarning("SetUseExternalRefclk not implemented\n"); +} + +void SiglentSCPIOscilloscope::SetTriggerOffset(int64_t offset) +{ + lock_guard lock(m_mutex); + + //Siglents 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:DELAY %e", (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 SiglentSCPIOscilloscope::GetTriggerOffset() +{ + //Early out if the value is in cache + { + lock_guard lock(m_cacheMutex); + if(m_triggerOffsetValid) + return m_triggerOffset; + } + + string reply; + { + lock_guard lock(m_mutex); + reply = converse(":TIMEBASE:DELAY?"); + } + + 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 SiglentSCPIOscilloscope::SetDeskewForChannel(size_t channel, int64_t skew) +{ + //Cannot deskew digital/trigger channels + if(channel >= m_analogChannelCount) + return; + + lock_guard lock(m_mutex); + + sendOnly(":CHANNEL%ld:SKEW %e", channel, skew * SECONDS_PER_FS); + + //Update cache + lock_guard lock2(m_cacheMutex); + m_channelDeskew[channel] = skew; +} + +int64_t SiglentSCPIOscilloscope::GetDeskewForChannel(size_t channel) +{ + //Cannot deskew digital/trigger channels + if(channel >= m_analogChannelCount) + 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 + lock_guard lock(m_mutex); + string reply = converse(":CHANNEL%ld:SKEW?", channel + 1); + + //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 SiglentSCPIOscilloscope::IsInterleaving() +{ + LogWarning("IsInterleaving is not implemented\n"); + return false; +} + +bool SiglentSCPIOscilloscope::SetInterleaving(bool /* combine*/) +{ + LogWarning("SetInterleaving is not implemented\n"); + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Analog bank configuration + +bool SiglentSCPIOscilloscope::IsADCModeConfigurable() +{ + return false; +} + +vector SiglentSCPIOscilloscope::GetADCModeNames(size_t /*channel*/) +{ + vector v; + LogWarning("GetADCModeNames is not implemented\n"); + return v; +} + +size_t SiglentSCPIOscilloscope::GetADCMode(size_t /*channel*/) +{ + return 0; +} + +void SiglentSCPIOscilloscope::SetADCMode(size_t /*channel*/, size_t /*mode*/) +{ + return; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Logic analyzer configuration + +vector SiglentSCPIOscilloscope::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 SiglentSCPIOscilloscope::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 SiglentSCPIOscilloscope::IsDigitalHysteresisConfigurable() +{ + return false; +} + +bool SiglentSCPIOscilloscope::IsDigitalThresholdConfigurable() +{ + return true; +} + +float SiglentSCPIOscilloscope::GetDigitalHysteresis(size_t /*channel*/) +{ + LogWarning("GetDigitalHysteresis is not implemented\n"); + return 0; +} + +float SiglentSCPIOscilloscope::GetDigitalThreshold(size_t channel) +{ + lock_guard lock(m_mutex); + + return atof(converse(":DIGITAL:THRESHOLD%d?", (channel / 8) + 1).c_str()); +} + +void SiglentSCPIOscilloscope::SetDigitalHysteresis(size_t /*channel*/, float /*level*/) +{ + LogWarning("SetDigitalHysteresis is not implemented\n"); +} + +void SiglentSCPIOscilloscope::SetDigitalThreshold(size_t channel, float level) +{ + lock_guard lock(m_mutex); + sendOnly(":DIGITAL:THRESHOLD%d %e", (channel / 8) + 1, level); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Triggering + +void SiglentSCPIOscilloscope::PullTrigger() +{ + lock_guard lock(m_mutex); + + //Figure out what kind of trigger is active. + string 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 == "UART") + PullUartTrigger(); + 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); + + //TODO: holdoff +} + +/** + @brief Reads the source of a trigger from the instrument + */ +void SiglentSCPIOscilloscope::PullTriggerSource(Trigger* trig, string triggerModeName) +{ + string reply = Trim(converse(":TRIGGER:%s:SOURCE?", triggerModeName.c_str())); + auto chan = GetChannelByHwName(reply); + 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 SiglentSCPIOscilloscope::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); + + //Level + dt->SetLevel(stof(converse(":TRIGGER:DROPOUT:LEVEL?"))); + + //Dropout time + Unit fs(Unit::UNIT_FS); + 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 + dt->SetResetType(DropoutTrigger::RESET_NONE); +} + +/** + @brief Reads settings for an edge trigger from the instrument + */ +void SiglentSCPIOscilloscope::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 SiglentSCPIOscilloscope::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); + + //Level + pt->SetLevel(stof(converse(":TRIGGER:INTERVAL:LEVEL?'"))); + + //Condition + pt->SetCondition(GetCondition(converse(":TRIGGER:INTERVAL:LIMIT?"))); + + //Min range + Unit fs(Unit::UNIT_FS); + 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 SiglentSCPIOscilloscope::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); + + //Lower bound + Unit v(Unit::UNIT_VOLTS); + rt->SetLowerBound(v.ParseString(converse(":TRIGGER:RUNT:LLEVEL?"))); + + //Upper bound + rt->SetUpperBound(v.ParseString(converse(":TRIGGER:RUNT:HLEVEL?"))); + + //Lower interval + Unit fs(Unit::UNIT_FS); + rt->SetLowerInterval(fs.ParseString(converse(":TRIGGER:RUNT:TLOWER?"))); + + //Upper interval + rt->SetUpperInterval(fs.ParseString(converse(":TRIGGER:RUNT:TUPPER?"))); + + //Slope + auto 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(m_transport->ReadReply())); +} + +/** + @brief Reads settings for a slew rate trigger from the instrument + */ +void SiglentSCPIOscilloscope::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); + + //Lower bound + Unit v(Unit::UNIT_VOLTS); + st->SetLowerBound(v.ParseString(converse(":TRIGGER:SLOPE:TLEVEL?"))); + + //Upper bound + st->SetUpperBound(v.ParseString(converse(":TRIGGER:SLOPE:HLEVEL?"))); + + //Lower interval + Unit fs(Unit::UNIT_FS); + st->SetLowerInterval(fs.ParseString(converse(":TRIGGER:SLOPE:TLOWER?"))); + + //Upper interval + st->SetUpperInterval(fs.ParseString(converse(":TRIGGER:SLOPE:TUPPER?"))); + + //Slope + auto reply = Trim(converse("TRIGGER:SLOPE:SLOPE?")); + if(reply == "POSitive") + st->SetSlope(SlewRateTrigger::EDGE_RISING); + else if(reply == "NEGative") + st->SetSlope(SlewRateTrigger::EDGE_FALLING); + + //Condition + //st->SetCondition(GetCondition(m_transport->ReadReply())); +} + +/** + @brief Reads settings for a UART trigger from the instrument + */ +void SiglentSCPIOscilloscope::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); + + //Bit rate + ut->SetBitRate(stoi(converse(":TRIGGER:UART:BAUD?"))); + + //Level + ut->SetLevel(stof(converse(":TRIGGER:UART:LIMIT?"))); + + //Parity + auto 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) + string p1 = Trim(converse(":TRIGGER:UART:DATA?")); + ut->SetPatterns(p1, "", true); +} + +/** + @brief Reads settings for a window trigger from the instrument + */ +void SiglentSCPIOscilloscope::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); + + //Lower bound + Unit v(Unit::UNIT_VOLTS); + 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 SiglentSCPIOscilloscope::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_ANY); + else + LogWarning("Unknown trigger slope %s\n", reply.c_str()); +} + +/** + @brief Parses a trigger condition + */ +Trigger::Condition SiglentSCPIOscilloscope::GetCondition(string reply) +{ + reply = Trim(reply); + + if(reply == "LessThan") + return Trigger::CONDITION_LESS; + else if(reply == "GreaterThan") + return Trigger::CONDITION_GREATER; + else if(reply == "InRange") + return Trigger::CONDITION_BETWEEN; + else if(reply == "OutOfRange") + return Trigger::CONDITION_NOT_BETWEEN; + + //unknown + return Trigger::CONDITION_LESS; +} + +void SiglentSCPIOscilloscope::PushTrigger() +{ + lock_guard lock(m_mutex); + + 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 C%d", m_trigger->GetInput(0).m_channel->GetIndex() + 1); + PushDropoutTrigger(dt); + } + else if(pt) + { + sendOnly(":TRIGGER:TYPE INTERVAL"); + sendOnly(":TRIGGER:INTERVAL:SOURCE C%d", m_trigger->GetInput(0).m_channel->GetIndex() + 1); + PushPulseWidthTrigger(pt); + } + else if(rt) + { + sendOnly(":TRIGGER:TYPE RUNT"); + sendOnly(":TRIGGER:RUNT:SOURCE C%d", m_trigger->GetInput(0).m_channel->GetIndex() + 1); + PushRuntTrigger(rt); + } + else if(st) + { + sendOnly(":TRIGGER:TYPE SLOPE"); + sendOnly(":TRIGGER:SLOPE:SOURCE C%d", m_trigger->GetInput(0).m_channel->GetIndex() + 1); + PushSlewRateTrigger(st); + } + else if(ut) + { + sendOnly(":TRIGGER:TYPE UART"); + // TODO: Validate these trigger allocations + sendOnly(":TRIGGER:UART:RXSOURCE C%d", m_trigger->GetInput(0).m_channel->GetIndex() + 1); + sendOnly(":TRIGGER:UART:TXSOURCE C%d", m_trigger->GetInput(1).m_channel->GetIndex() + 1); + PushUartTrigger(ut); + } + else if(wt) + { + sendOnly(":TRIGGER:TYPE WINDOW"); + sendOnly(":TRIGGER:WINDOW:SOURCE C%d", m_trigger->GetInput(0).m_channel->GetIndex() + 1); + 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 C%d", m_trigger->GetInput(0).m_channel->GetIndex() + 1); + PushEdgeTrigger(et, "EDGE"); + } + + else + LogWarning("Unknown trigger type (not an edge)\n"); +} + +/** + @brief Pushes settings for a dropout trigger to the instrument + */ +void SiglentSCPIOscilloscope::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"); +} + +/** + @brief Pushes settings for an edge trigger to the instrument + */ +void SiglentSCPIOscilloscope::PushEdgeTrigger(EdgeTrigger* trig, const std::string trigType) +{ + //Level + sendOnly(":TRIGGER:%s:LEVEL %e", trigType.c_str(), trig->GetLevel()); + + //Slope + 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()); + return; + } +} + +/** + @brief Pushes settings for a pulse width trigger to the instrument + */ +void SiglentSCPIOscilloscope::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 SiglentSCPIOscilloscope::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->GetUpperBound()); + PushFloat(":TRIGGER:RUNT:HLEVEL", trig->GetLowerBound()); + + sendOnly(":TRIGGER:RUNT:POLARITY %s", (trig->GetSlope() == RuntTrigger::EDGE_RISING) ? "RISING" : "FALLING"); +} + +/** + @brief Pushes settings for a slew rate trigger to the instrument + */ +void SiglentSCPIOscilloscope::PushSlewRateTrigger(SlewRateTrigger* trig) +{ + PushCondition(":TRIGGER:SLEW", trig->GetCondition()); + PushFloat(":TRIGGER:SLEW:TUPPER", trig->GetUpperInterval() * SECONDS_PER_FS); + PushFloat(":TRIGGER:SLEW:TLOWER", trig->GetLowerInterval() * SECONDS_PER_FS); + PushFloat(":TRIGGER:SLEW:HLEVEL", trig->GetUpperBound()); + PushFloat(":TRIGGER:SLEW:LLEVEL", trig->GetLowerBound()); + + sendOnly(":TRIGGER:SLEW:SLOPE %s", (trig->GetSlope() == SlewRateTrigger::EDGE_RISING) ? "POSITIVE" : "NEGATIVE"); +} + +/** + @brief Pushes settings for a UART trigger to the instrument + */ +void SiglentSCPIOscilloscope::PushUartTrigger(UartTrigger* trig) +{ + //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. + auto 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"); + + auto 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; + } + + //UARTCondition + //ViewingMode +} + +/** + @brief Pushes settings for a window trigger to the instrument + */ +void SiglentSCPIOscilloscope::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 SiglentSCPIOscilloscope::PushCondition(const string& path, Trigger::Condition cond) +{ + switch(cond) + { + case Trigger::CONDITION_LESS: + sendOnly("%s:LIMIT LESSTHAN", path); + break; + + case Trigger::CONDITION_GREATER: + sendOnly("%s:LIMIT GREATERTHAN", path); + break; + + case Trigger::CONDITION_BETWEEN: + sendOnly("%s:LIMIT INNER", path); + break; + + case Trigger::CONDITION_NOT_BETWEEN: + sendOnly("%s:LIMIT OUTER", path); + break; + + //Other values are not legal here, it seems + default: + break; + } +} + +void SiglentSCPIOscilloscope::PushFloat(string path, float f) +{ + sendOnly("%s = %e", path.c_str(), f); +} + +vector SiglentSCPIOscilloscope::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; } diff --git a/scopehal/SiglentSCPIOscilloscope.h b/scopehal/SiglentSCPIOscilloscope.h index 072e7027..588c8984 100644 --- a/scopehal/SiglentSCPIOscilloscope.h +++ b/scopehal/SiglentSCPIOscilloscope.h @@ -3,6 +3,8 @@ * ANTIKERNEL v0.1 * * * * Copyright (c) 2012-2020 Andrew D. Zonenberg * +* Contributions (c) 2021 Dave Marples * +* * * All rights reserved. * * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * @@ -11,7 +13,7 @@ * * 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 * +* * Redistributions in binary form must reproduce the above copyright notice, this list of condibtions 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 * @@ -30,177 +32,263 @@ #ifndef SiglentSCPIOscilloscope_h #define SiglentSCPIOscilloscope_h -#include "../xptools/Socket.h" - -#include "LeCroyOscilloscope.h" +#include -// temp forward declaration -struct SiglentWaveformDesc_t; +class DropoutTrigger; +class EdgeTrigger; +class GlitchTrigger; +class PulseWidthTrigger; +class RuntTrigger; +class SlewRateTrigger; +class UartTrigger; +class WindowTrigger; /** - @brief A Siglent SCPI (SCPI/TCP) oscilloscope + @brief A Siglent new generation scope based on linux (SDS2000X+/SDS5000/SDS6000) - Protocol layer is based on Siglent's reference manual - Implementation here modeled off of the LeCroy support in LeCroyVICPOscilloscope.cpp */ -class SiglentSCPIOscilloscope - : public LeCroyOscilloscope + +#define MAX_ANALOG 4 +#define WAVEDESC_SIZE 346 + +// These scopes will actually sample 200MPoints, but the maxiumum it can transfer in one +// chunk is 10MPoints +#define WAVEFORM_SIZE (20 * 1000 * 1000) + +class SiglentSCPIOscilloscope : public SCPIOscilloscope { public: SiglentSCPIOscilloscope(SCPITransport* transport); virtual ~SiglentSCPIOscilloscope(); + //not copyable or assignable + SiglentSCPIOscilloscope(const SiglentSCPIOscilloscope& rhs) = delete; + SiglentSCPIOscilloscope& operator=(const SiglentSCPIOscilloscope& rhs) = delete; + +private: + std::string converse(const char* fmt, ...); + void sendOnly(const char* fmt, ...); + +protected: + void IdentifyHardware(); + void SharedCtorInit(); + virtual void DetectAnalogChannels(); + void AddDigitalChannels(unsigned int count); + void DetectOptions(); + +public: + //Device information + virtual std::string GetName(); + virtual std::string GetVendor(); + virtual std::string GetSerial(); + virtual unsigned int GetInstrumentTypes(); + virtual unsigned int GetMeasurementTypes(); + + virtual void FlushConfigCache(); + + //Channel configuration + virtual bool IsChannelEnabled(size_t i); + virtual void EnableChannel(size_t i); + virtual bool CanEnableChannel(size_t i); + virtual void DisableChannel(size_t i); + virtual OscilloscopeChannel::CouplingType GetChannelCoupling(size_t i); + virtual void SetChannelCoupling(size_t i, OscilloscopeChannel::CouplingType type); + virtual double GetChannelAttenuation(size_t i); + virtual void SetChannelAttenuation(size_t i, double atten); + virtual int GetChannelBandwidthLimit(size_t i); + virtual void SetChannelBandwidthLimit(size_t i, unsigned int limit_mhz); + virtual double GetChannelVoltageRange(size_t i); virtual void SetChannelVoltageRange(size_t i, double range); + virtual OscilloscopeChannel* GetExternalTrigger(); + virtual double GetChannelOffset(size_t i); + virtual void SetChannelOffset(size_t i, double offset); + virtual std::string GetChannelDisplayName(size_t i); + virtual void SetChannelDisplayName(size_t i, std::string name); + virtual std::vector GetChannelBandwidthLimiters(size_t i); + virtual bool CanInvert(size_t i); + virtual void Invert(size_t i, bool invert); + virtual bool IsInverted(size_t i); + //Triggering + virtual Oscilloscope::TriggerMode PollTrigger(); virtual bool AcquireData(); + virtual void Start(); + virtual void StartSingleTrigger(); + virtual void Stop(); + virtual bool IsTriggerArmed(); + virtual void PushTrigger(); + virtual void PullTrigger(); + virtual void EnableTriggerOutput(); + virtual std::vector GetTriggerTypes(); + + //Scope models. + //We only distinguish down to the series of scope, exact SKU is mostly irrelevant. + enum Model + { + MODEL_SIGLENT_SDS2000XP, + MODEL_SIGLENT_SDS5000X, + MODEL_UNKNOWN + }; + + Model GetModelID() { return m_modelid; } + + //Timebase + virtual std::vector GetSampleRatesNonInterleaved(); + virtual std::vector GetSampleRatesInterleaved(); + virtual std::set GetInterleaveConflicts(); + virtual std::vector GetSampleDepthsNonInterleaved(); + virtual std::vector GetSampleDepthsInterleaved(); + virtual uint64_t GetSampleRate(); + virtual uint64_t GetSampleDepth(); + virtual void SetSampleDepth(uint64_t depth); + virtual void SetSampleRate(uint64_t rate); + virtual void SetUseExternalRefclk(bool external); + virtual bool IsInterleaving(); + virtual bool SetInterleaving(bool combine); + + virtual void SetTriggerOffset(int64_t offset); + virtual int64_t GetTriggerOffset(); + virtual void SetDeskewForChannel(size_t channel, int64_t skew); + virtual int64_t GetDeskewForChannel(size_t channel); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Logic analyzer configuration + + virtual std::vector GetDigitalBanks(); + virtual DigitalBank GetDigitalBank(size_t channel); + virtual bool IsDigitalHysteresisConfigurable(); + virtual bool IsDigitalThresholdConfigurable(); + virtual float GetDigitalHysteresis(size_t channel); + virtual float GetDigitalThreshold(size_t channel); + virtual void SetDigitalHysteresis(size_t channel, float level); + virtual void SetDigitalThreshold(size_t channel, float level); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // 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 + + virtual bool IsADCModeConfigurable(); + virtual std::vector GetADCModeNames(size_t channel); + virtual size_t GetADCMode(size_t channel); + virtual void SetADCMode(size_t channel, size_t mode); protected: + void PullDropoutTrigger(); + void PullEdgeTrigger(); + void PullPulseWidthTrigger(); + void PullRuntTrigger(); + void PullSlewRateTrigger(); + void PullUartTrigger(); + void PullWindowTrigger(); + void PullTriggerSource(Trigger* trig, std::string triggerModeName); - void ReadWaveDescriptorBlock(SiglentWaveformDesc_t *descriptor, unsigned int channel); - int ReadWaveHeader(char *header); + void GetTriggerSlope(EdgeTrigger* trig, std::string reply); + Trigger::Condition GetCondition(std::string reply); - bool m_acquiredDataIsSigned; - bool m_hasVdivAttnBug; + 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); -public: - static std::string GetDriverNameInternal(); + void BulkCheckChannelEnableState(); - OSCILLOSCOPE_INITPROC(SiglentSCPIOscilloscope) + std::string GetPossiblyEmptyString(const std::string& property); -}; + // bool ReadWaveformBlock(std::string& data); + int ReadWaveformBlock(uint32_t maxsize, char* data); + // bool ReadWavedescs( + // std::vector& wavedescs, + // bool* enabled, + // unsigned int& firstEnabledChannel, + // bool& any_enabled); + bool ReadWavedescs( + char wavedescs[MAX_ANALOG][WAVEDESC_SIZE], bool* enabled, unsigned int& firstEnabledChannel, bool& any_enabled); -#pragma pack(1) + void RequestWaveforms(bool* enabled, uint32_t num_sequences, bool denabled); + time_t ExtractTimestamp(unsigned char* wavedesc, double& basetime); -struct SignalWaveformTimestamp_t -{ - double Seconds; - uint8_t Minutes; - uint8_t Hours; - uint8_t Days; - uint8_t Months; - uint16_t Years; - uint16_t Unused; -}; + std::vector ProcessAnalogWaveform(const char* data, + size_t datalen, + char* wavedesc, + uint32_t num_sequences, + time_t ttime, + double basetime, + double* wavetime); + std::map ProcessDigitalWaveform(std::string& data); -#pragma pack(1) -struct SiglentWaveformDesc_t -{ - // nominally always "WAVEDESC" - char DescName[16]; - // nominally always "WAVEACE" - char TemplateName[16]; - // 0: byte, 1: word (error if != 0...) - uint16_t CommType; - // 0: big endian, 1: little endian - uint16_t CommOrder; - // length of wave descriptor (this block) - uint32_t WaveDescLen; - // length of user text block - uint32_t UserTextLen; - // length of whatever ResDesc1 is - uint32_t ResDesc1Len; - // length of TRIGTIME array - uint32_t TriggerTimeArrayLen; - // length of RIS_TIME array - uint32_t RISTimeArrayLen; - // weird reserved array - uint32_t ReservedArrayLen; - // length of the actual sample data - uint32_t WaveformArrayLen; - // length of the second waveform (?) - uint32_t Waveform2ArrayLen; - // two reserved entries - uint32_t ReservedLen1; - uint32_t ReservedLen2; - // Instrument name - char InstrumentName[16]; - uint32_t InstrumentNumber; - // seems to be garbage - char TraceLabel[16]; - uint16_t ReservedWord1; - uint16_t ReservedWord2; - // Num. points in data array (not bytes!) - uint32_t WaveArrayCount; - uint32_t PointsPerScreen; - uint32_t FirstValidPoint; - uint32_t LastValidPoint; - uint32_t FirstPoint; - uint32_t SparsingFactor; - uint32_t SegmentIndex; - uint32_t SubarrayCount; - uint32_t SweepsPerAcquisition; - // Apparently used for peak detect - uint16_t PointsPerPair; - uint16_t PairOffset; - float VerticalGain; - float VerticalOffset; - float MaximumValue; - float MinumumValue; - // scope makes a guess as to bitness... - uint16_t NominalBits; - uint16_t NominalSubarrayCount; - float HorizontalInterval; - double HorizontalOffset; - double PixelOffset; - char VerticalUnit[48]; - char HorizontalUnit[48]; - // jitter between acquisitions - float HorizontalUncertainty; - struct SignalWaveformTimestamp_t Timestamp; - float AcquisitionDuration; - /* - 0: single sweep - 1: interleaved - 2: histogram - 3: graph - 4: filter coefficient - 5: complex - 6: extrema - 7: sequence (obsolete?) - 8: centered RIS - 9: peak detect - */ - uint16_t RecordType; - /* - 0: no processing - 1: fir filter - 2: interpolated - 3: sparsed - 4: autoscaled - 5: no result (?) - 6: rolling - 7: cumulative - */ - uint16_t ProcessingDone; - uint16_t ReservedWord5; - uint16_t RISSweeps; - // enum from 0..35 for 200ps..100s - // 100 -> external - uint16_t Timebase; - /* - 0: DC - 1: AC - 2: GND - */ - uint16_t VerticalCoupling; - float ProbeAttenuation; - uint16_t FixedVerticalGain; - /* - 0: off - 1: 20M - 2: 200M - */ - uint16_t BandwidthLimit; - float VerticalVernier; - float AcquisitionVerticalOffset; - /* - 0: Chan 1 - 1: Chan 2 - 2: Chan 3 - 3: Chan 4 - 9: Unknown - */ - uint16_t WaveformSource; -}; + void Convert8BitSamples( + int64_t* offs, int64_t* durs, float* pout, int8_t* pin, float gain, float offset, size_t count, int64_t ibase); + void Convert8BitSamplesAVX2( + int64_t* offs, int64_t* durs, float* pout, int8_t* pin, float gain, float offset, size_t count, int64_t ibase); + + //hardware analog channel count, independent of LA option etc + unsigned int m_analogChannelCount; + unsigned int m_digitalChannelCount; + size_t m_digitalChannelBase; + + Model m_modelid; + + //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; + + // Transfer buffer. This is a bit hacky + char m_analogWaveformData[MAX_ANALOG][WAVEFORM_SIZE]; + int m_analogWaveformDataSize[MAX_ANALOG]; + char m_wavedescs[MAX_ANALOG][WAVEDESC_SIZE]; + char m_digitalWaveformDataBytes[WAVEFORM_SIZE]; + std::string m_digitalWaveformData; + + //Cached configuration + std::map m_channelVoltageRanges; + std::map m_channelOffsets; + 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; + bool m_interleaving; + bool m_interleavingValid; + Multimeter::MeasurementTypes m_meterMode; + bool m_meterModeValid; + std::map m_probeIsActive; + + //True if we have >8 bit capture depth + bool m_highDefinition; + + //External trigger input + OscilloscopeChannel* m_extTrigChannel; + std::vector m_digitalChannels; + + //Mutexing for thread safety + std::recursive_mutex m_cacheMutex; + +public: + static std::string GetDriverNameInternal(); + OSCILLOSCOPE_INITPROC(SiglentSCPIOscilloscope) +}; #endif diff --git a/scopehal/UartTrigger.cpp b/scopehal/UartTrigger.cpp index af15f910..a1d45d35 100644 --- a/scopehal/UartTrigger.cpp +++ b/scopehal/UartTrigger.cpp @@ -35,8 +35,7 @@ using namespace std; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Construction / destruction -UartTrigger::UartTrigger(Oscilloscope* scope) - : SerialTrigger(scope) +UartTrigger::UartTrigger(Oscilloscope* scope) : SerialTrigger(scope) { CreateInput("din"); @@ -48,6 +47,8 @@ UartTrigger::UartTrigger(Oscilloscope* scope) m_parameters[m_ptypename].AddEnumValue("None", PARITY_NONE); m_parameters[m_ptypename].AddEnumValue("Even", PARITY_EVEN); m_parameters[m_ptypename].AddEnumValue("Odd", PARITY_ODD); + m_parameters[m_ptypename].AddEnumValue("Mark", PARITY_MARK); + m_parameters[m_ptypename].AddEnumValue("Space", PARITY_SPACE); m_typename = "Trigger Type"; m_parameters[m_typename] = FilterParameter(FilterParameter::TYPE_ENUM, Unit(Unit::UNIT_COUNTS)); @@ -65,7 +66,6 @@ UartTrigger::UartTrigger(Oscilloscope* scope) UartTrigger::~UartTrigger() { - } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/scopehal/UartTrigger.h b/scopehal/UartTrigger.h index 8e13f21a..f23e9306 100644 --- a/scopehal/UartTrigger.h +++ b/scopehal/UartTrigger.h @@ -43,6 +43,8 @@ #undef PARITY_NONE #undef PARITY_ODD #undef PARITY_EVEN +#undef PARITY_MARK +#undef PARITY_SPACE #endif /** @@ -58,7 +60,9 @@ class UartTrigger : public SerialTrigger { PARITY_NONE, PARITY_ODD, - PARITY_EVEN + PARITY_EVEN, + PARITY_MARK, + PARITY_SPACE }; void SetParityType(ParityType type) @@ -70,7 +74,9 @@ class UartTrigger : public SerialTrigger enum MatchType { TYPE_DATA, - TYPE_PARITY_ERR + TYPE_PARITY_ERR, + TYPE_START, + TYPE_STOP }; void SetMatchType(MatchType type) diff --git a/xptools b/xptools index babf398c..4e120d7a 160000 --- a/xptools +++ b/xptools @@ -1 +1 @@ -Subproject commit babf398cc8a282bdd27118e4a3e7cd0551a354fc +Subproject commit 4e120d7abd47c5564c097bc3759536bad70e1b3c