header source
my icon
esplo.net
ぷるぷるした直方体
Cover Image for Running I2C on Pro Micro (3) - Pin State Detection

Running I2C on Pro Micro (3) - Pin State Detection

about28mins to read

In this third installment of the series, we will receive the state of the connected device.

Last time, we discovered the I2C device, so this time we will determine if a button is pressed at a specific address. The device connected this time has an address of 0x20, so we will perform initialization processing and confirm the high/low state of the pins.

Required Parts

Only a switch is needed.

  • 1 switch for operation verification
    • same as the switch used for resetting

Parts Used So Far

  • Breadboard (e.g., BB-801) x1

  • Pro Micro + Pin Header (e.g., from Yushakobo) x1

  • Reset switch x1

  • Jumper wires (e.g., from Akizuki Denshi) x many

  • Cable for connecting Pro Micro to PC

  • MCP23017 x1

  • 1kΩ resistors x2

  • Breadboard (e.g., BB-801) x1

    • You can use one breadboard if you want to separate it.

Wiring

Reading the Datasheet (Pin-Related)

Let's confirm the pins of the MCP23017 from the datasheet. You can also search for the datasheet by name or find it on parts sites like Akizuki.

The datasheet is like a research paper, with a summary at the beginning. Read the title and summary to get an overview, and then dive into the details as needed. There are also reference implementations, so take a look at those too.

From now on, we will learn how to read the datasheet using the MCP23017.

Reference: https://ww1.microchip.com/downloads/aemDocuments/documents/APID/ProductDocuments/DataSheets/MCP23017-Data-Sheet-DS20001952.pdf
Reference: https://ww1.microchip.com/downloads/aemDocuments/documents/APID/ProductDocuments/DataSheets/MCP23017-Data-Sheet-DS20001952.pdf

Even just reading the summary on the first page, we can see a lot of information.

  • It can handle 16-bit input/output with one chip.

  • There is a similar IC called MCP23S17 that uses SPI communication. Be careful not to confuse them!

  • It supports 400kHz I2C communication (Fast-mode).

  • There is a reset pin.

  • It can operate at both 5V and 3.3V.

    • The voltage of USB is 5V, and Pro Micro often outputs 3.3V.
  • GPA7 and GPB7 pins require attention.

There are also interrupt-related functions, but we won't use them, so let's ignore them. The pin assignment is also listed, which we will see many times in the future.

What we will see many times in the future
What we will see many times in the future

Pro Micro has some pins that are not seen, but basically, they are the same.

VDD and VSS can be thought of as VCC and GND, respectively. Drain and Source are abbreviations, but it's a mystery whether users need to worry about them. Detailed people may see it differently. For details, see: What are VCC, VEE, VDD, and VSS?

SCK is the same as SCL. It's a bit confusing. However, even if it's written as SCK, it's not I2C if it's not used for I2C, so be sure to check.

Pin details

If we ignore the difficult waveform page in the middle and look at the pin details, we can see that it's a general-purpose input/output (GPIO). Looking at this, we can see that we can receive input using GPxx.

Here, we need to confirm the internal weak pull-up resistor. This IC's GPIO has an internal pull-up resistor, so we don't need to prepare one ourselves. It's convenient!

GPIO and other input pins

Looking at the input pins other than GPIO, we can see that they Must be externally biased. This means we need to connect them to GND or VCC properly. If we don't, the address will fluctuate, and we might get stuck in a reset loop. I've experienced it myself.

By the way, unused input pins should be connected to VCC or GND. It's recommended to pull them up or down. Ideally, it's okay to connect them directly to VCC/GND, but it's better to pull them up to prevent noise and unexpected shorts (Reference 1, Reference 2).

NC pins are inputs, but they are not connected internally, so it's okay to leave them as is. However, it's better to connect them to GND for peace of mind.

Implementing on a Breadboard

Now that we've understood the wiring, let's add a switch. This time, we will connect it to GPB0.

Implementation: Adding a switch to the I2C device
Implementation: Adding a switch to the I2C device

