NVMEM 프레임워크

리눅스 NVMEM 프레임워크를 "비휘발성 저장소를 바이트 배열이 아니라 의미 있는 보드 데이터 공급원으로 추상화하는 계층"이라는 관점에서 정리합니다. nvmem_config, nvmem_cell_info, nvmem_device, cell 기반 consumer API, direct device API, Device Tree의 nvmem-cells, board-file lookup, layout parser, post-process hook, raw sysfs ABI, keepout / read_only / root_only, 그리고 EEPROM / efuse / OTP / FRAM / flash factory partition을 실제 커널 사용 패턴에 맞춰 최대한 자세히 설명합니다.

전제 조건: Device Tree, MTD, I2C/SPI/GPIO, MAC 주소, 디바이스 드라이버 문서를 먼저 읽으세요. NVMEM은 저장 매체 자체를 가르치는 문서가 아니라, 매체 안에 들어 있는 공장 데이터와 보드 고유 정보를 어떻게 안정된 ABI로 꺼내 쓰는지 설명하는 프레임워크입니다.
일상 비유: NVMEM은 보드별 봉인 서류 보관함에 가깝습니다. 서랍 전체(EEPROM, efuse, OTP)를 통째로 뒤지는 대신, "MAC 주소", "보드 리비전", "보정값", "시리얼 번호"처럼 라벨이 붙은 봉투(cell)를 꺼내 쓰게 해 줍니다.

핵심 요약

  • Provider — 실제 비휘발성 저장소를 NVMEM 코어에 등록하는 쪽입니다. EEPROM, efuse, OTP, FRAM, factory partition이 여기에 해당합니다.
  • Cell — 저장소 전체가 아니라 그 안의 의미 있는 조각입니다. MAC 주소, serial, board-id, calibration blob이 대표적입니다.
  • Layout — 셀의 위치가 고정 offset이 아니라 TLV 같은 내부 포맷에 따라 결정될 때, 저장소를 파싱해서 동적으로 cell을 추가하는 계층입니다.
  • Consumer API — 드라이버는 offset을 직접 읽지 않고 devm_nvmem_cell_get(), nvmem_cell_read_u32() 같은 API로 논리 이름 중심 접근을 합니다.
  • Raw sysfs ABI — 사용자 공간은 /sys/bus/nvmem/devices/.../nvmem로 전체 바이트를 읽고 쓸 수 있지만, 이는 cell 기반 ABI보다 훨씬 위험한 경로입니다.
  • 운영 포인트 — endian, 버전 필드, 체크섬, 쓰기 보호, keepout, 원샷 프로비저닝 여부를 처음부터 문서화하지 않으면 제품 파생이 늘수록 유지보수가 폭발합니다.

단계별 이해

  1. 매체와 의미를 분리한다
    먼저 "EEPROM인가 efuse인가"보다 "안에 무엇이 들어 있나"를 생각합니다.
  2. 필요한 데이터를 cell로 자른다
    MAC 주소, serial, board-id, calibration처럼 소비자가 실제로 원하는 의미 단위로 나눕니다.
  3. 소비자와 연결한다
    DT면 nvmem-cells, non-DT면 lookup table, 특수 포맷이면 layout parser로 소비자와 연결합니다.
  4. raw 읽기/쓰기를 마지막 수단으로 둔다
    raw sysfs는 bring-up과 공장 도구에 유용하지만, 일반 드라이버 경로는 cell 기반이 더 안전합니다.
  5. 수명과 불변식을 먼저 정한다
    OTP인지, 여러 번 쓸 수 있는 EEPROM인지, erase 단위가 큰 flash인지에 따라 update 전략이 완전히 달라집니다.

NVMEM이 해결하는 문제와 왜 필요한가

현실의 보드는 거의 항상 어떤 형태로든 factory data를 가집니다. Ethernet MAC 주소, Wi-Fi/Bluetooth 캘리브레이션, board revision, serial number, panel/배터리 정보, SoC speed bin, secure boot fuse, customer SKU, RF 파라미터가 대표적입니다. 문제는 이런 데이터가 각기 다른 물리 매체에, 각기 다른 포맷으로, 각기 다른 offset에 들어 있다는 점입니다.

NVMEM 프레임워크가 생기기 전에는 EEPROM/efuse 드라이버가 각자 sysfs binary file을 만들고, 다른 드라이버는 그 구현 세부사항을 직접 알아야 하는 경우가 많았습니다. 공식 커널 문서도 과거에는 EEPROM 계열 드라이버들이 제각각 다른 방식으로 sysfs와 in-kernel access 경로를 제공해 큰 abstraction leak가 있었다고 설명합니다. NVMEM은 이 문제를 해결하려고 만들어졌습니다.

문제NVMEM 이전 방식NVMEM 이후 방식
드라이버마다 접근 방식이 다름각 드라이버가 제각각 sysfs/debugfs/API 제공provider/cell/direct API로 통일
소비자가 offset을 직접 알아야 함이더넷 드라이버가 EEPROM 0x00~0x05를 직접 읽음"mac-address" cell 이름만 알면 됨
보드 파생 대응 어려움보드별 if/else와 offset 상수 난립DT 또는 lookup으로 provider-cell 매핑 교체
공장 포맷 변경 위험packed struct를 여러 드라이버가 공유cell, layout, post-process로 의미를 분리
NVMEM이 하는 일: 저장소와 의미를 분리 Provider EEPROM / efuse / OTP / FRAM / flash partition 물리 매체와 read/write 제약 Cell / Layout mac-address, serial, board-id, calibration offset 고정 또는 runtime parsing Consumer Ethernet, Wi-Fi, PHY, PMIC, board code 이름 기반 데이터 소비 핵심 효과 소비자는 "어디에 저장됐는가" 대신 "무엇을 읽는가"에 집중할 수 있고, 보드 파생은 DT/lookup/layout만 바꾸면 되는 경우가 많다.

NVMEM을 읽는 기본 관점: 저장소 전체, 의미 단위, 파싱 계층

NVMEM은 보통 세 층으로 이해하면 쉽습니다.

무엇을 표현하나대표 객체
저장소 전체바이트 배열로 본 비휘발성 메모리struct nvmem_device, raw sysfs nvmem 파일
의미 단위MAC 주소, serial, calibration 같은 조각struct nvmem_cell_info, struct nvmem_cell
파싱 계층TLV, vendor blob처럼 동적 포맷을 읽어 cell 생성struct nvmem_layout, post-process hook

이 구조를 받아들이면 "왜 raw read/write API와 cell API가 둘 다 있는가"도 자연스럽게 이해됩니다. 공장 도구나 특수 드라이버는 저장소 전체를 다뤄야 할 수 있지만, 대부분의 소비자는 의미 단위만 필요합니다.

이 세 층을 섞으면 문제가 생깁니다. 예를 들어 이더넷 드라이버가 EEPROM 오프셋 상수를 들고 직접 raw read를 하면 provider 세부 구현이 consumer로 새어 나옵니다. 반대로 공장 프로비저닝 도구가 raw 대신 cell 몇 개만 보고 전체 blob을 덮어쓰려 하면 versioning과 checksum을 깨뜨릴 수 있습니다.

