以 Python 和 C++ 创建流量生成器 - 2022.1 简体中文

Versal ACAP AI 引擎编程环境 用户指南 (UG1076)

Document ID
UG1076
Release Date
2022-05-25
Version
2022.1 简体中文

概述

同时(并行)启动仿真器和流量生成器 (TG) 即可使用外部流量生成器进行仿真。这些 TG 可以 Python 或 C++ 来编写,并且可使用这两种语言的多线程功能。

以 Python 编写流量生成器

以 Python 编写流量生成器需导入多个库:
# Mandatory
import os, sys

import multiprocessing as mp
import threading
import struct

from xilinx_xtlm import ipc_axis_master_util
from xilinx_xtlm import ipc_axis_slave_util
from xilinx_xtlm import xtlm_ipc

# Optionnal, just for ease of use
import numpy as np
import logging
进出 AI 引擎阵列的每个端口都应由作为独立进程启动的函数来处理。首先,应创建阻塞传输实用工具。阻塞传输实用工具将在处理端口的函数中使用:
mm2s_util = ipc_axis_master_util("DataIn1")
self.s2mm_util = ipc_axis_slave_util("DataOut1")
处理端口的函数(以下示例中的 mm2s)应作为独立进程来启动:
tx = mp.Process(target=mm2s)
tx.start()
函数结束时,应停止该进程:
tx.join()
这是一个阻塞函数,它会等待函数结束后再停止进程。
存在下列机制用于在父进程与子进程之间进行通信:pipes。父进程会声明管道,通信则是使用 sendrecv 函数来操作的:
parent0, child0 = mp.Pipe()

child0.send(Tx_data)
Rx_data = parent0.recv()
如果端口是 AI 引擎到可编程逻辑的端口,那么必须首先从该端口读取数据:
self.s2mm_util = ipc_axis_slave_util("DataOut1")
payload 变量实际采用的是包含多个不同字段的结构:
  • data_length 是数据的字节数。
  • data 是数据本身。
  • tlast 是 TLAST 标志,设为 true 或 false。
如果此端口是可编程逻辑到 AI 引擎的端口,那么必须首先创建包:
payload = xtlm_ipc.axi_stream_packet()
随后,设置不同字段的值,并使用 b_transpor 方法将其发送至 AI 引擎阵列:
mm2s_util.b_transport(payload)

通过流量生成器以 Python 来格式化数据

要对 AXI4-Stream 传输事务进行仿真,AXI Traffic Generator 需将有效载荷数据分割为相应大小的突发。要发送 PLIO 宽度为 32 位(4 字节)的 128 字节数据,就需要 128 字节/4 字节 = 32 项 AXI4-Stream 传输事务。字节阵列与 AXI 传输事务之间的转换可由 Python 来处理。

Python struct 库可提供相应机制以在 Python 数据类型与 C 语言数据类型之间进行转换。尤其是,struct.packstruct.unpack 函数可根据格式字符串实参对字节阵列进行打包和解包。下表显示了常用 C 语言数据类型和 PLIO 宽度的格式字符串。

如需了解更多信息,请访问:https://docs.python.org/3/library/struct.html

表 1. C 语言数据类型和 PLIO 宽度的格式字符串
数据类型 PLIO 宽度 Python 代码片段
cfloat PLIO32 不适用
PLIO64 rVec = np.real(data)

iVec = np.imag(data)

out2column = np.zeros((L,2)).astype(np.single)

out2column.tobytes()

