Continue to Site

Welcome to our site!

Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

  • Welcome to our site! Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.

Tips for MCP23S17 port expander IC?

Status
Not open for further replies.

Mr RB

Well-Known Member
Hi, I need to use a Microchip MCP23S17 16pin SPI port expander IC for the most basic application; I just need to send it 8bits to be output on 8 of its 16 pins.

All the examples i've got are way overcomplex and cover all of the IC's features including using its pins as inputs/outputs, using the IC's internal interrupts etc.

Does anyone have an example of the most basic procedure to do these 2 tasks;
1. Initialise IC and setup 8 pins (one port) as outputs
2. Then send a byte to the 8 outputs

I have limited PIC rom space (16F) to fit these 2 functions.
Thanks! :)
 
Like most devices it's not that hard once it's FINALLY working... ;)

The minimum procedure to get it working as a simple output port is;
1. Reset
2. Send SPI bytes 0x40,0x0A,0xA0 (configures IOCON reg)
3. Send SPI bytes 0x40,0x10,0x00 (set PORT1 direction all outs)
4. Send SPI bytes 0x40,0x1A (sets PORT1 address ready to receive a byte)
5. (Now every SPI byte sent just writes direct to PORT1 outputs)

Here's the basic code in C for PIC 16F, I got it to 21 rom for send_byte and 47 rom for init() so total 68 rom. Could reduce it down to 50 or so with just a change to assembler.

Code:
// define port expander SPI pins
#define   REXP_PIN_CS    PORTA.F2
#define   REXP_PIN_RST   PORTA.F3
#define   REXP_PIN_SCK   PORTC.F3
#define   REXP_PIN_SO    PORTC.F4
#define   REXP_PIN_SI    PORTC.F5


//=============================================================================
//  RomanEXP_SEND_BYTE   sends SPI byte to port expander IC
//=============================================================================
void RomanEXP_send_byte(unsigned char xbyte)
{
  // this sends one SPI byte out the selected pins
  unsigned char REXP_bit;
  
  // loop and send 8 SPI bits
  REXP_bit = 8;
  while(REXP_bit)
  {
    if(xbyte.F7)  REXP_PIN_SI = 1;  // prep data bit on SPI bit SI
    else          REXP_PIN_SI = 0;
    asm nop;
    REXP_PIN_SCK = 1;               // make a clock pulse
    asm nop;
    REXP_PIN_SCK = 0;               // end clock pulse
    xbyte = (xbyte << 1);   // prep next bit
    REXP_bit--;
  }
}


//-----------------------------------------------------------------------------

// wrap compiler inline delay as a function to save code size.
void delay_1mS(void)
{
  Delay_ms(1);
}


//=============================================================================
//  RomanEXP_INIT    initialises the port EXPander
//=============================================================================
void RomanEXP_init(void)
{
  //-------------------------------------------------------
  // this initialises the port EXPander very simply,
  // to set port1 as output and use port1 only.
  // run this only once at startup
  //-------------------------------------------------------
  // set the SPI pin directions for PIC 16F877
  REXP_PIN_RST = 1;     // safe! make RST pin low first
  TRISA &= 0b11110011;  // set 2pins as outputs; CS, RST
  TRISC &= 0b11010111;  // set 2 outputs; SCK, SI, (SO is already input)

  REXP_PIN_CS = 1;      // make CS pin hi, disables IC
  REXP_PIN_SCK = 0;     // SCK low before we start

  REXP_PIN_RST = 0;
  delay_1mS();

  REXP_PIN_RST = 1;     // reset hi, brings IC out of reset
  delay_1mS();

  // load IOCON, sets up port expander IC features
  REXP_PIN_CS = 0;
  RomanEXP_send_byte(0x40);       // IC address
  RomanEXP_send_byte(0x0A);       // IOCON address
  RomanEXP_send_byte(0b10100000); // setup IOCON; bank=1, SEQOP=off, HAEN=0
  REXP_PIN_CS = 1;

  // set PORT1(B) as output
  REXP_PIN_CS = 0;
  RomanEXP_send_byte(0x40);       // IC address
  RomanEXP_send_byte(0x10);       // IODIRB address
  RomanEXP_send_byte(0x00);       // all 8 pins as outputs
  REXP_PIN_CS = 1;

  // set OLAT as the receiving register
  REXP_PIN_CS = 0;
  RomanEXP_send_byte(0x40);       // IC address
  RomanEXP_send_byte(0x1A);       // OLATB address

  // now any bytes sent to IC will write to port1 pins!
  RomanEXP_send_byte(0b01010101);       // set PORT1 pins to pattern
}
//-----------------------------------------------------------------------------

