Communicating with an FPGA via a FT2232 Chip and the D2XX Library

Communicating with an FPGA via a FT2232 Chip and the D2XX Library #

One great use for FPGAs is as a sensor processing and data serialization platform. At the core of this operation is transfering data to a computer. In this lecture, we utilize an FT2232H multi-protocol to USB chip built into the Arty development board to transfer data to and from the FPGA.

Good Information Resources #

Information on the FT2232H #

The FT2232H is a multi-protocal to USB chip. It has two multi-protocol engines and a single USB protocol engine. The two multi-protocol engines can be used completely independently. On the Arty development board, the A engine is used to program the FPGA over JTAG and the B engine is wired to the FPGA for UART communication.

Source: Arty T100 reference manual https://digilent.com/reference/programmable-logic/arty-a7/reference-manual

There are two different drivers for use with the FT2xx series chips: Virtual Com Port (VCP) and D2XX. The VCP drivers are extremely backwards compatable; they make the FTDI device look like a legacy com port to the computer. When using the VCP drivers, the only protocol supported is UART. The VCP drivers are designed to be plug-in-play; they lack performance and configuration options. The D2XX drivers are designed for use with any protocol that isn’t UART and when higher performance and configuration options are required.

Block diagram of the FT2232H from the FT2232H data sheet.

The FT2232H must be configured (i.e., the EEPROM with the FT2232H) must be configured to select the driver type (without an EEPROM, VCP is default). Further, the EEPROM is required to use some protocol types. On the Arty dev board, the EEPROM is configured to use the VCP drivers by default. For the purposes of this tutorial, there are two more particulars about the EEPROM that are required. In “unused” parts of the EEPROM, Digilent and Xilinx store secret keys. Vivado checks for these keys and only if they are present, will Vivado program the FPGA over JTAG. So any changes to the EEPROM must not disrupt the keys. Second, the EEPROM data has a checksum that is stored in the last couple bytes. The FTxx devices verify the checksum and if it is incorrect, shut down. The checksum is only available under NDA, but thankfully libftd2xx handles it for us (incidentally, the pyftdi project reverse engineered the checksum).

Building and Linking Libftd2xx #

There are two options for writing driver interfaces with the libftd2xx: static or dynamic. For the sake of simplicity, this we will only use static linking. Go to the FTDI website and download the D2XX drivers for your architecture. Extract the tar to the directory of your project. For example, the directory structure would look like:

.
├── libftd2xx
│   └── release
│       ├── build
│       │   ├── libftd2xx
│       │   ├── libftd2xx.a
│       │   ├── libftd2xx.so.1.4.27
│       │   ├── libftd2xx.txt
│       │   └── libusb
│       ├── examples
│       │   └── ...
│       ├── libusb
│       │   └── ...
│       ├── ReadMe.txt
│       ├── release-notes.txt
│       ├── ftd2xx.h
│       └── WinTypes.h
├── Makefile
└── src
    └── main.cpp

The libftd2xx.a is the static library and ftd2xx.h and WinTypes.h are the headers for the library. Create the file src/main.cpp and paste the following contents:

#include <iostream>
#include <ftd2xx.h>

int main()
{
    FT_STATUS ftStatus; 
    FT_DEVICE_LIST_INFO_NODE *devInfo; 
    DWORD numDevs; 
    
    // Create the device information list 
    ftStatus = FT_CreateDeviceInfoList(&numDevs); 
    if (ftStatus == FT_OK) { 
        printf("Number of devices is %d\n",numDevs); 
    } 

    if (numDevs > 0) { 
        // Allocate storage for list based on numDevs 
        devInfo = (FT_DEVICE_LIST_INFO_NODE*)malloc(sizeof(FT_DEVICE_LIST_INFO_NODE)*numDevs);
        // Get the device information list 
        ftStatus = FT_GetDeviceInfoList(devInfo,&numDevs); 
        if (ftStatus == FT_OK) { 
            for (int i = 0; i < numDevs; i++) { 
                printf("Dev %d:\n",i); printf(" Flags=0x%x\n",devInfo[i].Flags); 
                printf(" Type=0x%x\n",devInfo[i].Type);
                printf(" ID=0x%x\n",devInfo[i].ID);
                printf(" LocId=0x%x\n",devInfo[i].LocId);
                printf(" SerialNumber=%s\n",devInfo[i].SerialNumber);
                printf(" Description=%s\n",devInfo[i].Description);
            } 
        }
    }
}

