news 2026/10/28 14:32:08

FPGA大作业:PARWAN控制段状态机VHDL设计与后仿真排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA大作业:PARWAN控制段状态机VHDL设计与后仿真排查

简介:这份文档是面向高校软件工程、电子信息类专业学生的FPGA数字系统设计课程大作业参考,对应院选课考核场景,可为初学者提供从处理器结构梳理到状态机实现的完整范例。内容围绕PARWAN处理器的运算、控制与寻址三部分架构展开,重点剖析CONTROL-SECTION状态机的VHDL设计思路,并给出在Spartan3E XC3S500E芯片上建工程、编写测试文件与后仿验证的完整过程,附有仿真结果截图与结论分析,适合用来对照理解取址、运算各阶段的控制流程。压缩包内为1个doc文档,约763KB,便于直接查阅与改写排版。该资源已有553人学习浏览,可作为课程作业撰写、VHDL入门练习与实验报告整理时的参考素材,帮助读者快速建立数字系统设计的整体框架认知。

1. 从一份 FPGA 数字系统设计大作业说起:PARWAN 的控制通路到底在考什么

很多人拿到这份 FPGA 数字系统设计大作业,第一反应是画个状态图、照模板把 VHDL 敲进去、跑出一张波形截图就交。真跑一遍才发现,PARWAN 处理器这道题的难点压根不在代码量——CONTROL-SECTION 状态机满打满算几十行,难的是你得说清楚:为什么 MAR 必须在取指沿之前把 PC 的值稳住,为什么 ALU 的运算结果要等写回沿才准进累加器,为什么后仿真跑出来的波形比功能仿真晚了一个时钟周期。这份大作业把 PARWAN 架构综述和 CONTROL-SECTION 状态机的 VHDL 设计、后仿真结果绑在一起,考的就是「你能不能把控制通路的时序账算明白」。适合两个阶段的人:刚摸到 fpga 开发门槛、想从「能编译」跨到「能对上波形」的新手,以及要交一份能答辩、能被追问细节的大作业的熟手。

2. 剖开 PARWAN 架构:运算段、控制段、寻址段的职责边界

2.1 三段划分与那条「控制→取址→运算→回控制」的主回路

PARWAN 这套模型机在教材里被切得很干脆:左边是运算段,中间是控制段,右边是寻址取数段。初次看结构图容易产生一个错觉——数据从存储器出来就直接进 ALU 了。实际上从存储器到 ALU 之间至少要过两拍:第一拍 PC 的值送进 MAR,MAR 把地址挂到地址总线上,存储器开始读;第二拍数据才回到 MDR,再由 MDR 转进 IR 或数据寄存器。这两拍不是多余的,它们就是控制段状态机必须存在、且必须至少有 fetch / capture 两个状态的物理原因。

我一般把这个主回路画成一句口诀:控制段发命令 → 寻址段把数据搬回来 → 运算段算完写回 → 状态寄存器的标志位再喂回控制段。控制段是唯一一个「既发命令又收结果」的部件,它的输入是 IR 里的操作码和 SR 里的条件码,输出是全套 load / enable / read / write 信号。理解了这一点,架构综述那部分就不会写成结构图的文字复述,而是能讲出「谁驱动谁、谁等谁」。

对写代码的人来说,更重要的是这条回路意味着状态机的状态数不是拍脑袋定的,而是被数据通路的最长延迟链卡住的。存储器读延迟如果是两拍,你就不能把 fetch 和 capture 合成一个状态,除非你愿意在组合逻辑里等存储器——上板之后那必然翻车。

2.2 六个关键部件的时序职责,谁先动谁后动

把结构图拆成部件表,每个部件的写使能由谁给、在哪一拍动作,这张表想清楚了,状态机的输出信号自然就出来了。下面按我实际写代码时的顺序列:

部件职责写使能来源动作时机
PC存放下一条指令地址控制段 pc_inc / pc_load取指状态结束的沿
MAR锁存本次访问的地址控制段 mar_load取指状态第一个沿
MDR接收存储器返回的数据存储器侧或控制段取指状态第二个沿
IR锁存当前指令控制段 ir_loadcapture 状态结束的沿
ALU组合运算控制段 alu_op组合,随输入变化
ACC锁存运算结果控制段 acc_load写回状态结束的沿
SHU移位控制段 shu_en写回状态
SR记录零标志、进位等控制段 sr_load写回状态结束的沿

