[PATCH v2 2/6] misc: Add support for bit fields in NVMEM cells

Aswin Murugan aswin.murugan at oss.qualcomm.com
Fri Feb 13 12:27:13 CET 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 multi-byte bit field support by porting bit manipulation
algorithms from the Linux kernel driver [1]:

1. nvmem_shift_read_buffer_in_place() - Extract bit fields from raw
   bytes by shifting and masking across byte boundaries. Handles fields
   that span multiple bytes.

2. nvmem_cell_prepare_write_buffer() - Perform Read-Modify-Write to
   preserve bits outside the target field. Read current values, mask to
   preserve adjacent bits, and combine with new value.

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.

[1] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/nvmem/core.c

Signed-off-by: Aswin Murugan <aswin.murugan at oss.qualcomm.com>
---
 drivers/misc/nvmem.c | 241 ++++++++++++++++++++++++++++++++++++++++---
 include/nvmem.h      |   4 +
 2 files changed, 231 insertions(+), 14 deletions(-)

diff --git a/drivers/misc/nvmem.c b/drivers/misc/nvmem.c
index 33e80858565..6a75c326189 100644
--- a/drivers/misc/nvmem.c
+++ b/drivers/misc/nvmem.c
@@ -12,52 +12,251 @@
 #include <dm/ofnode.h>
 #include <dm/read.h>
 #include <dm/uclass.h>
+#include <linux/bitops.h>
+#include <linux/kernel.h>
+
+/**
+ * nvmem_shift_read_buffer_in_place() - Shift read buffer to extract bit field
+ * @cell: NVMEM cell with bit field information
+ * @buf: Buffer containing raw bytes read from NVMEM
+ *
+ * This function shifts and masks the buffer to extract only the bits specified
+ * by cell->bit_offset and cell->nbits. It handles bit fields that span multiple
+ * bytes by shifting bits across byte boundaries.
+ *
+ */
+static void nvmem_shift_read_buffer_in_place(struct nvmem_cell *cell, void *buf)
+{
+	u8 *prev_byte, *buf_ptr;
+	int i, extra, bit_offset = cell->bit_offset;
+	int bytes_needed;
+
+	bytes_needed = DIV_ROUND_UP(cell->nbits + cell->bit_offset, BITS_PER_BYTE);
+
+	prev_byte = buf_ptr = buf;
+	if (bit_offset) {
+		/* First byte: shift right by bit_offset */
+		*buf_ptr++ >>= bit_offset;
+
+		/* Process remaining bytes, combining bits across boundaries */
+		for (i = 1; i < bytes_needed; i++) {
+			/* Get bits from next byte and shift them towards MSB */
+			*prev_byte |= *buf_ptr << (BITS_PER_BYTE - bit_offset);
+
+			prev_byte = buf_ptr;
+			*buf_ptr++ >>= bit_offset;
+		}
+	} else {
+		/* Point to the MSB */
+		prev_byte += bytes_needed - 1;
+	}
+
+	/* Clear extra bytes if result fits in fewer bytes */
+	extra = bytes_needed - DIV_ROUND_UP(cell->nbits, BITS_PER_BYTE);
+	while (--extra >= 0)
+		*prev_byte-- = 0;
+
+	/* Clear MSB bits if any leftover in the last byte */
+	if (cell->nbits % BITS_PER_BYTE)
+		*prev_byte &= GENMASK((cell->nbits % BITS_PER_BYTE) - 1, 0);
+}
+
+/**
+ * nvmem_cell_prepare_write_buffer() - Prepare buffer for writing bit field
+ * @cell: NVMEM cell with bit field information
+ * @buf: Buffer containing value to write
+ * @size: Size of buf
+ * @data: Output buffer with prepared data for writing
+ *
+ * This function performs Read-Modify-Write to preserve bits outside the
+ * specified bit field. It reads current values, modifies only the target
+ * bits, and prepares the complete bytes for writing back.
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+static int nvmem_cell_prepare_write_buffer(struct nvmem_cell *cell,
+					   const void *buf, size_t size,
+					   u8 *data)
+{
+	int i, rc, nbits, bit_offset = cell->bit_offset;
+	int bytes_needed;
+	u8 current_val, *prev_byte, *buf_ptr, prev_byte_val, prev_bits;
+
+	nbits = cell->nbits;
+	bytes_needed = DIV_ROUND_UP(nbits + bit_offset, BITS_PER_BYTE);
+
+	memcpy(data, buf, size);
+	prev_byte = buf_ptr = data;
+
+	if (bit_offset) {
+		prev_byte_val = *buf_ptr;
+		*buf_ptr <<= bit_offset;
+
+		/* Setup the first byte with LSB bits from NVMEM */
+		switch (cell->nvmem->driver->id) {
+		case UCLASS_I2C_EEPROM:
+			rc = i2c_eeprom_read(cell->nvmem, cell->offset, &current_val, 1);
+			break;
+		case UCLASS_MISC:
+			rc = misc_read(cell->nvmem, cell->offset, &current_val, 1);
+			if (rc < 0)
+				return rc;
+			if (rc != 1)
+				return -EIO;
+			rc = 0;
+			break;
+		case UCLASS_RTC:
+			rc = dm_rtc_read(cell->nvmem, cell->offset, &current_val, 1);
+			break;
+		default:
+			return -ENOSYS;
+		}
+
+		if (rc)
+			return rc;
+
+		*buf_ptr++ |= GENMASK(bit_offset - 1, 0) & current_val;
+
+		/* Setup rest of the bytes if any */
+		for (i = 1; i < bytes_needed; i++) {
+			/* Get last byte bits and shift them towards LSB */
+			prev_bits = prev_byte_val >> (BITS_PER_BYTE - 1 - bit_offset);
+			prev_byte_val = *buf_ptr;
+			prev_byte = buf_ptr;
+			*buf_ptr <<= bit_offset;
+			*buf_ptr++ |= prev_bits;
+		}
+	}
+
+	/* If it's not end on byte boundary */
+	if ((nbits + bit_offset) % BITS_PER_BYTE) {
+		/* Setup the last byte with MSB bits from NVMEM */
+		switch (cell->nvmem->driver->id) {
+		case UCLASS_I2C_EEPROM:
+			rc = i2c_eeprom_read(cell->nvmem,
+					     cell->offset + bytes_needed - 1,
+					     &current_val, 1);
+			break;
+		case UCLASS_MISC:
+			rc = misc_read(cell->nvmem,
+				       cell->offset + bytes_needed - 1,
+				       &current_val, 1);
+			if (rc < 0)
+				return rc;
+			if (rc != 1)
+				return -EIO;
+			rc = 0;
+			break;
+		case UCLASS_RTC:
+			rc = dm_rtc_read(cell->nvmem,
+					 cell->offset + bytes_needed - 1,
+					 &current_val, 1);
+			break;
+		default:
+			return -ENOSYS;
+		}
+
+		if (rc)
+			return rc;
+
+		*prev_byte |= GENMASK(7, (nbits + bit_offset) % BITS_PER_BYTE) & current_val;
+	}
+
+	return 0;
+}
 
 int nvmem_cell_read(struct nvmem_cell *cell, void *buf, size_t size)
 {
+	u8 data[size];
+	int bytes_needed;
+	int ret;
+
 	dev_dbg(cell->nvmem, "%s: off=%u size=%zu\n", __func__, cell->offset, size);
+
 	if (size != cell->size)
 		return -EINVAL;
 
+	/* Calculate how many bytes we need to read */
+	if (cell->nbits > 0) {
+		bytes_needed = DIV_ROUND_UP(cell->nbits + cell->bit_offset, BITS_PER_BYTE);
+		if (bytes_needed > sizeof(data))
+			return -EINVAL;
+	} else {
+		bytes_needed = 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, data, bytes_needed);
+		break;
+	case UCLASS_MISC:
+		ret = misc_read(cell->nvmem, cell->offset, data, bytes_needed);
 		if (ret < 0)
 			return ret;
-		if (ret != size)
+		if (ret != bytes_needed)
 			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, data, bytes_needed);
