├── .gitignore ├── .vscode └── tasks.json ├── Makefile ├── README.md ├── defaultwaveform.png ├── dumpFile.c ├── frequencylist.txt ├── libosmo-fl2k.c ├── modulatedFile.c ├── osmo-fl2k.h ├── vgaplay.c ├── vgaplay.geany ├── vgaplay.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist └── wspr_encode.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | vgaplay 3 | 4 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build ", 8 | "type": "shell", 9 | "command": "make" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | HEADERS = osmo-fl2k.h 2 | INCLUDES=-I/usr/include/libusb-1.0/ -I/usr/local/Cellar/libusb/1.0.22/include/libusb-1.0/ 3 | LDFLAGS=-lusb-1.0 -pthread -lm 4 | 5 | default: vgaplay 6 | 7 | vgaplay.o: vgaplay.c $(HEADERS) 8 | gcc -c vgaplay.c -o vgaplay.o $(INCLUDES) 9 | 10 | libosmo-fl2k.o: libosmo-fl2k.c $(HEADERS) 11 | gcc -c libosmo-fl2k.c -o libosmo-fl2k.o $(INCLUDES) 12 | 13 | vgaplay: vgaplay.o libosmo-fl2k.o 14 | gcc -ggdb vgaplay.o libosmo-fl2k.o -o vgaplay $(LDFLAGS) 15 | 16 | clean: 17 | -rm -f vgaplay.o 18 | -rm -f libosmo-fl2k.o 19 | -rm -f vgaplay 20 | -rm -f modulatedFile.o 21 | 22 | modulatedFile.o: modulatedFile.c 23 | gcc modulatedFile.c -c 24 | 25 | modulatedFile: modulatedFile.o 26 | gcc modulatedFile.o -o modulatedFile -lm 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fl2k 2 | Use cheap VGA dongles as a digital to analog converter for ham radio 3 | 4 | Based on great work from [Steve Markgraf](https://osmocom.org/projects/osmo-fl2k/wiki) 5 | 6 | This version is on [Github](https://github.com/peterbmarks/fl2k) 7 | I have a [Blog post](http://blog.marxy.org/2018/04/first-play-with-osmo-fl2k-compatible.html) about my experiments so far 8 | 9 | # Objective 10 | 11 | I'm stripping back the supplied FM example and statically linking 12 | the library code to make it easy to play with. 13 | 14 | Longer term I'd like to be able to generate a decent HF signal that 15 | can be modulated for things like WSPR. 16 | 17 | # Build 18 | 19 | On Ubuntu you might need: 20 | ``` 21 | sudo apt install git build-essential libusb-1.0-0-dev 22 | ``` 23 | 24 | To build just type: 25 | ``` 26 | make 27 | ``` 28 | 29 | You'll need to install the software this is derived from or you'll get this 30 | error: 31 | 32 | ``` 33 | libusb: error [_get_usbfs_fd] libusb couldn't open USB device /dev/bus/usb/002/014: Permission denied 34 | libusb: error [_get_usbfs_fd] libusb requires write access to USB device nodes. 35 | usb_open error -3 36 | Please fix the device permissions, e.g. by installing the udev rules file 37 | ``` 38 | 39 | Build and install the software as documented [here](https://osmocom.org/projects/osmo-fl2k/wiki) 40 | 41 | ## macOS 42 | All builds just fine on macOS but you need libusb which I installed using [homebrew](https://brew.sh) 43 | ``` 44 | brew install libusb 45 | ``` 46 | 47 | # USB memory 48 | 49 | You'll get this error: 50 | ``` 51 | libusb: error [op_dev_mem_alloc] alloc dev mem failed errno 12 52 | Failed to allocate zerocopy buffer for transfer 4 53 | libusb: error [submit_bulk_transfer] submiturb failed error -1 errno=12 54 | Failed to submit transfer 0 55 | Please increase your allowed usbfs buffer size with the following command: 56 | echo 0 > /sys/module/usbcore/parameters/usbfs_memory_mb 57 | ``` 58 | 59 | On Ubuntu you'll need increase the USB memory buffer by running: 60 | 61 | ``` 62 | sudo sh -c 'echo 1000 > /sys/module/usbcore/parameters/usbfs_memory_mb' 63 | ``` 64 | 65 | See [this article](https://importgeek.wordpress.com/2017/02/26/increase-usbfs-memory-limit-in-ubuntu/) 66 | for more info and how to add this to the grub command line so it's permanent. 67 | 68 | # Run 69 | Run locally. 70 | 71 | ``` 72 | ./vgaplay -s 130e6 -c 7e6 73 | ``` 74 | 75 | -s is the sample rate of the software DDS. 76 | The higher the sample rate the better the sine wave output. 77 | You can probably go up to about 150MS/s 78 | 79 | -c is the carrier frequency to generate. 80 | The closer the carrier frequency gets to half the sample rate, 81 | the more the signal becomes a square wave. 82 | 83 | Here's how the waveform looks at 7.159MHz with 150Ms/s: 84 | 85 | ![Beautiful CRO capture](/defaultwaveform.png) 86 | -------------------------------------------------------------------------------- /defaultwaveform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbmarks/fl2k/3d1226facc074587a87746a66b8e05861dec32df/defaultwaveform.png -------------------------------------------------------------------------------- /dumpFile.c: -------------------------------------------------------------------------------- 1 | // dump a binary data file with signed bytes 2 | // stop after kMaxBytesToDump 3 | // gcc dumpFile.c -o dumpFile 4 | // ./dumpFile /media/marksp/DeveloperDisk/Audio/am_out.dat >plotme.txt 5 | // 6 | // gnuplot 7 | // gnuplot> plot "plotme.txt" 8 | 9 | #include 10 | #include 11 | 12 | const long kMaxBytesToDump = 100000; 13 | 14 | int main(int argc, const char * argv[]) { 15 | if(argc < 2) { 16 | printf("Utility to dump signed byte binary files\n"); 17 | printf("Usage %s \n", argv[0]); 18 | exit(0); 19 | } 20 | const char *inFileName = argv[1]; 21 | //printf("dumping: %s\n", inFileName); 22 | FILE *infp = fopen(inFileName, "rb"); 23 | if(infp) { 24 | // get the length of the file 25 | fseek(infp, 0L, SEEK_END); 26 | long length = ftell(infp); 27 | // seek back to the start 28 | fseek(infp, 0L, SEEK_SET); 29 | if(length > kMaxBytesToDump) { 30 | length = kMaxBytesToDump; 31 | } 32 | char c; 33 | for(long position = 0; position < length; position++) { 34 | c = getc(infp); 35 | printf("%d\n", (int)c); 36 | } 37 | fclose(infp); 38 | } 39 | return 0; 40 | } 41 | -------------------------------------------------------------------------------- /frequencylist.txt: -------------------------------------------------------------------------------- 1 | 14070000 2 | 7000000 3 | 14070000 4 | 7000000 5 | 14070000 6 | 7000000 7 | 14070000 8 | 7000000 9 | 14070000 10 | 7000000 11 | 14070000 12 | 7000000 13 | 14070000 14 | 7000000 15 | 14070000 16 | 7000000 17 | -------------------------------------------------------------------------------- /libosmo-fl2k.c: -------------------------------------------------------------------------------- 1 | /* 2 | * osmo-fl2k, turns FL2000-based USB 3.0 to VGA adapters into 3 | * low cost DACs 4 | * 5 | * Copyright (C) 2016-2018 by Steve Markgraf 6 | * 7 | * SPDX-License-Identifier: GPL-2.0+ 8 | * 9 | * This program is free software: you can redistribute it and/or modify 10 | * it under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 2 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * This program is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | * GNU General Public License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public License 20 | * along with this program. If not, see . 21 | */ 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include 30 | #include 31 | #include 32 | #include // for usleep 33 | 34 | /* 35 | * All libusb callback functions should be marked with the LIBUSB_CALL macro 36 | * to ensure that they are compiled with the same calling convention as libusb. 37 | * 38 | * If the macro isn't available in older libusb versions, we simply define it. 39 | */ 40 | #ifndef LIBUSB_CALL 41 | #define LIBUSB_CALL 42 | #endif 43 | 44 | /* libusb < 1.0.9 doesn't have libusb_handle_events_timeout_completed */ 45 | #ifndef HAVE_LIBUSB_HANDLE_EVENTS_TIMEOUT_COMPLETED 46 | #define libusb_handle_events_timeout_completed(ctx, tv, c) \ 47 | libusb_handle_events_timeout(ctx, tv) 48 | #endif 49 | 50 | #include "osmo-fl2k.h" 51 | 52 | enum fl2k_async_status { 53 | FL2K_INACTIVE = 0, 54 | FL2K_CANCELING, 55 | FL2K_RUNNING 56 | }; 57 | 58 | typedef enum fl2k_buf_state { 59 | BUF_EMPTY = 0, 60 | BUF_SUBMITTED, 61 | BUF_FILLED, 62 | } fl2k_buf_state_t; 63 | 64 | typedef struct fl2k_xfer_info { 65 | fl2k_dev_t *dev; 66 | uint64_t seq; 67 | fl2k_buf_state_t state; 68 | } fl2k_xfer_info_t; 69 | 70 | struct fl2k_dev { 71 | libusb_context *ctx; 72 | struct libusb_device_handle *devh; 73 | uint32_t xfer_num; 74 | uint32_t xfer_buf_num; 75 | uint32_t xfer_buf_len; 76 | struct libusb_transfer **xfer; 77 | unsigned char **xfer_buf; 78 | 79 | fl2k_xfer_info_t *xfer_info; 80 | 81 | fl2k_tx_cb_t cb; 82 | void *cb_ctx; 83 | enum fl2k_async_status async_status; 84 | int async_cancel; 85 | 86 | int use_zerocopy; 87 | int terminate; 88 | 89 | /* thread related */ 90 | pthread_t usb_worker_thread; 91 | pthread_t sample_worker_thread; 92 | pthread_mutex_t buf_mutex; 93 | pthread_cond_t buf_cond; 94 | 95 | double rate; /* Hz */ 96 | 97 | /* status */ 98 | int dev_lost; 99 | int driver_active; 100 | uint32_t underflow_cnt; 101 | }; 102 | 103 | typedef struct fl2k_dongle { 104 | uint16_t vid; 105 | uint16_t pid; 106 | const char *name; 107 | } fl2k_dongle_t; 108 | 109 | static fl2k_dongle_t known_devices[] = { 110 | { 0x1d5c, 0x2000, "FL2000DX OEM" }, 111 | }; 112 | 113 | #define DEFAULT_BUF_NUMBER 4 114 | 115 | #define CTRL_IN (LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_ENDPOINT_IN) 116 | #define CTRL_OUT (LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_ENDPOINT_OUT) 117 | #define CTRL_TIMEOUT 300 118 | #define BULK_TIMEOUT 0 119 | 120 | static int fl2k_read_reg(fl2k_dev_t *dev, uint16_t reg, uint32_t *val) 121 | { 122 | int r; 123 | uint8_t data[4]; 124 | 125 | if (!dev || !val) 126 | return FL2K_ERROR_INVALID_PARAM; 127 | 128 | r = libusb_control_transfer(dev->devh, CTRL_IN, 0x40, 129 | 0, reg, data, 4, CTRL_TIMEOUT); 130 | 131 | if (r < 4) 132 | fprintf(stderr, "Error, short read from register!\n"); 133 | 134 | *val = (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | data[0]; 135 | 136 | return r; 137 | } 138 | 139 | static int fl2k_write_reg(fl2k_dev_t *dev, uint16_t reg, uint32_t val) 140 | { 141 | uint8_t data[4]; 142 | 143 | if (!dev) 144 | return FL2K_ERROR_INVALID_PARAM; 145 | 146 | data[0] = val & 0xff; 147 | data[1] = (val >> 8) & 0xff; 148 | data[2] = (val >> 16) & 0xff; 149 | data[3] = (val >> 24) & 0xff; 150 | 151 | return libusb_control_transfer(dev->devh, CTRL_OUT, 0x41, 152 | 0, reg, data, 4, CTRL_TIMEOUT); 153 | } 154 | 155 | int fl2k_init_device(fl2k_dev_t *dev) 156 | { 157 | if (!dev) 158 | return FL2K_ERROR_INVALID_PARAM; 159 | 160 | /* initialization */ 161 | fl2k_write_reg(dev, 0x8020, 0xdf0000cc); 162 | 163 | /* set DAC freq to lowest value possible to avoid 164 | * underrun during init */ 165 | fl2k_write_reg(dev, 0x802c, 0x00416f3f); 166 | 167 | fl2k_write_reg(dev, 0x8048, 0x7ffb8004); 168 | fl2k_write_reg(dev, 0x803c, 0xd701004d); 169 | fl2k_write_reg(dev, 0x8004, 0x0000031c); 170 | fl2k_write_reg(dev, 0x8004, 0x0010039d); 171 | fl2k_write_reg(dev, 0x8008, 0x07800898); 172 | 173 | fl2k_write_reg(dev, 0x801c, 0x00000000); 174 | fl2k_write_reg(dev, 0x0070, 0x04186085); 175 | 176 | /* blanking magic */ 177 | fl2k_write_reg(dev, 0x8008, 0xfeff0780); 178 | fl2k_write_reg(dev, 0x800c, 0x0000f001); 179 | 180 | /* VSYNC magic */ 181 | fl2k_write_reg(dev, 0x8010, 0x0400042a); 182 | fl2k_write_reg(dev, 0x8014, 0x0010002d); 183 | 184 | fl2k_write_reg(dev, 0x8004, 0x00000002); 185 | 186 | return 0; 187 | } 188 | 189 | int fl2k_deinit_device(fl2k_dev_t *dev) 190 | { 191 | int r = 0; 192 | 193 | if (!dev) 194 | return FL2K_ERROR_INVALID_PARAM; 195 | 196 | /* TODO, power down DACs, PLL, put device in reset */ 197 | 198 | return r; 199 | } 200 | 201 | static double fl2k_reg_to_freq(uint32_t reg) 202 | { 203 | double sample_clock, offset, offs_div; 204 | uint32_t pll_clock = 160000000; 205 | uint8_t div = reg & 0x3f; 206 | uint8_t out_div = (reg >> 8) & 0xf; 207 | uint8_t frac = (reg >> 16) & 0xf; 208 | uint8_t mult = (reg >> 20) & 0xf; 209 | 210 | sample_clock = (pll_clock * mult) / (uint32_t)div; 211 | offs_div = (pll_clock / 5.0f ) * mult; 212 | offset = ((double)sample_clock/(offs_div/2)) * 1000000.0f; 213 | sample_clock += (uint32_t)offset * frac; 214 | sample_clock /= out_div; 215 | 216 | // fprintf(stderr, "div: %d\tod: %d\tfrac: %d\tmult %d\tclock: %f\treg " 217 | // "%08x\n", div, out_div, frac, mult, sample_clock, reg); 218 | 219 | return sample_clock; 220 | } 221 | 222 | int fl2k_set_sample_rate(fl2k_dev_t *dev, uint32_t target_freq) 223 | { 224 | double sample_clock, error, last_error = 1e20f; 225 | uint32_t reg = 0, result_reg = 0; 226 | uint8_t div, mult, frac, out_div; 227 | 228 | if (!dev) 229 | return FL2K_ERROR_INVALID_PARAM; 230 | 231 | /* Output divider (accepts value 1-15) 232 | * works, but adds lots of phase noise, so do not use it */ 233 | out_div = 1; 234 | 235 | /* Observation: PLL multiplier of 7 works, but has more phase 236 | * noise. Prefer multiplier 6 and 5 */ 237 | for (mult = 6; mult >= 3; mult--) { 238 | for (div = 63; div > 1; div--) { 239 | for (frac = 1; frac <= 15; frac++) { 240 | reg = (mult << 20) | (frac << 16) | 241 | (0x60 << 8) | (out_div << 8) | div; 242 | 243 | sample_clock = fl2k_reg_to_freq(reg); 244 | error = sample_clock - (double)target_freq; 245 | 246 | /* Keep closest match */ 247 | if (fabsf(error) < last_error) { 248 | result_reg = reg; 249 | last_error = fabsf(error); 250 | } 251 | } 252 | } 253 | } 254 | 255 | sample_clock = fl2k_reg_to_freq(result_reg); 256 | error = sample_clock - (double)target_freq; 257 | dev->rate = sample_clock; 258 | 259 | if (fabsf(error) > 1) 260 | fprintf(stderr, "Requested sample rate %d not possible, using" 261 | " %f, error is %f\n", target_freq, sample_clock, error); 262 | 263 | return fl2k_write_reg(dev, 0x802c, result_reg); 264 | } 265 | 266 | uint32_t fl2k_get_sample_rate(fl2k_dev_t *dev) 267 | { 268 | if (!dev) 269 | return 0; 270 | 271 | return (uint32_t)dev->rate; 272 | } 273 | 274 | static fl2k_dongle_t *find_known_device(uint16_t vid, uint16_t pid) 275 | { 276 | unsigned int i; 277 | fl2k_dongle_t *device = NULL; 278 | 279 | for (i = 0; i < sizeof(known_devices)/sizeof(fl2k_dongle_t); i++ ) { 280 | if (known_devices[i].vid == vid && known_devices[i].pid == pid) { 281 | device = &known_devices[i]; 282 | break; 283 | } 284 | } 285 | 286 | return device; 287 | } 288 | 289 | uint32_t fl2k_get_device_count(void) 290 | { 291 | int i,r; 292 | libusb_context *ctx; 293 | libusb_device **list; 294 | uint32_t device_count = 0; 295 | struct libusb_device_descriptor dd; 296 | ssize_t cnt; 297 | 298 | r = libusb_init(&ctx); 299 | if (r < 0) 300 | return 0; 301 | 302 | cnt = libusb_get_device_list(ctx, &list); 303 | 304 | for (i = 0; i < cnt; i++) { 305 | libusb_get_device_descriptor(list[i], &dd); 306 | 307 | if (find_known_device(dd.idVendor, dd.idProduct)) 308 | device_count++; 309 | } 310 | 311 | libusb_free_device_list(list, 1); 312 | 313 | libusb_exit(ctx); 314 | 315 | return device_count; 316 | } 317 | 318 | const char *fl2k_get_device_name(uint32_t index) 319 | { 320 | int i,r; 321 | libusb_context *ctx; 322 | libusb_device **list; 323 | struct libusb_device_descriptor dd; 324 | fl2k_dongle_t *device = NULL; 325 | uint32_t device_count = 0; 326 | ssize_t cnt; 327 | 328 | r = libusb_init(&ctx); 329 | if (r < 0) 330 | return ""; 331 | 332 | cnt = libusb_get_device_list(ctx, &list); 333 | 334 | for (i = 0; i < cnt; i++) { 335 | libusb_get_device_descriptor(list[i], &dd); 336 | 337 | device = find_known_device(dd.idVendor, dd.idProduct); 338 | 339 | if (device) { 340 | device_count++; 341 | 342 | if (index == device_count - 1) 343 | break; 344 | } 345 | } 346 | 347 | libusb_free_device_list(list, 1); 348 | 349 | libusb_exit(ctx); 350 | 351 | if (device) 352 | return device->name; 353 | else 354 | return ""; 355 | } 356 | 357 | int fl2k_open(fl2k_dev_t **out_dev, uint32_t index) 358 | { 359 | int r; 360 | int i; 361 | libusb_device **list; 362 | fl2k_dev_t *dev = NULL; 363 | libusb_device *device = NULL; 364 | uint32_t device_count = 0; 365 | struct libusb_device_descriptor dd; 366 | uint8_t reg; 367 | ssize_t cnt; 368 | 369 | dev = malloc(sizeof(fl2k_dev_t)); 370 | if (NULL == dev) 371 | return -ENOMEM; 372 | 373 | memset(dev, 0, sizeof(fl2k_dev_t)); 374 | 375 | r = libusb_init(&dev->ctx); 376 | if(r < 0){ 377 | free(dev); 378 | return -1; 379 | } 380 | 381 | libusb_set_debug(dev->ctx, 3); 382 | 383 | dev->dev_lost = 1; 384 | 385 | cnt = libusb_get_device_list(dev->ctx, &list); 386 | 387 | for (i = 0; i < cnt; i++) { 388 | device = list[i]; 389 | 390 | libusb_get_device_descriptor(list[i], &dd); 391 | 392 | if (find_known_device(dd.idVendor, dd.idProduct)) { 393 | device_count++; 394 | } 395 | 396 | if (index == device_count - 1) 397 | break; 398 | 399 | device = NULL; 400 | } 401 | 402 | if (!device) { 403 | r = -1; 404 | goto err; 405 | } 406 | 407 | r = libusb_open(device, &dev->devh); 408 | libusb_free_device_list(list, 1); 409 | if (r < 0) { 410 | fprintf(stderr, "usb_open error %d\n", r); 411 | if(r == LIBUSB_ERROR_ACCESS) 412 | fprintf(stderr, "Please fix the device permissions, e.g. " 413 | "by installing the udev rules file\n"); 414 | goto err; 415 | } 416 | 417 | /* If the adapter has an SPI flash for the Windows driver, we 418 | * need to detach the USB mass storage driver first in order to 419 | * open the device */ 420 | if (libusb_kernel_driver_active(dev->devh, 3) == 1) { 421 | fprintf(stderr, "Kernel mass storage driver is attached, " 422 | "detaching driver. This may take more than" 423 | " 10 seconds!\n"); 424 | r = libusb_detach_kernel_driver(dev->devh, 3); 425 | if (r < 0) { 426 | fprintf(stderr, "Failed to detach mass storage " 427 | "driver: %d\n", r); 428 | goto err; 429 | } 430 | } 431 | 432 | r = libusb_claim_interface(dev->devh, 0); 433 | if (r < 0) { 434 | fprintf(stderr, "usb_claim_interface 0 error %d\n", r); 435 | goto err; 436 | } 437 | r = libusb_claim_interface(dev->devh, 1); 438 | 439 | if (r < 0) { 440 | fprintf(stderr, "usb_claim_interface 1 error %d\n", r); 441 | goto err; 442 | } 443 | 444 | r = fl2k_init_device(dev); 445 | if (r < 0) 446 | goto err; 447 | 448 | dev->dev_lost = 0; 449 | 450 | found: 451 | *out_dev = dev; 452 | 453 | return 0; 454 | err: 455 | if (dev) { 456 | if (dev->ctx) 457 | libusb_exit(dev->ctx); 458 | 459 | free(dev); 460 | } 461 | 462 | return r; 463 | } 464 | 465 | int fl2k_close(fl2k_dev_t *dev) 466 | { 467 | if (!dev) 468 | return FL2K_ERROR_INVALID_PARAM; 469 | 470 | if(!dev->dev_lost) { 471 | /* block until all async operations have been completed (if any) */ 472 | while (FL2K_INACTIVE != dev->async_status) { 473 | usleep(1000); 474 | } 475 | 476 | fl2k_deinit_device(dev); 477 | } 478 | 479 | libusb_release_interface(dev->devh, 0); 480 | libusb_close(dev->devh); 481 | libusb_exit(dev->ctx); 482 | 483 | free(dev); 484 | 485 | return 0; 486 | } 487 | 488 | static struct libusb_transfer *fl2k_get_next_xfer(fl2k_dev_t *dev, 489 | fl2k_buf_state_t state) 490 | { 491 | unsigned int i; 492 | int next_buf = -1; 493 | uint64_t next_seq = 0; 494 | fl2k_xfer_info_t *xfer_info; 495 | 496 | for (i = 0; i < dev->xfer_buf_num; i++) { 497 | xfer_info = (fl2k_xfer_info_t *)dev->xfer[i]->user_data; 498 | if (!xfer_info) 499 | continue; 500 | 501 | if (xfer_info->state == state) { 502 | if (state == BUF_EMPTY) { 503 | return dev->xfer[i]; 504 | } else if ((xfer_info->seq < next_seq) || next_buf < 0) { 505 | next_seq = xfer_info->seq; 506 | next_buf = i; 507 | } 508 | } 509 | } 510 | 511 | if ((state == BUF_FILLED) && (next_buf >= 0)) 512 | return dev->xfer[next_buf]; 513 | else 514 | return NULL; 515 | } 516 | 517 | static void LIBUSB_CALL _libusb_callback(struct libusb_transfer *xfer) 518 | { 519 | fl2k_xfer_info_t *xfer_info = (fl2k_xfer_info_t *)xfer->user_data; 520 | fl2k_xfer_info_t *next_xfer_info; 521 | fl2k_dev_t *dev = (fl2k_dev_t *)xfer_info->dev; 522 | struct libusb_transfer *next_xfer = NULL; 523 | 524 | if (LIBUSB_TRANSFER_COMPLETED == xfer->status) { 525 | /* resubmit transfer */ 526 | if (FL2K_RUNNING == dev->async_status) { 527 | /* get next transfer */ 528 | next_xfer = fl2k_get_next_xfer(dev, BUF_FILLED); 529 | 530 | if (next_xfer) { 531 | next_xfer_info = (fl2k_xfer_info_t *) next_xfer->user_data; 532 | 533 | /* Submit next filled transfer */ 534 | next_xfer_info->state = BUF_SUBMITTED; 535 | libusb_submit_transfer(next_xfer); 536 | 537 | xfer_info->state = BUF_EMPTY; 538 | pthread_cond_signal(&dev->buf_cond); 539 | } else { 540 | /* We need to re-submit the transfer 541 | * in any case, as otherwise the device 542 | * stops to output data and hangs 543 | * (happens only in the hacked 'gapless' 544 | * mode without HSYNC and VSYNC) */ 545 | libusb_submit_transfer(xfer); 546 | pthread_cond_signal(&dev->buf_cond); 547 | dev->underflow_cnt++; 548 | } 549 | } 550 | } else if (LIBUSB_TRANSFER_CANCELLED != xfer->status) { 551 | dev->dev_lost = 1; 552 | fl2k_stop_tx(dev); 553 | pthread_cond_signal(&dev->buf_cond); 554 | fprintf(stderr, "cb transfer status: %d, " 555 | "canceling...\n", xfer->status); 556 | } 557 | } 558 | 559 | static int fl2k_alloc_submit_transfers(fl2k_dev_t *dev) 560 | { 561 | unsigned int i; 562 | int r = 0; 563 | 564 | if (!dev) 565 | return FL2K_ERROR_INVALID_PARAM; 566 | 567 | dev->xfer = malloc(dev->xfer_buf_num * sizeof(struct libusb_transfer *)); 568 | 569 | for (i = 0; i < dev->xfer_buf_num; ++i) 570 | dev->xfer[i] = libusb_alloc_transfer(0); 571 | 572 | dev->xfer_buf = malloc(dev->xfer_buf_num * sizeof(unsigned char *)); 573 | dev->xfer_info = malloc(dev->xfer_buf_num * sizeof(fl2k_xfer_info_t)); 574 | 575 | #if defined (__linux__) && LIBUSB_API_VERSION >= 0x01000105 576 | fprintf(stderr, "Using %d zero-copy buffers\n", dev->xfer_buf_num); 577 | 578 | dev->use_zerocopy = 1; 579 | for (i = 0; i < dev->xfer_buf_num; ++i) { 580 | dev->xfer_buf[i] = libusb_dev_mem_alloc(dev->devh, dev->xfer_buf_len); 581 | 582 | if (!dev->xfer_buf[i]) { 583 | fprintf(stderr, "Failed to allocate zerocopy" 584 | " buffer for transfer %d\n", 585 | i); 586 | 587 | // TODO: free dev_mem buffers again 588 | dev->use_zerocopy = 0; 589 | break; 590 | } 591 | } 592 | #endif 593 | 594 | if (!dev->use_zerocopy) { 595 | for (i = 0; i < dev->xfer_buf_num; ++i) { 596 | dev->xfer_buf[i] = malloc(dev->xfer_buf_len); 597 | 598 | if (!dev->xfer_buf[i]) 599 | return FL2K_ERROR_NO_MEM; 600 | } 601 | } 602 | 603 | /* fill transfers */ 604 | for (i = 0; i < dev->xfer_buf_num; ++i) { 605 | libusb_fill_bulk_transfer(dev->xfer[i], 606 | dev->devh, 607 | 0x01, 608 | dev->xfer_buf[i], 609 | dev->xfer_buf_len, 610 | _libusb_callback, 611 | &dev->xfer_info[i], 612 | 0); 613 | 614 | dev->xfer_info[i].dev = dev; 615 | dev->xfer_info[i].state = BUF_EMPTY; 616 | 617 | /* if we allocate the memory through the Kernel, it is 618 | * already cleared */ 619 | if (!dev->use_zerocopy) 620 | memset(dev->xfer_buf[i], 0, dev->xfer_buf_len); 621 | } 622 | 623 | /* submit transfers */ 624 | for (i = 0; i < dev->xfer_num; ++i) { 625 | r = libusb_submit_transfer(dev->xfer[i]); 626 | dev->xfer_info[i].state = BUF_SUBMITTED; 627 | 628 | if (r < 0) { 629 | fprintf(stderr, "Failed to submit transfer %i\n" 630 | "Please increase your allowed " 631 | "usbfs buffer size with the " 632 | "following command:\n" 633 | "echo 0 > /sys/module/usbcore" 634 | "/parameters/usbfs_memory_mb\n", i); 635 | break; 636 | } 637 | } 638 | 639 | return 0; 640 | } 641 | 642 | static int _fl2k_free_async_buffers(fl2k_dev_t *dev) 643 | { 644 | unsigned int i; 645 | 646 | if (!dev) 647 | return FL2K_ERROR_INVALID_PARAM; 648 | 649 | if (dev->xfer) { 650 | for (i = 0; i < dev->xfer_buf_num; ++i) { 651 | if (dev->xfer[i]) { 652 | libusb_free_transfer(dev->xfer[i]); 653 | } 654 | } 655 | 656 | free(dev->xfer); 657 | dev->xfer = NULL; 658 | } 659 | 660 | if (dev->xfer_buf) { 661 | for (i = 0; i < dev->xfer_buf_num; ++i) { 662 | if (dev->xfer_buf[i]) { 663 | if (dev->use_zerocopy) { 664 | #if defined (__linux__) && LIBUSB_API_VERSION >= 0x01000105 665 | libusb_dev_mem_free(dev->devh, 666 | dev->xfer_buf[i], 667 | dev->xfer_buf_len); 668 | #endif 669 | } else { 670 | free(dev->xfer_buf[i]); 671 | } 672 | } 673 | } 674 | 675 | free(dev->xfer_buf); 676 | dev->xfer_buf = NULL; 677 | } 678 | 679 | return 0; 680 | } 681 | 682 | static void *fl2k_usb_worker(void *arg) 683 | { 684 | fl2k_dev_t *dev = (fl2k_dev_t *)arg; 685 | struct timeval tv = { 1, 0 }; 686 | struct timeval zerotv = { 0, 0 }; 687 | enum fl2k_async_status next_status = FL2K_INACTIVE; 688 | int r = 0; 689 | unsigned int i; 690 | 691 | while (FL2K_RUNNING == dev->async_status) { 692 | r = libusb_handle_events_timeout_completed(dev->ctx, &tv, 693 | &dev->async_cancel); 694 | } 695 | 696 | while (FL2K_INACTIVE != dev->async_status) { 697 | r = libusb_handle_events_timeout_completed(dev->ctx, &tv, 698 | &dev->async_cancel); 699 | if (r < 0) { 700 | /*fprintf(stderr, "handle_events returned: %d\n", r);*/ 701 | if (r == LIBUSB_ERROR_INTERRUPTED) /* stray signal */ 702 | continue; 703 | break; 704 | } 705 | 706 | if (FL2K_CANCELING == dev->async_status) { 707 | next_status = FL2K_INACTIVE; 708 | 709 | if (!dev->xfer) 710 | break; 711 | 712 | for (i = 0; i < dev->xfer_buf_num; ++i) { 713 | if (!dev->xfer[i]) 714 | continue; 715 | 716 | if (LIBUSB_TRANSFER_CANCELLED != 717 | dev->xfer[i]->status) { 718 | r = libusb_cancel_transfer(dev->xfer[i]); 719 | /* handle events after canceling 720 | * to allow transfer status to 721 | * propagate */ 722 | libusb_handle_events_timeout_completed(dev->ctx, 723 | &zerotv, NULL); 724 | if (r < 0) 725 | continue; 726 | 727 | next_status = FL2K_CANCELING; 728 | } 729 | } 730 | 731 | if (dev->dev_lost || FL2K_INACTIVE == next_status) { 732 | /* handle any events that still need to 733 | * be handled before exiting after we 734 | * just cancelled all transfers */ 735 | libusb_handle_events_timeout_completed(dev->ctx, 736 | &zerotv, NULL); 737 | break; 738 | } 739 | } 740 | } 741 | 742 | _fl2k_free_async_buffers(dev); 743 | dev->async_status = next_status; 744 | 745 | pthread_exit(NULL); 746 | } 747 | 748 | /* Buffer format conversion functions for R, G, B DACs */ 749 | static inline void fl2k_convert_r(char *out, 750 | char *in, 751 | uint32_t len, 752 | uint8_t offset) 753 | { 754 | unsigned int i, j = 0; 755 | 756 | if (!in || !out) 757 | return; 758 | 759 | for (i = 0; i < len; i += 24) { 760 | out[i+ 6] = in[j++] + offset; 761 | out[i+ 1] = in[j++] + offset; 762 | out[i+12] = in[j++] + offset; 763 | out[i+15] = in[j++] + offset; 764 | out[i+10] = in[j++] + offset; 765 | out[i+21] = in[j++] + offset; 766 | out[i+16] = in[j++] + offset; 767 | out[i+19] = in[j++] + offset; 768 | } 769 | } 770 | 771 | static inline void fl2k_convert_g(char *out, 772 | char *in, 773 | uint32_t len, 774 | uint8_t offset) 775 | { 776 | unsigned int i, j = 0; 777 | 778 | if (!in || !out) 779 | return; 780 | 781 | for (i = 0; i < len; i += 24) { 782 | out[i+ 5] = in[j++] + offset; 783 | out[i+ 0] = in[j++] + offset; 784 | out[i+ 3] = in[j++] + offset; 785 | out[i+14] = in[j++] + offset; 786 | out[i+ 9] = in[j++] + offset; 787 | out[i+20] = in[j++] + offset; 788 | out[i+23] = in[j++] + offset; 789 | out[i+18] = in[j++] + offset; 790 | } 791 | } 792 | 793 | static inline void fl2k_convert_b(char *out, 794 | char *in, 795 | uint32_t len, 796 | uint8_t offset) 797 | { 798 | unsigned int i, j = 0; 799 | 800 | if (!in || !out) 801 | return; 802 | 803 | for (i = 0; i < len; i += 24) { 804 | out[i+ 4] = in[j++] + offset; 805 | out[i+ 7] = in[j++] + offset; 806 | out[i+ 2] = in[j++] + offset; 807 | out[i+13] = in[j++] + offset; 808 | out[i+ 8] = in[j++] + offset; 809 | out[i+11] = in[j++] + offset; 810 | out[i+22] = in[j++] + offset; 811 | out[i+17] = in[j++] + offset; 812 | } 813 | } 814 | 815 | static void *fl2k_sample_worker(void *arg) 816 | { 817 | int r = 0; 818 | unsigned int i, j; 819 | fl2k_dev_t *dev = (fl2k_dev_t *)arg; 820 | fl2k_xfer_info_t *xfer_info = NULL; 821 | struct libusb_transfer *xfer = NULL; 822 | char *out_buf = NULL; 823 | fl2k_data_info_t data_info; 824 | uint32_t underflows = 0; 825 | uint64_t buf_cnt = 0; 826 | 827 | while (FL2K_RUNNING == dev->async_status) { 828 | memset(&data_info, 0, sizeof(fl2k_data_info_t)); 829 | 830 | data_info.len = FL2K_BUF_LEN; 831 | data_info.underflow_cnt = dev->underflow_cnt; 832 | data_info.ctx = dev->cb_ctx; 833 | 834 | if (dev->underflow_cnt > underflows) { 835 | fprintf(stderr, "Underflow! Skipped %d buffers\n", 836 | dev->underflow_cnt - underflows); 837 | underflows = dev->underflow_cnt; 838 | } 839 | 840 | /* call application callback to get samples */ 841 | if (dev->cb) 842 | dev->cb(&data_info); 843 | 844 | xfer = fl2k_get_next_xfer(dev, BUF_EMPTY); 845 | 846 | if (!xfer) { 847 | pthread_cond_wait(&dev->buf_cond, &dev->buf_mutex); 848 | /* in the meantime, the device might be gone */ 849 | if (FL2K_RUNNING != dev->async_status) 850 | break; 851 | 852 | xfer = fl2k_get_next_xfer(dev, BUF_EMPTY); 853 | if (!xfer) { 854 | fprintf(stderr, "no free transfer, skipping" 855 | " input buffer\n"); 856 | continue; 857 | } 858 | } 859 | 860 | /* We have an empty USB transfer buffer */ 861 | xfer_info = (fl2k_xfer_info_t *)xfer->user_data; 862 | out_buf = (char *)xfer->buffer; 863 | 864 | /* Re-arrange and copy bytes in buffer for DACs */ 865 | fl2k_convert_r(out_buf, data_info.r_buf, dev->xfer_buf_len, 866 | data_info.sampletype_signed ? 128 : 0); 867 | 868 | fl2k_convert_g(out_buf, data_info.g_buf, dev->xfer_buf_len, 869 | data_info.sampletype_signed ? 128 : 0); 870 | 871 | fl2k_convert_b(out_buf, data_info.b_buf, dev->xfer_buf_len, 872 | data_info.sampletype_signed ? 128 : 0); 873 | 874 | xfer_info->seq = buf_cnt++; 875 | xfer_info->state = BUF_FILLED; 876 | } 877 | 878 | /* notify application if we've lost the device */ 879 | if (dev->dev_lost && dev->cb) { 880 | data_info.device_error = 1; 881 | dev->cb(&data_info); 882 | fl2k_stop_tx(dev); 883 | } 884 | 885 | pthread_exit(NULL); 886 | } 887 | 888 | 889 | int fl2k_start_tx(fl2k_dev_t *dev, fl2k_tx_cb_t cb, void *ctx, 890 | uint32_t buf_num) 891 | { 892 | int r = 0; 893 | int i; 894 | pthread_attr_t attr; 895 | 896 | if (!dev || !cb) 897 | return FL2K_ERROR_INVALID_PARAM; 898 | 899 | dev->async_status = FL2K_RUNNING; 900 | dev->async_cancel = 0; 901 | 902 | dev->cb = cb; 903 | dev->cb_ctx = ctx; 904 | 905 | if (buf_num > 0) 906 | dev->xfer_num = buf_num; 907 | else 908 | dev->xfer_num = DEFAULT_BUF_NUMBER; 909 | 910 | /* have two spare buffers that can be filled while the 911 | * others are submitted */ 912 | dev->xfer_buf_num = dev->xfer_num + 2; 913 | dev->xfer_buf_len = FL2K_XFER_LEN; 914 | 915 | r = fl2k_alloc_submit_transfers(dev); 916 | if (r < 0) 917 | goto cleanup; 918 | 919 | pthread_mutex_init(&dev->buf_mutex, NULL); 920 | pthread_cond_init(&dev->buf_cond, NULL); 921 | pthread_attr_init(&attr); 922 | 923 | r = pthread_create(&dev->usb_worker_thread, &attr, 924 | fl2k_usb_worker, (void *)dev); 925 | if (r < 0) { 926 | fprintf(stderr, "Error spawning USB worker thread!\n"); 927 | goto cleanup; 928 | } 929 | 930 | r = pthread_create(&dev->sample_worker_thread, &attr, 931 | fl2k_sample_worker, (void *)dev); 932 | if (r < 0) { 933 | fprintf(stderr, "Error spawning sample worker thread!\n"); 934 | goto cleanup; 935 | } 936 | 937 | pthread_attr_destroy(&attr); 938 | 939 | return 0; 940 | 941 | cleanup: 942 | _fl2k_free_async_buffers(dev); 943 | return FL2K_ERROR_BUSY; 944 | 945 | } 946 | 947 | int fl2k_stop_tx(fl2k_dev_t *dev) 948 | { 949 | if (!dev) 950 | return FL2K_ERROR_INVALID_PARAM; 951 | 952 | /* if streaming, try to cancel gracefully */ 953 | if (FL2K_RUNNING == dev->async_status) { 954 | dev->async_status = FL2K_CANCELING; 955 | dev->async_cancel = 1; 956 | return 0; 957 | /* if called while in pending state, change the state forcefully */ 958 | } else if (FL2K_INACTIVE != dev->async_status) { 959 | dev->async_status = FL2K_INACTIVE; 960 | return 0; 961 | } 962 | 963 | return FL2K_ERROR_BUSY; 964 | } 965 | -------------------------------------------------------------------------------- /modulatedFile.c: -------------------------------------------------------------------------------- 1 | // 2 | // main.c 3 | // Generate AM samples to send with fl2k_file 4 | // 5 | // Created by Peter Marks on 26/5/18. 6 | // Copyright © 2018 Peter Marks. All rights reserved. 7 | // 8 | // Default sample rate for fl2k_file is 100e6 or 100MHz 9 | // so 100e6 / 60 samples gives a carrier on 1.66MHz 10 | // 80 samples gives a carrier on 1.25MHz 11 | // 12 | 13 | // fl2k_file samples.dat 14 | #include 15 | #include 16 | #include 17 | 18 | const char *outFileName = "samples.dat"; 19 | 20 | void makeCarrier(int samplesPerCycle) { 21 | FILE *outfile = fopen(outFileName, "wb"); 22 | int8_t byte; 23 | for(int sample = 0; sample < samplesPerCycle; sample++) { 24 | double current_radian = M_PI * sample * 2 / samplesPerCycle; 25 | double carrier_sin_value = sin(current_radian); 26 | byte = (int8_t)(carrier_sin_value * 127.0); 27 | printf("%f, val = %f, byte = %d\n", current_radian, carrier_sin_value, (int)byte); 28 | fwrite(&byte, sizeof(byte), 1, outfile); 29 | } 30 | fclose(outfile); 31 | } 32 | 33 | 34 | 35 | // Produces an AM'd signal suitable for fl2k_file 36 | void makeAm(int samplesPerCycle) { 37 | // ratio of the carrier to the modulating sine wave 38 | int ratio = 3000; 39 | 40 | FILE *outfile = fopen(outFileName, "wb"); 41 | // make sure we get enough samples for a full wave of the modulation 42 | int totalSamples = samplesPerCycle * ratio; 43 | for(int sample = 0; sample < totalSamples; sample++) { 44 | double carrier_radian = fmod((M_PI * sample * 2 / samplesPerCycle),(M_PI * 2)); 45 | double mod_radian = fmod((M_PI * sample * 2 / samplesPerCycle) / ratio,(M_PI * 2)); 46 | 47 | //printf("%03d carrier r = %f, mod r = %f\n", sample, carrier_radian, mod_radian); 48 | 49 | double carrier_sin_value = sin(carrier_radian); 50 | double mod_sin_value = sin(mod_radian); 51 | 52 | double am_sample = carrier_sin_value * mod_sin_value; 53 | //printf("%f\t%f\t%f\n", carrier_sin_value, mod_sin_value, am_sample); 54 | 55 | int8_t byte = (int8_t)(am_sample * 127.0); 56 | //printf("byte = %d\n", byte); 57 | fwrite(&byte, sizeof(byte), 1, outfile); 58 | } 59 | fclose(outfile); 60 | } 61 | 62 | // produce a sine wave suitable for fl2k_file 63 | int main(int argc, const char * argv[]) { 64 | //makeCarrier(80); 65 | makeAm(80); 66 | return 0; 67 | } 68 | -------------------------------------------------------------------------------- /osmo-fl2k.h: -------------------------------------------------------------------------------- 1 | /* 2 | * osmo-fl2k, turns FL2000-based USB 3.0 to VGA adapters into 3 | * low cost DACs 4 | * 5 | * Copyright (C) 2016-2018 by Steve Markgraf 6 | * 7 | * SPDX-License-Identifier: GPL-2.0+ 8 | * 9 | * This program is free software: you can redistribute it and/or modify 10 | * it under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 2 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * This program is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | * GNU General Public License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public License 20 | * along with this program. If not, see . 21 | */ 22 | 23 | #ifndef __FL2K_H 24 | #define __FL2K_H 25 | 26 | #ifdef __cplusplus 27 | extern "C" { 28 | #endif 29 | 30 | #include 31 | #include "osmo-fl2k.h" 32 | 33 | enum fl2k_error { 34 | FL2K_SUCCESS = 0, 35 | FL2K_TRUE = 1, 36 | FL2K_ERROR_INVALID_PARAM = -1, 37 | FL2K_ERROR_NO_DEVICE = -2, 38 | FL2K_ERROR_NOT_FOUND = -5, 39 | FL2K_ERROR_BUSY = -6, 40 | FL2K_ERROR_NO_MEM = -11, 41 | }; 42 | 43 | typedef struct fl2k_data_info { 44 | /* information provided by library */ 45 | void *ctx; 46 | uint32_t underflow_cnt; /* underflows since last callback */ 47 | uint32_t len; /* buffer length */ 48 | int using_zerocopy; /* using zerocopy kernel buffers */ 49 | int device_error; /* device error happened, terminate application */ 50 | 51 | /* filled in by application */ 52 | int sampletype_signed; /* are samples signed or unsigned? */ 53 | char *r_buf; /* pointer to red buffer */ 54 | char *g_buf; /* pointer to green buffer */ 55 | char *b_buf; /* pointer to blue buffer */ 56 | } fl2k_data_info_t; 57 | 58 | typedef struct fl2k_dev fl2k_dev_t; 59 | 60 | /** The transfer length was chosen by the following criteria: 61 | * - Must be a supported resolution of the FL2000DX 62 | * - Must be a multiple of 61440 bytes (URB payload length), 63 | * which is important for using the DAC without HSYNC/VSYNC blanking, 64 | * otherwise a couple of samples are missing between every buffer 65 | * - Should be smaller than 4MB in order to be allocatable by kmalloc() 66 | * for zerocopy transfers 67 | **/ 68 | #define FL2K_BUF_LEN (1280 * 1024) 69 | #define FL2K_XFER_LEN (FL2K_BUF_LEN * 3) 70 | 71 | uint32_t fl2k_get_device_count(void); 72 | 73 | const char* fl2k_get_device_name(uint32_t index); 74 | 75 | int fl2k_open(fl2k_dev_t **dev, uint32_t index); 76 | 77 | int fl2k_close(fl2k_dev_t *dev); 78 | 79 | /* configuration functions */ 80 | 81 | /*! 82 | * Set the sample rate (pixel clock) for the device 83 | * 84 | * \param dev the device handle given by fl2k_open() 85 | * \param samp_rate the sample rate to be set, maximum value depends 86 | * on host and USB controller 87 | * \return 0 on success, -EINVAL on invalid rate 88 | */ 89 | int fl2k_set_sample_rate(fl2k_dev_t *dev, uint32_t target_freq); 90 | 91 | /*! 92 | * Get actual sample rate the device is configured to. 93 | * 94 | * \param dev the device handle given by fl2k_open() 95 | * \return 0 on error, sample rate in Hz otherwise 96 | */ 97 | uint32_t fl2k_get_sample_rate(fl2k_dev_t *dev); 98 | 99 | /* streaming functions */ 100 | 101 | typedef void(*fl2k_tx_cb_t)(fl2k_data_info_t *data_info); 102 | 103 | /*! 104 | * Starts the tx thread. This function will block until 105 | * it is being canceled using fl2k_stop_tx() 106 | * 107 | * \param dev the device handle given by fl2k_open() 108 | * \param ctx user specific context to pass via the callback function 109 | * \param buf_num optional buffer count, buf_num * FL2K_BUF_LEN = overall buffer size 110 | * set to 0 for default buffer count (4) 111 | * \return 0 on success 112 | */ 113 | int fl2k_start_tx(fl2k_dev_t *dev, fl2k_tx_cb_t cb, 114 | void *ctx, uint32_t buf_num); 115 | 116 | /*! 117 | * Cancel all pending asynchronous operations on the device. 118 | * 119 | * \param dev the device handle given by fl2k_open() 120 | * \return 0 on success 121 | */ 122 | int fl2k_stop_tx(fl2k_dev_t *dev); 123 | 124 | #ifdef __cplusplus 125 | } 126 | #endif 127 | 128 | #endif /* __FL2K_H */ 129 | -------------------------------------------------------------------------------- /vgaplay.c: -------------------------------------------------------------------------------- 1 | /* 2 | * vgaplay, stripped down version of... 3 | * 4 | * osmo-fl2k, turns FL2000-based USB 3.0 to VGA adapters into 5 | * low cost DACs 6 | * 7 | * On Ubunutu: sudo sh -c 'echo 1000 > /sys/module/usbcore/parameters/usbfs_memory_mb' 8 | * 9 | * ./vgaplay -s 130e6 -c 71e5 10 | * 11 | * Copyright below 12 | */ 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | #include "osmo-fl2k.h" 29 | 30 | #define BUFFER_SAMPLES_SHIFT 16 31 | #define BUFFER_SAMPLES (1 << BUFFER_SAMPLES_SHIFT) 32 | #define BUFFER_SAMPLES_MASK ((1 << BUFFER_SAMPLES_SHIFT)-1) 33 | 34 | void dds_start(double frequency); 35 | void dds_stop(); 36 | 37 | typedef struct { 38 | double sample_freq; 39 | double freq; 40 | double fslope; 41 | unsigned long int phase; 42 | unsigned long int phase_step; 43 | //unsigned long int phase_slope; 44 | } dds_t; 45 | 46 | fl2k_dev_t *gFl2kDevicePtr = NULL; 47 | dds_t gCarrierDds; 48 | 49 | int gUserCancelled = 0; 50 | int gTransmitTimeExpired = 0; 51 | 52 | pthread_t gWorkerThread; 53 | pthread_mutex_t cb_mutex; 54 | pthread_mutex_t fm_mutex; 55 | pthread_cond_t cb_cond; 56 | 57 | int8_t *gTransmitBuffer = NULL; 58 | 59 | uint32_t gSampleRate = 150000000; 60 | double gCarrierFrequency = 7159000; 61 | double gDurationOfEachTx; 62 | int gDidSpecifyTime = 0; 63 | long long gStartTimeMs; 64 | uint32_t gFl2kDeviceIndex = 0; 65 | 66 | void usage(void) 67 | { 68 | fprintf(stderr, 69 | "vgaplay, stripped down code for FL2K VGA dongles\n\n" 70 | "Usage:" 71 | "\t[-d device index (default: 0)]\n" 72 | "\t[-c carrier frequency (default: 7.159 MHz)]\n" 73 | "\t[-s samplerate in Hz (default: 150 MS/s)]\n" 74 | "\t[-t time in seconds\n" 75 | "\t[-f read frequency list from a file\n" 76 | "./vgaplay -s 100e6 -c 10e6\n" 77 | ); 78 | exit(1); 79 | } 80 | 81 | // Catches ^C and stops 82 | static void sighandler(int signum) 83 | { 84 | fprintf(stderr, "Signal caught, exiting!\n"); 85 | dds_stop(); 86 | gUserCancelled = 1; 87 | exit(0); 88 | } 89 | 90 | /* DDS Functions */ 91 | 92 | #ifndef M_PI 93 | # define M_PI 3.14159265358979323846 /* pi */ 94 | # define M_PI_2 1.57079632679489661923 /* pi/2 */ 95 | # define M_PI_4 0.78539816339744830962 /* pi/4 */ 96 | # define M_1_PI 0.31830988618379067154 /* 1/pi */ 97 | # define M_2_PI 0.63661977236758134308 /* 2/pi */ 98 | #endif 99 | 100 | #define DDS_2PI (M_PI * 2) /* 2 * Pi */ 101 | #define DDS_3PI2 (M_PI_2 * 3) /* 3/2 * pi */ 102 | 103 | #define SIN_TABLE_ORDER 8 // 8 gives 256 values 104 | #define SIN_TABLE_SHIFT (32 - SIN_TABLE_ORDER) 105 | #define SIN_TABLE_LEN (1 << SIN_TABLE_ORDER) 106 | #define ANG_INCR (0xffffffff / DDS_2PI) 107 | 108 | int8_t gSineTable[SIN_TABLE_LEN]; // big table of sine values for DDS 109 | int gSineTableInitialised = 0; 110 | 111 | 112 | // was inline 113 | void dds_set_freq(dds_t *dds, double freq, double fslope) 114 | { 115 | fprintf(stderr, "dds_set_freq(%f\n", freq); 116 | dds->fslope = fslope; 117 | dds->phase_step = (freq / dds->sample_freq) * 2 * M_PI * ANG_INCR; 118 | fprintf(stderr, "dds->sample_freq = %f, dds->phase_step = %lu\n", dds->sample_freq, dds->phase_step); 119 | dds->freq = freq; 120 | } 121 | 122 | // write sine values to the gSineTable 123 | dds_t dds_init(double sample_freq, double freq, double phase) 124 | { 125 | dds_t dds; 126 | int i; 127 | 128 | dds.sample_freq = sample_freq; 129 | dds.phase = phase * ANG_INCR; 130 | dds_set_freq(&dds, freq, 0); 131 | 132 | // Initialize sine table, prescaled for 8 bit signed integer 133 | if (!gSineTableInitialised) { 134 | double incr = 1.0 / (double)SIN_TABLE_LEN; 135 | for (i = 0; i < SIN_TABLE_LEN; i++) { 136 | gSineTable[i] = sin(incr * i * DDS_2PI) * 127; 137 | // fprintf(stderr, "sine table value %d = %d\n", i, gSineTable[i]); 138 | } 139 | gSineTableInitialised = 1; 140 | } 141 | 142 | return dds; 143 | } 144 | 145 | // return the next value from the sine table and increment the step 146 | int8_t dds_real(dds_t *dds) 147 | { 148 | int tmp; 149 | 150 | tmp = dds->phase >> SIN_TABLE_SHIFT; 151 | dds->phase += dds->phase_step; 152 | dds->phase &= 0xffffffff; 153 | 154 | return gSineTable[tmp]; 155 | } 156 | 157 | // copy count sine samples from sine table into buf 158 | void dds_real_buf(dds_t *dds, int8_t *buf, int count) { 159 | for (int i = 0; i < count; i++) { 160 | buf[i] = dds_real(dds); 161 | } 162 | } 163 | 164 | /* Signal generation and some helpers */ 165 | 166 | /* Generate the radio signal using the pre-calculated frequency information 167 | * in the freq buffer */ 168 | // This runs in the gWorkerThread and modulates the carrier frequency 169 | static void *tx_worker_thread(void *arg) 170 | { 171 | // Prepare the DDS oscillator 172 | gCarrierDds = dds_init(gSampleRate, gCarrierFrequency, 0); 173 | // fill the transmit buffer with sine values 174 | dds_real_buf(&gCarrierDds, gTransmitBuffer, FL2K_BUF_LEN); 175 | 176 | while (!gUserCancelled && !gTransmitTimeExpired) { 177 | // stay in this thread until they ^C out 178 | if(gTransmitTimeExpired) { 179 | fprintf(stderr, "tx_worker_thread transmit time expired\n"); 180 | } 181 | } 182 | fprintf(stderr, "tx_worker_thread ending\n"); 183 | pthread_exit(NULL); 184 | } 185 | 186 | // USB calls back to get the next buffer of data 187 | void fl2k_callback(fl2k_data_info_t *data_info) 188 | { 189 | if (data_info->device_error) { 190 | gUserCancelled = 1; 191 | } 192 | 193 | pthread_cond_signal(&cb_cond); 194 | // unblock at least one of the threads that are blocked on the 195 | // specified condition variable cond (if any threads are blocked on cond). 196 | 197 | data_info->sampletype_signed = 1; 198 | data_info->r_buf = (char *)gTransmitBuffer; // in to red channel buffer 199 | } 200 | 201 | long long current_miliseconds() { 202 | struct timeval te; 203 | gettimeofday(&te, NULL); // get current time 204 | long long milliseconds = te.tv_sec*1000LL + te.tv_usec/1000; // calculate milliseconds 205 | // printf("milliseconds: %lld\n", milliseconds); 206 | return milliseconds; 207 | } 208 | 209 | void dds_start(double frequency) { 210 | int r; 211 | pthread_attr_t attr; 212 | struct sigaction sigact, sigign; 213 | 214 | fl2k_open(&gFl2kDevicePtr, gFl2kDeviceIndex); 215 | 216 | if (NULL == gFl2kDevicePtr) { 217 | fprintf(stderr, "Failed to open fl2k device #%d.\n", gFl2kDeviceIndex); 218 | exit(0); 219 | } 220 | fprintf(stderr, "Opened device\n"); 221 | 222 | fprintf(stderr, "dds_start(%f)\n", frequency); 223 | pthread_mutex_init(&cb_mutex, NULL); 224 | pthread_cond_init(&cb_cond, NULL); 225 | pthread_attr_init(&attr); 226 | 227 | r = pthread_create(&gWorkerThread, &attr, tx_worker_thread, NULL); 228 | if (r < 0) { 229 | fprintf(stderr, "Error spawning TX worker thread!\n"); 230 | return; 231 | } 232 | 233 | pthread_attr_destroy(&attr); 234 | r = fl2k_start_tx(gFl2kDevicePtr, fl2k_callback, NULL, 0); 235 | 236 | // Set the sample rate 237 | r = fl2k_set_sample_rate(gFl2kDevicePtr, gSampleRate); 238 | if (r < 0) { 239 | fprintf(stderr, "WARNING: Failed to set sample rate. %d\n", r); 240 | } 241 | 242 | /* read back actual frequency */ 243 | gSampleRate = fl2k_get_sample_rate(gFl2kDevicePtr); 244 | fprintf(stderr, "Actual sample rate = %d\n", gSampleRate); 245 | 246 | //dds_set_freq(&gCarrierDds, frequency, 0.0); 247 | 248 | sigact.sa_handler = sighandler; 249 | sigemptyset(&sigact.sa_mask); 250 | sigact.sa_flags = 0; 251 | sigign.sa_handler = SIG_IGN; 252 | sigaction(SIGINT, &sigact, NULL); 253 | sigaction(SIGTERM, &sigact, NULL); 254 | sigaction(SIGQUIT, &sigact, NULL); 255 | sigaction(SIGPIPE, &sigign, NULL); 256 | } 257 | 258 | void dds_stop() { 259 | fprintf(stderr, "dds_stop()\n"); 260 | fl2k_stop_tx(gFl2kDevicePtr); 261 | fl2k_close(gFl2kDevicePtr); 262 | } 263 | 264 | void dds_change_frequency(double frequency) { 265 | fprintf(stderr, "dds_change_frequency(%f)\n", frequency); 266 | dds_set_freq(&gCarrierDds, frequency, 0); 267 | // rebuild the transmit buffer 268 | dds_real_buf(&gCarrierDds, gTransmitBuffer, FL2K_BUF_LEN); 269 | } 270 | 271 | int main(int argc, char **argv) 272 | { 273 | int opt; 274 | 275 | int option_index = 0; 276 | FILE *frequencyFile = NULL; 277 | char frequencyFileName[FILENAME_MAX + 1]; 278 | frequencyFileName[0] = '\0'; 279 | 280 | struct sigaction sigact, sigign; 281 | 282 | static struct option long_options[] = 283 | { 284 | {0, 0, 0, 0} 285 | }; 286 | 287 | while (1) { 288 | opt = getopt_long(argc, argv, "d:c:f:s:t:", long_options, &option_index); 289 | 290 | /* end of options reached */ 291 | if (opt == -1) 292 | break; 293 | 294 | switch (opt) { 295 | case 0: 296 | break; 297 | case 'd': 298 | gFl2kDeviceIndex = (uint32_t)atoi(optarg); 299 | break; 300 | case 'c': 301 | gCarrierFrequency = atof(optarg); 302 | break; 303 | case 's': 304 | gSampleRate = (uint32_t)atof(optarg); 305 | break; 306 | case 't': 307 | gDurationOfEachTx = (double)atof(optarg); 308 | gDidSpecifyTime = 1; 309 | break; 310 | case 'f': 311 | strcpy(frequencyFileName, optarg); 312 | break; 313 | default: 314 | usage(); 315 | break; 316 | } 317 | } 318 | 319 | if (argc < optind) { 320 | usage(); 321 | } 322 | 323 | /* allocate buffer */ 324 | gTransmitBuffer = malloc(FL2K_BUF_LEN); 325 | if (!gTransmitBuffer) { 326 | fprintf(stderr, "malloc error!\n"); 327 | exit(1); 328 | } 329 | 330 | fprintf(stderr, "Sine table length: %d\n", SIN_TABLE_LEN); 331 | fprintf(stderr, "Samplerate:\t%3.2f MHz\n", (double)gSampleRate/1000000); 332 | fprintf(stderr, "Carrier:\t%3.2f MHz\n", gCarrierFrequency/1000000.0); 333 | if(gDidSpecifyTime) { 334 | fprintf(stderr, "Time of TX:\t%f seconds\n", gDurationOfEachTx); 335 | } 336 | if(strlen(frequencyFileName) > 3) { 337 | fprintf(stderr, "Frequency file: %s\n", frequencyFileName); 338 | frequencyFile = fopen(frequencyFileName, "r"); 339 | if(frequencyFile == NULL) { 340 | fprintf(stderr, "Error opening file: %s\n", frequencyFileName); 341 | } 342 | } 343 | 344 | //if(frequencyFile == 0) { 345 | dds_start(gCarrierFrequency); 346 | //} 347 | 348 | gStartTimeMs = current_miliseconds(); 349 | long long finishMs = gStartTimeMs + (gDurationOfEachTx * 1000.0); 350 | if(gDidSpecifyTime) { 351 | fprintf(stderr, "start ms = %lld until: %lld\n", gStartTimeMs, finishMs); 352 | } 353 | 354 | char * line = NULL; 355 | size_t len = 0; 356 | ssize_t read; 357 | 358 | while (!gUserCancelled) { 359 | if(frequencyFile) { 360 | while((read = getline(&line, &len, frequencyFile)) != -1 && !gUserCancelled) { 361 | gCarrierFrequency = atof(line); 362 | fprintf(stderr, "Read frequency = %f from file.\n", gCarrierFrequency); 363 | gTransmitTimeExpired = 0; 364 | dds_change_frequency(gCarrierFrequency); 365 | // keep going until cancelled or time expired 366 | if(gDidSpecifyTime) { 367 | long long nowMs = current_miliseconds(); 368 | // fprintf(stderr, "now ms = %lld end = %lld\n", nowMs, finishMs); 369 | while(nowMs < finishMs && !gUserCancelled) { 370 | nowMs = current_miliseconds(); 371 | } 372 | gTransmitTimeExpired = 1; 373 | fprintf(stderr, "time expired\n"); 374 | gStartTimeMs = current_miliseconds(); 375 | finishMs = gStartTimeMs + (gDurationOfEachTx * 1000.0); 376 | } 377 | } 378 | fprintf(stderr, "End of TX file\n"); 379 | gUserCancelled = 1; 380 | } 381 | } 382 | out: 383 | dds_stop(); 384 | 385 | return 0; 386 | } 387 | 388 | /* 389 | * Copyright (C) 2016-2018 by Steve Markgraf 390 | * 391 | * based on FM modulator code from VGASIG: 392 | * Copyright (C) 2009 by Bartek Kania 393 | * 394 | * SPDX-License-Identifier: GPL-2.0+ 395 | * 396 | * This program is free software: you can redistribute it and/or modify 397 | * it under the terms of the GNU General Public License as published by 398 | * the Free Software Foundation, either version 2 of the License, or 399 | * (at your option) any later version. 400 | * 401 | * This program is distributed in the hope that it will be useful, 402 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 403 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 404 | * GNU General Public License for more details. 405 | * 406 | * You should have received a copy of the GNU General Public License 407 | * along with this program. If not, see . 408 | */ 409 | -------------------------------------------------------------------------------- /vgaplay.geany: -------------------------------------------------------------------------------- 1 | [editor] 2 | line_wrapping=false 3 | line_break_column=72 4 | auto_continue_multiline=true 5 | 6 | [file_prefs] 7 | final_new_line=true 8 | ensure_convert_new_lines=false 9 | strip_trailing_spaces=false 10 | replace_tabs=false 11 | 12 | [indentation] 13 | indent_width=4 14 | indent_type=1 15 | indent_hard_tab_width=8 16 | detect_indent=false 17 | detect_indent_width=false 18 | indent_mode=2 19 | 20 | [project] 21 | name=vgaplay 22 | base_path=/home/marksp/Desktop/vgaplay 23 | description= 24 | 25 | [long line marker] 26 | long_line_behaviour=1 27 | long_line_column=72 28 | 29 | [files] 30 | current_page=1 31 | FILE_NAME_0=8528;C;0;EUTF-8;1;1;0;%2Fhome%2Fmarksp%2FDeveloper%2Ffl2k%2Fvgaplay.c;0;4 32 | FILE_NAME_1=18951;C;0;EUTF-8;1;1;0;%2Fhome%2Fmarksp%2FDeveloper%2Ffl2k%2Flibosmo-fl2k.c;0;4 33 | FILE_NAME_2=2817;Python;0;EUTF-8;1;1;0;%2Fhome%2Fmarksp%2FDeveloper%2Ffl2k%2Fwspr_encode.py;0;4 34 | 35 | [VTE] 36 | last_dir=/home/marksp 37 | 38 | [prjorg] 39 | source_patterns=*.c;*.C;*.cpp;*.cxx;*.c++;*.cc;*.m; 40 | header_patterns=*.h;*.H;*.hpp;*.hxx;*.h++;*.hh; 41 | ignored_dirs_patterns=.*;CVS; 42 | ignored_file_patterns=*.o;*.obj;*.a;*.lib;*.so;*.dll;*.lo;*.la;*.class;*.jar;*.pyc;*.mo;*.gmo; 43 | generate_tag_prefs=0 44 | external_dirs= 45 | -------------------------------------------------------------------------------- /vgaplay.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXFileReference section */ 10 | 72FEDF1E20AD65E9003379D3 /* wspr_encode.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = wspr_encode.py; sourceTree = ""; }; 11 | 72FEDF1F20AD65E9003379D3 /* libosmo-fl2k.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = "libosmo-fl2k.c"; sourceTree = ""; }; 12 | 72FEDF2020AD65E9003379D3 /* vgaplay.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = vgaplay.c; sourceTree = ""; }; 13 | 72FEDF2120AD65E9003379D3 /* osmo-fl2k.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "osmo-fl2k.h"; sourceTree = ""; }; 14 | 72FEDF2220AD65E9003379D3 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 15 | 72FEDF2320AD65E9003379D3 /* Makefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; 16 | 72FEDF2420AD65E9003379D3 /* frequencylist.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = frequencylist.txt; sourceTree = ""; }; 17 | /* End PBXFileReference section */ 18 | 19 | /* Begin PBXGroup section */ 20 | 72E242A320AD658500B314F6 = { 21 | isa = PBXGroup; 22 | children = ( 23 | 72FEDF2420AD65E9003379D3 /* frequencylist.txt */, 24 | 72FEDF1F20AD65E9003379D3 /* libosmo-fl2k.c */, 25 | 72FEDF2320AD65E9003379D3 /* Makefile */, 26 | 72FEDF2120AD65E9003379D3 /* osmo-fl2k.h */, 27 | 72FEDF2220AD65E9003379D3 /* README.md */, 28 | 72FEDF2020AD65E9003379D3 /* vgaplay.c */, 29 | 72FEDF1E20AD65E9003379D3 /* wspr_encode.py */, 30 | ); 31 | sourceTree = ""; 32 | }; 33 | /* End PBXGroup section */ 34 | 35 | /* Begin PBXLegacyTarget section */ 36 | 72E242A820AD658500B314F6 /* vgaplay */ = { 37 | isa = PBXLegacyTarget; 38 | buildArgumentsString = "$(ACTION)"; 39 | buildConfigurationList = 72E242AB20AD658500B314F6 /* Build configuration list for PBXLegacyTarget "vgaplay" */; 40 | buildPhases = ( 41 | ); 42 | buildToolPath = /usr/bin/make; 43 | dependencies = ( 44 | ); 45 | name = vgaplay; 46 | passBuildSettingsInEnvironment = 1; 47 | productName = vgaplay; 48 | }; 49 | /* End PBXLegacyTarget section */ 50 | 51 | /* Begin PBXProject section */ 52 | 72E242A420AD658500B314F6 /* Project object */ = { 53 | isa = PBXProject; 54 | attributes = { 55 | LastUpgradeCheck = 0930; 56 | ORGANIZATIONNAME = "Peter Marks"; 57 | TargetAttributes = { 58 | 72E242A820AD658500B314F6 = { 59 | CreatedOnToolsVersion = 9.3.1; 60 | }; 61 | }; 62 | }; 63 | buildConfigurationList = 72E242A720AD658500B314F6 /* Build configuration list for PBXProject "vgaplay" */; 64 | compatibilityVersion = "Xcode 9.3"; 65 | developmentRegion = en; 66 | hasScannedForEncodings = 0; 67 | knownRegions = ( 68 | en, 69 | ); 70 | mainGroup = 72E242A320AD658500B314F6; 71 | projectDirPath = ""; 72 | projectRoot = ""; 73 | targets = ( 74 | 72E242A820AD658500B314F6 /* vgaplay */, 75 | ); 76 | }; 77 | /* End PBXProject section */ 78 | 79 | /* Begin XCBuildConfiguration section */ 80 | 72E242A920AD658500B314F6 /* Debug */ = { 81 | isa = XCBuildConfiguration; 82 | buildSettings = { 83 | ALWAYS_SEARCH_USER_PATHS = NO; 84 | CLANG_ANALYZER_NONNULL = YES; 85 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 86 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 87 | CLANG_CXX_LIBRARY = "libc++"; 88 | CLANG_ENABLE_MODULES = YES; 89 | CLANG_ENABLE_OBJC_ARC = YES; 90 | CLANG_ENABLE_OBJC_WEAK = YES; 91 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 92 | CLANG_WARN_BOOL_CONVERSION = YES; 93 | CLANG_WARN_COMMA = YES; 94 | CLANG_WARN_CONSTANT_CONVERSION = YES; 95 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 96 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 97 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 98 | CLANG_WARN_EMPTY_BODY = YES; 99 | CLANG_WARN_ENUM_CONVERSION = YES; 100 | CLANG_WARN_INFINITE_RECURSION = YES; 101 | CLANG_WARN_INT_CONVERSION = YES; 102 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 103 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 104 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 105 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 106 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 107 | CLANG_WARN_STRICT_PROTOTYPES = YES; 108 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 109 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 110 | CLANG_WARN_UNREACHABLE_CODE = YES; 111 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 112 | COPY_PHASE_STRIP = NO; 113 | DEBUG_INFORMATION_FORMAT = dwarf; 114 | ENABLE_STRICT_OBJC_MSGSEND = YES; 115 | ENABLE_TESTABILITY = YES; 116 | GCC_C_LANGUAGE_STANDARD = gnu11; 117 | GCC_DYNAMIC_NO_PIC = NO; 118 | GCC_NO_COMMON_BLOCKS = YES; 119 | GCC_OPTIMIZATION_LEVEL = 0; 120 | GCC_PREPROCESSOR_DEFINITIONS = ( 121 | "DEBUG=1", 122 | "$(inherited)", 123 | ); 124 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 125 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 126 | GCC_WARN_UNDECLARED_SELECTOR = YES; 127 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 128 | GCC_WARN_UNUSED_FUNCTION = YES; 129 | GCC_WARN_UNUSED_VARIABLE = YES; 130 | MTL_ENABLE_DEBUG_INFO = YES; 131 | ONLY_ACTIVE_ARCH = YES; 132 | }; 133 | name = Debug; 134 | }; 135 | 72E242AA20AD658500B314F6 /* Release */ = { 136 | isa = XCBuildConfiguration; 137 | buildSettings = { 138 | ALWAYS_SEARCH_USER_PATHS = NO; 139 | CLANG_ANALYZER_NONNULL = YES; 140 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 141 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 142 | CLANG_CXX_LIBRARY = "libc++"; 143 | CLANG_ENABLE_MODULES = YES; 144 | CLANG_ENABLE_OBJC_ARC = YES; 145 | CLANG_ENABLE_OBJC_WEAK = YES; 146 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 147 | CLANG_WARN_BOOL_CONVERSION = YES; 148 | CLANG_WARN_COMMA = YES; 149 | CLANG_WARN_CONSTANT_CONVERSION = YES; 150 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 151 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 152 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 153 | CLANG_WARN_EMPTY_BODY = YES; 154 | CLANG_WARN_ENUM_CONVERSION = YES; 155 | CLANG_WARN_INFINITE_RECURSION = YES; 156 | CLANG_WARN_INT_CONVERSION = YES; 157 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 158 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 159 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 160 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 161 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 162 | CLANG_WARN_STRICT_PROTOTYPES = YES; 163 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 164 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 165 | CLANG_WARN_UNREACHABLE_CODE = YES; 166 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 167 | COPY_PHASE_STRIP = NO; 168 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 169 | ENABLE_NS_ASSERTIONS = NO; 170 | ENABLE_STRICT_OBJC_MSGSEND = YES; 171 | GCC_C_LANGUAGE_STANDARD = gnu11; 172 | GCC_NO_COMMON_BLOCKS = YES; 173 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 174 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 175 | GCC_WARN_UNDECLARED_SELECTOR = YES; 176 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 177 | GCC_WARN_UNUSED_FUNCTION = YES; 178 | GCC_WARN_UNUSED_VARIABLE = YES; 179 | MTL_ENABLE_DEBUG_INFO = NO; 180 | }; 181 | name = Release; 182 | }; 183 | 72E242AC20AD658500B314F6 /* Debug */ = { 184 | isa = XCBuildConfiguration; 185 | buildSettings = { 186 | CODE_SIGN_STYLE = Automatic; 187 | DEBUGGING_SYMBOLS = YES; 188 | DEBUG_INFORMATION_FORMAT = dwarf; 189 | GCC_GENERATE_DEBUGGING_SYMBOLS = YES; 190 | GCC_OPTIMIZATION_LEVEL = 0; 191 | OTHER_CFLAGS = ""; 192 | OTHER_LDFLAGS = ""; 193 | PRODUCT_NAME = "$(TARGET_NAME)"; 194 | }; 195 | name = Debug; 196 | }; 197 | 72E242AD20AD658500B314F6 /* Release */ = { 198 | isa = XCBuildConfiguration; 199 | buildSettings = { 200 | CODE_SIGN_STYLE = Automatic; 201 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 202 | OTHER_CFLAGS = ""; 203 | OTHER_LDFLAGS = ""; 204 | PRODUCT_NAME = "$(TARGET_NAME)"; 205 | }; 206 | name = Release; 207 | }; 208 | /* End XCBuildConfiguration section */ 209 | 210 | /* Begin XCConfigurationList section */ 211 | 72E242A720AD658500B314F6 /* Build configuration list for PBXProject "vgaplay" */ = { 212 | isa = XCConfigurationList; 213 | buildConfigurations = ( 214 | 72E242A920AD658500B314F6 /* Debug */, 215 | 72E242AA20AD658500B314F6 /* Release */, 216 | ); 217 | defaultConfigurationIsVisible = 0; 218 | defaultConfigurationName = Release; 219 | }; 220 | 72E242AB20AD658500B314F6 /* Build configuration list for PBXLegacyTarget "vgaplay" */ = { 221 | isa = XCConfigurationList; 222 | buildConfigurations = ( 223 | 72E242AC20AD658500B314F6 /* Debug */, 224 | 72E242AD20AD658500B314F6 /* Release */, 225 | ); 226 | defaultConfigurationIsVisible = 0; 227 | defaultConfigurationName = Release; 228 | }; 229 | /* End XCConfigurationList section */ 230 | }; 231 | rootObject = 72E242A420AD658500B314F6 /* Project object */; 232 | } 233 | -------------------------------------------------------------------------------- /vgaplay.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /wspr_encode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # WSPR protocol encoding script 3 | # 4 | # Input: callsign/locator/power 5 | # Output: raw symbol sequence (4-symbol alphabet for FSK) 6 | # 7 | # Robert Ostling SM0YSR 8 | # 2017-08-29 9 | # 10 | # Enhanced by Peter Marks VK2TPM to output frequencies 11 | 12 | ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ ' 13 | ALPHABET_IDX = {c:i for i,c in enumerate(ALPHABET)} 14 | 15 | LOC_ALPHABET = 'ABCDEFGHIJKLMNOPQR' 16 | LOC_ALPHABET_IDX = {c:i for i,c in enumerate(LOC_ALPHABET)} 17 | 18 | SYNC = [1,1,0,0,0,0,0,0,1,0,0,0,1,1,1,0,0,0,1,0,0,1,0,1,1,1, 19 | 1,0,0,0,0,0,0,0,1,0,0,1,0,1,0,0, 20 | 0,0,0,0,1,0,1,1,0,0,1,1,0,1,0,0,0,1,1,0,1,0,0,0,0,1, 21 | 1,0,1,0,1,0,1,0,1,0,0,1,0,0,1,0, 22 | 1,1,0,0,0,1,1,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0, 23 | 1,1,1,0,1,1,0,0,1,1,0,1,0,0,0,1, 24 | 1,1,0,0,0,0,0,1,0,1,0,0,1,1,0,0,0,0,0,0,0,1,1,0,1,0, 25 | 1,1,0,0,0,1,1,0,0,0] 26 | 27 | def encode_word(callsign, locator, dbm): 28 | if not callsign[2].isnumeric(): 29 | assert callsign[1].isnumeric() 30 | callsign = ' ' + callsign 31 | if len(callsign) < 6: 32 | callsign = callsign + (' ' * (6-len(callsign))) 33 | assert len(callsign) == 6 34 | 35 | callsign = [ALPHABET_IDX[c] for c in callsign] 36 | assert callsign[1] < 36 37 | assert callsign[2] < 10 38 | assert callsign[3] >= 10 39 | assert callsign[4] >= 10 40 | assert callsign[5] >= 10 41 | 42 | n_callsign = callsign[0] 43 | n_callsign = 36*n_callsign + callsign[1] 44 | n_callsign = 10*n_callsign + callsign[2] 45 | n_callsign = 27*n_callsign + (callsign[3]-10) 46 | n_callsign = 27*n_callsign + (callsign[4]-10) 47 | n_callsign = 27*n_callsign + (callsign[5]-10) 48 | assert n_callsign < 37*36*10*27*27*27 49 | 50 | assert len(locator) == 4 51 | assert locator[2:].isnumeric() 52 | 53 | locator = [LOC_ALPHABET_IDX[c] for c in locator[:2]] + \ 54 | [ALPHABET_IDX[c] for c in locator[2:]] 55 | 56 | n_locator = (179 - 10*locator[0] - locator[2])*180 + \ 57 | 10*locator[1] + locator[3] 58 | assert n_locator >= 179 59 | assert n_locator <= 32220 60 | 61 | assert dbm > -64 62 | assert dbm < 64 63 | n_dbm = 64 + dbm 64 | 65 | n = (n_callsign << (15+7)) | (n_locator << 7) | n_dbm 66 | 67 | # MSB -> LSB order 68 | return n 69 | 70 | 71 | def convolute(n): 72 | def parity(x): 73 | assert x >= 0 74 | p = 0 75 | while x: 76 | p ^= (x & 1) 77 | x = x >> 1 78 | return p 79 | 80 | # Add zero bits at the end for padding 81 | # Total number of bits at this stage: 81 82 | n = n << 31 83 | 84 | r0 = 0 85 | r1 = 0 86 | for i in range(81): 87 | b = (n >> 80) & 1 88 | n = n << 1 89 | r0 = (r0 << 1) | b 90 | r1 = (r1 << 1) | b 91 | b0 = parity(r0 & 0xF2D05351) 92 | b1 = parity(r1 & 0xE4613C47) 93 | yield b0 94 | yield b1 95 | 96 | 97 | def interleave(s): 98 | def byte_bit_reverse(x): 99 | assert x >= 0 and x <= 255 100 | return int("{0:08b}".format(x)[::-1], 2) 101 | 102 | d = [None]*162 103 | s_i = 0 104 | for i in range(256): 105 | d_i = byte_bit_reverse(i) 106 | if d_i < 162: 107 | d[d_i] = s[s_i] 108 | s_i += 1 109 | assert s_i == 162, s_i 110 | assert not (None in d), d 111 | return d 112 | 113 | 114 | def wspr_encode(callsign, locator, dbm): 115 | n = encode_word(callsign.upper(), locator.upper(), dbm) 116 | convoluted = list(convolute(n)) 117 | interleaved = interleave(convoluted) 118 | assert len(SYNC) == len(interleaved), (len(SYNC), len(interleaved)) 119 | symbols = [s + 2*x for s, x in zip(SYNC, interleaved)] 120 | return symbols 121 | 122 | #http://g4jnt.com/Coding/WSPR_Coding_Process.pdf 123 | def tx_frequency(base_freq, code): 124 | freq = base_freq + (code*1.4648) 125 | return freq 126 | 127 | if __name__ == '__main__': 128 | import sys 129 | if len(sys.argv) < 4: 130 | print('Usage: %s callsign locator dbm [base_freq]' % sys.argv[0]) 131 | sys.exit(1) 132 | callsign = sys.argv[1] 133 | locator = sys.argv[2] 134 | dbm = int(sys.argv[3]) 135 | print('{%s}' % ','.join( 136 | str(x) for x in wspr_encode(callsign, locator, dbm))) 137 | if len(sys.argv) == 5: 138 | print() 139 | base_freq = float(sys.argv[4]) 140 | symbol_length = 0.683 # seconds 141 | print("base_freq = %fHz" % base_freq) 142 | for code in wspr_encode(callsign, locator, dbm): 143 | print(tx_frequency(base_freq, code)) 144 | #print("./vgaplay -c %f -t %f" % (tx_frequency(base_freq, code), symbol_length)) 145 | 146 | 147 | --------------------------------------------------------------------------------