Turn a supported USB Wi-Fi dongle into an experimental SDR receiver with 14-bit I/Q at 40 MSps complex.
BirriaSDR ships as libbirriasdr, a C11 library with an API modeled on
librtlsdr, together with a command-line recorder. It talks to the device from
userspace through libusb and includes the source of its own small device
program. Start with Build and try, or jump to the API
to add the receiver to an application. To see it working without building
anything, try the rough proof of concept.
Important
This is experimental software. The tested board starts from a channel 6 initialization profile, and the library can tune anywhere from 2156 to 2610 MHz. Sensitivity across that range, frequency accuracy and gain are not calibrated. Use at your own risk.
github-screen-recording.mp4
Wi-Fi chipsets contain a complete wideband receiver. An analog front end tunes, filters and amplifies the antenna signal, and a digital physical layer then processes the resulting complex baseband samples. Some chips also include an internal I/Q capture engine (ICAP) meant for calibration and debugging. BirriaSDR repurposes that capture path to stream raw samples to the host over USB.
flowchart LR
rf["RF/PHY chain 0"] --> icap["ICAP"]
icap --> sram["Circular SRAM"]
sram --> dma["Device DMA"]
dma --> usb["USB"]
usb --> host["Host I/Q"]
The host programs the radio front end and the capture engine directly through their registers. The capture engine writes samples into the chip's internal memory. A small original device program, only 1,120 bytes and loaded through the chip's built-in boot ROM, sends that memory to the host over USB. The library then checks the stream and hands complex samples to the application.
Nothing from the vendor is downloaded to the device, whether RAM firmware or a
ROM patch. Nothing is written to flash or eFuse either, so all configuration is
volatile and disappears when the dongle loses power. The device program's
assembly source lives in firmware/iq_stream.S, and the
library embeds its assembled image.
The tested hardware is the COMFAST MT7961/MT7921AU dongle (3574:6211),
which pairs an MT7961 digital die with an MT7972 analog die. By default the
receiver is centered on Wi-Fi channel 6 (nominally 2437 MHz) and uses receive
chain 0 at 40 million complex samples per second. The library can retune
anywhere in the 2156 to 2610 MHz range, even while streaming. At 40 MSps it
pairs the 40 MHz analog filter with the matching digital bandwidth setting.
On the tested board, the measured noise spectrum falls about 3 dB at ±18 MHz.
The filters do not fully reject signals outside the sampled span, so strong
signals there can alias into the capture. Treat this as measured behavior
rather than a calibrated, flat bandwidth guarantee.
For more detail, see:
- The receiver overview
- The MT7921 / MT7961 / MT7972 receiver reference, with controls and register tables for each stage
- The architecture guide, which also covers adding a chipset
| Parameter | Current value |
|---|---|
| Hardware | COMFAST dongle 3574:6211 with an MT7961 digital die and an MT7972 analog die |
| Frequency tuning | 2156 to 2610 MHz, including live changes. Sensitivity across the range is unverified. No 5 GHz support yet |
| Tuning step | Nominally 28.61 Hz, using the full 20-bit fraction. Requests are in whole hertz with no Wi-Fi channel restriction. The physical response to fine steps is still unmeasured |
| Sample rate | 40 MSps complex, fixed |
| Sampled bandwidth | 40 MHz span with matching nominal 40 MHz analog and digital settings. See filter characteristics for rolloff and aliasing limits |
| Sample resolution | Signed 14-bit I and Q, from −8192 to 8191 |
| Receive channels | One (chain 0) |
| Transmit | None, receive only |
| Gain | Raw codes for LNA 0 to 3, filter stage 0 to 8 and digital 0 to 12. Not calibrated in dB |
| Live gain changes | Supported without restarting capture, with a brief settling transient |
| Automatic gain | Manual overrides can be released. Full AGC control is not implemented |
| Frequency accuracy | Uncalibrated, with no ppm correction |
| Reference clock | On-board 40 MHz crystal |
| Host interface | Bulk transfers over USB 3.0 at about 161 MB/s |
| Output format | Interleaved signed 16-bit I and Q (cs16). Buffers from the API are native-endian, and recorder files are little-endian |
| Firmware | Original 1,120-byte RAM program. No vendor firmware and nothing flashed |
| Host platforms | Linux (tested on Raspberry Pi 5) and macOS (not tested with hardware) |
The CyberEther flowgraph in
cyberether/birriasdr.yml is a very rough proof of
concept. It exists so people can quickly test the dongle and watch a live
spectrum without building anything. It runs its own Python receiver, separate
from the C library, with a fixed channel 6 profile, an older 20 MHz analog
filter and no automatic recovery. Expect rough edges and runs that stop on an
overrun. For anything beyond a quick test, use the C library.
To try it, install the CyberEther desktop app with a Python 3.11 or newer runtime and select that interpreter under Settings → Runtime → Python Runtime. Connect the dongle to a USB 3.0 port, open the flowgraph and let it install its Python dependencies. The note inside the flowgraph explains the USB setup for each platform and the gain settings. On a Raspberry Pi 5, the flowgraph power-cycles the USB ports by default, which needs root and only works when the dongle is the only USB device connected.
You need a C compiler, CMake and the libusb development files. On Debian or
Ubuntu, install them with
sudo apt install build-essential cmake pkg-config libusb-1.0-0-dev.
On macOS with Homebrew, run brew install cmake pkg-config libusb.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build --output-on-failure
./build/birria_rx --list
./build/birria_rx capture.cs16 8000000 manualThe last command records eight million complex samples from device 0 with fixed
manual gain codes (LNA 2, filter stage 8, digital 0). Leave out manual to
release the gain overrides, and leave out the sample count to record until you
press Ctrl-C. The file holds little-endian signed 16-bit I, Q pairs, with
each component between −8192 and 8191. The recorder applies no averaging or
decimation, and it refuses to overwrite an existing file. Always check the exit
status. An error means the capture is incomplete or its continuity could not be
confirmed, even if the file contains samples.
To pick a different center frequency, pass it in hertz:
./build/birria_rx --frequency 2437123456 off-channel.cs16 8000000 manualBefore you start, the dongle must have no vendor ROM patch loaded, and its
Wi-Fi interface must be free for libusb to claim. A software reset does not
clear a patch that is already loaded. On Linux, stop the Wi-Fi driver from
binding automatically before you reconnect the dongle, and give your user USB
access (or run the example with sudo). On macOS, reconnect the dongle first if
another driver loaded firmware onto it. The library refuses to run while a
kernel driver is attached, and it never unloads drivers or power-cycles a USB
hub.
The library follows the familiar enumerate, open, configure, read, cancel and close pattern of librtlsdr. It does not need a Python runtime or any vendor firmware download.
The header birriasdr.h documents every function along
with its units, ownership, threading and error behavior. For a complete
application, see the recorder example. The main entry
points are:
birriasdr_get_device_count();
birriasdr_get_device_info(index, &info);
birriasdr_open(&dev, index);
birriasdr_get_tuning_range(dev, &minimum_hz, &maximum_hz);
birriasdr_set_center_freq(dev, 2437000000ULL);
birriasdr_set_sample_rate(dev, 40000000);
birriasdr_set_tuner_gain_mode(dev, 1);
birriasdr_set_gain_codes(dev, &gain);
birriasdr_read_async(dev, callback, context, 0, 0);
birriasdr_cancel_async(dev); /* From callback or another normal thread. */
birriasdr_close(dev);This outline skips error handling, so check every return value as the example does. Frequencies are 64-bit values so the API can represent future 5 GHz operation. The raw gain codes are separate from the calibrated dB gain API, which is reserved for later.
The read_async function blocks the calling thread while a dedicated
background thread keeps USB transfers queued. Callbacks receive borrowed buffers
of native-endian int16_t I and Q values, the number of complex samples, and a
sample index counted from the start of the stream. While capture runs, another
thread can change the center frequency and raw gain, read them back, or call
cancel_async. When the device reports BIRRIASDR_CAP_LIVE_TUNING and
BIRRIASDR_CAP_LIVE_GAIN, those changes take effect without restarting capture.
On the tested Raspberry Pi, a retune takes about 87 ms and disturbs the signal
phase. Samples already queued may still come from the old center after the call
returns, and the library cannot report the exact sample where the change
happened. All other settings can only change while the device is idle. See the
tuning details and
gain details. Callbacks must
return quickly. If the device or host queue overflows, the library reports an
error instead of silently dropping samples.
The read_sync function captures a fixed number of complex samples, in
multiples of 2,048, using the same USB engine. Separate calls produce separate
captures, not one continuous stream. Both read functions check every packet for
gaps, repeats, stale data and corruption, confirm the device's final state and
transfer counts, and reset the device when they finish. The statistics they
return record how far initialization got and whether the final reset failed.
Cancelling still delivers the packets the device has already sent, so callbacks
can keep arriving until read_async returns. Wait for the reading thread and
any control threads to finish before closing the handle.
The library can restart streaming after an overrun, a transient transport error or a failed live gain or frequency change. Register a gap callback before reading:
/* The on_gap callback receives a const struct birriasdr_recovery_event * and your context. */
int result = birriasdr_set_recovery(dev, 3, on_gap, context);
if (result == 0)
result = birriasdr_read_async(dev, on_samples, context, 0, 0);With this budget, the library restarts up to three times within that read call.
Each notification arrives after the failed capture has been cleaned up and
before any resumed samples. The event describes the failure, includes
diagnostics, and gives next_sample, the running count of delivered samples at
which the gap occurs. The library cannot tell how many samples were lost. Reset
any processing that depends on continuity. A successful return after recovery
does not mean the whole recording was gapless.
Recovery reapplies the last frequency and gain that were set successfully. A setter that fails still returns its error to its own caller. Cancelling explicitly prevents any further restart. When the USB connection needs repair, the library uses a software USB reset. It never switches hub power or downloads vendor firmware. Device removal, a retained vendor patch and malformed stream data are still reported as errors. If a reset invalidates the handle, close it and find the device again.
Recovery stays off until you register a callback. Finite reads and the recorder example stay strict and stop at the first error. Startup itself retries once after a transient preparation failure, before any samples are captured. See the recovery mechanics.
The offline tests run with ctest --test-dir build --output-on-failure. To
exercise a connected dongle, run ./build/test_stability --run. It checks
repeated starts and stops, tuning across the supported range, live gain and
frequency changes, and a five-minute stream. Set up the device as described
above, and see the test scope. Run
individual phases with --run restart, --run live or --run soak.
A separate command, ./build/test_recovery --run, deliberately causes overruns
and checks that a new capture works afterward, including after closing and
reopening the device. Add auto after --run to test gap notification and
automatic resumption. The ./build/test_control_recovery --run command injects
failures into live gain and tuning writes, including failed rollbacks, and
checks that automatic recovery handles them.
cmake --install build --prefix /your/prefix
cc app.c $(pkg-config --cflags --libs birriasdr) -o appAdd your prefix's lib/pkgconfig directory (or the platform's equivalent
libdir) to PKG_CONFIG_PATH. Builds produce a shared library by default. For a
static library, configure with -DBUILD_SHARED_LIBS=OFF and link with
pkg-config --static.
Each function has a comment above its definition explaining its hardware role, ordering requirements and any unresolved assumptions. Register sequences are split into clock, synthesizer, routing, gain, capture and boot operations. Fixed writes that always happen live in small ordered tables, while handshakes and timing stay explicit in code.
Normal builds use the checked-in firmware header, which comes from the original assembly. To rebuild it, install Python 3.10 or newer and GNU binutils targeting NDS32, then run:
python3 tools/rebuild_firmware.py --checkRemove --check to replace the header after changing the assembly. The script
assembles firmware/iq_stream.S, links it at address 0x00915000, and appends
the alignment padding and checksum that the boot ROM expects. Python is only
needed for this optional development step, and no vendor binary or ROM dump is
used as input.
The original library, RF code, examples and firmware are licensed under MIT. The adapted USB and ROM transport code keeps its BSD-3-Clause-Clear notices. See LICENSE, NOTICE and the retained license. The software comes without warranty, so use it at your own risk.
BirriaSDR builds on open-source drivers and earlier research into using Wi-Fi hardware for raw I/Q capture. Thanks to:
- The mt76-usb-macos Python userspace USB driver by Network Weather, which laid the foundation for BirriaSDR’s device access and firmware loading.
- The upstream mt76 Linux driver and its contributors, which the userspace work builds on.
- The W1700K SDR project by h0m3us3r, related work that turns Wi-Fi hardware into a wideband SDR and documents capture registers and sample-clock controls.
- The MtkICAPtool project by MtkWifiRev, earlier work that exposed the chipset’s internal I/Q capture capabilities.
Third-party code keeps its original licenses and copyright notices.