AXI-Lite寄存器接口SV模板:可复制进工程的从机寄存器块(含中断sticky位)
几乎每个带 CPU 的子系统都要一块「寄存器接口」:软件读写配置、读状态、清中断。这部分逻辑高度套路化,却总有人每次都从头写、每次都在握手细节上翻车。本篇给出一份 AXI-Lite寄存器接口SV模板——单文件、纯 module + parameter(不用 interface/package,iverilog -g2012 就能编)、已过 Verilator -Wall 与 Icarus 仿真。协议背景见 AXI4 与 AXI4-Lite 核心区别 和 AMBA 总线入门,寄存器地址规划的方法论见 寄存器映射设计入门。
本文要回答的问题
- 这份模板适用于什么场景,寄存器映射长什么样?
- 完整代码分几段,每段的握手语义是什么?
- 中断 sticky 位为什么用 W1C(写 1 清除)?
- 想加一个自己的寄存器,要动哪几处?
一、使用场景与寄存器映射
AXI4-Lite 是 AXI4 的精简版:单次传输、无突发、无 ID,专为「CPU 配置外设寄存器」而生。典型使用场景一抓一把:UART/SPI 控制器的波特率与模式配置、DMA 控制器的源目地址与描述符指针、硬件加速器的启动位与完成中断、LED/按键这类慢速外设的控制字——它们的共同点是软件访问频率低、每次只动一个字,用 Full AXI 纯属浪费面积。
模板的默认映射只有三个寄存器,覆盖最常见的三类需求:
| 字节地址 | 名称 | 属性 | 说明 |
|---|---|---|---|
| 0x00 | CTRL | RW | bit[1:0] 为两路中断使能,其余位用户自定义 |
| 0x04 | STATUS | RO | bit[1:0] = {error_in, busy_in},用户逻辑实时电平 |
| 0x08 | IRQ_STATUS | W1C | 中断 sticky 位,硬件置位、软件写 1 清除 |
一次 AXI-Lite 写事务在这份模板里的旅程可以用五个步骤看全:
读这张卡抓住一个要点:AW 和 W 是两条独立通道,协议允许它们以任意顺序到达,所以模板把两边各自锁存、都到齐才算一次有效写——这是新手自己手写时最容易省掉、也最容易出 bug 的地方。valid/ready 握手本身的原则见 Valid-Ready 握手波形实验。
二、完整模板代码
完整文件约 180 行,逐段讲解见第三节。代码与 testbench 已同步公开仓(见文末「代码获取」),这里按功能分四段贴出核心部分。
端口与写通道握手(AW/W 各自锁存):
module axi_lite_regs #(
parameter ADDR_WIDTH = 6, // 字节地址宽度,6 => 64B 寄存器空间
parameter DATA_WIDTH = 32
) (
input wire aclk,
input wire aresetn,
input wire [ADDR_WIDTH-1:0] s_axi_awaddr,
input wire s_axi_awvalid,
output reg s_axi_awready,
input wire [DATA_WIDTH-1:0] s_axi_wdata,
input wire [DATA_WIDTH/8-1:0] s_axi_wstrb,
input wire s_axi_wvalid,
output reg s_axi_wready,
output reg [1:0] s_axi_bresp,
output reg s_axi_bvalid,
input wire s_axi_bready,
input wire [ADDR_WIDTH-1:0] s_axi_araddr,
input wire s_axi_arvalid,
output reg s_axi_arready,
output reg [DATA_WIDTH-1:0] s_axi_rdata,
output reg [1:0] s_axi_rresp,
output reg s_axi_rvalid,
input wire s_axi_rready,
input wire busy_in,
input wire error_in,
input wire evt_done, // 单拍事件脉冲,置位 sticky
input wire evt_error,
output reg [DATA_WIDTH-1:0] ctrl_reg,
output wire irq
);
// AW 通道:握手当拍锁存地址,置 awpend;W 通道同构
always @(posedge aclk or negedge aresetn) begin
if (!aresetn) begin
s_axi_awready <= 1'b0;
awaddr_q <= {ADDR_WIDTH-2{1'b0}};
awpend_q <= 1'b0;
end else begin
if (s_axi_awready && s_axi_awvalid) begin
awaddr_q <= s_axi_awaddr[ADDR_WIDTH-1:2];
awpend_q <= 1'b1;
s_axi_awready <= 1'b0;
end else if (wr_fire) begin
awpend_q <= 1'b0;
s_axi_awready <= 1'b1;
end else if (!awpend_q && !s_axi_awready) begin
s_axi_awready <= 1'b1; // 空闲时回到可接收态
end
end
end写译码与中断 sticky(核心一段):
wire wr_fire = awpend_q & wpend_q & ~s_axi_bvalid;
integer i;
always @(posedge aclk or negedge aresetn) begin
if (!aresetn) begin
ctrl_reg <= {DATA_WIDTH{1'b0}};
irq_sticky_q <= 2'b00;
end else begin
if (evt_done) irq_sticky_q[0] <= 1'b1; // 置位优先
if (evt_error) irq_sticky_q[1] <= 1'b1;
if (wr_fire) begin
case (awaddr_q[3:0])
REG_CTRL: begin
for (i = 0; i < DATA_WIDTH/8; i = i + 1)
if (wstrb_q[i])
ctrl_reg[i*8 +: 8] <= wdata_q[i*8 +: 8];
end
REG_IRQ_STATUS: begin
if (wstrb_q[0]) begin
if (wdata_q[0] && !evt_done) irq_sticky_q[0] <= 1'b0;
if (wdata_q[1] && !evt_error) irq_sticky_q[1] <= 1'b0;
end
end
default: ; // STATUS 只读、未定义地址写静默丢弃
endcase
end
end
endB 响应与读通道(AR 握手当拍译码,rvalid 保持到主机接收):
// B:写生效当拍拉高 bvalid,主机接收后撤下
always @(posedge aclk or negedge aresetn) begin
if (!aresetn) begin
s_axi_bvalid <= 1'b0;
s_axi_bresp <= 2'b00;
end else begin
if (wr_fire) s_axi_bvalid <= 1'b1;
else if (s_axi_bvalid && s_axi_bready) s_axi_bvalid <= 1'b0;
end
end
// R:AR 握手当拍按地址多路选择,rvalid 保持到 rready
always @(posedge aclk or negedge aresetn) begin
if (!aresetn) begin
s_axi_rvalid <= 1'b0;
s_axi_rdata <= {DATA_WIDTH{1'b0}};
end else begin
if (s_axi_arready && s_axi_arvalid) begin
s_axi_rvalid <= 1'b1;
case (s_axi_araddr[ADDR_WIDTH-1:2])
REG_CTRL: s_axi_rdata <= ctrl_reg;
REG_STATUS: s_axi_rdata <= {{(DATA_WIDTH-2){1'b0}}, error_in, busy_in};
REG_IRQ_STATUS: s_axi_rdata <= {{(DATA_WIDTH-2){1'b0}}, irq_sticky_q};
default: s_axi_rdata <= {DATA_WIDTH{1'b0}};
endcase
end else if (s_axi_rvalid && s_axi_rready) begin
s_axi_rvalid <= 1'b0;
end
end
end三、逐段讲解:四个设计决策
为什么 AW/W 各自锁存而不是同时等:协议不保证两通道同拍到达,主机可以先把地址挂几个周期再发数据。模板用 awpend_q/wpend_q 两个标志表达「已收到、等对方」,wr_fire 只在双方到齐且无未完成响应时拉高,写译码恰好生效一拍。
为什么译码用字地址:s_axi_awaddr[ADDR_WIDTH-1:2] 丢掉字节内偏移,寄存器按 32 位字对齐寻址。低两位不参与译码是 AXI-Lite 的通行做法(完整模板里用一根 unused_ok 线显式消费它们,保持 lint 干净)。
为什么中断位是 W1C 而不是 RW:如果软件读-改-写清除中断,读和写之间新来的事件会被旧值覆盖丢失。W1C 语义下软件写 1 只清指定位,且模板里「硬件置位优先于软件清除」——同拍到来时事件不丢。这与 中断与 DMA 入门 里讲的中断状态寄存器约定一致。
为什么 B/R 通道的 valid 用「保持到握手」写法:bvalid/rvalid 一旦拉高就不能随输入变化,必须保持到对方 ready。模板里两者都只在 fire 或 ready 握手时翻转,不存在毛刺窗口。
为什么语法只用 module + parameter 子集:模板刻意不用 interface、package、always_ff 这些更「SystemVerilog」的特性,只用 reg/wire/localparam/case 这套 2005 时代就稳定的写法。代价是代码稍显啰嗦,收益是工具兼容性拉满——Icarus -g2012、Verilator、Yosys、各家商业综合器全部直接吃,模板复制进任何工程都不会先卡在语法支持上。等团队工具链统一到全面支持 SV,再考虑升级写法不迟。
四、lint 与仿真验证
模板过了两道验证:Verilator -Wall 零告警,以及 Icarus 最小 testbench 的八项自检——写读回、字节选通部分写、STATUS 实时电平、中断置位、irq 输出、W1C 清除、irq 回落、未定义地址读零。真实运行输出:
读这段输出看三层:第 4-5 行是 lint,-Wall 下没有任何告警(unused_ok 命名约定起了作用);第 6-7 行是 iverilog 编译,-g2012 子集完全够用;第 8 行起是仿真,OK 行逐条对应八项检查,最后 TEST PASS 收尾。注意 CTRL wstrb = 0003ffff 这一条:先写全 1 再只写高两字节,证明字节选通译码没有退化成一个 32 位整体写。
这八项检查不是随便凑的,它们对应模板最容易写错的四类逻辑:握手状态(写读回成立说明 AW/W/B 链路通)、字节选通(部分写不串扰其他字节)、sticky 位竞态(置位与清除的顺序语义)、默认分支(未定义地址不挂死总线)。你自己改模板加寄存器后,给 tb 补一条同型检查,整套验证就仍然闭环。
五、使用注意事项
- 异步复位低有效:
aresetn直接进敏感表,上层记得做同步释放;如果你的库用同步复位,把敏感表里的or negedge aresetn删掉即可。 - 默认响应永远是 OKAY:未定义地址读返回 0、写静默丢弃,不触发 SLVERR。想让软件能发现地址错误,在写译码
default分支把s_axi_bresp改成2'b10。 - 单发不排队:同一时刻只服务一笔写、一笔读,这是 AXI-Lite 从机的标准容量,足够寄存器场景;不要在这个骨架上叠 outstanding。
- 事件脉冲要单拍:
evt_done/evt_error约定为单拍脉冲;电平型信号先进沿检测器再接进来。 - irq 是电平输出:
irq = |(irq_sticky_q & ctrl_reg[1:0]),清 sticky 后自动回落,中断控制器端按电平触发接。 - 复位后先读 ID/状态再配置:软件驱动的好习惯是复位释放后先读一遍 STATUS 确认硬件就绪,再写 CTRL 使能中断——模板复位值全 0,读到非零期望前先确认你自己写过。
- 位宽参数要成对改:
DATA_WIDTH支持 32/64,但wstrb循环与ctrl_reg分字节写都依赖DATA_WIDTH/8整除;改成非 8 倍数会直接编译报错,这是好事不是坏事。 - 不要把用户逻辑写进模板:模板只暴露
ctrl_reg/busy_in这类端口,业务逻辑放在上一级模块里接这些端口——寄存器块保持纯粹,换总线(比如换 APB)时只需替换这一个文件。
六、修改示范:加一个用户寄存器
假设要在 0x0C 加一个 RW 的 THRESHOLD 寄存器,输出给用户逻辑。改四处即可:
localparam REG_THRESHOLD = 4'h3; // 1) 地址表加一行
output reg [DATA_WIDTH-1:0] threshold_reg, // 2) 端口列表加输出
// 3) 写译码 case 加分支(带字节选通,照抄 REG_CTRL 写法):
REG_THRESHOLD: begin
for (i = 0; i < DATA_WIDTH/8; i = i + 1)
if (wstrb_q[i])
threshold_reg[i*8 +: 8] <= wdata_q[i*8 +: 8];
end
// 4) 读多路选择 case 加一行:
REG_THRESHOLD: s_axi_rdata <= threshold_reg;复位分支里别忘给 threshold_reg 加初值。整个模式是机械的:地址常量、端口、写分支、读分支、复位值——五处齐全,lint 和 tb 各补一条检查就闭环。加第二、第三个寄存器时照同一张清单重复,不要为了省行把多个寄存器的译码合并写,评审和调试时你会感谢这份啰嗦。
实验环境
- 机器/系统:Mac mini M4 / macOS 15
- 工具版本:Verilator 5.020(geek-eda:0.2 docker 镜像)/ Icarus Verilog 13.0 (stable v13_0)
- 日期:2026-10-29
- 完整命令:
verilator --lint-only -Wall axi_lite_regs.sv;iverilog -g2012 -o tb_axi_lite_regs.vvp axi_lite_regs.sv tb_axi_lite_regs.sv && vvp tb_axi_lite_regs.vvp
代码获取
模板源码与自检 testbench 在公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/tpl-axi-lite-register-sv。装好 Icarus Verilog 后在该目录跑 iverilog -g2012 -o tb_axi_lite_regs.vvp axi_lite_regs.sv tb_axi_lite_regs.sv && vvp tb_axi_lite_regs.vvp,看到 TEST PASS 即复现本文结果。
这一篇在知识体系里的位置
本篇是芯片设计入门簇的「模板件」:协议辨析看 AXI4 与 AXI4-Lite 核心区别,握手原理看 Valid-Ready 握手波形实验,地址规划看 寄存器映射设计入门;模板本体过 Verilator lint 的告警语义可对照 开源 EDA 报错排查导航。
总结
AXI-Lite寄存器接口SV模板是一个单文件、iverilog -g2012 可编的从机寄存器块:AW/W 双通道各自锁存、到齐当拍译码写、B 响应保持到握手;AR 握手当拍多路选择、rvalid 保持到接收;中断状态位走「硬件置位优先 + 软件 W1C」语义,irq 为电平输出。模板已过 Verilator -Wall 零告警与八项仿真自检;加自己的寄存器只需机械改五处——地址常量、端口、写分支、读分支、复位值。