| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * Persistent memory accessor | |
| * | |
| * Copyright (C) 2019-2020 Google, Inc. | |
| */ | |
| #include <linux/kernel.h> | |
| #include <linux/module.h> | |
| #include <linux/io.h> | |
| #include <linux/fs.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/miscdevice.h> | |
| #include <linux/of.h> | |
| #include <linux/of_address.h> | |
| #define DEVICE_NAME "access_ramoops" | |
| #define MAX_OPEN 4 | |
| struct access_ramoops_info { | |
| char *name; | |
| phys_addr_t phys; | |
| void *addr; | |
| size_t size; | |
| struct mutex lock; /* protect access to structure */ | |
| int is_mapped; | |
| int is_open; | |
| const char *label; | |
| struct miscdevice miscdev; | |
| }; | |
| static ssize_t label_show(struct device *dev, struct device_attribute *attr, char *buf) | |
| { | |
| struct miscdevice *miscdev = dev_get_drvdata(dev); | |
| struct access_ramoops_info *info = | |
| container_of(miscdev, struct access_ramoops_info, miscdev); | |
| return snprintf(buf, PAGE_SIZE, "%s\n", info->label); | |
| } | |
| static ssize_t size_show(struct device *dev, struct device_attribute *attr, char *buf) | |
| { | |
| struct miscdevice *miscdev = dev_get_drvdata(dev); | |
| struct access_ramoops_info *info = | |
| container_of(miscdev, struct access_ramoops_info, miscdev); | |
| return snprintf(buf, PAGE_SIZE, "%zd\n", info->size); | |
| } | |
| static DEVICE_ATTR_RO(label); | |
| static DEVICE_ATTR_RO(size); | |
| static struct attribute *access_ramoops_attrs[] = { | |
| &dev_attr_label.attr, | |
| &dev_attr_size.attr, | |
| NULL, | |
| }; | |
| ATTRIBUTE_GROUPS(access_ramoops); | |
| static ssize_t access_ramoops_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) | |
| { | |
| void *kbuf; | |
| ssize_t ret; | |
| struct access_ramoops_info *info = filp->private_data; | |
| kbuf = vmalloc(info->size); | |
| if (!kbuf) | |
| return -ENOMEM; | |
| memcpy_fromio(kbuf, info->addr, info->size); | |
| ret = simple_read_from_buffer(buf, count, ppos, kbuf, info->size); | |
| vfree(kbuf); | |
| return ret; | |
| } | |
| static ssize_t access_ramoops_write(struct file *filp, const char __user *buf, size_t count, | |
| loff_t *ppos) | |
| { | |
| void *kbuf; | |
| ssize_t ret; | |
| struct access_ramoops_info *info = filp->private_data; | |
| kbuf = vmalloc(info->size); | |
| if (!kbuf) | |
| return -ENOMEM; | |
| memcpy_fromio(kbuf, info->addr, info->size); | |
| ret = simple_write_to_buffer(kbuf, info->size, ppos, buf, count); | |
| if (ret > 0) | |
| memcpy_toio(info->addr, kbuf, info->size); | |
| vfree(kbuf); | |
| return ret; | |
| } | |
| static int access_ramoops_open(struct inode *inode, struct file *filp) | |
| { | |
| int res = 0; | |
| struct access_ramoops_info *info = | |
| container_of(filp->private_data, struct access_ramoops_info, | |
| miscdev); | |
| filp->private_data = info; | |
| mutex_lock(&info->lock); | |
| if (info->is_open == MAX_OPEN) { | |
| res = -EBUSY; | |
| goto out; | |
| } | |
| if (!info->is_open) { | |
| if (!request_mem_region(info->phys, info->size, | |
| "access_ramoops")) { | |
| dev_err(info->miscdev.this_device, | |
| "request mem region (%zx@%pa) failed\n", | |
| info->size, &info->phys); | |
| res = -ENOMEM; | |
| goto out; | |
| } | |
| info->addr = ioremap(info->phys, info->size); | |
| if (IS_ERR(info->addr)) { | |
| dev_err(info->miscdev.this_device, | |
| "unable to map region (%zx@%pa)\n", | |
| info->size, &info->phys); | |
| info->addr = NULL; | |
| res = -ENOMEM; | |
| goto out; | |
| } | |
| info->is_mapped = 1; | |
| } | |
| info->is_open++; | |
| out: | |
| mutex_unlock(&info->lock); | |
| return res; | |
| } | |
| static int access_ramoops_release(struct inode *inode, struct file *filp) | |
| { | |
| struct access_ramoops_info *info = filp->private_data; | |
| mutex_lock(&info->lock); | |
| info->is_open--; | |
| if (!info->is_open) { | |
| info->is_mapped = 0; | |
| iounmap(info->addr); | |
| info->addr = NULL; | |
| release_mem_region(info->phys, info->size); | |
| } | |
| filp->private_data = NULL; | |
| mutex_unlock(&info->lock); | |
| return 0; | |
| } | |
| static const struct file_operations access_ramoops_fops = { | |
| .owner = THIS_MODULE, | |
| .open = access_ramoops_open, | |
| .read = access_ramoops_read, | |
| .write = access_ramoops_write, | |
| .release = access_ramoops_release, | |
| }; | |
| static int __init access_ramoops_probe(struct platform_device *pdev) | |
| { | |
| int ret = 0; | |
| struct access_ramoops_info *info; | |
| struct device_node *of_node = pdev->dev.of_node; | |
| struct device_node *mem_region; | |
| struct resource res; | |
| info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL); | |
| if (!info) | |
| return -ENOMEM; | |
| platform_set_drvdata(pdev, info); | |
| mem_region = of_parse_phandle(of_node, "memory-region", 0); | |
| if (!mem_region) { | |
| dev_err(&pdev->dev, "no memory-region phandle\n"); | |
| return -ENODEV; | |
| } | |
| ret = of_property_read_string(of_node, "label", &info->label); | |
| if (ret) { | |
| dev_err(&pdev->dev, "failed to get region label: %d\n", ret); | |
| return -EINVAL; | |
| } | |
| info->name = devm_kasprintf(&pdev->dev, GFP_KERNEL, "access-%s", | |
| info->label); | |
| if (!info->name) { | |
| dev_err(&pdev->dev, "failed to alloc name\n"); | |
| return -ENOMEM; | |
| } | |
| ret = of_address_to_resource(mem_region, 0, &res); | |
| of_node_put(mem_region); | |
| if (ret) { | |
| dev_err(&pdev->dev, | |
| "failed to get memory-region resource: %d\n", ret); | |
| return -ENOMEM; | |
| } | |
| info->phys = res.start; | |
| info->size = resource_size(&res); | |
| mutex_init(&info->lock); | |
| info->miscdev.minor = MISC_DYNAMIC_MINOR; | |
| info->miscdev.name = info->name; | |
| info->miscdev.fops = &access_ramoops_fops; | |
| info->miscdev.groups = access_ramoops_groups; | |
| info->miscdev.parent = &pdev->dev; | |
| ret = misc_register(&info->miscdev); | |
| if (ret) { | |
| dev_err(info->miscdev.this_device, | |
| "failed to register misc device %d\n", ret); | |
| return ret; | |
| } | |
| dev_info(info->miscdev.this_device, | |
| "registered '%s' %d:%d, (%zx@%pa)\n", info->label, | |
| MISC_MAJOR, info->miscdev.minor, info->size, &info->phys); | |
| return ret; | |
| } | |
| static int __exit access_ramoops_remove(struct platform_device *pdev) | |
| { | |
| struct access_ramoops_info *info = platform_get_drvdata(pdev); | |
| dev_info(info->miscdev.this_device, "removing"); | |
| misc_deregister(&info->miscdev); | |
| if (!info->is_mapped) | |
| return 0; | |
| mutex_lock(&info->lock); | |
| info->is_mapped = 0; | |
| iounmap(info->addr); | |
| release_mem_region(info->phys, info->size); | |
| mutex_unlock(&info->lock); | |
| return 0; | |
| } | |
| static const struct of_device_id dt_match[] = { | |
| { .compatible = "access_ramoops" }, | |
| {} | |
| }; | |
| MODULE_DEVICE_TABLE(of, dt_match); | |
| static struct platform_driver access_ramoops_driver = { | |
| .driver = { | |
| .name = "access_ramoops", | |
| .of_match_table = dt_match, | |
| }, | |
| .remove = __exit_p(access_ramoops_remove), | |
| }; | |
| module_platform_driver_probe(access_ramoops_driver, access_ramoops_probe); | |
| MODULE_LICENSE("GPL v2"); | |
| MODULE_AUTHOR("Patrick Tjin <[email protected]>"); | |
| MODULE_DESCRIPTION("Persistent memory access driver"); |