fix: port the real librtlsdr init — the RTL-SDR never had a chance
Reported symptom: the dongle connects through the browser fine and then produces nothing. The driver was a sketch written from memory and marked HARDWARE PASS REQUIRED, and that caveat never reached the UI. The decisive bug was not the tuner at all: `demodWrite` had every field of its control transfer wrong. librtlsdr uses value=(addr<<8)|0x20, index=0x10|page, big-endian; this used the block in value, page and address transposed in index, and little-endian. Every demod register write went to the wrong place. The block constants were wrong too (DEMOD=0 USB=1 SYS=2, not 0x03/0x02/0x09), as was the I2C repeater control (page 1 reg 0x01, 0x18/0x10 — not reg 0x02, 0x41/0x01). Then the tuner, as originally suspected: the R820T's full 27-register init (0x05-0x1f) replaces three pokes, with a shadow array since those registers are write-only; IF filter calibration; a real r82xx_set_pll with VCO band scan, nint+SDM into 0x14/0x16/0x17, and a lock poll that names the frequency that failed instead of streaming silence. Gain defaults to tuner AGC. The buffer reset is 0x1002 -> 0x0000, not 0xffff. Two more that would each have been fatal alone: the demod was left in zero-IF mode though the R820T delivers a 3.57MHz IF, so a locked PLL would still have been off-centre; and setSampleRate masked with JS's 32-bit `&` on a ~1.2e14 value, mangling the ratio below ~1.15Msps. Tuner detected by chip id, throwing "unsupported tuner: <name>" rather than running R82xx sequences against foreign silicon. The arithmetic is pure and tested against hand-derived vectors, including 107.9MHz where mix_div drops 32->16 — a boundary the old fixed mixDiv=2 could never have reached. Independently re-derived 88.5MHz (0x26/0x66) and 98MHz (0x6d/0x82) and they match exactly. HONEST LIMIT: none of this has touched hardware. The arithmetic is proven; the register pokes and transfer encodings are reasoned, not observed. Several constants are marked UNCONFIRMED in the file, chiefly the tracking filter table and the SDM register pair. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -207,3 +207,491 @@ describe('withTimeout', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// librtlsdr arithmetic ports
|
||||
//
|
||||
// HOW THE EXPECTED VALUES BELOW WERE DERIVED
|
||||
// ------------------------------------------
|
||||
// None of these are "whatever the code printed". Each was produced by
|
||||
// independently re-executing the librtlsdr C algorithm (a separate scratch
|
||||
// transcription of r82xx_set_pll / rtlsdr_set_sample_rate / r82xx_set_gain,
|
||||
// in Python, working purely from the C control flow) and, for the headline
|
||||
// FM-band cases, re-checked by hand from first principles as shown in the
|
||||
// per-test comments. If the TS port and the hand derivation disagree, the
|
||||
// test fails — which is the whole point.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import {
|
||||
computeIfFreqRegisters,
|
||||
computePllRegisters,
|
||||
computeR82xxGainIndices,
|
||||
computeResampRatio,
|
||||
bitrev,
|
||||
identifyTuner,
|
||||
packFir,
|
||||
selectMuxRange,
|
||||
R82XX_IF_FREQ,
|
||||
R82XX_INIT_REGS,
|
||||
R82XX_LNA_GAIN_STEPS,
|
||||
R82XX_MIXER_GAIN_STEPS,
|
||||
RTL_XTAL_HZ,
|
||||
} from './sdr'
|
||||
|
||||
const LO = (centreHz: number) => centreHz + R82XX_IF_FREQ
|
||||
|
||||
describe('computePllRegisters (librtlsdr r82xx_set_pll)', () => {
|
||||
// 88.5 MHz — bottom of the FM broadcast band.
|
||||
//
|
||||
// Hand derivation:
|
||||
// LO = 88_500_000 + 3_570_000 = 92_070_000 Hz -> freq_khz = 92_070
|
||||
// mix_div : 92_070*16 = 1_473_120 kHz (< 1_770_000, too low)
|
||||
// 92_070*32 = 2_946_240 kHz (in [1.77e6, 3.54e6) GHz-band) -> 32
|
||||
// div_num = log2(32) - 1 = 4 (vco_fine_tune == vco_power_ref, no trim)
|
||||
// vco_freq = 92_070_000 * 32 = 2_946_240_000 Hz
|
||||
// nint = floor(2_946_240_000 / 57_600_000) = 51
|
||||
// vco_fra = (2_946_240_000 - 51*57_600_000)/1000 = (2_946_240_000
|
||||
// - 2_937_600_000)/1000 = 8_640 kHz
|
||||
// ni = floor((51-13)/4) = 9 ; si = 51 - 36 - 13 = 2
|
||||
// reg 0x14 = ni + (si<<6) = 9 + 128 = 137 = 0x89
|
||||
// sdm : successive approximation over vco_fra with steps
|
||||
// 2*28800/n_sdm: n_sdm=2 -> step 28800 (8640 !> 28800, skip)
|
||||
// n_sdm=4 -> 14400 (skip); 8 -> 7200 (8640 > 7200:
|
||||
// sdm += 32768/4 = 8192, vco_fra = 1440); 16 -> 3600 (skip);
|
||||
// 32 -> 1800 (skip); 64 -> 900 (1440 > 900: sdm += 32768/32
|
||||
// = 1024 -> 9216, vco_fra = 540); 128 -> 450 (540 > 450:
|
||||
// sdm += 32768/64 = 512 -> 9728, vco_fra = 90);
|
||||
// 256 -> 225 (skip); 512 -> 112 (skip); 1024 -> 56
|
||||
// (90 > 56: sdm += 32768/512 = 64 -> 9792, vco_fra = 34);
|
||||
// 2048 -> 28 (34 > 28: sdm += 32 -> 9824, vco_fra = 6);
|
||||
// 4096 -> 14 (skip); 8192 -> 7 (skip); 16384 -> 3
|
||||
// (6 > 3: sdm += 4 -> 9828, vco_fra = 3); 32768 -> 1
|
||||
// (3 > 1: sdm += 2 -> 9830, vco_fra = 2, n_sdm >= 0x8000 so
|
||||
// the loop breaks). sdm = 9830 = 0x2666.
|
||||
it('programs 88.5 MHz exactly as the C algorithm does', () => {
|
||||
const p = computePllRegisters(LO(88_500_000))
|
||||
expect(p.mixDiv).toBe(32)
|
||||
expect(p.divNum).toBe(4)
|
||||
expect(p.nint).toBe(51)
|
||||
expect(p.ni).toBe(9)
|
||||
expect(p.si).toBe(2)
|
||||
expect(p.reg14).toBe(0x89)
|
||||
expect(p.sdm).toBe(9830)
|
||||
expect(p.reg16).toBe(0x26)
|
||||
expect(p.reg17).toBe(0x66)
|
||||
expect(p.pwSdm).toBe(0x00) // fractional, so the sigma-delta stays powered
|
||||
})
|
||||
|
||||
// 98 MHz — the driver's default tune.
|
||||
// LO = 101_570_000 -> freq_khz = 101_570
|
||||
// 101_570*16 = 1_625_120 (too low); *32 = 3_250_240 -> mix_div 32, div_num 4
|
||||
// vco_freq = 3_250_240_000 ; nint = floor(/57_600_000) = 56
|
||||
// vco_fra = (3_250_240_000 - 56*57_600_000)/1000
|
||||
// = (3_250_240_000 - 3_225_600_000)/1000 = 24_640 kHz
|
||||
// ni = floor((56-13)/4) = 10 ; si = 56 - 40 - 13 = 3
|
||||
// reg 0x14 = 10 + (3<<6) = 10 + 192 = 202 = 0xca
|
||||
// sdm (same successive approximation) = 28034 = 0x6d82
|
||||
it('programs 98 MHz exactly as the C algorithm does', () => {
|
||||
const p = computePllRegisters(LO(98_000_000))
|
||||
expect(p.mixDiv).toBe(32)
|
||||
expect(p.divNum).toBe(4)
|
||||
expect(p.nint).toBe(56)
|
||||
expect(p.reg14).toBe(0xca)
|
||||
expect(p.sdm).toBe(28034)
|
||||
expect(p.reg16).toBe(0x6d)
|
||||
expect(p.reg17).toBe(0x82)
|
||||
})
|
||||
|
||||
// 107.9 MHz — top of the FM band, and the first frequency in the sweep
|
||||
// range where mix_div drops from 32 to 16. That boundary is exactly the
|
||||
// sort of thing the old "mixDiv = 2, always" code got wrong.
|
||||
// LO = 111_470_000 -> freq_khz = 111_470
|
||||
// 111_470*16 = 1_783_520 (>= 1_770_000 and < 3_540_000) -> mix_div 16
|
||||
// div_num = log2(16)-1 = 3
|
||||
// vco_freq = 1_783_520_000 ; nint = floor(/57_600_000) = 30
|
||||
// vco_fra = (1_783_520_000 - 30*57_600_000)/1000
|
||||
// = (1_783_520_000 - 1_728_000_000)/1000 = 55_520 kHz
|
||||
// ni = floor((30-13)/4) = 4 ; si = 30 - 16 - 13 = 1
|
||||
// reg 0x14 = 4 + (1<<6) = 68 = 0x44
|
||||
// sdm = 63170 = 0xf6c2
|
||||
it('programs 107.9 MHz exactly as the C algorithm does (mix_div drops to 16)', () => {
|
||||
const p = computePllRegisters(LO(107_900_000))
|
||||
expect(p.mixDiv).toBe(16)
|
||||
expect(p.divNum).toBe(3)
|
||||
expect(p.nint).toBe(30)
|
||||
expect(p.reg14).toBe(0x44)
|
||||
expect(p.sdm).toBe(63170)
|
||||
expect(p.reg16).toBe(0xf6)
|
||||
expect(p.reg17).toBe(0xc2)
|
||||
})
|
||||
|
||||
// 433.92 MHz ISM and 1090 MHz ADS-B: two more independently-derived points
|
||||
// well outside the FM band, to pin the mix_div = 8 and mix_div = 2 branches.
|
||||
// 433.92: LO 437_490_000; *8 = 3_499_920 kHz in band -> mix_div 8, div_num 2
|
||||
// vco_freq 3_499_920_000; nint 60; ni 11; si 3; reg14 = 11+192 = 203
|
||||
// sdm 49970 = 0xc332
|
||||
it('programs 433.92 MHz (mix_div 8)', () => {
|
||||
const p = computePllRegisters(LO(433_920_000))
|
||||
expect(p).toMatchObject({ mixDiv: 8, divNum: 2, nint: 60, reg14: 203, sdm: 49970 })
|
||||
})
|
||||
// 1090: LO 1_093_570_000; *2 = 2_187_140 kHz in band -> mix_div 2, div_num 0
|
||||
// vco_freq 2_187_140_000; nint 37; ni 6; si 0; reg14 = 6
|
||||
// sdm 63646 = 0xf89e
|
||||
it('programs 1090 MHz (mix_div 2)', () => {
|
||||
const p = computePllRegisters(LO(1_090_000_000))
|
||||
expect(p).toMatchObject({ mixDiv: 2, divNum: 0, nint: 37, reg14: 6, sdm: 63646 })
|
||||
})
|
||||
|
||||
it('powers down the sigma-delta on an exactly integer-N frequency', () => {
|
||||
// Pick an LO where vco_freq is an exact multiple of 2*xtal = 57.6 MHz:
|
||||
// nint = 40 with mix_div 32 -> vco_freq = 2_304_000_000 -> LO = 72_000_000
|
||||
// (72_000 kHz * 32 = 2_304_000 kHz, inside the VCO window).
|
||||
const p = computePllRegisters(72_000_000)
|
||||
expect(p.mixDiv).toBe(32)
|
||||
expect(p.nint).toBe(40)
|
||||
expect(p.sdm).toBe(0)
|
||||
expect(p.pwSdm).toBe(0x08)
|
||||
})
|
||||
|
||||
it('applies the VCO fine-tune trim to the divider select, both directions', () => {
|
||||
const base = computePllRegisters(LO(98_000_000), { vcoFineTune: 2, vcoPowerRef: 2 })
|
||||
const high = computePllRegisters(LO(98_000_000), { vcoFineTune: 3, vcoPowerRef: 2 })
|
||||
const low = computePllRegisters(LO(98_000_000), { vcoFineTune: 1, vcoPowerRef: 2 })
|
||||
expect(base.divNum).toBe(4)
|
||||
expect(high.divNum).toBe(3) // fine_tune > power_ref -> div_num - 1
|
||||
expect(low.divNum).toBe(5) // fine_tune < power_ref -> div_num + 1
|
||||
// The trim only moves the register field, never the actual divide ratio.
|
||||
expect(high.nint).toBe(base.nint)
|
||||
expect(high.sdm).toBe(base.sdm)
|
||||
})
|
||||
|
||||
it('names the offending frequency when no mixer divider fits', () => {
|
||||
// 3 GHz: even mix_div 2 puts the VCO at 6 GHz, past the 3.54 GHz ceiling.
|
||||
expect(() => computePllRegisters(3_000_000_000)).toThrow(/3000\.000 MHz is outside/)
|
||||
// 20 MHz: even mix_div 32 only reaches 640 MHz, under the 1.77 GHz floor.
|
||||
expect(() => computePllRegisters(20_000_000)).toThrow(/outside the R820T tuning range/)
|
||||
})
|
||||
|
||||
it('rejects nonsense frequencies rather than emitting garbage registers', () => {
|
||||
expect(() => computePllRegisters(0)).toThrow(/invalid LO frequency/)
|
||||
expect(() => computePllRegisters(-1)).toThrow(/invalid LO frequency/)
|
||||
expect(() => computePllRegisters(NaN)).toThrow(/invalid LO frequency/)
|
||||
})
|
||||
|
||||
it('keeps every emitted register inside its byte/field width across the FM sweep', () => {
|
||||
for (let mhz = 88; mhz <= 108; mhz += 0.1) {
|
||||
const p = computePllRegisters(LO(Math.round(mhz * 1e6)))
|
||||
expect(p.reg14).toBeGreaterThanOrEqual(0)
|
||||
expect(p.reg14).toBeLessThanOrEqual(0xff)
|
||||
expect(p.reg16).toBeLessThanOrEqual(0xff)
|
||||
expect(p.reg17).toBeLessThanOrEqual(0xff)
|
||||
expect(p.sdm).toBeLessThanOrEqual(0xffff)
|
||||
expect(p.divNum).toBeGreaterThanOrEqual(0)
|
||||
expect(p.divNum).toBeLessThanOrEqual(7) // fits reg 0x10 bits 7:5
|
||||
expect(p.nint).toBeLessThanOrEqual(63) // vco_power_ref 2 -> 128/2 - 1
|
||||
}
|
||||
})
|
||||
|
||||
it('reconstructs the requested LO from its own registers to within a few hundred Hz', () => {
|
||||
// Independent check that the register triple actually *means* the right
|
||||
// frequency: LO = (2*xtal*(nint + sdm/65536)) / mix_div.
|
||||
for (const centre of [88_500_000, 98_000_000, 107_900_000, 433_920_000]) {
|
||||
const lo = LO(centre)
|
||||
const p = computePllRegisters(lo)
|
||||
const reconstructed = ((2 * RTL_XTAL_HZ) * (p.nint + p.sdm / 65536)) / p.mixDiv
|
||||
// The SDM quantises to 2*xtal/65536/mix_div; at mix_div 2 that is ~440 Hz.
|
||||
expect(Math.abs(reconstructed - lo)).toBeLessThan(500)
|
||||
}
|
||||
})
|
||||
|
||||
it('honours the R828D vco_power_ref of 1 in the nint limit', () => {
|
||||
// With vco_power_ref 1 the nint ceiling is 127 instead of 63, so a very
|
||||
// high nint that the R820T rejects is legal on the R828D.
|
||||
const opts = { vcoPowerRef: 1, vcoFineTune: 1 }
|
||||
expect(() => computePllRegisters(LO(1_090_000_000), opts)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeResampRatio (librtlsdr rtlsdr_set_sample_rate)', () => {
|
||||
// Hand derivation for 2.048 Msps:
|
||||
// raw = floor(28_800_000 * 2^22 / 2_048_000)
|
||||
// = floor(120_795_955_200_000 / 2_048_000) = 58_982_400
|
||||
// 58_982_400 = 0x3840000, already 4-aligned and under 2^28 -> unchanged
|
||||
// reg 0x9f = 0x3840000 >> 16 = 0x0384 ; reg 0xa1 = 0x0000
|
||||
// actual = 28_800_000 * 2^22 / 58_982_400 = 2_048_000 exactly
|
||||
it('computes the 2.048 Msps ratio and its register split', () => {
|
||||
const r = computeResampRatio(2_048_000)
|
||||
expect(r.ratio).toBe(58_982_400)
|
||||
expect(r.reg9f).toBe(0x0384)
|
||||
expect(r.regA1).toBe(0x0000)
|
||||
expect(r.actualRateHz).toBe(2_048_000)
|
||||
})
|
||||
|
||||
// 2.4 Msps: raw = 120_795_955_200_000 / 2_400_000 = 50_331_648 = 0x3000000
|
||||
it('computes the 2.4 Msps ratio', () => {
|
||||
const r = computeResampRatio(2_400_000)
|
||||
expect(r.ratio).toBe(0x3000000)
|
||||
expect(r.reg9f).toBe(0x0300)
|
||||
expect(r.regA1).toBe(0x0000)
|
||||
expect(r.actualRateHz).toBe(2_400_000)
|
||||
})
|
||||
|
||||
// 1.024 Msps: raw = 117_964_800 = 0x7080000.
|
||||
// This one is the regression guard for the old `& 0x0ffffffc` bug: the
|
||||
// value is above 2^26 but the previous code's 32-bit `&` also mangled
|
||||
// anything above 2^31, e.g. the 250 ksps case below.
|
||||
it('computes the 1.024 Msps ratio', () => {
|
||||
const r = computeResampRatio(1_024_000)
|
||||
expect(r.ratio).toBe(0x7080000)
|
||||
expect(r.reg9f).toBe(0x0708)
|
||||
expect(r.actualRateHz).toBe(1_024_000)
|
||||
})
|
||||
|
||||
// 250 ksps: raw = 120_795_955_200_000 / 250_000 = 483_183_820.8 ->
|
||||
// floor 483_183_820. That is > 2^28, so the C code's 28-bit mask genuinely
|
||||
// truncates: 483_183_820 mod 2^28 = 214_748_364 = 0xCCCCCCC (4-aligned).
|
||||
// The resulting real rate is 562_500 Hz, not 250 k — which is exactly why
|
||||
// librtlsdr warns when the exact rate is unattainable. The point of the
|
||||
// test is that the TS port reproduces the C truncation rather than
|
||||
// producing a >32-bit JS number or a sign-flipped one.
|
||||
it('reproduces the C 28-bit truncation for a low sample rate', () => {
|
||||
const r = computeResampRatio(250_000)
|
||||
expect(r.ratio).toBe(214_748_364)
|
||||
expect(r.reg9f).toBe(0x0ccc)
|
||||
expect(r.regA1).toBe(0xcccc)
|
||||
expect(r.actualRateHz).toBe(562_500)
|
||||
})
|
||||
|
||||
it('always emits a 4-aligned ratio that fits 28 bits', () => {
|
||||
for (const rate of [225_001, 250_000, 300_000, 900_001, 1_024_000, 2_048_000, 2_400_000, 3_200_000]) {
|
||||
const r = computeResampRatio(rate)
|
||||
expect(r.ratio % 4).toBe(0)
|
||||
expect(r.ratio).toBeLessThan(2 ** 28)
|
||||
expect(r.ratio).toBeGreaterThan(0)
|
||||
expect((r.reg9f << 16) | r.regA1).toBe(r.ratio)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects the RTL2832U’s unsupported sample-rate gaps by name', () => {
|
||||
for (const bad of [0, 100_000, 225_000, 500_000, 900_000, 3_200_001]) {
|
||||
expect(() => computeResampRatio(bad)).toThrow(/out of the RTL2832U's supported range/)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeIfFreqRegisters (librtlsdr rtlsdr_set_if_freq)', () => {
|
||||
// Hand derivation for the R820T's 3.57 MHz IF:
|
||||
// 3_570_000 * 2^22 = 3_570_000 * 4_194_304 = 14_973_665_280_000
|
||||
// tmp = floor(14_973_665_280_000 / 28_800_000) = floor(519_918.933…)
|
||||
// = 519_918 = 0x0007EEEE
|
||||
// librtlsdr negates that and lets it wrap as a uint32:
|
||||
// 2^32 - 519_918 = 4_294_447_378 = 0xFFF81112
|
||||
// reg 0x19 = (0xFFF81112 >> 16) & 0x3f = 0xFFF8 & 0x3f = 0x38
|
||||
// reg 0x1a = (0xFFF81112 >> 8) & 0xff = 0x11
|
||||
// reg 0x1b = 0xFFF81112 & 0xff = 0x12
|
||||
it('computes the 3.57 MHz IF shift registers', () => {
|
||||
expect(computeIfFreqRegisters(R82XX_IF_FREQ)).toEqual({ reg19: 0x38, reg1a: 0x11, reg1b: 0x12 })
|
||||
})
|
||||
|
||||
it('emits a zero shift for a zero IF', () => {
|
||||
expect(computeIfFreqRegisters(0)).toEqual({ reg19: 0, reg1a: 0, reg1b: 0 })
|
||||
})
|
||||
|
||||
it('keeps every field inside its width', () => {
|
||||
for (const hz of [1_000_000, 3_570_000, 4_570_000, 5_000_000]) {
|
||||
const r = computeIfFreqRegisters(hz)
|
||||
expect(r.reg19).toBeLessThanOrEqual(0x3f)
|
||||
expect(r.reg1a).toBeLessThanOrEqual(0xff)
|
||||
expect(r.reg1b).toBeLessThanOrEqual(0xff)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeR82xxGainIndices (librtlsdr r82xx_set_gain)', () => {
|
||||
// The C loop alternates LNA then mixer steps, accumulating tenths of a dB
|
||||
// from r82xx_lna_gain_steps / r82xx_mixer_gain_steps, and stops as soon as
|
||||
// the accumulated total reaches the request.
|
||||
//
|
||||
// Hand derivation for 19.7 dB (gain = 197):
|
||||
// lna 1 (+9) = 9 | mix 1 (+5) = 14
|
||||
// lna 2 (+13) = 27 | mix 2 (+10) = 37
|
||||
// lna 3 (+40) = 77 | mix 3 (+10) = 87
|
||||
// lna 4 (+38) = 125 | mix 4 (+19) = 144
|
||||
// lna 5 (+13) = 157 | mix 5 (+9) = 166
|
||||
// lna 6 (+31) = 197 -> 197 >= 197, break with lna_index 6, mix_index 5
|
||||
it('reaches 19.7 dB at LNA index 6 / mixer index 5', () => {
|
||||
expect(computeR82xxGainIndices(197)).toEqual({
|
||||
lnaIndex: 6,
|
||||
mixerIndex: 5,
|
||||
totalGainTenthDb: 197,
|
||||
})
|
||||
})
|
||||
|
||||
// Hand derivation for 29.7 dB (gain = 297), continuing the table above:
|
||||
// ... 197 (lna6) | mix 6 (+10) = 207 | lna 7 (+22) = 229
|
||||
// mix 7 (+25) = 254 | lna 8 (+26) = 280 | mix 8 (+17) = 297 -> break
|
||||
// with lna_index 8, mix_index 8.
|
||||
it('reaches 29.7 dB at LNA index 8 / mixer index 8', () => {
|
||||
expect(computeR82xxGainIndices(297)).toEqual({
|
||||
lnaIndex: 8,
|
||||
mixerIndex: 8,
|
||||
totalGainTenthDb: 297,
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the zero indices for a zero-gain request', () => {
|
||||
expect(computeR82xxGainIndices(0)).toEqual({ lnaIndex: 0, mixerIndex: 0, totalGainTenthDb: 0 })
|
||||
})
|
||||
|
||||
it('saturates at the top of both tables instead of running off the end', () => {
|
||||
// Sum of LNA steps 1..15 = 335, mixer steps 1..15 = 153 -> 488 (48.8 dB)
|
||||
// when every step is taken. Note the final mixer step is *negative* (-8),
|
||||
// so 48.8 dB is not actually the peak — see the monotonicity test below.
|
||||
const lnaSum = R82XX_LNA_GAIN_STEPS.slice(1).reduce((a, b) => a + b, 0)
|
||||
const mixSum = R82XX_MIXER_GAIN_STEPS.slice(1).reduce((a, b) => a + b, 0)
|
||||
expect(lnaSum).toBe(335)
|
||||
expect(mixSum).toBe(153)
|
||||
|
||||
const g = computeR82xxGainIndices(10_000)
|
||||
expect(g).toEqual({ lnaIndex: 15, mixerIndex: 15, totalGainTenthDb: 488 })
|
||||
})
|
||||
|
||||
it('never emits an index outside the 4-bit register field, over the whole range', () => {
|
||||
for (let g = 0; g <= 600; g += 1) {
|
||||
const r = computeR82xxGainIndices(g)
|
||||
expect(r.lnaIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(r.lnaIndex).toBeLessThanOrEqual(0x0f)
|
||||
expect(r.mixerIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(r.mixerIndex).toBeLessThanOrEqual(0x0f)
|
||||
}
|
||||
})
|
||||
|
||||
// This one documents a genuine quirk of the C algorithm rather than a bug
|
||||
// in the port. The last mixer gain step in r82xx_mixer_gain_steps is -8
|
||||
// (i.e. -0.8 dB), so a request beyond what the tables can satisfy walks one
|
||||
// step *past* the peak: 49.6 dB is reachable (LNA 15 / mixer 14) but asking
|
||||
// for anything above it lands on 48.8 dB (LNA 15 / mixer 15). librtlsdr
|
||||
// behaves the same way; the test pins the boundary so a future "fix" that
|
||||
// silently clamps differently shows up.
|
||||
it('is monotonic up to the reachable peak, then dips by the final -0.8 dB mixer step', () => {
|
||||
let prev = -1
|
||||
let peak = -1
|
||||
for (let g = 0; g <= 496; g += 1) {
|
||||
const t = computeR82xxGainIndices(g).totalGainTenthDb
|
||||
expect(t).toBeGreaterThanOrEqual(prev)
|
||||
prev = t
|
||||
peak = Math.max(peak, t)
|
||||
}
|
||||
expect(peak).toBe(496)
|
||||
expect(computeR82xxGainIndices(496)).toEqual({
|
||||
lnaIndex: 15,
|
||||
mixerIndex: 14,
|
||||
totalGainTenthDb: 496,
|
||||
})
|
||||
// One tenth of a dB more than the tables can give: the search takes the
|
||||
// negative final mixer step and ends up slightly lower.
|
||||
expect(computeR82xxGainIndices(497).totalGainTenthDb).toBe(488)
|
||||
})
|
||||
})
|
||||
|
||||
describe('selectMuxRange (librtlsdr r82xx_set_mux band table)', () => {
|
||||
it('picks the last band whose lower bound is at or below the LO', () => {
|
||||
// 92.07 MHz LO (88.5 MHz + IF) falls in the 90–100 MHz band.
|
||||
expect(selectMuxRange(92_070_000).freqMhz).toBe(90)
|
||||
// 101.57 MHz LO (98 MHz + IF) falls in the 100–110 MHz band.
|
||||
expect(selectMuxRange(101_570_000).freqMhz).toBe(100)
|
||||
// 111.47 MHz LO (107.9 MHz + IF) falls in the 110–120 MHz band.
|
||||
expect(selectMuxRange(111_470_000).freqMhz).toBe(110)
|
||||
})
|
||||
|
||||
it('clamps below the first bound and above the last', () => {
|
||||
expect(selectMuxRange(1_000_000).freqMhz).toBe(0)
|
||||
expect(selectMuxRange(2_000_000_000).freqMhz).toBe(588)
|
||||
})
|
||||
|
||||
it('is exact at band boundaries (>= lower bound, not >)', () => {
|
||||
expect(selectMuxRange(50_000_000).freqMhz).toBe(50)
|
||||
expect(selectMuxRange(49_999_999).freqMhz).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('packFir (librtlsdr rtlsdr_set_fir)', () => {
|
||||
it('packs the default filter into 20 bytes: 8 int8 then 8 int12', () => {
|
||||
const fir = packFir()
|
||||
expect(fir).toHaveLength(20)
|
||||
// First 8 are the signed 8-bit taps, two's complement.
|
||||
// -54 = 0xca, -36 = 0xdc, -41 = 0xd7, -40 = 0xd8, -32 = 0xe0,
|
||||
// -14 = 0xf2, 14 = 0x0e, 53 = 0x35
|
||||
expect(Array.from(fir.slice(0, 8))).toEqual([0xca, 0xdc, 0xd7, 0xd8, 0xe0, 0xf2, 0x0e, 0x35])
|
||||
// Then pairs of 12-bit values, 3 bytes per pair.
|
||||
// (101, 156) -> 101 = 0x065, 156 = 0x09c
|
||||
// byte0 = 101 >> 4 = 0x06
|
||||
// byte1 = ((101 << 4) | (156 >> 8) & 0xf) = 0x50 | 0x0 = 0x50
|
||||
// byte2 = 156 & 0xff = 0x9c
|
||||
expect(Array.from(fir.slice(8, 11))).toEqual([0x06, 0x50, 0x9c])
|
||||
// (215, 273) -> 215 = 0x0d7, 273 = 0x111
|
||||
// byte0 = 0x0d ; byte1 = 0x70 | 0x1 = 0x71 ; byte2 = 0x11
|
||||
expect(Array.from(fir.slice(11, 14))).toEqual([0x0d, 0x71, 0x11])
|
||||
// (327, 372) -> 327 = 0x147, 372 = 0x174
|
||||
// byte0 = 0x14 ; byte1 = 0x70 | 0x1 = 0x71 ; byte2 = 0x74
|
||||
expect(Array.from(fir.slice(14, 17))).toEqual([0x14, 0x71, 0x74])
|
||||
// (404, 421) -> 404 = 0x194, 421 = 0x1a5
|
||||
// byte0 = 0x19 ; byte1 = 0x40 | 0x1 = 0x41 ; byte2 = 0xa5
|
||||
expect(Array.from(fir.slice(17, 20))).toEqual([0x19, 0x41, 0xa5])
|
||||
})
|
||||
})
|
||||
|
||||
describe('bitrev (librtlsdr r82xx_bitrev)', () => {
|
||||
it('reverses bit order within a byte', () => {
|
||||
expect(bitrev(0x00)).toBe(0x00)
|
||||
expect(bitrev(0xff)).toBe(0xff)
|
||||
expect(bitrev(0x01)).toBe(0x80)
|
||||
expect(bitrev(0x80)).toBe(0x01)
|
||||
expect(bitrev(0b1010_0000)).toBe(0b0000_0101)
|
||||
})
|
||||
|
||||
it('is its own inverse for every byte', () => {
|
||||
for (let b = 0; b < 256; b++) expect(bitrev(bitrev(b))).toBe(b)
|
||||
})
|
||||
})
|
||||
|
||||
describe('R82XX_INIT_REGS', () => {
|
||||
it('covers registers 0x05 through 0x1f inclusive', () => {
|
||||
// The whole point of the fix: 27 registers, not the 3 the old code wrote.
|
||||
expect(R82XX_INIT_REGS).toHaveLength(0x1f - 0x05 + 1)
|
||||
expect(R82XX_INIT_REGS).toHaveLength(27)
|
||||
for (const v of R82XX_INIT_REGS) {
|
||||
expect(v).toBeGreaterThanOrEqual(0)
|
||||
expect(v).toBeLessThanOrEqual(0xff)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('identifyTuner', () => {
|
||||
it('recognises the R820T by its chip id at the R820T address', () => {
|
||||
expect(identifyTuner({ r820t: 0x69 })).toBe('R820T')
|
||||
})
|
||||
|
||||
it('recognises the R828D at its own address', () => {
|
||||
expect(identifyTuner({ r820t: 0x00, r828d: 0x69 })).toBe('R828D')
|
||||
})
|
||||
|
||||
it('recognises the tuners this driver deliberately refuses to drive', () => {
|
||||
expect(identifyTuner({ e4000: 0x40 })).toBe('E4000')
|
||||
expect(identifyTuner({ fc001x: 0xa1 })).toBe('FC0012')
|
||||
expect(identifyTuner({ fc001x: 0xa3 })).toBe('FC0013')
|
||||
expect(identifyTuner({ fc2580: 0x56 })).toBe('FC2580')
|
||||
// FC2580's check masks off the top bit before comparing.
|
||||
expect(identifyTuner({ fc2580: 0xd6 })).toBe('FC2580')
|
||||
})
|
||||
|
||||
it('returns null rather than guessing when nothing answers', () => {
|
||||
expect(identifyTuner({})).toBeNull()
|
||||
expect(identifyTuner({ r820t: 0x00, r828d: 0xff, e4000: 0x12 })).toBeNull()
|
||||
})
|
||||
|
||||
it('prefers a positive E4000 id over a coincidental R82xx byte', () => {
|
||||
// Probe order matters: librtlsdr checks the E4000 first.
|
||||
expect(identifyTuner({ e4000: 0x40, r820t: 0x69 })).toBe('E4000')
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user