这张表里藏着一个容易被忽略的约束:IR 必须在译码状态之前就已经稳定。也就是说 ir_load 只能在 capture 状态拉高,不能在 decode 状态拉高——否则译码用的是上一拍的旧操作码,仿真波形上看状态跳得挺顺,实际执行的全是错指令。我见过不止一份作业栽在这个地方,波形上 opcode 和 state 差一拍,答辩时被问一句就露馅。

ACC 和 SR 的写回时机同理。ALU 是纯组合逻辑,它的输出在操作数变化后几十纳秒内就有效,但你不能让它在同一拍直接进 ACC,因为这时候总线上的数据可能还没稳定。稳妥做法是把 ALU 的运算放在 exec 状态,把结果锁进 ACC 放在 writeback 状态,中间隔一个时钟沿。多花一拍换来的是时序收敛和波形可读性。

2.3 先把控制段接口钉死:信号清单与编码约定

写状态机之前我一般先把 entity 的 port 列完,列不完就别急着写 architecture。原因是状态机的每个状态本质上就是「给哪些输出赋值」,接口没定,状态写出来一定是边写边改。

控制段的输入侧一共就四类:时钟与复位、启动脉冲、IR 送来的操作码、SR 送来的条件码。输出侧分三组,数据通路控制组(pc_inc、pc_load、mar_load、ir_load、acc_load、shu_en、sr_load)、存储器控制组(mem_read、mem_write)、运算控制组(alu_op)。

编码约定也要提前定。我这份里 alu_op 用 2 位,约定 "00" 直通、"01" 加、"10" 减、"11" 与;操作码用 3 位,"000" 空操作、"001" 加法、"010" 减法、"011" 条件跳转、"100" 移位、"111" 停机。这些编码不是标准,你自己的模型机文档写什么就跟什么,关键是全篇一致——状态机、Test Bench 的断言、答辩 PPT 上的状态图,三处编码必须一模一样,改一处就得三处全改。

提示:把这份信号清单单独存成一个文本文件放在工程目录下,写代码时对着敲,比来回翻结构图快得多,也避开了漏信号这种低级错误。

3. 从新建工程到仿真骨架:Spartan3E 器件配置与 par_control_unit_tb 搭建

3.1 新建工程的三项必填参数:器件、封装、仿真器

打开集成环境,File → New Project,填工程名和路径,路径里别带中文和空格,这是血泪经验——工具链对中文路径的处理一向玄学,报错信息还指向别处。点 Next 进器件选择页,四项必须和开发板对得上:

  • Family 选 Spartan3E
  • Device 选 XC3S500E
  • Package 选 FG320
  • Speed Grade 按开发板器件丝印上的后缀选,常见是 -4

综合工具保持默认,仿真器选 ISim,首选语言选 VHDL。这三项选错了后面很麻烦:仿真器选成 Modelsim 而本机没装,点仿真会直接弹找不到可执行文件;语言选成 Verilog,新建 VHDL 源文件时模板不全。

器件页之后是工程信息确认页,扫一眼没问题点 Finish。到这里工程是空的,先别急着写代码,右键工程 → New Source → VHDL Module,把 par_control_unit 这个 entity 建出来,端口按上一章那份清单填。工具会自动生成 entity 和空 architecture 的骨架,比自己手敲省事,也避免了端口名拼写不一致。

XC3S500E 是 500K 系统门级别的器件,跑一个七八个状态的控制器资源占用可以忽略不计。这也意味着你没法用「资源不够」当借口去解释为什么状态机写得别扭——资源从来不是这份大作业的约束,时序和可读性才是。

3.2 用 VHDL Test Bench 模板生成激励文件

Test Bench 不要手写,用模板生成能省掉大量样板代码。Project → New Source → 左侧选 VHDL Test Bench → 右侧文件名填 par_control_unit_tb → Next → 在关联文件列表里勾选 par_control_unit → Next → Finish。工具会生成一个已经实例化好 DUT、带时钟和复位骨架的 tb 文件。

生成的骨架需要改三处:把默认的时钟周期改成和板级晶振一致、把复位保持时间拉够、把激励序列补上。我改完的版本大致这样:

