1352 lines
53 KiB
C++
1352 lines
53 KiB
C++
#ifdef _WIN32
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#endif
|
|
|
|
#ifdef _MSC_VER
|
|
#pragma warning(push)
|
|
#pragma warning(disable: 4995)
|
|
#endif
|
|
#include "x502api.h"
|
|
#include "e502api.h"
|
|
#ifdef _MSC_VER
|
|
#pragma warning(pop)
|
|
#endif
|
|
|
|
#include "capture_file_writer.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <deque>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
enum class StopMode {
|
|
TargetFrames,
|
|
DiSyn2Rise,
|
|
DiSyn2Fall
|
|
};
|
|
|
|
struct Config {
|
|
std::string serial;
|
|
std::optional<uint32_t> ip_addr;
|
|
|
|
uint32_t channel_count = 2;
|
|
uint32_t mode = X502_LCH_MODE_DIFF;
|
|
uint32_t range = X502_ADC_RANGE_02;
|
|
uint32_t ch1 = 2;
|
|
uint32_t ch2 = 3;
|
|
|
|
double sample_clock_hz = 125000.0;
|
|
bool sample_clock_specified = false;
|
|
bool max_internal_clock = false;
|
|
double duration_ms = 100.0;
|
|
uint32_t packet_limit = 0;
|
|
uint32_t internal_ref_freq = X502_REF_FREQ_2000KHZ;
|
|
|
|
uint32_t sync_mode = X502_SYNC_DI_SYN1_RISE;
|
|
uint32_t sync_start_mode = X502_SYNC_DI_SYN2_RISE;
|
|
StopMode stop_mode = StopMode::DiSyn2Fall;
|
|
|
|
uint32_t recv_block_words = 4096;
|
|
uint32_t recv_timeout_ms = 50;
|
|
uint32_t stats_period_ms = 1000;
|
|
uint32_t start_wait_ms = 10000;
|
|
uint32_t input_buffer_words = 4 * 1024 * 1024;
|
|
uint32_t input_step_words = 4096;
|
|
uint32_t live_update_period_ms = 500;
|
|
|
|
bool pullup_syn1 = false;
|
|
bool pullup_syn2 = false;
|
|
bool pulldown_conv_in = false;
|
|
bool pulldown_start_in = false;
|
|
|
|
std::string csv_path = "capture.csv";
|
|
std::string svg_path = "capture.svg";
|
|
std::string live_html_path = "live_plot.html";
|
|
std::string live_json_path = "live_plot.json";
|
|
};
|
|
|
|
[[noreturn]] void fail(const std::string& message) {
|
|
throw std::runtime_error(message);
|
|
}
|
|
|
|
std::string trim_copy(const std::string& text) {
|
|
const auto first = text.find_first_not_of(" \t\r\n");
|
|
if (first == std::string::npos) {
|
|
return {};
|
|
}
|
|
const auto last = text.find_last_not_of(" \t\r\n");
|
|
return text.substr(first, last - first + 1);
|
|
}
|
|
|
|
bool starts_with(const std::string& value, const std::string& prefix) {
|
|
return value.rfind(prefix, 0) == 0;
|
|
}
|
|
|
|
uint32_t parse_u32(const std::string& text, const std::string& field_name) {
|
|
const std::string clean = trim_copy(text);
|
|
char* end = nullptr;
|
|
const auto value = std::strtoull(clean.c_str(), &end, 0);
|
|
if ((end == clean.c_str()) || (*end != '\0') || (value > std::numeric_limits<uint32_t>::max())) {
|
|
fail("Invalid integer for " + field_name + ": " + text);
|
|
}
|
|
return static_cast<uint32_t>(value);
|
|
}
|
|
|
|
double parse_double(const std::string& text, const std::string& field_name) {
|
|
const std::string clean = trim_copy(text);
|
|
char* end = nullptr;
|
|
const double value = std::strtod(clean.c_str(), &end);
|
|
if ((end == clean.c_str()) || (*end != '\0') || !std::isfinite(value)) {
|
|
fail("Invalid floating point value for " + field_name + ": " + text);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
uint32_t parse_ipv4(const std::string& text) {
|
|
std::array<uint32_t, 4> parts{};
|
|
std::stringstream ss(text);
|
|
std::string token;
|
|
for (std::size_t i = 0; i < parts.size(); ++i) {
|
|
if (!std::getline(ss, token, '.')) {
|
|
fail("Invalid IPv4 address: " + text);
|
|
}
|
|
parts[i] = parse_u32(token, "ip");
|
|
if (parts[i] > 255) {
|
|
fail("IPv4 byte out of range: " + token);
|
|
}
|
|
}
|
|
if (std::getline(ss, token, '.')) {
|
|
fail("Invalid IPv4 address: " + text);
|
|
}
|
|
return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
|
|
}
|
|
|
|
std::string ipv4_to_string(uint32_t ip_addr) {
|
|
std::ostringstream out;
|
|
out << ((ip_addr >> 24) & 0xFF) << '.'
|
|
<< ((ip_addr >> 16) & 0xFF) << '.'
|
|
<< ((ip_addr >> 8) & 0xFF) << '.'
|
|
<< (ip_addr & 0xFF);
|
|
return out.str();
|
|
}
|
|
|
|
uint32_t parse_range(const std::string& text) {
|
|
const double value = parse_double(text, "range");
|
|
if (std::fabs(value - 10.0) < 1e-9) {
|
|
return X502_ADC_RANGE_10;
|
|
}
|
|
if (std::fabs(value - 5.0) < 1e-9) {
|
|
return X502_ADC_RANGE_5;
|
|
}
|
|
if (std::fabs(value - 2.0) < 1e-9) {
|
|
return X502_ADC_RANGE_2;
|
|
}
|
|
if (std::fabs(value - 1.0) < 1e-9) {
|
|
return X502_ADC_RANGE_1;
|
|
}
|
|
if (std::fabs(value - 0.5) < 1e-9) {
|
|
return X502_ADC_RANGE_05;
|
|
}
|
|
if (std::fabs(value - 0.2) < 1e-9) {
|
|
return X502_ADC_RANGE_02;
|
|
}
|
|
fail("Unsupported E-502 range: " + text);
|
|
}
|
|
|
|
double range_to_volts(uint32_t range) {
|
|
switch (range) {
|
|
case X502_ADC_RANGE_10:
|
|
return 10.0;
|
|
case X502_ADC_RANGE_5:
|
|
return 5.0;
|
|
case X502_ADC_RANGE_2:
|
|
return 2.0;
|
|
case X502_ADC_RANGE_1:
|
|
return 1.0;
|
|
case X502_ADC_RANGE_05:
|
|
return 0.5;
|
|
case X502_ADC_RANGE_02:
|
|
return 0.2;
|
|
default:
|
|
fail("Unknown ADC range enum");
|
|
}
|
|
}
|
|
|
|
uint32_t parse_mode(const std::string& text) {
|
|
const std::string value = trim_copy(text);
|
|
if ((value == "comm") || (value == "gnd") || (value == "single_ended")) {
|
|
return X502_LCH_MODE_COMM;
|
|
}
|
|
if ((value == "diff") || (value == "differential")) {
|
|
return X502_LCH_MODE_DIFF;
|
|
}
|
|
fail("Unsupported input mode: " + text);
|
|
}
|
|
|
|
uint32_t parse_internal_ref_freq(const std::string& text) {
|
|
const std::string value = trim_copy(text);
|
|
if ((value == "2000000") || (value == "2000khz") || (value == "2mhz")) {
|
|
return X502_REF_FREQ_2000KHZ;
|
|
}
|
|
if ((value == "1500000") || (value == "1500khz") || (value == "1.5mhz")) {
|
|
return X502_REF_FREQ_1500KHZ;
|
|
}
|
|
fail("Unsupported internal_ref_hz value: " + text + ". Use 1500000 or 2000000");
|
|
}
|
|
|
|
uint32_t parse_channel_count(const std::string& text) {
|
|
const uint32_t value = parse_u32(text, "channels");
|
|
if ((value == 1U) || (value == 2U)) {
|
|
return value;
|
|
}
|
|
fail("channels must be 1 or 2");
|
|
}
|
|
|
|
std::string ref_freq_to_string(uint32_t freq) {
|
|
switch (freq) {
|
|
case X502_REF_FREQ_2000KHZ:
|
|
return "2000000";
|
|
case X502_REF_FREQ_1500KHZ:
|
|
return "1500000";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
bool use_internal_max_clock(const Config& cfg) {
|
|
return (cfg.sync_mode == X502_SYNC_INTERNAL) && (cfg.max_internal_clock || !cfg.sample_clock_specified);
|
|
}
|
|
|
|
uint32_t parse_sync_mode(const std::string& text) {
|
|
const std::string value = trim_copy(text);
|
|
if ((value == "conv_in") || (value == "start_in") || (value == "external_master")) {
|
|
return X502_SYNC_EXTERNAL_MASTER;
|
|
}
|
|
if (value == "di_syn1_rise") {
|
|
return X502_SYNC_DI_SYN1_RISE;
|
|
}
|
|
if (value == "di_syn1_fall") {
|
|
return X502_SYNC_DI_SYN1_FALL;
|
|
}
|
|
if (value == "di_syn2_rise") {
|
|
return X502_SYNC_DI_SYN2_RISE;
|
|
}
|
|
if (value == "di_syn2_fall") {
|
|
return X502_SYNC_DI_SYN2_FALL;
|
|
}
|
|
if ((value == "internal") || (value == "immediate")) {
|
|
return X502_SYNC_INTERNAL;
|
|
}
|
|
fail("Unsupported sync mode: " + text);
|
|
}
|
|
|
|
std::string sync_mode_to_string(uint32_t mode, bool for_start) {
|
|
switch (mode) {
|
|
case X502_SYNC_INTERNAL:
|
|
return for_start ? "immediate" : "internal";
|
|
case X502_SYNC_EXTERNAL_MASTER:
|
|
return for_start ? "start_in" : "conv_in";
|
|
case X502_SYNC_DI_SYN1_RISE:
|
|
return "di_syn1_rise";
|
|
case X502_SYNC_DI_SYN1_FALL:
|
|
return "di_syn1_fall";
|
|
case X502_SYNC_DI_SYN2_RISE:
|
|
return "di_syn2_rise";
|
|
case X502_SYNC_DI_SYN2_FALL:
|
|
return "di_syn2_fall";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
StopMode parse_stop_mode(const std::string& text) {
|
|
const std::string value = trim_copy(text);
|
|
if ((value == "frames") || (value == "duration") || (value == "none")) {
|
|
return StopMode::TargetFrames;
|
|
}
|
|
if (value == "di_syn2_rise") {
|
|
return StopMode::DiSyn2Rise;
|
|
}
|
|
if (value == "di_syn2_fall") {
|
|
return StopMode::DiSyn2Fall;
|
|
}
|
|
fail("Unsupported stop mode: " + text);
|
|
}
|
|
|
|
std::string stop_mode_to_string(StopMode mode) {
|
|
switch (mode) {
|
|
case StopMode::TargetFrames:
|
|
return "target_frames";
|
|
case StopMode::DiSyn2Rise:
|
|
return "di_syn2_rise";
|
|
case StopMode::DiSyn2Fall:
|
|
return "di_syn2_fall";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
bool sync_uses_di_syn1(uint32_t mode) {
|
|
return (mode == X502_SYNC_DI_SYN1_RISE) || (mode == X502_SYNC_DI_SYN1_FALL);
|
|
}
|
|
|
|
bool sync_uses_di_syn2(uint32_t mode) {
|
|
return (mode == X502_SYNC_DI_SYN2_RISE) || (mode == X502_SYNC_DI_SYN2_FALL);
|
|
}
|
|
|
|
std::string phy_channel_name(uint32_t mode, uint32_t phy_ch) {
|
|
if (mode == X502_LCH_MODE_DIFF) {
|
|
return "X" + std::to_string(phy_ch + 1) + "-Y" + std::to_string(phy_ch + 1);
|
|
}
|
|
if (phy_ch < 16) {
|
|
return "X" + std::to_string(phy_ch + 1);
|
|
}
|
|
if (phy_ch < 32) {
|
|
return "Y" + std::to_string((phy_ch - 16) + 1);
|
|
}
|
|
return "CH" + std::to_string(phy_ch);
|
|
}
|
|
|
|
void print_help(const char* exe_name) {
|
|
std::cout
|
|
<< "Usage:\n"
|
|
<< " " << exe_name << " [serial:SN] [ip:192.168.0.10] [channels:2] [ch1:2] [ch2:3]\n"
|
|
<< " [mode:diff|comm] [range:0.2] [clock:di_syn1_rise]\n"
|
|
<< " [start:di_syn2_rise] [stop:di_syn2_fall] [sample_clock_hz:125000|max]\n"
|
|
<< " [internal_ref_hz:2000000]\n"
|
|
<< " [duration_ms:100] [packet_limit:0] [csv:capture.csv] [svg:capture.svg]\n"
|
|
<< " [live_html:live_plot.html] [live_json:live_plot.json]\n"
|
|
<< " [recv_block:4096] [stats_period_ms:1000] [live_update_period_ms:500] [start_wait_ms:10000]\n"
|
|
<< " [pullup_syn1] [pullup_syn2] [pulldown_conv_in] [pulldown_start_in]\n"
|
|
<< "\n"
|
|
<< "Defaults for E-502:\n"
|
|
<< " channels:2 -> capture CH1 and CH2\n"
|
|
<< " ch1:2, ch2:3 -> X3-Y3 and X4-Y4\n"
|
|
<< " mode:diff -> differential measurement\n"
|
|
<< " range:0.2 -> +/-0.2 V range\n"
|
|
<< " supported ranges -> 10, 5, 2, 1, 0.5, 0.2 V\n"
|
|
<< " channels:1 -> capture only CH1, CH2 is ignored\n"
|
|
<< " clock:di_syn1_rise-> external sample clock on DI_SYN1 rising edge\n"
|
|
<< " clock:internal -> module generates its own clock\n"
|
|
<< " start:di_syn2_rise-> packet starts on DI_SYN2 rising edge\n"
|
|
<< " stop:di_syn2_fall -> packet stops on DI_SYN2 falling edge\n"
|
|
<< " sample_clock_hz:125000 -> requested ADC sample rate; for external clock it is the expected rate\n"
|
|
<< " sample_clock_hz:max -> with clock:internal, use the maximum ADC speed\n"
|
|
<< " internal_ref_hz:2000000 -> internal base clock for clock:internal (1500000 or 2000000)\n"
|
|
<< " stats_period_ms:1000 -> print online transfer/capture statistics every 1000 ms (0 disables)\n"
|
|
<< " live_update_period_ms:500 -> refresh live HTML/JSON no more than twice per second\n"
|
|
<< " duration_ms:100 -> max packet length if stop edge does not arrive\n"
|
|
<< " packet_limit:0 -> 0 means continuous until Ctrl+C, N means stop after N packets\n"
|
|
<< " live_html/live_json -> live graph files updated as packets arrive\n"
|
|
<< " If sample_clock_hz is omitted together with clock:internal, the maximum ADC speed is used\n"
|
|
<< "\n"
|
|
<< "Differential physical channel mapping:\n"
|
|
<< " 0..15 -> X1-Y1 .. X16-Y16\n"
|
|
<< "\n"
|
|
<< "Common-ground physical channel mapping:\n"
|
|
<< " 0..15 -> X1..X16\n"
|
|
<< " 16..31 -> Y1..Y16\n"
|
|
<< "\n"
|
|
<< "Useful sync lines on E-502:\n"
|
|
<< " clock: conv_in | di_syn1_rise | di_syn1_fall | di_syn2_rise | di_syn2_fall\n"
|
|
<< " start: immediate | start_in | di_syn1_rise | di_syn1_fall | di_syn2_rise | di_syn2_fall\n"
|
|
<< " stop: frames | di_syn2_rise | di_syn2_fall\n"
|
|
<< "\n"
|
|
<< "This build enables synchronous DIN together with ADC. DI_SYN2 stop edges are detected\n"
|
|
<< "inside the same input stream, packets are split continuously by DI_SYN2 edges, and if\n"
|
|
<< "digital input 1 changes state the corresponding ADC sample is written to the buffer as 0.\n"
|
|
<< "Open live_plot.html in a browser to see the live graph update over time.\n"
|
|
<< "\n"
|
|
<< "Recommended working example:\n"
|
|
<< " " << exe_name
|
|
<< " clock:di_syn1_rise start:di_syn2_rise stop:di_syn2_fall sample_clock_hz:125000"
|
|
<< " duration_ms:100 packet_limit:10 csv:chirp.csv svg:chirp.svg\n"
|
|
<< "\n"
|
|
<< "Internal-clock example:\n"
|
|
<< " " << exe_name
|
|
<< " clock:internal internal_ref_hz:2000000 start:di_syn2_rise stop:di_syn2_fall"
|
|
<< " sample_clock_hz:max duration_ms:100 packet_limit:0 csv:chirp.csv svg:chirp.svg\n";
|
|
}
|
|
|
|
Config parse_args(int argc, char** argv) {
|
|
Config cfg;
|
|
|
|
for (int i = 1; i < argc; ++i) {
|
|
const std::string arg = argv[i];
|
|
if ((arg == "help") || (arg == "--help") || (arg == "-h")) {
|
|
print_help(argv[0]);
|
|
std::exit(0);
|
|
}
|
|
if (arg == "pullup_syn1") {
|
|
cfg.pullup_syn1 = true;
|
|
continue;
|
|
}
|
|
if (arg == "pullup_syn2") {
|
|
cfg.pullup_syn2 = true;
|
|
continue;
|
|
}
|
|
if (arg == "pulldown_conv_in") {
|
|
cfg.pulldown_conv_in = true;
|
|
continue;
|
|
}
|
|
if (arg == "pulldown_start_in") {
|
|
cfg.pulldown_start_in = true;
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "serial:")) {
|
|
cfg.serial = arg.substr(7);
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "ip:")) {
|
|
cfg.ip_addr = parse_ipv4(arg.substr(3));
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "mode:")) {
|
|
cfg.mode = parse_mode(arg.substr(5));
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "channels:")) {
|
|
cfg.channel_count = parse_channel_count(arg.substr(9));
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "range:")) {
|
|
cfg.range = parse_range(arg.substr(6));
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "ch1:")) {
|
|
cfg.ch1 = parse_u32(arg.substr(4), "ch1");
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "ch2:")) {
|
|
cfg.ch2 = parse_u32(arg.substr(4), "ch2");
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "clock:")) {
|
|
cfg.sync_mode = parse_sync_mode(arg.substr(6));
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "start:")) {
|
|
cfg.sync_start_mode = parse_sync_mode(arg.substr(6));
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "stop:")) {
|
|
cfg.stop_mode = parse_stop_mode(arg.substr(5));
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "sample_clock_hz:")) {
|
|
const std::string value = trim_copy(arg.substr(16));
|
|
cfg.sample_clock_specified = true;
|
|
if ((value == "max") || (value == "maximum")) {
|
|
cfg.max_internal_clock = true;
|
|
} else {
|
|
cfg.sample_clock_hz = parse_double(value, "sample_clock_hz");
|
|
cfg.max_internal_clock = false;
|
|
}
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "internal_ref_hz:")) {
|
|
cfg.internal_ref_freq = parse_internal_ref_freq(arg.substr(16));
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "duration_ms:")) {
|
|
cfg.duration_ms = parse_double(arg.substr(12), "duration_ms");
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "packet_limit:")) {
|
|
cfg.packet_limit = parse_u32(arg.substr(13), "packet_limit");
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "recv_block:")) {
|
|
cfg.recv_block_words = parse_u32(arg.substr(11), "recv_block");
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "recv_timeout_ms:")) {
|
|
cfg.recv_timeout_ms = parse_u32(arg.substr(16), "recv_timeout_ms");
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "stats_period_ms:")) {
|
|
cfg.stats_period_ms = parse_u32(arg.substr(16), "stats_period_ms");
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "live_update_period_ms:")) {
|
|
cfg.live_update_period_ms = parse_u32(arg.substr(22), "live_update_period_ms");
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "start_wait_ms:")) {
|
|
cfg.start_wait_ms = parse_u32(arg.substr(14), "start_wait_ms");
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "buffer_words:")) {
|
|
cfg.input_buffer_words = parse_u32(arg.substr(13), "buffer_words");
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "step_words:")) {
|
|
cfg.input_step_words = parse_u32(arg.substr(11), "step_words");
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "csv:")) {
|
|
cfg.csv_path = arg.substr(4);
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "svg:")) {
|
|
cfg.svg_path = arg.substr(4);
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "live_html:")) {
|
|
cfg.live_html_path = arg.substr(10);
|
|
continue;
|
|
}
|
|
if (starts_with(arg, "live_json:")) {
|
|
cfg.live_json_path = arg.substr(10);
|
|
continue;
|
|
}
|
|
fail("Unknown argument: " + arg);
|
|
}
|
|
|
|
if (cfg.duration_ms <= 0.0) {
|
|
fail("duration_ms must be > 0");
|
|
}
|
|
if (!cfg.max_internal_clock && (cfg.sample_clock_hz <= 0.0)) {
|
|
fail("sample_clock_hz must be > 0");
|
|
}
|
|
if (cfg.max_internal_clock && (cfg.sync_mode != X502_SYNC_INTERNAL)) {
|
|
fail("sample_clock_hz:max is only valid together with clock:internal");
|
|
}
|
|
if (cfg.recv_block_words == 0) {
|
|
fail("recv_block must be > 0");
|
|
}
|
|
if (cfg.input_step_words == 0) {
|
|
cfg.input_step_words = cfg.recv_block_words;
|
|
}
|
|
if (cfg.input_buffer_words < cfg.recv_block_words) {
|
|
cfg.input_buffer_words = cfg.recv_block_words;
|
|
}
|
|
if (sync_uses_di_syn1(cfg.sync_mode) && sync_uses_di_syn1(cfg.sync_start_mode)) {
|
|
fail("clock and start cannot both use DI_SYN1; use start_in or immediate start");
|
|
}
|
|
if (sync_uses_di_syn2(cfg.sync_mode) && sync_uses_di_syn2(cfg.sync_start_mode)) {
|
|
fail("clock and start cannot both use DI_SYN2; use start_in or immediate start");
|
|
}
|
|
if ((cfg.stop_mode != StopMode::TargetFrames) && sync_uses_di_syn2(cfg.sync_mode)) {
|
|
fail("DI_SYN2 cannot be used simultaneously for clock and stop");
|
|
}
|
|
if ((cfg.stop_mode == StopMode::DiSyn2Rise) && (cfg.sync_start_mode == X502_SYNC_DI_SYN2_RISE)) {
|
|
fail("start and stop cannot both use the same DI_SYN2 rising edge");
|
|
}
|
|
if ((cfg.stop_mode == StopMode::DiSyn2Fall) && (cfg.sync_start_mode == X502_SYNC_DI_SYN2_FALL)) {
|
|
fail("start and stop cannot both use the same DI_SYN2 falling edge");
|
|
}
|
|
|
|
if (cfg.mode == X502_LCH_MODE_DIFF) {
|
|
if (cfg.ch1 >= X502_ADC_DIFF_CH_CNT) {
|
|
fail("For differential mode E-502 ch1 must be in range 0..15");
|
|
}
|
|
if ((cfg.channel_count >= 2U) && (cfg.ch2 >= X502_ADC_DIFF_CH_CNT)) {
|
|
fail("For differential mode E-502 ch2 must be in range 0..15");
|
|
}
|
|
} else {
|
|
if (cfg.ch1 >= X502_ADC_COMM_CH_CNT) {
|
|
fail("For common-ground mode E-502 ch1 must be in range 0..31");
|
|
}
|
|
if ((cfg.channel_count >= 2U) && (cfg.ch2 >= X502_ADC_COMM_CH_CNT)) {
|
|
fail("For common-ground mode E-502 ch2 must be in range 0..31");
|
|
}
|
|
}
|
|
|
|
return cfg;
|
|
}
|
|
|
|
template <typename Fn>
|
|
Fn load_symbol(HMODULE module, const char* name) {
|
|
const auto addr = GetProcAddress(module, name);
|
|
if (addr == nullptr) {
|
|
fail(std::string("GetProcAddress failed for symbol: ") + name);
|
|
}
|
|
return reinterpret_cast<Fn>(addr);
|
|
}
|
|
|
|
struct Api {
|
|
HMODULE x502_module = nullptr;
|
|
HMODULE e502_module = nullptr;
|
|
|
|
decltype(&X502_Create) Create = nullptr;
|
|
decltype(&X502_Free) Free = nullptr;
|
|
decltype(&X502_Close) Close = nullptr;
|
|
decltype(&X502_GetErrorString) GetErrorString = nullptr;
|
|
decltype(&X502_GetDevInfo2) GetDevInfo2 = nullptr;
|
|
decltype(&X502_SetMode) SetMode = nullptr;
|
|
decltype(&X502_StreamsStop) StreamsStop = nullptr;
|
|
decltype(&X502_StreamsDisable) StreamsDisable = nullptr;
|
|
decltype(&X502_SetSyncMode) SetSyncMode = nullptr;
|
|
decltype(&X502_SetSyncStartMode) SetSyncStartMode = nullptr;
|
|
decltype(&X502_SetLChannelCount) SetLChannelCount = nullptr;
|
|
decltype(&X502_SetLChannel) SetLChannel = nullptr;
|
|
decltype(&X502_SetAdcFreqDivider) SetAdcFreqDivider = nullptr;
|
|
decltype(&X502_SetAdcInterframeDelay) SetAdcInterframeDelay = nullptr;
|
|
decltype(&X502_SetDinFreqDivider) SetDinFreqDivider = nullptr;
|
|
decltype(&X502_SetAdcFreq) SetAdcFreq = nullptr;
|
|
decltype(&X502_GetAdcFreq) GetAdcFreq = nullptr;
|
|
decltype(&X502_SetDinFreq) SetDinFreq = nullptr;
|
|
decltype(&X502_SetRefFreq) SetRefFreq = nullptr;
|
|
decltype(&X502_SetStreamBufSize) SetStreamBufSize = nullptr;
|
|
decltype(&X502_SetStreamStep) SetStreamStep = nullptr;
|
|
decltype(&X502_SetDigInPullup) SetDigInPullup = nullptr;
|
|
decltype(&X502_SetExtRefFreqValue) SetExtRefFreqValue = nullptr;
|
|
decltype(&X502_Configure) Configure = nullptr;
|
|
decltype(&X502_StreamsEnable) StreamsEnable = nullptr;
|
|
decltype(&X502_StreamsStart) StreamsStart = nullptr;
|
|
decltype(&X502_Recv) Recv = nullptr;
|
|
decltype(&X502_ProcessData) ProcessData = nullptr;
|
|
|
|
decltype(&E502_OpenUsb) OpenUsb = nullptr;
|
|
decltype(&E502_OpenByIpAddr) OpenByIpAddr = nullptr;
|
|
|
|
Api() {
|
|
x502_module = LoadLibraryA("x502api.dll");
|
|
if (x502_module == nullptr) {
|
|
fail("Cannot load x502api.dll");
|
|
}
|
|
e502_module = LoadLibraryA("e502api.dll");
|
|
if (e502_module == nullptr) {
|
|
fail("Cannot load e502api.dll");
|
|
}
|
|
|
|
Create = load_symbol<decltype(Create)>(x502_module, "X502_Create");
|
|
Free = load_symbol<decltype(Free)>(x502_module, "X502_Free");
|
|
Close = load_symbol<decltype(Close)>(x502_module, "X502_Close");
|
|
GetErrorString = load_symbol<decltype(GetErrorString)>(x502_module, "X502_GetErrorString");
|
|
GetDevInfo2 = load_symbol<decltype(GetDevInfo2)>(x502_module, "X502_GetDevInfo2");
|
|
SetMode = load_symbol<decltype(SetMode)>(x502_module, "X502_SetMode");
|
|
StreamsStop = load_symbol<decltype(StreamsStop)>(x502_module, "X502_StreamsStop");
|
|
StreamsDisable = load_symbol<decltype(StreamsDisable)>(x502_module, "X502_StreamsDisable");
|
|
SetSyncMode = load_symbol<decltype(SetSyncMode)>(x502_module, "X502_SetSyncMode");
|
|
SetSyncStartMode = load_symbol<decltype(SetSyncStartMode)>(x502_module, "X502_SetSyncStartMode");
|
|
SetLChannelCount = load_symbol<decltype(SetLChannelCount)>(x502_module, "X502_SetLChannelCount");
|
|
SetLChannel = load_symbol<decltype(SetLChannel)>(x502_module, "X502_SetLChannel");
|
|
SetAdcFreqDivider = load_symbol<decltype(SetAdcFreqDivider)>(x502_module, "X502_SetAdcFreqDivider");
|
|
SetAdcInterframeDelay = load_symbol<decltype(SetAdcInterframeDelay)>(x502_module, "X502_SetAdcInterframeDelay");
|
|
SetDinFreqDivider = load_symbol<decltype(SetDinFreqDivider)>(x502_module, "X502_SetDinFreqDivider");
|
|
SetAdcFreq = load_symbol<decltype(SetAdcFreq)>(x502_module, "X502_SetAdcFreq");
|
|
GetAdcFreq = load_symbol<decltype(GetAdcFreq)>(x502_module, "X502_GetAdcFreq");
|
|
SetDinFreq = load_symbol<decltype(SetDinFreq)>(x502_module, "X502_SetDinFreq");
|
|
SetRefFreq = load_symbol<decltype(SetRefFreq)>(x502_module, "X502_SetRefFreq");
|
|
SetStreamBufSize = load_symbol<decltype(SetStreamBufSize)>(x502_module, "X502_SetStreamBufSize");
|
|
SetStreamStep = load_symbol<decltype(SetStreamStep)>(x502_module, "X502_SetStreamStep");
|
|
SetDigInPullup = load_symbol<decltype(SetDigInPullup)>(x502_module, "X502_SetDigInPullup");
|
|
SetExtRefFreqValue = load_symbol<decltype(SetExtRefFreqValue)>(x502_module, "X502_SetExtRefFreqValue");
|
|
Configure = load_symbol<decltype(Configure)>(x502_module, "X502_Configure");
|
|
StreamsEnable = load_symbol<decltype(StreamsEnable)>(x502_module, "X502_StreamsEnable");
|
|
StreamsStart = load_symbol<decltype(StreamsStart)>(x502_module, "X502_StreamsStart");
|
|
Recv = load_symbol<decltype(Recv)>(x502_module, "X502_Recv");
|
|
ProcessData = load_symbol<decltype(ProcessData)>(x502_module, "X502_ProcessData");
|
|
|
|
OpenUsb = load_symbol<decltype(OpenUsb)>(e502_module, "E502_OpenUsb");
|
|
OpenByIpAddr = load_symbol<decltype(OpenByIpAddr)>(e502_module, "E502_OpenByIpAddr");
|
|
}
|
|
|
|
~Api() {
|
|
if (e502_module != nullptr) {
|
|
FreeLibrary(e502_module);
|
|
}
|
|
if (x502_module != nullptr) {
|
|
FreeLibrary(x502_module);
|
|
}
|
|
}
|
|
};
|
|
|
|
std::string x502_error(const Api& api, int32_t err) {
|
|
const char* text = api.GetErrorString ? api.GetErrorString(err) : nullptr;
|
|
std::ostringstream out;
|
|
out << "err=" << err;
|
|
if ((text != nullptr) && (*text != '\0')) {
|
|
out << " (" << text << ")";
|
|
}
|
|
return out.str();
|
|
}
|
|
|
|
void expect_ok(const Api& api, int32_t err, const std::string& what) {
|
|
if (err != X502_ERR_OK) {
|
|
fail(what + ": " + x502_error(api, err));
|
|
}
|
|
}
|
|
|
|
constexpr uint32_t kE502DiSyn2Mask =
|
|
(static_cast<uint32_t>(1U) << 13U) | (static_cast<uint32_t>(1U) << 17U);
|
|
constexpr uint32_t kE502Digital1Mask = (static_cast<uint32_t>(1U) << 0U);
|
|
|
|
volatile LONG g_console_stop_requested = 0;
|
|
|
|
BOOL WINAPI console_ctrl_handler(DWORD ctrl_type) {
|
|
if ((ctrl_type == CTRL_C_EVENT) || (ctrl_type == CTRL_BREAK_EVENT) || (ctrl_type == CTRL_CLOSE_EVENT)) {
|
|
InterlockedExchange(&g_console_stop_requested, 1);
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
bool console_stop_requested() {
|
|
return InterlockedCompareExchange(&g_console_stop_requested, 0, 0) != 0;
|
|
}
|
|
|
|
enum class PacketCloseReason {
|
|
ExternalStopEdge,
|
|
DurationLimit,
|
|
UserStop
|
|
};
|
|
|
|
const char* packet_close_reason_to_string(PacketCloseReason reason) {
|
|
switch (reason) {
|
|
case PacketCloseReason::ExternalStopEdge:
|
|
return "DI_SYN2 edge";
|
|
case PacketCloseReason::DurationLimit:
|
|
return "duration limit";
|
|
case PacketCloseReason::UserStop:
|
|
return "user stop";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
bool matches_sync_edge(uint32_t mode, bool prev_level, bool current_level) {
|
|
if (mode == X502_SYNC_DI_SYN2_RISE) {
|
|
return !prev_level && current_level;
|
|
}
|
|
if (mode == X502_SYNC_DI_SYN2_FALL) {
|
|
return prev_level && !current_level;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool matches_stop_edge(StopMode mode, bool prev_level, bool current_level) {
|
|
if (mode == StopMode::DiSyn2Rise) {
|
|
return !prev_level && current_level;
|
|
}
|
|
if (mode == StopMode::DiSyn2Fall) {
|
|
return prev_level && !current_level;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
struct PacketAccumulator {
|
|
std::array<std::vector<double>, 2> channels;
|
|
std::size_t zeroed_samples = 0;
|
|
std::size_t stored_samples = 0;
|
|
|
|
void reset(std::size_t reserve_frames, std::size_t channel_count) {
|
|
for (std::size_t i = 0; i < channels.size(); ++i) {
|
|
auto& channel = channels[i];
|
|
channel.clear();
|
|
if (i < channel_count) {
|
|
channel.reserve(reserve_frames);
|
|
}
|
|
}
|
|
zeroed_samples = 0;
|
|
stored_samples = 0;
|
|
}
|
|
|
|
std::size_t frame_count(std::size_t channel_count) const {
|
|
return (channel_count <= 1U) ? channels[0].size() : std::min(channels[0].size(), channels[1].size());
|
|
}
|
|
};
|
|
|
|
struct ConsoleCtrlGuard {
|
|
bool installed = false;
|
|
|
|
ConsoleCtrlGuard() {
|
|
InterlockedExchange(&g_console_stop_requested, 0);
|
|
installed = SetConsoleCtrlHandler(console_ctrl_handler, TRUE) != 0;
|
|
}
|
|
|
|
~ConsoleCtrlGuard() {
|
|
if (installed) {
|
|
SetConsoleCtrlHandler(console_ctrl_handler, FALSE);
|
|
}
|
|
InterlockedExchange(&g_console_stop_requested, 0);
|
|
}
|
|
};
|
|
|
|
struct DeviceHandle {
|
|
const Api& api;
|
|
t_x502_hnd hnd = nullptr;
|
|
bool opened = false;
|
|
bool streams_started = false;
|
|
|
|
explicit DeviceHandle(const Api& api_ref) : api(api_ref), hnd(api.Create()) {
|
|
if (hnd == nullptr) {
|
|
fail("X502_Create failed");
|
|
}
|
|
}
|
|
|
|
~DeviceHandle() {
|
|
if (hnd != nullptr) {
|
|
if (streams_started) {
|
|
api.StreamsStop(hnd);
|
|
}
|
|
if (opened) {
|
|
api.Close(hnd);
|
|
}
|
|
api.Free(hnd);
|
|
}
|
|
}
|
|
};
|
|
|
|
void print_device_info(const t_x502_info& info) {
|
|
std::cout << "Device: " << info.name << "\n"
|
|
<< "Serial: " << info.serial << "\n"
|
|
<< "FPGA version: " << static_cast<unsigned>(info.fpga_ver >> 8) << "."
|
|
<< static_cast<unsigned>(info.fpga_ver & 0xFF) << "\n"
|
|
<< "PLDA version: " << static_cast<unsigned>(info.plda_ver) << "\n"
|
|
<< "Board revision: " << static_cast<unsigned>(info.board_rev) << "\n"
|
|
<< "MCU firmware: " << info.mcu_firmware_ver << "\n";
|
|
}
|
|
|
|
int run(const Config& cfg) {
|
|
Api api;
|
|
DeviceHandle device(api);
|
|
|
|
int32_t err = X502_ERR_OK;
|
|
if (cfg.ip_addr.has_value()) {
|
|
err = api.OpenByIpAddr(device.hnd, *cfg.ip_addr, 0, 5000);
|
|
} else {
|
|
err = api.OpenUsb(device.hnd, cfg.serial.empty() ? nullptr : cfg.serial.c_str());
|
|
}
|
|
expect_ok(api, err, "Open device");
|
|
device.opened = true;
|
|
|
|
t_x502_info info{};
|
|
err = api.GetDevInfo2(device.hnd, &info, sizeof(info));
|
|
expect_ok(api, err, "Get device info");
|
|
print_device_info(info);
|
|
|
|
expect_ok(api, api.SetMode(device.hnd, X502_MODE_FPGA), "Set FPGA mode");
|
|
api.StreamsStop(device.hnd);
|
|
api.StreamsDisable(device.hnd, X502_STREAM_ALL_IN | X502_STREAM_ALL_OUT);
|
|
|
|
expect_ok(api, api.SetSyncMode(device.hnd, cfg.sync_mode), "Set sync mode");
|
|
expect_ok(api, api.SetSyncStartMode(device.hnd, cfg.sync_start_mode), "Set sync start mode");
|
|
|
|
if (cfg.sync_mode == X502_SYNC_INTERNAL) {
|
|
expect_ok(api, api.SetRefFreq(device.hnd, cfg.internal_ref_freq), "Set internal reference frequency");
|
|
} else {
|
|
const int32_t ext_ref_err = api.SetExtRefFreqValue(device.hnd, cfg.sample_clock_hz);
|
|
if (ext_ref_err != X502_ERR_OK) {
|
|
if (cfg.sample_clock_hz <= 1500000.0) {
|
|
expect_ok(api, ext_ref_err, "Set external reference frequency");
|
|
} else {
|
|
std::cerr << "Warning: X502_SetExtRefFreqValue(" << cfg.sample_clock_hz
|
|
<< ") failed, continuing with manual divider configuration: "
|
|
<< x502_error(api, ext_ref_err) << "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
expect_ok(api, api.SetLChannelCount(device.hnd, cfg.channel_count), "Set logical channel count");
|
|
expect_ok(api, api.SetLChannel(device.hnd, 0, cfg.ch1, cfg.mode, cfg.range, 1), "Set logical channel 0");
|
|
if (cfg.channel_count >= 2U) {
|
|
expect_ok(api, api.SetLChannel(device.hnd, 1, cfg.ch2, cfg.mode, cfg.range, 1), "Set logical channel 1");
|
|
}
|
|
|
|
const bool internal_max_clock = use_internal_max_clock(cfg);
|
|
double actual_sample_clock_hz = cfg.sample_clock_hz;
|
|
double actual_frame_freq_hz = cfg.sample_clock_hz / 2.0;
|
|
if (internal_max_clock) {
|
|
actual_sample_clock_hz = static_cast<double>(cfg.internal_ref_freq);
|
|
actual_frame_freq_hz = 0.0;
|
|
expect_ok(api, api.SetAdcFreq(device.hnd, &actual_sample_clock_hz, nullptr), "Set ADC frequency to maximum");
|
|
expect_ok(api, api.GetAdcFreq(device.hnd, &actual_sample_clock_hz, &actual_frame_freq_hz),
|
|
"Get effective ADC frequency");
|
|
} else {
|
|
expect_ok(api, api.SetAdcFreq(device.hnd, &actual_sample_clock_hz, &actual_frame_freq_hz), "Set ADC frequency");
|
|
}
|
|
|
|
double actual_din_freq_hz = actual_sample_clock_hz;
|
|
expect_ok(api, api.SetDinFreq(device.hnd, &actual_din_freq_hz), "Set DIN frequency");
|
|
if (std::fabs(actual_din_freq_hz - actual_sample_clock_hz) > std::max(0.5, actual_sample_clock_hz * 1e-6)) {
|
|
std::ostringstream message;
|
|
message << "ADC and DIN frequencies no longer match closely enough for 1:1 masking logic. "
|
|
<< "ADC=" << actual_sample_clock_hz << " Hz, DIN=" << actual_din_freq_hz << " Hz.";
|
|
fail(message.str());
|
|
}
|
|
|
|
expect_ok(api, api.SetStreamBufSize(device.hnd, X502_STREAM_CH_IN, cfg.input_buffer_words), "Set input buffer size");
|
|
expect_ok(api, api.SetStreamStep(device.hnd, X502_STREAM_CH_IN, cfg.input_step_words), "Set input stream step");
|
|
|
|
uint32_t pullups = 0;
|
|
if (cfg.pullup_syn1) {
|
|
pullups |= X502_PULLUPS_DI_SYN1;
|
|
}
|
|
if (cfg.pullup_syn2) {
|
|
pullups |= X502_PULLUPS_DI_SYN2;
|
|
}
|
|
if (cfg.pulldown_conv_in) {
|
|
pullups |= X502_PULLDOWN_CONV_IN;
|
|
}
|
|
if (cfg.pulldown_start_in) {
|
|
pullups |= X502_PULLDOWN_START_IN;
|
|
}
|
|
expect_ok(api, api.SetDigInPullup(device.hnd, pullups), "Set digital input pullups/pulldowns");
|
|
|
|
expect_ok(api, api.Configure(device.hnd, 0), "Configure device");
|
|
expect_ok(api, api.StreamsEnable(device.hnd, X502_STREAM_ADC | X502_STREAM_DIN), "Enable ADC+DIN stream");
|
|
|
|
const std::size_t target_frames = std::max<std::size_t>(
|
|
1, static_cast<std::size_t>(std::llround((cfg.duration_ms / 1000.0) * actual_frame_freq_hz)));
|
|
|
|
std::cout << "Capture settings:\n"
|
|
<< " clock source: " << sync_mode_to_string(cfg.sync_mode, false) << "\n"
|
|
<< " start source: " << sync_mode_to_string(cfg.sync_start_mode, true) << "\n"
|
|
<< " stop source: " << stop_mode_to_string(cfg.stop_mode) << "\n"
|
|
<< " sample clock: " << actual_sample_clock_hz << " Hz";
|
|
if (cfg.sync_mode == X502_SYNC_INTERNAL) {
|
|
std::cout << " (internal ref " << ref_freq_to_string(cfg.internal_ref_freq) << " Hz)";
|
|
if (internal_max_clock) {
|
|
std::cout << ", maximum ADC speed";
|
|
}
|
|
}
|
|
std::cout << "\n"
|
|
<< " DIN clock: " << actual_din_freq_hz << " Hz\n"
|
|
<< " ADC logical channels: " << cfg.channel_count << "\n"
|
|
<< " per-channel frame rate: " << actual_frame_freq_hz << " Hz\n"
|
|
<< " duration: " << cfg.duration_ms << " ms\n"
|
|
<< " packet limit: "
|
|
<< ((cfg.packet_limit == 0U) ? std::string("continuous until Ctrl+C")
|
|
: std::to_string(cfg.packet_limit) + " packet(s)") << "\n"
|
|
<< " live update period: "
|
|
<< ((cfg.live_update_period_ms == 0U) ? std::string("every packet")
|
|
: std::to_string(cfg.live_update_period_ms) + " ms") << "\n"
|
|
<< " channel 1: " << phy_channel_name(cfg.mode, cfg.ch1) << "\n"
|
|
<< " channel 2: "
|
|
<< ((cfg.channel_count >= 2U) ? phy_channel_name(cfg.mode, cfg.ch2) : std::string("disabled")) << "\n"
|
|
<< " ADC range: +/-" << range_to_volts(cfg.range) << " V\n"
|
|
<< " max frames per packet per channel: " << target_frames << "\n";
|
|
|
|
CaptureFileWriter writer(cfg.csv_path, cfg.svg_path, cfg.live_html_path, cfg.live_json_path);
|
|
writer.initialize_live_plot();
|
|
std::cout << " live plot html: " << writer.live_html_path() << "\n"
|
|
<< " live plot data: " << writer.live_json_path() << "\n";
|
|
|
|
ConsoleCtrlGuard console_guard;
|
|
if (!console_guard.installed) {
|
|
std::cerr << "Warning: Ctrl+C handler could not be installed; continuous mode may stop abruptly.\n";
|
|
}
|
|
|
|
expect_ok(api, api.StreamsStart(device.hnd), "Start streams");
|
|
device.streams_started = true;
|
|
|
|
std::vector<uint32_t> raw(cfg.recv_block_words);
|
|
std::vector<double> adc_buffer(cfg.recv_block_words);
|
|
std::vector<uint32_t> din_buffer(cfg.recv_block_words);
|
|
std::deque<double> pending_adc;
|
|
std::deque<uint32_t> pending_din;
|
|
std::vector<CapturePacket> packets;
|
|
if (cfg.packet_limit != 0U) {
|
|
packets.reserve(cfg.packet_limit);
|
|
}
|
|
PacketAccumulator current_packet;
|
|
current_packet.reset(target_frames, cfg.channel_count);
|
|
|
|
bool capture_started = false;
|
|
bool stop_loop_requested = false;
|
|
bool packet_active = false;
|
|
bool trigger_level_initialized = false;
|
|
bool trigger_prev_level = false;
|
|
std::size_t total_zeroed_samples = 0;
|
|
uint32_t next_lch = 0;
|
|
bool di1_initialized = false;
|
|
bool di1_prev_level = false;
|
|
const ULONGLONG start_wait_deadline = GetTickCount64() + cfg.start_wait_ms;
|
|
const ULONGLONG capture_loop_start = GetTickCount64();
|
|
ULONGLONG stats_window_start = capture_loop_start;
|
|
ULONGLONG last_stats_print = capture_loop_start;
|
|
ULONGLONG last_live_update = 0;
|
|
|
|
uint64_t total_raw_words = 0;
|
|
uint64_t total_adc_samples = 0;
|
|
uint64_t total_din_samples = 0;
|
|
uint64_t total_stored_adc_samples = 0;
|
|
uint64_t total_completed_frames = 0;
|
|
uint64_t total_completed_packets = 0;
|
|
|
|
uint64_t stats_raw_words = 0;
|
|
uint64_t stats_adc_samples = 0;
|
|
uint64_t stats_din_samples = 0;
|
|
uint64_t stats_stored_adc_samples = 0;
|
|
uint64_t stats_zeroed_samples = 0;
|
|
uint64_t stats_completed_frames = 0;
|
|
uint64_t stats_completed_packets = 0;
|
|
|
|
auto print_stats = [&](bool final_report) {
|
|
const ULONGLONG now = GetTickCount64();
|
|
const double elapsed_s = std::max(1e-9, static_cast<double>(now - stats_window_start) / 1000.0);
|
|
const double mb_per_s = (static_cast<double>(stats_raw_words) * sizeof(uint32_t)) / elapsed_s / 1000.0 / 1000.0;
|
|
const double adc_samples_per_s = static_cast<double>(stats_adc_samples) / elapsed_s;
|
|
const double din_samples_per_s = static_cast<double>(stats_din_samples) / elapsed_s;
|
|
const double frames_per_ch_per_s = static_cast<double>(stats_completed_frames) / elapsed_s;
|
|
const double packets_per_s = static_cast<double>(stats_completed_packets) / elapsed_s;
|
|
const double zeroed_fraction = (stats_stored_adc_samples == 0U)
|
|
? 0.0
|
|
: (100.0 * static_cast<double>(stats_zeroed_samples) / static_cast<double>(stats_stored_adc_samples));
|
|
|
|
std::cout << std::fixed << std::setprecision(3)
|
|
<< (final_report ? "Final stats: " : "Online stats: ")
|
|
<< "MB/s=" << mb_per_s
|
|
<< ", ADC samples/s=" << adc_samples_per_s
|
|
<< ", DIN samples/s=" << din_samples_per_s
|
|
<< ", frames/s per channel=" << frames_per_ch_per_s
|
|
<< ", packets/s=" << packets_per_s
|
|
<< ", zeroed on DI1 change=" << zeroed_fraction << "% ("
|
|
<< stats_zeroed_samples << "/" << stats_stored_adc_samples << ")\n";
|
|
|
|
if (!final_report) {
|
|
stats_window_start = now;
|
|
last_stats_print = now;
|
|
stats_raw_words = 0;
|
|
stats_adc_samples = 0;
|
|
stats_din_samples = 0;
|
|
stats_stored_adc_samples = 0;
|
|
stats_zeroed_samples = 0;
|
|
stats_completed_frames = 0;
|
|
stats_completed_packets = 0;
|
|
}
|
|
};
|
|
|
|
auto start_packet = [&]() {
|
|
if (packet_active) {
|
|
return;
|
|
}
|
|
current_packet.reset(target_frames, cfg.channel_count);
|
|
packet_active = true;
|
|
};
|
|
|
|
auto finish_packet = [&](PacketCloseReason reason) {
|
|
const std::size_t frames = current_packet.frame_count(cfg.channel_count);
|
|
if (frames != 0U) {
|
|
current_packet.channels[0].resize(frames);
|
|
if (cfg.channel_count >= 2U) {
|
|
current_packet.channels[1].resize(frames);
|
|
} else {
|
|
current_packet.channels[1].clear();
|
|
}
|
|
|
|
CapturePacket packet;
|
|
packet.packet_index = packets.size() + 1U;
|
|
packet.channel_count = cfg.channel_count;
|
|
packet.ch1 = std::move(current_packet.channels[0]);
|
|
packet.ch2 = std::move(current_packet.channels[1]);
|
|
|
|
const double packet_duration_ms = (1000.0 * static_cast<double>(frames)) / actual_frame_freq_hz;
|
|
const double zeroed_fraction = (current_packet.stored_samples == 0U)
|
|
? 0.0
|
|
: (100.0 * static_cast<double>(current_packet.zeroed_samples) /
|
|
static_cast<double>(current_packet.stored_samples));
|
|
|
|
std::cout << std::fixed << std::setprecision(3)
|
|
<< "Packet " << packet.packet_index
|
|
<< ": frames/ch=" << frames
|
|
<< ", duration_ms=" << packet_duration_ms
|
|
<< ", close_reason=" << packet_close_reason_to_string(reason)
|
|
<< ", zeroed_on_DI1_change=" << zeroed_fraction << "% ("
|
|
<< current_packet.zeroed_samples << "/" << current_packet.stored_samples << ")\n";
|
|
|
|
packets.push_back(std::move(packet));
|
|
++total_completed_packets;
|
|
++stats_completed_packets;
|
|
const double elapsed_capture_s =
|
|
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0);
|
|
const double packets_per_second =
|
|
(elapsed_capture_s >= 0.1)
|
|
? (static_cast<double>(total_completed_packets) / elapsed_capture_s)
|
|
: 0.0;
|
|
const ULONGLONG now = GetTickCount64();
|
|
const bool should_update_live =
|
|
(cfg.live_update_period_ms == 0U) ||
|
|
(last_live_update == 0U) ||
|
|
((now - last_live_update) >= cfg.live_update_period_ms);
|
|
if (should_update_live) {
|
|
writer.update_live_plot(packets.back(),
|
|
packets.size(),
|
|
packets_per_second,
|
|
actual_frame_freq_hz,
|
|
packet_close_reason_to_string(reason),
|
|
current_packet.zeroed_samples,
|
|
current_packet.stored_samples);
|
|
last_live_update = now;
|
|
}
|
|
std::cout << std::fixed << std::setprecision(3)
|
|
<< " packets/s(avg)=" << packets_per_second;
|
|
if (elapsed_capture_s < 0.1) {
|
|
std::cout << " (warming up)";
|
|
}
|
|
std::cout << "\n";
|
|
}
|
|
|
|
packet_active = false;
|
|
current_packet.reset(target_frames, cfg.channel_count);
|
|
};
|
|
|
|
while (!stop_loop_requested) {
|
|
if ((cfg.packet_limit != 0U) && (packets.size() >= cfg.packet_limit)) {
|
|
stop_loop_requested = true;
|
|
break;
|
|
}
|
|
if (console_stop_requested()) {
|
|
if (packet_active) {
|
|
finish_packet(PacketCloseReason::UserStop);
|
|
}
|
|
stop_loop_requested = true;
|
|
break;
|
|
}
|
|
|
|
const int32_t recvd = api.Recv(device.hnd, raw.data(), cfg.recv_block_words, cfg.recv_timeout_ms);
|
|
if (recvd < 0) {
|
|
fail("X502_Recv failed: " + x502_error(api, recvd));
|
|
}
|
|
if (recvd > 0) {
|
|
total_raw_words += static_cast<uint64_t>(recvd);
|
|
stats_raw_words += static_cast<uint64_t>(recvd);
|
|
}
|
|
if (recvd == 0) {
|
|
if (!capture_started && (GetTickCount64() >= start_wait_deadline)) {
|
|
std::ostringstream message;
|
|
message << "Timeout before first ADC data. start="
|
|
<< sync_mode_to_string(cfg.sync_start_mode, true)
|
|
<< ", clock=" << sync_mode_to_string(cfg.sync_mode, false) << ". ";
|
|
|
|
if (cfg.sync_start_mode == X502_SYNC_EXTERNAL_MASTER) {
|
|
message << "With start:start_in the module waits for START_IN after StreamsStart(); "
|
|
"until that condition occurs the external clock is ignored. ";
|
|
} else if (cfg.sync_start_mode == X502_SYNC_INTERNAL) {
|
|
message << "Start is immediate, so this usually means there is no valid external clock on the selected clock line. ";
|
|
} else {
|
|
message << "This usually means the selected start condition did not occur after StreamsStart(), "
|
|
"or no valid external clock arrived afterwards. ";
|
|
}
|
|
|
|
message << "For a quick clock-only check, try start:immediate. "
|
|
"If you use a separate start pulse, it must arrive after the program starts waiting. "
|
|
"You can also increase start_wait_ms.";
|
|
fail(message.str());
|
|
}
|
|
continue;
|
|
}
|
|
|
|
uint32_t adc_count = static_cast<uint32_t>(adc_buffer.size());
|
|
uint32_t din_count = static_cast<uint32_t>(din_buffer.size());
|
|
const int32_t process_err = api.ProcessData(device.hnd,
|
|
raw.data(),
|
|
static_cast<uint32_t>(recvd),
|
|
X502_PROC_FLAGS_VOLT,
|
|
adc_buffer.data(),
|
|
&adc_count,
|
|
din_buffer.data(),
|
|
&din_count);
|
|
if (process_err == X502_ERR_STREAM_OVERFLOW) {
|
|
std::ostringstream message;
|
|
message << "Process ADC+DIN data: " << x502_error(api, process_err)
|
|
<< ". Try larger buffer_words/step_words, a longer live_update_period_ms, "
|
|
<< "or a lower sample_clock_hz / DIN load.";
|
|
fail(message.str());
|
|
}
|
|
expect_ok(api, process_err, "Process ADC+DIN data");
|
|
|
|
if ((adc_count == 0U) && (din_count == 0U)) {
|
|
continue;
|
|
}
|
|
|
|
capture_started = capture_started || (adc_count != 0U);
|
|
total_adc_samples += adc_count;
|
|
total_din_samples += din_count;
|
|
stats_adc_samples += adc_count;
|
|
stats_din_samples += din_count;
|
|
for (uint32_t i = 0; i < adc_count; ++i) {
|
|
pending_adc.push_back(adc_buffer[i]);
|
|
}
|
|
for (uint32_t i = 0; i < din_count; ++i) {
|
|
pending_din.push_back(din_buffer[i]);
|
|
}
|
|
|
|
if ((pending_adc.size() > 1000000U) || (pending_din.size() > 1000000U)) {
|
|
fail("Internal backlog grew too large while aligning ADC and DIN samples");
|
|
}
|
|
|
|
while (!pending_adc.empty() &&
|
|
!pending_din.empty() &&
|
|
!stop_loop_requested) {
|
|
const double adc_value = pending_adc.front();
|
|
pending_adc.pop_front();
|
|
|
|
const uint32_t din_value = pending_din.front();
|
|
pending_din.pop_front();
|
|
|
|
const bool di1_level = (din_value & kE502Digital1Mask) != 0U;
|
|
bool zero_on_di1_change = false;
|
|
if (!di1_initialized) {
|
|
di1_prev_level = di1_level;
|
|
di1_initialized = true;
|
|
} else if (di1_level != di1_prev_level) {
|
|
zero_on_di1_change = true;
|
|
di1_prev_level = di1_level;
|
|
}
|
|
|
|
const bool di_syn2_level = (din_value & kE502DiSyn2Mask) != 0U;
|
|
bool start_edge = false;
|
|
bool stop_edge = false;
|
|
if (!trigger_level_initialized) {
|
|
trigger_prev_level = di_syn2_level;
|
|
trigger_level_initialized = true;
|
|
if (!packet_active) {
|
|
if (cfg.sync_start_mode == X502_SYNC_INTERNAL) {
|
|
start_packet();
|
|
} else if (sync_uses_di_syn2(cfg.sync_start_mode) && di_syn2_level) {
|
|
start_packet();
|
|
} else if (!sync_uses_di_syn2(cfg.sync_start_mode)) {
|
|
start_packet();
|
|
}
|
|
}
|
|
} else {
|
|
start_edge = matches_sync_edge(cfg.sync_start_mode, trigger_prev_level, di_syn2_level);
|
|
if (cfg.stop_mode != StopMode::TargetFrames) {
|
|
stop_edge = matches_stop_edge(cfg.stop_mode, trigger_prev_level, di_syn2_level);
|
|
}
|
|
trigger_prev_level = di_syn2_level;
|
|
}
|
|
|
|
if (!packet_active && (cfg.sync_start_mode == X502_SYNC_INTERNAL)) {
|
|
start_packet();
|
|
}
|
|
|
|
if (!packet_active && start_edge) {
|
|
start_packet();
|
|
}
|
|
|
|
if (packet_active && stop_edge) {
|
|
finish_packet(PacketCloseReason::ExternalStopEdge);
|
|
if ((cfg.packet_limit != 0U) && (packets.size() >= cfg.packet_limit)) {
|
|
stop_loop_requested = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!packet_active) {
|
|
continue;
|
|
}
|
|
|
|
const uint32_t lch = next_lch;
|
|
next_lch = (next_lch + 1U) % cfg.channel_count;
|
|
|
|
double stored_value = adc_value;
|
|
if (zero_on_di1_change) {
|
|
stored_value = 0.0;
|
|
++total_zeroed_samples;
|
|
++stats_zeroed_samples;
|
|
++current_packet.zeroed_samples;
|
|
}
|
|
|
|
if (current_packet.channels[lch].size() < target_frames) {
|
|
current_packet.channels[lch].push_back(stored_value);
|
|
++current_packet.stored_samples;
|
|
++total_stored_adc_samples;
|
|
++stats_stored_adc_samples;
|
|
if (lch == (cfg.channel_count - 1U)) {
|
|
++total_completed_frames;
|
|
++stats_completed_frames;
|
|
}
|
|
}
|
|
|
|
if (current_packet.frame_count(cfg.channel_count) >= target_frames) {
|
|
finish_packet(PacketCloseReason::DurationLimit);
|
|
if ((cfg.packet_limit != 0U) && (packets.size() >= cfg.packet_limit)) {
|
|
stop_loop_requested = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (console_stop_requested()) {
|
|
if (packet_active) {
|
|
finish_packet(PacketCloseReason::UserStop);
|
|
}
|
|
stop_loop_requested = true;
|
|
}
|
|
|
|
if ((cfg.stats_period_ms != 0U) && ((GetTickCount64() - last_stats_print) >= cfg.stats_period_ms)) {
|
|
print_stats(false);
|
|
}
|
|
}
|
|
|
|
expect_ok(api, api.StreamsStop(device.hnd), "Stop streams");
|
|
device.streams_started = false;
|
|
|
|
if ((cfg.stats_period_ms != 0U) && ((stats_raw_words != 0U) || (stats_adc_samples != 0U) ||
|
|
(stats_din_samples != 0U) || (stats_stored_adc_samples != 0U) || (stats_zeroed_samples != 0U) ||
|
|
(stats_completed_frames != 0U))) {
|
|
print_stats(false);
|
|
}
|
|
|
|
writer.write(packets, actual_frame_freq_hz, range_to_volts(cfg.range));
|
|
|
|
std::size_t total_packet_frames = 0;
|
|
for (const auto& packet : packets) {
|
|
total_packet_frames += (packet.channel_count <= 1U)
|
|
? packet.ch1.size()
|
|
: std::min(packet.ch1.size(), packet.ch2.size());
|
|
}
|
|
|
|
std::cout << "Captured " << packets.size() << " packet(s), "
|
|
<< total_packet_frames << " total frames per channel\n"
|
|
<< "ADC samples forced to 0 on DI1 change: " << total_zeroed_samples << "\n"
|
|
<< "Average stats: "
|
|
<< "MB/s=" << std::fixed << std::setprecision(3)
|
|
<< ((static_cast<double>(total_raw_words) * sizeof(uint32_t)) /
|
|
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0) /
|
|
1000.0 / 1000.0)
|
|
<< ", ADC samples/s="
|
|
<< (static_cast<double>(total_adc_samples) /
|
|
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0))
|
|
<< ", DIN samples/s="
|
|
<< (static_cast<double>(total_din_samples) /
|
|
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0))
|
|
<< ", frames/s per channel="
|
|
<< (static_cast<double>(total_completed_frames) /
|
|
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0))
|
|
<< ", packets/s="
|
|
<< (static_cast<double>(total_completed_packets) /
|
|
std::max(1e-9, static_cast<double>(GetTickCount64() - capture_loop_start) / 1000.0))
|
|
<< ", packets captured=" << total_completed_packets
|
|
<< ", zeroed on DI1 change="
|
|
<< ((total_stored_adc_samples == 0U)
|
|
? 0.0
|
|
: (100.0 * static_cast<double>(total_zeroed_samples) / static_cast<double>(total_stored_adc_samples)))
|
|
<< "% (" << total_zeroed_samples << "/" << total_stored_adc_samples << ")\n"
|
|
<< "CSV: " << cfg.csv_path << "\n"
|
|
<< "SVG: " << cfg.svg_path << "\n";
|
|
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
try {
|
|
const Config cfg = parse_args(argc, argv);
|
|
return run(cfg);
|
|
} catch (const std::exception& ex) {
|
|
std::cerr << "Error: " << ex.what() << "\n";
|
|
return 1;
|
|
}
|
|
}
|