화면이 살아 움직이기 시작하면 생기는 문제Problems That Surface Once the Display Runs Live

2편 까지 오면 원하는 글자를 원하는 자리에 띄울 수 있다. 이제 실제로 쓸 수 있게 만들 차례다. 시계가 돌아야 하고, 날씨를 받아와야 하고, 와이파이가 없어도 안전 수칙은 계속 흘러야 한다.

By the end of Part 2, I could render custom Korean glyphs anywhere on the panel. Now it needed to work as a real signboard: keep accurate time, fetch live weather data, and keep scrolling safety notices even without Wi-Fi.

그 셋을 붙이면서 나온 문제 세 가지를 정리한다.

Putting those three pieces together exposed three new problems.

문제 1 — 날씨를 받아오면 화면이 멈춘다Problem 1 — Fetching Weather Freezes the Display

기상청 공공데이터 API를 HTTP GET으로 호출하도록 붙였다. 그랬더니 호출할 때마다 화면이 최대 15초씩 얼어붙었다. 시계 바늘도 멈추고 스크롤도 멈춘다.

I wired up an HTTP GET request to the Korea Meteorological Administration (KMA) public API. Every time it fetched data, the display froze for up to 15 seconds. Both the clock and the scrolling text stopped dead.

원인 — 동기식 호출이 메인 루프를 잡고 있다loop() 안에서 HTTP GET을 부르면, 응답이 올 때까지 그 함수가 반환하지 않는다. 그동안 아래에 있는 화면 갱신 코드는 한 줄도 실행되지 않는다. 네트워크가 느리거나 서버가 늦게 답할수록 정지 시간이 길어진다.
Root Cause — Synchronous HTTP Blocking the Main LoopWhen loop() calls a synchronous HTTP GET, that function does not return until the server responds or times out. None of the display update code below it runs during that wait. Slower network conditions or sluggish server responses translate directly into longer freezes.

ESP32는 코어가 두 개다The ESP32 Has Two Cores

여기서 ESP32의 구조가 답이 됐다. ESP32는 듀얼코어고, FreeRTOS가 기본으로 올라가 있다. 태스크를 특정 코어에 고정할 수 있다.

The hardware architecture of the ESP32 solved this cleanly. The ESP32 is dual-core and runs FreeRTOS out of the box, allowing tasks to be pinned to specific cores.

좌우로 밀어서 표 전체 내용을 확인하세요
코어맡은 일
코어 0기상청 API 수신 태스크. 느려도 상관없다
코어 1메인 루프 — I2S DMA 화면 갱신, 시계, 스크롤. 절대 멈추면 안 된다
Swipe horizontally to view the full table
CoreAssigned Role
Core 0KMA weather API fetch task. Blocking here does not affect rendering
Core 1Main loop — I2S DMA display refresh, clock, and scrolling marquee. Must never block

xTaskCreatePinnedToCore 로 수신 태스크를 코어 0에 붙였다. 그리고 받아온 값을 메인 루프로 바로 넘기지 않고 스테이징 버퍼(pendingTempStr, weatherPending)를 거치게 했다.

I pinned the network fetch task to Core 0 using xTaskCreatePinnedToCore. Instead of writing parsed strings directly into the main loop's state, it writes to a staging buffer (pendingTempStr, weatherPending).

왜 버퍼를 하나 더 두는가두 코어가 같은 변수를 동시에 건드리면, 메인 루프가 문자열을 절반만 읽은 상태로 화면에 그릴 수 있다. "23°C" 를 쓰는 도중에 읽으면 "2" 만 나오는 식이다. 수신 쪽은 버퍼에만 쓰고, 메인 루프가 안전한 시점에 플래그를 확인해 한 번에 가져간다.
Why Use a Staging BufferIf two cores access the same string variable simultaneously, the main loop can read a half-written string while drawing a frame—printing "2" while Core 0 is halfway through writing "23°C". The network task writes only to the staging buffer, and the main loop checks a ready flag at a safe point between frames to copy it over.

문제 2 — 와이파이가 두 종류다Problem 2 — Two Different Wi-Fi Environments

이 전광판은 테스트 환경과 실제 공사 현장 두 곳에서 돌아야 한다. 와이파이 SSID가 다르다. 설치할 때마다 코드를 고쳐 다시 업로드하는 건 현실적이지 않다.

This signboard has to run in two places: my test desk and the actual factory floor. Each has a different Wi-Fi SSID. Recompiling and re-uploading firmware every time the panel moves is impractical.

WiFiMulti 라이브러리로 AP를 여러 개 등록해 두고, 켜졌을 때 잡히는 쪽으로 자동 접속하게 했다.

I used the WiFiMulti library to register multiple access points so the board automatically connects to whichever known SSID is in range at boot.

라디오가 전압을 흔든다Radio Current Spikes Shake the Voltage Rail

