Telemetry Viewer v0.8

Telemetry Viewer v0.8 Changelog (2021-07-24)

  • Multiple telemetry connections are now possible.
  • Basic triggering was added for time domain charts. It works like an oscilloscope: trigger on a rising edge, falling edge, or both edges. The usual trigger modes are supported: auto, normal and single.
  • The timeline now has a full set of playback controls. You can jump to the beginning, jump to the end, play, pause, and rewind. Playback and rewinding speed can be adjusted from 1x to 8x.
  • Cameras are now managed like regular connections, and exporting them creates standard MKV files. The MKV files can be played back in common movie players like VLC, or they can be imported back into TelemetryViewer. The benefit of playing them with TelemetryViewer is that the timestamps for each frame are displayed on screen.
  • Exporting is much faster now and the exporting process can be canceled.
  • Added support for the RDTech TC66/TC66C USB-C power meters. They are available here:
  • Added a "Statistics Chart" which can calculate and display the minimum/maximum/mean/median/standard deviation/90th percentile. The chart can also be used as a simple numeric display (showing just the current value of a dataset.)
  • Transmitting to UARTs is now supported. Data can be specified in text/hex/binary forms. Data can be sent once or repeatedly, and the data can be bookmarked for later use.
  • "Test Mode" has been renamed to "Demo Mode" to make what it does more obvious. New waveforms were added to help demonstrate trigger functionality.
  • Massive speed improvements were made in the data processing logic, and a "Stress Test Mode" was added to benchmark it. A modern laptop can process and visualize telemetry at speeds over 5Gbps.
  • For binary mode, the sync word is now optional and its value can be specified. Example Java code is also provided for binary mode UDP connections.
  • Notifications are now drawn with OpenGL, resulting is much smoother animations. They now slide into or out of existence. The different notification categories can be enabled or disabled, and their colors can be changed.
  • Replaced the color picker with an easier and simpler design.
  • Benchmarking now profiles every chart on screen instead of just one.
  • Added support for uint32 binary datasets. Note that samples are processed and stored into float32's, so the full range of uint32 samples can not be perfectly represented.
  • Lots of minor changes to improve the user experience. Some of the textboxes now shows units to make things more obvious, and some dropdown boxes were replaced with button groups to require one less click from the user.
  • Various small bug fixes. See the git commit log for more details.

Java 16 Notes

Java 16 was recently released and made some changes to how the internal APIs work. The OpenGL library that I use interacts with some of those internal APIs, and an updated version that is compatible with Java 16 has not been released yet. As a work around, if you use Java 16 you must run the .jar file from the command line with a special flag:

java --illegal-access=permit -jar TelemetryViewer_v0.8.jar

This work around also applies to older versions of Telemetry Viewer when using Java 16.

Telemetry Viewer v0.8 Demo Video


Executables (.jar) and source code (.zip) can be downloaded at or the project can be viewed at

Telemetry Viewer v0.7

Telemetry Viewer v0.7 Changelog (2020-07-17)

  • Webcams and network cameras (MJPEG over HTTP) are now supported.
  • Initial support for the Raspberry Pi 4 (currently does not support antialiasing or cameras.)
  • A new "timeline" feature makes it easy to jump or scrub through lots of data.
  • Time domain charts can now show timestamps (date and time) along the x-axis.
  • Bitfield (boolean and enum) "levels" can now be visualized as bars drawn on top of the charts (similar to a logic analyzer.)
  • Timestamps can be shown in any of the common formats: YYYY-MM-DD, MM-DD-YYYY and DD-MM-YYYY.
  • Most of the OpenGL and chart code has been rewritten, resulting in massive speed improvements. CPU and GPU usage is often cut in half. When using Nvidia GPUs the GPU usage has been cut down by almost 80%!
  • Progress bars are now displayed when importing and exporting data.
  • Added support for Java 9+ (still works with Java 8.)
  • Various small bug fixes. See the git commit log for more details.

Raspberry Pi Notes

Telemetry Viewer will only work on the Pi 4. Older Pi's don't support some of the OpenGL ES features that are required, and implementing those features on the CPU would be slow.

The Pi 4 GPU is supposedly capable of OpenGL ES 3.2, but the drivers only fully support ES 3.1 and partially support ES 3.2. Telemetry Viewer requires "geometry shaders" which are part of ES 3.2.

As of today, the version of Mesa included in "Ubuntu MATE 20.04 Raspberry Pi 32-bit" supports geometry shaders, but the version of Mesa in "Raspberry Pi OS" does not. If you use Ubuntu, all you need to do is install Java ($ sudo apt install default-jre) and you are ready to use Telemetry Viewer.

If you want to use Raspberry Pi OS, try running Telemetry Viewer. Maybe you'll get lucky and by the time you read this an updated Mesa will already be in Raspberry Pi OS.

If you get GLSL errors (like the screenshot above) you will need to download Mesa from their git repo, then compile it and install it. You will also need to set an environment variable every time you want to run Telmetry Viewer. I do not recommend this for beginners, but here is how I got it working:

$ sudo pip3 install meson mako $ sudo apt install libdrm-dev llvm bison flex libxext-dev libxdamage-dev libxcb-glx0-dev libx11-xcb-dev libxcb-dri2-0-dev libxcb-dri3-dev libxcb-present-dev libxshmfence-dev libxxf86vm-dev libxrandr-dev ninja-build $ git clone $ cd mesa $ nano meson_options.txt set platforms to ['drm', 'x11', 'surfaceless'] and set gallium-drivers to ['kmsro', 'v3d', 'vc4', 'swrast'] $ mkdir build $ cd build $ meson .. $ sudo ninja install

To run Telemetry Viewer you will need to set an environment variable to select the new Mesa you just installed:

$ LD_LIBRARY_PATH="/usr/local/lib/arm-linux-gnueabihf" java -jar /path/to/TelemetryViewer_v0.7.jar

Telemetry Viewer v0.7 Demo Video


Executables (.jar) and source code (.zip) can be downloaded at or the project can be viewed at

Introduction to JNI with Eclipse, GCC and MSYS2

One of the most common uses for the Java Native Interface (JNI) is to allow Java code to interact with C libraries. In this guide I will show how to write a small Java library that uses JNI to read data from a USB device. I will use the "D2XX" API from FTDI to communicate with a USB device that implements their "Synchronous 245 FIFO" protocol. D2XX is a relatively simple C library, so it makes for a nice introduction to JNI.

Readers not familiar with D2XX might want to skim through my previous blog post:

Some Useful Links
Official JNI specification. This explains how the API works and why it was designed the way it is.
An excellent tutorial on JNI.

Install the IDE, C Compiler and Related Tools

While Visual Studio might be the most popular IDE for Windows, I will be using Eclipse and GCC instead. I'd like to make my library cross-platform in the future, and that will be easier to do with GCC. There are a variety of ways to install GCC and other common UNIX utilities (make, etc.) on Windows. I've settled on using MSYS2. For the IDE, I use Eclipse with the JDT (Java Developer Toolkit) CDT (C/C++ Developer Toolkit) and TM Terminal plug-ins.

  1. Install Java 8. The default options in the installer are fine: Next > Accept > Next > Next > Install > Finish
  2. Install Eclpse for Java Developers, CDT and TM Terminal. Select "Eclipse IDE for Java Developers" > Install > Accept Now > Launch In Eclipse: Check "Use this as the default and do not ask again" > Launch Help > Check for Updates Help > Eclipse Marketplace > Search for "CDT" > Install > Confirm > Accept > Finish > Restart Now Help > Eclipse Marketplace > Search for "TM Terminal" > Install > Confirm > Yes > Accept > Finish > Restart Now File > Exit
  3. Install MSYS2 and add it to the PATH environment variable. The default options in the installer are fine: Next > Next > Next > Finish In MSYS: $ pacman -Syu (and close the window when prompted to) Start > MSYS2 64bit > MSYS2 MSYS $ pacman -Syu $ pacman -S base-devel mingw-w64-x86_64-toolchain Start > "path" > Edit the System Environment Variables > Environment Variables > Path > Edit New > "C:\msys64\mingw64\bin" > New > "C:\msys64\usr\bin" > OK > OK > OK

Test Everything with a Hello World

  1. Open Eclipse and Create a New C Project.
    File > New > Project > C/C++ > C Project > Next > Project Name = "HelloWorld", Toolchain = "MinGW GCC" > Next > Finish > No File > New > Other > C/C++ > Source File > Next > Source File = "main.c" > Finish
  2. Write the hello world code.
    #include <stdio.h> int main(int argc, char** argv) { printf("hello world.\r\n"); return 0; }
  3. Verify that it compiles and runs without error.
    Project > Build All Run > Run

