data_mover_runtime 模块技术深度解析
开篇:这个模块在解决什么问题
想象你正在开发一个 FPGA 加速系统。你的算法内核期望以特定的二进制格式接收数据——通常是固定宽度的 AXI 流,数据以十六进制形式打包。但你的上游数据源往往是文本文件:CSV 日志、浮点数值、传感器读数。如何将这些人类可读的文本数据转换成 FPGA 能够直接消费的硬件格式?
这就是 data_mover_runtime 模块存在的意义。它不是一个运行时库,而是一组数据形态转换工具——将软件世界中的抽象数据类型(float、double、half、各种位宽的整数)转换成硬件世界中的位级表示(固定宽度的十六进制比特流)。
这个模块的核心洞察是:数据在跨域流动时需要被重新诠释。同样的 32 位模式,在 CPU 的浮点单元中是 1.5f,在 FPGA 的 AXI 总线上就是 0x3fc00000。data_mover_runtime 就是这个跨域诠释的翻译器。
架构全景:模块的角色与边界
在更大的 Vitis Libraries 生态中,data_mover_runtime 位于主机端软件栈的底层。它向上为各种算法演示和基准测试提供数据准备能力,向下直接操作文件系统和内存。
┌─────────────────────────────────────────────────────────────┐
│ 算法内核 (FPGA Kernel) │
│ 期望: 固定宽度 AXI 流, 十六进制格式 │
└─────────────────────────────────────────────────────────────┘
▲
│ 数据通过 OpenCL/XRT 加载
┌─────────────────────────────────────────────────────────────┐
│ data_mover_runtime (本模块) │
│ 职责: 文本 → 十六进制比特流的格式转换 │
│ 核心: data_converter.cpp │
└─────────────────────────────────────────────────────────────┘
▲
│ 读取文本文件
┌─────────────────────────────────────────────────────────────┐
│ 上游数据源 │
│ CSV 文件, 数值日志, 随机生成的测试数据 │
└─────────────────────────────────────────────────────────────┘
关键边界约定:
-
输入契约:上游必须提供可被
operator>>解析的文本流。对于half类型,模块会先将输入读作float再转换,因为标准流操作不支持half。 -
输出契约:下游内核必须能够消费固定宽度的十六进制字符串数组。输出格式是逗号分隔的 C 风格字符串字面量,如
"0x3fc00000",可直接嵌入 C/C++ 源码或硬件测试平台。 -
位宽约束:输出流宽度必须是输入数据类型位宽的整数倍。例如,若输入是 32 位
float,输出宽度可以是 64、96、128... 但不能是 48。
核心组件深度解析
1. read_type<T> —— 类型层面的适配器
这是一个典型的 C++ 策略模式应用,用模板特化来处理类型系统的差异。
// 默认实现:identity mapping
template <class T>
struct read_type {
typedef T type;
};
// 特化:half 类型需要先读作 float
template <>
struct read_type<half> {
typedef float type;
};
设计意图:half(16 位浮点)是 FPGA 加速器中常用的低精度数值类型,但标准 C++ I/O 库并不原生支持它。read_type 在编译期建立了一个类型映射表:当模板参数是 half 时,实际用于流读取的类型是 float,后续再通过显式转换得到 half。
使用方式:
typename read_type<T>::type tmp;
fin >> tmp; // 编译期确定实际类型
vec[i++] = tmp; // 若 T 是 half,这里会发生 float→half 转换
2. get_bits() 函数族 —— 位级reinterpretation
这是一组**类型双关(type punning)**函数,用于提取浮点数的底层位模式。
uint16_t get_bits(half v) {
return v.get_bits();
}
uint32_t get_bits(float v) {
union {
float f;
uint32_t u;
} u;
u.f = v;
uint32_t ret = u.u;
return ret;
}
uint64_t get_bits(double v) { /* 类似 float 的实现 */ }
设计意图:在硬件描述语言(如 Verilog/VHDL)中,数值通常以无符号整数的形式流动,语义由消费端根据固定位宽重新诠释。get_bits 就是软件端的对应物:它回答"这些比特在内存中究竟长什么样"的问题。
实现技巧:
half类型来自 Xilinx HLS 库,直接提供get_bits()成员函数float/double使用 union type punning,这是一种在 C/C++ 标准边缘跳舞的技术。它依赖实现定义行为(implementation-defined),但在主流平台(IEEE-754 浮点)上可靠
为什么不用 memcpy?memcpy 是实现定义行为的标准化方式,但会引入函数调用开销。在 I/O 密集的转换场景中,union 方式通常被内联,且对于 4/8 字节的复制,现代编译器会生成最优代码。
3. generate_hex<T>() —— 主转换引擎
这是模块的核心编排逻辑,处理文件 I/O、类型转换、格式化和流控制。
template <typename T>
void generate_hex(std::string& ifile, std::string& ofile,
const int width, const int total_num)
执行流程:
-
参数校验:
assert(width < MAXVEC)—— 输出宽度不能超过 512 位(硬件限制)assert(n > 0)—— 输出宽度必须是输入类型位宽的整数倍
-
流准备:
- 打开输入文件文本流
- 打开输出文件文本流
-
批处理循环:
while (fin >> tmp) { cur_num++; vec[i++] = tmp; // 累料到本地数组 if (i == n) { // 凑够一个输出宽度 // 输出十六进制字符串 // 格式: "0x{高位在前的大端十六进制}" i = 0; // 重置批处理指针 } if (cur_num == total_num) break; } -
尾部处理:
- 如果输入结束但数组未填满(
i > 0),剩余位置用 0 填充 - 这保证了硬件总能读到固定宽度的数据
- 如果输入结束但数组未填满(
-
资源清理:
- 关闭文件流
关键设计决策:
| 决策 | 选择 | 理由 |
|---|---|---|
| 批处理大小 | 编译期计算 n = width / (8 * sizeof(T)) |
硬件 AXI 流宽度固定,必须在编译期确定打包粒度 |
| 输出字节序 | 大端(高位在前) | 匹配硬件预期,符合 RTL 设计惯例 |
| 填充策略 | 尾部零填充 | 保证事务完整性,避免硬件读到不确定状态 |
| 类型转换 | 编译期多态(模板) | 零运行时开销,每种类型生成最优代码 |
内存与资源管理模型
内存所有权
| 资源 | 分配者 | 所有者 | 生命周期 |
|---|---|---|---|
vec[MAXVEC / W] |
栈帧自动分配 | generate_hex 函数 |
函数返回时销毁 |
fin/fout 流对象 |
栈帧自动分配 | generate_hex 函数 |
close() 后或析构时 |
| 文件描述符 | ifstream/ofstream 内部 |
流对象(RAII) | 流析构时自动关闭 |
关键模式:完全使用栈分配 + RAII,没有堆内存管理负担。vec 是固定大小的 C 风格数组,其大小在编译期由 MAXVEC 和类型位宽确定。这种设计确保在资源受限的嵌入式主机环境中也能可靠运行。
对象生命周期与值语义
read_type<T>:纯编译期元函数,无实例,无生命周期get_bits返回值:按值返回 POD(Plain Old Data)整数,无复杂生命周期- 流操作:标准 iostream 的异常安全保证(basic guarantee)
错误处理策略
| 错误场景 | 处理方式 | 行为 |
|---|---|---|
| 参数违反(width 过大) | assert() |
调试构建中断言失败,发布构建未定义行为 |
| 文件打开失败 | 隐式检查 is_open() |
无显式处理,操作失败标志需调用者检查 |
| 流提取失败 | while (fin >> tmp) 条件 |
循环自然终止,剩余数据按尾部填充处理 |
| 类型不匹配 | 编译期模板实例化 | 不支持类型在编译期报错 |
设计权衡:该工具采用**快速失败(fail-fast)**哲学。参数检查使用 assert 而非异常,因为这些都是编程错误(调用者违反了契约),而非运行时异常条件。文件 I/O 错误没有被显式传播,因为该工具的典型使用模式是脚本化的批处理——输入文件的存在和质量由调用环境保证。
数据流全链路追踪
让我们追踪一次典型的转换调用:generate_hex<float>("input.txt", "output.hex", 128, 1024)
阶段 1:输入解析(主机文本域)
input.txt 内容:
1.0
2.5
3.14159
...
std::ifstream fin("input.txt");
typename read_type<float>::type tmp; // 实际类型: float
fin >> tmp; // 使用 operator>>(istream&, float)
关键转换:ASCII 字符序列 → IEEE-754 二进制浮点(主机字节序)
阶段 2:类型转换(若有)
vec[i++] = tmp; // float → float (本例无转换)
若 T = half:
vec[i++] = half(tmp); // float → half (精度截断,范围检查)
阶段 3:批处理累积
const int W = 8 * sizeof(float); // 32
float vec[MAXVEC / W]; // 缓冲区
const int n = 128 / 32; // 每批 4 个 float
// 循环直到凑够 n 个元素
while (i < n) {
fin >> tmp;
vec[i++] = tmp;
}
阶段 4:比特提取与格式化(关键转换)
fout << "\"0x";
for (int j = n - 1; j >= 0; --j) {
// j=3: vec[3] 是最高位 (MSB)
// j=0: vec[0] 是最低位 (LSB)
fout << std::hex << std::setfill('0') << std::setw(8) << get_bits(vec[j]);
}
fout << "\"";
// 输出示例: "0x40490fd040080000400200003f800000"
// | vec[3] || vec[2] || vec[1] || vec[0] |
关键洞察:输出是大端(big-endian)格式——数组索引较小的元素被放在十六进制字符串的右侧(低位),这与硬件 AXI 流的预期一致。
阶段 5:尾部处理与资源释放
// 处理未填满的批次
if (i > 0) {
// 剩余位置用 0 填充
for (int j = n - 1; j >= 0; --j) {
if (j >= i) fout << "00000000";
else fout << get_bits(vec[j]);
}
}
fin.close();
fout.close();
设计决策与权衡分析
决策 1:模板编译期多态 vs. 运行时类型分支
选择:使用 C++ 模板,为每种类型生成独立实例
template <typename T>
void generate_hex(...) { ... }
// 实例化 9 种类型
if (type == "half") generate_hex<half>(...);
else if (type == "float") generate_hex<float>(...);
// ...
替代方案:使用 void* 和类型标记,在运行时统一处理
权衡分析:
| 维度 | 模板方案(实际选择) | 运行时方案 |
|---|---|---|
| 性能 | 零开销,类型特化代码最优 | 分支预测开销,难以向量化 |
| 代码体积 | 9 份独立函数实例 | 单一份通用代码 |
| 类型安全 | 编译期检查,类型不匹配报错 | 运行时错误,类型标记需人工维护 |
| 调试体验 | 模板实例化错误信息复杂 | 单一调用栈,易追踪 |
| 扩展性 | 新增类型需添加模板实例化点 | 新增类型只需扩展 switch 分支 |
为何选择模板:这是一个数据密集型批处理工具,性能是首要考虑。模板确保每种类型生成最优的比特提取代码(特别是 half 直接调用 get_bits() 而非通过运行时分发)。同时,9 种类型 × 单一函数的规模在代码膨胀上是可接受的。
决策 2:Union Type Punning vs. memcpy/std::bit_cast
选择:使用 Union 进行类型双关
uint32_t get_bits(float v) {
union { float f; uint32_t u; } u;
u.f = v;
return u.u;
}
替代方案:
- C++20
std::bit_cast<uint32_t>(v)—— 标准保证的合法类型双关 memcpy(&u, &v, sizeof(v))—— 标准保证,但可能引入函数调用开销
权衡分析:
| 方案 | 标准符合性 | 性能 | 代码复杂度 | 依赖 |
|---|---|---|---|---|
| Union punning | 实现定义(GCC/Clang 支持) | 通常内联为寄存器操作 | 极简 | 无 |
std::bit_cast |
标准 C++20 | 编译期为常量表达式,运行时最优 | 简单 | C++20 |
memcpy |
标准 C99/C++11 | 小尺寸可能被内联,大尺寸调用库 | 中等 | 标准库 |
为何选择 Union:该代码库的目标平台是 Xilinx/AMD FPGA 工具链,使用的编译器是 GCC/Clang,对 Union type punning 有良好支持。同时,代码库需要兼容 C++11/14(从头文件风格判断),无法依赖 C++20 的 std::bit_cast。Union 方案以零依赖、零开销、极简代码实现了目标。
潜在风险:在严格遵循 C++ 标准的场景下,Union 类型双关的返回值是"实现定义"的。若未来代码需要移植到对这方面严格的编译器,需要重构为 std::bit_cast(如果升级 C++20)或 memcpy。
决策 3:栈分配缓冲区 vs. 堆分配/内存映射
选择:使用固定大小的栈分配数组
#define MAXVEC 512
T vec[MAXVEC / W]; // W = 8 * sizeof(T)
替代方案:
std::vector<T>动态堆分配new T[n]手动堆分配mmap文件内存映射
权衡分析:
| 方案 | 内存位置 | 生命周期管理 | 缓存局部性 | 尺寸灵活性 | 失败模式 |
|---|---|---|---|---|---|
| 栈数组(当前) | 栈 | 自动,无泄漏风险 | 极佳(连续,热缓存) | 固定上限(512 位宽度) | 栈溢出(递归/大帧) |
std::vector |
堆 | RAII,自动释放 | 好(连续,但需堆遍历) | 运行时动态 | bad_alloc 异常 |
new[] |
堆 | 手动,易泄漏 | 好 | 运行时动态 | nullptr/异常 |
mmap |
内核页缓存 | 手动/自动 | 依赖文件系统缓存 | 文件尺寸 | 系统调用失败 |
为何选择栈数组:
-
确定性性能:数据处理是 I/O 密集型的,而非内存受限。栈分配避免了堆遍历和缓存未命中。
-
资源受限环境:FPGA 加速卡的主机端通常是嵌入式 ARM 或有限内存的服务器节点。栈分配不增加堆内存压力。
-
固定上限合理:
MAXVEC 512位(64 字节)是 AXI4-Stream 的典型最大宽度。更大的宽度需要自定义协议,超出本工具范围。 -
无异常设计:该代码库似乎避免异常(无
try/catch可见)。栈分配不抛出bad_alloc,符合错误处理哲学。
潜在限制:若未来硬件接口宽度超过 512 位,需要修改 MAXVEC 并重新编译。
C/C++ 特定分析
内存所有权与 RAII
std::ifstream fin(ifile, std::ios_base::in);
std::ofstream fout;
fout.open(ofile);
// ... 处理 ...
fin.close();
fout.close();
所有权模型:
fin/fout:栈分配,拥有底层FILE*(通过 stdio 实现)。RAII 保证即使异常抛出,析构函数也会关闭文件描述符。vec:函数栈帧拥有,无堆参与。ifile/ofile:std::string按引用传入,函数借用字符串视图,不拥有内存。
关键风险:代码手动调用 fin.close()/fout.close(),这是冗余但无害的。标准保证析构时会再次关闭,第二次 close() 是空操作。若中途需要错误检查,应在 close() 后检查 fail()。
对象生命周期与模板实例化
template <typename T>
void generate_hex(...) { ... }
// main.cpp 中的显式实例化点
if (type == "half") generate_hex<half>(...);
else if (type == "float") generate_hex<float>(...);
// ... 8 more types
生命周期:
- 模板本身无生命周期,是编译期代码生成模板。
- 每个
generate_hex<T>实例是独立的函数,拥有独立的代码段和栈帧布局。 if-else链在运行时选择调用哪个实例,一旦进入函数,类型T固定,无运行时多态开销。
值语义:T 以值传递/返回(get_bits(T v)),对于小尺寸类型(最大 8 字节 double),这是最优的(寄存器传参)。无指针别名问题,编译器可自由优化。
错误处理策略
代码采用分层错误处理:
| 层级 | 机制 | 适用场景 |
|---|---|---|
| 编译期 | 模板约束 | 类型不支持(如传入自定义类)导致编译失败 |
| 调试期 | assert() |
参数违反(width 过大,对齐错误)触发断言 |
| 运行时 | 流状态位 | 文件打开/读写失败通过 std::ios::fail() 隐式标记 |
显著特点:无异常传播。即使文件打开失败,代码继续执行(fout.open 后无检查),失败会延迟到第一个输出操作,此时 fout << ... 无实际写入。这是有意为之的简化:在脚本化使用场景中,调用者(shell/Makefile)负责前置条件检查。
危险区域:
// 若 ofile 路径不可写,此处无报错
fout.open(ofile);
// ... 数百行处理 ...
fout.close(); // 静默失败,输出文件可能为空或不存在
生产环境使用应添加:
if (!fout.is_open()) {
std::cerr << "Failed to open output file: " << ofile << std::endl;
return 1;
}
const 正确性分析
代码在 const 使用上较为宽松:
// 当前实现
uint32_t get_bits(float v) { ... } // v 不会被修改,但非 const
template <typename T>
void generate_hex(std::string& ifile, ...) // ifile 不会被修改,但非 const&
改进建议:
// 更严格的 const 正确性
uint32_t get_bits(const float v) { ... }
template <typename T>
void generate_hex(const std::string& ifile, const std::string& ofile,
const int width, const int total_num)
当前实现功能正确,但缺少 const 意味着编译器无法某些别名分析优化,且读者无法一眼确认参数是否被修改。
并发与线程安全
设计立场:单线程顺序执行。
- 无全局/静态可变状态(
MAXVEC是宏定义的编译期常量) - 所有状态(流对象、缓冲区、计数器)都是函数栈局部
- 流操作(
fin >> tmp)是阻塞 I/O,无并发优化
多线程使用后果:若强行在多线程中并发调用 generate_hex,每个线程有独立栈帧,理论上数据竞争安全。但 ofstream 的并发写入未定义,且磁盘 I/O 会成为瓶颈,无实际加速收益。
性能架构
热点分析:
| 代码段 | 复杂度 | 优化策略 |
|---|---|---|
fin >> tmp |
\(O(N)\),N=输入数量 | 缓冲 I/O,标准库优化 |
get_bits(vec[j]) |
\(O(1)\),每个元素 | 内联,寄存器操作 |
fout << std::hex << ... |
\(O(N \times W)\),W=输出宽度 | 格式化 I/O,不可避免 |
瓶颈:此程序是I/O 密集型。磁盘/文件系统 I/O 主导执行时间,CPU 计算(类型转换、格式化)占比低。优化方向应是减少 I/O 往返(如内存映射文件),而非微优化 get_bits。
内存布局:
vec是连续数组,顺序访问,缓存友好- 输出格式化为十六进制字符串,每个字符 1 字节,输出文件大小约为输入数字个数的 \((2 \times W/8 + 2)\) 倍(
0x前缀 + 两个十六进制字符/字节)
使用模式与示例
基础用法:转换浮点测试数据
# 准备输入:一行一个浮点数
$ cat input.txt
1.0
2.0
3.14159
1.618
2.718
# 转换:4 个 float 打包成 128 位 AXI 流
$ ./data_converter -t float -i input.txt -o output.hex -w 128 -n 6
# 查看输出:每行是一个 C 字符串字面量
$ cat output.hex
"0x400000003f800000",
"0x402df85440090e56",
"0x00000000400a3c54"
处理半精度(half)数据
# 输入文本使用 float 格式(half 无法直接从文本解析)
$ cat half_input.txt
0.5
1.0
1.5
# 转换:8 个 half 打包成 128 位(half=16 位)
$ ./data_converter -t half -i half_input.txt -o half_output.hex -w 128 -n 3
# 输出:每个 half 占 4 个十六进制字符
$ cat half_output.hex
"0x00003c0038003000"
# ^ ^ ^
# | | 0.5 (0x3000)
# | 1.0 (0x3800)
# 1.5 (0x3c00,因 padding 到 8 个 half)
批处理脚本集成
#!/bin/bash
# prepare_test_data.sh:为多个测试用例生成输入数据
TYPES=("float" "int32_t" "half")
WIDTHS=(64 128 256)
for type in "${TYPES[@]}"; do
for width in "${WIDTHS[@]}"; do
input="test_${type}.txt"
output="test_\({type}_w\){width}.hex"
./data_converter -t "\(type" -i "\)input" -o "$output" \
-w "$width" -n 1000
if [ $? -ne 0 ]; then
echo "Error processing \(type with width \)width" >&2
exit 1
fi
done
done
边缘情况与潜在陷阱
1. 输出宽度对齐约束
// 危险:float 是 32 位,width 必须是 32 的整数倍
// 以下将导致 assert 失败
./data_converter -t float -w 48 ... // 48 不是 32 的整数倍
应对:始终在脚本或调用代码中验证 width % (sizeof(T) * 8) == 0。
2. 输入数量不匹配
// 输入文件只有 5 行,但 -n 10
./data_converter -t float -n 10 -i small_input.txt ...
// 实际行为:读取到文件结束,处理 5 个有效输入
// 注意:不会报错,调用者需自行确保输入数量充足
应对:生产环境使用前应验证输入文件行数与 -n 参数匹配。
3. Half 类型精度损失
// 输入文本:1.0001
// Half 只有 10 位尾数,约 3 位十进制精度
./data_converter -t half -i precise_input.txt ...
// 输出 hex 对应的 half 值可能是 1.0,精度被截断
应对:使用 half 类型前,确保输入数据范围在 ±65504 内,且精度需求不超过 3-4 位有效数字。
4. 大端 vs 小端误解
// 输出示例:"0x400000003f800000"
// 对应两个 float: 2.0 (0x40000000) 和 1.0 (0x3f800000)
// 数组索引: vec[0]=1.0, vec[1]=2.0
// 输出 hex: 高位在前 -> 2.0 在左,1.0 在右
陷阱:硬件工程师若期望"第一个输入在低位",会误解输出。实际上vec[0](最先读取的输入)被放在十六进制字符串的最右侧(LSB)。
应对:文档中明确标注输出格式为"大端,输入顺序对应 LSB 到 MSB"。
与其他模块的关系
data_mover_runtime
│
├─ 调用(uses):
│ ├─ xf::common::utils_sw::ArgParser (参数解析)
│ │ 来自:外部库(xf_utils_sw)
│ │ 用途:命令行选项解析,--help 生成
│ │
│ ├─ hls_half.h (Half 浮点类型)
│ │ 来自:Vitis HLS 运行时
│ │ 用途:16 位浮点数的软件仿真
│ │
│ └─ 标准库:<iostream>, <fstream>, <iomanip>
│ 用途:文件 I/O、格式化输出
│
└─ 被调用(used by):
├─ 各类 kernel 测试平台(testbench)
│ 场景:为 FPGA kernel 准备输入数据
│ 示例:图像编解码器测试、FFT 输入生成
│
└─ CI/CD 自动化脚本
场景:批量生成回归测试向量
重要依赖说明:
-
xf_utils_sw/ArgParser:该依赖表明
data_mover_runtime不是孤立工具,而是 Vitis 生态系统的一部分。参数解析器的风格(-t,-i,-o短选项)与 Vitis 其他工具一致。 -
hls_half.h:使用
HLS_NO_XIL_FPO_LIB宏禁用完整 FPO 库,仅引入基础half类型。这是链接时优化——避免引入未使用的浮点运算库。
总结:给新贡献者的行动指南
当你需要修改此模块时:
-
添加新数据类型支持:
- 在
main()的if-else链中添加新分支 - 若类型需要特殊读取逻辑(如
half→float),添加read_type<>特化 - 提供
get_bits()重载(若类型非 POD)
- 在
-
修改输出格式:
- 调整
fout << ...块的格式化逻辑 - 注意保持逗号分隔符和引号格式(下游工具可能依赖)
- 调整
-
处理更大宽度:
- 修改
MAXVEC宏(当前 512) - 验证硬件平台是否支持更宽 AXI 流
- 修改
常见调试场景:
| 现象 | 诊断 | 解决 |
|---|---|---|
assert(width < MAXVEC) 失败 |
请求的 AXI 宽度超过硬件支持 | 检查 -w 参数,或增大 MAXVEC 重新编译 |
| 输出 hex 长度不对 | 输入类型位宽与输出宽度不对齐 | 确保 width % (sizeof(T)*8) == 0 |
| Half 结果与预期不符 | Half 精度和范围有限 | 验证输入在 ±65504 内,精度需求 ≤3 位十进制 |
| 输出文件为空 | 输入文件不存在或 -n 过大 |
检查输入文件路径,验证文件行数与 -n 匹配 |
代码审查检查清单:
- [ ] 新增类型是否提供了
get_bits()支持? - [ ]
read_type<>特化是否必要?(仅当类型无法直接用operator>>读取时) - [ ] 文件 I/O 是否检查了
is_open()? - [ ] 参数是否使用了
const&避免不必要的拷贝? - [ ] 格式化输出是否保持与下游工具的兼容性?
附录:核心代码快速参考
支持的类型映射表
| 命令行参数 | C++ 类型 | 位宽 | 读取方式 | 特殊处理 |
|---|---|---|---|---|
half |
hls::half |
16 | 先读 float |
read_type<half> 特化 |
float |
float |
32 | 直接读取 | 无 |
double |
double |
64 | 直接读取 | 无 |
int8_t |
int8_t |
8 | 直接读取 | get_bits 模板默认实现 |
int16_t |
int16_t |
16 | 直接读取 | 同上 |
int32_t |
int32_t |
32 | 直接读取 | 同上 |
int64_t |
int64_t |
64 | 直接读取 | 同上 |
输出格式规范
格式: 逗号分隔的 C 字符串字面量列表,每个字面量包含:
- 前缀: "0x
- 内容: 大端十六进制(高位字节在前)
- 后缀: "
示例(2 个 float,宽度 64 位):
输入: 1.0, 2.0
输出: "0x400000003f800000"
|______||______|
2.0 1.0 (vec[1]=2.0 在高位,vec[0]=1.0 在低位)
多个批次用逗号换行分隔:
"0x400000003f800000",
"0x4040000040400000"
编译与运行
# 编译(假设已设置 Vitis 环境)
g++ -std=c++11 -I${XILINX_XRT}/include \
-I${XILINX_HLS}/include \
data_converter.cpp -o data_converter
# 运行帮助
./data_converter --help
# 典型调用:将 1000 个 float 转换为 128 位宽 hex 格式
./data_converter -t float -i input.txt -o output.hex -w 128 -n 1000
文档版本: 1.0