화면은 켰는데 글자가 안 된다The Panel Turned On, But Korean Text Broke
1편 에서 좌표 변환으로 픽셀을 제자리에 보내는 데까지 성공했다. 이제 글자를 찍을 차례였다. 그런데 한글을 출력하니 획이 잘리고 깨졌다.
In Part 1, I got pixels landing in the right positions using coordinate remapping. Next was rendering text. As soon as I printed Korean characters, the strokes clipped and collapsed into blobs.
이 전광판이 띄워야 하는 문구는 전부 한글이다. "안전제일", "더위 조심", "안전미흡 식별시 관리자에게 보고" 같은 것들. 글자가 안 되면 프로젝트 자체가 성립하지 않는다.
Every message this signboard needs to display is in Korean—phrases like "Safety First," "Watch for Heat," and "Report Safety Hazards to Supervisor." Without readable Korean text, the project does not work.
왜 깨지는가Why It Breaks

아두이노 계열의 비트맵 폰트 라이브러리는 대부분 영문 기준으로 만들어져 있다. 알파벳은 획이 적어서 5×7이나 6×8 같은 작은 격자에도 들어간다.
Most bitmap font libraries in the Arduino ecosystem are designed around the Latin alphabet. Latin letters have relatively few strokes and fit inside small 5×7 or 6×8 grids.
패널 높이가 32px이고 그중 글자에 쓸 수 있는 영역은 24px 남짓이었다. 이 크기에서 기존 폰트로는 답이 없었다.
The panel is 32px tall, leaving roughly 24px for text height. At this resolution, existing bitmap fonts were not usable.
폰트를 찾는 대신 에디터를 만들었다Building an Editor Instead of Hunting for Fonts
선택지는 두 가지였다. 한글을 지원하는 비트맵 폰트를 찾아 이식하거나, 필요한 글자만 직접 찍거나.
There were two options: find and port a Korean bitmap font library, or draw only the specific characters I needed.
후자를 택했다. 이 전광판이 띄우는 문구는 정해져 있다. 시계 숫자, 날씨 단어, 안전 수칙 6단계. 전체 한글 음절 11,172자가 필요한 게 아니라 쓰는 글자만 있으면 된다.
I chose the second option. The messages on this signboard are fixed: clock digits, weather terms, and six safety notices. I did not need all 11,172 Hangul syllables—only the characters actually displayed.
그래서 브라우저에서 도는 HTML5 Canvas 픽셀 에디터를 만들었다. 격자 위에 마우스로 픽셀을 찍고, 결과를 C++ 배열로 뽑아내는 도구다.
So I built an in-browser HTML5 Canvas pixel editor—a tool where I click pixels on a grid with a mouse and export the result directly as C++ arrays.
4배로 그려서 줄인다Rendering at 4× and Downsampling
픽셀을 하나씩 손으로 찍는 것만으로는 부족했다. 글자 모양의 기준이 필요했다. 그래서 실제 폰트를 캔버스에 렌더링한 뒤 그걸 격자로 내리는 방식을 썼는데, 여기서 바로 줄이면 획이 끊어졌다.
Drawing every pixel from a blank grid was slow; I needed a reference shape for each character. I rendered a system font onto a canvas first and downsampled it onto the pixel grid, but direct downsampling caused thin strokes to drop out.
| 기법 | 내용 |
|---|---|
| 4× 슈퍼샘플링 | 96×96 오프스크린 캔버스에 먼저 렌더링한 뒤 24×24로 줄인다 |
| Dual-threshold 획 보정 | 단일 임계값으로 이진화하면 얇은 획이 통째로 사라진다. 두 단계 임계값으로 획 끊김을 막는다 |
| 404px 표준 크기 고정 | 24×24 픽셀 가이드 기준으로 글자 크기를 404px에 고정해 도안 간 굵기를 통일 |
| Technique | Details |
|---|---|
| 4× Supersampling | Render onto a 96×96 offscreen canvas first, then downsample to the 24×24 grid |
| Dual-threshold Stroke Compensation | Binarizing with a single threshold drops thin strokes entirely. A two-stage threshold prevents broken strokes |
| Fixed 404px Standard Size | Lock reference font size at 404px against the 24×24 guide to keep stroke weight consistent across assets |
슈퍼샘플링은 원래 그래픽스에서 계단 현상을 줄일 때 쓰는 기법이다. 큰 해상도로 그린 뒤 축소하면 픽셀 하나에 여러 샘플의 평균이 담긴다. 여기서는 그 평균값이 "이 픽셀에 획이 얼마나 걸쳐 있는가"의 척도가 된다. 그 값을 두 단계로 나눠 판정하니 가는 획이 살아났다.
Supersampling is a standard graphics technique for reducing jagged edges. Drawing at high resolution and scaling down stores the average of multiple samples in each output pixel. Here, that average serves as a measure of how much of a stroke overlaps that pixel. Splitting the decision into two thresholds kept thin strokes intact.
찍은 다음이 더 중요했다Automating the Pipeline After Drawing
도안을 53개 만들었다. 한글 단어와 아이콘이다. 이걸 손으로 C++ 배열에 옮겨 적는 건 현실적이지 않다. 하나 고칠 때마다 다시 옮겨야 하기 때문이다.
I created 53 pixel assets in total—Korean words and weather icons. Copying those into C++ arrays by hand was not realistic, because every minor pixel edit would require copying them all over again.
update_all_pixel_data.py 스크립트를 만들어 53개 전 도안의 16비트 RGB565 C++ 배열과 JSON을 자동 생성하도록 했다. 에디터에서 고치고 스크립트 한 번 돌리면 펌웨어에 반영된다.
I wrote update_all_pixel_data.py to automatically generate 16-bit RGB565 C++ arrays and JSON files for all 53 assets. After editing a glyph in the browser, running a single script updates the firmware data.
Adafruit_GFX 는 색을 16비트로 다룬다. R 5비트, G 6비트, B 5비트다. 사람 눈이 녹색에 가장 민감해서 G에 한 비트를 더 준다. 24비트 트루컬러 대비 메모리가 3분의 2로 줄고, 마이크로컨트롤러의 좁은 플래시에서는 이 차이가 크다.Adafruit_GFX represent color as 16-bit values: 5 bits for Red, 6 bits for Green, and 5 bits for Blue. Green gets the extra bit because human vision is most sensitive to green. Compared to 24-bit true color, memory usage drops to two-thirds, which matters on a microcontroller with limited flash.이 결정이 남긴 것What This Decision Left Behind
"라이브러리를 찾는다"에서 "필요한 것만 직접 만든다"로 방향을 튼 판단이었다. 결과적으로 세 가지를 얻었다.
Pivoting from "find a library" to "build only what is needed" gave me three practical benefits.
- 외부 폰트 라이브러리 의존이 0이 됐다. 플래시에 들어가는 건 실제로 쓰는 53개 도안뿐이다. 전체 한글 폰트를 넣었다면 용량이 감당 안 됐을 것이다.
- 글자 모양을 내가 통제한다. 24×24 격자에서 어떤 획을 살리고 어떤 획을 버릴지는 결국 사람이 판단해야 하는 문제였다. 자동 변환에만 맡겼다면 계속 아쉬운 결과를 받았을 것이다.
- 도안을 고치는 비용이 거의 0이다. 브라우저에서 찍고 스크립트 한 번 돌리면 끝난다. 이 왕복이 짧아진 덕분에 실제로 여러 번 고쳤다.
- Zero external font library dependencies. Only the 53 assets actually used go into flash. Embedding a full Hangul font would have blown past the memory budget.
- Full control over glyph shapes. Deciding which stroke to keep and which to drop on a 24×24 grid ultimately requires human judgment. Relying purely on automatic conversion would have left awkward results.
- Near-zero cost to revise a glyph. I tweak pixels in the browser and run one script. Because that loop was fast, I iterated on the glyphs multiple times.
반대로 대가도 있다. 이 에디터 자체가 유지보수 대상이 됐다. 새 문구를 추가하려면 도안을 찍어야 하고, 그건 폰트를 쓰는 프로젝트에서는 없는 작업이다. 문구 종류가 수십 개 수준이니까 성립하는 선택이었다.
There is a trade-off as well: the editor itself becomes something to maintain. Adding a new phrase requires drawing a new glyph asset—work that does not exist in projects using standard fonts. This approach worked because the signboard only displays a few dozen fixed phrases.
2편 정리, 그리고 다음Part 2 Summary, and What Comes Next
여기까지가 화면에 원하는 한글을 띄우기까지의 이야기다. 폰트 문제를 도구 제작으로 우회했고, 그 도구가 만든 데이터가 자동으로 펌웨어에 들어가는 파이프라인까지 만들었다.
That covers how I got clean Korean text onto the display. I bypassed the low-resolution font problem by building a custom pixel editor and an automated pipeline that compiles pixel data straight into firmware.
다음 편은 화면이 살아 움직이기 시작하면서 생긴 문제들이다. 기상청 API를 부르면 화면이 최대 15초씩 멈췄고, 부팅 초반에는 배경에 푸르스름한 잔상이 남았다. 각각 FreeRTOS 코어 분리와 가로 스위핑 연출로 해결했다.
The next part covers the issues that surfaced once the display started running live: calling the Korea Meteorological Administration API froze the display for up to 15 seconds, and a bluish ghosting artifact lingered on the background after boot. I addressed those using FreeRTOS core separation and a horizontal sweep transition.
그리고 4편에서 그 잔상 해결책이 사실 원인을 못 찾은 우회책이었다는 걸 알게 된다.
And in Part 4, I discover that the ghosting fix was actually a workaround that masked the real root cause.