HOME \ ELECTRONICS \ HT16K33 16×8 0805 SMD Dot Matrix Panel for ESP32 / ESP8266 IoT Projects
02.10.2026
Amber LED Matrix 16*8 HT16K33 Amber LED Matrix 16*8 HT16K33 Amber LED Matrix 16*8 HT16K33 ESP32 Scroller

HT16K33 16×8 0805 SMD Dot Matrix Panel for ESP32 / ESP8266 IoT Projects

This project is a compact 16×8 dot matrix LED display board built around the Holtek HT16K33 LED controller. The display contains 128 individual 0805 SMD LEDs arranged as a matrix and can be controlled through an I2C interface using only two signal lines.

This project is a compact 16×8 dot matrix LED display board built around the Holtek HT16K33 LED controller. The display contains 128 individual 0805 SMD LEDs arranged as a matrix and can be controlled through an I2C interface using only two signal lines.

I designed this board mainly as a reusable display module for ESP32, ESP8266 and other microcontroller-based IoT projects. Instead of using a ready-made LED matrix module, all 128 LEDs are placed directly on the PCB together with the HT16K33 driver, I2C pull-up resistors and local decoupling capacitors.

The result is a thin, simple and general-purpose dot matrix display that can be used for scrolling text, numbers, icons, status information, sensor values, animations, clocks, counters and small graphical interfaces.

16*8 LED Display HT16K33

16×8 HT16K33 dot matrix display using 128 individual 0805 SMD LEDs.

Main Features

Why I Built This Display

Small LED matrix displays are useful in many projects, but most inexpensive modules are designed around fixed commercial matrix packages. For some applications I wanted more control over the mechanical layout, LED type, spacing and PCB dimensions.

Using individual 0805 LEDs makes it possible to define the complete matrix directly in the PCB layout. It also makes the design easier to adapt for custom enclosures, front panels, clocks or embedded IoT devices.

The HT16K33 is particularly useful for this type of project because the microcontroller does not have to perform the LED multiplexing itself. The ESP32 or ESP8266 only updates the display memory through I2C and the HT16K33 continuously refreshes the matrix in hardware.


Hardware Overview

Component Function
HT16K33 LED matrix driver, display RAM and multiplexing controller
128 × 0805 LED 16×8 visible dot matrix
R1 4.7 kΩ I2C pull-up resistor
R2 4.7 kΩ I2C pull-up resistor
C1 1 µF Local power supply filtering
C2 0.1 µF HT16K33 supply decoupling
J1 Power and I2C connection
H1–H4 Mechanical mounting holes

Circuit Design

The circuit is intentionally simple. The HT16K33 provides sixteen ROW outputs and eight COM outputs, giving a complete 16×8 multiplexed LED matrix with a maximum of 128 LED positions.

The sixteen matrix lines are connected to ROW0 through ROW15, while the remaining eight matrix lines are connected to COM0 through COM7. All 128 LEDs are therefore driven directly by the HT16K33 matrix architecture.

The controller communicates with the host processor using SDA and SCL. The PCB contains two 4.7 kΩ pull-up resistors for the I2C bus. A 1 µF capacitor and a 100 nF capacitor are placed around the HT16K33 supply section for local filtering and decoupling.

The schematic uses the 28-pin SOP version of the HT16K33. All sixteen ROW signals and all eight COM signals are therefore available for the complete 128-pixel matrix.

Connector Pinout

The four-pin connector provides power and the two I2C signals. According to the PCB schematic, the connector is arranged as follows:

J1 Pin Signal Description
1 VDD Display supply voltage
2 VSS Ground
3 SDA I2C data
4 SCL I2C clock

Important ESP32 / ESP8266 Logic-Level Note

There is one important electrical detail to consider when connecting the original HT16K33 directly to an ESP32 or ESP8266. The HT16K33 is a 5 V-class device, while ESP32 and ESP8266 GPIO pins use 3.3 V logic and are not intended to be pulled up to 5 V.

On this PCB, the 4.7 kΩ SDA and SCL pull-up resistors are connected to the HT16K33 VDD rail. If the display is powered from 5 V, the I2C lines are therefore also pulled toward 5 V.

