Files
uv-k1-k5v3-firmware-custom/App/app/spectrum.c
T
2026-04-21 17:26:18 +02:00

2168 lines
54 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Copyright 2023 fagci
* https://github.com/fagci
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "app/spectrum.h"
#include "am_fix.h"
#include "audio.h"
#include "misc.h"
#ifdef ENABLE_SCAN_RANGES
#include "chFrScanner.h"
#endif
#include "driver/backlight.h"
#include "frequencies.h"
#include "ui/helper.h"
#include "ui/main.h"
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
#include "screenshot.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
#include "driver/py25q16.h"
#endif
struct FrequencyBandInfo
{
uint32_t lower;
uint32_t upper;
uint32_t middle;
};
#define F_MIN frequencyBandTable[0].lower
#define F_MAX frequencyBandTable[BAND_N_ELEM - 1].upper
const uint16_t RSSI_MAX_VALUE = 65535;
static uint32_t initialFreq;
static char String[32];
static bool isInitialized = false;
bool isListening = true;
bool monitorMode = false;
bool redrawStatus = true;
bool redrawScreen = false;
bool newScanStart = true;
bool preventKeypress = true;
bool audioState = true;
bool lockAGC = false;
State currentState = SPECTRUM, previousState = SPECTRUM;
PeakInfo peak;
ScanInfo scanInfo;
static KeyboardState kbd = {KEY_INVALID, KEY_INVALID, 0};
#ifdef ENABLE_SCAN_RANGES
static uint16_t blacklistFreqs[15];
static uint8_t blacklistFreqsIdx;
#endif
const char *bwOptions[] = {"25", "12.5", "6.25"};
const uint8_t modulationTypeTuneSteps[] = {100, 50, 10};
const uint8_t modTypeReg47Values[] = {1, 7, 5};
SpectrumSettings settings = {.stepsCount = STEPS_64,
.scanStepIndex = S_STEP_25_0kHz,
.frequencyChangeStep = 80000,
.scanDelay = 3200,
.rssiTriggerLevel = 150,
.backlightState = true,
.bw = BK4819_FILTER_BW_WIDE,
.listenBw = BK4819_FILTER_BW_WIDE,
.modulationType = false,
.dbMin = -130,
.dbMax = -50};
uint32_t fMeasure = 0;
uint32_t currentFreq, tempFreq;
uint16_t rssiHistory[128];
// Peak hold: tracks the highest Y per column with timed decay
static uint8_t peakHoldY[128]; // Peak Y value per display column (0=top)
static uint8_t peakHoldAge[64]; // Shared decay timer (1 per 2 columns)
#define PEAK_HOLD_DELAY 15 // Sweeps before decay starts
#define PEAK_HOLD_INIT 0xFF // "no peak" sentinel (same as SPECTRUM_TOPY_SKIP)
// Cached REG_30 value for scan steps: avoids re-reading it on every SetFScan()
// call (saves 1 SPI read per step = fewer SPI bus events = less SPI-induced audio interference).
static uint16_t scanReg30 = 0;
// Bidirectional sweep: true = left→right (fStart→fEnd), false = right→left.
static bool scanForward = true;
// Incremental display: one framebuffer page sent per tick instead of a full
// BlitFullScreen burst.
static uint8_t renderPage = 0;
// Decoupled render timer: Render() fires every RENDER_PERIOD_TICKS ticks
// regardless of step count, keeping it above the ~9 Hz flutter-fusion
// threshold that would cause an audible "tac" if tied to the sweep rate.
static uint16_t renderTimer = 0;
#define RENDER_PERIOD_TICKS 20
// Disabling automatic DbMax and squelch trigger settings
static bool manualSetFlag = false;
// EMA-smoothed RSSI for STILL display only (peak.rssi stays raw for trigger)
static uint16_t rssiSmoothed = 0;
// Sweeps remaining before auto-scaling of dbMax resumes (0 = auto)
static uint8_t manualDbMaxTimer = 0;
#define MANUAL_DBMAX_SWEEPS 15
uint8_t vfo;
uint8_t freqInputIndex = 0;
uint8_t freqInputDotIndex = 0;
KEY_Code_t freqInputArr[10];
char freqInputString[11];
uint8_t menuState = 0;
uint16_t listenT = 0;
RegisterSpec registerSpecs[] = {
{},
{"LNAs", BK4819_REG_13, 8, 0b11, 1},
{"LNA", BK4819_REG_13, 5, 0b111, 1},
{"PGA", BK4819_REG_13, 0, 0b111, 1},
//{"BPF", BK4819_REG_3D, 0, 0xFFFF, 0x2aaa},
// {"MIX", 0x13, 3, 0b11, 1}, // TODO: hidden
};
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
const int8_t LNAsOptions[] = {-19, -16, -11, 0};
const int8_t LNAOptions[] = {-24, -19, -14, -9, -6, -4, -2, 0};
const int8_t VGAOptions[] = {-33, -27, -21, -15, -9, -6, -3, 0};
//const char *BPFOptions[] = {"8.46", "7.25", "6.35", "5.64", "5.08", "4.62", "4.23"};
typedef struct {
const int8_t *options;
uint8_t count;
} MenuOptions;
static const MenuOptions regOptions[] = {
{NULL, 0}, // NULL
{LNAsOptions, 4}, // LNAs
{LNAOptions, 8}, // LNA
{VGAOptions, 8} // VGA
};
#endif
uint16_t statuslineUpdateTimer = 0;
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
static void LoadSettings()
{
uint8_t Data[8] = {0};
PY25Q16_ReadBuffer(0x00A158, Data, sizeof(Data));
settings.scanStepIndex = ((Data[3] & 0xF0) >> 4);
if (settings.scanStepIndex > 14)
settings.scanStepIndex = S_STEP_25_0kHz;
settings.stepsCount = ((Data[3] & 0x0F) & 0b1100) >> 2;
if (settings.stepsCount > 3)
settings.stepsCount = STEPS_64;
settings.listenBw = ((Data[3] & 0x0F) & 0b0011);
if (settings.listenBw > 2)
settings.listenBw = BK4819_FILTER_BW_WIDE;
}
static void SaveSettings()
{
uint8_t Data[8] = {0};
PY25Q16_ReadBuffer(0x00A158, Data, sizeof(Data));
Data[3] = (settings.scanStepIndex << 4) | (settings.stepsCount << 2) | settings.listenBw;
PY25Q16_WriteBuffer(0x00A158, Data, sizeof(Data), false);
}
#endif
static uint8_t DBm2S(int dbm)
{
uint8_t i = 0;
dbm *= -1;
for (i = 0; i < ARRAY_SIZE(U8RssiMap); i++)
{
if (dbm >= U8RssiMap[i])
{
return i;
}
}
return i;
}
static int Rssi2DBm(uint16_t rssi)
{
return (rssi / 2) - 160 + dBmCorrTable[gRxVfo->Band];
}
static uint16_t GetRegMenuValue(uint8_t st)
{
RegisterSpec s = registerSpecs[st];
return (BK4819_ReadRegister(s.num) >> s.offset) & s.mask;
}
void LockAGC()
{
//RADIO_SetupAGC(settings.modulationType == MODULATION_AM, lockAGC);
RADIO_SetupAGC(false, lockAGC);
//lockAGC = true;
lockAGC = false;
}
static void SetRegMenuValue(uint8_t st, bool add)
{
uint16_t v = GetRegMenuValue(st);
RegisterSpec s = registerSpecs[st];
if (s.num == BK4819_REG_13)
LockAGC();
uint16_t reg = BK4819_ReadRegister(s.num);
if (add && v <= s.mask - s.inc)
{
v += s.inc;
}
else if (!add && v >= 0 + s.inc)
{
v -= s.inc;
}
// TODO: use max value for bits count in max value, or reset by additional
// mask in spec
reg &= ~(s.mask << s.offset);
BK4819_WriteRegister(s.num, reg | (v << s.offset));
redrawScreen = true;
}
// GUI functions
#ifndef ENABLE_FEAT_F4HWN
static void PutPixel(uint8_t x, uint8_t y, bool fill)
{
UI_DrawPixelBuffer(gFrameBuffer, x, y, fill);
}
static void PutPixelStatus(uint8_t x, uint8_t y, bool fill)
{
UI_DrawPixelBuffer(&gStatusLine, x, y, fill);
}
#endif
#ifndef ENABLE_FEAT_F4HWN
static void GUI_DisplaySmallest(const char *pString, uint8_t x, uint8_t y,
bool statusbar, bool fill)
{
uint8_t c;
uint8_t pixels;
const uint8_t *p = (const uint8_t *)pString;
while ((c = *p++) && c != '\0')
{
c -= 0x20;
for (int i = 0; i < 3; ++i)
{
pixels = gFont3x5[c][i];
for (int j = 0; j < 6; ++j)
{
if (pixels & 1)
{
if (statusbar)
PutPixelStatus(x + i, y + j, fill);
else
PutPixel(x + i, y + j, fill);
}
pixels >>= 1;
}
}
x += 4;
}
}
#endif
// Utility functions
static int clamp(int v, int min, int max)
{
return v <= min ? min : (v >= max ? max : v);
}
static uint8_t my_abs(signed v) { return v > 0 ? v : -v; }
void SetState(State state)
{
previousState = currentState;
currentState = state;
redrawScreen = true;
redrawStatus = true;
}
// Radio functions
static void ToggleAFBit(bool on)
{
uint16_t reg = BK4819_ReadRegister(BK4819_REG_47);
reg &= ~(1 << 8);
if (on)
reg |= on << 8;
BK4819_WriteRegister(BK4819_REG_47, reg);
}
static const BK4819_REGISTER_t registers_to_save[] = {
BK4819_REG_30,
BK4819_REG_37,
BK4819_REG_3D,
BK4819_REG_43,
BK4819_REG_47,
BK4819_REG_48,
BK4819_REG_7E,
};
static uint16_t registers_stack[ARRAY_SIZE(registers_to_save)];
static void BackupRegisters()
{
for (uint32_t i = 0; i < ARRAY_SIZE(registers_to_save); i++)
{
registers_stack[i] = BK4819_ReadRegister(registers_to_save[i]);
}
}
static void RestoreRegisters()
{
for (uint32_t i = 0; i < ARRAY_SIZE(registers_to_save); i++)
{
BK4819_WriteRegister(registers_to_save[i], registers_stack[i]);
}
#ifdef ENABLE_FEAT_F4HWN
gVfoConfigureMode = VFO_CONFIGURE;
#endif
}
static void ToggleAFDAC(bool on)
{
uint32_t Reg = BK4819_ReadRegister(BK4819_REG_30);
Reg &= ~(1 << 9);
if (on)
Reg |= (1 << 9);
BK4819_WriteRegister(BK4819_REG_30, Reg);
}
static void SetF(uint32_t f)
{
fMeasure = f;
BK4819_SetFrequency(fMeasure);
BK4819_PickRXFilterPathBasedOnFrequency(fMeasure);
uint16_t reg = BK4819_ReadRegister(BK4819_REG_30);
BK4819_WriteRegister(BK4819_REG_30, 0);
BK4819_WriteRegister(BK4819_REG_30, reg);
}
// Lightweight frequency-set used during scanning.
// Skips the band-select GPIO writes (band does not change within a sweep)
// and uses a cached REG_30 value (read once in InitScan) instead of reading
// it on every step. Reduces per-step SPI transactions from ~7 to 4,
// cutting the SPI bus activity that causes SPI-induced audio interference.
static void SetFScan(uint32_t f)
{
// Refresh RF path only when crossing the VHF/UHF boundary (280 MHz)
if ((f < 28000000) != (fMeasure < 28000000))
BK4819_PickRXFilterPathBasedOnFrequency(f);
fMeasure = f;
BK4819_SetFrequency(f);
BK4819_WriteRegister(BK4819_REG_30, 0);
BK4819_WriteRegister(BK4819_REG_30, scanReg30);
}
// Spectrum related
bool IsPeakOverLevel() { return peak.rssi >= settings.rssiTriggerLevel; }
static void ResetPeak()
{
peak.t = 0;
peak.rssi = 0;
}
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
static void setTailFoundInterrupt()
{
BK4819_WriteRegister(BK4819_REG_3F, BK4819_REG_02_CxCSS_TAIL | BK4819_REG_02_SQUELCH_FOUND);
}
static bool checkIfTailFound()
{
uint16_t interrupt_status_bits;
// if interrupt waiting to be handled
if(BK4819_ReadRegister(BK4819_REG_0C) & 1u) {
// reset the interrupt
BK4819_WriteRegister(BK4819_REG_02, 0);
// fetch the interrupt status bits
interrupt_status_bits = BK4819_ReadRegister(BK4819_REG_02);
// if tail found interrupt
if (interrupt_status_bits & BK4819_REG_02_CxCSS_TAIL)
{
listenT = 0;
// disable interrupts
BK4819_WriteRegister(BK4819_REG_3F, 0);
// reset the interrupt
BK4819_WriteRegister(BK4819_REG_02, 0);
return true;
}
}
return false;
}
#endif
bool IsCenterMode() { return settings.scanStepIndex < S_STEP_2_5kHz; }
// scan step in 0.01khz
uint16_t GetScanStep() { return scanStepValues[settings.scanStepIndex]; }
uint16_t GetStepsCount()
{
#ifdef ENABLE_SCAN_RANGES
if (gScanRangeStart)
{
uint32_t range = gScanRangeStop - gScanRangeStart;
uint16_t step = GetScanStep();
return (range / step) + 1; // +1 to include up limit
}
#endif
return 128 >> settings.stepsCount;
}
#ifdef ENABLE_SCAN_RANGES
static uint16_t GetStepsCountDisplay()
{
if (gScanRangeStart)
{
return (gScanRangeStop - gScanRangeStart) / GetScanStep();
}
return GetStepsCount();
}
#endif
uint32_t GetBW() { return GetStepsCount() * GetScanStep(); }
uint32_t GetFStart()
{
return IsCenterMode() ? currentFreq - (GetBW() >> 1) : currentFreq;
}
uint32_t GetFEnd()
{
#ifdef ENABLE_SCAN_RANGES
if (gScanRangeStart)
{
return gScanRangeStop;
}
#endif
return currentFreq + GetBW();
}
static void TuneToPeak()
{
scanInfo.f = peak.f;
scanInfo.rssi = peak.rssi;
scanInfo.i = peak.i;
SetF(scanInfo.f);
}
static void DeInitSpectrum()
{
SetF(initialFreq);
RestoreRegisters();
isInitialized = false;
}
uint8_t GetBWRegValueForScan()
{
return scanStepBWRegValues[settings.scanStepIndex];
}
uint16_t GetRssi()
{
// Wait for glitch to settle below threshold (not just < 255)
uint8_t guard = 50;
while (guard-- && (BK4819_ReadRegister(0x63) & 0xFF) >= 200)
{
SYSTICK_DelayUs(10);
}
// Discard first read (AGC may still be transitioning), keep second
BK4819_GetRSSI();
uint16_t rssi = BK4819_GetRSSI();
#ifdef ENABLE_AM_FIX
if (settings.modulationType == MODULATION_AM && gSetting_AM_fix)
rssi += AM_fix_get_gain_diff() * 2;
#endif
return rssi;
}
static void ToggleAudio(bool on)
{
if (on == audioState)
{
return;
}
audioState = on;
if (on)
{
AUDIO_AudioPathOn();
}
else
{
AUDIO_AudioPathOff();
}
}
static void ToggleRX(bool on)
{
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
if (isListening == on) {
return;
}
#endif
isListening = on;
//RADIO_SetupAGC(settings.modulationType == MODULATION_AM, lockAGC);
RADIO_SetupAGC(false, lockAGC);
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, on);
ToggleAudio(on);
ToggleAFDAC(on);
ToggleAFBit(on);
if (on)
{
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
listenT = 25;
BK4819_WriteRegister(0x43, listenBWRegValues[settings.listenBw]);
setTailFoundInterrupt();
#else
listenT = 1000;
BK4819_WriteRegister(0x43, listenBWRegValues[settings.listenBw]);
#endif
}
else
{
BK4819_WriteRegister(0x43, GetBWRegValueForScan());
}
}
// Scan info
static void ResetScanStats()
{
scanInfo.rssi = 0;
scanInfo.rssiMax = 0;
scanInfo.rssiMin = RSSI_MAX_VALUE;
scanInfo.iPeak = 0;
scanInfo.fPeak = 0;
}
// Resets scan position and stats without touching the radio — safe to call
// on every sweep restart because scanReg30 and the RF filter path remain
// valid as long as the scan range hasn't changed.
static void InitScanPosition()
{
ResetScanStats();
scanInfo.scanStep = GetScanStep();
scanInfo.measurementsCount = GetStepsCount();
scanInfo.i = 0;
scanInfo.f = GetFStart();
scanForward = true;
}
static void InitScan()
{
InitScanPosition();
// Cache the band-select LNA and REG_30 for the upcoming sweep.
// SetFScan() will use these cached values, saving 3 SPI ops per step.
// Mask bit 9 (AF DAC enable) so the cached value is always correct for
// scanning regardless of whether audio was on when InitScan() was called
// (RelaunchScan calls InitScan before ToggleRX(false)).
BK4819_PickRXFilterPathBasedOnFrequency(scanInfo.f);
scanReg30 = BK4819_ReadRegister(BK4819_REG_30) & ~(1u << 9);
}
static void ResetBlacklist()
{
for (int i = 0; i < 128; ++i)
{
if (rssiHistory[i] == RSSI_MAX_VALUE)
rssiHistory[i] = 0;
}
#ifdef ENABLE_SCAN_RANGES
memset(blacklistFreqs, 0, sizeof(blacklistFreqs));
blacklistFreqsIdx = 0;
#endif
}
static void RelaunchScan()
{
InitScan();
ResetPeak();
ToggleRX(false);
#ifdef SPECTRUM_AUTOMATIC_SQUELCH
if (!manualSetFlag)
settings.rssiTriggerLevel = RSSI_MAX_VALUE;
#endif
preventKeypress = true;
scanInfo.rssiMin = RSSI_MAX_VALUE;
memset(peakHoldY, PEAK_HOLD_INIT, sizeof(peakHoldY));
memset(peakHoldAge, 0, sizeof(peakHoldAge));
}
static void UpdateScanInfo()
{
if (scanInfo.rssi > scanInfo.rssiMax)
{
scanInfo.rssiMax = scanInfo.rssi;
scanInfo.fPeak = scanInfo.f;
scanInfo.iPeak = scanInfo.i;
}
if (scanInfo.rssi < scanInfo.rssiMin)
{
scanInfo.rssiMin = scanInfo.rssi;
settings.dbMin = Rssi2DBm(scanInfo.rssiMin);
int dbMax = settings.dbMax - 10;
if (settings.dbMin > dbMax)
settings.dbMin = dbMax;
redrawStatus = true;
}
}
static void AutoTriggerLevel()
{
if (manualSetFlag)
return;
// Track the NOISE FLOOR (rssiMin = quietest bin in the sweep), not the
// signal peak (rssiMax). A squelch belongs just above the noise, so any
// real signal that clears the floor opens RX. Using rssiMax would push
// the threshold above all signals and the squelch would never open.
if (scanInfo.rssiMin == RSSI_MAX_VALUE)
return; // no measurement yet
// Target: noise floor + 16 RSSI units (~8 dBm above noise)
uint16_t target = scanInfo.rssiMin + 16;
if (settings.rssiTriggerLevel == RSSI_MAX_VALUE)
{
// Fresh calibration (first sweep, or after step change): jump directly.
settings.rssiTriggerLevel = target;
return;
}
// Adaptive slew: follow noise floor changes with rate limiting.
// Faster convergence when the gap is large (e.g. after filter BW change).
int16_t diff = (int16_t)target - (int16_t)settings.rssiTriggerLevel;
bool diffSign = diff < 0;
diff = my_abs(diff);
if (diff > 4)
{
int16_t step = (diff > 12) ? 4 : ((diff > 6) ? 2 : 1);
settings.rssiTriggerLevel += diffSign ? -step : step;
}
// Dead zone ±4: hold steady to avoid jitter near target
}
static void UpdatePeakInfoForce()
{
peak.t = 0;
peak.rssi = scanInfo.rssiMax;
peak.f = scanInfo.fPeak;
peak.i = scanInfo.iPeak;
AutoTriggerLevel();
}
static void UpdatePeakInfo()
{
if (peak.f == 0 || peak.t >= 1024 || peak.rssi < scanInfo.rssiMax)
UpdatePeakInfoForce();
}
static uint8_t GetHistorySlot(uint16_t idx)
{
#ifdef ENABLE_SCAN_RANGES
if (scanInfo.measurementsCount > ARRAY_SIZE(rssiHistory))
{
uint32_t slot = (uint32_t)idx * ARRAY_SIZE(rssiHistory) / scanInfo.measurementsCount;
if (slot >= ARRAY_SIZE(rssiHistory))
slot = ARRAY_SIZE(rssiHistory) - 1;
return (uint8_t)slot;
}
#endif
return (uint8_t)idx;
}
static void SetRssiHistory(uint16_t idx, uint16_t rssi)
{
uint8_t slot = GetHistorySlot(idx);
if (rssi == RSSI_MAX_VALUE)
{
rssiHistory[slot] = RSSI_MAX_VALUE;
return;
}
uint16_t prev = rssiHistory[slot];
#ifdef ENABLE_SCAN_RANGES
if (scanInfo.measurementsCount > ARRAY_SIZE(rssiHistory))
{
if (prev == RSSI_MAX_VALUE)
return;
// For large ranges: keep fast attack, soften decay to reduce flicker.
if (rssi >= prev)
rssiHistory[slot] = rssi;
else
rssiHistory[slot] = (uint16_t)((3u * prev + rssi) >> 2);
return;
}
#endif
// Attack/decay: instant rise, fast fall for stable display
if (rssi >= prev) {
rssiHistory[slot] = rssi; // Attack: instant
} else {
rssiHistory[slot] = (prev + rssi) >> 1; // Decay: halve the gap each sweep
}
}
static void Measure()
{
uint16_t rssi = scanInfo.rssi = GetRssi();
SetRssiHistory(scanInfo.i, rssi);
}
// Update things by keypress
static uint16_t dbm2rssi(int dBm)
{
return (dBm + 160 - dBmCorrTable[gRxVfo->Band]) * 2;
}
static void ClampRssiTriggerLevel()
{
settings.rssiTriggerLevel =
clamp(settings.rssiTriggerLevel, dbm2rssi(settings.dbMin),
dbm2rssi(settings.dbMax));
}
static void UpdateDbMax(bool inc)
{
settings.dbMax = clamp(settings.dbMax + (inc ? 5 : -5),
settings.dbMin + 10, 10);
ClampRssiTriggerLevel();
manualDbMaxTimer = MANUAL_DBMAX_SWEEPS;
redrawScreen = true;
redrawStatus = true;
}
static void UpdateRssiTriggerLevel(bool inc)
{
if (inc && isListening && IsPeakOverLevel())
{
// One-press escape: jump threshold above the active carrier without
// clamping — the interferer may exceed dbMax so ClampRssiTriggerLevel
// would silently prevent the escape.
settings.rssiTriggerLevel = peak.rssi + 8;
}
else
{
if (inc)
settings.rssiTriggerLevel += 2;
else
settings.rssiTriggerLevel -= 2;
if (settings.rssiTriggerLevel > dbm2rssi(settings.dbMax))
UpdateDbMax(true);
else
ClampRssiTriggerLevel();
}
redrawScreen = true;
redrawStatus = true;
}
static void UpdateScanStep(bool inc)
{
if (inc)
{
settings.scanStepIndex = settings.scanStepIndex != S_STEP_100_0kHz ? settings.scanStepIndex + 1 : 0;
}
else
{
settings.scanStepIndex = settings.scanStepIndex != 0 ? settings.scanStepIndex - 1 : S_STEP_100_0kHz;
}
settings.frequencyChangeStep = GetBW() >> 1;
RelaunchScan();
ResetBlacklist();
redrawScreen = true;
}
static void UpdateCurrentFreq(bool inc)
{
if (inc && currentFreq < F_MAX)
{
currentFreq += settings.frequencyChangeStep;
}
else if (!inc && currentFreq > F_MIN)
{
currentFreq -= settings.frequencyChangeStep;
}
else
{
return;
}
RelaunchScan();
ResetBlacklist();
redrawScreen = true;
}
static void UpdateCurrentFreqStill(bool inc)
{
uint8_t offset = modulationTypeTuneSteps[settings.modulationType];
uint32_t f = fMeasure;
if (inc && f < F_MAX)
{
f += offset;
}
else if (!inc && f > F_MIN)
{
f -= offset;
}
SetF(f);
redrawScreen = true;
}
static void UpdateFreqChangeStep(bool inc)
{
uint16_t diff = GetScanStep() * 4;
if (inc && settings.frequencyChangeStep < 200000)
{
settings.frequencyChangeStep += diff;
}
else if (!inc && settings.frequencyChangeStep > 10000)
{
settings.frequencyChangeStep -= diff;
}
SYSTEM_DelayMs(100);
redrawScreen = true;
}
static void ToggleModulation()
{
if (settings.modulationType < MODULATION_UKNOWN - 1)
{
settings.modulationType++;
}
else
{
settings.modulationType = MODULATION_FM;
}
RADIO_SetModulation(settings.modulationType);
RelaunchScan();
redrawScreen = true;
}
static void ToggleListeningBW()
{
if (settings.listenBw == BK4819_FILTER_BW_NARROWER)
{
settings.listenBw = BK4819_FILTER_BW_WIDE;
}
else
{
settings.listenBw++;
}
redrawScreen = true;
}
static void ToggleBacklight()
{
settings.backlightState = !settings.backlightState;
if (settings.backlightState)
{
// BACKLIGHT_TurnOn();
BACKLIGHT_SetBrightness(gEeprom.BACKLIGHT_MAX);
}
else
{
// BACKLIGHT_TurnOff();
BACKLIGHT_SetBrightness(gEeprom.BACKLIGHT_MIN);
}
}
static void ToggleStepsCount()
{
if (settings.stepsCount == STEPS_128)
{
settings.stepsCount = STEPS_16;
}
else
{
settings.stepsCount--;
}
settings.frequencyChangeStep = GetBW() >> 1;
RelaunchScan();
ResetBlacklist();
redrawScreen = true;
}
static void ResetFreqInput()
{
tempFreq = 0;
for (int i = 0; i < 10; ++i)
{
freqInputString[i] = '-';
}
}
static void FreqInput()
{
freqInputIndex = 0;
freqInputDotIndex = 0;
ResetFreqInput();
SetState(FREQ_INPUT);
}
static void UpdateFreqInput(KEY_Code_t key)
{
if (key != KEY_EXIT && freqInputIndex >= 10)
{
return;
}
if (key == KEY_STAR)
{
if (freqInputIndex == 0 || freqInputDotIndex)
{
return;
}
freqInputDotIndex = freqInputIndex;
}
if (key == KEY_EXIT)
{
freqInputIndex--;
if (freqInputDotIndex == freqInputIndex)
freqInputDotIndex = 0;
}
else
{
freqInputArr[freqInputIndex++] = key;
}
ResetFreqInput();
uint8_t dotIndex =
freqInputDotIndex == 0 ? freqInputIndex : freqInputDotIndex;
KEY_Code_t digitKey;
for (int i = 0; i < 10; ++i)
{
if (i < freqInputIndex)
{
digitKey = freqInputArr[i];
freqInputString[i] = digitKey <= KEY_9 ? '0' + digitKey - KEY_0 : '.';
}
else
{
freqInputString[i] = '-';
}
}
uint32_t base = 100000; // 1MHz in BK units
for (int i = dotIndex - 1; i >= 0; --i)
{
tempFreq += (freqInputArr[i] - KEY_0) * base;
base *= 10;
}
base = 10000; // 0.1MHz in BK units
if (dotIndex < freqInputIndex)
{
for (int i = dotIndex + 1; i < freqInputIndex; ++i)
{
tempFreq += (freqInputArr[i] - KEY_0) * base;
base /= 10;
}
}
redrawScreen = true;
}
static void Blacklist()
{
#ifdef ENABLE_SCAN_RANGES
blacklistFreqs[blacklistFreqsIdx++ % ARRAY_SIZE(blacklistFreqs)] = peak.i;
#endif
SetRssiHistory(peak.i, RSSI_MAX_VALUE);
ResetPeak();
ToggleRX(false);
ResetScanStats();
}
#ifdef ENABLE_SCAN_RANGES
static bool IsBlacklisted(uint16_t idx)
{
if (blacklistFreqsIdx)
for (uint8_t i = 0; i < ARRAY_SIZE(blacklistFreqs); i++)
if (blacklistFreqs[i] == idx)
return true;
return false;
}
#endif
// Draw things
// Integer square root (for sugar map non-linear compression)
static uint8_t iSqrt(uint16_t n)
{
if (n == 0) return 0;
uint16_t x = n;
uint16_t y = (x + 1) >> 1;
while (y < x) { x = y; y = (x + n / x) >> 1; }
return (uint8_t)x;
}
// applied x2 to prevent initial rounding.
// A mild square-root compression (sugar map) is applied so that weak signals
// occupy more of the display height while strong peaks are not clipped.
uint8_t Rssi2PX(uint16_t rssi, uint8_t pxMin, uint8_t pxMax)
{
const int DB_MIN = settings.dbMin << 1;
const int DB_MAX = settings.dbMax << 1;
const int DB_RANGE = DB_MAX - DB_MIN;
const uint8_t PX_RANGE = pxMax - pxMin;
int dbm = clamp(Rssi2DBm(rssi) << 1, DB_MIN, DB_MAX);
// Linear 0..PX_RANGE position
uint8_t linear = (uint8_t)(((dbm - DB_MIN) * PX_RANGE + DB_RANGE / 2) / DB_RANGE);
// Square-root compression: sqrt(linear * PX_RANGE) rescaled to PX_RANGE
uint8_t compressed = iSqrt((uint16_t)linear * PX_RANGE);
// Blend 50/50 between linear and compressed for a subtle effect
return ((uint16_t)linear + compressed) / 2 + pxMin;
}
uint8_t Rssi2Y(uint16_t rssi)
{
// Map into [DrawingTopY, DrawingEndY] so peaks never overdraw the
// frequency display rendered in gFrameBuffer[0] (pixels 0-7).
return DrawingEndY - Rssi2PX(rssi, 0, DrawingEndY - DrawingTopY);
}
// Resolve the RSSI value at fractional sample index (Q8 fixed-point) using
// linear interpolation. Blacklisted samples (RSSI_MAX_VALUE) are skipped by
// falling back to the other neighbour; if both are blacklisted, returns
// RSSI_MAX_VALUE so the caller can skip the column.
static uint16_t InterpolateRssi(uint8_t bars, uint16_t pos256)
{
uint8_t i = pos256 >> 8;
uint8_t frac = pos256 & 0xFF;
if (i >= bars - 1)
{
i = bars - 1;
frac = 0;
}
uint16_t rssiA = rssiHistory[i];
uint16_t rssiB = rssiHistory[(i + 1 < bars) ? (i + 1) : i];
if (rssiA == RSSI_MAX_VALUE && rssiB == RSSI_MAX_VALUE)
return RSSI_MAX_VALUE;
if (rssiA == RSSI_MAX_VALUE)
return rssiB;
if (rssiB == RSSI_MAX_VALUE)
return rssiA;
return ((uint32_t)rssiA * (256 - frac) + (uint32_t)rssiB * frac) >> 8;
}
// Sentinel value in topY[] to mark a column that should not be drawn
// (blacklisted RSSI sample on both neighbours).
#define SPECTRUM_TOPY_SKIP 0xFF
// Half-step bridging helper: compute crestTop/crestBot for column x
// from a topY-like array.
static void CalcCrest(const uint8_t *yArr, uint8_t x,
uint8_t *crestTop, uint8_t *crestBot)
{
uint8_t y0 = yArr[x];
*crestTop = y0;
*crestBot = y0;
bool goBack = true;
uint8_t n = 0;
if (x > 0) {
n = yArr[x - 1];
goto Start;
}
Back:
goBack = false;
if (x + 1 < 128) {
n = yArr[x + 1];
goto Start;
}
return;
Start:
if (n != SPECTRUM_TOPY_SKIP && n <= DrawingEndY) {
uint8_t mid = (y0 + n + 1) >> 1;
if (mid < *crestTop) *crestTop = mid;
if (mid > *crestBot) *crestBot = mid;
}
if (goBack)
goto Back;
}
// Draw the spectrum curve (solid crest + checkerboard body) and the peak hold
// dotted trace. Both use the same half-step bridging so the peak hold crest
// shape mirrors the live crest exactly, just rendered with a dotted pattern.
static void DrawSpectrumCurve(const uint8_t *topY)
{
// Pass 1: update peakHoldY[] from topY[] before rendering so that the
// bridging in Pass 2 already sees fully-updated neighbour values.
for (uint8_t x = 0; x < 128; x++)
{
uint8_t y0 = topY[x];
if (y0 == SPECTRUM_TOPY_SKIP || y0 > DrawingEndY) {
peakHoldY[x] = PEAK_HOLD_INIT;
continue;
}
uint8_t ph = peakHoldY[x];
if (ph == PEAK_HOLD_INIT || y0 <= ph)
{
peakHoldY[x] = y0;
peakHoldAge[x >> 1] = 0;
}
else
{
if (peakHoldAge[x >> 1] < PEAK_HOLD_DELAY) {
if (!(x & 1)) peakHoldAge[x >> 1]++;
} else {
ph += 2;
peakHoldY[x] = (ph <= DrawingEndY) ? ph : PEAK_HOLD_INIT;
}
}
}
// Pass 2: draw live curve (solid) then peak hold (dotted).
for (uint8_t x = 0; x < 128; x++)
{
// --- Live spectrum crest + body ---
uint8_t y0 = topY[x];
if (y0 != SPECTRUM_TOPY_SKIP && y0 <= DrawingEndY)
{
uint8_t crestTop, crestBot;
CalcCrest(topY, x, &crestTop, &crestBot);
// Solid crest contour.
for (uint8_t y = crestTop; y <= crestBot; y++)
PutPixel(x, y, true);
// Checkerboard body below the crest.
for (uint8_t y = crestBot + 1; y <= DrawingEndY; y++)
if (((x + y) & 1) == 0)
PutPixel(x, y, true);
}
// --- Peak hold dotted crest ---
uint8_t ph = peakHoldY[x];
if (ph != PEAK_HOLD_INIT && ph <= DrawingEndY)
{
uint8_t phTop, phBot;
CalcCrest(peakHoldY, x, &phTop, &phBot);
// Dotted crest: checkerboard pattern over the full crest range.
for (uint8_t y = phTop; y <= phBot; y++)
if (((x + y) & 1) == 0)
PutPixel(x, y, true);
}
}
}
// Spatial smoothing: 3-bin moving average on topY for a cleaner curve.
// Only averages valid (non-SKIP) neighbours.
static void SmoothTopY(uint8_t *topY)
{
uint8_t prev = topY[0];
for (uint8_t x = 1; x < 127; x++)
{
uint8_t cur = topY[x];
uint8_t next = topY[x + 1];
if (cur == SPECTRUM_TOPY_SKIP) {
prev = cur;
continue;
}
uint16_t sum = cur;
uint8_t n = 1;
if (prev != SPECTRUM_TOPY_SKIP) { sum += prev; n++; }
if (next != SPECTRUM_TOPY_SKIP) { sum += next; n++; }
prev = cur; // save unsmoothed value for next iteration
topY[x] = (sum + n / 2) / n; // rounded average
}
}
// Fill topY[0..127] by linear interpolation of `bars` RSSI samples across the
// 128 display columns. Invalid (blacklisted) samples become SPECTRUM_TOPY_SKIP.
static void BuildSpectrumTopY(uint8_t *topY, uint8_t bars)
{
if (bars == 0)
{
for (uint8_t x = 0; x < 128; x++)
topY[x] = SPECTRUM_TOPY_SKIP;
return;
}
if (bars == 1)
{
uint16_t rssi = rssiHistory[0];
uint8_t y = (rssi == RSSI_MAX_VALUE) ? SPECTRUM_TOPY_SKIP : Rssi2Y(rssi);
for (uint8_t x = 0; x < 128; x++)
topY[x] = y;
return;
}
// Q8 fixed-point: step256 / 256 advances one sample, multiplied by x.
uint16_t step256 = ((uint16_t)(bars - 1) << 8) / 127;
for (uint8_t x = 0; x < 128; x++)
{
uint16_t rssi = InterpolateRssi(bars, (uint16_t)x * step256);
topY[x] = (rssi == RSSI_MAX_VALUE) ? SPECTRUM_TOPY_SKIP : Rssi2Y(rssi);
}
}
#ifdef ENABLE_FEAT_F4HWN
static void DrawSpectrum()
{
uint16_t steps = GetStepsCount();
// max bars at 128 to correctly draw larger numbers of samples
uint8_t bars = (steps > 128) ? 128 : steps;
uint8_t topY[128];
BuildSpectrumTopY(topY, bars);
SmoothTopY(topY);
DrawSpectrumCurve(topY);
}
#else
static void DrawSpectrum()
{
uint8_t bars = 128 >> settings.stepsCount;
if (bars == 0)
bars = 1;
uint8_t topY[128];
BuildSpectrumTopY(topY, bars);
SmoothTopY(topY);
DrawSpectrumCurve(topY);
}
#endif
static void DrawStatus()
{
#ifdef SPECTRUM_EXTRA_VALUES
sprintf(String, "%d/%d%s P:%d T:%d", settings.dbMin, settings.dbMax,
manualSetFlag ? "M" : (manualDbMaxTimer ? "T" : ""),
Rssi2DBm(peak.rssi), Rssi2DBm(settings.rssiTriggerLevel));
#else
sprintf(String, "%d/%d%s", settings.dbMin, settings.dbMax,
manualSetFlag ? "M" : (manualDbMaxTimer ? "T" : ""));
#endif
GUI_DisplaySmallest(String, 0, 1, true, true);
BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryCheckCounter++ % 4],
&gBatteryCurrent);
uint16_t voltage = (gBatteryVoltages[0] + gBatteryVoltages[1] +
gBatteryVoltages[2] + gBatteryVoltages[3]) /
4 * 760 / gBatteryCalibration[3];
unsigned perc = BATTERY_VoltsToPercent(voltage);
// sprintf(String, "%d %d", voltage, perc);
// GUI_DisplaySmallest(String, 48, 1, true, true);
gStatusLine[116] = 0b00011100;
gStatusLine[117] = 0b00111110;
for (int i = 118; i <= 126; i++)
{
gStatusLine[i] = 0b00100010;
}
for (unsigned i = 127; i >= 118; i--)
{
if (127 - i <= (perc + 5) * 9 / 100)
{
gStatusLine[i] = 0b00111110;
}
}
}
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
static void ShowChannelName(uint32_t f)
{
static uint32_t channelF = 0;
static char channelName[12];
if (isListening)
{
if (f != channelF) {
channelF = f;
unsigned int i;
memset(channelName, 0, sizeof(channelName));
for (i = 0; IS_MR_CHANNEL(i); i++)
{
if (RADIO_CheckValidChannel(i, false, 0))
{
if (SETTINGS_FetchChannelFrequency(i) == channelF)
{
SETTINGS_FetchChannelName(channelName, i);
break;
}
}
}
}
if (channelName[0] != 0) {
// Channel name starts at x=43 (fixed), leaving room for the dBm
// string on the left (max ~40 px) and battery indicator at x=116.
// Clear first so a shorter name doesn't leave stale pixels.
memset(&gStatusLine[43], 0, 116 - 43);
UI_PrintStringSmallBufferNormal(channelName, gStatusLine + 43);
}
}
else
{
memset(&gStatusLine[43], 0, 116 - 43);
}
ST7565_BlitStatusLine();
}
#endif
static void FormatFrequency(uint32_t freq, char *buffer) {
sprintf(buffer, "%u.%05u", freq / 100000, freq % 100000);
}
static void DrawF(uint32_t f)
{
FormatFrequency(f, String);
// Align frequency with channel name in status bar (both at x=43).
// Left-aligned (End == Start = 43) so it does not collide with BW at x=108.
UI_PrintStringSmallNormal(String, 43, 43, 0);
sprintf(String, "%3s", gModulationStr[settings.modulationType]);
GUI_DisplaySmallest(String, 116, 1, false, true);
sprintf(String, "%4sk", bwOptions[settings.listenBw]);
GUI_DisplaySmallest(String, 108, 7, false, true);
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
ShowChannelName(f);
#endif
}
static void DrawNums()
{
if (currentState == SPECTRUM)
{
#ifdef ENABLE_SCAN_RANGES
sprintf(String, "%ux", gScanRangeStart ? GetStepsCountDisplay() : GetStepsCount());
#else
sprintf(String, "%ux", GetStepsCount());
#endif
GUI_DisplaySmallest(String, 0, 1, false, true);
sprintf(String, "%u.%02uk", GetScanStep() / 100, GetScanStep() % 100);
GUI_DisplaySmallest(String, 0, 7, false, true);
}
if (IsCenterMode())
{
sprintf(String, "%u.%05u \x7F%u.%02uk", currentFreq / 100000,
currentFreq % 100000, settings.frequencyChangeStep / 100,
settings.frequencyChangeStep % 100);
GUI_DisplaySmallest(String, 36, 49, false, true);
}
else
{
FormatFrequency(GetFStart(), String);
GUI_DisplaySmallest(String, 0, 49, false, true);
sprintf(String, "\x7F%u.%02uk", settings.frequencyChangeStep / 100,
settings.frequencyChangeStep % 100);
GUI_DisplaySmallest(String, 48, 49, false, true);
FormatFrequency(GetFEnd(), String);
GUI_DisplaySmallest(String, 93, 49, false, true);
}
}
static void DrawRssiTriggerLevel()
{
if (settings.rssiTriggerLevel == RSSI_MAX_VALUE || monitorMode)
return;
uint8_t y = Rssi2Y(settings.rssiTriggerLevel);
for (uint8_t x = 0; x < 128; x += 2)
{
PutPixel(x, y, true);
}
}
static void DrawTicks()
{
uint32_t f = GetFStart();
uint32_t span = GetFEnd() - GetFStart();
uint32_t step = span / 128;
for (uint8_t i = 0; i < 128; i += (1 << settings.stepsCount))
{
f = GetFStart() + span * i / 128;
uint8_t barValue = 0b00000001;
(f % 10000) < step && (barValue |= 0b00000010);
(f % 50000) < step && (barValue |= 0b00000100);
(f % 100000) < step && (barValue |= 0b00011000);
gFrameBuffer[5][i] |= barValue;
}
// center
if (IsCenterMode())
{
memset(gFrameBuffer[5] + 62, 0x80, 5);
gFrameBuffer[5][64] = 0xff;
}
else
{
memset(gFrameBuffer[5] + 1, 0x80, 3);
memset(gFrameBuffer[5] + 124, 0x80, 3);
gFrameBuffer[5][0] = 0xff;
gFrameBuffer[5][127] = 0xff;
}
}
static void DrawArrow(uint8_t x)
{
for (signed i = -2; i <= 2; ++i)
{
signed v = x + i;
if (!(v & 128))
{
gFrameBuffer[5][v] |= (0b01111000 << my_abs(i)) & 0b01111000;
}
}
}
static bool GetDirection(KEY_Code_t key) {
return (key == KEY_UP) ? !gEeprom.SET_NAV : gEeprom.SET_NAV;
}
// Returns true if the key was handled (stop state-specific processing).
static bool OnKeyDownCommon(uint8_t key) {
bool isTrue = (key == KEY_3 || key == KEY_STAR);
switch (key)
{
case KEY_3:
case KEY_9:
UpdateDbMax(isTrue);
return true;
case KEY_STAR:
case KEY_F:
UpdateRssiTriggerLevel(isTrue);
return true;
case KEY_0:
ToggleModulation();
return true;
case KEY_6:
ToggleListeningBW();
return true;
case KEY_SIDE2:
ToggleBacklight();
return true;
}
return false;
}
static void OnKeyDown(uint8_t key) {
bool isTrue = (key == KEY_1 || key == KEY_2);
switch (key)
{
case KEY_1:
case KEY_7:
UpdateScanStep(isTrue);
break;
case KEY_2:
case KEY_8:
UpdateFreqChangeStep(isTrue);
break;
case KEY_UP:
case KEY_DOWN:
#ifdef ENABLE_SCAN_RANGES
if (!gScanRangeStart) {
#endif
UpdateCurrentFreq(GetDirection(key));
#ifdef ENABLE_SCAN_RANGES
}
#endif
break;
case KEY_SIDE1:
Blacklist();
break;
case KEY_5:
#ifdef ENABLE_SCAN_RANGES
if (!gScanRangeStart)
#endif
FreqInput();
break;
case KEY_4:
#ifdef ENABLE_SCAN_RANGES
if (!gScanRangeStart)
#endif
ToggleStepsCount();
break;
case KEY_PTT:
SetState(STILL);
TuneToPeak();
break;
case KEY_MENU:
manualSetFlag = !manualSetFlag;
if (!manualSetFlag)
settings.rssiTriggerLevel = RSSI_MAX_VALUE;
redrawStatus = true;
break;
case KEY_EXIT:
if (menuState)
{
menuState = 0;
break;
}
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
SaveSettings();
#endif
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
gEeprom.CURRENT_STATE = 0;
SETTINGS_WriteCurrentState();
#endif
DeInitSpectrum();
break;
default:
break;
}
}
static void OnKeyDownFreqInput(KEY_Code_t key)
{
switch (key)
{
case KEY_0...KEY_9:
case KEY_STAR:
case KEY_EXIT:
if (freqInputIndex == 0 && key == KEY_EXIT)
{
SetState(previousState);
break;
}
UpdateFreqInput(key);
break;
case KEY_MENU:
if (tempFreq < F_MIN || tempFreq > F_MAX)
{
break;
}
SetState(previousState);
currentFreq = tempFreq;
if (currentState == SPECTRUM)
{
ResetBlacklist();
RelaunchScan();
}
else
{
SetF(currentFreq);
}
break;
default:
break;
}
}
static void OnKeyDownStill(KEY_Code_t key) {
switch (key)
{
case KEY_UP:
case KEY_DOWN:
if (menuState) {
SetRegMenuValue(menuState, GetDirection(key));
break;
}
UpdateCurrentFreqStill(GetDirection(key));
break;
case KEY_5:
FreqInput();
break;
case KEY_SIDE1:
monitorMode = !monitorMode;
break;
case KEY_MENU:
menuState = (menuState == ARRAY_SIZE(registerSpecs) - 1) ? 1 : menuState + 1;
redrawScreen = true;
break;
case KEY_EXIT:
if (!menuState)
{
SetState(SPECTRUM);
lockAGC = false;
monitorMode = false;
RelaunchScan();
break;
}
menuState = 0;
break;
default:
break;
}
}
static void RenderFreqInput() { UI_PrintString(freqInputString, 2, 127, 0, 8); }
static void RenderStatus()
{
memset(gStatusLine, 0, sizeof(gStatusLine));
DrawStatus();
ST7565_BlitStatusLine();
}
static void RenderSpectrum()
{
uint16_t steps = GetStepsCount();
uint8_t arrowX = (steps > 1) ? (uint8_t)(128u * peak.i / (steps - 1)) : 0;
DrawTicks();
DrawArrow(arrowX);
DrawSpectrum();
DrawRssiTriggerLevel();
DrawF(peak.f);
DrawNums();
}
static void RenderStill()
{
DrawF(fMeasure);
const uint8_t METER_PAD_LEFT = 3;
memset(&gFrameBuffer[2][METER_PAD_LEFT], 0b00010000, 121);
for (int i = 0; i < 121; i += 5)
{
gFrameBuffer[2][i + METER_PAD_LEFT] = 0b00110000;
}
for (int i = 0; i < 121; i += 10)
{
gFrameBuffer[2][i + METER_PAD_LEFT] = 0b01110000;
}
uint8_t x = Rssi2PX(rssiSmoothed, 0, 121);
for (int i = 0; i < x; ++i)
{
if (i % 5)
{
gFrameBuffer[2][i + METER_PAD_LEFT] |= 0b00000111;
}
}
int dbm = Rssi2DBm(rssiSmoothed);
uint8_t s = DBm2S(dbm);
sprintf(String, "S: %u", s);
GUI_DisplaySmallest(String, 4, 25, false, true);
sprintf(String, "%d dBm", dbm);
GUI_DisplaySmallest(String, 28, 25, false, true);
if (!monitorMode)
{
uint8_t x = Rssi2PX(settings.rssiTriggerLevel, 0, 121);
gFrameBuffer[2][METER_PAD_LEFT + x] = 0b11111111;
}
const uint8_t PAD_LEFT = 4;
const uint8_t CELL_WIDTH = 30;
uint8_t offset = PAD_LEFT;
uint8_t row = 4;
for (int i = 0, idx = 1; idx <= 3; ++i, ++idx)
{
if (idx == 4)
{
row += 2;
i = 0;
}
offset = PAD_LEFT + i * CELL_WIDTH;
if (menuState == idx)
{
for (int j = 0; j < CELL_WIDTH; ++j)
{
gFrameBuffer[row][j + offset] = 0xFF;
gFrameBuffer[row + 1][j + offset] = 0xFF;
}
}
sprintf(String, "%s", registerSpecs[idx].name);
GUI_DisplaySmallest(String, offset + 2, row * 8 + 2, false,
menuState != idx);
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
sprintf(String, "%ddB", regOptions[idx].options[GetRegMenuValue(idx)]);
/*
if(idx == 1)
{
sprintf(String, "%ddB", LNAsOptions[GetRegMenuValue(idx)]);
}
else if(idx == 2)
{
sprintf(String, "%ddB", LNAOptions[GetRegMenuValue(idx)]);
}
else if(idx == 3)
{
sprintf(String, "%ddB", VGAOptions[GetRegMenuValue(idx)]);
}
else if(idx == 4)
{
sprintf(String, "%skHz", BPFOptions[(GetRegMenuValue(idx) / 0x2aaa)]);
}
*/
#else
sprintf(String, "%u", GetRegMenuValue(idx));
#endif
GUI_DisplaySmallest(String, offset + 2, (row + 1) * 8 + 1, false,
menuState != idx);
}
}
static void Render()
{
UI_DisplayClear();
switch (currentState)
{
case SPECTRUM:
RenderSpectrum();
break;
case FREQ_INPUT:
RenderFreqInput();
break;
case STILL:
RenderStill();
break;
}
// Display blit is done incrementally (one page per tick) — see Tick().
}
static bool HandleUserInput()
{
kbd.prev = kbd.current;
kbd.current = KEYBOARD_GetKey();
if (kbd.current != KEY_INVALID && kbd.current == kbd.prev)
{
if (kbd.counter < 16)
kbd.counter++;
else
kbd.counter -= 3;
SYSTEM_DelayMs(20);
}
else
{
kbd.counter = 0;
}
if (kbd.counter == 3 || kbd.counter == 16)
{
if (currentState == FREQ_INPUT)
OnKeyDownFreqInput(kbd.current);
else if (!OnKeyDownCommon(kbd.current)) {
if (currentState == SPECTRUM)
OnKeyDown(kbd.current);
else if (currentState == STILL)
OnKeyDownStill(kbd.current);
}
}
return true;
}
static void Scan()
{
uint8_t slot = GetHistorySlot(scanInfo.i);
if (rssiHistory[slot] != RSSI_MAX_VALUE
#ifdef ENABLE_SCAN_RANGES
&& !IsBlacklisted(scanInfo.i)
#endif
)
{
SetFScan(scanInfo.f);
Measure();
UpdateScanInfo();
}
}
static void NextScanStep()
{
++peak.t;
if (scanForward) {
++scanInfo.i;
scanInfo.f += scanInfo.scanStep;
} else {
--scanInfo.i;
scanInfo.f -= scanInfo.scanStep;
}
}
static void UpdateScan()
{
Scan();
bool atEnd = scanForward ? (scanInfo.i >= scanInfo.measurementsCount - 1)
: (scanInfo.i <= 1);
if (!atEnd)
{
NextScanStep();
return;
}
// End of half-sweep: unlock keypad; Render() fires on its own timer.
preventKeypress = false;
UpdatePeakInfo();
if (IsPeakOverLevel())
{
ToggleRX(true);
TuneToPeak();
return;
}
if (scanForward)
{
// End of forward half-sweep: reverse direction.
// Advance one step immediately so the backward sweep starts at count-2,
// not count-1 — avoids scanning the same endpoint twice in a row,
// which would produce a double SPI burst ("ta-tac" audio artifact).
scanForward = false;
NextScanStep();
return;
}
// End of backward half-sweep: full round trip done.
if (! (scanInfo.measurementsCount >> 7)) // if (scanInfo.measurementsCount < 128)
memset(&rssiHistory[scanInfo.measurementsCount], 0,
sizeof(rssiHistory) - scanInfo.measurementsCount * sizeof(rssiHistory[0]));
// Auto-adjust dbMax unless the user has overridden it manually.
if (manualDbMaxTimer > 0) {
if (--manualDbMaxTimer == 0)
redrawStatus = true;
} else if (!manualSetFlag) {
int newMax = Rssi2DBm(scanInfo.rssiMax) + 5;
int dbMin = settings.dbMin + 10;
if (newMax < dbMin)
newMax = dbMin;
if (newMax > 10)
newMax = 10;
settings.dbMax = newMax;
}
newScanStart = true;
}
static void UpdateStill()
{
Measure();
redrawScreen = true;
preventKeypress = false;
peak.rssi = scanInfo.rssi;
// EMA α=0.25 for display only; seed on first sample
rssiSmoothed = rssiSmoothed ? (rssiSmoothed * 3 + scanInfo.rssi) >> 2
: scanInfo.rssi;
AutoTriggerLevel();
if (IsPeakOverLevel() || monitorMode) {
ToggleRX(true);
}
}
static void UpdateListening()
{
preventKeypress = false;
// listenT counts down with 1ms delay per tick — no SPI during this phase.
if (listenT)
{
listenT--;
SYSTEM_DelayMs(1);
return;
}
// --- Single SPI burst: all BK4819 accesses happen here, once per
// listenT expiry (every 320 ms). SPI repeats at ~3 Hz — below the
// audible range. Between bursts the bus is completely silent.
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
bool tailFound = checkIfTailFound();
if (tailFound)
{
ToggleRX(false);
ResetScanStats();
newScanStart = true;
return;
}
#endif
if (currentState == SPECTRUM)
{
BK4819_WriteRegister(0x43, GetBWRegValueForScan());
Measure();
BK4819_WriteRegister(0x43, listenBWRegValues[settings.listenBw]);
}
else
{
#ifndef ENABLE_FEAT_F4HWN_SPECTRUM
if (currentState == STILL)
{
ToggleRX(false);
ResetScanStats();
newScanStart = true;
return;
}
#endif
Measure();
}
peak.rssi = scanInfo.rssi;
rssiSmoothed = rssiSmoothed ? (rssiSmoothed * 3 + scanInfo.rssi) >> 2
: scanInfo.rssi;
redrawScreen = true;
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
if ((IsPeakOverLevel() && !tailFound) || monitorMode)
{
listenT = 320;
return;
}
#else
if (IsPeakOverLevel() || monitorMode)
{
listenT = 320;
return;
}
#endif
ToggleRX(false);
ResetScanStats();
newScanStart = true;
}
static void Tick()
{
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
// Parse incoming packets on every tick so serial keys are never missed,
// regardless of whether the screen needs redrawing.
SCREENSHOT_ParseInput();
#endif
if (gNextTimeslice)
{
gNextTimeslice = false;
#ifdef ENABLE_AM_FIX
if (settings.modulationType == MODULATION_AM && !lockAGC)
{
AM_fix_10ms(vfo); // allow AM_Fix to apply its AGC action
}
#endif
BACKLIGHT_Update();
}
#ifdef ENABLE_SCAN_RANGES
if (gNextTimeslice_500ms)
{
gNextTimeslice_500ms = false;
// For large scans (>128 steps), refresh display periodically but
// wait for the full sweep to complete before triggering listen mode.
// This avoids showing stale rssiHistory data from a previous sweep.
if (GetStepsCount() > 128 && !isListening)
{
redrawScreen = true;
preventKeypress = false;
}
}
#endif
if (!preventKeypress)
{
HandleUserInput();
}
if (newScanStart)
{
InitScanPosition();
newScanStart = false;
}
if (isListening && currentState != FREQ_INPUT)
{
UpdateListening();
}
else
{
if (currentState == SPECTRUM)
{
UpdateScan();
}
else if (currentState == STILL)
{
UpdateStill();
}
}
if (redrawStatus || ++statuslineUpdateTimer > 4096)
{
RenderStatus();
redrawStatus = false;
statuslineUpdateTimer = 0;
}
// Render at a fixed rate (RENDER_PERIOD_TICKS) independent of step count,
// so the CPU burst from Render() never falls below the ~9 Hz flutter-fusion
// threshold regardless of how many steps the scan uses. redrawScreen can
// still force an immediate repaint (key presses, settings changes, etc.).
if (redrawScreen || ++renderTimer >= RENDER_PERIOD_TICKS)
{
Render();
// For screenshot
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
SCREENSHOT_Update(false);
#endif
redrawScreen = false;
renderTimer = 0;
}
// Send one framebuffer page to the display per tick (~47 Hz full refresh).
ST7565_BlitLine(renderPage);
if (++renderPage >= FRAME_LINES)
renderPage = 0;
}
void APP_RunSpectrum()
{
settings.backlightState = gEeprom.BACKLIGHT_TIME == 0 ? false : true;
// TX here coz it always? set to active VFO
vfo = gEeprom.TX_VFO;
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
LoadSettings();
#endif
// set the current frequency in the middle of the display
#ifdef ENABLE_SCAN_RANGES
if (gScanRangeStart)
{
currentFreq = initialFreq = gScanRangeStart;
for (uint8_t i = 0; i < ARRAY_SIZE(scanStepValues); i++)
{
if (scanStepValues[i] >= gTxVfo->StepFrequency)
{
settings.scanStepIndex = i;
break;
}
}
settings.stepsCount = STEPS_128;
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
gEeprom.CURRENT_STATE = 5;
#endif
}
else {
#endif
currentFreq = initialFreq = gTxVfo->pRX->Frequency -
((GetStepsCount() / 2) * GetScanStep());
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
gEeprom.CURRENT_STATE = 4;
#endif
}
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
SETTINGS_WriteCurrentState();
#endif
BackupRegisters();
isListening = true; // to turn off RX later
redrawStatus = true;
redrawScreen = true;
newScanStart = true;
ToggleRX(true), ToggleRX(false); // hack to prevent noise when squelch off
RADIO_SetModulation(settings.modulationType = gTxVfo->Modulation);
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
BK4819_SetFilterBandwidth(settings.listenBw, false);
#else
BK4819_SetFilterBandwidth(settings.listenBw = BK4819_FILTER_BW_WIDE, false);
#endif
RelaunchScan();
memset(rssiHistory, 0, sizeof(rssiHistory));
memset(peakHoldY, PEAK_HOLD_INIT, sizeof(peakHoldY));
memset(peakHoldAge, 0, sizeof(peakHoldAge));
rssiSmoothed = 0;
manualDbMaxTimer = 0;
manualSetFlag = false;
isInitialized = true;
while (isInitialized)
{
Tick();
}
BACKLIGHT_TurnOn();
}