D触发器、同步与异步复位、脉冲边沿检测

HDLBits链接


D触发器

基础知识介绍

如果把数字电路比作一个人的身体,那么触发器(Flip-Flop)就是这个身体的”记忆细胞”。它让数字电路有了记忆能力,这也是时序逻辑电路区别于组合逻辑电路的关键!

什么是D触发器?

D触发器(D Flip-Flop)是最常用的触发器之一。这里的”D”代表”Data”(数据)或”Delay”(延迟),因为它的功能就是在时钟边沿到来时,将D输入端的数据保存到输出端Q

生活中的类比:

想象一个拍照的场景:

  • D输入端就像是相机镜头前的场景
  • 时钟信号就像是你按下快门的那一刻
  • Q输出端就像是拍出来的照片

只有在你按下快门(时钟边沿)的瞬间,镜头前的场景(D输入)才会被保存下来变成照片(Q输出)。在其他时间,无论场景怎么变化,照片都不会改变!

核心概念定义:

  • 边沿触发:触发器只在时钟信号的上升沿(0→1跳变)或下降沿(1→0跳变)时才会更新状态
  • 状态保持:在非时钟边沿时刻,无论输入怎么变化,输出都保持不变
  • 次态(Next State):时钟边沿之后触发器的新状态,记为Q’
  • 现态(Current State):时钟边沿之前触发器的旧状态,记为Q

为什么边沿触发很重要?

在时钟电平有效的期间(比如时钟为高电平时),D输入的数据可能会发生多次变化。如果使用电平触发,输出也会跟着变来变去。而边沿触发只在时钟跳变的瞬间采样一次,这样大大提高了电路的稳定性和抗干扰能力!

边沿D触发器的功能表:

D CLK Q QN 说明
0 时钟上升沿 0 1 置0
1 时钟上升沿 1 0 置1
× 0 last Q last QN 保持(时钟为低)
× 1 last Q last QN 保持(时钟为高)

特性方程:

1
Q' = D  (在时钟上升沿时)

这个方程很简单,意思就是:时钟边沿到来后,Q变成D的值

1


同步复位与异步复位

基础知识介绍

在实际的数字系统中,我们经常需要让电路回到一个已知的初始状态,这就需要复位信号(Reset)。复位可以让触发器回到预设的状态(通常是0),这样系统才能从一个确定的状态开始工作。

复位主要有两种类型:同步复位异步复位

生活中的类比:

想象一个电梯的控制系统:

  • 同步复位:就像是电梯只有在到达某一层楼停下时(时钟边沿),才会响应”回到一楼”的按钮
  • 异步复位:就像是电梯的紧急停止按钮,无论电梯在哪里、在做什么,只要按下这个按钮,电梯立刻停止

同步复位

什么是同步复位?

“同步”的意思是复位信号需要和时钟信号同步才能生效。换句话说,复位信号只有在时钟边沿到来时,才会被电路识别和处理。

Verilog代码模板:

1
2
3
4
5
6
7
8
always @(posedge clk) begin
if (!rst_n) begin // 低电平有效的同步复位
// 复位逻辑
end
else begin
// 正常工作逻辑
end
end

同步复位的优缺点:

优点

  • 可以滤除复位信号上的毛刺(因为只在时钟边沿采样)
  • 有利于静态时序分析(STA)
  • 不会出现异步复位释放时的亚稳态问题

缺点

  • 需要时钟信号才能复位(如果时钟不工作,就无法复位)
  • 复位信号必须持续足够长的时间,保证能被时钟采样到
  • 会消耗更多的组合逻辑资源

异步复位

什么是异步复位?

“异步”的意思是复位信号不受时钟信号的控制。无论时钟边沿是否到来,只要复位信号有效,电路就会立即复位!

Verilog代码模板:

1
2
3
4
5
6
7
8
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin // 低电平有效的异步复位
// 复位逻辑
end
else begin
// 正常工作逻辑
end
end

注意:在敏感列表中,我们同时列出了时钟边沿和复位边沿!

异步复位的优缺点:

优点

  • 不需要时钟信号就能立即复位(即使时钟不工作也能复位)
  • 响应速度快,立即生效
  • 通常不需要额外的组合逻辑

缺点

  • 复位信号上的毛刺可能导致误复位
  • 异步复位释放时(复位信号从有效变无效),如果刚好在时钟边沿附近,可能导致亚稳态

如何选择同步复位还是异步复位?

