FTDI chips are commonly used for FPGA boards and microcontroller programmer boards. Apart from basic UART, advanced FTDI chips also support synchronous IO using the MPSSE mode.
Unfortunately, examples for MPSSE are outdated, Windows-only or otherwise limited. This tutorial therefore explains the MPSSE basics and presents a simple SPI communication example for an FT232H controller using libftdi.
Installing Libraries
We first need to install libftdi-devel. Use the following commands for Fedora 41:
# If you're running in a new container, you won't have these installed
sudo dnf install gcc g++
# libftdi itself
sudo dnf install libftdi-devel
Helper Code
Now we can start programming our application. First, let’s include required headers:
#include <ftdi.h>
// For usleep
#include <unistd.h>
To make our life a little easier, let’s also define an enforce function that checks error codes and throws exceptions on unexpected results:
#include <stdexcept>
#include <string.h>
using std::string;
void enforce(bool condition, string msg = "Enforcement failed")
{
if (!condition)
throw std::runtime_error(msg);
}
Better error handling is needed for real applications, but this code at least ensures that we will not miss any errors.
I will put all code in a SPIDriver class, so let’s define it here.
Let’s also declare the _ftdi member, a handle for the libftdi FTDI device:
class SPIDriver
{
private:
struct ftdi_context _ftdi;
public:
SPIDriver() {}
}
To finally compile the examples in this article, use this command:
g++ main.cpp -o main -lftdi1 `pkg-config libftdi1 --cflags`
Opening the FTDI Device
Before we can send commands to the FTDI device, we have to open a handle to it.
Let’s define an open function in the SPIDriver class, following the usual libftdi tutorials:
#define VENDOR 0x0403
#define PRODUCT 0x6014
void open()
{
auto status = ftdi_init(&_ftdi);
enforce(status == 0, "Failed to initialize libftdi");
status = ftdi_usb_open(&_ftdi, VENDOR, PRODUCT);
enforce(status == 0, "Failed to open device: " + string(ftdi_get_error_string(&_ftdi)));
Next, we reset the FTDI device and choose the MPSSE mode.
enforce(ftdi_usb_reset(&_ftdi) == 0, "ftdi_usb_reset failed");
enforce(ftdi_set_interface(&_ftdi, INTERFACE_ANY) == 0, "ftdi_set_interface failed");
enforce(ftdi_set_bitmode(&_ftdi, 0, 0) == 0, "ftdi_set_bitmode failed");
enforce(ftdi_set_bitmode(&_ftdi, 0, BITMODE_MPSSE) == 0, "ftdi_set_bitmode failed");
Finally, clear the internal buffers. Just in case the device was not properly reset.
enforce(ftdi_tcioflush(&_ftdi) == 0, "ftdi_tcioflush failed");
usleep(50000);
}
Closing the FTDI Device
Let’s also add a close function to clean up the FTDI handle on exit:
void close()
{
ftdi_usb_reset(&_ftdi);
enforce(ftdi_usb_close(&_ftdi) == 0, "ftdi_usb_close failed");
}
Configuring the MPSSE
There’s unfortunately little documentation for the MPSSE engine in general and there’s no documentation for the MPSSE mode in libftdi at all.
If you want to check the sources for the information here, refer to App Notes AN2232C-01 Command Processor and FTDI MPSSE Basics.
Before we can transfer data using the MPSSE, we have to do some initial configuration. But before we can do that, we need to define which pins we want to use:
// ADBUS0, 1, 2
#define PIN_SCK 0
#define PIN_MOSI 1
#define PIN_NSS 2
#define OUTPUT_PINMASK ((1 << PIN_SCK) | (1 << PIN_MOSI) | (1 << PIN_NSS))
As described in FTDI MPSSE Basics, the pins for the SCK, MOSI and MISO signals are fixed.
The NSS signal however is handled manually and can be assigned to most pins.
Here, I actually assign it to the MISO pin, a very specific use-case that will be explained in a follow-up post.
For more common use-cases, you should use any other pin here.
As for all MPSSE commands, the command data is then written using ftdi_write_data:
void setupMPSSE()
{
// Configure Clock: 1 MHz
uint8_t buf[] = {TCK_DIVISOR, 0x05, 0x00,
// Disable adaptive and 3 phase clocking
DIS_ADAPTIVE,
DIS_3_PHASE,
// Set ADBUS: All SPI pins as output, NSS initial high
SET_BITS_LOW, (1 << PIN_NSS), OUTPUT_PINMASK
};
// Write the setup to the chip.
enforce(ftdi_write_data(&_ftdi, buf, sizeof(buf)) == sizeof(buf), "FTDI setup failed");
}
The code has been formatted so that each command is on a single line.
TCK_DIVISORconfigures the clock rate for the data transfers. The first argument byte is basically the low byte of a clock divider value, the second byte the high byte. The exact formula for the frequency is given in AN2232C-01 Command Processor asTCK/SK period = 12MHz / (( 1 +[(0xValueH * 256) OR 0xValueL] ) * 2.The
DIS_ADAPTIVEandDIS_3_PHASEcommands take no arguments and disable adaptive and 3 phase clocking features. Refer to the linked sources for details.The
SET_BITS_LOWcommand is used to set the initial value of theADBUSpins. The first argument specifies as a bitmask all pins that should be set high, in this case onlyNSS. The second bitmask specifies which pins are configured as outputs. Here, theSCK,MOSIandNSSpins will be configured as outputs. Ultimately,NSSwill therefore be set high andSCKandMOSIwill be set low.
The initial value of the SCK pin will define the initial clock state.
For each data bit transferred, the MPSSE will then simply toggle the SCK line twice.
Single Bit SPI Transfers
The MPSSE can write data as blocks of bytes or as bits. If we want to write an amount of bits not divisible by 8, we need to use the bit write command:
void writeTrigger(bool on)
{
uint8_t tbit = on ? 0b1 : 0b0;
uint8_t buf[] = {
// Set NSS low
SET_BITS_LOW, 0x00, OUTPUT_PINMASK,
// Write one Bit
MPSSE_DO_WRITE | MPSSE_WRITE_NEG | MPSSE_LSB | MPSSE_BITMODE, 0x00, tbit,
// Add some artitifical delay before toggling NSS
SET_BITS_LOW, 0, OUTPUT_PINMASK,
// Set NSS high
SET_BITS_LOW, (1 << PIN_NSS), OUTPUT_PINMASK
};
enforce(ftdi_write_data(&_ftdi, buf, sizeof(buf)) == sizeof(buf), "FTDI write failed");
}
We again send MPSSE commands using ftdi_write_data.
Here’s how we implement an SPI transfer that writes a single bit of data:
SET_BITS_LOW: First set theNSSpin (and other output pins) low, as explained before.MPSSE_DO_WRITEperforms the actual data transfer and can be combined with flags to configure the transfer behavior.MPSSE_WRITE_NEGwrites data on the falling clock edge,MPSSE_LSBwrites data starting from the LSB to the MSB andMPSSE_BITMODEspecifies that the length argument specifies the number of bits, not bytes.In
MPSSE_BITMODE, theMPSSE_DO_WRITEcommand uses one argument as the number of bits (minus one) and the data is in the second byte argument. Specifying length0therefore transfers 1 bit of data, the LSB of thetbitvariable.The next
SET_BITS_LOWcommand simply sets all bits low. It is used to introduce an artificial delay before theNSSsignal goes back to high state. Please note that unlikeMPSSE_DO_WRITE,SET_BITS_LOWdoes not toggleSCK. Accordingly, there won’t be any additional data transfers caused by this command.Finally,
SET_BITS_LOWsets theNSSsignal back to the high level, the idle state.
Byte based SPI Transfers
Writing whole bytes works similarly and can be mixed with writing bits. Here’s an example:
void writeConfig(bool trigger, uint8_t adsr_ai, uint16_t osc_count, uint8_t filter_a)
{
uint8_t tbit = trigger ? 0b1 : 0b0;
uint8_t buf[] = {
SET_BITS_LOW, 0x00, OUTPUT_PINMASK,
MPSSE_DO_WRITE | MPSSE_WRITE_NEG | MPSSE_LSB | MPSSE_BITMODE, 0x00, tbit,
MPSSE_DO_WRITE | MPSSE_WRITE_NEG | MPSSE_LSB, 0x01, 0x00, adsr_ai, (uint8_t)(osc_count & 0xff),
MPSSE_DO_WRITE | MPSSE_WRITE_NEG | MPSSE_LSB | MPSSE_BITMODE, 0x03, (uint8_t)((osc_count >> 8) & 0x0f),
MPSSE_DO_WRITE | MPSSE_WRITE_NEG | MPSSE_LSB, 0x00, 0x00, filter_a
};
enforce(ftdi_write_data(&_ftdi, buf, sizeof(buf)) == sizeof(buf), "FTDI write failed");
usleep(3125);
uint8_t buf2[] = {
SET_BITS_LOW, (1 << PIN_NSS), OUTPUT_PINMASK
};
enforce(ftdi_write_data(&_ftdi, buf2, sizeof(buf2)) == sizeof(buf2), "FTDI write failed");
}
The first set of commands does the following:
SET_BITS_LOWsets NSS low to start the transaction.MPSSE_DO_WRITEwithMPSSE_BITMODEwrites a single bit.MPSSE_DO_WRITEwrites 2 bytes of data. When in byte mode, the length is encoded as two bytes, the first being the low byte. Again, the length value written to the command stream is reduced by one.MPSSE_DO_WRITEwithMPSSE_BITMODEwrites four bits. Combined with the previous command, this shows how you can conveniently write a 12 bit data field.- The last command
MPSSE_DO_WRITEwrites another byte of data.
Note that at the end of the command, we do not release NSS yet.
It’s perfectly fine to do this in another command, as shown here.
This will of course affect the timing, but sometimes this is intentional.
In this special case, I want the NSS signal to be low for at least 3.125 ms.
I therefore added a usleep call after writing the initial command stream.
The final SET_BITS_LOW to release NSS is then issued in another ftdi_write_data call, after the usleep.
Bringing it all Together
With all functions prepared, we can not write the main function:
int main(int argc, char* argv[])
{
auto dev = new SPIDriver();
dev->open();
dev->writeTrigger(true);
dev->writeConfig(true, 0, 0, 0);
dev->close();
return 0;
}
And that’s it, this code should be all you need to send SPI data using a FT232H. If you need to read data as well, refer to the linked documents. The process is however essentially the same.