Then create ./Makefile with the following contents

all: build run

build:
	g++ ./src/main.cpp -o main ./libftd2xx/release/build/libftd2xx.a -I ./libftd2xx/release/
	
run:
	./main

Let’s understand the build command a little bit. First the source file and the output is defined with /src/main.cpp -o main. Next, the static library is added, and finally the headers are included. Building and running the project gives:

❯ make
g++ ./src/main.cpp -o main ./libftd2xx/release/build/libftd2xx.a -I ./libftd2xx/release/
./main
Number of devices is 2
Dev 0:
 Flags=0x2
 Type=0x6
 ID=0x4036010
 LocId=0x1051
 SerialNumber=210319B7CA83A
 Description=Digilent USB Device A
Dev 1:
 Flags=0x2
 Type=0x6
 ID=0x4036010
 LocId=0x1052
 SerialNumber=210319B7CA83B
 Description=Digilent USB Device B

And here we see there are two devices; one for each of the protocol engines of the FT2232H. The device A is used for JTAG and the device B is wired for UART. If you ran the program and got the following,

❯ make
g++ ./src/main.cpp -o main ./libftd2xx/release/build/libftd2xx.a -I ./libftd2xx/release/
./main
Number of devices is 2
Dev 0:
 Flags=0x1
 Type=0x3
 ID=0x0
 LocId=0x0
 SerialNumber=
 Description=
Dev 1:
 Flags=0x1
 Type=0x3
 ID=0x0
 LocId=0x0
 SerialNumber=
 Description=

Then the VCP drivers are active and you must disable them.

Disabling VCP Drivers on Linux #

Only one driver may be running at a time (VCP or D2xx). In order to use the D2XX drivers, you must disable the VCP drivers. Since the FTDI devices are ubiquitous, it’s probably best to only temporarily disable them. This is done easily enough with:

sudo rmmod ftdi_sio
sudo rmmod usbserial

This removes the ftdi_sio and usbserial Linux kernel modules. Note! The order is important because ftdi_sio uses usbserial.

Changing the Driver EEPROM Config to D2XX #

The next task we have at hand is to change the driver setting in the EEPROM configuration to D2XX. The tricky part about this is that we cannot disturb the secret keys in used by Vivado. The basic strategy we are going to use is three steps:

  1. Read out the user area of the EEPROM (this is where the keys are).
  2. Read out the device configuration
  3. Change the driver type
  4. Write the device configuration
  5. Write the user area

Opening the Device #

Let’s start by creating a new file src/swap_driver.cpp with the following contents:

#include <iostream>
#include <ftd2xx.h>

int main()
{
    // Device handle
    FT_HANDLE ftHandle;
    // Return status to check
    FT_STATUS ftStatus; 

    // Open the device with the description and make sure it worked
    ftStatus = FT_OpenEx((PVOID) "Digilent USB Device B",FT_OPEN_BY_DESCRIPTION,&ftHandle); 
    if (ftStatus == FT_OK) { 
        printf("Device Open...\n");
    } else { 
        printf("Failt to Open Device...\n");
        return 1;
    }

    // Close the Device
    FT_Close(ftHandle);
    printf("Closed device...\n");

}

This opens the B device, checks to make sure it opened correctly and then closes the device. Let’s add another entry to the Makefile for convenience.

# snip..

swap_driver:
	g++ ./src/swap_driver.cpp -o swap_driver ./libftd2xx/release/build/libftd2xx.a -I ./libftd2xx/release/
	./swap_driver

Then running the make entry gives

❯ ./swap_driver
Device Open

Yay! It worked.

Reading the EEPROM #

Size the User Area #

First we need to know the size of the user area. We could calculate it using the formula from ftdi or we can use libftd2xx to get it for us. Add the following to the src/swap_driver.cpp file before closing the device:

// snip..
	
	DWORD EEUA_Size; 
    ftStatus = FT_EE_UASize(ftHandle, &EEUA_Size); 
    if (ftStatus != FT_OK) { 
        printf("Failed to get the user area size\n");
        return 1;
    }
    printf("User Area Size = %d bytes\n", EEUA_Size);
    
// snip...

Running the modified script, results in

