From d55a420dfd5a7a29febd7173200c8a404537633d Mon Sep 17 00:00:00 2001 From: furrtek Date: Thu, 28 Apr 2016 14:59:14 +0200 Subject: [PATCH] Fixed module loading (again), only audio tx works for now --- firmware/Makefile | 12 +- firmware/application/Makefile | 1 + firmware/application/core_control.cpp | 53 ++ firmware/application/event_m0.cpp | 33 +- firmware/application/transmitter_model.cpp | 25 +- firmware/application/transmitter_model.hpp | 13 +- firmware/application/ui_about.cpp | 18 +- firmware/application/ui_about.hpp | 1 + firmware/application/ui_afsksetup.cpp | 3 +- firmware/application/ui_audiotx.cpp | 95 +++ .../ui_audiotx.hpp} | 72 ++- firmware/application/ui_debug.cpp | 56 +- firmware/application/ui_lcr.cpp | 22 +- firmware/application/ui_loadmodule.cpp | 24 +- firmware/application/ui_loadmodule.hpp | 2 +- firmware/application/ui_menu.cpp | 12 +- firmware/application/ui_menu.hpp | 1 + firmware/application/ui_navigation.cpp | 47 +- firmware/application/ui_navigation.hpp | 2 +- firmware/application/ui_rds.cpp | 3 +- firmware/application/ui_setup.cpp | 11 +- firmware/baseband-tx.bin | Bin 33296 -> 33280 bytes firmware/baseband-tx/Makefile | 9 +- firmware/baseband-tx/audio_dma.hpp | 15 +- firmware/baseband-tx/audio_output.cpp | 30 +- firmware/baseband-tx/audio_output.hpp | 12 +- .../baseband-tx/audio_stats_collector.cpp | 4 +- firmware/baseband-tx/baseband_dma.cpp | 41 +- firmware/baseband-tx/baseband_thread.cpp | 23 +- firmware/baseband-tx/baseband_thread.hpp | 12 +- firmware/baseband-tx/block_decimator.hpp | 6 +- firmware/baseband-tx/channel_decimator.cpp | 27 +- firmware/baseband-tx/channel_decimator.hpp | 24 +- .../baseband-tx/channel_stats_collector.hpp | 4 +- firmware/baseband-tx/chconf.h | 2 +- firmware/baseband-tx/clock_recovery.hpp | 19 +- firmware/baseband-tx/dsp_decimate.cpp | 557 +++++++++++++++--- firmware/baseband-tx/dsp_decimate.hpp | 276 +++++---- firmware/baseband-tx/dsp_demodulate.cpp | 96 ++- firmware/baseband-tx/dsp_demodulate.hpp | 53 +- firmware/baseband-tx/dsp_iir.hpp | 70 --- firmware/baseband-tx/dsp_iir_config.hpp | 37 -- firmware/baseband-tx/event_m4.cpp | 8 +- firmware/baseband-tx/halconf.h | 2 +- firmware/baseband-tx/main.cpp | 191 ------ firmware/baseband-tx/matched_filter.cpp | 18 + firmware/baseband-tx/matched_filter.hpp | 21 +- firmware/baseband-tx/packet_builder.hpp | 45 +- firmware/baseband-tx/proc_audiotx.cpp | 77 ++- firmware/baseband-tx/proc_audiotx.hpp | 14 +- firmware/baseband-tx/proc_playaudio.cpp | 15 +- firmware/baseband-tx/proc_playaudio.hpp | 2 +- firmware/baseband-tx/rssi_dma.cpp | 28 +- firmware/baseband-tx/rssi_stats_collector.hpp | 2 +- firmware/baseband-tx/rssi_thread.hpp | 5 - firmware/baseband-tx/spectrum_collector.cpp | 4 +- firmware/baseband-tx/spectrum_collector.hpp | 6 +- firmware/baseband-tx/thread_base.hpp | 11 +- firmware/baseband.bin | Bin 33296 -> 33280 bytes firmware/common/modules.h | 3 +- firmware/portapack-h1-firmware.bin | Bin 431436 -> 454488 bytes firmware/tools/make_baseband_file.py | 4 +- sdcard/baseband-tx.bin | Bin 33296 -> 33280 bytes sdcard/baseband.bin | Bin 33296 -> 33280 bytes 64 files changed, 1400 insertions(+), 879 deletions(-) create mode 100644 firmware/application/ui_audiotx.cpp rename firmware/{baseband-tx/dsp_iir.cpp => application/ui_audiotx.hpp} (51%) delete mode 100644 firmware/baseband-tx/dsp_iir.hpp delete mode 100644 firmware/baseband-tx/dsp_iir_config.hpp diff --git a/firmware/Makefile b/firmware/Makefile index 7e2c998e..18302e65 100644 --- a/firmware/Makefile +++ b/firmware/Makefile @@ -22,7 +22,7 @@ PATH_BOOTSTRAP=bootstrap PATH_APPLICATION=application PATH_BASEBAND=baseband -# PATH_BASEBAND_TX=baseband-tx +PATH_BASEBAND_TX=baseband-tx TARGET=portapack-h1-firmware @@ -30,7 +30,7 @@ TARGET_BOOTSTRAP=$(PATH_BOOTSTRAP)/bootstrap TARGET_HACKRF_FIRMWARE=hackrf_one_usb_ram TARGET_APPLICATION=$(PATH_APPLICATION)/build/application TARGET_BASEBAND=$(PATH_BASEBAND)/build/baseband -# TARGET_BASEBAND_TX=$(PATH_BASEBAND_TX)/build/baseband-tx +TARGET_BASEBAND_TX=$(PATH_BASEBAND_TX)/build/baseband-tx MAKE_SPI_IMAGE=tools/make_spi_image.py MAKE_MODULES_FILE=tools/make_baseband_file.py @@ -59,13 +59,13 @@ program: $(TARGET).bin modules sleep 1s hackrf_spiflash -w $(TARGET).bin -modules: $(TARGET_BASEBAND).bin # $(TARGET_BASEBAND_TX).bin +modules: $(TARGET_BASEBAND).bin $(TARGET_BASEBAND_TX).bin $(MAKE_MODULES_FILE) $(MODULES) cp $(PATH_BASEBAND).bin ../sdcard/$(PATH_BASEBAND).bin - # cp $(PATH_BASEBAND_TX).bin ../sdcard/$(PATH_BASEBAND_TX).bin + cp $(PATH_BASEBAND_TX).bin ../sdcard/$(PATH_BASEBAND_TX).bin -$(TARGET).bin: modules $(MAKE_SPI_IMAGE) $(TARGET_BOOTSTRAP).bin $(TARGET_HACKRF_FIRMWARE).dfu $(TARGET_BASEBAND).bin $(TARGET_APPLICATION).bin - $(MAKE_SPI_IMAGE) $(TARGET_BOOTSTRAP).bin $(TARGET_HACKRF_FIRMWARE).dfu $(TARGET_BASEBAND).bin $(TARGET_APPLICATION).bin $(TARGET).bin +$(TARGET).bin: modules $(MAKE_SPI_IMAGE) $(TARGET_BOOTSTRAP).bin $(TARGET_HACKRF_FIRMWARE).dfu $(TARGET_BASEBAND)_inc.bin $(TARGET_APPLICATION).bin + $(MAKE_SPI_IMAGE) $(TARGET_BOOTSTRAP).bin $(TARGET_HACKRF_FIRMWARE).dfu $(TARGET_BASEBAND)_inc.bin $(TARGET_APPLICATION).bin $(TARGET).bin $(TARGET_BOOTSTRAP).bin: $(TARGET_BOOTSTRAP).elf $(CP) -O binary $(TARGET_BOOTSTRAP).elf $(TARGET_BOOTSTRAP).bin diff --git a/firmware/application/Makefile b/firmware/application/Makefile index d74a662d..4aeff3f0 100755 --- a/firmware/application/Makefile +++ b/firmware/application/Makefile @@ -161,6 +161,7 @@ CPPSRC = main.cpp \ ui_rssi.cpp \ ui_channel.cpp \ ui_audio.cpp \ + ui_audiotx.cpp \ ui_lcr.cpp \ ui_rds.cpp \ ui_jammer.cpp \ diff --git a/firmware/application/core_control.cpp b/firmware/application/core_control.cpp index 6fdc8856..531cdb38 100644 --- a/firmware/application/core_control.cpp +++ b/firmware/application/core_control.cpp @@ -28,6 +28,7 @@ using namespace lpc43xx; #include "message.hpp" #include "baseband_api.hpp" +#include "portapack_shared_memory.hpp" #include @@ -63,3 +64,55 @@ void m0_halt() { port_wait_for_interrupt(); } } + +int m4_load_image(void) { + uint32_t mod_size; + UINT bw; + uint8_t i; + char md5sum[16]; + FILINFO modinfo; + FIL modfile; + DIR rootdir; + FRESULT res; + + // Scan SD card root directory for files with the right md5 fingerprint at the right location + f_opendir(&rootdir, "/"); + for (;;) { + res = f_readdir(&rootdir, &modinfo); + if (res != FR_OK || modinfo.fname[0] == 0) break; + if (!(modinfo.fattrib & AM_DIR)) { + f_open(&modfile, modinfo.fname, FA_OPEN_EXISTING | FA_READ); + f_lseek(&modfile, 26); + f_read(&modfile, &md5sum, 16, &bw); + for (i = 0; i < 16; i++) { + if (md5sum[i] != modhash[i]) break; + } + if (i == 16) { + f_lseek(&modfile, 6); + f_read(&modfile, &mod_size, 4, &bw); + f_lseek(&modfile, 256); + f_read(&modfile, reinterpret_cast(portapack::memory::map::m4_code.base()), mod_size, &bw); + LPC_RGU->RESET_CTRL[0] = (1 << 13); + f_close(&modfile); + return 1; + } + f_close(&modfile); + } + } + + return 0; +} + +void m4_switch(const char * hash) { + modhash = const_cast(hash); + + // Ask M4 to enter loop in RAM + BasebandConfiguration baseband_switch { + .mode = 0xFF, + .sampling_rate = 0, + .decimation_factor = 1, + }; + + BasebandConfigurationMessage message { baseband_switch }; + shared_memory.baseband_queue.push(message); +} diff --git a/firmware/application/event_m0.cpp b/firmware/application/event_m0.cpp index af12bee1..49021ada 100644 --- a/firmware/application/event_m0.cpp +++ b/firmware/application/event_m0.cpp @@ -22,7 +22,7 @@ #include "event_m0.hpp" #include "portapack.hpp" -#include "portapack_shared_memory.hpp" +#include "portapack_persistent_memory.hpp" #include "sd_card.hpp" @@ -99,6 +99,7 @@ void EventDispatcher::set_display_sleep(const bool sleep) { portapack::io.lcd_backlight(false); portapack::display.sleep(); } else { + portapack::bl_tick_counter = 0; portapack::display.wake(); portapack::io.lcd_backlight(true); } @@ -121,16 +122,16 @@ void EventDispatcher::dispatch(const eventmask_t events) { if( events & EVT_MASK_SWITCHES ) { handle_switches(); } + + if( events & EVT_MASK_ENCODER ) { + handle_encoder(); + } if( !display_sleep ) { if( events & EVT_MASK_LCD_FRAME_SYNC ) { handle_lcd_frame_sync(); } - if( events & EVT_MASK_ENCODER ) { - handle_encoder(); - } - if( events & EVT_MASK_TOUCH ) { handle_touch(); } @@ -144,9 +145,19 @@ void EventDispatcher::handle_application_queue() { } void EventDispatcher::handle_rtc_tick() { + uint16_t bloff; + sd_card::poll_inserted(); portapack::temperature_logger.second_tick(); + + bloff = portapack::persistent_memory::ui_config_bloff(); + if (bloff) { + if (portapack::bl_tick_counter == bloff) + set_display_sleep(true); + else + portapack::bl_tick_counter++; + } } ui::Widget* EventDispatcher::touch_widget(ui::Widget* const w, ui::TouchEvent event) { @@ -156,6 +167,7 @@ ui::Widget* EventDispatcher::touch_widget(ui::Widget* const w, ui::TouchEvent ev for(const auto child : w->children()) { const auto touched_widget = touch_widget(child, event); if( touched_widget ) { + portapack::bl_tick_counter = 0; return touched_widget; } } @@ -164,6 +176,7 @@ ui::Widget* EventDispatcher::touch_widget(ui::Widget* const w, ui::TouchEvent ev if( r.contains(event.point) ) { if( w->on_touch(event) ) { // This widget responded. Return it up the call stack. + portapack::bl_tick_counter = 0; return w; } } @@ -200,6 +213,8 @@ void EventDispatcher::handle_lcd_frame_sync() { void EventDispatcher::handle_switches() { const auto switches_state = get_switches_state(); + portapack::bl_tick_counter = 0; + if( display_sleep ) { // Swallow event, wake up display. if( switches_state.any() ) { @@ -220,6 +235,14 @@ void EventDispatcher::handle_switches() { } void EventDispatcher::handle_encoder() { + portapack::bl_tick_counter = 0; + + if( display_sleep ) { + // Swallow event, wake up display. + set_display_sleep(false); + return; + } + const uint32_t encoder_now = get_encoder_position(); const int32_t delta = static_cast(encoder_now - encoder_last); encoder_last = encoder_now; diff --git a/firmware/application/transmitter_model.cpp b/firmware/application/transmitter_model.cpp index f04002ed..7ed7f2e0 100644 --- a/firmware/application/transmitter_model.cpp +++ b/firmware/application/transmitter_model.cpp @@ -21,11 +21,14 @@ #include "transmitter_model.hpp" -#include "portapack_shared_memory.hpp" +#include "baseband_api.hpp" + #include "portapack_persistent_memory.hpp" -#include "portapack.hpp" using namespace portapack; +#include "radio.hpp" +#include "audio.hpp" + rf::Frequency TransmitterModel::tuning_frequency() const { return persistent_memory::tuned_frequency(); } @@ -86,17 +89,9 @@ void TransmitterModel::enable() { update_baseband_configuration(); } -void TransmitterModel::baseband_disable() { - shared_memory.baseband_queue.push_and_wait( - BasebandConfigurationMessage { - .configuration = { }, - } - ); -} - void TransmitterModel::disable() { enabled_ = false; - baseband_disable(); + baseband::stop(); // TODO: Responsibility for enabling/disabling the radio is muddy. // Some happens in ReceiverModel, some inside radio namespace. @@ -147,13 +142,11 @@ void TransmitterModel::update_baseband_configuration() { // protocols that need quick RX/TX turn-around. // Disabling baseband while changing sampling rates seems like a good idea... - baseband_disable(); + baseband::stop(); - clock_manager.set_sampling_frequency(sampling_rate() * baseband_oversampling()); + radio::set_baseband_rate(sampling_rate() * baseband_oversampling()); update_tuning_frequency(); radio::set_baseband_decimation_by(baseband_oversampling()); - BasebandConfigurationMessage message { baseband_configuration }; - shared_memory.baseband_queue.push(message); + baseband::start(baseband_configuration); } - diff --git a/firmware/application/transmitter_model.hpp b/firmware/application/transmitter_model.hpp index 43a385ed..55d078ac 100644 --- a/firmware/application/transmitter_model.hpp +++ b/firmware/application/transmitter_model.hpp @@ -33,11 +33,6 @@ class TransmitterModel { public: - constexpr TransmitterModel( - ) - { - } - rf::Frequency tuning_frequency() const; void set_tuning_frequency(rf::Frequency f); @@ -65,18 +60,16 @@ public: void set_baseband_configuration(const BasebandConfiguration config); private: - rf::Frequency frequency_step_ { 25000 }; bool enabled_ { false }; bool rf_amp_ { true }; int32_t lna_gain_db_ { 0 }; uint32_t baseband_bandwidth_ { max2837::filter::bandwidth_minimum }; int32_t vga_gain_db_ { 8 }; BasebandConfiguration baseband_configuration { - .mode = 1, - .sampling_rate = 2280000, + .mode = 0, /* TODO: Enum! */ + .sampling_rate = 3072000, .decimation_factor = 1, }; - int32_t tuning_offset(); void update_tuning_frequency(); @@ -86,8 +79,6 @@ private: void update_vga(); void update_modulation(); void update_baseband_configuration(); - - void baseband_disable(); }; #endif/*__TRANSMITTER_MODEL_H__*/ diff --git a/firmware/application/ui_about.cpp b/firmware/application/ui_about.cpp index aa924c42..bc6b9568 100644 --- a/firmware/application/ui_about.cpp +++ b/firmware/application/ui_about.cpp @@ -65,17 +65,25 @@ void AboutView::on_show() { FIFODataMessage datamessage; const auto message = static_cast(p); if (message->signaltype == 1) { + //debug_cnt++; + //if (debug_cnt == 250) for(;;) {} render_audio(); datamessage.data = ym_buffer; shared_memory.baseband_queue.push(datamessage); } } ); - + transmitter_model.set_tuning_frequency(92200000); // 92.2MHz, change ! - - audio::headphone::set_volume(volume_t::decibel(0 - 99) + audio::headphone::volume_range().max); + transmitter_model.set_baseband_configuration({ + .mode = 0, + .sampling_rate = 1536000, + .decimation_factor = 1, + }); + transmitter_model.set_rf_amp(true); transmitter_model.enable(); + + //audio::headphone::set_volume(volume_t::decibel(0 - 99) + audio::headphone::volume_range().max); } void AboutView::render_video() { @@ -376,11 +384,13 @@ AboutView::AboutView( { uint8_t p, c; + /* transmitter_model.set_baseband_configuration({ - .mode = 5, + .mode = 0, .sampling_rate = 1536000, .decimation_factor = 1, }); + */ add_children({ { &text_title, diff --git a/firmware/application/ui_about.hpp b/firmware/application/ui_about.hpp index 2db4b8bb..53840ad6 100644 --- a/firmware/application/ui_about.hpp +++ b/firmware/application/ui_about.hpp @@ -46,6 +46,7 @@ private: void render_video(); void render_audio(); void draw_demoglyph(ui::Point p, char ch, ui::Color * pal); + uint16_t debug_cnt = 0; typedef struct ymreg_t { uint8_t value; diff --git a/firmware/application/ui_afsksetup.cpp b/firmware/application/ui_afsksetup.cpp index 334f4425..a835b20d 100644 --- a/firmware/application/ui_afsksetup.cpp +++ b/firmware/application/ui_afsksetup.cpp @@ -104,11 +104,10 @@ AFSKSetupView::AFSKSetupView( field_repeat.set_value(rpt); button_setfreq.on_select = [this,&nav](Button&){ - auto new_view = new FrequencyKeypadView { nav, transmitter_model.tuning_frequency() }; + auto new_view = nav.push(transmitter_model.tuning_frequency()); new_view->on_changed = [this](rf::Frequency f) { updfreq(f); }; - nav.push(new_view); }; if (portapack::persistent_memory::afsk_bitrate() == 1200) { diff --git a/firmware/application/ui_audiotx.cpp b/firmware/application/ui_audiotx.cpp new file mode 100644 index 00000000..73bfa022 --- /dev/null +++ b/firmware/application/ui_audiotx.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * + * 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_audiotx.hpp" + +#include "ch.h" + +#include "ui_alphanum.hpp" +#include "ff.h" +#include "hackrf_gpio.hpp" +#include "portapack.hpp" +#include "radio.hpp" + +#include "hackrf_hal.hpp" +#include "portapack_shared_memory.hpp" + +#include + +using namespace portapack; + +namespace ui { + +void AudioTXView::focus() { + button_transmit.focus(); +} + +void AudioTXView::on_tuning_frequency_changed(rf::Frequency f) { + transmitter_model.set_tuning_frequency(f); +} + +AudioTXView::AudioTXView( + NavigationView& nav +) +{ + transmitter_model.set_tuning_frequency(92200000); + + add_children({ { + &text_title, + &field_frequency, + &button_transmit, + &button_exit + } }); + + field_frequency.set_value(transmitter_model.tuning_frequency()); + field_frequency.set_step(receiver_model.frequency_step()); + field_frequency.on_change = [this](rf::Frequency f) { + this->on_tuning_frequency_changed(f); + }; + field_frequency.on_edit = [this, &nav]() { + // TODO: Provide separate modal method/scheme? + auto new_view = nav.push(receiver_model.tuning_frequency()); + new_view->on_changed = [this](rf::Frequency f) { + this->on_tuning_frequency_changed(f); + this->field_frequency.set_value(f); + }; + }; + + button_transmit.on_select = [](Button&){ + transmitter_model.set_baseband_configuration({ + .mode = 1, + .sampling_rate = 1536000, + .decimation_factor = 1, + }); + transmitter_model.set_rf_amp(true); + transmitter_model.enable(); + }; + + button_exit.on_select = [&nav](Button&){ + nav.pop(); + }; +} + +AudioTXView::~AudioTXView() { + transmitter_model.disable(); +} + +} diff --git a/firmware/baseband-tx/dsp_iir.cpp b/firmware/application/ui_audiotx.hpp similarity index 51% rename from firmware/baseband-tx/dsp_iir.cpp rename to firmware/application/ui_audiotx.hpp index 443183ba..4cc7ede4 100644 --- a/firmware/baseband-tx/dsp_iir.cpp +++ b/firmware/application/ui_audiotx.hpp @@ -19,39 +19,51 @@ * Boston, MA 02110-1301, USA. */ -#include "dsp_iir.hpp" +#include "ui.hpp" +#include "ui_widget.hpp" +#include "ui_painter.hpp" +#include "ui_menu.hpp" +#include "ui_navigation.hpp" +#include "ui_font_fixed_8x16.hpp" +#include "clock_manager.hpp" +#include "message.hpp" +#include "rf_path.hpp" +#include "max2837.hpp" +#include "volume.hpp" +#include "ui_receiver.hpp" +#include "transmitter_model.hpp" -#include - -void IIRBiquadFilter::configure(const iir_biquad_config_t& new_config) { - config = new_config; -} - -void IIRBiquadFilter::execute(const buffer_f32_t& buffer_in, const buffer_f32_t& buffer_out) { - const auto a_ = config.a; - const auto b_ = config.b; +namespace ui { - auto x_ = x; - auto y_ = y; +class AudioTXView : public View { +public: + AudioTXView(NavigationView& nav); + ~AudioTXView(); - // TODO: Assert that buffer_out.count == buffer_in.count. - for(size_t i=0; i({ { - { "RFFC5072", [&nav](){ nav.push( + { "RFFC5072", ui::Color::white(), [&nav](){ nav.push( "RFFC5072", RegistersWidgetConfig { 31, 2, 4, 4 }, [](const size_t register_number) { return radio::debug::first_if::register_read(register_number); } ); } }, - { "MAX2837", [&nav](){ nav.push( + { "MAX2837", ui::Color::white(), [&nav](){ nav.push( "MAX2837", RegistersWidgetConfig { 32, 2, 3, 4 }, [](const size_t register_number) { return radio::debug::second_if::register_read(register_number); } ); } }, - { "Si5351C", [&nav](){ nav.push( + { "Si5351C", ui::Color::white(), [&nav](){ nav.push( "Si5351C", RegistersWidgetConfig { 96, 2, 2, 8 }, [](const size_t register_number) { return portapack::clock_generator.read_register(register_number); } ); } }, - { "WM8731", [&nav](){ nav.push( + { "WM8731",ui::Color::white(), [&nav](){ nav.push( "WM8731", RegistersWidgetConfig { audio::debug::reg_count(), 1, 3, 4 }, [](const size_t register_number) { return audio::debug::reg_read(register_number); } ); } }, @@ -272,13 +272,51 @@ DebugPeripheralsMenuView::DebugPeripheralsMenuView(NavigationView& nav) { DebugMenuView::DebugMenuView(NavigationView& nav) { add_items<5>({ { - { "Memory", [&nav](){ nav.push(); } }, - { "Radio State", [&nav](){ nav.push(); } }, - { "SD Card", [&nav](){ nav.push(); } }, - { "Peripherals", [&nav](){ nav.push(); } }, - { "Temperature", [&nav](){ nav.push(); } }, + { "Memory", ui::Color::white(), [&nav](){ nav.push(); } }, + { "Radio State", ui::Color::white(), [&nav](){ nav.push(); } }, + { "SD Card", ui::Color::white(), [&nav](){ nav.push(); } }, + { "Peripherals", ui::Color::white(), [&nav](){ nav.push(); } }, + { "Temperature", ui::Color::white(), [&nav](){ nav.push(); } }, } }); on_left = [&nav](){ nav.pop(); }; } +char hexify(char in) { + if (in > 9) in += 7; + return in + 0x30; +} + +DebugLCRView::DebugLCRView(NavigationView& nav, char * lcrstring, uint8_t checksum) { + char cstr[15] = "Checksum: 0x "; + + add_children({ { + &text_lcr1, + &text_lcr2, + &text_lcr3, + &text_lcr4, + &text_lcr5, + &text_checksum, + &button_done + } }); + + std::string b = std::string(lcrstring); + + text_lcr1.set(b.substr(8+(0*26),26)); + if (strlen(lcrstring) > 34) text_lcr2.set(b.substr(8+(1*26),26)); + if (strlen(lcrstring) > 34+26) text_lcr3.set(b.substr(8+(2*26),26)); + if (strlen(lcrstring) > 34+26+26) text_lcr4.set(b.substr(8+(3*26),26)); + if (strlen(lcrstring) > 34+26+26+26) text_lcr5.set(b.substr(8+(4*26),26)); + + cstr[12] = hexify(checksum >> 4); + cstr[13] = hexify(checksum & 15); + + text_checksum.set(cstr); + + button_done.on_select = [&nav](Button&){ nav.pop(); }; +} + +void DebugLCRView::focus() { + button_done.focus(); +} + } /* namespace ui */ diff --git a/firmware/application/ui_lcr.cpp b/firmware/application/ui_lcr.cpp index 4f70eb07..a0d8f60d 100644 --- a/firmware/application/ui_lcr.cpp +++ b/firmware/application/ui_lcr.cpp @@ -245,42 +245,36 @@ LCRView::LCRView( button_transmit_scan.set_style(&style_val); button_setrgsb.on_select = [this,&nav](Button&){ - auto an_view = new AlphanumView { nav, rgsb, 4 }; + auto an_view = nav.push(rgsb, 4); an_view->on_changed = [this](char *rgsb) { button_setrgsb.set_text(rgsb); }; - nav.push(an_view); }; button_setam_a.on_select = [this,&nav](Button&){ - auto an_view = new AlphanumView { nav, litteral[0], 7 }; + auto an_view = nav.push(litteral[0], 7); an_view->on_changed = [this](char *) {}; - nav.push(an_view); }; button_setam_b.on_select = [this,&nav](Button&){ - auto an_view = new AlphanumView { nav, litteral[1], 7 }; + auto an_view = nav.push(litteral[1], 7); an_view->on_changed = [this](char *) {}; - nav.push(an_view); }; button_setam_c.on_select = [this,&nav](Button&){ - auto an_view = new AlphanumView { nav, litteral[2], 7 }; + auto an_view = nav.push(litteral[2], 7); an_view->on_changed = [this](char *) {}; - nav.push(an_view); }; button_setam_d.on_select = [this,&nav](Button&){ - auto an_view = new AlphanumView { nav, litteral[3], 7 }; + auto an_view = nav.push(litteral[3], 7); an_view->on_changed = [this](char *) {}; - nav.push(an_view); }; button_setam_e.on_select = [this,&nav](Button&){ - auto an_view = new AlphanumView { nav, litteral[4], 7 }; + auto an_view = nav.push(litteral[4], 7); an_view->on_changed = [this](char *) {}; - nav.push(an_view); }; button_lcrdebug.on_select = [this,&nav](Button&){ make_frame(); - nav.push(new DebugLCRView { nav, lcrstring, checksum }); + nav.push(lcrstring, checksum); }; button_transmit.on_select = [this,&transmitter_model](Button&){ @@ -324,7 +318,7 @@ LCRView::LCRView( }; button_txsetup.on_select = [&nav](Button&){ - nav.push(new AFSKSetupView { nav }); + nav.push(); }; button_exit.on_select = [&nav](Button&){ diff --git a/firmware/application/ui_loadmodule.cpp b/firmware/application/ui_loadmodule.cpp index 92f63c0c..a183a9a6 100644 --- a/firmware/application/ui_loadmodule.cpp +++ b/firmware/application/ui_loadmodule.cpp @@ -32,6 +32,12 @@ #include "hackrf_hal.hpp" #include "string_format.hpp" +#include "ui_rds.hpp" +#include "ui_xylos.hpp" +#include "ui_lcr.hpp" +#include "ui_audiotx.hpp" +#include "ui_debug.hpp" + #include #include @@ -60,6 +66,8 @@ void LoadModuleView::on_show() { for (c=0; c<16; c++) { if (md5_signature[c] != _hash[c]) break; } + //text_info.set(to_string_hex(*((unsigned int*)0x10087FF0), 8)); + if (c == 16) { text_info.set("Module already loaded :)"); _mod_loaded = true; @@ -128,8 +136,6 @@ void LoadModuleView::loadmodule() { [this](Message* const p) { (void)p;*/ if (load_image()) { - text_info.set(to_string_hex(*((unsigned int*)0x10080000),8)); - //text_infob.set(to_string_hex(*((unsigned int*)0x10080004),8)); text_infob.set("Module loaded :)"); _mod_loaded = true; } else { @@ -144,7 +150,7 @@ void LoadModuleView::loadmodule() { LoadModuleView::LoadModuleView( NavigationView& nav, const char * hash, - View* new_view + uint8_t ViewID ) { add_children({ { @@ -155,9 +161,15 @@ LoadModuleView::LoadModuleView( _hash = hash; - button_ok.on_select = [this,&nav,new_view](Button&){ - //nav.pop(); - if (_mod_loaded == true) nav.push(new_view); + button_ok.on_select = [this, &nav, ViewID](Button&){ + if (_mod_loaded == true) { + if (ViewID == 0) nav.push(); + if (ViewID == 1) nav.push(); + if (ViewID == 2) nav.push(); + if (ViewID == 3) nav.push(); + } else { + nav.pop(); + } }; } diff --git a/firmware/application/ui_loadmodule.hpp b/firmware/application/ui_loadmodule.hpp index 5c5e7e52..37b78cd2 100644 --- a/firmware/application/ui_loadmodule.hpp +++ b/firmware/application/ui_loadmodule.hpp @@ -32,7 +32,7 @@ namespace ui { class LoadModuleView : public View { public: - LoadModuleView(NavigationView& nav, const char * hash, View * new_view); + LoadModuleView(NavigationView& nav, const char * hash, uint8_t ViewID); void loadmodule(); void on_show() override; diff --git a/firmware/application/ui_menu.cpp b/firmware/application/ui_menu.cpp index 4b79cbb1..5f584eeb 100644 --- a/firmware/application/ui_menu.cpp +++ b/firmware/application/ui_menu.cpp @@ -52,10 +52,20 @@ void MenuItemView::paint(Painter& painter) { r, paint_style.background ); + + ui::Color final_item_color = item.color; + + if (final_item_color.v == paint_style.background.v) final_item_color = paint_style.foreground; + + Style text_style { + .font = paint_style.font, + .background = paint_style.background, + .foreground = final_item_color + }; painter.draw_string( { r.pos.x + 8, r.pos.y + (r.size.h - font_height) / 2 }, - paint_style, + text_style, item.text ); } diff --git a/firmware/application/ui_menu.hpp b/firmware/application/ui_menu.hpp index 0bd19c11..9740bc1f 100644 --- a/firmware/application/ui_menu.hpp +++ b/firmware/application/ui_menu.hpp @@ -34,6 +34,7 @@ namespace ui { struct MenuItem { std::string text; + ui::Color color; std::function on_select; // TODO: Prevent default-constructed MenuItems. diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index 62d8c29a..a58e4fe0 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -20,6 +20,9 @@ */ #include "ui_navigation.hpp" +#include "ui_loadmodule.hpp" + +#include "modules.h" #include "portapack.hpp" #include "event_m0.hpp" @@ -156,9 +159,9 @@ void NavigationView::focus() { TranspondersMenuView::TranspondersMenuView(NavigationView& nav) { add_items<3>({ { - { "AIS: Boats", [&nav](){ nav.push(); } }, - { "ERT: Utility Meters", [&nav](){ nav.push(); } }, - { "TPMS: Cars", [&nav](){ nav.push(); } }, + { "AIS: Boats", ui::Color::white(), [&nav](){ nav.push(); } }, + { "ERT: Utility Meters", ui::Color::white(), [&nav](){ nav.push(); } }, + { "TPMS: Cars", ui::Color::white(), [&nav](){ nav.push(); } }, } }); on_left = [&nav](){ nav.pop(); }; } @@ -167,8 +170,8 @@ TranspondersMenuView::TranspondersMenuView(NavigationView& nav) { ReceiverMenuView::ReceiverMenuView(NavigationView& nav) { add_items<2>({ { - { "Audio", [&nav](){ nav.push(); } }, - { "Transponders", [&nav](){ nav.push(); } }, + { "Audio", ui::Color::white(), [&nav](){ nav.push(); } }, + { "Transponders", ui::Color::white(), [&nav](){ nav.push(); } }, } }); on_left = [&nav](){ nav.pop(); }; } @@ -176,36 +179,32 @@ ReceiverMenuView::ReceiverMenuView(NavigationView& nav) { /* SystemMenuView ********************************************************/ SystemMenuView::SystemMenuView(NavigationView& nav) { - add_items<7>({ { - { "Receiver", [&nav](){ nav.push(); } }, - { "Capture", [&nav](){ nav.push(); } }, - { "Analyze", [&nav](){ nav.push(); } }, - { "Setup", [&nav](){ nav.push(); } }, - { "About", [&nav](){ nav.push(); } }, - { "Debug", [&nav](){ nav.push(); } }, - { "HackRF", [&nav](){ nav.push(); } }, + add_items<10>({ { + { "Play dead", ui::Color::red(), [&nav](){ nav.push(false); } }, + { "Receiver", ui::Color::cyan(), [&nav](){ nav.push(); } }, + { "RDS TX", ui::Color::yellow(), [&nav](){ nav.push(md5_baseband_tx, 0); } }, + { "Xylos TX", ui::Color::yellow(), [&nav](){ nav.push(md5_baseband_tx, 1); } }, + { "TEDI/LCR TX", ui::Color::yellow(), [&nav](){ nav.push(md5_baseband_tx, 2); } }, + { "Audio TX", ui::Color::orange(), [&nav](){ nav.push(md5_baseband_tx, 3); } }, + //{ "Capture", ui::Color::white(), [&nav](){ nav.push(); } }, + //{ "Analyze", ui::Color::white(), [&nav](){ nav.push(); } }, + { "Setup", ui::Color::white(), [&nav](){ nav.push(); } }, + { "About", ui::Color::white(), [&nav](){ nav.push(); } }, + { "Debug", ui::Color::white(), [&nav](){ nav.push(); } }, + { "HackRF", ui::Color::white(), [&nav](){ nav.push(); } }, } }); -/* add_items<10>({ { - { "Play dead", ui::Color::red(), [&nav](){ nav.push(false); } }, - { "Receiver", ui::Color::cyan(), [&nav](){ nav.push(md5_baseband, new ReceiverMenuView(nav)); } }, +/* //{ "Nordic/BTLE RX", ui::Color::cyan(), [&nav](){ nav.push(new NotImplementedView { nav }); } }, { "Jammer", ui::Color::white(), [&nav](){ nav.push(md5_baseband, new JammerView(nav)); } }, //{ "Audio file TX", ui::Color::white(), [&nav](){ nav.push(new NotImplementedView { nav }); } }, //{ "Encoder TX", ui::Color::green(), [&nav](){ nav.push(new NotImplementedView { nav }); } }, //{ "Whistle", ui::Color::purple(), [&nav](){ nav.push(new LoadModuleView { nav, md5_baseband, new WhistleView { nav, transmitter_model }}); } }, //{ "SIGFOX RX", ui::Color::orange(), [&nav](){ nav.push(new LoadModuleView { nav, md5_baseband, new SIGFRXView { nav, receiver_model }}); } }, - { "RDS TX", ui::Color::yellow(), [&nav](){ nav.push(md5_baseband_tx, new RDSView(nav)); } }, - { "Xylos TX", ui::Color::orange(), [&nav](){ nav.push(md5_baseband_tx, new XylosView(nav)); } }, //{ "Xylos RX", ui::Color::green(), [&nav](){ nav.push(new LoadModuleView { nav, md5_baseband_tx, new XylosRXView { nav, receiver_model }}); } }, //{ "AFSK RX", ui::Color::cyan(), [&nav](){ nav.push(new LoadModuleView { nav, md5_baseband, new AFSKRXView { nav, receiver_model }}); } }, - { "TEDI/LCR TX", ui::Color::yellow(), [&nav](){ nav.push(md5_baseband_tx, new LCRView(nav)); } }, //{ "Numbers station", ui::Color::purple(), [&nav](){ nav.push(new LoadModuleView { nav, md5_baseband_tx, new NumbersStationView { nav, transmitter_model }}); } }, - { "Setup", ui::Color::white(), [&nav](){ nav.push(); } }, - { "About", ui::Color::white(), [&nav](){ nav.push(); } }, - { "Debug", ui::Color::white(), [&nav](){ nav.push(); } }, - { "HackRF", ui::Color::white(), [&nav](){ nav.push(); } }, - } });*/ +*/ } /* SystemView ************************************************************/ diff --git a/firmware/application/ui_navigation.hpp b/firmware/application/ui_navigation.hpp index 2d8f1e99..c165a3c3 100644 --- a/firmware/application/ui_navigation.hpp +++ b/firmware/application/ui_navigation.hpp @@ -93,7 +93,7 @@ public: void set_title(const std::string new_value); private: - static constexpr auto default_title = "PortaPack"; + static constexpr auto default_title = "PortaPack|Havoc"; static constexpr auto back_text_enabled = " < "; static constexpr auto back_text_disabled = " * "; diff --git a/firmware/application/ui_rds.cpp b/firmware/application/ui_rds.cpp index c94c1f95..e5e1aa9f 100644 --- a/firmware/application/ui_rds.cpp +++ b/firmware/application/ui_rds.cpp @@ -148,8 +148,7 @@ RDSView::RDSView( } }); button_setpsn.on_select = [this,&nav](Button&){ - auto an_view = new AlphanumView { nav, psname, 8 }; - nav.push(an_view); + nav.push(psname, 8); }; button_transmit.on_select = [&transmitter_model](Button&){ diff --git a/firmware/application/ui_setup.cpp b/firmware/application/ui_setup.cpp index 3b7445a6..4bd2ae3b 100644 --- a/firmware/application/ui_setup.cpp +++ b/firmware/application/ui_setup.cpp @@ -441,15 +441,6 @@ void ModInfoView::focus() { SetupMenuView::SetupMenuView(NavigationView& nav) { add_items<7>({ { - { "SD card modules", [&nav](){ nav.push(); } }, - { "Date/Time", [&nav](){ nav.push(); } }, - { "Frequency correction", [&nav](){ nav.push(); } }, - { "Antenna Bias Voltage", [&nav](){ nav.push(); } }, - { "Touch screen", [&nav](){ nav.push(); } }, - { "Play dead", [&nav](){ nav.push(); } }, - { "UI", [&nav](){ nav.push(); } }, - } }); - /*add_items<7>({ { { "SD card modules", ui::Color::white(), [&nav](){ nav.push(); } }, { "Date/Time", ui::Color::white(), [&nav](){ nav.push(); } }, { "Frequency correction", ui::Color::white(), [&nav](){ nav.push(); } }, @@ -457,7 +448,7 @@ SetupMenuView::SetupMenuView(NavigationView& nav) { { "Touch screen", ui::Color::white(), [&nav](){ nav.push(); } }, { "Play dead", ui::Color::red(), [&nav](){ nav.push(); } }, { "UI", ui::Color::white(), [&nav](){ nav.push(); } }, - } });*/ + } }); on_left = [&nav](){ nav.pop(); }; } diff --git a/firmware/baseband-tx.bin b/firmware/baseband-tx.bin index bbf75555afcee1bfd1e2450e86c3822128bcaf9c..f456c721be28b4e5f21ebcb04db5527c0fe4ef42 100644 GIT binary patch delta 6333 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 delta 12299 zcmZo@VVcmwq!ke0tH8v-(7?dpT2YW%l$o2FSCW{+#lXN|eORhJ?atGCO~?Lk+!nz3 zZ==>N#`=d$3=E7c5G-iKz`$t4!0^!!!iR~2=#5Y{AT|gqLir0t85kytW2y(KAp+OK zEhj=T4b;|Sx_l!O1H(Zk1~B-rHQXoMPdOvp)A@q{vvNXm28d2hxb{QG(Bsod1LrRe z!N-2+82W$UFlrkfdgzfh)zy8_CrQcINRRG`2!=faB_kolLEsB=3h)7 z7XD}Yu>3#MhrRzTJuE$~yqG62Dl@I{|M1_8b8_zw69cY^Jd;?avQA~2$}yE`I{P&4 zDU2V|e}Q!K{3_bU@L}gK#t;2Jb-g7r7``@yNSJUaGkmQ-7{EB0;lrE%SAMWqT=~JE z!TgmWdA1a*Hp>l$4Jq!b1}0hJqiJ_8I3_a#+6S&JPsy<1E0AV z4t!-uW7g~0FXlE?TEWH0jp@Uq|BS1(+-6EDxG{aW^MC*6_@_=cRw5|y zL1s*_hPwvLFxc9>;s1A;{|q0xelmQh_{s3W?jOU4ntu+8d>(wh3?J_QVpQSvKAhO_ zL&0H1BEyID9}qi|wynzK@SK>);lVIj$&JI?|I402h7TWpF>f%~3knaAT?{M?ssa~m z7#I>n7z7I0!1gix*vjyM_ouM8uMfliyc0w! zZ)EXi^^!_p`k=bE!2O0OgTNh827w2n3<3*~d;(I5uAUc7{RZpFVw`ePOdr1dVN_O5 z5ZKGGAKADa)|36f(l`DvV39sxJ-LljL4xVSZm4c#y(g?E?*mKDhw4R^ykI@~8>gBQ z(}#vX${CEt$_bDFM7RHj^<+CPIVGkK2_R+GxRgDxo?OQzCB*c>X%Dg`FRUl8;*wEj z`k=iB)mtB+p~UDdk-+ppX3vAkFS(@aVc`sl5@v4(rRW6dgy=-+#90Xi?x1)8iNmnK zRu;Ga-7Ah4I& zmsVs*G7qMmPU|w*vdhdo!Q5 zI@nig7`|duR89c-s`ZD0z~dZwkEGm(nOhSXjUT5lXK1G?mgPn+)ogVM?j9#-$x9|{VSV|XO$75=|@ z&h7!qfe5{PdqLp_@&bbm0|U$(5YI3$6l|6D{r~;VKaGqvnavpsGrFA^ZWQcg&d^R^ zRAW$zO=$R`kihJ=A`uop6PM7gDEvRSFJwxVJ5^2 zv!y{^V7>_o(QE$@A$npD%rP*B6l|6B1z8TZ5@MkOgYrU9Od706WOieC08JJ*K-ThO zvleb^J+ig4k*w|AgBg*q5csfF$j31yHPbbbL8&o;C6VEy0b@d<{|6>MMs}9A2CsEy(Km1wT z9GnC`Ff;JGO>&#*#_}Sc_k}$l z8Tt8sFb4R4_|L?0Vb4b<{`#l_h6{5G9!hq&Mqgt27|D9{BO|ky4D$zO<{;_GP8$1# z-Pp8d>=$-p&}R5BzadnOQM=_wM1qE!^aUpEmLHJ`EFYs7L@zLFxBQ4oVEGu$z<+^7 zyX8l80?Wr32F?p?+AJSq8CrhCB(Usn`4OAI@-dEKmXq3rOHyL;M_)7Y9oBix$G4S( z(RANOCVu}{t!#WZBaD?@{68>&d}hP7O??IT=i@E1 zw}0JIZ+%I_J?g<_2V{doKT&!mRA_IDtw1@DGOxu_=;i45}<24VW1fSw8Bp zDJ3Q(9{Itspyh{>KuF?|A1(`8ekc^Q{7?)~O?jG}nzS@g(~TuDIaxVb!R={^YHDig zksmS*&yxOs=lb{m`;I@_Zq1(sZ8{V{Ijn!z$9nz)H+Ftx;6HG2&+`byZnqw{Wo}|> z`p+HQnx6ZA2;)(E?*Ad2NBKF!eghi@22diNIN6b3y1wv_fcKnCRkvkHp+9u^S2(d< zV9t1)^#6PA-v?PMoPJ!K@YZsYIpB;6y%!|KHpGGJ8M@9Ybgl(*P}A zK&jGVE3X@io5n^4P3GGv2`ULo6D+``p!8oDk754@i28bvdWM3n4$3TUd>|in{Qs`; z=l}O>e{420=&9abnxUM*peuKKu@mzJA&>vxfBjjSu`GenltK5#{`?P&Y@BxzGMqD# z6J!$pe;4`7Xv(PDmh|=F|L^zy{{JrX2U52b>|N~i)kfke0{)?=OG<|Gxf@&*sG$N*N5=g0~krF<%h&`2T(HpT!xB)(qN<6L@bk zsQmx__BX_M1viVmi=5tGfXIEk#p$~`b52rHMpS}K0<%JLf^fqB@9+Pt&YY9=1nh*F ze=a^Zv4zBeN+Q@53=EKJCFakcjSy?Oz}E76{Quqrvep=6E!$mzy^EZ_TwnzAe%w*c zkjhX>U{DN6n3)8s0|XrYf1kVueYg<+*as z>x4B4swo9~C%oWdkW66A@J?vUnp`I!#kc!8nvhb??8}Ruvv~afe)D(#b5QC`uw!5- zv14Eeumc4SD0zU|h5x_5{H5Ub|9jd0|KI;WTZRIA<-9-%Xlf=z#f@J&ISe|w7ZzvC zbz;6C;qm|b`rkS^Y8j6+v{RNj$z90InJgeEQE!y9GQq%2Ie|ejCSh5^qXd(b|KBD5 z{QrI&YOI1=!CnUC#R*@*&T9FslXC{_unl$$40E7C2?|h9(&2Uc|K0ibo{f*Qr~FV* zaDBzh$f5Nxnfn5#7Q+RLy^FKDoqt~hIlB{-WRgH4l3`BYE?Vqm^!Wch{P*MMCiWns zAehg)VC$SrR(N_$|MfUq_!TP~+r_0x918!x_y2yJ&H9R&P5BiY^W)_I-}9jkHgGH0 z`(^T9LFuUXdJF;#4U7UJVhjQT(AGW3YyogKgM^adum9ig|KjrG^5)2p%6KfR{DGZW zNNTdv-2KuL+zBjhpcHNnHCe$;U@ye1$wfj+Y!YG&0)fJlX9+biNr+9B6ZT-L`87F1 zIElyW*P7=H`xn?VFifzYd`);+edP{Nv;P10+Mlq@1)iU7$!rfSFEaA zzcSPx{LIaG@M}Yq#(w`V45h1FHTJJ^Gjs`ZWBl;?@2S_UOuVnT7!2JQKRo#h>R2)8 zUcSIEn?qadV*3xB2AFk+FMzC2%3w4+{F$2>Wa8(T!Wbqa=U0r%!Y;}Qo1bU8S-3HL zsK535>2rqt;0_fi{GqsDtF(`__o7Ux1Xj1E4GoHn3je;B{$HHAI>Aek^`im<;|I1s zA&M^&G~5_IZ2ZNgHuZ;&f&Yj92HhL~fB*US5Ic*T;FZPfpRa1TF|K5s#_(bJZ}rj`9?+EB1pL)h*A_r(7%!O0dJoD2*MlVikW7;Pq3ih1+2{(!}b$B)Su#C#kq zp}m>^-%EagO80-?8}>0MFgVzNq5|r8W-k`+OFuXinLVTu{(o241Crk(#vpJ&j6q=X z8VMCOj;#!epp2aTo5hR8lhp$hpZ~w7><1|@lwe>Glwe?ZU_W`8_$J1h$&nJOMjW0T z9{<1p{l}mPYWXq#X7{@EL%s4v!~PmE27w2lUMt8Q3JeaDH%O?22zdPe zF8zDKPJVCcjP8y6UbCg71(}RNO$Dac%)#P3xe`#=F{Gj*S|4V}tiyMO?l_ z<4m?mOtaW0ZOY`FBH_k0g=3QNM(K>%^%*nV*;b0JVEEAZlR@L@1+kUfD;PdZ{>l4M zhiA_Veg@u;E_~}=@H5=`!C-Li2ZMoy+u0uu4gbGO{l5Bx#o{V_C_+-3>Bj%>^1qKf zzX~0O5Ln6jgLwjo2g*KN%>Ex3xZZ?Z{lS13ipbvmv0j|NW553g3GOI24VM}FPk)r) zV*J4KTVQpN8}n;!Mh!Q{53IjInFnN$l=0CY3>N>tzxm~`f5LN6P5>3|prC}~t<4*_ z+!!XXc(Zyqe##L1|K02d^M=?TEWa7{ACzKXxB-fExMBu|AN5c;qC+pqgS3Lf77QE+hI3w{QDH}@Yr zr~N-P@ErZoQ^5MMegXT@9}^Dn9|uXo$aydL8JxfUw`qUD&*1+-kdHz6=#Li%j{abH zc=QL0p@y46BEtvk{|t)u9{<0m|6@@2|DENZHQ1rbP=`)?!OkFJ((;0x;m2ObPaXk` zADDkRe)0%n1*fdsUm9+P-VQ%Rto`f1I3$4*TgtBk>JFd&E3kkfiN-^oLB% z(H|N%j33;7X}BGIZll2X;o(mkC5RC|zm&bT69kpBlQrD7KL=R{3KbEkl@SVzA5Q;_ zfT}h7CFw2YrJW$8{3>B}iiX?#=Z>FFFfe}D_0#cF{Rt+BfhxZoKh0og{n`-7`n4s9 z-$Nro%q__@Ob7(t!n%{8Yhm1|b5B>-HwO#LC_#w~` z;mtUA!VgKE3qKV0EdI^me4B&$13wGPCy^A^*DWkuzlEDqG8%qJ=p6ni$*l5=`7`qe zM&>AoShpUxh96Qk4L>Ax7`}PbyD(_QT?7r29rz()!|=M5iP1DLbL&@!dTy5$`x#W1 zIRz)azsUXZ0Q2Ur4)rVcul_1feBg(I4aY}D=0rE{zML)0@UGwB@op`};7mqSkOvmOW@H9=VT)66;@gXJpKti7ro(6qvixwd zsN2cs!A?>loNB+BJ{|a>Vk7F-@PqHcuICJ@2Y#r$(Q@1H95Te{|K^C_iD#^nze~#2 zGkys90WOgl7#IrNQHKe=GeDia=isQ+eaqC%pnCtug~t1D3%cgNE$C|Az~a{FrthYl z>GX-eXtI;Tr#TJTUdgor8S06Q#`ZQ|EDvY$a&%8lW*lOW^Qvteu>1=z0aPcBU_ zHLGCIdvoEO5kviJC!tF(8I&2no(;VCl5O=p{v$6%Rtvpm<8uDUz|HWrAuRq?HnaaL zD<%oEBQM#M8NMD2`2St|$F(0U7T4hAsi1b^jsM>bekj*QZF1ELxto+N%sHF6`9oK5 z^OtAB!Y&Mo%wIW!?Tin9Zg=8%-Nbb1Eko6U%;e~p&x$LmH$j@NyB^WHL4U3|+>rJSPvj-x95T}Rd4cgL$h`g-3o zRGodxST*+ti^1F<3<3*FAD7LN}obNY)Eau^3j9qJBW{{4U+k_yY>sa zU6Nu@n(P+nCgo<}!Z4ZP!~Xvav!mR&UNbWqxG{X#0Un|TnImL;><5Fzzwh(^JM7PS zUf|Ba(BRC#@XtvGR4*7fAPRXVx!oRBCpI1W$jQj<$ozq^o|#ii zxs>6It%zXJRL)Fb63=GOy z>F-v6<3{}51TBW&Odps*8L#f}=XS?)pWA)dUNiD2MTrkHLC$5+UU~5{*s&{~b9_A;z*sSHGso8>L5!9nMvMiIi#GIDV;$WXxo#>xM_H(@c= z{>H!WZT}dQ7?qPsB;9PDgY2w7_?e55Ly7ALqXLM_Xb9qS{a`e>h1@ntWY=Vz7;%$% zGUFtck0#8LZUHV#lNmk~{9~NWJWJAzX?B2{qzl7`jDMiU$*ms@7F@=s!0FNdpTmBH zH<-azf^2}>STBLkN@)x$&C#sX$FNfDpTqv%=fP6Uleyd;Lg^)-ECKE+8J;j*qMyGk!1_fONMsO!@K5;J-G* zhZPMBN=JUYP|z^G^5cy{$PZ-$VV5I6-qaf)C-5mh9vgs?x12Vp@Bt<8BX8}hf^JUv z@r2>Zj~)SrkBmH^VYADOEkB+dX!-H@f#MGVgDXEi82EpgR>=69>60*WW^jOHh6aWS z(9Ce;#~%gM>`?!_s`o7{KR~iW%Z~>FEk7PASY#ae@xg%MwJY27*pZ5AStn)^9Q2>hzm)HoIe;1Zek>U>6=WT#Q(LSSkleIg?Tc=hu41?XEV)`bYq_F z0Z#mn|9}$zO>p8r2~PZ{{y6NP|2$ZV37ir^GN3s~4kgYX%pm(LAcenw{RajP&L7O6 zJ}{#E&)W5ooqyW?Q?HqrC%G|BX8GDs+_C@c50(Z=w;66hZXPZSA6EZioW=2)i_yc4 z;lol4%X|Jf><9Vu__GNfprj2+@}TOV0IeA?IYBOp^~}QgZwof>m*Zrt*YoC_B#{AX zXfi4?d~FDlFp?|*)ys^N{(WEin{n2qA1oG^elSRI&H_!mMoV#Ov)}mleZ_C4S&YiU z1v`E)7)*T5IOD<(1_^NZDm#aO*pR?=0T5H~+W8{v*#1e&k^0ntUHT1^UkkQHO!yp*8~pLm&z=u@rGca&$<@DzCkj(J?G{@-Q zcZ0v6F@b;IO@6X@UHUNrIwml|kYg`MFUWq7dqD1=9HFF9U$B+MO~~WlckiDkb_#k+ zXKdam=qc#Mmm!qlnXy%FwiN4YW#P@gzkOxaiAe)bn*v-#=q~ge}a-L!~U~p&e(#I zDLjsmy6OI3z7+ob{_G!eH$7u=p^Cgxhz@F6W%6S7zVstQ5wwl~6i)$KlQ*eEsr~zY z;y24j9tKt~HqX5;1RVZ-Kd@UNkbxmkpMgP8pMjxJpMk+pf3lJ4Hoh5p3<4oq3<42) z3<424lfS8I2>tu+|J(OdkP^rQF3*48J$_F%P?O;T830lpp*J~9O{4x`kbsf$2WEz- zgc%7p6F|iY$U8Um7z6|i_#8MCJb5{`I(~{WV)JD4`1hUjHz}3oL42u~U7#1)vFwA9OV3+|K!((7z=w)DF zXk%btsAFJYC}Ut?$Yo$)NM>MQh+tq~@MU0NaAII!FlAt1&}3j>kY-?D-~-vmz`*e7 z|NsAw|NsAg<^TWxNB{r-zwQ73|I7dX|3Cfz|N8$O|NsB5{Qv)d*8l(iqyPW^@B9D% zfBXOc{~P`P|6lF@|NoNz|NrOv|NlSB|NsAg{rms_WMFV(WMD{PWMF7uWMG)X$iT3jk%8eN zBLl-LMg|5(CI$v^CI$v=CI$vOCI$vSCI*I>dL{;j3?>GKA|?igN+t$|S|$btkXRj* z4-yB-M=>!lcrh_Bm@+Xi$T2Z6Ff%bQJY{5HILyevFprUep_GwhDrto2H2#*{{R2~*MNQe|Npyx|Nk%j_y2$7zyJU1dH((Xzwht={{?^l|5y0? z|Nq55|Nl4r`TyVe&;S2VfB*kKev7O_P_uCKluCq|G+>0|G)q9|Nq>- z5Ep{t2^0md85kIzGcquIW@2CvU}0deWo2NfW@BJD#m>N>#mT@hn~Q-#mWP3111|$Z zFh2tWyC4I@X(0xN1tJU#En*A|6%q^#HBt-=eKHIT8{`-m?kg}bNGsPfFr=z7Fziuh zV35;dVCd0dU|`W_U}!UBU=TE6U|4F#z~ErX!0^bLfuYlmfx*m?f#I1m1H&vg1_pmm z1_o9i28P{!3=9Q<3=GO43=EgT7#M0J85ope7#I%3F))NBGB7+&W?)E5V_>+O!NA~= z&A_lEmx19^J_Cb(5d%YXeF+0YX&D1URRse>b`=AITMYvPR~-Yxwt5Bz&qfA@gG~$! z@+}Mu39Sqat!)ep-R%qv#T^U`)}0Itk2@I{vbq=;&UY~|2z4_s7s z?CxS<@ake<*wV?s@Uw$~L9&B^LAafP;dv_q!_*cA2DxSihQ>w)hWdT=3=G$585mAh zGce4kWMFVAXJ9y4!oZ+Y#K4f7&%n@=!@$s)3Gsn)G6TcGcm@W?7zT!=5ey7(LKqk% z0~r{!eHj>3JsB7{Tp1XyIxsNw+AuH}S}-siHfCUO)n{PXuEoG0t;WDmtjNG{K!$;V zMS_9BUYLQQl#ijFVHqa_!(|pw3^Oq3{{H_z>f8VS-5>w|KltYV|3Ai_?{FZ};+d*=WD^T+@HFFySLzxMwB|9|ZI|Nr*3|NpOU`v3p=y8r)0 zR{#GWz5M_G{fqzqw_ou8|J^zN|IeEF|9|y>|A*E8|J&F8|G&NN|9`KB|Np<$H~#;>tNH)`sjdJ2x3&NO-`)BD|I+UN z|1b9b|Iat!|Nodt|Nrlp^8df`bchdR=luV_YTp0<4h#SPzqk1R|K?@?|Ld*%|Nrjl z|Nkef`~Tl{V_m)i6H|EYcd|Cb;9|6lIN|NjS${r?|*^8f$$ zr~m)2I`{v7{htf}|Ce0;|Nqg||NmWX{Qp1u*8l%c@BII-c>n+ZfQSG87d`&}zvb!w z{~gc&|F3!Z|9`^k|Nl+j{{R2;-T(hHV9;h~V9;Y{V9;b|V31*F zU|?rwV0g{Oz;J?%fnh!y1498D1A`440|N^i1H*Av28MQ41_ozV28KT@3=ErD7#K2G z7#O5k7#PknGceRK*E2BaGczzeU}9jHz{J4d#Kgexg%RSyXhsGGAw~v3V||KI=r#s2;OumA7=f6ssa|EK)>|G(ki|NnFT{r|rUS_^*r z_y51h|NsAu|Ns9V`2YX^g8%>j_x=C>fA#C%uco`WO#26VEJ z^q^Rmk%2)QS_>*e)rm7QFz_=nFt9N)F#G|v13*PM0|Ub?1_p+63=9kh85rsrHZw3V zfEppw7#JAZ85kHU7#J8b85kHM85kJ6KntB27#MUI7#L*H8WbSkT>t<7|LOn#|L=oV z*(?74|3CNt|NoQ!|Nr0h|NsA{|NsA2{r~^J`2YX^x&QzF&-nlUfAatT{}cZI{~rsk z_M`v*kNp4te*_eR*zZhu8T{(l8J_-PXJBdIU|7w|$#8oyCxfR87sHo#TnrPJax>(l z@i1^O2r!r#voXB*#m5jhn~gz2kdL84Ald%I5;ysFEIyXD%em67pem0 zSco9lb)Xg?$o)_um_mqIjwvb5_4%cFC8`gfuzLZ)TG3`6p*T-;^NGSi6WC*v{d9ig7N}L5Q+~fFfd$H zU|AiK{QnCqY?uHqcQ`7pfW-oqZ$K)pc(^%q8gHz zCIf?`HUop8CP-{@nU)dLLfy$x#%?SOhhB4UzG&Q|r%=vjxXR?}w7+R~C;0D0FzBMM G><0kLDNMrv diff --git a/firmware/baseband-tx/Makefile b/firmware/baseband-tx/Makefile index 4f92b720..0b07fa9d 100755 --- a/firmware/baseband-tx/Makefile +++ b/firmware/baseband-tx/Makefile @@ -1,3 +1,4 @@ + # # Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. # @@ -126,6 +127,7 @@ CPPSRC = main.cpp \ message_queue.cpp \ event.cpp \ event_m4.cpp \ + thread_wait.cpp \ gpdma.cpp \ baseband_dma.cpp \ baseband_sgpio.cpp \ @@ -136,11 +138,6 @@ CPPSRC = main.cpp \ dsp_decimate.cpp \ dsp_demodulate.cpp \ matched_filter.cpp \ - spectrum_collector.cpp \ - proc_rds.cpp \ - proc_jammer.cpp \ - proc_fsk_lcr.cpp \ - proc_xylos.cpp \ proc_audiotx.cpp \ proc_playaudio.cpp \ dsp_squelch.cpp \ @@ -153,7 +150,7 @@ CPPSRC = main.cpp \ rssi.cpp \ rssi_dma.cpp \ rssi_thread.cpp \ - audio.cpp \ + audio_compressor.cpp \ audio_output.cpp \ audio_dma.cpp \ audio_stats_collector.cpp \ diff --git a/firmware/baseband-tx/audio_dma.hpp b/firmware/baseband-tx/audio_dma.hpp index ec85f080..863a4f39 100644 --- a/firmware/baseband-tx/audio_dma.hpp +++ b/firmware/baseband-tx/audio_dma.hpp @@ -24,9 +24,22 @@ #include -#include "audio.hpp" +#include "buffer.hpp" namespace audio { + +struct sample_t { + union { + struct { + int16_t left; + int16_t right; + }; + uint32_t raw; + }; +}; + +using buffer_t = buffer_t; + namespace dma { void init(); diff --git a/firmware/baseband-tx/audio_output.cpp b/firmware/baseband-tx/audio_output.cpp index 6e29fa16..05d56d20 100644 --- a/firmware/baseband-tx/audio_output.cpp +++ b/firmware/baseband-tx/audio_output.cpp @@ -46,7 +46,7 @@ void AudioOutput::write( ) { std::array audio_f; for(size_t i=0; ion_block(buffer); + } + ); +} + +void AudioOutput::on_block( + const buffer_f32_t& audio ) { const auto audio_present_now = squelch.execute(audio); @@ -66,24 +77,27 @@ void AudioOutput::write( audio_present_history = (audio_present_history << 1) | (audio_present_now ? 1 : 0); const bool audio_present = (audio_present_history != 0); - if( audio_present ) { - i2s::i2s0::tx_unmute(); - } else { - i2s::i2s0::tx_mute(); + if( !audio_present ) { for(size_t i=0; i audio_int; + auto audio_buffer = audio::dma::tx_empty_buffer(); for(size_t i=0; i @@ -43,15 +45,23 @@ public: void write(const buffer_f32_t& audio); private: + static constexpr float k = 32768.0f; + static constexpr float ki = 1.0f / k; + + BlockDecimator block_buffer { 1 }; + IIRBiquadFilter hpf; IIRBiquadFilter deemph; FMSquelch squelch; + StreamInput stream { 14 }; + AudioStatsCollector audio_stats; uint64_t audio_present_history = 0; - void fill_audio_buffer(const buffer_f32_t& audio); + void on_block(const buffer_f32_t& audio); + void fill_audio_buffer(const buffer_f32_t& audio, const bool send_to_fifo); void feed_audio_stats(const buffer_f32_t& audio); }; diff --git a/firmware/baseband-tx/audio_stats_collector.cpp b/firmware/baseband-tx/audio_stats_collector.cpp index 227d5887..53fceea8 100644 --- a/firmware/baseband-tx/audio_stats_collector.cpp +++ b/firmware/baseband-tx/audio_stats_collector.cpp @@ -42,8 +42,8 @@ bool AudioStatsCollector::update_stats(const size_t sample_count, const size_t s const size_t samples_per_update = sampling_rate * update_interval; if( count >= samples_per_update ) { - statistics.rms_db = complex16_mag_squared_to_dbv_norm(squared_sum / count); - statistics.max_db = complex16_mag_squared_to_dbv_norm(max_squared); + statistics.rms_db = mag2_to_dbv_norm(squared_sum / count); + statistics.max_db = mag2_to_dbv_norm(max_squared); statistics.count = count; squared_sum = 0; diff --git a/firmware/baseband-tx/baseband_dma.cpp b/firmware/baseband-tx/baseband_dma.cpp index 7467f551..c44ace81 100644 --- a/firmware/baseband-tx/baseband_dma.cpp +++ b/firmware/baseband-tx/baseband_dma.cpp @@ -32,6 +32,8 @@ using namespace lpc43xx; #include "portapack_dma.hpp" +#include "thread_wait.hpp" + namespace baseband { namespace dma { @@ -99,21 +101,19 @@ constexpr size_t msg_count = transfers_per_buffer - 1; static std::array lli_loop; static constexpr auto& gpdma_channel_sgpio = gpdma::channels[portapack::sgpio_gpdma_channel_number]; -static Semaphore semaphore; - -static volatile const gpdma::channel::LLI* next_lli = nullptr; +static ThreadWait thread_wait; static void transfer_complete() { - next_lli = gpdma_channel_sgpio.next_lli(); - chSemSignalI(&semaphore); + const auto next_lli_index = gpdma_channel_sgpio.next_lli() - &lli_loop[0]; + thread_wait.wake_from_interrupt(next_lli_index); } static void dma_error() { + thread_wait.wake_from_interrupt(-1); disable(); } void init() { - chSemInit(&semaphore, 0); gpdma_channel_sgpio.set_handlers(transfer_complete, dma_error); // LPC_GPDMA->SYNC |= (1 << gpdma_src_peripheral); @@ -138,9 +138,6 @@ void configure( void enable(const baseband::Direction direction) { const auto gpdma_config = config(direction); gpdma_channel_sgpio.configure(lli_loop[0], gpdma_config); - - chSemReset(&semaphore, 0); - gpdma_channel_sgpio.enable(); } @@ -153,16 +150,22 @@ void disable() { } baseband::buffer_t wait_for_rx_buffer() { - const auto status = chSemWait(&semaphore); - if( status == RDY_OK ) { - const auto next = next_lli; - if( next ) { - const size_t next_index = next - &lli_loop[0]; - const size_t free_index = (next_index + transfers_per_buffer - 2) & transfers_mask; - return { reinterpret_cast(lli_loop[free_index].destaddr), transfer_samples }; - } else { - return { }; - } + const auto next_index = thread_wait.sleep(); + + if( next_index >= 0 ) { + const size_t free_index = (next_index + transfers_per_buffer - 2) & transfers_mask; + return { reinterpret_cast(lli_loop[free_index].destaddr), transfer_samples }; + } else { + return { }; + } +} + +baseband::buffer_t wait_for_tx_buffer() { + const auto next_index = thread_wait.sleep(); + + if( next_index >= 0 ) { + const size_t free_index = (next_index + transfers_per_buffer - 2) & transfers_mask; + return { reinterpret_cast(lli_loop[free_index].srcaddr), transfer_samples }; } else { return { }; } diff --git a/firmware/baseband-tx/baseband_thread.cpp b/firmware/baseband-tx/baseband_thread.cpp index b25789b9..d87f43b0 100644 --- a/firmware/baseband-tx/baseband_thread.cpp +++ b/firmware/baseband-tx/baseband_thread.cpp @@ -31,11 +31,8 @@ #include "rssi.hpp" #include "i2s.hpp" -#include "proc_xylos.hpp" -#include "proc_fsk_lcr.hpp" -#include "proc_jammer.hpp" -#include "proc_rds.hpp" #include "proc_playaudio.hpp" +#include "proc_audiotx.hpp" #include "portapack_shared_memory.hpp" @@ -83,7 +80,7 @@ void BasebandThread::run() { baseband_sgpio.init(); baseband::dma::init(); - const auto baseband_buffer = new std::array(); + const auto baseband_buffer = std::make_unique>(); baseband::dma::configure( baseband_buffer->data(), direction() @@ -99,7 +96,7 @@ void BasebandThread::run() { while(true) { // TODO: Place correct sampling rate into buffer returned here: - const auto buffer_tmp = baseband::dma::wait_for_rx_buffer(); + const auto buffer_tmp = baseband::dma::wait_for_tx_buffer(); if( buffer_tmp ) { buffer_c8_t buffer { buffer_tmp.p, buffer_tmp.count, baseband_configuration.sampling_rate @@ -117,19 +114,12 @@ void BasebandThread::run() { ); } } - - delete baseband_buffer; } BasebandProcessor* BasebandThread::create_processor(const int32_t mode) { switch(mode) { - case 0: return new RDSProcessor(); - case 1: return new LCRFSKProcessor(); - case 2: return nullptr; //new ToneProcessor(); - case 3: return new JammerProcessor(); - case 4: return new XylosProcessor(); - case 5: return new PlayAudioProcessor(); - case 6: return nullptr; //new AFSKRXProcessor(); + case 0: return new PlayAudioProcessor(); + case 1: return new AudioTXProcessor(); default: return nullptr; } } @@ -145,9 +135,6 @@ void BasebandThread::disable() { void BasebandThread::enable() { if( baseband_processor ) { - if( direction() == baseband::Direction::Receive ) { - rf::rssi::start(); - } baseband_sgpio.configure(direction()); baseband::dma::enable(direction()); baseband_sgpio.streaming_enable(); diff --git a/firmware/baseband-tx/baseband_thread.hpp b/firmware/baseband-tx/baseband_thread.hpp index 81d35433..5abb1ce1 100644 --- a/firmware/baseband-tx/baseband_thread.hpp +++ b/firmware/baseband-tx/baseband_thread.hpp @@ -30,11 +30,6 @@ class BasebandThread : public ThreadBase { public: - BasebandThread( - ) : ThreadBase { "baseband" } - { - } - Thread* start(const tprio_t priority); void on_message(const Message* const message); @@ -42,14 +37,17 @@ public: // This getter should die, it's just here to leak information to code that // isn't in the right place to begin with. baseband::Direction direction() const { - return baseband::Direction::Receive; + return baseband::Direction::Transmit; } + void wait_for_switch(void); + Thread* thread_main { nullptr }; Thread* thread_rssi { nullptr }; - BasebandProcessor* baseband_processor { nullptr }; private: + BasebandProcessor* baseband_processor { nullptr }; + BasebandConfiguration baseband_configuration; void run() override; diff --git a/firmware/baseband-tx/block_decimator.hpp b/firmware/baseband-tx/block_decimator.hpp index dbca0ebd..7782dc37 100644 --- a/firmware/baseband-tx/block_decimator.hpp +++ b/firmware/baseband-tx/block_decimator.hpp @@ -29,7 +29,7 @@ #include "dsp_types.hpp" #include "complex.hpp" -template +template class BlockDecimator { public: constexpr BlockDecimator( @@ -65,7 +65,7 @@ public: } template - void feed(const buffer_c16_t src, BlockCallback callback) { + void feed(const buffer_t& src, BlockCallback callback) { /* NOTE: Input block size must be >= factor */ set_input_sampling_rate(src.sampling_rate); @@ -85,7 +85,7 @@ public: } private: - std::array buffer; + std::array buffer; uint32_t input_sampling_rate_ { 0 }; size_t factor_ { 1 }; size_t src_i { 0 }; diff --git a/firmware/baseband-tx/channel_decimator.cpp b/firmware/baseband-tx/channel_decimator.cpp index a7e7edf7..c8a7cf5c 100644 --- a/firmware/baseband-tx/channel_decimator.cpp +++ b/firmware/baseband-tx/channel_decimator.cpp @@ -21,7 +21,7 @@ #include "channel_decimator.hpp" -buffer_c16_t ChannelDecimator::execute_decimation(buffer_c8_t buffer) { +buffer_c16_t ChannelDecimator::execute_decimation(const buffer_c8_t& buffer) { const buffer_c16_t work_baseband_buffer { work_baseband.data(), work_baseband.size() @@ -39,19 +39,15 @@ buffer_c16_t ChannelDecimator::execute_decimation(buffer_c8_t buffer) { * -> gain of 256 * -> decimation by 2 * -> 1.544MHz complex[1024], [-32768, 32512] */ - const auto stage_0_out = translate.execute(buffer, work_baseband_buffer); - - //if( fs_over_4_downconvert ) { - // // TODO: - //} else { - // Won't work until cic_0 will accept input type of buffer_c8_t. - // stage_0_out = cic_0.execute(buffer, work_baseband_buffer); - //} + auto stage_0_out = execute_stage_0(buffer, work_baseband_buffer); + if( decimation_factor == DecimationFactor::By2 ) { + return stage_0_out; + } /* 1.536MHz complex[1024], [-32768, 32512] * -> 3rd order CIC: -0.1dB @ 0.028fs, -1dB @ 0.088fs, -60dB @ 0.468fs * -0.1dB @ 43kHz, -1dB @ 136kHz, -60dB @ 723kHz - * -> gain of 8 + * -> gain of 1 * -> decimation by 2 * -> 768kHz complex[512], [-8192, 8128] */ auto cic_1_out = cic_1.execute(stage_0_out, work_baseband_buffer); @@ -82,3 +78,14 @@ buffer_c16_t ChannelDecimator::execute_decimation(buffer_c8_t buffer) { return cic_4_out; } + +buffer_c16_t ChannelDecimator::execute_stage_0( + const buffer_c8_t& buffer, + const buffer_c16_t& work_baseband_buffer +) { + if( fs_over_4_downconvert ) { + return translate.execute(buffer, work_baseband_buffer); + } else { + return cic_0.execute(buffer, work_baseband_buffer); + } +} diff --git a/firmware/baseband-tx/channel_decimator.hpp b/firmware/baseband-tx/channel_decimator.hpp index 3e20c0df..956964b0 100644 --- a/firmware/baseband-tx/channel_decimator.hpp +++ b/firmware/baseband-tx/channel_decimator.hpp @@ -32,6 +32,7 @@ class ChannelDecimator { public: enum class DecimationFactor { + By2, By4, By8, By16, @@ -39,13 +40,16 @@ public: }; constexpr ChannelDecimator( - ) : decimation_factor { DecimationFactor::By32 } + ) : decimation_factor { DecimationFactor::By32 }, + fs_over_4_downconvert { true } { } constexpr ChannelDecimator( - const DecimationFactor decimation_factor - ) : decimation_factor { decimation_factor } + const DecimationFactor decimation_factor, + const bool fs_over_4_downconvert = true + ) : decimation_factor { decimation_factor }, + fs_over_4_downconvert { fs_over_4_downconvert } { } @@ -53,7 +57,7 @@ public: decimation_factor = f; } - buffer_c16_t execute(buffer_c8_t buffer) { + buffer_c16_t execute(const buffer_c8_t& buffer) { auto decimated = execute_decimation(buffer); return decimated; @@ -62,18 +66,22 @@ public: private: std::array work_baseband; - //const bool fs_over_4_downconvert = true; - dsp::decimate::TranslateByFSOver4AndDecimateBy2CIC3 translate; - //dsp::decimate::DecimateBy2CIC3 cic_0; + dsp::decimate::Complex8DecimateBy2CIC3 cic_0; dsp::decimate::DecimateBy2CIC3 cic_1; dsp::decimate::DecimateBy2CIC3 cic_2; dsp::decimate::DecimateBy2CIC3 cic_3; dsp::decimate::DecimateBy2CIC3 cic_4; DecimationFactor decimation_factor; + const bool fs_over_4_downconvert; - buffer_c16_t execute_decimation(buffer_c8_t buffer); + buffer_c16_t execute_decimation(const buffer_c8_t& buffer); + + buffer_c16_t execute_stage_0( + const buffer_c8_t& buffer, + const buffer_c16_t& work_baseband_buffer + ); }; #endif/*__CHANNEL_DECIMATOR_H__*/ diff --git a/firmware/baseband-tx/channel_stats_collector.hpp b/firmware/baseband-tx/channel_stats_collector.hpp index 8c91ac5b..ca7d0eed 100644 --- a/firmware/baseband-tx/channel_stats_collector.hpp +++ b/firmware/baseband-tx/channel_stats_collector.hpp @@ -34,7 +34,7 @@ class ChannelStatsCollector { public: template - void feed(buffer_c16_t src, Callback callback) { + void feed(const buffer_c16_t& src, Callback callback) { auto src_p = src.p; while(src_p < &src.p[src.count]) { const uint32_t sample = *__SIMD32(src_p)++; @@ -49,7 +49,7 @@ public: if( count >= samples_per_update ) { const float max_squared_f = max_squared; - const int32_t max_db = complex16_mag_squared_to_dbv_norm(max_squared_f); + const int32_t max_db = mag2_to_dbv_norm(max_squared_f * (1.0f / (32768.0f * 32768.0f))); callback({ max_db, count }); max_squared = 0; diff --git a/firmware/baseband-tx/chconf.h b/firmware/baseband-tx/chconf.h index 8045cf4f..0ad24529 100755 --- a/firmware/baseband-tx/chconf.h +++ b/firmware/baseband-tx/chconf.h @@ -129,7 +129,7 @@ * @note The default is @p TRUE. */ #if !defined(CH_USE_REGISTRY) || defined(__DOXYGEN__) -#define CH_USE_REGISTRY TRUE +#define CH_USE_REGISTRY FALSE #endif /** diff --git a/firmware/baseband-tx/clock_recovery.hpp b/firmware/baseband-tx/clock_recovery.hpp index bd17255d..9a6904a3 100644 --- a/firmware/baseband-tx/clock_recovery.hpp +++ b/firmware/baseband-tx/clock_recovery.hpp @@ -116,19 +116,21 @@ private: template class ClockRecovery { public: + using SymbolHandler = std::function; + ClockRecovery( const float sampling_rate, const float symbol_rate, ErrorFilter error_filter, - std::function symbol_handler - ) : symbol_handler { symbol_handler } + SymbolHandler symbol_handler + ) : symbol_handler { std::move(symbol_handler) } { configure(sampling_rate, symbol_rate, error_filter); } ClockRecovery( - std::function symbol_handler - ) : symbol_handler { symbol_handler } + SymbolHandler symbol_handler + ) : symbol_handler { std::move(symbol_handler) } { } @@ -155,7 +157,7 @@ private: dsp::interpolation::LinearResampler resampler; GardnerTimingErrorDetector timing_error_detector; ErrorFilter error_filter; - std::function symbol_handler; + const SymbolHandler symbol_handler; void resampler_callback(const float interpolated_sample) { timing_error_detector(interpolated_sample, @@ -166,7 +168,12 @@ private: } void symbol_callback(const float symbol, const float lateness) { - symbol_handler(symbol); + // NOTE: This check is to avoid std::function nullptr check, which + // brings in "_ZSt25__throw_bad_function_callv" and a lot of extra code. + // TODO: Make symbol_handler known at compile time. + if( symbol_handler) { + symbol_handler(symbol); + } const float adjustment = error_filter(lateness); resampler.advance(adjustment); diff --git a/firmware/baseband-tx/dsp_decimate.cpp b/firmware/baseband-tx/dsp_decimate.cpp index e8fd1824..93dbc602 100644 --- a/firmware/baseband-tx/dsp_decimate.cpp +++ b/firmware/baseband-tx/dsp_decimate.cpp @@ -26,7 +26,451 @@ namespace dsp { namespace decimate { -buffer_c16_t TranslateByFSOver4AndDecimateBy2CIC3::execute(buffer_c8_t src, buffer_c16_t dst) { +static inline complex32_t mac_fs4_shift( + const vec2_s16* const z, + const vec2_s16* const t, + const size_t index, + const complex32_t accum +) { + /* Accumulate sample * tap results for samples already in z buffer. + * Multiply using swap/negation to achieve Fs/4 shift. + * For iterations where samples are shifting out of z buffer (being discarded). + * Expect negated tap t[2] to accomodate instruction set limitations. + */ + const bool negated_t2 = index & 1; + const auto q1_i0 = z[index*2 + 0]; + const auto i1_q0 = z[index*2 + 1]; + const auto t1_t0 = t[index]; + const auto real = negated_t2 ? smlsd(q1_i0, t1_t0, accum.real()) : smlad(q1_i0, t1_t0, accum.real()); + const auto imag = negated_t2 ? smlad(i1_q0, t1_t0, accum.imag()) : smlsd(i1_q0, t1_t0, accum.imag()); + return { real, imag }; +} + +static inline complex32_t mac_shift( + const vec2_s16* const z, + const vec2_s16* const t, + const size_t index, + const complex32_t accum +) { + /* Accumulate sample * tap results for samples already in z buffer. + * For iterations where samples are shifting out of z buffer (being discarded). + * real += i1 * t1 + i0 * t0 + * imag += q1 * t1 + q0 * t0 + */ + const auto i1_i0 = z[index*2 + 0]; + const auto q1_q0 = z[index*2 + 1]; + const auto t1_t0 = t[index]; + const auto real = smlad(i1_i0, t1_t0, accum.real()); + const auto imag = smlad(q1_q0, t1_t0, accum.imag()); + return { real, imag }; +} + +static inline complex32_t mac_fs4_shift_and_store( + vec2_s16* const z, + const vec2_s16* const t, + const size_t decimation_factor, + const size_t index, + const complex32_t accum +) { + /* Accumulate sample * tap results for samples already in z buffer. + * Place new samples into z buffer. + * Expect negated tap t[2] to accomodate instruction set limitations. + */ + const bool negated_t2 = index & 1; + const auto q1_i0 = z[decimation_factor + index*2 + 0]; + const auto i1_q0 = z[decimation_factor + index*2 + 1]; + const auto t1_t0 = t[decimation_factor / 2 + index]; + z[index*2 + 0] = q1_i0; + const auto real = negated_t2 ? smlsd(q1_i0, t1_t0, accum.real()) : smlad(q1_i0, t1_t0, accum.real()); + z[index*2 + 1] = i1_q0; + const auto imag = negated_t2 ? smlad(i1_q0, t1_t0, accum.imag()) : smlsd(i1_q0, t1_t0, accum.imag()); + return { real, imag }; +} + +static inline complex32_t mac_shift_and_store( + vec2_s16* const z, + const vec2_s16* const t, + const size_t decimation_factor, + const size_t index, + const complex32_t accum +) { + /* Accumulate sample * tap results for samples already in z buffer. + * Place new samples into z buffer. + * Expect negated tap t[2] to accomodate instruction set limitations. + */ + const auto i1_i0 = z[decimation_factor + index*2 + 0]; + const auto q1_q0 = z[decimation_factor + index*2 + 1]; + const auto t1_t0 = t[decimation_factor / 2 + index]; + z[index*2 + 0] = i1_i0; + const auto real = smlad(i1_i0, t1_t0, accum.real()); + z[index*2 + 1] = q1_q0; + const auto imag = smlad(q1_q0, t1_t0, accum.imag()); + return { real, imag }; +} + +static inline complex32_t mac_fs4_shift_and_store_new_c8_samples( + vec2_s16* const z, + const vec2_s16* const t, + const vec4_s8* const in, + const size_t decimation_factor, + const size_t index, + const size_t length, + const complex32_t accum +) { + /* Accumulate sample * tap results for new samples. + * Place new samples into z buffer. + * Expect negated tap t[2] to accomodate instruction set limitations. + */ + const bool negated_t2 = index & 1; + const auto q1_i1_q0_i0 = in[index]; + const auto t1_t0 = t[(length - decimation_factor) / 2 + index]; + const auto i1_q1_i0_q0 = rev16(q1_i1_q0_i0); + const auto i1_q1_q0_i0 = pkhbt(q1_i1_q0_i0, i1_q1_i0_q0); + const auto q1_i0 = sxtb16(i1_q1_q0_i0); + const auto i1_q0 = sxtb16(i1_q1_q0_i0, 8); + z[length - decimation_factor * 2 + index*2 + 0] = q1_i0; + const auto real = negated_t2 ? smlsd(q1_i0, t1_t0, accum.real()) : smlad(q1_i0, t1_t0, accum.real()); + z[length - decimation_factor * 2 + index*2 + 1] = i1_q0; + const auto imag = negated_t2 ? smlad(i1_q0, t1_t0, accum.imag()) : smlsd(i1_q0, t1_t0, accum.imag()); + return { real, imag }; +} + +static inline complex32_t mac_shift_and_store_new_c16_samples( + vec2_s16* const z, + const vec2_s16* const t, + const vec2_s16* const in, + const size_t decimation_factor, + const size_t index, + const size_t length, + const complex32_t accum +) { + /* Accumulate sample * tap results for new samples. + * Place new samples into z buffer. + * Expect negated tap t[2] to accomodate instruction set limitations. + */ + const auto q0_i0 = in[index*2+0]; + const auto q1_i1 = in[index*2+1]; + const auto i1_i0 = pkhbt(q0_i0, q1_i1, 16); + const auto q1_q0 = pkhtb(q1_i1, q0_i0, 16); + const auto t1_t0 = t[(length - decimation_factor) / 2 + index]; + z[length - decimation_factor * 2 + index*2 + 0] = i1_i0; + const auto real = smlad(i1_i0, t1_t0, accum.real()); + z[length - decimation_factor * 2 + index*2 + 1] = q1_q0; + const auto imag = smlad(q1_q0, t1_t0, accum.imag()); + return { real, imag }; +} + +static inline uint32_t scale_round_and_pack( + const complex32_t value, + const int32_t scale_factor +) { + /* Multiply 32-bit components of the complex by a scale factor, + * into int64_ts, then round to nearest LSB (1 << 32), saturate to 16 bits, + * and pack into a complex. + */ + const auto scaled_real = __SMMULR(value.real(), scale_factor); + const auto saturated_real = __SSAT(scaled_real, 16); + + const auto scaled_imag = __SMMULR(value.imag(), scale_factor); + const auto saturated_imag = __SSAT(scaled_imag, 16); + + return __PKHBT(saturated_real, saturated_imag, 16); +} + +template +static void taps_copy( + const Tap* const source, + Tap* const target, + const size_t count, + const bool shift_up +) { + const uint32_t negate_pattern = shift_up ? 0b1110 : 0b0100; + for(size_t i=0; i> (i & 3)) & 1; + target[i] = negate ? -source[i] : source[i]; + } +} + +// FIRC8xR16x24FS4Decim4 ////////////////////////////////////////////////// + +void FIRC8xR16x24FS4Decim4::configure( + const std::array& taps, + const int32_t scale, + const Shift shift +) { + taps_copy(taps.data(), taps_.data(), taps_.size(), shift == Shift::Up); + output_scale = scale; + z_.fill({}); +} + +buffer_c16_t FIRC8xR16x24FS4Decim4::execute( + const buffer_c8_t& src, + const buffer_c16_t& dst +) { + vec2_s16* const z = static_cast(__builtin_assume_aligned(z_.data(), 4)); + const vec2_s16* const t = static_cast(__builtin_assume_aligned(taps_.data(), 4)); + uint32_t* const d = static_cast(__builtin_assume_aligned(dst.p, 4)); + + const auto k = output_scale; + + const size_t count = src.count / decimation_factor; + for(size_t i=0; i(__builtin_assume_aligned(&src.p[i * decimation_factor], 4)); + + complex32_t accum; + + // Oldest samples are discarded. + accum = mac_fs4_shift(z, t, 0, accum); + accum = mac_fs4_shift(z, t, 1, accum); + + // Middle samples are shifted earlier in the "z" delay buffer. + accum = mac_fs4_shift_and_store(z, t, decimation_factor, 0, accum); + accum = mac_fs4_shift_and_store(z, t, decimation_factor, 1, accum); + accum = mac_fs4_shift_and_store(z, t, decimation_factor, 2, accum); + accum = mac_fs4_shift_and_store(z, t, decimation_factor, 3, accum); + accum = mac_fs4_shift_and_store(z, t, decimation_factor, 4, accum); + accum = mac_fs4_shift_and_store(z, t, decimation_factor, 5, accum); + accum = mac_fs4_shift_and_store(z, t, decimation_factor, 6, accum); + accum = mac_fs4_shift_and_store(z, t, decimation_factor, 7, accum); + + // Newest samples come from "in" buffer, are copied to "z" delay buffer. + accum = mac_fs4_shift_and_store_new_c8_samples(z, t, in, decimation_factor, 0, taps_count, accum); + accum = mac_fs4_shift_and_store_new_c8_samples(z, t, in, decimation_factor, 1, taps_count, accum); + + d[i] = scale_round_and_pack(accum, k); + } + + return { + dst.p, + count, + src.sampling_rate / decimation_factor + }; +} + +// FIRC8xR16x24FS4Decim8 ////////////////////////////////////////////////// + +void FIRC8xR16x24FS4Decim8::configure( + const std::array& taps, + const int32_t scale, + const Shift shift +) { + taps_copy(taps.data(), taps_.data(), taps_.size(), shift == Shift::Up); + output_scale = scale; + z_.fill({}); +} + +buffer_c16_t FIRC8xR16x24FS4Decim8::execute( + const buffer_c8_t& src, + const buffer_c16_t& dst +) { + vec2_s16* const z = static_cast(__builtin_assume_aligned(z_.data(), 4)); + const vec2_s16* const t = static_cast(__builtin_assume_aligned(taps_.data(), 4)); + uint32_t* const d = static_cast(__builtin_assume_aligned(dst.p, 4)); + + const auto k = output_scale; + + const size_t count = src.count / decimation_factor; + for(size_t i=0; i(__builtin_assume_aligned(&src.p[i * decimation_factor], 4)); + + complex32_t accum; + + // Oldest samples are discarded. + accum = mac_fs4_shift(z, t, 0, accum); + accum = mac_fs4_shift(z, t, 1, accum); + accum = mac_fs4_shift(z, t, 2, accum); + accum = mac_fs4_shift(z, t, 3, accum); + + // Middle samples are shifted earlier in the "z" delay buffer. + accum = mac_fs4_shift_and_store(z, t, decimation_factor, 0, accum); + accum = mac_fs4_shift_and_store(z, t, decimation_factor, 1, accum); + accum = mac_fs4_shift_and_store(z, t, decimation_factor, 2, accum); + accum = mac_fs4_shift_and_store(z, t, decimation_factor, 3, accum); + + // Newest samples come from "in" buffer, are copied to "z" delay buffer. + accum = mac_fs4_shift_and_store_new_c8_samples(z, t, in, decimation_factor, 0, taps_count, accum); + accum = mac_fs4_shift_and_store_new_c8_samples(z, t, in, decimation_factor, 1, taps_count, accum); + accum = mac_fs4_shift_and_store_new_c8_samples(z, t, in, decimation_factor, 2, taps_count, accum); + accum = mac_fs4_shift_and_store_new_c8_samples(z, t, in, decimation_factor, 3, taps_count, accum); + + d[i] = scale_round_and_pack(accum, k); + } + + return { + dst.p, + count, + src.sampling_rate / decimation_factor + }; +} + +// FIRC16xR16x16Decim2 //////////////////////////////////////////////////// + +void FIRC16xR16x16Decim2::configure( + const std::array& taps, + const int32_t scale +) { + std::copy(taps.cbegin(), taps.cend(), taps_.begin()); + output_scale = scale; + z_.fill({}); +} + +buffer_c16_t FIRC16xR16x16Decim2::execute( + const buffer_c16_t& src, + const buffer_c16_t& dst +) { + vec2_s16* const z = static_cast(__builtin_assume_aligned(z_.data(), 4)); + const vec2_s16* const t = static_cast(__builtin_assume_aligned(taps_.data(), 4)); + uint32_t* const d = static_cast(__builtin_assume_aligned(dst.p, 4)); + + const auto k = output_scale; + + const size_t count = src.count / decimation_factor; + for(size_t i=0; i(__builtin_assume_aligned(&src.p[i * decimation_factor], 4)); + + complex32_t accum; + + // Oldest samples are discarded. + accum = mac_shift(z, t, 0, accum); + + // Middle samples are shifted earlier in the "z" delay buffer. + accum = mac_shift_and_store(z, t, decimation_factor, 0, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 1, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 2, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 3, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 4, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 5, accum); + + // Newest samples come from "in" buffer, are copied to "z" delay buffer. + accum = mac_shift_and_store_new_c16_samples(z, t, in, decimation_factor, 0, taps_count, accum); + + d[i] = scale_round_and_pack(accum, k); + } + + return { + dst.p, + count, + src.sampling_rate / decimation_factor + }; +} + +// FIRC16xR16x32Decim8 //////////////////////////////////////////////////// + +void FIRC16xR16x32Decim8::configure( + const std::array& taps, + const int32_t scale +) { + std::copy(taps.cbegin(), taps.cend(), taps_.begin()); + output_scale = scale; + z_.fill({}); +} + +buffer_c16_t FIRC16xR16x32Decim8::execute( + const buffer_c16_t& src, + const buffer_c16_t& dst +) { + vec2_s16* const z = static_cast(__builtin_assume_aligned(z_.data(), 4)); + const vec2_s16* const t = static_cast(__builtin_assume_aligned(taps_.data(), 4)); + uint32_t* const d = static_cast(__builtin_assume_aligned(dst.p, 4)); + + const auto k = output_scale; + + const size_t count = src.count / decimation_factor; + for(size_t i=0; i(__builtin_assume_aligned(&src.p[i * decimation_factor], 4)); + + complex32_t accum; + + // Oldest samples are discarded. + accum = mac_shift(z, t, 0, accum); + accum = mac_shift(z, t, 1, accum); + accum = mac_shift(z, t, 2, accum); + accum = mac_shift(z, t, 3, accum); + + // Middle samples are shifted earlier in the "z" delay buffer. + accum = mac_shift_and_store(z, t, decimation_factor, 0, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 1, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 2, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 3, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 4, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 5, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 6, accum); + accum = mac_shift_and_store(z, t, decimation_factor, 7, accum); + + // Newest samples come from "in" buffer, are copied to "z" delay buffer. + accum = mac_shift_and_store_new_c16_samples(z, t, in, decimation_factor, 0, taps_count, accum); + accum = mac_shift_and_store_new_c16_samples(z, t, in, decimation_factor, 1, taps_count, accum); + accum = mac_shift_and_store_new_c16_samples(z, t, in, decimation_factor, 2, taps_count, accum); + accum = mac_shift_and_store_new_c16_samples(z, t, in, decimation_factor, 3, taps_count, accum); + + d[i] = scale_round_and_pack(accum, k); + } + + return { + dst.p, + count, + src.sampling_rate / decimation_factor + }; +} + +buffer_c16_t Complex8DecimateBy2CIC3::execute(const buffer_c8_t& src, const buffer_c16_t& dst) { + /* Decimates by two using a non-recursive third-order CIC filter. + */ + + /* CIC filter (decimating by two): + * D_I0 = i3 * 1 + i2 * 3 + i1 * 3 + i0 * 1 + * D_Q0 = q3 * 1 + q2 * 3 + q1 * 3 + q0 * 1 + * + * D_I1 = i5 * 1 + i4 * 3 + i3 * 3 + i2 * 1 + * D_Q1 = q5 * 1 + q4 * 3 + q3 * 3 + q2 * 1 + */ + + uint32_t i1_i0 = _i1_i0; + uint32_t q1_q0 = _q1_q0; + + /* 3:1 Scaled by 32 to normalize output to +/-32768-ish. */ + constexpr uint32_t scale_factor = 32; + constexpr uint32_t k_3_1 = 0x00030001 * scale_factor; + uint32_t* src_p = reinterpret_cast(&src.p[0]); + uint32_t* const src_end = reinterpret_cast(&src.p[src.count]); + uint32_t* dst_p = reinterpret_cast(&dst.p[0]); + while(src_p < src_end) { + const uint32_t q3_i3_q2_i2 = *(src_p++); // 3 + const uint32_t q5_i5_q4_i4 = *(src_p++); + + const uint32_t d_i0_partial = __SMUAD(k_3_1, i1_i0); // 1: = 3 * i1 + 1 * i0 + const uint32_t i3_i2 = __SXTB16(q3_i3_q2_i2, 0); // 1: (q3_i3_q2_i2 ror 0)[23:16]:(q3_i3_q2_i2 ror 0)[7:0] + const uint32_t d_i0 = __SMLADX(k_3_1, i3_i2, d_i0_partial); // 1: + 3 * i2 + 1 * i3 + + const uint32_t d_q0_partial = __SMUAD(k_3_1, q1_q0); // 1: = 3 * q1 * 1 * q0 + const uint32_t q3_q2 = __SXTB16(q3_i3_q2_i2, 8); // 1: (q3_i3_q2_i2 ror 8)[23:16]:(q3_i3_q2_i2 ror 8)[7:0] + const uint32_t d_q0 = __SMLADX(k_3_1, q3_q2, d_q0_partial); // 1: + 3 * q2 + 1 * q3 + + const uint32_t d_q0_i0 = __PKHBT(d_i0, d_q0, 16); // 1: (Rm<<16)[31:16]:Rn[15:0] + + const uint32_t d_i1_partial = __SMUAD(k_3_1, i3_i2); // 1: = 3 * i3 + 1 * i2 + const uint32_t i5_i4 = __SXTB16(q5_i5_q4_i4, 0); // 1: (q5_i5_q4_i4 ror 0)[23:16]:(q5_i5_q4_i4 ror 0)[7:0] + const uint32_t d_i1 = __SMLADX(k_3_1, i5_i4, d_i1_partial); // 1: + 1 * i5 + 3 * i4 + + const uint32_t d_q1_partial = __SMUAD(k_3_1, q3_q2); // 1: = 3 * q3 * 1 * q2 + const uint32_t q5_q4 = __SXTB16(q5_i5_q4_i4, 8); // 1: (q5_i5_q4_i4 ror 8)[23:16]:(q5_i5_q4_i4 ror 8)[7:0] + const uint32_t d_q1 = __SMLADX(k_3_1, q5_q4, d_q1_partial); // 1: + 1 * q5 + 3 * q4 + + const uint32_t d_q1_i1 = __PKHBT(d_i1, d_q1, 16); // 1: (Rm<<16)[31:16]:Rn[15:0] + + *(dst_p++) = d_q0_i0; // 3 + *(dst_p++) = d_q1_i1; + + i1_i0 = i5_i4; + q1_q0 = q5_q4; + } + _i1_i0 = i1_i0; + _q1_q0 = q1_q0; + + return { dst.p, src.count / 2, src.sampling_rate / 2 }; +} + +buffer_c16_t TranslateByFSOver4AndDecimateBy2CIC3::execute(const buffer_c8_t& src, const buffer_c16_t& dst) { /* Translates incoming complex samples by -fs/4, * decimates by two using a non-recursive third-order CIC filter. */ @@ -111,8 +555,8 @@ buffer_c16_t TranslateByFSOver4AndDecimateBy2CIC3::execute(buffer_c8_t src, buff } buffer_c16_t DecimateBy2CIC3::execute( - buffer_c16_t src, - buffer_c16_t dst + const buffer_c16_t& src, + const buffer_c16_t& dst ) { /* Complex non-recursive 3rd-order CIC filter (taps 1,3,3,1). * Gain of 8. @@ -121,20 +565,18 @@ buffer_c16_t DecimateBy2CIC3::execute( */ uint32_t t1 = _iq0; uint32_t t2 = _iq1; - uint32_t t3, t4; const uint32_t taps = 0x00000003; auto s = src.p; auto d = dst.p; const auto d_end = &dst.p[src.count / 2]; - uint32_t i, q; while(d < d_end) { - i = __SXTH(t1, 0); /* 1: I0 */ - q = __SXTH(t1, 16); /* 1: Q0 */ + uint32_t i = __SXTH(t1, 0); /* 1: I0 */ + uint32_t q = __SXTH(t1, 16); /* 1: Q0 */ i = __SMLABB(t2, taps, i); /* 1: I1*3 + I0 */ q = __SMLATB(t2, taps, q); /* 1: Q1*3 + Q0 */ - t3 = *__SIMD32(s)++; /* 3: Q2:I2 */ - t4 = *__SIMD32(s)++; /* Q3:I3 */ + const uint32_t t3 = *__SIMD32(s)++; /* 3: Q2:I2 */ + const uint32_t t4 = *__SIMD32(s)++; /* Q3:I3 */ i = __SMLABB(t3, taps, i); /* 1: I2*3 + I1*3 + I0 */ q = __SMLATB(t3, taps, q); /* 1: Q2*3 + Q1*3 + Q0 */ @@ -164,9 +606,15 @@ buffer_c16_t DecimateBy2CIC3::execute( return { dst.p, src.count / 2, src.sampling_rate / 2 }; } +void FIR64AndDecimateBy2Real::configure( + const std::array& new_taps +) { + std::copy(new_taps.cbegin(), new_taps.cend(), taps.begin()); +} + buffer_s16_t FIR64AndDecimateBy2Real::execute( - buffer_s16_t src, - buffer_s16_t dst + const buffer_s16_t& src, + const buffer_s16_t& dst ) { /* int16_t input (sample count "n" must be multiple of 4) * -> int16_t output, decimated by 2. @@ -197,9 +645,18 @@ buffer_s16_t FIR64AndDecimateBy2Real::execute( return { dst.p, src.count / 2, src.sampling_rate / 2 }; } +void FIRAndDecimateComplex::configure_common( + const size_t taps_count, const size_t decimation_factor +) { + samples_ = std::make_unique(taps_count); + taps_reversed_ = std::make_unique(taps_count); + taps_count_ = taps_count; + decimation_factor_ = decimation_factor; +} + buffer_c16_t FIRAndDecimateComplex::execute( - buffer_c16_t src, - buffer_c16_t dst + const buffer_c16_t& src, + const buffer_c16_t& dst ) { /* int16_t input (sample count "n" must be multiple of decimation_factor) * -> int16_t output, decimated by decimation_factor. @@ -308,8 +765,8 @@ buffer_c16_t FIRAndDecimateComplex::execute( } buffer_s16_t DecimateBy2CIC4Real::execute( - buffer_s16_t src, - buffer_s16_t dst + const buffer_s16_t& src, + const buffer_s16_t& dst ) { auto src_p = src.p; auto dst_p = dst.p; @@ -328,76 +785,6 @@ buffer_s16_t DecimateBy2CIC4Real::execute( return { dst.p, src.count / 2, src.sampling_rate / 2 }; } -#if 0 -buffer_c16_t DecimateBy2HBF5Complex::execute( - buffer_c16_t const src, - buffer_c16_t const dst -) { - auto src_p = src.p; - auto dst_p = dst.p; - int32_t n = src.count; - for(; n>0; n-=2) { - /* TODO: Probably a lot of room to optimize... */ - z[0] = z[2]; - //z[1] = z[3]; - z[2] = z[4]; - //z[3] = z[5]; - z[4] = z[6]; - z[5] = z[7]; - z[6] = z[8]; - z[7] = z[9]; - z[8] = z[10]; - z[9] = *(src_p++); - z[10] = *(src_p++); - int32_t t_real { z[5].real * 256 }; - int32_t t_imag { z[5].imag * 256 }; - t_real += (z[ 0].real + z[10].real) * 3; - t_imag += (z[ 0].imag + z[10].imag) * 3; - t_real -= (z[ 2].real + z[ 8].real) * 25; - t_imag -= (z[ 2].imag + z[ 8].imag) * 25; - t_real += (z[ 4].real + z[ 6].real) * 150; - t_imag += (z[ 4].imag + z[ 6].imag) * 150; - *(dst_p++) = { t_real / 256, t_imag / 256 }; - } - - return { dst.p, src.count / 2, src.sampling_rate / 2 }; -} - -buffer_c16_t DecimateBy2HBF7Complex::execute( - buffer_c16_t const src, - buffer_c16_t const dst -) { - auto src_p = src.p; - auto dst_p = dst.p; - int32_t n = src.count; - for(; n>0; n-=2) { - /* TODO: Probably a lot of room to optimize... */ - z[0] = z[2]; - //z[1] = z[3]; - z[2] = z[4]; - //z[3] = z[5]; - z[4] = z[6]; - z[5] = z[7]; - z[6] = z[8]; - z[7] = z[9]; - z[8] = z[10]; - z[9] = *(src_p++); - z[10] = *(src_p++); - - int32_t t_real { z[5].real * 512 }; - int32_t t_imag { z[5].imag * 512 }; - t_real += (z[ 0].real + z[10].real) * 7; - t_imag += (z[ 0].imag + z[10].imag) * 7; - t_real -= (z[ 2].real + z[ 8].real) * 53; - t_imag -= (z[ 2].imag + z[ 8].imag) * 53; - t_real += (z[ 4].real + z[ 6].real) * 302; - t_imag += (z[ 4].imag + z[ 6].imag) * 302; - *(dst_p++) = { t_real / 512, t_imag / 512 }; - } - - return { dst.p, src.count / 2, src.sampling_rate / 2 }; -} -#endif } /* namespace decimate */ } /* namespace dsp */ diff --git a/firmware/baseband-tx/dsp_decimate.hpp b/firmware/baseband-tx/dsp_decimate.hpp index 9cc90b88..52727075 100644 --- a/firmware/baseband-tx/dsp_decimate.hpp +++ b/firmware/baseband-tx/dsp_decimate.hpp @@ -31,14 +31,28 @@ #include "dsp_types.hpp" +#include "simd.hpp" + namespace dsp { namespace decimate { +class Complex8DecimateBy2CIC3 { +public: + buffer_c16_t execute( + const buffer_c8_t& src, + const buffer_c16_t& dst + ); + +private: + uint32_t _i1_i0 { 0 }; + uint32_t _q1_q0 { 0 }; +}; + class TranslateByFSOver4AndDecimateBy2CIC3 { public: buffer_c16_t execute( - buffer_c8_t src, - buffer_c16_t dst + const buffer_c8_t& src, + const buffer_c16_t& dst ); private: @@ -49,8 +63,8 @@ private: class DecimateBy2CIC3 { public: buffer_c16_t execute( - buffer_c16_t src, - buffer_c16_t dst + const buffer_c16_t& src, + const buffer_c16_t& dst ); private: @@ -62,20 +76,126 @@ class FIR64AndDecimateBy2Real { public: static constexpr size_t taps_count = 64; - FIR64AndDecimateBy2Real( + void configure( const std::array& taps - ) : taps(taps) - { - } + ); buffer_s16_t execute( - buffer_s16_t src, - buffer_s16_t dst + const buffer_s16_t& src, + const buffer_s16_t& dst ); private: std::array z; - const std::array& taps; + std::array taps; +}; + +class FIRC8xR16x24FS4Decim4 { +public: + static constexpr size_t taps_count = 24; + static constexpr size_t decimation_factor = 4; + + using sample_t = complex8_t; + using tap_t = int16_t; + + enum class Shift : bool { + Down = true, + Up = false + }; + + void configure( + const std::array& taps, + const int32_t scale, + const Shift shift = Shift::Down + ); + + buffer_c16_t execute( + const buffer_c8_t& src, + const buffer_c16_t& dst + ); + +private: + std::array z_; + std::array taps_; + int32_t output_scale = 0; +}; + +class FIRC8xR16x24FS4Decim8 { +public: + static constexpr size_t taps_count = 24; + static constexpr size_t decimation_factor = 8; + + using sample_t = complex8_t; + using tap_t = int16_t; + + enum class Shift : bool { + Down = true, + Up = false + }; + + void configure( + const std::array& taps, + const int32_t scale, + const Shift shift = Shift::Down + ); + + buffer_c16_t execute( + const buffer_c8_t& src, + const buffer_c16_t& dst + ); + +private: + std::array z_; + std::array taps_; + int32_t output_scale = 0; +}; + +class FIRC16xR16x16Decim2 { +public: + static constexpr size_t taps_count = 16; + static constexpr size_t decimation_factor = 2; + + using sample_t = complex16_t; + using tap_t = int16_t; + + void configure( + const std::array& taps, + const int32_t scale + ); + + buffer_c16_t execute( + const buffer_c16_t& src, + const buffer_c16_t& dst + ); + +private: + std::array z_; + std::array taps_; + int32_t output_scale = 0; +}; + +class FIRC16xR16x32Decim8 { +public: + static constexpr size_t taps_count = 32; + static constexpr size_t decimation_factor = 8; + + using sample_t = complex16_t; + using tap_t = int16_t; + + void configure( + const std::array& taps, + const int32_t scale + ); + + buffer_c16_t execute( + const buffer_c16_t& src, + const buffer_c16_t& dst + ); + +private: + std::array z_; + std::array taps_; + int32_t output_scale = 0; }; class FIRAndDecimateComplex { @@ -99,16 +219,12 @@ public: const T& taps, const size_t decimation_factor ) { - samples_ = std::make_unique(taps.size()); - taps_reversed_ = std::make_unique(taps.size()); - taps_count_ = taps.size(); - decimation_factor_ = decimation_factor; - std::reverse_copy(taps.cbegin(), taps.cend(), &taps_reversed_[0]); + configure(taps.data(), taps.size(), decimation_factor); } buffer_c16_t execute( - buffer_c16_t src, - buffer_c16_t dst + const buffer_c16_t& src, + const buffer_c16_t& dst ); private: @@ -118,124 +234,34 @@ private: std::unique_ptr taps_reversed_; size_t taps_count_; size_t decimation_factor_; + + template + void configure( + const T* const taps, + const size_t taps_count, + const size_t decimation_factor + ) { + configure_common(taps_count, decimation_factor); + std::reverse_copy(&taps[0], &taps[taps_count], &taps_reversed_[0]); + } + + void configure_common( + const size_t taps_count, + const size_t decimation_factor + ); }; class DecimateBy2CIC4Real { public: buffer_s16_t execute( - buffer_s16_t src, - buffer_s16_t dst + const buffer_s16_t& src, + const buffer_s16_t& dst ); private: int16_t z[5]; }; -#if 0 -class DecimateBy2HBF5Complex { -public: - buffer_c16_t execute( - buffer_c16_t const src, - buffer_c16_t const dst - ); -private: - complex16_t z[11]; -}; - -class DecimateBy2HBF7Complex { -public: - buffer_c16_t execute( - buffer_c16_t const src, - buffer_c16_t const dst - ); - -private: - complex16_t z[11]; -}; -#endif -/* From http://www.dspguru.com/book/export/html/3 - -Here are several basic techniques to fake circular buffers: - -Split the calculation: You can split any FIR calculation into its "pre-wrap" -and "post-wrap" parts. By splitting the calculation into these two parts, you -essentially can do the circular logic only once, rather than once per tap. -(See fir_double_z in FirAlgs.c above.) - -Duplicate the delay line: For a FIR with N taps, use a delay line of size 2N. -Copy each sample to its proper location, as well as at location-plus-N. -Therefore, the FIR calculation's MAC loop can be done on a flat buffer of N -points, starting anywhere within the first set of N points. The second set of -N delayed samples provides the "wrap around" comparable to a true circular -buffer. (See fir_double_z in FirAlgs.c above.) - -Duplicate the coefficients: This is similar to the above, except that the -duplication occurs in terms of the coefficients, not the delay line. -Compared to the previous method, this has a calculation advantage of not -having to store each incoming sample twice, and it also has a memory -advantage when the same coefficient set will be used on multiple delay lines. -(See fir_double_h in FirAlgs.c above.) - -Use block processing: In block processing, you use a delay line which is a -multiple of the number of taps. You therefore only have to move the data -once per block to implement the delay-line mechanism. When the block size -becomes "large", the overhead of a moving the delay line once per block -becomes negligible. -*/ - -#if 0 -template -class FIRAndDecimateBy2Complex { -public: - FIR64AndDecimateBy2Complex( - const std::array& taps - ) : taps { taps } - { - } - - buffer_c16_t execute( - buffer_c16_t const src, - buffer_c16_t const dst - ) { - /* int16_t input (sample count "n" must be multiple of 4) - * -> int16_t output, decimated by 2. - * taps are normalized to 1 << 16 == 1.0. - */ - - return { dst.p, src.count / 2 }; - } - -private: - std::array z; - const std::array& taps; - - complex process_one(const size_t start_offset) { - const auto split = &z[start_offset]; - const auto end = &z[z.size()]; - auto tap = &taps[0]; - - complex t { 0, 0 }; - - auto p = split; - while(p < end) { - const auto t = *(tap++); - const auto c = *(p++); - t.real += c.real * t; - t.imag += c.imag * t; - } - - p = &z[0]; - while(p < split) { - const auto t = *(tap++); - const auto c = *(p++); - t.real += c.real * t; - t.imag += c.imag * t; - } - - return { t.real / 65536, t.imag / 65536 }; - } -}; -#endif } /* namespace decimate */ } /* namespace dsp */ diff --git a/firmware/baseband-tx/dsp_demodulate.cpp b/firmware/baseband-tx/dsp_demodulate.cpp index 4ee6ed68..cf79d0d0 100644 --- a/firmware/baseband-tx/dsp_demodulate.cpp +++ b/firmware/baseband-tx/dsp_demodulate.cpp @@ -30,34 +30,37 @@ namespace dsp { namespace demodulate { -buffer_s16_t AM::execute( - buffer_c16_t src, - buffer_s16_t dst +buffer_f32_t AM::execute( + const buffer_c16_t& src, + const buffer_f32_t& dst ) { - /* Intermediate maximum value: 46341 (when input is -32768,-32768). */ - /* Normalized to maximum 32767 for int16_t representation. */ - const auto src_p = src.p; const auto src_end = &src.p[src.count]; auto dst_p = dst.p; while(src_p < src_end) { - // const auto s = *(src_p++); - // const uint32_t r_sq = s.real() * s.real(); - // const uint32_t i_sq = s.imag() * s.imag(); - // const uint32_t mag_sq = r_sq + i_sq; const uint32_t sample0 = *__SIMD32(src_p)++; const uint32_t sample1 = *__SIMD32(src_p)++; const uint32_t mag_sq0 = __SMUAD(sample0, sample0); const uint32_t mag_sq1 = __SMUAD(sample1, sample1); - const int32_t mag0_int = __builtin_sqrtf(mag_sq0); - const int32_t mag0_sat = __SSAT(mag0_int, 16); - const int32_t mag1_int = __builtin_sqrtf(mag_sq1); - const int32_t mag1_sat = __SSAT(mag1_int, 16); - *__SIMD32(dst_p)++ = __PKHBT( - mag0_sat, - mag1_sat, - 16 - ); + *(dst_p++) = __builtin_sqrtf(mag_sq0) * k; + *(dst_p++) = __builtin_sqrtf(mag_sq1) * k; + } + + return { dst.p, src.count, src.sampling_rate }; +} + +buffer_f32_t SSB::execute( + const buffer_c16_t& src, + const buffer_f32_t& dst +) { + const complex16_t* src_p = src.p; + const auto src_end = &src.p[src.count]; + auto dst_p = dst.p; + while(src_p < src_end) { + *(dst_p++) = (src_p++)->real() * k; + *(dst_p++) = (src_p++)->real() * k; + *(dst_p++) = (src_p++)->real() * k; + *(dst_p++) = (src_p++)->real() * k; } return { dst.p, src.count, src.sampling_rate }; @@ -69,17 +72,21 @@ static inline float angle_approx_4deg0(const complex32_t t) { } */ static inline float angle_approx_0deg27(const complex32_t t) { - const auto x = static_cast(t.imag()) / static_cast(t.real()); - return x / (1.0f + 0.28086f * x * x); + if( t.real() ) { + const auto x = static_cast(t.imag()) / static_cast(t.real()); + return x / (1.0f + 0.28086f * x * x); + } else { + return (t.imag() < 0) ? -1.5707963268f : 1.5707963268f; + } } -/* + static inline float angle_precise(const complex32_t t) { return atan2f(t.imag(), t.real()); } -*/ -buffer_s16_t FM::execute( - buffer_c16_t src, - buffer_s16_t dst + +buffer_f32_t FM::execute( + const buffer_c16_t& src, + const buffer_f32_t& dst ) { auto z = z_; @@ -92,9 +99,32 @@ buffer_s16_t FM::execute( const auto t0 = multiply_conjugate_s16_s32(s0, z); const auto t1 = multiply_conjugate_s16_s32(s1, s0); z = s1; - const int32_t theta0_int = angle_approx_0deg27(t0) * k; + *(dst_p++) = angle_precise(t0) * kf; + *(dst_p++) = angle_precise(t1) * kf; + } + z_ = z; + + return { dst.p, src.count, src.sampling_rate }; +} + +buffer_s16_t FM::execute( + const buffer_c16_t& src, + const buffer_s16_t& dst +) { + auto z = z_; + + const auto src_p = src.p; + const auto src_end = &src.p[src.count]; + auto dst_p = dst.p; + while(src_p < src_end) { + const auto s0 = *__SIMD32(src_p)++; + const auto s1 = *__SIMD32(src_p)++; + const auto t0 = multiply_conjugate_s16_s32(s0, z); + const auto t1 = multiply_conjugate_s16_s32(s1, s0); + z = s1; + const int32_t theta0_int = angle_approx_0deg27(t0) * ks16; const int32_t theta0_sat = __SSAT(theta0_int, 16); - const int32_t theta1_int = angle_approx_0deg27(t1) * k; + const int32_t theta1_int = angle_approx_0deg27(t1) * ks16; const int32_t theta1_sat = __SSAT(theta1_int, 16); *__SIMD32(dst_p)++ = __PKHBT( theta0_sat, @@ -107,5 +137,15 @@ buffer_s16_t FM::execute( return { dst.p, src.count, src.sampling_rate }; } +void FM::configure(const float sampling_rate, const float deviation_hz) { + /* + * angle: -pi to pi. output range: -32768 to 32767. + * Maximum delta-theta (output of atan2) at maximum deviation frequency: + * delta_theta_max = 2 * pi * deviation / sampling_rate + */ + kf = static_cast(1.0f / (2.0 * pi * deviation_hz / sampling_rate)); + ks16 = 32767.0f * kf; +} + } } diff --git a/firmware/baseband-tx/dsp_demodulate.hpp b/firmware/baseband-tx/dsp_demodulate.hpp index c5f871e1..72706167 100644 --- a/firmware/baseband-tx/dsp_demodulate.hpp +++ b/firmware/baseband-tx/dsp_demodulate.hpp @@ -29,39 +29,44 @@ namespace demodulate { class AM { public: - buffer_s16_t execute( - buffer_c16_t src, - buffer_s16_t dst + buffer_f32_t execute( + const buffer_c16_t& src, + const buffer_f32_t& dst ); + +private: + static constexpr float k = 1.0f / 32768.0f; +}; + +class SSB { +public: + buffer_f32_t execute( + const buffer_c16_t& src, + const buffer_f32_t& dst + ); + +private: + static constexpr float k = 1.0f / 32768.0f; }; class FM { public: - /* - * angle: -pi to pi. output range: -32768 to 32767. - * Maximum delta-theta (output of atan2) at maximum deviation frequency: - * delta_theta_max = 2 * pi * deviation / sampling_rate - */ - constexpr FM( - const float sampling_rate, - const float deviation_hz - ) : z_ { 0 }, - k { static_cast(32767.0f / (2.0 * pi * deviation_hz / sampling_rate)) } - { - } - - buffer_s16_t execute( - buffer_c16_t src, - buffer_s16_t dst + buffer_f32_t execute( + const buffer_c16_t& src, + const buffer_f32_t& dst ); - void configure(const float sampling_rate, const float deviation_hz) { - k = static_cast(32767.0f / (2.0 * pi * deviation_hz / sampling_rate)); - } + buffer_s16_t execute( + const buffer_c16_t& src, + const buffer_s16_t& dst + ); + + void configure(const float sampling_rate, const float deviation_hz); private: - complex16_t::rep_type z_; - float k; + complex16_t::rep_type z_ { 0 }; + float kf { 0 }; + float ks16 { 0 }; }; } /* namespace demodulate */ diff --git a/firmware/baseband-tx/dsp_iir.hpp b/firmware/baseband-tx/dsp_iir.hpp deleted file mode 100644 index 0ecc996a..00000000 --- a/firmware/baseband-tx/dsp_iir.hpp +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. - * - * 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 __DSP_IIR_H__ -#define __DSP_IIR_H__ - -#include - -#include "dsp_types.hpp" - -struct iir_biquad_config_t { - std::array b; - std::array a; -}; - -constexpr iir_biquad_config_t iir_config_passthrough { - { { 1.0f, 0.0f, 0.0f } }, - { { 0.0f, 0.0f, 0.0f } }, -}; - -constexpr iir_biquad_config_t iir_config_no_pass { - { { 0.0f, 0.0f, 0.0f } }, - { { 0.0f, 0.0f, 0.0f } }, -}; - -class IIRBiquadFilter { -public: - // http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt - constexpr IIRBiquadFilter( - ) : IIRBiquadFilter(iir_config_no_pass) - { - } - - // Assume all coefficients are normalized so that a0=1.0 - constexpr IIRBiquadFilter( - const iir_biquad_config_t& config - ) : config(config) - { - } - - void configure(const iir_biquad_config_t& new_config); - - void execute(const buffer_f32_t& buffer_in, const buffer_f32_t& buffer_out); - void execute_in_place(const buffer_f32_t& buffer); - -private: - iir_biquad_config_t config; - std::array x { { 0.0f, 0.0f, 0.0f } }; - std::array y { { 0.0f, 0.0f, 0.0f } }; -}; - -#endif/*__DSP_IIR_H__*/ diff --git a/firmware/baseband-tx/dsp_iir_config.hpp b/firmware/baseband-tx/dsp_iir_config.hpp deleted file mode 100644 index e20a51fc..00000000 --- a/firmware/baseband-tx/dsp_iir_config.hpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. - * - * 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 __DSP_IIR_CONFIG_H__ -#define __DSP_IIR_CONFIG_H__ - -#include "dsp_iir.hpp" - -constexpr iir_biquad_config_t audio_hpf_config { - { 0.93346032f, -1.86687724f, 0.93346032f }, - { 1.0f , -1.97730264f, 0.97773668f } -}; - -constexpr iir_biquad_config_t non_audio_hpf_config { - { 0.51891061f, -0.95714180f, 0.51891061f }, - { 1.0f , -0.79878302f, 0.43960231f } -}; - -#endif/*__DSP_IIR_CONFIG_H__*/ diff --git a/firmware/baseband-tx/event_m4.cpp b/firmware/baseband-tx/event_m4.cpp index 0e90cd15..797ef5cb 100644 --- a/firmware/baseband-tx/event_m4.cpp +++ b/firmware/baseband-tx/event_m4.cpp @@ -86,11 +86,9 @@ void EventDispatcher::dispatch(const eventmask_t events) { } void EventDispatcher::handle_baseband_queue() { - std::array message_buffer; - while(Message* const message = shared_memory.baseband_queue.peek(message_buffer)) { - on_message(message); - shared_memory.baseband_queue.skip(); - } + shared_memory.baseband_queue.handle([this](Message* const message) { + this->on_message(message); + }); } void EventDispatcher::on_message(const Message* const message) { diff --git a/firmware/baseband-tx/halconf.h b/firmware/baseband-tx/halconf.h index 658d5869..c157afbd 100755 --- a/firmware/baseband-tx/halconf.h +++ b/firmware/baseband-tx/halconf.h @@ -261,7 +261,7 @@ * lower priority, this may slow down the driver a bit however. */ #if !defined(SDC_NICE_WAITING) || defined(__DOXYGEN__) -#define SDC_NICE_WAITING TRUE +#define SDC_NICE_WAITING FALSE #endif /*===========================================================================*/ diff --git a/firmware/baseband-tx/main.cpp b/firmware/baseband-tx/main.cpp index a1940851..5f8a7da0 100755 --- a/firmware/baseband-tx/main.cpp +++ b/firmware/baseband-tx/main.cpp @@ -72,19 +72,10 @@ void __late_init(void) { } static void init() { - i2s::i2s0::configure( - audio::i2s0_config_tx, - audio::i2s0_config_rx, - audio::i2s0_config_dma - ); - audio::dma::init(); audio::dma::configure(); audio::dma::enable(); - i2s::i2s0::tx_start(); - i2s::i2s0::rx_start(); - LPC_CREG->DMAMUX = portapack::gpdma_mux; gpdma::controller.enable(); nvicEnableVector(DMA_IRQn, CORTEX_PRIORITY_MASK(LPC_DMA_IRQ_PRIORITY)); @@ -128,185 +119,3 @@ int main(void) { return 0; } - -/* - void run() override { - while(true) { - if (direction == baseband::Direction::Transmit) { - const auto buffer_tmp = baseband::dma::wait_for_tx_buffer(); - - const buffer_c8_t buffer { - buffer_tmp.p, buffer_tmp.count, baseband_configuration.sampling_rate - }; - - if( baseband_processor ) { - baseband_processor->execute(buffer); - } - } else { - const auto buffer_tmp = baseband::dma::wait_for_rx_buffer(); - - const buffer_c8_t buffer { - buffer_tmp.p, buffer_tmp.count, baseband_configuration.sampling_rate - }; - - if( baseband_processor ) { - baseband_processor->execute(buffer); - } - } - } - } -}; - -class ToneProcessor : public BasebandProcessor { -public: - void execute(buffer_c8_t buffer) override { - - for (size_t i = 0; i= 9) { - s = 0; - aphase += 353205; // DEBUG - //sample = sintab[(aphase & 0x03FF0000)>>16]; - } else { - s++; - } - - //sample = sintab[(aphase & 0x03FF0000)>>16]; - - //FM - frq = sample * 500; // DEBUG - - phase = (phase + frq); - sphase = phase + (256<<16); - - //re = sintab[(sphase & 0x03FF0000)>>16]; - //im = sintab[(phase & 0x03FF0000)>>16]; - - buffer.p[i] = {(int8_t)re,(int8_t)im}; - } - } - -private: - int8_t re, im; - uint8_t s; - uint32_t sample_count; - uint32_t aphase, phase, sphase; - int32_t sample, sig, frq; -}; - -char ram_loop[32]; -typedef int (*fn_ptr)(void); -fn_ptr loop_ptr; - -void ram_loop_fn(void) { - while(1) {} -} - -void wait_for_switch(void) { - memcpy(&ram_loop[0], reinterpret_cast(&ram_loop_fn), 32); - loop_ptr = reinterpret_cast(&ram_loop[0]); - ReadyForSwitchMessage message; - shared_memory.application_queue.push(message); - (*loop_ptr)(); -} - -int main(void) { - init(); - - events_initialize(chThdSelf()); - m0apptxevent_interrupt_enable(); - - EventDispatcher event_dispatcher; - auto& message_handlers = event_dispatcher.message_handlers(); - - message_handlers.register_handler(Message::ID::ModuleID, - [](Message* p) { - ModuleIDMessage reply; - auto message = static_cast(p); - if (message->query == true) { // Shouldn't be needed - memcpy(reply.md5_signature, (const void *)(0x10087FF0), 16); - reply.query = false; - shared_memory.application_queue.push(reply); - } - } - ); - - message_handlers.register_handler(Message::ID::BasebandConfiguration, - [&message_handlers](const Message* const p) { - auto message = reinterpret_cast(p); - if( message->configuration.mode != baseband_thread.baseband_configuration.mode ) { - - if( baseband_thread.baseband_processor ) { - i2s::i2s0::tx_mute(); - baseband::dma::disable(); - } - - // TODO: Timing problem around disabling DMA and nulling and deleting old processor - auto old_p = baseband_thread.baseband_processor; - baseband_thread.baseband_processor = nullptr; - delete old_p; - - switch(message->configuration.mode) { - case TX_RDS: - direction = baseband::Direction::Transmit; - baseband_thread.baseband_processor = new RDSProcessor(); - break; - - case TX_LCR: - direction = baseband::Direction::Transmit; - baseband_thread.baseband_processor = new LCRFSKProcessor(); - break; - - case TX_TONE: - direction = baseband::Direction::Transmit; - baseband_thread.baseband_processor = new ToneProcessor(); - break; - - case TX_JAMMER: - direction = baseband::Direction::Transmit; - baseband_thread.baseband_processor = new JammerProcessor(); - break; - - case TX_XYLOS: - direction = baseband::Direction::Transmit; - baseband_thread.baseband_processor = new XylosProcessor(); - break; - - case PLAY_AUDIO: - direction = baseband::Direction::Transmit; - baseband_thread.baseband_processor = new PlayAudioProcessor(); - message_handlers.register_handler(Message::ID::FIFOData, - [](Message* p) { - auto message = static_cast(p); - baseband_thread.baseband_processor->fill_buffer(message->data); - } - ); - break; - - case SWITCH: - wait_for_switch(); - - default: - break; - } - - if( baseband_thread.baseband_processor ) - baseband::dma::enable(direction); - } - - baseband::dma::configure( - baseband_buffer->data(), - direction - ); - - baseband_thread.baseband_configuration = message->configuration; - } - ); - - message_handlers.register_handler(Message::ID::Shutdown, - [&event_dispatcher](const Message* const) { - event_dispatcher.request_stop(); - } - ); -*/ diff --git a/firmware/baseband-tx/matched_filter.cpp b/firmware/baseband-tx/matched_filter.cpp index 5040349a..9471a202 100644 --- a/firmware/baseband-tx/matched_filter.cpp +++ b/firmware/baseband-tx/matched_filter.cpp @@ -21,9 +21,27 @@ #include "matched_filter.hpp" +#include +#include + +#include "utility.hpp" + namespace dsp { namespace matched_filter { +void MatchedFilter::configure( + const tap_t* const taps, + const size_t taps_count, + const size_t decimation_factor +) { + samples_ = std::make_unique(taps_count); + taps_reversed_ = std::make_unique(taps_count); + taps_count_ = taps_count; + decimation_factor_ = decimation_factor; + output = 0; + std::reverse_copy(&taps[0], &taps[taps_count], &taps_reversed_[0]); +} + bool MatchedFilter::execute_once( const sample_t input ) { diff --git a/firmware/baseband-tx/matched_filter.hpp b/firmware/baseband-tx/matched_filter.hpp index 741f5018..49045db8 100644 --- a/firmware/baseband-tx/matched_filter.hpp +++ b/firmware/baseband-tx/matched_filter.hpp @@ -22,17 +22,10 @@ #ifndef __MATCHED_FILTER_H__ #define __MATCHED_FILTER_H__ -#include "utility.hpp" - #include - #include -#include #include -#include -#include - namespace dsp { namespace matched_filter { @@ -61,11 +54,7 @@ public: const T& taps, size_t decimation_factor ) { - samples_ = std::make_unique(taps.size()); - taps_reversed_ = std::make_unique(taps.size()); - taps_count_ = taps.size(); - decimation_factor_ = decimation_factor; - std::reverse_copy(taps.cbegin(), taps.cend(), &taps_reversed_[0]); + configure(taps.data(), taps.size(), decimation_factor); } bool execute_once(const sample_t input); @@ -82,7 +71,7 @@ private: size_t taps_count_ { 0 }; size_t decimation_factor_ { 1 }; size_t decimation_phase { 0 }; - float output; + float output { 0 }; void shift_by_decimation_factor(); @@ -93,6 +82,12 @@ private: bool is_new_decimation_cycle() const { return (decimation_phase == 0); } + + void configure( + const tap_t* const taps, + const size_t taps_count, + const size_t decimation_factor + ); }; } /* namespace matched_filter */ diff --git a/firmware/baseband-tx/packet_builder.hpp b/firmware/baseband-tx/packet_builder.hpp index edf50357..c8a9ea5b 100644 --- a/firmware/baseband-tx/packet_builder.hpp +++ b/firmware/baseband-tx/packet_builder.hpp @@ -28,19 +28,33 @@ #include #include "bit_pattern.hpp" +#include "baseband_packet.hpp" + +struct NeverMatch { + bool operator()(const BitHistory&, const size_t) const { + return false; + } +}; + +struct FixedLength { + bool operator()(const BitHistory&, const size_t symbols_received) const { + return symbols_received >= length; + } + + const size_t length; +}; template class PacketBuilder { public: - using PayloadType = std::bitset<1024>; - using PayloadHandlerFunc = std::function; + using PayloadHandlerFunc = std::function; PacketBuilder( const PreambleMatcher preamble_matcher, const UnstuffMatcher unstuff_matcher, const EndMatcher end_matcher, - const PayloadHandlerFunc payload_handler - ) : payload_handler { payload_handler }, + PayloadHandlerFunc payload_handler + ) : payload_handler { std::move(payload_handler) }, preamble(preamble_matcher), unstuff(unstuff_matcher), end(end_matcher) @@ -64,18 +78,24 @@ public: switch(state) { case State::Preamble: - if( preamble(bit_history, bits_received) ) { + if( preamble(bit_history, packet.size()) ) { state = State::Payload; } break; case State::Payload: - if( !unstuff(bit_history, bits_received) ) { - payload[bits_received++] = symbol; + if( !unstuff(bit_history, packet.size()) ) { + packet.add(symbol); } - if( end(bit_history, bits_received) ) { - payload_handler(payload, bits_received); + if( end(bit_history, packet.size()) ) { + // NOTE: This check is to avoid std::function nullptr check, which + // brings in "_ZSt25__throw_bad_function_callv" and a lot of extra code. + // TODO: Make payload_handler known at compile time. + if( payload_handler ) { + packet.set_timestamp(Timestamp::now()); + payload_handler(packet); + } reset_state(); } else { if( packet_truncated() ) { @@ -97,7 +117,7 @@ private: }; bool packet_truncated() const { - return bits_received >= payload.size(); + return packet.size() >= packet.capacity(); } const PayloadHandlerFunc payload_handler; @@ -107,12 +127,11 @@ private: UnstuffMatcher unstuff; EndMatcher end; - size_t bits_received { 0 }; State state { State::Preamble }; - PayloadType payload; + baseband::Packet packet; void reset_state() { - bits_received = 0; + packet.clear(); state = State::Preamble; } }; diff --git a/firmware/baseband-tx/proc_audiotx.cpp b/firmware/baseband-tx/proc_audiotx.cpp index c9c9c4bd..a619a620 100644 --- a/firmware/baseband-tx/proc_audiotx.cpp +++ b/firmware/baseband-tx/proc_audiotx.cpp @@ -23,9 +23,84 @@ #include "proc_audiotx.hpp" #include "portapack_shared_memory.hpp" #include "sine_table.hpp" +#include "audio_output.hpp" +#include "lfsr_random.hpp" #include -void AudioTXProcessor::execute(const buffer_c8_t& buffer) { +uint32_t lfsr(uint32_t v) { + enum { + length = 31, + tap_0 = 31, + tap_1 = 18, + shift_amount_0 = 12, + shift_amount_1 = 12, + shift_amount_2 = 8 + }; + + const lfsr_word_t zero = 0; + v = ( + ( + v << shift_amount_0 + ) | ( + ( + (v >> (tap_0 - shift_amount_0)) ^ + (v >> (tap_1 - shift_amount_0)) + ) & ( + ~(~zero << shift_amount_0) + ) + ) + ); + v = ( + ( + v << shift_amount_1 + ) | ( + ( + (v >> (tap_0 - shift_amount_1)) ^ + (v >> (tap_1 - shift_amount_1)) + ) & ( + ~(~zero << shift_amount_1) + ) + ) + ); + v = ( + ( + v << shift_amount_2 + ) | ( + ( + (v >> (tap_0 - shift_amount_2)) ^ + (v >> (tap_1 - shift_amount_2)) + ) & ( + ~(~zero << shift_amount_2) + ) + ) + ); + + return v; +} + +void AudioTXProcessor::execute(const buffer_c8_t& buffer){ + + for (size_t i = 0; i>18]*127); //(int8_t)lfsr(sample + i); + + if (bc & 0x40) + aphase += 60000; + else + aphase += 90000; + + //FM + frq = sample * 2500; + + phase = (phase + frq); + sphase = phase + (256<<16); + + re = (sine_table_f32[(sphase & 0x03FF0000)>>18]*127); + im = (sine_table_f32[(phase & 0x03FF0000)>>18]*127); + + buffer.p[i] = {(int8_t)re,(int8_t)im}; + } + + bc++; } diff --git a/firmware/baseband-tx/proc_audiotx.hpp b/firmware/baseband-tx/proc_audiotx.hpp index 53c639e2..88a5d99e 100644 --- a/firmware/baseband-tx/proc_audiotx.hpp +++ b/firmware/baseband-tx/proc_audiotx.hpp @@ -25,23 +25,25 @@ #include "baseband_processor.hpp" -#define SAMPLERATE 44100/4 +#include "dsp_decimate.hpp" +#include "dsp_demodulate.hpp" + +#include "audio_output.hpp" +#include "spectrum_collector.hpp" + +#include class AudioTXProcessor : public BasebandProcessor { public: void execute(const buffer_c8_t& buffer) override; - private: - int8_t audio_fifo[SAMPLERATE]; - int8_t re, im; uint8_t s, as = 0, ai; uint8_t byte_pos = 0; uint8_t digit = 0; uint32_t aphase, phase, sphase; - int32_t sample, frq; - TXDoneMessage message; + int32_t sample, frq, bc; }; #endif diff --git a/firmware/baseband-tx/proc_playaudio.cpp b/firmware/baseband-tx/proc_playaudio.cpp index c9fcaba3..a9aa6e56 100644 --- a/firmware/baseband-tx/proc_playaudio.cpp +++ b/firmware/baseband-tx/proc_playaudio.cpp @@ -23,14 +23,17 @@ #include "proc_playaudio.hpp" #include "portapack_shared_memory.hpp" #include "sine_table.hpp" +#include "audio_output.hpp" #include -// This is diry :( -void PlayAudioProcessor::fill_buffer(int8_t * inptr) { - memcpy(&audio_fifo[fifo_put], inptr, 1024); - fifo_put = (fifo_put + 1024) & 0x0FFF; - asked = false; +void PlayAudioProcessor::on_message(const Message* const msg) { + if (msg->id == Message::ID::FIFOData) { + const auto message = static_cast(msg); + memcpy(&audio_fifo[fifo_put], message->data, 1024); + fifo_put = (fifo_put + 1024) & 0x0FFF; + asked = false; + } } void PlayAudioProcessor::execute(const buffer_c8_t& buffer){ @@ -69,5 +72,5 @@ void PlayAudioProcessor::execute(const buffer_c8_t& buffer){ buffer.p[i] = {(int8_t)re,(int8_t)im}; } - //fill_audio_buffer(preview_audio_buffer); + //AudioOutput::fill_audio_buffer(preview_audio_buffer, true); } diff --git a/firmware/baseband-tx/proc_playaudio.hpp b/firmware/baseband-tx/proc_playaudio.hpp index e07c7b01..b2f61474 100644 --- a/firmware/baseband-tx/proc_playaudio.hpp +++ b/firmware/baseband-tx/proc_playaudio.hpp @@ -28,7 +28,7 @@ class PlayAudioProcessor : public BasebandProcessor { public: void execute(const buffer_c8_t& buffer) override; - void fill_buffer(int8_t * inptr); + void on_message(const Message* const msg) override; private: int8_t audio_fifo[4096]; // Probably too much (=85ms @ 48000Hz) diff --git a/firmware/baseband-tx/rssi_dma.cpp b/firmware/baseband-tx/rssi_dma.cpp index 91f3b496..355698ad 100644 --- a/firmware/baseband-tx/rssi_dma.cpp +++ b/firmware/baseband-tx/rssi_dma.cpp @@ -33,6 +33,8 @@ using namespace lpc43xx; #include "portapack_dma.hpp" #include "portapack_adc.hpp" +#include "thread_wait.hpp" + namespace rf { namespace rssi { namespace dma { @@ -99,20 +101,19 @@ static buffers_config_t buffers_config; static sample_t *samples { nullptr }; static gpdma::channel::LLI *lli { nullptr }; -static Semaphore semaphore; -static volatile const gpdma::channel::LLI* next_lli = nullptr; +static ThreadWait thread_wait; static void transfer_complete() { - next_lli = gpdma_channel.next_lli(); - chSemSignalI(&semaphore); + const auto next_lli_index = gpdma_channel.next_lli() - &lli[0]; + thread_wait.wake_from_interrupt(next_lli_index); } static void dma_error() { + thread_wait.wake_from_interrupt(-1); disable(); } void init() { - chSemInit(&semaphore, 0); gpdma_channel.set_handlers(transfer_complete, dma_error); // LPC_GPDMA->SYNC |= (1 << gpdma_peripheral); @@ -147,8 +148,6 @@ void free() { void enable() { const auto gpdma_config = config(); gpdma_channel.configure(lli[0], gpdma_config); - - chSemReset(&semaphore, 0); gpdma_channel.enable(); } @@ -161,16 +160,11 @@ void disable() { } rf::rssi::buffer_t wait_for_buffer() { - const auto status = chSemWait(&semaphore); - if( status == RDY_OK ) { - const auto next = next_lli; - if( next ) { - const size_t next_index = next - &lli[0]; - const size_t free_index = (next_index + buffers_config.count - 2) % buffers_config.count; - return { reinterpret_cast(lli[free_index].destaddr), buffers_config.items_per_buffer }; - } else { - return { nullptr, 0 }; - } + const auto next_index = thread_wait.sleep(); + + if( next_index >= 0 ) { + const size_t free_index = (next_index + buffers_config.count - 2) % buffers_config.count; + return { reinterpret_cast(lli[free_index].destaddr), buffers_config.items_per_buffer }; } else { // TODO: Should I return here, or loop if RDY_RESET? return { nullptr, 0 }; diff --git a/firmware/baseband-tx/rssi_stats_collector.hpp b/firmware/baseband-tx/rssi_stats_collector.hpp index 6ab945bf..7a1cd494 100644 --- a/firmware/baseband-tx/rssi_stats_collector.hpp +++ b/firmware/baseband-tx/rssi_stats_collector.hpp @@ -31,7 +31,7 @@ class RSSIStatisticsCollector { public: template - void process(rf::rssi::buffer_t buffer, Callback callback) { + void process(const rf::rssi::buffer_t& buffer, Callback callback) { auto p = buffer.p; if( p == nullptr ) { return; diff --git a/firmware/baseband-tx/rssi_thread.hpp b/firmware/baseband-tx/rssi_thread.hpp index 0a00ef8e..5233ddd5 100644 --- a/firmware/baseband-tx/rssi_thread.hpp +++ b/firmware/baseband-tx/rssi_thread.hpp @@ -30,11 +30,6 @@ class RSSIThread : public ThreadBase { public: - RSSIThread( - ) : ThreadBase { "rssi" } - { - } - Thread* start(const tprio_t priority); private: diff --git a/firmware/baseband-tx/spectrum_collector.cpp b/firmware/baseband-tx/spectrum_collector.cpp index 2b661c9a..0d288bee 100644 --- a/firmware/baseband-tx/spectrum_collector.cpp +++ b/firmware/baseband-tx/spectrum_collector.cpp @@ -117,8 +117,8 @@ void SpectrumCollector::update() { // Three point Hamming window. const auto corrected_sample = channel_spectrum[i] * 0.54f + (channel_spectrum[(i-1) & 0xff] + channel_spectrum[(i+1) & 0xff]) * -0.23f; - const auto mag2 = magnitude_squared(corrected_sample); - const float db = complex16_mag_squared_to_dbv_norm(mag2); + const auto mag2 = magnitude_squared(corrected_sample * (1.0f / 32768.0f)); + const float db = mag2_to_dbv_norm(mag2); constexpr float mag_scale = 5.0f; const unsigned int v = (db * mag_scale) + 255.0f; spectrum.db[i] = std::max(0U, std::min(255U, v)); diff --git a/firmware/baseband-tx/spectrum_collector.hpp b/firmware/baseband-tx/spectrum_collector.hpp index 4c2cd542..b238c1fe 100644 --- a/firmware/baseband-tx/spectrum_collector.hpp +++ b/firmware/baseband-tx/spectrum_collector.hpp @@ -35,7 +35,8 @@ class SpectrumCollector { public: constexpr SpectrumCollector( - ) : channel_spectrum_decimator { 1 } + ) : channel_spectrum_decimator { 1 }, + fifo { fifo_data, ChannelSpectrumConfigMessage::fifo_k } { } @@ -50,8 +51,9 @@ public: ); private: - BlockDecimator<256> channel_spectrum_decimator; + BlockDecimator channel_spectrum_decimator; ChannelSpectrumFIFO fifo; + ChannelSpectrum fifo_data[1 << ChannelSpectrumConfigMessage::fifo_k]; volatile bool channel_spectrum_request_update { false }; bool streaming { false }; diff --git a/firmware/baseband-tx/thread_base.hpp b/firmware/baseband-tx/thread_base.hpp index 9b5e8a99..921aeeb9 100644 --- a/firmware/baseband-tx/thread_base.hpp +++ b/firmware/baseband-tx/thread_base.hpp @@ -26,24 +26,17 @@ class ThreadBase { public: - constexpr ThreadBase( - const char* const name - ) : name { name } - { - } - + virtual ~ThreadBase() = default; + protected: static msg_t fn(void* arg) { auto obj = static_cast(arg); - chRegSetThreadName(obj->name); obj->run(); return 0; } private: - const char* const name; - virtual void run() = 0; }; diff --git a/firmware/baseband.bin b/firmware/baseband.bin index 7cdb67763951ab8de168abeaf408712a93ce2c32..ab19dc42d8862b65674f575fa305a3b8d9a2e020 100644 GIT binary patch delta 70 zcmbQx!qm{hq!ke0tH8wYp`L*uC^b1Xvn;iUiva@6S-uBmq;)A2FZiCh=+PFdjatVV H1<(Zn6l59p delta 86 zcmZo@VVcmwq!ke0tH8v-(7?bDl$xBHS(aMF#Q*_bGxxpGtVz5idvI&q7O{1)8?}x# ZDtuzp+kfcYd_J>MzU}QAo_o<10RYY&ArAlm diff --git a/firmware/common/modules.h b/firmware/common/modules.h index d005b3f0..5810db69 100644 --- a/firmware/common/modules.h +++ b/firmware/common/modules.h @@ -1 +1,2 @@ -const char md5_baseband[16] = {0x4a,0x99,0xbe,0xec,0x29,0x7c,0x61,0xd2,0x1d,0xc1,0xb5,0x5e,0xb4,0x16,0xae,0x5d,}; +const char md5_baseband[16] = {0x37,0x04,0xf7,0x51,0x68,0x66,0x8a,0x20,0x73,0xa0,0xf7,0x69,0xa2,0xe2,0xb4,0x3a,}; +const char md5_baseband_tx[16] = {0x6c,0x1a,0x90,0x6b,0x68,0x78,0x6e,0xd2,0x08,0x3b,0x05,0xb1,0xbe,0x61,0xf8,0xe7,}; diff --git a/firmware/portapack-h1-firmware.bin b/firmware/portapack-h1-firmware.bin index b58bd15df3e4fcce28f099a57277bd4ec281c412..a28ebe8f4a3254a0bb4747b48f79aff2a80d6b1b 100644 GIT binary patch delta 66377 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

