initial commit

This commit is contained in:
jhChun
2026-04-08 16:58:54 +09:00
commit 82e33d8bf9
2578 changed files with 1590432 additions and 0 deletions

View File

@@ -0,0 +1,603 @@
# nRF52840 내장 Flash (FDS) 사용 가이드
## 1. 개요
### FDS (Flash Data Storage)란?
Nordic SDK에서 제공하는 내장 Flash 저장소 라이브러리.
SoftDevice와 함께 동작하며, **자동 Wear Leveling**과 **Garbage Collection**을 지원한다.
본 프로젝트에서는 디바이스 설정(시리얼 번호, 패스키, 측정 파라미터 등)을 FDS에 저장하고,
`dr_memRead` / `dr_memWrite` 추상화 API를 통해 BLE 명령어로 읽기/쓰기한다.
### 저장소 구조
```
┌──────────────────────────────────────────────┐
│ BLE 명령어 │
│ (mrh?, mrs?, mwh?, mws?, ...) │
└──────────────┬───────────────────┬────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ dr_memRead/Write │ │ dr_memRead/Write │
│ (FDS backend) │ │ (W25Q32 backend) │
└──────────┬───────────┘ └──────────┬───────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ 내장 Flash (FDS) │ │ 외장 Flash (W25Q32) │
│ - config_data_t │ │ - AGC Gain (96B) │
│ - 41 bytes packed │ │ - Life Cycle (4B) │
│ - Wear Leveling │ │ - SPI 인터페이스 │
│ - RBP 보호 │ │ - 4MB │
└──────────────────────┘ └──────────────────────┘
```
### FDS vs 외장 W25Q32RV 비교
| 항목 | 내장 FDS | 외장 W25Q32RV |
|------|----------|---------------|
| 용량 | ~400KB (50 pages x 4KB x 2048 words) | 4MB |
| 인터페이스 | SoftDevice API | SPI (SPIM) |
| 자동 초기화 | `fds_init()` | 수동 (`w25q32_power_on()` + `w25q32_init()`) |
| Wear Leveling | FDS 내장 (자동) | 없음 (직접 구현) |
| 쓰기 단위 | 4 bytes (word) | 256 bytes (page) |
| 지우기 단위 | 자동 GC | 4KB (sector erase) |
| 부팅 시 사용 | 즉시 가능 | 불가 (SPIM + BSP 이벤트 충돌) |
| 용도 | 디바이스 설정 (7개 필드) | 캘리브레이션 + 데이터 (2개 필드) |
---
## 2. 데이터 구조
### config_data_t (fstorage.h)
FDS에 저장되는 디바이스 설정 구조체. `#pragma pack(1)`으로 패딩 없이 41 bytes.
```c
#pragma pack(1)
typedef struct
{
uint32_t magic_number; /* 4B - 0x20231226 (데이터 무결성 확인용) */
char hw_no[12]; /* 12B - HW 번호 (공장 입력) */
char serial_no[12]; /* 12B - 시리얼 번호 */
uint8_t static_passkey[6]; /* 6B - BLE 페어링 패스키 */
uint8_t bond_data_delete; /* 1B - 본딩 데이터 삭제 플래그 */
int8_t reset_status; /* 1B - 리셋 상태 */
uint8_t pd_adc_cnt; /* 1B - ADC 샘플링 횟수 */
uint16_t pd_delay_us; /* 2B - PD 지연 시간 (us) */
} config_data_t;
```
### 메모리 레이아웃 (바이트 오프셋)
```
Offset Size 필드 기본값 FDS에 저장
────── ──── ────────────── ──────────────── ──────────
0x00 4B magic_number 0x20231226 O
0x04 12B hw_no (0x00 x 12) O
0x10 12B serial_no "2025AAAAT001" O
0x1C 6B static_passkey "123456" O
0x22 1B bond_data_delete 1 O
0x23 1B reset_status 99 O
0x24 1B pd_adc_cnt 8 O
0x25 2B pd_delay_us 8000 O
────── ────
Total: 39B (packed) → FDS word 정렬: (39 + 3) / 4 = 10 words (40B)
```
### FDS 레코드 식별자
```c
#define CONFIG_FILE (0x8010) /* File ID */
#define CONFIG_REC_KEY (0x7010) /* Record Key */
#define CONFIG_MAGIC_NUMBER_VALUE (0x20231226)
```
전체 `config_data_t`를 **하나의 FDS 레코드**로 저장한다.
읽기/쓰기 시 구조체 전체를 한 번에 처리한다.
---
## 3. 부팅 시퀀스
### 초기화 순서 (main.c)
```
[6] ble_stack_init() ← SoftDevice 활성화 (FDS 필수 전제)
[6.5] fs_storage_init() ← FDS 이벤트 핸들러 등록 + fds_init()
config_load() ← FDS에서 config_data_t 로드 → m_config 전역변수
[6.6] load_device_configuration() ← m_config → 런타임 전역변수 복사
[7] gap_params_init() ← SERIAL_NO로 BLE 디바이스 이름 설정
[8] advertising_init() ← 광고 시작
```
**핵심**: FDS는 반드시 `ble_stack_init()` 이후에 초기화해야 한다.
SoftDevice가 활성화되어야 FDS 이벤트(SOC 이벤트)가 정상 발생한다.
### fs_storage_init() 동작
```c
void fs_storage_init(void)
{
ret_code_t rc;
/* 1. 이벤트 핸들러 등록 (반드시 fds_init 이전) */
rc = fds_register(fds_evt_handler);
APP_ERROR_CHECK(rc);
/* 2. FDS 초기화 시작 (비동기) */
rc = fds_init();
APP_ERROR_CHECK(rc);
/* 3. FDS_EVT_INIT 이벤트 대기 (최대 3초) */
wait_for_fds_ready();
/* 4. FDS 상태 확인 */
fds_stat_t stat = { 0 };
rc = fds_stat(&stat);
APP_ERROR_CHECK(rc);
}
```
### config_load() 동작 흐름
```
config_load() 시작
├─ fds_record_find() 호출
│ │
│ ├─ 실패 (rc != 0) ──→ 재시도 (최대 10회, 100ms 간격)
│ │ │
│ │ ├─ 10회 모두 실패 → 새 레코드 쓰기 (기본값)
│ │ └─ 성공 → 아래 진행
│ │
│ └─ 성공 (rc == 0)
│ │
│ ├─ fds_record_open()
│ │ ├─ 실패 → 레코드 삭제 + GC + 기본값 새로 쓰기
│ │ └─ 성공 → memcpy → m_config
│ │
│ ├─ fds_record_close()
│ │
│ └─ magic_number 검증
│ ├─ 불일치 → 레코드 삭제 + 기본값으로 덮어쓰기
│ └─ 일치 → "Loaded OK" (정상 완료)
└─ 새 레코드 쓰기 (기본값)
├─ fds_default_value_set()
├─ fds_record_write() (비동기)
├─ fds_flag_write == false 대기 (이벤트 핸들러에서 해제)
└─ config_load() 재진입 (goto cfg_load_start)
```
### 재시도 로직 (핵심!)
FDS가 완전히 준비되기 전에 `fds_record_find()`를 호출하면 실패할 수 있다 (rc=34313).
이를 "레코드 없음"으로 처리하면 기존 저장 데이터를 기본값으로 덮어쓰게 된다.
```c
uint8_t cfg_retry = 0;
cfg_load_start:
memset((char *)&desc, 0, sizeof(desc));
memset((char *)&tok, 0, sizeof(tok));
rc = fds_record_find(CONFIG_FILE, CONFIG_REC_KEY, &desc, &tok);
/* FDS가 아직 완전히 준비되지 않았을 수 있음 - 기본값 쓰기 전 재시도 */
if (rc != NRF_SUCCESS && cfg_retry < 10) {
cfg_retry++;
nrf_delay_ms(100);
goto cfg_load_start;
}
```
**이 재시도 로직이 없으면**: 부팅할 때마다 저장된 설정이 기본값으로 덮어씌워질 수 있다.
---
## 4. 데이터 저장 (config_save)
### 동작 방식
```c
void config_save(void)
{
/* 1. 이전 FDS 작업 진행 중이면 스킵 (비차단) */
if (fds_flag_write) return;
/* 2. magic_number 보정 */
if (m_config.magic_number != CONFIG_MAGIC_NUMBER_VALUE)
m_config.magic_number = CONFIG_MAGIC_NUMBER_VALUE;
/* 3. 기존 레코드 찾기 */
rc = fds_record_find(CONFIG_FILE, CONFIG_REC_KEY, &desc, &tok);
if (rc == NRF_SUCCESS) {
/* 4a. 기존 레코드 업데이트 (비동기) */
fds_flag_write = true;
rc = fds_record_update(&desc, &m_dummy_record);
/* Flash 공간 부족 시 GC 후 재시도 */
if (rc == FDS_ERR_NO_SPACE_IN_FLASH) {
fds_gc();
fds_record_update(&desc, &m_dummy_record);
}
} else {
/* 4b. 새 레코드 쓰기 (비동기) */
fds_flag_write = true;
rc = fds_record_write(&desc, &m_dummy_record);
}
}
```
### 비동기 처리 주의사항
`fds_record_update()` / `fds_record_write()`는 **비동기**로 동작한다.
호출 즉시 리턴하고, 실제 Flash 쓰기는 SoftDevice 이벤트 루프에서 수행된다.
```
config_save() 호출
├─ fds_flag_write = true
├─ fds_record_update() → 즉시 리턴 (NRF_SUCCESS)
│ ... BLE 이벤트 루프에서 실제 Flash 쓰기 수행 ...
└─ fds_evt_handler(FDS_EVT_UPDATE) 콜백
└─ fds_flag_write = false
```
**BLE 콜백 컨텍스트에서 config_save()를 호출할 때**:
`fds_flag_write``false`가 될 때까지 대기하면 **교착 상태**가 발생한다.
`config_save()`는 fire-and-forget 패턴으로 설계되어 있다.
### fds_evt_handler (이벤트 핸들러)
```c
static void fds_evt_handler(fds_evt_t const *p_evt)
{
fds_last_evt = p_evt->id;
switch (p_evt->id)
{
case FDS_EVT_INIT:
m_fds_initialized = true;
break;
case FDS_EVT_WRITE:
fds_flag_write = false;
break;
case FDS_EVT_UPDATE:
fds_flag_write = false;
/* 플래그에 따라 시스템 전원 제어 */
if (go_device_power_off) device_power_off();
if (go_sleep_mode_enter) sleep_mode_enter();
if (go_NVIC_SystemReset) NVIC_SystemReset();
break;
case FDS_EVT_DEL_RECORD:
case FDS_EVT_DEL_FILE:
case FDS_EVT_GC:
break;
}
}
```
**FDS_EVT_UPDATE에서 시스템 제어**: Flash 쓰기 완료 후 전원 OFF / 슬립 / 리셋을 수행한다.
이를 통해 설정 저장이 확실히 완료된 후에만 시스템 상태가 변경된다.
---
## 5. dr_mem 추상화 계층
### 구조
`dr_memRead()` / `dr_memWrite()`는 이름(key) 기반 메모리 접근 API이다.
내부적으로 룩업 테이블을 사용하여 FDS 또는 W25Q32 백엔드로 자동 라우팅한다.
```c
/* 사용 예시 */
dr_memWrite("serial_no", "2025VIVA0001", 12); /* → FDS에 저장 */
dr_memRead("serial_no", buf, 12); /* → FDS에서 읽기 */
dr_memWrite("agc_gain", gains, 96); /* → W25Q32에 저장 */
```
### 룩업 테이블 (dr_mem.c)
| Key | Backend | Offset | Size | 기본값 |
|-----|---------|--------|------|--------|
| `hw_no` | FDS | offsetof(hw_no) = 4 | 12B | "123456789012" |
| `serial_no` | FDS | offsetof(serial_no) = 16 | 12B | "2025AAAAT001" |
| `passkey` | FDS | offsetof(static_passkey) = 28 | 6B | "123456" |
| `bond_delete` | FDS | offsetof(bond_data_delete) = 34 | 1B | 1 |
| `reset_status` | FDS | offsetof(reset_status) = 35 | 1B | 99 |
| `pd_adc_cnt` | FDS | offsetof(pd_adc_cnt) = 36 | 1B | 8 |
| `pd_delay` | FDS | offsetof(pd_delay_us) = 37 | 2B | 8000 |
| `agc_gain` | W25Q32 | 0x000000 | 96B | 2048 x 48 |
| `life_cycle` | W25Q32 | 0x000060 | 4B | 0 |
### FDS 백엔드 동작
**쓰기 (dr_memWrite → fds_backend_write)**:
```
1. 룩업 테이블에서 entry 찾기
2. m_config의 해당 필드에 memcpy
3. config_save() 호출 (비동기 FDS 기록)
```
**읽기 (dr_memRead → fds_backend_read)**:
```
1. 룩업 테이블에서 entry 찾기
2. m_config의 해당 필드에서 memcpy (RAM 읽기, Flash 접근 없음)
```
**중요**: `dr_memRead()`는 항상 RAM(`m_config`)에서 읽는다.
부팅 시 `config_load()`가 Flash → RAM 복사를 한 번 수행하므로,
이후 `dr_memRead()`는 Flash에 직접 접근하지 않는다.
---
## 6. BLE 명령어 인터페이스
### 읽기 명령어 (mr* → rr*)
| 명령어 | Key | 응답 태그 | 응답 형식 | BLE 패킷 크기 |
|--------|-----|----------|-----------|--------------|
| `mrh?` | hw_no | `rrh:` | ASCII 12B | 4+12 = 16B |
| `mrs?` | serial_no | `rrs:` | ASCII 12B | 4+12 = 16B |
| `mrp?` | passkey | `rrp:` | Byte 6B | 4+6 = 10B |
| `mrb?` | bond_delete | `rrb:` | uint16 | 4+2 = 6B |
| `mrr?` | reset_status | `rrr:` | uint16 | 4+2 = 6B |
| `mrc?` | pd_adc_cnt | `rrc:` | uint16 | 4+2 = 6B |
| `mrd?` | pd_delay | `rrd:` | uint16 | 4+2 = 6B |
| `mrg?` | agc_gain | `rrg:` | uint16 x 48 | 4+96 = 100B |
| `mrl?` | life_cycle | `rrl:` | uint16 x 2 (hi, lo) | 4+4 = 8B |
### 쓰기 명령어 (mw* → rw*)
| 명령어 | Key | 입력 형식 | 응답 태그 | 응답 의미 |
|--------|-----|----------|----------|----------|
| `mwh?DATA` | hw_no | ASCII 12B | `rwh:` | 0=성공, 1=실패 |
| `mws?DATA` | serial_no | ASCII 12B | `rws:` | 0=성공, 1=실패 |
| `mwp?DATA` | passkey | ASCII 6B | `rwp:` | 0=성공, 1=실패 |
| `mwb?VAL` | bond_delete | uint16 | `rwb:` | 0=성공, 1=실패 |
| `mwr?VAL` | reset_status | uint16 | `rwr:` | 0=성공, 1=실패 |
| `mwc?VAL` | pd_adc_cnt | uint16 | `rwc:` | 0=성공, 1=실패 |
| `mwd?VAL` | pd_delay | uint16 | `rwd:` | 0=성공, 1=실패 |
| `mwg?V1,V2,...,V48` | agc_gain | uint16 x 48 | `rwg:` | 0=성공, 1=실패 |
| `mwl?HI,LO` | life_cycle | uint16 x 2 | `rwl:` | 0=성공, 1=실패 |
### 명령어 처리 흐름 예시
```
mws?2025VIVA0001 (BLE로 수신)
├─ Cmd_mws() 핸들러 호출
│ ├─ cmd_get_ascii(cmd, 0, serial, 12) → "2025VIVA0001"
│ ├─ dr_memWrite("serial_no", serial, 12)
│ │ ├─ find_entry("serial_no") → FDS backend, offset=16, size=12
│ │ ├─ memcpy(m_config + 16, "2025VIVA0001", 12)
│ │ └─ config_save() → FDS에 비동기 기록
│ └─ dr_ble_return_1("rws:", 0) → BLE로 성공 응답
└─ BLE 응답: "rws:" + 0x0000
```
```
mrs? (BLE로 수신)
├─ Cmd_mrs() 핸들러 호출
│ ├─ dr_memRead("serial_no", buf, 12)
│ │ ├─ find_entry("serial_no") → FDS backend, offset=16, size=12
│ │ └─ memcpy(buf, m_config + 16, 12) → "2025VIVA0001"
│ ├─ ascii_format_data(ble_bin_buffer, "rrs:", buf, 12)
│ └─ binary_tx_handler(ble_bin_buffer, 8)
└─ BLE 응답: "rrs:" + "2025VIVA0001"
```
---
## 7. 런타임 전역변수 연동
### 부팅 시 데이터 흐름
```
Flash (FDS Record)
├─ config_load()
│ └─ m_config (RAM 구조체) ← FDS에서 읽은 원본
├─ load_device_configuration()
│ ├─ dr_memRead("serial_no") → SERIAL_NO[] ← BLE 디바이스 이름
│ ├─ dr_memRead("passkey") → m_static_passkey[] ← BLE 패스키
│ ├─ dr_memRead("pd_delay") → m_pd_delay_us ← 측정 파라미터
│ ├─ dr_memRead("pd_adc_cnt")→ m_pd_adc_cnt ← 측정 파라미터
│ └─ ... (유효성 검증 + 기본값 폴백)
└─ gap_params_init()
└─ sd_ble_gap_device_name_set(SERIAL_NO) ← BLE 광고 이름
```
### 전역변수 목록
| 변수 | 타입 | 원본 (m_config) | 설명 |
|------|------|----------------|------|
| `SERIAL_NO[16]` | char[] | serial_no | BLE 디바이스 이름 |
| `m_static_passkey[7]` | char[] | static_passkey | BLE 페어링 패스키 |
| `m_pd_adc_cnt` | uint8_t | pd_adc_cnt | ADC 샘플링 횟수 |
| `m_pd_delay_us` | uint16_t | pd_delay_us | PD 지연 시간 |
| `bond_data_delete` | bool | bond_data_delete | 본딩 삭제 플래그 |
| `m_life_cycle` | uint32_t | (W25Q32) | 사용 횟수 |
| `led_pd_dac_v[48]` | uint16_t[] | (W25Q32) | AGC 게인 값 |
### 쓰기 시 RAM 동기화
`mw*` 명령으로 값을 변경할 때, 일부 명령어는 전역변수도 즉시 업데이트한다:
```c
/* mwc? - pd_adc_cnt 쓰기 */
dr_memWrite("pd_adc_cnt", &u8val, 1); // m_config + FDS 저장
m_pd_adc_cnt = u8val; // 전역변수 즉시 반영
/* mwd? - pd_delay 쓰기 */
dr_memWrite("pd_delay", &val, 2); // m_config + FDS 저장
m_pd_delay_us = val; // 전역변수 즉시 반영
```
**주의**: BLE 디바이스 이름(`SERIAL_NO`)은 부팅 시 한 번만 설정된다.
`mws?`로 시리얼 번호를 변경해도 재부팅 전까지 BLE 광고 이름은 바뀌지 않는다.
---
## 8. sdk_config.h 설정
### FDS 관련 설정
```c
/* FDS 활성화 */
#define FDS_ENABLED 1
/* 가상 페이지 수 (각 페이지 = FDS_VIRTUAL_PAGE_SIZE * 4 bytes) */
#define FDS_VIRTUAL_PAGES 50
/* 가상 페이지 크기 (words) - 기본 1024 words = 4KB */
#define FDS_VIRTUAL_PAGE_SIZE 2048
/* CRC 검사 (현재 비활성화) */
#define FDS_CRC_CHECK_ON_READ 0
#define FDS_CRC_CHECK_ON_WRITE 0
/* 내부 Flash 저장소 활성화 */
#define NRF_FSTORAGE_ENABLED 1
/* SOC 이벤트 핸들러 (FDS 이벤트 전달에 필수) */
#define NRF_SDH_SOC_ENABLED 1
```
### 저장 용량 계산
```
총 FDS 영역 = 50 pages × 2048 words × 4 bytes = 400KB
레코드 크기 = (39 + 3) / 4 = 10 words = 40 bytes
오버헤드 = 레코드 헤더 3 words = 12 bytes
실제 1 레코드 = 52 bytes (데이터 40B + 헤더 12B)
```
현재 프로젝트는 **1개의 레코드**만 사용하므로 용량은 충분하다.
50 페이지 설정은 Wear Leveling 수명을 극대화하기 위한 것이다.
---
## 9. 에러 코드
| 이름 | 값 | 의미 | 대응 |
|------|-----|------|------|
| `NRF_SUCCESS` | 0 | 성공 | - |
| `FDS_ERR_NOT_FOUND` | 34313 (0x8609) | 레코드 없음 | 새 레코드 쓰기 |
| `FDS_ERR_NO_SPACE_IN_FLASH` | 34050 (0x8502) | Flash 공간 부족 | `fds_gc()` 후 재시도 |
| `FDS_ERR_RECORD_TOO_LARGE` | 34051 (0x8503) | 레코드 크기 초과 | 구조체 크기 줄이기 |
| `FDS_ERR_NOT_INITIALIZED` | 34052 (0x8504) | FDS 미초기화 | 부팅 순서 확인 |
| `FDS_ERR_CRC_CHECK_FAILED` | - | CRC 불일치 | 레코드 삭제 + 재생성 |
---
## 10. 트러블슈팅
### 문제 1: 부팅 시 저장된 값이 기본값으로 초기화됨
**증상**: BLE 명령으로 값을 저장한 후 재부팅하면 기본값으로 돌아감.
BLE 읽기 명령(mr*)으로 읽으면 저장된 값이 정상 반환됨.
**원인**: `config_load()`에서 `fds_record_find()`가 첫 호출 시 실패 (rc=34313).
FDS가 완전히 초기화되기 전에 호출되어 "레코드 없음"으로 판단,
기존 데이터를 기본값으로 덮어씀.
**해결**: 재시도 로직 추가 (최대 10회, 100ms 간격).
```c
if (rc != NRF_SUCCESS && cfg_retry < 10) {
cfg_retry++;
nrf_delay_ms(100);
goto cfg_load_start;
}
```
### 문제 2: config_save() 호출 후 값이 저장되지 않음
**증상**: `dr_memWrite()` 호출 후 `config_save()` 리턴했지만 Flash에 기록 안 됨.
**원인**: `fds_flag_write`가 이미 `true`인 상태에서 `config_save()` 호출.
이전 FDS 작업이 완료되지 않아 스킵됨.
**해결**: RTT 로그에서 `[CFG_SAVE] busy, skipped` 메시지 확인.
연속 쓰기 시 적절한 간격을 두거나, 이전 쓰기 완료 후 재시도.
### 문제 3: wait_for_fds_ready()에서 타임아웃
**증상**: `[FDS] TIMEOUT!` 메시지 출력.
**원인**: `ble_stack_init()` 전에 `fs_storage_init()` 호출.
SoftDevice가 비활성 상태이면 FDS 이벤트가 발생하지 않음.
**해결**: main.c에서 초기화 순서 확인:
```
ble_stack_init(); // 반드시 먼저!
fs_storage_init(); // 그 다음
config_load();
```
### 문제 4: Flash 공간 부족 (FDS_ERR_NO_SPACE_IN_FLASH)
**증상**: `fds_record_update()` 실패, rc=34050.
**원인**: FDS는 업데이트 시 새 위치에 기록하고 기존을 무효화(invalidate).
GC(Garbage Collection) 없이 반복 업데이트하면 유효 공간 소진.
**해결**: `fds_gc()` 호출 후 재시도 (config_save에 이미 구현됨).
```c
if (rc == FDS_ERR_NO_SPACE_IN_FLASH) {
fds_gc();
rc = fds_record_update(&desc, &m_dummy_record);
}
```
### 문제 5: printf와 DBG_PRINTF 혼동
**증상**: RTT에서 FDS 관련 로그가 보이지 않음.
**원인**: `printf()`는 UART로 출력, `DBG_PRINTF()`는 RTT로 출력.
config_load() 내부에서 printf를 사용하면 RTT에서 볼 수 없음.
**해결**: FDS 관련 함수에서는 `DBG_PRINTF()` 사용.
---
## 11. 관련 파일
| 파일 | 역할 |
|------|------|
| [fstorage.c](../fstorage.c) | FDS 초기화, config_load(), config_save() |
| [fstorage.h](../fstorage.h) | config_data_t 구조체 정의 |
| [storage/dr_mem.c](../storage/dr_mem.c) | dr_memRead/Write 구현, 룩업 테이블 |
| [storage/dr_mem.h](../storage/dr_mem.h) | dr_mem API 선언 |
| [config/device_config.c](../config/device_config.c) | 부팅 시 설정 로드 + 유효성 검증 |
| [cmd/cmd.c](../cmd/cmd.c) | BLE 명령어 핸들러 (mr*/mw*) |
| [main.c](../main.c) | 초기화 순서 |
| [pca10056/s140/config/sdk_config.h](../pca10056/s140/config/sdk_config.h) | FDS SDK 설정 |
---
*문서 작성일: 2026-03-05*
*마지막 업데이트: FDS 재시도 로직, dr_mem 추상화 계층, BLE 명령어 인터페이스 추가*

