
Building SDR Waveforms in Rust
R4W Development Team
(Aida, Joe Mooney, Claude
Code)
December 2025
Complete waveform development lifecycle:
| Topic | Coverage |
|---|---|
| Architecture | Waveform trait system |
| Implementation | Modulation/demodulation |
| Testing | Unit tests, BER validation |
| Benchmarking | Performance profiling |
| Deployment | Cross-compilation, targets |
crates/
├── r4w-core/ # DSP and waveforms
│ ├── src/
│ │ ├── dsp/ # FFT, filters, chirp
│ │ └── waveform/ # All waveform implementations
│ │ ├── trait.rs
│ │ ├── factory.rs
│ │ ├── lora.rs
│ │ ├── psk.rs
│ │ └── fsk.rs
│ └── benches/ # Criterion benchmarks
├── r4w-sim/ # Channel simulation
├── r4w-fpga/ # FPGA acceleration
├── r4w-cli/ # Command-line interface
└── r4w-gui/ # GUI application
Every waveform implements:
pub trait Waveform: Send + Sync {
fn info(&self) -> WaveformInfo;
fn modulate(&self, bits: &[bool]) -> Vec<IQSample>;
fn demodulate(&self, samples: &[IQSample])
-> Vec<bool>;
fn constellation_points(&self) -> Vec<IQSample>;
// Educational methods
fn get_modulation_stages(&self, bits: &[bool])
-> Vec<ModulationStage>;
}Complex I/Q sample representation:
Transmit Path:
┌──────────┐ ┌──────────────┐ ┌───────────────┐
│ Data │──►│ Modulator │──►│ RF/Transport │
│ (bits) │ │ (waveform) │ │ (hardware) │
└──────────┘ └──────────────┘ └───────────────┘
Receive Path:
┌───────────────┐ ┌──────────────┐ ┌──────────┐
│ RF/Transport │──►│ Demodulator │──►│ Data │
│ (hardware) │ │ (waveform) │ │ (bits) │
└───────────────┘ └──────────────┘ └──────────┘
Basic phase shift keying:
Define ideal symbol positions:
fn constellation_points(&self) -> Vec<IQSample> {
vec![
IQSample::new(-1.0, 0.0), // Bit 0
IQSample::new(1.0, 0.0), // Bit 1
]
}Used by GUI for visualization.
| Category | Waveforms |
|---|---|
| Phase Shift | BPSK, QPSK, 8PSK |
| Amplitude | AM, ASK, OOK |
| Frequency | FSK, GFSK, MSK |
| Quadrature | 16-QAM, 64-QAM, 256-QAM |
| Spread Spectrum | DSSS, FHSS, LoRa (CSS) |
| Multi-carrier | OFDM |
| Military | SINCGARS, HAVEQUICK |
Chirp Spread Spectrum:
pub struct LoraWaveform {
sf: u8, // Spreading factor 5-12
bw: f64, // Bandwidth (125/250/500 kHz)
chips_per_symbol: usize,
}
fn modulate_symbol(&self, symbol: u16) -> Vec<IQSample> {
// Generate chirp with cyclic shift
let shift = symbol as f64 / self.chips_per_symbol as f64;
self.generate_chirp(shift)
}FFT-based chirp detection:
fn demodulate(&self, samples: &[IQSample]) -> Vec<bool> {
let downchirp = self.generate_downchirp();
samples.chunks(self.chips_per_symbol)
.flat_map(|chunk| {
// Multiply by downchirp
let product = self.multiply(chunk, &downchirp);
// FFT to find peak
let spectrum = fft(&product);
let peak_bin = find_peak(&spectrum);
// Convert bin to symbol to bits
symbol_to_bits(peak_bin, self.sf)
})
.collect()
}pub struct WaveformInfo {
pub name: String,
pub description: String,
pub modulation_type: ModulationType,
pub bits_per_symbol: usize,
pub sample_rate: f64,
pub symbol_rate: f64,
pub bandwidth: f64,
}Returned by info() method.
Show modulation pipeline:
fn get_modulation_stages(&self, bits: &[bool])
-> Vec<ModulationStage>
{
vec![
ModulationStage {
name: "Input Bits",
data: StageData::Bits(bits.to_vec()),
},
ModulationStage {
name: "Symbol Mapping",
data: StageData::Symbols(self.map_symbols(bits)),
},
ModulationStage {
name: "Pulse Shaping",
data: StageData::Samples(self.modulate(bits)),
},
]
}Dynamic waveform creation:
Test modulate/demodulate roundtrip:
Bit Error Rate with channel:
#[test]
fn test_bpsk_ber_at_10db() {
let waveform = BpskWaveform::new(1e6, 100e3);
let channel = AwgnChannel::new(10.0); // 10 dB SNR
let bits: Vec<bool> = (0..10000)
.map(|_| rand::random())
.collect();
let samples = waveform.modulate(&bits);
let noisy = channel.apply(&samples);
let recovered = waveform.demodulate(&noisy);
let ber = count_errors(&bits, &recovered) as f64
/ bits.len() as f64;
assert!(ber < 0.01); // BER < 1%
}Criterion benchmarks:
// benches/waveform_bench.rs
use criterion::{criterion_group, Criterion};
fn bench_lora_modulate(c: &mut Criterion) {
let waveform = LoraWaveform::new(7, 125e3);
let bits = vec![true; 1024];
c.bench_function("lora_sf7_modulate", |b| {
b.iter(|| waveform.modulate(&bits))
});
}Run: cargo bench
| Waveform | Throughput | Latency p99 |
|---|---|---|
| BPSK | 25.4 K/s | 20 us |
| QPSK | 22.1 K/s | 25 us |
| LoRa SF7 | 45.6 K/s | 18 us |
| 16-QAM | 18.3 K/s | 35 us |
| OFDM | 12.7 K/s | 80 us |
Build for embedded targets:
crates/r4w-core/src/waveform/<name>.rsWaveform traitmod.rs and factorypub struct CustomFskWaveform {
deviation: f64,
symbol_rate: f64,
}
impl Waveform for CustomFskWaveform {
fn modulate(&self, bits: &[bool]) -> Vec<IQSample> {
let mut phase = 0.0;
bits.iter()
.flat_map(|&bit| {
let freq = if bit {
self.deviation
} else {
-self.deviation
};
self.generate_tone(freq, &mut phase)
})
.collect()
}
}// .vscode/settings.json
{
"rust-analyzer.cargo.features": ["all"],
"rust-analyzer.checkOnSave.command": "clippy",
"rust-analyzer.lens.enable": true,
"editor.formatOnSave": true
}Extensions: rust-analyzer, CodeLLDB, Error Lens
| Tool | Purpose |
|---|---|
cargo test -- --nocapture |
See println output |
RUST_BACKTRACE=1 |
Full stack traces |
| CodeLLDB | Step debugging |
cargo flamegraph |
CPU profiling |
cargo bloat |
Binary size analysis |
| Step | Action |
|---|---|
| 1 | Implement Waveform trait |
| 2 | Add modulate/demodulate |
| 3 | Define constellation |
| 4 | Write unit tests |
| 5 | Add to factory |
| 6 | Benchmark performance |
| 7 | Cross-compile if needed |
38+ waveforms. Your next one.
R4W - Waveform Development
github.com/joemooney/r4w
Docs: docs/WAVEFORM_DEVELOPERS_GUIDE.md