数字逻辑实验期末:SystemVerilog / Vivado FPGA 备考手册

这份手册按“从零能写出能综合、能上板的代码”的目标组织。它不把重点放在仿真,也不过度展开亚稳态理论,而是把考试中最容易遇到的模块——译码器、全加器、计数器、分频器、按钮长短按、串行输入、状态机、七段数码管和 XDC 管脚约束——拆成可直接改写的模板。

SystemVerilog 可综合子集 Vivado 上板 组合逻辑 时序逻辑 FSM 七段数码管 Morse 模拟题

0. 考试时应该怎么用这份手册

先搭骨架,再填功能

上机题通常不是考“背语法”,而是给一个 FPGA 小项目,让你把输入、状态、计数和显示连起来。拿到题目后先写顶层端口、XDC 管脚、显示译码器和复位逻辑,再写核心功能。

所有项目都可拆成三类电路

组合逻辑:译码、比较、加法、选择。寄存器/计数器:保存状态、保存输入、计时。状态机:按步骤推进流程。绝大多数考试项目就是这三类的组合。

推荐顺序:先读第 1–7 章建立语法和设计模型,再看第 8–10 章的典型项目,最后把第 12–13 章作为考试速查。

考试原则:宁可写朴素、清晰、能综合的代码,也不要追求“高级”。不用仿真时,板上调试更依赖模块拆分、LED 暴露中间信号、复位逻辑可靠、端口和 XDC 名字一致。

1. 硬件描述的思维方式:你写的不是“按行执行的程序”

SystemVerilog 看起来像 C,但本质完全不同。你写的代码会被 Vivado 综合成电路:线、门、触发器、多路选择器、加法器、比较器等。理解这一点,很多语法规则就自然了。

输入信号开关 / 按钮 / 时钟 组合逻辑assign / always_comb立即由输入决定输出 寄存器always_ff @(posedge clk)只在时钟边沿更新 输出LED / SEG

1.1 三句话理解硬件代码

  1. assign 和 always_comb 描述组合电路。输入一变,输出就按逻辑关系变化。它们不会“记住”过去。
  2. always_ff @(posedge clk) 描述触发器/寄存器。只有时钟上升沿到来时,寄存器才把右侧的值保存进去。
  3. 状态机就是“寄存器保存当前状态 + 组合逻辑算下一状态 + 组合逻辑/寄存器产生输出”。

1.2 软件变量 vs 硬件信号

软件程序硬件描述考试中怎么想
变量存放在内存里,语句按顺序执行信号对应电路线;多个逻辑块同时存在、同时工作不要问“下一行什么时候执行”,要问“这根线接到哪里”
if 是运行时分支if 在组合逻辑中常综合成选择器,在时序逻辑中决定寄存器下一拍写什么每个分支都要给输出赋值,否则可能生成锁存器
循环可以跑很多轮可综合 for 常被展开成多份硬件循环次数必须是综合时可确定的小常量

2. SystemVerilog 基础语法:考试够用版

2.1 模块 module:一个“黑盒电路”

每个 FPGA 设计都由模块组成。顶层模块的端口会被 XDC 绑定到 FPGA 管脚;子模块通过例化连到顶层或其他模块中。

module AndGate (
    input  wire a,
    input  wire b,
    output wire y
);
    assign y = a & b;
endmodule

考试里最推荐的命名方式:端口名简单清楚,和 XDC 保持完全一致;内部信号可以用 xxx_r 表示寄存器,xxx_w 表示组合线/下一值。

2.2 wire、reg、logic 到底是什么

写法典型用途赋值位置建议
wire连续连线、子模块输出接线assign 或模块输出驱动课程模板常用;用于纯连线很安全
reg在 always_comb / always_ff 中被赋值的信号always 块内部课程文档常用;名字叫 reg 不代表一定是寄存器,取决于 always 类型
logicSystemVerilog 更现代的单驱动信号类型assign 或 always 块,但不要多处驱动考试可以用;若担心风格不一致,就用课程的 wire/reg
最简单不出错规则:用 assign 赋值的输出声明成 wire;在 always_comb 或 always_ff 里赋值的输出声明成 reg 或 logic;同一个信号只在一个地方被赋值。

2.3 位宽、下标、切片

logic        a;        // 1 bit
logic [3:0]  sw;       // 4 bit,常写成 sw[3] sw[2] sw[1] sw[0]
logic [6:0]  seg;      // 七段数码管 a~g,可约定 seg[6]=a, ..., seg[0]=g
logic [15:0] token;    // 4 个十六进制数码,可用 4 个 nibble 保存

assign high_nibble = token[15:12];
assign low_nibble  = token[3:0];
assign token2      = {sw, token[11:0]};   // 拼接:把新 sw 放到最高 4 位
assign doubled     = {2{sw}};             // 重复:sw sw,共 8 bit

2.4 字面量:4'hA 是什么意思

写法含义例子
宽度'进制数值指定一个有位宽的常量4'b1010、8'hFF、16'd1000
'b二进制3'b010
'd十进制20'd999_999
'h十六进制16'h1234
x无关/未知;综合时可视为 don't care,但谨慎使用4'b10xx

考试里强烈建议给所有常量写位宽,尤其是计数器比较值、七段码、状态编码。这样 Vivado 不容易出现位宽截断警告。

2.5 常用运算符