Provider 모델: nvmem_config와 등록 흐름

Provider는 실제 비휘발성 저장소를 NVMEM 코어에 등록하는 쪽입니다. 헤더 include/linux/nvmem-provider.h에 있는 struct nvmem_config가 등록의 중심이며, 공식 문서도 nvmem_register() / devm_nvmem_register()로 provider를 등록한다고 설명합니다.

필드의미실무 포인트
name, idprovider 이름과 IDNVMEM_DEVID_NONE, NVMEM_DEVID_AUTO 사용 가능
dev, ownerdevice model과 모듈 refcount대개 probe의 &pdev->devTHIS_MODULE
reg_read, reg_write실제 바이트 read/write 콜백provider의 본질
size저장소 전체 크기raw sysfs ABI 크기와 직결
word_size최소 접근 단위예: 1, 2, 4 bytes
stride최소 접근 stride홀수 offset 금지 같은 제약을 모델링
typeEEPROM/OTP/BATTERY_BACKED/FRAM 등sysfs type와 연결되는 의미
read_onlydevice-level write 금지OTP/efuse, read-only factory ROM에 적합
root_onlyroot만 접근 허용민감 정보가 있는 provider
ignore_wpWP 핀을 provider가 직접 관리하드웨어 write-protect와 소프트웨어 시퀀스가 얽힌 장치
keepout접근 회피 구간reserved bytes, ECC 영역, fuse hole 보호
cells, ncells정적 cell 정의provider가 직접 내장해 등록 가능
layout고정 layout 연결동적 parsing 기반 cell 생성
#include <linux/nvmem-provider.h>

struct board_eeprom {
    struct i2c_client *client;
    u8 buf[256];
};

static int board_eeprom_read(void *priv, unsigned int offset,
                             void *val, size_t bytes)
{
    struct board_eeprom *ee = priv;
    /* I2C EEPROM read transaction */
    return regmap_bulk_read(ee->client->dev.driver_data, offset, val, bytes);
}

static int board_eeprom_write(void *priv, unsigned int offset,
                              void *val, size_t bytes)
{
    struct board_eeprom *ee = priv;
    /* 페이지 write, write cycle time 고려 */
    return regmap_bulk_write(ee->client->dev.driver_data, offset, val, bytes);
}

static const struct nvmem_keepout board_keepout[] = {
    { .start = 0xf0, .end = 0x100, .value = 0xff },
};

static int board_eeprom_probe(struct i2c_client *client)
{
    struct board_eeprom *ee;
    struct nvmem_config cfg = {
        .dev = &client->dev,
        .name = "board-eeprom",
        .id = NVMEM_DEVID_NONE,
        .owner = THIS_MODULE,
        .type = NVMEM_TYPE_EEPROM,
        .size = 256,
        .word_size = 1,
        .stride = 1,
        .reg_read = board_eeprom_read,
        .reg_write = board_eeprom_write,
        .keepout = board_keepout,
        .nkeepout = ARRAY_SIZE(board_keepout),
    };

    ee = devm_kzalloc(&client->dev, sizeof(*ee), GFP_KERNEL);
    if (!ee)
        return -ENOMEM;

    cfg.priv = ee;

    return PTR_ERR_OR_ZERO(devm_nvmem_register(&client->dev, &cfg));
}

여기서 word_sizestride는 자주 생략되지만 중요합니다. 예를 들어 하드웨어가 4바이트 단위 접근만 허용하거나 2바이트 정렬을 요구한다면, 이 제약을 NVMEM 코어에 알려야 잘못된 raw access를 막을 수 있습니다.

Provider 등록과 raw access 경로 hardware backend EEPROM / efuse / OTP / FRAM read/write constraints nvmem_config size, stride, word_size, type reg_read/reg_write, keepout nvmem core devm_nvmem_register() cell/layout/ABI 노출 sysfs raw /sys/bus/nvmem/... nvmem, type, force_ro Provider가 책임지는 제약 OTP처럼 물리적으로 한 번만 쓸 수 있는지, EEPROM처럼 반복 쓰기가 가능한지, reserved range를 keepout으로 가릴지, raw sysfs를 root_only/read_only로 제한할지 모두 provider가 먼저 모델링해야 한다.

Cell 모델: offset보다 의미를 앞세우는 방법

Cell은 NVMEM 프레임워크의 가장 중요한 개념입니다. struct nvmem_cell_info는 단순히 offset + bytes만 담지 않고, raw_len, bit_offset, nbits, read_post_process 같은 필드도 가집니다. 즉, cell은 "저장소 일부를 읽는다"가 아니라, 의미 있는 데이터를 잘라내고 필요하면 해석까지 붙인다는 개념입니다.

필드의미언제 중요해지나
namecell 이름consumer는 이 이름으로 요청
offset저장소 내 시작 위치고정 레이아웃일 때 핵심
raw_lenpost-process 전 raw 길이헤더/CRC를 포함해 읽은 뒤 가공하는 경우
bytes최종 cell 길이consumer가 기대하는 결과 길이
bit_offset, nbits비트 단위 fieldrevision fuse, speed bin bitfield
read_post_process읽은 뒤 추가 가공vendor-specific swizzle, header strip, checksum 검증
#include <linux/etherdevice.h>
#include <linux/nvmem-provider.h>

static int board_mac_post_process(void *priv, const char *id, int index,
                                  unsigned int offset, void *buf, size_t bytes)
{
    u8 *addr = buf;

    if (bytes != ETH_ALEN)
        return -EINVAL;

    if (!is_valid_ether_addr(addr))
        return -EINVAL;

    return 0;
}

static const struct nvmem_cell_info board_cells[] = {
    {
        .name = "mac-address",
        .offset = 0x00,
        .bytes = 6,
        .read_post_process = board_mac_post_process,
    },
    {
        .name = "board-id",
        .offset = 0x10,
        .bytes = 4,
    },
    {
        .name = "soc-speed-bin",
        .offset = 0x20,
        .bytes = 1,
        .bit_offset = 2,
        .nbits = 3,
    },
};

비트필드 cell은 특히 efuse/OTP에서 자주 나옵니다. 여러 configuration bit가 한 워드 안에 몰려 있을 때, 각 consumer가 직접 마스크와 시프트를 들고 다니기보다 cell 정의에서 분리하는 것이 훨씬 낫습니다.

설계 원칙: "packed C 구조체를 raw로 저장하고 소비자들이 같은 구조체 정의를 공유한다"는 방식은 처음엔 쉬워 보여도 제품 파생과 버전 증가에 약합니다. 가능하면 의미 단위 cell, 길이 필드, 버전 필드, checksum을 분리하는 쪽이 장기적으로 안전합니다.

Consumer API: 대부분의 드라이버가 써야 하는 경로

