화면이 살아 움직이기 시작하면 생기는 문제

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

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

문제 1 — 날씨를 받아오면 화면이 멈춘다

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

원인 — 동기식 호출이 메인 루프를 잡고 있다loop() 안에서 HTTP GET을 부르면, 응답이 올 때까지 그 함수가 반환하지 않는다. 그동안 아래에 있는 화면 갱신 코드는 한 줄도 실행되지 않는다. 네트워크가 느리거나 서버가 늦게 답할수록 정지 시간이 길어진다.

ESP32는 코어가 두 개다

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

코어맡은 일
코어 0기상청 API 수신 태스크. 느려도 상관없다
코어 1메인 루프 — I2S DMA 화면 갱신, 시계, 스크롤. 절대 멈추면 안 된다

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

왜 버퍼를 하나 더 두는가두 코어가 같은 변수를 동시에 건드리면, 메인 루프가 문자열을 절반만 읽은 상태로 화면에 그릴 수 있다. "23°C" 를 쓰는 도중에 읽으면 "2" 만 나오는 식이다. 수신 쪽은 버퍼에만 쓰고, 메인 루프가 안전한 시점에 플래그를 확인해 한 번에 가져간다.

문제 2 — 와이파이가 두 종류다

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

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

라디오가 전압을 흔든다

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

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

이 판단은 나중에 뒤집힌다전압 안정에는 도움이 됐지만, 모뎀 슬립은 비콘을 놓칠 수 있다. 전파가 약한 환경에서는 접속 직후 끊기는 원인이 된다. 4편에서 이걸 WIFI_PS_NONE 으로 되돌린다.

비밀번호가 틀리면

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

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

문제 3 — 배경에 푸르스름한 게 남는다

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

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

LED 전광판 배경이 순수 검정이 아니라 푸르스름하게 떠 있는 상태
배경에 남는 푸르스름한 잔상. 아무것도 그리지 않은 영역인데도 완전한 검정이 아니다.

우연히 발견한 현상

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

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

if (elapsed < 1800UL) {
  // 1단계 [완벽 퍼지]: 4px 레이저 수직선이 PURE BLACK(0x0000)을 훑으며 잔류 전하 100% 방전
  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단계에서 바로 오프라인 모드로 빠진다.

결과 — 화면상으로는 완벽했다잔상이 사라졌고, 부팅 연출도 그럴듯해졌다. 문제를 해결하면서 연출까지 얻었다고 생각했다. 여기까지가 "초기 버전 완료" 시점의 기록이다.

여기서 초기 버전이 끝났다

LED 전광판에 시각과 기온, 날씨, 미세먼지 정보가 표시된 화면
NTP 실시간 시계와 기상청 기온·날씨·미세먼지를 함께 띄운 화면.

정리하면 이렇다.

문제당시 해결책
API 호출 시 화면 15초 정지FreeRTOS 코어 0에 수신 태스크 분리 + 스테이징 버퍼
환경마다 다른 와이파이WiFiMulti 자동 스캔 · 모뎀 슬립으로 전압 안정
비밀번호 오류 시 무한 재시도1회 감지로 확정하고 오프라인 고정
배경 푸른 잔상가로 스위핑으로 PURE BLACK 강제 방전

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

그런데설치를 준비하며 컨트롤러와 패널 구성을 바꿨다. 그러자 푸른 잔상이 다시 나타났다. 그리고 이번에는 변수를 하나씩 되돌리는 방식으로 원인을 확정했는데 — 위 표의 네 항목 중 세 개가 잘못된 판단이었다.

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