Rtty tx and rx (#2977)

This commit is contained in:
Totoo
2026-02-16 14:34:55 +01:00
committed by GitHub
parent 95eafeb812
commit 794daf5257
21 changed files with 1809 additions and 1 deletions
+16
View File
@@ -674,6 +674,22 @@ set(MODE_CPPSRC
)
DeclareTargets(PMRS morse)
### RTTY RX
set(MODE_CPPSRC
proc_rtty_rx.cpp
)
DeclareTargets(PRTR rtty_rx)
### RTTY TX
set(MODE_CPPSRC
proc_rtty_tx.cpp
)
DeclareTargets(PRTT rtty_tx)
### SD over USB
set(MODE_INCDIR
+334
View File
@@ -0,0 +1,334 @@
/*
* Copyright (C) 2026 HTotoo
*
* 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.
*/
#include "proc_rtty_rx.hpp"
#include "portapack_shared_memory.hpp"
#include "audio_dma.hpp"
#include "event_m4.hpp"
// RTTY Timing Limits (at 24kHz)
static constexpr uint32_t MIN_VALID_PULSE = 200;
static constexpr uint32_t MAX_VALID_PULSE = 800;
void RTTYRxProcessor::configure() {
configured = false;
baseband_thread.set_sampling_rate(baseband_fs);
// 1. 3.072M -> 384k
decim_0.configure(taps_4k25_decim_0.taps);
// 2. 384k -> 48k
decim_1.configure(taps_4k25_decim_1.taps);
// 3. 48k -> 24k
channel_filter.configure(taps_11k0_channel.taps, 2);
// FM Demodulator
demod.configure(24000, 9000);
// Audio Output
audio_output.configure(iir_config_passthrough, iir_config_passthrough, 1.0f);
// Reset State
val_max = -200000;
val_min = 200000;
uart_state = WAIT_START;
inverted_polarity = false;
// Default to standard 45.45 baud
estimated_bit_width = 528;
samples_per_bit = 528;
pulse_measure_counter = 0;
configured = true;
}
// Variables for Fast-Lock Auto Baud
uint32_t candidate_width = 0;
uint8_t candidate_hits = 0;
uint32_t squelch_closed_timer = 0;
bool is_squelched = true;
void RTTYRxProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
buffer_c16_t decim_1_target{dst_buffer_data.data() + 256, 256};
const auto decim_1_out = decim_1.execute(decim_0_out, decim_1_target);
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
auto audio = demod.execute(channel_out, audio_buffer);
feed_channel_stats(channel_out);
for (size_t i = 0; i < audio.count; i++) {
int16_t sample = audio.p[i];
// DECODER INPUT
int32_t fm_val = (int32_t)sample * 32;
// 1. ENVELOPE TRACKING (Floating)
if (fm_val > val_max) val_max = fm_val;
if (fm_val < val_min) val_min = fm_val;
// 2. DECAY
// Shrink the envelope spread (Max - Min) slowly.
if (++decay_timer == 0) {
int32_t spread = val_max - val_min;
if (spread > 200) {
int32_t decay = (spread >> 7) + 1;
if (val_max > val_min + decay) val_max -= decay;
if (val_min < val_max - decay) val_min += decay;
} else {
// val_max += 100;
// val_min -= 100;
// temp off. but kept it for memory
}
}
// 3. OFFSET CALCULATION
int32_t midpoint = (val_max + val_min) / 2;
// 4. SIGNAL CENTERING
int32_t centered_val = fm_val - midpoint;
// LPF for Slicer
fm_val_smoothed += (centered_val - fm_val_smoothed) >> LPF_ALPHA_SHIFT;
// 5. SQUELCH
int32_t spread = val_max - val_min;
if (is_squelched) {
if (spread > 600) is_squelched = false;
} else {
if (spread < 300) is_squelched = true;
}
process_demodulated_sample(fm_val_smoothed);
// 6. AUDIO PATH
if (is_squelched) {
audio.p[i] = 0;
} else {
int32_t audio_boost = sample * 48;
if (audio_boost > 32767)
audio_boost = 32767;
else if (audio_boost < -32768)
audio_boost = -32768;
audio.p[i] = (int16_t)audio_boost;
}
}
audio_output.write(audio);
// UI Update
if (tx_message.data_len > 0) {
if (baud_rate == 0 && samples_per_bit > 0) {
uint32_t b = (final_fs * 100) / samples_per_bit;
if (b > 4300 && b < 4700)
b = 4500;
else if (b > 4800 && b < 5200)
b = 5000;
else if (b > 7200 && b < 7800)
b = 7500;
tx_message.baud = (uint16_t)b;
} else {
tx_message.baud = baud_rate;
}
tx_message.shift = shift_hz;
if (shared_memory.application_queue.push(tx_message)) {
tx_message.data_len = 0;
}
}
}
void RTTYRxProcessor::process_demodulated_sample(int32_t sample) {
// 1. Squelch Check
if (is_squelched) {
squelch_closed_timer++;
if (squelch_closed_timer > 12000) {
uart_state = WAIT_START;
current_slicer_bit = 1;
pulse_measure_counter = 0;
inverted_polarity = false;
if (baud_rate == 0) {
estimated_bit_width = 528;
samples_per_bit = 528;
}
}
return;
}
squelch_closed_timer = 0;
// 2. Schmitt Trigger
int32_t hysteresis = (val_max - val_min) / 8;
uint8_t raw_bit = current_slicer_bit;
if (inverted_polarity) raw_bit = !raw_bit;
if (sample > hysteresis)
raw_bit = 1;
else if (sample < -hysteresis)
raw_bit = 0;
// Polarity Check
if (raw_bit == 0) {
if (++polarity_timer > 7200) {
inverted_polarity = !inverted_polarity;
polarity_timer = 0;
val_max = -200000;
val_min = 200000;
uart_state = WAIT_START;
}
} else {
polarity_timer = 0;
}
current_slicer_bit = inverted_polarity ? !raw_bit : raw_bit;
// 3. Auto Baud
if (baud_rate == 0) {
pulse_measure_counter++;
if (current_slicer_bit != last_bit_state) {
update_baud_estimation(pulse_measure_counter);
pulse_measure_counter = 0;
last_bit_state = current_slicer_bit;
}
}
// 4. UART State Machine
switch (uart_state) {
case WAIT_START:
if (current_slicer_bit == 0) {
phase_counter = samples_per_bit / 2;
uart_state = CHECK_START;
}
break;
case CHECK_START:
if (--phase_counter == 0) {
if (current_slicer_bit == 0) {
phase_counter = samples_per_bit;
bit_counter = 0;
shift_reg = 0;
uart_state = READ_BITS;
} else {
uart_state = WAIT_START;
}
}
break;
case READ_BITS:
if (--phase_counter == 0) {
if (current_slicer_bit) shift_reg |= (1 << bit_counter);
phase_counter = samples_per_bit;
bit_counter++;
if (bit_counter >= 5) {
uart_state = WAIT_STOP;
}
}
break;
case WAIT_STOP:
if (--phase_counter == 0) {
// Accept data even if stop bit is noisy (0)
// This improves reception during fades
// if (current_slicer_bit == 1) {
append_data(shift_reg & 0x1F);
//}
uart_state = WAIT_START;
}
break;
}
}
void RTTYRxProcessor::update_baud_estimation(uint32_t pulse_width) {
if (pulse_width < MIN_VALID_PULSE || pulse_width > MAX_VALID_PULSE) return;
int32_t diff = (int32_t)pulse_width - (int32_t)estimated_bit_width;
if (diff < 0) diff = -diff;
if (diff < (int32_t)(estimated_bit_width / 6)) {
estimated_bit_width = (estimated_bit_width * 7 + pulse_width) / 8;
samples_per_bit = estimated_bit_width;
candidate_hits = 0;
} else {
int32_t cand_diff = (int32_t)pulse_width - (int32_t)candidate_width;
if (cand_diff < 0) cand_diff = -cand_diff;
if (cand_diff < (int32_t)(candidate_width / 8)) {
candidate_hits++;
if (candidate_hits >= 3) {
estimated_bit_width = (candidate_width + pulse_width) / 2;
samples_per_bit = estimated_bit_width;
candidate_hits = 0;
uart_state = WAIT_START;
}
} else {
candidate_width = pulse_width;
candidate_hits = 1;
}
}
}
void RTTYRxProcessor::append_data(uint8_t raw_baudot_code) {
if (tx_message.data_len < tx_message.max_len) {
tx_message.data[tx_message.data_len] = raw_baudot_code;
tx_message.data_len++;
}
}
void RTTYRxProcessor::on_message(const Message* const message) {
if (message->id == Message::ID::RTTYData) {
const auto& rtty_msg = static_cast<const RTTYDataMessage&>(*message);
if (rtty_msg.baud != baud_rate) {
baud_rate = rtty_msg.baud;
if (baud_rate > 0) {
const float real_baud = (float)baud_rate / 100.0f;
samples_per_bit = (uint32_t)((float)final_fs / real_baud);
estimated_bit_width = samples_per_bit;
} else {
estimated_bit_width = 528;
samples_per_bit = 528;
inverted_polarity = false;
}
uart_state = WAIT_START;
}
shift_hz = rtty_msg.shift;
if (!configured) {
configure();
}
}
}
int main() {
audio::dma::init_audio_out();
EventDispatcher event_dispatcher{std::make_unique<RTTYRxProcessor>()};
event_dispatcher.run();
return 0;
}
+119
View File
@@ -0,0 +1,119 @@
/*
* Copyright (C) 2026 HTotoo
*
* 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_RTTY_RX_H__
#define __PROC_RTTY_RX_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "rssi_thread.hpp"
#include "message.hpp"
#include "dsp_decimate.hpp"
#include "dsp_demodulate.hpp"
#include "audio_output.hpp"
#include "dsp_fir_taps.hpp"
class RTTYRxProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
static constexpr size_t baseband_fs = 3072000;
// RTTY Config
uint16_t baud_rate = 0; // 0 = Auto-detect
uint16_t shift_hz = 170;
bool configured = false;
// DSP Rates
// Stage 0: 3.072M / 8 = 384k
// Stage 1: 384k / 8 = 48k
// Stage 2: 48k / 2 = 24k (Audio/Demod)
static constexpr uint32_t decim_0_out_fs = baseband_fs / 8;
static constexpr uint32_t decim_1_out_fs = decim_0_out_fs / 8;
static constexpr uint32_t final_fs = decim_1_out_fs / 2; // 24000 Hz
// Tuning Constants
static constexpr int32_t LPF_ALPHA_SHIFT = 2;
static constexpr int32_t MIN_SHIFT_SPREAD = 400;
// Decimation Chain
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
dsp::decimate::FIRAndDecimateComplex channel_filter{};
// Demodulator & Audio
dsp::demodulate::FM demod{};
AudioOutput audio_output{};
// Buffers
std::array<complex16_t, 512> dst_buffer_data{};
const buffer_c16_t dst_buffer{dst_buffer_data.data(), dst_buffer_data.size()};
std::array<int16_t, 32> audio_data{};
const buffer_s16_t audio_buffer{audio_data.data(), audio_data.size()};
// Output Message
RTTYDataMessage tx_message{};
// Demodulator State
int32_t fm_val_smoothed = 0;
// Tracker State
int32_t val_max = 0;
int32_t val_min = 0;
uint8_t decay_timer = 0;
// Auto-Baud State
uint32_t pulse_measure_counter = 0;
uint8_t last_bit_state = 0;
uint32_t estimated_bit_width = 528; // ~45 baud @ 24k
// UART State
enum UartState {
WAIT_START,
CHECK_START,
READ_BITS,
WAIT_STOP
};
UartState uart_state = WAIT_START;
uint32_t samples_per_bit = 528;
uint32_t phase_counter = 0;
uint8_t bit_counter = 0;
uint8_t shift_reg = 0;
uint8_t current_slicer_bit = 1;
// Polarity
bool inverted_polarity = true;
uint32_t polarity_timer = 0;
void configure();
void process_demodulated_sample(int32_t sample);
void update_baud_estimation(uint32_t pulse_width);
void append_data(uint8_t raw_baudot_code);
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
};
#endif /*__PROC_RTTY_RX_H__*/
+245
View File
@@ -0,0 +1,245 @@
/*
* Copyright (C) 2026 HTotoo
*
* 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.
*/
#include "proc_rtty_tx.hpp"
#include "sine_table_int8.hpp"
#include "event_m4.hpp"
#include <algorithm>
static constexpr uint32_t LEAD_IN_SAMPLES = 204800;
static inline uint32_t hz_to_delta(int32_t hz, uint32_t fs) {
int64_t delta = ((int64_t)hz * (int64_t)UINT32_MAX) / (int64_t)fs;
return (uint32_t)delta;
}
void RTTYTXProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) {
for (size_t i = 0; i < buffer.count; i++) {
buffer.p[i] = {0, 0};
}
return;
}
for (size_t i = 0; i < buffer.count; i++) {
bool advance = false;
if (state == State::LeadIn) {
lead_counter++;
if (lead_counter >= LEAD_IN_SAMPLES) {
advance = true;
}
} else if (state == State::LeadOut) {
lead_counter++;
if (lead_counter >= 460000) {
txprogress_message.done = true;
shared_memory.application_queue.push(txprogress_message);
configured = false;
state = State::Idle;
}
} else if (state != State::Idle) {
uint32_t previous_phase = baud_phase;
baud_phase += baud_phase_increment;
if (baud_phase < previous_phase) {
advance = true;
}
}
if (advance) {
advance_state();
}
// Tone selection
uint32_t target_delta;
bool is_mark = true;
switch (state) {
case State::StartBit:
is_mark = false;
break;
case State::DataBits:
is_mark = (current_char >> bit_pos) & 1;
break;
default:
is_mark = true;
break;
}
target_delta = is_mark ? delta_mark : delta_space;
// Slew limiter
int32_t diff = (int32_t)target_delta - (int32_t)current_delta;
int32_t abs_diff = diff < 0 ? -diff : diff;
if (abs_diff <= (int32_t)slew_rate) {
current_delta = target_delta;
} else {
current_delta += (diff > 0) ? slew_rate : -slew_rate;
}
phase += current_delta;
int8_t re = sine_table_i8[((phase + 0x40000000) & 0xFF000000) >> 24];
int8_t im = sine_table_i8[(phase & 0xFF000000) >> 24];
buffer.p[i] = {re, im};
}
}
void RTTYTXProcessor::advance_state() {
switch (state) {
case State::Idle:
state = State::LeadIn;
lead_counter = 0;
break;
case State::LeadIn:
if (buffer_pop(current_char)) {
state = State::StartBit;
baud_phase = 0;
baud_phase_increment = base_baud_phase_increment;
} else {
state = State::LeadOut;
lead_counter = 0;
}
break;
case State::StartBit:
state = State::DataBits;
bit_pos = 0;
break;
case State::DataBits:
bit_pos++;
if (bit_pos >= 5) {
state = State::StopBit;
}
break;
case State::StopBit:
if (buffer_pop(current_char)) {
state = State::StartBit;
} else {
state = State::LeadOut;
lead_counter = 0;
}
break;
case State::LeadOut:
break;
}
}
void RTTYTXProcessor::configure(
uint16_t baud,
uint16_t shift,
int16_t mark_tone_,
int16_t space_tone_,
uint8_t stop_bits_,
bool inverted_) {
if (baud == 0) return;
// baud phase increment
uint32_t new_base_baud_inc = (uint32_t)((uint64_t)baud * UINT32_MAX / (baseband_fs * 100ULL));
// Stop bits
configured_stop_bits = stop_bits_;
if (configured_stop_bits < 2) configured_stop_bits = 2;
int32_t freq_mark = mark_tone_;
int32_t freq_space = space_tone_;
if (inverted_) {
std::swap(freq_mark, freq_space);
}
uint32_t new_delta_mark = hz_to_delta(freq_mark, baseband_fs);
uint32_t new_delta_space = hz_to_delta(freq_space, baseband_fs);
// Slew rate
uint32_t samples_per_bit = (uint32_t)((uint64_t)UINT32_MAX / new_base_baud_inc);
uint32_t transition_samples = samples_per_bit / 10;
if (transition_samples == 0) transition_samples = 1;
uint32_t shift_delta = hz_to_delta(shift, baseband_fs);
uint32_t new_slew_rate = shift_delta / transition_samples;
if (new_slew_rate == 0) new_slew_rate = 1;
base_baud_phase_increment = new_base_baud_inc;
delta_mark = new_delta_mark;
delta_space = new_delta_space;
slew_rate = new_slew_rate;
if (!configured) {
current_delta = delta_mark;
lead_counter = 0;
phase = 0;
baud_phase = 0;
baud_phase_increment = base_baud_phase_increment;
configured = true;
}
}
void RTTYTXProcessor::on_message(const Message* const msg) {
if (msg->id == Message::ID::RTTYData) {
const auto& rtty_msg = *reinterpret_cast<const RTTYDataMessage*>(msg);
configure(rtty_msg.baud,
rtty_msg.shift,
rtty_msg.mark_tone,
rtty_msg.space_tone,
rtty_msg.stopbits,
rtty_msg.inverted);
for (int i = 0; i < 15; i++) {
buffer_push(0x1F); // LTRS
}
buffer_push(0x08); // CR
buffer_push(0x02); // LF
for (uint16_t i = 0; i < rtty_msg.data_len && i < rtty_msg.max_len; i++) {
buffer_push(rtty_msg.data[i]);
}
buffer_push(0x08); // CR
buffer_push(0x02); // LF
if (state == State::Idle) {
state = State::LeadIn;
lead_counter = 0;
}
}
}
// Ring Buffer Logic
bool RTTYTXProcessor::buffer_push(uint8_t byte) {
size_t next_head = (head + 1) % data_buffer.size();
if (next_head == tail) return false;
data_buffer[head] = byte;
head = next_head;
return true;
}
bool RTTYTXProcessor::buffer_pop(uint8_t& byte) {
if (head == tail) return false;
byte = data_buffer[tail];
tail = (tail + 1) % data_buffer.size();
return true;
}
bool RTTYTXProcessor::buffer_empty() const {
return head == tail;
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<RTTYTXProcessor>()};
event_dispatcher.run();
return 0;
}
+90
View File
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2026 HTotoo
*
* 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_RTTY_TX_H__
#define __PROC_RTTY_TX_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "portapack_shared_memory.hpp"
#include <array>
class RTTYTXProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const msg) override;
private:
static constexpr uint32_t baseband_fs = 2048000;
bool configured{false};
// RTTY Configuration
uint32_t samples_per_bit{0};
// Stop bits configuration: 2=1.0, 3=1.5, 4=2.0
uint8_t configured_stop_bits{2};
// FSK State
uint32_t delta_mark{0}; // Phase step for Mark
uint32_t delta_space{0}; // Phase step for Space
uint32_t current_delta{0}; // Smoothed delta
uint32_t slew_rate{0}; // Max change per sample
uint32_t phase{0}; // Phase accumulator
// Precision Timing
uint32_t baud_phase{0};
uint32_t baud_phase_increment{0};
uint32_t base_baud_phase_increment{0}; // Store the standard 1.0 bit rate
// State Machine
enum class State {
Idle,
LeadIn,
StartBit,
DataBits,
StopBit,
LeadOut
};
State state{State::Idle};
uint32_t lead_counter{0};
uint8_t current_char{0};
uint8_t bit_pos{0};
// Ring Buffer
std::array<uint8_t, 1024> data_buffer{};
volatile size_t head{0};
volatile size_t tail{0};
void configure(uint16_t baud, uint16_t shift, int16_t mark_tone_, int16_t space_tone_, uint8_t stop_bits_, bool inverted_);
void advance_state();
bool buffer_push(uint8_t byte);
bool buffer_pop(uint8_t& byte);
bool buffer_empty() const;
TXProgressMessage txprogress_message{};
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Transmit};
};
#endif /* __PROC_RTTY_TX_H__ */