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:
- Read out the user area of the EEPROM (this is where the keys are).
- Read out the device configuration
- Change the driver type
- Write the device configuration
- 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.