MTD 서브시스템 심화

Linux MTD(Memory Technology Device) 서브시스템: Flash 메모리 기초부터 MTD 아키텍처, NOR/NAND 드라이버, SPI-NOR/SPI-NAND, UBI/UBIFS, JFFS2, 파티션 관리, 유저스페이스 도구까지 종합 가이드.

커널 소스 위치: MTD 서브시스템의 핵심 소스는 drivers/mtd/ 디렉토리에 위치합니다. 주요 하위 디렉토리: nand/ (NAND 컨트롤러), spi-nor/ (SPI NOR Flash), spi/ (SPI NAND), ubi/ (UBI 레이어), chips/ (CFI/JEDEC), parsers/ (파티션 파서). 헤더: include/linux/mtd/mtd.h, include/linux/mtd/nand.h, include/linux/mtd/spi-nor.h.

MTD 서브시스템 개요

MTD(Memory Technology Device)는 Flash 메모리를 위한 Linux 커널 추상화 계층입니다. 일반 블록 디바이스(/dev/sdX)와 달리 Flash 메모리는 근본적으로 다른 특성을 가지므로 전용 서브시스템이 필요합니다.

Flash vs 블록 디바이스

Flash 메모리가 HDD/SSD와 같은 블록 디바이스와 다른 핵심 차이점은 다음과 같습니다:

특성블록 디바이스 (HDD/SSD)Flash (MTD)
쓰기 전 지우기불필요 (직접 덮어쓰기)필수 (erase-before-write)
지우기 단위섹터 (512B / 4KB)Erase Block (64KB ~ 256KB)
마모 (Wear)FTL이 내부 처리소프트웨어가 직접 관리
배드 블록FTL이 내부 숨김소프트웨어가 직접 관리
인터페이스/dev/sdX, /dev/nvmeXnY/dev/mtdN, /dev/mtdblockN
파일시스템ext4, XFS, BtrfsJFFS2, UBIFS, YAFFS2
FTL vs Raw Flash: SSD/eMMC/SD카드는 내부 FTL(Flash Translation Layer)이 있어 블록 디바이스처럼 동작합니다. MTD는 FTL이 없는 Raw Flash(NOR/NAND 칩)를 직접 제어하는 서브시스템입니다. eMMC는 drivers/mmc/, NVMe SSD는 drivers/nvme/에서 처리합니다.

MTD의 역할

MTD 서브시스템은 유저스페이스와 Flash 하드웨어 사이의 다층 추상화를 제공합니다. 유저스페이스 애플리케이션이나 Flash 전용 파일시스템은 struct mtd_info의 통일된 인터페이스를 통해 다양한 Flash 칩에 접근합니다.

User Space: flash_erase / nanddump / dd / mount UBIFS / JFFS2 mtdchar / mtdblock UBI (Wear Leveling + BBM) MTD Core (struct mtd_info) SPI-NOR 드라이버 NAND 드라이버 SPI-NAND 드라이버 Flash Hardware: NOR Chip / NAND Chip / SPI Flash

소스 트리 구조

MTD 관련 커널 소스 디렉토리 구조입니다:

/* drivers/mtd/ 디렉토리 구조 */
drivers/mtd/
├── mtdcore.c          /* MTD 코어: 디바이스 등록, sysfs */
├── mtdchar.c          /* /dev/mtdN 캐릭터 디바이스 */
├── mtdblock.c         /* /dev/mtdblockN 블록 디바이스 에뮬레이션 */
├── mtdpart.c          /* 파티션 관리 */
├── mtdconcat.c        /* MTD 디바이스 결합 */
├── chips/             /* CFI / JEDEC NOR Flash 프로브 */
│   ├── cfi_probe.c
│   ├── cfi_cmdset_0001.c  /* Intel/Sharp 확장 */
│   └── cfi_cmdset_0002.c  /* AMD/Fujitsu 확장 */
├── nand/              /* NAND Flash 서브시스템 */
│   ├── core.c         /* NAND 코어 */
│   ├── bbt.c          /* Bad Block Table */
│   ├── ecc.c          /* ECC 엔진 */
│   └── raw/           /* Raw NAND 컨트롤러 드라이버 */
├── spi-nor/           /* SPI NOR Flash */
│   ├── core.c
│   └── sfdp.c         /* SFDP 파싱 */
├── spi/               /* SPI NAND Flash */
│   └── core.c
├── ubi/               /* UBI (Unsorted Block Images) */
│   ├── build.c
│   ├── wl.c           /* Wear Leveling */
│   └── eba.c          /* Eraseblock Association */
└── parsers/           /* 파티션 파서 */
    ├── cmdlinepart.c
    └── ofpart.c       /* Device Tree 파티션 */

Flash 메모리 기초

MTD 서브시스템을 이해하려면 Flash 메모리의 물리적 특성을 파악해야 합니다. NOR Flash와 NAND Flash는 셀 구조, 접근 방식, 성능 특성이 크게 다릅니다.

NOR Flash vs NAND Flash

특성NOR FlashNAND Flash
셀 연결병렬 (NOR 게이트 구조)직렬 (NAND 게이트 구조)
읽기 방식바이트 단위 랜덤 접근페이지 단위 순차 접근
XIP (Execute-In-Place)지원 (메모리 매핑 가능)미지원
읽기 속도빠름 (100~150ns)느림 (25~50us 페이지 읽기)
쓰기 속도느림 (워드 프로그래밍)빠름 (페이지 프로그래밍)
지우기 속도매우 느림 (0.5~1초/블록)빠름 (1~3ms/블록)
지우기 블록 크기64KB ~ 256KB128KB ~ 512KB
집적도낮음 (수 MB ~ 수십 MB)높음 (수 GB ~ 수 TB)
내구성10만 ~ 100만 회 지우기1천 ~ 10만 회 지우기
배드 블록거의 없음공장 출하 시에도 존재 가능
ECC 필요일반적으로 불필요필수
주요 용도부트로더, 펌웨어 저장대용량 데이터 저장

Erase Block, Page, OOB

Flash 메모리의 기본 연산 단위를 이해하는 것이 중요합니다. 지우기(erase)는 블록 단위, 쓰기(program)는 페이지(또는 워드) 단위, 읽기는 페이지(또는 바이트) 단위로 수행됩니다.

NAND Flash Erase Block 구조 Erase Block (128KB ~ 512KB) Page 0: Data (2048B ~ 16KB) OOB (64B) Page 1: Data (2048B ~ 16KB) OOB (64B) Page 2: Data (2048B ~ 16KB) OOB (64B) ... Page N-1: Data (2048B ~ 16KB) OOB (64B) 지우기: 블록 전체 (모든 비트를 1로) | 쓰기: 페이지 단위 (1 → 0만 가능) | OOB: ECC + 메타데이터 일반 NAND: 64 pages/block, 2KB/page | 대용량 NAND: 128~256 pages/block, 4~16KB/page

OOB(Out-Of-Band) 영역은 각 페이지에 부속된 추가 데이터 영역으로, 주로 ECC 데이터와 파일시스템 메타데이터를 저장합니다. 2KB 페이지 기준 보통 64바이트, 4KB 페이지 기준 128~256바이트입니다.

배드 블록 관리

NAND Flash에는 두 종류의 배드 블록이 존재합니다:

BBT(Bad Block Table): 커널 NAND 드라이버는 Flash의 마지막 몇 블록에 BBT를 저장합니다. 부팅 시 전체 Flash를 스캔하는 대신 BBT를 읽어 배드 블록 정보를 빠르게 로드합니다. BBT 관련 코드는 drivers/mtd/nand/bbt.c에 구현되어 있습니다.

SLC / MLC / TLC / QLC

NAND Flash 셀은 저장하는 비트 수에 따라 분류됩니다:

유형비트/셀상태 수내구성 (P/E 사이클)읽기 속도주요 용도
SLC1250,000 ~ 100,000가장 빠름임베디드, 산업용
MLC243,000 ~ 10,000빠름엔터프라이즈 SSD
TLC38500 ~ 3,000보통소비자 SSD
QLC416100 ~ 1,000느림대용량 저장

MTD 서브시스템에서 직접 다루는 Raw NAND는 대부분 SLC입니다. MLC 이상의 Raw NAND는 ECC 요구 사항이 크게 증가하며(40비트/1KB 이상), 프로그래밍 순서 제약(paired page)도 있어 드라이버 복잡도가 높아집니다.

MTD 아키텍처

MTD 서브시스템의 핵심은 struct mtd_info 구조체입니다. 모든 Flash 디바이스는 이 구조체를 통해 커널에 등록되고, 상위 레이어(파일시스템, UBI)는 이 인터페이스를 통해 Flash에 접근합니다.

struct mtd_info 핵심 필드

/* include/linux/mtd/mtd.h */
struct mtd_info {
    u_char type;              /* MTD_NORFLASH, MTD_NANDFLASH 등 */
    uint32_t flags;            /* MTD_WRITEABLE, MTD_BIT_WRITEABLE 등 */
    uint64_t size;             /* 전체 디바이스 크기 (바이트) */
    uint32_t erasesize;        /* 지우기 블록 크기 */
    uint32_t writesize;        /* 최소 쓰기 단위 (NOR: 1, NAND: 페이지 크기) */
    uint32_t writebufsize;     /* 쓰기 버퍼 크기 */
    uint32_t oobsize;          /* OOB 영역 크기 (페이지당) */
    uint32_t oobavail;         /* 사용 가능한 OOB 바이트 수 */