공식 문서와 헤더가 모두 강조하듯, 대부분의 소비자는 cell 기반 API를 쓰는 것이 맞습니다. 핵심 흐름은 nvmem_cell_get() 또는 devm_nvmem_cell_get()으로 참조를 얻고, nvmem_cell_read() 또는 typed helper로 값을 읽은 뒤, 필요하면 nvmem_cell_put()으로 해제하는 것입니다.

API용도언제 쓰나
devm_nvmem_cell_get()관리형 cell 획득일반 probe 경로의 기본 선택
nvmem_cell_read()가변 길이 blob 읽기MAC 주소, calibration blob, serial 문자열
nvmem_cell_write()cell 쓰기반복 쓰기 가능한 EEPROM/FRAM에 제한적으로 사용
nvmem_cell_read_u8/u16/u32/u64()정수형 helper고정 길이 수치 cell
nvmem_cell_read_variable_le_u32/u64()가변 길이 little-endian 정수1~4 또는 1~8 byte length field
#include <linux/nvmem-consumer.h>

static int eth_probe(struct platform_device *pdev)
{
    struct nvmem_cell *cell;
    size_t len;
    u8 *addr;

    cell = devm_nvmem_cell_get(&pdev->dev, "mac-address");
    if (IS_ERR(cell))
        return PTR_ERR(cell);

    addr = nvmem_cell_read(cell, &len);
    if (IS_ERR(addr))
        return PTR_ERR(addr);

    if (len != ETH_ALEN || !is_valid_ether_addr(addr)) {
        kfree(addr);
        return -EINVAL;
    }

    eth_hw_addr_set(netdev, addr);
    kfree(addr);
    return 0;
}
static int board_id_probe(struct platform_device *pdev)
{
    u32 board_id;
    int ret;

    ret = nvmem_cell_read_u32(&pdev->dev, "board-id", &board_id);
    if (ret)
        return ret;

    /* board_id 기반으로 SKU/리비전 분기 */
    return 0;
}

typed helper를 쓸 때는 endian과 길이 해석을 명확히 해야 합니다. 예를 들어 nvmem_cell_read_u32()를 맹목적으로 쓰기보다, 저장소가 little-endian variable-length 값인지 확인하고 nvmem_cell_read_variable_le_u32()가 더 맞는지 판단해야 합니다.

Direct nvmem_device API: raw access가 정말 필요한 경우

공식 문서도 "어떤 경우에는 NVMEM을 직접 읽고 써야 한다"고 설명합니다. 이를 위해 nvmem_device_get(), nvmem_device_read(), nvmem_device_write(), nvmem_device_find(), nvmem_device_cell_read() 같은 direct API가 제공됩니다.

API의미주의점
devm_nvmem_device_get()provider 전체 획득셀 기반 ABI보다 provider 세부사항이 더 많이 보임
nvmem_device_read()offset 기반 raw readprovider 세부 레이아웃 누수 위험
nvmem_device_write()offset 기반 raw writeOTP/flash erase 제약을 특히 조심해야 함
nvmem_device_cell_read()cell_info 구조를 직접 넘겨 읽기커스텀 cell 정의를 코드 내에서 즉석 생성할 때 유용
nvmem_device_find()매칭 함수로 provider 탐색일반 드라이버보다는 core/board code 성격이 강함
static int factory_tool_read_blob(struct device *dev)
{
    struct nvmem_device *nvmem;
    u8 blob[128];
    int ret;

    nvmem = devm_nvmem_device_get(dev, "factory");
    if (IS_ERR(nvmem))
        return PTR_ERR(nvmem);

    ret = nvmem_device_read(nvmem, 0x100, sizeof(blob), blob);
    if (ret < 0)
        return ret;

    /* blob 파싱 */
    return 0;
}

이 경로는 공장 진단 도구, 보드 bring-up, format parser, 또는 단순한 cell 추상화로는 충분하지 않은 복합 blob을 읽을 때 유용합니다. 하지만 일반 consumer가 직접 offset을 들고 다니기 시작하면 NVMEM의 장점을 스스로 무너뜨리게 됩니다.

DT가 없는 환경과 machine lookup: nvmem_add_cell_lookups()

NVMEM은 DT 전용 프레임워크가 아닙니다. 공식 문서와 헤더 모두 lookup entry를 machine code에서 등록하는 경로를 제공합니다. 핵심 구조체는 struct nvmem_cell_lookup이며, provider 이름, cell 이름, consumer의 dev_id, consumer 측 연결 이름 con_id를 묶습니다.

#include <linux/nvmem-consumer.h>

static struct nvmem_cell_lookup board_nvmem_lookups[] = {
    {
        .nvmem_name = "board-eeprom",
        .cell_name = "mac-address",
        .dev_id = "foo-ethernet.0",
        .con_id = "mac-address",
    },
    {
        .nvmem_name = "board-eeprom",
        .cell_name = "board-id",
        .dev_id = "foo-pmic.0",
        .con_id = "board-id",
    },
};

static int __init board_init(void)
{
    nvmem_add_cell_lookups(board_nvmem_lookups,
                           ARRAY_SIZE(board_nvmem_lookups));
    return 0;
}

이 경로는 legacy board file, ACPI 기반 플랫폼, 혹은 DT 없이 장치를 붙이는 특정 아키텍처에서 여전히 쓸모가 있습니다. 이때 consumer가 nvmem_cell_get(dev, "mac-address")를 호출하면, 코어는 해당 consumer의 dev_idcon_id에 맞는 cell을 찾아 연결합니다.

Consumer 연결 방법 두 가지 Device Tree nvmem-cells = <&mac_addr> nvmem-cell-names = "mac-address" consumer는 이름만 안다 NVMEM core cell name, provider name, lookup consumer 요청을 provider로 연결 Machine lookup nvmem_name + cell_name + dev_id + con_id non-DT 환경 또는 board code 둘 다 최종 목표는 같다. consumer가 offset을 알지 않고 이름 중심으로 cell을 얻도록 만드는 것이다.

Provider 심화: type, keepout, stride, root_only, legacy OF cells

헤더에는 NVMEM의 실전 운용에서 중요한 추가 옵션이 여럿 들어 있습니다.

옵션의미실무 해석
enum nvmem_typeUNKNOWN, EEPROM, OTP, BATTERY_BACKED, FRAM매체 성격과 userspace 관찰 정보 제공
keepout읽기/쓰기를 피할 영역reserved fuse, ECC, secure region, broken bytes 가리기
root_onlyroot만 접근 가능raw sysfs로 민감 정보가 유출되면 안 되는 장치
read_only쓰기 금지OTP/efuse/ROM 성격의 provider
ignore_wpwrite protect 핀을 provider가 직접 관리WP GPIO를 제어해야 쓰기가 가능한 EEPROM
add_legacy_fixed_of_cells예전 OF cell 문법 지원새 binding보다 하위 호환 유지가 필요할 때
fixup_dt_cell_infoDT에서 읽은 cell 정보 수정길이/이름/post-process 보정
compat, base_dev오래된 misc/eeprom 계열 호환용새 드라이버가 적극적으로 의존할 필드는 아님

