Arduinos are cool, but...

Back with more digits!

A few days ago I posted about a digital I/O expander I was monkeying with for work, and posted an Arduino sketch I used to wring it out. That's all fine and good, and I did learn how to drive the chip around and accomplish my real goal, learning how to use the interrupt on change function. But, when it comes to work, we won't have an Arduino in our product. What we will have is essentially a PC; the computer part of it is a Com Express (Type 10) System-on-Module. While extremely flexible and reasonably priced (and available in industrial temperature range, this thing can actually operate at -40C), it doesn't have a SPI bus.

So why did I pick a SPI bus?

Well, the answer is long and to steal an expression from the t-shirt I wear to work on Fridays now, I don't have the time or the crayons to explain it to you. We have another, much more expensive I/O bus we also have to attach to, and the most sensible and affordable driver chips for that bus have a SPI interface. Don't believe me? Go price PC compatible adapters for the ARINC 429 bus. Pick your jaw up off the floor, and come back here, I'll wait.

So back to digital I/O. How to get a SPI bus on what is essentially a PC? Well, we do have USB. In particular, our module is an Advantech 7567, and it has 4 USB 2.0 and 1 USB 3.0 coming off the SOM. Our hardware engineer said "Did you look at FTDI?" Well, duh. If you want a "USB to whatever" of course you look at FTDI. Sure enough, we quickly ordered an FT4222 evaluation board, and that's what I'm banging on today.

The first challenge is, of course, to get it wired properly. I mostly had this program working Friday night, but no blinky lights. I spent yesterday in the real world, taking care of my home and a few other things, so I got back to this on a lazy and tired afternoon. The problem wasn't my coding skills, it was my ability to count pins on the FT4222, but now that I've got it wired correctly, here is the FT4222 and MCP23S17 equivalent of "Hello World:" I can count up in LEDs.

This starts off with the usual gobbledygook in any C/C++ program. The original example in the libft4222 spim.c was in C, I'm slowly transmogrifying it to C++ because I'm actually more accustomed to C++ than C these days. A fair bit of this will look familiar from the Arduino version, we are still talking to a 23S17 chip after all.


/* Read and write SPI Slave 23S17 I/O expander.
 * Linux instructions:
 *  1. Ensure libft4222.so is in the library search path (e.g. /usr/local/lib)
 *  2. gcc spim.c -lft4222 -Wl,-rpath,/usr/local/lib
 *  3. sudo ./a.out
 *
 * Note that this will not work as a non-rood user unless you fix the I/O
 * permissions in udev.
 */

#include <iostream>
#include <thread>
#include <chrono>

using namespace std;

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <ctype.h>
#include "ftd2xx.h"
#include "libft4222.h"

#ifndef _countof
    #define _countof(a) (sizeof((a))/sizeof(*(a)))
#endif

// SPI Master can assert SS0O in single mode
// SS0O and SS1O in dual mode, and
// SS0O, SS1O, SS2O and SS3O in quad mode.
#define SLAVE_SELECT(x) (1 << (x))


// MCP23S17 definitions:

const uint8_t ADDR = 0x20;      //MCP23S17 address, all 3 ADDR pins are grounded
const uint8_t OPCODE_READ  = (ADDR << 1 | 0x01); //MCP23S17 read command

// 23S17 register addresses

#define IODIRA   0x00
#define IODIRB   0x01
#define IPOLA    0x02
#define IPOLB    0x03
#define GPINTENA 0x04
#define GPINTENB 0x05
#define DEFVALA  0x06
#define DEFVALB  0x07
#define INTCONA  0x08
#define INTCONB  0x09
#define IOCONA   0x0a
#define IOCONB   0x0b
#define GPPUA    0x0c
#define GPPUB    0x0d
#define INTFA    0x0e
#define INTFB    0x0f
#define INTCAPA  0x10
#define INTCAPB  0x11
#define GPIOA    0x12
#define GPIOB    0x13
#define OLATA    0x14
#define OLATB    0x15

// MCP supports cascaded devices on the same SPI bus & CS
// We'll be device 0.

const uint8_t PORT_EXPANDER_ADDRESS = 0;
const uint8_t SLAVE_CONTROL_BYTE = 0b1000000 | (PORT_EXPANDER_ADDRESS << 1);


The code to read and write registers is different, because it uses libft4222 rather than the Arduino SPI library. One of the nice things about the FT4222 chipset is that the kernel driver for it is the same driver that all FTDI chips use, so it's already in Linux. All we have to install is the library, libft4222.so, and our application that calls it. Not writing a driver is crucial when we're trying to get this stuff working ASAP.

So, on to the read and write register routines. Note that I haven't debugged the read routine yet, I just barely got hello world working.


