구성을 바꾸자 증상이 돌아왔다
3편 에서 초기 버전을 완료했다. 실제 공장 설치를 준비하며 컨트롤러와 패널 구성을 바꿨다.
| 항목 | 초기 버전 | 교체 후 |
|---|---|---|
| 컨트롤러 | ESP32 NodeMCU-32S + 빵판 배선 + 외장 승압 모듈 | Adafruit Matrix Portal S3 (PID 5778) — 74AHCT245 레벨 시프터 내장, HUB75 16핀 직결 |
| 패널 | 64×32 × 2장 | 64×32 1/8 스캔 × 4장 (논리 256×32 / 물리 512×16 체인) |
| 배선 | 점퍼선 12가닥 | 리본 케이블 직결 (접촉 저항·유격 제거) |
레벨 시프터가 내장된 보드로 바꾸고 배선도 리본으로 정리했으니 신호는 더 깨끗해졌을 텐데, 배경의 푸르스름한 잔상이 다시 나타났다.

이번에는 변수를 하나씩 되돌렸다
3편에서는 우연히 발견한 현상(가로 스크롤이 지나가면 사라진다)에서 바로 해결책으로 갔다. 이번에는 순서를 바꿨다. 원인 후보를 하나씩 원래 값으로 되돌려 재현되는지 확인했다.
| 단계 | mxconfig.driver | GPIO 드라이브 강도 | 결과 |
|---|---|---|---|
| 1 | SHIFTREG | CAP_1 | 배경 정상 (Pitch Black) |
| 2 | ICN2038S | CAP_1 | 푸른기 즉시 재발 |
| 3 | SHIFTREG | CAP_3 | 재발 없음 |
mxconfig.driver = ICN2038S 설정 단 한 줄. GPIO 드라이브 강도도, 전원도, 배선도 아니었다.라이브러리 소스를 읽고 나서야 이해됐다
ESP32-HUB75-MatrixPanel-DMA 라이브러리는 driver != SHIFTREG(0) 이면 begin() 에서 shiftDriver() 를 호출한다. 그런데 ICN2038S, FM6124, FM6126A 세 값이 전부 동일한 fm6124init() 으로 분기한다.
이 초기화 루틴은 패널에 글로벌 전류 게인 레지스터(REG1)를 거의 최대값으로 기입한다. 그런데 내가 쓰는 패널의 실제 드라이버는 이 레지스터 포맷을 따르지 않는다. 기입된 값이 엉뚱하게 해석되면서 블랙 레벨 자체가 떠 버린 것이다.
// [원인] driver != SHIFTREG → shiftDriver() → fm6124init() → 전류 게인 레지스터 오기입
// mxconfig.driver = HUB75_I2S_CFG::ICN2038S; // ❌ 패널 라벨이 ICN2038S여도 이 옵션과는 비호환
constexpr HUB75_I2S_CFG::shift_driver PANEL_DRIVER = HUB75_I2S_CFG::SHIFTREG; // ✅ 확정 해결
mxconfig.driver = PANEL_DRIVER;
mxconfig.i2sspeed = HUB75_I2S_CFG::HZ_8M; // 512px 체인 안전 속도 명시
mxconfig.min_refresh_rate = 60;
ICN2038S 라고 해서 라이브러리의 ICN2038S 옵션을 고르면 되는 게 아니었다. 그 옵션이 실제로 어떤 초기화를 하는지는 소스를 직접 읽어야 알 수 있었다. 문서에는 "드라이버 칩에 맞춰 설정하라"고만 적혀 있다.왜 하필 파란색만 뜨는가
블랙 레벨이 떴다면 R, G, B가 고르게 떠야 할 것 같은데 유독 푸르스름했다. HUB75 리본 케이블의 핀 배치를 보면 이유가 보인다.
1 R1 | 2 G1
3 B1 | 4 GND ← B1이 GND 바로 옆
5 R2 | 6 G2
7 B2 | 8 E ← B2가 E 바로 옆 (1/8 스캔 패널은 내부 GND)
9 A | 10 B
11 C | 12 D ← 1/8 스캔 패널은 내부 GND
13 CLK | 14 LAT
15 OE | 16 GND
Blue 두 라인(B1=3번, B2=7번)만 GND 계열 핀과 물리적으로 인접해 있다. 따라서 블랙 레벨이 조금만 떠도 R/G보다 B가 먼저 문턱값을 넘어 미세하게 점등되고, 결과적으로 "푸르스름한 배경"으로 보인다.
함께 정정한 오진 두 건
digitalWrite(pin, LOW)는 GND가 아니다. 74AHCT245의 LOW 출력은 0.1~0.3V이고 임피던스도 수십 Ω이다. 실제 접지 전위차가 있다면 이 방법으로는 제거되지 않는다.- GPIO17은 HUB75 16번이 아니라 I2C의 SCL이다. Matrix Portal S3에서 HUB75 4번·16번은 보드상 하드와이어 GND이며, 소프트웨어로 접지 고정이 필요한 라인은 D(Pin12=GPIO35), E(Pin8=GPIO21) 두 개뿐이다. 기존의 GPIO17 LOW 클램프 코드는 HUB75와 무관하게 I2C 버스만 정지시키고 있었다.
그리고 화면이 굳었다
리팩터링을 하고 나니 새 증상이 생겼다. 와이파이가 연결된 직후 화면이 멈춘 채 굳었다. 증상은 하나였지만 원인은 두 개가 겹쳐 있었다.
원인 A — 스캔이 렌더링 루프를 잡고 있었다
WiFiMulti::run() 은 전 채널을 스캔하는 블로킹 호출이라 수 초간 CPU를 점유한다. 이걸 loop()(코어 1)에서 불렀기 때문에 스캔이 도는 동안 I2S 렌더링이 통째로 정지했다. 신호가 약할수록 스캔이 길어져 더 오래 멈췄다.
3편에서 HTTP 수신만 코어 0으로 뺐는데, 스캔과 연결도 블로킹이라는 걸 놓쳤다. 모든 블로킹 네트워크 작업을 코어 0 전담 NetTask 로 격리했다. loop() 는 volatile 상태값만 읽고 절대 블로킹되지 않는다.
원인 B — 리팩터링이 드러낸 순서 의존성
// [BEFORE] 스크롤 좌표 갱신이 else 분기 안에만 있었다
if (!isOfflineMode) {
drawOnlineScreen(); // ← 내부에서 getLocalTime() 실패 시 drawOfflineScreen()으로 폴백
} else {
if (now - lastScrollTime >= scrollSpeedMs) { offlineScrollX--; ... } // ← 여기에만 존재
drawOfflineScreen();
}
configTime() 은 비동기라 호출 직후에는 시계가 아직 무효다. NetTask 가 연결에 성공해 isOfflineMode = false 가 되면 drawOnlineScreen() 이 호출되지만, 시계가 없어 내부에서 오프라인 화면으로 폴백한다.
이때 offlineScrollX 는 else 분기에 있어 절대 갱신되지 않으므로 같은 프레임이 계속 그려진다 = 정지. UDP 123이 막혀 있거나 전파가 약해 SNTP가 끝내 동기화되지 않으면 영구 정지한다.
advanceOfflineScroll()— 스크롤 좌표 갱신을 분기 밖으로 옮겨, 어떤 화면을 그리든 항상 진행하게 했다.timeValid상태 도입 — 온라인 화면은 연결됨 && 시계 유효일 때만 렌더링한다.ntpWatchdog()— 동기화 성공 시 플래그를 세우고, 60초 이상 미동기화면configTime()을 재요청한다.
setup() 이 시계가 맞을 때까지 기다린 뒤 루프에 들어갔기 때문에 이 경로가 아예 발생하지 않았다.같이 잡은 것들
| 버그 | 영향 | 조치 |
|---|---|---|
wifiMulti.addAP() 를 매 사이클 반복 호출 | 내부 AP 리스트 무한 증식 → 힙 누수 + 스캔 시간 증가 | 부팅 시 1회만 등록 |
WL_CONNECT_FAILED 1회로 인증 실패 확정 (3편의 해결책) | 전파가 약해도 이 코드가 뜨므로, 한 번 삐끗하면 영영 오프라인 고정 | 3회 연속 기준으로 완화, 영구 잠금 제거 |
WIFI_PS_MIN_MODEM (3편의 판단) | 약전계에서 비콘 놓침 → 접속 직후 끊김 | WIFI_PS_NONE |
WIFI_POWER_17dBm | 최대 출력이 아니었음 (ESP32-S3 최대는 19.5dBm) | WIFI_POWER_19_5dBm, 연결 성공 시마다 재적용 |
| 재연결마다 날씨 API 강제 즉시 호출 | 링크가 불안정하면 기상청 호출 한도 소진 | weatherIsStale() 판정 — 연결 유지가 최우선 |
setup() 의 블로킹 연결 + NTP 대기 | 부팅 후 최대 20초 검은 화면 | 전부 제거, 즉시 오프라인 화면부터 시작 |
3편에서 "해결"이라고 적었던 판단 중 세 개가 여기서 뒤집혔다. 모뎀 슬립, 인증 실패 영구 고정, 그리고 잔상 퍼지. 구성이 바뀌자 당시의 판단이 오히려 발목을 잡았다.
현장에 걸기 전에 — 전파를 화면으로 보기
공장 현장은 금속 케이스 내부 설치가 예정돼 있어 RF 차폐가 우려됐다. 그런데 설치 환경에서는 PC를 붙이기 어렵고, PSU 5V가 인가된 상태에서 USB를 꽂으면 역전류 위험이 있어 시리얼 모니터에 의존할 수 없었다.
그래서 전파 세기를 LED 화면 자체에 표시하도록 만들었다.
| 기능 | 동작 |
|---|---|
| 연결 토스트 RSSI 표기 | WiFi OK -58dBm(초록) / RSSI WARN -78dBm(주황) / PASSWORD ERR!(빨강). 문자열 길이 기반 자동 중앙정렬 |
| RSSI 워치독 | 온라인 상태에서 −75dBm 미만이면 10분 주기로 경고 토스트 |
| UP 버튼 (온보드 GPIO6) | 전체화면 RSSI 모니터 토글 — 큰 dBm 수치 + GOOD/FAIR/WEAK 등급 + 게이지 바(−95~−35dBm) |
| DOWN 버튼 (온보드 GPIO7) | 패널 하드 리셋 — 표시가 깨졌을 때 전원 재투입 없이 DMA 재초기화 |
버튼 극성은 부팅 시 실측해 자동 판별하므로 보드 리비전 차이를 코드 수정 없이 흡수한다.
실측해 보니 자세가 전부였다

