状态、条件与输出:FPGA 小系统的通用骨架

本文除了部分经常复制粘贴的模块由我自己整理外,部分模块由AI整理,所有模块都仿真验证过,本文写法不一定最适合工程,但适合考试手搓嘿嘿
前几天准备一场 FPGA 考试,我把四道往年题、两道AI出的模拟题和练习上的一道题放在一起看,发现重复的东西很多:状态的划分方式差不多,状态之外的小模块也反复出现。这篇把这些重复的部分整理出来。文中用到的题目都放在最后的附录里,正文里提到某个模块时,会注明它出现在哪道题中。文末附录前有一节附件说明,这篇文章用到的模板库和参考实现都可以打包下载。
环境配置:从 VS Code 到仿真选择
先说我平时的配置。严格讲 VS Code 不是编译器,它只是编辑器,把它「配成编译器」实际是这么几步:
- 装插件 Verilog-HDL/SystemVerilog(作者 mshr-h),拿到语法高亮和代码补全;
- 在设置里把检查器选成 xvlog:设置项
verilog.linting.linter选xvlog; - 再设
verilog.linting.path,填 Vivado 安装目录下bin的路径(或者把这个目录加进系统 PATH,二选一)。
配完之后,写代码时语法错误会当场标出来,报错编号是 VRFC 10-xxxx——那就是 Vivado 编译器的编号,等于把正式编译的检查提前到了写字阶段,不用等打开 Vivado 才发现少了个分号。我用的是 Vivado 2023,考试环境是 2018,配置方法一样,只是路径换成对应版本的安装目录。
写这类代码会碰到三种「编译」,区别简单说:
- 语法编译(xvlog):只查代码写得合不合语法,不运行。最快,写代码的阶段用的就是它。
- 行为仿真(Vivado 仿真器):把设计和 TB 连起来真的跑一遍,输出波形,验的是逻辑和拍数对不对。这门课的仿真题最后看的都是它。
- 综合:把代码翻译成实际电路(查找表、触发器这些),是上板之前的一步。纯仿真题用不到,弩箭那种上板题才需要走综合、加引脚约束、生成下载文件。
另外还有第三方仿真器,比如 Icarus Verilog、Verilator,特点是轻量、启动快,一条命令就出结果,自己练题时快速验证很方便——上面那个插件的检查器也可以换成它们。它们和 Vivado 在个别细节(比如 X 态的处理)上可能不完全一致,所以我的习惯是:平时练题用轻量工具跑通,最后到 Vivado 里用官方 TB 再复核一遍,以课程环境为准。
一个例子:导弹防御题
练习上一个简单的题目是科技馆的一个演示系统,三个状态:
- S0 待机:没发现敌人时停在这里。
o_cnt保持 0,四个灯每 2 个 clk 翻转一次,0000 和 1111 交替。检测到i_enemy为高,进 S1。 - S1 准备:
o_cnt仍为 0。四个灯中只有一个亮,亮的位置按 [0]、[1]、[2]、[3] 的顺序轮换,每 2 个 clk 移一次,进入 S1 时从 [0] 开始。i_ready为高时进 S2。 - S2 倒计时:灯全灭,
o_cnt从 10 开始,每 1 个 clk 减 1,依次显示 10、9、……、1、0,然后回 S0。
状态机的意思就是这样:系统在某一时刻只处于一个状态,做这个状态里规定的事;转移条件满足,就换到下一个状态。电饭煲的做饭、煮粥、保温,洗衣机的放水、洗涤、漂洗、脱水,结构上是同一件事,区别在状态数量、每个状态停留的时间和输出的样子。
写代码的顺序
我写这类题时,代码分成三段,每一段只负责一件事:
- 第一段是状态寄存器。每个时钟沿把「下一状态」存进来,除此之外不写别的逻辑。
- 第二段决定下一状态。当前状态是什么、转移条件是否满足、满足去哪、不满足是否保持,都在这一段里写清楚。
- 第三段决定输出。灯和计数的具体数值由下面的小模块算好,这一段只按当前状态做选择,不现算。
动手写代码之前,先把三件事列出来:有哪些状态、每个状态的转移条件、每个状态的输出。列不出来的时候,通常是题还没读完,写下去容易返工。
我之前的写法:一个 always 块写到底
说明一下,我以前不是这么写的。以前我的习惯是把状态、计数和输出都写在一个时序 always 块里,一个 case 把每个状态要做的事全包了,状态跳转也在里面顺手改掉,也就是常说的一段式:
always @(posedge i_clk or negedge i_rstn) begin if (!i_rstn) begin state <= IDLE; cnt <= 0; o_led <= 4'b0000; end else begin case (state) FILL: begin o_led <= 4'b0000; if (cnt == 2) begin cnt <= 0; state <= WASH; end else cnt <= cnt + 1'b1; end // 其余状态照此写下去:判断、计数、输出都在同一个块里 endcase endend也有时写成两段:一个时序块管状态和计数,另一个组合块管转移和输出,介于一段和三段之间。
这两种写法本身没有问题,题目规模小的时候写起来还更快,课程里也常见。我换成三段加模块拆分,是因为在洗衣机题上遇到了几个实际问题:
- 一段式里每个状态的分支都很长,改一处要把整个块重新读一遍,复制粘贴时容易漏改。前面提到的闪烁初值写错,就是从相邻分支复制过来忘了改值。
- 计数、图案和状态挤在一起,跑出波形不对时,不容易判断是转移条件错了,还是计数错了,还是图案本身错了。拆开之后,每一块可以单独对着题目核对。
- 输出在时序块里直接赋值,输出错一拍时不容易看出来。拆开后输出只做选择,图案在进入状态之前就摆好待命值,核对点更明确。
所以这个写法不是唯一选择,是我这次改过来之后觉得更适合自己检查的一种。状态少、时间紧的时候,一段式写完也不是不行,先保证转移条件和计数正确,比结构更重要。
九个常用小模块
1. 边沿检测
按键类的输入,题目要的往往是「按下的那一个瞬间」,而不是「一直按着」。做法是保存上一拍的值,两拍做比较:
reg sig_d;always @(posedge i_clk or negedge i_rstn) begin if (!i_rstn) sig_d <= 1'b0; else sig_d <= i_sig;endwire sig_pos = i_sig & ~sig_d; // 上升沿:这一拍刚变高wire sig_neg = ~i_sig & sig_d; // 下降沿:这一拍刚变低出现位置:彩灯题的两个按钮、电饭煲设定信号的拉低、弩箭题的发射按键。
顺带一个读题时遇到的情况:导弹题的端口说明里写 i_enemy 是「上升沿表示敌人出现」,但后面的具体转换逻辑写的是「检测到为高时进入 S1」。两处不一致时,我按具体转换逻辑写,这道题用的是电平判断,不需要边沿检测。
2. 阶段计时
题目写「在某状态持续 N 个 clk」,用的是同一个写法:一个计数器只在本状态里数,不在本状态时清零;数满的信号用一根组合线引出来,直接给第二段当转移条件。
reg [7:0] cnt;always @(posedge i_clk or negedge i_rstn) begin if (!i_rstn) cnt <= 8'd0; else if (state != FILL) cnt <= 8'd0; // 不在本状态,清零 else cnt <= cnt + 1'b1;endwire fill_done = (state == FILL) && (cnt == 8'd2); // 持续 3 拍:数到 2
// 第二段里对应的一行// FILL: if (fill_done) next_state = WASH;fill_done 不用寄存器,要在本拍内生效。用寄存器存一拍再送出去,这个状态就会多停一拍,后面部分我也踩了坑。
出现位置:洗衣机 FILL、RINSE、SPIN 三个阶段,电饭煲做饭和煮粥的计时,导弹题 S2 之外的计时需求也多是这一类。
3. 三种灯的图案:流水、进度条、闪烁
四个灯常见的变化方式有三种,区别在移位时填进去什么:
// 流水灯:一个亮点循环移动(这里从高位向低位)reg [3:0] flow;always @(posedge i_clk or negedge i_rstn) begin if (!i_rstn) flow <= 4'b1000; else if (state != WASH) flow <= 4'b1000; // 待命值 = 进入时第一拍 else flow <= {flow[0], flow[3:1]};end// 变化过程:1000 -> 0100 -> 0010 -> 0001 -> 1000
// 进度条:亮起的格数逐拍增加,亮过的保持reg [3:0] prog;always @(posedge i_clk or negedge i_rstn) begin if (!i_rstn) prog <= 4'b1000; else if (state != RINSE) prog <= 4'b1000; // 待命时先摆好第一格 else prog <= {prog[3], prog[3:1]};end// 变化过程:1000 -> 1100 -> 1110 -> 1111
// 闪烁:整体取反reg [3:0] blink;always @(posedge i_clk or negedge i_rstn) begin if (!i_rstn) blink <= 4'b1111; else if (state != SPIN) blink <= 4'b1111; // 进入时第一拍为全亮 else blink <= ~blink;end// 变化过程:1111 -> 0000 -> 1111流水和进度条的代码很像,区别只在移入的那一位:流水移入的是绕回来的旧值,进度条在高位填 1、把已亮的保留下来。写的时候容易混,对照上面的变化过程核对一遍比较稳妥。
有些题目要求图案每 2 个 clk 才变一次(导弹题的 S0 和 S1、把漂洗改成 8 拍的版本)。办法是加一个来回翻转的小信号,图案只在它为 1 的那一拍更新:
reg div;always @(posedge i_clk or negedge i_rstn) begin if (!i_rstn) div <= 1'b0; else if (state != S1) div <= 1'b0; else div <= ~div;end// 图案块里把更新条件改成:else if (div) scan <= {scan[2:0], scan[3]};出现位置:流水灯在彩灯题和洗衣机 WASH,进度条在电饭煲和洗衣机 RINSE,闪烁在彩灯题、洗衣机 SPIN 和导弹题 S0。
4. 置位-清除锁存
自检类题目的输出和当前状态不是一回事,它记录的是「发生过什么」:电池检查通过,对应的灯亮起并保持到回 INIT 才灭;出现过错误,蜂鸣器保持响,同样到 INIT 才停。这种输出要用锁存写法,并且置位和清除看的是下一状态,这样进入目标状态的同一拍输出就对上:
always @(posedge i_clk or negedge i_rstn) begin if (!i_rstn) begin o_led_r <= 2'b00; o_buzz_r <= 1'b0; end else if (next_state == INIT) begin o_led_r <= 2'b00; // 只有回 INIT 才清除 o_buzz_r <= 1'b0; end else begin if (next_state == BAT_OK) o_led_r[0] <= 1'b1; if (next_state == MOT_OK) o_led_r[1] <= 1'b1; if (next_state == BAT_ERR || next_state == MOT_ERR) o_buzz_r <= 1'b1; // 其余时候保持不动 endend出现位置:启动自检题和无人机自检题的 LED 与蜂鸣器。
5. 一次性定时
蜂鸣器响几拍后停止、停止之后再打印学号,这类「触发一次、运行固定拍数、自己结束」的动作,单独写一个小块放在状态机外面,用一个 busy 标志表示正在运行,结束时给出一个 done 脉冲供下一步使用:
reg busy, buzzer_r;reg [7:0] bcnt;assign o_buzzer = buzzer_r;wire beep_done = busy && (bcnt == BEEP_N - 1); // 响完这一拍为 1
always @(posedge i_clk or negedge i_rstn) begin if (!i_rstn) begin busy <= 1'b0; buzzer_r <= 1'b0; bcnt <= 8'd0; end else if (!busy && trig) begin busy <= 1'b1; buzzer_r <= 1'b1; bcnt <= 8'd0; // 触发 end else if (busy) begin if (bcnt == BEEP_N - 1) begin busy <= 1'b0; buzzer_r <= 1'b0; // 响够,停 end else begin bcnt <= bcnt + 1'b1; end endend打印学号接在 beep_done 上:蜂鸣结束那一拍把学号送到输出口,之后保持到下一轮开始。把这类动作留在状态机外面,状态表能少几个状态,转移关系也清楚一些。
出现位置:电饭煲的蜂鸣 2 拍后打印、洗衣机的蜂鸣 3 拍后打印。
6. 倒计时计数
导弹题 S2 用的是向下的计数,而且计数值本身就是输出。写法是计数器在状态外保持起始值,进入状态后逐拍减 1,减到 0 作为转移条件:
reg [3:0] dcnt;always @(posedge i_clk or negedge i_rstn) begin if (!i_rstn) dcnt <= 4'd10; else if (state != S2) dcnt <= 4'd10; // 状态外保持 10 else if (dcnt > 0) dcnt <= dcnt - 1'b1;endassign o_cnt = (state == S2) ? dcnt : 4'd0;
// 第二段里对应的一行// S2: if (dcnt == 0) next_state = S0;一个容易数错的地方:从 10 显示到 0 是 11 个数值,这个状态共停留 11 拍,0 也要显示一拍。转移条件写「等于 0」;写成「等于 1」会少显示一个 0。
7. 模式计数
彩灯题里按一下按钮换一种模式,模式本身可以看成一个计数器:加到最大值绕回 0,减到 0 绕回最大值。两个按钮同一拍都有效时,要先定一个优先级,题面没写明的,自己在代码注释里注明:
reg [1:0] mode;always @(posedge i_clk or negedge i_rstn) begin if (!i_rstn) mode <= 2'd0; else if (btn1_pos) mode <= (mode == 2'd3) ? 2'd0 : mode + 1'b1; else if (btn0_pos) mode <= (mode == 2'd0) ? 2'd3 : mode - 1'b1;end// btn1_pos、btn0_pos 是模块 1 得到的上升沿;同拍时 btn1 优先8. 显示输出的两种情况
一种是纯数值端口,例如自检题的 o_msg 和电饭煲、洗衣机的 o_print。到达指定状态时直接把数值赋给输出,其余时候为 0,不需要译码:
assign o_msg = (state == READY) ? ID_LAST2 : 8'd0;另一种是真的七段数码管(弩箭题的箭数显示),要多两步:先把数值拆成十位和个位,再逐位查表换成段码。段码表的具体位序要按所用板子的定义写,下面只示意结构:
wire [5:0] val = arrow_cnt; // 0–64wire [3:0] tens = val / 10;wire [3:0] ones = val % 10;
function [6:0] seg7; input [3:0] d; begin case (d) 4'd0: seg7 = 7'b0111111; 4'd1: seg7 = 7'b0000110; 4'd2: seg7 = 7'b1011011; // 3–9 按同一张表依次列全 default: seg7 = 7'b0000000; endcase endendfunction9. 进入状态时的初值
题目经常对进入某状态的第一拍有明确要求:导弹题 S1「进入时从 [0] 开始」、S2「从 10 开始」,洗衣机 SPIN「第一个 clk 为全亮」。实现办法在前面的代码里已经出现多次:图案和计数器不在自己的状态时,保持在初值上不动:
else if (state != S1) scan <= 4'b0001; // 待命值就是进入后第一拍要显示的值这样进入状态的第一拍自然是要求的值,不需要在状态机里另外做处理。每个图案、每个计数器都要检查这一行:待命值写错,进入相位就错,洗衣机那次我的错误之一就在这里。
写模拟题一时,我错了四处
考前一晚我写洗衣机模拟题,状态划分和转移条件一次写完,四处错误都在细节上,是跑仿真时暴露出来的,单看代码没有看出来:
- 阶段计时的 done 信号我用寄存器输出,慢了一拍,放水阶段实际停留 4 拍而不是 3 拍。模块 2 里的
fill_done用组合线,就是为了避免这个问题。 - 复位极性写反。
i_rstn是低电平有效,几个 always 块的敏感列表我写成了上升沿触发。结果是复位拉低时不能立即生效,释放复位的瞬间又多触发一次。仿真里复位通常保持若干拍,表面上能运行,不容易察觉。 - 脱水闪烁的初值写成了 1000,是从流水灯那一段复制过来后没有改。输出在 1000 和 0111 之间变化,题目要求的是 1111 和 0000 交替。取反的逻辑没有问题,错在模块 9 说的待命值。
- 打印学号时把输出口的位宽看错。
o_print是 21 位,最大值 2097151,我填入的数值超过这个范围,高位被截掉,打印出来是另一个数。给常数赋值之前,先确认目标信号的位宽,是这一条的教训。
四处错误都不是思路问题,是写完之后没有逐项核对。现在我写完这类代码会先跑一遍仿真,对着波形把每个状态的停留拍数和进入第一拍的输出核对一遍。
做题的顺序
- 列状态和转移条件,画出状态转移图。
- 给每个状态定停留时间和输出,从上面的模块里选用现成的写法。
- 按三段分工写代码:状态寄存器、下一状态、输出选择。
- 计数、图案、蜂鸣这些放在状态机外面,分别写成小块。
- 跑仿真核对波形,重点看每个状态的停留拍数、进入第一拍的输出、离开那一拍的输出。
为什么学 Verilog
先坦白一下,我学 Verilog 的起点没有多高尚,主要原因是:课表里有,考试要考,不学不行。刚开始写的时候还挺不服,觉得这玩意儿怎么比写软件别扭这么多:软件里代码是一行一行往下跑的,Verilog 倒好,所有东西一起动,时钟一跳全场开会,我稍微数错一拍,波形就开始自由发挥。
写多了才慢慢认命,它其实不是故意折腾人,它只是逼着你把问题说清楚:现在处在哪个状态,满足什么条件才往下走,待在这个状态时输出到底是什么。以前我写东西喜欢先上手再说,到了 Verilog 这里基本行不通,脑子里没表,手上就全是补丁。
所以真要问为什么学:一开始是为了过课,后来发现状态、条件、输出这套拆法确实能救命,再后来做项目又绕回了它。想研究怎么让 AI 更好地生成和验证硬件代码,自己总不能连波形都看不懂,不然工具说啥都只能在旁边点头,那就有点太心虚了。
对大多数同学来说,学它的目的不是以后天天手写 RTL,而是能看懂、能改、能验证数字系统——这也是为什么这门课的考试最后落点都在状态机上。
幕间:能看到这里的,都是真爱
正文到这里就结束了,能一路看到这里的,说明你是真的对 Verilog 代码有点喜欢了,哈哈哈哈哈。毕竟这玩意儿平时没人会当睡前读物翻。
顺便吐槽一下这次考试:全程纯手搓,一个补全都没有,一个报错提示都没有,计数器写错一位只能自己肉眼找。平时写代码写惯了,敲一半编辑器就把后半截递到嘴边,考场上突然回到全手动模式,第一反应是手怎么这么生。更难受的是仿真还得在脑子里跑,波形长什么样全靠想象,我写洗衣机题那四个错,考场上要是也这么写,大概率就带着它们一起交卷了。所以考完之后我悟出来平时练题也是能跑仿真就跑仿真吧,考场上没这个条件,考前把手感喂熟一点,多少能救一点哈哈哈。
好了,吐槽完毕。下面是附录,把文中用到的题都收在一起了,真想动手写一遍的,可以从洗衣机那道开始,写完拿题面逐条对。
附件说明
这篇文章用到的代码整理成了一个附件包(fpga-小系统-附件包.zip),发布时附在本文下载区,内容如下:
small_modules_lib.v:正文九个小模块的模板库,包括边沿检测、节拍计数、流水灯、进度条、置位-清除锁存、一次性定时、可逆计数、七段译码等,可以直接改参数套用。buzzer_special.v:蜂鸣器的四种写法,锁存响、定时响、连响和方波发声。q1_led_control.v、q2_boot_fsm.v、q3_rice_cooker.v:三道真题的参考实现,文件头注释里写了参数和状态的设定依据。mock_exam/:两道模拟题的题面、参考实现和自测 TB。tb/:真题参考实现对应的自测 TB;run_sims.sh是一键运行全部仿真的脚本;README.md里列了题面没有写清、由我定的几处约定。
使用上有三点说明:
- 参考实现里的学号是占位值(参数默认 0,自测 TB 里用的是示例数),用之前要换成自己的。
- 自测 TB 是按我对题面的理解写的,检查的是状态停留拍数、图案顺序和蜂鸣打印时机;和官方 TB 的检查口径可能不完全一样,对不上时先核对 README 里列的约定。
- 模拟题的参考实现建议自己先写完再打开对照。真题部分没有收弩箭题的参考实现,那道是上板题,涉及板卡引脚和数码管扫描,纯仿真代码的参考意义不大,正文和附录里只用了它的题面。
附录:文中用到的题目
A. 模拟导弹防御系统(练习)
任务要求:设计一个有限状态机,模拟科技馆「宇宙探秘展厅」中的太空导弹防御演示系统。系统在无敌人时循环待机;敌人来袭后进入防御准备,等待锁定目标、方位调整、导弹装填等工作就绪;全部就绪后进入发射计时,倒计时结束后导弹发射,回到待机,等待下一次来袭。
输入输出端口:i_clk 时钟信号;i_rst_n 复位信号,低电平有效;i_enemy 敌军来袭,上升沿表示敌人出现;i_ready 防御系统就绪信号,高电平有效;o_led[3:0] 按需亮起;o_cnt[3:0] 倒计时计数。
状态定义:S0 正常运行模式,未发现敌人;S1 防御准备模式,敌人来袭,但未完全就绪;S2 发射计时模式,准备完毕,发射倒计时(每个状态各 20 分)。
具体转换逻辑:
- S0 状态:
o_cnt恒为 0;o_led[3:0]整体以 4 个 clk 为周期闪烁,即 4’b0000 与 4’b1111 交替,每 2 个 clk 翻转一次。在该状态下检测到i_enemy为高时进入 S1,否则保持 S0。 - S1 状态:
o_cnt恒为 0;o_led[3:0]中只有一位为 1,其余为 0,点亮位置按o_led[0]→ [1] → [2] → [3] → [0] 的顺序循环移动,每 2 个 clk 移动一次,进入 S1 时从o_led[0]开始。当i_ready == 1时进入 S2,否则保持 S1。 - S2 状态:
o_led[3:0]全灭;o_cnt从 10 开始,每 1 个 clk 减 1,依次显示 10、9、8……1、0,然后进入 S0。
B. 模拟题一:智能洗衣机控制器(AI,对应电饭煲那题)
设计一个状态机,包括以下状态:IDLE 待机;FILL 进水;WASH 洗涤;RINSE 漂洗;SPIN 脱水;DONE 完成。模块名 washer_ctrl。
具体转换逻辑:
- IDLE:
i_start拉高,进入 FILL。 - FILL:持续 3 个 clk 后进入 WASH,期间
o_led全灭。 - WASH:持续时间由
i_time设定(单位 clk,取值 1–255),期间亮起以 1 clk 为单位的流水灯(从高位到低位的顺序循环点亮)。附加分:i_gentle高电平时为轻柔模式,WASH 持续时间变为i_time的 2 倍,流水节拍不变。 - RINSE:持续 4 个 clk,每过 1 个 clk 按从高位到低位的顺序点亮一格作为进度条展示,四个灯全亮时进入 SPIN。
- SPIN:持续 5 个 clk,期间四个灯以 1 clk 为单位闪烁(全亮、全灭交替,进入 SPIN 的第一个 clk 为全亮)。
- DONE:四个灯全亮。如果
i_start拉低,进入 IDLE(蜂鸣器响 3 个 clk,打印学号)。
端口:i_clk 时钟;i_rstn 全局复位,低电平有效;i_start 启动信号,低电平关闭、高电平工作;i_time[7:0] 设定洗涤时间;i_gentle 轻柔模式,高电平有效;o_led[3:0] 按需亮起;o_buzzer 蜂鸣器,完成状态下关闭洗衣机时响 3 个 clk;o_print[20:0] 蜂鸣器叫后打印十进制学号。
C. 模拟题二:无人机起飞前自检(自拟,对应启动自检题型)
状态:INIT 初始化;CHK_BAT 检查电池;BAT_OK 电池正常;BAT_ERR 电池异常;CHK_MOT 检查电机;MOT_OK 电机正常;MOT_ERR 电机异常;READY 自检完成、可起飞。模块名 drone_check。
具体转换逻辑:INIT 转换到 CHK_BAT;CHK_BAT 中 i_bat_low 为 1 转 BAT_ERR,否则转 BAT_OK;BAT_OK 转换到 CHK_MOT;CHK_MOT 中 i_mot_err 为 1 转 MOT_ERR,否则转 MOT_OK;MOT_OK 转换到 READY;BAT_ERR、MOT_ERR、READY 三个状态中,i_rst_btn 按下转 INIT,否则保持。
输出:o_led[0] 在 BAT_OK 点亮后一直保持,直到 INIT 才灭;o_led[1] 在 MOT_OK 点亮后一直保持,直到 INIT 才灭;o_buzzer 在错误状态工作并一直保持,直到 INIT 才停止;o_msg[7:0] 自检完成后输出学号后两位(十进制)。
D. 往年题原文(AI按原题转录)
D1. 全真模拟测试一:2023 年秋测试真题(节日彩灯控制系统)
设计一种节日彩灯控制系统,该系统包含 2 个按键输入 btn[1<0>0>],和 4 个 LED 灯,该系统一共有 4 种模式,分别是:
- LED 全部关闭;
- LED 全部点亮;
- LED 以 200ns 的频率闪烁;(时钟频率 100Mhz,T=10ns)
- LED 流水灯,每个周期进行流水;
初始情况下,处于模式 1;每检测到 btn[1] 的上升沿,则进入下一种模式;每检测到 btn[0] 的上升沿,则进入上一种模式。
模块头:
module led_control( input clk, input rst, input [1:0] btn, output [3:0] led);注:统一使用 QQ 群内提供的仿真文件 “led_control_tb.v” 进行仿真。
打分标准:基础知识问答(20 分);画出状态转移图,描述思路(20 分);能够进入仿真,演示波形(10 分);模式 1 和 2 波形正确(10 分)、模式 3 波形正确(10 分)、模式 4 波形正确(10 分);模式切换正确(10 分);一小时内完成(10 分)。
D2. 基于操作系统启动自检报错的状态机设计
背景:在操作系统启动过程中,需要进行一系列初始化、检查和加载操作,同时可能会遇到自检出的各种错误。为提高系统的健壮性,设计一个状态机来识别并处理启动错误,以及通过按键和 LED 向用户反馈系统状态信息。
任务要求:设计一个状态机,包括以下状态:INIT 初始化状态,系统准备启动;CHECK_FS 检查文件系统状态;FS_OK 文件系统正常状态;FS_ERR 文件系统错误状态;LD_DRIVER 加载驱动状态;DRIVER_OK 驱动加载正常状态;DRIVER_ERR 驱动加载错误状态;BOOT_OK 启动完成状态。
具体转换逻辑:
- INIT 状态:转换到 CHECK_FS 状态。
- CHECK_FS 状态:
i_fs_err为 1,转移到 FS_ERR 状态,否则转换到 FS_OK 状态。 - FS_OK 状态:转换到 LD_DRIVER 状态。
- FS_ERR 状态:如果
i_rst_btn按键被按下,转换到 INIT 状态,否则保持在该状态。 - LD_DRIVER 状态:如果
i_driver_err为 1,转换到 DRIVER_ERR 状态,否则转换到 DRIVER_OK 状态。 - DRIVER_OK 状态:转换到 BOOT_OK 状态。
- DRIVER_ERR 状态:如果
i_rst_btn按键被按下,转移到 INIT 状态,否则保持在该状态。 - BOOT_OK 状态:如果
i_rst_btn按键被按下,转换到 INIT 状态,否则保持在该状态。
输出信号与状态的关系:
o_led[0]:FS_OK 状态时,点亮o_led[0],点亮后一直保持,直至 INIT 状态,才重新灭灯。o_led[1]:DRIVER_OK 状态时,点亮o_led[1],点亮后一直保持,直至 INIT 状态,才重新灭灯。o_msg[7:0]:启动完成,后输出自己学号的后两位(十进制输出)。o_buzzer:错误状态时,使蜂鸣器工作并一直保持,直至 INIT 状态,才停止工作。
输入输出端口说明:i_clk 时钟信号;i_rstn 状态机全局复位信号,低电平有效;i_fs_err 文件系统错误信号,高电平表示错误;i_driver_err 驱动错误信号,高电平表示错误;i_rst_btn 用户重置按键,高电平表示按下;o_led[1:0] 高位(o_led[1])表示 DRIVER 是否加载成功的绿色提示灯,低位(o_led[0])表示 FS(文件系统)是否加载成功的绿色提示灯,高电平表示灯亮;o_msg[7:0] boot 完成后输出自己学号的后两位;o_buzzer 蜂鸣器警告信号,高电平表示蜂鸣器工作,发出警告。
D3. 全真模拟测试三:2025 年秋测试真题(电饭煲)
设计一个状态机,包括以下状态:IDLE:初始化状态,系统准备启动;RICE:电饭煲进入做饭状态;PORRIDGE:电饭煲进入做粥状态;INSULATION:电饭煲进行保温。
具体转换逻辑:
- IDLE 状态:
i_set拉高,根据i_mode进入 RICE 状态或 PORRIDGE 状态。 - RICE 状态:每过 2 个 clk 按从高位到低位的顺序点亮灯作为进度条展示,四个灯全亮进入 INSULATION 状态。附加分:
i_extra高电平时,每过 4 个 clk 按从高位到低位的顺序点亮灯,然后进入 INSULATION 状态。 - PORRIDGE 状态:根据设定的
i_time设置煮粥时间,期间亮起以 1clk 为单位的流水灯(从高位到低位的顺序点亮流水灯)。 - INSULATION 状态:四个灯全亮。如果
i_set拉低,进入 IDLE 状态(蜂鸣器响 2 个 clk,打印学号)。
输入输出端口说明:i_clk 时钟信号;i_rstn 状态机全局复位信号,低电平有效;i_set 选择是否启动电饭煲,低电平关闭,高电平工作;i_mode 选择煮饭模式或煮粥模式,低电平煮饭,高电平煮粥;i_time[7:0] 设定煮粥时间;i_extra 附加功能,煮饭是否要锅巴,低电平不要锅巴,高电平要锅巴;o_led[3:0] 按需亮起,详情见具体转换逻辑;o_buzzer 蜂鸣器,保温状态下打开电饭煲盛饭或粥时响起 2 个 clk;o_print[20:0] 蜂鸣器叫后打印十进制学号。
D4. FPGA 期中补测题:《墨家机关术——连弩车发射模拟系统》(上板题)
题目背景:南京市华谊兄弟电影小镇的四期建设规划方案中,拟打算采用中国古代春秋战国时期的景象;其中一部分的街区靶场上需要放置墨家机关中的连弩车、转射机和辕车。通过让观众亲手操作,来体验中华古文化的灿烂,增强民族文化认同感。但是最终出于安全考虑,公司决定不打算让观众直接接触连弩车,而是采用远程的杆子拨动来控制连弩车的发射。现公司建设规划部门邀请你作为连弩车远程控制发射模拟的项目负责人,对系统的整体控制进行设计实现。已知目前讨论确定的控制部分流程(状态图),请按照该流程进行开发设计。
题目抽象:
- 连弩车共有 64 支箭,由拨动开关 SW0~SW15 来控制箭支的发射,每个拨动开关 SWi 控制 4 支弩箭的发射,即:每个开关 SWi 代表 4 支弩箭。
- 弩车解锁键 BTNC(U18),按下代表弩车解锁(只有在弩车锁定状态时,该按键才有效);弩车上锁键 BTNL(W19),按下代表弩车上锁(只有在弩车解锁状态时,该按键才有效)。
题目要求:
- 首先是弩车锁定状态(S0 状态),此时数码管显示当前弩箭数(拨动开关处于低电平的个数 × 4),锁定时弩箭数不得改变;16 个 LED(LD0~LD15)呈现全灭状态。如果弩车解锁键按下(BTNC),则进入弩车解锁状态(S1 状态)。
- 然后是弩车解锁状态(S1 状态),此时数码管显示当前弩箭数;可通过拨动开关 SW0
SW15 进行弩箭发射,每个拨动开关 SWi 拨至高电平,弩箭数减少 4 支,期间拨回开关,弩箭不可增加;每次发射(即:拨动开关 SWi 拨至高电平),16 个 LED(LD0LD15)都需要进行一次 0.1s 为间隔的左移高速流水,流水初始位置必须为 LD0(U16)。如果当前弩箭数为 0,则进入弩车装填状态(S2 状态)。如果弩车上锁键按下(BTNL),则回到弩车锁定状态(S0 状态)。 - 最后是弩车装填状态(S2 状态),此时数码管显示当前弩箭数(拨动开关处于低电平的个数 × 4),可通过拨动开关拨至低电平,进行弩箭装填,每次装填 4 支;16 个 LED(LD0~LD15)呈现全亮状态。如果全部装填完毕,即拨动开关全部拨至低电平,则回到弩车锁定状态(S0 状态)。
注:数码管显示的位置无要求,初始上电时,处于弩车锁定状态。
得分点参考:① 能够根据要求,实现弩车的 3 种状态跳转(10 分/每种);② 弩车锁定状态(S0 状态)下,数码管显示正确(10 分);16 个 LED 呈现全灭状态(10 分);③ 弩车解锁状态(S1 状态)下,数码管显示正确(10 分);每次发射,16 个 LED 灯能以 0.1s 为间隔进行一次左移流水灯(0.1s 流水灯 5 分,每次发射可进行流水 5 分,全部成功 5 分);④ 弩车装填状态(S2 状态)下,数码管显示正确(10 分),能够根据拨动开关进行装填 5 分,全部装填成功 5 分;16 个 LED 呈现全亮状态(5 分)。
(原题另有验收方式、提问与时间得分等考试事项,与题目本身无关,此处从略。)
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
评论区
校园随手记一下校园今晚头顶的星空最新日记无题碎碎念还得是听歌啊😭😭😭😭😭2026-11-09突然觉得写项目书也好难啊项目书被老师打回来两次了。。。2026-11-09早睡计划失败!啊啊啊啊私有化部分仓库啊喂2026-11-07今天立冬以为要吃饺子呢!反应了两三秒是冬至吃饺子!2026-11-07电子大楼你周围的风都快给我刮走了风真的好大,不想去实验室2026-11-07










































