Files
kamil_adc/main.cpp
2026-04-07 17:22:17 +03:00

1001 lines
38 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 <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fstream>
#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 mode = X502_LCH_MODE_DIFF;
uint32_t range = X502_ADC_RANGE_5;
uint32_t ch1 = 2;
uint32_t ch2 = 3;
double sample_clock_hz = 2000000.0;
double duration_ms = 40.0;
uint32_t sync_mode = X502_SYNC_EXTERNAL_MASTER;
uint32_t sync_start_mode = X502_SYNC_DI_SYN1_RISE;
StopMode stop_mode = StopMode::TargetFrames;
uint32_t recv_block_words = 8192;
uint32_t recv_timeout_ms = 100;
uint32_t start_wait_ms = 10000;
uint32_t input_buffer_words = 262144;
uint32_t input_step_words = 8192;
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";
};
[[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_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] [ch1:2] [ch2:3]\n"
<< " [mode:diff|comm] [range:5] [clock:conv_in]\n"
<< " [start:di_syn1_rise] [stop:frames] [sample_clock_hz:2000000]\n"
<< " [duration_ms:40] [csv:capture.csv] [svg:capture.svg]\n"
<< " [recv_block:8192] [start_wait_ms:10000]\n"
<< " [pullup_syn1] [pullup_syn2] [pulldown_conv_in] [pulldown_start_in]\n"
<< "\n"
<< "Defaults for E-502:\n"
<< " ch1:2, ch2:3 -> X3-Y3 and X4-Y4\n"
<< " mode:diff -> differential measurement\n"
<< " range:5 -> +/-5 V range\n"
<< " clock:conv_in -> external sample clock on CONV_IN\n"
<< " start:di_syn1_rise-> start on DI_SYN1 rising edge\n"
<< " stop:frames -> stop after duration_ms worth of frames\n"
<< " duration_ms:40 -> capture one 40 ms chirp or max length when stop is external\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"
<< "Stop on DI_SYN2 is polled asynchronously via X502_AsyncInDig(), so ADC stream stays\n"
<< "ADC-only. For reliable stop detection hold DI_SYN2 active until the program stops.\n"
<< "\n"
<< "Recommended working example:\n"
<< " " << exe_name
<< " clock:di_syn1_rise start:start_in stop:di_syn2_rise sample_clock_hz:2000000"
<< " duration_ms:40 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, "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:")) {
cfg.sample_clock_hz = parse_double(arg.substr(16), "sample_clock_hz");
continue;
}
if (starts_with(arg, "duration_ms:")) {
cfg.duration_ms = parse_double(arg.substr(12), "duration_ms");
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, "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;
}
fail("Unknown argument: " + arg);
}
if (cfg.duration_ms <= 0.0) {
fail("duration_ms must be > 0");
}
if (cfg.sample_clock_hz <= 0.0) {
fail("sample_clock_hz must be > 0");
}
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::TargetFrames) && sync_uses_di_syn2(cfg.sync_start_mode)) {
fail("DI_SYN2 cannot be used simultaneously for start and stop");
}
if (cfg.mode == X502_LCH_MODE_DIFF) {
if ((cfg.ch1 >= X502_ADC_DIFF_CH_CNT) || (cfg.ch2 >= X502_ADC_DIFF_CH_CNT)) {
fail("For differential mode E-502 channels must be in range 0..15");
}
} else {
if ((cfg.ch1 >= X502_ADC_COMM_CH_CNT) || (cfg.ch2 >= X502_ADC_COMM_CH_CNT)) {
fail("For common-ground mode E-502 channels 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_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_AsyncInDig) AsyncInDig = nullptr;
decltype(&X502_Recv) Recv = nullptr;
decltype(&X502_ProcessAdcData) ProcessAdcData = 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");
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");
AsyncInDig = load_symbol<decltype(AsyncInDig)>(x502_module, "X502_AsyncInDig");
Recv = load_symbol<decltype(Recv)>(x502_module, "X502_Recv");
ProcessAdcData = load_symbol<decltype(ProcessAdcData)>(x502_module, "X502_ProcessAdcData");
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);
bool read_di_syn2_level(const Api& api, t_x502_hnd hnd) {
uint32_t din = 0;
expect_ok(api, api.AsyncInDig(hnd, &din), "Read digital inputs");
return (din & kE502DiSyn2Mask) != 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";
}
struct PlotPoint {
std::size_t sample_index = 0;
double value = 0.0;
};
std::vector<PlotPoint> build_min_max_trace(const std::vector<double>& data, std::size_t max_columns) {
std::vector<PlotPoint> result;
if (data.empty()) {
return result;
}
const std::size_t bucket_size = std::max<std::size_t>(1, (data.size() + max_columns - 1) / max_columns);
result.reserve(max_columns * 2);
for (std::size_t begin = 0; begin < data.size(); begin += bucket_size) {
const std::size_t end = std::min(begin + bucket_size, data.size());
std::size_t min_index = begin;
std::size_t max_index = begin;
for (std::size_t i = begin + 1; i < end; ++i) {
if (data[i] < data[min_index]) {
min_index = i;
}
if (data[i] > data[max_index]) {
max_index = i;
}
}
if (min_index <= max_index) {
result.push_back({min_index, data[min_index]});
if (max_index != min_index) {
result.push_back({max_index, data[max_index]});
}
} else {
result.push_back({max_index, data[max_index]});
result.push_back({min_index, data[min_index]});
}
}
if (result.back().sample_index != (data.size() - 1)) {
result.push_back({data.size() - 1, data.back()});
}
return result;
}
std::string polyline_points(const std::vector<PlotPoint>& trace,
double max_time_s,
double y_min,
double y_max,
double left,
double top,
double width,
double height) {
std::ostringstream out;
out << std::fixed << std::setprecision(3);
const double y_span = std::max(1e-12, y_max - y_min);
const std::size_t max_index = trace.empty() ? 1 : trace.back().sample_index;
const double time_scale = (max_time_s > 0.0) ? max_time_s : 1.0;
for (const auto& point : trace) {
const double time_s = (max_index == 0)
? 0.0
: (static_cast<double>(point.sample_index) / static_cast<double>(max_index)) * time_scale;
const double x = left + (time_s / time_scale) * width;
const double y = top + height - ((point.value - y_min) / y_span) * height;
out << x << "," << y << " ";
}
return out.str();
}
void write_csv(const std::string& path,
const std::vector<double>& ch1,
const std::vector<double>& ch2,
double frame_freq_hz) {
std::ofstream file(path, std::ios::binary);
if (!file) {
fail("Cannot open CSV for writing: " + path);
}
file << "frame_index,time_s,ch1_v,ch2_v\n";
file << std::fixed << std::setprecision(9);
const std::size_t frames = std::min(ch1.size(), ch2.size());
for (std::size_t i = 0; i < frames; ++i) {
const double time_s = static_cast<double>(i) / frame_freq_hz;
file << i << "," << time_s << "," << ch1[i] << "," << ch2[i] << "\n";
}
}
void write_svg(const std::string& path,
const std::vector<double>& ch1,
const std::vector<double>& ch2,
double frame_freq_hz,
double nominal_range_v) {
std::ofstream file(path, std::ios::binary);
if (!file) {
fail("Cannot open SVG for writing: " + path);
}
const std::size_t frames = std::min(ch1.size(), ch2.size());
const double total_time_s = (frames > 1) ? (static_cast<double>(frames - 1) / frame_freq_hz) : 0.0;
double min_y = std::numeric_limits<double>::infinity();
double max_y = -std::numeric_limits<double>::infinity();
for (double v : ch1) {
min_y = std::min(min_y, v);
max_y = std::max(max_y, v);
}
for (double v : ch2) {
min_y = std::min(min_y, v);
max_y = std::max(max_y, v);
}
if (!std::isfinite(min_y) || !std::isfinite(max_y) || (min_y == max_y)) {
min_y = -nominal_range_v;
max_y = nominal_range_v;
} else {
const double pad = std::max(0.1, (max_y - min_y) * 0.08);
min_y -= pad;
max_y += pad;
}
const auto trace1 = build_min_max_trace(ch1, 1800);
const auto trace2 = build_min_max_trace(ch2, 1800);
const double width = 1400.0;
const double height = 800.0;
const double left = 90.0;
const double right = 40.0;
const double top = 40.0;
const double bottom = 80.0;
const double plot_w = width - left - right;
const double plot_h = height - top - bottom;
const double zero_y = top + plot_h - ((0.0 - min_y) / std::max(1e-12, max_y - min_y)) * plot_h;
file << "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" << width
<< "\" height=\"" << height << "\" viewBox=\"0 0 " << width << " " << height << "\">\n";
file << " <rect width=\"100%\" height=\"100%\" fill=\"#ffffff\"/>\n";
file << " <rect x=\"" << left << "\" y=\"" << top << "\" width=\"" << plot_w
<< "\" height=\"" << plot_h << "\" fill=\"#fbfcfe\" stroke=\"#ccd4dd\"/>\n";
for (int i = 0; i <= 10; ++i) {
const double x = left + (plot_w * i / 10.0);
const double y = top + (plot_h * i / 10.0);
file << " <line x1=\"" << x << "\" y1=\"" << top << "\" x2=\"" << x << "\" y2=\"" << (top + plot_h)
<< "\" stroke=\"#edf1f5\" stroke-width=\"1\"/>\n";
file << " <line x1=\"" << left << "\" y1=\"" << y << "\" x2=\"" << (left + plot_w) << "\" y2=\"" << y
<< "\" stroke=\"#edf1f5\" stroke-width=\"1\"/>\n";
}
if ((0.0 >= min_y) && (0.0 <= max_y)) {
file << " <line x1=\"" << left << "\" y1=\"" << zero_y << "\" x2=\"" << (left + plot_w)
<< "\" y2=\"" << zero_y << "\" stroke=\"#8fa1b3\" stroke-width=\"1.2\"/>\n";
}
file << " <polyline fill=\"none\" stroke=\"#005bbb\" stroke-width=\"1.2\" points=\""
<< polyline_points(trace1, total_time_s, min_y, max_y, left, top, plot_w, plot_h) << "\"/>\n";
file << " <polyline fill=\"none\" stroke=\"#d62828\" stroke-width=\"1.2\" points=\""
<< polyline_points(trace2, total_time_s, min_y, max_y, left, top, plot_w, plot_h) << "\"/>\n";
file << " <text x=\"" << left << "\" y=\"24\" font-size=\"22\" font-family=\"Segoe UI, Arial, sans-serif\""
<< " fill=\"#203040\">E-502 capture: CH1 and CH2</text>\n";
file << " <text x=\"" << left << "\" y=\"" << (height - 22)
<< "\" font-size=\"16\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#425466\">time, s</text>\n";
file << " <text x=\"18\" y=\"" << (top + 16)
<< "\" font-size=\"16\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#425466\">V</text>\n";
file << std::fixed << std::setprecision(6);
for (int i = 0; i <= 10; ++i) {
const double x = left + (plot_w * i / 10.0);
const double t = total_time_s * i / 10.0;
file << " <text x=\"" << x << "\" y=\"" << (top + plot_h + 24)
<< "\" text-anchor=\"middle\" font-size=\"12\" font-family=\"Segoe UI, Arial, sans-serif\""
<< " fill=\"#607080\">" << t << "</text>\n";
const double y = top + plot_h - (plot_h * i / 10.0);
const double v = min_y + (max_y - min_y) * i / 10.0;
file << " <text x=\"" << (left - 10) << "\" y=\"" << (y + 4)
<< "\" text-anchor=\"end\" font-size=\"12\" font-family=\"Segoe UI, Arial, sans-serif\""
<< " fill=\"#607080\">" << v << "</text>\n";
}
const double legend_y = height - 48.0;
file << " <line x1=\"" << (width - 270) << "\" y1=\"" << legend_y
<< "\" x2=\"" << (width - 230) << "\" y2=\"" << legend_y
<< "\" stroke=\"#005bbb\" stroke-width=\"3\"/>\n";
file << " <text x=\"" << (width - 220) << "\" y=\"" << (legend_y + 4)
<< "\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">CH1</text>\n";
file << " <line x1=\"" << (width - 160) << "\" y1=\"" << legend_y
<< "\" x2=\"" << (width - 120) << "\" y2=\"" << legend_y
<< "\" stroke=\"#d62828\" stroke-width=\"3\"/>\n";
file << " <text x=\"" << (width - 110) << "\" y=\"" << (legend_y + 4)
<< "\" font-size=\"14\" font-family=\"Segoe UI, Arial, sans-serif\" fill=\"#203040\">CH2</text>\n";
file << "</svg>\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) {
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, 2), "Set logical channel count");
expect_ok(api, api.SetLChannel(device.hnd, 0, cfg.ch1, cfg.mode, cfg.range, 1), "Set logical channel 0");
expect_ok(api, api.SetLChannel(device.hnd, 1, cfg.ch2, cfg.mode, cfg.range, 1), "Set logical channel 1");
expect_ok(api, api.SetAdcFreqDivider(device.hnd, 1), "Set ADC frequency divider");
expect_ok(api, api.SetAdcInterframeDelay(device.hnd, 0), "Set ADC interframe delay");
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), "Enable ADC stream");
const double frame_freq_hz = cfg.sample_clock_hz / 2.0;
const std::size_t target_frames = std::max<std::size_t>(
1, static_cast<std::size_t>(std::llround((cfg.duration_ms / 1000.0) * 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: " << cfg.sample_clock_hz << " Hz\n"
<< " per-channel frame rate: " << frame_freq_hz << " Hz\n"
<< " duration: " << cfg.duration_ms << " ms\n"
<< " channel 1: " << phy_channel_name(cfg.mode, cfg.ch1) << "\n"
<< " channel 2: " << phy_channel_name(cfg.mode, cfg.ch2) << "\n"
<< " ADC range: +/-" << range_to_volts(cfg.range) << " V\n"
<< " target frames per channel: " << target_frames << "\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> processed(cfg.recv_block_words);
std::array<std::vector<double>, 2> channels;
channels[0].reserve(target_frames);
channels[1].reserve(target_frames);
bool capture_started = false;
bool stop_requested = false;
bool stop_prev_initialized = false;
bool stop_prev_level = false;
bool stopped_by_external_signal = false;
uint32_t next_lch = 0;
const ULONGLONG start_wait_deadline = GetTickCount64() + cfg.start_wait_ms;
while (!stop_requested &&
((channels[0].size() < target_frames) || (channels[1].size() < target_frames))) {
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) {
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>(recvd);
expect_ok(api,
api.ProcessAdcData(device.hnd, raw.data(), processed.data(), &adc_count, X502_PROC_FLAGS_VOLT),
"Process ADC data");
if (adc_count == 0) {
continue;
}
capture_started = true;
for (uint32_t i = 0; i < adc_count; ++i) {
const uint32_t lch = next_lch;
next_lch = (next_lch + 1U) % 2U;
if (channels[lch].size() < target_frames) {
channels[lch].push_back(processed[i]);
}
if ((channels[0].size() >= target_frames) && (channels[1].size() >= target_frames)) {
break;
}
}
if ((cfg.stop_mode != StopMode::TargetFrames) && capture_started) {
const bool stop_level = read_di_syn2_level(api, device.hnd);
if (!stop_prev_initialized) {
stop_prev_level = stop_level;
stop_prev_initialized = true;
} else {
const bool is_edge =
((cfg.stop_mode == StopMode::DiSyn2Rise) && !stop_prev_level && stop_level) ||
((cfg.stop_mode == StopMode::DiSyn2Fall) && stop_prev_level && !stop_level);
if (is_edge) {
stop_requested = true;
stopped_by_external_signal = true;
}
stop_prev_level = stop_level;
}
}
}
expect_ok(api, api.StreamsStop(device.hnd), "Stop streams");
device.streams_started = false;
const std::size_t frames = std::min(channels[0].size(), channels[1].size());
channels[0].resize(frames);
channels[1].resize(frames);
write_csv(cfg.csv_path, channels[0], channels[1], frame_freq_hz);
write_svg(cfg.svg_path, channels[0], channels[1], frame_freq_hz, range_to_volts(cfg.range));
std::cout << "Captured " << frames << " frames per channel\n"
<< "Stop reason: "
<< (stopped_by_external_signal ? "DI_SYN2 edge" : "target frame count") << "\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;
}
}