keepout는 특히 저평가된 기능입니다. 어떤 저장소는 전체 주소 범위가 전부 동일한 의미를 갖지 않습니다. 예를 들어 secure fuse hole, ECC 영역, vendor secret, partially-programmed bytes, reserved area는 raw read 자체가 혼란을 만들 수 있습니다. 이때 keepout을 정의해 접근을 막거나 읽기 값을 특정 fill byte로 치환할 수 있습니다.

root_onlyread_only는 단순 권한 설정 이상입니다. raw sysfs ABI는 사용자가 전체 바이트열을 그대로 읽고 쓸 수 있기 때문에, provider가 민감하거나 되돌리기 어려운 매체라면 초기에 이 플래그를 엄격하게 잡는 것이 안전합니다. 이 설명은 헤더와 ABI 문서를 종합한 실무적 해석입니다.

Device Tree binding: nvmem-cells, nvmem-cell-names, 고정 cell 정의

DT 기반 시스템에서 NVMEM의 핵심은 provider 노드 아래에 cell을 정의하고, consumer가 nvmem-cellsnvmem-cell-names로 이를 참조하는 패턴입니다. 이 구조를 쓰면 provider 종류가 바뀌어도 consumer 드라이버 코드는 거의 그대로 유지될 수 있습니다.

/* Provider: EEPROM */
eeprom@50 {
    compatible = "atmel,24c02";
    reg = <0x50>;

    mac_addr: mac-address@0 {
        reg = <0x00 0x06>;
    };

    serial_num: serial-number@8 {
        reg = <0x08 0x10>;
    };

    board_id: board-id@20 {
        reg = <0x20 0x04>;
    };
};

/* Consumer: Ethernet */
ethernet@12340000 {
    nvmem-cells = <&mac_addr>;
    nvmem-cell-names = "mac-address";
};

/* Consumer: Board management */
board-mgr {
    nvmem-cells = <&serial_num>, <&board_id>;
    nvmem-cell-names = "serial-number", "board-id";
};

이 패턴의 장점은 분명합니다.