와이파이를 붙이고 나니 화면에 미세한 노이즈가 돌아왔다. 1편 에서 승압 모듈로 잡았던 그 증상과 비슷했다.

Once Wi-Fi was enabled, subtle visual noise returned to the panel—similar to the signal jitter I had fixed with a boost converter in Part 1.

와이파이 라디오는 송신할 때 전류를 순간적으로 크게 끌어간다. 그러면 3.3V 레일이 순간 내려앉고, 그 위에서 만들어지는 신호가 흔들린다. WiFi.setSleep(WIFI_PS_MIN_MODEM) 으로 모뎀 슬립을 켜 라디오가 항상 켜져 있지 않도록 했다.

When the Wi-Fi radio transmits, it draws sharp current spikes. That momentarily dips the 3.3V rail and disturbs the logic signals referenced to it. I enabled modem sleep with WiFi.setSleep(WIFI_PS_MIN_MODEM) so the radio would not stay active continuously.

이 판단은 나중에 뒤집힌다전압 안정에는 도움이 됐지만, 모뎀 슬립은 비콘을 놓칠 수 있다. 전파가 약한 환경에서는 접속 직후 끊기는 원인이 된다. 4편에서 이걸 WIFI_PS_NONE 으로 되돌린다.
This Decision Gets Reversed LaterModem sleep helped quiet the voltage rail, but it can cause the radio to miss AP beacons. In weak-signal environments, that leads to disconnects right after association. In Part 4, I revert this back to WIFI_PS_NONE.

비밀번호가 틀리면Handling Wrong Passwords

인증에 실패해도 라이브러리는 계속 재시도한다. 그동안 리소스를 쓰고, 오프라인 화면으로 넘어가는 것도 늦어진다. WL_CONNECT_FAILED 를 감지하면 빨간 Password Err! 토스트를 한 번 띄우고 즉시 라디오를 끄고 오프라인 스크롤 모드로 고정하게 했다.

When authentication fails, the Wi-Fi library keeps retrying indefinitely, burning resources and delaying the fallback to offline mode. When WL_CONNECT_FAILED appeared, I programmed it to show a red Password Err! toast once, turn off the radio immediately, and lock into offline scrolling mode.

이 판단도 4편에서 뒤집힌다. 전파가 약해도 같은 코드가 뜨기 때문에, 한 번 삐끗하면 영영 오프라인으로 굳어 버린다.

This decision also gets reversed in Part 4. Weak RF reception can trigger the exact same failure code, meaning a single momentary drop locked the signboard offline until reboot.

문제 3 — 배경에 푸르스름한 게 남는다Problem 3 — A Bluish Tint Lingering on the Background

부팅 초반에 배경이 완전한 검정이 아니라 푸르스름하게 떠 있었다. 아무것도 그리지 않은 영역인데도 그랬다.

Right after boot, the background was not pure black—it glowed with a faint bluish tint, even in regions where nothing was drawn.

당시 내린 진단은 이랬다. ICN2038S 드라이버 칩 내부의 static 래치에 잔류 전하가 남아 있고, 시프트 레지스터 초기화가 미흡해서 생긴다.

My diagnosis at the time was: residual charge remained trapped in the internal static latches of the ICN2038S driver chips due to incomplete shift register initialization.

LED 전광판 배경이 순수 검정이 아니라 푸르스름하게 떠 있는 상태
배경에 남는 푸르스름한 잔상. 아무것도 그리지 않은 영역인데도 완전한 검정이 아니다.Bluish ghosting on the background. Even unlit regions fail to settle at pure black.

우연히 발견한 현상An Accidental Observation

정적 화면에서는 잔상이 계속 유지되는데, 가로 스크롤이 패널 256px 전체를 한 번 훑고 지나가는 순간 배경이 순수 검정으로 밀려나고 그 뒤로는 정상이었다.

While the bluish tint persisted on static screens, I noticed that as soon as a horizontal scroll swept across all 256 pixels of the panel once, the background cleared to pure black and stayed clean afterward.

이걸 연출로 만들었다. 와이파이가 붙은 순간 WiFi Connected! 토스트를 띄우는데, 그 토스트가 4px 세로 레이저 선을 앞세워 1.8초에 걸쳐 화면을 가로지르도록 짰다. 지나간 자리는 0x0000 PURE BLACK으로 덮인다.

I turned that observation into a boot transition. Right when Wi-Fi connects and displays the WiFi Connected! toast, a 4px vertical cyan/white line sweeps across the 256px width over 1.8 seconds, overwriting everything behind it with 0x0000 (PURE BLACK).

