aboutsummaryrefslogtreecommitdiffstats
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
downloadfl2k_ampliphase-ba0aeee005d5fdaaab59cd7c099a237f51eddc86.tar.gz
fl2k_ampliphase-ba0aeee005d5fdaaab59cd7c099a237f51eddc86.tar.bz2
fl2k_ampliphase-ba0aeee005d5fdaaab59cd7c099a237f51eddc86.zip
Create project
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock290
-rw-r--r--Cargo.toml13
-rw-r--r--README.md10
-rw-r--r--build.rs36
-rw-r--r--c_sources/libosmo-fl2k.c1117
-rw-r--r--c_sources/osmo-fl2k.h163
-rw-r--r--c_sources/osmo-fl2k_export.h51
-rw-r--r--src/fl2k.rs52
-rw-r--r--src/lib.rs5
-rw-r--r--src/main.rs346
11 files changed, 2084 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..790f16b
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,290 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bindgen"
+version = "0.63.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "peeking_take_while",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+ "which",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cc"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clang-sys"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "either"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
+
+[[package]]
+name = "fl2k_ampliphase"
+version = "0.1.0"
+dependencies = [
+ "bindgen",
+ "cc",
+ "getopts",
+ "num-complex",
+]
+
+[[package]]
+name = "getopts"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nom"
+version = "7.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+
+[[package]]
+name = "peeking_take_while"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
+dependencies = [
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "shlex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
+
+[[package]]
+name = "syn"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "which"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b"
+dependencies = [
+ "either",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..78c9ac1
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "fl2k_ampliphase"
+version = "0.1.0"
+authors = ["Matthias P. Braendli <matthias.braendli@mpb.li>"]
+edition = "2021"
+
+[dependencies]
+getopts = "0.2"
+num-complex = "0.4"
+
+[build-dependencies]
+bindgen = "0.63"
+cc = "1.0"
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..72c551d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,10 @@
+# About
+
+This does ampliphase modulation using the Red and Green DACs of FL2K VGA dongles.
+
+# Install
+
+1. Install Rust using https://rustup.rs
+1. Install clang (Debian: `apt install llvm-dev libclang-dev clang`, others see
+ https://rust-lang.github.io/rust-bindgen/requirements.html)
+1. Install libusb 1.0 (include -dev package)
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..0c7e0f9
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,36 @@
+use std::env;
+use std::path::PathBuf;
+
+fn main() {
+ // First build osmo-fl2k
+ cc::Build::new()
+ .file("c_sources/libosmo-fl2k.c")
+ .include("c_sources")
+ .include("/usr/include/libusb-1.0")
+ .compile("libosmo-fl2k");
+
+ // Tell cargo to look for shared libraries in the specified directory
+ //println!("cargo:rustc-link-search=/path/to/lib");
+
+ // Tell cargo to tell rustc to link the system bzip2
+ // shared library.
+ println!("cargo:rustc-link-lib=libosmo-fl2k");
+ println!("cargo:rustc-link-lib=usb-1.0");
+
+ // Tell cargo to invalidate the built crate whenever the wrapper changes
+ println!("cargo:rerun-if-changed=c_sources/osmo-fl2k.h");
+
+ let bindings = bindgen::Builder::default()
+ .header("c_sources/osmo-fl2k.h")
+ // Tell cargo to invalidate the built crate whenever any of the
+ // included header files changed.
+ .parse_callbacks(Box::new(bindgen::CargoCallbacks))
+ .generate()
+ .expect("Unable to generate bindings");
+
+ let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+ bindings
+ .write_to_file(out_path.join("bindings.rs"))
+ .expect("Couldn't write bindings!");
+}
+
diff --git a/c_sources/libosmo-fl2k.c b/c_sources/libosmo-fl2k.c
new file mode 100644
index 0000000..99dcd33
--- /dev/null
+++ b/c_sources/libosmo-fl2k.c
@@ -0,0 +1,1117 @@
+/*
+ * osmo-fl2k, turns FL2000-based USB 3.0 to VGA adapters into
+ * low cost DACs
+ *
+ * Copyright (C) 2016-2020 by Steve Markgraf <steve@steve-m.de>
+ *
+ * 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/>.
+ */
+
+#include <errno.h>
+#include <signal.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+#include <libusb.h>
+#include <pthread.h>
+
+#ifndef _WIN32
+#include <unistd.h>
+#define sleep_ms(ms) usleep(ms*1000)
+#else
+#include <windows.h>
+#define sleep_ms(ms) Sleep(ms)
+#endif
+
+/*
+ * All libusb callback functions should be marked with the LIBUSB_CALL macro
+ * to ensure that they are compiled with the same calling convention as libusb.
+ *
+ * If the macro isn't available in older libusb versions, we simply define it.
+ */
+#ifndef LIBUSB_CALL
+#define LIBUSB_CALL
+#endif
+
+/* libusb < 1.0.9 doesn't have libusb_handle_events_timeout_completed */
+#ifndef HAVE_LIBUSB_HANDLE_EVENTS_TIMEOUT_COMPLETED
+#define libusb_handle_events_timeout_completed(ctx, tv, c) \
+ libusb_handle_events_timeout(ctx, tv)
+#endif
+
+#include "osmo-fl2k.h"
+
+enum fl2k_async_status {
+ FL2K_INACTIVE = 0,
+ FL2K_CANCELING,
+ FL2K_RUNNING
+};
+
+typedef enum fl2k_buf_state {
+ BUF_EMPTY = 0,
+ BUF_SUBMITTED,
+ BUF_FILLED,
+} fl2k_buf_state_t;
+
+typedef struct fl2k_xfer_info {
+ fl2k_dev_t *dev;
+ uint64_t seq;
+ fl2k_buf_state_t state;
+} fl2k_xfer_info_t;
+
+struct fl2k_dev {
+ libusb_context *ctx;
+ struct libusb_device_handle *devh;
+ uint32_t xfer_num;
+ uint32_t xfer_buf_num;
+ uint32_t xfer_buf_len;
+ struct libusb_transfer **xfer;
+ unsigned char **xfer_buf;
+
+ fl2k_xfer_info_t *xfer_info;
+
+ fl2k_tx_cb_t cb;
+ void *cb_ctx;
+ enum fl2k_async_status async_status;
+ int async_cancel;
+
+ int use_zerocopy;
+ int terminate;
+
+ /* thread related */
+ pthread_t usb_worker_thread;
+ pthread_t sample_worker_thread;
+ pthread_mutex_t buf_mutex;
+ pthread_cond_t buf_cond;
+
+ double rate; /* Hz */
+
+ /* status */
+ int dev_lost;
+ int driver_active;
+ uint32_t underflow_cnt;
+};
+
+typedef struct fl2k_dongle {
+ uint16_t vid;
+ uint16_t pid;
+ const char *name;
+} fl2k_dongle_t;
+
+static fl2k_dongle_t known_devices[] = {
+ { 0x1d5c, 0x2000, "FL2000DX OEM" },
+};
+
+#define DEFAULT_BUF_NUMBER 4
+
+#define CTRL_IN (LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_ENDPOINT_IN)
+#define CTRL_OUT (LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_ENDPOINT_OUT)
+#define CTRL_TIMEOUT 300
+#define BULK_TIMEOUT 0
+
+static int fl2k_read_reg(fl2k_dev_t *dev, uint16_t reg, uint32_t *val)
+{
+ int r;
+ uint8_t data[4];
+
+ if (!dev || !val)
+ return FL2K_ERROR_INVALID_PARAM;
+
+ r = libusb_control_transfer(dev->devh, CTRL_IN, 0x40,
+ 0, reg, data, 4, CTRL_TIMEOUT);
+
+ if (r < 4)
+ fprintf(stderr, "Error, short read from register!\n");
+
+ *val = (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | data[0];
+
+ return r;
+}
+
+static int fl2k_write_reg(fl2k_dev_t *dev, uint16_t reg, uint32_t val)
+{
+ uint8_t data[4];
+
+ if (!dev)
+ return FL2K_ERROR_INVALID_PARAM;
+
+ data[0] = val & 0xff;
+ data[1] = (val >> 8) & 0xff;
+ data[2] = (val >> 16) & 0xff;
+ data[3] = (val >> 24) & 0xff;
+
+ return libusb_control_transfer(dev->devh, CTRL_OUT, 0x41,
+ 0, reg, data, 4, CTRL_TIMEOUT);
+}
+
+int fl2k_init_device(fl2k_dev_t *dev)
+{
+ if (!dev)
+ return FL2K_ERROR_INVALID_PARAM;
+
+ /* initialization */
+ fl2k_write_reg(dev, 0x8020, 0xdf0000cc);
+
+ /* set DAC freq to lowest value possible to avoid
+ * underrun during init */
+ fl2k_write_reg(dev, 0x802c, 0x00416f3f);
+
+ fl2k_write_reg(dev, 0x8048, 0x7ffb8004);
+ fl2k_write_reg(dev, 0x803c, 0xd701004d);
+ fl2k_write_reg(dev, 0x8004, 0x0000031c);
+ fl2k_write_reg(dev, 0x8004, 0x0010039d);
+ fl2k_write_reg(dev, 0x8008, 0x07800898);
+
+ fl2k_write_reg(dev, 0x801c, 0x00000000);
+ fl2k_write_reg(dev, 0x0070, 0x04186085);
+
+ /* blanking magic */
+ fl2k_write_reg(dev, 0x8008, 0xfeff0780);
+ fl2k_write_reg(dev, 0x800c, 0x0000f001);
+
+ /* VSYNC magic */
+ fl2k_write_reg(dev, 0x8010, 0x0400042a);
+ fl2k_write_reg(dev, 0x8014, 0x0010002d);
+
+ fl2k_write_reg(dev, 0x8004, 0x00000002);
+
+ return 0;
+}
+
+int fl2k_deinit_device(fl2k_dev_t *dev)
+{
+ int r = 0;
+
+ if (!dev)
+ return FL2K_ERROR_INVALID_PARAM;
+
+ /* TODO, power down DACs, PLL, put device in reset */
+
+ return r;
+}
+
+static double fl2k_reg_to_freq(uint32_t reg)
+{
+ double sample_clock, offset, offs_div;
+ uint32_t pll_clock = 160000000;
+ uint8_t div = reg & 0x3f;
+ uint8_t out_div = (reg >> 8) & 0xf;
+ uint8_t frac = (reg >> 16) & 0xf;
+ uint8_t mult = (reg >> 20) & 0xf;
+
+ sample_clock = (pll_clock * mult) / (uint32_t)div;
+ offs_div = (pll_clock / 5.0f ) * mult;
+ offset = ((double)sample_clock/(offs_div/2)) * 1000000.0f;
+ sample_clock += (uint32_t)offset * frac;
+ sample_clock /= out_div;
+
+// fprintf(stderr, "div: %d\tod: %d\tfrac: %d\tmult %d\tclock: %f\treg "
+// "%08x\n", div, out_div, frac, mult, sample_clock, reg);
+
+ return sample_clock;
+}
+
+int fl2k_set_sample_rate(fl2k_dev_t *dev, uint32_t target_freq)
+{
+ double sample_clock, error, last_error = 1e20f;
+ uint32_t reg = 0, result_reg = 0;
+ uint8_t div, mult, frac, out_div;
+
+ if (!dev)
+ return FL2K_ERROR_INVALID_PARAM;
+
+ /* Output divider (accepts value 1-15)
+ * works, but adds lots of phase noise, so do not use it */
+ out_div = 1;
+
+ /* Observation: PLL multiplier of 7 works, but has more phase
+ * noise. Prefer multiplier 6 and 5 */
+ for (mult = 6; mult >= 3; mult--) {
+ for (div = 63; div > 1; div--) {
+ for (frac = 1; frac <= 15; frac++) {
+ reg = (mult << 20) | (frac << 16) |
+ (0x60 << 8) | (out_div << 8) | div;
+
+ sample_clock = fl2k_reg_to_freq(reg);
+ error = sample_clock - (double)target_freq;
+
+ /* Keep closest match */
+ if (fabs(error) < last_error) {
+ result_reg = reg;
+ last_error = fabs(error);
+ }
+ }
+ }
+ }
+
+ sample_clock = fl2k_reg_to_freq(result_reg);
+ error = sample_clock - (double)target_freq;
+ dev->rate = sample_clock;
+
+ if (fabs(error) > 1)
+ fprintf(stderr, "Requested sample rate %d not possible, using"
+ " %f, error is %f\n", target_freq, sample_clock, error);
+
+ return fl2k_write_reg(dev, 0x802c, result_reg);
+}
+
+uint32_t fl2k_get_sample_rate(fl2k_dev_t *dev)
+{
+ if (!dev)
+ return 0;
+
+ return (uint32_t)dev->rate;
+}
+
+static fl2k_dongle_t *find_known_device(uint16_t vid, uint16_t pid)
+{
+ unsigned int i;
+ fl2k_dongle_t *device = NULL;
+
+ for (i = 0; i < sizeof(known_devices)/sizeof(fl2k_dongle_t); i++ ) {
+ if (known_devices[i].vid == vid && known_devices[i].pid == pid) {
+ device = &known_devices[i];
+ break;
+ }
+ }
+
+ return device;
+}
+
+uint32_t fl2k_get_device_count(void)
+{
+ int i,r;
+ libusb_context *ctx;
+ libusb_device **list;
+ uint32_t device_count = 0;
+ struct libusb_device_descriptor dd;
+ ssize_t cnt;
+
+ r = libusb_init(&ctx);
+ if (r < 0)
+ return 0;
+
+ cnt = libusb_get_device_list(ctx, &list);
+
+ for (i = 0; i < cnt; i++) {
+ libusb_get_device_descriptor(list[i], &dd);
+
+ if (find_known_device(dd.idVendor, dd.idProduct))
+ device_count++;
+ }
+
+ libusb_free_device_list(list, 1);
+
+ libusb_exit(ctx);
+
+ return device_count;
+}
+
+const char *fl2k_get_device_name(uint32_t index)
+{
+ int i,r;
+ libusb_context *ctx;
+ libusb_device **list;
+ struct libusb_device_descriptor dd;
+ fl2k_dongle_t *device = NULL;
+ uint32_t device_count = 0;
+ ssize_t cnt;
+
+ r = libusb_init(&ctx);
+ if (r < 0)
+ return "";
+
+ cnt = libusb_get_device_list(ctx, &list);
+
+ for (i = 0; i < cnt; i++) {
+ libusb_get_device_descriptor(list[i], &dd);
+
+ device = find_known_device(dd.idVendor, dd.idProduct);
+
+ if (device) {
+ device_count++;
+
+ if (index == device_count - 1)
+ break;
+ }
+ }
+
+ libusb_free_device_list(list, 1);
+
+ libusb_exit(ctx);
+
+ if (device)
+ return device->name;
+ else
+ return "";
+}
+
+int fl2k_open(fl2k_dev_t **out_dev, uint32_t index)
+{
+ int r;
+ int i;
+ libusb_device **list;
+ fl2k_dev_t *dev = NULL;
+ libusb_device *device = NULL;
+ uint32_t device_count = 0;
+ struct libusb_device_descriptor dd;
+ uint8_t reg;
+ ssize_t cnt;
+
+ dev = malloc(sizeof(fl2k_dev_t));
+ if (NULL == dev)
+ return -ENOMEM;
+
+ memset(dev, 0, sizeof(fl2k_dev_t));
+
+ r = libusb_init(&dev->ctx);
+ if(r < 0){
+ free(dev);
+ return -1;
+ }
+
+#if LIBUSB_API_VERSION >= 0x01000106
+ libusb_set_option(dev->ctx, LIBUSB_OPTION_LOG_LEVEL, 3);
+#else
+ libusb_set_debug(dev->ctx, 3);
+#endif
+
+ dev->dev_lost = 1;
+
+ cnt = libusb_get_device_list(dev->ctx, &list);
+
+ for (i = 0; i < cnt; i++) {
+ device = list[i];
+
+ libusb_get_device_descriptor(list[i], &dd);
+
+ if (find_known_device(dd.idVendor, dd.idProduct)) {
+ device_count++;
+ }
+
+ if (index == device_count - 1)
+ break;
+
+ device = NULL;
+ }
+
+ if (!device) {
+ r = -1;
+ goto err;
+ }
+
+ r = libusb_open(device, &dev->devh);
+ libusb_free_device_list(list, 1);
+ if (r < 0) {
+ fprintf(stderr, "usb_open error %d\n", r);
+ if(r == LIBUSB_ERROR_ACCESS)
+ fprintf(stderr, "Please fix the device permissions, e.g. "
+ "by installing the udev rules file\n");
+ goto err;
+ }
+
+ /* If the adapter has an SPI flash for the Windows driver, we
+ * need to detach the USB mass storage driver first in order to
+ * open the device */
+ if (libusb_kernel_driver_active(dev->devh, 3) == 1) {
+ fprintf(stderr, "Kernel mass storage driver is attached, "
+ "detaching driver. This may take more than"
+ " 10 seconds!\n");
+ r = libusb_detach_kernel_driver(dev->devh, 3);
+ if (r < 0) {
+ fprintf(stderr, "Failed to detach mass storage "
+ "driver: %d\n", r);
+ goto err;
+ }
+ }
+
+ r = libusb_claim_interface(dev->devh, 0);
+ if (r < 0) {
+ fprintf(stderr, "usb_claim_interface 0 error %d\n", r);
+ goto err;
+ }
+
+ r = libusb_set_interface_alt_setting(dev->devh, 0, 1);
+ if (r < 0) {
+ fprintf(stderr, "Failed to switch interface 0 to "
+ "altsetting 1, trying to use interface 1\n");
+
+ r = libusb_claim_interface(dev->devh, 1);
+ if (r < 0) {
+ fprintf(stderr, "Could not claim interface 1: %d\n", r);
+ }
+ }
+
+ r = fl2k_init_device(dev);
+ if (r < 0)
+ goto err;
+
+ dev->dev_lost = 0;
+
+found:
+ *out_dev = dev;
+
+ return 0;
+err:
+ if (dev) {
+ if (dev->ctx)
+ libusb_exit(dev->ctx);
+
+ free(dev);
+ }
+
+ return r;
+}
+
+int fl2k_close(fl2k_dev_t *dev)
+{
+ if (!dev)
+ return FL2K_ERROR_INVALID_PARAM;
+
+ if(!dev->dev_lost) {
+ /* block until all async operations have been completed (if any) */
+ while (FL2K_INACTIVE != dev->async_status)
+ sleep_ms(100);
+
+ fl2k_deinit_device(dev);
+ }
+
+ libusb_release_interface(dev->devh, 0);
+ libusb_close(dev->devh);
+ libusb_exit(dev->ctx);
+
+ free(dev);
+
+ return 0;
+}
+
+static struct libusb_transfer *fl2k_get_next_xfer(fl2k_dev_t *dev,
+ fl2k_buf_state_t state)
+{
+ unsigned int i;
+ int next_buf = -1;
+ uint64_t next_seq = 0;
+ fl2k_xfer_info_t *xfer_info;
+
+ for (i = 0; i < dev->xfer_buf_num; i++) {
+ xfer_info = (fl2k_xfer_info_t *)dev->xfer[i]->user_data;
+ if (!xfer_info)
+ continue;
+
+ if (xfer_info->state == state) {
+ if (state == BUF_EMPTY) {
+ return dev->xfer[i];
+ } else if ((xfer_info->seq < next_seq) || next_buf < 0) {
+ next_seq = xfer_info->seq;
+ next_buf = i;
+ }
+ }
+ }
+
+ if ((state == BUF_FILLED) && (next_buf >= 0))
+ return dev->xfer[next_buf];
+ else
+ return NULL;
+}
+
+static void LIBUSB_CALL _libusb_callback(struct libusb_transfer *xfer)
+{
+ fl2k_xfer_info_t *xfer_info = (fl2k_xfer_info_t *)xfer->user_data;
+ fl2k_xfer_info_t *next_xfer_info;
+ fl2k_dev_t *dev = (fl2k_dev_t *)xfer_info->dev;
+ struct libusb_transfer *next_xfer = NULL;
+ int r = 0;
+
+ if (LIBUSB_TRANSFER_COMPLETED == xfer->status) {
+ /* resubmit transfer */
+ if (FL2K_RUNNING == dev->async_status) {
+ /* get next transfer */
+ next_xfer = fl2k_get_next_xfer(dev, BUF_FILLED);
+
+ if (next_xfer) {
+ next_xfer_info = (fl2k_xfer_info_t *) next_xfer->user_data;
+
+ /* Submit next filled transfer */
+ next_xfer_info->state = BUF_SUBMITTED;
+ r = libusb_submit_transfer(next_xfer);
+ xfer_info->state = BUF_EMPTY;
+ pthread_cond_signal(&dev->buf_cond);
+ } else {
+ /* We need to re-submit the transfer
+ * in any case, as otherwise the device
+ * stops to output data and hangs
+ * (happens only in the hacked 'gapless'
+ * mode without HSYNC and VSYNC) */
+ r = libusb_submit_transfer(xfer);
+ pthread_cond_signal(&dev->buf_cond);
+ dev->underflow_cnt++;
+ }
+ }
+ }
+
+ if (((LIBUSB_TRANSFER_CANCELLED != xfer->status) &&
+ (LIBUSB_TRANSFER_COMPLETED != xfer->status)) ||
+ (r == LIBUSB_ERROR_NO_DEVICE)) {
+ dev->dev_lost = 1;
+ fl2k_stop_tx(dev);
+ pthread_cond_signal(&dev->buf_cond);
+ fprintf(stderr, "cb transfer status: %d, submit "
+ "transfer %d, canceling...\n", xfer->status, r);
+ }
+}
+
+static int fl2k_alloc_submit_transfers(fl2k_dev_t *dev)
+{
+ unsigned int i;
+ int r = 0;
+ const char *incr_usbfs = "Please increase your allowed usbfs buffer"
+ " size with the following command:\n"
+ "echo 0 > /sys/module/usbcore/parameters/"
+ "usbfs_memory_mb\n";
+
+ if (!dev)
+ return FL2K_ERROR_INVALID_PARAM;
+
+ dev->xfer = malloc(dev->xfer_buf_num * sizeof(struct libusb_transfer *));
+
+ for (i = 0; i < dev->xfer_buf_num; ++i)
+ dev->xfer[i] = libusb_alloc_transfer(0);
+
+ dev->xfer_buf = malloc(dev->xfer_buf_num * sizeof(unsigned char *));
+ memset(dev->xfer_buf, 0, dev->xfer_buf_num * sizeof(unsigned char *));
+
+ dev->xfer_info = malloc(dev->xfer_buf_num * sizeof(fl2k_xfer_info_t));
+ memset(dev->xfer_info, 0, dev->xfer_buf_num * sizeof(fl2k_xfer_info_t));
+
+#if defined (__linux__) && LIBUSB_API_VERSION >= 0x01000105
+ fprintf(stderr, "Allocating %d zero-copy buffers\n", dev->xfer_buf_num);
+
+ dev->use_zerocopy = 1;
+ for (i = 0; i < dev->xfer_buf_num; ++i) {
+ dev->xfer_buf[i] = libusb_dev_mem_alloc(dev->devh, dev->xfer_buf_len);
+
+ if (dev->xfer_buf[i]) {
+ /* Check if Kernel usbfs mmap() bug is present: if the
+ * mapping is correct, the buffers point to memory that
+ * was memset to 0 by the Kernel, otherwise, they point
+ * to random memory. We check if the buffers are zeroed
+ * and otherwise fall back to buffers in userspace.
+ */
+ if (dev->xfer_buf[i][0] || memcmp(dev->xfer_buf[i],
+ dev->xfer_buf[i] + 1,
+ dev->xfer_buf_len - 1)) {
+ fprintf(stderr, "Detected Kernel usbfs mmap() "
+ "bug, falling back to buffers "
+ "in userspace\n");
+ dev->use_zerocopy = 0;
+ break;
+ }
+ } else {
+ fprintf(stderr, "Failed to allocate zero-copy "
+ "buffer for transfer %d\n%sFalling "
+ "back to buffers in userspace\n",
+ i, incr_usbfs);
+ dev->use_zerocopy = 0;
+ break;
+ }
+ }
+
+ /* zero-copy buffer allocation failed (partially or completely)
+ * we need to free the buffers again if already allocated */
+ if (!dev->use_zerocopy) {
+ for (i = 0; i < dev->xfer_buf_num; ++i) {
+ if (dev->xfer_buf[i])
+ libusb_dev_mem_free(dev->devh,
+ dev->xfer_buf[i],
+ dev->xfer_buf_len);
+ }
+ }
+#endif
+
+ /* no zero-copy available, allocate buffers in userspace */
+ if (!dev->use_zerocopy) {
+ for (i = 0; i < dev->xfer_buf_num; ++i) {
+ dev->xfer_buf[i] = malloc(dev->xfer_buf_len);
+
+ if (!dev->xfer_buf[i])
+ return FL2K_ERROR_NO_MEM;
+ }
+ }
+
+ /* fill transfers */
+ for (i = 0; i < dev->xfer_buf_num; ++i) {
+ libusb_fill_bulk_transfer(dev->xfer[i],
+ dev->devh,
+ 0x01,
+ dev->xfer_buf[i],
+ dev->xfer_buf_len,
+ _libusb_callback,
+ &dev->xfer_info[i],
+ 0);
+
+ dev->xfer_info[i].dev = dev;
+ dev->xfer_info[i].state = BUF_EMPTY;
+
+ /* if we allocate the memory through the Kernel, it is
+ * already cleared */
+ if (!dev->use_zerocopy)
+ memset(dev->xfer_buf[i], 0, dev->xfer_buf_len);
+ }
+
+ /* submit transfers */
+ for (i = 0; i < dev->xfer_num; ++i) {
+ r = libusb_submit_transfer(dev->xfer[i]);
+ dev->xfer_info[i].state = BUF_SUBMITTED;
+
+ if (r < 0) {
+ fprintf(stderr, "Failed to submit transfer %i\n%s",
+ i, incr_usbfs);
+ break;
+ }
+ }
+
+ return 0;
+}
+
+static int _fl2k_free_async_buffers(fl2k_dev_t *dev)
+{
+ unsigned int i;
+
+ if (!dev)
+ return FL2K_ERROR_INVALID_PARAM;
+
+ if (dev->xfer) {
+ for (i = 0; i < dev->xfer_buf_num; ++i) {
+ if (dev->xfer[i]) {
+ libusb_free_transfer(dev->xfer[i]);
+ }
+ }
+
+ free(dev->xfer);
+ dev->xfer = NULL;
+ }
+
+ if (dev->xfer_buf) {
+ for (i = 0; i < dev->xfer_buf_num; ++i) {
+ if (dev->xfer_buf[i]) {
+ if (dev->use_zerocopy) {
+#if defined (__linux__) && LIBUSB_API_VERSION >= 0x01000105
+ libusb_dev_mem_free(dev->devh,
+ dev->xfer_buf[i],
+ dev->xfer_buf_len);
+#endif
+ } else {
+ free(dev->xfer_buf[i]);
+ }
+ }
+ }
+
+ free(dev->xfer_buf);
+ dev->xfer_buf = NULL;
+ }
+
+ return 0;
+}
+
+static void *fl2k_usb_worker(void *arg)
+{
+ fl2k_dev_t *dev = (fl2k_dev_t *)arg;
+ struct timeval tv = { 1, 0 };
+ struct timeval zerotv = { 0, 0 };
+ enum fl2k_async_status next_status = FL2K_INACTIVE;
+ int r = 0;
+ unsigned int i;
+
+ while (FL2K_RUNNING == dev->async_status) {
+ r = libusb_handle_events_timeout_completed(dev->ctx, &tv,
+ &dev->async_cancel);
+ }
+
+ while (FL2K_INACTIVE != dev->async_status) {
+ r = libusb_handle_events_timeout_completed(dev->ctx, &tv,
+ &dev->async_cancel);
+ if (r < 0) {
+ /*fprintf(stderr, "handle_events returned: %d\n", r);*/
+ if (r == LIBUSB_ERROR_INTERRUPTED) /* stray signal */
+ continue;
+ break;
+ }
+
+ if (FL2K_CANCELING == dev->async_status) {
+ next_status = FL2K_INACTIVE;
+
+ if (!dev->xfer)
+ break;
+
+ for (i = 0; i < dev->xfer_buf_num; ++i) {
+ if (!dev->xfer[i])
+ continue;
+
+ if (LIBUSB_TRANSFER_CANCELLED !=
+ dev->xfer[i]->status) {
+ r = libusb_cancel_transfer(dev->xfer[i]);
+ /* handle events after canceling
+ * to allow transfer status to
+ * propagate */
+ libusb_handle_events_timeout_completed(dev->ctx,
+ &zerotv, NULL);
+ if (r < 0)
+ continue;
+
+ next_status = FL2K_CANCELING;
+ }
+ }
+
+ if (dev->dev_lost || FL2K_INACTIVE == next_status) {
+ /* handle any events that still need to
+ * be handled before exiting after we
+ * just cancelled all transfers */
+ libusb_handle_events_timeout_completed(dev->ctx,
+ &zerotv, NULL);
+ break;
+ }
+ }
+ }
+
+ /* wake up sample worker */
+ pthread_cond_signal(&dev->buf_cond);
+
+ /* wait for sample worker thread to finish before freeing buffers */
+ pthread_join(dev->sample_worker_thread, NULL);
+ _fl2k_free_async_buffers(dev);
+ dev->async_status = next_status;
+
+ pthread_exit(NULL);
+}
+
+/* Buffer format conversion functions for R, G, B DACs */
+static inline void fl2k_convert_r(char *out,
+ char *in,
+ uint32_t len,
+ uint8_t offset)
+{
+ unsigned int i, j = 0;
+
+ if (!in || !out)
+ return;
+
+ for (i = 0; i < len; i += 24) {
+ out[i+ 6] = in[j++] + offset;
+ out[i+ 1] = in[j++] + offset;
+ out[i+12] = in[j++] + offset;
+ out[i+15] = in[j++] + offset;
+ out[i+10] = in[j++] + offset;
+ out[i+21] = in[j++] + offset;
+ out[i+16] = in[j++] + offset;
+ out[i+19] = in[j++] + offset;
+ }
+}
+
+static inline void fl2k_convert_g(char *out,
+ char *in,
+ uint32_t len,
+ uint8_t offset)
+{
+ unsigned int i, j = 0;
+
+ if (!in || !out)
+ return;
+
+ for (i = 0; i < len; i += 24) {
+ out[i+ 5] = in[j++] + offset;
+ out[i+ 0] = in[j++] + offset;
+ out[i+ 3] = in[j++] + offset;
+ out[i+14] = in[j++] + offset;
+ out[i+ 9] = in[j++] + offset;
+ out[i+20] = in[j++] + offset;
+ out[i+23] = in[j++] + offset;
+ out[i+18] = in[j++] + offset;
+ }
+}
+
+static inline void fl2k_convert_b(char *out,
+ char *in,
+ uint32_t len,
+ uint8_t offset)
+{
+ unsigned int i, j = 0;
+
+ if (!in || !out)
+ return;
+
+ for (i = 0; i < len; i += 24) {
+ out[i+ 4] = in[j++] + offset;
+ out[i+ 7] = in[j++] + offset;
+ out[i+ 2] = in[j++] + offset;
+ out[i+13] = in[j++] + offset;
+ out[i+ 8] = in[j++] + offset;
+ out[i+11] = in[j++] + offset;
+ out[i+22] = in[j++] + offset;
+ out[i+17] = in[j++] + offset;
+ }
+}
+
+static void *fl2k_sample_worker(void *arg)
+{
+ int r = 0;
+ unsigned int i, j;
+ fl2k_dev_t *dev = (fl2k_dev_t *)arg;
+ fl2k_xfer_info_t *xfer_info = NULL;
+ struct libusb_transfer *xfer = NULL;
+ char *out_buf = NULL;
+ fl2k_data_info_t data_info;
+ uint32_t underflows = 0;
+ uint64_t buf_cnt = 0;
+
+ while (FL2K_RUNNING == dev->async_status) {
+ memset(&data_info, 0, sizeof(fl2k_data_info_t));
+
+ data_info.len = FL2K_BUF_LEN;
+ data_info.underflow_cnt = dev->underflow_cnt;
+ data_info.ctx = dev->cb_ctx;
+
+ if (dev->underflow_cnt > underflows) {
+ fprintf(stderr, "Underflow! Skipped %d buffers\n",
+ dev->underflow_cnt - underflows);
+ underflows = dev->underflow_cnt;
+ }
+
+ /* call application callback to get samples */
+ if (dev->cb)
+ dev->cb(&data_info);
+
+ xfer = fl2k_get_next_xfer(dev, BUF_EMPTY);
+
+ if (!xfer) {
+ pthread_cond_wait(&dev->buf_cond, &dev->buf_mutex);
+ /* in the meantime, the device might be gone */
+ if (FL2K_RUNNING != dev->async_status)
+ break;
+
+ xfer = fl2k_get_next_xfer(dev, BUF_EMPTY);
+ if (!xfer) {
+ fprintf(stderr, "no free transfer, skipping"
+ " input buffer\n");
+ continue;
+ }
+ }
+
+ /* We have an empty USB transfer buffer */
+ xfer_info = (fl2k_xfer_info_t *)xfer->user_data;
+ out_buf = (char *)xfer->buffer;
+
+ /* Re-arrange and copy bytes in buffer for DACs */
+ fl2k_convert_r(out_buf, data_info.r_buf, dev->xfer_buf_len,
+ data_info.sampletype_signed ? 128 : 0);
+
+ fl2k_convert_g(out_buf, data_info.g_buf, dev->xfer_buf_len,
+ data_info.sampletype_signed ? 128 : 0);
+
+ fl2k_convert_b(out_buf, data_info.b_buf, dev->xfer_buf_len,
+ data_info.sampletype_signed ? 128 : 0);
+
+ xfer_info->seq = buf_cnt++;
+ xfer_info->state = BUF_FILLED;
+ }
+
+ /* notify application if we've lost the device */
+ if (dev->dev_lost && dev->cb) {
+ data_info.device_error = 1;
+ dev->cb(&data_info);
+ }
+
+ pthread_exit(NULL);
+}
+
+
+int fl2k_start_tx(fl2k_dev_t *dev, fl2k_tx_cb_t cb, void *ctx,
+ uint32_t buf_num)
+{
+ int r = 0;
+ int i;
+ pthread_attr_t attr;
+
+ if (!dev || !cb)
+ return FL2K_ERROR_INVALID_PARAM;
+
+ dev->async_status = FL2K_RUNNING;
+ dev->async_cancel = 0;
+
+ dev->cb = cb;
+ dev->cb_ctx = ctx;
+
+ if (buf_num > 0)
+ dev->xfer_num = buf_num;
+ else
+ dev->xfer_num = DEFAULT_BUF_NUMBER;
+
+ /* have two spare buffers that can be filled while the
+ * others are submitted */
+ dev->xfer_buf_num = dev->xfer_num + 2;
+ dev->xfer_buf_len = FL2K_XFER_LEN;
+
+ r = fl2k_alloc_submit_transfers(dev);
+ if (r < 0)
+ goto cleanup;
+
+ pthread_mutex_init(&dev->buf_mutex, NULL);
+ pthread_cond_init(&dev->buf_cond, NULL);
+ pthread_attr_init(&attr);
+
+ r = pthread_create(&dev->usb_worker_thread, &attr,
+ fl2k_usb_worker, (void *)dev);
+ if (r < 0) {
+ fprintf(stderr, "Error spawning USB worker thread!\n");
+ goto cleanup;
+ }
+
+ r = pthread_create(&dev->sample_worker_thread, &attr,
+ fl2k_sample_worker, (void *)dev);
+ if (r < 0) {
+ fprintf(stderr, "Error spawning sample worker thread!\n");
+ goto cleanup;
+ }
+
+ pthread_attr_destroy(&attr);
+
+ return 0;
+
+cleanup:
+ _fl2k_free_async_buffers(dev);
+ return FL2K_ERROR_BUSY;
+
+}
+
+int fl2k_stop_tx(fl2k_dev_t *dev)
+{
+ if (!dev)
+ return FL2K_ERROR_INVALID_PARAM;
+
+ /* if streaming, try to cancel gracefully */
+ if (FL2K_RUNNING == dev->async_status) {
+ dev->async_status = FL2K_CANCELING;
+ dev->async_cancel = 1;
+ return 0;
+ /* if called while in pending state, change the state forcefully */
+ } else if (FL2K_INACTIVE != dev->async_status) {
+ dev->async_status = FL2K_INACTIVE;
+ return 0;
+ }
+
+ return FL2K_ERROR_BUSY;
+}
+
+int fl2k_i2c_read(fl2k_dev_t *dev, uint8_t i2c_addr, uint8_t reg_addr, uint8_t *data)
+{
+ int i, r, timeout = 1;
+ uint32_t reg;
+
+ if (!dev)
+ return FL2K_ERROR_INVALID_PARAM;
+
+ r = fl2k_read_reg(dev, 0x8020, &reg);
+ if (r < 0)
+ return r;
+
+ /* apply mask, clearing bit 30 disables periodic repetition of read */
+ reg &= 0x3ffc0000;
+
+ /* set I2C register and address, select I2C read (bit 7) */
+ reg |= (1 << 28) | (reg_addr << 8) | (1 << 7) | (i2c_addr & 0x7f);
+
+ r = fl2k_write_reg(dev, 0x8020, reg);
+ if (r < 0)
+ return r;
+
+ for (i = 0; i < 10; i++) {
+ sleep_ms(10);
+
+ r = fl2k_read_reg(dev, 0x8020, &reg);
+ if (r < 0)
+ return r;
+
+ /* check if operation completed */
+ if (reg & (1 << 31)) {
+ timeout = 0;
+ break;
+ }
+ }
+
+ if (timeout)
+ return FL2K_ERROR_TIMEOUT;
+
+ /* check if slave responded and all data was read */
+ if (reg & (0x0f << 24))
+ return FL2K_ERROR_NOT_FOUND;
+
+ /* read data from register 0x8024 */
+ return libusb_control_transfer(dev->devh, CTRL_IN, 0x40,
+ 0, 0x8024, data, 4, CTRL_TIMEOUT);
+}
+
+int fl2k_i2c_write(fl2k_dev_t *dev, uint8_t i2c_addr, uint8_t reg_addr, uint8_t *data)
+{
+ int i, r, timeout = 1;
+ uint32_t reg;
+
+ if (!dev)
+ return FL2K_ERROR_INVALID_PARAM;
+
+ /* write data to register 0x8028 */
+ r = libusb_control_transfer(dev->devh, CTRL_OUT, 0x41,
+ 0, 0x8028, data, 4, CTRL_TIMEOUT);
+
+ if (r < 0)
+ return r;
+
+ r = fl2k_read_reg(dev, 0x8020, &reg);
+ if (r < 0)
+ return r;
+
+ /* apply mask, clearing bit 30 disables periodic repetition of read */
+ reg &= 0x3ffc0000;
+
+ /* set I2C register and address */
+ reg |= (1 << 28) | (reg_addr << 8) | (i2c_addr & 0x7f);
+
+ r = fl2k_write_reg(dev, 0x8020, reg);
+ if (r < 0)
+ return r;
+
+ for (i = 0; i < 10; i++) {
+ sleep_ms(10);
+
+ r = fl2k_read_reg(dev, 0x8020, &reg);
+ if (r < 0)
+ return r;
+
+ /* check if operation completed */
+ if (reg & (1 << 31)) {
+ timeout = 0;
+ break;
+ }
+ }
+
+ if (timeout)
+ return FL2K_ERROR_TIMEOUT;
+
+ /* check if slave responded and all data was written */
+ if (reg & (0x0f << 24))
+ return FL2K_ERROR_NOT_FOUND;
+
+ return FL2K_SUCCESS;
+}
diff --git a/c_sources/osmo-fl2k.h b/c_sources/osmo-fl2k.h
new file mode 100644
index 0000000..14f2e2a
--- /dev/null
+++ b/c_sources/osmo-fl2k.h
@@ -0,0 +1,163 @@
+/*
+ * osmo-fl2k, turns FL2000-based USB 3.0 to VGA adapters into
+ * low cost DACs
+ *
+ * Copyright (C) 2016-2018 by Steve Markgraf <steve@steve-m.de>
+ *
+ * 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/>.
+ */
+
+#ifndef __FL2K_H
+#define __FL2K_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdint.h>
+#include "osmo-fl2k_export.h"
+
+enum fl2k_error {
+ FL2K_SUCCESS = 0,
+ FL2K_TRUE = 1,
+ FL2K_ERROR_INVALID_PARAM = -1,
+ FL2K_ERROR_NO_DEVICE = -2,
+ FL2K_ERROR_NOT_FOUND = -5,
+ FL2K_ERROR_BUSY = -6,
+ FL2K_ERROR_TIMEOUT = -7,
+ FL2K_ERROR_NO_MEM = -11,
+};
+
+typedef struct fl2k_data_info {
+ /* information provided by library */
+ void *ctx;
+ uint32_t underflow_cnt; /* underflows since last callback */
+ uint32_t len; /* buffer length */
+ int using_zerocopy; /* using zerocopy kernel buffers */
+ int device_error; /* device error happened, terminate application */
+
+ /* filled in by application */
+ int sampletype_signed; /* are samples signed or unsigned? */
+ char *r_buf; /* pointer to red buffer */
+ char *g_buf; /* pointer to green buffer */
+ char *b_buf; /* pointer to blue buffer */
+} fl2k_data_info_t;
+
+typedef struct fl2k_dev fl2k_dev_t;
+
+/** The transfer length was chosen by the following criteria:
+ * - Must be a supported resolution of the FL2000DX
+ * - Must be a multiple of 61440 bytes (URB payload length),
+ * which is important for using the DAC without HSYNC/VSYNC blanking,
+ * otherwise a couple of samples are missing between every buffer
+ * - Should be smaller than 4MB in order to be allocatable by kmalloc()
+ * for zerocopy transfers
+ **/
+#define FL2K_BUF_LEN (1280 * 1024)
+#define FL2K_XFER_LEN (FL2K_BUF_LEN * 3)
+
+FL2K_API uint32_t fl2k_get_device_count(void);
+
+FL2K_API const char* fl2k_get_device_name(uint32_t index);
+
+FL2K_API int fl2k_open(fl2k_dev_t **dev, uint32_t index);
+
+FL2K_API int fl2k_close(fl2k_dev_t *dev);
+
+/* configuration functions */
+
+/*!
+ * Set the sample rate (pixel clock) for the device
+ *
+ * \param dev the device handle given by fl2k_open()
+ * \param samp_rate the sample rate to be set, maximum value depends
+ * on host and USB controller
+ * \return 0 on success, -EINVAL on invalid rate
+ */
+FL2K_API int fl2k_set_sample_rate(fl2k_dev_t *dev, uint32_t target_freq);
+
+/*!
+ * Get actual sample rate the device is configured to.
+ *
+ * \param dev the device handle given by fl2k_open()
+ * \return 0 on error, sample rate in Hz otherwise
+ */
+FL2K_API uint32_t fl2k_get_sample_rate(fl2k_dev_t *dev);
+
+/* streaming functions */
+
+typedef void(*fl2k_tx_cb_t)(fl2k_data_info_t *data_info);
+
+/*!
+ * Starts the tx thread. This function will block until
+ * it is being canceled using fl2k_stop_tx()
+ *
+ * \param dev the device handle given by fl2k_open()
+ * \param ctx user specific context to pass via the callback function
+ * \param buf_num optional buffer count, buf_num * FL2K_BUF_LEN = overall buffer size
+ * set to 0 for default buffer count (4)
+ * \return 0 on success
+ */
+FL2K_API int fl2k_start_tx(fl2k_dev_t *dev, fl2k_tx_cb_t cb,
+ void *ctx, uint32_t buf_num);
+
+/*!
+ * Cancel all pending asynchronous operations on the device.
+ *
+ * \param dev the device handle given by fl2k_open()
+ * \return 0 on success
+ */
+FL2K_API int fl2k_stop_tx(fl2k_dev_t *dev);
+
+/*!
+ * Read 4 bytes via the FL2K I2C bus
+ *
+ * \param dev the device handle given by fl2k_open()
+ * \param i2c_addr address of the I2C device
+ * \param reg_addr start address of the 4 bytes to be read
+ * \param data pointer to byte array of size 4
+ * \return 0 on success
+ * \note A read operation will look like this on the bus:
+ * START, I2C_ADDR(W), REG_ADDR, REP_START, I2C_ADDR(R), DATA[0], STOP
+ * START, I2C_ADDR(W), REG_ADDR+1, REP_START, I2C_ADDR(R), DATA[1], STOP
+ * START, I2C_ADDR(W), REG_ADDR+2, REP_START, I2C_ADDR(R), DATA[2], STOP
+ * START, I2C_ADDR(W), REG_ADDR+3, REP_START, I2C_ADDR(R), DATA[3], STOP
+ */
+FL2K_API int fl2k_i2c_read(fl2k_dev_t *dev, uint8_t i2c_addr,
+ uint8_t reg_addr, uint8_t *data);
+
+/*!
+ * Write 4 bytes via the FL2K I2C bus
+ *
+ * \param dev the device handle given by fl2k_open()
+ * \param i2c_addr address of the I2C device
+ * \param reg_addr start address of the 4 bytes to be written
+ * \param data pointer to byte array of size 4
+ * \return 0 on success
+ * \note A write operation will look like this on the bus:
+ * START, I2C_ADDR(W), REG_ADDR, DATA[0], STOP
+ * START, I2C_ADDR(W), REG_ADDR+1, DATA[1], STOP
+ * START, I2C_ADDR(W), REG_ADDR+2, DATA[2], STOP
+ * START, I2C_ADDR(W), REG_ADDR+3, DATA[3], STOP
+ */
+FL2K_API int fl2k_i2c_write(fl2k_dev_t *dev, uint8_t i2c_addr,
+ uint8_t reg_addr, uint8_t *data);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __FL2K_H */
diff --git a/c_sources/osmo-fl2k_export.h b/c_sources/osmo-fl2k_export.h
new file mode 100644
index 0000000..71cba1a
--- /dev/null
+++ b/c_sources/osmo-fl2k_export.h
@@ -0,0 +1,51 @@
+/*
+ * osmo-fl2k, turns FL2000-based USB 3.0 to VGA adapters into
+ * low cost DACs
+ *
+ * Copyright (C) 2016-2018 by Steve Markgraf <steve@steve-m.de>
+ *
+ * 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/>.
+ */
+
+#ifndef FL2K_EXPORT_H
+#define FL2K_EXPORT_H
+
+#if defined __GNUC__
+# if __GNUC__ >= 4
+# define __FL2K_EXPORT __attribute__((visibility("default")))
+# define __FL2K_IMPORT __attribute__((visibility("default")))
+# else
+# define __FL2K_EXPORT
+# define __FL2K_IMPORT
+# endif
+#elif _MSC_VER
+# define __FL2K_EXPORT __declspec(dllexport)
+# define __FL2K_IMPORT __declspec(dllimport)
+#else
+# define __FL2K_EXPORT
+# define __FL2K_IMPORT
+#endif
+
+#ifndef libosmofl2k_STATIC
+# ifdef fl2k_EXPORTS
+# define FL2K_API __FL2K_EXPORT
+# else
+# define FL2K_API __FL2K_IMPORT
+# endif
+#else
+#define FL2K_API
+#endif
+#endif /* FL2K_EXPORT_H */
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");
+}