Create the Java Project

  1. If the C toolchain works, we can proceed to write the Java portion of the project.
    File > New > Java Project > Project Name = "EasyD2XX" > Finish File > New > Class > Package = "com.example.easyd2xx", Name = "EasyD2XX" > Finish
  2. Write the Java portion of the JNI code.
    package com.example.easyd2xx; import java.nio.ByteBuffer; import java.util.List; public class EasyD2XX { public String name; public String chipName; public String serialNumber; public int location; private long handle; private EasyD2XX(String name, String chipName, String serialNumber, int location) { = name; this.chipName = chipName; this.serialNumber = serialNumber; this.location = location; } /** * Automatically load the EasyD2XX.dll file before any methods of this class are called. */ static { try { System.loadLibrary("EasyD2XX"); } catch(UnsatisfiedLinkError e) { // do nothing, to allow graceful degradation if the FTDI driver or the EasyD2XX.dll file could not be found. // native function calls will now throw an UnsatasfiedLinkError when they are called. } } /** * Gets a list of devices. * * @return A list of the attached FTDI D2XX devices. */ public static native List<EasyD2XX> getDevices(); /** * Opens and configures the device for Synchronous 245 FIFO mode. * * @param readTimeoutMilliseconds Maximum amount of time to wait when reading. * @param writeTimeoutMilliseconds Maximum amount of time to wait when writing. * @throws Exception If the device could not be opened, or * if the device does not support the Synchronous 245 FIFO mode. */ public native void openAsFifo(int readTimeoutMilliseconds, int writeTimeoutMilliseconds) throws Exception; /** * Reads a series of bytes from the device into a byte[]. * * @param byteCount How many bytes to read. * @return The received bytes, as a byte[]. * @throws Exception If the read timed out, or * if the device is no longer available. */ public native byte[] read(int byteCount) throws Exception; /** * Reads a series of bytes from the device into a ByteBuffer. * * @param buffer Location to store the read bytes. * @param byteCount How many bytes to read. * @throws Exception If the read times out, or * if the device is no longer available. */ public native void read(ByteBuffer buffer, int byteCount) throws Exception; /** * Closes the device. * * @throws Exception If the device was not already open. */ public native void close() throws Exception; }
  3. Create a test class that can be used as a demo and as verification of proper functionality.
    File > New > Class > Name = Test > Finish
  4. Write the test code.
    package com.example.easyd2xx; import java.nio.ByteBuffer; import java.util.List; import java.util.Scanner; public class Test { /** * A simple test for the EasyD2XX class. * * @param args Not currently used. */ public static void main(String[] args) { try { // get a list of devices List<EasyD2XX> devices = EasyD2XX.getDevices(); if(devices.isEmpty()) { System.out.println("No devices were detected. Exiting."); return; } System.out.println("Select a device to read from:"); System.out.println(); for(int i = 0; i < devices.size(); i++) { System.out.println("Device " + i + ":"); EasyD2XX device = devices.get(i); System.out.println("Name: " +; System.out.println("Chip: " + device.chipName); System.out.println("SN: " + device.serialNumber); System.out.println("Location: " + device.location); System.out.println(); } // let the user pick a device Scanner stdin = new Scanner(; int deviceIndex = stdin.nextInt(); stdin.close(); // connect EasyD2XX device = devices.get(deviceIndex); device.openAsFifo(1000, 1000); // read 1GB into a byte[] long start = System.currentTimeMillis(); @SuppressWarnings("unused") byte[] oneGbArray =; long stop = System.currentTimeMillis(); System.out.println("Read 1GB into a byte[] in " + (stop - start) + "ms."); // also read 1GB into a ByteBuffer start = System.currentTimeMillis(); ByteBuffer buffer = ByteBuffer.allocateDirect(1073741824);, 1073741824); stop = System.currentTimeMillis(); System.out.println("Read 1GB into a ByteBuffer in " + (stop - start) + "ms."); // disconnect device.close(); System.out.println("Done. Exiting."); } catch(Exception | UnsatisfiedLinkError e) { System.out.println(e.getMessage() + " Exiting."); e.printStackTrace(); } } }

At this point the Java code is done and will compile just fine, but at run time you'll get a UnsatisfiedLinkError when any of the "native" methods are called. That is because the native (JNI) code needs to be written and compiled into a .dll file.

Add the C Portion of JNI Code to the Project

  1. Add C support to the Java project.
    File > New > Other > C/C++ > Convert to a C/C++ Project (Adds C/C++ Nature) > Next > check the project, choose "C Project", choose "Makefile Project" and "MinGW GCC" > Finish > No
  2. Add a Makefile.
    File > New > File > choose the project root directory, Filename = "makefile" > Finish
  3. Write the makefile with three targets. The first target, "make header" will be used to generate the JNI stubs. The next two targets are just helpers that print out signatures for your Java code ("make signatures") and also for any other Java class ("make sig"). Those two targets are optional, I just include them so I don't have to rememeber the commands to type in.
    # "make header" to generate the .h file header: mkdir -p jni javac -h jni src/com/example/easyd2xx/ rm src/com/example/easyd2xx/EasyD2xx.class # "make sig" to ask the user for a class name, then print the field and method signatures for that class sig: @bash -c 'read -p "Fully-qualified class name (example: java.util.List) ? " CLASSNAME && javap -s $$CLASSNAME'; # "make signatures" to print the field and method signatures for the EasyD2XX class signatures: javac src/com/farrellf/d2xx/ -d bin javap -s -p bin/com/farrellf/d2xx/EasyD2xx.class
  4. Show the "Build Targets" tab in Eclipse, then add targets for "header" and "signatures".
    Window > Show View > Other > Make > Build Targets > Open Select the project folder New Build Target > Target Name = "header" > OK New Build Target > Target Name = "signatures" > OK
  5. Run the "make header" target to generature the .h file.
    Double-click the "header" target to run it.

There will now be a "jni" subfolder in the project, containing a header file with stubs for each native method.

Unfortunately "make sig" can not be run from the Build Targets panel because Eclipse does not connect stdin when running it. Instead, you can run "make sig" from a terminal. I use the "TM Terminal" plug-in for Eclipse. With that plug-in, just select the project folder, then Ctrl-Alt-T to open a terminal in that folder.

If you open the com_example_easyd2xx_EasyD2XX.h file in Eclipse, you'll get lots of warnings and errors, because Eclipse doesn't know where to find the JNI headers (the "#include <jni.h>" line.)

  1. Add the JNI header folders to the Preprocessor Include Path.
    Project > Properties > C/C++ General > Preprocessor Include Paths, Macros etc. > Entires > GNU C > CDT User Setting Entries > Add Select "File System Path" Path = C:\Program Files\AdoptOpenJDK\jdk-\include Check "treat as built-in" Check "contains system headers" OK Repeat that again, but for path C:\Program Files\AdoptOpenJDK\jdk-\include\win32 Apply and Close
  2. Copy the D2XX .h and .lib files into the "jni" folder.
    Copy "ftd2xx.h" and "amd64\ftd2xx.lib" from the FTDI ZIP file into the "jni" folder Then in Eclipse: right-click the jni folder > Refresh
  3. Duplicate the com_example_easyd2xx_EasyD2XX.h file, rename the copy to .c, and write the C portion of the JNI code.
    #include "com_example_easyd2xx_EasyD2XX.h" #include "ftd2xx.h" #include <string.h> JNIEXPORT jobject JNICALL Java_com_example_easyd2xx_EasyD2XX_getDevices(JNIEnv* env, jclass thisClass) { // create an empty ArrayList object jclass listClass = (*env)->FindClass(env, "java/util/ArrayList"); if(listClass == NULL) return NULL; jmethodID listConstructor = (*env)->GetMethodID(env, listClass, "<init>", "()V"); if(listConstructor == NULL) return NULL; jobject list = (*env)->NewObject(env, listClass, listConstructor); if(list == NULL) return NULL; // get a handle for "list.add(object)" jmethodID listAddMethodHandle = (*env)->GetMethodID(env, listClass, "add", "(Ljava/lang/Object;)Z"); if(listAddMethodHandle == NULL) return NULL; // get a handle for "new EasyD2XX(name, chipName, serialNumber, location)" jmethodID easyD2xxConstructor = (*env)->GetMethodID(env, thisClass, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V"); if(easyD2xxConstructor == NULL) return NULL; // get the number of devices and build (but not read) the devices info list DWORD deviceCount = 0; if(FT_CreateDeviceInfoList(&deviceCount) != FT_OK) printf("Unable to get the FTDI devices count.\r\n"); // get the devices info list FT_DEVICE_LIST_INFO_NODE *devices = (FT_DEVICE_LIST_INFO_NODE*) malloc(sizeof(FT_DEVICE_LIST_INFO_NODE) * deviceCount); if(FT_GetDeviceInfoList(devices, &deviceCount) != FT_OK) printf("Unable to get the device info list.\r\n"); // for each FTDI device, create an EasyD2XX object and add it to the list for(DWORD i = 0; i < deviceCount; i++) { jstring name = (*env)->NewStringUTF(env, devices[i].Description); jstring chipName = devices[i].Type == FT_DEVICE_BM ? (*env)->NewStringUTF(env, "FT232BM") : devices[i].Type == FT_DEVICE_AM ? (*env)->NewStringUTF(env, "FT232AM") : devices[i].Type == FT_DEVICE_100AX ? (*env)->NewStringUTF(env, "100AX") : devices[i].Type == FT_DEVICE_UNKNOWN ? (*env)->NewStringUTF(env, "[Unknown Device]") : devices[i].Type == FT_DEVICE_2232C ? (*env)->NewStringUTF(env, "FT2232C") : devices[i].Type == FT_DEVICE_232R ? (*env)->NewStringUTF(env, "FT232R") : devices[i].Type == FT_DEVICE_2232H ? (*env)->NewStringUTF(env, "FT2232H") : devices[i].Type == FT_DEVICE_4232H ? (*env)->NewStringUTF(env, "FT4232H") : devices[i].Type == FT_DEVICE_232H ? (*env)->NewStringUTF(env, "FT232H") : devices[i].Type == FT_DEVICE_X_SERIES ? (*env)->NewStringUTF(env, "X Series") : devices[i].Type == FT_DEVICE_4222H_0 ? (*env)->NewStringUTF(env, "FT4222H, 0") : devices[i].Type == FT_DEVICE_4222H_1_2 ? (*env)->NewStringUTF(env, "FT4222H, 1-2") : devices[i].Type == FT_DEVICE_4222H_3 ? (*env)->NewStringUTF(env, "FT4222H, 3") : devices[i].Type == FT_DEVICE_4222_PROG ? (*env)->NewStringUTF(env, "FT4222, Prog") : devices[i].Type == FT_DEVICE_900 ? (*env)->NewStringUTF(env, "FT900 Series") : devices[i].Type == FT_DEVICE_930 ? (*env)->NewStringUTF(env, "FT930 Series") : devices[i].Type == FT_DEVICE_UMFTPD3A ? (*env)->NewStringUTF(env, "UMFTPD3A") : (*env)->NewStringUTF(env, "[Unknown Device]"); jstring serialNumber = (*env)->NewStringUTF(env, devices[i].SerialNumber); jint location = devices[i].LocId; jobject newEasyD2XXobject = (*env)->NewObject(env, thisClass, easyD2xxConstructor, name, chipName, serialNumber, location); if(newEasyD2XXobject == NULL) { free(devices); return NULL; } // list.add(newObject) (*env)->CallBooleanMethod(env, list, listAddMethodHandle, newEasyD2XXobject); if((*env)->ExceptionCheck(env)) { free(devices); return NULL; } } // done free(devices); return list; } JNIEXPORT void JNICALL Java_com_example_easyd2xx_EasyD2XX_openAsFifo(JNIEnv* env, jobject this, jint readTimeoutMilliseconds, jint writeTimeoutMilliseconds) { // get a handle for this class jclass thisClass = (*env)->GetObjectClass(env, this); if(thisClass == NULL) return; // get a handle for the Exception class, in case we need to throw an Exception jclass exception = (*env)->FindClass(env, "java/lang/Exception"); if(exception == NULL) return; // check if the device is a FT2232H or FT232H, because only those devices support FIFO mode jfieldID chipNameHandle = (*env)->GetFieldID(env, thisClass, "chipName", "Ljava/lang/String;"); if(chipNameHandle == NULL) return; jstring chipName = (*env)->GetObjectField(env, this, chipNameHandle); if(chipName == NULL) return; const char* chipNameCstring = (*env)->GetStringUTFChars(env, chipName, NULL); if(strcmp(chipNameCstring, "FT2232H") != 0 && strcmp(chipNameCstring, "FT232H") != 0) { (*env)->ReleaseStringUTFChars(env, chipName, chipNameCstring); (*env)->ThrowNew(env, exception, "Device does not support Synchronous 245 FIFO mode."); return; } (*env)->ReleaseStringUTFChars(env, chipName, chipNameCstring); FT_HANDLE ftdiHandle = 0; // open by location if possible (not possible on linux) jfieldID locationHandle = (*env)->GetFieldID(env, thisClass, "location", "I"); if(locationHandle == NULL) return; jint location = (*env)->GetIntField(env, this, locationHandle); if(FT_OpenEx((void*)(uintptr_t)location, FT_OPEN_BY_LOCATION, &ftdiHandle) != FT_OK) { // open by name if open by location failed jfieldID nameHandle = (*env)->GetFieldID(env, thisClass, "name", "Ljava/lang/String;"); if(nameHandle == NULL) return; jstring name = (*env)->GetObjectField(env, this, nameHandle); if(name == NULL) return; const char* nameCstring = (*env)->GetStringUTFChars(env, name, NULL); if(FT_OpenEx((void*)nameCstring, FT_OPEN_BY_DESCRIPTION, &ftdiHandle) != FT_OK) { (*env)->ReleaseStringUTFChars(env, name, nameCstring); (*env)->ThrowNew(env, exception, "Unable to open the device."); return; } (*env)->ReleaseStringUTFChars(env, name, nameCstring); } // configure the device if(FT_SetBitMode(ftdiHandle, 0xFF, 0x40) != FT_OK || // sync 245 FIFO mode FT_SetLatencyTimer(ftdiHandle, 2) != FT_OK || // minimum latency FT_SetUSBParameters(ftdiHandle, 65536, 65536) != FT_OK || // 64K buffers FT_SetFlowControl(ftdiHandle, FT_FLOW_RTS_CTS, 0, 0) != FT_OK || // flow control FT_Purge(ftdiHandle, FT_PURGE_RX | FT_PURGE_TX) != FT_OK || // flush FIFOs FT_SetTimeouts(ftdiHandle, readTimeoutMilliseconds, writeTimeoutMilliseconds) != FT_OK) { // timeouts // failure (*env)->ThrowNew(env, exception, "Unable to configure the device."); return; } else { // success jfieldID ftdiHandleHandle = (*env)->GetFieldID(env, thisClass, "handle", "J"); (*env)->SetLongField(env, this, ftdiHandleHandle, (uintptr_t) ftdiHandle); } } JNIEXPORT jbyteArray JNICALL Java_com_example_easyd2xx_EasyD2XX_read__I(JNIEnv* env, jobject this, jint byteCount) { // get a handle for this class jclass thisClass = (*env)->GetObjectClass(env, this); if(thisClass == NULL) return NULL; // get a handle for the Exception class, in case we need to throw an Exception jclass exception = (*env)->FindClass(env, "java/lang/Exception"); if(exception == NULL) return NULL; // get the value of "this.handle" jfieldID ftdiHandleHandle = (*env)->GetFieldID(env, thisClass, "handle", "J"); if(ftdiHandleHandle == NULL) return NULL; FT_HANDLE ftdiHandle = (FT_HANDLE) (uintptr_t) (*env)->GetLongField(env, this, ftdiHandleHandle); // create a new byte[] jbyteArray array = (*env)->NewByteArray(env, byteCount); if(array == NULL) return NULL; jbyte* buffer = (*env)->GetByteArrayElements(env, array, NULL); // read into the byte[] jint bytesRead = 0; while(byteCount > 0) { jint amount = (byteCount < 65536) ? byteCount : 65536; DWORD readAmount = 0; if(FT_Read(ftdiHandle, &buffer[bytesRead], amount, &readAmount) != FT_OK) { (*env)->ReleaseByteArrayElements(env, array, buffer, 0); (*env)->ThrowNew(env, exception, "Unable to read from the device."); return array; } bytesRead += readAmount; byteCount -= readAmount; } // done (*env)->ReleaseByteArrayElements(env, array, buffer, 0); return array; } JNIEXPORT void JNICALL Java_com_example_easyd2xx_EasyD2XX_read__Ljava_nio_ByteBuffer_2I(JNIEnv* env, jobject this, jobject buffer, jint byteCount) { // get a handle for this class jclass thisClass = (*env)->GetObjectClass(env, this); if(thisClass == NULL) return; // get a handle for the Exception class, in case we need to throw an Exception jclass exception = (*env)->FindClass(env, "java/lang/Exception"); if(exception == NULL) return; // get the value of "this.handle" jfieldID ftdiHandleHandle = (*env)->GetFieldID(env, thisClass, "handle", "J"); if(ftdiHandleHandle == NULL) return; FT_HANDLE ftdiHandle = (FT_HANDLE) (uintptr_t) (*env)->GetLongField(env, this, ftdiHandleHandle); // get the buffer and ensure it is big enough void* bufferPtr = (*env)->GetDirectBufferAddress(env, buffer); if(bufferPtr == NULL) return; jlong bufferSize = (*env)->GetDirectBufferCapacity(env, buffer); if(bufferSize < byteCount) { (*env)->ThrowNew(env, exception, "The buffer does not have enough space."); return; } // read into the ByteBuffer jint bytesRead = 0; while(byteCount > 0) { jint amount = (byteCount < 65536) ? byteCount : 65536; DWORD readAmount = 0; if(FT_Read(ftdiHandle, &((char*)bufferPtr)[bytesRead], amount, &readAmount) != FT_OK) { (*env)->ThrowNew(env, exception, "Unable to read from the device."); return; } bytesRead += readAmount; byteCount -= readAmount; } } JNIEXPORT void JNICALL Java_com_example_easyd2xx_EasyD2XX_close(JNIEnv* env, jobject this) { // get a handle for this class jclass thisClass = (*env)->GetObjectClass(env, this); if(thisClass == NULL) return; // get a handle for the Exception class, in case we need to throw an Exception jclass exception = (*env)->FindClass(env, "java/lang/Exception"); if(exception == NULL) return; // get the value of "this.handle" jfieldID ftdiHandleHandle = (*env)->GetFieldID(env, thisClass, "handle", "J"); if(ftdiHandleHandle == NULL) return; // close the device FT_HANDLE ftdiHandle = (FT_HANDLE) (uintptr_t) (*env)->GetLongField(env, this, ftdiHandleHandle); if(FT_Close(ftdiHandle) != FT_OK) { (*env)->ThrowNew(env, exception, "Unable to close the device."); return; } }
  4. Add a "dll" target to the makefile. It will be used to compile the JNI portion of the project into a .dll file. You can also add an "all" target, so that "make" with automatically call the "dll" target if no target is specified.
    dll: gcc jni/com_example_easyd2xx_EasyD2XX.c jni/ftd2xx.lib -I"C:/Program Files/AdoptOpenJDK/jdk-" -I"C:/Program Files/AdoptOpenJDK/jdk-" -shared -o EasyD2XX.dll file EasyD2XX.dll nm EasyD2XX.dll | grep Java ldd EasyD2XX.dll all: dll
  5. In the "Build Targets" tab, add another build target like before, but for "dll". Double-click the target to run it.

Keep in mind that you can compile this without the D2XX drivers installed, and there will be no error messages. But if you run it, Java will not be able to find the D2XX DLL at run time, and you will get an UnsatisfiedLinkError even if the EasyD2XX.dll file is fine. That is part of why I call "ldd" while compiling the DLL. If ldd prints out some lines with "???" then you will have problems at run time. For example, before installing the D2XX driver, I get this:

ldd EasyD2XX.dll ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7ffb6e4a0000) KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7ffb6cfd0000) KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7ffb6bf10000) ??? => ??? (0x6af80000) ??? => ??? (0x7ffb6cdb0000)

After installing the D2XX driver, I get this:

ldd EasyD2XX.dll ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7ffb6e4a0000) KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7ffb6cfd0000) KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7ffb6bf10000) msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x7ffb6cdb0000) FTD2XX.dll => /c/WINDOWS/SYSTEM32/FTD2XX.dll (0x180000000) SETUPAPI.dll => /c/WINDOWS/System32/SETUPAPI.dll (0x7ffb6c810000) cfgmgr32.dll => /c/WINDOWS/System32/cfgmgr32.dll (0x7ffb6c1c0000) ucrtbase.dll => /c/WINDOWS/System32/ucrtbase.dll (0x7ffb6c210000) RPCRT4.dll => /c/WINDOWS/System32/RPCRT4.dll (0x7ffb6d090000) bcrypt.dll => /c/WINDOWS/System32/bcrypt.dll (0x7ffb6bc30000) USER32.dll => /c/WINDOWS/System32/USER32.dll (0x7ffb6e2c0000) win32u.dll => /c/WINDOWS/System32/win32u.dll (0x7ffb6bd10000) GDI32.dll => /c/WINDOWS/System32/GDI32.dll (0x7ffb6e290000) gdi32full.dll => /c/WINDOWS/System32/gdi32full.dll (0x7ffb6c310000) msvcp_win.dll => /c/WINDOWS/System32/msvcp_win.dll (0x7ffb6c4b0000) ADVAPI32.dll => /c/WINDOWS/System32/ADVAPI32.dll (0x7ffb6d230000) sechost.dll => /c/WINDOWS/System32/sechost.dll (0x7ffb6c560000) IMM32.DLL => /c/WINDOWS/System32/IMM32.DLL (0x7ffb6d480000)