formatString = "<"+str(len(byte_arry)//4)+"f"

PLIO128
cint16 PLIO32 rVec = np.real(data).astype(np.int16)

iVec = np.imag(data).astype(np.int16)

formatString = "<"+str(len(byte_arry)//2)+"h"

PLIO64
PLIO128
int8 PLIO32 intvec = np.real(data).astype(np.int8)

formatString = "<"+str(len(byte_arry)//1)+"b"

PLIO64
PLIO128
int32 PLIO32 intvec = np.real(data).astype(np.int32)

formatString = "<"+str(len(byte_arry)//4)+"i"

PLIO64
PLIO128

以 C++ 编写流量生成器

使用 C++ 语言来实现外部流量生成器时,需要各种头文件才能使用某些库。Makefile 依赖关系如下所述:
# Libraries directories
PROTO_PATH=$(XILINX_VIVADO)/data/simmodels/xsim/2022.1/lnx64/6.2.0/ext/protobuf/
IPC_XTLM= $(XILINX_VIVADO)/data/emulation/ip_utils/xtlm_ipc/xtlm_ipc_v1_0/cpp/src/
IPC_XTLM_INC= $(XILINX_VIVADO)/data/emulation/ip_utils/xtlm_ipc/xtlm_ipc_v1_0/cpp/inc/
LOCAL_IPC= $(IPC_XTLM)../

LD_LIBRARY_PATH:=$(XILINX_VIVADO)/data/simmodels/xsim/2022.1/lnx64/6.2.0/ext/protobuf/:$(XILINX_VIVADO)/lib/lnx64.o/Default:$(XILINX_VIVADO)/lib/lnx64.o/:$(LD_LIBRARY_PATH)

# Kernel directories
PLKERNELS_DIR := ../../pl_kernels
PLKERNELS := $(PLKERNELS_DIR)/polar_clip.cpp
PLHEADERS := $(PLKERNELS_DIR)/polar_clip.hpp $(PLKERNELS_DIR)/s2mm.hpp $(PLKERNELS_DIR)/mm2s.hpp

# XTLM source files
IPC_SRC := $(LOCAL_IPC)/src/axis/*.cpp $(LOCAL_IPC)/src/common/*.cpp $(LOCAL_IPC)/src/common/*.cc

# Compiler/linker flags
INC_FLAGS := -I$(LOCAL_IPC)/inc -I$(LOCAL_IPC)/inc/axis/ -I$(LOCAL_IPC)/inc/common/ -I$(PROTO_PATH)/include/ -I$(PLKERNELS_DIR) -I$(XILINX_HLS)/include
LIB_FLAGS := -L$(PROTO_PATH)/ -lprotobuf -L$(XILINX_VIVADO)/lib/lnx64.o/ -lrdizlib -L$(GCC)/../../lib64/ -lstdc++ -lpthread

# Compilation
compile: main.cpp $(PLHEADERS) $(PLKERNELS)
  $(GCC) -g main.cpp $(PLKERNELS) $(IPC_SRC) $(INC_FLAGS) $(LIB_FLAGS) -o chain
适用于处理这些库的头文件包括:
# For the traffic generator
#include "xtlm_ipc.h"
#include <thread>
根据流量生成器是发射器还是接收器(实际可能两者兼具),套接字声明将不尽相同:
# Transmitter Traffic Generator
using b_init_socket = xtlm_ipc::axis_initiator_socket_util<xtlm_ipc::BLOCKING>;

# Receiver Traffic Generator
using b_targ_socket = xtlm_ipc::axis_target_socket_util<xtlm_ipc::BLOCKING>;
在此示例中,类用于处理流量生成器的各项功能:
class mm2s
{
std::thread m_thread;
std::unique_ptr<b_init_socket> m_socket_ptr;
int count;

void sock_data_handler()
{
m_socket_ptr = std::make_unique<b_init_socket>(m_sock_name);
std::vector<char> data_to_send;

while (count<512)
  {
  // Create a data to send ot the AI Engine Arra (vector of bytes)
  data_to_send = ...;

  m_socket_ptr->transport(data_to_send,count%128==127?true:false); // transport(data, tlast),   128 sample frame

  count++;
  }
}


protected :
// Name of the socket
const std::string m_sock_name;

public:
mm2s(const std::string sock_name) :
m_sock_name(sock_name), m_socket_ptr(nullptr),count(0)
{}

void run()
{
m_thread = std::thread(&mm2s::sock_data_handler, this);
}

// This function allows the user to check for the end of the transmission
int dataTransferred()
{
return(count);
}

// The destructor ends the thread
virtual ~mm2s()
{
std::cout << this->m_sock_name << " before join " << std::endl;
if(m_thread.joinable())
  m_thread.join();
std::cout << this->m_sock_name << " after join " << std::endl;
}
};
main 函数很简单,它仅用于启动流量生成器的各组件,同时在这些组件之间插入部分延迟,以使系统能够轻松完成初始化:
int main(int argc, char *argv[])
{

mm2s chain_1_mm2s("DataIn1");
polar_clip chain_1_pc ("clip_in", "clip_out");
s2mm_chain_1_s2mm("DataOut1");

using namespace std::chrono_literals;

chain_1_mm2s.run();
std::cout << "Started mm2s " << std::endl;
std::this_thread::sleep_for(500ms);

chain_1_pc.run();
std::cout << "Started polar_clip " << std::endl;
std::this_thread::sleep_for(400ms);

chain_1_s2mm.run();
std::cout << "Started s2mm " << std::endl;

# Waits for the end of the simulation (1024 samples received from S2MM block)
while(chain_1_s2mm.dataTransferred()!=1024)
  {
  // Waits 2s before retesting
  std::this_thread::sleep_for(2s);
  }
return(0)
}

C++ 流量生成器的有趣之处在于,HLS 内核一旦创建完成,即可立即使用和测试,无需在 .xo 文件内进行综合。这样您无需重新创建 .xclbin 文件即可给自己的仿真添加更多真实性和灵活性。