1. 从芯片到代码:理解74LVC161的核心功能
如果你刚开始接触数字电路设计,看到“74LVC161”这个型号可能会有点懵。别担心,这其实就是一颗非常经典、在FPGA和数字系统里出场率极高的4位二进制同步计数器芯片。你可以把它想象成一个特别听话的“数字累加器”,每来一个时钟脉冲,它就自动加1,从0(二进制0000)一直数到15(二进制1111),然后自动回到0重新开始,同时会给出一个进位信号。
这颗芯片之所以强大,是因为它提供了几个关键的控制“开关”,让我们能灵活地指挥它工作,而不是傻傻地只会从0数到15。这些开关就是我们要重点实现的异步清零和同步置数功能。我刚开始学的时候,也常常搞混“同步”和“异步”的区别,后来发现用生活中的例子来理解就简单多了。
想象一下,你有一个带闹钟和手动设置按钮的电子钟。
- 异步清零就像是你直接拔掉电池再装上,时间瞬间归零(00:00)。这个操作不需要等到下一个整点或半点,随时可以进行,立刻生效。在74LVC161里,对应的就是
CR(Clear,清零)引脚,低电平有效。只要CR变成0,不管当时时钟CP在干嘛,输出Q立马变成0000。 - 同步置数则像是你按下了“时间设置”按钮,然后调好小时和分钟,但时间并不会立刻跳变。你必须等到下一个“确认”信号(比如按下“确认”键,或者等下一个整分钟到来)时,你设置的时间才会真正生效。在74LVC161里,对应的就是
PE(Parallel Enable,并行使能)引脚,也是低电平有效。当PE为0时,你通过D[3:0]引脚准备好的数据(比如4‘b1100)被“预装”进去,但必须等到下一个时钟CP的上升沿到来时,这个数据才会被“确认”并出现在输出Q上。
至于CEP和CET这两个使能端,你可以把它们理解成计数器的“总开关”和“连锁开关”。只有它们俩都为1时,计数器才被允许在每个时钟上升沿进行加1计数。TC是进位输出,当计数器数到15(4‘b1111)且CET为1时,TC会输出一个高电平脉冲,告诉你“我数满一轮了”,这个信号在级联多个计数器构成更大位宽计数器时特别有用。
理解了这些,我们用Verilog来实现它就有了清晰的思路:我们需要用硬件描述语言,在FPGA内部“造”出一个行为逻辑和74LVC161完全一致的模块。这不仅能让你深入理解计数器的工作原理,更是学习时序逻辑电路设计的绝佳入门项目。
2. 动手编写:Verilog行为级建模实战
理论懂了,接下来就是动手写代码。用Verilog描述74LVC161,核心就是用一个always块来捕捉时钟边沿和异步清零信号的变化,然后根据控制信号的优先级来决定输出。
2.1 模块定义与端口声明
首先,我们得把芯片的“引脚”用Verilog定义出来。这步就像给我们要造的“房子”画好设计图的门窗。
module counter_74LVC161 (
input wire CP, // 时钟信号,上升沿有效
input wire CR_n, // 异步清零,低电平有效(n表示低有效)
input wire PE_n, // 同步置数使能,低电平有效
input wire CEP, // 计数使能端P
input wire CET, // 计数使能端T
input wire [3:0] D, // 并行数据输入,用于置数
output reg [3:0] Q, // 4位计数器输出
output wire TC // 进位输出
);
这里我特意把清零和置数信号命名为CR_n和PE_n,加个“_n”后缀是业界常见的做法,一眼就能看出是低电平有效,写条件判断时用~CR_n也更直观。输出Q因为需要在always块里被赋值,所以定义为reg类型;而TC是纯粹由当前Q和CET组合逻辑决定的,用assign连续赋值就行,所以是wire型。
2.2 核心逻辑:always块与优先级设计
芯片功能的实现,全在这个always块里。这里的关键是理解并实现控制信号的优先级。根据74LVC161的数据手册,优先级从高到低是:异步清零(CR_n) > 同步置数(PE_n) > 计数使能(CEP&CET) > 保持。
always @(posedge CP or negedge CR_n) begin
if (~CR_n) begin
// 1. 异步清零:优先级最高,立即执行,不等待时钟
Q <= 4'b0000;
end
else if (~PE_n) begin
// 2. 同步置数:在时钟上升沿,将D的值载入Q
Q <= D;
end
else if (CEP && CET) begin
// 3. 计数:两个使能都有效时,每个时钟沿加1
Q <= Q + 1'b1;
end
else begin
// 4. 保持:使能不满足时,输出保持不变
Q <= Q;
end
end
这段代码是行为级建模的典型写法。敏感列表@(posedge CP or negedge CR_n)意味着,这个块会在时钟CP的上升沿或者清零信号CR_n的下降沿被触发。这正是“异步”清零的精髓——清零动作独立于时钟。
我刚开始写的时候,曾犯过一个错误:把CR_n也放到if-else的链式判断里,并且只在posedge CP时触发。结果仿真时发现清零动作总是慢一拍,因为它变成了“同步清零”。记住,异步信号一定要放在敏感列表里,并且用独立的、最高优先级的if语句处理。
2.3 组合逻辑:进位信号的生成
进位信号TC的逻辑比较简单:当计数器计到最大值15(4‘b1111),并且计数使能CET有效时,TC输出高电平。注意,这里通常不要求CEP也为1,这是芯片的规定。
assign TC = (Q == 4'b1111) && CET;
这里用了一个连续赋值语句assign。它描述的是一个纯组合逻辑电路,TC的值会随着Q和CET的变化而实时变化,不需要等待时钟边沿。这种写法简洁明了。有些参考代码可能会把TC也放到always块里用reg类型,然后用非阻塞赋值<=,这虽然仿真结果可能一样,但严格来说,TC不是时序逻辑输出,用assign更符合其组合逻辑的本质,综合出来的电路也更优化。
3. 验证与调试:编写测试平台并分析波形
代码写完了,但它到底对不对呢?这时候就需要一个测试平台(Testbench)来扮演“信号发生器”和“监视器”的角色,给我们的设计模块输入各种激励,看看它的输出是否符合预期。
3.1 搭建测试平台框架
测试平台本身也是一个Verilog模块,但它不需要综合成实际电路,只用于仿真。我们在这个模块里实例化刚才设计好的计数器,并产生测试信号。
`timescale 1ns / 1ns // 定义仿真时间单位/精度
module tb_counter_74LVC161();
// 1. 声明连接到被测模块的信号
reg CP, CR_n, PE_n, CEP, CET;
reg [3:0] D;
wire [3:0] Q;
wire TC;
// 2. 实例化被测设计
counter_74LVC161 uut (
.CP(CP),
.CR_n(CR_n),
.PE_n(PE_n),
.CEP(CEP),
.CET(CET),
.D(D),
.Q(Q),
.TC(TC)
);
// 3. 生成时钟信号
initial begin
CP = 1'b0;
forever #10 CP = ~CP; // 周期20ns,频率50MHz
end
// 4. 生成测试激励
initial begin
// 初始化所有输入
CR_n = 1'b1;
PE_n = 1'b1;
CEP = 1'b0;
CET = 1'b0;
D = 4'b0000;
// 测试序列开始
// 测试1:异步清零
#15 CR_n = 1'b0; // 在15ns时触发清零(注意不在时钟边沿)
#20 CR_n = 1'b1; // 35ns时释放清零
// 测试2:同步置数
#20 D = 4'b1100; // 55ns时准备数据
PE_n = 1'b0; // 同时拉低置数使能
// 等待下一个时钟上升沿(在60ns),观察Q是否变为1100
#40 PE_n = 1'b1; // 95ns时关闭置数
// 测试3:正常计数
#10 CEP = 1'b1; CET = 1'b1; // 105ns开启计数使能
// 让计数器从1100开始,数几个周期
#160; // 计数8个周期左右
// 测试4:使能控制(保持)
#20 CEP = 1'b0; // 285ns关闭CEP,计数器应保持
#40 CEP = 1'b1; // 325ns重新开启
// 测试5:观察进位
// 让计数器继续计数,直到从1111回到0000,观察TC脉冲
#200;
$finish; // 结束仿真
end
// 5. 监控输出(可选,用波形看更直观)
initial begin
$monitor("Time=%t, CR_n=%b, PE_n=%b, CEP=%b, CET=%b, D=%b, Q=%b, TC=%b",
$time, CR_n, PE_n, CEP, CET, D, Q, TC);
end
endmodule
这个测试平台覆盖了主要功能点:在非时钟边沿进行异步清零、在时钟边沿进行同步置数、使能控制下的计数与保持、以及进位信号的产生。$monitor语句会在控制台打印信号变化,但对于复杂的时序分析,看波形图更直观。
3.2 解读仿真波形与关键现象
用Modelsim、Vivado Simulator等工具运行仿真后,你会得到类似下图的波形。我们重点看几个关键时刻:
注意:在波形图上,重点关注信号变化与时钟边沿(CP的上升沿)的对齐关系。
- 异步清零时刻:在
CR_n变低后(图中15ns处),Q的输出立刻变为0000,没有等待下一个时钟上升沿。这就是“异步”的直观体现。 - 同步置数时刻:在
PE_n变低后(图中55ns处),D上的数据1100并没有立刻出现在Q上。直到下一个时钟上升沿(60ns处),Q才跳变为1100。这就是“同步”的含义——动作发生在时钟边沿。 - 计数过程:当
CEP和CET都为高后,在每个时钟上升沿(80ns, 100ns, ...),Q的值依次加1(1100->1101->1110-> ...)。 - 保持状态:当
CEP变低(285ns处)后,尽管时钟仍在跳变,Q的值保持不变。 - 进位信号:当
Q计数到1111且CET为高时,TC输出一个时钟周期的高电平脉冲。
如果仿真结果与上述描述不符,就需要回头检查代码。常见的坑有:敏感列表没写对、优先级顺序弄反、CR_n和PE_n的有效电平搞错(我们设计的是低有效,但有些资料可能是高有效)。多调试几次,看着波形一点点调通,这个过程对理解时序逻辑至关重要。
4. 进阶应用:从模块到实际系统
一个能正确计数的模块只是开始。在实际项目中,比如电子钟、频率计、信号发生器里,计数器都是核心部件。这时,我们设计的这个74LVC161模块就能作为基础积木被调用。
4.1 实现任意进制计数器
74LVC161是4位二进制(16进制)计数器。但我们需要十进制、二十四进制、六十进制怎么办?这就需要用到反馈清零法或反馈置数法。本质上,就是利用我们刚才实现的异步清零或同步置数功能,在计数器达到某个特定值时,强行让它回到起点,从而形成更短的计数循环。
例如,用一个74LVC161模块实现十进制(0-9)计数器:
- 反馈清零法:让计数器从0开始计数。当它计到10(
1010)的瞬间,利用组合逻辑检测到这个状态(Q==4‘b1010),并立即产生一个低电平脉冲给CR_n引脚,让计数器异步清零回0。因为10这个状态只是瞬间出现,实际稳定的输出是0-9。这种方法电路简单,但存在短暂的毛刺状态。 - 反馈置数法:让计数器从0开始计数。当它计到9(
1001)时,在下一个时钟上升沿到来时,利用组合逻辑产生一个低电平给PE_n引脚,并将数据输入D设置为0。这样,在计完9之后,下一个时钟沿到来时不是变成10,而是被同步置数回0。这种方法没有毛刺状态,更稳定。
在Verilog顶层模块里,你可以实例化一个counter_74LVC161,然后用一些额外的门电路(在Verilog里就是assign语句或always块)来生成反馈信号。这比直接写一个十进制计数器的行为描述,更能锻炼你模块化设计和系统连接的能力。
4.2 多片级联与高位宽计数
一颗74LVC161只能数到15。要数更大的数,比如0-255,就需要把两片级联起来。级联的关键在于进位信号TC的传递。
你可以把低位片的TC输出,连接到高位片的CEP(或CET)输入端。这样,只有当低位片从15翻回0产生进位脉冲时,高位片才会计数一次。这就构成了一个8位(256进制)的计数器。在Verilog中,你可以实例化两个counter_74LVC161模块,然后用线网(wire)将它们连接起来。这种思路可以扩展到任意多片,构建出任意位宽的计数器。
4.3 在FPGA项目中的集成
在真实的FPGA项目中,这个计数器模块通常会作为某个更大系统(比如UART波特率发生器、PWM波形生成器、分频器)的子模块。你需要仔细考虑时钟域、复位策略以及使能信号的控制。
例如,做一个1秒闪烁一次的LED。假设系统时钟是50MHz,你需要一个计数器先分频出1Hz的信号。你可以先写一个像74LVC161这样的通用计数器模块,然后在顶层模块中实例化它,并将计数值设定为50_000_000 - 1。当计数值达到这个数时,让TC信号翻转LED的状态。这就是一个典型的分频器应用。
我个人的经验是,把这类基础功能模块写得稳健、接口清晰,并单独保存在一个modules文件夹里。以后做新项目时直接调用,能节省大量重复开发时间。这个74LVC161的Verilog实现,就是这样一个值得放入你个人代码库的基础模块。

82

被折叠的 条评论
为什么被折叠?



