第四章:时序逻辑建模——D触发器、寄存器、移位寄存器、计数器的RTL实现
各位同学,欢迎来到第四章。
上一章我们聊了组合逻辑,说白了就是“当前输入决定当前输出”。但数字电路里,光有组合逻辑可不行。你想想看,如果所有电路都只跟当前状态有关,那“记忆”从哪来?状态怎么保存?
嗯,这就是时序逻辑的活儿了。
时序逻辑的核心,就是时钟。时钟像心跳一样,一拍一拍地驱动着整个芯片。所有状态的变化,都发生在时钟的边沿(上升沿或下降沿)。今天我们就从最基础的D触发器开始,一路讲到计数器。这些都是我平时写RTL时天天打交道的玩意儿。
本章核心知识点:
- D触发器的RTL实现与工作原理
- 寄存器的概念与多比特实现
- 移位寄存器的几种玩法
- 二进制计数器与模N计数器的设计
4.1 D触发器——时序逻辑的基石
D触发器,全称Data Flip-Flop,是时序逻辑里最基础的存储单元。它只有一个数据输入D,一个时钟输入clk,一个输出Q。在时钟上升沿,Q = D。
说白了,它就是把输入“拍”到输出上,延迟了一个时钟周期。
先看一个最简单的D触发器RTL代码:
module d_flip_flop (
input wire clk,
input wire d,
output reg q
);
always @(posedge clk) begin
q <= d;
end
endmodule
注意这里用的是 always @(posedge clk),表示只在时钟上升沿触发。赋值用的是 <=(非阻塞赋值),这是时序逻辑的标准写法。我刚开始学的时候,老搞混阻塞和非阻塞,后来吃过一次仿真波形不对的亏,才彻底记住了——时序逻辑用非阻塞,组合逻辑用阻塞。
个人小技巧: 写RTL时,看到 always @(posedge clk) 就条件反射用 <=。养成肌肉记忆,能省很多debug时间。
实际项目中,D触发器通常还会带复位信号。复位分两种:同步复位和异步复位。
4.1.1 同步复位 vs 异步复位
同步复位:只在时钟上升沿检查复位信号。异步复位:复位信号一有效,立刻复位,不管时钟。
// 同步复位 D触发器
module dff_sync_rst (
input wire clk,
input wire rst_n,
input wire d,
output reg q
);
always @(posedge clk) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
endmodule
// 异步复位 D触发器
module dff_async_rst (
input wire clk,
input wire rst_n,
input wire d,
output reg q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
endmodule
我曾经踩过的坑: 异步复位的敏感列表里必须把复位信号写全。有一次我漏写了 negedge rst_n,综合出来复位根本不起作用。仿真时看着波形发呆了好久才找到原因。
我个人习惯:能选异步复位就选异步复位。因为异步复位不依赖时钟,上电瞬间就能把电路拉到已知状态。但要注意,异步复位释放时不能离时钟沿太近,否则会出亚稳态——这个我们后面章节再细聊。
4.2 寄存器——D触发器的“多胞胎”
单个D触发器只能存1比特。那要存8比特、16比特呢?简单,把多个D触发器并排放,共用一个时钟和复位,就是寄存器了。
module reg_8bit (
input wire clk,
input wire rst_n,
input wire [7:0] d,
output reg [7:0] q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 8'b0;
else
q <= d;
end
endmodule
你看,代码几乎一模一样,只是位宽从1变成了8。这就是RTL设计的魅力——一旦掌握了基本单元,扩展起来非常自然。
寄存器在芯片里无处不在。我做过一个通信芯片,里面光配置寄存器就有上百个,每个寄存器控制着不同的模式、使能、状态。写RTL时,我习惯把寄存器地址和功能列成一个表格,方便自己和验证同事对照。
| 寄存器名称 | 地址 | 位宽 | 功能描述 |
|---|---|---|---|
| CTRL_REG | 0x00 | 8 | 控制寄存器:使能、模式选择 |
| STATUS_REG | 0x04 | 8 | 状态寄存器:中断标志、忙信号 |
| DATA_REG | 0x08 | 32 | 数据寄存器:收发数据缓存 |
4.3 移位寄存器——数据“动起来”
移位寄存器,说白了就是让数据在寄存器链里“搬家”。每个时钟周期,数据向右或向左移动一位。
最常见的应用是串并转换。比如一个串行数据进来,你想把它拼成一个字节,那就需要一个8位的移位寄存器。
module shift_reg_8bit (
input wire clk,
input wire rst_n,
input wire sin, // 串行输入
output wire [7:0] pout // 并行输出
);
reg [7:0] shift_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
shift_reg <= 8'b0;
else
shift_reg <= {shift_reg[6:0], sin};
end
assign pout = shift_reg;
endmodule
这段代码里,{shift_reg[6:0], sin} 就是把原来的低7位往左移,新进来的sin塞到最低位。每个时钟周期,数据就向左“蠕动”一格。
移位寄存器还有几种变体:
- SISO(串入串出):输入输出都是串行,就是一根线进一根线出。
- SIPO(串入并出):上面那个例子就是,串行进,并行出。
- PISO(并入串出):并行加载,然后串行移出去。SPI接口的发送端常用这个。
- PIPO(并入并出):其实就是普通寄存器。
我的一点经验: 写移位寄存器时,注意移位方向。左移是乘2,右移是除2。如果你在做数字信号处理,移位寄存器经常用来实现延迟线或者FIR滤波器。我做过一个FIR滤波器,里面用了三级移位寄存器来对齐数据,那代码写起来特别规整。
4.4 计数器——数字电路的“节拍器”
计数器,本质上就是一个寄存器加上加法器。每个时钟周期加1,计到某个值就归零或者做其他操作。
先看一个最简单的二进制计数器:
module counter_8bit (
input wire clk,
input wire rst_n,
output reg [7:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'b0;
else
count <= count + 1'b1;
end
endmodule
这个计数器从0跑到255,然后溢出回0。简单吧?但实际项目中,我们很少用这种“裸奔”的计数器。更多时候需要模N计数器——计到N-1就归零。
// 模10计数器(0~9循环)
module counter_mod10 (
input wire clk,
input wire rst_n,
output reg [3:0] count,
output reg carry
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 4'b0;
carry <= 1'b0;
end
else if (count == 4'd9) begin
count <= 4'b0;
carry <= 1'b1;
end
else begin
count <= count + 1'b1;
carry <= 1'b0;
end
end
endmodule
这个模10计数器,计到9的时候,下一个时钟归零,同时拉高carry信号。carry可以级联到下一个计数器,实现多位计数。
避坑指南: 我曾经在写计数器时,把比较条件写成了 count == 4'd10。你想想看,4位二进制最大只能表示15,计到10其实没问题,但逻辑上不对——我要的是0~9循环,计到9就该归零。写成10的话,计数器会跑到10才归零,多跑了一个数。这种bug仿真时很容易漏掉,因为波形上看10确实存在。所以,边界条件一定要反复确认。
计数器在芯片里太常用了:
- 分频:把高频时钟分频成低频时钟
- 定时:比如等待1000个时钟周期后触发某个操作
- 地址生成:顺序访问存储器时,用计数器产生地址
- 状态机索引:有限状态机里,用计数器记录停留的周期数
嗯,说到状态机,那是我们下一章的重点。计数器经常和状态机配合使用,比如在某个状态里等待N个周期再跳转。
4.5 小结
这一章我们聊了时序逻辑的四个基本模块:
- D触发器:1比特存储,时序逻辑的细胞
- 寄存器:多比特D触发器,数据暂存的主力
- 移位寄存器:数据串并转换、延迟、对齐的好帮手
- 计数器:分频、定时、地址生成,无处不在
这些模块的RTL写法其实都很固定,说白了就是 always @(posedge clk) 加上 <= 赋值。但真正用好它们,需要理解背后的硬件行为。我建议你每写完一个模块,都在脑子里过一遍:综合出来是什么电路?几个触发器?组合逻辑在哪里?
养成这个习惯,你的RTL设计水平会进步很快。
课后练习: 写一个8位的双向移位寄存器,支持左移、右移、并行加载、保持四种模式。控制信号用2位sel选择。试试看,写完后仿真一下波形。
可可精品资料库