[PATCH v3 1/7] misc: Add support for bit fields in NVMEM cells

Aswin Murugan aswin.murugan at oss.qualcomm.com
Mon Mar 30 19:14:13 CEST 2026


NVMEM cells currently only support byte-level access. Many hardware
registers pack multiple fields into single bytes, requiring bit-level
granularity. For example, Qualcomm PMIC PON registers store a 7-bit
reboot reason field within a single byte, with bit 0 reserved for other
purposes.

Add support for the optional 'bits' property in NVMEM cell device tree
bindings. This property specifies <bit_offset num_bits> to define a bit
field within the cell's register space.

Implement bit‑field handling in the driver to max u32 size

Example device tree usage:
        reboot-reason at 48 {
                reg = <0x48 0x01>;
                bits = <0x01 0x07>;  /* 7 bits starting at bit 1 */
        };

This reads bits [7:1] from the byte at offset 0x48, leaving bit 0
untouched during write operations.

Cells without the 'bits' property continue to work unchanged, ensuring
backward compatibility with existing device trees.

Signed-off-by: Aswin Murugan <aswin.murugan at oss.qualcomm.com>
---
Changes in v3:
1. Simplified bit field handling to maximum u32 size (32 bits).
2. Enforced strict size matching (size == cell->size) when nbits == 0.

---
 drivers/misc/nvmem.c | 159 +++++++++++++++++++++++++++++++++++++------
 include/nvmem.h      |   4 ++
 2 files changed, 141 insertions(+), 22 deletions(-)

diff --git a/drivers/misc/nvmem.c b/drivers/misc/nvmem.c
index 33e80858565..883ced9e898 100644
--- a/drivers/misc/nvmem.c
+++ b/drivers/misc/nvmem.c
@@ -12,55 +12,156 @@
 #include <dm/ofnode.h>
 #include <dm/read.h>
 #include <dm/uclass.h>
+#include <linux/bitops.h>
+#include <linux/kernel.h>
 
