# 메인 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: <sig>` 헤더, 본문=`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: <sig_hex>" 추가, 본문은 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: <hex>" 추가, 본문은 위 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 확인).
