# 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 명령어 인터페이스 추가*