❯ make swap_driver
g++ ./src/swap_driver.cpp -o swap_driver ./libftd2xx/release/build/libftd2xx.a -I ./libftd2xx/release/
./swap_driver
Device Open...
User Area Size = 140 bytes
Closed device...

so we now know there are 140 bytes in the user area.

Read the User Area #

Now we can read the user area into a buffer of 140 bytes. First let’s modify the main() function to read the user area and call a function to print out the contents.

	// snip...
	
	const int BUFSIZE = 140;
    unsigned char buf [BUFSIZE]; 
    DWORD BytesRead; 
    ftStatus = FT_EE_UARead(ftHandle, buf, BUFSIZE, &BytesRead); 
    if (ftStatus != FT_OK) { 
        printf("Failed to read the user area\n");
        return 1;
    }
    print_ua(buf, BUFSIZE);
	
	// snip...

Add the printing function to the top of the file.

void print_ua(unsigned char buf[], int BUFSIZE) {
    for (int i = 0; i < BUFSIZE/4; i ++) {
        printf("| %2x %2x %2x %2x |", buf[i*4], buf[i*4+1], buf[i*4+2], buf[i*4+3]);
        for (int n = 0; n < 4; n ++) {
            if ((buf[i*4+n] <=31) | (buf[i*4+n] >=127) ){
                printf(" \\x");
            } else {
                printf("%3c", buf[i*4+n]);
            }
        }
        printf(" |\n");
    }
}

Running the script gives the user area as:

|  1  0 c7 92 | \x \x \x \x |
| 6a 35 51  2 |  j  5  Q \x |
|  0  2 41 72 | \x \x  A  r |
| 74 79  0  0 |  t  y \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0 44 | \x \x \x  D |
| 69 67 69 6c |  i  g  i  l |
| 65 6e 74 20 |  e  n  t    |
| 41 72 74 79 |  A  r  t  y |
| 20 41 37 2d |     A  7  - |
| 31 30 30 54 |  1  0  0  T |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  1  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |
|  0  0  0  0 | \x \x \x \x |

Near the top one can see the keys for Vivado, but there isn’t anything more interesting there, so we will comment out the printing line.

Read the FTDI Config #

Next, let’s read out the EEPROM. First we have to initialize the data structures. Then we can read it. There is a nice struct provided in the header which makes accessing the driver bit for the B side convenient. From the header file, 0 = VCP and 1 = d2xx. Insert the following code before the close handle statement.

// snip...
	
	// Create the arrays for the standard data 
    char Manufacturer[64];
    char ManufacturerId[64];
    char Description[64];
    char SerialNumber[64];
	
    // Create and initialize the structs 
    FT_EEPROM_HEADER ft_eeprom_header;
    ft_eeprom_header.deviceType = FT_DEVICE_2232H;
    // FTxxxx device type to be accessed 
    FT_EEPROM_2232H ft_eeprom_2232h;
    ft_eeprom_2232h.common = ft_eeprom_header;
    ft_eeprom_2232h.common.deviceType = FT_DEVICE_2232H;
	
    // Read the data
    ftStatus = FT_EEPROM_Read(ftHandle,&ft_eeprom_2232h, sizeof(ft_eeprom_2232h), Manufacturer,ManufacturerId, Description, SerialNumber);
    if (ftStatus != FT_OK) { 
        printf("Failed to read the config \n");
        return 1;
    }
    printf("Device Driver = %x\n", ft_eeprom_2232h.BDriverType);
	
// snip...

Executing the script shows the current (default) driver is the VCP.

Write the User Area #

Just to make sure we do not disturb the user area, we will now add the code to write back the user area to the main() function before the handle is closed.

// snip...
	
    // Write back the user area
    ftStatus = FT_EE_UAWrite(ftHandle, buf, BUFSIZE); 
    if (ftStatus != FT_OK) { 
        printf("Failed to write the user area\n");
        return 1;
    }
	
// snip...

Change the Driver and Re-Program #

Now all we need to do is change setting of ft_eeprom_2232h.BDriverType to a 1 and then write the configuration back to the EEPROM. Take care to re-write the user area AFTER writing the configuration. The whole main function should look like the following.