The MCP23S17 is fast enough that with 8Mhz xtal there is no problem with the timings shown.
 
Ok i've updated this project a bit. Now as well as the "minimalist" C code to send data to the port expander IC pins, I have included text LCD code.

The result is that you can run the port expander IC from 4 PIC pins (any pins as it is not using SPI module). And then one of the 8bit ports of the port expander IC is connected to a standard (44780 style) text LCD, using 4bit mode (6 pins needed). The other 8bit port of the port expander IC is free.

These functions below might be of use to anyone using the Microchip MCP23S17 port expander who needs to use a text LCD. My LCD functions are also very simple so since there have been a ton of PIC->LCD threads lately it may be of use to people who want the minimum code to initialise LCD and write some text to it. (My C code is very C-- so it should be very easy to convert to Basic or assembler.)

The full project can be found at the bottom of this page;
Some EasyPIC6 projects

And here are the port expander and LCD functions;


Code:
/******************************************************************************
  RomanEXPCOG.c       Open-source - Oct 2009 - www.RomanBlack.com
  These are some small routines to use the EasyPIC6 port expander
  and also drive the EasyPIC6 COG LCD.

  They are nothing fancy, but they do compile MUCH smaller so
  may be of use if you have limited ROM in an EasyPIC6 application.
  Also, they will work fine on the old MikroC C compiler (where the
  MikroE libraries require the new MikroC PRO compiler).
  
  Include this one file at the top of your source code and then you
  can use these simple port expander and COG LCD functions.

  For use with EasyPIC6 you don't need to edit this file!!
  NOTE! turn the EasyPIC6 port expander green LEDs OFF.
******************************************************************************/

// define port expander pins for EasyPIC6;
#define  REXP_PIN_CS    PORTA.F2
#define  REXP_PIN_RST   PORTA.F3
#define  REXP_PIN_SCK   PORTC.F3
#define  REXP_PIN_SO    PORTC.F4   (SO is not used)
#define  REXP_PIN_SI    PORTC.F5

// define EasyPIC6 COG LCD details
#define  REXP_LCD_HEIGHT        2   // char LCD display lines; 1,2,4, allowed
#define  REXP_LCD_WIDTH        16   // char LCD width; 8,16,20,40 allowed

#define  RCMD  0    // used in RomanEXP_lcd_byte()
#define  RCHAR 1

unsigned char REXP_trashdata; // used inside functions


//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

// this makes an 8 PIC instruction delay (including call)
void tiny_delay(void)
{
  asm goto $+1;     // 2 nops
  asm goto $+1;    
  asm goto $+1; 
}
// wrap compiler inline delay as a function to save code size.
void delay_6mS(void)
{
  Delay_ms(6);
}

//=============================================================================
//  RomanEXP_SEND_BYTE   sends SPI byte to port expander IC
//=============================================================================
void RomanEXP_Send_Byte(unsigned char xbyte)
{
  // this sends one SPI byte out the selected pins
  unsigned char REXP_bit;
  
  // loop and send 8 SPI bits
  REXP_bit = 8;
  while(REXP_bit)
  {
    if(xbyte.F7)  REXP_PIN_SI = 1;  // prep data bit on SPI bit SI
    else          REXP_PIN_SI = 0;
    tiny_delay();
    REXP_PIN_SCK = 1;               // make a clock pulse
    tiny_delay();
    REXP_PIN_SCK = 0;               // end clock pulse
    tiny_delay();
    xbyte = (xbyte << 1);   // prep next bit
    REXP_bit--;
  }
}
//-----------------------------------------------------------------------------


