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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 literal 33296 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 -#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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 Z>isN)aPw<|P+xOzi4^jCjSgJ%{|A*((0l*@ 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 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 literal 33296 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