본문으로 건너뛰기
5분 AI.
GITHUB TRENDING C DAILY #9 딥다이브 · 2026-10-23 분석

ai-passport 딥다이브
— 작은 웨어러블로 배우는 C 펌웨어의 설계와 수명 관리

펌웨어·개발자료

빠른 판단

무엇
ESP32-C3 웨어러블의 화면·버튼·오디오·배터리·무선·절전을 묶은 C 펌웨어와 개발 지침
이럴 때
소형 웨어러블 기능을 만들며 BSP·RTOS·버퍼·종료·저전력 상태의 자원 관리를 구성할 때
화면이 뜨고 소리가 난다고 임베디드 개발이 끝나는 것은 아닙니다. UP 버튼을 클릭하면 3초간 녹음하고, 화면을 닫을 때 작업을 멈추고, 잠들기 전에 주변 칩까지 정리해야 합니다. FoloToy AI Passport는 ESP32-C3 웨어러블을 위한 공개 펌웨어와 AI 개발 지침을 묶어 이 문제를 보여 줍니다. 기본 코드는 완성된 AI 서비스가 아니라 7개 하드웨어 테스트 화면입니다. C 소스 약 5,100줄과 헤더 약 1,000줄에서, 작은 기기를 안정적으로 움직이는 방법을 따라가 봅니다. (⭐706 · 포크 281 · MIT · 2026-10-23 GitHub 기준, 소스는 커밋 006c227 기준)
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

AI라는 이름보다, 기본 펌웨어가 하는 일부터 이해하기

ai-passport는 FoloToy의 작은 웨어러블 기기에서 화면·버튼·오디오·배터리·무선·절전 기능을 재사용하도록 만든 ESP-IDF 기반 C 펌웨어와 개발 자료 모음입니다. 제품 소개는 docs/README.md에 있고, 프로그램의 시작점은 main/main.c의 app_main()입니다.

이 저장소에서 배울 핵심은 AI 모델 호출법보다, 여러 하드웨어 기능이 같은 메모리와 시간을 나눠 쓰는 방법입니다. 기본 브랜치의 빌드 대상에는 LLM 추론 엔진이나 대화 서버가 없습니다. AI 코딩 도구가 읽을 지침과 스킬은 있지만, 그것과 기기 안에서 돌아가는 AI 모델은 다른 구성 요소입니다.

용어 먼저
펌웨어와 BSP
펌웨어는 기기에 기록되어 하드웨어를 제어하는 프로그램입니다. BSP(Board Support Package)는 특정 보드의 핀·버스·칩을 다루는 공통 코드입니다. 이 저장소에서는 components/bsp/가 보드 지식을, main/이 화면과 동작 순서를 담당합니다.
한 문장 비유

완성된 놀이공원보다, 설비와 안전 절차가 갖춰진 작은 공방에 가깝습니다

화면·스피커·버튼은 공방의 도구입니다. BSP는 도구를 켜고 끄는 사용법이고, 기본 데모는 도구가 정상인지 확인하는 점검대입니다. 여기에 어떤 작품을 만들지는 애플리케이션이 결정합니다.

작업자가 떠났는데 드릴이 계속 돌아가면 위험합니다. 화면을 지운 뒤에도 녹음 작업이 그 화면을 고치려 하는 문제도 같습니다. 이 저장소는 기능을 시작하는 법과 끝내는 법을 함께 보여 줍니다.

기본 프로그램에서 직접 제공하는 일

메뉴 항목구현 범위대표 파일
Display / Button화면 표시와 버튼 이벤트·ADC 전압 확인main/demo_display.c, main/demo_button.c
Audio1 kHz 시험음, 3초 녹음 후 재생main/demo_audio.c
BatteryCW2017의 잔량과 전압 읽기main/demo_battery.c
Wi-Fi / BLE주변 AP 검색 / 비연결 광고main/demo_wifi.c, main/demo_ble.c
Low Power2초 light sleep, 버튼 또는 5초 타이머로 깨는 deep sleepmain/demo_low_power.c

main/main.c의 DEMOS[]에 위 7개 페이지가 등록됩니다. Wi-Fi는 인터넷에 연결하거나 자격 증명을 저장하지 않고, BLE는 사용자 데이터를 주고받는 GATT 앱을 구현하지 않습니다. 범위를 정확히 잡아야 이후 추가할 기능도 정확해집니다.

학습의 출발점
보드가 없어도 첫 분석은 할 수 있습니다

main/demo_navigation.c와 main/key_guard.h에는 ESP-IDF나 LVGL 없이 이해할 수 있는 상태 로직이 있습니다. 먼저 입력과 상태 전이를 종이에 추적하고, 이후 실제 화면·오디오·전원 제어를 연결하면 난도가 단계적으로 올라갑니다.

2왜 주목받는가

트렌딩 선정 사실과, 코드에서 읽어낼 수 있는 관심 요인

2026-10-23 GitHub Trending C Daily에서 9위로 선정되었고, 같은 날 GitHub 기준 스타는 706개, 포크는 281개입니다. 생성일은 2026-07-31입니다. 다만 이 숫자와 순위만으로 유입 경로나 인기의 원인을 확정할 수는 없습니다. 아래는 저장소의 구성에서 추론할 수 있는 관심 요인입니다.

2-1. AI 개발 지침이 실제 하드웨어 경계까지 내려옵니다

AGENTS.md는 핀 배치의 근거, LVGL 잠금, 버튼 콜백의 제한, 작업 종료 순서, 검증 결과의 구분을 명시합니다. skills/README.md에는 개발·환경 준비·빌드·기기 테스트·디버깅을 담당하는 5개 핵심 스킬과 배포 후 경험을 정리하는 스킬이 등록되어 있습니다.

초보자가 AI에게 “타이머 만들어 줘”라고 말할 때 빠뜨리기 쉬운 것이 메모리·버스·수명 제약입니다. 자연어 요구사항을 보드의 제약과 검증 항목에 연결하는 지침이 코드와 함께 있다는 점이 이 저장소의 특징입니다. 이는 지침의 존재에서 확인되는 장점이며, 모든 AI 도구가 같은 품질로 구현한다는 성능 검증은 아닙니다.

2-2. 시작하기 쉬운 구성과, 숨길 수 없는 제약이 함께 있습니다

화면·마이크·스피커·세 버튼이 한 기기에 모여 있어 별도 주변장치를 하나씩 연결하는 부담을 줄일 수 있습니다. 반면 8 MB Flash와 PSRAM 부재는 이미지·소리·화면 버퍼를 무제한으로 늘릴 수 없게 만듭니다. 그래서 작은 기능도 메모리 예산과 종료 절차를 생각하게 하는 학습 재료가 됩니다.

선택지ai-passport가 줄여 주는 준비비교할 때 남는 차이
빈 ESP-IDF 프로젝트보드별 핀·칩 초기화와 테스트 화면이 이미 있음이 보드의 구성에 맞춰져 있어 다른 보드로 옮길 때 BSP 검토 필요
범용 MCU 보드 + 모듈 조립화면·오디오·배터리 입력을 통합한 기기 기준 제공회로와 주변장치를 자유롭게 고르는 폭은 개별 조립 쪽이 큼
Linux 기반 소형 컴퓨터운영체제 서비스 대신 펌웨어와 주변장치에 집중 가능프로세스·파일시스템 중심 경험과 RTOS 태스크·고정 자원 경험은 다름
완성된 대화형 AI 제품자신의 입력·화면·전원 동작을 소스에서 설계 가능기본 코드에는 대화 모델·음성 인식·TTS 서비스가 연결되어 있지 않음

