mirror of
https://git.rwth-aachen.de/acs/public/villas/node/
synced 2025-03-09 00:00:00 +01:00
Merge branch 'refactoring' into 'master'
Refactoring See merge request acs/public/villas/fpga/fpga!11
This commit is contained in:
commit
e3cfc26673
47 changed files with 361 additions and 469 deletions
|
@ -1,19 +1,13 @@
|
|||
variables:
|
||||
GIT_STRATEGY: fetch
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
PREFIX: /usr/
|
||||
DOCKER_TAG_DEV: ${CI_BUILD_REF_SLUG}
|
||||
DOCKER_IMAGE_DEV: villas/fpga-dev
|
||||
|
||||
# For some reason, GitLab CI prunes the contents of the submodules so we need to restore them.
|
||||
before_script:
|
||||
- git submodule foreach git checkout .
|
||||
|
||||
stages:
|
||||
- prepare
|
||||
- build
|
||||
- test
|
||||
# - deploy
|
||||
|
||||
# Stage: prepare
|
||||
##############################################################################
|
||||
|
@ -43,19 +37,6 @@ build:source:
|
|||
tags:
|
||||
- docker
|
||||
|
||||
#build:packages:
|
||||
# stage: build
|
||||
# script:
|
||||
# - mkdir build && cd build && cmake3 .. && make package
|
||||
# artifacts:
|
||||
# expire_in: 1 week
|
||||
# name: ${CI_PROJECT_NAME}-${CI_BUILD_REF}
|
||||
# paths:
|
||||
# - build/
|
||||
# image: ${DOCKER_IMAGE_DEV}:${DOCKER_TAG_DEV}
|
||||
# tags:
|
||||
# - docker
|
||||
|
||||
# Stage: test
|
||||
##############################################################################
|
||||
|
||||
|
@ -66,32 +47,12 @@ test:unit:
|
|||
- cuda
|
||||
allow_failure: true
|
||||
script: |
|
||||
rm -r build && mkdir build && cd build && cmake3 .. && make unit-tests -j8
|
||||
rm -r build && mkdir build && cd build
|
||||
cmake3 ..
|
||||
make -j$(nproc) unit-tests
|
||||
if [ "$(who | wc -l)" -eq "0" ]; then
|
||||
tests/unit-tests --jobs 1 --filter 'fpga/*'
|
||||
tests/fpga-unit-tests --jobs 1 --filter 'fpga/*'
|
||||
else
|
||||
echo "System is currently used by: $(who)"
|
||||
echo "We are skipping the test. Please restart manually."
|
||||
fi
|
||||
|
||||
# Stage: deploy
|
||||
##############################################################################
|
||||
|
||||
#deploy:packages:
|
||||
# stage: deploy
|
||||
# script:
|
||||
# - ssh ${DEPLOY_USER}@${DEPLOY_HOST} mkdir -p ${DEPLOY_PATH}/{dist,../packages}
|
||||
# - rsync ${RSYNC_OPTS} build/*.rpm ${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/../packages/
|
||||
# - rsync ${RSYNC_OPTS} build//*.tar.gz ${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/dist/
|
||||
# - ssh ${DEPLOY_USER}@${DEPLOY_HOST} createrepo ${DEPLOY_PATH}/../packages
|
||||
# dependencies:
|
||||
# - build:packages
|
||||
# tags:
|
||||
# - villas-deploy
|
||||
# only:
|
||||
# - tags
|
||||
#
|
||||
#deploy:git-mirror:
|
||||
# stage: deploy
|
||||
# script:
|
||||
# - git push --force --mirror --prune https://${GITHUB_USER}:${GITHUB_TOKEN}@github.com:VILLASframework/VILLASnode.git
|
||||
|
|
|
@ -29,9 +29,16 @@ project(VILLASfpga
|
|||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake)
|
||||
|
||||
# Several CMake settings/defaults
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror")
|
||||
|
||||
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
|
||||
set(TOPLEVEL_PROJECT ON)
|
||||
else()
|
||||
set(TOPLEVEL_PROJECT OFF)
|
||||
endif()
|
||||
|
||||
# GPU library is optional, check for CUDA presence
|
||||
include(CheckLanguage)
|
||||
check_language(CUDA)
|
||||
|
@ -42,14 +49,26 @@ else()
|
|||
message("No CUDA support, not building GPU library")
|
||||
endif()
|
||||
|
||||
include(FindPkgConfig)
|
||||
|
||||
set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:/usr/local/lib/pkgconfig:/usr/local/lib64/pkgconfig:/usr/local/share/pkgconfig:/usr/lib64/pkgconfig")
|
||||
|
||||
pkg_check_modules(JANSSON REQUIRED IMPORTED_TARGET jansson)
|
||||
pkg_check_modules(XIL REQUIRED IMPORTED_TARGET libxil)
|
||||
|
||||
find_package(Threads)
|
||||
|
||||
include_directories(thirdparty/CLI11)
|
||||
include_directories(thirdparty/rang)
|
||||
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(lib)
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(tests)
|
||||
|
||||
if(NOT CMAKE_PROJECT_NAME STREQUAL "villas-node")
|
||||
add_subdirectory(common)
|
||||
endif()
|
||||
|
||||
# Project settings
|
||||
set(PROJECT_NAME "VILLASfpga")
|
||||
set(PROJECT_DESCRIPTION "Host library for configuring and communicating with VILLASfpga")
|
||||
|
|
|
@ -42,9 +42,6 @@ LABEL \
|
|||
org.label-schema.vcs-url="https://git.rwth-aachen.de/VILLASframework/VILLASfpga" \
|
||||
org.label-schema.usage="https://villas.fein-aachen.org/doc/fpga.html"
|
||||
|
||||
# Some of the dependencies are only available in our own repo
|
||||
ADD https://villas.fein-aachen.org/packages/villas.repo /etc/yum.repos.d/
|
||||
|
||||
# Enable Extra Packages for Enterprise Linux (EPEL) and Software collection repo
|
||||
RUN yum -y install epel-release centos-release-scl
|
||||
|
||||
|
@ -62,12 +59,40 @@ RUN yum -y install \
|
|||
# Dependencies
|
||||
RUN yum -y install \
|
||||
jansson-devel \
|
||||
libxil-devel \
|
||||
openssl-devel \
|
||||
curl-devel \
|
||||
lapack-devel
|
||||
|
||||
# Build & Install Fmtlib
|
||||
RUN git clone --recursive https://github.com/fmtlib/fmt.git /tmp/fmt && \
|
||||
mkdir -p /tmp/fmt/build && cd /tmp/fmt/build && \
|
||||
git checkout 5.2.0 && \
|
||||
cmake3 -DBUILD_SHARED_LIBS=1 .. && \
|
||||
make -j$(nproc) install && \
|
||||
rm -rf /tmp/fmt
|
||||
|
||||
# Build & Install spdlog
|
||||
RUN git clone --recursive https://github.com/gabime/spdlog.git /tmp/spdlog && \
|
||||
mkdir -p /tmp/spdlog/build && cd /tmp/spdlog/build && \
|
||||
git checkout v1.3.1 && \
|
||||
cmake3 -DSPDLOG_FMT_EXTERNAL=ON -DSPDLOG_BUILD_BENCH=OFF .. && \
|
||||
make -j$(nproc) install && \
|
||||
rm -rf /tmp/spdlog
|
||||
|
||||
# Build & Install Criterion
|
||||
COPY thirdparty/criterion /tmp/criterion
|
||||
RUN mkdir -p /tmp/criterion/build && cd /tmp/criterion/build && cmake3 .. && make install && rm -rf /tmp/*
|
||||
RUN git clone --recursive https://github.com/Snaipe/Criterion /tmp/criterion && \
|
||||
mkdir -p /tmp/criterion/build && cd /tmp/criterion/build && \
|
||||
git checkout v2.3.3 && \
|
||||
cmake3 .. && \
|
||||
make -j$(nproc) install && \
|
||||
rm -rf /tmp/*
|
||||
|
||||
# Build & Install libxil
|
||||
RUN git clone https://git.rwth-aachen.de/acs/public/villas/fpga/libxil.git /tmp/libxil && \
|
||||
mkdir -p /tmp/libxil/build && cd /tmp/libxil/build && \
|
||||
cmake3 .. && \
|
||||
make -j$(nproc) install && \
|
||||
rm -rf /tmp/*
|
||||
|
||||
ENV LD_LIBRARY_PATH /usr/local/lib:/usr/local/lib64
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 952945fc4bcdcdca0dfbe1389f811ceb7b5c5744
|
||||
Subproject commit 3b5952a413ba8f8c7731c6a0c8336e1f523884b8
|
|
@ -45,25 +45,25 @@ public:
|
|||
|
||||
std::string getName() const;
|
||||
|
||||
GpuAllocator& getAllocator() const
|
||||
GpuAllocator &getAllocator() const
|
||||
{ return *allocator; }
|
||||
|
||||
|
||||
bool makeAccessibleToPCIeAndVA(const MemoryBlock& mem);
|
||||
bool makeAccessibleToPCIeAndVA(const MemoryBlock &mem);
|
||||
|
||||
/// Make some memory block accssible for this GPU
|
||||
bool makeAccessibleFromPCIeOrHostRam(const MemoryBlock& mem);
|
||||
bool makeAccessibleFromPCIeOrHostRam(const MemoryBlock &mem);
|
||||
|
||||
void memcpySync(const MemoryBlock& src, const MemoryBlock& dst, size_t size);
|
||||
void memcpySync(const MemoryBlock &src, const MemoryBlock &dst, size_t size);
|
||||
|
||||
void memcpyKernel(const MemoryBlock& src, const MemoryBlock& dst, size_t size);
|
||||
void memcpyKernel(const MemoryBlock &src, const MemoryBlock &dst, size_t size);
|
||||
|
||||
MemoryTranslation
|
||||
translate(const MemoryBlock& dst);
|
||||
translate(const MemoryBlock &dst);
|
||||
|
||||
private:
|
||||
bool registerIoMemory(const MemoryBlock& mem);
|
||||
bool registerHostMemory(const MemoryBlock& mem);
|
||||
bool registerIoMemory(const MemoryBlock &mem);
|
||||
bool registerHostMemory(const MemoryBlock &mem);
|
||||
|
||||
private:
|
||||
class impl;
|
||||
|
@ -86,7 +86,7 @@ class GpuAllocator : public BaseAllocator<GpuAllocator> {
|
|||
public:
|
||||
static constexpr size_t GpuPageSize = 64UL << 10;
|
||||
|
||||
GpuAllocator(Gpu& gpu);
|
||||
GpuAllocator(Gpu &gpu);
|
||||
|
||||
std::string getName() const;
|
||||
|
||||
|
@ -94,7 +94,7 @@ public:
|
|||
allocateBlock(size_t size);
|
||||
|
||||
private:
|
||||
Gpu& gpu;
|
||||
Gpu &gpu;
|
||||
// TODO: replace by multimap (key is available memory)
|
||||
std::list<std::unique_ptr<LinearAllocator>> chunks;
|
||||
};
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
#include <villas/gpu.hpp>
|
||||
#include <villas/log.hpp>
|
||||
#include <villas/kernel/pci.h>
|
||||
#include <villas/kernel/pci.hpp>
|
||||
#include <villas/memory_manager.hpp>
|
||||
|
||||
#include <cuda.h>
|
||||
|
@ -39,12 +39,12 @@
|
|||
|
||||
#include "kernels.hpp"
|
||||
|
||||
namespace villas {
|
||||
namespace gpu {
|
||||
|
||||
using namespace villas::gpu;
|
||||
|
||||
static GpuFactory gpuFactory;
|
||||
|
||||
GpuAllocator::GpuAllocator(Gpu& gpu) :
|
||||
GpuAllocator::GpuAllocator(Gpu &gpu) :
|
||||
BaseAllocator(gpu.masterPciEAddrSpaceId),
|
||||
gpu(gpu)
|
||||
{
|
||||
|
@ -77,7 +77,7 @@ GpuFactory::GpuFactory() :
|
|||
// required to be defined here for PIMPL to compile
|
||||
Gpu::~Gpu()
|
||||
{
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
mm.removeAddressSpace(masterPciEAddrSpaceId);
|
||||
}
|
||||
|
||||
|
@ -104,9 +104,9 @@ std::string Gpu::getName() const
|
|||
return name.str();
|
||||
}
|
||||
|
||||
bool Gpu::registerIoMemory(const MemoryBlock& mem)
|
||||
bool Gpu::registerIoMemory(const MemoryBlock &mem)
|
||||
{
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
const auto pciAddrSpaceId = mm.getPciAddressSpace();
|
||||
|
||||
// Check if we need to map anything at all, maybe it's already reachable
|
||||
|
@ -115,14 +115,14 @@ bool Gpu::registerIoMemory(const MemoryBlock& mem)
|
|||
// overlapping window, so this will fail badly!
|
||||
auto translation = mm.getTranslation(masterPciEAddrSpaceId,
|
||||
mem.getAddrSpaceId());
|
||||
if (translation.getSize() >= mem.getSize()) {
|
||||
if (translation.getSize() >= mem.getSize())
|
||||
// there is already a sufficient path
|
||||
logger->debug("Already mapped through another mapping");
|
||||
return true;
|
||||
} else {
|
||||
else
|
||||
logger->warn("There's already a mapping, but too small");
|
||||
}
|
||||
} catch(const std::out_of_range&) {
|
||||
}
|
||||
catch(const std::out_of_range&) {
|
||||
// not yet reachable, that's okay, proceed
|
||||
}
|
||||
|
||||
|
@ -187,9 +187,9 @@ bool Gpu::registerIoMemory(const MemoryBlock& mem)
|
|||
}
|
||||
|
||||
bool
|
||||
Gpu::registerHostMemory(const MemoryBlock& mem)
|
||||
Gpu::registerHostMemory(const MemoryBlock &mem)
|
||||
{
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
|
||||
auto translation = mm.getTranslationFromProcess(mem.getAddrSpaceId());
|
||||
auto localBase = reinterpret_cast<void*>(translation.getLocalAddr(0));
|
||||
|
@ -214,14 +214,14 @@ Gpu::registerHostMemory(const MemoryBlock& mem)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool Gpu::makeAccessibleToPCIeAndVA(const MemoryBlock& mem)
|
||||
bool Gpu::makeAccessibleToPCIeAndVA(const MemoryBlock &mem)
|
||||
{
|
||||
if (pImpl->gdr == nullptr) {
|
||||
logger->warn("GDRcopy not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
|
||||
try {
|
||||
auto path = mm.findPath(masterPciEAddrSpaceId, mem.getAddrSpaceId());
|
||||
|
@ -311,14 +311,14 @@ bool Gpu::makeAccessibleToPCIeAndVA(const MemoryBlock& mem)
|
|||
}
|
||||
|
||||
bool
|
||||
Gpu::makeAccessibleFromPCIeOrHostRam(const MemoryBlock& mem)
|
||||
Gpu::makeAccessibleFromPCIeOrHostRam(const MemoryBlock &mem)
|
||||
{
|
||||
// Check which kind of memory this is and where it resides
|
||||
// There are two possibilities:
|
||||
// - Host memory not managed by CUDA
|
||||
// - IO memory somewhere on the PCIe bus
|
||||
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
|
||||
bool isIoMemory = false;
|
||||
try {
|
||||
|
@ -333,7 +333,8 @@ Gpu::makeAccessibleFromPCIeOrHostRam(const MemoryBlock& mem)
|
|||
mem.getAddrSpaceId());
|
||||
|
||||
return registerIoMemory(mem);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
logger->debug("Memory block {} is assumed to be non-CUDA host memory",
|
||||
mem.getAddrSpaceId());
|
||||
|
||||
|
@ -341,9 +342,9 @@ Gpu::makeAccessibleFromPCIeOrHostRam(const MemoryBlock& mem)
|
|||
}
|
||||
}
|
||||
|
||||
void Gpu::memcpySync(const MemoryBlock& src, const MemoryBlock& dst, size_t size)
|
||||
void Gpu::memcpySync(const MemoryBlock &src, const MemoryBlock &dst, size_t size)
|
||||
{
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
|
||||
auto src_translation = mm.getTranslation(masterPciEAddrSpaceId,
|
||||
src.getAddrSpaceId());
|
||||
|
@ -357,9 +358,9 @@ void Gpu::memcpySync(const MemoryBlock& src, const MemoryBlock& dst, size_t size
|
|||
cudaMemcpy(dst_buf, src_buf, size, cudaMemcpyDefault);
|
||||
}
|
||||
|
||||
void Gpu::memcpyKernel(const MemoryBlock& src, const MemoryBlock& dst, size_t size)
|
||||
void Gpu::memcpyKernel(const MemoryBlock &src, const MemoryBlock &dst, size_t size)
|
||||
{
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
|
||||
auto src_translation = mm.getTranslation(masterPciEAddrSpaceId,
|
||||
src.getAddrSpaceId());
|
||||
|
@ -375,9 +376,9 @@ void Gpu::memcpyKernel(const MemoryBlock& src, const MemoryBlock& dst, size_t si
|
|||
}
|
||||
|
||||
MemoryTranslation
|
||||
Gpu::translate(const MemoryBlock& dst)
|
||||
Gpu::translate(const MemoryBlock &dst)
|
||||
{
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
return mm.getTranslation(masterPciEAddrSpaceId, dst.getAddrSpaceId());
|
||||
}
|
||||
|
||||
|
@ -388,10 +389,10 @@ GpuAllocator::allocateBlock(size_t size)
|
|||
cudaSetDevice(gpu.gpuId);
|
||||
|
||||
void* addr;
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
|
||||
// search for an existing chunk that has enough free memory
|
||||
auto chunk = std::find_if(chunks.begin(), chunks.end(), [&](const auto& chunk) {
|
||||
auto chunk = std::find_if(chunks.begin(), chunks.end(), [&](const auto &chunk) {
|
||||
return chunk->getAvailableMemory() >= size;
|
||||
});
|
||||
|
||||
|
@ -400,8 +401,8 @@ GpuAllocator::allocateBlock(size_t size)
|
|||
logger->debug("Found existing chunk that can host the requested block");
|
||||
|
||||
return (*chunk)->allocateBlock(size);
|
||||
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// allocate a new chunk
|
||||
|
||||
// rounded-up multiple of GPU page size
|
||||
|
@ -452,7 +453,7 @@ Gpu::Gpu(int gpuId) :
|
|||
|
||||
bool Gpu::init()
|
||||
{
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
|
||||
const auto gpuPciEAddrSpaceName = mm.getMasterAddrSpaceName(getName(), "pcie");
|
||||
masterPciEAddrSpaceId = mm.getOrCreateAddressSpace(gpuPciEAddrSpaceName);
|
||||
|
@ -517,12 +518,9 @@ GpuFactory::make()
|
|||
}
|
||||
|
||||
logger->info("Initialized {} GPUs", gpuList.size());
|
||||
for (auto& gpu : gpuList) {
|
||||
for (auto &gpu : gpuList) {
|
||||
logger->debug(" - {}", gpu->getName());
|
||||
}
|
||||
|
||||
return gpuList;
|
||||
}
|
||||
|
||||
} // namespace villas
|
||||
} // namespace gpu
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*********************************************************************************/
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include <villas/gpu.hpp>
|
||||
|
||||
|
@ -30,8 +30,7 @@
|
|||
|
||||
#include "kernels.hpp"
|
||||
|
||||
namespace villas {
|
||||
namespace gpu {
|
||||
using namespace villas::gpu;
|
||||
|
||||
|
||||
__global__ void
|
||||
|
@ -61,6 +60,3 @@ kernel_memcpy(volatile uint8_t* dst, volatile uint8_t* src, size_t length)
|
|||
length--;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace villas
|
||||
} // namespace gpu
|
||||
|
|
|
@ -38,35 +38,32 @@
|
|||
#include <villas/plugin.hpp>
|
||||
#include <villas/memory.hpp>
|
||||
|
||||
#include <villas/kernel/pci.h>
|
||||
#include <villas/kernel/pci.hpp>
|
||||
#include <villas/kernel/vfio.hpp>
|
||||
|
||||
#include <villas/fpga/config.h>
|
||||
#include <villas/fpga/core.hpp>
|
||||
|
||||
#define PCI_FILTER_DEFAULT_FPGA { \
|
||||
.id = { \
|
||||
.vendor = FPGA_PCI_VID_XILINX, \
|
||||
.device = FPGA_PCI_PID_VFPGA, \
|
||||
.class_code = 0 \
|
||||
}, \
|
||||
.slot = { } \
|
||||
}
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
|
||||
|
||||
/* Forward declarations */
|
||||
struct vfio_container;
|
||||
class PCIeCardFactory;
|
||||
|
||||
class PCIeCard {
|
||||
class Card {
|
||||
public:
|
||||
|
||||
using Ptr = std::shared_ptr<PCIeCard>;
|
||||
using List = std::list<Ptr>;
|
||||
|
||||
friend PCIeCardFactory;
|
||||
|
||||
PCIeCard() : filter(PCI_FILTER_DEFAULT_FPGA) {}
|
||||
};
|
||||
|
||||
class PCIeCard : public Card {
|
||||
public:
|
||||
|
||||
~PCIeCard();
|
||||
|
||||
bool init();
|
||||
|
@ -75,12 +72,18 @@ public:
|
|||
bool reset() { return true; }
|
||||
void dump() { }
|
||||
|
||||
ip::Core::Ptr lookupIp(const std::string& name) const;
|
||||
ip::Core::Ptr lookupIp(const Vlnv& vlnv) const;
|
||||
ip::Core::Ptr lookupIp(const ip::IpIdentifier& id) const;
|
||||
ip::Core::Ptr
|
||||
lookupIp(const std::string &name) const;
|
||||
|
||||
ip::Core::Ptr
|
||||
lookupIp(const Vlnv &vlnv) const;
|
||||
|
||||
ip::Core::Ptr
|
||||
lookupIp(const ip::IpIdentifier &id) const;
|
||||
|
||||
|
||||
bool
|
||||
mapMemoryBlock(const MemoryBlock& block);
|
||||
mapMemoryBlock(const MemoryBlock &block);
|
||||
|
||||
private:
|
||||
/// Cache a set of already mapped memory blocks
|
||||
|
@ -89,20 +92,18 @@ private:
|
|||
public: // TODO: make this private
|
||||
ip::Core::List ips; ///< IPs located on this FPGA card
|
||||
|
||||
bool do_reset; /**< Reset VILLASfpga during startup? */
|
||||
bool doReset; /**< Reset VILLASfpga during startup? */
|
||||
int affinity; /**< Affinity for MSI interrupts */
|
||||
|
||||
std::string name; /**< The name of the FPGA card */
|
||||
|
||||
struct pci* pci;
|
||||
struct pci_device filter; /**< Filter for PCI device. */
|
||||
struct pci_device* pdev; /**< PCI device handle */
|
||||
std::shared_ptr<kernel::pci::Device> pdev; /**< PCI device handle */
|
||||
|
||||
/// The VFIO container that this card is part of
|
||||
std::shared_ptr<VfioContainer> vfioContainer;
|
||||
std::shared_ptr<kernel::vfio::Container> vfioContainer;
|
||||
|
||||
/// The VFIO device that represents this card
|
||||
VfioDevice* vfioDevice;
|
||||
kernel::vfio::Device* vfioDevice;
|
||||
|
||||
/// Slave address space ID to access the PCIe address space from the FPGA
|
||||
MemoryManager::AddressSpaceId addrSpaceIdDeviceToHost;
|
||||
|
@ -119,16 +120,15 @@ protected:
|
|||
Logger logger;
|
||||
};
|
||||
|
||||
using CardList = std::list<std::shared_ptr<PCIeCard>>;
|
||||
|
||||
class PCIeCardFactory : public plugin::Plugin {
|
||||
public:
|
||||
|
||||
static CardList
|
||||
make(json_t *json, struct pci* pci, std::shared_ptr<VfioContainer> vc);
|
||||
static Card::List
|
||||
make(json_t *json, std::shared_ptr<kernel::pci::DeviceList> pci, std::shared_ptr<kernel::vfio::Container> vc);
|
||||
|
||||
static PCIeCard*
|
||||
create();
|
||||
create()
|
||||
{ return new PCIeCard(); }
|
||||
|
||||
static Logger
|
||||
getStaticLogger()
|
||||
|
|
|
@ -74,11 +74,11 @@ public:
|
|||
{ return vlnv; }
|
||||
|
||||
friend std::ostream&
|
||||
operator<< (std::ostream& stream, const IpIdentifier& id)
|
||||
operator<< (std::ostream &stream, const IpIdentifier &id)
|
||||
{ return stream << id.name << " vlnv=" << id.vlnv; }
|
||||
|
||||
bool
|
||||
operator==(const IpIdentifier& otherId) const {
|
||||
operator==(const IpIdentifier &otherId) const {
|
||||
const bool vlnvWildcard = otherId.getVlnv() == Vlnv::getWildcard();
|
||||
const bool nameWildcard = this->getName().empty() or otherId.getName().empty();
|
||||
|
||||
|
@ -89,7 +89,7 @@ public:
|
|||
}
|
||||
|
||||
bool
|
||||
operator!=(const IpIdentifier& otherId) const
|
||||
operator!=(const IpIdentifier &otherId) const
|
||||
{ return !(*this == otherId); }
|
||||
|
||||
private:
|
||||
|
@ -144,66 +144,66 @@ public:
|
|||
/* Operators */
|
||||
|
||||
bool
|
||||
operator==(const Vlnv& otherVlnv) const
|
||||
operator==(const Vlnv &otherVlnv) const
|
||||
{ return id.getVlnv() == otherVlnv; }
|
||||
|
||||
bool
|
||||
operator!=(const Vlnv& otherVlnv) const
|
||||
operator!=(const Vlnv &otherVlnv) const
|
||||
{ return id.getVlnv() != otherVlnv; }
|
||||
|
||||
bool
|
||||
operator==(const IpIdentifier& otherId) const
|
||||
operator==(const IpIdentifier &otherId) const
|
||||
{ return this->id == otherId; }
|
||||
|
||||
bool
|
||||
operator!=(const IpIdentifier& otherId) const
|
||||
operator!=(const IpIdentifier &otherId) const
|
||||
{ return this->id != otherId; }
|
||||
|
||||
bool
|
||||
operator==(const std::string& otherName) const
|
||||
operator==(const std::string &otherName) const
|
||||
{ return getInstanceName() == otherName; }
|
||||
|
||||
bool
|
||||
operator!=(const std::string& otherName) const
|
||||
operator!=(const std::string &otherName) const
|
||||
{ return getInstanceName() != otherName; }
|
||||
|
||||
bool
|
||||
operator==(const Core& otherIp) const
|
||||
operator==(const Core &otherIp) const
|
||||
{ return this->id == otherIp.id; }
|
||||
|
||||
bool
|
||||
operator!=(const Core& otherIp) const
|
||||
operator!=(const Core &otherIp) const
|
||||
{ return this->id != otherIp.id; }
|
||||
|
||||
friend std::ostream&
|
||||
operator<< (std::ostream& stream, const Core& ip)
|
||||
operator<< (std::ostream &stream, const Core &ip)
|
||||
{ return stream << ip.id; }
|
||||
|
||||
protected:
|
||||
uintptr_t
|
||||
getBaseAddr(const MemoryBlockName& block) const
|
||||
getBaseAddr(const MemoryBlockName &block) const
|
||||
{ return getLocalAddr(block, 0); }
|
||||
|
||||
uintptr_t
|
||||
getLocalAddr(const MemoryBlockName& block, uintptr_t address) const;
|
||||
getLocalAddr(const MemoryBlockName &block, uintptr_t address) const;
|
||||
|
||||
MemoryManager::AddressSpaceId
|
||||
getAddressSpaceId(const MemoryBlockName& block) const
|
||||
getAddressSpaceId(const MemoryBlockName &block) const
|
||||
{ return slaveAddressSpaces.at(block); }
|
||||
|
||||
InterruptController*
|
||||
getInterruptController(const std::string& interruptName) const;
|
||||
getInterruptController(const std::string &interruptName) const;
|
||||
|
||||
MemoryManager::AddressSpaceId
|
||||
getMasterAddrSpaceByInterface(const std::string& masterInterfaceName) const
|
||||
getMasterAddrSpaceByInterface(const std::string &masterInterfaceName) const
|
||||
{ return busMasterInterfaces.at(masterInterfaceName); }
|
||||
|
||||
template<typename T>
|
||||
T readMemory(const std::string& block, uintptr_t address) const
|
||||
T readMemory(const std::string &block, uintptr_t address) const
|
||||
{ return *(reinterpret_cast<T*>(getLocalAddr(block, address))); }
|
||||
|
||||
template<typename T>
|
||||
void writeMemory(const std::string& block, uintptr_t address, T value)
|
||||
void writeMemory(const std::string &block, uintptr_t address, T value)
|
||||
{ T* ptr = reinterpret_cast<T*>(getLocalAddr(block, address)); *ptr = value; }
|
||||
|
||||
protected:
|
||||
|
@ -266,7 +266,7 @@ protected:
|
|||
|
||||
private:
|
||||
static CoreFactory*
|
||||
lookup(const Vlnv& vlnv);
|
||||
lookup(const Vlnv &vlnv);
|
||||
};
|
||||
|
||||
/** @} */
|
||||
|
|
|
@ -59,7 +59,7 @@ private:
|
|||
class BramFactory : public CoreFactory {
|
||||
public:
|
||||
|
||||
bool configureJson(Core& ip, json_t *json_ip);
|
||||
bool configureJson(Core &ip, json_t *json_ip);
|
||||
|
||||
Core* create()
|
||||
{ return new Bram; }
|
||||
|
|
|
@ -44,10 +44,10 @@ public:
|
|||
bool reset();
|
||||
|
||||
// memory-mapped to stream (MM2S)
|
||||
bool write(const MemoryBlock& mem, size_t len);
|
||||
bool write(const MemoryBlock &mem, size_t len);
|
||||
|
||||
// stream to memory-mapped (S2MM)
|
||||
bool read(const MemoryBlock& mem, size_t len);
|
||||
bool read(const MemoryBlock &mem, size_t len);
|
||||
|
||||
size_t writeComplete()
|
||||
{ return hasScatterGather() ? writeCompleteSG() : writeCompleteSimple(); }
|
||||
|
@ -55,10 +55,10 @@ public:
|
|||
size_t readComplete()
|
||||
{ return hasScatterGather() ? readCompleteSG() : readCompleteSimple(); }
|
||||
|
||||
bool memcpy(const MemoryBlock& src, const MemoryBlock& dst, size_t len);
|
||||
bool memcpy(const MemoryBlock &src, const MemoryBlock &dst, size_t len);
|
||||
|
||||
bool makeAccesibleFromVA(const MemoryBlock& mem);
|
||||
bool makeInaccesibleFromVA(const MemoryBlock& mem);
|
||||
bool makeAccesibleFromVA(const MemoryBlock &mem);
|
||||
bool makeInaccesibleFromVA(const MemoryBlock &mem);
|
||||
|
||||
inline bool
|
||||
hasScatterGather() const
|
||||
|
@ -87,7 +87,7 @@ public:
|
|||
static constexpr const char* s2mmPort = "S2MM";
|
||||
static constexpr const char* mm2sPort = "MM2S";
|
||||
|
||||
bool isMemoryBlockAccesible(const MemoryBlock& mem, const std::string& interface);
|
||||
bool isMemoryBlockAccesible(const MemoryBlock &mem, const std::string &interface);
|
||||
|
||||
virtual void dump();
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ class Hls : public virtual Core
|
|||
public:
|
||||
virtual bool init()
|
||||
{
|
||||
auto& registers = addressTranslations.at(registerMemory);
|
||||
auto ®isters = addressTranslations.at(registerMemory);
|
||||
|
||||
controlRegister = reinterpret_cast<ControlRegister*>(registers.getLocalAddr(registerControlAddr));
|
||||
globalIntRegister = reinterpret_cast<GlobalIntRegister*>(registers.getLocalAddr(registerGlobalIntEnableAddr));
|
||||
|
|
|
@ -70,7 +70,7 @@ public:
|
|||
getCompatibleVlnvString()
|
||||
{ return "xilinx.com:ip:axi_pcie:"; }
|
||||
|
||||
bool configureJson(Core& ip, json_t *json_ip);
|
||||
bool configureJson(Core &ip, json_t *json_ip);
|
||||
|
||||
Core* create()
|
||||
{ return new AxiPciExpressBridge; }
|
||||
|
|
|
@ -34,16 +34,16 @@ public:
|
|||
|
||||
void dump(spdlog::level::level_enum logLevel = spdlog::level::info);
|
||||
|
||||
bool startOnce(const MemoryBlock& mem, size_t frameSize, size_t dataOffset, size_t doorbellOffset);
|
||||
bool startOnce(const MemoryBlock &mem, size_t frameSize, size_t dataOffset, size_t doorbellOffset);
|
||||
|
||||
size_t getMaxFrameSize();
|
||||
|
||||
void dumpDoorbell(uint32_t doorbellRegister) const;
|
||||
|
||||
bool doorbellIsValid(const uint32_t& doorbellRegister) const
|
||||
bool doorbellIsValid(const uint32_t &doorbellRegister) const
|
||||
{ return reinterpret_cast<const reg_doorbell_t&>(doorbellRegister).is_valid; }
|
||||
|
||||
void doorbellReset(uint32_t& doorbellRegister) const
|
||||
void doorbellReset(uint32_t &doorbellRegister) const
|
||||
{ doorbellRegister = 0; }
|
||||
|
||||
static constexpr const char* registerMemory = "Reg";
|
||||
|
|
|
@ -46,11 +46,11 @@ public:
|
|||
|
||||
bool init();
|
||||
|
||||
bool connectInternal(const std::string& slavePort,
|
||||
const std::string& masterPort);
|
||||
bool connectInternal(const std::string &slavePort,
|
||||
const std::string &masterPort);
|
||||
|
||||
private:
|
||||
int portNameToNum(const std::string& portName);
|
||||
int portNameToNum(const std::string &portName);
|
||||
|
||||
private:
|
||||
static constexpr const char* PORT_DISABLED = "DISABLED";
|
||||
|
@ -77,7 +77,7 @@ public:
|
|||
getCompatibleVlnvString()
|
||||
{ return "xilinx.com:ip:axis_switch:"; }
|
||||
|
||||
bool configureJson(Core& ip, json_t *json_ip);
|
||||
bool configureJson(Core &ip, json_t *json_ip);
|
||||
|
||||
Core* create()
|
||||
{ return new AxiStreamSwitch; }
|
||||
|
|
|
@ -44,14 +44,14 @@ namespace ip {
|
|||
|
||||
class StreamVertex : public graph::Vertex {
|
||||
public:
|
||||
StreamVertex(const std::string& node, const std::string& port, bool isMaster) :
|
||||
StreamVertex(const std::string &node, const std::string &port, bool isMaster) :
|
||||
nodeName(node), portName(port), isMaster(isMaster) {}
|
||||
|
||||
std::string getName() const
|
||||
{ return nodeName + "/" + portName + "(" + (isMaster ? "M" : "S") + ")"; }
|
||||
|
||||
friend std::ostream&
|
||||
operator<< (std::ostream& stream, const StreamVertex& vertex)
|
||||
operator<< (std::ostream &stream, const StreamVertex &vertex)
|
||||
{ return stream << vertex.getIdentifier() << ": " << vertex.getName(); }
|
||||
|
||||
public:
|
||||
|
@ -66,12 +66,12 @@ public:
|
|||
StreamGraph() : graph::DirectedGraph<StreamVertex>("StreamGraph") {}
|
||||
|
||||
std::shared_ptr<StreamVertex>
|
||||
getOrCreateStreamVertex(const std::string& node,
|
||||
const std::string& port,
|
||||
getOrCreateStreamVertex(const std::string &node,
|
||||
const std::string &port,
|
||||
bool isMaster)
|
||||
{
|
||||
for (auto& vertexEntry : vertices) {
|
||||
auto& vertex = vertexEntry.second;
|
||||
for (auto &vertexEntry : vertices) {
|
||||
auto &vertex = vertexEntry.second;
|
||||
if (vertex->nodeName == node and vertex->portName == port and vertex->isMaster == isMaster)
|
||||
return vertex;
|
||||
}
|
||||
|
@ -88,6 +88,8 @@ public:
|
|||
class Node : public virtual Core {
|
||||
public:
|
||||
|
||||
using Ptr = std::shared_ptr<Node>;
|
||||
|
||||
friend class NodeFactory;
|
||||
|
||||
struct StreamPort {
|
||||
|
@ -95,20 +97,31 @@ public:
|
|||
std::string nodeName;
|
||||
};
|
||||
|
||||
bool connect(const StreamVertex& from, const StreamVertex& to);
|
||||
|
||||
const StreamVertex&
|
||||
getMasterPort(const std::string& name) const
|
||||
getMasterPort(const std::string &name) const
|
||||
{ return *portsMaster.at(name); }
|
||||
|
||||
const StreamVertex&
|
||||
getSlavePort(const std::string& name) const
|
||||
getSlavePort(const std::string &name) const
|
||||
{ return *portsSlave.at(name); }
|
||||
|
||||
bool connect(const StreamVertex &from, const StreamVertex &to);
|
||||
bool connect(const StreamVertex &from, const StreamVertex &to, bool reverse)
|
||||
{
|
||||
bool ret;
|
||||
|
||||
ret = connect(from, to);
|
||||
|
||||
if (reverse)
|
||||
ret &= connect(to, from);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// easy-usage assuming that the slave IP to connect to only has one slave
|
||||
// port and implements the getDefaultSlavePort() function
|
||||
bool connect(const Node& slaveNode)
|
||||
{ return this->connect(this->getDefaultMasterPort(), slaveNode.getDefaultSlavePort()); }
|
||||
bool connect(const Node &slaveNode, bool reverse = false)
|
||||
{ return this->connect(this->getDefaultMasterPort(), slaveNode.getDefaultSlavePort(), reverse); }
|
||||
|
||||
// used by easy-usage connect, will throw if not implemented by derived node
|
||||
virtual const StreamVertex&
|
||||
|
@ -127,8 +140,8 @@ public:
|
|||
|
||||
protected:
|
||||
virtual bool
|
||||
connectInternal(const std::string& slavePort,
|
||||
const std::string& masterPort);
|
||||
connectInternal(const std::string &slavePort,
|
||||
const std::string &masterPort);
|
||||
|
||||
private:
|
||||
std::pair<std::string, std::string> getLoopbackPorts() const;
|
||||
|
@ -144,7 +157,7 @@ class NodeFactory : public CoreFactory {
|
|||
public:
|
||||
using CoreFactory::CoreFactory;
|
||||
|
||||
virtual bool configureJson(Core& ip, json_t *json_ip);
|
||||
virtual bool configureJson(Core &ip, json_t *json_ip);
|
||||
};
|
||||
|
||||
/** @} */
|
||||
|
|
|
@ -53,14 +53,14 @@ public:
|
|||
toString() const;
|
||||
|
||||
bool
|
||||
operator==(const Vlnv& other) const;
|
||||
operator==(const Vlnv &other) const;
|
||||
|
||||
bool
|
||||
operator!=(const Vlnv& other) const
|
||||
operator!=(const Vlnv &other) const
|
||||
{ return !(*this == other); }
|
||||
|
||||
friend std::ostream&
|
||||
operator<< (std::ostream& stream, const Vlnv& vlnv)
|
||||
operator<< (std::ostream &stream, const Vlnv &vlnv)
|
||||
{
|
||||
return stream
|
||||
<< (vlnv.vendor.empty() ? "*" : vlnv.vendor) << ":"
|
||||
|
|
|
@ -46,13 +46,6 @@ set(SOURCES
|
|||
set_source_files_properties(ips/rtds2gpu/xrtds2gpu.c
|
||||
PROPERTIES COMPILE_FLAGS -Wno-int-to-pointer-cast)
|
||||
|
||||
include(FindPkgConfig)
|
||||
|
||||
pkg_check_modules(JANSSON jansson)
|
||||
pkg_check_modules(XIL libxil)
|
||||
|
||||
find_package(Threads)
|
||||
|
||||
add_library(villas-fpga SHARED ${SOURCES})
|
||||
|
||||
target_link_libraries(villas-fpga PUBLIC villas-common)
|
||||
|
@ -64,7 +57,8 @@ target_compile_definitions(villas-fpga PRIVATE
|
|||
|
||||
target_include_directories(villas-fpga
|
||||
PUBLIC
|
||||
../include
|
||||
${PROJECT_BINARY_DIR}/include
|
||||
${PROJECT_SOURCE_DIR}/include
|
||||
${XIL_INCLUDE_DIRS}
|
||||
${JANSSON_INCLUDE_DIRS}
|
||||
)
|
||||
|
|
|
@ -24,24 +24,27 @@
|
|||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include <villas/exceptions.hpp>
|
||||
#include <villas/memory.hpp>
|
||||
|
||||
#include <villas/kernel/pci.h>
|
||||
#include <villas/kernel/pci.hpp>
|
||||
#include <villas/kernel/vfio.hpp>
|
||||
|
||||
#include <villas/fpga/core.hpp>
|
||||
#include <villas/fpga/card.hpp>
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
using namespace villas;
|
||||
using namespace villas::fpga;
|
||||
|
||||
// instantiate factory to register
|
||||
static PCIeCardFactory PCIeCardFactory;
|
||||
static PCIeCardFactory villas::fpga::PCIeCardFactory;
|
||||
|
||||
CardList
|
||||
PCIeCardFactory::make(json_t *json, struct pci* pci, std::shared_ptr<VfioContainer> vc)
|
||||
static const kernel::pci::Device defaultFilter((kernel::pci::Id(FPGA_PCI_VID_XILINX, FPGA_PCI_PID_VFPGA)));
|
||||
|
||||
PCIeCard::List
|
||||
PCIeCardFactory::make(json_t *json, std::shared_ptr<kernel::pci::DeviceList> pci, std::shared_ptr<kernel::vfio::Container> vc)
|
||||
{
|
||||
CardList cards;
|
||||
PCIeCard::List cards;
|
||||
auto logger = getStaticLogger();
|
||||
|
||||
const char *card_name;
|
||||
|
@ -71,37 +74,38 @@ PCIeCardFactory::make(json_t *json, struct pci* pci, std::shared_ptr<VfioContain
|
|||
|
||||
// populate generic properties
|
||||
card->name = std::string(card_name);
|
||||
card->pci = pci;
|
||||
card->vfioContainer = std::move(vc);
|
||||
card->affinity = affinity;
|
||||
card->do_reset = do_reset != 0;
|
||||
card->doReset = do_reset != 0;
|
||||
|
||||
const char* error;
|
||||
kernel::pci::Device filter = defaultFilter;
|
||||
|
||||
if (pci_id)
|
||||
filter.id = kernel::pci::Id(pci_id);
|
||||
if (pci_slot)
|
||||
filter.slot = kernel::pci::Slot(pci_slot);
|
||||
|
||||
if (pci_slot != nullptr and pci_device_parse_slot(&card->filter, pci_slot, &error) != 0) {
|
||||
logger->warn("Failed to parse PCI slot: {}", error);
|
||||
/* Search for FPGA card */
|
||||
card->pdev = pci->lookupDevice(filter);
|
||||
if (!card->pdev) {
|
||||
logger->warn("Failed to find PCI device");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pci_id != nullptr and pci_device_parse_id(&card->filter, pci_id, &error) != 0) {
|
||||
logger->warn("Failed to parse PCI ID: {}", error);
|
||||
}
|
||||
|
||||
|
||||
if (not card->init()) {
|
||||
logger->warn("Cannot start FPGA card {}", card_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
card->ips = ip::CoreFactory::make(card.get(), json_ips);
|
||||
if (card->ips.empty()) {
|
||||
logger->error("Cannot initialize IPs of FPGA card {}", card_name);
|
||||
continue;
|
||||
}
|
||||
if (not json_is_object(json_ips))
|
||||
throw ConfigError(json_ips, "node-config-fpga-ips", "FPGA IP core list must be an object!");
|
||||
|
||||
if (not card->check()) {
|
||||
logger->warn("Checking of FPGA card {} failed", card_name);
|
||||
continue;
|
||||
}
|
||||
card->ips = ip::CoreFactory::make(card.get(), json_ips);
|
||||
if (card->ips.empty())
|
||||
throw ConfigError(json_ips, "node-config-fpga-ips", "Cannot initialize IPs of FPGA card {}", card_name);
|
||||
|
||||
if (not card->check())
|
||||
throw RuntimeError("Checking of FPGA card {} failed", card_name);
|
||||
|
||||
cards.push_back(std::move(card));
|
||||
}
|
||||
|
@ -109,20 +113,12 @@ PCIeCardFactory::make(json_t *json, struct pci* pci, std::shared_ptr<VfioContain
|
|||
return cards;
|
||||
}
|
||||
|
||||
|
||||
PCIeCard*
|
||||
PCIeCardFactory::create()
|
||||
{
|
||||
return new fpga::PCIeCard;
|
||||
}
|
||||
|
||||
|
||||
PCIeCard::~PCIeCard()
|
||||
{
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
|
||||
// unmap all memory blocks
|
||||
for (auto& mappedMemoryBlock : memoryBlocksMapped) {
|
||||
for (auto &mappedMemoryBlock : memoryBlocksMapped) {
|
||||
auto translation = mm.getTranslation(addrSpaceIdDeviceToHost,
|
||||
mappedMemoryBlock);
|
||||
|
||||
|
@ -137,9 +133,9 @@ PCIeCard::~PCIeCard()
|
|||
|
||||
|
||||
ip::Core::Ptr
|
||||
PCIeCard::lookupIp(const std::string& name) const
|
||||
PCIeCard::lookupIp(const std::string &name) const
|
||||
{
|
||||
for (auto& ip : ips) {
|
||||
for (auto &ip : ips) {
|
||||
if (*ip == name) {
|
||||
return ip;
|
||||
}
|
||||
|
@ -150,9 +146,9 @@ PCIeCard::lookupIp(const std::string& name) const
|
|||
|
||||
|
||||
ip::Core::Ptr
|
||||
PCIeCard::lookupIp(const Vlnv& vlnv) const
|
||||
PCIeCard::lookupIp(const Vlnv &vlnv) const
|
||||
{
|
||||
for (auto& ip : ips) {
|
||||
for (auto &ip : ips) {
|
||||
if (*ip == vlnv) {
|
||||
return ip;
|
||||
}
|
||||
|
@ -162,9 +158,9 @@ PCIeCard::lookupIp(const Vlnv& vlnv) const
|
|||
}
|
||||
|
||||
ip::Core::Ptr
|
||||
PCIeCard::lookupIp(const ip::IpIdentifier& id) const
|
||||
PCIeCard::lookupIp(const ip::IpIdentifier &id) const
|
||||
{
|
||||
for (auto& ip : ips) {
|
||||
for (auto &ip : ips) {
|
||||
if (*ip == id) {
|
||||
return ip;
|
||||
}
|
||||
|
@ -175,22 +171,21 @@ PCIeCard::lookupIp(const ip::IpIdentifier& id) const
|
|||
|
||||
|
||||
bool
|
||||
PCIeCard::mapMemoryBlock(const MemoryBlock& block)
|
||||
PCIeCard::mapMemoryBlock(const MemoryBlock &block)
|
||||
{
|
||||
if (not vfioContainer->isIommuEnabled()) {
|
||||
logger->warn("VFIO mapping not supported without IOMMU");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& mm = MemoryManager::get();
|
||||
const auto& addrSpaceId = block.getAddrSpaceId();
|
||||
auto &mm = MemoryManager::get();
|
||||
const auto &addrSpaceId = block.getAddrSpaceId();
|
||||
|
||||
if (memoryBlocksMapped.find(addrSpaceId) != memoryBlocksMapped.end()) {
|
||||
if (memoryBlocksMapped.find(addrSpaceId) != memoryBlocksMapped.end())
|
||||
// block already mapped
|
||||
return true;
|
||||
} else {
|
||||
else
|
||||
logger->debug("Create VFIO mapping for {}", addrSpaceId);
|
||||
}
|
||||
|
||||
auto translationFromProcess = mm.getTranslationFromProcess(addrSpaceId);
|
||||
uintptr_t processBaseAddr = translationFromProcess.getLocalAddr(0);
|
||||
|
@ -223,15 +218,8 @@ PCIeCard::init()
|
|||
|
||||
logger->info("Initializing FPGA card {}", name);
|
||||
|
||||
/* Search for FPGA card */
|
||||
pdev = pci_lookup_device(pci, &filter);
|
||||
if (!pdev) {
|
||||
logger->error("Failed to find PCI device");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Attach PCIe card to VFIO container */
|
||||
VfioDevice& device = vfioContainer->attachDevice(pdev);
|
||||
kernel::vfio::Device &device = vfioContainer->attachDevice(*pdev);
|
||||
this->vfioDevice = &device;
|
||||
|
||||
/* Enable memory access and PCI bus mastering for DMA */
|
||||
|
@ -241,7 +229,7 @@ PCIeCard::init()
|
|||
}
|
||||
|
||||
/* Reset system? */
|
||||
if (do_reset) {
|
||||
if (doReset) {
|
||||
/* Reset / detect PCI device */
|
||||
if (not vfioDevice->pciHotReset()) {
|
||||
logger->error("Failed to reset PCI device");
|
||||
|
@ -256,6 +244,3 @@ PCIeCard::init()
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -36,10 +36,8 @@
|
|||
#include <villas/fpga/ips/intc.hpp>
|
||||
#include <villas/fpga/ips/switch.hpp>
|
||||
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
namespace ip {
|
||||
using namespace villas::fpga;
|
||||
using namespace villas::fpga::ip;
|
||||
|
||||
// Special IPs that have to be initialized first. Will be initialized in the
|
||||
// same order as they appear in this list, i.e. first here will be initialized
|
||||
|
@ -85,7 +83,7 @@ CoreFactory::make(PCIeCard* card, json_t *json_ips)
|
|||
// first to be initialized.
|
||||
vlnvInitializationOrder.reverse();
|
||||
|
||||
for (auto& vlnvInitFirst : vlnvInitializationOrder) {
|
||||
for (auto &vlnvInitFirst : vlnvInitializationOrder) {
|
||||
// iterate over IPs, if VLNV matches, push to front and remove from list
|
||||
for (auto it = allIps.begin(); it != allIps.end(); ++it) {
|
||||
if (vlnvInitFirst == it->getVlnv()) {
|
||||
|
@ -99,12 +97,12 @@ CoreFactory::make(PCIeCard* card, json_t *json_ips)
|
|||
orderedIps.splice(orderedIps.end(), allIps);
|
||||
|
||||
loggerStatic->debug("IP initialization order:");
|
||||
for (auto& id : orderedIps) {
|
||||
for (auto &id : orderedIps) {
|
||||
loggerStatic->debug(" " CLR_BLD("{}"), id.getName());
|
||||
}
|
||||
|
||||
// configure all IPs
|
||||
for (auto& id : orderedIps) {
|
||||
for (auto &id : orderedIps) {
|
||||
loggerStatic->info("Configuring {}", id);
|
||||
|
||||
// find the appropriate factory that can create the specified VLNV
|
||||
|
@ -117,10 +115,9 @@ CoreFactory::make(PCIeCard* card, json_t *json_ips)
|
|||
if (CoreFactory == nullptr) {
|
||||
loggerStatic->warn("No plugin found to handle {}", id.getVlnv());
|
||||
continue;
|
||||
} else {
|
||||
loggerStatic->debug("Using {} for IP {}",
|
||||
CoreFactory->getName(), id.getVlnv());
|
||||
}
|
||||
else
|
||||
loggerStatic->debug("Using {} for IP {}", CoreFactory->getName(), id.getVlnv());
|
||||
|
||||
auto logger = CoreFactory->getLogger();
|
||||
|
||||
|
@ -162,10 +159,10 @@ CoreFactory::make(PCIeCard* card, json_t *json_ips)
|
|||
continue;
|
||||
}
|
||||
|
||||
const std::string& irqControllerName = tokens[0];
|
||||
const std::string &irqControllerName = tokens[0];
|
||||
InterruptController* intc = nullptr;
|
||||
|
||||
for (auto& configuredIp : configuredIps) {
|
||||
for (auto &configuredIp : configuredIps) {
|
||||
if (*configuredIp == irqControllerName) {
|
||||
intc = dynamic_cast<InterruptController*>(configuredIp.get());
|
||||
break;
|
||||
|
@ -265,12 +262,12 @@ CoreFactory::make(PCIeCard* card, json_t *json_ips)
|
|||
}
|
||||
|
||||
// Start and check IPs now
|
||||
for (auto& ip : configuredIps) {
|
||||
for (auto &ip : configuredIps) {
|
||||
|
||||
// Translate all memory blocks that the IP needs to be accessible from
|
||||
// the process and cache in the instance, so this has not to be done at
|
||||
// runtime.
|
||||
for (auto& memoryBlock : ip->getMemoryBlocks()) {
|
||||
for (auto &memoryBlock : ip->getMemoryBlocks()) {
|
||||
// construct the global name of this address block
|
||||
const auto addrSpaceName =
|
||||
MemoryManager::getSlaveAddrSpaceName(ip->getInstanceName(),
|
||||
|
@ -284,7 +281,7 @@ CoreFactory::make(PCIeCard* card, json_t *json_ips)
|
|||
ip->slaveAddressSpaces.emplace(memoryBlock, addrSpaceId);
|
||||
|
||||
// get the translation to the address space
|
||||
const auto& translation =
|
||||
const auto &translation =
|
||||
MemoryManager::get().getTranslationFromProcess(addrSpaceId);
|
||||
|
||||
// cache it in the IP instance only with local name
|
||||
|
@ -309,7 +306,7 @@ CoreFactory::make(PCIeCard* card, json_t *json_ips)
|
|||
|
||||
|
||||
loggerStatic->debug("Initialized IPs:");
|
||||
for (auto& ip : initializedIps) {
|
||||
for (auto &ip : initializedIps) {
|
||||
loggerStatic->debug(" {}", *ip);
|
||||
}
|
||||
|
||||
|
@ -335,7 +332,7 @@ Core::dump()
|
|||
CoreFactory*
|
||||
CoreFactory::lookup(const Vlnv &vlnv)
|
||||
{
|
||||
for (auto& ip : plugin::Registry::lookup<CoreFactory>()) {
|
||||
for (auto &ip : plugin::Registry::lookup<CoreFactory>()) {
|
||||
if (ip->getCompatibleVlnv() == vlnv)
|
||||
return ip;
|
||||
}
|
||||
|
@ -345,17 +342,17 @@ CoreFactory::lookup(const Vlnv &vlnv)
|
|||
|
||||
|
||||
uintptr_t
|
||||
Core::getLocalAddr(const MemoryBlockName& block, uintptr_t address) const
|
||||
Core::getLocalAddr(const MemoryBlockName &block, uintptr_t address) const
|
||||
{
|
||||
// throws exception if block not present
|
||||
auto& translation = addressTranslations.at(block);
|
||||
auto &translation = addressTranslations.at(block);
|
||||
|
||||
return translation.getLocalAddr(address);
|
||||
}
|
||||
|
||||
|
||||
InterruptController*
|
||||
Core::getInterruptController(const std::string& interruptName) const
|
||||
Core::getInterruptController(const std::string &interruptName) const
|
||||
{
|
||||
try {
|
||||
const IrqPort irq = irqs.at(interruptName);
|
||||
|
@ -364,8 +361,3 @@ Core::getInterruptController(const std::string& interruptName) const
|
|||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -66,9 +66,7 @@
|
|||
#define AURORA_AXIS_CR_SEQ_ECHO (1 << 4)
|
||||
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
namespace ip {
|
||||
using namespace villas::fpga::ip;
|
||||
|
||||
static AuroraFactory auroraFactoryInstance;
|
||||
|
||||
|
@ -118,7 +116,3 @@ void Aurora::resetFrameCounters()
|
|||
|
||||
writeMemory<uint32_t>(registerMemory, AURORA_AXIS_CR_OFFSET, cr);
|
||||
}
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -22,16 +22,14 @@
|
|||
|
||||
#include <villas/fpga/ips/bram.hpp>
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
namespace ip {
|
||||
using namespace villas::fpga::ip;
|
||||
|
||||
static BramFactory factory;
|
||||
|
||||
bool
|
||||
BramFactory::configureJson(Core& ip, json_t* json_ip)
|
||||
BramFactory::configureJson(Core &ip, json_t* json_ip)
|
||||
{
|
||||
auto& bram = dynamic_cast<Bram&>(ip);
|
||||
auto &bram = dynamic_cast<Bram&>(ip);
|
||||
|
||||
if (json_unpack(json_ip, "{ s: i }", "size", &bram.size) != 0) {
|
||||
getLogger()->error("Cannot parse 'size'");
|
||||
|
@ -49,6 +47,3 @@ bool Bram::init()
|
|||
return true;
|
||||
}
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -35,9 +35,7 @@
|
|||
#define FPGA_DMA_BOUNDARY 0x1000
|
||||
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
namespace ip {
|
||||
using namespace villas::fpga::ip;
|
||||
|
||||
// instantiate factory to make available to plugin infrastructure
|
||||
static DmaFactory factory;
|
||||
|
@ -75,9 +73,9 @@ Dma::init()
|
|||
if (XAxiDma_Selftest(&xDma) != XST_SUCCESS) {
|
||||
logger->error("DMA selftest failed");
|
||||
return false;
|
||||
} else {
|
||||
logger->debug("DMA selftest passed");
|
||||
}
|
||||
else
|
||||
logger->debug("DMA selftest passed");
|
||||
|
||||
/* Map buffer descriptors */
|
||||
if (hasScatterGather()) {
|
||||
|
@ -126,7 +124,7 @@ Dma::reset()
|
|||
|
||||
|
||||
bool
|
||||
Dma::memcpy(const MemoryBlock& src, const MemoryBlock& dst, size_t len)
|
||||
Dma::memcpy(const MemoryBlock &src, const MemoryBlock &dst, size_t len)
|
||||
{
|
||||
if (len == 0)
|
||||
return true;
|
||||
|
@ -151,9 +149,9 @@ Dma::memcpy(const MemoryBlock& src, const MemoryBlock& dst, size_t len)
|
|||
|
||||
|
||||
bool
|
||||
Dma::write(const MemoryBlock& mem, size_t len)
|
||||
Dma::write(const MemoryBlock &mem, size_t len)
|
||||
{
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
|
||||
// user has to make sure that memory is accessible, otherwise this will throw
|
||||
auto translation = mm.getTranslation(busMasterInterfaces[mm2sInterface],
|
||||
|
@ -166,9 +164,9 @@ Dma::write(const MemoryBlock& mem, size_t len)
|
|||
|
||||
|
||||
bool
|
||||
Dma::read(const MemoryBlock& mem, size_t len)
|
||||
Dma::read(const MemoryBlock &mem, size_t len)
|
||||
{
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
|
||||
// user has to make sure that memory is accessible, otherwise this will throw
|
||||
auto translation = mm.getTranslation(busMasterInterfaces[s2mmInterface],
|
||||
|
@ -351,7 +349,7 @@ Dma::readCompleteSimple()
|
|||
|
||||
|
||||
bool
|
||||
Dma::makeAccesibleFromVA(const MemoryBlock& mem)
|
||||
Dma::makeAccesibleFromVA(const MemoryBlock &mem)
|
||||
{
|
||||
// only symmetric mapping supported currently
|
||||
if (isMemoryBlockAccesible(mem, s2mmInterface) and
|
||||
|
@ -377,9 +375,9 @@ Dma::makeAccesibleFromVA(const MemoryBlock& mem)
|
|||
|
||||
|
||||
bool
|
||||
Dma::isMemoryBlockAccesible(const MemoryBlock& mem, const std::string& interface)
|
||||
Dma::isMemoryBlockAccesible(const MemoryBlock &mem, const std::string &interface)
|
||||
{
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
|
||||
try {
|
||||
mm.findPath(getMasterAddrSpaceByInterface(interface), mem.getAddrSpaceId());
|
||||
|
@ -401,7 +399,3 @@ Dma::dump()
|
|||
logger->info("S2MM_LENGTH: {:x}", XAxiDma_ReadReg(xDma.RegBase, XAXIDMA_RX_OFFSET + XAXIDMA_BUFFLEN_OFFSET));
|
||||
}
|
||||
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -32,9 +32,7 @@
|
|||
#include <villas/fpga/ips/intc.hpp>
|
||||
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
namespace ip {
|
||||
using namespace villas::fpga::ip;
|
||||
|
||||
// instantiate factory to make available to plugin infrastructure
|
||||
static FifoFactory factory;
|
||||
|
@ -113,6 +111,3 @@ size_t Fifo::read(void *buf, size_t len)
|
|||
return nextlen;
|
||||
}
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -31,9 +31,7 @@
|
|||
#include <villas/fpga/card.hpp>
|
||||
#include <villas/fpga/ips/gpio.hpp>
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
namespace ip {
|
||||
using namespace villas::fpga::ip;
|
||||
|
||||
|
||||
// instantiate factory to make available to plugin infrastructure
|
||||
|
@ -47,6 +45,3 @@ GeneralPurposeIO::init()
|
|||
return true;
|
||||
}
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -31,10 +31,7 @@
|
|||
#include <villas/fpga/card.hpp>
|
||||
#include <villas/fpga/ips/intc.hpp>
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
namespace ip {
|
||||
|
||||
using namespace villas::fpga::ip;
|
||||
|
||||
// instantiate factory to make available to plugin infrastructure
|
||||
static InterruptControllerFactory factory;
|
||||
|
@ -171,6 +168,3 @@ InterruptController::waitForInterrupt(int irq)
|
|||
}
|
||||
}
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -29,16 +29,14 @@
|
|||
#include <villas/fpga/ips/pcie.hpp>
|
||||
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
namespace ip {
|
||||
using namespace villas::fpga::ip;
|
||||
|
||||
static AxiPciExpressBridgeFactory factory;
|
||||
|
||||
bool
|
||||
AxiPciExpressBridge::init()
|
||||
{
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
|
||||
// Throw an exception if the is no bus master interface and thus no
|
||||
// address space we can use for translation -> error
|
||||
|
@ -74,34 +72,28 @@ AxiPciExpressBridge::init()
|
|||
|
||||
auto pciAddrSpaceId = mm.getPciAddressSpace();
|
||||
|
||||
struct pci_region* pci_regions = nullptr;
|
||||
size_t num_regions = pci_get_regions(card->pdev, &pci_regions);
|
||||
auto regions = card->pdev->getRegions();
|
||||
|
||||
for (size_t i = 0; i < num_regions; i++) {
|
||||
const size_t region_size = pci_regions[i].end - pci_regions[i].start + 1;
|
||||
int i = 0;
|
||||
for (auto region : regions) {
|
||||
const size_t region_size = region.end - region.start + 1;
|
||||
|
||||
char barName[] = "BARx";
|
||||
barName[3] = '0' + pci_regions[i].num;
|
||||
barName[3] = '0' + region.num;
|
||||
auto pciBar = pcieToAxiTranslations.at(barName);
|
||||
|
||||
|
||||
logger->info("PCI-BAR{}: bus addr={:#x} size={:#x}",
|
||||
pci_regions[i].num, pci_regions[i].start, region_size);
|
||||
region.num, region.start, region_size);
|
||||
logger->info("PCI-BAR{}: AXI translation offset {:#x}",
|
||||
i, pciBar.translation);
|
||||
|
||||
mm.createMapping(pci_regions[i].start, pciBar.translation, region_size,
|
||||
mm.createMapping(region.start, pciBar.translation, region_size,
|
||||
std::string("PCI-") + barName,
|
||||
pciAddrSpaceId, card->addrSpaceIdHostToDevice);
|
||||
|
||||
}
|
||||
|
||||
if (pci_regions != nullptr) {
|
||||
logger->debug("freeing pci regions");
|
||||
free(pci_regions);
|
||||
}
|
||||
|
||||
|
||||
for (auto& [barName, axiBar] : axiToPcieTranslations) {
|
||||
logger->info("AXI-{}: bus addr={:#x} size={:#x}",
|
||||
barName, axiBar.base, axiBar.size);
|
||||
|
@ -116,16 +108,18 @@ AxiPciExpressBridge::init()
|
|||
mm.createMapping(0, axiBar.translation, axiBar.size,
|
||||
std::string("AXI-") + barName,
|
||||
barXAddrSpaceId, pciAddrSpaceId);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
AxiPciExpressBridgeFactory::configureJson(Core& ip, json_t* json_ip)
|
||||
AxiPciExpressBridgeFactory::configureJson(Core &ip, json_t* json_ip)
|
||||
{
|
||||
auto logger = getLogger();
|
||||
auto& pcie = dynamic_cast<AxiPciExpressBridge&>(ip);
|
||||
auto &pcie = dynamic_cast<AxiPciExpressBridge&>(ip);
|
||||
|
||||
for (auto barType : std::list<std::string>{"axi_bars", "pcie_bars"}) {
|
||||
json_t* json_bars = json_object_get(json_ip, barType.c_str());
|
||||
|
@ -159,18 +153,12 @@ AxiPciExpressBridgeFactory::configureJson(Core& ip, json_t* json_ip)
|
|||
.size = static_cast<size_t>(size),
|
||||
.translation = translation
|
||||
};
|
||||
|
||||
} else {
|
||||
} else
|
||||
pcie.pcieToAxiTranslations[bar_name] = {
|
||||
.translation = translation
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -53,9 +53,7 @@
|
|||
/* Control register bits */
|
||||
#define RTDS_AXIS_CR_DISABLE_LINK 0 /**< Disable SFP TX when set */
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
namespace ip {
|
||||
using namespace villas::fpga::ip;
|
||||
|
||||
static RtdsFactory rtdsFactoryInstance;
|
||||
|
||||
|
@ -92,6 +90,3 @@ double Rtds::getDt()
|
|||
return (dt == 0xFFFF) ? 0.0 : (double) dt / RTDS_HZ;
|
||||
}
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
#include <villas/memory_manager.hpp>
|
||||
#include <villas/fpga/ips/gpu2rtds.hpp>
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
namespace ip {
|
||||
using namespace villas::fpga::ip;
|
||||
|
||||
static Gpu2RtdsFactory factory;
|
||||
|
||||
|
@ -15,7 +13,7 @@ bool Gpu2Rtds::init()
|
|||
{
|
||||
Hls::init();
|
||||
|
||||
auto& registers = addressTranslations.at(registerMemory);
|
||||
auto ®isters = addressTranslations.at(registerMemory);
|
||||
|
||||
registerStatus = reinterpret_cast<StatusRegister*>(registers.getLocalAddr(registerStatusOffset));
|
||||
registerStatusCtrl = reinterpret_cast<StatusControlRegister*>(registers.getLocalAddr(registerStatusCtrlOffset));
|
||||
|
@ -55,9 +53,9 @@ void Gpu2Rtds::dump(spdlog::level::level_enum logLevel)
|
|||
logger->log(logLevel, " Max. frame size: {}", status.max_frame_size);
|
||||
}
|
||||
|
||||
//bool Gpu2Rtds::startOnce(const MemoryBlock& mem, size_t frameSize, size_t dataOffset, size_t doorbellOffset)
|
||||
//bool Gpu2Rtds::startOnce(const MemoryBlock &mem, size_t frameSize, size_t dataOffset, size_t doorbellOffset)
|
||||
//{
|
||||
// auto& mm = MemoryManager::get();
|
||||
// auto &mm = MemoryManager::get();
|
||||
|
||||
// if (frameSize > maxFrameSize) {
|
||||
// logger->error("Requested frame size of {} exceeds max. frame size of {}",
|
||||
|
@ -123,7 +121,7 @@ Gpu2Rtds::getMaxFrameSize()
|
|||
//void
|
||||
//Gpu2Rtds::dumpDoorbell(uint32_t doorbellRegister) const
|
||||
//{
|
||||
// auto& doorbell = reinterpret_cast<reg_doorbell_t&>(doorbellRegister);
|
||||
// auto &doorbell = reinterpret_cast<reg_doorbell_t&>(doorbellRegister);
|
||||
|
||||
// logger->info("Doorbell register: {:#08x}", doorbell.value);
|
||||
// logger->info(" Valid: {}", (doorbell.is_valid ? "yes" : "no"));
|
||||
|
@ -131,6 +129,3 @@ Gpu2Rtds::getMaxFrameSize()
|
|||
// logger->info(" Seq. number: {}", doorbell.seq_nr);
|
||||
//}
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -5,10 +5,7 @@
|
|||
#include <villas/memory_manager.hpp>
|
||||
#include <villas/fpga/ips/rtds2gpu.hpp>
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
namespace ip {
|
||||
|
||||
using namespace villas::fpga::ip;
|
||||
static Rtds2GpuFactory factory;
|
||||
|
||||
bool Rtds2Gpu::init()
|
||||
|
@ -28,8 +25,6 @@ bool Rtds2Gpu::init()
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Rtds2Gpu::dump(spdlog::level::level_enum logLevel)
|
||||
{
|
||||
const auto baseaddr = XRtds2gpu_Get_baseaddr(&xInstance);
|
||||
|
@ -52,9 +47,9 @@ void Rtds2Gpu::dump(spdlog::level::level_enum logLevel)
|
|||
logger->log(logLevel, " Max. frame size: {}", status.max_frame_size);
|
||||
}
|
||||
|
||||
bool Rtds2Gpu::startOnce(const MemoryBlock& mem, size_t frameSize, size_t dataOffset, size_t doorbellOffset)
|
||||
bool Rtds2Gpu::startOnce(const MemoryBlock &mem, size_t frameSize, size_t dataOffset, size_t doorbellOffset)
|
||||
{
|
||||
auto& mm = MemoryManager::get();
|
||||
auto &mm = MemoryManager::get();
|
||||
|
||||
if (frameSize > maxFrameSize) {
|
||||
logger->error("Requested frame size of {} exceeds max. frame size of {}",
|
||||
|
@ -108,7 +103,7 @@ Rtds2Gpu::getMaxFrameSize()
|
|||
void
|
||||
Rtds2Gpu::dumpDoorbell(uint32_t doorbellRegister) const
|
||||
{
|
||||
auto& doorbell = reinterpret_cast<reg_doorbell_t&>(doorbellRegister);
|
||||
auto &doorbell = reinterpret_cast<reg_doorbell_t&>(doorbellRegister);
|
||||
|
||||
logger->info("Doorbell register: {:#08x}", doorbell.value);
|
||||
logger->info(" Valid: {}", (doorbell.is_valid ? "yes" : "no"));
|
||||
|
@ -116,6 +111,3 @@ Rtds2Gpu::dumpDoorbell(uint32_t doorbellRegister) const
|
|||
logger->info(" Seq. number: {}", doorbell.seq_nr);
|
||||
}
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -70,8 +70,8 @@ AxiStreamSwitch::init()
|
|||
}
|
||||
|
||||
bool
|
||||
AxiStreamSwitch::connectInternal(const std::string& portSlave,
|
||||
const std::string& portMaster)
|
||||
AxiStreamSwitch::connectInternal(const std::string &portSlave,
|
||||
const std::string &portMaster)
|
||||
{
|
||||
// check if slave port exists
|
||||
try {
|
||||
|
@ -129,21 +129,21 @@ AxiStreamSwitch::connectInternal(const std::string& portSlave,
|
|||
}
|
||||
|
||||
int
|
||||
AxiStreamSwitch::portNameToNum(const std::string& portName)
|
||||
AxiStreamSwitch::portNameToNum(const std::string &portName)
|
||||
{
|
||||
const std::string number = portName.substr(1, 2);
|
||||
return std::stoi(number);
|
||||
}
|
||||
|
||||
bool
|
||||
AxiStreamSwitchFactory::configureJson(Core& ip, json_t* json_ip)
|
||||
AxiStreamSwitchFactory::configureJson(Core &ip, json_t* json_ip)
|
||||
{
|
||||
if (not NodeFactory::configureJson(ip, json_ip))
|
||||
return false;
|
||||
|
||||
auto logger = getLogger();
|
||||
|
||||
auto& axiSwitch = dynamic_cast<AxiStreamSwitch&>(ip);
|
||||
auto &axiSwitch = dynamic_cast<AxiStreamSwitch&>(ip);
|
||||
|
||||
if (json_unpack(json_ip, "{ s: i }", "num_ports", &axiSwitch.num_ports) != 0) {
|
||||
logger->error("Cannot parse 'num_ports'");
|
||||
|
@ -154,6 +154,6 @@ AxiStreamSwitchFactory::configureJson(Core& ip, json_t* json_ip)
|
|||
}
|
||||
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
} /* namespace ip */
|
||||
} /* namespace fpga */
|
||||
} /* namespace villas */
|
||||
|
|
|
@ -30,9 +30,7 @@
|
|||
#include <villas/fpga/ips/timer.hpp>
|
||||
#include <villas/fpga/ips/intc.hpp>
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
namespace ip {
|
||||
using namespace villas::fpga::ip;
|
||||
|
||||
|
||||
// instantiate factory to make available to plugin infrastructure
|
||||
|
@ -80,8 +78,3 @@ uint32_t Timer::remaining()
|
|||
{
|
||||
return XTmrCtr_GetValue(&xTmr, 0);
|
||||
}
|
||||
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
#include <villas/memory.hpp>
|
||||
|
||||
namespace villas {
|
||||
using namespace villas;
|
||||
|
||||
bool
|
||||
HostRam::free(void* addr, size_t length)
|
||||
|
@ -42,5 +42,3 @@ HostRam::allocate(size_t length, int flags)
|
|||
|
||||
return mmap(nullptr, length, mmap_protection, mmap_flags, 0, 0);
|
||||
}
|
||||
|
||||
} // namespace villas
|
||||
|
|
|
@ -30,18 +30,15 @@
|
|||
#include <villas/fpga/node.hpp>
|
||||
#include <villas/fpga/ips/switch.hpp>
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
namespace ip {
|
||||
|
||||
using namespace villas::fpga::ip;
|
||||
|
||||
StreamGraph
|
||||
Node::streamGraph;
|
||||
|
||||
bool
|
||||
NodeFactory::configureJson(Core& ip, json_t* json_ip)
|
||||
NodeFactory::configureJson(Core &ip, json_t* json_ip)
|
||||
{
|
||||
auto& Node = dynamic_cast<ip::Node&>(ip);
|
||||
auto &Node = dynamic_cast<ip::Node&>(ip);
|
||||
auto logger = getLogger();
|
||||
|
||||
json_t* json_ports = json_object_get(json_ip, "ports");
|
||||
|
@ -115,7 +112,7 @@ Node::getLoopbackPorts() const
|
|||
return { "", "" };
|
||||
}
|
||||
|
||||
bool Node::connect(const StreamVertex& from, const StreamVertex& to)
|
||||
bool Node::connect(const StreamVertex &from, const StreamVertex &to)
|
||||
{
|
||||
if (from.nodeName != getInstanceName()) {
|
||||
logger->error("Cannot connect from a foreign StreamVertex: {}", from);
|
||||
|
@ -192,8 +189,8 @@ Node::loopbackPossible() const
|
|||
}
|
||||
|
||||
bool
|
||||
Node::connectInternal(const std::string& slavePort,
|
||||
const std::string& masterPort)
|
||||
Node::connectInternal(const std::string &slavePort,
|
||||
const std::string &masterPort)
|
||||
{
|
||||
(void) slavePort;
|
||||
(void) masterPort;
|
||||
|
@ -206,15 +203,11 @@ bool
|
|||
Node::connectLoopback()
|
||||
{
|
||||
auto ports = getLoopbackPorts();
|
||||
const auto& portMaster = portsMaster[ports.first];
|
||||
const auto& portSlave = portsSlave[ports.second];
|
||||
const auto &portMaster = portsMaster[ports.first];
|
||||
const auto &portSlave = portsSlave[ports.second];
|
||||
|
||||
logger->debug("master port: {}", ports.first);
|
||||
logger->debug("slave port: {}", ports.second);
|
||||
|
||||
return connect(*portMaster, *portSlave);
|
||||
}
|
||||
|
||||
} // namespace ip
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -25,8 +25,7 @@
|
|||
|
||||
#include <villas/fpga/vlnv.hpp>
|
||||
|
||||
namespace villas {
|
||||
namespace fpga {
|
||||
using namespace villas::fpga;
|
||||
|
||||
bool
|
||||
Vlnv::operator==(const Vlnv &other) const
|
||||
|
@ -74,5 +73,3 @@ Vlnv::toString() const
|
|||
return string;
|
||||
}
|
||||
|
||||
} // namespace fpga
|
||||
} // namespace villas
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
#include <villas/log.h>
|
||||
#include <villas/utils.hpp>
|
||||
|
||||
#include <villas/kernel/pci.h>
|
||||
#include <villas/kernel/pci.hpp>
|
||||
#include <villas/kernel/kernel.hpp>
|
||||
|
||||
#include <villas/fpga/card.h>
|
||||
|
@ -54,7 +54,6 @@ int main(int argc, char *argv[])
|
|||
|
||||
struct list cards;
|
||||
struct vfio_container vc;
|
||||
struct pci pci;
|
||||
struct fpga_card *card;
|
||||
|
||||
/* Parse arguments */
|
||||
|
@ -88,9 +87,7 @@ check: if (optarg == endptr)
|
|||
json_error_t err;
|
||||
json_t *json;
|
||||
|
||||
ret = pci_init(&pci);
|
||||
if (ret)
|
||||
return -1;
|
||||
auto pciDevices = std::make_shared<kernel::pci::DeviceList>();
|
||||
|
||||
ret = vfio_init(&vc);
|
||||
if (ret)
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
using namespace villas;
|
||||
|
||||
static struct pci pci;
|
||||
static std::shared_ptr<kernel::pci::DeviceList> pciDevices;
|
||||
static auto logger = villas::logging.get("streamer");
|
||||
|
||||
void setupColorHandling()
|
||||
|
@ -64,14 +64,11 @@ void setupColorHandling()
|
|||
}
|
||||
|
||||
std::shared_ptr<fpga::PCIeCard>
|
||||
setupFpgaCard(const std::string& configFile, const std::string& fpgaName)
|
||||
setupFpgaCard(const std::string &configFile, const std::string &fpgaName)
|
||||
{
|
||||
if (pci_init(&pci) != 0) {
|
||||
logger->error("Cannot initialize PCI");
|
||||
exit(1);
|
||||
}
|
||||
pciDevices = std::make_shared<kernel::pci::DeviceList>();
|
||||
|
||||
auto vfioContainer = villas::VfioContainer::create();
|
||||
auto vfioContainer = kernel::vfio::Container::create();
|
||||
|
||||
/* Parse FPGA configuration */
|
||||
FILE* f = fopen(configFile.c_str(), "r");
|
||||
|
@ -102,9 +99,9 @@ setupFpgaCard(const std::string& configFile, const std::string& fpgaName)
|
|||
}
|
||||
|
||||
// create all FPGA card instances using the corresponding plugin
|
||||
auto cards = fpgaCardPlugin->make(fpgas, &pci, vfioContainer);
|
||||
auto cards = fpgaCardPlugin->make(fpgas, pciDevices, vfioContainer);
|
||||
|
||||
for (auto& fpgaCard : cards) {
|
||||
for (auto &fpgaCard : cards) {
|
||||
if (fpgaCard->name == fpgaName) {
|
||||
return fpgaCard;
|
||||
}
|
||||
|
@ -192,7 +189,7 @@ int main(int argc, char* argv[])
|
|||
|
||||
size_t memIdx = 0;
|
||||
|
||||
for (auto& value: values) {
|
||||
for (auto &value: values) {
|
||||
if (value.empty()) continue;
|
||||
|
||||
const int32_t number = std::stoi(value);
|
||||
|
|
|
@ -34,22 +34,22 @@ set(SOURCES
|
|||
# hls.cpp
|
||||
# intc.cpp
|
||||
|
||||
add_executable(unit-tests ${SOURCES})
|
||||
add_executable(unit-tests-fpga ${SOURCES})
|
||||
|
||||
if (CMAKE_CUDA_COMPILER)
|
||||
enable_language(CUDA)
|
||||
target_sources(unit-tests PRIVATE
|
||||
target_sources(unit-tests-fpga PRIVATE
|
||||
gpu.cpp rtds2gpu.cpp gpu_kernels.cu)
|
||||
endif ()
|
||||
|
||||
find_package(Criterion REQUIRED)
|
||||
|
||||
target_include_directories(unit-tests PUBLIC
|
||||
target_include_directories(unit-tests-fpga PUBLIC
|
||||
../include
|
||||
${CRITERION_INCLUDE_DIRECTORIES}
|
||||
)
|
||||
|
||||
target_link_libraries(unit-tests PUBLIC
|
||||
target_link_libraries(unit-tests-fpga PUBLIC
|
||||
villas-fpga
|
||||
${CRITERION_LIBRARIES}
|
||||
)
|
||||
|
|
|
@ -39,7 +39,7 @@ Test(fpga, dma, .description = "DMA")
|
|||
|
||||
std::list<std::shared_ptr<fpga::ip::Dma>> dmaIps;
|
||||
|
||||
for (auto& ip : state.cards.front()->ips) {
|
||||
for (auto &ip : state.cards.front()->ips) {
|
||||
if (*ip == fpga::Vlnv("xilinx.com:ip:axi_dma:")) {
|
||||
auto dma = std::dynamic_pointer_cast<fpga::ip::Dma>(ip);
|
||||
dmaIps.push_back(dma);
|
||||
|
@ -47,7 +47,7 @@ Test(fpga, dma, .description = "DMA")
|
|||
}
|
||||
|
||||
size_t count = 0;
|
||||
for (auto& dma : dmaIps) {
|
||||
for (auto &dma : dmaIps) {
|
||||
logger->info("Testing {}", *dma);
|
||||
|
||||
if (not dma->loopbackPossible()) {
|
||||
|
|
|
@ -40,7 +40,7 @@ Test(fpga, fifo, .description = "FIFO")
|
|||
|
||||
auto logger = logging.get("unit-test:fifo");
|
||||
|
||||
for (auto& ip : state.cards.front()->ips) {
|
||||
for (auto &ip : state.cards.front()->ips) {
|
||||
// skip non-fifo IPs
|
||||
if (*ip != fpga::Vlnv("xilinx.com:ip:axi_fifo_mm_s:"))
|
||||
continue;
|
||||
|
|
|
@ -40,14 +40,12 @@
|
|||
|
||||
using namespace villas;
|
||||
|
||||
static struct pci pci;
|
||||
static std::shared_ptr<kernel::pci::DeviceList> pciDevices;
|
||||
|
||||
FpgaState state;
|
||||
|
||||
static void init()
|
||||
{
|
||||
int ret;
|
||||
|
||||
FILE *f;
|
||||
json_error_t err;
|
||||
|
||||
|
@ -56,10 +54,9 @@ static void init()
|
|||
|
||||
plugin::Registry::dumpList();
|
||||
|
||||
ret = pci_init(&pci);
|
||||
cr_assert_eq(ret, 0, "Failed to initialize PCI sub-system");
|
||||
pciDevices = std::make_shared<kernel::pci::DeviceList>();
|
||||
|
||||
auto vfioContainer = VfioContainer::create();
|
||||
auto vfioContainer = kernel::vfio::Container::create();
|
||||
|
||||
/* Parse FPGA configuration */
|
||||
char *fn = getenv("TEST_CONFIG");
|
||||
|
@ -80,7 +77,7 @@ static void init()
|
|||
cr_assert_not_null(fpgaCardPlugin, "No plugin for FPGA card found");
|
||||
|
||||
// create all FPGA card instances using the corresponding plugin
|
||||
state.cards = fpgaCardPlugin->make(fpgas, &pci, vfioContainer);
|
||||
state.cards = fpgaCardPlugin->make(fpgas, pciDevices, vfioContainer);
|
||||
|
||||
cr_assert(state.cards.size() != 0, "No FPGA cards found!");
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
class FpgaState {
|
||||
public:
|
||||
// list of all available FPGA cards, only first will be tested at the moment
|
||||
villas::fpga::CardList cards;
|
||||
villas::fpga::PCIeCard::List cards;
|
||||
};
|
||||
|
||||
// global state to be shared by unittests
|
||||
|
|
|
@ -44,7 +44,7 @@ Test(fpga, gpu_dma, .description = "GPU DMA tests")
|
|||
{
|
||||
auto logger = logging.get("unit-test:dma");
|
||||
|
||||
auto& card = state.cards.front();
|
||||
auto &card = state.cards.front();
|
||||
|
||||
auto gpuPlugin = Plugin::Registry<GpuFactory>("cuda");
|
||||
cr_assert_not_null(gpuPlugin, "No GPU plugin found");
|
||||
|
@ -53,10 +53,10 @@ Test(fpga, gpu_dma, .description = "GPU DMA tests")
|
|||
cr_assert(gpus.size() > 0, "No GPUs found");
|
||||
|
||||
// just get first cpu
|
||||
auto& gpu = gpus.front();
|
||||
auto &gpu = gpus.front();
|
||||
|
||||
size_t count = 0;
|
||||
for (auto& ip : card->ips) {
|
||||
for (auto &ip : card->ips) {
|
||||
// skip non-dma IPs
|
||||
if (*ip != fpga::Vlnv("xilinx.com:ip:axi_bram_ctrl:"))
|
||||
continue;
|
||||
|
@ -97,17 +97,17 @@ Test(fpga, gpu_dma, .description = "GPU DMA tests")
|
|||
gpu->makeAccessibleToPCIeAndVA(gpuMem1.getMemoryBlock());
|
||||
|
||||
|
||||
// auto& src = bram0;
|
||||
// auto& dst = bram1;
|
||||
// auto &src = bram0;
|
||||
// auto &dst = bram1;
|
||||
|
||||
// auto& src = hostRam0;
|
||||
// auto& dst = hostRam1;
|
||||
// auto &src = hostRam0;
|
||||
// auto &dst = hostRam1;
|
||||
|
||||
auto& src = dmaRam0;
|
||||
// auto& dst = dmaRam1;
|
||||
auto &src = dmaRam0;
|
||||
// auto &dst = dmaRam1;
|
||||
|
||||
// auto& src = gpuMem0;
|
||||
auto& dst = gpuMem1;
|
||||
// auto &src = gpuMem0;
|
||||
auto &dst = gpuMem1;
|
||||
|
||||
|
||||
std::list<std::pair<std::string, std::function<void()>>> memcpyFuncs = {
|
||||
|
|
|
@ -49,7 +49,7 @@ Test(fpga, rtds, .description = "RTDS")
|
|||
std::list<villas::fpga::ip::Rtds*> rtdsIps;
|
||||
std::list<villas::fpga::ip::Dma*> dmaIps;
|
||||
|
||||
for (auto& ip : state.cards.front()->ips) {
|
||||
for (auto &ip : state.cards.front()->ips) {
|
||||
if (*ip == villas::fpga::Vlnv("acs.eonerc.rwth-aachen.de:user:rtds_axis:")) {
|
||||
auto rtds = reinterpret_cast<villas::fpga::ip::Rtds*>(ip.get());
|
||||
rtdsIps.push_back(rtds);
|
||||
|
|
|
@ -72,7 +72,7 @@ Test(fpga, rtds2gpu_loopback_dma, .description = "Rtds2Gpu")
|
|||
{
|
||||
auto logger = logging.get("unit-test:rtds2gpu");
|
||||
|
||||
for (auto& ip : state.cards.front()->ips) {
|
||||
for (auto &ip : state.cards.front()->ips) {
|
||||
if (*ip != fpga::Vlnv("acs.eonerc.rwth-aachen.de:hls:rtds2gpu:"))
|
||||
continue;
|
||||
|
||||
|
@ -191,11 +191,11 @@ Test(fpga, rtds2gpu_rtt_cpu, .description = "Rtds2Gpu RTT via CPU")
|
|||
cr_assert_not_null(gpu2rtds, "No Gpu2Rtds IP found");
|
||||
cr_assert_not_null(rtds2gpu, "No Rtds2Gpu IP not found");
|
||||
|
||||
for (auto& ip : state.cards.front()->ips) {
|
||||
for (auto &ip : state.cards.front()->ips) {
|
||||
if (*ip != fpga::Vlnv("acs.eonerc.rwth-aachen.de:user:rtds_axis:"))
|
||||
continue;
|
||||
|
||||
auto& rtds = dynamic_cast<fpga::ip::Rtds&>(*ip);
|
||||
auto &rtds = dynamic_cast<fpga::ip::Rtds&>(*ip);
|
||||
logger->info("Testing {}", rtds);
|
||||
|
||||
auto dmaRam = HostDmaRam::getAllocator().allocate<uint32_t>(SAMPLE_COUNT + 1);
|
||||
|
@ -269,7 +269,7 @@ Test(fpga, rtds2gpu_rtt_gpu, .description = "Rtds2Gpu RTT via GPU")
|
|||
cr_assert(gpus.size() > 0, "No GPUs found");
|
||||
|
||||
// just get first cpu
|
||||
auto& gpu = gpus.front();
|
||||
auto &gpu = gpus.front();
|
||||
|
||||
// allocate memory on GPU and make accessible by to PCIe/FPGA
|
||||
auto gpuRam = gpu->getAllocator().allocate<uint32_t>(SAMPLE_COUNT + 1);
|
||||
|
@ -291,11 +291,11 @@ Test(fpga, rtds2gpu_rtt_gpu, .description = "Rtds2Gpu RTT via GPU")
|
|||
|
||||
// auto doorbellInCpu = reinterpret_cast<reg_doorbell_t*>(&gpuRam[DOORBELL_OFFSET]);
|
||||
|
||||
for (auto& ip : state.cards.front()->ips) {
|
||||
for (auto &ip : state.cards.front()->ips) {
|
||||
if (*ip != fpga::Vlnv("acs.eonerc.rwth-aachen.de:user:rtds_axis:"))
|
||||
continue;
|
||||
|
||||
auto& rtds = dynamic_cast<fpga::ip::Rtds&>(*ip);
|
||||
auto &rtds = dynamic_cast<fpga::ip::Rtds&>(*ip);
|
||||
logger->info("Testing {}", rtds);
|
||||
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ Test(fpga, rtds_rtt, .description = "RTDS: tight rtt")
|
|||
std::list<villas::fpga::ip::Dma*> dmaIps;
|
||||
|
||||
/* Get IP cores */
|
||||
for (auto& ip : state.cards.front()->ips) {
|
||||
for (auto &ip : state.cards.front()->ips) {
|
||||
if (*ip == villas::fpga::Vlnv("acs.eonerc.rwth-aachen.de:user:rtds_axis:")) {
|
||||
auto rtds = reinterpret_cast<villas::fpga::ip::Rtds*>(ip.get());
|
||||
rtdsIps.push_back(rtds);
|
||||
|
|
|
@ -34,7 +34,7 @@ Test(fpga, timer, .description = "Timer Counter")
|
|||
auto logger = villas::logging.get("unit-test:timer");
|
||||
|
||||
size_t count = 0;
|
||||
for (auto& ip : state.cards.front()->ips) {
|
||||
for (auto &ip : state.cards.front()->ips) {
|
||||
// skip non-timer IPs
|
||||
if (*ip != villas::fpga::Vlnv("xilinx.com:ip:axi_timer:")) {
|
||||
continue;
|
||||
|
|
Loading…
Add table
Reference in a new issue