diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Cargo.lock | 290 | ||||
-rw-r--r-- | Cargo.toml | 13 | ||||
-rw-r--r-- | README.md | 10 | ||||
-rw-r--r-- | build.rs | 36 | ||||
-rw-r--r-- | c_sources/libosmo-fl2k.c | 1117 | ||||
-rw-r--r-- | c_sources/osmo-fl2k.h | 163 | ||||
-rw-r--r-- | c_sources/osmo-fl2k_export.h | 51 | ||||
-rw-r--r-- | src/fl2k.rs | 52 | ||||
-rw-r--r-- | src/lib.rs | 5 | ||||
-rw-r--r-- | src/main.rs | 346 |
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, ®); + 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, ®); + 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, ®); + 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, ®); + 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"); +} |