HLS数据通信机制详解:共享数组、hls::stream、乒乓缓存pingpong等
引言
在高层次综合(HLS)设计中,选择合适的数据通信和存储机制对于实现高性能硬件加速器至关重要。不同的通信机制在性能、资源使用和编程复杂度上各有优劣,理解它们的特点可以帮助开发者根据具体应用需求做出最佳选择。本文将详细介绍HLS中常用的几种数据通信机制,帮助您在设计中做出明智的选择。
1. 共享数组(Shared Arrays)
概念
共享数组是HLS中最基本、最直观的数据通信方式,即多个函数或处理阶段访问同一个数组变量。在硬件实现中,共享数组通常被映射为片上内存资源(如BRAM、LUTRAM或寄存器)。
工作原理与代码示例
void process_data(int input[1024], int output[1024]) {
int shared_buffer[1024];
// 阶段1:填充共享缓冲区
for(int i = 0; i < 1024; i++) {
shared_buffer[i] = input[i] * 2;
}
// 阶段2:处理共享缓冲区的数据
for(int i = 0; i < 1024; i++) {
output[i] = shared_buffer[i] + 10;
}
}
在DATAFLOW优化环境中使用共享数组:
void stage1(int input[1024], int shared_buffer[1024]) {
for (int i = 0; i < 1024; i++) {
#pragma HLS PIPELINE II=1
shared_buffer[i] = input[i] * 2;
}
}
void stage2(int shared_buffer[1024], int output[1024]) {
for (int i = 0; i < 1024; i++) {
#pragma HLS PIPELINE II=1
output[i] = shared_buffer[i] + 10;
}
}
void top_function(int input[1024], int output[1024]) {
int shared_buffer[1024];
#pragma HLS DATAFLOW
stage1(input, shared_buffer);
stage2(shared_buffer, output);
}
优点
- 简单直观:符合传统软件编程思维,易于理解和调试
- 随机访问:支持任意顺序的读写操作,适合需要非顺序访问的算法
- 易于实现:无需特殊库或复杂语法,直接使用C/C++数组
- 适合复杂数据结构:可以存储结构体、类等复杂数据类型
缺点
- 并行性受限:不同阶段必须等待前一阶段完全完成才能开始,难以实现真正的并行
- DATAFLOW优化受限:在DATAFLOW区域内使用共享数组通常会导致HLS工具发出警告,可能无法达到预期的并行效果
- 资源消耗大:需要完整存储所有中间数据,占用大量内存资源
- 访问延迟高:特别是对于大型数组,访问BRAM会引入延迟
- 需要显式同步:为避免数据竞争,需要确保写操作完成后再进行读操作
适用场景
- 简单算法:不需要高度并行化的简单处理流程
- 随机访问模式:需要非顺序访问数据的算法
- 原型开发:快速验证算法正确性的初始实现
- 小型数据集:处理少量数据,内存资源不是瓶颈
优化技巧
数组分区:使用
#pragma HLS ARRAY_PARTITION
将数组分割到多个存储单元,提高并行访问能力int buffer[1024]; #pragma HLS ARRAY_PARTITION variable=buffer cyclic factor=4 dim=1
资源指定:使用
#pragma HLS RESOURCE
指定特定类型的存储资源int buffer[1024]; #pragma HLS RESOURCE variable=buffer core=RAM_2P_BRAM
转换为流或乒乓缓冲:在DATAFLOW区域内,考虑将共享数组转换为hls::stream或乒乓缓冲
2. hls stream 流
概念
hls stream
是Vitis HLS提供的一种高级数据流抽象,用于模拟硬件中的FIFO(先进先出)缓冲区。它是实现任务级并行和流水线处理的理想选择,特别适合DATAFLOW优化。
工作原理与代码示例
#include "hls_stream.h"
void process_data(int input[1024], int output[1024]) {
hls::stream<int> fifo;
#pragma HLS STREAM variable=fifo depth=16
#pragma HLS DATAFLOW
// 阶段1:生产者
for(int i = 0; i < 1024; i++) {
#pragma HLS PIPELINE II=1
fifo.write(input[i] * 2);
}
// 阶段2:消费者
for(int i = 0; i < 1024; i++) {
#pragma HLS PIPELINE II=1
output[i] = fifo.read() + 10;
}
}
使用函数模块化的DATAFLOW设计:
#include "hls_stream.h"
void stage1(int input[1024], hls::stream<int>& stream) {
for (int i = 0; i < 1024; i++) {
#pragma HLS PIPELINE II=1
stream.write(input[i] * 2);
}
}
void stage2(hls::stream<int>& stream, int output[1024]) {
for (int i = 0; i < 1024; i++) {
#pragma HLS PIPELINE II=1
output[i] = stream.read() + 10;
}
}
void top_function(int input[1024], int output[1024]) {
hls::stream<int> stream;
#pragma HLS STREAM variable=stream depth=16
#pragma HLS DATAFLOW
stage1(input, stream);
stage2(stream, output);
}
优点
- 高效并行:支持生产者-消费者模型,实现真正的流水线并行,显著提高吞吐量
- 隐式同步:读写操作自动阻塞,提供内置的同步机制,无需手动添加同步逻辑
- 资源高效:只需要小型FIFO缓冲区,而非完整数组,大幅减少内存使用
- DATAFLOW友好:是DATAFLOW优化的理想数据传输机制,HLS工具可以高效地将其映射到硬件
- 解耦计算阶段:将不同的计算阶段解耦,降低了模块间的依赖性,提高代码可维护性
缺点
- 仅支持顺序访问:只能按FIFO顺序读写,不支持随机访问,不适合需要随机访问的算法
- 缓冲区深度管理:需要谨慎设置缓冲区深度,过小可能导致死锁,过大会浪费资源
- 调试复杂性:流行为在仿真和硬件中可能有差异,调试相对复杂
- 学习曲线:对于不熟悉数据流编程的开发者,需要一定的学习成本
适用场景
- 流式处理:数据按顺序处理的应用,如音频、视频、图像处理
- DATAFLOW优化:任务级并行的设计,特别是在DATAFLOW区域内
- 生产者-消费者模型:一个模块产生数据,另一个模块消费数据
- 高吞吐量要求:需要最大化系统吞吐量的应用
优化技巧
设置适当的缓冲区深度:通过
depth
参数调整FIFO大小,平衡资源使用和性能hls::stream<int> fifo; #pragma HLS STREAM variable=fifo depth=32
多级流水线:创建多级流水线,进一步提高并行度
hls::stream<int> stream1, stream2; #pragma HLS DATAFLOW stage1(input, stream1); stage2(stream1, stream2); stage3(stream2, output);
数据批处理:考虑批量读写数据,减少流操作的开销
typedef struct { int data[16]; // 一次处理16个元素 } batch_t; hls::stream<batch_t> stream;
避免空读和满写:使用
empty()
和full()
方法检查流状态,避免阻塞操作
3. 乒乓缓冲区(Ping-Pong Buffers)
概念
乒乓缓冲区是一种使用两个交替缓冲区的技术,一个用于当前操作,另一个用于下一次操作。这种方式允许读取和写入操作并行进行,提高数据处理效率。
工作原理与代码示例
void process_data(int input[1024], int output[1024]) {
int ping_pong[2][512];
#pragma HLS ARRAY_PARTITION variable=ping_pong complete dim=1
// 第一次迭代:填充第一个缓冲区
for(int i = 0; i < 512; i++) {
ping_pong[0][i] = input[i];
}
// 主处理循环
for(int iter = 0; iter < 1; iter++) {
int read_buffer = iter % 2;
int write_buffer = (iter + 1) % 2;
// 处理当前缓冲区的数据,同时填充另一个缓冲区
for(int i = 0; i < 512; i++) {
#pragma HLS PIPELINE II=1
// 处理当前缓冲区
output[iter*512 + i] = ping_pong[read_buffer][i] * 2;
// 准备下一个缓冲区
if(iter < 1) {
ping_pong[write_buffer][i] = input[(iter+1)*512 + i];
}
}
}
}
在DATAFLOW中使用乒乓缓冲区:
void stage1(int input[512], int buffer[512]) {
for (int i = 0; i < 512; i++) {
#pragma HLS PIPELINE II=1
buffer[i] = input[i] * 2;
}
}
void stage2(int buffer[512], int output[512]) {
for (int i = 0; i < 512; i++) {
#pragma HLS PIPELINE II=1
output[i] = buffer[i] + 10;
}
}
void top_function(int input[1024], int output[1024]) {
int ping_pong[2][512];
#pragma HLS ARRAY_PARTITION variable=ping_pong complete dim=1
#pragma HLS DATAFLOW
// 第一批数据
stage1(input, ping_pong[0]);
stage2(ping_pong[1], output);
// 第二批数据
stage1(input+512, ping_pong[1]);
stage2(ping_pong[0], output+512);
}
优点
- 支持并行操作:允许一个阶段处理当前数据,同时另一个阶段准备下一批数据
- 支持随机访问:每个缓冲区内部支持随机访问,兼顾了并行性和随机访问能力
- 适合迭代算法:特别适合需要多次迭代的算法
- 明确的控制流:相比流,控制流更加明确,行为更加确定和可预测
缺点
- 资源使用增加:需要两倍的存储空间
- 实现复杂:需要手动管理缓冲区切换逻辑,增加了代码复杂性
- 有限的并行度:只支持两个阶段的重叠,不如流那样灵活
- 同步复杂:需要精心设计同步机制,避免数据冲突
适用场景
- 迭代算法:需要多次迭代处理的算法
- 图像处理:一次处理一行或一块图像数据
- 需要随机访问的并行处理:既需要并行处理,又需要随机访问数据的场景
- 双缓冲视频处理:一个缓冲区显示,另一个缓冲区填充
优化技巧
完全分区第一维:确保两个缓冲区可以并行访问
int ping_pong[2][1024]; #pragma HLS ARRAY_PARTITION variable=ping_pong complete dim=1
循环流水线:对访问乒乓缓冲区的循环进行流水线优化
for(int i = 0; i < 1024; i++) { #pragma HLS PIPELINE II=1 // 访问乒乓缓冲区 }
考虑多重缓冲:某些情况下,三重或四重缓冲可能更有效
int triple_buffer[3][1024]; // 三重缓冲 #pragma HLS ARRAY_PARTITION variable=triple_buffer complete dim=1
4. 行缓冲区(Line Buffers)
概念
行缓冲区是一种特殊的缓冲技术,常用于图像处理中存储几行图像数据,以支持窗口操作(如卷积)。它允许在处理当前像素时访问其邻域像素。
工作原理与代码示例
void line_buffer_filter(pixel_t input[HEIGHT][WIDTH], pixel_t output[HEIGHT-2][WIDTH-2]) {
// 定义3行缓冲区
pixel_t line_buffer[3][WIDTH];
#pragma HLS ARRAY_PARTITION variable=line_buffer complete dim=1
// 定义3x3滑动窗口
pixel_t window[3][3];
#pragma HLS ARRAY_PARTITION variable=window complete dim=0
// 初始化行缓冲区
for(int col = 0; col < WIDTH; col++) {
line_buffer[0][col] = 0;
line_buffer[1][col] = 0;
line_buffer[2][col] = 0;
}
// 主处理循环
for(int row = 0; row < HEIGHT; row++) {
for(int col = 0; col < WIDTH; col++) {
#pragma HLS PIPELINE II=1
// 移动数据:向上移动两行
for(int i = 0; i < 2; i++) {
line_buffer[i][col] = line_buffer[i+1][col];
}
// 读取新的一行
if(row < HEIGHT) {
line_buffer[2][col] = input[row][col];
} else {
line_buffer[2][col] = 0;
}
// 更新窗口
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 2; j++) {
window[i][j] = window[i][j+1];
}
}
// 从行缓冲区填充窗口的最后一列
for(int i = 0; i < 3; i++) {
window[i][2] = line_buffer[i][col];
}
// 应用3x3卷积
if(row >= 2 && col >= 2) {
pixel_t result = apply_filter(window);
output[row-2][col-2] = result;
}
}
}
}
优点
- 高效的滑动窗口操作:非常适合卷积、滤波等需要邻域数据的操作
- 内存使用高效:只存储必要的几行数据,而非整个图像
- 高吞吐量:支持每个时钟周期处理一个像素
- 支持流式处理:可以与流式输入/输出结合使用
缺点
- 专用于特定应用:主要适用于图像处理等规则网格数据
- 实现复杂:需要精心设计数据移动逻辑
- 有限的应用范围:不适合通用数据处理
适用场景
- 图像滤波:卷积、中值滤波、高斯滤波等
- 视频处理:帧间处理、运动估计等
- 网格计算:任何需要邻域数据的规则网格计算
优化技巧
数组分区:对行缓冲区和窗口进行完全分区
#pragma HLS ARRAY_PARTITION variable=line_buffer complete dim=1 #pragma HLS ARRAY_PARTITION variable=window complete dim=0
循环流水线:对主处理循环进行流水线优化
#pragma HLS PIPELINE II=1
结合hls::stream使用:使用流进行数据输入和输出,创建完整的流水线
void line_buffer_filter(hls::stream<pixel_t>& input_stream, hls::stream<pixel_t>& output_stream, int height, int width) { // 行缓冲区实现 }
5. 窗口缓冲区(Window Buffers)
概念
窗口缓冲区是行缓冲区的扩展,直接维护一个滑动窗口。在HLS中,可以使用hls::Window
类来简化窗口操作的实现。
工作原理与代码示例
#include "hls_video.h"
void window_filter(pixel_t input[HEIGHT][WIDTH], pixel_t output[HEIGHT-2][WIDTH-2]) {
// 定义3x3窗口缓冲区
hls::Window<3, 3, pixel_t> window;
// 行缓冲区
hls::LineBuffer<3, WIDTH, pixel_t> line_buffer;
// 主处理循环
for(int row = 0; row < HEIGHT; row++) {
for(int col = 0; col < WIDTH; col++) {
#pragma HLS PIPELINE II=1
// 读取新像素
pixel_t new_pixel = input[row][col];
// 更新行缓冲区和窗口
line_buffer.shift_pixels_up(col);
line_buffer.insert_bottom_row(new_pixel, col);
// 将行缓冲区数据复制到窗口
for(int i = 0; i < 3; i++) {
window.shift_pixels_left();
window.insert_pixel(line_buffer.getval(i, col), i, 2);
}
// 应用滤波器
if(row >= 2 && col >= 2) {
pixel_t result = apply_filter(window);
output[row-2][col-2] = result;
}
}
}
}
优点
- 高级抽象:提供更高级的接口,简化代码
- 内置优化:HLS库中的实现已经包含了许多优化
- 易于使用:API简单直观
- 专为图像处理优化:针对常见图像处理操作进行了优化
缺点
- 灵活性有限:难以适应非标准窗口大小或特殊处理需求
- 依赖特定库:需要使用HLS视频库
- 可能不如手动实现高效:对于特定应用,手动优化可能获得更好性能
适用场景
- 标准图像滤波:使用标准窗口大小的卷积操作
- 快速原型开发:需要快速实现图像处理算法
- 视频处理流水线:作为视频处理流水线的一部分
优化技巧
使用正确的窗口大小:确保窗口大小与算法需求匹配
hls::Window<5, 5, pixel_t> window; // 5x5窗口
考虑行缓冲区深度:根据图像宽度调整行缓冲区大小
hls::LineBuffer<3, MAX_WIDTH, pixel_t> line_buffer;
结合流使用:将窗口缓冲区与hls::stream结合使用,创建完整流水线
hls::stream<pixel_t> input_stream, output_stream;
6. 其他数据通信机制
6.1 循环缓冲区(Circular Buffers)
循环缓冲区是一种使用单一连续的内存块以循环方式存储数据的结构,特别适合于流式数据处理。它的核心特性是"环形"存储,当指针到达缓冲区末尾时,会自动回绕到缓冲区的开头。
工作原理与代码示例
循环缓冲区通常通过两个指针来管理:一个用于写入数据(写指针),另一个用于读取数据(读指针)。这两个指针以循环方式移动,当到达缓冲区末尾时回到开头。
void process_with_circular_buffer(data_t input[1024], data_t output[1024]) {
const int buffer_size = 256;
data_t buffer[buffer_size];
int read_ptr = 0, write_ptr = 0;
int input_idx = 0, output_idx = 0;
// 初始预填充
for (int i = 0; i < buffer_size/2 && input_idx < 1024; i++) {
buffer[write_ptr] = input[input_idx++];
write_ptr = (write_ptr + 1) % buffer_size;
}
// 处理循环
while (output_idx < 1024) {
// 读取并处理数据
if (read_ptr != write_ptr) {
data_t value = buffer[read_ptr];
read_ptr = (read_ptr + 1) % buffer_size;
// 处理数据
output[output_idx++] = value * 2;
}
// 填充更多数据
if ((write_ptr + 1) % buffer_size != read_ptr && input_idx < 1024) {
buffer[write_ptr] = input[input_idx++];
write_ptr = (write_ptr + 1) % buffer_size;
}
}
}
优点
- 内存高效:使用固定大小的缓冲区,无需完整存储所有数据
- 适合流式处理:非常适合连续的数据流处理
- 支持生产者-消费者模型:允许同时读写操作
- 无需数据移动:只需更新指针,避免了数据移动的开销
缺点
- 实现复杂:需要手动管理读写指针和边界条件
- 难以并行化:指针依赖性使得并行访问变得复杂
- 随机访问受限:虽然支持一定程度的随机访问,但不如普通数组灵活
- 综合结果可能次优:HLS工具可能无法完全优化循环缓冲区
适用场景
- 音频处理:处理连续的音频数据流
- 数据流过滤:需要缓存部分历史数据的过滤操作
- 有限资源环境:内存资源有限的情况下处理大量数据
6.2 双端口RAM(Dual-Port RAM)
双端口RAM允许同时进行读写操作,可以提高数据访问的并行性。在HLS中,可以通过特定的资源指令将数组映射为双端口RAM。
工作原理与代码示例
void process_with_dual_port_ram(data_t input[1024], data_t output[1024]) {
data_t ram[1024];
#pragma HLS RESOURCE variable=ram core=RAM_2P
for (int i = 0; i < 1024; ++i) {
#pragma HLS PIPELINE II=1
ram[i] = input[i] * 2;
output[i] = ram[i] + 10;
}
}
在这个例子中,我们使用#pragma HLS RESOURCE
指令将ram
数组映射为双端口RAM。这使得我们可以在同一个循环迭代中同时写入和读取数据,提高吞吐量。
优点
- 并行访问:支持同时读写,提高数据访问并行性
- 简化流水线:简化了需要同时读写的流水线设计
- 减少循环次数:可以合并多个循环,减少控制开销
- 简单实现:实现相对简单,不需要复杂的同步机制
缺点
- 资源消耗:双端口RAM比单端口RAM消耗更多资源
- 端口数量有限:通常仅限于两个端口,限制了并行度
- 可能的时序问题:在高频设计中可能面临时序挑战
适用场景
- 同时读写:需要在同一周期内读写数据的应用
- 流水线优化:需要提高循环流水线效率的场景
- 数据预处理:需要在一次迭代中同时读取输入和写入输出
6.3 PIPO(Ping-Pong I/O)缓冲区
PIPO是乒乓缓冲区的一种特殊形式,专门用于HLS DATAFLOW优化中。当HLS编译器检测到在DATAFLOW区域内使用共享数组时,可能会自动将其转换为PIPO缓冲区。
工作原理与代码示例
void top_function(data_t input[1024], data_t output[1024]) {
data_t buffer[1024];
#pragma HLS DATAFLOW
// HLS可能自动将buffer转换为PIPO
stage1(input, buffer);
stage2(buffer, output);
}
工作原理
PIPO缓冲区在硬件实现中使用两个独立的内存块,并添加控制逻辑来协调访问:
- 当第一个函数完成对整个缓冲区的写入后,发出完成信号
- 第二个函数收到信号后,开始从缓冲区读取数据
- 同时,第一个函数可以开始处理下一批数据
这种机制允许在DATAFLOW中使用共享数组,同时保持一定程度的并行性。
与手动乒乓缓冲区的区别
- 自动生成:PIPO由HLS工具自动生成,无需手动实现缓冲区切换逻辑
- 使用简单:使用方式与普通共享数组相同
- 控制逻辑:HLS会自动生成必要的控制逻辑,确保数据完整性
- 优化程度:可能不如手动实现的乒乓缓冲区高效
使用注意事项
- 依赖HLS工具的自动转换,可能在不同版本的工具中行为不同
- 通常会收到警告信息,建议使用流或显式乒乓缓冲区
- 性能可能不如显式使用hls::stream
- 不适合需要细粒度并行的场景
6.4 寄存器链(Register Chains)
寄存器链是一种将数据通过级联的寄存器传递的方式,常用于实现延迟线或移位寄存器。
工作原理与代码示例
void register_chain(data_t input[1024], data_t output[1024]) {
// 5级寄存器链
data_t reg1, reg2, reg3, reg4, reg5;
for (int i = 0; i < 1024; i++) {
#pragma HLS PIPELINE II=1
// 移位操作
reg5 = reg4;
reg4 = reg3;
reg3 = reg2;
reg2 = reg1;
reg1 = input[i];
// 输出是5个周期前的输入
if (i >= 4) {
output[i-4] = reg5;
}
}
}
优点
- 低延迟:寄存器访问非常快
- 高吞吐量:每个时钟周期可以处理一个数据
- 完全流水线化:自然支持流水线操作
- 资源高效:对于小型延迟线,使用寄存器比内存更高效
- 确定性延迟:提供精确的延迟周期
缺点
- 扩展性差:不适合长延迟线,寄存器资源有限
- 资源消耗随长度增加:长度与资源使用成正比
- 仅适合顺序访问:不支持随机访问
- 有限的应用场景:主要用于实现短延迟或移位操作
适用场景
- FIR滤波器:实现抽头延迟线
- 短序列检测:检测特定的短数据序列
- 时序对齐:对不同信号路径进行延迟匹配
- 流水线平衡:平衡不同流水线阶段的延迟
6.5 分布式RAM(Distributed RAM)
分布式RAM是使用FPGA的LUT资源实现的小型、低延迟内存,适用于需要快速访问的小型数据结构。
工作原理与代码示例
void distributed_ram_example(data_t input[1024], data_t output[1024]) {
data_t small_buffer[64];
#pragma HLS RESOURCE variable=small_buffer core=RAM_1P_LUTRAM
// 使用分布式RAM实现的小缓冲区
for (int i = 0; i < 1024; i++) {
#pragma HLS PIPELINE II=1
int idx = i % 64;
data_t value = small_buffer[idx];
small_buffer[idx] = input[i];
output[i] = value;
}
}
优点
- 低延迟:比块RAM访问更快
- 单周期访问:支持每周期一次读写操作
- 高带宽:可以实现多端口访问
- 适合小型缓冲区:非常适合小型查找表或缓冲区
缺点
- 容量有限:只适合小型数据结构(通常几十到几百个元素)
- 资源使用率高:占用LUT资源,可能影响逻辑实现
- 扩展性差:难以实现大型存储
- 可能影响时序:大型分布式RAM可能导致时序问题
适用场景
- 小型查找表:存储常数、系数或小型映射表
- 小型FIFO:需要低延迟的小型FIFO
- 寄存器文件:处理器设计中的寄存器组
- 需要多端口访问的小型存储
6.6 AXI接口缓冲区
AXI接口缓冲区用于HLS设计与外部系统(如处理器或其他IP核)之间的高效数据传输。
工作原理与代码示例
void axi_buffer_example(
data_t *input, // AXI主接口
data_t *output, // AXI主接口
int size
) {
#pragma HLS INTERFACE m_axi port=input offset=slave bundle=gmem0
#pragma HLS INTERFACE m_axi port=output offset=slave bundle=gmem1
#pragma HLS INTERFACE s_axilite port=size bundle=control
#pragma HLS INTERFACE s_axilite port=return bundle=control
data_t local_buffer[1024];
// 读取输入数据
for (int i = 0; i < size; i++) {
#pragma HLS PIPELINE II=1
local_buffer[i] = input[i];
}
// 处理数据
for (int i = 0; i < size; i++) {
#pragma HLS PIPELINE II=1
local_buffer[i] = local_buffer[i] * 2 + 10;
}
// 写回输出数据
for (int i = 0; i < size; i++) {
#pragma HLS PIPELINE II=1
output[i] = local_buffer[i];
}
}
优点
- 高带宽:支持突发传输,提高内存访问效率
- 标准接口:与ARM处理器和其他IP核兼容
- 缓存友好:支持数据缓存和预取
- 灵活的数据宽度:支持不同的数据位宽
缺点
- 额外延迟:AXI协议引入额外的延迟
- 复杂的控制逻辑:需要处理握手协议和突发传输
- 资源开销:AXI接口需要额外的硬件资源
- 可能的性能瓶颈:如果访问模式不佳,可能导致性能下降
适用场景
- 处理器协处理器:HLS加速器作为处理器的协处理器
- 大数据集处理:需要处理存储在外部内存中的大型数据集
- 系统集成:与其他使用AXI接口的IP核集成
- DMA传输:高效的直接内存访问
6.7 TLAST信号(AXI-Stream协议)
在使用AXI-Stream接口时,TLAST信号用于指示数据流的结束,是流处理中的一个重要控制机制。
工作原理与代码示例
typedef struct {
data_t data;
bool last;
} axis_t;
void process_axi_stream(hls::stream<axis_t>& input, hls::stream<axis_t>& output) {
axis_t data_in, data_out;
while (true) {
data_in = input.read();
data_out.data = data_in.data * 2;
data_out.last = data_in.last;
output.write(data_out);
if (data_in.last)
break;
}
}
优点
- 流边界控制:明确指示流数据的结束
- 可变长度数据:支持处理可变长度的数据流
- 标准协议:符合AXI-Stream标准,便于与其他IP核集成
缺点
- 额外开销:需要额外的信号线和处理逻辑
- 实现复杂性:需要正确处理控制信号
适用场景
- 可变长度数据流:处理长度不固定的数据包
- 流协议集成:与使用AXI-Stream协议的其他IP核集成
- 视频处理:标记帧边界
7. 选择合适的数据通信机制
7.1 决策指南
选择合适的数据通信机制应考虑以下因素:
数据访问模式
- 顺序访问:考虑hls::stream或FIFO
- 随机访问:考虑共享数组或乒乓缓冲区
- 窗口访问:考虑行缓冲区或窗口缓冲区
数据量
- 小数据量:考虑寄存器链或分布式RAM
- 中等数据量:考虑BRAM实现的共享数组或乒乓缓冲区
- 大数据量:考虑AXI接口与外部内存
并行要求
- 高吞吐量:优先考虑hls::stream和DATAFLOW
- 低延迟:考虑分布式RAM或寄存器实现
- 任务级并行:使用hls::stream或乒乓缓冲区
应用特性
- 图像处理:考虑行缓冲区或窗口缓冲区
- 流媒体处理:考虑hls::stream或循环缓冲区
- 迭代算法:考虑乒乓缓冲区
7.2 各机制的比较表
数据通信机制 | 随机访问 | 并行性 | 资源使用 | 实现复杂度 | 适用场景 |
---|---|---|---|---|---|
共享数组 | 高 | 低 | 高 | 低 | 简单算法,随机访问 |
hls::stream | 无 | 高 | 低 | 中 | DATAFLOW,流式处理 |
乒乓缓冲区 | 高 | 中 | 中 | 高 | 迭代算法,需要随机访问的并行处理 |
行缓冲区 | 有限 | 高 | 中 | 高 | 图像处理,窗口操作 |
窗口缓冲区 | 有限 | 高 | 中 | 中 | 标准图像滤波,快速原型 |
循环缓冲区 | 有限 | 中 | 低 | 高 | 音频处理,数据流过滤 |
双端口RAM | 高 | 中 | 中 | 低 | 同时读写,流水线优化 |
寄存器链 | 无 | 高 | 与长度成正比 | 低 | 短延迟线,FIR滤波器 |
分布式RAM | 高 | 高 | 高(LUT) | 低 | 小型查找表,多端口访问 |
AXI缓冲区 | 高 | 中 | 高 | 高 | 处理器协处理器,大数据集 |
8. 混合使用不同的数据通信机制
在实际应用中,通常需要混合使用不同的数据通信机制,以充分发挥各自的优势。以下是一个图像处理流水线的示例,展示了如何结合多种机制。
图像处理流水线示例
#include "hls_stream.h"
#include "hls_video.h"
typedef unsigned char pixel_t;
void image_processing_pipeline(
pixel_t input[HEIGHT][WIDTH],
pixel_t output[HEIGHT][WIDTH]
) {
#pragma HLS INTERFACE m_axi port=input offset=slave bundle=gmem0
#pragma HLS INTERFACE m_axi port=output offset=slave bundle=gmem1
#pragma HLS INTERFACE s_axilite port=return bundle=control
// 使用流在处理阶段之间传递数据
hls::stream<pixel_t> stream1, stream2, stream3;
#pragma HLS DATAFLOW
// 阶段1:从外部内存读取数据到流
read_image(input, stream1);
// 阶段2:使用行缓冲区进行卷积
convolve_image(stream1, stream2);
// 阶段3:使用查找表进行颜色映射
color_map(stream2, stream3);
// 阶段4:将处理后的数据写回外部内存
write_image(stream3, output);
}
// 从外部内存读取数据
void read_image(pixel_t input[HEIGHT][WIDTH], hls::stream<pixel_t>& output) {
for (int row = 0; row < HEIGHT; row++) {
for (int col = 0; col < WIDTH; col++) {
#pragma HLS PIPELINE II=1
output.write(input[row][col]);
}
}
}
// 使用行缓冲区进行卷积
void convolve_image(hls::stream<pixel_t>& input, hls::stream<pixel_t>& output) {
// 使用行缓冲区存储多行数据
hls::LineBuffer<3, WIDTH, pixel_t> line_buffer;
// 使用窗口缓冲区存储当前处理窗口
hls::Window<3, 3, pixel_t> window;
// 卷积核系数存储在分布式RAM中
int kernel[3][3] = {{1, 2, 1}, {2, 4, 2}, {1, 2, 1}};
#pragma HLS ARRAY_PARTITION variable=kernel complete dim=0
// 处理图像
for (int row = 0; row < HEIGHT+1; row++) {
for (int col = 0; col < WIDTH+1; col++) {
#pragma HLS PIPELINE II=1
// 读取新像素
pixel_t new_pixel = (row < HEIGHT && col < WIDTH) ? input.read() : 0;
// 更新行缓冲区和窗口
if (col < WIDTH) {
line_buffer.shift_pixels_up(col);
line_buffer.insert_bottom_row(new_pixel, col);
}
if (col < WIDTH && row < HEIGHT) {
for (int i = 0; i < 3; i++) {
window.shift_pixels_left(i);
window.insert_pixel(line_buffer.getval(i, col), i, 2);
}
}
// 应用卷积
if (row >= 2 && col >= 2) {
int sum = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
sum += window.getval(i, j) * kernel[i][j];
}
}
sum = sum / 16; // 归一化
output.write((pixel_t)sum);
}
}
}
}
// 使用查找表进行颜色映射
void color_map(hls::stream<pixel_t>& input, hls::stream<pixel_t>& output) {
// 使用分布式RAM实现查找表
pixel_t lut[256];
#pragma HLS RESOURCE variable=lut core=RAM_1P_LUTRAM
// 初始化查找表
for (int i = 0; i < 256; i++) {
lut[i] = custom_mapping(i);
}
// 应用颜色映射
for (int row = 0; row < HEIGHT-2; row++) {
for (int col = 0; col < WIDTH-2; col++) {
#pragma HLS PIPELINE II=1
pixel_t pixel = input.read();
output.write(lut[pixel]);
}
}
}