Verilog实现74LVC161计数器的异步清零与同步置数功能

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上。

至于CEPCET这两个使能端,你可以把它们理解成计数器的“总开关”和“连锁开关”。只有它们俩都为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_nPE_n,加个“_n”后缀是业界常见的做法,一眼就能看出是低电平有效,写条件判断时用~CR_n也更直观。输出Q因为需要在always块里被赋值,所以定义为reg类型;而TC是纯粹由当前QCET组合逻辑决定的,用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的值会随着QCET的变化而实时变化,不需要等待时钟边沿。这种写法简洁明了。有些参考代码可能会把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的上升沿)的对齐关系。

  1. 异步清零时刻:在CR_n变低后(图中15ns处),Q的输出立刻变为0000,没有等待下一个时钟上升沿。这就是“异步”的直观体现。
  2. 同步置数时刻:在PE_n变低后(图中55ns处),D上的数据1100并没有立刻出现在Q上。直到下一个时钟上升沿(60ns处),Q才跳变为1100。这就是“同步”的含义——动作发生在时钟边沿。
  3. 计数过程:当CEPCET都为高后,在每个时钟上升沿(80ns, 100ns, ...),Q的值依次加1(1100 -> 1101 -> 1110 -> ...)。
  4. 保持状态:当CEP变低(285ns处)后,尽管时钟仍在跳变,Q的值保持不变。
  5. 进位信号:当Q计数到1111CET为高时,TC输出一个时钟周期的高电平脉冲。

如果仿真结果与上述描述不符,就需要回头检查代码。常见的坑有:敏感列表没写对、优先级顺序弄反、CR_nPE_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实现,就是这样一个值得放入你个人代码库的基础模块。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值