Table of Contents

Charlieplexing LEDs on the ESP32

The ESP32 has a stack of GPIO pins allowing you to attach a whole bunch of LEDs. But it's a neat practice to try using fewer pins with several LEDs using Charlieplexing. There's quite a few things you can learn along the way.

For all experiments in this article, I'll be using pins 21, 22 and 23 on the ESP32.

Going back to basics, a simple single LED on a GPIO pin would be like this:

And the code in Arduino IDE to flash the LED from pin 23 would be something like:

#define LED_PIN 23
 
void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);
}
 
void loop() {
  digitalWrite(LED_PIN, !digitalRead(LED_PIN));
  delay(500);  
}

This code makes pin 23 an output pin, and then sets it to HIGH which means to supply voltage from the pin to the resistor and the LED.

This is all probably fairly elementary but it's good to have this running to ensure the basics are set up on your ESP32 and breadboard.

Two LEDs

Let's now move to our first attempt at Charlieplexing, and we'll begin with two pins to drive two LEDs. This might sound a bit silly, and not much better than driving two LEDs on two pins the regular way, but it will set the groundwork for planning more pins, and a greater number of LEDs.

Firstly, the simple schematic for two Charlieplexed LEDs:

Although not pictured, both pins reduce the voltage to the LEDs using a resistor each. If we set the pin 22's mode to OUTPUT and set the pin itself to HIGH, voltage will be supplied from the pin. If pin 23's mode is also set to OUTPUT, but the pin is set to LOW, this is effectively making the pin become GND, providing a voltage potential drop, meaning that voltage will flow from pin 22, through the first LED and into pin 23.

This lights the first LED. LEDs being a diode means that the current cannot flow through the second LED.

This concept of the pins being OUTPUT pins, one being HIGH and the other LOW is one of the important principles of Charlieplexing.

If pin voltages are swapped, pin 22 becoming LOW and pin 23 becoming HIGH, the reverse situation is created. Current will flow the other way around and light the second LED, but not able to pass through the first LED.

Let's build this onto the breadboard. The wiring is fairly simple so the breadboard layout is:

The following code would allow them to alternate:

#define LED_PIN_A 22
#define LED_PIN_B 23
 
#define DELAY 400 
 
void led_1(){
  digitalWrite(LED_PIN_A, HIGH);
  digitalWrite(LED_PIN_B, LOW);
}
 
void led_2(){
  digitalWrite(LED_PIN_A, LOW);
  digitalWrite(LED_PIN_B, HIGH);
}
 
void setup() {
  pinMode(LED_PIN_A, OUTPUT);
  pinMode(LED_PIN_B, OUTPUT);
}
 
void loop() {
  led_1();
  delay(DELAY);
  led_2();
  delay(DELAY);    
}

Don't worry about the efficiency of the code examples in this article. They are verbose to make everything as clear as possible, not intended to be optimised.

Send the code to the ESP32 and watch the LEDs swap back and forth. Very neat. One of the properties of Charlieplexing is that in truth, only one LED can be actually on at a time. But there is a trick to work around that. Using Pulse Width Modulation, in addition to switching pin configurations really quickly, you can make all LEDs appear to be on at the same time.

We can reproduce a crude form of PWM by simply changing the delay value in our code example to around 2:

#define DELAY 2

Send this updated code to the ESP32 and both LEDs should show as lit with no flicker. You can probably get away with a delay of 3 or 4 milliseconds before any perceptible flicker. If in doubt, always borrow a small child (preferably your own) and see what flicker threshold kid's younger eyes can perceive. Kids can provide great Quality Assurance.

Six LEDs

I know two LEDs doesn't seem worth it, but it's good to get a feel for the underlying behaviour.

Ok so what about Charlieplexing six LEDs from three pins?

This takes a little more planning (but a very worthwhile exercise).

Firstly, let's expand the original schematic to take in six LEDs using three pins. Immediately more complex, right? We can work around this, don't be put off.

Notice there are now two groups of two LEDs, plus two more bridging across at the bottom. This arrangement lets us use lots of pin mode combinations to get each LED to light up on it's own.

We'll trace the schematic to see what is needed to light LED1. If we set pin 21 to be OUTPUT and HIGH to supply voltage, and on pin 22, we set to OUTPUT and LOW to act as GND, then we can be pretty sure that current will happily flow on that path and LED1 will light up.

But what about pin 23? What's happening with this one? What do we set this to? We don't really want this pin to supply any voltage. Do we want it to be GND too?

Well, no. Don't forget that a GPIO pin can also have a mode of INPUT which is high impedance. This is a fancy way of saying that INPUT on a pin has a really high resistance on it, way too much for our voltage to push any current to. And that effectively makes the pin act as like it is switched off for our purposes.

So the basic idea is this: one pin will be OUTPUT and HIGH to supply voltage, one will be OUTPUT and LOW to act as a GND, and a third pin will be disconnected. That's clever, right?

That's the second principle of Charlieplexing: you can turn pins off by setting them as INPUT.

We would configure lighting LED1 like this:

In that pin configuration, current takes a path that would definitely pass though LED1.

Great! But hang on a second. There is also a current path that can travel though LED5, and then though LED4, and out to pin 22 ground.

Won't all three LEDs be lit? Well, it's true that LED1 and LED5 are in parallel, so more current could be drawn to power both. But LED5 and LED4 are in series. Therefore the forward voltage is too great for our supply. For two 1.8V LEDs, there is not enough voltage (plus resistors) to power both LEDs if only 3.3V is being output. 3.3V < 3.6V.

Therefore, there is only enough voltage to power LED1.

What about powering LED4? How do we determine the INPUTs and OUTPUTs here? We'll trace the schematic again:

Voltage should be supplied on pin 23 as OUTPUT and HIGH, and pin 22 should be OUTPUT and LOW to provide ground. That will give the most obvious current path.

Current could potentially flow in parallel through LED6 and light that at the same time. But we don't want that, so we can switch off that current path by setting pin 21 to INPUT.

If we add these two configurations to a table so far, we would have:

LED Pin 21 Pin 22 Pin 23
LED1 OUTPUT HIGH OUTPUT LOW INPUT
LED4 INPUT OUTPUT LOW OUTPUT HIGH

One more example. What about LED5?

Pin 21 would supply to the voltage with OUTPUT and HIGH. Pin 23 would act as ground with OUTPUT and LOW. This would result in two LEDs: 1 and 5 potentially being lit. Therefore, pin 22 should be disabled by setting the mode to INPUT, leaving only LED5 lit.

LED5 in the table would be:

LED Pin 21 Pin 22 Pin 23
LED5 OUTPUT HIGH INPUT OUTPUT LOW

By using the same method to trace all the other combinations through the schematic, all our configurations would result as:

LED Pin 21 Pin 22 Pin 23
LED1 OUTPUT HIGH OUTPUT LOW INPUT
LED2 OUTPUT LOW OUTPUT HIGH INPUT
LED3 INPUT OUTPUT HIGH OUTPUT LOW
LED4 INPUT OUTPUT LOW OUTPUT HIGH
LED5 OUTPUT HIGH INPUT OUTPUT LOW
LED6 OUTPUT LOW INPUT OUTPUT HIGH

Now it is time to breadboard the schematic. This can be tricky. I like to take the existing schematic and stretch it out to be as similar as possible to how it will be on a breadboard. I used KiCad to move the existing schematic to be laid out like this:

Which made it easier to plan out the wiring on the breadboard, and less likely to get lost. The breadboard wiring ends up being like this:

If you wire up the six LEDs onto the breadboard as pictured above, then we're ready to do the code. Again, the code will not be optimised, but rather, fully expanded to make it easy to follow:

#define LED_PIN_A 21
#define LED_PIN_B 22
#define LED_PIN_C 23
 
#define DELAY 2 // two to three ms between each LED in a set of six reduces flicker 
 
/** 
 * See the table of pin configurations above
 * for each LED function in this program.
 */
 
void led_1(){
  pinMode(LED_PIN_A, OUTPUT);
  pinMode(LED_PIN_B, OUTPUT);
  pinMode(LED_PIN_C, INPUT);
  digitalWrite(LED_PIN_A, HIGH);
  digitalWrite(LED_PIN_B, LOW);
  digitalWrite(LED_PIN_C, LOW);
}
 
void led_2(){
  pinMode(LED_PIN_A, OUTPUT);
  pinMode(LED_PIN_B, OUTPUT);
  pinMode(LED_PIN_C, INPUT);
  digitalWrite(LED_PIN_A, LOW);
  digitalWrite(LED_PIN_B, HIGH);
  digitalWrite(LED_PIN_C, LOW);
}
 
void led_3(){
  pinMode(LED_PIN_A, INPUT);
  pinMode(LED_PIN_B, OUTPUT);
  pinMode(LED_PIN_C, OUTPUT);
  digitalWrite(LED_PIN_A, LOW);
  digitalWrite(LED_PIN_B, HIGH);
  digitalWrite(LED_PIN_C, LOW);
}
 
void led_4(){
  pinMode(LED_PIN_A, INPUT);
  pinMode(LED_PIN_B, OUTPUT);
  pinMode(LED_PIN_C, OUTPUT);
  digitalWrite(LED_PIN_A, LOW);
  digitalWrite(LED_PIN_B, LOW);
  digitalWrite(LED_PIN_C, HIGH);
}
 
void led_5(){
  pinMode(LED_PIN_A, OUTPUT);
  pinMode(LED_PIN_B, INPUT);
  pinMode(LED_PIN_C, OUTPUT);
  digitalWrite(LED_PIN_A, HIGH);
  digitalWrite(LED_PIN_B, LOW);
  digitalWrite(LED_PIN_C, LOW);
}
 
void led_6(){
  pinMode(LED_PIN_A, OUTPUT);
  pinMode(LED_PIN_B, INPUT);
  pinMode(LED_PIN_C, OUTPUT);
  digitalWrite(LED_PIN_A, LOW);
  digitalWrite(LED_PIN_B, LOW);
  digitalWrite(LED_PIN_C, HIGH);
}
 
void setup() {
 
}
 
void loop() {
  led_1();
  delay(DELAY);
  led_2();
  delay(DELAY);
  led_3();
  delay(DELAY);
  led_4();
  delay(DELAY);
  led_5();
  delay(DELAY);
  led_6();
  delay(DELAY);
}

Send the code to the ESP32 and all the LEDs will appear lit at once. Of course, this is just a trick of the eye just like we did with the two-pin Charlieplexing. Increase the delay to clearly see what's really going on:

#define DELAY 500

And that's pretty much how it's all done. Of course, you can code your program so that you can show one, two, three or more LEDs at the same time, and switch them around in whatever fashion you like.

More than six LEDs?

Yep you can have more LEDs with more pins. There is a formula that helps you work out how many LEDs per pins you can have which is:

(n2)-n

You know that two pins gives two LEDs, three pins gives six LEDs. This chart shows how many more beyond this:

Number of Pins Number of LEDs
2 2
3 6
4 12
5 20
6 30
7 43

In reality, there are current limitations to allow all this to happen, and the limits of speed switching the various pins.

Hope that helps you work through Charlieplexing on ESP32. Let me know if it helped and send me a pic of your breadboard. I'll post it on this page.

Here's my lovely effort cramming everything on a small breadboard and using a 10-LED block: