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:
+296
-268
@@ -1,268 +1,296 @@
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __PROC_EPIRB_H__
|
||||
#define __PROC_EPIRB_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
#include <complex>
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
#include "channel_decimator.hpp"
|
||||
#include "matched_filter.hpp"
|
||||
#include "packet_builder.hpp"
|
||||
#include "baseband_packet.hpp"
|
||||
#include "message.hpp"
|
||||
#include "buffer.hpp"
|
||||
|
||||
// Specan is disable to keep application size below the 32k limit
|
||||
// #define SPECAN
|
||||
|
||||
#ifdef SPECAN
|
||||
#include "spectrum_collector.hpp"
|
||||
#endif
|
||||
|
||||
#include "audio_output.hpp"
|
||||
#include "dsp_demodulate.hpp"
|
||||
|
||||
// Forward declarations for types only used as pointers/references
|
||||
class Message;
|
||||
namespace baseband {
|
||||
class Packet;
|
||||
}
|
||||
|
||||
// COSPAS / SARSAT 406 frame constants
|
||||
// Size of preamble (bits)
|
||||
#define COSPAS_PREAMBLE_SIZE 24
|
||||
// Size of long frame (bits)
|
||||
#define COSPAS_LONG_FRAME_SIZE 144
|
||||
// Siz of short frame (bits)
|
||||
#define COSPAS_SHORT_FRAME_SIZE 112
|
||||
// Preamble for real frames
|
||||
#define COSPAS_REAL_PREAMBLE 0b1111'1111'1111'1110'0010'1111
|
||||
// Preable for test frames
|
||||
#define COSPAS_TEST_PREAMBLE 0b1111'1111'1111'1110'1101'0000
|
||||
|
||||
// Dedicated EPIRB PacketBuilder
|
||||
// Usees diedicated preamble detection logic to find both real and test frames
|
||||
// Also as a dedicated packet size detection based on frame's size bit
|
||||
class EPIRBPacketBuilder {
|
||||
public:
|
||||
using EPIRBHandler = void (*)(void* context, const baseband::Packet& packet);
|
||||
|
||||
EPIRBPacketBuilder(
|
||||
void* context,
|
||||
EPIRBHandler handler)
|
||||
: context(context),
|
||||
handler(handler) {
|
||||
}
|
||||
|
||||
void execute(
|
||||
const uint_fast8_t symbol) {
|
||||
bit_history.add(symbol);
|
||||
|
||||
switch (state) {
|
||||
case State::Preamble: {
|
||||
// Detect both real and test fram preambles
|
||||
bool is_real = real_sync_matcher(bit_history, packet.size());
|
||||
bool is_test = test_sync_matcher(bit_history, packet.size());
|
||||
if (is_real || is_test) {
|
||||
// Append preamble to the begining of the packet
|
||||
uint64_t preamble = is_real ? COSPAS_REAL_PREAMBLE : COSPAS_TEST_PREAMBLE;
|
||||
for (int8_t i = (COSPAS_PREAMBLE_SIZE - 1); i >= 0; i--) {
|
||||
packet.add((preamble >> i) & 0x1);
|
||||
}
|
||||
state = State::Format;
|
||||
}
|
||||
} break;
|
||||
case State::Format:
|
||||
packet.add(symbol);
|
||||
// 144 bits for long frames and 112 for short frames
|
||||
size = symbol ? COSPAS_LONG_FRAME_SIZE : COSPAS_SHORT_FRAME_SIZE;
|
||||
state = State::Payload;
|
||||
|
||||
break;
|
||||
case State::Payload:
|
||||
packet.add(symbol);
|
||||
|
||||
if (packet.size() >= size) {
|
||||
flush();
|
||||
} else {
|
||||
if (packet.size() >= packet.capacity()) {
|
||||
reset_state();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
reset_state();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void flush() {
|
||||
// Timestamp is not set here to save some app space (not used on ui side)
|
||||
// packet.set_timestamp(Timestamp::now());
|
||||
if (handler) handler(context, packet);
|
||||
reset_state();
|
||||
}
|
||||
|
||||
void reset_state() {
|
||||
packet.clear();
|
||||
bit_history = BitHistory();
|
||||
state = State::Preamble;
|
||||
}
|
||||
|
||||
private:
|
||||
enum State {
|
||||
Preamble,
|
||||
Format,
|
||||
Payload,
|
||||
};
|
||||
|
||||
BitHistory bit_history{};
|
||||
BitPattern real_sync_matcher{COSPAS_REAL_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
||||
BitPattern test_sync_matcher{COSPAS_TEST_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
||||
void* context;
|
||||
EPIRBHandler handler;
|
||||
uint8_t size{0};
|
||||
|
||||
State state{State::Preamble};
|
||||
baseband::Packet packet{};
|
||||
};
|
||||
|
||||
class EPIRBProcessor : public BasebandProcessor {
|
||||
public:
|
||||
EPIRBProcessor();
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
// Baseband frequency is set to 3,072,000 samples / sec
|
||||
static constexpr uint32_t BASEBAND_SAMPLE_RATE = 3072000;
|
||||
static constexpr uint32_t SAMPLE_RATE = BASEBAND_SAMPLE_RATE / 8 / 8; // We use to decimators with factor 8 each
|
||||
static constexpr uint32_t SYMBOL_RATE = 800; // 400 bps + Manchester (2 1/2 bits per symbol) => 800
|
||||
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_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 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_MAX_SAMPLES = 0.900f * SAMPLE_RATE; // Carrier + frame lasts 160ms + 520ms + 100ms post carrier = 880ms
|
||||
static constexpr size_t FRAME_MAX_SAMPLES = SAMPLES_PER_BIT * (144 * 1.1f); // Frame max length (add 1% error margin)
|
||||
|
||||
AudioOutput audio_output{};
|
||||
|
||||
// Config
|
||||
uint8_t squelch_level{50};
|
||||
// Audio on/off logic is disabled to save app space
|
||||
// bool audio_on{true};
|
||||
#ifdef SPECAN
|
||||
bool spectrum_on{false};
|
||||
#endif
|
||||
|
||||
std::array<float, 32> audio{};
|
||||
const buffer_f32_t audio_buffer{
|
||||
audio.data(),
|
||||
audio.size()};
|
||||
|
||||
// Last received bit (for manchester deconding)
|
||||
bool last_bit = false;
|
||||
// Sample count since last symbol
|
||||
uint16_t sample_count{0};
|
||||
// Sample count since frame start
|
||||
uint16_t frame_sample_count{0};
|
||||
// True if last phase shift was positive, false otherwise
|
||||
bool last_phase_positive = false;
|
||||
// Counter used for peak filtering
|
||||
uint16_t rise_detection_count{0};
|
||||
|
||||
// Frame detection state machine states
|
||||
enum State { IDLE,
|
||||
CARRIER_LOCKED,
|
||||
DATA_SYNC,
|
||||
POST_FRAME };
|
||||
// Current state for frame detection state machine
|
||||
State current_state = IDLE;
|
||||
// Carrier detection counter
|
||||
uint32_t stability_counter = 0;
|
||||
|
||||
// Phase delta accumulator (6 samples)
|
||||
static constexpr size_t PHASE_DELTA_ACC_SIZE = SAMPLES_ACCUMUMLATOR;
|
||||
float phase_delta_buffer[PHASE_DELTA_ACC_SIZE] = {0.0f};
|
||||
size_t pahse_delta_index = 0;
|
||||
float phase_delta_acc = 0.0f;
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
|
||||
// Decimation chain for 406 MHz EPIRB signal processing
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
// Audio filtering
|
||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||
// Audio demodulation
|
||||
dsp::demodulate::FM demod{};
|
||||
#ifdef SPECAN
|
||||
SpectrumCollector channel_spectrum{};
|
||||
#endif
|
||||
// Store last stample for phase delta calculation
|
||||
complex16_t last_sample{};
|
||||
|
||||
// EPIRB packet structure:
|
||||
// - Sync pattern: 111111111111111 (15 bits)
|
||||
// - Frame sync: 000101111(real) / 011010000(test) (9 bits)
|
||||
// - Data: 120 bits (long frame) / // bits (short frame)
|
||||
// - BCH error correction: 10 bits
|
||||
// Total: 144 bits (long frame) / 112 bits (short frame)
|
||||
EPIRBPacketBuilder packet_builder{
|
||||
this,
|
||||
[](void* ctx, const baseband::Packet& p) {
|
||||
static_cast<EPIRBProcessor*>(ctx)->payload_handler(p);
|
||||
}};
|
||||
|
||||
void payload_handler(const baseband::Packet& packet);
|
||||
// Compute phase diff between two samples
|
||||
float get_phase_diff(const complex16_t& sample0, const complex16_t& sample1);
|
||||
// End current frame
|
||||
void frame_end();
|
||||
// Rise detection with peak filtering
|
||||
bool filtered_rise_detect(bool condition);
|
||||
// Configure audio processing
|
||||
void configure_audio();
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
BASEBAND_SAMPLE_RATE, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
#endif /*__PROC_EPIRB_H__*/
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __PROC_EPIRB_H__
|
||||
#define __PROC_EPIRB_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
#include <complex>
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
#include "channel_decimator.hpp"
|
||||
#include "matched_filter.hpp"
|
||||
#include "packet_builder.hpp"
|
||||
#include "baseband_packet.hpp"
|
||||
#include "message.hpp"
|
||||
#include "buffer.hpp"
|
||||
|
||||
// Specan is disable to keep application size below the 32k limit
|
||||
// #define SPECAN
|
||||
|
||||
#ifdef SPECAN
|
||||
#include "spectrum_collector.hpp"
|
||||
#endif
|
||||
|
||||
#include "audio_output.hpp"
|
||||
#include "dsp_demodulate.hpp"
|
||||
|
||||
// Forward declarations for types only used as pointers/references
|
||||
class Message;
|
||||
namespace baseband {
|
||||
class Packet;
|
||||
}
|
||||
|
||||
// COSPAS / SARSAT 406 frame constants
|
||||
// Size of preamble (bits)
|
||||
#define COSPAS_PREAMBLE_SIZE 24
|
||||
// Size of long frame (bits)
|
||||
#define COSPAS_LONG_FRAME_SIZE 144
|
||||
// Size of short frame (bits)
|
||||
#define COSPAS_SHORT_FRAME_SIZE 112
|
||||
// Preamble for real frames
|
||||
#define COSPAS_REAL_PREAMBLE 0b1111'1111'1111'1110'0010'1111
|
||||
// Preamble for test frames
|
||||
#define COSPAS_TEST_PREAMBLE 0b1111'1111'1111'1110'1101'0000
|
||||
|
||||
// Dedicated EPIRB PacketBuilder
|
||||
// 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
|
||||
class EPIRBPacketBuilder {
|
||||
public:
|
||||
using EPIRBHandler = void (*)(void* context, const baseband::Packet& packet);
|
||||
|
||||
EPIRBPacketBuilder(
|
||||
void* context,
|
||||
EPIRBHandler handler)
|
||||
: context(context),
|
||||
handler(handler) {
|
||||
}
|
||||
|
||||
void execute(
|
||||
const uint_fast8_t symbol) {
|
||||
bit_history.add(symbol);
|
||||
|
||||
switch (state) {
|
||||
case State::Preamble: {
|
||||
// Detect both real and test fram preambles
|
||||
bool is_real = real_sync_matcher(bit_history, packet.size());
|
||||
bool is_test = test_sync_matcher(bit_history, packet.size());
|
||||
if (is_real || is_test) {
|
||||
// Append preamble to the begining of the packet
|
||||
uint64_t preamble = is_real ? COSPAS_REAL_PREAMBLE : COSPAS_TEST_PREAMBLE;
|
||||
for (int8_t i = (COSPAS_PREAMBLE_SIZE - 1); i >= 0; i--) {
|
||||
packet.add((preamble >> i) & 0x1);
|
||||
}
|
||||
state = State::Format;
|
||||
}
|
||||
} break;
|
||||
case State::Format:
|
||||
packet.add(symbol);
|
||||
// 144 bits for long frames and 112 for short frames
|
||||
size = symbol ? COSPAS_LONG_FRAME_SIZE : COSPAS_SHORT_FRAME_SIZE;
|
||||
state = State::Payload;
|
||||
|
||||
break;
|
||||
case State::Payload:
|
||||
packet.add(symbol);
|
||||
|
||||
if (packet.size() >= size) {
|
||||
flush();
|
||||
} else {
|
||||
if (packet.size() >= packet.capacity()) {
|
||||
reset_state();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
reset_state();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void flush() {
|
||||
// Timestamp is not set here to save some app space (not used on ui side)
|
||||
// packet.set_timestamp(Timestamp::now());
|
||||
if (handler) handler(context, packet);
|
||||
reset_state();
|
||||
}
|
||||
|
||||
void reset_state() {
|
||||
packet.clear();
|
||||
bit_history = BitHistory();
|
||||
state = State::Preamble;
|
||||
}
|
||||
|
||||
private:
|
||||
enum State {
|
||||
Preamble,
|
||||
Format,
|
||||
Payload,
|
||||
};
|
||||
|
||||
BitHistory bit_history{};
|
||||
BitPattern real_sync_matcher{COSPAS_REAL_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
||||
BitPattern test_sync_matcher{COSPAS_TEST_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
||||
void* context;
|
||||
EPIRBHandler handler;
|
||||
uint8_t size{0};
|
||||
|
||||
State state{State::Preamble};
|
||||
baseband::Packet packet{};
|
||||
};
|
||||
|
||||
class EPIRBProcessor : public BasebandProcessor {
|
||||
public:
|
||||
EPIRBProcessor();
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
// Baseband frequency is set to 3,072,000 samples / sec
|
||||
static constexpr uint32_t BASEBAND_SAMPLE_RATE = 3072000;
|
||||
static constexpr uint32_t SAMPLE_RATE = BASEBAND_SAMPLE_RATE / 8 / 8; // We use to decimators with factor 8 each
|
||||
static constexpr uint32_t SYMBOL_RATE = 800; // 400 bps + Manchester (2 1/2 bits per symbol) => 800
|
||||
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_MARGIN = SAMPLES_PER_SYMBOL / 3; // = Allow 20 sample drift
|
||||
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 CARRIER_SAMPLES_THRESHOLD = 0.080f * SAMPLE_RATE; // Carrier before frame lasts 160ms, require at least 80ms
|
||||
static constexpr size_t CARRIER_MAX_SAMPLES = 0.900f * SAMPLE_RATE; // Carrier + frame lasts 160ms + 520ms + 100ms post carrier = 880ms
|
||||
static constexpr size_t FRAME_MAX_SAMPLES = SAMPLES_PER_BIT * (144 * 1.1f); // Frame max length (add 1% error margin)
|
||||
|
||||
AudioOutput audio_output{};
|
||||
|
||||
// Config
|
||||
uint8_t squelch_level{50};
|
||||
// Audio on/off logic is disabled to save app space
|
||||
// bool audio_on{true};
|
||||
#ifdef SPECAN
|
||||
bool spectrum_on{false};
|
||||
#endif
|
||||
|
||||
std::array<float, 32> audio{};
|
||||
const buffer_f32_t audio_buffer{
|
||||
audio.data(),
|
||||
audio.size()};
|
||||
|
||||
// Last received bit (for manchester deconding)
|
||||
bool last_bit = false;
|
||||
// Sample count since last symbol
|
||||
uint16_t sample_count{0};
|
||||
// Sample count since frame start
|
||||
uint16_t frame_sample_count{0};
|
||||
// True if last phase shift was positive, false otherwise
|
||||
bool last_phase_positive = false;
|
||||
// Counter used for peak filtering
|
||||
uint16_t rise_detection_count{0};
|
||||
|
||||
// Frame detection state machine states
|
||||
enum State { IDLE,
|
||||
CARRIER_LOCKED,
|
||||
DATA_SYNC,
|
||||
POST_FRAME };
|
||||
// Current state for frame detection state machine
|
||||
State current_state = IDLE;
|
||||
// Carrier detection counter
|
||||
uint32_t stability_counter = 0;
|
||||
|
||||
// Phase delta accumulator (12 samples)
|
||||
static constexpr size_t PHASE_DELTA_ACC_SIZE = SAMPLES_PER_SYMBOL / 5; // 12 samples
|
||||
float phase_delta_buffer[PHASE_DELTA_ACC_SIZE] = {0.0f};
|
||||
size_t phase_delta_index = 0;
|
||||
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{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
|
||||
// Decimation chain for 406 MHz EPIRB signal processing
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
// Audio filtering
|
||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||
// Audio demodulation
|
||||
dsp::demodulate::FM demod{};
|
||||
#ifdef SPECAN
|
||||
SpectrumCollector channel_spectrum{};
|
||||
#endif
|
||||
// Store last stample for phase delta calculation
|
||||
complex16_t last_sample{};
|
||||
|
||||
// EPIRB packet structure:
|
||||
// - Sync pattern: 111111111111111 (15 bits)
|
||||
// - Frame sync: 000101111(real) / 011010000(test) (9 bits)
|
||||
// - Data: 120 bits (long frame) / // bits (short frame)
|
||||
// - BCH error correction: 10 bits
|
||||
// Total: 144 bits (long frame) / 112 bits (short frame)
|
||||
EPIRBPacketBuilder packet_builder{
|
||||
this,
|
||||
[](void* ctx, const baseband::Packet& p) {
|
||||
static_cast<EPIRBProcessor*>(ctx)->payload_handler(p);
|
||||
}};
|
||||
|
||||
void payload_handler(const baseband::Packet& packet);
|
||||
// Compute phase diff between two samples
|
||||
float get_phase_diff(const complex16_t& sample0, const complex16_t& sample1);
|
||||
// End current frame
|
||||
void frame_end();
|
||||
// Rise detection with peak filtering
|
||||
bool filtered_rise_detect(bool condition);
|
||||
// Configure audio processing
|
||||
void configure_audio();
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
BASEBAND_SAMPLE_RATE, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
#endif /*__PROC_EPIRB_H__*/
|
||||
|
||||
Reference in New Issue
Block a user