    unsigned int erasesize_shift;  /* log2(erasesize) */
    unsigned int writesize_shift;  /* log2(writesize) */

    const char *name;          /* 디바이스 이름 */
    int index;                 /* MTD 디바이스 번호 */

    /* ECC 레이아웃 */
    struct mtd_oob_region ooblayout;

    /* 연산 콜백 */
    int (*_erase)(struct mtd_info *mtd, struct erase_info *instr);
    int (*_read)(struct mtd_info *mtd, loff_t from, size_t len,
                 size_t *retlen, u_char *buf);
    int (*_write)(struct mtd_info *mtd, loff_t to, size_t len,
                  size_t *retlen, const u_char *buf);
    int (*_read_oob)(struct mtd_info *mtd, loff_t from,
                     struct mtd_oob_ops *ops);
    int (*_write_oob)(struct mtd_info *mtd, loff_t to,
                      struct mtd_oob_ops *ops);
    int (*_block_isbad)(struct mtd_info *mtd, loff_t ofs);
    int (*_block_markbad)(struct mtd_info *mtd, loff_t ofs);

    struct module *owner;
    struct device dev;
    int usecount;
};

MTD 계층 구조

MTD 서브시스템은 세 가지 주요 계층으로 구성됩니다:

MTD 디바이스 등록

Flash 드라이버는 struct mtd_info를 채운 뒤 mtd_device_register() 또는 mtd_device_parse_register()를 호출하여 MTD 코어에 등록합니다:

/* drivers/mtd/mtdcore.c */

/**
 * mtd_device_register - MTD 디바이스를 코어에 등록
 * @mtd: 등록할 mtd_info 구조체
 * @parts: 정적 파티션 배열 (NULL이면 파티션 없음)
 * @nr_parts: 파티션 개수
 */
int mtd_device_register(struct mtd_info *mtd,
                        const struct mtd_partition *parts,
                        int nr_parts)
{
    int ret;

    ret = add_mtd_device(mtd);  /* 마스터 디바이스 등록 */
    if (ret)
        return ret;

    if (parts && nr_parts > 0)
        ret = add_mtd_partitions(mtd, parts, nr_parts);

    return ret;
}

/**
 * mtd_device_parse_register - 파티션 파서를 사용하여 등록
 * 파서 우선순위: 커널 커맨드라인(cmdlinepart) > DT(ofpart) > 정적(fallback)
 */
int mtd_device_parse_register(struct mtd_info *mtd,
                              const char * const *part_probe_types,
                              struct mtd_part_parser_data *parser_data,
                              const struct mtd_partition *parts,
                              int nr_parts);

MTD 핵심 연산

상위 레이어는 mtd_info의 래퍼 함수를 통해 Flash에 접근합니다. 이 함수들은 내부적으로 드라이버가 등록한 콜백(_read, _write, _erase)을 호출합니다:

/* MTD 코어 래퍼 함수들 (include/linux/mtd/mtd.h) */

/* 읽기: from 오프셋에서 len 바이트를 buf로 읽기 */
int mtd_read(struct mtd_info *mtd, loff_t from,
             size_t len, size_t *retlen, u_char *buf);

/* 쓰기: to 오프셋에 len 바이트를 buf에서 쓰기 */
int mtd_write(struct mtd_info *mtd, loff_t to,
              size_t len, size_t *retlen, const u_char *buf);

/* 지우기 */
int mtd_erase(struct mtd_info *mtd, struct erase_info *instr);

/* OOB 읽기/쓰기 */
int mtd_read_oob(struct mtd_info *mtd, loff_t from,
                 struct mtd_oob_ops *ops);
int mtd_write_oob(struct mtd_info *mtd, loff_t to,
                  struct mtd_oob_ops *ops);

/* 배드 블록 확인/마킹 */
int mtd_block_isbad(struct mtd_info *mtd, loff_t ofs);
int mtd_block_markbad(struct mtd_info *mtd, loff_t ofs);

커널 모듈에서 MTD 디바이스에 접근하는 예제입니다:

#include <linux/mtd/mtd.h>

static int read_flash_data(void)
{
    struct mtd_info *mtd;
    size_t retlen;
    u_char buf[4096];
    int ret;

    /* MTD 디바이스 이름으로 검색 */
    mtd = get_mtd_device_nm("firmware");
    if (IS_ERR(mtd)) {
        pr_err("MTD device 'firmware' not found\n");
        return PTR_ERR(mtd);
    }

    /* 오프셋 0에서 4096바이트 읽기 */
    ret = mtd_read(mtd, 0, sizeof(buf), &retlen, buf);
    if (ret && ret != -EUCLEAN) {  /* EUCLEAN = ECC 보정 발생 */
        pr_err("MTD read failed: %d\n", ret);
        goto out;
    }

    pr_info("Read %zu bytes from flash\n", retlen);

out:
    put_mtd_device(mtd);  /* 참조 카운트 해제 */
    return ret;
}
EUCLEAN 반환값: mtd_read()-EUCLEAN을 반환하면 ECC가 비트 오류를 성공적으로 보정했음을 의미합니다. 데이터는 정상이지만, 해당 블록의 마모가 진행되고 있으므로 UBI와 같은 상위 레이어가 데이터를 다른 블록으로 이동(scrubbing)하는 트리거로 사용합니다.

MTD 파티션

하나의 물리 Flash 디바이스를 논리적으로 분할하여 부트로더, 커널, 루트파일시스템 등 영역을 구분합니다. MTD는 세 가지 파티션 정의 방식을 지원합니다.

정적 파티션 테이블

드라이버 코드에서 직접 파티션을 정의하는 방식입니다. 임베디드 보드의 보드 파일에서 자주 사용됩니다:

/* 정적 파티션 테이블 정의 */
static struct mtd_partition my_flash_parts[] = {
    {
        .name   = "bootloader",
        .offset = 0,
        .size   = 256 * 1024,           /* 256KB */
        .mask_flags = MTD_WRITEABLE,  /* 읽기 전용 */
    },
    {
        .name   = "kernel",
        .offset = MTDPART_OFS_APPEND,   /* 이전 파티션 바로 뒤 */
        .size   = 4 * 1024 * 1024,     /* 4MB */
    },
    {
        .name   = "rootfs",
        .offset = MTDPART_OFS_APPEND,
        .size   = 16 * 1024 * 1024,    /* 16MB */
    },
    {
        .name   = "data",
        .offset = MTDPART_OFS_APPEND,
        .size   = MTDPART_SIZ_FULL,     /* 나머지 전체 */
    },
};

/* 드라이버 probe에서 등록 */
mtd_device_register(mtd, my_flash_parts, ARRAY_SIZE(my_flash_parts));

커맨드라인 파티션 (cmdlinepart)

커널 부트 파라미터로 파티션을 동적으로 정의합니다. CONFIG_MTD_CMDLINE_PARTS가 활성화되어야 합니다:

/* 커널 커맨드라인 파티션 문법 */
mtdparts=<mtddef>[;<mtddef>...]

mtddef  := <mtd-id>:<partdef>[,<partdef>...]
partdef := <size>[@<offset>][(<name>)][ro]

/* 예제: 32MB Flash를 4개 파티션으로 분할 */
mtdparts=spi0.0:256k(bootloader)ro,4m(kernel),16m(rootfs),-(data)

/* 복수 Flash 디바이스 */
mtdparts=spi0.0:256k(boot)ro,-(firmware);nand0:4m(kernel),-(rootfs)
파티션 크기 단위: k (킬로바이트), m (메가바이트), g (기가바이트). -는 남은 공간 전체를 의미합니다. ro는 읽기 전용 플래그입니다.

Device Tree 파티션 (ofpart)

Device Tree를 사용하는 시스템에서 가장 일반적인 파티션 정의 방법입니다. CONFIG_MTD_OF_PARTS가 필요합니다:

/* Device Tree 파티션 정의 예제 */
flash@0 {
    compatible = "jedec,spi-nor";
    reg = <0>;
    spi-max-frequency = <50000000>;
    #address-cells = <1>;
    #size-cells = <1>;

    partitions {
        compatible = "fixed-partitions";
        #address-cells = <1>;
        #size-cells = <1>;

        bootloader@0 {
            label = "bootloader";
            reg = <0x000000 0x040000>;  /* 0 ~ 256KB */
            read-only;
        };
        kernel@40000 {
            label = "kernel";
            reg = <0x040000 0x400000>;  /* 256KB ~ 4.25MB */
        };
        rootfs@440000 {
            label = "rootfs";
            reg = <0x440000 0x1000000>; /* 4.25MB ~ 20.25MB */
        };
        data@1440000 {
            label = "data";
            reg = <0x1440000 0x0BC0000>; /* 나머지 */
        };
    };
};

파티션 파싱 흐름

mtd_device_parse_register()는 다음 순서로 파티션을 탐색합니다:

/* mtd_device_parse_register() 파티션 파싱 우선순위 */

1. part_probe_types 배열에 지정된 파서 순서대로 시도
   /* 일반적으로: {"cmdlinepart", "ofpart", NULL} */

2. "cmdlinepart" 파서:
   - 커널 커맨드라인에서 mtdparts= 파라미터 검색
   - mtd-id가 일치하면 파티션 생성
   - drivers/mtd/parsers/cmdlinepart.c

3. "ofpart" 파서:
   - Device Tree에서 "fixed-partitions" compatible 검색
   - 파티션 노드의 reg 속성으로 오프셋/크기 결정
   - drivers/mtd/parsers/ofpart_core.c

4. 정적 fallback:
   - 파서가 모두 실패하면 드라이버가 전달한 정적 파티션 사용
   - parts == NULL이면 파티션 없이 전체 디바이스를 단일 MTD로 등록

NOR Flash 드라이버

NOR Flash 드라이버는 두 가지 주요 프레임워크로 나뉩니다: 병렬 NOR을 위한 CFI(Common Flash Interface) 프레임워크와 SPI 버스 NOR을 위한 SPI-NOR 프레임워크입니다.

CFI 프레임워크

CFI는 병렬 버스로 연결된 NOR Flash의 자동 탐지 표준입니다. Flash 칩의 특정 주소에 쿼리 명령을 보내면 칩이 자신의 크기, 지우기 블록 구성, 전압 사양 등을 응답합니다:

/* CFI 프로브 과정 (drivers/mtd/chips/cfi_probe.c) */

/* 1. CFI 쿼리 모드 진입 */
cfi_send_gen_cmd(0x98, 0x55, base, map, cfi, ...);

/* 2. "QRY" 시그니처 확인 (오프셋 0x10) */
if (cfi_read_query(map, base + 0x10) == 'Q' &&
    cfi_read_query(map, base + 0x11) == 'R' &&
    cfi_read_query(map, base + 0x12) == 'Y')

/* 3. 커맨드셋 ID로 드라이버 선택 */
/*    0x0001: Intel/Sharp Extended */
/*    0x0002: AMD/Fujitsu Standard */
/*    0x0003: Intel Standard */

JEDEC 프로브

CFI를 지원하지 않는 구형 NOR Flash는 JEDEC 프로브를 사용합니다. 제조사 ID와 디바이스 ID를 읽어 하드코딩된 테이블에서 Flash 파라미터를 찾습니다:

/* drivers/mtd/chips/jedec_probe.c */
static const struct amd_flash_info jedec_table[] = {
    {
        .mfr_id   = CFI_MFR_AMD,
        .dev_id   = 0x227E,
        .name     = "AMD AM29LV320DB",
        .devtypes = CFI_DEVICETYPE_X16,
        .uaddr    = MTD_UADDR_0x0555_0x02AA,
        .dev_size = SIZE_4MiB,
        .cmd_set  = P_ID_AMD_STD,
        .nr_regions = 2,
        .regions  = {
            ERASEINFO(0x10000, 63),  /* 64KB x 63 */
            ERASEINFO(0x02000, 8),   /* 8KB x 8 */
        },
    },
    /* ... 더 많은 칩 정의 ... */
};

struct map_info

병렬 NOR Flash 드라이버는 struct map_info를 통해 Flash의 물리 주소와 접근 방법을 정의합니다:

/* include/linux/mtd/map.h */
struct map_info {
    const char *name;           /* 디바이스 이름 */
    unsigned long size;          /* Flash 윈도우 크기 */
    resource_size_t phys;        /* 물리 시작 주소 */
    void __iomem *virt;          /* ioremap된 가상 주소 */
    int bankwidth;               /* 버스 폭 (바이트): 1, 2, 4 */

    /* 커스텀 읽기/쓰기 함수 (NULL이면 기본 사용) */
    map_word (*read)(struct map_info *, unsigned long);
    void (*write)(struct map_info *, map_word, unsigned long);
    void (*copy_from)(struct map_info *, void *, unsigned long, ssize_t);
};

/* 병렬 NOR Flash platform driver 예제 */
static int my_nor_probe(struct platform_device *pdev)
{
    struct map_info *map;
    struct mtd_info *mtd;

    map = devm_kzalloc(&pdev->dev, sizeof(*map), GFP_KERNEL);
    map->name = "my-nor";
    map->phys = 0x08000000;         /* Flash 물리 주소 */
    map->size = 0x01000000;         /* 16MB */
    map->bankwidth = 2;             /* 16-bit 버스 */
    map->virt = devm_ioremap_resource(&pdev->dev, res);

    simple_map_init(map);

    /* CFI 프로브 시도, 실패 시 JEDEC */
    mtd = do_map_probe("cfi_probe", map);
    if (!mtd)
        mtd = do_map_probe("jedec_probe", map);

    if (!mtd)
        return -ENODEV;

    mtd->owner = THIS_MODULE;
    return mtd_device_parse_register(mtd, NULL, NULL,
                                      my_flash_parts,
                                      ARRAY_SIZE(my_flash_parts));
}

SPI-NOR 프레임워크

현대 임베디드 시스템에서 가장 많이 사용되는 Flash 유형입니다. drivers/mtd/spi-nor/에 구현된 SPI-NOR 프레임워크는 SFDP(Serial Flash Discoverable Parameters) 표준을 통해 Flash 파라미터를 자동 탐지합니다:

/* include/linux/mtd/spi-nor.h - 핵심 구조체 */
struct spi_nor {
    struct mtd_info mtd;           /* MTD 인터페이스 (내장) */
    struct mutex lock;              /* 동시 접근 보호 */
    struct spi_mem *spimem;        /* SPI MEM 디바이스 */
    const struct spi_nor_controller_ops *controller_ops;

    u8 addr_nbytes;                /* 주소 바이트 수 (3 or 4) */
    u8 erase_opcode;               /* 지우기 명령어 */
    u8 read_opcode;                /* 읽기 명령어 */
    u8 read_dummy;                 /* 읽기 더미 사이클 수 */
    u8 program_opcode;             /* 쓰기(프로그램) 명령어 */

    enum spi_nor_protocol read_proto;    /* 1-1-1, 1-1-4, 1-4-4 등 */
    enum spi_nor_protocol write_proto;
    struct spi_nor_flash_parameter *params; /* SFDP에서 파싱한 파라미터 */
};

/* SPI-NOR 드라이버 probe 흐름 */
static int spi_nor_probe(struct spi_mem *spimem)
{
    struct spi_nor *nor;

    nor = devm_kzalloc(dev, sizeof(*nor), GFP_KERNEL);
    nor->spimem = spimem;

    /* SFDP 테이블 읽기 + Flash ID 매칭 + 파라미터 설정 */
    ret = spi_nor_scan(nor, NULL, &hwcaps);

    /* MTD 디바이스 등록 (파티션 포함) */
    ret = mtd_device_parse_register(&nor->mtd, NULL, NULL,
                                      NULL, 0);
    return ret;
}
SFDP(Serial Flash Discoverable Parameters): JEDEC JESD216 표준으로, SPI Flash 칩이 자신의 파라미터(크기, 지우기 명령, 속도 등)를 표준화된 형식으로 제공합니다. spi_nor_scan()은 먼저 SFDP를 읽고, 추가 정보가 필요하면 Flash ID 테이블에서 보완합니다.

NAND Flash 드라이버

NAND Flash 드라이버 프레임워크는 drivers/mtd/nand/에 구현되어 있습니다. Raw NAND 컨트롤러 드라이버는 struct nand_chip을 설정하고 컨트롤러별 연산을 제공합니다.

struct nand_chip 핵심 필드

/* include/linux/mtd/rawnand.h */
struct nand_chip {
    struct nand_device base;       /* 기본 NAND 디바이스 */
    struct mtd_info mtd;            /* MTD 인터페이스 */

    /* 컨트롤러 연산 */
    struct nand_controller *controller;
    const struct nand_controller_ops *controller_ops;

    /* 칩 선택 */
    void (*select_chip)(struct nand_chip *chip, int cs);

    /* 명령/주소/데이터 전송 */
    void (*cmd_ctrl)(struct nand_chip *chip, int dat, unsigned int ctrl);
    int (*exec_op)(struct nand_chip *chip,
                   const struct nand_operation *op, bool check_only);

    /* ECC 설정 */
    struct nand_ecc_ctrl ecc;

    /* 옵션 */
    unsigned int options;          /* NAND_BUSWIDTH_16 등 */
    unsigned int bbt_options;      /* NAND_BBT_USE_FLASH 등 */

    /* BBT(Bad Block Table) */
    uint8_t *bbt;                  /* 메모리 내 BBT */
    struct nand_bbt_descr *bbt_td; /* 기본 BBT 디스크립터 */
    struct nand_bbt_descr *bbt_md; /* 미러 BBT 디스크립터 */

    /* 타이밍 */
    struct nand_data_interface *data_iface;
};

ECC 엔진

NAND Flash는 비트 오류가 발생할 수 있으므로 ECC(Error Correction Code)가 필수입니다. 커널은 여러 ECC 모드를 지원합니다:

ECC 모드구현 위치성능설명
NAND_ECC_ENGINE_TYPE_SW_HAMMING소프트웨어 (커널)느림1비트 보정/2비트 감지, SLC 전용
NAND_ECC_ENGINE_TYPE_SW_BCH소프트웨어 (커널)느림다중 비트 보정, BCH 알고리즘
NAND_ECC_ENGINE_TYPE_ON_HOSTNAND 컨트롤러 HW빠름컨트롤러 내장 ECC 엔진
NAND_ECC_ENGINE_TYPE_ON_DIENAND 칩 내장빠름칩 자체 ECC (주로 SPI-NAND)
NAND_ECC_ENGINE_TYPE_NONE없음-ECC 비활성 (테스트용)
/* ECC 설정 예제 (드라이버 probe에서) */
struct nand_chip *chip = &nand->chip;

/* 하드웨어 ECC 사용 (컨트롤러 내장) */
chip->ecc.engine_type = NAND_ECC_ENGINE_TYPE_ON_HOST;
chip->ecc.algo = NAND_ECC_ALGO_BCH;
chip->ecc.strength = 8;        /* 8비트 보정 능력 */
chip->ecc.bytes = 13;           /* ECC 바이트 수 (512B 데이터당) */
chip->ecc.size = 512;           /* ECC 스텝 크기 */

/* 소프트웨어 ECC 사용 (BCH) */
chip->ecc.engine_type = NAND_ECC_ENGINE_TYPE_SW_BCH;
chip->ecc.algo = NAND_ECC_ALGO_BCH;
chip->ecc.strength = 4;
chip->ecc.size = 512;

BBT (Bad Block Table) 관리

BBT는 배드 블록 정보를 효율적으로 관리하기 위한 메커니즘입니다. 부팅 시 Flash 전체를 스캔하는 대신 Flash에 저장된 테이블을 읽습니다:

/* drivers/mtd/nand/bbt.c - BBT 구조 */

/* BBT 엔트리: 블록당 2비트 */
/* 00b = 정상 블록 */
/* 01b = 마모된 블록 (wear out) */
/* 10b = 예약 블록 */
/* 11b = 배드 블록 */

/* BBT 디스크립터 */
struct nand_bbt_descr {
    int options;           /* NAND_BBT_LASTBLOCK 등 */
    int pages[NAND_MAX_CHIPS];  /* BBT가 저장된 페이지 */
    int offs;              /* 패턴 오프셋 */
    int veroffs;           /* 버전 오프셋 */
    uint8_t version[NAND_MAX_CHIPS];  /* BBT 버전 */
    int len;               /* 패턴 길이 */
    int maxblocks;         /* 검색할 최대 블록 수 */
    uint8_t *pattern;      /* 식별 패턴 ("Bbt0", "1tbB") */
};

/* BBT 옵션 플래그 */
#define NAND_BBT_USE_FLASH     0x00020000  /* Flash에 BBT 저장 */
#define NAND_BBT_NO_OOB       0x00040000  /* BBT를 데이터 영역에 저장 */
#define NAND_BBT_LASTBLOCK    0x00000010  /* 마지막 블록부터 검색 */

Raw NAND 컨트롤러 드라이버 골격

새로운 NAND 컨트롤러 드라이버를 작성하는 기본 구조입니다. 현대 커널은 exec_op() 인터페이스를 사용합니다:

#include <linux/mtd/rawnand.h>
#include <linux/platform_device.h>

struct my_nand_ctrl {
    struct nand_controller controller;
    struct nand_chip chip;
    void __iomem *regs;
    struct clk *clk;
};

/* exec_op: NAND 명령 시퀀스 실행 */
static int my_nand_exec_op(struct nand_chip *chip,
                            const struct nand_operation *op,
                            bool check_only)
{
    struct my_nand_ctrl *ctrl = nand_get_controller_data(chip);
    const struct nand_op_instr *instr;
    unsigned int i;

    if (check_only)
        return 0;  /* 연산 지원 여부만 확인 */

    for (i = 0; i < op->ninstrs; i++) {
        instr = &op->instrs[i];
        switch (instr->type) {
        case NAND_OP_CMD_INSTR:
            writel(instr->ctx.cmd.opcode, ctrl->regs + CMD_REG);
            break;
        case NAND_OP_ADDR_INSTR:
            for (int j = 0; j < instr->ctx.addr.naddrs; j++)
                writel(instr->ctx.addr.addrs[j], ctrl->regs + ADDR_REG);
            break;
        case NAND_OP_DATA_IN_INSTR:
            my_nand_read_buf(ctrl, instr->ctx.data.buf.in,
                             instr->ctx.data.len);
            break;
        case NAND_OP_DATA_OUT_INSTR:
            my_nand_write_buf(ctrl, instr->ctx.data.buf.out,
                              instr->ctx.data.len);
            break;
        case NAND_OP_WAITRDY_INSTR:
            my_nand_wait_ready(ctrl, instr->ctx.waitrdy.timeout_ms);
            break;
        }
    }
    return 0;
}

static const struct nand_controller_ops my_nand_ops = {
    .exec_op = my_nand_exec_op,
};

static int my_nand_probe(struct platform_device *pdev)
{
    struct my_nand_ctrl *ctrl;
    struct nand_chip *chip;
    struct mtd_info *mtd;
    int ret;

    ctrl = devm_kzalloc(&pdev->dev, sizeof(*ctrl), GFP_KERNEL);
    ctrl->regs = devm_platform_ioremap_resource(pdev, 0);

    chip = &ctrl->chip;
    mtd = nand_to_mtd(chip);
    mtd->dev.parent = &pdev->dev;

    /* 컨트롤러 연산 설정 */
    nand_controller_init(&ctrl->controller);
    ctrl->controller.ops = &my_nand_ops;
    chip->controller = &ctrl->controller;

    /* ECC 설정 */
    chip->ecc.engine_type = NAND_ECC_ENGINE_TYPE_ON_HOST;
    chip->ecc.algo = NAND_ECC_ALGO_BCH;
    chip->ecc.strength = 8;
    chip->ecc.size = 512;

    /* BBT를 Flash에 저장 */
    chip->bbt_options = NAND_BBT_USE_FLASH | NAND_BBT_NO_OOB;

    /* NAND 칩 스캔 (ID 읽기, 파라미터 설정) */
    ret = nand_scan(chip, 1);  /* maxchips = 1 */
    if (ret)
        return ret;

    /* MTD 디바이스 등록 */
    ret = mtd_device_parse_register(mtd, NULL, NULL, NULL, 0);
    if (ret) {
        nand_cleanup(chip);
        return ret;
    }

    platform_set_drvdata(pdev, ctrl);
    return 0;
}

static void my_nand_remove(struct platform_device *pdev)
{
    struct my_nand_ctrl *ctrl = platform_get_drvdata(pdev);
    struct nand_chip *chip = &ctrl->chip;
    struct mtd_info *mtd = nand_to_mtd(chip);
    int ret;

    ret = mtd_device_unregister(mtd);
    WARN_ON(ret);
    nand_cleanup(chip);
}

static const struct of_device_id my_nand_ids[] = {
    { .compatible = "vendor,my-nand-controller" },
    { }
};
MODULE_DEVICE_TABLE(of, my_nand_ids);

static struct platform_driver my_nand_driver = {
    .probe  = my_nand_probe,
    .remove = my_nand_remove,
    .driver = {
        .name = "my-nand",
        .of_match_table = my_nand_ids,
    },
};
module_platform_driver(my_nand_driver);
exec_op() vs 레거시 인터페이스: 커널 4.16+에서 exec_op()이 도입되어 기존 cmd_ctrl(), cmdfunc(), read_byte() 등의 레거시 콜백을 대체합니다. 새 드라이버는 반드시 exec_op() 인터페이스를 사용해야 합니다. 레거시 인터페이스는 유지보수 모드이며, 향후 커널에서 제거될 수 있습니다.

SPI NAND

SPI NAND는 SPI 버스를 통해 접근하는 NAND Flash입니다. 기존 병렬 NAND 대비 핀 수가 적어 소형 임베디드 시스템에서 널리 사용됩니다. 커널의 SPI NAND 프레임워크는 drivers/mtd/spi/에 구현되어 있습니다.

struct spinand_device

/* include/linux/mtd/spinand.h */
struct spinand_device {
    struct nand_device base;        /* 기본 NAND 디바이스 (내장) */
    struct spi_mem *spimem;         /* SPI MEM 디바이스 */

    struct mutex lock;               /* 동시 접근 보호 */
    struct spinand_id id;            /* 디바이스 ID */

    const struct spinand_manufacturer *manufacturer;
    u8 *databuf;                    /* 페이지 데이터 버퍼 */
    u8 *oobbuf;                     /* OOB 버퍼 */
    u8 *scratchbuf;                 /* 임시 버퍼 */

    /* 칩 내장 ECC (On-Die ECC) */
    bool oobbuf_is_databuf;
    const struct spinand_ecc_info *eccinfo;
    u8 eccstatus;
};

SPI NAND vs SPI NOR 비교

특성SPI NORSPI NAND
용량 범위512KB ~ 256MB128MB ~ 4GB
페이지 크기256B (프로그램 단위)2KB ~ 4KB
지우기 블록4KB ~ 256KB (섹터/블록)128KB ~ 256KB
읽기 방식연속 바이트 읽기페이지 → 캐시 → SPI 전송
쓰기 속도느림 (바이트/워드 단위)빠름 (페이지 단위)
ECC불필요필수 (대부분 On-Die ECC)
배드 블록없음존재 가능
XIP가능 (일부 컨트롤러)불가
비용 (용량당)높음낮음
커널 프레임워크drivers/mtd/spi-nor/drivers/mtd/spi/
주요 용도부트로더, 소용량 펌웨어루트FS, 대용량 데이터

SPI NAND 드라이버 구조

SPI NAND 프레임워크는 SPI MEM 레이어 위에서 동작합니다. SPI MEM은 SPI 컨트롤러가 메모리 매핑이나 DMA 전송 등 최적화된 경로를 사용할 수 있게 해주는 추상화 레이어입니다:

/* SPI NAND 칩 제조사 정의 예제 (drivers/mtd/spi/) */

/* 칩 파라미터 정의 */
static SPINAND_OP_VARIANTS(read_cache_variants,
    SPINAND_PAGE_READ_FROM_CACHE_QUADIO_OP(0, 2, NULL, 0),
    SPINAND_PAGE_READ_FROM_CACHE_X4_OP(0, 1, NULL, 0),
    SPINAND_PAGE_READ_FROM_CACHE_DUALIO_OP(0, 1, NULL, 0),
    SPINAND_PAGE_READ_FROM_CACHE_X2_OP(0, 1, NULL, 0),
    SPINAND_PAGE_READ_FROM_CACHE_OP(true, 0, 1, NULL, 0));

static SPINAND_OP_VARIANTS(write_cache_variants,
    SPINAND_PROG_LOAD_X4(true, 0, NULL, 0),
    SPINAND_PROG_LOAD(true, 0, NULL, 0));

static SPINAND_OP_VARIANTS(update_cache_variants,
    SPINAND_PROG_LOAD_X4(false, 0, NULL, 0),
    SPINAND_PROG_LOAD(false, 0, NULL, 0));

/* 칩 정보 테이블 */
static const struct spinand_info winbond_spinand_table[] = {
    SPINAND_INFO("W25N01GV",
                 SPINAND_ID(SPINAND_READID_METHOD_OPCODE_DUMMY,
                            0xEF, 0xAA, 0x21),
                 NAND_MEMORG(1, 2048, 64, 64, 1024, 20, 1, 1, 1),
                 NAND_ECCREQ(1, 512),
                 SPINAND_INFO_OP_VARIANTS(&read_cache_variants,
                                         &write_cache_variants,
                                         &update_cache_variants),
                 0,
                 SPINAND_ECCINFO(&w25n_ooblayout, w25n_ecc_get_status)),
};

/* 제조사 등록 */
static const struct spinand_manufacturer winbond_spinand_manufacturer = {
    .id = SPINAND_MFR_WINBOND,
    .name = "Winbond",
    .chips = winbond_spinand_table,
    .nchips = ARRAY_SIZE(winbond_spinand_table),
    .ops = &winbond_spinand_manuf_ops,
};

SPI NAND의 읽기/쓰기 연산은 두 단계로 수행됩니다:

SPI MEM 레이어: struct spi_meminclude/linux/spi/spi-mem.h에 정의되어 있으며, SPI 컨트롤러가 메모리 디바이스에 최적화된 전송 경로(DMA, 메모리 매핑 등)를 제공할 수 있게 합니다. SPI-NOR과 SPI-NAND 모두 이 레이어를 통해 SPI 컨트롤러와 통신합니다.

UBI (Unsorted Block Images)

UBI 개념과 목적

UBI(Unsorted Block Images)는 MTD 디바이스 위에서 동작하는 볼륨 관리 레이어입니다. 원시 Flash의 복잡성(wear leveling, bad block 관리)을 추상화하여 상위 파일시스템이나 애플리케이션에게 논리적 볼륨을 제공합니다. UBI의 핵심 역할은 다음과 같습니다:

MTD Device (Physical Erase Blocks: PEB0, PEB1, PEB2, ... PEBn) Raw NAND / SPI-NOR Flash UBI Layer Wear Leveling | Bad Block 관리 | PEB ↔ LEB 매핑 UBI Volume 0 LEB0, LEB1, LEB2 ... (UBIFS) UBI Volume 1 LEB0, LEB1 ... (Static Data) UBI Volume 2 LEB0 ... (App Data)

UBI 내부 구조: PEB vs LEB

UBI의 핵심 개념은 물리적 erase block(PEB)과 논리적 erase block(LEB) 간의 매핑입니다. 각 PEB의 시작부에는 두 개의 UBI 헤더가 기록됩니다:

/* include/uapi/mtd/ubi-user.h 기반 UBI 헤더 구조 */

/* EC Header - 각 PEB의 첫 번째 헤더 */
struct ubi_ec_hdr {
    __be32 magic;          /* UBI_EC_HDR_MAGIC = 0x55424923 ("UBI#") */
    __u8   version;        /* UBI 구현 버전 */
    __be64 ec;             /* erase counter 값 */
    __be32 vid_hdr_offset; /* VID header 오프셋 */
    __be32 data_offset;    /* 데이터 시작 오프셋 */
    __be32 image_seq;      /* UBI 이미지 시퀀스 번호 */
    __be32 hdr_crc;        /* 헤더 CRC32 */
};

/* VID Header - 각 PEB의 두 번째 헤더 */
struct ubi_vid_hdr {
    __be32 magic;      /* UBI_VID_HDR_MAGIC = 0x55424921 ("UBI!") */
    __u8   version;    /* UBI 구현 버전 */
    __u8   vol_type;   /* UBI_VID_DYNAMIC 또는 UBI_VID_STATIC */
    __u8   copy_flag;  /* 원자적 LEB 변경 시 사용 */
    __u8   compat;     /* 호환성 플래그 */
    __be32 vol_id;     /* 볼륨 ID */
    __be32 lnum;       /* 논리적 erase block 번호 */
    __be32 data_size;  /* static 볼륨의 데이터 크기 */
    __be32 used_ebs;   /* static 볼륨의 사용된 LEB 수 */
    __be32 data_crc;   /* 데이터 CRC32 (static 볼륨) */
    __be32 sqnum;      /* 시퀀스 번호 */
    __be32 hdr_crc;    /* 헤더 CRC32 */
};

PEB 내 데이터 배치는 다음과 같습니다: [EC Header | VID Header | Data (LEB)]. NOR Flash의 경우 EC와 VID 헤더가 같은 서브페이지에 위치할 수 있지만, NAND Flash에서는 일반적으로 별도의 서브페이지 또는 별도의 min-I/O 단위에 배치됩니다.

UBI Attach 프로세스

UBI가 MTD 디바이스에 attach될 때, 모든 PEB를 스캔하여 EC/VID 헤더를 읽고 PEB-to-LEB 매핑 테이블을 메모리에 구축합니다. 이 과정은 대용량 Flash에서 시간이 오래 걸릴 수 있습니다:

/* drivers/mtd/ubi/attach.c - UBI attach 프로세스 핵심 */

static int scan_peb(struct ubi_device *ubi,
                    struct ubi_attach_info *ai,
                    int pnum, bool fast)
{
    struct ubi_ec_hdr *ech = ai->ech;
    struct ubi_vid_hdr *vidh = ai->vidh;
    int err;

    /* 1단계: EC 헤더 읽기 */
    err = ubi_io_read_ec_hdr(ubi, pnum, ech, 0);
    if (err == UBI_IO_FF) {
        /* 빈 PEB - free 목록에 추가 */
        return add_to_free(ai, pnum, ech);
    }

    /* 2단계: VID 헤더 읽기 */
    err = ubi_io_read_vid_hdr(ubi, pnum, vidh, 0);
    if (err == UBI_IO_FF) {
        /* EC 있지만 VID 없음 - free로 처리 */
        return add_to_free(ai, pnum, ech);
    }

    /* 3단계: 볼륨/LEB 매핑 테이블 구축 */
    return ubi_add_to_av(ubi, ai, pnum, ech, vidh);
}

Fastmap은 이 스캔 시간을 획기적으로 줄이기 위한 기능입니다. UBI가 매핑 정보의 스냅샷을 특정 PEB에 저장하고, 다음 attach 시 전체 스캔 대신 이 스냅샷을 로드합니다. CONFIG_MTD_UBI_FASTMAP으로 활성화하며, UBI 모듈 파라미터 fm_autoconvert=1로 자동 변환할 수 있습니다.

정적 볼륨과 동적 볼륨

UBI는 두 가지 유형의 볼륨을 지원합니다:

항목동적 볼륨 (Dynamic)정적 볼륨 (Static)
용도파일시스템 (UBIFS 등)커널 이미지, 부트로더 등 읽기 전용 데이터
CRC 보호없음 (파일시스템이 자체 관리)전체 데이터에 대한 CRC32 검증
크기 변경ubi_resize_volume()으로 가능ubi_resize_volume()으로 가능
부분 쓰기가능 (LEB 단위)전체 볼륨 업데이트만 가능
VID 헤더 vol_typeUBI_VID_DYNAMICUBI_VID_STATIC

