some kamil_adc fixes
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "shared_types.hpp"
|
||||
#include "switch_driver.hpp"
|
||||
|
||||
namespace radar::kamil {
|
||||
|
||||
/**
|
||||
* @brief Drives the input/output RF switches in lock-step with sweep boundaries.
|
||||
*
|
||||
* The E-502 stream is free-running: sweeps are delimited by DI_SYN2 edges with a
|
||||
* hardware idle gap between them. On every completed sweep we step to the next
|
||||
* switch combination *during that gap*, so the next sweep starts already settled
|
||||
* in the new state and no samples are lost.
|
||||
*
|
||||
* The one failure mode of doing this purely in software is the readout backlog:
|
||||
* if, at the moment we observe a sweep's end, we are running so far behind real
|
||||
* time that the toggle could not have landed within the gap, the upcoming sweep
|
||||
* straddles the transition. We flag that sweep *dirty*; the consumer drops it and
|
||||
* we re-take the same combination on the following sweep (which is then clean,
|
||||
* because no transition happens during its gap). A clean sweep costs nothing; a
|
||||
* miss costs exactly one extra sweep period for that one combination.
|
||||
*
|
||||
* The sequencer owns the two switch drivers and exposes only what the collector
|
||||
* needs at a sweep boundary: which combination the upcoming sweep belongs to and
|
||||
* whether it is expected to be clean. All timing policy lives here so the recv
|
||||
* loop stays readable.
|
||||
*/
|
||||
class SwitchSequencer {
|
||||
public:
|
||||
struct Settings {
|
||||
/// Combinations to cycle through, one sweep each, in order. Empty disables
|
||||
/// the sequencer entirely (the collector then streams a single passthrough
|
||||
/// channel exactly as before).
|
||||
std::vector<radar::ipc::ComboKey> combos{};
|
||||
/// Guaranteed minimum inter-sweep idle window, in milliseconds. This is a
|
||||
/// hardware property of the sweep generator; treat it as a lower bound.
|
||||
double gap_ms = 10.0;
|
||||
/// RF settle time required after a position change, in milliseconds.
|
||||
double settle_ms = 1.0;
|
||||
};
|
||||
|
||||
SwitchSequencer(
|
||||
Settings settings,
|
||||
std::unique_ptr<drivers::SwitchDriver> input_switch,
|
||||
std::unique_ptr<drivers::SwitchDriver> output_switch
|
||||
)
|
||||
: settings_(std::move(settings)),
|
||||
input_switch_(std::move(input_switch)),
|
||||
output_switch_(std::move(output_switch)) {}
|
||||
|
||||
[[nodiscard]] auto enabled() const -> bool { return !settings_.combos.empty(); }
|
||||
|
||||
/// Open both switch drivers and park at the first combination.
|
||||
void open() {
|
||||
if (!enabled()) {
|
||||
return;
|
||||
}
|
||||
output_switch_->open();
|
||||
input_switch_->open();
|
||||
index_ = 0;
|
||||
applied_ = settings_.combos.front();
|
||||
apply(applied_);
|
||||
upcoming_dirty_ = false; // the first sweep is captured in a settled state
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (!enabled()) {
|
||||
return;
|
||||
}
|
||||
// Best-effort: teardown must never throw out of the collector's stop path.
|
||||
try {
|
||||
output_switch_->close();
|
||||
} catch (...) {
|
||||
}
|
||||
try {
|
||||
input_switch_->close();
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Combination the in-progress / upcoming sweep is captured under.
|
||||
[[nodiscard]] auto current_combo() const -> radar::ipc::ComboKey {
|
||||
return settings_.combos[index_];
|
||||
}
|
||||
|
||||
/// Whether the in-progress / upcoming sweep is expected to straddle a switch
|
||||
/// transition and must be dropped by the consumer.
|
||||
[[nodiscard]] auto current_dirty() const -> bool { return upcoming_dirty_; }
|
||||
|
||||
/**
|
||||
* @brief Advance the sequence at a completed sweep's end (start of the gap).
|
||||
*
|
||||
* @param backlog_ms Estimated readout lag at this instant — how far behind
|
||||
* real time we are, i.e. how late the toggle we issue now will land.
|
||||
*
|
||||
* If the sweep that just ended was clean we step to the next combination and
|
||||
* toggle the switches now, inside the gap. If it was dirty we re-take the same
|
||||
* combination (the switches are already there and long settled). The upcoming
|
||||
* sweep is marked dirty only when a real transition happens *and* the backlog
|
||||
* leaves no room for it to settle before the next sweep starts.
|
||||
*/
|
||||
void on_sweep_end(double backlog_ms) {
|
||||
if (!enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (upcoming_dirty_) {
|
||||
// The sweep that just ended straddled a transition: re-take the same
|
||||
// combination. The switches are already applied and settled, so the
|
||||
// next sweep is clean without touching the hardware again.
|
||||
upcoming_dirty_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
index_ = (index_ + 1U) % settings_.combos.size();
|
||||
const radar::ipc::ComboKey next = settings_.combos[index_];
|
||||
const bool position_changed = !(next == applied_);
|
||||
if (position_changed) {
|
||||
apply(next);
|
||||
applied_ = next;
|
||||
}
|
||||
upcoming_dirty_ = position_changed && ((backlog_ms + settings_.settle_ms) > settings_.gap_ms);
|
||||
}
|
||||
|
||||
private:
|
||||
void apply(const radar::ipc::ComboKey& combo) {
|
||||
// Output first, then input — same order the sweep orchestrator uses, so
|
||||
// both acquisition paths drive an identical hardware sequence.
|
||||
output_switch_->switch_to(combo.output_pos);
|
||||
input_switch_->switch_to(combo.input_pos);
|
||||
}
|
||||
|
||||
Settings settings_;
|
||||
std::unique_ptr<drivers::SwitchDriver> input_switch_;
|
||||
std::unique_ptr<drivers::SwitchDriver> output_switch_;
|
||||
std::size_t index_ = 0;
|
||||
radar::ipc::ComboKey applied_{};
|
||||
bool upcoming_dirty_ = false;
|
||||
};
|
||||
|
||||
} // namespace radar::kamil
|
||||
@@ -17,6 +17,11 @@
|
||||
#include "capture_file_writer.h"
|
||||
#include "tty_protocol_writer.h"
|
||||
|
||||
#include "switch_sequencer.h"
|
||||
#include "run_config.hpp"
|
||||
#include "h7992_minimal_driver.hpp"
|
||||
#include "hmc349a_minimal_driver.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
@@ -113,6 +118,12 @@ struct Config {
|
||||
std::string live_html_path = "live_plot.html";
|
||||
std::string live_json_path = "live_plot.json";
|
||||
std::optional<std::string> tty_path;
|
||||
// When set, the collector reads switch and combo configuration from this
|
||||
// run_config.json and drives the RF switches itself, one combination per
|
||||
// sweep, tagging each emitted sweep with its combo (see SwitchSequencer).
|
||||
std::optional<std::string> run_config_path;
|
||||
double switch_gap_ms = 10.0;
|
||||
double switch_settle_ms = 1.0;
|
||||
bool di1_group_average = false;
|
||||
bool do1_toggle_per_frame = false;
|
||||
bool do1_noise_subtract = false;
|
||||
@@ -479,6 +490,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"
|
||||
<< " [config:run_config.json] [switch_gap_ms:10] [switch_settle_ms:1]\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"
|
||||
@@ -514,6 +526,13 @@ void print_help(const char* exe_name) {
|
||||
<< " 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 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"
|
||||
<< " config:run_config.json -> switch-aware mode: read the input/output switch and combo\n"
|
||||
<< " configuration from this run_config.json and drive the RF switches\n"
|
||||
<< " in lock-step with sweeps; each sweep is tagged with its combo via a\n"
|
||||
<< " 0x00C0 frame (input_pos, output_pos, dirty). Omit for standalone mode.\n"
|
||||
<< " switch_gap_ms:10 -> guaranteed minimum inter-sweep idle window; lower bound used to decide\n"
|
||||
<< " whether an in-gap switch lands before the next sweep starts\n"
|
||||
<< " switch_settle_ms:1 -> RF settle time required after a switch position change\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"
|
||||
@@ -793,6 +812,18 @@ Config parse_args(int argc, char** argv) {
|
||||
cfg.tty_path = arg.substr(4);
|
||||
continue;
|
||||
}
|
||||
if (starts_with(arg, "config:")) {
|
||||
cfg.run_config_path = arg.substr(7);
|
||||
continue;
|
||||
}
|
||||
if (starts_with(arg, "switch_gap_ms:")) {
|
||||
cfg.switch_gap_ms = parse_double(arg.substr(14), "switch_gap_ms");
|
||||
continue;
|
||||
}
|
||||
if (starts_with(arg, "switch_settle_ms:")) {
|
||||
cfg.switch_settle_ms = parse_double(arg.substr(17), "switch_settle_ms");
|
||||
continue;
|
||||
}
|
||||
fail("Unknown argument: " + arg);
|
||||
}
|
||||
|
||||
@@ -1203,6 +1234,10 @@ constexpr uint32_t kDo1TogglePeriodTicks = 2U;
|
||||
constexpr uint32_t kDo1CyclePatternWords = kDo1TogglePeriodTicks * 2U;
|
||||
constexpr uint32_t kDo8HighTicks = 2U;
|
||||
constexpr uint16_t kTtyMarkerDi8High = 0x00A8U;
|
||||
// Combo tag frame: [0x00C0, input_pos, output_pos, dirty]. Emitted right after a
|
||||
// sweep-boundary frame to tell the consumer which switch combination the upcoming
|
||||
// sweep belongs to, and whether it straddled a switch transition (dirty != 0).
|
||||
constexpr uint16_t kTtyMarkerComboTag = 0x00C0U;
|
||||
constexpr uint32_t kStreamInputAdcFlag = 0x80000000U;
|
||||
constexpr uint32_t kStreamInputCalibratedAdcFlag = 0x40000000U;
|
||||
|
||||
@@ -1802,6 +1837,55 @@ void print_device_info(const t_x502_info& info) {
|
||||
<< "MCU firmware: " << info.mcu_firmware_ver << "\n";
|
||||
}
|
||||
|
||||
// Build one switch driver (native GPIO or mock) from a parsed switch config,
|
||||
// reusing the exact driver implementations the sweep orchestrator uses so both
|
||||
// acquisition paths drive identical hardware.
|
||||
std::unique_ptr<radar::drivers::SwitchDriver> make_switch_driver(const radar::config::SwitchConfig& sc) {
|
||||
if (sc.driver_kind == radar::config::SwitchDriverKind::HMC349A) {
|
||||
return std::make_unique<radar::drivers::HMC349AMinimalDriver>(
|
||||
radar::drivers::HMC349AMinimalDriverSettings{
|
||||
.name = sc.name,
|
||||
.mode = sc.driver_mode,
|
||||
.positions = sc.positions,
|
||||
.default_position = sc.default_position,
|
||||
.gpio_chip = sc.gpio_chip,
|
||||
.pin_a = sc.pin_a,
|
||||
.invert_logic = sc.invert_logic,
|
||||
});
|
||||
}
|
||||
return std::make_unique<radar::drivers::H7992MinimalDriver>(
|
||||
radar::drivers::H7992MinimalDriverSettings{
|
||||
.name = sc.name,
|
||||
.mode = sc.driver_mode,
|
||||
.positions = sc.positions,
|
||||
.default_position = sc.default_position,
|
||||
.gpio_chip = sc.gpio_chip,
|
||||
.pin_a = sc.pin_a,
|
||||
.pin_b = sc.pin_b,
|
||||
});
|
||||
}
|
||||
|
||||
// Construct the switch sequencer from the run_config given via `config:<path>`.
|
||||
// Returns nullptr (switching disabled, single-channel passthrough) when no config
|
||||
// was supplied or it declares no combinations.
|
||||
std::unique_ptr<radar::kamil::SwitchSequencer> make_switch_sequencer(const Config& cfg) {
|
||||
if (!cfg.run_config_path.has_value()) {
|
||||
return nullptr;
|
||||
}
|
||||
const radar::config::RunConfig rc = radar::config::load_run_config(*cfg.run_config_path);
|
||||
if (rc.run_combos.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
radar::kamil::SwitchSequencer::Settings settings;
|
||||
settings.combos = rc.run_combos;
|
||||
settings.gap_ms = cfg.switch_gap_ms;
|
||||
settings.settle_ms = cfg.switch_settle_ms;
|
||||
return std::make_unique<radar::kamil::SwitchSequencer>(
|
||||
std::move(settings),
|
||||
make_switch_driver(rc.input_switch),
|
||||
make_switch_driver(rc.output_switch));
|
||||
}
|
||||
|
||||
int run(const Config& cfg) {
|
||||
Api api;
|
||||
DeviceHandle device(api);
|
||||
@@ -2029,6 +2113,21 @@ int run(const Config& cfg) {
|
||||
tty_writer->emit_packet_start(tty_packet_start_marker);
|
||||
}
|
||||
}
|
||||
// Switch sequencer (optional): drives the RF switches in lock-step with sweep
|
||||
// boundaries and tags each sweep with its combination. Disabled (nullptr) when
|
||||
// no run_config was passed, so the standalone/calibration use stays unchanged.
|
||||
std::unique_ptr<radar::kamil::SwitchSequencer> sequencer = make_switch_sequencer(cfg);
|
||||
if (sequencer && sequencer->enabled()) {
|
||||
sequencer->open();
|
||||
std::cout << "Switch sequencer enabled: cycling switch combinations per sweep "
|
||||
<< "(gap_ms=" << cfg.switch_gap_ms << ", settle_ms=" << cfg.switch_settle_ms << ")\n";
|
||||
} else {
|
||||
sequencer.reset(); // normalize to nullptr so call sites can test the pointer
|
||||
}
|
||||
// Estimated readout backlog (ms) at the current loop iteration; how far behind
|
||||
// real time we are, used to decide whether an in-gap switch lands in time.
|
||||
double current_backlog_ms = 0.0;
|
||||
|
||||
std::unique_ptr<CaptureFileWriter> writer;
|
||||
if (!fast_tty_avg_stream_mode) {
|
||||
writer = std::make_unique<CaptureFileWriter>(cfg.csv_path, cfg.svg_path, cfg.live_html_path, cfg.live_json_path);
|
||||
@@ -2317,6 +2416,15 @@ int run(const Config& cfg) {
|
||||
|
||||
auto append_tty_packet_start = [&]() {
|
||||
append_tty_frame(tty_packet_start_marker, 0xFFFF, 0xFFFF, 0xFFFF);
|
||||
// Right after the boundary, tag the upcoming sweep with its switch combo so
|
||||
// the consumer can group sweeps and drop the dirty ones.
|
||||
if (sequencer) {
|
||||
const radar::ipc::ComboKey combo = sequencer->current_combo();
|
||||
append_tty_frame(kTtyMarkerComboTag,
|
||||
static_cast<uint16_t>(combo.input_pos),
|
||||
static_cast<uint16_t>(combo.output_pos),
|
||||
static_cast<uint16_t>(sequencer->current_dirty() ? 1U : 0U));
|
||||
}
|
||||
};
|
||||
|
||||
auto append_tty_group_step = [&]() {
|
||||
@@ -2714,6 +2822,12 @@ int run(const Config& cfg) {
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
// A real sweep just completed: step the switch sequence into the gap. A
|
||||
// user-stop teardown is not a sweep boundary, so skip it.
|
||||
if (sequencer && (frames != 0U) && (reason != PacketCloseReason::UserStop)) {
|
||||
sequencer->on_sweep_end(current_backlog_ms);
|
||||
}
|
||||
|
||||
packet_active = false;
|
||||
packet_avg_steps = 0;
|
||||
fast_packet_frames = 0;
|
||||
@@ -2768,6 +2882,11 @@ int run(const Config& cfg) {
|
||||
recv_request_words = std::min<uint32_t>(ready_words, read_capacity_words);
|
||||
recv_timeout_ms = 0;
|
||||
}
|
||||
// Backlog already queued in the driver = how far behind real time we are.
|
||||
// Drives the switch sequencer's in-gap clean/dirty decision.
|
||||
if ((ready_err == X502_ERR_OK) && (combined_input_rate_hz > 0.0)) {
|
||||
current_backlog_ms = 1000.0 * static_cast<double>(ready_words) / combined_input_rate_hz;
|
||||
}
|
||||
|
||||
const int32_t recvd = api.Recv(device.hnd, raw.data(), recv_request_words, recv_timeout_ms);
|
||||
if (recvd < 0) {
|
||||
@@ -3170,6 +3289,11 @@ int run(const Config& cfg) {
|
||||
|
||||
expect_ok(api, api.StreamsStop(device.hnd), "Stop streams");
|
||||
device.streams_started = false;
|
||||
|
||||
// Park the switches in their safe default and release the GPIO lines.
|
||||
if (sequencer) {
|
||||
sequencer->close();
|
||||
}
|
||||
if (cfg.do1_toggle_per_frame) {
|
||||
const uint32_t clear_mask = kE502Do1Mask | kE502Do2Mask | (cfg.do8_freq_ref ? kE502Do8Mask : 0U);
|
||||
expect_ok(api,
|
||||
|
||||
Reference in New Issue
Block a user