library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity par_control_unit_tb is end entity par_control_unit_tb; architecture sim of par_control_unit_tb is -- 20 ns 对应 50 MHz,和开发板晶振一致;换板子先改这个常数 constant CLK_PERIOD : time := 20 ns; signal clk : std_logic := '0'; signal rst : std_logic := '1'; signal start : std_logic := '0'; signal opcode : std_logic_vector(2 downto 0) := (others => '0'); signal z_flag : std_logic := '0'; signal pc_inc : std_logic; signal mar_load : std_logic; signal ir_load : std_logic; signal acc_load : std_logic; signal shu_en : std_logic; signal sr_load : std_logic; signal alu_op : std_logic_vector(1 downto 0); signal mem_read : std_logic; signal mem_write : std_logic; signal state_out : std_logic_vector(2 downto 0); signal done : std_logic; begin -- 时钟由 tb 自己产生,不依赖外部激励 clk <= not clk after CLK_PERIOD / 2; dut : entity work.par_control_unit port map ( clk => clk, rst => rst, start => start, opcode => opcode, z_flag => z_flag, pc_inc => pc_inc, mar_load => mar_load, ir_load => ir_load, acc_load => acc_load, shu_en => shu_en, sr_load => sr_load, alu_op => alu_op, mem_read => mem_read, mem_write => mem_write, state_out => state_out, done => done ); stim : process begin -- 复位至少压 3 个时钟,让所有输出寄存器落到确定值 rst <= '1'; wait for 3 * CLK_PERIOD; rst <= '0'; wait until rising_edge(clk); -- 用例一:普通加法指令,验证 fetch 阶段 MAR 有没有拉高 opcode <= "001"; start <= '1'; wait until rising_edge(clk); start <= '0'; wait until rising_edge(clk); -- 进入 fetch wait until rising_edge(clk); -- 输出寄存晚一拍,等一拍再断言 assert mar_load = '1' report "fetch 阶段 mar_load 没有拉高" severity error; wait for 5 * CLK_PERIOD; -- 用例二:条件跳转且 z_flag 为 1,验证跳转分支 opcode <= "011"; z_flag <= '1'; start <= '1'; wait until rising_edge(clk); start <= '0'; wait for 8 * CLK_PERIOD; assert pc_inc = '0' report "跳转成立时 pc_inc 不应该拉高" severity error; report "simulation finished" severity note; wait; end process; end architecture sim;

代码里两个 assert 是关键。第一个卡住的是方向性错误——很多人 fetch 阶段漏了 mar_load,仿真能跑完,但数据通路根本不动;第二个卡的是分支逻辑,跳转成立与否直接决定 PC 是加一还是被覆盖。assert 在仿真日志里给的 severity error 会直接标红,比人眼扫波形靠谱多了。

参数说明:CLK_PERIOD 是全篇唯一的时间基准,后面对拍全靠它;复位保持 3 个时钟是我习惯的下限,1 个时钟有时候不够——输出寄存器多的时候,最后一个未必来得及落到初值;waith until rising_edge 和 wait for 的区别在于前者对齐沿、后者对齐时间,断言一律用前者,避免相位漂移。

3.3 编译顺序、顶层设置与 UCF 的最小集合

源码树里的层次是 par_control_unit_tb 顶层 → par_control_unit 实例。综合的时候要把顶层切回 par_control_unit,仿真的时候再切回 tb,这一步忘了会报「顶层模块没有输出端口」之类的怪错。

编译顺序上,工具一般能自动解析依赖,但如果 tb 先于被实例化的实体编译,会报「component not found」。稳妥做法是在源文件列表里把 par_control_unit 调到 tb 上面,右键 → Set as Top Module 时也只对目标文件操作。

UCF 只影响综合和实现,仿真不读它。但这份大作业要上板验证的话,至少三个信号要有约束:

# 引脚号是示例,必须对照开发板原理图逐个替换 NET "clk" LOC = "C9" | IOSTANDARD = LVCMOS33; # 晶振输入 NET "rst" LOC = "K17" | IOSTANDARD = LVCMOS33; # 复位按键 NET "start" LOC = "K18" | IOSTANDARD = LVCMOS33; # 启动按键 NET "done" LOC = "K19" | IOSTANDARD = LVCMOS33; # 完成指示