这是一个经典的问题,答案是:看具体需求

在通信IC设计中,笔者的经验是:

  • 如果系统对复位的响应速度要求很高,或者需要在没有时钟的情况下也能复位 → 选择异步复位
  • 如果系统对可靠性要求很高,或者复位信号可能有毛刺 → 选择同步复位

入门者避坑指南

时序逻辑设计中有很多容易踩坑的地方,下面我们总结几个最常见的错误。

错误1:在时序逻辑中使用阻塞赋值

错误表现:

1
2
3
4
5
// ❌ 错误示例:在时序always块中使用阻塞赋值=
always @(posedge clk) begin
a = b;
c = a; // 这里的a是上面刚更新的值!
end

错误原因分析:

在Verilog中,有两种赋值方式:

  • 阻塞赋值(Blocking Assignment)=:立即执行,会阻塞后续语句的执行
  • 非阻塞赋值(Non-Blocking Assignment)<=:并行执行,不会阻塞后续语句

记住这个铁律:

  • 组合逻辑用阻塞赋值 =
  • 时序逻辑用非阻塞赋值 <=

正确做法对比:

1
2
3
4
5
// ✅ 正确示例:在时序always块中使用非阻塞赋值<=
always @(posedge clk) begin
a <= b;
c <= a; // 这里的a是时钟边沿之前的值!
end

调试技巧:

  • 如果不确定该用哪种赋值,先想想这是组合逻辑还是时序逻辑
  • 写代码时,把”时序逻辑用<=,组合逻辑用=”当成口头禅

错误2:异步复位的敏感列表写错

错误表现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ❌ 错误示例1:异步复位没有写在敏感列表中
always @(posedge clk) begin
if (rst) begin // rst是异步复位,但没有写在敏感列表里
q <= 1'b0;
end
else begin
q <= d;
end
end

// ❌ 错误示例2:敏感列表中写了rst,但没有写边沿
always @(posedge clk or rst) begin // 应该是posedge rst或negedge rst
if (rst) begin
q <= 1'b0;
end
else begin
q <= d;
end
end

错误原因分析:

对于异步复位,敏感列表中必须包含复位信号的边沿(posedge或negedge),否则综合器可能无法正确识别这是一个异步复位。

正确做法对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ✅ 正确示例:高电平有效的异步复位
always @(posedge clk or posedge rst) begin
if (rst) begin
q <= 1'b0;
end
else begin
q <= d;
end
end

// ✅ 正确示例:低电平有效的异步复位
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
q <= 1'b0;
end
else begin
q <= d;
end
end


错误3:在异步复位的if-else中放组合逻辑

错误表现:

1
2
3
4
5
6
7
8
9
10
// ❌ 错误示例:在异步复位的else分支中使用了组合逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
q <= 1'b0;
end
else begin
// 这里不能用assign!
assign q = a & b;
end
end

错误原因分析:

在时序always块中,我们应该只对寄存器进行赋值,不要在里面写assign语句或复杂的组合逻辑。组合逻辑应该放在单独的always块中,或者用连续赋值语句。

正确做法对比:

1
2
3
4
5
6
7
8
9
10
11
// ✅ 正确示例:把组合逻辑和时序逻辑分开
wire q_next = a & b; // 组合逻辑用连续赋值

always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
q <= 1'b0;
end
else begin
q <= q_next; // 时序逻辑只负责更新寄存器
end
end


错误4:锁存器(Latch)的意外生成

错误表现:

1
2
3
4
5
6
7
// ❌ 错误示例:组合逻辑always块中没有写完整的if-else
always @(*) begin
if (ena) begin
q = d;
end
// 缺少else分支!当ena=0时,q保持原值,生成锁存器
end

错误原因分析:

在组合逻辑always块中,如果某个信号在某些条件下没有被赋值,综合器就会认为这个信号需要”保持原值”,从而生成一个锁存器(Latch)。锁存器是电平敏感的存储元件,在设计中我们通常要避免意外生成锁存器。

正确做法对比:

如果确实需要锁存器(比如题目7明确要求),可以这样写:

1
2
3
4
5
6
7
// ✅ 正确示例:有意设计锁存器
always @(*) begin
if (ena) begin
q = d;
end
// 没有else,这时候确实需要锁存器
end

如果不需要锁存器,一定要确保所有信号在所有条件下都有赋值:

