免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Verilog实现硬件轮询调度器:从算法原理到RTL设计

Verilog实现硬件轮询调度器:从算法原理到RTL设计 1. 项目概述当硬件逻辑遇上“公平”的艺术在芯片前端设计的江湖里数据流的调度是个永恒的话题。当多个请求源比如几个处理器核心、几个DMA控制器或者几个外设同时嗷嗷待哺都想访问同一个共享资源比如一块内存、一个总线或者一个计算单元时你该怎么办是让某个“关系户”一直霸占着还是搞个先来后到的排队这两种简单粗暴的方式在追求高性能和低延迟的硬件世界里往往都会带来问题。前者可能导致“饿死”——某些请求永远得不到响应后者在请求频率不均时又显得不够灵活。这时候轮询调度也就是我们常说的RR调度就成了一种非常经典且实用的折中方案。RR调度的核心思想就两个字公平。它像一个耐心的裁判让所有等待的请求者排成一个圆圈然后按顺序、一个接一个地提供服务每个请求者服务一次或一个固定的时间片后就轮到下一个如此循环往复。这种机制保证了在足够长的时间窗口内每个请求者都能获得大致相等的服务机会避免了单一请求源的垄断。在芯片内部从总线仲裁、缓存替换策略到多核任务调度RR的身影无处不在。这次我们不谈软件算法而是深入到硅片之上用Verilog这门硬件描述语言亲手打造一个真正的、能在FPGA或ASIC中运行的RR调度器。这不仅仅是写几行代码更是理解硬件如何以并行的、时钟驱动的思维方式去实现一个在软件看来很“顺序”的算法。你会发现用Verilog实现RR就像用乐高积木搭建一个精密的机械钟表每一个触发器、每一根连线都需要你精心设计以确保在每一个时钟沿整个系统都能准确无误地运转。接下来我们就从最核心的需求开始拆解这个硬件调度器的设计与实现。1.1 核心需求解析硬件调度器的独特挑战在软件中实现一个RR调度器你可能用一个链表或数组配合一个指针就能轻松搞定。但在硬件世界里一切都变了。硬件设计首要考虑的是并行性、时序和面积。我们的RR调度器需要满足以下几个硬核需求确定性延迟从输入请求有效到输出授权有效这个延迟必须是确定且尽可能短的。软件调度可能因为操作系统中断而波动但硬件调度器必须在几个时钟周期内给出结果这对系统整体性能至关重要。高吞吐量理想情况下每个时钟周期都能处理一次调度决策。这意味着我们的设计必须是流水线化或组合逻辑优化的不能有复杂的循环依赖。低硬件开销在芯片上每一个触发器FF和查找表LUT都是宝贵的资源。我们的设计需要在满足功能的前提下尽可能节省面积。处理动态请求请求源可能在任何时刻拉高或拉低其请求信号。调度器必须能实时响应这些变化在下一轮调度中立刻排除不再请求的源或者加入新出现的请求源。避免优先级反转一个基础的RR应该是公平的但有时我们可能需要引入“权重”或“优先级”的概念这属于增强型RR。我们首先实现最基础的公平轮询。基于这些需求一个典型的硬件RR调度器接口可以这样定义clkrst_n 全局时钟和低有效复位。req[N-1:0] N位宽的输入请求向量req[i]为1表示第i个请求源当前有请求。grant[N-1:0] N位宽的输出授权向量grant[i]为1表示本轮调度授权给第i个请求源。通常采用独热码即同一时刻只有一位为1。grant_valid 输出有效信号当有任意请求且调度完成时拉高。我们的目标就是设计一个模块在每一个时钟周期根据当前的req和内部维护的“上一次服务指针”计算出本次应该授权的grant信号。1.2 方案选型从朴素算法到高效硬件映射实现RR的硬件算法有很多选择哪种取决于我们对性能、面积和代码可读性的权衡。方案一优先级编码器 指针循环这是最直观的思路。我们可以维护一个指针pointer指向下一个具有最高优先级的请求源假设指针所指的源优先级最高然后序号递增的源优先级依次降低。每一轮调度我们以pointer所指位置为最高优先级对req进行一个“循环优先级编码”。这可以通过将req向量循环左移pointer位然后进行普通的优先级编码如找最低位为1的位再将编码结果加上pointer并取模N来实现。这种方案逻辑清晰但需要循环移位和模运算在硬件上可能不是最省面积的。方案二掩码比较法这是一种更经典、更高效的硬件实现方法也是我们本次重点实现的方案。其核心思想是维护一个上一次被授权的源索引last_grant_id或者一个独热码形式的last_grant向量。将req向量根据last_grant_id分成两部分比last_grant_id序号高的部分和比它低的部分。在硬件上这可以通过生成两个掩码来实现一个“高位掩码”用于屏蔽掉last_grant_id及其之前的位只保留序号更高的请求一个“低位掩码”用于屏蔽掉last_grant_id之后的位只保留序号更低的请求。调度策略是先检查高位部分是否有请求有则授权给其中序号最小的如果高位部分全无请求则检查低位部分授权给其中序号最小的。这完美模拟了“从上一次服务点的下一个位置开始循环查找第一个请求”的RR行为。找到新的授权源后更新last_grant_id。这种方法将循环查找转化为了并行的掩码生成和优先级编码非常契合硬件并行处理的特性关键路径短易于达到高频率。我们将采用这种方案进行实现。注意在真正的工业级设计中尤其是请求源数量N较大比如64时可能会采用“矩阵仲裁”或“并行前缀”等更复杂的结构来优化关键路径。但对于大多数中小规模N16的应用掩码比较法在性能、面积和复杂度上取得了很好的平衡。2. 核心模块设计与接口定义明确了算法我们就可以开始动手画框图、写代码了。一个严谨的设计总是从接口开始。2.1 模块接口与参数化设计我们希望设计一个高度参数化的调度器可以灵活配置请求源的数量。这通过Verilog的parameter来实现。module rr_arbiter #( parameter N 4 // 请求源数量默认为4 )( input wire clk, input wire rst_n, // 低电平有效异步复位 input wire [N-1:0] req, // 输入请求每位代表一个请求源 output reg [N-1:0] grant, // 输出授权独热码 output wire grant_valid // 授权有效信号 );接口信号详解clk和rst_n 标准全局信号。采用异步复位、同步释放的策略是常见且可靠的选择。req[N-1:0] 输入请求。这是一个向量req[i]为1表示第i号请求源在当前周期有请求。它是电平敏感的只要为高就表示该源持续请求。grant[N-1:0] 输出授权。这是一个独热码输出同一时刻只能有一位为1除非无请求则全0。grant[i]为1表示仲裁器决定将资源授权给第i号请求源。使用独热码方便下游逻辑直接使用无需解码。grant_valid 这是一个组合逻辑输出。当req不为零且仲裁逻辑完成计算后该信号拉高指示grant输出有效。这个信号对于连接那些需要“握手”协议的下游模块非常有用。参数化设计的好处 通过parameter N我们可以用同一份RTL代码实例化出仲裁任意数量请求源的RR调度器极大地提高了代码的复用性。在顶层集成时只需要根据实际需求修改N的值即可。2.2 内部状态与关键寄存器为了实现“轮询”模块内部必须记忆“上一次把权限给了谁”。这是调度器唯一需要记忆的状态。// 内部寄存器声明 reg [N-1:0] last_grant; // 上一次的授权结果独热码格式 // 或者另一种存储方式 // reg [$clog2(N)-1:0] last_grant_id; // 上一次授权的源索引二进制两种存储方式各有优劣独热码存储 (last_grant) 优点是生成掩码时非常方便可以直接用于位操作。缺点是消耗的触发器数量为N个当N很大时面积开销较大。索引存储 (last_grant_id) 优点是存储面积小只需要$clog2(N)个触发器。缺点是在生成掩码时需要将索引解码成独热码或者进行更复杂的算术比较增加了一些组合逻辑。对于中小规模N比如8独热码存储因其逻辑简单、时序性好而被广泛采用。我们这里选择独热码存储方式。last_grant在复位时会被清零表示初始状态下没有任何源被服务过调度将从0号源开始查找。3. 仲裁逻辑的硬件实现详解这是整个设计的核心我们将“掩码比较法”用Verilog描述出来。整个仲裁逻辑可以分为组合逻辑部分和时序逻辑部分。3.1 组合逻辑部分实时计算授权组合逻辑负责在每个时钟周期根据当前的req和寄存的last_grant计算出本次应该的grant和grant_valid。注意这部分代码是always *块或assign语句不依赖于时钟。第一步生成掩码我们需要生成两个掩码upper_mask和lower_mask。upper_mask 用于筛选出序号比last_grant所指源更高的请求。假设last_grant是独热码0001表示上次服务了源0那么upper_mask应该是1110屏蔽掉第0位。lower_mask 用于筛选出序号比last_grant所指源更低的请求。接上例lower_mask应该是0000因为0是最低序号没有更低的源了。如果last_grant是0010源1则lower_mask为0001。在Verilog中可以利用位拼接和循环左移/右移来优雅地实现。一种常见技巧是// 假设 last_grant 是独热码 wire [N-1:0] upper_mask {last_grant[N-2:0], 1‘b0}; // 将last_grant左移一位最低位补0 wire [N-1:0] lower_mask ~(upper_mask | last_grant); // 既不是last_grant也不是upper_mask的部分但更通用、更清晰的方法是使用双倍的位宽来避免边界条件判断wire [2*N-1:0] double_req {req, req}; // 将req复制一份拼接起来 wire [2*N-1:0] double_mask ({{N{1‘b1}}, {N{1‘b0}}} (last_grant_id 1)); // last_grant_id需要从独热码解码得到 wire [N-1:0] upper_req double_req[last_grant_id : N] double_mask[last_grant_id : N]; // 然后对 upper_req 进行优先级编码找最低位1这种方法逻辑正确但略显复杂。为了首次实现更直观我们采用一种基于循环的查找描述再让综合器去优化。实际上对于综合工具我们可以直接描述算法行为// 这是一个描述性的行为级代码综合工具会将其映射为相应的硬件电路 always * begin grant {N{1‘b0}}; // 默认授权为0 grant_valid 1‘b0; if (req ! 0) begin // 如果有任何请求 integer i, start; // 将 last_grant 的独热码转换为索引用于查找起点。这里用一个for循环模拟。 start 0; for (i 0; i N; i i 1) begin if (last_grant[i]) start (i 1) % N; // 从下一个位置开始 end // 从start开始循环查找第一个请求 for (int j 0; j N; j j 1) begin int idx (start j) % N; if (req[idx]) begin grant[idx] 1‘b1; grant_valid 1‘b1; break; // 找到第一个就退出 end end end end注意上面的代码是高度可读的行为级描述它清晰地表达了RR算法“从上一次的下一个开始循环找第一个”的语义。现代综合工具如Synopsys DC, Cadence Genus能够很好地识别这种模式并将其综合成与“掩码比较法”性能相近的硬件电路可能是多路选择器链或并行比较树。对于初学者理解算法本质比一开始就纠结于最优门级实现更重要。我们可以先以此为基础实现功能。3.2 时序逻辑部分更新历史状态当时钟上升沿到来时我们需要用本次计算出的grant来更新last_grant寄存器为下一个周期的仲裁做准备。always (posedge clk or negedge rst_n) begin if (!rst_n) begin last_grant {N{1‘b0}}; // 复位时没有历史授权 // 或者可以初始化为 last_grant 1; // 让第一次调度从源0开始查找这取决于你的“循环”定义 end else begin if (grant_valid) begin last_grant grant; // 只有当本次有有效授权时才更新历史记录 end // 如果没有有效授权req全为0则last_grant保持不变 end end关键点last_grant只在grant_valid有效时才更新。如果当前周期没有请求req全0则grant为0grant_valid为0last_grant保持原值。这确保了调度器在请求间歇期能记住上一次服务的位置。3.3 一个完整的Verilog实现示例将组合逻辑和时序逻辑结合起来并优化一下行为级描述我们得到一个完整且可综合的模块module rr_arbiter #( parameter N 4 )( input wire clk, input wire rst_n, input wire [N-1:0] req, output reg [N-1:0] grant, output wire grant_valid ); reg [N-1:0] last_grant; // 组合逻辑计算本次授权 always * begin grant {N{1‘b0}}; grant_valid 1‘b0; if (|req) begin // 简化写法等价于 req ! 0 // 查找 last_grant 中为1的位确定起点 integer start_idx; start_idx 0; // 默认值防止锁存器 for (integer k 0; k N; k k 1) begin if (last_grant[k]) begin start_idx (k 1) % N; end end // 从start_idx开始循环查找 for (integer j 0; j N; j j 1) begin integer idx; idx (start_idx j) % N; if (req[idx]) begin grant[idx] 1‘b1; grant_valid 1‘b1; disable find_grant; // 使用disable退出命名块模拟break end end find_grant: ; // 命名块标签 end end // 时序逻辑更新历史授权记录 always (posedge clk or negedge rst_n) begin if (!rst_n) begin last_grant {N{1‘b0}}; end else begin if (grant_valid) begin last_grant grant; end end end endmodule实操心得代码中的disable find_grant;是一种行为级建模技巧用于跳出循环。在可综合代码中综合工具会理解这个意图并将其转化为适当的硬件结构如优先级编码逻辑。你也可以使用break语句SystemVerilog支持但确保你的工具链支持。另一种更“硬件化”的写法是使用generate块和多个assign语句构建一个多级选择器但对于中等大小的N行为级描述更简洁且综合结果通常不错。4. 功能仿真与测试向量设计代码写完了但绝不能直接上板。没有经过充分仿真的硬件设计就像没经过测试的软件bug百出。我们需要搭建一个测试平台。4.1 编写Testbench我们使用SystemVerilog来编写一个更强大的测试平台它可以方便地产生随机激励并检查结果。timescale 1ns/1ps module tb_rr_arbiter(); parameter N 4; logic clk; logic rst_n; logic [N-1:0] req; logic [N-1:0] grant; logic grant_valid; // 实例化被测设计 rr_arbiter #(.N(N)) u_rr_arbiter ( .clk(clk), .rst_n(rst_n), .req(req), .grant(grant), .grant_valid(grant_valid) ); // 生成时钟 initial begin clk 0; forever #5 clk ~clk; // 100MHz时钟 end // 测试过程 initial begin // 初始化 rst_n 0; req ‘0; #20; rst_n 1; #10; // 测试用例1顺序请求 $display(“[%0t] Test 1: Sequential request“, $time); for (int i 0; i N; i) begin req (1 i); // 独热码只有第i位为1 (posedge clk); #1; // 等待组合逻辑稳定 check_grant(req, grant, grant_valid); // 自定义检查任务 end // 测试用例2随机多个请求 $display(“\n[%0t] Test 2: Random multiple requests“, $time); repeat (20) begin std::randomize(req) with {req inside {[1:(1N)-1]};}; // 随机非零请求 (posedge clk); #1; // 检查授权是否在请求中且是“轮询”预期的那个 // 这里需要根据last_grant的状态来预测略复杂可以事后人工查看波形验证 $display(“ req%4b, grant%4b, valid%b“, req, grant, grant_valid); end // 测试用例3无请求 $display(“\n[%0t] Test 3: No request“, $time); req ‘0; repeat(3) (posedge clk); if (grant ! 0 || grant_valid ! 0) $error(“Error when no request!“); // 测试用例4请求保持观察轮询 $display(“\n[%0t] Test 4: Sustained requests to observe round-robin“, $time); req 4‘b1111; // 所有源持续请求 repeat (10) begin (posedge clk); #1; $display(“ Cycle %0d: grant%4b“, $time/10-1, grant); // 预期看到 grant 依次为 0001, 0010, 0100, 1000, 0001... end #100; $finish; end // 简单的检查任务授权应为独热码且在请求中 task automatic check_grant(input logic [N-1:0] exp_req, input logic [N-1:0] act_grant, input logic act_valid); logic [N-1:0] one_hot_check; one_hot_check act_grant (act_grant - 1); // 检查是否为独热码独热码减1与原码相与结果为0 if (act_valid ! 1‘b1) begin $error(“[%0t] grant_valid should be 1 when req%4b“, $time, exp_req); end else if (one_hot_check ! 0) begin $error(“[%0t] grant is not one-hot! grant%4b“, $time, act_grant); end else if ((act_grant exp_req) 0) begin $error(“[%0t] grant bit not in req! req%4b, grant%4b“, $time, exp_req, act_grant); end else begin $display(“[%0t] PASS: req%4b - grant%4b“, $time, exp_req, act_grant); end endtask endmodule4.2 仿真结果分析与调试使用仿真工具如ModelSim、VCS或开源的Verilator/Icarus Verilog运行上述testbench。关键是通过波形图观察以下行为复位后状态last_grant是否清零第一个请求到来时授权是否给了最低索引的请求源如req0001-grant0001这取决于你复位last_grant的逻辑。如果复位为全0我们的查找逻辑start_idx (k 1) % N在last_grant全0时k循环找不到为1的位start_idx会保持初始值0因此会从索引0开始查找。这符合预期。轮询顺序在测试用例4中当所有源持续请求时波形图上的grant信号应该严格按照0001-0010-0100-1000-0001...的顺序循环变化。每个时钟周期变化一次。动态请求变化在随机测试中观察当req突然变化时grant是否能在一个时钟周期内正确响应并且新的last_grant是否被正确更新。无请求处理当req变为全0时grant和grant_valid是否立刻变为0last_grant是否保持不变调试技巧如果行为不符合预期首先检查你的查找逻辑。一个常见的错误是处理last_grant全0的边界条件。另一个错误是在没有请求时grant_valid没有拉低。仔细查看波形对比req、last_grant、计算出的start_idx和最终的grant一步步追踪逻辑。5. 常见问题、优化与扩展一个基础的RR调度器工作后我们来看看实际项目中会遇到哪些问题以及如何让它变得更强大。5.1 典型问题与排查清单问题现象可能原因排查方法授权grant不是独热码多位同时为1组合逻辑中找到请求后没有及时“跳出”查找循环导致后续符合条件的请求也被赋值。检查行为级描述中的break或disable语句是否生效。或者检查是否写成了多个并行的if语句而没有互斥。grant_valid在无请求时仍为高判断req ! 0的逻辑错误或者grant_valid赋值逻辑没有覆盖所有情况。确保grant_valid的默认值为0并且仅在req ! 0且找到有效grant后才被赋值为1。轮询顺序错乱不“公平”last_grant更新逻辑错误。例如在grant_valid为低时也更新了last_grant。确认last_grant只在grant_valid为高的时钟沿更新。检查复位值是否影响了初始轮询顺序。时序违例建立/保持时间 violation组合逻辑路径太长从req/last_grant变化到grant稳定计算出来的延迟超过了时钟周期。1. 查看综合后的时序报告。2. 考虑将部分逻辑流水线化插入寄存器。3. 对于大的N使用“并行前缀”或“矩阵”仲裁结构来优化关键路径。仿真与综合后行为不一致代码中存在不可综合的语句或对初始化值依赖过多。行为级仿真中的for循环在综合后可能被展开成并行逻辑若逻辑描述有歧义会导致不同。1. 确保所有always块都使用可综合的编码风格。2. 避免使用初始化变量来决定关键路径。3. 进行门级仿真SDF反标以验证综合后网表。5.2 性能优化方向关键路径优化当N很大16时我们行为级描述中的循环查找逻辑综合出来可能是一条很长的选择器链成为时序瓶颈。此时可以采用“并行前缀”算法。基本思想是将N个请求分成两组先组内仲裁再组间仲裁形成一个树状结构将路径长度从O(N)降低到O(logN)。流水线化如果系统时钟频率要求极高可以将仲裁逻辑拆分成两拍。第一拍计算upper_req和lower_req的优先级编码结果第二拍进行最终选择并更新last_grant。这会引入一个周期的调度延迟但能显著提高最高工作频率。面积优化用二进制索引last_grant_id代替独热码last_grant存储可以节省触发器。但需要增加一个索引到独热码的解码器或者修改掩码生成逻辑使用比较器 last_grant_id。5.3 功能扩展加权轮询与饥饿避免基础的RR是平等的但现实世界需要权重。加权轮询为每个请求源分配一个权重计数器。每次授权后该源的计数器减1直到减到0才移出本轮调度。这需要为每个源增加一个计数寄存器和一个比较逻辑面积开销较大。饥饿避免即使是最公平的RR如果一个高优先级源持续有请求低优先级源在它每次被服务后的极短时间内发出请求也可能被“饿”很多轮。一种改进是“老化”机制为每个请求记录等待时间当等待时间超过阈值时临时提升其优先级。实现这些扩展会显著增加设计复杂度需要根据具体应用场景权衡。万变不离其宗其核心仍然是维护状态权重、年龄并基于此状态做出仲裁决策。从一行行代码到一个在时序约束下稳定工作的硬件模块实现一个RR调度器的过程是芯片前端设计思维的典型体现。它要求你将一个抽象的算法精确地映射到时钟驱动的寄存器传输级电路上并仔细考量时序、面积和功能正确性。这个简单的调度器是通往更复杂互连网络、内存控制器和片上系统的基础砖石。当你下次看到grant信号在波形图上规律地跳动时你会知道这背后是一整套关于公平与效率的硬件哲学在默默运转。
返回列表