이 표는 개발 구성의 비교입니다. 가격·배터리 시간·처리 속도를 측정한 경쟁 제품 벤치마크는 아닙니다. 같은 크기의 하드웨어라도 원하는 앱이 음성 대화인지, 오프라인 도구인지에 따라 적합성이 달라집니다.

2-3. 커뮤니티 결과물과 기본 코드의 거리를 배울 수 있습니다

docs/reference/README.md에는 음성 키체인, 게임, 독서기, 음성 메모 등의 응용 사례와 메모리·폰트·BLE 경험 글이 정리되어 있습니다. 이는 참고 문서에 소개된 프로젝트입니다. 그 기능이 전부 현재 main/CMakeLists.txt의 빌드에 포함된다는 뜻은 아닙니다.

비유

공방 벽에 걸린 작품 사진과, 작업대 위의 기본 공구 상자를 구분하면 됩니다. 작품 사진은 가능성을 보여 주지만, 사진만 보고 기본 공구 상자에 작품의 모든 부품이 들어 있다고 생각해서는 안 됩니다.

최근 커밋도 실제 기기 운용과 맞닿아 있습니다. 006c227은 500 mAh 배터리 프로필 수정 PR #110을 병합한 커밋이며, 코드에는 제조사 프로필 80바이트의 일치 검사와 읽기 검증이 있습니다. 눈에 띄는 UI뿐 아니라 기기별 보정 데이터도 소프트웨어 품질의 일부라는 사례입니다.

3기술 스택 전체 지도

웹 서비스의 계층 이름을 임베디드 프로그램에 맞춰 읽기

이 프로젝트에는 웹 백엔드·브라우저 프론트엔드·데이터베이스 서버 조합이 없습니다. 대신 제어 계층은 C와 ESP-IDF, 화면 계층은 LVGL, 빌드·검증 계층은 CMake·Python·셸·GitHub Actions입니다. package.json, Cargo.toml, pyproject.toml, go.mod, Dockerfile은 커밋 기준 저장소에 없습니다.

3-1. 백엔드에 해당하는 제어 계층

구성 요소실제 버전·설정근거와 역할
ESP-IDF요구 범위 >=5.5.3,<5.6.0
잠금 버전 5.5.3
components/bsp/idf_component.yml, dependencies.lock · 칩 SDK와 드라이버
FreeRTOSCONFIG_FREERTOS_HZ=1000sdkconfig.defaults · 태스크, 큐, 세마포어. 별도 버전 핀은 없으며 ESP-IDF에 포함
button4.2.0컴포넌트 명세와 잠금 파일 · 클릭·길게 누르기 이벤트
esp_codec_dev1.6.2컴포넌트 명세와 잠금 파일 · ES8311 오디오 제어
NimBLE / Wi-FiESP-IDF에 포함
BLE 최대 연결 설정 1
sdkconfig.defaults, main/demo_ble.c · 현재 데모는 비연결 광고
I2C / I2S / SPI / ADCESP-IDF 드라이버 사용components/bsp/CMakeLists.txt의 REQUIRES에 드라이버 의존성 선언

FreeRTOS의 1,000 Hz 설정은 스케줄링의 틱 단위가 1 ms라는 뜻입니다. 모든 작업이 1 ms 안에 끝난다는 보장은 아닙니다. 녹음과 재생은 블로킹 I/O를 포함하므로 별도 태스크에서 진행합니다.

3-2. 프론트엔드에 해당하는 기기 화면

구성 요소실제 버전·설정확인할 파일
LVGL명세 ^9.5.0, 잠금 9.5.0components/bsp/idf_component.yml, dependencies.lock
esp_lvgl_port2.9.0같은 의존성 파일 · LVGL 태스크와 ESP LCD 연결
픽셀 형식RGB565, 16비트CONFIG_LV_COLOR_DEPTH_16=y, components/bsp/src/bsp_display_lvgl.c
LVGL 메모리 풀24 KiBCONFIG_LV_MEM_SIZE_KILOBYTES=24
갱신 주기20 msCONFIG_LV_DEF_REFR_PERIOD=20 · 약 50 FPS를 목표로 하는 주기이며 실측 FPS 아님
기본 폰트Montserrat 14 / 20sdkconfig.defaults · 기본 설정만으로 한글 글리프 확보를 보장하지 않음
의존성 읽기
명세의 허용 범위와 잠금 파일의 선택 결과
components/bsp/idf_component.yml은 어떤 버전까지 허용하는지 적습니다. dependencies.lock은 그중 실제 선택된 버전과 컴포넌트 해시를 기록합니다. ^9.5.0을 “항상 정확히 9.5.0”으로 읽으면 안 됩니다. 이 커밋의 재현 기준은 잠금 파일의 9.5.0입니다.

설치 가능한 버전 범위와 현재 고정된 버전을 나눠 읽어야, 같은 소스의 동작을 다시 재현할 수 있습니다. 참고 문서에 다른 LVGL 버전의 경험이 등장하더라도 기본 펌웨어의 버전은 이 잠금 파일로 판단합니다.

3-3. 인프라·빌드·검증

루트 CMakeLists.txt는 cmake_minimum_required(VERSION 3.16)과 project(FoloToy-AI-Passport)를 선언합니다. 여기서 3.16은 최소 요구값이지 설치된 CMake의 확정 버전이 아닙니다. main/CMakeLists.txt는 12개 C 파일을, BSP의 CMake 파일은 8개 C 파일을 등록합니다.

도구·작업실제 구성배울 점
로컬 정적 검증tools/validate.sh --static
C11 호스트 테스트 13개 실행 파일 + Python 테스트 파일 7개
MCU 보드 없이 로직과 실패 경로를 검사하는 구조
워크플로 검사tools/install-actionlint.sh의 1.7.12, 배포 파일 SHA-256 확인검증 도구도 버전과 무결성을 관리
펌웨어 검증tools/validate.sh --firmware → 빌드·병합·검사·보관컴파일 결과와 전달할 이미지의 일치를 따로 검증
CI 환경ubuntu-latest, ESP-IDF v5.5.3, esp32c3.github/workflows/firmware-checks.yml에서 동일 게이트 사용
CI 액션checkout v6, cache v5, upload-artifact v7
실제 참조는 전체 커밋 SHA
주석의 버전명과 실행되는 SHA를 구분

독립적인 Dockerfile은 없지만 펌웨어 CI는 espressif/esp-idf-ci-action을 사용합니다. .github/workflows/build-firmware.yml에는 태그 push 시 릴리스를 만드는 설정이 있습니다. 릴리스 자동화 코드가 존재하는 것과 2026-10-23에 게시된 최신 릴리스가 없는 것은 모순이 아닙니다.

여기서 테스트 개수는 tools/validate.sh가 실행하도록 구성된 대상 수입니다. 테스트가 작성되어 있다는 사실과 특정 환경에서 실행해 통과했다는 사실은 구분해야 합니다.

4아키텍처 심화 분석

입력·화면·작업·주변장치를 연결하는 네 가지 설계

4-1. 시스템 구조를 한 장으로 보기

[사용자: UP / DOWN / OK] | v GPIO0 ADC 전압 → components/bsp/src/bsp_button.c | 클릭·더블클릭·길게 누르기·해제 이벤트 v main/main.c: on_key() [esp_timer 태스크] | key_guard_consume() → 깨운 버튼의 오동작 억제 | xQueueSend(..., 0) v [입력 큐: 깊이 8] | v input_task() → process_input() [demo_input 태스크] | | | +→ demo_navigation_handle() [순수 상태 로직] | +→ enter / exit / key ─ UI 접근 시 잠금 → [LVGL 객체] | | +→ start / stop v | esp_lvgl_port 태스크 +→ 오디오 / 절전 작업 | +→ Wi-Fi 이벤트 / NimBLE v rounded_flush_event() | v RGB565 DMA → SPI → LCD [애플리케이션: main/] | 공개 BSP 함수 호출 v [보드 계층: components/bsp/] +→ I2C0 ── ES8311 제어 / CW2017 잔량계 +→ I2S0 ── 마이크 PCM / 스피커 PCM +→ SPI2 ── ST7789P3 화면 +→ ADC1 ── 세 버튼 공통 전압 입력 +→ LEDC ── 백라이트 밝기

