diff --git a/main.cpp b/main.cpp index 77409b8..4685665 100644 --- a/main.cpp +++ b/main.cpp @@ -114,6 +114,8 @@ struct Config { std::optional tty_path; bool di1_group_average = false; bool do1_toggle_per_frame = false; + bool do1_noise_subtract = false; + std::optional noise_avg_steps; }; [[noreturn]] void fail(const std::string& message) { @@ -471,7 +473,7 @@ void print_help(const char* exe_name) { << " [di1:zero|trace|ignore]\n" << " [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]\n" + << " [do1_toggle_per_frame] [do1_noise_subtract] [noise_avg_steps:N]\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" @@ -496,20 +498,25 @@ void print_help(const char* exe_name) { << " stats_period_ms:1000 -> print online transfer/capture statistics every 1000 ms (0 disables)\n" << " recv_block:32768 -> request up to 32768 raw 32-bit words per X502_Recv() call\n" << " live_update_period_ms:1000 -> refresh live HTML/JSON no more than once per second" - << " (ignored in tty+di1_group_avg fast mode)\n" + << " (ignored in tty fast stream-only modes)\n" << " svg_history_packets:50 -> keep last 50 packets in RAM for final SVG (0 keeps all, risky)" - << " (ignored in tty+di1_group_avg fast mode)\n" + << " (ignored in tty fast stream-only modes)\n" << " duration_ms:100 -> max packet length if stop edge does not arrive\n" << " packet_limit:0 -> 0 means continuous until Ctrl+C, N means stop after N packets\n" << " buffer_words:8388608 -> input stream buffer size in 32-bit words\n" << " step_words:32768 -> input stream transfer step in 32-bit words\n" - << " live_html/live_json -> live graph files updated as packets arrive outside tty+di1_group_avg fast mode\n" + << " live_html/live_json -> live graph files updated as packets arrive outside tty fast stream-only modes\n" << " tty:/tmp/ttyADC_data -> write a continuous legacy 4-word CH1/CH2 stream; with channels:1, CH2 is 0\n" << " di1_group_avg -> with tty + di1:trace, emit one averaged 4-word step per constant DI1 run\n" << " do1_toggle_per_frame -> hardware cyclic DO1 pattern in module memory:\n" << " DO1 outputs 00110011... continuously (toggle every 2 ADC ticks)\n" << " without per-tick DOUT updates from PC\n" - << " tty+di1_group_avg -> fast stream-only mode: skip CSV/SVG/live outputs and send only averaged tty data\n" + << " do1_noise_subtract -> with tty + do1_toggle_per_frame, use programmed DO1 phase (0011...)\n" + << " to average recent noise steps and subtract it from useful steps before tty output\n" + << " (only corrected DO1=LOW steps are sent to tty)\n" + << " noise_avg_steps:N -> number of recent DO1=HIGH noise steps per channel used as subtraction baseline\n" + << " (required when do1_noise_subtract is enabled)\n" + << " tty fast stream-only modes -> di1_group_avg or do1_noise_subtract; skip CSV/SVG/live outputs\n" << " If sample_clock_hz is omitted together with clock:internal, the maximum ADC speed is used\n" << "\n" << "Profiles:\n" @@ -532,10 +539,12 @@ void print_help(const char* exe_name) { << "This build enables synchronous DIN together with ADC. DI_SYN2 stop edges are detected\n" << "inside the same input stream, packets are split continuously by DI_SYN2 edges, and DI1\n" << "can either zero ADC samples on change, be exported as a separate synchronous trace, or be ignored.\n" - << "Outside tty+di1_group_avg fast mode, open live_plot.html in a browser to see the live graph update over time.\n" + << "Outside tty fast stream-only modes, open live_plot.html in a browser to see the live graph update over time.\n" << "The live HTML supports X/Y zoom buttons, mouse-wheel zoom, and reset.\n" << "For tty output, use di1_group_avg together with di1:trace to emit one averaged 4-word step\n" << "per constant DI1 run while keeping the current ring-buffered binary frame format.\n" + << "For DO1-phase subtraction, use do1_toggle_per_frame + do1_noise_subtract + noise_avg_steps:N;\n" + << "the first ADC sample in each packet is treated as DO1 LOW in the 0011... sequence.\n" << "Amplitude mode keeps the raw AD8317 voltage as captured on X1; no inversion is applied.\n" << "\n" << "Phase profile example:\n" @@ -552,7 +561,13 @@ void print_help(const char* exe_name) { << " " << exe_name << " profile:amplitude clock:internal internal_ref_hz:2000000 start:di_syn2_rise stop:di_syn2_fall" << " sample_clock_hz:max range:0.2 di1:trace di1_group_avg duration_ms:100 packet_limit:0" - << " tty:/tmp/ttyADC_data\n"; + << " tty:/tmp/ttyADC_data\n" + << "\n" + << "Amplitude TTY DO1 noise subtract example:\n" + << " " << exe_name + << " profile:amplitude clock:internal internal_ref_hz:2000000 start:di_syn2_rise stop:di_syn2_fall" + << " sample_clock_hz:max range:0.2 do1_toggle_per_frame do1_noise_subtract noise_avg_steps:16" + << " duration_ms:100 packet_limit:0 tty:/tmp/ttyADC_data\n"; } Config parse_args(int argc, char** argv) { @@ -636,10 +651,18 @@ Config parse_args(int argc, char** argv) { cfg.di1_group_average = true; continue; } + if (arg == "do1_noise_subtract") { + cfg.do1_noise_subtract = true; + continue; + } if (arg == "do1_toggle_per_frame") { cfg.do1_toggle_per_frame = true; continue; } + if (starts_with(arg, "noise_avg_steps:")) { + cfg.noise_avg_steps = parse_u32(arg.substr(16), "noise_avg_steps"); + continue; + } if (starts_with(arg, "sample_clock_hz:")) { const std::string value = trim_copy(arg.substr(16)); cfg.sample_clock_specified = true; @@ -751,7 +774,7 @@ Config parse_args(int argc, char** argv) { cfg.live_update_period_ms = std::max(cfg.live_update_period_ms, 1000U); } } - if (cfg.tty_path && cfg.di1_group_average) { + if (cfg.tty_path && (cfg.di1_group_average || cfg.do1_noise_subtract)) { if (!cfg.recv_block_specified) { cfg.recv_block_words = std::max(cfg.recv_block_words, 65536U); } @@ -774,6 +797,25 @@ Config parse_args(int argc, char** argv) { if (cfg.di1_group_average && (cfg.di1_mode != Di1Mode::Trace)) { fail("di1_group_avg requires di1:trace"); } + if (cfg.do1_noise_subtract) { + if (!cfg.tty_path.has_value()) { + fail("do1_noise_subtract requires tty:"); + } + if (!cfg.do1_toggle_per_frame) { + fail("do1_noise_subtract requires do1_toggle_per_frame"); + } + if (cfg.di1_group_average) { + fail("do1_noise_subtract is incompatible with di1_group_avg"); + } + if (!cfg.noise_avg_steps.has_value()) { + fail("do1_noise_subtract requires noise_avg_steps:N"); + } + if (*cfg.noise_avg_steps == 0U) { + fail("noise_avg_steps must be > 0"); + } + } else if (cfg.noise_avg_steps.has_value()) { + fail("noise_avg_steps:N can only be used together with do1_noise_subtract"); + } 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"); } @@ -1188,6 +1230,110 @@ struct TtyGroupAverageState { } }; +struct Do1NoiseSubtractState { + uint32_t noise_avg_steps = 0; + std::array, 2> noise_ring; + std::array noise_ring_head {}; + std::array noise_ring_size {}; + std::array noise_ring_sum {}; + std::array step_sum {}; + std::array step_count {}; + uint32_t ticks_in_step = 0; + bool step_do1_high = false; + uint16_t next_index = 1; + + void configure(uint32_t history_steps) { + noise_avg_steps = history_steps; + for (auto& ring : noise_ring) { + ring.assign(noise_avg_steps, 0.0); + } + reset_packet(); + } + + void clear_step() { + step_sum = {}; + step_count = {}; + ticks_in_step = 0; + } + + void reset_packet() { + clear_step(); + step_do1_high = false; + next_index = 1; + noise_ring_head = {}; + noise_ring_size = {}; + noise_ring_sum = {}; + } + + void add_sample(uint32_t lch, double raw_code) { + if (lch >= step_sum.size()) { + return; + } + step_sum[lch] += raw_code; + ++step_count[lch]; + } + + bool advance_tick() { + ++ticks_in_step; + if (ticks_in_step < kDo1TogglePeriodTicks) { + return false; + } + ticks_in_step = 0; + return true; + } + + bool has_complete_step(uint32_t channel_count) const { + return (step_count[0] != 0U) && ((channel_count <= 1U) || (step_count[1] != 0U)); + } + + double step_average(uint32_t lch) const { + if ((lch >= step_sum.size()) || (step_count[lch] == 0U)) { + return 0.0; + } + return step_sum[lch] / static_cast(step_count[lch]); + } + + void push_noise_average(uint32_t lch, double avg_value) { + if ((noise_avg_steps == 0U) || (lch >= noise_ring.size())) { + return; + } + + auto& ring = noise_ring[lch]; + auto& head = noise_ring_head[lch]; + auto& size = noise_ring_size[lch]; + auto& sum = noise_ring_sum[lch]; + + if (size < noise_avg_steps) { + ring[size] = avg_value; + sum += avg_value; + ++size; + return; + } + + sum -= ring[head]; + ring[head] = avg_value; + sum += avg_value; + head = (head + 1U) % noise_avg_steps; + } + + double noise_baseline(uint32_t lch) const { + if (lch >= noise_ring_size.size()) { + return 0.0; + } + const std::size_t size = noise_ring_size[lch]; + if (size == 0U) { + return 0.0; + } + return noise_ring_sum[lch] / static_cast(size); + } + + void finish_step() { + step_do1_high = !step_do1_high; + step_sum = {}; + step_count = {}; + } +}; + 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); @@ -1467,9 +1613,14 @@ int run(const Config& cfg) { std::size_t tty_ring_capacity_bytes = 0; std::unique_ptr tty_writer; const bool tty_di1_group_average = cfg.tty_path.has_value() && cfg.di1_group_average; - const bool fast_tty_avg_stream_mode = tty_di1_group_average; + const bool tty_do1_noise_subtract = cfg.tty_path.has_value() && cfg.do1_noise_subtract; + const bool fast_tty_avg_stream_mode = tty_di1_group_average || tty_do1_noise_subtract; const uint16_t tty_packet_start_marker = (cfg.profile == CaptureProfile::Amplitude) ? 0x001AU : 0x000AU; + const std::string fast_tty_mode_name = + tty_do1_noise_subtract ? std::string("do1_noise_subtract") + : (tty_di1_group_average ? std::string("di1_group_avg") + : std::string("none")); if (cfg.tty_path) { const uint64_t typical_tty_batch_frames = (static_cast(read_capacity_words) + 1U) / 2U; const uint64_t typical_tty_batch_bytes = typical_tty_batch_frames * sizeof(uint16_t) * 4U; @@ -1480,7 +1631,7 @@ int run(const Config& cfg) { } tty_ring_capacity_bytes = static_cast(tty_ring_bytes_u64); tty_writer = std::make_unique(*cfg.tty_path, tty_ring_capacity_bytes); - if (!tty_di1_group_average) { + if (!tty_di1_group_average && !tty_do1_noise_subtract) { tty_writer->emit_packet_start(tty_packet_start_marker); } } @@ -1491,13 +1642,16 @@ int run(const Config& cfg) { writer->initialize_csv(cfg.channel_count, cfg.di1_mode == Di1Mode::Trace); } if (fast_tty_avg_stream_mode) { - std::cout << " tty avg stream-only mode: enabled\n" + std::cout << " tty fast stream-only mode: enabled (" << fast_tty_mode_name << ")\n" << " tty stream output: " << *cfg.tty_path << "\n" << " tty ring buffer bytes: " << tty_ring_capacity_bytes << "\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" - << " tty di1_group_avg: enabled\n" + << " tty di1_group_avg: " << (tty_di1_group_average ? "enabled" : "disabled") << "\n" + << " tty do1_noise_subtract: " << (tty_do1_noise_subtract ? "enabled" : "disabled") << "\n" + << " noise_avg_steps: " + << (tty_do1_noise_subtract ? std::to_string(*cfg.noise_avg_steps) : std::string("n/a")) << "\n" << " stream-only note: ignoring csv/svg/live_html/live_json/live_update_period_ms/svg_history_packets\n"; } else { std::cout << " live plot html: " << writer->live_html_path() << "\n" @@ -1513,6 +1667,11 @@ int run(const Config& cfg) { : (cfg.di1_group_average ? std::string("requested, tty disabled") : std::string("disabled"))) << "\n" + << " tty do1_noise_subtract: " + << (tty_do1_noise_subtract ? std::string("enabled") + : (cfg.do1_noise_subtract ? std::string("requested, tty disabled") + : std::string("disabled"))) + << "\n" << " SVG history packets kept in RAM: " << ((cfg.svg_history_packets == 0U) ? std::string("all (unbounded)") : std::to_string(cfg.svg_history_packets)) << "\n"; @@ -1579,8 +1738,12 @@ int run(const Config& cfg) { PacketAccumulator current_packet; TtyContinuousState tty_state; TtyGroupAverageState tty_group_state; + Do1NoiseSubtractState tty_do1_noise_state; std::vector tty_frame_words; tty_frame_words.reserve(static_cast(read_capacity_words) * 2U + 16U); + if (tty_do1_noise_subtract) { + tty_do1_noise_state.configure(*cfg.noise_avg_steps); + } if (!fast_tty_avg_stream_mode) { current_packet.reset(target_frames, cfg.channel_count); } @@ -1647,11 +1810,13 @@ int run(const Config& cfg) { << ", DIN samples/s=" << din_samples_per_s << ", frames/s per channel=" << frames_per_ch_per_s << ", packets/s=" << packets_per_s; - if (cfg.di1_mode == Di1Mode::ZeroOnChange) { - std::cout << ", zeroed on DI1 change=" << zeroed_fraction << "% (" - << stats_zeroed_samples << "/" << stats_stored_adc_samples << ")"; - } else if (cfg.di1_mode == Di1Mode::Trace) { - std::cout << ", DI1 trace=enabled"; + if (!fast_tty_avg_stream_mode) { + if (cfg.di1_mode == Di1Mode::ZeroOnChange) { + std::cout << ", zeroed on DI1 change=" << zeroed_fraction << "% (" + << stats_zeroed_samples << "/" << stats_stored_adc_samples << ")"; + } else if (cfg.di1_mode == Di1Mode::Trace) { + std::cout << ", DI1 trace=enabled"; + } } if (tty_writer) { std::cout << ", tty_frames_written=" << tty_frames_written @@ -1706,6 +1871,45 @@ int run(const Config& cfg) { ++packet_avg_steps; }; + auto append_tty_do1_subtracted_step = [&]() { + if (!tty_do1_noise_subtract) { + return; + } + + if (!tty_do1_noise_state.has_complete_step(cfg.channel_count)) { + tty_do1_noise_state.finish_step(); + return; + } + + const double ch1_avg = tty_do1_noise_state.step_average(0U); + const double ch2_avg = (cfg.channel_count <= 1U) ? 0.0 : tty_do1_noise_state.step_average(1U); + + if (tty_do1_noise_state.step_do1_high) { + tty_do1_noise_state.push_noise_average(0U, ch1_avg); + if (cfg.channel_count > 1U) { + tty_do1_noise_state.push_noise_average(1U, ch2_avg); + } + tty_do1_noise_state.finish_step(); + return; + } + + if (tty_do1_noise_state.next_index >= 0xFFFFU) { + fail("TTY protocol step index overflow within packet"); + } + + const double ch1_corrected = ch1_avg - tty_do1_noise_state.noise_baseline(0U); + const double ch2_corrected = + (cfg.channel_count <= 1U) ? 0.0 : (ch2_avg - tty_do1_noise_state.noise_baseline(1U)); + + append_tty_frame((cfg.profile == CaptureProfile::Amplitude) ? 0x001AU : 0x000AU, + tty_do1_noise_state.next_index, + 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.finish_step(); + }; + auto flush_tty_frames = [&]() { if (!tty_writer || tty_frame_words.empty()) { return; @@ -1736,6 +1940,9 @@ int run(const Config& cfg) { if (tty_di1_group_average) { tty_group_state.reset_packet(); append_tty_packet_start(); + } else if (tty_do1_noise_subtract) { + tty_do1_noise_state.reset_packet(); + append_tty_packet_start(); } packet_active = true; }; @@ -1752,6 +1959,9 @@ int run(const Config& cfg) { if (tty_di1_group_average) { append_tty_group_step(); tty_group_state.clear_step(); + } else if (tty_do1_noise_subtract) { + // Keep only complete programmed DO1 steps. Partial steps are dropped on packet close. + tty_do1_noise_state.clear_step(); } const std::size_t frames = fast_tty_avg_stream_mode @@ -1870,6 +2080,8 @@ int run(const Config& cfg) { } if (tty_di1_group_average) { tty_group_state.clear_step(); + } else if (tty_do1_noise_subtract) { + tty_do1_noise_state.clear_step(); } }; @@ -2142,7 +2354,14 @@ int run(const Config& cfg) { if (fast_tty_avg_stream_mode) { if (fast_packet_frames < target_frames) { - tty_group_state.add_sample(lch, adc_raw_value); + if (tty_do1_noise_subtract) { + tty_do1_noise_state.add_sample(lch, adc_raw_value); + if (tty_do1_noise_state.advance_tick()) { + append_tty_do1_subtracted_step(); + } + } else { + tty_group_state.add_sample(lch, adc_raw_value); + } if (lch == (cfg.channel_count - 1U)) { ++fast_packet_frames; ++total_completed_frames; @@ -2234,7 +2453,7 @@ int run(const Config& cfg) { } } else { std::cout << "Captured " << total_completed_packets - << " packet(s) in tty avg stream-only mode\n"; + << " packet(s) in tty fast stream-only mode (" << fast_tty_mode_name << ")\n"; } std::cout << "Average stats: " << "MB/s=" << std::fixed << std::setprecision(3) @@ -2254,14 +2473,16 @@ int run(const Config& cfg) { << (static_cast(total_completed_packets) / std::max(1e-9, static_cast(tick_count_ms() - capture_loop_start) / 1000.0)) << ", packets captured=" << total_completed_packets; - if (cfg.di1_mode == Di1Mode::ZeroOnChange) { - std::cout << ", zeroed on DI1 change=" - << ((total_stored_adc_samples == 0U) - ? 0.0 - : (100.0 * static_cast(total_zeroed_samples) / static_cast(total_stored_adc_samples))) - << "% (" << total_zeroed_samples << "/" << total_stored_adc_samples << ")"; - } else if (cfg.di1_mode == Di1Mode::Trace) { - std::cout << ", DI1 trace=enabled"; + if (!fast_tty_avg_stream_mode) { + if (cfg.di1_mode == Di1Mode::ZeroOnChange) { + std::cout << ", zeroed on DI1 change=" + << ((total_stored_adc_samples == 0U) + ? 0.0 + : (100.0 * static_cast(total_zeroed_samples) / static_cast(total_stored_adc_samples))) + << "% (" << total_zeroed_samples << "/" << total_stored_adc_samples << ")"; + } else if (cfg.di1_mode == Di1Mode::Trace) { + std::cout << ", DI1 trace=enabled"; + } } if (tty_writer) { std::cout << ", tty_frames_written=" << tty_final_stats.frames_written @@ -2269,7 +2490,7 @@ int run(const Config& cfg) { << ", tty_ring_overflows=" << tty_final_stats.ring_overflows; } if (fast_tty_avg_stream_mode) { - std::cout << ", tty avg stream-only mode=enabled"; + std::cout << ", tty fast stream-only mode=" << fast_tty_mode_name; } std::cout << "\n"; diff --git a/run_do1_noise_subtract.sh b/run_do1_noise_subtract.sh new file mode 100755 index 0000000..7df128a --- /dev/null +++ b/run_do1_noise_subtract.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +BIN="${BIN:-./main.exe}" +TTY_PATH="${TTY_PATH:-/tmp/ttyADC_data}" +LIB_DIR="${LIB_DIR:-$HOME/.local/lib}" +NOISE_AVG_STEPS="${NOISE_AVG_STEPS:-}" + +if [[ ! -x "$BIN" ]]; then + echo "Binary '$BIN' not found or not executable. Run ./build_main.sh first." >&2 + exit 1 +fi + +if [[ -z "$NOISE_AVG_STEPS" ]]; then + echo "Set NOISE_AVG_STEPS to a positive integer, for example: NOISE_AVG_STEPS=16" >&2 + exit 1 +fi + +if ! [[ "$NOISE_AVG_STEPS" =~ ^[0-9]+$ ]] || [[ "$NOISE_AVG_STEPS" -eq 0 ]]; then + echo "NOISE_AVG_STEPS must be a positive integer, got '$NOISE_AVG_STEPS'" >&2 + exit 1 +fi + +if [[ -d "$LIB_DIR" ]]; then + export LD_LIBRARY_PATH="${LIB_DIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" +fi + +exec "$BIN" \ + clock:internal \ + internal_ref_hz:2000000 \ + start:di_syn2_rise \ + stop:di_syn2_fall \ + sample_clock_hz:max \ + range:5 \ + duration_ms:100 \ + packet_limit:0 \ + do1_toggle_per_frame \ + do1_noise_subtract \ + "noise_avg_steps:${NOISE_AVG_STEPS}" \ + profile:amplitude \ + "tty:${TTY_PATH}" \ + "$@"