From nobody Tue Apr 7 05:41:11 2026 Received: from mail-pj1-f46.google.com (mail-pj1-f46.google.com [209.85.216.46]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 42AD81684B4 for ; Sun, 15 Mar 2026 18:18:19 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.216.46 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1773598700; cv=none; b=BTrYvQC+O/DXtClZW9bPIgXc4X19aG6Ke0eBGehJj1yxrsH//uWPGvOQbd6b0kZF1YNFefhimIZrNa6XoiGNVpCkBieVfcTfGbXDxERkvIOb+Rlv31yI72iCLWtJLvU/jVAFXfZ3uqirXUjojSM6sAhl9ZY0Tc9iMc3fO202hPs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1773598700; c=relaxed/simple; bh=nyeoLmIhBXwZ5ZxG0+gzb6NHoffEdZzHjXdoozleKkE=; h=From:To:Cc:Subject:Date:Message-ID:MIME-Version; b=pRmM+7rvXXq1NUz5PthPvjZwicxL+rqJKdSmbwvJfbsYIxIJ+6W8Z5SdKXjoxIYQ2kIcb/nsfUWcP2Fw6HhuQwK2/LLN6M++hg3L6BJJwMDVPMpUJsK3Xpx/8gOiT0DT1a9xvaAbmNxjqJiZYlP1GtQf2gzPklFzv5CN9etnJqw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=jKKlloOm; arc=none smtp.client-ip=209.85.216.46 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="jKKlloOm" Received: by mail-pj1-f46.google.com with SMTP id 98e67ed59e1d1-359ff894f0dso1339699a91.1 for ; Sun, 15 Mar 2026 11:18:19 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1773598699; x=1774203499; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to; bh=xiIhy2MgNXMZB+v5tYjFioljWukKWzp7iQ1ApZGogkI=; b=jKKlloOmfD91Drj8WwZL7W8C5vB0ftHecNju1mN8cvJUPukNjuz6wemgy4S48Vdcmw Xw8FXSjsWIdk+KZuhBu6PCIpftu4Fs11/uBzewUccbxy1lrl9En3RE10BunVAUPmcEto eMH8H2WogjBblTEiW1YfzPCFQq0iM9ByArYb4Om00u1HPHJaGJKmhpIkTJmUbD9TENVw WHWsFis6CphZnGxTXjhXl0ADwli770j3cUP/HlR3q7F05XTuwfOeb4esfArmFbzc7YCI oyoEJ4bn3AWfBoWI1Cx7GqEhFy9oGGOHOe5bPOfQPtkhtIfi8ZlVzNCxoWg/eXaCzDsQ 6/IA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1773598699; x=1774203499; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=xiIhy2MgNXMZB+v5tYjFioljWukKWzp7iQ1ApZGogkI=; b=c4P5F23ozptWnWHdQ9vlOIJEHa3TFQy0WpM3qwtvJ7CIXzVxSWJIpI50DaDNVqU1jJ DHg8zr5k6mj1xb8vJeejrHEbJPs4lNOxkcUdITeB46O0Rc8EEtdybH7Sg2A1krOV7fc5 IxjL17B+yxJbGefdnTXxDl3Mfin3XYA8aIhePnOwuk7Gx99+qtg/Lov9W6GxNVPWC6Hl mIK7RbIXzbdl/fKanxYIK1/PSPAZklpuKK8VNz2VdKdj7dzYpDY7C/FQHQEDZwJ9vkZk rYZ5pNQcHWIidfSkMGoCgpoKPEHkWTEUMlQOeq5SRKd518PpMtQhTqC4CmwB9ygZGyo2 gaWA== X-Forwarded-Encrypted: i=1; AJvYcCWbaFcpfXRCeb4iIx19xL2HgqGY9RYcar8LTPmkxClPCw1jrWTwISUv2/EzjlI1IuYrUTctIWC9gB1GQ5E=@vger.kernel.org X-Gm-Message-State: AOJu0YwtCInOxKcJ4C1BmCbg3YQyzgW5M+hWEP/DiJNRs4AwSOEje7b9 HUzfT5ng+qbq/wZ4HwyzYvgZVj5ab0u7AdG6dIS97fLYrXFxvcgrnq5cKiEPDQ== X-Gm-Gg: ATEYQzz92UEhaxIGMK9ogtjBH1munIjE/QEtcso2oxl7oBN7BZ0GCq1aAVkmsIStrxd HlBrr12YoTDzCLQKs8ftZcFIR9UmyDDilxJVYTvjMlsG4s7BV7qzEsOJLVmDoXPyD5kqIIWWG4v 1TIH9a+++D2NSGHYnYMtfBBcIgtgLL8lBeph+oM5RVo1q8JH3e3vVpgMsmH16AH43nVsOiOn4KD 4xq/oLqRFQ4ET2MpddQX5mJkNBqsiX2HxQ3ayVofInvOvXEeA7FTdSBdoxhkRvo5mPTdxOgzJ3Q y9orFVSibUlV/3r52qUnTfichqgSe8iJFBsx7aMqtFFXKLYXB9iQxRkHpB19NrxBbE2ynlcMfsr mFN1X6fUQV4eX6iYVVehJYG95shbvMMXHLPGK0McZY0IerBxNLX1GUK7TQr3u6EZoftNEaw+BEO PV2GhOqoayh+jVle9jdZwFoQncDduIFs/nJCj2NY2KbUWZLPPRNl8NTL9gxlNXiCY0COleadlj8 YwKiHMbRsuQW0iMLh4rEf3EzbFMfXao20Zg X-Received: by 2002:a17:90b:38c1:b0:359:fecd:1cb6 with SMTP id 98e67ed59e1d1-35a21cb4b21mr9220668a91.0.1773598698533; Sun, 15 Mar 2026 11:18:18 -0700 (PDT) Received: from visitorckw-work01.c.googlers.com.com (7.162.199.104.bc.googleusercontent.com. [104.199.162.7]) by smtp.gmail.com with ESMTPSA id 98e67ed59e1d1-35b8e1cb560sm2036358a91.1.2026.03.15.11.18.15 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 15 Mar 2026 11:18:17 -0700 (PDT) From: Kuan-Wei Chiu To: srini@kernel.org Cc: akpm@linux-foundation.org, jserv@ccns.ncku.edu.tw, eleanor15x@gmail.com, linux-kernel@vger.kernel.org, Kuan-Wei Chiu Subject: [RFC PATCH] tools: nvmem: add nvmemctl for userspace NVMEM device management Date: Sun, 15 Mar 2026 18:18:02 +0000 Message-ID: <20260315181802.61399-1-visitorckw@gmail.com> X-Mailer: git-send-email 2.53.0.851.ga537e3e6e9-goog Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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 --- 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 =20 +NVMEM TOOL +M: Kuan-Wei Chiu +L: linux-kernel@vger.kernel.org +S: Maintained +F: tools/nvmem/ + NXP BLUETOOTH WIRELESS DRIVERS M: Amitkumar Karwar M: Neeraj Kale 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/ _install' @@ -123,11 +124,14 @@ kvm_stat: FORCE ynl: FORCE $(call descend,net/ynl) =20 +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 =20 acpi_install: $(call descend,power/$(@:_install=3D),install) @@ -165,13 +169,17 @@ kvm_stat_install: ynl_install: $(call descend,net/$(@:_install=3D),install) =20 +nvmem_install: + $(call descend,nvmem,install) + install: acpi_install counter_install cpupower_install dma_install gpio_in= stall \ 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 =20 acpi_clean: $(call descend,power/acpi,clean) @@ -225,12 +233,15 @@ build_clean: ynl_clean: $(call descend,net/$(@:_clean=3D),clean) =20 +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 us= b_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_c= lean \ intel-speed-select_clean tracing_clean thermal_clean thermometer_clean t= hermal-engine_clean \ - sched_ext_clean ynl_clean + sched_ext_clean ynl_clean nvmem_clean =20 .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 ?=3D $(CROSS_COMPILE)gcc +CFLAGS +=3D -Wall -Wextra -O2 + +PROG :=3D nvmemctl +SRCS :=3D 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 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 =3D fopen(path, "r"); + if (!f) + return -1; + + if (fgets(buf, buf_size, f)) { + len =3D strlen(buf); + if (len > 0 && buf[len - 1] =3D=3D '\n') + buf[len - 1] =3D '\0'; + } else { + buf[0] =3D '\0'; + } + + fclose(f); + return 0; +} + +static int write_sysfs_string(const char *dev_name, const char *attr, cons= t char *val) +{ + char path[PATH_MAX]; + int fd, ret; + + snprintf(path, sizeof(path), "%s/%s/%s", NVMEM_SYSPATH, dev_name, attr); + fd =3D open(path, O_WRONLY); + if (fd < 0) { + fprintf(stderr, "Failed to open %s for writing: %s\n", path, strerror(er= rno)); + return -1; + } + + ret =3D 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 =3D NULL, *tail =3D NULL; + + snprintf(cells_path, sizeof(cells_path), "%s/%s/cells", NVMEM_SYSPATH, de= v_name); + dp =3D opendir(cells_path); + if (!dp) + return NULL; + + while ((entry =3D readdir(dp)) !=3D 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 =3D calloc(1, sizeof(*cell)); + if (!cell) + break; + + parsed =3D 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 =3D 0; + cell->bit_offset =3D 0; + } else if (parsed =3D=3D 2) { + cell->bit_offset =3D 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) =3D=3D 0) + cell->size =3D st.st_size; + + if (!head) { + head =3D cell; + tail =3D cell; + } else { + tail->next =3D cell; + tail =3D cell; + } + } + closedir(dp); + return head; +} + +static struct nvmem_device *scan_nvmem_devices(void) +{ + DIR *dp; + struct dirent *entry; + struct nvmem_device *head =3D NULL, *tail =3D NULL; + + dp =3D opendir(NVMEM_SYSPATH); + if (!dp) { + perror("Failed to open NVMEM sysfs directory"); + return NULL; + } + + while ((entry =3D readdir(dp)) !=3D NULL) { + struct nvmem_device *dev; + char ro_buf[8] =3D {0}; + char nvmem_file_path[PATH_MAX]; + struct stat st; + + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) + continue; + + dev =3D 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)) =3D= =3D 0) + dev->force_ro =3D (ro_buf[0] =3D=3D '1'); + + snprintf(nvmem_file_path, sizeof(nvmem_file_path), "%s/%s/nvmem", + NVMEM_SYSPATH, dev->name); + if (stat(nvmem_file_path, &st) =3D=3D 0) + dev->size =3D st.st_size; + + dev->cells =3D scan_nvmem_cells(dev->name); + + if (!head) { + head =3D dev; + tail =3D dev; + } else { + tail->next =3D dev; + tail =3D 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 =3D devices; + c =3D devices->cells; + + while (c) { + tmp_c =3D c; + c =3D c->next; + free(tmp_c); + } + + devices =3D 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 =3D 0; + + fd =3D 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 =3D read(fd, buf, sizeof(buf))) > 0) { + printf("%08zX ", offset); + + for (i =3D 0; i < 16; i++) { + if (i < bytes_read) + printf("%02X ", buf[i]); + else + printf(" "); + } + printf(" | "); + for (i =3D 0; i < bytes_read; i++) { + if (buf[i] >=3D 32 && buf[i] <=3D 126) + printf("%c", buf[i]); + else + printf("."); + } + printf("\n"); + offset +=3D 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 =3D scan_nvmem_devices(); + if (!devices) { + printf("No NVMEM devices found.\n"); + return; + } + + for (current =3D devices; current; current =3D current->next) { + printf("%-15s | %-15s | %-5s | %-8zu\n", + current->name, current->type, + current->force_ro ? "Yes" : "No", current->size); + + for (c =3D current->cells; c; c =3D 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 [args]\n\n", progname); + printf("Commands:\n"); + printf(" list List all NVMEM devices and cells\= n"); + printf(" dump Hexdump the entire NVMEM device\n= "); + printf(" read Hexdump a specific cell within a = device\n"); + printf(" lock Set device read-only (force_ro=3D= 1)\n"); + printf(" unlock Set device read-write (force_ro= =3D0)\n"); +} + +int main(int argc, char *argv[]) +{ + char path[PATH_MAX]; + DIR *dp; + struct dirent *entry; + int found =3D 0; + + if (argc < 2) { + print_usage(argv[0]); + return 1; + } + + if (!strcmp(argv[1], "list")) { + cmd_list(); + } else if (!strcmp(argv[1], "dump") && argc =3D=3D 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 =3D=3D 4) { + snprintf(path, sizeof(path), "%s/%s/cells", NVMEM_SYSPATH, argv[2]); + dp =3D opendir(path); + if (!dp) { + fprintf(stderr, "Could not open cells directory for %s\n", argv[2]); + return 1; + } + + while ((entry =3D readdir(dp)) !=3D NULL) { + if (strncmp(entry->d_name, argv[3], strlen(argv[3])) =3D=3D 0) { + snprintf(path, sizeof(path), "%s/%s/cells/%s", + NVMEM_SYSPATH, argv[2], entry->d_name); + dump_binary_data(path, argv[3]); + found =3D 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 =3D=3D 3) { + if (write_sysfs_string(argv[2], "force_ro", "1") =3D=3D 0) + printf("Successfully locked %s (Read-Only)\n", argv[2]); + } else if (!strcmp(argv[1], "unlock") && argc =3D=3D 3) { + if (write_sysfs_string(argv[2], "force_ro", "0") =3D=3D 0) + printf("Successfully unlocked %s (Read-Write)\n", argv[2]); + } else { + print_usage(argv[0]); + return 1; + } + + return 0; +} --=20 2.53.0.851.ga537e3e6e9-goog