---
title: "Real-Time UDP Pixel Display & Stage Visualizer"
url: "https://maker.wiznet.io/TheoIm/projects/real-time-udp-pixel-display-stage-visualizer/"
markdown_url: "https://maker.wiznet.io/TheoIm/projects/real-time-udp-pixel-display-stage-visualizer/md"
type: "UCC: User Created Content"
author: "Jonathan L"
author_url: "https://www.hackster.io/happypikachoo/real-time-udp-pixel-display-stage-visualizer-part-1-f67f21"
editor: "WIZnet"
editor_url: "https://maker.wiznet.io/"
original_author: "Jonathan L"
original_url: "https://www.hackster.io/happypikachoo/real-time-udp-pixel-display-stage-visualizer-part-1-f67f21"
published: "2026-10-08"
updated: "2026-10-12"
language: "en"
hardware: ["WiznetHK W55MH32Q_EVB"]
likes: 0
views: 76
comments: 0
source: "WIZnet Makers (https://maker.wiznet.io/)"
---

# Real-Time UDP Pixel Display & Stage Visualizer

> A W55MH32L-EVB paints 772-byte RGB frames streamed over UDP onto an 8x32 WS2812B matrix, counting dropped frames on an OLED instead of hiding them.

Original author: Jonathan L (source: https://www.hackster.io/happypikachoo/real-time-udp-pixel-display-stage-visualizer-part-1-f67f21)

## Components

- **WiznetHK W55MH32Q_EVB** x 1

## Article

## Overview

**Real-Time UDP Pixel Display & Stage Visualizer** turns a W55MH32L-EVB into a network-controlled LED stage fixture. A host PC streams 772-byte RGB frames to the board over UDP through its on-board WIZnet W5500 Ethernet controller; the board paints them onto an 8×32 WS2812B matrix, reports its own IP, frame rate and dropped-frame count on an SSD1306 OLED, and sends a rotary encoder's position back to the PC on a second UDP port so the host can use it as a live control input.

The author publishes it as Part 1 of a series under GPL3+, with the complete MicroPython source walked through section by section.

이 프로젝트는 W55MH32L-EVB를 네트워크로 제어되는 LED 무대 조명 장치로 만듭니다. PC가 UDP로 772바이트 RGB 프레임을 보내면 보드가 8×32 WS2812B 매트릭스에 출력하고, 자신의 IP·프레임률·누락 프레임 수를 SSD1306 OLED에 표시하며, 로터리 엔코더 값을 별도 UDP 포트로 PC에 되돌려 보냅니다.

## Purpose

The author's earlier projects in this series used TCP. This one deliberately does not, and the reasoning is stated rather than assumed: TCP guarantees that data arrives exactly as sent, and pays for it with connection setup and retransmission. For LED animation that trade runs the wrong way — **if one frame is lost, showing the next frame is more useful than waiting for the missing one to be retransmitted.**

The interesting part is what follows from that decision. Having given up the transport's guarantees, the firmware builds its own minimum: a magic header so foreign packets cannot be painted onto the matrix, a frame number so gaps are visible, and a counter on the display so the cost of the choice is something you can read off the device.

**Core Objectives**

- Receive RGB animation frames over UDP with the lowest practical delay

- Reject malformed or unrelated packets before they reach the LEDs

- Detect and count missing or stale frames rather than hide them

- Keep the control loop responsive while frames are arriving

- Send control telemetry back to the host on the same wired link

TCP 대신 UDP를 택한 이유를 저자가 직접 밝히고 있습니다. 프레임 하나가 유실되면 재전송을 기다리기보다 다음 프레임을 보여주는 편이 낫기 때문입니다. 중요한 것은 그 다음입니다 — 전송 계층의 보장을 포기한 대신, 펌웨어가 매직 헤더·프레임 번호·드롭 카운터라는 최소한의 장치를 직접 만들어 그 대가를 **측정 가능한 형태**로 남겨 두었습니다.

## System Architecture

```
Host PC
   |  UDP 6454   772-byte RGB frames
   v
W55MH32L-EVB
   |- W5500 Ethernet   internal SPI2, CS PB12, RST PD9
   |- WS2812B 8x32     DIN PA7          (256 LEDs)
   |- SSD1306 OLED     I2C1 PB6/PB7, 0x3D
   |- Rotary encoder   PA0 / PA1, interrupt driven
   |
   |  UDP 6455   encoder telemetry
   v
Host PC
```

| Component | Description |
| --- | --- |
| Controller | W55MH32L-EVB running MicroPython, developed in Thonny. STM32-style pin naming (PA/PB/PD). |
| Network | WIZnet W5500 on the board's internal SPI2 bus, brought up with DHCP. |
| Display output | 8×32 WS2812B flexible matrix, 256 pixels, driven from a single data pin. |
| Diagnostics | SSD1306 OLED over I2C1, refreshed periodically so it does not interfere with frame processing. |
| Control input | Rotary encoder on two interrupt-capable pins with internal pull-ups; the ISR records movement and the main loop applies it. |
| Protocol | Custom 772-byte UDP frame: 2-byte magic header, frame number, and 3 bytes × 256 LEDs of RGB payload. |

## Role and Application of the W5500

**The wired link is the point, not an accessory.** A stage fixture that drops frames because of Wi-Fi contention is a fixture that visibly stutters in front of an audience. Ethernet provides a more predictable and contention-resistant link than Wi-Fi for this application, and the project's whole diagnostic design — counting drops and showing them — only makes sense on a link where a drop is an event rather than background noise.

**How the firmware brings it up.**

```
nic = network.WIZNET5K(spi, cs_pin, reset_pin)
nic.active(True)
nic.ifconfig("dhcp")
```

The reset pin is pulsed low then high, with a 200 ms delay afterwards to let the chip start, before DHCP is requested. Sockets are then created through the standard MicroPython socket module.

The firmware initializes the W5500 using MicroPython's `network.WIZNET5K` interface and uses the standard `socket` API for UDP communication. The specific socket handling path depends on the MicroPython firmware build and has not been independently verified.

이 프로젝트는 MicroPython의 `network.WIZNET5K` 인터페이스로 W5500을 초기화하고, 표준 `socket` API로 UDP 통신을 구현합니다. 다만 실제 소켓 처리 경로는 사용된 MicroPython 펌웨어 빌드에서 별도로 확인되지 않았습니다.

## Key Features

| Feature | Description |
| --- | --- |
| Fixed-length frame validation | A valid frame is exactly 772 bytes; anything else is discarded before processing. 길이가 정확히 772바이트가 아니면 버립니다. |
| Magic header | A two-byte header keeps unrelated UDP traffic from being interpreted as LED data. 매직 헤더로 무관한 패킷을 걸러냅니다. |
| Frame-gap detection | A frame number is compared against the last accepted one; a jump is counted as a drop. 프레임 번호의 건너뜀을 드롭으로 집계합니다. |
| On-device diagnostics | IP, measured FPS, cumulative drops and encoder value on the OLED. IP·FPS·누적 드롭·노브 값을 OLED에 표시합니다. |
| Bounded receive drain | Up to four packets per loop pass, non-blocking, so a burst cannot starve the rest of the loop. 루프당 최대 4패킷까지만 처리합니다. |
| Serpentine index map | Logical (x, y) is translated to the panel's physical zigzag order once, at startup. 지그재그 배선을 시작 시 1회 해소합니다. |
| Return telemetry | Encoder position is sent to the host each loop on a separate UDP port. 엔코더 값을 매 루프 PC로 송신합니다. |

## How a frame is validated

The frame-processing function checks length and header before it touches a pixel:

```
if len(data) != EXPECTED_FRAME_SIZE:
    return False
if data[0:2] != FRAME_HEADER_MAGIC:
    return False
```

Only then is the frame number compared against `last_frame_id`, and the RGB payload unpacked three bytes at a time into the matrix. The board keeps `frame_count` for the current statistics period and `dropped_frames` as a running total.

That last counter is what makes the UDP decision honest. The controller does not pretend nothing was lost — it shows you how much.

## Keeping the loop responsive

Both sockets are non-blocking, and the receive function drains a bounded number of packets per pass:

```
rx_sock.bind(("0.0.0.0", UDP_RX_PORT))
rx_sock.setblocking(False)
tx_sock.setblocking(False)

MAX_PACKETS_PER_LOOP = 4
```

The reasoning given is that handling several packets per pass keeps the receive buffer from filling when the PC sends quickly, while the bound stops a burst from starving the encoder, the OLED and the telemetry path. When nothing is waiting, `OSError` breaks out immediately and control returns to the main loop.

The board listens on UDP port 6454 and sends telemetry on 6455. **Port 6454 is the port Art-Net uses, but this is not Art-Net** — the frame format described above is the project's own.

## The serpentine problem

The program treats the display logically as rows running left to right. The panel is not wired that way: each column holds eight LEDs, even columns run top to bottom and odd columns bottom to top. `build_index_map()` resolves the difference once, at startup:

```
if x % 2 == 0:
    physical_index = x * HEIGHT + y
else:
    physical_index = x * HEIGHT + (HEIGHT - 1 - y)
```

so the per-frame path is a flat lookup — `physical_index = index_map[logical_index]` — instead of arithmetic repeated 256 times for every frame. A small decision, but it is the difference between a mapping cost paid once and one paid at frame rate.

## Wiring

```
WS2812B 8x32 matrix   DIN  -> PA7          5 V, GND
Rotary encoder        CLK  -> PA0  (internal pull-up)
                      DT   -> PA1  (internal pull-up)
SSD1306 OLED          SCL  -> PB6 (I2C1)   SDA -> PB7, address 0x3D
W5500 (on board)      CS   -> PB12         RST -> PD9, hardware SPI2
```

![](https://maker.wiznet.io/upload/ckeditor5/7f6e33f03c994c4db003db70d54573e0.png)

## Engineering Value and Notes

The value of this project goes beyond simply driving LEDs over Ethernet. It shows a practical way to design a low-latency UDP application while keeping packet loss visible and measurable. Frame numbering, drop counting, and on-device diagnostics make it easy to understand what is happening on the network without hiding UDP's trade-offs.

The project demonstrates UDP communication using the W5500 through MicroPython's `network.WIZNET5K` interface and the standard `socket` API. The underlying socket implementation has not been verified for the specific firmware build.

A few points are worth keeping in mind when reproducing or extending the project:

- The firmware calculates and displays FPS on the OLED, although the article does not provide a specific measured frame-rate result.

- Dropped frames are counted, but no drop-rate measurement is provided for a particular sending rate.

- The host-side frame sender is not included in Part 1.

- The author presents this project as **Part 1 / work in progress**, leaving room for further development.

이 프로젝트의 가치는 단순히 Ethernet으로 LED를 제어한다는 데 있지 않습니다. UDP의 낮은 지연 특성을 활용하면서도 프레임 번호, 드롭 카운터, OLED 진단 정보를 통해 패킷 손실을 숨기지 않고 직접 확인할 수 있도록 구성한 점이 흥미롭습니다.

또한 MicroPython의 `network.WIZNET5K` 인터페이스와 표준 `socket` API를 사용해 W5500 기반 UDP 통신을 구현합니다. 다만 해당 펌웨어 빌드의 하드웨어 소켓 처리 경로는 별도로 검증되지 않았습니다.

재현하거나 확장할 때 참고할 점도 있습니다. 펌웨어에는 FPS 측정과 드롭 프레임 집계 기능이 구현되어 있지만, 게시글에는 특정 조건에서 측정한 FPS나 드롭률 수치는 제시되어 있지 않습니다. 또한 Part 1에는 PC 측 프레임 송신 프로그램이 포함되어 있지 않으며, 저자는 프로젝트를 **Part 1 / work in progress**로 소개하고 있습니다.

## Market & Application Value

**Stage and architectural lighting.** Networked pixel control is standard in this field, and the parts of the problem this project addresses — latency, frame integrity, and a technician being able to see at a glance whether the fixture is receiving — are the parts that decide whether an installation is trusted.

**A pattern beyond LEDs.** Fixed-size frames, a magic header, a sequence number and a visible drop counter make up a reusable recipe for any low-latency UDP control link: motion control, telemetry displays, sensor fan-out. The payload changes; the shape does not.

**Teaching value.** Because the author explains the TCP-versus-UDP trade before writing a line of socket code, and then shows the compensating machinery, the project reads as a worked lesson rather than a snippet — useful to anyone meeting UDP on an MCU for the first time.

네트워크 기반 픽셀 제어는 무대·건축 조명에서 이미 표준이며, 이 프로젝트가 다루는 지연·프레임 무결성·현장에서 수신 상태를 한눈에 확인하는 문제는 설치물의 신뢰도를 좌우하는 지점입니다. 고정 길이 프레임 + 매직 헤더 + 시퀀스 번호 + 드롭 카운터라는 조합은 LED에 한정되지 않고, 저지연 UDP 제어 링크 전반에 재사용할 수 있는 설계입니다.

## Source-Backed Summary

| Board | W55MH32L-EVB, W5500 on internal SPI2 (CS PB12, RST PD9) |
| --- | --- |
| Language | MicroPython, developed in Thonny |
| Transport | UDP — RX port 6454, TX port 6455, both non-blocking |
| Frame | 772 bytes: 2-byte magic + frame number + 3 bytes × 256 LEDs |
| Integrity | Length check, magic check, frame-number gap counting |
| Display | 8×32 WS2812B, serpentine mapping resolved once at startup |
| Diagnostics | SSD1306 shows IP, FPS, cumulative drops, encoder value |
| Return path | Rotary encoder value sent to the host each loop |
| Licence | GPL3+ |
| Status | Part 1, work in progress |

## FAQ

**Q. Why UDP instead of TCP for LED data?**
Because retransmission is worth less than low delay here. A retransmitted frame arrives after the moment it was meant for; the next frame is already more correct. The author states this explicitly as the reason for the change from earlier projects in the series.

**Q. If UDP can drop frames, how do you know it is working?**
That is what the frame number and the OLED drop counter are for. The controller does not assume delivery — it counts the gaps and displays the total, so the cost of the choice is visible on the device.

**Q. Does the project use the W5500's hardwired TCP/IP capabilities?**
The project uses the W5500 through MicroPython's `network.WIZNET5K` interface. However, the exact socket implementation path has not been verified for the firmware build used in this project.

**Q. Is this Art-Net, since it uses port 6454?**
No. The port number is the same one Art-Net uses, but the frame format is the project's own 772-byte layout. The two are not interchangeable.

**Q. Can I reproduce it from the article?**
The board-side firmware is published in full with a pin-by-pin wiring description. The host-side sender is not included in Part 1.

Source: Real-Time UDP Pixel Display & Stage Visualizer Part 1 by Jonathan L on Hackster.io

---

Source: https://maker.wiznet.io/TheoIm/projects/real-time-udp-pixel-display-stage-visualizer/