화살표는 호출 또는 데이터 전달 관계입니다. 모든 블록이 동시에 독립적으로 실행되는 것은 아닙니다. 기본 메뉴는 한 페이지를 활성화하며, Wi-Fi·BLE는 해당 페이지의 시작·종료 함수가 자원을 관리합니다. enter()·exit()은 호출자가 LVGL 잠금을 잡고, key()는 각 함수 내부에서 UI 접근 시 잠금을 잡습니다.

설계 ① 계층 분리와 자원 소유권

bsp_i2c_init()은 정적 핸들 s_bus가 이미 있으면 ESP_OK를 반환합니다. bsp_audio_init()과 bsp_battery_init()은 이 버스를 공유합니다. 앱은 SDA/SCL을 다시 설정하기보다 공개 BSP 함수를 이용합니다.

중요한 것은 파일을 나눈 모양보다 누가 생성하고, 누가 정리하는가입니다. I2C 버스는 components/bsp/src/bsp_i2c.c, 버튼의 ADC와 보정 핸들은 components/bsp/src/bsp_button.c, 페이지 객체는 각 main/demo_*.c가 소유합니다. 버튼 드라이버의 정리가 실패하면 ADC를 먼저 지우지 않는 것도 이 소유권을 지키기 위해서입니다.

설계 용어
멱등성
같은 작업을 반복해도 이미 완료된 상태를 불필요하게 바꾸지 않는 성질입니다. bsp_i2c_init()의 재호출은 버스를 하나 더 만들지 않습니다. 다만 “재호출 안전”이 곧 “여러 태스크에서 동시에 호출해도 안전”이라는 뜻은 아닙니다.

상수의 중심은 components/bsp/include/bsp_pins.h입니다. 화면 크기 240×320, SPI 80 MHz, 버튼 임계 전압, I2C 주소를 여기서 가져옵니다. 다른 보드로 옮길 때도 핀만 바꾸면 충분하다고 가정하면 안 됩니다. 디스플레이 초기화와 칩별 동작 계약까지 함께 검토해야 합니다.

설계 ② 이벤트 큐와 순수 상태 머신

버튼 콜백은 공유 esp_timer 태스크에서 호출됩니다. 그 자리에서 3초 녹음을 수행하면 같은 타이머 태스크의 다른 작업도 늦어집니다. on_key()는 깨움 입력 보호와 준비 상태를 확인한 뒤 짧은 이벤트만 큐로 보냅니다.

// main/main.c: on_key()의 큐 전달 부분
if (!s_input_ready || !s_input_queue) return;
const input_event_t input = { .btn = btn, .event = ev };
(void)xQueueSend(s_input_queue, &input, 0);

// input_task(): 실제 입력 처리는 다른 태스크에서
if (xQueueReceive(s_input_queue, &input, portMAX_DELAY) == pdTRUE) {
    process_input(&input);
}

전송 대기값 0은 큐에 공간이 생길 때까지 기다리지 않겠다는 뜻입니다. INPUT_QUEUE_DEPTH는 8이고 전송 반환값은 버립니다. 따라서 큐가 꽉 차면 이벤트가 유실될 수 있습니다. 이는 콜백 지연을 줄이는 대신 감수하는 경계이며, 입력이 반드시 보존되어야 하는 앱은 유실 횟수와 처리 정책을 설계해야 합니다.

메뉴의 선택 규칙은 demo_navigation_handle()로 분리됩니다. 이 함수는 화면을 그리지 않고 REFRESH, ENTER, EXIT, FORWARD 같은 다음 행동을 반환합니다.

// main/demo_navigation.c: 첫 항목에서 위를 누르면 마지막으로 순환
navigation->selected = (navigation->selected + navigation->count - 1)
                     % navigation->count;

// 활성 페이지에서 OK 길게 누르기: 종료 요청만 반환
result.action = input == DEMO_NAV_INPUT_OK_LONG
              ? DEMO_NAV_ACTION_EXIT
              : DEMO_NAV_ACTION_FORWARD;

tests/test_demo_navigation.c는 종료 요청 직후에도 active가 유지됨을 검사합니다. 실제 정리가 성공한 뒤 demo_navigation_complete_exit()가 호출되어야 메뉴 상태로 돌아갑니다. “종료하고 싶다”와 “종료가 끝났다”를 구분한 것입니다.

비유

버튼 콜백은 주문을 받는 직원, 큐는 주문표 꽂이, 입력 태스크는 작업 담당자입니다. 주문 받는 직원이 직접 요리까지 하면 다음 손님이 기다립니다. 주문표 꽂이가 작으면 밀릴 때 주문이 빠질 수 있으므로, 속도와 보존 정책을 함께 생각해야 합니다.

설계 ③ 페이지 수명과 작업 종료 확인

main/demo.h의 demo_entry_t는 함수 포인터로 페이지의 공통 인터페이스를 정의합니다. 객체 지향 언어의 메서드 표처럼 쓸 수 있지만, C에서는 함수 호출 순서와 자원 수명을 직접 지켜야 합니다.

// main/demo.h: 주석을 풀어 쓴 인터페이스
typedef struct {
    const char *name;
    void (*enter)(void);    // LVGL 잠금 안에서 화면 생성
    void (*exit)(void);     // 작업 정리가 끝난 뒤 화면 삭제
    void (*key)(bsp_btn_t btn, bsp_btn_ev_t ev);
    esp_err_t (*start)(void); // 잠금 밖에서 느린 서비스 시작
    esp_err_t (*stop)(void);  // 잠금 밖에서 작업 중단 확인
} demo_entry_t;
진입 순서종료 순서이유
LVGL 잠금 → enter() → 잠금 해제 → start()stop() 성공 → LVGL 잠금 → exit() → 메뉴 재생성긴 I/O와 종료 대기를 화면 잠금 밖에 두고, 작업이 멈춘 후 객체 삭제

오디오의 demo_audio_stop()은 취소 플래그를 세우고 종료 명령을 보낸 뒤, 최대 2000 ms 동안 세마포어를 기다립니다. 작업은 PCM 청크 사이에서 취소를 확인하고, 더 이상 공유 상태나 UI를 건드리지 않는 시점에 xSemaphoreGive()로 알립니다.

화면 삭제의 조건은 종료 명령을 보냈다는 사실이 아니라, 그 화면을 만질 작업이 멈췄다는 확인입니다. 응답이 시간 안에 오지 않으면 process_input()은 화면을 유지합니다. 재시도할 수 있도록 태스크 핸들도 남겨 둡니다.

실패 경로에서 배우기
타임아웃을 무시하면 해제한 화면을 다시 만질 수 있습니다

tests/test_demo_audio_runtime.c에는 종료 대기가 먼저 실패하고, 늦은 종료 응답이 도착한 뒤 재시도가 성공하는 검사가 있습니다. 타임아웃에서 태스크 핸들을 비우거나 화면을 지워 버리면 이 계약이 깨집니다. 잠금을 잡은 채 종료를 기다리는 것도 작업의 마지막 UI 갱신과 충돌할 수 있습니다.

설계 ④ 메모리 예산에 맞춘 화면 경계 처리