-int nvmem_cell_read(struct nvmem_cell *cell, void *buf, size_t size)
+/* Maximum supported NVMEM cell size */
+#define MAX_NVMEM_CELL_SIZE sizeof(u32)  /* 4 bytes */
+
+/**
+ * nvmem_cell_read_raw() - Read raw bytes from NVMEM cell without bit field extraction
+ * @cell: NVMEM cell to read from
+ * @buf: Buffer to store read data
+ * @size: Size of buffer
+ *
+ * This is an internal helper that reads raw bytes from hardware without applying
+ * bit field extraction. Used by both nvmem_cell_read() and nvmem_cell_write().
+ * Caller must validate buffer size before calling this function.
+ *
+ * Return: Number of bytes read on success, negative error code on failure
+ */
+static int nvmem_cell_read_raw(struct nvmem_cell *cell, void *buf, size_t size)
 {
-	dev_dbg(cell->nvmem, "%s: off=%u size=%zu\n", __func__, cell->offset, size);
-	if (size != cell->size)
-		return -EINVAL;
+	int ret;
+
+	memset(buf, 0, size);
 
 	switch (cell->nvmem->driver->id) {
 	case UCLASS_I2C_EEPROM:
-		return i2c_eeprom_read(cell->nvmem, cell->offset, buf, size);
-	case UCLASS_MISC: {
-		int ret = misc_read(cell->nvmem, cell->offset, buf, size);
-
+		ret = i2c_eeprom_read(cell->nvmem, cell->offset, buf, cell->size);
+		break;
+	case UCLASS_MISC:
+		ret = misc_read(cell->nvmem, cell->offset, buf, cell->size);
 		if (ret < 0)
 			return ret;
-		if (ret != size)
+		if (ret != cell->size)
 			return -EIO;
-		return 0;
-	}
+		ret = 0;
+		break;
 	case UCLASS_RTC:
-		return dm_rtc_read(cell->nvmem, cell->offset, buf, size);
+		ret = dm_rtc_read(cell->nvmem, cell->offset, buf, cell->size);
+		break;
 	default:
 		return -ENOSYS;
 	}
+
+	if (ret)
+		return ret;
+
+	return cell->size;
+}
+
+int nvmem_cell_read(struct nvmem_cell *cell, void *buf, size_t size)
+{
+	int ret, bytes_needed;
+	u32 value;
+
+	dev_dbg(cell->nvmem, "%s: off=%u size=%zu\n", __func__, cell->offset, size);
+
+	if ((cell->nbits && size < cell->size) || (!cell->nbits && size != cell->size)) {
+		dev_dbg(cell->nvmem, "NVMEM: buffer size %zu invalid for cell size %zu\n",
+			size, cell->size);
+		return -EINVAL;
+	}
+
+	if (cell->nbits) {
+		bytes_needed = DIV_ROUND_UP(cell->nbits + cell->bit_offset, BITS_PER_BYTE);
+
+		if (bytes_needed > cell->size || bytes_needed > MAX_NVMEM_CELL_SIZE ||
+		    size != MAX_NVMEM_CELL_SIZE) {
+			dev_dbg(cell->nvmem, "NVMEM: bit field requires %d bytes, cell size %zu, buffer size %d, got %zu\n",
+				bytes_needed, cell->size, MAX_NVMEM_CELL_SIZE, size);
+			return -EINVAL;
+		}
+	}
+
+	ret = nvmem_cell_read_raw(cell, buf, size);
+	if (ret < 0)
+		return ret;
+
+	if (cell->nbits) {
+		value = *(u32 *)buf;
+		value >>= cell->bit_offset;
+		/* Handle nbits == 32 specially to avoid undefined behavior */
+		if (cell->nbits < 32)
+			value &= (1U << cell->nbits) - 1;
+		*(u32 *)buf = value;
+	}
+
+	return 0;
 }
 
 int nvmem_cell_write(struct nvmem_cell *cell, const void *buf, size_t size)
 {
+	int ret, bytes_needed;
+	u32 current, value, mask;
+
 	dev_dbg(cell->nvmem, "%s: off=%u size=%zu\n", __func__, cell->offset, size);
-	if (size != cell->size)
+
+	if ((cell->nbits && size < cell->size) || (!cell->nbits && size != cell->size)) {
+		dev_dbg(cell->nvmem, "NVMEM: buffer size %zu invalid for cell size %zu\n",
+			size, cell->size);
 		return -EINVAL;
+	}
+
+	if (cell->nbits) {
+		bytes_needed = DIV_ROUND_UP(cell->nbits + cell->bit_offset, BITS_PER_BYTE);
+
+		if (bytes_needed > cell->size || bytes_needed > MAX_NVMEM_CELL_SIZE ||
+		    size != MAX_NVMEM_CELL_SIZE) {
+			dev_dbg(cell->nvmem, "NVMEM: bit field requires %d bytes, cell size %zu, buffer size %d, got %zu\n",
+				bytes_needed, cell->size, MAX_NVMEM_CELL_SIZE, size);
+			return -EINVAL;
+		}
+
+		/* For bit fields, perform Read-Modify-Write */
+		ret = nvmem_cell_read_raw(cell, &current, sizeof(current));
+		if (ret < 0)
+			return ret;
+
+		/* Apply masked bitfield update (shift + mask + merge) */
+		value = *(u32 *)buf;
+		value &= (1U << cell->nbits) - 1;
+		value <<= cell->bit_offset;
+
+		mask = ((1U << cell->nbits) - 1) << cell->bit_offset;
+
+		*(u32 *)buf = (current & ~mask) | value;
+	}
 
 	switch (cell->nvmem->driver->id) {
 	case UCLASS_I2C_EEPROM:
-		return i2c_eeprom_write(cell->nvmem, cell->offset, buf, size);
-	case UCLASS_MISC: {
-		int ret = misc_write(cell->nvmem, cell->offset, buf, size);
-
+		ret = i2c_eeprom_write(cell->nvmem, cell->offset, buf, cell->size);
+		break;
+	case UCLASS_MISC:
+		ret = misc_write(cell->nvmem, cell->offset, buf, cell->size);
 		if (ret < 0)
 			return ret;
-		if (ret != size)
+		if (ret != cell->size)
 			return -EIO;
-		return 0;
-	}
+		ret = 0;
+		break;
 	case UCLASS_RTC:
-		return dm_rtc_write(cell->nvmem, cell->offset, buf, size);
+		ret = dm_rtc_write(cell->nvmem, cell->offset, buf, cell->size);
+		break;
 	default:
 		return -ENOSYS;
 	}
+
+	if (ret)
+		return ret;
+
+	return 0;
 }
 
 /**
@@ -121,13 +222,27 @@ int nvmem_cell_get_by_index(struct udevice *dev, int index,
 
 	offset = ofnode_get_addr_size_index_notrans(args.node, 0, &size);
 	if (offset == FDT_ADDR_T_NONE || size == FDT_SIZE_T_NONE) {
-		dev_dbg(cell->nvmem, "missing address or size for %s\n",
+		dev_err(cell->nvmem, "missing address or size for %s\n",
 			ofnode_get_name(args.node));
 		return -EINVAL;
 	}
 
 	cell->offset = offset;
 	cell->size = size;
+
+	ret = ofnode_read_u32_index(args.node, "bits", 0, &cell->bit_offset);
+	if (ret) {
+		cell->bit_offset = 0;
+		cell->nbits = 0;
+	} else {
+		ret = ofnode_read_u32_index(args.node, "bits", 1, &cell->nbits);
+		if (ret)
+			return -EINVAL;
+
+		if (cell->bit_offset + cell->nbits > cell->size * 8)
+			return -EINVAL;
+	}
+
 	return 0;
 }
 
diff --git a/include/nvmem.h b/include/nvmem.h
index e6a8a98828b..dd82122f16f 100644
--- a/include/nvmem.h
+++ b/include/nvmem.h
@@ -26,11 +26,15 @@
  * @nvmem: The backing storage device
  * @offset: The offset of the cell from the start of @nvmem
  * @size: The size of the cell, in bytes
+ * @bit_offset: Bit offset within the cell (0 for byte-level access)
+ * @nbits: Number of bits to use (0 for byte-level access)
  */
 struct nvmem_cell {
 	struct udevice *nvmem;
 	unsigned int offset;
 	size_t size;
+	unsigned int bit_offset;
+	unsigned int nbits;
 };
 
 struct udevice;
-- 
2.34.1



More information about the U-Boot mailing list