A platform for developing, testing, and deploying SDR waveforms in Rust.
██████╗ ██╗ ██╗██╗ ██╗
██╔══██╗██║ ██║██║ ██║
██████╔╝███████║██║ █╗ ██║ Rust 4 Waveforms
██╔══██╗╚════██║██║███╗██║ SDR Developer Studio
██║ ██║ ██║╚███╔███╔╝
╚═╝ ╚═╝ ╚═╝ ╚══╝╚══╝
R4W is a waveform development platform that brings the power of Rust to Software Defined Radio. We provide:
| Feature | Benefit |
|---|---|
| Memory Safety | No buffer overflows, data races, or undefined behavior in signal processing |
| Zero-Cost Abstractions | High-level APIs with C-level performance |
| Fearless Concurrency | Safe parallel processing for real-time DSP |
| Cross-Compilation | Single codebase for ARM, x86, embedded, and WASM |
| Cargo Ecosystem | Rich library ecosystem: FFT, linear algebra, async I/O |
| SIMD Support | Portable SIMD for vectorized operations |
| WASM Target | Run in browsers for education and demos |
| No Runtime | No garbage collection pauses - predictable real-time behavior |
R4W significantly outperforms GNU Radio on core DSP operations:
| Operation | R4W | GNU Radio | Speedup |
|---|---|---|---|
| FFT 1024-pt | 371 M samples/sec | 50 M samples/sec | 7.4x faster |
| FFT 4096-pt | 330 M samples/sec | 12 M samples/sec | 27x faster |
| FFT 2048-pt | 179 M samples/sec | ~25 M samples/sec | 7x faster |
Benchmarks: R4W with rustfft. GNU Radio baseline: i7-10700K, FFTW3+VOLK.
For teams migrating from GNU Radio, R4W provides complete C/C++ interoperability:
include/r4w.h)include/r4w.hpp)find_package(R4W) supportexamples/c/#include <r4w.hpp>
auto waveform = r4w::Waveform::bpsk(48000.0, 1200.0);
auto samples = waveform.modulate({1, 0, 1, 1, 0, 0, 1, 0});┌─────────────────────────────────────────────────────────────────────────────────┐
│ R4W Platform Stack │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Applications Layer │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │ │
│ │ │ r4w-explorer│ │ r4w │ │ r4w-web │ │ Your Waveform │ │ │
│ │ │ (GUI) │ │ (CLI) │ │ (WASM) │ │ Application │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Waveform Framework │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ LoRa │ │PSK/QAM │ │ FSK │ │ SINCGARS │ │HAVEQUICK │ │ │
│ │ │ CSS │ │BPSK/QPSK │ │ 2/4-FSK │ │ FHSS │ │UHF AM/FH │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Core Libraries │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────────────────┐ │ │
│ │ │ r4w-core │ │ r4w-sim │ │ r4w-gui (lib) │ │ │
│ │ │ DSP Kernels │ │ Channel Models│ │ Visualization Components │ │ │
│ │ │ FFT, Filters │ │ AWGN, Fading │ │ egui Widgets │ │ │
│ │ │ Coding, FEC │ │ UDP Transport │ │ Plot/Spectrum/Waterfall │ │ │
│ │ └───────────────┘ └───────────────┘ └───────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Hardware Abstraction │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────────────────┐ │ │
│ │ │ SdrDevice │ │ UDP I/Q │ │ r4w-fpga │ │ │
│ │ │ Trait │ │ Transport │ │ Xilinx Zynq mmap │ │ │
│ │ │ USRP/RTL-SDR │ │ GNU Radio │ │ Lattice FTDI/SPI │ │ │
│ │ └───────────────┘ └───────────────┘ └───────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
| Crate | Purpose | Key Features |
|---|---|---|
| r4w-core | DSP algorithms and waveform trait | FFT, chirp gen, PSK/FSK/QAM, FEC, Gray coding, benchmarking |
| r4w-sim | Channel simulation and transport | AWGN/Rayleigh/Rician, UDP I/Q, device abstraction |
| r4w-fpga | FPGA hardware acceleration | Xilinx Zynq, Lattice iCE40/ECP5, simulated backend |
| r4w-sandbox | Waveform isolation | Secure memory, namespaces, seccomp, container/VM support |
| r4w-gui | Visualization library + app | egui widgets, spectrum plots, constellation diagrams |
| r4w-cli | Command-line tool (r4w) |
TX/RX, benchmarking, remote agents, waveform simulation |
| r4w-web | WebAssembly entry point | Browser-based demo and education |
R4W supports dynamic loading of waveform plugins at runtime.
Plugins are shared libraries (.so on Linux,
.dll on Windows, .dylib on macOS) that
implement the R4W plugin ABI.
use r4w_core::plugin::{PluginInfo, PLUGIN_API_VERSION};
use std::ffi::c_char;
#[no_mangle]
pub extern "C" fn r4w_plugin_api_version() -> u32 {
PLUGIN_API_VERSION
}
#[no_mangle]
pub extern "C" fn r4w_plugin_info() -> *const PluginInfo {
static INFO: PluginInfo = PluginInfo {
name: b"my_waveform\0".as_ptr() as *const c_char,
version: b"1.0.0\0".as_ptr() as *const c_char,
description: b"Custom waveform plugin\0".as_ptr() as *const c_char,
author: b"Author Name\0".as_ptr() as *const c_char,
waveform_count: 1,
};
&INFO
}use r4w_core::plugin::PluginManager;
let mut manager = PluginManager::new();
manager.add_search_path("/usr/lib/r4w/plugins");
manager.discover_plugins()?;
for wf in manager.list_waveforms() {
println!("{}: {}", wf.name, wf.description);
}# Build the example plugin
cargo build --release -p r4w-example-plugin
# Output: target/release/libr4w_example_plugin.soEnable the plugins feature in r4w-core for real
dynamic loading:
r4w-core = { version = "0.1", features = ["plugins"] }# Clone and build
git clone https://github.com/joemooney/r4w
cd r4w
cargo build --release
# Run the GUI explorer
cargo run --bin r4w-explorer
# List available waveforms
cargo run --bin r4w -- waveform --list
# Simulate LoRa transmission
cargo run --bin r4w -- simulate --message "Hello R4W!" --snr 10.0
# Run in browser (WASM)
cd crates/r4w-web && trunk serveR4W includes 38+ waveform implementations:
Simple: CW, OOK, PPM, ADS-B
Analog: AM-Broadcast, FM-Broadcast, NBFM
Amplitude: ASK, 4-ASK
Frequency: BFSK, 4-FSK
Phase: BPSK, QPSK, 8-PSK
QAM: 16-QAM, 64-QAM, 256-QAM
Multi-carrier: OFDM
Spread: DSSS, DSSS-QPSK, FHSS, LoRa (SF7-SF12)
IoT/Radar: Zigbee (802.15.4), UWB, FMCW
HF/Military: STANAG 4285, ALE, 3G-ALE, MIL-STD-188-110
SINCGARS*, HAVEQUICK*, Link-16*, P25*
PMR: TETRA, DMR (Tier II/III)
* Framework implementations - These waveforms use a trait-based architecture where classified/proprietary components (frequency hopping algorithms, TRANSEC, voice codecs) are represented by simulator stubs. The unclassified signal processing, modulation, and framing are fully implemented. See docs/PORTING_GUIDE_MILITARY.md for details.
All tests pass across the workspace:
| Crate | Tests | Status |
|---|---|---|
| r4w-core | 421 | ✅ Pass |
| r4w-sim | 50 | ✅ Pass |
| r4w-gui | 22 | ✅ Pass |
| r4w-sandbox | 14 | ✅ Pass |
| r4w-fpga | 7 | ✅ Pass |
| r4w-ffi | 7 | ✅ Pass |
| r4w-example-plugin | 6 | ✅ Pass |
| Total | 527 | ✅ All Pass |
Run tests: cargo test --workspace
Measured with tokei:
| Language | Code Lines | Files | Purpose |
|---|---|---|---|
| Rust | 66,572 | 217 | Core implementation (79%) |
| Coq | 6,324 | 27 | Formal verification proofs |
| YAML | 3,445 | 11 | Requirements, configs, waveform specs |
| HTML/CSS/JS | 3,378 | 4 | Web interface |
| C/C++ | 2,037 | 5 | FFI bindings, hardware interfaces |
| Makefile | 829 | 4 | Build automation |
| TOML | 494 | 17 | Cargo configs |
| CMake | 402 | 12 | C/C++ build system |
| TCL | 198 | 4 | FPGA tooling scripts |
| Total | 84,467 | 359 |
Highlights: - Rust dominates (79%) as expected for an SDR platform - 6,324 lines of Coq proofs demonstrates commitment to formal verification - Extensive documentation: 9,690 comment lines in Markdown code blocks - Test density: 527 tests covering 66k lines = ~1 test per 126 lines of Rust
LICENSE and
THIRD_PARTY.md with proper attributionsThe Waveform Wizard is an interactive GUI tool for designing new waveforms and generating AI implementation prompts.
cargo run --bin r4w-explorer and navigate to “Waveform
Wizard”| Mode | Extension | Contents | Use Case |
|---|---|---|---|
| Spec Only | .yaml |
Just the waveform specification | Storage, reference, manual implementation |
| With Prompt | .md |
Full R4W context + specification | AI-assisted implementation with Claude |
When implementing a new waveform, two registrations are needed:
mod.rs): Makes
waveform available to CLI and coreapp.rs): Makes
waveform appear in GUI dropdownThe implementation prompt documents both registration points.
| File | Purpose |
|---|---|
waveform-spec/schema.yaml |
Complete specification schema |
waveform-spec/IMPLEMENTATION_PROMPT.md |
AI implementation context template |
waveform-spec/examples/ |
Example waveform specifications |
This section covers how to implement new waveforms in R4W.
Every waveform implements the Waveform trait from
r4w-core:
pub trait Waveform: Send + Sync {
/// Get information about this waveform
fn info(&self) -> WaveformInfo;
/// Modulate bits to I/Q samples
fn modulate(&self, bits: &[bool]) -> Vec<IQSample>;
/// Demodulate I/Q samples to bits
fn demodulate(&self, samples: &[IQSample]) -> Vec<bool>;
/// Get constellation points for visualization
fn constellation_points(&self) -> Vec<IQSample>;
/// Get educational pipeline stages
fn get_modulation_stages(&self, bits: &[bool]) -> Vec<ModulationStage>;
fn get_demodulation_steps(&self, samples: &[IQSample]) -> Vec<DemodulationStep>;
}
pub struct WaveformInfo {
pub name: &'static str, // Short name (e.g., "QPSK")
pub full_name: &'static str, // Full name (e.g., "Quadrature Phase Shift Keying")
pub bits_per_symbol: u8, // Spectral efficiency
pub sample_rate: f64, // Operating sample rate
pub symbol_rate: f64, // Symbol rate in Hz
pub carries_data: bool, // Does this waveform carry data?
}// crates/r4w-core/src/waveform/my_waveform.rs
use crate::types::IQSample;
use crate::waveform::{Waveform, WaveformInfo, ModulationStage, DemodulationStep};
pub struct MyWaveform {
sample_rate: f64,
symbol_rate: f64,
// ... your parameters
}
impl MyWaveform {
pub fn new(sample_rate: f64) -> Self {
Self {
sample_rate,
symbol_rate: sample_rate / 10.0, // 10 samples per symbol
}
}
}impl Waveform for MyWaveform {
fn info(&self) -> WaveformInfo {
WaveformInfo {
name: "MyWave",
full_name: "My Custom Waveform",
bits_per_symbol: 2,
sample_rate: self.sample_rate,
symbol_rate: self.symbol_rate,
carries_data: true,
}
}
fn modulate(&self, bits: &[bool]) -> Vec<IQSample> {
let samples_per_symbol = (self.sample_rate / self.symbol_rate) as usize;
let mut samples = Vec::new();
// Process bits in groups based on bits_per_symbol
for chunk in bits.chunks(2) {
let symbol = match (chunk.get(0), chunk.get(1)) {
(Some(&false), Some(&false)) => IQSample::new(-1.0, -1.0),
(Some(&false), Some(&true)) => IQSample::new(-1.0, 1.0),
(Some(&true), Some(&false)) => IQSample::new( 1.0, -1.0),
(Some(&true), Some(&true)) => IQSample::new( 1.0, 1.0),
_ => IQSample::new(1.0, 1.0),
};
// Repeat symbol for samples_per_symbol
samples.extend(std::iter::repeat(symbol).take(samples_per_symbol));
}
samples
}
fn demodulate(&self, samples: &[IQSample]) -> Vec<bool> {
let samples_per_symbol = (self.sample_rate / self.symbol_rate) as usize;
let mut bits = Vec::new();
for chunk in samples.chunks(samples_per_symbol) {
// Average samples in symbol period
let avg = chunk.iter().fold(IQSample::new(0.0, 0.0), |acc, s| {
IQSample::new(acc.re + s.re, acc.im + s.im)
});
let avg = IQSample::new(
avg.re / chunk.len() as f32,
avg.im / chunk.len() as f32,
);
// Decision regions
bits.push(avg.re > 0.0); // Bit 0
bits.push(avg.im > 0.0); // Bit 1
}
bits
}
fn constellation_points(&self) -> Vec<IQSample> {
vec![
IQSample::new(-1.0, -1.0), // 00
IQSample::new(-1.0, 1.0), // 01
IQSample::new( 1.0, -1.0), // 10
IQSample::new( 1.0, 1.0), // 11
]
}
fn get_modulation_stages(&self, bits: &[bool]) -> Vec<ModulationStage> {
// Return educational visualization stages
vec![
ModulationStage {
name: "Bit Grouping".to_string(),
description: "Group bits into dibits".to_string(),
samples: vec![], // Intermediate data
},
ModulationStage {
name: "Symbol Mapping".to_string(),
description: "Map dibits to constellation points".to_string(),
samples: self.modulate(bits),
},
]
}
fn get_demodulation_steps(&self, samples: &[IQSample]) -> Vec<DemodulationStep> {
vec![
DemodulationStep {
name: "Symbol Detection".to_string(),
description: "Find nearest constellation point".to_string(),
data: samples.to_vec(),
bits: self.demodulate(samples),
},
]
}
}// In crates/r4w-core/src/waveform/factory.rs
impl WaveformFactory {
pub fn create(name: &str, sample_rate: f64) -> Option<Box<dyn Waveform>> {
match name.to_uppercase().as_str() {
// ... existing waveforms ...
"MYWAVE" | "MY-WAVE" => Some(Box::new(MyWaveform::new(sample_rate))),
_ => None,
}
}
pub fn list() -> &'static [&'static str] {
&[
// ... existing ...
"MyWave",
]
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_roundtrip() {
let waveform = MyWaveform::new(48000.0);
let original_bits = vec![true, false, true, true, false, false];
let samples = waveform.modulate(&original_bits);
let recovered_bits = waveform.demodulate(&samples);
assert_eq!(original_bits, recovered_bits);
}
#[test]
fn test_with_noise() {
use r4w_sim::channel::AwgnChannel;
let waveform = MyWaveform::new(48000.0);
let bits = vec![true; 100];
let samples = waveform.modulate(&bits);
let noisy = AwgnChannel::new(10.0).apply(&samples); // 10 dB SNR
let recovered = waveform.demodulate(&noisy);
let errors = bits.iter().zip(&recovered)
.filter(|(a, b)| a != b)
.count();
let ber = errors as f64 / bits.len() as f64;
assert!(ber < 0.1, "BER too high: {:.2}%", ber * 100.0);
}
}This section covers how to port existing waveform implementations to R4W.
Look for these patterns in the original code: -
modulate(), encode(), tx() →
maps to Waveform::modulate() -
demodulate(), decode(), rx()
→ maps to Waveform::demodulate() - Complex number types
→ use num_complex::Complex<f32> or
IQSample
| C/C++ Pattern | Rust Equivalent |
|---|---|
float* samples |
&[IQSample] or
Vec<IQSample> |
malloc/free |
Stack allocation or Vec |
fftw_plan |
rustfft::FftPlanner |
memcpy |
.clone() or .copy_from_slice() |
| Raw pointer arithmetic | Iterator methods |
#define SYMBOL_SIZE 128 |
const SYMBOL_SIZE: usize = 128; |
Original C code:
void fsk_modulate(float* samples, int* bits, int n_bits, float sample_rate) {
float f0 = 1200.0, f1 = 2200.0;
int samples_per_bit = (int)(sample_rate / 300.0); // 300 baud
for (int i = 0; i < n_bits; i++) {
float freq = bits[i] ? f1 : f0;
for (int j = 0; j < samples_per_bit; j++) {
int idx = i * samples_per_bit + j;
float t = (float)idx / sample_rate;
samples[idx * 2] = cosf(2 * M_PI * freq * t); // I
samples[idx * 2 + 1] = sinf(2 * M_PI * freq * t); // Q
}
}
}Ported Rust code:
use std::f32::consts::PI;
use num_complex::Complex;
pub struct BfskWaveform {
sample_rate: f64,
baud_rate: f64,
f0: f32, // Mark frequency
f1: f32, // Space frequency
}
impl BfskWaveform {
pub fn new(sample_rate: f64) -> Self {
Self {
sample_rate,
baud_rate: 300.0,
f0: 1200.0,
f1: 2200.0,
}
}
}
impl Waveform for BfskWaveform {
fn modulate(&self, bits: &[bool]) -> Vec<IQSample> {
let samples_per_bit = (self.sample_rate / self.baud_rate) as usize;
bits.iter()
.enumerate()
.flat_map(|(bit_idx, &bit)| {
let freq = if bit { self.f1 } else { self.f0 };
(0..samples_per_bit).map(move |j| {
let sample_idx = bit_idx * samples_per_bit + j;
let t = sample_idx as f32 / self.sample_rate as f32;
let phase = 2.0 * PI * freq * t;
IQSample::new(phase.cos(), phase.sin())
})
})
.collect()
}
fn demodulate(&self, samples: &[IQSample]) -> Vec<bool> {
// Use Goertzel algorithm for each frequency
let samples_per_bit = (self.sample_rate / self.baud_rate) as usize;
samples.chunks(samples_per_bit)
.map(|chunk| {
let power_f0 = goertzel_power(chunk, self.f0, self.sample_rate as f32);
let power_f1 = goertzel_power(chunk, self.f1, self.sample_rate as f32);
power_f1 > power_f0
})
.collect()
}
// ... rest of trait implementation
}GNU Radio blocks can be ported by:
gr_complex → Complex<f32> /
IQSamplepmt::pmt_t → Rust enums or structswork() function → iterator-based processingGNU Radio Python block:
class my_block(gr.sync_block):
def work(self, input_items, output_items):
in0 = input_items[0]
out = output_items[0]
for i in range(len(in0)):
out[i] = in0[i] * self.gain
return len(out)Rust equivalent:
pub struct GainBlock {
gain: f32,
}
impl GainBlock {
pub fn new(gain: f32) -> Self {
Self { gain }
}
pub fn process(&self, input: &[IQSample]) -> Vec<IQSample> {
input.iter()
.map(|s| IQSample::new(s.re * self.gain, s.im * self.gain))
.collect()
}
// For streaming/real-time, use an iterator adapter:
pub fn process_stream<'a>(&'a self, input: impl Iterator<Item = IQSample> + 'a)
-> impl Iterator<Item = IQSample> + 'a
{
input.map(move |s| IQSample::new(s.re * self.gain, s.im * self.gain))
}
}R4W is designed for FPGA acceleration, with Xilinx Zynq as the primary target platform.
| Priority | Platform | Use Case | Status |
|---|---|---|---|
| 1st | Xilinx Zynq | Production SDR, high-performance | Active Development |
| 2nd | Lattice iCE40/ECP5 | Low-cost prototyping, education | Implemented |
| 3rd | Intel/Altera | Enterprise, high-end | Future |
| 4th | LiteX SoC | Open-source FPGA SoCs | Exploratory |
The Zynq combines ARM Cortex-A cores with FPGA fabric, making it ideal for R4W:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Xilinx Zynq SoC │
├─────────────────────────────────┬───────────────────────────────────────────┤
│ Processing System (PS) │ Programmable Logic (PL) │
│ ┌───────────────────────────┐ │ ┌─────────────────────────────────────┐ │
│ │ ARM Cortex-A9/A53 │ │ │ DSP Accelerators │ │
│ │ Running Linux + R4W │ │ │ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ │ │ │ FFT Core │ │ Chirp Correlator │ │ │
│ │ ┌─────────────────────┐ │ │ │ │ (Radix-4)│ │ (LoRa demod) │ │ │
│ │ │ r4w-core │ │ │ │ └──────────┘ └──────────────────┘ │ │
│ │ │ r4w-cli │◄─┼──┼──┤ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ r4w-explorer │ │ │ │ │FIR Filter│ │ Symbol Detector │ │ │
│ │ └─────────────────────┘ │ │ │ │(up to │ │ (matched filter) │ │ │
│ │ │ │ │ │ │ 256 taps)│ │ │ │ │
│ │ │ mmap() │ │ │ └──────────┘ └──────────────────┘ │ │
│ │ ▼ │ │ │ ┌──────────┐ ┌──────────────────┐ │ │
│ │ ┌─────────────────────┐ │ │ │ │ NCO/DDS │ │ CORDIC │ │ │
│ │ │ /dev/mem │ │ │ │ │(carrier) │ │ (sin/cos) │ │ │
│ │ │ /dev/uio* │◄─┼──┼──┤ └──────────┘ └──────────────────┘ │ │
│ │ └─────────────────────┘ │ │ └─────────────────────────────────────┘ │
│ │ │ │ │ ▲ │ │
│ └───────────┼───────────────┘ │ │ ▼ │
│ │ │ ┌─────────────────────────────────────┐ │
│ │ AXI-Lite │ │ AXI DMA Engine │ │
│ └──────────────────┼──┤ (scatter-gather, up to 8 channels) │ │
│ │ └─────────────────────────────────────┘ │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ ┌─────────────┐ ┌─────────────────────┐ │
│ │ │ ADC/DAC │ │ RF Frontend │ │
│ │ │ Interface │ │ (AD9361/AD9364) │ │
│ │ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────┴───────────────────────────────────────────┘
| Board | Zynq Device | Use Case | Approx. Cost |
|---|---|---|---|
| PYNQ-Z2 | Zynq-7020 | Learning, prototyping | $120 |
| ZedBoard | Zynq-7020 | Development | $500 |
| ADALM-PLUTO | Zynq-7010 + AD9363 | Complete SDR | $150 |
| ZCU102 | Zynq UltraScale+ | High-performance | $3,000 |
| Red Pitaya | Zynq-7010 | Test equipment + SDR | $300 |
/// r4w-fpga crate (planned)
pub mod zynq {
use std::fs::OpenOptions;
use std::os::unix::io::AsRawFd;
/// Memory-mapped register access via /dev/mem
pub struct ZynqMmap {
base_addr: usize,
size: usize,
ptr: *mut u8,
}
impl ZynqMmap {
/// Map FPGA registers into userspace
pub fn new(base_addr: usize, size: usize) -> Result<Self, std::io::Error> {
let fd = OpenOptions::new()
.read(true)
.write(true)
.open("/dev/mem")?;
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
size,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED,
fd.as_raw_fd(),
base_addr as libc::off_t,
)
};
if ptr == libc::MAP_FAILED {
return Err(std::io::Error::last_os_error());
}
Ok(Self { base_addr, size, ptr: ptr as *mut u8 })
}
/// Write to FPGA register
pub fn write_reg(&self, offset: usize, value: u32) {
unsafe {
let reg = self.ptr.add(offset) as *mut u32;
std::ptr::write_volatile(reg, value);
}
}
/// Read from FPGA register
pub fn read_reg(&self, offset: usize) -> u32 {
unsafe {
let reg = self.ptr.add(offset) as *const u32;
std::ptr::read_volatile(reg)
}
}
}
/// UIO-based interrupt handling
pub struct ZynqUio {
fd: std::fs::File,
irq_count: u32,
}
impl ZynqUio {
pub fn new(uio_device: &str) -> Result<Self, std::io::Error> {
let fd = OpenOptions::new()
.read(true)
.write(true)
.open(uio_device)?;
Ok(Self { fd, irq_count: 0 })
}
/// Wait for interrupt from FPGA
pub fn wait_irq(&mut self) -> Result<u32, std::io::Error> {
use std::io::Read;
let mut buf = [0u8; 4];
self.fd.read_exact(&mut buf)?;
self.irq_count = u32::from_ne_bytes(buf);
Ok(self.irq_count)
}
}
}Located in vivado/ip/:
| IP Core | Function | Interface | Resource Est. | Status |
|---|---|---|---|---|
r4w_fft |
1024-pt Radix-4 FFT | AXI-Stream + AXI-Lite | ~15k LUTs | ✅ Done |
r4w_fir |
256-tap FIR filter | AXI-Stream + AXI-Lite | ~8k LUTs | ✅ Done |
r4w_chirp_gen |
LoRa chirp generator | AXI-Lite + AXI-Stream | ~2k LUTs | ✅ Done |
r4w_chirp_corr |
Chirp correlator | AXI-Stream + AXI-Lite | ~12k LUTs | ✅ Done |
r4w_nco |
Numerically Controlled Oscillator | AXI-Lite | ~1.5k LUTs | ✅ Done |
r4w_dma |
DMA controller for PS↔︎PL | AXI-Stream + AXI-Lite | ~3k LUTs | ✅ Done |
Build with Vivado:
cd vivado
vivado -mode batch -source scripts/build_project.tcl
vivado -mode batch -source scripts/build_bitstream.tclDeploy to PYNQ-Z2:
scp output/r4w_design.bit xilinx@pynq:/home/xilinx/
# On PYNQ:
sudo fpgautil -b r4w_design.bit -o r4w-overlay.dtboLattice FPGAs are ideal for low-cost, low-power applications and education.
| Family | Logic Cells | DSP Blocks | Use Case | Open Tools? |
|---|---|---|---|---|
| iCE40 UP5K | 5,280 | 8 DSPs | IoT, battery-powered | Yes (Yosys+nextpnr) |
| iCE40 HX8K | 7,680 | 0 | Prototyping | Yes |
| ECP5 | 12k-85k | 12-156 DSPs | Mid-range SDR | Yes (Yosys+nextpnr) |
| CrossLink-NX | 17k-40k | 28-56 DSPs | High-speed I/O | Partial |
| Certus-NX | 17k-39k | 28-56 DSPs | General purpose | No |
Open-Source Toolchain: iCE40 and ECP5 work with fully open-source tools:
Low Cost: iCE40 UP5K boards start at $12 (Upduino), ECP5 at $45 (OrangeCrab)
USB Programming: Simple iceprog
or openFPGALoader - no JTAG dongles
Rust Integration: The open toolchain integrates well with Rust build systems
| Board | FPGA | Features | Approx. Cost |
|---|---|---|---|
| Upduino v3 | iCE40 UP5K | 5k LUTs, 1Mbit SPRAM, RGB LED | $12 |
| iCEBreaker | iCE40 UP5K | PMOD, USB-C | $70 |
| OrangeCrab | ECP5-25F | 24k LUTs, DDR3, USB-C | $45 |
| ULX3S | ECP5-12F/85F | WiFi, HDMI, buttons | $100-200 |
| Colorlight i5 | ECP5-25F | Cheap (LED controller) | $15 |
/// r4w-fpga crate (planned) - Lattice support
pub mod lattice {
use std::io::{Read, Write};
/// SPI-based communication with iCE40/ECP5
pub struct LatticeSpi {
device: String,
speed_hz: u32,
}
impl LatticeSpi {
pub fn new(spi_device: &str, speed_hz: u32) -> Result<Self, std::io::Error> {
// Uses spidev for Linux SPI access
Ok(Self {
device: spi_device.to_string(),
speed_hz,
})
}
/// Send samples to FPGA for processing
pub fn send_samples(&mut self, samples: &[u8]) -> Result<(), std::io::Error> {
// SPI transaction to FPGA
todo!()
}
/// Receive processed samples from FPGA
pub fn recv_samples(&mut self, buffer: &mut [u8]) -> Result<usize, std::io::Error> {
todo!()
}
}
/// USB-based communication via FTDI
pub struct LatticeFtdi {
// Uses libftdi or ftd2xx
}
}# Example: Building FPGA bitstream with open tools
YOSYS := yosys
NEXTPNR := nextpnr-ice40
ICEPACK := icepack
ICEPROG := iceprog
# Synthesize Verilog to JSON
%.json: %.v
$(YOSYS) -p "synth_ice40 -top top -json $@" $<
# Place and route
%.asc: %.json %.pcf
$(NEXTPNR) --up5k --package sg48 --json $< --pcf $(word 2,$^) --asc $@
# Generate bitstream
%.bin: %.asc
$(ICEPACK) $< $@
# Program FPGA
program: design.bin
$(ICEPROG) $<Located in lattice/ip/:
| IP Core | FPGA | Function | Est. LUTs | Status |
|---|---|---|---|---|
r4w_spi_slave |
iCE40/ECP5 | SPI slave interface | ~200 | Done |
r4w_nco |
iCE40/ECP5 | NCO/DDS (LUT or CORDIC) | ~200 | Done |
r4w_chirp_gen |
iCE40/ECP5 | LoRa chirp generator | ~500 | Done |
Build with open-source toolchain:
cd lattice/scripts
make ice40 # Build for iCE40-HX8K
make ecp5 # Build for ECP5-25K
make sim # Run simulation
make lint # Verilator lint checkFuture Lattice IP: | IP Core | FPGA | Function |
Est. LUTs | |———|——|———-|———–| | r4w_fft_256 |
iCE40/ECP5 | 256-pt FFT | ~3k | | r4w_fir_32 | iCE40 |
32-tap FIR | ~500 | | r4w_fir_128 | ECP5 | 128-tap FIR
| ~2k |
All FPGA platforms implement a common trait:
/// Trait for FPGA-accelerated operations
pub trait FpgaAccelerator: Send + Sync {
/// Get platform info
fn info(&self) -> FpgaInfo;
/// Check if FPGA is available and configured
fn is_available(&self) -> bool;
/// Get FPGA capabilities
fn capabilities(&self) -> FpgaCapabilities;
/// Offload FFT computation
fn fft(&self, samples: &[IQSample], inverse: bool) -> Result<Vec<IQSample>, FpgaError>;
/// Offload FIR filtering
fn fir_filter(&self, samples: &[IQSample], taps: &[f32]) -> Result<Vec<IQSample>, FpgaError>;
/// Offload complete modulation
fn modulate(&self, waveform_id: u32, bits: &[bool]) -> Result<Vec<IQSample>, FpgaError>;
/// Offload complete demodulation
fn demodulate(&self, waveform_id: u32, samples: &[IQSample]) -> Result<Vec<bool>, FpgaError>;
/// Stream processing (for real-time)
fn start_stream(&mut self, config: StreamConfig) -> Result<StreamHandle, FpgaError>;
fn stop_stream(&mut self, handle: StreamHandle) -> Result<(), FpgaError>;
}
pub struct FpgaInfo {
pub platform: FpgaPlatform,
pub device: String,
pub bitstream_version: Option<String>,
}
pub enum FpgaPlatform {
XilinxZynq { part: String },
LatticeIce40 { variant: String },
LatticeEcp5 { variant: String },
IntelCyclone { variant: String },
Other(String),
}
pub struct FpgaCapabilities {
pub max_fft_size: usize,
pub max_fir_taps: usize,
pub supported_waveforms: Vec<String>,
pub dma_buffer_size: usize,
pub clock_frequency_hz: u64,
pub dsp_blocks: usize,
pub logic_cells: usize,
}R4W DSP kernels are designed to be HLS-friendly for Vitis HLS (Xilinx):
// DSP kernel designed for potential HLS transpilation
#[inline(never)] // Preserve function boundary for HLS
pub fn chirp_correlate_kernel(
samples: &[IQSample; 1024],
chirp: &[IQSample; 1024],
) -> [IQSample; 1024] {
let mut result = [IQSample::new(0.0, 0.0); 1024];
// HLS pragma: pipeline this loop
for i in 0..1024 {
result[i] = IQSample::new(
samples[i].re * chirp[i].re + samples[i].im * chirp[i].im,
samples[i].im * chirp[i].re - samples[i].re * chirp[i].im,
);
}
result
}For Lattice (no HLS), we provide hand-written Verilog:
// r4w_correlator.v - Hand-optimized for iCE40/ECP5
module r4w_correlator #(
parameter N = 256
) (
input wire clk,
input wire rst,
input wire valid_in,
input wire [31:0] sample_re, // Q15.16 fixed-point
input wire [31:0] sample_im,
input wire [31:0] chirp_re,
input wire [31:0] chirp_im,
output reg valid_out,
output reg [31:0] result_re,
output reg [31:0] result_im
);
// Complex multiply: (a+bi)(c+di) = (ac-bd) + (ad+bc)i
always @(posedge clk) begin
if (rst) begin
valid_out <= 0;
end else if (valid_in) begin
result_re <= (sample_re * chirp_re - sample_im * chirp_im) >>> 16;
result_im <= (sample_re * chirp_im + sample_im * chirp_re) >>> 16;
valid_out <= 1;
end else begin
valid_out <= 0;
end
end
endmoduleReal-world benchmark data from distributed TX/RX testing (Pi 500 → Pi 3, 125 kHz):
| Metric | BPSK | QPSK | LoRa (SF7) |
|---|---|---|---|
| Throughput | 85,771 Sps | 77,224 Sps | 83,129 Sps |
| Bits/symbol | 1 | 2 | 7 |
| Demod rate | 168 bps | 228 bps | 322 bps |
| Avg latency | 29 μs | 38 μs | 395 μs |
| P99 latency | 56 μs | 61 μs | 565 μs |
| SNR | BPSK | QPSK | LoRa (SF7) |
|---|---|---|---|
| 20 dB | 1,296 | 1,861 | 2,616 |
| 10 dB | 1,266 | 1,895 | 2,620 |
| 5 dB | 1,336 | 1,904 | 2,580 |
| 0 dB | 1,221 | 1,892 | 2,428 |
| Parameter | SF7 | SF12 | Ratio |
|---|---|---|---|
| Symbol time @ 125kHz | 1.02 ms | 32.77 ms | 32x slower |
| Data rate | ~5.5 kbps | ~293 bps | 19x slower |
| Processing gain | 7 dB | 21 dB | +14 dB |
| Typical range | 2-5 km | 10-15 km | ~3x farther |
Deploy R4W agents to Raspberry Pis for distributed TX/RX testing:
┌─────────────────┐ ┌─────────────────┐
│ Development │ │ TX Agent │
│ Machine │ TCP │ (Raspberry Pi)│
│ │ ────────│ │
│ r4w-explorer │ 6000 │ r4w agent │
│ (GUI) │ │ │
│ │ └────────┬────────┘
│ │ │ UDP I/Q
│ │ ┌────────▼────────┐
│ │ TCP │ RX Agent │
│ │ ────────│ (Raspberry Pi)│
│ │ 6000 │ r4w agent │
└─────────────────┘ └─────────────────┘
# Build for ARM
make build-cli-arm64
# Deploy to both Pis
make deploy-both TX_HOST=joe@192.168.1.100 RX_HOST=joe@192.168.1.101
# Start testing from GUI or CLI
r4w remote -a 192.168.1.100 start-tx -w BPSK -t 192.168.1.101:5000
r4w remote -a 192.168.1.101 start-rx -w BPSK -p 5000R4W addresses a critical need in SDR deployments:
preventing interference between waveforms and
separating sensitive communications. The
r4w-sandbox crate provides 8 levels of isolation, from
basic memory safety to complete air-gapped systems.
┌────────────────────────────────────────────────────────────────────────────┐
│ Isolation Levels │
├────────────────────────────────────────────────────────────────────────────┤
│ │
│ L1 ┌──────────────┐ Rust memory safety TURN-KEY │
│ │ No Sandbox │ - Zero-cost, always-on │
│ └──────────────┘ - Prevents buffer overflows, use-after-free │
│ │
│ L2 ┌──────────────┐ Linux namespaces TURN-KEY │
│ │ Namespaces │ - Process, network, mount isolation │
│ └──────────────┘ - Separate /proc, network stack per waveform │
│ │
│ L3 ┌──────────────┐ Seccomp + LSM (SELinux/AppArmor) TURN-KEY │
│ │ Syscall Lock │ - Restrict system calls to DSP operations │
│ └──────────────┘ - Mandatory access control policies │
│ │
│ L4 ┌──────────────┐ Container (Docker/Podman) TEMPLATES │
│ │ Container │ - cgroups for resource limits │
│ └──────────────┘ - Pre-built Dockerfile provided │
│ │
│ L5 ┌──────────────┐ MicroVM (Firecracker) CONFIG │
│ │ MicroVM │ - VM-level isolation, minimal overhead │
│ └──────────────┘ - Sub-second boot times │
│ │
│ L6 ┌──────────────┐ Full VM (KVM/QEMU) CONFIG │
│ │ Full VM │ - Complete virtual machine isolation │
│ └──────────────┘ - For certification requirements │
│ │
│ L7 ┌──────────────┐ Hardware isolation SETUP │
│ │ Hardware │ - FPGA partitions with AXI firewalls │
│ └──────────────┘ - CPU pinning, IOMMU, memory encryption │
│ │
│ L8 ┌──────────────┐ Air gap SETUP │
│ │ Air Gap │ - Physically separate systems │
│ └──────────────┘ - Data diodes for one-way transfer │
│ │
└────────────────────────────────────────────────────────────────────────────┘
The following security features work out of the box:
| Feature | Description | Crate |
|---|---|---|
| SecureBuffer | Memory zeroization on drop, mlock to prevent swap | r4w-sandbox |
| EncryptedBuffer | AES-GCM encrypted memory for keys | r4w-sandbox |
| GuardedBuffer | Guard pages to detect overflows | r4w-sandbox |
| Namespace Isolation | PID/NET/MOUNT/USER separation | r4w-sandbox |
| Seccomp Profiles | DSP-optimized syscall allowlists | r4w-sandbox |
| Shared Memory IPC | Zero-copy sample transfer between sandboxes | r4w-sandbox |
| Control Channels | Unix socket communication for isolated waveforms | r4w-sandbox |
| FPGA Partitions | AXI firewall configuration for PL isolation | r4w-sandbox |
use r4w_sandbox::{Sandbox, IsolationLevel, SecureBuffer};
use r4w_sandbox::policy::{SeccompProfile, Capability};
// Create a sandbox for classified waveform processing
let sandbox = Sandbox::builder()
.isolation_level(IsolationLevel::L3_LSM)
.waveform("SINCGARS")
.memory_limit(512 * 1024 * 1024) // 512 MB limit
.cpu_limit(100) // 1 CPU core
.seccomp_profile(SeccompProfile::DSP) // DSP-only syscalls
.capabilities(&[Capability::IpcLock]) // Allow mlock for secure memory
.allow_network(false) // No network access
.build()?;
// Run classified processing in isolation
sandbox.run(|| {
// This code runs with:
// - Isolated PID namespace (can't see other processes)
// - Restricted syscalls (only DSP operations)
// - Memory limits enforced
// - No network access
let key = SecureBuffer::new(32); // Auto-zeroized on drop
process_sincgars(&samples, &key);
})?;For complete documentation, see: - docs/ISOLATION_GUIDE.md - Comprehensive isolation architecture - docs/SECURITY_GUIDE.md - Security best practices
For commercial secure SDR applications requiring formal separation between trusted and untrusted domains, R4W supports integration with a Crypto Service Interface (CSI).
┌─────────────────────────────────────────┐
│ Application (Voice/Data) │ RED (Trusted)
│ PlaintextIn { payload, policy_id } │
├═════════════════════════════════════════┤
│ Crypto Service Interface (CSI) │ ← CRYPTO BOUNDARY
│ - AEAD encryption/decryption │
│ - Replay protection (sliding window) │
│ - Key references (no raw keys) │
│ - Zeroization with observable state │
├═════════════════════════════════════════┤
│ Waveform Layer (sees only ciphertext) │ BLACK (Untrusted)
│ modulate(&[u8]) -> Vec<IQSample> │
├─────────────────────────────────────────┤
│ HAL / RF Hardware │ BLACK
└─────────────────────────────────────────┘
&[u8] -
opaque bytes, whether plaintext or ciphertextno_std readiness| Property | Description |
|---|---|
| Directionality | Plaintext only enters CSI; ciphertext only exits |
| No RF metadata with plaintext | Frequency, modulation, etc. never cross boundary |
| Replay protection | Per-flow sliding window (64-128 packets) |
| Key references | Never raw key material in waveform code |
| Zeroization | Explicit command with observable state transition |
CSI is designed as an optional layer that sits above the waveform:
// Without CSI (educational/hobby)
let samples = waveform.modulate(plaintext_bytes);
// With CSI (commercial secure)
csi.submit_plaintext(plaintext_msg)?;
if let Some(ct) = csi.poll_ciphertext() {
let samples = waveform.modulate(&ct.ciphertext);
}| Phase | Status | Description |
|---|---|---|
| Architecture Design | Complete | Documented in docs/CRYPTO_BOUNDARY.md |
| CSI Specification | Complete | Flow management, replay, zeroization |
| R4W Compatibility | Ready | No changes needed to existing code |
| CSI Implementation | Future | csi-core, csi-queues,
csi-backend-soft |
| Embedded Target | Future | STM32H7 (no_std), Zynq |
| Platform | Use Case | CSI Backend |
|---|---|---|
| Desktop SDR | Educational, development | Software (ChaCha20-Poly1305) |
| STM32H7 | Embedded radio control plane | Software or secure element |
| Zynq | Production SDR with FPGA PHY | Hardware crypto acceleration |
For complete documentation: docs/CRYPTO_BOUNDARY.md
For comprehensive documentation, see the docs/ directory:
| Document | Description |
|---|---|
| docs/README.md | Documentation index and navigation guide |
| docs/WAVEFORM_DEVELOPERS_GUIDE.md | Complete guide for waveform developers: debugging, testing, deployment |
| docs/PHYSICAL_LAYER_GUIDE.md | Timing model, HAL, RT primitives, configuration, observability |
| docs/TICK_SCHEDULER_GUIDE.md | Discrete event simulation and time control |
| docs/REALTIME_SCHEDULER_GUIDE.md | TX/RX coordination, FHSS, TDMA timing |
| docs/FPGA_DEVELOPERS_GUIDE.md | FPGA engineer’s guide: IP cores, register maps, collaboration |
| docs/SECURITY_GUIDE.md | Security: memory safety, crypto, isolation, secure deployment |
| docs/ISOLATION_GUIDE.md | Waveform isolation: containers, VMs, hardware separation |
| docs/PORTING_GUIDE_MILITARY.md | Military waveform porting: SINCGARS, HAVEQUICK, Link-16, P25 |
| MISSING_FEATURES.md | Production readiness assessment and roadmap |