diff --git a/MAINTAINERS b/MAINTAINERS
index aca97cd2a3..1084cc6e54 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1048,6 +1048,13 @@ F:	cmd/nvme.c
 F:	include/nvme.h
 F:	doc/develop/driver-model/nvme.rst
 
+NVMEM
+M:	Sean Anderson <seanga2@gmail.com>
+S:	Maintained
+F:	doc/api/nvmem.rst
+F:	drivers/misc/nvmem.c
+F:	include/nvmem.h
+
 NXP C45 TJA11XX PHY DRIVER
 M:	Radu Pirea <radu-nicolae.pirea@oss.nxp.com>
 S:	Maintained
diff --git a/doc/api/index.rst b/doc/api/index.rst
index 72fea981b7..a9338cfef9 100644
--- a/doc/api/index.rst
+++ b/doc/api/index.rst
@@ -14,6 +14,7 @@ U-Boot API documentation
    linker_lists
    lmb
    logging
+   nvmem
    pinctrl
    rng
    sandbox
diff --git a/doc/api/nvmem.rst b/doc/api/nvmem.rst
new file mode 100644
index 0000000000..15c9b5b839
--- /dev/null
+++ b/doc/api/nvmem.rst
@@ -0,0 +1,7 @@
+.. SPDX-License-Identifier: GPL-2.0+
+
+NVMEM API
+=========
+
+.. kernel-doc:: include/nvmem.h
+   :internal:
diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index 7029bb7b5c..e968219aa9 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -31,6 +31,22 @@ config TPL_MISC
 	  set of generic read, write and ioctl methods may be used to
 	  access the device.
 
+config NVMEM
+	bool "NVMEM support"
+	help
+	  This adds support for a common interface to different types of
+	  non-volatile memory. Consumers can use nvmem-cells properties to look
+	  up hardware configuration data such as MAC addresses and calibration
+	  settings.
+
+config SPL_NVMEM
+	bool "NVMEM support in SPL"
+	help
+	  This adds support for a common interface to different types of
+	  non-volatile memory. Consumers can use nvmem-cells properties to look
+	  up hardware configuration data such as MAC addresses and calibration
+	  settings.
+
 config ALTERA_SYSID
 	bool "Altera Sysid support"
 	depends on MISC
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index f22eff601a..c393eed7f9 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -4,6 +4,7 @@
 # Wolfgang Denk, DENX Software Engineering, wd@denx.de.
 
 obj-$(CONFIG_MISC) += misc-uclass.o
+obj-$(CONFIG_$(SPL_TPL_)NVMEM) += nvmem.o
 
 obj-$(CONFIG_$(SPL_TPL_)CROS_EC) += cros_ec.o
 obj-$(CONFIG_$(SPL_TPL_)CROS_EC_SANDBOX) += cros_ec_sandbox.o
