3. APB协议详解:APB总线架构、APB状态机、APB传输时序、APB桥接设计

APB,全称Advanced Peripheral Bus,是ARM公司AMBA总线体系中的“轻量级选手”。

说白了,它就是为那些对性能要求不高的外设准备的——比如UART、GPIO、I2C、定时器这类寄存器访问型设备。我刚开始接触SoC设计时,总觉得APB太简单,没什么好学的。后来踩了几个坑才发现,越是简单的东西,越容易在细节上翻车。

3.1 APB总线架构——为什么需要它?

先看一张图,这是我个人习惯画的APB在SoC中的位置:

CPU / 处理器 AHB / AXI 高性能总线 DMA DDR控制器 Ethernet APB桥 APB总线 UART GPIO I2C 定时器 处理器 高速总线 APB总线 APB桥

你看,APB在整个SoC中处于“末端”位置。它通过一个APB桥接器挂在AHB或AXI总线上。为什么这么设计?

原因很简单——APB协议本身不支持流水线操作,也没有突发传输。它每次只能做一笔读写,而且至少需要两个时钟周期。你想想看,如果让CPU直接通过APB去访问DDR控制器,那性能就彻底完蛋了。

核心要点:APB的设计哲学就是“简单、低功耗、低成本”。它不需要复杂的仲裁逻辑,也不需要乱序传输支持。对于寄存器配置类操作,APB完全够用。

3.2 APB状态机——三个状态的舞蹈

APB的状态机只有三个状态。我当年第一次看AMBA规范时,觉得这也太简单了吧?但实际做设计时发现,越简单的状态机,越容易在边界条件上出问题。

三个状态分别是:

  • IDLE:空闲状态。总线没有传输在进行。
  • SETUP:建立状态。当有传输请求时,进入此状态。地址和控制信号在此周期被驱动。
  • ACCESS:访问状态。数据在此周期被传输。如果外设需要等待,可以插入等待周期(通过PREADY信号)。

状态转换逻辑其实很简单:

  • IDLE → SETUP:当PSEL有效时
  • SETUP → ACCESS:下一个时钟周期自动跳转
  • ACCESS → IDLE:传输完成(PREADY为高)且没有新的传输请求
  • ACCESS → SETUP:传输完成(PREADY为高)且有新的传输请求(流水线操作)
  • ACCESS → ACCESS:外设还没准备好(PREADY为低),继续等待

嗯,这里要注意一个细节。我曾经在项目中遇到过一个bug:外设的PREADY信号在ACCESS状态被拉低后,设计人员忘记保持地址和控制信号不变。结果外设读到的地址在等待期间发生了变化,导致数据完全错乱。

避坑指南:在APB的ACCESS状态中,如果PREADY为低(插入等待周期),所有地址和控制信号必须保持稳定,不能变化。这是APB协议的基本要求,但很多人会忽略。

3.3 APB传输时序——读写操作详解

咱们直接看时序图。APB的写操作和读操作在时序上基本对称,区别只在于写数据由主机驱动,读数据由从机驱动。

3.3.1 无等待写传输

这是最理想的情况,外设一拍就能响应:

时钟周期:    T0      T1      T2      T3
PCLK      :  _/¯\_/¯\_/¯\_/¯\_/¯\_/¯\_
PSEL      :  ___/¯¯¯¯¯¯¯¯¯¯¯¯\_______
PENABLE   :  _______/¯¯¯¯¯¯\_________
PWRITE    :  ___/¯¯¯¯¯¯¯¯¯¯¯¯\_______
PADDR     :  ___[  Addr  ]___________
PWDATA    :  ___[  Data  ]___________
PREADY    :  ___/¯¯¯¯¯¯¯¯¯¯¯¯\_______

你看,在T0周期(SETUP状态),PSEL被拉高,地址和写数据被驱动。T1周期进入ACCESS状态,PENABLE拉高,同时PREADY为高表示外设准备好了。数据在T1的上升沿被采样。

整个传输只用了两个时钟周期。这就是APB的“标准速度”。

3.3.2 带等待的写传输

如果外设比较慢,比如一个需要多个时钟周期才能完成内部操作的寄存器:

时钟周期:    T0      T1      T2      T3      T4
PCLK      :  _/¯\_/¯\_/¯\_/¯\_/¯\_/¯\_/¯\_
PSEL      :  ___/¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯\___
PENABLE   :  _______/¯¯¯¯¯¯¯¯¯¯¯¯\_____
PWRITE    :  ___/¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯\___
PADDR     :  ___[  Addr  ]______________
PWDATA    :  ___[  Data  ]______________
PREADY    :  ___/¯\___/¯¯¯¯¯¯¯¯\_______

注意看T2周期,PREADY被拉低了。这意味着外设还没准备好。此时PENABLE保持高,地址和数据保持稳定。直到T3周期PREADY变高,传输才完成。

我个人习惯在设计APB从机时,尽量让PREADY在ACCESS状态的第一个周期就拉高。如果实在需要等待,等待周期数最好固定,不要随机变化。否则时序分析会很麻烦。

3.3.3 流水线操作

APB支持一种“背靠背”传输,也就是前一笔传输的ACCESS状态和后一笔传输的SETUP状态重叠:

时钟周期:    T0      T1      T2      T3      T4
PCLK      :  _/¯\_/¯\_/¯\_/¯\_/¯\_/¯\_/¯\_
PSEL      :  ___/¯¯¯¯¯¯¯¯\___/¯¯¯¯¯¯\___
PENABLE   :  _______/¯¯¯¯\_______/¯¯¯¯\_
PWRITE    :  ___/¯¯¯¯\___/¯¯¯¯\_________
PADDR     :  ___[A0]___[A1]____________
PWDATA    :  ___[D0]___[D1]____________
PREADY    :  ___/¯¯¯¯¯¯¯¯\___/¯¯¯¯¯¯\___

在T2周期,前一笔传输(地址A0)在ACCESS状态完成,同时下一笔传输(地址A1)的SETUP状态已经开始。这样能提高总线利用率。

不过要注意,流水线操作要求从机必须能在两个周期内完成一笔传输。如果从机需要插入等待周期,流水线就会被破坏。

经验之谈:我在设计APB桥接器时,通常会实现一个深度为1的写缓冲。这样即使APB从机需要等待,桥接器也能提前接收来自AHB/AXI的下一笔写请求,提高整体吞吐量。

3.4 APB桥接设计——从AHB到APB的翻译官

APB桥接器是连接高速总线(AHB/AXI)和APB总线的关键模块。它的核心功能就是“翻译”——把高速总线的复杂协议翻译成APB的简单协议。

桥接器需要完成以下工作:

  1. 地址译码:判断AHB/AXI的访问地址是否落在APB外设的地址范围内
  2. 协议转换:将AHB/AXI的读写请求转换为APB的PSEL、PENABLE、PWRITE等控制信号
  3. 数据对齐:处理字节使能(如果APB数据宽度小于AHB/AXI)
  4. 等待处理:将APB的PREADY等待信号反馈到AHB/AXI的HREADY/READY信号
  5. 错误处理:如果APB从机返回错误(PSLVERR),需要转换为AHB/AXI的错误响应

下面是一个简化的APB桥接器Verilog代码示例:

module ahb_to_apb_bridge (
    // AHB接口
    input  wire        HCLK,
    input  wire        HRESETn,
    input  wire        HSEL,
    input  wire [31:0] HADDR,
    input  wire [31:0] HWDATA,
    input  wire        HWRITE,
    input  wire [1:0]  HTRANS,
    output reg  [31:0] HRDATA,
    output reg         HREADYOUT,
    output reg         HRESP,
    
    // APB接口
    output reg         PSEL,
    output reg         PENABLE,
    output reg         PWRITE,
    output reg  [31:0] PADDR,
    output reg  [31:0] PWDATA,
    input  wire [31:0] PRDATA,
    input  wire        PREADY,
    input  wire        PSLVERR
);

    // 状态定义
    typedef enum logic [1:0] {
        IDLE   = 2'b00,
        SETUP  = 2'b01,
        ACCESS = 2'b10
    } state_t;
    
    state_t state, next_state;
    
    // 状态机
    always_ff @(posedge HCLK or negedge HRESETn) begin
        if (!HRESETn)
            state <= IDLE;
        else
            state <= next_state;
    end
    
    // 下一状态逻辑
    always_comb begin
        next_state = state;
        case (state)
            IDLE: begin
                if (HSEL && (HTRANS == 2'b10 || HTRANS == 2'b11))
                    next_state = SETUP;
            end
            SETUP: begin
                next_state = ACCESS;
            end
            ACCESS: begin
                if (PREADY) begin
                    if (HSEL && (HTRANS == 2'b10 || HTRANS == 2'b11))
                        next_state = SETUP;  // 流水线
                    else
                        next_state = IDLE;
                end else begin
                    next_state = ACCESS;  // 等待
                end
            end
        endcase
    end
    
    // 输出逻辑
    always_ff @(posedge HCLK or negedge HRESETn) begin
        if (!HRESETn) begin
            PSEL    <= 1'b0;
            PENABLE <= 1'b0;
            PADDR   <= 32'b0;
            PWDATA  <= 32'b0;
            PWRITE  <= 1'b0;
            HREADYOUT <= 1'b1;
            HRESP   <= 1'b0;
        end else begin
            case (next_state)
                IDLE: begin
                    PSEL    <= 1'b0;
                    PENABLE <= 1'b0;
                    HREADYOUT <= 1'b1;
                end
                SETUP: begin
                    PSEL    <= 1'b1;
                    PENABLE <= 1'b0;
                    PADDR   <= HADDR;
                    PWDATA  <= HWDATA;
                    PWRITE  <= HWRITE;
                    HREADYOUT <= 1'b0;  // 等待APB完成
                end
                ACCESS: begin
                    PENABLE <= 1'b1;
                    if (PREADY) begin
                        HREADYOUT <= 1'b1;
                        HRDATA <= PRDATA;
                        HRESP  <= PSLVERR;
                    end
                end
            endcase
        end
    end

endmodule

这段代码我简化了很多细节,但核心逻辑都在了。你注意看HREADYOUT信号的处理——在SETUP状态,桥接器会拉低HREADYOUT,告诉AHB主机“我还没准备好”。直到APB的ACCESS状态完成,HREADYOUT才被拉高。

我曾经在一个项目中,因为忘记在SETUP状态拉低HREADYOUT,导致AHB主机认为传输已经完成,实际上APB还没开始处理。结果就是数据丢失,调试了整整两天才找到问题。

设计要点总结:

  • APB桥接器必须正确处理HREADY的反压机制
  • 地址译码要覆盖所有APB外设的地址空间,不要有空洞
  • 如果APB数据宽度小于AHB,需要实现数据宽度转换和字节对齐
  • PSLVERR信号必须传递到AHB的HRESP,不能忽略

3.5 实际项目中的APB设计建议

最后,分享几个我在实际项目中积累的经验:

  • APB时钟域:APB通常和AHB/AXI使用同一个时钟域,不需要跨时钟域处理。但如果APB外设工作在更低频率,就需要在桥接器中插入同步器。
  • 地址对齐:APB外设的寄存器地址最好按32位对齐。虽然APB支持字节访问,但不对齐的地址会增加译码逻辑的复杂度。
  • PREADY默认值:如果APB从机不需要等待周期,PREADY应该默认拉高。不要用组合逻辑产生PREADY,否则容易产生毛刺。
  • PSEL的用法:PSEL是“片选”信号,不是“使能”信号。它表示当前总线周期正在访问这个从机。从机应该在PSEL有效时采样地址和控制信号。

重要提醒:APB协议规定,从机不能在SETUP状态改变PREADY的值。PREADY只能在ACCESS状态被拉低。违反这个规则会导致总线协议违规,仿真时可能会报错。

好了,APB协议的核心内容就这些。它虽然简单,但却是SoC中不可或缺的一环。下次你看到UART或GPIO的寄存器配置代码时,可以想想背后APB总线是怎么工作的——嗯,其实也没那么复杂,对吧?


公众号:蓝海资料掘金营,微信deep3321