top of page
Adiuvo Engineering & Training logo
MicroZed Chronicles icon

MicroZed Chronicles: Generating FPGA Interface Stimulus with Python and the FT4232

  • 12 minutes ago
  • 7 min read

One of the things I have been meaning to write about for some time is how we provided flexible interfacing when we developed the Tile Carrier Card. Of course, we wanted the Embedded System Tile to be able to communicate with both the Raspberry Pi Compute Module and the Pmod interfaces available on the board.



Pmods provide support for a wide range of interface standards, from common protocols such as SPI, I²C, and UART through to higher-speed interfaces like TMDS for DVI and RMII for Ethernet. As a result, the Pmod interface offers a broad range of connectivity possibilities.


However, I also wanted the ability to simulate a variety of interfaces that are commonly deployed in embedded systems. Typically, I was thinking about interfaces such as I²C, SPI, UART, parallel buses, and general GPIO-style connections. Ideally, these interfaces would be flexible and controllable directly from a host machine so that we could easily generate stimulus for the FPGA design.


The solution turned out to be something I had not originally considered: using an FTDI FT4232H device as a programmable interface bridge.


On the Tile Carrier Card, 24 of the FPGA I/O pins are connected to the four ports of the FT4232H device. Each of these ports can be configured independently as SPI, UART, I²C, JTAG, or even bit-banged GPIO. This provides a highly flexible mechanism for generating and controlling interface signals directly from a host system.


These four FT4232 ports can then be controlled from a host machine using Python. Because the Tile Carrier Card and the installed tile expose several USB endpoints, a USB hub is included on the carrier card so that everything can be accessed through a single USB-C connection.


The simplest way to control the FTDI device from the host is with a Python application using the D2XX driver package provided by FTDI.


To demonstrate the concept, I created perhaps the simplest FPGA design I have ever implemented. The design simply routes the input pins from FTDI channels A and B into a Vivado Integrated Logic Analyzer (ILA).



With this simple FPGA design loaded onto the board, I wrote a Python script that outputs a counter on Port A of the FTDI device, counting from 0 to 255 and then back down to 0 repeatedly. I also used Port B pin 0 as a "valid" signal so that the count value could be captured by the ILA on the rising edge.


The Python script is shown below.


import ftd2xx as ftd
import time
import sys


def find_ft4232_devices():
    """Find all FT4232 devices and return their indices."""
    num_devices = ftd.listDevices()
    if not num_devices:
        return []
    
    ft4232_indices = []
    for i in range(len(num_devices)):
        try:
            dev = ftd.open(i)
            desc = dev.getDeviceInfo()['description'].decode('utf-8')
            dev.close()
            if 'Quad RS232-HS' in desc or 'FT4232' in desc or '4232' in desc:
                ft4232_indices.append(i)
                print(f"Found FT4232 port at index {i}: {desc}")
        except Exception as e:
            print(f"Could not query device {i}: {e}")
    
    return ft4232_indices


def configure_port_as_gpio(dev):
    """Configure an FT4232 port for GPIO bit-bang mode."""
    dev.resetDevice()
    dev.setLatencyTimer(1)  # Minimum latency for speed
    dev.setBitMode(0xFF, 0x01)  # All 8 pins as outputs, async bit-bang
    dev.setBaudRate(9600)
    return dev


