aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMatthias P. Braendli <matthias.braendli@mpb.li>2022-12-31 14:36:18 +0100
committerMatthias P. Braendli <matthias.braendli@mpb.li>2022-12-31 14:36:18 +0100
commitba0aeee005d5fdaaab59cd7c099a237f51eddc86 (patch)
treea4819820cfb6fc9d2d126ab6050ba798bf925359 /src
downloadfl2k_ampliphase-ba0aeee005d5fdaaab59cd7c099a237f51eddc86.tar.gz
fl2k_ampliphase-ba0aeee005d5fdaaab59cd7c099a237f51eddc86.tar.bz2
fl2k_ampliphase-ba0aeee005d5fdaaab59cd7c099a237f51eddc86.zip
Create project
Diffstat (limited to 'src')
-rw-r--r--src/fl2k.rs52
-rw-r--r--src/lib.rs5
-rw-r--r--src/main.rs346
3 files changed, 403 insertions, 0 deletions
diff --git a/src/fl2k.rs b/src/fl2k.rs
new file mode 100644
index 0000000..7bade50
--- /dev/null
+++ b/src/fl2k.rs
@@ -0,0 +1,52 @@
+use std::ffi::c_int;
+use fl2k_ampliphase::{fl2k_dev_t, fl2k_get_device_count, fl2k_open, fl2k_close};
+
+enum FL2KError {
+ InvalidParam,
+ NoDevice,
+ NotFound,
+ Busy,
+ Timeout,
+ NoMem,
+ Unknown(c_int)
+}
+
+fn handle_return_value(val : c_int) -> Result<(), FL2KError> {
+ match val {
+ #![allow(non_snake_case)]
+ fl2k_error_FL2K_SUCCESS => Ok(()),
+ fl2k_error_FL2K_TRUE => Ok(()),
+ fl2k_error_FL2K_ERROR_INVALID_PARAM => Err(FL2KError::InvalidParam),
+ fl2k_error_FL2K_ERROR_NO_DEVICE => Err(FL2KError::NoDevice),
+ fl2k_error_FL2K_ERROR_NOT_FOUND => Err(FL2KError::NotFound),
+ fl2k_error_FL2K_ERROR_BUSY => Err(FL2KError::Busy),
+ fl2k_error_FL2K_ERROR_TIMEOUT => Err(FL2KError::Timeout),
+ fl2k_error_FL2K_ERROR_NO_MEM => Err(FL2KError::NoMem),
+ v => Err(FL2KError::Unknown(v)),
+ }
+}
+
+pub fn get_device_count() -> u32 {
+ unsafe { fl2k_get_device_count() }
+}
+
+struct FL2K {
+ device : *mut fl2k_dev_t,
+}
+
+impl FL2K {
+ pub fn open(device_index : u32) -> Result<Self, FL2KError> {
+ unsafe {
+ let mut fl2k = FL2K { device: std::mem::zeroed() };
+ handle_return_value(fl2k_open(&mut fl2k.device, device_index))?;
+ Ok(fl2k)
+ }
+ }
+
+ pub fn close(self) -> Result<(), FL2KError> {
+ unsafe {
+ handle_return_value(fl2k_close(self.device))?;
+ }
+ Ok(())
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..a38a13a
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,5 @@
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..4bdb879
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2022 by Matthias P. Braendli <matthias.braendli@mpb.li>
+ *
+ * Based on previous work by
+ * Copyright (C) 2022 by Felix Erckenbrecht <eligs@eligs.de>
+ * Copyright (C) 2016-2018 by Steve Markgraf <steve@steve-m.de>
+ * Copyright (C) 2009 by Bartek Kania <mbk@gnarf.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use std::{env, thread};
+use std::io::{prelude::*, BufReader, BufWriter};
+use std::fs::File;
+use std::sync::mpsc;
+use getopts::Options;
+use num_complex::Complex;
+
+//mod fl2k;
+
+const TRIG_TABLE_ORDER : usize = 8;
+const TRIG_TABLE_SHIFT : usize = 32 - TRIG_TABLE_ORDER;
+const TRIG_TABLE_LEN : usize = 1 << TRIG_TABLE_ORDER;
+
+const PI : f32 = std::f32::consts::PI;
+const INT32_MAX_AS_FLOAT : f32 = 0x1_0000_0000u64 as f32;
+const ANG_INCR : f32 = INT32_MAX_AS_FLOAT / (2.0 * PI);
+
+enum Waveform { Sine, Rect }
+
+enum Output { Debug }
+
+struct DDS {
+ trig_table_quadrature : Vec<i16>,
+ trig_table_inphase : Vec<i16>,
+
+ samp_rate : f32,
+ freq : f32,
+
+ /* instantaneous phase */
+ phase : u32,
+ /* phase increment */
+ phase_step : u32,
+
+ /* for phase modulation */
+ phase_delta : i32,
+ phase_slope : i32,
+
+ amplitude : f32,
+}
+
+impl DDS {
+ fn init(samp_rate : f32, freq : f32, phase : f32, amp : f32, waveform : Waveform) -> Self {
+ let mut trig_table_inphase = Vec::with_capacity(TRIG_TABLE_LEN);
+ let mut trig_table_quadrature = Vec::with_capacity(TRIG_TABLE_LEN);
+
+ let incr = 1.0f32 / TRIG_TABLE_LEN as f32;
+ for i in 0..TRIG_TABLE_LEN {
+ let i = f32::cos(incr * i as f32 * 2.0 * PI) * 32767.0;
+ let q = f32::sin(incr * i as f32 * 2.0 * PI) * 32767.8;
+
+ match waveform {
+ Waveform::Sine => {
+ trig_table_inphase.push(f32::round(i) as i16);
+ trig_table_quadrature.push(f32::round(q) as i16);
+ }
+ Waveform::Rect => {
+ trig_table_inphase.push(if i >= 0.0 { 32767 } else { -32767 });
+ trig_table_quadrature.push(if q >= 0.0 { 32767 } else { -32767 });
+ }
+ }
+ }
+
+ let phase_step = (freq / samp_rate) * 2.0 * PI * ANG_INCR;
+
+ DDS {
+ trig_table_quadrature,
+ trig_table_inphase,
+ samp_rate,
+ freq,
+ phase: f32::round(phase * ANG_INCR) as u32,
+ phase_step: f32::round(phase_step) as u32,
+ phase_delta: 0,
+ phase_slope: 0,
+ amplitude: amp,
+ }
+ }
+
+ fn set_phase(&mut self, phase_delta : i32, phase_slope : i32) {
+ self.phase_delta = phase_delta;
+ self.phase_slope = phase_slope;
+ }
+
+ fn generate_iq(&mut self, len : usize) -> Vec<(i8, i8)> {
+ let mut out = Vec::with_capacity(len);
+ for _ in 0..len {
+ let phase = self.phase as i32;
+ // get current carrier phase, add phase mod, calculate table index
+ let phase_idx_i = (phase - self.phase_delta) >> TRIG_TABLE_SHIFT;
+ let phase_idx_q = (phase + self.phase_delta) >> TRIG_TABLE_SHIFT;
+
+ if phase_idx_q > 255 || phase_idx_i > 255 {
+ panic!("Phase IDX out of bounds");
+ }
+
+ self.phase = phase as u32 + self.phase_step;
+
+ let amp = (self.amplitude * 32767.0) as i32; // 0..15
+ let amp_i = amp * self.trig_table_inphase[phase_idx_i as usize] as i32; // 0..31
+ let amp_q = amp * self.trig_table_quadrature[phase_idx_q as usize] as i32; // 0..31
+ //
+ let i = (amp_i >> 24) as i8; // 0..31 >> 24 => 0..8
+ let q = (amp_q >> 24) as i8; // 0..31 >> 24 => 0..8
+ out.push((i, q));
+
+ self.phase_delta += self.phase_slope;
+ }
+ out
+ }
+}
+
+fn print_usage(program: &str, opts: Options) {
+ let brief = format!("Usage: {} FILE [options]", program);
+ eprint!("{}", opts.usage(&brief));
+}
+
+fn main() {
+ let args: Vec<String> = env::args().collect();
+ let program = args[0].clone();
+
+ let mut opts = Options::new();
+ opts.optopt("f", "file", "Input file, containing signed 16-bit samples.", "FILE");
+ opts.optopt("d", "device-index", "Select device index", "DEVINDEX");
+ opts.optopt("c", "center-freq", "Center frequency in Hz (default: 1440 kHz)", "FREQ");
+ opts.optopt("s", "samplerate", "Samplerate in Hz (default: 96 MS/s)", "RATE");
+ opts.optopt("m", "mod-index", "Modulation index (default: 0.25)]", "FACTOR");
+ opts.optopt("i", "input-rate", "Input baseband sample rate (default: 48000 Hz)", "RATE");
+ opts.optopt("w", "waveform", "(sine|rect) default: rect", "WAVEFORM");
+ opts.optflag("D", "debug", "Write to debug files instead of FL2K");
+ opts.optflag("h", "help", "print this help menu");
+ let cli_args = match opts.parse(&args[1..]) {
+ Ok(m) => { m }
+ Err(f) => { panic!("{}", f.to_string()) }
+ };
+ if cli_args.opt_present("h") {
+ print_usage(&program, opts);
+ std::process::exit(1);
+ }
+
+ let output = if cli_args.opt_str("D").is_none() {
+ eprintln!("Only debug supported for now");
+ std::process::exit(1);
+ }
+ else {
+ Output::Debug
+ };
+
+ let samp_rate : u32 = match cli_args.opt_str("s") {
+ Some(s) => s.parse().expect("floating point value"),
+ None => 96_000_000,
+ };
+
+ let base_freq = match cli_args.opt_str("c") {
+ Some(s) => s.parse().expect("floating point value"),
+ None => 1_440_000.0,
+ };
+
+ let input_freq : u32 = match cli_args.opt_str("i") {
+ Some(s) => s.parse().expect("floating point value"),
+ None => 48_000,
+ };
+
+ let modulation_index = match cli_args.opt_str("i") {
+ Some(s) => s.parse().expect("floating point value"),
+ None => 0.25,
+ };
+
+ let waveform = match cli_args.opt_str("w") {
+ None => Waveform::Rect,
+ Some(w) if w == "sine" => Waveform::Sine,
+ Some(w) if w == "rect" => Waveform::Rect,
+ _ => {
+ eprintln!("Waveform must be 'sine' or 'rect'");
+ print_usage(&program, opts);
+ std::process::exit(1);
+ }
+ };
+
+ //eprintln!("Device count {}", fl2k::get_device_count());
+
+ let source_file_name = match cli_args.opt_str("f") {
+ Some(f) => f,
+ None => {
+ eprintln!("Specify input file!");
+ print_usage(&program, opts);
+ std::process::exit(1);
+ }
+ };
+
+ if samp_rate % input_freq != 0 {
+ eprintln!("WARNING: input freq does not divide sample rate.");
+ }
+ let rf_to_baseband_sample_ratio = samp_rate / input_freq;
+
+ eprintln!("Samplerate: {} MHz", (samp_rate as f32)/1000000.0);
+ eprintln!("Center frequency: {} kHz", base_freq/1000.0);
+
+
+ let (input_samples_tx, input_samples_rx) = mpsc::sync_channel::<Vec<f32>>(2);
+ let (pd_tx, pd_rx) = mpsc::sync_channel(2);
+ let (iq_tx, iq_rx) = mpsc::sync_channel(2);
+
+ let source_file = File::open(source_file_name).expect("open file");
+ let mut source_file = BufReader::new(source_file);
+
+ const BASEBAND_BUF_SAMPS : usize = 1024;
+
+ // Read file and convert samples
+ thread::spawn(move || {
+ loop {
+ let mut buf = Vec::with_capacity(BASEBAND_BUF_SAMPS);
+ buf.resize(BASEBAND_BUF_SAMPS, 0i16);
+
+ let mut buf_u8: &mut [u8] = unsafe {
+ std::slice::from_raw_parts_mut(
+ buf.as_mut_ptr() as *mut u8,
+ buf.len() * std::mem::size_of::<i16>()
+ )
+ };
+
+ source_file.read_exact(&mut buf_u8).expect("Read from source file");
+
+ let buf : Vec<f32> = buf
+ .iter()
+ .map(|v| (v/2 + i16::MAX) as f32 / 32768.0)
+ .collect();
+
+ if let Err(_) = input_samples_tx.send(buf) {
+ eprintln!("Quit read thread");
+ break;
+ }
+ }
+ });
+
+ // Read samples, calculate PD and PDSLOPE
+ thread::spawn(move || {
+ let mut lastamp = 0f32;
+ loop {
+ let Ok(buf) = input_samples_rx.recv() else { break };
+
+ let mut pd_buf = Vec::with_capacity(buf.len());
+
+ /* What we do here is calculate a linear "slope" from
+ the previous sample to this one. This is then used by
+ the modulator to gently increase/decrease the phase
+ with each sample without the need to recalculate
+ the dds parameters. In fact this gives us a very
+ efficient and pretty good interpolation filter. */
+
+ for sample in buf {
+ let slope = sample - lastamp;
+ let slope = slope * 1.0 / rf_to_baseband_sample_ratio as f32;
+
+ let pd = lastamp * modulation_index * INT32_MAX_AS_FLOAT;
+
+ const MIN_VAL : f32 = std::i32::MIN as f32;
+ const MAX_VAL : f32 = std::i32::MAX as f32;
+
+ if pd < MIN_VAL || pd > MAX_VAL {
+ panic!("pd out of bounds {}", pd);
+ }
+
+ let pdslope = slope * modulation_index * INT32_MAX_AS_FLOAT;
+ if pdslope < MIN_VAL || pdslope > MAX_VAL {
+ panic!("pdslope out of bounds {}", pdslope);
+ }
+
+ pd_buf.push((pd as i32, pdslope as i32));
+
+ lastamp = sample;
+ }
+
+ if let Err(_) = pd_tx.send(pd_buf) {
+ eprintln!("Quit pd thread");
+ break;
+ }
+ }
+ });
+
+ // Read PD and PDSLOPE, interpolate to higher rate
+ thread::spawn(move || {
+ let mut dds = DDS::init(samp_rate as f32, base_freq, 0.0, 1.0, waveform);
+ loop {
+ let Ok(buf) = pd_rx.recv() else { break };
+
+ for (pd, pdslope) in buf {
+ dds.set_phase(pd, pdslope);
+
+ let iq_buf = dds.generate_iq(rf_to_baseband_sample_ratio as usize);
+
+ if let Err(_) = iq_tx.send(iq_buf) {
+ eprintln!("Quit dds thread");
+ break;
+ }
+ }
+ }
+ });
+
+ // Main thread, output to file/device
+ match output {
+ Output::Debug => {
+ let out_file = File::create("debug-out.i8").expect("create file");
+ let mut out_file = BufWriter::new(out_file);
+ loop {
+ let Ok(buf) = iq_rx.recv() else { break };
+
+ let buf_u8: &[u8] = unsafe {
+ std::slice::from_raw_parts(
+ buf.as_ptr() as *const u8,
+ buf.len()
+ )
+ };
+
+ if let Err(e) = out_file.write_all(buf_u8) {
+ eprintln!("Write output error: {}", e);
+ break;
+ }
+ }
+ }
+ }
+
+ eprintln!("Leaving main thread");
+}