You can now run the Java code with Run > Run, and it should print out a list of attached FTDI devices. That's all there is to a basic JNI project. You could merge the code into an existing project, or export this project as a Jar file so it can be used as a library for other projects.

Unlike regular Java code, JNI code is obviously platform-dependant. In other words, you have to compile a .dll/.so/.dylib file for each operating system and each architecture that you wish to support. This guide only covered 64-bit Windows, but in my next post I will cover other platforms and show how to bundle everything into a single Jar file.

Finally, a small note for anyone familiar with the D2XX library: FTDI provides their library in both static and dynamic forms. I used the dynamic form because the static version for Windows will not link properly when using GCC. It seems like FTDI only intends for it to be used with the Visual C++ compiler.

YouTube Video

FTDI Synchronous 245 Tutorial: D2XX with Visual Studio 2019

Perhaps the easiest way to get data between an FPGA (or microcontroller) and a PC is with a UART. It works great up to a few megabits per second, but often becomes unreliable if you push it much past that. One easy way to transfer hundreds of megabits per second is to use an FTDI chip that supports their "Synchronous 245 FIFO" protocol. It is very easy for an FPGA to implement, and I have been able to reliably transfer data to my PC at just over 350Mbps.

I used an FT232H on a "UM232H" development board from FTDI. DigiKey sells them for around $22:

I replaced it's headers with some regular 0.1" pin headers because the factory-installed pins do not mate well with jumper wires. Here's what my setup looks like when wired to a Lattice MachXO2 FPGA development board:

Some Useful Links
Datasheet for the FTDI development board. It summarizes the features, lists the pin out, specifies how to configure the power pins, and contains the schematic.
To use the Synchronous 245 FIFO mode, you will need the "D2XX" driver. This might have been automatically installed when you plugged an FTDI into your PC, but you need to download this anyway, because the ZIP file contains the .lib and .h files needed when writing the software.'s_Guide(FT_000071).pdf
The D2XX Programmer's Guide explains how to use their API. It covers all of the data structures and functions that will be used.
Applications Notes explaining how the buffers and latency timer interact, which you need to understand in order to get the best performance from the system.

Explains how to setup a Visual Studio project so it can use the D2XX API. This is basically the "hello world" tutorial.

Explains how to use the Synchronous 245 FIFO mode, from both a hardware and software perspective. It contains timing diagrams, demo code, some advise, etc.

Prepare a Visual Studio 2019 Project

  1. Open Visual Studio 2019, then create a new project:
    File > New > Project > Empty C++ Project > Next > Project Name = "d2xx_test", Location = Desktop > Create
  2. Create the main.cpp file:
    Right-click the project > Add > New Item > C++ File, Name = "main.cpp" > Add
  3. Copy the header file and library file from the D2XX Driver zip file ("CDM v2.12.28 WHQL" or similar) into the project's source code folder:
    From the ZIP file: copy "/Static/amd64/ftd2xx.lib" and "/ftd2xx.h" into "Desktop/d2xx_test/d2xx_test/"
  4. Update the IDE so it knows about those files:
    Drag-n-drop the LIB file onto the Resource Files folder in the Visual Studio Solution Explorer. Drag-n-drop the H file onto the Header Files folder in the Visual Studio Solution Explorer. Right-click the project > Properties > Configuration = "All Configurations", Platform = "All Platforms" > Configuration Properties > C / C++ > Preprocessor > Preprocessor Definitions > click the "V" icon > Edit > type "FTD2XX_STATIC" > OK Linker > Input > Additional Dependencies > click the "V" icon > Edit > type "ftd2xx.lib" > OK > OK

