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을 실제 커널 사용 패턴에 맞춰 최대한 자세히 설명합니다.
핵심 요약
- 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, 원샷 프로비저닝 여부를 처음부터 문서화하지 않으면 제품 파생이 늘수록 유지보수가 폭발합니다.
단계별 이해
- 매체와 의미를 분리한다
먼저 "EEPROM인가 efuse인가"보다 "안에 무엇이 들어 있나"를 생각합니다. - 필요한 데이터를 cell로 자른다
MAC 주소, serial, board-id, calibration처럼 소비자가 실제로 원하는 의미 단위로 나눕니다. - 소비자와 연결한다
DT면nvmem-cells, non-DT면 lookup table, 특수 포맷이면 layout parser로 소비자와 연결합니다. - raw 읽기/쓰기를 마지막 수단으로 둔다
raw sysfs는 bring-up과 공장 도구에 유용하지만, 일반 드라이버 경로는 cell 기반이 더 안전합니다. - 수명과 불변식을 먼저 정한다
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을 읽는 기본 관점: 저장소 전체, 의미 단위, 파싱 계층
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가 둘 다 있는가"도 자연스럽게 이해됩니다. 공장 도구나 특수 드라이버는 저장소 전체를 다뤄야 할 수 있지만, 대부분의 소비자는 의미 단위만 필요합니다.
- Provider는 바이트를 책임진다 — word size, stride, keepout, read-only 여부, 물리적 쓰기 제약을 모델링합니다.
- Cell은 의미를 책임진다 — 이름, 길이, bit offset, post-process를 통해 "이 바이트가 무엇인지"를 정의합니다.
- Layout은 구조를 책임진다 — 셀이 고정 오프셋이 아닌 포맷 기반일 때 runtime parsing으로 cell을 만듭니다.
이 세 층을 섞으면 문제가 생깁니다. 예를 들어 이더넷 드라이버가 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, id | provider 이름과 ID | NVMEM_DEVID_NONE, NVMEM_DEVID_AUTO 사용 가능 |
dev, owner | device model과 모듈 refcount | 대개 probe의 &pdev->dev와 THIS_MODULE |
reg_read, reg_write | 실제 바이트 read/write 콜백 | provider의 본질 |
size | 저장소 전체 크기 | raw sysfs ABI 크기와 직결 |
word_size | 최소 접근 단위 | 예: 1, 2, 4 bytes |
stride | 최소 접근 stride | 홀수 offset 금지 같은 제약을 모델링 |
type | EEPROM/OTP/BATTERY_BACKED/FRAM 등 | sysfs type와 연결되는 의미 |
read_only | device-level write 금지 | OTP/efuse, read-only factory ROM에 적합 |
root_only | root만 접근 허용 | 민감 정보가 있는 provider |
ignore_wp | WP 핀을 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_size와 stride는 자주 생략되지만 중요합니다. 예를 들어 하드웨어가 4바이트 단위 접근만 허용하거나 2바이트 정렬을 요구한다면, 이 제약을 NVMEM 코어에 알려야 잘못된 raw access를 막을 수 있습니다.
Cell 모델: offset보다 의미를 앞세우는 방법
Cell은 NVMEM 프레임워크의 가장 중요한 개념입니다. struct nvmem_cell_info는 단순히 offset + bytes만 담지 않고, raw_len, bit_offset, nbits, read_post_process 같은 필드도 가집니다. 즉, cell은 "저장소 일부를 읽는다"가 아니라, 의미 있는 데이터를 잘라내고 필요하면 해석까지 붙인다는 개념입니다.
| 필드 | 의미 | 언제 중요해지나 |
|---|---|---|
name | cell 이름 | consumer는 이 이름으로 요청 |
offset | 저장소 내 시작 위치 | 고정 레이아웃일 때 핵심 |
raw_len | post-process 전 raw 길이 | 헤더/CRC를 포함해 읽은 뒤 가공하는 경우 |
bytes | 최종 cell 길이 | consumer가 기대하는 결과 길이 |
bit_offset, nbits | 비트 단위 field | revision 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 정의에서 분리하는 것이 훨씬 낫습니다.
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 read | provider 세부 레이아웃 누수 위험 |
nvmem_device_write() | offset 기반 raw write | OTP/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_id와 con_id에 맞는 cell을 찾아 연결합니다.
Provider 심화: type, keepout, stride, root_only, legacy OF cells
헤더에는 NVMEM의 실전 운용에서 중요한 추가 옵션이 여럿 들어 있습니다.
| 옵션 | 의미 | 실무 해석 |
|---|---|---|
enum nvmem_type | UNKNOWN, EEPROM, OTP, BATTERY_BACKED, FRAM | 매체 성격과 userspace 관찰 정보 제공 |
keepout | 읽기/쓰기를 피할 영역 | reserved fuse, ECC, secure region, broken bytes 가리기 |
root_only | root만 접근 가능 | raw sysfs로 민감 정보가 유출되면 안 되는 장치 |
read_only | 쓰기 금지 | OTP/efuse/ROM 성격의 provider |
ignore_wp | write protect 핀을 provider가 직접 관리 | WP GPIO를 제어해야 쓰기가 가능한 EEPROM |
add_legacy_fixed_of_cells | 예전 OF cell 문법 지원 | 새 binding보다 하위 호환 유지가 필요할 때 |
fixup_dt_cell_info | DT에서 읽은 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_only와 read_only는 단순 권한 설정 이상입니다. raw sysfs ABI는 사용자가 전체 바이트열을 그대로 읽고 쓸 수 있기 때문에, provider가 민감하거나 되돌리기 어려운 매체라면 초기에 이 플래그를 엄격하게 잡는 것이 안전합니다. 이 설명은 헤더와 ABI 문서를 종합한 실무적 해석입니다.
Device Tree binding: nvmem-cells, nvmem-cell-names, 고정 cell 정의
DT 기반 시스템에서 NVMEM의 핵심은 provider 노드 아래에 cell을 정의하고, consumer가 nvmem-cells와 nvmem-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";
};
이 패턴의 장점은 분명합니다.
- Provider 교체가 쉽다 — EEPROM 대신 efuse나 flash partition으로 옮겨도 consumer 이름을 유지할 수 있습니다.
- 파생 보드 대응이 쉽다 — offset이 달라도 DT만 바꾸면 되는 경우가 많습니다.
- 소비자 드라이버가 단순해진다 — 하드웨어 스토리지 세부사항을 몰라도 됩니다.
이미 [mac-address.html](/mnt/public_home/work/linuxkernel/pages/mac-address.html#L3990)에서도 보듯, 네트워크 스택은 NVMEM cell을 통한 MAC 주소 제공을 중요한 프로비저닝 경로 중 하나로 취급합니다. 따라서 "mac-address" cell 이름은 사실상 현장에서 널리 공유되는 계약입니다.
NVMEM layouts와 post-process: 고정 offset만으로 부족할 때
공식 NVMEM 문서는 layout을 "runtime에 NVMEM 내용을 읽어 셀을 동적으로 추가하는 메커니즘"이라고 설명합니다. 단순 offset/length로 표현되는 고정 cell만으로는 TLV, name=value 목록, vendor header가 있는 blob, CRC 뒤에 실제 데이터가 오는 형식을 다루기 어렵기 때문입니다.
헤더에는 struct nvmem_layout와 struct 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만 보여 줄 수 있습니다.
사용자 공간 ABI: raw nvmem, force_ro, type, cells 디렉터리
공식 NVMEM 문서는 사용자 공간이 raw NVMEM 파일을 /sys/bus/nvmem/devices/*/nvmem에서 읽고 쓸 수 있다고 설명합니다. 또한 stable ABI 문서는 같은 디렉터리에 force_ro와 type이 있음을 보여 주고, testing ABI 문서는 /sys/bus/nvmem/devices/.../cells/<cell-name> 경로도 정의합니다.
| 경로 | 의미 | 출처 |
|---|---|---|
/sys/bus/nvmem/devices/<dev>/nvmem | raw binary 파일 | 공식 NVMEM 문서, stable ABI |
/sys/bus/nvmem/devices/<dev>/force_ro | raw 쓰기 허용/금지 제어용 속성 | stable ABI |
/sys/bus/nvmem/devices/<dev>/type | provider 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는 강력하지만 위험합니다.
- 장점 — bring-up, factory provisioning, hexdump, layout parser 디버깅에 매우 유용
- 단점 — cell 의미를 우회하고 전체 저장소를 건드리므로 versioning, checksum, atomic update를 깨뜨리기 쉬움
- 보안 문제 — serial, secure configuration, calibration secret이 그대로 노출될 수 있음
따라서 일반 애플리케이션이나 개별 device driver는 raw sysfs보다 cell 기반 ABI 또는 상위 전용 API를 우선하는 편이 맞습니다.
백엔드별 특성과 제약: EEPROM, efuse, OTP, FRAM, flash factory partition
NVMEM은 추상화 계층이지만, 물리 매체의 제약은 사라지지 않습니다. 이 제약을 무시하면 API는 맞는데 제품은 망가집니다.
| 백엔드 | 장점 | 제약 | NVMEM 설계 포인트 |
|---|---|---|---|
| I2C EEPROM | 범용, 저렴, 재기록 가능 | 페이지 write, write-cycle 지연, wear | raw write 남용 금지, checksum/버전 필드 필요 |
| efuse | 고유성, tamper 내성 | 대개 one-time programmable | read_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 write | factory 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 역시 실질적으로 같은 성격을 가집니다. 이미 현장에 출하된 장비가 그 포맷을 영구히 들고 다니기 때문입니다.
디버깅 절차: provider 존재, DT 매핑, raw dump, cell 길이, consumer 경로
NVMEM 문제는 보통 "드라이버가 못 읽는다"로 시작하지만, 실제 원인은 크게 다섯 가지입니다.
- provider 등록 실패
장치가 NVMEM bus에 안 올라옴 - DT / lookup 매핑 실패
consumer가 cell 이름을 못 찾음 - 길이/엔디언 오류
값은 읽었지만 잘못 해석함 - layout/parser 오류
동적 cell 생성이 틀림 - 물리 매체 제약 위반
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 주소입니다. 전체 흐름을 끝까지 따라가 보면 프레임워크의 장점이 분명해집니다.
- provider 등록
I2C EEPROM 또는 efuse 드라이버가devm_nvmem_register()로 저장소를 올립니다. - cell 정의
DT 또는 정적 cell 배열에서"mac-address"cell을 6바이트로 정의합니다. - consumer 연결
Ethernet 노드가nvmem-cells/nvmem-cell-names로"mac-address"를 참조합니다. - probe 시 읽기
이더넷 드라이버 또는 공통 helper가 NVMEM cell을 읽습니다. - 유효성 검사
길이와 유효한 MAC 형식을 확인하고, 실패하면 random MAC 또는 다른 fallback으로 내려갑니다. - 네트워크 장치에 적용
eth_hw_addr_set()같은 경로로 net_device 주소가 설정됩니다.
커널 내부 구조: 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;
};
이 구조체에서 특히 중요한 필드들을 살펴보면:
- flags:
NVMEM_FLAGS_EXTERNAL_CONFIG,NVMEM_FLAGS_EXTERNAL_OF_CELLS등의 플래그가 각 위치를 정보를 제어합니다. - lock: 동시 접근을 방지하는 mutex로, 특히 여러 consumer가 같은 provider를 사용할 때 중요합니다.
- bus: NVMEM 버스 디바이스에 대한 참조로, Reggie/reg_write를 wraps하여 추가 처리를 가능하게 합니다.
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의 핵심 특성은 다음과 같습니다:
- offset: provider 내 실제 바이트 위치입니다. DT나 lookup으로 지정된 offset을 의미합니다.
- raw_len vs bytes:
raw_len은 post-process 전 읽을 크기,bytes는 최종 consumer에게 반환될 크기입니다. - bit_offset/nbits: 비트 단위 읽기를 지원하여, 한 바이트 안에서 개별 비트를 cell로 만들 수 있습니다.
- read_post_process: 읽은 후에 호출되는 콜백으로, endian 변환, 바이트 스왑, checksum 검증 등을 수행합니다.
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는 일반적으로 다음과 같은 패턴으로 구현됩니다:
- 저장소 헤더를 읽어 포맷을 파악합니다.
- 각 항목을 파싱하여
nvmem_cell_info구조체를 채웁니다. nvmem_add_one_cell()을 호출하여 cell을 등록합니다.- 필요시 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 드라이버의 특징은:
- 보안 우선:
root_only와read_only가 기본값입니다. - efuse 특성: 일반 EEPROM과 달리 bit별 Lock이 존재합니다.
- Speed bin: SoC의 성능 등급 정보를 담고 있어 부팅 초기에 읽어야 합니다.
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 드라이버의 핵심 특징:
- 동적 크기 탐지: I2C EEPROM 용량(1KB~1MB)에 따라
size와stride가 결정됩니다. - 쓰기 제약 모델링:
stride는 페이지 크기로, 페이지 경계 쓰기 실패를 방지합니다. - wp-gpios 처리:
ignore_wp는 하드웨어 write protect를 무시하고 NVMEM 레벨에서 쓰기를 시도합니다.
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 드라이버의 특징:
- 보안 sensitive: secure boot fuse가 포함되어 있어
root_only가 기본입니다. - Keepout 세분화: 읽기/쓰기별로 다른 keepout 정책을 적용할 수 있습니다.
- Lock bits: 한 번 프로그래밍되면 읽기/쓰기가 모두 막히는 영역을 모델링합니다.
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 구조체의 flags에 NVMEM_FLAGS_CACHE를 설정하면 됩니다:
/* NVMEM 캐시 활성화 */
struct nvmem_config cfg = {
.name = "board-eeprom",
.size = 256,
/* ... */
};
/* device tree에서 cache 속성이 있으면 자동으로 적용됨 */
/* eeprom@50 { nvmem-cells = <&mac_addr>; nvmem-keep-content; }; */
캐싱이 유용한 상황:
- 빈번한 읽기: 센서 보정값처럼 매 프레임마다 읽어야 하는 경우
- EEPROM 쓰기 지연: 페이지 쓰기 주기가 긴 EEPROM에서 반복 읽기
- FPGA/ASIC 읽기 지연: I2C/SPI 버스를 거치는 경우
배치 읽기
여러 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 fuse | root_only 접근 제한 테스트 |
| ECC | Flash-based | ECC 오류 주입 후 복구 테스트 |
| 전원 이상 | 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 out | erase block 단위 쓰기, redundancy |
| 쓰기 원자성 미고려 | 전원 이상 시 partially written 상태 | atomic update, journal, backup 복사본 |
향후 확장: NVMEM의 발전 방향
NVMEM 프레임워크는 계속 진화하고 있습니다. 커널 Mailing List와 patches에서 볼 수 있는 향후 확장 방향:
보안 강화
- Encrypted NVMEM: 저장소 암호화 지원. Secure Enclave와 연동.
- Attestation: 읽은 값의 무결성 증명.
- Rate Limiting: efuse/OTP 읽기 시도 제한으로 timing 공격 방어.
성능 개선
- Zero-copy reads: 버퍼 할당 없이 직접 consumer 버퍼로 읽기.
- Prefetching: 알려진 cell 패턴의 선행 읽기.
- Async I/O: 비동기 읽기/쓰기 지원.
새로운 백엔드 지원
- CXL.mem: CXL.mem 기반 비휘발성 메모리.
- Remote NVMEM: 이더넷/USB를 통한 원격 프로비저닝.
- Security Controller: dedicated security chip 연동.
관련 문서와 참고 자료
- MTD — flash partition 기반 factory data 저장소와 erase 제약
- Device Tree —
nvmem-cells/nvmem-cell-names바인딩 - MAC 주소 — NVMEM이 실제로 많이 쓰이는 대표 사례
- I2C/SPI/GPIO — EEPROM과 외부 저장소가 붙는 일반적인 버스
- 디바이스 드라이버 — provider/consumer probe 패턴의 기본
- Power Supply — 배터리/보드 고유 정보와 NVMEM 연계가 자주 나타나는 상위 서브시스템
- Linux Kernel NVMEM Subsystem Documentation
- Devicetree ABI
- Stable sysfs ABI files
- Testing ABI files