3、XRT运行时基础:XRT库初始化、设备枚举与上下文创建、管理Command Queue与Buffer
好,咱们今天聊聊XRT运行时。这东西说白了,就是FPGA加速卡在Linux系统里的“管家”。你写的上层应用,不管是Python还是C++,最终都得通过XRT跟板卡打交道。我个人习惯把XRT理解成一座桥——桥这边是你的业务逻辑,桥那边是硬件加速器。
嗯,咱们一步步来。先看看怎么把这座桥搭起来。
3.1 XRT库初始化
初始化XRT库,其实就一个函数调用的事。但你别小看这一步,我见过不少新手上来就报错,就是因为环境没配好。
#include <xrt/xrt_device.h>
#include <xrt/xrt_bo.h>
#include <xrt/xrt_kernel.h>
int main() {
// 第一步:获取设备
// 这里传入的是设备索引,0表示第一块卡
xrt::device device = xrt::device(0);
// 检查设备是否有效
if (!device) {
std::cerr << "设备打开失败" << std::endl;
return -1;
}
std::cout << "设备初始化成功" << std::endl;
return 0;
}
你看,代码就这么几行。但有个坑我必须提醒你——设备索引号。我曾经在一个项目里,服务器插了4块Alveo卡,结果索引0是空的,索引1才是第一块可用的卡。为什么?因为BIOS枚举顺序跟PCIe槽位不一定对应。
xrt::device::enumerate_devices() 获取所有可用设备列表,再根据设备ID或BDF号选择目标设备。
3.2 设备枚举与上下文创建
设备枚举,说白了就是“查户口”。你得知道系统里插了几块卡,每块卡是什么型号,有没有加载xclbin文件。
// 枚举所有设备
auto devices = xrt::device::enumerate_devices();
std::cout << "发现 " << devices.size() << " 个设备" << std::endl;
// 遍历设备,打印信息
for (auto& dev_id : devices) {
xrt::device dev(dev_id);
std::string name = dev.get_info<xrt::info::device::name>();
std::string bdf = dev.get_info<xrt::info::device::bdf>();
std::cout << "设备 " << dev_id
<< ": " << name
<< " (BDF: " << bdf << ")" << std::endl;
}
这里有个小技巧。我习惯在程序启动时先做一次枚举,把设备信息打印到日志里。这样调试的时候,一眼就能看出板卡连接状态对不对。
上下文创建呢?其实就是加载xclbin文件。xclbin是啥?你可以理解成FPGA的“固件镜像”。没有它,加速卡就是一块“砖”。
// 加载xclbin文件
std::string xclbin_path = "/path/to/your/accel.xclbin";
auto uuid = device.load_xclbin(xclbin_path);
// 检查加载是否成功
if (uuid == xrt::uuid()) {
std::cerr << "xclbin加载失败" << std::endl;
return -1;
}
std::cout << "xclbin加载成功,UUID: " << uuid.to_string() << std::endl;
3.3 管理Command Queue与Buffer
Command Queue,命令队列。这玩意儿是XRT里最核心的机制之一。你想想看,CPU和FPGA之间要传数据、要启动内核,总得有个“调度中心”吧?Command Queue就是干这个的。
Buffer呢?就是数据缓冲区。FPGA不能直接访问你的malloc内存,你得通过XRT申请一块“共享内存”,让CPU和FPGA都能读写。
3.3.1 创建Buffer
// 申请一个4KB的buffer
size_t buffer_size = 4096;
xrt::bo buffer = xrt::bo(device, buffer_size, xrt::bo::flags::normal, 0);
// 获取buffer的物理地址(FPGA侧使用)
uint64_t phy_addr = buffer.address();
std::cout << "Buffer物理地址: 0x" << std::hex << phy_addr << std::endl;
// 映射到用户空间(CPU侧使用)
void* host_ptr = buffer.map();
memset(host_ptr, 0, buffer_size); // 初始化为0
这里有个细节要注意——buffer的flags参数。normal模式是最常用的,但如果你需要高性能,可以考虑xrt::bo::flags::cacheable或xrt::bo::flags::device_only。我做过一个视频处理项目,用device_only模式直接把数据留在FPGA侧,省去了来回拷贝的开销,性能提升了30%。
3.3.2 创建Command Queue
// 创建命令队列
xrt::queue queue(device, 1024); // 队列深度1024
// 或者从kernel对象获取
xrt::kernel kernel = xrt::kernel(device, uuid, "my_kernel");
xrt::queue queue_from_kernel = kernel.queue();
队列深度设多少合适?这得看你的应用场景。我个人建议:
| 场景 | 推荐队列深度 | 说明 |
|---|---|---|
| 单次计算 | 16-64 | 够用就行,省内存 |
| 流水线处理 | 256-1024 | 深度大,吞吐高 |
| 实时控制 | 4-16 | 延迟敏感,队列浅 |
3.3.3 提交任务与同步
// 准备数据
int* data = (int*)host_ptr;
for (int i = 0; i < 1024; i++) {
data[i] = i;
}
// 将数据同步到FPGA
buffer.sync(XCL_BO_SYNC_BO_TO_DEVICE);
// 启动内核(假设内核有一个输入参数)
xrt::run run = kernel(buffer); // 传入buffer作为参数
run.start();
// 等待内核完成
run.wait();
// 将结果同步回CPU
buffer.sync(XCL_BO_SYNC_BO_FROM_DEVICE);
// 打印结果
std::cout << "第一个结果: " << data[0] << std::endl;
sync操作是显式的,CPU和FPGA不会自动同步数据run.wait()是阻塞的,会一直等到内核执行完毕- 如果你需要非阻塞操作,可以用
run.start()后立即返回,稍后再run.wait()
3.4 知识体系总览
说了这么多,咱们用一张图把整个流程串起来。这样你脑子里就有个清晰的脉络了。
嗯,这张图基本把整个流程串起来了。你写代码的时候,就按这个顺序来,准没错。
sync() 把数据传过去。结果内核跑完了,处理的全是垃圾数据。调试了整整两天才发现问题。所以,sync的顺序一定不能搞反:先同步到设备,再启动内核,最后同步回主机。
好了,XRT运行时的核心内容就这些。说白了就是三板斧:初始化设备、创建Buffer和Queue、同步数据启动内核。你把这几个步骤练熟了,后面写驱动就轻松多了。