ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

PARWAN处理器控制状态机:从架构图到VHDL后仿验证链

PARWAN处理器控制状态机:从架构图到VHDL后仿验证链

简介:面向高校软件工程、计算机等专业学生的FPGA数字系统设计大作业参考文档,聚焦PARWAN处理器架构与CONTROL-SECTION状态机的VHDL实现,可用于课程作业思路梳理、实验报告撰写与仿真结果对照。文档围绕PARWAN处理器的运算、控制、寻址三部分结构展开,逐项梳理PC、MAR、ALU、SR、IR与CONTROLLER等模块的职责分工,并给出控制段状态机的VHDL设计流程,涉及Spartan3E XC3S500E芯片选型、工程创建、测试文件编写及后仿结果记录,实验步骤与仿真截图相对完整。压缩包内共1个doc文件,约763KB,体量轻便,直接用Word即可打开查阅、摘录与二次整理。目前已有553人学习,适合刚接触FPGA开发、需完成大作业或复习数字系统设计流程的本科生参考。

1. 从一份课程大作业说起:PARWAN 处理器为什么值得拆

把一份 FPGA 开发课程大作业拆开看,最容易露怯的地方不是写不出 VHDL,而是代码能编译、仿真波形却跟架构图对不上号。这份材料围绕 PARWAN 处理器展开,前半段做架构综述,把运算、控制、寻址三段的数据通路和关键寄存器讲清楚;后半段落到 CONTROL-SECTION 状态机,给出可综合的 VHDL 代码、Test Bench 和一次完整的后仿记录。它适合正在做数字系统课程设计的人、需要快速回忆状态机标准写法的人,也适合把它当成“小而完整”的处理器控制单元样板来读的人。整份材料真正的价值不在现成答案,而在那条从架构图到信号表、从状态图到波形、从功能仿真到后仿真的链路。走通一遍,FPGA 开发里最常卡住的那几步基本就有谱了。

2. 拆 PARWAN 数据通路:运算、控制、寻址三段怎么划界

2.1 三段式分工:谁发命令、谁干活、谁搬数据

PARWAN 的结构图乍看箭头很乱,但按功能切一刀就清爽了:左边是运算区,中间是控制区,右边是寻址取数区。取址主要落在 PC 到 MAR 这一段,各种寻址方式可能需要地址计算,所以架构图里会看到从 PC 直接接到 ALU 的支路,算完再经 ADBUS 去读存储器,数据从 cache 出来进 ALU 做运算,SHU 负责移位,SR 记录状态标志,最后送进 IR 和 CONTROLLER,由控制器决定下一步该干什么。

把这条链路压成一句话就是:控制 → 取址/取数据 → 运算 → 再回到控制。这个闭环里,控制器是唯一“发号施令”的角色,其余部件都是被动接收使能信号。理解这一点很关键,因为它直接决定了后面状态机该怎么写:状态机的每个状态对应的是“这一拍谁被使能”,而不是“这一拍算出了什么值”。前者是控制逻辑,后者是数据通路逻辑,混在一起写,代码就会拧巴。

我见过不少人写这类作业时,习惯在状态机里顺手把 ALU 的运算结果也算一遍,写完之后仿真能跑,一综合资源暴涨,而且时序很难收敛。正确做法是在架构层面就把两件事分干净:数据通路负责“怎么算”,控制单元负责“什么时候算、算完往哪送”。这条边界画得越早,后面的代码越短。

2.2 寄存器与总线清单:把架构图上的箭头变成信号

从架构综述走到 VHDL 实现,中间必须补一张信号清单。架构图上的箭头是给人看的,VHDL 需要的是方向、位宽和驱动源。我一般的做法是先按“谁写谁、谁读谁”列一张表,宽度对不上的地方就是后面必然出问题的地方。为了统一,下面的表里寄存器位宽按 16 位写,实际以架构图标注为准,如果架构图是 8 位就整体替换,别混着用。

信号位宽驱动方去向出错时的典型现象
PC16控制器 pc_inc、分支逻辑MAR 输入、ALU 的 A 口取址地址错位,程序整体跑飞
MAR16控制器 mar_load地址总线 ADBUS读回的数据不是当前指令
IR16控制器 ir_load控制器译码、ALU 操作数选择操作码译错,行为完全对不上
ALU 输出16控制器 alu_op数据总线、寄存器堆写口结果整体偏移或位序反了
SR 标志若干位控制器 sr_load控制器条件判断分支条件跳转永远不成立或恒成立
控制器输出多位时钟与复位上述所有部件的使能端系统停在某个状态不动

这张表真正的作用不是抄进报告里交差,而是拿去做自查。你写完状态机后,把每个输出信号在表里找一遍:它的驱动源是不是只有状态机的输出进程?有没有第二个地方也在给同一个信号赋值?如果有,综合工具多半会给你一个多驱动警告,仿真时表现是波形在某一拍突然变成 X。这类问题在纯功能仿真里有时看不出来,上了后仿或者上板才炸,排查成本翻好几倍。

还有一类容易漏的是三态总线。早期的教学处理器架构常用双向 ADBUS,读的时候是输入,写的时候是输出。VHDL 里用inout端口配合'Z'能写,但对新手极不友好:一旦某个状态忘了把输出驱动成高阻,总线上就是两个源在打架。我一般会把双向总线拆成adbus_out、adbus_in、adbus_oe三个信号,在顶层用adbus <= adbus_out when adbus_oe = '1' else (others => 'Z');收口,仿真波形更干净,综合结果也更可控。

2.3 从架构图到信号表的五步整理法

第一步,先把架构图按左中右切成三块,用三种颜色把属于运算、控制、寻址的部件圈出来。这一步不做任何技术判断,只是减少视觉噪音。第二步,只盯着有寄存器符号的方块,把它们单独抄成一张列表,标注“谁写我、我写给谁”。第三步,把每条箭头翻译成一个信号名,命名规则固定成源_目的_含义,比如pc_mar_addr、alu_reg_result,避免后面出现tmp1、tmp2这种两周后自己都看不懂的名字。

第四步,逐个确认方向。凡是控制类信号,方向一定是控制器指向部件,也就是out;凡是状态标志,方向一定是部件指向控制器,也就是in。如果某条箭头你判断不出方向,说明架构图上那部分你还没读懂,回去补,别硬写。第五步,把清单里的每个信号跟状态机的状态对一遍:每个输出信号至少要被一个状态置为有效,每个输入信号至少要被一个状态读到,对不上的要么是多余信号,要么是漏写的状态。

这五步做完,通常能提前发现两三类问题:某个状态忘了给某个使能信号赋值(后面会综合出锁存器)、某个寄存器有写没读(说明状态设计缺了一环)、某个输入信号从来没被用到(多半是条件分支漏了)。这些在写代码之前发现,改起来只是改表格;等波形跑出来再回头找,就是一下午。

提示:信号清单建议直接写成注释贴在 VHDL 文件头部,后面改状态时对照着改,比翻架构图快得多。

3. CONTROL-SECTION 状态机:从状态图到可综合 VHDL

3.1 状态编码选型:二进制、独热、格雷的取舍

状态编码这件事,教学代码里往往一句type state_t is (S_IDLE, S_FETCH, ...)就带过去了,综合工具会自动选。但既然这份材料的目标是“能复现、能上板”,就得知道自动选择背后的取舍。二进制编码用的触发器最少,n 个状态需要 log2(n) 个触发器,代价是次态组合逻辑要解码多位,扇入偏大;独热编码每个状态一个触发器,次态逻辑退化成简单的与或,速度快但触发器浪费,状态多的时候资源吃不消;格雷编码相邻状态只变一位,适合状态按固定顺序循环、且对毛刺敏感的场合。

编码方式触发器用量次态逻辑复杂度适合场景
二进制最少较高,解码扇入大状态多、资源紧张、速度要求一般
独热等于状态数低,接近译码表FPGA 上状态少、追求时序余量
格雷同二进制低,但只对顺序迁移友好固定循环、异步跨时钟采样

CONTROL-SECTION 这类指令周期状态机,状态数通常不超过十个,而且状态之间的迁移基本是固定顺序,偶尔有分支。在 Spartan-3E 这种 4 输入 LUT 架构上,独热编码往往能让次态逻辑压在一两级 LUT 里,时序更稳。我在课程设计里一般先用默认编码把功能跑通,等要做时序报告时再切一次独热对比资源占用,两次综合报告放一起看,比空谈选型有说服力。

3.2 实体定义与端口命名约定

端口定义决定了这个模块能不能被别人复用。命名上我坚持两条:控制输出统一用“动作 + 动词”的形式,比如mar_load、ir_load、reg_write,一看就知道是高有效脉冲;状态类输入统一加_flag后缀,比如zero_flag,避免和多位宽数据混淆。位宽上,操作码用 4 位够表达十六种指令类型,ALU 操作码用 3 位够八种运算,这两个宽度按自己的指令集调,别照抄。

library ieee; use ieee.std_logic_1164.all; entity par_control_unit is port ( clk : in std_logic; -- 系统时钟,建议 50 MHz rst_n : in std_logic; -- 低有效异步复位 start : in std_logic; -- 启动一次指令周期,单拍脉冲 opcode : in std_logic_vector(3 downto 0); -- IR 送来的操作码 zero_flag : in std_logic; -- SR 送来的零标志 pc_inc : out std_logic; -- PC 自增使能 mar_load : out std_logic; -- PC -> MAR 锁存 mem_read : out std_logic; -- 存储器读使能 ir_load : out std_logic; -- 指令寄存器锁存 alu_op : out std_logic_vector(2 downto 0); -- ALU 运算选择 sr_load : out std_logic; -- 状态寄存器更新 reg_write : out std_logic; -- 通用寄存器写使能 busy : out std_logic; -- 指令周期进行中 done : out std_logic -- 停机指示 ); end entity par_control_unit;

这里的端口划分对应上一章的信号清单:所有out都是控制器发给各部件的使能,所有in都是部件反馈给控制器的状态。busy和done是给外部观察用的,仿真时直接盯这两个信号就能判断状态机在不在动,不用每次都去翻内部状态信号。start特意设计成单拍脉冲而不是电平,是为了避免上层一直拉高导致状态机反复重启,这个细节在测试平台里必须配合好,后面第 4 章会讲。

3.3 双进程写法:状态寄存器 + 次态逻辑 + 输出逻辑

状态机的写法流派不少,单进程、双进程、三进程都有人用。我的建议是把这个模块拆成三个进程:一个时序进程只负责状态寄存器,一个组合进程只算次态,一个组合进程只出输出信号。这么拆的好处是每一块职责单一,出问题时能立刻定位是“状态跳错了”还是“输出给错了”,而不是在一大坨if里找。

architecture rtl of par_control_unit is type state_t is (S_IDLE, S_FETCH, S_DECODE, S_EXEC, S_WB, S_HALT); signal cur_state : state_t; signal nxt_state : state_t; begin -- 进程 1:状态寄存器,异步复位,同步更新 p_state : process (clk, rst_n) begin if rst_n = '0' then cur_state <= S_IDLE; elsif rising_edge(clk) then cur_state <= nxt_state; end if; end process p_state; -- 进程 2:次态逻辑,纯组合,不产生任何寄存器 p_next : process (cur_state, start, opcode, zero_flag) begin nxt_state <= cur_state; -- 默认保持当前状态 case cur_state is when S_IDLE => if start = '1' then nxt_state <= S_FETCH; end if; when S_FETCH => nxt_state <= S_DECODE; -- 取指完成,进入译码 when S_DECODE => nxt_state <= S_EXEC; -- 译码一拍,操作码已稳定 when S_EXEC => if opcode = "1111" then -- 示例:1111 视为停机指令 nxt_state <= S_HALT; else nxt_state <= S_WB; -- 普通指令进入写回 end if; when S_WB => nxt_state <= S_IDLE; -- 一个周期结束,回到空闲 when S_HALT => nxt_state <= S_HALT; -- 停在停机态,等复位释放 when others => nxt_state <= S_IDLE; end case; end process p_next; -- 进程 3:输出逻辑,Moore 型,输出只与当前状态有关 p_out : process (cur_state) begin pc_inc <= '0'; -- 先全部给默认值,防止综合出锁存器 mar_load <= '0'; mem_read <= '0'; ir_load <= '0'; alu_op <= "000"; sr_load <= '0'; reg_write <= '0'; busy <= '1'; done <= '0'; case cur_state is when S_IDLE => busy <= '0'; when S_FETCH => mar_load <= '1'; -- 把 PC 送到 MAR mem_read <= '1'; -- 同时发起存储器读 ir_load <= '1'; -- 读回的数据打进 IR when S_DECODE => null; -- 译码阶段只等组合逻辑稳定 when S_EXEC => alu_op <= "001"; -- 执行阶段选择加法,按指令集调整 sr_load <= '1'; -- 运算结果更新状态标志 when S_WB => reg_write <= '1'; -- 写回通用寄存器 pc_inc <= '1'; -- 同时推进 PC when S_HALT => done <= '1'; busy <= '0'; when others => busy <= '0'; end case; end process p_out; end architecture rtl;

进程 1 里的异步复位写法是教学板卡的常见选择,按键复位拉低时立刻回S_IDLE,不用等时钟沿,上电行为确定。代价是复位信号本身需要做同步释放处理,否则在释放瞬间可能采到亚稳态,这一点在第 4 章的约束部分会补。

进程 2 开头那句nxt_state <= cur_state;是整段代码里最不起眼但最重要的一行。它保证每个分支只描述“什么条件下要去哪”,没有描述到的条件默认保持,不会因为漏写一个else就有了推断锁存器的机会。我在评审别人的状态机代码时,第一眼就看有没有这行默认赋值。

进程 3 的处理方式同理,进入case之前把九个输出全部赋默认值,case里只写需要置高的那几个。这样综合工具看到的是一张完整的真值表,不会因为某个分支缺赋值而推断出锁存器。S_DECODE分支里用null;是 VHDL 的合法空语句,明确表示“这一拍什么都不做”,比留空更清楚。

3.4 输出默认赋值的必要性

上一节的写法里有一个容易被忽略的细节:默认赋值和case分支的覆盖关系。假设某个状态你既没写alu_op,也没写reg_write的赋值,而进程开头的默认赋值又忘了写,综合工具就会认为这个信号在该状态下应该保持上一次的值,于是推断出一个锁存器。锁存器在 FPGA 里能用,但它对毛刺敏感、时序分析困难,而且几乎不是你想要的东西。ISE 综合报告里出现Latch inferred for signal ...这类警告,八成就是这个原因。

还有一种情况是输出依赖输入信号,也就是 Mealy 型。比如某个输出直接写成reg_write <= '1' when cur_state = S_WB and zero_flag = '1' else '0';,这种写法省状态但带来两个问题:输出会在同一拍内随输入变化,波形上看起来像毛刺;上板后如果输入来自异步源(比如按键),输出会跟着抖。我的习惯是除非面积实在不够,一律用 Moore 型,输出只跟状态走,需要条件判断的地方宁可多拆一个状态。

这条原则放到 PARWAN 这个场景里尤其合适,因为控制器管理的都是“这一拍启动哪个部件”这种一次性动作。动作本身不需要在同一拍内根据 ALU 结果变化,ALU 结果稳定了再进入下一个状态判断,逻辑更干净,也更容易跟波形对照。

4. 建工程、写 Test Bench、跑后仿:一条能复现的验证链

4.1 工程与器件设置:Spartan-3E XC3S500E FG320

打开 ISE,走File -> New Project,在弹出的对话框里填工程名和路径。路径里别带中文和空格,这一条是老生常谈,但每年都有人踩。点下一步进入器件选择页,开发板上的芯片型号是 Spartan-3E 系列的 XC3S500E,封装 FG320,速度等级按板卡丝印填。这几个参数选错的表现很典型:综合能过,实现阶段报引脚找不到,或者时序报告跟板卡完全对不上。

确认工程信息后点 Finish。接着加源文件,Project -> New Source,类型选VHDL Module,文件名par_control_unit,端口可以在这个向导里先填个大概,也可以直接跳过写代码。测试文件单独建,Project -> New Source里选左侧的VHDL Test Bench,右侧文件名填par_control_unit_tb,下一步会问你关联哪个源文件,选中par_control_unit,这样生成的模板会自动带上例化框架,省得手打端口映射。

