mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
Spectrum refactoring
This commit is contained in:
1 parent
9e92037674
commit
a7fed2c2d8
2 files changed
+421
-156
No files matched your search
+401
-143
@@ -90,7 +90,24 @@ SpectrumSettings settings = {.stepsCount = STEPS_64,
|
||||
uint32_t fMeasure = 0;
|
||||
uint32_t currentFreq, tempFreq;
|
||||
uint16_t rssiHistory[128];
|
||||
int vfo;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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];
|
||||
@@ -124,25 +141,17 @@ static void LoadSettings()
|
||||
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()
|
||||
@@ -152,6 +161,7 @@ static void SaveSettings()
|
||||
|
||||
Data[3] = (settings.scanStepIndex << 4) | (settings.stepsCount << 2) | settings.listenBw;
|
||||
|
||||
|
||||
PY25Q16_WriteBuffer(0x00A158, Data, sizeof(Data), false);
|
||||
}
|
||||
#endif
|
||||
@@ -226,17 +236,6 @@ static void PutPixelStatus(uint8_t x, uint8_t y, bool fill)
|
||||
}
|
||||
#endif
|
||||
|
||||
static void DrawVLine(int sy, int ey, int nx, bool fill)
|
||||
{
|
||||
for (int i = sy; i <= ey; i++)
|
||||
{
|
||||
if (i < 56 && nx < 128)
|
||||
{
|
||||
PutPixel(nx, i, fill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef ENABLE_FEAT_F4HWN
|
||||
static void GUI_DisplaySmallest(const char *pString, uint8_t x, uint8_t y,
|
||||
bool statusbar, bool fill)
|
||||
@@ -306,7 +305,7 @@ static const BK4819_REGISTER_t registers_to_save[] = {
|
||||
BK4819_REG_7E,
|
||||
};
|
||||
|
||||
static uint16_t registers_stack[sizeof(registers_to_save)];
|
||||
static uint16_t registers_stack[ARRAY_SIZE(registers_to_save)];
|
||||
|
||||
static void BackupRegisters()
|
||||
{
|
||||
@@ -349,6 +348,22 @@ static void SetF(uint32_t f)
|
||||
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; }
|
||||
@@ -456,12 +471,14 @@ uint8_t GetBWRegValueForScan()
|
||||
|
||||
uint16_t GetRssi()
|
||||
{
|
||||
// SYSTICK_DelayUs(800);
|
||||
// testing autodelay based on Glitch value
|
||||
while ((BK4819_ReadRegister(0x63) & 0b11111111) >= 255)
|
||||
// Wait for glitch to settle below threshold (not just < 255)
|
||||
uint8_t guard = 50;
|
||||
while (guard-- && (BK4819_ReadRegister(0x63) & 0xFF) >= 200)
|
||||
{
|
||||
SYSTICK_DelayUs(100);
|
||||
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)
|
||||
@@ -508,7 +525,7 @@ static void ToggleRX(bool on)
|
||||
if (on)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
|
||||
listenT = 100;
|
||||
listenT = 25;
|
||||
BK4819_WriteRegister(0x43, listenBWRegValues[settings.listenBw]);
|
||||
setTailFoundInterrupt();
|
||||
#else
|
||||
@@ -528,6 +545,7 @@ static void ResetScanStats()
|
||||
{
|
||||
scanInfo.rssi = 0;
|
||||
scanInfo.rssiMax = 0;
|
||||
scanInfo.rssiMin = RSSI_MAX_VALUE;
|
||||
scanInfo.iPeak = 0;
|
||||
scanInfo.fPeak = 0;
|
||||
}
|
||||
@@ -535,11 +553,18 @@ static void ResetScanStats()
|
||||
static void InitScan()
|
||||
{
|
||||
ResetScanStats();
|
||||
scanInfo.scanStep = GetScanStep();
|
||||
scanInfo.measurementsCount = GetStepsCount();
|
||||
scanInfo.i = 0;
|
||||
scanInfo.f = GetFStart();
|
||||
|
||||
scanInfo.scanStep = GetScanStep();
|
||||
scanInfo.measurementsCount = GetStepsCount();
|
||||
// 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()
|
||||
@@ -565,6 +590,9 @@ static void RelaunchScan()
|
||||
#endif
|
||||
preventKeypress = true;
|
||||
scanInfo.rssiMin = RSSI_MAX_VALUE;
|
||||
memset(peakHoldY, PEAK_HOLD_INIT, sizeof(peakHoldY));
|
||||
memset(peakHoldAge, 0, sizeof(peakHoldAge));
|
||||
|
||||
}
|
||||
|
||||
static void UpdateScanInfo()
|
||||
@@ -586,10 +614,39 @@ static void UpdateScanInfo()
|
||||
|
||||
static void AutoTriggerLevel()
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
settings.rssiTriggerLevel = clamp(scanInfo.rssiMax + 8, 0, 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;
|
||||
|
||||
if (diff > 4)
|
||||
{
|
||||
int16_t step = (diff > 12) ? 4 : ((diff > 6) ? 2 : 1);
|
||||
settings.rssiTriggerLevel += step;
|
||||
}
|
||||
else if (diff < -4)
|
||||
{
|
||||
int16_t absDiff = -diff;
|
||||
int16_t step = (absDiff > 12) ? 4 : ((absDiff > 6) ? 2 : 1);
|
||||
settings.rssiTriggerLevel -= step;
|
||||
}
|
||||
// Dead zone ±4: hold steady to avoid jitter near target
|
||||
}
|
||||
|
||||
static void UpdatePeakInfoForce()
|
||||
@@ -619,7 +676,13 @@ static void SetRssiHistory(uint16_t idx, uint16_t rssi)
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
rssiHistory[idx] = rssi;
|
||||
// Attack/decay: instant rise, fast fall for stable display
|
||||
uint16_t prev = rssiHistory[idx];
|
||||
if (rssi >= prev) {
|
||||
rssiHistory[idx] = rssi; // Attack: instant
|
||||
} else {
|
||||
rssiHistory[idx] = (prev + rssi) >> 1; // Decay: halve the gap each sweep
|
||||
}
|
||||
}
|
||||
|
||||
static void Measure()
|
||||
@@ -642,6 +705,16 @@ static void ClampRssiTriggerLevel()
|
||||
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)
|
||||
@@ -655,26 +728,6 @@ static void UpdateRssiTriggerLevel(bool inc)
|
||||
redrawStatus = true;
|
||||
}
|
||||
|
||||
static void UpdateDBMax(bool inc)
|
||||
{
|
||||
if (inc && settings.dbMax < 10)
|
||||
{
|
||||
settings.dbMax += 1;
|
||||
}
|
||||
else if (!inc && settings.dbMax > settings.dbMin)
|
||||
{
|
||||
settings.dbMax -= 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClampRssiTriggerLevel();
|
||||
redrawStatus = true;
|
||||
redrawScreen = true;
|
||||
SYSTEM_DelayMs(20);
|
||||
}
|
||||
|
||||
static void UpdateScanStep(bool inc)
|
||||
{
|
||||
@@ -688,6 +741,9 @@ static void UpdateScanStep(bool inc)
|
||||
}
|
||||
|
||||
settings.frequencyChangeStep = GetBW() >> 1;
|
||||
// Reset squelch trigger so AutoTriggerLevel() recalibrates on next sweep.
|
||||
// The filter BW changes with the step, so the old level is no longer valid.
|
||||
settings.rssiTriggerLevel = RSSI_MAX_VALUE;
|
||||
RelaunchScan();
|
||||
ResetBlacklist();
|
||||
redrawScreen = true;
|
||||
@@ -908,7 +964,19 @@ static bool IsBlacklisted(uint16_t idx)
|
||||
|
||||
// Draw things
|
||||
|
||||
// applied x2 to prevent initial rounding
|
||||
// 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;
|
||||
@@ -919,12 +987,203 @@ uint8_t Rssi2PX(uint16_t rssi, uint8_t pxMin, uint8_t pxMax)
|
||||
|
||||
int dbm = clamp(Rssi2DBm(rssi) << 1, DB_MIN, DB_MAX);
|
||||
|
||||
return ((dbm - DB_MIN) * PX_RANGE + DB_RANGE / 2) / DB_RANGE + pxMin;
|
||||
// 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)
|
||||
{
|
||||
return DrawingEndY - Rssi2PX(rssi, 0, DrawingEndY);
|
||||
// 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;
|
||||
|
||||
if (x > 0)
|
||||
{
|
||||
uint8_t n = yArr[x - 1];
|
||||
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 (x + 1 < 128)
|
||||
{
|
||||
uint8_t n = yArr[x + 1];
|
||||
if (n != SPECTRUM_TOPY_SKIP && n <= DrawingEndY)
|
||||
{
|
||||
uint8_t mid = (y0 + n + 1) >> 1;
|
||||
if (mid < *crestTop) *crestTop = mid;
|
||||
if (mid > *crestBot) *crestBot = mid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -934,69 +1193,34 @@ uint8_t Rssi2Y(uint16_t rssi)
|
||||
// max bars at 128 to correctly draw larger numbers of samples
|
||||
uint8_t bars = (steps > 128) ? 128 : steps;
|
||||
|
||||
uint8_t ox = 0;
|
||||
for (uint8_t i = 0; i < bars; ++i)
|
||||
{
|
||||
uint16_t rssi = rssiHistory[(bars>128) ? i >> settings.stepsCount : i];
|
||||
|
||||
#ifdef ENABLE_SCAN_RANGES
|
||||
uint8_t x;
|
||||
if (gScanRangeStart && bars > 1)
|
||||
{
|
||||
// Total width units = (bars - 1) full bars + 2 half bars = bars
|
||||
// First bar: half width, middle bars: full width, last bar: half width
|
||||
// Scale: 128 pixels / (bars - 1) = pixels per full bar
|
||||
uint16_t fullWidth = (128 << 8) / (bars - 1); // x256 for precision
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
x = fullWidth / (2 << 8); // half of /256 (because fullWidth is x256)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Position = half + (i-1) full bars + current bar
|
||||
x = fullWidth / (2 << 8) + (uint16_t)i * fullWidth / (1 << 8);
|
||||
if (i == bars - 1) x = 128; // Last bar ends at screen edge
|
||||
}
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
uint8_t shift_graph = 64 / steps + 1;
|
||||
x = i * 128 / bars + shift_graph;
|
||||
}
|
||||
|
||||
if (rssi != RSSI_MAX_VALUE)
|
||||
{
|
||||
for (uint8_t xx = ox; xx < x; xx++)
|
||||
{
|
||||
DrawVLine(Rssi2Y(rssi), DrawingEndY, xx, true);
|
||||
}
|
||||
}
|
||||
ox = x;
|
||||
}
|
||||
uint8_t topY[128];
|
||||
BuildSpectrumTopY(topY, bars);
|
||||
SmoothTopY(topY);
|
||||
DrawSpectrumCurve(topY);
|
||||
}
|
||||
#else
|
||||
static void DrawSpectrum()
|
||||
{
|
||||
for (uint8_t x = 0; x < 128; ++x)
|
||||
{
|
||||
uint16_t rssi = rssiHistory[x >> settings.stepsCount];
|
||||
if (rssi != RSSI_MAX_VALUE)
|
||||
{
|
||||
DrawVLine(Rssi2Y(rssi), DrawingEndY, x, true);
|
||||
}
|
||||
}
|
||||
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 P:%d T:%d", settings.dbMin, settings.dbMax,
|
||||
sprintf(String, "%d/%d%s P:%d T:%d", settings.dbMin, settings.dbMax,
|
||||
manualDbMaxTimer ? "M" : "",
|
||||
Rssi2DBm(peak.rssi), Rssi2DBm(settings.rssiTriggerLevel));
|
||||
#else
|
||||
sprintf(String, "%d/%d", settings.dbMin, settings.dbMax);
|
||||
sprintf(String, "%d/%d%s", settings.dbMin, settings.dbMax,
|
||||
manualDbMaxTimer ? "M" : "");
|
||||
#endif
|
||||
GUI_DisplaySmallest(String, 0, 1, true, true);
|
||||
|
||||
@@ -1097,6 +1321,7 @@ static void DrawNums()
|
||||
GUI_DisplaySmallest(String, 0, 1, false, true);
|
||||
sprintf(String, "%u.%02uk", GetScanStep() / 100, GetScanStep() % 100);
|
||||
GUI_DisplaySmallest(String, 0, 7, false, true);
|
||||
|
||||
}
|
||||
|
||||
if (IsCenterMode())
|
||||
@@ -1186,7 +1411,7 @@ static void OnKeyDown(uint8_t key)
|
||||
isTrue = true;
|
||||
[[fallthrough]];
|
||||
case KEY_9:
|
||||
UpdateDBMax(isTrue);
|
||||
UpdateDbMax(isTrue);
|
||||
break;
|
||||
case KEY_1:
|
||||
isTrue = true;
|
||||
@@ -1315,7 +1540,7 @@ void OnKeyDownStill(KEY_Code_t key)
|
||||
isTrue = true;
|
||||
[[fallthrough]];
|
||||
case KEY_9:
|
||||
UpdateDBMax(isTrue);
|
||||
UpdateDbMax(isTrue);
|
||||
break;
|
||||
case KEY_UP:
|
||||
nav = !nav;
|
||||
@@ -1410,7 +1635,7 @@ static void RenderStill()
|
||||
gFrameBuffer[2][i + METER_PAD_LEFT] = 0b01110000;
|
||||
}
|
||||
|
||||
uint8_t x = Rssi2PX(scanInfo.rssi, 0, 121);
|
||||
uint8_t x = Rssi2PX(rssiSmoothed, 0, 121);
|
||||
for (int i = 0; i < x; ++i)
|
||||
{
|
||||
if (i % 5)
|
||||
@@ -1419,7 +1644,7 @@ static void RenderStill()
|
||||
}
|
||||
}
|
||||
|
||||
int dbm = Rssi2DBm(scanInfo.rssi);
|
||||
int dbm = Rssi2DBm(rssiSmoothed);
|
||||
uint8_t s = DBm2S(dbm);
|
||||
sprintf(String, "S: %u", s);
|
||||
GUI_DisplaySmallest(String, 4, 25, false, true);
|
||||
@@ -1549,7 +1774,7 @@ static void Scan()
|
||||
#endif
|
||||
)
|
||||
{
|
||||
SetF(scanInfo.f);
|
||||
SetFScan(scanInfo.f);
|
||||
Measure();
|
||||
UpdateScanInfo();
|
||||
}
|
||||
@@ -1566,7 +1791,7 @@ static void UpdateScan()
|
||||
{
|
||||
Scan();
|
||||
|
||||
if (scanInfo.i + 1 < scanInfo.measurementsCount)
|
||||
if (scanInfo.i < scanInfo.measurementsCount - 1)
|
||||
{
|
||||
NextScanStep();
|
||||
return;
|
||||
@@ -1576,6 +1801,19 @@ static void UpdateScan()
|
||||
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 {
|
||||
int newMax = Rssi2DBm(scanInfo.rssiMax) + 5;
|
||||
if (newMax < settings.dbMin + 10)
|
||||
newMax = settings.dbMin + 10;
|
||||
if (newMax > 10)
|
||||
newMax = 10;
|
||||
settings.dbMax = newMax;
|
||||
}
|
||||
|
||||
redrawScreen = true;
|
||||
preventKeypress = false;
|
||||
|
||||
@@ -1597,6 +1835,9 @@ static void UpdateStill()
|
||||
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) {
|
||||
@@ -1607,15 +1848,8 @@ static void UpdateStill()
|
||||
static void UpdateListening()
|
||||
{
|
||||
preventKeypress = false;
|
||||
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
|
||||
bool tailFound = checkIfTailFound();
|
||||
if (tailFound)
|
||||
#else
|
||||
if (currentState == STILL)
|
||||
#endif
|
||||
{
|
||||
listenT = 0;
|
||||
}
|
||||
|
||||
// listenT counts down with 1ms delay per tick — no SPI during this phase.
|
||||
if (listenT)
|
||||
{
|
||||
listenT--;
|
||||
@@ -1623,6 +1857,21 @@ static void UpdateListening()
|
||||
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());
|
||||
@@ -1631,28 +1880,40 @@ static void UpdateListening()
|
||||
}
|
||||
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 = 100;
|
||||
return;
|
||||
}
|
||||
#else
|
||||
if (IsPeakOverLevel() || monitorMode)
|
||||
{
|
||||
listenT = 1000;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#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()
|
||||
@@ -1680,18 +1941,11 @@ static void Tick()
|
||||
{
|
||||
gNextTimeslice_500ms = false;
|
||||
|
||||
// if a lot of steps then it takes long time
|
||||
// we don't want to wait for whole scan
|
||||
// listening has it's own timer
|
||||
// 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)
|
||||
{
|
||||
UpdatePeakInfo();
|
||||
if (IsPeakOverLevel())
|
||||
{
|
||||
ToggleRX(true);
|
||||
TuneToPeak();
|
||||
return;
|
||||
}
|
||||
redrawScreen = true;
|
||||
preventKeypress = false;
|
||||
}
|
||||
@@ -1798,6 +2052,10 @@ void APP_RunSpectrum()
|
||||
RelaunchScan();
|
||||
|
||||
memset(rssiHistory, 0, sizeof(rssiHistory));
|
||||
memset(peakHoldY, PEAK_HOLD_INIT, sizeof(peakHoldY));
|
||||
memset(peakHoldAge, 0, sizeof(peakHoldAge));
|
||||
rssiSmoothed = 0;
|
||||
manualDbMaxTimer = 0;
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
|
||||
Reference in new issue
Block a user