From 6e496e2b26691acb8d6e1e6e958859460dbddf92 Mon Sep 17 00:00:00 2001 From: furrtek Date: Thu, 4 Feb 2016 10:27:53 +0100 Subject: [PATCH] Merge fixing, commit to catch up on recent files --- firmware/application/Makefile | 5 + firmware/application/file.hpp | 44 +- firmware/application/m4_startup.cpp | 2 +- firmware/application/main.cpp | 1 - firmware/application/transmitter_model.cpp | 65 +-- firmware/application/transmitter_model.hpp | 15 +- firmware/application/ui_about.cpp | 22 +- firmware/application/ui_about.hpp | 4 +- firmware/application/ui_afskrx.cpp | 16 +- firmware/application/ui_afskrx.hpp | 4 +- firmware/application/ui_afsksetup.cpp | 5 +- firmware/application/ui_afsksetup.hpp | 4 +- firmware/application/ui_alphanum.cpp | 4 +- firmware/application/ui_debug.cpp | 30 +- firmware/application/ui_debug.hpp | 2 - firmware/application/ui_jammer.cpp | 21 +- firmware/application/ui_jammer.hpp | 3 +- firmware/application/ui_lcr.cpp | 23 +- firmware/application/ui_lcr.hpp | 3 +- firmware/application/ui_loadmodule.cpp | 76 ++- firmware/application/ui_loadmodule.hpp | 10 +- firmware/application/ui_navigation.cpp | 69 ++- firmware/application/ui_navigation.hpp | 25 + firmware/application/ui_rds.cpp | 7 +- firmware/application/ui_rds.hpp | 3 +- firmware/application/ui_setup.cpp | 23 +- firmware/application/ui_setup.hpp | 127 +++++ firmware/application/ui_sigfrx.cpp | 21 +- firmware/application/ui_sigfrx.hpp | 7 +- firmware/application/ui_xylos.cpp | 31 +- firmware/application/ui_xylos.hpp | 8 +- firmware/baseband-tx.bin | Bin 33024 -> 33296 bytes firmware/baseband-tx/Makefile | 12 +- firmware/baseband-tx/audio_dma.cpp | 6 +- firmware/baseband-tx/audio_output.cpp | 100 ++++ firmware/baseband-tx/audio_output.hpp | 58 +++ .../baseband-tx/audio_stats_collector.cpp | 67 +++ .../baseband-tx/audio_stats_collector.hpp | 52 +- firmware/baseband-tx/baseband_dma.cpp | 39 +- firmware/baseband-tx/baseband_processor.cpp | 98 +--- firmware/baseband-tx/baseband_processor.hpp | 39 +- .../baseband-tx/baseband_stats_collector.cpp | 59 +++ .../baseband-tx/baseband_stats_collector.hpp | 37 +- firmware/baseband-tx/baseband_thread.cpp | 155 ++++++ firmware/baseband-tx/baseband_thread.hpp | 65 +++ firmware/baseband-tx/description | 2 +- firmware/baseband-tx/dsp_iir.cpp | 57 +++ firmware/baseband-tx/dsp_iir.hpp | 45 +- firmware/baseband-tx/dsp_squelch.cpp | 24 +- firmware/baseband-tx/dsp_squelch.hpp | 8 +- firmware/baseband-tx/event_m4.cpp | 95 +++- firmware/baseband-tx/event_m4.hpp | 51 +- firmware/baseband-tx/main.cpp | 443 +++--------------- firmware/baseband-tx/name | 2 +- firmware/baseband-tx/proc_audiotx.cpp | 2 +- firmware/baseband-tx/proc_audiotx.hpp | 3 +- firmware/baseband-tx/proc_fsk_lcr.cpp | 2 +- firmware/baseband-tx/proc_fsk_lcr.hpp | 2 +- firmware/baseband-tx/proc_jammer.cpp | 26 +- firmware/baseband-tx/proc_jammer.hpp | 2 +- firmware/baseband-tx/proc_playaudio.cpp | 4 +- firmware/baseband-tx/proc_playaudio.hpp | 2 +- firmware/baseband-tx/proc_rds.cpp | 90 ++++ firmware/baseband-tx/proc_rds.hpp | 130 +++++ firmware/baseband-tx/proc_xylos.cpp | 8 +- firmware/baseband-tx/proc_xylos.hpp | 10 +- firmware/baseband-tx/rssi_dma.cpp | 2 +- firmware/baseband-tx/rssi_thread.cpp | 63 +++ firmware/baseband-tx/rssi_thread.hpp | 46 ++ firmware/baseband-tx/spectrum_collector.cpp | 130 +++++ firmware/baseband-tx/spectrum_collector.hpp | 72 +++ firmware/baseband-tx/thread_base.hpp | 50 ++ firmware/baseband-tx/touch_dma.cpp | 2 +- firmware/baseband.bin | Bin 33024 -> 33296 bytes firmware/baseband/description | 2 +- firmware/baseband/main.cpp | 287 +----------- firmware/baseband/name | 2 +- firmware/baseband/proc_afskrx.cpp | 41 +- firmware/baseband/proc_afskrx.hpp | 44 +- firmware/baseband/proc_sigfrx.cpp | 42 +- firmware/baseband/proc_sigfrx.hpp | 18 +- firmware/bootstrap/bootstrap.bin | Bin 0 -> 228 bytes firmware/chibios-portapack/ext/fatfs/src/ff.c | 1 - .../os/hal/platforms/LPC43xx/sdc_lld.c | 2 +- firmware/common/message.hpp | 8 +- firmware/common/modules.h | 4 +- firmware/common/ui_widget.cpp | 297 ++++++++++-- firmware/common/ui_widget.hpp | 81 ++-- firmware/portapack-h1-firmware.bin | Bin 425504 -> 429152 bytes firmware/tools/make_baseband_file.py | 16 +- 90 files changed, 2257 insertions(+), 1428 deletions(-) create mode 100644 firmware/baseband-tx/audio_output.cpp create mode 100644 firmware/baseband-tx/audio_output.hpp create mode 100644 firmware/baseband-tx/audio_stats_collector.cpp create mode 100644 firmware/baseband-tx/baseband_stats_collector.cpp create mode 100644 firmware/baseband-tx/baseband_thread.cpp create mode 100644 firmware/baseband-tx/baseband_thread.hpp create mode 100644 firmware/baseband-tx/dsp_iir.cpp create mode 100644 firmware/baseband-tx/proc_rds.cpp create mode 100644 firmware/baseband-tx/proc_rds.hpp create mode 100644 firmware/baseband-tx/rssi_thread.cpp create mode 100644 firmware/baseband-tx/rssi_thread.hpp create mode 100644 firmware/baseband-tx/spectrum_collector.cpp create mode 100644 firmware/baseband-tx/spectrum_collector.hpp create mode 100644 firmware/baseband-tx/thread_base.hpp create mode 100755 firmware/bootstrap/bootstrap.bin diff --git a/firmware/application/Makefile b/firmware/application/Makefile index cb1ab48e..2e5bb800 100755 --- a/firmware/application/Makefile +++ b/firmware/application/Makefile @@ -148,6 +148,8 @@ CPPSRC = main.cpp \ encoder.cpp \ lcd_ili9341.cpp \ ui.cpp \ + ui_alphanum.cpp \ + ui_about.cpp \ ui_text.cpp \ ui_widget.cpp \ ui_painter.cpp \ @@ -157,6 +159,9 @@ CPPSRC = main.cpp \ ui_rssi.cpp \ ui_channel.cpp \ ui_audio.cpp \ + ui_lcr.cpp \ + ui_rds.cpp \ + ui_jammer.cpp \ ui_font_fixed_8x16.cpp \ ui_setup.cpp \ ui_debug.cpp \ diff --git a/firmware/application/file.hpp b/firmware/application/file.hpp index 0dc085c7..363bd866 100644 --- a/firmware/application/file.hpp +++ b/firmware/application/file.hpp @@ -19,36 +19,34 @@ * Boston, MA 02110-1301, USA. */ -#include "irq_ipc.hpp" +#ifndef __FILE_H__ +#define __FILE_H__ -#include "event.hpp" +#include "ff.h" -#include "ch.h" -#include "hal.h" +#include +#include -#include "lpc43xx_cpp.hpp" -using namespace lpc43xx; +class File { +public: + ~File(); -void m4txevent_interrupt_enable() { - nvicEnableVector(M4CORE_IRQn, CORTEX_PRIORITY_MASK(LPC43XX_M4TXEVENT_IRQ_PRIORITY)); -} + bool open_for_append(const std::string file_path); + bool close(); -void m4txevent_interrupt_disable() { - nvicDisableVector(M4CORE_IRQn); -} + bool is_ready(); -extern "C" { + bool read(void* const data, const size_t bytes_to_read); + bool write(const void* const data, const size_t bytes_to_write); -CH_IRQ_HANDLER(M4Core_IRQHandler) { - CH_IRQ_PROLOGUE(); + bool puts(const std::string string); - chSysLockFromIsr(); - events_flag_isr(EVT_MASK_APPLICATION); - chSysUnlockFromIsr(); + bool sync(); - creg::m4txevent::clear(); +private: + const std::string file_path; + + FIL f; +}; - CH_IRQ_EPILOGUE(); -} - -} +#endif/*__FILE_H__*/ diff --git a/firmware/application/m4_startup.cpp b/firmware/application/m4_startup.cpp index 69345414..42ceeb43 100644 --- a/firmware/application/m4_startup.cpp +++ b/firmware/application/m4_startup.cpp @@ -106,7 +106,7 @@ void m4_switch(const char * hash) { // Ask M4 to enter wait loop in RAM BasebandConfiguration baseband_switch { - .mode = SWITCH, + .mode = 255, // DEBUG .sampling_rate = 0, .decimation_factor = 1, }; diff --git a/firmware/application/main.cpp b/firmware/application/main.cpp index a36598cf..0b374a16 100755 --- a/firmware/application/main.cpp +++ b/firmware/application/main.cpp @@ -62,7 +62,6 @@ #include "ui_painter.hpp" #include "ui_navigation.hpp" -#include "irq_ipc.hpp" #include "irq_lcd_frame.hpp" #include "irq_controls.hpp" #include "irq_rtc.hpp" diff --git a/firmware/application/transmitter_model.cpp b/firmware/application/transmitter_model.cpp index 6ca9c390..f04002ed 100644 --- a/firmware/application/transmitter_model.cpp +++ b/firmware/application/transmitter_model.cpp @@ -75,31 +75,8 @@ uint32_t TransmitterModel::sampling_rate() const { return baseband_configuration.sampling_rate; } -void TransmitterModel::set_sampling_rate(uint32_t hz) { - baseband_configuration.sampling_rate = hz; - update_baseband_configuration(); -} - -mode_type TransmitterModel::modulation() const { - return baseband_configuration.mode; -} - -void TransmitterModel::set_modulation(mode_type v) { - baseband_configuration.mode = v; - update_modulation(); -} - -uint32_t TransmitterModel::baseband_oversampling() const { - // TODO: Rename decimation_factor. - return baseband_configuration.decimation_factor; -} - -void TransmitterModel::set_baseband_oversampling(uint32_t v) { - baseband_configuration.decimation_factor = v; - update_baseband_configuration(); -} - void TransmitterModel::enable() { + enabled_ = true; radio::set_direction(rf::Direction::Transmit); update_tuning_frequency(); update_rf_amp(); @@ -107,23 +84,30 @@ void TransmitterModel::enable() { update_vga(); update_baseband_bandwidth(); update_baseband_configuration(); - radio::streaming_enable(); +} + +void TransmitterModel::baseband_disable() { + shared_memory.baseband_queue.push_and_wait( + BasebandConfigurationMessage { + .configuration = { }, + } + ); } void TransmitterModel::disable() { - /* TODO: This is a dumb hack to stop baseband from working so hard. */ - BasebandConfigurationMessage message { - .configuration = { - .mode = NONE, - .sampling_rate = 0, - .decimation_factor = 1, - } - }; - shared_memory.baseband_queue.push(message); + enabled_ = false; + baseband_disable(); + // TODO: Responsibility for enabling/disabling the radio is muddy. + // Some happens in ReceiverModel, some inside radio namespace. radio::disable(); } +uint32_t TransmitterModel::baseband_oversampling() const { + // TODO: Rename decimation_factor. + return baseband_configuration.decimation_factor; +} + int32_t TransmitterModel::tuning_offset() { return -(sampling_rate() / 4); } @@ -148,8 +132,8 @@ void TransmitterModel::update_vga() { radio::set_vga_gain(vga_gain_db_); } -void TransmitterModel::update_modulation() { - update_baseband_configuration(); +uint32_t TransmitterModel::modulation() const { + return baseband_configuration.mode; } void TransmitterModel::set_baseband_configuration(const BasebandConfiguration config) { @@ -158,7 +142,12 @@ void TransmitterModel::set_baseband_configuration(const BasebandConfiguration co } void TransmitterModel::update_baseband_configuration() { - radio::streaming_disable(); + // TODO: Move more low-level radio control stuff to M4. It'll enable tighter + // synchronization for things like wideband (sweeping) spectrum analysis, and + // protocols that need quick RX/TX turn-around. + + // Disabling baseband while changing sampling rates seems like a good idea... + baseband_disable(); clock_manager.set_sampling_frequency(sampling_rate() * baseband_oversampling()); update_tuning_frequency(); @@ -166,7 +155,5 @@ void TransmitterModel::update_baseband_configuration() { BasebandConfigurationMessage message { baseband_configuration }; shared_memory.baseband_queue.push(message); - - radio::streaming_enable(); } diff --git a/firmware/application/transmitter_model.hpp b/firmware/application/transmitter_model.hpp index d8506935..f941e294 100644 --- a/firmware/application/transmitter_model.hpp +++ b/firmware/application/transmitter_model.hpp @@ -38,7 +38,7 @@ public: ) : clock_manager(clock_manager) { } - + rf::Frequency tuning_frequency() const; void set_tuning_frequency(rf::Frequency f); @@ -55,13 +55,10 @@ public: void set_vga(int32_t v_db); uint32_t sampling_rate() const; - void set_sampling_rate(uint32_t hz); - - mode_type modulation() const; - void set_modulation(mode_type v); + uint32_t modulation() const; + uint32_t baseband_oversampling() const; - void set_baseband_oversampling(uint32_t v); void enable(); void disable(); @@ -69,12 +66,14 @@ 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 = NONE, + .mode = 1, .sampling_rate = 2280000, .decimation_factor = 1, }; @@ -89,6 +88,8 @@ 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 978965e3..235d7294 100644 --- a/firmware/application/ui_about.cpp +++ b/firmware/application/ui_about.cpp @@ -26,6 +26,7 @@ #include "ymdata.hpp" #include "portapack.hpp" +#include "event_m0.hpp" #include "ui_about.hpp" #include "touch.hpp" @@ -44,23 +45,21 @@ using namespace portapack; namespace ui { void AboutView::on_show() { - auto& message_map = context().message_map(); - // Just in case - message_map.unregister_handler(Message::ID::DisplayFrameSync); + EventDispatcher::message_map().unregister_handler(Message::ID::DisplayFrameSync); // "Vertical blank interrupt" - message_map.register_handler(Message::ID::DisplayFrameSync, + EventDispatcher::message_map().register_handler(Message::ID::DisplayFrameSync, [this](const Message* const) { update(); } ); // Just in case - message_map.unregister_handler(Message::ID::FIFOSignal); + EventDispatcher::message_map().unregister_handler(Message::ID::FIFOSignal); // Handle baseband asking to fill up FIFO - message_map.register_handler(Message::ID::FIFOSignal, + EventDispatcher::message_map().register_handler(Message::ID::FIFOSignal, [this](Message* const p) { FIFODataMessage datamessage; const auto message = static_cast(p); @@ -349,6 +348,7 @@ void AboutView::update() { refresh_cnt++; } + // Slowly increase volume to avoid jumpscare if (headphone_vol < (70<<2)) { portapack::audio_codec.set_headphone_volume(volume_t::decibel((headphone_vol/4) - 99) + wolfson::wm8731::headphone_gain_range.max); headphone_vol++; @@ -370,14 +370,13 @@ void AboutView::ym_init() { } AboutView::AboutView( - NavigationView& nav, - TransmitterModel& transmitter_model -) : transmitter_model(transmitter_model) + NavigationView& nav +) { uint8_t p, c; transmitter_model.set_baseband_configuration({ - .mode = PLAY_AUDIO, + .mode = 5, .sampling_rate = 1536000, .decimation_factor = 1, }); @@ -412,8 +411,7 @@ AboutView::AboutView( ym_init(); button_ok.on_select = [this,&nav](Button&){ - auto& message_map = context().message_map(); - message_map.unregister_handler(Message::ID::DisplayFrameSync); + EventDispatcher::message_map().unregister_handler(Message::ID::DisplayFrameSync); if (framebuffer) chHeapFree(framebuffer); // Do NOT forget this nav.pop(); }; diff --git a/firmware/application/ui_about.hpp b/firmware/application/ui_about.hpp index c0ca162c..2db4b8bb 100644 --- a/firmware/application/ui_about.hpp +++ b/firmware/application/ui_about.hpp @@ -34,7 +34,7 @@ namespace ui { class AboutView : public View { public: - AboutView(NavigationView& nav, TransmitterModel& transmitter_model); + AboutView(NavigationView& nav); ~AboutView(); void on_show() override; @@ -54,8 +54,6 @@ private: bool same; } ymreg_t; - TransmitterModel& transmitter_model; - uint16_t headphone_vol = 5<<2; ymreg_t ym_regs[14]; diff --git a/firmware/application/ui_afskrx.cpp b/firmware/application/ui_afskrx.cpp index fa6b6eba..47375efc 100644 --- a/firmware/application/ui_afskrx.cpp +++ b/firmware/application/ui_afskrx.cpp @@ -24,19 +24,17 @@ #include "ui_spectrum.hpp" #include "ui_console.hpp" +#include "event_m0.hpp" #include "ff.h" #include "portapack.hpp" using namespace portapack; -#include "ui_receiver.hpp" - namespace ui { AFSKRXView::AFSKRXView( - NavigationView& nav, - ReceiverModel& receiver_model -) : receiver_model(receiver_model) + NavigationView& nav +) { add_children({ { &button_done, @@ -48,7 +46,7 @@ AFSKRXView::AFSKRXView( }; receiver_model.set_baseband_configuration({ - .mode = RX_AFSK, + .mode = 6, .sampling_rate = 3072000, .decimation_factor = 4, }); @@ -60,8 +58,7 @@ AFSKRXView::~AFSKRXView() { } void AFSKRXView::on_show() { - auto& message_map = context().message_map(); - message_map.register_handler(Message::ID::AFSKData, + EventDispatcher::message_map().register_handler(Message::ID::AFSKData, [this](Message* const p) { const auto message = static_cast(p); this->on_data_afsk(*message); @@ -72,8 +69,7 @@ void AFSKRXView::on_show() { } void AFSKRXView::on_hide() { - auto& message_map = context().message_map(); - message_map.unregister_handler(Message::ID::AFSKData); + EventDispatcher::message_map().unregister_handler(Message::ID::AFSKData); } void AFSKRXView::on_data_afsk(const AFSKDataMessage& message) { diff --git a/firmware/application/ui_afskrx.hpp b/firmware/application/ui_afskrx.hpp index 9651eb55..ccd37a3b 100644 --- a/firmware/application/ui_afskrx.hpp +++ b/firmware/application/ui_afskrx.hpp @@ -42,7 +42,7 @@ namespace ui { class AFSKRXView : public View { public: - AFSKRXView(NavigationView& nav, ReceiverModel& receiver_model); + AFSKRXView(NavigationView& nav); ~AFSKRXView(); void focus() override; @@ -51,8 +51,6 @@ public: void on_hide() override; private: - ReceiverModel& receiver_model; - std::unique_ptr widget_content; Button button_done { diff --git a/firmware/application/ui_afsksetup.cpp b/firmware/application/ui_afsksetup.cpp index 2176fb96..e445b344 100644 --- a/firmware/application/ui_afsksetup.cpp +++ b/firmware/application/ui_afsksetup.cpp @@ -66,9 +66,8 @@ void AFSKSetupView::updfreq(rf::Frequency f) { } AFSKSetupView::AFSKSetupView( - NavigationView& nav, - TransmitterModel& transmitter_model -) : transmitter_model(transmitter_model) + NavigationView& nav +) { uint8_t rpt; diff --git a/firmware/application/ui_afsksetup.hpp b/firmware/application/ui_afsksetup.hpp index 297f4a11..0862d518 100644 --- a/firmware/application/ui_afsksetup.hpp +++ b/firmware/application/ui_afsksetup.hpp @@ -36,15 +36,13 @@ namespace ui { class AFSKSetupView : public View { public: - AFSKSetupView(NavigationView& nav, TransmitterModel& transmitter_model); + AFSKSetupView(NavigationView& nav); void updfreq(rf::Frequency f); void focus() override; void paint(Painter& painter) override; private: - TransmitterModel& transmitter_model; - Text text_title { { 40, 32, 160, 16 }, "AFSK modulator setup" diff --git a/firmware/application/ui_alphanum.cpp b/firmware/application/ui_alphanum.cpp index 9c433868..33578b10 100644 --- a/firmware/application/ui_alphanum.cpp +++ b/firmware/application/ui_alphanum.cpp @@ -110,7 +110,7 @@ void AlphanumView::set_uppercase() { size_t n = 0; for(auto& button : buttons) { - add_child(&button); + //add_child(&button); const std::string label { key_caps[n] }; @@ -125,7 +125,7 @@ void AlphanumView::set_lowercase() { size_t n = 0; for(auto& button : buttons) { - add_child(&button); + //add_child(&button); const std::string label { key_caps[n] }; diff --git a/firmware/application/ui_debug.cpp b/firmware/application/ui_debug.cpp index 7e26d571..cdfdc54c 100644 --- a/firmware/application/ui_debug.cpp +++ b/firmware/application/ui_debug.cpp @@ -245,20 +245,6 @@ void RegistersView::focus() { button_done.focus(); } -void DebugLCRView::paint(Painter& painter) { - const Point offset = { - static_cast(32), - static_cast(32) - }; - - const auto text = to_string_hex(fr, 2); - painter.draw_string( - screen_pos() + offset, - style(), - text - ); -} - char hexify(char in) { if (in > 9) in += 7; return in + 0x30; @@ -301,26 +287,26 @@ void DebugLCRView::focus() { DebugMenuView::DebugMenuView(NavigationView& nav) { add_items<8>({ { - { "Memory", [&nav](){ nav.push(); } }, - { "Radio State", [&nav](){ nav.push(); } }, - { "SD Card", [&nav](){ nav.push(); } }, - { "RFFC5072", [&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(); } }, + { "RFFC5072", ui::Color::white(), [&nav](){ nav.push( "RFFC5072", RegistersWidgetConfig { 31, 2, 4, 4 }, [](const size_t register_number) { return radio::first_if.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::second_if.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 { wolfson::wm8731::reg_count, 1, 3, 4 }, [](const size_t register_number) { return portapack::audio_codec.read(register_number); } ); } }, - { "Temperature", [&nav](){ nav.push(); } }, + { "Temperature", ui::Color::white(), [&nav](){ nav.push(); } }, } }); on_left = [&nav](){ nav.pop(); }; } diff --git a/firmware/application/ui_debug.hpp b/firmware/application/ui_debug.hpp index ecae1bdb..3340c969 100644 --- a/firmware/application/ui_debug.hpp +++ b/firmware/application/ui_debug.hpp @@ -246,8 +246,6 @@ public: void focus() override; - void paint(Painter& painter) override; - private: Text text_lcr1 { { 16, 32, 208, 8 }, diff --git a/firmware/application/ui_jammer.cpp b/firmware/application/ui_jammer.cpp index 4707a6c9..4f29b010 100644 --- a/firmware/application/ui_jammer.cpp +++ b/firmware/application/ui_jammer.cpp @@ -29,6 +29,8 @@ #include "hackrf_gpio.hpp" #include "portapack.hpp" #include "radio.hpp" +#include "string_format.hpp" +#include "event_m0.hpp" #include "hackrf_hal.hpp" #include "portapack_shared_memory.hpp" @@ -37,7 +39,7 @@ #include #include -using namespace hackrf::one; +using namespace portapack; namespace ui { @@ -146,9 +148,8 @@ void JammerView::updfreq(uint8_t id, rf::Frequency f) { } JammerView::JammerView( - NavigationView& nav, - TransmitterModel& transmitter_model -) : transmitter_model(transmitter_model) + NavigationView& nav +) { static constexpr Style style_val { @@ -169,7 +170,11 @@ JammerView::JammerView( .foreground = Color::grey(), }; - transmitter_model.set_modulation(TX_JAMMER); + transmitter_model.set_baseband_configuration({ + .mode = 3, + .sampling_rate = 1536000, // ? + .decimation_factor = 1, + }); add_children({ { &text_type, @@ -232,11 +237,9 @@ JammerView::JammerView( button_transmit.on_select = [this,&transmitter_model](Button&) { uint8_t i = 0; rf::Frequency t, range_lower; - auto& message_map = context().message_map(); + EventDispatcher::message_map().unregister_handler(Message::ID::Retune); - message_map.unregister_handler(Message::ID::Retune); - - message_map.register_handler(Message::ID::Retune, + EventDispatcher::message_map().register_handler(Message::ID::Retune, [this,&transmitter_model](Message* const p) { const auto message = static_cast(p); if (message->freq > 0) { diff --git a/firmware/application/ui_jammer.hpp b/firmware/application/ui_jammer.hpp index f7174450..ac30ce01 100644 --- a/firmware/application/ui_jammer.hpp +++ b/firmware/application/ui_jammer.hpp @@ -36,7 +36,7 @@ namespace ui { class JammerView : public View { public: - JammerView(NavigationView& nav, TransmitterModel& transmitter_model); + JammerView(NavigationView& nav); ~JammerView(); void updfreq(uint8_t id, rf::Frequency f); @@ -117,7 +117,6 @@ private: bool jamming = false; rf::Frequency f; - TransmitterModel& transmitter_model; Text text_type { { 1 * 8, 1 * 16, 40, 16 }, diff --git a/firmware/application/ui_lcr.cpp b/firmware/application/ui_lcr.cpp index c3f166f3..76fd9749 100644 --- a/firmware/application/ui_lcr.cpp +++ b/firmware/application/ui_lcr.cpp @@ -31,8 +31,11 @@ #include "ff.h" #include "hackrf_gpio.hpp" #include "portapack.hpp" +#include "event_m0.hpp" #include "radio.hpp" +#include "string_format.hpp" + #include "hackrf_hal.hpp" #include "portapack_shared_memory.hpp" #include "portapack_persistent_memory.hpp" @@ -40,7 +43,7 @@ #include #include -using namespace hackrf::one; +using namespace portapack; namespace ui { @@ -176,9 +179,8 @@ void LCRView::paint(Painter& painter) { } LCRView::LCRView( - NavigationView& nav, - TransmitterModel& transmitter_model -) : transmitter_model(transmitter_model) + NavigationView& nav +) { char finalstr[24] = {0}; @@ -188,7 +190,12 @@ LCRView::LCRView( .foreground = Color::black(), }; - transmitter_model.set_modulation(TX_LCR); + transmitter_model.set_baseband_configuration({ + .mode = 1, + .sampling_rate = 1536000, // Is this right ? + .decimation_factor = 1, + }); + transmitter_model.set_tuning_frequency(portapack::persistent_memory::tuned_frequency()); memset(litteral, 0, 5*8); memset(rgsb, 0, 5); @@ -277,8 +284,6 @@ LCRView::LCRView( }; button_transmit.on_select = [this,&transmitter_model](Button&){ - auto& message_map = context().message_map(); - make_frame(); shared_memory.afsk_samples_per_bit = 228000/portapack::persistent_memory::afsk_bitrate(); @@ -293,7 +298,7 @@ LCRView::LCRView( shared_memory.afsk_transmit_done = false; shared_memory.afsk_repeat = (portapack::persistent_memory::afsk_config() >> 8) & 0xFF; - message_map.register_handler(Message::ID::TXDone, + EventDispatcher::message_map().register_handler(Message::ID::TXDone, [this,&transmitter_model](Message* const p) { char str[8]; const auto message = static_cast(p); @@ -319,7 +324,7 @@ LCRView::LCRView( }; button_txsetup.on_select = [&nav, &transmitter_model](Button&){ - nav.push(new AFSKSetupView { nav, transmitter_model }); + nav.push(new AFSKSetupView { nav }); }; button_exit.on_select = [&nav](Button&){ diff --git a/firmware/application/ui_lcr.hpp b/firmware/application/ui_lcr.hpp index 76368087..76543c64 100644 --- a/firmware/application/ui_lcr.hpp +++ b/firmware/application/ui_lcr.hpp @@ -36,7 +36,7 @@ namespace ui { class LCRView : public View { public: - LCRView(NavigationView& nav, TransmitterModel& transmitter_model); + LCRView(NavigationView& nav); ~LCRView(); void make_frame(); @@ -63,7 +63,6 @@ private: char lcrframe[256]; char lcrframe_f[256]; rf::Frequency f; - TransmitterModel& transmitter_model; Text text_status { { 168, 196, 64, 16 }, diff --git a/firmware/application/ui_loadmodule.cpp b/firmware/application/ui_loadmodule.cpp index 36569ed9..865864c6 100644 --- a/firmware/application/ui_loadmodule.cpp +++ b/firmware/application/ui_loadmodule.cpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek * * This file is part of PortaPack. * @@ -24,10 +25,12 @@ #include "ch.h" #include "ff.h" +#include "event_m0.hpp" #include "hackrf_gpio.hpp" #include "portapack.hpp" #include "portapack_shared_memory.hpp" #include "hackrf_hal.hpp" +#include "string_format.hpp" #include #include @@ -45,18 +48,17 @@ void LoadModuleView::paint(Painter& painter) { } void LoadModuleView::on_hide() { - auto& message_map = context().message_map(); - message_map.unregister_handler(Message::ID::ReadyForSwitch); + EventDispatcher::message_map().unregister_handler(Message::ID::ReadyForSwitch); + EventDispatcher::message_map().unregister_handler(Message::ID::ModuleID); } void LoadModuleView::on_show() { // Ask for MD5 signature and compare ModuleIDMessage message; - auto& message_map = context().message_map(); + + //message_map.unregister_handler(Message::ID::ModuleID); - message_map.unregister_handler(Message::ID::ModuleID); - - message_map.register_handler(Message::ID::ModuleID, + EventDispatcher::message_map().register_handler(Message::ID::ModuleID, [this](Message* const p) { uint8_t c; const auto message = static_cast(p); @@ -78,13 +80,66 @@ void LoadModuleView::on_show() { shared_memory.baseband_queue.push(message); } +int LoadModuleView::load_image() { + const char magic[6] = {'P', 'P', 'M', ' ', 0x02, 0x00}; + UINT bw; + uint8_t i; + uint32_t cnt; + 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 + if (f_opendir(&rootdir, "/") == FR_OK) { + for (;;) { + res = f_readdir(&rootdir, &modinfo); + if (res != FR_OK || modinfo.fname[0] == 0) break; // Reached last file, abort + // Only care about files with .bin extension + if ((!(modinfo.fattrib & AM_DIR)) && (modinfo.fname[9] == 'B') && (modinfo.fname[10] == 'I') && (modinfo.fname[11] == 'N')) { + res = f_open(&modfile, modinfo.fname, FA_OPEN_EXISTING | FA_READ); + if (res != FR_OK) return 0; + // Magic bytes and version check + f_read(&modfile, &md5sum, 6, &bw); + for (i = 0; i < 6; i++) + if (md5sum[i] != magic[i]) break; + if (i == 6) { + f_lseek(&modfile, 26); + f_read(&modfile, &md5sum, 16, &bw); + for (i = 0; i < 16; i++) + if (md5sum[i] != _hash[i]) break; + // f_read can't read more than 512 bytes at a time ? + if (i == 16) { + f_lseek(&modfile, 512); + for (cnt = 0; cnt < 64; cnt++) { + if (f_read(&modfile, reinterpret_cast(portapack::memory::map::m4_code.base() + (cnt * 512)), 512, &bw)) return 0; + } + f_close(&modfile); + f_closedir(&rootdir); + LPC_RGU->RESET_CTRL[0] = (1 << 13); + return 1; + } + } + f_close(&modfile); + } + } + f_closedir(&rootdir); + } + + return 0; +} + void LoadModuleView::loadmodule() { - auto& message_map = context().message_map(); - message_map.register_handler(Message::ID::ReadyForSwitch, + //message_map.unregister_handler(Message::ID::ReadyForSwitch); + + EventDispatcher::message_map().register_handler(Message::ID::ReadyForSwitch, [this](Message* const p) { (void)p; - if (m4_load_image()) { - text_info.set("Module loaded :)"); + 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 { text_info.set("Module not found :("); @@ -104,6 +159,7 @@ LoadModuleView::LoadModuleView( { add_children({ { &text_info, + &text_infob, &button_ok } }); diff --git a/firmware/application/ui_loadmodule.hpp b/firmware/application/ui_loadmodule.hpp index ab2dfc67..605e1314 100644 --- a/firmware/application/ui_loadmodule.hpp +++ b/firmware/application/ui_loadmodule.hpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek * * This file is part of PortaPack. * @@ -31,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, View * new_view); void loadmodule(); void on_show() override; @@ -40,12 +41,17 @@ public: void paint(Painter& painter) override; private: + int load_image(void); const char * _hash; bool _mod_loaded = false; Text text_info { { 8, 64, 224, 16 }, - "Searching module..." + "-" + }; + Text text_infob { + { 8, 64+16, 224, 16 }, + "-" }; Button button_ok { diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index 711931ea..960e5c57 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -108,6 +108,10 @@ View* NavigationView::push_view(std::unique_ptr new_view) { return p; } +void NavigationView::push(View* v) { + push_view(std::unique_ptr(v)); +} + void NavigationView::pop() { // Can't pop last item from stack. if( view_stack.size() > 1 ) { @@ -149,9 +153,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(); } }, } }); } @@ -159,8 +163,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(); } }, } }); } @@ -168,24 +172,24 @@ ReceiverMenuView::ReceiverMenuView(NavigationView& nav) { SystemMenuView::SystemMenuView(NavigationView& nav) { add_items<10>({ { - { "Play dead", ui::Color::red(), [&nav](){ nav.push(new PlayDeadView { nav, false }); } }, - { "Receiver", ui::Color::cyan(), [&nav](){ nav.push(new LoadModuleView { nav, md5_baseband, new ReceiverMenuView { nav, receiver_model }}); } }, + { "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(new LoadModuleView { nav, md5_baseband, new JammerView { nav, transmitter_model }}); } }, + { "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(new LoadModuleView { nav, md5_baseband_tx, new RDSView { nav, transmitter_model }}); } }, - { "Xylos TX", ui::Color::orange(), [&nav](){ nav.push(new LoadModuleView { nav, md5_baseband_tx, new XylosView { nav, transmitter_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(new LoadModuleView { nav, md5_baseband_tx, new LCRView { nav, transmitter_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(new SetupMenuView { nav }); } }, - { "About", ui::Color::white(), [&nav](){ nav.push(new AboutView { nav, transmitter_model }); } }, - { "Debug", ui::Color::white(), [&nav](){ nav.push(new DebugMenuView { nav }); } }, - { "HackRF", ui::Color::white(), [&nav](){ nav.push(new HackRFFirmwareView { nav }); } }, + { "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(); } }, } }); } @@ -266,6 +270,41 @@ HackRFFirmwareView::HackRFFirmwareView(NavigationView& nav) { } }); } +/* PlayDeadView **********************************************************/ + +void PlayDeadView::focus() { + button_done.focus(); +} + +PlayDeadView::PlayDeadView(NavigationView& nav, bool booting) { + _booting = booting; + persistent_memory::set_playing_dead(0x59); + + add_children({ { + &text_playdead1, + &text_playdead2, + &button_done, + } }); + + button_done.on_dir = [this,&nav](Button&, KeyEvent key){ + sequence = (sequence<<3) | static_cast::type>(key); + }; + + button_done.on_select = [this,&nav](Button&){ + if (sequence == persistent_memory::playdead_sequence()) { + persistent_memory::set_playing_dead(0); + if (_booting) { + nav.pop(); + nav.push(); + } else { + nav.pop(); + } + } else { + sequence = 0; + } + }; +} + void HackRFFirmwareView::focus() { button_no.focus(); } diff --git a/firmware/application/ui_navigation.hpp b/firmware/application/ui_navigation.hpp index 4e00b4e3..2e902f4d 100644 --- a/firmware/application/ui_navigation.hpp +++ b/firmware/application/ui_navigation.hpp @@ -82,6 +82,8 @@ public: T* push(Args&&... args) { return reinterpret_cast(push_view(std::unique_ptr(new T(*this, std::forward(args)...)))); } + + void push(View* v); void pop(); @@ -120,6 +122,29 @@ private: }; }; +class PlayDeadView : public View { +public: + PlayDeadView(NavigationView& nav, bool booting); + void focus() override; + +private: + bool _booting; + uint32_t sequence = 0; + Text text_playdead1 { + { 6 * 8, 7 * 16, 14 * 8, 16 }, + "Firmware error" + }; + Text text_playdead2 { + { 6 * 8, 9 * 16, 16 * 8, 16 }, + "0x1400_0000 : 2C" + }; + + Button button_done { + { 240, 0, 1, 1 }, + "" + }; +}; + class ReceiverMenuView : public MenuView { public: ReceiverMenuView(NavigationView& nav); diff --git a/firmware/application/ui_rds.cpp b/firmware/application/ui_rds.cpp index c9ea6e2d..c94c1f95 100644 --- a/firmware/application/ui_rds.cpp +++ b/firmware/application/ui_rds.cpp @@ -34,7 +34,7 @@ #include -using namespace hackrf::one; +using namespace portapack; namespace ui { @@ -134,9 +134,8 @@ void RDSView::paint(Painter& painter) { } RDSView::RDSView( - NavigationView& nav, - TransmitterModel& transmitter_model -) : transmitter_model(transmitter_model) + NavigationView& nav +) { transmitter_model.set_tuning_frequency(93000000); strcpy(psname, "TEST1234"); diff --git a/firmware/application/ui_rds.hpp b/firmware/application/ui_rds.hpp index e417fcc6..24586647 100644 --- a/firmware/application/ui_rds.hpp +++ b/firmware/application/ui_rds.hpp @@ -86,7 +86,7 @@ private: */ class RDSView : public View { public: - RDSView(NavigationView& nav, TransmitterModel& transmitter_model); + RDSView(NavigationView& nav); ~RDSView(); void focus() override; @@ -94,7 +94,6 @@ public: private: char psname[9]; - TransmitterModel& transmitter_model; Text text_title { { 76, 16, 88, 16 }, diff --git a/firmware/application/ui_setup.cpp b/firmware/application/ui_setup.cpp index 809b42dc..0a0c7062 100644 --- a/firmware/application/ui_setup.cpp +++ b/firmware/application/ui_setup.cpp @@ -20,13 +20,16 @@ */ #include "ui_setup.hpp" - +#include "string_format.hpp" #include "portapack_persistent_memory.hpp" +#include "ui_font_fixed_8x16.hpp" + #include "lpc43xx_cpp.hpp" using namespace lpc43xx; #include "portapack.hpp" using portapack::receiver_model; +using namespace portapack; namespace ui { @@ -167,12 +170,6 @@ void AntennaBiasSetupView::focus() { button_done.focus(); } - - -void AboutView::focus() { - button_ok.focus(); -} - void SetTouchCalibView::focus() { button_ok.focus(); } @@ -433,13 +430,13 @@ void ModInfoView::focus() { SetupMenuView::SetupMenuView(NavigationView& nav) { add_items<7>({ { - { "SD card modules", ui::Color::white(), [&nav](){ nav.push(new ModInfoView { nav }); } }, - { "Date/Time", ui::Color::white(), [&nav](){ nav.push(new SetDateTimeView { nav }); } }, + { "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(); } }, - { "Antenna Bias Voltage", [&nav](){ nav.push(); } }, - { "Touch screen", ui::Color::white(), [&nav](){ nav.push(new SetTouchCalibView { nav }); } }, - { "Play dead", ui::Color::red(), [&nav](){ nav.push(new SetPlayDeadView { nav }); } }, - { "UI", ui::Color::white(), [&nav](){ nav.push(new SetUIView { nav }); } }, + { "Antenna Bias Voltage", ui::Color::white(), [&nav](){ nav.push(); } }, + { "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/application/ui_setup.hpp b/firmware/application/ui_setup.hpp index a1383851..9f650b43 100644 --- a/firmware/application/ui_setup.hpp +++ b/firmware/application/ui_setup.hpp @@ -25,6 +25,7 @@ #include "ui_widget.hpp" #include "ui_menu.hpp" #include "ui_navigation.hpp" +#include "ff.h" #include @@ -252,6 +253,132 @@ private: }; }; +class SetUIView : public View { +public: + SetUIView(NavigationView& nav); + void focus() override; + +private: + Checkbox checkbox_showsplash { + { 3 * 8, 2 * 16}, + "Show splash" + }; + + Checkbox checkbox_bloff { + { 3 * 8, 4 * 16}, + "Backlight off after:" + }; + + OptionsField options_bloff { + { 10 * 8, 5 * 16 + 4 }, + 10, + { + { "5 seconds ", 0 }, + { "15 seconds", 1 }, + { "1 minute ", 2 }, + { "5 minutes ", 3 }, + { "10 minutes", 4 } + } + }; + + Button button_ok { + { 4 * 8, 272, 64, 24 }, + "Ok" + }; +}; + +class SetPlayDeadView : public View { +public: + SetPlayDeadView(NavigationView& nav); + void focus() override; +private: + bool entermode = false; + uint32_t sequence = 0; + uint8_t keycount, key_code; + char sequence_txt[11]; + + Text text_sequence { + { 64, 32, 14 * 8, 16 }, + "Enter sequence", + }; + + Button button_enter { + { 16, 192, 96, 24 }, + "Enter" + }; + Button button_cancel { + { 128, 192, 96, 24 }, + "Cancel" + }; +}; + +class ModInfoView : public View { +public: + ModInfoView(NavigationView& nav); + void focus() override; + void on_show() override; + +private: + void update_infos(uint8_t modn); + + typedef struct moduleinfo_t{ + char filename[9]; + uint16_t version; + uint32_t size; + char md5[16]; + char name[16]; + char description[214]; + } moduleinfo_t; + + moduleinfo_t module_list[8]; // 8 max for now + + Text text_modcount { + { 2 * 8, 1 * 16, 18 * 8, 16 }, + "Searching..." + }; + + OptionsField option_modules { + { 2 * 8, 2 * 16 }, + 24, + { { "-", 0 } + } + }; + + Text text_name { + { 2 * 8, 4 * 16, 5 * 8, 16 }, + "Name:" + }; + Text text_namestr { + { 8 * 8, 4 * 16, 16 * 8, 16 }, + "..." + }; + Text text_size { + { 2 * 8, 5 * 16, 5 * 8, 16 }, + "Size:" + }; + Text text_sizestr { + { 8 * 8, 5 * 16, 16 * 8, 16 }, + "..." + }; + Text text_md5 { + { 2 * 8, 6 * 16, 4 * 8, 16 }, + "MD5:" + }; + Text text_md5_a { + { 7 * 8, 6 * 16, 16 * 8, 16 }, + "..." + }; + Text text_md5_b { + { 7 * 8, 7 * 16, 16 * 8, 16 }, + "..." + }; + + Button button_ok { + { 4 * 8, 272, 64, 24 }, + "Ok" + }; +}; + class SetupMenuView : public MenuView { public: SetupMenuView(NavigationView& nav); diff --git a/firmware/application/ui_sigfrx.cpp b/firmware/application/ui_sigfrx.cpp index 514e3963..bc6c5f6e 100644 --- a/firmware/application/ui_sigfrx.cpp +++ b/firmware/application/ui_sigfrx.cpp @@ -25,11 +25,13 @@ #include "ch.h" #include "evtimer.h" +#include "event_m0.hpp" #include "ff.h" #include "hackrf_gpio.hpp" #include "portapack.hpp" #include "radio.hpp" -//#include "fox_bmp.hpp" + +#include "string_format.hpp" #include "hackrf_hal.hpp" #include "portapack_shared_memory.hpp" @@ -38,7 +40,7 @@ #include #include -using namespace hackrf::one; +using namespace portapack; namespace ui { @@ -98,20 +100,23 @@ void SIGFRXView::on_channel_spectrum(const ChannelSpectrum& spectrum) { } void SIGFRXView::on_show() { - context().message_map().register_handler(Message::ID::ChannelSpectrum, + /*EventDispatcher::message_map().register_handler(Message::ID::ChannelSpectrum, [this](const Message* const p) { this->on_channel_spectrum(reinterpret_cast(p)->spectrum); } - ); + );*/ +} + +void SIGFRXView::on_hide() { + //EventDispatcher::message_map().unregister_handler(Message::ID::ChannelSpectrum); } SIGFRXView::SIGFRXView( - NavigationView& nav, - ReceiverModel& receiver_model -) : receiver_model(receiver_model) + NavigationView& nav +) { receiver_model.set_baseband_configuration({ - .mode = RX_SIGFOX, + .mode = 255, // DEBUG .sampling_rate = 3072000, .decimation_factor = 4, }); diff --git a/firmware/application/ui_sigfrx.hpp b/firmware/application/ui_sigfrx.hpp index aebe474c..3b83ac07 100644 --- a/firmware/application/ui_sigfrx.hpp +++ b/firmware/application/ui_sigfrx.hpp @@ -36,17 +36,16 @@ namespace ui { class SIGFRXView : public View { public: - SIGFRXView(NavigationView& nav, ReceiverModel& receiver_model); + SIGFRXView(NavigationView& nav); ~SIGFRXView(); void on_channel_spectrum(const ChannelSpectrum& spectrum); void on_show() override; + void on_hide() override; void focus() override; void paint(Painter& painter) override; -private: - ReceiverModel& receiver_model; - +private: uint8_t last_channel; uint8_t detect_counter = 0; diff --git a/firmware/application/ui_xylos.cpp b/firmware/application/ui_xylos.cpp index c62f1163..d80164c8 100644 --- a/firmware/application/ui_xylos.cpp +++ b/firmware/application/ui_xylos.cpp @@ -25,6 +25,7 @@ #include "ch.h" #include "hackrf_hal.hpp" +#include "event_m0.hpp" #include "ui_alphanum.hpp" #include "ff.h" #include "hackrf_gpio.hpp" @@ -38,7 +39,7 @@ #include #include -using namespace hackrf::one; +using namespace portapack; namespace ui { @@ -91,9 +92,8 @@ void XylosRXView::on_show() { } XylosRXView::XylosRXView( - NavigationView& nav, - ReceiverModel& receiver_model -) : receiver_model(receiver_model) + NavigationView& nav +) { char ccirdebug[21] = { 0,0,0,0,1,8,1,10,10,10,11,1,1,2,0,11,0,0,0,0,0xFF }; @@ -211,9 +211,8 @@ void XylosView::journuit() { } XylosView::XylosView( - NavigationView& nav, - TransmitterModel& transmitter_model -) : transmitter_model(transmitter_model) + NavigationView& nav +) { static constexpr Style style_val { .font = font::fixed_8x16, @@ -228,13 +227,11 @@ XylosView::XylosView( }; transmitter_model.set_baseband_configuration({ - .mode = TX_XYLOS, + .mode = 4, .sampling_rate = 1536000, .decimation_factor = 1, }); - - transmitter_model.set_modulation(TX_XYLOS); // Useless ? - + add_children({ { &text_title, &button_txtest, @@ -323,11 +320,9 @@ XylosView::XylosView( button_txtest.on_select = [this,&transmitter_model](Button&) { const uint8_t ccirtest[21] = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,14,13,12,11,0xFF }; if (txing == false) { - auto& message_map = context().message_map(); + EventDispatcher::message_map().unregister_handler(Message::ID::TXDone); - message_map.unregister_handler(Message::ID::TXDone); - - message_map.register_handler(Message::ID::TXDone, + EventDispatcher::message_map().register_handler(Message::ID::TXDone, [this,&transmitter_model](Message* const p) { const auto message = static_cast(p); if (message->n == 25) { @@ -359,11 +354,9 @@ XylosView::XylosView( if (txing == false) { upd_message(); - auto& message_map = context().message_map(); + EventDispatcher::message_map().unregister_handler(Message::ID::TXDone); - message_map.unregister_handler(Message::ID::TXDone); - - message_map.register_handler(Message::ID::TXDone, + EventDispatcher::message_map().register_handler(Message::ID::TXDone, [this,&transmitter_model](Message* const p) { uint8_t c; char progress[21]; diff --git a/firmware/application/ui_xylos.hpp b/firmware/application/ui_xylos.hpp index 34e52dce..37fd8fb7 100644 --- a/firmware/application/ui_xylos.hpp +++ b/firmware/application/ui_xylos.hpp @@ -45,7 +45,7 @@ void do_something(); class XylosRXView : public View { public: - XylosRXView(NavigationView& nav, ReceiverModel& receiver_model); + XylosRXView(NavigationView& nav); ~XylosRXView(); void talk(); @@ -91,8 +91,6 @@ private: "trailer.wav" }; char ccir_received[21]; - - ReceiverModel& receiver_model; Text text_title { { 1 * 8, 1 * 16, 11, 16 }, @@ -142,7 +140,7 @@ private: class XylosView : public View { public: - XylosView(NavigationView& nav, TransmitterModel& transmitter_model); + XylosView(NavigationView& nav); ~XylosView(); void journuit(); @@ -184,8 +182,6 @@ private: }; char ccirmessage[21]; char ccir_received[21]; - - TransmitterModel& transmitter_model; Text text_title { { 1 * 8, 1 * 16, 11, 16 }, diff --git a/firmware/baseband-tx.bin b/firmware/baseband-tx.bin index 62631276c086e8c55605a8b42745d95d9070c176..226a8ead7d25755ef931d441301980c88b13b16a 100644 GIT binary patch delta 8345 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 delta 13788 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 +#include +#include + +void AudioOutput::configure( + const iir_biquad_config_t& hpf_config, + const iir_biquad_config_t& deemph_config, + const float squelch_threshold +) { + hpf.configure(hpf_config); + deemph.configure(deemph_config); + squelch.set_threshold(squelch_threshold); +} + +void AudioOutput::write( + const buffer_s16_t& audio +) { + std::array audio_f; + for(size_t i=0; i + +class AudioOutput { +public: + void configure( + const iir_biquad_config_t& hpf_config, + const iir_biquad_config_t& deemph_config = iir_config_passthrough, + const float squelch_threshold = 0.0f + ); + + void write(const buffer_s16_t& audio); + void write(const buffer_f32_t& audio); + +private: + IIRBiquadFilter hpf; + IIRBiquadFilter deemph; + FMSquelch squelch; + + AudioStatsCollector audio_stats; + + uint64_t audio_present_history = 0; + + void fill_audio_buffer(const buffer_f32_t& audio); + void feed_audio_stats(const buffer_f32_t& audio); +}; + +#endif/*__AUDIO_OUTPUT_H__*/ diff --git a/firmware/baseband-tx/audio_stats_collector.cpp b/firmware/baseband-tx/audio_stats_collector.cpp new file mode 100644 index 00000000..227d5887 --- /dev/null +++ b/firmware/baseband-tx/audio_stats_collector.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2014 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 "audio_stats_collector.hpp" + +#include "utility.hpp" + +void AudioStatsCollector::consume_audio_buffer(const buffer_f32_t& src) { + auto src_p = src.p; + const auto src_end = &src.p[src.count]; + while(src_p < src_end) { + const auto sample = *(src_p++); + const auto sample_squared = sample * sample; + squared_sum += sample_squared; + if( sample_squared > max_squared ) { + max_squared = sample_squared; + } + } +} + +bool AudioStatsCollector::update_stats(const size_t sample_count, const size_t sampling_rate) { + count += sample_count; + + 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.count = count; + + squared_sum = 0; + max_squared = 0; + count = 0; + + return true; + } else { + return false; + } +} + +bool AudioStatsCollector::feed(const buffer_f32_t& src) { + consume_audio_buffer(src); + + return update_stats(src.count, src.sampling_rate); +} + +bool AudioStatsCollector::mute(const size_t sample_count, const size_t sampling_rate) { + return update_stats(sample_count, sampling_rate); +} diff --git a/firmware/baseband-tx/audio_stats_collector.hpp b/firmware/baseband-tx/audio_stats_collector.hpp index 189fff80..10e17edc 100644 --- a/firmware/baseband-tx/audio_stats_collector.hpp +++ b/firmware/baseband-tx/audio_stats_collector.hpp @@ -22,9 +22,8 @@ #ifndef __AUDIO_STATS_COLLECTOR_H__ #define __AUDIO_STATS_COLLECTOR_H__ -#include "buffer.hpp" +#include "dsp_types.hpp" #include "message.hpp" -#include "utility.hpp" #include #include @@ -32,64 +31,33 @@ class AudioStatsCollector { public: template - void feed(buffer_s16_t src, Callback callback) { - consume_audio_buffer(src); - - if( update_stats(src.count, src.sampling_rate) ) { + void feed(const buffer_f32_t& src, Callback callback) { + if( feed(src) ) { callback(statistics); } } template void mute(const size_t sample_count, const size_t sampling_rate, Callback callback) { - if( update_stats(sample_count, sampling_rate) ) { + if( mute(sample_count, sampling_rate) ) { callback(statistics); } } private: static constexpr float update_interval { 0.1f }; - uint64_t squared_sum { 0 }; - uint32_t max_squared { 0 }; + float squared_sum { 0 }; + float max_squared { 0 }; size_t count { 0 }; AudioStatistics statistics; - void consume_audio_buffer(buffer_s16_t src) { - auto src_p = src.p; - const auto src_end = &src.p[src.count]; - while(src_p < src_end) { - const auto sample = *(src_p++); - const uint64_t sample_squared = sample * sample; - squared_sum += sample_squared; - if( sample_squared > max_squared ) { - max_squared = sample_squared; - } - } - } + void consume_audio_buffer(const buffer_f32_t& src); - bool update_stats(const size_t sample_count, const size_t sampling_rate) { - count += sample_count; + bool update_stats(const size_t sample_count, const size_t sampling_rate); - const size_t samples_per_update = sampling_rate * update_interval; - - if( count >= samples_per_update ) { - const float squared_sum_f = squared_sum; - const float max_squared_f = max_squared; - const float squared_avg_f = squared_sum_f / count; - statistics.rms_db = complex16_mag_squared_to_dbv_norm(squared_avg_f); - statistics.max_db = complex16_mag_squared_to_dbv_norm(max_squared_f); - statistics.count = count; - - squared_sum = 0; - max_squared = 0; - count = 0; - - return true; - } else { - return false; - } - } + bool feed(const buffer_f32_t& src); + bool mute(const size_t sample_count, const size_t sampling_rate); }; #endif/*__AUDIO_STATS_COLLECTOR_H__*/ diff --git a/firmware/baseband-tx/baseband_dma.cpp b/firmware/baseband-tx/baseband_dma.cpp index aa119dc6..7467f551 100644 --- a/firmware/baseband-tx/baseband_dma.cpp +++ b/firmware/baseband-tx/baseband_dma.cpp @@ -20,7 +20,6 @@ */ #include "baseband_dma.hpp" -#include "portapack_shared_memory.hpp" #include #include @@ -35,8 +34,6 @@ using namespace lpc43xx; namespace baseband { namespace dma { - - int quitt = 0; constexpr uint32_t gpdma_ahb_master_sgpio = 0; constexpr uint32_t gpdma_ahb_master_memory = 1; @@ -102,17 +99,12 @@ 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 Mailbox mailbox; -//static std::array messages; static Semaphore semaphore; static volatile const gpdma::channel::LLI* next_lli = nullptr; -void transfer_complete() { +static void transfer_complete() { next_lli = gpdma_channel_sgpio.next_lli(); - quitt = 0; - /* TODO: Is Mailbox the proper synchronization mechanism for this? */ - //chMBPostI(&mailbox, 0); chSemSignalI(&semaphore); } @@ -121,7 +113,6 @@ static void dma_error() { } void init() { - //chMBInit(&mailbox, messages.data(), messages.size()); chSemInit(&semaphore, 0); gpdma_channel_sgpio.set_handlers(transfer_complete, dma_error); @@ -148,7 +139,6 @@ void enable(const baseband::Direction direction) { const auto gpdma_config = config(direction); gpdma_channel_sgpio.configure(lli_loop[0], gpdma_config); - //chMBReset(&mailbox); chSemReset(&semaphore, 0); gpdma_channel_sgpio.enable(); @@ -159,14 +149,11 @@ bool is_enabled() { } void disable() { - gpdma_channel_sgpio.disable_force(); + gpdma_channel_sgpio.disable(); } baseband::buffer_t wait_for_rx_buffer() { - //msg_t msg; - //const auto status = chMBFetch(&mailbox, &msg, TIME_INFINITE); const auto status = chSemWait(&semaphore); - if (quitt) return { nullptr, 0 }; if( status == RDY_OK ) { const auto next = next_lli; if( next ) { @@ -174,28 +161,10 @@ baseband::buffer_t wait_for_rx_buffer() { 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 { nullptr, 0 }; + return { }; } } else { - return { nullptr, 0 }; - } -} - -baseband::buffer_t wait_for_tx_buffer() { - //msg_t msg; - //const auto status = chMBFetch(&mailbox, &msg, TIME_INFINITE); - 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].srcaddr), transfer_samples }; - } else { - return { nullptr, 0 }; - } - } else { - return { nullptr, 0 }; + return { }; } } diff --git a/firmware/baseband-tx/baseband_processor.cpp b/firmware/baseband-tx/baseband_processor.cpp index 1e9ffaf6..4113dec5 100644 --- a/firmware/baseband-tx/baseband_processor.cpp +++ b/firmware/baseband-tx/baseband_processor.cpp @@ -23,104 +23,14 @@ #include "portapack_shared_memory.hpp" -#include "dsp_fft.hpp" - -#include "audio_dma.hpp" - #include "message.hpp" -#include "event_m4.hpp" -#include "utility.hpp" -#include -#include - -void BasebandProcessor::update_spectrum() { - // Called from idle thread (after EVT_MASK_SPECTRUM is flagged) - if( channel_spectrum_request_update ) { - /* Decimated buffer is full. Compute spectrum. */ - channel_spectrum_request_update = false; - fft_c_preswapped(channel_spectrum); - - ChannelSpectrumMessage spectrum_message; - for(size_t i=0; i .magnitude, or something more (less!) accurate. */ - spectrum_message.spectrum.db_count = spectrum_message.spectrum.db.size(); - spectrum_message.spectrum.sampling_rate = channel_spectrum_sampling_rate; - spectrum_message.spectrum.channel_filter_pass_frequency = channel_filter_pass_frequency; - spectrum_message.spectrum.channel_filter_stop_frequency = channel_filter_stop_frequency; - shared_memory.application_queue.push(spectrum_message); - } -} - -void BasebandProcessor::feed_channel_stats(const buffer_c16_t channel) { +void BasebandProcessor::feed_channel_stats(const buffer_c16_t& channel) { channel_stats.feed( channel, - [this](const ChannelStatistics statistics) { - this->post_channel_stats_message(statistics); + [](const ChannelStatistics& statistics) { + const ChannelStatisticsMessage channel_stats_message { statistics }; + shared_memory.application_queue.push(channel_stats_message); } ); } - -void BasebandProcessor::feed_channel_spectrum( - const buffer_c16_t channel, - const uint32_t filter_pass_frequency, - const uint32_t filter_stop_frequency -) { - channel_filter_pass_frequency = filter_pass_frequency; - channel_filter_stop_frequency = filter_stop_frequency; - channel_spectrum_decimator.feed( - channel, - [this](const buffer_c16_t data) { - this->post_channel_spectrum_message(data); - } - ); -} - -void BasebandProcessor::fill_audio_buffer(const buffer_s16_t audio) { - auto audio_buffer = audio::dma::tx_empty_buffer();; - for(size_t i=0; ipost_audio_stats_message(statistics); - } - ); -} - -void BasebandProcessor::post_audio_stats_message(const AudioStatistics statistics) { - audio_stats_message.statistics = statistics; - shared_memory.application_queue.push(audio_stats_message); -} - -void BasebandProcessor::fill_buffer(int8_t * inptr) { - (void)inptr; -} diff --git a/firmware/baseband-tx/baseband_processor.hpp b/firmware/baseband-tx/baseband_processor.hpp index d7f87e38..28ef5063 100644 --- a/firmware/baseband-tx/baseband_processor.hpp +++ b/firmware/baseband-tx/baseband_processor.hpp @@ -23,55 +23,24 @@ #define __BASEBAND_PROCESSOR_H__ #include "dsp_types.hpp" -#include "complex.hpp" -#include "block_decimator.hpp" #include "channel_stats_collector.hpp" -#include "audio_stats_collector.hpp" -#include -#include -#include +#include "message.hpp" class BasebandProcessor { public: virtual ~BasebandProcessor() = default; - virtual void execute(buffer_c8_t buffer) = 0; - virtual void fill_buffer(int8_t * inptr); + virtual void execute(const buffer_c8_t& buffer) = 0; - void update_spectrum(); + virtual void on_message(const Message* const) { }; protected: - void feed_channel_stats(const buffer_c16_t channel); - - void feed_channel_spectrum( - const buffer_c16_t channel, - const uint32_t filter_pass_frequency, - const uint32_t filter_stop_frequency - ); - - void fill_audio_buffer(const buffer_s16_t audio); - - volatile bool channel_spectrum_request_update { false }; - std::array, 256> channel_spectrum; - uint32_t channel_spectrum_sampling_rate { 0 }; - uint32_t channel_filter_pass_frequency { 0 }; - uint32_t channel_filter_stop_frequency { 0 }; + void feed_channel_stats(const buffer_c16_t& channel); private: - BlockDecimator<256> channel_spectrum_decimator { 4 }; - ChannelStatsCollector channel_stats; - ChannelStatisticsMessage channel_stats_message; - - AudioStatsCollector audio_stats; - AudioStatisticsMessage audio_stats_message; - - void post_channel_stats_message(const ChannelStatistics statistics); - void post_channel_spectrum_message(const buffer_c16_t data); - void feed_audio_stats(const buffer_s16_t audio); - void post_audio_stats_message(const AudioStatistics statistics); }; #endif/*__BASEBAND_PROCESSOR_H__*/ diff --git a/firmware/baseband-tx/baseband_stats_collector.cpp b/firmware/baseband-tx/baseband_stats_collector.cpp new file mode 100644 index 00000000..8eec12c5 --- /dev/null +++ b/firmware/baseband-tx/baseband_stats_collector.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2014 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 "baseband_stats_collector.hpp" + +#include "lpc43xx_cpp.hpp" + +bool BasebandStatsCollector::process(const buffer_c8_t& buffer) { + samples += buffer.count; + + const size_t report_samples = buffer.sampling_rate * report_interval; + const auto report_delta = samples - samples_last_report; + return report_delta >= report_samples; +} + +BasebandStatistics BasebandStatsCollector::capture_statistics() { + BasebandStatistics statistics; + + const auto idle_ticks = thread_idle->total_ticks; + statistics.idle_ticks = (idle_ticks - last_idle_ticks); + last_idle_ticks = idle_ticks; + + const auto main_ticks = thread_main->total_ticks; + statistics.main_ticks = (main_ticks - last_main_ticks); + last_main_ticks = main_ticks; + + const auto rssi_ticks = thread_rssi->total_ticks; + statistics.rssi_ticks = (rssi_ticks - last_rssi_ticks); + last_rssi_ticks = rssi_ticks; + + const auto baseband_ticks = thread_baseband->total_ticks; + statistics.baseband_ticks = (baseband_ticks - last_baseband_ticks); + last_baseband_ticks = baseband_ticks; + + statistics.saturation = lpc43xx::m4::flag_saturation(); + lpc43xx::m4::clear_flag_saturation(); + + samples_last_report = samples; + + return statistics; +} diff --git a/firmware/baseband-tx/baseband_stats_collector.hpp b/firmware/baseband-tx/baseband_stats_collector.hpp index a44c2936..b628a8b2 100644 --- a/firmware/baseband-tx/baseband_stats_collector.hpp +++ b/firmware/baseband-tx/baseband_stats_collector.hpp @@ -26,7 +26,6 @@ #include "dsp_types.hpp" #include "message.hpp" -#include "utility_m4.hpp" #include #include @@ -46,36 +45,9 @@ public: } template - void process(buffer_c8_t buffer, Callback callback) { - samples += buffer.count; - - const size_t report_samples = buffer.sampling_rate * report_interval; - const auto report_delta = samples - samples_last_report; - if( report_delta >= report_samples ) { - BasebandStatistics statistics; - - const auto idle_ticks = thread_idle->total_ticks; - statistics.idle_ticks = (idle_ticks - last_idle_ticks); - last_idle_ticks = idle_ticks; - - const auto main_ticks = thread_main->total_ticks; - statistics.main_ticks = (main_ticks - last_main_ticks); - last_main_ticks = main_ticks; - - const auto rssi_ticks = thread_rssi->total_ticks; - statistics.rssi_ticks = (rssi_ticks - last_rssi_ticks); - last_rssi_ticks = rssi_ticks; - - const auto baseband_ticks = thread_baseband->total_ticks; - statistics.baseband_ticks = (baseband_ticks - last_baseband_ticks); - last_baseband_ticks = baseband_ticks; - - statistics.saturation = m4_flag_saturation(); - clear_m4_flag_saturation(); - - callback(statistics); - - samples_last_report = samples; + void process(const buffer_c8_t& buffer, Callback callback) { + if( process(buffer) ) { + callback(capture_statistics()); } } @@ -91,6 +63,9 @@ private: uint32_t last_rssi_ticks { 0 }; const Thread* const thread_baseband; uint32_t last_baseband_ticks { 0 }; + + bool process(const buffer_c8_t& buffer); + BasebandStatistics capture_statistics(); }; #endif/*__BASEBAND_STATS_COLLECTOR_H__*/ diff --git a/firmware/baseband-tx/baseband_thread.cpp b/firmware/baseband-tx/baseband_thread.cpp new file mode 100644 index 00000000..b25789b9 --- /dev/null +++ b/firmware/baseband-tx/baseband_thread.cpp @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2014 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 "baseband_thread.hpp" + +#include "dsp_types.hpp" + +#include "baseband.hpp" +#include "baseband_stats_collector.hpp" +#include "baseband_sgpio.hpp" +#include "baseband_dma.hpp" + +#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 "portapack_shared_memory.hpp" + +#include + +static baseband::SGPIO baseband_sgpio; + +WORKING_AREA(baseband_thread_wa, 4096); + +Thread* BasebandThread::start(const tprio_t priority) { + return chThdCreateStatic(baseband_thread_wa, sizeof(baseband_thread_wa), + priority, ThreadBase::fn, + this + ); +} + +void BasebandThread::set_configuration(const BasebandConfiguration& new_configuration) { + if( new_configuration.mode != baseband_configuration.mode ) { + disable(); + + // TODO: Timing problem around disabling DMA and nulling and deleting old processor + auto old_p = baseband_processor; + baseband_processor = nullptr; + delete old_p; + + baseband_processor = create_processor(new_configuration.mode); + + enable(); + } + + baseband_configuration = new_configuration; +} + +void BasebandThread::on_message(const Message* const message) { + if( message->id == Message::ID::BasebandConfiguration ) { + set_configuration(reinterpret_cast(message)->configuration); + } else { + if( baseband_processor ) { + baseband_processor->on_message(message); + } + } +} + +void BasebandThread::run() { + baseband_sgpio.init(); + baseband::dma::init(); + + const auto baseband_buffer = new std::array(); + baseband::dma::configure( + baseband_buffer->data(), + direction() + ); + //baseband::dma::allocate(4, 2048); + + BasebandStatsCollector stats { + chSysGetIdleThread(), + thread_main, + thread_rssi, + chThdSelf() + }; + + while(true) { + // TODO: Place correct sampling rate into buffer returned here: + const auto buffer_tmp = baseband::dma::wait_for_rx_buffer(); + if( buffer_tmp ) { + buffer_c8_t buffer { + buffer_tmp.p, buffer_tmp.count, baseband_configuration.sampling_rate + }; + + if( baseband_processor ) { + baseband_processor->execute(buffer); + } + + stats.process(buffer, + [](const BasebandStatistics& statistics) { + const BasebandStatisticsMessage message { statistics }; + shared_memory.application_queue.push(message); + } + ); + } + } + + 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(); + default: return nullptr; + } +} + +void BasebandThread::disable() { + if( baseband_processor ) { + i2s::i2s0::tx_mute(); + baseband::dma::disable(); + baseband_sgpio.streaming_disable(); + rf::rssi::stop(); + } +} + +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 new file mode 100644 index 00000000..81d35433 --- /dev/null +++ b/firmware/baseband-tx/baseband_thread.hpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2014 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 __BASEBAND_THREAD_H__ +#define __BASEBAND_THREAD_H__ + +#include "thread_base.hpp" +#include "message.hpp" +#include "baseband_processor.hpp" + +#include + +class BasebandThread : public ThreadBase { +public: + BasebandThread( + ) : ThreadBase { "baseband" } + { + } + + Thread* start(const tprio_t priority); + + void on_message(const Message* const message); + + // 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; + } + + Thread* thread_main { nullptr }; + Thread* thread_rssi { nullptr }; + BasebandProcessor* baseband_processor { nullptr }; + +private: + BasebandConfiguration baseband_configuration; + + void run() override; + + BasebandProcessor* create_processor(const int32_t mode); + + void disable(); + void enable(); + + void set_configuration(const BasebandConfiguration& new_configuration); +}; + +#endif/*__BASEBAND_THREAD_H__*/ diff --git a/firmware/baseband-tx/description b/firmware/baseband-tx/description index f2606500..4af72ebe 100644 --- a/firmware/baseband-tx/description +++ b/firmware/baseband-tx/description @@ -1 +1 @@ -More specific stuff for testing :o +More or less experimental stuff, mainly TX. diff --git a/firmware/baseband-tx/dsp_iir.cpp b/firmware/baseband-tx/dsp_iir.cpp new file mode 100644 index 00000000..443183ba --- /dev/null +++ b/firmware/baseband-tx/dsp_iir.cpp @@ -0,0 +1,57 @@ +/* + * 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 "dsp_iir.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; + + auto x_ = x; + auto y_ = y; + + // TODO: Assert that buffer_out.count == buffer_in.count. + for(size_t i=0; i b; - const std::array a; + 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( @@ -42,34 +56,15 @@ public: { } - void execute(buffer_s16_t buffer_in, buffer_s16_t buffer_out) { - // TODO: Assert that buffer_out.count == buffer_in.count. - for(size_t i=0; i x { { 0.0f, 0.0f, 0.0f } }; std::array y { { 0.0f, 0.0f, 0.0f } }; - - float execute_sample(const float in) { - x[0] = x[1]; - x[1] = x[2]; - x[2] = in; - - y[0] = y[1]; - y[1] = y[2]; - y[2] = config.b[0] * x[2] + config.b[1] * x[1] + config.b[2] * x[0] - - config.a[1] * y[1] - config.a[2] * y[0]; - - return y[2]; - } }; #endif/*__DSP_IIR_H__*/ diff --git a/firmware/baseband-tx/dsp_squelch.cpp b/firmware/baseband-tx/dsp_squelch.cpp index 4cfe651a..b4a2d3a4 100644 --- a/firmware/baseband-tx/dsp_squelch.cpp +++ b/firmware/baseband-tx/dsp_squelch.cpp @@ -24,22 +24,30 @@ #include #include -bool FMSquelch::execute(buffer_s16_t audio) { +bool FMSquelch::execute(const buffer_f32_t& audio) { + if( threshold_squared == 0.0f ) { + return true; + } + // TODO: No hard-coded array size. - std::array squelch_energy_buffer; - const buffer_s16_t squelch_energy { + std::array squelch_energy_buffer; + const buffer_f32_t squelch_energy { squelch_energy_buffer.data(), squelch_energy_buffer.size() }; non_audio_hpf.execute(audio, squelch_energy); - uint64_t max_squared = 0; + float non_audio_max_squared = 0; for(const auto sample : squelch_energy_buffer) { - const uint64_t sample_squared = sample * sample; - if( sample_squared > max_squared ) { - max_squared = sample_squared; + const float sample_squared = sample * sample; + if( sample_squared > non_audio_max_squared ) { + non_audio_max_squared = sample_squared; } } - return (max_squared < (threshold * threshold)); + return (non_audio_max_squared < threshold_squared); +} + +void FMSquelch::set_threshold(const float new_value) { + threshold_squared = new_value * new_value; } diff --git a/firmware/baseband-tx/dsp_squelch.hpp b/firmware/baseband-tx/dsp_squelch.hpp index 06cb2cdd..4701b9ac 100644 --- a/firmware/baseband-tx/dsp_squelch.hpp +++ b/firmware/baseband-tx/dsp_squelch.hpp @@ -31,14 +31,14 @@ class FMSquelch { public: - bool execute(buffer_s16_t audio); + bool execute(const buffer_f32_t& audio); + + void set_threshold(const float new_value); private: static constexpr size_t N = 32; - static constexpr int16_t threshold = 3072; + float threshold_squared { 0.0f }; - // nyquist = 48000 / 2.0 - // scipy.signal.iirdesign(wp=8000 / nyquist, ws= 4000 / nyquist, gpass=1, gstop=18, ftype='ellip') IIRBiquadFilter non_audio_hpf { non_audio_hpf_config }; }; diff --git a/firmware/baseband-tx/event_m4.cpp b/firmware/baseband-tx/event_m4.cpp index 50b0c0b7..0e90cd15 100644 --- a/firmware/baseband-tx/event_m4.cpp +++ b/firmware/baseband-tx/event_m4.cpp @@ -21,10 +21,99 @@ #include "event_m4.hpp" +#include "portapack_shared_memory.hpp" + +#include "message_queue.hpp" + #include "ch.h" -Thread* thread_event_loop = nullptr; +#include "lpc43xx_cpp.hpp" +using namespace lpc43xx; -void events_initialize(Thread* const event_loop_thread) { - thread_event_loop = event_loop_thread; +#include +#include + +extern "C" { + +CH_IRQ_HANDLER(MAPP_IRQHandler) { + CH_IRQ_PROLOGUE(); + + chSysLockFromIsr(); + EventDispatcher::events_flag_isr(EVT_MASK_BASEBAND); + chSysUnlockFromIsr(); + + creg::m0apptxevent::clear(); + + CH_IRQ_EPILOGUE(); +} + +} + +Thread* EventDispatcher::thread_event_loop = nullptr; + +void EventDispatcher::run() { + thread_event_loop = chThdSelf(); + lpc43xx::creg::m0apptxevent::enable(); + + baseband_thread.thread_main = chThdSelf(); + baseband_thread.thread_rssi = rssi_thread.start(NORMALPRIO + 10); + baseband_thread.start(NORMALPRIO + 20); + + while(is_running) { + const auto events = wait(); + dispatch(events); + } + + lpc43xx::creg::m0apptxevent::disable(); +} + +void EventDispatcher::request_stop() { + is_running = false; +} + +eventmask_t EventDispatcher::wait() { + return chEvtWaitAny(ALL_EVENTS); +} + +void EventDispatcher::dispatch(const eventmask_t events) { + if( events & EVT_MASK_BASEBAND ) { + handle_baseband_queue(); + } + + if( events & EVT_MASK_SPECTRUM ) { + handle_spectrum(); + } +} + +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(); + } +} + +void EventDispatcher::on_message(const Message* const message) { + switch(message->id) { + case Message::ID::Shutdown: + on_message_shutdown(*reinterpret_cast(message)); + break; + + default: + on_message_default(message); + break; + } +} + +void EventDispatcher::on_message_shutdown(const ShutdownMessage&) { + request_stop(); +} + +void EventDispatcher::on_message_default(const Message* const message) { + baseband_thread.on_message(message); +} + +void EventDispatcher::handle_spectrum() { + const UpdateSpectrumMessage message; + baseband_thread.on_message(&message); } diff --git a/firmware/baseband-tx/event_m4.hpp b/firmware/baseband-tx/event_m4.hpp index be113ca8..ad9871d1 100644 --- a/firmware/baseband-tx/event_m4.hpp +++ b/firmware/baseband-tx/event_m4.hpp @@ -22,25 +22,54 @@ #ifndef __EVENT_M4_H__ #define __EVENT_M4_H__ +#include "event.hpp" + +#include "baseband_thread.hpp" +#include "rssi_thread.hpp" + +#include "message.hpp" + #include "ch.h" constexpr auto EVT_MASK_BASEBAND = EVENT_MASK(0); constexpr auto EVT_MASK_SPECTRUM = EVENT_MASK(1); -void events_initialize(Thread* const event_loop_thread); +class EventDispatcher { +public: + void run(); + void request_stop(); -extern Thread* thread_event_loop; - -inline void events_flag(const eventmask_t events) { - if( thread_event_loop ) { - chEvtSignal(thread_event_loop, events); + static inline void events_flag(const eventmask_t events) { + if( thread_event_loop ) { + chEvtSignal(thread_event_loop, events); + } } -} -inline void events_flag_isr(const eventmask_t events) { - if( thread_event_loop ) { - chEvtSignalI(thread_event_loop, events); + static inline void events_flag_isr(const eventmask_t events) { + if( thread_event_loop ) { + chEvtSignalI(thread_event_loop, events); + } } -} + +private: + static Thread* thread_event_loop; + + BasebandThread baseband_thread; + RSSIThread rssi_thread; + + bool is_running = true; + + eventmask_t wait(); + + void dispatch(const eventmask_t events); + + void handle_baseband_queue(); + + void on_message(const Message* const message); + void on_message_shutdown(const ShutdownMessage&); + void on_message_default(const Message* const message); + + void handle_spectrum(); +}; #endif/*__EVENT_M4_H__*/ diff --git a/firmware/baseband-tx/main.cpp b/firmware/baseband-tx/main.cpp index 586f245c..3222755d 100755 --- a/firmware/baseband-tx/main.cpp +++ b/firmware/baseband-tx/main.cpp @@ -15,13 +15,11 @@ * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to - * 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 "ch.h" -#include "test.h" #include "lpc43xx_cpp.hpp" @@ -30,34 +28,13 @@ #include "gpdma.hpp" -#include "baseband.hpp" -#include "baseband_dma.hpp" - #include "event_m4.hpp" -#include "irq_ipc_m4.hpp" - #include "touch_dma.hpp" -#include "modules.h" - -#include "dsp_decimate.hpp" -#include "dsp_demodulate.hpp" -#include "dsp_fft.hpp" -#include "dsp_fir_taps.hpp" -#include "dsp_iir.hpp" -#include "dsp_iir_config.hpp" -#include "dsp_squelch.hpp" - -#include "channel_decimator.hpp" +#include "baseband_thread.hpp" +#include "rssi_thread.hpp" #include "baseband_processor.hpp" -#include "proc_fsk_lcr.hpp" -#include "proc_jammer.hpp" -#include "proc_xylos.hpp" -#include "proc_playaudio.hpp" - -#include "clock_recovery.hpp" -#include "packet_builder.hpp" #include "message_queue.hpp" @@ -73,55 +50,87 @@ #include #include #include -#include -#include -#include -static baseband::Direction direction = baseband::Direction::Receive; +extern "C" { -class ThreadBase { -public: - constexpr ThreadBase( - const char* const name - ) : name { name } - { +void __late_init(void) { + /* + * System initializations. + * - HAL initialization, this also initializes the configured device drivers + * and performs the board-specific initializations. + * - Kernel initialization, the main() function becomes a thread and the + * RTOS is active. + */ + halInit(); + + /* After this call, scheduler, systick, heap, etc. are available. */ + /* By doing chSysInit() here, it runs before C++ constructors, which may + * require the heap. + */ + chSysInit(); +} + +} + +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)); + + touch::dma::init(); + touch::dma::allocate(); + touch::dma::enable(); +} + +static void halt() { + port_disable(); + while(true) { + port_wait_for_interrupt(); } +} - static msg_t fn(void* arg) { - auto obj = static_cast(arg); - chRegSetThreadName(obj->name); - obj->run(); +static void shutdown() { + // TODO: Is this complete? + + nvicDisableVector(DMA_IRQn); + + chSysDisable(); - return 0; - } + systick_stop(); - virtual void run() = 0; + ShutdownMessage shutdown_message; + shared_memory.application_queue.push(shutdown_message); -private: - const char* const name; -}; + halt(); +} -class BasebandThread : public ThreadBase { -public: - BasebandThread( - ) : ThreadBase { "baseband" } - { - } +int main(void) { + init(); - Thread* start(const tprio_t priority) { - return chThdCreateStatic(wa, sizeof(wa), - priority, ThreadBase::fn, - this - ); - } + /* TODO: Ensure DMAs are configured to point at first LLI in chain. */ - Thread* thread_main { nullptr }; - BasebandProcessor* baseband_processor { nullptr }; - BasebandConfiguration baseband_configuration; + EventDispatcher event_dispatcher; + event_dispatcher.run(); -private: - WORKING_AREA(wa, 2048); + shutdown(); + return 0; +} + +/* void run() override { while(true) { if (direction == baseband::Direction::Transmit) { @@ -149,167 +158,6 @@ private: } }; -#define SAMPLES_PER_BIT 192 -#define FILTER_SIZE 576 -#define SAMPLE_BUFFER_SIZE SAMPLES_PER_BIT + FILTER_SIZE - -static int32_t waveform_biphase[] = { - 165,167,168,168,167,166,163,160, - 157,152,147,141,134,126,118,109, - 99,88,77,66,53,41,27,14, - 0,-14,-29,-44,-59,-74,-89,-105, - -120,-135,-150,-165,-179,-193,-206,-218, - -231,-242,-252,-262,-271,-279,-286,-291, - -296,-299,-301,-302,-302,-300,-297,-292, - -286,-278,-269,-259,-247,-233,-219,-202, - -185,-166,-145,-124,-101,-77,-52,-26, - 0,27,56,85,114,144,175,205, - 236,266,296,326,356,384,412,439, - 465,490,513,535,555,574,590,604, - 616,626,633,637,639,638,633,626, - 616,602,586,565,542,515,485,451, - 414,373,329,282,232,178,121,62, - 0,-65,-132,-202,-274,-347,-423,-500, - -578,-656,-736,-815,-894,-973,-1051,-1128, - -1203,-1276,-1347,-1415,-1479,-1540,-1596,-1648, - -1695,-1736,-1771,-1799,-1820,-1833,-1838,-1835, - -1822,-1800,-1767,-1724,-1670,-1605,-1527,-1437, - -1334,-1217,-1087,-943,-785,-611,-423,-219, - 0,235,487,755,1040,1341,1659,1994, - 2346,2715,3101,3504,3923,4359,4811,5280, - 5764,6264,6780,7310,7856,8415,8987,9573, - 10172,10782,11404,12036,12678,13329,13989,14656, - 15330,16009,16694,17382,18074,18767,19461,20155, - 20848,21539,22226,22909,23586,24256,24918,25571, - 26214,26845,27464,28068,28658,29231,29787,30325, - 30842,31339,31814,32266,32694,33097,33473,33823, - 34144,34437,34699,34931,35131,35299,35434,35535, - 35602,35634,35630,35591,35515,35402,35252,35065, - 34841,34579,34279,33941,33566,33153,32702,32214, - 31689,31128,30530,29897,29228,28525,27788,27017, - 26214,25379,24513,23617,22693,21740,20761,19755, - 18725,17672,16597,15501,14385,13251,12101,10935, - 9755,8563,7360,6148,4927,3701,2470,1235, - 0,-1235,-2470,-3701,-4927,-6148,-7360,-8563, - -9755,-10935,-12101,-13251,-14385,-15501,-16597,-17672, - -18725,-19755,-20761,-21740,-22693,-23617,-24513,-25379, - -26214,-27017,-27788,-28525,-29228,-29897,-30530,-31128, - -31689,-32214,-32702,-33153,-33566,-33941,-34279,-34579, - -34841,-35065,-35252,-35402,-35515,-35591,-35630,-35634, - -35602,-35535,-35434,-35299,-35131,-34931,-34699,-34437, - -34144,-33823,-33473,-33097,-32694,-32266,-31814,-31339, - -30842,-30325,-29787,-29231,-28658,-28068,-27464,-26845, - -26214,-25571,-24918,-24256,-23586,-22909,-22226,-21539, - -20848,-20155,-19461,-18767,-18074,-17382,-16694,-16009, - -15330,-14656,-13989,-13329,-12678,-12036,-11404,-10782, - -10172,-9573,-8987,-8415,-7856,-7310,-6780,-6264, - -5764,-5280,-4811,-4359,-3923,-3504,-3101,-2715, - -2346,-1994,-1659,-1341,-1040,-755,-487,-235, - 0,219,423,611,785,943,1087,1217, - 1334,1437,1527,1605,1670,1724,1767,1800, - 1822,1835,1838,1833,1820,1799,1771,1736, - 1695,1648,1596,1540,1479,1415,1347,1276, - 1203,1128,1051,973,894,815,736,656, - 578,500,423,347,274,202,132,65, - 0,-62,-121,-178,-232,-282,-329,-373, - -414,-451,-485,-515,-542,-565,-586,-602, - -616,-626,-633,-638,-639,-637,-633,-626, - -616,-604,-590,-574,-555,-535,-513,-490, - -465,-439,-412,-384,-356,-326,-296,-266, - -236,-205,-175,-144,-114,-85,-56,-27, - 0,26,52,77,101,124,145,166, - 185,202,219,233,247,259,269,278, - 286,292,297,300,302,302,301,299, - 296,291,286,279,271,262,252,242, - 231,218,206,193,179,165,150,135, - 120,105,89,74,59,44,29,14, - 0,-14,-27,-41,-53,-66,-77,-88, - -99,-109,-118,-126,-134,-141,-147,-152, - -157,-160,-163,-166,-167,-168,-168,-167 -}; - -class RDSProcessor : public BasebandProcessor { -public: - void execute(buffer_c8_t buffer) override { - - for (size_t i = 0; i= 9) { - s = 0; - if(sample_count >= SAMPLES_PER_BIT) { - cur_bit = (shared_memory.rdsdata[(bit_pos / 26) & 15]>>(25-(bit_pos % 26))) & 1; - prev_output = cur_output; - cur_output = prev_output ^ cur_bit; - - int32_t *src = waveform_biphase; - int idx = in_sample_index; - - for(int j=0; j= SAMPLE_BUFFER_SIZE) idx = 0; - } - - in_sample_index += SAMPLES_PER_BIT; - if (in_sample_index >= SAMPLE_BUFFER_SIZE) in_sample_index -= SAMPLE_BUFFER_SIZE; - - bit_pos++; - sample_count = 0; - } - - sample = sample_buffer[out_sample_index]; - sample_buffer[out_sample_index] = 0; - out_sample_index++; - if (out_sample_index >= SAMPLE_BUFFER_SIZE) out_sample_index = 0; - - //AM @ 228k/4=57kHz - switch (mphase) { - case 0: - case 2: sample = 0; break; - case 1: break; - case 3: sample = -sample; break; - } - mphase++; - if (mphase >= 4) mphase = 0; - - sample_count++; - } else { - s++; - } - - //FM - frq = (sample>>16) * 386760; - - 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 mphase, s; - uint32_t bit_pos; - int32_t sample_buffer[SAMPLE_BUFFER_SIZE] = {0}; - int32_t val; - uint8_t prev_output = 0; - uint8_t cur_output = 0; - uint8_t cur_bit = 0; - int sample_count = SAMPLES_PER_BIT; - int in_sample_index = 0; - int32_t sample; - int out_sample_index = SAMPLE_BUFFER_SIZE-1; - uint32_t phase, sphase; - int32_t sig, frq, frq_im, rdsc; - int32_t k; -}; - class ToneProcessor : public BasebandProcessor { public: void execute(buffer_c8_t buffer) override { @@ -348,130 +196,6 @@ private: int32_t sample, sig, frq; }; -extern "C" { - -void __late_init(void) { - /* - * System initializations. - * - HAL initialization, this also initializes the configured device drivers - * and performs the board-specific initializations. - * - Kernel initialization, the main() function becomes a thread and the - * RTOS is active. - */ - halInit(); - - /* After this call, scheduler, systick, heap, etc. are available. */ - /* By doing chSysInit() here, it runs before C++ constructors, which may - * require the heap. - */ - chSysInit(); -} - -} - -static BasebandThread baseband_thread; - -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)); - - baseband::dma::init(); - - touch::dma::init(); - - const auto thread_main = chThdSelf(); - - baseband_thread.thread_main = thread_main; - - baseband_thread.start(NORMALPRIO + 20); -} - -static void shutdown() { - // TODO: Is this complete? - - nvicDisableVector(DMA_IRQn); - - m0apptxevent_interrupt_disable(); - - chSysDisable(); - - systick_stop(); -} - -static void halt() { - port_disable(); - while(true) { - port_wait_for_interrupt(); - } -} - -class EventDispatcher { -public: - MessageHandlerMap& message_handlers() { - return message_map; - } - - void run() { - while(is_running) { - const auto events = wait(); - dispatch(events); - } - } - - void request_stop() { - is_running = false; - } - -private: - MessageHandlerMap message_map; - - bool is_running = true; - - eventmask_t wait() { - return chEvtWaitAny(ALL_EVENTS); - } - - void dispatch(const eventmask_t events) { - if( events & EVT_MASK_BASEBAND ) { - handle_baseband_queue(); - } - - if( events & EVT_MASK_SPECTRUM ) { - handle_spectrum(); - } - } - - void handle_baseband_queue() { - std::array message_buffer; - while(Message* const message = shared_memory.baseband_queue.pop(message_buffer)) { - message_map.send(message); - } - } - - void handle_spectrum() { - if( baseband_thread.baseband_processor ) { - baseband_thread.baseband_processor->update_spectrum(); - } - } -}; - -const auto baseband_buffer = - new std::array(); - char ram_loop[32]; typedef int (*fn_ptr)(void); fn_ptr loop_ptr; @@ -586,27 +310,4 @@ int main(void) { event_dispatcher.request_stop(); } ); - - /* TODO: Ensure DMAs are configured to point at first LLI in chain. */ - - touch::dma::allocate(); - touch::dma::enable(); - - baseband::dma::configure( - baseband_buffer->data(), - direction - ); - - //baseband::dma::allocate(4, 2048); - - event_dispatcher.run(); - - shutdown(); - - ShutdownMessage shutdown_message; - shared_memory.application_queue.push(shutdown_message); - - halt(); - - return 0; -} +*/ diff --git a/firmware/baseband-tx/name b/firmware/baseband-tx/name index ff5e208f..dd4d2a91 100644 --- a/firmware/baseband-tx/name +++ b/firmware/baseband-tx/name @@ -1 +1 @@ -Second module +Experimental diff --git a/firmware/baseband-tx/proc_audiotx.cpp b/firmware/baseband-tx/proc_audiotx.cpp index 8d51287f..c9c9c4bd 100644 --- a/firmware/baseband-tx/proc_audiotx.cpp +++ b/firmware/baseband-tx/proc_audiotx.cpp @@ -26,6 +26,6 @@ #include -void AudioTXProcessor::execute(buffer_c8_t buffer) { +void AudioTXProcessor::execute(const buffer_c8_t& buffer) { } diff --git a/firmware/baseband-tx/proc_audiotx.hpp b/firmware/baseband-tx/proc_audiotx.hpp index 30a1c030..53c639e2 100644 --- a/firmware/baseband-tx/proc_audiotx.hpp +++ b/firmware/baseband-tx/proc_audiotx.hpp @@ -29,7 +29,8 @@ class AudioTXProcessor : public BasebandProcessor { public: - void execute(buffer_c8_t buffer) override; + void execute(const buffer_c8_t& buffer) override; + private: int8_t audio_fifo[SAMPLERATE]; diff --git a/firmware/baseband-tx/proc_fsk_lcr.cpp b/firmware/baseband-tx/proc_fsk_lcr.cpp index 4e0b8112..a7de6a09 100644 --- a/firmware/baseband-tx/proc_fsk_lcr.cpp +++ b/firmware/baseband-tx/proc_fsk_lcr.cpp @@ -25,7 +25,7 @@ #include -void LCRFSKProcessor::execute(buffer_c8_t buffer) { +void LCRFSKProcessor::execute(const buffer_c8_t& buffer) { for (size_t i = 0; i 3000000) { - s = 0; - feedback = lfsr & 1; - lfsr >>= 1; - if (feedback == 1) - lfsr ^= POLY_MASK_32; - } else { - s++; - } - - aphase += lfsr;*/ - - /*if (s >= 10) { - s = 0; - aphase += 353205; // DEBUG - } else { - s++; - } - - sample = sintab[(aphase & 0x03FF0000)>>16];*/ - - // Duration timer - // if (s >= 10000) { //shared_memory.jammer_ranges[ir].duration s = 0; for (;;) { diff --git a/firmware/baseband-tx/proc_jammer.hpp b/firmware/baseband-tx/proc_jammer.hpp index 9a75940a..50cca639 100644 --- a/firmware/baseband-tx/proc_jammer.hpp +++ b/firmware/baseband-tx/proc_jammer.hpp @@ -26,7 +26,7 @@ class JammerProcessor : public BasebandProcessor { public: - void execute(buffer_c8_t buffer) override; + void execute(const buffer_c8_t& buffer) override; private: int32_t lfsr32 = 0xABCDE; diff --git a/firmware/baseband-tx/proc_playaudio.cpp b/firmware/baseband-tx/proc_playaudio.cpp index 816c7fd1..c9fcaba3 100644 --- a/firmware/baseband-tx/proc_playaudio.cpp +++ b/firmware/baseband-tx/proc_playaudio.cpp @@ -33,7 +33,7 @@ void PlayAudioProcessor::fill_buffer(int8_t * inptr) { asked = false; } -void PlayAudioProcessor::execute(buffer_c8_t buffer) { +void PlayAudioProcessor::execute(const buffer_c8_t& buffer){ // This is called at 1536000/2048 = 750Hz @@ -69,5 +69,5 @@ void PlayAudioProcessor::execute(buffer_c8_t buffer) { buffer.p[i] = {(int8_t)re,(int8_t)im}; } - fill_audio_buffer(preview_audio_buffer); + //fill_audio_buffer(preview_audio_buffer); } diff --git a/firmware/baseband-tx/proc_playaudio.hpp b/firmware/baseband-tx/proc_playaudio.hpp index 07c67412..e07c7b01 100644 --- a/firmware/baseband-tx/proc_playaudio.hpp +++ b/firmware/baseband-tx/proc_playaudio.hpp @@ -27,7 +27,7 @@ class PlayAudioProcessor : public BasebandProcessor { public: - void execute(buffer_c8_t buffer) override; + void execute(const buffer_c8_t& buffer) override; void fill_buffer(int8_t * inptr); private: diff --git a/firmware/baseband-tx/proc_rds.cpp b/firmware/baseband-tx/proc_rds.cpp new file mode 100644 index 00000000..d6fb366f --- /dev/null +++ b/firmware/baseband-tx/proc_rds.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * + * 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 "proc_rds.hpp" +#include "portapack_shared_memory.hpp" +#include "sine_table.hpp" + +#include + +void RDSProcessor::execute(const buffer_c8_t& buffer) { + + for (size_t i = 0; i= 9) { + s = 0; + if(sample_count >= SAMPLES_PER_BIT) { + cur_bit = (shared_memory.rdsdata[(bit_pos / 26) & 15]>>(25-(bit_pos % 26))) & 1; + prev_output = cur_output; + cur_output = prev_output ^ cur_bit; + + int32_t *src = waveform_biphase; + int idx = in_sample_index; + + for(int j=0; j= SAMPLE_BUFFER_SIZE) idx = 0; + } + + in_sample_index += SAMPLES_PER_BIT; + if (in_sample_index >= SAMPLE_BUFFER_SIZE) in_sample_index -= SAMPLE_BUFFER_SIZE; + + bit_pos++; + sample_count = 0; + } + + sample = sample_buffer[out_sample_index]; + sample_buffer[out_sample_index] = 0; + out_sample_index++; + if (out_sample_index >= SAMPLE_BUFFER_SIZE) out_sample_index = 0; + + //AM @ 228k/4=57kHz + switch (mphase) { + case 0: + case 2: sample = 0; break; + case 1: break; + case 3: sample = -sample; break; + } + mphase++; + if (mphase >= 4) mphase = 0; + + sample_count++; + } else { + s++; + } + + //FM + frq = (sample>>16) * 386760; + + 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}; + } +} + diff --git a/firmware/baseband-tx/proc_rds.hpp b/firmware/baseband-tx/proc_rds.hpp new file mode 100644 index 00000000..17faadaa --- /dev/null +++ b/firmware/baseband-tx/proc_rds.hpp @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * + * 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 __PROC_RDS_H__ +#define __PROC_RDS_H__ + +#include "baseband_processor.hpp" + +#define SAMPLES_PER_BIT 192 +#define FILTER_SIZE 576 +#define SAMPLE_BUFFER_SIZE SAMPLES_PER_BIT + FILTER_SIZE + +class RDSProcessor : public BasebandProcessor { +public: + void execute(const buffer_c8_t& buffer) override; + +private: + int8_t re, im; + uint8_t mphase, s; + uint32_t bit_pos; + int32_t sample_buffer[SAMPLE_BUFFER_SIZE] = {0}; + int32_t val; + uint8_t prev_output = 0; + uint8_t cur_output = 0; + uint8_t cur_bit = 0; + int sample_count = SAMPLES_PER_BIT; + int in_sample_index = 0; + int32_t sample; + int out_sample_index = SAMPLE_BUFFER_SIZE-1; + uint32_t phase, sphase; + int32_t sig, frq, frq_im, rdsc; + int32_t k; + + int32_t waveform_biphase[576] = { + 165,167,168,168,167,166,163,160, + 157,152,147,141,134,126,118,109, + 99,88,77,66,53,41,27,14, + 0,-14,-29,-44,-59,-74,-89,-105, + -120,-135,-150,-165,-179,-193,-206,-218, + -231,-242,-252,-262,-271,-279,-286,-291, + -296,-299,-301,-302,-302,-300,-297,-292, + -286,-278,-269,-259,-247,-233,-219,-202, + -185,-166,-145,-124,-101,-77,-52,-26, + 0,27,56,85,114,144,175,205, + 236,266,296,326,356,384,412,439, + 465,490,513,535,555,574,590,604, + 616,626,633,637,639,638,633,626, + 616,602,586,565,542,515,485,451, + 414,373,329,282,232,178,121,62, + 0,-65,-132,-202,-274,-347,-423,-500, + -578,-656,-736,-815,-894,-973,-1051,-1128, + -1203,-1276,-1347,-1415,-1479,-1540,-1596,-1648, + -1695,-1736,-1771,-1799,-1820,-1833,-1838,-1835, + -1822,-1800,-1767,-1724,-1670,-1605,-1527,-1437, + -1334,-1217,-1087,-943,-785,-611,-423,-219, + 0,235,487,755,1040,1341,1659,1994, + 2346,2715,3101,3504,3923,4359,4811,5280, + 5764,6264,6780,7310,7856,8415,8987,9573, + 10172,10782,11404,12036,12678,13329,13989,14656, + 15330,16009,16694,17382,18074,18767,19461,20155, + 20848,21539,22226,22909,23586,24256,24918,25571, + 26214,26845,27464,28068,28658,29231,29787,30325, + 30842,31339,31814,32266,32694,33097,33473,33823, + 34144,34437,34699,34931,35131,35299,35434,35535, + 35602,35634,35630,35591,35515,35402,35252,35065, + 34841,34579,34279,33941,33566,33153,32702,32214, + 31689,31128,30530,29897,29228,28525,27788,27017, + 26214,25379,24513,23617,22693,21740,20761,19755, + 18725,17672,16597,15501,14385,13251,12101,10935, + 9755,8563,7360,6148,4927,3701,2470,1235, + 0,-1235,-2470,-3701,-4927,-6148,-7360,-8563, + -9755,-10935,-12101,-13251,-14385,-15501,-16597,-17672, + -18725,-19755,-20761,-21740,-22693,-23617,-24513,-25379, + -26214,-27017,-27788,-28525,-29228,-29897,-30530,-31128, + -31689,-32214,-32702,-33153,-33566,-33941,-34279,-34579, + -34841,-35065,-35252,-35402,-35515,-35591,-35630,-35634, + -35602,-35535,-35434,-35299,-35131,-34931,-34699,-34437, + -34144,-33823,-33473,-33097,-32694,-32266,-31814,-31339, + -30842,-30325,-29787,-29231,-28658,-28068,-27464,-26845, + -26214,-25571,-24918,-24256,-23586,-22909,-22226,-21539, + -20848,-20155,-19461,-18767,-18074,-17382,-16694,-16009, + -15330,-14656,-13989,-13329,-12678,-12036,-11404,-10782, + -10172,-9573,-8987,-8415,-7856,-7310,-6780,-6264, + -5764,-5280,-4811,-4359,-3923,-3504,-3101,-2715, + -2346,-1994,-1659,-1341,-1040,-755,-487,-235, + 0,219,423,611,785,943,1087,1217, + 1334,1437,1527,1605,1670,1724,1767,1800, + 1822,1835,1838,1833,1820,1799,1771,1736, + 1695,1648,1596,1540,1479,1415,1347,1276, + 1203,1128,1051,973,894,815,736,656, + 578,500,423,347,274,202,132,65, + 0,-62,-121,-178,-232,-282,-329,-373, + -414,-451,-485,-515,-542,-565,-586,-602, + -616,-626,-633,-638,-639,-637,-633,-626, + -616,-604,-590,-574,-555,-535,-513,-490, + -465,-439,-412,-384,-356,-326,-296,-266, + -236,-205,-175,-144,-114,-85,-56,-27, + 0,26,52,77,101,124,145,166, + 185,202,219,233,247,259,269,278, + 286,292,297,300,302,302,301,299, + 296,291,286,279,271,262,252,242, + 231,218,206,193,179,165,150,135, + 120,105,89,74,59,44,29,14, + 0,-14,-27,-41,-53,-66,-77,-88, + -99,-109,-118,-126,-134,-141,-147,-152, + -157,-160,-163,-166,-167,-168,-168,-167 + }; + +}; + +#endif diff --git a/firmware/baseband-tx/proc_xylos.cpp b/firmware/baseband-tx/proc_xylos.cpp index bd377175..b3da393d 100644 --- a/firmware/baseband-tx/proc_xylos.cpp +++ b/firmware/baseband-tx/proc_xylos.cpp @@ -21,6 +21,10 @@ */ #include "proc_xylos.hpp" + +#include "dsp_iir_config.hpp" +//#include "audio_output.hpp" + #include "portapack_shared_memory.hpp" #include "sine_table.hpp" @@ -31,7 +35,7 @@ // 14 13 12 11: // 2108 989 2259 931 -void XylosProcessor::execute(buffer_c8_t buffer) { +void XylosProcessor::execute(const buffer_c8_t& buffer) { // This is called at 1536000/2048 = 750Hz @@ -88,5 +92,5 @@ void XylosProcessor::execute(buffer_c8_t buffer) { buffer.p[i] = {(int8_t)re,(int8_t)im}; } - fill_audio_buffer(preview_audio_buffer); + //audio_output.write(preview_audio_buffer); } diff --git a/firmware/baseband-tx/proc_xylos.hpp b/firmware/baseband-tx/proc_xylos.hpp index 09e322a9..0add6712 100644 --- a/firmware/baseband-tx/proc_xylos.hpp +++ b/firmware/baseband-tx/proc_xylos.hpp @@ -25,12 +25,18 @@ #include "baseband_processor.hpp" +#include "dsp_decimate.hpp" +#include "dsp_demodulate.hpp" + +//#include "audio_output.hpp" +#include "baseband_processor.hpp" + #define CCIR_TONELENGTH 15360-1 // 1536000/10/10 #define PHASEV 436.91 // (65536*1024)/1536000*10 class XylosProcessor : public BasebandProcessor { public: - void execute(buffer_c8_t buffer) override; + void execute(const buffer_c8_t& buffer) override; private: int16_t audio_data[64]; @@ -67,6 +73,8 @@ private: uint32_t aphase, phase, sphase; int32_t sample, frq; TXDoneMessage message; + + //AudioOutput audio_output; }; #endif diff --git a/firmware/baseband-tx/rssi_dma.cpp b/firmware/baseband-tx/rssi_dma.cpp index 5b060604..91f3b496 100644 --- a/firmware/baseband-tx/rssi_dma.cpp +++ b/firmware/baseband-tx/rssi_dma.cpp @@ -157,7 +157,7 @@ bool is_enabled() { } void disable() { - gpdma_channel.disable_force(); + gpdma_channel.disable(); } rf::rssi::buffer_t wait_for_buffer() { diff --git a/firmware/baseband-tx/rssi_thread.cpp b/firmware/baseband-tx/rssi_thread.cpp new file mode 100644 index 00000000..0bf15334 --- /dev/null +++ b/firmware/baseband-tx/rssi_thread.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2014 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 "rssi_thread.hpp" + +#include "rssi.hpp" +#include "rssi_dma.hpp" +#include "rssi_stats_collector.hpp" + +#include "message.hpp" +#include "portapack_shared_memory.hpp" + +WORKING_AREA(rssi_thread_wa, 128); + +Thread* RSSIThread::start(const tprio_t priority) { + return chThdCreateStatic(rssi_thread_wa, sizeof(rssi_thread_wa), + priority, ThreadBase::fn, + this + ); +} + +void RSSIThread::run() { + rf::rssi::init(); + rf::rssi::dma::allocate(4, 400); + + RSSIStatisticsCollector stats; + + while(true) { + // TODO: Place correct sampling rate into buffer returned here: + const auto buffer_tmp = rf::rssi::dma::wait_for_buffer(); + const rf::rssi::buffer_t buffer { + buffer_tmp.p, buffer_tmp.count, sampling_rate + }; + + stats.process( + buffer, + [](const RSSIStatistics& statistics) { + const RSSIStatisticsMessage message { statistics }; + shared_memory.application_queue.push(message); + } + ); + } + + rf::rssi::dma::free(); +} diff --git a/firmware/baseband-tx/rssi_thread.hpp b/firmware/baseband-tx/rssi_thread.hpp new file mode 100644 index 00000000..0a00ef8e --- /dev/null +++ b/firmware/baseband-tx/rssi_thread.hpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2014 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 __RSSI_THREAD_H__ +#define __RSSI_THREAD_H__ + +#include "thread_base.hpp" + +#include + +#include + +class RSSIThread : public ThreadBase { +public: + RSSIThread( + ) : ThreadBase { "rssi" } + { + } + + Thread* start(const tprio_t priority); + +private: + void run() override; + + const uint32_t sampling_rate { 400000 }; +}; + +#endif/*__RSSI_THREAD_H__*/ diff --git a/firmware/baseband-tx/spectrum_collector.cpp b/firmware/baseband-tx/spectrum_collector.cpp new file mode 100644 index 00000000..2b661c9a --- /dev/null +++ b/firmware/baseband-tx/spectrum_collector.cpp @@ -0,0 +1,130 @@ +/* + * 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 "spectrum_collector.hpp" + +#include "dsp_fft.hpp" + +#include "utility.hpp" +#include "event_m4.hpp" +#include "portapack_shared_memory.hpp" + +#include "event_m4.hpp" + +#include + +void SpectrumCollector::on_message(const Message* const message) { + switch(message->id) { + case Message::ID::UpdateSpectrum: + update(); + break; + + case Message::ID::SpectrumStreamingConfig: + set_state(*reinterpret_cast(message)); + break; + + default: + break; + } +} + +void SpectrumCollector::set_state(const SpectrumStreamingConfigMessage& message) { + if( message.mode == SpectrumStreamingConfigMessage::Mode::Running ) { + start(); + } else { + stop(); + } +} + +void SpectrumCollector::start() { + streaming = true; + ChannelSpectrumConfigMessage message { &fifo }; + shared_memory.application_queue.push(message); +} + +void SpectrumCollector::stop() { + streaming = false; + fifo.reset_in(); +} + +void SpectrumCollector::set_decimation_factor( + const size_t decimation_factor +) { + channel_spectrum_decimator.set_factor(decimation_factor); +} + +/* TODO: Refactor to register task with idle thread? + * It's sad that the idle thread has to call all the way back here just to + * perform the deferred task on the buffer of data we prepared. + */ + +void SpectrumCollector::feed( + const buffer_c16_t& channel, + const uint32_t filter_pass_frequency, + const uint32_t filter_stop_frequency +) { + // Called from baseband processing thread. + channel_filter_pass_frequency = filter_pass_frequency; + channel_filter_stop_frequency = filter_stop_frequency; + + channel_spectrum_decimator.feed( + channel, + [this](const buffer_c16_t& data) { + this->post_message(data); + } + ); +} + +void SpectrumCollector::post_message(const buffer_c16_t& data) { + // Called from baseband processing thread. + if( streaming && !channel_spectrum_request_update ) { + fft_swap(data, channel_spectrum); + channel_spectrum_sampling_rate = data.sampling_rate; + channel_spectrum_request_update = true; + EventDispatcher::events_flag(EVT_MASK_SPECTRUM); + } +} + +void SpectrumCollector::update() { + // Called from idle thread (after EVT_MASK_SPECTRUM is flagged) + if( streaming && channel_spectrum_request_update ) { + /* Decimated buffer is full. Compute spectrum. */ + fft_c_preswapped(channel_spectrum); + + ChannelSpectrum spectrum; + spectrum.sampling_rate = channel_spectrum_sampling_rate; + spectrum.channel_filter_pass_frequency = channel_filter_pass_frequency; + spectrum.channel_filter_stop_frequency = channel_filter_stop_frequency; + for(size_t i=0; i +#include + +#include "message.hpp" + +class SpectrumCollector { +public: + constexpr SpectrumCollector( + ) : channel_spectrum_decimator { 1 } + { + } + + void on_message(const Message* const message); + + void set_decimation_factor(const size_t decimation_factor); + + void feed( + const buffer_c16_t& channel, + const uint32_t filter_pass_frequency, + const uint32_t filter_stop_frequency + ); + +private: + BlockDecimator<256> channel_spectrum_decimator; + ChannelSpectrumFIFO fifo; + + volatile bool channel_spectrum_request_update { false }; + bool streaming { false }; + std::array, 256> channel_spectrum; + uint32_t channel_spectrum_sampling_rate { 0 }; + uint32_t channel_filter_pass_frequency { 0 }; + uint32_t channel_filter_stop_frequency { 0 }; + + void post_message(const buffer_c16_t& data); + + void set_state(const SpectrumStreamingConfigMessage& message); + void start(); + void stop(); + + void update(); +}; + +#endif/*__SPECTRUM_COLLECTOR_H__*/ diff --git a/firmware/baseband-tx/thread_base.hpp b/firmware/baseband-tx/thread_base.hpp new file mode 100644 index 00000000..9b5e8a99 --- /dev/null +++ b/firmware/baseband-tx/thread_base.hpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2014 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 __THREAD_BASE_H__ +#define __THREAD_BASE_H__ + +#include + +class ThreadBase { +public: + constexpr ThreadBase( + const char* const name + ) : name { name } + { + } + +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; +}; + +#endif/*__THREAD_BASE_H__*/ diff --git a/firmware/baseband-tx/touch_dma.cpp b/firmware/baseband-tx/touch_dma.cpp index aa120ff3..ce66617a 100644 --- a/firmware/baseband-tx/touch_dma.cpp +++ b/firmware/baseband-tx/touch_dma.cpp @@ -122,7 +122,7 @@ bool is_enabled() { } void disable() { - gpdma_channel.disable_force(); + gpdma_channel.disable(); } } /* namespace dma */ diff --git a/firmware/baseband.bin b/firmware/baseband.bin index 6ff2307ae6476112c302e381e111b11aad7de34e..41e3840f412738c8423c9b2aff4e90e632b675b9 100644 GIT binary patch literal 33296 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 Ux!Nq_^1@cNjb$_4rv7~c0Lx1|Y5)KL literal 33024 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 LZ?8=xvTytZzruvg diff --git a/firmware/baseband/description b/firmware/baseband/description index 517d61c1..884ea0b0 100644 --- a/firmware/baseband/description +++ b/firmware/baseband/description @@ -1 +1 @@ -Basic RX/TX stuff for testing :) +Original firmware's functionalities: Receiver. diff --git a/firmware/baseband/main.cpp b/firmware/baseband/main.cpp index eea7e572..d6a895a7 100755 --- a/firmware/baseband/main.cpp +++ b/firmware/baseband/main.cpp @@ -50,121 +50,6 @@ #include #include #include -#include -#include - -class ThreadBase { -public: - constexpr ThreadBase( - const char* const name - ) : name { name } - { - } - - static msg_t fn(void* arg) { - auto obj = static_cast(arg); - chRegSetThreadName(obj->name); - obj->run(); - - return 0; - } - - virtual void run() = 0; - -private: - const char* const name; -}; - -class BasebandThread : public ThreadBase { -public: - BasebandThread( - ) : ThreadBase { "baseband" } - { - } - - Thread* start(const tprio_t priority) { - return chThdCreateStatic(wa, sizeof(wa), - priority, ThreadBase::fn, - this - ); - } - - Thread* thread_main { nullptr }; - Thread* thread_rssi { nullptr }; - BasebandProcessor* baseband_processor { nullptr }; - BasebandConfiguration baseband_configuration; - -private: - WORKING_AREA(wa, 2048); - - void run() override { - BasebandStatsCollector stats { - chSysGetIdleThread(), - thread_main, - thread_rssi, - chThdSelf() - }; - - while(true) { - // TODO: Place correct sampling rate into buffer returned here: - 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); - } - - stats.process(buffer, - [](const BasebandStatistics statistics) { - const BasebandStatisticsMessage message { statistics }; - shared_memory.application_queue.push(message); - } - ); - } - } -}; - -class RSSIThread : public ThreadBase { -public: - RSSIThread( - ) : ThreadBase { "rssi" } - { - } - - Thread* start(const tprio_t priority) { - return chThdCreateStatic(wa, sizeof(wa), - priority, ThreadBase::fn, - this - ); - } - - uint32_t sampling_rate { 400000 }; - -private: - WORKING_AREA(wa, 128); - - void run() override { - RSSIStatisticsCollector stats; - - while(true) { - // TODO: Place correct sampling rate into buffer returned here: - const auto buffer_tmp = rf::rssi::dma::wait_for_buffer(); - const rf::rssi::buffer_t buffer { - buffer_tmp.p, buffer_tmp.count, sampling_rate - }; - - stats.process( - buffer, - [](const RSSIStatistics statistics) { - const RSSIStatisticsMessage message { statistics }; - shared_memory.application_queue.push(message); - } - ); - } - } -}; extern "C" { @@ -187,9 +72,6 @@ void __late_init(void) { } -static BasebandThread baseband_thread; -static RSSIThread rssi_thread; - static void init() { i2s::i2s0::configure( audio::i2s0_config_tx, @@ -208,31 +90,9 @@ static void init() { gpdma::controller.enable(); nvicEnableVector(DMA_IRQn, CORTEX_PRIORITY_MASK(LPC_DMA_IRQ_PRIORITY)); - baseband::dma::init(); - - rf::rssi::init(); touch::dma::init(); - - const auto thread_main = chThdSelf(); - - const auto thread_rssi = rssi_thread.start(NORMALPRIO + 10); - - baseband_thread.thread_main = thread_main; - baseband_thread.thread_rssi = thread_rssi; - - baseband_thread.start(NORMALPRIO + 20); -} - -static void shutdown() { - // TODO: Is this complete? - - nvicDisableVector(DMA_IRQn); - - m0apptxevent_interrupt_disable(); - - chSysDisable(); - - systick_stop(); + touch::dma::allocate(); + touch::dma::enable(); } static void halt() { @@ -242,146 +102,27 @@ static void halt() { } } -class EventDispatcher { -public: - MessageHandlerMap& message_handlers() { - return message_map; - } +static void shutdown() { + // TODO: Is this complete? + + nvicDisableVector(DMA_IRQn); + + chSysDisable(); - void run() { - while(is_running) { - const auto events = wait(); - dispatch(events); - } - } + systick_stop(); - void request_stop() { - is_running = false; - } + ShutdownMessage shutdown_message; + shared_memory.application_queue.push(shutdown_message); -private: - MessageHandlerMap message_map; - - bool is_running = true; - - eventmask_t wait() { - return chEvtWaitAny(ALL_EVENTS); - } - - void dispatch(const eventmask_t events) { - if( events & EVT_MASK_BASEBAND ) { - handle_baseband_queue(); - } - - if( events & EVT_MASK_SPECTRUM ) { - handle_spectrum(); - } - } - - void handle_baseband_queue() { - std::array message_buffer; - while(Message* const message = shared_memory.baseband_queue.pop(message_buffer)) { - message_map.send(message); - } - } - - void handle_spectrum() { - if( baseband_thread.baseband_processor ) { - baseband_thread.baseband_processor->update_spectrum(); - } - } -}; - -static constexpr auto direction = baseband::Direction::Receive; + halt(); +} 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::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(); - rf::rssi::stop(); - } - - // 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 0: - baseband_thread.baseband_processor = new NarrowbandAMAudio(); - break; - - case 1: - baseband_thread.baseband_processor = new NarrowbandFMAudio(); - break; - - case 2: - baseband_thread.baseband_processor = new WidebandFMAudio(); - break; - - case 3: - baseband_thread.baseband_processor = new AISProcessor(); - break; - - case 4: - baseband_thread.baseband_processor = new WidebandSpectrum(); - break; - - case 5: - baseband_thread.baseband_processor = new TPMSProcessor(); - break; - - default: - break; - } - - if( baseband_thread.baseband_processor ) { - if( direction == baseband::Direction::Receive ) { - rf::rssi::start(); - } - baseband::dma::enable(direction); - } - } - - baseband_thread.baseband_configuration = message->configuration; - } - ); - - message_handlers.register_handler(Message::ID::Shutdown, - [&event_dispatcher](const Message* const) { - event_dispatcher.request_stop(); - } - ); - /* TODO: Ensure DMAs are configured to point at first LLI in chain. */ - if( direction == baseband::Direction::Receive ) { - rf::rssi::dma::allocate(4, 400); - } - - touch::dma::allocate(); - touch::dma::enable(); - - const auto baseband_buffer = - new std::array(); - baseband::dma::configure( - baseband_buffer->data(), - direction - ); - + EventDispatcher event_dispatcher; event_dispatcher.run(); shutdown(); diff --git a/firmware/baseband/name b/firmware/baseband/name index 8d194a1d..dae227ea 100644 --- a/firmware/baseband/name +++ b/firmware/baseband/name @@ -1 +1 @@ -First module +Receiver diff --git a/firmware/baseband/proc_afskrx.cpp b/firmware/baseband/proc_afskrx.cpp index a7514954..ad3e3bbb 100644 --- a/firmware/baseband/proc_afskrx.cpp +++ b/firmware/baseband/proc_afskrx.cpp @@ -25,30 +25,17 @@ using namespace lpc43xx; -void AFSKRXProcessor::execute(buffer_c8_t buffer) { +void AFSKRXProcessor::execute(const buffer_c8_t& buffer) { + if( !configured ) { + return; + } /* Called every 2048/3072000 second -- 1500Hz. */ - - auto decimator_out = decimator.execute(buffer); - - const buffer_c16_t work_baseband_buffer { - (complex16_t*)decimator_out.p, - sizeof(*decimator_out.p) * decimator_out.count - }; - - /* 96kHz complex[64] - * -> FIR filter, <6kHz (0.063fs) pass, gain 1.0 - * -> 48kHz int16_t[32] */ - auto channel = channel_filter.execute(decimator_out, work_baseband_buffer); - - const buffer_s16_t work_audio_buffer { - (int16_t*)decimator_out.p, - sizeof(*decimator_out.p) * decimator_out.count - }; - /* 48kHz complex[32] - * -> FM demodulation - * -> 48kHz int16_t[32] */ - auto audio = demod.execute(channel, work_audio_buffer); + const auto decim_0_out = decim_0.execute(buffer, dst_buffer); + const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); + const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer); + + auto audio = demod.execute(channel_out, work_audio_buffer); /*static uint64_t audio_present_history = 0; const auto audio_present_now = squelch.execute(audio); @@ -65,7 +52,7 @@ void AFSKRXProcessor::execute(buffer_c8_t buffer) { }*/ //} - audio_hpf.execute_in_place(audio); + //audio_hpf.execute_in_place(audio); for(size_t i=0; i 10) { @@ -97,9 +84,9 @@ void AFSKRXProcessor::execute(buffer_c8_t buffer) { if (sc >= 600) { sc = 0; - AFSKDataMessage message; - memcpy(message.data,aud,128*2); - shared_memory.application_queue.push(message); + //AFSKDataMessage message; + //memcpy(message.data,aud,128*2); + //shared_memory.application_queue.push(message); audc = 0; } else { sc++; @@ -110,7 +97,7 @@ void AFSKRXProcessor::execute(buffer_c8_t buffer) { audc++; } - fill_audio_buffer(audio); + audio_output.write(audio); } void AFSKRXProcessor::data_handler( diff --git a/firmware/baseband/proc_afskrx.hpp b/firmware/baseband/proc_afskrx.hpp index 275d174c..8a06743c 100644 --- a/firmware/baseband/proc_afskrx.hpp +++ b/firmware/baseband/proc_afskrx.hpp @@ -24,38 +24,35 @@ #include "baseband_processor.hpp" -#include "channel_decimator.hpp" #include "dsp_decimate.hpp" #include "dsp_demodulate.hpp" -#include "dsp_fir_taps.hpp" -#include "dsp_iir.hpp" -#include "dsp_iir_config.hpp" -#include "dsp_squelch.hpp" +#include "audio_output.hpp" #include "message.hpp" -#include -#include -#include - class AFSKRXProcessor : public BasebandProcessor { public: - AFSKRXProcessor() { - decimator.set_decimation_factor(ChannelDecimator::DecimationFactor::By32); - channel_filter.configure(channel_filter_taps.taps, 2); - } - - void execute(buffer_c8_t buffer) override; + void execute(const buffer_c8_t& buffer) override; private: - ChannelDecimator decimator; - const fir_taps_real<64>& channel_filter_taps = taps_64_lp_042_078_tfilter; - dsp::decimate::FIRAndDecimateComplex channel_filter; - dsp::demodulate::FM demod { 48000, 5000 }; - - IIRBiquadFilter audio_hpf { audio_hpf_config }; - //FMSquelch squelch; + std::array dst; + const buffer_c16_t dst_buffer { + dst.data(), + dst.size() + }; + const buffer_f32_t work_audio_buffer { + (float*)dst.data(), + sizeof(dst) / sizeof(float) + }; + dsp::decimate::FIRAndDecimateComplex channel_filter; + dsp::demodulate::FM demod; // 48000 5000 + + dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0; + dsp::decimate::FIRC16xR16x32Decim8 decim_1; + + AudioOutput audio_output; + uint16_t bit_timer = 0, freq_timer = 0; uint16_t sc; uint8_t audc, spur, sign, prev_sign, bit = 0; @@ -63,6 +60,9 @@ private: int16_t aud[128]; void data_handler(const double data); + + bool configured { false }; + void configure(const NBFMConfigureMessage& message); }; #endif/*__PROC_TPMS_H__*/ diff --git a/firmware/baseband/proc_sigfrx.cpp b/firmware/baseband/proc_sigfrx.cpp index c6ca3a8e..1dbfbed7 100644 --- a/firmware/baseband/proc_sigfrx.cpp +++ b/firmware/baseband/proc_sigfrx.cpp @@ -24,46 +24,6 @@ #include #include -void SIGFRXProcessor::execute(buffer_c8_t buffer) { +void SIGFRXProcessor::execute(const buffer_c8_t& buffer) { /* Called every 2048/3072000 second -- 1500Hz. */ - - auto decimator_out = decimator.execute(buffer); - - const buffer_c16_t work_baseband_buffer { - (complex16_t*)decimator_out.p, - sizeof(*decimator_out.p) * decimator_out.count - }; - - /* 192kHz complex[64] - * -> 96kHz int16_t[32] */ - //auto channel = channel_filter.execute(decimator_out, work_baseband_buffer); - - // TODO: Feed channel_stats post-decimation data? - feed_channel_spectrum( - decimator_out, - 41000, //decimator_out.sampling_rate * channel_filter_taps.pass_frequency_normalized, - 70000 //decimator_out.sampling_rate * channel_filter_taps.stop_frequency_normalized - ); - - /*const buffer_s16_t work_audio_buffer { - (int16_t*)decimator_out.p, - sizeof(*decimator_out.p) * decimator_out.count - }; - * - auto audio = demod.execute(channel, work_audio_buffer); - - static uint64_t audio_present_history = 0; - const auto audio_present_now = squelch.execute(audio); - audio_present_history = (audio_present_history << 1) | (audio_present_now ? 1 : 0); - const bool audio_present = (audio_present_history != 0); - - if( !audio_present ) { - // Zero audio buffer. - for(size_t i=0; i& channel_filter_taps = taps_64_lp_410_700_tfilter; //taps_64_lp_104_140_tfilter - //dsp::decimate::FIRAndDecimateComplex channel_filter; - dsp::demodulate::FM demod { 48000, 7500 }; - IIRBiquadFilter audio_hpf { audio_hpf_config }; - FMSquelch squelch; }; #endif diff --git a/firmware/bootstrap/bootstrap.bin b/firmware/bootstrap/bootstrap.bin new file mode 100755 index 0000000000000000000000000000000000000000..c234f8ae31e4a5df3bef9695206fd90c78bc6fef GIT binary patch literal 228 zcmZQ*U=+w@U|^Vv1p|dp089-?5XJ`499tPasQ+N~X7xJ!gGGh;YlAU^5|E{+u>bFK z1_Krc1_l<90(S-mHcxghcJEnE`}PO^VBvNG;vdidfB*FV|Mz?USAYy z_A_o_@@R2m+>_uAWCO*xy*~rZ>`^@YV*(IQ0pb}zJO_vuC@%iWtjyvimB9RUmNK)~ WtrVbL`xz2A9RB}jOkm+)-~<4p+eK>t literal 0 HcmV?d00001 diff --git a/firmware/chibios-portapack/ext/fatfs/src/ff.c b/firmware/chibios-portapack/ext/fatfs/src/ff.c index 9c887c45..385063f5 100644 --- a/firmware/chibios-portapack/ext/fatfs/src/ff.c +++ b/firmware/chibios-portapack/ext/fatfs/src/ff.c @@ -2579,7 +2579,6 @@ FRESULT f_read ( UINT rcnt, cc; BYTE csect, *rbuff = (BYTE*)buff; - *br = 0; /* Clear read byte counter */ res = validate(fp); /* Check validity */ diff --git a/firmware/chibios-portapack/os/hal/platforms/LPC43xx/sdc_lld.c b/firmware/chibios-portapack/os/hal/platforms/LPC43xx/sdc_lld.c index 835abd80..e02accbe 100644 --- a/firmware/chibios-portapack/os/hal/platforms/LPC43xx/sdc_lld.c +++ b/firmware/chibios-portapack/os/hal/platforms/LPC43xx/sdc_lld.c @@ -619,7 +619,7 @@ void sdc_lld_start_clk(SDCDriver *sdcp) { (void)sdcp; sdio_cclk_set_400khz(); /* TODO: Reset card using CMD0 + init flag? */ - sdio_send_command(sdcp, 0 | (1U << 15), 0); + if (sdio_send_command(sdcp, 0 | (1U << 15), 0) != CH_SUCCESS) for(;;); } /** diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index 50afc442..95657f99 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -419,12 +419,14 @@ public: int64_t freq = 0; }; -class DisplayFrameSyncMessage : public Message { +class AFSKDataMessage : public Message { public: - constexpr DisplayFrameSyncMessage( - ) : Message { ID::DisplayFrameSync } + constexpr AFSKDataMessage( + ) : Message { ID::AFSKData } { } + + int16_t data[128] = {0}; }; class FIFOSignalMessage : public Message { diff --git a/firmware/common/modules.h b/firmware/common/modules.h index 5fb77add..a6c9c36b 100644 --- a/firmware/common/modules.h +++ b/firmware/common/modules.h @@ -1,2 +1,2 @@ -const char md5_baseband[16] = {0x0f,0xf7,0xa8,0x6f,0x0f,0xb3,0x88,0x4c,0xec,0x45,0xcf,0x8d,0xd5,0xf8,0x11,0x92,}; -const char md5_baseband_tx[16] = {0x77,0xa8,0x27,0xec,0xb4,0xcb,0xe6,0x17,0x06,0x70,0x49,0x01,0xed,0x48,0x1f,0x54,}; +const char md5_baseband[16] = {0xf8,0xf3,0x7b,0x36,0x68,0xd3,0xa1,0x85,0x26,0xb1,0x76,0x99,0x46,0x95,0xfd,0xec,}; +const char md5_baseband_tx[16] = {0xfc,0xe9,0x57,0x6b,0xfb,0xac,0x72,0x61,0x65,0x4e,0x3d,0x7f,0xb4,0x78,0xbd,0xd2,}; diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index 26fee967..78fcde43 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -1,24 +1,3 @@ -/* - * Copyright (C) 2014 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_widget.hpp" #include "ui_painter.hpp" @@ -305,77 +284,91 @@ void Text::set(const std::string value) { } void Text::paint(Painter& painter) { - if (style_ == nullptr) style_ = &style(); const auto rect = screen_rect(); + const auto s = style(); painter.fill_rectangle(rect, s.background); painter.draw_string( rect.pos, - (*style_), + s, text ); } -/* Button ****************************************************************/ +/* Checkbox **************************************************************/ - Button::Button( - Rect parent_rect, - std::string text -) : Widget { parent_rect }, - text_ { text } -{ - flags.focusable = true; -} - -void Button::set_text(const std::string value) { +void Checkbox::set_text(const std::string value) { text_ = value; set_dirty(); } -std::string Button::text() const { +std::string Checkbox::text() const { return text_; } -void Button::paint(Painter& painter) { +void Checkbox::set_value(const bool value) { + value_ = value; + set_dirty(); +} + +bool Checkbox::value() const { + return value_; +} + +void Checkbox::paint(Painter& painter) { const auto r = screen_rect(); - if (style_ == nullptr) style_ = &style(); - - const auto paint_style = (has_focus() || flags.highlighted) ? style_->invert() : *(style_); - - painter.draw_rectangle(r, style().foreground); + const auto paint_style = (has_focus() || flags.highlighted) ? style().invert() : style(); + + painter.draw_rectangle({ r.pos.x, r.pos.y, 24, 24 }, style().foreground); painter.fill_rectangle( - { r.pos.x + 1, r.pos.y + 1, r.size.w - 2, r.size.h - 2 }, - paint_style.background + { + static_cast(r.pos.x + 1), static_cast(r.pos.y + 1), + static_cast(24 - 2), static_cast(24 - 2) + }, + style().background ); - + + painter.draw_rectangle({ r.pos.x+2, r.pos.y+2, 24-4, 24-4 }, paint_style.background); + + if (value_ == true) { + // Check + portapack::display.draw_line( {r.pos.x+2, r.pos.y+14}, {r.pos.x+6, r.pos.y+18}, ui::Color::green()); + portapack::display.draw_line( {r.pos.x+6, r.pos.y+18}, {r.pos.x+20, r.pos.y+4}, ui::Color::green()); + } else { + // Cross + portapack::display.draw_line( {r.pos.x+1, r.pos.y+1}, {r.pos.x+24-2, r.pos.y+24-2}, ui::Color::red()); + portapack::display.draw_line( {r.pos.x+24-2, r.pos.y+1}, {r.pos.x+1, r.pos.y+24-2}, ui::Color::red()); + } + const auto label_r = paint_style.font.size_of(text_); painter.draw_string( - { r.pos.x + (r.size.w - label_r.w) / 2, r.pos.y + (r.size.h - label_r.h) / 2 }, + { + static_cast(r.pos.x + 24 + 4), + static_cast(r.pos.y + (24 - label_r.h) / 2) + }, paint_style, text_ ); } -bool Button::on_key(const KeyEvent key) { +bool Checkbox::on_key(const KeyEvent key) { if( key == KeyEvent::Select ) { + value_ = not value_; + set_dirty(); + if( on_select ) { on_select(*this); return true; } - } else { - if( on_dir ) { - on_dir(*this, key); - return false; - } } return false; } -bool Button::on_touch(const TouchEvent event) { +bool Checkbox::on_touch(const TouchEvent event) { switch(event.type) { case TouchEvent::Type::Start: flags.highlighted = true; @@ -385,6 +378,7 @@ bool Button::on_touch(const TouchEvent event) { case TouchEvent::Type::End: flags.highlighted = false; + value_ = not value_; set_dirty(); if( on_select ) { on_select(*this); @@ -427,6 +421,199 @@ bool Button::on_touch(const TouchEvent event) { #endif } +/* Button ****************************************************************/ + + Button::Button( + Rect parent_rect, + std::string text +) : Widget { parent_rect }, + text_ { text } +{ + flags.focusable = true; +} + +void Button::set_text(const std::string value) { + text_ = value; + set_dirty(); +} + +std::string Button::text() const { + return text_; +} + +void Button::paint(Painter& painter) { + const auto r = screen_rect(); + + const auto paint_style = (has_focus() || flags.highlighted) ? style().invert() : style(); + + painter.draw_rectangle(r, style().foreground); + + painter.fill_rectangle( + { r.pos.x + 1, r.pos.y + 1, r.size.w - 2, r.size.h - 2 }, + paint_style.background + ); + + const auto label_r = paint_style.font.size_of(text_); + painter.draw_string( + { r.pos.x + (r.size.w - label_r.w) / 2, r.pos.y + (r.size.h - label_r.h) / 2 }, + paint_style, + text_ + ); +} + +bool Button::on_key(const KeyEvent key) { + if( key == KeyEvent::Select ) { + if( on_select ) { + on_select(*this); + return true; + } + } + + return false; +} + +bool Button::on_touch(const TouchEvent event) { + switch(event.type) { + case TouchEvent::Type::Start: + flags.highlighted = true; + set_dirty(); + return true; + + + case TouchEvent::Type::End: + flags.highlighted = false; + set_dirty(); + if( on_select ) { + on_select(*this); + } + return true; + + default: + return false; + } +#if 0 + switch(event.type) { + case TouchEvent::Type::Start: + flags.highlighted = true; + set_dirty(); + return true; + case TouchEvent::Type::Move: + { + const bool new_highlighted = screen_rect().contains(event.point); + if( flags.highlighted != new_highlighted ) { + flags.highlighted = new_highlighted; + set_dirty(); + } + } + return true; + case TouchEvent::Type::End: + if( flags.highlighted ) { + flags.highlighted = false; + set_dirty(); + if( on_select ) { + on_select(*this); + } + } + return true; + default: + return false; + } +#endif +} + +/* Image *****************************************************************/ + +Image::Image( +) : Image { { }, nullptr, Color::white(), Color::black() } +{ +} + +Image::Image( + const Rect parent_rect, + const Bitmap* bitmap, + const Color foreground, + const Color background +) : Widget { parent_rect }, + bitmap_ { bitmap }, + foreground_ { foreground }, + background_ { background } +{ +} + +void Image::set_bitmap(const Bitmap* bitmap) { + bitmap_ = bitmap; + set_dirty(); +} + +void Image::set_foreground(const Color color) { + foreground_ = color; + set_dirty(); +} + +void Image::set_background(const Color color) { + background_ = color; + set_dirty(); +} + +void Image::paint(Painter& painter) { + if( bitmap_ ) { + // Code also handles ImageButton behavior. + const bool selected = (has_focus() || flags.highlighted); + painter.draw_bitmap( + screen_pos(), + *bitmap_, + selected ? background_ : foreground_, + selected ? foreground_ : background_ + ); + } +} + +/* ImageButton ***********************************************************/ + +// TODO: Virtually all this code is duplicated from Button. Base class? + +ImageButton::ImageButton( + const Rect parent_rect, + const Bitmap* bitmap, + const Color foreground, + const Color background +) : Image { parent_rect, bitmap, foreground, background } +{ + flags.focusable = true; +} + +bool ImageButton::on_key(const KeyEvent key) { + if( key == KeyEvent::Select ) { + if( on_select ) { + on_select(*this); + return true; + } + } + + return false; +} + +bool ImageButton::on_touch(const TouchEvent event) { + switch(event.type) { + case TouchEvent::Type::Start: + flags.highlighted = true; + set_dirty(); + return true; + + + case TouchEvent::Type::End: + flags.highlighted = false; + set_dirty(); + if( on_select ) { + on_select(*this); + } + return true; + + default: + return false; + } +} + /* OptionsField **********************************************************/ OptionsField::OptionsField( @@ -480,6 +667,12 @@ void OptionsField::paint(Painter& painter) { } } +void OptionsField::on_focus() { + if( on_show_options ) { + on_show_options(); + } +} + bool OptionsField::on_encoder(const EncoderEvent delta) { set_selected_index(selected_index() + delta); return true; diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index b6bfeb0c..80d3de46 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -188,48 +188,11 @@ public: Text(Rect parent_rect); void set(const std::string value); - void set_style(const Style* new_style); - + void paint(Painter& painter) override; private: std::string text; - const Style* style_ { nullptr }; -}; - -class Checkbox : public Widget { -public: - std::function on_select; - - Checkbox( - Point parent_point, - std::string text - ) : Widget { parent_point }, - text_ { text } - { - flags.focusable = true; - } - - Checkbox( - ) : Checkbox { { }, { } } - { - } - - void set_text(const std::string value); - void set_style(const Style* new_style); - std::string text() const; - void set_value(const bool value); - bool value() const; - - void paint(Painter& painter) override; - - bool on_key(const KeyEvent key) override; - bool on_touch(const TouchEvent event) override; - -private: - std::string text_; - bool value_ = false; - const Style* style_ { nullptr }; }; class Button : public Widget { @@ -244,8 +207,6 @@ public: } void set_text(const std::string value); - void set_text(const int value); - void set_style(const Style* new_style); std::string text() const; void paint(Painter& painter) override; @@ -255,7 +216,43 @@ public: private: std::string text_; - const Style* style_ { nullptr }; +}; + +class Image : public Widget { +public: + Image(); + Image( + const Rect parent_rect, + const Bitmap* bitmap, + const Color foreground, + const Color background + ); + + void set_bitmap(const Bitmap* bitmap); + void set_foreground(const Color color); + void set_background(const Color color); + + void paint(Painter& painter) override; + +private: + const Bitmap* bitmap_; + Color foreground_; + Color background_; +}; + +class ImageButton : public Image { +public: + std::function on_select; + + ImageButton( + const Rect parent_rect, + const Bitmap* bitmap, + const Color foreground, + const Color background + ); + + bool on_key(const KeyEvent key) override; + bool on_touch(const TouchEvent event) override; }; class OptionsField : public Widget { @@ -266,6 +263,7 @@ public: using options_t = std::vector; std::function on_change; + std::function on_show_options; OptionsField(Point parent_pos, size_t length, options_t options); @@ -276,6 +274,7 @@ public: void paint(Painter& painter) override; + void on_focus() override; bool on_encoder(const EncoderEvent delta) override; bool on_touch(const TouchEvent event) override; diff --git a/firmware/portapack-h1-firmware.bin b/firmware/portapack-h1-firmware.bin index a17ff1f542df9133ee5a15fe535d1007e31383e0..719b8ad60d6b6e63df19e7f0d481aeca585eb82e 100644 GIT binary patch delta 54363 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 hP6w>_swgh;K-*Vw3~0y2m@5Y{{zgwRM-Fj diff --git a/firmware/tools/make_baseband_file.py b/firmware/tools/make_baseband_file.py index 7f741569..5e30acd5 100755 --- a/firmware/tools/make_baseband_file.py +++ b/firmware/tools/make_baseband_file.py @@ -55,8 +55,9 @@ sys.argv = sys.argv[1:] # Format for module file: # Magic (4), Version (2), Length (4), Name (16), MD5 (16), Description (214) -# Module binary... -# MD5 (16) +# 0x00 pad bytes (256) +# Module binary (padded to 32768-16) +# MD5 (16) again, so that module code can read it (dirty...) for args in sys.argv: data = read_image(args + '/build/' + args + '.bin') @@ -66,7 +67,7 @@ for args in sys.argv: info = 'PPM ' # Version - info += struct.pack('H', 1) + info += struct.pack('H', 2) # Length info += struct.pack('I', len(data)) @@ -94,9 +95,12 @@ for args in sys.argv: description += (data_default_byte * pad_size) info += description - # Padding + # Header padding to fit in SD card sector + info += (data_default_byte * 256) + + # Binary padding data = info + data - pad_size = (32768 + 256 - 16) - len(data) + pad_size = (32768 + 512 - 16) - len(data) data += (data_default_byte * pad_size) data += digest write_file(data, args + '.bin') @@ -108,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[256:(32768+256)], args + '/build/' + args + '.bin') + write_file(data[512:(32768+512)], args + '/build/' + args + '.bin') write_file(h_data, 'common/modules.h')