//=============================================================================
//  RomanEXP_LCD_BYTE            sends a BYTE to COG LCD
//=============================================================================
void RomanEXP_Lcd_Byte(unsigned char REXP_char, unsigned char REXP_cmd)
{
  //-----------------------------------------------------
  // this sends 2 4bit nibbles to the LCD, and each
  // nibble is clocked by a \ edge of the LCD E pin.
  // the 6 LCD pins are controlled by the EasyPIC6 port
  // expander IC.
  //-----------------------------------------------------
           
  // send top nibble first
  REXP_trashdata = (REXP_char & 0xF0);       // get top 4 bits
  REXP_trashdata.F3 = 1;                     // set E pin hi
  if(REXP_cmd) REXP_trashdata.F2 = 1;       // set RS (command=0/char=1)
  RomanEXP_Send_Byte(REXP_trashdata);        // send data and E hi
  REXP_trashdata.F3 = 0;                     // set E pin lo
  RomanEXP_Send_Byte(REXP_trashdata);        // send data and E \ clock pulse
  
  // now send bottom nibble
  REXP_trashdata = (REXP_char << 4);
  REXP_trashdata.F3 = 1;                                
  if(REXP_cmd) REXP_trashdata.F2 = 1;       // set RS (command=0/char=1)
  RomanEXP_Send_Byte(REXP_trashdata);                
  REXP_trashdata.F3 = 0;                        
  RomanEXP_Send_Byte(REXP_trashdata);        
}
//-----------------------------------------------------------------------------


//=============================================================================
//  RomanEXP_LCD_MOVE        moves LCD cursor to line 0-3 , col 0-39
//=============================================================================
void RomanEXP_Lcd_Move(unsigned char REXP_line, unsigned char REXP_col)
{
  //-----------------------------------------------------
  // formats line, col into the 1byte command needed
  // by the LCD and send it to the LCD.
  //  LCD command; 1aaaaaaa
  //
  // This should work for all char LCD sizes;
  //  LCD display HEIGHT; 1, 2, 4
  //  LCD display WIDTH; 8, 16, 20, 40
  //-----------------------------------------------------
  
  // safe limit to the defined char LCD screen size
  if(REXP_line >= REXP_LCD_HEIGHT) REXP_line = (REXP_LCD_HEIGHT - 1);
  if(REXP_col >= REXP_LCD_WIDTH)   REXP_col = (REXP_LCD_WIDTH - 1);
  
  // (now re-use variable SH1_col to hold the command)
  // for 4 line displays only; if line >1 fix the address
  if(REXP_line.F1) REXP_col += REXP_LCD_WIDTH;        
  
  // for 2 and 4 line displays; set address bit6 if on ODD line
  if(REXP_line.F0) REXP_col.F6 = 1;        
  
  // set bit7, makes this byte into an LCD move command
  REXP_col.F7 = 1;
  
  // finally send that command to the LCD
  RomanEXP_Lcd_Byte(REXP_col,RCMD);
}
//-----------------------------------------------------------------------------


//=============================================================================
//  RomanEXP_LCD_OUT       send text string to COG LCD 
//=============================================================================
void RomanEXP_Lcd_Out(unsigned char sline, unsigned char scol,
                          unsigned char *stext)
{
  //-----------------------------------------------------
  unsigned char scount;
  
  // move display cursor
  RomanEXP_Lcd_Move(sline,scol);

  // now just print each char until NULL found
  scount = 0;
  while(stext[scount])
  {
    RomanEXP_Lcd_Byte(stext[scount],RCHAR);
    scount++;
  }
}
//-----------------------------------------------------------------------------

//=============================================================================
//  RomanEXP_LCD_CHAR       send one text character to COG LCD 
//=============================================================================
void RomanEXP_Lcd_Char(unsigned char schar)
{
  //-----------------------------------------------------
  RomanEXP_Lcd_Byte(schar,RCHAR);
}
//-----------------------------------------------------------------------------


//=============================================================================
//  RomanEXP_LCD_INIT     initialise COG LCD 
//=============================================================================
void RomanEXP_Lcd_Init(void)
{
  //-----------------------------------------------------
  // The COG LCD operates in 4bit data mode. (6 wires)
  // It is controlled from the EasyPIC6 port expander IC.
  //-----------------------------------------------------
  
  // manually send first control nibble to LCD!
  RomanEXP_Send_Byte(0b00101000);                // 0010 code for init, E=1, RS=0
  RomanEXP_Send_Byte(0b00100000);                // make E low, is LCD \ clock pulse
  delay_6mS();                                // 6mS delay needed by LCD
  
  //-----------------------------------------------------
  // now we can send commands to LCD as bytes
  //-----------------------------------------------------
  
  // Cmd, set interface length
  RomanEXP_Lcd_Byte(0x28,RCMD);     // 2=4bit, 8=2line display
  delay_6mS();
  
  // Cmd, display on/off control
  RomanEXP_Lcd_Byte(0x0C,RCMD);     // disp on, cursor off, block off.
  
  // Cmd, clear display (is the slow command)
  RomanEXP_Lcd_Byte(0x01,RCMD);     // clear display
  delay_6mS();                      // 6mS is long enough even for old LCDs
  
  // Cmd, entry mode set
  RomanEXP_Lcd_Byte(0x06,RCMD);     // 6=move cursor right, but don't scroll display
}
//-----------------------------------------------------------------------------



