Designing a Binary Clock: Understanding Multiplexing Constraints

The binary clock is an attractive project that allows you to showcase your knowledge of digital electronics, microcontroller programming, and ability to physically assemble and moderately complex circuit. Many articles have been written on this topic. However, few delve into the design decisions and necessary compromises. This is a shame, as the topic provides an excellent learning opportunity to explore the physical limitation of a microcontroller, applications of Ohm’s Law, the diminishing returns of multiplexing many LEDs, and component layout for a clean easy to troubleshoot system.

This article is focused on a six column by four row LED matrix in the form of the binary clock as shown in Figure 1. For greatest accessibility, we will limit the conversation to physical capabilities of the I/O pins and power supply found on the Arduino Nano Every. Note that the ideas presented in this article are directly applicable to seven segment LED displays, or more generally, to all multiplexed LED arrays.

This is a good time to review the operation of a binary clock, as we will be focused on the low-level hardware and multiplexing aspects. A good starting point is this DigiKey Arduino article that describes how to read the clock. It also presents a contrasting design along with Arduino code that may help you better understand the multiplexing operation.

Figure 1: Picture of Arduino Nano Every on Breadboard with the LED array and the Digilent Analog Discovery in the background.

Multiplexing defined

Multiplexing is a form of time sharing of a limited resource. In our example, the microcontroller has a limited number of I/O pins. With Time Division Multiplexing (TDM), we use a round-robin sequence to consecutively activate an LED or groups of LEDs. By rapidly sequencing through the LED activation, all LEDs will appear to be equally lit. This quality depends on flicker fusion which is the inability of the human eye to detect the rapid “blinking” of LED in the multiplexed array.

The LEDs are typically arranged in groups using either a common anode or common cathode configuration. An example is shown in Figure 2 featuring a 6 column by 4 row LED matrix with a common anode configuration.

Tech Tip: The term Time Division Multiplexing (TDM) is typically associated with communications systems such as Ethernet. In the Ethernet network, many computers share a communications channel. Since the channel is a limited resource, each computer takes its turn within the established communication protocol. While our application of the term TDM to the LED multiplexing is an oversimplification, it’s a good starting point for understanding our complex communication systems.

Figure 2: Schematic of a 6 column by 4 row multiplexed LED matrix featuring the Arduino Nano Every.

The term common anode and common cathode can cause confusion. For example, an inspection of the LED matrix in Figure 2 shows many diodes share anode connections by column and cathode connections by rows. It’s only when we examine the activation sequence that the common anode configuration is revealed.

Figure 3 presents the logic analyzer view of the multiplexer’s timing sequence. With regards to the common diode connection, we note that one, and only one, column is activated at any given time. Therefore, the common connection is the anode. In this example, the associated column transistor pulls the anode of all associated LEDs to the 5.0 VDC rail. The individual row drivers then apply a ground to activate the desired LED(s).

With regards to Figure 3, we observe:

  • The column drive signals are presented as the first 6 signals corresponding the Arduino D2 to D7 pins. For convenience the signal names have been added as red highlighted text. We see that one, and only one, signal is active at any time. Note that that a PNP transistor is used as the column driver. Also, recall that this transistor will be activated by pulling its base toward ground.

  • The lowest 4 lines are the row drivers corresponding the Arduino output pins D19 to D21. These are also active low as suggested in Figure 2 where a diode is turned on by pulling it toward ground.

  • The Digilent Analog Discovery has been configured to show the binary number associated with the 4 row drivers. This is a feature of some logic analyzers that allow the operator to “join” several lines into a single convenient ASCII, HEX, or binary display. For convenience, this has been highlighted in blue.

Tech Tip: There are two general ways to configure LED multiplexing including the common anode and common cathode. This article features PNP column drivers with common anode LEDs. Here the PNP transistor connects the common anode to the positive rail and grounds are then applied to the individual cathodes. The previously mentioned DigiKey Binary Clock article features a matrix with NPN column drivers with common cathode LEDS. There, the NPN transistor connects the common cathode to ground, and a positive voltage is applied to the desired rows. Careful inspection is required when the assembly is rotated 90 degrees with common drivers applied to the rows instead of the columns.

Figure 3: Logic analyzer screen capture showing the LED timing with test and highlights added for clarification.

Tech Tip: This discussion about multiplexing and common anode / cathode is applicable to the seven segment LED displays and arrays of LEDs.

Current limitations

This article is constrained by the capabilities of the Arduino Nano Every. This includes both the available power supply as well as Nano Every’s on-board power supply, individual pin, and groups of pins. The

The relevant Arduino Nano Every with the featured ATMega4809 microcontroller specifications are:

ABX00028-datasheet.pdf (arduino.cc)

  • 5.0 VDC supply current limit = 950 mA

  • individual pin (source or sink) = 40 mA

  • rail pin (source or sink at elevated temperature) = 100 mA

As for the chosen SSL-LX21573GD LED:

  • steady current at room temperature = 25 mA

  • peak current for a 10 us pulse = 150 mA

Most of these numbers are the absolute design maximum ratings. For long life, we must maintain a wide safety margin. As a starting point, let’s cut each current value in half. As we will see, this decision has serious implication for the multiplexing options. It casts a shadow on the Figure 2 schematic with an impact on transistor column drivers and the number of multiplexed LEDs.

Tech Tip: The internal die of the microcontroller is connected to the I/O pin using small bond wires. These wires along with the silicon traces have limited current carrying capability. This is reflected in the devices’s design maximum specifications. In the previous example, we see that an individual I/O pin has a 40 mA design maximum. However, three such pins operating at maximum current would overwhelm the die-to-power or the die-to-ground bond wires or silicon traces. This cumulative current is an important but often overlooked design consideration. It’s a mistake that will destroy a microcontroller, perhaps not immediately but it will result in an unreliable product.

Duty cycle vs brightness

Before providing a full explanation of the transistor column drivers and resistor selection, we must first understand the relationship between flicker and LED brightness. Recall that the LED matrix is Time-Division-Multiplexed (TDM). Each LED “flickers” as it is sequentially turned on and off as shown in the Figure 3 timing diagram. Chances are you have already performed a related experiment exploring LED flicker.

Early in your Arduino learning, you likely used a PWM to dim a LED using a commands such as this:

void loop() {
    static uint8_t val;             // Maintain contents across loop iterations
    analogWrite(LED_PIN, val++);    // About 5 seconds to reach 100% duty cycle
    delay(20);
}

This PWM operation is directly associated with the TDM operation when we consider the impact on a single LED. In both cases, the LED will flicker with a given time on and time off. In both cases, it is this duty cycle that determined LED brightness.

The results are shown in Figure 4 with LEDs operating at 100, 50, 25, 12.5, and 6.25 % duty cycle corresponding to multiplexing with TDM slots of 1, 2, 4, 8, and 16 time divisions respectively. The results are not good for the low duty cycle LEDs. In fact, it is very difficult to see an LED with a duty cycle of 1/16.

Figure 4: Picture showing the relative brightness of LEDs when driven at different duty cycles.

Multiplexing methods

Our multiplexing objective is to obtain maximum LED brightness while staying within the current limitation of the hardware. Now that we have a better understanding of the physical limitation of the hardware, we can explore multiplexing methods:

  • One LED at a time: We could eliminate the column drivers and allow the microcontroller to directly control both column and row. While this is highly desirable for circuit cost and simplicity, we quickly run into current limitations. Without the column driver, the associated microcontroller pin must supply the necessary current. This limits us to one or perhaps two LEDs at a time as the cumulative current will exceed the microcontrollers design max value. Given the 6 x 4 array, this necessitates a duty cycle of 4 % or perhaps 8 %. The LEDs will be very dim.

  • One column at a time with column drivers and rows controlled by the microcontroller: With the PNP transistor supplying the current to a column, we are free to turn on all rows. Given a 6 by 4 array, this results in a 17 % duty cycle for each LED. The results as presented in this article are minimally acceptable for a dimly lit room.

  • One column at a time with column and row drive transistors. This approach allows the LEDs to be driven with current higher than the capacity of the microcontroller. Since the LED has a low duty cycle it is possible to increase brightness by increasing the current. There is a certain ambiguity in LED datasheets, however the current may certainly be increased to the design maximum and perhaps 2x higher without damaging the LED. Recall that our chose LED was specified for 25 mA continuous and 150 mA with a 10 us pulse. This approach requires careful consideration of LED temperature and could lead to significant reduction in LED lifespan. Use at your own risk.

  • Parallel control: For this entire post we have assume that the microcontroller pins are the limiting resource. There are any number of port-expanding options that would allow direct control of the LED. An example is the 8-bit TLC6C598 shift register. It features 50 mA open drain drivers with a maximum V_{DS} of 40 VDC. The 74HC595 is another common option suitable for breadboard prototyping.

Resistor selection

Proper resistor selection is required to establish the working current of the LED. It is also important to select appropriate resistors for the base of the column driving transistors. This base resistor is important to maintain a consistent LED brightness as the number of activated LEDs changes with the displayed number.

