ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Verilog实现参数化RR调度器:从算法到硬件电路的公平仲裁设计

Verilog实现参数化RR调度器:从算法到硬件电路的公平仲裁设计 1. 项目概述为什么硬件工程师绕不开RR调度器在芯片前端设计的江湖里但凡涉及到多路请求仲裁、资源分配的场景有一个名字你绝对避不开——轮询调度器也就是我们常说的RR调度器。我第一次在项目中独立负责一个AXI总线交叉开关的仲裁模块时导师就丢给我一句话“先把RR调度搞明白不然仲裁逻辑全是乱的。” 这句话我记到现在。RR调度听起来简单不就是“大家轮流来”吗但真要用Verilog在硅片上实现一个稳定、公平、无毛刺的轮询逻辑你会发现里面全是细节和“坑”。从状态机的设计到优先级指针的更新时机再到处理背靠背请求和空闲周期的边界情况每一步都需要精心设计。这个项目就是一次将经典的RR调度算法从教科书上的流程图落地成可综合、可验证的硬件电路的过程。它不仅仅是写几行always块那么简单更是对硬件设计思维的一次深度锤炼。你会遇到诸如“公平性如何在硬件时序中体现”、“指针更新是组合逻辑还是时序逻辑”、“如何防止饥饿但又不失效率”等一系列工程问题。通过Verilog实现它你能深刻理解同步数字电路的设计范式掌握状态机、计数器、优先级编码器等基础元件的灵活运用而这些正是构成复杂SoC的基石。无论你是正在学习数字电路的学生还是初入行业的芯片前端工程师亲手实现一个RR调度器都是一次性价比极高的技能升级。2. RR调度器的核心原理与硬件实现挑战2.1 从软件算法到硬件电路的思维转换在软件中RR调度可能就是一个循环队列用一个索引变量i每次执行i (i 1) % N然后检查第i个请求是否有效。简单直观。但在硬件中我们面对的是时钟驱动的同步电路。所有的操作必须在时钟沿的指挥下一拍一拍地完成。这带来了几个根本性的转变首先是“并行性”与“时序性”的矛盾。软件是串行执行硬件是并行感知。我们的电路需要在一个时钟周期内同时检查所有N路请求的有效性并根据当前的调度指针从中选出一路进行授权。这个“选择”逻辑必须是组合逻辑以确保在同一个周期内就能输出结果。其次是“状态”的维持。软件中的变量i就是状态。在硬件中这个状态即调度指针必须由一个寄存器来保存。这个寄存器何时更新是授权发生后立刻更新还是等到下一个周期再更新这直接影响了调度器的行为和时序。最后是“公平性”的硬件定义。软件的公平是逻辑上的。硬件的公平则体现在每个有效的请求都能在有限且确定的时钟周期内获得服务不会因为电路结构原因被永久忽略。这就要求我们的指针更新逻辑必须能“跳过”无效的请求指向下一个有效的请求。2.2 经典固定优先级仲裁与RR仲裁的对比在深入RR之前理解它的对立面——固定优先级仲裁——很有必要。这能让你明白RR的价值所在。假设我们有4个主设备Master0 ~ Master3竞争一个从设备的访问权。固定优先级通常设定Master0优先级最高Master3最低。只要Master0有请求它永远优先获得授权。这实现起来最简单一段优先级编码器Priority Encoder即可。但问题很明显如果高优先级的Master持续发起请求低优先级的Master将永远得不到服务即发生“饥饿”。轮询调度它引入了“动态公平”的概念。核心是一个会移动的指针。假设当前指针指向Master1。这一轮仲裁会从Master1开始向后查找第一个有效的请求。授权完成后指针会更新到被授权主设备的下一个位置。下一轮仲裁就从新的位置开始。这样每个主设备在其请求有效时都有机会在指针扫过时获得服务。为了更直观我们用一个场景对比两者的行为时钟周期Master3Master2Master1Master0固定优先级授权RR授权 (指针初值0)Cycle 10111Master0Master0Cycle 20110Master1Master1Cycle 30100Master2Master2Cycle 41000Master3Master3Cycle 51100Master2Master2 (指针从4回到0发现0无效1无效2有效)从表格可以看出在Cycle 2-4固定优先级下Master0无请求Master1/2/3依次获得机会看似公平但这只是因为高优先级者“休息”了。一旦Master0重新发起请求它会立刻霸占通道。而RR调度则展现了一种有秩序的公平授权顺序与指针位置强相关。注意这里展示的是最简单的“无权重”RR。在实际芯片中如网络交换芯片或高性能总线可能会使用加权轮询为不同端口分配不同的服务权重但这需要更复杂的计数器逻辑本项目我们先攻克基础版本。2.3 硬件实现的关键挑战与设计决策基于以上分析我们可以梳理出用Verilog实现一个参数化RR调度器必须解决的几个关键挑战指针的编码与存储指针用二进制编码还是One-hot编码二进制编码节省寄存器但查找逻辑需要比较器。One-hot编码一个N位的寄存器只有一位为1在查找下一个有效请求时可以利用桶式移位或优先级编码逻辑可能更简洁。本项目为追求面积优化采用二进制编码。“查找下一个有效请求”算法这是RR的核心算法。给定一个起始指针ptr和一个请求向量req[N-1:0]如何高效地在硬件中找出下一个为1的位常见方法有双循环法先将req向量循环左移ptr位然后用优先级编码器找到第一个‘1’的位置pos最终授权索引grant_id (ptr pos) % N。这需要桶式移位器在N较大时面积较大。掩码比较法将请求向量按指针位置拆分成高段和低段分别查找再合并。逻辑层次较深。状态机遍历法每个周期只检查一位依次遍历。这保证了绝对公平但延迟高吞吐率低N个周期才能仲裁一次不适合高性能场景。 我们将采用一种在中小规模设计中非常高效且直观的**“双重优先级编码”**法。指针更新时机这是最容易出BUG的地方。更新必须在授权确认之后。并且更新后的指针应该指向被授权者的下一位而不是当前查找结果的下一一位。同时必须考虑所有请求都无效的情况此时指针应保持不变。参数化设计一个好的调度器应该是可配置的能够通过参数N来设定仲裁的端口数量。这要求我们的代码中不能出现[3:0]这样的硬编码而要使用[WIDTH-1:0]。3. 基于Verilog的参数化RR调度器设计与实现3.1 模块接口定义与设计思路我们首先定义模块的输入输出。一个通用的参数化RR调度器接口如下module rr_arbiter #( parameter N 4 // 仲裁端口数量默认4 )( input wire clk, input wire rst_n, // 异步低电平复位 input wire [N-1:0] req, // 仲裁请求信号位宽N1表示有请求 output reg [N-1:0] gnt, // 仲裁授权信号One-hot编码位宽N output reg valid // 当前周期是否有有效授权至少有一个请求 );设计思路解析gnt采用One-hot编码输出这是行业惯例。因为授权信号通常直接用于使能后续的多路选择器或控制逻辑One-hot编码无需解码使用方便。增加valid信号非常重要。它指示本次仲裁结果是否有效即req不全为0。下游逻辑可以根据valid决定是否采样gnt和执行操作。当req全0时gnt应输出全0valid为0指针保持不变。内部我们需要一个寄存器来存储当前的调度指针ptr_reg位宽为$clog2(N)即编码N个状态所需的最少位数。核心算法双重优先级编码的步骤可以分解为生成掩码根据当前指针ptr_reg生成一个掩码mask该掩码将req中优先级低于当前指针的位屏蔽掉置0。因为RR的规则是“从指针位置开始向后找绕回到开头”。第一级查找在req mask即高优先级段中查找第一个‘1’。如果找到则授权索引就在这个段内。第二级查找如果第一级没找到即高优先级段全0则在req ~mask即低优先级段也就是从0到ptr_reg-1的原始请求中查找第一个‘1’。生成授权根据两级查找的结果计算最终的授权索引grant_index并将其转换为One-hot形式的gnt。更新指针如果本次有有效授权(valid1’b1)则将指针更新为(grant_index 1) % N。3.2 核心逻辑的Verilog实现细节接下来我们分部分实现上述逻辑。首先定义内部信号和寄存器。// 内部信号定义 reg [$clog2(N)-1:0] ptr_reg; // 调度指针寄存器 wire [$clog2(N)-1:0] grant_index; // 本次授权的索引二进制 wire [N-1:0] mask; // 用于分割高低优先级段的掩码 wire [N-1:0] req_masked_high; // 高优先级段请求ptr及之后 wire [N-1:0] req_masked_low; // 低优先级段请求ptr之前 wire [N-1:0] gnt_high, gnt_low; // 高/低段查找产生的One-hot授权临时 wire found_high; // 高优先级段是否找到有效请求 // 指针寄存器更新逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin ptr_reg {$clog2(N){1b0}}; // 复位后指针指向0 end else if (valid) begin // 关键仅在有效授权后更新指针 ptr_reg (grant_index 1) % N; end // 如果valid为0ptr_reg保持原值 end关键点1掩码的生成掩码mask的作用是将所有优先级低于当前指针ptr_reg的请求位屏蔽。假设ptr_reg 2二进制10对于4端口N4我们希望mask为4b1100即第3、2位为1第1、0位为0。这样req mask就得到了索引2和3的请求。 生成掩码有一个非常巧妙的技巧利用循环左移后再取反的位运算但为了清晰我们这里使用一个易于理解的函数来实现// 掩码生成函数生成一个N位的掩码从ptr位到最高位为1其余为0。 // 例如 N4, ptr2 - mask4b1100 function [N-1:0] gen_mask; input [$clog2(N)-1:0] ptr; integer i; begin gen_mask {N{1b0}}; for (i 0; i N; i i 1) begin gen_mask[i] (i ptr) ? 1b1 : 1b0; end end endfunction assign mask gen_mask(ptr_reg);关键点2双重优先级查找这是组合逻辑部分使用for循环实现优先级编码器。注意for循环在Verilog中是可综合的它会被综合工具展开成并行的硬件电路。// 第一级查找在高优先级段(req mask)中找第一个‘1’ always (*) begin gnt_high {N{1b0}}; found_high 1b0; for (int i N-1; i 0; i i - 1) begin // 从高位向低位找实现高位优先 if (mask[i] req[i]) begin // 只检查mask为1的位 gnt_high {N{1b0}}; gnt_high[i] 1b1; found_high 1b1; break; // 找到第一个就退出循环 end end end // 第二级查找如果高段没找到则在低优先级段(req ~mask)中找第一个‘1’ always (*) begin gnt_low {N{1b0}}; if (!found_high) begin for (int i N-1; i 0; i i - 1) begin if (!mask[i] req[i]) begin // 只检查mask为0的位 gnt_low {N{1b0}}; gnt_low[i] 1b1; break; end end end end // 最终的授权信号 assign gnt found_high ? gnt_high : gnt_low;关键点3授权索引与有效信号生成我们需要从One-hot的gnt信号中解码出二进制的grant_index用于更新指针。同时生成valid信号。// 将One-hot授权gnt转换为二进制索引grant_index // 这是一个标准的优先级编码器或称为“独热码转二进制码” always (*) begin grant_index {$clog2(N){1b0}}; for (int i 0; i N; i i 1) begin if (gnt[i]) begin grant_index i[$clog2(N)-1:0]; // 注意位宽转换 end end end // 有效信号只要有一个请求本次仲裁就有效 assign valid (|req); // 这是按位或的归约操作等价于 req ! 03.3 设计整合与模块代码将上述所有部分整合就得到了完整的参数化RR仲裁器模块。为了代码的整洁和可维护性我们可以将优先级查找部分封装成任务或函数但为了教学清晰上面采用了直接描述的方式。这里给出一个整合后的简化版本框架强调关键部分的连接module rr_arbiter #( parameter N 4 )( input wire clk, input wire rst_n, input wire [N-1:0] req, output reg [N-1:0] gnt, output wire valid ); reg [$clog2(N)-1:0] ptr_reg; wire [$clog2(N)-1:0] grant_index; wire [N-1:0] mask; // ... (内部信号声明如上文) // 函数生成掩码 function [N-1:0] gen_mask(input [$clog2(N)-1:0] ptr); // ... 函数体 endfunction assign mask gen_mask(ptr_reg); // 组合逻辑块双重优先级查找 // ... (两个always块生成gnt_high, gnt_low, found_high) // 组合逻辑生成最终gnt和grant_index assign gnt_comb found_high ? gnt_high : gnt_low; always (posedge clk or negedge rst_n) begin if (!rst_n) begin gnt {N{1b0}}; end else begin gnt gnt_comb; // 将授权信号寄存一拍使输出与指针更新同步时序更干净 end end // 组合逻辑从gnt_comb解码grant_index (注意这里用gnt_comb不是寄存后的gnt) always (*) begin grant_index {$clog2(N){1b0}}; for (int i 0; i N; i i 1) begin if (gnt_comb[i]) begin grant_index i; end end end // 指针更新逻辑时序逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin ptr_reg {$clog2(N){1b0}}; end else if (valid) begin // valid基于req判断是组合逻辑 ptr_reg (grant_index 1) % N; end end // 有效信号 assign valid (|req); endmodule重要实操心得在上面的整合代码中我特意将授权信号gnt也寄存了一拍。这是一个非常实用的工程技巧。如果不寄存gnt是纯组合逻辑输出那么grant_index也是组合逻辑指针更新逻辑ptr_reg grant_index 1就会形成一条从req输入经过查找、解码、加法再到ptr_reg的冗长组合路径。这条路径容易成为时序瓶颈。将gnt寄存后grant_index的计算可以基于寄存前的组合信号gnt_comb而指针更新则使用寄存后的grant_index需要额外从gnt解码或直接寄存grant_index这样就将关键路径打断了提高了电路的最高工作频率。代价是授权输出延迟了一个时钟周期这在大多数总线仲裁场景中是可接受的。4. 仿真验证与深度调试如何证明你的调度器是对的写完了RTL代码只是万里长征第一步。没有经过充分验证的硬件代码无异于一堆废铁。对于RR调度器这种功能明确但细节繁多的模块仿真验证至关重要。4.1 搭建SystemVerilog测试平台我们使用SystemVerilog来搭建一个更强大、更自动化的测试平台。主要验证点包括复位后指针为0、无请求时指针保持、单请求持续授权、多请求轮询公平性、请求动态变化、全0请求等。timescale 1ns/1ps module tb_rr_arbiter(); parameter N 4; logic clk 0; logic rst_n; logic [N-1:0] req; logic [N-1:0] gnt; logic valid; // 实例化被测设计 rr_arbiter #(.N(N)) u_dut ( .clk(clk), .rst_n(rst_n), .req(req), .gnt(gnt), .valid(valid) ); // 时钟生成 always #5 clk ~clk; // 100MHz时钟 // 记录授权历史的队列用于后期分析公平性 int grant_history[$]; // 主测试过程 initial begin // 初始化 rst_n 0; req 0; grant_history.delete(); #20 rst_n 1; // 释放复位 // 测试用例1复位后指针为0且无请求时输出全0 #10; if (gnt ! 0 || valid ! 0) $error(Test1 Failed after reset.); else $display(Test1 Pass: Output zeros after reset.); // 测试用例2单一路请求Master0 req 4b0001; repeat(5) (posedge clk); // 观察5个周期 // 预期每个周期gnt都应为4b0001, valid1 foreach (grant_history[i]) begin if (grant_history[i] ! 0) $error(Test2 Failed: Single req not always granted.); end $display(Test2 Pass: Single request sustained grant.); // 测试用例3多路请求轮询经典场景 req 4b1111; // 所有Master同时请求 repeat(20) (posedge clk); // 运行20个周期应能看到清晰的轮询序列 // 简单检查授权历史中0,1,2,3应该循环出现 // 更严谨的做法是写一个自动检查器检查序列是否符合RR规则 check_rr_sequence(grant_history); $display(Test3 Pass: Round-robin sequence verified.); // 测试用例4请求动态变化指针跳转 req 4b0100; // 只有Master2请求 repeat(3) (posedge clk); // 指针应停在Master2并持续授权 req 4b1001; // Master3和Master0请求指针从2开始下一个有效是3 (posedge clk); if (gnt ! 4b1000) $error(Test4 Failed: Pointer jump logic error.); $display(Test4 Pass: Dynamic request handling.); // 测试用例5请求全0指针保持 int saved_ptr; // 可以通过层次化引用或添加调试信号来获取DUT内部的ptr_reg这里假设我们有一个观察接口 // saved_ptr u_dut.ptr_reg; // 需要将ptr_reg在模块中声明为public或通过接口引出 req 4b0000; repeat(5) (posedge clk); // 再次读取ptr_reg应该与saved_ptr相等 // if (u_dut.ptr_reg ! saved_ptr) $error(Test5 Failed: Pointer changed when no request.); $display(Test5: Pointer hold check (requires internal signal access).); $display(All major tests completed.); $finish; end // 在每个时钟沿收集授权信息 always (posedge clk) begin if (rst_n valid) begin int gnt_id; // 将One-hot的gnt转换为ID for (int i0; iN; i) begin if (gnt[i]) gnt_id i; end grant_history.push_back(gnt_id); $display(Cycle %0t: req%4b, gnt%4b (id%0d), valid%b, $time, req, gnt, gnt_id, valid); end end // 任务检查授权历史序列是否符合RR规则简化版 task check_rr_sequence(ref int history[$]); // 这是一个复杂的检查器需要知道每个周期开始的指针位置。 // 更实际的做法是在测试平台中建模一个参考模型与DUT输出实时对比。 $display(Note: RR sequence check is complex, often done with reference model.); endtask endmodule4.2 使用ModelSim/QuestaSim进行调试与波形分析将上述测试平台和RTL代码一起编译仿真打开波形窗口添加关键信号clk,rst_n,req,gnt,valid以及DUT内部的ptr_reg。调试技巧实录观察复位序列确保复位后ptr_reg0,gnt0,valid0。单请求测试设置req4‘b0001观察波形。你应该看到gnt稳定为4‘b0001valid为高且ptr_reg不会在每个周期更新这是一个关键点。因为我们的设计是valid为高有请求时才更新指针。此时指针应该一直指向0但由于授权索引是0ptr_reg会更新为1吗不会因为(01)%41但请注意我们的指针更新逻辑在valid为高时每个周期都执行。所以实际上在单请求持续的情况下指针会在0-1-2-3-0之间循环。但这不影响授权因为查找逻辑总是能找到唯一的请求者Master0。这是RR调度器的一个特性指针仍在“空转”。你可以思考一下这是否是期望的行为在某些优化设计中可能会在只有一路请求时锁定指针以避免不必要的功耗。多请求轮询设置req4‘b1111。这是最关键的测试。在波形中你应该清晰地看到gnt按照0, 1, 2, 3, 0, 1, 2, 3...的顺序循环。同时观察ptr_reg的变化它应该紧随gnt之后指向被授权者的下一位。例如当gnt4‘b0001id0时下一个周期ptr_reg变为1。这验证了指针更新逻辑(grant_index 1) % N的正确性。动态请求与指针跳转这是最容易出BUG的地方。设计一个场景先让req4‘b0010只有Master1请求几个周期指针会停在1附近。然后突然将请求变为req4‘b1001Master3和Master0请求。观察下一个周期的授权。根据RR规则指针从当前位置假设是2因为Master1授权后指针指向2开始找第一个有效请求是Master3因为Master2无效所以gnt应为4‘b1000。仔细核对波形确保查找和掩码逻辑完全正确。覆盖率收集在仿真中可以添加覆盖组来收集代码覆盖率行覆盖、条件覆盖、状态机覆盖和功能覆盖率如各种请求组合、指针状态与请求的组合等。确保你的测试用例触发了所有重要的代码分支和边界情况。4.3 常见问题与排查技巧实录在实际实现和调试中我踩过不少坑这里分享几个最典型的问题仿真时授权信号gnt出现毛刺Glitch。现象在波形上gnt在时钟沿附近有短暂的脉冲不是稳定的0或1。原因gnt是纯组合逻辑输出。当req或内部ptr_reg变化时经过多层查找逻辑for循环、与或门会产生短暂的中间态在时钟采样前未能稳定下来。解决这就是为什么我在最终设计中建议将gnt寄存一拍。将gnt的输出用寄存器打一拍可以消除毛刺输出干净稳定的信号。这符合同步设计原则。代价是输出延迟一个周期需要在系统级考虑这个延迟是否可接受。问题指针更新后授权没有立刻切换到下一个请求者。现象在连续请求下授权顺序不是严格的0,1,2,3...有时会连续授权给同一个主设备两次。排查检查指针更新逻辑的使能条件。必须是valid (|req)吗在我们的设计中valid就是|req所以条件是valid。确保它只在时钟上升沿且条件满足时更新。最关键的一点检查grant_index的来源。如果grant_index来源于寄存后的gnt而指针更新也使用这个grant_index那么就会出现问题本周期用于更新指针的grant_index实际上是上一个周期的授权结果这会导致调度错乱。正确做法指针更新应该基于当前周期新计算出的授权索引。因此要么使用组合逻辑的grant_index_comb如上文代码注释所述要么将组合逻辑的授权索引也寄存一拍但指针更新使用新寄存的值确保时序对齐。仔细分析你的波形看ptr_reg的变化是紧跟当前gnt还是落后一个周期。问题当某一路请求从1变为0时调度出现异常。现象假设req4‘b1111正常轮询。突然将Master1的请求撤除req变为4‘b1101下一个授权可能不是预期的Master2。分析这考验的是“查找下一个有效请求”算法的健壮性。我们的双重查找法应该能正确处理。当指针指向1而Master1请求消失高优先级段位1及以后的第一个有效请求是Master2所以授权给Master2指针更新为3。这是符合RR精神的指针总是指向上次被服务者的下一个位置然后从那里开始找下一个有效的。用你的仿真波形验证这一行为。问题综合后时序违例Setup/Hold Time Violation。现象在综合并做静态时序分析STA后报告显示从req到gnt或到ptr_reg的路径时序紧张。解决流水线化如果N很大比如32、64双重查找逻辑的路径会很长。可以考虑将查找逻辑拆分成两个时钟周期完成即插入流水线寄存器。但这会进一步增加仲裁延迟。优化编码尝试使用One-hot编码的指针和不同的查找电路如使用casez语句或查找表LUT看是否能获得更好的时序。放宽时钟频率如果性能允许降低时钟频率是最直接的办法。使用综合工具指令在关键路径上使用(* keep_hierarchy *)或(* max_delay *)等综合属性引导工具优化。5. 进阶思考与优化方向一个基础的RR调度器实现后我们可以从工程和优化的角度思考更多锁存效应Locking与饥饿避免我们实现的RR是“无权重”且“无锁存”的。在某些协议如PCIe中一旦某个主设备获得授权它可以持续传输多个数据包直到完成或达到某个上限这称为锁存。我们的简单实现每个周期都重新仲裁。如果需要支持锁存需要添加一个计数器或状态机在授权后维持gnt信号并在此期间暂停指针更新。加权轮询Weighted Round-Robin, WRR这是RR的增强版为每个端口分配一个权重值信用值。每次授权后该端口的信用减1信用为0后不再参与本轮调度直到所有端口信用归零后重新分配。这需要为每个端口维护一个信用计数器仲裁逻辑会更复杂但能提供差异化的服务质量。面积与性能的权衡我们的双重查找算法使用了两个嵌套的for循环综合后可能会生成较多的比较器逻辑。对于超大规模N如128需要评估面积开销。另一种常见的硬件优化是使用“仲裁树”Arbiter Tree将一个大仲裁器分解成多层的小仲裁器如先8组16选1再1组8选1形成树状结构可以优化路径延迟和面积。形式化验证Formal Verification对于仲裁器这类控制密集型设计形式化验证是比仿真更强大的工具。你可以使用SVASystemVerilog Assertions定义RR调度器的核心属性例如“任何有效的请求必须在有限周期内获得授权”无饥饿“同一时间最多只有一个授权信号为高”互斥。然后使用形式化验证工具如JasperGold、VC Formal去穷尽所有可能的输入序列证明这些属性永远成立。这能极大提高验证信心。实现一个RR调度器就像练武中的扎马步它巩固了你对硬件并发控制、状态机、时序逻辑和验证方法学的理解。当你看到自己设计的模块在波形中按照预想的公平法则有序运行时那种成就感是纯粹的。希望这篇超详细的拆解能帮你不仅“实现”它更能“吃透”它。下次在项目中遇到仲裁问题你就能自信地说让我来RR调度我熟。
返回列表