时钟约束这一步容易被跳过。教学板卡上时钟通常是几十兆的晶振,不写周期约束,实现工具的时序分析就是空转。在 UCF 文件里加上时钟周期约束,语法如下,把周期数值改成你板卡的实际值:

# 时钟周期约束:20 ns 对应 50 MHz,按板卡实际晶振修改 NET "clk" TNM_NET = "clk"; TIMESPEC "TS_clk" = PERIOD "clk" 20 ns HIGH 50 %; # 复位信号不做周期约束,但要标记为异步,避免被当成数据路径分析 NET "rst_n" TIG;

第一段把clk网络标记成一个时序组并给出周期,实现工具据此计算每条路径的余量;第二段里TIG是“忽略时序”的意思,用在复位这种异步控制信号上,避免工具把复位路径也当成需要满足建立保持时间的数据路径来分析。这里不写具体的引脚位置约束,因为不同开发板的按键和晶振脚位不一样,照抄别人的 UCF 大概率上板不亮,脚位一律以自己板卡的原理图为准。

4.2 Test Bench 激励:复位、启动脉冲、断言

测试平台的价值不在于跑出波形,而在于让波形能自己说话。一个只有时钟和复位、跑完一百个周期什么也不检查的 test bench,本质上是把判断责任推给了肉眼看波形的人。我在课程设计里要求 test bench 至少包含三类内容:确定的复位时序、单拍启动脉冲、以及针对关键信号的断言。

library ieee; use ieee.std_logic_1164.all; entity par_control_unit_tb is end entity par_control_unit_tb; architecture sim of par_control_unit_tb is constant CLK_PERIOD : time := 20 ns; -- 50 MHz,与约束保持一致 signal clk : std_logic := '0'; signal rst_n : std_logic := '0'; signal start : std_logic := '0'; signal opcode : std_logic_vector(3 downto 0) := "0010"; signal zero_flag : std_logic := '0'; signal pc_inc, mar_load, mem_read, ir_load : std_logic; signal sr_load, reg_write, busy, done : std_logic; signal alu_op : std_logic_vector(2 downto 0); begin dut : entity work.par_control_unit port map ( clk => clk, rst_n => rst_n, start => start, opcode => opcode, zero_flag => zero_flag, pc_inc => pc_inc, mar_load => mar_load, mem_read => mem_read, ir_load => ir_load, alu_op => alu_op, sr_load => sr_load, reg_write => reg_write, busy => busy, done => done ); clk <= not clk after CLK_PERIOD / 2; -- 自由运行时钟 stim : process begin rst_n <= '0'; -- 上电先复位,至少保持 3 拍 opcode <= "0010"; wait for 3 * CLK_PERIOD; rst_n <= '1'; wait until rising_edge(clk); start <= '1'; -- 启动脉冲只打一拍 wait until rising_edge(clk); start <= '0'; -- 断言 1:普通指令必须走到写回,否则说明状态链断了 wait until reg_write = '1' for 20 * CLK_PERIOD; assert reg_write = '1' report "no write-back pulse: state machine stuck before S_WB" severity error; -- 断言 2:停机指令必须进入 HALT 并拉起 done opcode <= "1111"; wait until rising_edge(clk); start <= '1'; wait until rising_edge(clk); start <= '0'; wait until done = '1' for 20 * CLK_PERIOD; assert done = '1' report "HALT not reached, check opcode decode branch" severity error; wait for 4 * CLK_PERIOD; wait; -- 挂起,让仿真自然结束 end process stim; end architecture sim;

CLK_PERIOD设成 20 ns 是为了跟 UCF 里的约束对齐,仿真和实现的时序口径一致,后面看后仿波形时不会有时间尺度上的错觉。复位保持三个周期而不是一个,是给异步复位的释放留出余量,实际板卡上复位按键的机械抖动远不止三个周期,第 5 章会专门讲这个问题。

两处assert是这段 test bench 的核心。wait until reg_write = '1' for 20 * CLK_PERIOD;这种写法带超时,条件不满足时不会永久挂起,而是等到超时后继续往下走,紧接着的断言就会报错,并在 transcript 里打印出你写的那句话。仿真报错信息写清楚一点,半年后回来看还能立刻知道当初想验证什么。