이미 [mac-address.html](/mnt/public_home/work/linuxkernel/pages/mac-address.html#L3990)에서도 보듯, 네트워크 스택은 NVMEM cell을 통한 MAC 주소 제공을 중요한 프로비저닝 경로 중 하나로 취급합니다. 따라서 "mac-address" cell 이름은 사실상 현장에서 널리 공유되는 계약입니다.

DT에서의 provider-cell-consumer 연결 Provider node eeprom@50 mac-address@0 serial-number@8 board-id@20 NVMEM core cell 이름과 reg 범위를 해석 consumer 요청과 매칭 devm_nvmem_cell_get() Consumer nodes ethernet: "mac-address" board-mgr: "serial-number" board-mgr: "board-id" DT binding의 핵심은 cell 이름을 소비자 ABI로 만드는 것이다. offset은 provider 구현 세부사항으로 남겨 두는 편이 장기적으로 안전하다.

NVMEM layouts와 post-process: 고정 offset만으로 부족할 때

공식 NVMEM 문서는 layout을 "runtime에 NVMEM 내용을 읽어 셀을 동적으로 추가하는 메커니즘"이라고 설명합니다. 단순 offset/length로 표현되는 고정 cell만으로는 TLV, name=value 목록, vendor header가 있는 blob, CRC 뒤에 실제 데이터가 오는 형식을 다루기 어렵기 때문입니다.

헤더에는 struct nvmem_layoutstruct nvmem_layout_driver가 정의되어 있고, 핵심 콜백은 add_cells()입니다. layout driver는 저장소 내용을 읽고, 발견한 항목마다 nvmem_add_one_cell()을 호출해 동적으로 cell을 만듭니다.

고정 cell 방식layout 방식
offset과 length가 고정offset이 blob 내용에 따라 달라질 수 있음
DT만으로 충분한 경우가 많음parser 코드가 필요
단순 EEPROM 표에 적합TLV, header+checksum, vendor blob에 적합
구현이 단순초기 parsing 비용이 있지만 ABI가 더 깔끔해질 수 있음
struct tlv_hdr {
    u8 type;
    u8 len;
};

static int vendor_layout_add_cells(struct nvmem_layout *layout)
{
    struct nvmem_device *nvmem = layout->nvmem;
    u8 hdrbuf[2];
    unsigned int off = 0;

    while (off + 2 < nvmem_dev_size(nvmem)) {
        struct tlv_hdr *hdr = (struct tlv_hdr *)hdrbuf;
        struct nvmem_cell_info info = { };

        if (nvmem_device_read(nvmem, off, sizeof(hdrbuf), hdrbuf) < 0)
            return -EIO;

        if (hdr->type == 0xff)
            break;

        if (hdr->type == 1) {
            info.name = "mac-address";
            info.offset = off + 2;
            info.bytes = hdr->len;
            nvmem_add_one_cell(nvmem, &info);
        }

        off += 2 + hdr->len;
    }

    return 0;
}

Layout의 또 다른 강점은 post-processing입니다. 공식 문서도 layout이 자기 자신이 만든 cell뿐 아니라 기존 cell에도 post-process hook을 붙일 수 있다고 설명합니다. 따라서 vendor blob 안의 crc + payload, 바이트 스왑, 가변 길이 little-endian 수치 같은 형식을 layout에서 정리한 뒤 consumer에는 깨끗한 cell만 보여 줄 수 있습니다.

레이아웃 설계 원칙: consumer에게 "이 blob의 3번째 TLV가 MAC, 5번째가 board-id" 같은 지식을 노출하지 마세요. 그 parsing 지식은 layout 또는 provider 쪽에 가두는 편이 훨씬 안정적입니다.

사용자 공간 ABI: raw nvmem, force_ro, type, cells 디렉터리

공식 NVMEM 문서는 사용자 공간이 raw NVMEM 파일을 /sys/bus/nvmem/devices/*/nvmem에서 읽고 쓸 수 있다고 설명합니다. 또한 stable ABI 문서는 같은 디렉터리에 force_rotype이 있음을 보여 주고, testing ABI 문서는 /sys/bus/nvmem/devices/.../cells/<cell-name> 경로도 정의합니다.

경로의미출처
/sys/bus/nvmem/devices/<dev>/nvmemraw binary 파일공식 NVMEM 문서, stable ABI
/sys/bus/nvmem/devices/<dev>/force_roraw 쓰기 허용/금지 제어용 속성stable ABI
/sys/bus/nvmem/devices/<dev>/typeprovider type 표시stable ABI
/sys/bus/nvmem/devices/<dev>/cells/<cell>cell 단위 노출testing ABI
# 등록된 NVMEM 장치 확인
ls /sys/bus/nvmem/devices/

# raw 바이트열 확인
xxd /sys/bus/nvmem/devices/board-eeprom/nvmem | head

# provider type 확인
cat /sys/bus/nvmem/devices/board-eeprom/type

# cell별 ABI가 있는 경우 확인
find /sys/bus/nvmem/devices/board-eeprom/cells -maxdepth 1 -type f 2>/dev/null

raw sysfs ABI는 강력하지만 위험합니다.

따라서 일반 애플리케이션이나 개별 device driver는 raw sysfs보다 cell 기반 ABI 또는 상위 전용 API를 우선하는 편이 맞습니다.

raw ABI와 cell ABI의 차이 raw sysfs /sys/bus/nvmem/devices/<dev>/nvmem 장점: 전체 덤프, 공장 도구, bring-up 위험: 의미/버전/체크섬을 우회 force_ro, root_only, read_only 정책이 중요 cell 기반 접근 devm_nvmem_cell_get(), nvmem_cell_read_u32() 장점: 의미 단위, provider 교체 내성 안전: consumer가 offset/format을 몰라도 됨 제약: 전체 blob 조작에는 부적합 일반 드라이버는 오른쪽, 공장 도구와 parser/진단 코드는 왼쪽 경로가 더 자주 맞는다.

백엔드별 특성과 제약: EEPROM, efuse, OTP, FRAM, flash factory partition

NVMEM은 추상화 계층이지만, 물리 매체의 제약은 사라지지 않습니다. 이 제약을 무시하면 API는 맞는데 제품은 망가집니다.

백엔드장점제약NVMEM 설계 포인트
I2C EEPROM범용, 저렴, 재기록 가능페이지 write, write-cycle 지연, wearraw write 남용 금지, checksum/버전 필드 필요
efuse고유성, tamper 내성대개 one-time programmableread_only, bitfield cell, keepout 설계 중요
OTP ROM/OTP controller보안과 식별 값 저장에 적합복구 불가, 제조 단계 프로비저닝typed helper, endian 문서화, raw write 극히 제한
FRAM빠르고 write endurance 높음용량/비용 제약자주 업데이트되는 serial/config에도 적합
flash partition용량 큼, factory blob 저장 쉬움erase block, atomic update, power-cut 위험layout parser, redundant copy, version/CRC 필요

특히 flash partition 기반 provider는 단순 EEPROM처럼 다루면 안 됩니다. erase-before-write, 최소 erase block, power cut 중단, wear leveling 부재가 한꺼번에 얽힐 수 있기 때문입니다. 이 경우 NVMEM은 "접근 추상화"를 제공할 뿐, atomic update나 redundancy 자체를 자동으로 해결해 주지는 않습니다.

안전성과 보안: write protect, keepout, versioning, raw write 통제

NVMEM은 종종 제품의 출하 정체성을 담고 있습니다. MAC 주소, secure fuse, anti-rollback, calibration secret, board serial이 여기에 들어가므로, "읽을 수 있으면 좋다" 수준으로 다루면 안 됩니다.

위험설명완화 전략
잘못된 raw writefactory data가 부분적으로 깨짐read_only, force_ro, 전용 공장 도구 사용
OTP 오기록복구 불가프로비저닝 단계 분리, simulation/test path 분리
endian 혼선board-id나 calibration 값이 잘못 해석됨셀 단위 문서화, typed helper 사용
reserved 영역 오염secure/hidden bytes 손상keepout, root_only, raw access 제한
버전 없는 packed struct파생 제품에서 해석 충돌버전, 길이, CRC, layout parser 도입

공장 데이터 포맷은 DT ABI만큼이나 보수적으로 바꿔야 합니다. [DT ABI 문서](https://docs.kernel.org/devicetree/bindings/ABI.html)도 binding은 오래 살아남는 안정 계약이어야 한다고 설명하는데, factory data layout 역시 실질적으로 같은 성격을 가집니다. 이미 현장에 출하된 장비가 그 포맷을 영구히 들고 다니기 때문입니다.

강한 권고: 새 설계에서는 최소한 magic + version + length + payload + CRC를 고려하세요. 10년 뒤에도 읽힐 포맷인지 먼저 생각해야 합니다. 이 문장은 특정 공식 문서의 직접 표현이 아니라, NVMEM/DT ABI 원칙을 종합한 실무적 결론입니다.

디버깅 절차: provider 존재, DT 매핑, raw dump, cell 길이, consumer 경로

NVMEM 문제는 보통 "드라이버가 못 읽는다"로 시작하지만, 실제 원인은 크게 다섯 가지입니다.

  1. provider 등록 실패
    장치가 NVMEM bus에 안 올라옴
  2. DT / lookup 매핑 실패
    consumer가 cell 이름을 못 찾음
  3. 길이/엔디언 오류
    값은 읽었지만 잘못 해석함
  4. layout/parser 오류
    동적 cell 생성이 틀림
  5. 물리 매체 제약 위반
    EEPROM write-cycle, flash erase, OTP read-only 문제
# 1. provider 존재 확인
ls /sys/bus/nvmem/devices/

# 2. raw dump 확인
xxd /sys/bus/nvmem/devices/board-eeprom/nvmem | head

# 3. type / force_ro 확인
cat /sys/bus/nvmem/devices/board-eeprom/type
cat /sys/bus/nvmem/devices/board-eeprom/force_ro 2>/dev/null

# 4. DT에서 consumer 연결 확인
grep -R "nvmem-cells" /sys/firmware/devicetree/base 2>/dev/null
grep -R "nvmem-cell-names" /sys/firmware/devicetree/base 2>/dev/null

# 5. kernel log 확인
dmesg | grep -iE 'nvmem|eeprom|efuse|otp|qfprom|ocotp'

MAC 주소처럼 길이가 명확한 데이터는 raw dump만 봐도 이상 여부를 빠르게 판단할 수 있습니다. 예를 들어 6바이트 대신 8바이트가 정의돼 있거나, all-zero, multicast bit가 켜진 값이면 cell 정의 또는 factory data가 잘못된 가능성이 높습니다.

layout 기반 장치라면 raw dump와 생성된 cell 목록을 함께 비교해야 합니다. parser가 TLV header 길이를 잘못 해석하면 이후 모든 cell offset이 연쇄적으로 틀어지기 때문입니다.

흔한 실패 패턴과 원인 추적

증상흔한 원인대응
consumer가 -ENOENT로 cell을 못 찾음DT 이름 불일치 또는 lookup 누락nvmem-cell-names, con_id, provider cell 이름 비교
MAC 주소가 랜덤 생성으로 대체됨cell 길이 오류, invalid ether addr, provider read 실패raw dump와 [mac-address.html](/mnt/public_home/work/linuxkernel/pages/mac-address.html#L4012) 경로 확인
board-id 값이 엉뚱함endian 혼선 또는 variable-length 해석 오류nvmem_cell_read_u32() 대신 variable LE helper 검토
raw write 후 일부 필드만 깨짐flash erase/page write 제약 무시매체별 atomic update 전략 재설계
reserved bytes가 노출됨keepout 미설정provider keepout 범위 정의
layout 기반 장치에서 cell이 하나도 안 보임layout parser probe/add_cells 실패raw dump와 parser 로그 비교
OTP 쓰기 테스트 후 보드가 영구 손상개발 보드와 생산 경로 분리 실패에뮬레이션용 provider와 실칩 provider 분리

끝까지 따라가는 상태 전이 예제: Ethernet MAC 주소가 NVMEM에서 올라오기까지

NVMEM의 대표 사례는 Ethernet MAC 주소입니다. 전체 흐름을 끝까지 따라가 보면 프레임워크의 장점이 분명해집니다.

  1. provider 등록
    I2C EEPROM 또는 efuse 드라이버가 devm_nvmem_register()로 저장소를 올립니다.
  2. cell 정의
    DT 또는 정적 cell 배열에서 "mac-address" cell을 6바이트로 정의합니다.
  3. consumer 연결
    Ethernet 노드가 nvmem-cells / nvmem-cell-names"mac-address"를 참조합니다.
  4. probe 시 읽기
    이더넷 드라이버 또는 공통 helper가 NVMEM cell을 읽습니다.
  5. 유효성 검사
    길이와 유효한 MAC 형식을 확인하고, 실패하면 random MAC 또는 다른 fallback으로 내려갑니다.
  6. 네트워크 장치에 적용
    eth_hw_addr_set() 같은 경로로 net_device 주소가 설정됩니다.
상태 전이: MAC 주소를 NVMEM에서 읽는 흐름 provider 등록 EEPROM / efuse cell 정의 "mac-address" consumer 매핑 DT or lookup probe read nvmem_cell_read() 검사 길이/유효성 적용 netdev 이 흐름 덕분에 Ethernet 드라이버는 EEPROM 종류나 offset을 모르고도 MAC 주소를 얻을 수 있다.

커널 내부 구조: nvmem_device, nvmem_cell, nvmem_layout 상세

Provider와 Consumer 사이에서 NVMEM 코어가 어떻게 동작하는지 정확히 이해하려면 핵심 구조체들을 살펴봐야 합니다.

nvmem_device 구조체

struct nvmem_device는 NVMEM 프레임워크의 중심 객체입니다. 이 구조체는/provider의 물리적 특성과 논리적 설정을 모두 담고 있습니다.

struct nvmem_device {
    struct device dev;
    int id;

    /* 물리적 특성 */
    size_t size;            /* 총 바이트 크기 */
    size_t word_size;       /* 최소 접근 단위 (1, 2, 4) */
    size_t stride;          /* 접근 정렬 요구사항 */
    enum nvmem_type type;  /* EEPROM, OTP, FRAM 등 */
    flags flags;

    /* 읽기/쓰기 콜백 */
    int (*reg_read)(struct nvmem_device *nvmem,
                      unsigned int offset,
                      void *val, size_t bytes);
    int (*reg_write)(struct nvmem_device *nvmem,
                       unsigned int offset,
                       void *val, size_t bytes);

    /* 권한 및 보호 */
    bool read_only;
    bool root_only;
    bool ignore_wp;

    /* keepout 영역 */
    struct nvmem_keepout *keepout;
    int nkeepout;

    /* cell 관리 */
    struct nvmem_cell_table *cell_table;
    struct list_head cells;

    /* layout (동적 cell 생성) */
    struct nvmem_layout *layout;

    /* 사용자 데이터 */
    void *priv;

    /* 버스specific 데이터 */
    struct nvmem_bus *bus;
    struct mutex lock;
};

