Skip to content

Instantly share code, notes, and snippets.

@bjornvaktaren
Last active March 26, 2025 00:52
Show Gist options
  • Save bjornvaktaren/d2461738ec44e3ad8b3bae4ce69445b4 to your computer and use it in GitHub Desktop.
Save bjornvaktaren/d2461738ec44e3ad8b3bae4ce69445b4 to your computer and use it in GitHub Desktop.
Simple SPI example with libftdi and FTDI UM232H
// Quite minimal example showing how to configure MPSSE for SPI using libftdi
// compile like this: g++ minimal_spi.cpp -o minimal_spi -lftdipp -lftdi
#include <ftdi.hpp>
#include <usb.h>
#include <stdio.h>
#include <iostream>
#include <string.h>
// UM232H development module
#define VENDOR 0x0403
#define PRODUCT 0x6014
using namespace Ftdi;
namespace Pin {
// enumerate the AD bus for conveniance.
enum bus_t {
SK = 0x01, // ADBUS0, SPI data clock
DO = 0x02, // ADBUS1, SPI data out
DI = 0x04, // ADBUS2, SPI data in
CS = 0x08, // ADBUS3, SPI chip select
L0 = 0x10, // ADBUS4, general-ourpose i/o, GPIOL0
L1 = 0x20, // ADBUS5, general-ourpose i/o, GPIOL1
L2 = 0x40, // ADBUS6, general-ourpose i/o, GPIOL2
l3 = 0x80 // ADBUS7, general-ourpose i/o, GPIOL3
};
}
// Set these pins high
const unsigned char pinInitialState = Pin::CS|Pin::L0|Pin::L1;
// Use these pins as outputs
const unsigned char pinDirection = Pin::SK|Pin::DO|Pin::CS|Pin::L0|Pin::L1;
int main(void)
{
// initialize
struct ftdi_context ftdi;
int ftdi_status = 0;
ftdi_status = ftdi_init(&ftdi);
if ( ftdi_status != 0 ) {
std::cout << "Failed to initialize device\n";
return 1;
}
ftdi_status = ftdi_usb_open(&ftdi, VENDOR, PRODUCT);
if ( ftdi_status != 0 ) {
std::cout << "Can't open device. Got error\n"
<< ftdi_get_error_string(&ftdi) << '\n';
return 1;
}
ftdi_usb_reset(&ftdi);
ftdi_set_interface(&ftdi, INTERFACE_ANY);
ftdi_set_bitmode(&ftdi, 0, 0); // reset
ftdi_set_bitmode(&ftdi, 0, BITMODE_MPSSE); // enable mpsse on all bits
ftdi_usb_purge_buffers(&ftdi);
usleep(50000); // sleep 50 ms for setup to complete
// Setup MPSSE; Operation code followed by 0 or more arguments.
unsigned int icmd = 0;
unsigned char buf[256] = {0};
buf[icmd++] = TCK_DIVISOR; // opcode: set clk divisor
buf[icmd++] = 0x05; // argument: low bit. 60 MHz / (5+1) = 1 MHz
buf[icmd++] = 0x00; // argument: high bit.
buf[icmd++] = DIS_ADAPTIVE; // opcode: disable adaptive clocking
buf[icmd++] = DIS_3_PHASE; // opcode: disable 3-phase clocking
buf[icmd++] = SET_BITS_LOW; // opcode: set low bits (ADBUS[0-7])
buf[icmd++] = pinInitialState; // argument: inital pin states
buf[icmd++] = pinDirection; // argument: pin direction
// Write the setup to the chip.
if ( ftdi_write_data(&ftdi, buf, icmd) != icmd ) {
std::cout << "Write failed\n";
}
// zero the buffer for good measure
memset(buf, 0, sizeof(buf));
icmd = 0;
// Now we will write and read 1 byte.
// The DO and DI pins should be physically connected on the breadboard.
// Next three commands sets the GPIOL0 pin low. Pulling CS low.
buf[icmd++] = SET_BITS_LOW;
buf[icmd++] = pinInitialState & ~Pin::CS;
buf[icmd++] = pinDirection;
// commands to write and read one byte in SPI0 (polarity = phase = 0) mode
buf[icmd++] = MPSSE_DO_WRITE | MPSSE_WRITE_NEG | MPSSE_DO_READ;
buf[icmd++] = 0x00; // length low byte, 0x0000 ==> 1 byte
buf[icmd++] = 0x00; // length high byte
buf[icmd++] = 0x12; // byte to send
// Next three commands sets the GPIOL0 pin high. Pulling CS high.
buf[icmd++] = SET_BITS_LOW;
buf[icmd++] = pinInitialState | Pin::CS;
buf[icmd++] = pinDirection;
std::cout << "Writing: ";
for ( int i = 0; i < icmd; ++i ) {
std::cout << std::hex << (unsigned int)buf[i] << ' ';
}
std::cout << '\n';
// need to purge tx when reading for some etherial reason
ftdi_usb_purge_tx_buffer(&ftdi);
if ( ftdi_write_data(&ftdi, buf, icmd) != icmd ) {
std::cout << "Write failed\n";
}
// zero the buffer for good measure
memset(buf, 0, sizeof(buf));
icmd = 0;
// now get the data we read just read from the chip
unsigned char readBuf[256] = {0};
if ( ftdi_read_data(&ftdi, readBuf, 1) != 1 ) std::cout << "Read failed\n";
else std::cout << "Answer: " << std::hex << (unsigned int)readBuf[0] << '\n';
// close ftdi
ftdi_usb_reset(&ftdi);
ftdi_usb_close(&ftdi);
return 0;
}
@ericfont
Copy link

Note there is a minor typo: The equation in line 61 "60 MHz / (5+1) = 1 MHz" should actually say "6 MHz / (5+1) = 1 MHz". I have verified that on my oscilloscope, and it is what agrees with https://www.ftdichip.com/Support/Knowledgebase/index.html?clkdivisor.htm, which gives the following examples:

Value      TCK/SK Max
0x0000      6 MHz
0x0001      3 MHz
0x0002      2 MHz
0x0003      1.5 MHz
0x0004      1.2 MHz
............      ..............
0xFFFF      91.553 Hz

@ericfont
Copy link

ericfont commented Mar 26, 2025

Also note that while the FT2232D is limited to a 6 MHz clock, FTx232H devices by default will use a 6 MHz clock but can go up to 30 Mhz...see MPSSE protocol in section "3.2.1 Divisors" in bottom of page 9 of that PDF:

As with the FT2232D, the FT2232H Divisor is a 16-bit hex value between 0x0000 and 0xFFFF. With the faster base clock, data rates range between 30MHz and ~460Hz. The FTx232H devices also have a divide by 5 option. It is enabled by default to maintain compatibility with FT2232D. With the divide by 5 option enabled, the FT2232D divisor formula is used.

So while that "6 MHz / (divisor+1) = 1 MHz" equation applies by default to FTx232H devices, it is possible to instead get "30 MHz / (divisor+1) = 1 MHz" by running the following command just prior to setting the clock divisor:

buf[icmd++] = DIS_DIV_5; // opcode: Disable division by 5

Sorry just thought I would share this useful tidbit for others... I found your SPI example very useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment