2、HLS基础回顾:HLS开发流程、数据类型与精度、流水线与数据流、接口综合基础

好,咱们正式开始HLS SerDes接口设计实战的第二讲。

这一章我打算带大家快速回顾一下HLS的核心基础。别觉得基础就简单,我见过太多人栽在这些看似不起眼的地方。说白了,HLS不是写C语言,它是用C描述硬件。这个思维转换,很多人一开始没转过来。

2.1 HLS开发流程:从C到RTL

HLS的开发流程,其实就三步:写C、加约束、跑综合。但每一步都有坑。

我个人习惯把流程拆成五个阶段:

  1. 算法建模:用C/C++写出功能正确的模型。这一步不用考虑硬件,纯软件思维。
  2. 接口定义:告诉工具,你的数据从哪进、从哪出。是AXI Stream?还是FIFO?还是寄存器?
  3. 综合与优化:加流水线、加数组分割、加循环展开。工具会根据你的指令生成RTL。
  4. C/RTL协同仿真:跑一下看看RTL行为和C模型是否一致。这一步我建议必做,别偷懒。
  5. 导出IP:生成RTL代码或IP核,丢到Vivado里用。

核心要点:HLS不是自动把C翻译成好电路。它需要你告诉它「怎么做」,而不是「做什么」。

我在项目中遇到过一位同事,写了个很漂亮的C算法,综合出来资源爆炸。为什么?因为他用了大量的动态内存分配和递归。HLS不支持这些。嗯,这里要注意:HLS只支持固定大小、静态分配的数据结构。

C/C++建模 接口定义 综合优化 C/RTL仿真 导出IP 迭代优化

2.2 数据类型与精度:别让精度坑了你

HLS里数据类型的选择,直接决定了你的资源消耗和性能。你想想看,用int和用ap_uint<4>,综合出来的电路能一样吗?

常用的数据类型就这几类:

类型 说明 资源开销
char/short/int/long 标准C类型,位宽固定 较高(对齐到8/16/32/64位)
ap_int<N> 任意位宽有符号整数 按需分配,节省资源
ap_uint<N> 任意位宽无符号整数 最省资源,推荐使用
ap_fixed<W,I> 定点数,W总位宽,I整数位 比浮点省很多
float/double 浮点数 资源巨大,慎用

我的经验:做SerDes接口时,数据位宽通常和SerDes的通道位宽对齐。比如8B/10B编码,一个符号10位,用ap_uint<10>最合适。别用int,那会浪费6位宽的LUT。

我曾经在一个项目中,用了float做均衡器系数计算。综合出来一看,一个乘法器占了2000多个LUT。后来改成ap_fixed<16,8>,LUT降到400,精度损失不到1dB。嗯,这就是教训。

2.3 流水线与数据流:吞吐量的关键

流水线(Pipelining)和数据流(Dataflow)是HLS里最常用的两个优化手段。但很多人搞混它们。

流水线是针对单个循环或函数的。它让循环的每次迭代可以重叠执行。比如一个循环有3级操作,不加流水线要等上一轮全部做完才做下一轮。加了流水线,第2轮可以在第1轮的第2级开始时就开始。

// 不加流水线:3个时钟周期处理1个数据
for (int i = 0; i < N; i++) {
    a[i] = b[i] + c[i];  // 1级
    d[i] = a[i] * 2;     // 2级
    out[i] = d[i] >> 4;  // 3级
}

// 加流水线:每个时钟周期输出1个数据
#pragma HLS PIPELINE II=1
for (int i = 0; i < N; i++) {
    a[i] = b[i] + c[i];
    d[i] = a[i] * 2;
    out[i] = d[i] >> 4;
}

数据流是针对函数间或循环间的。它让不同函数/循环可以并行执行,像流水线一样传递数据。说白了,就是生产者-消费者模型。

注意:数据流要求数据单向流动,不能有反馈。如果有反馈路径,工具会报错。我曾经在一个SerDes的CDR(时钟数据恢复)模块里用了数据流,结果因为反馈环路导致综合失败。后来改成乒乓Buffer才解决。

2.4 接口综合基础:告诉数据怎么走

接口综合,就是告诉HLS工具,你的模块和外界怎么通信。SerDes接口设计里,最常用的就是AXI Stream和FIFO接口。

常见的接口类型:

  • ap_none:无协议,直接连线。适合简单控制信号。
  • ap_vld:带有效信号。数据有效时拉高vld。
  • ap_ack:带应答信号。对方收到后拉高ack。
  • ap_hs:握手协议。vld和ack都有,最可靠。
  • axis:AXI Stream协议。SerDes接口的标配。
  • ap_fifo:FIFO接口。适合跨时钟域。

我个人习惯,SerDes的收发通道一律用axis接口。为什么?因为它有TLAST信号,可以标记包的结束。这在SerDes的帧同步里太重要了。

// AXI Stream接口示例
void serdes_tx(
    hls::stream<ap_axis<32,1,1,1> >& tx_axis,  // 输入
    ap_uint<1> serial_data[4]                     // 输出
) {
#pragma HLS INTERFACE axis port=tx_axis
#pragma HLS INTERFACE ap_none port=serial_data
    // ... 处理逻辑
}

避坑指南:我曾经在定义AXI Stream时,忘了指定TUSER和TKEEP的位宽。结果综合出来的接口和IP核不匹配,联调时花了三天才找到问题。记住:ap_axis<W, U, TI, TD>四个参数,W是数据位宽,U是TUSER位宽,TI是TID位宽,TD是TDEST位宽。SerDes场景下,U和TD通常设为1就够了。

好了,这一章的基础回顾就到这里。这些内容看起来简单,但都是后面做SerDes接口设计的基石。你想想看,如果数据类型选错了,流水线加错了地方,接口协议没配对,后面再优化也白搭。

记住一句话:HLS不是魔法,它只是把你写的C翻译成电路。你写什么样的C,它就给你什么样的电路。

下一章,我们会正式进入SerDes接口的核心——看看怎么用HLS实现高速串行数据的收发。到时候会用到今天讲的所有基础,尤其是AXI Stream和流水线。

可可精品资料库