화면이 살아 움직이기 시작하면 생기는 문제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을 부르면, 응답이 올 때까지 그 함수가 반환하지 않는다. 그동안 아래에 있는 화면 갱신 코드는 한 줄도 실행되지 않는다. 네트워크가 느리거나 서버가 늦게 답할수록 정지 시간이 길어진다.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 화면 갱신, 시계, 스크롤. 절대 멈추면 안 된다 |
| Core | Assigned Role |
|---|---|
| Core 0 | KMA weather API fetch task. Blocking here does not affect rendering |
| Core 1 | Main 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).
문제 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.
WIFI_PS_NONE 으로 되돌린다.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.

우연히 발견한 현상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.
여기서 초기 버전이 끝났다Wrapping Up the Initial Version

정리하면 이렇다.
Here is how the four issues stood at the end of the initial build:
| 문제 | 당시 해결책 |
|---|---|
| API 호출 시 화면 15초 정지 | FreeRTOS 코어 0에 수신 태스크 분리 + 스테이징 버퍼 |
| 환경마다 다른 와이파이 | WiFiMulti 자동 스캔 · 모뎀 슬립으로 전압 안정 |
| 비밀번호 오류 시 무한 재시도 | 1회 감지로 확정하고 오프라인 고정 |
| 배경 푸른 잔상 | 가로 스위핑으로 PURE BLACK 강제 방전 |
| Issue | Solution at the Time |
|---|---|
| 15-second screen freeze during API fetch | Pin HTTP task to FreeRTOS Core 0 + staging buffer |
| Different Wi-Fi networks across sites | WiFiMulti auto-scan · modem sleep for voltage stability |
| Infinite retry on wrong password | Lock into offline mode on first WL_CONNECT_FAILED |
| Bluish background ghosting | Horizontal 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.
4편은 그 이야기다. 이 시리즈에서 가장 쓰고 싶었던 편이기도 하다.
Part 4 covers that debugging process—and is the main reason I wanted to write this series.