Raspberry Pi Pico
4 x 7 Segment Display HT16K33

This page uses an Adafruit 0.56inch 7-segment display with an i2c backpack. The backpack contains an HT16K33 display controller. This is the same chip that is used to control the alphanumeric displays in lots of Raspberry Pi accessories. It is also used on a handful of other useful backpacks that Adafruit make. This display has a colon and a decimal place for each digit.

Without the backpack, there is a complex circuit required. The backpack also copes with the work of turning the correct LEDs on and off really quickly. With these displays, you can only light up the LEDs of a single digit at a time. To display more than one digit, the microcontroller has to switch between digits really quickly.

This display costs around £10 and is very easy to connect. It works at 3V with slightly less bright digits.

Pico Circuit

I made 4 connections for this circuit. It needs 3V power (to +), GND connected to the pin labelled -. The pin labelled D is the SDA pin and is connected to GP16. The pin labelled C is the SCL pin and is connected to 17.

The following is a library file that you can save to the Pico and keep separate from your main file. Save this file as ht16k33.py.

from machine import Pin, I2C

class backpack:
    ADDRESS             = 0x70
    BLINK_CMD           = 0x80
    CMD_BRIGHTNESS      = 0xE0
    # Digits 0 - F
    NUMS = [0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D,
        0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71]
       
    def __init__(self, d, c):
        self.buffer = bytearray([0]*16)
        self.i2c=I2C(0,sda=Pin(d), scl=Pin(c))
        self.i2c.writeto(self.ADDRESS,b'\x21')
        # 0 to 3
        self.blink_rate(0)
        # 0 to 15
        self.set_brightness(15)
        self.update_display()
    
    def set_brightness(self,b):
        self.i2c.writeto(self.ADDRESS,bytes([self.CMD_BRIGHTNESS | b]))       
    
    def blink_rate(self, b):
        self.i2c.writeto(self.ADDRESS,bytes([self.BLINK_CMD | 1 | (b << 1)]))
    
    def write_digit(self, position, digit, dot=False):
        # skip the colon
        offset = 0 if position < 2 else 1
        pos = offset + position
        self.buffer[pos*2] = self.NUMS[digit] & 0xFF
        if dot:
            self.buffer[pos*2] |= 0x80                    
    
    def update_display(self):
        data = bytearray([0]) + self.buffer
        self.i2c.writeto(self.ADDRESS,data)
    
    def print(self,value):
        if value<0 or value>9999:
            return
        sdig =  '{:04d}'.format(value)
        dts = [int(x) for x in sdig]
        for i,d in enumerate(dts):
            self.write_digit(i,d)
    
    def set_decimal(self, position, dot=True): 
        # skip the colon
        offset = 0 if position < 2 else 1
        pos = offset + position        
        if dot:
            self.buffer[pos*2] |= 0x80
        else:
            self.buffer[pos*2] &= 0x7F
        
    def clear(self):
        self.buffer = bytearray([0]*16)
        self.update_display()
    
    def set_colon(self, colon=True):
        if colon:
            self.buffer[4] |= 0x02
        else:
            self.buffer[4] &= 0xFD

Here is some test code to save as your main.py file,

from ht16k33 import backpack
from time import sleep

# declare an instance
f = backpack(16,17)
# decimals on
for i in range(4):
    f.set_decimal(i)
    f.update_display()
    sleep(0.1)
# decimals off
for i in range(4):
    f.set_decimal(i, False)
    f.update_display()
    sleep(0.1)

# print something
f.print(1234)
f.update_display()
sleep(0.1)
# clear the display
f.clear()
sleep(0.1)
# blink the colon
for i in range(4):
    f.set_colon()
    f.update_display()
    sleep(0.5)
    f.set_colon(False)
    f.update_display()
    sleep(0.5)       
# do some counting    
for i in range(10000):
    f.print(i)
    f.update_display()
    sleep(0.05)