前言
APB是AMBA总线的一部分,主要用于低带宽的外设连接,比如UART、SPI等。
需要设计一个APB接口,包含必要的信号,比如PCLK(时钟)、PRESETn(复位)、PADDR(地址)、PWRITE(读写控制)、PWDATA(写数据)、PRDATA(读数据)、PSELx(选择信号)、PENABLE(使能信号)和PREADY(传输完成)。
APB接口应该包括modport定义,区分Master和Slave的端口方向。时钟块用于同步Master和Slave的信号,比如在时钟上升沿驱动和采样。
示例一:APB3总线接口设计(1)
1. APB接口定义
`timescale 1ns/1ps
// APB3协议接口定义:ml-citation{ref="2" data="citationList"}
interface apb3_interface(input logic pclk, presetn);
logic [31:0] paddr; // 地址总线
logic pwrite; // 写使能
logic [31:0] pwdata; // 写数据
logic [31:0] prdata; // 读数据
logic psel; // 外设选择
logic penable; // 传输使能
logic pready; // 传输完成标志
// 主设备时钟块(驱动端时序控制):ml-citation{ref="2" data="citationList"}
clocking master_cb @(posedge pclk);
output #1ns paddr, pwrite, pwdata, psel;
input #2ns prdata, pready;
output penable;
endclocking
// 从设备时钟块(采样端时序控制):ml-citation{ref="2" data="citationList"}
clocking slave_cb @(posedge pclk);
input #1ns paddr, pwrite, pwdata, psel;
output #2ns prdata;
input penable;
output pready;
endclocking
endinterface
2. 简单Slave设备实现
module apb_slave(
apb3_interface.slave_cb apb
);
reg [31:0] mem [0:1023]; // 1KB寄存器空间
always_ff @(posedge apb.pclk or negedge apb.presetn) begin
if (!apb.presetn) begin
apb.prdata <= 32'h0;
apb.pready <= 1'b0;
end else if (apb.psel && apb.penable) begin
if (apb.pwrite) begin
mem[apb.paddr[11:2]] <= apb.pwdata; // 4字节对齐
apb.pready <= 1'b1;
end else begin
apb.prdata <= mem[apb.paddr[11:2]];
apb.pready <= 1'b1;
end
end
end
endmodule
3. 测试平台
module apb_tb;
logic pclk = 0;
logic presetn;
// 生成100MHz时钟:ml-citation{ref="3" data="citationList"}
always #5 pclk = ~pclk;
// 实例化接口和DUT
apb3_interface apb_if(pclk, presetn);
apb_slave dut(apb_if.slave_cb);
// 主设备驱动任务
task apb_write(input [31:0] addr, data);
@(apb_if.master_cb);
apb_if.master_cb.psel <= 1;
apb_if.master_cb.paddr <= addr;
apb_if.master_cb.pwrite <= 1;
apb_if.master_cb.pwdata <= data;
apb_if.master_cb.penable <= 0;
@(apb_if.master_cb);
apb_if.master_cb.penable <= 1;
while (!apb_if.master_cb.pready) @(apb_if.master_cb);
apb_if.master_cb.psel <= 0;
apb_if.master_cb.penable <= 0;
endtask
// 测试激励:ml-citation{ref="1" data="citationList"}
initial begin
presetn = 0;
#20 presetn = 1;
apb_write(32'h0000_1000, 32'h1234_5678);
apb_write(32'h0000_1004, 32'h9ABC_DEF0);
#50 $finish;
end
// 波形记录:ml-citation{ref="3" data="citationList"}
initial begin
$shm_open("apb_waves.shm");
$shm_probe(apb_tb, "AS");
end
endmodule
4. Xcelium运行脚本
#!/bin/bash
xrun -64bit \
-access +rwc \
-xmlibdirpath ./xcelium_apb \
-input wave_cfg.tcl \
apb3_interface.sv apb_slave.sv apb_tb.sv
波形配置 (wave_cfg.tcl)
database -open waves -shm
probe -create -database waves -all -depth all
run
5. 仿真验证
执行命令:
chmod +x xrun_apb.sh
./xrun_apb.sh
预期波形特征:
- Setup Phase:PSEL=1且PENABLE=0
- Access Phase:PSEL=1且PENABLE=1
- 数据传输:在PREADY=1时完成写操作
时序验证点:
- 地址相位对齐(paddr变化在时钟上升沿后1ns)
- 写数据在penable有效后保持稳定
- pready响应符合协议时序
关键设计原理
- 接口封装:通过interface整合APB3协议信号,实现模块化连接
- 时钟块同步:使用clocking block确保主从设备时序隔离
- 协议状态机:Slave设备实现PSEL→PENABLE→PREADY状态流转
- 地址对齐:paddr[11:2]实现4字节寻址(32-bit数据总线)
扩展建议
- 增加覆盖率:添加covergroup统计地址分布和操作类型
- 错误注入:在Slave中模拟PREADY延迟响应场景
- 协议检查器:使用assert验证APB时序规则
示例二:APB3总线驱动时钟设计(2)
1. 接口定义与时钟块
`timescale 1ns/1ps
interface apb3_if(input logic pclk, presetn);
// 核心信号组:ml-citation{ref="1,3" data="citationList"}
logic [31:0] paddr;
logic pwrite;
logic [31:0] pwdata;
logic [31:0] prdata;
logic psel;
logic penable;
logic pready;
// 主设备时钟驱动策略:ml-citation{ref="2,5" data="citationList"}
clocking master_cb @(posedge pclk);
output #1ns paddr, pwrite, pwdata, psel; // 上升沿后1ns驱动
input #2ns pready, prdata; // 上升沿前2ns采样
output penable;
endclocking
// 从设备时钟采样策略:ml-citation{ref="3,4" data="citationList"}
clocking slave_cb @(posedge pclk);
input #1ns paddr, pwrite, pwdata, psel; // 上升沿前1ns采样
output #2ns prdata; // 上升沿后2ns驱动
input penable;
output pready;
endclocking
endinterface
2. 从设备实现
module apb_slave(
apb3_if.slave_cb apb
);
reg [31:0] mem[0:1023];
always_ff @(posedge apb.pclk or negedge apb.presetn) begin
if (!apb.presetn) begin
apb.pready <= 0;
apb.prdata <= 32'h0;
end else if (apb.psel && apb.penable) begin
apb.pready <= 1; // 固定1周期等待:ml-citation{ref="4" data="citationList"}
if (apb.pwrite)
mem[apb.paddr[11:0]] <= apb.pwdata;
else
apb.prdata <= mem[apb.paddr[11:0]];
end
end
endmodule
3. 测试平台与时钟驱动
module apb_tb;
logic pclk = 0;
logic presetn;
// 100MHz时钟生成:ml-citation{ref="3,5" data="citationList"}
always #5 pclk = ~pclk; // 周期10ns
apb3_if apb_if(pclk, presetn);
apb_slave dut(apb_if.slave_cb);
// 主设备驱动任务
task apb_transfer(
input [31:0] addr,
input [31:0] data,
input write_en
);
// Setup Phase:ml-citation{ref="4" data="citationList"}
@(apb_if.master_cb);
apb_if.master_cb.psel <= 1;
apb_if.master_cb.paddr <= addr;
apb_if.master_cb.pwrite <= write_en;
apb_if.master_cb.pwdata <= data;
apb_if.master_cb.penable <= 0;
// Access Phase:ml-citation{ref="1,4" data="citationList"}
@(apb_if.master_cb);
apb_if.master_cb.penable <= 1;
// 等待传输完成
wait(apb_if.master_cb.pready);
@(apb_if.master_cb);
apb_if.master_cb.psel <= 0;
apb_if.master_cb.penable <= 0;
endtask
initial begin
presetn = 0;
#20 presetn = 1; // 复位释放:ml-citation{ref="5" data="citationList"}
// 写操作示例
apb_transfer(32'h0000_1000, 32'hA5A5_A5A5, 1);
// 读操作示例
apb_transfer(32'h0000_1000, 32'h0, 0);
#50 $finish;
end
endmodule
4. 仿真运行命令
xrun -64bit \
-access +rwc \
-xmlibdirpath ./xcelium_apb \
-input wave_cfg.tcl \
apb3_if.sv apb_slave.sv apb_tb.sv
波形配置 (wave_cfg.tcl):
database -open waves -shm
probe -create -database waves -all -depth all
run
5. 关键时序验证
- 时钟驱动验证
- 时钟周期10ns(100MHz)
- 复位信号在时钟上升沿后20ns释放
- 协议时序检查
- 波形特征示例
Time(ns) | 事件
------------------------------
25 | PSEL上升沿(Setup开始)
35 | PENABLE上升沿(Access开始)
45 | PREADY变高(传输完成)
6. 设计原理说明
时钟驱动策略
- 主设备输出信号在时钟上升沿后1ns变化
- 从设备采样信号在时钟上升沿前1ns稳定
协议状态机
graph LR
IDLE -->|传输请求| SETUP
SETUP -->|下一个时钟| ACCESS
ACCESS -->|PREADY=1| IDLE
地址对齐机制
- 使用paddr[11:0]实现4KB地址空间映射,符合APB外设典型设计
扩展建议
动态时钟控制
// 可配置时钟频率
real clock_freq = 100e6;
always #(0.5/clock_freq*1e9) pclk = ~pclk; // 动态调整:ml-citation{ref="5" data="citationList"}
协议断言检查
assert property (@(posedge apb_if.pclk)
apb_if.psel |-> ##1 apb_if.penable)
else $error("协议违规:PENABLE未在PSEL后置位"); // :ml-citation{ref="4,5" data="citationList"}
示例三:APB总线接口设计(3)
// apb_interface.sv
interface apb_if(input logic pclk);
logic [31:0] paddr;
logic psel;
logic penable;
logic pwrite;
logic [31:0] pwdata;
logic [31:0] prdata;
logic pready;
logic pslverr;
modport master (
output paddr, psel, penable, pwrite, pwdata,
input prdata, pready, pslverr
);
modport slave (
input paddr, psel, penable, pwrite, pwdata,
output prdata, pready, pslverr
);
endinterface
// apb_master.sv
module apb_master(apb_if.master bus);
typedef enum {IDLE, SETUP, ACCESS} state_t;
state_t state = IDLE;
task automatic write(input [31:0] addr, data);
bus.paddr = addr;
bus.pwrite = 1'b1;
bus.pwdata = data;
bus.psel = 1'b1;
bus.penable = 1'b0;
state = SETUP;
@(posedge bus.pclk);
bus.penable = 1'b1;
state = ACCESS;
wait(bus.pready);
@(posedge bus.pclk);
bus.psel = 1'b0;
bus.penable = 1'b0;
state = IDLE;
endtask
task automatic read(input [31:0] addr, output [31:0] data);
bus.paddr = addr;
bus.pwrite = 1'b0;
bus.psel = 1'b1;
bus.penable = 1'b0;
state = SETUP;
@(posedge bus.pclk);
bus.penable = 1'b1;
state = ACCESS;
wait(bus.pready);
data = bus.prdata;
@(posedge bus.pclk);
bus.psel = 1'b0;
bus.penable = 1'b0;
state = IDLE;
endtask
endmodule
// apb_slave.sv
module apb_slave(apb_if.slave bus);
logic [31:0] mem [0:255];
always @(posedge bus.pclk) begin
if(bus.psel && bus.penable) begin
bus.pready <= 1'b1;
if(bus.pwrite) begin
mem[bus.paddr] <= bus.pwdata;
bus.pslverr <= 1'b0;
end else begin
bus.prdata <= mem[bus.paddr];
bus.pslverr <= (bus.paddr > 255) ? 1'b1 : 1'b0;
end
end else begin
bus.pready <= 1'b0;
end
end
endmodule
// tb.sv
module tb;
logic pclk;
// 生成50MHz时钟
initial begin
pclk = 0;
forever #5ns pclk = ~pclk;
end
// 实例化接口
apb_if bus(pclk);
// 连接DUT
apb_master master(bus);
apb_slave slave(bus);
initial begin
logic [31:0] rd_data;
// 初始化
master.bus.psel = 0;
master.bus.penable = 0;
// 写操作测试
master.write(32'h0000_0010, 32'h1234_5678);
master.write(32'h0000_0020, 32'hdead_beef);
// 读操作测试
master.read(32'h0000_0010, rd_data);
master.read(32'h0000_0020, rd_data);
// 错误测试
master.read(32'h0000_1000, rd_data); // 触发slverr
#100ns;
$finish;
end
endmodule
1. 仿真运行指令:
使用Cadence Xcelium(xrun):
xrun -sv tb.sv apb_interface.sv apb_master.sv apb_slave.sv -access +rw -input run.tcl
创建run.tcl文件:
# run.tcl
run
exit
使用Synopsys VCS:
vcs -sverilog tb.sv apb_interface.sv apb_master.sv apb_slave.sv -debug_all
./simv
- 关键设计说明:
接口定义(apb_if)包含完整的APB4信号:
- pclk: APB时钟(由测试平台生成)
- paddr: 32位地址总线
- psel: 选择信号
- penable: 使能信号
- pwrite: 写使能
- pwdata/prdata: 写/读数据总线
- pready: 传输完成指示
- pslverr: 错误指示
主设备(apb_master)实现:
- 状态机控制(IDLE->SETUP->ACCESS)
- 支持基本的读写任务
- 符合APB协议时序要求
-从设备(apb_slave)实现:
- 256x32位存储阵列
- 自动响应PSEL和PENABLE
- 地址越界错误检测
波形查看建议:
verdi -ssf tb.fsdb # 使用Verdi
dve -vpd vcdplus.vpd # 使用Cadence DVE
示例四:APB总线接口设计(4)
1. APB接口定义(SystemVerilog)
// APB接口定义(支持APB3协议)
interface apb_if #(parameter ADDR_WIDTH=32, DATA_WIDTH=32) (
input logic pclk,
input logic presetn
);
logic [ADDR_WIDTH-1:0] paddr;
logic pwrite;
logic psel;
logic penable;
logic [DATA_WIDTH-1:0] pwdata;
logic [DATA_WIDTH-1:0] prdata;
logic pready;
logic pslverr;
// Master驱动时钟块
clocking master_cb @(posedge pclk);
default input #1step output #2;
output paddr, pwrite, psel, penable, pwdata;
input prdata, pready, pslverr;
endclocking
// Slave采样时钟块
clocking slave_cb @(posedge pclk);
default input #1step;
input paddr, pwrite, psel, penable, pwdata;
output prdata, pready, pslverr;
endclocking
modport master (clocking master_cb);
modport slave (clocking slave_cb);
endinterface
2. APB Master设计
module apb_master (
apb_if.master apb,
input logic [31:0] wr_addr,
input logic [31:0] wr_data,
input logic wr_en,
input logic [31:0] rd_addr,
output logic [31:0] rd_data,
output logic rd_ready
);
enum {IDLE, SETUP, ACCESS} state;
always_ff @(posedge apb.pclk or negedge apb.presetn) begin
if (!apb.presetn) begin
state <= IDLE;
apb.master_cb.psel <= 0;
apb.master_cb.penable <= 0;
end else begin
case(state)
IDLE:
if (wr_en || rd_ready) begin
apb.master_cb.paddr <= wr_en ? wr_addr : rd_addr;
apb.master_cb.pwrite <= wr_en;
apb.master_cb.psel <= 1;
state <= SETUP;
end
SETUP:
begin
apb.master_cb.penable <= 1;
state <= ACCESS;
end
ACCESS:
if (apb.master_cb.pready) begin
apb.master_cb.psel <= 0;
apb.master_cb.penable <= 0;
if (!apb.master_cb.pwrite)
rd_data <= apb.master_cb.prdata;
state <= IDLE;
end
endcase
end
end
endmodule
3. APB Slave设计(带寄存器)
module apb_slave (
apb_if.slave apb,
input logic [3:0] reg_sel,
output logic [31:0] reg_data_out,
input logic [31:0] reg_data_in
);
logic [31:0] mem [0:15];
always_ff @(posedge apb.pclk or negedge apb.presetn) begin
if (!apb.presetn) begin
foreach(mem[i]) mem[i] <= 32'h0;
end else if (apb.slave_cb.psel && apb.slave_cb.penable) begin
if (apb.slave_cb.pwrite)
mem[apb.slave_cb.paddr[5:2]] <= apb.slave_cb.pwdata;
else
reg_data_out <= mem[apb.slave_cb.paddr[5:2]];
end
end
assign apb.slave_cb.prdata = reg_data_in;
assign apb.slave_cb.pready = 1'b1; // 无等待
assign apb.slave_cb.pslverr = 1'b0; // 无错误
endmodule
4. 测试平台
module tb;
logic pclk = 0;
logic presetn;
logic [31:0] wr_addr = 32'h1000_0000;
logic [31:0] wr_data = 32'h1234_5678;
logic wr_en = 0;
logic [31:0] rd_data;
apb_if apb(pclk, presetn);
apb_master master(.*);
apb_slave slave(.reg_data_in(rd_data), .*);
// 时钟生成
always #5 pclk = ~pclk;
initial begin
$dumpfile("apb_wave.vcd");
$dumpvars(0, tb);
// 复位
presetn = 0;
#20 presetn = 1;
// 写操作
wr_en = 1;
#30 wr_en = 0;
// 读操作
#30 wr_addr = 32'h1000_0000;
#30;
$finish;
end
endmodule
5. 仿真指令
# 使用Synopsys Xrun
xrun -sv -access +rwc apb_if.sv apb_master.sv apb_slave.sv tb.sv
# 或使用VCS
vcs -full64 -sverilog apb_if.sv apb_master.sv apb_slave.sv tb.sv
./simv
6. 关键时序说明
写时序:
- T0-T1:Master在IDLE状态检测到wr_en有效
- T1-T2:SETUP阶段(PSEL=1,PENABLE=0)
- T2-T3:ACCESS阶段(PENABLE=1,Slave采样PWData)
- 地址相位和数据相位对齐
读时序: - 类似写时序,但PWRITE=0
- Slave在ACCESS阶段返回PRDATA
7. 波形验证要点
- 观察PSEL/PENABLE的相位关系
- 验证地址与数据的同步性
- 检查PREADY始终为1(本设计无等待)
- 确认写数据在ACCESS阶段被Slave存储
示例五:APB总线接口设计(5)
1. 完整代码示例
1.1 APB总线接口定义 (apb_if.sv)
// =================================================
// APB总线接口定义(符合AMBA APB协议)
// =================================================
interface apb_if(input logic pclk, input logic presetn);
// -----------------------------------------------
// 信号定义
// -----------------------------------------------
logic [31:0] paddr; // 地址总线
logic pwrite; // 写使能(1=写,0=读)
logic [31:0] pwdata; // 写数据
logic [31:0] prdata; // 读数据
logic psel; // 外设选择信号
logic penable; // 传输使能
logic pready; // 外设就绪信号(简化设计,恒为1)
// -----------------------------------------------
// 时钟块定义(用于Testbench同步驱动和采样)
// -----------------------------------------------
clocking driver_cb @(posedge pclk);
default input #1ns output #1ns; // 输入采样在时钟沿后1ns,输出驱动在时钟沿前1ns
output paddr, pwrite, pwdata, psel, penable;
input prdata, pready;
endclocking
clocking monitor_cb @(posedge pclk);
input paddr, pwrite, pwdata, psel, penable, prdata;
endclocking
// -----------------------------------------------
// Modport定义(模块视图)
// -----------------------------------------------
modport master (
clocking driver_cb, // Testbench或Master驱动
output presetn // 复位信号由Master控制
);
modport slave (
input pclk, presetn,
input paddr, pwrite, pwdata, psel, penable,
output prdata,
output pready = 1'b1 // 简化设计,外设始终就绪
);
endinterface
1.2 APB Slave模块 (apb_slave.sv)
// =================================================
// APB Slave模块:实现简单的寄存器读写
// =================================================
module apb_slave (
apb_if.slave apb
);
// -----------------------------------------------
// 内部寄存器定义
// -----------------------------------------------
logic [31:0] mem[0:255]; // 256个32位寄存器
// -----------------------------------------------
// APB协议逻辑
// -----------------------------------------------
always_ff @(posedge apb.pclk or negedge apb.presetn) begin
if (!apb.presetn) begin
apb.prdata <= 32'h0;
end else if (apb.psel && apb.penable) begin
if (apb.pwrite) begin
// 写操作
mem[apb.paddr[7:0]] <= apb.pwdata;
$display("[SLAVE] Write Addr=0x%08h, Data=0x%08h", apb.paddr, apb.pwdata);
end else begin
// 读操作
apb.prdata <= mem[apb.paddr[7:0]];
$display("[SLAVE] Read Addr=0x%08h, Data=0x%08h", apb.paddr, apb.prdata);
end
end
end
endmodule
1.3 测试平台 (tb.sv)
// =================================================
// Testbench模块:驱动APB总线并验证功能
// =================================================
module tb;
// -----------------------------------------------
// 生成时钟和复位信号
// -----------------------------------------------
logic pclk;
logic presetn;
initial begin
pclk = 0;
forever #5 pclk = ~pclk; // 100MHz时钟(周期10ns)
end
initial begin
presetn = 0; // 复位初始为低
#20 presetn = 1; // 20ns后释放复位
end
// -----------------------------------------------
// 实例化APB接口和Slave模块
// -----------------------------------------------
apb_if apb0(pclk, presetn);
apb_slave slave(apb0);
// -----------------------------------------------
// 测试逻辑
// -----------------------------------------------
initial begin
// 等待复位完成
@(posedge presetn);
$display("[TB] Reset released at %0t ns", $time);
// 通过时钟块驱动APB信号
fork
begin: WRITE_OP
// 写操作:地址0x1000,数据0x12345678
apb0.driver_cb.psel <= 1;
apb0.driver_cb.paddr <= 32'h1000;
apb0.driver_cb.pwrite <= 1;
apb0.driver_cb.pwdata <= 32'h12345678;
@(apb0.driver_cb);
apb0.driver_cb.penable <= 1;
@(apb0.driver_cb);
apb0.driver_cb.psel <= 0;
apb0.driver_cb.penable <= 0;
$display("[TB] Write operation completed at %0t ns", $time);
end
begin: READ_OP
// 读操作:地址0x1000
#30; // 等待写操作完成
apb0.driver_cb.psel <= 1;
apb0.driver_cb.paddr <= 32'h1000;
apb0.driver_cb.pwrite <= 0;
@(apb0.driver_cb);
apb0.driver_cb.penable <= 1;
@(apb0.driver_cb);
apb0.driver_cb.psel <= 0;
apb0.driver_cb.penable <= 0;
$display("[TB] Read operation completed, Data=0x%08h", apb0.driver_cb.prdata);
end
join
#50;
$finish;
end
endmodule
2. 仿真步骤
2.1 运行仿真
xrun -sv apb_if.sv apb_slave.sv tb.sv +access+r
- 命令解析:
-sv
:启用SystemVerilog模式。+access+r
:生成波形数据库(用于查看波形)。
2.2 预期输出日志
[TB] Reset released at 20 ns
[SLAVE] Write Addr=0x00001000, Data=0x12345678
[TB] Write operation completed at 40 ns
[SLAVE] Read Addr=0x00001000, Data=0x12345678
[TB] Read operation completed, Data=0x12345678
2.3 查看波形
使用SimVision打开生成的波形数据库(默认名为tb.shm),查看以下信号:
- 时钟和复位:pclk、presetn
- APB控制信号:psel、penable、pwrite
- 数据信号:paddr、pwdata、prdata
3. 关键代码解析
3.1 APB接口时钟块
clocking driver_cb @(posedge pclk);
default input #1ns output #1ns;
output paddr, pwrite, pwdata, psel, penable;
input prdata, pready;
endclocking
作用:在时钟上升沿同步驱动和采样信号,避免时序冲突。
3.2 APB Slave读写逻辑
always_ff @(posedge apb.pclk or negedge apb.presetn) begin
if (!apb.presetn) begin
apb.prdata <= 32'h0;
end else if (apb.psel && apb.penable) begin
if (apb.pwrite) begin
mem[apb.paddr[7:0]] <= apb.pwdata;
end else begin
apb.prdata <= mem[apb.paddr[7:0]];
end
end
end
复位时:清除读数据。
传输阶段:当psel和penable同时有效时,执行读写操作。
3.3 测试平台驱动
// 写操作流程
apb0.driver_cb.psel <= 1;
apb0.driver_cb.paddr <= 32'h1000;
apb0.driver_cb.pwrite <= 1;
apb0.driver_cb.pwdata <= 32'h12345678;
@(apb0.driver_cb);
apb0.driver_cb.penable <= 1;
@(apb0.driver_cb);
apb0.driver_cb.psel <= 0;
apb0.driver_cb.penable <= 0;
Setup阶段:设置地址、数据和控制信号(psel=1)。
Access阶段:拉高penable完成传输。