The LED calculation is relatively simple. The first step is to determine the current limitations. With the chosen multiplexing scheme, up to 4 LEDs are active at any given time. The first limit we encounter is the cumulative current for the microcontroller. For our conservative design, this is 50 mA. Consequently, each LED is limited to about 13 mA. The resistor is calculated as:

R_{LED} = \dfrac{V_{rail} – V_{LED}}{I} = \dfrac {5.0-2.0}{0.013}= 220\, \Omega

Where we assume that the PNP column driver V_{CE} is very close to zero.

For the transistor base resistor calculation, we will implement a forced beta condition. This transistor operating point ensures that the transistor is deep into saturation which will ensure that all LEDs have equal brightness. To set this condition we select resistors so that the base current to 1/10 of the collector current. Given 4 active LEDs, the collector current is approximately 50 mA. With forced beta operation we will force the base current to be 5 mA.

R_{Base} = \dfrac{V_{rail} – V_{diode}}{I} = \dfrac {5.0-0.7}{0.005} \approx 820\, \Omega

Room for improvement

This post is focused on aspects of multiplexing LEDs. Little to no consideration is given to operating the device as a reliable timekeeper. The code appended to this note uses the Arduino millis( ) function which is not known as a reliable real-timekeeper. All such devices depend on the microcontroller’s high-speed oscillator which is not nearly as accurate as a properly implemented 32.768 kHz clock crystal.

For improved performance, you may want to integrate a real time clock module. For an additional challenge try a GPS or WWVB (atomic clock receiver) based timekeeper with antenna. We could certainly explore this in a future post if there is interest.

Please include your comments and questions in the space below.

Best Wishes,

APDahlen

#define C0_PIN 2
#define C1_PIN 3
#define C2_PIN 4
#define C3_PIN 5
#define C4_PIN 6
#define C5_PIN 7

#define R0_PIN 21
#define R1_PIN 20
#define R2_PIN 19
#define R3_PIN 18

void set_column(uint8_t c) {

  digitalWrite(C0_PIN, HIGH);
  digitalWrite(C1_PIN, HIGH);
  digitalWrite(C2_PIN, HIGH);
  digitalWrite(C3_PIN, HIGH);
  digitalWrite(C4_PIN, HIGH);
  digitalWrite(C5_PIN, HIGH);

  switch (c) {
    case 0: digitalWrite(C0_PIN, LOW); break;
    case 1: digitalWrite(C1_PIN, LOW); break;
    case 2: digitalWrite(C2_PIN, LOW); break;
    case 3: digitalWrite(C3_PIN, LOW); break;
    case 4: digitalWrite(C4_PIN, LOW); break;
    case 5: digitalWrite(C5_PIN, LOW); break;
    default: break;
  }

}


void set_row(uint8_t n) {

  bool D0 = ((n & 0x01) == 0);
  bool D1 = ((n & 0x02) == 0);
  bool D2 = ((n & 0x04) == 0);
  bool D3 = ((n & 0x08) == 0);

  digitalWrite(R0_PIN, D0);
  digitalWrite(R1_PIN, D1);
  digitalWrite(R2_PIN, D2);
  digitalWrite(R3_PIN, D3);

}


void set_LED(uint8_t c, uint8_t n) {

  set_column(c);
  set_row(n);

}


void setup() {
  pinMode(C5_PIN, OUTPUT);
  pinMode(C4_PIN, OUTPUT);
  pinMode(C3_PIN, OUTPUT);
  pinMode(C2_PIN, OUTPUT);
  pinMode(C1_PIN, OUTPUT);
  pinMode(C0_PIN, OUTPUT);

  pinMode(R3_PIN, OUTPUT);
  pinMode(R2_PIN, OUTPUT);
  pinMode(R1_PIN, OUTPUT);
  pinMode(R0_PIN, OUTPUT);
}


void loop() {

  uint8_t i;

  uint32_t now = millis() /1000;

  uint8_t seconds = now % 60;
  uint8_t minutes = (now / 60) % 60;
  uint8_t hours = (now / 3600) % 24;

  uint8_t hoursHigh = hours / 10;
  uint8_t hoursLow = hours % 10;

  uint8_t minutesHigh = minutes / 10;
  uint8_t minutesLow = minutes % 10;

  uint8_t secondsHigh = seconds / 10;
  uint8_t secondsLow = seconds % 10;

  for (i = 0; i < 6; i++) {

    switch (i) {

      case 0: set_LED(0, secondsLow); break;
      case 1: set_LED(1, secondsHigh); break;
      case 2: set_LED(2, minutesLow); break;
      case 3: set_LED(3, minutesHigh);break;
      case 4: set_LED(4, hoursLow); break;
      case 5: set_LED(5, hoursHigh); break;
      default: break;
    }
    delay(2);
  }
}