第2章 轮询仲裁(Round-Robin):原理与实现、公平性分析、在AHB/APB总线中的应用

大家好,欢迎来到第二章。上一章我们聊了总线仲裁的基本概念,今天咱们深入一个最常用的仲裁算法——轮询仲裁,也就是 Round-Robin。

为什么说它常用?因为它在“公平”和“效率”之间找到了一个不错的平衡点。我做了这么多年总线设计,可以说,80% 的多主设备场景,第一版原型我都会先用轮询。为什么?因为它简单、可靠,而且不容易饿死谁。

2.1 轮询仲裁的核心思想

说白了,轮询仲裁就像老师上课点名回答问题。老师手里有个名单,按顺序一个一个叫。叫完最后一个,再回到第一个重新开始。每个学生都有机会,谁也别想插队。

在总线里,这个“老师”就是仲裁器,“学生”就是各个主设备(Master)。仲裁器维护一个指针,叫“当前优先级指针”或者“轮询指针”。每次仲裁结束后,这个指针就移到下一个主设备。

核心原则: 每个主设备获得总线控制权的机会均等,没有哪个设备能一直霸占总线。

你想想看,如果不用轮询,用固定优先级,那低优先级的设备可能永远抢不到总线。这在很多实时系统里是致命的。我有个项目,早期用了固定优先级,结果一个低速传感器死活拿不到总线,数据一直丢。换成轮询,问题立刻解决。

2.2 轮询仲裁的实现方式

实现轮询仲裁,主要有两种方式:硬件轮询和软件轮询。咱们做硬件架构的,当然主要关注硬件实现。

2.2.1 硬件轮询仲裁器结构

一个典型的硬件轮询仲裁器,核心是一个循环移位寄存器和一个优先级编码器。

我画个图,你一看就明白。

轮询仲裁器硬件结构图 轮询仲裁器硬件结构 Master 0 请求 req[0] Master 1 请求 req[1] Master 2 请求 req[2] Master 3 请求 req[3] 轮询指针寄存器 (Pointer) 当前优先级:Master 2 仲裁逻辑 从指针位置开始,按顺序查找请求 授权输出 grant[2] 指针更新 每次仲裁完成后,指针移动到下一个主设备(若当前是 Master 2,则下一个是 Master 3)

这个图里,流程很清楚:

  1. 请求输入:所有主设备把请求信号(req)发给仲裁器。
  2. 轮询指针:仲裁器内部有个指针,记录当前谁有最高优先级。
  3. 仲裁逻辑:从指针指向的设备开始,按顺序检查谁有请求。找到第一个有请求的,就授权给它。
  4. 指针更新:授权完成后,指针移到下一个设备。
小技巧: 我个人习惯用格雷码实现轮询指针,这样指针切换时只变化一位,能减少毛刺和功耗。在低功耗设计里,这个小细节挺管用。

2.2.2 代码实现示例

咱们用 Verilog 写一个简单的 4 主设备轮询仲裁器。代码不长,但核心逻辑都在里面。

module round_robin_arbiter (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [3:0]  req,        // 4个主设备的请求
    output reg  [3:0]  grant,      // 授权输出,独热码
    output reg         grant_valid // 授权有效信号
);

    reg [1:0] pointer;  // 轮询指针,2位,指向当前最高优先级

    // 仲裁逻辑:从指针位置开始,循环查找第一个请求
    always @(*) begin
        grant = 4'b0;
        grant_valid = 1'b0;
        
        case (pointer)
            2'b00: begin
                if (req[0]) begin grant = 4'b0001; grant_valid = 1'b1; end
                else if (req[1]) begin grant = 4'b0010; grant_valid = 1'b1; end
                else if (req[2]) begin grant = 4'b0100; grant_valid = 1'b1; end
                else if (req[3]) begin grant = 4'b1000; grant_valid = 1'b1; end
            end
            2'b01: begin
                if (req[1]) begin grant = 4'b0010; grant_valid = 1'b1; end
                else if (req[2]) begin grant = 4'b0100; grant_valid = 1'b1; end
                else if (req[3]) begin grant = 4'b1000; grant_valid = 1'b1; end
                else if (req[0]) begin grant = 4'b0001; grant_valid = 1'b1; end
            end
            2'b10: begin
                if (req[2]) begin grant = 4'b0100; grant_valid = 1'b1; end
                else if (req[3]) begin grant = 4'b1000; grant_valid = 1'b1; end
                else if (req[0]) begin grant = 4'b0001; grant_valid = 1'b1; end
                else if (req[1]) begin grant = 4'b0010; grant_valid = 1'b1; end
            end
            2'b11: begin
                if (req[3]) begin grant = 4'b1000; grant_valid = 1'b1; end
                else if (req[0]) begin grant = 4'b0001; grant_valid = 1'b1; end
                else if (req[1]) begin grant = 4'b0010; grant_valid = 1'b1; end
                else if (req[2]) begin grant = 4'b0100; grant_valid = 1'b1; end
            end
        endcase
    end

    // 指针更新:每次仲裁有效后,指针指向下一个
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            pointer <= 2'b00;
        end else if (grant_valid) begin
            pointer <= pointer + 1'b1;  // 循环加1
        end
    end

endmodule

这段代码里,我用了 case 语句实现循环优先级查找。你可能会问:为什么不用 for 循环?嗯,在硬件里,for 循环综合出来可能不是你想要的样子。这种显式的 case 写法,综合工具能更好地优化。

注意: 上面的代码是简化版,实际项目中还要考虑:
  • 请求信号同步(跨时钟域处理)
  • 总线占用超时处理(防止某个设备死锁)
  • 低功耗优化(时钟门控)