bsp_lvgl_init()은 40행 단일 버퍼를 설정하며 double_buffer=false, buff_dma=true, swap_bytes=true를 사용합니다. 화면 전체를 두 장 잡아 빠르게 그리는 방향보다 오디오·무선에 남길 내부 RAM을 고려한 구성입니다.

// components/bsp/src/bsp_display_lvgl.c
#define BSP_LVGL_DRAW_BUFFER_LINES 40
.buffer_size   = (uint32_t)BSP_LCD_W * BSP_LVGL_DRAW_BUFFER_LINES,
.double_buffer = false,
.flags = { .buff_dma = true, .swap_bytes = true },

// RGB565는 한 픽셀에 2바이트
// 부분 버퍼: 240 × 40 × 2 = 19,200바이트 = 18.75 KiB
// 전체 화면: 240 × 320 × 2 = 153,600바이트 = 150 KiB

둥근 화면 모서리는 전체 화면을 별도 투명 레이어로 합성하지 않고, LV_EVENT_FLUSH_START의 rounded_flush_event()에서 최종 RGB565 버퍼의 바깥 픽셀을 검게 만듭니다. 반경은 BSP_LVGL_SCREEN_RADIUS=30입니다.

bsp_display_rounded_row_span()은 각 행에서 보이는 x 좌표 범위를 계산합니다. 실제 메모리 접근은 행의 stride를 사용합니다. 버퍼가 항상 화면 너비와 정확히 같은 간격이라고 가정하지 않는다는 점도 살펴볼 만합니다. 기하 계산은 별도 C 파일에 있어 호스트 테스트로 확인할 수 있습니다.

문서와 코드 대조
하드웨어 가이드의 20행 설명은 현재 구현과 다릅니다

docs/hardware-design/AI_HARDWARE_DEVELOPMENT_GUIDE.md의 5절과 10절에는 20행·약 9.6 KB 버퍼 설명이 남아 있습니다. 커밋 006c227의 components/bsp/src/bsp_display_lvgl.c는 40행입니다. 현재 버퍼 예산은 19,200바이트로 계산해야 합니다. 문서의 값과 소스의 상수가 다를 때 확인할 대상을 보여 주는 사례입니다.

부팅과 배터리 초기화도 같은 관점으로 읽기

app_main()은 깨움 원인 확인 → I2C 준비·검색 → 화면·LVGL 초기화 → 입력 처리·버튼 → 오디오 → 배터리 → 메뉴 순으로 진행합니다. 화면이 실패하면 메뉴를 만들 수 없어 반환하지만, 오디오나 배터리 실패는 해당 항목을 [FAIL]로 표시하여 다른 기능을 남깁니다. Wi-Fi·BLE·절전은 페이지 안에서 시작 실패를 표시할 수 있습니다.

배터리의 bsp_battery_init()은 VERSION 응답을 확인하고, cw_profile_matches()로 업데이트 플래그와 80바이트 전체를 비교합니다. 다르면 cw_update_profile()이 절전 전환 → 쓰기 → 전체 읽기 검증 → 플래그 설정 → 재시작 순서를 수행합니다. 플래그 하나만 보고 데이터 전체가 맞다고 가정하지 않습니다.

이 로직은 assets/battery/wanli-500mah-profile2-xil40x-20261020.txt의 제조사 데이터와 연결됩니다. _Static_assert는 배열 길이 80을 확인하고, tests/test_battery_profile.py는 원본·해석된 바이트의 해시와 C 배열 일치를 검사하도록 작성되어 있습니다. 프로필 쓰기의 성공과 실제 잔량 정확도는 별도 검증 대상입니다.

5디렉토리 구조 해부

처음부터 모든 파일을 읽기보다, 책임을 따라 한 경로씩 읽기
ai-passport/ ├── AGENTS.md / CLAUDE.md AI 도구가 읽는 진입 지침 ├── CMakeLists.txt ESP-IDF 프로젝트 선언 ├── dependencies.lock 선택된 의존성 버전·해시 ├── sdkconfig.defaults 칩·Flash·화면·BLE 기본 설정 ├── partitions.csv Flash 영역 배치 ├── main/ │ ├── main.c 부팅·메뉴·입력 분배 │ ├── demo.h 페이지 공통 인터페이스 │ ├── demo_navigation.[ch] 메뉴 상태 전이 │ ├── key_guard.h 깨운 버튼의 입력 보호 │ ├── demo_audio.c 녹음·재생 작업과 종료 확인 │ ├── demo_wifi.c / demo_ble.c 무선 스택의 페이지별 수명 │ ├── demo_low_power.c light/deep sleep 절차 │ ├── demo_display.c 화면 테스트 │ ├── demo_button.c 입력·전압 테스트 │ ├── demo_battery.c 잔량·전압 표시 │ ├── demo_radio.[ch] NVS·네트워크 공통 준비 │ └── ui_pixel*.c / *.h 화면 부품·그리기 계산 ├── components/bsp/ │ ├── include/bsp_pins.h 보드 핀·타이밍·임계값 │ ├── include/bsp_*.h 외부에서 쓰는 BSP 계약 │ ├── src/bsp_*.c 칩·버스 드라이버와 표시 연결 │ ├── idf_component.yml 컴포넌트 의존성 명세 │ └── CMakeLists.txt BSP 소스·드라이버 의존성 ├── tests/ │ ├── test_*.c / test_*.py 순수 로직·실패 경로·도구 검사 │ └── *_stubs/ 호스트 테스트용 대체 인터페이스 ├── tools/ 검증·이미지 검사·디버그 산출물 보관 ├── skills/ AI 개발 절차와 전문 작업 지침 ├── docs/ │ ├── README.md 제품 소개와 문서 지도 │ ├── hardware-design/ 제품 사양·보드 규칙 │ ├── development/ 빌드·메모리·폰트·릴리스 안내 │ └── reference/ 응용 사례와 경험 기록 ├── assets/ 제품 이미지·배터리 프로필 원본 └── .github/workflows/ 정적 검사·펌웨어 검사·릴리스 등

파일 수와 줄 수를 범위별로 보기

범위파일·물리적 줄 수읽는 관점
C 구현34개 · 5,089줄앱·BSP·C 테스트·테스트 런타임 포함
C 헤더72개 · 988줄공개 계약과 테스트용 대체 헤더 포함
앱 / BSP / 테스트C·헤더 합산 1,958 / 1,960 / 2,159줄합계 6,077줄. 실제 펌웨어 앱·BSP만 합치면 3,918줄
Python11개 · 2,729줄검증·보관·설치 도구와 테스트
셸 / YAML / CMake186 / 328 / 33줄각각 2 / 7 / 3개 파일. YAML에는 워크플로 외 명세·이슈 양식도 포함
Markdown202개 · 17,868줄영어·중국어 문서 쌍과 스킬 포함, 코드 줄 수와 별도

커밋 기준 추적 파일은 총 351개입니다. 위 집계는 .git/을 제외한 물리적 줄 수이며 빈 줄과 주석도 포함합니다. 관리형 의존성의 다운로드 결과나 ESP-IDF 전체 코드는 포함하지 않습니다. “C 프로젝트”라는 분류만으로 소스의 대부분이 기기에서 실행된다고 생각하지 않도록 범위를 나눠 보세요.

권장 읽기 순서
입력 한 번의 여행을 따라갑니다

main/demo_navigation.h → main/demo_navigation.c → tests/test_demo_navigation.c → main/main.c의 on_key()와 process_input() → main/demo_audio.c 순서가 좋습니다. 타입·규칙·검사·연결·느린 작업이 차례로 등장합니다.

새 기능을 설계할 때의 질문도 이 트리에서 나옵니다. “다른 앱에서도 필요한 칩 동작인가?”라면 BSP, “우리 앱의 메뉴·상태·표시인가?”라면 main/, “칩 없이 검사할 수 있는 계산인가?”라면 순수 로직 파일과 호스트 테스트가 적절합니다.