View File

@@ -0,0 +1,939 @@
# FDS 내장 메모리 모듈 - 이식(Porting) 매뉴얼
> **대상**: Claude AI 또는 개발자가 이 문서를 읽고 새 프로젝트에 FDS 메모리 모듈을 이식할 수 있도록 작성됨.
> 모든 단계를 순서대로 실행하면 빌드 가능한 상태까지 도달한다.
---
## 목차
1. [모듈 개요](#1-모듈-개요)
2. [파일 목록 및 의존성](#2-파일-목록-및-의존성)
3. [Step 1: 파일 복사](#step-1-파일-복사)
4. [Step 2: config_data_t 커스터마이징](#step-2-config_data_t-커스터마이징)
5. [Step 3: dr_mem 룩업 테이블 수정](#step-3-dr_mem-룩업-테이블-수정)
6. [Step 4: device_config 전역변수 수정](#step-4-device_config-전역변수-수정)
7. [Step 5: BLE 명령어 핸들러 추가](#step-5-ble-명령어-핸들러-추가)
8. [Step 6: main.c 초기화 순서 통합](#step-6-mainc-초기화-순서-통합)
9. [Step 7: sdk_config.h 설정](#step-7-sdk_configh-설정)
10. [Step 8: Keil 프로젝트에 파일 추가](#step-8-keil-프로젝트에-파일-추가)
11. [Step 9: 빌드 및 검증](#step-9-빌드-및-검증)
12. [핵심 주의사항](#핵심-주의사항)
13. [체크리스트](#체크리스트)
---
## 1. 모듈 개요
### 이 모듈이 하는 일
nRF52840 내장 Flash에 디바이스 설정을 저장하고, BLE 명령어로 읽기/쓰기하는 완전한 시스템.
```
┌───────────────────────────────────────────────────────┐
│ BLE 명령어 │
│ mrh? mrs? mwh? mws? ... │
└───────────┬───────────────────────────┬───────────────┘
│ │
▼ ▼
┌───────────────────────┐ ┌───────────────────────────┐
│ cmd/cmd.c │ │ cmd/cmd.c │
│ Cmd_mrh(), Cmd_mrs() │ │ Cmd_mwh(), Cmd_mws() │
│ (읽기 핸들러) │ │ (쓰기 핸들러) │
└───────────┬───────────┘ └──────────┬────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────┐
│ storage/dr_mem.c │
│ dr_memRead() / dr_memWrite() │
│ (이름 기반 메모리 접근 - 룩업 테이블 라우팅) │
└──────────┬──────────────────────────┬────────────────┘
│ │
▼ ▼
┌────────────────────┐ ┌───────────────────────┐
│ FDS (내장 Flash) │ │ W25Q32 (외장 Flash) │
│ config_data_t │ │ (선택사항) │
│ fstorage.c │ │ w25q32 드라이버 │
└────────────────────┘ └───────────────────────┘
```
### 모듈 구성 요소
| 계층 | 역할 | 수정 필요? |
|------|------|-----------|
| **fstorage.c/h** | FDS 초기화, 레코드 읽기/쓰기 | 구조체만 수정 |
| **storage/dr_mem.c/h** | 이름 기반 추상화 API | 룩업 테이블 수정 |
| **config/device_config.c/h** | 부팅 시 전역변수 로드 | 전역변수 수정 |
| **cmd/cmd.c** (mr*/mw* 부분) | BLE 명령어 핸들러 | 필드에 맞게 수정 |
| **mt_parser** (외부 라이브러리) | 명령어 파싱/디스패치 | 수정 불필요 |
| **ble/ble_data_tx.c/h** | BLE 데이터 전송 | 수정 불필요 |
---
## 2. 파일 목록 및 의존성
### 복사해야 할 파일 (프로젝트 내부)
```
프로젝트 루트/
├── fstorage.c ← FDS 코어 (config_load, config_save)
├── fstorage.h ← config_data_t 정의
├── storage/
│ ├── dr_mem.c ← 이름 기반 메모리 API
│ └── dr_mem.h ← dr_memRead/Write 선언
├── config/
│ ├── device_config.c ← 부팅 시 설정 로드
│ └── device_config.h ← 전역변수 선언
└── cmd/
├── cmd.c ← BLE 명령어 핸들러 (mr*/mw* 부분 추가)
└── cmd.h ← cmd_get_u16, cmd_get_ascii 래퍼
```
### 외부 의존 라이브러리 (프로젝트 외부 - 이미 존재해야 함)
```
mt_parser/ ← 명령어 파싱 라이브러리 (공용)
├── parser.c ParsedCmd, CmdEntry, dr_cmd_parser()
├── parser.h 타입 정의
└── dr_util/
├── dr_util.c dr_ble_return_1(), dr_ble_return_2()
└── dr_util.h 함수 선언
```
### Nordic SDK 의존성 (sdk_config.h에서 활성화)
| 모듈 | 용도 | 필수? |
|------|------|------|
| `FDS` | Flash Data Storage | 필수 |
| `NRF_FSTORAGE` | Flash Storage 하위 계층 | 필수 |
| `NRF_SDH_SOC` | SoftDevice SOC 이벤트 | 필수 |
| `CRC16` | 패킷 CRC 검증 | 필수 |
| `NRF_PWR_MGMT` | 대기 시 전력 관리 | 필수 |
### 펌웨어 내부 의존 (이미 존재해야 함)
| 파일 | 제공하는 것 | 용도 |
|------|-----------|------|
| `main.h` | `which_cmd_t`, `BLE_CONNECTED_ST`, `binary_tx_handler()` 선언 | BLE 전송 |
| `ble/ble_data_tx.c/h` | `format_data()`, `ascii_format_data()`, `ble_bin_buffer[]` | 데이터 포맷팅 |
| `debug_print.h` | `DBG_PRINTF` 매크로 | 디버그 출력 |
| `power/power_ctrl.c` | `device_power_off()`, `sleep_mode_enter()` | 전원 제어 |
| `drivers/w25q32/w25q32.h` | `w25q32_read()`, `w25q32_write()` | 외장 Flash (선택) |
---
## Step 1: 파일 복사
### 1.1 디렉토리 생성
```bash
# 새 프로젝트 루트에서
mkdir -p storage
mkdir -p config
mkdir -p cmd # 이미 있으면 스킵
```
### 1.2 파일 복사
**VivaMyo 프로젝트에서 복사할 파일:**
```bash
# FDS 코어
cp <VivaMayo>/fstorage.c <새 프로젝트>/fstorage.c
cp <VivaMayo>/fstorage.h <새 프로젝트>/fstorage.h
# dr_mem 추상화
cp <VivaMayo>/storage/dr_mem.c <새 프로젝트>/storage/dr_mem.c
cp <VivaMayo>/storage/dr_mem.h <새 프로젝트>/storage/dr_mem.h
# device_config
cp <VivaMayo>/config/device_config.c <새 프로젝트>/config/device_config.c
cp <VivaMayo>/config/device_config.h <새 프로젝트>/config/device_config.h
```
**cmd/cmd.c는 복사하지 않음** - 기존 cmd.c에 핸들러 코드를 추가한다 (Step 5 참조).
### 1.3 debug_print.h 확인
새 프로젝트에 `debug_print.h`가 없으면 복사:
```bash
cp <VivaMayo>/debug_print.h <새 프로젝트>/debug_print.h
```
내용 (RTT/UART 선택 가능):
```c
#ifndef DEBUG_PRINT_H
#define DEBUG_PRINT_H
#include <stdbool.h>
#include <stdio.h>
#include "nrf_delay.h"
#include "SEGGER_RTT.h"
#define USE_RTT_OUTPUT 1 // 0=UART(printf), 1=RTT
#define ENABLE_PRINTF 1 // 0=DBG_PRINTF 비활성화
#if USE_RTT_OUTPUT
#if ENABLE_PRINTF
#define DBG_PRINTF(...) SEGGER_RTT_printf(0, __VA_ARGS__)
#else
#define DBG_PRINTF(...)
#endif
#define LOG_PRINTF(...) do { if (g_log_enable) SEGGER_RTT_printf(0, __VA_ARGS__); } while(0)
#else
#if ENABLE_PRINTF
#define DBG_PRINTF(...) printf(__VA_ARGS__)
#else
#define DBG_PRINTF(...)
#endif
#define LOG_PRINTF(...) do { if (g_log_enable) printf(__VA_ARGS__); } while(0)
#endif
extern bool g_log_enable;
#endif
```
---
## Step 2: config_data_t 커스터마이징
### 2.1 fstorage.h 수정
새 프로젝트에 맞게 `config_data_t` 구조체를 수정한다.
**규칙:**
- 반드시 `#pragma pack(1)` 사용 (패딩 없이 연속 저장)
- 첫 번째 필드는 반드시 `uint32_t magic_number` (데이터 무결성 검증용)
- 나머지 필드는 프로젝트 요구사항에 맞게 정의
**VivaMayo 원본:**
```c
#pragma pack(1)
typedef struct
{
uint32_t magic_number; /* 4B - 고정 (수정 금지) */
char hw_no[12]; /* 12B - HW 번호 */
char serial_no[12]; /* 12B - 시리얼 번호 */
uint8_t static_passkey[6]; /* 6B - BLE 패스키 */
uint8_t bond_data_delete; /* 1B - 본딩 삭제 플래그 */
int8_t reset_status; /* 1B - 리셋 상태 */
uint8_t pd_adc_cnt; /* 1B - ADC 샘플 수 */
uint16_t pd_delay_us; /* 2B - PD 지연 시간 */
} config_data_t; /* 39 bytes */
```
**새 프로젝트용 예시 (커스터마이징):**
```c
#pragma pack(1)
typedef struct
{
uint32_t magic_number; /* 4B - 고정 (0x새날짜값) */
char hw_no[12]; /* 12B - HW 번호 */
char serial_no[12]; /* 12B - 시리얼 번호 */
uint8_t static_passkey[6]; /* 6B - BLE 패스키 */
uint8_t bond_data_delete; /* 1B - 본딩 삭제 */
int8_t reset_status; /* 1B - 리셋 상태 */
// ---- 여기서부터 프로젝트별 필드 추가/삭제 ----
uint8_t my_custom_field; /* 1B */
uint16_t my_custom_u16; /* 2B */
} config_data_t;
```
### 2.2 magic_number 값 변경
`fstorage.c`에서 magic_number를 새 날짜로 변경:
```c
#define CONFIG_MAGIC_NUMBER_VALUE (0x20260305) /* 새 프로젝트 날짜 */
```
**중요**: magic_number를 변경하면 기존 FDS 레코드는 무효화되고 기본값으로 초기화된다.
이것은 의도된 동작이다 (새 프로젝트이므로).
### 2.3 fds_default_value_set() 수정
`fstorage.c``fds_default_value_set()` 함수를 config_data_t 필드에 맞게 수정:
```c
void fds_default_value_set(void)
{
memset(m_config.hw_no, 0, 12);
memcpy(m_config.serial_no, "2025AAAAT001", 12); /* 프로젝트 기본 시리얼 */
memcpy(m_config.static_passkey, "123456", 6); /* 기본 패스키 */
m_config.bond_data_delete = 1;
m_config.reset_status = 99;
// ---- 프로젝트별 필드 기본값 ----
m_config.my_custom_field = 0;
m_config.my_custom_u16 = 1000;
}
```
---
## Step 3: dr_mem 룩업 테이블 수정
### 3.1 storage/dr_mem.c 룩업 테이블 수정
`mem_table[]`을 config_data_t 필드에 맞게 수정한다.
**각 엔트리 형식:**
```c
{ "키이름", , offsetof(config_data_t, ), (bytes), false, & },
```
**VivaMayo 원본:**
```c
static const dr_mem_entry_t mem_table[] = {
/* FDS entries */
{ "hw_no", MEM_BACKEND_FDS, offsetof(config_data_t, hw_no), 12, false, dflt_hw_no },
{ "serial_no", MEM_BACKEND_FDS, offsetof(config_data_t, serial_no), 12, false, dflt_serial },
{ "passkey", MEM_BACKEND_FDS, offsetof(config_data_t, static_passkey), 6, false, dflt_passkey },
{ "bond_delete", MEM_BACKEND_FDS, offsetof(config_data_t, bond_data_delete), 1, false, &dflt_bond_del },
{ "reset_status", MEM_BACKEND_FDS, offsetof(config_data_t, reset_status), 1, false, &dflt_reset_st },
{ "pd_adc_cnt", MEM_BACKEND_FDS, offsetof(config_data_t, pd_adc_cnt), 1, false, &dflt_adc_cnt },
{ "pd_delay", MEM_BACKEND_FDS, offsetof(config_data_t, pd_delay_us), 2, false, &dflt_pd_delay },
/* W25Q32 entries (외장 Flash가 없으면 제거) */
{ "agc_gain", MEM_BACKEND_W25Q32, 0x000000, 96, false, dflt_agc },
{ "life_cycle", MEM_BACKEND_W25Q32, 0x000060, 4, false, &dflt_life_cycle },
};
```
**수정 방법:**
1. config_data_t에서 필드를 추가/삭제했으면 테이블도 동일하게 수정
2. `offsetof(config_data_t, 필드명)` 은 반드시 정확해야 함
3. 기본값 `static const` 변수도 파일 상단에 추가
4. **W25Q32가 없으면** W25Q32 엔트리 전부 제거 + `#include "drivers/w25q32/w25q32.h"` 제거
5. **W25Q32가 없으면** `w25q_backend_write()`, `w25q_backend_read()` 함수도 제거
### 3.2 W25Q32 없는 프로젝트 (FDS만 사용)
외장 Flash가 없는 프로젝트의 경우 dr_mem.c를 다음과 같이 단순화:
```c
/* W25Q32 관련 코드 전부 제거 */
// #include "drivers/w25q32/w25q32.h" ← 삭제
// w25q_backend_write() 함수 ← 삭제
// w25q_backend_read() 함수 ← 삭제
// mem_table에서 MEM_BACKEND_W25Q32 엔트리 ← 삭제
// dr_memWrite / dr_memRead의 switch문에서
// case MEM_BACKEND_W25Q32: ← 삭제
```
### 3.3 dr_mem.h는 수정 불필요
`dr_mem.h`는 범용 인터페이스이므로 그대로 사용한다.
---
## Step 4: device_config 전역변수 수정
### 4.1 config/device_config.h 수정
프로젝트에서 런타임에 사용하는 전역변수를 선언한다.
이 변수들은 부팅 시 `m_config` (FDS)에서 복사된다.
```c
#ifndef DEVICE_CONFIG_H
#define DEVICE_CONFIG_H
#include <stdint.h>
#include <stdbool.h>
/* ---- 프로젝트별 전역변수 extern 선언 ---- */
extern char m_static_passkey[7]; /* BLE 패스키 (6자리 + null) */
extern char SERIAL_NO[16]; /* 시리얼 번호 (BLE 디바이스 이름으로 사용) */
extern bool bond_data_delete;
// extern uint16_t my_custom_var; /* 추가 변수 */
void load_device_configuration(void);
void load_default_values(void);
bool is_valid_serial_no(const char *serial);
bool is_valid_passkey(const char *passkey);
#endif
```
### 4.2 config/device_config.c 수정
**전역변수 정의부:**
```c
char m_static_passkey[7] = "123456";
char SERIAL_NO[16] = "2025XXXX00001";
bool bond_data_delete = true;
// uint16_t my_custom_var = 0;
```
**load_default_values() 함수** - 각 전역변수의 기본값 설정:
```c
void load_default_values(void)
{
memset(SERIAL_NO, 0, sizeof(SERIAL_NO));
memcpy(SERIAL_NO, "2025AAAAT001", 12);
memset(m_static_passkey, 0, sizeof(m_static_passkey));
memcpy(m_static_passkey, "123456", 6);
bond_data_delete = 1;
// my_custom_var = 0;
}
```
**load_device_configuration() 함수** - FDS(m_config)에서 전역변수로 복사:
```c
void load_device_configuration(void)
{
ret_code_t err_code;
load_default_values();
/* FDS → 전역변수 복사 */
err_code = dr_memRead("serial_no", (uint8_t *)SERIAL_NO, 12);
if (err_code != NRF_SUCCESS || !is_valid_serial_no(SERIAL_NO)) {
memcpy(SERIAL_NO, "2025AAAAT001", 12); /* 폴백 */
}
err_code = dr_memRead("passkey", (uint8_t *)m_static_passkey, 6);
if (err_code != NRF_SUCCESS || !is_valid_passkey(m_static_passkey)) {
memcpy(m_static_passkey, "123456", 6);
}
{
uint8_t raw = 0;
dr_memRead("bond_delete", &raw, 1);
bond_data_delete = (raw != 0);
}
// dr_memRead("my_custom_key", &my_custom_var, 2);
}
```
---
## Step 5: BLE 명령어 핸들러 추가
### 5.1 cmd/cmd.c에 include 추가
```c
#include "../storage/dr_mem.h" /* dr_memRead, dr_memWrite */
#include "../fstorage.h" /* m_config */
#include "../debug_print.h"
```
### 5.2 읽기 핸들러 패턴 (mr* → rr*)
config_data_t의 각 필드 타입에 따라 4가지 패턴을 사용한다.
**패턴 A: ASCII 문자열 (char[]) 필드**
```c
static int Cmd_mrh(const ParsedCmd *cmd)
{
(void)cmd;
char buf[12];
dr_memRead("hw_no", buf, 12);
ascii_format_data(ble_bin_buffer, "rrh:", buf, 12);
binary_tx_handler(ble_bin_buffer, 8); /* (4+12)/2 = 8 words */
return 1;
}
```
**패턴 B: uint8_t 바이트 배열 필드**
```c
static int Cmd_mrp(const ParsedCmd *cmd)
{
(void)cmd;
uint8_t buf[6];
dr_memRead("passkey", buf, 6);
format_data_byte(ble_bin_buffer, "rrp:", buf, 6);
binary_tx_handler(ble_bin_buffer, 5); /* (4+6)/2 = 5 words */
return 1;
}
```
**패턴 C: 단일 숫자 값 (1~2 byte)**
```c
static int Cmd_mrb(const ParsedCmd *cmd)
{
(void)cmd;
uint8_t val = 0;
dr_memRead("bond_delete", &val, 1);
dr_ble_return_1("rrb:", (uint16_t)val); /* tag + uint16 응답 */
return 1;
}
static int Cmd_mrd(const ParsedCmd *cmd)
{
(void)cmd;
uint16_t val = 0;
dr_memRead("pd_delay", &val, 2);
dr_ble_return_1("rrd:", val);
return 1;
}
```
**패턴 D: uint32_t 값 (hi16 + lo16로 분할)**
```c
static int Cmd_mrl(const ParsedCmd *cmd)
{
(void)cmd;
uint32_t val = 0;
dr_memRead("life_cycle", &val, 4);
uint16_t hi = (uint16_t)(val >> 16);
uint16_t lo = (uint16_t)(val & 0xFFFF);
dr_ble_return_2("rrl:", hi, lo); /* tag + uint16 x 2 응답 */
return 1;
}
```
### 5.3 쓰기 핸들러 패턴 (mw* → rw*)
**패턴 A: ASCII 문자열 쓰기**
```c
static int Cmd_mwh(const ParsedCmd *cmd)
{
char hw[13];
memset(hw, 0, sizeof(hw));
cmd_get_ascii(cmd, 0, hw, 12);
ret_code_t err = dr_memWrite("hw_no", hw, 12);
dr_ble_return_1("rwh:", err == NRF_SUCCESS ? 0 : 1);
return 1;
}
```
**패턴 B: uint8/uint16 값 쓰기**
```c
static int Cmd_mwb(const ParsedCmd *cmd)
{
uint16_t val = 0;
(void)cmd_get_u16(cmd, 0, &val);
uint8_t u8val = (uint8_t)val;
ret_code_t err = dr_memWrite("bond_delete", &u8val, 1);
dr_ble_return_1("rwb:", err == NRF_SUCCESS ? 0 : 1);
return 1;
}
```
**패턴 C: uint16 배열 쓰기 + RAM 동기화**
```c
static int Cmd_mwg(const ParsedCmd *cmd)
{
uint16_t gains[48];
uint8_t i;
for (i = 0; i < 48; i++) {
if (!cmd_get_u16(cmd, i, &gains[i])) gains[i] = 0;
}
ret_code_t err = dr_memWrite("agc_gain", gains, sizeof(gains));
/* RAM 전역변수도 즉시 업데이트 */
for (i = 0; i < 48; i++) led_pd_dac_v[i] = gains[i];
dr_ble_return_1("rwg:", err == NRF_SUCCESS ? 0 : 1);
return 1;
}
```
**패턴 D: uint32 쓰기 (hi16 + lo16 결합)**
```c
static int Cmd_mwl(const ParsedCmd *cmd)
{
uint16_t hi = 0, lo = 0;
(void)cmd_get_u16(cmd, 0, &hi);
(void)cmd_get_u16(cmd, 1, &lo);
uint32_t life = ((uint32_t)hi << 16) | (uint32_t)lo;
ret_code_t err = dr_memWrite("life_cycle", &life, 4);
m_life_cycle = life; /* RAM 전역변수도 즉시 업데이트 */
dr_ble_return_1("rwl:", err == NRF_SUCCESS ? 0 : 1);
return 1;
}
```
### 5.4 binary_tx_handler 호출 시 word 수 계산
```
words = (TAG 4바이트 + DATA 바이트 수) / 2
예시:
ASCII 12B 응답: (4 + 12) / 2 = 8 words
Byte 6B 응답: (4 + 6) / 2 = 5 words
uint16 1개 응답: dr_ble_return_1 사용 (내부에서 3 words)
uint16 2개 응답: dr_ble_return_2 사용 (내부에서 4 words)
```
### 5.5 명령어 테이블에 등록
`g_cmd_table[]`에 핸들러 엔트리 추가:
```c
CmdEntry g_cmd_table[] = {
/* ... 기존 명령어들 ... */
/* N. Memory Read (dr_mem) */
{ "mrh?", true, Cmd_mrh }, /* hw_no → rrh: + 12B ASCII */
{ "mrs?", true, Cmd_mrs }, /* serial_no → rrs: + 12B ASCII */
{ "mrp?", true, Cmd_mrp }, /* passkey → rrp: + 6B */
{ "mrb?", true, Cmd_mrb }, /* bond_delete → rrb: + uint16 */
{ "mrr?", true, Cmd_mrr }, /* reset_status → rrr: + uint16 */
/* ... 프로젝트별 추가 mr* 명령어 ... */
/* O. Memory Write (dr_mem) */
{ "mwh?", true, Cmd_mwh }, /* hw_no ← 12B ASCII → rwh: */
{ "mws?", true, Cmd_mws }, /* serial_no ← 12B ASCII → rws: */
{ "mwp?", true, Cmd_mwp }, /* passkey ← 6B → rwp: */
{ "mwb?", true, Cmd_mwb }, /* bond_delete ← uint16 → rwb: */
{ "mwr?", true, Cmd_mwr }, /* reset_status ← uint16 → rwr: */
/* ... 프로젝트별 추가 mw* 명령어 ... */
};
```
### 5.6 forward declaration 추가
`g_cmd_table[]` 앞에 핸들러 함수 선언:
```c
/* Memory Read handlers */
static int Cmd_mrh(const ParsedCmd *cmd);
static int Cmd_mrs(const ParsedCmd *cmd);
static int Cmd_mrp(const ParsedCmd *cmd);
/* ... */
/* Memory Write handlers */
static int Cmd_mwh(const ParsedCmd *cmd);
static int Cmd_mws(const ParsedCmd *cmd);
static int Cmd_mwp(const ParsedCmd *cmd);
/* ... */
```
### 5.7 명명 규칙
```
읽기: mr + 필드 약자 + ? → 응답: rr + 필드 약자 + :
쓰기: mw + 필드 약자 + ? → 응답: rw + 필드 약자 + :
약자 예시:
h = hw_no
s = serial_no
p = passkey
b = bond_delete
r = reset_status
c = pd_adc_cnt (count)
d = pd_delay
g = agc_gain
l = life_cycle
```
새 필드를 추가할 때는 기존 약자와 겹치지 않는 알파벳 사용.
---
## Step 6: main.c 초기화 순서 통합
### 6.1 필수 초기화 순서
```c
int main(void)
{
/* ... GPIO, Timer 등 초기화 ... */
/* ① BLE 스택 초기화 (SoftDevice 활성화) - 반드시 FDS보다 먼저! */
ble_stack_init();
/* ② FDS 초기화 */
fs_storage_init(); /* fds_register + fds_init + wait_for_fds_ready */
/* ③ FDS에서 config_data_t 로드 (→ m_config 전역변수) */
config_load(); /* = fs_set_value() 호출해도 동일 */
/* ④ m_config → 런타임 전역변수 복사 */
load_device_configuration();
/* ⑤ GAP 파라미터 설정 (SERIAL_NO로 디바이스 이름 설정) */
gap_params_init();
gatt_init();
services_init();
advertising_init();
/* ... 나머지 초기화 ... */
for (;;) {
idle_state_handle();
}
}
```
### 6.2 순서를 반드시 지켜야 하는 이유
| 순서 | 이유 |
|------|------|
| ① → ② | SoftDevice가 FDS 이벤트를 전달해야 하므로 BLE 스택이 먼저 |
| ② → ③ | FDS 초기화 완료 후에야 레코드 검색 가능 |
| ③ → ④ | m_config에 FDS 데이터가 로드된 후에 전역변수로 복사 |
| ④ → ⑤ | SERIAL_NO가 설정된 후에 GAP 이름 설정 |
### 6.3 필요한 extern/include
```c
/* main.c에 추가할 include */
#include "fstorage.h"
#include "config/device_config.h"
/* main.c에 추가할 extern (전원 제어 플래그) */
bool go_device_power_off = false;
bool go_sleep_mode_enter = false;
bool go_NVIC_SystemReset = false;
```
---
## Step 7: sdk_config.h 설정
### 7.1 필수 설정
```c
/* FDS 활성화 */
#define FDS_ENABLED 1
#define FDS_VIRTUAL_PAGES 50 /* 50 pages (Wear Leveling용) */
#define FDS_VIRTUAL_PAGE_SIZE 2048 /* words (= 8KB per page) */
#define FDS_CRC_CHECK_ON_READ 0 /* 0 권장 (성능) */
#define FDS_CRC_CHECK_ON_WRITE 0 /* 0 권장 (성능) */
#define FDS_BACKEND 2 /* 2 = nrf_fstorage_sd (SoftDevice 사용) */
#define FDS_OP_QUEUE_SIZE 4
/* Flash Storage 활성화 */
#define NRF_FSTORAGE_ENABLED 1
/* SOC 이벤트 핸들러 (FDS가 SoftDevice에서 이벤트 받기 위해 필수) */
#define NRF_SDH_SOC_ENABLED 1
#define NRF_SDH_SOC_OBSERVER_PRIO_LEVELS 2
/* 전력 관리 (wait_for_fds_ready에서 사용) */
#define NRF_PWR_MGMT_ENABLED 1
```
### 7.2 선택 설정
```c
/* RTT 디버그 버퍼 (DBG_PRINTF 사용 시) */
#define SEGGER_RTT_CONFIG_BUFFER_SIZE_UP 1024
#define NRF_LOG_BACKEND_RTT_ENABLED 1
```
### 7.3 Flash 메모리 맵 확인
FDS가 사용하는 Flash 영역은 링커 스크립트 또는 `nrf_dfu_types.h`에서 설정.
기본적으로 **Application 영역 끝부분**에 배치된다.
FDS_VIRTUAL_PAGES를 늘리면 Application 코드 공간이 줄어드므로 주의.
---
## Step 8: Keil 프로젝트에 파일 추가
### 8.1 Source 파일 추가
Keil uVision → Project → Manage Project Items에서 다음 파일 추가:
| 그룹 | 파일 | 경로 |
|------|------|------|
| Application | `fstorage.c` | `../../fstorage.c` |
| Application | `device_config.c` | `../../config/device_config.c` |
| Application | `dr_mem.c` | `../../storage/dr_mem.c` |
| mt_parser | `parser.c` | `mt_parser/parser.c` (이미 있으면 스킵) |
| mt_parser | `dr_util.c` | `mt_parser/dr_util/dr_util.c` (이미 있으면 스킵) |
| nRF_Libraries | `fds.c` | SDK `components/libraries/fds/fds.c` |
| nRF_Libraries | `nrf_fstorage.c` | SDK `components/libraries/fstorage/nrf_fstorage.c` |
| nRF_Libraries | `nrf_fstorage_sd.c` | SDK `components/libraries/fstorage/nrf_fstorage_sd.c` |
| nRF_Libraries | `nrf_atflags.c` | SDK `components/libraries/atomic_flags/nrf_atflags.c` |
### 8.2 Include Path 추가
```
Options → C/C++ → Include Paths에 추가:
../../storage
../../config
../../cmd
<mt_parser 경로>
<mt_parser>/dr_util
```
### 8.3 Nordic SDK Include 확인
다음 SDK 경로들이 이미 Include Path에 있는지 확인:
```
.../components/libraries/fds
.../components/libraries/fstorage
.../components/libraries/atomic_flags
.../components/libraries/experimental_section_vars
```
---
## Step 9: 빌드 및 검증
### 9.1 빌드
Keil에서 Build (F7). 예상되는 에러와 해결:
| 에러 | 원인 | 해결 |
|------|------|------|
| `config_data_t` undeclared | fstorage.h include 누락 | `#include "fstorage.h"` 추가 |
| `dr_memRead` undeclared | dr_mem.h include 누락 | `#include "storage/dr_mem.h"` 추가 |
| `w25q32_read` undefined | W25Q32 드라이버 없음 | dr_mem.c에서 W25Q32 코드 제거 |
| `device_power_off` undefined | power_ctrl 미구현 | 빈 함수 작성 또는 FDS_EVT_UPDATE에서 제거 |
| Linker: FDS region overlap | Flash 영역 충돌 | FDS_VIRTUAL_PAGES 줄이기 (예: 10) |
| `go_device_power_off` undeclared | main.c에 정의 안 됨 | main.c에 `bool go_device_power_off = false;` 추가 |
### 9.2 RTT 로그로 검증
부팅 후 RTT 로그에서 다음 확인:
```
[FDS] OK ← FDS 초기화 성공
[FDS] find rc=0 ← 레코드 발견 (두 번째 부팅부터)
[FDS] Loaded OK ← 정상 로드
[CFG] FDS serial_no='2025AAAAT001' ← 시리얼 번호 확인
```
첫 부팅 시에는:
```
[FDS] OK
[FDS] find rc=34313 ← 레코드 없음 (정상)
[FDS] retry 1/10 ← 재시도
[FDS] New - writing defaults ← 기본값으로 새 레코드 생성
[FDS] find rc=0
[FDS] Loaded OK
```
### 9.3 BLE 명령어 검증
1. BLE 연결
2. `mrs?` 전송 → `rrs:2025AAAAT001` 응답 확인
3. `mws?2025TEST0001` 전송 → `rws:0000` (성공) 응답 확인
4. 재부팅
5. `mrs?` 전송 → `rrs:2025TEST0001` 응답 확인 (값 유지됨)
---
## 핵심 주의사항
### 1. config_load()의 재시도 로직을 절대 제거하지 말 것
```c
if (rc != NRF_SUCCESS && cfg_retry < 10) {
cfg_retry++;
nrf_delay_ms(100);
goto cfg_load_start;
}
```
이 코드가 없으면 부팅 시 FDS가 아직 준비되지 않았을 때 저장된 값을 기본값으로 덮어쓴다.
### 2. config_save()에서 절대 대기하지 말 것
`config_save()`는 BLE 콜백 컨텍스트에서 호출될 수 있다.
내부에서 `while(fds_flag_write)` 등으로 대기하면 **교착 상태** 발생.
fire-and-forget 패턴을 유지해야 한다.
### 3. printf vs DBG_PRINTF 구분
| 함수 | 출력 경로 | RTT에서 보이나? |
|------|----------|----------------|
| `printf()` | UART | X |
| `DBG_PRINTF()` | RTT (또는 UART, 설정에 따라) | O |
FDS 관련 디버그는 반드시 `DBG_PRINTF()` 사용.
### 4. BLE 디바이스 이름은 부팅 시 한 번만 설정됨
`gap_params_init()``sd_ble_gap_device_name_set(SERIAL_NO)`는 부팅 시 한 번만 호출.
`mws?`로 시리얼 변경해도 재부팅 전까지 BLE 광고 이름은 바뀌지 않는다.
### 5. fds_record_update는 비동기
`fds_record_update()` 호출 → 즉시 리턴 → 실제 Flash 쓰기는 나중에 수행 →
`FDS_EVT_UPDATE` 이벤트에서 `fds_flag_write = false`.
연속 쓰기 시 이전 쓰기가 완료될 때까지 다음 쓰기가 스킵될 수 있다 (`[CFG_SAVE] busy, skipped`).
### 6. #pragma pack(1) 필수
`config_data_t``#pragma pack(1)`이 없으면 컴파일러가 패딩을 삽입한다.
이 경우 `offsetof()` 값이 달라지고, FDS에서 읽은 데이터가 엉뚱한 필드에 매핑된다.
---
## 체크리스트
새 프로젝트에 이식할 때 이 체크리스트를 순서대로 확인:
```
[ ] 1. 파일 복사 (fstorage.c/h, dr_mem.c/h, device_config.c/h)
[ ] 2. config_data_t 구조체 수정 (fstorage.h)
[ ] 3. CONFIG_MAGIC_NUMBER_VALUE 날짜 변경 (fstorage.c)
[ ] 4. fds_default_value_set() 기본값 수정 (fstorage.c)
[ ] 5. dr_mem.c 룩업 테이블 수정 (필드 추가/삭제)
[ ] 6. dr_mem.c 기본값 static const 변수 수정
[ ] 7. W25Q32 없으면 dr_mem.c에서 W25Q32 관련 코드 제거
[ ] 8. device_config.h 전역변수 extern 수정
[ ] 9. device_config.c 전역변수 정의 + load 함수 수정
[ ] 10. cmd/cmd.c에 mr*/mw* 핸들러 추가
[ ] 11. cmd/cmd.c의 g_cmd_table[]에 명령어 등록
[ ] 12. main.c 초기화 순서 확인 (BLE → FDS → config_load → device_config → GAP)
[ ] 13. main.c에 go_device_power_off 등 플래그 정의
[ ] 14. sdk_config.h FDS 설정 확인
[ ] 15. Keil 프로젝트에 .c 파일 추가
[ ] 16. Keil Include Path 추가
[ ] 17. 빌드 성공 확인
[ ] 18. RTT 로그로 FDS 초기화 + 로드 확인
[ ] 19. BLE mr*/mw* 명령어 읽기/쓰기 검증
[ ] 20. 재부팅 후 값 유지 검증
```
---
## 빠른 참조: 새 필드 추가 시 수정 위치 요약
새 필드 `my_field`(uint16_t, 2B)를 추가할 때:
| 순서 | 파일 | 수정 내용 |
|------|------|----------|
| 1 | `fstorage.h` | `config_data_t``uint16_t my_field;` 추가 |
| 2 | `fstorage.c` | `fds_default_value_set()``m_config.my_field = 1000;` |
| 3 | `storage/dr_mem.c` | 기본값: `static const uint16_t dflt_my_field = 1000;` |
| 4 | `storage/dr_mem.c` | 테이블: `{ "my_field", MEM_BACKEND_FDS, offsetof(config_data_t, my_field), 2, false, &dflt_my_field }` |
| 5 | `config/device_config.h` | `extern uint16_t my_field_var;` |
| 6 | `config/device_config.c` | `uint16_t my_field_var = 1000;` + `dr_memRead("my_field", ...)` |
| 7 | `cmd/cmd.c` | `Cmd_mrf()` 읽기 핸들러 + `Cmd_mwf()` 쓰기 핸들러 |
| 8 | `cmd/cmd.c` | `g_cmd_table[]``{ "mrf?", true, Cmd_mrf }`, `{ "mwf?", true, Cmd_mwf }` |
**magic_number를 변경하면** 기존 디바이스의 저장 데이터가 초기화된다.
구조체 크기만 바뀌고 기존 필드 순서가 유지되면 magic_number를 유지해도 되지만,
필드 순서가 바뀌면 반드시 magic_number를 변경해야 한다.
---
*문서 작성일: 2026-03-05*
*기준 프로젝트: VivaMyo (ble_app_vivaMayo)*

View File

@@ -0,0 +1,589 @@
# VivaMyo BLE Application - 프로그램 아키텍처 문서
## 1. 프로젝트 개요
VivaMyo는 nRF52840 기반 BLE 의료 기기 펌웨어로, 방광 모니터링을 위한 NIRS(Near-Infrared Spectroscopy) 장치입니다.
### 1.1 주요 특징
- **MCU**: Nordic nRF52840
- **통신**: BLE (Nordic UART Service)
- **보안**: LESC 페어링, Static Passkey
- **센서**: LED 48개, Photodetector, IMU (ICM42670P), 온도센서, 압력센서
- **저장**: EEPROM (설정값 암호화 저장)
---
## 2. 시스템 아키텍처
```
┌─────────────────────────────────────────────────────────────────────┐
│ main.c │
│ (Application Entry Point) │
└──────────────────────────────┬──────────────────────────────────────┘
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌─────────────────────┐
│ BLE Stack │ │ Power Control │ │ Timer Management │
│ (ble_core.c) │ │ (power_ctrl.c) │ │ (main_timer.c) │
└───────┬───────┘ └─────────────────┘ └─────────────────────┘
┌───────────────────────────────────────────────────────────────────┐
│ nus_data_handler() │
│ (BLE Data Reception) │
└──────────────────────────────┬────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────┐
│ received_command_process() │
│ (cmd_parse.c) │
└──────────────────────────────┬────────────────────────────────────┘
┌──────────────────────┴──────────────────────┐
▼ ▼
┌───────────────────────┐ ┌─────────────────────┐
│ Mt_parser │ │ Legacy Parser │
│ (dr_cmd_parser) │ │ (cmd_parse.c) │
│ │ │ │
│ parser.c + cmd.c │ │ 기존 명령어 처리 │
└───────────┬───────────┘ └─────────────────────┘
┌───────────────────────────────────────────────────────────────────┐
│ Command Handlers (cmd.c) │
│ │
│ Cmd_mta, Cmd_sta, Cmd_mcj, Cmd_msn, Cmd_mag ... │
└───────────────────────────────────────────────────────────────────┘
```
---
## 3. Mt_parser 모듈 구조
Mt_parser는 명령어 파싱과 디스패치를 담당하는 독립 모듈입니다.
### 3.1 파일 구성
| 파일 | 경로 | 설명 |
|------|------|------|
| `parser.h` | `/mt_parser/parser.h` | 파서 인터페이스 정의 |
| `parser.c` | `/mt_parser/parser.c` | CRC 검증, TAG 추출, 명령어 디스패치 |
| `cmd.h` | `cmd/cmd.h` | 명령어 테이블 구조체 정의 |
| `cmd.c` | `cmd/cmd.c` | 명령어 핸들러 구현 및 테이블 |
| `dr_util.h/c` | `/mt_parser/dr_util/` | BLE 응답 유틸리티 함수 |
### 3.2 핵심 구조체
```c
// parser.h - 플랫폼 인터페이스
typedef struct {
void (*log)(const char *fmt, ...); // 로그 출력 함수
void (*tx_bin)(const uint8_t *buf, uint16_t len); // BLE 전송 함수
bool crc_check; // CRC 검사 활성화 여부
} dr_platform_if_t;
// cmd.h - 파싱된 명령어 구조체
typedef struct {
char tag[5]; // 4글자 명령어 + NULL ("sta?")
uint8_t data[CMD_MAX_DATA]; // TAG 이후 데이터
uint8_t data_len; // 데이터 길이
} ParsedCmd;
// cmd.h - 명령어 테이블 엔트리
typedef struct {
char tag[5]; // 명령어 TAG ("sta?")
bool enabled; // 활성화 여부
cmd_handler_t handler; // 핸들러 함수 포인터
} CmdEntry;
```
### 3.3 전역 변수
```c
extern dr_platform_if_t g_plat; // 플랫폼 인터페이스
extern bool g_log_enable; // 로그 활성화 플래그
extern CmdEntry g_cmd_table[]; // 명령어 테이블
extern const uint16_t g_cmd_count; // 명령어 개수
```
---
## 4. 명령어 처리 흐름
### 4.1 데이터 수신 경로
```
[BLE Central]
▼ (NUS RX Characteristic Write)
[ble_core.c:nus_data_handler()]
[cmd_parse.c:received_command_process(data, CMD_BLE, length)]
├─► [Mt_parser 초기화] (최초 1회)
│ g_plat.log = log_printf
│ g_plat.tx_bin = binary_tx_handler
│ g_plat.crc_check = true
[parser.c:dr_cmd_parser(data, length)]
├─► [CRC16 검증] → 실패시 "crc!" 응답
├─► [TAG 추출] → 4글자 명령어 파싱
[dr_cmd_dispatch()] → g_cmd_table 검색
├─► [명령어 발견 + 활성화] → handler() 호출
├─► [명령어 발견 + 비활성화] → return 0
└─► [명령어 미발견] → return 0 (Legacy 파서로 fallback)
```
### 4.2 CRC16 검증
```c
// 패킷 구조: [TAG(4)] [DATA(N)] [CRC_LO] [CRC_HI]
// CRC 계산: TAG + DATA 영역
// CRC 위치: 패킷 마지막 2바이트 (Little Endian)
static bool dr_crc16_check_packet(const uint8_t *packet, uint32_t packet_len)
{
uint32_t data_len = packet_len - 2;
uint16_t expected_crc = (uint16_t)packet[packet_len - 2]
| ((uint16_t)packet[packet_len - 1] << 8);
return dr_crc16_check(packet, data_len, expected_crc);
}
```
---
## 5. 명령어 테이블 (cmd.c)
### 5.1 명령어 카테고리
| 카테고리 | 명령어 | 설명 |
|----------|--------|------|
| **A. Device Status** | `mta?`, `sta?`, `str?`, `mqq?` | 디바이스 상태 제어/조회 |
| **B. AGC/Gain** | `mag?`, `sag?`, `sar?` | Auto Gain Control |
| **C. LED DP Value** | `ssa?`, `sab?`, `ssb?`, `srb?` | LED 밝기 설정/조회 |
| **D. LED On/Off** | `ssc?`, `ssd?`, `sse?`, `ssf?`, `sif?`, `ssg?` | LED/Gain 제어 |
| **E. Simple PD** | `ssh?` | 간단한 PD 측정 |
| **F. M48 Full** | `mcj?`, `scj?`, `sdj?`, `sej?`, `sfj?`, `ssj?`, `szj?` | 48채널 PD 측정 |
| **G. IMM ADC** | `saj?` | Immediate ADC 측정 |
| **H. PD Settings** | `ssk?`, `srk?`, `ssl?`, `srl?` | ADC 횟수/지연 설정 |
| **I. Sensors** | `msn?`, `ssn?`, `spn?`, `sso?`, `ssp?` | 배터리/압력/온도/IMU |
| **J. System** | `ssq?`, `ssr?`, `sss?`, `sst?`, `ssv?` | 전원/리셋/버전 |
| **K. Serial/Passkey** | `ssz?`, `spz?`, `sqz?`, `srz?` | 시리얼번호/패스키 |
| **L. EEPROM Array** | `sez?`, `sfz?`, `sgz?` | EEPROM 배열 R/W |
| **M. HW/Life Cycle** | `siz?`, `shz?`, `sxz?`, `syz?` | HW번호/사용횟수 |
| **N. Debug** | `cmd?` | GPIO 테스트 |
### 5.2 명령어 상세 테이블
```c
CmdEntry g_cmd_table[] = {
/* Debug */
{ "cmd?", true, Cmd_cmd }, // GPIO 테스트
/* A. Device Status */
{ "mta?", true, Cmd_mta }, // Device Activate/Sleep (신규)
{ "sta?", true, Cmd_sta }, // Device Activate/Sleep (호환)
{ "str?", false, Cmd_str }, // Status Read
{ "mqq?", true, Cmd_mqq }, // Quick Measurement
/* B. AGC / Gain Measurement */
{ "mag?", true, Cmd_mag }, // Full AGC (신규)
{ "sag?", true, Cmd_mag }, // Full AGC (호환)
{ "sar?", false, Cmd_sar }, // Read LED-PD Gain Array
/* C. LED Power / DP Value */
{ "ssa?", false, Cmd_ssa }, // Single LED Power Read
{ "sab?", false, Cmd_sab }, // All LED AGC Data Read
{ "ssb?", false, Cmd_ssb }, // Single LED Power Write
{ "srb?", false, Cmd_srb }, // Read All 48 LED DP
/* D. LED On/Off & Gain Control */
{ "ssc?", false, Cmd_ssc }, // LED On/Off (index 0-47, 99=off)
{ "ssd?", false, Cmd_ssd }, // AGC Switch On/Off
{ "sse?", false, Cmd_sse }, // Measure DAC Voltage
{ "ssf?", false, Cmd_ssf }, // Set LED DAC Value
{ "sif?", false, Cmd_sif }, // Immediate Gain Set
{ "ssg?", false, Cmd_ssg }, // Set PD Channel
/* E. Simple PD Measurement */
{ "ssh?", false, Cmd_ssh }, // Simple PD Measurement
/* F. PD-ADC M48 Full Measurement */
{ "mcj?", true, Cmd_mcj }, // MODE=2 (Pressure + M48) - 신규
{ "scj?", true, Cmd_mcj }, // MODE=2 (호환)
{ "sdj?", false, Cmd_sdj }, // MODE=3 (M48 Only)
{ "sej?", false, Cmd_sej }, // MODE=4 (M48 + Batt + IMU)
{ "sfj?", false, Cmd_sfj }, // MODE=5 (M48 + Init)
{ "ssj?", false, Cmd_ssj }, // MODE=0 (Combined)
{ "szj?", false, Cmd_szj }, // FAST Mode Settings
/* G. IMM ADC */
{ "saj?", false, Cmd_saj }, // 4-LED Immediate ADC
/* H. PD-ADC Count & Delay */
{ "ssk?", false, Cmd_ssk }, // Set ADC Count (8/16/24/32)
{ "srk?", false, Cmd_srk }, // Read ADC Count
{ "ssl?", false, Cmd_ssl }, // Set PD Delay (us)
{ "srl?", false, Cmd_srl }, // Read PD Delay
/* I. Sensor Measurements */
{ "msn?", true, Cmd_msn }, // Battery Level (신규)
{ "ssn?", true, Cmd_msn }, // Battery Level (호환)
{ "spn?", false, Cmd_spn }, // Pressure Measurement
{ "sso?", false, Cmd_sso }, // Temperature Measurement
{ "ssp?", false, Cmd_ssp }, // IMU Raw Data
/* J. Power / Reset / Version */
{ "ssq?", false, Cmd_ssq }, // Power Off
{ "ssr?", false, Cmd_ssr }, // Bond Delete + Reset
{ "sss?", false, Cmd_sss }, // Device Reset
{ "sst?", false, Cmd_sst }, // Ready Response
{ "ssv?", false, Cmd_ssv }, // Firmware Version
/* K. Serial / Passkey */
{ "ssz?", false, Cmd_ssz }, // Write Serial Number
{ "spz?", false, Cmd_spz }, // Write Passkey
{ "sqz?", false, Cmd_sqz }, // Read Passkey
{ "srz?", false, Cmd_srz }, // Read Serial Number
/* L. EEPROM Array */
{ "sez?", false, Cmd_sez }, // Write 48*uint16 Array
{ "sfz?", false, Cmd_sfz }, // Read 48*uint16 Array
{ "sgz?", false, Cmd_sgz }, // Load DP Preset
/* M. Hardware No / Life Cycle */
{ "siz?", false, Cmd_siz }, // Read HW Number
{ "shz?", false, Cmd_shz }, // Write HW Number
{ "sxz?", false, Cmd_sxz }, // Write Life Cycle
{ "syz?", false, Cmd_syz }, // Read Life Cycle
};
```
### 5.3 활성화된 명령어 (enabled=true)
| 명령어 | 핸들러 | 기능 |
|--------|--------|------|
| `cmd?` | `Cmd_cmd` | GPIO 핀 제어 테스트 |
| `mta?` | `Cmd_mta` | 디바이스 활성화/슬립 |
| `sta?` | `Cmd_sta` | 디바이스 활성화/슬립 (호환) |
| `mqq?` | `Cmd_mqq` | 빠른 측정 시작 |
| `mag?` | `Cmd_mag` | Full AGC 측정 |
| `sag?` | `Cmd_mag` | Full AGC 측정 (호환) |
| `mcj?` | `Cmd_mcj` | M48 전체 측정 (MODE=2) |
| `scj?` | `Cmd_mcj` | M48 전체 측정 (호환) |
| `msn?` | `Cmd_msn` | 배터리 레벨 측정 |
| `ssn?` | `Cmd_msn` | 배터리 레벨 측정 (호환) |
---
## 6. main.c와의 연결
### 6.1 초기화 순서
```c
int main(void)
{
// 1. 기본 초기화
uart_handler_init(); // UART 디버그
log_init(); // NRF 로그
gpio_init(); // GPIO 설정 (LED, PD, GAIN_SW)
timers_init(); // 타이머 초기화
// 2. 설정 로드
load_device_configuration(); // EEPROM에서 설정 읽기
// 3. BLE 초기화
ble_stack_init(); // SoftDevice 활성화
gap_params_init(); // GAP 파라미터 (디바이스명, Passkey)
gatt_init(); // GATT 초기화
services_init(); // NUS 서비스 등록
advertising_init(); // 광고 설정
conn_params_init(); // 연결 파라미터
// 4. 전원 버튼 타이머 시작
power_ctrl_timers_start();
// 5. 메인 루프
for (;;) {
idle_state_handle(); // 전원 관리 + 보안 처리
}
}
```
### 6.2 BLE 데이터 수신 경로
```c
// ble_core.c
static void nus_data_handler(ble_nus_evt_t * p_evt)
{
if (p_evt->type == BLE_NUS_EVT_RX_DATA) {
// cmd_parse.c의 received_command_process() 호출
received_command_process(
p_evt->params.rx_data.p_data,
CMD_BLE,
p_evt->params.rx_data.length
);
}
}
```
### 6.3 Mt_parser 초기화 (cmd_parse.c)
```c
void received_command_process(uint8_t const *data_array, which_cmd_t cmd_t, uint8_t length)
{
// Mt_parser 초기화 (최초 1회)
static bool parser_initialized = false;
if (!parser_initialized) {
g_plat.log = log_printf; // 로그 함수 연결
g_plat.tx_bin = binary_tx_handler; // BLE TX 함수 연결
g_plat.crc_check = true; // CRC 검사 활성화
g_log_enable = true;
parser_initialized = true;
}
// Mt_parser 호출
int result = dr_cmd_parser(r_data, length);
if (result > 0) {
return; // Mt_parser에서 처리됨
}
// Mt_parser에서 처리 안됨 → Legacy 파서로 처리
// ... 기존 scmd 기반 처리 ...
}
```
---
## 7. 소스코드 구조
### 7.1 디렉토리 구조
```
ble_app_vivaMayo/
├── main.c # 메인 엔트리 포인트
├── main.h # 메인 헤더 (타입 정의, 함수 프로토타입)
├── main_timer.c/h # 애플리케이션 타이머
├── cmd_parse.c/h # 명령어 파싱 (Legacy + Mt_parser 연결)
├── cmd/
│ ├── cmd.c # 명령어 핸들러 구현 + 테이블
│ └── cmd.h # 명령어 구조체 정의
├── ble/
│ ├── ble_core.c/h # BLE 스택, GAP, GATT, 광고
│ ├── ble_data_tx.c/h # BLE 데이터 전송, 포맷팅
│ ├── ble_services.c/h # BLE 서비스
│ └── ble_security.c/h # BLE 보안 (페어링)
├── power/
│ └── power_ctrl.c/h # 전원 제어, 슬립 모드
├── peripheral/
│ └── uart_handler.c/h # UART 디버그 출력
├── config/
│ └── device_config.c/h # EEPROM 설정 관리
├── measurements.c/h # 측정 유틸리티
├── meas_pd_*.c/h # PD ADC 측정 모듈들
├── full_agc.c/h # AGC 자동 조절
├── battery_saadc.c/h # 배터리 ADC
├── icm42670p/ # IMU 드라이버
├── i2c_manager.c/h # I2C 관리
├── ada2200_spi.c/h # SPI 디바이스
├── mcp4725_i2c.c/h # DAC 제어
├── tmp235_q1.c/h # 온도 센서
├── cat_interface.c/h # CAT (압력센서) 인터페이스
└── docs/
└── PROGRAM_ARCHITECTURE.md # 이 문서
```
### 7.2 Mt_parser 디렉토리 (별도 위치)
```
/mt_parser/
├── parser.h # 파서 인터페이스
├── parser.c # 파서 구현 (CRC, TAG, 디스패치)
├── cmd.h # (참조용)
└── dr_util/
├── dr_util.h # BLE 응답 유틸리티
└── dr_util.c # dr_ble_return_1/2/3()
```
---
## 8. 명령어 프로토콜
### 8.1 패킷 구조
```
┌─────────┬──────────────────┬─────────┐
│ TAG │ DATA │ CRC │
│ 4 bytes │ N bytes │ 2 bytes │
└─────────┴──────────────────┴─────────┘
예시: sta? 명령어로 디바이스 활성화
TX: [73 74 61 3F] [00 01] [CRC_LO] [CRC_HI]
s t a ? value=1
응답: rta: value
RX: [72 74 61 3A] [00 01] [CRC_LO] [CRC_HI]
r t a : value=1
```
### 8.2 데이터 형식
| 형식 | 설명 | 바이트 순서 |
|------|------|-------------|
| uint16 | 16비트 정수 | Big Endian (MSB first) |
| ASCII | 문자열 | 순차적 |
| CRC16 | 체크섬 | Little Endian (LSB first) |
### 8.3 응답 규칙
| 요청 TAG | 응답 TAG | 예시 |
|----------|----------|------|
| `sta?` | `rta:` | 상태 응답 |
| `mcj?` | (측정 데이터) | 멀티 패킷 |
| 오류 | `xxx!` | `crc!`, `err!` |
---
## 9. 주요 핸들러 구현 예시
### 9.1 Cmd_mta (디바이스 활성화)
```c
static int Cmd_mta(const ParsedCmd *cmd)
{
uint16_t mode = 0;
resetCount = 0;
// 데이터에서 mode 추출 (word index 0)
(void)cmd_get_u16(cmd, 0, &mode);
if (mode == 1) {
// 디바이스 활성화
if (device_activated() == 0) {
device_status = true;
}
}
else if (mode == 0) {
// 슬립 모드 진입
if (device_status == true) {
if (device_sleep_mode() == 0) {
device_status = false;
}
}
}
// BLE 응답 전송
if (g_plat.tx_bin) {
single_format_data(ble_bin_buffer, "rta:", mode);
binary_tx_handler(ble_bin_buffer, 3);
}
return 1;
}
```
### 9.2 Cmd_mcj (M48 전체 측정)
```c
static int Cmd_mcj(const ParsedCmd *cmd)
{
(void)cmd;
// 디바이스 활성화 확인
if (device_status != true) {
param_error("mcj?");
return 1;
}
// 측정 모드 설정
ADC_PD_MODE = 2;
info4 = true;
ble_got_new_data = false;
processing = true;
// 압력 측정
pressure_all_level_meas();
// AGC 스위치 OFF
AGC_GAIN_SW(false);
// M48 ADC 시작
m48_samples_in_buffer = m_pd_adc_cnt;
pd_adc_m48_start = true;
// 타이머 시작
battery_timer_stop();
go_batt = true;
motion_data_once = true;
main_timer_start();
return 1;
}
```
---
## 10. 에러 코드
| 코드 | 값 | 설명 |
|------|-----|------|
| `err_code1` | 65535 | 길이 오류 |
| `err_code2` | 65534 | 활성화 오류 |
| `err_code3` | 65533 | 파라미터 오류 |
| `err_code4` | 65532 | '?' 누락 |
| `err_code5` | 65531 | 알 수 없는 명령어 |
| `err_code6` | 65530 | CRC 오류 |
---
## 11. EEPROM 주소 맵
| 주소 | 크기 | 내용 |
|------|------|------|
| 0x0010 | 12 bytes | HW_NO (암호화) |
| 0x0020 | 6 bytes | Passkey (암호화) |
| 0x0030 | 12 bytes | SERIAL_NO (암호화) |
| 0x0060 | 1 byte | bond_data_delete |
| 0x0065 | 1 byte | reset_status |
| 0x0070 | 1 byte | m_pd_adc_cnt |
| 0x0080 | 2 bytes | m_pd_delay_us |
| 0x0090 | 4 bytes | m_life_cycle |
| 0x0480 | 96 bytes | led_pd_dac_v[48] (AGC Gain) |
---
## 12. 버전 정보
- **펌웨어 버전**: FW25LIT2B102
- **디바이스명**: MEDIDEV_2004
- **문서 작성일**: 2026-01-30
- **작성자**: Claude Assistant
---
## 13. 관련 문서
- [W25Q32RV_FLASH_MEMORY.md](W25Q32RV_FLASH_MEMORY.md) - Flash 메모리 스펙
- Nordic nRF52840 Datasheet
- Nordic SDK 17.x Documentation

View File

@@ -0,0 +1,325 @@
# W25Q32RV Serial NOR Flash Memory
## Overview
W25Q32RV는 Winbond에서 제조한 32M-bit (4MB) Serial NOR Flash Memory입니다. Dual/Quad SPI 및 QPI 인터페이스를 지원하며, 제한된 공간, 핀 수, 전력을 가진 시스템에 적합한 저장 솔루션을 제공합니다.
## 주요 사양
| 항목 | 사양 |
|------|------|
| 용량 | 32M-bit (4MB) |
| 동작 전압 | 2.7V ~ 3.6V |
| 최대 클럭 주파수 | 133MHz (SPI/QPI) |
| Program/Erase 사이클 | 최소 100,000회 |
| 데이터 보존 기간 | 20년 이상 |
| 동작 온도 | -40°C ~ +105°C |
| 대기 전류 | 10µA (typ), 28µA (max @85°C) |
| Power-down 전류 | 0.1µA (typ) |
## 메모리 구조
### 메모리 배열
```
총 용량: 4,194,304 bytes (4MB)
├── 64 Blocks (64KB each)
│ └── 16 Sectors per Block (4KB each)
│ └── 16 Pages per Sector (256 bytes each)
└── 총 16,384 Pages
```
| 단위 | 크기 | 개수 | 주소 범위 |
|------|------|------|----------|
| Page | 256 bytes | 16,384 | - |
| Sector | 4KB | 1,024 | 0x000000 - 0x3FFFFF |
| Block (32KB) | 32KB | 128 | - |
| Block (64KB) | 64KB | 64 | - |
| Chip | 4MB | 1 | 0x000000 - 0x3FFFFF |
### 주소 맵핑
```
Block 0 (64KB): 0x000000 - 0x00FFFF
Block 1 (64KB): 0x010000 - 0x01FFFF
...
Block 63 (64KB): 0x3F0000 - 0x3FFFFF
```
## 핀 구성 (8-pin SOP/WSON/XSON/USON)
| Pin | 이름 | I/O | 기능 |
|-----|------|-----|------|
| 1 | /CS | I | Chip Select (Active Low) |
| 2 | DO (IO1) | I/O | Data Output / IO1 |
| 3 | /WP (IO2) | I/O | Write Protect / IO2 |
| 4 | VSS | - | Ground |
| 5 | DI (IO0) | I/O | Data Input / IO0 |
| 6 | CLK | I | Serial Clock |
| 7 | /HOLD or /RESET (IO3) | I/O | Hold/Reset / IO3 |
| 8 | VCC | - | Power Supply |
## SPI 모드
### 지원 모드
1. **Standard SPI**: CLK, /CS, DI, DO, /WP, /HOLD
2. **Dual SPI**: CLK, /CS, IO0, IO1, /WP, /HOLD
3. **Quad SPI**: CLK, /CS, IO0, IO1, IO2, IO3
4. **QPI**: CLK, /CS, IO0, IO1, IO2, IO3 (4-bit 명령어)
### SPI 모드 0/3 지원
- **Mode 0** (CPOL=0, CPHA=0): CLK idle = LOW
- **Mode 3** (CPOL=1, CPHA=1): CLK idle = HIGH
## 주요 명령어 (Instructions)
### Device ID 읽기
| 명령어 | 코드 | 설명 |
|--------|------|------|
| Read JEDEC ID | 9Fh | Manufacturer/Device ID 읽기 |
| Read Manufacturer/Device ID | 90h | MFR ID (EFh) + Device ID (15h) |
| Release Power-down/Device ID | ABh | Power-down 해제 및 ID 읽기 |
**Device ID 값:**
- Manufacturer ID: `0xEF` (Winbond)
- Device ID (8-bit): `0x15`
- Device ID (16-bit): `0x4016`
### 읽기 명령어
| 명령어 | 코드 | 더미 클럭 | 최대 주파수 |
|--------|------|-----------|-------------|
| Read Data | 03h | 0 | 66MHz |
| Fast Read | 0Bh | 8 | 133MHz |
| Fast Read Dual Output | 3Bh | 8 | 133MHz |
| Fast Read Quad Output | 6Bh | 8 | 133MHz |
| Fast Read Dual I/O | BBh | 4 (Mode bits) | 133MHz |
| Fast Read Quad I/O | EBh | 6 (Mode bits + Dummy) | 133MHz |
### 쓰기/프로그램 명령어
| 명령어 | 코드 | 설명 |
|--------|------|------|
| Write Enable | 06h | 쓰기 활성화 (WEL=1) |
| Write Disable | 04h | 쓰기 비활성화 (WEL=0) |
| Page Program | 02h | 페이지 프로그램 (최대 256 bytes) |
| Quad Input Page Program | 32h | Quad 모드 페이지 프로그램 |
### 지우기 명령어
| 명령어 | 코드 | 크기 | 시간 (typ/max) |
|--------|------|------|----------------|
| Sector Erase | 20h | 4KB | 30ms / 240ms |
| Block Erase (32KB) | 52h | 32KB | 80ms / 800ms |
| Block Erase (64KB) | D8h | 64KB | 120ms / 1200ms |
| Chip Erase | C7h/60h | 전체 | 6s / 40s |
### 상태 레지스터 명령어
| 명령어 | 코드 | 설명 |
|--------|------|------|
| Read Status Register-1 | 05h | SR1 읽기 |
| Read Status Register-2 | 35h | SR2 읽기 |
| Read Status Register-3 | 15h | SR3 읽기 |
| Write Status Register-1 | 01h | SR1 쓰기 |
| Write Status Register-2 | 31h | SR2 쓰기 |
| Write Status Register-3 | 11h | SR3 쓰기 |
### 기타 명령어
| 명령어 | 코드 | 설명 |
|--------|------|------|
| Erase/Program Suspend | 75h | Erase/Program 일시 중단 |
| Erase/Program Resume | 7Ah | Erase/Program 재개 |
| Power-down | B9h | 저전력 모드 진입 |
| Enable Reset | 66h | 리셋 활성화 |
| Reset Device | 99h | 디바이스 리셋 |
| Enter QPI Mode | 38h | QPI 모드 진입 |
| Exit QPI Mode | FFh | QPI 모드 종료 |
## 상태 레지스터 (Status Registers)
### Status Register-1 (05h)
| Bit | 이름 | 설명 | R/W |
|-----|------|------|-----|
| S7 | SRP | Status Register Protect | R/W |
| S6 | SEC | Sector Protect | R/W |
| S5 | TB | Top/Bottom Block Protect | R/W |
| S4 | BP2 | Block Protect Bit 2 | R/W |
| S3 | BP1 | Block Protect Bit 1 | R/W |
| S2 | BP0 | Block Protect Bit 0 | R/W |
| S1 | WEL | Write Enable Latch | R |
| S0 | BUSY | Erase/Write In Progress | R |
### Status Register-2 (35h)
| Bit | 이름 | 설명 | R/W |
|-----|------|------|-----|
| S15 | SUS | Suspend Status | R |
| S14 | CMP | Complement Protect | R/W |
| S13 | LB3 | Security Register Lock 3 | OTP |
| S12 | LB2 | Security Register Lock 2 | OTP |
| S11 | LB1 | Security Register Lock 1 | OTP |
| S10 | LB0 | SFDP Lock | OTP |
| S9 | QE | Quad Enable | R/W |
| S8 | SRL | Status Register Lock | R/W |
### Status Register-3 (15h)
| Bit | 이름 | 설명 | R/W |
|-----|------|------|-----|
| S23 | HOLD/RST | /HOLD or /RESET 기능 | R/W |
| S22 | DRV1 | Output Driver Strength 1 | R/W |
| S21 | DRV0 | Output Driver Strength 0 | R/W |
| S20-S16 | Reserved | 예약됨 | - |
### Output Driver Strength 설정
| DRV1 | DRV0 | 출력 임피던스 |
|------|------|--------------|
| 0 | 0 | 25Ω |
| 0 | 1 | 33Ω |
| 1 | 0 | 50Ω (기본값) |
| 1 | 1 | 100Ω |
## 타이밍 특성
### AC 타이밍 파라미터
| 파라미터 | 심볼 | Min | Max | 단위 |
|----------|------|-----|-----|------|
| Clock Frequency (Fast Read) | fR | DC | 133 | MHz |
| Clock Frequency (Read Data) | fR | DC | 66 | MHz |
| /CS Setup Time | tSLCH | 5 | - | ns |
| /CS Hold Time | tCHSL | 5 | - | ns |
| Data In Setup Time | tDVCH | 2 | - | ns |
| Data In Hold Time | tCHDX | 2.5 | - | ns |
| Clock to Output Valid | tCLQV | - | 4.5 | ns |
| Output Disable Time | tSHQZ | - | 7 | ns |
| /CS Deselect Time (Read) | tSHSL1 | 10 | - | ns |
| /CS Deselect Time (Write) | tSHSL2 | 50 | - | ns |
### 프로그램/지우기 타이밍
| 동작 | 심볼 | Typical | Maximum | 단위 |
|------|------|---------|---------|------|
| Page Program | tPP | 0.25 | 2 | ms |
| Sector Erase (4KB) | tSE | 30 | 240 | ms |
| Block Erase (32KB) | tBE1 | 80 | 800 | ms |
| Block Erase (64KB) | tBE2 | 120 | 1200 | ms |
| Chip Erase | tCE | 6 | 40 | s |
| Write Status Register | tW | 1.5 | 15 | ms |
### 전원 타이밍
| 파라미터 | 심볼 | Min | Max | 단위 |
|----------|------|-----|-----|------|
| VCC(min) to /CS Low | tVSL | 20 | - | µs |
| Power-up to Write Allowed | tPUW | 5 | - | ms |
| Power-down Entry Time | tDP | - | 3 | µs |
| Release from Power-down | tRES1 | - | 3 | µs |
| Reset Time | tRST | - | 30 | µs |
## 전기적 특성
### DC 특성
| 파라미터 | 심볼 | Min | Typ | Max | 단위 |
|----------|------|-----|-----|-----|------|
| Input Low Voltage | VIL | -0.5 | - | 0.3×VCC | V |
| Input High Voltage | VIH | 0.7×VCC | - | VCC+0.4 | V |
| Output Low Voltage | VOL | - | - | 0.2 | V |
| Output High Voltage | VOH | VCC-0.2 | - | - | V |
| Standby Current | ICC1 | - | 10 | 28 | µA |
| Power-down Current | ICC2 | - | 0.1 | 8 | µA |
| Read Current (133MHz) | ICC3 | - | 11 | 20 | mA |
| Program Current | ICC5 | - | 8 | 15 | mA |
| Erase Current | ICC6 | - | 8 | 15 | mA |
## 쓰기 보호 (Write Protection)
### Block Protect 비트 조합 (CMP=0)
| SEC | TB | BP2 | BP1 | BP0 | 보호 영역 | 크기 |
|-----|-----|-----|-----|-----|----------|------|
| X | X | 0 | 0 | 0 | None | - |
| 0 | 0 | 0 | 0 | 1 | Upper 1/64 | 64KB |
| 0 | 0 | 0 | 1 | 0 | Upper 1/32 | 128KB |
| 0 | 0 | 0 | 1 | 1 | Upper 1/16 | 256KB |
| 0 | 0 | 1 | 0 | 0 | Upper 1/8 | 512KB |
| 0 | 0 | 1 | 0 | 1 | Upper 1/4 | 1MB |
| 0 | 0 | 1 | 1 | 0 | Upper 1/2 | 2MB |
| X | X | 1 | 1 | 1 | ALL | 4MB |
## Security Registers
W25Q32RV는 3개의 256-byte Security Register를 제공합니다.
| Register | 주소 범위 |
|----------|----------|
| Security Register 1 | 0x001000 - 0x0010FF |
| Security Register 2 | 0x002000 - 0x0020FF |
| Security Register 3 | 0x003000 - 0x0030FF |
### Security Register 명령어
| 명령어 | 코드 | 설명 |
|--------|------|------|
| Erase Security Register | 44h | Security Register 지우기 |
| Program Security Register | 42h | Security Register 프로그램 |
| Read Security Register | 48h | Security Register 읽기 |
## QPI 모드
### QPI 모드 진입/종료
1. **QPI 진입 조건**: QE 비트가 1로 설정되어야 함
2. **진입**: Enter QPI (38h) 명령어 실행
3. **종료**: Exit QPI (FFh) 명령어 실행
### QPI 모드에서의 명령어 전송
- 모든 명령어, 주소, 데이터가 4비트씩 전송됨
- 명령어 전송에 2클럭만 필요 (SPI 모드의 8클럭 대비)
## 소프트웨어 리셋
디바이스를 초기 상태로 리셋하려면 두 명령어를 순차적으로 실행:
1. Enable Reset (66h)
2. Reset Device (99h)
**주의**: 리셋 중 진행 중인 Erase/Program 동작이 있으면 데이터 손상 가능
## 패키지 정보
| 패키지 | 코드 | 크기 |
|--------|------|------|
| SOP 150-mil | CN | 8-pin |
| SOP 208-mil | CS | 8-pin |
| WSON 6x5-mm | CP | 8-pad |
| XSON 2x3-mm | XH | 8-pad |
| USON 4x3-mm | UU | 8-pad |
## 주문 정보
**Part Number 형식**: W25Q32RV[Package][Temp][Option]
예시: `W25Q32RVCNJQ`
- CN: 8-pin SOP 150-mil
- J: Industrial Plus (-40°C ~ +105°C)
- Q: QE=1 (고정)
## 참조 문서
- Winbond W25Q32RV Datasheet (Revision E, November 2025)
- JEDEC JESD216 (SFDP Standard)
---
*문서 작성일: 2026-01-30*
*소스: W25Q32RV Datasheet Rev.E*

View File

@@ -0,0 +1,317 @@
# W25Q32RV 외장 Flash 초기화 가이드
## 개요
W25Q32RV는 4MB SPI NOR Flash입니다. nRF52 내장 FDS와 달리 **자동 초기화가 없으므로** 드라이버에서 직접 처리해야 합니다.
## 내장 FDS vs 외장 W25Q32RV 비교
| 항목 | 내장 FDS | 외장 W25Q32RV |
|------|----------|---------------|
| 용량 | ~8KB (설정에 따라) | 4MB |
| 인터페이스 | SoftDevice API | SPI |
| 자동 초기화 | ✅ `fds_init()` | ❌ 수동 |
| Wear Leveling | ✅ FDS 내장 | ❌ 직접 구현 |
| 포맷 필요 | 자동 처리 | Sector Erase 필수 |
| 쓰기 단위 | 4 bytes (word) | 256 bytes (page) |
| 지우기 단위 | 자동 GC | 4KB (sector) |
## 하드웨어 연결 (nRF52840)
```
nRF52840 W25Q32RV
───────── ────────
P0.xx (SCK) ───► CLK (Pin 6)
P0.xx (MOSI) ───► DI (Pin 5)
P0.xx (MISO) ◄─── DO (Pin 2)
P0.xx (CS) ───► /CS (Pin 1)
3.3V ───► VCC (Pin 8)
GND ───► VSS (Pin 4)
3.3V ───► /WP (Pin 3) [또는 GPIO로 제어]
3.3V ───► /HOLD (Pin 7) [또는 GPIO로 제어]
```
## 초기화 순서
### 1단계: SPI 초기화
```c
#include "nrf_drv_spi.h"
#define SPI_INSTANCE 0
static const nrf_drv_spi_t spi = NRF_DRV_SPI_INSTANCE(SPI_INSTANCE);
void w25q32_spi_init(void)
{
nrf_drv_spi_config_t spi_config = NRF_DRV_SPI_DEFAULT_CONFIG;
spi_config.ss_pin = W25Q_CS_PIN;
spi_config.miso_pin = W25Q_MISO_PIN;
spi_config.mosi_pin = W25Q_MOSI_PIN;
spi_config.sck_pin = W25Q_SCK_PIN;
spi_config.frequency = NRF_DRV_SPI_FREQ_8M; // 최대 8MHz (nRF52 제한)
spi_config.mode = NRF_DRV_SPI_MODE_0; // CPOL=0, CPHA=0
APP_ERROR_CHECK(nrf_drv_spi_init(&spi, &spi_config, NULL, NULL));
}
```
### 2단계: 칩 확인 (JEDEC ID 읽기)
```c
#define W25Q_CMD_JEDEC_ID 0x9F
#define W25Q_MANUFACTURER_ID 0xEF // Winbond
#define W25Q_DEVICE_ID 0x4016 // W25Q32
bool w25q32_check_id(void)
{
uint8_t tx_buf[1] = { W25Q_CMD_JEDEC_ID };
uint8_t rx_buf[4] = { 0 };
nrf_gpio_pin_clear(W25Q_CS_PIN);
nrf_drv_spi_transfer(&spi, tx_buf, 1, rx_buf, 4);
nrf_gpio_pin_set(W25Q_CS_PIN);
// rx_buf[1] = Manufacturer ID (0xEF)
// rx_buf[2] = Memory Type (0x40)
// rx_buf[3] = Capacity (0x16 = 32Mbit)
if (rx_buf[1] == 0xEF && rx_buf[2] == 0x40 && rx_buf[3] == 0x16) {
DBG_PRINTF("W25Q32RV detected!\r\n");
return true;
}
DBG_PRINTF("Flash ID mismatch: %02X %02X %02X\r\n",
rx_buf[1], rx_buf[2], rx_buf[3]);
return false;
}
```
### 3단계: 상태 확인 (BUSY 체크)
```c
#define W25Q_CMD_READ_STATUS1 0x05
#define W25Q_STATUS_BUSY 0x01
bool w25q32_is_busy(void)
{
uint8_t tx_buf[1] = { W25Q_CMD_READ_STATUS1 };
uint8_t rx_buf[2] = { 0 };
nrf_gpio_pin_clear(W25Q_CS_PIN);
nrf_drv_spi_transfer(&spi, tx_buf, 1, rx_buf, 2);
nrf_gpio_pin_set(W25Q_CS_PIN);
return (rx_buf[1] & W25Q_STATUS_BUSY) != 0;
}
void w25q32_wait_busy(void)
{
while (w25q32_is_busy()) {
nrf_delay_us(100);
}
}
```
### 4단계: Write Enable
```c
#define W25Q_CMD_WRITE_ENABLE 0x06
void w25q32_write_enable(void)
{
uint8_t cmd = W25Q_CMD_WRITE_ENABLE;
nrf_gpio_pin_clear(W25Q_CS_PIN);
nrf_drv_spi_transfer(&spi, &cmd, 1, NULL, 0);
nrf_gpio_pin_set(W25Q_CS_PIN);
}
```
## 섹터 지우기 (Erase)
**중요**: Flash는 1→0만 가능합니다. 0→1로 바꾸려면 반드시 Erase가 필요합니다.
```c
#define W25Q_CMD_SECTOR_ERASE 0x20 // 4KB 단위
void w25q32_sector_erase(uint32_t address)
{
uint8_t tx_buf[4];
// 4KB 경계로 정렬
address &= 0xFFFFF000;
w25q32_write_enable();
tx_buf[0] = W25Q_CMD_SECTOR_ERASE;
tx_buf[1] = (address >> 16) & 0xFF;
tx_buf[2] = (address >> 8) & 0xFF;
tx_buf[3] = address & 0xFF;
nrf_gpio_pin_clear(W25Q_CS_PIN);
nrf_drv_spi_transfer(&spi, tx_buf, 4, NULL, 0);
nrf_gpio_pin_set(W25Q_CS_PIN);
// Erase 완료 대기 (최대 240ms)
w25q32_wait_busy();
}
```
### 지우기 시간
| 명령 | 크기 | 일반 시간 | 최대 시간 |
|------|------|-----------|-----------|
| Sector Erase (0x20) | 4KB | 30ms | 240ms |
| Block Erase (0x52) | 32KB | 80ms | 800ms |
| Block Erase (0xD8) | 64KB | 120ms | 1200ms |
| Chip Erase (0xC7) | 4MB | 6s | 40s |
## 페이지 쓰기 (Page Program)
```c
#define W25Q_CMD_PAGE_PROGRAM 0x02
#define W25Q_PAGE_SIZE 256
void w25q32_page_write(uint32_t address, const uint8_t *data, uint16_t len)
{
uint8_t tx_buf[4 + W25Q_PAGE_SIZE];
if (len > W25Q_PAGE_SIZE) {
len = W25Q_PAGE_SIZE;
}
w25q32_write_enable();
tx_buf[0] = W25Q_CMD_PAGE_PROGRAM;
tx_buf[1] = (address >> 16) & 0xFF;
tx_buf[2] = (address >> 8) & 0xFF;
tx_buf[3] = address & 0xFF;
memcpy(&tx_buf[4], data, len);
nrf_gpio_pin_clear(W25Q_CS_PIN);
nrf_drv_spi_transfer(&spi, tx_buf, 4 + len, NULL, 0);
nrf_gpio_pin_set(W25Q_CS_PIN);
// Write 완료 대기 (최대 2ms)
w25q32_wait_busy();
}
```
## 데이터 읽기
```c
#define W25Q_CMD_READ_DATA 0x03
void w25q32_read(uint32_t address, uint8_t *data, uint16_t len)
{
uint8_t tx_buf[4];
tx_buf[0] = W25Q_CMD_READ_DATA;
tx_buf[1] = (address >> 16) & 0xFF;
tx_buf[2] = (address >> 8) & 0xFF;
tx_buf[3] = address & 0xFF;
nrf_gpio_pin_clear(W25Q_CS_PIN);
nrf_drv_spi_transfer(&spi, tx_buf, 4, NULL, 0);
nrf_drv_spi_transfer(&spi, NULL, 0, data, len);
nrf_gpio_pin_set(W25Q_CS_PIN);
}
```
## 첫 사용 시 초기화 플로우
```
┌─────────────────────────────────────────┐
│ 1. SPI 초기화 │
│ w25q32_spi_init() │
└────────────────┬────────────────────────┘
┌─────────────────────────────────────────┐
│ 2. 칩 ID 확인 │
│ w25q32_check_id() │
│ → 실패 시 에러 처리 │
└────────────────┬────────────────────────┘
┌─────────────────────────────────────────┐
│ 3. Magic Number 확인 (주소 0x000000) │
│ w25q32_read(0, &magic, 4) │
└────────────────┬────────────────────────┘
┌───────┴───────┐
│ magic == 유효? │
└───────┬───────┘
│ │
Yes No (첫 사용/손상)
│ │
↓ ↓
┌──────────────┐ ┌─────────────────────────┐
│ 정상 사용 │ │ 4. 첫 섹터 Erase │
│ │ │ w25q32_sector_erase(0)│
└──────────────┘ │ │
│ 5. Magic Number 쓰기 │
│ w25q32_page_write(0, │
│ &magic, 4) │
│ │
│ 6. 기본값 저장 │
└─────────────────────────┘
```
## 메모리 맵 예시
```
주소 범위 용도 크기
─────────────────────────────────────────────
0x000000 - 0x000FFF Config 영역 4KB (Sector 0)
0x001000 - 0x001FFF Calibration 1 4KB (Sector 1)
0x002000 - 0x002FFF Calibration 2 4KB (Sector 2)
0x003000 - 0x00FFFF Reserved 52KB
0x010000 - 0x0FFFFF Data Log 1 960KB
0x100000 - 0x1FFFFF Data Log 2 1MB
0x200000 - 0x3FFFFF Data Log 3 2MB
─────────────────────────────────────────────
Total 4MB
```
## Power-down 모드
저전력 모드가 필요한 경우:
```c
#define W25Q_CMD_POWER_DOWN 0xB9
#define W25Q_CMD_RELEASE_PD 0xAB
void w25q32_power_down(void)
{
uint8_t cmd = W25Q_CMD_POWER_DOWN;
nrf_gpio_pin_clear(W25Q_CS_PIN);
nrf_drv_spi_transfer(&spi, &cmd, 1, NULL, 0);
nrf_gpio_pin_set(W25Q_CS_PIN);
// 전류: 0.1µA (typ)
}
void w25q32_wake_up(void)
{
uint8_t cmd = W25Q_CMD_RELEASE_PD;
nrf_gpio_pin_clear(W25Q_CS_PIN);
nrf_drv_spi_transfer(&spi, &cmd, 1, NULL, 0);
nrf_gpio_pin_set(W25Q_CS_PIN);
nrf_delay_us(3); // tRES1: Release from power-down
}
```
## 주의사항
1. **Erase 전 확인**: 쓰기 전 해당 섹터가 이미 Erase 되어 있는지 확인
2. **Page 경계**: Page Program은 256바이트 경계를 넘지 않도록 주의
3. **Wear Leveling**: 같은 섹터 반복 사용 시 수명 단축 (최소 100,000회)
4. **전원 차단**: Erase/Program 중 전원 차단 시 데이터 손상 가능
5. **SPI 속도**: nRF52840은 최대 8MHz, W25Q32RV는 133MHz까지 지원
## 관련 파일
- [W25Q32RV_FLASH_MEMORY.md](./W25Q32RV_FLASH_MEMORY.md) - 상세 스펙
- [fstorage.c](../fstorage.c) - 내장 FDS 구현 참조
---
*문서 작성일: 2026-02-04*

View File

@@ -0,0 +1,214 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="900px" height="1200px" viewBox="-0.5 -0.5 900 1200" content="&lt;mxfile host=&quot;app.diagrams.net&quot;&gt;&lt;diagram name=&quot;VivaMayo Architecture&quot;&gt;&lt;mxGraphModel dx=&quot;900&quot; dy=&quot;1200&quot; grid=&quot;1&quot; gridSize=&quot;10&quot; guides=&quot;1&quot; tooltips=&quot;1&quot; connect=&quot;1&quot; arrows=&quot;1&quot; fold=&quot;1&quot; page=&quot;1&quot; pageScale=&quot;1&quot; pageWidth=&quot;900&quot; pageHeight=&quot;1200&quot;&gt;&lt;root&gt;&lt;mxCell id=&quot;0&quot;/&gt;&lt;mxCell id=&quot;1&quot; parent=&quot;0&quot;/&gt;&lt;/root&gt;&lt;/mxGraphModel&gt;&lt;/diagram&gt;&lt;/mxfile&gt;">
<defs>
<style type="text/css">
.title { font: bold 24px 'Segoe UI', Arial, sans-serif; fill: #1a1a1a; }
.subtitle { font: 16px 'Segoe UI', Arial, sans-serif; fill: #666; }
.box-main { fill: #4a90d9; stroke: #2d5a8a; stroke-width: 2; rx: 8; }
.box-ble { fill: #5cb85c; stroke: #3d8b3d; stroke-width: 2; rx: 8; }
.box-parser { fill: #f0ad4e; stroke: #c77c00; stroke-width: 2; rx: 8; }
.box-cmd { fill: #d9534f; stroke: #a33b38; stroke-width: 2; rx: 8; }
.box-handler { fill: #9b59b6; stroke: #6c3483; stroke-width: 2; rx: 8; }
.box-peripheral { fill: #17a2b8; stroke: #0d6efd; stroke-width: 2; rx: 8; }
.box-storage { fill: #6c757d; stroke: #495057; stroke-width: 2; rx: 8; }
.box-label { font: bold 14px 'Segoe UI', Arial, sans-serif; fill: white; text-anchor: middle; }
.box-sublabel { font: 11px 'Segoe UI', Arial, sans-serif; fill: rgba(255,255,255,0.8); text-anchor: middle; }
.arrow { stroke: #333; stroke-width: 2; fill: none; marker-end: url(#arrowhead); }
.arrow-dashed { stroke: #666; stroke-width: 2; fill: none; stroke-dasharray: 5,5; marker-end: url(#arrowhead); }
.section-title { font: bold 16px 'Segoe UI', Arial, sans-serif; fill: #333; }
.note { font: 12px 'Segoe UI', Arial, sans-serif; fill: #666; }
.legend-box { stroke: #ccc; stroke-width: 1; fill: #fafafa; rx: 5; }
</style>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#333"/>
</marker>
</defs>
<!-- Background -->
<rect width="900" height="1200" fill="#ffffff"/>
<!-- Title -->
<text x="450" y="40" class="title" text-anchor="middle">VivaMayo BLE Application Architecture</text>
<text x="450" y="65" class="subtitle" text-anchor="middle">nRF52840 기반 NIRS 방광 모니터링 장치</text>
<!-- ==================== Section 1: Application Entry ==================== -->
<text x="50" y="110" class="section-title">1. Application Entry</text>
<!-- main.c -->
<rect x="350" y="130" width="200" height="60" class="box-main"/>
<text x="450" y="158" class="box-label">main.c</text>
<text x="450" y="175" class="box-sublabel">Application Entry Point</text>
<!-- ==================== Section 2: Core Modules ==================== -->
<text x="50" y="230" class="section-title">2. Core Modules</text>
<!-- BLE Stack -->
<rect x="100" y="250" width="160" height="50" class="box-ble"/>
<text x="180" y="275" class="box-label">ble_core.c</text>
<text x="180" y="290" class="box-sublabel">BLE Stack</text>
<!-- Power Control -->
<rect x="370" y="250" width="160" height="50" class="box-peripheral"/>
<text x="450" y="275" class="box-label">power_ctrl.c</text>
<text x="450" y="290" class="box-sublabel">Power Management</text>
<!-- Timer -->
<rect x="640" y="250" width="160" height="50" class="box-peripheral"/>
<text x="720" y="275" class="box-label">main_timer.c</text>
<text x="720" y="290" class="box-sublabel">Timer Management</text>
<!-- Arrows from main -->
<path d="M 400 190 L 400 210 L 180 210 L 180 250" class="arrow"/>
<path d="M 450 190 L 450 250" class="arrow"/>
<path d="M 500 190 L 500 210 L 720 210 L 720 250" class="arrow"/>
<!-- ==================== Section 3: BLE Data Flow ==================== -->
<text x="50" y="350" class="section-title">3. BLE Data Reception</text>
<!-- NUS Data Handler -->
<rect x="100" y="370" width="200" height="50" class="box-ble"/>
<text x="200" y="395" class="box-label">nus_data_handler()</text>
<text x="200" y="410" class="box-sublabel">BLE NUS RX Event</text>
<!-- Arrow -->
<path d="M 180 300 L 180 330 L 200 330 L 200 370" class="arrow"/>
<!-- ==================== Section 4: Command Processing ==================== -->
<text x="50" y="470" class="section-title">4. Command Processing</text>
<!-- received_command_process -->
<rect x="100" y="490" width="250" height="50" class="box-parser"/>
<text x="225" y="515" class="box-label">received_command_process()</text>
<text x="225" y="530" class="box-sublabel">cmd_parse.c</text>
<!-- Arrow -->
<path d="M 200 420 L 200 450 L 225 450 L 225 490" class="arrow"/>
<!-- ==================== Section 5: Parser Layer ==================== -->
<text x="50" y="590" class="section-title">5. Parser Layer (Mt_parser)</text>
<!-- dr_cmd_parser -->
<rect x="100" y="610" width="200" height="60" class="box-parser"/>
<text x="200" y="635" class="box-label">dr_cmd_parser()</text>
<text x="200" y="655" class="box-sublabel">parser.c - CRC + TAG</text>
<!-- Legacy Parser -->
<rect x="400" y="610" width="200" height="60" class="box-storage"/>
<text x="500" y="635" class="box-label">Legacy Parser</text>
<text x="500" y="655" class="box-sublabel">cmd_parse.c (fallback)</text>
<!-- Arrows -->
<path d="M 225 540 L 225 570 L 200 570 L 200 610" class="arrow"/>
<path d="M 300 640 L 400 640" class="arrow-dashed"/>
<text x="350" y="630" class="note">fallback</text>
<!-- ==================== Section 6: Command Dispatch ==================== -->
<text x="50" y="720" class="section-title">6. Command Dispatch</text>
<!-- g_cmd_table -->
<rect x="100" y="740" width="200" height="50" class="box-cmd"/>
<text x="200" y="765" class="box-label">g_cmd_table[]</text>
<text x="200" y="780" class="box-sublabel">51 Commands</text>
<!-- Arrow -->
<path d="M 200 670 L 200 740" class="arrow"/>
<!-- ==================== Section 7: Command Handlers ==================== -->
<text x="50" y="840" class="section-title">7. Command Handlers (cmd.c)</text>
<!-- Handler boxes -->
<rect x="50" y="860" width="120" height="45" class="box-handler"/>
<text x="110" y="882" class="box-label">Cmd_mta</text>
<text x="110" y="897" class="box-sublabel">Device Status</text>
<rect x="180" y="860" width="120" height="45" class="box-handler"/>
<text x="240" y="882" class="box-label">Cmd_mcj</text>
<text x="240" y="897" class="box-sublabel">M48 Measure</text>
<rect x="310" y="860" width="120" height="45" class="box-handler"/>
<text x="370" y="882" class="box-label">Cmd_msn</text>
<text x="370" y="897" class="box-sublabel">Battery</text>
<rect x="440" y="860" width="120" height="45" class="box-handler"/>
<text x="500" y="882" class="box-label">Cmd_mag</text>
<text x="500" y="897" class="box-sublabel">AGC</text>
<rect x="570" y="860" width="120" height="45" class="box-handler"/>
<text x="630" y="882" class="box-label">Cmd_cmd</text>
<text x="630" y="897" class="box-sublabel">GPIO Test</text>
<rect x="700" y="860" width="120" height="45" class="box-handler"/>
<text x="760" y="882" class="box-label">...</text>
<text x="760" y="897" class="box-sublabel">+45 more</text>
<!-- Arrows to handlers -->
<path d="M 200 790 L 200 820 L 110 820 L 110 860" class="arrow"/>
<path d="M 200 790 L 200 820 L 240 820 L 240 860" class="arrow"/>
<path d="M 200 790 L 200 820 L 370 820 L 370 860" class="arrow"/>
<path d="M 200 790 L 200 820 L 500 820 L 500 860" class="arrow"/>
<path d="M 200 790 L 200 820 L 630 820 L 630 860" class="arrow"/>
<path d="M 200 790 L 200 820 L 760 820 L 760 860" class="arrow"/>
<!-- ==================== Section 8: Hardware Layer ==================== -->
<text x="50" y="960" class="section-title">8. Hardware / Peripherals</text>
<rect x="50" y="980" width="100" height="40" class="box-peripheral"/>
<text x="100" y="1005" class="box-label">LED x48</text>
<rect x="160" y="980" width="100" height="40" class="box-peripheral"/>
<text x="210" y="1005" class="box-label">PD ADC</text>
<rect x="270" y="980" width="100" height="40" class="box-peripheral"/>
<text x="320" y="1005" class="box-label">IMU</text>
<rect x="380" y="980" width="100" height="40" class="box-peripheral"/>
<text x="430" y="1005" class="box-label">Temp</text>
<rect x="490" y="980" width="100" height="40" class="box-peripheral"/>
<text x="540" y="1005" class="box-label">Pressure</text>
<rect x="600" y="980" width="100" height="40" class="box-peripheral"/>
<text x="650" y="1005" class="box-label">Battery</text>
<rect x="710" y="980" width="100" height="40" class="box-peripheral"/>
<text x="760" y="1005" class="box-label">EEPROM</text>
<!-- ==================== Section 9: BLE Response ==================== -->
<text x="50" y="1070" class="section-title">9. BLE Response</text>
<rect x="100" y="1090" width="200" height="50" class="box-ble"/>
<text x="200" y="1115" class="box-label">binary_tx_handler()</text>
<text x="200" y="1130" class="box-sublabel">ble_data_tx.c + CRC16</text>
<rect x="400" y="1090" width="200" height="50" class="box-ble"/>
<text x="500" y="1115" class="box-label">BLE Central</text>
<text x="500" y="1130" class="box-sublabel">Mobile App</text>
<path d="M 300 1115 L 400 1115" class="arrow"/>
<!-- ==================== Legend ==================== -->
<rect x="650" y="1050" width="220" height="130" class="legend-box"/>
<text x="760" y="1072" class="section-title" text-anchor="middle">Legend</text>
<rect x="665" y="1085" width="20" height="15" class="box-main"/>
<text x="695" y="1097" class="note">Main Entry</text>
<rect x="665" y="1105" width="20" height="15" class="box-ble"/>
<text x="695" y="1117" class="note">BLE Layer</text>
<rect x="665" y="1125" width="20" height="15" class="box-parser"/>
<text x="695" y="1137" class="note">Parser Layer</text>
<rect x="780" y="1085" width="20" height="15" class="box-cmd"/>
<text x="810" y="1097" class="note">Cmd Table</text>
<rect x="780" y="1105" width="20" height="15" class="box-handler"/>
<text x="810" y="1117" class="note">Handlers</text>
<rect x="780" y="1125" width="20" height="15" class="box-peripheral"/>
<text x="810" y="1137" class="note">Peripherals</text>
<!-- Version info -->
<text x="450" y="1180" class="note" text-anchor="middle">VivaMayo Architecture v1.0 | Generated: 2026-01-30</text>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="800px" height="980px" viewBox="-0.5 -0.5 800 980">
<defs>
<style type="text/css">
.title { font: bold 22px 'Segoe UI', Arial, sans-serif; fill: #1a1a1a; }
.subtitle { font: 14px 'Segoe UI', Arial, sans-serif; fill: #666; }
.process { fill: #e3f2fd; stroke: #1976d2; stroke-width: 2; rx: 8; }
.decision { fill: #fff3e0; stroke: #f57c00; stroke-width: 2; }
.terminal { fill: #e8f5e9; stroke: #388e3c; stroke-width: 2; rx: 20; }
.error { fill: #ffebee; stroke: #d32f2f; stroke-width: 2; rx: 8; }
.data { fill: #f3e5f5; stroke: #7b1fa2; stroke-width: 2; }
.box-label { font: bold 12px 'Segoe UI', Arial, sans-serif; fill: #333; text-anchor: middle; }
.box-sublabel { font: 10px 'Segoe UI', Arial, sans-serif; fill: #666; text-anchor: middle; }
.arrow { stroke: #333; stroke-width: 2; fill: none; marker-end: url(#arrowhead); }
.arrow-yes { stroke: #388e3c; stroke-width: 2; fill: none; marker-end: url(#arrowhead-green); }
.arrow-no { stroke: #d32f2f; stroke-width: 2; fill: none; marker-end: url(#arrowhead-red); }
.label { font: 11px 'Segoe UI', Arial, sans-serif; fill: #333; }
.label-yes { font: bold 11px 'Segoe UI', Arial, sans-serif; fill: #388e3c; }
.label-no { font: bold 11px 'Segoe UI', Arial, sans-serif; fill: #d32f2f; }
.note { font: 10px 'Segoe UI', Arial, sans-serif; fill: #666; font-style: italic; }
.packet { fill: #fafafa; stroke: #999; stroke-width: 1; }
.packet-label { font: 10px 'Courier New', monospace; fill: #333; }
</style>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#333"/>
</marker>
<marker id="arrowhead-green" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#388e3c"/>
</marker>
<marker id="arrowhead-red" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#d32f2f"/>
</marker>
</defs>
<!-- Background -->
<rect width="800" height="980" fill="#ffffff"/>
<!-- Title -->
<text x="400" y="35" class="title" text-anchor="middle">Command Processing Flow</text>
<text x="400" y="55" class="subtitle" text-anchor="middle">Mt_parser 명령어 처리 흐름도</text>
<!-- ==================== Start ==================== -->
<rect x="300" y="80" width="200" height="40" class="terminal"/>
<text x="400" y="105" class="box-label">BLE Data Received</text>
<path d="M 400 120 L 400 150" class="arrow"/>
<!-- ==================== nus_data_handler ==================== -->
<rect x="300" y="150" width="200" height="50" class="process"/>
<text x="400" y="175" class="box-label">nus_data_handler()</text>
<text x="400" y="190" class="box-sublabel">ble_core.c</text>
<path d="M 400 200 L 400 230" class="arrow"/>
<!-- ==================== received_command_process ==================== -->
<rect x="275" y="230" width="250" height="50" class="process"/>
<text x="400" y="255" class="box-label">received_command_process()</text>
<text x="400" y="270" class="box-sublabel">cmd_parse.c</text>
<path d="M 400 280 L 400 310" class="arrow"/>
<!-- ==================== dr_cmd_parser ==================== -->
<rect x="300" y="310" width="200" height="50" class="process"/>
<text x="400" y="335" class="box-label">dr_cmd_parser()</text>
<text x="400" y="350" class="box-sublabel">parser.c</text>
<path d="M 400 360 L 400 400" class="arrow"/>
<!-- ==================== CRC Check Decision ==================== -->
<polygon points="400,400 480,440 400,480 320,440" class="decision"/>
<text x="400" y="445" class="box-label">CRC OK?</text>
<!-- CRC Fail -->
<path d="M 480 440 L 580 440 L 580 400" class="arrow-no"/>
<text x="520" y="430" class="label-no">NO</text>
<rect x="530" y="360" width="120" height="40" class="error"/>
<text x="590" y="385" class="box-label">"crc!" 응답</text>
<!-- CRC OK -->
<path d="M 400 480 L 400 520" class="arrow-yes"/>
<text x="415" y="500" class="label-yes">YES</text>
<!-- ==================== TAG Extract ==================== -->
<rect x="300" y="520" width="200" height="50" class="process"/>
<text x="400" y="540" class="box-label">TAG 추출</text>
<text x="400" y="555" class="box-sublabel">"xxx?" 형식 (? 포함 4글자)</text>
<text x="400" y="568" class="box-sublabel">dr_copy_tag()</text>
<path d="M 400 570 L 400 610" class="arrow"/>
<!-- ==================== Command Table Search Decision ==================== -->
<polygon points="400,610 500,660 400,710 300,660" class="decision"/>
<text x="400" y="655" class="box-label">g_cmd_table</text>
<text x="400" y="670" class="box-label">에서 검색?</text>
<!-- Not Found -->
<path d="M 500 660 L 600 660 L 600 620" class="arrow-no"/>
<text x="540" y="650" class="label-no">NOT FOUND</text>
<rect x="530" y="580" width="140" height="40" class="process"/>
<text x="600" y="605" class="box-label">Legacy Parser로</text>
<!-- Found -->
<path d="M 400 710 L 400 750" class="arrow-yes"/>
<text x="415" y="730" class="label-yes">FOUND</text>
<!-- ==================== Enabled Check ==================== -->
<polygon points="400,750 470,785 400,820 330,785" class="decision"/>
<text x="400" y="790" class="box-label">enabled?</text>
<!-- Disabled -->
<path d="M 470 785 L 550 785" class="arrow-no"/>
<text x="500" y="775" class="label-no">false</text>
<rect x="550" y="765" width="100" height="40" class="error"/>
<text x="600" y="790" class="box-label">return 0</text>
<!-- Enabled -->
<path d="M 400 820 L 400 860" class="arrow-yes"/>
<text x="415" y="840" class="label-yes">true</text>
<!-- ==================== Handler Execute ==================== -->
<rect x="300" y="860" width="200" height="50" class="terminal"/>
<text x="400" y="885" class="box-label">handler() 실행</text>
<text x="400" y="900" class="box-sublabel">Cmd_xxx()</text>
<!-- ==================== Packet Structure ==================== -->
<text x="80" y="110" class="label" font-weight="bold">Packet Structure:</text>
<rect x="30" y="125" width="60" height="30" class="packet"/>
<text x="60" y="145" class="packet-label">TAG</text>
<text x="60" y="165" class="note">4 bytes</text>
<rect x="90" y="125" width="80" height="30" class="packet"/>
<text x="130" y="145" class="packet-label">DATA</text>
<text x="130" y="165" class="note">N bytes</text>
<rect x="170" y="125" width="60" height="30" class="packet"/>
<text x="200" y="145" class="packet-label">CRC</text>
<text x="200" y="165" class="note">2 bytes</text>
<!-- Example -->
<text x="80" y="200" class="label" font-weight="bold">Example (sta? mode=1):</text>
<!-- TAG box -->
<rect x="30" y="215" width="80" height="50" class="packet"/>
<text x="70" y="235" class="packet-label" text-anchor="middle">73 74 61 3F</text>
<text x="70" y="255" class="note" text-anchor="middle">s t a ?</text>
<!-- DATA box -->
<rect x="110" y="215" width="60" height="50" class="packet"/>
<text x="140" y="235" class="packet-label" text-anchor="middle">00 01</text>
<text x="140" y="255" class="note" text-anchor="middle">val=1</text>
<!-- CRC box -->
<rect x="170" y="215" width="60" height="50" class="packet"/>
<text x="200" y="235" class="packet-label" text-anchor="middle">XX XX</text>
<text x="200" y="255" class="note" text-anchor="middle">CRC</text>
<!-- Legend -->
<rect x="580" y="80" width="190" height="180" fill="#fafafa" stroke="#ddd" rx="5"/>
<text x="675" y="105" class="label" font-weight="bold" text-anchor="middle">Legend</text>
<rect x="595" y="120" width="25" height="18" class="process"/>
<text x="630" y="133" class="label">Process</text>
<polygon points="607,155 620,165 607,175 595,165" class="decision"/>
<text x="630" y="168" class="label">Decision</text>
<rect x="595" y="185" width="25" height="18" class="terminal"/>
<text x="630" y="198" class="label">Start/End</text>
<rect x="595" y="215" width="25" height="18" class="error"/>
<text x="630" y="228" class="label">Error</text>
<!-- Version -->
<text x="400" y="960" class="note" text-anchor="middle">Command Flow v1.0 | Generated: 2026-01-30</text>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB