From 76e59f2d1b2ca965187f229bb17abf8a3798579d Mon Sep 17 00:00:00 2001 From: awe Date: Thu, 28 May 2026 16:06:45 +0300 Subject: [PATCH] new di8do8 switch --- main.cpp | 315 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 288 insertions(+), 27 deletions(-) diff --git a/main.cpp b/main.cpp index 2f2a86d..9156538 100644 --- a/main.cpp +++ b/main.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -118,6 +119,9 @@ struct Config { bool do1_raw_tty_marked = false; bool do1_pair_subtract_avg = false; std::optional noise_avg_steps; + bool do8_freq_ref = false; + uint32_t do8_cycle_period_ticks = 10; + std::optional do8_amplitude_threshold; }; [[noreturn]] void fail(const std::string& message) { @@ -476,6 +480,7 @@ void print_help(const char* exe_name) { << " [duration_ms:100] [packet_limit:0] [csv:capture.csv] [svg:capture.svg]\n" << " [live_html:live_plot.html] [live_json:live_plot.json] [tty:/tmp/ttyADC_data] [di1_group_avg]\n" << " [do1_toggle_per_frame] [do1_noise_subtract] [do1_raw_tty_marked] [do1_pair_subtract_avg] [noise_avg_steps:N]\n" + << " [do8_freq_ref] [do8_cycle_period:10] [do8_threshold:X]\n" << " [recv_block:32768] [stats_period_ms:1000] [live_update_period_ms:1000] [svg_history_packets:50] [start_wait_ms:10000]\n" << " [buffer_words:8388608] [step_words:32768]\n" << " [pullup_syn1] [pullup_syn2] [pulldown_conv_in] [pulldown_start_in]\n" @@ -526,6 +531,11 @@ void print_help(const char* exe_name) { << " average within each constant DI1 run, and emit legacy 4-word tty frames (word0=0x000A)\n" << " noise_avg_steps:N -> kept for compatibility in do1_noise_subtract mode (current baseline is previous HIGH step)\n" << " (still required when do1_noise_subtract is enabled)\n" + << " do8_freq_ref -> enable DO8/DI8 frequency reference overlay (DO8 and DI8 must be physically looped back);\n" + << " DO8 toggles HIGH for 2 ticks within each do8_cycle_period cycle;\n" + << " DI8=HIGH samples are accumulated separately and emitted as 0x00A8 frames\n" + << " do8_cycle_period:10 -> DO8 cycle period in ADC ticks (default 10, minimum 4)\n" + << " do8_threshold:X -> only emit DI8 reference frame when channel amplitude exceeds X (raw ADC code units)\n" << " tty fast stream-only modes -> di1_group_avg, do1_noise_subtract, do1_raw_tty_marked, do1_pair_subtract_avg;\n" << " skip CSV/SVG/live outputs\n" << " If sample_clock_hz is omitted together with clock:internal, the maximum ADC speed is used\n" @@ -692,6 +702,18 @@ Config parse_args(int argc, char** argv) { cfg.noise_avg_steps = parse_u32(arg.substr(16), "noise_avg_steps"); continue; } + if (arg == "do8_freq_ref") { + cfg.do8_freq_ref = true; + continue; + } + if (starts_with(arg, "do8_cycle_period:")) { + cfg.do8_cycle_period_ticks = parse_u32(arg.substr(17), "do8_cycle_period"); + continue; + } + if (starts_with(arg, "do8_threshold:")) { + cfg.do8_amplitude_threshold = parse_double(arg.substr(14), "do8_threshold"); + continue; + } if (starts_with(arg, "sample_clock_hz:")) { const std::string value = trim_copy(arg.substr(16)); cfg.sample_clock_specified = true; @@ -894,6 +916,25 @@ Config parse_args(int argc, char** argv) { fail("do1_pair_subtract_avg requires phase configuration: mode:diff channels:2 ch1:2 ch2:3"); } } + if (cfg.do8_freq_ref) { + if (!cfg.tty_path.has_value()) { + fail("do8_freq_ref requires tty:"); + } + if (!cfg.do1_toggle_per_frame) { + fail("do8_freq_ref requires do1_toggle_per_frame (cyclic output must be active)"); + } + if (cfg.do8_cycle_period_ticks < 4U) { + fail("do8_cycle_period must be >= 4 (at least 2 LOW ticks + 2 HIGH ticks)"); + } + if (cfg.do8_amplitude_threshold.has_value() && *cfg.do8_amplitude_threshold < 0.0) { + fail("do8_threshold must be >= 0"); + } + if (std::gcd(4U, cfg.do8_cycle_period_ticks) > 2U) { + std::cerr << "Warning: gcd(DO1 period=4" + << ", DO8 period=" << cfg.do8_cycle_period_ticks + << ") > 2; DI8=HIGH may systematically align with the same DO1 phase\n"; + } + } 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"); } @@ -1154,8 +1195,12 @@ constexpr uint32_t kE502Digital1Mask = (static_cast(1U) << 0U); constexpr uint32_t kE502Digital2Mask = (static_cast(1U) << 1U); constexpr uint32_t kE502Do1Mask = (static_cast(1U) << 0U); constexpr uint32_t kE502Do2Mask = (static_cast(1U) << 1U); +constexpr uint32_t kE502Digital8Mask = (static_cast(1U) << 7U); +constexpr uint32_t kE502Do8Mask = (static_cast(1U) << 7U); constexpr uint32_t kDo1TogglePeriodTicks = 2U; constexpr uint32_t kDo1CyclePatternWords = kDo1TogglePeriodTicks * 2U; +constexpr uint32_t kDo8HighTicks = 2U; +constexpr uint16_t kTtyMarkerDi8High = 0x00A8U; constexpr uint32_t kStreamInputAdcFlag = 0x80000000U; constexpr uint32_t kStreamInputCalibratedAdcFlag = 0x40000000U; @@ -1466,6 +1511,8 @@ struct Do1PairSubtractAverageState { double pending_ch1_raw = 0.0; bool pending_ch1_di1_high = false; bool pending_ch1_di2_high = false; + bool pending_ch1_di8_settled_high = false; + bool pending_ch1_di8_in_guard = false; uint16_t next_index = 1; @@ -1482,6 +1529,8 @@ struct Do1PairSubtractAverageState { pending_ch1_raw = 0.0; pending_ch1_di1_high = false; pending_ch1_di2_high = false; + pending_ch1_di8_settled_high = false; + pending_ch1_di8_in_guard = false; } void reset_packet() { @@ -1503,11 +1552,14 @@ struct Do1PairSubtractAverageState { run_initialized = false; } - void add_pending_ch1(double raw_code, bool di1_high, bool di2_high) { + void add_pending_ch1(double raw_code, bool di1_high, bool di2_high, + bool di8_settled_high = false, bool di8_in_guard = false) { pending_ch1_valid = true; pending_ch1_raw = raw_code; pending_ch1_di1_high = di1_high; pending_ch1_di2_high = di2_high; + pending_ch1_di8_settled_high = di8_settled_high; + pending_ch1_di8_in_guard = di8_in_guard; } void add_pair_sample(bool di2_high, double ch1_value, double ch2_value) { @@ -1547,6 +1599,85 @@ struct Do1PairSubtractAverageState { } }; +struct Di8FreqRefState { + std::array high_sum {}; + std::array high_count {}; + + static constexpr uint32_t kGuardSamples = 2U; + bool di8_prev_level = false; + bool di8_initialized = false; + uint32_t samples_since_transition = 0; + bool in_guard = false; + + uint16_t last_standard_index = 0; + bool last_standard_valid = false; + + void clear_step() { + high_sum = {}; + high_count = {}; + } + + void reset_packet() { + clear_step(); + di8_prev_level = false; + di8_initialized = false; + samples_since_transition = 0; + in_guard = false; + last_standard_index = 0; + last_standard_valid = false; + } + + // Returns true if the sample is in the settled DI8=HIGH region. + // Returns false for guard samples and DI8=LOW. + // Sets in_guard for samples within the guard window after any transition. + bool update_di8(bool di8_level) { + if (!di8_initialized) { + di8_prev_level = di8_level; + di8_initialized = true; + in_guard = true; + samples_since_transition = 0; + return false; + } + + if (di8_level != di8_prev_level) { + di8_prev_level = di8_level; + samples_since_transition = 0; + in_guard = true; + return false; + } + + if (in_guard) { + ++samples_since_transition; + if (samples_since_transition >= kGuardSamples) { + in_guard = false; + } else { + return false; + } + } + + return di8_level; + } + + void add_sample(uint32_t lch, double raw_code) { + if (lch >= high_sum.size()) { + return; + } + high_sum[lch] += raw_code; + ++high_count[lch]; + } + + bool has_complete_step(uint32_t channel_count) const { + return (high_count[0] != 0U) && ((channel_count <= 1U) || (high_count[1] != 0U)); + } + + double step_average(uint32_t lch) const { + if ((lch >= high_sum.size()) || (high_count[lch] == 0U)) { + return 0.0; + } + return high_sum[lch] / static_cast(high_count[lch]); + } +}; + int16_t pack_raw_code_to_int16(double avg_raw_code) { const double scaled = avg_raw_code * 32767.0 / static_cast(X502_ADC_SCALE_CODE_MAX); @@ -1821,6 +1952,15 @@ int run(const Config& cfg) { << " DO2 state: forced HIGH\n" << " DOUT rate: " << (cfg.do1_toggle_per_frame ? std::to_string(actual_dout_freq_hz) + " Hz" : std::string("disabled")) << "\n" + << " DO8 freq ref: " + << (cfg.do8_freq_ref + ? ("enabled (period=" + std::to_string(cfg.do8_cycle_period_ticks) + + " ticks, high=" + std::to_string(kDo8HighTicks) + " ticks" + + (cfg.do8_amplitude_threshold.has_value() + ? ", threshold=" + std::to_string(*cfg.do8_amplitude_threshold) : "") + + ")") + : std::string("disabled")) + << "\n" << " ADC range: +/-" << range_to_volts(cfg.range) << " V\n" << " max frames per packet per channel: " << target_frames << "\n"; @@ -1921,15 +2061,37 @@ int run(const Config& cfg) { std::cerr << "Warning: Ctrl+C handler could not be installed; continuous mode may stop abruptly.\n"; } - std::array do1_cycle_pattern {}; - std::array do1_cycle_encoded {}; + const uint32_t cycle_pattern_len = cfg.do8_freq_ref + ? static_cast(std::lcm(static_cast(kDo1CyclePatternWords), + cfg.do8_cycle_period_ticks)) + : kDo1CyclePatternWords; + std::vector do1_cycle_pattern(cycle_pattern_len, 0U); + std::vector do1_cycle_encoded(cycle_pattern_len, 0U); if (cfg.do1_toggle_per_frame) { if ((api.OutCycleLoadStart == nullptr) || (api.OutCycleSetup == nullptr) || (api.OutCycleStop == nullptr)) { fail("do1_toggle_per_frame requires X502_OutCycle* API support (cyclic output mode)"); } for (std::size_t i = 0; i < do1_cycle_pattern.size(); ++i) { - do1_cycle_pattern[i] = ((i < kDo1TogglePeriodTicks) ? 0U : kE502Do1Mask) | kE502Do2Mask; + uint32_t word = kE502Do2Mask; + + const uint32_t do1_phase = static_cast(i) % kDo1CyclePatternWords; + if (do1_phase >= kDo1TogglePeriodTicks) { + word |= kE502Do1Mask; + } + + if (cfg.do8_freq_ref) { + const uint32_t do8_phase = static_cast(i) % cfg.do8_cycle_period_ticks; + if (do8_phase >= (cfg.do8_cycle_period_ticks - kDo8HighTicks)) { + word |= kE502Do8Mask; + } + } + + do1_cycle_pattern[i] = word; + } + + if (cycle_pattern_len > 262144U) { + fail("Combined DO1+DO8 cyclic pattern too large: " + std::to_string(cycle_pattern_len)); } expect_ok(api, @@ -1980,6 +2142,7 @@ int run(const Config& cfg) { Do1RawTaggedState tty_do1_raw_state; Do1NoiseSubtractState tty_do1_noise_state; Do1PairSubtractAverageState tty_do1_pair_state; + Di8FreqRefState tty_di8_state; std::vector tty_frame_words; tty_frame_words.reserve(static_cast(read_capacity_words) * 2U + 16U); if (tty_do1_noise_subtract) { @@ -2104,12 +2267,17 @@ int run(const Config& cfg) { (cfg.channel_count <= 1U) ? 0.0 : (tty_group_state.sum_ch2 / static_cast(tty_group_state.count_ch2)); + const uint16_t step_idx = static_cast(tty_group_state.next_index); append_tty_frame( (cfg.profile == CaptureProfile::Amplitude) ? 0x001AU : 0x000AU, - static_cast(tty_group_state.next_index), + step_idx, static_cast(pack_raw_code_to_int16(ch1_avg)), static_cast(pack_raw_code_to_int16(ch2_avg))); ++tty_group_state.next_index; ++packet_avg_steps; + if (cfg.do8_freq_ref) { + tty_di8_state.last_standard_index = step_idx; + tty_di8_state.last_standard_valid = true; + } }; auto append_tty_do1_raw_marked_step = [&]() { @@ -2130,13 +2298,18 @@ int run(const Config& cfg) { const double ch1_avg = tty_do1_raw_state.step_average(0U); const double ch2_avg = (cfg.channel_count <= 1U) ? 0.0 : tty_do1_raw_state.step_average(1U); const uint16_t marker = tty_do1_raw_state.step_di1_high ? 0x00A4U : 0x00A3U; + const uint16_t step_idx = tty_do1_raw_state.next_index; append_tty_frame(marker, - tty_do1_raw_state.next_index, + step_idx, static_cast(pack_raw_code_to_int16(ch1_avg)), static_cast(pack_raw_code_to_int16(ch2_avg))); ++tty_do1_raw_state.next_index; ++packet_avg_steps; tty_do1_raw_state.clear_step(); + if (cfg.do8_freq_ref) { + tty_di8_state.last_standard_index = step_idx; + tty_di8_state.last_standard_valid = true; + } }; auto append_tty_do1_subtracted_step = [&]() { @@ -2185,14 +2358,19 @@ int run(const Config& cfg) { const double ch2_corrected = (cfg.channel_count <= 1U) ? 0.0 : (ch2_avg - tty_do1_noise_state.last_high(1U)); + const uint16_t step_idx = tty_do1_noise_state.next_index; append_tty_frame((cfg.profile == CaptureProfile::Amplitude) ? 0x001AU : 0x000AU, - tty_do1_noise_state.next_index, + step_idx, static_cast(pack_raw_code_to_int16(ch1_corrected)), static_cast(pack_raw_code_to_int16(ch2_corrected))); ++tty_do1_noise_state.next_index; ++packet_avg_steps; tty_do1_noise_state.consume_pending_high(); tty_do1_noise_state.finish_step(); + if (cfg.do8_freq_ref) { + tty_di8_state.last_standard_index = step_idx; + tty_di8_state.last_standard_valid = true; + } }; auto append_tty_do1_pair_subtracted_avg_run = [&]() { @@ -2212,13 +2390,54 @@ int run(const Config& cfg) { fail("TTY protocol step index overflow within packet"); } + const uint16_t step_idx = tty_do1_pair_state.next_index; append_tty_frame(0x000AU, - tty_do1_pair_state.next_index, + step_idx, static_cast(pack_raw_code_to_int16(tty_do1_pair_state.avg_diff_ch1())), static_cast(pack_raw_code_to_int16(tty_do1_pair_state.avg_diff_ch2()))); ++tty_do1_pair_state.next_index; ++packet_avg_steps; tty_do1_pair_state.clear_run(); + if (cfg.do8_freq_ref) { + tty_di8_state.last_standard_index = step_idx; + tty_di8_state.last_standard_valid = true; + } + }; + + auto append_tty_di8_ref_step = [&]() { + if (!cfg.do8_freq_ref) { + return; + } + if (!tty_di8_state.has_complete_step(cfg.channel_count)) { + tty_di8_state.clear_step(); + return; + } + + const double ch1_avg = tty_di8_state.step_average(0U); + const double ch2_avg = (cfg.channel_count <= 1U) ? 0.0 : tty_di8_state.step_average(1U); + + bool exceeds = true; + if (cfg.do8_amplitude_threshold.has_value()) { + const double t = *cfg.do8_amplitude_threshold; + exceeds = (std::abs(ch1_avg) > t) || + ((cfg.channel_count > 1U) && (std::abs(ch2_avg) > t)); + } + + if (!exceeds) { + tty_di8_state.clear_step(); + return; + } + + if (!tty_di8_state.last_standard_valid) { + tty_di8_state.clear_step(); + return; + } + + append_tty_frame(kTtyMarkerDi8High, + tty_di8_state.last_standard_index, + static_cast(pack_raw_code_to_int16(ch1_avg)), + static_cast(pack_raw_code_to_int16(ch2_avg))); + tty_di8_state.clear_step(); }; auto flush_tty_frames = [&]() { @@ -2261,6 +2480,9 @@ int run(const Config& cfg) { tty_do1_pair_state.reset_packet(); append_tty_packet_start(); } + if (cfg.do8_freq_ref) { + tty_di8_state.reset_packet(); + } packet_active = true; }; @@ -2275,18 +2497,25 @@ int run(const Config& cfg) { if (tty_di1_group_average) { append_tty_group_step(); + if (cfg.do8_freq_ref) { append_tty_di8_ref_step(); } tty_group_state.clear_step(); } else if (tty_do1_raw_tty_marked) { append_tty_do1_raw_marked_step(); + if (cfg.do8_freq_ref) { append_tty_di8_ref_step(); } tty_do1_raw_state.clear_step(); } else if (tty_do1_noise_subtract) { // Finalize the last DI1 run in packet, drop only incomplete channel tuples. append_tty_do1_subtracted_step(); + if (cfg.do8_freq_ref) { append_tty_di8_ref_step(); } tty_do1_noise_state.clear_step(); } else if (tty_do1_pair_subtract_avg) { append_tty_do1_pair_subtracted_avg_run(); + if (cfg.do8_freq_ref) { append_tty_di8_ref_step(); } tty_do1_pair_state.reset_packet(); } + if (cfg.do8_freq_ref) { + tty_di8_state.clear_step(); + } const std::size_t frames = fast_tty_avg_stream_mode ? fast_packet_frames @@ -2669,14 +2898,24 @@ int run(const Config& cfg) { } ++packet_clock_count; + const bool di8_level = cfg.do8_freq_ref + ? ((din_value & kE502Digital8Mask) != 0U) + : false; + const bool di8_settled_high = cfg.do8_freq_ref + ? tty_di8_state.update_di8(di8_level) + : false; + const bool di8_in_guard = cfg.do8_freq_ref && tty_di8_state.in_guard; + if (tty_di1_group_average && di1_changed) { append_tty_group_step(); + if (cfg.do8_freq_ref) { append_tty_di8_ref_step(); tty_di8_state.clear_step(); } tty_group_state.clear_step(); } else if (tty_do1_raw_tty_marked) { if (!tty_do1_raw_state.step_level_initialized) { tty_do1_raw_state.start_new_step(di1_level); } else if (di1_changed) { append_tty_do1_raw_marked_step(); + if (cfg.do8_freq_ref) { append_tty_di8_ref_step(); tty_di8_state.clear_step(); } tty_do1_raw_state.start_new_step(di1_level); } } else if (tty_do1_noise_subtract) { @@ -2684,6 +2923,7 @@ int run(const Config& cfg) { tty_do1_noise_state.start_new_step(di1_level); } else if (di1_changed) { append_tty_do1_subtracted_step(); + if (cfg.do8_freq_ref) { append_tty_di8_ref_step(); tty_di8_state.clear_step(); } tty_do1_noise_state.start_new_step(di1_level); } } @@ -2699,37 +2939,57 @@ int run(const Config& cfg) { if (fast_packet_frames < target_frames) { if (tty_do1_pair_subtract_avg) { if (lch == 0U) { - tty_do1_pair_state.add_pending_ch1(adc_raw_value, di1_level, di2_level); + tty_do1_pair_state.add_pending_ch1(adc_raw_value, di1_level, di2_level, + di8_settled_high, di8_in_guard); } else if (lch == 1U) { if (tty_do1_pair_state.pending_ch1_valid) { const bool pair_di1_matches = (tty_do1_pair_state.pending_ch1_di1_high == di1_level); const bool pair_di2_matches = (tty_do1_pair_state.pending_ch1_di2_high == di2_level); + const bool either_di8_guard = + tty_do1_pair_state.pending_ch1_di8_in_guard || di8_in_guard; + const bool both_di8_high = + tty_do1_pair_state.pending_ch1_di8_settled_high && di8_settled_high; + const bool both_di8_normal = + !tty_do1_pair_state.pending_ch1_di8_settled_high && + !tty_do1_pair_state.pending_ch1_di8_in_guard && + !di8_settled_high && !di8_in_guard; - if (pair_di1_matches && pair_di2_matches) { - if (!tty_do1_pair_state.run_initialized) { - tty_do1_pair_state.start_run(di1_level); - } else if (tty_do1_pair_state.run_di1_high != di1_level) { - append_tty_do1_pair_subtracted_avg_run(); - tty_do1_pair_state.start_run(di1_level); + if (pair_di1_matches && pair_di2_matches && !either_di8_guard) { + if (both_di8_high) { + tty_di8_state.add_sample(0U, tty_do1_pair_state.pending_ch1_raw); + tty_di8_state.add_sample(1U, adc_raw_value); + } else if (both_di8_normal) { + if (!tty_do1_pair_state.run_initialized) { + tty_do1_pair_state.start_run(di1_level); + } else if (tty_do1_pair_state.run_di1_high != di1_level) { + append_tty_do1_pair_subtracted_avg_run(); + if (cfg.do8_freq_ref) { append_tty_di8_ref_step(); tty_di8_state.clear_step(); } + tty_do1_pair_state.start_run(di1_level); + } + + tty_do1_pair_state.add_pair_sample(di2_level, + tty_do1_pair_state.pending_ch1_raw, + adc_raw_value); } - - tty_do1_pair_state.add_pair_sample(di2_level, - tty_do1_pair_state.pending_ch1_raw, - adc_raw_value); + // else: mixed DI8 state pair -- discard } } tty_do1_pair_state.clear_pending_ch1(); } else { tty_do1_pair_state.clear_pending_ch1(); } - } else if (tty_do1_noise_subtract) { - tty_do1_noise_state.add_sample(lch, adc_raw_value); - } else if (tty_do1_raw_tty_marked) { - tty_do1_raw_state.add_sample(lch, adc_raw_value); - } else { - tty_group_state.add_sample(lch, adc_raw_value); + } else if (di8_settled_high) { + tty_di8_state.add_sample(lch, adc_raw_value); + } else if (!di8_in_guard) { + if (tty_do1_noise_subtract) { + tty_do1_noise_state.add_sample(lch, adc_raw_value); + } else if (tty_do1_raw_tty_marked) { + tty_do1_raw_state.add_sample(lch, adc_raw_value); + } else { + tty_group_state.add_sample(lch, adc_raw_value); + } } if (lch == (cfg.channel_count - 1U)) { ++fast_packet_frames; @@ -2797,9 +3057,10 @@ int run(const Config& cfg) { expect_ok(api, api.StreamsStop(device.hnd), "Stop streams"); device.streams_started = false; if (cfg.do1_toggle_per_frame) { + const uint32_t clear_mask = kE502Do1Mask | kE502Do2Mask | (cfg.do8_freq_ref ? kE502Do8Mask : 0U); expect_ok(api, - api.AsyncOutDig(device.hnd, kE502Do2Mask, ~(kE502Do1Mask | kE502Do2Mask)), - "Force DO1 LOW and keep DO2 HIGH after cyclic DOUT stop"); + api.AsyncOutDig(device.hnd, kE502Do2Mask, ~clear_mask), + "Force DO1/DO8 LOW and keep DO2 HIGH after cyclic DOUT stop"); } else { expect_ok(api, api.AsyncOutDig(device.hnd, kE502Do2Mask, ~kE502Do2Mask), "Keep DO2 HIGH after streams stop"); }