Skip to Content

Elecrow 2.8inch Round IPS Display HDMI

Elecrow 2.8inch Round IPS Display HDMI

In this article, we’ll take a closer look at the Elecrow 2.8inch Round IPS Display that comes with an HDMI interface. This compact circular display features a 480 × 480 IPS panel, a Mini-HDMI interface, and a CNC-machined aluminum enclosure. Because it connects via HDMI, it can work with many devices such as the Raspberry Pi 4 Model B, mini PCs, or even standard desktop computers.

Round displays are becoming increasingly popular in maker projects. Their circular form factor is perfect for dashboards, gauges, clocks, and futuristic user interfaces that look more interesting than traditional rectangular screens. While many small round displays use SPI or I²C interfaces and require custom graphics libraries, HDMI displays provide a much simpler approach: they behave like a regular monitor.

We’ll explore the display’s specifications, hardware design, compatibility, and implement a nice analog clock that fits perfectly on this display.

Where to Buy

2.8inch Round IPS Display

Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.

Hardware of the Elecrow 2.8inch Round IPS Display

The module uses a 2.8-inch circular IPS TFT display panel. The panel has a resolution of 480 × 480 pixels, which produces a square pixel grid arranged inside a circular viewing area. The active display area measures approximately 70.13 mm by 70.13 mm. This resolution provides enough pixel density to render smooth graphics, icons, and text on a small screen.

The IPS panel technology provides wide viewing angles and consistent colors when viewed from different directions. Typical IPS displays offer viewing angles close to 178 degrees. This is useful for embedded projects where the display may be mounted at different orientations.

Display Driver Controller

The display is controlled by the ST7701S display driver IC. This controller is responsible for converting incoming video signals into the signals required to drive the TFT pixels. The controller manages pixel addressing, timing generation, and color processing inside the panel. It also controls the internal display scanning process that refreshes the image continuously.

Video Interface

The display uses a Mini-HDMI interface for video input. HDMI is a digital video interface that transmits pixel data from a host device to the display. The host device generates the graphical content and sends the video signal to the display through the HDMI connection. This allows the module to operate like a small external monitor.

Compatibility

Thanks to its HDMI interface, the display is compatible with a wide variety of devices. It can be used with Raspberry Pi boards, Linux single-board computers, mini PCs, Windows computers, and even media players. For example, it works well with compact systems such as the Raspberry Pi Zero 2 W as well as more powerful devices like the Raspberry Pi 4 Model B. Most operating systems, including Raspberry Pi OS, Ubuntu, Kali Linux, and Windows 7, 10, and 11, recognize the display automatically because it functions like a standard monitor.

Electrical Characteristics

The display operates from a 5 V power supply. Power is provided through a USB-C connector on the module. The typical power consumption is approximately 0.9 W during normal operation.

Front and back of the Display (source)

The display brightness is approximately 250 cd/m². The backlight uses white LEDs to illuminate the TFT panel from behind. A pinhole button on the module allows the user to adjust the backlight brightness in steps of about ten percent. Holding the button can turn the backlight off completely.

Mechanical Design

The module is housed in a CNC-machined aluminum enclosure. The housing measures about 89.5 mm by 89.5 mm with a thickness of roughly 10 mm. The total weight of the display is about 85 grams. The aluminum enclosure provides structural rigidity and also helps to dissipate heat generated by the display electronics and backlight.

Technical Specification

The following table summarizes the technical specification of the display:

FeatureSpecification
Display size2.8 inches
Resolution480 × 480 pixels
Display typeIPS
Driver ICST7701S
Brightness~250 cd/m²
InterfaceMini-HDMI video
PowerUSB-C
Viewing angleWide IPS viewing angles
Power consumption~0.9 W
Dimensions89.5 × 89.5 × 10 mm
Weight~85 g

HDMI vs SPI Displays