类别运算符说明
按位逻辑& | ^ ~每一位分别做与/或/异或/取反。全加器常用 ^。
逻辑判断&& || !把整个表达式当真/假,常用于 if 条件。
比较== != < >=状态判断、计数器到达阈值。
移位<< >>串行输入、Morse 编码、流水移位。
拼接{a,b}把多段信号连成一段,常用于保存 4 个十六进制数字。
三目cond ? x : y简单二选一;综合为 mux。

3. 组合逻辑写法:译码、选择、比较、查表

3.1 assign:适合一行表达式

module FullAdder1 (
    input  wire A,
    input  wire B,
    input  wire Cin,
    output wire Sum,
    output wire Cout
);
    assign Sum  = A ^ B ^ Cin;
    assign Cout = (A & B) | (Cin & (A ^ B));
endmodule

只要能写成一个表达式,就可以用 assign。这类逻辑没有记忆,不需要时钟。

3.2 always_comb:适合 if / case / 查表

module Mux4 (
    input  wire [1:0] sel,
    input  wire [3:0] a, b, c, d,
    output reg  [3:0] y
);
    always_comb begin
        case (sel)
            2'd0:    y = a;
            2'd1:    y = b;
            2'd2:    y = c;
            2'd3:    y = d;
            default: y = 4'h0;  // 理论上到不了,也写上,防止不完整
        endcase
    end
endmodule

3.3 防止锁存器:组合逻辑必须“每条路都有赋值”

错误:某些分支没给 y 赋值

always_comb begin
    if (en) begin
        y = a;
    end
    // en=0 时 y 没有新值,综合器可能推断锁存器
end

正确:先给默认值,特殊情况再覆盖

always_comb begin
    y = 4'h0;       // 默认值
    if (en) begin
        y = a;
    end
end

3.4 case 查表模板

七段数码管、Morse 表、状态输出都适合用 case。写查表时要先定清楚“输入编码”和“输出位序”。

function automatic logic [6:0] hex_to_seg(input logic [3:0] x);
    // 位序约定:seg[6:0] = {a,b,c,d,e,f,g};1 表示点亮。
    case (x)
        4'h0: hex_to_seg = 7'b1111110;
        4'h1: hex_to_seg = 7'b0110000;
        4'h2: hex_to_seg = 7'b1101101;
        4'h3: hex_to_seg = 7'b1111001;
        4'h4: hex_to_seg = 7'b0110011;
        4'h5: hex_to_seg = 7'b1011011;
        4'h6: hex_to_seg = 7'b1011111;
        4'h7: hex_to_seg = 7'b1110000;
        4'h8: hex_to_seg = 7'b1111111;
        4'h9: hex_to_seg = 7'b1111011;
        4'hA: hex_to_seg = 7'b1110111;
        4'hB: hex_to_seg = 7'b0011111; // 显示小写 b
        4'hC: hex_to_seg = 7'b1001110;
        4'hD: hex_to_seg = 7'b0111101; // 显示小写 d
        4'hE: hex_to_seg = 7'b1001111;
        4'hF: hex_to_seg = 7'b1000111;
        default: hex_to_seg = 7'b0000000;
    endcase
endfunction
注意 active-high / active-low:课程文档中“不带译码数码管”示例采用输入为高时点亮相应段。若你的板子或题目写的是低电平点亮,只需要输出取反:assign seg = ~hex_to_seg(x);。

4. 模块化与例化:把大题拆成小电路

考试项目一旦超过一个功能,就不要把所有逻辑堆到一个 always 块里。先写可复用的小模块,再在顶层连起来。这样即使某部分错了,也容易用 LED 暴露中间信号。

4.1 命名端口例化

module Adder4 (
    input  wire [3:0] A,
    input  wire [3:0] B,
    input  wire       Cin,
    output wire [3:0] Sum,
    output wire       Cout
);
    wire [2:0] c;

    FullAdder1 u0 (.A(A[0]), .B(B[0]), .Cin(Cin),  .Sum(Sum[0]), .Cout(c[0]));
    FullAdder1 u1 (.A(A[1]), .B(B[1]), .Cin(c[0]), .Sum(Sum[1]), .Cout(c[1]));
    FullAdder1 u2 (.A(A[2]), .B(B[2]), .Cin(c[1]), .Sum(Sum[2]), .Cout(c[2]));
    FullAdder1 u3 (.A(A[3]), .B(B[3]), .Cin(c[2]), .Sum(Sum[3]), .Cout(Cout));
endmodule

强烈建议始终使用命名端口例化,也就是 .端口名(信号名)。不要依赖位置顺序,因为一改模块端口顺序就会连错。

4.2 顶层模块的职责

  1. 端口名与 XDC 完全一致。
  2. 只做“连线”和少量顶层 glue logic;核心功能下沉到子模块。
  3. 把显示译码器作为独立模块,别在每个功能模块里重复写。
  4. 必要时输出调试 LED,如当前状态、计数器最高位、错误标志。
按钮/开关 时钟 输入处理边沿 / 长短按 / 串行码 计时器分频 / 计数 / tick FSM保存状态 / 决定流程产生结果码 显示译码hex → segerr → LED

5. 时序逻辑:寄存器、复位、非阻塞赋值

5.1 寄存器模板:always_ff

