diff --git a/build_lchm_clock_counter.sh b/build_lchm_clock_counter.sh new file mode 100755 index 0000000..7210fd8 --- /dev/null +++ b/build_lchm_clock_counter.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +CXX="${CXX:-g++}" +OUT="${OUT:-lchm_clock_counter.exe}" + +CXXFLAGS=( + -std=c++17 + -O2 + -Wall + -Wextra + -pedantic +) + +SOURCES=( + lchm_clock_counter.cpp +) + +LDFLAGS=( + -ldl +) + +"$CXX" "${CXXFLAGS[@]}" "${SOURCES[@]}" "${LDFLAGS[@]}" -o "$OUT" + +echo "Built $OUT" diff --git a/lchm_clock_counter.cpp b/lchm_clock_counter.cpp new file mode 100644 index 0000000..5ac743c --- /dev/null +++ b/lchm_clock_counter.cpp @@ -0,0 +1,883 @@ +#ifdef _WIN32 + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include +#else + #include + #include +#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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr uint32_t kE502DiSyn2Mask = + (static_cast(1U) << 13U) | (static_cast(1U) << 17U); +constexpr uint32_t kE502Digital2Mask = (static_cast(1U) << 1U); + +struct Config { + std::string serial; + std::optional ip_addr; + + uint32_t clock_mode = X502_SYNC_DI_SYN1_RISE; + double clock_hz = 2000000.0; + uint32_t windows = 1000; + + uint32_t recv_block_words = 8192; + uint32_t recv_timeout_ms = 50; + uint32_t clock_wait_ms = 5000; + uint32_t lchm_wait_ms = 5000; + uint32_t input_buffer_words = 262144; + uint32_t input_step_words = 8192; + + bool pullup_syn1 = false; + bool pullup_syn2 = false; +}; + +struct LchmClockCount { + uint64_t clocks = 0; + uint64_t di2_high_clocks = 0; + uint64_t di2_low_clocks = 0; + + void clear() { + clocks = 0; + di2_high_clocks = 0; + di2_low_clocks = 0; + } + + void add(bool di2_high) { + ++clocks; + if (di2_high) { + ++di2_high_clocks; + } else { + ++di2_low_clocks; + } + } +}; + +struct RunningStats { + uint64_t count = 0; + uint64_t clocks_min = std::numeric_limits::max(); + uint64_t clocks_max = 0; + uint64_t clocks_sum = 0; + uint64_t high_min = std::numeric_limits::max(); + uint64_t high_max = 0; + uint64_t high_sum = 0; + uint64_t low_min = std::numeric_limits::max(); + uint64_t low_max = 0; + uint64_t low_sum = 0; + + void add(const LchmClockCount& value) { + ++count; + clocks_min = std::min(clocks_min, value.clocks); + clocks_max = std::max(clocks_max, value.clocks); + clocks_sum += value.clocks; + high_min = std::min(high_min, value.di2_high_clocks); + high_max = std::max(high_max, value.di2_high_clocks); + high_sum += value.di2_high_clocks; + low_min = std::min(low_min, value.di2_low_clocks); + low_max = std::max(low_max, value.di2_low_clocks); + low_sum += value.di2_low_clocks; + } +}; + +[[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::max())) { + fail("Invalid integer for " + field_name + ": " + text); + } + return static_cast(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 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] > 255U) { + fail("IPv4 byte out of range: " + token); + } + } + if (std::getline(ss, token, '.')) { + fail("Invalid IPv4 address: " + text); + } + return (parts[0] << 24U) | (parts[1] << 16U) | (parts[2] << 8U) | parts[3]; +} + +std::string ipv4_to_string(uint32_t ip_addr) { + std::ostringstream out; + out << ((ip_addr >> 24U) & 0xFFU) << '.' + << ((ip_addr >> 16U) & 0xFFU) << '.' + << ((ip_addr >> 8U) & 0xFFU) << '.' + << (ip_addr & 0xFFU); + return out.str(); +} + +uint32_t parse_clock_mode(const std::string& text) { + const std::string value = trim_copy(text); + if (value == "di_syn1_rise") { + return X502_SYNC_DI_SYN1_RISE; + } + if (value == "di_syn1_fall") { + return X502_SYNC_DI_SYN1_FALL; + } + fail("Only clock:di_syn1_rise or clock:di_syn1_fall are supported"); +} + +std::string clock_mode_to_string(uint32_t mode) { + switch (mode) { + case X502_SYNC_DI_SYN1_RISE: + return "di_syn1_rise"; + case X502_SYNC_DI_SYN1_FALL: + return "di_syn1_fall"; + default: + return "unknown"; + } +} + +void print_help(const char* exe_name) { + std::cout + << "Usage:\n" + << " " << exe_name << " [serial:SN] [ip:192.168.0.10]\n" + << " [clock:di_syn1_rise] [clock_hz:2000000]\n" + << " [windows:1000]\n" + << " [recv_block:8192] [recv_timeout_ms:50]\n" + << " [clock_wait_ms:5000] [lchm_wait_ms:5000]\n" + << " [buffer_words:262144] [step_words:8192]\n" + << " [pullup_syn1] [pullup_syn2]\n" + << "\n" + << "Fixed counting scheme:\n" + << " DI_SYN1 -> external clock; one synchronous DIN sample is one clock\n" + << " DI_SYN2 -> LCHM window; rising edge starts, falling edge stops\n" + << " DI2 -> extra digital level split into high/low clock counts\n" + << "\n" + << "If DI_SYN2 is already high at startup, the first partial LCHM is skipped\n" + << "until a clean low->high transition is observed.\n" + << "\n" + << "Example:\n" + << " " << exe_name << " clock:di_syn1_rise clock_hz:2000000 windows:1000\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 (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, "clock:")) { + cfg.clock_mode = parse_clock_mode(arg.substr(6)); + continue; + } + if (starts_with(arg, "clock_hz:")) { + cfg.clock_hz = parse_double(arg.substr(9), "clock_hz"); + continue; + } + if (starts_with(arg, "sample_clock_hz:")) { + cfg.clock_hz = parse_double(arg.substr(16), "sample_clock_hz"); + continue; + } + if (starts_with(arg, "windows:")) { + cfg.windows = parse_u32(arg.substr(8), "windows"); + 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, "clock_wait_ms:")) { + cfg.clock_wait_ms = parse_u32(arg.substr(14), "clock_wait_ms"); + continue; + } + if (starts_with(arg, "lchm_wait_ms:")) { + cfg.lchm_wait_ms = parse_u32(arg.substr(13), "lchm_wait_ms"); + continue; + } + if (starts_with(arg, "window_wait_ms:")) { + cfg.lchm_wait_ms = parse_u32(arg.substr(15), "window_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; + } + fail("Unknown argument: " + arg); + } + + if (cfg.clock_hz <= 0.0) { + fail("clock_hz must be > 0"); + } + if (cfg.windows == 0U) { + fail("windows must be > 0"); + } + if (cfg.recv_block_words == 0U) { + fail("recv_block must be > 0"); + } + if (cfg.input_step_words == 0U) { + 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; + } + + return cfg; +} + +#ifdef _WIN32 +using ModuleHandle = HMODULE; +#else +using ModuleHandle = void*; +#endif + +std::string dynamic_loader_error() { +#ifdef _WIN32 + return "unknown error"; +#else + const char* error = dlerror(); + return ((error != nullptr) && (*error != '\0')) ? std::string(error) : std::string("unknown error"); +#endif +} + +ModuleHandle open_library(const char* path) { +#ifdef _WIN32 + return LoadLibraryA(path); +#else + dlerror(); + return dlopen(path, RTLD_LAZY | RTLD_LOCAL); +#endif +} + +void close_library(ModuleHandle module) { +#ifdef _WIN32 + if (module != nullptr) { + FreeLibrary(module); + } +#else + if (module != nullptr) { + dlclose(module); + } +#endif +} + +ModuleHandle load_library_or_fail(const std::vector& candidates, + const std::string& description) { + std::string last_error = "no candidates provided"; + for (const auto& candidate : candidates) { + ModuleHandle module = open_library(candidate.c_str()); + if (module != nullptr) { + return module; + } + last_error = dynamic_loader_error(); + } + + std::ostringstream out; + out << "Cannot load " << description << ". Tried:"; + for (const auto& candidate : candidates) { + out << " " << candidate; + } + out << ". Last error: " << last_error; + fail(out.str()); +} + +template +Fn load_symbol(ModuleHandle module, const char* name) { +#ifdef _WIN32 + const auto addr = GetProcAddress(module, name); + if (addr == nullptr) { + fail(std::string("GetProcAddress failed for symbol: ") + name); + } + return reinterpret_cast(addr); +#else + dlerror(); + void* addr = dlsym(module, name); + const char* error = dlerror(); + if ((addr == nullptr) || (error != nullptr)) { + std::ostringstream out; + out << "dlsym failed for symbol " << name << ": " + << ((error != nullptr) ? error : "unknown error"); + fail(out.str()); + } + return reinterpret_cast(addr); +#endif +} + +template +Fn try_load_symbol(ModuleHandle module, const char* name) { +#ifdef _WIN32 + const auto addr = GetProcAddress(module, name); + return reinterpret_cast(addr); +#else + dlerror(); + void* addr = dlsym(module, name); + (void) dlerror(); + return reinterpret_cast(addr); +#endif +} + +struct Api { + ModuleHandle x502_module = nullptr; + ModuleHandle 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_GetDevInfo) GetDevInfo = 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_SetDinFreqDivider) SetDinFreqDivider = 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_GetRecvReadyCount) GetRecvReadyCount = nullptr; + decltype(&X502_Recv) Recv = nullptr; + decltype(&X502_ProcessData) ProcessData = nullptr; + + decltype(&E502_OpenUsb) OpenUsb = nullptr; + decltype(&E502_OpenByIpAddr) OpenByIpAddr = nullptr; + + Api() { + x502_module = load_library_or_fail( +#ifdef _WIN32 + {"x502api.dll"}, +#else + {"libx502api.so", "x502api.so", "./libx502api.so", "./x502api.so"}, +#endif + "x502 API library"); + e502_module = load_library_or_fail( +#ifdef _WIN32 + {"e502api.dll"}, +#else + {"libe502api.so", "e502api.so", "./libe502api.so", "./e502api.so"}, +#endif + "e502 API library"); + + Create = load_symbol(x502_module, "X502_Create"); + Free = load_symbol(x502_module, "X502_Free"); + Close = load_symbol(x502_module, "X502_Close"); + GetErrorString = load_symbol(x502_module, "X502_GetErrorString"); + GetDevInfo = try_load_symbol(x502_module, "X502_GetDevInfo"); + GetDevInfo2 = try_load_symbol(x502_module, "X502_GetDevInfo2"); + if ((GetDevInfo == nullptr) && (GetDevInfo2 == nullptr)) { + fail("Neither X502_GetDevInfo nor X502_GetDevInfo2 is available in x502 API library"); + } + SetMode = load_symbol(x502_module, "X502_SetMode"); + StreamsStop = load_symbol(x502_module, "X502_StreamsStop"); + StreamsDisable = load_symbol(x502_module, "X502_StreamsDisable"); + SetSyncMode = load_symbol(x502_module, "X502_SetSyncMode"); + SetSyncStartMode = load_symbol(x502_module, "X502_SetSyncStartMode"); + SetDinFreqDivider = load_symbol(x502_module, "X502_SetDinFreqDivider"); + SetStreamBufSize = load_symbol(x502_module, "X502_SetStreamBufSize"); + SetStreamStep = load_symbol(x502_module, "X502_SetStreamStep"); + SetDigInPullup = load_symbol(x502_module, "X502_SetDigInPullup"); + SetExtRefFreqValue = load_symbol(x502_module, "X502_SetExtRefFreqValue"); + Configure = load_symbol(x502_module, "X502_Configure"); + StreamsEnable = load_symbol(x502_module, "X502_StreamsEnable"); + StreamsStart = load_symbol(x502_module, "X502_StreamsStart"); + GetRecvReadyCount = load_symbol(x502_module, "X502_GetRecvReadyCount"); + Recv = load_symbol(x502_module, "X502_Recv"); + ProcessData = load_symbol(x502_module, "X502_ProcessData"); + + OpenUsb = load_symbol(e502_module, "E502_OpenUsb"); + OpenByIpAddr = load_symbol(e502_module, "E502_OpenByIpAddr"); + } + + ~Api() { + close_library(e502_module); + close_library(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)); + } +} + +using TickMs = uint64_t; + +TickMs tick_count_ms() { +#ifdef _WIN32 + return static_cast(GetTickCount64()); +#else + using namespace std::chrono; + return static_cast( + duration_cast(steady_clock::now().time_since_epoch()).count()); +#endif +} + +#ifdef _WIN32 +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; +} +#else +volatile std::sig_atomic_t g_console_stop_requested = 0; + +void console_ctrl_handler(int) { + g_console_stop_requested = 1; +} + +bool console_stop_requested() { + return g_console_stop_requested != 0; +} +#endif + +struct ConsoleCtrlGuard { + bool installed = false; + + ConsoleCtrlGuard() { +#ifdef _WIN32 + installed = SetConsoleCtrlHandler(console_ctrl_handler, TRUE) != 0; +#else + struct sigaction action {}; + action.sa_handler = console_ctrl_handler; + sigemptyset(&action.sa_mask); + installed = (sigaction(SIGINT, &action, nullptr) == 0); +#endif + } + + ~ConsoleCtrlGuard() { +#ifdef _WIN32 + if (installed) { + SetConsoleCtrlHandler(console_ctrl_handler, FALSE); + } +#else + if (installed) { + struct sigaction action {}; + action.sa_handler = SIG_DFL; + sigemptyset(&action.sa_mask); + sigaction(SIGINT, &action, nullptr); + } +#endif + } +}; + +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(info.fpga_ver >> 8U) << "." + << static_cast(info.fpga_ver & 0xFFU) << "\n" + << "PLDA version: " << static_cast(info.plda_ver) << "\n" + << "Board revision: " << static_cast(info.board_rev) << "\n" + << "MCU firmware: " << info.mcu_firmware_ver << "\n"; +} + +void print_summary(const RunningStats& stats) { + if (stats.count == 0U) { + std::cout << "No complete LCHM windows captured\n"; + return; + } + + const auto avg = [count = stats.count](uint64_t sum) { + return static_cast(sum) / static_cast(count); + }; + + std::cout << std::fixed << std::setprecision(3) + << "Summary: windows=" << stats.count << "\n" + << " clocks: min=" << stats.clocks_min + << ", avg=" << avg(stats.clocks_sum) + << ", max=" << stats.clocks_max << "\n" + << " di2_high_clocks: min=" << stats.high_min + << ", avg=" << avg(stats.high_sum) + << ", max=" << stats.high_max << "\n" + << " di2_low_clocks: min=" << stats.low_min + << ", avg=" << avg(stats.low_sum) + << ", max=" << stats.low_max << "\n"; +} + +int run(const Config& cfg) { + Api api; + DeviceHandle device(api); + + int32_t open_err = X502_ERR_OK; + if (cfg.ip_addr.has_value()) { + open_err = api.OpenByIpAddr(device.hnd, *cfg.ip_addr, 0, 5000); + } else { + open_err = api.OpenUsb(device.hnd, cfg.serial.empty() ? nullptr : cfg.serial.c_str()); + } + expect_ok(api, open_err, cfg.ip_addr.has_value() + ? ("Open device by IP " + ipv4_to_string(*cfg.ip_addr)) + : std::string("Open device over USB")); + device.opened = true; + + t_x502_info info {}; + int32_t info_err = X502_ERR_OK; + if (api.GetDevInfo2 != nullptr) { + info_err = api.GetDevInfo2(device.hnd, &info, sizeof(info)); + } else { + info_err = api.GetDevInfo(device.hnd, &info); + } + expect_ok(api, info_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.clock_mode), "Set external clock on DI_SYN1"); + expect_ok(api, api.SetSyncStartMode(device.hnd, X502_SYNC_INTERNAL), "Set immediate stream start"); + + const int32_t ext_ref_err = api.SetExtRefFreqValue(device.hnd, cfg.clock_hz); + if (ext_ref_err != X502_ERR_OK) { + if (cfg.clock_hz <= 1500000.0) { + expect_ok(api, ext_ref_err, "Set external reference frequency"); + } else { + std::cerr << "Warning: X502_SetExtRefFreqValue(" << cfg.clock_hz + << ") failed, continuing with DIN divider 1: " + << x502_error(api, ext_ref_err) << "\n"; + } + } + + expect_ok(api, api.SetDinFreqDivider(device.hnd, 1), "Set DIN frequency divider to one clock"); + 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; + } + expect_ok(api, api.SetDigInPullup(device.hnd, pullups), "Set digital input pullups"); + + expect_ok(api, api.Configure(device.hnd, 0), "Configure device"); + expect_ok(api, api.StreamsEnable(device.hnd, X502_STREAM_DIN), "Enable DIN stream"); + + std::cout << "LCHM clock counter settings:\n" + << " clock source: " << clock_mode_to_string(cfg.clock_mode) << "\n" + << " nominal clock: " << cfg.clock_hz << " Hz\n" + << " LCHM gate: DI_SYN2 high window\n" + << " DI2 split: enabled\n" + << " target windows: " << cfg.windows << "\n" + << " recv block words: " << cfg.recv_block_words << "\n" + << " input step words: " << cfg.input_step_words << "\n" + << " input buffer words: " << cfg.input_buffer_words << "\n"; + + ConsoleCtrlGuard console_guard; + if (!console_guard.installed) { + std::cerr << "Warning: Ctrl+C handler could not be installed; stop may be abrupt.\n"; + } + + expect_ok(api, api.StreamsStart(device.hnd), "Start streams"); + device.streams_started = true; + + std::vector raw(cfg.recv_block_words); + std::vector din_buffer(cfg.recv_block_words); + + bool gate_initialized = false; + bool last_gate = false; + bool saw_low_before_capture = false; + bool in_lchm = false; + LchmClockCount current; + RunningStats stats; + + const TickMs session_start = tick_count_ms(); + TickMs last_stream_activity = session_start; + TickMs last_lchm_complete = session_start; + TickMs last_din_activity = session_start; + TickMs last_gate_edge = session_start; + + uint64_t total_raw_words = 0; + uint64_t total_din_words = 0; + uint64_t total_gate_edges = 0; + + auto finalize_lchm = [&]() { + if (current.clocks != (current.di2_high_clocks + current.di2_low_clocks)) { + std::ostringstream message; + message << "DI2 clock split invariant failed: clocks=" << current.clocks + << ", high=" << current.di2_high_clocks + << ", low=" << current.di2_low_clocks; + fail(message.str()); + } + + if (current.clocks != 0U) { + const uint64_t lchm_index = stats.count + 1U; + stats.add(current); + std::cout << "LCHM " << lchm_index + << ": clocks=" << current.clocks + << ", di2_high_clocks=" << current.di2_high_clocks + << ", di2_low_clocks=" << current.di2_low_clocks + << "\n"; + last_lchm_complete = tick_count_ms(); + } + current.clear(); + }; + + auto fail_waiting_for_lchm = [&](TickMs now) { + std::ostringstream message; + message << "ADC/DIN clock is present, but no complete DI_SYN2 LCHM window was captured within " + << cfg.lchm_wait_ms << " ms. " + << "DIN samples=" << total_din_words + << ", gate edges=" << total_gate_edges << ". "; + + if (total_din_words == 0U) { + message << "No synchronous DIN words were decoded. "; + } else if (!gate_initialized) { + message << "DIN data is present, but DI_SYN2 state was not initialized yet. "; + } else if (total_gate_edges == 0U) { + message << "DI_SYN2 appears stuck " << (last_gate ? "HIGH" : "LOW") << ". "; + } else if (in_lchm) { + message << "DI_SYN2 produced a rising edge, but no matching falling edge closed the LCHM. "; + } else { + message << "DI_SYN2 toggled, but no complete low->high->low LCHM was accepted. "; + } + + message << "Check DI_SYN2 wiring, common DGND, and signal level around 0/3.3 V. " + << "Progress: last DIN activity " << (now - last_din_activity) + << " ms ago, last DI_SYN2 edge " << (now - last_gate_edge) << " ms ago."; + fail(message.str()); + }; + + while ((stats.count < cfg.windows) && !console_stop_requested()) { + uint32_t recv_request_words = cfg.recv_block_words; + uint32_t recv_timeout_ms = cfg.recv_timeout_ms; + uint32_t ready_words = 0; + const int32_t ready_err = api.GetRecvReadyCount(device.hnd, &ready_words); + if ((ready_err == X502_ERR_OK) && (ready_words != 0U)) { + recv_request_words = std::min(ready_words, cfg.recv_block_words); + recv_timeout_ms = 0; + } + + const int32_t recvd = api.Recv(device.hnd, raw.data(), recv_request_words, recv_timeout_ms); + if (recvd < 0) { + fail("X502_Recv failed: " + x502_error(api, recvd)); + } + + const TickMs now = tick_count_ms(); + if (recvd == 0) { + if ((now - last_stream_activity) >= cfg.clock_wait_ms) { + fail("Timeout waiting for external clock on DI_SYN1. " + "The stream starts immediately, so this usually means there is no valid clock on DI_SYN1."); + } + if ((now - last_lchm_complete) >= cfg.lchm_wait_ms) { + fail_waiting_for_lchm(now); + } + continue; + } + + last_stream_activity = now; + total_raw_words += static_cast(recvd); + + uint32_t din_count = static_cast(din_buffer.size()); + expect_ok(api, + api.ProcessData(device.hnd, + raw.data(), + static_cast(recvd), + 0U, + nullptr, + nullptr, + din_buffer.data(), + &din_count), + "Process DIN data"); + + if (din_count != 0U) { + total_din_words += din_count; + last_din_activity = now; + } + + for (uint32_t i = 0; (i < din_count) && (stats.count < cfg.windows); ++i) { + const uint32_t din_value = din_buffer[i]; + const bool gate = (din_value & kE502DiSyn2Mask) != 0U; + const bool di2_high = (din_value & kE502Digital2Mask) != 0U; + + if (!gate_initialized) { + gate_initialized = true; + last_gate = gate; + saw_low_before_capture = !gate; + last_gate_edge = now; + } else if (!saw_low_before_capture && !gate) { + saw_low_before_capture = true; + } + + if (gate_initialized && (gate != last_gate)) { + ++total_gate_edges; + last_gate_edge = now; + } + + if (!in_lchm && saw_low_before_capture && gate_initialized && !last_gate && gate) { + in_lchm = true; + current.clear(); + } + + if (in_lchm && !gate) { + finalize_lchm(); + in_lchm = false; + } + + if (in_lchm && gate) { + current.add(di2_high); + } + + last_gate = gate; + } + + if ((stats.count < cfg.windows) && ((now - last_lchm_complete) >= cfg.lchm_wait_ms)) { + fail_waiting_for_lchm(now); + } + } + + if (console_stop_requested() && in_lchm) { + finalize_lchm(); + } + + expect_ok(api, api.StreamsStop(device.hnd), "Stop streams"); + device.streams_started = false; + + std::cout << "Raw words read: " << total_raw_words + << ", DIN samples: " << total_din_words + << ", DI_SYN2 edges: " << total_gate_edges << "\n"; + print_summary(stats); + + 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; + } +} diff --git a/main.cpp b/main.cpp index d93f0fc..1d357d1 100644 --- a/main.cpp +++ b/main.cpp @@ -1008,6 +1008,7 @@ void expect_ok(const Api& api, int32_t err, const std::string& what) { constexpr uint32_t kE502DiSyn2Mask = (static_cast(1U) << 13U) | (static_cast(1U) << 17U); constexpr uint32_t kE502Digital1Mask = (static_cast(1U) << 0U); +constexpr uint32_t kE502Digital2Mask = (static_cast(1U) << 1U); constexpr uint32_t kStreamInputAdcFlag = 0x80000000U; constexpr uint32_t kStreamInputCalibratedAdcFlag = 0x40000000U; @@ -1497,6 +1498,9 @@ int run(const Config& cfg) { std::size_t csv_global_frame_index = 0; std::size_t packet_avg_steps = 0; std::size_t fast_packet_frames = 0; + uint64_t packet_clock_count = 0; + uint64_t packet_di2_high_clocks = 0; + uint64_t packet_di2_low_clocks = 0; bool capture_started = false; bool stop_loop_requested = false; @@ -1634,6 +1638,9 @@ int run(const Config& cfg) { } packet_avg_steps = 0; fast_packet_frames = 0; + packet_clock_count = 0; + packet_di2_high_clocks = 0; + packet_di2_low_clocks = 0; if (!fast_tty_avg_stream_mode) { current_packet.reset(target_frames, cfg.channel_count); } @@ -1645,6 +1652,14 @@ int run(const Config& cfg) { }; auto finish_packet = [&](PacketCloseReason reason) { + if (packet_clock_count != (packet_di2_high_clocks + packet_di2_low_clocks)) { + std::ostringstream message; + message << "DI2 clock split invariant failed: clocks=" << packet_clock_count + << ", high=" << packet_di2_high_clocks + << ", low=" << packet_di2_low_clocks; + fail(message.str()); + } + if (tty_di1_group_average) { append_tty_group_step(); tty_group_state.clear_step(); @@ -1662,6 +1677,9 @@ int run(const Config& cfg) { << ": frames/ch=" << frames << ", duration_ms=" << packet_duration_ms << ", close_reason=" << packet_close_reason_to_string(reason) + << ", clocks=" << packet_clock_count + << ", di2_high_clocks=" << packet_di2_high_clocks + << ", di2_low_clocks=" << packet_di2_low_clocks << ", avg_steps=" << packet_avg_steps << "\n"; } else { @@ -1694,7 +1712,10 @@ int run(const Config& cfg) { << "Packet " << packet.packet_index << ": frames/ch=" << frames << ", duration_ms=" << packet_duration_ms - << ", close_reason=" << packet_close_reason_to_string(reason); + << ", close_reason=" << packet_close_reason_to_string(reason) + << ", clocks=" << packet_clock_count + << ", di2_high_clocks=" << packet_di2_high_clocks + << ", di2_low_clocks=" << packet_di2_low_clocks; if (cfg.di1_mode == Di1Mode::ZeroOnChange) { std::cout << ", zeroed_on_DI1_change=" << zeroed_fraction << "% (" << current_packet.zeroed_samples << "/" << current_packet.stored_samples << ")"; @@ -1752,6 +1773,9 @@ int run(const Config& cfg) { packet_active = false; packet_avg_steps = 0; fast_packet_frames = 0; + packet_clock_count = 0; + packet_di2_high_clocks = 0; + packet_di2_low_clocks = 0; if (!fast_tty_avg_stream_mode) { current_packet.reset(target_frames, cfg.channel_count); } @@ -2009,6 +2033,13 @@ int run(const Config& cfg) { continue; } + if ((din_value & kE502Digital2Mask) != 0U) { + ++packet_di2_high_clocks; + } else { + ++packet_di2_low_clocks; + } + ++packet_clock_count; + if (tty_di1_group_average && di1_changed) { append_tty_group_step(); tty_group_state.clear_step(); diff --git a/tty_protocol_writer.cpp b/tty_protocol_writer.cpp index 5c8f074..f7e5c43 100644 --- a/tty_protocol_writer.cpp +++ b/tty_protocol_writer.cpp @@ -265,7 +265,7 @@ void TtyProtocolWriter::emit_packet_start(uint16_t marker) { } void TtyProtocolWriter::emit_step(uint16_t index, int16_t ch1_avg, int16_t ch2_avg) { - enqueue_frame( (cfg.profile == CaptureProfile::Amplitude) ? 0x001AU : 0x000AU, + enqueue_frame(0x000A, index, static_cast(ch1_avg), static_cast(ch2_avg));