Thursday 3 January 2013

I2C Port Expander & LCD working with Marlin

Previously...

The I2C port expander proved capable of driving a 20x4 LCD screen, reading the status of a quadrature encoder with click switch, driving some LEDs and a piezo buzzer.

Here is a quick video of this configuration working with Marlin firmware:


I had to make a few changes to marlin to make this happen which are detailed below.

Marlin Firmware integration

Lincomatic has forked Marlin and edited it to use his LiquidTWI2 library. This is a fast I2C LCD library designed for the adafruit RGB LCD shield, I added a couple of functions for this setup which Lincomatic has incorporated and updated the version number of LiquidTWI2 to 1.1.1. I used these as a start state as Lincomatic's version of Marlin was already proven with an I2C LCD -  all the line numbers below refer to this version.

The changes required are:

to configuration.h (line 292) add:

   #define PANELOLU2

   #ifdef PANELOLU2
   #define VERSAPANEL

   #define I2CENCODER //define this if you want to use the encoder over I2C
  #endif


This allows using the Panelolu2 with the encoder connected to either the port expander or directly to the main board.

to pins.h add (at line 657) :

  #ifdef ULTRA_LCD
    #ifdef NEWPANEL
      #define BEEPER -1

    #ifdef PANELOLU2
      #ifndef I2CENCODER
        #define BTN_EN1 10
        #define BTN_EN2 11 
        #define BTN_ENC 30  //the switch
      #endif
    #else
        //we have no buzzer installed
        #define BEEPER -1
        //LCD Pins
        #define LCD_PINS_RS        4
        #define LCD_PINS_ENABLE    17
        #define LCD_PINS_D4        30
        #define LCD_PINS_D5        29
        #define LCD_PINS_D6        28
        #define LCD_PINS_D7        27

        //The encoder and click button
        #define BTN_EN1 11
        #define BTN_EN2 10
        #define BTN_ENC 16  //the switch
      #endif //PANELOLU2

      //not connected to a pin
      #define SDCARDDETECT -1

      //from the same bit in the RAMPS Newpanel define
      //encoder rotation values
      #define encrot0 0
      #define encrot1 2
      #define encrot2 3
      #define encrot3 1
    
      #define BLEN_C 2
      #define BLEN_B 1
      #define BLEN_A 0

    #endif //Newpanel
  #endif //Ultipanel


to temperature.cpp replace

  #ifdef ULTIPANEL
    buttons_check();
  #endif

with (4 times at lines 956,980,1001,1022) :

  #ifdef ULTIPANEL
    #ifndef I2CENCODER
      buttons_check();
    #endif

  #endif

This is because Marlin uses a timer interrupt to run the temperature check code. Each time this interrupt runs, the status of the buttons on a Ultipanel-compatible controller are checked. If connected over I2C the encoder & click buttons cannot be checked within this interrupt routine as the Arduino wire library is blocking it. So we exclude this button check call and will check the encoder at another point.

On to ultralcd.pde. In the beep() and beepshort() functions replace

  //return;
  #ifdef ULTIPANEL

with (2 times at lines 189, 207) :
 
  //return;
  #ifdef PANELOLU2
     lcd.buzz(300,4000);
  #elif defined ULTIPANEL

This uses the buzzer on the port expander whenever beep() or beepshort() are called. Still in ultralcd.pde, in the lcd_status() function replace:

  #ifdef MCP23017_LCD
    if (lcd.readButtons() & BUTTON_SELECT)
      buttons |= EN_C;
  #endif


with (at line 225) :


  #ifdef MCP23017_LCD
    #ifndef I2CENCODER
      if (lcd.readButtons() & BUTTON_SELECT)
        buttons |= EN_C;
    #endif
  #endif


and


 //  buttons_check(); // Done in temperature interrupt

with (at line 252) :

  #ifdef I2CENCODER
    buttons_check(); //Done in temperature interrupt except if using I2C buttons
  #endif


As mentioned earlier Marlin uses the temperature check routine to call buttons_check() but as that is inside an interrupt routine we can't check it there - hence the check here.

Still within ultralcd.pde, the buttons_init() function has been edited so as not to throw errors if the buttons are not defined in pins.h (from line 281)



  #ifdef NEWPANEL
    #ifdef BTN_EN1
      pinMode(BTN_EN1,INPUT);
      WRITE(BTN_EN1,HIGH);
    #endif

    #ifdef BTN_EN2
      pinMode(BTN_EN2,INPUT);
      WRITE(BTN_EN2,HIGH);
    #endif

    #ifdef BTN_ENC
      pinMode(BTN_ENC,INPUT);
      WRITE(BTN_ENC,HIGH);
    #endif

    #if (SDCARDDETECT > -1)
    {
      pinMode(SDCARDDETECT,INPUT);
      WRITE(SDCARDDETECT,HIGH);
    }
    #endif

 
Still within ultralcd.pde the buttons_check() function has been edited to handle the different cases of an encoder connected via I2C or directly. The first part of the function was changed to the following (from line 310):


void buttons_check()
{
  uint8_t newbutton=0;
  #ifdef I2CENCODER
    uint8_t lrb = lcd.readButtons();
    if (lrb & ENCODER_A) newbutton |= EN_A;
    if (lrb & ENCODER_B) newbutton |= EN_B;
    if (lrb & ENCODER_C) newbutton |= EN_C;
  #elif defined(NEWPANEL)
    #ifdef BTN_EN1
      if(READ(BTN_EN1)==0)  newbutton|=EN_A;
    #endif

    #ifdef BTN_EN2
      if(READ(BTN_EN2)==0)  newbutton|=EN_B;
    #endif

    #ifdef BTN_ENC
      if((blocking<millis()) &&(READ(BTN_ENC)==0))
        newbutton|=EN_C;
    #endif     
    buttons=newbutton;


These changes allow for two configurations which can be switched between using the #define I2CENCODER within configuration.h :

with I2CENCODER defined:
  outputs (LCD, Buzzer, LEDs) use I2C
  inputs (Encoder with click switch) use I2C

with I2CENCODER not defined:
  outputs (LCD, Buzzer, LEDs) use I2C
  inputs (Encoder with click switch) are directly connected on the pins defined in pins.h

The reason I have included the option of directly connecting the encoder is because of the way marlin checks the buttons. Currently it's quite possible for encoder rotations to be lost if connected over I2C.


Next Steps

Since starting on this project Marlin has been updated to change how lcd displays are handled, splitting the menu system from the display implementation. There is currently a header file for the "standard" implementation using an LCD directly like the Panelolu. The obvious next step is to write a similar header file to use an I2C LCD using LiquidTWI2. In addition I need to add an option to use an interrupt on a pin to check the encoder status rather than a timer interrupt - this will ensure steps are not missed and the encoder can be safely used over I2C.