Hey, Wes is posting code again!

I'm back, with a (tiny) vengeance. I'm working on a brand new system at work, where we have a bunch of sense lines we have to pay attention to, mostly to decide if we're allowed to transmit or not. The CPU module we've chosen doesn't have nearly enough digital I/O lines to support all of these, so I went searching for a cheap, fast enough, and reliable enough way to hang more digital I/Os on the board we're making.

I looked at some Mini PCIe designs, which tend to be fairly expensive (for what you get) and also require a Mini PCIe connector, and we don't like connectors. This thing is going to live outside the pressure shell on these aircraft, so it needs to be able to work at 55,000 feet, where it is very cold.

Once I started looking carefully at the designs, I noticed that the cheaper ones were not using the PCIe lane on the Mini PCIe connector, they were use the USB 2.0 lane. Ah hah! A quick search for USB to SPI bus interfaces led me to the FTDI FT4222 chip. More about this in another post. From there, I went looking for SPI based Digital I/Os, and found the MCP23S17 from MicroChip. MicroChip is a good company to work with; they're very open about datasheets for their products, errata notices, and they provide eval units and even sample chips. So we got a couple of samples of the 23S17 in DIP form factor to make them easy to breadboard.

The FT4222 eval hasn't arrived yet, so I stuffed one of the 23S17s into a breadboard with my favorite microcontroller, my trusty and well-used Adafruit Feather M0. This on specifically is the Basic Proto version, but I have several of the ones with the RFM69 radios too.

After wiring the chip to the SPI bus, I started plunking around. Ultimately what we want to do is use one of these for all the "discrete" sense lines coming into the ModMan, even the ones that go to other units inside the box, because we're responsible for logging the state of all these inputs. So we'll condition all those signals, tee them where needed, and deliver them to my little I/O maven for sensing.

The 23S17 (and it's I2C brother the 23017) offer an interrupt-on-change mode; if any of the configured inputs changes state, the chip will read the port into an internal register and issue an interrupt. This is perfect for our needs, since we only need to log these when they change. So I wanted to explore getting interrupt-on-change to work. Here's my solution. I should create a diagram of the circuit in Fritzing and add that, but for now a photo will have to suffice.

The code is simple. I used the SPI hardware on the M0, but created all my own register definitions and such, so you can see exactly what I'm doing to the chip.


#include <SPI.h>

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);

// Feather M0 setup

#define CS 5  // Chip select on pin 5
#define INT 6 // Input change interrupt on pin 6

SPISettings spiSettings(SPI_CLOCK_DIV8, MSBFIRST, SPI_MODE0);


// Utility functions to talk to the 23S17.
// There are C++ libraries that wrap this, but meh.

void writeByte(uint8_t reg, uint8_t data) 
{
  digitalWrite(CS, LOW);

  SPI.transfer(SLAVE_CONTROL_BYTE);
  SPI.transfer(reg);
  SPI.transfer(data);

  digitalWrite(CS, HIGH);
}

uint8_t readByte(uint8_t reg) 
{
  digitalWrite(CS, LOW);

  SPI.transfer(SLAVE_CONTROL_BYTE | 1);
  SPI.transfer(reg);
  uint8_t data = SPI.transfer(0);

  digitalWrite(CS, HIGH);
  return data;
}

This is the interrupt handler for the change interrupt. It notes to the loop code that an interrupt has been received, grabs the saved contents of the input port from the "capture" port, and does some Arduino-ish housekeeping to keep itself going.


// Interrupt on change handler

volatile bool gotOne = true;
volatile uint8_t capture = 0;

void changeInt()
{
  // Read the change FIFO here...
  gotOne = true;
  
  SPI.beginTransaction(spiSettings);
    capture = readByte(INTCAPB);
  SPI.endTransaction();

  // Reattach the interrupt (is this necessary?)
  attachInterrupt(INT, changeInt, LOW);
}

Arduino programs start by calling the setup() function once.

Here we open the serial console port and wait for it to connect, then setup the 23S17 chip with a series of register writes.


void setup()
{
  Serial.begin(9600);
  while (!Serial);

  pinMode(CS, OUTPUT);                //configure controller's Slave Select pin to output
  digitalWrite(CS, HIGH);             //disable Slave Select

  pinMode(INT, INPUT_PULLUP);         // configure pin 6 for interrupt-on-change
  attachInterrupt(INT, changeInt, LOW);

  // DIO configuration:
  SPI.begin();

  // configure Port A for output
  
  SPI.beginTransaction(spiSettings);
    writeByte(IODIRA, 0);             // All Port A pins are outputs
  SPI.endTransaction();
  
  // configure Port B for input, pullup, inverted, interrupt on change

  SPI.beginTransaction(spiSettings);
    writeByte(IODIRB, 0xff);          // All Port B pins are inputs
    writeByte(GPPUB, 0xff);           // All Port B pins are pulled up
    writeByte(IPOLB, 0xff);           // All Port B pins are inverted (GND = 1)
    writeByte(GPINTENB, 0xff);        // All Port B pins interrupt on change
    writeByte(INTCONB, 0);            // Interrupt on any change
    readByte(INTCAPB);                // Empty the Port B capture register
  SPI.endTransaction();

  // Merge interrupts even though we're only using one

  SPI.beginTransaction(spiSettings);
    writeByte(IOCONA, 0b01000000);             // Mirror Port A/B interrupts
    writeByte(IOCONB, 0b01000000);
  SPI.endTransaction();

  Serial.println("MCP23S17 port B->A inverter:");
}

Arduino programs continue by calling the loop() function continuously, once the setup() function has returned.

This is fairly trivial, in the general case we just delay() for 50 milliseconds. If we did catch an interrupt, we invert the value and write it to Port A, which has 8 LEDs attached, then print the new value on the console.


void loop() 
{
  // This is totally not debounced.  We use the delay below to keep it from bouncing too much.
  if (gotOne)
  {
    gotOne = false;
    SPI.beginTransaction(spiSettings);
      writeByte(GPIOA, capture ^ 0xff);
    SPI.endTransaction();               //release the SPI bus
    Serial.print("Got one: ");
    Serial.println(capture, BIN);
  }

  delay(50);
}

So that's it! When I get the FT4222 board, the first task will be to duplicate this through the USB interface, from Linux. That will be time for another post.

I couldn't find any simple, clear examples of using the interrupt on capture mode, so I made my own. I should probably gist this too.

Comments

Popular Posts