def run_counter(port_a, port_b, delay=0.01):
    """
    Run counter on Port A with valid strobe on Port B pin 0.
    
    Args:
        port_a: FT4232 port A device handle (counter output)
        port_b: FT4232 port B device handle (valid strobe on pin 0)
        delay: Time between count updates in seconds
    """
    print(f"\nRunning counter with {delay*1000:.1f}ms delay between values")
    print("Port A: Counter 0-255-0")
    print("Port B pin 0: Valid strobe")
    print("Press Ctrl+C to stop\n")
    
    cycle = 0
    try:
        while True:
            cycle += 1
            print(f"--- Cycle {cycle} ---")
            
            # Count up: 0 to 255
            for count in range(256):
                # Set counter value on Port A
                port_a.write(bytes([count]))
                
                # Strobe valid HIGH on Port B pin 0
                port_b.write(bytes([0x01]))
                time.sleep(delay / 2)
                
                # Strobe valid LOW
                port_b.write(bytes([0x00]))
                time.sleep(delay / 2)
                
                if count % 64 == 0:
                    print(f"  Count UP: {count}")
            
            # Count down: 255 to 0
            for count in range(255, -1, -1):
                # Set counter value on Port A
                port_a.write(bytes([count]))
                
                # Strobe valid HIGH on Port B pin 0
                port_b.write(bytes([0x01]))
                time.sleep(delay / 2)
                
                # Strobe valid LOW
                port_b.write(bytes([0x00]))
                time.sleep(delay / 2)
                
                if count % 64 == 0:
                    print(f"  Count DOWN: {count}")
                    
    except KeyboardInterrupt:
        print("\n\nStopping...")


def run_counter_fast(port_a, port_b):
    """
    Run counter as fast as possible using buffered writes.
    """
    print(f"\nRunning counter at maximum speed")
    print("Port A: Counter 0-255-0")
    print("Port B pin 0: Valid strobe")
    print("Press Ctrl+C to stop\n")
    
    # Pre-build data buffers for Port A and Port B
    # For each count value: Port A gets count, Port B strobes 0x01 then 0x00
    
    cycle = 0
    start_time = time.perf_counter()
    
    try:
        while True:
            cycle += 1
            
            # Count up: 0 to 255
            for count in range(256):
                port_a.write(bytes([count]))
                port_b.write(bytes([0x01]))  # Valid HIGH
                port_b.write(bytes([0x00]))  # Valid LOW
            
            # Count down: 255 to 0
            for count in range(255, -1, -1):
                port_a.write(bytes([count]))
                port_b.write(bytes([0x01]))  # Valid HIGH
                port_b.write(bytes([0x00]))  # Valid LOW
            
            if cycle % 100 == 0:
                elapsed = time.perf_counter() - start_time
                rate = (cycle * 512) / elapsed  # 512 values per cycle (256 up + 256 down)
                print(f"Cycle {cycle}: {rate:.0f} values/sec")
                    
    except KeyboardInterrupt:
        elapsed = time.perf_counter() - start_time
        print(f"\n\nCompleted {cycle} cycles in {elapsed:.1f}s")


def main():
    print("FT4232 Counter with Valid Strobe")
    print("=" * 40)
    
    ft4232_indices = find_ft4232_devices()
    
    if len(ft4232_indices) < 2:
        print(f"\nError: Need at least 2 FT4232 ports (A and B), found {len(ft4232_indices)}")
        if not ft4232_indices:
            print("No FT4232 devices found!")
        sys.exit(1)
    
    port_a = None
    port_b = None
    
    try:
        # Open Port A (first FT4232 port) - Counter output
        print(f"\nOpening Port A (index {ft4232_indices[0]}) for counter...")
        port_a = ftd.open(ft4232_indices[0])
        configure_port_as_gpio(port_a)
        print("  Configured as 8-bit counter output")
        
        # Open Port B (second FT4232 port) - Valid strobe
        print(f"Opening Port B (index {ft4232_indices[1]}) for valid strobe...")
        port_b = ftd.open(ft4232_indices[1])
        configure_port_as_gpio(port_b)
        print("  Configured as valid strobe (pin 0)")
        
        # Initialize outputs
        port_a.write(bytes([0x00]))  # Counter = 0
        port_b.write(bytes([0x00]))  # Valid = LOW
        
        # Ask user for speed mode
        print("\nSelect mode:")
        print("  1. Slow (10ms delay, visible on scope)")
        print("  2. Fast (maximum speed)")
        choice = input("Enter choice [1/2]: ").strip()
        
        if choice == "2":
            run_counter_fast(port_a, port_b)
        else:
            run_counter(port_a, port_b, delay=0.01)
        
    finally:
        print("Closing devices...")
        for dev in [port_a, port_b]:
            if dev:
                try:
                    dev.write(bytes([0x00]))
                    dev.setBitMode(0x00, 0x00)
                    dev.close()
                except:
                    pass
        print("Done.")