6학습 포인트 (기술별)

새 라이브러리 이름보다, 다른 프로젝트로 가져갈 수 있는 사고방식

6-1. C: 포인터 문법을 수명과 연결하기

demo_entry_t는 함수 포인터를 저장하고, DEMOS[]는 구현을 등록합니다. const, 구조체 초기화, 배열 크기 계산을 실제 용도로 볼 수 있습니다. lv_obj_t *나 태스크 핸들은 숫자처럼 복사할 수 있어도, 가리키는 대상이 언제까지 유효한지는 별도 문제입니다.

demo_audio_stop()이 s_task를 지역 변수에 복사하는 이유와, 작업 스스로 핸들을 지우지 않는 이유를 살펴보세요. “포인터에 NULL을 대입했다”는 사실만으로 다른 실행 흐름의 접근이 끝나지는 않습니다.

6-2. FreeRTOS: 짧은 콜백과 긴 작업 나누기

입력에는 큐, 오디오 명령에는 태스크 알림, 종료 확인에는 이진 세마포어가 쓰입니다. 큐는 여러 이벤트를 담고, eSetValueWithOverwrite 알림은 알림값을 덮어씁니다. 그러므로 오디오 알림을 “명령이 순서대로 무한히 쌓이는 목록”으로 이해하면 안 됩니다.

실행 흐름
동시성과 비동기 처리
여러 일을 번갈아 진행하거나, 요청한 일이 끝나기 전에 다른 일을 계속하는 방식입니다. 별도 태스크를 만들었다고 모든 함수가 자동으로 안전해지지는 않습니다. UI 접근에는 LVGL 잠금, 작업 종료에는 명시적인 확인이 필요합니다. volatile 역시 큐나 잠금의 역할을 대신하지 않습니다.

6-3. LVGL: 화면을 그리는 순간과 보내는 순간 구분하기

main/ui_pixel.c는 위젯을 만들고, components/bsp/src/bsp_display_lvgl.c는 그 결과를 LCD에 보내는 경로를 연결합니다. rounded_flush_event()가 다루는 것은 버튼이나 레이블 객체가 아니라 최종 픽셀 버퍼입니다. 그래서 모든 페이지에 동일한 모서리 마스크를 적용할 수 있습니다.

성능 관점에서는 main/demo_battery.c도 읽어 보세요. 현재 tick()은 LVGL 타이머에서 bsp_battery_soc()와 bsp_battery_mv()를 동기 호출합니다. 각각의 I2C 전송 제한시간은 100 ms이므로, 버스 이상 시 화면 작업을 오래 붙잡을 여지가 있습니다. 모든 I/O가 별도 작업으로 옮겨진 예제라고 일반화하지 말고, 느린 환경에서는 읽기와 표시를 나누는 개선을 생각해 볼 수 있습니다.

6-4. 오디오: 바이트 예산과 시간 예산을 함께 계산하기

// main/demo_audio.c의 설정으로 계산
// 초당 바이트 = 샘플레이트 × 샘플당 바이트 × 채널 수
16000 × 2 × 1 = 32000 바이트/초
32000 × 3     = 96000 바이트   // 3초 전체 녹음 버퍼
512 × 2       = 1024 바이트    // 한 번의 PCM 청크
512 / 16000   = 0.032초        // 청크의 오디오 길이

components/bsp/src/bsp_audio.c의 I2S DMA는 6개 디스크립터·240프레임 설정입니다. 16 kHz에서 모두 차 있을 때 시간 길이는 6 × 240 ÷ 16000 = 0.09초입니다. 실제 여유는 현재 채워진 양에 따라 줄어듭니다. 90 ms라는 숫자는 모든 지연을 흡수한다는 보증이 아닙니다.

임베디드의 메모리 부족은 총 남은 양뿐 아니라, 필요한 크기의 연속 공간을 확보할 수 있는가의 문제입니다. 96,000바이트를 한 번에 요구하는 녹음과 1,024바이트 단위의 전송을 비교하면, 긴 녹음에 순환 버퍼나 스트리밍이 필요한 이유가 드러납니다.

6-5. 드라이버: 쓰기 성공과 상태 확인 분리하기

components/bsp/src/bsp_es8311_sleep_check.c의 bsp_es8311_sleep_check_matches()는 읽은 레지스터에 마스크를 적용합니다. REG0E는 쓰기값 0xFF와 달리 검증 마스크·기대값이 0x7F입니다. 정의되지 않은 비트까지 일치시켜 거짓 실패를 만들지 않으려는 구현입니다.

// 실제 검증식: 검사 대상 비트만 비교
return (actual & check->mask) == (check->value & check->mask);

// REG0E에서 actual=0xFF와 actual=0x7F는 모두 통과
// actual=0x3F는 검사 대상 비트가 달라 실패

또한 components/bsp/src/bsp_audio.c는 audio_track_error()와 감싼 제어·데이터 인터페이스로 첫 하위 오류를 보존합니다. 소스 주석은 esp_codec_dev 1.6.2가 일부 오류를 버리거나 뒤의 성공으로 덮을 수 있음을 이유로 듭니다. 상위 반환값 하나만 믿기 어려울 때 공개 인터페이스 경계에서 오류를 관찰하는 사례로 읽을 수 있습니다.

6-6. 테스트: 정상 동작보다 복구 계약에 주목하기

tests/test_demo_audio_runtime.c는 태스크 생성 실패, 늦은 종료 응답, 첫·중간 녹음 실패, 재생 실패, 취소를 다룹니다. tests/test_bsp_battery.c는 가짜 I2C 동작으로 초기화·프로필 갱신·SOC 및 전압 오류 경로를 검사합니다. 이런 테스트는 물리적 음질이나 SOC 정확도를 대신하지 않지만, 실패 후 코드가 어떤 상태여야 하는지는 빠르게 확인할 수 있습니다.

공통 질문
각 기능에서 세 가지를 찾아보세요

무엇을 소유하는가, 실패하면 무엇을 남기는가, 어떤 증거로 완료를 확인하는가. 이 질문은 화면·오디오·배터리뿐 아니라 파일 업로드, 서버 작업, 데이터베이스 연결을 설계할 때도 그대로 사용할 수 있습니다.

7하드웨어 / 시스템 요구사항

용량 숫자와 핀 배치를, 실행 조건으로 바꿔 읽기

7-1. 대상 보드의 명시된 사양

항목사양·설정개발에 미치는 영향
MCU / 저장장치ESP32-C3 · Flash 8 MB · PSRAM 없음Flash 용량을 실행 중 가변 버퍼의 RAM으로 쓸 수는 없음
화면ST7789P3 · 240×320 · RGB565SPI2 80 MHz 설정, 단일 40행 DMA 버퍼
입력UP / DOWN / OK · GPIO0 ADC 저항 사다리세 버튼이 한 ADC를 공유하므로 전압 범위로 구별
오디오마이크·스피커 · ES8311I2C 제어와 I2S PCM 경로를 구분
무선2.4 GHz Wi-Fi b/g/n · Bluetooth 5 LE제품 사양. 기본 데모의 동작은 검색·비연결 광고
전원500 mAh 리튬 배터리 · CW2017 · USB Type-C 5 V프로필은 해당 셀용. 용량만 같다고 다른 셀에 그대로 적용 불가
외형 / NFC60×95×8.5 mm · 50 g · 수동 NTAG213docs/hardware-design/specifications.md 기준. NFC 앱 드라이버가 기본 빌드에 있는 것은 아님

