Verilog计数器

HDLBits链接


前言

计数器可以说是数字电路中最常用的模块之一了!无论是在项目开发、面试应聘还是实际工作中,计数器几乎无处不在。从简单的时序控制到复杂的协议处理,计数器都扮演着重要的角色。

在这篇文章中,我们将通过一系列练习题,从最基本的二进制计数器开始,逐步学习如何设计各种功能的计数器。


基础知识介绍

什么是计数器?

计数器就是能够对时钟脉冲进行计数的时序电路。它的主要功能是记录脉冲的个数,也可以用来实现分频、定时、产生节拍脉冲等功能。

生活中的类比:

想象一下汽车上的里程表——它随着汽车的行驶不断累加数字,这就是一个计数器!或者你在健身房用的跑步机,它会记录你跑了多少圈,这也是一个计数器。

计数器的分类

计数器有很多种分类方式:

  1. 按计数进制分

    • 二进制计数器:按二进制规律计数
    • 十进制计数器:按十进制规律计数
    • N进制计数器:其他任意进制
  2. 按计数增减分

    • 加法计数器:只做加法计数(越来越大)
    • 减法计数器:只做减法计数(越来越小)
    • 可逆计数器:既可加又可减
  3. 按复位方式分

    • 同步复位:复位信号需要等待时钟边沿才能生效
    • 异步复位:复位信号立即生效,不需要时钟

入门者避坑指南

在设计计数器时,初学者很容易犯一些常见的错误,下面我们来总结一下。

错误1:计数器溢出判断错误

错误表现:

