Skip to content

Managing Multiple Displays

Microesque edited this page Dec 16, 2024 · 5 revisions

Overview


General Considerations

Since this is an object-oriented library, setting up multiple displays is as simple as initializing multiple display objects. There are some points to consider however:

  • Unfortunately, the SSD1306 driver only supports two possible addresses (either 0x3C or 0x3D). Meaning, you can have at most two independent displays per I2C bus.
  • If you want to show the same content on multiple displays you have two options:
    • For displays with the same addresses, initializing a single display and connecting them on the same I2C bus is all you need.
    • For displays with different addresses, initialize two displays for the two addresses using the same buffer array. You'll need to update both displays after drawing on one.
  • If you want independent buffer contents for each display, define a separate buffer array for each one. However, if memory is limited, you have the option to share a single buffer across multiple displays. The display and getter/setter functions will operate independently, even with a shared buffer. However, the draw functions will affect all displays sharing the same buffer. To display different contents on sharing displays, you'll need to clear and redraw the entire buffer before every update.

Same I2C Bus with Different Addresses

To use multiple displays with different addresses on the same I2C bus, simply initialize the displays with the same i2c_write() implementation:

/* Your i2c_write() implementation... */

static struct ssd1306_display display1;  /* 128x64 - I2C address 0x3C */
static struct ssd1306_display display2;  /* 128x64 - I2C address 0x3D */

/* display1 initialization */
static uint8_t display1_array[SSD1306_ARRAY_SIZE_64];
ssd1306_init(&display1,
             0x3C,
             SSD1306_DISPLAY_TYPE_64,
             display1_array,
             i2c_write);

/* display2 initialization */
static uint8_t display2_array[SSD1306_ARRAY_SIZE_64];
ssd1306_init(&display2,
             0x3D,
             SSD1306_DISPLAY_TYPE_64,
             display2_array,
             i2c_write);

Multiple I2C Buses

To use multiple displays with different I2C buses, simply define separate i2c_write() functions for each I2C hardware:

/* Your i2c1_write() implementation for the I2C1 module */
/* Your i2c2_write() implementation for the I2C2 module */

static struct ssd1306_display display1;  /* 128x64 - I2C address 0x3C */
static struct ssd1306_display display2;  /* 128x64 - I2C address 0x3C */

/* display1 initialization (connected to I2C1) */
static uint8_t display1_array[SSD1306_ARRAY_SIZE_64];
ssd1306_init(&display1,
             0x3C,
             SSD1306_DISPLAY_TYPE_64,
             display1_array,
             i2c1_write);

/* display2 initialization (connected to I2C2) */
static uint8_t display2_array[SSD1306_ARRAY_SIZE_64];
ssd1306_init(&display2,
             0x3C,
             SSD1306_DISPLAY_TYPE_64,
             display2_array,
             i2c2_write);

I2C Multiplexers (e.g., TCA9548A)

The simplest way to overcome the "maximum two independent displays per I2C bus" limit is by using an I2C multiplexer. An I2C multiplexer sits between the master and slave devices, routing communication from the source I2C bus to its multiple I2C channels.

Below is an example of managing four displays independently with a single I2C bus. The example uses an STM32 microcontroller with a popular TCA9548A I2C multiplexer breakout board.

TCA9548A Overview

The pin descriptions for TCA9548A are:

Pin Description
VCC Supply voltage
GND Ground
SDA Serial data source
SCL Serial clock source
RESET Active-low reset input
Ax Address inputs
SDx Serial data channels [0-7]
SCx Serial clock channels [0-7]
img

The working principle of TCA9548A is straightforward. It is an I2C-controlled device with a single 8-bit register. Each bit in the register corresponds to the selected or deselected state of one of its eight multiplexed I2C channels. Selecting a channel directs all bidirectional serial communication from the source to that channel.

The diagrams from the datasheet illustrate the complete I2C transaction required to select and deselect channels:

img

Circuit Diagram

img

Notes:

  • TCA9548A breakout boards usually come with pull-up/pull-down resistors on some of the pins, but we'll connect all lines accordingly anyways for demonstration purposes
  • The STM32 micro has internal pull-ups for its SDA and SCL lines; hence no external resistors are required.
  • All Ax pins of the TCA9548A are tied to ground; hence the resulting 7-bit address for it is 0x70.
  • Display-1 and Display-2 are connected to channel 7 of the TCA9548A, while Display-3 and Display-4 are connected to channel 6.

Source Code

/* Other includes... */

#include "ssd1306.h"

void i2c_write_c7(uint8_t *data, uint16_t length) {
    /* Select TCA9548A channel 7 */
    uint8_t channel = (1 << 7);
    HAL_I2C_Master_Transmit(&hi2c1, (0x70 << 1), &channel, 1, 1000);

    /* Send display commands (first byte is 8-bit write address) */
    uint8_t address = *data;
    HAL_I2C_Master_Transmit(&hi2c1, address, ++data, --length, 1000);
}

void i2c_write_c6(uint8_t *data, uint16_t length) {
    /* Select TCA9548A channel 6 */
    uint8_t channel = (1 << 6);
    HAL_I2C_Master_Transmit(&hi2c1, (0x70 << 1), &channel, 1, 1000);

    /* Send display commands (first byte is 8-bit write address) */
    uint8_t address = *data;
    HAL_I2C_Master_Transmit(&hi2c1, address, ++data, --length, 1000);
}

int main(void) {
    /* System setup... */

    /* Display-1 */
	static struct ssd1306_display display1;
	static uint8_t display1_array[SSD1306_ARRAY_SIZE_64];
	ssd1306_init(&display1,
                 0x3C,
                 SSD1306_DISPLAY_TYPE_64,
                 display1_array,
                 i2c_write_c7);

    /* Display-2 */
	static struct ssd1306_display display2;
	static uint8_t display2_array[SSD1306_ARRAY_SIZE_64];
	ssd1306_init(&display2,
                 0x3D,
                 SSD1306_DISPLAY_TYPE_64,
                 display2_array,
                 i2c_write_c7);

    /* Display-3 */
	static struct ssd1306_display display3;
	static uint8_t display3_array[SSD1306_ARRAY_SIZE_64];
	ssd1306_init(&display3,
                 0x3C,
                 SSD1306_DISPLAY_TYPE_64,
                 display3_array,
                 i2c_write_c6);

    /* Display-4 */
	static struct ssd1306_display display4;
	static uint8_t display4_array[SSD1306_ARRAY_SIZE_64];
	ssd1306_init(&display4,
                 0x3D,
                 SSD1306_DISPLAY_TYPE_64,
                 display4_array,
                 i2c_write_c6);

    /* Draw functions... */
}

Note: The code works by selecting the appropriate TCA9548A channel for each display before sending the actual I2C packages. This can be accomplished by creating a separate i2c_write() function for each of the I2C channels.

  • i2c_write_c7() is for displays that connect to channel 7.
  • i2c_write_c6() is for displays that connect to channel 6.