我曾经在一个项目里忘了加超时处理,结果一个 DMA 控制器出错了,一直占着总线不放,整个系统都卡死了。从那以后,我每个仲裁器都会加一个 watchdog 定时器。

2.3 公平性分析

轮询仲裁最大的优点就是公平。但公平也分很多种,咱们得仔细聊聊。

2.3.1 什么是真正的公平?

在总线仲裁里,公平通常指:每个主设备在长期运行中,获得总线访问权的概率相等。

轮询仲裁能做到这一点吗?理论上可以。但实际中,有几个因素会影响公平性:

影响因素 影响方式 严重程度
请求频率差异 高频请求的设备看起来“抢”到的次数多 低(轮询本身不偏向)
传输长度差异 一次传输占用的时钟周期不同 中(需要加权轮询)
指针更新时机 在传输开始还是结束时更新指针 高(设计细节决定)
优先级重置 是否允许高优先级打断 高(破坏公平性)

你看,公平性不是非黑即白的。我见过一个设计,指针在传输开始时就更新,结果一个设备刚拿到总线,下一个设备立刻变成最高优先级。如果这个设备一直有请求,它就能连续拿到总线。这其实破坏了公平性。

我的建议: 指针应该在传输结束时更新,而不是在传输开始时。这样能保证每个设备完成一次完整传输后,才把机会让给别人。

2.3.2 加权轮询(Weighted Round-Robin)

有时候,我们需要“不公平”。比如,一个视频处理单元需要更高的带宽,而一个配置寄存器访问频率很低。这时候,可以用加权轮询。

加权轮询的原理很简单:给每个主设备分配一个权重值,权重高的设备在一次轮询周期里可以多次获得总线。

举个例子:

  • Master A:权重 3(每次轮询可以拿 3 次总线)
  • Master B:权重 2
  • Master C:权重 1

轮询顺序就是:A, A, A, B, B, C, 然后重复。这样 A 的带宽是 C 的 3 倍。

我在一个视频编解码芯片里用过加权轮询。编码器需要大量带宽,解码器次之,CPU 只需要偶尔配置一下。权重配成 4:2:1,效果很好。

2.4 在AHB/APB总线中的应用

好,咱们回到实际的总线协议。AHB 和 APB 是 ARM 公司 AMBA 总线体系里的两个重要成员。

2.4.1 AHB 总线中的轮询仲裁

AHB 总线支持多个主设备,仲裁器是必须的。AHB 协议本身没有规定用哪种仲裁算法,所以轮询仲裁是常见选择。

在 AHB 里,仲裁发生在以下几个时刻:

  1. 地址相位开始:仲裁器根据当前请求,决定下一个时钟周期谁可以驱动地址和控制信号。
  2. 传输结束:当 HREADY 为高时,表示当前传输完成,仲裁器可以切换主设备。
  3. 分裂传输(Split):如果从设备发出 SPLIT 响应,仲裁器可以临时把总线让给其他主设备。

我画个 AHB 轮询仲裁的时序图,帮你理解。

AHB轮询仲裁时序图 AHB 轮询仲裁时序示例 HCLK T0 T1 T2 T3 T4 T5 HBUSREQ[0] HBUSREQ[1] HGRANT[0] HGRANT[1] HADDR Master 0 地址 Master 1 地址 HRDATA Master 0 数据 Master 1 数据 Master 0 在 T0 请求,T1 获得授权,T2 开始传输;Master 1 在 T2 请求,T3 获得授权,T4 开始传输

这个时序图展示了两个主设备轮流获得总线的情况。注意看,Master 0 在 T0 发起请求,T1 获得授权,T2 开始传输地址。Master 1 在 T2 发起请求,T3 获得授权,T4 开始传输。这就是轮询的典型表现。

2.4.2 APB 总线中的特殊情况

APB 总线比较特殊。它通常只有一个主设备(一般是 AHB 到 APB 的桥),所以不需要仲裁。但如果你在 APB 上挂了多个主设备(比如某些 SoC 设计),轮询仲裁也是可行的。

不过,APB 的时序比 AHB 简单,没有流水线。所以 APB 上的轮询仲裁实现起来更直接:

  • 每个 APB 传输需要两个时钟周期(SETUP 和 ACCESS)
  • 仲裁可以在 SETUP 相位进行
  • 传输完成后立即切换
实战经验: 我在一个 IoT 芯片里,把 APB 总线挂了个 DMA 控制器和几个外设。APB 桥里集成了一个简单的轮询仲裁器。因为 APB 速度慢(通常几十 MHz),仲裁逻辑用组合逻辑实现,不占什么面积。

2.5 轮询仲裁的优缺点总结

咱们来做个总结,把轮询仲裁的优缺点摆出来。

优点 缺点
公平性好,不会饿死任何设备 延迟不确定,高优先级请求不能立即响应
实现简单,硬件开销小 带宽利用率不是最优(需要等待轮询)
可预测性强,适合实时系统 不支持紧急请求(除非加优先级打断)
容易扩展,增加主设备只需修改指针宽度 所有设备获得相同机会,无法差异化服务

你看,没有完美的仲裁算法。轮询仲裁适合那些对公平性要求高、对延迟不太敏感的场景。如果你的系统里有一个设备需要紧急响应,那就得考虑优先级仲裁或者混合仲裁了。

选型建议:
  • 所有设备重要性相同 → 轮询仲裁
  • 有明确优先级需求 → 固定优先级仲裁
  • 需要兼顾公平和紧急 → 加权轮询 + 优先级打断

好了,这一章的内容就到这里。轮询仲裁虽然基础,但用好它并不容易。我建议你在做设计时,先画清楚时序图,再写代码。很多时候,时序图画清楚了,代码就是翻译工作。