A quick word about the differences between HDMI displays and the SPI displays that are frequently used in conjunction with microcontrollers such as the Arduino or the ESP32. Here is how HDMI displays compare to typical SPI displays:

FeatureHDMI DisplaySPI Display
Setup complexityVery simpleRequires libraries
Graphics performanceGPU-acceleratedLimited
Software supportFull operating systemsCustom firmware
Interface capabilityFull desktop GUIBasic graphics

With an HDMI display, you can run full graphical applications. This opens the door to much richer interfaces. But you will also need a more powerful computer, e.g. a Raspberry PI, since you generally cannot connect an HDMI display to an Arduino or ESP32 board without extra hardware.

Typical Applications

The circular form factor makes this display particularly well suited for visual interfaces that use radial or symmetrical layouts. Common applications include Analog Clocks, IoT dashboards, Media Player Interfaces or Smart Home Control Panels, for instance.

In the following section you will learn how to implement an Analog Clock that looks perfect on this display.

Code Example: Analog Clock

The code below implements an analog clock with day and date information. The following photo show how the clock looks like on the 2.8inch Round IPS Display:

Analog Clock on 2.8inch Round IPS Display
Analog Clock on 2.8inch Round IPS Display

The code uses the Pygame library to render a smooth, visually appealing clock face with hour, minute, and second hands, along with date and day information. The clock uses 2× supersampling for anti-aliasing, drawing everything at double resolution and then scaling down for a crisp output.

Have a quick look at the code first and in the following sections we will discuss the details of the code.

"""
www.makerguides.com
Analog Clock for 480x480 Round IPS Display
Designed for Elecrow 2.8" round display (or any 480x480 screen).

Install:    pip install pygame
Run:        python analog_clock.py
Fullscreen: press F11 to toggle

On Raspberry Pi without a desktop (framebuffer):
    SDL_VIDEODRIVER=fbcon SDL_FBDEV=/dev/fb0 python analog_clock.py
"""

import pygame
import math
import datetime
import sys

# ── Configuration ─────────────────────────────────────────────────────────────

WIDTH, HEIGHT = 480, 480
FPS = 30
SCALE = 2
SW, SH = WIDTH * SCALE, HEIGHT * SCALE   # render-surface size

CX, CY = SW // 2, SH // 2               # center in render-space
R  = 228 * SCALE                        # outer dial radius in render-space

def S(v):
    """Scale a display-space value to render-space."""
    return int(v * SCALE)

# ── Palette — refined dark "pilot watch" ──────────────────────────────────────
COL_BG          = (  8,  10,  16)
COL_FACE        = ( 12,  16,  26)
COL_BEZEL       = ( 30,  36,  52)
COL_BEZEL_LIGHT = ( 58,  68,  92)
COL_HOUR_MARK   = (220, 214, 196)
COL_MIN_MARK    = ( 90,  96, 115)
COL_NUMERAL     = (210, 204, 186)
COL_HAND_H      = (218, 210, 190)
COL_HAND_M      = (218, 210, 190)
COL_HAND_S      = (220,  80,  50)
COL_CENTER_DOT  = (220,  80,  50)
COL_DATE_BG     = ( 22,  28,  44)
COL_DATE_FG     = (200, 196, 178)

# ── Helpers ───────────────────────────────────────────────────────────────────

def polar(angle_deg, radius):
    """Clock angle (0° = 12, clockwise) → render-space (x, y)."""
    rad = math.radians(angle_deg - 90)
    return (
        int(CX + radius * math.cos(rad)),
        int(CY + radius * math.sin(rad)),
    )


