-
-
Save bjornvaktaren/d2461738ec44e3ad8b3bae4ce69445b4 to your computer and use it in GitHub Desktop.
// 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; | |
} |
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.
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: