本节摘要:计数器是触发器阵列的最直接应用,也是分频、定时、状态节拍的通用源。本节写一个泛型化的通用计数器(可配位宽与模值、带使能与进位输出),验证它的分频行为,并整理三种常见变体的取舍。
把上一节的触发器输出反馈回来加一再存回去,就是计数器:cnt <= cnt + 1 写在时钟进程里,每个上升沿自增。真正工程级的计数器还要回答四个问题:数到多少回零(模值)?什么时候允许数(使能)?怎么告诉别人"数满了"(进位)?位宽能不能参数化(泛型)?四个问题都答进实体:
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity counter_n is generic ( WIDTH : positive := 8; -- 计数器位宽 MAX : positive := 9 -- 模值减一:数到这里归零 ); port ( clk : in std_logic; rst : in std_logic; -- 同步复位 en : in std_logic; -- 计数使能 q : out std_logic_vector(WIDTH-1 downto 0); carry : out std_logic -- 计满脉冲,持续一个时钟周期 ); end entity counter_n; architecture rtl of counter_n is signal cnt : unsigned(WIDTH-1 downto 0) := (others => '0'); begin process (clk) begin if rising_edge(clk) then if rst = '1' then cnt <= (others => '0'); elsif en = '1' then if cnt = MAX then cnt <= (others => '0'); -- 到顶归零 else cnt <= cnt + 1; end if; end if; end if; end process; q <= std_logic_vector(cnt); carry <= '1' when cnt = MAX and en = '1' and rst = '0' else '0'; end architecture rtl;
carry 的写法有讲究:它是组合输出,条件里带上 en 与 rst,保证"只在真正发生跳回零的那一拍"拉高。计数比较 cnt = MAX 用的无符号语义,MAX 由泛型传入,正整数比较自动转换,无需手工转换。
模十计数器的 carry 每十个时钟冒一个脉冲——把 carry 接到 LED,LED 的闪烁频率就是时钟的十分之一,这就是整数分频。测试台重点验证三件事:数列正确(从零到九循环)、进位时刻(计到九且使能时)、使能冻结(en 拉低期间计数值保持)。激励安排成三段:先正常跑三十拍以上看两轮循环,中途拉低使能几拍看冻结,再拉高确认从冻结值继续。看波形时先数 carry 脉冲间隔——必须是整齐的十个时钟周期,多一拍少一拍都说明模值写错。
实际项目里计数器常以三种变体出现,写法差异与适用场景列一张表:
| 变体 | 核心写法 | 适用场景 | 注意点 |
|---|---|---|---|
| 自由跑计数器 | 无使能,一直自增 | 时钟分频、时间戳 | 复位后从零开始数 |
| 使能计数器 | en 条件门控 | 事件计数(收包数、按键数) | en 毛刺会多数,源头要同步 |
| 可预置计数器 | load 信号加载初值 | 可编程定时器、波特率发生 | load 优先级要高于 en |
可预置版本只需在进程里加一个分支:
if rising_edge(clk) then if rst = '1' then cnt <= (others => '0'); elsif load = '1' then cnt <= unsigned(din); -- 预置优先 elsif en = '1' then -- 自增逻辑同上 end if; end if;
优先级顺序 rst 最高、load 次之、en 最后——if-elsif 链的顺序就是优先级,第 3 章的规则在这里兑现。
计算器原型需要一个"秒闪烁"节拍:板载时钟五十兆赫兹,一秒要数五千万——一位计数器装不下。做法是两级级联:低位计数器计满产生进位,进位当作高位计数器的使能:
-- 低八位:0..249 循环,进位脉冲周期 250 个时钟 low_cnt : entity work.counter_n generic map (WIDTH => 8, MAX => 249) port map (clk => clk, rst => rst, en => '1', q => low_q, carry => low_tick); -- 高位:每次 low_tick 到来才加一,计到 199999 次约为一秒量级 high_cnt : entity work.counter_n generic map (WIDTH => 18, MAX => 199999) port map (clk => clk, rst => rst, en => low_tick, q => open, carry => sec_tick);
两级合计 250 乘 20 万等于五千万,sec_tick 每秒一拍。port map (q => open) 是 VHDL 的"此端口悬空"写法,不需要的输出不必接信号。级联思想比"造一个二十六位大计数器"更模块化——每个计数器独立可测,模值调整互不牵连。
⚠️ 常见坑:用计数器高位直接当慢时钟去驱动另一个模块的 clk 端口(比如 carry 接到下一级的 clk)。这会制造出真正的第二时钟域,时序分析一片混乱。正确姿势是所有模块共用一个时钟,慢节拍走使能端口——时钟走树,节拍走使能。
💡 关键直觉:计数器是"数字节拍器"。分频输出别理解成"造了个新时钟",而是一次"每 N 拍点头一次"的使能脉冲——这个理解能避开时钟域的大坑。