As mentioned earlier, we will use the internal pull-up, so the wiring is simple: GPB0 pin → switch → GND.

Reading Pin States

From now on, we will create a program to read the pin states.

Reading the Datasheet (Register-Related)

Let's confirm the process of reading the pin states from the datasheet. However, it's too much to read everything, so we will extract the important points.

First, let's look at the list of settable registers.

https://ww1.microchip.com/downloads/aemDocuments/documents/APID/ProductDocuments/DataSheets/MCP23017-Data-Sheet-DS20001952.pdf
https://ww1.microchip.com/downloads/aemDocuments/documents/APID/ProductDocuments/DataSheets/MCP23017-Data-Sheet-DS20001952.pdf

IOCON.BANK's value changes the setting address. The default is BANK=0, so we won't change it.

There are many registers, but the important ones are IODIRx, GPPUx, GPIOx, and IPOLx. Each has A and B pin columns. Let's look at the details.

IODIR

IODIR

This sets the input/output of each pin. 1 is input, 0 is output, and the default value is 0 (output). This time, we want to input, so we set all pins to 1.

However, as noted, the most significant bit must be 0.

GPPU

GPPU

This sets the pull-up. As mentioned earlier, the IC has an internal pull-up resistor. This setting enables or disables it. 1 enables, 0 disables, and the default value is 0.

It's written that a 100kΩ resistor is built-in. The pull-up resistor is weak, so we might need to prepare an external one depending on the situation.

IPOL

IPOL

This sets the polarity of the input (Low/High). The default is as is. It's not clear what this setting is for, but it's actually convenient. We will see how to use it later.

GPIOx

GPIOx

This is used to read the values. This register address is specified, and the values of each pin column are returned.

Reading Method

To read the pin states from the MCP23017, we send a read command from the Pro Micro. Then, the values are returned in byte units, and we process them on the Pro Micro side. Since the MCP23017 has 16 bits, we need to perform this process twice to get all the pin information.

To change the behavior, we write the setting values to the registers. We write the settings to the registers before reading the pin states, and the settings are reflected from the first read.

The register settings are set to their default values when the power is turned on. We need to confirm this. This time, we can't change the reset of the MCP23017, so we will forcibly set the default values by resetting the Pro Micro.

Creating a Program (Arduino Standard Library Edition)

Let's actually read the input and confirm it.

The MCP23017 is widely used, and there are convenient libraries available. First, we will write it using the standard library, and then we will use a convenient library.

Writing to Registers

Writing to registers uses beginTransmission, write, and endTransmission.

void writeRegister(byte i2c_addr, byte addr, byte v) {
  Wire.beginTransmission(i2c_addr);
  Wire.write(addr);
  Wire.write(v);
  Wire.endTransmission();
}

Reading from GPIO

Reading the pin states requires two steps. First, we use beginTransmission, write, and endTransmission to specify the GPIOx address. Then, we use requestFrom to read the values.

int readGPIO(byte i2c_addr) {
  Wire.beginTransmission(i2c_addr);
  Wire.write(GPIOA);
  Wire.endTransmission();
  byte received_bytes = Wire.requestFrom(i2c_addr, static_cast<byte>(2));
  if (received_bytes != 2) {
    Serial.println("cannot read 2 bytes");
  }
  uint8_t gpioA = Wire.read();
  uint8_t gpioB = Wire.read();

  Serial.print("gpioA: ");
  Serial.println(gpioA);

  Serial.print("gpioB: ");
  Serial.println(gpioB);

  return gpioA | (gpioB << 8);
}

Here, we specify only GPIOA and read 2 bytes. This is because the GPIOA and GPIOB addresses are consecutive, so we can get both values at once.

Whole Program

Combining these processes, we get the following program.

#include <Wire.h>

const byte I2C_ADDR = 0x20;

const byte IODIRA = 0x00;
const byte IODIRB = 0x01;
const byte IPOLA = 0x02;
const byte IPOLB = 0x03;
const byte GPPUA = 0x0C;
const byte GPPUB = 0x0D;

const byte GPIOA = 0x12;
const byte GPIOB = 0x13;