Demo Program

By now the Visual Studio project is fully setup, so you can start using the API. Below is a simple demo program I wrote. It's reads 1GB of data from the Lattice MachXO2 FPGA.

  1. The program starts by displaying information about each FTDI device that is attached. Keep in mind that not all FTDI devices support the Synchronous 245 FIFO protocol.
  2. If a device with a certain serial number is found, the program will attempt to connect, configure for FIFO mode, and read 1GB of data.
  3. An error message will be displayed if there are any problems, and the program will exit.

Software Source Code (C++)

#include <stdio.h> #include <time.h> #include "ftd2xx.h" int main(int argc, char** argv) { FT_HANDLE handle; // check how many FTDI devices are attached to this PC unsigned long deviceCount = 0; if(FT_CreateDeviceInfoList(&deviceCount) != FT_OK) { printf("Unable to query devices. Exiting.\r\n"); return 1; } // get a list of information about each FTDI device FT_DEVICE_LIST_INFO_NODE* deviceInfo = (FT_DEVICE_LIST_INFO_NODE*) malloc(sizeof(FT_DEVICE_LIST_INFO_NODE) * deviceCount); if(FT_GetDeviceInfoList(deviceInfo, &deviceCount) != FT_OK) { printf("Unable to get the list of info. Exiting.\r\n"); return 1; } // print the list of information for(unsigned long i = 0; i < deviceCount; i++) { printf("Device = %d\r\n", i); printf("Flags = 0x%X\r\n", deviceInfo[i].Flags); printf("Type = 0x%X\r\n", deviceInfo[i].Type); printf("ID = 0x%X\r\n", deviceInfo[i].ID); printf("LocId = 0x%X\r\n", deviceInfo[i].LocId); printf("SN = %s\r\n", deviceInfo[i].SerialNumber); printf("Description = %s\r\n", deviceInfo[i].Description); printf("Handle = 0x%X\r\n", deviceInfo[i].ftHandle); printf("\r\n"); // connect to the device with SN "FT3SSN2O" if(strcmp(deviceInfo[i].SerialNumber, "FT3SSN2O") == 0) { if (FT_OpenEx(deviceInfo[i].SerialNumber, FT_OPEN_BY_SERIAL_NUMBER, &handle) == FT_OK && FT_SetBitMode(handle, 0xFF, 0x40) == FT_OK && FT_SetLatencyTimer(handle, 2) == FT_OK && FT_SetUSBParameters(handle, 65536, 65536) == FT_OK && FT_SetFlowControl(handle, FT_FLOW_RTS_CTS, 0, 0) == FT_OK && FT_Purge(handle, FT_PURGE_RX | FT_PURGE_TX) == FT_OK && FT_SetTimeouts(handle, 1000, 1000) == FT_OK) { // connected and configured successfully // read 1GB of data from the FTDI/FPGA char rxBuffer[65536] = { 0 }; unsigned long byteCount = 0; time_t startTime = clock(); for(int i = 0; i < 16384; i++) { if(FT_Read(handle, rxBuffer, 65536, &byteCount) != FT_OK || byteCount != 65536) { printf("Error while reading from the device. Exiting.\r\n"); return 1; } } time_t stopTime = clock(); double secondsElapsed = (double)(stopTime - startTime) / CLOCKS_PER_SEC; double mbps = 8589.934592 / secondsElapsed; printf("Read 1GB from the FTDI in %0.1f seconds.\r\n", secondsElapsed); printf("Average read speed: %0.1f Mbps.\r\n", mbps); return 0; } else { // unable to connect or configure printf("Unable to connect to or configure the device. Exiting.\r\n"); return 1; } } } return 0; }