不写 UCF 也能生成比特流,工具会自己分配引脚,结果是上板之后按键按下去没反应——不是逻辑错,是引脚分配到了你根本够不着的地方。这个坑我在第一次上板时踩过,排查了半个下午才想起来没写约束文件。

4. CONTROL-SECTION 状态机的 VHDL 落地:三段式写法与输出寄存

4.1 状态编码怎么选:枚举、二进制还是独热码

状态编码这件事新手容易纠结,实际经验是:状态数在 8 个以内、时序不紧张,直接用 VHDL 的枚举类型,让综合工具自己决定编码,可读性最好;状态数上到十几个、关键路径开始吃紧,才考虑手动指定独热码,一个状态一位,次态逻辑变成稀疏的与或,速度快但触发器用量上去。

这份控制段状态机一共七个状态,用枚举完全够用。要强制工具用二进制编码,加一条综合属性即可:

type state_t is (ST_IDLE, ST_FETCH, ST_CAPTURE, ST_DECODE, ST_EXEC, ST_WB, ST_HALT); signal cur_state, nxt_state : state_t; attribute fsm_encoding : string; attribute fsm_encoding of cur_state : signal is "binary";

属性值 "binary" / "one-hot" / "compact" 综合工具都认,"compact" 是让它自己算最省资源的方案。我的做法是先跑一遍 "compact" 看资源报告,如果触发器用量和时序余量都还行就不动它,别为了「看起来专业」硬套独热码。

还有一个隐性的好处:枚举类型在仿真波形里直接显示状态名,ST_FETCH 比 0001 好认一百倍。后仿真的时候波形里几十个信号挤在一起,能一眼看出状态名,排查效率差好几个量级。

4.2 完整状态机代码与逐段拆解

三段式写法的好处是每一段的职责单一:第一段只做状态寄存,第二段只算次态,第三段只驱动输出。这样综合出来的电路清晰,仿真时也容易定位问题出在哪一段。完整代码:

library ieee; use ieee.std_logic_1164.all; entity par_control_unit is port ( clk : in std_logic; -- 系统时钟 rst : in std_logic; -- 异步复位,高有效 start : in std_logic; -- 启动脉冲 opcode : in std_logic_vector(2 downto 0); -- IR 送来的操作码 z_flag : in std_logic; -- SR 的零标志 pc_inc : out std_logic; mar_load : out std_logic; ir_load : out std_logic; acc_load : out std_logic; shu_en : out std_logic; sr_load : out std_logic; alu_op : out std_logic_vector(1 downto 0); mem_read : out std_logic; mem_write : out std_logic; state_out : out std_logic_vector(2 downto 0); -- 仅供仿真观测 done : out std_logic ); end entity par_control_unit; architecture rtl of par_control_unit is type state_t is (ST_IDLE, ST_FETCH, ST_CAPTURE, ST_DECODE, ST_EXEC, ST_WB, ST_HALT); signal cur_state, nxt_state : state_t; attribute fsm_encoding : string; attribute fsm_encoding of cur_state : signal is "binary"; begin -- 第一段:状态寄存器,只干一件事 seq_state : process (clk, rst) begin if rst = '1' then cur_state <= ST_IDLE; elsif rising_edge(clk) then cur_state <= nxt_state; end if; end process; -- 第二段:纯组合的次态逻辑,不碰任何输出端口 comb_next : process (cur_state, start, opcode, z_flag) begin nxt_state <= cur_state; -- 默认保持,避免锁存器 case cur_state is when ST_IDLE => if start = '1' then nxt_state <= ST_FETCH; end if; when ST_FETCH => nxt_state <= ST_CAPTURE; -- 固定等一拍存储器返回 when ST_CAPTURE => nxt_state <= ST_DECODE; when ST_DECODE => nxt_state <= ST_EXEC; when ST_EXEC => if opcode = "111" then -- 停机 nxt_state <= ST_HALT; elsif opcode = "011" and z_flag = '1' then -- 条件跳转成立,不回写 nxt_state <= ST_FETCH; else nxt_state <= ST_WB; end if; when ST_WB => nxt_state <= ST_FETCH; -- 回到取指,形成闭环 when ST_HALT => nxt_state <= ST_HALT; -- 停机自锁 when others => nxt_state <= ST_IDLE; end case; end process; -- 第三段:寄存器输出,与状态同步一拍 out_reg : process (clk, rst) begin if rst = '1' then pc_inc <= '0'; mar_load <= '0'; ir_load <= '0'; acc_load <= '0'; shu_en <= '0'; sr_load <= '0'; alu_op <= "00"; mem_read <= '0'; mem_write <= '0'; done <= '0'; elsif rising_edge(clk) then -- 先整体清零,再按状态覆盖,避免上一拍的信号残留 pc_inc <= '0'; mar_load <= '0'; ir_load <= '0'; acc_load <= '0'; shu_en <= '0'; sr_load <= '0'; alu_op <= "00"; mem_read <= '0'; mem_write <= '0'; case cur_state is when ST_FETCH => mar_load <= '1'; mem_read <= '1'; when ST_CAPTURE => ir_load <= '1'; mem_read <= '1'; when ST_EXEC => case opcode is when "001" => alu_op <= "01"; -- 加 when "010" => alu_op <= "10"; -- 减 when "100" => shu_en <= '1'; -- 移位 when others => alu_op <= "00"; end case; when ST_WB => acc_load <= '1'; sr_load <= '1'; pc_inc <= '1'; -- 顺序执行,PC 加一 when ST_HALT => done <= '1'; when others => null; end case; end if; end process; -- 状态编码输出,只为了波形好看,综合时会优化掉 with cur_state select state_out <= "000" when ST_IDLE, "001" when ST_FETCH, "010" when ST_CAPTURE, "011" when ST_DECODE, "100" when ST_EXEC, "101" when ST_WB, "110" when ST_HALT, "111" when others; end architecture rtl;

