简介:一份面向高校软件工程、电子信息类专业学生的FPGA数字系统设计课程大作业文档,对应院选课综合考核场景,适合正在学习fpga开发、需要完成PARWAN处理器相关作业或复盘数字系统设计流程的读者参考。文档围绕PARWAN处理器的运算、控制与寻址三部分架构展开,梳理PC、MAR、ALU、SR、IR与CONTROLLER的协作关系,并给出CONTROL-SECTION状态机的VHDL设计思路。包内共1个doc文件,约763KB,内容覆盖工程创建与Spartan3E XC3S500E芯片选型、VHDL测试文件编写、状态机代码设计、后仿结果与仿真截图,以及实验步骤和总结,可作为课程报告的写作模板与实现参照。已有553人学习,适合用来对照检查状态划分、信号控制和仿真验证是否完整。
1. PARWAN 处理器 + FPGA 大作业:这份文档卡在什么地方
很多人拿到这份 FPGA 数字系统设计大作业文档,第一反应是照着“3.1 创建工程”把 ISE 打开,然后卡在“状态机到底该写几个状态”这一步,因为文档给出的是综述、流程和一张后仿截图,真正的 VHDL 骨架得自己补。它更像一份半成品的对照样本:前半部分把 PARWAN 处理器的运算段、控制段、寻址段拆开讲清楚了;后半部分记录了从建工程、选 Spartan-3E XC3S500E 器件、生成 Test Bench 到跑通 CONTROL-SECTION 状态机仿真的完整路径。适合正在做 FPGA 开发课程设计、需要交一份能跑起来的状态机代码的人,也适合想重新熟悉 VHDL 手感、复习有限状态机三段式写法的熟手。
2. 架构先看明白:从 PC/MAR 到 ALU 的 PARWAN 数据通路
PARWAN 是个教学味儿很浓的处理器,指令周期串行执行,控制器是硬布线状态机而不是微程序,模块少但五脏俱全。原文那句“左边运算、中间控制、右边寻址”其实已经把整张结构图讲完了,真正需要补的是每个方框之间靠什么信号、在哪个时钟拍上握手。如果这一层没理顺,后面写状态机就是凭感觉敲 case,波形一出来全是问号。
2.1 三段式结构:运算、控制、寻址各自管什么
寻址段的核心是 PC 和 MAR。PC 存的是“下一条要取的指令地址”,MAR 存的是“当前这一次访问内存用的地址”,两者容易混淆。取指节拍里,PC 的值先打到 MAR,同时 PC 自增,等存储器把数据吐回总线,指令字才落到 IR。地址侧的加法逻辑一般挂在 PC 前面,用于相对寻址和跳转目标计算,这也是原文提到“应对各种寻址方式,也可能需要计算”的落点。
运算段是 ALU、SHU(移位器)、ACC(累加器)和 SR(状态寄存器)的组合。ALU 负责加减与逻辑运算,SHU 负责移位,ACC 承接结果,SR 记录零标志、进位这类状态位。注意 SR 的写使能要和 ALU 的运算同拍,否则条件跳转读到的就是上一轮的陈旧标志,这类时序错位在仿真里非常隐蔽。
控制段由 IR 和 CONTROLLER 组成。IR 锁存指令字,把操作码和操作数地址拆开送出去;CONTROLLER 是状态机本体,它根据当前状态和 opcode、zero 这些条件,产出下一拍该拉高哪些控制信号。原文里的“控制 -> 取址/取数据 -> 运算 -> 控制”这个大回环,本质就是状态机的状态转移图,把它画成圆圈箭头,代码基本就出来了。
| 段位 | 主要部件 | 职责 | 典型输出信号 |
|---|---|---|---|
| 寻址段 | PC、MAR、地址加法器 | 产生访存地址、维护指令序 | pc_inc、pc_load、mar_load |
| 运算段 | ALU、SHU、ACC、SR | 执行运算、暂存结果与标志 | alu_op、acc_we、sr_we |
| 控制段 | IR、CONTROLLER | 锁存指令、产生全部使能 | ir_load、mem_read、done |
2.2 控制段信号清单:把 Applied to / categories 那张表翻译成端口
原文里给了“Inputs and outputs of PARWAN control sections”,按 Applied to、categories、signal name、functions 四列组织。这张表的价值不在抄,而在它是端口的直接来源:Applied to 决定信号该连到哪个模块,category 决定它属于寄存器写使能、存储器控制、ALU 操作码还是总线驱动,function 决定它的有效电平和高电平持续几拍。我一般按“目的地分组”重排这张表,写 entity 的时候一组一组往下排,比按字母序好维护得多。
library ieee; use ieee.std_logic_1164.all; entity par_control_unit is port ( clk : in std_logic; -- 系统时钟 rst : in std_logic; -- 高有效同步复位 opcode : in std_logic_vector(3 downto 0); -- IR 高 4 位 zero : in std_logic; -- SR 的零标志 -- 寻址段 pc_inc : out std_logic; -- PC 自增 pc_load : 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 操作码 acc_we : out std_logic; -- 累加器写使能 sr_we : out std_logic; -- 状态寄存器写使能 done : out std_logic -- 复位态指示,可接 LED ); end entity par_control_unit;这段声明里没有时钟分频、没有总线三态,是刻意做薄的:控制器的职责只是把状态翻译成使能,数据通路上的多路选择和三态驱动放在上层去连。opcode的口宽取 4,是因为 PARWAN 教学版的指令字里操作码只占高 4 位,剩下的位拿去做地址或立即数;alu_op取 3 位,够编 8 种运算,实际只用了前 5 种。端口方向全部按“控制器输出使能、外部模块接收”来定,这样控制器和数据通路之间是单向的,综合出来的扇出也干净。
2.3 一条 LD 指令走完:从 PC 到 ALU 的时序链路
拿一条最普通的取数指令 LD 走一遍,能看清五个状态是怎么来的。T0 复位态,所有使能归零,done拉高表示系统就绪;T1 取指拍,mar_load和mem_read同时有效,PC 的值进 MAR,存储器开始读,同拍pc_inc把 PC 加一,保证下条指令地址不会丢;T2 译码拍,存储器数据已经稳定在总线上,ir_load拉高把指令字锁进 IR,外部逻辑把 IR 的高 4 位送到opcode端口。
T3 执行拍,控制器按 opcode 选中alu_op。如果是 ADD 就出加法码,同时acc_we和sr_we一起拉高,结果和标志在同一拍落地;如果是 LD,ALU 走直通(PASS),真正的写回要留到 T4。T4 写回拍,对 LD 来说acc_we有效,把存储器取回的数据写进 ACC;对 ST 来说这一拍拉的是写使能。执行完回 T1 继续取下一条。
T0 RESET : done=1 T1 FETCH : mar_load=1 mem_read=1 pc_inc=1 T2 DECODE : ir_load=1 T3 EXEC : alu_op=ADD/SUB/AND acc_we=1 sr_we=1 T4 WB : LD -> acc_we=1 ;ST -> mem_write=1这张拍位表就是状态机的设计说明。它解释了两件事:为什么状态数取 5 而不是随便取 3 或 8,以及为什么运算类指令可以跳过 T4 直接回 T1。原文结果部分只留了仿真截图没留状态图,很多人就在这里凭感觉编状态,最后综合出来的时序对不上,波形里 acc 老是慢一拍,翻车的根源基本都在这里。
3. 从零建工程:器件选型、Test Bench 和约束文件
文档里“3.1 创建工程”那三条步骤是对的,但只写了点哪里,没写为什么这么点。器件选型填错一个下拉框,后面所有引脚约束都会失效,报错还特别绕。这一章按实际动手顺序走一遍:先建工程、再搭 Test Bench、最后补约束和仿真脚本,把“能出波形”当作阶段性目标。
3.1 新建工程与 XC3S500E / FG320 的选型细节
ISE 的 New Project 对话框里有四个下拉框要填:Family、Device、Package、Speed。板上芯片是 Spartan3E 系列的 XC3S500E,封装 FG320,速度等级一般是 -4,按板子丝印或原理图上的型号核对,别凭记忆填。Family 选 Spartan3E、Device 选 XC3S500E、Package 选 FG320 之后,Speed 下拉框会自动收敛到该器件可用的等级,选错了后面 Generate Programming File 会直接报“器件与工程设置不匹配”。
Top-level source type 选 HDL,Simulator 按你装的工具选 ISim 或 ModelSim。这两项在建工程时是写进 .xise 的,后面再改要走 Project 属性,容易漏。工程路径别带中文和空格,老版本 ISE 对这两样特别敏感,路径里有中文时综合能过、仿真加载设计时却报找不到库,这类问题排查起来相当费时间。
# ISE 命令行建工程(批处理时用),路径和器件参数按实际改 project new par_control_unit.xise project set family Spartan3E project set device xc3s500e project set package fg320 project set speed -4 project set "Top-Level Source Type" HDL project set "Simulator" "ISim"这几行对应的就是对话框里的五个选项,好处是可以进版本库、可复现。project set的键名在不同 ISE 版本上略有差异,13.x 和 14.x 基本一致;如果某一行报 unknown property,用project set不带值先跑一遍,把属性列表打出来对着改。工程建完先别急着写代码,右键工程选 Properties 确认一遍器件参数,这一步花三十秒,能省掉后面半小时的报错排查。
3.2 Test Bench 的两种生成方式与手动补全
原文给的做法是 Project -> New Source -> VHDL Test Bench,填文件名 par_control_unit_tb,再选关联的源文件,工具会自动生成一个带实例化和空激励进程的模板。这个模板的好处是端口映射不用手抄,坏处是激励进程里只有一句wait;,时钟也没有,直接跑会一直停在 0 时刻。另一种做法是完全手写,适合你已经清楚要测哪几个状态。
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 : std_logic := '1'; signal opcode : std_logic_vector(3 downto 0) := "0000"; signal zero : std_logic := '0'; signal pc_inc : std_logic; signal pc_load : std_logic; signal mar_load : std_logic; signal mem_read : std_logic; signal ir_load : std_logic; signal acc_we : std_logic; signal sr_we : std_logic; signal alu_op : std_logic_vector(2 downto 0); signal done : std_logic; begin clk <= not clk after CLK_PERIOD / 2; -- 自由运行时钟 u_dut : entity work.par_control_unit port map ( clk => clk, rst => rst, opcode => opcode, zero => zero, pc_inc => pc_inc, pc_load => pc_load, mar_load => mar_load, mem_read => mem_read, ir_load => ir_load, acc_we => acc_we, sr_we => sr_we, alu_op => alu_op, done => done ); p_stim : process begin rst <= '1'; -- 先复位三拍,让状态机落到 S_RESET wait for 3 * CLK_PERIOD; rst <= '0'; opcode <= "0001"; -- LD:应看到 T3 直通、T4 写 ACC wait for 5 * CLK_PERIOD; opcode <= "0011"; -- ADD:T3 出加法码 + 双写使能 wait for 5 * CLK_PERIOD; opcode <= "0110"; -- JC:zero=1 时应拉 pc_load zero <= '1'; wait for 5 * CLK_PERIOD; opcode <= "1111"; -- HALT:回复位态,done 拉高 wait for 3 * CLK_PERIOD; assert false report "simulation done" severity failure; end process; end architecture sim;CLK_PERIOD定 20 ns 是为了和板上 50 MHz 晶振对齐,前仿阶段这个值不影响功能,但后仿带 SDF 延迟时它会直接决定时序是否收敛,所以一开始就按真实频率写,别图省事写成 100 ns。u_dut用的是直接实例化(entity work.xxx),不用 component 声明,省一层样板代码;VHDL-93 下这么写完全合法。
激励进程里每一段wait for 5 * CLK_PERIOD都对应一次完整的指令周期,这样波形上能明显看出状态机的循环节奏。最后那句assert false ... severity failure是给仿真器一个硬停信号,ModelSim 和 ISim 遇到 failure 都会终止并保留波形,否则仿真会一直跑到默认的超时时间,白等几分钟。zero信号单独抽出来控制,是为了验证条件跳转分支,别把它固定成常量,不然后面想测跳转还得改一遍。
3.3 引脚约束与仿真脚本:让波形一次出来
上板需要 UCF 约束,前仿不需要,但建议在建工程时就一起写,免得综合完了才发现引脚没分配、布局布线报一堆 warning。引脚号必须对照手上板子的原理图,下面只是格式示例,不同版本的开发板定义不一样,抄之前务必核对。
# 引脚号按实际板子原理图改,这里只演示写法 NET "clk" LOC = "C9" | IOSTANDARD = LVCMOS33; # 晶振输入 NET "rst" LOC = "D18" | IOSTANDARD = LVCMOS33; # 按键复位 NET "done" LOC = "K17" | IOSTANDARD = LVCMOS33; # 接 LED 观察只约束时钟、复位和观察用的done三根线,opcode、zero这些输入在板子上没有物理按键,硬约束反而给自己找麻烦。UCF 的语法注意末尾分号,|两边可以有空格,但LOC和IOSTANDARD的顺序不能反,写反了 Translate 阶段会直接报语法错。
跑仿真用一个 do 脚本,比在图形界面里一层层点快得多:
# ModelSim 批处理:编译 -> 加载 -> 加波形 -> 跑 300 ns vlib work vcom -93 -work work ../src/par_control_unit.vhd vcom -93 -work work ./par_control_unit_tb.vhd vsim -t 1ns work.par_control_unit_tb add wave -radix binary /par_control_unit_tb/clk add wave /par_control_unit_tb/rst add wave -radix binary /par_control_unit_tb/opcode add wave -radix binary /par_control_unit_tb/alu_op add wave /par_control_unit_tb/done add wave -label "STATE" /par_control_unit_tb/u_dut/cur_state run 300 ns-93明确用 VHDL-93 编译,ISE 工程默认就是这个标准,混用 2008 会出现枚举类型显示成整数的怪现象。-t 1ns设仿真时间精度,后仿带 SDF 时精度太粗会把小延迟抹掉,导致前仿后仿结果对不上。add wave里路径/par_control_unit_tb/u_dut/cur_state是层次化引用,cur_state是枚举类型,ModelSim 会直接显示 S_RESET、S_FETCH 这些字面量,比看二进制码直观得多。最后run 300 ns是按指令周期估算的,四条指令各占 5 拍、每拍 20 ns,刚好跑完还留了余量。
跑后仿的流程是:综合、实现、Generate Post-Place & Route Simulation Model,工具会吐出 netsim 目录下的网表 VHDL 和 .sdf 文件,再在 do 脚本里加一句vsim -sdfmax /par_control_unit_tb/u_dut=../netgen/par_control_unit_timesim.sdf。这一步最容易出错的地方是 SDF 路径写错,路径不对时不报错、只是默默不做时序反标,波形看起来和功能仿真一模一样,很容易误以为后仿通过了。
4. CONTROL-SECTION 状态机:两段式 VHDL 怎么写才不锁存
原文只说要给 CONTROL-SECTION 状态机写 VHDL,没给代码。这一章把完整实现摊开讲,包括状态怎么定、进程怎么拆、默认赋值为什么必须写。用两段式而不是一段式,是因为一段式把状态寄存、次态判断、输出译码全塞进一个进程,出了问题时很难定位到底是状态跳错还是输出译错;拆成两段后,波形上能分别看到当前状态和输出,排查效率差一个量级。
4.1 先定状态和编码,别急着敲 case
状态数量取决于指令周期的拆分粒度。前面那张拍位表已经给了答案:S_RESET、S_FETCH、S_DECODE、S_EXEC、S_WB,五个状态覆盖一条串行执行的指令。很多同学喜欢一上来给每条指令单独开状态(LD_S1、LD_S2、ADD_S1……),指令一多状态爆炸,改一条指令要动半个文件,这是典型的拿来主义后遗症。
type state_t is (S_RESET, S_FETCH, S_DECODE, S_EXEC, S_WB); signal cur_state, nxt_state : state_t; -- 指令操作码,与 IR 高 4 位对齐 constant OP_NOP : std_logic_vector(3 downto 0) := "0000"; constant OP_LD : std_logic_vector(3 downto 0) := "0001"; constant OP_ST : std_logic_vector(3 downto 0) := "0010"; constant OP_ADD : std_logic_vector(3 downto 0) := "0011"; constant OP_SUB : std_logic_vector(3 downto 0) := "0100"; constant OP_AND : std_logic_vector(3 downto 0) := "0101"; constant OP_JC : std_logic_vector(3 downto 0) := "0110"; constant OP_HALT : std_logic_vector(3 downto 0) := "1111"; -- ALU 操作码 constant ALU_PASS : std_logic_vector(2 downto 0) := "000"; constant ALU_ADD : std_logic_vector(2 downto 0) := "001"; constant ALU_SUB : std_logic_vector(2 downto 0) := "010"; constant ALU_AND : std_logic_vector(2 downto 0) := "011";枚举类型让综合工具自由选编码,默认会给 one-hot 或 gray,对状态数在 10 以内的设计两者差别不大,不必手动指定enum_encoding。操作码和 ALU 码用 constant 定义,好处是后面对照波形时看到的是名字不是魔数;乘法和除法这类本设计不支持的运算干脆不定义,避免写了常量没人用、综合报 unused constant 的警告。
sensitivity list里该放什么也有讲究。次态进程依赖当前状态、opcode 和 zero,三个都要列全;VHDL-93 没有process(all),漏一个信号在仿真里会表现为“条件变了但状态不跳”,而且综合前不报错,属于最难查的一类问题。
4.2 同步时序段:只做一件事,current <= next
第一段进程只负责把次态打进当前态,除此之外什么都不干。这条纪律听起来很教条,但它是避免锁存器和多驱动的基础。
architecture rtl of par_control_unit is -- 状态、常量的声明放在这里 begin -- 第 1 段:同步时序,只负责状态寄存 p_seq : process(clk) begin if rising_edge(clk) then if rst = '1' then cur_state <= S_RESET; -- 同步复位,和时钟沿对齐 else cur_state <= nxt_state; end if; end if; end process;复位放在rising_edge内部,是同步复位写法。异步复位写成if rst = '1' then ... elsif rising_edge(clk),两者综合出来的电路不同:同步复位省一个触发器端口,但要求复位信号宽度大于一个时钟周期;异步复位响应快,但要额外处理复位释放时的亚稳态。教学板上的按键抖动明显,用同步复位配合外部消抖更稳,仿真时也更容易对齐节拍。
这段进程里没有else分支遗漏的问题,因为 if 两条路径都赋了值,不会推出生成锁存器。如果写成if rst = '1' then cur_state <= S_RESET; end if;,少了一个 else,综合工具就会认为“其他情况下保持原值”,在时序进程里这是触发器行为没问题,但在组合进程里就是锁存器,很多新手把这段和下面那段搞混,锁存器警告就是这么来的。
4.3 组合次态段与输出译码段:默认赋值 + case 全覆盖
第二段算次态,第三段算输出,两段都用组合逻辑。组合进程的纪律比时序段更严:进程开头必须给所有输出赋默认值,case 必须写when others。
-- 第 2 段:组合次态逻辑 p_next : process(cur_state, opcode, zero) begin nxt_state <= cur_state; -- 默认保持,防止意外锁存 case cur_state is when S_RESET => nxt_state <= S_FETCH; when S_FETCH => nxt_state <= S_DECODE; when S_DECODE => if opcode = OP_HALT then nxt_state <= S_RESET; -- 停机后回复位态 else nxt_state <= S_EXEC; end if; when S_EXEC => if opcode = OP_LD or opcode = OP_ST then nxt_state <= S_WB; -- 访存类多一拍写回 else nxt_state <= S_FETCH; -- 运算类直接回取指 end if; when S_WB => nxt_state <= S_FETCH; when others => nxt_state <= S_RESET; end case; end process;zero在这个进程里没被用到,但列进敏感表不会报错,只是多一次触发。真正用到它的是下面输出段的条件跳转。次态逻辑里把 HALT 单独拎出来判断,是因为它在译码拍之后就该终止,不该进执行拍;如果统一进 S_EXEC 再判断,波形上会多出一拍无效执行,在时序严格的场合就是 bug。
-- 第 3 段:组合输出译码 p_out : process(cur_state, opcode, zero) begin -- 所有输出先给安全默认值,这一句是防锁存器的关键 pc_inc <= '0'; pc_load <= '0'; mar_load <= '0'; mem_read <= '0'; ir_load <= '0'; acc_we <= '0'; sr_we <= '0'; done <= '0'; alu_op <= ALU_PASS; case cur_state is when S_FETCH => mar_load <= '1'; -- PC 值打入 MAR mem_read <= '1'; -- 发起存储器读 pc_inc <= '1'; -- PC 指向下一条 when S_DECODE => ir_load <= '1'; -- 锁存指令字 when S_EXEC => if opcode = OP_JC and zero = '1' then pc_load <= '1'; -- 条件成立,目标地址装入 PC end if; case opcode is when OP_ADD => alu_op <= ALU_ADD; acc_we <= '1'; sr_we <= '1'; when OP_SUB => alu_op <= ALU_SUB; acc_we <= '1'; sr_we <= '1'; when OP_AND => alu_op <= ALU_AND; acc_we <= '1'; sr_we <= '1'; when others => alu_op <= ALU_PASS; end case; when S_WB => if opcode = OP_LD then acc_we <= '1'; -- 取数写回 ACC elsif opcode = OP_ST then mem_read <= '0'; -- 写使能由上层总线控制器接管 end if; when others => done <= '1'; -- 复位态点亮 LED end case; end process; end architecture rtl;进程开头那九行默认赋值是整个设计里最关键的一处。去掉它们,pc_load只在 S_EXEC 的某个条件分支里被赋值,其他状态下综合工具不知道它该是什么电平,只能推一个锁存器出来维持原值。锁存器在 FPGA 里能实现但不是好习惯:它让静态时序分析变复杂,跨时钟域时容易出毛刺,而且综合报告里的 “latch inferred” 警告一堆人当成耳旁风,后面时序不收敛了才回头翻。我一般把默认值写成一组连续的赋零,视觉上就能看出哪些信号真的被驱动过。
S_EXEC 里的嵌套 case 只处理运算类指令,when others => alu_op <= ALU_PASS兜住 LD、ST、JC、HALT 这些不需要 ALU 动手的情况,让它们走直通。写通(直通)而不是不赋值的区别在于:不赋值就走默认值,同样能出 PASS,但可读性差;显式写出来,后面加指令时不容易漏。
4.4 接上数据通路:控制信号怎么打到 ALU / MAR / IR
控制器单独跑起来只是波形好看,要验证它真的有用,得有最简数据通路配合。我的做法是在顶层例化控制器、一个简化 ALU、一个行为级存储器模型和几个寄存器,用信号把控制器的输出接到各模块的使能端。存储器模型用 VHDL 的数组加进程实现,读写都按时钟同步,前仿足够。
-- 顶层连线示意:控制器输出直接当使能,不做额外加工 u_ctrl : entity work.par_control_unit port map ( clk => clk, rst => rst, opcode => ir(15 downto 12), zero => sr_zero, pc_inc => pc_inc, pc_load => pc_load, mar_load => mar_load, mem_read => mem_read, ir_load => ir_load, acc_we => acc_we, sr_we => sr_we, alu_op => alu_op, done => done ); u_alu : entity work.simple_alu port map ( a => acc, b => mem_data, op => alu_op, y => alu_y, zero => alu_zero );ir(15 downto 12)直接把 IR 高四位切给控制器,切法要和指令格式定义严格一致,位宽对不上综合会报类型不匹配,仿真里则可能静默截断,波形上 opcode 永远是一个值,很容易被误判成状态机卡死。zero接 SR 的零标志,标志位的更新节拍由sr_we控制,所以 ADD 指令的 T3 拍里acc_we和sr_we必须同时有效,否则下一条 JC 读到的还是上上条指令的标志。这处细节在纯控制器仿真里看不出来,必须连上 ALU 和 SR 才暴露,这也是为什么建议至少搭一个最简数据通路再跑一遍。
5. 避坑与排查:仿真波形一片 X 时先查这几处
状态机代码不长,但出问题的姿势特别多,而且症状相似。下面五条是我自己在这个设计上真实遇到过的,按“现象 -> 原因 -> 解决”记,遇到问题按顺序对一遍,命中率挺高。
5.1 五条踩坑记录
第一条:波形里 cur_state 全是 X,一条线都看不出来。原因是枚举类型信号没给初值,复位又没在 0 时刻生效。Test Bench 里rst声明成'1'只是给了初值,如果进程里第一句是rst <= '0'再等几拍,状态机在复位到来之前就是未定义。解决办法是让激励进程第一句就拉高复位并保持至少三个时钟周期,别依赖信号声明时的默认值。
第二条:状态停在 S_FETCH 不动,之后再也不跳。十有八九是次态进程的敏感表漏了opcode。仿真开始时 opcode 是 0,跟 S_DECODE 里比较永远不相等,状态机就卡在译码前一步。另一种可能是时钟写成了clk'event and clk = '1'和rising_edge(clk)混用,综合没问题,但仿真在某些工具上会漏沿。统一用rising_edge,敏感表列全,这条能避开。
第三条:综合报告一堆 “latch inferred”。组合进程里某个输出不是所有分支都赋值,或者 case 少写了when others。VHDL 的 case 在枚举类型上是要求全覆盖的,漏一个状态综合直接报错;但 std_logic_vector 上的 case 必须手写 others,漏了就锁存。最省事的解法就是进程开头统一赋默认值,别指望综合工具猜。
第四条:后仿结果和前仿不一致,波形差一两拍。先看 SDF 有没有真的反标上去。ModelSim 加载时如果路径写错,只在 transcript 里留一行 warning,波形照样出,但所有延迟都是 0,看起来和前仿一样。确认办法是看vsim输出里有没有 “SDF annotation completed” 字样,或者故意把时钟周期调到临界值,前仿能过、后仿挂,才说明延迟生效了。
第五条:工程综合通过,上板 LED 不亮。多半是引脚约束没生效或者约束错了封装。FG320 和 CP132 的引脚编号完全不一样,器件选错封装后 Translate 阶段会报引脚不存在,但如果 UCF 里只约束了三根线、其余引脚被工具自动分配,也能过,只是 LED 接在了一个你没想到的脚上。养成习惯:实现完成后打开 Pinout 报告,把 clk、rst、done 三个脚的位置和原理图对一遍。
5.2 上板前的自检顺序
顺序错了会浪费大量时间在错误的方向上。我的固定流程是:先跑纯控制器前仿,看状态跳转对不对;再接上简化数据通路跑一次,看 ACC 和 SR 的更新节拍;然后生成 bit 流做时序分析,看 fmax 和建立保持余量;最后才上板。
时序分析那一步别只看 “Timing constraints are met” 这一句。ISE 的静态时序报告里要重点看最差路径是不是跨在控制器输出上——如果alu_op这条组合路径的延迟接近时钟周期,说明输出译码的逻辑级数太深,后面加指令时会先在这里出问题。留 20% 以上的余量,比事后加流水线省事得多。
提示:上板调试时先把
done接到 LED,复位态常亮说明时钟和复位都到了;再考虑把cur_state编码后接几个 LED,能直接看出状态机在不在跑。这招比逻辑分析仪快,成本也低。
6. 二次开发:把单状态 Demo 扩成完整控制器的三步改法
这份文档给出的东西,本质上是一个能跑通的状态机骨架。想拿它继续往下做,有三步可以走。
第一步是把 opcode 译码从 case 里抽出来,换成常量表或记录类型。现在每条指令都在 S_EXEC 里占一个分支,加到十几条指令时进程会长得没法看。改成type micro_instr_t is record alu_op : std_logic_vector(2 downto 0); acc_we : std_logic; ... end record;,用一个常量数组按 opcode 索引,译码段只剩一行查表,加指令就是往数组里加一项,不动逻辑。
第二步是把硬布线输出译码换成微指令 ROM。状态和条件拼成地址,ROM 输出就是整个控制字,改指令集只需要重新生成 ROM 内容,不用碰 VHDL 结构。教学版的 PARWAN 用硬布线是为了讲清楚状态机,实际工程里微指令的可维护性更好。
-- 微指令 ROM 的查表写法示意 signal cw : std_logic_vector(15 downto 0); constant UROM : rom_array := ( -- state & opcode 拼地址,内容是控制字 0 => x"0001", -- RESET: done=1 1 => x"0032", -- FETCH: mar_load + mem_read + pc_inc 2 => x"0040" -- DECODE: ir_load ); cw <= UROM(to_integer(unsigned(state_code & opcode)));第三步是输出加一级寄存器,改三段式。组合输出在状态切换的瞬间会有毛刺,如果下游是敏感的异步逻辑就会误触发;打一拍之后输出和时钟沿对齐,代价是多一个周期延迟。验证方法很直接:改完之后把前仿和后仿的波形并排摆着看,重点对比状态切换那一刻的输出,毛刺消失了才算改对。
我吃过一次亏:输出没打拍,直接在 board 上调,功能时好时坏,示波器上看使能线上全是振铃,查了两天才定位到是组合译码的竞争。从那以后,凡是控制信号要跨出模块边界的,我都强制加一级输出寄存器再走线。希望帮到你。
本文还有配套的精品资源,点击获取