module RegEn #(
    parameter int W = 8
)(
    input  wire         clk,
    input  wire         rst,   // 高有效同步复位
    input  wire         en,
    input  wire [W-1:0] d,
    output reg  [W-1:0] q
);
    always_ff @(posedge clk) begin
        if (rst) begin
            q <= '0;
        end else if (en) begin
            q <= d;
        end
        // en=0 时不写 q,寄存器自然保持原值。这里不是锁存器,是触发器保持。
    end
endmodule

5.2 非阻塞赋值 <=:时序逻辑必须用它

在 always_ff 中,几乎总是用 <=。它的直觉是:本拍所有右侧都先用“旧值”计算,到时钟边沿结束后一起更新。

always_ff @(posedge clk) begin
    a <= b;
    b <= a;
end
// 这会交换 a 和 b。因为 a<=b 用旧 b,b<=a 用旧 a。
口诀:always_comb 里用阻塞赋值 =;always_ff 里用非阻塞赋值 <=;不要在同一个 always 块混用。

5.3 同步复位 vs 异步复位

同步复位

always_ff @(posedge clk) begin
    if (rst) q <= 0;
    else     q <= d;
end

复位信号只在时钟上升沿被检查。代码更简单,考试推荐优先使用,除非题目明确要求某个边沿异步复位。

异步复位

always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) q <= 0;
    else        q <= d;
end

复位边沿一来立即清零。课程计数器示例中出现过下降沿复位。敏感列表除了时钟,只应再放异步复位。

5.4 多寄存器同拍更新:写计数器、串行输入很常见

// 每按一次 CLK,保存一个 4-bit 输入。注意 token[11:0] 用的是旧 token。
always_ff @(posedge CLK or posedge RST) begin
    if (RST) begin
        token <= 16'h0000;
        count <= 2'd0;
    end else begin
        token <= {token[11:0], Code};  // 左移 4 位,末尾放入新 Code
        count <= count + 1;
    end
end

如果你在同一个 always_ff 里既更新 token 又马上要比较“加入当前 Code 后的新 token”,不要直接用 token,因为它还是旧值。可以写成 {token[11:0], Code} 参与比较。

6. 计数器、分频、按钮:期末最常见的时序工具

6.1 模 N 计数器

module ModCounter #(
    parameter int MOD = 10,
    parameter int W   = 4
)(
    input  wire       clk,
    input  wire       rst,
    input  wire       en,
    output reg [W-1:0] cnt,
    output wire      last
);
    assign last = (cnt == MOD-1);

    always_ff @(posedge clk) begin
        if (rst) begin
            cnt <= '0;
        end else if (en) begin
            if (last) cnt <= '0;
            else      cnt <= cnt + 1'b1;
        end
    end
endmodule

6.2 00–59 BCD 计数器

两个 BCD 位:低位 0–9,高位 0–5。低位到 9 时归 0 并进位;高位到 5 且低位到 9 时整体回 00。

