From bc0908605a9ed3407aa7bbc32699c5c77a4f6e11 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Sat, 28 Feb 2026 12:03:38 +0100 Subject: [PATCH] Audible harmonic --- main.c | 1 + nanovna.h | 1 + sa_core.c | 163 ++++++++++++++++++++++++++++++++++-------------------- ui.c | 16 ++++++ 4 files changed, 120 insertions(+), 61 deletions(-) diff --git a/main.c b/main.c index dce42b1..76614b5 100644 --- a/main.c +++ b/main.c @@ -1259,6 +1259,7 @@ config_t config = { .direct_stop = 985000000UL, .overclock = 0, .hide_21MHz = false, + .wfm_1khz_harmonic = 0, // Disabled by default; 0=off, 1-100=amplitude percentage for ~1kHz harmonic #endif #ifdef __USE_SD_CARD__ .sd_icon_save = SDIS_DEFAULT, diff --git a/nanovna.h b/nanovna.h index 46d7256..5827db7 100644 --- a/nanovna.h +++ b/nanovna.h @@ -893,6 +893,7 @@ typedef struct config { #ifdef TINYSA4 uint8_t hide_21MHz; uint8_t no_audio_agc; + uint8_t wfm_1khz_harmonic; // 0=off, 1-100=amplitude percentage for adding ~1kHz harmonic to low freq WFM #endif #ifdef __USE_SD_CARD__ uint8_t sd_icon_save; // enum diff --git a/sa_core.c b/sa_core.c index f8d2aeb..7280a66 100644 --- a/sa_core.c +++ b/sa_core.c @@ -71,7 +71,7 @@ freq_t frequency_step_x10 = 0; uint16_t vbwSteps = 1; freq_t minFreq = 0; freq_t maxFreq = 520000000; -static float old_a = -150; // cached value to reduce writes to level registers +static float prev_output_level_dBm = -150; // cached output level to reduce redundant hardware writes int spur_gate = 100; #ifdef __BANDS__ @@ -3672,7 +3672,7 @@ pureRSSI_t perform(bool break_on_operation, int i, freq_t f, int tracking) / set_calibration_freq(setting.refer); update_rbw(); calculate_step_delay(); - old_a = -150; // clear cached level setting + prev_output_level_dBm = -150; // clear cached level setting // Initialize HW scandirty = true; // This is the first pass with new settings for (int t=0;t>= 1; } if (modulation_steps >= 4) { + int hn = (config.wfm_1khz_harmonic > 0 && setting.modulation_frequency < 250.0) ? (int)((1000.0 / setting.modulation_frequency) + 0.5) : 0; for (int i = 0; i < modulation_steps/4+1; i++) { fm_modulation[i] = setting.modulation_deviation_div100 * sine_wave[i*sine_wave_index]/100; fm_modulation[modulation_steps/2 - i] = fm_modulation[i]; fm_modulation[modulation_steps/2 + i] = -fm_modulation[i]; fm_modulation[modulation_steps - i] = -fm_modulation[i]; } + if (hn) { + for (int i = 0; i < modulation_steps; i++) { + int table_index = (i*sine_wave_index*hn) % MAX_MODULATION_STEPS; + int mul = 1; + if (table_index > MAX_MODULATION_STEPS/2) { + mul = -1; + table_index = MAX_MODULATION_STEPS - table_index; + } + if (table_index > MAX_MODULATION_STEPS/4) + table_index = MAX_MODULATION_STEPS/2 - table_index; + int v = setting.modulation_deviation_div100 * sine_wave[table_index] * mul / 100; + fm_modulation[i] += v; + } + } } else { modulation_steps = 2; fm_modulation[0] = (setting.modulation_deviation_div100*347)/100; @@ -3847,71 +3862,91 @@ pureRSSI_t perform(bool break_on_operation, int i, freq_t f, int tracking) / } else #endif - if (setting.mode == M_GENLOW) {// if in low output mode and level sweep or frequency weep is active or at start of sweep - float ls=setting.level_sweep; // calculate and set the output level - if (ls > 0) - ls += 0.5; - else if (ls < 0) - ls -= 0.5; - float a = ((int)((setting.level + ((float)i / sweep_points) * ls)*2.0)) / 2.0 /* + get_level_offset() */ ; -#ifdef TINYSA4 - set_output_path(f, a); + if (setting.mode == M_GENLOW) { // Low output mode: calculate and set output level + // Calculate level sweep amount with rounding + float level_sweep_amount = setting.level_sweep; + if (level_sweep_amount > 0) + level_sweep_amount += 0.5; + else if (level_sweep_amount < 0) + level_sweep_amount -= 0.5; + + // Calculate target output level including sweep progression (rounded to 0.5 dB steps) + float target_output_level_dBm = ((int)((setting.level + ((float)i / sweep_points) * level_sweep_amount)*2.0)) / 2.0; +#ifdef TINYSA4 + set_output_path(f, target_output_level_dBm); #else - int d; + int drive_index; #if 0 if (force_signal_path) { setting.atten_step = test_output_switch; - d = test_output_drive; + drive_index = test_output_drive; setting.attenuate_x2 = test_output_attenuate; goto set_path; } #endif + // Apply frequency-dependent correction correct_RSSI_freq = get_frequency_correction(f); - a += PURE_TO_float(correct_RSSI_freq); - if (a != old_a) { - old_a = a; - a = a - level_max(); // convert to all settings maximum power output equals a = zero - if (a < -SWITCH_ATTENUATION) { - a = a + SWITCH_ATTENUATION; - setting.atten_step = true; + target_output_level_dBm += PURE_TO_float(correct_RSSI_freq); + + // Only update hardware if level changed (avoids unnecessary register writes) + if (target_output_level_dBm != prev_output_level_dBm) { + prev_output_level_dBm = target_output_level_dBm; + + // Convert to relative level (0 = maximum power output) + float level_relative_to_max_dBm = target_output_level_dBm - level_max(); + + // Determine if path switching is needed for large attenuation + if (level_relative_to_max_dBm < -SWITCH_ATTENUATION) { + level_relative_to_max_dBm += SWITCH_ATTENUATION; + setting.atten_step = true; // Use attenuated signal path } else { - setting.atten_step = false; + setting.atten_step = false; // Use direct signal path } + #define LOWEST_LEVEL MIN_DRIVE - d = MAX_DRIVE-3; // Start in the middle - float blw = BELOW_MAX_DRIVE(d); - while (a > blw && d < MAX_DRIVE) { // Increase if needed - d++; - blw = BELOW_MAX_DRIVE(d); + // Search for optimal drive setting - start in middle range + drive_index = MAX_DRIVE-3; + float drive_offset_dBm = BELOW_MAX_DRIVE(drive_index); + + // Increase drive if current setting is too low + while (level_relative_to_max_dBm > drive_offset_dBm && drive_index < MAX_DRIVE) { + drive_index++; + drive_offset_dBm = BELOW_MAX_DRIVE(drive_index); } - while (a + 28 < blw && d > LOWEST_LEVEL) { // reduce till it fits attenuator (31 - 3) - d--; - blw = BELOW_MAX_DRIVE(d); + + // Reduce drive while it still fits in attenuator range (31 - 3 = 28 dB) + while (level_relative_to_max_dBm + 28 < drive_offset_dBm && drive_index > LOWEST_LEVEL) { + drive_index--; + drive_offset_dBm = BELOW_MAX_DRIVE(drive_index); } - a -= blw; - if (a > 0) - a = 0; - if (a < -31.5) - a = -31.5; - a = -a - 0.25; // Rounding - setting.attenuate_x2 = (int)(a * 2); + + // Calculate attenuator setting + float attenuator_level_dBm = level_relative_to_max_dBm - drive_offset_dBm; + if (attenuator_level_dBm > 0) + attenuator_level_dBm = 0; + if (attenuator_level_dBm < -31.5) + attenuator_level_dBm = -31.5; + attenuator_level_dBm = -attenuator_level_dBm - 0.25; // Rounding + setting.attenuate_x2 = (int)(attenuator_level_dBm * 2); + // set_path: - SI4432_Sel = SI4432_RX ; - SI4432_Drive(d); - SI4432_Sel = SI4432_RX ; - if ( setting.atten_step) - set_switch_receive(); + // Apply settings to hardware + SI4432_Sel = SI4432_RX; + SI4432_Drive(drive_index); + SI4432_Sel = SI4432_RX; + if (setting.atten_step) + set_switch_receive(); // Attenuated path else - set_switch_transmit(); + set_switch_transmit(); // Direct path PE4302_Write_Byte(setting.attenuate_x2); #if 0 if (debug_level && SDU1.config->usbp->state == USB_ACTIVE) - shell_printf ("level=%f, d=%d, a=%d, s=%d\r\n", setting.level, d, setting.attenuate_x2, (setting.atten_step ? 1 : 0)); + shell_printf ("level=%f, d=%d, a=%d, s=%d\r\n", setting.level, drive_index, setting.attenuate_x2, (setting.atten_step ? 1 : 0)); #endif } #endif } - else if (setting.mode == M_GENHIGH) { + else if (setting.mode == M_GENHIGH) { // High output mode: set output level #ifdef TINYSA4 if (force_signal_path) { enable_rx_output(!test_output_switch); @@ -3919,21 +3954,24 @@ pureRSSI_t perform(bool break_on_operation, int i, freq_t f, int tracking) / } else #endif { - float a = setting.level - level_max(); - if (a <= -SWITCH_ATTENUATION) { - setting.atten_step = true; - a = a + SWITCH_ATTENUATION; + // Calculate level relative to maximum output power + float level_relative_to_max_dBm = setting.level - level_max(); + + // Determine if path switching is needed for large attenuation + if (level_relative_to_max_dBm <= -SWITCH_ATTENUATION) { + setting.atten_step = true; // Use attenuated signal path + level_relative_to_max_dBm += SWITCH_ATTENUATION; #ifdef TINYSA3 - SI4432_Sel = SI4432_LO ; + SI4432_Sel = SI4432_LO; set_switch_receive(); #else enable_ADF_output(true, false); ; #endif } else { - setting.atten_step = false; + setting.atten_step = false; // Use direct signal path #ifdef TINYSA3 - SI4432_Sel = SI4432_LO ; + SI4432_Sel = SI4432_LO; set_switch_transmit(); #else enable_ADF_output(true, true); @@ -3941,24 +3979,27 @@ pureRSSI_t perform(bool break_on_operation, int i, freq_t f, int tracking) / #endif } - int d = MIN_DRIVE; - while (drive_dBm[d] - level_max() < a && d < MAX_DRIVE) // Find level equal or above requested level - d++; - // if (d == 8 && v < -12) // Round towards closest level - // d = 7; - setting.level = drive_dBm[d] + config.high_level_output_offset - (setting.atten_step ? SWITCH_ATTENUATION : 0); + // Find drive setting that meets or exceeds requested level + int drive_index = MIN_DRIVE; + while (drive_dBm[drive_index] - level_max() < level_relative_to_max_dBm && drive_index < MAX_DRIVE) + drive_index++; + // if (drive_index == 8 && v < -12) // Round towards closest level + // drive_index = 7; + + // Update actual output level to closest achievable value + setting.level = drive_dBm[drive_index] + config.high_level_output_offset - (setting.atten_step ? SWITCH_ATTENUATION : 0); #ifdef __SI4432__ - SI4432_Sel = SI4432_LO ; - SI4432_Drive(d); + SI4432_Sel = SI4432_LO; + SI4432_Drive(drive_index); #endif #ifdef TINYSA4 #ifndef __NEW_SWITCHES__ if (config.high_out_adf4350) - SI4463_set_output_level(d); + SI4463_set_output_level(drive_index); else #endif - // ADF4351_aux_drive(d); + // ADF4351_aux_drive(drive_index); #endif } } diff --git a/ui.c b/ui.c index a830408..46e9e61 100644 --- a/ui.c +++ b/ui.c @@ -2506,6 +2506,21 @@ static UI_FUNCTION_ADV_CALLBACK(menu_level_in_dBuV) menu_move_back(false); } +#ifdef TINYSA4 +static UI_FUNCTION_ADV_CALLBACK(menu_audible_harmonic) +{ + (void)item; + (void)data; + if (b){ + b->icon = config.wfm_1khz_harmonic ? BUTTON_ICON_CHECK : BUTTON_ICON_NOCHECK; + return; + } + config.wfm_1khz_harmonic = config.wfm_1khz_harmonic ? 0 : 100; // Toggle between off and 100% + dirty = true; +// menu_move_back(false); +} +#endif + static UI_FUNCTION_ADV_CALLBACK(menu_smodulation_acb){ (void)item; (void)data; @@ -4457,6 +4472,7 @@ static const menuitem_t menu_modulation[] = { { MT_FORM | MT_KEYPAD, KM_MODULATION, "FREQ: %s", "1Hz..3.5kHz"}, { MT_FORM | MT_KEYPAD, KM_DEPTH, "AM DEPTH: %s%%", "0..100"}, { MT_FORM | MT_KEYPAD, KM_DEVIATION, "FM DEVIATION: %s", "1kHz..300kHz"}, + { MT_FORM | MT_ADV_CALLBACK, 0, "Add Audible harmonic", menu_audible_harmonic}, // { MT_FORM | MT_ADV_CALLBACK, MO_NFM2, MT_CUSTOM_LABEL, menu_modulation_acb}, // { MT_FORM | MT_ADV_CALLBACK, MO_NFM3, MT_CUSTOM_LABEL, menu_modulation_acb}, #else