diff --git a/drivers/misc/nvmem.c b/drivers/misc/nvmem.c
new file mode 100644
index 0000000000..fd80a72394
--- /dev/null
+++ b/drivers/misc/nvmem.c
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2022 Sean Anderson <sean.anderson@seco.com>
+ */
+
+#include <common.h>
+#include <i2c_eeprom.h>
+#include <linker_lists.h>
+#include <misc.h>
+#include <nvmem.h>
+#include <rtc.h>
+#include <dm/device_compat.h>
+#include <dm/ofnode.h>
+#include <dm/read.h>
+#include <dm/uclass.h>
+
+int nvmem_cell_read(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;
+
+	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);
+
+		if (ret < 0)
+			return ret;
+		if (ret != size)
+			return -EIO;
+		return 0;
+	}
+	case UCLASS_RTC:
+		return dm_rtc_read(cell->nvmem, cell->offset, buf, size);
+	default:
+		return -ENOSYS;
+	}
+}
+
+int nvmem_cell_write(struct nvmem_cell *cell, const 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;
+
+	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);
+
+		if (ret < 0)
+			return ret;
+		if (ret != size)
+			return -EIO;
+		return 0;
+	}
+	case UCLASS_RTC:
+		return dm_rtc_write(cell->nvmem, cell->offset, buf, size);
+	default:
+		return -ENOSYS;
+	}
+}
+
+/**
+ * nvmem_get_device() - Get an nvmem device for a cell
+ * @node: ofnode of the nvmem device
+ * @cell: Cell to look up
+ *
+ * Try to find a nvmem-compatible device by going through the nvmem interfaces.
+ *
+ * Return:
+ * * 0 on success
+ * * -ENODEV if we didn't find anything
+ * * A negative error if there was a problem looking up the device
+ */
+static int nvmem_get_device(ofnode node, struct nvmem_cell *cell)
+{
+	int i, ret;
+	enum uclass_id ids[] = {
+		UCLASS_I2C_EEPROM,
+		UCLASS_MISC,
+		UCLASS_RTC,
+	};
+
+	for (i = 0; i < ARRAY_SIZE(ids); i++) {
+		ret = uclass_get_device_by_ofnode(ids[i], node, &cell->nvmem);
+		if (!ret)
+			return 0;
+		if (ret != -ENODEV)
+			return ret;
+	}
+
+	return -ENODEV;
+}
+
+int nvmem_cell_get_by_index(struct udevice *dev, int index,
+			    struct nvmem_cell *cell)
+{
+	fdt_addr_t offset;
+	fdt_size_t size = FDT_SIZE_T_NONE;
+	int ret;
+	struct ofnode_phandle_args args;
+
+	dev_dbg(dev, "%s: index=%d\n", __func__, index);
+
+	ret = dev_read_phandle_with_args(dev, "nvmem-cells", NULL, 0, index,
+					 &args);
+	if (ret)
+		return ret;
+
+	ret = nvmem_get_device(ofnode_get_parent(args.node), cell);
+	if (ret)
+		return ret;
+
+	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",
+			ofnode_get_name(args.node));
+		return -EINVAL;
+	}
+
+	cell->offset = offset;
+	cell->size = size;
+	return 0;
+}
+
+int nvmem_cell_get_by_name(struct udevice *dev, const char *name,
+			   struct nvmem_cell *cell)
+{
+	int index;
+
+	dev_dbg(dev, "%s, name=%s\n", __func__, name);
+
+	index = dev_read_stringlist_search(dev, "nvmem-cell-names", name);
+	if (index < 0)
+		return index;
+
+	return nvmem_cell_get_by_index(dev, index, cell);
+}
diff --git a/include/nvmem.h b/include/nvmem.h
new file mode 100644
index 0000000000..2751713a68
--- /dev/null
+++ b/include/nvmem.h
@@ -0,0 +1,151 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright (c) 2022 Sean Anderson <sean.anderson@seco.com>
+ */
+
+#ifndef NVMEM_H
+#define NVMEM_H
+
+/**
+ * typedef nvmem_reg_read_t - Read a register from an nvmem device
+ *
+ * @dev: The device to read from
+ * @offset: The offset of the register from the beginning of @dev
+ * @buf: The buffer to read into
+ * @size: The size of @buf, in bytes
+ *
+ * Return:
+ * * 0 on success
+ * * A negative error on failure
+ */
+typedef int (*nvmem_reg_read_t)(struct udevice *dev, unsigned int offset,
+				void *buf, size_t size);
+
+/**
+ * typedef nvmem_reg_write_t - Write a register to an nvmem device
+ * @dev: The device to write
+ * @offset: The offset of the register from the beginning of @dev
+ * @buf: The buffer to write
+ * @size: The size of @buf, in bytes
+ *
+ * Return:
+ * * 0 on success
+ * * -ENOSYS if the device is read-only
+ * * A negative error on other failures
+ */
+typedef int (*nvmem_reg_write_t)(struct udevice *dev, unsigned int offset,
+				 const void *buf, size_t size);
+
+/**
+ * struct nvmem_cell - One datum within non-volatile memory
+ * @nvmem: The backing storage device
+ * @offset: The offset of the cell from the start of @nvmem
+ * @size: The size of the cell, in bytes
+ */
+struct nvmem_cell {
+	struct udevice *nvmem;
+	unsigned int offset;
+	size_t size;
+};
+
+struct udevice;
+
+#if CONFIG_IS_ENABLED(NVMEM)
+
+/**
+ * nvmem_cell_read() - Read the value of an nvmem cell
+ * @cell: The nvmem cell to read
+ * @buf: The buffer to read into
+ * @size: The size of @buf
+ *
+ * Return:
+ * * 0 on success
+ * * -EINVAL if @buf is not the same size as @cell.
+ * * -ENOSYS if CONFIG_NVMEM is disabled
+ * * A negative error if there was a problem reading the underlying storage
+ */
+int nvmem_cell_read(struct nvmem_cell *cell, void *buf, size_t size);
+
+/**
+ * nvmem_cell_write() - Write a value to an nvmem cell
+ * @cell: The nvmem cell to write
+ * @buf: The buffer to write from
+ * @size: The size of @buf
+ *
+ * Return:
+ * * 0 on success
+ * * -EINVAL if @buf is not the same size as @cell
+ * * -ENOSYS if @cell is read-only, or if CONFIG_NVMEM is disabled
+ * * A negative error if there was a problem writing the underlying storage
+ */
+int nvmem_cell_write(struct nvmem_cell *cell, const void *buf, size_t size);
+
+/**
+ * nvmem_cell_get_by_index() - Get an nvmem cell from a given device and index
+ * @dev: The device that uses the nvmem cell
+ * @index: The index of the cell in nvmem-cells
+ * @cell: The cell to initialize
+ *
+ * Look up the nvmem cell referenced by the phandle at @index in nvmem-cells in
+ * @dev.
+ *
+ * Return:
+ * * 0 on success
+ * * -EINVAL if the regs property is missing, empty, or undersized
+ * * -ENODEV if the nvmem device is missing or unimplemented
+ * * -ENOSYS if CONFIG_NVMEM is disabled
+ * * A negative error if there was a problem reading nvmem-cells or getting the
+ *   device
+ */
+int nvmem_cell_get_by_index(struct udevice *dev, int index,
+			    struct nvmem_cell *cell);
+
+/**
+ * nvmem_cell_get_by_name() - Get an nvmem cell from a given device and name
+ * @dev: The device that uses the nvmem cell
+ * @name: The name of the nvmem cell
+ * @cell: The cell to initialize
+ *
+ * Look up the nvmem cell referenced by @name in the nvmem-cell-names property
+ * of @dev.
+ *
+ * Return:
+ * * 0 on success
+ * * -EINVAL if the regs property is missing, empty, or undersized
+ * * -ENODEV if the nvmem device is missing or unimplemented
+ * * -ENODATA if @name is not in nvmem-cell-names
+ * * -ENOSYS if CONFIG_NVMEM is disabled
+ * * A negative error if there was a problem reading nvmem-cell-names,
+ *   nvmem-cells, or getting the device
+ */
+int nvmem_cell_get_by_name(struct udevice *dev, const char *name,
+			   struct nvmem_cell *cell);
+
+#else /* CONFIG_NVMEM */
+
+static inline int nvmem_cell_read(struct nvmem_cell *cell, void *buf, int size)
+{
+	return -ENOSYS;
+}
+
+static inline int nvmem_cell_write(struct nvmem_cell *cell, const void *buf,
+				   int size)
+{
+	return -ENOSYS;
+}
+
+static inline int nvmem_cell_get_by_index(struct udevice *dev, int index,
+					  struct nvmem_cell *cell)
+{
+	return -ENOSYS;
+}
+
+static inline int nvmem_cell_get_by_name(struct udevice *dev, const char *name,
+					 struct nvmem_cell *cell)
+{
+	return -ENOSYS;
+}
+
+#endif /* CONFIG_NVMEM */
+
+#endif /* NVMEM_H */
