/* * Copyright (c) 2015, Xilinx Inc. and Contributors. All rights reserved. * * SPDX-License-Identifier: BSD-3-Clause */ /* * @file linux/device.c * @brief Linux libmetal device operations. */ #include #include #include #include #define MAX_DRIVERS 64 struct linux_bus; struct linux_device; struct linux_driver { const char *drv_name; const char *mod_name; const char *cls_name; struct sysfs_driver *sdrv; int (*dev_open)(struct linux_bus *lbus, struct linux_device *ldev); void (*dev_close)(struct linux_bus *lbus, struct linux_device *ldev); void (*dev_irq_ack)(struct linux_bus *lbus, struct linux_device *ldev, int irq); int (*dev_dma_map)(struct linux_bus *lbus, struct linux_device *ldev, uint32_t dir, struct metal_sg *sg_in, int nents_in, struct metal_sg *sg_out); void (*dev_dma_unmap)(struct linux_bus *lbus, struct linux_device *ldev, uint32_t dir, struct metal_sg *sg, int nents); }; struct linux_bus { struct metal_bus bus; const char *bus_name; struct linux_driver drivers[MAX_DRIVERS]; struct sysfs_bus *sbus; }; struct linux_device { struct metal_device device; char dev_name[PATH_MAX]; char dev_path[PATH_MAX]; char cls_path[PATH_MAX]; metal_phys_addr_t region_phys[METAL_MAX_DEVICE_REGIONS]; struct linux_driver *ldrv; struct sysfs_device *sdev; struct sysfs_attribute *override; int fd; }; static struct linux_bus *to_linux_bus(struct metal_bus *bus) { return metal_container_of(bus, struct linux_bus, bus); } static struct linux_device *to_linux_device(struct metal_device *device) { return metal_container_of(device, struct linux_device, device); } static int metal_uio_read_map_attr(struct linux_device *ldev, unsigned index, const char *name, unsigned long *value) { const char *cls = ldev->cls_path; struct sysfs_attribute *attr; char path[SYSFS_PATH_MAX]; int result; result = snprintf(path, sizeof(path), "%s/maps/map%u/%s", cls, index, name); if (result >= (int)sizeof(path)) return -EOVERFLOW; attr = sysfs_open_attribute(path); if (!attr || sysfs_read_attribute(attr) != 0) return -errno; *value = strtoul(attr->value, NULL, 0); return 0; } static int metal_uio_dev_bind(struct linux_device *ldev, struct linux_driver *ldrv) { struct sysfs_attribute *attr; int result; if (strcmp(ldev->sdev->driver_name, ldrv->drv_name) == 0) return 0; if (strcmp(ldev->sdev->driver_name, SYSFS_UNKNOWN) != 0) { metal_log(METAL_LOG_INFO, "device %s in use by driver %s\n", ldev->dev_name, ldev->sdev->driver_name); return -EBUSY; } attr = sysfs_get_device_attr(ldev->sdev, "driver_override"); if (!attr) { metal_log(METAL_LOG_ERROR, "device %s has no override\n", ldev->dev_name); return -errno; } result = sysfs_write_attribute(attr, ldrv->drv_name, strlen(ldrv->drv_name)); if (result) { metal_log(METAL_LOG_ERROR, "failed to set override on %s\n", ldev->dev_name); return -errno; } ldev->override = attr; attr = sysfs_get_driver_attr(ldrv->sdrv, "bind"); if (!attr) { metal_log(METAL_LOG_ERROR, "driver %s has no bind\n", ldrv->drv_name); return -ENOTSUP; } result = sysfs_write_attribute(attr, ldev->dev_name, strlen(ldev->dev_name)); if (result) { metal_log(METAL_LOG_ERROR, "failed to bind %s to %s\n", ldev->dev_name, ldrv->drv_name); return -errno; } metal_log(METAL_LOG_DEBUG, "bound device %s to driver %s\n", ldev->dev_name, ldrv->drv_name); return 0; } static int metal_uio_dev_open(struct linux_bus *lbus, struct linux_device *ldev) { char *instance, path[SYSFS_PATH_MAX]; struct linux_driver *ldrv = ldev->ldrv; unsigned long *phys, offset=0, size=0; struct metal_io_region *io; struct dlist *dlist; int result, i; void *virt; int irq_info; ldev->fd = -1; ldev->sdev = sysfs_open_device(lbus->bus_name, ldev->dev_name); if (!ldev->sdev) { metal_log(METAL_LOG_ERROR, "device %s:%s not found\n", lbus->bus_name, ldev->dev_name); return -ENODEV; } metal_log(METAL_LOG_DEBUG, "opened sysfs device %s:%s\n", lbus->bus_name, ldev->dev_name); result = metal_uio_dev_bind(ldev, ldrv); if (result) return result; result = snprintf(path, sizeof(path), "%s/uio", ldev->sdev->path); if (result >= (int)sizeof(path)) return -EOVERFLOW; dlist = sysfs_open_directory_list(path); if (!dlist) { metal_log(METAL_LOG_ERROR, "failed to scan class path %s\n", path); return -errno; } dlist_for_each_data(dlist, instance, char) { result = snprintf(ldev->cls_path, sizeof(ldev->cls_path), "%s/%s", path, instance); if (result >= (int)sizeof(ldev->cls_path)) return -EOVERFLOW; result = snprintf(ldev->dev_path, sizeof(ldev->dev_path), "/dev/%s", instance); if (result >= (int)sizeof(ldev->dev_path)) return -EOVERFLOW; break; } sysfs_close_list(dlist); if (sysfs_path_is_dir(ldev->cls_path) != 0) { metal_log(METAL_LOG_ERROR, "invalid device class path %s\n", ldev->cls_path); return -ENODEV; } i = 0; do { if (!access(ldev->dev_path, F_OK)) break; usleep(10); i++; } while (i < 1000); if (i >= 1000) { metal_log(METAL_LOG_ERROR, "failed to open file %s, timeout.\n", ldev->dev_path); return -ENODEV; } result = metal_open(ldev->dev_path, 0); if (result < 0) { metal_log(METAL_LOG_ERROR, "failed to open device %s\n", ldev->dev_path, strerror(-result)); return result; } ldev->fd = result; metal_log(METAL_LOG_DEBUG, "opened %s:%s as %s\n", lbus->bus_name, ldev->dev_name, ldev->dev_path); for (i = 0, result = 0; !result && i < METAL_MAX_DEVICE_REGIONS; i++) { phys = &ldev->region_phys[ldev->device.num_regions]; result = (result ? result : metal_uio_read_map_attr(ldev, i, "offset", &offset)); result = (result ? result : metal_uio_read_map_attr(ldev, i, "addr", phys)); result = (result ? result : metal_uio_read_map_attr(ldev, i, "size", &size)); result = (result ? result : metal_map(ldev->fd, offset, size, 0, 0, &virt)); if (!result) { io = &ldev->device.regions[ldev->device.num_regions]; metal_io_init(io, virt, phys, size, -1, 0, NULL); ldev->device.num_regions++; } } irq_info = 1; if (write(ldev->fd, &irq_info, sizeof(irq_info)) <= 0) { metal_log(METAL_LOG_INFO, "%s: No IRQ for device %s.\n", __func__, ldev->dev_name); ldev->device.irq_num = 0; ldev->device.irq_info = (void *)-1; } else { ldev->device.irq_num = 1; ldev->device.irq_info = (void *)(intptr_t)ldev->fd; } return 0; } static void metal_uio_dev_close(struct linux_bus *lbus, struct linux_device *ldev) { (void)lbus; if ((intptr_t)ldev->device.irq_info >= 0) /* Normally this call would not be needed, and is added as precaution. Also for uio there is only 1 interrupt associated to the fd/device, we therefore do not need to specify a particular device */ metal_irq_unregister(ldev->fd, NULL, NULL, NULL); if (ldev->override) { sysfs_write_attribute(ldev->override, "", 1); ldev->override = NULL; } if (ldev->sdev) { sysfs_close_device(ldev->sdev); ldev->sdev = NULL; } if (ldev->fd >= 0) { close(ldev->fd); } } static void metal_uio_dev_irq_ack(struct linux_bus *lbus, struct linux_device *ldev, int irq) { (void)lbus; (void)irq; int irq_info = 1; unsigned int val; int ret; ret = read(ldev->fd, (void *)&val, sizeof(val)); if (ret < 0) { metal_log(METAL_LOG_ERROR, "%s, read uio irq fd %d failed: %d.\n", __func__, ldev->fd, ret); return; } ret = write(ldev->fd, &irq_info, sizeof(irq_info)); if (ret < 0) { metal_log(METAL_LOG_ERROR, "%s, write uio irq fd %d failed: %d.\n", __func__, ldev->fd, errno); } } static int metal_uio_dev_dma_map(struct linux_bus *lbus, struct linux_device *ldev, uint32_t dir, struct metal_sg *sg_in, int nents_in, struct metal_sg *sg_out) { int i, j; void *vaddr_sg_lo, *vaddr_sg_hi, *vaddr_lo, *vaddr_hi; struct metal_io_region *io; (void)lbus; (void)dir; /* Check if the the input virt address is MMIO address */ for (i = 0; i < nents_in; i++) { vaddr_sg_lo = sg_in[i].virt; vaddr_sg_hi = vaddr_sg_lo + sg_in[i].len; for (j = 0, io = ldev->device.regions; j < (int)ldev->device.num_regions; j++, io++) { vaddr_lo = io->virt; vaddr_hi = vaddr_lo + io->size; if (vaddr_sg_lo >= vaddr_lo && vaddr_sg_hi <= vaddr_hi) { break; } } if (j == (int)ldev->device.num_regions) { metal_log(METAL_LOG_WARNING, "%s,%s: input address isn't MMIO addr: 0x%x,%d.\n", __func__, ldev->dev_name, vaddr_sg_lo, sg_in[i].len); return -EINVAL; } } if (sg_out != sg_in) memcpy(sg_out, sg_in, nents_in*(sizeof(struct metal_sg))); return nents_in; } static void metal_uio_dev_dma_unmap(struct linux_bus *lbus, struct linux_device *ldev, uint32_t dir, struct metal_sg *sg, int nents) { (void) lbus; (void) ldev; (void) dir; (void) sg; (void) nents; return; } static struct linux_bus linux_bus[] = { { .bus_name = "platform", .drivers = { { .drv_name = "uio_pdrv_genirq", .mod_name = "uio_pdrv_genirq", .cls_name = "uio", .dev_open = metal_uio_dev_open, .dev_close = metal_uio_dev_close, .dev_irq_ack = metal_uio_dev_irq_ack, .dev_dma_map = metal_uio_dev_dma_map, .dev_dma_unmap = metal_uio_dev_dma_unmap, }, { .drv_name = "uio_dmem_genirq", .mod_name = "uio_dmem_genirq", .cls_name = "uio", .dev_open = metal_uio_dev_open, .dev_close = metal_uio_dev_close, .dev_irq_ack = metal_uio_dev_irq_ack, .dev_dma_map = metal_uio_dev_dma_map, .dev_dma_unmap = metal_uio_dev_dma_unmap, }, { 0 /* sentinel */ } } }, { .bus_name = "pci", .drivers = { { .drv_name = "vfio-pci", .mod_name = "vfio-pci", }, { .drv_name = "uio_pci_generic", .mod_name = "uio_pci_generic", .cls_name = "uio", .dev_open = metal_uio_dev_open, .dev_close = metal_uio_dev_close, .dev_irq_ack = metal_uio_dev_irq_ack, .dev_dma_map = metal_uio_dev_dma_map, .dev_dma_unmap = metal_uio_dev_dma_unmap, }, { 0 /* sentinel */ } } }, { /* sentinel */ .bus_name = NULL, }, }; #define for_each_linux_bus(lbus) \ for ((lbus) = linux_bus; (lbus)->bus_name; (lbus)++) #define for_each_linux_driver(lbus, ldrv) \ for ((ldrv) = lbus->drivers; (ldrv)->drv_name; (ldrv)++) static int metal_linux_dev_open(struct metal_bus *bus, const char *dev_name, struct metal_device **device) { struct linux_bus *lbus = to_linux_bus(bus); struct linux_device *ldev = NULL; struct linux_driver *ldrv; int error; ldev = malloc(sizeof(*ldev)); if (!ldev) return -ENOMEM; for_each_linux_driver(lbus, ldrv) { /* Check if we have a viable driver. */ if (!ldrv->sdrv || !ldrv->dev_open) continue; /* Allocate a linux device if we haven't already. */ if (!ldev) ldev = malloc(sizeof(*ldev)); if (!ldev) return -ENOMEM; /* Reset device data. */ memset(ldev, 0, sizeof(*ldev)); strncpy(ldev->dev_name, dev_name, sizeof(ldev->dev_name) - 1); ldev->fd = -1; ldev->ldrv = ldrv; ldev->device.bus = bus; /* Try and open the device. */ error = ldrv->dev_open(lbus, ldev); if (error) { ldrv->dev_close(lbus, ldev); continue; } *device = &ldev->device; (*device)->name = ldev->dev_name; metal_list_add_tail(&bus->devices, &(*device)->node); return 0; } if (ldev) free(ldev); return -ENODEV; } static void metal_linux_dev_close(struct metal_bus *bus, struct metal_device *device) { struct linux_device *ldev = to_linux_device(device); struct linux_bus *lbus = to_linux_bus(bus); ldev->ldrv->dev_close(lbus, ldev); metal_list_del(&device->node); free(ldev); } static void metal_linux_bus_close(struct metal_bus *bus) { struct linux_bus *lbus = to_linux_bus(bus); struct linux_driver *ldrv; for_each_linux_driver(lbus, ldrv) { if (ldrv->sdrv) sysfs_close_driver(ldrv->sdrv); ldrv->sdrv = NULL; } sysfs_close_bus(lbus->sbus); lbus->sbus = NULL; } static void metal_linux_dev_irq_ack(struct metal_bus *bus, struct metal_device *device, int irq) { struct linux_device *ldev = to_linux_device(device); struct linux_bus *lbus = to_linux_bus(bus); return ldev->ldrv->dev_irq_ack(lbus, ldev, irq); } static int metal_linux_dev_dma_map(struct metal_bus *bus, struct metal_device *device, uint32_t dir, struct metal_sg *sg_in, int nents_in, struct metal_sg *sg_out) { struct linux_device *ldev = to_linux_device(device); struct linux_bus *lbus = to_linux_bus(bus); return ldev->ldrv->dev_dma_map(lbus, ldev, dir, sg_in, nents_in, sg_out); } static void metal_linux_dev_dma_unmap(struct metal_bus *bus, struct metal_device *device, uint32_t dir, struct metal_sg *sg, int nents) { struct linux_device *ldev = to_linux_device(device); struct linux_bus *lbus = to_linux_bus(bus); ldev->ldrv->dev_dma_unmap(lbus, ldev, dir, sg, nents); } static const struct metal_bus_ops metal_linux_bus_ops = { .bus_close = metal_linux_bus_close, .dev_open = metal_linux_dev_open, .dev_close = metal_linux_dev_close, .dev_irq_ack = metal_linux_dev_irq_ack, .dev_dma_map = metal_linux_dev_dma_map, .dev_dma_unmap = metal_linux_dev_dma_unmap, }; static int metal_linux_register_bus(struct linux_bus *lbus) { lbus->bus.name = lbus->bus_name; lbus->bus.ops = metal_linux_bus_ops; return metal_bus_register(&lbus->bus); } static int metal_linux_probe_driver(struct linux_bus *lbus, struct linux_driver *ldrv) { char command[256]; int ret; ldrv->sdrv = sysfs_open_driver(lbus->bus_name, ldrv->drv_name); /* Try probing the module and then open the driver. */ if (!ldrv->sdrv) { ret = snprintf(command, sizeof(command), "modprobe %s > /dev/null 2>&1", ldrv->mod_name); if (ret >= (int)sizeof(command)) return -EOVERFLOW; ret = system(command); if (ret < 0) { metal_log(METAL_LOG_WARNING, "%s: executing system command '%s' failed.\n", __func__, command); } ldrv->sdrv = sysfs_open_driver(lbus->bus_name, ldrv->drv_name); } /* Try sudo probing the module and then open the driver. */ if (!ldrv->sdrv) { ret = snprintf(command, sizeof(command), "sudo modprobe %s > /dev/null 2>&1", ldrv->mod_name); if (ret >= (int)sizeof(command)) return -EOVERFLOW; ret = system(command); if (ret < 0) { metal_log(METAL_LOG_WARNING, "%s: executing system command '%s' failed.\n", __func__, command); } ldrv->sdrv = sysfs_open_driver(lbus->bus_name, ldrv->drv_name); } /* If all else fails... */ return ldrv->sdrv ? 0 : -ENODEV; } static int metal_linux_probe_bus(struct linux_bus *lbus) { struct linux_driver *ldrv; int error = -ENODEV; lbus->sbus = sysfs_open_bus(lbus->bus_name); if (!lbus->sbus) return -ENODEV; for_each_linux_driver(lbus, ldrv) { error = metal_linux_probe_driver(lbus, ldrv); if (!error) break; } if (error) { sysfs_close_bus(lbus->sbus); lbus->sbus = NULL; return error; } error = metal_linux_register_bus(lbus); if (error) { sysfs_close_driver(ldrv->sdrv); ldrv->sdrv = NULL; sysfs_close_bus(lbus->sbus); lbus->sbus = NULL; } return error; } int metal_linux_bus_init(void) { struct linux_bus *lbus; int valid = 0; for_each_linux_bus(lbus) valid += metal_linux_probe_bus(lbus) ? 0 : 1; return valid ? 0 : -ENODEV; } void metal_linux_bus_finish(void) { struct linux_bus *lbus; struct metal_bus *bus; for_each_linux_bus(lbus) { if (metal_bus_find(lbus->bus_name, &bus) == 0) metal_bus_unregister(bus); } } int metal_generic_dev_sys_open(struct metal_device *dev) { (void)dev; return 0; } int metal_linux_get_device_property(struct metal_device *device, const char *property_name, void *output, int len) { int fd = 0; int status = 0; const int flags = O_RDONLY; const int mode = S_IRUSR | S_IRGRP | S_IROTH; struct linux_device *ldev = to_linux_device(device); char path[PATH_MAX]; snprintf(path, sizeof(path), "%s/of_node/%s", ldev->sdev->path, property_name); fd = open(path, flags, mode); if (fd < 0) return -errno; status = read(fd, output, len); return status < 0 ? -errno : 0; }