제품 사양은 docs/hardware-design/specifications.md, 펌웨어의 핀·주파수·주소는 components/bsp/include/bsp_pins.h 기준입니다. 소비 전류, 실제 배터리 사용 시간, 부하별 최고 프레임률은 코드 상수만으로 확정할 수 없습니다.

7-2. 같은 핀을 두 용도로 쓸 수 없는 이유

묶음핀·주소주의할 연결
LCDMOSI 9 · SCLK 8 · CS 1 · DC 20 · BL 21리셋 핀은 -1, 소프트웨어 리셋 사용
공유 I2C0SDA 10 · SCL 7
ES8311 0x18 · CW2017 0x63
스캔을 위해 두 번째 같은 버스를 생성하지 않음
I2S0MCLK 6 · BCLK 5 · WS 3 · DOUT 2 · DIN 4출력·입력이 클록을 공유. MCU가 마스터
USB / 버튼USB 18·19 · 버튼 ADC 0UART0 기본 TX GPIO21은 백라이트와 충돌

CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y에는 이 배경이 있습니다. 콘솔 출력 방법을 바꾸는 설정 하나가 화면 밝기 제어와 충돌할 수 있습니다. “범용 ESP32 예제에서 쓰는 핀”보다 이 보드의 실제 배치를 먼저 확인해야 합니다.

버튼 전압 범위는 [0,150), [150,447), [447,1900) mV입니다. button_level()은 반열린 구간으로 비교해 경계에서 두 버튼이 동시에 일치하지 않게 합니다. 이 수치는 이 회로의 기준이며, 다른 저항·보드에서는 실측 분포를 확인해야 합니다.

7-3. Flash와 RAM 예산은 별개의 표입니다

Flash 영역시작 / 크기의미
NVS0x9000 / 0x600024 KiB 키·값 저장 공간
PHY 데이터0xF000 / 0x10004 KiB PHY 데이터용 영역
factory 앱0x10000 / 0x7F00008,323,072바이트, 약 7.94 MiB. 기본 OTA 슬롯 없음

partitions.csv의 기본 레이아웃입니다. 사용자 앱이 다른 유효한 배치를 설계할 수 있지만, 현재 테이블에는 OTA 슬롯이나 별도 파일시스템 파티션이 예약되어 있지 않습니다.

RAM 소비 항목코드로 계산되는 크기별도로 고려할 것
LVGL 내부 풀24,576바이트위젯·그리기 메모리. 시스템 힙의 여유와 구분
화면 DMA 버퍼19,200바이트풀과 별도. 관련 구조체 등 부가 비용 미포함
3초 녹음 버퍼96,000바이트단일 동적 할당, 연속 공간 필요
태스크·오디오·무선설정과 실행 상태에 따라 추가태스크 스택, I2S DMA, 드라이버, 네트워크 버퍼

앞의 세 항목만 합쳐도 139,776바이트입니다. 이는 같은 시점에 녹음 버퍼가 존재한다고 가정한 부분 합계이며 전체 RAM 사용량이나 최소 필요 RAM 수치가 아닙니다. 전체 여유와 가장 큰 연속 블록, LVGL 자체 풀의 최고 사용량을 각각 확인하는 편이 좋습니다.

7-4. 개발 컴퓨터와 도구

개발 안내는 Linux·macOS·Windows·WSL2를 다룹니다. 기준 SDK는 ESP-IDF 5.5.3이며, Python·CMake·Ninja와 ESP32-C3용 도구 체인이 필요합니다. Windows에서는 ESP-IDF가 준비한 터미널을 사용하고, WSL2의 USB 접근은 별도로 준비해야 합니다.

호스트 검증에는 Python 3, C 컴파일러, actionlint와 관련 다운로드·해시 도구가 쓰입니다. Python의 독립적인 프로젝트 버전 핀은 없습니다. 펌웨어 빌드 환경은 ESP-IDF의 요구사항을 따라야 하며, 저장소는 개발 PC의 최소 RAM·디스크 용량을 별도 보증값으로 명시하지 않습니다.

전원·저장 데이터의 경계
화면이 꺼졌다는 사실과 전체 보드의 저전력 상태는 다릅니다

BSP_I2S_PA_CTRL=-1이므로 외부 앰프 전원을 소프트웨어로 차단하는 핀은 없습니다. 절전 전류는 계측이 필요합니다. 또한 병합 이미지를 0x0에서 기록하면 패딩 영역이 NVS에 영향을 줄 수 있으므로, 저장 데이터 보존은 펌웨어 갱신 방식·파티션 호환성과 함께 판단해야 합니다.

8직접 해볼 수 있는 실습 과제

읽기 → 계산 → 상태 추적 → 독립 기능 설계로 난도를 올리기

앞의 두 과제는 소스를 읽고 계산하는 것만으로 진행할 수 있습니다. 구현 과제는 자신의 학습용 브랜치에서 진행하세요. 보드용 앱을 만든다면 AGENTS.md의 규칙에 따라 기존 테스트 메뉴를 복제하지 않고 자신의 화면을 설계합니다. BSP와 비 UI 로직은 재사용할 수 있습니다.

실습 결과를 화면이 나왔다는 감상 대신, 입력·기대 상태·실패 조건이 있는 기록으로 남겨 보세요. 각 과제의 완료 기준은 이후 테스트 항목으로 바꿀 수 있습니다.

과제 1

메뉴 상태를 손으로 실행하기 입문 · 보드 불필요

읽을 코드: main/demo_navigation.c, tests/test_demo_navigation.c.

할 일: 항목 수를 3으로 놓고 UP → OK → OK 길게 누르기 → 종료 완료 순서의 상태표를 채웁니다. 두 번째에는 선택 항목의 사용 가능 값을 false로 바꿔 비교합니다.

제출물: 단계별 selected, active, action 표와, 종료 요청이 바로 active=-1을 만들지 않는 이유 한 문단.

완료 기준: 아래 표와 일치하고, 사용 불가 항목에서는 OK를 눌러도 진입하지 않는다고 설명합니다.

단계selected / active반환 행동
초기화0 / -1메뉴 대기
UP 클릭2 / -1REFRESH
OK 클릭 · 사용 가능2 / 2ENTER
OK 길게 누르기2 / 2EXIT 요청
종료 완료 호출2 / -1메뉴 상태 복귀
과제 2

1초·3초·10초 녹음의 예산표 입문 · 보드 불필요

읽을 코드: main/demo_audio.c의 SAMPLE_RATE, RECORD_SEC, CHUNK_SAMPLES와 components/bsp/src/bsp_display_lvgl.c의 버퍼 설정.

할 일: 16 kHz·16비트·모노 조건에서 전체 녹음 버퍼를 계산합니다. 한 번에 512샘플씩 처리해도 왜 전체 녹음 버퍼는 따로 필요한지 record_and_play()에서 확인합니다.

제출물: 녹음 길이별 바이트 표와, 10초를 지원하기 위한 두 가지 대안. 예를 들어 길이 제한 또는 순환 버퍼·외부 저장·전송 구조를 검토할 수 있습니다.

완료 기준: 1초 32,000바이트, 3초 96,000바이트, 10초 320,000바이트를 얻습니다. 청크를 줄이는 것만으로 rec 전체 할당이 줄어들지는 않음을 설명합니다.

과제 3

깨운 버튼이 바로 앱을 종료하지 않게 하기 중급 · 상태 로직부터

읽을 코드: main/key_guard.h, main/main.c의 on_key(), tests/test_key_guard.c.

할 일: 버튼을 누른 채 기기가 깨어나면, 입력 드라이버가 뒤늦게 시작되어 이를 새로운 길게 누르기로 판단하는 흐름을 그립니다. 이어 드라이버 시작 전에 버튼을 놓은 경우를 추가합니다.

