CORE学习:AMBA3--APB总线协议及简单例子

AMBA3—APB总线

APB总线是AMBA里面最简单的一个总线接口了,它是一个非流水线结构,且控制逻辑简单,这也就决定了它是利用于低带宽的外围总线设备上,例如UART、IIC、定时器等等。注意,APB还有一个特点就是,APB的主机只有一个,那就是APB总线桥,不可能有其他主机,也不可能有多个主机。

APB状态机

APB总线接口的状态转换图如图所示(ARM IHI 0024B - Page3-2):

APB状态转移图

可见,当总线复位后,APB进入IDLE状态,然后根据控制端口的输入切换状态,输出的数据和状态以及输入数据有关,因此是一个典型的Mealy型状态机,对它的详细解释如下:

  • IDLE:APB 的默认状态,也就是没有传输时候的状态;
  • SETUP:在IDLE状态下,将信号PSELx拉高(x是从设备选择信号),进入SETUP状态,收到PENABLE信号进入下一状态;
  • ACCESS:传输状态,根据PWRITE、PADDR、PWDATA/PRDATA写入/读取寄存器。完成后将PREADY拉高,PSELx未选中这个APB从机的话退回IDLE,选中但不传输数据(PENABLE拉低)的话退回SETUO;未完成的话PREADY拉低,并在下一个上升沿继续进入ACCESS传输数据;

整个状态很简单,三言两语就可以描述清楚了,那我们再回来看看APB接口是由哪些输入输出信号组成的:

1
2
3
4
5
6
7
8
9
10
//apb_interface
input pclk, //时钟
input [AW-1:0] paddr, //地址
input pwrite, //读/写控制信号
input psel, //选择信号,类似于片选信号
input penable, //使能信号
input [DW-1:0] pwdata, //写(输入)数据
output reg [DW-1:0] prdata, //读(输出)数据
output reg pready, //传输完成标志,low未完成,high完成
output reg pslverr //错误标志,high出现错误

传输时序

根据这些信号的不同组合,又可以分为带等待信号的数据输入/输出以及不带等待信号的输入/输出,接下来根据ARM的官方手册ARM IHI 0024B对这四种情况进行描(fan)述(yi):

不带等待信号的数据写入时序

不带等待信号的写入时序

T1上升沿:拉高PSEL,选中这个APB从机,并且拉高PWRITE,表示接下来要写入数据给从机,并且将数据地址放在PADDR,数据放在PWDATA,从IDLE进入SETUP

T2上升沿:PENABLE拉高,从SETUP进入ACCESS,进行一次数据传输

T3上升沿:PREADY被从机拉高,说明传输完成,PSEL和PENABLE可以拉低

带等待信号的写入时序

带等待信号的写入时序

T1上升沿:拉高PSEL,选中这个APB从机,并且拉高PWRITE,表示接下来要写入数据给从机,并且将数据地址放在PADDR,数据放在PWDATA,从IDLE进入SETUP

T2上升沿:PENABLE拉高,从SETUP进入ACCESS,进行一次数据传输

T3上升沿:PREADY为低,说明数据还未写完/过程未处理完,需要等待一个时钟周期,再次进入ACCESS

T4上升沿:PREADY还是低,再次进入ACCESS

T5上升沿:PREADY被拉高,从机完成接受数据,PSEL和PENABLE拉低

读取时序

读取时序的过程与写入时序基本一致,只不过读取时是将PWRITE拉低,而写入是将它拉低,这里不再详细分析时序图,有兴趣可以看官方文档,把图放在这里:

不带等待信号的读取时序

带等待信号的读取时序

动手写一个APB外设

APB外设的逻辑状态和时序已经清楚了,接下来就自己动手写一个APB从机试试吧😍

简单的APB从机

先确定好从机需要访问到的寄存器,在这里我定义四个寄存器来模拟:

1
2
3
4
5
reg [31:0]   readonly32;     //0x10000000,只可读
reg [15:0] readonly16; //0x10000004,只可读
reg [31:0] readwrite32; //0x10000008,可读可写
reg [15:0] readwrite16; //0x1000000C,可读可写

实际上这四个寄存器可以通过改改名字和例化其他模块来实现APB外设的所有功能,比如将readwrite16中的16个位分别设置成UART输出模块的波特率控制位、传输帧设置位、中断使能位等等,readwrite32设置为UART输出信息的缓冲寄存器,这样可以实现一个简单的APB-UART外设