UBI 커널 API

커널 내부에서 UBI 볼륨에 접근하는 주요 API입니다 (include/linux/mtd/ubi.h):

#include <linux/mtd/ubi.h>

/* 볼륨 열기/닫기 */
struct ubi_volume_desc *ubi_open_volume(int ubi_num, int vol_id, int mode);
struct ubi_volume_desc *ubi_open_volume_nm(int ubi_num,
                                             const char *name, int mode);
void ubi_close_volume(struct ubi_volume_desc *desc);

/* LEB 읽기/쓰기/소거 */
int ubi_leb_read(struct ubi_volume_desc *desc,
                int lnum, char *buf, int offset, int len, int check);
int ubi_leb_write(struct ubi_volume_desc *desc,
                 int lnum, const void *buf, int offset, int len);
int ubi_leb_change(struct ubi_volume_desc *desc,
                  int lnum, const void *buf, int len);
int ubi_leb_erase(struct ubi_volume_desc *desc, int lnum);

/* LEB 매핑 상태 확인 */
int ubi_is_mapped(struct ubi_volume_desc *desc, int lnum);

/* 사용 예시 */
static int read_ubi_data(void)
{
    struct ubi_volume_desc *desc;
    char *buf;
    int err;

    /* UBI 디바이스 0, 볼륨 0을 읽기 모드로 열기 */
    desc = ubi_open_volume(0, 0, UBI_READONLY);
    if (IS_ERR(desc))
        return PTR_ERR(desc);

    buf = kmalloc(desc->vol->leb_size, GFP_KERNEL);
    if (!buf) {
        ubi_close_volume(desc);
        return -ENOMEM;
    }

    /* LEB 0 전체 읽기 */
    err = ubi_leb_read(desc, 0, buf, 0, desc->vol->leb_size, 0);
    if (err)
        pr_err("UBI read failed: %d\n", err);

    kfree(buf);
    ubi_close_volume(desc);
    return err;
}

Wear leveling 알고리즘은 erase counter 차이가 임계값(UBI_WL_THRESHOLD, 기본값 4096)을 초과하면 가장 많이 소거된 PEB의 데이터를 가장 적게 소거된 PEB로 복사하여 균형을 맞춥니다. 이 과정은 ubi_wl_worker()가 백그라운드에서 수행합니다.

UBI 오버헤드: UBI는 내부 관리를 위해 상당한 PEB를 소비합니다. 각 PEB에서 EC/VID 헤더가 2개의 min-I/O 페이지를 차지하며, 볼륨 테이블용 2개 PEB, wear leveling 예비 1개 PEB, 불량 블록 예비 PEB(전체의 약 2%), 그리고 Fastmap 사용 시 추가 PEB가 필요합니다. 128MiB NAND(128KiB erase block)에서 실제 사용 가능한 공간은 약 90-95% 수준입니다.

UBIFS (UBI File System)

UBIFS 설계 원칙

UBIFS는 UBI 볼륨 위에서 동작하도록 설계된 Flash 전용 파일시스템입니다. 전통적인 블록 디바이스 파일시스템과 달리, UBI의 LEB 추상화를 직접 활용하며 다음과 같은 특징을 가집니다:

Wandering Tree와 TNC

UBIFS의 인덱스 구조는 wandering tree라 불립니다. B+ 트리 형태의 인덱스 노드들이 Flash의 고정된 위치가 아닌, 매번 새로운 위치에 기록되기 때문입니다. 이 방식은 Flash의 in-place 업데이트 불가 특성에 자연스럽게 맞습니다.

TNC(Tree Node Cache)는 wandering tree의 인메모리 표현입니다. 마운트 시 인덱스 영역의 루트 노드부터 트리를 읽어들이며, 실제로는 필요한 부분만 lazy하게 로드됩니다:

/* fs/ubifs/ubifs.h - TNC 관련 주요 구조체 */

/* 인메모리 인덱스 노드 */
struct ubifs_znode {
    struct ubifs_znode *parent;  /* 부모 znode */
    struct ubifs_zbranch *zbranch; /* 자식 브랜치 배열 */
    int level;                    /* 트리 레벨 (0 = leaf) */
    int child_cnt;                /* 자식 수 */
    int iip;                      /* 부모에서의 인덱스 */
    int lnum;                     /* 이 znode가 저장된 LEB 번호 */
    int offs;                     /* LEB 내 오프셋 */
};

/* TNC에서 키를 기반으로 데이터 노드 검색 */
int ubifs_tnc_lookup(struct ubifs_info *c,
                     const union ubifs_key *key,
                     void *node);
int ubifs_tnc_locate(struct ubifs_info *c,
                     const union ubifs_key *key,
                     void *node, int *lnum, int *offs);

저널 (Journal)

UBIFS는 저널을 통해 데이터 일관성을 보장합니다. 저널은 하나 이상의 bud LEB으로 구성되며, 새로운 데이터와 인덱스 변경 사항이 순차적으로 기록됩니다:

commit은 저널이 가득 차거나, 명시적 sync 호출 시, 또는 백그라운드 커밋 타이머에 의해 트리거됩니다.

압축

UBIFS는 데이터 노드 단위로 압축을 수행합니다. 마운트 시 기본 압축 알고리즘을 지정할 수 있으며, 파일별로 ioctl을 통해 변경할 수도 있습니다:

/* 마운트 옵션으로 압축 지정 */
mount -t ubifs -o compr=lzo ubi0:rootfs /mnt     # LZO (기본, 빠름)
mount -t ubifs -o compr=zlib ubi0:rootfs /mnt    # ZLIB (높은 압축률)
mount -t ubifs -o compr=zstd ubi0:rootfs /mnt    # ZSTD (균형 잡힌 성능)
mount -t ubifs -o compr=none ubi0:rootfs /mnt    # 압축 비활성화

UBIFS vs ext4 vs JFFS2 비교

항목UBIFSext4 (블록 디바이스)JFFS2
대상 미디어Raw Flash (UBI 위)블록 디바이스 (eMMC, SSD 등)Raw Flash (MTD 직접)
마운트 시간빠름 (인덱스 로드)빠름 (슈퍼블록 기반)느림 (전체 스캔)
메모리 사용량중간 (TNC 캐시)낮음높음 (전체 inode 맵)
확장성수 GiB까지 양호수 TiB 이상수백 MiB까지
Wear LevelingUBI가 처리FTL이 처리자체 GC로 간접 처리
압축LZO, ZLIB, ZSTD없음 (별도 도구)zlib, rtime, lzo
쓰기 방식Out-of-place (wandering tree)Journaling (in-place)Log-structured
전원 차단 안전성우수 (저널 + UBI atomic LEB)우수 (저널링)양호 (log-structured)

마운트 및 설정

# UBIFS 마운트 절차

# 1. UBI를 MTD 디바이스에 attach
ubiattach /dev/ubi_ctrl -m 3                  # MTD 파티션 3에 UBI attach

# 2. UBI 볼륨 생성 (최초 1회)
ubimkvol /dev/ubi0 -N rootfs -m                  # 최대 크기로 'rootfs' 볼륨 생성

# 3. UBIFS 마운트
mount -t ubifs ubi0:rootfs /mnt

# 커널 설정 옵션
# CONFIG_UBIFS_FS=y
# CONFIG_UBIFS_FS_ADVANCED_COMPR=y
# CONFIG_UBIFS_FS_LZO=y
# CONFIG_UBIFS_FS_ZLIB=y
# CONFIG_UBIFS_FS_ZSTD=y
# CONFIG_UBIFS_FS_AUTHENTICATION=y   # UBIFS 인증 지원
UBIFS 인증: 커널 5.0부터 UBIFS는 HMAC 기반 인증을 지원합니다. CONFIG_UBIFS_FS_AUTHENTICATION을 활성화하면 데이터와 메타데이터의 무결성을 암호학적으로 검증할 수 있어, 임베디드 보안 요구사항을 충족합니다. 마운트 시 auth_key=auth_hash_name= 옵션으로 설정합니다.

JFFS2 (Journalling Flash File System v2)

로그 구조 설계

JFFS2는 MTD 디바이스 위에서 직접 동작하는 로그 구조 파일시스템입니다. UBI 없이 Flash를 직접 관리하며, 모든 데이터를 순차적으로 기록합니다. JFFS2의 핵심 노드 타입은 다음과 같습니다:

/* include/uapi/linux/jffs2.h - JFFS2 노드 구조 */

/* 공통 노드 헤더 */
struct jffs2_unknown_node {
    __u16 magic;      /* JFFS2_MAGIC_BITMASK = 0x1985 */
    __u16 nodetype;   /* 노드 타입 */
    __u32 totlen;     /* 전체 노드 길이 */
    __u32 hdr_crc;    /* 헤더 CRC */
};