start的处理也值得说一下。这里刻意做成单拍脉冲:拉高一拍后立刻拉低,保证状态机只被触发一次。如果测试平台图省事一直把start拉高,状态机从S_WB回到S_IDLE后会立刻再次启动,波形上看到的是连续不断的指令周期,很容易误判成状态机“自己循环了”。

4.3 功能仿真与后仿的差别在哪

功能仿真跑的是 RTL 代码,没有布线延迟,信号在同一时刻同时变化,波形干净得像教科书。后仿跑的是实现之后带延迟信息的网表模型,每个门、每根线都有延时,信号到达时间参差不齐,波形上会出现大量毛刺和建立时间不足导致的 X 态。这两者不是“做一遍就行”的关系,是两道不同的关卡。

对比项功能仿真后仿(布局布线后)
仿真对象RTL 源码带 SDF 延时的网表
延迟信息无,零延迟门延时 + 线延时
仿真速度快慢,规模大时明显
主要用途验证逻辑功能、状态迁移验证时序收敛、检查竞争冒险
典型异常状态跳错、输出漏赋值建立时间违例、窄脉冲被吃掉

在 ISE 里的操作路径是:先跑完Implement Design,然后在 Processes 面板里找到Generate Post-Place & Route Simulation Model,生成网表模型和对应的 SDF 文件;接着切到Simulate Post-Place & Route Model启动仿真。如果这一步波形全是红杠,先别急着改代码,八成是模型没生成或者 SDF 没加载上,第 5 章会展开。

什么时候必须做后仿?我自己的判断标准是:只要设计里有异步输入直接进状态机、或者有跨时钟域的信号,就必须跑一次后仿。PARWAN 这类教学处理器通常单时钟,逻辑简单,功能仿真过关后上板大概率能跑;但只要加了按键输入、或者板卡上用了不同源的时钟,后仿就能提前把那些“仿真好、上板死”的问题暴露出来。

4.4 波形上必须盯住的四个观测点

第一个是cur_state,把状态信号拖到波形窗口最上面,改成枚举显示,一眼就能看出状态迁移对不对。第二个是start到S_FETCH的延迟,正常情况下start有效后的下一个时钟沿状态就应该跳到S_FETCH,如果隔了好几拍才动,说明次态逻辑里有额外的条件卡住了。

第三个是各个使能输出的脉冲宽度,mar_load、ir_load、reg_write这些都应该是单拍脉冲。如果在波形上看到某个信号连续好几拍都是高,要么是状态设计里重复写了,要么是输出逻辑漏了默认赋值。第四个是busy和done的配合关系,busy从S_FETCH开始拉高,到S_WB或S_HALT结束,done只在S_HALT拉高,两者不应该同时为高。

注意:观测点固定下来之后,把波形窗口的布局存成.wcfg文件一起放进工程目录。下次打开工程直接加载,不用再一个个拖信号,这个习惯能省掉大量重复劳动。

5. 避坑记录:五个让波形和综合结果翻车的问题

5.1 复位、锁存器与按键抖动

现象一:仿真一开始,cur_state在波形上是一段红色的 U,过了好几个周期才变成S_IDLE。原因是状态信号声明时没给初值,而测试平台又没在时间零点把rst_n拉低,VHDL 仿真器对未初始化的枚举类型默认就是'U'。解决办法是在 test bench 里把rst_n的初值直接写成'0',并在时间零点保持至少三个时钟周期。RTL 里也可以给cur_state一个初值做兜底,但真正可靠的还是复位,别指望仿真器的默认行为,更别指望综合工具会把初值映射到 FPGA 的上电状态。

现象二:综合报告里出现Latch inferred for signal alu_op,功能仿真却完全正常。原因是组合进程里有分支没覆盖到某个输出,综合工具只能推断成锁存器来保持上次的值。仿真器不做这种推断,所以功能仿真是看不出来的。解决办法是把输出进程改成“先给全部默认值,再进 case”的结构,也就是第 3 章里那段代码的写法。改完重新综合,警告消失,顺便看一眼资源报告里的 LUT 数量是不是降了,通常会有惊喜。