For a reliable ESP32 or ESP8266 connection, I recommend using a bidirectional I2C level shifter between the controller and the display. A small BSS138-based level converter is sufficient for this application.

ESP32 / ESP8266                HT16K33 Display

3.3 V  -------------------- LV side of level shifter
5 V    ------------------------------------- VDD
GND    ------------------------------------- GND

SDA    ---- level shifter ------------------ SDA
SCL    ---- level shifter ------------------ SCL

If I make another revision of this PCB specifically for ESP32 and ESP8266 applications, integrating the I2C level translator directly onto the display PCB would be a useful improvement.

PCB Design

The PCB is approximately 74.93 mm wide and 30.48 mm high. It has rounded corners and four mounting holes, making it easy to attach behind an acrylic front panel or inside a small enclosure.

The 128 LEDs occupy most of the front surface. The LEDs are rotated approximately 45 degrees relative to the PCB edges, creating a diamond-shaped appearance for each illuminated point. The HT16K33 and the remaining components are positioned in the central and lower areas of the board.





Matrix Organization

Electrically, the display can be considered an 8-pixel-wide by 16-pixel-high matrix. For many applications, however, it is more convenient to treat the same hardware as a 16×8 landscape display.

The test firmware therefore provides both coordinate systems.

Physical orientation:

X = 0 ... 7
Y = 0 ... 15

Logical landscape orientation:

X = 0 ... 15
Y = 0 ... 7

The basic physical pixel function is:

void setPixel(int x, int y, bool state)
{
    if (x < 0 || x >= 8) return;
    if (y < 0 || y >= 16) return;

    uint8_t ramAddress;
    uint8_t bitNumber;

    if (y < 8)
    {
        ramAddress = (x * 2) + 1;
        bitNumber = 7 - y;
    }
    else
    {
        ramAddress = x * 2;
        bitNumber = 15 - y;
    }

    if (state)
        displayRAM[ramAddress] |= (1 << bitNumber);
    else
        displayRAM[ramAddress] &= ~(1 << bitNumber);
}

For normal 16×8 landscape graphics, I use a second function that translates the coordinates:

void setPixelLandscape(int x, int y, bool state)
{
    if (x < 0 || x >= 16) return;
    if (y < 0 || y >= 8) return;

    setPixel(y, 15 - x, state);
}

This means the application code can work with a familiar 16-pixel-wide display without having to know how the LEDs are physically connected to the HT16K33 display RAM.

HT16K33 Display RAM

The software keeps a local 16-byte framebuffer:

uint8_t displayRAM[16];

Once the framebuffer has been modified, the entire block can be sent to the controller:

void writeDisplay()
{
    Wire.beginTransmission(HT16K33_ADDR);
    Wire.write(0x00);

    for (int i = 0; i < 16; i++)
        Wire.write(displayRAM[i]);

    Wire.endTransmission();
}

Using a framebuffer makes animations much easier because an entire frame can be constructed in RAM before it is transferred to the display.

ESP32 Connection

The supplied test firmware uses the normal ESP32 I2C pins:

ESP32 Function
GPIO21 SDA
GPIO22 SCL
#define HT16K33_ADDR 0x70

#define SDA_PIN 21
#define SCL_PIN 22

#define DEFAULT_BRIGHTNESS 8

The I2C interface is initialized at 100 kHz:

Wire.begin(SDA_PIN, SCL_PIN);
Wire.setClock(100000);

ESP8266 Connection

The same display routines can also be used with an ESP8266. Only the I2C pin definitions normally need to be changed.

For example, on many ESP8266 development boards:

#define SDA_PIN 4
#define SCL_PIN 5

Wire.begin(SDA_PIN, SCL_PIN);

These correspond to GPIO4 and GPIO5. Some development boards label these pins differently, so the GPIO numbers should always be checked for the particular ESP8266 board being used.

The same 3.3 V / 5 V I2C level-shifting consideration applies to the ESP8266.

HT16K33 Initialization

The test software initializes the display with only a few commands.