1
2
3
4
5
6
7
8
9
// ✅ 正确示例:避免生成锁存器
always @(*) begin
if (ena) begin
q = d;
end
else begin
q = 1'b0; // 给q一个默认值
end
end


错误5:在同一个always块中混合使用同步和异步复位

错误表现:

1
2
3
4
5
6
7
8
9
10
11
12
// ❌ 错误示例:想同时用同步和异步复位,结果逻辑混乱
always @(posedge clk or posedge rst1) begin
if (rst1) begin // 异步复位
q <= 1'b0;
end
else if (rst2) begin // 同步复位
q <= 1'b0;
end
else begin
q <= d;
end
end

错误原因分析:

虽然从语法上讲上面的代码是可以综合的,但这会让复位策略变得混乱。在一个设计中,建议统一使用一种复位方式,要么全同步,要么全异步,不要混用。

正确做法对比:

选择一种复位方式,坚持使用:

1
2
3
4
5
6
7
8
9
// ✅ 正确示例:统一使用异步复位
always @(posedge clk or posedge rst) begin
if (rst) begin
q <= 1'b0;
end
else begin
q <= d;
end
end


D触发器巩固练习

题目1:基本D触发器

题目描述:创建一个D触发器。

Solution1

1
2
3
4
5
6
7
8
9
10
11
12
module top_module (
input clk,
input d,
output reg q
);

// 基本的D触发器:时钟上升沿时,q <= d
always @(posedge clk) begin
q <= d;
end

endmodule

小贴士:
记住!时序逻辑的always块使用非阻塞赋值 <=


题目2:8位D触发器

题目描述:创建8个D触发器,每个都由时钟的上升沿触发。

Solution2

1
2
3
4
5
6
7
8
9
10
11
12
module top_module (
input clk,
input [7:0] d,
output reg [7:0] q
);

// 向量形式的D触发器,简洁高效!
always @(posedge clk) begin
q <= d;
end

endmodule

注意:
在Verilog中,向量赋值会自动对应到每一位,所以上面的代码等价于实例化了8个独立的D触发器。


题目3:带同步复位的8位D触发器

题目描述:创建8个D触发器与高电平有效同步复位。所有D触发器由clk的上升沿触发。

Solution3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module (
input clk,
input reset, // 高电平有效的同步复位
input [7:0] d,
output reg [7:0] q
);

always @(posedge clk) begin
if (reset) begin
q <= 8'd0; // 同步复位时,输出置0
end
else begin
q <= d; // 正常工作时,更新数据
end
end

endmodule

题目4:带同步复位的下降沿触发D触发器

题目描述:创建8个D触发器与高电平有效同步复位。触发器必须被重置为0x34,而不是0。所有D触发器应由clk的下降沿触发。

Solution4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module top_module (
input clk,
input reset,
input [7:0] d,
output reg [7:0] q
);

// 注意:这里使用negedge clk表示下降沿触发
always @(negedge clk) begin
if (reset) begin
q <= 8'h34; // 复位到指定值0x34(十六进制)
end
else begin
q <= d;
end
end

endmodule

题目5:带异步复位的8位D触发器

题目描述:创建8个D触发器与高电平有效异步复位。所有D触发器应由clk的上升沿触发。

Solution5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module top_module (
input clk,
input areset, // 高电平有效的异步复位
input [7:0] d,
output reg [7:0] q
);

// 敏感列表中同时包含时钟上升沿和复位上升沿
always @(posedge clk or posedge areset) begin
if (areset) begin
q <= 8'd0; // 异步复位立即生效
end
else begin
q <= d;
end
end

endmodule

提示:
使用posedge areset时,在if条件中应该使用if(areset)来判断,而不是if(!areset),否则逻辑会相反!


题目6:带字节使能的16位D触发器

题目描述:创建一个16位D触发器,有时我们仅需要修改部分触发器中的值。字节使能信号控制当前时钟周期中16个寄存器中哪个字节需被修改。byteena[1]控制高字节d[15:8],而byteena[0]控制低字节d[7:0]

resetn是一个同步低复位信号。

所有的D触发器由时钟的上升沿触发。

Solution6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module top_module (
input clk,
input resetn, // 低电平有效的同步复位
input [1:0] byteena, // 字节使能信号
input [15:0] d,
output reg [15:0] q
);