当然,这里我只想模拟下读写寄存器,就不需要例化其他模块了,完整的代码贴在下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
`timescale 1ns/100ps

module apb_slave_test #(
parameter AW = 32,//地址总线宽度
parameter DW = 32 //数据总线宽度
) (
//system
input reset_n,
//apb_interface
input pclk, //时钟
input [AW-1:0] paddr, //地址
input pwrite, //读/写控制信号
input psel, //选择信号
input penable, //使能信号
input [DW-1:0] pwdata, //写(输入)数据
output reg [DW-1:0] prdata, //读(输出)数据
output reg pready, //传输完成标志,low未完成,high完成
output reg pslverr //错误标志,high出现错误
);
//apb状态机
parameter IDLE = 2'b00;
parameter SETUP = 2'b01;
parameter ACCESS = 2'b10;
reg [1:0] state_now = 2'b00;
reg [1:0] state_next = 2'b00;

//映射到总线上的寄存器
reg [31:0] readonly32; //0x10000000,只可读
reg [15:0] readonly16; //0x10000004,只可读
reg [31:0] readwrite32; //0x10000008,可读可写
reg [15:0] readwrite16; //0x1000000C,可读可写

always @(posedge pclk or negedge reset_n) begin: init_and_change_state
if (!reset_n) begin
pready <= 1'b0;
pslverr <= 1'b0;
prdata <= 32'h0;
readwrite32 <= 32'h0;
readwrite16 <= 16'h0;
readonly32 <= 32'h12345678;
readonly16 <= 16'habcd;
state_now <= IDLE;
end
else begin
state_now <= state_next;
end
end

//! fsm_extract
always @(posedge pclk) begin :main_fsm_of_apb
case (state_now)
IDLE: begin //IDLE状态
case (psel)
1'b1: state_next <= SETUP; //选择本apb外设,进入setup状态
1'b0: state_next <= IDLE;
default: state_next <= IDLE;
endcase
end

SETUP: begin//SETUP状态
case (psel)
1'b1: begin
case (penable)
1'b1: state_next <= ACCESS;//选中且enable信号拉高,进入ACCESS模式传输数据
1'b0: state_next <= SETUP;
default: state_next <= SETUP;
endcase
end
1'b0: state_next <= IDLE; //未被选中,回到IDLE
default: state_next <= IDLE;
endcase
pready <= 1'b0;
end

ACCESS: begin //ACCESS状态,根据寄存器地址读写
prdata <= 32'h0;
pslverr <= 1'b0;
if (pwrite) begin //wirte信号拉高,写入模式
case (paddr)
32'h10000008: begin readwrite32 <= pwdata; pready <= 1'b1; end//数据传输完成
32'h1000000C: begin readwrite16 <= pwdata[15:0]; pready <= 1'b1; end//数据传输完成
default: pslverr <= 1'b1;
endcase
end
else if(!pwrite) begin //write信号拉低,读取模式
case (paddr)
32'h10000000: begin prdata <= readonly32; pready <= 1'b1; end//数据传输完成
32'h10000004: begin prdata <= readonly16; pready <= 1'b1; end//数据传输完成
32'h10000008: begin prdata <= readwrite32; pready <= 1'b1; end//数据传输完成
32'h1000000C: begin prdata <= readwrite16; pready <= 1'b1; end//数据传输完成
default: pslverr <= 1'b1;
endcase
end
if (pready) begin
case (psel)
1'b1: state_next <= SETUP; //不需要传输数据,回到SETUP
1'b0: state_next <= IDLE; //未被选中,回到IDLE
default: state_next <= IDLE;
endcase
end
else state_next <= ACCESS; //需要继续传输数据,则回到ACCESS
end
default: state_next <= IDLE;
endcase
end
endmodule

仿真测试一下

利用Vivado跑个行为仿真看看:

仿真结果

一点点来分析:

0ns~15ns:reset_n信号置0以后拉高,初始化APB从机

15ns:拉高了psel,模拟选中这个APB从机,即将进入SETUP状态

35ns:拉高了penable,即将进入ACCESS状态,但我并没有给出paddr以及pwrite,因此输出全0

55ns:pwrite拉高,paddr设为0x10000008,pwdata设为0x1234ffff,表明我即将向地址为0x10000008的寄存器写入0x1234ffff,这个地址的寄存器是readwrite32,可以看到这个时候它的值还是全0

57.5ns:可以看到readwrite32寄存器的值已经变成0x1234ffff,且pready已经置1(其实图中pready早就置1了而且没变过,这个图有bug,后来代码改了)

85ns:pwrite拉低,paddr设为0x10000000,表明我要读取地址为0x10000000的寄存器readonly32的数据

87.5ns:prdata输出0x12345678,这和我们设置的readonly32寄存器初始值相同

145ns:pwrite拉高,paddr设为0x1000000C,pwdata设为0x1234abcd,表明我即将向地址为0x1000000C的16位寄存器写入0x1234ffff,这个地址的寄存器是readwrite16,可以看到这个时候它的值还是全0

147.5ns:readwrite16寄存器的值已经变成0xabcd,成功地写入低16位到寄存器

185ns:尝试读取未定义的寄存器,地址为0x10000010

187.5ns:pslverr被置1,出现错误,功能正常

205ns:尝试读取地址为0x10000004的只读寄存器readonly16

207.5ns:pslverr重新置0,读取到寄存器的值0x0000abcd,功能正常

所以这个简单的APB外设功能应该没问题,有空把它挂到软核/硬核上看看情况☺️

作者

Hank.Gan

发布于

2021-08-24

更新于

2021-08-25

许可协议

评论