second in

This commit is contained in:
awe
2026-04-20 13:26:09 +03:00
parent 3817f21473
commit 60a678168f
4 changed files with 944 additions and 2 deletions

28
build_lchm_clock_counter.sh Executable file
View File

@ -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"

883
lchm_clock_counter.cpp Normal file
View File

@ -0,0 +1,883 @@
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <csignal>
#include <dlfcn.h>
#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 <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <limits>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
constexpr uint32_t kE502DiSyn2Mask =
(static_cast<uint32_t>(1U) << 13U) | (static_cast<uint32_t>(1U) << 17U);
constexpr uint32_t kE502Digital2Mask = (static_cast<uint32_t>(1U) << 1U);
struct Config {
std::string serial;
std::optional<uint32_t> 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<uint64_t>::max();
uint64_t clocks_max = 0;
uint64_t clocks_sum = 0;
uint64_t high_min = std::numeric_limits<uint64_t>::max();
uint64_t high_max = 0;
uint64_t high_sum = 0;
uint64_t low_min = std::numeric_limits<uint64_t>::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<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] > 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<std::string>& 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 <typename Fn>
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<Fn>(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<Fn>(addr);
#endif
}
template <typename Fn>
Fn try_load_symbol(ModuleHandle module, const char* name) {
#ifdef _WIN32
const auto addr = GetProcAddress(module, name);
return reinterpret_cast<Fn>(addr);
#else
dlerror();
void* addr = dlsym(module, name);
(void) dlerror();
return reinterpret_cast<Fn>(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<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");
GetDevInfo = try_load_symbol<decltype(GetDevInfo)>(x502_module, "X502_GetDevInfo");
GetDevInfo2 = try_load_symbol<decltype(GetDevInfo2)>(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<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");
SetDinFreqDivider = load_symbol<decltype(SetDinFreqDivider)>(x502_module, "X502_SetDinFreqDivider");
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");
GetRecvReadyCount = load_symbol<decltype(GetRecvReadyCount)>(x502_module, "X502_GetRecvReadyCount");
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() {
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<TickMs>(GetTickCount64());
#else
using namespace std::chrono;
return static_cast<TickMs>(
duration_cast<milliseconds>(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<unsigned>(info.fpga_ver >> 8U) << "."
<< static_cast<unsigned>(info.fpga_ver & 0xFFU) << "\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";
}
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<double>(sum) / static_cast<double>(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<uint32_t> raw(cfg.recv_block_words);
std::vector<uint32_t> 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<uint32_t>(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<uint64_t>(recvd);
uint32_t din_count = static_cast<uint32_t>(din_buffer.size());
expect_ok(api,
api.ProcessData(device.hnd,
raw.data(),
static_cast<uint32_t>(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;
}
}

View File

@ -1008,6 +1008,7 @@ void expect_ok(const Api& api, int32_t err, const std::string& what) {
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);
constexpr uint32_t kE502Digital2Mask = (static_cast<uint32_t>(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();

View File

@ -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<uint16_t>(ch1_avg),
static_cast<uint16_t>(ch2_avg));