拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FPGA 中的 for 不是“循环执行”:Verilog for 语句的综合原理、正确写法与常见坑

前言本文从综合角度讲清 FPGA 设计中for语句的本质区分always块中的过程for与generate for结合组合逻辑、移位寄存器、数组复位等代码说明正确写法并重点分析非阻塞赋值累加、动态循环边界、资源暴涨、时序变差和 BRAM 无法正常推断等常见问题。关键词FPGA、Verilog、for循环、generate、RTL设计、时序逻辑、可综合代码一、先说结论FPGA 中的 for 和软件中的 for 不一样刚接触 FPGA 时很多人会下意识地把 Verilog 中的for理解成 C 语言里的循环for (i 0; i 8; i) { // 每次执行一次 }软件中的for通常表示同一段指令依次执行多次。但在可综合的 FPGA RTL 代码中for通常表示让综合工具按照循环次数把硬件逻辑重复展开多份。例如integer i; always (*) begin for (i 0; i 8; i i 1) begin y[i] a[i] b[i]; end end这段代码不会生成一个“循环控制器”也不会让一个与门工作 8 次。综合后更接近下面的硬件assign y[0] a[0] b[0]; assign y[1] a[1] b[1]; assign y[2] a[2] b[2]; assign y[3] a[3] b[3]; assign y[4] a[4] b[4]; assign y[5] a[5] b[5]; assign y[6] a[6] b[6]; assign y[7] a[7] b[7];也就是说综合工具会展开出 8 组对应逻辑。这是理解 FPGA 中for语句的基础。二、FPGA 中常见的两类 for实际设计中最常见的是下面两种。类型使用位置循环变量主要用途过程foralways、initial等过程块内部integer或 SystemVerilog 的int重复描述组合逻辑、寄存器赋值、数组操作generate for模块内部、过程块外部genvar批量例化模块、生成连续赋值或重复硬件结构两者看起来相似但使用位置和用途不同。三、过程 for在 always 块中重复描述逻辑3.1 基本语法Verilog 中常见写法如下integer i; always (*) begin for (i 0; i 8; i i 1) begin // 组合逻辑 end end在 SystemVerilog 中也可以把循环变量写在for内部always_comb begin for (int i 0; i 8; i) begin // 组合逻辑 end end如果工程使用的是传统 Verilog建议使用第一种写法兼容性更好。四、组合逻辑中使用 for下面实现一个 16 位输入中“1”的个数统计。输入为input wire [15:0] din;输出最大可能为 16因此需要 5 位output reg [4:0] ones_count;完整组合逻辑如下integer i; always (*) begin ones_count 5d0; for (i 0; i 16; i i 1) begin ones_count ones_count din[i]; end end这里有两个关键点。4.1 循环前必须给输出赋初值下面这种写法是不完整的always (*) begin for (i 0; i 16; i i 1) begin ones_count ones_count din[i]; end endones_count在计算前没有初始值既使用旧值又给自己赋值容易形成组合反馈或者锁存器相关问题。正确写法应该先赋默认值ones_count 5d0;4.2 组合逻辑中使用阻塞赋值组合逻辑内部通常使用阻塞赋值ones_count ones_count din[i];因为后一次循环需要使用前一次循环刚刚计算出的结果。这段代码综合后通常会形成一组加法逻辑。综合工具可能继续优化结构但不能简单理解为“只使用一个加法器计算 16 次”。五、时序逻辑中使用 for移位寄存器示例下面使用for实现一个 4 级、每级 8 位的移位寄存器。reg [7:0] shift_reg [0:3]; integer i; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i 4; i i 1) begin shift_reg[i] 8d0; end end else if (en) begin shift_reg[0] din; for (i 1; i 4; i i 1) begin shift_reg[i] shift_reg[i-1]; end end end assign dout shift_reg[3];5.1 为什么这里要使用非阻塞赋值时序逻辑中使用非阻塞赋值shift_reg[i] shift_reg[i-1];在时钟上升沿到来时所有寄存器都会读取更新前的旧值然后同时更新。假设每个时钟输入一个数据上升沿编号 1 2 3 4 din A B C D shift_reg[0] A B C D shift_reg[1] - A B C shift_reg[2] - - A B shift_reg[3] - - - A这里的表格表示每个时钟上升沿之后的寄存器值。需要注意的是产生 4 个时钟周期延迟的原因不是for循环执行了 4 次而是设计中确实存在 4 级寄存器。六、新人最容易踩的坑在 for 中连续使用非阻塞赋值累加下面这段代码看起来像是在统计din中“1”的个数integer i; reg [4:0] ones_count; always (posedge clk or negedge rst_n) begin if (!rst_n) begin ones_count 5d0; end else if (valid) begin ones_count 5d0; for (i 0; i 16; i i 1) begin ones_count ones_count din[i]; end end end这段代码是错误的。6.1 错误原因非阻塞赋值不会立即更新左侧变量。执行下面的语句时ones_count ones_count din[i];每次循环读取的通常都是当前时钟沿到来前的旧ones_count而不是上一次循环的计算结果。同一个always块中对ones_count进行了多次非阻塞赋值最终前面的赋值会被后面的赋值覆盖。最终结果并不是 16 位累加值而更接近最后一次赋值对应的结果。6.2 正确写法先在组合逻辑中使用阻塞赋值完成累加再在时钟沿把最终结果写入寄存器reg [4:0] ones_count_next; reg [4:0] ones_count; integer i; always (*) begin ones_count_next 5d0; for (i 0; i 16; i i 1) begin ones_count_next ones_count_next din[i]; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin ones_count 5d0; end else if (valid) begin ones_count ones_count_next; end end这样做的逻辑很明确组合逻辑计算完整结果时钟上升沿保存一次最终结果时序逻辑中只对ones_count赋值一次。七、generate for批量生成硬件结构如果要批量生成连续赋值、模块实例或者重复硬件单元应该使用generate for。7.1 基本写法下面实现参数化的按位与逻辑module bit_and_array #( parameter WIDTH 8 )( input wire [WIDTH-1:0] a, input wire [WIDTH-1:0] b, output wire [WIDTH-1:0] y ); genvar g; generate for (g 0; g WIDTH; g g 1) begin : GEN_BIT_AND assign y[g] a[g] b[g]; end endgenerate endmodule这里的g使用genvar声明。generate for会在设计展开阶段生成多组独立逻辑。假设WIDTH8综合工具会生成 8 组按位与逻辑。7.2 为什么要给 generate 块命名下面这一段中的GEN_BIT_AND是生成块名称begin : GEN_BIT_AND建议始终给生成块命名。命名后仿真和综合工具中的层次结构通常类似GEN_BIT_AND[0] GEN_BIT_AND[1] GEN_BIT_AND[2] ...这样更方便查看仿真波形定位综合后的层次分析时序路径调试批量例化的模块。八、generate for 批量例化模块假设已经有一个独立的处理单元module data_unit ( input wire clk, input wire rst_n, input wire din, output wire dout ); // 内部逻辑 endmodule现在需要例化 8 个相同单元module data_unit_array ( input wire clk, input wire rst_n, input wire [7:0] din, output wire [7:0] dout ); genvar g; generate for (g 0; g 8; g g 1) begin : GEN_DATA_UNIT data_unit u_data_unit ( .clk (clk), .rst_n (rst_n), .din (din[g]), .dout (dout[g]) ); end endgenerate endmodule过程for不能放在always块中例化模块。批量例化模块时应使用generate for。九、for 不会自动增加时钟周期这是一个非常常见的误区。例如always (posedge clk) begin if (en) begin for (i 0; i 16; i i 1) begin result[i] a[i] b[i]; end end end这段代码不是每个时钟计算一位也不是需要 16 个时钟才能完成。它描述的是 16 组逻辑和 16 个对应寄存器在同一个时钟沿完成更新。如果希望每个时钟只处理一个数据需要自己设计计数器状态机数据索引累加寄存器完成信号。也就是说for适合描述空间上的重复状态机适合描述时间上的重复。十、循环次数必须尽量在综合时确定推荐下面这种固定循环边界parameter DATA_WIDTH 16; integer i; always (*) begin result 0; for (i 0; i DATA_WIDTH; i i 1) begin result result din[i]; end endDATA_WIDTH是参数综合时可以确定具体值因此综合工具能够展开循环。不推荐直接使用运行时输入信号作为循环上限for (i 0; i count; i i 1) begin // 逻辑 end这里的count如果是 FPGA 运行过程中的输入信号循环次数在综合阶段无法确定。不同综合工具对这种写法的支持情况不同可能出现无法综合综合报错推断出结构复杂的逻辑仿真通过但综合失败不同工具结果不一致。更稳妥的写法是使用固定最大循环次数并在循环内部进行条件判断parameter MAX_COUNT 16; integer i; always (*) begin result 0; for (i 0; i MAX_COUNT; i i 1) begin if (i count) begin result result din[i]; end end end但需要注意这样仍然会按照MAX_COUNT展开硬件资源消耗取决于最大循环次数而不是运行时的count。十一、for 循环次数越多资源不一定越省考虑下面的代码integer i; always (*) begin sum 0; for (i 0; i 256; i i 1) begin sum sum data[i]; end end从代码上看只有几行但综合工具需要实现 256 个数据的求和逻辑。可能带来以下问题使用大量 LUT 和加法资源形成较长的组合路径降低最大工作频率布线压力增大综合和布局布线时间增加。代码短不代表硬件少。对于大量数据处理需要根据性能要求选择结构。方案一完全并行所有数据在一个周期内完成处理。优点吞吐率高延迟周期少。缺点资源消耗大组合路径可能较长时序收敛困难。方案二流水线加法树把运算分成多级每一级加入寄存器。优点工作频率高吞吐率可以保持较高。缺点增加流水线延迟设计复杂度提高需要处理有效信号对齐。方案三计数器加累加器每个时钟处理一个或少量数据。优点资源消耗小可以复用加法器。缺点完成一次计算需要多个时钟吞吐率较低。因此是否使用for直接展开取决于面积、频率和吞吐率要求而不是代码是否容易编写。十二、使用 for 复位数组时要注意 BRAM 推断下面的写法经常用于复位寄存器数组reg [7:0] mem [0:255]; integer i; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i 256; i i 1) begin mem[i] 8d0; end end else if (wr_en) begin mem[wr_addr] wr_data; end end仿真结果可能完全正确但综合时需要特别注意。很多 FPGA 内部的块 RAM 不支持对全部存储单元进行统一的异步复位。给整个数组增加复位逻辑后综合工具可能无法把它推断成 BRAM而是使用大量触发器或分布式 RAM 实现。结果可能是触发器数量明显增加LUT 消耗增加复位网络压力变大原本应该使用 BRAM 的存储器没有使用 BRAM。对于较大的存储器常见处理方法包括使用初始化文件只复位读写地址和控制状态使用有效位标记数据是否有效上电后通过状态机逐地址清零根据目标器件支持情况选择同步初始化方式。小规模寄存器数组可以直接使用for复位但大规模存储器不能只看仿真结果还要检查综合报告中的资源类型。十三、不要在多个 always 块中共用同一个循环变量传统 Verilog 经常在模块中声明循环变量integer i;如果模块中有多个always块不建议都使用同一个iinteger i; always (*) begin for (i 0; i 8; i i 1) begin // 逻辑 A end end always (*) begin for (i 0; i 16; i i 1) begin // 逻辑 B end end因为i是同一个过程变量被多个过程块同时使用可能导致仿真竞争或工具兼容性问题。更稳妥的写法是为不同过程块使用不同变量integer i; integer j; always (*) begin for (i 0; i 8; i i 1) begin // 逻辑 A end end always (*) begin for (j 0; j 16; j j 1) begin // 逻辑 B end end如果使用 SystemVerilog可以直接在循环内部声明局部变量always_comb begin for (int i 0; i 8; i) begin // 逻辑 end end十四、integer 循环变量不一定会生成 32 位硬件看到下面的代码时新人可能担心integer i会生成一个 32 位计数器integer i; always (*) begin for (i 0; i 8; i i 1) begin y[i] a[i] b[i]; end end在这种固定边界的可综合循环中i主要用于帮助综合工具展开逻辑通常不会生成一个真正运行的 32 位硬件计数器。但是如果integer被当成实际数据参与寄存、运算或模块接口处理就可能影响综合结果。因此需要区分只作为循环展开索引作为真正的数据变量参与硬件逻辑。十五、嵌套 for 要特别关注资源规模二维数组或者矩阵运算中经常出现嵌套循环integer row; integer col; always (*) begin for (row 0; row 8; row row 1) begin for (col 0; col 8; col col 1) begin result[row][col] a[row][col] b[row][col]; end end end两个循环分别执行 8 次最终会展开成 64 组逻辑。如果两个循环分别为 64 次和 64 次就可能展开成 4096 组逻辑。因此看到嵌套循环时应先计算总展开次数总展开次数 外层循环次数 × 内层循环次数不要只看每一层的循环次数。十六、for 中使用数组索引时要检查越界下面的代码存在越界风险for (i 0; i 8; i i 1) begin data_out[i] data_in[i1]; end当i7时会访问data_in[8]如果data_in只有[7:0]该索引已经越界。正确写法需要根据功能调整循环范围for (i 0; i 7; i i 1) begin data_out[i] data_in[i1]; end data_out[7] 1b0;这类问题有时不会直接导致明显的编译错误却可能在仿真中产生X因此需要特别检查循环的起始值、终止条件和数组范围。十七、不要依赖 break 和 continue 提高可综合性SystemVerilog 支持break和continue部分综合工具也能处理但在强调工具兼容性和代码可移植性的 FPGA 项目中不建议过度依赖。例如不推荐把数据相关的退出条件写成for (int i 0; i WIDTH; i) begin if (din[i]) begin index i; break; end end更稳妥的方式是明确描述优先级逻辑integer i; reg found; always (*) begin index 0; found 1b0; for (i 0; i WIDTH; i i 1) begin if (!found din[i]) begin index i; found 1b1; end end end这段代码描述的是优先编码逻辑。但循环方向会影响优先级。如果从低位向高位遍历并使用found保留第一次匹配那么低位优先如果需要高位优先应改变循环方向或重新设计条件。十八、过程 for 和 generate for 应该怎么选可以按照下面的规则判断。使用过程 for 的场景在组合逻辑中逐位处理向量对多个寄存器执行相同赋值复位小规模寄存器数组实现移位寄存器进行固定次数的组合累加对数组进行固定范围遍历。示例always (*) begin for (i 0; i WIDTH; i i 1) begin y[i] a[i] ^ b[i]; end end使用 generate for 的场景批量例化模块批量生成连续赋值生成参数化硬件结构每次循环都需要独立的层次名称在模块级生成多组相同电路。示例generate for (g 0; g WIDTH; g g 1) begin : GEN_LOGIC assign y[g] a[g] ^ b[g]; end endgenerate使用状态机和计数器的场景希望每个时钟只处理一个数据希望复用同一个加法器或乘法器运算次数较多不能一次全部展开循环次数由运行时数据决定需要控制处理开始、结束和忙闲状态。十九、仿真通过不代表 for 的写法适合综合使用for后至少需要检查以下内容循环边界能否在综合时确定是否出现数组越界组合逻辑是否有完整默认赋值时序逻辑是否正确使用非阻塞赋值是否在同一时钟块中对同一寄存器多次非阻塞赋值循环展开后资源数量是否过大组合路径是否过长存储器是否仍然被推断成 BRAM综合报告中的 LUT、触发器、DSP、BRAM 数量是否符合预期时序报告是否满足目标时钟频率。FPGA 工程不能只停留在 RTL 仿真。代码最终描述的是硬件因此必须结合RTL 仿真综合结果资源报告RTL 原理图时序报告上板测试。二十、实际工程中的检查清单写完for后可以按下面的顺序快速检查。1. 循环次数是否固定推荐for (i 0; i WIDTH; i i 1)其中WIDTH是参数或常量。2. 是否误把 for 当成多周期执行for不会自动生成计数器也不会自动产生多周期控制。3. 组合逻辑是否先赋默认值result 0;避免锁存器和组合反馈。4. 组合逻辑是否使用阻塞赋值result result din[i];5. 时序逻辑是否使用非阻塞赋值shift_reg[i] shift_reg[i-1];6. 同一寄存器是否在循环中被多次非阻塞赋值下面这种写法需要重点检查sum sum data[i];7. 展开后会生成多少份硬件循环 16 次通常代表 16 份相关逻辑而不是一份逻辑运行 16 次。8. 是否可能影响 BRAM 或 DSP 推断尤其是大数组复位、复杂循环运算和嵌套循环。9. 是否检查了数组边界重点检查初始值 终止条件 步进方向 最大索引 最小索引10. 是否查看综合和时序报告不要只看代码和功能仿真。二十一、总结FPGA 中的for本质上是一种重复描述硬件结构的方式。需要记住以下几点for通常会被综合工具展开不是软件意义上的顺序循环过程for常用于always块中的重复赋值和数据处理generate for常用于批量例化模块和生成重复硬件结构组合逻辑中通常使用阻塞赋值并在循环前给变量赋初值时序逻辑中通常使用非阻塞赋值不要在循环中对同一个寄存器连续进行非阻塞累加循环边界应尽量使用参数或常量循环次数越多展开后的硬件资源通常越多大规模数组复位可能导致 BRAM 无法正常推断仿真通过后还要检查综合资源和时序结果。判断一段for代码是否合理不能只看代码能不能运行而要问三个问题这段循环会展开成什么硬件 会占用多少资源 能否在目标时钟频率下完成把这三个问题想清楚才算真正理解 FPGA 中的for。#FPGA #Verilog #System #Verilog #FIFO 异步FIFO #同步FIFO #Gray码 #CDC #跨时钟域 #数字电路
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门