void writeRegister(byte i2c_addr, byte addr, byte v) {
  Wire.beginTransmission(i2c_addr);
  Wire.write(addr);
  Wire.write(v);
  Wire.endTransmission();
}

int readGPIO(byte i2c_addr) {
  Wire.beginTransmission(i2c_addr);
  Wire.write(GPIOA);
  Wire.endTransmission();
  byte received_bytes = Wire.requestFrom(i2c_addr, static_cast<byte>(2));
  if (received_bytes != 2) {
    Serial.println("cannot read 2 bytes");
  }
  uint8_t gpioA = Wire.read();
  uint8_t gpioB = Wire.read();

  Serial.print("gpioA: ");
  Serial.println(gpioA);

  Serial.print("gpioB: ");
  Serial.println(gpioB);

  return gpioA | (gpioB << 8);
}

void I2CSetup(byte address) {
  writeRegister(address, IODIRA, 0xFF >> 1);
  writeRegister(address, IODIRB, 0xFF >> 1);

  writeRegister(address, GPPUA, 0xFF);
  writeRegister(address, GPPUB, 0xFF);

  // writeRegister(address, IPOLA, 0x00);
  // writeRegister(address, IPOLB, 0x00);
  // writeRegister(address, IPOLA, 0xFF);
  // writeRegister(address, IPOLB, 0xFF);
}

void setup() {
  Wire.begin();

  Serial.begin(9600);
  while (!Serial)
    ;  // Leonardo: wait for serial monitor
  I2CSetup(I2C_ADDR);
}

void loop() {
  int gpio = readGPIO(I2C_ADDR);

  Serial.print("gpio: ");
  Serial.println(gpio);

  delay(1000);
}

This program reads the input every second and outputs the result. If the button is not pressed, it's 127, and if the button is pressed, gpioB becomes 126.

No input state
No input state

Changing Polarity

Looking at the output values, we can see that GPIOA and GPIOB are both 127. Why is that?

It's because the internal pull-up is enabled, so the input becomes high if there is no input. Also, GPIOA/B's 7th bit is output and always 0, so the result is 0b01111111 = 127.

However, when we handle it in the program, we want 0 if there is no input and 1 if the button is pressed. That's when the IPOLx register comes in handy. It changes the polarity of the input.

If we uncomment the following code, the polarity changes.

  writeRegister(address, IPOLA, 0xFF);
  writeRegister(address, IPOLB, 0xFF);

Running the program, we get a very human-friendly display.

Polarity changed state
Polarity changed state

Using a Library

Finally, let's rewrite the program using a library. The Arduino IDE has a library manager, so we will use it.

Searching for MCP23017, we find several libraries. We will use Adafruit's library. It's convenient and has dependencies, so it's easy to install.

Using Adafruit's library. Convenient.
Using Adafruit's library. Convenient.

The code is also available on Github.

https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library

Referencing the sample code, we can write a program like this. There is no function to invert polarity, so I manually invert

#include <Adafruit_MCP23X17.h>

const byte I2C_ADDR = 0x20;
const byte button_pin = 8;

Adafruit_MCP23X17 mcp;

void I2CSetup(byte address) {
  if (!mcp.begin_I2C()) {
    Serial.println("Error.");
    while (1)
      ;
  }
  for (byte i = 0; i < 16; i++)
    mcp.pinMode(i, INPUT_PULLUP);
}

void setup() {
  Wire.begin();

  Serial.begin(9600);
  while (!Serial)
    ;  // Leonardo: wait for serial monitor
  I2CSetup(I2C_ADDR);
}

void loop() {
  uint16_t gpio = mcp.readGPIOAB();

  Serial.print("gpio: ");
  Serial.println(~gpio);

  delay(1000);
}

Using a library
Using a library

It's clean and tidy. Let's stand on the shoulders of giants.

Summary

We have finally received the input and utilized the I2C device. The process we went through this time will be the same for various devices that will appear in the future.

Next time, we will connect breadboards using a TRRS cable and try to connect them remotely.

Share