简介:这份资源是2023年FPGA国赛能力测试题的PDF文档,面向备战FPGA竞赛的高校学生与数字逻辑设计学习者,帮助读者熟悉赛题风格与Verilog HDL实现思路。压缩包内共1个PDF文件,约243KB,内容以赛题原文与程序框架为主,便于打印研读或对照仿真。题目涵盖两大典型设计:电梯控制电路要求实现40层楼、初始1层、2至9层未开放需紧急按键解锁,并限制13人、1000kg以内,超标触发alert;非重叠序列检测则要求以六位为一组识别011010,匹配时match拉高、不匹配时not_match拉高,且首位不符即跳过后续五位。文档给出了模块端口定义与benchmark测试代码,读者可据此搭建仿真环境、验证波形,理解状态机与条件判断的写法。目前已有982人学习下载,适合作为赛前自测与查漏补缺的参考材料。
1. 2023FPGA国赛能力测试题:两道仿真题拆开看,哪道更适合练手
如果你手头正好有一块 FPGA 开发板,或者正在准备类似的能力测试,这份 2023FPGA 国赛能力测试题值得花一个下午认真拆一遍。它只包含两道题——电梯控制电路和非重叠序列检测,而且明确要求“只验收仿真波形”,不需要上板。这意味着你不需要昂贵的开发板,只要装好仿真工具就能跑通全流程。电梯题考的是状态机加边界条件处理,序列检测题考的是移位寄存器加时序对齐,两道题覆盖了数字逻辑设计里最核心的两类场景。适合已经学过 Verilog 基础语法、但还没完整写过带 testbench 的仿真项目的同学。下面我把两道题的实现思路、参数设置和容易翻车的地方逐一拆开。
2. 电梯控制电路:40 层状态机怎么搭才不翻车
2.1 为什么用状态机而不是纯组合逻辑
电梯控制的核心是“当前楼层”这个状态变量。每来一个时钟沿,如果目标楼层与当前楼层不同,电梯就向目标方向移动一层。这个行为天然适合用时序逻辑描述。纯组合逻辑做不到“记住当前在哪一层”,所以必须用寄存器保存 N 的值。
题目给了参数floor = 40,但实际楼层范围是 1 到 40。初始楼层为 1 层,复位后 N 应该等于 1。这里有个容易忽略的点:复位信号rst_n是低电平有效,在 testbench 里#15 rst_n = 1之后才释放复位。所以你的复位逻辑要写成if (!rst_n) N <= 1;而不是N <= 0。
另一个关键约束是 2 到 9 层为未开发区域。正常情况下不能把 H 设为 2 到 9 之间的值。但紧急按键 key 的上升沿可以解锁这个限制。注意题目说的是“按下 2-9 的楼层号后,如果检测到紧急按键 key 的上升沿,此时可以到达这些楼层”。这意味着判断顺序是:先看 H 是否在 2 到 9 之间,再看 key 是否有上升沿。如果 key 的上升沿和 H 的设置不在同一个时钟周期内,需要用一个标志位记住“紧急状态已激活”。
2.2 完整实现代码与逐段说明
下面是我按题目要求写的 elevator 模块。代码里加了详细注释,方便你对照理解每个 always 块的作用。
module elevator#(parameter floor = 40)( input clk, rst_n, input key, input [7:0] H, input [3:0] num, input [9:0] weight, output reg [7:0] N, output alert ); // 紧急状态标志:key 上升沿后置 1,允许前往 2-9 层 reg emergency_flag; reg key_dly; // 用于检测 key 的上升沿 // 警报逻辑:人数超过 13 或重量超过 1000kg assign alert = (num > 13) || (weight > 1000); // 检测 key 的上升沿 always @(posedge clk or negedge rst_n) begin if (!rst_n) key_dly <= 0; else key_dly <= key; end // 紧急状态标志管理 always @(posedge clk or negedge rst_n) begin if (!rst_n) emergency_flag <= 0; else if (key && !key_dly) // 检测到上升沿 emergency_flag <= 1; else if (H >= 10 && H <= 40) // 目标楼层回到正常区域后清除 emergency_flag <= 0; end // 楼层移动逻辑 always @(posedge clk or negedge rst_n) begin if (!rst_n) N <= 1; else begin // 判断目标楼层是否合法 if (H >= 1 && H <= floor) begin // 2-9 层需要紧急标志才能前往 if ((H >= 2 && H <= 9) && !emergency_flag) begin // 非法目标,电梯不动 N <= N; end else if (H > N) begin N <= N + 1; // 向上移动一层 end else if (H < N) begin N <= N - 1; // 向下移动一层 end else begin N <= N; // 已到达目标楼层,保持 end end else begin N <= N; // H 超出范围,电梯不动 end end end endmodule这段代码的核心是三个 always 块。第一个块用key_dly延迟一拍来检测上升沿,这是 Verilog 里最常用的边沿检测写法。第二个块管理emergency_flag,当检测到 key 上升沿时置 1,当目标楼层回到 10 层以上时清零。第三个块是楼层移动逻辑,先判断 H 是否在合法范围内,再判断是否需要紧急标志,最后比较 H 和 N 的大小来决定移动方向。
参数方面,floor默认是 40,但你可以改成其他值来测试边界。num是 4 位宽,最大能表示 15,而题目要求超过 13 就报警,所以num > 13这个条件能正确覆盖 14 和 15 两种情况。weight是 10 位宽,最大 1023,超过 1000 就报警,逻辑也是对的。
2.3 用题目给的 testbench 跑仿真
题目已经提供了 benchmark,直接拿来用就行。不过要注意 testbench 里的时序:#20 H = 30; num = 1; weight = 50;之后电梯从 1 层开始向 30 层移动,每个时钟周期移动一层。时钟周期是 10 个时间单位(forever #5 clk = ~clk),所以从 1 层到 30 层需要 29 个时钟周期,也就是 290 个时间单位。testbench 里#400 H = 8;给了足够的时间让电梯到达 30 层。
然后#10 key = 1; @(negedge clk); key = 0;这段是在 8 层目标设置后 10 个时间单位拉高 key,然后在下一个时钟下降沿拉低。这正好产生一个上升沿,让emergency_flag置 1。之后电梯就能向 8 层移动了。
最后#300 H = 12; num = 14;设置新目标并让人数超标,alert应该立刻拉高。再过 20 个时间单位num = 10; weight = 1001;人数恢复正常但重量超标,alert仍然保持高电平。直到weight = 900之后alert才拉低。
跑仿真时重点看几个时间点:复位释放后 N 是否等于 1;H=30 后 N 是否逐周期递增;key 上升沿后 N 是否开始向 8 递减;num=14 时 alert 是否立即变高。如果波形对不上,先检查复位逻辑和边沿检测是否正确。
3. 非重叠序列检测:011010 的六位分组怎么对齐
3.1 非重叠检测和普通序列检测的区别
普通序列检测器用移位寄存器或状态机,每来一个数据位就和目标序列比较,允许重叠。比如检测 011010,如果输入是 011011010,普通检测器会在第 6 位和第 9 位各输出一次 match。但非重叠检测要求每六个输入为一组,组内检测,组间不重叠。题目说得很清楚:“第一位数据不符合,则不考虑后五位。一直到第七位数据即下一组信号的第一位开始检测。”
这意味着你需要一个 3 位计数器,从 0 数到 5,每来一个时钟周期加 1。计数器为 0 时开始接收新的一组六位数据。如果在这六位中间发现不匹配,也要等计数器数到 5 才能开始下一组。换句话说,计数器不受匹配结果影响,始终每 6 个周期循环一次。
输出信号有两个:match 和 not_match。题目要求“在最后一位数据输入的下一拍指示信号 match 拉高一个周期”。注意是“下一拍”,不是同一拍。所以当计数器数到 5 时,你需要在下一个时钟周期输出 match 或 not_match,持续一个周期。
3.2 移位寄存器加计数器的实现方案
下面是我写的 sequence_detect 模块。核心是一个 6 位移位寄存器和一个 3 位计数器。
module sequence_detect( input clk, input rst_n, input data, output reg match, output reg not_match ); reg [5:0] shift_reg; // 6 位移位寄存器 reg [2:0] bit_cnt; // 0-5 计数器 reg [5:0] captured; // 捕获完整六位数据 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin shift_reg <= 6'b0; bit_cnt <= 3'd0; captured <= 6'b0; match <= 0; not_match <= 0; end else begin // 默认输出拉低 match <= 0; not_match <= 0; // 移位寄存 shift_reg <= {shift_reg[4:0], data}; if (bit_cnt == 3'd5) begin // 第六位数据已移入,下一拍比较 captured <= {shift_reg[4:0], data}; bit_cnt <= 3'd0; end else begin bit_cnt <= bit_cnt + 1; end // 在计数器归零的那一拍输出比较结果 if (bit_cnt == 3'd0 && captured != 6'b0) begin if (captured == 6'b011010) begin match <= 1; end else begin not_match <= 1; end end end end endmodule这里有个细节需要解释:captured寄存器在bit_cnt == 5时捕获完整的六位数据,然后在下一个周期bit_cnt == 0时进行比较并输出。这样 match 或 not_match 正好在最后一位数据输入后的下一拍拉高,符合题目要求。
shift_reg在每个时钟周期左移一位,新数据从最低位进入。当bit_cnt == 5时,{shift_reg[4:0], data}就是完整的六位数据。注意这里用的是data而不是shift_reg[5],因为shift_reg还没有更新,当前周期的 data 才是第六位。
3.3 用题目 testbench 验证波形
题目给的 testbench 里数据是24'b011100_011010_011110_011101,每 8 位一组,但实际检测是按每 6 位一组进行的。数据流是循环移出的:D_in <= data[23]; data <= {data[22:0], data[23]};所以实际输入的串行序列是 011100 011010 011110 011101 循环。
按六位分组,第一组是 011100,不匹配,not_match 应该在第六位后拉高。第二组是 011010,匹配,match 应该在第六位后拉高。第三组是 011110,不匹配,not_match 拉高。第四组是 011101,不匹配,not_match 拉高。
跑仿真时重点看 match 和 not_match 是否交替出现,以及它们是否在每组第六位数据后的下一拍拉高。如果 match 和 not_match 同时为高,或者持续时间超过一个周期,说明输出逻辑有问题。
注意:testbench 里的
data是 24 位,但循环移位后会不断重复这 24 位。所以仿真足够长时间后,你会看到 match 和 not_match 按固定模式重复。这是正常的,因为输入序列在循环。
4. 避坑与排查:仿真波形对不上时先查这五处
4.1 复位后 N 不等于 1
现象:仿真开始后 N 一直是 0 或者不定态。原因:复位逻辑写成了N <= 0,或者复位释放的时机和时钟沿没对齐。解决:把复位值改成 1,并确保rst_n在时钟上升沿之前已经拉高。题目 testbench 里#15 rst_n = 1,而时钟在#5和#15各翻转一次,所以复位释放正好在时钟下降沿附近,不会产生竞争。
4.2 紧急按键上升沿检测不到
现象:按下 key 后电梯仍然不去 2-9 层。原因:key_dly没有正确延迟,或者上升沿检测条件写反了。解决:确认key_dly <= key在时钟上升沿执行,检测条件写成key && !key_dly。如果 testbench 里 key 只拉高一个时钟周期,这个写法能正确捕获。
4.3 序列检测的 match 提前或延后一拍
现象:match 在第六位数据输入的同一拍就拉高,而不是下一拍。原因:比较逻辑放在了bit_cnt == 5的分支里直接输出。解决:把比较和输出放到bit_cnt == 0的分支,用captured寄存器保存上一组数据。这样输出自然延后一拍。
4.4 非重叠检测变成了重叠检测
现象:输入 011011010 时 match 出现了两次。原因:没有用计数器强制分组,而是用了纯移位寄存器比较。解决:加入bit_cnt计数器,每 6 个周期强制归零,确保组与组之间不重叠。
4.5 alert 信号毛刺或延迟
现象:num 或 weight 超标的瞬间 alert 没有立即拉高。原因:alert 用了时序逻辑输出,多了一拍延迟。解决:用assign alert = (num > 13) || (weight > 1000);组合逻辑直接输出。题目没有要求 alert 同步,组合逻辑输出更及时。
5. 进阶技巧:用参数化 testbench 自动跑边界用例
两道题都只验收仿真波形,所以 testbench 的质量直接决定你能否快速定位问题。我一般会写一个参数化的 testbench,把关键边界条件都覆盖一遍。比如电梯题,除了题目给的用例,还可以加几个自动检查:复位后 N 是否等于 1;H 设为 1 时 N 是否保持不变;H 设为 41 时 N 是否不动;num 设为 13 时 alert 是否为 0;num 设为 14 时 alert 是否为 1。
下面是一个简单的自动检查片段,放在 testbench 的 initial 块里:
// 自动检查任务 task check_N; input [7:0] expected; begin if (N !== expected) begin $display("FAIL: N=%d, expected=%d at time %t", N, expected, $time); end else begin $display("PASS: N=%d at time %t", N, $time); end end endtask // 在关键时间点调用 initial begin #20; // 复位释放后 check_N(1); // 初始楼层应为 1 #400; check_N(30); // 应该到达 30 层 // ... 其他检查点 end序列检测题也可以用类似方法,在每组数据结束后检查 match 和 not_match 是否符合预期。比如第一组 011100 结束后 not_match 应该为 1,第二组 011010 结束后 match 应该为 1。把这些检查写成 task,跑一次仿真就能看到所有边界条件是否通过,不用盯着波形一个个数周期。
从那以后我每次写仿真题都会先搭一个自动检查的 testbench 骨架,再填功能代码。这样即使波形看不过来,终端输出也能告诉你哪一拍出了问题。希望帮到你。
本文还有配套的精品资源,点击获取