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


  1. Hey! this is a super useful post! I think I am finally understanding I2C communication THANK YOU!

    If you check your comments, wanted to ask about a part just to make sure I am understanding it correctly. I’m a n00b when it comes to bit-wise operations.

    so I get that
    each LED row is saved as a 2 byte value.

    all the LEDs on would be

    since the arduino is 8-bit, I ‘m guessing that why you have to send it 8 bits at a time, this is the part that confused me a little but i think i get it.

    Wire.write(displayBuffer[i] & 0xFF);
    this sends the first 8 bits, it changes our original value to 0b0000000011111111 aka 0b11111111

    Wire.write(displayBuffer[i] >> 8);
    send the second 8 bits, changing the value to

    0b1111111100000000 aka 0b11111111

    am I getting this right? Thank you!!

    1. Hi Dan! Glad you found the post useful.

      The per-byte thing isn’t because the Arduino is an 8-bit processor, that’s just how the bus works. On a 32 bit processor you’d still be sending 1 byte at a time.

      You’re close with your interpretation. The “& 0xFF” command is a bitwise “AND” that gives you the first byte (right-hand side) of the variable. The “>> 8” command is a bit shift that moves the bits 8 places to the right, effectively giving you the second (left-hand) byte in the variable. Though it’s important to note that 0xFF00 is not the same as 0xFF (e.g. 65,280 is not the same as 255), which matters if you’re saving the value. This Wikipedia page has some good information:

      You don’t *have* to use a 16-bit variable for the LED data though, you could just as well use an 8-bit array and omit the bitwise operations. The 16-bit array just makes more sense to me with 16 ‘row’ pins per common.

  2. Great info here. I’m working on a Time Circuits project as well and while looking for info on the HT16k33 as way to drive the LEDs I came across your page. I wasn’t even specifically looking for info on building the Time Circuits at that point

    I’m using Kingbright LED displays as well and find the yellow ones to be a bit dim and was curious if encountered the same to determine if this is normal for them.

    In your video above and photo on another page, they do possibly look dim, but it’s hard to determine brightness from a photo.

    Thanks for posting all this info, the dimensions for the display are very helpful.

    1. Hi John! Glad you’re finding the information useful.

      Yes, the yellow displays are dim for me as well. I was planning on using some gels on top of the LEDs like the first movie which would significantly increase the contrast, though as you can see the project has stalled.

  3. Hi Dave,
    I’ve been pulling my hair out for days with the ht16k33. I can get the demo code to run, and also the adafruit demos, but I can’t for the life of me understand how to send my own data to the display no matter how many times I read this page, I just don’t understand it.
    Is there any way you could share even a simple example (e.g. the one shown on the other page with a date) so that I can try to make a little bit of headway in to the project. I just can’t get this to work as I’m used to the MAX7219 which was easy! Thanks for your help.

    1. Hi Darryl,

      Sorry to hear you’re having trouble, I know the feeling all too well. Unfortunately the Arduino sketch in the post cannot be made much simpler, but I’ll try to break it down for you. There are two steps: initializing the chip, and then sending the LED data to it.

      To initialize the chip, you need to start the oscillator, set the brightness to max, and disable the blinking/blanking functionality. That is all done within setup(), with calls to setBrightness() and blink(). Each command starts an I2C write to a specific register at the chip’s address, and then sets the register to the relevant values according to the datasheet.

      Once it’s initialized you can send LED data. Any bit set to ‘1’ within the LED buffer (displayBuffer[]) is an LED row + column combination that is “on”. Any bit set to ‘0’ within the LED buffer is an LED row + column combination that is set to “off”. The array indices of the displayBuffer correspond to the ‘column’ (common) pins of the device. You then call show() to push the display buffer to the HT16K33 over I2C.

      I hope that helps. If you’re still stuck, try looking up tutorials for how the I2C bus functions, or examples of how to use the Wire library.

      1. I’ve managed it! I was the Eureka moment I was waiting for. I don’t think I was initialising the display properly (it’s strange to set the brightness!), I’m not sure what I did, but I now know how it works. Thanks for all of your help, and this excellent blog. I’ve also managed to use your ASCII library and started to implement some of the procedures in to my test code.
        The command I was struggling to put together was things like: displayBuffer[0] |= 0x06 & 0xFF; and it now works with this or Binary. I’ve also got the writeCharacter working and referencing you library, I’m only using a 4 digit 7 segment at the moment until I order my other displays, so I guess I don’t need the & 0xFF.

        Once again, thankyou for pointing me in the right direction. Once it clicks, I understand it, but I’m on a steep learning curve with the Arduino even though I’ve had one since they first came out. I managed to program the kepad side of the time circuit with no problems, but displays always stump me.

        Thanks again for replying.


Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.