mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-20 14:39:01 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dadefe86f | |||
| ea40e3a46d | |||
| 86584feb16 | |||
| 47c94dbf26 | |||
| b048b8f4f1 | |||
| 9067e007b6 | |||
| 367eaf54c0 | |||
| de30cbfe1b | |||
| a4d91f12ef | |||
| c4f32ac436 | |||
| efe214167d | |||
| 483a100100 | |||
| 8561ff32b3 | |||
| 87eb8e5863 | |||
| d11669f711 | |||
| 7513cd46fc | |||
| d671455f79 | |||
| 42122a739c | |||
| 03fe508aae |
@@ -239,7 +239,6 @@ set(CPPSRC
|
||||
serializer.cpp
|
||||
spectrum_color_lut.cpp
|
||||
string_format.cpp
|
||||
temperature_logger.cpp
|
||||
theme.cpp
|
||||
touch.cpp
|
||||
tone_key.cpp
|
||||
@@ -280,16 +279,12 @@ set(CPPSRC
|
||||
ui/ui_tone_key.cpp
|
||||
ui/ui_transmitter.cpp
|
||||
ui/ui_bmpview.cpp
|
||||
apps/ais_app.cpp
|
||||
apps/analog_audio_app.cpp
|
||||
apps/ble_rx_app.cpp
|
||||
apps/ble_tx_app.cpp
|
||||
apps/capture_app.cpp
|
||||
apps/pocsag_app.cpp
|
||||
apps/ui_about_simple.cpp
|
||||
apps/ui_adsb_rx.cpp
|
||||
apps/ui_aprs_rx.cpp
|
||||
apps/ui_aprs_tx.cpp
|
||||
apps/ui_battinfo.cpp
|
||||
apps/ui_bmp_file_viewer.cpp
|
||||
apps/ui_btle_rx.cpp
|
||||
@@ -494,7 +489,10 @@ add_executable(${PROJECT_NAME}.elf ${CSRC} ${CPPSRC} ${ASMSRC})
|
||||
target_link_options(${PROJECT_NAME}.elf PRIVATE
|
||||
"LINKER:--defsym,LD_FLASH_SIZE=${FLASH_BYTES_LIMIT_SIZE}"
|
||||
)
|
||||
set_target_properties(${PROJECT_NAME}.elf PROPERTIES LINK_DEPENDS ${LDSCRIPT})
|
||||
# NB: LPC43xx_M0.ld does INCLUDE external.ld, so that one has to be listed too -
|
||||
# otherwise editing the external app section rules silently does not relink.
|
||||
set_target_properties(${PROJECT_NAME}.elf PROPERTIES
|
||||
LINK_DEPENDS "${LDSCRIPT};${CMAKE_CURRENT_SOURCE_DIR}/external/external.ld")
|
||||
add_definitions(${DEFS})
|
||||
include_directories(. ${INCDIR})
|
||||
link_directories(${LLIBDIR})
|
||||
@@ -514,4 +512,4 @@ add_custom_target(
|
||||
DEPENDS ${PROJECT_NAME}.bin
|
||||
)
|
||||
|
||||
add_dependencies(${PROJECT_NAME} baseband)
|
||||
add_dependencies(${PROJECT_NAME} baseband)
|
||||
|
||||
@@ -280,6 +280,12 @@ AnalogAudioView::AnalogAudioView(
|
||||
field_frequency.on_show_options = [this]() {
|
||||
this->on_show_options_frequency();
|
||||
};
|
||||
field_frequency.changing = [this](rf::Frequency frequency) {
|
||||
return this->on_frequency_changed(frequency);
|
||||
};
|
||||
field_frequency.entered = [this](rf::Frequency frequency) {
|
||||
this->set_frequency_absolute(frequency);
|
||||
};
|
||||
|
||||
field_lna.on_show_options = [this]() {
|
||||
this->on_show_options_rf_gain();
|
||||
@@ -308,8 +314,10 @@ AnalogAudioView::AnalogAudioView(
|
||||
};
|
||||
|
||||
waterfall.on_select = [this](int32_t offset) {
|
||||
field_frequency.set_value(receiver_model.target_frequency() + offset);
|
||||
field_frequency.set_value(
|
||||
field_frequency.value() + offset * receiver_model.frequency_step());
|
||||
};
|
||||
waterfall.set_live_tuning(true);
|
||||
|
||||
#ifdef PRALINE
|
||||
button_pro.on_select = [this](Button&) { this->on_show_options_praline(); };
|
||||
@@ -319,6 +327,7 @@ AnalogAudioView::AnalogAudioView(
|
||||
|
||||
// This call starts the correct baseband image to run
|
||||
// and sets the radio up as necessary for the given modulation.
|
||||
sliding_center_frequency = receiver_model.target_frequency();
|
||||
on_modulation_changed(modulation);
|
||||
}
|
||||
|
||||
@@ -328,7 +337,7 @@ AnalogAudioView::AnalogAudioView(
|
||||
: AnalogAudioView(nav) {
|
||||
// Settings to override when launched from another app (versus from AppSettings .ini file)
|
||||
// TODO: Which other settings make sense to override?
|
||||
field_frequency.set_value(override.frequency_app_override);
|
||||
set_frequency_absolute(override.frequency_app_override);
|
||||
on_frequency_step_changed(override.frequency_step);
|
||||
options_modulation.set_by_value(toUType(override.mode));
|
||||
}
|
||||
@@ -573,10 +582,14 @@ void AnalogAudioView::update_modulation(ReceiverModel::Mode modulation) {
|
||||
receiver_model.set_sampling_rate(is_wideband_spectrum_mode ? spec_bw : 3072000);
|
||||
receiver_model.set_baseband_bandwidth(is_wideband_spectrum_mode ? spec_bw / 2 : 1750000);
|
||||
|
||||
receiver_model.set_hidden_offset(modulation == ReceiverModel::Mode::AMAudioFMApt ? -2200 : 0); // wefax needs to be shifted, see wefax rx app.
|
||||
reset_sliding_frequency(modulation);
|
||||
|
||||
receiver_model.enable();
|
||||
|
||||
if (sliding_enabled) {
|
||||
baseband::set_audio_ddc_frequency(0);
|
||||
}
|
||||
|
||||
// TODO: This doesn't belong here! There's a better way.
|
||||
size_t sampling_rate = 0;
|
||||
switch (modulation) {
|
||||
@@ -610,7 +623,64 @@ void AnalogAudioView::handle_coded_squelch(uint32_t value) {
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_freqchg(int64_t freq) {
|
||||
field_frequency.set_value(freq);
|
||||
set_frequency_absolute(freq);
|
||||
}
|
||||
|
||||
void AnalogAudioView::set_frequency_absolute(rf::Frequency frequency) {
|
||||
if (!sliding_enabled) {
|
||||
field_frequency.set_value(frequency);
|
||||
return;
|
||||
}
|
||||
|
||||
sliding_center_frequency = frequency;
|
||||
|
||||
/* set_value() does not call on_change when the displayed frequency is
|
||||
* already equal, so reset the hardware and DDC explicitly in that case. */
|
||||
if (field_frequency.value() == frequency) {
|
||||
receiver_model.set_target_frequency_with_hidden_offset(frequency, 0);
|
||||
baseband::set_audio_ddc_frequency(0);
|
||||
} else {
|
||||
field_frequency.set_value(frequency);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t AnalogAudioView::sliding_limit() const {
|
||||
const bool zoom_x2 =
|
||||
receiver_model.modulation() == ReceiverModel::Mode::AMAudio &&
|
||||
previous_zoom != 0;
|
||||
return zoom_x2 ? sliding_limit_zoom_x2 : sliding_limit_zoom_x1;
|
||||
}
|
||||
|
||||
void AnalogAudioView::reset_sliding_frequency(ReceiverModel::Mode modulation) {
|
||||
sliding_enabled =
|
||||
modulation == ReceiverModel::Mode::AMAudio ||
|
||||
modulation == ReceiverModel::Mode::NarrowbandFMAudio;
|
||||
sliding_center_frequency = receiver_model.target_frequency();
|
||||
|
||||
/* AMFM keeps its existing Wefax offset; sliding applies to AM and NFM. */
|
||||
receiver_model.set_hidden_offset(
|
||||
modulation == ReceiverModel::Mode::AMAudioFMApt ? -2200 : 0);
|
||||
}
|
||||
|
||||
bool AnalogAudioView::on_frequency_changed(rf::Frequency frequency) {
|
||||
if (!sliding_enabled)
|
||||
return false;
|
||||
|
||||
const auto limit = sliding_limit();
|
||||
int64_t offset = frequency - sliding_center_frequency;
|
||||
if (offset > limit) {
|
||||
sliding_center_frequency = frequency - limit;
|
||||
offset = limit;
|
||||
} else if (offset < -limit) {
|
||||
sliding_center_frequency = frequency + limit;
|
||||
offset = -limit;
|
||||
}
|
||||
|
||||
/* Store the displayed frequency and retune the hardware centre atomically. */
|
||||
receiver_model.set_target_frequency_with_hidden_offset(
|
||||
frequency, sliding_center_frequency - frequency);
|
||||
baseband::set_audio_ddc_frequency(static_cast<int32_t>(offset));
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef PRALINE
|
||||
|
||||
@@ -269,6 +269,10 @@ class AnalogAudioView : public View {
|
||||
uint8_t zoom_factor_amfm{0}; // initial zoom factor in AMFM mode
|
||||
uint8_t previous_AM_mode_option{0}; // GUI 5 AM modes : (0..4 ) (DSB9K, DSB6K, USB,LSB, CW). Used to select proper FIR filter (0..11) AM mode + offset 0 (zoom+1) or +6 (if zoom+2)
|
||||
uint8_t previous_zoom{0}; // GUI ZOOM+1, ZOOM+2 , equivalent to two values offset 0 (zoom+1) or +6 (if zoom+2)
|
||||
static constexpr int32_t sliding_limit_zoom_x1 = 50000;
|
||||
static constexpr int32_t sliding_limit_zoom_x2 = 30000;
|
||||
rf::Frequency sliding_center_frequency{0};
|
||||
bool sliding_enabled{false};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_audio",
|
||||
@@ -354,6 +358,10 @@ class AnalogAudioView : public View {
|
||||
void handle_coded_squelch(uint32_t value);
|
||||
|
||||
void on_freqchg(int64_t freq);
|
||||
bool on_frequency_changed(rf::Frequency frequency);
|
||||
void set_frequency_absolute(rf::Frequency frequency);
|
||||
int32_t sliding_limit() const;
|
||||
void reset_sliding_frequency(ReceiverModel::Mode modulation);
|
||||
|
||||
MessageHandlerRegistration message_handler_coded_squelch{
|
||||
Message::ID::CodedSquelch,
|
||||
|
||||
@@ -328,8 +328,8 @@ void GlassView::update_min(int32_t v) {
|
||||
min_size = search_span;
|
||||
if (min_size < 1)
|
||||
min_size = 1;
|
||||
if (v > 7200 - min_size) {
|
||||
v = 7200 - min_size;
|
||||
if (v > LOOKING_GLASS_MAX_FREQ_MHZ - min_size) {
|
||||
v = LOOKING_GLASS_MAX_FREQ_MHZ - min_size;
|
||||
}
|
||||
if (v > (field_frequency_max.value() - min_size))
|
||||
field_frequency_max.set_value(v + min_size, false);
|
||||
@@ -348,6 +348,9 @@ void GlassView::update_max(int32_t v) {
|
||||
if (v < min_size) {
|
||||
v = min_size;
|
||||
}
|
||||
if (v > LOOKING_GLASS_MAX_FREQ_MHZ) {
|
||||
v = LOOKING_GLASS_MAX_FREQ_MHZ;
|
||||
}
|
||||
if (v < (field_frequency_min.value() + min_size))
|
||||
field_frequency_min.set_value(v - min_size, false);
|
||||
if (locked_range)
|
||||
@@ -398,7 +401,7 @@ GlassView::GlassView(
|
||||
&button_jump,
|
||||
&button_rst,
|
||||
&button_rssi,
|
||||
&freq_stats});
|
||||
&freq_stats, &button_play});
|
||||
|
||||
load_presets(); // Load available presets from TXT files (or default).
|
||||
preset_index = clip<uint8_t>(preset_index, 0, presets_db.size());
|
||||
@@ -554,6 +557,21 @@ GlassView::GlassView(
|
||||
reset_live_view();
|
||||
};
|
||||
|
||||
button_play.on_select = [this](ImageButton&) {
|
||||
paused = !paused;
|
||||
if (paused) {
|
||||
button_play.set_foreground(Theme::getInstance()->fg_red->foreground);
|
||||
button_play.set_background(Theme::getInstance()->fg_red->background);
|
||||
baseband::spectrum_streaming_stop();
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
} else {
|
||||
button_play.set_foreground(Theme::getInstance()->fg_green->foreground);
|
||||
button_play.set_background(Theme::getInstance()->fg_green->background);
|
||||
baseband::spectrum_streaming_start();
|
||||
button_play.set_bitmap(&bitmap_stop);
|
||||
}
|
||||
};
|
||||
|
||||
display.scroll_set_area(109, screen_height - 1); // Restart scroll on the correct coordinates
|
||||
|
||||
// trigger:
|
||||
@@ -630,7 +648,7 @@ void GlassView::load_presets() {
|
||||
parse_int(cols[1], entry.max);
|
||||
entry.label = trimr(cols[2]);
|
||||
|
||||
if (entry.min == 0 || entry.max == 0 || entry.min >= entry.max)
|
||||
if (entry.max == 0 || entry.min >= entry.max)
|
||||
continue; // Invalid line.
|
||||
|
||||
presets_db.emplace_back(std::move(entry));
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace ui {
|
||||
#define LOOKING_GLASS_SLICE_WIDTH_MAX 20000000
|
||||
#define LOOKING_GLASS_MAX_SAMPLERATE 20000000
|
||||
#define MHZ_DIV 1000000
|
||||
#define LOOKING_GLASS_MAX_FREQ_MHZ 7250 // HackRF upper tuning limit (band_high top), in MHz
|
||||
|
||||
// blanked DC (16 centered bins ignored ) and top left and right (2 bins ignored on each side )
|
||||
#define LOOKING_GLASS_FASTSCAN 0
|
||||
@@ -129,6 +130,8 @@ class GlassView : public View {
|
||||
void populate_presets();
|
||||
void launch_audio(rf::Frequency center_freq);
|
||||
|
||||
bool paused{false};
|
||||
|
||||
rf::Frequency search_span{0};
|
||||
rf::Frequency f_center{0};
|
||||
rf::Frequency f_center_ini{0};
|
||||
@@ -179,14 +182,14 @@ class GlassView : public View {
|
||||
NumberField field_frequency_min{
|
||||
{UI_POS_X(4), UI_POS_Y(0)},
|
||||
4,
|
||||
{0, 7199},
|
||||
{0, LOOKING_GLASS_MAX_FREQ_MHZ - 1},
|
||||
1, // number of steps by encoder delta
|
||||
' '};
|
||||
|
||||
NumberField field_frequency_max{
|
||||
{UI_POS_X(13), UI_POS_Y(0)},
|
||||
4,
|
||||
{1, 7200},
|
||||
{1, LOOKING_GLASS_MAX_FREQ_MHZ},
|
||||
1, // number of steps by encoder delta
|
||||
' '};
|
||||
|
||||
@@ -221,6 +224,12 @@ class GlassView : public View {
|
||||
{UI_POS_X_RIGHT(8), UI_POS_Y(2.25), UI_POS_WIDTH(8), UI_POS_HEIGHT(0.5)},
|
||||
""};
|
||||
|
||||
ImageButton button_play{
|
||||
{UI_POS_X_RIGHT(2), UI_POS_Y(3.25) - 2, UI_POS_WIDTH(2), UI_POS_HEIGHT(0.5)},
|
||||
&bitmap_stop,
|
||||
Theme::getInstance()->fg_green->foreground,
|
||||
Theme::getInstance()->fg_green->background};
|
||||
|
||||
TextField field_marker{
|
||||
{UI_POS_X(12), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(1)},
|
||||
""};
|
||||
|
||||
@@ -74,7 +74,7 @@ void SubGhzDRecentEntryDetailView::update_data() {
|
||||
console.writeln("Action: 0x" + to_string_hex(func_code));
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPS_KEELOQ) {
|
||||
if (entry_.sensorType == FPS_KEELOQ || entry_.sensorType == FPS_SUPERROLLO) {
|
||||
console.writeln("Fix: " + to_string_hex(fix));
|
||||
console.writeln("Encrypted: " + to_string_hex(encrypted));
|
||||
console.writeln("Manufacturer: " + mf_name);
|
||||
@@ -243,6 +243,8 @@ const char* SubGhzDView::getSensorTypeName(FPROTO_SUBGHZD_SENSOR type) {
|
||||
return "Ido 11x";
|
||||
case FPS_INTERTECHNOV3:
|
||||
return "InterTehcno v3";
|
||||
case FPS_SUPERROLLO:
|
||||
return "Superrollo";
|
||||
case FPS_KEELOQ:
|
||||
return "KeeLoq";
|
||||
case FPS_KINGGATESSTYLO4K:
|
||||
@@ -671,7 +673,7 @@ void SubGhzDRecentEntryDetailView::parseProtocol() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPS_KEELOQ) {
|
||||
if (entry_.sensorType == FPS_KEELOQ || entry_.sensorType == FPS_SUPERROLLO) {
|
||||
uint64_t data_rev = FProtoGeneral::subghz_protocol_blocks_reverse_key(entry_.data, 64);
|
||||
|
||||
btn = data_rev >> 60;
|
||||
|
||||
@@ -32,10 +32,6 @@
|
||||
|
||||
#include "core_control.hpp"
|
||||
|
||||
/* Set true to enable additional checks to ensure
|
||||
* M4 and M0 are synchronized before passing messages. */
|
||||
static constexpr bool enforce_core_sync = true;
|
||||
|
||||
/* Set true to enable check for baseband messages getting stuck.
|
||||
* This implies the baseband thread is not dequeuing and has probably stalled.
|
||||
* NB: This check adds a small amout of overhead to the message sending code
|
||||
@@ -343,6 +339,11 @@ void set_spectrum(
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_audio_ddc_frequency(int32_t frequency) {
|
||||
const AudioDDCConfigMessage message{frequency};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_time_sink(
|
||||
const size_t sampling_rate,
|
||||
const size_t trigger) {
|
||||
@@ -469,7 +470,7 @@ bool is_image_running() {
|
||||
return baseband_image_running;
|
||||
}
|
||||
|
||||
void run_image(const spi_flash::image_tag_t image_tag) {
|
||||
void run_image(const spi_flash::image_tag_t image_tag, bool enforce_core_sync) {
|
||||
if (baseband_image_running) {
|
||||
chDbgPanic("BBRunning");
|
||||
}
|
||||
@@ -482,7 +483,7 @@ void run_image(const spi_flash::image_tag_t image_tag) {
|
||||
|
||||
creg::m4txevent::enable();
|
||||
|
||||
if constexpr (enforce_core_sync) {
|
||||
if (enforce_core_sync) {
|
||||
// Wait up to 3 seconds for baseband to start handling events.
|
||||
auto count = 3'000u;
|
||||
while (!shared_memory.baseband_ready && --count)
|
||||
@@ -493,7 +494,7 @@ void run_image(const spi_flash::image_tag_t image_tag) {
|
||||
}
|
||||
}
|
||||
|
||||
void run_prepared_image(const uint32_t m4_code) {
|
||||
void run_prepared_image(const uint32_t m4_code, bool enforce_core_sync) {
|
||||
if (baseband_image_running) {
|
||||
chDbgPanic("BBRunning");
|
||||
}
|
||||
@@ -506,7 +507,7 @@ void run_prepared_image(const uint32_t m4_code) {
|
||||
|
||||
creg::m4txevent::enable();
|
||||
|
||||
if constexpr (enforce_core_sync) {
|
||||
if (enforce_core_sync) {
|
||||
// Wait up to 3 seconds for baseband to start handling events.
|
||||
auto count = 3'000u;
|
||||
while (!shared_memory.baseband_ready && --count)
|
||||
|
||||
@@ -100,6 +100,7 @@ void set_rds_data(const uint16_t message_length);
|
||||
void set_spectrum(
|
||||
const size_t sampling_rate,
|
||||
const size_t trigger);
|
||||
void set_audio_ddc_frequency(int32_t frequency);
|
||||
void set_time_sink(
|
||||
const size_t sampling_rate,
|
||||
const size_t trigger);
|
||||
@@ -130,8 +131,8 @@ void request_beep_stop();
|
||||
void request_audio_beep(uint32_t freq, uint32_t sample_rate, uint32_t duration_ms);
|
||||
|
||||
bool is_image_running();
|
||||
void run_image(const portapack::spi_flash::image_tag_t image_tag);
|
||||
void run_prepared_image(const uint32_t m4_code);
|
||||
void run_image(const portapack::spi_flash::image_tag_t image_tag, bool enforce_core_sync = true);
|
||||
void run_prepared_image(const uint32_t m4_code, bool enforce_core_sync = true);
|
||||
void shutdown();
|
||||
|
||||
void spectrum_streaming_start();
|
||||
|
||||
@@ -907,12 +907,24 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) {
|
||||
// Set FPGA RX decimation register
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, n);
|
||||
|
||||
/* RX Mode: Register 3 is FPGA_REG_RX_DIGITAL_GAIN.
|
||||
* We shift up by (3 * n) to compensate for CIC bit-growth.
|
||||
/* No RX digital-gain register is written here.
|
||||
*
|
||||
* Register 0x03 used to be programmed with (3 * n + 2) as a "CIC
|
||||
* bit-growth" renormalisation. The gateware has no such register: the
|
||||
* RX decimator is a chain of unity-gain half-band FIRs selected by
|
||||
* rx_decim (fpga/top/standard.py), and 0x03 is rx_pstep, whose top two
|
||||
* bits are the quarter-rate shift. Writing a gain here silently
|
||||
* cancelled the shift that set_tuning_frequency() had programmed, which
|
||||
* left the analogue passband offset with no matching rotation.
|
||||
*
|
||||
* The shift depends on the AFE rate we just chose, so re-apply it after
|
||||
* the rate change. ReceiverModel::update_sampling_rate() calls
|
||||
* update_tuning_frequency() straight after this, which does exactly
|
||||
* that; the write below only keeps the register consistent in between.
|
||||
*/
|
||||
uint8_t ds = (3 * n);
|
||||
ds += 2;
|
||||
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, ds);
|
||||
fpga_debug_register_write(
|
||||
FPGA_REG_RX_PSTEP,
|
||||
(radio::debug::get_cached_quarter_shift() & 0b11) << FPGA_RX_QUARTER_SHIFT_SHIFT);
|
||||
|
||||
// Re-enable FPGA processing with clean state ===
|
||||
fpga_debug_register_write(1, 0x01);
|
||||
|
||||
@@ -61,7 +61,6 @@ uint32_t blink_patterns[] = {
|
||||
|
||||
void config_mode_run() {
|
||||
configure_pins_portapack();
|
||||
portapack::gpio_dfu.input();
|
||||
portapack::persistent_memory::cache::init();
|
||||
|
||||
if (hackrf_r9) {
|
||||
@@ -80,14 +79,14 @@ void config_mode_run() {
|
||||
config_mode_blink_until_dfu();
|
||||
}
|
||||
|
||||
auto last_dfu_btn = portapack::gpio_dfu.read();
|
||||
auto last_dfu_btn = dfu_button.read();
|
||||
|
||||
int32_t counter = 0;
|
||||
int8_t blink_pattern_value = portapack::persistent_memory::config_cpld() +
|
||||
(portapack::persistent_memory::config_disable_external_tcxo() ? 5 : 0);
|
||||
|
||||
while (true) {
|
||||
auto dfu_btn = portapack::gpio_dfu.read();
|
||||
auto dfu_btn = dfu_button.read();
|
||||
auto dfu_clicked = last_dfu_btn == true && dfu_btn == false;
|
||||
last_dfu_btn = dfu_btn;
|
||||
|
||||
@@ -137,7 +136,7 @@ void config_mode_blink_until_dfu() {
|
||||
led_usb.setInactive();
|
||||
chThdSleepMilliseconds(115);
|
||||
|
||||
auto dfu_btn = portapack::gpio_dfu.read();
|
||||
auto dfu_btn = dfu_button.read();
|
||||
if (dfu_btn)
|
||||
break;
|
||||
}
|
||||
@@ -145,7 +144,7 @@ void config_mode_blink_until_dfu() {
|
||||
while (true) {
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
auto dfu_btn = portapack::gpio_dfu.read();
|
||||
auto dfu_btn = dfu_button.read();
|
||||
if (!dfu_btn)
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -400,8 +400,6 @@ void EventDispatcher::handle_local_queue() {
|
||||
void EventDispatcher::handle_rtc_tick() {
|
||||
sd_card::poll_inserted();
|
||||
|
||||
portapack::temperature_logger.second_tick();
|
||||
|
||||
const auto backlight_timer = portapack::persistent_memory::config_backlight_timer();
|
||||
if (backlight_timer.timeout_enabled()) {
|
||||
if (portapack::bl_tick_counter == backlight_timer.timeout_seconds())
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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_adsb_rx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::adsbrx {
|
||||
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<ADSBRxView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::adsbrx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_adsbrx.application_information"), used)) application_information_t _application_information_adsbrx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::adsbrx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "ADS-B",
|
||||
/*.bitmap_data = */ {
|
||||
0x80,
|
||||
0x01,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_adsbrx */ {'P', 'A', 'D', 'R'},
|
||||
/*.m4_app_offset = */ 0x00000000,
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
Vendored
+27
-6
@@ -38,7 +38,7 @@ using namespace portapack;
|
||||
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::adsbrx {
|
||||
|
||||
static const char speed_type_msg[][6] = {" Spd:", " IAS:", " TAS:"};
|
||||
|
||||
@@ -46,13 +46,19 @@ static std::string get_map_tag(const AircraftRecentEntry& entry) {
|
||||
return trimr(entry.callsign.empty() ? entry.icao_str : entry.callsign);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::adsbrx
|
||||
|
||||
namespace ui {
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<AircraftRecentEntries>::draw(
|
||||
void RecentEntriesTable<external_app::adsbrx::AircraftRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style,
|
||||
RecentEntriesColumns& columns) {
|
||||
using namespace external_app::adsbrx;
|
||||
|
||||
Color target_color;
|
||||
std::string entry_string;
|
||||
|
||||
@@ -75,7 +81,7 @@ void RecentEntriesTable<AircraftRecentEntries>::draw(
|
||||
uint8_t firstcolwidth = columns.at(0).second;
|
||||
ipc.resize(firstcolwidth, ' '); // Make sure this is always match the first column's width that is dynamic.
|
||||
|
||||
entry_string += ipc + to_string_dec_uint((unsigned int)(entry.pos.altitude / 100), 4);
|
||||
entry_string += ipc + to_string_dec_int((int32_t)(entry.pos.altitude / 100), 4);
|
||||
|
||||
if (entry.velo.type == SPD_IAS && entry.pos.alt_valid) { // IAS can be converted to TAS
|
||||
// It is generally accepted that for every thousand feet of altitude,
|
||||
@@ -107,6 +113,10 @@ void RecentEntriesTable<AircraftRecentEntries>::draw(
|
||||
bitmap_target, target_color, style.background);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
namespace ui::external_app::adsbrx {
|
||||
|
||||
/* ADSBLogger ********************************************/
|
||||
|
||||
void ADSBLogger::log(const ADSBLogEntry& log_entry) {
|
||||
@@ -532,6 +542,18 @@ ADSBRxView::ADSBRxView(NavigationView& nav) {
|
||||
logger = std::make_unique<ADSBLogger>();
|
||||
logger->append(logs_dir / u"ADSB.TXT");
|
||||
|
||||
/* First run only: start from the configuration that is known to receive
|
||||
* ADS-B on this hardware -- LNA 32, VGA 32, RF amp ON. The first two are
|
||||
* already ReceiverModel's defaults; the amp is not, and running without it
|
||||
* costs about 14 dB, which is the difference between a busy list and an
|
||||
* empty one. Once the user has saved settings for this app their choice
|
||||
* wins, so this only sets the starting point.
|
||||
* Going through the field rather than receiver_model keeps the displayed
|
||||
* value in step: RFAmpField snapshots rf_amp() in its own constructor,
|
||||
* which has already run by the time we get here. */
|
||||
if (!settings_.loaded())
|
||||
field_rf_amp.set_value(1);
|
||||
|
||||
receiver_model.enable();
|
||||
baseband::set_adsb();
|
||||
|
||||
@@ -668,9 +690,8 @@ void ADSBRxView::on_frame(const ADSBFrameMessage* message) {
|
||||
(raw_data[3] & 15);
|
||||
|
||||
// The final altitude is due to the resulting number multiplied by 25, minus 1000.
|
||||
// altitude = 25N - 1000 (ft); minimum is -1000 ft when N=0 (Q=1, M=0 per ICAO Annex 10).
|
||||
altitude = 25 * n - 1000;
|
||||
if (altitude < 0)
|
||||
altitude = 0;
|
||||
} // else N is an 11 bit Gillham coded altitude
|
||||
}
|
||||
|
||||
@@ -817,4 +838,4 @@ void ADSBRxView::remove_expired_entries() {
|
||||
recent.erase(it.base(), recent.end());
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::adsbrx
|
||||
Vendored
+2
-2
@@ -39,7 +39,7 @@
|
||||
|
||||
using namespace adsb;
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::adsbrx {
|
||||
#define AIRCRAFT_ID_L 1 // aircraft ID message type (lowest type id)
|
||||
#define AIRCRAFT_ID_H 4 // aircraft ID message type (highest type id)
|
||||
|
||||
@@ -491,4 +491,4 @@ class ADSBRxView : public View {
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::adsbrx
|
||||
+37
-29
@@ -35,7 +35,8 @@ using namespace portapack;
|
||||
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
namespace ais {
|
||||
namespace ui::external_app::ais_rx {
|
||||
|
||||
namespace format {
|
||||
|
||||
static std::string latlon_abs_normalized(const int32_t normalized, const char suffixes[2]) {
|
||||
@@ -47,7 +48,7 @@ static std::string latlon_abs_normalized(const int32_t normalized, const char su
|
||||
return to_string_dec_uint(degrees) + "." + to_string_dec_uint(fraction, 6, '0') + suffix;
|
||||
}
|
||||
|
||||
static std::string latlon(const Latitude latitude, const Longitude longitude) {
|
||||
static std::string latlon(const ais::Latitude latitude, const ais::Longitude longitude) {
|
||||
if (latitude.is_valid() && longitude.is_valid()) {
|
||||
return latlon_abs_normalized(latitude.normalized(), "SN") + " " + latlon_abs_normalized(longitude.normalized(), "WE");
|
||||
} else if (latitude.is_not_available() && longitude.is_not_available()) {
|
||||
@@ -128,7 +129,7 @@ static std::string navigational_status(const unsigned int value) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string rate_of_turn(const RateOfTurn value) {
|
||||
static std::string rate_of_turn(const ais::RateOfTurn value) {
|
||||
switch (value) {
|
||||
case -128:
|
||||
return "not available";
|
||||
@@ -149,7 +150,7 @@ static std::string rate_of_turn(const RateOfTurn value) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string speed_over_ground(const SpeedOverGround value) {
|
||||
static std::string speed_over_ground(const ais::SpeedOverGround value) {
|
||||
if (value == 1023) {
|
||||
return "not available";
|
||||
} else if (value == 1022) {
|
||||
@@ -159,7 +160,7 @@ static std::string speed_over_ground(const SpeedOverGround value) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string course_over_ground(const CourseOverGround value) {
|
||||
static std::string course_over_ground(const ais::CourseOverGround value) {
|
||||
if (value > 3600) {
|
||||
return "invalid";
|
||||
} else if (value == 3600) {
|
||||
@@ -169,7 +170,7 @@ static std::string course_over_ground(const CourseOverGround value) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string true_heading(const TrueHeading value) {
|
||||
static std::string true_heading(const ais::TrueHeading value) {
|
||||
if (value == 511) {
|
||||
return "not available";
|
||||
} else if (value > 359) {
|
||||
@@ -180,7 +181,6 @@ static std::string true_heading(const TrueHeading value) {
|
||||
}
|
||||
|
||||
} /* namespace format */
|
||||
} /* namespace ais */
|
||||
|
||||
void AISLogger::on_packet(const ais::Packet& packet) {
|
||||
// TODO: Unstuff here, not in baseband!
|
||||
@@ -265,25 +265,33 @@ void AISRecentEntry::update(const ais::Packet& packet) {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::ais_rx
|
||||
|
||||
namespace ui {
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<AISRecentEntries>::draw(
|
||||
void RecentEntriesTable<external_app::ais_rx::AISRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style,
|
||||
RecentEntriesColumns&) {
|
||||
std::string line = ais::format::mmsi(entry.mmsi) + " ";
|
||||
using namespace external_app::ais_rx;
|
||||
|
||||
std::string line = format::mmsi(entry.mmsi) + " ";
|
||||
if (!entry.name.empty()) {
|
||||
line += ais::format::text(entry.name);
|
||||
line += format::text(entry.name);
|
||||
} else {
|
||||
line += ais::format::text(entry.call_sign);
|
||||
line += format::text(entry.call_sign);
|
||||
}
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
namespace ui::external_app::ais_rx {
|
||||
|
||||
AISRecentEntryDetailView::AISRecentEntryDetailView(NavigationView& nav) {
|
||||
add_children({
|
||||
&button_done,
|
||||
@@ -298,12 +306,12 @@ AISRecentEntryDetailView::AISRecentEntryDetailView(NavigationView& nav) {
|
||||
|
||||
button_see_map.on_select = [this, &nav](Button&) {
|
||||
geomap_view = nav.push<GeoMapView>(
|
||||
ais::format::text(entry_.name),
|
||||
format::text(entry_.name),
|
||||
0,
|
||||
GeoPos::alt_unit::METERS,
|
||||
GeoPos::spd_unit::KNOTS,
|
||||
ais::format::latlon_float(entry_.last_position.latitude.normalized()),
|
||||
ais::format::latlon_float(entry_.last_position.longitude.normalized()),
|
||||
format::latlon_float(entry_.last_position.latitude.normalized()),
|
||||
format::latlon_float(entry_.last_position.longitude.normalized()),
|
||||
entry_.last_position.true_heading,
|
||||
[this]() {
|
||||
send_updates = false;
|
||||
@@ -324,14 +332,14 @@ AISRecentEntryDetailView& AISRecentEntryDetailView::operator=(const AISRecentEnt
|
||||
|
||||
void AISRecentEntryDetailView::update_position() {
|
||||
if (send_updates)
|
||||
geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground > 1022 ? 0 : entry_.last_position.speed_over_ground / 10);
|
||||
geomap_view->update_position(format::latlon_float(entry_.last_position.latitude.normalized()), format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground > 1022 ? 0 : entry_.last_position.speed_over_ground / 10);
|
||||
}
|
||||
|
||||
bool AISRecentEntryDetailView::add_map_marker(const AISRecentEntry& entry) {
|
||||
if (geomap_view && send_updates) {
|
||||
GeoMarker marker{};
|
||||
marker.lon = ais::format::latlon_float(entry.last_position.longitude.normalized());
|
||||
marker.lat = ais::format::latlon_float(entry.last_position.latitude.normalized());
|
||||
marker.lon = format::latlon_float(entry.last_position.longitude.normalized());
|
||||
marker.lat = format::latlon_float(entry.last_position.latitude.normalized());
|
||||
marker.angle = entry.last_position.true_heading;
|
||||
marker.tag = entry.call_sign.empty() ? to_string_dec_uint(entry.mmsi) : entry.call_sign;
|
||||
auto markerStored = geomap_view->store_marker(marker);
|
||||
@@ -377,18 +385,18 @@ void AISRecentEntryDetailView::paint(Painter& painter) {
|
||||
|
||||
auto field_rect = Rect{rect.left(), rect.top() + 16, rect.width(), 16};
|
||||
|
||||
field_rect = draw_field(painter, field_rect, s, "MMSI", ais::format::mmsi(entry_.mmsi));
|
||||
field_rect = draw_field(painter, field_rect, s, "Ctry", ais::format::mid(entry_.mmsi));
|
||||
field_rect = draw_field(painter, field_rect, s, "Name", ais::format::text(entry_.name));
|
||||
field_rect = draw_field(painter, field_rect, s, "Call", ais::format::text(entry_.call_sign));
|
||||
field_rect = draw_field(painter, field_rect, s, "Dest", ais::format::text(entry_.destination));
|
||||
field_rect = draw_field(painter, field_rect, s, "MMSI", format::mmsi(entry_.mmsi));
|
||||
field_rect = draw_field(painter, field_rect, s, "Ctry", format::mid(entry_.mmsi));
|
||||
field_rect = draw_field(painter, field_rect, s, "Name", format::text(entry_.name));
|
||||
field_rect = draw_field(painter, field_rect, s, "Call", format::text(entry_.call_sign));
|
||||
field_rect = draw_field(painter, field_rect, s, "Dest", format::text(entry_.destination));
|
||||
field_rect = draw_field(painter, field_rect, s, "Last", to_string_datetime(entry_.last_position.timestamp));
|
||||
field_rect = draw_field(painter, field_rect, s, "Pos ", ais::format::latlon(entry_.last_position.latitude, entry_.last_position.longitude));
|
||||
field_rect = draw_field(painter, field_rect, s, "Stat", ais::format::navigational_status(entry_.navigational_status));
|
||||
field_rect = draw_field(painter, field_rect, s, "RoT ", ais::format::rate_of_turn(entry_.last_position.rate_of_turn));
|
||||
field_rect = draw_field(painter, field_rect, s, "SoG ", ais::format::speed_over_ground(entry_.last_position.speed_over_ground));
|
||||
field_rect = draw_field(painter, field_rect, s, "CoG ", ais::format::course_over_ground(entry_.last_position.course_over_ground));
|
||||
field_rect = draw_field(painter, field_rect, s, "Head", ais::format::true_heading(entry_.last_position.true_heading));
|
||||
field_rect = draw_field(painter, field_rect, s, "Pos ", format::latlon(entry_.last_position.latitude, entry_.last_position.longitude));
|
||||
field_rect = draw_field(painter, field_rect, s, "Stat", format::navigational_status(entry_.navigational_status));
|
||||
field_rect = draw_field(painter, field_rect, s, "RoT ", format::rate_of_turn(entry_.last_position.rate_of_turn));
|
||||
field_rect = draw_field(painter, field_rect, s, "SoG ", format::speed_over_ground(entry_.last_position.speed_over_ground));
|
||||
field_rect = draw_field(painter, field_rect, s, "CoG ", format::course_over_ground(entry_.last_position.course_over_ground));
|
||||
field_rect = draw_field(painter, field_rect, s, "Head", format::true_heading(entry_.last_position.true_heading));
|
||||
field_rect = draw_field(painter, field_rect, s, "Rx #", to_string_dec_uint(entry_.received_count));
|
||||
}
|
||||
|
||||
@@ -504,4 +512,4 @@ void AISAppView::on_show_detail(const AISRecentEntry& entry) {
|
||||
recent_entry_detail_view.update_map_markers(recent);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::ais_rx
|
||||
+3
-3
@@ -50,6 +50,8 @@ using namespace lpc43xx;
|
||||
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
namespace ui::external_app::ais_rx {
|
||||
|
||||
struct AISPosition {
|
||||
rtc::RTC timestamp{};
|
||||
ais::Latitude latitude{};
|
||||
@@ -109,8 +111,6 @@ class AISLogger {
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
|
||||
using AISRecentEntriesView = RecentEntriesView<AISRecentEntries>;
|
||||
|
||||
class AISRecentEntryDetailView : public View {
|
||||
@@ -239,6 +239,6 @@ class AISAppView : public View {
|
||||
void on_tick_second();
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::ais_rx
|
||||
|
||||
#endif /*__AIS_APP_H__*/
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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 "ais_app.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::ais_rx {
|
||||
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<AISAppView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::ais_rx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_ais_rx.application_information"), used)) application_information_t _application_information_ais_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::ais_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "AIS Boats",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x01,
|
||||
0x80,
|
||||
0x01,
|
||||
0xC0,
|
||||
0x01,
|
||||
0xC0,
|
||||
0x0D,
|
||||
0xE0,
|
||||
0x3D,
|
||||
0xF0,
|
||||
0x3D,
|
||||
0xF8,
|
||||
0x7D,
|
||||
0xFC,
|
||||
0x7D,
|
||||
0xFC,
|
||||
0x7D,
|
||||
0xFE,
|
||||
0x7D,
|
||||
0xFF,
|
||||
0x7D,
|
||||
0x00,
|
||||
0x00,
|
||||
0xF8,
|
||||
0x7F,
|
||||
0xF8,
|
||||
0x3F,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_ais */ {'P', 'A', 'I', 'S'},
|
||||
/*.m4_app_offset = */ 0x00000000,
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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_aprs_rx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::aprs_rx {
|
||||
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<APRSRXView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::aprs_rx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_aprs_rx.application_information"), used)) application_information_t _application_information_aprs_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::aprs_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "APRS",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x4C,
|
||||
0x32,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x25,
|
||||
0xA4,
|
||||
0x25,
|
||||
0xA4,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0x25,
|
||||
0xA4,
|
||||
0x25,
|
||||
0xA4,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x4C,
|
||||
0x32,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_aprs_rx */ {'P', 'A', 'P', 'R'},
|
||||
/*.m4_app_offset = */ 0x00000000,
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
Vendored
+12
-2
@@ -31,19 +31,25 @@
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui::external_app::aprs_rx {
|
||||
|
||||
void APRSLogger::log_raw_data(const std::string& data) {
|
||||
log_file.write_entry(data);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::aprs_rx
|
||||
|
||||
namespace ui {
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<APRSRecentEntries>::draw(
|
||||
void RecentEntriesTable<external_app::aprs_rx::APRSRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style,
|
||||
RecentEntriesColumns& columns) {
|
||||
using namespace external_app::aprs_rx;
|
||||
|
||||
Color target_color;
|
||||
// auto entry_age = entry.age;
|
||||
|
||||
@@ -68,6 +74,10 @@ void RecentEntriesTable<APRSRecentEntries>::draw(
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
namespace ui::external_app::aprs_rx {
|
||||
|
||||
void APRSRxView::focus() {
|
||||
options_region.focus();
|
||||
}
|
||||
@@ -402,4 +412,4 @@ void APRSRXView::focus() {
|
||||
|
||||
APRSRXView::~APRSRXView() {
|
||||
}
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::aprs_rx
|
||||
Vendored
+3
-3
@@ -38,6 +38,8 @@
|
||||
#include "utility.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
namespace ui::external_app::aprs_rx {
|
||||
|
||||
class APRSLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
@@ -50,8 +52,6 @@ class APRSLogger {
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
|
||||
struct APRSRecentEntry {
|
||||
using Key = uint64_t;
|
||||
|
||||
@@ -290,6 +290,6 @@ class APRSRXView : public View {
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::aprs_rx
|
||||
|
||||
#endif /*__UI_APRS_RX_H__*/
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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_aprs_tx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::aprs_tx {
|
||||
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<APRSTXView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::aprs_tx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_aprs_tx.application_information"), used)) application_information_t _application_information_aprs_tx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::aprs_tx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "APRS TX",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x4C,
|
||||
0x32,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x25,
|
||||
0xA4,
|
||||
0x25,
|
||||
0xA4,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0x25,
|
||||
0xA4,
|
||||
0x25,
|
||||
0xA4,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x4C,
|
||||
0x32,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_afsk */ {'P', 'A', 'F', 'T'},
|
||||
/*.m4_app_offset = */ 0x00000000,
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
Vendored
+2
-2
@@ -37,7 +37,7 @@
|
||||
using namespace aprs;
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::aprs_tx {
|
||||
|
||||
void APRSTXView::focus() {
|
||||
tx_view.focus();
|
||||
@@ -242,4 +242,4 @@ APRSTXView::APRSTXView(NavigationView& nav) {
|
||||
// process_coordinates(last_lat, last_lon); //don't load last, so won't confuse users
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::aprs_tx
|
||||
Vendored
+2
-2
@@ -33,7 +33,7 @@
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::aprs_tx {
|
||||
|
||||
class APRSTXView : public View {
|
||||
public:
|
||||
@@ -161,4 +161,4 @@ class APRSTXView : public View {
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::aprs_tx
|
||||
+198
-24
@@ -143,30 +143,6 @@ static void push_bits(uint8_t* buf, int& pos, uint64_t v, int n) {
|
||||
set_bit(buf, pos++, (v >> i) & 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a beacon to hex string representation
|
||||
* @param frame the frame to convert
|
||||
* @param start true for the first half of the frame, false for the second half
|
||||
* @return the hex string representation of the specieid half of the frame
|
||||
*/
|
||||
std::string beacon_to_hex_string(const uint8_t* frame, bool start) {
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
|
||||
std::string out;
|
||||
out.resize(18);
|
||||
|
||||
int offset = start ? 0 : 9;
|
||||
|
||||
for (int i = 0; i < 9; i++) {
|
||||
uint8_t b = frame[offset + i];
|
||||
|
||||
out[i * 2] = hex[b >> 4];
|
||||
out[i * 2 + 1] = hex[b & 0x0F];
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a beacon in the provided buffer
|
||||
* @param frame the buffer to generate the frame in
|
||||
@@ -351,6 +327,204 @@ size_t generate_beacon(uint8_t* frame, const BeaconParams& params) {
|
||||
return 18;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a beacon hex string representation (generic range)
|
||||
* @param frame the frame data
|
||||
* @param offset_bytes starting byte offset
|
||||
* @param count_bytes number of bytes to convert
|
||||
* @return the hex string representation
|
||||
*/
|
||||
std::string beacon_to_hex_string_range(const uint8_t* frame, int offset_bytes, int count_bytes) {
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
std::string out;
|
||||
out.resize(count_bytes * 2);
|
||||
for (int i = 0; i < count_bytes; i++) {
|
||||
uint8_t b = frame[offset_bytes + i];
|
||||
out[i * 2] = hex[b >> 4];
|
||||
out[i * 2 + 1] = hex[b & 0x0F];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SGB (Second Generation Beacon) — T.018 Rev 7, March 2021
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Reference values from T.018 canonical test vector (Appendix B.1)
|
||||
#define SGB_TAC_NUMBER 230 // TAC number (16 bits, 0–65535)
|
||||
#define SGB_SERIAL_NUMBER 573 // Serial number within TAC (14 bits, 0–16383)
|
||||
#define SGB_HOMING_DEVICE 1 // 1 = beacon has 121.5 / 243 MHz homing device
|
||||
#define SGB_RLS_FUNCTION 0 // 0 = no Return Link Service function
|
||||
|
||||
// BCH(250,202) generator polynomial — lower 48 bits (without leading X^48 term)
|
||||
// Full g(x): X^48+X^47+X^46+X^42+X^41+X^40+X^39+X^38+X^37+X^35+X^33+X^32+X^31
|
||||
// +X^26+X^24+X^23+X^22+X^20+X^19+X^18+X^17+X^16+X^13+X^12+X^11+X^10
|
||||
// +X^7+X^4+X^2+X+1
|
||||
// Binary MSB-first: 1110001111110101110000101110111110011110010010111 (T.018 App. B.1)
|
||||
#define SGB_BCH_G_LOWER UINT64_C(0xC7EB85DF3C97)
|
||||
|
||||
/**
|
||||
* Encode latitude for SGB GNSS location protocol (T.018 Appendix C)
|
||||
* Pushes 23 bits: 1-bit N/S flag + 7-bit degrees + 15-bit decimal fraction
|
||||
* @param frame the frame buffer
|
||||
* @param pos current bit position (modified in-place)
|
||||
* @param south true if southern hemisphere
|
||||
* @param lat_mag absolute latitude in decimal degrees (0..90)
|
||||
*/
|
||||
static void push_sgb_latitude(uint8_t* frame, int& pos, bool south, float lat_mag) {
|
||||
push_bits(frame, pos, south ? 1 : 0, 1);
|
||||
uint32_t deg = (uint32_t)lat_mag;
|
||||
if (deg > 90) deg = 90;
|
||||
float frac = lat_mag - (float)deg;
|
||||
uint32_t frac_n = (uint32_t)(frac * 32768.0f + 0.5f);
|
||||
if (frac_n >= 32768) {
|
||||
frac_n = 0;
|
||||
if (deg < 90) deg++;
|
||||
}
|
||||
push_bits(frame, pos, deg, 7);
|
||||
push_bits(frame, pos, frac_n, 15);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode longitude for SGB GNSS location protocol (T.018 Appendix C)
|
||||
* Pushes 24 bits: 1-bit E/W flag + 8-bit degrees + 15-bit decimal fraction
|
||||
* @param frame the frame buffer
|
||||
* @param pos current bit position (modified in-place)
|
||||
* @param west true if western hemisphere
|
||||
* @param lon_mag absolute longitude in decimal degrees (0..180)
|
||||
*/
|
||||
static void push_sgb_longitude(uint8_t* frame, int& pos, bool west, float lon_mag) {
|
||||
push_bits(frame, pos, west ? 1 : 0, 1);
|
||||
uint32_t deg = (uint32_t)lon_mag;
|
||||
if (deg > 180) deg = 180;
|
||||
float frac = lon_mag - (float)deg;
|
||||
uint32_t frac_n = (uint32_t)(frac * 32768.0f + 0.5f);
|
||||
if (frac_n >= 32768) {
|
||||
frac_n = 0;
|
||||
if (deg < 180) deg++;
|
||||
}
|
||||
push_bits(frame, pos, deg, 8);
|
||||
push_bits(frame, pos, frac_n, 15);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute BCH(250,202) parity using LFSR method (T.018 Appendix B.1)
|
||||
* Reads 202 message bits from buffer positions 6..207 and writes
|
||||
* 48 BCH parity bits to buffer positions 208..255.
|
||||
* @param frame the 32-byte SGB buffer (message already written at bits 6..207)
|
||||
*/
|
||||
static void compute_sgb_bch(uint8_t* frame) {
|
||||
uint64_t reg = 0;
|
||||
for (int i = 0; i < 202; i++) {
|
||||
int bp = i + 6; // buffer bit position (6 = start of message after padding)
|
||||
bool msg_bit = (frame[bp >> 3] >> (7 - (bp & 7))) & 1;
|
||||
bool feedback = ((reg >> 47) & 1) ^ (msg_bit ? 1u : 0u);
|
||||
reg = (reg << 1) & UINT64_C(0xFFFFFFFFFFFF);
|
||||
if (feedback) reg ^= SGB_BCH_G_LOWER;
|
||||
}
|
||||
// Append 48-bit remainder at buffer positions 208..255
|
||||
int pos = 208;
|
||||
push_bits(frame, pos, reg, 48);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map BeaconType to SGB 3-bit beacon type code (T.018 Table 3.1, bits 138-140)
|
||||
* 000=ELT, 001=EPIRB, 010=PLB
|
||||
*/
|
||||
static uint8_t sgb_beacon_type_code(BeaconType t) {
|
||||
switch (t) {
|
||||
case BeaconType::EPIRB:
|
||||
return 0b001;
|
||||
case BeaconType::PLB:
|
||||
return 0b010;
|
||||
default:
|
||||
case BeaconType::ELT:
|
||||
return 0b000;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a Second Generation Beacon (SGB / T.018) frame.
|
||||
* The 250-bit frame is stored in 32 bytes with 6 leading padding bits
|
||||
* (bits 0..4 = 0, bit 5 = 1).
|
||||
* The rotating field is Type 0 — G.008 Objective Requirements (Table 3.3),
|
||||
* with elapsed_hours and time_last_loc_min updated from elapsed_s.
|
||||
* @param frame 32-byte output buffer
|
||||
* @param params beacon parameters
|
||||
* @param elapsed_s seconds elapsed since beacon activation (drives rotating field)
|
||||
* @return 32 (size of the generated frame in bytes)
|
||||
*/
|
||||
size_t generate_sgb_beacon(uint8_t* frame, const BeaconParams& params, uint32_t elapsed_s) {
|
||||
memset(frame, 0, 32);
|
||||
|
||||
// 6 padding bits at start of 32-byte buffer: [0,0,0,0,1,0] (5th bit (1 based index as per specification) = 1 to indicate self-test mode)
|
||||
set_bit(frame, 4, 1);
|
||||
|
||||
// Message bits start at buffer bit position 6 (= SGB message bit 1)
|
||||
int pos = 6;
|
||||
|
||||
// Bits 1-16: TAC number (16 bits) — T.018 Table 3.1
|
||||
push_bits(frame, pos, SGB_TAC_NUMBER, 16);
|
||||
// Bits 17-30: Serial number within TAC (14 bits)
|
||||
push_bits(frame, pos, SGB_SERIAL_NUMBER, 14);
|
||||
// Bits 31-40: Country code (10 bits, ITU MID)
|
||||
push_bits(frame, pos, params.country & 0x3FFU, 10);
|
||||
// Bit 41: Status of homing device (1 = has 121.5/243 MHz homing device)
|
||||
push_bits(frame, pos, params.has_121_5 ? 1 : 0, 1);
|
||||
// Bit 42: RLS function flag (0 = no RLS)
|
||||
push_bits(frame, pos, SGB_RLS_FUNCTION, 1);
|
||||
// Bit 43: Test protocol flag
|
||||
push_bits(frame, pos, params.is_test ? 1 : 0, 1);
|
||||
|
||||
// Bits 44-66: Encoded GNSS latitude (23 bits) — T.018 Appendix C
|
||||
float lat_mag = params.location.latitude < 0.0f ? -params.location.latitude : params.location.latitude;
|
||||
push_sgb_latitude(frame, pos, params.location.south, lat_mag);
|
||||
// Bits 67-90: Encoded GNSS longitude (24 bits)
|
||||
float lon_mag = params.location.longitude < 0.0f ? -params.location.longitude : params.location.longitude;
|
||||
push_sgb_longitude(frame, pos, params.location.west, lon_mag);
|
||||
|
||||
// Bits 91-137: Vessel ID (47 bits = 3-bit type selector + 44-bit body)
|
||||
// Type 000 = No aircraft/maritime identity; body all zeros
|
||||
push_bits(frame, pos, 0, 47);
|
||||
|
||||
// Bits 138-140: Beacon type (3 bits)
|
||||
push_bits(frame, pos, sgb_beacon_type_code(params.type), 3);
|
||||
|
||||
// Bits 141-154: Spare (14 bits, all 1s in normal operation)
|
||||
push_bits(frame, pos, 0x3FFFU, 14);
|
||||
|
||||
// Bits 155-202: Rotating Field Type 0 — G.008 Objective Requirements (Table 3.3)
|
||||
// Bits 155-158: Identifier (4 bits = 0000)
|
||||
push_bits(frame, pos, 0b0000U, 4);
|
||||
// Bits 159-164: Elapsed time since activation (6 bits, 0-63 h, truncated at 63)
|
||||
uint32_t elapsed_h = elapsed_s / 3600;
|
||||
if (elapsed_h > 63) elapsed_h = 63;
|
||||
push_bits(frame, pos, elapsed_h, 6);
|
||||
// Bits 165-175: Time from last encoded location (11 bits, 0-2046 min; 2047 = no fix)
|
||||
uint32_t loc_age_min = elapsed_s / 60;
|
||||
if (loc_age_min > 2046) loc_age_min = 2046;
|
||||
push_bits(frame, pos, loc_age_min, 11);
|
||||
// Bits 176-185: Altitude of encoded location (10 bits; 0x3FF = no altitude)
|
||||
push_bits(frame, pos, 0x3FFU, 10);
|
||||
// Bits 186-193: Dilution of Precision (4-bit HDOP + 4-bit VDOP; 0 = best HDOP, 0 = best VDOP)
|
||||
push_bits(frame, pos, 0b00000000U, 8);
|
||||
// Bits 194-195: Activation notification (00 = manual by user)
|
||||
push_bits(frame, pos, 0b00U, 2);
|
||||
// Bits 196-198: Remaining battery capacity (101 = >75%)
|
||||
push_bits(frame, pos, 0b101U, 3);
|
||||
// Bits 199-200: GNSS status (10 = 3D fix)
|
||||
push_bits(frame, pos, 0b10U, 2);
|
||||
// Bits 201-202: Spare (00)
|
||||
push_bits(frame, pos, 0b00U, 2);
|
||||
|
||||
// pos == 208: 6 padding + 202 message bits consumed
|
||||
|
||||
// Bits 203-250: BCH(250,202) parity (48 bits) computed and written at positions 208-255
|
||||
compute_sgb_bch(frame);
|
||||
|
||||
return 32;
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::epirb_tx
|
||||
|
||||
#endif /*__BEACON_H__*/
|
||||
+63
-14
@@ -67,12 +67,17 @@ static uint8_t hexToByte(char high, char low) {
|
||||
return (hexval(high) << 4) | hexval(low);
|
||||
}
|
||||
|
||||
std::string EPIRBTXAppView::frame_to_hex_string(bool start) {
|
||||
return beacon_to_hex_string(epirb_tx_message.data, start);
|
||||
std::string EPIRBTXAppView::frame_to_hex_string_range(int offset_bytes, int count_bytes) {
|
||||
return beacon_to_hex_string_range(epirb_tx_message.data, offset_bytes, count_bytes);
|
||||
}
|
||||
|
||||
void EPIRBTXAppView::generate_frame(BeaconParams params) {
|
||||
epirb_tx_message.data_len = generate_beacon(epirb_tx_message.data, params);
|
||||
if (format_sgb) {
|
||||
uint32_t elapsed_s = sgb_start_time > 0 ? (chTimeNow() - sgb_start_time) / 1000 : 0;
|
||||
epirb_tx_message.data_len = generate_sgb_beacon(epirb_tx_message.data, params, elapsed_s);
|
||||
} else {
|
||||
epirb_tx_message.data_len = generate_beacon(epirb_tx_message.data, params);
|
||||
}
|
||||
}
|
||||
|
||||
void EPIRBTXAppView::on_timer() {
|
||||
@@ -125,7 +130,7 @@ void EPIRBTXAppView::update_am_transmission() {
|
||||
if (am_enabled && transmitting && !transmitting_bpsk) {
|
||||
// Start am transmission
|
||||
// Restore am frequency
|
||||
epirb_tx_message.mode_bpsk = false;
|
||||
epirb_tx_message.mode_406 = false;
|
||||
transmitter_model.set_target_frequency(am_frequency);
|
||||
// Send config to baseband
|
||||
baseband::set_epirb_tx_config(epirb_tx_message);
|
||||
@@ -146,10 +151,18 @@ void EPIRBTXAppView::update_frame(bool updateConfig) {
|
||||
file_mode_ui->text_description.set(beacon.description.substr(0, max_text_width_ext));
|
||||
file_mode_ui->text_description_end.set(beacon.description.size() > max_text_width_ext ? "-" + beacon.description.substr(max_text_width_ext, max_text_width_ext + max_text_width_ext - 1) : "");
|
||||
// Udapte frame content on display
|
||||
text_frame.set(beacon.frame.substr(0, 18));
|
||||
text_frame_end.set(beacon.frame.size() > 18 ? beacon.frame.substr(18, 36) : "");
|
||||
bool is_fgb = beacon.frame.size() <= BEACON_HEXA_SIZE_FGB;
|
||||
if (is_fgb) {
|
||||
text_frame.set(beacon.frame.substr(0, BEACON_HEXA_SPLIT_FGB));
|
||||
text_frame_end.set(beacon.frame.size() > BEACON_HEXA_SPLIT_FGB ? beacon.frame.substr(BEACON_HEXA_SPLIT_FGB, BEACON_HEXA_SPLIT_FGB) : "");
|
||||
text_frame_sgb_end.set("");
|
||||
} else {
|
||||
text_frame.set(" " + beacon.frame.substr(1, BEACON_HEXA_SPLIT_SGB - 1));
|
||||
text_frame_end.set(beacon.frame.size() > BEACON_HEXA_SPLIT_SGB ? beacon.frame.substr(BEACON_HEXA_SPLIT_SGB, BEACON_HEXA_SPLIT_SGB) : "");
|
||||
text_frame_sgb_end.set(beacon.frame.size() > 2 * BEACON_HEXA_SPLIT_SGB ? beacon.frame.substr(2 * BEACON_HEXA_SPLIT_SGB, BEACON_HEXA_SPLIT_SGB) : "");
|
||||
}
|
||||
// Prepare tx configuration
|
||||
epirb_tx_message.data_len = std::min<size_t>((beacon.frame.size() / 2), 18);
|
||||
epirb_tx_message.data_len = std::min<size_t>((beacon.frame.size() / 2), BEACON_SIZE_SGB);
|
||||
for (uint8_t i = 0; i < epirb_tx_message.data_len; i++) {
|
||||
epirb_tx_message.data[i] = hexToByte(
|
||||
beacon.frame[2 * i],
|
||||
@@ -159,8 +172,17 @@ void EPIRBTXAppView::update_frame(bool updateConfig) {
|
||||
// In manual mode, generate frame content for current beacon params
|
||||
generate_frame(beacon_params);
|
||||
// Update frame content on display
|
||||
text_frame.set(frame_to_hex_string(true));
|
||||
text_frame_end.set(frame_to_hex_string(false));
|
||||
if (format_sgb) {
|
||||
// SGB: 32 bytes across 3 lines (BEACON_HEXA_SPLIT_SGB = 22 hex chars = 11 bytes)
|
||||
// Remove first nibble and replace it with a space to match COSPAS hexadecimal representation specification
|
||||
text_frame.set(" " + frame_to_hex_string_range(0, BEACON_HEXA_SPLIT_SGB / 2).substr(1));
|
||||
text_frame_end.set(frame_to_hex_string_range(BEACON_HEXA_SPLIT_SGB / 2, BEACON_HEXA_SPLIT_SGB / 2));
|
||||
text_frame_sgb_end.set(frame_to_hex_string_range(BEACON_HEXA_SPLIT_SGB, (BEACON_HEXA_SPLIT_SGB / 2) - 1));
|
||||
} else {
|
||||
text_frame.set(frame_to_hex_string_range(0, BEACON_HEXA_SPLIT_FGB / 2));
|
||||
text_frame_end.set(frame_to_hex_string_range(BEACON_HEXA_SPLIT_FGB / 2, BEACON_HEXA_SPLIT_FGB / 2));
|
||||
text_frame_sgb_end.set("");
|
||||
}
|
||||
}
|
||||
if (updateConfig && send_on_change && loop) {
|
||||
// Need to update config / send new beacon
|
||||
@@ -176,14 +198,14 @@ void EPIRBTXAppView::update_frame(bool updateConfig) {
|
||||
}
|
||||
|
||||
void EPIRBTXAppView::update_config() {
|
||||
if (!epirb_tx_message.mode_bpsk) {
|
||||
if (!epirb_tx_message.mode_406) {
|
||||
// Previously in AM mode => restore bpsk frequency
|
||||
transmitter_model.set_target_frequency(bpsk_frequency);
|
||||
// Update displayed frequency
|
||||
tx_view.on_show();
|
||||
}
|
||||
// Set mode to bpsk
|
||||
epirb_tx_message.mode_bpsk = true;
|
||||
epirb_tx_message.mode_406 = true;
|
||||
transmitting_bpsk = true;
|
||||
// Set pre/post count
|
||||
epirb_tx_message.pre_count = (160 * TONES_SAMPLERATE) / 1000; // 160 ms carrier (COSPAS spec.)
|
||||
@@ -198,6 +220,13 @@ void EPIRBTXAppView::set_tx_button_state(bool active) {
|
||||
}
|
||||
|
||||
void EPIRBTXAppView::start_tx() {
|
||||
if (format_sgb && !mode_file) {
|
||||
// Track start time for the SGB rotating field elapsed-time counter
|
||||
if (!transmitting) sgb_start_time = chTimeNow();
|
||||
// update_frame regenerates the SGB frame with current elapsed time and refreshes display
|
||||
update_frame(false);
|
||||
set_dirty();
|
||||
}
|
||||
last_frame_time = chTimeNow();
|
||||
update_config();
|
||||
loop = loop_enabled;
|
||||
@@ -221,7 +250,7 @@ void EPIRBTXAppView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
transmitting_bpsk = false;
|
||||
if (am_enabled) {
|
||||
// BPSK frame sent, switch back to 121.5 AM signal
|
||||
epirb_tx_message.mode_bpsk = false;
|
||||
epirb_tx_message.mode_406 = false;
|
||||
// Start am transmission
|
||||
update_am_transmission();
|
||||
} else {
|
||||
@@ -310,6 +339,7 @@ void EPIRBTXAppView::update_mode() {
|
||||
options_beacon_type.hidden(mode_file);
|
||||
options_beacon_protocol.hidden(mode_file);
|
||||
options_beacon_country.hidden(mode_file);
|
||||
options_format.hidden(mode_file);
|
||||
text_field_beacon_locator.hidden(mode_file);
|
||||
}
|
||||
|
||||
@@ -323,6 +353,7 @@ EPIRBTXAppView::EPIRBTXAppView(
|
||||
&text_beacon_type,
|
||||
&options_beacon_type,
|
||||
&options_beacon_protocol,
|
||||
&options_format,
|
||||
&text_beacon_country,
|
||||
&options_beacon_country,
|
||||
&checkbox_beacon_internal,
|
||||
@@ -335,6 +366,7 @@ EPIRBTXAppView::EPIRBTXAppView(
|
||||
&text_field_beacon_locator,
|
||||
&text_frame,
|
||||
&text_frame_end,
|
||||
&text_frame_sgb_end,
|
||||
&text_timeout,
|
||||
&checkbox_loop,
|
||||
&field_delay,
|
||||
@@ -376,6 +408,7 @@ EPIRBTXAppView::EPIRBTXAppView(
|
||||
init_from_locator(beacon_params.location);
|
||||
update_mode();
|
||||
update_location();
|
||||
options_format.set_by_value(format_sgb ? 1 : 0);
|
||||
|
||||
options_mode.on_change = [this](size_t, OptionsField::value_t value) {
|
||||
mode_file = (((BeaconMode)value) == BeaconMode::FILE);
|
||||
@@ -405,6 +438,13 @@ EPIRBTXAppView::EPIRBTXAppView(
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
options_format.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
format_sgb = (v == 1);
|
||||
sgb_start_time = 0; // reset elapsed counter when format changes
|
||||
update_frame();
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
options_am_channel.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
bool is_real = false;
|
||||
switch ((AmChannel)v) {
|
||||
@@ -458,6 +498,9 @@ EPIRBTXAppView::EPIRBTXAppView(
|
||||
case BpskChannel::O:
|
||||
bpsk_frequency = BPSK_FREQUENCY_O;
|
||||
break;
|
||||
case BpskChannel::SGB:
|
||||
bpsk_frequency = BPSK_FREQUENCY_SGB;
|
||||
break;
|
||||
default:
|
||||
v = (uint8_t)BpskChannel::HAM;
|
||||
// fallthrough
|
||||
@@ -590,6 +633,9 @@ EPIRBTXAppView::EPIRBTXAppView(
|
||||
case BPSK_FREQUENCY_O:
|
||||
bpsk_channel = (uint8_t)BpskChannel::O;
|
||||
break;
|
||||
case BPSK_FREQUENCY_SGB:
|
||||
bpsk_channel = (uint8_t)BpskChannel::SGB;
|
||||
break;
|
||||
default:
|
||||
bpsk_channel = (uint8_t)BpskChannel::MANUAL;
|
||||
manual_bpsk_frequency = bpsk_frequency;
|
||||
@@ -642,11 +688,14 @@ void EPIRBTXAppView::load_beacons() {
|
||||
Beacon beacon{};
|
||||
beacon.title = trim(cols[0]);
|
||||
beacon.description = trim(cols[1]);
|
||||
// Make sure frame is not longer tha 18 bytes / 36 hex character
|
||||
beacon.frame = trim(cols[2]).substr(0, 36);
|
||||
// 1G uses up to 36 hex chars, 2G uses 64 hex chars (250 bits rounded to 32 bytes).
|
||||
beacon.frame = trim(cols[2]).substr(0, BEACON_HEXA_SIZE_SGB);
|
||||
size_t size = beacon.frame.size();
|
||||
if (size <= 0)
|
||||
continue; // Invalid line.
|
||||
if ((size % 2) != 0) {
|
||||
beacon.frame = "0" + beacon.frame; // Pad with leading 0 to make it even length
|
||||
}
|
||||
// Beacon is valid, add it to the list
|
||||
beacons.emplace_back(std::move(beacon));
|
||||
}
|
||||
|
||||
+27
-6
@@ -33,9 +33,12 @@
|
||||
#include "message.hpp"
|
||||
#include "tonesets.hpp"
|
||||
|
||||
#define BEACON_HEXA_SIZE 36
|
||||
#define BEACON_HEXA_HALF_SIZE 18
|
||||
#define BEACON_SIZE 18
|
||||
#define BEACON_SIZE_FGB 18
|
||||
#define BEACON_SIZE_SGB 32
|
||||
#define BEACON_HEXA_SIZE_FGB BEACON_SIZE_FGB * 2
|
||||
#define BEACON_HEXA_SIZE_SGB BEACON_SIZE_SGB * 2
|
||||
#define BEACON_HEXA_SPLIT_FGB 18
|
||||
#define BEACON_HEXA_SPLIT_SGB 22
|
||||
|
||||
#define AM_TEST_FREQUENCY 121375000
|
||||
#define AM_REAL_FREQUENCY 121500000
|
||||
@@ -49,6 +52,7 @@
|
||||
#define BPSK_FREQUENCY_K 406052000
|
||||
#define BPSK_FREQUENCY_N 406061000
|
||||
#define BPSK_FREQUENCY_O 406064000
|
||||
#define BPSK_FREQUENCY_SGB 406050000
|
||||
|
||||
namespace ui::external_app::epirb_tx {
|
||||
|
||||
@@ -85,7 +89,8 @@ enum class BpskChannel {
|
||||
K = 6,
|
||||
N = 7,
|
||||
O = 8,
|
||||
MANUAL = 10
|
||||
MANUAL = 10,
|
||||
SGB = 100
|
||||
};
|
||||
|
||||
struct Location {
|
||||
@@ -129,7 +134,7 @@ class EPIRBTXAppView : public View {
|
||||
void on_timer();
|
||||
void load_beacons();
|
||||
void set_tx_button_state(bool active);
|
||||
std::string frame_to_hex_string(bool start);
|
||||
std::string frame_to_hex_string_range(int offset_bytes, int count_bytes);
|
||||
void generate_frame(BeaconParams params);
|
||||
void update_frame(bool updateConfig = true);
|
||||
void update_bpsk_frequency();
|
||||
@@ -214,6 +219,7 @@ class EPIRBTXAppView : public View {
|
||||
{"country"sv, &beacon_country},
|
||||
{"internal"sv, &beacon_internal},
|
||||
{"locator"sv, &locator},
|
||||
{"sgb"sv, &format_sgb},
|
||||
}};
|
||||
|
||||
// Time of the last sent frame
|
||||
@@ -224,6 +230,10 @@ class EPIRBTXAppView : public View {
|
||||
bool transmitting_bpsk{false};
|
||||
// True when currently looping on sending beacons
|
||||
bool loop{false};
|
||||
// True when SGB (Second Generation Beacon) format is selected
|
||||
bool format_sgb{false};
|
||||
// System time (ms) when the current SGB transmission session started
|
||||
uint32_t sgb_start_time{0};
|
||||
|
||||
// Current EPIRBTXDataMessage for baseband
|
||||
EPIRBTXDataMessage epirb_tx_message{};
|
||||
@@ -302,10 +312,17 @@ class EPIRBTXAppView : public View {
|
||||
{"PLB", (uint8_t)BeaconType::PLB}}};
|
||||
OptionsField options_beacon_protocol{
|
||||
{UI_POS_X(9 + 7), UI_POS_Y(1)},
|
||||
30,
|
||||
8,
|
||||
{{"User", (uint8_t)BeaconProtocol::USER},
|
||||
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
|
||||
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
|
||||
// Format selector (FGB / SGB) — manual mode only, same row as type/protocol
|
||||
OptionsField options_format{
|
||||
{UI_POS_X_RIGHT(4), UI_POS_Y(1)},
|
||||
4,
|
||||
{{"FGB", 0},
|
||||
{"SGB", 1}}};
|
||||
|
||||
OptionsField options_beacon_country{
|
||||
{UI_POS_X(9), UI_POS_Y(2)},
|
||||
7,
|
||||
@@ -337,6 +354,9 @@ class EPIRBTXAppView : public View {
|
||||
Text text_frame_end{
|
||||
{UI_POS_X(6), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
Text text_frame_sgb_end{
|
||||
{UI_POS_X(6), UI_POS_Y(8), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||
""};
|
||||
|
||||
Text text_timeout{
|
||||
{UI_POS_X(14), UI_POS_Y(10), UI_POS_WIDTH(2), UI_POS_DEFAULT_HEIGHT},
|
||||
@@ -389,6 +409,7 @@ class EPIRBTXAppView : public View {
|
||||
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
|
||||
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
|
||||
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
|
||||
{"406.050 MHz (SGB)", (uint8_t)BpskChannel::SGB},
|
||||
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
|
||||
|
||||
// Transmitter view
|
||||
|
||||
+38
-10
@@ -169,6 +169,7 @@ set(EXTCPPSRC
|
||||
#mcu_temperature 112
|
||||
external/mcu_temperature/main.cpp
|
||||
external/mcu_temperature/mcu_temperature.cpp
|
||||
external/mcu_temperature/temperature_logger.cpp
|
||||
|
||||
#fmradio 640
|
||||
external/fmradio/main.cpp
|
||||
@@ -379,6 +380,37 @@ set(EXTCPPSRC
|
||||
#signal_hunter
|
||||
external/signal_hunter/main.cpp
|
||||
external/signal_hunter/ui_signal_hunter.cpp
|
||||
|
||||
|
||||
#tetra rx
|
||||
external/tetra_rx/main.cpp
|
||||
external/tetra_rx/ui_tetra_rx.cpp
|
||||
external/tetra_rx/tetra_crc.cpp
|
||||
external/tetra_rx/tetra_descrambler.cpp
|
||||
external/tetra_rx/tetra_interleave.cpp
|
||||
external/tetra_rx/tetra_rcpc.cpp
|
||||
external/tetra_rx/tetra_viterbi.cpp
|
||||
|
||||
#adsb rx
|
||||
external/adsbrx/main.cpp
|
||||
external/adsbrx/ui_adsb_rx.cpp
|
||||
|
||||
#ais rx
|
||||
external/ais_rx/main.cpp
|
||||
external/ais_rx/ais_app.cpp
|
||||
|
||||
#aprs rx
|
||||
external/aprs_rx/main.cpp
|
||||
external/aprs_rx/ui_aprs_rx.cpp
|
||||
|
||||
#aprs tx
|
||||
external/aprs_tx/main.cpp
|
||||
external/aprs_tx/ui_aprs_tx.cpp
|
||||
|
||||
#sd over usb
|
||||
external/sdusb/main.cpp
|
||||
external/sdusb/ui_sd_over_usb.cpp
|
||||
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -472,14 +504,10 @@ set(EXTAPPLIST
|
||||
hard_reset
|
||||
secplustx
|
||||
signal_hunter
|
||||
tetra_rx
|
||||
adsbrx
|
||||
ais_rx
|
||||
aprs_rx
|
||||
aprs_tx
|
||||
sdusb
|
||||
)
|
||||
|
||||
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
||||
if(NOT BOARD STREQUAL "PRALINE")
|
||||
list(APPEND EXTCPPSRC
|
||||
external/sdusb/main.cpp
|
||||
external/sdusb/ui_sd_over_usb.cpp
|
||||
)
|
||||
list(APPEND EXTAPPLIST sdusb)
|
||||
endif()
|
||||
|
||||
|
||||
+129
-91
@@ -114,6 +114,11 @@ MEMORY
|
||||
ram_external_app_vor_rx (rwx) : org = 0xAE090000, len = 32k
|
||||
ram_external_app_vor_tx (rwx) : org = 0xAE0A0000, len = 32k
|
||||
ram_external_app_signal_hunter (rwx) : org = 0xAE0B0000, len = 32k
|
||||
ram_external_app_tetra_rx (rwx) : org = 0xAE0C0000, len = 32k
|
||||
ram_external_app_adsbrx (rwx) : org = 0xAE0D0000, len = 32k
|
||||
ram_external_app_ais_rx (rwx) : org = 0xAE0E0000, len = 32k
|
||||
ram_external_app_aprs_rx (rwx) : org = 0xAE0F0000, len = 32k
|
||||
ram_external_app_aprs_tx (rwx) : org = 0xAE100000, len = 32k
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -121,546 +126,579 @@ SECTIONS
|
||||
.external_app_afsk_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_afsk_rx.application_information));
|
||||
*(*ui*external_app*afsk_rx*);
|
||||
*/external/afsk_rx/*(*ui*external_app*afsk_rx*);
|
||||
} > ram_external_app_afsk_rx
|
||||
|
||||
.external_app_calculator : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_calculator.application_information));
|
||||
*(*ui*external_app*calculator*);
|
||||
*/external/calculator/*(*ui*external_app*calculator*);
|
||||
} > ram_external_app_calculator
|
||||
|
||||
.external_app_font_viewer : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_font_viewer.application_information));
|
||||
*(*ui*external_app*font_viewer*);
|
||||
*/external/font_viewer/*(*ui*external_app*font_viewer*);
|
||||
} > ram_external_app_font_viewer
|
||||
|
||||
.external_app_blespam : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_blespam.application_information));
|
||||
*(*ui*external_app*blespam*);
|
||||
*/external/blespam/*(*ui*external_app*blespam*);
|
||||
} > ram_external_app_blespam
|
||||
|
||||
.external_app_analogtv : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_analogtv.application_information));
|
||||
*(*ui*external_app*analogtv*);
|
||||
*/external/analogtv/*(*ui*external_app*analogtv*);
|
||||
} > ram_external_app_analogtv
|
||||
|
||||
.external_app_nrf_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_nrf_rx.application_information));
|
||||
*(*ui*external_app*nrf_rx*);
|
||||
*/external/nrf_rx/*(*ui*external_app*nrf_rx*);
|
||||
} > ram_external_app_nrf_rx
|
||||
|
||||
.external_app_coasterp : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_coasterp.application_information));
|
||||
*(*ui*external_app*coasterp*);
|
||||
*/external/coasterp/*(*ui*external_app*coasterp*);
|
||||
} > ram_external_app_coasterp
|
||||
|
||||
.external_app_lge : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_lge.application_information));
|
||||
*(*ui*external_app*lge*);
|
||||
*/external/lge/*(*ui*external_app*lge*);
|
||||
} > ram_external_app_lge
|
||||
|
||||
.external_app_lcr : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_lcr.application_information));
|
||||
*(*ui*external_app*lcr*);
|
||||
*/external/lcr/*(*ui*external_app*lcr*);
|
||||
} > ram_external_app_lcr
|
||||
|
||||
.external_app_jammer : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_jammer.application_information));
|
||||
*(*ui*external_app*jammer*);
|
||||
*/external/jammer/*(*ui*external_app*jammer*);
|
||||
} > ram_external_app_jammer
|
||||
|
||||
.external_app_gpssim : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_gpssim.application_information));
|
||||
*(*ui*external_app*gpssim*);
|
||||
*/external/gpssim/*(*ui*external_app*gpssim*);
|
||||
} > ram_external_app_gpssim
|
||||
|
||||
.external_app_spainter : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_spainter.application_information));
|
||||
*(*ui*external_app*spainter*);
|
||||
*/external/spainter/*(*ui*external_app*spainter*);
|
||||
} > ram_external_app_spainter
|
||||
|
||||
.external_app_keyfob : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_keyfob.application_information));
|
||||
*(*ui*external_app*keyfob*);
|
||||
*/external/keyfob/*(*ui*external_app*keyfob*);
|
||||
} > ram_external_app_keyfob
|
||||
|
||||
.external_app_tetris : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_tetris.application_information));
|
||||
*(*ui*external_app*tetris*);
|
||||
*/external/tetris/*(*ui*external_app*tetris*);
|
||||
} > ram_external_app_tetris
|
||||
|
||||
.external_app_extsensors : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_extsensors.application_information));
|
||||
*(*ui*external_app*extsensors*);
|
||||
*/external/extsensors/*(*ui*external_app*extsensors*);
|
||||
} > ram_external_app_extsensors
|
||||
|
||||
.external_app_foxhunt_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_foxhunt_rx.application_information));
|
||||
*(*ui*external_app*foxhunt_rx*);
|
||||
*/external/foxhunt/*(*ui*external_app*foxhunt_rx*);
|
||||
} > ram_external_app_foxhunt_rx
|
||||
|
||||
.external_app_audio_test : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_audio_test.application_information));
|
||||
*(*ui*external_app*audio_test*);
|
||||
*/external/audio_test/*(*ui*external_app*audio_test*);
|
||||
} > ram_external_app_audio_test
|
||||
|
||||
.external_app_wardrivemap : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_wardrivemap.application_information));
|
||||
*(*ui*external_app*wardrivemap*);
|
||||
*/external/wardrivemap/*(*ui*external_app*wardrivemap*);
|
||||
} > ram_external_app_wardrivemap
|
||||
|
||||
.external_app_tpmsrx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_tpmsrx.application_information));
|
||||
*(*ui*external_app*tpmsrx*);
|
||||
*/external/tpmsrx/*(*ui*external_app*tpmsrx*);
|
||||
} > ram_external_app_tpmsrx
|
||||
|
||||
.external_app_tpmstx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_tpmstx.application_information));
|
||||
*(*ui*external_app*tpmstx*);
|
||||
*/external/tpmstx/*(*ui*external_app*tpmstx*);
|
||||
} > ram_external_app_tpmstx
|
||||
|
||||
.external_app_protoview : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_protoview.application_information));
|
||||
*(*ui*external_app*protoview*);
|
||||
*/external/protoview/*(*ui*external_app*protoview*);
|
||||
} > ram_external_app_protoview
|
||||
|
||||
.external_app_adsbtx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_adsbtx.application_information));
|
||||
*(*ui*external_app*adsbtx*);
|
||||
*/external/adsbtx/*(*ui*external_app*adsbtx*);
|
||||
} > ram_external_app_adsbtx
|
||||
|
||||
.external_app_morse_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_morse_tx.application_information));
|
||||
*(*ui*external_app*morse_tx*);
|
||||
*/external/morse_tx/*(*ui*external_app*morse_tx*);
|
||||
} > ram_external_app_morse_tx
|
||||
|
||||
.external_app_sstvtx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_sstvtx.application_information));
|
||||
*(*ui*external_app*sstvtx*);
|
||||
*/external/sstvtx/*(*ui*external_app*sstvtx*);
|
||||
} > ram_external_app_sstvtx
|
||||
|
||||
.external_app_random_password : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_random_password.application_information));
|
||||
*(*ui*external_app*random_password*);
|
||||
*/external/random_password/*(*ui*external_app*random_password*);
|
||||
} > ram_external_app_random_password
|
||||
|
||||
.external_app_acars_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_acars_rx.application_information));
|
||||
*(*ui*external_app*acars_rx*);
|
||||
*/external/acars_rx/*(*ui*external_app*acars_rx*);
|
||||
} > ram_external_app_acars_rx
|
||||
|
||||
.external_app_shoppingcart_lock : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_shoppingcart_lock.application_information));
|
||||
*(*ui*external_app*shoppingcart_lock*);
|
||||
*/external/shoppingcart_lock/*(*ui*external_app*shoppingcart_lock*);
|
||||
} > ram_external_app_shoppingcart_lock
|
||||
|
||||
.external_app_cvs_spam : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_cvs_spam.application_information));
|
||||
*(*ui*external_app*cvs_spam*);
|
||||
*/external/cvs_spam/*(*ui*external_app*cvs_spam*);
|
||||
} > ram_external_app_cvs_spam
|
||||
|
||||
.external_app_ookbrute : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_ookbrute.application_information));
|
||||
*(*ui*external_app*ookbrute*);
|
||||
*/external/ookbrute/*(*ui*external_app*ookbrute*);
|
||||
} > ram_external_app_ookbrute
|
||||
|
||||
.external_app_ook_editor : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_ook_editor.application_information));
|
||||
*(*ui*external_app*ook_editor*);
|
||||
*/external/ook_editor/*(*ui*external_app*ook_editor*);
|
||||
} > ram_external_app_ook_editor
|
||||
|
||||
.external_app_flippertx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_flippertx.application_information));
|
||||
*(*ui*external_app*flippertx*);
|
||||
*/external/flippertx/*(*ui*external_app*flippertx*);
|
||||
} > ram_external_app_flippertx
|
||||
|
||||
.external_app_remote : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_remote.application_information));
|
||||
*(*ui*external_app*remote*);
|
||||
*/external/remote/*(*ui*external_app*remote*);
|
||||
} > ram_external_app_remote
|
||||
|
||||
.external_app_mcu_temperature : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_mcu_temperature.application_information));
|
||||
*(*ui*external_app*mcu_temperature*);
|
||||
*/external/mcu_temperature/*(*ui*external_app*mcu_temperature*);
|
||||
} > ram_external_app_mcu_temperature
|
||||
|
||||
.external_app_fmradio : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_fmradio.application_information));
|
||||
*(*ui*external_app*fmradio*);
|
||||
*/external/fmradio/*(*ui*external_app*fmradio*);
|
||||
} > ram_external_app_fmradio
|
||||
|
||||
.external_app_tuner : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_tuner.application_information));
|
||||
*(*ui*external_app*tuner*);
|
||||
*/external/tuner/*(*ui*external_app*tuner*);
|
||||
} > ram_external_app_tuner
|
||||
|
||||
.external_app_metronome : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_metronome.application_information));
|
||||
*(*ui*external_app*metronome*);
|
||||
*/external/metronome/*(*ui*external_app*metronome*);
|
||||
} > ram_external_app_metronome
|
||||
|
||||
.external_app_app_manager : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_app_manager.application_information));
|
||||
*(*ui*external_app*app_manager*);
|
||||
*/external/app_manager/*(*ui*external_app*app_manager*);
|
||||
} > ram_external_app_app_manager
|
||||
|
||||
.external_app_hopper : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_hopper.application_information));
|
||||
*(*ui*external_app*hopper*);
|
||||
*/external/hopper/*(*ui*external_app*hopper*);
|
||||
} > ram_external_app_hopper
|
||||
|
||||
.external_app_antenna_length : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_antenna_length.application_information));
|
||||
*(*ui*external_app*antenna_length*);
|
||||
*/external/antenna_length/*(*ui*external_app*antenna_length*);
|
||||
} > ram_external_app_antenna_length
|
||||
|
||||
.external_app_view_wav : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_view_wav.application_information));
|
||||
*(*ui*external_app*view_wav*);
|
||||
*/external/wav_view/*(*ui*external_app*view_wav*);
|
||||
} > ram_external_app_view_wav
|
||||
|
||||
.external_app_sd_wipe : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_sd_wipe.application_information));
|
||||
*(*ui*external_app*sd_wipe*);
|
||||
*/external/sd_wipe/*(*ui*external_app*sd_wipe*);
|
||||
} > ram_external_app_sd_wipe
|
||||
|
||||
.external_app_playlist_editor : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_playlist_editor.application_information));
|
||||
*(*ui*external_app*playlist_editor*);
|
||||
*/external/playlist_editor/*(*ui*external_app*playlist_editor*);
|
||||
} > ram_external_app_playlist_editor
|
||||
|
||||
.external_app_snake : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_snake.application_information));
|
||||
*(*ui*external_app*snake*);
|
||||
*/external/snake/*(*ui*external_app*snake*);
|
||||
} > ram_external_app_snake
|
||||
|
||||
.external_app_stopwatch : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_stopwatch.application_information));
|
||||
*(*ui*external_app*stopwatch*);
|
||||
*/external/stopwatch/*(*ui*external_app*stopwatch*);
|
||||
} > ram_external_app_stopwatch
|
||||
|
||||
.external_app_wefax_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_wefax_rx.application_information));
|
||||
*(*ui*external_app*wefax_rx*);
|
||||
*/external/wefax_rx/*(*ui*external_app*wefax_rx*);
|
||||
} > ram_external_app_wefax_rx
|
||||
|
||||
.external_app_noaaapt_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_noaaapt_rx.application_information));
|
||||
*(*ui*external_app*noaaapt_rx*);
|
||||
*/external/noaaapt_rx/*(*ui*external_app*noaaapt_rx*);
|
||||
} > ram_external_app_noaaapt_rx
|
||||
|
||||
.external_app_vor_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_vor_rx.application_information));
|
||||
*(*ui*external_app*vor_rx*);
|
||||
*/external/vor_rx/*(*ui*external_app*vor_rx*);
|
||||
} > ram_external_app_vor_rx
|
||||
|
||||
.external_app_vor_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_vor_tx.application_information));
|
||||
*(*ui*external_app*vor_tx*);
|
||||
*/external/vor_tx/*(*ui*external_app*vor_tx*);
|
||||
} > ram_external_app_vor_tx
|
||||
|
||||
.external_app_breakout : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_breakout.application_information));
|
||||
*(*ui*external_app*breakout*);
|
||||
*/external/breakout/*(*ui*external_app*breakout*);
|
||||
} > ram_external_app_breakout
|
||||
|
||||
.external_app_doom : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_doom.application_information));
|
||||
*(*ui*external_app*doom*);
|
||||
*/external/doom/*(*ui*external_app*doom*);
|
||||
} > ram_external_app_doom
|
||||
|
||||
.external_app_debug_pmem : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_debug_pmem.application_information));
|
||||
*(*ui*external_app*debug_pmem*);
|
||||
*/external/debug_pmem/*(*ui*external_app*debug_pmem*);
|
||||
} > ram_external_app_debug_pmem
|
||||
|
||||
.external_app_scanner : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_scanner.application_information));
|
||||
*(*ui*external_app*scanner*);
|
||||
*/external/scanner/*(*ui*external_app*scanner*);
|
||||
} > ram_external_app_scanner
|
||||
|
||||
.external_app_level : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_level.application_information));
|
||||
*(*ui*external_app*level*);
|
||||
*/external/level/*(*ui*external_app*level*);
|
||||
} > ram_external_app_level
|
||||
|
||||
.external_app_gfxeq : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_gfxeq.application_information));
|
||||
*(*ui*external_app*gfxeq*);
|
||||
*/external/gfxeq/*(*ui*external_app*gfxeq*);
|
||||
} > ram_external_app_gfxeq
|
||||
|
||||
.external_app_waterfall_designer : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_waterfall_designer.application_information));
|
||||
*(*ui*external_app*waterfall_designer*);
|
||||
*/external/waterfall_designer/*(*ui*external_app*waterfall_designer*);
|
||||
} > ram_external_app_waterfall_designer
|
||||
|
||||
.external_app_detector_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_detector_rx.application_information));
|
||||
*(*ui*external_app*detector_rx*);
|
||||
*/external/detector_rx/*(*ui*external_app*detector_rx*);
|
||||
} > ram_external_app_detector_rx
|
||||
|
||||
.external_app_dinogame : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_dinogame.application_information));
|
||||
*(*ui*external_app*dinogame*);
|
||||
*/external/dinogame/*(*ui*external_app*dinogame*);
|
||||
} > ram_external_app_dinogame
|
||||
|
||||
.external_app_spaceinv : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_spaceinv.application_information));
|
||||
*(*ui*external_app*spaceinv*);
|
||||
*/external/spaceinv/*(*ui*external_app*spaceinv*);
|
||||
} > ram_external_app_spaceinv
|
||||
|
||||
.external_app_blackjack : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_blackjack.application_information));
|
||||
*(*ui*external_app*blackjack*);
|
||||
*/external/blackjack/*(*ui*external_app*blackjack*);
|
||||
} > ram_external_app_blackjack
|
||||
|
||||
.external_app_battleship : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_battleship.application_information));
|
||||
*(*ui*external_app*battleship*);
|
||||
*/external/battleship/*(*ui*external_app*battleship*);
|
||||
} > ram_external_app_battleship
|
||||
|
||||
.external_app_ert : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_ert.application_information));
|
||||
*(*ui*external_app*ert*);
|
||||
*/external/ert/*(*ui*external_app*ert*);
|
||||
} > ram_external_app_ert
|
||||
|
||||
.external_app_epirb_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_epirb_rx.application_information));
|
||||
*(*ui*external_app*epirb_rx*);
|
||||
*/external/epirb_rx/*(*ui*external_app*epirb_rx*);
|
||||
} > ram_external_app_epirb_rx
|
||||
|
||||
.external_app_soundboard : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_soundboard.application_information));
|
||||
*(*ui*external_app*soundboard*);
|
||||
*/external/soundboard/*(*ui*external_app*soundboard*);
|
||||
} > ram_external_app_soundboard
|
||||
|
||||
.external_app_game2048 : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_game2048.application_information));
|
||||
*(*ui*external_app*game2048*);
|
||||
*/external/game2048/*(*ui*external_app*game2048*);
|
||||
} > ram_external_app_game2048
|
||||
|
||||
.external_app_bht_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_bht_tx.application_information));
|
||||
*(*ui*external_app*bht_tx*);
|
||||
*/external/bht_tx/*(*ui*external_app*bht_tx*);
|
||||
} > ram_external_app_bht_tx
|
||||
|
||||
.external_app_morse_practice : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_morse_practice.application_information));
|
||||
*(*ui*external_app*morse_practice*);
|
||||
*/external/morse_practice/*(*ui*external_app*morse_practice*);
|
||||
} > ram_external_app_morse_practice
|
||||
|
||||
.external_app_adult_toys_controller : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_adult_toys_controller.application_information));
|
||||
*(*ui*external_app*adult_toys_controller*);
|
||||
*/external/adult_toys_controller/*(*ui*external_app*adult_toys_controller*);
|
||||
} > ram_external_app_adult_toys_controller
|
||||
|
||||
.external_app_flex_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_flex_rx.application_information));
|
||||
*(*ui*external_app*flex_rx*);
|
||||
*/external/flex_rx/*(*ui*external_app*flex_rx*);
|
||||
} > ram_external_app_flex_rx
|
||||
|
||||
.external_app_sstvrx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_sstvrx.application_information));
|
||||
*(*ui*external_app*sstvrx*);
|
||||
*/external/sstvrx/*(*ui*external_app*sstvrx*);
|
||||
} > ram_external_app_sstvrx
|
||||
|
||||
.external_app_subcarrx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_subcarrx.application_information));
|
||||
*(*ui*external_app*subcarrx*);
|
||||
*/external/subcarrx/*(*ui*external_app*subcarrx*);
|
||||
} > ram_external_app_subcarrx
|
||||
|
||||
.external_app_siggen : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_siggen.application_information));
|
||||
*(*ui*external_app*siggen*);
|
||||
*/external/siggen/*(*ui*external_app*siggen*);
|
||||
} > ram_external_app_siggen
|
||||
|
||||
.external_app_sdusb : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_sdusb.application_information));
|
||||
*(*ui*external_app*sdusb*);
|
||||
*/external/sdusb/*(*ui*external_app*sdusb*);
|
||||
} > ram_external_app_sdusb
|
||||
|
||||
.external_app_morse_radio : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_morse_radio.application_information));
|
||||
*(*ui*external_app*morse_radio*);
|
||||
*/external/morse_radio/*(*ui*external_app*morse_radio*);
|
||||
} > ram_external_app_morse_radio
|
||||
|
||||
.external_app_morseradiotx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_morseradiotx.application_information));
|
||||
*(*ui*external_app*morseradiotx*);
|
||||
*/external/morseradiotx/*(*ui*external_app*morseradiotx*);
|
||||
} > ram_external_app_morseradiotx
|
||||
|
||||
.external_app_keeloqtx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_keeloqtx.application_information));
|
||||
*(*ui*external_app*keeloqtx*);
|
||||
*/external/keeloqtx/*(*ui*external_app*keeloqtx*);
|
||||
} > ram_external_app_keeloqtx
|
||||
|
||||
.external_app_rtty_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_rtty_rx.application_information));
|
||||
*(*ui*external_app*rtty_rx*);
|
||||
*/external/rtty_rx/*(*ui*external_app*rtty_rx*);
|
||||
} > ram_external_app_rtty_rx
|
||||
|
||||
.external_app_rtty_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_rtty_tx.application_information));
|
||||
*(*ui*external_app*rtty_tx*);
|
||||
*/external/rtty_tx/*(*ui*external_app*rtty_tx*);
|
||||
} > ram_external_app_rtty_tx
|
||||
|
||||
.external_app_pocsag_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_pocsag_tx.application_information));
|
||||
*(*ui*external_app*pocsag_tx*);
|
||||
*/external/pocsag_tx/*(*ui*external_app*pocsag_tx*);
|
||||
} > ram_external_app_pocsag_tx
|
||||
|
||||
.external_app_time_sink : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_time_sink.application_information));
|
||||
*(*ui*external_app*time_sink*);
|
||||
*/external/time_sink/*(*ui*external_app*time_sink*);
|
||||
} > ram_external_app_time_sink
|
||||
|
||||
.external_app_same_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_same_tx.application_information));
|
||||
*(*ui*external_app*same_tx*);
|
||||
*/external/same_tx/*(*ui*external_app*same_tx*);
|
||||
} > ram_external_app_same_tx
|
||||
|
||||
.external_app_kiss_tnc : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_kiss_tnc.application_information));
|
||||
*(*ui*external_app*kiss_tnc*);
|
||||
*/external/kiss_tnc/*(*ui*external_app*kiss_tnc*);
|
||||
} > ram_external_app_kiss_tnc
|
||||
|
||||
.external_app_mdc_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_mdc_tx.application_information));
|
||||
*(*ui*external_app*mdc_tx*);
|
||||
*/external/mdc_tx/*(*ui*external_app*mdc_tx*);
|
||||
} > ram_external_app_mdc_tx
|
||||
|
||||
.external_app_epirb_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_epirb_tx.application_information));
|
||||
*(*ui*external_app*epirb_tx*);
|
||||
*/external/epirb_tx/*(*ui*external_app*epirb_tx*);
|
||||
} > ram_external_app_epirb_tx
|
||||
|
||||
.external_app_fpv_detect : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_fpv_detect.application_information));
|
||||
*(*ui*external_app*fpv_detect*);
|
||||
*/external/fpv_detect/*(*ui*external_app*fpv_detect*);
|
||||
} > ram_external_app_fpv_detect
|
||||
|
||||
.external_app_p25_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_p25_tx.application_information));
|
||||
*(*ui*external_app*p25_tx*);
|
||||
*/external/p25_tx/*(*ui*external_app*p25_tx*);
|
||||
} > ram_external_app_p25_tx
|
||||
|
||||
.external_app_two_tone_pager : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_two_tone_pager.application_information));
|
||||
*(*ui*external_app*two_tone_pager*);
|
||||
*/external/two_tone_pager/*(*ui*external_app*two_tone_pager*);
|
||||
} > ram_external_app_two_tone_pager
|
||||
|
||||
.external_app_two_tone_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_two_tone_rx.application_information));
|
||||
*(*ui*external_app*two_tone_rx*);
|
||||
*/external/two_tone_rx/*(*ui*external_app*two_tone_rx*);
|
||||
} > ram_external_app_two_tone_rx
|
||||
|
||||
.external_app_flex_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_flex_tx.application_information));
|
||||
*(*ui*external_app*flex_tx*);
|
||||
*/external/flex_tx/*(*ui*external_app*flex_tx*);
|
||||
} > ram_external_app_flex_tx
|
||||
|
||||
.external_app_hard_reset : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_hard_reset.application_information));
|
||||
*(*ui*external_app*hard_reset*);
|
||||
*/external/hard_reset/*(*ui*external_app*hard_reset*);
|
||||
} > ram_external_app_hard_reset
|
||||
|
||||
.external_app_secplustx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_secplustx.application_information));
|
||||
*(*ui*external_app*secplustx*);
|
||||
*/external/secplustx/*(*ui*external_app*secplustx*);
|
||||
} > ram_external_app_secplustx
|
||||
|
||||
.external_app_signal_hunter : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_signal_hunter.application_information));
|
||||
*(*ui*external_app*signal_hunter*);
|
||||
*/external/signal_hunter/*(*ui*external_app*signal_hunter*);
|
||||
} > ram_external_app_signal_hunter
|
||||
|
||||
.external_app_tetra_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_tetra_rx.application_information));
|
||||
*/external/tetra_rx/*(*ui*external_app*tetra_rx*);
|
||||
} > ram_external_app_tetra_rx
|
||||
|
||||
.external_app_adsbrx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_adsbrx.application_information));
|
||||
*(*ui*external_app*adsbrx*);
|
||||
} > ram_external_app_adsbrx
|
||||
|
||||
.external_app_ais_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_ais_rx.application_information));
|
||||
*(*ui*external_app*ais_rx*);
|
||||
} > ram_external_app_ais_rx
|
||||
|
||||
.external_app_aprs_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_aprs_rx.application_information));
|
||||
*(*ui*external_app*aprs_rx*);
|
||||
} > ram_external_app_aprs_rx
|
||||
|
||||
.external_app_aprs_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_aprs_tx.application_information));
|
||||
*(*ui*external_app*aprs_tx*);
|
||||
} > ram_external_app_aprs_tx
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
+38
-2
@@ -34,7 +34,7 @@ void KeeloqTXView::update_hop() {
|
||||
|
||||
hop = data.btn << 28 | (apri_serial & 0xFFF) << 16 | data.counter;
|
||||
} else if (
|
||||
data.mf_name == "DTM_Neo" || data.mf_name == "FAAC_RC,XT" || data.mf_name == "Mutanco_Mutancode" || data.mf_name == "Came_Space" || data.mf_name == "Genius_Bravo" || data.mf_name == "GSN" || data.mf_name == "Rosh" || data.mf_name == "Rossi" || data.mf_name == "Peccinin" || data.mf_name == "Steelmate" || data.mf_name == "Cardin_S449") {
|
||||
data.mf_name == "DTM_Neo" || data.mf_name == "FAAC_RC,XT" || data.mf_name == "Mutanco_Mutancode" || data.mf_name == "Came_Space" || data.mf_name == "Genius_Bravo" || data.mf_name == "GSN" || data.mf_name == "Rosh" || data.mf_name == "Rossi" || data.mf_name == "Peccinin" || data.mf_name == "Steelmate" || data.mf_name == "Cardin_S449" || data.mf_name == "Superrollo") {
|
||||
hop = data.btn << 28 | (data.serial & 0xFFF) << 16 | data.counter;
|
||||
} else if (
|
||||
data.mf_name == "NICE_Smilo" || data.mf_name == "NICE_MHOUSE" || data.mf_name == "JCM_Tech") {
|
||||
@@ -75,6 +75,12 @@ void KeeloqTXView::update_payload() {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.mf_name == "Superrollo") {
|
||||
text_payload.set(to_string_hex((uint64_t)(uint32_t)encrypt));
|
||||
encode_data_gw60((uint32_t)encrypt);
|
||||
return;
|
||||
}
|
||||
|
||||
payload = (uint64_t)fix << 32 | encrypt;
|
||||
|
||||
uint64_t preview_payload = FProtoGeneral::subghz_protocol_blocks_reverse_key(payload, 64);
|
||||
@@ -83,6 +89,34 @@ void KeeloqTXView::update_payload() {
|
||||
encode_data();
|
||||
}
|
||||
|
||||
// Superrollo GW60 (HCS361) 67-bit OOK frame at Te=450us.
|
||||
void KeeloqTXView::encode_data_gw60(uint32_t hop_enc) {
|
||||
int bits[67];
|
||||
for (uint32_t i = 0; i < 32; i++) bits[i] = (hop_enc >> i) & 1;
|
||||
for (uint32_t i = 0; i < 28; i++) bits[32 + i] = (data.serial >> i) & 1;
|
||||
for (uint32_t i = 0; i < 4; i++) bits[60 + i] = (data.btn >> i) & 1;
|
||||
bits[64] = 1;
|
||||
|
||||
int crc0 = 0, crc1 = 0;
|
||||
for (uint32_t i = 0; i < 65; i++) {
|
||||
int new_crc1 = crc0 ^ bits[i];
|
||||
int new_crc0 = new_crc1 ^ crc1;
|
||||
crc0 = new_crc0 & 1;
|
||||
crc1 = new_crc1 & 1;
|
||||
}
|
||||
bits[65] = crc0;
|
||||
bits[66] = crc1;
|
||||
|
||||
std::string s{};
|
||||
for (int i = 0; i < 9; i++) s += "100";
|
||||
s += "11111111110000000000";
|
||||
for (int i = 0; i < 67; i++) s += keeloq_fragments[bits[i]];
|
||||
for (int i = 0; i < 18; i++) s += "0";
|
||||
|
||||
encoded_data = s;
|
||||
pause_duration = 0;
|
||||
}
|
||||
|
||||
void KeeloqTXView::encode_data() {
|
||||
std::string fragments{};
|
||||
|
||||
@@ -206,9 +240,11 @@ void KeeloqTXView::start_tx() {
|
||||
|
||||
transmitter_model.enable();
|
||||
|
||||
const double te_us = (data.mf_name == "Superrollo") ? 450.0 : 400.0;
|
||||
|
||||
baseband::set_ook_data(
|
||||
bitstream_length,
|
||||
OOK_SAMPLERATE * (400.0 / 1000000.0),
|
||||
OOK_SAMPLERATE * (te_us / 1000000.0),
|
||||
repeat,
|
||||
pause_duration);
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ class KeeloqTXView : public View {
|
||||
std::string encoded_data{};
|
||||
|
||||
void encode_data();
|
||||
void encode_data_gw60(uint32_t hop_enc);
|
||||
|
||||
uint32_t repeat = 4;
|
||||
uint32_t pause_duration = 0;
|
||||
|
||||
@@ -8,14 +8,12 @@ using namespace portapack;
|
||||
namespace ui::external_app::mcu_temperature {
|
||||
|
||||
void McuTemperatureWidget::paint(Painter& painter) {
|
||||
const auto logger = portapack::temperature_logger;
|
||||
|
||||
const auto rect = screen_rect();
|
||||
const Color color_background{0, 0, 64};
|
||||
const Color color_foreground = Theme::getInstance()->fg_green->foreground;
|
||||
const Color color_reticle{128, 128, 128};
|
||||
|
||||
const auto graph_width = static_cast<int>(logger.capacity()) * bar_width;
|
||||
const auto graph_width = static_cast<int>(temperature_logger.capacity()) * bar_width;
|
||||
const Rect graph_rect{
|
||||
rect.left() + (rect.width() - graph_width) / 2, rect.top() + 8,
|
||||
graph_width, rect.height()};
|
||||
@@ -25,7 +23,7 @@ void McuTemperatureWidget::paint(Painter& painter) {
|
||||
painter.draw_rectangle(frame_rect, color_reticle);
|
||||
painter.fill_rectangle(graph_rect, color_background);
|
||||
|
||||
const auto history = logger.history();
|
||||
const auto history = temperature_logger.history();
|
||||
for (size_t i = 0; i < history.size(); i++) {
|
||||
const Coord x = graph_rect.right() - (history.size() - i) * bar_width;
|
||||
const auto sample = history[i];
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
#include "portapack.hpp"
|
||||
#include "memory_map.hpp"
|
||||
#include "irq_controls.hpp"
|
||||
|
||||
#include "temperature_logger.hpp"
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
@@ -43,14 +43,25 @@ class McuTemperatureWidget : public Widget {
|
||||
explicit McuTemperatureWidget(
|
||||
Rect parent_rect)
|
||||
: Widget{parent_rect} {
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
this->on_tick_second();
|
||||
};
|
||||
}
|
||||
~McuTemperatureWidget() {
|
||||
rtc_time::signal_tick_second -= signal_token_tick_second;
|
||||
}
|
||||
void on_tick_second() {
|
||||
temperature_logger.second_tick();
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
TemperatureLogger temperature_logger{};
|
||||
|
||||
using sample_t = uint32_t;
|
||||
using temperature_t = int32_t;
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
temperature_t temperature(const sample_t sensor_value) const;
|
||||
Coord screen_y(const temperature_t temperature, const Rect& screen_rect) const;
|
||||
|
||||
|
||||
+4
@@ -25,6 +25,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ui::external_app::mcu_temperature {
|
||||
|
||||
void TemperatureLogger::second_tick() {
|
||||
sample_phase++;
|
||||
if (sample_phase >= sample_interval) {
|
||||
@@ -67,3 +69,5 @@ void TemperatureLogger::push_sample(const TemperatureLogger::sample_t sample) {
|
||||
samples_count++;
|
||||
sample_phase = 0;
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::mcu_temperature
|
||||
+4
@@ -27,6 +27,8 @@
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace ui::external_app::mcu_temperature {
|
||||
|
||||
class TemperatureLogger {
|
||||
public:
|
||||
using sample_t = int8_t;
|
||||
@@ -49,4 +51,6 @@ class TemperatureLogger {
|
||||
void push_sample(const sample_t sample);
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::mcu_temperature
|
||||
|
||||
#endif /*__TEMPERATURE_LOGGER_H__*/
|
||||
+1
-1
@@ -46,7 +46,7 @@ SdOverUsbView::SdOverUsbView(NavigationView& nav)
|
||||
sdcStop(&SDCD1);
|
||||
|
||||
portapack::shutdown(true, false);
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base(), false);
|
||||
m0_halt();
|
||||
/* will not return*/
|
||||
};
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* 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_tetra_rx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<TetraRxView>();
|
||||
}
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_tetra_rx.application_information"), used)) application_information_t _application_information_tetra_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::tetra_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "Tetra",
|
||||
/*.bitmap_data = */ {
|
||||
0xE0,
|
||||
0x0F,
|
||||
0x18,
|
||||
0x38,
|
||||
0xE4,
|
||||
0x67,
|
||||
0x7E,
|
||||
0xCE,
|
||||
0xC7,
|
||||
0xCC,
|
||||
0x00,
|
||||
0x00,
|
||||
0xFF,
|
||||
0x4F,
|
||||
0xBA,
|
||||
0xB2,
|
||||
0x9A,
|
||||
0xEE,
|
||||
0xBA,
|
||||
0xB2,
|
||||
0x00,
|
||||
0x00,
|
||||
0x3B,
|
||||
0xE3,
|
||||
0x73,
|
||||
0x7E,
|
||||
0xC6,
|
||||
0x27,
|
||||
0x1C,
|
||||
0x18,
|
||||
0xF0,
|
||||
0x07,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_tetrarx*/ {'P', 'T', 'E', 'T'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
|
||||
class BitVector {
|
||||
public:
|
||||
BitVector(uint8_t* p, size_t bits)
|
||||
: data_(p),
|
||||
bits_(bits) {
|
||||
}
|
||||
|
||||
inline bool get(size_t bit) const {
|
||||
return (data_[bit >> 3] >>
|
||||
(7 - (bit & 7))) &
|
||||
1;
|
||||
}
|
||||
|
||||
inline void set(size_t bit, bool value) {
|
||||
if (value)
|
||||
data_[bit >> 3] |=
|
||||
1 << (7 - (bit & 7));
|
||||
else
|
||||
data_[bit >> 3] &=
|
||||
~(1 << (7 - (bit & 7)));
|
||||
}
|
||||
|
||||
inline void xor_bit(size_t bit, bool value) {
|
||||
if (value)
|
||||
data_[bit >> 3] ^=
|
||||
1 << (7 - (bit & 7));
|
||||
}
|
||||
|
||||
inline size_t size() const {
|
||||
return bits_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t* data_;
|
||||
size_t bits_;
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "tetra_crc.hpp"
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
|
||||
uint16_t CRC::crc16_itut_bits(
|
||||
const uint8_t* bits,
|
||||
uint32_t n_bits,
|
||||
uint16_t init) {
|
||||
uint16_t crc = init;
|
||||
|
||||
for (uint32_t i = 0; i < n_bits; i++) {
|
||||
uint8_t bit =
|
||||
(bits[i >> 3] >>
|
||||
(7 - (i & 7))) &
|
||||
1;
|
||||
|
||||
crc ^= uint16_t(bit) << 15;
|
||||
|
||||
if (crc & 0x8000) {
|
||||
crc =
|
||||
(crc << 1) ^
|
||||
0x1021;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
|
||||
crc &= 0xffff;
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
|
||||
class CRC {
|
||||
public:
|
||||
static constexpr uint16_t CRC_OK = 0x1d0f;
|
||||
|
||||
static uint16_t crc16_itut_bits(
|
||||
const uint8_t* bits,
|
||||
uint32_t n_bits,
|
||||
uint16_t init = 0xffff);
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "tetra_descrambler.hpp"
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
|
||||
static inline uint8_t lfsr_step(uint32_t& s) {
|
||||
uint8_t b = 0;
|
||||
|
||||
b ^= (s >> (32 - 32)) & 1;
|
||||
b ^= (s >> (32 - 26)) & 1;
|
||||
b ^= (s >> (32 - 23)) & 1;
|
||||
b ^= (s >> (32 - 22)) & 1;
|
||||
b ^= (s >> (32 - 16)) & 1;
|
||||
b ^= (s >> (32 - 12)) & 1;
|
||||
b ^= (s >> (32 - 11)) & 1;
|
||||
b ^= (s >> (32 - 10)) & 1;
|
||||
b ^= (s >> (32 - 8)) & 1;
|
||||
b ^= (s >> (32 - 7)) & 1;
|
||||
b ^= (s >> (32 - 5)) & 1;
|
||||
b ^= (s >> (32 - 4)) & 1;
|
||||
b ^= (s >> (32 - 2)) & 1;
|
||||
b ^= (s >> (32 - 1)) & 1;
|
||||
|
||||
s = (s >> 1) | (uint32_t(b) << 31);
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
void Descrambler::generate(
|
||||
uint32_t init,
|
||||
uint8_t* seq,
|
||||
size_t bits) {
|
||||
uint32_t s = init;
|
||||
|
||||
for (size_t i = 0; i < bits; i++)
|
||||
seq[i] = lfsr_step(s);
|
||||
}
|
||||
|
||||
void Descrambler::descramble(
|
||||
BitVector& bits,
|
||||
uint32_t init) {
|
||||
uint32_t s = init;
|
||||
|
||||
for (size_t i = 0; i < bits.size(); i++)
|
||||
bits.xor_bit(i, lfsr_step(s));
|
||||
}
|
||||
|
||||
uint32_t Descrambler::dmo_scramb_init(
|
||||
uint32_t mni,
|
||||
uint32_t src) {
|
||||
uint32_t init =
|
||||
(src & 0xFFFFFF) |
|
||||
((mni & 0x3F) << 24);
|
||||
|
||||
init <<= 2;
|
||||
init |= 3;
|
||||
|
||||
return init;
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "tetra_bits.hpp"
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
|
||||
class Descrambler {
|
||||
public:
|
||||
static constexpr uint32_t SCRAMB_INIT_BSCH = 0x00000003;
|
||||
|
||||
static void descramble(
|
||||
BitVector& bits,
|
||||
uint32_t init = SCRAMB_INIT_BSCH);
|
||||
|
||||
static void generate(
|
||||
uint32_t init,
|
||||
uint8_t* sequence,
|
||||
size_t bits);
|
||||
|
||||
static uint32_t dmo_scramb_init(
|
||||
uint32_t mni,
|
||||
uint32_t src);
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "tetra_interleave.hpp"
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
|
||||
void Interleave::block_deinterleave(
|
||||
BitVector& in,
|
||||
BitVector& out,
|
||||
uint32_t K,
|
||||
uint32_t a) {
|
||||
for (uint32_t i = 1; i <= K; i++) {
|
||||
uint32_t k =
|
||||
1 +
|
||||
((a * i) % K);
|
||||
|
||||
out.set(
|
||||
i - 1,
|
||||
in.get(k - 1));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "tetra_bits.hpp"
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
|
||||
class Interleave {
|
||||
public:
|
||||
static void block_deinterleave(
|
||||
BitVector& in,
|
||||
BitVector& out,
|
||||
uint32_t K,
|
||||
uint32_t a);
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "tetra_rcpc.hpp"
|
||||
#include <string.h>
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
|
||||
static constexpr uint8_t P_RATE_2_3[3] = {1, 2, 5};
|
||||
|
||||
static constexpr uint32_t T_RATE_2_3 = 3;
|
||||
static constexpr uint32_t PERIOD = 8;
|
||||
|
||||
void RCPC::depuncture_2_3(BitVector& in, uint8_t* out, uint32_t in_bits) {
|
||||
const uint32_t mother_bits = ((in_bits + T_RATE_2_3 - 1) / T_RATE_2_3) * PERIOD + PERIOD;
|
||||
memset(out, ERASED, mother_bits);
|
||||
for (uint32_t j = 1; j <= in_bits; j++) {
|
||||
uint32_t i = j;
|
||||
uint32_t k = PERIOD * ((i - 1) / T_RATE_2_3);
|
||||
k += P_RATE_2_3[(i - 1) % T_RATE_2_3];
|
||||
if (k >= 1 && k <= mother_bits) {
|
||||
out[k - 1] = in.get(j - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "tetra_bits.hpp"
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
|
||||
class RCPC {
|
||||
public:
|
||||
static constexpr uint8_t ERASED = 0xff;
|
||||
|
||||
static void depuncture_2_3(
|
||||
BitVector& in,
|
||||
uint8_t* out,
|
||||
uint32_t in_bits);
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
@@ -0,0 +1,73 @@
|
||||
#include "tetra_viterbi.hpp"
|
||||
#include <string.h>
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
|
||||
int Viterbi::decode_cch(
|
||||
const uint8_t* mother,
|
||||
uint32_t n_info,
|
||||
uint8_t* bits,
|
||||
uint8_t* trace_buffer) { // Puffer by caller
|
||||
|
||||
// Store these on stack, to avoid flash space usage
|
||||
const uint8_t next_state[16][2] = {
|
||||
{0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}, {10, 11}, {12, 13}, {14, 15}, {0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}, {10, 11}, {12, 13}, {14, 15}};
|
||||
const uint8_t next_output[16][2] = {
|
||||
{0, 15}, {11, 4}, {6, 9}, {13, 2}, {5, 10}, {14, 1}, {3, 12}, {8, 7}, {15, 0}, {4, 11}, {9, 6}, {2, 13}, {10, 5}, {1, 14}, {12, 3}, {7, 8}};
|
||||
|
||||
const uint16_t INF = 0x3fff;
|
||||
uint16_t cost[16];
|
||||
uint16_t new_cost[16];
|
||||
|
||||
// Initialization
|
||||
for (int i = 0; i < 16; i++) cost[i] = INF;
|
||||
cost[0] = 0;
|
||||
|
||||
for (uint32_t step = 0; step < n_info; step++) {
|
||||
for (int i = 0; i < 16; i++) new_cost[i] = INF;
|
||||
const uint8_t* rx = &mother[step * 4];
|
||||
|
||||
for (int prev = 0; prev < 16; prev++) {
|
||||
if (cost[prev] >= INF) continue;
|
||||
|
||||
for (int bit = 0; bit < 2; bit++) {
|
||||
const uint8_t pattern = next_output[prev][bit];
|
||||
const uint8_t next = next_state[prev][bit];
|
||||
|
||||
uint16_t d = 0;
|
||||
for (int b = 0; b < 4; b++) {
|
||||
if (rx[b] != 0xff && rx[b] != ((pattern >> (3 - b)) & 1)) d++;
|
||||
}
|
||||
|
||||
const uint16_t total = cost[prev] + d;
|
||||
if (total < new_cost[next]) {
|
||||
new_cost[next] = total;
|
||||
trace_buffer[step * 16 + next] = (prev << 1) | bit;
|
||||
}
|
||||
}
|
||||
}
|
||||
memcpy(cost, new_cost, sizeof(cost));
|
||||
}
|
||||
|
||||
// Traceback
|
||||
uint8_t best_state = 0;
|
||||
for (int i = 1; i < 16; i++) {
|
||||
if (cost[i] < cost[best_state]) best_state = i;
|
||||
}
|
||||
|
||||
uint8_t state = best_state;
|
||||
memset(bits, 0, (n_info + 7) >> 3);
|
||||
|
||||
for (int step = (int)n_info - 1; step >= 0; step--) {
|
||||
uint8_t packed = trace_buffer[step * 16 + state];
|
||||
uint8_t prev = (packed >> 1) & 0x0F;
|
||||
uint8_t bit = packed & 0x01;
|
||||
|
||||
if (bit) bits[step >> 3] |= (1 << (7 - (step & 7)));
|
||||
state = prev;
|
||||
}
|
||||
|
||||
return cost[best_state];
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
|
||||
class Viterbi {
|
||||
public:
|
||||
int decode_cch(
|
||||
const uint8_t* mother,
|
||||
uint32_t n_info,
|
||||
uint8_t* bits,
|
||||
uint8_t* trace_buffer);
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
@@ -0,0 +1,354 @@
|
||||
#include "ui_tetra_rx.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "tetra_crc.hpp"
|
||||
#include "tetra_descrambler.hpp"
|
||||
#include "tetra_interleave.hpp"
|
||||
#include "tetra_rcpc.hpp"
|
||||
#include "tetra_viterbi.hpp"
|
||||
#include <cstring>
|
||||
|
||||
using namespace portapack;
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
|
||||
template <size_t OutBytes>
|
||||
bool TetraChannelDecoder::decode_block(
|
||||
const std::array<uint8_t, 63>& burst,
|
||||
size_t offset,
|
||||
size_t len_t5,
|
||||
size_t type1_bits,
|
||||
size_t type2_bits,
|
||||
size_t interleave_a,
|
||||
std::array<uint8_t, OutBytes>& out,
|
||||
uint16_t& crc,
|
||||
int& cost,
|
||||
uint32_t scramb_init) {
|
||||
std::array<uint8_t, 64> t5{};
|
||||
std::array<uint8_t, 64> t3{};
|
||||
std::array<uint8_t, 1200> mother{};
|
||||
std::array<uint8_t, 64> type2{};
|
||||
|
||||
out.fill(0);
|
||||
|
||||
for (size_t i = 0; i < len_t5; i++) {
|
||||
set_bit(t5, i, get_bit(burst, offset + i));
|
||||
}
|
||||
|
||||
BitVector t5_bits{t5.data(), len_t5};
|
||||
Descrambler::descramble(t5_bits, scramb_init);
|
||||
|
||||
BitVector t3_bits{t3.data(), len_t5};
|
||||
Interleave::block_deinterleave(t5_bits, t3_bits, len_t5, interleave_a);
|
||||
|
||||
RCPC::depuncture_2_3(t3_bits, mother.data(), len_t5);
|
||||
Viterbi viterbi;
|
||||
cost = viterbi.decode_cch(mother.data(), type2_bits, type2.data(), trace_buffer);
|
||||
crc = CRC::crc16_itut_bits(type2.data(), type1_bits + 16);
|
||||
for (size_t i = 0; i < type1_bits; i++) {
|
||||
set_bit(out, i, (type2[i >> 3] >> (7 - (i & 7))) & 1);
|
||||
}
|
||||
return crc == CRC::CRC_OK;
|
||||
}
|
||||
|
||||
TetraChannelDecoder::Result TetraChannelDecoder::decode_dnb(const TetraDnbMessage& message) {
|
||||
Result result{};
|
||||
result.inverted = message.inverted;
|
||||
|
||||
if (!network_synced)
|
||||
return result;
|
||||
|
||||
std::array<uint8_t, 63> buffer{};
|
||||
std::copy(message.payload.begin(), message.payload.end(), buffer.begin());
|
||||
|
||||
uint32_t tmo_dnb_seed = (((uint32_t)last_mcc << 20) | ((uint32_t)last_mnc << 6) | (uint32_t)last_bcc) << 2 | 3;
|
||||
// 1. DNB Full slot (SCH/F)
|
||||
bool ok = decode_block(buffer, 0, SCH_F_LEN_BITS, SCH_F_TYPE1_BITS, SCH_F_TYPE2_BITS,
|
||||
SCH_F_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed);
|
||||
|
||||
// 2. SCH/HD Block 1
|
||||
if (!ok) {
|
||||
ok = decode_block(buffer, 0, BLK2_LEN_BITS, SB2_TYPE1_BITS, SB2_TYPE2_BITS,
|
||||
SB2_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed);
|
||||
}
|
||||
|
||||
// 3. SCH/HD Block 2
|
||||
if (!ok) {
|
||||
ok = decode_block(buffer, 216, BLK2_LEN_BITS, SB2_TYPE1_BITS, SB2_TYPE2_BITS,
|
||||
SB2_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
result.type = Result::Type::Dnb;
|
||||
result.is_ok = true;
|
||||
parse_mac_pdu(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
TetraChannelDecoder::Result TetraChannelDecoder::decode_burst(const TetraBurstMessage& message) {
|
||||
Result result{};
|
||||
result.sync_errors = message.sync_errors;
|
||||
result.inverted = message.inverted;
|
||||
|
||||
// 1. SCH/S
|
||||
bool ok = decode_block(
|
||||
message.payload, BLK1_OFFSET_BITS, BLK1_LEN_BITS,
|
||||
SB1_TYPE1_BITS, SB1_TYPE2_BITS, SB1_INTERLEAVE_A,
|
||||
result.payload, result.crc, result.cost,
|
||||
Descrambler::SCRAMB_INIT_BSCH);
|
||||
|
||||
if (!ok) return result;
|
||||
|
||||
result.type = Result::Type::Sync;
|
||||
result.is_ok = true;
|
||||
parse_sync_pdu(result);
|
||||
|
||||
uint16_t current_mcc = result.mcc;
|
||||
uint16_t current_mnc = result.mnc;
|
||||
uint8_t current_bcc = result.bcc;
|
||||
uint8_t current_enc = result.encryption;
|
||||
|
||||
// 2. SCH/H
|
||||
uint16_t h_crc = 0;
|
||||
int h_cost = 0;
|
||||
|
||||
ok = decode_block(
|
||||
message.payload, TMO_BLK2_OFFSET_BITS, BLK2_LEN_BITS,
|
||||
SB2_TYPE1_BITS, SB2_TYPE2_BITS, SB2_INTERLEAVE_A,
|
||||
result.payload, h_crc, h_cost,
|
||||
Descrambler::SCRAMB_INIT_BSCH);
|
||||
|
||||
if (ok) {
|
||||
result.type = Result::Type::SyncFull;
|
||||
result.crc = h_crc;
|
||||
result.cost = h_cost;
|
||||
|
||||
result.mcc = current_mcc;
|
||||
result.mnc = current_mnc;
|
||||
result.bcc = current_bcc;
|
||||
result.encryption = current_enc;
|
||||
|
||||
parse_mac_pdu(result);
|
||||
} else {
|
||||
result.encryption = current_enc;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t TetraChannelDecoder::read_bits(const uint8_t* bytes, size_t bit, size_t count) const {
|
||||
uint32_t value = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
value <<= 1;
|
||||
value |= (bytes[(bit + i) >> 3] >> (7 - ((bit + i) & 7))) & 1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void TetraChannelDecoder::parse_sync_pdu(Result& result) {
|
||||
// Sys 4 bit, then BCC
|
||||
result.bcc = read_bits(result.payload.data(), 4, 6);
|
||||
result.timeslot = read_bits(result.payload.data(), 10, 2);
|
||||
result.frame_number = read_bits(result.payload.data(), 12, 5);
|
||||
result.encryption = read_bits(result.payload.data(), 30, 1);
|
||||
result.mcc = read_bits(result.payload.data(), 31, 10);
|
||||
result.mnc = read_bits(result.payload.data(), 41, 14);
|
||||
|
||||
last_mcc = result.mcc;
|
||||
last_mnc = result.mnc;
|
||||
last_bcc = result.bcc;
|
||||
network_synced = true;
|
||||
}
|
||||
|
||||
void TetraChannelDecoder::parse_mac_pdu(Result& result) {
|
||||
result.pdu_type = read_bits(result.payload.data(), 0, 2);
|
||||
|
||||
if (result.pdu_type == 0) { // MAC-RESOURCE
|
||||
result.encryption = read_bits(result.payload.data(), 4, 2);
|
||||
uint8_t length_ind = read_bits(result.payload.data(), 7, 6);
|
||||
|
||||
if (result.encryption == 0 && length_ind > 0 && length_ind < 62) {
|
||||
size_t offset = 13;
|
||||
uint8_t addr_type = read_bits(result.payload.data(), offset, 3);
|
||||
offset += 3;
|
||||
|
||||
if (addr_type == 1) {
|
||||
result.calling_ssi = read_bits(result.payload.data(), offset, 24);
|
||||
offset += 24;
|
||||
} else if (addr_type == 2) {
|
||||
offset += 10;
|
||||
} else if (addr_type == 3 || addr_type == 4) {
|
||||
result.calling_ssi = read_bits(result.payload.data(), offset, 24);
|
||||
offset += 24;
|
||||
} else if (addr_type == 5) {
|
||||
offset += 34;
|
||||
} else if (addr_type == 6) {
|
||||
offset += 30;
|
||||
} else if (addr_type == 7) {
|
||||
offset += 34;
|
||||
}
|
||||
|
||||
// skip optional fields
|
||||
if (read_bits(result.payload.data(), offset++, 1)) offset += 4; // Power control
|
||||
if (read_bits(result.payload.data(), offset++, 1)) offset += 8; // Slot grant
|
||||
|
||||
uint8_t chan_alloc = read_bits(result.payload.data(), offset++, 1);
|
||||
|
||||
if (chan_alloc == 0) {
|
||||
uint8_t llc_type = read_bits(result.payload.data(), offset, 4);
|
||||
offset += 4;
|
||||
|
||||
// LLC header skipped
|
||||
if (llc_type == 0 || llc_type == 1)
|
||||
offset += 2;
|
||||
else if (llc_type == 2 || llc_type == 3 || llc_type == 6 || llc_type == 7)
|
||||
offset += 1;
|
||||
|
||||
uint8_t mle_type = read_bits(result.payload.data(), offset, 3);
|
||||
offset += 3;
|
||||
|
||||
// 1 = CMCE
|
||||
if (mle_type == 1) {
|
||||
result.cmce_type = read_bits(result.payload.data(), offset, 5);
|
||||
offset += 5;
|
||||
result.call_id = read_bits(result.payload.data(), offset, 14);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (result.pdu_type == 2) { // SYSINFO/BCAST
|
||||
uint8_t bcast_type = read_bits(result.payload.data(), 2, 2);
|
||||
if (bcast_type == 0) {
|
||||
result.la = read_bits(result.payload.data(), 82, 14);
|
||||
result.encryption = read_bits(result.payload.data(), 122, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TetraRxView::TetraRxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
add_children({&rssi, &channel, &field_rf_amp, &field_lna, &field_vga,
|
||||
&field_frequency, &text_mcc, &text_la, &text_pdu,
|
||||
&text_mnc, &text_ts, &text_fn, &text_bcc, &text_enc, &text_debug, &console});
|
||||
|
||||
field_frequency.set_step(25000);
|
||||
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.set_squelch_level(0);
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
void TetraRxView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void TetraRxView::on_data_tetra(const TetraBurstMessage& message) {
|
||||
packet_count++;
|
||||
auto result = decoder.decode_burst(message);
|
||||
|
||||
if (result.type == TetraChannelDecoder::Result::Type::Sync ||
|
||||
result.type == TetraChannelDecoder::Result::Type::SyncFull) {
|
||||
valid_count++;
|
||||
|
||||
text_mcc.set("MCC: " + to_string_dec_uint(result.mcc));
|
||||
text_mnc.set("MNC: " + to_string_dec_uint(result.mnc));
|
||||
text_bcc.set("BCC: " + to_string_dec_uint(result.bcc));
|
||||
text_ts.set("TS: " + to_string_dec_uint(result.timeslot));
|
||||
text_fn.set("FN: " + to_string_dec_uint(result.frame_number));
|
||||
|
||||
std::string enc_str = (result.encryption != 0) ? "Encrypted" : "Clear";
|
||||
text_enc.set("ENC: " + enc_str);
|
||||
|
||||
if (result.type == TetraChannelDecoder::Result::Type::SyncFull) {
|
||||
h_valid_count++;
|
||||
if (result.la != 0xffff) text_la.set("LA: " + to_string_dec_uint(result.la));
|
||||
text_pdu.set("PDU: " + decoder.get_pdu_name(result.pdu_type));
|
||||
}
|
||||
}
|
||||
text_debug.set(
|
||||
"Syn: " + to_string_dec_uint(packet_count) +
|
||||
", V: " + to_string_dec_uint(valid_count) +
|
||||
", H: " + to_string_dec_uint(h_valid_count) +
|
||||
", E:" + to_string_dec_uint(result.sync_errors));
|
||||
}
|
||||
|
||||
void TetraRxView::on_data_dnb(const TetraDnbMessage& message) {
|
||||
dnb_count++;
|
||||
auto result = decoder.decode_dnb(message);
|
||||
|
||||
if (result.is_ok && result.type == TetraChannelDecoder::Result::Type::Dnb) {
|
||||
if (result.la != 0xffff) text_la.set("LA: " + to_string_dec_uint(result.la));
|
||||
text_pdu.set("PDU: " + decoder.get_pdu_name(result.pdu_type));
|
||||
|
||||
std::string enc_str = (result.encryption != 0) ? "Encrypted" : "Clear";
|
||||
text_enc.set("ENC: " + enc_str);
|
||||
|
||||
// if (rate_limiter++ < 2) console.writeln("DNB: " + decoder.get_pdu_name(result.pdu_type));
|
||||
if (result.cmce_type != 0xff) {
|
||||
std::string msg = "";
|
||||
if (result.cmce_type == 7)
|
||||
msg = "SETUP";
|
||||
else if (result.cmce_type == 1)
|
||||
msg = "CALL PROCEEDING";
|
||||
else if (result.cmce_type == 0)
|
||||
msg = "ALERTING";
|
||||
else if (result.cmce_type == 2)
|
||||
msg = "CONNECT";
|
||||
else if (result.cmce_type == 3)
|
||||
msg = "CONNECT ACK";
|
||||
else if (result.cmce_type == 11)
|
||||
msg = "TX GRANTED";
|
||||
else if (result.cmce_type == 9)
|
||||
msg = "TX CEASED";
|
||||
else if (result.cmce_type == 13)
|
||||
msg = "TX WAIT";
|
||||
else if (result.cmce_type == 10)
|
||||
msg = "TX CONTINUE";
|
||||
else if (result.cmce_type == 12)
|
||||
msg = "TX INTERRUPT";
|
||||
else if (result.cmce_type == 4)
|
||||
msg = "DISCONNECT";
|
||||
else if (result.cmce_type == 6)
|
||||
msg = "RELEASE";
|
||||
else if (result.cmce_type == 5)
|
||||
msg = "INFO";
|
||||
else if (result.cmce_type == 8)
|
||||
msg = "STATUS";
|
||||
else if (result.cmce_type == 14)
|
||||
msg = "SDS DATA";
|
||||
else if (result.cmce_type == 15)
|
||||
msg = "FACILITY";
|
||||
else if (result.cmce_type == 16)
|
||||
msg = "CALL RESTORE";
|
||||
else if (result.cmce_type == 31)
|
||||
msg = "NOT SUPPORTED";
|
||||
else
|
||||
msg = "CMCE:" + to_string_dec_uint(result.cmce_type);
|
||||
|
||||
console.writeln(msg);
|
||||
if (result.call_id != 0xffff) {
|
||||
console.writeln("ID:" + to_string_dec_uint(result.call_id) + " | SSI:" + to_string_dec_uint(result.calling_ssi));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TetraRxView::on_timer() {
|
||||
rate_limiter = 0;
|
||||
}
|
||||
|
||||
TetraRxView::~TetraRxView() {
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
@@ -0,0 +1,192 @@
|
||||
#ifndef __UI_TETRA_RX_H__
|
||||
#define __UI_TETRA_RX_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_language.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "message.hpp"
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::tetra_rx {
|
||||
|
||||
class TetraChannelDecoder {
|
||||
public:
|
||||
struct Result {
|
||||
enum class Type { None,
|
||||
Sync,
|
||||
SyncFull,
|
||||
Dnb };
|
||||
Type type{Type::None};
|
||||
|
||||
bool is_ok{false};
|
||||
uint8_t sync_errors{0};
|
||||
bool inverted{false};
|
||||
|
||||
uint16_t crc{0};
|
||||
int cost{0};
|
||||
|
||||
uint8_t timeslot{0xff};
|
||||
uint8_t frame_number{0xff};
|
||||
uint16_t mcc{0xffff};
|
||||
uint16_t mnc{0xffff};
|
||||
uint8_t bcc{0xff};
|
||||
|
||||
uint8_t pdu_type{0xff};
|
||||
uint16_t la{0xffff};
|
||||
uint8_t encryption{0xff};
|
||||
|
||||
uint8_t cmce_type{0xff};
|
||||
uint16_t call_id{0xffff};
|
||||
uint32_t calling_ssi{0};
|
||||
|
||||
std::array<uint8_t, 34> payload{};
|
||||
};
|
||||
|
||||
std::string get_pdu_name(uint8_t pdu_type) {
|
||||
switch (pdu_type) {
|
||||
case 0:
|
||||
return "MAC-RESOURCE";
|
||||
case 1:
|
||||
return "MAC-FRAG";
|
||||
case 2:
|
||||
return "SYSINFO/BCAST";
|
||||
case 3:
|
||||
return "MAC-U-SIGNAL";
|
||||
default:
|
||||
return "UNK_" + std::to_string(pdu_type);
|
||||
}
|
||||
}
|
||||
|
||||
Result decode_burst(const TetraBurstMessage& message);
|
||||
Result decode_dnb(const TetraDnbMessage& message);
|
||||
|
||||
private:
|
||||
static constexpr size_t BLK1_OFFSET_BITS = 94;
|
||||
static constexpr size_t BLK1_LEN_BITS = 120;
|
||||
static constexpr size_t TMO_BLK2_OFFSET_BITS = 282;
|
||||
static constexpr size_t BLK2_LEN_BITS = 216;
|
||||
static constexpr size_t SB1_TYPE1_BITS = 60;
|
||||
static constexpr size_t SB1_TYPE2_BITS = 80;
|
||||
static constexpr size_t SB1_INTERLEAVE_A = 11;
|
||||
static constexpr size_t SB2_TYPE1_BITS = 124;
|
||||
static constexpr size_t SB2_TYPE2_BITS = 144;
|
||||
static constexpr size_t SB2_INTERLEAVE_A = 101;
|
||||
|
||||
static constexpr size_t SCH_F_LEN_BITS = 432;
|
||||
static constexpr size_t SCH_F_TYPE1_BITS = 268;
|
||||
static constexpr size_t SCH_F_TYPE2_BITS = 288;
|
||||
static constexpr size_t SCH_F_INTERLEAVE_A = 103;
|
||||
|
||||
uint16_t last_mcc{0};
|
||||
uint16_t last_mnc{0};
|
||||
uint8_t last_bcc{0};
|
||||
bool network_synced{false};
|
||||
|
||||
// Viterbi buffer
|
||||
uint8_t trace_buffer[9216];
|
||||
|
||||
template <size_t OutBytes>
|
||||
bool decode_block(const std::array<uint8_t, 63>& burst,
|
||||
size_t offset,
|
||||
size_t len_t5,
|
||||
size_t type1_bits,
|
||||
size_t type2_bits,
|
||||
size_t interleave_a,
|
||||
std::array<uint8_t, OutBytes>& out,
|
||||
uint16_t& crc,
|
||||
int& cost,
|
||||
uint32_t scramb_init);
|
||||
|
||||
void parse_sync_pdu(Result& result);
|
||||
void parse_mac_pdu(Result& result);
|
||||
uint32_t read_bits(const uint8_t* bytes, size_t bit, size_t count) const;
|
||||
|
||||
template <size_t N>
|
||||
uint8_t get_bit(const std::array<uint8_t, N>& arr, size_t bit_idx) const {
|
||||
return (arr[bit_idx / 8] >> (7 - (bit_idx % 8))) & 0x01;
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
void set_bit(std::array<uint8_t, N>& arr, size_t bit_idx, uint8_t val) const {
|
||||
if (val)
|
||||
arr[bit_idx / 8] |= (1 << (7 - (bit_idx % 8)));
|
||||
else
|
||||
arr[bit_idx / 8] &= ~(1 << (7 - (bit_idx % 8)));
|
||||
}
|
||||
};
|
||||
|
||||
class TetraRxView : public View {
|
||||
public:
|
||||
TetraRxView(NavigationView& nav);
|
||||
~TetraRxView();
|
||||
void focus() override;
|
||||
std::string title() const override { return "Tetra RX"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{};
|
||||
app_settings::SettingsManager settings_{"rx_tetra", app_settings::Mode::RX};
|
||||
|
||||
uint32_t packet_count{0};
|
||||
uint32_t valid_count{0};
|
||||
uint32_t h_valid_count{0};
|
||||
uint32_t dnb_count{0};
|
||||
|
||||
RxFrequencyField field_frequency{{UI_POS_X(0), UI_POS_Y(0)}, nav_};
|
||||
RFAmpField field_rf_amp{{UI_POS_X(13), UI_POS_Y(0)}};
|
||||
LNAGainField field_lna{{UI_POS_X(15), UI_POS_Y(0)}};
|
||||
VGAGainField field_vga{{UI_POS_X(18), UI_POS_Y(0)}};
|
||||
RSSI rssi{{UI_POS_X_RIGHT(9), UI_POS_Y(0), UI_POS_WIDTH(9), 4}};
|
||||
Channel channel{{UI_POS_X_RIGHT(9), UI_POS_Y(0) + 5, UI_POS_WIDTH(9), 4}};
|
||||
|
||||
Text text_mcc{{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "MCC: ---"};
|
||||
Text text_mnc{{UI_POS_X(15), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "MNC: ---"};
|
||||
Text text_ts{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "TS: -"}; // time slot
|
||||
Text text_fn{{UI_POS_X(15), UI_POS_Y(2), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "FN: -"}; // frame number
|
||||
Text text_bcc{{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "BCC: -"}; // Base Station Color Code
|
||||
Text text_enc{{UI_POS_X(15), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "ENC: -"}; // encoded or not
|
||||
Text text_la{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "LA: ----"}; // Location Area
|
||||
Text text_pdu{{UI_POS_X(15), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "PDU: ----"}; // pdu content type
|
||||
|
||||
Text text_debug{{UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "Syn: 0, V: 0, H: 0, E:0"}; // sync, valid, h_valid, errors corrected in top part
|
||||
Console console{{UI_POS_X(0), UI_POS_Y(6) + 8, UI_POS_MAXWIDTH, screen_height - (UI_POS_Y(7) + 8)}};
|
||||
|
||||
TetraChannelDecoder decoder{};
|
||||
|
||||
void on_data_tetra(const TetraBurstMessage&);
|
||||
void on_data_dnb(const TetraDnbMessage&);
|
||||
|
||||
uint8_t rate_limiter{0}; // limiter, to keep the device responsive
|
||||
void on_timer();
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::TetraBsch,
|
||||
[this](Message* const p) {
|
||||
this->on_data_tetra(*static_cast<const TetraBurstMessage*>(p));
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_dnb{
|
||||
Message::ID::TetraDnb,
|
||||
[this](Message* const p) {
|
||||
this->on_data_dnb(*static_cast<const TetraDnbMessage*>(p));
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::tetra_rx
|
||||
|
||||
#endif
|
||||
+210
-142
@@ -28,11 +28,47 @@
|
||||
#include "ui_fileman.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
#include "convert.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui::external_app::waterfall_designer {
|
||||
|
||||
namespace {
|
||||
|
||||
/* Parses an "index,R,G,B" profile line.
|
||||
* NB: std::stoi() must not be used here - the firmware is built with
|
||||
* -fno-exceptions, so any malformed field in a hand-edited profile makes it
|
||||
* call std::terminate() instead of throwing. parse_int() is what gradient.cpp
|
||||
* uses to read the very same file format. */
|
||||
bool parse_level(std::string_view line, uint8_t& index, uint8_t& r, uint8_t& g, uint8_t& b) {
|
||||
// Comments are skipped by Gradient::load_file() too; parse_int() ignores
|
||||
// trailing junk, so "#0,0,0,0" would otherwise read back as a real level.
|
||||
if (line.empty() || line.front() == '#') return false;
|
||||
|
||||
auto cols = split_string(line, ',');
|
||||
if (cols.size() != 4) return false;
|
||||
|
||||
uint8_t* const out[4] = {&index, &r, &g, &b};
|
||||
|
||||
for (size_t i = 0; i < 4; i++) {
|
||||
int32_t value;
|
||||
if (!parse_int(cols[i], value) || value < 0 || value > 255) return false;
|
||||
*out[i] = static_cast<uint8_t>(value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_color_level(const std::string& line) {
|
||||
uint8_t index, r, g, b;
|
||||
return parse_level(line, index, r, g, b);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
@@ -152,7 +188,11 @@ WaterfallDesignerView::WaterfallDesignerView(
|
||||
}
|
||||
|
||||
WaterfallDesignerView::~WaterfallDesignerView() {
|
||||
if (!if_apply_setting) restore_current_profile();
|
||||
if (!if_apply_setting)
|
||||
restore_current_profile();
|
||||
else if (profile_backed_up_)
|
||||
delete_file(waterfalls_dir / u"wtf_des_bk.bk"); // kept the edit, drop the backup
|
||||
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
@@ -176,35 +216,68 @@ void WaterfallDesignerView::on_open_profile() {
|
||||
auto open_view = nav_.push<FileLoadView>(".txt");
|
||||
open_view->push_dir(waterfalls_dir);
|
||||
open_view->on_changed = [this](std::filesystem::path new_file_path) {
|
||||
on_profile_changed(new_file_path);
|
||||
// NB: FileLoadView calls this and then pops, same as text_prompt does.
|
||||
// Defer for the same two reasons - see on_create_new_profile().
|
||||
pending_profile_path = std::move(new_file_path);
|
||||
nav_.set_on_pop([this]() {
|
||||
on_profile_changed(pending_profile_path);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_profile_changed(std::filesystem::path new_profile_path) {
|
||||
current_profile_path = new_profile_path;
|
||||
bool WaterfallDesignerView::read_profile_file(const std::filesystem::path& path) {
|
||||
File profile_file;
|
||||
|
||||
if (profile_file.open(path)) return false;
|
||||
|
||||
profile_levels.clear();
|
||||
|
||||
File playlist_file;
|
||||
auto error = playlist_file.open(new_profile_path.string());
|
||||
|
||||
if (error) return;
|
||||
|
||||
menu_view.clear();
|
||||
auto reader = FileLineReader(playlist_file);
|
||||
auto reader = FileLineReader(profile_file);
|
||||
|
||||
for (const auto& line : reader) {
|
||||
// remove empty lines
|
||||
if (line == "\n" || line == "\r\n" || line == "\r") continue;
|
||||
profile_levels.push_back(line);
|
||||
auto entry = line;
|
||||
|
||||
// Strip the whole line terminator; the old code only dropped one char,
|
||||
// so CRLF files kept a stray '\r' in every entry.
|
||||
while (!entry.empty() && (entry.back() == '\n' || entry.back() == '\r'))
|
||||
entry.pop_back();
|
||||
|
||||
if (entry.empty()) continue;
|
||||
profile_levels.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
for (auto& line : profile_levels) {
|
||||
// remove line end \n etc
|
||||
if (line.length() > 0 && (line[line.length() - 1] == '\n' || line[line.length() - 1] == '\r')) {
|
||||
line = line.substr(0, line.length() - 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WaterfallDesignerView::write_profile_file(const std::filesystem::path& path) {
|
||||
File profile_file;
|
||||
|
||||
if (profile_file.open(path, false, true)) return false;
|
||||
|
||||
// FA_OPEN_ALWAYS leaves the position at end-of-file, so rewind before
|
||||
// truncating or the existing contents are kept and appended to.
|
||||
if (profile_file.seek(0).is_error()) return false;
|
||||
if (profile_file.truncate().is_error()) return false;
|
||||
|
||||
for (const auto& entry : profile_levels)
|
||||
if (profile_file.write_line(entry)) return false;
|
||||
|
||||
/* ~File() calls f_close(), which flushes, but its result is unreachable.
|
||||
* Sync explicitly so a full or yanked card is reported to the caller
|
||||
* instead of being announced as a successful save. */
|
||||
return !profile_file.sync();
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_profile_changed(const std::filesystem::path& new_profile_path) {
|
||||
// read_profile_file() only touches profile_levels once the open succeeded,
|
||||
// so a failure here leaves the previously loaded profile intact.
|
||||
if (!read_profile_file(new_profile_path)) {
|
||||
nav_.display_modal("Err", "open err");
|
||||
return;
|
||||
}
|
||||
|
||||
current_profile_path = new_profile_path;
|
||||
highlighted_index_ = 0;
|
||||
|
||||
button_save.hidden(false);
|
||||
button_add_level.hidden(false);
|
||||
button_remove_level.hidden(false);
|
||||
@@ -219,53 +292,52 @@ void WaterfallDesignerView::refresh_menu_view() {
|
||||
menu_view.clear();
|
||||
|
||||
for (const auto& line : profile_levels) {
|
||||
if (line.length() == 0 || line[0] == '#') {
|
||||
uint8_t index, r, g, b;
|
||||
|
||||
// index,R,G,B - anything else (comment, header, garbage) is shown greyed.
|
||||
if (parse_level(line, index, r, g, b)) {
|
||||
menu_view.add_item({line,
|
||||
ui::Color(r, g, b),
|
||||
&bitmap_icon_cwgen,
|
||||
[this](KeyEvent) {
|
||||
button_remove_level.focus();
|
||||
}});
|
||||
} else {
|
||||
menu_view.add_item({line,
|
||||
ui::Color::grey(),
|
||||
&bitmap_icon_notepad,
|
||||
[this](KeyEvent) {
|
||||
button_add_level.focus();
|
||||
}});
|
||||
} else {
|
||||
// index,R,G,B
|
||||
size_t pos = 0;
|
||||
size_t next_pos = 0;
|
||||
|
||||
// pass index
|
||||
next_pos = line.find(',', pos);
|
||||
if (next_pos == std::string::npos) continue;
|
||||
pos = next_pos + 1;
|
||||
|
||||
// r
|
||||
next_pos = line.find(',', pos);
|
||||
if (next_pos == std::string::npos) continue;
|
||||
uint8_t r = static_cast<uint8_t>(std::stoi(line.substr(pos, next_pos - pos)));
|
||||
pos = next_pos + 1;
|
||||
|
||||
// g
|
||||
next_pos = line.find(',', pos);
|
||||
if (next_pos == std::string::npos) continue;
|
||||
uint8_t g = static_cast<uint8_t>(std::stoi(line.substr(pos, next_pos - pos)));
|
||||
pos = next_pos + 1;
|
||||
|
||||
// b
|
||||
uint8_t b = static_cast<uint8_t>(std::stoi(line.substr(pos)));
|
||||
|
||||
ui::Color color = ui::Color(r, g, b);
|
||||
menu_view.add_item({line,
|
||||
color,
|
||||
&bitmap_icon_cwgen,
|
||||
[this](KeyEvent) {
|
||||
button_remove_level.focus();
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
// menu_view.clear() resets its own highlight to 0 but does not fire
|
||||
// on_highlight, so highlighted_index_ has to be re-synced by hand or the
|
||||
// edit/remove buttons act on a different row than the one the user sees.
|
||||
if (highlighted_index_ >= profile_levels.size())
|
||||
highlighted_index_ = profile_levels.empty() ? 0 : profile_levels.size() - 1;
|
||||
|
||||
if (!profile_levels.empty())
|
||||
menu_view.set_highlighted(highlighted_index_);
|
||||
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_apply_current_to_wtf() {
|
||||
std::filesystem::path system_read_path = "waterfall.txt";
|
||||
copy_file(current_profile_path, system_read_path);
|
||||
/* Take the backup lazily, right before the first overwrite of the live
|
||||
* gradient. Doing it in the constructor meant copy_file() (~1.8kB of stack)
|
||||
* ran underneath run_external_app(), which holds a 776 byte frame with a
|
||||
* File of its own - about 3.6kB of the 4kB M0 process stack. It also did SD
|
||||
* I/O even when the user just opened the app and backed straight out. */
|
||||
backup_current_profile();
|
||||
|
||||
/* NB: this used to copy_file(current_profile_path, "waterfall.txt"), which
|
||||
* costs ~1.8kB of stack (two FILs + a 512 byte block buffer) out of the 4kB
|
||||
* M0 process stack, on top of whatever UI callback we are nested under.
|
||||
* profile_levels is the authoritative copy of the profile anyway, so write
|
||||
* it straight out through a single File instead. */
|
||||
if (!write_profile_file(u"waterfall.txt")) return;
|
||||
|
||||
waterfall.load_gradient();
|
||||
|
||||
@@ -281,71 +353,82 @@ void WaterfallDesignerView::on_save_profile() {
|
||||
return;
|
||||
}
|
||||
|
||||
File profile_file;
|
||||
auto error = profile_file.open(current_profile_path.string(), false, false);
|
||||
|
||||
if (error) {
|
||||
nav_.display_modal("Err", "open err");
|
||||
// NB: not just "open err" any more - this now also covers a failed
|
||||
// truncate/write/flush, i.e. the file may be partially written.
|
||||
if (!write_profile_file(current_profile_path)) {
|
||||
nav_.display_modal("Err", "write failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// clear file
|
||||
profile_file.seek(0);
|
||||
profile_file.truncate();
|
||||
|
||||
// write new data
|
||||
for (const auto& entry : profile_levels) {
|
||||
profile_file.write_line(entry);
|
||||
}
|
||||
|
||||
nav_.display_modal("Save", "Saved profile\n" + current_profile_path.string());
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_add_level() {
|
||||
if (highlighted_index_ >= profile_levels.size()) return;
|
||||
if (profile_levels[highlighted_index_].empty()) return;
|
||||
if (profile_levels[highlighted_index_][0] == '#') return;
|
||||
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
|
||||
size_t insert_pos = highlighted_index_;
|
||||
std::string new_entry = "0,128,128,128";
|
||||
profile_levels.insert(profile_levels.begin() + insert_pos, new_entry);
|
||||
if (current_profile_path.empty()) return;
|
||||
|
||||
/* A freshly created profile has no rows at all, and the old guard
|
||||
* (highlighted_index_ >= size()) then refused with no feedback - so the
|
||||
* button looked dead on exactly the file you just made. Seed instead. */
|
||||
size_t insert_pos = std::min<size_t>(highlighted_index_, profile_levels.size());
|
||||
|
||||
// Don't push a level above a comment/header row; drop it after instead.
|
||||
if (insert_pos < profile_levels.size() && !is_color_level(profile_levels[insert_pos]))
|
||||
insert_pos++;
|
||||
|
||||
profile_levels.insert(profile_levels.begin() + insert_pos, "0,128,128,128");
|
||||
highlighted_index_ = insert_pos;
|
||||
|
||||
refresh_menu_view();
|
||||
on_edit_color();
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_remove_level() {
|
||||
if (highlighted_index_ >= profile_levels.size()) return;
|
||||
if (profile_levels[highlighted_index_].empty()) return;
|
||||
if (profile_levels[highlighted_index_][0] == '#') return;
|
||||
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
|
||||
if (!is_color_level(profile_levels[highlighted_index_])) return;
|
||||
|
||||
profile_levels.erase(profile_levels.begin() + highlighted_index_);
|
||||
refresh_menu_view();
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_edit_color() {
|
||||
if (highlighted_index_ >= profile_levels.size()) return;
|
||||
if (profile_levels[highlighted_index_].empty()) return;
|
||||
if (profile_levels[highlighted_index_][0] == '#') return;
|
||||
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
|
||||
if (!is_color_level(profile_levels[highlighted_index_])) return;
|
||||
|
||||
auto color_picker_view = nav_.push<WaterfallDesignerColorPickerView>(profile_levels[highlighted_index_]);
|
||||
color_picker_view->on_save = [this](std::string new_color) {
|
||||
profile_levels[highlighted_index_] = new_color;
|
||||
// Capture the row by value: highlighted_index_ can move (or the list can
|
||||
// shrink) while the picker is up, and the old code wrote to whatever
|
||||
// highlighted_index_ happened to be at save time with no bounds check.
|
||||
const size_t index = highlighted_index_;
|
||||
|
||||
auto color_picker_view = nav_.push<WaterfallDesignerColorPickerView>(profile_levels[index]);
|
||||
color_picker_view->on_save = [this, index](std::string new_color) {
|
||||
if (index >= profile_levels.size()) return;
|
||||
|
||||
profile_levels[index] = std::move(new_color);
|
||||
refresh_menu_view();
|
||||
on_apply_current_to_wtf();
|
||||
};
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::backup_current_profile() {
|
||||
std::filesystem::path curren_wtf_path = "waterfall.txt";
|
||||
std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk";
|
||||
copy_file(curren_wtf_path, backup_path);
|
||||
if (backup_attempted_) return;
|
||||
backup_attempted_ = true;
|
||||
|
||||
std::filesystem::path current_wtf_path = u"waterfall.txt";
|
||||
std::filesystem::path backup_path = waterfalls_dir / u"wtf_des_bk.bk";
|
||||
|
||||
if (!file_exists(current_wtf_path)) return;
|
||||
|
||||
profile_backed_up_ = copy_file(current_wtf_path, backup_path).ok();
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::restore_current_profile() {
|
||||
std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk";
|
||||
std::filesystem::path put_back_path = "waterfall.txt";
|
||||
copy_file(backup_path, put_back_path);
|
||||
// Only touch waterfall.txt if we actually took a backup on entry; otherwise
|
||||
// this would silently do nothing and leave the edited gradient installed.
|
||||
if (!profile_backed_up_) return;
|
||||
|
||||
std::filesystem::path backup_path = waterfalls_dir / u"wtf_des_bk.bk";
|
||||
|
||||
copy_file(backup_path, u"waterfall.txt");
|
||||
delete_file(backup_path);
|
||||
}
|
||||
|
||||
@@ -365,16 +448,35 @@ void WaterfallDesignerView::on_create_new_profile() {
|
||||
buffer += ".txt";
|
||||
}
|
||||
|
||||
File new_file;
|
||||
auto error = new_file.create(waterfalls_dir / buffer);
|
||||
if (error) {
|
||||
nav_.display_modal("Err", "create file err");
|
||||
auto new_path = waterfalls_dir / buffer;
|
||||
bool created;
|
||||
|
||||
{
|
||||
// Scoped so the FIL is closed (and its 512 byte sector cache is
|
||||
// off the stack) before anything re-opens the same file - _FS_LOCK
|
||||
// is 0, so FatFs will not stop us from double-opening it.
|
||||
File new_file;
|
||||
created = !new_file.create(new_path);
|
||||
}
|
||||
|
||||
/* NB: everything below has to run *after* the keyboard view is gone.
|
||||
* text_prompt()'s caller invokes this handler and then pops, so:
|
||||
* - pushing a modal from here would pop the modal, not the keyboard;
|
||||
* - and running on_profile_changed() inline stacks its File plus the
|
||||
* gradient write under the alphanum/button/dispatch frames, which
|
||||
* is what blows the 4kB M0 process stack ("Stack Overflow" guru).
|
||||
* set_on_pop() defers it to just after the pop, at a shallow depth. */
|
||||
if (!created) {
|
||||
nav_.set_on_pop([this]() {
|
||||
nav_.display_modal("Err", "create file err");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
profile_levels.clear();
|
||||
current_profile_path = waterfalls_dir / buffer;
|
||||
on_profile_changed(current_profile_path);
|
||||
pending_profile_path = new_path;
|
||||
nav_.set_on_pop([this]() {
|
||||
on_profile_changed(pending_profile_path);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -392,32 +494,7 @@ WaterfallDesignerColorPickerView::WaterfallDesignerColorPickerView(NavigationVie
|
||||
|
||||
progressbar.set_max(UINT8_MAX);
|
||||
|
||||
size_t pos = 0;
|
||||
size_t next_pos = 0;
|
||||
|
||||
// index
|
||||
next_pos = color_str.find(',', pos);
|
||||
if (next_pos != std::string::npos) {
|
||||
index_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
|
||||
pos = next_pos + 1;
|
||||
}
|
||||
|
||||
// r
|
||||
next_pos = color_str.find(',', pos);
|
||||
if (next_pos != std::string::npos) {
|
||||
red_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
|
||||
pos = next_pos + 1;
|
||||
}
|
||||
|
||||
// g
|
||||
next_pos = color_str.find(',', pos);
|
||||
if (next_pos != std::string::npos) {
|
||||
green_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
|
||||
pos = next_pos + 1;
|
||||
}
|
||||
|
||||
// b
|
||||
blue_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos)));
|
||||
parse_level(color_str_, index_, red_, green_, blue_);
|
||||
|
||||
field_red.set_value(red_);
|
||||
field_green.set_value(green_);
|
||||
@@ -466,19 +543,12 @@ void WaterfallDesignerColorPickerView::update_color_index() {
|
||||
green_ = static_cast<uint8_t>(field_green.value());
|
||||
blue_ = static_cast<uint8_t>(field_blue.value());
|
||||
|
||||
const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40};
|
||||
|
||||
Painter painter_instance_2;
|
||||
painter_instance_2.fill_rectangle(
|
||||
{preview_rect.left(), preview_rect.top(), preview_rect.width(), preview_rect.height()},
|
||||
ui::Color(red_, green_, blue_));
|
||||
// Repaint through the normal dirty pass instead of driving the display from
|
||||
// inside an on_change handler; paint() below already draws the swatch.
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void WaterfallDesignerColorPickerView::paint(Painter& painter) {
|
||||
// this is not duplicated code.
|
||||
// because need to display color when enter,
|
||||
// but it is too early to call update_color() in the constructor.
|
||||
|
||||
const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40};
|
||||
|
||||
painter.fill_rectangle(
|
||||
@@ -487,14 +557,12 @@ void WaterfallDesignerColorPickerView::paint(Painter& painter) {
|
||||
}
|
||||
|
||||
std::string WaterfallDesignerColorPickerView::build_color_str() {
|
||||
size_t index_pos = color_str_.find(',');
|
||||
if (index_pos != std::string::npos) {
|
||||
return color_str_.substr(0, index_pos + 1) +
|
||||
to_string_dec_uint(red_) + "," +
|
||||
to_string_dec_uint(green_) + "," +
|
||||
to_string_dec_uint(blue_);
|
||||
}
|
||||
return to_string_dec_uint(index_) + "," + to_string_dec_uint(red_) + "," + to_string_dec_uint(green_) + "," + to_string_dec_uint(blue_);
|
||||
// Always emit index_ - the old version kept the index substring from the
|
||||
// input line, so edits made with field_index were silently discarded.
|
||||
return to_string_dec_uint(index_) + "," +
|
||||
to_string_dec_uint(red_) + "," +
|
||||
to_string_dec_uint(green_) + "," +
|
||||
to_string_dec_uint(blue_);
|
||||
}
|
||||
|
||||
} /* namespace ui::external_app::waterfall_designer */
|
||||
|
||||
+14
-1
@@ -217,18 +217,31 @@ class WaterfallDesignerView : public View {
|
||||
void restore_current_profile();
|
||||
void on_create_new_profile();
|
||||
void on_open_profile();
|
||||
void on_profile_changed(std::filesystem::path new_profile_path);
|
||||
void on_profile_changed(const std::filesystem::path& new_profile_path);
|
||||
void on_save_profile();
|
||||
void on_add_level();
|
||||
void on_remove_level();
|
||||
void on_edit_color();
|
||||
|
||||
/* Each of these owns the only File in its call chain. Keeping them as
|
||||
* separate frames matters: a File carries a 512 byte FIL sector cache
|
||||
* (_FS_TINY is 0) and the M0 process stack is 4kB, so if read_profile_file
|
||||
* merged into on_profile_changed its 784 byte frame would stay live across
|
||||
* the copy_file() in on_apply_current_to_wtf() - 3.6kB of 4kB instead of
|
||||
* 2.8kB. GCC does not inline them at -O2 today; noinline just pins that so
|
||||
* a heuristic change can't quietly eat the headroom. */
|
||||
__attribute__((noinline)) bool read_profile_file(const std::filesystem::path& path);
|
||||
__attribute__((noinline)) bool write_profile_file(const std::filesystem::path& path);
|
||||
|
||||
void refresh_menu_view();
|
||||
|
||||
void on_apply_current_to_wtf(); // will restore if didn't apple, when distruct
|
||||
void on_apply_setting(); // apply set
|
||||
|
||||
bool if_apply_setting{false};
|
||||
bool backup_attempted_{false};
|
||||
bool profile_backed_up_{false};
|
||||
std::filesystem::path pending_profile_path{}; // set by the New dialog, consumed on_pop
|
||||
/*NB:
|
||||
this works as:
|
||||
each time you change color, it apply as file realtime
|
||||
|
||||
@@ -28,10 +28,15 @@
|
||||
|
||||
#include "max2831.hpp"
|
||||
|
||||
#include "portapack_adc.hpp"
|
||||
|
||||
#include "hackrf_hal.hpp"
|
||||
#include "hackrf_gpio.hpp"
|
||||
using namespace hackrf::one;
|
||||
|
||||
#include "gpio.hpp"
|
||||
using namespace gpio_control;
|
||||
|
||||
#include "ch.h"
|
||||
#include "hal.h"
|
||||
|
||||
@@ -151,7 +156,7 @@ void MAX2831::set_mode(const Mode mode) {
|
||||
* RX: ENABLE=1, RXTX=0
|
||||
* TX: ENABLE=1, RXTX=1
|
||||
*
|
||||
* Note: gpio_max2831_rx_enable is the RXTX mode select pin.
|
||||
* Note: max2831_rxtx_enable is the RXTX mode select pin.
|
||||
* RXTX=0 selects RX, RXTX=1 selects TX.
|
||||
*/
|
||||
|
||||
@@ -174,26 +179,26 @@ void MAX2831::set_mode(const Mode mode) {
|
||||
switch (mode) {
|
||||
default:
|
||||
case Mode::Shutdown:
|
||||
gpio_max2831_rx_enable.write(0); /* RXTX=0 */
|
||||
gpio_max283x_enable.write(0); /* ENABLE=0 */
|
||||
max2831_rxtx_enable.setInactive(); /* RXTX=0 */
|
||||
max283x_enable.setInactive(); /* ENABLE=0 */
|
||||
set_rssi_mux(0);
|
||||
break;
|
||||
case Mode::Standby:
|
||||
gpio_max2831_rx_enable.write(1); /* RXTX=1 */
|
||||
gpio_max283x_enable.write(0); /* ENABLE=0 */
|
||||
max2831_rxtx_enable.setActive(); /* RXTX=1 */
|
||||
max283x_enable.setInactive(); /* ENABLE=0 */
|
||||
set_rssi_mux(0);
|
||||
break;
|
||||
case Mode::Transmit:
|
||||
case Mode::Tx_Calibration:
|
||||
gpio_max2831_rx_enable.write(1); /* RXTX=1 for TX */
|
||||
gpio_max283x_enable.write(1); /* ENABLE=1 */
|
||||
max2831_rxtx_enable.setActive(); /* RXTX=1 for TX */
|
||||
max283x_enable.setActive(); /* ENABLE=1 */
|
||||
set_rssi_mux(2); // transmit power
|
||||
break;
|
||||
case Mode::Receive:
|
||||
case Mode::Rx_Calibration:
|
||||
gpio_max2831_rx_enable.write(0); /* RXTX=0 for RX */
|
||||
gpio_max283x_enable.write(1); /* ENABLE=1 */
|
||||
set_rssi_mux(1); // RSSI
|
||||
max2831_rxtx_enable.setInactive(); /* RXTX=0 for RX */
|
||||
max283x_enable.setActive(); /* ENABLE=1 */
|
||||
set_rssi_mux(1); // RSSI
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -227,8 +232,15 @@ void MAX2831::set_lna_gain(const int_fast8_t db) {
|
||||
} else {
|
||||
gain_val = REG11_LNA_GAIN_M33;
|
||||
}
|
||||
|
||||
max2831_rxhp.setActive(); // Fast DC offset compensation
|
||||
|
||||
set_reg_field(11, REG11_LNA_GAIN_MASK, gain_val);
|
||||
flush_reg(11);
|
||||
|
||||
chThdSleepMicroseconds(2);
|
||||
|
||||
max2831_rxhp.setInactive();
|
||||
}
|
||||
|
||||
void MAX2831::set_vga_gain(const int_fast8_t db) {
|
||||
@@ -237,10 +249,18 @@ void MAX2831::set_vga_gain(const int_fast8_t db) {
|
||||
if ((db & 0x1) || db > 62) {
|
||||
return; /* Invalid: must be even and <= 62 */
|
||||
}
|
||||
|
||||
int_fast8_t db_clipped = std::max(0, std::min(62, (int)db));
|
||||
uint16_t value = (db_clipped >> 1) & 0x1f;
|
||||
|
||||
max2831_rxhp.setActive(); // Fast DC offset compensation
|
||||
|
||||
set_reg_field(11, REG11_RXVGA_GAIN_MASK, value);
|
||||
flush_reg(11);
|
||||
|
||||
chThdSleepMicroseconds(2);
|
||||
|
||||
max2831_rxhp.setInactive();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -365,9 +385,9 @@ void MAX2831::set_lpf_rf_bandwidth_rx(const uint32_t bandwidth_minimum) {
|
||||
|
||||
void MAX2831::set_lpf_rf_bandwidth_tx(const uint32_t bandwidth_minimum) {
|
||||
_desired_lpf_bw = bandwidth_minimum;
|
||||
#ifdef PRALINE
|
||||
gpio_control::aa_en.clear(); // Disable external AA filter for wideband operations
|
||||
#endif
|
||||
|
||||
gpio_control::aa_en.setInactive(); // Disable external AA filter for wideband operations
|
||||
|
||||
if (_mode == Mode::Transmit || _mode == Mode::Tx_Calibration) {
|
||||
set_lpf_bandwidth_internal(bandwidth_minimum);
|
||||
}
|
||||
@@ -461,13 +481,56 @@ void MAX2831::set_rx_buff_vcm(const size_t v) {
|
||||
}
|
||||
|
||||
int8_t MAX2831::temp_sense() {
|
||||
/* MAX2831 temperature sensor can be read via RSSI MUX.
|
||||
* This would require:
|
||||
* 1. Switch RSSI_MUX to temperature mode
|
||||
* 2. Read the ADC
|
||||
* 3. Switch back to RSSI mode
|
||||
* For now, return a placeholder value. */
|
||||
return 25; /* Room temperature placeholder */
|
||||
bool not_in_rx = (_mode != Mode::Receive && _mode != Mode::Rx_Calibration);
|
||||
|
||||
if (not_in_rx) {
|
||||
max2831_rxtx_enable.setInactive();
|
||||
max283x_enable.setActive();
|
||||
chThdSleepMilliseconds(1);
|
||||
}
|
||||
|
||||
set_rssi_mux(3);
|
||||
chThdSleepMilliseconds(5);
|
||||
|
||||
uint32_t saved_cr = LPC_ADC1->CR;
|
||||
uint32_t cr_temp = saved_cr;
|
||||
cr_temp &= ~0xFF;
|
||||
cr_temp |= (1 << portapack::adc1_rssi_input);
|
||||
|
||||
if (((cr_temp >> 8) & 0xFF) == 0) {
|
||||
cr_temp |= (0xFF << 8);
|
||||
}
|
||||
|
||||
cr_temp &= ~(1 << 16);
|
||||
cr_temp |= (1 << 21);
|
||||
cr_temp &= ~(7 << 24);
|
||||
cr_temp |= (1 << 24);
|
||||
|
||||
LPC_ADC1->CR = cr_temp;
|
||||
|
||||
uint32_t val;
|
||||
while (((val = LPC_ADC1->DR[portapack::adc1_rssi_input]) & (1UL << 31)) == 0) {
|
||||
__asm__("nop");
|
||||
}
|
||||
|
||||
uint32_t adc_raw = (val >> 6) & 0x3FF;
|
||||
|
||||
LPC_ADC1->CR = saved_cr;
|
||||
|
||||
if (not_in_rx) {
|
||||
set_mode(_mode);
|
||||
} else {
|
||||
set_rssi_mux(1);
|
||||
}
|
||||
|
||||
float v_adc = (adc_raw / 1023.0f) * 3.3f;
|
||||
|
||||
constexpr float V_25_CELSIUS = 1.185f;
|
||||
constexpr float SLOPE = 0.003f;
|
||||
|
||||
float temp = 25.0f + ((v_adc - V_25_CELSIUS) / SLOPE);
|
||||
|
||||
return static_cast<int8_t>(std::max(-128.0f, std::min(127.0f, temp)));
|
||||
}
|
||||
|
||||
reg_t MAX2831::read(const address_t reg_num) {
|
||||
|
||||
@@ -19,12 +19,17 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef PRALINE // not praline
|
||||
|
||||
#include "max2837.hpp"
|
||||
|
||||
#include "hackrf_hal.hpp"
|
||||
#include "hackrf_gpio.hpp"
|
||||
using namespace hackrf::one;
|
||||
|
||||
#include "gpio.hpp"
|
||||
using namespace gpio_control;
|
||||
|
||||
#include "ch.h"
|
||||
#include "hal.h"
|
||||
|
||||
@@ -99,12 +104,6 @@ constexpr uint32_t pll_factor = 1.0 / (4.0 / 3.0 / reference_frequency) + 0.5;
|
||||
void MAX2837::init() {
|
||||
set_mode(Mode::Shutdown);
|
||||
|
||||
#ifndef PRALINE
|
||||
gpio_max283x_enable.output();
|
||||
gpio_max2837_rxenable.output();
|
||||
gpio_max2837_txenable.output();
|
||||
#endif
|
||||
|
||||
_map.r.tx_gain.TXVGA_GAIN_SPI_EN = 1;
|
||||
_map.r.tx_gain.TXVGA_GAIN_MSB_SPI_EN = 1;
|
||||
_map.r.tx_gain.TXVGA_GAIN_SPI = 0x00;
|
||||
@@ -159,12 +158,6 @@ void MAX2837::set_tx_LO_iq_phase_calibration(const size_t v) {
|
||||
// TX calibration , Logic pins , ENABLE, RXENABLE, TXENABLE = 1,0,1 (5dec), and Reg address 16, D1 (CAL mode 1):DO (CHIP ENABLE 1)
|
||||
set_mode(Mode::Tx_Calibration); // write to ram 3 LOGIC Pins .
|
||||
|
||||
#ifndef PRALINE
|
||||
gpio_max283x_enable.output();
|
||||
gpio_max2837_rxenable.output();
|
||||
gpio_max2837_txenable.output();
|
||||
#endif
|
||||
|
||||
_map.r.spi_en.CAL_SPI = 1; // Register Settings reg address 16, D1 (CAL mode 1)
|
||||
_map.r.spi_en.EN_SPI = 1; // Register Settings reg address 16, DO (CHIP ENABLE 1)
|
||||
flush_one(Register::SPI_EN);
|
||||
@@ -214,7 +207,7 @@ void MAX2837::set_mode(const Mode mode) { // We set up the 3 Logic Pins ENABLE,
|
||||
_mode = mode;
|
||||
|
||||
Mask mask = mode_mask(mode);
|
||||
gpio_max283x_enable.write(toUType(mask) & toUType(Mask::Enable));
|
||||
max283x_enable.write(toUType(mask) & toUType(Mask::Enable));
|
||||
gpio_max2837_rxenable.write(toUType(mask) & toUType(Mask::RxEnable));
|
||||
gpio_max2837_txenable.write(toUType(mask) & toUType(Mask::TxEnable));
|
||||
}
|
||||
@@ -352,12 +345,6 @@ void MAX2837::set_rx_LO_iq_phase_calibration(const size_t v) {
|
||||
// RX calibration , Logic pins , ENABLE, RXENABLE, TXENABLE = 1,1,0 (3dec), and Reg address 16, D1 (CAL mode 1):DO (CHIP ENABLE 1)
|
||||
set_mode(Mode::Rx_Calibration); // write to ram 3 LOGIC Pins .
|
||||
|
||||
#ifndef PRALINE
|
||||
gpio_max283x_enable.output();
|
||||
gpio_max2837_rxenable.output();
|
||||
gpio_max2837_txenable.output();
|
||||
#endif
|
||||
|
||||
_map.r.spi_en.CAL_SPI = 1; // Register Settings reg address 16, D1 (CAL mode 1)
|
||||
_map.r.spi_en.EN_SPI = 1; // Register Settings reg address 16, DO (CHIP ENABLE 1)
|
||||
flush_one(Register::SPI_EN);
|
||||
@@ -419,3 +406,5 @@ int8_t MAX2837::temp_sense() {
|
||||
}
|
||||
|
||||
} // namespace max2837
|
||||
|
||||
#endif // not PRALINE
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
#ifndef PRALINE // not praline
|
||||
|
||||
#include "max2839.hpp"
|
||||
|
||||
@@ -26,6 +27,9 @@
|
||||
#include "hackrf_gpio.hpp"
|
||||
using namespace hackrf::one;
|
||||
|
||||
#include "gpio.hpp"
|
||||
using namespace gpio_control;
|
||||
|
||||
#include "ch.h"
|
||||
#include "hal.h"
|
||||
|
||||
@@ -100,11 +104,6 @@ static int_fast8_t requested_rx_vga_gain = 0;
|
||||
void MAX2839::init() {
|
||||
set_mode(Mode::Shutdown);
|
||||
|
||||
#ifndef PRALINE
|
||||
gpio_max283x_enable.output();
|
||||
gpio_max2839_rxtx.output();
|
||||
#endif
|
||||
|
||||
_map.r.rxrf_1.MIMOmode = 1; /* enable RXINB */
|
||||
|
||||
_map.r.pa_drv.TXVGA_GAIN_SPI_EN = 1;
|
||||
@@ -154,11 +153,6 @@ void MAX2839::set_tx_LO_iq_phase_calibration(const size_t v) {
|
||||
// TX calibration , 2 x Logic pins , ENABLE, RXENABLE = 1,0, (2dec), and Reg address 16, D1 (CAL mode 1):DO (CHIP ENABLE 1)
|
||||
set_mode(Mode::Tx_Calibration); // write to ram 3 LOGIC Pins .
|
||||
|
||||
#ifndef PRALINE
|
||||
gpio_max283x_enable.output(); // max2839 has only 2 x pins + regs to decide mode.
|
||||
gpio_max2839_rxtx.output(); // Here is combined rx & tx pin in one port.
|
||||
#endif
|
||||
|
||||
_map.r.spi_en.CAL_SPI = 1; // Register Settings reg address 16, D1 (CAL mode 1)
|
||||
_map.r.spi_en.EN_SPI = 1; // Register Settings reg address 16, DO (CHIP ENABLE 1)
|
||||
flush_one(Register::SPI_EN);
|
||||
@@ -207,7 +201,7 @@ void MAX2839::set_mode(const Mode mode) {
|
||||
_mode = mode;
|
||||
|
||||
Mask mask = mode_mask(mode);
|
||||
gpio_max283x_enable.write(toUType(mask) & toUType(Mask::Enable));
|
||||
max283x_enable.write(toUType(mask) & toUType(Mask::Enable));
|
||||
gpio_max2839_rxtx.write(toUType(mask) & toUType(Mask::RxTx));
|
||||
}
|
||||
|
||||
@@ -395,11 +389,6 @@ void MAX2839::set_rx_LO_iq_phase_calibration(const size_t v) {
|
||||
// RX calibration , Logic pins , ENABLE, RXENABLE, TXENABLE = 1,1,0 (3dec), and Reg address 16, D1 (CAL mode 1):DO (CHIP ENABLE 1)
|
||||
set_mode(Mode::Rx_Calibration); // write to ram 3 LOGIC Pins .
|
||||
|
||||
#ifndef PRALINE
|
||||
gpio_max283x_enable.output(); // max2839 has only 2 x pins + regs to decide mode.
|
||||
gpio_max2839_rxtx.output(); // Here is combined rx & tx pin in one port.
|
||||
#endif
|
||||
|
||||
_map.r.spi_en.CAL_SPI = 1; // Register Settings reg address 16, D1 (CAL mode 1)
|
||||
_map.r.spi_en.EN_SPI = 1; // Register Settings reg address 16, DO (CHIP ENABLE 1)
|
||||
flush_one(Register::SPI_EN);
|
||||
@@ -446,4 +435,5 @@ int8_t MAX2839::temp_sense() {
|
||||
return std::min(127, (int)(value * 4.31 - 40)); // reg value is 0 to 31; possible return range is -40 C to 127 C
|
||||
}
|
||||
|
||||
} // namespace max2839
|
||||
} // namespace max2839
|
||||
#endif // not PRALINE
|
||||
@@ -26,9 +26,13 @@
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "hackrf_hal.hpp"
|
||||
|
||||
#include "hackrf_gpio.hpp"
|
||||
using namespace hackrf::one;
|
||||
|
||||
#include "gpio.hpp"
|
||||
using namespace gpio_control;
|
||||
|
||||
#include "hal.h"
|
||||
|
||||
#ifdef PRALINE
|
||||
@@ -176,13 +180,11 @@ struct SynthConfig {
|
||||
|
||||
void RFFC507x::init() {
|
||||
#ifdef PRALINE
|
||||
gpio_control::rf5072_mix_en.setActive(); // RF5072_MIX_EN
|
||||
rf5072_mix_en.setActive(); // RF5072_MIX_EN
|
||||
#endif
|
||||
|
||||
gpio_rffc5072_resetx.set();
|
||||
#ifndef PRALINE
|
||||
gpio_rffc5072_resetx.output();
|
||||
#endif
|
||||
rffc5072_resetx.setInactive();
|
||||
|
||||
reset();
|
||||
|
||||
_bus.init();
|
||||
@@ -195,9 +197,9 @@ void RFFC507x::reset() {
|
||||
/* TODO: Is RESETB pin ignored if sdi_ctrl.sipin=1? Programming guide
|
||||
* description of sdi_ctrl.sipin suggests the pin is not ignored.
|
||||
*/
|
||||
gpio_rffc5072_resetx.clear();
|
||||
rffc5072_resetx.setActive();
|
||||
halPolledDelay(ticks_during_reset);
|
||||
gpio_rffc5072_resetx.set();
|
||||
rffc5072_resetx.setInactive();
|
||||
halPolledDelay(ticks_after_reset);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,50 +23,44 @@
|
||||
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "hackrf_gpio.hpp"
|
||||
using namespace hackrf::one;
|
||||
#include "gpio.hpp"
|
||||
using namespace gpio_control;
|
||||
|
||||
namespace rffc507x {
|
||||
namespace spi {
|
||||
|
||||
void SPI::init() {
|
||||
gpio_rffc5072_select.set();
|
||||
gpio_rffc5072_clock.clear();
|
||||
|
||||
#ifndef PRALINE
|
||||
gpio_rffc5072_select.output();
|
||||
gpio_rffc5072_clock.output();
|
||||
#endif
|
||||
gpio_rffc5072_data.input();
|
||||
|
||||
gpio_rffc5072_data.clear();
|
||||
rffc5072_select.setInactive();
|
||||
rffc5072_clock.setInactive();
|
||||
rffc5072_sdata.input();
|
||||
rffc5072_sdata.setInactive();
|
||||
}
|
||||
|
||||
inline void SPI::select(const bool active) {
|
||||
gpio_rffc5072_select.write(!active);
|
||||
rffc5072_select.setState(active);
|
||||
}
|
||||
|
||||
inline void SPI::direction_out() {
|
||||
gpio_rffc5072_data.output();
|
||||
rffc5072_sdata.output();
|
||||
}
|
||||
|
||||
inline void SPI::direction_in() {
|
||||
gpio_rffc5072_data.input();
|
||||
rffc5072_sdata.input();
|
||||
}
|
||||
|
||||
inline void SPI::write_bit(const bit_t value) {
|
||||
gpio_rffc5072_data.write(value);
|
||||
rffc5072_sdata.write(value);
|
||||
}
|
||||
|
||||
inline bit_t SPI::read_bit() {
|
||||
return gpio_rffc5072_data.read() & 1;
|
||||
return rffc5072_sdata.read() & 1;
|
||||
}
|
||||
|
||||
inline bit_t SPI::transfer_bit(const bit_t bit_out) {
|
||||
gpio_rffc5072_clock.clear();
|
||||
rffc5072_clock.setInactive();
|
||||
write_bit(bit_out);
|
||||
const bit_t bit_in = read_bit();
|
||||
gpio_rffc5072_clock.set();
|
||||
rffc5072_clock.setActive();
|
||||
return bit_in;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,13 +57,13 @@ using asahi_kasei::ak4951::AK4951;
|
||||
#include "battery.hpp"
|
||||
|
||||
#include "gpio.hpp"
|
||||
using namespace gpio_control;
|
||||
|
||||
extern "C" {
|
||||
#include "platform_detect.h"
|
||||
|
||||
#ifdef PRALINE
|
||||
#include "fpga_bridge.h"
|
||||
#include "board.h"
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -74,11 +74,9 @@ const char* init_error = nullptr;
|
||||
portapack::IO io{
|
||||
portapack::gpio_dir,
|
||||
portapack::gpio_lcd_rdx,
|
||||
portapack::gpio_lcd_wrx,
|
||||
portapack::gpio_io_stbx,
|
||||
portapack::gpio_addr,
|
||||
portapack::gpio_lcd_te,
|
||||
portapack::gpio_dfu,
|
||||
};
|
||||
|
||||
portapack::BacklightCAT4004 backlight_cat4004;
|
||||
@@ -102,8 +100,6 @@ AK4951 audio_codec_ak4951{i2c0, 0x12};
|
||||
ReceiverModel receiver_model;
|
||||
TransmitterModel transmitter_model;
|
||||
|
||||
TemperatureLogger temperature_logger;
|
||||
|
||||
bool antenna_bias{false};
|
||||
uint32_t bl_tick_counter{0};
|
||||
uint16_t touch_threshold{32};
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
|
||||
#include "radio.hpp"
|
||||
#include "clock_manager.hpp"
|
||||
#include "temperature_logger.hpp"
|
||||
#include "theme.hpp"
|
||||
|
||||
/* TODO: This would be better as a class to add
|
||||
@@ -69,8 +68,6 @@ extern TransmitterModel transmitter_model;
|
||||
extern uint32_t bl_tick_counter;
|
||||
extern uint16_t touch_threshold;
|
||||
|
||||
extern TemperatureLogger temperature_logger;
|
||||
|
||||
/* Get or set the antenna_bias flag.
|
||||
* NB: Does not actually update the radio state. */
|
||||
void set_antenna_bias(const bool v);
|
||||
|
||||
@@ -76,8 +76,8 @@ static constexpr uint32_t ssp_scr(
|
||||
/* MAX2831 uses 9-bit SPI transfers */
|
||||
static constexpr SPIConfig ssp_config_max283x = {
|
||||
.end_cb = NULL,
|
||||
.ssport = gpio_max283x_select.port(),
|
||||
.sspad = gpio_max283x_select.pad(),
|
||||
.ssport = map_max283x_select.gpio_port,
|
||||
.sspad = map_max283x_select.gpio_pad,
|
||||
.cr0 =
|
||||
CR0_CLOCKRATE(ssp_scr(ssp1_pclk_f, ssp1_cpsr, max283x_spi_f) + 3) | CR0_FRFSPI | CR0_DSS9BIT,
|
||||
.cpsr = ssp1_cpsr,
|
||||
@@ -95,8 +95,8 @@ void set_rx_buff_vcm(const size_t v) {
|
||||
/* MAX2837/MAX2839 use 16-bit SPI transfers */
|
||||
static constexpr SPIConfig ssp_config_max283x = {
|
||||
.end_cb = NULL,
|
||||
.ssport = gpio_max283x_select.port(),
|
||||
.sspad = gpio_max283x_select.pad(),
|
||||
.ssport = map_max283x_select.gpio_port,
|
||||
.sspad = map_max283x_select.gpio_pad,
|
||||
.cr0 =
|
||||
CR0_CLOCKRATE(ssp_scr(ssp1_pclk_f, ssp1_cpsr, max283x_spi_f) + 3) | CR0_FRFSPI | CR0_DSS16BIT,
|
||||
.cpsr = ssp1_cpsr,
|
||||
@@ -144,6 +144,10 @@ static rf::Direction cached_direction = rf::Direction::Receive;
|
||||
static bool cached_rf_amp = false;
|
||||
static int_fast8_t cached_lna_gain = 0;
|
||||
static int_fast8_t cached_vga_gain = 0;
|
||||
/* FPGA quarter-rate shift mode currently programmed, in gateware encoding
|
||||
* (0b00 none / 0b11 up / 0b01 down). The baseband filter width depends on it,
|
||||
* so ReceiverModel reads it back through get_quarter_shift(). */
|
||||
static uint8_t cached_quarter_shift = 0;
|
||||
#endif
|
||||
|
||||
void init() {
|
||||
@@ -182,18 +186,15 @@ void init() {
|
||||
|
||||
fpga_set_mode(FPGA_MODE_RX);
|
||||
|
||||
// These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation.
|
||||
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim
|
||||
|
||||
// RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift).
|
||||
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
|
||||
|
||||
/* RX Mode: Initialize DC Block parameters to standard Praline values.
|
||||
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
|
||||
*/
|
||||
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
|
||||
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
|
||||
/* Boot register state, matching fpga_init() in hackrf/firmware/common/fpga.c:
|
||||
* DC block on, no PRBS, no external trigger, no quarter shift, TX NCO off.
|
||||
* The decimation ratio and the quarter shift are programmed later by
|
||||
* ClockManager::set_sampling_frequency() and set_tuning_frequency(). */
|
||||
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM = no decimation
|
||||
fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00); // quarter shift off
|
||||
fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00);
|
||||
cached_quarter_shift = 0;
|
||||
|
||||
ssp1_arbiter.invalidate();
|
||||
chThdSleepMilliseconds(10); // Let FPGA registers settle
|
||||
@@ -224,13 +225,13 @@ void set_direction(const rf::Direction new_direction) {
|
||||
fpga_debug_register_write(FPGA_REG_TX_PHASE_STEP, 0x00);
|
||||
} else {
|
||||
fpga_set_mode(FPGA_MODE_RX);
|
||||
// RX Mode: Ensure NCO is disabled and reset digital gain
|
||||
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
|
||||
/* RX Mode: Initialize DC Block parameters to standard Praline values.
|
||||
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
|
||||
*/
|
||||
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
|
||||
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
|
||||
/* RX Mode: DC block on, TX NCO off. The quarter shift is re-applied by
|
||||
* set_tuning_frequency(); clear it here so a stale TX/RX transition
|
||||
* cannot leave a rotation programmed with no matching LO offset. */
|
||||
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
|
||||
fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00);
|
||||
fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00);
|
||||
cached_quarter_shift = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -309,7 +310,19 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
|
||||
final_frequency = final_frequency + portapack::persistent_memory::config_freq_rx_correction();
|
||||
}
|
||||
|
||||
#ifdef PRALINE
|
||||
/* The PRALINE tuning tables offset the analogue passband by a quarter of
|
||||
* the ADC sample rate and have the FPGA rotate it back to DC, so the
|
||||
* planner needs to know the AFE rate. See tuning.cpp. */
|
||||
const uint32_t afe_rate = portapack::clock_manager.get_sampling_frequency()
|
||||
<< portapack::clock_manager.get_resampling_n();
|
||||
const auto tuning_config = tuning::config::create(
|
||||
final_frequency,
|
||||
afe_rate,
|
||||
direction == rf::Direction::Transmit);
|
||||
#else
|
||||
const auto tuning_config = tuning::config::create(final_frequency);
|
||||
#endif
|
||||
if (tuning_config.is_valid()) {
|
||||
first_if.disable();
|
||||
|
||||
@@ -339,6 +352,22 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
|
||||
LPC_GPIO->CLR[0] = (1 << 13); // SGPIO12 = 0 (Q normal)
|
||||
}
|
||||
|
||||
/* Program the FPGA's quarter-rate shift to match the offset the tuning
|
||||
* table just applied to the analogue centre frequency. The gateware
|
||||
* (hackrf/firmware/fpga/top/standard.py) takes both bits from the top
|
||||
* of register 0x03 (rx_pstep):
|
||||
* rx_pstep[6] -> quarter_shift.enable
|
||||
* rx_pstep[7] -> quarter_shift.up
|
||||
* which is exactly fpga_set_rx_quarter_shift_mode() in
|
||||
* hackrf/firmware/common/fpga.c: write (mode & 0b11) << 6.
|
||||
*
|
||||
* These two settings MUST be programmed together. Tuning off-centre
|
||||
* without the rotation puts the signal outside the decimation filter's
|
||||
* passband and it disappears entirely; rotating without the offset
|
||||
* moves the wanted signal off DC by the same amount. */
|
||||
cached_quarter_shift = tuning_config.quarter_shift;
|
||||
fpga_debug_register_write(FPGA_REG_RX_PSTEP, (cached_quarter_shift & 0b11) << 6);
|
||||
|
||||
ssp1_arbiter.invalidate();
|
||||
#else
|
||||
baseband_cpld.set_invert(mixer_invert ^ baseband_invert);
|
||||
@@ -469,6 +498,10 @@ int_fast8_t get_cached_lna_gain() {
|
||||
int_fast8_t get_cached_vga_gain() {
|
||||
return cached_vga_gain;
|
||||
}
|
||||
|
||||
uint8_t get_cached_quarter_shift() {
|
||||
return cached_quarter_shift;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace first_if {
|
||||
@@ -570,18 +603,11 @@ void register_write(const size_t register_number, uint32_t value) {
|
||||
void init() {
|
||||
fpga_set_mode(FPGA_MODE_RX);
|
||||
|
||||
// These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation.
|
||||
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim
|
||||
|
||||
// RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift).
|
||||
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
|
||||
|
||||
/* RX Mode: Initialize DC Block parameters to standard Praline values.
|
||||
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
|
||||
*/
|
||||
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
|
||||
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
|
||||
/* Same boot state as fpga_init() in hackrf/firmware/common/fpga.c. */
|
||||
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM = no decimation
|
||||
fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00); // quarter shift off
|
||||
fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00);
|
||||
|
||||
ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer
|
||||
}
|
||||
|
||||
@@ -126,6 +126,10 @@ rf::Direction get_cached_direction();
|
||||
bool get_cached_rf_amp();
|
||||
int_fast8_t get_cached_lna_gain();
|
||||
int_fast8_t get_cached_vga_gain();
|
||||
|
||||
/* FPGA RX quarter-rate shift currently programmed, in gateware encoding:
|
||||
* 0b00 none, 0b11 up, 0b01 down. */
|
||||
uint8_t get_cached_quarter_shift();
|
||||
#endif
|
||||
|
||||
namespace sgpio {
|
||||
|
||||
@@ -88,6 +88,15 @@ void ReceiverModel::set_target_frequency(rf::Frequency f) {
|
||||
update_tuning_frequency();
|
||||
}
|
||||
|
||||
void ReceiverModel::set_target_frequency_with_hidden_offset(
|
||||
rf::Frequency f,
|
||||
rf::Frequency offset) {
|
||||
persistent_memory::set_target_frequency(f);
|
||||
settings_.frequency_app_override = f;
|
||||
hidden_offset = offset;
|
||||
update_tuning_frequency();
|
||||
}
|
||||
|
||||
uint32_t ReceiverModel::baseband_bandwidth() const {
|
||||
return settings_.baseband_bandwidth;
|
||||
}
|
||||
@@ -244,10 +253,24 @@ void ReceiverModel::enable() {
|
||||
radio::set_direction(rf::Direction::Receive);
|
||||
|
||||
#ifdef PRALINE
|
||||
/* Anchor the Common Mode Voltage (VCM) to 1.2V.
|
||||
* This stabilizes the electrical floor of the I/Q signals.
|
||||
*/
|
||||
radio::set_rx_buff_vcm(1);
|
||||
/* MAX2831 RX IQ common-mode voltage (register 15).
|
||||
*
|
||||
* 0 = 1.1 V, 1 = 1.2 V, 2 = 1.3 V, 3 = 1.45 V.
|
||||
*
|
||||
* The reference firmware leaves this alone: max2831.c's default register
|
||||
* table has reg 15 = 0x0145 (1.1 V) and the line that would raise it is
|
||||
* commented out ("maximum rx output common-mode voltage"). Mayhem used to
|
||||
* force 1.2 V here on the theory that it "stabilises the electrical floor
|
||||
* of the I/Q signals" -- plausible, but never measured, and it was the
|
||||
* last remaining RF-path setting where this branch disagreed with the
|
||||
* configuration that is proven to receive ADS-B on this board.
|
||||
*
|
||||
* Set to 0 to match the reference (writing 1.1 V is a no-op against the
|
||||
* power-on default), or back to 1 to restore the old Mayhem behaviour.
|
||||
* If reception measurably worsens, put it back to 1 and say so -- neither
|
||||
* value has been verified on hardware. */
|
||||
#define PRALINE_RX_IQ_VCM 0
|
||||
radio::set_rx_buff_vcm(PRALINE_RX_IQ_VCM);
|
||||
#endif
|
||||
|
||||
update_tuning_frequency();
|
||||
@@ -326,47 +349,39 @@ void ReceiverModel::update_baseband_bandwidth() {
|
||||
if (enabled_) {
|
||||
#ifdef PRALINE
|
||||
/*
|
||||
* PRALINE LPF bandwidth calculation from GSG hackrf_usb radio.c
|
||||
* PRALINE LPF bandwidth, ported from auto_bandwidth() in
|
||||
* hackrf/firmware/common/radio.c:
|
||||
*
|
||||
* The LPF should be set to capture the desired signal bandwidth
|
||||
* while the FPGA decimation filter handles anti-aliasing.
|
||||
* bb_bandwidth = sample_rate * 3 / 4
|
||||
* lpf_bandwidth = bb_bandwidth + offset_hz * 2
|
||||
*
|
||||
* For most modes: LPF = (output_sample_rate * 3) / 8
|
||||
* For quarter-shift: add offset for shifted spectrum
|
||||
* where offset_hz is the quarter-rate shift, i.e. afe_rate / 4 when a
|
||||
* shift is in use. The doubling is because the wanted signal sits
|
||||
* offset from the analogue centre, so the analogue filter has to stay
|
||||
* open out to that offset on the far side too.
|
||||
*
|
||||
* Note: MAX2831 minimum LPF is 11.6 MHz, so for narrow sample rates
|
||||
* the hardware limit applies and FPGA filter does the real work.
|
||||
* The previous version used /8 in both places and read the shift from
|
||||
* bits 2-3 of FPGA register 1, which do not exist in the gateware, so
|
||||
* it always took the no-shift branch. At the ADS-B rate that asked for
|
||||
* 750 kHz, which is below the MAX2831's 1.75 MHz floor and therefore
|
||||
* also switched in the external narrowband AA filter
|
||||
* (MAX2831::set_lpf_rf_bandwidth_rx), squeezing the RX path shut. The
|
||||
* reference asks for 17.5 MHz at the same rate and the AA filter stays
|
||||
* out of circuit.
|
||||
*/
|
||||
|
||||
uint32_t sample_rate = sampling_rate();
|
||||
uint8_t resampling_n = portapack::clock_manager.get_resampling_n();
|
||||
uint32_t afe_rate = sample_rate << resampling_n;
|
||||
|
||||
// Base LPF: enough to capture desired bandwidth
|
||||
uint32_t lpf_bandwidth = (sample_rate * 3) / 8;
|
||||
|
||||
// Check if quarter-shift is enabled (FPGA register 1, bits 2-3)
|
||||
uint32_t fpga_ctrl = radio::debug::fpga::register_read(1);
|
||||
uint8_t quarter_shift = (fpga_ctrl >> 2) & 0x03;
|
||||
uint32_t lpf_bandwidth = (sample_rate * 3) / 4;
|
||||
|
||||
const uint8_t quarter_shift = radio::debug::get_cached_quarter_shift();
|
||||
if (quarter_shift != 0) {
|
||||
// Quarter-shift moves spectrum by AFE_rate/4, need wider LPF
|
||||
uint32_t offset = afe_rate / 8;
|
||||
const uint32_t offset = afe_rate / 4;
|
||||
lpf_bandwidth += offset * 2;
|
||||
}
|
||||
|
||||
// For best anti-alias performance, also consider AFE Nyquist
|
||||
// If our calculated LPF is below MAX2831 minimum, it doesn't matter
|
||||
// But if we can set LPF to just below AFE Nyquist, that's optimal
|
||||
uint32_t afe_nyquist = afe_rate / 2;
|
||||
|
||||
// Use the larger of: signal bandwidth requirement OR Nyquist protection
|
||||
// (but MAX2831 driver will clamp to its available settings anyway)
|
||||
if (lpf_bandwidth < afe_nyquist) {
|
||||
// Set LPF close to Nyquist for maximum alias rejection
|
||||
lpf_bandwidth = (afe_nyquist * 9) / 10; // 90% of Nyquist
|
||||
}
|
||||
|
||||
radio::set_baseband_filter_bandwidth_rx(lpf_bandwidth);
|
||||
#else
|
||||
radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth());
|
||||
|
||||
@@ -66,6 +66,7 @@ class ReceiverModel {
|
||||
/* The frequency to receive (no offset). */
|
||||
rf::Frequency target_frequency() const;
|
||||
void set_target_frequency(rf::Frequency f);
|
||||
void set_target_frequency_with_hidden_offset(rf::Frequency f, rf::Frequency offset);
|
||||
|
||||
uint32_t baseband_bandwidth() const;
|
||||
void set_baseband_bandwidth(uint32_t v);
|
||||
|
||||
+332
-99
@@ -27,8 +27,8 @@ namespace tuning {
|
||||
namespace config {
|
||||
|
||||
// Forward declarations
|
||||
Config low_band(const rf::Frequency target_frequency);
|
||||
Config mid_band(const rf::Frequency target_frequency);
|
||||
Config low_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit);
|
||||
Config mid_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit);
|
||||
Config high_band(const rf::Frequency target_frequency);
|
||||
|
||||
#ifdef PRALINE
|
||||
@@ -36,133 +36,366 @@ Config high_band(const rf::Frequency target_frequency);
|
||||
* PRALINE Tuning Configuration
|
||||
* ============================
|
||||
*
|
||||
* Reference: hackrf_usb/common/tune_config.h praline_tune_config_rx[]
|
||||
* These tables are copied verbatim from the reference firmware,
|
||||
* hackrf/firmware/common/tune_config.h (praline_tune_config_rx /
|
||||
* praline_tune_config_tx), and the selection and offset maths below reproduce
|
||||
* hackrf/firmware/common/radio.c radio_update_frequency() /
|
||||
* analog_from_digital_rf() / compute_offset().
|
||||
*
|
||||
* The hackrf_usb firmware uses a table-driven approach where each entry
|
||||
* specifies:
|
||||
* - rf_range_end_mhz: Upper frequency limit for this config
|
||||
* - if_mhz: IF frequency (what MAX2831 tunes to)
|
||||
* - high_lo: true = high-side injection, false = low-side
|
||||
* - shift: FPGA quarter-shift mode (not implemented in Mayhem yet)
|
||||
* Each entry gives, for target frequencies up to rf_range_end_mhz:
|
||||
* if_mhz the IF the MAX2831 tunes to (0 = mixer bypassed, IF = RF)
|
||||
* high_lo true -> LO = IF + analogue RF (mixer inverts the spectrum)
|
||||
* false -> LO = IF - analogue RF (no inversion)
|
||||
* shift the FPGA quarter-rate shift mode used for this entry
|
||||
*
|
||||
* Key insight: The IF frequency varies to keep the RFFC5072 VCO in a
|
||||
* safe operating range (ideally 3500-5000 MHz, avoiding extremes).
|
||||
* The quarter-rate shift is the part Mayhem was previously missing. RX entries
|
||||
* deliberately place the analogue passband a quarter of the ADC rate away from
|
||||
* the requested frequency (+8 MHz at the usual 32 Msps AFE rate) so that the
|
||||
* wanted signal never sits on the DC offset / LO leakage, and then ask the FPGA
|
||||
* to rotate it back down to DC. Both halves have to be programmed together:
|
||||
* - tuning to target + offset without asking the FPGA to rotate leaves the
|
||||
* signal 8 MHz out and the decimation filter deletes it;
|
||||
* - tuning to target with no offset (what Mayhem did) parks the signal on DC,
|
||||
* under the LO leakage and the gateware's adaptive DC block.
|
||||
*
|
||||
* RFFC5072 VCO calculation:
|
||||
* High-side injection: LO = IF + RF, VCO = LO × lodiv
|
||||
* Low-side injection: LO = IF - RF, VCO = LO × lodiv
|
||||
* Where lodiv = 2 for frequencies where VCO > 2700 MHz
|
||||
*
|
||||
* From hackrf_usb tune_config_rx (simplified):
|
||||
* 0-2100 MHz: IF=2375, high_lo=true → VCO = (2375+RF)×2
|
||||
* 2105-2115: IF=2375, high_lo=false → VCO = (2375-RF)×2
|
||||
* 2115-2130: IF=2425, high_lo=false → VCO = (2425-RF)×2
|
||||
* ... (more entries for fine-grained control)
|
||||
* 2320-2580: IF=0 (bypass mode, no mixer)
|
||||
* 2580+: High-pass mode
|
||||
* "up" and "down" are named for the direction the FPGA rotates, so
|
||||
* FPGA_QUARTER_SHIFT_MODE_UP means the analogue centre is placed ABOVE the
|
||||
* requested frequency, and DOWN below it (radio.c analog_from_digital_rf()).
|
||||
*/
|
||||
|
||||
// Simplified tune_config lookup for Mayhem
|
||||
// Returns the IF frequency in Hz for a given target frequency
|
||||
constexpr rf::Frequency praline_get_if_frequency(const rf::Frequency target_frequency) {
|
||||
const uint32_t freq_mhz = target_frequency / 1'000'000;
|
||||
namespace {
|
||||
|
||||
// Based on hackrf_usb tune_config_rx table
|
||||
if (freq_mhz < 2100) {
|
||||
// Most low-band frequencies: use 2375 MHz IF
|
||||
// This keeps VCO around 4750-4950 MHz for FM band
|
||||
return 2375'000'000;
|
||||
} else if (freq_mhz < 2320) {
|
||||
// Transition zone: use varying IF to avoid VCO edges
|
||||
// These frequencies are tricky - near MAX2831 minimum
|
||||
// Use 2425 MHz to give some margin
|
||||
return 2425'000'000;
|
||||
} else {
|
||||
// Bypass mode or high-band - IF not used for mixer
|
||||
return 0;
|
||||
struct PralineTuneConfig {
|
||||
uint16_t rf_range_end_mhz;
|
||||
uint16_t if_mhz;
|
||||
bool high_lo;
|
||||
uint8_t shift; /* 0b00 none, 0b11 up, 0b01 down */
|
||||
};
|
||||
|
||||
/* The tables below are kept column-aligned to match the reference source, so
|
||||
* they are exempt from reformatting. */
|
||||
// clang-format off
|
||||
|
||||
/* tuning table optimized for RX */
|
||||
constexpr PralineTuneConfig praline_tune_config_rx[] = {
|
||||
{ 0, 2360, true, 0b00},
|
||||
{ 50, 2320, true, 0b11},
|
||||
{ 100, 2320, true, 0b01},
|
||||
{ 140, 2320, true, 0b11},
|
||||
{ 406, 2560, true, 0b11},
|
||||
{ 511, 2380, true, 0b11},
|
||||
{ 578, 2560, true, 0b01},
|
||||
{ 741, 2340, true, 0b11},
|
||||
{ 861, 2560, true, 0b01},
|
||||
{ 921, 2560, true, 0b11},
|
||||
{ 1049, 2340, true, 0b01},
|
||||
{ 1169, 2380, true, 0b11},
|
||||
{ 1360, 2340, true, 0b11},
|
||||
{ 1544, 2560, true, 0b01},
|
||||
{ 1675, 2560, true, 0b11},
|
||||
{ 1992, 2380, true, 0b01},
|
||||
{ 2070, 2340, true, 0b01},
|
||||
{ 2150, 2360, true, 0b01},
|
||||
{ 2168, 2560, false, 0b11},
|
||||
{ 2185, 2580, false, 0b11},
|
||||
{ 2202, 2580, false, 0b01},
|
||||
{ 2205, 2520, false, 0b11},
|
||||
{ 2216, 2560, false, 0b11},
|
||||
{ 2223, 2540, false, 0b11},
|
||||
{ 2234, 2580, false, 0b11},
|
||||
{ 2240, 2560, false, 0b11},
|
||||
{ 2251, 2580, false, 0b01},
|
||||
{ 2258, 2580, false, 0b11},
|
||||
{ 2265, 2540, false, 0b01},
|
||||
{ 2271, 2580, false, 0b11},
|
||||
{ 2273, 2560, false, 0b11},
|
||||
{ 2275, 2580, false, 0b01},
|
||||
{ 2280, 2500, false, 0b01},
|
||||
{ 2284, 2540, false, 0b11},
|
||||
{ 2289, 2580, false, 0b01},
|
||||
{ 2293, 2540, false, 0b01},
|
||||
{ 2298, 2520, false, 0b01},
|
||||
{ 2300, 2580, false, 0b11},
|
||||
{ 2302, 2540, false, 0b01},
|
||||
{ 2309, 2560, false, 0b01},
|
||||
{ 2311, 2580, false, 0b01},
|
||||
{ 2314, 2540, false, 0b11},
|
||||
{ 2315, 2540, false, 0b01},
|
||||
{ 2320, 2580, false, 0b11},
|
||||
{ 2380, 0, false, 0b11},
|
||||
{ 2440, 0, false, 0b01},
|
||||
{ 2500, 0, false, 0b11},
|
||||
{ 2580, 0, false, 0b01},
|
||||
{ 2583, 2360, false, 0b11},
|
||||
{ 2584, 2380, false, 0b11},
|
||||
{ 2587, 2340, false, 0b11},
|
||||
{ 2593, 2340, false, 0b01},
|
||||
{ 2607, 2340, false, 0b11},
|
||||
{ 2609, 2360, false, 0b11},
|
||||
{ 2615, 2360, false, 0b01},
|
||||
{ 2627, 2340, false, 0b01},
|
||||
{ 2629, 2360, false, 0b01},
|
||||
{ 2631, 2380, false, 0b11},
|
||||
{ 2644, 2340, false, 0b11},
|
||||
{ 2649, 2380, false, 0b11},
|
||||
{ 2651, 2380, false, 0b01},
|
||||
{ 2654, 2500, false, 0b11},
|
||||
{ 2665, 2360, false, 0b11},
|
||||
{ 2669, 2380, false, 0b01},
|
||||
{ 2672, 2360, false, 0b01},
|
||||
{ 2682, 2340, false, 0b11},
|
||||
{ 2687, 2380, false, 0b11},
|
||||
{ 2692, 2340, false, 0b11},
|
||||
{ 2695, 2500, false, 0b11},
|
||||
{ 2705, 2360, false, 0b11},
|
||||
{ 2707, 2380, false, 0b01},
|
||||
{ 2712, 2340, false, 0b01},
|
||||
{ 2717, 2520, false, 0b11},
|
||||
{ 2728, 2380, false, 0b11},
|
||||
{ 2730, 2560, false, 0b11},
|
||||
{ 2734, 2500, false, 0b11},
|
||||
{ 2758, 2340, false, 0b11},
|
||||
{ 2780, 2360, false, 0b11},
|
||||
{ 2787, 2520, false, 0b11},
|
||||
{ 2802, 2380, false, 0b11},
|
||||
{ 2809, 2540, false, 0b11},
|
||||
{ 2822, 2380, false, 0b01},
|
||||
{ 2831, 2560, false, 0b11},
|
||||
{ 2854, 2340, false, 0b11},
|
||||
{ 2875, 2360, false, 0b11},
|
||||
{ 2898, 2380, false, 0b11},
|
||||
{ 2918, 2380, false, 0b01},
|
||||
{ 2936, 2520, false, 0b01},
|
||||
{ 2944, 2380, false, 0b01},
|
||||
{ 2959, 2560, false, 0b11},
|
||||
{ 2976, 2340, false, 0b11},
|
||||
{ 2985, 2500, false, 0b01},
|
||||
{ 3003, 2340, false, 0b11},
|
||||
{ 3009, 2540, false, 0b11},
|
||||
{ 3027, 2380, false, 0b11},
|
||||
{ 3034, 2560, false, 0b11},
|
||||
{ 3050, 2380, false, 0b01},
|
||||
{ 3069, 2500, false, 0b11},
|
||||
{ 3094, 2520, false, 0b11},
|
||||
{ 3119, 2540, false, 0b11},
|
||||
{ 3144, 2560, false, 0b11},
|
||||
{ 3169, 2560, false, 0b01},
|
||||
{ 3180, 2500, false, 0b11},
|
||||
{ 3204, 2340, false, 0b11},
|
||||
{ 3232, 2360, false, 0b11},
|
||||
{ 3292, 2340, false, 0b01},
|
||||
{ 3340, 2380, false, 0b01},
|
||||
{ 3369, 2340, false, 0b11},
|
||||
{ 3399, 2360, false, 0b11},
|
||||
{ 3429, 2380, false, 0b11},
|
||||
{ 3464, 2500, false, 0b11},
|
||||
{ 3489, 2520, false, 0b11},
|
||||
{ 3512, 2540, false, 0b11},
|
||||
{ 3551, 2500, false, 0b01},
|
||||
{ 3582, 2540, false, 0b11},
|
||||
{ 3611, 2560, false, 0b11},
|
||||
{ 3639, 2520, false, 0b11},
|
||||
{ 3729, 2340, false, 0b11},
|
||||
{ 3817, 2380, false, 0b01},
|
||||
{ 3942, 2360, false, 0b01},
|
||||
{ 4049, 2540, false, 0b11},
|
||||
{ 4134, 2500, false, 0b01},
|
||||
{ 4194, 2560, false, 0b11},
|
||||
{ 4353, 2520, false, 0b11},
|
||||
{ 4449, 2360, false, 0b01},
|
||||
{ 4562, 2500, false, 0b11},
|
||||
{ 4672, 2560, false, 0b11},
|
||||
{ 4769, 2540, false, 0b11},
|
||||
{ 4849, 2560, false, 0b01},
|
||||
{ 4889, 2560, false, 0b11},
|
||||
{ 4929, 2560, false, 0b11},
|
||||
{ 4969, 2560, false, 0b11},
|
||||
{ 5009, 2560, false, 0b11},
|
||||
{ 5049, 2560, false, 0b11},
|
||||
{ 5092, 2360, false, 0b11},
|
||||
{ 5209, 2340, false, 0b01},
|
||||
{ 5298, 2380, false, 0b01},
|
||||
{ 5468, 2340, false, 0b01},
|
||||
{ 5582, 2520, false, 0b11},
|
||||
{ 5702, 2340, false, 0b11},
|
||||
{ 5888, 2520, false, 0b01},
|
||||
{ 6092, 2340, false, 0b01},
|
||||
{ 6240, 2560, false, 0b11},
|
||||
{ 6609, 2340, false, 0b11},
|
||||
{ 6752, 2380, false, 0b01},
|
||||
{ 6930, 2520, false, 0b01},
|
||||
{ 7000, 2560, false, 0b11},
|
||||
{ 7070, 2560, false, 0b01},
|
||||
{ 7251, 2580, false, 0b01},
|
||||
{ 0, 0, false, 0b00},
|
||||
};
|
||||
|
||||
/* tuning table optimized for TX */
|
||||
constexpr PralineTuneConfig praline_tune_config_tx[] = {
|
||||
{ 2100, 2375, true, 0b00},
|
||||
{ 2105, 2375, false, 0b00},
|
||||
{ 2115, 2425, false, 0b00},
|
||||
{ 2130, 2375, false, 0b00},
|
||||
{ 2150, 2425, false, 0b00},
|
||||
{ 2160, 2475, false, 0b00},
|
||||
{ 2175, 2425, false, 0b00},
|
||||
{ 2190, 2475, false, 0b00},
|
||||
{ 2195, 2425, false, 0b00},
|
||||
{ 2210, 2375, false, 0b00},
|
||||
{ 2248, 2425, false, 0b00},
|
||||
{ 2265, 2525, false, 0b00},
|
||||
{ 2300, 2425, false, 0b00},
|
||||
{ 2320, 2525, false, 0b00},
|
||||
{ 2580, 0, false, 0b00},
|
||||
{ 3000, 2325, false, 0b00},
|
||||
{ 3140, 2375, false, 0b00},
|
||||
{ 3200, 2425, false, 0b00},
|
||||
{ 3280, 2375, false, 0b00},
|
||||
{ 3340, 2425, false, 0b00},
|
||||
{ 3420, 2475, false, 0b00},
|
||||
{ 3480, 2525, false, 0b00},
|
||||
{ 3500, 2475, false, 0b00},
|
||||
{ 3595, 2425, false, 0b00},
|
||||
{ 3625, 2375, false, 0b00},
|
||||
{ 3670, 2475, false, 0b00},
|
||||
{ 3710, 2425, false, 0b00},
|
||||
{ 3760, 2525, false, 0b00},
|
||||
{ 3790, 2475, false, 0b00},
|
||||
{ 3860, 2425, false, 0b00},
|
||||
{ 3915, 2375, false, 0b00},
|
||||
{ 4000, 2425, false, 0b00},
|
||||
{ 4055, 2375, false, 0b00},
|
||||
{ 4125, 2425, false, 0b00},
|
||||
{ 4700, 2375, false, 0b00},
|
||||
{ 4800, 2425, false, 0b00},
|
||||
{ 5000, 2375, false, 0b00},
|
||||
{ 5260, 2475, false, 0b00},
|
||||
{ 5465, 2525, false, 0b00},
|
||||
{ 5560, 2375, false, 0b00},
|
||||
{ 5720, 2425, false, 0b00},
|
||||
{ 5860, 2475, false, 0b00},
|
||||
{ 5970, 2575, false, 0b00},
|
||||
{ 6000, 2375, false, 0b00},
|
||||
{ 6500, 2325, false, 0b00},
|
||||
{ 6750, 2375, false, 0b00},
|
||||
{ 6850, 2425, false, 0b00},
|
||||
{ 6950, 2475, false, 0b00},
|
||||
{ 7000, 2525, false, 0b00},
|
||||
{ 7251, 2575, false, 0b00},
|
||||
{ 0, 0, false, 0b00},
|
||||
};
|
||||
|
||||
// clang-format on
|
||||
|
||||
/* radio.c select_tune_config(): first entry whose range end is above the
|
||||
* requested frequency. The list is terminated by an all-zero entry, which is
|
||||
* also what a frequency past the end of the table lands on. */
|
||||
const PralineTuneConfig* select_tune_config(const rf::Frequency target_frequency, const bool transmit) {
|
||||
const PralineTuneConfig* entry = transmit ? praline_tune_config_tx : praline_tune_config_rx;
|
||||
const uint32_t freq_mhz = static_cast<uint32_t>(target_frequency / 1'000'000);
|
||||
|
||||
while ((entry->rf_range_end_mhz != 0) || (entry->if_mhz != 0)) {
|
||||
if ((target_frequency == 0) || (entry->rf_range_end_mhz > freq_mhz))
|
||||
break;
|
||||
entry++;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
// Returns true for high-side injection, false for low-side
|
||||
constexpr bool praline_use_high_side_injection(const rf::Frequency target_frequency) {
|
||||
const uint32_t freq_mhz = target_frequency / 1'000'000;
|
||||
|
||||
// Based on hackrf_usb tune_config_rx table
|
||||
if (freq_mhz < 2100) {
|
||||
// Standard low-band: high-side injection
|
||||
// LO = IF + RF, mixer inverts spectrum
|
||||
return true;
|
||||
} else if (freq_mhz < 2105) {
|
||||
// Narrow transition: still high-side
|
||||
return true;
|
||||
} else if (freq_mhz < 2320) {
|
||||
// Near MAX2831 minimum: use low-side injection
|
||||
// LO = IF - RF, no spectrum inversion
|
||||
return false;
|
||||
} else {
|
||||
// Bypass/high-band - doesn't matter, mixer bypassed
|
||||
return false;
|
||||
}
|
||||
/* radio.c compute_offset(): a quarter of the AFE (ADC) sample rate, or zero if
|
||||
* no shift is in use or the AFE rate isn't known yet. */
|
||||
constexpr uint32_t quarter_shift_offset(const uint8_t shift, const uint32_t afe_rate) {
|
||||
return (shift == 0) ? 0 : (afe_rate / 4);
|
||||
}
|
||||
|
||||
/* radio.c analog_from_digital_rf(). */
|
||||
rf::Frequency analog_from_digital_rf(const rf::Frequency target_frequency, const uint8_t shift, const uint32_t afe_rate) {
|
||||
const rf::Frequency offset = quarter_shift_offset(shift, afe_rate);
|
||||
|
||||
if (shift == 0b11)
|
||||
return target_frequency + offset;
|
||||
|
||||
if (shift == 0b01)
|
||||
return (offset > target_frequency) ? (offset - target_frequency)
|
||||
: (target_frequency - offset);
|
||||
|
||||
return target_frequency;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#endif // PRALINE
|
||||
|
||||
// Low band <2170 Mhz (HackRF One) or <2320 MHz (PRALINE):
|
||||
constexpr rf::Frequency low_band_second_lo_frequency(const rf::Frequency target_frequency) {
|
||||
Config low_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
|
||||
#ifdef PRALINE
|
||||
// Use the tune_config lookup for PRALINE
|
||||
return praline_get_if_frequency(target_frequency);
|
||||
#else
|
||||
return 2650'000'000 - (target_frequency / 7);
|
||||
#endif
|
||||
}
|
||||
const PralineTuneConfig* entry = select_tune_config(target_frequency, transmit);
|
||||
|
||||
Config low_band(const rf::Frequency target_frequency) {
|
||||
const rf::Frequency second_lo_frequency = low_band_second_lo_frequency(target_frequency);
|
||||
/* Past the end of the table: no usable configuration. */
|
||||
if ((entry->rf_range_end_mhz == 0) && (entry->if_mhz == 0))
|
||||
return {};
|
||||
|
||||
#ifdef PRALINE
|
||||
/* afe_rate == 0 means the caller doesn't know the ADC rate, so no LO offset
|
||||
* is applied. The FPGA rotation has to be dropped with it: rotating without
|
||||
* the matching offset moves the wanted signal off DC by afe_rate / 4. */
|
||||
const uint8_t shift = (afe_rate == 0) ? 0 : entry->shift;
|
||||
const rf::Frequency analog_rf = analog_from_digital_rf(target_frequency, shift, afe_rate);
|
||||
|
||||
/* if_mhz == 0 means the mixer is bypassed and the transceiver tunes the RF
|
||||
* directly; there is no first LO in that case. */
|
||||
const rf::Frequency second_lo_frequency =
|
||||
(entry->if_mhz == 0) ? analog_rf : (static_cast<rf::Frequency>(entry->if_mhz) * 1'000'000);
|
||||
|
||||
if (entry->if_mhz == 0)
|
||||
return {0, second_lo_frequency, rf::path::Band::Low, false, shift};
|
||||
|
||||
/* The low band always runs through the low-pass image-reject filter, so
|
||||
* the spectrum is inverted exactly when the first LO ends up above the IF,
|
||||
* i.e. for high-side injection. This is hackrf_usb.c radio_changed():
|
||||
* invert = (img_reject == RF_PATH_FILTER_LOW_PASS) && (freq_lo > freq_if)
|
||||
*/
|
||||
rf::Frequency first_lo_frequency;
|
||||
bool mixer_invert;
|
||||
|
||||
if (praline_use_high_side_injection(target_frequency)) {
|
||||
// High-side injection: LO = IF + RF
|
||||
first_lo_frequency = second_lo_frequency + target_frequency;
|
||||
if (entry->high_lo) {
|
||||
first_lo_frequency = second_lo_frequency + analog_rf;
|
||||
mixer_invert = true;
|
||||
} else {
|
||||
// Low-side injection: LO = IF - RF
|
||||
first_lo_frequency = second_lo_frequency - target_frequency;
|
||||
first_lo_frequency = second_lo_frequency - analog_rf;
|
||||
mixer_invert = false;
|
||||
}
|
||||
|
||||
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert};
|
||||
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert, shift};
|
||||
#else
|
||||
(void)afe_rate;
|
||||
(void)transmit;
|
||||
const rf::Frequency second_lo_frequency = 2650'000'000 - (target_frequency / 7);
|
||||
const rf::Frequency first_lo_frequency = target_frequency + second_lo_frequency;
|
||||
const bool mixer_invert = true;
|
||||
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert};
|
||||
#endif
|
||||
}
|
||||
|
||||
// Mid band 2170-2740 Mhz (HackRF One) or 2320-2580 MHz (PRALINE):
|
||||
Config mid_band(const rf::Frequency target_frequency) {
|
||||
// Mid band 2170-2740 Mhz (HackRF One) or 2320-2740 MHz (PRALINE):
|
||||
Config mid_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
|
||||
#ifdef PRALINE
|
||||
// For Praline with MAX2831 (2.3-2.6 GHz range)
|
||||
// Frequencies 2170-2300 MHz need upconversion since they're below MAX2831 minimum
|
||||
if (target_frequency < 2300'000'000) {
|
||||
// Treat as low band - need mixer
|
||||
return low_band(target_frequency);
|
||||
}
|
||||
// Frequencies 2300-2600 MHz can go direct (no RFFC5072)
|
||||
else if (target_frequency <= 2600'000'000) {
|
||||
const rf::Frequency second_lo_frequency = target_frequency;
|
||||
const rf::Frequency first_lo_frequency = 0;
|
||||
const bool mixer_invert = false;
|
||||
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Mid, mixer_invert};
|
||||
}
|
||||
// Frequencies 2600-2740 MHz need downconversion since they're above MAX2831 maximum
|
||||
else {
|
||||
// Treat as high band
|
||||
return high_band(target_frequency);
|
||||
/* radio.c select_img_reject() / tuning.c: on PRALINE the MAX2831 tunes
|
||||
* direct (mixer bypassed) from 2320 to 2580 MHz. band_mid starts at
|
||||
* TRANSITION = 2320 MHz, so everything below that already went to
|
||||
* low_band(). */
|
||||
if (target_frequency <= 2580'000'000) {
|
||||
const PralineTuneConfig* entry = select_tune_config(target_frequency, transmit);
|
||||
const uint8_t shift = (afe_rate == 0) ? 0 : entry->shift;
|
||||
const rf::Frequency analog_rf = analog_from_digital_rf(target_frequency, shift, afe_rate);
|
||||
|
||||
/* Mixer bypassed: no first LO, the MAX2831 tunes the (offset)
|
||||
* analogue RF directly and the FPGA rotates it back. */
|
||||
return {0, analog_rf, rf::path::Band::Mid, false, shift};
|
||||
}
|
||||
|
||||
/* 2580-2740 MHz: above the bypass window, downconvert. */
|
||||
return high_band(target_frequency);
|
||||
#else
|
||||
(void)afe_rate;
|
||||
(void)transmit;
|
||||
const rf::Frequency second_lo_frequency = target_frequency;
|
||||
const rf::Frequency first_lo_frequency = 0;
|
||||
const bool mixer_invert = false;
|
||||
@@ -203,12 +436,12 @@ Config high_band(const rf::Frequency target_frequency) {
|
||||
return {first_lo_frequency, second_lo_frequency, rf::path::Band::High, mixer_invert};
|
||||
}
|
||||
|
||||
Config create(const rf::Frequency target_frequency) {
|
||||
Config create(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
|
||||
/* TODO: This is some lame code. */
|
||||
if (rf::path::band_low.contains(target_frequency)) {
|
||||
return low_band(target_frequency);
|
||||
return low_band(target_frequency, afe_rate, transmit);
|
||||
} else if (rf::path::band_mid.contains(target_frequency)) {
|
||||
return mid_band(target_frequency);
|
||||
return mid_band(target_frequency, afe_rate, transmit);
|
||||
} else if (rf::path::band_high.contains(target_frequency)) {
|
||||
return high_band(target_frequency);
|
||||
} else {
|
||||
|
||||
@@ -33,18 +33,21 @@ struct Config {
|
||||
: first_lo_frequency(0),
|
||||
second_lo_frequency(0),
|
||||
rf_path_band(rf::path::Band::Mid),
|
||||
mixer_invert(false) {
|
||||
mixer_invert(false),
|
||||
quarter_shift(0) {
|
||||
}
|
||||
|
||||
constexpr Config(
|
||||
rf::Frequency first_lo_frequency,
|
||||
rf::Frequency second_lo_frequency,
|
||||
rf::path::Band rf_path_band,
|
||||
bool mixer_invert)
|
||||
bool mixer_invert,
|
||||
uint8_t quarter_shift = 0)
|
||||
: first_lo_frequency(first_lo_frequency),
|
||||
second_lo_frequency(second_lo_frequency),
|
||||
rf_path_band(rf_path_band),
|
||||
mixer_invert(mixer_invert) {
|
||||
mixer_invert(mixer_invert),
|
||||
quarter_shift(quarter_shift) {
|
||||
}
|
||||
|
||||
bool is_valid() const {
|
||||
@@ -55,9 +58,23 @@ struct Config {
|
||||
const rf::Frequency second_lo_frequency;
|
||||
const rf::path::Band rf_path_band;
|
||||
const bool mixer_invert;
|
||||
|
||||
/* PRALINE only: FPGA RX quarter-rate shift mode, in the encoding the
|
||||
* gateware expects in the top two bits of register 0x03 (rx_pstep):
|
||||
* 0b00 = none, 0b11 = up, 0b01 = down.
|
||||
* Matches fpga_quarter_shift_mode_t in hackrf/firmware/common/fpga.h.
|
||||
* Always 0 on HackRF One (no FPGA). */
|
||||
const uint8_t quarter_shift;
|
||||
};
|
||||
|
||||
Config create(const rf::Frequency target_frequency);
|
||||
/* afe_rate is the ADC sample rate in Hz (output rate << decimation), needed on
|
||||
* PRALINE to work out how far off centre the quarter-rate shift places the
|
||||
* analogue passband. Pass 0 (or leave defaulted) to disable the shift.
|
||||
* transmit selects the TX tuning table. Both are ignored on HackRF One. */
|
||||
Config create(
|
||||
const rf::Frequency target_frequency,
|
||||
const uint32_t afe_rate = 0,
|
||||
const bool transmit = false);
|
||||
|
||||
} /* namespace config */
|
||||
} /* namespace tuning */
|
||||
|
||||
@@ -39,6 +39,8 @@ class BoundFrequencyField : public FrequencyField {
|
||||
|
||||
public:
|
||||
decltype(FrequencyField::on_change) updated{};
|
||||
std::function<bool(rf::Frequency)> changing{};
|
||||
std::function<void(rf::Frequency)> entered{};
|
||||
|
||||
BoundFrequencyField(Point parent_pos, NavigationView& nav)
|
||||
: FrequencyField(parent_pos) {
|
||||
@@ -47,7 +49,8 @@ class BoundFrequencyField : public FrequencyField {
|
||||
set_value(model->target_frequency());
|
||||
|
||||
on_change = [this](rf::Frequency f) {
|
||||
model->set_target_frequency(f);
|
||||
if (!changing || !changing(f))
|
||||
model->set_target_frequency(f);
|
||||
if (updated)
|
||||
updated(f);
|
||||
};
|
||||
@@ -57,7 +60,10 @@ class BoundFrequencyField : public FrequencyField {
|
||||
on_edit_shown();
|
||||
auto freq_view = nav.push<FrequencyKeypadView>(model->target_frequency());
|
||||
freq_view->on_changed = [this](rf::Frequency f) {
|
||||
set_value(f);
|
||||
if (entered)
|
||||
entered(f);
|
||||
else
|
||||
set_value(f);
|
||||
};
|
||||
nav.set_on_pop([this]() {
|
||||
if (on_edit_hidden)
|
||||
@@ -76,4 +82,4 @@ using TxFrequencyField = BoundFrequencyField<TransmitterModel, &portapack::trans
|
||||
|
||||
} // namespace ui
|
||||
|
||||
#endif // __UI_FREQ_FIELD_H__
|
||||
#endif // __UI_FREQ_FIELD_H__
|
||||
|
||||
@@ -46,10 +46,12 @@ GeoPos::GeoPos(
|
||||
&field_speed,
|
||||
&text_alt_unit,
|
||||
&text_speed_unit,
|
||||
&field_lat_hemisphere,
|
||||
&field_lat_degrees,
|
||||
&field_lat_minutes,
|
||||
&field_lat_seconds,
|
||||
&text_lat_decimal,
|
||||
&field_lon_hemisphere,
|
||||
&field_lon_degrees,
|
||||
&field_lon_minutes,
|
||||
&field_lon_seconds,
|
||||
@@ -73,8 +75,14 @@ GeoPos::GeoPos(
|
||||
on_change(altitude(), lat_value, lon_value, speed());
|
||||
};
|
||||
|
||||
const auto changed_hemisphere_fn = [changed_fn](size_t, OptionsField::value_t) {
|
||||
changed_fn(0);
|
||||
};
|
||||
|
||||
field_altitude.on_change = changed_fn;
|
||||
field_speed.on_change = changed_fn;
|
||||
field_lat_hemisphere.on_change = changed_hemisphere_fn;
|
||||
field_lon_hemisphere.on_change = changed_hemisphere_fn;
|
||||
field_lat_degrees.on_change = changed_fn;
|
||||
field_lat_minutes.on_change = changed_fn;
|
||||
field_lat_seconds.on_change = changed_fn;
|
||||
@@ -83,19 +91,37 @@ GeoPos::GeoPos(
|
||||
field_lon_seconds.on_change = changed_fn;
|
||||
|
||||
const auto wrapped_lat_seconds = [this](int32_t v) {
|
||||
const auto old_minutes = field_lat_minutes.value();
|
||||
field_lat_minutes.on_encoder(v);
|
||||
if (field_lat_minutes.value() == old_minutes) {
|
||||
field_lat_seconds.set_value((v > 0) ? 59 : 0);
|
||||
}
|
||||
};
|
||||
|
||||
// Degrees now holds a magnitude, so a minutes wrap always carries in the
|
||||
// same direction regardless of hemisphere.
|
||||
const auto wrapped_lat_minutes = [this](int32_t v) {
|
||||
field_lat_degrees.on_encoder((field_lat_degrees.value() >= 0) ? v : -v);
|
||||
const auto old_degrees = field_lat_degrees.value();
|
||||
field_lat_degrees.on_encoder(v);
|
||||
if (field_lat_degrees.value() == old_degrees) {
|
||||
field_lat_minutes.set_value((v > 0) ? 59 : 0);
|
||||
}
|
||||
};
|
||||
|
||||
const auto wrapped_lon_seconds = [this](int32_t v) {
|
||||
const auto old_minutes = field_lon_minutes.value();
|
||||
field_lon_minutes.on_encoder(v);
|
||||
if (field_lon_minutes.value() == old_minutes) {
|
||||
field_lon_seconds.set_value((v > 0) ? 59 : 0);
|
||||
}
|
||||
};
|
||||
|
||||
const auto wrapped_lon_minutes = [this](int32_t v) {
|
||||
field_lon_degrees.on_encoder((field_lon_degrees.value() >= 0) ? v : -v);
|
||||
const auto old_degrees = field_lon_degrees.value();
|
||||
field_lon_degrees.on_encoder(v);
|
||||
if (field_lon_degrees.value() == old_degrees) {
|
||||
field_lon_minutes.set_value((v > 0) ? 59 : 0);
|
||||
}
|
||||
};
|
||||
|
||||
field_lat_seconds.on_wrap = wrapped_lat_seconds;
|
||||
@@ -146,31 +172,31 @@ void GeoPos::set_speed(int32_t speed) {
|
||||
}
|
||||
|
||||
void GeoPos::set_lat(float lat) {
|
||||
field_lat_degrees.set_value(lat);
|
||||
field_lat_minutes.set_value((uint32_t)abs(lat / (1.0 / 60)) % 60);
|
||||
field_lat_seconds.set_value((uint32_t)abs(lat / (1.0 / 3600)) % 60);
|
||||
bool south = lat < 0;
|
||||
float magnitude = south ? -lat : lat;
|
||||
field_lat_hemisphere.set_by_value(south ? 1 : 0);
|
||||
field_lat_degrees.set_value((int32_t)magnitude);
|
||||
field_lat_minutes.set_value((uint32_t)(magnitude * 60) % 60);
|
||||
field_lat_seconds.set_value((uint32_t)(magnitude * 3600) % 60);
|
||||
}
|
||||
|
||||
void GeoPos::set_lon(float lon) {
|
||||
field_lon_degrees.set_value(lon);
|
||||
field_lon_minutes.set_value((uint32_t)abs(lon / (1.0 / 60)) % 60);
|
||||
field_lon_seconds.set_value((uint32_t)abs(lon / (1.0 / 3600)) % 60);
|
||||
bool west = lon < 0;
|
||||
float magnitude = west ? -lon : lon;
|
||||
field_lon_hemisphere.set_by_value(west ? 1 : 0);
|
||||
field_lon_degrees.set_value((int32_t)magnitude);
|
||||
field_lon_minutes.set_value((uint32_t)(magnitude * 60) % 60);
|
||||
field_lon_seconds.set_value((uint32_t)(magnitude * 3600) % 60);
|
||||
}
|
||||
|
||||
float GeoPos::lat() {
|
||||
if (field_lat_degrees.value() < 0) {
|
||||
return -1 * (-1 * field_lat_degrees.value() + (field_lat_minutes.value() / 60.0) + (field_lat_seconds.value() / 3600.0));
|
||||
} else {
|
||||
return field_lat_degrees.value() + (field_lat_minutes.value() / 60.0) + (field_lat_seconds.value() / 3600.0);
|
||||
}
|
||||
float magnitude = field_lat_degrees.value() + (field_lat_minutes.value() / 60.0) + (field_lat_seconds.value() / 3600.0);
|
||||
return (field_lat_hemisphere.selected_index_value() != 0) ? -magnitude : magnitude;
|
||||
};
|
||||
|
||||
float GeoPos::lon() {
|
||||
if (field_lon_degrees.value() < 0) {
|
||||
return -1 * (-1 * field_lon_degrees.value() + (field_lon_minutes.value() / 60.0) + (field_lon_seconds.value() / 3600.0));
|
||||
} else {
|
||||
return field_lon_degrees.value() + (field_lon_minutes.value() / 60.0) + (field_lon_seconds.value() / 3600.0);
|
||||
}
|
||||
float magnitude = field_lon_degrees.value() + (field_lon_minutes.value() / 60.0) + (field_lon_seconds.value() / 3600.0);
|
||||
return (field_lon_hemisphere.selected_index_value() != 0) ? -magnitude : magnitude;
|
||||
};
|
||||
|
||||
int32_t GeoPos::altitude() {
|
||||
|
||||
@@ -239,12 +239,21 @@ class GeoPos : public View {
|
||||
{25 * 8, UI_POS_Y(0), 5 * 8, 16},
|
||||
""};
|
||||
|
||||
NumberField field_lat_degrees{
|
||||
// Sign is held by the hemisphere field, not by the degrees field: an
|
||||
// int32_t degrees field has no negative zero, so a coordinate in
|
||||
// (-1, 0) could not otherwise be represented (see issue #3234).
|
||||
OptionsField field_lat_hemisphere{
|
||||
{5 * 8, 1 * 16},
|
||||
4,
|
||||
{-90, 90},
|
||||
1,
|
||||
' '};
|
||||
{{"N", 0},
|
||||
{"S", 1}}};
|
||||
NumberField field_lat_degrees{
|
||||
{6 * 8, 1 * 16},
|
||||
3,
|
||||
{0, 90},
|
||||
1,
|
||||
' ',
|
||||
false};
|
||||
NumberField field_lat_minutes{
|
||||
{10 * 8, 1 * 16},
|
||||
2,
|
||||
@@ -263,12 +272,18 @@ class GeoPos : public View {
|
||||
{17 * 8, 1 * 16, 13 * 8, 1 * 16},
|
||||
""};
|
||||
|
||||
NumberField field_lon_degrees{
|
||||
OptionsField field_lon_hemisphere{
|
||||
{5 * 8, 2 * 16},
|
||||
4,
|
||||
{-180, 180},
|
||||
1,
|
||||
' '};
|
||||
{{"E", 0},
|
||||
{"W", 1}}};
|
||||
NumberField field_lon_degrees{
|
||||
{6 * 8, 2 * 16},
|
||||
3,
|
||||
{0, 180},
|
||||
1,
|
||||
' ',
|
||||
false};
|
||||
NumberField field_lon_minutes{
|
||||
{10 * 8, 2 * 16},
|
||||
2,
|
||||
|
||||
@@ -344,7 +344,9 @@ bool MenuView::on_key(const KeyEvent key) {
|
||||
}
|
||||
[[fallthrough]];
|
||||
case KeyEvent::Select:
|
||||
if (menu_items[highlighted_item].on_select) {
|
||||
// NB: a MenuView can legitimately be empty (e.g. after clear()) while
|
||||
// still holding focus; indexing menu_items unchecked faults there.
|
||||
if (highlighted_item < menu_items.size() && menu_items[highlighted_item].on_select) {
|
||||
menu_items[highlighted_item].on_select(key);
|
||||
}
|
||||
return true;
|
||||
@@ -364,7 +366,7 @@ bool MenuView::on_keyboard(const KeyboardEvent key) {
|
||||
if (key == '-') return set_highlighted(highlighted_item - 1);
|
||||
if (key == '+') return set_highlighted(highlighted_item + 1);
|
||||
if (key == 10) {
|
||||
if (menu_items[highlighted_item].on_select) {
|
||||
if (highlighted_item < menu_items.size() && menu_items[highlighted_item].on_select) {
|
||||
menu_items[highlighted_item].on_select(KeyEvent::Right);
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -88,16 +88,29 @@ void FrequencyScale::set_spectrum_sampling_rate(const int new_sampling_rate) {
|
||||
}
|
||||
|
||||
void FrequencyScale::set_channel_filter(
|
||||
const int offset,
|
||||
const int low_frequency,
|
||||
const int high_frequency,
|
||||
const int transition) {
|
||||
if ((channel_filter_low_frequency != low_frequency) ||
|
||||
const bool shape_changed =
|
||||
(channel_filter_low_frequency != low_frequency) ||
|
||||
(channel_filter_high_frequency != high_frequency) ||
|
||||
(channel_filter_transition != transition)) {
|
||||
(channel_filter_transition != transition);
|
||||
const bool offset_changed = channel_filter_offset != offset;
|
||||
|
||||
if (shape_changed) {
|
||||
channel_filter_offset = offset;
|
||||
channel_filter_low_frequency = low_frequency;
|
||||
channel_filter_high_frequency = high_frequency;
|
||||
channel_filter_transition = transition;
|
||||
set_dirty();
|
||||
} else if (offset_changed) {
|
||||
const auto old_offset = channel_filter_offset;
|
||||
channel_filter_offset = offset;
|
||||
if (live_tuning && spectrum_sampling_rate && drawn())
|
||||
redraw_filter_cursor(old_offset);
|
||||
else
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,12 +136,14 @@ void FrequencyScale::paint(Painter& painter) {
|
||||
draw_filter_ranges(painter, r);
|
||||
draw_frequency_ticks(painter, r);
|
||||
|
||||
const Rect r_cursor{
|
||||
(screen_width / 2 - 2) + cursor_position, r.bottom() - filter_band_height,
|
||||
5, filter_band_height};
|
||||
painter.fill_rectangle(
|
||||
r_cursor,
|
||||
Color::red());
|
||||
if (!live_tuning) {
|
||||
const Rect r_cursor{
|
||||
(screen_width / 2 - 2) + cursor_position, r.bottom() - filter_band_height,
|
||||
5, filter_band_height};
|
||||
painter.fill_rectangle(
|
||||
r_cursor,
|
||||
Color::red());
|
||||
}
|
||||
}
|
||||
|
||||
void FrequencyScale::clear() {
|
||||
@@ -184,9 +199,79 @@ void FrequencyScale::draw_frequency_ticks(Painter& painter, const Rect r) {
|
||||
}
|
||||
}
|
||||
|
||||
void FrequencyScale::redraw_filter_cursor(const int old_offset) {
|
||||
const auto r = screen_rect();
|
||||
const auto x_center = r.width() / 2;
|
||||
const auto trans =
|
||||
channel_filter_transition * spectrum_bins / spectrum_sampling_rate;
|
||||
|
||||
const auto cursor_left = [&](const int offset) {
|
||||
return r.left() + x_center +
|
||||
(offset + channel_filter_low_frequency) *
|
||||
spectrum_bins / spectrum_sampling_rate -
|
||||
trans;
|
||||
};
|
||||
const auto cursor_right = [&](const int offset) {
|
||||
return r.left() + x_center +
|
||||
(offset + channel_filter_high_frequency) *
|
||||
spectrum_bins / spectrum_sampling_rate +
|
||||
trans;
|
||||
};
|
||||
|
||||
const auto dirty_left =
|
||||
std::min(cursor_left(old_offset), cursor_left(channel_filter_offset)) - 1;
|
||||
const auto dirty_right =
|
||||
std::max(cursor_right(old_offset), cursor_right(channel_filter_offset)) + 1;
|
||||
const Rect dirty{
|
||||
dirty_left,
|
||||
r.bottom() - filter_band_height,
|
||||
dirty_right - dirty_left,
|
||||
filter_band_height};
|
||||
|
||||
Painter painter;
|
||||
painter.fill_rectangle(dirty, Theme::getInstance()->bg_darkest->background);
|
||||
draw_filter_ranges(painter, r);
|
||||
restore_tick_lines(painter, r, dirty);
|
||||
}
|
||||
|
||||
void FrequencyScale::restore_tick_lines(
|
||||
Painter& painter,
|
||||
const Rect r,
|
||||
const Rect dirty) {
|
||||
const auto draw_if_dirty = [&](const Coord x) {
|
||||
if (x >= dirty.left() && x < dirty.right()) {
|
||||
painter.fill_rectangle(
|
||||
{x, dirty.top(), 1, dirty.height()},
|
||||
Theme::getInstance()->bg_darkest->foreground);
|
||||
}
|
||||
};
|
||||
|
||||
const auto x_center = r.left() + r.width() / 2;
|
||||
draw_if_dirty(x_center);
|
||||
|
||||
constexpr int tick_count_max = 4;
|
||||
float rough_tick_interval = float(spectrum_sampling_rate) / tick_count_max;
|
||||
int magnitude = 1;
|
||||
while (rough_tick_interval >= 10.0f) {
|
||||
rough_tick_interval /= 10;
|
||||
magnitude *= 10;
|
||||
}
|
||||
|
||||
const int tick_interval = std::ceil(rough_tick_interval);
|
||||
auto tick_offset = tick_interval;
|
||||
while ((tick_offset * magnitude) < spectrum_sampling_rate / 2) {
|
||||
const Dim pixel_offset =
|
||||
tick_offset * magnitude * spectrum_bins / spectrum_sampling_rate;
|
||||
draw_if_dirty(x_center - pixel_offset);
|
||||
draw_if_dirty(x_center + pixel_offset);
|
||||
tick_offset += tick_interval;
|
||||
}
|
||||
}
|
||||
|
||||
void FrequencyScale::draw_filter_ranges(Painter& painter, const Rect r) {
|
||||
if (channel_filter_low_frequency != channel_filter_high_frequency) {
|
||||
const auto x_center = r.width() / 2;
|
||||
const auto x_center = r.width() / 2 +
|
||||
channel_filter_offset * spectrum_bins / spectrum_sampling_rate;
|
||||
|
||||
const auto x_low = x_center + channel_filter_low_frequency * spectrum_bins / spectrum_sampling_rate;
|
||||
const auto x_high = x_center + channel_filter_high_frequency * spectrum_bins / spectrum_sampling_rate;
|
||||
@@ -220,6 +305,11 @@ void FrequencyScale::on_blur() {
|
||||
}
|
||||
|
||||
bool FrequencyScale::on_encoder(const EncoderEvent delta) {
|
||||
if (live_tuning) {
|
||||
if (on_select) on_select(delta);
|
||||
return true;
|
||||
}
|
||||
|
||||
cursor_position += delta;
|
||||
|
||||
cursor_position = std::min<int32_t>(cursor_position, screen_width / 2 - 1);
|
||||
@@ -326,9 +416,11 @@ WaterfallView::WaterfallView(const bool cursor) {
|
||||
frequency_scale.focus(); // focus on frequency scale to show cursor
|
||||
|
||||
if (sampling_rate) {
|
||||
// screen x to frequency scale x, NB we need two widgets align
|
||||
int32_t cursor_position = x - (screen_width / 2);
|
||||
frequency_scale.set_cursor_position(cursor_position);
|
||||
const int32_t cursor_position = x - (screen_width / 2);
|
||||
if (!frequency_scale.is_live_tuning()) {
|
||||
// screen x to frequency scale x, NB we need two widgets align
|
||||
frequency_scale.set_cursor_position(cursor_position);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -405,6 +497,7 @@ void WaterfallView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
|
||||
sampling_rate = spectrum.sampling_rate;
|
||||
frequency_scale.set_spectrum_sampling_rate(sampling_rate);
|
||||
frequency_scale.set_channel_filter(
|
||||
spectrum.channel_filter_offset,
|
||||
spectrum.channel_filter_low_frequency,
|
||||
spectrum.channel_filter_high_frequency,
|
||||
spectrum.channel_filter_transition);
|
||||
|
||||
@@ -72,6 +72,8 @@ class AudioSpectrumView : public View {
|
||||
|
||||
class FrequencyScale : public Widget {
|
||||
public:
|
||||
/* Receives a frequency offset in Hz for key/touch selection. With live
|
||||
* tuning enabled, encoder events instead pass the raw encoder delta. */
|
||||
std::function<void(int32_t offset)> on_select{};
|
||||
|
||||
void on_show() override;
|
||||
@@ -83,8 +85,10 @@ class FrequencyScale : public Widget {
|
||||
bool on_touch(const TouchEvent touch) override;
|
||||
|
||||
void set_spectrum_sampling_rate(const int new_sampling_rate);
|
||||
void set_channel_filter(const int low_frequency, const int high_frequency, const int transition);
|
||||
void set_channel_filter(const int offset, const int low_frequency, const int high_frequency, const int transition);
|
||||
void set_cursor_position(const int32_t position);
|
||||
void set_live_tuning(const bool enabled) { live_tuning = enabled; }
|
||||
bool is_live_tuning() const { return live_tuning; }
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
@@ -94,15 +98,19 @@ class FrequencyScale : public Widget {
|
||||
int32_t cursor_position{0};
|
||||
int spectrum_sampling_rate{0};
|
||||
const int spectrum_bins = std::tuple_size<decltype(ChannelSpectrum::db)>::value;
|
||||
int channel_filter_offset{0};
|
||||
int channel_filter_low_frequency{0};
|
||||
int channel_filter_high_frequency{0};
|
||||
int channel_filter_transition{0};
|
||||
bool live_tuning{false};
|
||||
|
||||
void clear();
|
||||
void clear_background(Painter& painter, const Rect r);
|
||||
|
||||
void draw_frequency_ticks(Painter& painter, const Rect r);
|
||||
void draw_filter_ranges(Painter& painter, const Rect r);
|
||||
void redraw_filter_cursor(const int old_offset);
|
||||
void restore_tick_lines(Painter& painter, const Rect r, const Rect dirty);
|
||||
};
|
||||
|
||||
/* NB: These visualizations rely on having a baseband image running.
|
||||
@@ -147,6 +155,7 @@ class WaterfallView : public View {
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void show_audio_spectrum_view(const bool show);
|
||||
void load_gradient();
|
||||
void set_live_tuning(const bool enabled) { frequency_scale.set_live_tuning(enabled); }
|
||||
|
||||
private:
|
||||
void update_widgets_rect();
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
#include "portapack.hpp"
|
||||
|
||||
#include "ui_about_simple.hpp"
|
||||
#include "ui_adsb_rx.hpp"
|
||||
#include "ui_aprs_rx.hpp"
|
||||
#include "ui_aprs_tx.hpp"
|
||||
#include "ui_btle_rx.hpp"
|
||||
#include "ui_debug.hpp"
|
||||
#include "ui_encoders.hpp"
|
||||
@@ -60,7 +57,6 @@
|
||||
#include "ui_battinfo.hpp"
|
||||
#include "ui_external_items_menu_loader.hpp"
|
||||
|
||||
#include "ais_app.hpp"
|
||||
#include "analog_audio_app.hpp"
|
||||
#include "ble_rx_app.hpp"
|
||||
#include "ble_tx_app.hpp"
|
||||
@@ -99,9 +95,6 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
{nullptr, "Games", HOME, Color::cyan(), &bitmap_icon_games, new ViewFactory<GamesMenuView>()},
|
||||
{nullptr, "Settings", HOME, Color::cyan(), &bitmap_icon_setup, new ViewFactory<SettingsMenuView>()},
|
||||
/* RX ********************************************************************/
|
||||
{"adsbrx", "ADS-B", RX, Color::green(), &bitmap_icon_adsb, new ViewFactory<ADSBRxView>()},
|
||||
{"ais", "AIS Boats", RX, Color::green(), &bitmap_icon_ais, new ViewFactory<AISAppView>()},
|
||||
{"aprsrx", "APRS", RX, Color::green(), &bitmap_icon_aprs, new ViewFactory<APRSRXView>()},
|
||||
{"audio", "Audio", RX, Color::green(), &bitmap_icon_speaker, new ViewFactory<AnalogAudioView>()},
|
||||
{"blerx", "BLE Rx", RX, Color::green(), &bitmap_icon_btle, new ViewFactory<BLERxView>()},
|
||||
{"pocsag", "POCSAG", RX, Color::green(), &bitmap_icon_pocsag, new ViewFactory<POCSAGAppView>()},
|
||||
@@ -110,7 +103,6 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
{"subghzd", "SubGhzD", RX, Color::yellow(), &bitmap_icon_remote, new ViewFactory<SubGhzDView>()},
|
||||
{"weather", "Weather", RX, Color::green(), &bitmap_icon_thermometer, new ViewFactory<WeatherView>()},
|
||||
/* TX ********************************************************************/
|
||||
{"aprstx", "APRS TX", TX, ui::Color::green(), &bitmap_icon_aprs, new ViewFactory<APRSTXView>()},
|
||||
{"bletx", "BLE Tx", TX, ui::Color::green(), &bitmap_icon_btle, new ViewFactory<BLETxView>()},
|
||||
{"ooktx", "OOK", TX, ui::Color::yellow(), &bitmap_icon_remote, new ViewFactory<EncodersView>()},
|
||||
{"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, new ViewFactory<RDSView>()},
|
||||
|
||||
@@ -124,6 +124,7 @@ set(CPPSRC
|
||||
dsp_goertzel.cpp
|
||||
matched_filter.cpp
|
||||
spectrum_collector.cpp
|
||||
filtered_spectrum_collector.cpp
|
||||
tv_collector.cpp
|
||||
stream_input.cpp
|
||||
stream_output.cpp
|
||||
@@ -747,13 +748,6 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PRTT rtty_tx)
|
||||
|
||||
### SD over USB
|
||||
|
||||
set(MODE_INCDIR
|
||||
${HACKRF_PATH}/firmware
|
||||
${HACKRF_PATH}/firmware/common
|
||||
${HACKRF_PATH}/firmware/libopencm3/include
|
||||
)
|
||||
|
||||
### Morse TX
|
||||
|
||||
@@ -785,6 +779,24 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PTSK time_sink)
|
||||
|
||||
|
||||
### Tetra Rx
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_tetra.cpp
|
||||
)
|
||||
DeclareTargets(PTET tetra_rx)
|
||||
|
||||
### SD over USB
|
||||
|
||||
set(MODE_INCDIR
|
||||
${HACKRF_PATH}/firmware
|
||||
${HACKRF_PATH}/firmware/common
|
||||
${HACKRF_PATH}/firmware/libopencm3/include
|
||||
${HACKRF_PATH}/firmware/hackrf_usb
|
||||
sd_over_usb/
|
||||
)
|
||||
|
||||
set(MODE_CPPSRC
|
||||
sd_over_usb/proc_sd_over_usb.cpp
|
||||
|
||||
@@ -792,7 +804,9 @@ set(MODE_CPPSRC
|
||||
sd_over_usb/diskio.c
|
||||
sd_over_usb/sd_over_usb.c
|
||||
sd_over_usb/usb_descriptor.c
|
||||
sd_over_usb/hackrf_core.c
|
||||
sd_over_usb/usb_api_transceiver.c
|
||||
sd_over_usb/cpu_clock.c
|
||||
sd_over_usb/pins.c
|
||||
|
||||
${HACKRF_PATH}/firmware/common/adc.c
|
||||
${HACKRF_PATH}/firmware/common/selftest.c
|
||||
@@ -819,6 +833,15 @@ set(MODE_CPPSRC
|
||||
${HACKRF_PATH}/firmware/common/rffc5071_spi.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071.c
|
||||
${HACKRF_PATH}/firmware/common/gpdma.c
|
||||
${HACKRF_PATH}/firmware/common/clock_gen.c
|
||||
${HACKRF_PATH}/firmware/common/cpld_jtag.c
|
||||
${HACKRF_PATH}/firmware/common/leds.c
|
||||
${HACKRF_PATH}/firmware/common/power.c
|
||||
${HACKRF_PATH}/firmware/common/clock_io.c
|
||||
${HACKRF_PATH}/firmware/common/rom_iap.c
|
||||
${HACKRF_PATH}/firmware/common/w25q80bv.c
|
||||
${HACKRF_PATH}/firmware/common/w25q80bv_target.c
|
||||
|
||||
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/nvic.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/sync.c
|
||||
@@ -826,10 +849,7 @@ set(MODE_CPPSRC
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/timer.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/i2c.c
|
||||
)
|
||||
# sd_over_usb has type conflicts with PRALINE (HackRF Pro) - disable for now
|
||||
if(NOT BOARD STREQUAL "PRALINE")
|
||||
DeclareTargets(PUSB sd_over_usb)
|
||||
endif()
|
||||
|
||||
|
||||
### HackRF "factory" firmware
|
||||
|
||||
@@ -337,6 +337,66 @@ buffer_c16_t FIRC16xR16x16Decim2::execute(
|
||||
src.sampling_rate / decimation_factor};
|
||||
}
|
||||
|
||||
// FIRC16xR16x63HalfbandDecim2 ////////////////////////////////////////////
|
||||
|
||||
void FIRC16xR16x63HalfbandDecim2::configure(
|
||||
const std::array<int16_t, taps_count>& taps) {
|
||||
std::copy(taps.cbegin(), taps.cend(), taps_.begin());
|
||||
reset();
|
||||
}
|
||||
|
||||
void FIRC16xR16x63HalfbandDecim2::reset() {
|
||||
samples_.fill({});
|
||||
samples_head_ = 0;
|
||||
}
|
||||
|
||||
buffer_c16_t FIRC16xR16x63HalfbandDecim2::execute(
|
||||
const buffer_c16_t& src,
|
||||
const buffer_c16_t& dst) {
|
||||
auto* dst_p = reinterpret_cast<uint32_t*>(dst.p);
|
||||
|
||||
for (size_t output = 0; output < src.count / decimation_factor; ++output) {
|
||||
for (size_t i = 0; i < decimation_factor; ++i) {
|
||||
const auto sample = src.p[output * decimation_factor + i];
|
||||
samples_[samples_head_] = sample;
|
||||
samples_[samples_head_ + taps_count] = sample;
|
||||
if (++samples_head_ == taps_count)
|
||||
samples_head_ = 0;
|
||||
}
|
||||
|
||||
int64_t real = 0;
|
||||
int64_t imag = 0;
|
||||
for (size_t tap = 0; tap < taps_count; tap += 4) {
|
||||
const auto sample_0 =
|
||||
*reinterpret_cast<const uint32_t*>(&samples_[samples_head_ + tap]);
|
||||
const auto sample_1 =
|
||||
*reinterpret_cast<const uint32_t*>(&samples_[samples_head_ + tap + 2]);
|
||||
const auto real_pair = __PKHBT(sample_0, sample_1, 16);
|
||||
const auto imag_pair = __PKHTB(sample_1, sample_0, 16);
|
||||
const auto taps_pair = uint32_t(uint16_t(taps_[tap])) |
|
||||
(uint32_t(uint16_t(taps_[tap + 2])) << 16);
|
||||
real = __SMLALD(real_pair, taps_pair, real);
|
||||
imag = __SMLALD(imag_pair, taps_pair, imag);
|
||||
}
|
||||
|
||||
const auto center =
|
||||
*reinterpret_cast<const uint32_t*>(&samples_[samples_head_ + taps_count / 2]);
|
||||
real += int16_t(center) * taps_[taps_count / 2];
|
||||
imag += int16_t(center >> 16) * taps_[taps_count / 2];
|
||||
|
||||
const auto real_s16 =
|
||||
__SSAT((real + (real >= 0 ? 32768 : -32768)) / 65536, 16);
|
||||
const auto imag_s16 =
|
||||
__SSAT((imag + (imag >= 0 ? 32768 : -32768)) / 65536, 16);
|
||||
*(dst_p++) = __PKHBT(real_s16, imag_s16, 16);
|
||||
}
|
||||
|
||||
return {
|
||||
dst.p,
|
||||
src.count / decimation_factor,
|
||||
src.sampling_rate / decimation_factor};
|
||||
}
|
||||
|
||||
// FIRC16xR16x32Decim8 ////////////////////////////////////////////////////
|
||||
|
||||
void FIRC16xR16x32Decim8::configure(
|
||||
@@ -625,10 +685,13 @@ buffer_s16_t FIR64AndDecimateBy2Real::execute(
|
||||
void FIRAndDecimateComplex::configure_common(
|
||||
const size_t taps_count,
|
||||
const size_t decimation_factor) {
|
||||
samples_ = std::make_unique<samples_t>(taps_count);
|
||||
/* Mirror the delay line so a convolution always sees one contiguous
|
||||
* taps_count window, even when the logical head wraps. */
|
||||
samples_ = std::make_unique<samples_t>(taps_count * 2);
|
||||
taps_reversed_ = std::make_unique<taps_t>(taps_count);
|
||||
taps_count_ = taps_count;
|
||||
decimation_factor_ = decimation_factor;
|
||||
samples_head_ = 0;
|
||||
}
|
||||
|
||||
buffer_c16_t FIRAndDecimateComplex::execute(
|
||||
@@ -647,15 +710,18 @@ buffer_c16_t FIRAndDecimateComplex::execute(
|
||||
const void* src_p = src.p;
|
||||
size_t outer_count = output_samples;
|
||||
while (outer_count > 0) {
|
||||
/* Put new samples into delay buffer */
|
||||
void* z_new_p = &samples_[taps_count_ - decimation_factor_];
|
||||
/* Put new samples into both halves of the mirrored ring. */
|
||||
for (size_t i = 0; i < decimation_factor_; i++) {
|
||||
*__SIMD32(z_new_p)++ = *__SIMD32(src_p)++;
|
||||
const uint32_t sample = *__SIMD32(src_p)++;
|
||||
*reinterpret_cast<uint32_t*>(&samples_[samples_head_]) = sample;
|
||||
*reinterpret_cast<uint32_t*>(&samples_[samples_head_ + taps_count_]) = sample;
|
||||
if (++samples_head_ == taps_count_)
|
||||
samples_head_ = 0;
|
||||
}
|
||||
|
||||
size_t loop_count = taps_count_ / 8;
|
||||
void* t_p = &taps_reversed_[0];
|
||||
void* z_p = &samples_[0];
|
||||
void* z_p = &samples_[samples_head_];
|
||||
|
||||
int64_t t_real = 0;
|
||||
int64_t t_imag = 0;
|
||||
@@ -712,27 +778,6 @@ buffer_c16_t FIRAndDecimateComplex::execute(
|
||||
i_sat,
|
||||
16);
|
||||
|
||||
/* Shift sample buffer left/down by decimation factor. */
|
||||
const size_t unroll_factor = 4;
|
||||
size_t shift_count = (taps_count_ - decimation_factor_) / unroll_factor;
|
||||
|
||||
void* t = &samples_[0];
|
||||
const void* s = &samples_[decimation_factor_];
|
||||
|
||||
while (shift_count > 0) {
|
||||
*__SIMD32(t)++ = *__SIMD32(s)++;
|
||||
*__SIMD32(t)++ = *__SIMD32(s)++;
|
||||
*__SIMD32(t)++ = *__SIMD32(s)++;
|
||||
*__SIMD32(t)++ = *__SIMD32(s)++;
|
||||
shift_count--;
|
||||
}
|
||||
|
||||
shift_count = (taps_count_ - decimation_factor_) % unroll_factor;
|
||||
while (shift_count > 0) {
|
||||
*__SIMD32(t)++ = *__SIMD32(s)++;
|
||||
shift_count--;
|
||||
}
|
||||
|
||||
outer_count--;
|
||||
}
|
||||
|
||||
|
||||
@@ -170,6 +170,24 @@ class FIRC16xR16x16Decim2 {
|
||||
int32_t output_scale = 0;
|
||||
};
|
||||
|
||||
class FIRC16xR16x63HalfbandDecim2 {
|
||||
public:
|
||||
static constexpr size_t taps_count = 63;
|
||||
static constexpr size_t decimation_factor = 2;
|
||||
|
||||
void configure(const std::array<int16_t, taps_count>& taps);
|
||||
void reset();
|
||||
|
||||
buffer_c16_t execute(
|
||||
const buffer_c16_t& src,
|
||||
const buffer_c16_t& dst);
|
||||
|
||||
private:
|
||||
alignas(4) std::array<complex16_t, taps_count * 2> samples_{};
|
||||
alignas(4) std::array<int16_t, taps_count> taps_{};
|
||||
size_t samples_head_{0};
|
||||
};
|
||||
|
||||
class FIRC16xR16x32Decim8 {
|
||||
public:
|
||||
static constexpr size_t taps_count = 32;
|
||||
@@ -210,6 +228,23 @@ class FIRAndDecimateComplex {
|
||||
configure(taps.data(), taps.size(), decimation_factor);
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
void configure(
|
||||
const std::array<int16_t, N>& taps,
|
||||
const size_t decimation_factor) {
|
||||
configure_common(N, decimation_factor);
|
||||
for (size_t i = 0; i < N; ++i) {
|
||||
taps_reversed_[i] = {taps[N - 1 - i], 0};
|
||||
}
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
void set_taps(const std::array<complex16_t, N>& taps) {
|
||||
if (N == taps_count_) {
|
||||
std::reverse_copy(taps.begin(), taps.end(), &taps_reversed_[0]);
|
||||
}
|
||||
}
|
||||
|
||||
buffer_c16_t execute(
|
||||
const buffer_c16_t& src,
|
||||
const buffer_c16_t& dst);
|
||||
@@ -221,6 +256,7 @@ class FIRAndDecimateComplex {
|
||||
std::unique_ptr<taps_t> taps_reversed_{};
|
||||
size_t taps_count_{0};
|
||||
size_t decimation_factor_{1};
|
||||
size_t samples_head_{0};
|
||||
|
||||
template <typename T>
|
||||
void configure(
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
/*
|
||||
* Copyright (C) 2026
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*/
|
||||
|
||||
#ifndef __DSP_FREQUENCY_XLATOR_H__
|
||||
#define __DSP_FREQUENCY_XLATOR_H__
|
||||
|
||||
#include "dsp_decimate.hpp"
|
||||
#include "dsp_types.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace dsp {
|
||||
|
||||
/* Fixed-point complex mixer for the Audio RX channelizer. */
|
||||
class FrequencyTranslator {
|
||||
public:
|
||||
FrequencyTranslator() {
|
||||
for (size_t i = 0; i < oscillator_q15_.size(); ++i) {
|
||||
oscillator_q15_[i] =
|
||||
static_cast<uint16_t>(sine_q15_[static_cast<uint8_t>(i + 64)]) |
|
||||
(static_cast<uint32_t>(
|
||||
static_cast<uint16_t>(sine_q15_[i]))
|
||||
<< 16);
|
||||
}
|
||||
}
|
||||
|
||||
void set_sample_rate(const uint32_t sampling_rate) {
|
||||
sampling_rate_ = sampling_rate;
|
||||
update_phase_increment();
|
||||
}
|
||||
|
||||
void set_frequency(const int32_t frequency) {
|
||||
frequency_ = frequency;
|
||||
update_phase_increment();
|
||||
}
|
||||
|
||||
buffer_c16_t execute(const buffer_c16_t& src, const buffer_c16_t& dst) {
|
||||
auto phase = phase_;
|
||||
for (size_t i = 0; i < src.count; ++i) {
|
||||
const uint8_t index = phase >> 24;
|
||||
const uint8_t next = index + 1;
|
||||
const int32_t fraction = (phase >> 16) & 0xff;
|
||||
const uint32_t oscillator_first = oscillator_q15_[index];
|
||||
const uint32_t oscillator_next = oscillator_q15_[next];
|
||||
const int32_t sine_first =
|
||||
static_cast<int16_t>(oscillator_first >> 16);
|
||||
const int32_t cosine_first =
|
||||
static_cast<int16_t>(oscillator_first);
|
||||
const int32_t sine =
|
||||
sine_first +
|
||||
(((static_cast<int16_t>(oscillator_next >> 16) -
|
||||
sine_first) *
|
||||
fraction) >>
|
||||
8);
|
||||
const int32_t cosine =
|
||||
cosine_first +
|
||||
(((static_cast<int16_t>(oscillator_next) -
|
||||
cosine_first) *
|
||||
fraction) >>
|
||||
8);
|
||||
const uint32_t oscillator =
|
||||
static_cast<uint16_t>(cosine) |
|
||||
(static_cast<uint32_t>(
|
||||
static_cast<uint16_t>(sine))
|
||||
<< 16);
|
||||
const uint32_t sample =
|
||||
*reinterpret_cast<const uint32_t*>(&src.p[i]);
|
||||
|
||||
/* Two packed dual-16-bit multiplies implement
|
||||
* (I+jQ) * (cos-j sin). */
|
||||
const int32_t out_i =
|
||||
rounded_shift(__SMUAD(sample, oscillator), 15);
|
||||
const int32_t out_q =
|
||||
rounded_shift(__SMUSDX(oscillator, sample), 15);
|
||||
*reinterpret_cast<uint32_t*>(&dst.p[i]) =
|
||||
__PKHBT(__SSAT(out_i, 16), __SSAT(out_q, 16), 16);
|
||||
phase += phase_increment_;
|
||||
}
|
||||
phase_ = phase;
|
||||
return {dst.p, src.count, src.sampling_rate};
|
||||
}
|
||||
|
||||
private:
|
||||
friend class FrequencyTranslatingDecimator32By8;
|
||||
|
||||
static constexpr int32_t rounded_shift(
|
||||
const int32_t value,
|
||||
const uint32_t bits) {
|
||||
const int32_t rounding = int32_t{1} << (bits - 1);
|
||||
return value >= 0
|
||||
? (value + rounding) >> bits
|
||||
: -((-value + rounding) >> bits);
|
||||
}
|
||||
|
||||
void update_phase_increment() {
|
||||
if (sampling_rate_) {
|
||||
phase_increment_ = static_cast<uint32_t>(
|
||||
(static_cast<int64_t>(frequency_) * (int64_t{1} << 32)) /
|
||||
sampling_rate_);
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr std::array<int16_t, 256> sine_q15_{{
|
||||
0,
|
||||
804,
|
||||
1608,
|
||||
2410,
|
||||
3212,
|
||||
4011,
|
||||
4808,
|
||||
5602,
|
||||
6393,
|
||||
7179,
|
||||
7962,
|
||||
8739,
|
||||
9512,
|
||||
10278,
|
||||
11039,
|
||||
11793,
|
||||
12539,
|
||||
13279,
|
||||
14010,
|
||||
14732,
|
||||
15446,
|
||||
16151,
|
||||
16846,
|
||||
17530,
|
||||
18204,
|
||||
18868,
|
||||
19519,
|
||||
20159,
|
||||
20787,
|
||||
21403,
|
||||
22005,
|
||||
22594,
|
||||
23170,
|
||||
23731,
|
||||
24279,
|
||||
24811,
|
||||
25329,
|
||||
25832,
|
||||
26319,
|
||||
26790,
|
||||
27245,
|
||||
27683,
|
||||
28105,
|
||||
28510,
|
||||
28898,
|
||||
29268,
|
||||
29621,
|
||||
29956,
|
||||
30273,
|
||||
30571,
|
||||
30852,
|
||||
31113,
|
||||
31356,
|
||||
31580,
|
||||
31785,
|
||||
31971,
|
||||
32137,
|
||||
32285,
|
||||
32412,
|
||||
32521,
|
||||
32609,
|
||||
32678,
|
||||
32728,
|
||||
32757,
|
||||
32767,
|
||||
32757,
|
||||
32728,
|
||||
32678,
|
||||
32609,
|
||||
32521,
|
||||
32412,
|
||||
32285,
|
||||
32137,
|
||||
31971,
|
||||
31785,
|
||||
31580,
|
||||
31356,
|
||||
31113,
|
||||
30852,
|
||||
30571,
|
||||
30273,
|
||||
29956,
|
||||
29621,
|
||||
29268,
|
||||
28898,
|
||||
28510,
|
||||
28105,
|
||||
27683,
|
||||
27245,
|
||||
26790,
|
||||
26319,
|
||||
25832,
|
||||
25329,
|
||||
24811,
|
||||
24279,
|
||||
23731,
|
||||
23170,
|
||||
22594,
|
||||
22005,
|
||||
21403,
|
||||
20787,
|
||||
20159,
|
||||
19519,
|
||||
18868,
|
||||
18204,
|
||||
17530,
|
||||
16846,
|
||||
16151,
|
||||
15446,
|
||||
14732,
|
||||
14010,
|
||||
13279,
|
||||
12539,
|
||||
11793,
|
||||
11039,
|
||||
10278,
|
||||
9512,
|
||||
8739,
|
||||
7962,
|
||||
7179,
|
||||
6393,
|
||||
5602,
|
||||
4808,
|
||||
4011,
|
||||
3212,
|
||||
2410,
|
||||
1608,
|
||||
804,
|
||||
0,
|
||||
-804,
|
||||
-1608,
|
||||
-2410,
|
||||
-3212,
|
||||
-4011,
|
||||
-4808,
|
||||
-5602,
|
||||
-6393,
|
||||
-7179,
|
||||
-7962,
|
||||
-8739,
|
||||
-9512,
|
||||
-10278,
|
||||
-11039,
|
||||
-11793,
|
||||
-12539,
|
||||
-13279,
|
||||
-14010,
|
||||
-14732,
|
||||
-15446,
|
||||
-16151,
|
||||
-16846,
|
||||
-17530,
|
||||
-18204,
|
||||
-18868,
|
||||
-19519,
|
||||
-20159,
|
||||
-20787,
|
||||
-21403,
|
||||
-22005,
|
||||
-22594,
|
||||
-23170,
|
||||
-23731,
|
||||
-24279,
|
||||
-24811,
|
||||
-25329,
|
||||
-25832,
|
||||
-26319,
|
||||
-26790,
|
||||
-27245,
|
||||
-27683,
|
||||
-28105,
|
||||
-28510,
|
||||
-28898,
|
||||
-29268,
|
||||
-29621,
|
||||
-29956,
|
||||
-30273,
|
||||
-30571,
|
||||
-30852,
|
||||
-31113,
|
||||
-31356,
|
||||
-31580,
|
||||
-31785,
|
||||
-31971,
|
||||
-32137,
|
||||
-32285,
|
||||
-32412,
|
||||
-32521,
|
||||
-32609,
|
||||
-32678,
|
||||
-32728,
|
||||
-32757,
|
||||
-32767,
|
||||
-32757,
|
||||
-32728,
|
||||
-32678,
|
||||
-32609,
|
||||
-32521,
|
||||
-32412,
|
||||
-32285,
|
||||
-32137,
|
||||
-31971,
|
||||
-31785,
|
||||
-31580,
|
||||
-31356,
|
||||
-31113,
|
||||
-30852,
|
||||
-30571,
|
||||
-30273,
|
||||
-29956,
|
||||
-29621,
|
||||
-29268,
|
||||
-28898,
|
||||
-28510,
|
||||
-28105,
|
||||
-27683,
|
||||
-27245,
|
||||
-26790,
|
||||
-26319,
|
||||
-25832,
|
||||
-25329,
|
||||
-24811,
|
||||
-24279,
|
||||
-23731,
|
||||
-23170,
|
||||
-22594,
|
||||
-22005,
|
||||
-21403,
|
||||
-20787,
|
||||
-20159,
|
||||
-19519,
|
||||
-18868,
|
||||
-18204,
|
||||
-17530,
|
||||
-16846,
|
||||
-16151,
|
||||
-15446,
|
||||
-14732,
|
||||
-14010,
|
||||
-13279,
|
||||
-12539,
|
||||
-11793,
|
||||
-11039,
|
||||
-10278,
|
||||
-9512,
|
||||
-8739,
|
||||
-7962,
|
||||
-7179,
|
||||
-6393,
|
||||
-5602,
|
||||
-4808,
|
||||
-4011,
|
||||
-3212,
|
||||
-2410,
|
||||
-1608,
|
||||
-804,
|
||||
}};
|
||||
|
||||
std::array<uint32_t, 256> oscillator_q15_{};
|
||||
uint32_t phase_{0};
|
||||
uint32_t phase_increment_{0};
|
||||
uint32_t sampling_rate_{192000};
|
||||
int32_t frequency_{0};
|
||||
};
|
||||
|
||||
/*
|
||||
* Frequency-translating 32-tap FIR decimator. Frequency translation is
|
||||
* split between coefficients modulated when tuning changes and a cheap
|
||||
* output-rate phase rotation. This avoids running an NCO at the 384kHz
|
||||
* input rate.
|
||||
*/
|
||||
class FrequencyTranslatingDecimator32By8 {
|
||||
public:
|
||||
static constexpr size_t decimation_factor = 8;
|
||||
static constexpr size_t taps_count = 32;
|
||||
|
||||
void configure(
|
||||
const std::array<int16_t, taps_count>& taps,
|
||||
const uint32_t input_sampling_rate) {
|
||||
taps_ = taps;
|
||||
input_sampling_rate_ = input_sampling_rate;
|
||||
decimator_.configure(complex_taps_, decimation_factor);
|
||||
output_xlator_.set_sample_rate(input_sampling_rate / decimation_factor);
|
||||
update_taps();
|
||||
}
|
||||
|
||||
void set_frequency(const int32_t frequency) {
|
||||
frequency_ = frequency;
|
||||
output_xlator_.set_frequency(frequency);
|
||||
update_taps();
|
||||
}
|
||||
|
||||
buffer_c16_t execute(
|
||||
const buffer_c16_t& src,
|
||||
const buffer_c16_t& dst) {
|
||||
const auto filtered = decimator_.execute(src, dst);
|
||||
return output_xlator_.execute(filtered, dst);
|
||||
}
|
||||
|
||||
private:
|
||||
static void oscillator(
|
||||
const uint32_t phase,
|
||||
int32_t& sine,
|
||||
int32_t& cosine) {
|
||||
const uint8_t index = phase >> 24;
|
||||
const uint8_t next = index + 1;
|
||||
const uint8_t cosine_index = index + 64;
|
||||
const uint8_t cosine_next = cosine_index + 1;
|
||||
const int32_t fraction = (phase >> 16) & 0xff;
|
||||
const int32_t sine_first = FrequencyTranslator::sine_q15_[index];
|
||||
const int32_t cosine_first = FrequencyTranslator::sine_q15_[cosine_index];
|
||||
sine = sine_first +
|
||||
(((FrequencyTranslator::sine_q15_[next] - sine_first) * fraction) >> 8);
|
||||
cosine = cosine_first +
|
||||
(((FrequencyTranslator::sine_q15_[cosine_next] - cosine_first) * fraction) >> 8);
|
||||
}
|
||||
|
||||
void update_taps() {
|
||||
if (!input_sampling_rate_)
|
||||
return;
|
||||
|
||||
const uint32_t tap_phase_increment = static_cast<uint32_t>(
|
||||
(static_cast<int64_t>(frequency_) * (int64_t{1} << 32)) /
|
||||
input_sampling_rate_);
|
||||
|
||||
/* Centre the modulation on the FIR midpoint. Besides changing
|
||||
* only a constant output phase, this makes the two coefficients
|
||||
* in each symmetric pair complex conjugates. That property is
|
||||
* important after quantization: starting at tap zero accumulated
|
||||
* a one-sided phase and rounding error across the whole filter. */
|
||||
uint32_t phase = static_cast<uint32_t>(
|
||||
-((static_cast<int64_t>(
|
||||
static_cast<int32_t>(tap_phase_increment)) *
|
||||
static_cast<int64_t>(taps_count - 1)) /
|
||||
2));
|
||||
for (size_t i = 0; i < taps_count; ++i) {
|
||||
int32_t sine;
|
||||
int32_t cosine;
|
||||
oscillator(phase, sine, cosine);
|
||||
/* FIRAndDecimateComplex uses Q16 coefficients; the source
|
||||
* real-tap filters use Q15 coefficients. */
|
||||
const int32_t tap = taps_[i];
|
||||
complex_taps_[i] = {
|
||||
static_cast<int16_t>(
|
||||
FrequencyTranslator::rounded_shift(
|
||||
tap * cosine, 14)),
|
||||
static_cast<int16_t>(
|
||||
FrequencyTranslator::rounded_shift(
|
||||
tap * sine, 14))};
|
||||
phase += tap_phase_increment;
|
||||
}
|
||||
decimator_.set_taps(complex_taps_);
|
||||
}
|
||||
|
||||
std::array<int16_t, taps_count> taps_{};
|
||||
std::array<complex16_t, taps_count> complex_taps_{};
|
||||
decimate::FIRAndDecimateComplex decimator_{};
|
||||
FrequencyTranslator output_xlator_{};
|
||||
uint32_t input_sampling_rate_{0};
|
||||
int32_t frequency_{0};
|
||||
};
|
||||
|
||||
} /* namespace dsp */
|
||||
|
||||
#endif /*__DSP_FREQUENCY_XLATOR_H__*/
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "filtered_spectrum_collector.hpp"
|
||||
|
||||
#include "dsp_fir_taps.hpp"
|
||||
#include "event_m4.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
void FilteredSpectrumCollector::on_message(const Message* const message) {
|
||||
if (message->id == Message::ID::UpdateSpectrum) {
|
||||
update();
|
||||
}
|
||||
SpectrumCollector::on_message(message);
|
||||
}
|
||||
|
||||
void FilteredSpectrumCollector::start_capture(
|
||||
const size_t decimation_factor) {
|
||||
capture_decimation_ = decimation_factor;
|
||||
capture_count_ = 0;
|
||||
capture_ready_ = false;
|
||||
}
|
||||
|
||||
bool FilteredSpectrumCollector::feed(
|
||||
const buffer_c16_t& channel,
|
||||
const int32_t filter_low_frequency,
|
||||
const int32_t filter_high_frequency,
|
||||
const int32_t filter_transition) {
|
||||
set_filter(
|
||||
filter_low_frequency,
|
||||
filter_high_frequency,
|
||||
filter_transition);
|
||||
|
||||
const size_t required_samples = 256 * capture_decimation_;
|
||||
const size_t copy_count = std::min(
|
||||
channel.count, required_samples - capture_count_);
|
||||
std::copy_n(
|
||||
channel.p,
|
||||
copy_count,
|
||||
capture_.begin() + capture_count_);
|
||||
capture_count_ += copy_count;
|
||||
|
||||
if (capture_count_ == required_samples) {
|
||||
sampling_rate_ = channel.sampling_rate / capture_decimation_;
|
||||
capture_ready_ = true;
|
||||
EventDispatcher::events_flag(EVT_MASK_SPECTRUM);
|
||||
if (is_streaming()) {
|
||||
capture_ready_ = true;
|
||||
EventDispatcher::events_flag(EVT_MASK_SPECTRUM);
|
||||
} else {
|
||||
capture_ready_ = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void FilteredSpectrumCollector::update() {
|
||||
if (!capture_ready_) {
|
||||
return;
|
||||
}
|
||||
|
||||
decim_0_.configure(taps_audio_spectrum_halfband.taps);
|
||||
const buffer_c16_t capture{
|
||||
capture_.data(),
|
||||
256 * capture_decimation_,
|
||||
sampling_rate_ * capture_decimation_};
|
||||
const buffer_c16_t stage_0{
|
||||
stage_0_.data(),
|
||||
stage_0_.size()};
|
||||
const auto filtered = decim_0_.execute(capture, stage_0);
|
||||
|
||||
if (capture_decimation_ == 4) {
|
||||
decim_1_.configure(taps_audio_spectrum_halfband.taps);
|
||||
const buffer_c16_t stage_1{
|
||||
stage_1_.data(),
|
||||
stage_1_.size()};
|
||||
post_message(decim_1_.execute(filtered, stage_1));
|
||||
} else {
|
||||
post_message(filtered);
|
||||
}
|
||||
capture_ready_ = false;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef __FILTERED_SPECTRUM_COLLECTOR_H__
|
||||
#define __FILTERED_SPECTRUM_COLLECTOR_H__
|
||||
|
||||
#include "dsp_decimate.hpp"
|
||||
#include "spectrum_collector.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
class FilteredSpectrumCollector : public SpectrumCollector {
|
||||
public:
|
||||
void on_message(const Message* const message);
|
||||
|
||||
void start_capture(const size_t decimation_factor);
|
||||
bool feed(
|
||||
const buffer_c16_t& channel,
|
||||
const int32_t filter_low_frequency,
|
||||
const int32_t filter_high_frequency,
|
||||
const int32_t filter_transition);
|
||||
|
||||
private:
|
||||
std::array<complex16_t, 1024> capture_{};
|
||||
std::array<complex16_t, 512> stage_0_{};
|
||||
std::array<complex16_t, 256> stage_1_{};
|
||||
dsp::decimate::FIRC16xR16x63HalfbandDecim2 decim_0_{};
|
||||
dsp::decimate::FIRC16xR16x63HalfbandDecim2 decim_1_{};
|
||||
size_t capture_count_{0};
|
||||
size_t capture_decimation_{1};
|
||||
uint32_t sampling_rate_{0};
|
||||
bool capture_ready_{false};
|
||||
|
||||
void update();
|
||||
};
|
||||
|
||||
#endif /*__FILTERED_SPECTRUM_COLLECTOR_H__*/
|
||||
@@ -0,0 +1,124 @@
|
||||
|
||||
#ifndef __FPROTO_SUPERROLLO_H__
|
||||
#define __FPROTO_SUPERROLLO_H__
|
||||
|
||||
#include "subghzdbase.hpp"
|
||||
|
||||
// Superrollo GW60 (HCS361) 67-bit OOK decoder. Reports FPS_SUPERROLLO; SubGhzD
|
||||
// treats FPS_SUPERROLLO like KeeLoq for decrypt/display (see ui_subghzd.cpp), so
|
||||
// the "Superrollo" keystore key decrypts it while it stays identifiable in logs.
|
||||
|
||||
typedef enum : uint8_t {
|
||||
SuperrolloStepReset = 0,
|
||||
SuperrolloStepPreambleLow,
|
||||
SuperrolloStepSyncLow,
|
||||
SuperrolloStepSaveDuration,
|
||||
SuperrolloStepCheckDuration,
|
||||
} SuperrolloDecoderStep;
|
||||
|
||||
class FProtoSubGhzDSuperrollo : public FProtoSubGhzDBase {
|
||||
public:
|
||||
FProtoSubGhzDSuperrollo() {
|
||||
sensorType = FPS_SUPERROLLO;
|
||||
te_short = 450;
|
||||
te_long = 900;
|
||||
te_delta = 200;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case SuperrolloStepReset:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
header_count++;
|
||||
parser_step = SuperrolloStepPreambleLow;
|
||||
} else if (
|
||||
(header_count >= 4) &&
|
||||
(DURATION_DIFF(duration, te_short * 10) < te_delta * 10)) {
|
||||
parser_step = SuperrolloStepSyncLow;
|
||||
} else {
|
||||
header_count = 0;
|
||||
}
|
||||
} else {
|
||||
header_count = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SuperrolloStepPreambleLow:
|
||||
if ((!level) && (DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
parser_step = SuperrolloStepReset;
|
||||
} else {
|
||||
parser_step = SuperrolloStepReset;
|
||||
header_count = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SuperrolloStepSyncLow:
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short * 10) < te_delta * 10)) {
|
||||
parser_step = SuperrolloStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
} else {
|
||||
parser_step = SuperrolloStepReset;
|
||||
header_count = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SuperrolloStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = SuperrolloStepCheckDuration;
|
||||
} else if (duration >= te_short * 12) {
|
||||
endFrame();
|
||||
}
|
||||
break;
|
||||
|
||||
case SuperrolloStepCheckDuration:
|
||||
if (!level) {
|
||||
if ((DURATION_DIFF(te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
if (decode_count_bit < min_count_bit_for_found)
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
else
|
||||
decode_count_bit++;
|
||||
parser_step = SuperrolloStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
if (decode_count_bit < min_count_bit_for_found)
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
else
|
||||
decode_count_bit++;
|
||||
parser_step = SuperrolloStepSaveDuration;
|
||||
} else if (duration >= te_short * 12) {
|
||||
endFrame();
|
||||
} else {
|
||||
parser_step = SuperrolloStepReset;
|
||||
header_count = 0;
|
||||
}
|
||||
} else {
|
||||
parser_step = SuperrolloStepReset;
|
||||
header_count = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t header_count = 0;
|
||||
|
||||
private:
|
||||
void endFrame() {
|
||||
if ((decode_count_bit >= min_count_bit_for_found) &&
|
||||
(decode_count_bit <= min_count_bit_for_found + 3)) {
|
||||
data_count_bit = min_count_bit_for_found;
|
||||
if (callback) callback(this);
|
||||
}
|
||||
parser_step = SuperrolloStepReset;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
header_count = 0;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -29,6 +29,7 @@ So include here the .hpp, and add a new element to the protos vector in the cons
|
||||
#include "s-ido.hpp"
|
||||
#include "s-intertechnov3.hpp"
|
||||
#include "s-keeloq.hpp"
|
||||
#include "s-superrollo.hpp"
|
||||
#include "s-kinggates_stylo_4k.hpp"
|
||||
#include "s-linear.hpp"
|
||||
#include "s-linear_delta3.hpp"
|
||||
@@ -85,6 +86,7 @@ class SubGhzDProtos : public FProtoListGeneral {
|
||||
protos[FPS_IDO] = new FProtoSubGhzDIdo();
|
||||
protos[FPS_INTERTECHNOV3] = new FProtoSubGhzDIntertechnoV3();
|
||||
protos[FPS_KEELOQ] = new FProtoSubGhzDKeeLoq();
|
||||
protos[FPS_SUPERROLLO] = new FProtoSubGhzDSuperrollo();
|
||||
protos[FPS_KINGGATESSTYLO4K] = new FProtoSubGhzDKinggatesStylo4K();
|
||||
protos[FPS_LINEAR] = new FProtoSubGhzDLinear();
|
||||
protos[FPS_LINEARDELTA3] = new FProtoSubGhzDLinearDelta3();
|
||||
|
||||
@@ -60,6 +60,7 @@ enum FPROTO_SUBGHZD_SENSOR : uint8_t {
|
||||
FPS_MARANTEC24,
|
||||
FPS_HOLTEKHT6P20B,
|
||||
FPS_RESTAURANT_PAGER,
|
||||
FPS_SUPERROLLO,
|
||||
FPS_COUNT
|
||||
};
|
||||
|
||||
|
||||
@@ -45,10 +45,26 @@ void NarrowbandAMAudio::execute(const buffer_c8_t& 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 audio_decim_0_out = audio_decim_0.execute(decim_0_out, dst_buffer);
|
||||
|
||||
channel_spectrum.feed(decim_1_out, channel_filter_low_f, channel_filter_high_f, channel_filter_transition);
|
||||
spectrum_samples += decim_0_out.count;
|
||||
if (!spectrum_capture_active &&
|
||||
spectrum_samples >= spectrum_interval_samples) {
|
||||
spectrum_samples -= spectrum_interval_samples;
|
||||
channel_spectrum.start_capture(spectrum_zoom_x2 ? 4 : 2);
|
||||
spectrum_capture_active = true;
|
||||
}
|
||||
|
||||
if (spectrum_capture_active &&
|
||||
channel_spectrum.feed(
|
||||
audio_decim_0_out,
|
||||
channel_filter_low_f,
|
||||
channel_filter_high_f,
|
||||
channel_filter_transition)) {
|
||||
spectrum_capture_active = false;
|
||||
}
|
||||
|
||||
const auto decim_1_out = translating_decim_1.execute(audio_decim_0_out, dst_buffer);
|
||||
const auto decim_2_out = decim_2.execute(decim_1_out, dst_buffer);
|
||||
const auto channel_out = channel_filter.execute(decim_2_out, dst_buffer);
|
||||
|
||||
@@ -97,6 +113,10 @@ void NarrowbandAMAudio::on_message(const Message* const message) {
|
||||
capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
|
||||
break;
|
||||
|
||||
case Message::ID::AudioDDCConfig:
|
||||
ddc_config(*reinterpret_cast<const AudioDDCConfigMessage*>(message));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -106,17 +126,19 @@ void NarrowbandAMAudio::configure(const AMConfigureMessage& message) {
|
||||
constexpr size_t decim_0_input_fs = baseband_fs;
|
||||
constexpr size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor;
|
||||
|
||||
constexpr size_t decim_1_input_fs = decim_0_output_fs;
|
||||
constexpr size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
|
||||
|
||||
constexpr size_t audio_decim_0_output_fs = decim_0_output_fs / 2;
|
||||
constexpr size_t decim_1_output_fs =
|
||||
audio_decim_0_output_fs / translating_decim_1.decimation_factor;
|
||||
constexpr size_t decim_2_input_fs = decim_1_output_fs;
|
||||
constexpr size_t decim_2_output_fs = decim_2_input_fs / decim_2_decimation_factor;
|
||||
|
||||
constexpr size_t channel_filter_input_fs = decim_2_output_fs;
|
||||
// const size_t channel_filter_output_fs = channel_filter_input_fs / channel_filter_decimation_factor;
|
||||
|
||||
decim_0.configure(message.decim_0_filter.taps);
|
||||
decim_1.configure(message.decim_1_filter.taps);
|
||||
decim_0.configure(message.decim_0_filter.taps, 33554432);
|
||||
audio_decim_0.configure(taps_audio_wide_halfband_0.taps);
|
||||
translating_decim_1.configure(
|
||||
message.decim_1_filter.taps, audio_decim_0_output_fs);
|
||||
decim_2.configure(message.decim_2_filter.taps, decim_2_decimation_factor);
|
||||
channel_filter.configure(message.channel_filter.taps, channel_filter_decimation_factor);
|
||||
channel_filter_low_f = message.channel_filter.low_frequency_normalized * channel_filter_input_fs;
|
||||
@@ -124,12 +146,20 @@ void NarrowbandAMAudio::configure(const AMConfigureMessage& message) {
|
||||
channel_filter_transition = message.channel_filter.transition_normalized * channel_filter_input_fs;
|
||||
|
||||
modulation_ssb = (int)message.modulation; // now sending by message , 3 types of AM demod : enum class Modulation : int32_t {DSB = 0, SSB = 1, SSB_FM = 2}
|
||||
channel_spectrum.set_decimation_factor(message.channel_spectrum_decimation_factor);
|
||||
spectrum_zoom_x2 = message.channel_spectrum_decimation_factor == 2;
|
||||
channel_spectrum.set_decimation_factor(1);
|
||||
spectrum_interval_samples =
|
||||
decim_0_output_fs / spectrum_rate_hz;
|
||||
audio_output.configure(message.audio_hpf_lpf_config); // hpf in all AM demod modes (AM-6K/9K, USB/LSB,DSB), except Wefax (lpf there).
|
||||
|
||||
configured = true;
|
||||
}
|
||||
|
||||
void NarrowbandAMAudio::ddc_config(const AudioDDCConfigMessage& message) {
|
||||
translating_decim_1.set_frequency(message.frequency);
|
||||
channel_spectrum.set_channel_filter_offset(message.frequency);
|
||||
}
|
||||
|
||||
void NarrowbandAMAudio::capture_config(const CaptureConfigMessage& message) {
|
||||
if (message.config) {
|
||||
audio_output.set_stream(std::make_unique<StreamInput>(message.config));
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
|
||||
#include "dsp_decimate.hpp"
|
||||
#include "dsp_demodulate.hpp"
|
||||
#include "dsp_frequency_xlator.hpp"
|
||||
#include "audio_compressor.hpp"
|
||||
|
||||
#include "audio_output.hpp"
|
||||
#include "spectrum_collector.hpp"
|
||||
#include "filtered_spectrum_collector.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -44,6 +45,7 @@ class NarrowbandAMAudio : public BasebandProcessor {
|
||||
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 3072000;
|
||||
static constexpr auto spectrum_rate_hz = 30.0f;
|
||||
static constexpr size_t decim_2_decimation_factor = 4;
|
||||
static constexpr size_t channel_filter_decimation_factor = 1;
|
||||
|
||||
@@ -56,14 +58,19 @@ class NarrowbandAMAudio : public BasebandProcessor {
|
||||
audio.data(),
|
||||
audio.size()};
|
||||
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x16Decim2 audio_decim_0{};
|
||||
dsp::FrequencyTranslatingDecimator32By8 translating_decim_1{};
|
||||
dsp::decimate::FIRAndDecimateComplex decim_2{};
|
||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||
int32_t channel_filter_low_f = 0;
|
||||
int32_t channel_filter_high_f = 0;
|
||||
int32_t channel_filter_transition = 0;
|
||||
bool configured{false};
|
||||
size_t spectrum_interval_samples{0};
|
||||
size_t spectrum_samples{0};
|
||||
bool spectrum_capture_active{false};
|
||||
bool spectrum_zoom_x2{false};
|
||||
|
||||
// bool modulation_ssb = false; // Origianlly we only had 2 AM demod types {DSB = 0, SSB = 1} , and we could handle it with bool var , 1 bit.
|
||||
int8_t modulation_ssb = 0; // Now we have 3 AM demod types we will send now index integer {DSB = 0, SSB = 1, SSB_FM = 2}
|
||||
@@ -73,7 +80,7 @@ class NarrowbandAMAudio : public BasebandProcessor {
|
||||
FeedForwardCompressor audio_compressor{};
|
||||
AudioOutput audio_output{};
|
||||
|
||||
SpectrumCollector channel_spectrum{};
|
||||
FilteredSpectrumCollector channel_spectrum{};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
#ifdef PRALINE
|
||||
@@ -86,6 +93,7 @@ class NarrowbandAMAudio : public BasebandProcessor {
|
||||
#endif
|
||||
|
||||
void configure(const AMConfigureMessage& message);
|
||||
void ddc_config(const AudioDDCConfigMessage& message);
|
||||
void capture_config(const CaptureConfigMessage& message);
|
||||
|
||||
buffer_f32_t demodulate(const buffer_c16_t& channel);
|
||||
|
||||
@@ -27,6 +27,70 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
uint8_t EPIRBTXProcessor::get_frame_bit(uint16_t bit_pos) const {
|
||||
// Skip first 6 bits (padding for 250 bits -> 32 bytes)
|
||||
bit_pos += 6;
|
||||
if (bit_pos >= frame_sgb_bits_len) {
|
||||
return 0;
|
||||
}
|
||||
const uint8_t byte_value = frame_data[bit_pos >> 3];
|
||||
const uint8_t bit_offset = 7 - (bit_pos & 0x07);
|
||||
return (byte_value >> bit_offset) & 0x01;
|
||||
}
|
||||
|
||||
int8_t EPIRBTXProcessor::compute_sgb_chip_level(SGBChannelState& channel, bool q_channel) {
|
||||
// Table 2.4 behavior: chip = PRN for bit 0, inverted PRN for bit 1.
|
||||
// PRN generator (23-bit LFSR): out = reg[0], feedback = reg[0] XOR reg[18].
|
||||
const uint8_t prn_chip = channel.prn_state & 0x01;
|
||||
|
||||
uint8_t info_bit = 0;
|
||||
const uint32_t bit_in_channel = channel.chip_index / sgb_chips_per_bit;
|
||||
if (bit_in_channel >= sgb_preamble_bits_per_channel) {
|
||||
const uint32_t message_index = bit_in_channel - sgb_preamble_bits_per_channel;
|
||||
if (message_index < (sgb_message_bits / 2)) {
|
||||
const uint16_t frame_bit_pos = q_channel ? (message_index * 2) + 1 : (message_index * 2);
|
||||
info_bit = get_frame_bit(frame_bit_pos);
|
||||
}
|
||||
}
|
||||
|
||||
const uint8_t xor_chip = info_bit ^ prn_chip;
|
||||
const int8_t chip_level = xor_chip ? -sgb_chip_amplitude : sgb_chip_amplitude;
|
||||
|
||||
const uint8_t feedback = prn_chip ^ ((channel.prn_state >> 18) & 0x01);
|
||||
channel.prn_state = (channel.prn_state >> 1) | (uint32_t(feedback) << 22);
|
||||
channel.chip_index++;
|
||||
|
||||
return chip_level;
|
||||
}
|
||||
|
||||
int8_t EPIRBTXProcessor::sample_sgb_channel(SGBChannelState& channel, bool q_channel) {
|
||||
int8_t level = channel.chip_level;
|
||||
|
||||
if (channel.sample_in_chip == 0) {
|
||||
if (channel.chip_index < sgb_segment_chips) {
|
||||
channel.chip_level = compute_sgb_chip_level(channel, q_channel);
|
||||
level = channel.chip_level;
|
||||
} else {
|
||||
level = 0;
|
||||
channel.chip_level = 0;
|
||||
}
|
||||
}
|
||||
|
||||
channel.sample_in_chip++;
|
||||
if (channel.sample_in_chip >= sgb_samples_per_chip) {
|
||||
channel.sample_in_chip = 0;
|
||||
}
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
void EPIRBTXProcessor::init_sgb_channel(SGBChannelState& channel, uint32_t initial_state) {
|
||||
channel.prn_state = initial_state;
|
||||
channel.chip_index = 0;
|
||||
channel.sample_in_chip = 0;
|
||||
channel.chip_level = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processing method for this processor
|
||||
*/
|
||||
@@ -43,75 +107,92 @@ void EPIRBTXProcessor::execute(const buffer_c8_t& buffer) {
|
||||
shared_memory.application_queue.push(txprogress_message);
|
||||
}
|
||||
|
||||
if (mode_bpsk) {
|
||||
// BPSK Manchester beacon signal
|
||||
if (bpsk_pre_count < config_pre_count) {
|
||||
// Pre-count state: send carrier only during pre-count
|
||||
bpsk_pre_count++;
|
||||
re = i_carrier;
|
||||
im = q_carrier;
|
||||
} else if (bpsk_post_count > 0) {
|
||||
// Post-count: send carrier only during post-count
|
||||
bpsk_post_count++;
|
||||
re = i_carrier;
|
||||
im = q_carrier;
|
||||
if (bpsk_post_count >= config_post_count) {
|
||||
// End transmission here
|
||||
byte_index = 0;
|
||||
bpsk_post_count = 0;
|
||||
bpsk_pre_count = 0;
|
||||
if (mode_406) {
|
||||
if (mode_sgb) {
|
||||
// 2G SGB DSSS-OQPSK signal
|
||||
const int8_t i_sample = sample_sgb_channel(sgb_i, false);
|
||||
const int8_t q_sample = (sgb_sample_counter < sgb_half_chip_samples)
|
||||
? 0
|
||||
: sample_sgb_channel(sgb_q, true);
|
||||
|
||||
re = i_sample;
|
||||
im = q_sample;
|
||||
|
||||
sgb_sample_counter++;
|
||||
if (sgb_sample_counter >= sgb_total_samples) {
|
||||
sgb_sample_counter = 0;
|
||||
end_of_transmission = true;
|
||||
}
|
||||
} else {
|
||||
if (sample_counter == 0 && manchester_half == false) {
|
||||
if (bit_index == 0) {
|
||||
// Read current byte
|
||||
current_byte = frame_data[byte_index];
|
||||
// Move to next byte
|
||||
byte_index++;
|
||||
}
|
||||
// Get current bit
|
||||
current_bit = (current_byte >> (7 - bit_index)) & 0x01;
|
||||
}
|
||||
|
||||
// Manchester encoding
|
||||
if (current_bit == 1) {
|
||||
// 1 = falling signal
|
||||
if (manchester_half == false) {
|
||||
re = i_pos;
|
||||
im = q_pos;
|
||||
} else {
|
||||
re = i_neg;
|
||||
im = q_neg;
|
||||
// 1G BPSK Manchester beacon signal
|
||||
if (bpsk_pre_count < config_pre_count) {
|
||||
// Pre-count state: send carrier only during pre-count
|
||||
bpsk_pre_count++;
|
||||
re = i_carrier;
|
||||
im = q_carrier;
|
||||
} else if (bpsk_post_count > 0) {
|
||||
// Post-count: send carrier only during post-count
|
||||
bpsk_post_count++;
|
||||
re = i_carrier;
|
||||
im = q_carrier;
|
||||
if (bpsk_post_count >= config_post_count) {
|
||||
// End transmission here
|
||||
byte_index = 0;
|
||||
bpsk_post_count = 0;
|
||||
bpsk_pre_count = 0;
|
||||
end_of_transmission = true;
|
||||
}
|
||||
} else {
|
||||
// 0 = rising signal
|
||||
if (manchester_half == false) {
|
||||
re = i_neg;
|
||||
im = q_neg;
|
||||
} else {
|
||||
re = i_pos;
|
||||
im = q_pos;
|
||||
if (sample_counter == 0 && manchester_half == false) {
|
||||
if (bit_index == 0) {
|
||||
// Read current byte
|
||||
current_byte = frame_data[byte_index];
|
||||
// Move to next byte
|
||||
byte_index++;
|
||||
}
|
||||
// Get current bit
|
||||
current_bit = (current_byte >> (7 - bit_index)) & 0x01;
|
||||
}
|
||||
}
|
||||
// Move to next sample
|
||||
sample_counter++;
|
||||
|
||||
if (sample_counter >= samples_per_halfbit) {
|
||||
// Move to next half-bit
|
||||
sample_counter = 0;
|
||||
manchester_half = !manchester_half;
|
||||
// Manchester encoding
|
||||
if (current_bit == 1) {
|
||||
// 1 = falling signal
|
||||
if (manchester_half == false) {
|
||||
re = i_pos;
|
||||
im = q_pos;
|
||||
} else {
|
||||
re = i_neg;
|
||||
im = q_neg;
|
||||
}
|
||||
} else {
|
||||
// 0 = rising signal
|
||||
if (manchester_half == false) {
|
||||
re = i_neg;
|
||||
im = q_neg;
|
||||
} else {
|
||||
re = i_pos;
|
||||
im = q_pos;
|
||||
}
|
||||
}
|
||||
// Move to next sample
|
||||
sample_counter++;
|
||||
|
||||
// Next bit after two half bits
|
||||
if (manchester_half == false) {
|
||||
// Move to next bit
|
||||
bit_index++;
|
||||
if (bit_index >= 8) {
|
||||
// End of byte
|
||||
bit_index = 0;
|
||||
if (byte_index >= frame_data_len) {
|
||||
// End of frame => move to post-count
|
||||
bpsk_post_count = 1;
|
||||
if (sample_counter >= samples_per_halfbit) {
|
||||
// Move to next half-bit
|
||||
sample_counter = 0;
|
||||
manchester_half = !manchester_half;
|
||||
|
||||
// Next bit after two half bits
|
||||
if (manchester_half == false) {
|
||||
// Move to next bit
|
||||
bit_index++;
|
||||
if (bit_index >= 8) {
|
||||
// End of byte
|
||||
bit_index = 0;
|
||||
if (byte_index >= frame_data_len) {
|
||||
// End of frame => move to post-count
|
||||
bpsk_post_count = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,26 +235,51 @@ void EPIRBTXProcessor::on_message(const Message* const msg) {
|
||||
case Message::ID::EPIRBTXData: {
|
||||
const auto message = *reinterpret_cast<const EPIRBTXDataMessage*>(msg);
|
||||
// Check transmission mode
|
||||
mode_bpsk = message.mode_bpsk;
|
||||
if (mode_bpsk) {
|
||||
// BPSK mode for 406 frame
|
||||
mode_406 = message.mode_406;
|
||||
if (mode_406) {
|
||||
// 406 MHz frame mode:
|
||||
// - FGB if data_len <= 144 bits (i.e. 18 bytes)
|
||||
// - SGB if data_len > 144 bits (actually 250 bits (i.e. 32 bytes))
|
||||
config_pre_count = message.pre_count;
|
||||
config_post_count = message.post_count;
|
||||
|
||||
mode_sgb = message.data_len > frame_data_fgb_max_len; // SGB if data_len > 18 bytes (144 bits)
|
||||
frame_data_len = message.data_len;
|
||||
|
||||
if (mode_sgb) {
|
||||
frame_sgb_bits_len = std::min<uint16_t>(((uint16_t)message.data_len) * 8, sgb_message_bits); // Total bits in the frame
|
||||
frame_data_len = std::min<uint8_t>(frame_data_len, frame_data_sgb_len);
|
||||
// Detect self-test mode: bit 5 (1 based index as per specification) of message.data[0]
|
||||
mode_sgb_selftest = (message.data[0] >> 3) & 0x01;
|
||||
} else {
|
||||
frame_data_len = std::min<uint8_t>(frame_data_len, frame_data_fgb_max_len);
|
||||
}
|
||||
|
||||
// Get the frame data from the message
|
||||
memcpy(frame_data, message.data, std::min(frame_data_len, EPIRBTXDataMessage::max_len));
|
||||
// Init BPSK sequencer
|
||||
sample_counter = 0;
|
||||
bpsk_pre_count = 0;
|
||||
bpsk_post_count = 0;
|
||||
bit_index = 0;
|
||||
byte_index = 0;
|
||||
current_byte = 0;
|
||||
current_bit = 0;
|
||||
memcpy(frame_data, message.data, frame_data_len);
|
||||
|
||||
if (mode_sgb) {
|
||||
// Init SGB DSSS-OQPSK sequencer
|
||||
sgb_sample_counter = 0;
|
||||
const uint32_t init_i = mode_sgb_selftest ? sgb_init_selftest_i : sgb_init_normal_i;
|
||||
const uint32_t init_q = mode_sgb_selftest ? sgb_init_selftest_q : sgb_init_normal_q;
|
||||
init_sgb_channel(sgb_i, init_i);
|
||||
init_sgb_channel(sgb_q, init_q);
|
||||
} else {
|
||||
// Init FGB BPSK sequencer
|
||||
sample_counter = 0;
|
||||
bpsk_pre_count = 0;
|
||||
bpsk_post_count = 0;
|
||||
bit_index = 0;
|
||||
byte_index = 0;
|
||||
current_byte = 0;
|
||||
current_bit = 0;
|
||||
}
|
||||
} else {
|
||||
// AM mode for 121.5 signal => init AM sequencer
|
||||
sweep_phase = 0;
|
||||
audio_phase = 0;
|
||||
mode_sgb = false;
|
||||
}
|
||||
// Tell the processor to start
|
||||
configured = true;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "baseband_thread.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "tonesets.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
/**
|
||||
@@ -42,8 +43,12 @@ class EPIRBTXProcessor : public BasebandProcessor {
|
||||
private:
|
||||
// True when the processor has received a configuration message from the app
|
||||
bool configured{false};
|
||||
// True when in BPSK transmission mode, false for AM transmission mode
|
||||
bool mode_bpsk{false};
|
||||
// True when in 406 MHz transmission mode, false for AM transmission mode
|
||||
bool mode_406{false};
|
||||
// True when current 406 frame is a 2nd generation DSSS-OQPSK frame
|
||||
bool mode_sgb{false};
|
||||
// True when in self-test mode (detected from bit 5 of message.data)
|
||||
bool mode_sgb_selftest{true};
|
||||
|
||||
// True when the transmission has to be stopped (e.g. at the end of a frame)
|
||||
bool end_of_transmission{};
|
||||
@@ -55,10 +60,15 @@ class EPIRBTXProcessor : public BasebandProcessor {
|
||||
// Configured post-count value: used to continue the carrier for config_post_count samples after the end of a frame
|
||||
uint32_t config_post_count = 0;
|
||||
|
||||
// Data of the frame to send in BPSK mode
|
||||
uint8_t frame_data[18]{0};
|
||||
// Size of the frame to send in BPSK mode
|
||||
// Data of the frame to send in 406 mode (FGB BPSK or SGB DSSS-OQPSK)
|
||||
static constexpr uint8_t frame_data_max_len = 32;
|
||||
static constexpr uint8_t frame_data_fgb_max_len = 18;
|
||||
static constexpr uint8_t frame_data_sgb_len = 32; // ceil(250 / 8)
|
||||
uint8_t frame_data[frame_data_max_len]{0};
|
||||
// Size of the frame to send in bytes
|
||||
uint8_t frame_data_len = 0;
|
||||
// Size of the frame payload in bits
|
||||
uint16_t frame_sgb_bits_len = 0;
|
||||
|
||||
// BPSK parameters: Target phase +/-1.1 RAD as per COSPAS/SARSAT specifications
|
||||
static constexpr float phase_rad = 1.1f;
|
||||
@@ -89,6 +99,40 @@ class EPIRBTXProcessor : public BasebandProcessor {
|
||||
// Position in the current manchester bit
|
||||
bool manchester_half = false; // false = first half
|
||||
|
||||
// 2G SGB DSSS-OQPSK parameters
|
||||
static constexpr uint16_t sgb_message_bits = 256; // 250 bits of data + 6 bits padding
|
||||
static constexpr uint32_t sgb_chip_rate = 38400;
|
||||
static constexpr uint32_t sgb_chips_per_bit = 256;
|
||||
static constexpr uint32_t sgb_preamble_bits_per_channel = 25;
|
||||
static constexpr uint32_t sgb_bits_per_channel = 150;
|
||||
static constexpr uint32_t sgb_segment_chips = 38400;
|
||||
static constexpr uint32_t sgb_samples_per_chip = TONES_SAMPLERATE / sgb_chip_rate;
|
||||
static constexpr uint32_t sgb_half_chip_samples = sgb_samples_per_chip / 2;
|
||||
// SGB burst duration is exactly 1 second.
|
||||
static constexpr uint32_t sgb_total_samples = sgb_segment_chips * sgb_samples_per_chip;
|
||||
static constexpr int8_t sgb_chip_amplitude = 90;
|
||||
|
||||
static constexpr uint32_t sgb_init_normal_i = 0x1;
|
||||
static constexpr uint32_t sgb_init_normal_q = 0x1AC1FC;
|
||||
static constexpr uint32_t sgb_init_selftest_i = 0x52C9F0;
|
||||
static constexpr uint32_t sgb_init_selftest_q = 0x3CE928;
|
||||
|
||||
struct SGBChannelState {
|
||||
uint32_t prn_state = 1;
|
||||
uint32_t chip_index = 0;
|
||||
uint32_t sample_in_chip = 0;
|
||||
int8_t chip_level = 0;
|
||||
};
|
||||
|
||||
SGBChannelState sgb_i{};
|
||||
SGBChannelState sgb_q{};
|
||||
uint32_t sgb_sample_counter = 0;
|
||||
|
||||
uint8_t get_frame_bit(uint16_t bit_pos) const;
|
||||
int8_t compute_sgb_chip_level(SGBChannelState& channel, bool q_channel);
|
||||
int8_t sample_sgb_channel(SGBChannelState& channel, bool q_channel);
|
||||
void init_sgb_channel(SGBChannelState& channel, uint32_t initial_state);
|
||||
|
||||
// 127.5 AM signal parameters
|
||||
static const uint32_t sweep_rate = 3; // 3 Hz
|
||||
static const uint32_t f_min = 300; // Sweep min frequency (Hz)
|
||||
|
||||
@@ -51,10 +51,26 @@ void NarrowbandFMAudio::execute(const buffer_c8_t& 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 audio_decim_0_out = audio_decim_0.execute(decim_0_out, dst_buffer);
|
||||
|
||||
channel_spectrum.feed(decim_1_out, channel_filter_low_f, channel_filter_high_f, channel_filter_transition);
|
||||
spectrum_samples += decim_0_out.count;
|
||||
if (!spectrum_capture_active &&
|
||||
spectrum_samples >= spectrum_interval_samples) {
|
||||
spectrum_samples -= spectrum_interval_samples;
|
||||
channel_spectrum.start_capture(2);
|
||||
spectrum_capture_active = true;
|
||||
}
|
||||
|
||||
if (spectrum_capture_active &&
|
||||
channel_spectrum.feed(
|
||||
audio_decim_0_out,
|
||||
channel_filter_low_f,
|
||||
channel_filter_high_f,
|
||||
channel_filter_transition)) {
|
||||
spectrum_capture_active = false;
|
||||
}
|
||||
|
||||
const auto decim_1_out = translating_decim_1.execute(audio_decim_0_out, dst_buffer);
|
||||
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
|
||||
|
||||
feed_channel_stats(channel_out);
|
||||
@@ -141,6 +157,10 @@ void NarrowbandFMAudio::on_message(const Message* const message) {
|
||||
pitch_rssi_config(*reinterpret_cast<const PitchRSSIConfigureMessage*>(message));
|
||||
break;
|
||||
|
||||
case Message::ID::AudioDDCConfig:
|
||||
ddc_config(*reinterpret_cast<const AudioDDCConfigMessage*>(message));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -150,22 +170,26 @@ void NarrowbandFMAudio::configure(const NBFMConfigureMessage& message) {
|
||||
constexpr size_t decim_0_input_fs = baseband_fs;
|
||||
constexpr size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor;
|
||||
|
||||
constexpr size_t decim_1_input_fs = decim_0_output_fs;
|
||||
constexpr size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
|
||||
|
||||
constexpr size_t audio_decim_0_output_fs = decim_0_output_fs / 2;
|
||||
constexpr size_t decim_1_output_fs =
|
||||
audio_decim_0_output_fs / translating_decim_1.decimation_factor;
|
||||
constexpr size_t channel_filter_input_fs = decim_1_output_fs;
|
||||
const size_t channel_filter_output_fs = channel_filter_input_fs / message.channel_decimation;
|
||||
|
||||
const size_t demod_input_fs = channel_filter_output_fs;
|
||||
|
||||
decim_0.configure(message.decim_0_filter.taps);
|
||||
decim_1.configure(message.decim_1_filter.taps);
|
||||
decim_0.configure(message.decim_0_filter.taps, 33554432);
|
||||
audio_decim_0.configure(taps_audio_wide_halfband_0.taps);
|
||||
translating_decim_1.configure(
|
||||
message.decim_1_filter.taps, audio_decim_0_output_fs);
|
||||
channel_filter.configure(message.channel_filter.taps, message.channel_decimation);
|
||||
demod.configure(demod_input_fs, message.deviation);
|
||||
channel_filter_low_f = message.channel_filter.low_frequency_normalized * channel_filter_input_fs;
|
||||
channel_filter_high_f = message.channel_filter.high_frequency_normalized * channel_filter_input_fs;
|
||||
channel_filter_transition = message.channel_filter.transition_normalized * channel_filter_input_fs;
|
||||
channel_spectrum.set_decimation_factor(1.0f);
|
||||
channel_spectrum.set_decimation_factor(1);
|
||||
spectrum_interval_samples =
|
||||
decim_0_output_fs / spectrum_rate_hz;
|
||||
audio_output.configure(message.audio_hpf_config, message.audio_deemph_config, (float)message.squelch_level / 100.0);
|
||||
|
||||
hpf.configure(audio_24k_hpf_30hz_config);
|
||||
@@ -174,6 +198,11 @@ void NarrowbandFMAudio::configure(const NBFMConfigureMessage& message) {
|
||||
configured = true;
|
||||
}
|
||||
|
||||
void NarrowbandFMAudio::ddc_config(const AudioDDCConfigMessage& message) {
|
||||
translating_decim_1.set_frequency(message.frequency);
|
||||
channel_spectrum.set_channel_filter_offset(message.frequency);
|
||||
}
|
||||
|
||||
void NarrowbandFMAudio::pitch_rssi_config(const PitchRSSIConfigureMessage& message) {
|
||||
pitch_rssi_enabled = message.enabled;
|
||||
tone_delta = (message.rssi + 1000) * ((1ULL << 32) / 24000);
|
||||
|
||||
@@ -29,10 +29,11 @@
|
||||
|
||||
#include "dsp_decimate.hpp"
|
||||
#include "dsp_demodulate.hpp"
|
||||
#include "dsp_frequency_xlator.hpp"
|
||||
#include "dsp_iir.hpp"
|
||||
|
||||
#include "audio_output.hpp"
|
||||
#include "spectrum_collector.hpp"
|
||||
#include "filtered_spectrum_collector.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -50,6 +51,7 @@ class NarrowbandFMAudio : public BasebandProcessor {
|
||||
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 3072000;
|
||||
static constexpr auto spectrum_rate_hz = 30.0f;
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
@@ -69,8 +71,9 @@ class NarrowbandFMAudio : public BasebandProcessor {
|
||||
(int16_t*)tone.data(),
|
||||
sizeof(tone) / sizeof(int16_t)};
|
||||
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x16Decim2 audio_decim_0{};
|
||||
dsp::FrequencyTranslatingDecimator32By8 translating_decim_1{};
|
||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||
int32_t channel_filter_low_f = 0;
|
||||
int32_t channel_filter_high_f = 0;
|
||||
@@ -84,7 +87,10 @@ class NarrowbandFMAudio : public BasebandProcessor {
|
||||
|
||||
AudioOutput audio_output{};
|
||||
|
||||
SpectrumCollector channel_spectrum{};
|
||||
FilteredSpectrumCollector channel_spectrum{};
|
||||
size_t spectrum_interval_samples{0};
|
||||
size_t spectrum_samples{0};
|
||||
bool spectrum_capture_active{false};
|
||||
|
||||
uint32_t tone_phase{0};
|
||||
uint32_t tone_delta{0};
|
||||
@@ -113,6 +119,7 @@ class NarrowbandFMAudio : public BasebandProcessor {
|
||||
void pitch_rssi_config(const PitchRSSIConfigureMessage& message);
|
||||
void configure(const NBFMConfigureMessage& message);
|
||||
void capture_config(const CaptureConfigMessage& message);
|
||||
void ddc_config(const AudioDDCConfigMessage& message);
|
||||
};
|
||||
|
||||
#endif /*__PROC_NFM_AUDIO_H__*/
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
#include "proc_tetra.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "sine_table_int8.hpp"
|
||||
#include "event_m4.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
TetraProcessor::TetraProcessor() {
|
||||
decim_0.configure(taps_25k0_tetra_decim_0.taps);
|
||||
decim_1.configure(taps_25k0_tetra_decim_1.taps);
|
||||
|
||||
baseband_thread.start();
|
||||
configured = true;
|
||||
}
|
||||
|
||||
void TetraProcessor::execute(const buffer_c8_t& buffer) {
|
||||
if (!configured) return;
|
||||
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer_0);
|
||||
const auto channel_out = decim_1.execute(decim_0_out, dst_buffer_1);
|
||||
|
||||
feed_channel_stats(channel_out);
|
||||
|
||||
for (size_t i = 0; i < channel_out.count; i++) {
|
||||
complex16_t sample = channel_out.p[i];
|
||||
|
||||
// Carrier PLL Rotation
|
||||
uint8_t phase_idx = (pll_phase >> 24) & 0xFF;
|
||||
uint8_t cos_idx = (phase_idx + 64) & 0xFF;
|
||||
int16_t rotated_real = (sample.real() * sine_table_i8[cos_idx] - sample.imag() * sine_table_i8[phase_idx]) >> 7;
|
||||
int16_t rotated_imag = (sample.real() * sine_table_i8[phase_idx] + sample.imag() * sine_table_i8[cos_idx]) >> 7;
|
||||
complex16_t rotated_sample = {rotated_real, rotated_imag};
|
||||
|
||||
uint32_t old_phase = symbol_phase;
|
||||
symbol_phase += symbol_phase_inc;
|
||||
|
||||
// Half-symbol timing
|
||||
if ((old_phase < 0x80000000) && (symbol_phase >= 0x80000000)) {
|
||||
mid_sample = rotated_sample;
|
||||
}
|
||||
|
||||
// Full-symbol timing
|
||||
if (symbol_phase < old_phase) {
|
||||
prev_prompt = prompt_sample;
|
||||
prompt_sample = rotated_sample;
|
||||
|
||||
// Gardner Timing Error Detector
|
||||
int32_t err_i = (mid_sample.real() * (prompt_sample.real() - prev_prompt.real()));
|
||||
int32_t err_q = (mid_sample.imag() * (prompt_sample.imag() - prev_prompt.imag()));
|
||||
int32_t timing_error = err_i + err_q;
|
||||
|
||||
const int64_t delta = (timing_error >> 16);
|
||||
int64_t inc = static_cast<int64_t>(symbol_phase_inc) + delta;
|
||||
// CLAMPING: Prevent the NCO from drifting into the noise!
|
||||
if (inc < 1600000000) inc = 1600000000;
|
||||
if (inc > 1620000000) inc = 1620000000;
|
||||
symbol_phase_inc = static_cast<uint32_t>(inc);
|
||||
|
||||
process_symbol(prompt_sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TetraProcessor::process_symbol(const complex16_t& current_sample) {
|
||||
// Differential phase demodulation
|
||||
int32_t dot_i = (current_sample.real() * delay_sample.real()) + (current_sample.imag() * delay_sample.imag());
|
||||
int32_t dot_q = (current_sample.imag() * delay_sample.real()) - (current_sample.real() * delay_sample.imag());
|
||||
delay_sample = current_sample;
|
||||
|
||||
// Phase error for Carrier PLL
|
||||
int32_t phase_err = 0;
|
||||
if (dot_i > 0 && dot_q > 0)
|
||||
phase_err = dot_q - dot_i;
|
||||
else if (dot_i < 0 && dot_q > 0)
|
||||
phase_err = dot_q + dot_i;
|
||||
else if (dot_i < 0 && dot_q < 0)
|
||||
phase_err = -dot_q + dot_i;
|
||||
else
|
||||
phase_err = -dot_q - dot_i;
|
||||
|
||||
pll_freq += (phase_err * pll_beta) >> 8;
|
||||
|
||||
// CLAMPING: Prevent frequency tracking from flying away
|
||||
if (pll_freq > 400000000) pll_freq = 400000000;
|
||||
if (pll_freq < -400000000) pll_freq = -400000000;
|
||||
|
||||
pll_phase += ((phase_err * pll_alpha) >> 8) + pll_freq;
|
||||
|
||||
// ETSI EN 300 392-2 pi/4 DQPSK bit mapping
|
||||
uint8_t dibit = 0;
|
||||
if (dot_i > 0 && dot_q > 0)
|
||||
dibit = 0b00;
|
||||
else if (dot_i < 0 && dot_q > 0)
|
||||
dibit = 0b01;
|
||||
else if (dot_i < 0 && dot_q < 0)
|
||||
dibit = 0b11;
|
||||
else
|
||||
dibit = 0b10;
|
||||
|
||||
// Insert bits into history buffer and sync register
|
||||
for (int b = 1; b >= 0; b--) {
|
||||
uint8_t bit_val = (dibit >> b) & 0x01;
|
||||
|
||||
size_t byte_idx = (history_write_idx / 8) % 128;
|
||||
if ((history_write_idx % 8) == 0) bit_history_buffer[byte_idx] = 0;
|
||||
|
||||
bit_history_buffer[byte_idx] |= (bit_val << (7 - (history_write_idx % 8)));
|
||||
history_write_idx = (history_write_idx + 1) % 1024;
|
||||
bit_count++;
|
||||
|
||||
sync_register = ((sync_register << 1) | bit_val);
|
||||
|
||||
uint64_t sync =
|
||||
sync_register & 0x3FFFFFFFFFULL;
|
||||
|
||||
uint32_t err_pos =
|
||||
__builtin_popcountll(sync ^ Y_SYNC);
|
||||
|
||||
uint32_t err_neg =
|
||||
__builtin_popcountll(
|
||||
sync ^
|
||||
(~Y_SYNC & 0x3FFFFFFFFFULL));
|
||||
|
||||
if (!pending_dsb.valid && bit_count >= Y_SYNC_BITS && (err_pos <= 4 || err_neg <= 4)) {
|
||||
const uint64_t sync_start = bit_count - Y_SYNC_BITS;
|
||||
|
||||
if (sync_start >= SYNC_OFFSET) {
|
||||
pending_dsb.valid = true;
|
||||
pending_dsb.burst_start = sync_start - SYNC_OFFSET;
|
||||
pending_dsb.ready_at = pending_dsb.burst_start + BURST_BITS;
|
||||
pending_dsb.inverted = err_neg < err_pos;
|
||||
pending_dsb.errors = std::min(err_pos, err_neg);
|
||||
}
|
||||
}
|
||||
|
||||
if (ENABLE_DNB_MESSAGES) {
|
||||
const uint32_t train_mask = (1UL << DNB_TRAIN_BITS) - 1;
|
||||
const uint32_t train = sync_register & train_mask;
|
||||
const uint32_t n_err_pos = __builtin_popcount(train ^ N_SYNC);
|
||||
const uint32_t n_err_neg = __builtin_popcount(train ^ (~N_SYNC & train_mask));
|
||||
const uint32_t p_err_pos = __builtin_popcount(train ^ P_SYNC);
|
||||
const uint32_t p_err_neg = __builtin_popcount(train ^ (~P_SYNC & train_mask));
|
||||
|
||||
if (!pending_dnb.valid && bit_count >= DNB_TRAIN_BITS && (n_err_pos <= 1 || n_err_neg <= 1 || p_err_pos <= 1 || p_err_neg <= 1)) {
|
||||
const bool p_train = std::min(p_err_pos, p_err_neg) < std::min(n_err_pos, n_err_neg);
|
||||
const uint32_t pos_err = p_train ? p_err_pos : n_err_pos;
|
||||
const uint32_t neg_err = p_train ? p_err_neg : n_err_neg;
|
||||
|
||||
const uint64_t train_start = bit_count - DNB_TRAIN_BITS;
|
||||
const uint64_t block1_start = train_start - 230;
|
||||
const uint64_t block2_start = train_start + 38;
|
||||
|
||||
if (train_start >= 230) {
|
||||
pending_dnb.valid = true;
|
||||
pending_dnb.block1_start = block1_start;
|
||||
pending_dnb.block2_start = block2_start;
|
||||
pending_dnb.ready_at = block2_start + DNB_BLK_BITS;
|
||||
pending_dnb.inverted = neg_err < pos_err;
|
||||
pending_dnb.p_train = p_train;
|
||||
pending_dnb.errors = std::min(pos_err, neg_err);
|
||||
}
|
||||
}
|
||||
|
||||
if (pending_dnb.valid && bit_count >= pending_dnb.ready_at)
|
||||
push_dnb_to_ui();
|
||||
}
|
||||
|
||||
if (pending_dsb.valid && bit_count >= pending_dsb.ready_at)
|
||||
push_burst_to_ui();
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t TetraProcessor::history_bit(uint64_t absolute_bit) const {
|
||||
const size_t p = absolute_bit % 1024;
|
||||
|
||||
return (bit_history_buffer[p >> 3] >>
|
||||
(7 - (p & 7))) &
|
||||
1;
|
||||
}
|
||||
|
||||
void TetraProcessor::push_burst_to_ui() {
|
||||
std::array<uint8_t, 63> burst{};
|
||||
|
||||
for (size_t i = 0; i < BURST_BITS; i++) {
|
||||
uint8_t b = history_bit(pending_dsb.burst_start + i);
|
||||
|
||||
if (pending_dsb.inverted)
|
||||
b ^= 1;
|
||||
|
||||
burst[i >> 3] |=
|
||||
b << (7 - (i & 7));
|
||||
}
|
||||
|
||||
shared_memory.application_queue.push(
|
||||
TetraBurstMessage(
|
||||
burst.data(),
|
||||
pending_dsb.inverted,
|
||||
pending_dsb.errors));
|
||||
|
||||
pending_dsb.valid = false;
|
||||
}
|
||||
|
||||
void TetraProcessor::push_dnb_to_ui() {
|
||||
std::array<uint8_t, 54> burst{};
|
||||
|
||||
for (size_t i = 0; i < DNB_BLK_BITS; i++) {
|
||||
uint8_t b = history_bit(pending_dnb.block1_start + i);
|
||||
if (pending_dnb.inverted)
|
||||
b ^= 1;
|
||||
burst[i >> 3] |= b << (7 - (i & 7));
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < DNB_BLK_BITS; i++) {
|
||||
uint8_t b = history_bit(pending_dnb.block2_start + i);
|
||||
if (pending_dnb.inverted)
|
||||
b ^= 1;
|
||||
const size_t out_bit = DNB_BLK_BITS + i;
|
||||
burst[out_bit >> 3] |= b << (7 - (out_bit & 7));
|
||||
}
|
||||
|
||||
shared_memory.application_queue.push(
|
||||
TetraDnbMessage(
|
||||
burst.data(),
|
||||
pending_dnb.inverted,
|
||||
pending_dnb.errors,
|
||||
pending_dnb.p_train));
|
||||
|
||||
pending_dnb.valid = false;
|
||||
}
|
||||
|
||||
void TetraProcessor::on_message(const Message* const msg) {
|
||||
(void)msg;
|
||||
}
|
||||
|
||||
int main() {
|
||||
EventDispatcher event_dispatcher{std::make_unique<TetraProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#ifndef __PROC_TETRA_H__
|
||||
#define __PROC_TETRA_H__
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "message.hpp"
|
||||
#include "dsp_decimate.hpp"
|
||||
#include "dsp_fir_taps.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
class TetraProcessor : public BasebandProcessor {
|
||||
public:
|
||||
TetraProcessor();
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
void on_message(const Message* const msg);
|
||||
|
||||
private:
|
||||
static constexpr size_t baseband_fs = 3072000;
|
||||
static constexpr size_t channel_fs = 48000;
|
||||
static constexpr uint32_t symbol_rate = 18000;
|
||||
|
||||
std::array<complex16_t, 512> dst_0{};
|
||||
const buffer_c16_t dst_buffer_0{dst_0.data(), dst_0.size()};
|
||||
|
||||
std::array<complex16_t, 512> dst_1{};
|
||||
const buffer_c16_t dst_buffer_1{dst_1.data(), dst_1.size()};
|
||||
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
|
||||
// PLL and Timing
|
||||
uint32_t symbol_phase{0};
|
||||
uint32_t symbol_phase_inc{(uint32_t)((symbol_rate * 4294967296.0) / channel_fs)};
|
||||
|
||||
complex16_t prompt_sample{0, 0};
|
||||
complex16_t prev_prompt{0, 0};
|
||||
complex16_t mid_sample{0, 0};
|
||||
complex16_t delay_sample{0, 0};
|
||||
static constexpr uint32_t BURST_BITS = 500;
|
||||
static constexpr uint32_t Y_SYNC_BITS = 38;
|
||||
static constexpr uint32_t SYNC_OFFSET = 214;
|
||||
static constexpr uint64_t Y_SYNC = 0x30673A7067ULL;
|
||||
static constexpr uint32_t DNB_TRAIN_BITS = 22;
|
||||
static constexpr uint32_t DNB_BLK_BITS = 216;
|
||||
static constexpr uint32_t DNB_TRAIN_TO_BLK2 = 22;
|
||||
static constexpr uint32_t DNB_TOTAL_T5_BITS = DNB_BLK_BITS * 2;
|
||||
static constexpr uint32_t N_SYNC = 0x343A74UL;
|
||||
static constexpr uint32_t P_SYNC = 0x1E90DEUL;
|
||||
static constexpr bool ENABLE_DNB_MESSAGES = true;
|
||||
|
||||
uint32_t pll_phase{0};
|
||||
int32_t pll_freq{0};
|
||||
static constexpr int32_t pll_alpha = 150;
|
||||
static constexpr int32_t pll_beta = 2;
|
||||
|
||||
uint64_t sync_register{0};
|
||||
std::array<uint8_t, 128> bit_history_buffer{};
|
||||
size_t history_write_idx{0};
|
||||
uint64_t bit_count{0};
|
||||
bool configured{false};
|
||||
|
||||
struct PendingDnb {
|
||||
bool valid{false};
|
||||
uint64_t block1_start{0};
|
||||
uint64_t block2_start{0};
|
||||
uint64_t ready_at{0};
|
||||
bool inverted{false};
|
||||
bool p_train{false};
|
||||
uint8_t errors{0};
|
||||
} pending_dnb{};
|
||||
|
||||
struct PendingDsb {
|
||||
bool valid{false};
|
||||
uint64_t burst_start{0};
|
||||
uint64_t ready_at{0};
|
||||
bool inverted{false};
|
||||
uint8_t errors{0};
|
||||
} pending_dsb{};
|
||||
|
||||
void process_symbol(const complex16_t& sample);
|
||||
uint8_t history_bit(uint64_t absolute_bit) const;
|
||||
void push_burst_to_ui();
|
||||
void push_dnb_to_ui();
|
||||
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive, false};
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
#endif // __PROC_TETRA_H__
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* Copyright 2026 Great Scott Gadgets <info@greatscottgadgets.com>
|
||||
*
|
||||
* This file is part of HackRF.
|
||||
*
|
||||
* 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 "cpu_clock.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <libopencm3/lpc43xx/cgu.h>
|
||||
#if defined(IS_JAWBREAKER) || defined(IS_HACKRF_ONE) || defined(IS_PRALINE)
|
||||
#include <libopencm3/lpc43xx/ccu.h>
|
||||
#endif
|
||||
|
||||
#include "delay.h"
|
||||
#include "i2c_bus.h"
|
||||
#include "i2c_lpc.h"
|
||||
#include "si5351c.h"
|
||||
|
||||
/* We start with the CPU clock at 96MHz */
|
||||
unsigned int cpu_clock_mhz = 96;
|
||||
|
||||
/*
|
||||
Configure PLL1 (Main MCU Clock) to max speed (204MHz).
|
||||
Note: PLL1 clock is used by M4/M0 core, Peripheral, APB1.
|
||||
This function shall be called after cpu_clock_init().
|
||||
*/
|
||||
static void cpu_clock_pll1_max_speed(void) {
|
||||
uint32_t reg_val;
|
||||
|
||||
/* This function implements the sequence recommended in:
|
||||
* UM10503 Rev 2.4 (Aug 2018), section 13.2.1.1, page 167. */
|
||||
|
||||
/* 1. Select the IRC as BASE_M4_CLK source. */
|
||||
reg_val = CGU_BASE_M4_CLK;
|
||||
reg_val &= ~CGU_BASE_M4_CLK_CLK_SEL_MASK;
|
||||
reg_val |= CGU_BASE_M4_CLK_CLK_SEL(CGU_SRC_IRC) | CGU_BASE_M4_CLK_AUTOBLOCK(1);
|
||||
CGU_BASE_M4_CLK = reg_val;
|
||||
|
||||
/* CPU is now at 12MHz */
|
||||
cpu_clock_mhz = 12;
|
||||
|
||||
/* 2. Enable the crystal oscillator. */
|
||||
CGU_XTAL_OSC_CTRL &= ~CGU_XTAL_OSC_CTRL_ENABLE_MASK;
|
||||
|
||||
/* 3. Wait 250us. */
|
||||
delay_us(250);
|
||||
|
||||
/* 4. Set the AUTOBLOCK bit. */
|
||||
CGU_PLL1_CTRL |= CGU_PLL1_CTRL_AUTOBLOCK(1);
|
||||
|
||||
/* 5. Reconfigure PLL1 to produce the final output frequency, with the
|
||||
* crystal oscillator as clock source. */
|
||||
reg_val = CGU_PLL1_CTRL;
|
||||
// clang-format off
|
||||
reg_val &= ~( CGU_PLL1_CTRL_CLK_SEL_MASK |
|
||||
CGU_PLL1_CTRL_PD_MASK |
|
||||
CGU_PLL1_CTRL_FBSEL_MASK |
|
||||
CGU_PLL1_CTRL_BYPASS_MASK |
|
||||
CGU_PLL1_CTRL_DIRECT_MASK |
|
||||
CGU_PLL1_CTRL_PSEL_MASK |
|
||||
CGU_PLL1_CTRL_MSEL_MASK |
|
||||
CGU_PLL1_CTRL_NSEL_MASK );
|
||||
/* Set PLL1 up to 12MHz * 17 = 204MHz.
|
||||
* Direct mode: FCLKOUT = FCCO = M*(FCLKIN/N) */
|
||||
reg_val |= CGU_PLL1_CTRL_CLK_SEL(CGU_SRC_XTAL) |
|
||||
CGU_PLL1_CTRL_PSEL(0) |
|
||||
CGU_PLL1_CTRL_NSEL(0) |
|
||||
CGU_PLL1_CTRL_MSEL(16) |
|
||||
CGU_PLL1_CTRL_FBSEL(0) |
|
||||
CGU_PLL1_CTRL_DIRECT(1);
|
||||
// clang-format on
|
||||
CGU_PLL1_CTRL = reg_val;
|
||||
|
||||
/* 6. Wait for PLL1 to lock. */
|
||||
while (!(CGU_PLL1_STAT & CGU_PLL1_STAT_LOCK_MASK)) {
|
||||
}
|
||||
|
||||
/* 7. Set the PLL1 P-divider to divide by 2 (DIRECT=0, PSEL=0). */
|
||||
CGU_PLL1_CTRL &= ~CGU_PLL1_CTRL_DIRECT_MASK;
|
||||
|
||||
/* 8. Select PLL1 as BASE_M4_CLK source. */
|
||||
reg_val = CGU_BASE_M4_CLK;
|
||||
reg_val &= ~CGU_BASE_M4_CLK_CLK_SEL_MASK;
|
||||
reg_val |= CGU_BASE_M4_CLK_CLK_SEL(CGU_SRC_PLL1);
|
||||
CGU_BASE_M4_CLK = reg_val;
|
||||
|
||||
/* CPU is now at 102MHz */
|
||||
cpu_clock_mhz = 102;
|
||||
|
||||
/* 9. Wait 50us. */
|
||||
delay_us(50);
|
||||
|
||||
/* 10. Set the PLL1 P-divider to direct output mode (DIRECT=1). */
|
||||
CGU_PLL1_CTRL |= CGU_PLL1_CTRL_DIRECT_MASK;
|
||||
|
||||
/* CPU is now at 204MHz */
|
||||
cpu_clock_mhz = 204;
|
||||
}
|
||||
|
||||
/* clock startup for LPC4320 configure PLL1 to max speed (204MHz).
|
||||
Note: PLL1 clock is used by M4/M0 core, Peripheral, APB1. */
|
||||
void cpu_clock_init(void) {
|
||||
/* use IRC as clock source for APB1 (including I2C0) */
|
||||
CGU_BASE_APB1_CLK = CGU_BASE_APB1_CLK_CLK_SEL(CGU_SRC_IRC);
|
||||
|
||||
/* use IRC as clock source for APB3 */
|
||||
CGU_BASE_APB3_CLK = CGU_BASE_APB3_CLK_CLK_SEL(CGU_SRC_IRC);
|
||||
|
||||
// FIXME disable I2C
|
||||
/* Kick I2C0 down to 400kHz when we switch over to APB1 clock = 204MHz */
|
||||
i2c_bus_start(si5351c.bus, &i2c_config_fast_clock);
|
||||
|
||||
/*
|
||||
* 12MHz clock is entering LPC XTAL1/OSC input now.
|
||||
* On HackRF One and Jawbreaker, there is a 12 MHz crystal at the LPC.
|
||||
* Set up PLL1 to run from XTAL1 input.
|
||||
*/
|
||||
|
||||
// FIXME a lot of the details here should be in a CGU driver
|
||||
|
||||
/* set xtal oscillator to low frequency mode */
|
||||
CGU_XTAL_OSC_CTRL &= ~CGU_XTAL_OSC_CTRL_HF_MASK;
|
||||
|
||||
cpu_clock_pll1_max_speed();
|
||||
|
||||
/* use XTAL_OSC as clock source for APB1 */
|
||||
CGU_BASE_APB1_CLK =
|
||||
CGU_BASE_APB1_CLK_AUTOBLOCK(1) | CGU_BASE_APB1_CLK_CLK_SEL(CGU_SRC_XTAL);
|
||||
|
||||
/* use XTAL_OSC as clock source for APB3 */
|
||||
CGU_BASE_APB3_CLK =
|
||||
CGU_BASE_APB3_CLK_AUTOBLOCK(1) | CGU_BASE_APB3_CLK_CLK_SEL(CGU_SRC_XTAL);
|
||||
|
||||
/* use XTAL_OSC as clock source for PLL0USB */
|
||||
CGU_PLL0USB_CTRL = CGU_PLL0USB_CTRL_PD(1) | CGU_PLL0USB_CTRL_AUTOBLOCK(1) |
|
||||
CGU_PLL0USB_CTRL_CLK_SEL(CGU_SRC_XTAL);
|
||||
while (CGU_PLL0USB_STAT & CGU_PLL0USB_STAT_LOCK_MASK) {
|
||||
}
|
||||
|
||||
/* configure PLL0USB to produce 480 MHz clock from 12 MHz XTAL_OSC */
|
||||
/* Values from User Manual v1.4 Table 94, for 12MHz oscillator. */
|
||||
CGU_PLL0USB_MDIV = 0x06167FFA;
|
||||
CGU_PLL0USB_NP_DIV = 0x00302062;
|
||||
CGU_PLL0USB_CTRL |=
|
||||
(CGU_PLL0USB_CTRL_PD(1) | CGU_PLL0USB_CTRL_DIRECTI(1) |
|
||||
CGU_PLL0USB_CTRL_DIRECTO(1) | CGU_PLL0USB_CTRL_CLKEN(1));
|
||||
|
||||
/* power on PLL0USB and wait until stable */
|
||||
CGU_PLL0USB_CTRL &= ~CGU_PLL0USB_CTRL_PD_MASK;
|
||||
while (!(CGU_PLL0USB_STAT & CGU_PLL0USB_STAT_LOCK_MASK)) {
|
||||
}
|
||||
|
||||
/* use PLL0USB as clock source for USB0 */
|
||||
CGU_BASE_USB0_CLK = CGU_BASE_USB0_CLK_AUTOBLOCK(1) |
|
||||
CGU_BASE_USB0_CLK_CLK_SEL(CGU_SRC_PLL0USB);
|
||||
|
||||
/* Switch peripheral clock over to use PLL1 (204MHz) */
|
||||
CGU_BASE_PERIPH_CLK = CGU_BASE_PERIPH_CLK_AUTOBLOCK(1) |
|
||||
CGU_BASE_PERIPH_CLK_CLK_SEL(CGU_SRC_PLL1);
|
||||
|
||||
/* Switch APB1 clock over to use PLL1 (204MHz) */
|
||||
CGU_BASE_APB1_CLK =
|
||||
CGU_BASE_APB1_CLK_AUTOBLOCK(1) | CGU_BASE_APB1_CLK_CLK_SEL(CGU_SRC_PLL1);
|
||||
|
||||
/* Switch APB3 clock over to use PLL1 (204MHz) */
|
||||
CGU_BASE_APB3_CLK =
|
||||
CGU_BASE_APB3_CLK_AUTOBLOCK(1) | CGU_BASE_APB3_CLK_CLK_SEL(CGU_SRC_PLL1);
|
||||
|
||||
CGU_BASE_SSP0_CLK =
|
||||
CGU_BASE_SSP0_CLK_AUTOBLOCK(1) | CGU_BASE_SSP0_CLK_CLK_SEL(CGU_SRC_PLL1);
|
||||
|
||||
CGU_BASE_SSP1_CLK =
|
||||
CGU_BASE_SSP1_CLK_AUTOBLOCK(1) | CGU_BASE_SSP1_CLK_CLK_SEL(CGU_SRC_PLL1);
|
||||
|
||||
#ifdef IS_NOT_RAD1O
|
||||
if (IS_NOT_RAD1O) {
|
||||
/* Disable unused clocks */
|
||||
/* Start with PLLs */
|
||||
CGU_PLL0AUDIO_CTRL = CGU_PLL0AUDIO_CTRL_PD(1);
|
||||
|
||||
/* Dividers */
|
||||
CGU_IDIVA_CTRL = CGU_IDIVA_CTRL_PD(1);
|
||||
CGU_IDIVB_CTRL = CGU_IDIVB_CTRL_PD(1);
|
||||
CGU_IDIVC_CTRL = CGU_IDIVC_CTRL_PD(1);
|
||||
CGU_IDIVD_CTRL = CGU_IDIVD_CTRL_PD(1);
|
||||
CGU_IDIVE_CTRL = CGU_IDIVE_CTRL_PD(1);
|
||||
|
||||
/* Base clocks */
|
||||
CGU_BASE_SPIFI_CLK =
|
||||
CGU_BASE_SPIFI_CLK_PD(1); /* SPIFI is only used at boot */
|
||||
CGU_BASE_USB1_CLK =
|
||||
CGU_BASE_USB1_CLK_PD(1); /* USB1 is not exposed on HackRF */
|
||||
CGU_BASE_PHY_RX_CLK = CGU_BASE_PHY_RX_CLK_PD(1);
|
||||
CGU_BASE_PHY_TX_CLK = CGU_BASE_PHY_TX_CLK_PD(1);
|
||||
CGU_BASE_LCD_CLK = CGU_BASE_LCD_CLK_PD(1);
|
||||
CGU_BASE_VADC_CLK = CGU_BASE_VADC_CLK_PD(1);
|
||||
CGU_BASE_SDIO_CLK = CGU_BASE_SDIO_CLK_PD(0) | CGU_BASE_SDIO_CLK_AUTOBLOCK(1) | CGU_BASE_SDIO_CLK_CLK_SEL(0x09);
|
||||
CGU_BASE_UART0_CLK = CGU_BASE_UART0_CLK_PD(1);
|
||||
CGU_BASE_UART1_CLK = CGU_BASE_UART1_CLK_PD(1);
|
||||
CGU_BASE_UART2_CLK = CGU_BASE_UART2_CLK_PD(1);
|
||||
CGU_BASE_UART3_CLK = CGU_BASE_UART3_CLK_PD(1);
|
||||
CGU_BASE_OUT_CLK = CGU_BASE_OUT_CLK_PD(1);
|
||||
CGU_BASE_AUDIO_CLK = CGU_BASE_AUDIO_CLK_PD(1);
|
||||
CGU_BASE_CGU_OUT0_CLK = CGU_BASE_CGU_OUT0_CLK_PD(1);
|
||||
CGU_BASE_CGU_OUT1_CLK = CGU_BASE_CGU_OUT1_CLK_PD(1);
|
||||
|
||||
/* Disable unused peripheral clocks */
|
||||
CCU1_CLK_APB1_CAN1_CFG = 0;
|
||||
CCU1_CLK_APB1_I2S_CFG = 0;
|
||||
CCU1_CLK_APB1_MOTOCONPWM_CFG = 0;
|
||||
// CCU1_CLK_APB3_ADC0_CFG = 0;
|
||||
CCU1_CLK_APB3_ADC1_CFG = 0;
|
||||
CCU1_CLK_APB3_CAN0_CFG = 0;
|
||||
CCU1_CLK_APB3_DAC_CFG = 0;
|
||||
// CCU1_CLK_M4_DMA_CFG = 0;
|
||||
CCU1_CLK_M4_EMC_CFG = 0;
|
||||
CCU1_CLK_M4_EMCDIV_CFG = 0;
|
||||
CCU1_CLK_M4_ETHERNET_CFG = 0;
|
||||
CCU1_CLK_M4_LCD_CFG = 0;
|
||||
CCU1_CLK_M4_QEI_CFG = 0;
|
||||
CCU1_CLK_M4_RITIMER_CFG = 0;
|
||||
// CCU1_CLK_M4_SCT_CFG = 0;
|
||||
// CCU1_CLK_M4_SDIO_CFG = 1;
|
||||
CCU1_CLK_M4_SPIFI_CFG = 0;
|
||||
CCU1_CLK_M4_TIMER0_CFG = 0;
|
||||
// CCU1_CLK_M4_TIMER1_CFG = 0;
|
||||
// CCU1_CLK_M4_TIMER2_CFG = 0;
|
||||
CCU1_CLK_M4_TIMER3_CFG = 0;
|
||||
CCU1_CLK_M4_UART1_CFG = 0;
|
||||
CCU1_CLK_M4_USART0_CFG = 0;
|
||||
CCU1_CLK_M4_USART2_CFG = 0;
|
||||
CCU1_CLK_M4_USART3_CFG = 0;
|
||||
CCU1_CLK_M4_USB1_CFG = 0;
|
||||
CCU1_CLK_M4_VADC_CFG = 0;
|
||||
// CCU1_CLK_SPIFI_CFG = 0;
|
||||
// CCU1_CLK_USB1_CFG = 0;
|
||||
// CCU1_CLK_VADC_CFG = 0;
|
||||
// CCU2_CLK_APB0_UART1_CFG = 0;
|
||||
// CCU2_CLK_APB0_USART0_CFG = 0;
|
||||
// CCU2_CLK_APB2_USART2_CFG = 0;
|
||||
// CCU2_CLK_APB2_USART3_CFG = 0;
|
||||
// CCU2_CLK_APLL_CFG = 0;
|
||||
// CCU2_CLK_SDIO_CFG = 0;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
#pragma once
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright 2012-2026 Great Scott Gadgets <info@greatscottgadgets.com>
|
||||
* Copyright 2012 Jared Boone <jared@sharebrained.com>
|
||||
* Copyright 2013 Benjamin Vernoux <titanmkd@gmail.com>
|
||||
*
|
||||
* This file is part of HackRF.
|
||||
*
|
||||
* 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 "pins.h"
|
||||
|
||||
#include <libopencm3/lpc43xx/scu.h>
|
||||
|
||||
#include "gpio.h"
|
||||
#include "leds.h"
|
||||
#include "platform_detect.h"
|
||||
#include "platform_gpio.h"
|
||||
#include "platform_scu.h"
|
||||
#include "power.h"
|
||||
#ifdef IS_PRALINE
|
||||
#include "clock_io.h"
|
||||
#endif
|
||||
|
||||
void pins_shutdown(void) {
|
||||
/* Configure all GPIO as Input (safe state) */
|
||||
// gpio_init();
|
||||
|
||||
/* Detect Platform */
|
||||
const platform_gpio_t* gpio = platform_gpio();
|
||||
const platform_scu_t* scu = platform_scu();
|
||||
|
||||
/* TDI and TMS pull-ups are required in all JTAG-compliant devices.
|
||||
*
|
||||
* The HackRF CPLD is always present, so let the CPLD pull up its TDI and TMS.
|
||||
*
|
||||
* The PortaPack may not be present, so pull up the PortaPack TMS pin from the
|
||||
* microcontroller.
|
||||
*
|
||||
* TCK is recommended to be held low, so use microcontroller pull-down.
|
||||
*
|
||||
* TDO is undriven except when in Shift-IR or Shift-DR phases.
|
||||
* Use the microcontroller to pull down to keep from floating.
|
||||
*
|
||||
* LPC43xx pull-up and pull-down resistors are approximately 53K.
|
||||
*/
|
||||
#ifdef IS_EXPANSION_COMPATIBLE
|
||||
if (IS_EXPANSION_COMPATIBLE) {
|
||||
scu_pinmux(scu->PINMUX_PP_TMS, SCU_GPIO_PUP | SCU_CONF_FUNCTION0);
|
||||
scu_pinmux(scu->PINMUX_PP_TDO, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
|
||||
}
|
||||
#endif
|
||||
scu_pinmux(scu->PINMUX_CPLD_TCK, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
|
||||
#ifdef IS_NOT_PRALINE
|
||||
if (IS_NOT_PRALINE) {
|
||||
scu_pinmux(scu->PINMUX_CPLD_TMS, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION0);
|
||||
scu_pinmux(scu->PINMUX_CPLD_TDI, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION0);
|
||||
scu_pinmux(scu->PINMUX_CPLD_TDO, SCU_GPIO_PDN | SCU_CONF_FUNCTION4);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Configure SCU Pin Mux as GPIO */
|
||||
scu_pinmux(scu->PINMUX_LED1, SCU_GPIO_NOPULL);
|
||||
scu_pinmux(scu->PINMUX_LED2, SCU_GPIO_NOPULL);
|
||||
scu_pinmux(scu->PINMUX_LED3, SCU_GPIO_NOPULL);
|
||||
#ifdef IS_RAD1O
|
||||
if (IS_RAD1O) {
|
||||
scu_pinmux(scu->PINMUX_LED4, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION4);
|
||||
}
|
||||
#endif
|
||||
#ifdef IS_PRALINE
|
||||
if (IS_PRALINE) {
|
||||
scu_pinmux(scu->PINMUX_LED4, SCU_GPIO_NOPULL | SCU_CONF_FUNCTION0);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Configure USB indicators */
|
||||
#ifdef IS_JAWBREAKER
|
||||
if (IS_JAWBREAKER) {
|
||||
scu_pinmux(scu->PINMUX_USB_LED0, SCU_CONF_FUNCTION3);
|
||||
scu_pinmux(scu->PINMUX_USB_LED1, SCU_CONF_FUNCTION3);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef IS_PRALINE
|
||||
if (IS_PRALINE) {
|
||||
// disable_1v2_power();
|
||||
// disable_3v3aux_power();
|
||||
// gpio_output(gpio->gpio_1v2_enable);
|
||||
// gpio_output(gpio->gpio_3v3aux_enable_n);
|
||||
// scu_pinmux(scu->PINMUX_EN1V2, SCU_GPIO_FAST | SCU_CONF_FUNCTION0);
|
||||
// scu_pinmux(scu->PINMUX_EN3V3_AUX_N, SCU_GPIO_FAST | SCU_CONF_FUNCTION4);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef IS_NOT_PRALINE
|
||||
if (IS_NOT_PRALINE) {
|
||||
disable_1v8_power();
|
||||
#ifdef IS_H1_R9
|
||||
if (IS_H1_R9) {
|
||||
gpio_output(gpio->h1r9_1v8_enable);
|
||||
scu_pinmux(scu->H1R9_EN1V8, SCU_GPIO_FAST | SCU_CONF_FUNCTION0);
|
||||
}
|
||||
#endif
|
||||
#ifdef IS_NOT_H1_R9
|
||||
if (IS_NOT_H1_R9) {
|
||||
gpio_output(gpio->gpio_1v8_enable);
|
||||
scu_pinmux(scu->PINMUX_EN1V8, SCU_GPIO_FAST | SCU_CONF_FUNCTION0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef IS_H1_OR_PRALINE
|
||||
if (IS_H1_OR_PRALINE) {
|
||||
/* Safe state: start with VAA turned off: */
|
||||
disable_rf_power();
|
||||
|
||||
/* Configure RF power supply (VAA) switch control signal as output */
|
||||
#ifdef IS_H1_R9
|
||||
if (IS_H1_R9) {
|
||||
gpio_output(gpio->h1r9_vaa_disable);
|
||||
}
|
||||
#endif
|
||||
#ifdef IS_NOT_H1_R9
|
||||
if (IS_NOT_H1_R9) {
|
||||
gpio_output(gpio->vaa_disable);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef IS_RAD1O
|
||||
if (IS_RAD1O) {
|
||||
/* Safe state: start with VAA turned off: */
|
||||
disable_rf_power();
|
||||
|
||||
/* Configure RF power supply (VAA) switch control signal as output */
|
||||
gpio_output(gpio->vaa_enable);
|
||||
|
||||
/* Disable unused clock outputs. They generate noise. */
|
||||
scu_pinmux(CLK0, SCU_CLK_IN | SCU_CONF_FUNCTION7);
|
||||
scu_pinmux(CLK2, SCU_CLK_IN | SCU_CONF_FUNCTION7);
|
||||
|
||||
scu_pinmux(scu->PINMUX_GPIO3_10, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
|
||||
scu_pinmux(scu->PINMUX_GPIO3_11, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef IS_PRALINE
|
||||
if (IS_PRALINE) {
|
||||
scu_pinmux(scu->P2_CTRL0, scu->P2_CTRL0_PINCFG);
|
||||
scu_pinmux(scu->P2_CTRL1, scu->P2_CTRL1_PINCFG);
|
||||
scu_pinmux(scu->P1_CTRL0, scu->P1_CTRL0_PINCFG);
|
||||
scu_pinmux(scu->P1_CTRL1, scu->P1_CTRL1_PINCFG);
|
||||
scu_pinmux(scu->P1_CTRL2, scu->P1_CTRL2_PINCFG);
|
||||
scu_pinmux(scu->CLKIN_CTRL, scu->CLKIN_CTRL_PINCFG);
|
||||
scu_pinmux(scu->AA_EN, scu->AA_EN_PINCFG);
|
||||
scu_pinmux(scu->TRIGGER_IN, scu->TRIGGER_IN_PINCFG);
|
||||
scu_pinmux(scu->TRIGGER_OUT, scu->TRIGGER_OUT_PINCFG);
|
||||
scu_pinmux(scu->PPS_OUT, scu->PPS_OUT_PINCFG);
|
||||
scu_pinmux(scu->SCT_CLK, scu->SCT_CLK_PINCFG);
|
||||
|
||||
scu_pinmux(scu->PINMUX_FPGA_CRESET, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
|
||||
scu_pinmux(scu->PINMUX_FPGA_CDONE, SCU_GPIO_PDN | SCU_CONF_FUNCTION4);
|
||||
scu_pinmux(scu->PINMUX_FPGA_SPI_CS, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
|
||||
|
||||
scu_pinmux(scu->SSP1_CIPO, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
|
||||
scu_pinmux(scu->SSP1_COPI, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
|
||||
scu_pinmux(scu->SSP1_SCK, SCU_GPIO_PDN | SCU_CONF_FUNCTION2);
|
||||
|
||||
scu_pinmux(scu->XCVR_ENABLE, SCU_GPIO_PDN | SCU_CONF_FUNCTION4);
|
||||
scu_pinmux(scu->XCVR_RXENABLE, SCU_GPIO_PDN | SCU_CONF_FUNCTION4);
|
||||
scu_pinmux(scu->XCVR_CS, SCU_GPIO_PDN | SCU_CONF_FUNCTION4);
|
||||
scu_pinmux(scu->XCVR_RXHP, SCU_GPIO_PDN | SCU_CONF_FUNCTION4);
|
||||
scu_pinmux(scu->XCVR_LD, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
|
||||
|
||||
scu_pinmux(scu->MIXER_LD, SCU_GPIO_PDN | SCU_CONF_FUNCTION4);
|
||||
scu_pinmux(scu->MIXER_SCLK, SCU_GPIO_PDN | SCU_CONF_FUNCTION4);
|
||||
scu_pinmux(scu->MIXER_SDATA, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
|
||||
scu_pinmux(scu->MIXER_ENX, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
|
||||
scu_pinmux(scu->MIXER_RESETX, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
|
||||
scu_pinmux(scu->MIXER_ENBL, SCU_GPIO_PDN | SCU_CONF_FUNCTION0);
|
||||
|
||||
scu_pinmux(scu->AD_CS, SCU_GPIO_PDN | SCU_CONF_FUNCTION4);
|
||||
|
||||
p2_ctrl_set(P2_SIGNAL_CLK3);
|
||||
p1_ctrl_set(P1_SIGNAL_CLKIN);
|
||||
clkin_ctrl_set(CLKIN_SIGNAL_P1);
|
||||
|
||||
gpio_output(gpio->p2_ctrl0);
|
||||
gpio_output(gpio->p2_ctrl1);
|
||||
gpio_output(gpio->p1_ctrl0);
|
||||
gpio_output(gpio->p1_ctrl1);
|
||||
gpio_output(gpio->p1_ctrl2);
|
||||
gpio_output(gpio->clkin_ctrl);
|
||||
gpio_output(gpio->pps_out);
|
||||
gpio_input(gpio->trigger_in);
|
||||
gpio_input(gpio->trigger_out);
|
||||
gpio_clear(gpio->fpga_cfg_spi_cs);
|
||||
gpio_output(gpio->fpga_cfg_spi_cs);
|
||||
gpio_clear(gpio->fpga_cfg_creset);
|
||||
gpio_output(gpio->fpga_cfg_creset);
|
||||
gpio_input(gpio->fpga_cfg_cdone);
|
||||
gpio_input(gpio->max5864_select);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* enable input on SCL and SDA pins */
|
||||
SCU_SFSI2C0 = SCU_I2C0_NOMINAL;
|
||||
}
|
||||
|
||||
/* Run after pins_shutdown() and prior to enabling power supplies. */
|
||||
void pins_setup(void) {
|
||||
/* Detect Platform */
|
||||
const platform_gpio_t* gpio = platform_gpio();
|
||||
const platform_scu_t* scu = platform_scu();
|
||||
|
||||
/* Configure LEDs */
|
||||
led_off(0);
|
||||
led_off(1);
|
||||
led_off(2);
|
||||
#ifdef IS_FOUR_LEDS
|
||||
if (IS_FOUR_LEDS) {
|
||||
led_off(3);
|
||||
}
|
||||
#endif
|
||||
|
||||
gpio_output(gpio->led[0]);
|
||||
gpio_output(gpio->led[1]);
|
||||
gpio_output(gpio->led[2]);
|
||||
|
||||
#ifdef IS_FOUR_LEDS
|
||||
if (IS_FOUR_LEDS) {
|
||||
gpio_output(gpio->led[3]);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Configure external clock in */
|
||||
scu_pinmux(scu->PINMUX_GP_CLKIN, SCU_CLK_IN | SCU_CONF_FUNCTION1);
|
||||
}
|
||||
@@ -44,11 +44,11 @@ CH_IRQ_HANDLER(Vector60) {
|
||||
}
|
||||
|
||||
int main() {
|
||||
start_usb();
|
||||
|
||||
sdcStart(&SDCD1, nullptr);
|
||||
if (sdcConnect(&SDCD1) == CH_FAILED) chDbgPanic("no sd card #1");
|
||||
|
||||
start_usb();
|
||||
|
||||
while (true) {
|
||||
usb_transfer();
|
||||
}
|
||||
|
||||
@@ -23,14 +23,35 @@
|
||||
#include "scsi.h"
|
||||
#include "diskio.h"
|
||||
#include "gpio_lpc.h"
|
||||
#include "delay.h"
|
||||
#include <libopencm3/lpc43xx/scu.h>
|
||||
#include <libopencm3/lpc43xx/rgu.h>
|
||||
#include <libopencm3/lpc43xx/wwdt.h>
|
||||
#include "string.h"
|
||||
|
||||
/* Maximum number of 512-byte blocks transferred per SD command / USB bulk
|
||||
* transfer. The data region of usb_bulk_buffer is 16 KiB (0x0000..0x3FFF),
|
||||
* so this must be <= 32. It is split into two equal halves for double
|
||||
* buffering. */
|
||||
#define MAX_BLOCKS_PER_TRANSFER 32
|
||||
#define HALF_BLOCKS (MAX_BLOCKS_PER_TRANSFER / 2)
|
||||
|
||||
volatile bool usb_bulk_block_done = false;
|
||||
|
||||
void delay(uint32_t duration);
|
||||
/* Per-buffer-half completion flags for the asynchronous (double-buffered)
|
||||
* bulk transfers. Index 0 = usb_bulk_buffer[0], index 1 = second half. */
|
||||
volatile bool usb_bulk_block_done_async[2] = {false, false};
|
||||
|
||||
static uint32_t usb_bulk_buffer_index(const void* const data) {
|
||||
return (data == &usb_bulk_buffer[HALF_BLOCKS * 512]) ? 1 : 0;
|
||||
}
|
||||
|
||||
void usb_bulk_block_cb_async(void* user_data, unsigned int bytes_transferred) {
|
||||
const uint32_t idx = (uint32_t)(uintptr_t)user_data;
|
||||
usb_bulk_block_done_async[idx] = true;
|
||||
|
||||
(void)bytes_transferred;
|
||||
}
|
||||
|
||||
void usb_bulk_block_cb(void* user_data, unsigned int bytes_transferred) {
|
||||
usb_bulk_block_done = true;
|
||||
@@ -65,6 +86,49 @@ void usb_receive_bulk(void* const data, const uint32_t maximum_length) {
|
||||
while (!usb_bulk_block_done);
|
||||
}
|
||||
|
||||
/* Schedule a bulk IN transfer without waiting for it to complete. The caller
|
||||
* must later call usb_send_bulk_wait_finish() with the same buffer before
|
||||
* reusing it. */
|
||||
void usb_send_bulk_start(void* const data, const uint32_t maximum_length) {
|
||||
const uint32_t idx = usb_bulk_buffer_index(data);
|
||||
usb_bulk_block_done_async[idx] = false;
|
||||
|
||||
usb_transfer_schedule_block(
|
||||
&usb_endpoint_bulk_in,
|
||||
data,
|
||||
maximum_length,
|
||||
usb_bulk_block_cb_async,
|
||||
(void*)(uintptr_t)idx);
|
||||
}
|
||||
|
||||
/* Wait for a bulk IN transfer scheduled by usb_send_bulk_start() to finish. */
|
||||
void usb_send_bulk_wait_finish(void* const data) {
|
||||
const uint32_t idx = usb_bulk_buffer_index(data);
|
||||
while (!usb_bulk_block_done_async[idx]);
|
||||
}
|
||||
|
||||
/* Schedule a bulk OUT transfer without waiting for it to complete. The caller
|
||||
* must later call usb_receive_bulk_finish() with the same buffer before
|
||||
* reading from it. */
|
||||
void usb_receive_bulk_start(void* const data, const uint32_t maximum_length) {
|
||||
const uint32_t idx = usb_bulk_buffer_index(data);
|
||||
usb_bulk_block_done_async[idx] = false;
|
||||
|
||||
usb_transfer_schedule_block(
|
||||
&usb_endpoint_bulk_out,
|
||||
data,
|
||||
maximum_length,
|
||||
usb_bulk_block_cb_async,
|
||||
(void*)(uintptr_t)idx);
|
||||
}
|
||||
|
||||
/* Wait for a bulk OUT transfer scheduled by usb_receive_bulk_start() to
|
||||
* finish. */
|
||||
void usb_receive_bulk_finish(void* const data) {
|
||||
const uint32_t idx = usb_bulk_buffer_index(data);
|
||||
while (!usb_bulk_block_done_async[idx]);
|
||||
}
|
||||
|
||||
void usb_send_csw(msd_cbw_t* msd_cbw_data, uint8_t status) {
|
||||
msd_csw_t csw = {
|
||||
.signature = MSD_CSW_SIGNATURE,
|
||||
@@ -213,22 +277,95 @@ static data_request_t decode_data_request(const uint8_t* cmd) {
|
||||
uint8_t data_read10(msd_cbw_t* msd_cbw_data) {
|
||||
data_request_t req = decode_data_request(msd_cbw_data->cmd_data);
|
||||
|
||||
for (size_t block_index = 0; block_index < req.blk_cnt; block_index++) {
|
||||
read_block(req.first_lba + block_index, &usb_bulk_buffer[0], 1 /* n blocks */);
|
||||
usb_send_bulk(&usb_bulk_buffer[0], 512);
|
||||
uint32_t lba = req.first_lba;
|
||||
uint32_t remaining = req.blk_cnt;
|
||||
uint8_t* buf[2] = {&usb_bulk_buffer[0], &usb_bulk_buffer[HALF_BLOCKS * 512]};
|
||||
uint32_t buf_idx = 0;
|
||||
uint8_t* in_flight = NULL;
|
||||
|
||||
if (remaining == 0)
|
||||
return 0;
|
||||
|
||||
/* Read the first chunk and start sending it. */
|
||||
uint32_t n = (remaining > HALF_BLOCKS) ? HALF_BLOCKS : remaining;
|
||||
if (read_block(lba, buf[0], n))
|
||||
return 1;
|
||||
usb_send_bulk_start(buf[0], n * 512);
|
||||
in_flight = buf[0];
|
||||
lba += n;
|
||||
remaining -= n;
|
||||
|
||||
/* While USB sends the previous chunk, read the next one into the other
|
||||
* half of the buffer. */
|
||||
while (remaining > 0) {
|
||||
buf_idx ^= 1;
|
||||
n = (remaining > HALF_BLOCKS) ? HALF_BLOCKS : remaining;
|
||||
|
||||
if (read_block(lba, buf[buf_idx], n)) {
|
||||
usb_send_bulk_wait_finish(in_flight);
|
||||
return 1;
|
||||
}
|
||||
|
||||
usb_send_bulk_wait_finish(in_flight);
|
||||
usb_send_bulk_start(buf[buf_idx], n * 512);
|
||||
in_flight = buf[buf_idx];
|
||||
|
||||
lba += n;
|
||||
remaining -= n;
|
||||
}
|
||||
|
||||
usb_send_bulk_wait_finish(in_flight);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t data_write10(msd_cbw_t* msd_cbw_data) {
|
||||
data_request_t req = decode_data_request(msd_cbw_data->cmd_data);
|
||||
|
||||
for (size_t block_index = 0; block_index < req.blk_cnt; block_index++) {
|
||||
usb_receive_bulk(&usb_bulk_buffer[0], 512);
|
||||
write_block(req.first_lba + block_index, &usb_bulk_buffer[0], 1 /* n blocks */);
|
||||
uint32_t lba = req.first_lba;
|
||||
uint32_t remaining = req.blk_cnt;
|
||||
uint8_t* buf[2] = {&usb_bulk_buffer[0], &usb_bulk_buffer[HALF_BLOCKS * 512]};
|
||||
uint32_t buf_idx = 0;
|
||||
uint8_t* pending_buf = NULL;
|
||||
uint32_t pending_lba = 0;
|
||||
uint32_t pending_n = 0;
|
||||
|
||||
if (remaining == 0)
|
||||
return 0;
|
||||
|
||||
/* Start receiving the first chunk. */
|
||||
uint32_t n = (remaining > HALF_BLOCKS) ? HALF_BLOCKS : remaining;
|
||||
usb_receive_bulk_start(buf[0], n * 512);
|
||||
pending_buf = buf[0];
|
||||
pending_lba = lba;
|
||||
pending_n = n;
|
||||
lba += n;
|
||||
remaining -= n;
|
||||
|
||||
/* While USB receives the next chunk, write the previous one to the SD
|
||||
* card. */
|
||||
while (remaining > 0) {
|
||||
buf_idx ^= 1;
|
||||
n = (remaining > HALF_BLOCKS) ? HALF_BLOCKS : remaining;
|
||||
|
||||
usb_receive_bulk_start(buf[buf_idx], n * 512);
|
||||
|
||||
usb_receive_bulk_finish(pending_buf);
|
||||
if (write_block(pending_lba, pending_buf, pending_n)) {
|
||||
usb_receive_bulk_finish(buf[buf_idx]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
pending_buf = buf[buf_idx];
|
||||
pending_lba = lba;
|
||||
pending_n = n;
|
||||
lba += n;
|
||||
remaining -= n;
|
||||
}
|
||||
|
||||
usb_receive_bulk_finish(pending_buf);
|
||||
if (write_block(pending_lba, pending_buf, pending_n))
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -293,7 +430,7 @@ void scsi_command(msd_cbw_t* msd_cbw_data) {
|
||||
gpio_output(&dfu);
|
||||
gpio_clear(&dfu);
|
||||
|
||||
delay(50 * 40800);
|
||||
delay_ms(50);
|
||||
|
||||
RESET_CTRL0 = (1 << 0);
|
||||
break;
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "sd_over_usb.h"
|
||||
#include "scsi.h"
|
||||
#include "usb_descriptor.h"
|
||||
#include <rom_iap.h>
|
||||
#include "delay.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
@@ -36,8 +38,14 @@
|
||||
extern usb_configuration_t* usb_configurations[];
|
||||
|
||||
static const usb_device_t usb_device_sd_over_usb = {
|
||||
.descriptor = usb_descriptor_device,
|
||||
.descriptor_strings = usb_descriptor_strings,
|
||||
#ifdef IS_NOT_PRALINE
|
||||
.descriptor = usb_descriptor_device_hackrf,
|
||||
.descriptor_strings = usb_descriptor_strings_hackrf_one,
|
||||
#endif
|
||||
#ifdef IS_PRALINE
|
||||
.descriptor = usb_descriptor_device_hackrf,
|
||||
.descriptor_strings = usb_descriptor_strings_praline,
|
||||
#endif
|
||||
.qualifier_descriptor = usb_descriptor_device_qualifier,
|
||||
.configurations = &usb_configurations,
|
||||
.configuration = 0,
|
||||
@@ -91,16 +99,83 @@ void usb_configuration_changed(usb_device_t* const device) {
|
||||
usb_endpoint_init(&usb_endpoint_bulk_out, false);
|
||||
}
|
||||
|
||||
void usb_set_descriptor_by_serial_number(void) {
|
||||
iap_cmd_res_t iap_cmd_res;
|
||||
|
||||
/* Read IAP Serial Number Identification */
|
||||
iap_cmd_res.cmd_param.command_code = IAP_CMD_READ_SERIAL_NO;
|
||||
iap_cmd_call(&iap_cmd_res);
|
||||
|
||||
if (iap_cmd_res.status_res.status_ret == CMD_SUCCESS) {
|
||||
usb_descriptor_string_serial_number[0] =
|
||||
USB_DESCRIPTOR_STRING_SERIAL_BUF_LEN;
|
||||
usb_descriptor_string_serial_number[1] = USB_DESCRIPTOR_TYPE_STRING;
|
||||
|
||||
/* 32 characters of serial number, convert to UTF-16LE */
|
||||
for (size_t i = 0; i < USB_DESCRIPTOR_STRING_SERIAL_LEN; i++) {
|
||||
const uint_fast8_t nibble =
|
||||
(iap_cmd_res.status_res.iap_result[i >> 3] >>
|
||||
(28 - (i & 7) * 4)) &
|
||||
0xf;
|
||||
const char c =
|
||||
(nibble > 9) ? ('a' + nibble - 10) : ('0' + nibble);
|
||||
usb_descriptor_string_serial_number[2 + i * 2] = c;
|
||||
usb_descriptor_string_serial_number[3 + i * 2] = 0x00;
|
||||
}
|
||||
} else {
|
||||
usb_descriptor_string_serial_number[0] = 2;
|
||||
usb_descriptor_string_serial_number[1] = USB_DESCRIPTOR_TYPE_STRING;
|
||||
}
|
||||
}
|
||||
|
||||
void start_usb(void) {
|
||||
// Detect hardware platform before we do anything else.
|
||||
detect_hardware_platform();
|
||||
pin_setup();
|
||||
board_id_t board_id = detected_platform();
|
||||
|
||||
pins_shutdown();
|
||||
sgpio_pin_shutdown(&sgpio_config);
|
||||
rf_path_pin_shutdown();
|
||||
if (board_id != BOARD_ID_RAD1O) {
|
||||
clock_gen_shutdown();
|
||||
}
|
||||
delay_ms(10);
|
||||
pins_setup();
|
||||
cpld_jtag_pin_setup();
|
||||
|
||||
cpu_clock_init();
|
||||
|
||||
memcpy(&usb_device, &usb_device_sd_over_usb, sizeof(usb_device_sd_over_usb));
|
||||
#ifndef DFU_MODE
|
||||
usb_set_descriptor_by_serial_number();
|
||||
#endif
|
||||
|
||||
usb_set_configuration_changed_cb(usb_configuration_changed);
|
||||
usb_peripheral_reset();
|
||||
|
||||
#ifdef IS_HACKRF_ONE
|
||||
if (IS_HACKRF_ONE) {
|
||||
memcpy(&usb_device,
|
||||
&usb_device_sd_over_usb,
|
||||
sizeof(usb_device_sd_over_usb));
|
||||
}
|
||||
#endif
|
||||
#ifdef IS_JAWBREAKER
|
||||
if (IS_JAWBREAKER) {
|
||||
memcpy(&usb_device,
|
||||
&usb_device_jawbreaker,
|
||||
sizeof(usb_device_jawbreaker));
|
||||
}
|
||||
#endif
|
||||
#ifdef IS_RAD1O
|
||||
if (IS_RAD1O) {
|
||||
memcpy(&usb_device, &usb_device_rad1o, sizeof(usb_device_rad1o));
|
||||
}
|
||||
#endif
|
||||
#ifdef IS_PRALINE
|
||||
if (IS_PRALINE) {
|
||||
memcpy(&usb_device, &usb_device_sd_over_usb, sizeof(usb_device_sd_over_usb));
|
||||
}
|
||||
#endif
|
||||
usb_device_init(0, &usb_device);
|
||||
|
||||
usb_queue_init(&usb_endpoint_control_out_queue);
|
||||
@@ -109,9 +184,6 @@ void start_usb(void) {
|
||||
usb_queue_init(&usb_endpoint_bulk_in_queue);
|
||||
|
||||
usb_endpoint_init(&usb_endpoint_control_out, false);
|
||||
/* Match the new usb_endpoint_init() contract introduced upstream by
|
||||
* db73ecbf, control IN needs ZLP for transfers whose length is a
|
||||
* multiple of the EP0 max packet size, otherwise the host hangs. */
|
||||
usb_endpoint_init(&usb_endpoint_control_in, true);
|
||||
|
||||
nvic_set_priority(NVIC_USB0_IRQ, 255);
|
||||
|
||||
@@ -0,0 +1,766 @@
|
||||
/*
|
||||
* Copyright 2012-2022 Great Scott Gadgets <info@greatscottgadgets.com>
|
||||
* Copyright 2012 Jared Boone
|
||||
* Copyright 2013 Benjamin Vernoux
|
||||
* Copyright 2024 Bernd Herzog
|
||||
*
|
||||
* This file is part of HackRF.
|
||||
*
|
||||
* 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 "usb_api_transceiver.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include <libopencm3/cm3/nvic.h>
|
||||
#include <libopencm3/lpc43xx/gpdma.h>
|
||||
#include <libopencm3/lpc43xx/usb.h>
|
||||
|
||||
#include <clock_gen.h>
|
||||
#include <fixed_point.h>
|
||||
#include <gpdma.h>
|
||||
#include <hackrf_ui.h>
|
||||
#include <leds.h>
|
||||
#include <m0_state.h>
|
||||
#include <operacake_sctimer.h>
|
||||
#include <platform_detect.h>
|
||||
#include <radio.h>
|
||||
#include <sgpio.h>
|
||||
#include <streaming.h>
|
||||
#include <transceiver_mode.h>
|
||||
#include "common/usb.h"
|
||||
#include <usb_queue.h>
|
||||
#include <usb_request.h>
|
||||
#include <usb_type.h>
|
||||
|
||||
#include "usb_buffer.h"
|
||||
#include "usb_endpoint.h"
|
||||
|
||||
#define USB_TRANSFER_SIZE 0x4000
|
||||
#define DMA_TRANSFER_SIZE 0x2000
|
||||
|
||||
#define BUF_HALF_MASK (USB_SAMP_BUFFER_SIZE >> 1)
|
||||
|
||||
// Unless we know the host knows our buffer size, we'll avoid leaving TX
|
||||
// until we've transmitted all bytes sent by the host. This flag is cleared
|
||||
// when the host requests our buffer size.
|
||||
bool auto_tx_flush = true;
|
||||
|
||||
volatile uint32_t dma_started, dma_pending, usb_started, usb_completed;
|
||||
|
||||
typedef struct {
|
||||
uint32_t freq_mhz;
|
||||
uint32_t freq_hz;
|
||||
} set_freq_params_t;
|
||||
|
||||
set_freq_params_t set_freq_params;
|
||||
|
||||
struct set_freq_explicit_params {
|
||||
uint64_t if_freq_hz; /* intermediate frequency */
|
||||
uint64_t lo_freq_hz; /* front-end local oscillator frequency */
|
||||
uint8_t path; /* image rejection filter path */
|
||||
};
|
||||
|
||||
struct set_freq_explicit_params explicit_params;
|
||||
|
||||
typedef struct {
|
||||
uint32_t freq_hz;
|
||||
uint32_t divider;
|
||||
} set_sample_r_params_t;
|
||||
|
||||
set_sample_r_params_t set_sample_r_params;
|
||||
|
||||
void transceiver_dma_setup(void);
|
||||
|
||||
usb_request_status_t usb_vendor_request_set_baseband_filter_bandwidth(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
const uint32_t bandwidth =
|
||||
(endpoint->setup.index << 16) | endpoint->setup.value;
|
||||
radio_reg_write(
|
||||
&radio,
|
||||
RADIO_BANK_REQUESTED,
|
||||
RADIO_BB_BANDWIDTH_TX,
|
||||
bandwidth);
|
||||
radio_reg_write(
|
||||
&radio,
|
||||
RADIO_BANK_REQUESTED,
|
||||
RADIO_BB_BANDWIDTH_RX,
|
||||
bandwidth);
|
||||
usb_transfer_schedule_ack(endpoint->in);
|
||||
}
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
|
||||
usb_request_status_t usb_vendor_request_set_freq(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
usb_transfer_schedule_block(
|
||||
endpoint->out,
|
||||
&set_freq_params,
|
||||
sizeof(set_freq_params_t),
|
||||
NULL,
|
||||
NULL);
|
||||
} else if (stage == USB_TRANSFER_STAGE_DATA) {
|
||||
const uint64_t freq =
|
||||
set_freq_params.freq_mhz * 1000000ULL + set_freq_params.freq_hz;
|
||||
radio_reg_write(
|
||||
&radio,
|
||||
RADIO_BANK_REQUESTED,
|
||||
RADIO_FREQUENCY_RF,
|
||||
freq * FP_ONE_HZ);
|
||||
radio_reg_write(
|
||||
&radio,
|
||||
RADIO_BANK_REQUESTED,
|
||||
RADIO_FREQUENCY_IF,
|
||||
RADIO_UNSET);
|
||||
radio_reg_write(
|
||||
&radio,
|
||||
RADIO_BANK_REQUESTED,
|
||||
RADIO_FREQUENCY_LO,
|
||||
RADIO_UNSET);
|
||||
radio_reg_write(
|
||||
&radio,
|
||||
RADIO_BANK_REQUESTED,
|
||||
RADIO_IMAGE_REJECT,
|
||||
RADIO_UNSET);
|
||||
usb_transfer_schedule_ack(endpoint->in);
|
||||
}
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
|
||||
usb_request_status_t usb_vendor_request_set_freq_explicit(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
usb_transfer_schedule_block(
|
||||
endpoint->out,
|
||||
&explicit_params,
|
||||
sizeof(struct set_freq_explicit_params),
|
||||
NULL,
|
||||
NULL);
|
||||
} else if (stage == USB_TRANSFER_STAGE_DATA) {
|
||||
radio_reg_write(
|
||||
&radio,
|
||||
RADIO_BANK_REQUESTED,
|
||||
RADIO_FREQUENCY_IF,
|
||||
explicit_params.if_freq_hz * FP_ONE_HZ);
|
||||
radio_reg_write(
|
||||
&radio,
|
||||
RADIO_BANK_REQUESTED,
|
||||
RADIO_FREQUENCY_LO,
|
||||
explicit_params.lo_freq_hz * FP_ONE_HZ);
|
||||
radio_reg_write(
|
||||
&radio,
|
||||
RADIO_BANK_REQUESTED,
|
||||
RADIO_IMAGE_REJECT,
|
||||
explicit_params.path);
|
||||
usb_transfer_schedule_ack(endpoint->in);
|
||||
}
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert fractional sample rate to units of 1/(2**36) Hz.
|
||||
*/
|
||||
static inline fp_28_36_t round_sample_rate(uint64_t num, uint32_t denom) {
|
||||
uint64_t q1, r1, q2, r2, q3;
|
||||
|
||||
if (denom == 0) {
|
||||
denom = 1;
|
||||
}
|
||||
|
||||
q1 = num / denom;
|
||||
r1 = num % denom;
|
||||
|
||||
q2 = (r1 << 32) / denom;
|
||||
r2 = (r1 << 32) % denom;
|
||||
|
||||
q3 = ((r2 << 4) + (denom >> 1)) / denom;
|
||||
|
||||
return (q1 << 36) + (q2 << 4) + q3;
|
||||
}
|
||||
|
||||
usb_request_status_t usb_vendor_request_set_sample_rate_frac(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
usb_transfer_schedule_block(
|
||||
endpoint->out,
|
||||
&set_sample_r_params,
|
||||
sizeof(set_sample_r_params_t),
|
||||
NULL,
|
||||
NULL);
|
||||
} else if (stage == USB_TRANSFER_STAGE_DATA) {
|
||||
uint32_t numerator = set_sample_r_params.freq_hz;
|
||||
uint32_t denominator = set_sample_r_params.divider;
|
||||
uint64_t value = round_sample_rate(numerator, denominator);
|
||||
radio_reg_write(&radio, RADIO_BANK_REQUESTED, RADIO_SAMPLE_RATE, value);
|
||||
usb_transfer_schedule_ack(endpoint->in);
|
||||
}
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
|
||||
usb_request_status_t usb_vendor_request_set_amp_enable(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
radio_reg_write(
|
||||
&radio,
|
||||
RADIO_BANK_REQUESTED,
|
||||
RADIO_GAIN_TX_RF,
|
||||
endpoint->setup.value);
|
||||
radio_reg_write(
|
||||
&radio,
|
||||
RADIO_BANK_REQUESTED,
|
||||
RADIO_GAIN_RX_RF,
|
||||
endpoint->setup.value);
|
||||
usb_transfer_schedule_ack(endpoint->in);
|
||||
}
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
|
||||
usb_request_status_t usb_vendor_request_set_lna_gain(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
uint8_t gain = endpoint->setup.index;
|
||||
radio_reg_write(&radio, RADIO_BANK_REQUESTED, RADIO_GAIN_RX_IF, gain);
|
||||
endpoint->buffer[0] = RADIO_OK;
|
||||
usb_transfer_schedule_block(
|
||||
endpoint->in,
|
||||
&endpoint->buffer,
|
||||
1,
|
||||
NULL,
|
||||
NULL);
|
||||
usb_transfer_schedule_ack(endpoint->out);
|
||||
}
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
|
||||
usb_request_status_t usb_vendor_request_set_vga_gain(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
uint8_t gain = endpoint->setup.index;
|
||||
radio_reg_write(&radio, RADIO_BANK_REQUESTED, RADIO_GAIN_RX_BB, gain);
|
||||
endpoint->buffer[0] = RADIO_OK;
|
||||
usb_transfer_schedule_block(
|
||||
endpoint->in,
|
||||
&endpoint->buffer,
|
||||
1,
|
||||
NULL,
|
||||
NULL);
|
||||
usb_transfer_schedule_ack(endpoint->out);
|
||||
}
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
|
||||
usb_request_status_t usb_vendor_request_set_txvga_gain(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
uint8_t gain = endpoint->setup.index;
|
||||
radio_reg_write(&radio, RADIO_BANK_REQUESTED, RADIO_GAIN_TX_IF, gain);
|
||||
endpoint->buffer[0] = RADIO_OK;
|
||||
usb_transfer_schedule_block(
|
||||
endpoint->in,
|
||||
&endpoint->buffer,
|
||||
1,
|
||||
NULL,
|
||||
NULL);
|
||||
usb_transfer_schedule_ack(endpoint->out);
|
||||
}
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
|
||||
usb_request_status_t usb_vendor_request_set_antenna_enable(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
switch (detected_platform()) {
|
||||
case BOARD_ID_HACKRF1_OG:
|
||||
case BOARD_ID_HACKRF1_R9:
|
||||
case BOARD_ID_PRALINE:
|
||||
// supported
|
||||
break;
|
||||
default:
|
||||
return USB_REQUEST_STATUS_STALL;
|
||||
}
|
||||
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
radio_reg_write(
|
||||
&radio,
|
||||
RADIO_BANK_REQUESTED,
|
||||
RADIO_BIAS_TEE,
|
||||
endpoint->setup.value);
|
||||
usb_transfer_schedule_ack(endpoint->in);
|
||||
}
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
|
||||
static volatile uint32_t _tx_underrun_limit;
|
||||
static volatile uint32_t _rx_overrun_limit;
|
||||
|
||||
volatile transceiver_request_t transceiver_request = {
|
||||
.mode = TRANSCEIVER_MODE_OFF,
|
||||
.seq = 0,
|
||||
};
|
||||
|
||||
void transceiver_usb_setup_complete(usb_endpoint_t* const endpoint) {
|
||||
if (transceiver_request.mode == TRANSCEIVER_MODE_TX &&
|
||||
endpoint->setup.request == 1 && auto_tx_flush) {
|
||||
// This is a request to leave TX mode. Do so but NAK for now.
|
||||
request_transceiver_mode(endpoint->setup.value);
|
||||
} else {
|
||||
usb_setup_complete(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
// Must be called from an atomic context (normally USB ISR)
|
||||
void request_transceiver_mode(transceiver_mode_t mode) {
|
||||
usb_endpoint_flush(&usb_endpoint_bulk_in);
|
||||
usb_endpoint_flush(&usb_endpoint_bulk_out);
|
||||
|
||||
transceiver_request.mode = mode;
|
||||
transceiver_request.seq++;
|
||||
}
|
||||
|
||||
void transceiver_shutdown(void) {
|
||||
baseband_streaming_disable(&sgpio_config);
|
||||
operacake_sctimer_reset_state();
|
||||
|
||||
usb_endpoint_flush(&usb_endpoint_bulk_in);
|
||||
usb_endpoint_flush(&usb_endpoint_bulk_out);
|
||||
|
||||
led_off(LED2);
|
||||
led_off(LED3);
|
||||
radio_switch_opmode(&radio, TRANSCEIVER_MODE_OFF);
|
||||
m0_set_mode(M0_MODE_IDLE);
|
||||
}
|
||||
|
||||
void transceiver_startup(const transceiver_mode_t mode) {
|
||||
dma_started = 0;
|
||||
dma_pending = 0;
|
||||
usb_started = 0;
|
||||
usb_completed = 0;
|
||||
|
||||
transceiver_dma_setup();
|
||||
|
||||
radio_switch_opmode(&radio, mode);
|
||||
|
||||
switch (mode) {
|
||||
case TRANSCEIVER_MODE_RX_SWEEP:
|
||||
case TRANSCEIVER_MODE_RX:
|
||||
led_off(LED3);
|
||||
led_on(LED2);
|
||||
m0_set_mode(M0_MODE_RX);
|
||||
m0_state.shortfall_limit = _rx_overrun_limit;
|
||||
break;
|
||||
case TRANSCEIVER_MODE_TX:
|
||||
led_off(LED2);
|
||||
led_on(LED3);
|
||||
m0_set_mode(M0_MODE_TX_START);
|
||||
m0_state.shortfall_limit = _tx_underrun_limit;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
activate_best_clock_source();
|
||||
}
|
||||
|
||||
usb_request_status_t usb_vendor_request_set_transceiver_mode(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
switch (endpoint->setup.value) {
|
||||
case TRANSCEIVER_MODE_OFF:
|
||||
case TRANSCEIVER_MODE_RX:
|
||||
case TRANSCEIVER_MODE_TX:
|
||||
case TRANSCEIVER_MODE_RX_SWEEP:
|
||||
case TRANSCEIVER_MODE_CPLD_UPDATE:
|
||||
request_transceiver_mode(endpoint->setup.value);
|
||||
usb_transfer_schedule_ack(endpoint->in);
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
default:
|
||||
return USB_REQUEST_STATUS_STALL;
|
||||
}
|
||||
} else {
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
}
|
||||
|
||||
usb_request_status_t usb_vendor_request_set_hw_sync_mode(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
radio_reg_write(
|
||||
&radio,
|
||||
RADIO_BANK_REQUESTED,
|
||||
RADIO_TRIGGER,
|
||||
endpoint->setup.value);
|
||||
usb_transfer_schedule_ack(endpoint->in);
|
||||
}
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
|
||||
usb_request_status_t usb_vendor_request_set_tx_underrun_limit(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
uint32_t value = (endpoint->setup.index << 16) + endpoint->setup.value;
|
||||
_tx_underrun_limit = value;
|
||||
usb_transfer_schedule_ack(endpoint->in);
|
||||
}
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
|
||||
usb_request_status_t usb_vendor_request_set_rx_overrun_limit(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
uint32_t value = (endpoint->setup.index << 16) + endpoint->setup.value;
|
||||
_rx_overrun_limit = value;
|
||||
usb_transfer_schedule_ack(endpoint->in);
|
||||
}
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
|
||||
usb_request_status_t usb_vendor_request_get_buffer_size(
|
||||
usb_endpoint_t* const endpoint,
|
||||
const usb_transfer_stage_t stage) {
|
||||
if (stage == USB_TRANSFER_STAGE_SETUP) {
|
||||
uint32_t value = USB_SAMP_BUFFER_SIZE + USB_BULK_BUFFER_SIZE;
|
||||
endpoint->buffer[0] = value & 0xff;
|
||||
endpoint->buffer[1] = (value & 0xff00) >> 8;
|
||||
endpoint->buffer[2] = (value & 0xff0000) >> 16;
|
||||
endpoint->buffer[3] = (value & 0xff000000) >> 24;
|
||||
usb_transfer_schedule_block(
|
||||
endpoint->in,
|
||||
&endpoint->buffer,
|
||||
4,
|
||||
NULL,
|
||||
NULL);
|
||||
usb_transfer_schedule_ack(endpoint->out);
|
||||
|
||||
// We now know the host is aware of our buffer size, so it
|
||||
// can make its own decisions about flushing the buffer.
|
||||
auto_tx_flush = false;
|
||||
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
return USB_REQUEST_STATUS_OK;
|
||||
}
|
||||
|
||||
/* clang-format off */
|
||||
|
||||
// Which GPDMA channel to use.
|
||||
const uint32_t DMA_CHANNEL = 1;
|
||||
|
||||
// GPDMA CCONFIG register setting.
|
||||
const uint32_t DMA_CONFIG =
|
||||
GPDMA_CCONFIG_FLOWCNTRL(0) // memory-to-memory
|
||||
| GPDMA_CCONFIG_IE(0) // no error interrupt
|
||||
| GPDMA_CCONFIG_ITC(1) // terminal count interrupt
|
||||
| GPDMA_CCONFIG_L(0) // do not lock
|
||||
| GPDMA_CCONFIG_H(0); // do not halt
|
||||
|
||||
// GPDMA CCONTROL register setting (excluding TRANSFERSIZE field).
|
||||
const uint32_t DMA_CONTROL =
|
||||
GPDMA_CCONTROL_SBSIZE(7) // 256-transfer src bursts
|
||||
| GPDMA_CCONTROL_DBSIZE(7) // 256-transfer dst bursts
|
||||
| GPDMA_CCONTROL_SWIDTH(2) // 32-bit src transfers
|
||||
| GPDMA_CCONTROL_DWIDTH(2) // 32-bit dst transfers
|
||||
| GPDMA_CCONTROL_S(0) // AHB Master 0
|
||||
| GPDMA_CCONTROL_D(1) // AHB Master 1
|
||||
| GPDMA_CCONTROL_SI(1) // increment source
|
||||
| GPDMA_CCONTROL_DI(1) // increment destination
|
||||
| GPDMA_CCONTROL_PROT1(0) // user mode
|
||||
| GPDMA_CCONTROL_PROT2(0) // not bufferable
|
||||
| GPDMA_CCONTROL_PROT3(0) // not cacheable
|
||||
| GPDMA_CCONTROL_I(1); // interrupt enabled
|
||||
|
||||
/* clang-format on */
|
||||
|
||||
// Called before any sequence of DMA transfers.
|
||||
void transceiver_dma_setup(void) {
|
||||
gpdma_controller_enable();
|
||||
GPDMA_CCONFIG(DMA_CHANNEL) = DMA_CONFIG;
|
||||
GPDMA_CCONTROL(DMA_CHANNEL) = DMA_CONTROL;
|
||||
GPDMA_CLLI(DMA_CHANNEL) = 0;
|
||||
GPDMA_INTTCCLEAR = (1 << DMA_CHANNEL);
|
||||
nvic_enable_irq(NVIC_DMA_IRQ);
|
||||
}
|
||||
|
||||
// Called to start each DMA transfer.
|
||||
void transceiver_start_dma(void* src, void* dest, size_t size) {
|
||||
uint32_t num_transfers = size >> 2;
|
||||
GPDMA_CCONTROL(DMA_CHANNEL) = DMA_CONTROL | num_transfers;
|
||||
GPDMA_CSRCADDR(DMA_CHANNEL) = (uint32_t)src;
|
||||
GPDMA_CDESTADDR(DMA_CHANNEL) = (uint32_t)dest;
|
||||
dma_pending = size;
|
||||
gpdma_channel_enable(DMA_CHANNEL);
|
||||
}
|
||||
|
||||
// Called when a DMA transfer completes.
|
||||
void dma_isr(void) {
|
||||
gpdma_channel_disable(DMA_CHANNEL);
|
||||
GPDMA_INTTCCLEAR = (1 << DMA_CHANNEL);
|
||||
m0_state.m4_count += dma_pending;
|
||||
dma_pending = 0;
|
||||
}
|
||||
|
||||
void transceiver_bulk_transfer_complete(void* user_data, unsigned int bytes_transferred) {
|
||||
(void)user_data;
|
||||
usb_completed += bytes_transferred;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
DIRECTION_RX,
|
||||
DIRECTION_TX,
|
||||
} direction_t;
|
||||
|
||||
void start_dma_if_possible(direction_t direction, size_t size) {
|
||||
if (dma_pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t sampling_completed = m0_state.m0_count;
|
||||
uint32_t dma_completed = m0_state.m4_count;
|
||||
uint32_t samp_offset = dma_started & USB_SAMP_BUFFER_MASK;
|
||||
uint32_t bulk_offset = dma_started & USB_BULK_BUFFER_MASK;
|
||||
uint32_t data_available, space_in_use, space_available, samp_buf_margin;
|
||||
uint8_t *dest, *src;
|
||||
|
||||
if (direction == DIRECTION_RX) {
|
||||
data_available = sampling_completed - dma_started;
|
||||
space_in_use = usb_completed - dma_completed;
|
||||
space_available = USB_BULK_BUFFER_SIZE - space_in_use;
|
||||
samp_buf_margin = USB_SAMP_BUFFER_SIZE - data_available;
|
||||
src = &usb_samp_buffer[samp_offset];
|
||||
dest = &usb_bulk_buffer[bulk_offset];
|
||||
} else {
|
||||
data_available = usb_completed - dma_started;
|
||||
space_in_use = dma_completed - sampling_completed;
|
||||
space_available = USB_SAMP_BUFFER_SIZE - space_in_use;
|
||||
samp_buf_margin = space_in_use;
|
||||
src = &usb_bulk_buffer[bulk_offset];
|
||||
dest = &usb_samp_buffer[samp_offset];
|
||||
}
|
||||
|
||||
if (data_available < size || size > space_available) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t m0_buf_half = sampling_completed & BUF_HALF_MASK;
|
||||
uint32_t dma_buf_half = dma_started & BUF_HALF_MASK;
|
||||
bool same_buf_half = m0_buf_half == dma_buf_half;
|
||||
|
||||
if (same_buf_half && samp_buf_margin >= (USB_SAMP_BUFFER_SIZE / 2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
transceiver_start_dma(src, dest, size);
|
||||
|
||||
dma_started += size;
|
||||
}
|
||||
|
||||
void start_usb_if_possible(direction_t direction) {
|
||||
uint32_t bulk_offset = usb_started & USB_BULK_BUFFER_MASK;
|
||||
uint32_t dma_completed = m0_state.m4_count;
|
||||
uint32_t bytes_available;
|
||||
usb_endpoint_t* usb_endpoint;
|
||||
|
||||
if (direction == DIRECTION_RX) {
|
||||
bytes_available = dma_completed - usb_started;
|
||||
usb_endpoint = &usb_endpoint_bulk_in;
|
||||
} else {
|
||||
uint32_t space_used = usb_started - dma_completed;
|
||||
bytes_available = USB_BULK_BUFFER_SIZE - space_used;
|
||||
usb_endpoint = &usb_endpoint_bulk_out;
|
||||
}
|
||||
|
||||
if (bytes_available < USB_TRANSFER_SIZE) {
|
||||
return;
|
||||
}
|
||||
|
||||
usb_transfer_schedule_block(
|
||||
usb_endpoint,
|
||||
&usb_bulk_buffer[bulk_offset],
|
||||
USB_TRANSFER_SIZE,
|
||||
transceiver_bulk_transfer_complete,
|
||||
NULL);
|
||||
|
||||
usb_started += USB_TRANSFER_SIZE;
|
||||
}
|
||||
|
||||
int8_t saturation_buffer = 0;
|
||||
uint64_t saturation_buffer_time = 0;
|
||||
volatile uint64_t systick_counter = 0;
|
||||
|
||||
void sys_tick_handler(void) {
|
||||
systick_counter++;
|
||||
}
|
||||
|
||||
void rx_mode(uint32_t seq) {
|
||||
transceiver_startup(TRANSCEIVER_MODE_RX);
|
||||
|
||||
baseband_streaming_enable(&sgpio_config);
|
||||
|
||||
while (transceiver_request.seq == seq) {
|
||||
start_dma_if_possible(DIRECTION_RX, DMA_TRANSFER_SIZE);
|
||||
start_usb_if_possible(DIRECTION_RX);
|
||||
|
||||
int8_t sample_value = *(
|
||||
int8_t*)&usb_samp_buffer[m0_state.m0_count & USB_SAMP_BUFFER_MASK];
|
||||
|
||||
if (sample_value > saturation_buffer)
|
||||
saturation_buffer = sample_value;
|
||||
|
||||
if (-sample_value > saturation_buffer)
|
||||
saturation_buffer = -sample_value;
|
||||
|
||||
if (saturation_buffer_time + 4 < systick_counter) {
|
||||
saturation_buffer_time = systick_counter;
|
||||
hackrf_ui()->set_saturation(saturation_buffer);
|
||||
saturation_buffer = 0;
|
||||
}
|
||||
|
||||
radio_update(&radio);
|
||||
}
|
||||
|
||||
transceiver_shutdown();
|
||||
}
|
||||
|
||||
void tx_mode(uint32_t seq) {
|
||||
transceiver_startup(TRANSCEIVER_MODE_TX);
|
||||
|
||||
// First, make transfers directly into the sample buffer to fill it.
|
||||
for (int i = 0; i < (USB_SAMP_BUFFER_SIZE / USB_TRANSFER_SIZE); i++) {
|
||||
// Set up transfer.
|
||||
usb_transfer_schedule_block(
|
||||
&usb_endpoint_bulk_out,
|
||||
&usb_samp_buffer[usb_started],
|
||||
USB_TRANSFER_SIZE,
|
||||
transceiver_bulk_transfer_complete,
|
||||
NULL);
|
||||
usb_started += USB_TRANSFER_SIZE;
|
||||
|
||||
// Wait for the transfer to complete.
|
||||
while (usb_completed < usb_started) {
|
||||
// Handle the host switching modes before filling the buffer.
|
||||
if (transceiver_request.seq != seq) {
|
||||
transceiver_shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
radio_update(&radio);
|
||||
}
|
||||
}
|
||||
|
||||
// Sample buffer is now full. Update DMA counters accordingly.
|
||||
dma_started = USB_SAMP_BUFFER_SIZE;
|
||||
m0_state.m4_count = USB_SAMP_BUFFER_SIZE;
|
||||
|
||||
// Start transmitting samples.
|
||||
baseband_streaming_enable(&sgpio_config);
|
||||
|
||||
// Continue feeding samples to the sample buffer.
|
||||
while (transceiver_request.seq == seq) {
|
||||
start_dma_if_possible(DIRECTION_TX, DMA_TRANSFER_SIZE);
|
||||
start_usb_if_possible(DIRECTION_TX);
|
||||
radio_update(&radio);
|
||||
}
|
||||
|
||||
// Host has now requested to stop TX. If we're not auto-flushing, we
|
||||
// should now stop TX immediately.
|
||||
|
||||
if (!auto_tx_flush) {
|
||||
transceiver_shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, we should now ensure all bytes sent by the host are
|
||||
// transmitted before we leave TX. First, we should make sure all data
|
||||
// currently in the USB bulk buffer reaches the sample buffer.
|
||||
|
||||
if ((usb_started - usb_completed) > 0) {
|
||||
// We were part way through a 16KB firmware-side transfer when
|
||||
// the transceiver mode change request to stop TX was received.
|
||||
//
|
||||
// We want to include the contents of that partial transfer in
|
||||
// the data we move to the sample buffer.
|
||||
//
|
||||
// The transfer was already stopped by usb_endpoint_flush(),
|
||||
// which was called from request_transceiver_mode().
|
||||
//
|
||||
// We will not have had a callback, and the transfer descriptor
|
||||
// (dTD) will not have been updated, since the transfer did not
|
||||
// complete.
|
||||
//
|
||||
// However, as long as we haven't started a new transfer, we
|
||||
// can retrieve the partial byte count from the transfer
|
||||
// overlay in the endpoint queue head (dQH) (UM10503 25.9.1).
|
||||
|
||||
usb_queue_head_t* const qh =
|
||||
usb_queue_head(usb_endpoint_bulk_out.address);
|
||||
unsigned int bytes_remaining =
|
||||
(qh->total_bytes & USB_TD_DTD_TOKEN_TOTAL_BYTES_MASK) >>
|
||||
USB_TD_DTD_TOKEN_TOTAL_BYTES_SHIFT;
|
||||
unsigned int bytes_transferred = USB_TRANSFER_SIZE - bytes_remaining;
|
||||
usb_completed += bytes_transferred;
|
||||
}
|
||||
|
||||
// Feed the remaining data from the bulk buffer to the sample buffer.
|
||||
// At this point, we also need to handle the case where there is less data
|
||||
// to be transferred to the sample buffer than a full-sized DMA transfer.
|
||||
|
||||
// Any remainder of less than 4 bytes will be ignored; this is the chunk
|
||||
// size of our DMA transfers.
|
||||
while ((usb_completed - m0_state.m4_count) >= 4) {
|
||||
uint32_t data_available = usb_completed - dma_started;
|
||||
if (data_available > DMA_TRANSFER_SIZE) {
|
||||
start_dma_if_possible(DIRECTION_TX, DMA_TRANSFER_SIZE);
|
||||
} else {
|
||||
start_dma_if_possible(DIRECTION_TX, data_available);
|
||||
}
|
||||
radio_update(&radio);
|
||||
}
|
||||
|
||||
// Wait for the data in the sample buffer to be transmitted.
|
||||
|
||||
// Any remainder of less than 32 bytes will be ignored; this is
|
||||
// the chunk size used by the M0 core to transfer samples to SGPIO.
|
||||
while ((m0_state.m4_count - m0_state.m0_count) >= 32) {
|
||||
radio_update(&radio);
|
||||
}
|
||||
|
||||
// All data received from the host has now been transmitted.
|
||||
// Now we can ACK the control request that took us out of TX mode.
|
||||
usb_transfer_schedule_ack(usb_endpoint_control_in.in);
|
||||
|
||||
transceiver_shutdown();
|
||||
}
|
||||
|
||||
void off_mode(uint32_t seq) {
|
||||
while (transceiver_request.seq == seq) {
|
||||
radio_update(&radio);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 Great Scott Gadgets <info@greatscottgadgets.com>
|
||||
* Copyright 2012-2026 Great Scott Gadgets <info@greatscottgadgets.com>
|
||||
* Copyright 2012 Jared Boone
|
||||
*
|
||||
* This file is part of HackRF.
|
||||
@@ -20,22 +20,13 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "usb_type.h"
|
||||
#include "usb_descriptor.h"
|
||||
|
||||
#define USB_VENDOR_ID (0x0781) /* SanDisk Corp. */
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef HACKRF_ONE
|
||||
#define USB_PRODUCT_ID (0xa7a8) /* SD card reader */
|
||||
#elif JAWBREAKER
|
||||
#define USB_PRODUCT_ID (0x604B)
|
||||
#elif RAD1O
|
||||
#define USB_PRODUCT_ID (0xCC15)
|
||||
#else
|
||||
#define USB_PRODUCT_ID (0xFFFF)
|
||||
#endif
|
||||
#include <usb_type.h>
|
||||
|
||||
#define USB_VENDOR_ID (0x0781) /* SanDisk Corp. */
|
||||
|
||||
#define USB_API_VERSION (0x0127) /* hardware revision */
|
||||
|
||||
@@ -50,7 +41,8 @@
|
||||
|
||||
#define USB_STRING_LANGID (0x0409)
|
||||
|
||||
uint8_t usb_descriptor_device[] = {
|
||||
#if defined(IS_HACKRF_ONE) || defined(IS_PRALINE)
|
||||
uint8_t usb_descriptor_device_hackrf[] = {
|
||||
18, // bLength
|
||||
USB_DESCRIPTOR_TYPE_DEVICE, // bDescriptorType
|
||||
USB_WORD(0x0200), // bcdUSB
|
||||
@@ -59,13 +51,50 @@ uint8_t usb_descriptor_device[] = {
|
||||
0x00, // bDeviceProtocol
|
||||
USB_MAX_PACKET0, // bMaxPacketSize0
|
||||
USB_WORD(USB_VENDOR_ID), // idVendor
|
||||
USB_WORD(USB_PRODUCT_ID), // idProduct
|
||||
USB_WORD(0xa7a8), // idProduct /* SD card reader */
|
||||
USB_WORD(USB_API_VERSION), // bcdDevice
|
||||
0x01, // iManufacturer
|
||||
0x02, // iProduct
|
||||
0x04, // iSerialNumber
|
||||
0x01 // bNumConfigurations
|
||||
};
|
||||
#endif
|
||||
#ifdef IS_JAWBREAKER
|
||||
uint8_t usb_descriptor_device_jawbreaker[] = {
|
||||
18, // bLength
|
||||
USB_DESCRIPTOR_TYPE_DEVICE, // bDescriptorType
|
||||
USB_WORD(0x0200), // bcdUSB
|
||||
0x00, // bDeviceClass
|
||||
0x00, // bDeviceSubClass
|
||||
0x00, // bDeviceProtocol
|
||||
USB_MAX_PACKET0, // bMaxPacketSize0
|
||||
USB_WORD(USB_VENDOR_ID), // idVendor
|
||||
USB_WORD(0x604B), // idProduct
|
||||
USB_WORD(USB_API_VERSION), // bcdDevice
|
||||
0x01, // iManufacturer
|
||||
0x02, // iProduct
|
||||
0x04, // iSerialNumber
|
||||
0x01 // bNumConfigurations
|
||||
};
|
||||
#endif
|
||||
#ifdef IS_RAD1O
|
||||
uint8_t usb_descriptor_device_rad1o[] = {
|
||||
18, // bLength
|
||||
USB_DESCRIPTOR_TYPE_DEVICE, // bDescriptorType
|
||||
USB_WORD(0x0200), // bcdUSB
|
||||
0x00, // bDeviceClass
|
||||
0x00, // bDeviceSubClass
|
||||
0x00, // bDeviceProtocol
|
||||
USB_MAX_PACKET0, // bMaxPacketSize0
|
||||
USB_WORD(USB_VENDOR_ID), // idVendor
|
||||
USB_WORD(0xCC15), // idProduct
|
||||
USB_WORD(USB_API_VERSION), // bcdDevice
|
||||
0x01, // iManufacturer
|
||||
0x02, // iProduct
|
||||
0x04, // iSerialNumber
|
||||
0x01 // bNumConfigurations
|
||||
};
|
||||
#endif
|
||||
|
||||
uint8_t usb_descriptor_device_qualifier[] = {
|
||||
10, // bLength
|
||||
@@ -161,162 +190,213 @@ uint8_t usb_descriptor_string_languages[] = {
|
||||
|
||||
// clang-format off
|
||||
uint8_t usb_descriptor_string_manufacturer[] = {
|
||||
40, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'G', 0x00,
|
||||
'r', 0x00,
|
||||
'e', 0x00,
|
||||
'a', 0x00,
|
||||
't', 0x00,
|
||||
' ', 0x00,
|
||||
'S', 0x00,
|
||||
'c', 0x00,
|
||||
'o', 0x00,
|
||||
't', 0x00,
|
||||
't', 0x00,
|
||||
' ', 0x00,
|
||||
'G', 0x00,
|
||||
'a', 0x00,
|
||||
'd', 0x00,
|
||||
'g', 0x00,
|
||||
'e', 0x00,
|
||||
't', 0x00,
|
||||
's', 0x00,
|
||||
40, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'G', 0x00,
|
||||
'r', 0x00,
|
||||
'e', 0x00,
|
||||
'a', 0x00,
|
||||
't', 0x00,
|
||||
' ', 0x00,
|
||||
'S', 0x00,
|
||||
'c', 0x00,
|
||||
'o', 0x00,
|
||||
't', 0x00,
|
||||
't', 0x00,
|
||||
' ', 0x00,
|
||||
'G', 0x00,
|
||||
'a', 0x00,
|
||||
'd', 0x00,
|
||||
'g', 0x00,
|
||||
'e', 0x00,
|
||||
't', 0x00,
|
||||
's', 0x00,
|
||||
};
|
||||
|
||||
uint8_t usb_descriptor_string_product[] = {
|
||||
#ifdef HACKRF_ONE
|
||||
34, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'P', 0x00,
|
||||
'o', 0x00,
|
||||
'r', 0x00,
|
||||
't', 0x00,
|
||||
'a', 0x00,
|
||||
'P', 0x00,
|
||||
'a', 0x00,
|
||||
'c', 0x00,
|
||||
'k', 0x00,
|
||||
' ', 0x00,
|
||||
#ifdef IS_HACKRF_ONE
|
||||
uint8_t usb_descriptor_string_product_hackrf_one[] = {
|
||||
34, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'P', 0x00,
|
||||
'o', 0x00,
|
||||
'r', 0x00,
|
||||
't', 0x00,
|
||||
'a', 0x00,
|
||||
'P', 0x00,
|
||||
'a', 0x00,
|
||||
'c', 0x00,
|
||||
'k', 0x00,
|
||||
' ', 0x00,
|
||||
|
||||
'M', 0x00,
|
||||
'a', 0x00,
|
||||
'y', 0x00,
|
||||
'h', 0x00,
|
||||
'e', 0x00,
|
||||
'm', 0x00,
|
||||
|
||||
#elif JAWBREAKER
|
||||
36, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'H', 0x00,
|
||||
'a', 0x00,
|
||||
'c', 0x00,
|
||||
'k', 0x00,
|
||||
'R', 0x00,
|
||||
'F', 0x00,
|
||||
' ', 0x00,
|
||||
'J', 0x00,
|
||||
'a', 0x00,
|
||||
'w', 0x00,
|
||||
'b', 0x00,
|
||||
'r', 0x00,
|
||||
'e', 0x00,
|
||||
'a', 0x00,
|
||||
'k', 0x00,
|
||||
'e', 0x00,
|
||||
'r', 0x00,
|
||||
#elif RAD1O
|
||||
12, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'r', 0x00,
|
||||
'a', 0x00,
|
||||
'd', 0x00,
|
||||
'1', 0x00,
|
||||
'o', 0x00,
|
||||
#else
|
||||
14, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'H', 0x00,
|
||||
'a', 0x00,
|
||||
'c', 0x00,
|
||||
'k', 0x00,
|
||||
'R', 0x00,
|
||||
'F', 0x00,
|
||||
'M', 0x00,
|
||||
'a', 0x00,
|
||||
'y', 0x00,
|
||||
'h', 0x00,
|
||||
'e', 0x00,
|
||||
'm', 0x00,
|
||||
};
|
||||
#endif
|
||||
#ifdef IS_PRALINE
|
||||
uint8_t usb_descriptor_string_product_praline[] = {
|
||||
34, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'P', 0x00,
|
||||
'o', 0x00,
|
||||
'r', 0x00,
|
||||
't', 0x00,
|
||||
'a', 0x00,
|
||||
'P', 0x00,
|
||||
'a', 0x00,
|
||||
'c', 0x00,
|
||||
'k', 0x00,
|
||||
' ', 0x00,
|
||||
|
||||
'M', 0x00,
|
||||
'a', 0x00,
|
||||
'y', 0x00,
|
||||
'h', 0x00,
|
||||
'e', 0x00,
|
||||
'm', 0x00,
|
||||
};
|
||||
#endif
|
||||
#ifdef IS_JAWBREAKER
|
||||
uint8_t usb_descriptor_string_product_jawbreaker[] = {
|
||||
36, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'H', 0x00,
|
||||
'a', 0x00,
|
||||
'c', 0x00,
|
||||
'k', 0x00,
|
||||
'R', 0x00,
|
||||
'F', 0x00,
|
||||
' ', 0x00,
|
||||
'J', 0x00,
|
||||
'a', 0x00,
|
||||
'w', 0x00,
|
||||
'b', 0x00,
|
||||
'r', 0x00,
|
||||
'e', 0x00,
|
||||
'a', 0x00,
|
||||
'k', 0x00,
|
||||
'e', 0x00,
|
||||
'r', 0x00,
|
||||
};
|
||||
#endif
|
||||
#ifdef IS_RAD1O
|
||||
uint8_t usb_descriptor_string_product_rad1o[] = {
|
||||
12, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'r', 0x00,
|
||||
'a', 0x00,
|
||||
'd', 0x00,
|
||||
'1', 0x00,
|
||||
'o', 0x00,
|
||||
};
|
||||
#endif
|
||||
|
||||
uint8_t usb_descriptor_string_config_description[] = {
|
||||
24, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'T', 0x00,
|
||||
'r', 0x00,
|
||||
'a', 0x00,
|
||||
'n', 0x00,
|
||||
's', 0x00,
|
||||
'c', 0x00,
|
||||
'e', 0x00,
|
||||
'i', 0x00,
|
||||
'v', 0x00,
|
||||
'e', 0x00,
|
||||
'r', 0x00,
|
||||
24, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'T', 0x00,
|
||||
'r', 0x00,
|
||||
'a', 0x00,
|
||||
'n', 0x00,
|
||||
's', 0x00,
|
||||
'c', 0x00,
|
||||
'e', 0x00,
|
||||
'i', 0x00,
|
||||
'v', 0x00,
|
||||
'e', 0x00,
|
||||
'r', 0x00,
|
||||
};
|
||||
|
||||
#ifdef DFU_MODE
|
||||
uint8_t usb_descriptor_string_serial_number[] = {
|
||||
30, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'R', 0x00,
|
||||
'u', 0x00,
|
||||
'n', 0x00,
|
||||
'n', 0x00,
|
||||
'i', 0x00,
|
||||
'n', 0x00,
|
||||
'g', 0x00,
|
||||
'F', 0x00,
|
||||
'r', 0x00,
|
||||
'o', 0x00,
|
||||
'm', 0x00,
|
||||
'R', 0x00,
|
||||
'A', 0x00,
|
||||
'M', 0x00,
|
||||
30, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'R', 0x00,
|
||||
'u', 0x00,
|
||||
'n', 0x00,
|
||||
'n', 0x00,
|
||||
'i', 0x00,
|
||||
'n', 0x00,
|
||||
'g', 0x00,
|
||||
'F', 0x00,
|
||||
'r', 0x00,
|
||||
'o', 0x00,
|
||||
'm', 0x00,
|
||||
'R', 0x00,
|
||||
'A', 0x00,
|
||||
'M', 0x00,
|
||||
};
|
||||
#else
|
||||
uint8_t usb_descriptor_string_serial_number[USB_DESCRIPTOR_STRING_SERIAL_BUF_LEN];
|
||||
#endif
|
||||
|
||||
uint8_t* usb_descriptor_strings[] = {
|
||||
usb_descriptor_string_languages,
|
||||
usb_descriptor_string_manufacturer,
|
||||
usb_descriptor_string_product,
|
||||
usb_descriptor_string_config_description,
|
||||
usb_descriptor_string_serial_number,
|
||||
0, // TERMINATOR
|
||||
#ifdef IS_HACKRF_ONE
|
||||
uint8_t* usb_descriptor_strings_hackrf_one[] = {
|
||||
usb_descriptor_string_languages,
|
||||
usb_descriptor_string_manufacturer,
|
||||
usb_descriptor_string_product_hackrf_one,
|
||||
usb_descriptor_string_config_description,
|
||||
usb_descriptor_string_serial_number,
|
||||
0, // TERMINATOR
|
||||
};
|
||||
#endif
|
||||
#ifdef IS_PRALINE
|
||||
uint8_t* usb_descriptor_strings_praline[] = {
|
||||
usb_descriptor_string_languages,
|
||||
usb_descriptor_string_manufacturer,
|
||||
usb_descriptor_string_product_praline,
|
||||
usb_descriptor_string_config_description,
|
||||
usb_descriptor_string_serial_number,
|
||||
0, // TERMINATOR
|
||||
};
|
||||
#endif
|
||||
#ifdef IS_JAWBREAKER
|
||||
uint8_t* usb_descriptor_strings_jawbreaker[] = {
|
||||
usb_descriptor_string_languages,
|
||||
usb_descriptor_string_manufacturer,
|
||||
usb_descriptor_string_product_jawbreaker,
|
||||
usb_descriptor_string_config_description,
|
||||
usb_descriptor_string_serial_number,
|
||||
0, // TERMINATOR
|
||||
};
|
||||
#endif
|
||||
#ifdef IS_RAD1O
|
||||
uint8_t* usb_descriptor_strings_rad1o[] = {
|
||||
usb_descriptor_string_languages,
|
||||
usb_descriptor_string_manufacturer,
|
||||
usb_descriptor_string_product_rad1o,
|
||||
usb_descriptor_string_config_description,
|
||||
usb_descriptor_string_serial_number,
|
||||
0, // TERMINATOR
|
||||
};
|
||||
#endif
|
||||
|
||||
uint8_t wcid_string_descriptor[] = {
|
||||
18, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'M', 0x00,
|
||||
'S', 0x00,
|
||||
'F', 0x00,
|
||||
'T', 0x00,
|
||||
'1', 0x00,
|
||||
'0', 0x00,
|
||||
'0', 0x00,
|
||||
USB_WCID_VENDOR_REQ, // vendor request code for further descriptor
|
||||
0x00
|
||||
18, // bLength
|
||||
USB_DESCRIPTOR_TYPE_STRING, // bDescriptorType
|
||||
'M', 0x00,
|
||||
'S', 0x00,
|
||||
'F', 0x00,
|
||||
'T', 0x00,
|
||||
'1', 0x00,
|
||||
'0', 0x00,
|
||||
'0', 0x00,
|
||||
USB_WCID_VENDOR_REQ, // vendor request code for further descriptor
|
||||
0x00
|
||||
};
|
||||
|
||||
uint8_t wcid_feature_descriptor[] = {
|
||||
0x28, 0x00, 0x00, 0x00, // bLength
|
||||
USB_WORD(0x0100), // WCID version
|
||||
USB_WORD(0x0004), // WICD descriptor index
|
||||
0x01, // bNumSections
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00, // Reserved
|
||||
0x00, // bInterfaceNumber
|
||||
0x01, // Reserved
|
||||
'W', 'I', 'N', 'U', 'S', 'B', 0x00,0x00, // Compatible ID, padded with zeros
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // Sub-compatible ID
|
||||
0x00,0x00,0x00,0x00,0x00,0x00 // Reserved
|
||||
};
|
||||
0x28, 0x00, 0x00, 0x00, // bLength
|
||||
USB_WORD(0x0100), // WCID version
|
||||
USB_WORD(0x0004), // WICD descriptor index
|
||||
0x01, // bNumSections
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00, // Reserved
|
||||
0x00, // bInterfaceNumber
|
||||
0x01, // Reserved
|
||||
'W', 'I', 'N', 'U', 'S', 'B', 0x00,0x00, // Compatible ID, padded with zeros
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, // Sub-compatible ID
|
||||
0x00,0x00,0x00,0x00,0x00,0x00 // Reserved
|
||||
};
|
||||
@@ -20,22 +20,46 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
extern uint8_t usb_descriptor_device[];
|
||||
#if defined(IS_HACKRF_ONE) || defined(IS_PRALINE)
|
||||
extern uint8_t usb_descriptor_device_hackrf[];
|
||||
#endif
|
||||
#ifdef IS_JAWBREAKER
|
||||
extern uint8_t usb_descriptor_device_jawbreaker[];
|
||||
#endif
|
||||
#ifdef IS_RAD1O
|
||||
extern uint8_t usb_descriptor_device_rad1o[];
|
||||
#endif
|
||||
extern uint8_t usb_descriptor_device_qualifier[];
|
||||
extern uint8_t usb_descriptor_configuration_full_speed[];
|
||||
extern uint8_t usb_descriptor_configuration_high_speed[];
|
||||
extern uint8_t usb_descriptor_string_languages[];
|
||||
extern uint8_t usb_descriptor_string_manufacturer[];
|
||||
extern uint8_t usb_descriptor_string_product[];
|
||||
#ifdef IS_HACKRF_ONE
|
||||
extern uint8_t usb_descriptor_string_product_hackrf_one[];
|
||||
#endif
|
||||
#ifdef IS_PRALINE
|
||||
extern uint8_t usb_descriptor_string_product_praline[];
|
||||
#endif
|
||||
#ifdef IS_JAWBREAKER
|
||||
extern uint8_t usb_descriptor_string_product_jawbreaker[];
|
||||
#endif
|
||||
#ifdef IS_RAD1O
|
||||
extern uint8_t usb_descriptor_string_product_rad1o[];
|
||||
#endif
|
||||
|
||||
#define USB_DESCRIPTOR_STRING_SERIAL_LEN 32
|
||||
#define USB_DESCRIPTOR_STRING_SERIAL_BUF_LEN \
|
||||
(USB_DESCRIPTOR_STRING_SERIAL_LEN * 2 + 2) /* UTF-16LE */
|
||||
extern uint8_t usb_descriptor_string_serial_number[];
|
||||
|
||||
extern uint8_t* usb_descriptor_strings[];
|
||||
extern uint8_t* usb_descriptor_strings_hackrf_one[];
|
||||
extern uint8_t* usb_descriptor_strings_jawbreaker[];
|
||||
extern uint8_t* usb_descriptor_strings_rad1o[];
|
||||
extern uint8_t* usb_descriptor_strings_praline[];
|
||||
|
||||
#define USB_WCID_VENDOR_REQ 0x19
|
||||
extern uint8_t wcid_string_descriptor[];
|
||||
|
||||
@@ -73,21 +73,34 @@ void SpectrumCollector::set_decimation_factor(
|
||||
* perform the deferred task on the buffer of data we prepared.
|
||||
*/
|
||||
|
||||
void SpectrumCollector::feed(
|
||||
bool SpectrumCollector::feed(
|
||||
const buffer_c16_t& channel,
|
||||
const int32_t filter_low_frequency,
|
||||
const int32_t filter_high_frequency,
|
||||
const int32_t filter_transition) {
|
||||
// Called from baseband processing thread.
|
||||
set_filter(
|
||||
filter_low_frequency,
|
||||
filter_high_frequency,
|
||||
filter_transition);
|
||||
|
||||
bool block_completed = false;
|
||||
channel_spectrum_decimator.feed(
|
||||
channel,
|
||||
[this, &block_completed](const buffer_c16_t& data) {
|
||||
this->post_message(data);
|
||||
block_completed = true;
|
||||
});
|
||||
return block_completed;
|
||||
}
|
||||
|
||||
void SpectrumCollector::set_filter(
|
||||
const int32_t filter_low_frequency,
|
||||
const int32_t filter_high_frequency,
|
||||
const int32_t filter_transition) {
|
||||
channel_filter_low_frequency = filter_low_frequency;
|
||||
channel_filter_high_frequency = filter_high_frequency;
|
||||
channel_filter_transition = filter_transition;
|
||||
|
||||
channel_spectrum_decimator.feed(
|
||||
channel,
|
||||
[this](const buffer_c16_t& data) {
|
||||
this->post_message(data);
|
||||
});
|
||||
}
|
||||
|
||||
void SpectrumCollector::post_message(const buffer_c16_t& data) {
|
||||
@@ -136,6 +149,7 @@ void SpectrumCollector::update() {
|
||||
|
||||
ChannelSpectrum spectrum;
|
||||
spectrum.sampling_rate = channel_spectrum_sampling_rate;
|
||||
spectrum.channel_filter_offset = channel_filter_offset;
|
||||
spectrum.channel_filter_low_frequency = channel_filter_low_frequency;
|
||||
spectrum.channel_filter_high_frequency = channel_filter_high_frequency;
|
||||
spectrum.channel_filter_transition = channel_filter_transition;
|
||||
|
||||
@@ -40,13 +40,24 @@ class SpectrumCollector {
|
||||
void on_message(const Message* const message);
|
||||
|
||||
void set_decimation_factor(const size_t decimation_factor);
|
||||
void set_channel_filter_offset(const int32_t offset) {
|
||||
channel_filter_offset = offset;
|
||||
}
|
||||
|
||||
void feed(
|
||||
bool feed(
|
||||
const buffer_c16_t& channel,
|
||||
const int32_t filter_low_frequency,
|
||||
const int32_t filter_high_frequency,
|
||||
const int32_t filter_transition);
|
||||
|
||||
protected:
|
||||
bool is_streaming() const { return streaming; }
|
||||
void set_filter(
|
||||
const int32_t filter_low_frequency,
|
||||
const int32_t filter_high_frequency,
|
||||
const int32_t filter_transition);
|
||||
void post_message(const buffer_c16_t& data);
|
||||
|
||||
private:
|
||||
BlockDecimator<complex16_t, 256> channel_spectrum_decimator{1};
|
||||
ChannelSpectrum fifo_data[1 << ChannelSpectrumConfigMessage::fifo_k]{};
|
||||
@@ -59,8 +70,7 @@ class SpectrumCollector {
|
||||
int32_t channel_filter_low_frequency{0};
|
||||
int32_t channel_filter_high_frequency{0};
|
||||
int32_t channel_filter_transition{0};
|
||||
|
||||
void post_message(const buffer_c16_t& data);
|
||||
int32_t channel_filter_offset{0};
|
||||
|
||||
void set_state(const SpectrumStreamingConfigMessage& message);
|
||||
void start();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user