3. APB协议详解:APB总线架构、APB状态机、APB传输时序、APB桥接设计
APB,全称Advanced Peripheral Bus,是ARM公司AMBA总线体系中的“轻量级选手”。
说白了,它就是为那些对性能要求不高的外设准备的——比如UART、GPIO、I2C、定时器这类寄存器访问型设备。我刚开始接触SoC设计时,总觉得APB太简单,没什么好学的。后来踩了几个坑才发现,越是简单的东西,越容易在细节上翻车。
3.1 APB总线架构——为什么需要它?
先看一张图,这是我个人习惯画的APB在SoC中的位置:
你看,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的简单协议。
桥接器需要完成以下工作:
- 地址译码:判断AHB/AXI的访问地址是否落在APB外设的地址范围内
- 协议转换:将AHB/AXI的读写请求转换为APB的PSEL、PENABLE、PWRITE等控制信号
- 数据对齐:处理字节使能(如果APB数据宽度小于AHB/AXI)
- 等待处理:将APB的PREADY等待信号反馈到AHB/AXI的HREADY/READY信号
- 错误处理:如果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