Running I2C on Pro Micro (3) - Pin State Detection
目次
- Top
- Required Parts
- Parts Used So Far
- Wiring
- Reading the Datasheet (Pin-Related)
- Implementing on a Breadboard
- Reading Pin States
- Reading the Datasheet (Register-Related)
- IODIR
- GPPU
- IPOL
- GPIOx
- Reading Method
- Creating a Program (Arduino Standard Library Edition)
- Writing to Registers
- Reading from GPIO
- Whole Program
- Changing Polarity
- Using a Library
- Summary
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
.
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
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.
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!
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
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.
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
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
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
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
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
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
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.
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
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.