计数器构成了一个基本的构建块。 它们有各种形状和形式......
本文引用地址:计数器 1 - 计数器
最简单的计数器
可以使用几行 Verilog 构建快速高效的计数器。例如,下面是一个 32 位计数器。
reg [31:0] cnt;
always @(posedge clk) cnt <= cnt+1;
此类计数器从 0 计数到 4294967295,然后回滚 0 以继续其进程。 它占用的资源很少,并且在中运行速度快,这要归功于隐藏的携带链(稍后会详细介绍)。 现在,让我们看看一些变化。
首先,最好明确给出一个起始值,即使它是 0。
reg [31:0] cnt = 0;
always @(posedge clk) cnt <= cnt+1;
请注意,如果我们不指定起始值,仿真工具将拒绝工作,并且某些合成工具也可能会自行更改起始值......因此,最好始终指定一个起始值。 我们也可以使用异步重置来指定起始值,但最简单的方法如上所示。
现在,如果您需要更多功能,这里有一个 10 位计数器(最多计数 1023)的示例,该计数器从 300 开始计数,并具有启用和方向控制。
reg [9:0] cnt = 10'd300; // 10bit counter, starts at 300
wire cnt_enable; // 0 to disable the counter, 1 to enable it
wire cnt_direction; // 0 to counter backward, 1 to count forward
always @(posedge clk) if(cnt_enable) cnt <= cnt_direction ? cnt+1 : cnt-1;
请注意,综合工具必须采取一些技巧才能使计数器从300开始。 FPGA 内部的触发器总是从 0 开始,因此您可以认为计数器初始值必须为 0...但是,通过在逻辑中放置一些位置良好的逆变器,任何起始值都是可能的。 逆变器在FPGA中是“免费”的,因此没有缺点。
计数器滴答声
假设我们需要一个“滴答”信号,该信号每 1024 个时钟断言一次。 最有可能的是,我们会创建一个 10 位计数器和一些逻辑来生成“滴答声”。 让我们看看如何做到这一点。
首先,我们制作 10 位计数器。它从 0 到 1023 计数。
reg [9:0] cnt = 0;
always @(posedge clk) cnt <= cnt+1;
现在我们可以决定,当计数器达到其最大值时(就在回滚到 0 之前),我们的“tick”被断言。
wire tick = (cnt==1023);
另一种写作方式是
wire tick = &cnt; // assert "tick" when all the cnt bits are 1这些滴答信号的缺点是它们会产生一大块逻辑(这里是 10 位 AND 门)。 只有 10 位没什么大不了的,但如果我们的计数器是 32 位或更大,那将是一种浪费。 另一种方法是依赖FPGA在后台使用的(通常是隐藏的)进位链。 我们只需要稍微扭动一下手臂来说服FPGA提供他隐藏的信息......
reg [31:0] cnt = 0; // 32bit counter
wire [32:0] cnt_next = cnt+1; // next value calculated with 33bit (one bit more than the counter)
always @(posedge clk) cnt <= cnt_next[31:0]; // now the counter uses only 32 bits
wire tick = cnt_next[32]; // but we access to the last bit of the carry chain to create the tick (asserted when the counter reaches its maximum value)
尝试一下,你会发现它的工作原理是一样的,但在FPGA中占用的空间更少 (注意:在撰写本文时,我们尝试了 ISE 和 Quartus-II,并且都以 0 作为起始值做得很好)。
计数器 2 - 特殊计数器
模量计数器
模量计数器是在其自然结束值之前回滚的计数器。 例如,假设你想要一个模数 10 计数器(从 0 到 9 计数),你可以写这个。
reg [3:0] cnt = 0; // we need 4 bits to be able to reach 9
always @(posedge clk)
if(cnt==9)
cnt <= 0;
else
cnt <= cnt+1;
或者这个(更紧凑一点)
reg [3:0] cnt = 0;
always @(posedge clk) cnt <= (cnt==9) ? 0 : cnt+1;
现在,如果您意识到我们实际上不需要将计数器的所有 4 位与 9 位进行比较,则可以进行一些(免费)优化。 下面的代码在比较中仅使用位 0 和位 3。
always @(posedge clk) cnt <= ((cnt & 9)==9) ? 0 : cnt+1;
灰色计数器
灰色计数器是一种二进制计数器,一次只有一个位发生变化。
以下是 4bit Gray 计数器的运行方式。
0000
0001
0011
0010
0110
0111
0101
0100
1100
1101
1111
1110
1010
1011
1001
1000
and then wraps back to 0000...
格雷码对于跨时钟域发送值很有用(这样它的不确定性仅为 1)。
创建 Gray 计数器的最简单方法是首先创建一个二进制计数器,然后将值转换为 Gray。
module GrayCounter(
input clk,
output [3:0] cnt_gray
);
reg [3:0] cnt = 0;
always @(posedge clk) cnt <= cnt+1; // 4bit binary counter
assign cnt_gray = cnt ^ cnt[3:1]; // then convert to gray
endmodule
也可以创建本机 Gray 计数器。
module GrayCounter(
input clk,
output reg [3:0] cnt_gray = 0
);
wire [3:0] cnt_cc = {cnt_cc[2:1] & ~cnt_gray[1:0], ^cnt_gray, 1'b1}; // carry-chain type logic
always @(posedge clk) cnt_gray <= cnt_gray ^ cnt_cc ^ cnt_cc[3:1];
endmodule
计数器 3 - LFSR 计数器
假设您想要一个或多或少“随机”计数的计数器,您可以使用 LFSR。
下面是一个示例。
如您所见,LFSR是带有一些XOR门的移位寄存器。 上图所示的是 8 抽头 LFSR(它使用 8 个人字拖)。
输出序列开始如下(假设所有触发器都从“1”开始):
11111111
11000111
11011011
11010101
11010010
01101001
10001100
01000110
00100011
10101001
11101100
01110110
00111011
10100101
...
这是 LFSR 源代码。
module LFSR8_11D(
input clk,
output reg [7:0] LFSR = 255 // put here the initial value
);
wire feedback = LFSR[7];
always @(posedge clk)
begin
LFSR[0] <= feedback;
LFSR[1] <= LFSR[0];
LFSR[2] <= LFSR[1] ^ feedback;
LFSR[3] <= LFSR[2] ^ feedback;
LFSR[4] <= LFSR[3] ^ feedback;
LFSR[5] <= LFSR[4];
LFSR[6] <= LFSR[5];
LFSR[7] <= LFSR[6];
end
endmodule
请注意,我们从 255 开始。 这是因为 0 是死胡同状态,因此我们可以选择除 0 以外的任何起始值。 之后,LFSR 在每个时钟周期更改值,但永远不会达到 0,因此在重新开始之前,它只经过 255 个 8 位值(256 位输出的 8 种可能组合)。
现在,我们可以只输出一位,而不是输出完整的 255 位(即选择任何触发器进行输出,将其他 0 位保留在内部)。 这会产生一个由 1 个 255 和 <> 组成的字符串,看起来是随机的(尽管它在 <> 个时钟后会重复)。 对噪声发生器很有用...
定制
您可以调整 LFSR:
选择不同数量的水龙头(我们在上面选择了 8 个)。
改变反馈网络的接线方式 - 例如更改XOR门的数量,它们的放置位置,或用XNOR替换XOR门。
某些反馈配置将创建可能值的孤岛。 例如,此 LFSR 看起来与第一个 LFSR 类似,但仅循环遍历 30 个值。
module LFSR8_105(
input clk,
output reg [7:0] LFSR = 255
);
wire feedback = LFSR[7];
always @(posedge clk)
begin
LFSR[0] <= feedback;
LFSR[1] <= LFSR[0];
LFSR[2] <= LFSR[1] ^ feedback;
LFSR[3] <= LFSR[2];
LFSR[4] <= LFSR[3];
LFSR[5] <= LFSR[4];
LFSR[6] <= LFSR[5];
LFSR[7] <= LFSR[6];
end
endmodule
也可以在反馈中添加一些逻辑,以便LFSR达到所有可能的状态。
module LFSR8_11D(
input clk,
output reg [7:0] LFSR = 255
);
wire feedback = LFSR[7] ^ (LFSR[6:0]==7'b0000000); // modified feedback allows reaching 256 states instead of 255
always @(posedge clk)
begin
LFSR[0] <= feedback;
LFSR[1] <= LFSR[0];
LFSR[2] <= LFSR[1] ^ feedback;
LFSR[3] <= LFSR[2] ^ feedback;
LFSR[4] <= LFSR[3] ^ feedback;
LFSR[5] <= LFSR[4];
LFSR[6] <= LFSR[5];
LFSR[7] <= LFSR[6];
end
endmodule
LFSR测试台
我们制作了一个小型 Windows 实用程序,允许对 LFSR 设计进行试验。
在此处下载。
计数器4-携带链
进位链是允许 FPGA 高效运算(计数器、加法器等)的功能。 让我们更多地了解使用计数器的进位链。 使用 T 字拖可以轻松构建计数器。
T 字拖非常简单。 在时钟上升沿,如果 T 输入为高电平,则其 Q 输出切换,如果 T 为低电平,则不会改变。
FPGA 内部使用 D 触发器,但 D 和 T 触发器很容易互换,只需一些逻辑即可。 因此,我们在此页面上使用T触发器,因为我们知道FPGA软件可以很容易地将它们映射到FPGA中。
纹波计数器
最小的二进制计数器是纹波计数器。 这是一个 4 位纹波计数器。
基本上,每个T触发器输出驱动下一个触发器的时钟。 它在硬件方面非常高效,但对于FPGA来说不是很好,因为我们现在的时钟域与计数器中的位数一样多。 FPGA 针对同步电路进行了优化,因此我们需要所有计数器位同时切换的东西。
同步计数器
在同步计数器中,时钟同时馈送所有触发器,因此只有一个时钟域。
现在,如果我们看一下二进制计数器的计数方式,我们会看到 bit0 总是切换,并且对于任何要切换的更高位,所有低阶位都需要为 1。 因此,我们的同步计数器通过使用几个 AND 门来形成。
只要柜台小就好。 我们的示例 4 位计数器只需要两个 AND 门(显然还有触发器),因此它非常高效。 但这并不能很好地扩展。 对于 32 位计数器,我们需要 30 个 AND 门,最后一个有 31 个输入...... 但是,我们可以很容易地以这种方式重新绘制计数器(这次我们制作了一个 6 位计数器)。
基本上,我们没有让 AND 门变大,而是将它们保持较小并将它们链接起来。
这就是 FPGA 实现计数器的方式。 它在硬件方面是高效的,但问题是速度...... 例如,一个 32 位计数器需要 30 个链式 AND 门。 而这个链是计数器“关键路径”(设置最大计数器时钟速度)的主要部分。 因此,保持这条道路的快速性很重要......FPGA 有一个很好的技巧来保持速度。 它被称为...
携带链
FPGA 由“逻辑元件”组成,每个元件包含一个 LUT 和一个 D 触发器。 每个逻辑元件可以实现一个计数器位(一个 32 位计数器需要 32 个逻辑元件)。
逻辑元素可以通过通用路由结构与周围环境进行通信,但这速度很慢。 因此,FPGA 设计人员确保并排放置的逻辑元件具有额外的本地路由信号。
实现为
此本地路由通常用作进位链。 每次要求FPGA软件实现二进制计数器时,它都会将位彼此相邻映射,以便将本地路由用作进位链。 这给映射增加了一些限制,但软件会处理它。
FPGA 制造商还确保逻辑元件针对沿携带链路径的速度进行了大量优化。 结果是计数器可以在数百MHz的频率下轻松运行...计数器的速度通常不是问题(FPGA设计的关键路径比进位链更有可能通过常规逻辑)。 当然,这取决于您希望以多快的速度运行设计。 大型计数器具有较长的进位链,因此无法像小型计数器那样快速计时。 如果这是一个问题,您可以分解进位链(即使用一系列小计数器)或选择不使用进位链的计数器架构。
对于那些喜欢冒险的人,请单击此处查看 Spartan-3A FPGA 设计中实现计数器的切片(两个逻辑元件)的 ISE FPGA 编辑器屏幕截图。 该视图适用于计数器的第 6 位和第 7 位。 我们可以立即识别出从下到上穿过中间切片的携带链。 不太明显的是 AND 门和 T 形触发器在哪里。 他们实际上都在那里...... AND门是使用运载链线上的大多路复用器制成的 T触发器使用异或门和D触发器输出,环回LUT输入(通过逻辑元件外部的布线)。 LUT 只是直通。
进位链也用于加法器和比较器。 但感谢数百名工程师致力于构建智能 HDL 工具,我们可以使用携带链的强大功能而不必担心它们。 生活是美好的。