제출물: 버튼 유지·해제·초기화 순서가 다른 세 시나리오와 각각의 armed 변화. 함수 호출 순서도 포함합니다.

완료 기준: 해제 이벤트로 보호를 해제하는 경우, 물리 전압 동기화로 해제하는 경우, 10초 제한에 도달한 다음 이벤트에서 해제하는 경우를 구별합니다. 제한시간에 도달했다고 독립 타이머가 자동 실행되는 구조는 아닙니다.

// main/key_guard.h를 읽으며 추적할 호출 예
key_guard_t g = {0};
key_guard_arm(&g, 1000, 10LL * 1000 * 1000);  // 시각 단위: 마이크로초, 제한: 10초
key_guard_sync_released(&g, true);
// 드라이버 관측 전에 버튼을 놓았어도 즉시 보호 해제
// 다음 key_guard_consume()은 false: 이벤트 정상 전달
과제 4

배터리 읽기와 화면 표시를 나누기 중급 · 구현 과제

읽을 코드: main/demo_battery.c, main/demo_audio.c의 작업 수명 관리.

할 일: 자신의 앱에서 I2C 읽기는 작업 태스크가 수행하고, 화면은 마지막 측정값만 표시하도록 설계합니다. 측정 시각·성공 여부를 데이터에 포함하고, 오래된 측정값은 구별해 표시합니다.

제출물: 새 화면 설계, 작업 → 큐 또는 보호된 스냅샷 → UI 흐름도, 시작·종료 계약. 큐가 가득 찼을 때 최신 값만 남길지 정합니다.

완료 기준: I2C 타임아웃 중에도 UI가 긴 읽기를 직접 기다리지 않습니다. 종료 응답 전에 화면을 삭제하지 않으며, 빠른 진입·종료를 반복해도 작업·타이머 수가 누적되지 않습니다.

과제 5

3초 녹음을 취소 가능한 스트리밍으로 확장하기 고급 · 보드 검증 필요

읽을 코드: record_and_play(), demo_audio_stop(), bsp_audio_set_format(), docs/reference/phoenixzhc/network-audio-streaming-and-memory.md.

할 일: 목적지를 먼저 정하고 고정 크기 버퍼로 PCM을 넘기는 구조를 설계합니다. 전송이 녹음보다 느릴 때 중단·유실·대기 중 어느 정책을 쓸지 정합니다. 네트워크를 붙인다면 현재 Wi-Fi 검색 데모와 별개로 연결·인증·실패 복구가 필요합니다.

제출물: 최대 RAM 표, 버퍼 소유권, 취소 흐름, 전송 지연 시나리오와 검증 기록.

완료 기준: 길이가 늘어도 버퍼 총량이 정한 상한을 넘지 않습니다. 전송 실패나 화면 종료 후에도 PCM 작업이 종료 응답을 보내며, 음질·입력 반응·실제 소비 전류는 기기에서 따로 확인합니다.

구현 단계의 검증은 저장소가 정의한 tools/validate.sh의 정적·펌웨어 게이트와 실기기 관찰을 구분해서 기록합니다. 호스트 테스트를 통과했더라도 마이크 입력이 실제로 잡히는지, 전원 상태가 목표에 도달했는지는 별개의 증거가 필요합니다.

9관련 기술 심화 학습 로드맵

6주 동안, 작은 순수 함수에서 실제 기기의 실패 복구까지

처음부터 음성 AI 앱 전체를 만들기보다, 한 주에 하나의 경계를 익히는 구성이 좋습니다. 입력과 상태를 먼저 검증하고, 그 위에 화면·작업·하드웨어를 하나씩 연결하면 고장의 원인을 좁히기 쉽습니다. 진도가 느리면 주차를 늘리되 산출물은 유지해 보세요.

주차배울 주제·읽을 소스주간 산출물과 확인 기준
1주C 구조체·열거형·포인터·함수 포인터
main/demo_navigation.[ch], main/demo.h
입력 10개 이상의 상태 전이 표. 사용 불가 항목과 종료 대기 상태 포함
2주RTOS 태스크·큐·알림·세마포어
main/main.c, main/demo_audio.c
입력 처리와 작업 종료 시퀀스. 큐 포화·알림 덮어쓰기·종료 타임아웃 설명
3주SPI·RGB565·DMA·LVGL 잠금
components/bsp/src/bsp_display_lvgl.c, components/bsp/src/bsp_display_rounding.c
화면 버퍼와 풀 예산표. 반경 0·최대 반경·잘못된 좌표의 기대 결과
4주ADC·I2C·레지스터·읽기 검증
components/bsp/src/bsp_button.c, components/bsp/src/bsp_battery.c
버튼 전압 경계표와 배터리 초기화 흐름도. 통신 실패를 값 0과 구분
5주I2S·PCM·오디오 지연·메모리 조각화
components/bsp/src/bsp_audio.c, main/demo_audio.c
고정 메모리 녹음 설계와 오류·취소 정책. 기기가 있으면 청크 간격과 재생 확인
6주절전·깨움·이미지 검증·CI
main/demo_low_power.c, tools/verify_firmware.py
자신의 앱 인수 조건. 빌드·호스트 테스트·기기 관찰 결과를 각각 기록

로드맵의 마지막에서 읽을 이미지 검증 코드

verify_flash_images()는 flash_args에 적힌 각 이미지가 존재하고 비어 있지 않은지, Flash 범위 안에 들어가는지, 서로 겹치지 않는지 확인합니다. 이어 병합 파일의 해당 위치가 원본 이미지 바이트와 같은지 비교합니다. 파일 이름이 맞는 것보다 더 강한 검증입니다.

verify_firmware_layout()은 파티션 테이블의 MD5 표식·중복 라벨·겹침을 검사하고, 앱 시작 주소가 실제 앱 파티션에 맞는지, 앱 크기가 그 파티션 안에 들어가는지 확인합니다. verify_extra_image_partitions()는 추가 이미지도 한 파티션 안에 완전히 들어가도록 검사합니다.

완주 기준
잘못된 산출물을 설명할 수 있으면 더 많이 배운 것입니다

“빌드는 됐지만 앱 파티션보다 크다”, “정상 파일이지만 병합 위치가 다르다”, “화면은 뜨지만 종료 후 작업이 남는다”를 서로 다른 실패로 구분해 보세요. 이 구분이 있어야 테스트와 로그가 실제 문제 해결로 이어집니다.

다음 확장 방향은 한 가지씩 선택하기

폰트 안내의 대상은 중국어지만, UTF-8 디코딩·글리프 포함 여부·폰트 선택·레이아웃을 따로 확인하라는 접근은 한글에도 적용됩니다. 중국어용 서브셋을 켜는 것만으로 한글까지 지원된다고 가정하지 마세요.

10핵심 키워드 사전

