diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 6e21957bc..f1c85eccc 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -380,6 +380,17 @@ set(EXTCPPSRC #signal_hunter external/signal_hunter/main.cpp external/signal_hunter/ui_signal_hunter.cpp + + + #tetra rx + external/tetra_rx/main.cpp + external/tetra_rx/ui_tetra_rx.cpp + external/tetra_rx/tetra_crc.cpp + external/tetra_rx/tetra_descrambler.cpp + external/tetra_rx/tetra_interleave.cpp + external/tetra_rx/tetra_rcpc.cpp + external/tetra_rx/tetra_viterbi.cpp + ) set(EXTAPPLIST @@ -473,6 +484,7 @@ set(EXTAPPLIST hard_reset secplustx signal_hunter + tetra_rx ) # sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index bb06da7ec..6fc27a33a 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -114,6 +114,7 @@ MEMORY ram_external_app_vor_rx (rwx) : org = 0xAE090000, len = 32k ram_external_app_vor_tx (rwx) : org = 0xAE0A0000, len = 32k ram_external_app_signal_hunter (rwx) : org = 0xAE0B0000, len = 32k + ram_external_app_tetra_rx (rwx) : org = 0xAE0C0000, len = 32k } SECTIONS @@ -663,4 +664,13 @@ SECTIONS KEEP(*(.external_app.app_signal_hunter.application_information)); *(*ui*external_app*signal_hunter*); } > ram_external_app_signal_hunter + + .external_app_tetra_rx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_tetra_rx.application_information)); + *(*ui*external_app*tetra_rx*); + } > ram_external_app_tetra_rx + + + } diff --git a/firmware/application/external/tetra_rx/main.cpp b/firmware/application/external/tetra_rx/main.cpp new file mode 100644 index 000000000..f4fb57ddf --- /dev/null +++ b/firmware/application/external/tetra_rx/main.cpp @@ -0,0 +1,81 @@ +/* + * 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 "ui_tetra_rx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::tetra_rx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::tetra_rx +extern "C" { + +__attribute__((section(".external_app.app_tetra_rx.application_information"), used)) application_information_t _application_information_tetra_rx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::tetra_rx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "Tetra", + /*.bitmap_data = */ { + 0xE0, + 0x0F, + 0x18, + 0x38, + 0xE4, + 0x67, + 0x7E, + 0xCE, + 0xC7, + 0xCC, + 0x00, + 0x00, + 0xFF, + 0x4F, + 0xBA, + 0xB2, + 0x9A, + 0xEE, + 0xBA, + 0xB2, + 0x00, + 0x00, + 0x3B, + 0xE3, + 0x73, + 0x7E, + 0xC6, + 0x27, + 0x1C, + 0x18, + 0xF0, + 0x07, + }, + /*.icon_color = */ ui::Color::orange().v, + /*.menu_location = */ app_location_t::RX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_tetrarx*/ {'P', 'T', 'E', 'T'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} diff --git a/firmware/application/external/tetra_rx/tetra_bits.hpp b/firmware/application/external/tetra_rx/tetra_bits.hpp new file mode 100644 index 000000000..00d8cfa8e --- /dev/null +++ b/firmware/application/external/tetra_rx/tetra_bits.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +namespace ui::external_app::tetra_rx { + +class BitVector { + public: + BitVector(uint8_t* p, size_t bits) + : data_(p), + bits_(bits) { + } + + inline bool get(size_t bit) const { + return (data_[bit >> 3] >> + (7 - (bit & 7))) & + 1; + } + + inline void set(size_t bit, bool value) { + if (value) + data_[bit >> 3] |= + 1 << (7 - (bit & 7)); + else + data_[bit >> 3] &= + ~(1 << (7 - (bit & 7))); + } + + inline void xor_bit(size_t bit, bool value) { + if (value) + data_[bit >> 3] ^= + 1 << (7 - (bit & 7)); + } + + inline size_t size() const { + return bits_; + } + + private: + uint8_t* data_; + size_t bits_; +}; + +} // namespace ui::external_app::tetra_rx diff --git a/firmware/application/external/tetra_rx/tetra_crc.cpp b/firmware/application/external/tetra_rx/tetra_crc.cpp new file mode 100644 index 000000000..2b0e7ac88 --- /dev/null +++ b/firmware/application/external/tetra_rx/tetra_crc.cpp @@ -0,0 +1,33 @@ +#include "tetra_crc.hpp" + +namespace ui::external_app::tetra_rx { + +uint16_t CRC::crc16_itut_bits( + const uint8_t* bits, + uint32_t n_bits, + uint16_t init) { + uint16_t crc = init; + + for (uint32_t i = 0; i < n_bits; i++) { + uint8_t bit = + (bits[i >> 3] >> + (7 - (i & 7))) & + 1; + + crc ^= uint16_t(bit) << 15; + + if (crc & 0x8000) { + crc = + (crc << 1) ^ + 0x1021; + } else { + crc <<= 1; + } + + crc &= 0xffff; + } + + return crc; +} + +} // namespace ui::external_app::tetra_rx diff --git a/firmware/application/external/tetra_rx/tetra_crc.hpp b/firmware/application/external/tetra_rx/tetra_crc.hpp new file mode 100644 index 000000000..eedb21044 --- /dev/null +++ b/firmware/application/external/tetra_rx/tetra_crc.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace ui::external_app::tetra_rx { + +class CRC { + public: + static constexpr uint16_t CRC_OK = 0x1d0f; + + static uint16_t crc16_itut_bits( + const uint8_t* bits, + uint32_t n_bits, + uint16_t init = 0xffff); +}; + +} // namespace ui::external_app::tetra_rx diff --git a/firmware/application/external/tetra_rx/tetra_descrambler.cpp b/firmware/application/external/tetra_rx/tetra_descrambler.cpp new file mode 100644 index 000000000..045b1146e --- /dev/null +++ b/firmware/application/external/tetra_rx/tetra_descrambler.cpp @@ -0,0 +1,60 @@ +#include "tetra_descrambler.hpp" + +namespace ui::external_app::tetra_rx { + +static inline uint8_t lfsr_step(uint32_t& s) { + uint8_t b = 0; + + b ^= (s >> (32 - 32)) & 1; + b ^= (s >> (32 - 26)) & 1; + b ^= (s >> (32 - 23)) & 1; + b ^= (s >> (32 - 22)) & 1; + b ^= (s >> (32 - 16)) & 1; + b ^= (s >> (32 - 12)) & 1; + b ^= (s >> (32 - 11)) & 1; + b ^= (s >> (32 - 10)) & 1; + b ^= (s >> (32 - 8)) & 1; + b ^= (s >> (32 - 7)) & 1; + b ^= (s >> (32 - 5)) & 1; + b ^= (s >> (32 - 4)) & 1; + b ^= (s >> (32 - 2)) & 1; + b ^= (s >> (32 - 1)) & 1; + + s = (s >> 1) | (uint32_t(b) << 31); + + return b; +} + +void Descrambler::generate( + uint32_t init, + uint8_t* seq, + size_t bits) { + uint32_t s = init; + + for (size_t i = 0; i < bits; i++) + seq[i] = lfsr_step(s); +} + +void Descrambler::descramble( + BitVector& bits, + uint32_t init) { + uint32_t s = init; + + for (size_t i = 0; i < bits.size(); i++) + bits.xor_bit(i, lfsr_step(s)); +} + +uint32_t Descrambler::dmo_scramb_init( + uint32_t mni, + uint32_t src) { + uint32_t init = + (src & 0xFFFFFF) | + ((mni & 0x3F) << 24); + + init <<= 2; + init |= 3; + + return init; +} + +} // namespace ui::external_app::tetra_rx diff --git a/firmware/application/external/tetra_rx/tetra_descrambler.hpp b/firmware/application/external/tetra_rx/tetra_descrambler.hpp new file mode 100644 index 000000000..2f96a37c8 --- /dev/null +++ b/firmware/application/external/tetra_rx/tetra_descrambler.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include "tetra_bits.hpp" + +namespace ui::external_app::tetra_rx { + +class Descrambler { + public: + static constexpr uint32_t SCRAMB_INIT_BSCH = 0x00000003; + + static void descramble( + BitVector& bits, + uint32_t init = SCRAMB_INIT_BSCH); + + static void generate( + uint32_t init, + uint8_t* sequence, + size_t bits); + + static uint32_t dmo_scramb_init( + uint32_t mni, + uint32_t src); +}; + +} // namespace ui::external_app::tetra_rx diff --git a/firmware/application/external/tetra_rx/tetra_interleave.cpp b/firmware/application/external/tetra_rx/tetra_interleave.cpp new file mode 100644 index 000000000..c37afaa53 --- /dev/null +++ b/firmware/application/external/tetra_rx/tetra_interleave.cpp @@ -0,0 +1,21 @@ +#include "tetra_interleave.hpp" + +namespace ui::external_app::tetra_rx { + +void Interleave::block_deinterleave( + BitVector& in, + BitVector& out, + uint32_t K, + uint32_t a) { + for (uint32_t i = 1; i <= K; i++) { + uint32_t k = + 1 + + ((a * i) % K); + + out.set( + i - 1, + in.get(k - 1)); + } +} + +} // namespace ui::external_app::tetra_rx diff --git a/firmware/application/external/tetra_rx/tetra_interleave.hpp b/firmware/application/external/tetra_rx/tetra_interleave.hpp new file mode 100644 index 000000000..57dc25349 --- /dev/null +++ b/firmware/application/external/tetra_rx/tetra_interleave.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "tetra_bits.hpp" + +namespace ui::external_app::tetra_rx { + +class Interleave { + public: + static void block_deinterleave( + BitVector& in, + BitVector& out, + uint32_t K, + uint32_t a); +}; + +} // namespace ui::external_app::tetra_rx diff --git a/firmware/application/external/tetra_rx/tetra_rcpc.cpp b/firmware/application/external/tetra_rx/tetra_rcpc.cpp new file mode 100644 index 000000000..1bbc116d5 --- /dev/null +++ b/firmware/application/external/tetra_rx/tetra_rcpc.cpp @@ -0,0 +1,24 @@ +#include "tetra_rcpc.hpp" +#include + +namespace ui::external_app::tetra_rx { + +static constexpr uint8_t P_RATE_2_3[3] = {1, 2, 5}; + +static constexpr uint32_t T_RATE_2_3 = 3; +static constexpr uint32_t PERIOD = 8; + +void RCPC::depuncture_2_3(BitVector& in, uint8_t* out, uint32_t in_bits) { + const uint32_t mother_bits = ((in_bits + T_RATE_2_3 - 1) / T_RATE_2_3) * PERIOD + PERIOD; + memset(out, ERASED, mother_bits); + for (uint32_t j = 1; j <= in_bits; j++) { + uint32_t i = j; + uint32_t k = PERIOD * ((i - 1) / T_RATE_2_3); + k += P_RATE_2_3[(i - 1) % T_RATE_2_3]; + if (k >= 1 && k <= mother_bits) { + out[k - 1] = in.get(j - 1); + } + } +} + +} // namespace ui::external_app::tetra_rx \ No newline at end of file diff --git a/firmware/application/external/tetra_rx/tetra_rcpc.hpp b/firmware/application/external/tetra_rx/tetra_rcpc.hpp new file mode 100644 index 000000000..5ec8a9a85 --- /dev/null +++ b/firmware/application/external/tetra_rx/tetra_rcpc.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include "tetra_bits.hpp" + +namespace ui::external_app::tetra_rx { + +class RCPC { + public: + static constexpr uint8_t ERASED = 0xff; + + static void depuncture_2_3( + BitVector& in, + uint8_t* out, + uint32_t in_bits); +}; + +} // namespace ui::external_app::tetra_rx diff --git a/firmware/application/external/tetra_rx/tetra_viterbi.cpp b/firmware/application/external/tetra_rx/tetra_viterbi.cpp new file mode 100644 index 000000000..539aa2e17 --- /dev/null +++ b/firmware/application/external/tetra_rx/tetra_viterbi.cpp @@ -0,0 +1,73 @@ +#include "tetra_viterbi.hpp" +#include + +namespace ui::external_app::tetra_rx { + +int Viterbi::decode_cch( + const uint8_t* mother, + uint32_t n_info, + uint8_t* bits, + uint8_t* trace_buffer) { // Puffer by caller + + // Store these on stack, to avoid flash space usage + const uint8_t next_state[16][2] = { + {0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}, {10, 11}, {12, 13}, {14, 15}, {0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}, {10, 11}, {12, 13}, {14, 15}}; + const uint8_t next_output[16][2] = { + {0, 15}, {11, 4}, {6, 9}, {13, 2}, {5, 10}, {14, 1}, {3, 12}, {8, 7}, {15, 0}, {4, 11}, {9, 6}, {2, 13}, {10, 5}, {1, 14}, {12, 3}, {7, 8}}; + + const uint16_t INF = 0x3fff; + uint16_t cost[16]; + uint16_t new_cost[16]; + + // Initialization + for (int i = 0; i < 16; i++) cost[i] = INF; + cost[0] = 0; + + for (uint32_t step = 0; step < n_info; step++) { + for (int i = 0; i < 16; i++) new_cost[i] = INF; + const uint8_t* rx = &mother[step * 4]; + + for (int prev = 0; prev < 16; prev++) { + if (cost[prev] >= INF) continue; + + for (int bit = 0; bit < 2; bit++) { + const uint8_t pattern = next_output[prev][bit]; + const uint8_t next = next_state[prev][bit]; + + uint16_t d = 0; + for (int b = 0; b < 4; b++) { + if (rx[b] != 0xff && rx[b] != ((pattern >> (3 - b)) & 1)) d++; + } + + const uint16_t total = cost[prev] + d; + if (total < new_cost[next]) { + new_cost[next] = total; + trace_buffer[step * 16 + next] = (prev << 1) | bit; + } + } + } + memcpy(cost, new_cost, sizeof(cost)); + } + + // Traceback + uint8_t best_state = 0; + for (int i = 1; i < 16; i++) { + if (cost[i] < cost[best_state]) best_state = i; + } + + uint8_t state = best_state; + memset(bits, 0, (n_info + 7) >> 3); + + for (int step = (int)n_info - 1; step >= 0; step--) { + uint8_t packed = trace_buffer[step * 16 + state]; + uint8_t prev = (packed >> 1) & 0x0F; + uint8_t bit = packed & 0x01; + + if (bit) bits[step >> 3] |= (1 << (7 - (step & 7))); + state = prev; + } + + return cost[best_state]; +} + +} // namespace ui::external_app::tetra_rx \ No newline at end of file diff --git a/firmware/application/external/tetra_rx/tetra_viterbi.hpp b/firmware/application/external/tetra_rx/tetra_viterbi.hpp new file mode 100644 index 000000000..dda704d33 --- /dev/null +++ b/firmware/application/external/tetra_rx/tetra_viterbi.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +namespace ui::external_app::tetra_rx { + +class Viterbi { + public: + int decode_cch( + const uint8_t* mother, + uint32_t n_info, + uint8_t* bits, + uint8_t* trace_buffer); +}; + +} // namespace ui::external_app::tetra_rx \ No newline at end of file diff --git a/firmware/application/external/tetra_rx/ui_tetra_rx.cpp b/firmware/application/external/tetra_rx/ui_tetra_rx.cpp new file mode 100644 index 000000000..6a02d4b70 --- /dev/null +++ b/firmware/application/external/tetra_rx/ui_tetra_rx.cpp @@ -0,0 +1,354 @@ +#include "ui_tetra_rx.hpp" +#include "rtc_time.hpp" +#include "baseband_api.hpp" +#include "string_format.hpp" +#include "portapack_persistent_memory.hpp" +#include "file_path.hpp" +#include "tetra_crc.hpp" +#include "tetra_descrambler.hpp" +#include "tetra_interleave.hpp" +#include "tetra_rcpc.hpp" +#include "tetra_viterbi.hpp" +#include + +using namespace portapack; +using namespace ui; + +namespace ui::external_app::tetra_rx { + +template +bool TetraChannelDecoder::decode_block( + const std::array& burst, + size_t offset, + size_t len_t5, + size_t type1_bits, + size_t type2_bits, + size_t interleave_a, + std::array& out, + uint16_t& crc, + int& cost, + uint32_t scramb_init) { + std::array t5{}; + std::array t3{}; + std::array mother{}; + std::array type2{}; + + out.fill(0); + + for (size_t i = 0; i < len_t5; i++) { + set_bit(t5, i, get_bit(burst, offset + i)); + } + + BitVector t5_bits{t5.data(), len_t5}; + Descrambler::descramble(t5_bits, scramb_init); + + BitVector t3_bits{t3.data(), len_t5}; + Interleave::block_deinterleave(t5_bits, t3_bits, len_t5, interleave_a); + + RCPC::depuncture_2_3(t3_bits, mother.data(), len_t5); + Viterbi viterbi; + cost = viterbi.decode_cch(mother.data(), type2_bits, type2.data(), trace_buffer); + crc = CRC::crc16_itut_bits(type2.data(), type1_bits + 16); + for (size_t i = 0; i < type1_bits; i++) { + set_bit(out, i, (type2[i >> 3] >> (7 - (i & 7))) & 1); + } + return crc == CRC::CRC_OK; +} + +TetraChannelDecoder::Result TetraChannelDecoder::decode_dnb(const TetraDnbMessage& message) { + Result result{}; + result.inverted = message.inverted; + + if (!network_synced) + return result; + + std::array buffer{}; + std::copy(message.payload.begin(), message.payload.end(), buffer.begin()); + + uint32_t tmo_dnb_seed = (((uint32_t)last_mcc << 20) | ((uint32_t)last_mnc << 6) | (uint32_t)last_bcc) << 2 | 3; + // 1. DNB Full slot (SCH/F) + bool ok = decode_block(buffer, 0, SCH_F_LEN_BITS, SCH_F_TYPE1_BITS, SCH_F_TYPE2_BITS, + SCH_F_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed); + + // 2. SCH/HD Block 1 + if (!ok) { + ok = decode_block(buffer, 0, BLK2_LEN_BITS, SB2_TYPE1_BITS, SB2_TYPE2_BITS, + SB2_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed); + } + + // 3. SCH/HD Block 2 + if (!ok) { + ok = decode_block(buffer, 216, BLK2_LEN_BITS, SB2_TYPE1_BITS, SB2_TYPE2_BITS, + SB2_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed); + } + + if (ok) { + result.type = Result::Type::Dnb; + result.is_ok = true; + parse_mac_pdu(result); + } + + return result; +} + +TetraChannelDecoder::Result TetraChannelDecoder::decode_burst(const TetraBurstMessage& message) { + Result result{}; + result.sync_errors = message.sync_errors; + result.inverted = message.inverted; + + // 1. SCH/S + bool ok = decode_block( + message.payload, BLK1_OFFSET_BITS, BLK1_LEN_BITS, + SB1_TYPE1_BITS, SB1_TYPE2_BITS, SB1_INTERLEAVE_A, + result.payload, result.crc, result.cost, + Descrambler::SCRAMB_INIT_BSCH); + + if (!ok) return result; + + result.type = Result::Type::Sync; + result.is_ok = true; + parse_sync_pdu(result); + + uint16_t current_mcc = result.mcc; + uint16_t current_mnc = result.mnc; + uint8_t current_bcc = result.bcc; + uint8_t current_enc = result.encryption; + + // 2. SCH/H + uint16_t h_crc = 0; + int h_cost = 0; + + ok = decode_block( + message.payload, TMO_BLK2_OFFSET_BITS, BLK2_LEN_BITS, + SB2_TYPE1_BITS, SB2_TYPE2_BITS, SB2_INTERLEAVE_A, + result.payload, h_crc, h_cost, + Descrambler::SCRAMB_INIT_BSCH); + + if (ok) { + result.type = Result::Type::SyncFull; + result.crc = h_crc; + result.cost = h_cost; + + result.mcc = current_mcc; + result.mnc = current_mnc; + result.bcc = current_bcc; + result.encryption = current_enc; + + parse_mac_pdu(result); + } else { + result.encryption = current_enc; + } + + return result; +} + +uint32_t TetraChannelDecoder::read_bits(const uint8_t* bytes, size_t bit, size_t count) const { + uint32_t value = 0; + for (size_t i = 0; i < count; i++) { + value <<= 1; + value |= (bytes[(bit + i) >> 3] >> (7 - ((bit + i) & 7))) & 1; + } + return value; +} + +void TetraChannelDecoder::parse_sync_pdu(Result& result) { + // Sys 4 bit, then BCC + result.bcc = read_bits(result.payload.data(), 4, 6); + result.timeslot = read_bits(result.payload.data(), 10, 2); + result.frame_number = read_bits(result.payload.data(), 12, 5); + result.encryption = read_bits(result.payload.data(), 30, 1); + result.mcc = read_bits(result.payload.data(), 31, 10); + result.mnc = read_bits(result.payload.data(), 41, 14); + + last_mcc = result.mcc; + last_mnc = result.mnc; + last_bcc = result.bcc; + network_synced = true; +} + +void TetraChannelDecoder::parse_mac_pdu(Result& result) { + result.pdu_type = read_bits(result.payload.data(), 0, 2); + + if (result.pdu_type == 0) { // MAC-RESOURCE + result.encryption = read_bits(result.payload.data(), 4, 2); + uint8_t length_ind = read_bits(result.payload.data(), 7, 6); + + if (result.encryption == 0 && length_ind > 0 && length_ind < 62) { + size_t offset = 13; + uint8_t addr_type = read_bits(result.payload.data(), offset, 3); + offset += 3; + + if (addr_type == 1) { + result.calling_ssi = read_bits(result.payload.data(), offset, 24); + offset += 24; + } else if (addr_type == 2) { + offset += 10; + } else if (addr_type == 3 || addr_type == 4) { + result.calling_ssi = read_bits(result.payload.data(), offset, 24); + offset += 24; + } else if (addr_type == 5) { + offset += 34; + } else if (addr_type == 6) { + offset += 30; + } else if (addr_type == 7) { + offset += 34; + } + + // skip optional fields + if (read_bits(result.payload.data(), offset++, 1)) offset += 4; // Power control + if (read_bits(result.payload.data(), offset++, 1)) offset += 8; // Slot grant + + uint8_t chan_alloc = read_bits(result.payload.data(), offset++, 1); + + if (chan_alloc == 0) { + uint8_t llc_type = read_bits(result.payload.data(), offset, 4); + offset += 4; + + // LLC header skipped + if (llc_type == 0 || llc_type == 1) + offset += 2; + else if (llc_type == 2 || llc_type == 3 || llc_type == 6 || llc_type == 7) + offset += 1; + + uint8_t mle_type = read_bits(result.payload.data(), offset, 3); + offset += 3; + + // 1 = CMCE + if (mle_type == 1) { + result.cmce_type = read_bits(result.payload.data(), offset, 5); + offset += 5; + result.call_id = read_bits(result.payload.data(), offset, 14); + } + } + } + } else if (result.pdu_type == 2) { // SYSINFO/BCAST + uint8_t bcast_type = read_bits(result.payload.data(), 2, 2); + if (bcast_type == 0) { + result.la = read_bits(result.payload.data(), 82, 14); + result.encryption = read_bits(result.payload.data(), 122, 1); + } + } +} + +TetraRxView::TetraRxView(NavigationView& nav) + : nav_{nav} { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + + add_children({&rssi, &channel, &field_rf_amp, &field_lna, &field_vga, + &field_frequency, &text_mcc, &text_la, &text_pdu, + &text_mnc, &text_ts, &text_fn, &text_bcc, &text_enc, &text_debug, &console}); + + field_frequency.set_step(25000); + + receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio); + receiver_model.set_sampling_rate(3072000); + receiver_model.set_baseband_bandwidth(1750000); + receiver_model.set_squelch_level(0); + receiver_model.enable(); +} + +void TetraRxView::focus() { + field_frequency.focus(); +} + +void TetraRxView::on_data_tetra(const TetraBurstMessage& message) { + packet_count++; + auto result = decoder.decode_burst(message); + + if (result.type == TetraChannelDecoder::Result::Type::Sync || + result.type == TetraChannelDecoder::Result::Type::SyncFull) { + valid_count++; + + text_mcc.set("MCC: " + to_string_dec_uint(result.mcc)); + text_mnc.set("MNC: " + to_string_dec_uint(result.mnc)); + text_bcc.set("BCC: " + to_string_dec_uint(result.bcc)); + text_ts.set("TS: " + to_string_dec_uint(result.timeslot)); + text_fn.set("FN: " + to_string_dec_uint(result.frame_number)); + + std::string enc_str = (result.encryption != 0) ? "Encrypted" : "Clear"; + text_enc.set("ENC: " + enc_str); + + if (result.type == TetraChannelDecoder::Result::Type::SyncFull) { + h_valid_count++; + if (result.la != 0xffff) text_la.set("LA: " + to_string_dec_uint(result.la)); + text_pdu.set("PDU: " + decoder.get_pdu_name(result.pdu_type)); + } + } + text_debug.set( + "Syn: " + to_string_dec_uint(packet_count) + + ", V: " + to_string_dec_uint(valid_count) + + ", H: " + to_string_dec_uint(h_valid_count) + + ", E:" + to_string_dec_uint(result.sync_errors)); +} + +void TetraRxView::on_data_dnb(const TetraDnbMessage& message) { + dnb_count++; + auto result = decoder.decode_dnb(message); + + if (result.is_ok && result.type == TetraChannelDecoder::Result::Type::Dnb) { + if (result.la != 0xffff) text_la.set("LA: " + to_string_dec_uint(result.la)); + text_pdu.set("PDU: " + decoder.get_pdu_name(result.pdu_type)); + + std::string enc_str = (result.encryption != 0) ? "Encrypted" : "Clear"; + text_enc.set("ENC: " + enc_str); + + // if (rate_limiter++ < 2) console.writeln("DNB: " + decoder.get_pdu_name(result.pdu_type)); + if (result.cmce_type != 0xff) { + std::string msg = ""; + if (result.cmce_type == 7) + msg = "SETUP"; + else if (result.cmce_type == 1) + msg = "CALL PROCEEDING"; + else if (result.cmce_type == 0) + msg = "ALERTING"; + else if (result.cmce_type == 2) + msg = "CONNECT"; + else if (result.cmce_type == 3) + msg = "CONNECT ACK"; + else if (result.cmce_type == 11) + msg = "TX GRANTED"; + else if (result.cmce_type == 9) + msg = "TX CEASED"; + else if (result.cmce_type == 13) + msg = "TX WAIT"; + else if (result.cmce_type == 10) + msg = "TX CONTINUE"; + else if (result.cmce_type == 12) + msg = "TX INTERRUPT"; + else if (result.cmce_type == 4) + msg = "DISCONNECT"; + else if (result.cmce_type == 6) + msg = "RELEASE"; + else if (result.cmce_type == 5) + msg = "INFO"; + else if (result.cmce_type == 8) + msg = "STATUS"; + else if (result.cmce_type == 14) + msg = "SDS DATA"; + else if (result.cmce_type == 15) + msg = "FACILITY"; + else if (result.cmce_type == 16) + msg = "CALL RESTORE"; + else if (result.cmce_type == 31) + msg = "NOT SUPPORTED"; + else + msg = "CMCE:" + to_string_dec_uint(result.cmce_type); + + console.writeln(msg); + if (result.call_id != 0xffff) { + console.writeln("ID:" + to_string_dec_uint(result.call_id) + " | SSI:" + to_string_dec_uint(result.calling_ssi)); + } + } + } +} + +void TetraRxView::on_timer() { + rate_limiter = 0; +} + +TetraRxView::~TetraRxView() { + receiver_model.disable(); + baseband::shutdown(); +} + +} // namespace ui::external_app::tetra_rx \ No newline at end of file diff --git a/firmware/application/external/tetra_rx/ui_tetra_rx.hpp b/firmware/application/external/tetra_rx/ui_tetra_rx.hpp new file mode 100644 index 000000000..03503164f --- /dev/null +++ b/firmware/application/external/tetra_rx/ui_tetra_rx.hpp @@ -0,0 +1,192 @@ +#ifndef __UI_TETRA_RX_H__ +#define __UI_TETRA_RX_H__ + +#include "ui.hpp" +#include "ui_language.hpp" +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "ui_freq_field.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "log_file.hpp" +#include "utility.hpp" +#include "message.hpp" +#include +#include + +using namespace ui; + +namespace ui::external_app::tetra_rx { + +class TetraChannelDecoder { + public: + struct Result { + enum class Type { None, + Sync, + SyncFull, + Dnb }; + Type type{Type::None}; + + bool is_ok{false}; + uint8_t sync_errors{0}; + bool inverted{false}; + + uint16_t crc{0}; + int cost{0}; + + uint8_t timeslot{0xff}; + uint8_t frame_number{0xff}; + uint16_t mcc{0xffff}; + uint16_t mnc{0xffff}; + uint8_t bcc{0xff}; + + uint8_t pdu_type{0xff}; + uint16_t la{0xffff}; + uint8_t encryption{0xff}; + + uint8_t cmce_type{0xff}; + uint16_t call_id{0xffff}; + uint32_t calling_ssi{0}; + + std::array payload{}; + }; + + std::string get_pdu_name(uint8_t pdu_type) { + switch (pdu_type) { + case 0: + return "MAC-RESOURCE"; + case 1: + return "MAC-FRAG"; + case 2: + return "SYSINFO/BCAST"; + case 3: + return "MAC-U-SIGNAL"; + default: + return "UNK_" + std::to_string(pdu_type); + } + } + + Result decode_burst(const TetraBurstMessage& message); + Result decode_dnb(const TetraDnbMessage& message); + + private: + static constexpr size_t BLK1_OFFSET_BITS = 94; + static constexpr size_t BLK1_LEN_BITS = 120; + static constexpr size_t TMO_BLK2_OFFSET_BITS = 282; + static constexpr size_t BLK2_LEN_BITS = 216; + static constexpr size_t SB1_TYPE1_BITS = 60; + static constexpr size_t SB1_TYPE2_BITS = 80; + static constexpr size_t SB1_INTERLEAVE_A = 11; + static constexpr size_t SB2_TYPE1_BITS = 124; + static constexpr size_t SB2_TYPE2_BITS = 144; + static constexpr size_t SB2_INTERLEAVE_A = 101; + + static constexpr size_t SCH_F_LEN_BITS = 432; + static constexpr size_t SCH_F_TYPE1_BITS = 268; + static constexpr size_t SCH_F_TYPE2_BITS = 288; + static constexpr size_t SCH_F_INTERLEAVE_A = 103; + + uint16_t last_mcc{0}; + uint16_t last_mnc{0}; + uint8_t last_bcc{0}; + bool network_synced{false}; + + // Viterbi buffer + uint8_t trace_buffer[9216]; + + template + bool decode_block(const std::array& burst, + size_t offset, + size_t len_t5, + size_t type1_bits, + size_t type2_bits, + size_t interleave_a, + std::array& out, + uint16_t& crc, + int& cost, + uint32_t scramb_init); + + void parse_sync_pdu(Result& result); + void parse_mac_pdu(Result& result); + uint32_t read_bits(const uint8_t* bytes, size_t bit, size_t count) const; + + template + uint8_t get_bit(const std::array& arr, size_t bit_idx) const { + return (arr[bit_idx / 8] >> (7 - (bit_idx % 8))) & 0x01; + } + + template + void set_bit(std::array& arr, size_t bit_idx, uint8_t val) const { + if (val) + arr[bit_idx / 8] |= (1 << (7 - (bit_idx % 8))); + else + arr[bit_idx / 8] &= ~(1 << (7 - (bit_idx % 8))); + } +}; + +class TetraRxView : public View { + public: + TetraRxView(NavigationView& nav); + ~TetraRxView(); + void focus() override; + std::string title() const override { return "Tetra RX"; }; + + private: + NavigationView& nav_; + RxRadioState radio_state_{}; + app_settings::SettingsManager settings_{"rx_tetra", app_settings::Mode::RX}; + + uint32_t packet_count{0}; + uint32_t valid_count{0}; + uint32_t h_valid_count{0}; + uint32_t dnb_count{0}; + + RxFrequencyField field_frequency{{UI_POS_X(0), UI_POS_Y(0)}, nav_}; + RFAmpField field_rf_amp{{UI_POS_X(13), UI_POS_Y(0)}}; + LNAGainField field_lna{{UI_POS_X(15), UI_POS_Y(0)}}; + VGAGainField field_vga{{UI_POS_X(18), UI_POS_Y(0)}}; + RSSI rssi{{UI_POS_X_RIGHT(9), UI_POS_Y(0), UI_POS_WIDTH(9), 4}}; + Channel channel{{UI_POS_X_RIGHT(9), UI_POS_Y(0) + 5, UI_POS_WIDTH(9), 4}}; + + Text text_mcc{{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "MCC: ---"}; + Text text_mnc{{UI_POS_X(15), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "MNC: ---"}; + Text text_ts{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "TS: -"}; // time slot + Text text_fn{{UI_POS_X(15), UI_POS_Y(2), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "FN: -"}; // frame number + Text text_bcc{{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "BCC: -"}; // Base Station Color Code + Text text_enc{{UI_POS_X(15), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "ENC: -"}; // encoded or not + Text text_la{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "LA: ----"}; // Location Area + Text text_pdu{{UI_POS_X(15), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "PDU: ----"}; // pdu content type + + Text text_debug{{UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "Syn: 0, V: 0, H: 0, E:0"}; // sync, valid, h_valid, errors corrected in top part + Console console{{UI_POS_X(0), UI_POS_Y(6) + 8, UI_POS_MAXWIDTH, screen_height - (UI_POS_Y(7) + 8)}}; + + TetraChannelDecoder decoder{}; + + void on_data_tetra(const TetraBurstMessage&); + void on_data_dnb(const TetraDnbMessage&); + + uint8_t rate_limiter{0}; // limiter, to keep the device responsive + void on_timer(); + + MessageHandlerRegistration message_handler_frame_sync{ + Message::ID::DisplayFrameSync, + [this](const Message* const) { + this->on_timer(); + }}; + + MessageHandlerRegistration message_handler_packet{ + Message::ID::TetraBsch, + [this](Message* const p) { + this->on_data_tetra(*static_cast(p)); + }}; + + MessageHandlerRegistration message_handler_dnb{ + Message::ID::TetraDnb, + [this](Message* const p) { + this->on_data_dnb(*static_cast(p)); + }}; +}; + +} // namespace ui::external_app::tetra_rx + +#endif \ No newline at end of file diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt index 6fbdb1b25..6ea2595dd 100644 --- a/firmware/baseband/CMakeLists.txt +++ b/firmware/baseband/CMakeLists.txt @@ -785,6 +785,15 @@ set(MODE_CPPSRC ) DeclareTargets(PTSK time_sink) + +### Tetra Rx + +set(MODE_CPPSRC + proc_tetra.cpp +) +DeclareTargets(PTET tetra_rx) + + set(MODE_CPPSRC sd_over_usb/proc_sd_over_usb.cpp diff --git a/firmware/baseband/proc_tetra.cpp b/firmware/baseband/proc_tetra.cpp new file mode 100644 index 000000000..195b49845 --- /dev/null +++ b/firmware/baseband/proc_tetra.cpp @@ -0,0 +1,239 @@ +#include "proc_tetra.hpp" +#include "portapack_shared_memory.hpp" +#include "sine_table_int8.hpp" +#include "event_m4.hpp" + +#include + +TetraProcessor::TetraProcessor() { + decim_0.configure(taps_25k0_tetra_decim_0.taps); + decim_1.configure(taps_25k0_tetra_decim_1.taps); + + baseband_thread.start(); + configured = true; +} + +void TetraProcessor::execute(const buffer_c8_t& buffer) { + if (!configured) return; + + const auto decim_0_out = decim_0.execute(buffer, dst_buffer_0); + const auto channel_out = decim_1.execute(decim_0_out, dst_buffer_1); + + feed_channel_stats(channel_out); + + for (size_t i = 0; i < channel_out.count; i++) { + complex16_t sample = channel_out.p[i]; + + // Carrier PLL Rotation + uint8_t phase_idx = (pll_phase >> 24) & 0xFF; + uint8_t cos_idx = (phase_idx + 64) & 0xFF; + int16_t rotated_real = (sample.real() * sine_table_i8[cos_idx] - sample.imag() * sine_table_i8[phase_idx]) >> 7; + int16_t rotated_imag = (sample.real() * sine_table_i8[phase_idx] + sample.imag() * sine_table_i8[cos_idx]) >> 7; + complex16_t rotated_sample = {rotated_real, rotated_imag}; + + uint32_t old_phase = symbol_phase; + symbol_phase += symbol_phase_inc; + + // Half-symbol timing + if ((old_phase < 0x80000000) && (symbol_phase >= 0x80000000)) { + mid_sample = rotated_sample; + } + + // Full-symbol timing + if (symbol_phase < old_phase) { + prev_prompt = prompt_sample; + prompt_sample = rotated_sample; + + // Gardner Timing Error Detector + int32_t err_i = (mid_sample.real() * (prompt_sample.real() - prev_prompt.real())); + int32_t err_q = (mid_sample.imag() * (prompt_sample.imag() - prev_prompt.imag())); + int32_t timing_error = err_i + err_q; + + const int64_t delta = (timing_error >> 16); + int64_t inc = static_cast(symbol_phase_inc) + delta; + // CLAMPING: Prevent the NCO from drifting into the noise! + if (inc < 1600000000) inc = 1600000000; + if (inc > 1620000000) inc = 1620000000; + symbol_phase_inc = static_cast(inc); + + process_symbol(prompt_sample); + } + } +} + +void TetraProcessor::process_symbol(const complex16_t& current_sample) { + // Differential phase demodulation + int32_t dot_i = (current_sample.real() * delay_sample.real()) + (current_sample.imag() * delay_sample.imag()); + int32_t dot_q = (current_sample.imag() * delay_sample.real()) - (current_sample.real() * delay_sample.imag()); + delay_sample = current_sample; + + // Phase error for Carrier PLL + int32_t phase_err = 0; + if (dot_i > 0 && dot_q > 0) + phase_err = dot_q - dot_i; + else if (dot_i < 0 && dot_q > 0) + phase_err = dot_q + dot_i; + else if (dot_i < 0 && dot_q < 0) + phase_err = -dot_q + dot_i; + else + phase_err = -dot_q - dot_i; + + pll_freq += (phase_err * pll_beta) >> 8; + + // CLAMPING: Prevent frequency tracking from flying away + if (pll_freq > 400000000) pll_freq = 400000000; + if (pll_freq < -400000000) pll_freq = -400000000; + + pll_phase += ((phase_err * pll_alpha) >> 8) + pll_freq; + + // ETSI EN 300 392-2 pi/4 DQPSK bit mapping + uint8_t dibit = 0; + if (dot_i > 0 && dot_q > 0) + dibit = 0b00; + else if (dot_i < 0 && dot_q > 0) + dibit = 0b01; + else if (dot_i < 0 && dot_q < 0) + dibit = 0b11; + else + dibit = 0b10; + + // Insert bits into history buffer and sync register + for (int b = 1; b >= 0; b--) { + uint8_t bit_val = (dibit >> b) & 0x01; + + size_t byte_idx = (history_write_idx / 8) % 128; + if ((history_write_idx % 8) == 0) bit_history_buffer[byte_idx] = 0; + + bit_history_buffer[byte_idx] |= (bit_val << (7 - (history_write_idx % 8))); + history_write_idx = (history_write_idx + 1) % 1024; + bit_count++; + + sync_register = ((sync_register << 1) | bit_val); + + uint64_t sync = + sync_register & 0x3FFFFFFFFFULL; + + uint32_t err_pos = + __builtin_popcountll(sync ^ Y_SYNC); + + uint32_t err_neg = + __builtin_popcountll( + sync ^ + (~Y_SYNC & 0x3FFFFFFFFFULL)); + + if (!pending_dsb.valid && bit_count >= Y_SYNC_BITS && (err_pos <= 4 || err_neg <= 4)) { + const uint64_t sync_start = bit_count - Y_SYNC_BITS; + + if (sync_start >= SYNC_OFFSET) { + pending_dsb.valid = true; + pending_dsb.burst_start = sync_start - SYNC_OFFSET; + pending_dsb.ready_at = pending_dsb.burst_start + BURST_BITS; + pending_dsb.inverted = err_neg < err_pos; + pending_dsb.errors = std::min(err_pos, err_neg); + } + } + + if (ENABLE_DNB_MESSAGES) { + const uint32_t train_mask = (1UL << DNB_TRAIN_BITS) - 1; + const uint32_t train = sync_register & train_mask; + const uint32_t n_err_pos = __builtin_popcount(train ^ N_SYNC); + const uint32_t n_err_neg = __builtin_popcount(train ^ (~N_SYNC & train_mask)); + const uint32_t p_err_pos = __builtin_popcount(train ^ P_SYNC); + const uint32_t p_err_neg = __builtin_popcount(train ^ (~P_SYNC & train_mask)); + + if (!pending_dnb.valid && bit_count >= DNB_TRAIN_BITS && (n_err_pos <= 1 || n_err_neg <= 1 || p_err_pos <= 1 || p_err_neg <= 1)) { + const bool p_train = std::min(p_err_pos, p_err_neg) < std::min(n_err_pos, n_err_neg); + const uint32_t pos_err = p_train ? p_err_pos : n_err_pos; + const uint32_t neg_err = p_train ? p_err_neg : n_err_neg; + + const uint64_t train_start = bit_count - DNB_TRAIN_BITS; + const uint64_t block1_start = train_start - 230; + const uint64_t block2_start = train_start + 38; + + if (train_start >= 230) { + pending_dnb.valid = true; + pending_dnb.block1_start = block1_start; + pending_dnb.block2_start = block2_start; + pending_dnb.ready_at = block2_start + DNB_BLK_BITS; + pending_dnb.inverted = neg_err < pos_err; + pending_dnb.p_train = p_train; + pending_dnb.errors = std::min(pos_err, neg_err); + } + } + + if (pending_dnb.valid && bit_count >= pending_dnb.ready_at) + push_dnb_to_ui(); + } + + if (pending_dsb.valid && bit_count >= pending_dsb.ready_at) + push_burst_to_ui(); + } +} + +uint8_t TetraProcessor::history_bit(uint64_t absolute_bit) const { + const size_t p = absolute_bit % 1024; + + return (bit_history_buffer[p >> 3] >> + (7 - (p & 7))) & + 1; +} + +void TetraProcessor::push_burst_to_ui() { + std::array burst{}; + + for (size_t i = 0; i < BURST_BITS; i++) { + uint8_t b = history_bit(pending_dsb.burst_start + i); + + if (pending_dsb.inverted) + b ^= 1; + + burst[i >> 3] |= + b << (7 - (i & 7)); + } + + shared_memory.application_queue.push( + TetraBurstMessage( + burst.data(), + pending_dsb.inverted, + pending_dsb.errors)); + + pending_dsb.valid = false; +} + +void TetraProcessor::push_dnb_to_ui() { + std::array burst{}; + + for (size_t i = 0; i < DNB_BLK_BITS; i++) { + uint8_t b = history_bit(pending_dnb.block1_start + i); + if (pending_dnb.inverted) + b ^= 1; + burst[i >> 3] |= b << (7 - (i & 7)); + } + + for (size_t i = 0; i < DNB_BLK_BITS; i++) { + uint8_t b = history_bit(pending_dnb.block2_start + i); + if (pending_dnb.inverted) + b ^= 1; + const size_t out_bit = DNB_BLK_BITS + i; + burst[out_bit >> 3] |= b << (7 - (out_bit & 7)); + } + + shared_memory.application_queue.push( + TetraDnbMessage( + burst.data(), + pending_dnb.inverted, + pending_dnb.errors, + pending_dnb.p_train)); + + pending_dnb.valid = false; +} + +void TetraProcessor::on_message(const Message* const msg) { + (void)msg; +} + +int main() { + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} \ No newline at end of file diff --git a/firmware/baseband/proc_tetra.hpp b/firmware/baseband/proc_tetra.hpp new file mode 100644 index 000000000..fe89a36ac --- /dev/null +++ b/firmware/baseband/proc_tetra.hpp @@ -0,0 +1,94 @@ +#ifndef __PROC_TETRA_H__ +#define __PROC_TETRA_H__ + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "message.hpp" +#include "dsp_decimate.hpp" +#include "dsp_fir_taps.hpp" +#include "rssi_thread.hpp" + +#include +#include +#include + +class TetraProcessor : public BasebandProcessor { + public: + TetraProcessor(); + + void execute(const buffer_c8_t& buffer) override; + void on_message(const Message* const msg); + + private: + static constexpr size_t baseband_fs = 3072000; + static constexpr size_t channel_fs = 48000; + static constexpr uint32_t symbol_rate = 18000; + + std::array dst_0{}; + const buffer_c16_t dst_buffer_0{dst_0.data(), dst_0.size()}; + + std::array dst_1{}; + const buffer_c16_t dst_buffer_1{dst_1.data(), dst_1.size()}; + + dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{}; + dsp::decimate::FIRC16xR16x32Decim8 decim_1{}; + + // PLL and Timing + uint32_t symbol_phase{0}; + uint32_t symbol_phase_inc{(uint32_t)((symbol_rate * 4294967296.0) / channel_fs)}; + + complex16_t prompt_sample{0, 0}; + complex16_t prev_prompt{0, 0}; + complex16_t mid_sample{0, 0}; + complex16_t delay_sample{0, 0}; + static constexpr uint32_t BURST_BITS = 500; + static constexpr uint32_t Y_SYNC_BITS = 38; + static constexpr uint32_t SYNC_OFFSET = 214; + static constexpr uint64_t Y_SYNC = 0x30673A7067ULL; + static constexpr uint32_t DNB_TRAIN_BITS = 22; + static constexpr uint32_t DNB_BLK_BITS = 216; + static constexpr uint32_t DNB_TRAIN_TO_BLK2 = 22; + static constexpr uint32_t DNB_TOTAL_T5_BITS = DNB_BLK_BITS * 2; + static constexpr uint32_t N_SYNC = 0x343A74UL; + static constexpr uint32_t P_SYNC = 0x1E90DEUL; + static constexpr bool ENABLE_DNB_MESSAGES = true; + + uint32_t pll_phase{0}; + int32_t pll_freq{0}; + static constexpr int32_t pll_alpha = 150; + static constexpr int32_t pll_beta = 2; + + uint64_t sync_register{0}; + std::array bit_history_buffer{}; + size_t history_write_idx{0}; + uint64_t bit_count{0}; + bool configured{false}; + + struct PendingDnb { + bool valid{false}; + uint64_t block1_start{0}; + uint64_t block2_start{0}; + uint64_t ready_at{0}; + bool inverted{false}; + bool p_train{false}; + uint8_t errors{0}; + } pending_dnb{}; + + struct PendingDsb { + bool valid{false}; + uint64_t burst_start{0}; + uint64_t ready_at{0}; + bool inverted{false}; + uint8_t errors{0}; + } pending_dsb{}; + + void process_symbol(const complex16_t& sample); + uint8_t history_bit(uint64_t absolute_bit) const; + void push_burst_to_ui(); + void push_dnb_to_ui(); + + BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive, false}; + RSSIThread rssi_thread{}; +}; + +#endif // __PROC_TETRA_H__ \ No newline at end of file diff --git a/firmware/common/dsp_fir_taps.hpp b/firmware/common/dsp_fir_taps.hpp index 37ea6307b..1f32898f3 100644 --- a/firmware/common/dsp_fir_taps.hpp +++ b/firmware/common/dsp_fir_taps.hpp @@ -2070,4 +2070,20 @@ static constexpr fir_taps_complex<63> taps_1k5_LSB_channel = { }}, }; +// TETRA 25kHz filters +// IFIR wideband pre-filter: fs=3072000, pass=90000, decim=8, fout=384000 +constexpr fir_taps_real<24> taps_25k0_tetra_decim_0 = { + .low_frequency_normalized = -90000.0f / 3072000.0f, + .high_frequency_normalized = 90000.0f / 3072000.0f, + .transition_normalized = 160000.0f / 3072000.0f, + .taps = {{55, 122, 244, 424, 666, 965, 1308, 1669, 2019, 2321, 2544, 2663, 2663, 2544, 2321, 2019, 1669, 1308, 965, 666, 424, 244, 122, 55}}, +}; +// IFIR channel filter: fs=384000, pass=15000, decim=8, fout=48000 +constexpr fir_taps_real<32> taps_25k0_tetra_decim_1 = { + .low_frequency_normalized = -15000.0f / 384000.0f, + .high_frequency_normalized = 15000.0f / 384000.0f, + .transition_normalized = 25000.0f / 384000.0f, + .taps = {{2, -18, -60, -124, -198, -259, -267, -177, 53, 448, 1003, 1677, 2398, 3068, 3585, 3868, 3868, 3585, 3068, 2398, 1677, 1003, 448, 53, -177, -267, -259, -198, -124, -60, -18, 2}}, +}; + #endif /*__DSP_FIR_TAPS_H__*/ diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index 2c2382277..ca7c738a8 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -168,6 +168,8 @@ class Message { HunterConfig = 110, HunterTrigger = 111, HunterStop = 112, + TetraBsch = 113, + TetraDnb = 114, MAX }; @@ -2009,4 +2011,46 @@ class HunterStopMessage : public Message { : Message{ID::HunterStop} {} }; +struct TetraBurstMessage : public Message { + constexpr TetraBurstMessage( + const uint8_t* bits, + bool inv, + uint8_t err) + : Message(Message::ID::TetraBsch), + inverted(inv), + sync_errors(err), + payload{} { + for (size_t i = 0; i < 63; i++) + payload[i] = bits[i]; + } + + bool inverted; + uint8_t sync_errors; + + // 500 bit + std::array payload; +}; + +struct TetraDnbMessage : public Message { + constexpr TetraDnbMessage( + const uint8_t* bits, + bool inv, + uint8_t err, + bool p_train) + : Message(Message::ID::TetraDnb), + inverted(inv), + train_errors(err), + is_p_train(p_train), + payload{} { + for (size_t i = 0; i < 54; i++) + payload[i] = bits[i]; + } + + bool inverted; + uint8_t train_errors; + bool is_p_train; + + // 432 TCH type-5 bits: 216 bits before the training sequence + 216 bits after. + std::array payload; +}; #endif /*__MESSAGE_H__*/ diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp index ed6ecffe7..844af0657 100644 --- a/firmware/common/spi_image.hpp +++ b/firmware/common/spi_image.hpp @@ -134,6 +134,7 @@ constexpr image_tag_t image_tag_vor_rx{'P', 'V', 'R', 'X'}; constexpr image_tag_t image_tag_rttyrx{'P', 'R', 'T', 'R'}; constexpr image_tag_t image_tag_rttytx{'P', 'R', 'T', 'T'}; constexpr image_tag_t image_tag_tonedetect{'P', 'T', 'N', 'E'}; +constexpr image_tag_t image_tag_tetrarx{'P', 'T', 'E', 'T'}; constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'};