# 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)*
