diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index 24067603..899e60da 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -91,6 +91,14 @@ void set_tones_data(const uint32_t bw, const uint32_t pre_silence, const uint16_ send_message(&message); } +void set_sstv_data(const uint8_t vis_code, const uint32_t pixel_duration) { + const SSTVConfigureMessage message { + vis_code, + pixel_duration + }; + send_message(&message); +} + void set_afsk_data(const uint32_t afsk_samples_per_bit, const uint32_t afsk_phase_inc_mark, const uint32_t afsk_phase_inc_space, const uint8_t afsk_repeat, const uint32_t afsk_bw, const bool afsk_alt_format) { const AFSKConfigureMessage message { diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index 1f3089c2..ec31ec28 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -57,6 +57,7 @@ struct WFMConfig { void set_tones_data(const uint32_t bw, const uint32_t pre_silence, const uint16_t tone_count, const bool dual_tone, const bool audio_out); +void set_sstv_data(const uint8_t vis_code, const uint32_t pixel_duration); void set_audiotx_data(const uint32_t divider, const uint32_t bw, const uint32_t gain_x10, const bool ctcss_enabled, const uint32_t ctcss_phase_inc); void set_fifo_data(const int8_t * data); diff --git a/firmware/application/main.cpp b/firmware/application/main.cpp index 0e221012..931e479f 100755 --- a/firmware/application/main.cpp +++ b/firmware/application/main.cpp @@ -23,6 +23,7 @@ // Color bitmaps generated with: // Gimp image > indexed colors (16), then "xxd -i *.bmp" +//BUG: Length is wrong in soundboard for long files (> 1min ?) //BUG: RDS Radiotext is not recognized in Redsea (and car radio) //BUG: RDS doesn't stop baseband when stopping tx ? //BUG: Check AFSK transmit end, skips last bits ? diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index fb576bc0..11d0be14 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -361,7 +361,7 @@ SystemMenuView::SystemMenuView(NavigationView& nav) { { "Replay", ui::Color::grey(), &bitmap_icon_replay, [&nav](){ nav.push(); } }, { "Audio transmitters", ui::Color::green(), &bitmap_icon_audiotx, [&nav](){ nav.push(); } }, { "Code transmitters", ui::Color::green(), &bitmap_icon_codetx, [&nav](){ nav.push(); } }, - { "SSTV transmitter", ui::Color::grey(), &bitmap_icon_sstv, [&nav](){ nav.push(); } }, + { "SSTV transmitter", ui::Color::dark_orange(), &bitmap_icon_sstv, [&nav](){ nav.push(); } }, { "Close Call", ui::Color::cyan(), &bitmap_icon_closecall, [&nav](){ nav.push(); } }, { "Jammer", ui::Color::orange(),&bitmap_icon_jammer, [&nav](){ nav.push(); } }, { "Utilities", ui::Color::purple(),&bitmap_icon_utilities, [&nav](){ nav.push(); } }, diff --git a/firmware/application/ui_sstvtx.cpp b/firmware/application/ui_sstvtx.cpp index 84785f0e..9117e3c8 100644 --- a/firmware/application/ui_sstvtx.cpp +++ b/firmware/application/ui_sstvtx.cpp @@ -68,7 +68,6 @@ void SSTVTXView::paint(Painter&) { ui::Color line_buffer[160]; Coord line; uint32_t data_idx, bmp_px, pixel_idx; - uint8_t pixels_buffer[320 * 3]; // 320 pixels @ 24bpp data_idx = bmp_header.image_data; @@ -97,28 +96,22 @@ void SSTVTXView::on_tuning_frequency_changed(rf::Frequency f) { transmitter_model.set_tuning_frequency(f); } -void SSTVTXView::start_tx() { - // Baseband SSTV TX code should have a 2 scanlines buffer, and ask - // for fill-up when there's 1 or less remaining. This should leave - // enough time for the code here to generate the scanline data - // before tx. +void SSTVTXView::prepare_scanline() { + sstv_scanline scanline_buffer; + uint32_t component, pixel_idx; - const uint8_t VIS_code = 0b00011101; // Scottie 2 + if (scanline_counter >= (256 * 3)) { + progressbar.set_value(0); + transmitter_model.disable(); + options_bitmaps.set_focusable(true); + tx_view.set_transmitting(false); + return; + } - // Calibration: - // 1900 300ms - // 1200 10ms - // 1900 300ms - // VIS: (30ms * 10 = 300ms) - // 1200 - // 00011101 (0=1300, 1=1100) - // 1200 - - // Scottie 2: 320x256 px - - // V-sync ? - // First line: 1200 9ms - // Scanline: + progressbar.set_value(scanline_counter); + + // Scottie 2 scanline: + // (First line: 1200 9ms) // 1500 1.5ms // Green // 1500 1.5ms @@ -126,27 +119,71 @@ void SSTVTXView::start_tx() { // 1200 9ms // 1500 1.5ms // Red - // Scanline time: 88.064ms (.2752ms/pixel @ 320 pixels/line) + // Scanline time: 88.064ms (275.2us/pixel @ 320 pixels/line) - transmitter_model.set_sampling_rate(1536000U); + component = scanline_counter % 3; + + if ((!scanline_counter) || (component == 2)) { + scanline_buffer.start_tone.frequency = SSTV_F2D(1200); + scanline_buffer.start_tone.duration = SSTV_MS2S(9); + } else + scanline_buffer.start_tone.duration = 0; + + scanline_buffer.gap_tone.frequency = SSTV_F2D(1500); + scanline_buffer.gap_tone.duration = SSTV_MS2S(1.5); + + if (component == 0) { + // Read a new line + read_boundary(pixels_buffer, + bmp_header.image_data + ((255 - (scanline_counter / 3)) * sizeof(pixels_buffer)), + sizeof(pixels_buffer)); + } + + for (uint32_t bmp_px = 0; bmp_px < 320; bmp_px++) { + pixel_idx = bmp_px * 3; + if (component == 0) + scanline_buffer.luma[bmp_px] = pixels_buffer[pixel_idx + 1]; // Green + else if (component == 1) + scanline_buffer.luma[bmp_px] = pixels_buffer[pixel_idx]; // Blue + else + scanline_buffer.luma[bmp_px] = pixels_buffer[pixel_idx + 2]; // Red + } + + baseband::set_fifo_data((int8_t *)&scanline_buffer); + + scanline_counter++; +} + +void SSTVTXView::start_tx() { + // Baseband SSTV TX code should have a 2 scanlines buffer, and ask + // for fill-up when there's 1 or less remaining. This should leave + // enough time for the code here to generate the scanline data + // before tx. See sstv.hpp: + + // Scottie 2 is 320x256 px + + scanline_counter = 0; + prepare_scanline(); + + transmitter_model.set_sampling_rate(3072000U); transmitter_model.set_rf_amp(true); - //transmitter_model.set_lna(40); - //transmitter_model.set_vga(40); transmitter_model.set_baseband_bandwidth(1750000); transmitter_model.enable(); - /*baseband::set_sstvtx_data( - (1536000 / 44100) - 1, - 12000, - 1, - false, - 0 - );*/ + baseband::set_sstv_data( + 0b00011101, // Scottie 2, 275.2us/px + (uint32_t)(0.0002752 * 3072000.0) + ); + + // Todo: Find a better way to prevent user from changing bitmap during tx + options_bitmaps.set_focusable(false); + tx_view.focus(); } void SSTVTXView::on_bitmap_changed(size_t index) { bmp_file.open("/sstv/" + bitmaps[index].string()); bmp_file.read(&bmp_header, sizeof(bmp_header)); + progressbar.set_max(256 * 3); set_dirty(); } @@ -158,7 +195,6 @@ SSTVTXView::SSTVTXView( using option_t = std::pair; using options_t = std::vector; options_t bitmap_options; - uint32_t c; // Search for valid bitmaps file_list = scan_root_files(u"/sstv", u"*.bmp"); @@ -184,12 +220,16 @@ SSTVTXView::SSTVTXView( return; } - //baseband::run_image(portapack::spi_flash::image_tag_sstv_tx); + // Maybe this could be merged with proc_tones ? Pretty much the same except lots + // of different tones (256+) + baseband::run_image(portapack::spi_flash::image_tag_sstv_tx); add_children({ &labels, &options_bitmaps, - &text_mode + &text_mode, + &progressbar, + &tx_view }); for (const auto& bitmap : bitmaps) @@ -200,6 +240,26 @@ SSTVTXView::SSTVTXView( this->on_bitmap_changed(i); }; options_bitmaps.set_selected_index(0); + on_bitmap_changed(0); + + tx_view.on_edit_frequency = [this, &nav]() { + auto new_view = nav.push(receiver_model.tuning_frequency()); + new_view->on_changed = [this](rf::Frequency f) { + receiver_model.set_tuning_frequency(f); + }; + }; + + tx_view.on_start = [this]() { + start_tx(); + tx_view.set_transmitting(true); + }; + + tx_view.on_stop = [this]() { + baseband::set_sstv_data(0, 0); + tx_view.set_transmitting(false); + transmitter_model.disable(); + options_bitmaps.set_focusable(true); + }; } } /* namespace ui */ diff --git a/firmware/application/ui_sstvtx.hpp b/firmware/application/ui_sstvtx.hpp index fa989970..c83b8e49 100644 --- a/firmware/application/ui_sstvtx.hpp +++ b/firmware/application/ui_sstvtx.hpp @@ -47,20 +47,25 @@ public: void focus() override; void paint(Painter&) override; - std::string title() const override { return "SSTV transmit"; }; + std::string title() const override { return "SSTV TX (beta)"; }; private: NavigationView& nav_; + sstv_scanline scanline_buffer { }; + File bmp_file { }; bmp_header_t bmp_header { }; std::vector bitmaps { }; bool file_error { false }; + uint32_t scanline_counter { 0 }; + uint8_t pixels_buffer[320 * 3]; // 320 pixels @ 24bpp void read_boundary(uint8_t * buffer, uint32_t position, uint32_t length); void on_bitmap_changed(size_t index); void on_tuning_frequency_changed(rf::Frequency f); void start_tx(); + void prepare_scanline(); Labels labels { { { 1 * 8, 1 * 8 }, "File:", Color::light_grey() } @@ -73,7 +78,7 @@ private: }; Text text_mode { { 2 * 8, 4 * 8, 16 * 8, 16 }, - "Scottie 2 (beta)" + "Scottie 2" }; ProgressBar progressbar { @@ -86,15 +91,15 @@ private: 12 }; - /*MessageHandlerRegistration message_handler_fifo_signal { + MessageHandlerRegistration message_handler_fifo_signal { Message::ID::RequestSignal, [this](const Message* const p) { const auto message = static_cast(p); if (message->signal == RequestSignalMessage::Signal::FillRequest) { - this->prepare_audio(); + this->prepare_scanline(); } } - };*/ + }; }; } /* namespace ui */ diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt index 21d4f1f6..68462a10 100644 --- a/firmware/baseband/CMakeLists.txt +++ b/firmware/baseband/CMakeLists.txt @@ -387,6 +387,13 @@ set(MODE_CPPSRC ) DeclareTargets(PREP replay) +### SSTV TX + +set(MODE_CPPSRC + proc_sstvtx.cpp +) +DeclareTargets(PSTX sstvtx) + ### Tones set(MODE_CPPSRC diff --git a/firmware/baseband/proc_sstvtx.cpp b/firmware/baseband/proc_sstvtx.cpp new file mode 100644 index 00000000..c1f5e111 --- /dev/null +++ b/firmware/baseband/proc_sstvtx.cpp @@ -0,0 +1,166 @@ +/* + * 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_sstvtx.hpp" +#include "sine_table_int8.hpp" +#include "event_m4.hpp" + +#include + +// This is called at 3072000/2048 = 1500Hz +void SSTVTXProcessor::execute(const buffer_c8_t& buffer) { + + if (!configured) return; + + for (size_t i = 0; i < buffer.count; i++) { + + if (!sample_count) { + + // This FSM is a mess. It seems to do a lot where it shouldn't (I/Q loop), + // but it actually doesn't do much. Used for sequencing the different parts + // of the scanline. Todo: simplify ! + + if (state == STATE_CALIBRATION) { + // Once per scanline + tone_delta = calibration_sequence[substep].first; + sample_count = calibration_sequence[substep].second; + if (substep == 2) { + substep = 0; + state = STATE_VIS; + } else + substep++; + } else if (state == STATE_VIS) { + // Once per scanline + if (substep == 10) { + current_scanline = &scanline_buffer[buffer_flip]; + buffer_flip ^= 1; + if (asked == false) { + // Ask application for a new scanline + shared_memory.application_queue.push(sig_message); + asked = true; + } + if (current_scanline->start_tone.duration) { + state = STATE_START; + tone_delta = current_scanline->start_tone.frequency; + sample_count = current_scanline->start_tone.duration; + } else { + state = STATE_PIXELS; + tone_delta = current_scanline->gap_tone.frequency; + sample_count = current_scanline->gap_tone.duration; + } + } else { + tone_delta = vis_code_sequence[substep]; + sample_count = SSTV_MS2S(30); // VIS code bit is 30ms + substep++; + } + } else if (state == STATE_START) { + // Once per scanline, optional + state = STATE_PIXELS; + tone_delta = current_scanline->gap_tone.frequency; + sample_count = current_scanline->gap_tone.duration; + } else if (state == STATE_PIXELS) { + // Many times per scanline + pixel_luma = current_scanline->luma[pixel_index]; + + pixel_index++; + + tone_delta = SSTV_F2D(1500 + ((pixel_luma * 800) / 256)); + + sample_count = pixel_duration; + + if (pixel_index >= 320) { + // Scanline done, (dirty) state jump + pixel_index = 0; + state = STATE_VIS; + substep = 10; + } + } + } else { + sample_count--; + } + + tone_sample = (sine_table_i8[(tone_phase & 0xFF000000U) >> 24]); + tone_phase += tone_delta; + + // FM + delta = tone_sample * fm_delta; + + phase += delta; + sphase = phase + (64 << 24); + + re = (sine_table_i8[(sphase & 0xFF000000U) >> 24]); + im = (sine_table_i8[(phase & 0xFF000000U) >> 24]); + + buffer.p[i] = {re, im}; + } +} + +void SSTVTXProcessor::on_message(const Message* const msg) { + const auto message = *reinterpret_cast(msg); + + switch(msg->id) { + case Message::ID::SSTVConfigure: + pixel_duration = message.pixel_duration; + + if (!pixel_duration) { + configured = false; + return; + } + + vis_code = message.vis_code; + + // VIS code: + // 1200 + // 00011101 (0=1300, 1=1100) + // 1200 + vis_code_sequence[0] = SSTV_VIS_SS; + for (uint32_t c = 0; c < 8; c++) { + vis_code_sequence[c + 1] = ((vis_code << c) & 0x80) ? SSTV_VIS_ONE : SSTV_VIS_ZERO; + } + vis_code_sequence[9] = SSTV_VIS_SS; + + fm_delta = 9000 * (0xFFFFFFULL / 3072000); // Fixed for now + + pixel_index = 0; + sample_count = 0; + tone_phase = 0; + state = STATE_CALIBRATION; + substep = 0; + + configured = true; + break; + + case Message::ID::FIFOData: + memcpy(&scanline_buffer[buffer_flip], static_cast(msg)->data, sizeof(sstv_scanline)); + asked = false; + break; + + default: + break; + } +} + +int main() { + EventDispatcher event_dispatcher { std::make_unique() }; + event_dispatcher.run(); + return 0; +} diff --git a/firmware/baseband/proc_sstvtx.hpp b/firmware/baseband/proc_sstvtx.hpp new file mode 100644 index 00000000..c213d45c --- /dev/null +++ b/firmware/baseband/proc_sstvtx.hpp @@ -0,0 +1,87 @@ +/* + * 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_SSTV_TX_H__ +#define __PROC_SSTV_TX_H__ + +#include "portapack_shared_memory.hpp" +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "sstv.hpp" + +using namespace sstv; + +class SSTVTXProcessor : public BasebandProcessor { +public: + void execute(const buffer_c8_t& buffer) override; + + void on_message(const Message* const p) override; + +private: + + // 1900 300ms + // 1200 10ms + // 1900 300ms + const std::pair calibration_sequence[3] = { + { SSTV_F2D(1900), SSTV_MS2S(300) }, + { SSTV_F2D(1200), SSTV_MS2S(10) }, + { SSTV_F2D(1900), SSTV_MS2S(300) } + }; + + enum state_t { + STATE_CALIBRATION = 0, + STATE_VIS, + STATE_START, + STATE_PIXELS + }; + + state_t state { }; + + bool configured { false }; + + BasebandThread baseband_thread { 3072000, this, NORMALPRIO + 20, baseband::Direction::Transmit }; + + uint32_t vis_code_sequence[10] { }; + sstv_scanline scanline_buffer[2] { }; + uint8_t buffer_flip { 0 }, substep { 0 }; + + uint8_t vis_code { }; + uint32_t pixel_duration { }; + + sstv_scanline * current_scanline { }; + + uint8_t pixel_luma { }; + uint32_t fm_delta { 0 }; + uint32_t tone_phase { 0 }; + uint32_t tone_delta { 0 }; + uint32_t pixel_index { 0 }; + uint32_t sample_count { 0 }; + uint32_t phase { 0 }, sphase { 0 }; + int32_t tone_sample { 0 }, delta { 0 }; + int8_t re { 0 }, im { 0 }; + + bool asked { false }; + + RequestSignalMessage sig_message { RequestSignalMessage::Signal::FillRequest }; +}; + +#endif diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index d24369c8..5ff82bbd 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -86,6 +86,7 @@ public: JammerConfigure = 41, WidebandSpectrumConfig = 42, FSKConfigure = 43, + SSTVConfigure = 44, POCSAGPacket = 50, @@ -712,6 +713,21 @@ public: const uint32_t pause_symbols; }; +class SSTVConfigureMessage : public Message { +public: + constexpr SSTVConfigureMessage( + const uint8_t vis_code, + const uint32_t pixel_duration + ) : Message { ID::SSTVConfigure }, + vis_code(vis_code), + pixel_duration(pixel_duration) + { + } + + const uint8_t vis_code; + const uint32_t pixel_duration; +}; + class FSKConfigureMessage : public Message { public: constexpr FSKConfigureMessage( diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp index 59cd0f08..1ead3279 100644 --- a/firmware/common/spi_image.hpp +++ b/firmware/common/spi_image.hpp @@ -76,7 +76,6 @@ constexpr image_tag_t image_tag_wideband_spectrum { 'P', 'S', 'P', 'E' }; constexpr image_tag_t image_tag_jammer { 'P', 'J', 'A', 'M' }; constexpr image_tag_t image_tag_audio_tx { 'P', 'A', 'T', 'X' }; constexpr image_tag_t image_tag_afsk { 'P', 'A', 'F', 'S' }; -constexpr image_tag_t image_tag_epar { 'P', 'E', 'P', 'R' }; constexpr image_tag_t image_tag_tones { 'P', 'T', 'O', 'N' }; constexpr image_tag_t image_tag_rds { 'P', 'R', 'D', 'S' }; constexpr image_tag_t image_tag_ook { 'P', 'O', 'O', 'K' }; @@ -84,6 +83,7 @@ constexpr image_tag_t image_tag_adsb_tx { 'P', 'A', 'D', 'S' }; constexpr image_tag_t image_tag_replay { 'P', 'R', 'E', 'P' }; constexpr image_tag_t image_tag_fsktx { 'P', 'F', 'S', 'K' }; constexpr image_tag_t image_tag_mic_tx { 'P', 'M', 'T', 'X' }; +constexpr image_tag_t image_tag_sstv_tx { 'P', 'S', 'T', 'X' }; constexpr image_tag_t image_tag_noop { 'P', 'N', 'O', 'P' }; diff --git a/firmware/common/sstv.hpp b/firmware/common/sstv.hpp index 19af7772..50ec09fd 100644 --- a/firmware/common/sstv.hpp +++ b/firmware/common/sstv.hpp @@ -19,12 +19,25 @@ * the Free Software Foundation, Inc., 51 Franklin Street, * Boston, MA 02110-1301, USA. */ - + +#ifndef __SSTV_H__ +#define __SSTV_H__ + namespace sstv { +#define SSTV_SAMPLERATE 3072000 +#define SSTV_DELTA_COEF ((1ULL << 32) / SSTV_SAMPLERATE) + +#define SSTV_F2D(f) (uint32_t)((f) * SSTV_DELTA_COEF) +#define SSTV_MS2S(f) (uint32_t)((f) / 1000.0 * (float)SSTV_SAMPLERATE) + +#define SSTV_VIS_SS SSTV_F2D(1200) +#define SSTV_VIS_ZERO SSTV_F2D(1300) +#define SSTV_VIS_ONE SSTV_F2D(1100) + struct sstv_tone { - uint16_t frequency; - uint16_t duration; + uint32_t frequency; + uint32_t duration; }; struct sstv_scanline { @@ -35,3 +48,4 @@ struct sstv_scanline { } /* namespace sstv */ +#endif/*__SSTV_H__*/ diff --git a/firmware/portapack-h1-havoc.bin b/firmware/portapack-h1-havoc.bin index 7e7a4e42..d14ce394 100644 Binary files a/firmware/portapack-h1-havoc.bin and b/firmware/portapack-h1-havoc.bin differ