Tuesday 18 December 2012

I2C port expander driving an LCD screen, click encoder and more

Adventures with the MCP23017 I2C port expander

Since working on the Panelolu I have been planning a project using the MCP23017 port expander. This chip gives 16 additional I/O ports which can be accessed using the I2C protocol, an easy to use bus communication protocol that is supported in the Arduino environment using the Wire library. My first exposure to I2C was during development of the Panelolu when I came across the Hobbytronics I2C LCD backpack, Adafruit also do a fancy 3 colour backlight LCD. I started this project in August but then put it on the back burner due to other commitments.

I re-invigorated the project recently as we get a lot of emails from people wanting to use the Panelolu with a Melzi or a Sanguinololu where pins on the expansion header are dedicated to other things. In both these cases the original Panelolu took too many pins. Added to this Nophead has announced he will be using Melzi on the Mendel90 which I feel is the best printer design out there right now.

Hardware setup.

The setup I have tested on uses a Sanguinololu but any similar reprap controller should work. The ISP header and 4 more pins are required:


Leaving 6 pins for other uses.

The circuit is setup as in the schematic




The Sanguinololu ISP and Expansion header pins are allocated as follows:
  • SCL and SDA are connected to pin 12 and 13 of the MCP23017
  • MOSI, MISO, SCK and A0 provide the SD card signals as they do in the Panelolu and SDSL.
  • A1 is the interrupt pin and I added an LED to this to help with debugging which is not illustrated in the schematic.
  • 5V and GND are distributed as required
from the MCP23017 the LCD display, Buzzer, Click Encoder and 3 LEDs are connected. There are 2 spare pins so further LEDS or something else could be connected.

Software Setup

I began testing in August with the library from Adafruit which does a great job of exposing the basic read and write functions of the expander. Recently Lincomatic updated the LiquidTWI2 library which extends the Adafruit library and I switched to that. It does not provide access to the interrupt functionality of the MCP23107 so I added some additional functions to the library:


//check registers
uint8_t LiquidTWI2::readRegister(uint8_t reg) {
  // read a register
  Wire.beginTransmission(MCP23017_ADDRESS | _i2cAddr);
  wiresend(reg);   
  Wire.endTransmission();
 
  Wire.requestFrom(MCP23017_ADDRESS | _i2cAddr, 1);
  return wirerecv();
}

//set registers

void LiquidTWI2::setRegister(uint8_t reg, uint8_t value) {
    Wire.beginTransmission(MCP23017_ADDRESS | _i2cAddr);
    wiresend(reg);
    wiresend(value);
    Wire.endTransmission();
}


In addition I added a function to switch the buzzer pin rapidly at a certain frequency to make a sound

//cycle the buzzer pin for a certain duration (ms) and at a certain freq (hz)
void LiquidTWI2::buzz(long duration, uint8_t freq) {
  int currentRegister = 0;
  // read gpio register
  Wire.beginTransmission(MCP23017_ADDRESS | _i2cAddr);
  wiresend(MCP23017_GPIOA);   
  Wire.endTransmission();
 
  Wire.requestFrom(MCP23017_ADDRESS | _i2cAddr, 1);
  currentRegister = wirerecv();
  duration *=1000; //convert from ms to us
  int period = (1.0 / freq) * 1000000; //*1000000 as the delay is in us
  long elapsed_time = 0;
  while (elapsed_time < duration)
  {
        Wire.beginTransmission(MCP23017_ADDRESS | _i2cAddr);
        wiresend(MCP23017_GPIOA);
        wiresend(currentRegister |= M17_BIT_BZ);
        while(Wire.endTransmission());
        delayMicroseconds(period / 2);
        Wire.beginTransmission(MCP23017_ADDRESS | _i2cAddr);
        wiresend(MCP23017_GPIOA);
        wiresend(currentRegister &= ~M17_BIT_BZ);
        while(Wire.endTransmission());
        elapsed_time += (period);
   }
}



I need to get up and running on GitHub until I do so the T3P3 version of LiquidTWI2 is available here. just download the as a zip and extract into the libraries subdirectory of the arduino folder so it looks something like:

C:\arduino-0023\libraries\LiquidTWI2\

At this time I have only tested it with Arduino 0023.

A1 is not a hardware interrupt pin so the PinChangeInt library for the Arduino is required. I used version 2.19, just download it according to the instructions and put in the libraries subdirectory of the arduino folder.

Example Sketch

I adapted the HelloWorld_i2c sketch that comes with LiquidTWI2, the full sketch is in the example sub directory of the T3P3 version of LiquidTWI. In essence the program uses the LiquidTWI2 library to setup communication with the MCP23017, configure and display information on the LCD and set the buzzer and LED pins.

It also sets interrupts on the click encoder pins and switch which trigger an interrupt on A1 every time the click encoder is turned or switch is pressed.

(from setup() )
...
    lcd.setRegister(MCP23017_GPINTENA,0x07); //enable interrupts on 3 pins
  lcd.setRegister(MCP23017_DEFVALA,0x07); //set the default values as 1

  //set to compare with register values set to 1 with DEFVALB 
  lcd.setRegister(MCP23017_INTCONA,0x07);
  lcd.setRegister(MCP23017_IOCONA,0x02); //enable active high for interrupt pin
  //read the interrupt capture register to reset it
  uint8_t reg = lcd.readRegister(MCP23017_INTCAPA);
  pinMode(interruptPin, INPUT); //set A1 (pin 30) as input

  //attach an interrupt using the pinChangeInt library to interruptPin
  PCintPort::attachInterrupt(interruptPin, &quickInt, RISING);

... 


The state of the click encoder and switch pins are are captured at the interrupt point and read, determining which way the encoder is turned or if the switch was pressed.

  void handleInterrupt() {
    //reset the flag
    rotating=false;  
    //get the interrupt status of the pins
    //this will clear the flag allowing further interrupts
    intCap=lcd.readRegister(MCP23017_INTCAPA);
    uint8_t test = intCap & 0b00000111;

    //only deal with the situation where the encoder is turned without the
    //click button held down
    if (test == 0b101) encoderPos += 1;
    if (test == 0b110) encoderPos -= 1;
    //mask out the encoder bits and look at the click button
    test &= 0b100;
    if (test == 0b000) clickButton = LOW;


    // while interrupts are still occurring clear the register (switch bouncing) 
    while(digitalRead(interruptPin) > 0)
    {
      
      lcd.readRegister(MCP23017_INTCAPA);
      delay (10);
    }
  }


The program does not make use of the SD card part of the design - that is planned for later when this is integrated into Marlin firmware. Also further testing on the buzzer is required as I believe the frequency is fairly limited by the I2C protocol and associated processing.

The video below demonstrated the example in operation.

Now off to make it work with Marlin firmware.