htCommand(0x21);   // Enable internal oscillator
htCommand(0x81);   // Display ON
setBrightness(DEFAULT_BRIGHTNESS);
clearDisplay();

The command helper function is:

void htCommand(uint8_t cmd)
{
    Wire.beginTransmission(HT16K33_ADDR);
    Wire.write(cmd);
    Wire.endTransmission();
}

Brightness Control

The HT16K33 provides hardware brightness control with sixteen available levels. The test program accepts values from 0 to 15.

void setBrightness(uint8_t b)
{
    if (b > 15) b = 15;
    htCommand(0xE0 | b);
}

This is useful for IoT devices that operate in different lighting conditions. For example, an LDR or ambient-light sensor could be connected to the ESP32 and used to automatically reduce the matrix brightness at night.

5×7 Character Font

The demonstration firmware also includes a simple 5×7 bitmap font for uppercase letters and numbers. Each character is stored as five columns of seven visible pixels.

The font currently includes:

A character can be drawn directly into the framebuffer:

void drawCharLandscape(int px, int py, char c)
{
    for (int col = 0; col < 5; col++)
    {
        uint8_t bits = fontCol(c, col);

        for (int row = 0; row < 7; row++)
        {
            if (bits & (1 << row))
            {
                setPixelLandscape(
                    px + col,
                    py + row,
                    true
                );
            }
        }
    }
}

Scrolling Text

A 16×8 matrix is too narrow to display long words at once, but it is very suitable for horizontal scrolling text. The supplied demo scrolls:

HT16K33 DEMO

across the display using the 5×7 font.

void scrollText(const char *text)
{
    int len = strlen(text) * 6;

    for (int offset = 16; offset > -len; offset--)
    {
        clearBuffer();

        int x = offset;

        for (int i = 0; text[i]; i++)
        {
            drawCharLandscape(x, 0, text[i]);
            x += 6;
        }

        writeDisplay();
        delay(60);
    }

    clearDisplay();
}

Included Test Animations

I wrote a fairly extensive test program for checking the PCB, pixel mapping and animation performance. The main loop automatically cycles through several demonstrations.

Demo Purpose
All LED Test Turns all 128 LEDs on and off
Brightness Demo Fades through the available HT16K33 brightness levels
Row Scan Checks every physical matrix row
Column Scan Checks every physical matrix column
Snake Fill Fills the complete matrix using a snake pattern
Scrolling Text Displays horizontal 5×7 text
Vertical Text Scrolls characters through the physical 8×16 orientation
Matrix Rain Creates a falling-pixel Matrix-style animation
Smile Displays a simple smile face
Sad Face Displays a simple sad face
Heart Pulse Uses hardware brightness control to create a pulsing heart
8-Channel VU Meter Uses the physical 8×16 orientation as eight vertical bars
16-Channel VU Meter Uses the landscape 16×8 orientation as sixteen level bars
Large Up Arrow Scrolls a large arrow vertically
Scrolling Numbers Displays large 8×12 style digits
Car Animation Moves a small vehicle across the screen
Bouncing Ball Single-pixel bouncing animation
Pong Small automatic Pong animation
Arkanoid Mini brick and paddle animation
Sine Wave Animated mathematical wave
KITT Scanner Moving scanner-style light effect
Spiral Fills the 16×8 display using a spiral path

All LED Test

The first test is deliberately simple. It turns every LED on for approximately 900 ms and then switches the matrix off.

void allLEDTest()
{
    setBrightness(8);

    allLED(true);
    delay(900);

    allLED(false);
    delay(300);
}

This is one of the most useful tests after assembling the PCB because missing, reversed or poorly soldered LEDs can immediately be identified.

Row and Column Tests

The row and column scan routines are useful for checking the electrical connections between the LEDs and the HT16K33. If one complete line fails to illuminate, the problem is more likely to be associated with a ROW or COM connection rather than with an individual LED.

 

Snake Fill Test

The snake animation lights each pixel sequentially while alternating direction on every row. Apart from being a simple animation, it is a very useful way of confirming that the logical pixel order matches the physical LED arrangement.

Matrix Rain

