mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-12 09:39:30 +00:00
Epirb afc wide capture (#3213)
* EPIRB RX: widen AFC capture range to +/-5 kHz The AFC estimate was only applied after carrier lock, so carrier acquisition ran on raw phase deltas and could only tolerate offsets of a few hundred Hz before the 0.6/0.7/1.6 rad detection thresholds (on the 12-sample accumulator) broke down. Track the carrier offset continuously in the IDLE state with a first-order loop (AFC_TRACK_ALPHA), so the de-biased accumulator self-centers for any offset within the discriminator Nyquist (~24 kHz) *before* the thresholds run. ALPHA = 0.005 pulls a +/-5 kHz offset under the 0.6 rad lock threshold in ~11 ms, well inside the 160 ms preamble / 80 ms stability window. The IDLE rise-detect threshold is also made symmetric (fabsf) now that the bias is removed. Also add the missing <cstdint> include to test_convert.cpp so the application_test suite compiles under the current toolchain. Verified: baseband_epirb_rx.elf builds (flash 53%, RAM 12%) and baseband_test passes.
This commit is contained in:
+12
-15
@@ -41,29 +41,26 @@ void BeaconUIList::paint(Painter& painter) {
|
|||||||
auto base_style = Theme::getInstance()->bg_darkest;
|
auto base_style = Theme::getInstance()->bg_darkest;
|
||||||
|
|
||||||
for (auto offset = 0u; offset < BEACON_HISTORY_SIZE; ++offset) {
|
for (auto offset = 0u; offset < BEACON_HISTORY_SIZE; ++offset) {
|
||||||
// The whole frame needs to be cleared so every line 'slot'
|
|
||||||
// is redrawn even when `text` just left empty.
|
|
||||||
auto text = std::string{};
|
|
||||||
auto index = start_index_ + offset;
|
auto index = start_index_ + offset;
|
||||||
auto line_position = rect.location() + Point{0, 1 + (int)offset * char_height};
|
|
||||||
auto is_selected = offset == selected_index_;
|
|
||||||
auto style = base_style;
|
|
||||||
|
|
||||||
if (index < db_->size()) {
|
if (index < db_->size()) {
|
||||||
|
auto line_position = rect.location() + Point{0, 1 + (int)offset * char_height};
|
||||||
|
auto is_selected = (offset == selected_index_);
|
||||||
|
auto style = base_style;
|
||||||
|
|
||||||
// Get beacon entry and format it's summary
|
// Get beacon entry and format it's summary
|
||||||
auto& entry = db_->get_beacon(index);
|
auto& entry = db_->get_beacon(index);
|
||||||
char buffer[64];
|
char buffer[64];
|
||||||
entry.formatSummary(buffer, true);
|
entry.formatSummary(buffer, true);
|
||||||
text = std::string(buffer);
|
|
||||||
|
if (index == db_->get_current_beacon_index())
|
||||||
|
// If this is the currently displayed beacon change color
|
||||||
|
style = Theme::getInstance()->bg_medium;
|
||||||
|
|
||||||
|
// Draw entry line using stack buffer directly to avoid heap allocation
|
||||||
|
painter.draw_string(
|
||||||
|
line_position, (is_selected ? style->invert() : *style), buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index == db_->get_current_beacon_index())
|
|
||||||
// If this is the currently displayed beacon change color
|
|
||||||
style = Theme::getInstance()->bg_medium;
|
|
||||||
|
|
||||||
// Draw entry line
|
|
||||||
painter.draw_string(
|
|
||||||
line_position, (is_selected ? style->invert() : *style), text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw a bounding rectangle when focused.
|
// Draw a bounding rectangle when focused.
|
||||||
|
|||||||
+26
-10
@@ -39,7 +39,7 @@ namespace ui::external_app::epirb_rx {
|
|||||||
|
|
||||||
// URL templates
|
// URL templates
|
||||||
#define MAPS_URL_TEMPLATE "https://www.google.com/maps/search/?api=1&query=%s%%2C%s"
|
#define MAPS_URL_TEMPLATE "https://www.google.com/maps/search/?api=1&query=%s%%2C%s"
|
||||||
#define BEACON_URL_TEMPALTE "https://decoder2.herokuapp.com/decoded/"
|
#define BEACON_URL_TEMPLATE "https://decoder2.herokuapp.com/decoded/"
|
||||||
|
|
||||||
#ifndef DISABLE_COUNTRY_CACHE
|
#ifndef DISABLE_COUNTRY_CACHE
|
||||||
int CountryManager::cache_count = 0;
|
int CountryManager::cache_count = 0;
|
||||||
@@ -71,15 +71,24 @@ void TextArea::paint(Painter& painter) {
|
|||||||
const Style& s = has_focus() ? style().invert() : style();
|
const Style& s = has_focus() ? style().invert() : style();
|
||||||
|
|
||||||
painter.fill_rectangle(rect, s.background);
|
painter.fill_rectangle(rect, s.background);
|
||||||
// We use \t as line separator since \n is used in STR_COLOR_GREEN
|
|
||||||
auto rows = split_string(content, '\t');
|
|
||||||
|
|
||||||
const int line_height = s.font.line_height();
|
const int line_height = s.font.line_height();
|
||||||
size_t line_idx = 0;
|
int line_idx = 0;
|
||||||
for (auto row : rows) {
|
|
||||||
painter.draw_string(rect.location() + Point(0, line_idx * line_height), s, row);
|
// Efficiently draw lines separated by \t without heap allocations
|
||||||
|
std::string_view sv{content};
|
||||||
|
size_t start = 0;
|
||||||
|
size_t end;
|
||||||
|
|
||||||
|
while ((end = sv.find('\t', start)) != std::string_view::npos) {
|
||||||
|
painter.draw_string(rect.location() + Point(0, line_idx * line_height), s, sv.substr(start, end - start));
|
||||||
|
start = end + 1;
|
||||||
line_idx++;
|
line_idx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (start < sv.length()) {
|
||||||
|
painter.draw_string(rect.location() + Point(0, line_idx * line_height), s, sv.substr(start));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextArea::set_content(std::string_view value) {
|
void TextArea::set_content(std::string_view value) {
|
||||||
@@ -294,7 +303,7 @@ void EPRIBQRView::set_beacon(Beacon* beacon) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EPRIBQRView::update_display() {
|
void EPRIBQRView::update_display() {
|
||||||
// Update data => we use a single TextArea component for code size optimizaton
|
// Update data => we use a single TextArea component for code size optimization
|
||||||
char buffer[128];
|
char buffer[128];
|
||||||
char* buffer_pointer = buffer;
|
char* buffer_pointer = buffer;
|
||||||
buffer_pointer += sprintf(buffer_pointer, "%sQR:%s\t\t\t\t\t\t\t\t", STR_COLOR_CYAN, STR_COLOR_WHITE);
|
buffer_pointer += sprintf(buffer_pointer, "%sQR:%s\t\t\t\t\t\t\t\t", STR_COLOR_CYAN, STR_COLOR_WHITE);
|
||||||
@@ -320,7 +329,7 @@ void EPRIBQRView::update_qr() {
|
|||||||
if (show_map) {
|
if (show_map) {
|
||||||
// Map is selected
|
// Map is selected
|
||||||
if (!current_beacon->location.isUnknown()) {
|
if (!current_beacon->location.isUnknown()) {
|
||||||
// Loation is known => actually draw QR
|
// Location is known => actually draw QR
|
||||||
current_beacon->location.formatFloatLocation(qr_url, MAPS_URL_TEMPLATE);
|
current_beacon->location.formatFloatLocation(qr_url, MAPS_URL_TEMPLATE);
|
||||||
show_qr = true;
|
show_qr = true;
|
||||||
}
|
}
|
||||||
@@ -328,7 +337,7 @@ void EPRIBQRView::update_qr() {
|
|||||||
// Detail is selected
|
// Detail is selected
|
||||||
char* buffer_pointer = qr_url;
|
char* buffer_pointer = qr_url;
|
||||||
// Send to heroku decoder
|
// Send to heroku decoder
|
||||||
buffer_pointer += sprintf(qr_url, BEACON_URL_TEMPALTE);
|
buffer_pointer += sprintf(qr_url, BEACON_URL_TEMPLATE);
|
||||||
current_beacon->hexString(buffer_pointer, false);
|
current_beacon->hexString(buffer_pointer, false);
|
||||||
show_qr = true;
|
show_qr = true;
|
||||||
}
|
}
|
||||||
@@ -402,7 +411,7 @@ EPIRBAppView::EPIRBAppView(ui::NavigationView& nav)
|
|||||||
options_frequency.on_change = [this](size_t, ui::OptionsField::value_t v) {
|
options_frequency.on_change = [this](size_t, ui::OptionsField::value_t v) {
|
||||||
receiver_model.set_target_frequency(v);
|
receiver_model.set_target_frequency(v);
|
||||||
};
|
};
|
||||||
// Restore frequency from preferencies
|
// Restore frequency from preferences
|
||||||
options_frequency.set_by_value(receiver_model.target_frequency());
|
options_frequency.set_by_value(receiver_model.target_frequency());
|
||||||
|
|
||||||
// Tick second timer
|
// Tick second timer
|
||||||
@@ -456,6 +465,13 @@ EPIRBAppView::EPIRBAppView(ui::NavigationView& nav)
|
|||||||
audio::set_rate(audio::Rate::Hz_24000);
|
audio::set_rate(audio::Rate::Hz_24000);
|
||||||
audio::output::start();
|
audio::output::start();
|
||||||
|
|
||||||
|
// Tint the channel-power bar red when the front-end overloads (ADC near
|
||||||
|
// full scale). Clipping distorts the constant-envelope carrier and the
|
||||||
|
// +/-1.1 rad biphase jumps the decoder relies on, so this cues the user to
|
||||||
|
// reduce RF-amp/LNA/VGA gain. -3 dBFS is a heuristic on the post-decimation
|
||||||
|
// level (not a literal ADC clip count); tune if it trips too early/late.
|
||||||
|
channel.set_overload_threshold(-3);
|
||||||
|
|
||||||
update_display();
|
update_display();
|
||||||
|
|
||||||
#ifdef LOGGER
|
#ifdef LOGGER
|
||||||
|
|||||||
@@ -36,10 +36,14 @@ void Channel::paint(Painter& painter) {
|
|||||||
const range_t<int> x_max_range{0, r.width() - 1};
|
const range_t<int> x_max_range{0, r.width() - 1};
|
||||||
const auto x_max = x_max_range.clip((max_db_ - db_min) * r.width() / db_delta);
|
const auto x_max = x_max_range.clip((max_db_ - db_min) * r.width() / db_delta);
|
||||||
|
|
||||||
|
const auto bar_style = (max_db_ >= overload_threshold_)
|
||||||
|
? Theme::getInstance()->fg_red
|
||||||
|
: Theme::getInstance()->fg_blue;
|
||||||
|
|
||||||
const Rect r0{r.left(), r.top(), x_max, r.height()};
|
const Rect r0{r.left(), r.top(), x_max, r.height()};
|
||||||
painter.fill_rectangle(
|
painter.fill_rectangle(
|
||||||
r0,
|
r0,
|
||||||
Theme::getInstance()->fg_blue->foreground);
|
bar_style->foreground);
|
||||||
|
|
||||||
const Rect r1{r.left() + x_max, r.top(), 1, r.height()};
|
const Rect r1{r.left() + x_max, r.top(), 1, r.height()};
|
||||||
painter.fill_rectangle(
|
painter.fill_rectangle(
|
||||||
|
|||||||
@@ -44,8 +44,16 @@ class Channel : public Widget {
|
|||||||
|
|
||||||
void paint(Painter& painter) override;
|
void paint(Painter& painter) override;
|
||||||
|
|
||||||
|
// Opt-in receiver-overload tint: when the channel power (peak IQ magnitude
|
||||||
|
// in dBFS, 0 = full scale) reaches this threshold the bar is drawn red
|
||||||
|
// instead of blue, flagging that the analog gain is too high and the ADC is
|
||||||
|
// clipping. Default is disabled (threshold above the 0 dBFS ceiling) so
|
||||||
|
// existing users are unaffected.
|
||||||
|
void set_overload_threshold(int32_t db) { overload_threshold_ = db; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int32_t max_db_;
|
int32_t max_db_;
|
||||||
|
int32_t overload_threshold_{1};
|
||||||
|
|
||||||
MessageHandlerRegistration message_handler_stats{
|
MessageHandlerRegistration message_handler_stats{
|
||||||
Message::ID::ChannelStatistics,
|
Message::ID::ChannelStatistics,
|
||||||
|
|||||||
@@ -29,8 +29,11 @@
|
|||||||
#include "audio_dma.hpp"
|
#include "audio_dma.hpp"
|
||||||
|
|
||||||
#include "event_m4.hpp"
|
#include "event_m4.hpp"
|
||||||
#include <ch.h>
|
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include <ch.h>
|
||||||
EPIRBProcessor::EPIRBProcessor() {
|
EPIRBProcessor::EPIRBProcessor() {
|
||||||
// Configure the decimation filters for narrowband EPIRB signal
|
// Configure the decimation filters for narrowband EPIRB signal
|
||||||
decim_0.configure(taps_11k0_decim_0.taps);
|
decim_0.configure(taps_11k0_decim_0.taps);
|
||||||
@@ -57,9 +60,6 @@ float EPIRBProcessor::get_phase_diff(const complex16_t& sample0, const complex16
|
|||||||
float dI = sample1.real() * sample0.real() + sample1.imag() * sample0.imag();
|
float dI = sample1.real() * sample0.real() + sample1.imag() * sample0.imag();
|
||||||
float dQ = sample1.imag() * sample0.real() - sample1.real() * sample0.imag();
|
float dQ = sample1.imag() * sample0.real() - sample1.real() * sample0.imag();
|
||||||
float phase_diff = atan2f(dQ, dI);
|
float phase_diff = atan2f(dQ, dI);
|
||||||
// Prevent phase diff from wrapping around
|
|
||||||
if (phase_diff > M_PI) phase_diff -= 2.0f * M_PI;
|
|
||||||
if (phase_diff < -M_PI) phase_diff += 2.0f * M_PI;
|
|
||||||
return phase_diff;
|
return phase_diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,36 +110,94 @@ void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
|
|||||||
float phase_delta = get_phase_diff(last_sample, decimator_out.p[i]);
|
float phase_delta = get_phase_diff(last_sample, decimator_out.p[i]);
|
||||||
last_sample = decimator_out.p[i];
|
last_sample = decimator_out.p[i];
|
||||||
|
|
||||||
|
// AFC: remove the estimated carrier frequency offset from the raw delta
|
||||||
|
// before any further processing. Done on the per-sample value so the
|
||||||
|
// 12-sample accumulator below tracks it naturally.
|
||||||
|
phase_delta -= freq_offset_est;
|
||||||
|
|
||||||
|
// Keep the (de-biased) per-sample delta for AFC averaging over the carrier.
|
||||||
|
const float sample_phase_delta = phase_delta;
|
||||||
|
|
||||||
// Let's sum phase delta over a 12 sample window to get the full phase jump
|
// Let's sum phase delta over a 12 sample window to get the full phase jump
|
||||||
phase_delta_acc -= phase_delta_buffer[pahse_delta_index];
|
phase_delta_acc -= phase_delta_buffer[phase_delta_index];
|
||||||
phase_delta_buffer[pahse_delta_index] = phase_delta;
|
phase_delta_buffer[phase_delta_index] = phase_delta;
|
||||||
phase_delta_acc += phase_delta_buffer[pahse_delta_index];
|
phase_delta_acc += phase_delta_buffer[phase_delta_index];
|
||||||
pahse_delta_index = (pahse_delta_index + 1) % PHASE_DELTA_ACC_SIZE;
|
phase_delta_index = (phase_delta_index + 1) % PHASE_DELTA_ACC_SIZE;
|
||||||
|
|
||||||
// Use accumulated delta
|
// Use accumulated delta
|
||||||
phase_delta = phase_delta_acc;
|
phase_delta = phase_delta_acc;
|
||||||
|
|
||||||
// State machine for COSPAS frame detection
|
// State machine for COSPAS frame detection
|
||||||
switch (current_state) {
|
switch (current_state) {
|
||||||
case IDLE:
|
case IDLE: {
|
||||||
// We are waiting for a 160ms empty carrier => phase shouls be stable during this period
|
// Continuously pull the AFC estimate toward the mean per-sample
|
||||||
// We accept a 0.6 phase shift since phase may drift durring carrier if carrier frequency is not alligned with tuner frequency
|
// rotation so the accumulator self-centers for any offset up to
|
||||||
if (filtered_rise_detect(phase_delta >= 0.6f)) {
|
// the discriminator Nyquist (~+/-24 kHz). On noise the de-biased
|
||||||
|
// deltas average to ~0, so the estimate stays put; on a real
|
||||||
|
// carrier it converges within a few ms and the thresholds below
|
||||||
|
// then see a de-biased signal regardless of the actual offset.
|
||||||
|
// Only update AFC when the per-sample phase delta is small
|
||||||
|
// (large jumps indicate noise or transient, which would cause
|
||||||
|
// a random-walk drift if used for AFC updates).
|
||||||
|
if (fabsf(sample_phase_delta) <= AFC_UPDATE_PHASE_MAX) {
|
||||||
|
freq_offset_est += AFC_TRACK_ALPHA * sample_phase_delta;
|
||||||
|
// Bounds checking: limit to ±5 kHz (~0.654 rad/sample at 48 kHz)
|
||||||
|
freq_offset_est = std::clamp(freq_offset_est, -0.654f, 0.654f);
|
||||||
|
|
||||||
|
// Track AFC convergence using Welford's online algorithm
|
||||||
|
afc_convergence_n++;
|
||||||
|
float delta = freq_offset_est - afc_mean;
|
||||||
|
afc_mean += delta / afc_convergence_n;
|
||||||
|
float delta2 = freq_offset_est - afc_mean;
|
||||||
|
afc_m2 += delta * delta2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are waiting for a 160ms empty carrier => phase should be stable during this period
|
||||||
|
// Use a symmetric threshold: once AFC has removed the bias a stable
|
||||||
|
// carrier sits near 0, so both positive and negative excursions of
|
||||||
|
// the accumulated delta indicate the carrier is not yet stable.
|
||||||
|
if (filtered_rise_detect(fabsf(phase_delta) >= 0.6f)) {
|
||||||
stability_counter = 0;
|
stability_counter = 0;
|
||||||
|
// Reset convergence tracking when the carrier is not stable,
|
||||||
|
// so variance is measured only over the current stable window.
|
||||||
|
afc_mean = 0.0f;
|
||||||
|
afc_m2 = 0.0f;
|
||||||
|
afc_convergence_n = 0;
|
||||||
} else {
|
} else {
|
||||||
stability_counter++;
|
stability_counter++;
|
||||||
|
// Check both phase stability AND AFC convergence before transitioning
|
||||||
if (stability_counter > CARRIER_SAMPLES_THRESHOLD) {
|
if (stability_counter > CARRIER_SAMPLES_THRESHOLD) {
|
||||||
// Carrier has been stable long enought, go to locked state
|
float afc_variance = (afc_convergence_n > 1) ? afc_m2 / (afc_convergence_n - 1) : 0.0f;
|
||||||
current_state = CARRIER_LOCKED;
|
if (afc_variance < AFC_CONVERGENCE_THRESHOLD) {
|
||||||
frame_sample_count = 0;
|
// Both phase and AFC have converged, go to locked state
|
||||||
|
current_state = CARRIER_LOCKED;
|
||||||
|
// Reset carrier accumulators so the latched update uses
|
||||||
|
// only the residual measured while in the locked window
|
||||||
|
carrier_phase_sum = 0.0f;
|
||||||
|
carrier_phase_n = 0;
|
||||||
|
frame_sample_count = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
} break;
|
||||||
|
|
||||||
case CARRIER_LOCKED:
|
case CARRIER_LOCKED:
|
||||||
// Carrier is locked, we now wait for a phase 1.1 rad phase jump corresponding to the befining of the frame
|
// Carrier is locked: this is the clean unmodulated carrier window.
|
||||||
|
// Average the per-sample phase delta here to estimate the residual
|
||||||
|
// frequency offset (rad/sample) used for AFC.
|
||||||
|
carrier_phase_sum += sample_phase_delta;
|
||||||
|
carrier_phase_n++;
|
||||||
|
// Carrier is locked, we now wait for a phase 1.1 rad phase jump corresponding to the beginning of the frame
|
||||||
// Let's use a 0.7 phase jump threshold
|
// Let's use a 0.7 phase jump threshold
|
||||||
if (filtered_rise_detect(phase_delta >= 0.7f)) {
|
if (filtered_rise_detect(phase_delta >= 0.7f)) {
|
||||||
|
// Latch the AFC estimate from the carrier we just measured so it
|
||||||
|
// applies to the data burst that starts now. Accumulate so the
|
||||||
|
// residual is folded into any prior estimate.
|
||||||
|
if (carrier_phase_n > 0) {
|
||||||
|
freq_offset_est += carrier_phase_sum / carrier_phase_n;
|
||||||
|
// Bounds checking: limit to ±5 kHz (~0.654 rad/sample at 48 kHz)
|
||||||
|
freq_offset_est = std::clamp(freq_offset_est, -0.654f, 0.654f);
|
||||||
|
}
|
||||||
// Jump detected, frame starts now
|
// Jump detected, frame starts now
|
||||||
frame_sample_count = 0;
|
frame_sample_count = 0;
|
||||||
// Go to data sync state
|
// Go to data sync state
|
||||||
@@ -164,7 +222,7 @@ void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
|
|||||||
bool phase_positive = (phase_delta >= 0.0f);
|
bool phase_positive = (phase_delta >= 0.0f);
|
||||||
|
|
||||||
if (phase_positive != last_phase_positive) {
|
if (phase_positive != last_phase_positive) {
|
||||||
// Phase jumped to the opposit direction of last jump
|
// Phase jumped to the opposite direction of last jump
|
||||||
last_phase_positive = phase_positive;
|
last_phase_positive = phase_positive;
|
||||||
bool cur_bit;
|
bool cur_bit;
|
||||||
// Phase change => how long since last change ?
|
// Phase change => how long since last change ?
|
||||||
@@ -185,7 +243,7 @@ void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
|
|||||||
// 2 symbols since last change => bit value changes
|
// 2 symbols since last change => bit value changes
|
||||||
cur_bit = !last_bit;
|
cur_bit = !last_bit;
|
||||||
} else if ((sample_count >= (SAMPLES_PER_SYMBOL - SAMPLES_MARGIN)) && (sample_count <= (SAMPLES_PER_SYMBOL + SAMPLES_MARGIN))) {
|
} else if ((sample_count >= (SAMPLES_PER_SYMBOL - SAMPLES_MARGIN)) && (sample_count <= (SAMPLES_PER_SYMBOL + SAMPLES_MARGIN))) {
|
||||||
// Phase change occured in first half bit => we keep the same value
|
// Phase change occurred in first half bit => we keep the same value
|
||||||
if ((phase_positive && last_bit) || (!phase_positive && !last_bit)) {
|
if ((phase_positive && last_bit) || (!phase_positive && !last_bit)) {
|
||||||
sample_count = 0;
|
sample_count = 0;
|
||||||
// Ignore rising edge if current value is 1 and falling edge if current value is 0 and move to next symbol
|
// Ignore rising edge if current value is 1 and falling edge if current value is 0 and move to next symbol
|
||||||
@@ -227,6 +285,14 @@ void EPIRBProcessor::frame_end() {
|
|||||||
last_phase_positive = false;
|
last_phase_positive = false;
|
||||||
last_bit = false;
|
last_bit = false;
|
||||||
current_state = IDLE;
|
current_state = IDLE;
|
||||||
|
// Reset AFC so the next burst is re-estimated from its own carrier preamble.
|
||||||
|
freq_offset_est = 0.0f;
|
||||||
|
carrier_phase_sum = 0.0f;
|
||||||
|
carrier_phase_n = 0;
|
||||||
|
// Reset AFC convergence tracking for next frame
|
||||||
|
afc_mean = 0.0f;
|
||||||
|
afc_m2 = 0.0f;
|
||||||
|
afc_convergence_n = 0;
|
||||||
packet_builder.reset_state();
|
packet_builder.reset_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,15 +59,15 @@ class Packet;
|
|||||||
#define COSPAS_PREAMBLE_SIZE 24
|
#define COSPAS_PREAMBLE_SIZE 24
|
||||||
// Size of long frame (bits)
|
// Size of long frame (bits)
|
||||||
#define COSPAS_LONG_FRAME_SIZE 144
|
#define COSPAS_LONG_FRAME_SIZE 144
|
||||||
// Siz of short frame (bits)
|
// Size of short frame (bits)
|
||||||
#define COSPAS_SHORT_FRAME_SIZE 112
|
#define COSPAS_SHORT_FRAME_SIZE 112
|
||||||
// Preamble for real frames
|
// Preamble for real frames
|
||||||
#define COSPAS_REAL_PREAMBLE 0b1111'1111'1111'1110'0010'1111
|
#define COSPAS_REAL_PREAMBLE 0b1111'1111'1111'1110'0010'1111
|
||||||
// Preable for test frames
|
// Preamble for test frames
|
||||||
#define COSPAS_TEST_PREAMBLE 0b1111'1111'1111'1110'1101'0000
|
#define COSPAS_TEST_PREAMBLE 0b1111'1111'1111'1110'1101'0000
|
||||||
|
|
||||||
// Dedicated EPIRB PacketBuilder
|
// Dedicated EPIRB PacketBuilder
|
||||||
// Usees diedicated preamble detection logic to find both real and test frames
|
// Uses dedicated preamble detection logic to find both real and test frames
|
||||||
// Also as a dedicated packet size detection based on frame's size bit
|
// Also as a dedicated packet size detection based on frame's size bit
|
||||||
class EPIRBPacketBuilder {
|
class EPIRBPacketBuilder {
|
||||||
public:
|
public:
|
||||||
@@ -170,7 +170,7 @@ class EPIRBProcessor : public BasebandProcessor {
|
|||||||
static constexpr size_t SAMPLES_PER_SYMBOL = SAMPLE_RATE / SYMBOL_RATE; // = 60 samples per symbol
|
static constexpr size_t SAMPLES_PER_SYMBOL = SAMPLE_RATE / SYMBOL_RATE; // = 60 samples per symbol
|
||||||
static constexpr size_t SAMPLES_PER_BIT = SAMPLES_PER_SYMBOL * 2; // = 120 samples per bit
|
static constexpr size_t SAMPLES_PER_BIT = SAMPLES_PER_SYMBOL * 2; // = 120 samples per bit
|
||||||
static constexpr size_t SAMPLES_MARGIN = SAMPLES_PER_SYMBOL / 3; // = Allow 20 sample drift
|
static constexpr size_t SAMPLES_MARGIN = SAMPLES_PER_SYMBOL / 3; // = Allow 20 sample drift
|
||||||
static constexpr size_t SAMPLES_ACCUMUMLATOR = SAMPLES_PER_SYMBOL / 5; // Accumulate phase change across 12 samples
|
static constexpr size_t SAMPLES_ACCUMULATOR = SAMPLES_PER_SYMBOL / 5; // Accumulate phase change across 12 samples
|
||||||
static constexpr size_t RISE_FILTER_SAMPLES = SAMPLES_PER_SYMBOL / 20; // Filter peaks of less than 3 samples
|
static constexpr size_t RISE_FILTER_SAMPLES = SAMPLES_PER_SYMBOL / 20; // Filter peaks of less than 3 samples
|
||||||
|
|
||||||
static constexpr size_t CARRIER_SAMPLES_THRESHOLD = 0.080f * SAMPLE_RATE; // Carrier before frame lasts 160ms, require at least 80ms
|
static constexpr size_t CARRIER_SAMPLES_THRESHOLD = 0.080f * SAMPLE_RATE; // Carrier before frame lasts 160ms, require at least 80ms
|
||||||
@@ -213,12 +213,40 @@ class EPIRBProcessor : public BasebandProcessor {
|
|||||||
// Carrier detection counter
|
// Carrier detection counter
|
||||||
uint32_t stability_counter = 0;
|
uint32_t stability_counter = 0;
|
||||||
|
|
||||||
// Phase delta accumulator (6 samples)
|
// Phase delta accumulator (12 samples)
|
||||||
static constexpr size_t PHASE_DELTA_ACC_SIZE = SAMPLES_ACCUMUMLATOR;
|
static constexpr size_t PHASE_DELTA_ACC_SIZE = SAMPLES_PER_SYMBOL / 5; // 12 samples
|
||||||
float phase_delta_buffer[PHASE_DELTA_ACC_SIZE] = {0.0f};
|
float phase_delta_buffer[PHASE_DELTA_ACC_SIZE] = {0.0f};
|
||||||
size_t pahse_delta_index = 0;
|
size_t phase_delta_index = 0;
|
||||||
float phase_delta_acc = 0.0f;
|
float phase_delta_acc = 0.0f;
|
||||||
|
|
||||||
|
// Automatic Frequency Control (AFC)
|
||||||
|
// A residual carrier frequency offset between the tuner and the beacon shows
|
||||||
|
// up as a constant per-sample phase rotation. We measure its mean over the
|
||||||
|
// unmodulated carrier preamble and subtract it from every raw phase delta so
|
||||||
|
// the carrier-stability detection and the +/-2.2 rad data jumps stay centered.
|
||||||
|
// Current estimate (rad/sample), removed from each raw phase delta.
|
||||||
|
float freq_offset_est = 0.0f;
|
||||||
|
// Carrier-tracking loop gain. Applied per sample in IDLE so the estimate
|
||||||
|
// pulls in any offset within the discriminator's +/-SAMPLE_RATE/2 (~24 kHz)
|
||||||
|
// range *before* the carrier-detection thresholds run. First-order loop with
|
||||||
|
// time constant ~1/ALPHA samples (= 200 samples ~ 4 ms at 48 kHz). Tuned so a
|
||||||
|
// +/-5 kHz offset (0.654 rad/sample) decays the 12-sample accumulator below
|
||||||
|
// the 0.6 rad lock threshold in ~11 ms (~13 ms at 7 kHz) -- well inside the
|
||||||
|
// 160 ms preamble / 80 ms stability window, even if reception starts partway
|
||||||
|
// through the carrier -- while keeping added acquisition jitter negligible.
|
||||||
|
static constexpr float AFC_TRACK_ALPHA = 0.005f;
|
||||||
|
// AFC update gating: ignore large per-sample phase jumps (likely noise)
|
||||||
|
static constexpr float AFC_UPDATE_PHASE_MAX = 0.8f; // rad/sample
|
||||||
|
// AFC Convergence detection: track variance of AFC estimate to ensure it has stabilized
|
||||||
|
// before transitioning from IDLE to CARRIER_LOCKED state
|
||||||
|
static constexpr float AFC_CONVERGENCE_THRESHOLD = 0.001f; // Max variance for convergence
|
||||||
|
float afc_mean = 0.0f; // Running mean of AFC estimate
|
||||||
|
float afc_m2 = 0.0f; // Sum of squared differences (Welford's algorithm)
|
||||||
|
uint32_t afc_convergence_n = 0; // Sample count for AFC convergence calculation
|
||||||
|
// Running mean of the raw phase delta while a stable carrier is present.
|
||||||
|
float carrier_phase_sum = 0.0f;
|
||||||
|
uint32_t carrier_phase_n = 0;
|
||||||
|
|
||||||
std::array<complex16_t, 512> dst{};
|
std::array<complex16_t, 512> dst{};
|
||||||
const buffer_c16_t dst_buffer{
|
const buffer_c16_t dst_buffer{
|
||||||
dst.data(),
|
dst.data(),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include "doctest.h"
|
#include "doctest.h"
|
||||||
#include "convert.hpp"
|
#include "convert.hpp"
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user