무엇을 만들었나What I Built
산업·공사 현장에 걸 동적 전광판이다. 실시간 시계와 기상청 날씨를 띄우고, 기온에 따라 더위·동상 주의를 자동으로 바꿔 표시하며, 안전 수칙 6단계를 순서대로 스크롤한다.
This is a dynamic LED sign built for industrial and construction sites. It displays a real-time clock and Korea Meteorological Administration (KMA) weather feed, automatically switches between heatwave and frostbite warnings based on temperature, and scrolls through six workplace safety rules.
| 항목 | 내용 |
|---|---|
| 기간 | 2026.07.18 ~ 07.26 |
| 컨트롤러 | ESP32 (NodeMCU-32S) |
| 패널 | 64×32 HUB75 RGB LED 매트릭스 2장 — 물리 128×32 / 논리 256×32 |
| 드라이버 | 1/8 스캔 · ICN2038S |
| 전원 | 5V 20A 파워 서플라이 + 선 터미널 블록, 승압(레벨 시프터) 모듈 |
| 소프트웨어 | Arduino IDE · ESP32-HUB75-LED-MATRIX-PANEL-DMA-Display · Adafruit_GFX · FreeRTOS |
| 역할 | 1인 풀스택 — 회로/전원 설계, 펌웨어, API 연동, 픽셀 에디터 제작 |
| Item | Details |
|---|---|
| Period | 2026.07.18 ~ 07.26 |
| Controller | ESP32 (NodeMCU-32S) |
| Panels | Two 64×32 HUB75 RGB LED matrix panels — 128×32 physical / 256×32 logical |
| Driver | 1/8 scan · ICN2038S |
| Power | 5V 20A switching power supply + crimped spade terminal block, logic level shifter module |
| Software | Arduino IDE · ESP32-HUB75-LED-MATRIX-PANEL-DMA-Display · Adafruit_GFX · FreeRTOS |
| Role | Solo full-stack — power/circuit wiring, firmware, weather API integration, custom browser pixel editor |
전원부터Starting with Power
LED 매트릭스는 전기를 많이 먹는다. 64×32 패널 하나에 픽셀이 2,048개, 두 장이면 4,096개다. 전부 흰색으로 켜면 순간 전류가 상당히 올라간다. 5V 20A 파워 서플라이를 쓰고 선 터미널을 압착해 단자대에 물렸다.
LED matrices draw serious current. One 64×32 panel has 2,048 RGB pixels; two chained together have 4,096. Driving all of them white at once pulls a large current spike, so I used a 5V 20A power supply and crimped fork terminals onto the power leads to secure them to the terminal block.
여기까지는 예상한 작업이었다. 문제는 신호선 쪽에서 나왔다.
That part went as expected. The trouble started on the data lines.
문제 1 — 3.3V로는 부족했다Problem 1 — 3.3V Logic Was Not Enough
ESP32의 GPIO 신호를 HUB75 리본 케이블에 점퍼선으로 직접 물렸더니 픽셀이 계속 깜빡이고 화면이 깨졌다.
When I wired the ESP32's GPIO pins directly to the HUB75 ribbon cable with jumper wires, pixels flickered constantly and the image tore across the panel.
회로 중간에 승압 칩(Step-up Converter / Level Shifter) 모듈을 넣어 로직 레벨을 5V로 올렸다. 깜빡임과 화면 깨짐이 한 번에 사라졌다.
Inserting a step-up logic level shifter module between the ESP32 and the HUB75 connector boosted the signals to 5V and eliminated the flickering immediately.
문제 2 — 픽셀이 뒤죽박죽으로 나온다Problem 2 — Pixels Rendered Out of Order