ph*doGWo!J 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%0A&Uc1ByX`X9Hz}D398I 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 Q$aYH$HvU)^g9%Iw0MVWdM*si- delta 45167 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 KWf3rAVgLY@QP6w< diff --git a/firmware/tools/make_baseband_file.py b/firmware/tools/make_baseband_file.py index 5e30acd5..50ac5031 100755 --- a/firmware/tools/make_baseband_file.py +++ b/firmware/tools/make_baseband_file.py @@ -49,7 +49,6 @@ name = bytearray() info = bytearray() description = bytearray() data_default_byte = bytearray((0,)) -m = md5.new() sys.argv = sys.argv[1:] @@ -60,6 +59,7 @@ sys.argv = sys.argv[1:] # MD5 (16) again, so that module code can read it (dirty...) for args in sys.argv: + m = md5.new() data = read_image(args + '/build/' + args + '.bin') data_r = data @@ -112,6 +112,6 @@ for args in sys.argv: h_data += 'const char md5_' + args.replace('-','_') + '[16] = {' + md5sum + '};\n' # Update original binary with MD5 footprint - write_file(data[512:(32768+512)], args + '/build/' + args + '.bin') + write_file(data[512:(32768+512)], args + '/build/' + args + '_inc.bin') write_file(h_data, 'common/modules.h') diff --git a/sdcard/baseband-tx.bin b/sdcard/baseband-tx.bin index bbf75555afcee1bfd1e2450e86c3822128bcaf9c..f456c721be28b4e5f21ebcb04db5527c0fe4ef42 100644 GIT binary patch delta 6333 zcmbQx!qm{hq!ke0tH8u?#*l%*wW1)kC^I)TuOu;ti-Cb5M`}WLMn&Ew4r|tp`x1XV z->7wqu|AQBfuWHZf)iC47z$Mx7#cwsDh87?WPzxG@fSkn9fcVf4vL`4!PF2z*TXF* zLNN{0)?>Qdkcol8k%<8e3byuaVEXXp2h#_xpG+SLep+~2dRclN{vjhMobCL9ky$u7 zL6J#;;lt;jOdmRbGJWX($@F3APa|(5Ph&5p$qcHD%1kT#Km0f0nB4nA$AEJp*Cght zEK^yhvQK53!uWyVCsVxw!-u~=i?%U*nDvYCgTW6qZT9G0YHS5McCX@{&p@a0js& z8aP0FB@q9I`s7nAO-zzMC;PFwNHTm_`+toS%cYPXEY>sJ*Es#UEaEHb^Z)y=$*Wl{ zV^0_|FtCU)2sG$12*@xpFo-ZRFo0+fX7phAuyz;2{sXGu2w-6NaO@|;hnC+AA8!3* zxX-YkV=Kc4-CqvM&7TE37(RUX$?zfUC&LGpUkp$Y`^k=Mri?<9%h?pUu81-SOk`$Y zn8Q4I5!)n2p~_+^uKOs6b+!nYqd=Q(wnEh-$C~%>+gDHWnO&k7ySNYHI zq3I{Xhk~CBAHMxz_+awaL6OIU;X}nQZZ95hfkZ|X=0uiKh7SopASNbl^Z&A?kYTct z8>=^)2g8S!UrZYW_JaHkau)*&gTO~t28I9;27w=289wm-6!sSOmCDq~n9Q2VXgoP_ zvIwU`5=i9{Q3e5!Gg!T)GlX}7Nac+z-mG3y35*|f_ZPVD5M>b9Bg!CfK$Jn?0xZ-R z7#Kh*(bbEhsehn4c^jvk6yt|K{}`2(69o1$>_;~4h3e#YVCe_{7_dlxP@SyCr69rh z;V@J;vfdx6ljFdW%b|LaB^lHvPvcTkV*JqYPdS6pSUCX_Wa##Ds7=1dC8xyrAp@k$ z8kaHwwaIeaQbLR$y!IhmBB3_fi(5vS@q_U`RBtJ$fx?r4fzewcf$@XNz5|mxxus2D z;S7>v@@7zqPLNKRl~CXgiUWbIEN=h5ulcWB+q{uao9_aH(!vZ5Z>7@7Z@8uEkNto1 zoMAsmnSmMuL%~)tUpBY@-xvSa$<)qJE0xR;_V7($RFW!H%F#?<+Ao;3gaZ{4yQ#atTzH&~be`s&j^J8`6cl-Z6>%U3n`^3c=t1@Oe>EHbS-Rr+{t@UO;ZB?*)RY2}Nz#~$x zS*n~P=dmb9G1D}W(O5Z!IU_qIAx9YG5+-S>iB5}C6mKZHX}Ilu@x$T&_lUhB9{<1l z{=fSC|97eX+O^di`Lua2Fet6e;PKWfmCl%z(dG2}QdCA(f_6e|f@s2zy&N9@znlJl z`<&h5|98Xx2$PKWf_+dg0P+GTs9@eG*ecdFhjXiBazvS z;Q`c&8%jBm8T@|A35-UG2{8#g2}&tO8BQsSbJB7YGa-(jEe&!!^G#6Def@_By6=0y z7K80|QLATQ_^_2hiQ%IPi~k2EK1Owhj|yxIAAMLG6B?5neuzA1FJ((&O>9YWNpdQ6 zN@hxKN@PlLN@+^@uoo2GAgufHhd+y(gOk7qW(HojiEcC8SU&o&Gkmz&@L!oxL%D$E zBO@2XhuaOz8p#DLADOruKHO+nT)^^?nalr!XFbn>k3QV~A3XSk)D&D8l>I;aH)1s2 z?Bwu+Mcenva?L!k&+e{9Hfy1pGhzXJWaq=OYt;Q~|??Onefth)d^kgTE{lac+S~KwEZQwU{2#D<3}E2Az^2XeF_591P>LH@7W`1o%67%RK@e_-J8 z{}5`$^O2D!&DHq_%j%yuid+j^xGpfeg(@-sVAP)J#4`Cazi_?7-~Zp={T11`_3N5? zgGTlz7DNy&w4hNAbQ1P0YZKO7nYQiRi(6j?s1FtdDAU}98QoRplTSh}E8 z(@iN!!R^QojtNJ8SWHk)Of5}KR!v@-sG5?RvNYxY_soC)zpMY%b^}!s3ZQh{zw=`~ z|LGe$J~Hs1zIgn(zsd(6zRzx7+!nbBtC>BwaO-;R|G}F_=ehp}FCNY34Eqz*7#IX6 z9~6+bd-#XjTP9Q0?O9^z4+Z{KC$XZMSmG|GcT;jV9+qRuqdNBW0n)! zP3GFB1Qu=A3#{7ulevWzeV!)%|Gwhy>Wn!~Oc&HV{(n#Uo18t@N$7$K)Vip>6JGEz zFe);ZN+mF51SkCeUikOw^W^N;32PEklMD7vc)`UWnZTIgop3C3a+R_6tt77vZcg%3hgZ;HI3wpcs>|Ea6c?N%H^io`3&;XNTIN;8w7g zL3we)SFlTu{K?5a19oeHIs-!t)SaL(w%E$z#_RU~d-I8N%+1_6c!Mu9h?3<58pwK>SC$iZ6p`~P?OKU|(%-W(ZH8INU^Kd>_kNlkW|yI)#@ zJAuWGLFNDVa;V7)ZUTEDW=)Jp|ltI8yVDc`}CZ-RflkLPjnD+dh+##04)Af4| zC@pI+Fa&5!{wB7p{@_lA{kktdII_5{{mM|w=fv8EW^w_|SOe z2hV}jj7J%Y8CQPrJjj+~G?H@QdBw~l_=BlI@CVNUzK={yKCd`5gdKK#WvE~JnweE& zzyBA83RhQ+{jP43E?RDkA3}c~ea*qd`j0*q1pZ%ennVsOJ$of%+f#E~g{}9C$2^ww; zA8!5<@tgWX#lZi=e}mZ@|9@xvcZi+EP58=U_Rm+DR&psj{$Q~Msf+y$Q^lbD`Xb20 zGe4L%6ztV>GsyV=-R=J~PzxbhltDm1j6ne0b_RI`iY>OnQWuBM|L+Yyl{2*ym=ykh zpZ!ZY^G@df@BPp=s)CyZxO`#G@ZKn}^=2ZA8$$-jhoH6@qk_O*3AY0G$vToUj3Sdg zCB1p({)8pjgrAcaNcz+VLe2UAz2_&Wy!!Wj#ePsbUqlSj!UQ>oV=Jf*V)gj{z3M-U z*QFlJum;@Fl6zFl;RZp|2=*`1K7+EF$MvU zdL}PsPiBw*-;?(d_~=j7$=Es{}? z5tXqqLpoDx0)r~3Psj36fJ0`Y#3Z@NVpGMZN=}uYDmztvDx<>LF4HKv6b8_7(U4U zyVEX^>UwW!T!hfy<3y0*g1RhvTOR!T;ai{b1e@`-9~-!~Tcj3=9iENerwILj2gu zT%XCCG1rO3jo;1m!r_eF8%@20Gn~ILI14BI|K9M=$m`M%5hLd>4atXoh!`<^*#GN6 z1}MS>HZo^KXUuT7&0yWksOxr7M}gr(hUb$|M#i?KpEER zpFG$sWvE%xU$8R>n6$j8XJ`1a*YVQ{28Iuje;q%aU}AIAbYu8%_^-H!K?2*?hCs(p zGZ@&uwgihAmL_aXFbrr=yqPGQ#P+o{$nnz>KF3ch7#u&XVHQ(#`@t7r8t^edwB*op zkl|ownzXzCnGeb|Tn-EZ0uDEH7#QY=F$i!NLPCH`GlNx&>%xHyfh`IfnKNR4uz26j zkZ=dl1@0_{|G(#havWIg*bIxS&W%6cS`{D6mUC!iV3V!uT7eGn({#&Cg zBmd?N9c~P&4&RPQI%l?J9{$|!#QwU8>C#(F#~Y zP<8e#W7XUr3<-08utX{}XR#GK72o*5u;IWDl>@>b9Qav2Ca~~*Jy9pJU)inLjr+AL zi*sUQ;(;GB3%GxXCL|Ty_`zbpXu8^I&5hDP%s8CUkiqRDEybzLe&hf5;vY=27?p(!cKl#4nDU%)_KXba3`R>v2mcQoe9S)> zAAm$8-J&x%GZ+mRt(hH8g)q+S$neZ)mhq62aNw3=*Y4QQHHmQ+=j1v255MNCXXctL zl+m&O><^U(AV=^ROS%cTaL(eM%%-eCYoT$~cU(CTDONPfqy% zUH^x}{+-W3<63$Tm(qJr{zyZ?izz44jKqC^De{>iMX25EKv|XTb z`wz$*4F&;3y%O~cRIf~c)+-YXIrcJWFhmG|6oV{+VOYfxyo+IfqdWscqC5kG0W?p5 z)cn{g<16E*k-+$y;X`Huj~l~>x?kEE1__%#{>z`TUxE=-V>5gxnOv^tE5V|{Ai$%+ zAOLcVD3YTm|It;60W}x}JpO+_{F6aL)sXP7VCD)E#57|?f>^>|L4A7aVVI4Q{SuJ zfOR_a*TzWBj{*#5e@HA~pUN@i><^U<|GrQBck>62!p$En0ylqfFmSmDNr`H)-uU-@ z>OYXNx7@Zr2Z`VO!Jq(1%p%OeAvb?;fOIh2{J|i=Xu)X2UDT9IqjFD;r|1WO}|9vm}gIp5`OlCBcS9+t3 znhcq|n7uFk$WR0gIWj0PIK0rD>|_|F_W%2eA1ohv7+Ag7Jomm3aQOdy!Hxt&28M%5 z3=EA*3=9vI7#J2RO+I9}jn6=jLEw!hgTMz}27wRSliQ3mgh2Vt_fwD($OJCW|KBx! zOg>;F!v!(`r22#IPr$7XP>?|Qci{Yg-^KQU z#KCxSuW_;#qsPDRdv`MIUjQ*Lj&XF2aXzij1mkCf)Wf2iV`5BH>)`*vG5tnFfcgEFfas4GcbGVPM!OH~E^Qkp<86)&o~*aIA$W$o0Qy`603{6U1ONa4 delta 12299 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 G><0kLDNMrv diff --git a/sdcard/baseband.bin b/sdcard/baseband.bin index 7cdb67763951ab8de168abeaf408712a93ce2c32..ab19dc42d8862b65674f575fa305a3b8d9a2e020 100644 GIT binary patch delta 70 zcmbQx!qm{hq!ke0tH8wYp`L*uC^b1Xvn;iUiva@6S-uBmq;)A2FZiCh=+PFdjatVV H1<(Zn6l59p delta 86 zcmZo@VVcmwq!ke0tH8v-(7?bDl$xBHS(aMF#Q*_bGxxpGtVz5idvI&q7O{1)8?}x# ZDtuzp+kfcYd_J>MzU}QAo_o<10RYY&ArAlm