Vitis HLS 系列教程番外(1)- 三大编程范例

作者:阿白叔 发布时间: 2025-09-02 阅读量:11 评论数:0

引言

阅读UG1399的时候,我们会看到它罗列了三种编程范例。

而这个正是我们更好去理解高级编程语言和FPGA编程之间的区别。

Xilinx的HLS编程中,它主要有三大范例:生产者使用者范例、串流数据范例、流水线范例,我会在下文中详细罗列出UG1399对这部分的介绍,如果你已经看过了可以跳过这一部分。原文写得虽然逻辑严谨,但是内容冗长不易理解。

我在网上搜索了许多关于HLS编程的三大范例的,网上大多都是原文照搬,特别不负责任!于是我才想要展开这番外篇的内容。

这个番外篇是针对教程的理论补充,以便于大家的理解,不看的话也能完成教程里面的实验。

原文截图

生产者使用者

image_57.png

image_58.png

image_59.png

image_60.png

串流数据范例

image_56.png

image_61.png

image_62.png

流水线范例

image_63.png

image_64.png

image_65.png

生产者使用者范例

我们先抛弃掉复杂的概念,回归到生活中来,

场景:

一个工厂生产手机屏幕,分为两个步骤:

<1>生产者:切割玻璃(数据生成)。

<2>使用者:打磨玻璃(数据处理)。

<3>缓冲区:中间仓库(存储切割后的玻璃,等待打磨)。

问题:

如果生产者和使用者顺序执行,效率很低。例如:

- 切割完一片玻璃后,必须等打磨完成,才能开始下一片切割。

- 总耗时 = 切割时间 + 打磨时间 × 片数。

解决方案:

生产者-使用者模型,让两者并行工作:

<1>生产者切割玻璃后,直接放入中间仓库。

<2>使用者从仓库取玻璃打磨,无需等待生产者。

<3>两者独立运行,效率翻倍。

简单理解以后我们来通过代码的范例理解一串C语言的代码是如何表述这个场景,然后转化成HLS的开发逻辑的。

C语言实现(传统逻辑)

场景

% 生产者(CutGlass)切割玻璃,放入缓冲区。

% 消费者(Polish)从缓冲区取玻璃进行打磨。

% 缓冲区用数组模拟。

代码:

#include <stdio.h>
#include <string.h>

#define BUFFER_SIZE 10
#define NUM_GLASSES 20

void CutGlass(int buffer[BUFFER_SIZE], int *write_index) {
    for (int i = 0; i < NUM_GLASSES; i++) {
        while (*write_index == BUFFER_SIZE) {} // 等待缓冲区空
        buffer[*write_index] = i;              // 切割玻璃
        *write_index += 1;
        printf("Produced glass %d\n", i);
    }
}

void Polish(int buffer[BUFFER_SIZE], int *read_index) {
    for (int i = 0; i < NUM_GLASSES; i++) {
        while (*read_index == 0) {}             // 等待缓冲区有数据
        int glass = buffer[*read_index];        // 取出玻璃
        *read_index += 1;
        printf("Polished glass %d\n", glass);
    }
}

int main() {
    int buffer[BUFFER_SIZE];
    int write_index = 0, read_index = 0;

    // 顺序执行:先切割,再打磨
    CutGlass(buffer, &write_index);
    Polish(buffer, &read_index);

    return 0;
}

通过阅读代码我们可以发现出问题:

顺序执行:生产者必须等全部切割完成,消费者才能开始打磨,生产者和消费者无法同时运行。

缓冲区依赖:使用普通数组,需手动管理读/写索引,效率低。

修改后的HLS代码:

#include <hls_stream.h>

// 定义数据类型(假设每个玻璃数据为整数)
typedef int Glass;

// 生产者模块
void CutGlass(hls::stream<Glass> &output) {
    for (int i = 0; i < 20; i++) {
        output.write(i); // 将切割后的玻璃写入流
        printf("Produced glass %d\n", i);
    }
}

// 消费者模块
void Polish(hls::stream<Glass> &input) {
    for (int i = 0; i < 20; i++) {
        Glass glass = input.read(); // 从流中读取玻璃
        printf("Polished glass %d\n", glass);
    }
}

// 主函数(HLS入口)
void ScreenProduction() {
    #pragma HLS DATAFLOW // 允许生产者和消费者并行运行
    hls::stream<Glass> buffer; // 使用流代替数组

    CutGlass(buffer);      // 生产者
    Polish(buffer);        // 消费者

hls::stream<T>

使用该函数声明它是一个硬件级 FIFO(先进先出)缓冲区。

内部会自动管理指针,不需要像C语言一样维护read 和 write指针。

#pragma HLS DATAFLOW

在HLS中,DATAFLOW 指令告诉工具:这两个模块可以独立运行,只要数据流满足。也就是这两个可以同时跑。

串流数据范例

场景:

快递公司运输包裹:

  1. 快递员:收集包裹(数据生成)。

  2. 中转站:暂存包裹(数据缓冲)。

  3. 运输车:将包裹送至目的地(数据处理)。

问题:

如果一次性加载所有包裹到中转站,内存占用高,效率低。

解决方案:

串流(Streaming)

  1. 快递员将包裹按顺序放入中转站(hls::stream)。

  2. 运输车按需从中转站取包裹,无需一次性加载全部数据。

“生产者使用者范例”中已经有提到过hls::stream的使用,本小节再举一个例子以便大家更好理解

C语言实现(传统逻辑)

场景:

  • 快递员收集包裹(数据生成)。

  • 运输车将包裹送至目的地(数据处理)。

  • 用数组模拟缓冲区,手动管理读写索引。

代码:

#include <stdio.h>
#include <string.h>

#define BUFFER_SIZE 10
#define NUM_PACKAGES 20

// 快递员收集包裹
void Courier(int buffer[BUFFER_SIZE], int *write_index) {
    for (int i = 0; i < NUM_PACKAGES; i++) {
        while (*write_index == BUFFER_SIZE) {} // 等待缓冲区空
        buffer[*write_index] = i;              // 存放包裹
        *write_index += 1;
        printf("Collected package %d\n", i);
    }
}

// 运输车运输包裹
void Transport(int buffer[BUFFER_SIZE], int *read_index) {
    for (int i = 0; i < NUM_PACKAGES; i++) {
        while (*read_index == 0) {}            // 等待缓冲区有数据
        int package = buffer[*read_index];     // 取包裹
        *read_index += 1;
        printf("Transported package %d\n", package);
    }
}

int main() {
    int buffer[BUFFER_SIZE] = {0};             // 缓冲区
    int write_index = 0, read_index = 0;

    // 顺序执行:先收集包裹,再运输
    Courier(buffer, &write_index);
    Transport(buffer, &read_index);

    return 0;
}

这样写的话,顺序执行,十分依赖上一级是否有处理完。

修改后的HLS代码:

#include <hls_stream.h>

// 定义包裹数据类型
typedef int Package;

// 快递员模块
void Courier(hls::stream<Package> &output) {
    for (int i = 0; i < 20; i++) {
        output.write(i); // 放入包裹到流中
        printf("Collected package %d\n", i);
    }
}

// 运输车模块
void Transport(hls::stream<Package> &input) {
    for (int i = 0; i < 20; i++) {
        Package package = input.read(); // 从流中取包裹
        printf("Transported package %d\n", package);
    }
}

// 主函数(HLS入口)
void PackageTransport() {
    #pragma HLS DATAFLOW // 允许快递员和运输车并行运行
    hls::stream<Package> packageStream; // 使用流代替数组

    Courier(packageStream);   // 快递员模块
    Transport(packageStream); // 运输车模块
}

流水线范例

C语言实现(传统逻辑)

场景:

  • 对图像的每个像素依次进行 加法乘法阈值处理 三个步骤。

  • 顺序执行,无法并行。

代码:

#include <stdio.h>

#define IMG_SIZE 10

// 图像处理模块
void ImageProcessing(int input[IMG_SIZE], int output[IMG_SIZE]) {
    for (int i = 0; i < IMG_SIZE; i++) {
        int a = input[i] + 10;          // 阶段1:加法
        int b = a * 2;                  // 阶段2:乘法
        int c = (b > 128) ? 255 : 0;    // 阶段3:阈值处理
        output[i] = c;
        printf("Processed pixel %d\n", i);
    }
}

int main() {
    int input[IMG_SIZE]  = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90};
    int output[IMG_SIZE] = {0};

    ImageProcessing(input, output);

    return 0;
}

修改后的HLS代码:

#include <hls_stream.h>

// 定义像素数据类型
typedef int Pixel;

// 流水线模块(加法、乘法、阈值处理)
void ImagePipeline(hls::stream<Pixel> &input, hls::stream<Pixel> &output) {
    #pragma HLS PIPELINE II=1 // 启用流水线,每周期启动一个新像素
    Pixel a, b, c;

    while (!input.empty()) {
        a = input.read();               // 阶段1:加法
        b = a * 2;                      // 阶段2:乘法
        c = (b > 128) ? 255 : 0;        // 阶段3:阈值处理
        output.write(c);                // 输出结果
        printf("Processed pixel\n");
    }
}

// 主函数(HLS入口)
void ImageProcessing() {
    hls::stream<Pixel> inputStream, outputStream;

    // 填充输入流
    for (int i = 0; i < 10; i++) {
        inputStream.write(i * 10); // 模拟输入数据
    }

    ImagePipeline(inputStream, outputStream); // 调用流水线模块
}

#pragma HLS PIPELINE II=1

该指令将内部分为多个流水线,每周期启动一个新像素。

在传统C中,每次循环必须等所有步骤完成才能开始下一次循环。在HLS中,PIPELINE 指令告诉工具:将循环体拆分为多级流水线,每级独立运行。

总结

实际上我们在上面的例子也会看得出来,各个范例并不是对立关系。他们之间可以相互结合以求最大处理速率。

同时我们也能在C语言和HLS编程逻辑的对比中发现,FPGA的并行处理能力十分强大,传统的处理逻辑依托于CPU去一条条处理指令,尽管有系统去最大化的利用CPU的资源,在这种处理简单逻辑的运算中,一定数量的FPGA运算单元是能超过CPU 的效率的。本质上你的电脑需要GPU去处理图像的原因也正是如此,术业有专攻。而FPGA正是由于它的通用性,才能在运算性能不断升级的时代中依旧保有一席之地。

三者如何相互结合,如何在效率和资源占用上进行取舍,是需要我们长期积累经验,才能在开发前期比较清晰地判断出资源和效率见的平衡点的,不过相信你在看完这一篇后,能够对HLS开发和C语言的异同有了更深的理解。文章中的代码或许有不成熟的地方,欢迎大家评论区指正!

评论