Communicating with an FPGA via a FT2232 Chip and the VCP Driver

Communicating with an FPGA via a FT2232 Chip and the VCP Driver #

Communicating with an FPGA through an FTDI chip is a common requirement in many projects. FTDI provides two different ways for communicating with the FTDI chip: the Virtual Com Port (VCP) and the D2XX library. In the previous tutorial we explored how to use the D2XX library. Now we will use the VCP Drivers.

Prerequisites #

The following tutorial is design for the Digilent CMOD-A7 Development Board that features a Xilinx xc7a35tlcpg236 FPGA. If you are using a different board, adjust the constraints and clock frequency accordingly. By default, the Digilent boards come configured for use with the VCP drivers so there is no need to re-program the EEPROM unless it had previously been swapped to D2XX (see the previous tutorial).

Create a Vivado project as described previously for your specific development board. It is recommended to read the D2XX tutorial first for background.

Install VCP Drivers #

If you are running Windows, install the VCP drivers from the FTDI website: [FTDI VCP Drivers](https://ftdichip.com/drivers/vcp-drivers/. If you are on Linux newer than sometime in 2009, it should already have the drivers needed.

FPGA Design #

For this tutorial, we will make a basic echo state machine that receives one ASCII character and echo’s it back.

Verilog #

Create a top.v file in your Vivado project with the following contents.

`timescale 1ns / 1ps

module top_cmod #(
        parameter integer CLK_HZ = 12_000_000,
        parameter integer BAUD = 9600
    ) (
        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;
    // State
    reg [1:0] state = 2'b00;


    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

    always @(posedge clk)
    begin
        case(state)
            S0: // Reset
            begin
                reset <= 1;
                // rx_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

Note: if you are using a different development board, the CLK_HZ parameter will likely need to be updated to the frequency of the oscillator on the development board. For example, for an Arty board, the input clock oscillates at 100MHz

To keep this tutorial simple, the UART modules are from the open source repo: https://github.com/pConst/basic_verilog. Download the following files and add them to the project:

  • uart_tx.sv
  • uart_rx.sv
  • delay.sv
  • edge_detect.sv

Constraints #

Add the following constraints file to the project.

## 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

These constraints simply create a clock and add the appropriate connections to the FTDI chip.

Design Operation #

The finite state machine has a simple state diagram. The reset state resets all registers and then moves to the ready state. In the ready state, if a C0=0x00 code is received, then a reset is performed. If the code C2=0x02 is received, then the state waits to receive the data char. Once the data char is received, then state machine goes to the write state and then resets.

stateDiagram-v2
S0: S0 - Reset
S1: S1 - Ready 
S2: S2 - Read Char 
S3: S3 - Write Char 

S0 --> S1 
S1 --> S0 : C0
S1 --> S2 : C2
S2 --> S3 : rx_done
S3 --> S0 

That should be everything required on the FPGA side. Now, simply synthesize, run implementation and boot it onto the bitstream FPGA.

Matlab Control #

Arbitrarily we will use Matlab to talk to the com port. Just about every language has com port support so one doesn’t need to use Matlab.

First, we must find the correct Com port. Running the following command in Matlab will return a list of available serial ports:

>> serialportlist

ans =

    "/dev/ttyUSB1"

In this case on Linux, the com port is named /dev/ttyUSB1. If there are two options like USB0 and USB1, the correct option will be the highest number. The lower one is the A interface in the FT2232 used for JTAG programming the FPGA.

With the name of the serial port, consider the following script

clear

% Open the com port
sp = serialport("/dev/ttyUSB1", 9600);

% Perform Reset
write(sp, 0, "uint8")

% Ready to send
write(sp, 2, "uint8")

% Send char 'a'
data = 97;
write(sp, data, "uint8")

% Read the response
resp = read(sp, 1, "uint8");

fprintf("sent = %s\n", data)
fprintf("response = %s\n", resp)

% Disconnect from the serial port
clear sp

First, the com port is opened. Then we reset, the state machine, prepare it to read a character and send the ASCII character ‘a’ which is 97 in decimal. Then we read the character back and print the results. If it all works the script should return

sent = a
response = a

Conclusion #

If speed and control are not required by the application, then using VCP drivers is a great quick and dirty method for communicating with an FPGA through an FTDI chip. The upper bound on baud rate for the VCP drivers are a little loose, but from experience 4 Mega Baud is certainly the highest one should go and likely, 1 Mega Buad is pushing it. Further, there is less control over the USB timing. So if higher throughput or more control is required, then the D2XX lib is much better.