if __name__ == "__main__":
    main()

This script contains five main functions which implement the required functionality.


find_ft4232_devices()

This function enumerates all FTDI devices connected via USB using ftd.listDevices(). Each device index is opened sequentially, and its description string is read from the EEPROM using getDeviceInfo(). The function returns the indices of devices matching "Quad RS232-HS", which is the default descriptor for the FT4232. Under the D2XX driver the FT4232 appears as four separate device indices, corresponding to channels A, B, C, and D.


configure_port_as_gpio(dev)

This function configures a port of the FT4232 for GPIO output. The device is first reset, and the USB latency timer is set to 1 ms (the minimum value) to reduce round-trip delay. Asynchronous bit-bang mode is then enabled using:

setBitMode(0xFF, 0x01)

Here 0xFF configures all eight pins as outputs, while 0x01 selects asynchronous bit-bang mode.


run_counter(port_a, port_b, delay=0.01)

This function implements a slower triangular counter that runs continuously. It counts from 0 to 255 and then from 255 back down to 0. Each value is written as a single byte to Port A. After writing the value, Port B pin 0 is toggled HIGH (0x01) and then LOW (0x00) to act as a strobe signal. A configurable delay (default 10 ms per count) is implemented using time.sleep().


run_counter_fast(port_a, port_b)

This function implements a maximum-speed version of the counter. All delays are removed, allowing values to be written as fast as Python and the USB latency allow. Performance is measured using time.perf_counter(), and the effective update rate is reported every 100 cycles.


main()

This is the entry point of the script. It discovers FT4232 devices, checks that at least two ports are available (Port A for the counter and Port B for the strobe), and opens the corresponding device indices using ftd.open(). Both ports are configured as GPIO outputs and initialised to 0x00. The user is then prompted to select either slow or fast mode before the appropriate counter function is executed.


Running this example on the hardware produces the waveform shown in the ILA. The triangular waveform is captured cleanly, with no missing codes.


While this is a simple demonstration, it highlights the advantages of including the FT4232 device on the Tile Carrier Card. It provides a convenient mechanism for generating stimulus directly from a host machine, enabling rapid testing of embedded interfaces such as SPI, I²C, UART, and GPIO.


Beyond simple stimulus generation, this approach can also be used to support more advanced testing scenarios, such as injecting faults or creating automated hardware verification setups.


FPGA Conference

FPGA Horizons US East - April 28th, 29th 2026 - THE FPGA Conference, find out more and get Tickets here.


FPGA Journal

Read about cutting edge FPGA developments, in the FPGA Horizons Journal or contribute an article.


Workshops and Webinars:

If you enjoyed the blog why not take a look at the free webinars, workshops and training courses we have created over the years. Highlights include:



Boards

Get an Adiuvo development board:

  • Adiuvo Embedded System Development board - Embedded System Development Board

  • Adiuvo Embedded System Tile - Low Risk way to add a FPGA to your design.

  • SpaceWire CODEC - SpaceWire CODEC, digital download, AXIS Interfaces

  • SpaceWire RMAP Initiator - SpaceWire RMAP Initiator,  digital download, AXIS & AXI4 Interfaces

  • SpaceWire RMAP Target - SpaceWire Target, digital download, AXI4 and AXIS Interfaces

  • Other Adiuvo Boards & Projects.


Embedded System Book   

Do you want to know more about designing embedded systems from scratch? Check out our book on creating embedded systems. This book will walk you through all the stages of requirements, architecture, component selection, schematics, layout, and FPGA / software design. We designed and manufactured the board at the heart of the book! The schematics and layout are available in Altium here.  Learn more about the board (see previous blogs on Bring up, DDR validation, USB, Sensors) and view the schematics here.


Sponsored by AMD

bottom of page