From 55a15958ecf37c16b69e2bfd738db910a64c6bca Mon Sep 17 00:00:00 2001 From: Sarah Rose <156587765+SarahRoseLives@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:41:01 -0400 Subject: [PATCH] Add P25 Phase 1 TSCC TX external app (p25_tx) (#3090) * Add P25 TX external app (UI shell) - Register p25_tx in external.cmake and external.ld at 0xAE010000 - UI with NAC, SYSID, WACN, RFSSID, SITEID, TG, VCH fields - 2.4 Msps TX, 155 MHz default, no baseband (m4_app_tag={0,0,0,0}) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add P25 TX baseband processor (M4) - P25TxConfigure = 102 in message.hpp + P25TxConfigureMessage - image_tag_p25_tx in spi_image.hpp - set_p25tx_data() in baseband_api (copies dibits to bb_data, sends msg) - proc_p25_tx registered in baseband CMakeLists (tag P25T) - m4_app_tag updated to {'P','2','5','T'} - TX wired: transmits P25 FSW test pattern (512 dibits) via M4 C4FM proc - on_tx_progress handler stops TX when frame complete Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * p25_tx: implement correct P25 TSBK encoding from ccemu - BCH(63,16) via generator matrix (not polynomial division) - CRC-CCITT bit-by-bit init=0 over 80 bits - 1/2-rate trellis encoder (4 states) - DataInterleave per TIA-102 - Status symbol insertion (SS=0x02 every 35 dibits) - TSBK rotation: IDEN_UP -> NET_STS -> RFSS_STS -> NET_STS - tsbk_idx_ persists across buffer refills, resets on stop_tx Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * p25_tx: apply clang-format Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * p25_tx: fix HackRF lockup - zero output buffer when idle When configured=false, execute() returned without writing to the output buffer. The DMA would re-use stale IQ samples from the previous cycle, sending garbage to the HackRF and causing it to lock up mid-transmission. Fix: explicitly zero the output buffer when the proc is idle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * p25_tx: address PR review feedback and fix TX restart lockup - Replace NumberField with SymField::Type::Hex for NAC, SYSID, WACN, RFSSID, SITEID, TG, VCH (hex values should display as hex) - Add voice grant TSBK (OpcodeGrpVChGrant 0x00) to rotation cycle; rotation is now 5-step: IDEN_UP->NET_STS->RFSS_STS->GRP_V_GRANT->NET_STS - Wire TG and VCH fields into fill_tx_buffer for voice grant encoding - Reset txprogress_message.done=false on new P25TxConfigure in baseband - Add #include to baseband_api.cpp (needed for memcpy) - Fix TX restart lockup: remove baseband::shutdown() from stop_tx() so the M4 stays loaded between sessions; only shut down in destructor. Previously, the stale ShutdownMessage in the queue would be picked up by the restarted M4 on second TX, causing it to exit immediately and leaving the HackRF stuck in TX mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ifndef guard instead of pragma --- firmware/application/baseband_api.cpp | 10 + firmware/application/baseband_api.hpp | 1 + firmware/application/external/external.cmake | 5 + firmware/application/external/external.ld | 7 + firmware/application/external/p25_tx/main.cpp | 83 ++++++ .../application/external/p25_tx/ui_p25_tx.cpp | 282 ++++++++++++++++++ .../application/external/p25_tx/ui_p25_tx.hpp | 96 ++++++ firmware/baseband/CMakeLists.txt | 7 + firmware/baseband/proc_p25_tx.cpp | 93 ++++++ firmware/baseband/proc_p25_tx.hpp | 55 ++++ firmware/common/message.hpp | 9 + firmware/common/spi_image.hpp | 1 + 12 files changed, 649 insertions(+) create mode 100644 firmware/application/external/p25_tx/main.cpp create mode 100644 firmware/application/external/p25_tx/ui_p25_tx.cpp create mode 100644 firmware/application/external/p25_tx/ui_p25_tx.hpp create mode 100644 firmware/baseband/proc_p25_tx.cpp create mode 100644 firmware/baseband/proc_p25_tx.hpp diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index c3ae6475f..42a172e7b 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -21,6 +21,7 @@ */ #include "baseband_api.hpp" +#include #include "audio.hpp" #include "tonesets.hpp" @@ -426,6 +427,15 @@ void set_epirb_tx_config(EPIRBTXDataMessage& message) { send_message(&message); } +void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length) { + const size_t max_len = sizeof(shared_memory.bb_data.data); + if (frame_length > max_len) frame_length = max_len; + memcpy(shared_memory.bb_data.data, dibits, frame_length); + P25TxConfigureMessage msg{}; + msg.frame_length = frame_length; + send_message(&msg); +} + static bool baseband_image_running = false; void run_image(const spi_flash::image_tag_t image_tag) { diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index 49c4f9c27..95a6b6913 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -117,6 +117,7 @@ void set_bitstream_config(uint32_t deviation, uint8_t mode); void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload = nullptr, uint16_t payload_length = 0); // baud*100 void set_rtty_config(RTTYDataMessage& message); void set_epirb_tx_config(EPIRBTXDataMessage& message); +void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length); void request_roger_beep(); void request_rssi_beep(); diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 14d46ea4c..dfa89efc4 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -336,6 +336,10 @@ set(EXTCPPSRC #fpv_detect external/fpv_detect/main.cpp external/fpv_detect/ui_fpv_detect.cpp + + #p25_tx + external/p25_tx/main.cpp + external/p25_tx/ui_p25_tx.cpp ) set(EXTAPPLIST @@ -420,6 +424,7 @@ set(EXTAPPLIST pocsag_tx time_sink kiss_tnc + p25_tx ) # 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 0ea98acf2..cd61633c5 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -105,6 +105,7 @@ MEMORY ram_external_app_mdc_tx (rwx) : org = 0xAE000000, len = 32k ram_external_app_epirb_tx (rwx) : org = 0xAE010000, len = 32k ram_external_app_fpv_detect (rwx) : org = 0xAE020000, len = 32k + ram_external_app_p25_tx (rwx) : org = 0xAE030000, len = 32k } SECTIONS @@ -600,4 +601,10 @@ SECTIONS KEEP(*(.external_app.app_fpv_detect.application_information)); *(*ui*external_app*fpv_detect*); } > ram_external_app_fpv_detect + + .external_app_p25_tx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_p25_tx.application_information)); + *(*ui*external_app*p25_tx*); + } > ram_external_app_p25_tx } diff --git a/firmware/application/external/p25_tx/main.cpp b/firmware/application/external/p25_tx/main.cpp new file mode 100644 index 000000000..a76e31275 --- /dev/null +++ b/firmware/application/external/p25_tx/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025 Sarah Rose + * + * 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.hpp" +#include "ui_p25_tx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::p25_tx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::p25_tx + +extern "C" { + +__attribute__((section(".external_app.app_p25_tx.application_information"), used)) +application_information_t _application_information_p25_tx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::p25_tx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "P25 TX", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0xFE, + 0x7F, + 0x02, + 0x40, + 0xFA, + 0x5F, + 0x0A, + 0x50, + 0x0A, + 0x50, + 0xFA, + 0x5F, + 0x02, + 0x40, + 0x02, + 0x40, + 0xE2, + 0x47, + 0x22, + 0x44, + 0xE2, + 0x47, + 0x02, + 0x40, + 0x02, + 0x40, + 0xFE, + 0x7F, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::blue().v, + /*.menu_location = */ app_location_t::TX, + /*.desired_menu_position = */ -1, + /*.m4_app_tag = */ {'P', '2', '5', 'T'}, + /*.m4_app_offset = */ 0x00000000, +}; +} diff --git a/firmware/application/external/p25_tx/ui_p25_tx.cpp b/firmware/application/external/p25_tx/ui_p25_tx.cpp new file mode 100644 index 000000000..7f78d3a20 --- /dev/null +++ b/firmware/application/external/p25_tx/ui_p25_tx.cpp @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2025 Sarah Rose + * + * 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_p25_tx.hpp" +#include "portapack.hpp" +#include "transmitter_model.hpp" +#include "baseband_api.hpp" +#include "ui_freq_field.hpp" +#include + +using namespace portapack; + +namespace ui::external_app::p25_tx { + +static constexpr uint8_t DUID_TSBK = 0x07; +static constexpr uint8_t SS_TSCC = 0x02; + +// BCH(63,16) generator matrix from TIA-102.BAAA-A (ccemu/p25/bch.go) +static const uint64_t bch_matrix[16] = { + 0x8000cd930bdd3b2aULL, + 0x4000ab5a8e33a6beULL, + 0x2000983e4cc4e874ULL, + 0x10004c1f2662743aULL, + 0x0800eb9c98ec0136ULL, + 0x0400b85d47ab3bb0ULL, + 0x02005c2ea3d59dd8ULL, + 0x01002e1751eaceecULL, + 0x0080170ba8f56776ULL, + 0x0040c616dfa78890ULL, + 0x0020630b6fd3c448ULL, + 0x00103185b7e9e224ULL, + 0x000818c2dbf4f112ULL, + 0x0004c1f2662743a2ULL, + 0x0002ad6a38ce9afbULL, + 0x00019b2617ba7657ULL, +}; + +static uint64_t bch_encode(uint16_t data) { + uint64_t cw = 0; + for (int i = 0; i < 16; i++) + if (data & (0x8000u >> i)) + cw ^= bch_matrix[i]; + return cw; +} + +// CRC-CCITT bit-by-bit, init=0, final XOR=0xFFFF (ccemu/p25/crc.go) +static uint16_t crc_ccitt_80(uint16_t high, uint64_t low) { + uint32_t crc = 0; + for (int i = 15; i >= 0; i--) { + crc <<= 1; + if (((crc >> 16) ^ ((high >> i) & 1)) & 1) crc ^= 0x1021; + } + for (int i = 63; i >= 0; i--) { + crc <<= 1; + if (((crc >> 16) ^ ((uint32_t)((low >> i) & 1))) & 1) crc ^= 0x1021; + } + return (uint16_t)((crc & 0xFFFF) ^ 0xFFFF); +} + +// 1/2-rate trellis: [state][inputDibit]={outDibit0,outDibit1} (ccemu/p25/trellis.go) +static const uint8_t trellis_table[4][4][2] = { + {{0, 2}, {3, 0}, {0, 1}, {3, 3}}, + {{3, 2}, {0, 0}, {3, 1}, {0, 3}}, + {{2, 1}, {1, 3}, {2, 2}, {1, 0}}, + {{1, 1}, {2, 3}, {1, 2}, {2, 0}}, +}; + +static void trellis_encode(const uint8_t* in48, uint8_t* out98) { + uint8_t state = 0; + for (int i = 0; i < 49; i++) { + uint8_t d = (i < 48) ? in48[i] : 0; + out98[i * 2 + 0] = trellis_table[state][d][0]; + out98[i * 2 + 1] = trellis_table[state][d][1]; + state = d; + } +} + +static void data_interleave(const uint8_t* in98, uint8_t* out98) { + int idx = 0; + for (int j = 0; j < 97; j += 8) { + out98[idx++] = in98[j]; + out98[idx++] = in98[j + 1]; + } + for (int i = 2; i < 7; i += 2) + for (int j = 0; j < 89; j += 8) { + out98[idx++] = in98[i + j]; + out98[idx++] = in98[i + j + 1]; + } +} + +static int insert_status(const uint8_t* data, int dlen, uint8_t* out, uint8_t ss) { + int num_ss = (dlen + 34) / 35, remaining = num_ss, oi = 0, i = 1; + for (int d = 0; d < dlen; d++) { + out[oi++] = data[d]; + if ((i % 35 == 0) && remaining > 0) { + out[oi++] = ss; + remaining--; + } + i++; + } + while (remaining > 0) { + out[oi++] = 0; + if (i % 35 == 0) { + out[oi++] = ss; + remaining--; + } + i++; + } + return oi; +} + +static void build_tsbk(uint8_t* out12, uint8_t opcode, uint64_t args) { + uint16_t high = (1u << 15) | ((uint16_t)opcode << 8); + uint16_t crc = crc_ccitt_80(high, args); + out12[0] = (uint8_t)(high >> 8); + out12[1] = (uint8_t)high; + for (int i = 0; i < 8; i++) out12[2 + i] = (uint8_t)(args >> (56 - 8 * i)); + out12[10] = (uint8_t)(crc >> 8); + out12[11] = (uint8_t)crc; +} + +static void tsbk_iden_up(uint8_t* out12, uint64_t freq_hz) { + uint64_t args = ((uint64_t)100 << 51) | ((uint64_t)100 << 32) | ((freq_hz / 5) & 0xFFFFFFFF); + build_tsbk(out12, 0x3D, args); +} +static void tsbk_net_status(uint8_t* out12, uint32_t wacn, uint16_t sysid) { + build_tsbk(out12, 0x3B, ((uint64_t)(wacn & 0xFFFFF) << 36) | ((uint64_t)(sysid & 0xFFF) << 24)); +} + +static void tsbk_grp_v_grant(uint8_t* out12, uint8_t chan_id, uint16_t chan_num, uint16_t tg) { + uint64_t ch = ((uint64_t)(chan_id & 0xF) << 12) | (chan_num & 0xFFF); + // opts: bit1=group + uint64_t args = ((uint64_t)0x02 << 56) | (ch << 40) | ((uint64_t)tg << 24); + build_tsbk(out12, 0x00, args); +} +static void tsbk_rfss_status(uint8_t* out12, uint16_t sysid, uint8_t rfssid, uint8_t siteid) { + build_tsbk(out12, 0x3A, ((uint64_t)(sysid & 0xFFF) << 40) | ((uint64_t)rfssid << 32) | ((uint64_t)siteid << 24)); +} + +static int build_frame(uint8_t* out, uint16_t nac, const uint8_t* tsbk12) { + uint8_t pre[160]; + int idx = 0; + static const uint64_t FSW = 0x5575F5FF77FFULL; + for (int i = 0; i < 24; i++) pre[idx++] = (uint8_t)((FSW >> (46 - i * 2)) & 3); + uint64_t nid = bch_encode((uint16_t)((nac << 4) | DUID_TSBK)); + for (int i = 0; i < 32; i++) pre[idx++] = (uint8_t)((nid >> (62 - i * 2)) & 3); + uint8_t in_d[48]; + for (int i = 0; i < 12; i++) { + in_d[i * 4 + 0] = (tsbk12[i] >> 6) & 3; + in_d[i * 4 + 1] = (tsbk12[i] >> 4) & 3; + in_d[i * 4 + 2] = (tsbk12[i] >> 2) & 3; + in_d[i * 4 + 3] = (tsbk12[i] >> 0) & 3; + } + uint8_t enc[98], ilv[98]; + trellis_encode(in_d, enc); + data_interleave(enc, ilv); + memcpy(&pre[idx], ilv, 98); + idx += 98; + return insert_status(pre, idx, out, SS_TSCC); +} + +static constexpr int TX_BUF_SIZE = 512; + +static int fill_tx_buffer(uint8_t* buf, uint16_t nac, uint32_t wacn, uint16_t sysid, uint8_t rfssid, uint8_t siteid, uint64_t freq_hz, uint16_t tg, uint16_t vch, int& tsbk_idx) { + memset(buf, 0, TX_BUF_SIZE); + int total = 0; + uint8_t tsbk[12]; + while (true) { + switch (tsbk_idx % 5) { + case 0: + tsbk_iden_up(tsbk, freq_hz); + break; + case 1: + tsbk_net_status(tsbk, wacn, sysid); + break; + case 2: + tsbk_rfss_status(tsbk, sysid, rfssid, siteid); + break; + case 3: + tsbk_grp_v_grant(tsbk, 0, vch, tg); + break; + default: + tsbk_net_status(tsbk, wacn, sysid); + break; + } + tsbk_idx++; + uint8_t probe[200]; + int flen = build_frame(probe, nac, tsbk); + if (total + flen > TX_BUF_SIZE) break; + memcpy(&buf[total], probe, flen); + total += flen; + } + return total; +} + +void P25TxView::start_tx() { + uint8_t dibits[TX_BUF_SIZE]; + int len = fill_tx_buffer(dibits, + (uint16_t)field_nac.to_integer(), (uint32_t)field_wacn.to_integer(), + (uint16_t)field_sysid.to_integer(), (uint8_t)field_rfssid.to_integer(), + (uint8_t)field_siteid.to_integer(), (uint64_t)transmitter_model.target_frequency(), + (uint16_t)field_tg.to_integer(), (uint16_t)field_vch.to_integer(), + tsbk_idx_); + transmitter_model.enable(); + tx_view.set_transmitting(true); + text_status.set("TX TSCC"); + transmitting = true; + baseband::set_p25tx_data(dibits, (uint16_t)len); +} + +void P25TxView::stop_tx() { + transmitting = false; + transmitter_model.disable(); + tx_view.set_transmitting(false); + text_status.set("Ready"); + tsbk_idx_ = 0; +} + +void P25TxView::on_tx_progress(const uint32_t, const bool done) { + if (done && transmitting) { + uint8_t dibits[TX_BUF_SIZE]; + int len = fill_tx_buffer(dibits, + (uint16_t)field_nac.to_integer(), (uint32_t)field_wacn.to_integer(), + (uint16_t)field_sysid.to_integer(), (uint8_t)field_rfssid.to_integer(), + (uint8_t)field_siteid.to_integer(), (uint64_t)transmitter_model.target_frequency(), + (uint16_t)field_tg.to_integer(), (uint16_t)field_vch.to_integer(), + tsbk_idx_); + baseband::set_p25tx_data(dibits, (uint16_t)len); + } +} + +P25TxView::P25TxView(NavigationView& nav) + : nav_{nav} { + add_children({&labels_, &field_nac, &field_sysid, &field_wacn, + &field_rfssid, &field_siteid, &field_tg, &field_vch, + &text_status, &tx_view}); + field_nac.set_value(0x293); + field_wacn.set_value(0xBEEF0); + field_sysid.set_value(0x001); + field_rfssid.set_value(1); + field_siteid.set_value(1); + field_tg.set_value(1); + field_vch.set_value(1); + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + tx_view.on_start = [this]() { start_tx(); }; + tx_view.on_stop = [this]() { stop_tx(); }; + tx_view.on_edit_frequency = [this, &nav]() { + auto new_view = nav.push(transmitter_model.target_frequency()); + new_view->on_changed = [this](rf::Frequency f) { transmitter_model.set_target_frequency(f); }; + }; +} + +P25TxView::~P25TxView() { + if (transmitting) + stop_tx(); + transmitter_model.disable(); + baseband::shutdown(); +} + +void P25TxView::focus() { + field_nac.focus(); +} + +} // namespace ui::external_app::p25_tx diff --git a/firmware/application/external/p25_tx/ui_p25_tx.hpp b/firmware/application/external/p25_tx/ui_p25_tx.hpp new file mode 100644 index 000000000..b54f31af1 --- /dev/null +++ b/firmware/application/external/p25_tx/ui_p25_tx.hpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2025 Sarah Rose + * + * 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 __UI_P25_TX +#define __UI_P25_TX + +#include "ui.hpp" +#include "ui_widget.hpp" +#include "ui_transmitter.hpp" +#include "ui_freq_field.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "message.hpp" + +namespace ui::external_app::p25_tx { + +class P25TxView : public View { + public: + explicit P25TxView(NavigationView& nav); + ~P25TxView(); + + void focus() override; + std::string title() const override { return "P25 TX"; } + + private: + void start_tx(); + void stop_tx(); + void on_tx_progress(const uint32_t progress, const bool done); + NavigationView& nav_; + + TxRadioState radio_state_{ + 155'000'000, + 12500, + 2'400'000}; + + app_settings::SettingsManager settings_{ + "tx_p25_tx", + app_settings::Mode::TX}; + + Labels labels_{{ + {{UI_POS_X(0), UI_POS_Y(0)}, "NAC:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(9), UI_POS_Y(0)}, "SYS:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(1)}, "WACN:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(2)}, "RFSS:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(9), UI_POS_Y(2)}, "SITE:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(3)}, "TG:", ui::Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(9), UI_POS_Y(3)}, "VCH:", ui::Theme::getInstance()->fg_light->foreground}, + }}; + + SymField field_nac{{UI_POS_X(4), UI_POS_Y(0)}, 3, SymField::Type::Hex}; + SymField field_sysid{{UI_POS_X(13), UI_POS_Y(0)}, 3, SymField::Type::Hex}; + SymField field_wacn{{UI_POS_X(5), UI_POS_Y(1)}, 5, SymField::Type::Hex}; + SymField field_rfssid{{UI_POS_X(5), UI_POS_Y(2)}, 2, SymField::Type::Hex}; + SymField field_siteid{{UI_POS_X(14), UI_POS_Y(2)}, 2, SymField::Type::Hex}; + SymField field_tg{{UI_POS_X(3), UI_POS_Y(3)}, 4, SymField::Type::Hex}; + SymField field_vch{{UI_POS_X(13), UI_POS_Y(3)}, 3, SymField::Type::Hex}; + + Text text_status{{0, UI_POS_Y(4), UI_POS_MAXWIDTH, UI_POS_DEFAULT_HEIGHT}, "Ready"}; + + TransmitterView tx_view{ + (int16_t)UI_POS_Y_BOTTOM(5), + 12500, + 0}; + + bool transmitting{false}; + int tsbk_idx_{0}; + + MessageHandlerRegistration message_handler_tx_progress{ + Message::ID::TXProgress, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->on_tx_progress(message.progress, message.done); + }}; +}; + +} // namespace ui::external_app::p25_tx + +#endif /* __UI_P25_TX */ diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt index 4ea0c4e69..38f499d4b 100644 --- a/firmware/baseband/CMakeLists.txt +++ b/firmware/baseband/CMakeLists.txt @@ -543,6 +543,13 @@ set(MODE_CPPSRC DeclareTargets(PEPT epirb_tx) +### P25 TX + +set(MODE_CPPSRC + proc_p25_tx.cpp +) +DeclareTargets(P25T p25_tx) + ### NRF RX diff --git a/firmware/baseband/proc_p25_tx.cpp b/firmware/baseband/proc_p25_tx.cpp new file mode 100644 index 000000000..0901b217b --- /dev/null +++ b/firmware/baseband/proc_p25_tx.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 PortaPack-MAYHEM Contributors + * + * 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 of the License, 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. If not, see . + */ + +#include "proc_p25_tx.hpp" +#include "portapack_shared_memory.hpp" +#include "sine_table_int8.hpp" +#include "event_m4.hpp" + +#include + +// Gray-coded dibit → FM deviation level: +// dibit 0 → +1 unit (+600 Hz) +// dibit 1 → +3 units (+1800 Hz) +// dibit 2 → -1 unit (-600 Hz) +// dibit 3 → -3 units (-1800 Hz) +static constexpr int8_t dibit_to_level[4] = {1, 3, -1, -3}; + +void P25TxProcessor::execute(const buffer_c8_t& buffer) { + if (!configured) { + // Zero output - never emit garbage while idle + for (size_t i = 0; i < buffer.count; i++) + buffer.p[i] = {0, 0}; + return; + } + + for (size_t i = 0; i < buffer.count; i++) { + if (sample_count == 0) { + if (dibit_index >= frame_length) { + // Frame complete - notify A7 and stop + txprogress_message.done = true; + shared_memory.application_queue.push(txprogress_message); + configured = false; + for (size_t j = i; j < buffer.count; j++) + buffer.p[j] = {0, 0}; + return; + } + } + + // Current dibit → frequency level + uint8_t dibit = shared_memory.bb_data.data[dibit_index] & 0x03; + int8_t level = dibit_to_level[dibit]; + + // Accumulate FM phase at ±level × base_phase_step per sample + phase += (uint32_t)((int32_t)level * (int32_t)base_phase_step); + + int8_t re = sine_table_i8[(phase + 0x40000000) >> 24]; // cos + int8_t im = sine_table_i8[phase >> 24]; // sin + buffer.p[i] = {re, im}; + + sample_count++; + if (sample_count >= P25_SAMPLES_PER_DIBIT) { + sample_count = 0; + dibit_index++; + } + } +} + +void P25TxProcessor::on_message(const Message* const msg) { + if (msg->id == Message::ID::P25TxConfigure) { + const auto& m = *reinterpret_cast(msg); + if (m.frame_length == 0) { + configured = false; + return; + } + frame_length = m.frame_length; + dibit_index = 0; + sample_count = 0; + txprogress_message.done = false; + configured = true; + } +} + +int main() { + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} diff --git a/firmware/baseband/proc_p25_tx.hpp b/firmware/baseband/proc_p25_tx.hpp new file mode 100644 index 000000000..8b777dc9e --- /dev/null +++ b/firmware/baseband/proc_p25_tx.hpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 PortaPack-MAYHEM Contributors + * + * 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 of the License, 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. If not, see . + */ + +#ifndef __PROC_P25_TX_H__ +#define __PROC_P25_TX_H__ + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "message.hpp" + +// P25 C4FM: 4800 baud, 2.4 Msps = 500 samples per dibit +#define P25_SAMPLERATE 2400000 +#define P25_SAMPLES_PER_DIBIT 500 + +class P25TxProcessor : public BasebandProcessor { + public: + void execute(const buffer_c8_t& buffer) override; + void on_message(const Message* const msg) override; + + private: + bool configured{false}; + uint16_t frame_length{0}; // total dibits in frame + uint16_t dibit_index{0}; // current dibit being transmitted + uint32_t sample_count{0}; // samples emitted for current dibit + + // FM phase accumulator (32-bit wrapping) + uint32_t phase{0}; + + // Phase step per sample per deviation unit (600 Hz at 2.4 Msps) + // = round(600 * 2^32 / 2400000) = 1073742 + static constexpr uint32_t base_phase_step{1073742}; + + TXProgressMessage txprogress_message{}; + + /* NB: Threads should be the last members in the class definition. */ + BasebandThread baseband_thread{P25_SAMPLERATE, this, baseband::Direction::Transmit}; +}; + +#endif /* __PROC_P25_TX_H__ */ diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index d6340af6f..475fcf585 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -157,6 +157,7 @@ class Message { NotificationData = 99, TimeSinkConfig = 100, EPIRBTXData = 101, + P25TxConfigure = 102, MAX }; @@ -1124,6 +1125,14 @@ class EPIRBTXDataMessage : public Message { uint32_t post_count = 0; }; +class P25TxConfigureMessage : public Message { + public: + constexpr P25TxConfigureMessage() + : Message{ID::P25TxConfigure} { + } + uint16_t frame_length{0}; +}; + class AFSKTxConfigureMessage : public Message { public: constexpr AFSKTxConfigureMessage( diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp index bab9d1099..6c9fde35e 100644 --- a/firmware/common/spi_image.hpp +++ b/firmware/common/spi_image.hpp @@ -89,6 +89,7 @@ constexpr image_tag_t image_tag_capture{'P', 'C', 'A', 'P'}; constexpr image_tag_t image_tag_ert{'P', 'E', 'R', 'T'}; constexpr image_tag_t image_tag_epirb_rx{'P', 'E', 'P', 'I'}; constexpr image_tag_t image_tag_epirb_tx{'P', 'E', 'P', 'T'}; +constexpr image_tag_t image_tag_p25_tx{'P', '2', '5', 'T'}; constexpr image_tag_t image_tag_nfm_audio{'P', 'N', 'F', 'M'}; constexpr image_tag_t image_tag_pocsag{'P', 'P', 'O', 'C'}; constexpr image_tag_t image_tag_pocsag2{'P', 'P', 'O', '2'};