/* Dirent 노드 - 디렉터리 엔트리 */
struct jffs2_raw_dirent {
    __u16 magic;
    __u16 nodetype;   /* JFFS2_NODETYPE_DIRENT */
    __u32 totlen;
    __u32 hdr_crc;
    __u32 pino;       /* 부모 inode 번호 */
    __u32 version;    /* dirent 버전 */
    __u32 ino;        /* 대상 inode 번호 */
    __u32 mctime;     /* 수정/생성 시간 */
    __u8  nsize;      /* 이름 길이 */
    __u8  type;       /* 파일 타입 (DT_REG 등) */
    __u32 node_crc;
    __u32 name_crc;
    __u8  name[];     /* 파일/디렉터리 이름 */
};

/* Inode 노드 - 파일 데이터와 메타데이터 */
struct jffs2_raw_inode {
    __u16 magic;
    __u16 nodetype;   /* JFFS2_NODETYPE_INODE */
    __u32 totlen;
    __u32 hdr_crc;
    __u32 ino;        /* inode 번호 */
    __u32 version;    /* inode 버전 */
    __u32 mode;       /* 파일 모드 */
    __u32 offset;     /* 데이터 오프셋 */
    __u32 csize;      /* 압축 후 데이터 크기 */
    __u32 dsize;      /* 압축 전 데이터 크기 */
    __u8  compr;      /* 압축 알고리즘 */
    __u32 data_crc;
    __u32 node_crc;
    __u8  data[];     /* 압축된 데이터 */
};

Cleanmarker는 완전히 소거된 erase block의 시작에 기록되는 특수 노드로, 해당 블록이 정상적으로 소거되었음을 표시합니다. NAND Flash에서는 OOB 영역에 저장됩니다.

가비지 컬렉션 (GC)

JFFS2의 GC 스레드(jffs2_garbage_collect_thread())는 백그라운드에서 동작하며, 유효하지 않은(obsolete) 노드가 포함된 erase block을 회수합니다:

/* fs/jffs2/background.c - GC 스레드 */

static int jffs2_garbage_collect_thread(void *_c)
{
    struct jffs2_sb_info *c = _c;

    while (!kthread_should_stop()) {
        /* dirty 블록 비율이 임계값 초과 시 GC 수행 */
        if (jffs2_garbage_collect_pass(c) == -ENOSPC) {
            pr_warn("jffs2: No space for GC\n");
        }
        schedule_timeout_interruptible(5 * HZ);
    }
    return 0;
}

/* GC 한 패스: 가장 dirty한 블록 선택 → 유효 노드 이동 → 블록 소거 */
int jffs2_garbage_collect_pass(struct jffs2_sb_info *c);

GC는 erase block을 세 가지 목록으로 관리합니다:

JFFS2 압축

JFFS2는 다음 압축 알고리즘을 지원합니다:

압축 모드는 CONFIG_JFFS2_CMODE_NONE, CONFIG_JFFS2_CMODE_PRIORITY, CONFIG_JFFS2_CMODE_SIZE, CONFIG_JFFS2_CMODE_FAVOURLZO로 선택합니다. PRIORITY 모드는 모든 알고리즘을 시도하여 가장 작은 결과를 사용합니다.

JFFS2의 한계와 선택 기준

JFFS2 확장성 한계: JFFS2는 마운트 시 Flash 전체를 스캔하여 인메모리 inode 맵을 구축합니다. 이로 인해 (1) 마운트 시간이 Flash 크기에 비례하여 증가하고, (2) 모든 inode 메타데이터가 RAM에 상주하므로 대용량 Flash에서 메모리 소비가 과도해집니다. 일반적으로 128MiB 이상의 Flash에는 UBIFS를 권장합니다.

JFFS2를 선택해야 하는 경우:

UBIFS를 선택해야 하는 경우:

유저스페이스 인터페이스

MTD 디바이스 노드

MTD 서브시스템은 두 종류의 디바이스 노드를 유저스페이스에 제공합니다:

주요 MTD ioctl 명령:

#include <mtd/mtd-user.h>
#include <sys/ioctl.h>

int fd = open("/dev/mtd0", O_RDWR);

/* MTD 정보 조회 */
struct mtd_info_user info;
ioctl(fd, MEMGETINFO, &info);
printf("Type: %u, Size: %u, Erase size: %u\n",
       info.type, info.size, info.erasesize);

/* Erase block 소거 */
struct erase_info_user erase;
erase.start = 0;
erase.length = info.erasesize;
ioctl(fd, MEMERASE, &erase);

/* Bad block 확인 */
loff_t offset = 0;
int ret = ioctl(fd, MEMGETBADBLOCK, &offset);
/* ret > 0: bad block, ret == 0: good block */

/* OOB 데이터 읽기 */
struct mtd_oob_buf oob;
oob.start = 0;
oob.length = info.oobsize;
oob.ptr = buf;
ioctl(fd, MEMREADOOB, &oob);

mtd-utils 도구

mtd-utils 패키지는 MTD 디바이스를 관리하기 위한 유저스페이스 유틸리티 모음입니다:

# MTD 디바이스 정보 조회
mtdinfo /dev/mtd0
mtdinfo --all                          # 모든 MTD 디바이스 요약

# Flash 소거
flash_erase /dev/mtd0 0 0              # 전체 소거 (offset=0, count=0=전체)
flash_erase /dev/mtd0 0x20000 4        # offset 0x20000부터 4개 블록 소거
flash_erase -j /dev/mtd0 0 0           # JFFS2 cleanmarker 포함 소거

# Flash에 이미지 쓰기
flashcp firmware.bin /dev/mtd0          # NOR Flash에 이미지 복사
flashcp -v firmware.bin /dev/mtd0       # 검증 포함

# NAND 전용 유틸리티
nanddump /dev/mtd0 -f dump.bin         # NAND 내용 덤프 (bad block 건너뜀)
nanddump -o /dev/mtd0 -f dump_oob.bin  # OOB 포함 덤프
nandwrite -p /dev/mtd0 image.bin       # NAND에 이미지 쓰기 (패딩 포함)
nandtest /dev/mtd0                     # NAND 읽기/쓰기/소거 테스트

UBI 유틸리티

UBI 디바이스와 볼륨을 관리하는 유틸리티입니다:

# UBI 포맷 (MTD 디바이스에 UBI 초기화)
ubiformat /dev/mtd3                    # MTD3을 UBI용으로 포맷
ubiformat /dev/mtd3 -f ubi.img         # 포맷하면서 UBI 이미지 플래싱
ubiformat /dev/mtd3 -s 2048           # sub-page 크기 지정

# UBI attach/detach
ubiattach /dev/ubi_ctrl -m 3           # MTD3에 UBI attach → /dev/ubi0 생성
ubiattach /dev/ubi_ctrl -m 3 -d 1     # UBI 디바이스 번호 지정 → /dev/ubi1
ubidetach /dev/ubi_ctrl -m 3           # UBI detach

# UBI 볼륨 관리
ubimkvol /dev/ubi0 -N rootfs -s 100MiB    # 100MiB 동적 볼륨 생성
ubimkvol /dev/ubi0 -N kernel -s 10MiB -t static  # 정적 볼륨
ubimkvol /dev/ubi0 -N data -m              # 최대 크기 볼륨
ubirmvol /dev/ubi0 -N rootfs               # 볼륨 삭제

# 정적 볼륨 업데이트
ubiupdatevol /dev/ubi0_1 kernel.img    # 정적 볼륨에 이미지 쓰기

# UBI 정보 조회
ubinfo /dev/ubi0                       # UBI 디바이스 정보
ubinfo /dev/ubi0 -a                    # 모든 볼륨 정보
ubinfo /dev/ubi0 -N rootfs             # 이름으로 볼륨 조회

Device Tree 바인딩

고정 파티션 정의

Flash 메모리의 파티션 레이아웃은 Device Tree에서 fixed-partitions compatible을 사용하여 정의합니다:

/* 표준 파티션 정의 예시 */
flash@0 {
    compatible = "jedec,spi-nor";
    reg = <0>;
    spi-max-frequency = <50000000>;

    partitions {
        compatible = "fixed-partitions";
        #address-cells = <1>;
        #size-cells = <1>;

        bootloader@0 {
            label = "u-boot";
            reg = <0x000000 0x080000>;    /* 0 ~ 512KiB */
            read-only;
        };

        env@80000 {
            label = "u-boot-env";
            reg = <0x080000 0x010000>;    /* 512KiB ~ 576KiB */
        };

        kernel@90000 {
            label = "kernel";
            reg = <0x090000 0x400000>;    /* 576KiB ~ 4.5MiB */
        };

        rootfs@490000 {
            label = "rootfs";
            reg = <0x490000 0xB70000>;    /* 4.5MiB ~ 16MiB */
        };
    };
};

SPI-NOR Device Tree 바인딩

/* SPI-NOR Flash DT 바인딩 */
&spi0 {
    status = "okay";

    flash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;                        /* SPI chip select 0 */
        spi-max-frequency = <104000000>; /* 최대 104MHz */
        spi-tx-bus-width = <4>;          /* Quad SPI 쓰기 */
        spi-rx-bus-width = <4>;          /* Quad SPI 읽기 */
        m25p,fast-read;                    /* Fast Read 명령 사용 */

        /* SFDP 자동 감지로 대부분의 속성이 자동 설정됨 */
        /* 필요 시 수동으로 호환 칩 지정 가능: */
        /* compatible = "winbond,w25q128", "jedec,spi-nor"; */

        partitions {
            compatible = "fixed-partitions";
            #address-cells = <1>;
            #size-cells = <1>;

            /* 파티션 정의 ... */
        };
    };
};

