diff options
author | Matthias P. Braendli <matthias.braendli@mpb.li> | 2022-12-31 14:36:18 +0100 |
---|---|---|
committer | Matthias P. Braendli <matthias.braendli@mpb.li> | 2022-12-31 14:36:18 +0100 |
commit | ba0aeee005d5fdaaab59cd7c099a237f51eddc86 (patch) | |
tree | a4819820cfb6fc9d2d126ab6050ba798bf925359 /c_sources | |
download | fl2k_ampliphase-ba0aeee005d5fdaaab59cd7c099a237f51eddc86.tar.gz fl2k_ampliphase-ba0aeee005d5fdaaab59cd7c099a237f51eddc86.tar.bz2 fl2k_ampliphase-ba0aeee005d5fdaaab59cd7c099a237f51eddc86.zip |
Create project
Diffstat (limited to 'c_sources')
-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 |
3 files changed, 1331 insertions, 0 deletions
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 */ |