always @(posedge clk) begin
if (!resetn) begin
q <= 16'd0; // 同步复位
end
else begin
// 根据byteena信号决定更新哪个字节
if (byteena[1]) begin
q[15:8] <= d[15:8]; // 更新高字节
end
if (byteena[0]) begin
q[7:0] <= d[7:0]; // 更新低字节
end
// 注意:两个if是并列的,不是if-else!
end
end

endmodule

题目7:D锁存器

题目描述:实现下面的电路(锁存器)

2

Solution7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module (
input d,
input ena,
output reg q
);

// 这是一个锁存器(Latch),不是触发器!
// 注意:使用组合逻辑always块
always @(*) begin
if (ena) begin
q = d; // 阻塞赋值
end
// 没有else分支:当ena=0时,q保持原值
// 这会生成锁存器,在这个题目中是有意为之
end

endmodule

注意:
锁存器和触发器是不同的!锁存器是电平敏感的,而触发器是边沿敏感的。在大多数现代设计中,我们推荐使用触发器,但了解锁存器也很重要。


题目8:带异步复位的D触发器

题目描述:实现下面的电路

3

Solution8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module (
input clk,
input d,
input ar, // 异步复位(高电平有效)
output reg q
);

always @(posedge clk or posedge ar) begin
if (ar) begin
q <= 1'b0;
end
else begin
q <= d;
end
end

endmodule

题目9:带同步复位的D触发器

题目描述:实现下面的电路

4

Solution9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module (
input clk,
input d,
input r, // 同步复位(高电平有效)
output reg q
);

always @(posedge clk) begin
if (r) begin
q <= 1'b0;
end
else begin
q <= d;
end
end

endmodule

题目10:简单的有限状态机

题目描述:实现下面的电路

5

Solution10

1
2
3
4
5
6
7
8
9
10
11
module top_module (
input clk,
input in,
output reg out
);

always @(posedge clk) begin
out <= in ^ out; // 异或操作:in和out不同时输出1
end

endmodule

题目11:带多路选择器的触发器子模块

题目描述:考虑下图的电路:

6

假设要为这个电路实现分层的Verilog代码,使用一个子模块的三个实例,该子模块中有一个触发器和多路选择器。为这个子模块编写一个名为top_module的Verilog模块(包含一个触发器和多路选择器)。

Solution11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module top_module (
input clk,
input L,
input r_in,
input q_in,
output reg Q
);

always @(posedge clk) begin
// 三元操作符:条件 ? 真值 : 假值
// 相当于:if (L) Q <= r_in; else Q <= q_in;
Q <= L ? r_in : q_in;
end

endmodule

题目12:移位寄存器的一个阶段

题目描述:考虑下图的n-bit移位寄存器

7

为该电路的一个阶段编写一个Verilog模块顶层模块,包括触发器和多路选择器。

Solution12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module top_module (
input clk,
input w,
input R,
input E,
input L,
output reg Q
);

always @(posedge clk) begin
case({E, L})
2'b00: Q <= Q; // 保持原值
2'b01: Q <= R; // 加载R
2'b10: Q <= w; // 移位输入w
2'b11: Q <= R; // 优先加载R
endcase
end

endmodule

题目13:三个触发器的有限状态机

题目描述:给定如图所示的有限状态机电路,假设D触发器在机器开始之前被初始重置为零。

8

Solution13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module top_module (
input clk,
input x,
output z
);

reg q1, q2, q3;

always @(posedge clk) begin
q1 <= x ^ q1;
q2 <= x & ~q2;
q3 <= x | ~q3;
end

// 输出逻辑:三个触发器或非
assign z = ~(q1 | q2 | q3);

endmodule

题目14:用D触发器实现JK触发器

题目描述:JK触发器有下面的真值表。只使用D触发器和逻辑门实现JK触发器。注:Qold是时钟上升沿前的D触发器的输出。

J K Q 说明
0 0 Qold 保持
0 1 0 置0
1 0 1 置1
1 1 ~Qold 翻转

Solution14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module (
input clk,
input j,
input k,
output reg Q
);

always @(posedge clk) begin
case({j, k})
2'b00: Q <= Q; // 保持
2'b01: Q <= 1'b0; // 置0
2'b10: Q <= 1'b1; // 置1
2'b11: Q <= ~Q; // 翻转
endcase
end

endmodule

脉冲边沿检测

基础知识介绍

在数字系统中,我们经常需要检测信号的变化——比如信号什么时候从0变成1,或者从1变成0。这就是边沿检测(Edge Detection)。

什么是边沿?

