Skip to content

Commit 9a598ca

Browse files
committed
calibration example. rough implementation
1 parent a7cb2cd commit 9a598ca

1 file changed

Lines changed: 156 additions & 0 deletions

File tree

hackrfpy/examples/calibrate.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#! /usr/bin/python3
2+
##--------------------------------------------------------------------\
3+
# hackrfpy 'examples/calibrate.py'
4+
# Calibration WORKFLOW (Levels 2-3) on top of the library's Level-1
5+
# gain-normalized readings. This is an EXAMPLE, not library code, because
6+
# it depends on hardware you must supply: a signal of KNOWN power.
7+
#
8+
# What this can and cannot do:
9+
# * The HackRF is NOT a calibrated instrument. There is no factory
10+
# per-unit characterization of absolute power.
11+
# * Level 1 (in the library, relative_power_db): subtracts the gain chain
12+
# so readings are CONSISTENT across gain settings. Relative, honest.
13+
# * Level 3 (here): if you feed a KNOWN power into the antenna port, you
14+
# can compute a single-point OFFSET that makes readings approximately
15+
# absolute (dBm) -- good to maybe +/-2-3 dB, NOT lab-traceable, and only
16+
# valid for THIS board near the reference frequency/gain.
17+
# * Level 2 (here, optional): sweep a flat reference to characterize the
18+
# front-end's frequency response, producing a correction you can feed
19+
# back into relative_power_db(freq_correction=...).
20+
#
21+
# You need a reference source: a signal generator set to a known dBm, or a
22+
# characterized noise source. WITHOUT one, you cannot get absolute dBm --
23+
# the script will still show you gain-consistent RELATIVE readings.
24+
#
25+
# SAFETY: read-only / receive-only. Never transmits.
26+
#
27+
# Usage (single-point offset at a known reference):
28+
# uv run python examples/calibrate.py --ref-freq 100e6 --ref-dbm -30
29+
# Then use the printed offset:
30+
# h.relative_power_db(iq, offset_db=<printed offset>)
31+
##--------------------------------------------------------------------\
32+
import argparse
33+
import json
34+
import sys
35+
import numpy as np
36+
from hackrfpy import HackRF
37+
38+
39+
def measure_dbfs(h, freq, rate, n, lna, vga, amp):
40+
iq = h.capture_array(freq, rate, n, lna=lna, vga=vga, amp=amp)
41+
return h.power_dbfs(iq)
42+
43+
44+
def single_point_offset(h, args):
45+
# Level 3: feed a KNOWN dBm at ref-freq, measure dBFS, derive offset such
46+
# that approx_dbm = relative_power_db(dbfs, ..., offset_db=offset).
47+
print(f"\n== single-point offset @ {args.ref_freq/1e6:g} MHz ==")
48+
print(f" FEED A KNOWN {args.ref_dbm} dBm SIGNAL into the antenna port now.")
49+
if not args.assume_ready:
50+
input(" press Enter when the reference is connected and on... ")
51+
52+
dbfs = measure_dbfs(h, args.ref_freq, args.rate, args.samples,
53+
args.lna, args.vga, args.amp)
54+
# relative reading with zero offset = dbfs - gain_chain
55+
relative = h.relative_power_db(dbfs, lna=args.lna, vga=args.vga,
56+
amp=args.amp, offset_db=0.0)
57+
# we want: ref_dbm = relative + offset -> offset = ref_dbm - relative
58+
offset = args.ref_dbm - relative
59+
print(f" measured: {dbfs:.2f} dBFS (relative {relative:.2f} dB)")
60+
print(f" => offset_db = {offset:.2f}")
61+
print(f" sanity: relative_power_db(..., offset_db={offset:.2f}) now reads "
62+
f"~{args.ref_dbm} dBm at this freq/gain")
63+
return offset
64+
65+
66+
def freq_response_curve(h, args):
67+
# Level 2: characterize the front-end shape. IDEALLY you sweep a FLAT
68+
# reference (a broadband noise source with known-flat output). Lacking
69+
# that, this still captures the *relative* shape vs the reference freq,
70+
# which removes most of the "why is 2.4 GHz lower than 100 MHz" confound.
71+
print(f"\n== frequency-response characterization ==")
72+
print(" NOTE: only meaningful with a FLAT reference source connected.")
73+
if not args.assume_ready:
74+
input(" press Enter with the flat reference on (or Ctrl-C to skip)... ")
75+
76+
freqs = np.linspace(args.fr_min, args.fr_max, args.fr_points)
77+
table = {}
78+
ref = None
79+
for f in freqs:
80+
dbfs = measure_dbfs(h, float(f), args.rate, args.samples,
81+
args.lna, args.vga, args.amp)
82+
rel = h.relative_power_db(dbfs, lna=args.lna, vga=args.vga, amp=args.amp)
83+
if ref is None:
84+
ref = rel
85+
# correction = how much this freq reads ABOVE the reference; subtract
86+
# it to flatten. Stored as freq -> correction_dB.
87+
table[float(f)] = rel - ref
88+
print(f" {f/1e6:8.1f} MHz : {rel:7.2f} dB (corr {table[float(f)]:+.2f})")
89+
return table
90+
91+
92+
def main():
93+
p = argparse.ArgumentParser(
94+
description="HackRF calibration workflow (read-only). Levels 2-3 on "
95+
"top of the library's Level-1 relative readings.")
96+
p.add_argument("--tools-dir", default=None)
97+
p.add_argument("--rate", type=float, default=2e6)
98+
p.add_argument("--samples", type=int, default=200_000)
99+
p.add_argument("--lna", type=int, default=16)
100+
p.add_argument("--vga", type=int, default=20)
101+
p.add_argument("--amp", action="store_true")
102+
# Level 3
103+
p.add_argument("--ref-freq", type=float, default=None,
104+
help="reference frequency Hz (enables single-point offset)")
105+
p.add_argument("--ref-dbm", type=float, default=None,
106+
help="known reference power in dBm at --ref-freq")
107+
# Level 2
108+
p.add_argument("--freq-response", action="store_true",
109+
help="characterize front-end frequency response")
110+
p.add_argument("--fr-min", type=float, default=100e6)
111+
p.add_argument("--fr-max", type=float, default=1e9)
112+
p.add_argument("--fr-points", type=int, default=10)
113+
# misc
114+
p.add_argument("--assume-ready", action="store_true",
115+
help="skip the 'press Enter' prompts (reference already on)")
116+
p.add_argument("--out", default="calibration.json",
117+
help="where to save the derived calibration")
118+
args = p.parse_args()
119+
120+
h = HackRF(tools_dir=args.tools_dir)
121+
det = h.detect()
122+
if not det["ready"]:
123+
print(f"no usable HackRF: {det['problem']}", file=sys.stderr)
124+
return 1
125+
print(f"[*] board ready: firmware {det['boards'][0].get('firmware')}")
126+
print("[*] REMINDER: the HackRF is not a calibrated instrument; results "
127+
"are approximate and specific to this board.")
128+
129+
cal = {"board_serial": det["boards"][0]["serial"],
130+
"gain": {"lna": args.lna, "vga": args.vga, "amp": args.amp},
131+
"offset_db": None, "freq_correction": None}
132+
133+
if args.ref_freq is not None and args.ref_dbm is not None:
134+
cal["offset_db"] = single_point_offset(h, args)
135+
cal["offset_ref_freq_hz"] = args.ref_freq
136+
else:
137+
print("\n[*] no --ref-freq/--ref-dbm given: skipping absolute offset "
138+
"(readings stay RELATIVE, gain-consistent).")
139+
140+
if args.freq_response:
141+
try:
142+
cal["freq_correction"] = freq_response_curve(h, args)
143+
except KeyboardInterrupt:
144+
print("\n (skipped frequency-response step)")
145+
146+
with open(args.out, "w") as f:
147+
json.dump(cal, f, indent=2)
148+
print(f"\n[*] saved calibration -> {args.out}")
149+
print("[*] use it like:")
150+
print(" cal = json.load(open('calibration.json'))")
151+
print(" db = h.relative_power_db(iq, offset_db=cal['offset_db'] or 0)")
152+
return 0
153+
154+
155+
if __name__ == "__main__":
156+
sys.exit(main())

0 commit comments

Comments
 (0)