这一篇在干嘛?

这是计数器系列的第一课:做一个能从 0 数到 9、数满归零的十进制计数器,并把结果送到数码管上显示。除了自动计数,它还有三个「人工干预」的旋钮——复位、加载、使能,全部由开发板上的按键控制。搞懂这一篇,后面八位计数器的级联就水到渠成了。

实验目标与原理

实验任务

可加载数计数器输出四位二进制连接七段译码器模块,累加到 9 循环计数。

一句话拆解:

  • 「4 位二进制」:用 4 个比特(cnt[3:0])保存计数值,理论上一共能表示 0~15 共 16 个状态;
  • 「累加到 9 循环」:虽然能表示到 15,但我们只用到 0~9,数到 9 之后下一个时钟回到 0——这就是「十进制」的来历;
  • 「连接七段译码器」:计数器输出的是二进制数(比如 4'b0101 代表 5),人看不懂,所以要经过译码器翻译成数码管的 7 段亮灭信号。

工作原理

本实验利用 4 位二进制计数方法,通过时钟 CLK 上升沿触发一次,计数一次,从 0000 一直计到 1111,再接「实验一」七段译码模块,通过累加到 9 进行循环计数方法。利用一个四位二进制计数器,随着时钟上升沿到来,输出一次信号进行累加输出给七段译码器,译码器模块根据接受到的四位二进制信号进行对应译码点亮数码管七段。


图 1.1

理解这个结构的关键是分清两条「流水线」:

  1. 计数流水线:时钟(经过分频后的慢时钟 clk)每个上升沿到来时,cnt 加一;数到 9 就归零,同时 cout(进位信号)拉高一拍——以后级联多位计数器,靠的就是这个进位。
  2. 译码流水线cnt 的值随时送进一个 case 查表电路,把 09(甚至 AF)翻译成 7 段码 LED7S,拼上进位信号后从 DIG 端口输出点亮数码管。

为什么不复用 10~15?

注意代码里的 case 其实把 10101111(AF)的译码也写全了,只是计数逻辑在 cnt < 9 时加一、否则归零,所以 A~F 的译码分支永远轮不到。这叫「留着不用」,好处是译码模块可以直接抄去别的实验复用。

还有一个容易被忽略的细节:板子上 50MHz 时钟太快了,人眼根本跟不上,所以代码里用一个 25 位的分频寄存器 div,取它的最高位 div[LEN-1] 当作计数时钟——最高位每 2^25 个时钟周期翻转一次,等效于一个约 1.5Hz 的慢时钟,数码管上的数字就能被人眼看清地跳动。

Verilog 实现

顶层模块 cnt10_disp 一共做四件事:分频、计数、译码、选通数码管位选。原文程序分三段给出(连续的一个模块,中间因排版断开):

顶层文件:

module cnt10_disp (
    input CLOCK,
    input [2:0] KEY,
    input [3:0] SW,
    output [7:0] SEL,
    output [7:0] DIG
);
localparam LEN = 25;
wire en,load_n,reset_n;
wire [3:0] d;
wire cout;
wire clk;
reg [6:0] LED7S;
reg [3:0] cnt;
reg [LEN-1:0] div;
assign {en,load_n,reset_n} = KEY;
assign DIG = {~cout,LED7S};
assign d = SW;
assign clk = div[LEN-1];
assign SEL = 8'b1111_1110;
always @(posedge clk,negedge reset_n)
    if(!reset_n)
    cnt <= 0;
    else if(en)
    if(!load_n)
    cnt <= d;
    else if(cnt < 9)
    cnt <= cnt + 1'b1;
    else
    cnt <= 0;
assign cout = (cnt==9);
always @*
case(cnt)
4'b0000 : LED7S <= 7'b1000_000;//7'b0111_111;
4'b0001 : LED7S <= 7'b1111_001;//7'b0000_110;
4'b0010 : LED7S <= 7'b0100_100;//7'b1011_011;
4'b0011 : LED7S <= 7'b0110_000;//7'b1001_111;
4'b0100 : LED7S <= 7'b0011_001;//7'b1100_110;
4'b0101 : LED7S <= 7'b0010_010;//7'b1101_101;
4'b0110 : LED7S <= 7'b0000_010;//7'b1111_101;
4'b0111 : LED7S <= 7'b1111_000;//7'b0000_111;
4'b1000 : LED7S <= 7'b0000_000;//7'b1111_111;
4'b1001 : LED7S <= 7'b0010_000;//7'b1101_111;
4'b1010 : LED7S <= 7'b0001_000;//7'b1110_111;
4'b1011 : LED7S <= 7'b0000_011;//7'b1111_100;
4'b1100 : LED7S <= 7'b1000_110;//7'b0111_001;
4'b1101 : LED7S <= 7'b0100_001;//7'b1011_110;
4'b1110 : LED7S <= 7'b0000_110;//7'b1111_001;
4'b1111 : LED7S <= 7'b0001_110;//7'b1110_001;
default : LED7S <= 7'b1000_000;//7'b0111_111;
endcase
always @(posedge CLOCK,negedge reset_n)
if(!reset_n)
    div <= 0;
else
    div <= div + 1'b1;
endmodule

几个值得逐行琢磨的点:

  • assign {en,load_n,reset_n} = KEY; —— 一个拼接赋值把三个按键一次性接到使能、加载、复位上。注意按键按下是低电平,所以 reset_n 为 0 时复位(异步低有效,写在 always 敏感列表 negedge reset_n 里)。
  • 加载逻辑:en 有效(松开键 3)时,若 load_n 有效(按下键 2),就把开关输入 d(即 SW)直接装进 cnt——这就是「可加载」三个字的含义。
  • assign DIG = {~cout,LED7S}; —— DIG[7] 接的是取反后的进位信号,数码管的小数点位会在计到 9 的一瞬间亮一下,等于给你一个「满 10 归零」的可视化提示。
  • assign SEL = 8'b1111_1110; —— 位选固定点亮最右边一位数码管,其余七位全部关闭。

实验仿真

根据描述语言进行仿真测试,分析波形图 1.2。


图 1.2 仿真结果

看仿真波形时重点盯三件事:复位释放后 cnt 是否从 0 开始;load_n 拉低的一拍 cnt 是否立刻变成开关值;计到 9 时 cout 是否出现一个高脉冲,下一拍 cnt 是否归零。这三点都对,上板基本一次成功。

引脚约束

复位、加载、使能分别接键 1、2、3,待加载数据分别接开关 SW1-4,拨上位 1,DIG/LED 开关拨下。

set_pin_assignment { CLOCK } { LOCATION = R7; IOSTANDARD = LVCMOS33;
PULLTYPE = PULLUP; }
set_pin_assignment { DIG[0] } { LOCATION = C15; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { DIG[1] } { LOCATION = C16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { DIG[2] } { LOCATION = B6; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { DIG[3] } { LOCATION = A5; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { DIG[4] } { LOCATION = B5; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { DIG[5] } { LOCATION = A8; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { DIG[6] } { LOCATION = A7; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { DIG[7] } { LOCATION = A6; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { KEY[0] } { LOCATION = A2; IOSTANDARD = LVCMOS33;
PULLTYPE = PULLUP; }
set_pin_assignment { KEY[1] } { LOCATION = B2; IOSTANDARD = LVCMOS33;
PULLTYPE = PULLUP; }
set_pin_assignment { KEY[2] } { LOCATION = B1; IOSTANDARD = LVCMOS33;
PULLTYPE = PULLUP; }
set_pin_assignment { SEL[0] } { LOCATION = D11; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { SEL[1] } { LOCATION = E11; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { SEL[2] } { LOCATION = F10; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { SEL[3] } { LOCATION = C13; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { SEL[4] } { LOCATION = E10; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { SEL[5] } { LOCATION = E12; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { SEL[6] } { LOCATION = E16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { SEL[7] } { LOCATION = F16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { SW[0] } { LOCATION = C8; IOSTANDARD = LVCMOS33; PULLTYPE
= PULLUP; }
set_pin_assignment { SW[1] } { LOCATION = C7; IOSTANDARD = LVCMOS33; PULLTYPE
= PULLUP; }
set_pin_assignment { SW[2] } { LOCATION = C6; IOSTANDARD = LVCMOS33; PULLTYPE
= PULLUP; }
set_pin_assignment { SW[3] } { LOCATION = C5; IOSTANDARD = LVCMOS33; PULLTYPE
= PULLUP; }

对照着读一遍就能发现规律:KEY 类引脚都配了 PULLTYPE = PULLUP(平时靠上拉保持高电平,按下才变低);数码管引脚配 DRIVESTRENGTH = 8 保证驱动能力。以后自己写约束文件,照抄这套模板即可。

按键是低有效

板上按键按下去接触为低电平。所以「键 3 使能,按下去停止计数」在代码里对应的是 en 变 0 跳过计数分支——如果你的现象正好相反(按下才计数),先检查是不是把电平极性想反了。

上板流程与现象

下载测试的操作步骤(原文描述):

  • 开关 SW1-SW3 输入待加载数,键 1 复位,按下去复位,松开计数;
  • 键 2 加载输入开关输入的数据;
  • 键 3 使能,按下去停止计数,松开使能。

计数输出——键 1 复位,键 2 加载,键 3 使能待加载数据输入。

上板时可以按这个顺序玩一遍,每一步验证一个功能:

  1. 上电后数码管应从 0 开始自动跳到 9 再归零(使能默认有效);
  2. 按住键 1 松开,数字回到 0 重新开始;
  3. 把 SW1-SW4 拨成一个数(比如 0101 即 5),按下键 2,数码管立刻变成 5;
  4. 按住键 3,计数停住;松开继续。

现象与总结

这个实验麻雀虽小五脏俱全:分频、同步/异步复位、同步加载、使能、模 10 计数、进位输出、组合逻辑译码,全是时序设计的标准积木。下一篇的八位十进制计数器,就是把这里的「进位」思路放大成多级级联,再配合数码管动态扫描来显示——基础打牢了,进阶只是体力活。

自测