逐段说明:第一段是标准的异步复位 D 触发器阵列,复位信号写在敏感列表里,上板按复位键立刻回 idle,不等时钟沿;第二段全部是组合逻辑,case 之前先给 nxt_state 赋默认值 cur_state,这一行是防锁存器的关键,漏了综合工具会生成一堆锁存器加警告;第三段是输出寄存器,每个时钟沿先全清零再覆盖,这样任何状态下没被显式赋值的信号都保证是低电平,不会残留上一个状态的值。

参数说明:opcode 的编码必须和 IR 那侧一致,改一处要全改;z_flag 只在 ST_EXEC 分支里参与判断,别的状态读它没意义;pc_inc 在 ST_WB 拉高意味着「顺序执行时 PC 加一」,而跳转分支在 ST_EXEC 直接回 ST_FETCH 绕开了 ST_WB,所以 pc_inc 不会误拉高——这个绕行设计比在 ST_WB 里再判一次操作码要干净。

4.3 输出为什么必须寄存一拍

组合输出和寄存器输出的差别,功能仿真上几乎看不出来,后仿真和上板上差别巨大。组合输出的信号在状态切换的瞬间会随着次态逻辑的中间值抖一下,形成毛刺。功能仿真不建延迟模型,毛刺不显示;后仿真加载了布线延迟和器件延迟,毛刺会被完整还原,于是波形上你会看到 mar_load 在某个沿跳了两下——下游的存储器一旦把这个毛刺当有效读信号,读出来的就是垃圾数据。

代价是寄存器输出比状态本身晚一拍。cur_state 在沿 T 更新,输出寄存器在同一沿读的却是更新前的 cur_state,所以输出在沿 T+1 才反映状态 T 的值。写断言的时候必须把等待时间往后挪一个周期,否则断言全红,人会以为逻辑错了,其实是采样点错了。

注意:如果实在不想晚这一拍,可以把第三段的判断对象从 cur_state 换成 nxt_state,输出就和状态同拍。代价是输出变成 Mealy 型,依赖输入信号的组合路径,时序分析上多了一条从输入到输出的路径,状态数一多就容易违例。七个状态的小设计可以这么干,大设计别学。

5. 排查清单:五个让后仿真波形对不上的坑

5.1 坑一:复位释放太早,状态机停在未初始化态

现象:功能仿真波形里 cur_state 从第一个沿开始就是某个确定状态,但后仿真里前面一大段是 X,直到很久之后才落到某个值。

原因:Test Bench 里的复位脉冲只给了一个时钟周期,而输出寄存器有十几个,复位路径经过的布线延迟不完全一致,最后一个寄存器还没落到初值,复位就撤了。撤复位的那一拍,一部分寄存器是 0、一部分是 X,状态机次态逻辑读到 X 就一路 X 下去。