module Counter00To59 (
    input  wire       clk,
    input  wire       rst,
    input  wire       en,
    output reg  [3:0] high,
    output reg  [3:0] low
);
    always_ff @(posedge clk) begin
        if (rst) begin
            high <= 4'd0;
            low  <= 4'd0;
        end else if (en) begin
            if (low == 4'd9) begin
                low <= 4'd0;
                if (high == 4'd5) high <= 4'd0;
                else              high <= high + 1'b1;
            end else begin
                low <= low + 1'b1;
            end
        end
    end
endmodule

6.3 用 1 MHz 时钟做 1 秒 tick

不要把计数器输出当新时钟乱接。更稳妥的写法是用原始时钟驱动所有寄存器,只产生一个一拍宽的 tick 作为使能。

module Tick1HzFrom1MHz (
    input  wire clk_1m,
    input  wire rst,
    output reg  tick
);
    localparam int DIV = 1_000_000;
    logic [19:0] cnt;  // 2^20 = 1,048,576,够数到 999,999

    always_ff @(posedge clk_1m) begin
        if (rst) begin
            cnt  <= 20'd0;
            tick <= 1'b0;
        end else begin
            tick <= 1'b0;              // 默认没有 tick
            if (cnt == DIV-1) begin
                cnt  <= 20'd0;
                tick <= 1'b1;          // 只有这一拍为 1
            end else begin
                cnt <= cnt + 1'b1;
            end
        end
    end
endmodule

6.4 6 分频:输出频率为输入的 1/6

若要得到近似 50% 占空比的 1/6 频率,可以每 3 个输入时钟翻转一次输出。输出完整周期需要 6 个输入周期。

module Div6 (
    input  wire clk,
    input  wire rst,
    output reg  clk_div6
);
    logic [1:0] cnt; // 0,1,2

    always_ff @(posedge clk) begin
        if (rst) begin
            cnt      <= 2'd0;
            clk_div6 <= 1'b0;
        end else begin
            if (cnt == 2'd2) begin
                cnt      <= 2'd0;
                clk_div6 <= ~clk_div6;
            end else begin
                cnt <= cnt + 1'b1;
            end
        end
    end
endmodule

6.5 边沿检测:把“按下的一瞬间”变成一个时钟周期的脉冲

module EdgeDetect (
    input  wire clk,
    input  wire rst,
    input  wire level,
    output wire rise,
    output wire fall
);
    logic level_d;

    always_ff @(posedge clk) begin
        if (rst) level_d <= 1'b0;
        else     level_d <= level;
    end

    assign rise =  level & ~level_d;
    assign fall = ~level &  level_d;
endmodule

6.6 长按 / 短按判定:在释放按钮时给出 dot/dash 脉冲

模拟题规定短于 1 秒为短按,长于 1 秒为长按。若输入是 1 MHz 时钟,阈值可设为 1,000,000 个周期。下面模板在按钮释放时产生一个一拍脉冲:短按输出 dot_pulse,长按输出 dash_pulse。

module PressClassifier #(
    parameter int THRESH = 1_000_000,
    parameter int W = 21
)(
    input  wire clk,
    input  wire rst,
    input  wire button,
    output reg  dot_pulse,
    output reg  dash_pulse
);
    logic button_d;
    logic [W-1:0] cnt;

    always_ff @(posedge clk) begin
        if (rst) begin
            button_d   <= 1'b0;
            cnt        <= '0;
            dot_pulse  <= 1'b0;
            dash_pulse <= 1'b0;
        end else begin
            dot_pulse  <= 1'b0;       // 脉冲默认拉低
            dash_pulse <= 1'b0;
            button_d   <= button;     // 非阻塞:下面看到的是上一拍 button_d

            if (button && !button_d) begin
                cnt <= '0;             // 刚按下,清零计时
            end else if (button) begin
                if (cnt != {W{1'b1}}) cnt <= cnt + 1'b1;
            end else if (!button && button_d) begin
                // 刚释放,根据按住时长分类
                if (cnt >= THRESH) dash_pulse <= 1'b1;
                else               dot_pulse  <= 1'b1;
                cnt <= '0;
            end
        end
    end
endmodule
手动按钮会抖动。本手册按课程上机题的抽象写法处理,重点是会用计数器和状态机。若实板按钮抖动明显,可在输入前加简单消抖;但期末时间有限,先保证主要功能和复位逻辑。

7. 状态机 FSM:把“按步骤完成任务”翻译成电路

串行密码锁、Morse 输入、菜单模式、逐步显示、协议控制,本质都是状态机。状态机不要直接从代码开始写,先画状态和转移条件。

7.1 三段式 FSM 模板

状态寄存器always_ff: state <= next 次状态逻辑always_comb: next_state 输出逻辑always_comb: outputs 输出LED / 数码管 / flag next_state
typedef enum logic [1:0] {S0, S1, S2, S3} state_t;
state_t state, next_state;

// 1) 状态寄存器:只负责在时钟边沿切换状态
always_ff @(posedge clk or posedge rst) begin
    if (rst) state <= S0;
    else     state <= next_state;
end

// 2) 次状态组合逻辑:根据 state 和输入算 next_state
always_comb begin
    next_state = state; // 默认保持,避免漏赋值
    case (state)
        S0: if (go) next_state = S1;
        S1:         next_state = S2;
        S2: if (ok) next_state = S3;
            else    next_state = S0;
        S3: if (rst_request) next_state = S0;
        default: next_state = S0;
    endcase
end

// 3) 输出组合逻辑:根据当前状态和必要输入产生输出
always_comb begin
    unlock = 1'b0;
    err    = 1'b0;
    case (state)
        S3: unlock = 1'b1;
        default: ;
    endcase
end

7.2 Moore 和 Mealy:考试中怎么选

类型输出依赖优点考试建议
Moore只依赖当前状态稳定、好调试、画图清楚默认使用;数码管显示、状态指示很适合
Mealy依赖当前状态和输入反应可更快,状态数可能更少只在“同一拍输入后马上给 flag”时使用;要小心毛刺

7.3 状态设计流程

  1. 列出必须记住的信息:已经输入几位?当前模式?是否出错?是否完成?
  2. 把“已经到哪一步”变成状态:例如 WAIT1, WAIT2, WAIT3, CHECK, DONE。
  3. 为每条边写条件:按下 CLK、收到 end、计数到阈值、RST。
  4. 为每个状态写输出:显示什么、LED 亮不亮、是否清空/保存寄存器。
  5. 最后翻译成三段式代码。

8. 七段数码管:从 4-bit 十六进制到 seg[6:0]

实验和模拟题都反复使用不带译码的七段数码管。先统一约定:seg[6:0] = {a,b,c,d,e,f,g},且 1 点亮。若题目给的硬件是低有效,输出时取反。

七段位序

   a
 f   b
   g
 e   c
   d

常用模块

module Hex7Seg (
    input  wire [3:0] hex,
    output reg  [6:0] seg
);
    always_comb begin
        case (hex)
            4'h0: seg = 7'b1111110;
            4'h1: seg = 7'b0110000;
            4'h2: seg = 7'b1101101;
            4'h3: seg = 7'b1111001;
            4'h4: seg = 7'b0110011;
            4'h5: seg = 7'b1011011;
            4'h6: seg = 7'b1011111;
            4'h7: seg = 7'b1110000;
            4'h8: seg = 7'b1111111;
            4'h9: seg = 7'b1111011;
            4'hA: seg = 7'b1110111;
            4'hB: seg = 7'b0011111;
            4'hC: seg = 7'b1001110;
            4'hD: seg = 7'b0111101;
            4'hE: seg = 7'b1001111;
            4'hF: seg = 7'b1000111;
            default: seg = 7'b0000000;
        endcase
    end
endmodule

8.1 多位显示:两个或四个数码管

课程实验里的不带译码数码管通常是每个数码管各有一组段线,所以直接例化多个译码器即可。

module TwoDigitDisplay (
    input  wire [3:0] high_hex,
    input  wire [3:0] low_hex,
    output wire [6:0] high_seg,
    output wire [6:0] low_seg
);
    Hex7Seg u_high (.hex(high_hex), .seg(high_seg));
    Hex7Seg u_low  (.hex(low_hex),  .seg(low_seg));
endmodule

8.2 显示调试信息

当核心功能没反应时,先不要猜。把内部变量接到数码管或 LED:当前状态显示为 0/1/2/3,计数低位显示出来,错误标志接 LED。上板考试没有仿真时,这比盲改代码有效得多。

9. 实验 4–7 典型题解法

9.1 实验 4:点亮数码管 / 译码器

题目抽象

输入 sw[3:0],输出 seg[6:0]。本质是 16 项查表。

解法

写 Hex7Seg 模块,用 always_comb case 覆盖 0–F 和 default。重点检查位序和高/低有效。

9.2 实验 5:四位全加器

有两种写法。若题目要求“元件例化”,用一位全加器级联;若只要求实现功能,可以直接用 {Cout, Sum} = A + B + Cin。

结构化级联写法

FullAdder1 u0 (.A(A[0]), .B(B[0]), .Cin(Cin),  .Sum(Sum[0]), .Cout(c[0]));
FullAdder1 u1 (.A(A[1]), .B(B[1]), .Cin(c[0]), .Sum(Sum[1]), .Cout(c[1]));
FullAdder1 u2 (.A(A[2]), .B(B[2]), .Cin(c[1]), .Sum(Sum[2]), .Cout(c[2]));
FullAdder1 u3 (.A(A[3]), .B(B[3]), .Cin(c[2]), .Sum(Sum[3]), .Cout(Cout));

快速行为写法

module Adder4Fast(
    input  wire [3:0] A, B,
    input  wire       Cin,
    output wire [3:0] Sum,
    output wire       Cout
);
    assign {Cout, Sum} = A + B + Cin;
endmodule
如果题目明确要求“使用自己设计的一位全加器构建四位全加器”,就不要只写快速行为写法;否则会被认为没有按要求体现元件例化。

9.3 实验 6:00–59 计数器

核心代码就是两个 BCD 寄存器。输出到七段数码管时,不要让计数器模块直接写一堆七段码;更清楚的结构是:计数器输出 high/low 两个 4-bit BCD,再例化两个 Hex7Seg。

module CounterDisplay (
    input  wire clk,
    input  wire rst,
    input  wire en,
    output wire [6:0] High,
    output wire [6:0] Low
);
    wire [3:0] h, l;
    Counter00To59 u_cnt (.clk(clk), .rst(rst), .en(en), .high(h), .low(l));
    Hex7Seg       u_h   (.hex(h), .seg(High));
    Hex7Seg       u_l   (.hex(l), .seg(Low));
endmodule

9.4 实验 7:串行密码锁

串行密码锁最重要的是“已经输入第几位”的状态。每按一次 CLK 保存当前 Code[3:0],四次后在设置模式保存为密码,在验证模式比较。

module SerialLock (
    input  wire       CLK,     // 手动输入时钟:每按一次输入一位
    input  wire       RST,     // 高有效复位:回到输入第 1 位
    input  wire       Mode,    // 1=设置密码,0=验证密码
    input  wire [3:0] Code,
    output reg        Unlock,
    output reg        Err
);
    typedef enum logic [2:0] {S0, S1, S2, S3, DONE} state_t;
    state_t state;

    logic [15:0] token;
    logic [15:0] password = 16'h1234; // 默认密码;设置模式完成后会改写
    logic [15:0] token_after_input;

    assign token_after_input = {token[11:0], Code};

    always_ff @(posedge CLK or posedge RST) begin
        if (RST) begin
            state  <= S0;
            token  <= 16'h0000;
            Unlock <= 1'b0;
            Err    <= 1'b0;
            // 注意:这里不重置 password。否则设置完密码后按 RST 会丢失密码。
        end else begin
            Unlock <= 1'b0;
            Err    <= 1'b0;

            case (state)
                S0: begin token <= token_after_input; state <= S1; end
                S1: begin token <= token_after_input; state <= S2; end
                S2: begin token <= token_after_input; state <= S3; end
                S3: begin
                    token <= token_after_input;
                    state <= DONE;
                    if (Mode) begin
                        password <= token_after_input;   // 设置新密码
                    end else begin
                        if (token_after_input == password) Unlock <= 1'b1;
                        else                               Err    <= 1'b1;
                    end
                end
                DONE: begin
                    // 等待 RST。也可按题目要求自动回 S0。
                    state <= DONE;
                end
                default: state <= S0;
            endcase
        end
    end
endmodule
修改点:如果题目要求“输入错误立刻报错”,就不要等四位输完再比较;每输入一位就和对应密码位比较,并用 bad 寄存器累积错误。若题目要求“设置密码时也亮一个成功灯”,就在 Mode 分支中置一个 SetOK。

10. 模拟题:摩斯电码项目完整解法思路

模拟题可以拆成五个子问题:十六进制七段译码、用拨码开关输入点/划/结束并查表、用计数器判定长短按、把按钮长短按接入查表、1M 时钟 6 分频。

10.1 Morse 表的编码方式

表中 0–F 的 Morse 码如下:

值Morse值Morse值Morse值Morse
0-----1.----2..---3...--
4....-5.....6-....7--...
8---..9----.A.-B-...
C-.-.D-..E.F..-.

为了让查表简单,可以用“二叉树路径”编码:初始 idx=1;点 . 就左移补 0;划 - 就左移补 1。额外保存长度 len。例如:

  • E = .:len=1, idx=2(二进制 10)
  • A = .-:len=2, idx=5(二进制 101)
  • 0 = -----:len=5, idx=63(二进制 111111)

10.2 Morse 查表函数

function automatic logic [4:0] morse_lookup(
    input logic [2:0] len,
    input logic [5:0] idx
);
    // 返回 {err, hex[3:0]},err=1 表示表中没有这个 Morse 码
    morse_lookup = {1'b1, 4'h0};
    case ({len, idx})
        {3'd5, 6'd63}: morse_lookup = {1'b0, 4'h0}; // 0 -----
        {3'd5, 6'd47}: morse_lookup = {1'b0, 4'h1}; // 1 .----
        {3'd5, 6'd39}: morse_lookup = {1'b0, 4'h2}; // 2 ..---
        {3'd5, 6'd35}: morse_lookup = {1'b0, 4'h3}; // 3 ...--
        {3'd5, 6'd33}: morse_lookup = {1'b0, 4'h4}; // 4 ....-
        {3'd5, 6'd32}: morse_lookup = {1'b0, 4'h5}; // 5 .....
        {3'd5, 6'd48}: morse_lookup = {1'b0, 4'h6}; // 6 -....
        {3'd5, 6'd56}: morse_lookup = {1'b0, 4'h7}; // 7 --...
        {3'd5, 6'd60}: morse_lookup = {1'b0, 4'h8}; // 8 ---..
        {3'd5, 6'd62}: morse_lookup = {1'b0, 4'h9}; // 9 ----.
        {3'd2, 6'd5 }: morse_lookup = {1'b0, 4'hA}; // A .-
        {3'd4, 6'd24}: morse_lookup = {1'b0, 4'hB}; // B -...
        {3'd4, 6'd26}: morse_lookup = {1'b0, 4'hC}; // C -.-.
        {3'd3, 6'd12}: morse_lookup = {1'b0, 4'hD}; // D -..
        {3'd1, 6'd2 }: morse_lookup = {1'b0, 4'hE}; // E .
        {3'd4, 6'd18}: morse_lookup = {1'b0, 4'hF}; // F ..-.
        default: ;
    endcase
endfunction

10.3 简单 Morse 翻译:拨码开关 00/01/10 + CLK

拨码开关 op[1:0] 约定:00=点,01=划,10=结束。每次按 CLK,把当前操作输入一次。结束时查表并显示。

module MorseSwitchInput (
    input  wire       CLK,
    input  wire       RST,
    input  wire [1:0] op,       // 00=dot, 01=dash, 10=end
    output reg  [3:0] hex,
    output reg        err
);
    localparam int MAX_LEN = 5;
    logic [2:0] len;
    logic [5:0] idx;
    logic [4:0] decoded;

    always_comb begin
        decoded = morse_lookup(len, idx);
    end

    always_ff @(posedge CLK or posedge RST) begin
        if (RST) begin
            len <= 3'd0;
            idx <= 6'd1;
            hex <= 4'h0;
            err <= 1'b0;
        end else begin
            case (op)
                2'b00: begin // dot
                    if (len < MAX_LEN) begin
                        idx <= {idx[4:0], 1'b0};
                        len <= len + 1'b1;
                    end else begin
                        err <= 1'b1;
                    end
                end
                2'b01: begin // dash
                    if (len < MAX_LEN) begin
                        idx <= {idx[4:0], 1'b1};
                        len <= len + 1'b1;
                    end else begin
                        err <= 1'b1;
                    end
                end
                2'b10: begin // end, lookup and clear input buffer
                    err <= decoded[4];
                    if (!decoded[4]) hex <= decoded[3:0];
                    len <= 3'd0;
                    idx <= 6'd1;
                end
                default: begin
                    err <= 1'b1;
                end
            endcase
        end
    end
endmodule

10.4 完整 Morse 核心:接收 dot/dash/end 三个脉冲

为了复用,把输入方式抽象成三个“一拍脉冲”:dot_pulse、dash_pulse、end_pulse。拨码输入、长短按输入都可以转成这三个脉冲。

module MorseCore (
    input  wire clk,
    input  wire rst,
    input  wire dot_pulse,
    input  wire dash_pulse,
    input  wire end_pulse,
    output reg  [3:0] hex,
    output reg        err
);
    localparam int MAX_LEN = 5;
    logic [2:0] len;
    logic [5:0] idx;
    logic [4:0] decoded;

    always_comb begin
        decoded = morse_lookup(len, idx);
    end

    always_ff @(posedge clk) begin
        if (rst) begin
            len <= 3'd0;
            idx <= 6'd1;
            hex <= 4'h0;
            err <= 1'b0;
        end else begin
            if (dot_pulse || dash_pulse) begin
                if (len < MAX_LEN) begin
                    idx <= {idx[4:0], dash_pulse}; // dot=0, dash=1
                    len <= len + 1'b1;
                    err <= 1'b0;
                end else begin
                    err <= 1'b1;
                end
            end else if (end_pulse) begin
                err <= decoded[4];
                if (!decoded[4]) hex <= decoded[3:0];
                len <= 3'd0;
                idx <= 6'd1;
            end
        end
    end
endmodule

10.5 长短按 + MorseCore + 七段显示:完整顶层骨架

一个可能的顶层:按钮 KEY 用按住时间产生点/划;另一个按钮或开关 END 表示结束;结果通过七段数码管显示,错误接 LED。

module MorseTop (
    input  wire       CLK1M,
    input  wire       RST,
    input  wire       KEY,       // 按住/释放,分类为 dot/dash
    input  wire       END_BTN,   // 按一下表示结束输入
    output wire [6:0] SEG,
    output wire       ERR_LED,
    output wire       CLK_DIV6_OUT
);
    wire dot_pulse, dash_pulse;
    wire end_pulse;
    wire [3:0] hex;

    PressClassifier #(.THRESH(1_000_000), .W(21)) u_press (
        .clk(CLK1M), .rst(RST), .button(KEY),
        .dot_pulse(dot_pulse), .dash_pulse(dash_pulse)
    );

    EdgeDetect u_end (
        .clk(CLK1M), .rst(RST), .level(END_BTN),
        .rise(end_pulse), .fall()
    );

    MorseCore u_morse (
        .clk(CLK1M), .rst(RST),
        .dot_pulse(dot_pulse), .dash_pulse(dash_pulse), .end_pulse(end_pulse),
        .hex(hex), .err(ERR_LED)
    );

    Hex7Seg u_seg (.hex(hex), .seg(SEG));
    Div6    u_div (.clk(CLK1M), .rst(RST), .clk_div6(CLK_DIV6_OUT));
endmodule
如果题目规定“CLK 按钮每次输入一位/结束”而不是 END_BTN:就用第 10.3 节的 MorseSwitchInput 模块;如果题目规定“同一个按钮按出点/划,另一个动作表示结束”,就用第 10.5 节结构。关键是把题目输入抽象成 dot/dash/end 三种事件。

11. Vivado / XDC / 上板排错

11.1 Vivado 工程流程速记

  1. 新建 RTL Project,选择正确 FPGA 型号/板卡。
  2. 添加 .sv 源文件。确认顶层模块是你要综合的 top。
  3. 添加 .xdc 约束文件。端口名必须和顶层模块完全一致,大小写敏感。
  4. Run Synthesis → Run Implementation → Generate Bitstream → Open Hardware Manager → Program Device。
  5. 若综合失败,先看第一条 error;若实现失败,多半是管脚约束、时钟约束或多驱动。

11.2 常见 XDC 管脚模板

以下是课程文档中反复出现的常用接插孔映射。考试时以题目和课程提供约束为准;若你改了顶层端口名,XDC 也必须同步改。

接口PACKAGE_PIN常见用途
CLK 接插孔J19手动 CLK 或时钟输入
IO0K18RST
IO1–IO4M21, N20, N22, P214 位开关输入或 seg 高位
IO5–IO9P22, T21, U21, R21, R22Cin / B[3:0] / 其他输入
IO11–IO17W21, W22, Y22, Y21, AB22, AA18, AB18数码管低位、LED、Mode
IO18–IO20AA20, AB21, AA21Sum 或 seg 输出
# 示例:顶层端口名为 CLK, RST, sw[3:0], seg[6:0]
set_property -dict {PACKAGE_PIN J19 IOSTANDARD LVCMOS33} [get_ports CLK]
set_property -dict {PACKAGE_PIN K18 IOSTANDARD LVCMOS33} [get_ports RST]

set_property -dict {PACKAGE_PIN M21 IOSTANDARD LVCMOS33} [get_ports {sw[3]}]
set_property -dict {PACKAGE_PIN N20 IOSTANDARD LVCMOS33} [get_ports {sw[2]}]
set_property -dict {PACKAGE_PIN N22 IOSTANDARD LVCMOS33} [get_ports {sw[1]}]
set_property -dict {PACKAGE_PIN P21 IOSTANDARD LVCMOS33} [get_ports {sw[0]}]

set_property -dict {PACKAGE_PIN Y21  IOSTANDARD LVCMOS33} [get_ports {seg[6]}]
set_property -dict {PACKAGE_PIN AB22 IOSTANDARD LVCMOS33} [get_ports {seg[5]}]
set_property -dict {PACKAGE_PIN AA18 IOSTANDARD LVCMOS33} [get_ports {seg[4]}]
set_property -dict {PACKAGE_PIN AB18 IOSTANDARD LVCMOS33} [get_ports {seg[3]}]
set_property -dict {PACKAGE_PIN AA20 IOSTANDARD LVCMOS33} [get_ports {seg[2]}]
set_property -dict {PACKAGE_PIN AB21 IOSTANDARD LVCMOS33} [get_ports {seg[1]}]
set_property -dict {PACKAGE_PIN AA21 IOSTANDARD LVCMOS33} [get_ports {seg[0]}]

set_property CFGBVS VCCO [current_design]
set_property CONFIG_VOLTAGE 3.3 [current_design]

# 如果用非专用时钟管脚作为时钟,课程计数器实验中使用过:
# 注意 get_nets 后的名字可能随端口名变化。若端口叫 CLK,常见为 CLK_IBUF。
set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets CLK_IBUF]

11.3 常见报错与处理

现象 / 报错高概率原因处理
Port ... not foundXDC 端口名和顶层模块端口名不一致检查大小写、数组写法 {seg[0]},确认 top 模块正确
multiple drivers同一信号在多个 always 块或 assign 中赋值每个 reg/logic 只在一个 always 块赋值;线网只由一个源驱动
综合出 latch 警告always_comb 某些分支没赋值给默认值;写完整 else/default
板上数码管乱码seg 位序反了,或高低有效弄反先只显示 0,确认 7'b1111110 是否为 0;必要时调换位序或取反
按钮按一次跳多次机械抖动或手动时钟不干净用慢速按钮时先接受;若影响大,加入简单消抖或改用 1M 时钟采样 + 边沿检测
复位后密码丢失在 RST 分支清空了 password明确 RST 是“回到输入初始状态”还是“恢复出厂密码”;按题目选择
时钟相关 implementation 报错用普通 IO 当时钟按课程约束加 CLOCK_DEDICATED_ROUTE FALSE,或改用真正时钟输入 + clock enable

12. 考试模板库:复制后按题意改

12.1 顶层模板

module Top (
    input  wire       CLK,
    input  wire       RST,
    input  wire [3:0] sw,
    output wire [6:0] seg,
    output wire       led0,
    output wire       led1
);
    // 1. 内部信号
    wire [3:0] hex;
    wire err;

    // 2. 核心模块
    // YourCore u_core (...);

    // 3. 显示/LED
    Hex7Seg u_seg (.hex(hex), .seg(seg));
    assign led0 = err;
    assign led1 = RST; // 临时调试也可以接内部状态
endmodule

12.2 always_comb 模板

always_comb begin
    // 默认值:先给所有输出赋一个确定值
    y      = 4'h0;
    flag   = 1'b0;
    next_s = state;

    case (state)
        S0: begin
            y = 4'h1;
            if (cond) next_s = S1;
        end
        S1: begin
            y = 4'h2;
            if (done) next_s = S0;
        end
        default: begin
            next_s = S0;
        end
    endcase
end

12.3 always_ff 模板

always_ff @(posedge clk) begin
    if (rst) begin
        state <= S0;
        cnt   <= '0;
        data  <= '0;
    end else begin
        state <= next_state;
        if (en) cnt <= cnt + 1'b1;
        if (load) data <= input_data;
    end
end

12.4 函数模板:组合查表

function automatic logic [3:0] table_lookup(input logic [2:0] key);
    case (key)
        3'd0: table_lookup = 4'hA;
        3'd1: table_lookup = 4'h3;
        3'd2: table_lookup = 4'hF;
        default: table_lookup = 4'h0;
    endcase
endfunction

12.5 简单消抖模板(可选)

如果按钮抖动导致一次按下触发多次,可以用一个计数器要求输入稳定若干拍后才更新。考试时间紧时,只有在确实影响结果时再加。

module Debounce #(
    parameter int W = 16
)(
    input  wire clk,
    input  wire rst,
    input  wire noisy,
    output reg  clean
);
    logic [W-1:0] cnt;
    logic last;

    always_ff @(posedge clk) begin
        if (rst) begin
            clean <= 1'b0;
            last  <= 1'b0;
            cnt   <= '0;
        end else begin
            if (noisy != last) begin
                last <= noisy;
                cnt  <= '0;
            end else begin
                if (cnt != {W{1'b1}}) cnt <= cnt + 1'b1;
                else clean <= last;
            end
        end
    end
endmodule

13. 最后检查清单:上板前逐项确认

代码综合前

  • 顶层模块名是否被 Vivado 识别为 top?
  • 所有端口方向、位宽是否和 XDC 一致?
  • always_comb 中是否所有输出都有默认值或完整分支?
  • always_ff 中是否使用 <=?
  • 同一个信号是否只在一个地方赋值?
  • 所有 case 是否有 default?
  • 复位极性和题目一致吗?

上板不对时

  • 先让数码管固定显示 0,验证段码和管脚。
  • 把 state 接到 LED/数码管,看状态是否推进。
  • 把按键边沿脉冲接 LED 或计数器,看是否产生事件。
  • 把错误 LED 和成功 LED 都先默认拉低,避免锁存旧值。
  • 检查 RST 是否一直有效,尤其高/低有效是否反了。
  • 检查输入顺序:四位密码是高位先输还是低位先输。

13.1 期末写代码的推荐顺序

  1. 写显示译码器。它几乎一定会用到,也是最容易独立验证的。
  2. 写核心寄存器/计数器。比如 BCD 计数、Morse idx/len、密码 token。
  3. 写状态机。先只让状态能跑,再填输出。
  4. 写顶层连线。把输入处理、核心、显示接起来。
  5. 写 XDC。端口名字不要临时改来改去。
  6. 综合/实现。先解决语法和多驱动,再上板看现象。
最低可行目标:先做出题目每一步的基础分功能。例如模拟题中先让拨码开关显示 0–F,再做 Morse 查表,再做长短按,最后做完整整合。不要一上来写“大一统顶层”。

参考资料与本手册依据

  • 清华大学《数字逻辑实验》课程文档:实验 4 点亮数码管、实验 5 四位全加器、实验 6 计数器、实验 7 串行密码锁、模拟实验考试、硬件描述语言入门。
  • SystemVerilog.dev:FSM 编码风格、常用设计模式、寄存器和同步/异步复位模板。
  • AMD Vivado Design Suite User Guide: Synthesis (UG901):Vivado 支持 SystemVerilog 构造和综合流程相关参考。

说明:本手册中的代码为备考用途重写的模板,不是课程文档代码的逐字复制。实际考试以题目给出的端口、管脚、复位极性、数码管高低有效为准。