Disclaimer. This is independent work. It is not a product of, endorsed by, or supported by Great Scott Gadgets. It is provided "as is", without warranty of any kind, express or implied, including any warranty of merchantability, fitness for a particular purpose or accuracy. You use it entirely at your own risk. Installing modified firmware and loading custom FPGA images can leave a device unusable until it is recovered and can affect your warranty. The authors accept no liability for any damage or loss arising from its use. You are responsible for operating your equipment in accordance with the laws and regulations that apply where you are, including those on intercepting communications.
A HackRF Pro as a 15-channel Bluetooth receiver for the Ubertooth tools (BR, and LE 1M), and the
Ubertooth and libbtbb work behind it. An Ubertooth demodulates one 1 MHz channel and sends the host
one 64-byte packet per 400 symbols; libbtbb searches the symbols for access codes. Here the HackRF
Pro's FPGA does the same for 15 channels at once: a channelizer and demodulator send one 16-bit word
per symbol (one bit per channel), and libubertooth repacks the bits into Ubertooth packets. LE
channels lie on the same 1 MHz raster (the LE channel at 2402 + 2k MHz is BR channel 2k): the
window's even channels also carry LE 1M packets, which libubertooth finds in the same words. The
Ubertooth host tools then run on the Pro unchanged: listed as one more device and selected with
-U.
- Gateware: a receive-only FPGA image for the HackRF Pro's iCE40UP5K: a 16-branch polyphase filter bank and a 16-point FFT split 16 Msps into 1 MHz channels, and a per-channel differential phase demodulator makes one decision per symbol. Strong signals that also decode on neighboring channels are suppressed there ("leak suppression").
- libubertooth (the
ubertoothsubmodule, a fork of the Ubertooth project): a HackRF Pro backend that loads the image into the FPGA from RAM, answers the Ubertooth's control requests (a channel moves the 15-channel window; a sweep scans the band), turns the word stream into Ubertooth packets, and returns the board to its stock image when the program ends, however it ends. Forubertooth-btleit searches the even channels' decisions for LE advertising packets and for the packets of connections whose CONNECT_IND it saw. The fork also brings a rewritten LE following engine for the Ubertooth One firmware, LE connection recovery, a faster BR search, and hardening against forged packets. - libbtbb (the
libbtbbsubmodule, a fork): a faster, checked access code search, and adaptive frequency hopping (AFH) that the original does not handle: the hop selection of the specification, the piconet clock found without the channel map, and the map deduced from the packets and followed as the master changes it. - The firmware (
firmware/hackrf, the HackRF firmware fork also used by hackrf-gpsdo) loads FPGA images staged in RAM. Nothing is written to flash at run time.
What the Pro does not do: follow a piconet's or an LE connection's hops (a retune takes longer than a slot, so the window stays put or scans; an Ubertooth follows), LE promiscuous recovery, LE 2M or Coded, EDR payloads, or report signal levels.
| Path | Contents |
|---|---|
firmware/hackrf |
submodule: the HackRF firmware and host tools with RAM image loading |
ubertooth |
submodule: the Ubertooth fork (firmware, libubertooth with the HackRF Pro backend, tools) |
libbtbb |
submodule: the libbtbb fork |
fpga-build/ |
the gateware (Amaranth): the BR image top/br_rx.py, the channelizer dsp/br_chan.py, the image gate (check_image.py) and packer (pack.py), and the gateware tests |
fpga-build/packs/br_rx.pack |
the built, gated BR image |
br/ |
the bit-exact model of the gateware (br_fixed.py), a floating-point model (br_channelizer.py), a synthetic BR signal generator (synth.py), converters and checkers, and their tests |
prebuilt/ |
the HackRF firmware (hackrf_usb.bin for flash, hackrf_usb_fpga.dfu for RAM) and SHA256SUMS |
build.sh |
builds everything from source and compares it with the committed binaries |
- A HackRF Pro. Optionally one or more Ubertooth Ones.
- A Linux host. To build:
git,cmake,make, a C compiler,pkg-config, the development files oflibusb-1.0,arm-none-eabi-gccwith newlib (firmware), and Python 3.12 or newer withvenv(gateware and models). To install the HackRF firmware:dfu-utilorhackrf_spiflash; for the Ubertooth firmware,ubertooth-dfu(built here).
git clone --recurse-submodules https://github.com/willcode/hackrf-ubertooth.gitIn an existing clone, git submodule update --init --recursive fetches the submodules.
./build.sh outout must be empty or absent. The script clones the repository at HEAD into out/src, builds
the HackRF firmware, libhackrf, libbtbb, the Ubertooth host tools with the HackRF Pro backend, the
Ubertooth firmware and the BR image, installs the libraries and tools into out/install, and
reports whether the firmware and the image match prebuilt/ and fpga-build/packs/br_rx.pack.
--tests also runs the test suites; --venv DIR reuses a Python environment that has
fpga-build/requirements-lock.txt installed; --only builds a subset (the steps are listed at the top of the script).
The gateware environment is made with pip from the lock file and needs network access; the pinned
YoWASP Yosys and nextpnr versions make the image build deterministic.
To install the tools elsewhere, build the submodules as usual: libbtbb, then ubertooth/host with
-DUBERTOOTH_HACKRF_PACK_FILE=<this repository>/fpga-build/packs/br_rx.pack, which installs the
image as share/ubertooth/br_rx.pack. The HackRF Pro backend is built when pkg-config finds
libhackrf; the libhackrf of a distribution is enough.
HackRF Pro. The backend needs the firmware from firmware/hackrf, which can load FPGA images
into RAM. The flash firmware keeps the stock receive, transmit and sweep functions.
From RAM, nothing written: put the board in DFU mode (hold DFU, press and release RESET, release DFU) and load the RAM firmware:
dfu-util --device 1fc9:000c --alt 0 --download prebuilt/hackrf_usb_fpga.dfuIt runs until the board is reset or unpowered. A RAM firmware has no FPGA images of its own in flash, so at exit the tools cannot return the board to the stock image; reset the board for other use.
In flash: first save the current flash, then write the new firmware and reset:
hackrf_spiflash -r hackrf-pro-flash-backup.binhackrf_spiflash -w prebuilt/hackrf_usb.bin -Rhackrf_spiflash -w erases the whole flash before writing. To go back, write the backup the same
way. If the board does not start, the boot ROM's DFU mode always works: load a RAM firmware as above,
then write the backup.
Ubertooth One (for the fork's LE and other improvements; the HackRF backend does not need it): list the devices, then flash each Ubertooth by its number:
out/install/bin/ubertooth-util -Lout/install/bin/ubertooth-dfu -U 0 -d out/build/ubertooth-fw/bluetooth_rxtx.dfu -rIf an Ubertooth does not come back on USB within a few seconds, unplug it and plug it in again. The firmware reports API 1.08; the stock Ubertooth tools work with it (they print a warning about the newer API), and the tools here need it.
A qualifying HackRF Pro (the firmware above and the image installed) is listed after the Ubertooths:
ubertooth-util -LWith no Ubertooth attached the Pro is device 0; with one, device 1. Select it with -U:
ubertooth-rx -U 1 -c 39ubertooth-rx -U 1 -zubertooth-rx -U 1 -l <LAP> -u <UAP>The first receives with the 15-channel window centered on channel 39 (channels 32 to 46); the second surveys the band for piconets (with no channel, the window scans the 79 channels in six positions); the third recovers one piconet's clock and, for an AFH piconet, its channel map.
ubertooth-btle -n -U 1 -A 37LE: -A 37 and -A 39 put the window at the band's edge with the advertising channel in its outer
bin (2402 to 2416 MHz: LE channels 37 and 0 to 6; 2466 to 2480 MHz: 30 to 36 and 39); -A 38
centers it at 2425 MHz (2418 to 2432 MHz: 7 to 13 and 38). Advertising packets on those channels
are reported, and the packets of a connection whose CONNECT_IND was seen there are reported on the
window's data channels, with their direction. The window does not hop: -f behaves as -n.
Packets come in the Ubertooth firmware's follow-mode format, and -r and -q write captures as
usual.
Selecting the Pro loads the image into its FPGA from RAM. When the program ends, normally, by a
signal or by a crash, the receiver is turned off and the stock image is loaded back from flash. A
program killed with kill -9 cannot do this; the next program that selects the board turns its
receiver off and proceeds. A board that another program is using is refused and left alone. Tools
for modes the image cannot do (LE promiscuous recovery, spectrum analysis, transmitting) stop with a
message.
| Environment variable | Meaning | Default |
|---|---|---|
UBERTOOTH_HACKRF_GAIN |
lna,vga gain in dB |
16,16 |
UBERTOOTH_HACKRF_SCAN_MS |
dwell per window when scanning, ms | 100 |
UBERTOOTH_HACKRF_CENTER |
initial window center in MHz, for tools without a channel option | 2441 |
UBERTOOTH_HACKRF_LEAK |
leak suppression margin in 6 dB steps, 1–6; 0 turns it off | 3 |
UBERTOOTH_HACKRF_PACK |
another gateware pack | the installed one |
UBERTOOTH_HACKRF_LE_CENTER |
LE window center in MHz, overriding -A |
unset |
UBERTOOTH_HACKRF_LE_AA_ERRORS |
LE access address bits that may differ, 0–3 | 2 |
UBERTOOTH_HACKRF_LE_CORRECT |
1 corrects one bit error per LE packet from its CRC | 1 |
UBERTOOTH_HACKRF_LE_THRESHOLD |
1 selects the slower threshold in LE mode; 0 keeps BR's | 1 |
The HackRF's 8-bit ADC clips on strong nearby transmitters, which then decode poorly; lower the gain if a close source is missed. Weak signals need more gain.
The gateware (fpga-build/dsp/br_chan.py, image fpga-build/top/br_rx.py) runs at 16 Msps, three
clock cycles per sample:
- a 16-branch, 4-tap polyphase filter bank decimating by 8 (four SB_MAC16 blocks in dual 8 x 8 mode, one per tap row, each with its own copy of the sample ring in block RAM);
- a radix-4 16-point FFT (multiplier-free 4-point stages, twiddles on three SB_MAC16 blocks in Gauss's three-multiply form, a block-RAM transpose);
- per channel: a 5-bit normalization, a 6-bit phase table, the phase step over one symbol, a carrier-offset threshold and the choice between the two sampling phases as leaky averages, with the state in block RAM.
Gateware register 0x06 selects the output: 0 streams I/Q as the receive-only image does; 0x80 streams BR words, with bits 0–3 the "signal present" exponent threshold (0 means 6) and bits 4–6 the leak suppression margin (0 means 3, 7 turns it off). Bit 8 of a word marks the first word of each 400-symbol bank; bit k is the decision for channel center + k MHz (k ≥ 9 meaning k − 16). Register 0x04 bit 0 (the transmit oscillator enable, unused by this receive-only image) slows the carrier-offset threshold's average from a shift of 4 to 6. The faster threshold follows the data within a packet. A legacy advertisement's CRC covers up to 312 bits, and one wrong bit fails the packet: LE mode selects the slower threshold, and BR keeps the shift of 4. The firmware writes register 0x04 only at boot and for the transmit oscillator.
Leak suppression. A strong signal also decodes on nearby channels (1 MHz away it turns 2π per symbol, which the one-symbol detector cannot see) and at the bins of the tuner's ±4 MHz offset, so one packet can be found on several channels. Each frame the gateware notes the strongest channel; in the next frame, a channel within 4 of it, or at bin 4 or 12, whose level is the set margin or more below sends 0101... instead of decisions. libubertooth also treats detections of one LAP within 2 µs on different channels as one packet and keeps the copy with the fewest access code errors.
LE. Each LE channel of the window keeps its last 32 decisions; when they are within the allowed number of bits of a known access address (the advertising one on every channel, a seen connection's on data channels), the following bits are dewhitened and collected, the header gives the length, and the CRC over header and payload is checked, with one bit error corrected from the CRC's syndrome (the length byte excepted; on the primary advertising channels only a legacy length). A strong packet that also decodes on another channel is dewhitened there with the wrong sequence and fails its CRC. A data packet that starts T_IFS after the previous one on the same channel answers it: the direction alternates within an event, and any other packet opens an event and is the master's. A master packet that fails its CRC still counts; one not heard at all makes its reply read as the master's. LE windows are centered on odd frequencies, which keeps the tuner's ±4 MHz bins, muted by leak suppression, off the LE channels. On recorded advertisements the slower threshold found about two in three of the packets a software receiver decodes from the same air, against about one in three with the BR threshold.
Retunes and scanning. Banks around a retune are discarded, located by mapping host time to the word stream; later banks carry the new center.
AFH. libbtbb recovers the clock of an AFH piconet without its channel map and then deduces the
map from the packets, following its changes: a basic-sequence channel that is in use is never
remapped, and each remapped packet gives its channel's rank among the used channels. A receiver
that cannot set its own clock (the HackRF, a capture file) is trimmed in software. ubertooth-rx -A
accepts a known map.
br/br_fixed.pyis a bit-exact model of the gateware;fpga-build/test/test_br_chan.pysimulates the gateware on synthetic signals and compares every output word with the model, with the sample spacing fixed and varied (as a clock domain crossing does), and with LE mode's threshold.- libubertooth's
test_hackrf_leruns the LE search on a word stream built from bit-exact LE frames (blesim): bank edges, a dropped bank, access address and body errors, the largest PDU, connection directions with a damaged master packet. - An image built with
BR_DEBUG=1streams debug snapshots when register 0x06 is 0x8F: 128 input samples and 64 consecutive outputs of the filter bank, the first FFT stage, the twiddle stage and the FFT.br/check_snap.pyrecomputes each stage from the one before; the gateware test checks a simulated snapshot the same way. br/synth.pymakes BR bursts with a LAP's access code (checked against libbtbb's) at chosen channels and levels;br/test_br.pydecodes them with the models, andbuild.sh --testsruns them through the model intoubertooth-rx.- libbtbb's
test_afh_blindchecks the AFH clock search and map deduction on traffic from an independent implementation of the hop selection, for fixed and scanning receivers.
- Sync word errors. Each channel's threshold and sampling-phase choice are leaky averages that start fresh with each transmitter, and a change of sampling phase within a packet can slip a bit. On clean synthetic bursts, about half of the access codes come out with one or more errors (most near the start, and nearly all at one unlucky symbol timing). libbtbb's search tolerates a few errors, so these packets are still found, but the margin for weak signals is smaller than it could be. A better threshold exists as a model but does not yet fit in the FPGA.
- LE reception is limited by the same demodulator: about one LE packet in three is lost on recordings where a software receiver decodes them (see "LE"); the better threshold above would help LE too.
- No hop following, no LE 2M, Coded or promiscuous recovery, no signal levels (see "What it does").
- Sensitivity has not been compared with an Ubertooth under controlled conditions.
- The filter bank leaves the SB_MAC16 8 x 8 product registers off and registers the products in the
fabric: on the device, the top product register (
TOP_8x8_MULT_REG) did not take effect although the simulation models honor it, which put Q a cycle ahead of I. The debug snapshots found this; simulation could not. - Keep timing and placement margin. The image fills about three quarters of the iCE40UP5K's logic cells; images above about 80% that passed timing in the tools did not run correctly on the device (no register access, or wrong decisions).
- Every image must pass the gateware gate (
check_image.py) before it is loaded.SEED=nselects the placement seed; the committed image uses seed 1.
The gateware, models, scripts and documentation in this repository are under the BSD-3-Clause
license (LICENSE). The submodules keep their own licenses: the HackRF firmware (GPL-2.0), the
Ubertooth project (GPL-2.0) and libbtbb (GPL-2.0).