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'};