if (elapsed < 1800UL) {
  // 1단계 [퍼지 스위핑]: 4px 레이저 수직선이 PURE BLACK(0x0000)을 훑으며 잔류 전하 방전
  int sweepX = (int)((elapsed * 256UL) / 1800UL);
  if (sweepX > 0) {
    display.fillRect(0, 0, sweepX, 32, boxBg);
    display.drawRect(2, 2, sweepX > 4 ? sweepX - 4 : 0, 28, boxBorder);
    display.setTextSize(2);
    display.setTextColor(C_WHITE, boxBg);
    display.setCursor(textCursorX, 8);
    display.print(toastText);
  }
  if (sweepX < 256) {
    display.fillRect(sweepX, 0, 256 - sweepX, 32, C_BLACK);
    display.drawFastVLine(sweepX, 0, 32, C_CYAN);
    display.drawFastVLine(sweepX + 1, 0, 32, C_WHITE);
  }
}
if (elapsed < 1800UL) {
  // Stage 1 [Purge Sweep]: 4px vertical line sweeps PURE BLACK (0x0000) across the panel
  int sweepX = (int)((elapsed * 256UL) / 1800UL);
  if (sweepX > 0) {
    display.fillRect(0, 0, sweepX, 32, boxBg);
    display.drawRect(2, 2, sweepX > 4 ? sweepX - 4 : 0, 28, boxBorder);
    display.setTextSize(2);
    display.setTextColor(C_WHITE, boxBg);
    display.setCursor(textCursorX, 8);
    display.print(toastText);
  }
  if (sweepX < 256) {
    display.fillRect(sweepX, 0, 256 - sweepX, 32, C_BLACK);
    display.drawFastVLine(sweepX, 0, 32, C_CYAN);
    display.drawFastVLine(sweepX + 1, 0, 32, C_WHITE);
  }
}

전체는 5단계로 짰다. 퍼지 스위핑 → 팝업 유지 → 역방향 스위핑으로 지우기 → 구분선 정착 → 시계와 안전수칙 슬라이드. 비밀번호 오류일 때는 3단계에서 바로 오프라인 모드로 빠진다.

The boot sequence ran in five stages: purge sweep → hold toast popup → reverse sweep clear → divider line settle → clock and safety marquee slide-in. On a password error, stage 3 branched straight into offline mode.

결과 — 화면상으로는 정상이었다잔상이 사라졌고, 부팅 연출도 자연스러워졌다. 문제를 해결하면서 연출까지 얻었다고 생각했다. 여기까지가 "초기 버전 완료" 시점의 기록이다.
Result — Visually Clean on ScreenThe ghosting vanished and the boot animation looked intentional. I thought I had solved a hardware quirk and gained a nice transition at the same time. That was the state of the project when I marked the initial version complete.

여기서 초기 버전이 끝났다Wrapping Up the Initial Version

LED 전광판에 시각과 기온, 날씨, 미세먼지 정보가 표시된 화면
NTP 실시간 시계와 기상청 기온·날씨·미세먼지를 함께 띄운 화면.The panel displaying the real-time NTP clock alongside KMA temperature, weather icon, and fine dust status.

정리하면 이렇다.

Here is how the four issues stood at the end of the initial build:

좌우로 밀어서 표 전체 내용을 확인하세요
문제당시 해결책
API 호출 시 화면 15초 정지FreeRTOS 코어 0에 수신 태스크 분리 + 스테이징 버퍼
환경마다 다른 와이파이WiFiMulti 자동 스캔 · 모뎀 슬립으로 전압 안정
비밀번호 오류 시 무한 재시도1회 감지로 확정하고 오프라인 고정
배경 푸른 잔상가로 스위핑으로 PURE BLACK 강제 방전
Swipe horizontally to view the full table
IssueSolution at the Time
15-second screen freeze during API fetchPin HTTP task to FreeRTOS Core 0 + staging buffer
Different Wi-Fi networks across sitesWiFiMulti auto-scan · modem sleep for voltage stability
Infinite retry on wrong passwordLock into offline mode on first WL_CONNECT_FAILED
Bluish background ghostingHorizontal sweep forcing PURE BLACK across all pixels

네 가지 모두 화면에서 증상이 사라졌다. 하드웨어 전원부터 펌웨어, 폰트 에디터까지 1인으로 완성했고, 공장 설치를 준비하기 시작했다.

All four symptoms disappeared on screen. Having built the power wiring, firmware, and custom font pipeline solo, I started preparing the hardware for factory installation.

그런데설치를 준비하며 컨트롤러와 패널 구성을 바꿨다. 그러자 푸른 잔상이 다시 나타났다. 그리고 이번에는 변수를 하나씩 되돌리는 방식으로 원인을 확정했는데 — 위 표의 네 항목 중 세 개가 잘못된 판단이었다.
HoweverWhile preparing for installation, I upgraded the controller board and panel chain configuration. The bluish ghosting came right back. When I isolated variables one by one to find the true cause, three of the four solutions in the table above turned out to be misdiagnoses.

4편은 그 이야기다. 이 시리즈에서 가장 쓰고 싶었던 편이기도 하다.

Part 4 covers that debugging process—and is the main reason I wanted to write this series.