数字逻辑实验期末:SystemVerilog / Vivado FPGA 备考手册
这份手册按“从零能写出能综合、能上板的代码”的目标组织。它不把重点放在仿真,也不过度展开亚稳态理论,而是把考试中最容易遇到的模块——译码器、全加器、计数器、分频器、按钮长短按、串行输入、状态机、七段数码管和 XDC 管脚约束——拆成可直接改写的模板。
SystemVerilog 可综合子集 Vivado 上板 组合逻辑 时序逻辑 FSM 七段数码管 Morse 模拟题0. 考试时应该怎么用这份手册
先搭骨架,再填功能
上机题通常不是考“背语法”,而是给一个 FPGA 小项目,让你把输入、状态、计数和显示连起来。拿到题目后先写顶层端口、XDC 管脚、显示译码器和复位逻辑,再写核心功能。
所有项目都可拆成三类电路
组合逻辑:译码、比较、加法、选择。寄存器/计数器:保存状态、保存输入、计时。状态机:按步骤推进流程。绝大多数考试项目就是这三类的组合。
推荐顺序:先读第 1–7 章建立语法和设计模型,再看第 8–10 章的典型项目,最后把第 12–13 章作为考试速查。
1. 硬件描述的思维方式:你写的不是“按行执行的程序”
SystemVerilog 看起来像 C,但本质完全不同。你写的代码会被 Vivado 综合成电路:线、门、触发器、多路选择器、加法器、比较器等。理解这一点,很多语法规则就自然了。
1.1 三句话理解硬件代码
assign和always_comb描述组合电路。输入一变,输出就按逻辑关系变化。它们不会“记住”过去。always_ff @(posedge clk)描述触发器/寄存器。只有时钟上升沿到来时,寄存器才把右侧的值保存进去。- 状态机就是“寄存器保存当前状态 + 组合逻辑算下一状态 + 组合逻辑/寄存器产生输出”。
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 类型 |
logic | SystemVerilog 更现代的单驱动信号类型 | 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
end3.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
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 顶层模块的职责
- 端口名与 XDC 完全一致。
- 只做“连线”和少量顶层 glue logic;核心功能下沉到子模块。
- 把显示译码器作为独立模块,别在每个功能模块里重复写。
- 必要时输出调试 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 模板
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 状态设计流程
- 列出必须记住的信息:已经输入几位?当前模式?是否出错?是否完成?
- 把“已经到哪一步”变成状态:例如
WAIT1, WAIT2, WAIT3, CHECK, DONE。 - 为每条边写条件:按下 CLK、收到 end、计数到阈值、RST。
- 为每个状态写输出:显示什么、LED 亮不亮、是否清空/保存寄存器。
- 最后翻译成三段式代码。
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
endmodule8.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;
endmodule9.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
MorseSwitchInput 模块;如果题目规定“同一个按钮按出点/划,另一个动作表示结束”,就用第 10.5 节结构。关键是把题目输入抽象成 dot/dash/end 三种事件。11. Vivado / XDC / 上板排错
11.1 Vivado 工程流程速记
- 新建 RTL Project,选择正确 FPGA 型号/板卡。
- 添加
.sv源文件。确认顶层模块是你要综合的 top。 - 添加
.xdc约束文件。端口名必须和顶层模块完全一致,大小写敏感。 - Run Synthesis → Run Implementation → Generate Bitstream → Open Hardware Manager → Program Device。
- 若综合失败,先看第一条 error;若实现失败,多半是管脚约束、时钟约束或多驱动。
11.2 常见 XDC 管脚模板
以下是课程文档中反复出现的常用接插孔映射。考试时以题目和课程提供约束为准;若你改了顶层端口名,XDC 也必须同步改。
| 接口 | PACKAGE_PIN | 常见用途 |
|---|---|---|
| CLK 接插孔 | J19 | 手动 CLK 或时钟输入 |
| IO0 | K18 | RST |
| IO1–IO4 | M21, N20, N22, P21 | 4 位开关输入或 seg 高位 |
| IO5–IO9 | P22, T21, U21, R21, R22 | Cin / B[3:0] / 其他输入 |
| IO11–IO17 | W21, W22, Y22, Y21, AB22, AA18, AB18 | 数码管低位、LED、Mode |
| IO18–IO20 | AA20, AB21, AA21 | Sum 或 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 found | XDC 端口名和顶层模块端口名不一致 | 检查大小写、数组写法 {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 期末写代码的推荐顺序
- 写显示译码器。它几乎一定会用到,也是最容易独立验证的。
- 写核心寄存器/计数器。比如 BCD 计数、Morse idx/len、密码 token。
- 写状态机。先只让状态能跑,再填输出。
- 写顶层连线。把输入处理、核心、显示接起来。
- 写 XDC。端口名字不要临时改来改去。
- 综合/实现。先解决语法和多驱动,再上板看现象。
参考资料与本手册依据
- 清华大学《数字逻辑实验》课程文档:实验 4 点亮数码管、实验 5 四位全加器、实验 6 计数器、实验 7 串行密码锁、模拟实验考试、硬件描述语言入门。
- SystemVerilog.dev:FSM 编码风格、常用设计模式、寄存器和同步/异步复位模板。
- AMD Vivado Design Suite User Guide: Synthesis (UG901):Vivado 支持 SystemVerilog 构造和综合流程相关参考。
说明:本手册中的代码为备考用途重写的模板,不是课程文档代码的逐字复制。实际考试以题目给出的端口、管脚、复位极性、数码管高低有效为准。