numam-spdk/lib/vmd/vmd.c
Konrad Sztyber ce6171d465 lib/vmd: detach devices during shutdown
Added spdk_vmd_fini(), which detaches all PCI devices acquired by the
VMD subsystem.

Fixes #1148

Change-Id: I43218ef5f9a764546b655c28688897fb91b779cb
Signed-off-by: Konrad Sztyber <konrad.sztyber@intel.com>
Reviewed-on: https://review.gerrithub.io/c/spdk/spdk/+/482852
Community-CI: SPDK CI Jenkins <sys_sgci@intel.com>
Tested-by: SPDK CI Jenkins <sys_sgci@intel.com>
Reviewed-by: Shuhei Matsumoto <shuhei.matsumoto.xt@hitachi.com>
Reviewed-by: Tomasz Zawadzki <tomasz.zawadzki@intel.com>
Reviewed-by: Jim Harris <james.r.harris@intel.com>
2020-02-04 16:50:25 +00:00

1378 lines
38 KiB
C

/*-
* BSD LICENSE
*
* Copyright (c) Intel Corporation.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Intel Corporation nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "vmd.h"
#include "spdk/stdinc.h"
#include "spdk/likely.h"
static unsigned char *device_type[] = {
"PCI Express Endpoint",
"Legacy PCI Express Endpoint",
"Reserved 1",
"Reserved 2",
"Root Port of PCI Express Root Complex",
"Upstream Port of PCI Express Switch",
"Downstream Port of PCI Express Switch",
"PCI Express to PCI/PCI-X Bridge",
"PCI/PCI-X to PCI Express Bridge",
"Root Complex Integrated Endpoint",
"Root Complex Event Collector",
"Reserved Capability"
};
/*
* Container for all VMD adapter probed in the system.
*/
struct vmd_container {
uint32_t count;
struct vmd_adapter vmd[MAX_VMD_SUPPORTED];
};
static struct vmd_container g_vmd_container;
static uint8_t g_end_device_count;
static bool
vmd_is_valid_cfg_addr(struct vmd_pci_bus *bus, uint64_t addr)
{
return addr >= (uint64_t)bus->vmd->cfg_vaddr &&
addr < bus->vmd->cfgbar_size + (uint64_t)bus->vmd->cfg_vaddr;
}
static void
vmd_align_base_addrs(struct vmd_adapter *vmd, uint32_t alignment)
{
uint32_t pad;
/*
* Device is not in hot plug path, align the base address remaining from membar 1.
*/
if (vmd->physical_addr & (alignment - 1)) {
pad = alignment - (vmd->physical_addr & (alignment - 1));
vmd->physical_addr += pad;
vmd->current_addr_size -= pad;
}
}
static bool
vmd_device_is_enumerated(const struct vmd_pci_device *vmd_device)
{
return vmd_device->header->one.prefetch_base_upper == VMD_UPPER_BASE_SIGNATURE &&
vmd_device->header->one.prefetch_limit_upper == VMD_UPPER_LIMIT_SIGNATURE;
}
static bool
vmd_device_is_root_port(const struct vmd_pci_device *vmd_device)
{
return vmd_device->header->common.vendor_id == 0x8086 &&
(vmd_device->header->common.device_id == 0x2030 ||
vmd_device->header->common.device_id == 0x2031 ||
vmd_device->header->common.device_id == 0x2032 ||
vmd_device->header->common.device_id == 0x2033);
}
static void
vmd_hotplug_coalesce_regions(struct vmd_hot_plug *hp)
{
struct pci_mem_mgr *region, *prev;
do {
prev = NULL;
TAILQ_FOREACH(region, &hp->free_mem_queue, tailq) {
if (prev != NULL && (prev->addr + prev->size == region->addr)) {
break;
}
prev = region;
}
if (region != NULL) {
prev->size += region->size;
TAILQ_REMOVE(&hp->free_mem_queue, region, tailq);
TAILQ_INSERT_TAIL(&hp->unused_mem_queue, region, tailq);
}
} while (region != NULL);
}
static void
vmd_hotplug_free_region(struct vmd_hot_plug *hp, struct pci_mem_mgr *region)
{
struct pci_mem_mgr *current, *prev = NULL;
assert(region->addr >= hp->bar.start && region->addr < hp->bar.start + hp->bar.size);
TAILQ_FOREACH(current, &hp->free_mem_queue, tailq) {
if (current->addr > region->addr) {
break;
}
prev = current;
}
if (prev != NULL) {
assert(prev->addr + prev->size <= region->addr);
assert(current == NULL || (region->addr + region->size <= current->addr));
TAILQ_INSERT_AFTER(&hp->free_mem_queue, prev, region, tailq);
} else {
TAILQ_INSERT_HEAD(&hp->free_mem_queue, region, tailq);
}
vmd_hotplug_coalesce_regions(hp);
}
static void
vmd_hotplug_free_addr(struct vmd_hot_plug *hp, uint64_t addr)
{
struct pci_mem_mgr *region;
TAILQ_FOREACH(region, &hp->alloc_mem_queue, tailq) {
if (region->addr == addr) {
break;
}
}
assert(region != NULL);
TAILQ_REMOVE(&hp->alloc_mem_queue, region, tailq);
vmd_hotplug_free_region(hp, region);
}
static uint64_t
vmd_hotplug_allocate_base_addr(struct vmd_hot_plug *hp, uint32_t size)
{
struct pci_mem_mgr *region = NULL, *free_region;
TAILQ_FOREACH(region, &hp->free_mem_queue, tailq) {
if (region->size >= size) {
break;
}
}
if (region == NULL) {
SPDK_DEBUGLOG(SPDK_LOG_VMD, "Unable to find free hotplug memory region of size:"
"%"PRIx32"\n", size);
return 0;
}
TAILQ_REMOVE(&hp->free_mem_queue, region, tailq);
if (size < region->size) {
free_region = TAILQ_FIRST(&hp->unused_mem_queue);
if (free_region == NULL) {
SPDK_DEBUGLOG(SPDK_LOG_VMD, "Unable to find unused descriptor to store the "
"free region of size: %"PRIu32"\n", region->size - size);
} else {
TAILQ_REMOVE(&hp->unused_mem_queue, free_region, tailq);
free_region->size = region->size - size;
free_region->addr = region->addr + size;
region->size = size;
vmd_hotplug_free_region(hp, free_region);
}
}
TAILQ_INSERT_TAIL(&hp->alloc_mem_queue, region, tailq);
return region->addr;
}
/*
* Allocates an address from vmd membar for the input memory size
* vmdAdapter - vmd adapter object
* dev - vmd_pci_device to allocate a base address for.
* size - size of the memory window requested.
* Size must be an integral multiple of 2. Addresses are returned on the size boundary.
* Returns physical address within the VMD membar window, or 0x0 if cannot allocate window.
* Consider increasing the size of vmd membar if 0x0 is returned.
*/
static uint64_t
vmd_allocate_base_addr(struct vmd_adapter *vmd, struct vmd_pci_device *dev, uint32_t size)
{
uint64_t base_address = 0, padding = 0;
struct vmd_pci_bus *hp_bus;
if (size && ((size & (~size + 1)) != size)) {
return base_address;
}
/*
* If device is downstream of a hot plug port, allocate address from the
* range dedicated for the hot plug slot. Search the list of addresses allocated to determine
* if a free range exists that satisfy the input request. If a free range cannot be found,
* get a buffer from the unused chunk. First fit algorithm, is used.
*/
if (dev) {
hp_bus = dev->parent;
if (hp_bus && hp_bus->self && hp_bus->self->hotplug_capable) {
return vmd_hotplug_allocate_base_addr(&hp_bus->self->hp, size);
}
}
/* Ensure physical membar allocated is size aligned */
if (vmd->physical_addr & (size - 1)) {
padding = size - (vmd->physical_addr & (size - 1));
}
/* Allocate from membar if enough memory is left */
if (vmd->current_addr_size >= size + padding) {
base_address = vmd->physical_addr + padding;
vmd->physical_addr += size + padding;
vmd->current_addr_size -= size + padding;
}
SPDK_DEBUGLOG(SPDK_LOG_VMD, "allocated(size) %lx (%x)\n", base_address, size);
return base_address;
}
static bool
vmd_is_end_device(struct vmd_pci_device *dev)
{
return (dev && dev->header) &&
((dev->header->common.header_type & ~PCI_MULTI_FUNCTION) == PCI_HEADER_TYPE_NORMAL);
}
static void
vmd_update_base_limit_register(struct vmd_pci_device *dev, uint16_t base, uint16_t limit)
{
struct vmd_pci_bus *bus;
struct vmd_pci_device *bridge;
if (base == 0 || limit == 0) {
return;
}
if (dev->header->common.header_type == PCI_HEADER_TYPE_BRIDGE) {
bus = dev->bus_object;
} else {
bus = dev->parent;
}
bridge = bus->self;
SPDK_DEBUGLOG(SPDK_LOG_VMD, "base:limit = %x:%x\n", bridge->header->one.mem_base,
bridge->header->one.mem_limit);
if (dev->bus->vmd->scan_completed) {
return;
}
while (bus && bus->self != NULL) {
bridge = bus->self;
/* This is only for 32-bit memory space, need to revisit to support 64-bit */
if (bridge->header->one.mem_base > base) {
bridge->header->one.mem_base = base;
base = bridge->header->one.mem_base;
}
if (bridge->header->one.mem_limit < limit) {
bridge->header->one.mem_limit = limit;
limit = bridge->header->one.mem_limit;
}
bus = bus->parent;
}
}
static uint64_t
vmd_get_base_addr(struct vmd_pci_device *dev, uint32_t index, uint32_t size)
{
struct vmd_pci_bus *bus = dev->parent;
if (dev->header_type == PCI_HEADER_TYPE_BRIDGE) {
return dev->header->zero.BAR[index] & ~0xf;
} else {
if (bus->self->hotplug_capable) {
return vmd_hotplug_allocate_base_addr(&bus->self->hp, size);
} else {
return (uint64_t)bus->self->header->one.mem_base << 16;
}
}
}
static bool
vmd_assign_base_addrs(struct vmd_pci_device *dev)
{
uint16_t mem_base = 0, mem_limit = 0;
unsigned char mem_attr = 0;
int last;
struct vmd_adapter *vmd = NULL;
bool ret_val = false;
uint32_t bar_value;
uint32_t table_offset;
if (dev && dev->bus) {
vmd = dev->bus->vmd;
}
if (!vmd) {
return 0;
}
vmd_align_base_addrs(vmd, ONE_MB);
last = dev->header_type ? 2 : 6;
for (int i = 0; i < last; i++) {
bar_value = dev->header->zero.BAR[i];
dev->header->zero.BAR[i] = ~(0U);
dev->bar[i].size = dev->header->zero.BAR[i];
dev->header->zero.BAR[i] = bar_value;
if (dev->bar[i].size == ~(0U) || dev->bar[i].size == 0 ||
dev->header->zero.BAR[i] & 1) {
dev->bar[i].size = 0;
continue;
}
mem_attr = dev->bar[i].size & PCI_BASE_ADDR_MASK;
dev->bar[i].size = TWOS_COMPLEMENT(dev->bar[i].size & PCI_BASE_ADDR_MASK);
if (vmd->scan_completed) {
dev->bar[i].start = vmd_get_base_addr(dev, i, dev->bar[i].size);
} else {
dev->bar[i].start = vmd_allocate_base_addr(vmd, dev, dev->bar[i].size);
}
dev->header->zero.BAR[i] = (uint32_t)dev->bar[i].start;
if (!dev->bar[i].start) {
if (mem_attr == (PCI_BAR_MEMORY_PREFETCH | PCI_BAR_MEMORY_TYPE_64)) {
i++;
}
continue;
}
dev->bar[i].vaddr = ((uint64_t)vmd->mem_vaddr + (dev->bar[i].start - vmd->membar));
mem_limit = BRIDGE_BASEREG(dev->header->zero.BAR[i]) +
BRIDGE_BASEREG(dev->bar[i].size - 1);
if (!mem_base) {
mem_base = BRIDGE_BASEREG(dev->header->zero.BAR[i]);
}
ret_val = true;
if (mem_attr == (PCI_BAR_MEMORY_PREFETCH | PCI_BAR_MEMORY_TYPE_64)) {
i++;
if (i < last) {
dev->header->zero.BAR[i] = (uint32_t)(dev->bar[i].start >> PCI_DWORD_SHIFT);
}
}
}
/* Enable device MEM and bus mastering */
dev->header->zero.command |= (PCI_COMMAND_MEMORY | PCI_COMMAND_MASTER);
uint16_t cmd = dev->header->zero.command;
cmd++;
if (dev->msix_cap && ret_val) {
table_offset = ((volatile struct pci_msix_cap *)dev->msix_cap)->msix_table_offset;
if (dev->bar[table_offset & 0x3].vaddr) {
dev->msix_table = (volatile struct pci_msix_table_entry *)
(dev->bar[table_offset & 0x3].vaddr + (table_offset & 0xfff8));
}
}
if (ret_val && vmd_is_end_device(dev)) {
vmd_update_base_limit_register(dev, mem_base, mem_limit);
}
return ret_val;
}
static void
vmd_get_device_capabilities(struct vmd_pci_device *dev)
{
volatile uint8_t *config_space;
uint8_t capabilities_offset;
struct pci_capabilities_header *capabilities_hdr;
config_space = (volatile uint8_t *)dev->header;
if ((dev->header->common.status & PCI_CAPABILITIES_LIST) == 0) {
return;
}
capabilities_offset = dev->header->zero.cap_pointer;
if (dev->header->common.header_type & PCI_HEADER_TYPE_BRIDGE) {
capabilities_offset = dev->header->one.cap_pointer;
}
while (capabilities_offset > 0) {
capabilities_hdr = (struct pci_capabilities_header *)
&config_space[capabilities_offset];
switch (capabilities_hdr->capability_id) {
case CAPABILITY_ID_PCI_EXPRESS:
dev->pcie_cap = (volatile struct pci_express_cap *)(capabilities_hdr);
break;
case CAPABILITY_ID_MSI:
dev->msi_cap = (volatile struct pci_msi_cap *)capabilities_hdr;
break;
case CAPABILITY_ID_MSIX:
dev->msix_cap = (volatile struct pci_msix_capability *)capabilities_hdr;
dev->msix_table_size = dev->msix_cap->message_control.bit.table_size + 1;
break;
default:
break;
}
capabilities_offset = capabilities_hdr->next;
}
}
static volatile struct pci_enhanced_capability_header *
vmd_get_enhanced_capabilities(struct vmd_pci_device *dev, uint16_t capability_id)
{
uint8_t *data;
uint16_t cap_offset = EXTENDED_CAPABILITY_OFFSET;
volatile struct pci_enhanced_capability_header *cap_hdr = NULL;
data = (uint8_t *)dev->header;
while (cap_offset >= EXTENDED_CAPABILITY_OFFSET) {
cap_hdr = (volatile struct pci_enhanced_capability_header *) &data[cap_offset];
if (cap_hdr->capability_id == capability_id) {
return cap_hdr;
}
cap_offset = cap_hdr->next;
if (cap_offset == 0 || cap_offset < EXTENDED_CAPABILITY_OFFSET) {
break;
}
}
return NULL;
}
static void
vmd_read_config_space(struct vmd_pci_device *dev)
{
/*
* Writes to the pci config space is posted weite. To ensure transaction reaches its destination
* before another write is posed, an immediate read of the written value should be performed.
*/
dev->header->common.command |= (BUS_MASTER_ENABLE | MEMORY_SPACE_ENABLE);
{ uint16_t cmd = dev->header->common.command; (void)cmd; }
vmd_get_device_capabilities(dev);
dev->sn_cap = (struct serial_number_capability *)vmd_get_enhanced_capabilities(dev,
DEVICE_SERIAL_NUMBER_CAP_ID);
}
static void
vmd_update_scan_info(struct vmd_pci_device *dev)
{
struct vmd_adapter *vmd_adapter = dev->bus->vmd;
if (vmd_adapter->root_port_updated) {
return;
}
if (dev->header_type == PCI_HEADER_TYPE_NORMAL) {
return;
}
if (vmd_device_is_root_port(dev)) {
vmd_adapter->root_port_updated = 1;
SPDK_DEBUGLOG(SPDK_LOG_VMD, "root_port_updated = %d\n",
vmd_adapter->root_port_updated);
SPDK_DEBUGLOG(SPDK_LOG_VMD, "upper:limit = %x : %x\n",
dev->header->one.prefetch_base_upper,
dev->header->one.prefetch_limit_upper);
if (vmd_device_is_enumerated(dev)) {
vmd_adapter->scan_completed = 1;
SPDK_DEBUGLOG(SPDK_LOG_VMD, "scan_completed = %d\n",
vmd_adapter->scan_completed);
}
}
}
static void
vmd_reset_base_limit_registers(struct vmd_pci_device *dev)
{
uint32_t reg __attribute__((unused));
assert(dev->header_type != PCI_HEADER_TYPE_NORMAL);
/*
* Writes to the pci config space are posted writes.
* To ensure transaction reaches its destination
* before another write is posted, an immediate read
* of the written value should be performed.
*/
dev->header->one.mem_base = 0xfff0;
reg = dev->header->one.mem_base;
dev->header->one.mem_limit = 0x0;
reg = dev->header->one.mem_limit;
dev->header->one.prefetch_base = 0x0;
reg = dev->header->one.prefetch_base;
dev->header->one.prefetch_limit = 0x0;
reg = dev->header->one.prefetch_limit;
dev->header->one.prefetch_base_upper = 0x0;
reg = dev->header->one.prefetch_base_upper;
dev->header->one.prefetch_limit_upper = 0x0;
reg = dev->header->one.prefetch_limit_upper;
dev->header->one.io_base_upper = 0x0;
reg = dev->header->one.io_base_upper;
dev->header->one.io_limit_upper = 0x0;
reg = dev->header->one.io_limit_upper;
dev->header->one.primary = 0;
reg = dev->header->one.primary;
dev->header->one.secondary = 0;
reg = dev->header->one.secondary;
dev->header->one.subordinate = 0;
reg = dev->header->one.subordinate;
}
static void
vmd_init_hotplug(struct vmd_pci_device *dev, struct vmd_pci_bus *bus)
{
struct vmd_adapter *vmd = bus->vmd;
struct vmd_hot_plug *hp = &dev->hp;
size_t mem_id;
dev->hotplug_capable = true;
hp->bar.size = 1 << 20;
if (!vmd->scan_completed) {
hp->bar.start = vmd_allocate_base_addr(vmd, NULL, hp->bar.size);
bus->self->header->one.mem_base = BRIDGE_BASEREG(hp->bar.start);
bus->self->header->one.mem_limit =
bus->self->header->one.mem_base + BRIDGE_BASEREG(hp->bar.size - 1);
} else {
hp->bar.start = (uint64_t)bus->self->header->one.mem_base << 16;
}
hp->bar.vaddr = (uint64_t)vmd->mem_vaddr + (hp->bar.start - vmd->membar);
TAILQ_INIT(&hp->free_mem_queue);
TAILQ_INIT(&hp->unused_mem_queue);
TAILQ_INIT(&hp->alloc_mem_queue);
hp->mem[0].size = hp->bar.size;
hp->mem[0].addr = hp->bar.start;
TAILQ_INSERT_TAIL(&hp->free_mem_queue, &hp->mem[0], tailq);
for (mem_id = 1; mem_id < ADDR_ELEM_COUNT; ++mem_id) {
TAILQ_INSERT_TAIL(&hp->unused_mem_queue, &hp->mem[mem_id], tailq);
}
SPDK_DEBUGLOG(SPDK_LOG_VMD, "%s: mem_base:mem_limit = %x : %x\n", __func__,
bus->self->header->one.mem_base, bus->self->header->one.mem_limit);
}
static bool
vmd_bus_device_present(struct vmd_pci_bus *bus, uint32_t devfn)
{
volatile struct pci_header *header;
header = (volatile struct pci_header *)(bus->vmd->cfg_vaddr +
CONFIG_OFFSET_ADDR(bus->bus_number, devfn, 0, 0));
if (!vmd_is_valid_cfg_addr(bus, (uint64_t)header)) {
return false;
}
if (header->common.vendor_id == PCI_INVALID_VENDORID || header->common.vendor_id == 0x0) {
return false;
}
return true;
}
static struct vmd_pci_device *
vmd_alloc_dev(struct vmd_pci_bus *bus, uint32_t devfn)
{
struct vmd_pci_device *dev = NULL;
struct pci_header volatile *header;
uint8_t header_type;
uint32_t rev_class;
/* Make sure we're not creating two devices on the same dev/fn */
TAILQ_FOREACH(dev, &bus->dev_list, tailq) {
if (dev->devfn == devfn) {
return NULL;
}
}
if (!vmd_bus_device_present(bus, devfn)) {
return NULL;
}
header = (struct pci_header * volatile)(bus->vmd->cfg_vaddr +
CONFIG_OFFSET_ADDR(bus->bus_number, devfn, 0, 0));
SPDK_DEBUGLOG(SPDK_LOG_VMD, "PCI device found: %04x:%04x ***\n",
header->common.vendor_id, header->common.device_id);
dev = calloc(1, sizeof(*dev));
if (!dev) {
return NULL;
}
dev->header = header;
dev->vid = dev->header->common.vendor_id;
dev->did = dev->header->common.device_id;
dev->bus = bus;
dev->parent = bus;
dev->devfn = devfn;
header_type = dev->header->common.header_type;
rev_class = dev->header->common.rev_class;
dev->class = rev_class >> 8;
dev->header_type = header_type & 0x7;
if (header_type == PCI_HEADER_TYPE_BRIDGE) {
vmd_update_scan_info(dev);
if (!dev->bus->vmd->scan_completed) {
vmd_reset_base_limit_registers(dev);
}
}
vmd_read_config_space(dev);
return dev;
}
static struct vmd_pci_bus *
vmd_create_new_bus(struct vmd_pci_bus *parent, struct vmd_pci_device *bridge, uint8_t bus_number)
{
struct vmd_pci_bus *new_bus;
new_bus = calloc(1, sizeof(*new_bus));
if (!new_bus) {
return NULL;
}
new_bus->parent = parent;
new_bus->domain = parent->domain;
new_bus->bus_number = bus_number;
new_bus->secondary_bus = new_bus->subordinate_bus = bus_number;
new_bus->self = bridge;
new_bus->vmd = parent->vmd;
TAILQ_INIT(&new_bus->dev_list);
bridge->subordinate = new_bus;
bridge->pci.addr.bus = new_bus->bus_number;
bridge->pci.addr.dev = bridge->devfn;
bridge->pci.addr.func = 0;
bridge->pci.addr.domain = parent->vmd->pci->addr.domain;
return new_bus;
}
/*
* Assigns a bus number from the list of available
* bus numbers. If the device is downstream of a hot plug port,
* assign the bus number from thiose assigned to the HP port. Otherwise,
* assign the next bus number from the vmd bus number list.
*/
static uint8_t
vmd_get_next_bus_number(struct vmd_pci_device *dev, struct vmd_adapter *vmd)
{
uint8_t bus = 0xff;
struct vmd_pci_bus *hp_bus;
if (dev) {
hp_bus = vmd_is_dev_in_hotplug_path(dev);
if (hp_bus && hp_bus->self && hp_bus->self->hotplug_capable) {
return vmd_hp_get_next_bus_number(&hp_bus->self->hp);
}
}
/* Device is not under a hot plug path. Return next global bus number */
if ((vmd->next_bus_number + 1) < vmd->max_pci_bus) {
bus = vmd->next_bus_number;
vmd->next_bus_number++;
}
return bus;
}
static uint8_t
vmd_get_hotplug_bus_numbers(struct vmd_pci_device *dev)
{
uint8_t bus_number = 0xff;
if (dev && dev->bus && dev->bus->vmd &&
((dev->bus->vmd->next_bus_number + RESERVED_HOTPLUG_BUSES) < dev->bus->vmd->max_pci_bus)) {
bus_number = RESERVED_HOTPLUG_BUSES;
dev->bus->vmd->next_bus_number += RESERVED_HOTPLUG_BUSES;
}
return bus_number;
}
static void
vmd_enable_msix(struct vmd_pci_device *dev)
{
volatile uint16_t control;
control = dev->msix_cap->message_control.as_uint16_t | (1 << 14);
dev->msix_cap->message_control.as_uint16_t = control;
control = dev->msix_cap->message_control.as_uint16_t;
dev->msix_cap->message_control.as_uint16_t = (control | (1 << 15));
control = dev->msix_cap->message_control.as_uint16_t;
control = control & ~(1 << 14);
dev->msix_cap->message_control.as_uint16_t = control;
control = dev->msix_cap->message_control.as_uint16_t;
}
static void
vmd_disable_msix(struct vmd_pci_device *dev)
{
volatile uint16_t control;
control = dev->msix_cap->message_control.as_uint16_t | (1 << 14);
dev->msix_cap->message_control.as_uint16_t = control;
control = dev->msix_cap->message_control.as_uint16_t & ~(1 << 15);
dev->msix_cap->message_control.as_uint16_t = control;
control = dev->msix_cap->message_control.as_uint16_t;
}
/*
* Set up MSI-X table entries for the port. Vmd MSIX vector 0 is used for
* port interrupt, so vector 0 is mapped to all MSIX entries for the port.
*/
static void
vmd_setup_msix(struct vmd_pci_device *dev, volatile struct pci_msix_table_entry *vmdEntry)
{
int entry;
if (!dev || !vmdEntry || !dev->msix_cap) {
return;
}
vmd_disable_msix(dev);
if (dev->msix_table == NULL || dev->msix_table_size > MAX_MSIX_TABLE_SIZE) {
return;
}
for (entry = 0; entry < dev->msix_table_size; ++entry) {
dev->msix_table[entry].vector_control = 1;
}
vmd_enable_msix(dev);
}
static void
vmd_bus_update_bridge_info(struct vmd_pci_device *bridge)
{
/* Update the subordinate bus of all bridges above this bridge */
volatile struct vmd_pci_device *dev = bridge;
uint8_t subordinate_bus;
if (!dev) {
return;
}
subordinate_bus = bridge->header->one.subordinate;
while (dev->parent_bridge != NULL) {
dev = dev->parent_bridge;
if (dev->header->one.subordinate < subordinate_bus) {
dev->header->one.subordinate = subordinate_bus;
subordinate_bus = dev->header->one.subordinate;
}
}
}
static bool
vmd_is_supported_device(struct vmd_pci_device *dev)
{
return dev->class == PCI_CLASS_STORAGE_EXPRESS;
}
static int
vmd_dev_map_bar(struct spdk_pci_device *pci_dev, uint32_t bar,
void **mapped_addr, uint64_t *phys_addr, uint64_t *size)
{
struct vmd_pci_device *dev = SPDK_CONTAINEROF(pci_dev, struct vmd_pci_device, pci);
*size = dev->bar[bar].size;
*phys_addr = dev->bar[bar].start;
*mapped_addr = (void *)dev->bar[bar].vaddr;
return 0;
}
static int
vmd_dev_unmap_bar(struct spdk_pci_device *_dev, uint32_t bar, void *addr)
{
return 0;
}
static int
vmd_dev_cfg_read(struct spdk_pci_device *_dev, void *value, uint32_t len,
uint32_t offset)
{
struct vmd_pci_device *dev = SPDK_CONTAINEROF(_dev, struct vmd_pci_device, pci);
volatile uint8_t *src = (volatile uint8_t *)dev->header;
uint8_t *dst = value;
size_t i;
if (len + offset > PCI_MAX_CFG_SIZE) {
return -1;
}
for (i = 0; i < len; ++i) {
dst[i] = src[offset + i];
}
return 0;
}
static int
vmd_dev_cfg_write(struct spdk_pci_device *_dev, void *value,
uint32_t len, uint32_t offset)
{
struct vmd_pci_device *dev = SPDK_CONTAINEROF(_dev, struct vmd_pci_device, pci);
volatile uint8_t *dst = (volatile uint8_t *)dev->header;
uint8_t *src = value;
size_t i;
if ((len + offset) > PCI_MAX_CFG_SIZE) {
return -1;
}
for (i = 0; i < len; ++i) {
dst[offset + i] = src[i];
}
return 0;
}
static void
vmd_dev_detach(struct spdk_pci_device *dev)
{
struct vmd_pci_device *vmd_device = (struct vmd_pci_device *)dev;
struct vmd_pci_device *bus_device = vmd_device->bus->self;
struct vmd_pci_bus *bus = vmd_device->bus;
size_t i, num_bars = vmd_device->header_type ? 2 : 6;
spdk_pci_unhook_device(dev);
TAILQ_REMOVE(&bus->dev_list, vmd_device, tailq);
/* Release the hotplug region if the device is under hotplug-capable bus */
if (bus_device && bus_device->hotplug_capable) {
for (i = 0; i < num_bars; ++i) {
if (vmd_device->bar[i].start != 0) {
vmd_hotplug_free_addr(&bus_device->hp, vmd_device->bar[i].start);
}
}
}
free(dev);
}
static void
vmd_dev_init(struct vmd_pci_device *dev)
{
uint8_t bdf[32];
dev->pci.addr.domain = dev->bus->vmd->domain;
dev->pci.addr.bus = dev->bus->bus_number;
dev->pci.addr.dev = dev->devfn;
dev->pci.addr.func = 0;
dev->pci.id.vendor_id = dev->header->common.vendor_id;
dev->pci.id.device_id = dev->header->common.device_id;
dev->pci.type = "vmd";
dev->pci.map_bar = vmd_dev_map_bar;
dev->pci.unmap_bar = vmd_dev_unmap_bar;
dev->pci.cfg_read = vmd_dev_cfg_read;
dev->pci.cfg_write = vmd_dev_cfg_write;
dev->pci.detach = vmd_dev_detach;
dev->hotplug_capable = false;
if (dev->pcie_cap != NULL) {
dev->cached_slot_control = dev->pcie_cap->slot_control;
}
if (vmd_is_supported_device(dev)) {
spdk_pci_addr_fmt(bdf, sizeof(bdf), &dev->pci.addr);
SPDK_DEBUGLOG(SPDK_LOG_VMD, "Initalizing NVMe device at %s\n", bdf);
dev->pci.parent = dev->bus->vmd->pci;
spdk_pci_hook_device(spdk_pci_nvme_get_driver(), &dev->pci);
}
}
/*
* Scans a single bus for all devices attached and return a count of
* how many devices found. In the VMD topology, it is assume there are no multi-
* function devices. Hence a bus(bridge) will not have multi function with both type
* 0 and 1 header.
*
* The other option for implementing this function is the bus is an int and
* create a new device PciBridge. PciBridge would inherit from PciDevice with extra fields,
* sub/pri/sec bus. The input becomes PciPort, bus number and parent_bridge.
*
* The bus number is scanned and if a device is found, based on the header_type, create
* either PciBridge(1) or PciDevice(0).
*
* If a PciBridge, assign bus numbers and rescan new bus. The currenty PciBridge being
* scanned becomes the passed in parent_bridge with the new bus number.
*
* The linked list becomes list of pciBridges with PciDevices attached.
*
* Return count of how many devices found(type1 + type 0 header devices)
*/
static uint8_t
vmd_scan_single_bus(struct vmd_pci_bus *bus, struct vmd_pci_device *parent_bridge)
{
/* assuming only single function devices are on the bus */
struct vmd_pci_device *new_dev;
struct vmd_adapter *vmd;
union express_slot_capabilities_register slot_cap;
struct vmd_pci_bus *new_bus;
uint8_t device_number, dev_cnt = 0;
uint8_t new_bus_num;
for (device_number = 0; device_number < 32; device_number++) {
new_dev = vmd_alloc_dev(bus, device_number);
if (new_dev == NULL) {
continue;
}
dev_cnt++;
if (new_dev->header->common.header_type & PCI_HEADER_TYPE_BRIDGE) {
slot_cap.as_uint32_t = 0;
if (new_dev->pcie_cap != NULL) {
slot_cap.as_uint32_t = new_dev->pcie_cap->slot_cap.as_uint32_t;
}
new_bus_num = vmd_get_next_bus_number(bus->vmd->is_hotplug_scan ? new_dev : NULL, bus->vmd);
if (new_bus_num == 0xff) {
free(new_dev);
return dev_cnt;
}
new_bus = vmd_create_new_bus(bus, new_dev, new_bus_num);
if (!new_bus) {
free(new_dev);
return dev_cnt;
}
new_bus->primary_bus = bus->secondary_bus;
new_bus->self = new_dev;
new_dev->bus_object = new_bus;
if (slot_cap.bit_field.hotplug_capable && new_dev->pcie_cap != NULL &&
new_dev->pcie_cap->express_cap_register.bit_field.slot_implemented) {
new_bus->hotplug_buses = vmd_get_hotplug_bus_numbers(new_dev);
new_bus->subordinate_bus += new_bus->hotplug_buses;
/* Attach hot plug instance if HP is supported */
/* Hot inserted SSDs can be assigned port bus of sub-ordinate + 1 */
SPDK_DEBUGLOG(SPDK_LOG_VMD, "hotplug_capable/slot_implemented = "
"%x:%x\n", slot_cap.bit_field.hotplug_capable,
new_dev->pcie_cap->express_cap_register.bit_field.slot_implemented);
}
new_dev->parent_bridge = parent_bridge;
new_dev->header->one.primary = new_bus->primary_bus;
new_dev->header->one.secondary = new_bus->secondary_bus;
new_dev->header->one.subordinate = new_bus->subordinate_bus;
vmd_bus_update_bridge_info(new_dev);
TAILQ_INSERT_TAIL(&bus->vmd->bus_list, new_bus, tailq);
vmd_dev_init(new_dev);
if (slot_cap.bit_field.hotplug_capable && new_dev->pcie_cap != NULL &&
new_dev->pcie_cap->express_cap_register.bit_field.slot_implemented) {
vmd_init_hotplug(new_dev, new_bus);
}
dev_cnt += vmd_scan_single_bus(new_bus, new_dev);
if (new_dev->pcie_cap != NULL) {
if (new_dev->pcie_cap->express_cap_register.bit_field.device_type == SwitchUpstreamPort) {
return dev_cnt;
}
}
} else {
/* Attach the device to the current bus and assign base addresses */
TAILQ_INSERT_TAIL(&bus->dev_list, new_dev, tailq);
g_end_device_count++;
if (vmd_assign_base_addrs(new_dev)) {
vmd_setup_msix(new_dev, &bus->vmd->msix_table[0]);
vmd_dev_init(new_dev);
if (vmd_is_supported_device(new_dev)) {
vmd = bus->vmd;
vmd->target[vmd->nvme_count] = new_dev;
vmd->nvme_count++;
}
} else {
SPDK_DEBUGLOG(SPDK_LOG_VMD, "Removing failed device:%p\n", new_dev);
TAILQ_REMOVE(&bus->dev_list, new_dev, tailq);
free(new_dev);
if (dev_cnt) {
dev_cnt--;
}
}
}
}
return dev_cnt;
}
static void
vmd_print_pci_info(struct vmd_pci_device *dev)
{
if (!dev) {
return;
}
if (dev->pcie_cap != NULL) {
SPDK_INFOLOG(SPDK_LOG_VMD, "PCI DEVICE: [%04X:%04X] type(%x) : %s\n",
dev->header->common.vendor_id, dev->header->common.device_id,
dev->pcie_cap->express_cap_register.bit_field.device_type,
device_type[dev->pcie_cap->express_cap_register.bit_field.device_type]);
} else {
SPDK_INFOLOG(SPDK_LOG_VMD, "PCI DEVICE: [%04X:%04X]\n",
dev->header->common.vendor_id, dev->header->common.device_id);
}
SPDK_INFOLOG(SPDK_LOG_VMD, "\tDOMAIN:BDF: %04x:%02x:%02x:%x\n", dev->pci.addr.domain,
dev->pci.addr.bus, dev->pci.addr.dev, dev->pci.addr.func);
if (!(dev->header_type & PCI_HEADER_TYPE_BRIDGE) && dev->bus) {
SPDK_INFOLOG(SPDK_LOG_VMD, "\tbase addr: %x : %p\n",
dev->header->zero.BAR[0], (void *)dev->bar[0].vaddr);
}
if ((dev->header_type & PCI_HEADER_TYPE_BRIDGE)) {
SPDK_INFOLOG(SPDK_LOG_VMD, "\tPrimary = %d, Secondary = %d, Subordinate = %d\n",
dev->header->one.primary, dev->header->one.secondary, dev->header->one.subordinate);
if (dev->pcie_cap && dev->pcie_cap->express_cap_register.bit_field.slot_implemented) {
SPDK_INFOLOG(SPDK_LOG_VMD, "\tSlot implemented on this device.\n");
if (dev->pcie_cap->slot_cap.bit_field.hotplug_capable) {
SPDK_INFOLOG(SPDK_LOG_VMD, "Device has HOT-PLUG capable slot.\n");
}
}
}
if (dev->sn_cap != NULL) {
uint8_t *snLow = (uint8_t *)&dev->sn_cap->sn_low;
uint8_t *snHi = (uint8_t *)&dev->sn_cap->sn_hi;
SPDK_INFOLOG(SPDK_LOG_VMD, "\tSN: %02x-%02x-%02x-%02x-%02x-%02x-%02x-%02x\n",
snHi[3], snHi[2], snHi[1], snHi[0], snLow[3], snLow[2], snLow[1], snLow[0]);
}
}
static void
vmd_cache_scan_info(struct vmd_pci_device *dev)
{
uint32_t reg __attribute__((unused));
if (dev->header_type == PCI_HEADER_TYPE_NORMAL) {
return;
}
SPDK_DEBUGLOG(SPDK_LOG_VMD, "vendor/device id:%x:%x\n", dev->header->common.vendor_id,
dev->header->common.device_id);
if (vmd_device_is_root_port(dev)) {
dev->header->one.prefetch_base_upper = VMD_UPPER_BASE_SIGNATURE;
reg = dev->header->one.prefetch_base_upper;
dev->header->one.prefetch_limit_upper = VMD_UPPER_LIMIT_SIGNATURE;
reg = dev->header->one.prefetch_limit_upper;
SPDK_DEBUGLOG(SPDK_LOG_VMD, "prefetch: %x:%x\n",
dev->header->one.prefetch_base_upper,
dev->header->one.prefetch_limit_upper);
}
}
static uint8_t
vmd_scan_pcibus(struct vmd_pci_bus *bus)
{
struct vmd_pci_bus *bus_entry;
struct vmd_pci_device *dev;
uint8_t dev_cnt;
g_end_device_count = 0;
TAILQ_INSERT_TAIL(&bus->vmd->bus_list, bus, tailq);
bus->vmd->next_bus_number = bus->bus_number + 1;
dev_cnt = vmd_scan_single_bus(bus, NULL);
SPDK_DEBUGLOG(SPDK_LOG_VMD, "VMD scan found %u devices\n", dev_cnt);
SPDK_DEBUGLOG(SPDK_LOG_VMD, "VMD scan found %u END DEVICES\n", g_end_device_count);
SPDK_INFOLOG(SPDK_LOG_VMD, "PCIe devices attached to VMD %04x:%02x:%02x:%x...\n",
bus->vmd->pci->addr.domain, bus->vmd->pci->addr.bus,
bus->vmd->pci->addr.dev, bus->vmd->pci->addr.func);
TAILQ_FOREACH(bus_entry, &bus->vmd->bus_list, tailq) {
if (bus_entry->self != NULL) {
vmd_print_pci_info(bus_entry->self);
vmd_cache_scan_info(bus_entry->self);
}
TAILQ_FOREACH(dev, &bus_entry->dev_list, tailq) {
vmd_print_pci_info(dev);
}
}
return dev_cnt;
}
static int
vmd_map_bars(struct vmd_adapter *vmd, struct spdk_pci_device *dev)
{
int rc;
rc = spdk_pci_device_map_bar(dev, 0, (void **)&vmd->cfg_vaddr,
&vmd->cfgbar, &vmd->cfgbar_size);
if (rc == 0) {
rc = spdk_pci_device_map_bar(dev, 2, (void **)&vmd->mem_vaddr,
&vmd->membar, &vmd->membar_size);
}
if (rc == 0) {
rc = spdk_pci_device_map_bar(dev, 4, (void **)&vmd->msix_vaddr,
&vmd->msixbar, &vmd->msixbar_size);
}
if (rc == 0) {
vmd->physical_addr = vmd->membar;
vmd->current_addr_size = vmd->membar_size;
}
return rc;
}
static int
vmd_enumerate_devices(struct vmd_adapter *vmd)
{
vmd->vmd_bus.vmd = vmd;
vmd->vmd_bus.secondary_bus = vmd->vmd_bus.subordinate_bus = 0;
vmd->vmd_bus.primary_bus = vmd->vmd_bus.bus_number = 0;
vmd->vmd_bus.domain = vmd->pci->addr.domain;
return vmd_scan_pcibus(&vmd->vmd_bus);
}
struct vmd_pci_device *
vmd_find_device(const struct spdk_pci_addr *addr)
{
struct vmd_pci_bus *bus;
struct vmd_pci_device *dev;
int i;
for (i = 0; i < MAX_VMD_TARGET; ++i) {
TAILQ_FOREACH(bus, &g_vmd_container.vmd[i].bus_list, tailq) {
if (bus->self) {
if (spdk_pci_addr_compare(&bus->self->pci.addr, addr) == 0) {
return bus->self;
}
}
TAILQ_FOREACH(dev, &bus->dev_list, tailq) {
if (spdk_pci_addr_compare(&dev->pci.addr, addr) == 0) {
return dev;
}
}
}
}
return NULL;
}
static int
vmd_enum_cb(void *ctx, struct spdk_pci_device *pci_dev)
{
uint32_t cmd_reg = 0;
char bdf[32] = {0};
struct vmd_container *vmd_c = ctx;
size_t i;
spdk_pci_device_cfg_read32(pci_dev, &cmd_reg, 4);
cmd_reg |= 0x6; /* PCI bus master/memory enable. */
spdk_pci_device_cfg_write32(pci_dev, cmd_reg, 4);
spdk_pci_addr_fmt(bdf, sizeof(bdf), &pci_dev->addr);
SPDK_DEBUGLOG(SPDK_LOG_VMD, "Found a VMD[ %d ] at %s\n", vmd_c->count, bdf);
/* map vmd bars */
i = vmd_c->count;
vmd_c->vmd[i].pci = pci_dev;
vmd_c->vmd[i].vmd_index = i;
vmd_c->vmd[i].domain =
(pci_dev->addr.bus << 16) | (pci_dev->addr.dev << 8) | pci_dev->addr.func;
vmd_c->vmd[i].max_pci_bus = PCI_MAX_BUS_NUMBER;
TAILQ_INIT(&vmd_c->vmd[i].bus_list);
if (vmd_map_bars(&vmd_c->vmd[i], pci_dev) == -1) {
return -1;
}
SPDK_DEBUGLOG(SPDK_LOG_VMD, "vmd config bar(%p) vaddr(%p) size(%x)\n",
(void *)vmd_c->vmd[i].cfgbar, (void *)vmd_c->vmd[i].cfg_vaddr,
(uint32_t)vmd_c->vmd[i].cfgbar_size);
SPDK_DEBUGLOG(SPDK_LOG_VMD, "vmd mem bar(%p) vaddr(%p) size(%x)\n",
(void *)vmd_c->vmd[i].membar, (void *)vmd_c->vmd[i].mem_vaddr,
(uint32_t)vmd_c->vmd[i].membar_size);
SPDK_DEBUGLOG(SPDK_LOG_VMD, "vmd msix bar(%p) vaddr(%p) size(%x)\n\n",
(void *)vmd_c->vmd[i].msixbar, (void *)vmd_c->vmd[i].msix_vaddr,
(uint32_t)vmd_c->vmd[i].msixbar_size);
vmd_c->count = i + 1;
vmd_enumerate_devices(&vmd_c->vmd[i]);
return 0;
}
int
spdk_vmd_pci_device_list(struct spdk_pci_addr vmd_addr, struct spdk_pci_device *nvme_list)
{
int cnt = 0;
struct vmd_pci_bus *bus;
struct vmd_pci_device *dev;
if (!nvme_list) {
return -1;
}
for (int i = 0; i < MAX_VMD_TARGET; ++i) {
if (spdk_pci_addr_compare(&vmd_addr, &g_vmd_container.vmd[i].pci->addr) == 0) {
TAILQ_FOREACH(bus, &g_vmd_container.vmd[i].bus_list, tailq) {
TAILQ_FOREACH(dev, &bus->dev_list, tailq) {
nvme_list[cnt++] = dev->pci;
if (!dev->is_hooked) {
vmd_dev_init(dev);
dev->is_hooked = 1;
}
}
}
}
}
return cnt;
}
static void
vmd_clear_hotplug_status(struct vmd_pci_bus *bus)
{
struct vmd_pci_device *device = bus->self;
uint16_t status __attribute__((unused));
status = device->pcie_cap->slot_status.as_uint16_t;
device->pcie_cap->slot_status.as_uint16_t = status;
status = device->pcie_cap->slot_status.as_uint16_t;
status = device->pcie_cap->link_status.as_uint16_t;
device->pcie_cap->link_status.as_uint16_t = status;
status = device->pcie_cap->link_status.as_uint16_t;
}
static void
vmd_bus_handle_hotplug(struct vmd_pci_bus *bus)
{
uint8_t num_devices, sleep_count;
for (sleep_count = 0; sleep_count < 20; ++sleep_count) {
/* Scan until a new device is found */
num_devices = vmd_scan_single_bus(bus, bus->self);
if (num_devices > 0) {
break;
}
spdk_delay_us(200000);
}
if (num_devices == 0) {
SPDK_ERRLOG("Timed out while scanning for hotplugged devices\n");
}
}
static void
vmd_bus_handle_hotremove(struct vmd_pci_bus *bus)
{
struct vmd_pci_device *device, *tmpdev;
TAILQ_FOREACH_SAFE(device, &bus->dev_list, tailq, tmpdev) {
if (!vmd_bus_device_present(bus, device->devfn)) {
device->pci.internal.pending_removal = true;
/* If the device isn't attached, remove it immediately */
if (!device->pci.internal.attached) {
vmd_dev_detach(&device->pci);
}
}
}
}
int
spdk_vmd_hotplug_monitor(void)
{
struct vmd_pci_bus *bus;
struct vmd_pci_device *device;
int num_hotplugs = 0;
uint32_t i;
for (i = 0; i < g_vmd_container.count; ++i) {
TAILQ_FOREACH(bus, &g_vmd_container.vmd[i].bus_list, tailq) {
device = bus->self;
if (device == NULL || !device->hotplug_capable) {
continue;
}
if (device->pcie_cap->slot_status.bit_field.datalink_state_changed != 1) {
continue;
}
if (device->pcie_cap->link_status.bit_field.datalink_layer_active == 1) {
SPDK_DEBUGLOG(SPDK_LOG_VMD, "Device hotplug detected on bus "
"%"PRIu32"\n", bus->bus_number);
vmd_bus_handle_hotplug(bus);
} else {
SPDK_DEBUGLOG(SPDK_LOG_VMD, "Device hotremove detected on bus "
"%"PRIu32"\n", bus->bus_number);
vmd_bus_handle_hotremove(bus);
}
vmd_clear_hotplug_status(bus);
num_hotplugs++;
}
}
return num_hotplugs;
}
int
spdk_vmd_init(void)
{
return spdk_pci_enumerate(spdk_pci_vmd_get_driver(), vmd_enum_cb, &g_vmd_container);
}
void
spdk_vmd_fini(void)
{
uint32_t i;
for (i = 0; i < g_vmd_container.count; ++i) {
spdk_pci_device_detach(g_vmd_container.vmd[i].pci);
}
}
SPDK_LOG_REGISTER_COMPONENT("vmd", SPDK_LOG_VMD)