☰
Verilog找1模块设计:从casez到前缀OR的工程优化
2026/9/30 8:51:19 网站建设 项目流程

如果你在芯片设计或FPGA开发里待过一阵,八成已经写过无数遍“找1”这种代码——仲裁器里找最低优先级请求、浮点单元里找最高位1做归一化、状态机里找某个触发标志位。它听上去太简单,以至于很多人随手就是一个casez完事。但只要输入位宽从8变成64,或者某天综合时序收敛不过去,这个“简单逻辑”就会变成设计里最扎手的一环。

这篇文章就围绕“Verilog设计找到1的位置”这个经典模块,把背后的需求边界、实现思路、RTL写法、仿真验证、综合优化,以及真实项目里怎么复用它,完整拆一遍。适合刚入门Verilog的初学者,也适合想优化现有仲裁器、浮点单元、位图分配器的在职工程师。我会直接给代码、给对比、给踩坑记录,争取你读完能直接拿一版可综合、可复用、经得起时序考验的找1模块去用。

1. 先搞清楚这模块用在哪:四个真实场景与需求边界

很多人把“找1”当成一道练习题,但它在真实芯片里几乎是所有仲裁、扫描、归一化逻辑的地基。理解它到底解决什么问题,比背代码重要得多。

1.1 场景一:总线仲裁器

多路master并发请求共享总线时,仲裁器必须从一堆请求信号里选出一个获准者。固定优先级仲裁,本质上就是找request向量里最低位(或最高位)的那个1,然后对应的grant位拉高。这是找1模块最经典的应用。

1.2 场景二:浮点运算归一化

浮点加法、乘法做完之后,尾数往往不在规格化范围内,比如0.001xxx这样。这时候需要找到最高位的1在哪,决定尾数左移多少位、指数寄存器减多少。在浮点流水线里,这个“找最高位1”的模块往往直接躺在关键路径上,快不快直接决定主频。

1.3 场景三:位图分配与资源管理

内存管理里的bitmap、寄存器堆的空闲块分配、缓存line的替换扫描,核心操作都是从一堆标志位里找到第一个可用的slot。有时候找1,有时候找0——把标志位取反就变成找1了。

1.4 场景四:FIFO空位扫描

集中式FIFO或带valid位的队列,需要找第一个空entry去写入;或者网络交换机里找第一个有数据的entry去读出。本质上也是掩码加找1。

真正动手设计之前,有四个需求边界必须先定清楚,否则写出来的模块别人没法用:

  • 返回索引还是详情编码:有些下游只需要二进制索引,比如移位器的移位量;有些下游希望直接拿独热码作为grant或enable信号。我的建议是两个都输出,调用方按需取用,省得下游再放一个译码器。
  • 找最低位1还是最高位1:仲裁器一般需要最低位1,浮点归一化需要最高位1。两种方向都存在,模块应当可以灵活切换。
  • 全0输入怎么办:输出索引没有意义,必须有一个found信号告诉下游“这次结果是无效的”。如果不给found,下游拿一个无效值去乱算,排查起来极其痛苦。
  • 位宽是否固定:固定4位8位,casez真值表很直观;如果是64位或参数化总线宽度,就必须写可参数化的实现。

2. 三种实现思路的原理与取舍:casez、for循环加锁存、前缀OR加编码

这一节我们从“最暴力”到“最适合工程落地”逐一展开。每种方案都有它的位置,关键是你得知道它综合成电路之后长什么样。

2.1 casez真值表:最直观,但只在窄位宽下好用

module find_one_casez #( parameter WIDTH = 4 )( input wire [WIDTH-1:0] din, output reg [WIDTH-1:0] one_hot, output reg [$clog2(WIDTH)-1:0] idx, output reg found ); always @(*) begin found = 1'b1; casez (din) 4'b???1 : begin idx = 2'd0; one_hot = 4'b0001; end 4'b??10 : begin idx = 2'd1; one_hot = 4'b0010; end 4'b?100 : begin idx = 2'd2; one_hot = 4'b0100; end 4'b1000 : begin idx = 2'd3; one_hot = 4'b1000; end default : begin found = 1'b0; idx = 2'd0; one_hot = 4'b0000; end endcase end endmodule

casez里的?表示don't care。从上往下第一个匹配的分支生效,所以分支顺序就是优先级顺序。这里???1优先级最高,对应最低位1被优先选中。

优点:真值表一目了然,窄位宽下代码review非常轻松。

缺点:无法参数化,位宽增加时case分支数量成倍爆炸;综合后本质是一条很长的if-else优先级链,延迟随位宽线性增长。8位凑合,16位就开始难受,32位以上基本不推荐。

2.2 for循环加锁存:可参数化版本,综合后通常还是优先级链

module find_one_for #( parameter WIDTH = 8 )( input wire [WIDTH-1:0] din, output reg [WIDTH-1:0] one_hot, output reg [$clog2(WIDTH)-1:0] idx, output reg found ); always @(*) begin found = 1'b0; idx = '0; one_hot = '0; for (int i = 0; i < WIDTH; i = i + 1) begin if (!found && din[i]) begin found = 1'b1; idx = i; one_hot = (1 << i); end end end endmodule

两个非常容易写错的地方:

  • 进入循环前的初始化不能少。这是纯组合逻辑,found和idx不会继承上一次值。漏了初始化会被综合器推断成锁存器,后仿真出现各种诡异行为。
  • 判断条件必须是!found && din[i]。如果只写if (din[i]),循环会一直执行到最后,最终得到的是最高位的1,不是最低位的1,行为完全相反。这个坑我亲眼见过同事踩过,功能仿真不出错、综合也没报错,就是仲裁行为反了。

综合时for循环会被展开成串行的if判断结构,本质上和casez一样是优先级链。它的优势是任意位宽都能参数化,劣势是位宽大了以后路径延迟不太乐观。

2.3 前缀OR加编码:接近最优的工程方案

这个思路核心只有一句话:最低位1的独热码,可以通过“本位是1且低位区间全0”来得到。先算前缀OR,再按位取反。

module find_one_lsb #( parameter WIDTH = 8 )( input wire [WIDTH-1:0] din, output wire [WIDTH-1:0] one_hot, output wire [$clog2(WIDTH)-1:0] idx, output wire found ); reg [WIDTH-1:0] prefix_or; always @(*) begin prefix_or[0] = din[0]; for (int i = 1; i < WIDTH; i = i + 1) prefix_or[i] = prefix_or[i-1] | din[i]; end assign one_hot = din & ~{prefix_or[WIDTH-2:0], 1'b0}; assign found = |din; always @(*) begin idx = '0; for (int i = 0; i < WIDTH; i = i + 1) if (one_hot[i]) idx = i; end endmodule

这段逻辑的关键在{prefix_or[WIDTH-2:0], 1'b0}:它相当于把prefix_or左移一位,让第i位对应prefix_or[i-1],也就是“比i更低的区间里是否已经有1”。取反再和din相与,只有在“本位是1且低位区间全0”时才输出1。这样得到的one_hot天然就是独热码。

最后独热码转二进制索引,写一个简单的for循环即可,综合器通常能优化成编码器树。

它的最大优势是去掉了优先级依赖,综合工具可以把前缀OR展开成平衡树,逻辑级数从线性降到log级别,宽位下时序表现好得多。

2.4 三种方案对比与选择建议

实现方式可读性可参数化逻辑级数(宽位)面积适用场景
casez真值表好(窄位)差高(N级)低位宽固定且不超过4~8位
for循环加锁存好好中到高低位宽一般,工具优化有限时慎用
前缀OR加编码中好低(约log N级)低宽位、时序敏感,推荐默认方案

我的建议是:16位以上直接用前缀OR方案,16位以下随便。别在casez真值表上硬撑,位宽一改就要重写。

3. RTL实战:一份可参数化复用性拉满的找1模块

有了原理铺垫,现在给一份可以直接放进工程的完整实现,并解释每一段为什么这么写。

3.1 接口设计

端口方向位宽说明
dininputWIDTH输入位向量
one_hotoutputWIDTH找到的1的独热码,全0时全零
idxoutput$clog2(WIDTH)找到的1的二进制索引,全0时为0
foundoutput1是否存在1

同时给one_hot和idx,是为了让仲裁器等场景直接用one_hot做grant,省一次译码;让浮点归一化等场景直接用idx做移位量。全0时idx给0只是兼容值,下游必须以found为准。

3.2 完整代码

module find_one_lsb #( parameter WIDTH = 8 )( input wire [WIDTH-1:0] din, output wire [WIDTH-1:0] one_hot, output wire [$clog2(WIDTH)-1:0] idx, output wire found ); localparam IDX_W = WIDTH > 1 ? $clog2(WIDTH) : 1; reg [WIDTH-1:0] prefix_or; always @(*) begin prefix_or[0] = din[0]; for (int i = 1; i < WIDTH; i = i + 1) prefix_or[i] = prefix_or[i-1] | din[i]; end assign one_hot = din & ~{prefix_or[WIDTH-2:0], 1'b0}; assign found = |din; reg [IDX_W-1:0] idx_r; always @(*) begin idx_r = '0; for (int i = 0; i < WIDTH; i = i + 1) if (one_hot[i]) idx_r = i; end assign idx = idx_r; endmodule

3.3 逐段解读

第一段计算prefix_or:prefix_or[i]表示din[i:0]这个区间内有没有1。这段代码无论WIDTH是多少都能自动展开,属于纯组合逻辑。

第二段生成one_hot:~{prefix_or[WIDTH-2:0], 1'b0}把前缀OR左移一位并取反,再和din相与。注意位宽拼接,{prefix_or[WIDTH-2:0], 1'b0}整体仍然保持WIDTH位。如果直接写~(prefix_or << 1)在某些不支持移位自动宽度的写法里容易出位宽警告,拼接写法更稳。

第三段是独热码转索引。因为one_hot保证只有一位是1,for循环遇到它才赋值,逻辑等价于一个编码器。这里用了reg加assign,是为了让索引输出看起来更像一个“计算结果”。在SystemVerilog里可以直接写always_comb,代码更干净。

3.4 找最高位1的两种做法

如果需要找最高位1,有两个办法。

方法一:把din反转,调用find_one_lsb,再用WIDTH-1-idx还原。缺点是额外消耗反转逻辑,好处是代码复用最彻底。

方法二:把前缀OR改成后缀OR。后缀OR的[i]表示din[WIDTH-1:i]里有没有1,然后:

wire [WIDTH-1:0] suffix_or; always @(*) begin suffix_or[WIDTH-1] = din[WIDTH-1]; for (int i = WIDTH-2; i >= 0; i = i - 1) suffix_or[i] = suffix_or[i+1] | din[i]; end assign one_hot_msb = din & ~{1'b0, suffix_or[WIDTH-1:1]};

实际项目里如果两种方向都会用到,我建议封装成一个带DIRECTION参数的模块,或者干脆写两个独立模块,逻辑简单清楚。不要在一个always块里硬揉方向判断,综合后容易留下冗余逻辑。

3.5 支持掩码的版本

仲裁器带屏蔽、缓存扫描带过滤的场合,希望只在一个生效区间里找1。给模块加一个mask输入,内部统一处理:

module find_one_masked #( parameter WIDTH = 8 )( input wire [WIDTH-1:0] din, input wire [WIDTH-1:0] mask, output wire [WIDTH-1:0] one_hot, output wire [$clog2(WIDTH)-1:0] idx, output wire found ); wire [WIDTH-1:0] dat = din & mask; find_one_lsb #(.WIDTH(WIDTH)) u_find ( .din (dat), .one_hot (one_hot), .idx (idx), .found (found) ); endmodule

调用方不需要自己先做与运算,接口更内聚。注意mask和din必须同宽,参数化时很容易踩宽度的坑。

4. 仿真验证:边界用例、随机回归与三个易翻车的细节

找1模块逻辑简单,但越简单越容易想当然。仿真验证的重点是边界和特殊情况,尤其是全0输入。

4.1 边界用例表

输入din(8位)期望one_hot期望idx期望found
8’b0000_00008’b0000_000000
8’b0000_00018’b0000_000101
8’b1000_00008’b1000_000071
8’b0000_10108’b0000_001011
8’b0100_01008’b0000_010021
8’b1111_11118’b0000_000101

注意多个1出现时,取的是最低位的1。

4.2 随机回归testbench框架

module tb_find_one; parameter WIDTH = 8; reg [WIDTH-1:0] din; wire [WIDTH-1:0] one_hot; wire [$clog2(WIDTH)-1:0] idx; wire found; find_one_lsb #(.WIDTH(WIDTH)) dut ( .din (din), .one_hot (one_hot), .idx (idx), .found (found) ); integer i; reg [WIDTH-1:0] exp_hot; reg [$clog2(WIDTH)-1:0] exp_idx; reg exp_found; integer error_cnt; // 期望值计算:找最低位1 task compute_exp; begin exp_found = 0; exp_hot = 0; exp_idx = 0; for (i = 0; i < WIDTH; i = i + 1) begin if (din[i] && !exp_found) begin exp_found = 1; exp_idx = i; exp_hot = (1 << i); end end end endtask initial begin error_cnt = 0; // 定向边界 din = 8'h00; #1; compute_exp; check_result; din = 8'h01; #1; compute_exp; check_result; din = 8'h80; #1; compute_exp; check_result; din = 8'h0A; #1; compute_exp; check_result; din = 8'hFF; #1; compute_exp; check_result; // 随机回归 repeat (1000) begin din = $random; #1; compute_exp; check_result; end if (error_cnt == 0) $display("TEST PASSED"); else $display("TEST FAILED, errors = %0d", error_cnt); $finish; end task check_result; begin if (found !== exp_found || idx !== exp_idx || one_hot !== exp_hot) begin error_cnt = error_cnt + 1; $error("mismatch din=%b one_hot=%b exp_hot=%b idx=%0d exp_idx=%0d found=%b exp_found=%b", din, one_hot, exp_hot, idx, exp_idx, found, exp_found); end end endtask endmodule

用$random随机回归1000轮,同时保留定向边界用例。这个模板可以直接跑在iverilog或主流仿真工具里。

4.3 三个易翻车细节

  • 组合输出打拍要一视同仁:如果下游需要在时钟沿采样found和idx,要把两者打在同一拍。只打found不打idx,或者反过来,会出现一拍错位,功能仿真极难发现。
  • casez仿真和综合不一致:casez里的?在综合里是don't care,但仿真时输入出现X,?并不会匹配任意X分支,可能落到default。这就造成仿真行为和后仿综合结果不一致。所以宽位输入尽量别用casez去匹配,这就是我推荐前缀OR方案的另一个原因。
  • 循环内直接生成one_hot容易惹latch:如果写成在循环里对one_hot逐位赋值,某些工具会推断出优先级选择器甚至锁存器。正确做法是循环里只算found和idx,循环结束后统一用1 << idx生成one_hot,或者干脆让one_hot由独立assign生成。

4.4 全0和X态处理建议

全0输入时found为0,idx给什么数值本身不重要,但建议固定给0,方便形式化验证和后端一致性检查。仿真时如果din出现X,建议加断言提示warning,否则X传播会让found或idx的波形看起来“半高不高”,白白浪费调试时间。后仿真阶段更要检查组合路径时序,找1模块输出直连触发器时,输入到采样的组合延迟必须满足收敛要求。

5. 综合视角:面积、延时差异与工程代码风格

RTL写得对只是第一步,综合出来能不能收敛是另一码事。这节聊一些我在项目里实测过的现象和代码规范。

5.1 三种实现风格在宽位下的差异

以下数值是我在主流FPGA和工艺库下的经验参考,不同工具、不同工艺会有差异,重点看相对趋势。

实现方式N=8逻辑级数N=32逻辑级数N=64逻辑级数LUT量级(N=64)
casez真值表低很高(接近32级)几乎不可用高
for循环加锁存低中高高中
前缀OR加编码低低(约log级)低中

casez和for循环本质上都是优先级链,位宽翻倍延迟就翻倍;前缀OR加编码器则可以把长链变成树形结构。我早期在项目里用32位for循环版找1模块做仲裁器,综合后组合路径成为关键路径,改成前缀OR版本后路径延迟大致掉了40%。那次之后我在宽位组合逻辑里基本不用隐式优先级链。

5.2 casez、casex与parallel_case、full_case的工程取舍

业界基本共识:尽量不要用casex,因为它把X也当成don't care,会盖住许多真实仿真问题。casez可以用,但只建议用于窄位真值表场景。parallel_case和full_case这类综合指令能改语义,能不用就不用,非用不可时必须有充分的验证保证所有输入都被覆盖。最稳妥的写法就是普通case加default,或者直接写成赋值逻辑,让工具自己去优化。

5.3 参数化里的坑:$clog2和位宽为1

$clog2(1)的结果是0,这意味着WIDTH=1时索引位宽会是0位,编译直接报一堆怪异错误。防御性写法:

localparam IDX_W = WIDTH > 1 ? $clog2(WIDTH) : 1;

另外一个容易忽略的点:前缀OR方案对任意位宽都成立,不需要位宽是2的幂。递归二分方案才会被非2的幂宽度恶心到。所以工程首选前缀OR不是没理由的。

5.4 工具与代码风格建议

  • int i是SystemVerilog语法,文件后缀得是.sv。如果项目还在纯Verilog-2001下,把int i改成integer i即可。
  • 组合逻辑块内所有输出先给默认值,再写条件分支。这是防latch最朴素的习惯,也方便同事review。
  • 建立Lint规则:禁casex、组合敏感列表必须写@(*)、for循环索引变量不要跨always共享。
  • 综合报告里重点看两个指标:组合路径延迟和LUT数量。如果大面积超标,把逻辑拆出来单独分析,找到底是编码器的问题还是前缀OR树的问题。

5.5 超宽位还想更快怎么办

如果WIDTH已经到128位甚至更宽,前缀OR法仍然可以进一步优化:把WIDTH拆成若干段,段内用前缀OR,段间再做一层OR,形成两级或三级树。不过这属于微优化,大多数场景做到“前缀OR加编码器”已经足够。真正的宽位时序瓶颈往往在下游扇出,比如one_hot直接驱动了一大片使能逻辑,那就要考虑插寄存器了。

6. 从找1到仲裁、浮点归一化:把这个模块用到真实设计里

最后给几个找1模块的真实用法,帮你把它从“练习题”变成“工具”。

6.1 固定优先级仲裁器

wire [WIDTH-1:0] request; wire [WIDTH-1:0] grant; wire any_req; find_one_lsb #(.WIDTH(WIDTH)) u_arb ( .din (request), .one_hot (grant), .idx (), .found (any_req) );

低位优先级最高,grant直接就是one_hot,不需要额外译码。

6.2 轮询仲裁器

轮询仲裁是固定优先级的升级版,关键是把request按当前轮询指针旋转,然后在旋转后的空间里找1。核心逻辑三行:

wire [WIDTH-1:0] rot_req = (req >> base) | (req << (WIDTH - base)); // 对rot_req做找最低位1,得到rot_grant // 再把位置映射回原空间

这里有个小坑:当base为0时,req << WIDTH在多数工具里结果是0,不会报错,但严格来说这是未定义边界。建议显式处理base=0的情况,或者把移位量写成(WIDTH - base) % WIDTH,避免代码review被揪出来。

6.3 浮点运算归一化

浮点加法收尾时,尾数可能是0.001xxx,要找到最高位的1,然后左移尾数、修正指数:

wire [MAN_W-1:0] man; wire [$clog2(MAN_W)-1:0] shift; wire man_zero; find_one_msb #(.WIDTH(MAN_W)) u_norm ( .din (man), .idx (shift), .found (~man_zero) ); // 尾数左移shift位,指数减去shift

归一化路径基本就是“找最高位1加桶形移位器”的组合,找1模块的速度直接影响浮点单元的主频。

6.4 FIFO空位扫描与bitmap分配

找第一个空entry,本质是把valid向量取反,再找最低位的1。bitmap分配器同理:分配时扫描空闲bit并清零,释放时置1。这个场景里one_hot输出可以直接当地址的块使能信号,省掉一个译码器,面积和时序都有收益。

我自己在实际项目里吃过窄位casez的亏,所以现在凡是要参数化、要跑宽位、要过综合时序的找1逻辑,默认都上前缀OR方案。这种基础模块值得花十分钟认认真真打磨一版,后面所有用到它的地方都会跟着受益。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询