SPI-NAND Device Tree 바인딩

/* SPI-NAND Flash DT 바인딩 */
&spi0 {
    status = "okay";

    nand@0 {
        compatible = "spi-nand";
        reg = <0>;
        spi-max-frequency = <104000000>;
        spi-tx-bus-width = <4>;
        spi-rx-bus-width = <4>;

        partitions {
            compatible = "fixed-partitions";
            #address-cells = <1>;
            #size-cells = <1>;

            bootloader@0 {
                label = "u-boot";
                reg = <0x000000 0x100000>;
                read-only;
            };

            ubi@100000 {
                label = "ubi";
                reg = <0x100000 0x7F00000>;
            };
        };
    };
};

Parallel NAND Device Tree 바인딩

/* Parallel NAND (Raw NAND) DT 바인딩 */
nand-controller@ff900000 {
    compatible = "vendor,nand-controller";
    reg = <0xff900000 0x1000>;
    #address-cells = <1>;
    #size-cells = <0>;

    nand@0 {
        reg = <0>;                        /* NAND chip select */
        nand-bus-width = <8>;             /* 8-bit 또는 16-bit */
        nand-ecc-mode = "hw";             /* 하드웨어 ECC */
        nand-ecc-strength = <8>;          /* 8-bit ECC */
        nand-ecc-step-size = <512>;       /* 512바이트 단위 ECC */
        nand-on-flash-bbt;                 /* Flash 내 BBT 사용 */

        partitions {
            compatible = "fixed-partitions";
            #address-cells = <1>;
            #size-cells = <1>;

            spl@0 {
                label = "SPL";
                reg = <0x000000 0x080000>;
                read-only;
            };

            uboot@80000 {
                label = "u-boot";
                reg = <0x080000 0x180000>;
                read-only;
            };

            env@200000 {
                label = "u-boot-env";
                reg = <0x200000 0x080000>;
            };

            ubi@280000 {
                label = "ubi";
                reg = <0x280000 0xFD80000>;
            };
        };
    };
};

디버깅 & 도구

/proc/mtd

/proc/mtd는 시스템에 등록된 모든 MTD 디바이스의 요약 정보를 제공합니다:

# /proc/mtd 출력 예시
$ cat /proc/mtd
dev:    size   erasesize  name
mtd0: 00080000 00010000 "u-boot"
mtd1: 00010000 00010000 "u-boot-env"
mtd2: 00400000 00010000 "kernel"
mtd3: 0fb70000 00020000 "rootfs"

sysfs 인터페이스

/sys/class/mtd/mtdN/ 디렉터리는 각 MTD 디바이스의 상세 속성을 노출합니다:

# MTD sysfs 속성 조회
$ ls /sys/class/mtd/mtd0/
type         # Flash 타입 (nor, nand, dataflash, ...)
name         # 파티션 이름
size         # 디바이스 크기 (바이트)
erasesize    # erase block 크기
writesize    # 최소 쓰기 단위 (페이지 크기)
oobsize      # OOB 영역 크기 (NAND)
subpagesize  # sub-page 크기
numeraseregions  # erase region 수
flags        # MTD 플래그
ecc_strength # ECC 강도
ecc_step_size # ECC 단위 크기
bitflip_threshold # 비트 플립 보고 임계값

# 사용 예시
$ cat /sys/class/mtd/mtd0/type
nand
$ cat /sys/class/mtd/mtd0/size
268435456
$ cat /sys/class/mtd/mtd0/erasesize
131072

nandsim — NAND 시뮬레이터

nandsim은 실제 NAND 칩 없이 NAND MTD 디바이스를 시뮬레이션하는 커널 모듈입니다. 드라이버 개발과 파일시스템 테스트에 매우 유용합니다:

# nandsim 모듈 로드 예시

# 128MiB NAND 시뮬레이션 (2048+64 바이트 페이지, 128KiB erase block)
modprobe nandsim first_id_byte=0x20 \
                 second_id_byte=0xaa \
                 third_id_byte=0x00 \
                 fourth_id_byte=0x15

# 생성된 MTD 디바이스 확인
cat /proc/mtd

# UBI/UBIFS 테스트
ubiformat /dev/mtd0
ubiattach /dev/ubi_ctrl -m 0
ubimkvol /dev/ubi0 -N test -m
mount -t ubifs ubi0:test /mnt

# 파티션이 있는 nandsim
modprobe nandsim first_id_byte=0x20 \
                 second_id_byte=0xaa \
                 parts=32,64,0     # 3개 파티션 (erase block 수 기준)

# 시뮬레이션 종료
rmmod nandsim
nandsim 활용: nandsim은 비트 플립, bad block 삽입 등의 에러 주입 기능도 제공합니다. bitflips=, weakblocks=, gravepages= 파라미터로 다양한 에러 시나리오를 테스트할 수 있습니다. 또한 cache_file= 파라미터로 시뮬레이션 데이터를 파일에 영속화할 수 있습니다.

mtdram — RAM 기반 MTD

mtdram 모듈은 RAM에서 MTD 디바이스를 에뮬레이션합니다. NOR Flash와 유사하게 동작하며, 빠른 테스트에 적합합니다:

# 4MiB RAM 기반 MTD 생성 (16KiB erase block)
modprobe mtdram total_size=4096 erase_size=16

# JFFS2 테스트
flash_erase -j /dev/mtd0 0 0
mount -t jffs2 /dev/mtdblock0 /mnt

# 정리
umount /mnt
rmmod mtdram

debugfs 엔트리

MTD, UBI, UBIFS는 debugfs를 통해 내부 상태를 노출합니다:

# debugfs 마운트 (아직 마운트되지 않은 경우)
mount -t debugfs none /sys/kernel/debug

# UBI debugfs
ls /sys/kernel/debug/ubi/
# ubi0/           — UBI 디바이스 0 디버그 정보
#   chk_gen       — 일반 검사 활성화 여부
#   chk_io        — I/O 검사 활성화 여부
#   tst_disable_bgt — 백그라운드 스레드 비활성화

# UBIFS debugfs
ls /sys/kernel/debug/ubifs/
# ubi0_0/         — UBIFS 인스턴스 디버그 정보
#   dump_lprops   — LEB 속성 덤프
#   dump_budg     — 버짓 정보 덤프
#   dump_tnc      — TNC 트리 덤프

# 디버그 정보 읽기 예시
cat /sys/kernel/debug/ubifs/ubi0_0/dump_budg

관련 Kconfig 옵션

# MTD 핵심
CONFIG_MTD=y                      # MTD 서브시스템
CONFIG_MTD_BLOCK=y                # 블록 디바이스 에뮬레이션
CONFIG_MTD_CMDLINE_PARTS=y        # 커널 커맨드라인 파티션
CONFIG_MTD_OF_PARTS=y             # Device Tree 파티션

# NOR Flash
CONFIG_MTD_SPI_NOR=y              # SPI-NOR 지원
CONFIG_MTD_SPI_NOR_USE_4K_SECTORS=y # 4KiB 섹터 소거 지원
CONFIG_SPI_MEM=y                  # SPI 메모리 레이어

# NAND Flash
CONFIG_MTD_RAW_NAND=y             # Raw NAND 지원
CONFIG_MTD_SPI_NAND=y             # SPI-NAND 지원
CONFIG_MTD_NAND_ECC_SW_HAMMING=y  # 소프트웨어 Hamming ECC
CONFIG_MTD_NAND_ECC_SW_BCH=y      # 소프트웨어 BCH ECC

# UBI
CONFIG_MTD_UBI=y                  # UBI 지원
CONFIG_MTD_UBI_WL_THRESHOLD=4096 # Wear leveling 임계값
CONFIG_MTD_UBI_BEB_LIMIT=20      # 최대 bad block 수 (1024 PEB당)
CONFIG_MTD_UBI_FASTMAP=y          # Fastmap 지원
CONFIG_MTD_UBI_GLUEBI=y           # UBI 볼륨의 MTD 에뮬레이션

# 파일시스템
CONFIG_UBIFS_FS=y                 # UBIFS
CONFIG_UBIFS_FS_ADVANCED_COMPR=y  # 고급 압축 옵션
CONFIG_UBIFS_FS_ZSTD=y            # ZSTD 압축
CONFIG_JFFS2_FS=y                 # JFFS2
CONFIG_JFFS2_FS_WRITEBUFFER=y     # NAND wbuf 지원

# 테스트/디버깅
CONFIG_MTD_NANDSIM=m              # NAND 시뮬레이터 (모듈)
CONFIG_MTD_MTDRAM=m               # RAM 기반 MTD (모듈)
CONFIG_MTD_TESTS=m                # MTD 테스트 모듈
CONFIG_UBIFS_FS_AUTHENTICATION=y  # UBIFS 인증