//=============================================================================
//  RomanEXP_INIT    initialises the EasyPIC6 port EXPander
//=============================================================================
void RomanEXP_Init(void)
{
  //-------------------------------------------------------
  // this initialises the EasyPIC port EXPander very simply,
  // to set port1 as output and use port1 only.
  //-------------------------------------------------------

  REXP_PIN_CS = 1;      // make CS pin hi, disables IC
  REXP_PIN_SCK = 0;     // SCK low before we start
  delay_6mS();

  REXP_PIN_RST = 1;     // reset hi, brings IC out of reset
  delay_6mS();

  // load IOCON, sets up port expander IC features
  REXP_PIN_CS = 0;
  RomanEXP_Send_Byte(0x40);       // IC address
  RomanEXP_Send_Byte(0x0A);       // IOCON address
  RomanEXP_Send_Byte(0b10100000); // setup IOCON; bank=1, SEQOP=off, HAEN=0
  REXP_PIN_CS = 1;

  // set PORT1(B) as output
  REXP_PIN_CS = 0;
  RomanEXP_Send_Byte(0x40);       // IC address
  RomanEXP_Send_Byte(0x10);       // IODIRB address
  RomanEXP_Send_Byte(0x00);       // all 8 pins as outputs
  REXP_PIN_CS = 1;

  // set OLAT as the receiving register
  REXP_PIN_CS = 0;
  RomanEXP_Send_Byte(0x40);       // IC address
  RomanEXP_Send_Byte(0x1A);       // OLATB address

  // now every byte sent to expander IC will just write to port1 pins!
  RomanEXP_Send_Byte(0x00);       // set PORT1 pins LOW to start
}
//-----------------------------------------------------------------------------
 
Last edited:
Nice work Roman. Thank you for sharing.

If you're trying to squeeze even more functionality out of that module you might consider a function for setting the D4..D7 and RS outputs, without sending the E pulse, for use as column or row driver lines for a keypad. I've done this with a couple simple 44780 type LCD interfaces now and it works well.

Actually now that I think about it, you could do the whole keypad interface with that port expander IC (duh!).

Take care. Kind regards, Mike

keypad-driver-mods-png.34399
 

Attachments

  • keypad driver mods.PNG
    keypad driver mods.PNG
    59.9 KB · Views: 5,561
Last edited:
Good one Mike, I didn't think about the keyboard driving.

One of my functions in the library above does exactly what you asked, ie send a byte to the LCD pins;

Code:
//=============================================================================
//  RomanEXP_SEND_BYTE   sends SPI byte to port expander IC
//=============================================================================
void RomanEXP_Send_Byte(unsigned char xbyte)
{
  // this sends one SPI byte out the selected pins
  unsigned char REXP_bit;
  
  // loop and send 8 SPI bits
  REXP_bit = 8;
  while(REXP_bit)
  {
    if(xbyte.F7)  REXP_PIN_SI = 1;  // prep data bit on SPI bit SI
    else          REXP_PIN_SI = 0;
    tiny_delay();
    REXP_PIN_SCK = 1;               // make a clock pulse
    tiny_delay();
    REXP_PIN_SCK = 0;               // end clock pulse
    tiny_delay();
    xbyte = (xbyte << 1);   // prep next bit
    REXP_bit--;
  }
}
//-----------------------------------------------------------------------------

You can use that to send data to the LCD pins (to use them for other uses), provided the E pin (bit3) is kept HIGH there should be no issues and it will be ignored by the LCD.
 
Last edited:
Status
Not open for further replies.

Latest threads

New Articles From Microcontroller Tips

Back
Top