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 https://gitlab.freedesktop.org/mesa/mesa.git
$ 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:
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.
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.
Install Java 8. https://adoptopenjdk.net/
The default options in the installer are fine:
Next > Accept > Next > Next > Install > Finish
Install Eclpse for Java Developers, CDT and TM Terminal. https://www.eclipse.org/downloads/packages/installer
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
Install MSYS2 and add it to the PATH environment variable. https://www.msys2.org/
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
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
Write the hello world code. #include <stdio.h>
int main(int argc, char** argv) {
printf("hello world.\r\n");
return 0;
}
Verify that it compiles and runs without error. Project > Build All
Run > Run
Create the Java Project
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
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) {
this.name = 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;
}
Create a test class that can be used as a demo and as verification of proper functionality. File > New > Class > Name = Test > Finish
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: " + device.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(System.in);
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 = device.read(1073741824);
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);
device.read(buffer, 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
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
Add a Makefile. File > New > File > choose the project root directory, Filename = "makefile" > Finish
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/EasyD2xx.java
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/EasyD2xx.java -d bin
javap -s -p bin/com/farrellf/d2xx/EasyD2xx.class
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
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.)
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-8.0.252.09-hotspot\include
Check "treat as built-in"
Check "contains system headers"
OK
Repeat that again, but for path C:\Program Files\AdoptOpenJDK\jdk-8.0.252.09-hotspot\include\win32
Apply and Close
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
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;
}
}
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-8.0.242.08-hotspot/include" -I"C:/Program Files/AdoptOpenJDK/jdk-8.0.242.08-hotspot/include/win32" -shared -o EasyD2XX.dll
file EasyD2XX.dll
nm EasyD2XX.dll | grep Java
ldd EasyD2XX.dll
all: dll
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:
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
April 18, 2020
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 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:
https://www.ftdichip.com/Drivers/D2XX.htm
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.
Open Visual Studio 2019, then create a new project: File > New > Project > Empty C++ Project > Next > Project Name = "d2xx_test", Location = Desktop > Create
Create the main.cpp file: Right-click the project > Add > New Item > C++ File, Name = "main.cpp" > Add
Copy the header file and library file from the D2XX Driver zip file ("CDM v2.12.28 WHQL Certified.zip" 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/"
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.
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.
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.
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;
}