Cornell University ECE4760
mcp23s17 Port Expander
Pi Pico rp2040/2350
The mcp23s17 description and wiring
The Microchip mcp23s17 port expander adds 16 lines of dgital i/o, driven by an SPI connection to the Pico. The 16 lines are grouped into two 8-bit ports. Each i/o line may be set to input or output. Each i/o line may turn on an internal pullup resistor (there are no pulldowns), if it is an input. The grouping of lines into two 8-bit ports means that one spi transaction can affect the eight i/o lines of PortA or of PortB. For example, you can write a uint8_t number to a port with one spi transaction. You can also set the data direction of each of the eight bits of a port by writing an 8-bit value with desired direction encoded as high for output and low for input. For example writing 0xf0 to the data direction register of PortA sets bits 0 to 3 as input and bits 4 to 7 as output. Similarly, writing eight bits to the pullup enable register turns on pullups on lines with a high bit. For example writing 0x03 to the pullup enable register of PortB turns on pullups on bits zero and one of PortB.
The mcp23s17 we use is in 28-pin PDIP package as shown below.
For the code below the INTA and INTB pins are not connected, and the RESET pin is wired high.

The wiring for the PortA to PortB loop-back test. The convienient wiring of the 470 ohm resistors directly
across the package from PortA to PortB results in bit reversal when reading the loop-back results in the code.
The wiring details as a text table.


The Port Expander command interface.
Adding
#include "mcp23s17_rp2040.h"
to your program adds the port expander code.
In the following functions:
- port_name must be either PORTA or PORTB
- write data must be in the range 0x00 to 0xff (0 to 255)
- both direction and pullup data must be in the range 0x00 to 0xff
- in the event that you want more general expander function, reg_addr directly references all config reguisters.
Functions:
- void PEinit(void)
Sets up the spi channel speed, mode and i/o pins
Writes the config register of the port expander
- void PEgpioSetDir(int port_name, unsigned char dir)
Sets all eight bits of the named port for either output (1) or input (0)
- void PEgpioSetPullup(int port_name, unsigned char pullup)
Sets all eight bits of the named port for either pullup (1) or no pullup(0)
- void PEwritePort(int port_name, unsigned char data)
Sets all eight bits of the named port output data
- void PEwriteBit(int port_name, char bit_num, int data)
Sets the value of one bit by reading the port, modifiying the data, then writing the port.
Note that it takes twice the time to write one bit as it does to write the whole port.
- unsigned char PEreadPort(int port_name)
Reads all eight bits of the named port input data
- unsigned char PEreadBit(int port_name, char bit_num)
Note that it takes twice the time to read one bit as it does to read the whole port.
- lower level routines:
- unsigned char PEreadReg(unsigned char reg_addr)
Does a direct read of any of 15 config registers
of the port expander.
Typical use does not require you to call this function
- void PEwriteReg(unsigned char reg_addr, unsigned char data)
Does a direct wite of any of 15 config registers
of the port expander.
Typical use does not require you to call this function
The loop-back test code.
This program merely exercises the inteface routines to the port expander to that you can test connections and see the syntax.
There are two threads running on one core. The usual blinky thread, of course, blinks. The serial thread:
- Sets up the port expander
PEinit() ;
makes all 8 port A pins outputs by writing 0xff to PortA direction register
PEgpioSetDir(PORTA, 0xff) ;
makes port B all inputs by writing zeros to PortB direction register
PEgpioSetDir(PORTB, 0x00) ;
- prompts for a 2-digit hexadecimal number and sends the number to output of PortA.
sscanf(pt_serial_in_buffer,"%2x", &port_A_out) ;
PEwritePort(PORTA, port_A_out) ;
then reads PortB, reverses the bits and prints
port_B_input = PEreadPort(PORTB) ;
port_B_input_rev = __rev((uint32_t) port_B_input) >> 24; // shift 24 because __rev is 32 bit
printf ("A_out=%02x B_in=%02x B_in_rev=%02x\n\r", PEreadPort(PORTA), port_B_input, port_B_input_rev);
- prompts for a bit number (0 to 7) and bit value (1/0) inserts the bit into PortA output
sscanf(pt_serial_in_buffer,"%d %d", &bit_position, &bit_value) ;
PEwriteBit(PORTA, bit_position, bit_value) ;
then reads PortB as above.
- prompts for a bit position (0 to 7) then reads that bit from PortA.
sscanf(pt_serial_in_buffer,"%d", &bit_position) ;
printf ("A bit=%1d\n\r\n\r",PEreadBit(PORTA, bit_position));
- repeats from 2 above
These commands exercise most of what the port expander can do.
A serial screen dump shows two examples.

Test Code, mcp23s17_rp2040.h, Project ZIP
Copyright Cornell University
July 4, 2025