Skip to content

Commit

Permalink
block: implement NVMEM provider
Browse files Browse the repository at this point in the history
On embedded devices using an eMMC it is common that one or more partitions
on the eMMC are used to store MAC addresses and Wi-Fi calibration EEPROM
data. Allow referencing the partition in device tree for the kernel and
Wi-Fi drivers accessing it via the NVMEM layer.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
  • Loading branch information
dangowrt committed Aug 3, 2023
1 parent 7e25026 commit dcde36e
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 0 deletions.
9 changes: 9 additions & 0 deletions block/Kconfig
Expand Up @@ -206,6 +206,15 @@ config BLK_INLINE_ENCRYPTION_FALLBACK
by falling back to the kernel crypto API when inline
encryption hardware is not present.

config BLK_NVMEM
bool "Block device NVMEM provider"
depends on OF
depends on NVMEM
help
Allow block devices (or partitions) to act as NVMEM prodivers,
typically used with eMMC to store MAC addresses or Wi-Fi
calibration data on embedded devices.

source "block/partitions/Kconfig"

config BLK_MQ_PCI
Expand Down
1 change: 1 addition & 0 deletions block/Makefile
Expand Up @@ -34,6 +34,7 @@ obj-$(CONFIG_BLK_DEV_ZONED) += blk-zoned.o
obj-$(CONFIG_BLK_WBT) += blk-wbt.o
obj-$(CONFIG_BLK_DEBUG_FS) += blk-mq-debugfs.o
obj-$(CONFIG_BLK_DEBUG_FS_ZONED)+= blk-mq-debugfs-zoned.o
obj-$(CONFIG_BLK_NVMEM) += blk-nvmem.o
obj-$(CONFIG_BLK_SED_OPAL) += sed-opal.o
obj-$(CONFIG_BLK_PM) += blk-pm.o
obj-$(CONFIG_BLK_INLINE_ENCRYPTION) += blk-crypto.o blk-crypto-profile.o \
Expand Down
163 changes: 163 additions & 0 deletions block/blk-nvmem.c
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* block device NVMEM provider
*
* Copyright (c) 2023 Daniel Golle <daniel@makrotopia.org>
*
* Useful on devices using a partition on an eMMC for MAC addresses or
* Wi-Fi calibration EEPROM data.
*/

#include "blk.h"
#include <linux/nvmem-provider.h>
#include <linux/of.h>
#include <linux/pagemap.h>
#include <linux/property.h>

/* List of all NVMEM devices */
static LIST_HEAD(nvmem_devices);
static DEFINE_MUTEX(devices_mutex);

struct blk_nvmem {
struct nvmem_device *nvmem;
struct block_device *bdev;
struct list_head list;
};

static int blk_nvmem_reg_read(void *priv, unsigned int from,
void *val, size_t bytes)
{
pgoff_t f_index = from >> PAGE_SHIFT;
struct address_space *mapping;
struct blk_nvmem *bnv = priv;
unsigned long offs, to_read;
size_t bytes_left = bytes;
struct folio *folio;
void *p;

if (!bnv->bdev)
return -ENODEV;

offs = from & ~PAGE_MASK;
mapping = bnv->bdev->bd_inode->i_mapping;

while (bytes_left) {
folio = read_mapping_folio(mapping, f_index++, NULL);
if (IS_ERR(folio))
return PTR_ERR(folio);

to_read = min_t(unsigned long, bytes_left, PAGE_SIZE - offs);
p = folio_address(folio) + offset_in_folio(folio, offs);
memcpy(val, p, to_read);
offs = 0;
bytes_left -= to_read;
val += to_read;
folio_put(folio);
}

return 0;
}

static int blk_nvmem_register(struct device *dev)
{
struct block_device *bdev = dev_to_bdev(dev);
struct nvmem_config config = {};
struct blk_nvmem *bnv;

/*
* skip devices which don't have GENHD_FL_NVMEM set
*
* This flag is used for mtdblock and ubiblock devices because
* both, MTD and UBI already implement their own NVMEM provider.
* To avoid registering multiple NVMEM providers for the same
* device node, skip the block NVMEM provider.
*/
if (!(bdev->bd_disk->flags & GENHD_FL_NVMEM))
return 0;

if (!fwnode_device_is_compatible(dev->fwnode, "nvmem-cells"))
return 0;

/*
* skip block device too large to be represented as NVMEM devices
* which are using an 'int' as address
*/
if (bdev_nr_bytes(bdev) > INT_MAX)
return -EFBIG;

bnv = kzalloc(sizeof(struct blk_nvmem), GFP_KERNEL);
if (!bnv)
return -ENOMEM;

config.id = NVMEM_DEVID_NONE;
config.dev = &bdev->bd_device;
config.name = dev_name(&bdev->bd_device);
config.owner = THIS_MODULE;
config.priv = bnv;
config.reg_read = blk_nvmem_reg_read;
config.size = bdev_nr_bytes(bdev);
config.word_size = 1;
config.stride = 1;
config.read_only = true;
config.root_only = true;
config.ignore_wp = true;
config.of_node = to_of_node(dev->fwnode);

bnv->bdev = bdev;
bnv->nvmem = nvmem_register(&config);
if (IS_ERR(bnv->nvmem)) {
dev_err_probe(&bdev->bd_device, PTR_ERR(bnv->nvmem),
"Failed to register NVMEM device\n");

kfree(bnv);
return PTR_ERR(bnv->nvmem);
}

mutex_lock(&devices_mutex);
list_add_tail(&bnv->list, &nvmem_devices);
mutex_unlock(&devices_mutex);

return 0;
}

static void blk_nvmem_unregister(struct device *dev)
{
struct block_device *bdev = dev_to_bdev(dev);
struct blk_nvmem *bnv_c, *bnv = NULL;

mutex_lock(&devices_mutex);
list_for_each_entry(bnv_c, &nvmem_devices, list) {
if (bnv_c->bdev == bdev) {
bnv = bnv_c;
break;
}
}

if (!bnv) {
mutex_unlock(&devices_mutex);
return;
}

list_del(&bnv->list);
mutex_unlock(&devices_mutex);
nvmem_unregister(bnv->nvmem);
kfree(bnv);
}

static struct class_interface blk_nvmem_bus_interface __refdata = {
.class = &block_class,
.add_dev = &blk_nvmem_register,
.remove_dev = &blk_nvmem_unregister,
};

static int __init blk_nvmem_init(void)
{
int ret;

ret = class_interface_register(&blk_nvmem_bus_interface);
if (ret)
return ret;

return 0;
}
device_initcall(blk_nvmem_init);

0 comments on commit dcde36e

Please sign in to comment.