+		break;
 	default:
 		return -ENOSYS;
 	}
+
+	if (ret)
+		return ret;
+
+	if (cell->nbits > 0) {
+		/* Extract bit field from raw bytes */
+		nvmem_shift_read_buffer_in_place(cell, data);
+		/* Copy only the bytes containing the bit field result */
+		memcpy(buf, data, DIV_ROUND_UP(cell->nbits, BITS_PER_BYTE));
+	} else {
+		memcpy(buf, data, size);
+	}
+
+	return 0;
 }
 
 int nvmem_cell_write(struct nvmem_cell *cell, const void *buf, size_t size)
 {
+	u8 data[size];
+	int bytes_needed;
+	int ret;
+
 	dev_dbg(cell->nvmem, "%s: off=%u size=%zu\n", __func__, cell->offset, size);
+
 	if (size != cell->size)
 		return -EINVAL;
 
+	if (cell->nbits > 0) {
+		bytes_needed = DIV_ROUND_UP(cell->nbits + cell->bit_offset, BITS_PER_BYTE);
+		if (bytes_needed > sizeof(data))
+			return -EINVAL;
+
+		/* Prepare write buffer with Read-Modify-Write */
+		ret = nvmem_cell_prepare_write_buffer(cell, buf, size, data);
+		if (ret)
+			return ret;
+	} else {
+		bytes_needed = size;
+		memcpy(data, buf, size);
+	}
+
 	switch (cell->nvmem->driver->id) {
 	case UCLASS_I2C_EEPROM:
-		return i2c_eeprom_write(cell->nvmem, cell->offset, buf, size);
+		return i2c_eeprom_write(cell->nvmem, cell->offset, data, bytes_needed);
 	case UCLASS_MISC: {
-		int ret = misc_write(cell->nvmem, cell->offset, buf, size);
-
+		ret = misc_write(cell->nvmem, cell->offset, data, bytes_needed);
 		if (ret < 0)
 			return ret;
-		if (ret != size)
+		if (ret != bytes_needed)
 			return -EIO;
 		return 0;
 	}
 	case UCLASS_RTC:
-		return dm_rtc_write(cell->nvmem, cell->offset, buf, size);
+		return dm_rtc_write(cell->nvmem, cell->offset, data, bytes_needed);
 	default:
 		return -ENOSYS;
 	}
@@ -121,13 +320,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