The Matrix-style rain effect uses a number of independently moving pixel heads with randomized starting positions and speeds. It demonstrates that the framebuffer can be updated fast enough for simple real-time animations without requiring complicated hardware.

Heart Pulse

The heart demo combines a bitmap graphic with the HT16K33 hardware brightness control. Instead of redrawing the graphic continuously, the heart remains stored in display RAM while the global brightness is increased and decreased.

VU Meter Examples

Both possible matrix orientations are useful for bar-graph applications.

In the 8×16 orientation, eight relatively tall channels can be displayed:

8 channels × 16 levels

In landscape orientation, the same display can be used as:

16 channels × 8 levels

This makes the PCB suitable for audio level displays, spectrum-style indicators, sensor arrays and status dashboards.

Large Numbers

The demo firmware also contains larger digit bitmaps for the numbers 1 through 9. Each character is approximately 8 pixels wide and 12 pixels high and is scrolled vertically through the physical 8×16 display area.

Large custom fonts can be useful for:

Simple Games and Graphic Demos

A 16×8 display obviously has very limited resolution, but that limitation is also part of the fun. The test firmware contains automatic Pong and Arkanoid-style animations as well as a bouncing ball and moving car.

These examples are not intended to be complete games. They are simply useful demonstrations of coordinate mapping, animation timing and framebuffer updates.

 

 

 

Example Main Loop

The complete test program continuously cycles through all demonstrations:

void loop()
{
    allLEDTest();

    brightnessDemo();

    rowScan();

    columnScan();

    snakeFill();

    scrollText(" HT16K33 DEMO ");

    verticalText();

    matrixRain();

    smileEmoji();

    sadEmoji();

    heartPulse();

    vuMeter8();

    vuMeter16();

    bigUpArrow();

    scrollingNumbers();

    carDemo();

    bouncingBall();

    pongDemo();

    arkanoidDemo();

    sineWave();

    kittScanner();

    spiralDemo();

    delay(500);
}

Basic ESP32 Test Program Structure

For a real project it is not necessary to use all the demonstrations. The software can be reduced to a very small driver consisting of:

A minimal initialization sequence looks like this:

#include <Wire.h>

#define HT16K33_ADDR 0x70
#define SDA_PIN 21
#define SCL_PIN 22

uint8_t displayRAM[16];

void htCommand(uint8_t cmd)
{
    Wire.beginTransmission(HT16K33_ADDR);
    Wire.write(cmd);
    Wire.endTransmission();
}

void writeDisplay()
{
    Wire.beginTransmission(HT16K33_ADDR);
    Wire.write(0x00);

    for (int i = 0; i < 16; i++)
        Wire.write(displayRAM[i]);

    Wire.endTransmission();
}

void setup()
{
    Wire.begin(SDA_PIN, SCL_PIN);
    Wire.setClock(100000);

    delay(200);

    htCommand(0x21);
    htCommand(0x81);
    htCommand(0xE8);
}

void loop()
{
}

Possible IoT Applications

The display is intentionally generic. The HT16K33 only handles the LEDs, so the information shown on the panel can come from practically any ESP32 or ESP8266 application.

Some possible uses include:

Using the Panel with MQTT

One application I particularly like for this type of panel is a small MQTT-connected information display. An ESP32 can subscribe to one or more MQTT topics and convert the received information into text, icons or bar graphs.

For example:

home/livingroom/temperature
home/livingroom/humidity
home/security/status
home/internet/status
home/weather/condition

The display can then alternate between several pages or scroll the received information. Because the HT16K33 handles the multiplexing, the ESP32 remains free to manage Wi-Fi, MQTT, sensors and other application tasks.

Using Multiple Panels

Multiple matrices can also be placed next to each other to create a larger display. This requires each HT16K33 to use a different I2C address. The application can then treat several physical panels as one larger logical framebuffer.

For example, two panels can form:

32 × 8 pixels

and four panels can form:

64 × 8 pixels

or:

32 × 16 pixels

depending on the physical arrangement.

Custom Graphics

Small monochrome icons are very easy to define as arrays or strings. For example, the supplied test program stores the heart as a small character map:

static const char *heart[] =
{
    " 11 11 ",
    "1111111",
    "1111111",
    "1111111",
    " 11111 ",
    "  111  ",
    "   1   "
};

The same technique can be used for Wi-Fi icons, battery symbols, arrows, weather icons or custom status indicators.

PCB Assembly

With 128 LEDs on a relatively small PCB, LED orientation is the most important part of assembly. A single reversed LED will remain dark even if the rest of the matrix is operating correctly.

My preferred assembly sequence is:

  1. Solder the HT16K33.
  2. Solder the two 4.7 kΩ resistors.
  3. Solder the 1 µF and 100 nF capacitors.
  4. Solder the four-pin connector.
  5. Install the 0805 LEDs in groups.
  6. Check LED polarity before continuing with the next group.
  7. Inspect all solder joints under magnification.
  8. Check VDD to GND resistance before applying power.
  9. Connect the controller through the appropriate I2C level interface.
  10. Run the all-LED test.
  11. Run the row and column scans.
  12. Run the snake test to confirm pixel order.

First Power-Up

Before connecting the ESP32, I recommend checking the PCB for shorts with a multimeter. Pay particular attention to the HT16K33 pins because the SOP package has a much finer pitch than the 0805 LEDs.

After the hardware has been checked, start with a simple I2C scanner. The HT16K33 should appear at:

0x70

Once the device is visible on the bus, upload the test firmware. The first all-LED test immediately shows whether the matrix is basically functional.

Troubleshooting

HT16K33 is not detected at 0x70

No LEDs illuminate

One LED does not illuminate

An entire row or column is missing

The image is mirrored or rotated

This normally means the electrical matrix is working correctly but the software coordinate mapping does not match the desired physical orientation. Modify the transformation inside setPixelLandscape() rather than changing every graphics routine.

The display works but the ESP32 becomes unstable

Check the SDA and SCL voltage levels. If the HT16K33 board is powered from 5 V and its pull-up resistors are also connected to 5 V, do not connect those lines directly to ESP32 or ESP8266 GPIO pins. Use a suitable bidirectional level translator.

Power Supply Considerations

The current required by the panel depends on the number of illuminated LEDs, the selected brightness level, LED characteristics and multiplexing duty cycle. For testing and normal IoT indicators, I generally do not need to operate the matrix at maximum brightness continuously.

A stable 5 V supply with enough current headroom should be used for the LED board. The ESP32 can use its normal regulated supply, with both sides sharing a common ground through the I2C level-shifting arrangement.

Why Use the HT16K33 Instead of Driving the LEDs Directly?

A 128-LED matrix could theoretically be multiplexed directly from a microcontroller, but doing so would require many GPIO pins, additional driver circuitry and continuous timing-sensitive refresh code.

With the HT16K33:

For an IoT project this separation is especially useful because display timing remains independent of network activity.

Ideas for Future Revisions

The current board is intentionally simple, but several additions could make a future version even more convenient.

 

 

This project started as a simple way of building my own 16×8 LED matrix instead of using a commercial module, but it turned into a useful general-purpose display platform.

With 128 individual 0805 LEDs, a compact HT16K33 driver and a simple I2C interface, the panel can be used for anything from basic status indicators to scrolling text, animated graphics, sensor dashboards, clocks, counters and Wi-Fi-connected IoT displays.

The test firmware intentionally contains many different animations because it also serves as a hardware diagnostic program. The all-LED, row, column and snake tests make it easy to verify a newly assembled PCB, while the text, graphics, VU meters and game demonstrations provide examples that can be reused in other projects.

The PCB, schematic, Gerber files and test firmware are included below so the project can be reproduced or modified for other applications.

Suggested Media for This Article

 

 

Project Files

PCB Design Files Kicad ZIP / 341.1 KiB - ZIP File Kicad SCH & PCB Gerber Files for PCB Fabrication ZIP / 38.3 KiB - GRB Files All Layer Demo Code for ESP32 DEVKitC ZIP / 4.7 KiB - Arduino Code for Demo Test
License: CC BY-NC-SA 4.0
Personal and non-commercial use is allowed. Commercial use requires written permission from Aytac Gul.