파일을 다시 읽을 때 옆에 놓고 볼 작은 사전
용어쉬운 설명
ESP-IDFEspressif 칩용 SDK. 컴파일·설정·드라이버·RTOS를 함께 제공하며 이 저장소의 기준은 5.5.3입니다.
BSP특정 보드의 핀과 주변 칩을 다루는 공통 계층. 앱은 이를 통해 장치를 사용합니다.
MCU프로그램을 실행하고 주변장치를 제어하는 마이크로컨트롤러. 이 보드는 ESP32-C3를 사용합니다.
Flash / RAMFlash는 전원이 꺼져도 프로그램·데이터를 보관하고, RAM은 실행 중 작업 공간입니다. 둘의 용량은 서로 대체되지 않습니다.
PSRAM추가 작업 메모리로 쓰는 외부 RAM. 이 기기에는 없어 내부 RAM 예산이 중요합니다.
태스크RTOS가 번갈아 실행하는 작업 단위. 입력·오디오·화면의 긴 일을 나누는 데 씁니다.
콜백이벤트가 발생했을 때 호출하도록 등록한 함수. 어떤 태스크에서 호출되는지 알아야 잠금과 대기 규칙을 지킬 수 있습니다.
큐이벤트를 순서대로 담는 유한한 대기 공간. 이 저장소의 입력 큐는 8개 항목을 담습니다.
태스크 알림특정 태스크에 값이나 상태를 전달하는 수단. 여기서는 오디오·절전 명령에 쓰며 값 덮어쓰기 방식을 사용합니다.
세마포어작업 완료 같은 신호를 주고받는 동기화 수단. 오디오 종료 확인에는 이진 세마포어가 쓰입니다.
잠금공유 대상을 동시에 건드리지 않도록 접근을 조절하는 장치. 외부 태스크의 LVGL 객체 접근은 잠금이 필요합니다.
상태 머신현재 상태와 입력을 바탕으로 다음 상태·행동을 결정하는 규칙. 메뉴와 깨움 입력 보호가 좋은 예입니다.
수명 관리대상을 만들고 사용하는 동안 유지한 뒤, 접근이 끝나면 정리하는 일. 삭제 전 작업 중단 확인이 포함됩니다.
멱등성완료한 작업을 다시 호출해도 결과를 불필요하게 바꾸지 않는 성질. 동시 호출 안전성과는 별개입니다.
ADC / 저항 사다리전압을 숫자로 읽는 회로 / 버튼마다 다른 저항으로 전압을 달리하는 입력 방식. 세 버튼이 GPIO0을 공유합니다.
I2C주소로 여러 칩을 구별하는 직렬 버스. 이 보드에서는 ES8311 제어와 CW2017 통신이 같은 버스를 씁니다.
I2S / PCM디지털 오디오 전송 규칙 / 일정 간격으로 측정한 음성 샘플 데이터. 파일 압축 형식과 구분합니다.
SPI클록과 데이터 선으로 주변장치에 빠르게 정보를 보내는 버스. 여기서는 LCD 픽셀 전송에 씁니다.
DMACPU가 모든 바이트를 직접 옮기지 않도록 전송을 맡기는 하드웨어 기능. 사용할 수 있는 메모리와 버퍼 수명에 제약이 있습니다.
RGB565빨강 5비트·초록 6비트·파랑 5비트로 한 픽셀을 표현하는 16비트 색 형식입니다.
flush / stride그린 픽셀을 화면 장치로 보내는 단계 / 버퍼에서 다음 행으로 이동하는 바이트 간격입니다.
메모리 풀 / 조각화정해 둔 공간 안에서 할당하는 방식 / 빈 공간이 여러 조각으로 나뉘는 현상. 총 여유와 단일 할당 가능 크기가 달라질 수 있습니다.
KiB / kB각각 1,024바이트 / 1,000바이트. 19,200바이트는 19.2 kB이자 18.75 KiB입니다.
NVS / 파티션전원 차단 뒤에도 남는 키·값 저장 방식 / Flash의 용도별 영역. 현재 기본 표에는 NVS·PHY·factory 앱이 있습니다.
OTA통신을 통한 펌웨어 갱신. 현재 기본 파티션에는 이를 위한 앱 슬롯이 따로 없습니다.
BLE 광고 / GATT주변에 존재를 알리는 동작 / 연결 후 서비스·특성으로 데이터를 다루는 규칙. 기본 데모는 비연결 광고입니다.
SOC / 배터리 프로필추정 충전 잔량 / 셀 특성에 맞춘 잔량계 설정 데이터. 500 mAh라는 용량 표시만으로 프로필을 대체할 수 없습니다.
light / deep sleep이 데모에서는 각각 타이머 후 실행을 이어 가는 절전 / 깨운 뒤 앱이 다시 시작하는 절전 경로입니다.
호스트 테스트 / 스텁개발 PC에서 실행하는 검사 / 칩·RTOS 함수를 대신하는 가짜 인터페이스. 물리적 하드웨어 관찰을 대체하지는 않습니다.
잠금 파일 / CI선택된 의존성을 고정한 기록 / 코드 변경 때 자동 검증을 실행하는 환경입니다.

비슷해 보이는 용어를 쌍으로 구분하면 소스 읽기가 쉬워집니다. Flash와 RAM, 광고와 연결, 종료 요청과 종료 완료, 빌드 성공과 기기 검증을 각각 다른 항목으로 생각해 보세요.

11참고 링크

코드의 근거와, 다음에 읽을 공식 자료

공식 저장소와 프로젝트 문서

본문의 핵심 구현을 다시 읽는 링크

관련 기술의 공식 자료

현재 상태 (2026-10-23 GitHub 기준)

항목상태
선정GitHub Trending C Daily #9 · 2026-10-23
스타 / 포크706 / 281 · 2026-10-23 GitHub 기준
생성일2026-07-31
주 언어 / 설명C · FOLOTOY AI Passport develop resources for Agent
최신 릴리스없음 · 릴리스 자동화 워크플로는 존재
최근 push2026-10-21
최근 커밋006c227 · 2026-10-21 · Wanli 500 mAh 배터리 프로필 수정 PR #110 병합
코드 규모C 5,089줄 + 헤더 988줄, 이 중 테스트·스텁 2,159줄. Python 2,729줄. 빈 줄·주석 포함
문서 / 파일 수Markdown 17,868줄 · 전체 추적 파일 351개 · 다운로드 의존성 제외
기준 환경ESP-IDF 5.5.3 · ESP32-C3 · 8 MB Flash · PSRAM 없음 · LVGL 9.5.0
기본 앱 범위7개 하드웨어 테스트 페이지 · 대화형 AI 서비스 연결 없음
문서 언어영어 기본 문서와 중국어 번역 쌍. 기본 경로의 제품 소개는 docs/README.md
라이선스루트 LICENSE는 MIT. 외부 의존성·폰트·응용 콘텐츠의 조건은 각각 확인
쓰기 전에 알아둘 한계
가능성·기본 기능·실제 검증을 나누어 판단하세요

① 기본 펌웨어의 범위. AI 제품 소개와 응용 사례는 확장 방향을 보여 줍니다. 기본 빌드는 하드웨어 테스트 기준선이며, LLM·음성 인식·TTS·완성된 무선 데이터 앱이 포함된 것으로 해석하면 안 됩니다. 파생 앱은 자체 UI를 설계해야 한다는 저장소 규칙도 있습니다.

② 하드웨어와 메모리의 범위. PSRAM이 없고 96,000바이트 녹음 버퍼 같은 큰 단일 할당이 존재합니다. 기본 버퍼 설정이 자신의 앱에도 적합하다는 보장은 없습니다. 화면 갱신·오디오·저장·무선을 함께 쓸 때는 최악 조건의 지연과 연속 메모리를 확인해야 합니다.

③ 검증의 범위. 소스에 테스트와 CI 설정이 있다는 사실만으로 특정 빌드의 통과·실제 음질·전류·배터리 정확도가 증명되지는 않습니다. 빌드 결과, 호스트 테스트, 실기기 관찰을 따로 확인하세요. 배터리 프로필의 쓰기·읽기 일치 역시 충방전 전 구간의 정확도 검증과 다릅니다.

④ 시점과 데이터 보존. 코드 설명은 커밋 006c227 기준이며 이후 브랜치와 다를 수 있습니다. 하드웨어 가이드의 화면 버퍼 값에는 현재 구현과 차이가 있습니다. OTA 슬롯은 기본 제공되지 않으며, 병합 이미지 갱신이 저장 데이터 보존을 보장하지 않습니다.