PCB Friday: High, Low, Tri-State – Microcontroller Output Fundamentals

Microcontroller outputs are more nuanced than "on" and "off." Three states allow for versatile usage.

Jeremy Cook
11 days ago

If you were to describe a microcontroller development board (i.e. an "Arduino") to someone who understands only the most basic concepts in electronics, you might say it's like a computer that can turn physical things on and off in the real world. And that would be close enough for most people.

For the technically inclined (i.e. you since you're still reading), microcontroller outputs are more nuanced than ON/OFF, consisting instead of HIGH, LOW, and high-impedance (AKA, hi-Z, or tri-state). In other words, there are three output options for microcontrollers, not just the binary 1s and 0s that you might expect out of a computing system.

Typical microcontroller output operation

When a circuit is connected between a microcontroller, load, and ground, HIGH effectively acts as an on switch, causing a voltage differential that enables electrons to flow. LOW acts as off since it's at the same potential as ground. A circuit can also be configured with a common positive, where a voltage differential is produced when the microcontroller output goes low.

Whether hooked up to ground as shown above, or instead to positive, a tri-state/hi-Z microcontroller output acts as an off signal since electrons are blocked in either direction. However, hi-Z outputs aren't used that often, perhaps in part because this isn't as straightforward to implement in software. In the world of Arduino, setting a pin to INPUT is equivalent to setting it as tri-state. There's no explicit built-in tri-state function.

Why tri-state? Charlieplexing!

Practically speaking, HIGH is typically equivalent to on in the world of microcontrollers, while LOW works as off. Using a hi-Z setting, plus the addition of diodes and a technique called Charlieplexing, you can interact with more things than the number of IO pins used would indicate.

Or you can pull off the rather useless (but instructive) Charlieplexing example below, controlling two reversed-polarity LEDs with two microcontroller outputs and no external path to ground:

Code for alternating the two LEDs is shown below:

const int charlie1 = 2;
const int charlie2 = 3;
void setup() {
pinMode(charlie1, OUTPUT);
pinMode(charlie2, OUTPUT);
}
// the loop function runs over and over again forever
void loop() {
//// first LED on ////
digitalWrite(charlie1, HIGH); //first charlieplexed pin on
digitalWrite(charlie2, LOW);
delay(500);
//// second LED on ////
digitalWrite(charlie1, LOW);
digitalWrite(charlie2, HIGH); //second charlieplexed pin on
delay(500);
}

You could also set both pins to the same potential (HIGH/LOW) to turn both LEDs off, or set either to INPUT (hi-Z) to stop the flow of electrons. This is shown below. Here digitalWrite is defined in the setup, while pinMode is defined in the loop (opposite of how it's normally done in Arduino).

const int charlie1 = 2;
const int charlie2 = 3;
void setup() {
// initialize digital pin LED_BUILTIN as an output.
digitalWrite(charlie1, LOW); //first charlieplexed pin on
}
// the loop function runs over and over again forever
void loop() {
//// first LED on ////
pinMode(charlie1, OUTPUT); //pinmode usually set in setup
delay(2000);
pinMode(charlie1, INPUT);
delay(2000);
}

With those fundamental concepts out of the way, the video below by Brian Lough outlines how Charlieplexing works to multiply LED outputs, setting A, B, and C pins to HIGH, LOW, and Hi-Z to control six LEDs. Note that the value of LEDs that you can control is N*(N-1), where N is the number of GPIO pins used. So three GPIOs allow you to control of 3*(3-1) LEDs = 6. Four GPIOs gives you as many as 12 LEDs. Two is a wash, with only two LEDs under control.

Of course, with the help of transistors and/or other electronics, Charlieplexing can be used to control more than just LED lighting. It's even possible to use Charlieplexing to help with inputs, though that is your rabbit hole to follow if you so choose.

What about INPUT_PULLUP?

While input pins can be used as tri-state (non) outputs, their nominal purpose is to sense the electrical potential applied to them. If none exists, these pins will float – i.e. bounce around from positive to negative. The solution is to add pullup or pulldown resistors (typically in the 10K or more range) to force the input state HIGH or LOW when no other voltage source is present.

To make this much easier, microcontrollers like the Arduino UNO's ATmega328 have built-in resistors that optionally pull the input state HIGH by default. This can be set in the Arduino IDE via the INPUT_PULLUP function. This might work for Charlieplexing and other hi-Z operations, but it's less than ideal.

HIGH, LOW, Hi-Z

Using HIGH as on, LOW as off in the "traditional" microcontroller-load-ground arrangement will work well in the vast majority of cases. And if you want to control more LEDs, addressable units are an easy solution without the complication of Charlieplexing. While you won't always need to put an output pin into hi-Z mode, or otherwise manipulate HIGH and LOW in clever ways, you do have those options. This can potentially lead to better code, PCBs, and products!

I hope this bit of information comes in handy now or one day in the future. As Mortal Kombat and/or Ralph Waldo Emerson taught us, "There is no knowledge that is not power!"

Thanks for reading! - JC

Jeremy Cook
Engineer, maker of random contraptions, love learning about tech. Write for various publications, including Hackster!
Latest articles
Sponsored articles
Related articles
Latest articles
Read more
Related articles