Verilog找1模块设计:从casez到前缀OR的工程优化

发布时间:2026/9/30 8:51:27
Verilog找1模块设计:从casez到前缀OR的工程优化
如果你在芯片设计或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 1b1; casez (din) 4b???1 : begin idx 2d0; one_hot 4b0001; end 4b??10 : begin idx 2d1; one_hot 4b0010; end 4b?100 : begin idx 2d2; one_hot 4b0100; end 4b1000 : begin idx 2d3; one_hot 4b1000; end default : begin found 1b0; idx 2d0; one_hot 4b0000; end endcase end endmodulecasez里的?表示dont 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 1b0; idx 0; one_hot 0; for (int i 0; i WIDTH; i i 1) begin if (!found din[i]) begin found 1b1; 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], 1b0}; 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], 1b0}它相当于把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时为0foundoutput1是否存在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], 1b0}; 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; endmodule3.3 逐段解读第一段计算prefix_orprefix_or[i]表示din[i:0]这个区间内有没有1。这段代码无论WIDTH是多少都能自动展开属于纯组合逻辑。第二段生成one_hot~{prefix_or[WIDTH-2:0], 1b0}把前缀OR左移一位并取反再和din相与。注意位宽拼接{prefix_or[WIDTH-2:0], 1b0}整体仍然保持WIDTH位。如果直接写~prefix_or 1在某些不支持移位自动宽度的写法里容易出位宽警告拼接写法更稳。第三段是独热码转索引。因为one_hot保证只有一位是1for循环遇到它才赋值逻辑等价于一个编码器。这里用了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[i1] | din[i]; end assign one_hot_msb din ~{1b0, 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 边界用例表输入din8位期望one_hot期望idx期望found8’b0000_00008’b0000_0000008’b0000_00018’b0000_0001018’b1000_00008’b1000_0000718’b0000_10108’b0000_0010118’b0100_01008’b0000_0100218’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 8h00; #1; compute_exp; check_result; din 8h01; #1; compute_exp; check_result; din 8h80; #1; compute_exp; check_result; din 8h0A; #1; compute_exp; check_result; din 8hFF; #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里的?在综合里是dont 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为0idx给什么数值本身不重要但建议固定给0方便形式化验证和后端一致性检查。仿真时如果din出现X建议加断言提示warning否则X传播会让found或idx的波形看起来“半高不高”白白浪费调试时间。后仿真阶段更要检查组合路径时序找1模块输出直连触发器时输入到采样的组合延迟必须满足收敛要求。5. 综合视角面积、延时差异与工程代码风格RTL写得对只是第一步综合出来能不能收敛是另一码事。这节聊一些我在项目里实测过的现象和代码规范。5.1 三种实现风格在宽位下的差异以下数值是我在主流FPGA和工艺库下的经验参考不同工具、不同工艺会有差异重点看相对趋势。实现方式N8逻辑级数N32逻辑级数N64逻辑级数LUT量级N64casez真值表低很高接近32级几乎不可用高for循环加锁存低中高高中前缀OR加编码低低约log级低中casez和for循环本质上都是优先级链位宽翻倍延迟就翻倍前缀OR加编码器则可以把长链变成树形结构。我早期在项目里用32位for循环版找1模块做仲裁器综合后组合路径成为关键路径改成前缀OR版本后路径延迟大致掉了40%。那次之后我在宽位组合逻辑里基本不用隐式优先级链。5.2 casez、casex与parallel_case、full_case的工程取舍业界基本共识尽量不要用casex因为它把X也当成dont care会盖住许多真实仿真问题。casez可以用但只建议用于窄位真值表场景。parallel_case和full_case这类综合指令能改语义能不用就不用非用不可时必须有充分的验证保证所有输入都被覆盖。最稳妥的写法就是普通case加default或者直接写成赋值逻辑让工具自己去优化。5.3 参数化里的坑$clog2和位宽为1$clog2(1)的结果是0这意味着WIDTH1时索引位宽会是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不会报错但严格来说这是未定义边界。建议显式处理base0的情况或者把移位量写成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方案。这种基础模块值得花十分钟认认真真打磨一版后面所有用到它的地方都会跟着受益。