As always, if you prefer to watch instead of read, there is a corresponding YouTube video of this project.
For the first part of this article, I'll review the MCP23017, including pinouts, limitations and power options. I'll create a breadboard test with inputs and outputs and cover then ESPHome code that makes it work.
Then in part 2 of this article, I show an example of how I used a five MCP23017 chips and a single ESP8266 to control 72 different individual outputs to build a (completely impractical) clock with hour and minute LED lights. The ESP32 can always be substituted for the ESP8266 that I am using if desired.
But first, let's dig into the MCP23017.
Part 1 - MCP23017 Overview and Testing
The MCP23017 is a 16-bit I2C device that can provide up to 16 digital pins that can be used for either input or output. Since it is an I2C device, it has a unique address. By changing the I2C address (easily done through the pins, which I'll cover in the pinout below), up to eight of these devices can be 'daisy-chained' together using just two wires/GPIO pins from the ESP board giving you a total of 128 additional I/O pins from just two on the ESP board!
What's more, ESPHome (and Arduino/C++) has support for the MCP230xx series of chips, making it easy to use all these extra I/O pins and integrate them into Home Assistant. More on that below as well.
However, there are some important limitations to be aware of when using this chip.
First, the I/O pins are digital only. This means they only support input or output with on/off or true/false values. These are things like binary sensors (door window sensors, push buttons, etc.) for input and things like switches (LEDs, relays, etc.) for output. If you need pins for sensors that use analog values for things like temperature or humidity, then you need something like a multiplexer that supports analog values. But as long as your input/output just has an on or off state, this chip should do the job.
While PWM (pulse width modulation) is technically digital and is commonly used for things like controlling brightness of a light by quickly switching the signal on/off, it is unlikely that the MCP23017 would work well for PWM due to the precise nature of the timing required. But your mileage may vary.
The other big caveat here is that there is a limitation on the amount of current that can be passed through the output pins. Each pin is capable of 25 mA of current, but the total amount of current that can be passed through the chip for all output pins is limited to 125 mA (technically 125 mA through VDD and 150 mA through VSS but you should limit the total to 125 mA to be safe). Attempting to run more current than this through the chip will cause it to overheat and likely burn out. If driving a number of LEDs via the output pins, it will be important to use the proper resistors to keep the overall current draw below this total value.
MCP23017 Pinout and Connections
Before identifying the individual pins, it is important to orient the chip properly. Pin "1" is indicated by the small inset dot, as shown in the lower left corner above.
Click to enlarge |
Here are the pinouts and descriptions:
VDD (+) and VSS (-): These are the positive voltage and ground connections for the chip. The chip has an operating voltage range of 1.8 to 5.5V, meaning you can run it from either 3.3V or 5V. Since the GPIO pins from the ESP operate at 3.3V, I generally run the chip at 3.3V so I don't have to worry about any voltage differences or using any logic level shifters.
If you are only powering one MCP23017 chip, you can probably power it off the 3.3V pin of your ESP board. But be aware of the total current draw especially if using more than one IO port expander because your can only run a small amount of current from the ESP onboard 3.3V pin. More on this below.
SCL and SDA: These are the I2C interface pins and the only two that are required to be connected to the ESP board. For a Wemos D1 Mini, the SCL (clock) is normally connected to D1/GPIO5 and SDA (data) is connected to D2/GPIO4. For the ESP32, there are two I2C buses available and any pin can be configured for I2C, but the default pins are GPIO22 for SCL and GPIO21 for SDA.
A0, A1 and A2: These are the pins that are used to set the I2C address. This is most important when using multiple expanders on a single I2C connection as each device must have a unique address. Each pin can either be pulled low (GND) or high (Vcc), meaning there is a total of eight addresses that can be specified:
By default, the address is all three pins pulled to ground for a hex address of 0x20. But as you can see from the chart above, if pin A0 is pulled high (to 3.3V or 5V depending on what voltage you are using to power the chip), the I2C address can be changed to 0x21. When we get to the ESPHome configuration, it is this address that we will use to specify which expander (and thereby which pin) with which we are interacting.
Since eight different addresses are available, eight expanders can be connecting to just those same to SCL/SDA pins on the ESP board, providing 16 x 8 or a total of 128 additional I/O pins!
RESET: If this pin is pulled to ground, it will reset and reinitialize the board and all the output pins. For normal operation, you will connect this pin to your positive operating voltage (or high). You could optionally connect this to a different pin on the ESP board and reset the I/O expander by pulling this pin to low in your code.
A0-A7 and B0-B7: These are the general purpose digital input or output pins. They are provided in two "banks" A and B for the purpose of the interrupt pins. But otherwise, all pins are identical and can be used for input or output. Remember that each individual pin, when configure for output, can only handle a maximum of 25 mA.
INT A and INT B: These are the interrupt pins. Each will activate when any of the pins in their corresponding "bank" (A or B) changes state. They can be configured to be active high or active low... and can be OR'd together. Note that each pin state change is also reported back on the I2C bus, but if you have a special need, you can connect these pins back to the ESP board and take action when any of the pins change state. I won't be using these pins in my project as I'll get (or set) state changes via the I2C interface.
Connecting Multiple I/O Port Expanders
As mentioned above, if 16 additional I/O ports aren't enough, you can connect a total of eight expanders to the same two I2C pins of the ESP board.
Each connected port expander must have a unique address by pulling the A0-A3 pins either high or low as described above. Each of the individual I/O pins will be seen and can be used just like a regular digital GPIO pin on the ESP board.
Power Considerations and Connections
As mentioned, the total current draw of all outputs on a single MCP23017 is limited to 125 mA. But when using more than one port expander, it is easily possible to exceed the current draw available on the ESP board's 3.3V pin. If you don't want to use multiple power supplies, then you can use a single 5V power supply and a buck converter to create a separate 3.3V bus for all the power and address connections to the MCP23017 ICs.
Click to enlarge |
You can use any appropriate buck converter, variable or fixed, that can step down the 5V supply to the 3.3V for all the necessary ground and power connections of the MCP23017 chips. The buck converter I am using is rated for 3A. If all eight expanders are used at maximum rated current (125 mA x 8), that is only a total of 1A. Too much for the ESP 3.3V pin, but well within the rated capacity of the buck converter.
Creating a Simple Bench Test
To test out the MCP23017 and the associated ESPHome code, I created a breadboard test that has 5 buttons as inputs and 5 LEDs as outputs. In this simple example, each button will toggle the corresponding LED off or on.
I'm not using a buck converter in this case, as I am using my DIY Benchtop Power Supply from an Old PC because it can output up to three different voltages simultaneously.
So I will create a 5V bus for powering the ESP and a 3.3V bus for powering the MCP23017. When powering by a single 5V power source, then the buck converter would be needed for the 3.3V bus.
Click to enlarge |
This is the wiring diagram for the breadboard image above. The MCP23017 is wired as covered above. Note that the three address pins are all connected to ground, so the I2C address for the chip will be 0x20. Five LEDs are connected to B0 through B5 and five normally-open push buttons are connected to A3 through A7. The selection of these pins are arbitrary. I just used the topmost pins on each "bank".
Also note that six of the I/O pins are unused, meaning six more inputs or outputs could be connected.
Each push button simply toggles the corresponding color LED off or on. But to make all this work, we need some code on our ESP controller. Naturally, you could use Arduino/C++, Micropython or other compatible firmware, but in this case I'm going to use ESPHome, which means all my inputs and outputs will automatically integrate into Home Assistant as well.
ESPHome
ESPHome has native support for the MCP230xx series of chips. But how do you refer to the individual pins of the MCP23017 when it is only connected to two pins on the ESP board? From the ESPHome web site:
"Once configured, you can use any of the 16 pins as pins for your projects. Within ESPHome they emulate a real internal GPIO pin and can therefore be used with many of ESPHome’s components such as the GPIO binary sensor or GPIO switch.
GPIO pins in the datasheet are labelled A0 to A7 and B0 to B7, these are mapped consecutively in this component to numbers from 0 to 15."
Translating the MCP23017 pins to the corresponding ESPHome pins, it looks like this for each MCP23017 connected:
# Define MCP23017
mcp23017:
- id: 'mcp23017_01'
address: 0x20
The declaration includes a unique ID that will be referred to when using the individual pins, along with the hex address defined by how the address pins on the MCP23017 have been connected.
If you have more than one MCP23017 connected to the same two I2C pins on your ESP board, you just add each one with a unique ID and its address:
- id: 'mcp23017_02'
address: 0x21
- id: 'mcp23017_03'
address: 0x22
You can now define each output as a switch and each input as a binary sensor, referring to the MCP23017's ID and pins 0 - 15 as shown in the diagram above:
# Define Outputs (switches / LEDs)
switch:
- platform: gpio
name: "Red LED"
id: red_led
pin:
mcp23xxx: mcp23017_01
# Use pin B0
number: 8
mode:
output: true
- platform: gpio
name: "White LED"
id: white_led
pin:
mcp23xxx: mcp23017_01
# Use pin B1
number: 9
mode:
output: true
This would continue for the rest of the outputs. If using more than one MCP23017, you would just indicate which one is being used by changing the MCP23xxx: line to indicate which board, such as mcp23017_02. The same pin numbers would then be used for this board.
When the ESPHome device is integrated into Home Assistant, you would have a switch for each defined output.
Devices connected as inputs are declared as binary sensors in ESPHome:
# Define Inputs (Binary Sensors/Push buttons)
binary_sensor:
- platform: gpio
name: "Red Button"
id: red_button
pin:
mcp23xxx: mcp23017_01
# Use pin A7
number: 7
mode:
input: true
pullup: true
inverted: true
on_press:
then:
- switch.toggle: red_led
- platform: gpio
name: "White Button"
id: white_button
pin:
mcp23xxx: mcp23017_01
# Use pin A6
number: 6
mode:
input: true
pullup: true
inverted: true
on_press:
then:
- switch.toggle: white_led
For my push buttons, I'm adding a simple automation that toggles the on/off state of the corresponding LED via the on_press: event. Note that this is all occurring on the ESP board without any Home Assistant integration. Of course you could also use Home Assistant automations to create the same process.
The above YAML is just a portion of the code used. To see the full ESPHome YAML code for the breadboard test, see this Github Gist File.
Part 2 - Creating the Analog Clock
Using the above process, I'm going to use five separate MCP23107 chips connected to a single ESP8266 for 72 outputs and 3 inputs... a whopping 75 I/O devices from just two D1 Mini pins! Please note:
This is not the best way to build a clock! There are many better options and other more efficient ways. The primary purpose for this clock is to demonstrate the use of the I/O port expander. As you will see, this version (especially the breadboard version shown) is simply impractical... if for no other reason than the size of the controller and number of wires involved. For this reason, I'm only providing highlights of the build to serve as inspiration for your own projects.
Background
A few months ago, I was browsing a local hobby store and ran across this wooden clock face with the numbers carved out. It was only a couple of bucks so I bought it and put it on a shelf for a "future project".
One MCP23017 will be used for each fifteen minute segment... so four of those with 15 digital outputs each. Then a fifth MCP23017 will be used for the 12 LEDs to indicate the hour. I'll also add three push buttons as inputs to this last expander, for the total of 75 I/O pins all connected back to just two GPIO pins on my ESP8266 controller.
Parts List
As with all my projects, I like to provide a complete parts list. As always, many items can be substituted and some can simply be omitted depending upon the goals of your own build. This is for the breadboard version. If and when I create a PCB version, I will update this article, including the parts list, for that version.
QTY |
ITEM |
NOTES |
1 |
|
|
72 |
Colors of your choice |
|
- |
|
|
1 |
In lieu of benchtop
supply |
|
1 |
In lieu of benchtop
supply |
|
1 |
Wemos D1 Mini (ESP8266) |
|
5 |
May be able to source
cheaper with longer delivery time. |
|
1 |
|
|
- |
Mostly male-to-female.
More than one kit may be needed. |
|
72 |
Values will depend on
LEDs used. |
|
- |
Optional |
|
|
|
|
Other optional parts and tools used:
QTY |
ITEM |
NOTES |
|
|
|
|
|
|
|
Spray paint, stain or
other finish |
Source locally |
|
(lots and lots of hot
glue!) |
|
|
Optional and not
really part of the project, but I included it in mine. |
|
|
Not an exact match
for the old model I have, but this is close |
|
|
|
|
Some of these links may be Amazon affiliate links. Use of these links will not affect your pricing, but as an affiliate this blog may earn a small commission if you make a purchase.
Building the Clock Face
To create the layout of for the minute LED ring, I simply used a compass to create a circle. I then used a digital angle finder to first create a vertical and horizontal line through the center of the clock. These lines were then used to make marks every 6°. This gives 60 marks for the full circle and an LEDs will be installed at each mark.
Holes were drilled using these marks as guides. The hole was sized to just accommodate a 3 mm LED so that the very top edge of the LED would just slightly protrude from the front face. I believe I used a 9/64" or 5/32" bit, but you may want to test with a scrap piece of wood.
I added an ILI9341 display to the clock as well, so cut out the opening for that with a jigsaw and 3D printed a trim piece. This will be used to show the day and date, or possibly temperature or other information.
Note that the ILI9341 I used has its own ESP32 controller and is mounted on a custom PCB, so it really isn't part of this clock project using the port expanders. Therefore, I won't be covering it any further in this article. But I do have another YouTube video and blog article where I incorporated the ILI9341 into my DIY amp, so you can check those out if you want to know more about the display and how to use it with ESPHome.
I then gave the clock face a quick paint job. I selected a semi-gloss black, but you could use any color, stain or even leave it natural. Once dry, I installed the LEDs for the minutes.
Mock up of the initial planned clock design (not an actual photo!) |
I opted to use red LEDs for the minutes, substituting a yellow LED at each 5 minute mark. And I used green for the hours (more on the hour LEDs in a minute). Of course you can use any color you like, but do note that the color selected may impact the size resistor needed (as the forward voltage of different color LEDs varies), and recall that we need to limit the total current draw on each MCP23017 chip to no more than 125 mA. I'll talk about the resistors below.
Click to enlarge |
After installing the LEDs into the drilled out holes, I used some bare 24-gauge copper wire to create a "ground loop". All ground legs from the LEDs were then soldered to this ring. This means that I will only need to run a single ground wire back to the controller for all the LEDs instead of a separate ground wire for each one. I also attached the ILI9341 display to the back using small screws. Once I had each LED positioned so that the top just protruded from the front (I could just barely feel the 'bump' for each LED when running my finger across the front of the clock base), I added a small dab of hot glue to keep the LED in place.
For the hour LEDs, I wanted to prevent the LED light from spilling over from one number to the next.
I 3D printed some small enclosures that would just cover each number and hot glued the LED inside this enclosure.
Each of the 12 hour LEDs and covers were then hot glued into place. The ground connections for these 12 LEDs were also connected to the 'ground loop' wire. I also added the green wire seen above that will serve as the single ground connection back to the controller. I also 3D printed a stand to hold the clock face upright while I wired it.
Building the Clock Controller
As I mentioned above, I will be building this on a protoboard. This will make it very unwieldy with a LOT of wires. But after I determine it is working according to plan, the goal will be to build a custom PCB that will fit on the back of the clock and eliminate most of the wiring.
Power Connections
As covered above, I'll be using my benchtop power supply to provide both 5V and 3.3V to my breadboard. In lieu of a dual output supply, you can use a single 5V power supply (at least 5A, but 10A recommended) and a 5V to 3.3V buck converter to supply the 3.3V to the MCP23017 chips.
Click to enlarge |
I created a single 5V rail. This will only be used to power the Wemos D1 Mini (ESP8266). The other three 3.3V rails will provide all the necessary connections for the MCP23017 expanders.
As in the pinout covered above, each expander needs power and ground to the Vdd and Vss pins. The reset pin needs to be pulled high and the 3 address pins (A0-A2) set to ground or pulled high to 3.3V to set each expander's I2C address (each individual expander must have a different I2C address).
I2C Data Connections
Click to enlarge |
Next are all the I2C data connections. From the I2C data pins on the Wemos D1 Mini (D1 for SCL and D1 for SDA ), connections are made to the SCL and SDA pins on each chip. These are the yellow wires (SCL) and blue wires (SDA) in the above image. You can make these connections in serial or parallel fashion, or a combination of both as I have done.
At this point, you now have a total of 80 input and output pins available!
I/O Connections
Click to enlarge |
Now's where it starts to get a little crowded! Connections need to be made for each output to the individual LEDs on the clock face. Note that I ended up labeling each expander with the corresponding I2C hex address to help me keep things straight.
The top four expanders (numbered 22 through 24) will each drive 15 LEDs for the minute segments. To make the programming easier, I used the A1-A7 and B0-B7 pins (skipping A0). This is because those will map to pins 1-15 in ESPHome (or Arduino/C++), covering the minutes 1-15 in each segment.
The other port expander (20) is used to control the 12 LEDs for hours. Again, A0 was skipped so that the pin number (1-12) in code align with the LED behind the numbers 1-12. Pins B15-B17 are used for the three push buttons. These will be used to do things like reboot the clock, sync the time and turn the clock display off/on if I want to temporarily darken the clock.
Resistor Selection
Obviously each LED needs a resistor. Normally this is to limit the current for the LED itself. But in this case, I also needed to limit the current further so that 15 total LEDs would not exceed the maximum current draw of 125 mA on any single port expander.
While it is easy to calculate the theoretical value of the resistor using Ohm's law, I went a step further and actually measured the current draw using different resistors. If the maximum current is 125 mA and there are 15 LEDs, the max per LED is 8.3 mA. I wanted the maximum brightness while still staying under the max value.
So in addition to calculating theoretical values (not exact because an LED has a Vf range), I actually measured the current draw of the different colored LEDs at 3.3V using different resistors. I settled on 180 Ω resistors for the minute LEDs and 56 Ω for the green hour LEDs (there are only 12 of those so I could reduce the resistance a bit to get a brighter light).
Connecting the Controller to the Clock Face
Now the real fun began. I had to connect 72 pins from the 5 port expanders on the breadboard to the 72 LED leads on the clock face. To make matters more complicated, the pin numbers for ESPHome (and the Arduino library) run in opposite directions on each side of the port expander.
It was easy to get some of the wires reversed or in the wrong order (which I did multiple times). In fact, I had to write a quick little Arduino app to install on the D1 Mini that would just light up all the LEDs in order so I could test things as I connected them.
(This is after everything was finally wired correctly)
Since this was just a breadboard test, I opted to use Dupont jumpers for these connections. And the Dupont connectors don't fit tightly onto the thin wires from the LEDs. So as I started adding more wires, I'd knock others loose. I had to resort to doing eight LEDs at a time (one side of a port expander), testing that the order and pin connections were correct, then adding a little hot glue to hold the Dupont connector in place.
Click to enlarge... or maybe don't! |
And here's the mess that is the final wiring! It does work, but as I mentioned, it is completely impractical for regular use... unless your name is Rube Goldberg and you designed a clock!
To make this clock actually useful, the breadboard and all the wiring would need to be migrated to a custom designed PCB that would fit on the back of the clock. But that's a project for another day!
Using Arduino/C++ Code
I covered the basics of using ESPHome for control of the inputs and outputs (as binary sensors and switches). Using Arduino/C++ code is actually very similar.
Using the Adafruit-MCP23017-Arduino-Library, you simply declare each board you are using.
//Define MCP boards
Adafruit_MCP23X17 mcp_hour; //0x20
Adafruit_MCP23X17 mcp_0115; //0x21
Adafruit_MCP23X17 mcp_1630; //0x22
Adafruit_MCP23X17 mcp_3145; //0x23
Adafruit_MCP23X17 mcp_4660; //0x24
I used the name mcp_hour for the port expander controlling the hour LEDs and then the minute segment numbers for each of the other four. So for example, the second board listed controls the minute LEDs for minutes 1-15, the third board minutes 16-30, etc.
In the setup section of the code, you start each board using the I2C address for that board.
void setup() {
...
byte i;
//Initialize all MCP Minute boards
mcp_hour.begin_I2C( 0x20 ); //hour digits
mcp_0115.begin_I2C( 0x21 ); //minutes 1-15
mcp_1630.begin_I2C( 0x22 ); //minutes 16-30
mcp_3145.begin_I2C( 0x23 ); //minutes 31-45
mcp_4660.begin_I2C( 0x24 ); //minutes 46-60
//for minute boards, set pins 1-15 for output
for (i = 1; i < 16; ++i){
mcp_0115.pinMode(i, OUTPUT);
mcp_1630.pinMode(i, OUTPUT);
mcp_3145.pinMode(i, OUTPUT);
mcp_4660.pinMode(i, OUTPUT);
}
//for hour board, set pins 1-12 for output
for (i = 1; i < 13; ++i) {
mcp_hour.pinMode(i, OUTPUT);
}
//hour board has three input buttons
mcp_hour.pinMode(RED_BUTTON, INPUT_PULLUP);
mcp_hour.pinMode(GREEN_BUTTON, INPUT_PULLUP);
mcp_hour.pinMode(BLUE_BUTTON, INPUT_PULLUP);
...
}
The nice option (vs. ESPHome) is that if you kept your LEDs and outputs in sequential order, you can just run for loops to set the pins to input or output. This is also handy for turning all the lights off or on in sequence, as I did for the LED testing shown in the above video clip.
void testLEDS() {
byte i;
//Test Loop - Test each segment - Lights on
// Segment 1
for (i = 1; i < 16; i++) {
mcp_0115.digitalWrite(i, HIGH);
delay(50);
}
// Segment 2
for (i = 1; i < 16; i++) {
mcp_1630.digitalWrite(i, HIGH);
delay(50);
}
// Segment 3
for (i = 1; i < 16; i++) {
mcp_3145.digitalWrite(i, HIGH);
delay(50);
}
// Segment 4
for (i = 1; i < 16; i++) {
mcp_4660.digitalWrite(i, HIGH);
delay(50);
}
...
}
I then did the same thing to turn them back off by setting each pin to LOW, followed by the same process for the hour LEDs. Basically, once your pins are defined, you can use them like any other digital I/O pin in your code.
While a lot of other code is needed for setting and keeping the time, and providing routines for the three button presses (currently I have one button that runs the LED test and another that turns all the LEDs off), covering that is beyond the scope of this article. To be honest, I haven't even finished writing the code myself and will probably wait until I migrate the "Franken-Clock" over to a version using a PCB. My plan is to use MQTT to integrate the controller into Home Assistant for getting and syncing the time (although you could also use an NTP time server for that function), along with other features. If and when the time comes and I finish the code, I'll update this article and share the code on Github.
Conclusions
While the clock in its current state isn't really practical, hopefully the build and sample code provides you with enough information to use the MCP23017 port expanders in your own projects where you need more digital I/O pins than provided on a standard ESP8266 or ESP32 board.
Let me know down in the comments if you've used port expanders before and how you used them in your own projects.
And thanks for reading!
Additional Info and Links:
If you'd like to support future content on this blog and the related YouTube channel, or just say thanks for something that helped you out, you can say thanks by buying me a one-off cup of coffee at:
No comments:
Post a Comment
To help eliminate spam and to keep all conversations civil, submitted comments are moderated. Therefore your post may not show up immediately. Please be patient as most reviews are completed within a few hours.