解决:复位保持时间给到 3 个时钟周期起步,器件大、扇出高就给 5 个。更稳的做法是在 Test Bench 里加一句wait for 3 * CLK_PERIOD;之后再撤复位,别用wait until rising_edge(clk)单拍撤。后仿真里 X 的传播很顽固,一旦进了状态机,后面几十拍全是 X,看到这种波形先怀疑复位,别怀疑代码。

5.2 坑二:仿真时钟和板级晶振不是一个节奏

现象:仿真跑得好好的,上板之后八段数码管上的计数飞快,或者按键按下去完全没反应。

原因:Test Bench 里的 CLK_PERIOD 随手写了 10 ns(100 MHz),而开发板晶振是 50 MHz。仿真只关心逻辑对不对,不关心快慢,所以功能仿真完全发现不了这个问题。上板之后所有计数、消抖、状态驻留时间全部减半,按键消抖逻辑直接失效。

解决:把 CLK_PERIOD 定义成一个常数,值按开发板原理图上的晶振频率算,50 MHz 就是 20 ns。所有时间相关的常数(消抖计数、延时计数)都用CLK_PERIOD * N的形式写,别写死绝对时间。换板子只改一个常数,全篇跟着变。

5.3 坑三:组合输出毛刺被后仿真放大成假跳变

现象:后仿真波形里 mem_read 在状态切换的沿附近出现一两个极窄的脉冲,宽度只有几纳秒,功能仿真里完全没有。

原因:第三段输出逻辑写成了组合进程(process 里没有 clk),输出直接由 cur_state 译码得到。状态切换瞬间,cur_state 的多个位不是同时翻转,译码逻辑在中间态上会短暂输出错误值。功能仿真零延迟,看不到;后仿真有真实延迟,毛刺完整呈现。

解决:输出一律走时钟沿寄存,按 4.3 节那样写。如果某个信号必须是组合的(比如 alu_op 要给组合 ALU 用),那就在下游加一级寄存器采样,别让毛刺直接进存储器或寄存器堆。

5.4 坑四:opcode 与状态转移没对齐,跳进死锁

现象:仿真跑到一半状态不动了,波形上 cur_state 停在某个状态一直循环,done 也不拉高。

原因:次态逻辑里某个分支漏了 else,case 又不全,综合工具插了锁存器或者默认回到自身。更常见的是操作码比较写错——比如条件跳转编码应该是 "011",代码里写成 "011 " 或者位宽不匹配被截断,导致这个分支永远进不去,程序在 exec 和 wb 之间无限循环。

解决:次态进程的 case 之前必须给 nxt_state 赋默认值;case 必须带 others 分支;所有状态转移的条件用字面量比较时,位宽和信号声明严格一致。我习惯在每个状态转移后面加注释写明触发条件,改代码时对着注释看,比对着逻辑推导快。

5.5 坑五:后仿真没加载延迟模型,跑的还是功能仿真

现象:点了「后仿真」,跑出来的波形和功能仿真一模一样,没有任何延迟,毛刺也看不见。

原因:后仿真需要先完成综合、映射、布局布线,然后生成带时序信息的网表和后缀为 sdf 的延迟文件。只点了仿真按钮、没走完实现流程,工具拿到的还是 RTL 模型,跑的自然还是功能仿真。另一常见原因是仿真设置里没把 sdf 文件关联上。

解决:流程走全——综合、实现、生成后仿真网表,三步都完成后再启动后仿真。启动前在仿真属性里确认 sdf 文件路径指向实现目录下生成的那个文件,时间单位(timescale)和 tb 里的保持一致。跑起来之后先看波形开头有没有延迟,有延迟才说明模型加载对了。

6. 波形判读的进阶手法:拿状态编码反推时序违例

后仿真波形拉出来之后,别从头到尾一格格看,先看三个采样点。第一个点是复位撤销后的第一个时钟沿,看 cur_state 是否落在 ST_IDLE、所有输出是否全零,这一步不过后面全白搭。第二个点是 ST_FETCH 状态,看 mar_load 和 mem_read 是否同时为高,且这个高电平只维持一个时钟周期——如果维持了两个周期,说明次态逻辑里 fetch 有两个出口,得回去查 case。第三个点是 ST_WB 之后,看 pc_inc 是否拉起、acc_load 是否在同拍有效。