Firmware Source Code (Verilog)

`default_nettype none module top ( // ftdi 245 fifo signals output reg [7:0] data, // [7:0] = pins 1,2,3,4,9,10,11,12 input wire rx_empty, // pin 13 input wire tx_full, // pin 14 output reg read_n, // pin 19 output reg write_n, // pin 20 output reg send_immediately_n, // pin 21 input wire clock_60mhz, // pin 27 output reg output_enable_n, // pin 28 // status leds output reg power_led_n, // pin 97 output reg tx_active_led_n // pin 107 ); reg [7:0] counter; always @(posedge clock_60mhz) begin power_led_n <= 0; output_enable_n <= 1; send_immediately_n <= 1; if(!tx_full) begin write_n <= 0; data <= counter; tx_active_led_n <= 0; counter <= counter + 1; end else begin write_n <= 1; read_n <= 1; tx_active_led_n <= 1; end end endmodule

Output of Test Run

Device = 0 Flags = 0x2 Type = 0x6 ID = 0x4036010 LocId = 0x262 SN = B Description = Lattice FTUSB Interface Cable B Handle = 0x0 Device = 1 Flags = 0x2 Type = 0x6 ID = 0x4036010 LocId = 0x261 SN = A Description = Lattice FTUSB Interface Cable A Handle = 0x0 Device = 2 Flags = 0x2 Type = 0x8 ID = 0x4036014 LocId = 0x281 SN = FT3SSN2O Description = UM232H Handle = 0x0 Read 1GB from the FTDI in 24.1 seconds. Average read speed: 356.5 Mbps.

YouTube Video

Telemetry Viewer v0.6

Telemetry Viewer v0.6 Changelog (2019-09-08)

  • The x-axis of Time Domain Charts can now display elapsed time.
  • Timestamps are recorded. Exported CSV files contain the UNIX timestamp for each sample.
  • CSV files can be imported (replayed.)
  • New and existing charts are configured with a non-modal side panel instead of a pop-up window.
  • Layout files and CSV files can be imported via drag-n-drop.
  • Charts can be maximized (full-screened.)
  • The Time Domain Chart now renders properly even when the sample number is very large.
  • Samples are automatically swapped to disk if there's not enough space in RAM.
  • Binary mode supports uint8 values.
  • Binary mode supports bitfields (for showing boolean and enum values.)
  • Various small bug fixes. See the git commit log for more details.

Telemetry Viewer v0.6 Demo Video


Executables (.jar) and source code (.zip) can be downloaded at or the project can be viewed at

  1  Next >