이 구조체에서 특히 중요한 필드들을 살펴보면:

nvmem_cell 구조체

struct nvmem_cell은 consumer에게 반환되는 단위입니다. 단순히 데이터만 담는 것이 아니라, 읽기 작업의 전후 처리를 위한 메타데이터도 포함합니다.

struct nvmem_cell {
    struct nvmem_device *nvmem;
    struct nvmem_cell_info info;
    unsigned int offset;
    size_t raw_len;
    size_t bytes;
    unsigned int bit_offset;
    unsigned int nbits;
    nvmem_post_process_t read_post_process;
    void *priv;
};

Cell의 핵심 특성은 다음과 같습니다:

nvmem_layout 구조체

Layout은 동적 cell 생성을 위한 프레임워크입니다. 특히 복잡한 포맷(TLV, vendor-specific blob)을 가진 저장소에서 강점을 발휘합니다.

struct nvmem_layout {
    struct nvmem_device *nvmem;
    struct device *dev;
    const char *name;

    /* layout driver가 구현해야 하는 콜백들 */
    int (*add_cells)(struct nvmem_layout *layout);

    /* device tree 연동 */
    struct device_node *np;

    /* 사용자 데이터 */
    void *priv;
};

Layout driver는 일반적으로 다음과 같은 패턴으로 구현됩니다:

  1. 저장소 헤더를 읽어 포맷을 파악합니다.
  2. 각 항목을 파싱하여 nvmem_cell_info 구조체를 채웁니다.
  3. nvmem_add_one_cell()을 호출하여 cell을 등록합니다.
  4. 필요시 post-process 콜백을 등록합니다.

실제 커널 드라이버 분석: qfprom, at24, qcom, snvs

커널에는 이미 다양한 NVMEM provider 드라이버가 존재합니다. 대표적인 몇 가지를 분석하면 NVMEM의 실제 사용 패턴을 더 잘 이해할 수 있습니다.

qfprom: Qualcomm PMIC/PPA/PPAFUSE 드라이버

Qualcomm의 Qualcomm Protected Memory(qfprom)는 SoC 내부의 OTP 메모리에 접근하는 드라이버입니다. 이 드라이버는 다음 위치에서 찾을 수 있습니다: drivers/nvmem/qfprom.c

/* qfprom 드라이버의 핵심 구조 */

static struct nvmem_config qfprom_config = {
    .name = "qfprom",
    .dev = &pdev->dev,
    .owner = THIS_MODULE,
    .type = NVMEM_TYPE_OTP,
    .read_only = true,  /* 기본적으로 읽기 전용 */
    .root_only = true,  /* 보안 민감 */
    .reg_read = qfprom_read,
    .reg_write = qfprom_write,
};

qfprom 드라이버의 특징은:

at24: I2C EEPROM 드라이버

가장 널리 사용되는 EEPROM 드라이버 중 하나인 at24는 drivers/misc/eeprom/at24.c에 있습니다.

/* at24 드라이버의 NVMEM 등록 */

static int at24_probe(struct i2c_client *client)
{
    struct at24_data *at24;
    struct nvmem_config config = {};

    at24 = devm_kzalloc(&client->dev, sizeof(*at24), GFP_KERNEL);
    if (!at24)
        return -ENOMEM;

    config.dev = &client->dev;
    config.name = "at24";
    config.type = NVMEM_TYPE_EEPROM;
    config.size = at24->byte_len;
    config.word_size = 1;
    config.stride = at24->page_size;  /* 페이지 정렬 */
    config.reg_read = at24_read;
    config.reg_write = at24_write;
    config.priv = at24;
    config.ignore_wp = true;

    at24->nvmem = devm_nvmem_register(&client->dev, &config);

    /* NVMEM을 통한 접근은 I2C를 타므로 wp-gpios 무시 */
}

at24 드라이버의 핵심 특징:

qcom-sns: Qualcomm 온도/전원 센서 NVMEM

