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:
Indiana
2026-08-01 02:46:56 +00:00
parent 6f2905c2f0
commit da675bd198
2 changed files with 1481 additions and 149 deletions

View File

@@ -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