1
2
3
4
5
6
7
8
9
10
11
12
// ❌ 错误示例:判断条件不对,导致计数到9后没有正确回0
always @(posedge clk) begin
if (reset) begin
q <= 4'd0;
end
else if (q == 4'd10) begin // 应该是q == 4'd9!
q <= 4'd0;
end
else begin
q <= q + 1'd1;
end
end

错误原因分析:

对于0-9的十进制计数器,当计数到9后,下一个时钟周期应该回到0。如果判断条件写成 q == 4'd10,那么计数器会先到10,然后才回到0,这样就多了一个状态!

正确做法对比:

1
2
3
4
5
6
7
8
9
10
11
12
// ✅ 正确示例:在q == 9时就判断要回0
always @(posedge clk) begin
if (reset) begin
q <= 4'd0;
end
else if (q == 4'd9) begin // 计数到9,下一个是0
q <= 4'd0;
end
else begin
q <= q + 1'd1;
end
end


错误2:忘记指定位宽

错误表现:

1
2
// ❌ 错误示例:没有指定位宽,可能导致意外的位宽扩展
q <= q + 1; // 1是32位的!

错误原因分析:

在Verilog中,未指定位宽的常量默认是32位(或更大)的。虽然在简单的计数器中这个问题可能不会立即显现,但在某些情况下会导致意外的结果。

正确做法对比:

1
2
// ✅ 正确示例:明确指定位宽
q <= q + 4'd1; // 4位的1


错误3:在组合逻辑中写计数器

错误表现:

1
2
3
4
5
6
7
8
9
// ❌ 错误示例:在组合逻辑always块中写计数器
always @(*) begin
if (reset) begin
q = 4'd0;
end
else begin
q = q + 1'd1; // 这里会形成组合环路!
end
end

错误原因分析:

计数器必须用时序逻辑(时钟边沿触发)来实现!如果用组合逻辑来写,会形成无限循环,电路会不停地振荡。

正确做法对比:

1
2
3
4
5
6
7
8
9
// ✅ 正确示例:用时序逻辑always块
always @(posedge clk) begin
if (reset) begin
q <= 4'd0;
end
else begin
q <= q + 1'd1;
end
end


错误4:使能信号处理不当

错误表现:

1
2
3
4
5
6
7
8
9
10
// ❌ 错误示例:没有使能信号时,计数器在else分支没有保持原值
always @(posedge clk) begin
if (reset) begin
q <= 4'd0;
end
else if (enable) begin
q <= q + 1'd1;
end
// 缺少else分支!虽然综合器会自动保持,但显式写出来更清晰
end

错误原因分析:

虽然在时序逻辑中,如果某个条件下没有给寄存器赋值,综合器会默认保持原值,但显式地写出来可以让代码更清晰,也更容易理解。

正确做法对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ✅ 正确示例:显式写出所有情况
always @(posedge clk) begin
if (reset) begin
q <= 4'd0;
end
else if (enable) begin
if (q == 4'd9) begin
q <= 4'd0;
end
else begin
q <= q + 1'd1;
end
end
else begin
q <= q; // 显式保持
end
end


题库

题目1:4位二进制计数器

题目描述:构建一个从0到15的4位二进制计数器,周期为16。同步复位,复位应该将计数器重置为0。

1

Solution1

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

always @(posedge clk) begin
if (reset) begin
q <= 4'd0; // 复位到0
end
else begin
q <= q + 4'd1; // 每个时钟周期加1
end
end

endmodule

注意:
4位计数器可以自然地从0计数到15(2^4 - 1),然后在下一个时钟周期自动回到0,不需要额外的判断逻辑!


题目2:0-9十进制计数器

题目描述:构建一个从0到9(包括9)的十进制计数器,其周期为10。同步复位,复位应该将计数器重置为0。

2

Solution2

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

always @(posedge clk) begin
if (reset || q >= 4'd9) begin
q <= 4'd0; // 复位或计数到9时回到0
end
else begin
q <= q + 4'd1; // 正常计数
end
end

endmodule

题目3:1-10十进制计数器

题目描述:制作一个从1到10的十进制计数器。同步复位,复位应该将计数器复位为1。

3

Solution3

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

always @(posedge clk) begin
if (reset || q >= 4'd10) begin
q <= 4'd1; // 复位或计数到10时回到1
end
else begin
q <= q + 4'd1; // 正常计数
end
end

endmodule

题目4:带使能的0-9十进制计数器

题目描述:构建一个从0到9(包括9)的十进制计数器,其周期为10。同步复位,复位应该将计数器重置为0。我们希望能够暂停计数器,而不是总是在每个时钟周期中递增,因此 slowena 输入指示计数器应该何时递增。

4

Solution4

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 slowena, // 计数使能信号
input reset,
output reg [3:0] q
);

always @(posedge clk) begin
if (reset) begin
q <= 4'd0;
end
else if (slowena) begin
if (q == 4'd9) begin
q <= 4'd0; // 计数到9,回到0
end
else begin
q <= q + 4'd1;
end
end
// else分支:slowena=0时,保持q不变
end

endmodule

题目4(补充):1-12计数器

题目描述:设计一个1-12计数器。

Solution4(补充)

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 reset,
input enable,
output [3:0] Q,
output c_enable,
output c_load,
output [3:0] c_d
);

// 控制信号生成
assign c_enable = enable;
assign c_load = reset | ((Q == 4'd12) && (enable == 1'b1));
assign c_d = c_load ? 4'd1 : 4'd0;

// 例化现成的4位计数器模块
count4 the_counter (
.clk(clk),
.enable(c_enable),
.load(c_load),
.d(c_d),
.Q(Q)
);

endmodule

题目5:BCD计数器降频(1kHz → 1Hz)

题目描述:例化BCD模块实现降频操作,1kHz → 1Hz。

Solution5

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
module top_module (
input clk,
input reset,
output OneHertz,
output [2:0] c_enable
);

wire [3:0] one, ten, hundred; // 个位、十位、百位

// 生成每一位的使能信号
// 百位:只有当个位和十位都是9时才加1
// 十位:只有当个位是9时才加1
// 个位:总是使能
assign c_enable = {
one == 4'd9 && ten == 4'd9, // 百位使能
one == 4'd9, // 十位使能
1'b1 // 个位使能
};

// 当三位都是9时,输出1Hz脉冲
assign OneHertz = (one == 4'd9 && ten == 4'd9 && hundred == 4'd9);

// 例化三个BCD计数器
bcdcount counter0 (clk, reset, c_enable[0], one);
bcdcount counter1 (clk, reset, c_enable[1], ten);
bcdcount counter2 (clk, reset, c_enable[2], hundred);

endmodule

题目6:4位BCD计数器

题目描述:构建一个4位BCD(二进制编码的十进制)计数器。每个十进制数字使用4位进行编码:q[3:0] 是个位,q[7:4] 是十位,以此类推。各进制上的进位时也需输出一个使能信号,指示三位数字何时应该增加。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
module top_module (
input clk,
input reset, // 高电平有效的同步复位
output [3:1] ena, // 十位、百位、千位的使能信号
output [15:0] q
);

reg [3:0] ones; // 个位
reg [3:0] tens; // 十位
reg [3:0] hundreds; // 百位
reg [3:0] thousands;// 千位

// 个位计数器
always @(posedge clk) begin
if (reset) begin
ones <= 4'd0;
end
else if (ones == 4'd9) begin
ones <= 4'd0;
end
else begin
ones <= ones + 4'd1;
end
end

// 十位计数器
always @(posedge clk) begin
if (reset) begin
tens <= 4'd0;
end
else if (tens == 4'd9 && ones == 4'd9) begin
tens <= 4'd0;
end
else if (ones == 4'd9) begin
tens <= tens + 4'd1;
end
end

// 百位计数器
always @(posedge clk) begin
if (reset) begin
hundreds <= 4'd0;
end
else if (hundreds == 4'd9 && tens == 4'd9 && ones == 4'd9) begin
hundreds <= 4'd0;
end
else if (tens == 4'd9 && ones == 4'd9) begin
hundreds <= hundreds + 4'd1;
end
end

// 千位计数器
always @(posedge clk) begin
if (reset) begin
thousands <= 4'd0;
end
else if (thousands == 4'd9 && hundreds == 4'd9 && tens == 4'd9 && ones == 4'd9) begin
thousands <= 4'd0;
end
else if (hundreds == 4'd9 && tens == 4'd9 && ones == 4'd9) begin
thousands <= thousands + 4'd1;
end
end

// 输出拼接
assign q = {thousands, hundreds, tens, ones};

// 生成使能信号
assign ena[1] = (ones == 4'd9);
assign ena[2] = (ones == 4'd9) && (tens == 4'd9);
assign ena[3] = (ones == 4'd9) && (tens == 4'd9) && (hundreds == 4'd9);

endmodule

小贴士:
把进位条件用单独的assign语句列出来,可以让代码的层次感更加清晰,也更容易调试!


题目7:12小时时钟计数器

题目描述:创建一组适合作为12小时的时钟使用的计数器(带有am/pm指示器)。你的计数器是由一个快速运行的clk驱动,每次时钟增加时ena必须为1。reset将时钟重置到中午12点。上午时pm=0,下午时pm=1。hh,mm和ss分别是小时(01-12)、分钟(00-59)和秒(00-59)的两个BCD(二进制编码的十进制)数字。

Reset比enable具有更高的优先级,并且即使在没有启用时也会发生。

下面的时序图显示了从11:59:59 AM到12:00:00 PM的翻转行为以及同步的Reset和enable行为。

5

Solution7

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
module top_module(
input clk,
input reset,
input ena,
output pm,
output [7:0] hh,
output [7:0] mm,
output [7:0] ss
);

reg pm_temp;
reg [3:0] ss_ones; // 秒个位
reg [3:0] ss_tens; // 秒十位
reg [3:0] mm_ones; // 分个位
reg [3:0] mm_tens; // 分十位
reg [3:0] hh_ones; // 时个位
reg [3:0] hh_tens; // 时十位

// 定义中间条件信号,让逻辑更清晰
wire add_ss_ones;
wire end_ss_ones;
wire add_ss_tens;
wire end_ss_tens;
wire add_mm_ones;
wire end_mm_ones;
wire add_mm_tens;
wire end_mm_tens;
wire add_hh_ones;
wire end_hh_ones_0;
wire end_hh_ones_1;
wire add_hh_tens;
wire end_hh_tens_0;
wire end_hh_tens_1;
wire pm_ding;

// ====================== 秒的个位 ======================
assign add_ss_ones = ena;
assign end_ss_ones = add_ss_ones && (ss_ones == 4'd9);

always @(posedge clk) begin
if (reset) begin
ss_ones <= 4'd0;
end
else if (add_ss_ones) begin
if (end_ss_ones) begin
ss_ones <= 4'd0;
end
else begin
ss_ones <= ss_ones + 4'd1;
end
end
end

// ====================== 秒的十位 ======================
assign add_ss_tens = end_ss_ones;
assign end_ss_tens = add_ss_tens && (ss_tens == 4'd5);

always @(posedge clk) begin
if (reset) begin
ss_tens <= 4'd0;
end
else if (add_ss_tens) begin
if (end_ss_tens) begin
ss_tens <= 4'd0;
end
else begin
ss_tens <= ss_tens + 4'd1;
end
end
end

// ====================== 分的个位 ======================
assign add_mm_ones = end_ss_tens;
assign end_mm_ones = add_mm_ones && (mm_ones == 4'd9);

always @(posedge clk) begin
if (reset) begin
mm_ones <= 4'd0;
end
else if (add_mm_ones) begin
if (end_mm_ones) begin
mm_ones <= 4'd0;
end
else begin
mm_ones <= mm_ones + 4'd1;
end
end
end

// ====================== 分的十位 ======================
assign add_mm_tens = end_mm_ones;
assign end_mm_tens = add_mm_tens && (mm_tens == 4'd5);

always @(posedge clk) begin
if (reset) begin
mm_tens <= 4'd0;
end
else if (add_mm_tens) begin
if (end_mm_tens) begin
mm_tens <= 4'd0;
end
else begin
mm_tens <= mm_tens + 4'd1;
end
end
end

// ====================== 时的个位 ======================
assign add_hh_ones = end_mm_tens;
assign end_hh_ones_0 = add_hh_ones && (hh_ones == 4'd9);
assign end_hh_ones_1 = add_hh_ones && ((hh_ones == 4'd2) && (hh_tens == 4'd1));

always @(posedge clk) begin
if (reset) begin
hh_ones <= 4'd2; // 复位到12点,个位是2
end
else if (add_hh_ones) begin
if (end_hh_ones_0) begin
hh_ones <= 4'd0;
end
else if (end_hh_ones_1) begin
hh_ones <= 4'd1; // 12点后回到1点
end
else begin
hh_ones <= hh_ones + 4'd1;
end
end
end

// ====================== 时的十位 ======================
assign add_hh_tens = end_mm_tens;
assign end_hh_tens_0 = add_hh_tens && end_hh_ones_1;
assign end_hh_tens_1 = add_hh_tens && end_hh_ones_0;

always @(posedge clk) begin
if (reset) begin
hh_tens <= 4'd1; // 复位到12点,十位是1
end
else if (add_hh_tens) begin
if (end_hh_tens_0) begin
hh_tens <= 4'd0; // 12点后变成0
end
else if (end_hh_tens_1) begin
hh_tens <= hh_tens + 4'd1;
end
end
end

// ====================== AM/PM标志 ======================
always @(posedge clk) begin
if (reset) begin
pm_temp <= 1'd0; // 复位到中午,AM
end
else if (pm_ding) begin
pm_temp <= ~pm_temp; // 11:59:59 AM → 12:00:00 PM
end
end

// 在11:59:59的时候,下一个就是12:00:00,需要翻转AM/PM
assign pm_ding = (hh_tens == 4'd1) && (hh_ones == 4'd1) && end_mm_tens;

// ====================== 输出 ======================
assign ss = {ss_tens, ss_ones};
assign mm = {mm_tens, mm_ones};
assign hh = {hh_tens, hh_ones};
assign pm = pm_temp;

endmodule

总结

在这篇文章中,我们通过一系列练习题,深入学习了计数器的设计:

  • 基本计数器:从简单的4位二进制计数器开始
  • 十进制计数器:0-9、1-10等特殊计数范围的设计
  • 带使能的计数器:可以暂停计数
  • BCD计数器:处理多位十进制数的计数
  • 12小时时钟:一个复杂的综合应用例子

从通信IC设计的角度来看,计数器是非常基础但又极其重要的模块。在通信芯片中,我们需要用计数器来做各种定时、分频、序列产生等工作。特别是在协议处理中,计数器往往是状态机的重要组成部分。

希望这篇文章能帮助你掌握计数器的设计技巧!