[RFC PATCH] tools: nvmem: add nvmemctl for userspace NVMEM device management
From: Kuan-Wei Chiu
Date: Sun Mar 15 2026 - 14:18:37 EST
Introduce nvmemctl, a userspace CLI tool designed to cleanly interact
with the NVMEM subsystem. It eliminates the need for manual sysfs
traversal and raw binary parsing by providing a standard interface
strictly based on the documented ABIs:
- Documentation/ABI/stable/sysfs-bus-nvmem
- Documentation/ABI/testing/sysfs-nvmem-cells
Supported operations:
- list: Discovers and displays NVMEM devices, types, read-only
status, sizes, and device tree cells in a tree topology.
- dump: Safely hexdumps the entire binary content of a device.
- read: Parses the cells directory and hexdumps a specific cell.
- lock/unlock: Toggles the force_ro attribute to prevent or
allow writes.
Signed-off-by: Kuan-Wei Chiu <visitorckw@xxxxxxxxx>
---
MAINTAINERS | 6 +
tools/Makefile | 17 ++-
tools/nvmem/Makefile | 22 +++
tools/nvmem/nvmemctl.c | 340 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 382 insertions(+), 3 deletions(-)
create mode 100644 tools/nvmem/Makefile
create mode 100644 tools/nvmem/nvmemctl.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 61bf550fd37c..4bc40b6fe4ff 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18998,6 +18998,12 @@ F: include/dt-bindings/nvmem/
F: include/linux/nvmem-consumer.h
F: include/linux/nvmem-provider.h
+NVMEM TOOL
+M: Kuan-Wei Chiu <visitorckw@xxxxxxxxx>
+L: linux-kernel@xxxxxxxxxxxxxxx
+S: Maintained
+F: tools/nvmem/
+
NXP BLUETOOTH WIRELESS DRIVERS
M: Amitkumar Karwar <amitkumar.karwar@xxxxxxx>
M: Neeraj Kale <neeraj.sanjaykale@xxxxxxx>
diff --git a/tools/Makefile b/tools/Makefile
index cb40961a740f..59b2f245dd39 100644
--- a/tools/Makefile
+++ b/tools/Makefile
@@ -43,6 +43,7 @@ help:
@echo ' wmi - WMI interface examples'
@echo ' x86_energy_perf_policy - Intel energy policy tool'
@echo ' ynl - ynl headers, library, and python tool'
+ @echo ' nvmem - NVMEM tools'
@echo ''
@echo 'You can do:'
@echo ' $$ make -C tools/ <tool>_install'
@@ -123,11 +124,14 @@ kvm_stat: FORCE
ynl: FORCE
$(call descend,net/ynl)
+nvmem: FORCE
+ $(call descend,nvmem)
+
all: acpi counter cpupower dma gpio hv firewire \
perf selftests bootconfig spi turbostat usb \
virtio mm bpf x86_energy_perf_policy \
tmon freefall iio objtool kvm_stat wmi \
- debugging tracing thermal thermometer thermal-engine ynl
+ debugging tracing thermal thermometer thermal-engine ynl nvmem
acpi_install:
$(call descend,power/$(@:_install=),install)
@@ -165,13 +169,17 @@ kvm_stat_install:
ynl_install:
$(call descend,net/$(@:_install=),install)
+nvmem_install:
+ $(call descend,nvmem,install)
+
install: acpi_install counter_install cpupower_install dma_install gpio_install \
hv_install firewire_install iio_install \
perf_install selftests_install turbostat_install usb_install \
virtio_install mm_install bpf_install x86_energy_perf_policy_install \
tmon_install freefall_install objtool_install kvm_stat_install \
wmi_install debugging_install intel-speed-select_install \
- tracing_install thermometer_install thermal-engine_install ynl_install
+ tracing_install thermometer_install thermal-engine_install ynl_install \
+ nvmem_install
acpi_clean:
$(call descend,power/acpi,clean)
@@ -225,12 +233,15 @@ build_clean:
ynl_clean:
$(call descend,net/$(@:_clean=),clean)
+nvmem_clean:
+ $(call descend,nvmem,clean)
+
clean: acpi_clean counter_clean cpupower_clean dma_clean hv_clean firewire_clean \
perf_clean selftests_clean turbostat_clean bootconfig_clean spi_clean usb_clean virtio_clean \
mm_clean bpf_clean iio_clean x86_energy_perf_policy_clean tmon_clean \
freefall_clean build_clean libbpf_clean libsubcmd_clean \
gpio_clean objtool_clean leds_clean wmi_clean firmware_clean debugging_clean \
intel-speed-select_clean tracing_clean thermal_clean thermometer_clean thermal-engine_clean \
- sched_ext_clean ynl_clean
+ sched_ext_clean ynl_clean nvmem_clean
.PHONY: FORCE
diff --git a/tools/nvmem/Makefile b/tools/nvmem/Makefile
new file mode 100644
index 000000000000..7ef25f616f7a
--- /dev/null
+++ b/tools/nvmem/Makefile
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: GPL-2.0-only
+include ../scripts/Makefile.include
+
+CC ?= $(CROSS_COMPILE)gcc
+CFLAGS += -Wall -Wextra -O2
+
+PROG := nvmemctl
+SRCS := nvmemctl.c
+
+all: $(PROG)
+
+$(PROG): $(SRCS)
+ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
+
+install: $(PROG)
+ install -d $(DESTDIR)$(PREFIX)/bin
+ install -m 755 $(PROG) $(DESTDIR)$(PREFIX)/bin/
+
+clean:
+ rm -f $(PROG)
+
+.PHONY: all install clean
diff --git a/tools/nvmem/nvmemctl.c b/tools/nvmem/nvmemctl.c
new file mode 100644
index 000000000000..cec70f99dc3d
--- /dev/null
+++ b/tools/nvmem/nvmemctl.c
@@ -0,0 +1,340 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * nvmemctl - Userspace tool to list and access NVMEM devices
+ *
+ * Copyright (C) 2026 Kuan-Wei Chiu <visitorckw@xxxxxxxxx>
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <dirent.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <limits.h>
+
+#define NVMEM_SYSPATH "/sys/bus/nvmem/devices"
+
+struct nvmem_cell {
+ char name[256];
+ unsigned int byte_offset;
+ unsigned int bit_offset;
+ size_t size;
+ struct nvmem_cell *next;
+};
+
+struct nvmem_device {
+ char name[256];
+ char type[32];
+ bool force_ro;
+ size_t size;
+ struct nvmem_cell *cells;
+ struct nvmem_device *next;
+};
+
+static int read_sysfs_string(const char *dev_name, const char *attr, char *buf, size_t buf_size)
+{
+ char path[PATH_MAX];
+ size_t len;
+ FILE *f;
+
+ snprintf(path, sizeof(path), "%s/%s/%s", NVMEM_SYSPATH, dev_name, attr);
+ f = fopen(path, "r");
+ if (!f)
+ return -1;
+
+ if (fgets(buf, buf_size, f)) {
+ len = strlen(buf);
+ if (len > 0 && buf[len - 1] == '\n')
+ buf[len - 1] = '\0';
+ } else {
+ buf[0] = '\0';
+ }
+
+ fclose(f);
+ return 0;
+}
+
+static int write_sysfs_string(const char *dev_name, const char *attr, const char *val)
+{
+ char path[PATH_MAX];
+ int fd, ret;
+
+ snprintf(path, sizeof(path), "%s/%s/%s", NVMEM_SYSPATH, dev_name, attr);
+ fd = open(path, O_WRONLY);
+ if (fd < 0) {
+ fprintf(stderr, "Failed to open %s for writing: %s\n", path, strerror(errno));
+ return -1;
+ }
+
+ ret = write(fd, val, strlen(val));
+ close(fd);
+
+ if (ret < 0) {
+ fprintf(stderr, "Failed to write to %s: %s\n", path, strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+static struct nvmem_cell *scan_nvmem_cells(const char *dev_name)
+{
+ char cells_path[PATH_MAX];
+ DIR *dp;
+ struct dirent *entry;
+ struct nvmem_cell *head = NULL, *tail = NULL;
+
+ snprintf(cells_path, sizeof(cells_path), "%s/%s/cells", NVMEM_SYSPATH, dev_name);
+ dp = opendir(cells_path);
+ if (!dp)
+ return NULL;
+
+ while ((entry = readdir(dp)) != NULL) {
+ struct nvmem_cell *cell;
+ char cell_file_path[PATH_MAX];
+ struct stat st;
+ int parsed;
+
+ if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
+ continue;
+
+ cell = calloc(1, sizeof(*cell));
+ if (!cell)
+ break;
+
+ parsed = sscanf(entry->d_name, "%[^@]@%x,%x", cell->name,
+ &cell->byte_offset, &cell->bit_offset);
+ if (parsed < 2) {
+ snprintf(cell->name, sizeof(cell->name), "%s", entry->d_name);
+ cell->byte_offset = 0;
+ cell->bit_offset = 0;
+ } else if (parsed == 2) {
+ cell->bit_offset = 0;
+ }
+
+ snprintf(cell_file_path, sizeof(cell_file_path), "%s/%s/cells/%s",
+ NVMEM_SYSPATH, dev_name, entry->d_name);
+ if (stat(cell_file_path, &st) == 0)
+ cell->size = st.st_size;
+
+ if (!head) {
+ head = cell;
+ tail = cell;
+ } else {
+ tail->next = cell;
+ tail = cell;
+ }
+ }
+ closedir(dp);
+ return head;
+}
+
+static struct nvmem_device *scan_nvmem_devices(void)
+{
+ DIR *dp;
+ struct dirent *entry;
+ struct nvmem_device *head = NULL, *tail = NULL;
+
+ dp = opendir(NVMEM_SYSPATH);
+ if (!dp) {
+ perror("Failed to open NVMEM sysfs directory");
+ return NULL;
+ }
+
+ while ((entry = readdir(dp)) != NULL) {
+ struct nvmem_device *dev;
+ char ro_buf[8] = {0};
+ char nvmem_file_path[PATH_MAX];
+ struct stat st;
+
+ if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
+ continue;
+
+ dev = calloc(1, sizeof(*dev));
+ if (!dev)
+ break;
+
+ snprintf(dev->name, sizeof(dev->name), "%s", entry->d_name);
+
+ if (read_sysfs_string(dev->name, "type", dev->type, sizeof(dev->type)) < 0)
+ strncpy(dev->type, "Unknown", sizeof(dev->type));
+
+ if (read_sysfs_string(dev->name, "force_ro", ro_buf, sizeof(ro_buf)) == 0)
+ dev->force_ro = (ro_buf[0] == '1');
+
+ snprintf(nvmem_file_path, sizeof(nvmem_file_path), "%s/%s/nvmem",
+ NVMEM_SYSPATH, dev->name);
+ if (stat(nvmem_file_path, &st) == 0)
+ dev->size = st.st_size;
+
+ dev->cells = scan_nvmem_cells(dev->name);
+
+ if (!head) {
+ head = dev;
+ tail = dev;
+ } else {
+ tail->next = dev;
+ tail = dev;
+ }
+ }
+ closedir(dp);
+ return head;
+}
+
+static void free_nvmem_devices(struct nvmem_device *devices)
+{
+ struct nvmem_device *tmp_dev;
+ struct nvmem_cell *c, *tmp_c;
+
+ while (devices) {
+ tmp_dev = devices;
+ c = devices->cells;
+
+ while (c) {
+ tmp_c = c;
+ c = c->next;
+ free(tmp_c);
+ }
+
+ devices = devices->next;
+ free(tmp_dev);
+ }
+}
+
+static void dump_binary_data(const char *path, const char *title)
+{
+ int fd, i;
+ unsigned char buf[16];
+ ssize_t bytes_read;
+ size_t offset = 0;
+
+ fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno));
+ return;
+ }
+
+ printf("Dumping %s:\n", title);
+ printf("Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F | ASCII\n");
+ printf("------------------------------------------------------------------\n");
+
+ while ((bytes_read = read(fd, buf, sizeof(buf))) > 0) {
+ printf("%08zX ", offset);
+
+ for (i = 0; i < 16; i++) {
+ if (i < bytes_read)
+ printf("%02X ", buf[i]);
+ else
+ printf(" ");
+ }
+ printf(" | ");
+ for (i = 0; i < bytes_read; i++) {
+ if (buf[i] >= 32 && buf[i] <= 126)
+ printf("%c", buf[i]);
+ else
+ printf(".");
+ }
+ printf("\n");
+ offset += bytes_read;
+ }
+
+ if (bytes_read < 0)
+ perror("Error reading file");
+
+ printf("------------------------------------------------------------------\n");
+ close(fd);
+}
+
+static void cmd_list(void)
+{
+ struct nvmem_device *devices, *current;
+ struct nvmem_cell *c;
+
+ printf("%-15s | %-15s | %-5s | %-8s\n", "Device", "Type", "R/O", "Size(B)");
+ printf("------------------------------------------------------------------\n");
+
+ devices = scan_nvmem_devices();
+ if (!devices) {
+ printf("No NVMEM devices found.\n");
+ return;
+ }
+
+ for (current = devices; current; current = current->next) {
+ printf("%-15s | %-15s | %-5s | %-8zu\n",
+ current->name, current->type,
+ current->force_ro ? "Yes" : "No", current->size);
+
+ for (c = current->cells; c; c = c->next) {
+ printf(" |- Cell: %-15s (Offset: 0x%04X, Bit: %d, Size: %zu B)\n",
+ c->name, c->byte_offset, c->bit_offset, c->size);
+ }
+ }
+ printf("------------------------------------------------------------------\n");
+ free_nvmem_devices(devices);
+}
+
+static void print_usage(const char *progname)
+{
+ printf("Usage: %s <command> [args]\n\n", progname);
+ printf("Commands:\n");
+ printf(" list List all NVMEM devices and cells\n");
+ printf(" dump <device> Hexdump the entire NVMEM device\n");
+ printf(" read <device> <cell_name> Hexdump a specific cell within a device\n");
+ printf(" lock <device> Set device read-only (force_ro=1)\n");
+ printf(" unlock <device> Set device read-write (force_ro=0)\n");
+}
+
+int main(int argc, char *argv[])
+{
+ char path[PATH_MAX];
+ DIR *dp;
+ struct dirent *entry;
+ int found = 0;
+
+ if (argc < 2) {
+ print_usage(argv[0]);
+ return 1;
+ }
+
+ if (!strcmp(argv[1], "list")) {
+ cmd_list();
+ } else if (!strcmp(argv[1], "dump") && argc == 3) {
+ snprintf(path, sizeof(path), "%s/%s/nvmem", NVMEM_SYSPATH, argv[2]);
+ dump_binary_data(path, argv[2]);
+ } else if (!strcmp(argv[1], "read") && argc == 4) {
+ snprintf(path, sizeof(path), "%s/%s/cells", NVMEM_SYSPATH, argv[2]);
+ dp = opendir(path);
+ if (!dp) {
+ fprintf(stderr, "Could not open cells directory for %s\n", argv[2]);
+ return 1;
+ }
+
+ while ((entry = readdir(dp)) != NULL) {
+ if (strncmp(entry->d_name, argv[3], strlen(argv[3])) == 0) {
+ snprintf(path, sizeof(path), "%s/%s/cells/%s",
+ NVMEM_SYSPATH, argv[2], entry->d_name);
+ dump_binary_data(path, argv[3]);
+ found = 1;
+ break;
+ }
+ }
+ closedir(dp);
+ if (!found)
+ fprintf(stderr, "Cell '%s' not found in device '%s'\n", argv[3], argv[2]);
+
+ } else if (!strcmp(argv[1], "lock") && argc == 3) {
+ if (write_sysfs_string(argv[2], "force_ro", "1") == 0)
+ printf("Successfully locked %s (Read-Only)\n", argv[2]);
+ } else if (!strcmp(argv[1], "unlock") && argc == 3) {
+ if (write_sysfs_string(argv[2], "force_ro", "0") == 0)
+ printf("Successfully unlocked %s (Read-Write)\n", argv[2]);
+ } else {
+ print_usage(argv[0]);
+ return 1;
+ }
+
+ return 0;
+}
--
2.53.0.851.ga537e3e6e9-goog