4. 计数器设计:二进制计数器、模N计数器、可逆计数器、计数器在分频中的应用
计数器这东西,说白了就是数字电路里的「记步器」。我做了这么多年芯片,几乎每个项目都离不开它。从最简单的时钟分频,到复杂的时序控制,计数器都是最基础也最实用的模块之一。今天咱们就把计数器这摊事儿彻底聊透。
4.1 二进制计数器
二进制计数器是最基础的计数器。它就是一个寄存器,每个时钟周期加1。当计数值达到全1后,下一个时钟就翻回0。嗯,这就是所谓的「自动回绕」。
我个人习惯把二进制计数器分成两种:同步复位和异步复位。同步复位更安全,但异步复位更省资源。你想想看,在高速设计中,异步复位容易引起时序问题,所以我一般推荐用同步复位。
核心要点:二进制计数器的位宽决定了最大计数值。N位计数器,最大能计到2^N - 1。
来看一个4位二进制计数器的Verilog代码:
module binary_counter (
input wire clk,
input wire rst_n,
input wire en,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0;
else if (en)
count <= count + 1'b1;
end
endmodule
这段代码很简单,但有个坑——count + 1'b1这个加法器,综合出来会消耗逻辑资源。我在项目中遇到过有人为了省资源,直接用count <= count + 1,结果综合工具推断出了32位加法器,白白浪费了面积。
小技巧:写计数器时,加数一定要指定位宽。用1'b1而不是1,这样综合工具就知道你只需要1位加法。
4.2 模N计数器
模N计数器,就是计数到N-1后回0。比如模10计数器,计到9就回0。这玩意儿在数字时钟、频率计里特别常见。
实现模N计数器有两种思路:
- 检测回绕法:当计数值等于N-1时,下一个时钟清零
- 比较器法:用比较器判断是否达到N-1
我个人更推荐比较器法,虽然多一个比较器,但时序更清晰。我曾经在某个项目中用了回绕法,结果因为组合逻辑路径太长,导致建立时间违例,折腾了两天才搞定。
来看模10计数器的代码:
module mod10_counter (
input wire clk,
input wire rst_n,
input wire en,
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 (en) begin
if (count == 4'd9) begin
count <= 4'b0;
carry <= 1'b1;
end
else begin
count <= count + 1'b1;
carry <= 1'b0;
end
end
end
endmodule
注意:模N计数器的比较值N-1必须用十进制或参数化写法,不要用魔法数字。我曾经见过有人写count == 4'd9,后来改成模16时忘了改比较值,结果芯片功能全乱套了。
4.3 可逆计数器
可逆计数器,也叫加减计数器。它有一个方向控制信号,为1时加计数,为0时减计数。这东西在数字锁相环、自动增益控制里用得很多。
可逆计数器的难点在于边界处理。加计数到最大值要回绕,减计数到0也要回绕。嗯,这里要注意,回绕逻辑必须和方向信号配合好。
来看一个4位可逆计数器的代码:
module updown_counter (
input wire clk,
input wire rst_n,
input wire en,
input wire up, // 1:加计数, 0:减计数
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0;
else if (en) begin
if (up)
count <= count + 1'b1;
else
count <= count - 1'b1;
end
end
endmodule
这段代码看起来没问题,但实际项目中我踩过坑——当count为0且up为0时,减1会变成4'b1111。这在某些应用里是允许的,但如果你需要饱和输出(到0就停),就得加饱和逻辑。
避坑指南:我曾经在一个音频处理芯片里用了可逆计数器,没加饱和逻辑。结果音量调到最小后继续减,音量直接爆满,把客户吓一跳。从那以后,我所有可逆计数器都默认加饱和选项。
4.4 计数器在分频中的应用
分频是计数器最经典的应用。说白了,就是用计数器把高频时钟变成低频时钟。比如系统时钟是100MHz,你需要一个1MHz的时钟,那就用计数器计50个周期(占空比50%的话,计25个高、25个低)。
分频器有两种:
- 偶数分频:直接计数器翻转,简单
- 奇数分频:需要双边沿触发,稍微复杂
来看一个偶数分频的例子——10分频:
module div10 (
input wire clk,
input wire rst_n,
output reg clk_out
);
reg [2:0] count; // 需要计到4,3位就够了
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 3'b0;
clk_out <= 1'b0;
end
else begin
if (count == 3'd4) begin
count <= 3'b0;
clk_out <= ~clk_out;
end
else
count <= count + 1'b1;
end
end
endmodule
奇数分频呢?比如5分频,需要占空比50%。做法是用两个计数器,一个在上升沿触发,一个在下降沿触发,然后组合输出。嗯,这里不展开细讲,但记住一点——奇数分频的代码一定要做仿真验证,我见过太多人在这上面翻车。
经验之谈:分频器的输出时钟,一定要用寄存器输出,不要用组合逻辑。组合逻辑输出的时钟毛刺多,容易导致下游电路误触发。我早期一个项目就是吃了这个亏,流片回来时钟老是不稳定,查了三天才发现是分频输出没打寄存器。
知识体系总览
下面这张图,是我自己总结的计数器知识体系。你把它理清楚了,计数器这块基本就通了。
这张图把计数器分成了三个层次:基础型(二进制)、扩展型(模N、可逆)、应用型(分频)。你从基础开始学,一步步往上走,很快就能掌握。
好了,计数器这块就聊到这儿。记住我一句话——计数器虽小,但坑不少。写代码时多想想边界条件,多跑跑仿真,能省去后面很多麻烦。
公众号:蓝海资料掘金营,微信deep3321