전원과 신호를 정리하고 나니 화면은 켜졌는데, 글자를 그리면 라인 순서가 엉망으로 나왔다. 코드는 멀쩡한데 화면만 조각나 있었다.
Once power and signal voltage were clean, the panel lit up stably—but any text I drew came out sliced and scrambled out of order. My drawing code was fine; the panel layout was not.
원인은 패널 내부 구조에 있었다. 1/8 스캔 방식 64×32 패널은 픽셀 블록이 2-1-4-3 순서로 배치되어 있다. 라이브러리가 가정하는 일반적인 순서와 다르기 때문에, 논리 좌표를 그대로 넘기면 화면 위에서 블록이 뒤바뀐다.
The culprit was the panel's internal multiplexing layout. These 1/8-scan 64×32 panels route pixel blocks in a 2-1-4-3 order. Because that differs from the standard 1/16-scan mapping assumed by the DMA library, passing logical (x, y) coordinates straight through swaps the left and right halves and folds rows.
좌표 변환으로 덮어버리기Masking Hardware Routing with Coordinate Remapping
배선을 다시 만드는 대신 소프트웨어에서 좌표를 바꿔 넘기기로 했다. Adafruit_GFX 를 상속한 VirtualMatrix 클래스를 만들어, 논리 좌표 (x, y) 를 실제 패널의 물리 좌표 (px, py) 로 변환한 뒤 그리도록 했다.
Instead of hacking ribbon cables, I remapped the coordinates in software. I wrote a VirtualMatrix class inheriting from Adafruit_GFX that translates logical (x, y) coordinates into the panel's physical (px, py) coordinates before forwarding them to the DMA buffer.
class VirtualMatrix : public Adafruit_GFX {
public:
VirtualMatrix() : Adafruit_GFX(LOGICAL_WIDTH, LOGICAL_HEIGHT) {}
void drawPixel(int16_t x, int16_t y, uint16_t color) override {
if (x < 0 || x >= LOGICAL_WIDTH || y < 0 || y >= LOGICAL_HEIGHT) return;
if (!dma_display) return;
const int panel_idx = x / 64;
const int local_x = x % 64;
int px, py;
// 1/8 스캔 (2-1-4-3) 픽셀 블록 및 출력 라인 순서 재배치 연산
if (y < 16) {
if (local_x < 32) {
px = panel_idx * 64 + local_x + 32; py = y;
} else {
px = panel_idx * 64 + (local_x - 32); py = y;
}
} else {
if (local_x < 32) {
px = panel_idx * 64 + local_x + 32; py = y - 16;
} else {
px = panel_idx * 64 + (local_x - 32); py = y - 16;
}
}
dma_display->drawPixel(px, py, color);
}
};
class VirtualMatrix : public Adafruit_GFX {
public:
VirtualMatrix() : Adafruit_GFX(LOGICAL_WIDTH, LOGICAL_HEIGHT) {}
void drawPixel(int16_t x, int16_t y, uint16_t color) override {
if (x < 0 || x >= LOGICAL_WIDTH || y < 0 || y >= LOGICAL_HEIGHT) return;
if (!dma_display) return;
const int panel_idx = x / 64;
const int local_x = x % 64;
int px, py;
// Remap 1/8-scan (2-1-4-3) pixel blocks and row order
if (y < 16) {
if (local_x < 32) {
px = panel_idx * 64 + local_x + 32; py = y;
} else {
px = panel_idx * 64 + (local_x - 32); py = y;
}
} else {
if (local_x < 32) {
px = panel_idx * 64 + local_x + 32; py = y - 16;
} else {
px = panel_idx * 64 + (local_x - 32); py = y - 16;
}
}
dma_display->drawPixel(px, py, color);
}
};
핵심은 drawPixel 하나만 오버라이드했다는 점이다. Adafruit_GFX 의 선 그리기, 사각형, 텍스트 출력은 전부 내부적으로 drawPixel 을 부른다. 그래서 이 함수 하나만 고쳐 놓으면 그 위의 모든 그리기 함수가 자동으로 보정된다.
The key here is that I only had to override drawPixel. Every higher-level drawing routine in Adafruit_GFX—lines, rectangles, bitmaps, text—ultimately calls drawPixel. Fixing that single virtual method automatically corrected every drawing call built on top of it.
| 변환하는 것 | 이유 |
|---|---|
x / 64, x % 64 | 몇 번째 패널인지, 그 패널 안에서 몇 번째 열인지 분리 |
local_x < 32 분기로 ±32 | 좌우 절반이 서로 뒤바뀌어 있어 맞바꾼다 |
y >= 16 일 때 y - 16 | 아래쪽 16행이 물리적으로는 위쪽과 같은 행에 들어간다 |
| Transformation | Why It Is Needed |
|---|---|
x / 64, x % 64 | Separates which physical 64-wide panel we are on from the column index inside that panel |
±32 via local_x < 32 | Swaps the left and right 32-column halves to match the 2-1-4-3 block order |
y - 16 when y >= 16 | Folds the bottom 16 logical rows into the physical scan row address space |
VirtualMatrix ever needs to know the panel is 1/8 scan.1편 정리Part 1 Wrap-Up
여기까지가 화면에 원하는 그림을 띄울 수 있게 되기까지의 이야기다. 전원을 넉넉히 잡고, 신호 레벨을 5V로 올리고, 픽셀 좌표를 패널에 맞게 재배치했다.
That covers getting the hardware to render pixels where I actually wanted them: sizing the 5V 20A supply, shifting GPIO logic up to 5V, and remapping the 1/8-scan coordinates.
다음 편에서는 한글이 깨져서 결국 픽셀 에디터를 직접 만든 일을 쓴다. 아두이노 기본 폰트로는 한글이 성립하지 않았고, 결국 브라우저에서 도는 픽셀 에디터를 만들어 53개 도안을 직접 찍었다.
In Part 2, I will cover why Korean text turned into illegible blobs at 32 pixels tall, and how I ended up building a browser-based pixel editor to hand-craft 53 custom bitmaps.
3편은 화면이 15초씩 멈추던 문제와 배경에 남는 푸르스름한 잔상 이야기다. 그리고 4편에서 그 잔상 진단이 틀렸다는 걸 알게 된다.
Part 3 covers a 15-second UI freeze and a strange blue ghosting artifact in the background—and Part 4 is about discovering that my diagnosis of that blue ghosting was completely wrong.