def draw_tapered_hand(surf, col, base_w, tip_w, tail_r, tip_r, angle):
    """Tapered trapezoid hand with a short tail behind centre."""
    perp = math.radians(angle)
    nx, ny = math.cos(perp), math.sin(perp)

    bx, by = polar(angle, -tail_r)
    tx, ty = polar(angle,  tip_r)

    hw_b = base_w / 2
    hw_t = tip_w  / 2

    pts = [
        (bx + nx * hw_b, by + ny * hw_b),
        (tx + nx * hw_t, ty + ny * hw_t),
        (tx - nx * hw_t, ty - ny * hw_t),
        (bx - nx * hw_b, by - ny * hw_b),
    ]
    pygame.draw.polygon(surf, col, [(int(x), int(y)) for x, y in pts])


def draw_aa_line(surf, col, p1, p2, width):
    """Polygon-based thick line with rounded tip."""
    x1, y1 = p1
    x2, y2 = p2
    length = math.hypot(x2 - x1, y2 - y1)
    if length == 0:
        return
    ux = -(y2 - y1) / length * (width / 2)
    uy =  (x2 - x1) / length * (width / 2)
    pts = [(x1+ux, y1+uy), (x2+ux, y2+uy), (x2-ux, y2-uy), (x1-ux, y1-uy)]
    pygame.draw.polygon(surf, col, pts)
    pygame.draw.circle(surf, col, (int(x2), int(y2)), width // 2)


# ── Static dial ───────────────────────────────────────────────────────────────

def draw_dial(surf, font_num, font_date):
    """Render the fixed parts of the dial.  Returns the date-box Rect."""

    surf.fill(COL_BG)
    pygame.draw.circle(surf, COL_FACE, (CX, CY), R)
    pygame.draw.circle(surf, COL_BEZEL,       (CX, CY), R,        S(4))
    pygame.draw.circle(surf, COL_BEZEL_LIGHT, (CX, CY), R - S(5), S(1))

    # Tick marks
    for m in range(60):
        is_hour = (m % 5 == 0)
        outer = R - S(8)
        inner = R - S(22) if is_hour else R - S(15)
        width = S(3)      if is_hour else S(1)
        col   = COL_HOUR_MARK if is_hour else COL_MIN_MARK
        pygame.draw.line(surf, col, polar(m * 6, outer), polar(m * 6, inner), width)

    # Cardinal numerals
    for hour, angle in [(12, 0), (3, 90), (6, 180), (9, 270)]:
        txt  = font_num.render(str(hour), True, COL_NUMERAL)
        pos  = polar(angle, R - S(52))
        surf.blit(txt, txt.get_rect(center=pos))

    # ── Two separate date windows ──────────────────────────────────────────────
    DAY_NAMES_ALL = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
    pad_x, pad_y = S(12), S(6)
    line_h = font_date.get_height()

    # Both boxes share the same width — sized to the widest possible content
    box_w = max(
        max(font_date.size(d)[0] for d in DAY_NAMES_ALL),
        font_date.size('00/00/00')[0]
    ) + pad_x * 2
    box_h = line_h + pad_y * 2

    # Top box (day name) — 70 display-px above centre
    dw_day = pygame.Rect(0, 0, box_w, box_h)
    dw_day.center = polar(0, S(70))

    pygame.draw.rect(surf, COL_DATE_BG, dw_day, border_radius=S(5))
    pygame.draw.rect(surf, COL_BEZEL,   dw_day, width=S(1), border_radius=S(5))

    # Bottom box (numeric date) — 70 display-px below centre
    dw_date = pygame.Rect(0, 0, box_w, box_h)
    dw_date.center = polar(180, S(70))

    pygame.draw.rect(surf, COL_DATE_BG, dw_date, border_radius=S(5))
    pygame.draw.rect(surf, COL_BEZEL,   dw_date, width=S(1), border_radius=S(5))

    return dw_day, dw_date


# ── Animated hands + date ─────────────────────────────────────────────────────

def draw_hands(surf, now, dw_day, dw_date, font_date):
    sec  = now.second + now.microsecond / 1e6
    mins = now.minute + sec  / 60
    hrs  = (now.hour % 12) + mins / 60

    angle_h = hrs  * 30
    angle_m = mins *  6
    angle_s = sec  *  6

    # Hour hand
    draw_tapered_hand(surf, COL_HAND_H,
                      base_w=S(12), tip_w=S(5),
                      tail_r=S(20), tip_r=S(120), angle=angle_h)

    # Minute hand
    draw_tapered_hand(surf, COL_HAND_M,
                      base_w=S(9),  tip_w=S(3),
                      tail_r=S(24), tip_r=S(175), angle=angle_m)

    # Second hand — thin needle + counterweight lollipop
    draw_aa_line(surf, COL_HAND_S,
                 polar(angle_s, -S(38)),
                 polar(angle_s,  S(190)), S(2))
    pygame.draw.circle(surf, COL_HAND_S, polar(angle_s, -S(28)), S(6))

    # Centre pinion
    pygame.draw.circle(surf, COL_BEZEL,      (CX, CY), S(11))
    pygame.draw.circle(surf, COL_CENTER_DOT, (CX, CY),  S(8))
    pygame.draw.circle(surf, (255, 255, 255),(CX, CY),  S(2))

    # Day name in top box
    DAY_NAMES = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
    t_day = font_date.render(DAY_NAMES[now.weekday()], True, COL_DATE_FG)
    surf.blit(t_day, t_day.get_rect(center=dw_day.center))

    # Numeric date in bottom box
    t_date = font_date.render(
        f"{now.day:02d}/{now.month:02d}/{now.year % 100:02d}", True, COL_DATE_FG)
    surf.blit(t_date, t_date.get_rect(center=dw_date.center))


# ── Main ──────────────────────────────────────────────────────────────────────

def make_screen(fullscreen):
    """Create (or recreate) the display surface."""
    if fullscreen:
        flags = pygame.FULLSCREEN | pygame.NOFRAME
        # Use the native desktop resolution so the OS doesn't letterbox us
        info  = pygame.display.Info()
        return pygame.display.set_mode(
            (info.current_w, info.current_h), flags)
    else:
        flags = pygame.NOFRAME if "--kiosk" in sys.argv else 0
        return pygame.display.set_mode((WIDTH, HEIGHT), flags)


def main():
    pygame.init()

    fullscreen = "--fullscreen" in sys.argv or "-f" in sys.argv
    screen = make_screen(fullscreen)
    pygame.display.set_caption("Analog Clock")
    pygame.mouse.set_visible(False)

    clock = pygame.time.Clock()

    def load_font(display_size, bold=False):
        """Load a font at 2× size to match the render surface."""
        size = display_size * SCALE
        for name in ("DejaVu Sans", "Liberation Sans", "FreeSans", None):
            try:
                return pygame.font.SysFont(name, size, bold=bold)
            except Exception:
                continue
        return pygame.font.Font(None, size)

    font_num  = load_font(32, bold=True)
    font_date = load_font(20, bold=True)

    # Offscreen render surface at 2× resolution
    render_surf = pygame.Surface((SW, SH))

    # Pre-draw static dial onto its own surface
    dial_surf = pygame.Surface((SW, SH))
    dw_day, dw_date = draw_dial(dial_surf, font_num, font_date)

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit(); sys.exit()
            if event.type == pygame.KEYDOWN:
                if event.key in (pygame.K_ESCAPE, pygame.K_q):
                    pygame.quit(); sys.exit()
                if event.key == pygame.K_F11:
                    fullscreen = not fullscreen
                    screen = make_screen(fullscreen)
                    pygame.mouse.set_visible(not fullscreen)

        now = datetime.datetime.now()

        # Scale the 2× render surface to fill the screen (letterboxed if needed)
        sw, sh = screen.get_size()
        # Fit the clock square into the screen, centred
        side   = min(sw, sh)
        ox, oy = (sw - side) // 2, (sh - side) // 2
        scaled = pygame.transform.smoothscale(render_surf, (side, side))

        # Composite: static dial + animated hands onto render surface
        render_surf.blit(dial_surf, (0, 0))
        draw_hands(render_surf, now, dw_day, dw_date, font_date)
        scaled = pygame.transform.smoothscale(render_surf, (side, side))

        screen.fill((0, 0, 0))          # black bars on non-square screens
        screen.blit(scaled, (ox, oy))

        pygame.display.flip()
        clock.tick(FPS)


if __name__ == "__main__":
    main()

Imports

The code starts by importing essential Python modules. pygame is used for graphics rendering and event handling. Since pygame is not a standard library you will have to install it via

pip install pygame

The math module provides mathematical functions for angle and coordinate calculations. datetime is used to get the current time and date, and sys handles command-line arguments and system exit.

import pygame
import math
import datetime
import sys

Configuration Constants

Several constants define the display size and rendering parameters. The clock face is 480×480 pixels, but to achieve smooth edges, the code uses 2× supersampling, rendering at 960×960 pixels internally (SCALE = 2). The center coordinates and outer dial radius are calculated in this higher-resolution render space.

WIDTH, HEIGHT = 480, 480
FPS = 30

SCALE = 2
SW, SH = WIDTH * SCALE, HEIGHT * SCALE

CX, CY = SW // 2, SH // 2
R  = 228 * SCALE

Scaling Helper Function

The S(v) function converts display-space values to render-space by multiplying by the scale factor. This ensures consistent sizing in the supersampled render surface.

def S(v):
    """Scale a display-space value to render-space."""
    return int(v * SCALE)

Color Palette

A carefully chosen color palette defines the clock’s appearance, inspired by dark “pilot watch” aesthetics. Colors are specified as RGB tuples for background, dial face, bezel, hour and minute marks, numerals, hands, center dot, and date display.

COL_BG          = (  8,  10,  16)
COL_FACE        = ( 12,  16,  26)
COL_BEZEL       = ( 30,  36,  52)
COL_BEZEL_LIGHT = ( 58,  68,  92)
COL_HOUR_MARK   = (220, 214, 196)
COL_MIN_MARK    = ( 90,  96, 115)
COL_NUMERAL     = (210, 204, 186)
COL_HAND_H      = (218, 210, 190)
COL_HAND_M      = (218, 210, 190)
COL_HAND_S      = (220,  80,  50)
COL_CENTER_DOT  = (220,  80,  50)
COL_DATE_BG     = ( 22,  28,  44)
COL_DATE_FG     = (200, 196, 178)

Coordinate Conversion: polar() Function

The polar(angle_deg, radius) function converts clock angles (where 0° corresponds to 12 o’clock and angles increase clockwise) into Cartesian coordinates on the render surface. It adjusts the angle by -90° to align 0° with the top and returns integer pixel positions relative to the center.

def polar(angle_deg, radius):
    """Clock angle (0° = 12, clockwise) → render-space (x, y)."""
    rad = math.radians(angle_deg - 90)
    return (
        int(CX + radius * math.cos(rad)),
        int(CY + radius * math.sin(rad)),
    )

Drawing Clock Hands: draw_tapered_hand() and draw_aa_line()

The draw_tapered_hand() function draws a tapered trapezoidal hand with a short tail behind the center. It calculates perpendicular vectors to the hand’s angle to create a polygon representing the hand’s shape and then draws it filled with the specified color.

def draw_tapered_hand(surf, col, base_w, tip_w, tail_r, tip_r, angle):
    """Tapered trapezoid hand with a short tail behind centre."""
    perp = math.radians(angle)
    nx, ny = math.cos(perp), math.sin(perp)

    bx, by = polar(angle, -tail_r)
    tx, ty = polar(angle,  tip_r)

    hw_b = base_w / 2
    hw_t = tip_w  / 2

    pts = [
        (bx + nx * hw_b, by + ny * hw_b),
        (tx + nx * hw_t, ty + ny * hw_t),
        (tx - nx * hw_t, ty - ny * hw_t),
        (bx - nx * hw_b, by - ny * hw_b),
    ]
    pygame.draw.polygon(surf, col, [(int(x), int(y)) for x, y in pts])

The draw_aa_line() function draws a thick line with rounded ends between two points, used for the second hand needle. It creates a polygon for the line body and draws a circle at the tip for smooth edges.

def draw_aa_line(surf, col, p1, p2, width):
    """Polygon-based thick line with rounded tip."""
    x1, y1 = p1
    x2, y2 = p2
    length = math.hypot(x2 - x1, y2 - y1)
    if length == 0:
        return
    ux = -(y2 - y1) / length * (width / 2)
    uy =  (x2 - x1) / length * (width / 2)
    pts = [(x1+ux, y1+uy), (x2+ux, y2+uy), (x2-ux, y2-uy), (x1-ux, y1-uy)]
    pygame.draw.polygon(surf, col, pts)
    pygame.draw.circle(surf, col, (int(x2), int(y2)), width // 2)

Drawing the Static Dial: draw_dial() Function

The draw_dial() function renders the fixed parts of the clock face onto a surface. It fills the background, draws the circular dial face and bezel with multiple rings for depth, and adds tick marks for minutes and hours. Hour marks are thicker and longer than minute marks.

It also draws cardinal numerals (12, 3, 6, 9) positioned appropriately around the dial. Two date windows are created: one above the center for the day name and one below for the numeric date. These boxes have rounded corners and are sized dynamically to fit the widest possible content.

def draw_dial(surf, font_num, font_date):
    """Render the fixed parts of the dial.  Returns the date-box Rect."""

    surf.fill(COL_BG)
    pygame.draw.circle(surf, COL_FACE, (CX, CY), R)
    pygame.draw.circle(surf, COL_BEZEL,       (CX, CY), R,        S(4))
    pygame.draw.circle(surf, COL_BEZEL_LIGHT, (CX, CY), R - S(5), S(1))

    # Tick marks
    for m in range(60):
        is_hour = (m % 5 == 0)
        outer = R - S(8)
        inner = R - S(22) if is_hour else R - S(15)
        width = S(3)      if is_hour else S(1)
        col   = COL_HOUR_MARK if is_hour else COL_MIN_MARK
        pygame.draw.line(surf, col, polar(m * 6, outer), polar(m * 6, inner), width)

    # Cardinal numerals
    for hour, angle in [(12, 0), (3, 90), (6, 180), (9, 270)]:
        txt  = font_num.render(str(hour), True, COL_NUMERAL)
        pos  = polar(angle, R - S(52))
        surf.blit(txt, txt.get_rect(center=pos))

    # ── Two separate date windows ──────────────────────────────────────────────
    DAY_NAMES_ALL = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
    pad_x, pad_y = S(12), S(6)
    line_h = font_date.get_height()

    # Both boxes share the same width — sized to the widest possible content
    box_w = max(
        max(font_date.size(d)[0] for d in DAY_NAMES_ALL),
        font_date.size('00/00/00')[0]
    ) + pad_x * 2
    box_h = line_h + pad_y * 2

    # Top box (day name) — 70 display-px above centre
    dw_day = pygame.Rect(0, 0, box_w, box_h)
    dw_day.center = polar(0, S(70))

    pygame.draw.rect(surf, COL_DATE_BG, dw_day, border_radius=S(5))
    pygame.draw.rect(surf, COL_BEZEL,   dw_day, width=S(1), border_radius=S(5))

    # Bottom box (numeric date) — 70 display-px below centre
    dw_date = pygame.Rect(0, 0, box_w, box_h)
    dw_date.center = polar(180, S(70))

    pygame.draw.rect(surf, COL_DATE_BG, dw_date, border_radius=S(5))
    pygame.draw.rect(surf, COL_BEZEL,   dw_date, width=S(1), border_radius=S(5))

    return dw_day, dw_date

Drawing Animated Hands and Date: draw_hands() Function

The draw_hands() function updates the clock hands and date display based on the current time. It calculates smooth angles for the hour, minute, and second hands, including fractional seconds for fluid motion.

The hour and minute hands are drawn as tapered polygons with tails behind the center. The second hand is a thin anti-aliased line with a circular counterweight behind the center. The center pinion is drawn as concentric circles for a polished look.

The function also renders the day name in the top date box and the numeric date in the bottom box, using the provided fonts and colors.

def draw_hands(surf, now, dw_day, dw_date, font_date):
    sec  = now.second + now.microsecond / 1e6
    mins = now.minute + sec  / 60
    hrs  = (now.hour % 12) + mins / 60

    angle_h = hrs  * 30
    angle_m = mins *  6
    angle_s = sec  *  6

    # Hour hand
    draw_tapered_hand(surf, COL_HAND_H,
                      base_w=S(12), tip_w=S(5),
                      tail_r=S(20), tip_r=S(120), angle=angle_h)

    # Minute hand
    draw_tapered_hand(surf, COL_HAND_M,
                      base_w=S(9),  tip_w=S(3),
                      tail_r=S(24), tip_r=S(175), angle=angle_m)

    # Second hand — thin needle + counterweight lollipop
    draw_aa_line(surf, COL_HAND_S,
                 polar(angle_s, -S(38)),
                 polar(angle_s,  S(190)), S(2))
    pygame.draw.circle(surf, COL_HAND_S, polar(angle_s, -S(28)), S(6))

    # Centre pinion
    pygame.draw.circle(surf, COL_BEZEL,      (CX, CY), S(11))
    pygame.draw.circle(surf, COL_CENTER_DOT, (CX, CY),  S(8))
    pygame.draw.circle(surf, (255, 255, 255),(CX, CY),  S(2))

    # Day name in top box
    DAY_NAMES = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
    t_day = font_date.render(DAY_NAMES[now.weekday()], True, COL_DATE_FG)
    surf.blit(t_day, t_day.get_rect(center=dw_day.center))

    # Numeric date in bottom box
    t_date = font_date.render(
        f"{now.day:02d}/{now.month:02d}/{now.year % 100:02d}", True, COL_DATE_FG)
    surf.blit(t_date, t_date.get_rect(center=dw_date.center))

Display Setup: make_screen() Function

The make_screen() function creates or recreates the Pygame display surface. It supports fullscreen mode with no window frame and uses the native desktop resolution to avoid letterboxing. In windowed mode, it creates a fixed-size window matching the configured clock size.

def make_screen(fullscreen):
    """Create (or recreate) the display surface."""
    if fullscreen:
        flags = pygame.FULLSCREEN | pygame.NOFRAME
        info  = pygame.display.Info()
        return pygame.display.set_mode(
            (info.current_w, info.current_h), flags)
    else:
        flags = pygame.NOFRAME if "--kiosk" in sys.argv else 0
        return pygame.display.set_mode((WIDTH, HEIGHT), flags)

Main Program Loop: main() Function

The main() function initializes Pygame, sets up fonts scaled for supersampling, and creates the render and dial surfaces. It pre-renders the static dial to optimize performance.

The main loop handles user input events such as quitting, toggling fullscreen with F11, and hiding the mouse cursor in fullscreen mode. It continuously fetches the current time, composites the static dial with the animated hands, scales the render surface to fit the screen while preserving aspect ratio, and updates the display at 30 frames per second.

This structure ensures smooth animation and responsive user interaction.

def main():
    pygame.init()

    fullscreen = "--fullscreen" in sys.argv or "-f" in sys.argv
    screen = make_screen(fullscreen)
    pygame.display.set_caption("Analog Clock")
    pygame.mouse.set_visible(False)

    clock = pygame.time.Clock()

    def load_font(display_size, bold=False):
        size = display_size * SCALE
        for name in ("DejaVu Sans", "Liberation Sans", "FreeSans", None):
            try:
                return pygame.font.SysFont(name, size, bold=bold)
            except Exception:
                continue
        return pygame.font.Font(None, size)

    font_num  = load_font(32, bold=True)
    font_date = load_font(20, bold=True)

    render_surf = pygame.Surface((SW, SH))
    dial_surf = pygame.Surface((SW, SH))
    dw_day, dw_date = draw_dial(dial_surf, font_num, font_date)

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit(); sys.exit()
            if event.type == pygame.KEYDOWN:
                if event.key in (pygame.K_ESCAPE, pygame.K_q):
                    pygame.quit(); sys.exit()
                if event.key == pygame.K_F11:
                    fullscreen = not fullscreen
                    screen = make_screen(fullscreen)
                    pygame.mouse.set_visible(not fullscreen)

        now = datetime.datetime.now()

        sw, sh = screen.get_size()
        side   = min(sw, sh)
        ox, oy = (sw - side) // 2, (sh - side) // 2

        render_surf.blit(dial_surf, (0, 0))
        draw_hands(render_surf, now, dw_day, dw_date, font_date)
        scaled = pygame.transform.smoothscale(render_surf, (side, side))

        screen.fill((0, 0, 0))
        screen.blit(scaled, (ox, oy))

        pygame.display.flip()
        clock.tick(FPS)

Program Entry Point

The script runs the main() function when executed directly, starting the clock application.

if __name__ == "__main__":
    main()

Run the Clock app

Here a the commands and parameters you can use to run the analog clock app:

# Run in Window
python analog_clock.py

# Run windowed, press F11 to toggle fullscreen
python analog_clock.py

# Run in fullscreen immediately
python analog_clock.py --fullscreen
python analog_clock.py -f

# On Raspberry Pi with a desktop
python analog_clock.py --kiosk

# On Raspberry Pi without a desktop (raw framebuffer)
SDL_VIDEODRIVER=fbcon SDL_FBDEV=/dev/fb0 python analog_clock.py --kiosk

Press Q or Esc to quit. --kiosk removes the window frame and hides the mouse cursor.

When fullscreen, the clock square is centred and letterboxed with black bars on non-square monitors — so it always looks right whether you’re on a 16:9 desktop screen or the round 480×480 display itself.
The mouse cursor is hidden automatically when going fullscreen and restored when going back to windowed.

On the Pi in framebuffer mode (SDL_VIDEODRIVER=fbcon), just pass –fullscreen and it will fill /dev/fb0 at its native resolution.

Conclusion

The Elecrow 2.8-inch round HDMI display is a unique and versatile option for makers who want something more visually interesting than a traditional rectangular screen.

Its 480 × 480 IPS panel, Mini-HDMI interface, and aluminum housing make it both practical and aesthetically appealing. Because it works like a standard monitor, it can easily connect to devices such as the Raspberry Pi and other small computers without requiring specialized display libraries.

The IPS panel provides wide viewing angles and good color reproduction, while the circular format creates a distinctive appearance that works well for dashboards, gauges, and clocks.

However, the display requires a device with HDMI output and therefore cannot be connected directly to most microcontrollers. It is also more expensive than simple SPI displays, and the circular screen may require custom user interface designs.

However, if you are building a dashboard, clock, cyberdeck, or smart home interface, this round display can add a distinctive visual element that immediately stands out.

If you want to use a round display with an ESP32 have a look at our Digital Clock on CrowPanel 1.28″ Round Display tutorial. And you may also be interested in the LED Ring Clock with WS2812 or the Flip Clock with LILYGO T-Display S3 Long tutorial for different clock designs.

Enjoy the display, and if you have any questions feel free to leave them in the comment section.

Happy Tinkering 😉