边沿就是信号电平发生跳变的瞬间:

  • 上升沿(Rising Edge):信号从0变成1(0→1)
  • 下降沿(Falling Edge):信号从1变成0(1→0)

生活中的类比:

想象你在看一条波浪线:

  • 上升沿就像波浪从谷底升到波峰
  • 下降沿就像波浪从波峰降到谷底

边沿检测就是要找出这些”转折点”!

为什么需要边沿检测?

在通信系统中,边沿检测非常重要:

  • 检测数据帧的开始和结束
  • 同步时钟和数据
  • 识别控制信号的变化
  • 捕捉短暂的脉冲信号

边沿检测的基本原理

边沿检测的核心思想很简单:比较信号的当前值和前一个值

  • 如果之前是0,现在是1 → 检测到上升沿
  • 如果之前是1,现在是0 → 检测到下降沿
  • 如果之前和现在一样 → 没有边沿

如何实现?

我们需要用一个D触发器来保存信号的前一个值,然后把当前值和前一个值进行比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 保存前一个值
always @(posedge clk) begin
prev_in <= in; // prev_in保存的是上一个时钟周期的in值
end

// 上升沿检测:之前是0,现在是1
assign rise_edge = ~prev_in & in;

// 下降沿检测:之前是1,现在是0
assign fall_edge = prev_in & ~in;

// 任意边沿检测:只要有变化就为1
assign any_edge = prev_in ^ in;

这个逻辑是不是很直观?


边沿检测电路的时序图

让我们用一个简单的时序图来理解边沿检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
时钟:     ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐
└─┘ └─┘ └─┘ └─┘ └─┘

输入in: ────────┐ ┌─────────
└─────┘

prev_in: ───┐ ┌─────┐
└─────┘ └──────

rise_edge:───────┐ ┌──────────────
└─┘

fall_edge:───────────┐ ┌──────────
└─┘

从这个时序图可以清楚地看到:

  • rise_edge只在in从0变1后的那个时钟周期为1
  • fall_edge只在in从1变0后的那个时钟周期为1

带滤波的边沿检测

在实际的硬件环境中,信号上可能会有毛刺(Glitch)——这是一种短暂的、不希望出现的电平跳变。如果我们直接用简单的边沿检测,可能会把这些毛刺误判为真实的信号跳变。

如何解决?

我们可以用多个级联的触发器来对信号进行采样,然后判断信号是否真的发生了稳定的跳变。

这是一个实际工程中常用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module edge_detector (
input clk,
input rst_n,
input rs232_rx, // 输入信号,可能有毛刺
output neg_edge // 检测到的下降沿
);

// 4级寄存器级联,用于滤波
reg rs232_rx0, rs232_rx1, rs232_rx2, rs232_rx3;

always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rs232_rx0 <= 1'b0;
rs232_rx1 <= 1'b0;
rs232_rx2 <= 1'b0;
rs232_rx3 <= 1'b0;
end
else begin
rs232_rx0 <= rs232_rx;
rs232_rx1 <= rs232_rx0;
rs232_rx2 <= rs232_rx1;
rs232_rx3 <= rs232_rx2;
end
end

// 只有当rs232_rx3和rs232_rx2都是1,
// 且rs232_rx1和rs232_rx0都是0时,才认为是真正的下降沿
// 这样可以滤除持续时间小于3个时钟周期的毛刺
assign neg_edge = rs232_rx3 & rs232_rx2 & ~rs232_rx1 & ~rs232_rx0;

endmodule

这个电路的原理是:只有当信号稳定地从1变成0(连续两个周期都是1,然后连续两个周期都是0),我们才认为是真正的下降沿。这样可以有效滤除毛刺!


边沿检测巩固练习

题目1:上升沿检测

题目描述:对于8位向量中的每一位,检测输入信号何时从一个时钟周期的0变化到下一个时钟周期的1(正边缘检测)。输出位应该在发生0到1转换后的周期输出1,如下示意图所示:

10

Solution1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module top_module (
input clk,
input [7:0] in,
output reg [7:0] pedge // positive edge
);

reg [7:0] temp_in; // 保存上一个周期的in值

always @(posedge clk) begin
temp_in <= in;
// 上升沿:之前是0,现在是1 → ~temp_in & in
pedge <= ~temp_in & in;
end

endmodule

题目2:任意边沿检测

题目描述:对于8位向量中的每一位,检测输入信号何时从一个时钟周期变化到下一个时钟周期(检测脉冲边沿)。输出位应该在发生转换后的周期输出1。

