# 메인 MCU 마이그레이션 계획: Raspberry Pi Zero 2 W → STM32F407VGT6 (Cortex-M4) > 대상: SHT30 온습도 모니터링 시스템 펌웨어 이식 (SHT30 단일 보드) > 작성일: 2026-06-09 · 구현 완료: 2026-06-09 · 단일 보드 전환: 2026-06 ## 0. 구현 현황 (✅ 완료) 펌웨어가 `firmware/` 에 구현되었고 레거시 RPi 펌웨어(`raspberry_pi/`, `systemd/`)는 제거되었습니다. 시스템은 **SHT30 온습도 단일 보드**로 전환되었습니다(누수 보드/누수 펌웨어는 제거됨). **산출물**: 검증된 이식 코어(`common/{sha256_sw,hexutil,sig,jsonbody,sht30_convert,httpapi}.c`), 타깃 모듈(`net/ethernetif/tls_mbedtls/timesync/sht30/reporter/applog/watchdog/bsp/rtos_hooks`), 보드 앱(`board_sht30/`), 설정(`config/{FreeRTOSConfig,lwipopts,mbedtls_config,stm32f4xx_hal_conf}.h`), 빌드(`CMakeLists.txt`, `cmake/`, `ld/`, `scripts/`), 인증서(`certs/`), 문서(`README.md`, `docs/`). **검증 (이 환경에서 실행 가능한 범위)**: - 이식 코어 ARM 크로스컴파일 게이트: `arm-none-eabi-gcc -mcpu=cortex-m4 ... -Werror -Wconversion` 무경고 통과. - wire 계약 패리티: Python 레퍼런스(=C와 동일 알고리즘) → 서버 PHP `verify_signature_raw` 수용 통과(`firmware/test/host/parity_test.py`). - PHP 서명/보안 회귀: `tests/raw_body_signature_test.php`, `tests/security_hardening_test.php` 통과. - ⚠️ **전체 펌웨어 링크/실행은 미검증**: HAL/FreeRTOS/lwip/mbedtls 는 폐쇄망 빌드 머신에서 벤더링되므로(`firmware/third_party`), 실제 보드 빌드·플래시·HIL 시험은 운영 측에서 수행해야 함. **다중 에이전트 코드리뷰 후 수정된 결함**: - (CRITICAL) TLS 인증서 유효기간(notBefore/notAfter) 미검증 → `MBEDTLS_HAVE_TIME_DATE` + RTC/SNTP 시간 공급(`tls_platform_time`), 시간 미동기 시 connect 차단. - (CRITICAL) FreeRTOS 정적메모리 콜백 중복정의(커널 제공과 충돌) → `rtos_hooks.c` 에서 제거(커널 소유). - (HIGH) LwIP 코어락 위반: PHY 링크 통지 raw API → `netifapi_*` + 콜백 내부 raw `netif_set_up`(중첩락 데드락 회피). - (CRITICAL/통합) 로그 UART PA2(=RMII MDIO) 충돌 → USART3(PD8/PD9), FreeRTOS 훅 단일 소유, `-u _printf_float`. - (MEDIUM/LOW) `close_notify` 무한루프 상한, `dns_setserver` 코어락, HW RNG `bsp_rand32`, SHT30 태스크 스택 12KB, CA 자리표시자 동작 주석. **배포 전 하드웨어 확정 필요 — TODO(hw)** (상세: `firmware/docs/HARDWARE.md`, `PORTING_NOTES.md`): LAN8720 PHY 주소·RMII 50MHz REF_CLK 소스, HSE 주파수(PLLM), RTC 클럭원(LSE 32.768k vs LSI), 상태 LED 핀, MAC 주소(노드별 고유), SHT30 I2C 핀(PB6/PB7), 실제 Cafe24 루트 CA(`certs/server_ca.c`), `secrets.h`(APP_API_KEY). --- ## 1. 결정 사항 (확정) | 항목 | 현재 (RPi) | 신규 (STM32F407VGT6) | |---|---|---| | 메인 MCU | RPi Zero 2 W (Linux/Python) | STM32F407VGT6 (Cortex-M4F, bare HW) | | 네트워크 | WiFi | **유선 Ethernet (LAN8720 RMII PHY)** | | TLS/HTTPS | OpenSSL (`requests`) | **온칩 mbedTLS + LwIP** | | 보드 구성 | RPi SHT30 노드 | **STM32 SHT30 단일 보드** (SHT30=sensor_id 2) | | 임계 판정/SMS | 서버측 처리 | **서버 `config.php` METRIC_* 에서 판정** (펌웨어는 원값만 보고) | | 동시성 | Python threads | **FreeRTOS tasks** | | 시간원 | NTP (OS) | **SNTP (LwIP) → RTC** | | 설정 | env / systemd | **컴파일 타임 `config.h` + (옵션) flash 설정** | | 로깅 | rotating file log | **UART 콘솔 + (옵션) 서버 로그** | | 프로세스 관리 | systemd 재시작 | **IWDG 워치독 + 재연결 로직** | 핵심 원칙: **서버측 PHP API는 변경하지 않는다(가능한 한).** 펌웨어가 기존 서명 규약·요청 포맷을 바이트 단위로 그대로 재현하여 무중단으로 전환한다. --- ## 2. 하드웨어 사양 / 제약 (STM32F407VGT6) - Cortex-M4F @ 168 MHz, **Flash 1 MB**, **RAM 192 KB** (112+16 KB SRAM + 64 KB CCM) - Ethernet MAC 내장 → 외부 PHY 필요 (**LAN8720, RMII**) - 하드웨어 **RNG 있음** (TLS 엔트로피에 사용) — 중요 - 하드웨어 해시/암호 가속기 **없음** (CRYP/HASH는 F415/417 전용) → **SHA-256/AES는 소프트웨어(mbedTLS sw)** - I²C 3채널, USART 다수, EXTI, RTC, IWDG ### 메모리 예산 (가장 큰 리스크) - mbedTLS TLS1.2 클라이언트(ECDHE-RSA + AES-GCM) 기본 record buffer 2×16 KB = 32 KB. → **`MBEDTLS_SSL_IN/OUT_CONTENT_LEN`을 4~6 KB로 축소** (서버 응답이 작으므로 가능) → LwIP pbuf pool과 합산 시 SRAM 빠듯 → **FreeRTOS task stack을 CCM(64 KB)에 배치** - RSA-2048 핸드셰이크는 소프트웨어로 ~1–2 s 소요(허용 가능, 보고 주기가 길다) --- ## 3. 현재 코드 → 펌웨어 모듈 매핑 | 현재 (Python) | 신규 (C 펌웨어 모듈) | 비고 | |---|---|---| | `sht30_monitor.py` | `app_sht30.c` (Task) | SHT30 보드 메인 | | `make_signature()` | `sig.c : sig_raw_body()` | mbedtls_sha256 (raw-body) | | 표준 JSON 직렬화 | `jsonbody.c : jb_sht30_event()` | raw-body 서명 → 정규화 불필요 | | `post_to_server()` | `api.c : api_post_json()` | TLS + 재시도 루프 | | `post_reading()` | `reporter.c : report_sht30_event()` | 본문→서명→POST→재시도 | | `run_loop()` / `periodic_reporter()` | `Task_PeriodicReport` | vTaskDelay 기반 | | `read_sht30()` | `sht30.c : sht30_read()` | I²C HAL | | `crc8_sht3x()` | `sht30.c : sht30_crc8()` | 동일 알고리즘 | | `classify_reading()` | reporter 호출부 `metric_status` | plausibility 만. 임계는 서버 | | `config.py` | `config.h` (+ flash 설정 옵션) | | | `setup_logger()` | `log.c` (UART) | | | `systemd/*.service` | IWDG + 메인 재연결 | | --- ## 4. 펌웨어 아키텍처 (FreeRTOS) ``` main() ├─ HAL/Clock/RNG/IWDG init ├─ Ethernet/LwIP init (RMII LAN8720) ├─ Task_Net : 링크 업 대기 → DHCP(or static) → SNTP로 RTC 설정 → "online" 플래그 ├─ Task_Sensor : SHT30 주기(300s) I2C 측정 → temperature_c/humidity_percent + metric_status ├─ Task_Report : SHT30_REPORT_INTERVAL(300s) 로 api_post_json() (startup → periodic) └─ Task_Watchdog: 각 task 헬스 비트 확인 후 IWDG refresh ``` - SHT30 측정값(원값 + plausibility `metric_status`)을 서버로 POST → **서버가 `config.php` METRIC_* 임계와 비교해 고온/저온/고습/저습 시 SMS 발송**. - TLS 컨텍스트는 매 요청마다 connect→handshake→POST→close (메모리 절약). 보고 주기가 길어 성능 무관. --- ## 5. 단계별 마이그레이션 계획 ### Phase 0 — 환경/툴체인 (폐쇄망 대응) - STM32CubeIDE 또는 `arm-none-eabi-gcc` + CMake + OpenOCD/ST-Link. - **오프라인 번들**: 툴체인 설치본, CubeF4 HAL 팩, FreeRTOS·LwIP·mbedTLS 소스를 벤더링(`third_party/`에 고정 버전 포함). 빌드 시 외부 다운로드 0건. - ST-Link/J-Link로 플래시 + UART 콘솔 확보. ### Phase 1 — 프로젝트 스켈레톤 - CubeMX로 클럭(HSE 8 MHz → 168 MHz), RMII, I²C, USART, RNG, IWDG, RTC 핀맵 생성. - FreeRTOS(CMSIS-RTOS2), LwIP, mbedTLS 통합. CCM RAM에 task stack 배치(linker script). - "Hello UART" + LED blink로 부팅 검증. ### Phase 2 — 네트워크 브링업 - LAN8720 RMII 드라이버 + LwIP. DHCP 우선, 실패 시 static fallback(`config.h`). - **SNTP 클라이언트로 시간 동기 → RTC 설정** (TLS 인증서 유효기간 검증·`timestamp` 필드에 필수). 순서 주의: SNTP는 UDP라 TLS 불필요 → TLS보다 먼저 수행(닭-달걀 회피). - 링크 다운/DHCP 실패 시 백오프 재시도. ### Phase 3 — TLS + HTTPS 클라이언트 - mbedTLS config 축소(`mbedtls_config.h`): TLS1.2만, ECDHE-RSA + AES-GCM, `MBEDTLS_SSL_IN/OUT_CONTENT_LEN ≈ 4096`, MFL extension. - 엔트로피: 하드웨어 RNG 연결. - **서버 루트 CA PEM을 flash에 임베드**(Cafe24 인증서 체인 확인 필요). - `api_post_json(url_path, body, out_resp)` 구현: connect→handshake(SNI)→ `POST` 헤더 작성→본문 전송→상태코드 파싱. - 검증: `https://your-domain/.../sensor_data.php`로 200 수신. ### Phase 4 — 서명 (raw-body 방식, R1 해결로 단순화) 서버가 **raw-body 서명**을 지원하도록 변경 완료 (PHP `verify_signature_raw`, 본 저장소 적용됨): - 서버 검증식: `sha256(API_KEY . 요청_본문_바이트_그대로)`, 값은 HTTP 헤더 **`X-Signature`**로 전송. - 펌웨어는 **자신이 전송하는 본문 바이트 그대로**를 SHA-256으로 서명만 하면 된다. → 키 정렬·부동소수 포맷·슬래시 이스케이프 **재현 불필요** (SHT30 float 문제 원천 제거). - 클라이언트 절차: 1. `body` 문자열 1회 생성(필드 순서 자유). 2. `sig = sha256_hex(API_KEY || body)`. 3. `POST` 시 `Content-Type: application/json` + `X-Signature: ` 헤더, 본문=`body`. 4. 본문의 `signature` 필드는 불필요(있어도 서버가 무시). - 서버는 `X-Signature` 헤더가 없으면 레거시 본문 필드 서명으로 폴백 → **RPi 무중단 공존**. - 회귀 테스트: `tests/raw_body_signature_test.php` (통과 확인됨). ```c // 펌웨어: 전송 바이트 그대로 서명 sig_sha256_hex(body, sig_hex); // sha256(API_KEY || body) // HTTP 헤더에 "X-Signature: " 추가, 본문은 body 그대로 전송 ``` ### Phase 5 — SHT30 보드 앱 (sensor_id=2) - I²C: 예 `I2C1`(PB6/PB7), addr `0x44`, cmd `0x2C06`(high-repeatability), 20 ms 대기, 6 byte read. - `sht30_crc8`(poly 0x31, init 0xFF) 온도/습도 각각 검증 — Python과 동일. - 변환: `T=-45+175*raw/65535`, `RH=100*raw/65535`, **소수 2자리 반올림**. - 필드: `metric_type="sht30"`, `temperature_c`, `humidity_percent`, `metric_status`, `app_version` (`is_leak` 필드 없음). - 이벤트: `startup`(부팅 시 1회) → `periodic`(300 s) 주기 측정 보고. - ⚠️ **float 서명 패리티**(아래 리스크 R1) 반드시 처리. ### Phase 6 — 서버 임계 판정 / SMS (펌웨어 외부) - 펌웨어는 `temperature_c`/`humidity_percent` **원값** + `metric_status`(plausibility) 만 보고한다. - 고온/저온/고습/저습 **운영 임계는 서버 `php/config.php` 의 `METRIC_*` 상수**가 판정하고, 초과 시 `sensor_data.php` 가 SMS 게이트웨이로 문자를 발송한다. - 폐쇄망 재플래시 비용을 피하려 임계를 서버에 두므로, 임계 변경 시 펌웨어 재빌드/재배포가 불필요하다. ### Phase 7 — 신뢰성/운영 - IWDG 워치독, TLS/네트워크 실패 시 지수 백오프 재연결. - 브라운아웃 리셋(BOR) 설정, 부팅 카운터. - 로그: UART(개발) + (옵션) 실패 통계 서버 보고. - 설정 변경: flash 마지막 섹터에 `device_id/location/static IP/API_KEY` 저장 + UART 명령으로 갱신(옵션). 미구현 시 `config.h` 재빌드. ### Phase 8 — 검증 - §7 호스트 서명 패리티 단위 테스트 통과. - 실서버(또는 스테이징 PHP)로 `startup/periodic`(SHT30) E2E + 서버 임계 초과 시 SMS 트리거 확인. - 24 h 안정성 + 망 단절 복구 + 워치독 동작 시험. ### Phase 9 — 빌드/배포 (폐쇄망) - 단일 `.bin`/`.hex` 산출, 오프라인 플래시 절차 문서화. - 단일 빌드 타깃 `sht30_fw`(컴파일 플래그 `BOARD_SHT30`). - 펌웨어 버전 = `app_version` 필드로 보고(`v2606-sht30`, `firmware/VERSION`=`fw-2.0.0`). --- ## 6. 핵심 코드 스케치 ### 6.1 본문 생성 + raw-body 서명 (키 정렬·정규화 불필요) ```c // 필드 순서/포맷 자유 — 서버는 보낸 바이트 그대로 서명을 검증한다. // SHT30 본문에는 is_leak 필드가 없다(온습도 원값 + metric_status 만 보고). int build_body_sht30(char *out, size_t n, const Sht30Msg *m) { return snprintf(out, n, "{\"device_id\":\"%s\",\"device_location\":\"%s\"," "\"sensor_id\":%d,\"sensor_name\":\"%s\",\"event_type\":\"%s\"," "\"metric_type\":\"sht30\",\"temperature_c\":%.2f,\"humidity_percent\":%.2f," "\"metric_status\":\"%s\",\"app_version\":\"%s\",\"timestamp\":%ld}", m->device_id, m->device_location, m->sensor_id, m->sensor_name, m->event_type, m->temperature_c, m->humidity_percent, m->metric_status, m->app_version, (long)m->timestamp); } // sha256(API_KEY || body) → hex. body는 전송 바이트와 100% 동일해야 한다. void sig_sha256_hex(const char *body, char hex[65]) { uint8_t d[32]; mbedtls_sha256_context c; mbedtls_sha256_init(&c); mbedtls_sha256_starts(&c, 0); mbedtls_sha256_update(&c, (const uint8_t*)API_KEY, strlen(API_KEY)); mbedtls_sha256_update(&c, (const uint8_t*)body, strlen(body)); mbedtls_sha256_finish(&c, d); mbedtls_sha256_free(&c); for (int i=0;i<32;i++) sprintf(hex+i*2, "%02x", d[i]); } // POST 시 헤더에 "X-Signature: " 추가, 본문은 위 body 그대로 전송. ``` 주의(유일): 서명한 `body` 바이트와 실제 전송 바이트가 **반드시 동일**해야 한다. 문자열 필드에 `"`나 `\`가 들어갈 수 있으면 JSON 이스케이프만 적용(서버는 그 바이트로 검증하므로 PHP 포맷 재현 불요). ### 6.2 SHT30 읽기 (HAL) ```c HAL_I2C_Master_Transmit(&hi2c1, 0x44<<1, (uint8_t[]){0x2C,0x06}, 2, 100); HAL_Delay(20); uint8_t b[6]; HAL_I2C_Master_Receive(&hi2c1, 0x44<<1, b, 6, 100); if (sht30_crc8(b,2)!=b[2] || sht30_crc8(b+3,2)!=b[5]) return ERR_CRC; float t = -45.0f + 175.0f*((b[0]<<8|b[1])/65535.0f); float rh = 100.0f*((b[3]<<8|b[4])/65535.0f); ``` --- ## 7. 리스크 & 대응 | ID | 리스크 | 영향 | 대응 | |---|---|---|---| | **R1** | ~~float 서명 패리티 (SHT30)~~ | ~~SHT30 보고 전건 401 가능~~ | ✅ **해결됨**: PHP를 **raw-body 서명**으로 전환(`verify_signature_raw`, `X-Signature` 헤더). 본문 바이트 그대로 서명 → JSON 재직렬화/float 포맷/슬래시 이스케이프 의존 제거. 레거시 본문 필드 서명은 RPi 호환을 위해 이중 모드로 유지. 상세 §5 Phase 4 참조 | | **R2** | TLS RAM 부족(192 KB) | 핸드셰이크 실패/리셋 | content_len 4 KB 축소, CCM에 stack, LwIP pbuf 튜닝 | | **R3** | Cafe24 인증서 cipher/체인/만료 | TLS 연결 불가 | 사전 `openssl s_client`로 체인·cipher 확인, 루트 CA 임베드, 만료 시 펌웨어 업데이트 절차 | | **R4** | SNTP 실패 → TLS 시간검증·timestamp 오류 | 인증/중복판정 오류 | SNTP 재시도, 실패 시 RTC 보존값 사용 + 경보 | | **R5** | `/` 이스케이프 누락 | 서명 불일치(값에 `/` 포함 시) | 정규화기에서 `\/` 처리 | | **R6** | 임계 판정을 서버에 위임(폐쇄망 재플래시 회피) | 임계 변경이 서버 운영에 종속 | 경보 임계는 서버 `config.php` METRIC_* 가 소유. 펌웨어는 원값+`metric_status` 만 보고(§5 Phase 6) | --- ## 8. 산출물 / 디렉터리(제안) ``` firmware/ ├─ third_party/ {CMSIS, STM32F4_HAL, FreeRTOS, lwip, mbedtls} # 벤더링(폐쇄망) ├─ common/ {api.c, sig.c, jsonbody.c, net.c, sntp.c, log.c, config.h} ├─ board_sht30/ {app_sht30.c, sht30.c, main.c} ├─ certs/ server_ca.pem.h ├─ test/ test_signature_parity.c # 호스트 빌드, PHP 기대값과 대조 └─ CMakeLists.txt ``` ## 9. 다음 작업 1. **R1/R3 사전조사**: Cafe24 TLS 체인 확인, float 서명 정책 결정(PHP raw-body 서명으로 전환 권장). 2. Phase 0–1 스켈레톤 + UART/LED 부팅. 3. Phase 2–3 네트워크/TLS로 `sensor_data.php` 200 확보. 4. Phase 4 서명 패리티 테스트 그린. 5. Phase 5 SHT30 앱 완성 → E2E(+ 서버 임계 SMS 확인).