Controlling lots of OLED displays with a few GPIO pins

On my mission to learn as much as possible about "IoT" and all of the accessories/sensors/displays that are popular in the market, I came across some projects which were using multiple OLED displays to form dashboards and control panels. I hadn't thought about this usage, but it makes sense considering how inexpensive (and small) they are. The challenge presented with this idea is that the inexpensive hobby parts either have a fixed I2C address (usually 0x3C) or a single jumper to select an alternate address (usually 0x3D).

The photo above shows a collection of readily available and inexpensive I2C OLED displays. The top 3 have a single address selection jumper. The bottom 2 don't show any specific jumper for changing the address, although one or more of the SMD resistors might control it. Either way, most hobbyists are not prepared to work with tiny, surface mount resistors.

If they all are set to the same I2C address, then in order to get more than 1 display working from the same MCU, you'll need multiple I2C buses. I had already written code to bit bang I2C on GPIO lines, so it seemed like a relatively easy task to write code which could manage multiple I2C buses using multiple GPIO lines. Knowing how the I2C protocol works, another thought formed in my head: "Can multiple I2C buses coming from the same source share a single clock line?" The reason I thought it might be possible is because the I2C start signal requires both the clock and data lines to go low. If a common clock line went low, would that activate devices that you didn't intend to activate?

My first experiment was to try to control 4 OLED displays (all having the same I2C address) using only 5 GPIO lines (4 SDA + 1 SCL). I chose the Arduino Pro Micro (aka Leonardo) as a good board for this test.

As you can see in the image above, it works. The 4 displays are all sharing the same clock line and all are individually addressable with no interference issues. Then I realized that I had designed my Multi_OLED library by tying one display to each I2C bus. I took a second pass at the code and added a new parameter so that each display was specified with an address and bus number so that multiple displays per bus could be controlled.

In this photo, the top 2 displays are set to different addresses and share the same bus, while the 3 below are all on their own I2C buses. A unique backing buffer (1K of RAM) is needed for pixel operations. This is not viable on most AVR MCUs due to their tiny amount of onboard RAM, but other MCUs (e.g. ESP32, Cortex-M) have enough RAM to allocate multiple 1K buffers. For these MCUs, line and pixel functions are available. Here's a video of 2 displays running on an Adafruit nRF52840 Feather:

I published the code on Github here:

Multi_OLED library on Github

I thought it might be useful to use multiple I2C buses for things other than OLED displays, so I broke out the Multi_BitBang library separately:

Multi_BitBang library on Github

The functions I created to work with the displays are pretty simple. Begin by initializing your displays:

void Multi_OLEDInit(uint8_t *iBus, uint8_t *iAddr, uint8_t *iType, uint8_t *bFlip, uint8_t *bInvert, uint8_t iCount);

Each parameter is a list (one for each display you want to control)
iBus - I2C bus number (0 to the max bus you defined with the Multi_BitBang library)
iAddr - I2C address (e.g. 0x3C)
iType - OLED display type (enumerated values such as oled_128x32)
bFlip - A boolean indicating if the display should be flipped 180 degrees
bInvert - A boolean indicating if the display should enable inverted color mode
iCount - Total number of displays

I included a sample sketch which demonstrates how to drive multiple OLED displays here:

Multi_OLED sample sketch

One last note here about code I added to the Multi BitBang library to speed it up. The Arduino core library includes a standard way of manipulating GPIO pins. These functions (pinMode, digitalRead, digitalWrite) hide many implementation details which allow them to work the same way on all MCUs supported by the Arduino toolset. On AVR MCUs (e.g. Arduino Uno / aka ATmega328P), these functions are quite slow. They're slow because they do table lookups, bounds checking and try to place nice with interrupts. For the community at large, it normally doesn't matter. Blinking an LED or reading a pushbutton state doesn't require utmost speed, but generating digital signals for communication does. Using the Arduino GPIO functions, the Multi_OLED library is able to update the displays very slowly. There is a faster way, but it requires manipulating the port registers directly. I came up with a scheme that keeps the simplicity of the pin functions, but still gains the speed of direct port access. Each AVR chip has a unique port->pin mapping which converts the pin numbers into a port and bit. The image below shows the pinout of the ATmega32u4 (Pro Micro). Looking at digital pin 2, you can see that it's also labeled PD1. This means that pin 2 is actually bit 1 of Port D. Using my numbering scheme, you would access this pin by referencing it as pin 0xD1.
(image credit - Sparkfun Electronics)

My code doesn't need to use a table lookup, doesn't do much bounds checking and doesn't disable interrupts. It does allow you to use existing code and just change the pin numbers passed. With the simplified pin numbering scheme, the Multi_OLED library is able to drive the displays just as fast as normal I2C on AVR MCUs. Faster MCUs (e.g. ESP32) aren't slowed down as much by the extra overhead of using the original pin functions. To differentiate between the 2 situations, pin numbers less than 0xA0 will call the original (slow) pin functions, otherwise the fast functions will be used with the Port/Bit numbering scheme. I also separated this code into a stand alone library called "FastIO". Here it is:

FastIO library on Github


Popular posts from this blog

Fast SSD1306 OLED drawing with I2C bit banging

My adventures in writing an OTA bootloader for the ATmega128RFA1