int main()
{
    // Device handle
    FT_HANDLE ftHandle;
    // Return status to check
    FT_STATUS ftStatus; 

    // Open the device with the description and make sure it worked
    ftStatus = FT_OpenEx((PVOID) "Digilent USB Device B",FT_OPEN_BY_DESCRIPTION,&ftHandle); 
    if (ftStatus == FT_OK) { 
        printf("Device Open...\n");
    } else { 
        printf("Failt to Open Device...\n");
        return 1;
    }

    // Get the user area size
    DWORD EEUA_Size; 
    ftStatus = FT_EE_UASize(ftHandle, &EEUA_Size); 
    if (ftStatus != FT_OK) { 
        printf("Failed to get the user area size\n");
        return 1;
    }
    printf("User Area Size = %d bytes\n", EEUA_Size);
  
    // Read the user area
    const int BUFSIZE = 140;
    unsigned char buf [BUFSIZE]; 
    DWORD BytesRead; 
    ftStatus = FT_EE_UARead(ftHandle, buf, BUFSIZE, &BytesRead); 
    if (ftStatus != FT_OK) { 
        printf("Failed to read the user area\n");
        return 1;
    }
    // print_ua(buf, BUFSIZE);
   
    
    // Create the arrays for the standard data 
    char Manufacturer[64];
    char ManufacturerId[64];
    char Description[64];
    char SerialNumber[64];

    // Create and initialize the structs 
    FT_EEPROM_HEADER ft_eeprom_header;
    ft_eeprom_header.deviceType = FT_DEVICE_2232H;
    // FTxxxx device type to be accessed 
    FT_EEPROM_2232H ft_eeprom_2232h;
    ft_eeprom_2232h.common = ft_eeprom_header;
    ft_eeprom_2232h.common.deviceType = FT_DEVICE_2232H;

    // Read the data
    ftStatus = FT_EEPROM_Read(ftHandle,&ft_eeprom_2232h, sizeof(ft_eeprom_2232h), Manufacturer,ManufacturerId, Description, SerialNumber);
    if (ftStatus != FT_OK) { 
        printf("Failed to read the config \n");
        return 1;
    }
    printf("Device Driver = %x\n", ft_eeprom_2232h.BDriverType);

    // Set the driver to D2xx!
    ft_eeprom_2232h.BDriverType = 1;

    ftStatus = FT_EEPROM_Program(ftHandle, &ft_eeprom_2232h, sizeof(ft_eeprom_2232h), Manufacturer, ManufacturerId, Description, SerialNumber);
    if (ftStatus != FT_OK) { 
        printf("Failed to write the config \n");
        return 1;
    }

    // Write back the user area
    ftStatus = FT_EE_UAWrite(ftHandle, buf, BUFSIZE); 
    if (ftStatus != FT_OK) { 
        printf("Failed to write the user area\n");
        return 1;
    }

    // Close the Device
    FT_Close(ftHandle);
    printf("Closed device...\n");

}

Now, execute the script and swap the driver. Verify, the user area was undisturbed by checking Vivado will still program the FPGA.

Echo Example #

With the driver swapped, we can now make a little echo example. The goal is to take a character from the user, send it to the FPGA and the FPGA will echo back the character.

Quick Start Repo #

Clone the starting repository from here for the T100 Arty dev board (there is also a solution branch if you get stuck). The core of the design is a simple finite state machine.