bool writeReg(FT_HANDLE ftHandle, uint8_t reg, uint8_t data)
{
  uint16 sendSize = 3;
 uint16 sentSize;
 uint8 command[3], response[3];

  command[0] = SLAVE_CONTROL_BYTE;
 command[1] = reg;
 command[2] = data;

 FT4222_STATUS ft4222Status = FT4222_SPIMaster_SingleWrite(
  ftHandle, command, sendSize, &sentSize, true); // de-assert slave select after command

  if (FT4222_OK != ft4222Status)
  {
    cerr << "writeReg SingleWrite failed, error " <<  ft4222Status << endl;
    return false;
  }
  if (sendSize != sentSize)
  {
    cerr << "writeReg SingleWrite sent " << sentSize << " bytes out of " << sendSize << endl;
    return false;
  }

  cout << "writeReg SingleWrite sent " << sentSize << " bytes." << endl;

 return true;
}


uint8_t readReg(FT_HANDLE ftHandle, uint8_t reg)
{
  uint16 sendSize = 2;
 uint16 sentSize;
 uint8 command[2];

  command[0] = SLAVE_CONTROL_BYTE | 1;
 command[1] = reg;

  // Send the read register command

  FT4222_STATUS ft4222Status = FT4222_SPIMaster_SingleWrite(
  ftHandle, command, sendSize, &sentSize, false); // not done yet...

  if (FT4222_OK != ft4222Status)
  {
    cerr << "readReg SingleWrite failed, error " <<  ft4222Status << endl;
    return false;
  }
  if (sendSize != sentSize)
  {
    cerr << "readReg SingleWrite sent " << sentSize << " bytes out of " << sendSize << endl;
    return false;
  }

  uint16 readSize = 1;
  uint16 numRead;
  uint8 response[1];

 ft4222Status = FT4222_SPIMaster_SingleRead(
  ftHandle, response,   // Response should hold byte read
    readSize, &numRead,
  true); // de-assert slave select after command

  if (FT4222_OK != ft4222Status)
  {
    cerr << "readReg SingleRead failed error " << ft4222Status << endl;
    return false;
  }
  if (numRead != readSize)
  {
    cerr << "readReg SingleRead received " << numRead << " bytes out of " << readSize << endl;
    return false;
  }

  cout << "readReg SingleRead read " << numRead << " bytes." << endl;

 return true;
}


The read routine is fairly carefully crafted. You can't use the fancy ReadWriteMulti API in libft4222 because the 23S17 is half duplex, it's either reading or writing the SPI bus, not both. So we send the read register command with a final argument of false to keep the slave select line asserted, then read the single byte of data we expect in response. I'll find out if that actually works later, when I try to get the input, and then the interrupt working.

This next routine is a mashup of the spim.c example, which read and write a SPI flash chip. We may need one of those too, or maybe a FRAM, so that code may yet come in handy, in the meantime this one just sets up PORT A for output and then starts counting.


static int exercise23S17(FT_HANDLE &ftHandle)
{
    std::cout << "Setting up 23S17:" << std::endl;

    // Repeat the first setup command a few times, to make sure we get the
    // initial SPI transaction closed...

    writeReg(ftHandle, IODIRA, 0);             // All Port A pins are outputs
    writeReg(ftHandle, IODIRA, 0);             // All Port A pins are outputs
    writeReg(ftHandle, IODIRA, 0);             // All Port A pins are outputs

    writeReg(ftHandle, IODIRB, 0xff);          // All Port B pins are inputs
    writeReg(ftHandle, GPPUB, 0xff);           // All Port B pins are pulled up
    writeReg(ftHandle, IPOLB, 0xff);           // All Port B pins are inverted (GND = 1)

    uint8 count = 0;

    while (true)
    {
        //uint8 capture = readByte(ftHandle, GPIOB);                // Read the Port B GPIO register
        //writeReg(ftHandle, GPIOA, capture);
        writeReg(ftHandle, GPIOA, count++);

        //std::cout << std::hex << static_cast(capture) << std::endl;
        std::cout << std::hex << static_cast(count) << std::endl;

        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }
    return 0;
}


Check out the cool C++11 sleep!

This is called by a barely changed function from spim.c:


static int exercise4222(DWORD locationId)
{
    FT_HANDLE            ftHandle = 0;
    FT4222_Version       ft4222Version;
    uint8                address;

    FT_STATUS ftStatus = FT_OpenEx((PVOID)(uintptr_t)locationId, FT_OPEN_BY_LOCATION,  &ftHandle);
    if (ftStatus != FT_OK)
    {
        printf("FT_OpenEx failed (error %d)\n", (int)ftStatus);
        return -1;
    }

    FT4222_STATUS ft4222Status = FT4222_GetVersion(ftHandle,  &ft4222Version);
    if (FT4222_OK != ft4222Status)
    {
        printf("FT4222_GetVersion failed (error %d)\n",
               (int)ft4222Status);
        return -2;
    }

    printf("Chip version: %08X, LibFT4222 version: %08X\n",
           (unsigned int)ft4222Version.chipVersion,
           (unsigned int)ft4222Version.dllVersion);

    // Configure the FT4222 as an SPI Master.
    ft4222Status = FT4222_SPIMaster_Init(
                        ftHandle,
                        SPI_IO_SINGLE,    // 1 channel
                        CLK_DIV_32,       // 60 MHz / 32 == 1.875 MHz
                        CLK_IDLE_LOW,     // clock idles at logic 0
                        CLK_LEADING,      // data captured on rising edge
                        SLAVE_SELECT(0)); // Use SS0O for slave-select
    if (FT4222_OK != ft4222Status)
    {
        printf("FT4222_SPIMaster_Init failed (error %d)\n", (int)ft4222Status);
        return -3;
    }

    ft4222Status = FT4222_SPI_SetDrivingStrength(ftHandle, DS_8MA, DS_8MA, DS_8MA);
    if (FT4222_OK != ft4222Status)
    {
        printf("FT4222_SPI_SetDrivingStrength failed (error %d)\n", (int)ft4222Status);
        return -4;
    }

    ft4222Status = FT4222_SPIMaster_SetLines(ftHandle, SPI_IO_SINGLE);
    if (FT4222_OK != ft4222Status)
    {
        printf("FT4222_SPIMaster_SetLines failed (error %d)\n", (int)ft4222Status);
        return -5;
    }

    // @todo: actually bang on the 23S17 chip a bit here...
    int mcp23S17Status = exercise23S17(ftHandle);
    if (mcp23S17Status != 0)
    {
        printf("exercise23S17 failed (error %d)\n", mcp23S17Status);
        return -10;
    }

    if (ftHandle != (FT_HANDLE)NULL)
    {
        (void)FT4222_UnInitialize(ftHandle);
        (void)FT_Close(ftHandle);
    }

    return 0;
}

There's a fair bit to experiment with here. The arguments for SPIMaster_Init were mostly guesses based on looking at the SPI timing section of the 23S17 data sheet. I added the redundant call to SetLines when I was trying to figure out why the chip wasn't working, it can almost certainly come out. This is called by, again pretty much culled from spim.c:


const char *deviceType[] =
{
 "BM",
 "AM",
 "100AX",
 "UNKNOWN",
 "2232C",
 "232R",
 "2232H",
 "4232H",
 "232H",
 "X_SERIES",
 "4222H_0",
 "4222H_1_2",
 "4222H_3",
 "4222_PROG"
};


static int testFT4222(void)
{
    FT_STATUS                 ftStatus;
    FT_DEVICE_LIST_INFO_NODE *devInfo = NULL;
    DWORD                     numDevs = 0;

    ftStatus = FT_CreateDeviceInfoList(&numDevs);
    if (ftStatus != FT_OK)
    {
        printf("FT_CreateDeviceInfoList failed (error code %d)\n", (int)ftStatus);
        return -10;
    }

    if (numDevs == 0)
    {
        printf("No devices connected.\n");
        return -20;
    }

    /* Allocate storage */
    devInfo = (FT_DEVICE_LIST_INFO_NODE *) calloc((size_t)numDevs, sizeof(FT_DEVICE_LIST_INFO_NODE));
    if (devInfo == NULL)
    {
        printf("Allocation failure.\n");
        return -30;
    }

    /* Populate the list of info nodes */
    ftStatus = FT_GetDeviceInfoList(devInfo, &numDevs);
    if (ftStatus != FT_OK)
    {
        printf("FT_GetDeviceInfoList failed (error code %d)\n", (int)ftStatus);
        free(devInfo);
        return -40;
    }

    for (int i = 0; i < (int)numDevs; i++)
    {
        printf("Device [%d] is type %s [%d]\n", i,
               deviceType[devInfo[i].Type], devInfo[i].Type);

        if (devInfo[i].Type == FT_DEVICE_4222H_0)
        {
           printf("\nDevice %d is FT4222H in mode 0 (Master or Slave?):\n",i);
           printf("  0x%08x  %s  %s\n",
                  (unsigned int)devInfo[i].ID,
                  devInfo[i].SerialNumber,
                  devInfo[i].Description);
           (void)exercise4222(devInfo[i].LocId);
           break;
        }

        if (devInfo[i].Type == FT_DEVICE_4222H_3)
        {
            printf("\nDevice %d is FT4222H in mode 3 (single Master or Slave):\n",i);
            printf("  0x%08x  %s  %s\n",
                   (unsigned int)devInfo[i].ID,
                   devInfo[i].SerialNumber,
                   devInfo[i].Description);
            (void)exercise4222(devInfo[i].LocId);
            break;
        }
    }

    free(devInfo);
    return 0;
}

int main(void)
{
    return testFT4222();
}


So that's it! It counts, and runs from my Ubuntu box without loading any custom drivers. Next step is to test it on a CentOS virtual machine, then on the Advantech prototype, on it's evaluation carrier board, in the lab.

Comments

Popular Posts