Qualcomm SoC에서 온도 센서 보정값이나 전원 레AIL 정보를 NVMEM으로 노출하는 드라이버입니다.

/* qcom-sns-drv의 NVMEM 연동 예시 */

static int qcom_thermal_calib_probe(struct platform_device *pwd)
{
    struct nvmem_cell *cell;
    u32 *calib_data;
    size_t len;

    /* 온도 센서 보정값 NVMEM에서 읽기 */
    cell = devm_nvmem_cell_get(&pwd->dev, "thermal-calib");
    if (IS_ERR(cell))
        return PTR_ERR(cell);

    calib_data = nvmem_cell_read(cell, &len);
    nvmem_cell_put(cell);

    if (IS_ERR(calib_data) || len != sizeof(struct thermal_calib)) {
        dev_err(&pwd->dev, "invalid calibration data\n");
        return -EINVAL;
    }

    return thermal_sensor_init(calib_data);
}

snvs: Freescale/NXP Secure Non-Volatile Storage

NXP의 i.MX SoC에서 secure boot 관련 fuse 정보를 제공하는 드라이버입니다.

/* snvs driver의 keepout 예시 */

static const struct nvmem_keepout snvs_keepout[] = {
    /* Secure boot fuse (잠글 경우 읽기 불가) */
    { .start = 0x00, .end = 0x10, .value = 0x00, .flags = NVMEM_KEEPOUT_READ },
    /* OEM reserved */
    { .start = 0x20, .end = 0x30, .value = 0xff, .flags = NVMEM_KEEPOUT_WRITE },
};

static struct nvmem_config snvs_config = {
    .name = "snvs",
    .type = NVMEM_TYPE_OTP,
    .read_only = true,
    .keepout = snvs_keepout,
    .nkeepout = ARRAY_SIZE(snvs_keepout),
    .reg_read = snvs_read,
    .reg_write = snvs_write,
};

SNVS 드라이버의 특징:

firmware 서브시스템 연동: efi, imx-ocotp, ti-pruss

NVMEM은 firmware와 긴밀하게 연동됩니다. 특히 secure boot, calibrate, OTP 프로그래밍에서 firmware 서브시스템과 함께 동작합니다.

EFI 변수와 NVMEM

UEFI 환경에서는 EFI 변수가 비휘발성 저장소를 사용합니다. Linux에서는 efivarfs로EFI 변수를 노출하지만, 일부 플랫폼에서는 이를 NVMEM으로 맵핑하기도 합니다.

/* EFI 변수를 NVMEM으로 내보내는 예시 */

static int efi_nvmem_probe(struct platform_device *pwd)
{
    struct nvmem_config cfg = {
        .name = "efi-nvmem",
        .type = NVMEM_TYPE_BATTERY_BACKED,
        .reg_read = efi_nvmem_read,
        .reg_write = NULL,  /* EFI 변수는 별도 경로로 */
    };

    /* EFI variable store에서 읽기 */
    cfg.size = efi_query_variable_store("NVMEM", &cfg.priv);

    return devm_nvmem_register(&pwd->dev, &cfg);
}

Freescale i.MX OCOTP

i.MX SoC의 On-Chip OTP(OCOTP)는 다음과 같은 구조로 NVMEM을 제공합니다:

/* i.MX OCOTP NVMEM 구조 */

struct imx_ocotp {
    struct nvmem_config config;
    struct clk *clk;
    void __iomem *base;
    unsigned int num_words;
    unsigned int word_size;
};

/* OCOTP는 Shadow register를 사용 - timing 민감 */

static int imx_ocotp_read(struct nvmem_device *nvmem,
                         unsigned int offset,
                         void *val, size_t bytes)
{
    struct imx_ocotp *ocotp = nvmem->priv;
    u32 *buf = val;

    clk_enable(ocotp->clk);  /* timing 필수 */

    /* shadow register에서 읽기 - bank:word addressing */
    for (size_t i = 0; i < bytes; i += 4) {
        int bank = (offset + i) / 36;  int word = (offset + i) % 36;
        buf[i/4] = readl(ocotp->base + bank * 36 + word * 4);
    }

    clk_disable(ocotp->clk);
    return 0;
}

성능 최적화와 고려사항

NVMEM은 일반적으로 부팅 시 한 번 읽는 용도가 대부분이지만, 특정 사용 사례에서는 성능이 중요해질 수 있습니다.

캐싱 전략

NVMEM 프레임워크는 기본적으로 캐싱을 지원합니다. nvmem_device 구조체의 flagsNVMEM_FLAGS_CACHE를 설정하면 됩니다:

/* NVMEM 캐시 활성화 */

struct nvmem_config cfg = {
    .name = "board-eeprom",
    .size = 256,
    /* ... */
};

/* device tree에서 cache 속성이 있으면 자동으로 적용됨 */
/* eeprom@50 { nvmem-cells = <&mac_addr>; nvmem-keep-content; }; */

캐싱이 유용한 상황:

배치 읽기

여러 cell을 읽을 때 개별 읽기보다 배치 읽기가 더 효율적입니다:

/* 효율적인 배치 읽기 */

static int read_board_data(struct device *dev,
                          struct board_config *cfg)
{
    struct nvmem_device *nvmem;
    u8 buf[64];
    int ret;

    nvmem = devm_nvmem_device_get(dev, "board-eeprom");
    if (IS_ERR(nvmem))
        return PTR_ERR(nvmem);

    /* 한 번에 64바이트 읽기 - I2C 전송 1회 */
    ret = nvmem_device_read(nvmem, 0, sizeof(buf), buf);
    if (ret < 0)
        return ret;

    /* 버퍼에서 개별 필드 파싱 */
    memcpy(cfg->mac, buf + 0, 6);
    cfg->board_id = get_unaligned_le32(buf + 6);
    cfg->rev = buf[10];

    return 0;
}

비동기 쓰기

EEPROM의 쓰기 주기가 긴 경우(non-blocking 쓰기):

/* 비동기 EEPROM 쓰기 */

static void eeprom_async_write(struct work_struct *work)
{
    struct async_write *req = container_of(work, struct async_write, work);
    int retries = 3;

    /* 페이지 쓰기 - 최대 5ms 소요 */
    while (retries--) {
        int ret = regmap_bulk_write(req->regmap, req->offset,
                                           req->data, req->len);
        if (!ret)
            break;
        usleep_range(5000, 10000);  /* write cycle time */
    }

    complete(&req->done);
}

Device Tree 고급 패턴: overlay, postprocess, fixed-cells

Device Tree에서 NVMEM은 단순히 cell 정의만으로 그치지 않습니다. 고급 사용 사례를 살펴보겠습니다.

post-process가 있는 cell 정의

/* Device Tree에서 post-process 지정 */
&eeprom {
    nvmem-name = "board-data";

    /* Consumer가 읽은 후 swizzle 적용 */
    mac-address@0 {
        reg = <0x00 0x06>;
        nvmem,bit-offset = 0;
        nvmem,nbits = 48;
    };

    /*Vendor-specific-swizzle: MAC 바이트 순서 반전 */
    /* 이 속성은 provider driver의 post-process hook에서 파싱됨 */
    mac-swapped@0 {
        reg = <0x00 0x06>;
        nvmem,post-process = "mac-swap-bytes";
    };
};