11

Solution2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module top_module (
input clk,
input [7:0] in,
output reg [7:0] anyedge
);

reg [7:0] temp_in;

always @(posedge clk) begin
temp_in <= in;
// 异或操作:两个值不同就输出1(任意边沿)
anyedge <= temp_in ^ in;
end

endmodule

题目3:下降沿捕获(带保持)

题目描述:对于32位向量中的每一位,当输入信号从一个时钟周期的1变化到下一个时钟周期的0时捕获(捕捉下降沿),”捕获”意味着输出将保持1直到被reset(同步重置)。

每个输出位的行为就像一个SR触发器:输出位应该在发生1到0转换后的周期被设置(为1)。当复位为高时,输出位应该在正时钟边缘复位(为0)。如果上述两个事件同时发生,则reset具有优先级。

在下面示例波形的最后4个周期中,”reset”事件比”set”事件早一个周期发生,因此这里不存在冲突。

12

Solution3(方法1:用case语句)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module top_module (
input clk,
input reset,
input [31:0] in,
output reg [31:0] out
);

reg [31:0] temp_in;
integer i;

always @(posedge clk) begin
temp_in <= in;
for (i = 0; i < 32; i = i + 1) begin
case({temp_in[i] & ~in[i], reset})
2'b10: out[i] <= 1'b1; // 检测到下降沿,置1
2'b11: out[i] <= 1'b0; // 同时发生,复位优先
2'b01: out[i] <= 1'b0; // 复位
default: out[i] <= out[i]; // 保持
endcase
end
end

endmodule

Solution3(方法2:更简洁的写法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module top_module (
input clk,
input reset,
input [31:0] in,
output reg [31:0] out
);

reg [31:0] temp_in;

// 先把temp_in更新(分开写更清晰)
always @(posedge clk) begin
temp_in <= in;
end

// 再处理输出逻辑
always @(posedge clk) begin
if (reset) begin
out <= 32'd0;
end
else begin
// (temp_in & ~in)检测下降沿,| out表示保持
out <= (temp_in & ~in) | out;
end
end

endmodule

题目4:时钟双沿触发器

题目描述:实现一个在时钟上升沿和下降沿都能采样的触发器。

注意:不能直接写 always @(posedge clk or negedge clk),这在Verilog中是不允许的。

Solution4(方法1:使用两个触发器和多路选择器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module top_module (
input clk,
input d,
output q
);

reg q1, q2;

// 上升沿采样
always @(posedge clk) begin
q1 <= d;
end

// 下降沿采样
always @(negedge clk) begin
q2 <= d;
end

// 根据当前时钟电平选择输出
assign q = clk ? q1 : q2;

endmodule

Solution4(方法2:使用异或电路)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module top_module (
input clk,
input d,
output q
);

reg q1, q2;

always @(posedge clk) begin
q1 <= d ^ q2;
end

always @(negedge clk) begin
q2 <= d ^ q1;
end

assign q = q1 ^ q2;

endmodule

小贴士:
我们知道一个数异或另一个数再异或同一个数,会得到原来的数。这就是最简单的加密与解密原理!

第二种方法相较于第一种方法,不需要用clk信号进行选择,这样可以避免在输出多路选择器处产生毛刺。在实际工程中,推荐使用第二种方法。


总结

在这篇文章中,我们深入学习了时序逻辑设计的核心内容:

  • D触发器:数字电路的”记忆细胞”,在时钟边沿保存数据
  • 同步复位与异步复位:两种复位方式各有优缺点,需要根据具体需求选择
  • 入门者避坑指南:总结了5个最常见的错误,包括阻塞/非阻塞赋值、敏感列表、锁存器生成等
  • D触发器巩固练习:通过14道练习题,掌握各种触发器的设计方法
  • 脉冲边沿检测:检测信号的上升沿、下降沿,以及带滤波的边沿检测
  • 边沿检测巩固练习:通过4道练习题,掌握边沿检测电路的设计

从通信IC设计的角度来看,触发器和边沿检测是非常基础但又极其重要的内容。在通信芯片中,我们需要处理大量的时序逻辑,从简单的数据缓存到复杂的协议状态机,都离不开触发器。而边沿检测则在数据恢复、时钟同步、信号识别等场景中有着广泛的应用。

希望这篇文章能帮助你打好时序逻辑设计的基础!