判断有没有时序违例,有个偷懒但管用的办法:把 state_out 和几个关键输出叠在一条时间轴上,量它们边沿之间的距离。寄存器输出的设计,输出边沿应该比状态边沿正好晚一个时钟周期;如果某个输出的边沿比状态晚了两个周期,说明它被两级寄存器打了,查代码里是不是多写了一级 process;如果它比状态早,那它大概率是组合输出,回去补寄存。

波形里出现窄脉冲(宽度小于半个时钟周期)一律当成毛刺处理,别安慰自己「可能是显示器刷新问题」。真正有效的做法是把游标卡在那个脉冲上,看它出现在哪个状态的切换沿,再回头看那个状态的输出赋值是不是有互斥分支没写全。

最后一件事,做完后仿真之后把波形保存成截图,同时在报告里把「功能仿真 vs 后仿真」的差异写清楚:延迟增加了多少、毛刺出现在哪几个信号上、为什么最后确认功能正确。答辩的时候被追问「你怎么证明这是对的」,能拿出这三样东西,比十张漂亮的结构图管用。

从那以后我每次交这种带仿真的作业,都会强制走一遍「功能仿真 → 加断言的 tb → 后仿真 → 波形对比」四步,哪怕时间再紧也不跳步。跳过一步省下的半小时,往往要在答辩前用一整个通宵补回来。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 14:32:07

大模型落地工业现场:嵌入式硬件选型与边缘推理实战指南

工业现场对大模型的接纳速度&#xff0c;比大多数人预想的要快。过去两年我跑过不少产线改造项目&#xff0c;也蹲过几次工业展会的展台&#xff0c;最大的感受是&#xff1a;前几年大家聊边缘计算还停留在"把数据传上去再算"&#xff0c;而现在越来越多的方案开始强…

作者头像 李华
网站建设 2026/10/28 14:31:30

PARWAN处理器FPGA控制段VHDL状态机实现与仿真避坑

简介&#xff1a;一份面向高校软件工程、电子信息类专业学生的FPGA数字系统设计课程大作业文档&#xff0c;对应院选课综合考核场景&#xff0c;适合正在学习fpga开发、需要完成PARWAN处理器相关作业或复盘数字系统设计流程的读者参考。文档围绕PARWAN处理器的运算、控制与寻址…

作者头像 李华
网站建设 2026/10/28 14:30:52

脉动阵列深度解析:从数据流动到AI芯片设计实践

1. 从一次面试翻车聊起&#xff1a;为什么脉动阵列值得单独拎出来讲前几年我面一家做推理加速的芯片团队&#xff0c;面试官问了一个问题&#xff1a;“你们上一版加速器里MAC阵列的利用率是多少&#xff1f;”我当时脱口而出“峰值算力的百分之七八十吧”&#xff0c;对方接着…

作者头像 李华
网站建设 2026/10/28 14:30:42

impeccable用法全解析:从词根到场景,告别翻译腔

这个单词我盯了很久&#xff1a;impeccable。词典里翻来覆去就那几句&#xff0c;"无可挑剔的""无懈可击的"&#xff0c;但真到了写邮件、发简历、跟客户沟通的时候&#xff0c;十个人里有九个用不对。要么把它跟"perfect"混为一谈&#xff0c;要…

作者头像 李华
网站建设 2026/10/28 14:29:08

Caveman数字极简指南:离线优先与注意力管理的三个月实践

1. Caveman 是什么&#xff1a;我为什么把自己活成穴居人如果你每天解锁手机八十多次&#xff0c;却想不起来上一次完整读完一篇文章是什么时候&#xff0c;那 Caveman 这套玩法或许值得你看看。Caveman 是我给自己一个“低科技生活实验”起的代号&#xff0c;听起来像是要搬进…

作者头像 李华
网站建设 2026/10/28 14:28:24

impeccable用法详解:与perfect、flawless的区别及地道搭配

1. 说白了&#xff1a;拆解“impeccable”的本义和词源1.1 词典定义之外的隐性含义很多人第一次见到“impeccable”这个词&#xff0c;是在雅思作文或职场邮件的模板里。词典给的解释很简单&#xff1a;完美的、无可挑剔的、无懈可击的。但如果你只是在写作时把它当成“perfect…

作者头像 李华