`timescale 1ns / 1ps

module top #(
        parameter integer CLK_HZ = 100_000_000,
        parameter integer BAUD = 4_000_000
    ) (
        input   wire    clk,
        input   wire    uart_txd_in,
        output  wire    uart_rxd_out,
    );

    // UART TX and RX wire and regs
    wire    [7:0]                           rx_data;
    wire                                    rx_done;
    reg     [7:0]                           tx_data = 8'b0;
    reg                                     tx_start = 1'b0;
    wire                                    tx_busy;
    reg                                     reset = 1'b0;
    reg                                     bytes_sent = 1'b0;

    uart_rx #(
      .CLK_HZ( CLK_HZ ),  // in Hertz
      .BAUD( BAUD )            // max. BAUD is CLK_HZ / 2
    ) uart_rx_inst0 (
      .clk( clk ),
      .nrst( ~reset ),
      .rx_data( rx_data ),
      .rx_done( rx_done ),
      .rxd( uart_txd_in )
    );

    uart_tx #(
      .CLK_HZ( CLK_HZ ),  // in Hertz
      .BAUD( BAUD )            // max. BAUD is CLK_HZ / 2
    ) uart_tx_inst0 (
      .clk( clk ),
      .nrst( ~reset ),
      //.tx_do_sample(  ),
      .tx_data( tx_data ),
      .tx_start( tx_start ),
      .tx_busy( tx_busy ),
      .txd( uart_rxd_out )
    );

    // Codes
    localparam integer C0 = 8'h00; // reset 
    localparam integer C2 = 8'h02; // ready receive

    // State
    localparam integer S0 = 2'b00; // reset
    localparam integer S1 = 2'b01; // ready 
    localparam integer S2 = 2'b10; // reading 
    localparam integer S3 = 2'b11; // writing 

    // State
    reg [1:0] state = 2'b00;
    
    always @(posedge clk) 
    begin
        case(state)
            S0: // Reset
            begin
                reset <= 1;
                data <= 0;
                tx_data <= 0;
                tx_start <= 0;
                bytes_sent <= 0;
                state <= S1;
            end

            S1: // ready
            begin
                reset <= 0;
                if (rx_done)
                begin
                    if (rx_data == C0)
                        state <= S0; // reset
                    else if (rx_data == C2)
                        state <= S2; // ready to receive
                end
            end

            S2: // reading
            begin
                if (rx_done)
                begin
                    tx_data <= rx_data;
                    state <= S3;
                end
            end

            S3: // writing
            begin
                if (~tx_busy & ~bytes_sent & ~tx_start)
                begin
                    tx_start <= 1;
                    bytes_sent <= 1;
                end
                else if (~tx_busy & ~tx_start)
                begin
                    state <= S0;
                    bytes_sent <= 0;
                end
                else if (tx_busy & tx_start)
                begin
                    tx_start <= 0;
                end
            end

            // Default
            default: state <= S0;
        endcase
    end

endmodule

The constraints are also simple.

## Clock signal
set_property -dict { PACKAGE_PIN E3    IOSTANDARD LVCMOS33 } [get_ports { clk }]; #IO_L12P_T1_MRCC_35 Sch=gclk[100]
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports { clk }];


## USB-UART Interface
set_property -dict { PACKAGE_PIN D10   IOSTANDARD LVCMOS33 } [get_ports { uart_rxd_out }]; #IO_L19N_T3_VREF_16 Sch=uart_rxd_out
set_property -dict { PACKAGE_PIN A9    IOSTANDARD LVCMOS33 } [get_ports { uart_txd_in }]; #IO_L14N_T2_SRCC_16 Sch=uart_txd_in

FTDI Control Software #

Let’s go back to src/main.cpp and build our control software in there. First, let’s open the device from the description.

#include <iostream>
#include <ftd2xx.h>
using namespace std;


int main()
{
    FT_HANDLE ftHandle;
    FT_STATUS ftStatus; 

    ftStatus = FT_OpenEx((PVOID) "Digilent USB Device B",FT_OPEN_BY_DESCRIPTION,&ftHandle); 
    if (ftStatus == FT_OK) { 
        printf("Device Open\n");
    } else { 
        printf("Failt to Open Device...\n");
        return 1;
    }

Next, we will perform a reset upon the device to clear any previous settings and set the required settings for the UART communication to the FPGA.

// snip...

    // Reset the Device
    ftStatus = FT_ResetDevice(ftHandle);
    if (ftStatus != FT_OK) {
        printf("Failt to Reset Device...\n");
        return 1;
    }

    // Set the Baud Rate
    ftStatus = FT_SetBaudRate(ftHandle, 4000000);
    if (ftStatus != FT_OK) {
        printf("Failt to Set Buad Rate ...\n");
        return 1;
    }

    // Set 8 data bits, 1 stop bit and no parity 
    ftStatus = FT_SetDataCharacteristics(ftHandle, FT_BITS_8, FT_STOP_BITS_1, FT_PARITY_NONE);
    if (ftStatus != FT_OK) {
        printf("Failt to Configure UART...\n");
        return 1;
    }

// snip...

Now we will perform a reset on the finite state machine and then tell the FPGA to get ready to receive the character.

// snip...

    // Reset and get it ready to receive 
    char tx_data[2];
    DWORD BytesWritten;
    tx_data[0] = 0x00;
    tx_data[1] = 0x02;
    ftStatus = FT_Write(ftHandle, tx_data, sizeof(tx_data), &BytesWritten);
    if (ftStatus != FT_OK) {
        printf("Failt to write to device ...\n");
        return 1;
    }
    
// snip...

The FPGA is now waiting to receive a character so we will get a character from the user and then send it to the FPGA

// snip...

    // Get a character from the user and transmit it
    char tx_echo[1];
    printf("input character > ");
    cin >> tx_echo[0];
    ftStatus = FT_Write(ftHandle, tx_echo, sizeof(tx_echo), &BytesWritten);
    if (ftStatus != FT_OK) {
        printf("Failt to write to device ...\n");
        return 1;
    }
    
// snip...

Finally, we will read the single character from the device and then close the device handle

// snip...

    // Read back the data
    DWORD RxBytes = 1;
    DWORD BytesReceived;
    char RxBuffer[1];
    ftStatus = FT_Read(ftHandle,RxBuffer,RxBytes,&BytesReceived);
    if (ftStatus != FT_OK) {
        printf("Failt to read from device ...\n");
        return 1;
    }
    printf("echo character  > %c\n", RxBuffer[0]);

    // Close the Device
    FT_Close(ftHandle);
}

In summary, the whole program is shown below.

#include <iostream>
#include <ftd2xx.h>
using namespace std;


int main()
{
    FT_HANDLE ftHandle;
    FT_STATUS ftStatus; 

    ftStatus = FT_OpenEx((PVOID) "Digilent USB Device B",FT_OPEN_BY_DESCRIPTION,&ftHandle); 
    if (ftStatus == FT_OK) { 
        printf("Device Open\n");
    } else { 
        printf("Failt to Open Device...\n");
        return 1;
    }

    // Reset the Device
    ftStatus = FT_ResetDevice(ftHandle);
    if (ftStatus != FT_OK) {
        printf("Failt to Reset Device...\n");
        return 1;
    }

    // Set the Baud Rate
    ftStatus = FT_SetBaudRate(ftHandle, 4000000);
    if (ftStatus != FT_OK) {
        printf("Failt to Set Buad Rate ...\n");
        return 1;
    }

    // Set 8 data bits, 1 stop bit and no parity 
    ftStatus = FT_SetDataCharacteristics(ftHandle, FT_BITS_8, FT_STOP_BITS_1, FT_PARITY_NONE);
    if (ftStatus != FT_OK) {
        printf("Failt to Configure UART...\n");
        return 1;
    }

    // Reset and get it ready to receive 
    char tx_data[2];
    DWORD BytesWritten;
    tx_data[0] = 0x00;
    tx_data[1] = 0x02;
    ftStatus = FT_Write(ftHandle, tx_data, sizeof(tx_data), &BytesWritten);
    if (ftStatus != FT_OK) {
        printf("Failt to write to device ...\n");
        return 1;
    }
 
    // Get a character from the user and transmit it
    char tx_echo[1];
    printf("input character > ");
    cin >> tx_echo[0];
    ftStatus = FT_Write(ftHandle, tx_echo, sizeof(tx_echo), &BytesWritten);
    if (ftStatus != FT_OK) {
        printf("Failt to write to device ...\n");
        return 1;
    }

    // Read back the data
    DWORD RxBytes = 1;
    DWORD BytesReceived;
    char RxBuffer[1];
    ftStatus = FT_Read(ftHandle,RxBuffer,RxBytes,&BytesReceived);
    if (ftStatus != FT_OK) {
        printf("Failt to read from device ...\n");
        return 1;
    }
    printf("echo character  > %c\n", RxBuffer[0]);

    // Close the Device
    FT_Close(ftHandle);
}

Now running the application gives the following:

❯ make
g++ ./src/main.cpp -o main ./libftd2xx/release/build/libftd2xx.a -I ./libftd2xx/release/
./main
Device Open
input character > a
echo character  > a

Final Notes #

The example program is not efficient by any means. The biggest time eater is the USB buffer and work will be needed to efficiently use the provided buffers. More information can be found in the FT2232H data sheet, the D2XX Programmer’s Guide and especially in the Data Throughput and Latency Application Notes (see links at top). Don’t forget to look at both the D2XX lib and also in the EEPROM configuration for the settings.