Controlling an LED Matrix with the HT16K33

Now that all of the time circuit display electronics are in the mail it’s time to talk about programming the display. This starts with learning how to control LEDs with the Holtek HT16K33 integrated circuit (IC), which I’m using as the display’s matrix driver.

Although I’m writing this post as part of a series on creating my own version of the Back to the Future time circuits, this is a powerful little integrated circuit that can be useful for a number of DIY projects involving LEDs. You can find some more generalized information about the HT16K33 in my other post here.

All of this information below is derived from the Holtek datasheet, which you can find hosted at Adafruit. I’m referencing Rev. 1.10, which is dated May 16, 2011.

HT16K33 Setup

Before sending any commands to the HT16K33 the hardware side of the circuit needs to be set up properly.


The HT16K33 needs to be connected to a 5V DC power supply. Current requirements increase depending on how many segments are lit, but the IC and matrix shouldn’t draw more than 0.5 A with all segments enabled.

Each display receives its data over the bidirectional inter-integrated circuit (I2C) bus. The bus has two wires: serial data (SDA) and serial clock (SCL). At some point in the schematic these need to be pulled to logical high (5V) through pull-up resistors. All of the HT16K33 displays need to be on the same bus as the controller.

On my time circuit display (TCD) prototype I’m using Adafruit’s HT16K33 breakout board. Like the dedicated TCD circuit board, the breakout board has pull-up resistors for the I2C lines and a decoupling capacitor for the IC. These are a must for the integrated circuit to work properly.

I2C Address Settings

The I2C bus is addressable, which allows a large number of devices to communicate using only two wires. With 7-bit addresses, I could add a total of 128 different devices to the same bus.

Page 26 of the datasheet talks about setting addresses for the HT16K33. The HT16K33 uses a 7-bit address, with the four most significant bits (MSB) set to ‘1110’ (0x70 in hexadecimal, 112 in decimal). This is the ‘default’ address for the chip if no jumpers are set.

Note: The hexadecimal value is a little misleading, as the read/write bit follows the address rather than precedes it. If we’re talking about the entire byte including the r/w bit the first hex character is ‘E’ rather than ‘7’. However for a 7-bit address the constant 4 bits are spread between nybbles, which changes the hex value. C’est la vie.

The remaining three address bits are either ‘0’ or user-configurable depending on the package. I’m using the 28-pin package for the time circuit displays, which allows me to change all three remaining address bits. This means I can use a total of 8 HT16K33 chips on the same bus (23) without using a multiplexer, although I only need three for this project.

The address bits are set in hardware by wiring 39K resistors in-line with a diode to connect the COM0 pin with either the ROW2 (A0), ROW1 (A1), or ROW0 (A2) pins. Each bridged connection sets its respective bit to ‘1’, while a floating connection sets the bit to ‘0’. On the TCD circuit boards these connections are exposed as jumper pads.

I know it doesn’t particularly matter once it’s stored in the code, but it makes sense to me to set the addresses in a logical order. I’m going to set them increasing from top to bottom, which follows Doc’s little speech when he’s describing the time circuits for the first time:

This readout tells you where you’re going. This one tells you where you are. This one tells you where you were.

As such, the addresses are as follows:

DisplayI2C Address (Hex)I2C Address (Dec)I2C Address (Bin)
Destination Time0x711130b1110001
Present Time0x721140b1110010
Last Time Departed0x741160b1110100

The first display has its address bits floating, while the second and third have just one jumper soldered on each.

Updated 2017-10-10: I ended up soldering the jumpers for all three displays – A0 through A2, top to bottom. The logic is that I may end up using another HT16K33 somewhere else in the circuit and I could avoid the address resistors / diode altogether. The display boards already have the hardware for changing the addresses, so it makes sense to take advantage of that capability while I can. I have updated the table above accordingly.

I2C Commands for the HT16K33

With the address in-hand, the next step is to send some data over the bus to the integrated circuit. After sending the 7-bit address with an appended ‘write’ bit (0) the HT16K33 will take read control of the bus. The next byte it receives will be a command code.

Each command byte is split into two parts of 4 bits each, called a ‘nybble‘. The first nybble is the command, while the second nybble is the setting.

Note: If you can’t take a byte, you can at least take a nybble. Get it? Who ever said programmers weren’t clever.

Start Oscillation

The first command is the one that turns on the chip’s internal oscillator. This wakes the chip from standby and starts the LED multiplexing.

The command nybble is ‘0b0010’ (0x2). The setting nybble can either be ‘0’ or ‘1’ to toggle the oscillator’s state.

Sending LED Data

With the oscillator enabled I can start sending LED data. The command for this sets the position of the RAM address pointer, and subsequent bytes are the LED data itself.

The command nybble for setting the LED states is ‘0b0000’ (0x0). The setting nybble is the pointer’s RAM address. There are 16 addresses, with each bit corresponding to an LED state (16 addresses * 8 bits per byte = 16 rows * 8 columns). When the address pointer reaches the end of the block it returns to the beginning.

After the command byte, each successive byte sent on the bus will set an LED block state. This means that the command byte and 16 data bytes will set all LED states.


After enabling some LEDs, the dimming function allows you to dim the displaying by lowering the pulse width of each ‘ROW’ pin. The dimming function is adjustable with 16 steps (24).

The command nybble for dimming is ‘0b1110’ (0xE). The setting nybble sets the duty cycle from 1 – 16. A setting of ‘0’ is 1/16th duty, while a setting of ’15’ is 16/16 duty.

Blinking and Blanking

The HT16K33 also includes an automatic blinking function with three levels: 2 Hz, 1 Hz, and 0.5 Hz. This command is combined with a blanking feature that turns off the display without turning off the chip’s oscillator or clearing the LED matrix data.

The command nybble for both of these is ‘0b1000’ (0x8). The LSB of the setting nybble sets the display state (off / on) while the second and third bits set the blinking level (0 = off, 4 = 0.5 hz).

There are a few other available commands but they’re only for the chip’s keyscanning operations and they aren’t relevant for the LED displays.

I2C and Arduino

For prototyping I’m using an Arduino Nano as my controller. The Arduino IDE has a built-in library for handling I2C communication called Wire.

After calling Wire.begin() to start the I2C communication, each request over the bus follows the same format:

  1. Wire.beginTransmission(address) will start a queue for communicating with a slave device at a specific address.
  2. Wire.write() will queue a byte for transfer.
  3. Wire.endTransmission() will transmit the bytes queued with write and end the communication with the slave device.

With an Arduino, the Wire library is all that’s needed for communicating with the HT16K33.

HT16K33 Arduino Demo

I put together a quick demo sketch using the commands I described above. This sketch initializes the HT16K33, turns on all LEDs sequentially, and then shows off the blinking, blanking, and brightness commands.

The functions included in this demo sketch should be all that’s needed to control the HT16K33’s LED matrix from an Arduino.

Note: For the demo sketch I’m using a 16-bit buffer, which makes each array element correspond to all outputs for a given ‘COM’ pin. You could also use an 8-bit buffer, which would make things easier if I only had 7-segment displays.

Because programming is more fun when you see LEDs blink, here’s a gif of part of that test sketch. I would upload the whole thing, but gfycat only has a 15 second limit (and this is the fun part anyways).


Communication has been established and the LEDs are showing patterns! Once you break down the I2C commands into their components the HT16K33 is pretty easy to work with.

Next up: Adding letters and numbers to the time circuit display

Leave a Reply

Your email address will not be published. Required fields are marked *