手把手教你用Verilog实现一个APB3 Slave模块(附完整代码与仿真)
手把手教你用Verilog实现一个APB3 Slave模块(附完整代码与仿真)
在数字IC设计和FPGA开发中,AMBA总线协议家族是最常用的片上互连标准之一。作为AMBA协议中最简单的一员,APB(Advanced Peripheral Bus)因其低功耗和接口简单的特性,常被用于连接低速外设。本文将从一个工程师的视角,带你从零开始实现一个符合APB3规范的Slave模块。
1. APB3协议核心要点解析
APB3协议相比APB2主要增加了PREADY和PSLVERR两个信号,这使得从设备可以控制传输时序和报告错误状态。理解以下几个关键点对实现Slave模块至关重要:
- 两阶段传输机制:每个APB传输至少需要两个时钟周期。第一阶段(PSEL=1, PENABLE=0)用于地址建立,第二阶段(PSEL=1, PENABLE=1)完成数据传输。
- 关键信号作用:
- PSEL:选择当前从设备
- PENABLE:使能数据传输阶段
- PREADY:从设备准备好信号
- PWRITE:区分读写操作(1=写,0=读)
// APB3接口信号定义示例 module apb_slave ( input PCLK, // 时钟 input PRESETn, // 低有效复位 input PSEL, // 选择信号 input PENABLE, // 使能信号 input PWRITE, // 读写控制 input [31:0] PADDR, // 地址总线 input [31:0] PWDATA, // 写数据 output [31:0] PRDATA, // 读数据 output PREADY, // 准备好信号 output PSLVERR // 错误信号(可选) );2. Slave模块设计思路
一个基础的APB Slave模块通常需要实现以下功能:
- 地址解码:识别总线访问的目标寄存器
- 读写控制:根据PWRITE信号执行读/写操作
- 时序响应:正确产生PREADY信号
- 寄存器组:存储外设的控制和状态信息
建议设计时采用状态机来管理APB传输的各个阶段,这会使代码更清晰且易于维护。典型的状态转移如下:
IDLE → SETUP → ACCESS → (可能返回SETUP或IDLE)3. 完整Verilog实现
下面是一个支持4个32位寄存器的APB3 Slave实现。我们采用参数化设计,方便地址配置和模块复用。
module apb_slave #( parameter BASE_ADDR = 32'h4000_0000, parameter REG0_ADDR = 32'h0, parameter REG1_ADDR = 32'h4, parameter REG2_ADDR = 32'h8, parameter REG3_ADDR = 32'hC )( // APB3接口 input PCLK, input PRESETn, input PSEL, input PENABLE, input PWRITE, input [31:0] PADDR, input [31:0] PWDATA, output reg [31:0] PRDATA, output PREADY, output PSLVERR ); // 内部寄存器定义 reg [31:0] reg0, reg1, reg2, reg3; // 地址有效信号 wire addr_valid = (PADDR[31:4] == BASE_ADDR[31:4]); // 寄存器选择信号 wire reg0_sel = addr_valid && (PADDR[3:0] == REG0_ADDR); wire reg1_sel = addr_valid && (PADDR[3:0] == REG1_ADDR); wire reg2_sel = addr_valid && (PADDR[3:0] == REG2_ADDR); wire reg3_sel = addr_valid && (PADDR[3:0] == REG3_ADDR); // 传输有效信号 wire xfer_valid = PSEL && PENABLE; wire wr_en = xfer_valid && PWRITE; wire rd_en = xfer_valid && !PWRITE; // 寄存器写操作 always @(posedge PCLK or negedge PRESETn) begin if (!PRESETn) begin reg0 <= 32'h0; reg1 <= 32'h0; reg2 <= 32'h0; reg3 <= 32'h0; end else if (wr_en) begin case (1'b1) reg0_sel: reg0 <= PWDATA; reg1_sel: reg1 <= PWDATA; reg2_sel: reg2 <= PWDATA; reg3_sel: reg3 <= PWDATA; endcase end end // 寄存器读操作 always @(*) begin if (rd_en) begin case (1'b1) reg0_sel: PRDATA = reg0; reg1_sel: PRDATA = reg1; reg2_sel: PRDATA = reg2; reg3_sel: PRDATA = reg3; default: PRDATA = 32'hDEADBEEF; // 默认值 endcase end else begin PRDATA = 32'h0; end end // 准备好信号和错误信号 assign PREADY = 1'b1; // 本实现总是准备好 assign PSLVERR = 1'b0; // 无错误 endmodule4. Testbench设计与仿真
验证是设计过程中至关重要的一环。下面是一个完整的测试平台,包含读写测试用例:
`timescale 1ns/1ps module apb_slave_tb(); // 时钟和复位 reg PCLK; reg PRESETn; // APB接口信号 reg PSEL; reg PENABLE; reg PWRITE; reg [31:0] PADDR; reg [31:0] PWDATA; wire [31:0] PRDATA; wire PREADY; wire PSLVERR; // 实例化DUT apb_slave #( .BASE_ADDR(32'h4000_0000) ) u_dut ( .PCLK(PCLK), .PRESETn(PRESETn), .PSEL(PSEL), .PENABLE(PENABLE), .PWRITE(PWRITE), .PADDR(PADDR), .PWDATA(PWDATA), .PRDATA(PRDATA), .PREADY(PREADY), .PSLVERR(PSLVERR) ); // 时钟生成 initial begin PCLK = 0; forever #5 PCLK = ~PCLK; end // 测试流程 initial begin // 初始化 PRESETn = 0; PSEL = 0; PENABLE = 0; PWRITE = 0; PADDR = 0; PWDATA = 0; // 复位释放 #20; PRESETn = 1; // 测试写操作 apb_write(32'h4000_0000, 32'h1234_5678); // 写REG0 apb_write(32'h4000_0004, 32'h9ABC_DEF0); // 写REG1 // 测试读操作 apb_read(32'h4000_0000); // 读REG0 apb_read(32'h4000_0004); // 读REG1 // 测试非法地址访问 apb_read(32'h4000_00FF); // 读无效地址 #100; $finish; end // APB写任务 task apb_write(input [31:0] addr, input [31:0] data); begin @(posedge PCLK); PSEL = 1; PWRITE = 1; PENABLE = 0; PADDR = addr; PWDATA = data; @(posedge PCLK); PENABLE = 1; @(posedge PCLK); while (!PREADY) @(posedge PCLK); PSEL = 0; PENABLE = 0; end endtask // APB读任务 task apb_read(input [31:0] addr); begin @(posedge PCLK); PSEL = 1; PWRITE = 0; PENABLE = 0; PADDR = addr; @(posedge PCLK); PENABLE = 1; @(posedge PCLK); while (!PREADY) @(posedge PCLK); $display("[%t] Read from 0x%08h: 0x%08h", $time, addr, PRDATA); PSEL = 0; PENABLE = 0; end endtask // 波形记录 initial begin $dumpfile("apb_slave.vcd"); $dumpvars(0, apb_slave_tb); end endmodule仿真波形中应关注以下几点:
- PSEL和PENABLE的时序关系
- PWRITE控制下的数据流向
- PREADY信号的响应时间
- 读写数据的正确性
5. 进阶功能扩展
基础版本实现后,可以考虑添加以下增强功能:
- 可配置延迟响应:通过PREADY信号实现可变延迟
- 错误处理:利用PSLVERR报告访问错误
- 保护机制:实现PPROT信号支持(APB4)
- 字节使能:支持PSTRB信号(APB4)
// 带延迟响应的PREADY生成示例 reg [1:0] delay_cnt; assign PREADY = (delay_cnt == 2'b00); always @(posedge PCLK or negedge PRESETn) begin if (!PRESETn) begin delay_cnt <= 2'b00; end else if (xfer_valid && !PREADY) begin delay_cnt <= delay_cnt - 1; end else begin delay_cnt <= 2'b10; // 设置2周期延迟 end end实现APB Slave模块时,最常见的几个坑包括:
- 混淆PSEL和PENABLE的作用时机
- 读数据未在地址相位保持稳定
- 复位后寄存器未正确初始化
- 未正确处理PREADY时序
