Authors: Ben Everson, Wilson Guo, Bowen Quan
Before writing any code for this tutorial, make sure you checkout a new branch with git checkout -b ______
, where _____
is the name of your branch. Including your name or username in the branch name is helpful for keeping track of who wrote which branch.
In this tutorial, you will write a driver that runs on the L432KC Nucleo microcontroller which can interface with the MCP23017 GPIO (General Purpose Input Output) expander via the I2C communication protocol.
A GPIO expander provides additional pins which can be used to read input values or write output values. For example, you can write a 1 to a pin that's connected to an LED to turn it on, or read the value from a pin that's connected to a switch. This device is useful if you are already using all the GPIO pins on your microcontroller and need more GPIO pins.
- Complete the functions in mcp23017.cpp
- Write test code in main.cpp to turn on an LED attached to pin 7 and read in the value from a switch from pin 5.
The datasheet has a lot of information about the registers of the MCP23017, their default values, and its I2C subordinate address.
Here are a few sections of the datasheet that are really helpful for this tutorial:
- Each register stores 8 bits of data.
- Each register has a specific address (or offset). (Ex. the GPIOA register has an address of 0x12)
- Since the MCP23017 has 2 banks, each type of register has an A register and a B register. (Ex. IODIRA and IODIRB) For this tutorial, we will only use bank A.
The I/O Direction register controls whether each individual pin should be configured as an input or an output. Each bit corresponds to an individual pin.
- A bit set to 0 indicates the pin is an output.
- A bit set to 1 indicates the pin is an input.
The bits in the GPIO port register correspond to the value at each GPIO pin.
- 0 means the pin is driven low.
- 1 means the pin is driven high.
- For an input pin, the MCP23017 will set the value of the bit to the level the pin is driven to.
- For an output pin, we can write a 0 or a 1 to that bit to change the level of the pin.
I2C communication is done between a monarch and one or more subordinates. In our case:
- the MCU (microcontroller unit) is the monarch
- the MCP23017 is the subordinate.
I2C subordinates have an address. This allows the monarch to communicate with multiple devices via I2C. To determine the MCP23017's I2C address, see this part of the datasheet:
A2, A1, A0 are the values tied to the address pins of the MCP23017. In the current hardware setup, they are all tied to GND (ground), which means they have a value of 0.
The monarch will write:
- the control word (I2C subordinate address with LSB=0)
- the register offset
- data (however many bytes)
The monarch will first write:
- the control word (I2C subordinate address with LSB=1)
- the register offset
Then, the monarch will read by:
- writing the control word (I2C subordinate address with LSB = 1)
- reading data (however many bytes you specify)
Here are a few functions from the Arduino library that will be useful for implementing I2C reads and writes:
The data in an I2C message is always one or more bytes long. This means that we can't just tell the device to write or read a single bit of a register. We will always be dealing with all 8 bits, AKA a byte, at once.
- Record the device address. This will be used in the I2C reads and writes of other functions.
This function should return the value of a specific pin's direction. To do this, we should:
- Specify to the MCP that we want to read from the IODIRA register from by writing the correct register offset.
- Read ONE byte from the IODIRA register we just specified.
- Do bitwise arithmetic to figure out what part of the byte corresponds to the pin we are looking for.
- Return the value of the pin.
Hint: The "and" (&) and "shift right" (>>) operators might be helpful to filter out the pin you are looking for.
This function should do the same thing as get_dir()
, except it reads from the GPIO register instead of the direction register.
Hint: We will read and write to these IODIR and GPIO registers a lot, so defining macros at the top for these values is a good idea!
This function should set the direction (input/output) of a given pin to a given value.
- Find out which value (0 or 1) means "input" and which value means "output". It's a good idea to write a comment in the code for this to help anyone who will be using this driver in the future.
- Remember that all I2C communication is done with bytes, so we can't write to specific bit in the register to change its value. Think about what we need to do to preserve the values of the other bits of the register.
- We will need to use bitwise operations to set a certain bit position to a 0 or a 1. (Hint: the operations for 0s and 1s are different).
Similar to how get_dir
was nearly identical to get_state
, set_state
is going to be very similar to set_dir
. There shouldn't be too many changes here.
This function should:
- read and verify the default value of the IODIRA register to confirm I2C communication between the microcontroller and the MCP23017
- set the direction of each pin on the MCP23017. Note that this function takes in an array of 8 integers called
directions
. These define the direction to be set for each pin in bank A. - return a 0 if the initialization is a success (default value of IODIRA is read correctly), 1 otherwise.
It is a good idea to perform a verifiable I2C transaction to make sure everything is working correctly. Most I2C devices contain a register that holds what is called a Device ID number, but the MCP23017 doesn't. So, we will use the IODIRA register as our "Device ID" register instead. If we can read and verify the default value of the IODIRA register in the begin
function, we can have confidence that our I2C communication is working properly.
Hint: the default value of the IODIRA register is in the datasheet.
First, make sure the functions you implemented all compile by clicking the check mark at the bottom bar of VS Code. The target has already been set as the Nucleo L432KC.
Second, open a Pull Request in this Github Repo. This way, the firmware lead can look over your code and give feedback.
To test on the hardware, you can write test code in main.cpp that:
- initializes an
Mcp23017
object. (See the I2C section to learn how to determine the I2C address of the MCP23017) - calls
begin()
with an array specifying the directions of the pins (remember that the LED pin should be output and the switch pin should be input) - turns the LED on and off, and reads the value of the switch in
loop()
. Don't forget to use a few delay() or your code will run super quickly!