| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * Copyright (c) 2023 Daniel Golle <daniel@makrotopia.org> |
| */ |
| |
| /* UBI NVMEM provider */ |
| #include "ubi.h" |
| #include <linux/nvmem-provider.h> |
| #include <asm/div64.h> |
| |
| /* List of all NVMEM devices */ |
| static LIST_HEAD(nvmem_devices); |
| static DEFINE_MUTEX(devices_mutex); |
| |
| struct ubi_nvmem { |
| struct nvmem_device *nvmem; |
| int ubi_num; |
| int vol_id; |
| int usable_leb_size; |
| struct list_head list; |
| }; |
| |
| static int ubi_nvmem_reg_read(void *priv, unsigned int from, |
| void *val, size_t bytes) |
| { |
| size_t to_read, bytes_left = bytes; |
| struct ubi_nvmem *unv = priv; |
| struct ubi_volume_desc *desc; |
| uint32_t offs; |
| uint64_t lnum = from; |
| int err = 0; |
| |
| desc = ubi_open_volume(unv->ubi_num, unv->vol_id, UBI_READONLY); |
| if (IS_ERR(desc)) |
| return PTR_ERR(desc); |
| |
| offs = do_div(lnum, unv->usable_leb_size); |
| while (bytes_left) { |
| to_read = unv->usable_leb_size - offs; |
| |
| if (to_read > bytes_left) |
| to_read = bytes_left; |
| |
| err = ubi_read(desc, lnum, val, offs, to_read); |
| if (err) |
| break; |
| |
| lnum += 1; |
| offs = 0; |
| bytes_left -= to_read; |
| val += to_read; |
| } |
| ubi_close_volume(desc); |
| |
| if (err) |
| return err; |
| |
| return bytes_left == 0 ? 0 : -EIO; |
| } |
| |
| static int ubi_nvmem_add(struct ubi_volume_info *vi) |
| { |
| struct device_node *np = dev_of_node(vi->dev); |
| struct nvmem_config config = {}; |
| struct ubi_nvmem *unv; |
| int ret; |
| |
| if (!np) |
| return 0; |
| |
| if (!of_get_child_by_name(np, "nvmem-layout")) |
| return 0; |
| |
| if (WARN_ON_ONCE(vi->usable_leb_size <= 0) || |
| WARN_ON_ONCE(vi->size <= 0)) |
| return -EINVAL; |
| |
| unv = kzalloc(sizeof(struct ubi_nvmem), GFP_KERNEL); |
| if (!unv) |
| return -ENOMEM; |
| |
| config.id = NVMEM_DEVID_NONE; |
| config.dev = vi->dev; |
| config.name = dev_name(vi->dev); |
| config.owner = THIS_MODULE; |
| config.priv = unv; |
| config.reg_read = ubi_nvmem_reg_read; |
| config.size = vi->usable_leb_size * vi->size; |
| config.word_size = 1; |
| config.stride = 1; |
| config.read_only = true; |
| config.root_only = true; |
| config.ignore_wp = true; |
| config.of_node = np; |
| |
| unv->ubi_num = vi->ubi_num; |
| unv->vol_id = vi->vol_id; |
| unv->usable_leb_size = vi->usable_leb_size; |
| unv->nvmem = nvmem_register(&config); |
| if (IS_ERR(unv->nvmem)) { |
| ret = dev_err_probe(vi->dev, PTR_ERR(unv->nvmem), |
| "Failed to register NVMEM device\n"); |
| kfree(unv); |
| return ret; |
| } |
| |
| mutex_lock(&devices_mutex); |
| list_add_tail(&unv->list, &nvmem_devices); |
| mutex_unlock(&devices_mutex); |
| |
| return 0; |
| } |
| |
| static void ubi_nvmem_remove(struct ubi_volume_info *vi) |
| { |
| struct ubi_nvmem *unv_c, *unv = NULL; |
| |
| mutex_lock(&devices_mutex); |
| list_for_each_entry(unv_c, &nvmem_devices, list) |
| if (unv_c->ubi_num == vi->ubi_num && unv_c->vol_id == vi->vol_id) { |
| unv = unv_c; |
| break; |
| } |
| |
| if (!unv) { |
| mutex_unlock(&devices_mutex); |
| return; |
| } |
| |
| list_del(&unv->list); |
| mutex_unlock(&devices_mutex); |
| nvmem_unregister(unv->nvmem); |
| kfree(unv); |
| } |
| |
| /** |
| * nvmem_notify - UBI notification handler. |
| * @nb: registered notifier block |
| * @l: notification type |
| * @ns_ptr: pointer to the &struct ubi_notification object |
| */ |
| static int nvmem_notify(struct notifier_block *nb, unsigned long l, |
| void *ns_ptr) |
| { |
| struct ubi_notification *nt = ns_ptr; |
| |
| switch (l) { |
| case UBI_VOLUME_RESIZED: |
| ubi_nvmem_remove(&nt->vi); |
| fallthrough; |
| case UBI_VOLUME_ADDED: |
| ubi_nvmem_add(&nt->vi); |
| break; |
| case UBI_VOLUME_SHUTDOWN: |
| ubi_nvmem_remove(&nt->vi); |
| break; |
| default: |
| break; |
| } |
| return NOTIFY_OK; |
| } |
| |
| static struct notifier_block nvmem_notifier = { |
| .notifier_call = nvmem_notify, |
| }; |
| |
| static int __init ubi_nvmem_init(void) |
| { |
| return ubi_register_volume_notifier(&nvmem_notifier, 0); |
| } |
| |
| static void __exit ubi_nvmem_exit(void) |
| { |
| struct ubi_nvmem *unv, *tmp; |
| |
| mutex_lock(&devices_mutex); |
| list_for_each_entry_safe(unv, tmp, &nvmem_devices, list) { |
| nvmem_unregister(unv->nvmem); |
| list_del(&unv->list); |
| kfree(unv); |
| } |
| mutex_unlock(&devices_mutex); |
| |
| ubi_unregister_volume_notifier(&nvmem_notifier); |
| } |
| |
| module_init(ubi_nvmem_init); |
| module_exit(ubi_nvmem_exit); |
| MODULE_DESCRIPTION("NVMEM layer over UBI volumes"); |
| MODULE_AUTHOR("Daniel Golle"); |
| MODULE_LICENSE("GPL"); |