고정 cell 정의와 consumer 연결

/* 보드 전체 NVMEM 설정 예시 */

/* 1. Provider 정의 */
board_eeprom: eeprom@50 {
    compatible = "atmel,24c02";
    reg = <0x50>;
    page-size = 8;

    /* NVMEM provider로 등록 */
    nvmem-layout {
        compatible = "board-eeprom-layout";
    };
};

/* 2. Cell 정의 */
&board_eeprom {
    mac_addr: mac-address@0 {
        reg = <0x00 0x06>;
    };

    serial_num: serial-number@10 {
        reg = <0x10 0x10>;
    };

    hw_rev: hw-revision@20 {
        reg = <0x20 0x04>;
    };

    /* Calibration blob */
    wifi_cal: wifi-calibration@40 {
        reg = <0x40 0x100>;
    };
};

/* 3. Consumer 연결 */
&gmac {
    nvmem-cells = <&mac_addr>;
    nvmem-cell-names = "mac-address";
};

/* Wi-Fi는 calibration blob이 두 개 */
&wifi {
    nvmem-cells = <&wifi_cal>, <&mac_addr>;
    nvmem-cell-names = "calibration", "mac-address";
};

테스트 전략: 단위 테스트, 통합 테스트, 하드웨어 테스트

NVMEM 드라이버와 consumer의 신뢰성을 보장하기 위한 테스트 전략을 살펴보겠습니다.

커널 내 NVMEM 단위 테스트

커널에는 NVMEM 프레임워크 자체를 테스트하는 테스트 모듈이 있습니다:

/* drivers/misc/eeprom/eeprom-nvmem-test.c 예시 */

static int eeprom_nvmem_test_init(void)
{
    struct nvmem_device *nvmem;
    u8 test_data[16];
    u8 read_data[16];
    int ret;

    /* 테스트용 virtual nvmem 생성 */
    nvmem = nvmem_device_find("test-eeprom");
    if (IS_ERR(nvmem))
        return PTR_ERR(nvmem);

    /* 쓰기 테스트 */
    get_random_bytes(test_data, sizeof(test_data));
    ret = nvmem_device_write(nvmem, 0, sizeof(test_data), test_data);
    if (ret < 0)
        return ret;

    /* 읽기 검증 */
    ret = nvmem_device_read(nvmem, 0, sizeof(read_data), read_data);
    if (ret < 0)
        return ret;

    if (memcmp(test_data, read_data, sizeof(test_data))) {
        pr_err("NVMEM read/write test failed\n");
        return -EIO;
    }

    pr_info("NVMEM read/write test passed\n");
    return 0;
}

사용자 공간 테스트 도구

# NVMEM 테스트 스크립트

#!/bin/bash

NVMEM_DEV="/sys/bus/nvmem/devices/board-eeprom/nvmem"

echo "=== NVMEM Basic Test ==="

# 1. device exists
if [ ! -e "$NVMEM_DEV" ]; then
    echo "FAIL: NVMEM device not found"
    exit 1
fi

# 2. Read test
dd if="$NVMEM_DEV" bs=1 count=16 2>/dev/null | xxd
if [ $? -ne 0 ]; then
    echo "FAIL: Read failed"
    exit 1
fi

# 3. Write test (read-only 체크)
if [ -w "$NVMEM_DEV" ]; then
    echo "WARNING: NVMEM is writable"
else
    echo "PASS: NVMEM is read-only as expected"
fi

echo "=== Cell List ==="
ls /sys/bus/nvmem/devices/board-eeprom/cells/ 2>/dev/null

하드웨어 레벨 테스트 패턴

실제 하드웨어에서 NVMEM을 테스트할 때 고려해야 할 사항들:

테스트 유형대상방법
읽기 내성EEPROM/FRAM반복 읽기 후 데이터 일관성 검증
쓰기 내성EEPROM/FRAM반복 쓰기/읽기 후 ECC 검증
OTP 동작efuse/OTP읽기 전용 확인, 쓰기 후 재읽기
보안 영역secure fuseroot_only 접근 제한 테스트
ECCFlash-basedECC 오류 주입 후 복구 테스트
전원 이상Battery-backed전원 차단 후 데이터 유지 확인

고급 트러블슈팅: 레이아웃 디버깅, 추적, 프로파일링

Layout 디버깅

동적 cell 생성이 예상과 다를 때:

# Layout 디버깅 활성화
echo "module nvmem_core +p" > /sys/kernel/debug/dynamic_debug/control

# Layout cell 생성 로그 확인
dmesg | grep -iE 'nvmem:layout'

# 생성된 cell 목록 확인
cat /sys/bus/nvmem/devices/*/cells/*/name 2>/dev/null

# Cell별 속성 확인
ls -la /sys/bus/nvmem/devices/board-eeprom/cells/

Tracepoints

커널 NVMEM 서브시스템은 여러 tracepoint를 제공합니다:

# Tracepoint 활성화
echo '*nvmem*:p' > /sys/kernel/debug/tracing/set_event

# 읽기 추적
cat /sys/kernel/debug/tracing/trace_pipe | grep nvmem

# 특정 provider 추적
echo 'nvmem:nvmem_device_read devname==board-eeprom' > \
    /sys/kernel/debug/tracing/events/nvmem/nvmem_device_read/enable

ftrace로 NVMEM 읽기 시각화

# ftrace로 읽기 지연 측정
echo "function_graph" > /sys/kernel/debug/tracing/current_tracer

echo 'nvmem*:*' > /sys/kernel/debug/tracing/set_ftrace_filter

echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 1
echo 0 > /sys/kernel/debug/tracing/tracing_on

cat /sys/kernel/debug/tracing/trace

일반적인 함정과 설계 실수

NVMEM을 사용할 때 흔히 저지르는 실수들을 정리합니다.

실수결과해결책
offset 하드코딩EEPROM 용량 변경 시 consumer 전체 수정cell 이름 사용, DT로 추상화
endian 미지정다양한 플랫폼에서 다른 값 해석명시적 typed helper 사용
버전 필드 누락파생 제품에서 호환성 파괴magic + version + length + CRC 구조
keepout 미설정reserved 영역 오염provider 등록 시 keepout 정의
raw sysfs 무제한 개방보안 취약점, 의도치 않은 쓰기root_only, read_only 기본 적용
cell 중복 이름consumer가 잘못된 cell 참조provider당 고유 이름 보장
flash erase 무시데이터 손상, wear outerase block 단위 쓰기, redundancy
쓰기 원자성 미고려전원 이상 시 partially written 상태atomic update, journal, backup 복사본

향후 확장: NVMEM의 발전 방향

NVMEM 프레임워크는 계속 진화하고 있습니다. 커널 Mailing List와 patches에서 볼 수 있는 향후 확장 방향:

보안 강화

성능 개선

새로운 백엔드 지원