3. 寄存器建模:从单比特到寄存器堆
寄存器,说白了就是数字电路里的“记忆单元”。单比特的D触发器我们上一章聊过了,但实际项目中,谁会用单比特打天下?你想想看,一个32位的处理器,光通用寄存器就32个,每个还都是32位的。所以这一章,我们聊聊怎么把寄存器玩出花来。
3.1 多位寄存器的实现
多位寄存器,其实就是把多个D触发器并排摆在一起。我习惯叫它“寄存器向量”。在RTL里,声明方式特别直接:
// 一个8位的寄存器
reg [7:0] data_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
data_reg <= 8'b0;
else
data_reg <= data_in;
end
嗯,这里要注意:[7:0] 这种写法,bit 7是最高位。我个人习惯统一用 [N-1:0] 的格式,这样代码看起来整齐,不容易搞混位序。我在项目中遇到过一位同事,因为位序写反了,仿真怎么都对,上板子就乱码——查了整整两天。
reg [31:0]。
3.2 移位寄存器的设计
移位寄存器,说白了就是数据在时钟边沿“搬家”。左移、右移、循环移位,都是基本功。但真正好用的,是带使能控制的移位寄存器。
我曾经做过一个串行通信接口,接收端就用了一个8位移位寄存器。每来一个时钟,数据就移一位,8个时钟后刚好收完一个字节。代码长这样:
// 带使能的8位右移寄存器
reg [7:0] shift_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
shift_reg <= 8'b0;
else if (shift_en)
shift_reg <= {shift_reg[6:0], serial_in};
end
这里 shift_en 就是使能信号。没有它,寄存器就保持不动。你想想看,如果每个时钟都移,那数据不就乱套了?
{serial_in, shift_reg[7:1]},右移用 {shift_reg[6:0], serial_in}。记住这个规律,写代码不用查手册。
3.3 流水线寄存器的应用
流水线寄存器,是时序优化的利器。说白了,就是在组合逻辑路径中间插入寄存器,把一个大逻辑拆成几段。这样每段路径变短了,时钟频率就能提上去。
我举个例子。假设你有一个三级加法树:
// 无流水线:一个时钟内完成所有加法
always @(posedge clk)
result <= a + b + c + d + e + f + g + h;
// 三级流水线:每级只做两个加法
reg [7:0] stage1, stage2, stage3;
always @(posedge clk) begin
stage1 <= (a + b) + (c + d);
stage2 <= stage1 + (e + f);
stage3 <= stage2 + (g + h);
end
assign result = stage3;
无流水线版本,组合逻辑路径太长,频率上不去。三级流水线版本,每级只做两个加法,路径短了,频率自然就高了。代价是什么?延迟多了3个时钟周期。这就是典型的“用面积换速度”。
3.4 寄存器堆(Register File)建模
寄存器堆,就是一堆寄存器的集合。CPU里的通用寄存器组就是典型的寄存器堆。它有读端口和写端口,可以同时读写。
一个双端口寄存器堆的建模如下:
// 32×32 寄存器堆,2读1写
module regfile (
input clk,
input rst_n,
input [4:0] raddr1, // 读地址1
input [4:0] raddr2, // 读地址2
output [31:0] rdata1,
output [31:0] rdata2,
input [4:0] waddr, // 写地址
input [31:0] wdata,
input we // 写使能
);
reg [31:0] mem [0:31]; // 32个32位寄存器
// 写操作
always @(posedge clk) begin
if (we && waddr != 5'b0) // 地址0通常为只读
mem[waddr] <= wdata;
end
// 读操作(组合逻辑读)
assign rdata1 = (raddr1 == 5'b0) ? 32'b0 : mem[raddr1];
assign rdata2 = (raddr2 == 5'b0) ? 32'b0 : mem[raddr2];
endmodule
这里有个细节:地址0通常硬编码为0,写不进去。这是RISC-V等架构的惯例。我在项目中遇到过有人忘了这个约定,结果写地址0的数据丢了,查了半天才发现。
- 读操作可以是组合逻辑(立即读出),也可以是时序逻辑(延迟一拍读出)
- 写操作必须是时序逻辑,在时钟上升沿写入
- 读写地址冲突时,通常写优先(先写后读)
下面这张图,展示了寄存器堆的内部结构:
寄存器堆的设计,说白了就是“多端口存储器”。读端口可以很多,但写端口通常只有1-2个,因为写冲突很难处理。我在项目中见过一个设计,写了4个写端口,结果仲裁逻辑比寄存器堆本身还大——得不偿失。
好了,这一章的内容就这些。寄存器建模是时序逻辑的基础,多练练,自然就熟了。