| 환경 | 배치 | RSSI | 등급 |
|---|---|---|---|
| 실내 (배면 목재벽) | 기본 자세 | −66 ~ −59 | FAIR~GOOD |
| 실내 | LED 패널면이 AP를 향함 | −58 ~ −56 | GOOD |
| 실내 | 모듈 안테나부가 AP를 향함 | −49 | GOOD |
| 옥외 (대리석 벽 밀착) | 안테나가 대리석을 향함 | 순간 −89 → −78 ~ −76 | WEAK |
| 옥외 (대리석 벽 밀착) | 안테나가 패널 쪽을 향함 | −74 ~ −68 | FAIR |
| 옥외 | 벽에서 떼어냄 | −66 ~ −61 | GOOD |
- 설치 원칙 ① 모듈 안테나부가 패널 반대쪽·AP 방향을 향하게 배치한다.
- 설치 원칙 ② 어떤 벽면이든 20~30mm 이상 이격한다. 비용 0원으로 10dB 이상 확보할 수 있다.
- 공장은 거리 증가(2배마다 −6dB), 철골·철제 랙, 2.4GHz 간섭, 지게차에 의한 멀티패스 페이딩으로 마진이 깎인다. 현장 목표치는 임계값(−75)이 아니라 −65 이상으로 잡았다.
NetTask 도입 이후에는 와이파이가 끊겨도 화면이 멈추지 않는다. 오프라인 마키가 계속 구동되고 15초 주기로 백그라운드 재연결을 시도하므로, 전파가 나빠도 전광판 본연의 안전수칙 표시 기능은 정상 동작한다. 날씨만 갱신이 늦어질 뿐이다.코드 정리
- 매직넘버 전면 상수화 — GPIO 번호, 드라이브 강도, 드라이버 IC, i2s 속도, TX 파워, 절전 모드, 재시도 간격, RSSI 임계값, 게이지 좌표, 버튼 핀/디바운스까지 전부
constexpr로 상단에 모았다. - 하드코딩 좌표 제거 — 토스트 X좌표를 44/52로 고정하던 것을 문자열 길이 기반 자동 중앙정렬로 대체했다.
- 데드코드 제거 —
NetTask통합으로 불필요해진 별도WeatherTask(태스크 생성/소멸 레이스 포함)와 write-only 변수를 정리했다. - 진단 스위치 내장 —
#define PANEL_DIAG_BLACK 1로 렌더링을 전부 우회하고 PURE BLACK만 유지한다. 소프트웨어 원인 배제 여부를 1분 안에 판정할 수 있다.
이 시리즈에서 남은 것
네 편에 걸쳐 쓴 것을 한 줄씩 줄이면 이렇게 된다.
- "증상을 없애는 것"과 "원인을 제거하는 것"은 다르다. 가로 스위핑은 화면상으로 완벽하게 동작했지만 원인을 덮고 있었을 뿐이고, 실제로는 설정 한 줄이 문제였다.
- 교차 검증만이 원인을 확정한다. 후보가 여러 개일 때 한꺼번에 고치면 무엇이 효과가 있었는지 영영 알 수 없다. 변수를 하나씩 되돌려 재현 여부를 확인하는 절차가 유일하게 신뢰할 수 있는 방법이었다.
- 라이브러리는 문서가 아니라 소스로 확인한다.
ICN2038S옵션이 내부적으로fm6124init()으로 분기한다는 사실은 소스를 직접 읽고 나서야 알 수 있었다. - 리팩터링은 숨어 있던 의존성을 드러낸다. 블로킹 대기를 없애자 그 대기에 기대고 있던 순서 문제가 표면으로 올라왔다.
- 현장 제약이 기능 요구사항을 만든다. 역전류 위험으로 시리얼을 못 쓴다는 제약이 없었다면 온스크린 RSSI 모니터와 버튼 진단 기능은 만들지 않았을 것이다.
이 유형의 실수(라이브러리 옵션이 실제로 무엇을 하는지 확인하지 않은 것, 핀 매핑을 가정으로 둔 것, 증상만 덮는 우회책)는 표로 정리해 프로젝트 문서에 남겼다. 다음에 같은 자리에서 시간을 쓰지 않기 위해서다.