现象三:上板之后按一下启动按键,状态机跑飞或者连续跑好几轮。仿真是好的,代码也没错,问题出在按键本身。机械按键按下和松开时会有几毫秒的抖动,直接接到状态机的start端,等于给了一串随机脉冲。解决办法是加一个按键消抖模块,用计数器把输入信号稳定若干个时钟周期后再输出,或者干脆用单脉冲展宽电路,保证一次按下只产生一个宽度确定的脉冲。教学板卡上这个问题特别常见,因为很多人习惯把按键直接接到时钟或者使能上,仿真环境里按键是理想信号,永远不会抖。

5.2 后仿模型与仿真精度

现象四:切到后仿之后,波形全是 X,或者所有信号一条直线不动。原因通常是网表模型没生成成功,或者仿真时没有加载对应的 SDF 延时文件。ISE 里生成模型和启动仿真之间有依赖关系,必须先在 Processes 面板里把Generate Post-Place & Route Simulation Model跑完,等它变绿,再去启动后仿。如果确认都跑了还是全 X,检查一下仿真器设置里 SDF 的路径指向是不是当前工程目录,换过工程路径的情况尤其容易出错。

现象五:同一份代码在别人的机器上仿真结果一样,在你的机器上波形不刷新,或者时间轴对不上。原因是不同仿真器的默认时间精度不同,1 ns和1 ps精度下,同一段wait for语句的实际行为会有细微差别;另外工程目录里残留的旧编译产物也会导致增量编译结果和源码不一致。解决办法是统一在 test bench 里显式声明时间精度,并养成“改代码前先清一次工程产物”的习惯。这一条属于血泪经验,一个残留的旧网表能让人怀疑人生半天。

提示:每次换机器或者换工程路径后,先跑一遍干净的功能仿真,确认基础波形正常,再去折腾后仿,能少走很多弯路。

6. 把这份作业用出余量:状态覆盖率自检与两个可延伸方向

代码跑通只说明写出来的路径是对的,没写出来的路径对不对,得靠覆盖率说话。我的习惯是把手写状态机的每个状态列成一张表,逐个标注“进入条件”和“退出条件”,然后对照 test bench 看每个状态是不是都被至少一组激励覆盖过。PARWAN 这个控制器只有六个状态,一张表就能列完。

状态进入条件退出条件是否被激励覆盖
S_IDLE复位释放start 有效是
S_FETCHstart 有效固定单拍是
S_DECODE取指完成固定单拍是
S_EXEC译码完成操作码判定是
S_WB非停机指令固定单拍是
S_HALT操作码为 1111仅复位可退出是

这张表的最后一列不要急着填“是”,先问自己:S_HALT之后如果不复位,状态机会不会自己跳出来?如果代码里写了nxt_state <= S_HALT;这一行,答案是不会,那测试平台里就必须有一次“停在 HALT 若干周期再复位”的激励,否则这条路径其实没被验证过。同样的道理适用于zero_flag这种条件输入,它在当前设计里没被用到,但如果你的指令集里有条件跳转,就必须构造出zero_flag为 0 和为 1 两组激励,否则分支跳转的代码等于没测。

如果想在这个基础上再往前走一步,有两个方向值得试。一个是把 Moore 型输出改成带条件的分支,也就是让状态机在某些状态下根据zero_flag直接决定下一拍去哪个状态,而不是多拆一个状态,这样能在控制器里塞进简单的条件跳转,更接近真实处理器的行为。另一个是把状态机的状态输出引到板卡的数码管或者 LED 上,用肉眼观察状态迁移,这种“物理波形”对理解时序的帮助比盯着仿真窗口大得多,尤其是刚开始接触数字设计的时候。

我现在每次写状态机,不管多简单,都强制走一遍“信号清单 → 状态表 → 默认赋值 → 断言覆盖”这四步,哪怕只是改一行次态逻辑。吃过一次亏:改完忘了更新 test bench 里的超时时间,仿真跑了二十分钟以为卡死,结果是断言在后台早就报了错,transcript 被刷过去了。从那以后我每次跑仿真第一件事就是先翻 transcript 末尾,确认没有severity error再去开波形窗口。希望这些经验能帮到你。

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

返回列表