
简介FPGA数字系统设计课程大作业文档面向计算机、软件工程等专业修读数字系统设计的学生聚焦PARWAN处理器架构与CONTROL-SECTION状态机的VHDL设计及后仿。内容按运算、控制、寻址三部分梳理处理器数据通路说明PC、MAR、ALU、SR、IR与CONTROLLER的职责并给出Spartan3E XC3S500E平台上的工程创建、测试文件编写、状态机实现与仿真结果可作为课程作业参考和复习数字系统流程的实操样本。压缩包共1个doc文件约763KB结构单一、便于直接查阅。目前已有554人学习适合需要理清PARWAN处理器控制流程、对照VHDL代码与后仿波形完成报告撰写或查漏补缺的读者。1. 从一份大作业文档说起PARWAN 处理器与 FPGA 数字系统设计到底在做什么翻到这份文档的第三页会发现一个挺反直觉的现象整份大作业里最费时间的不是写 VHDL 代码而是把 PARWAN 处理器结构图上那些密密麻麻的控制线一根一根对应到状态机的某个状态上。这份资源就是围绕这件事展开的 FPGA 开发记录——把一个教学用的经典处理器拆成「运算段、控制段、寻址段」三块然后挑出 CONTROL-SECTION 状态机用 VHDL 重新实现一遍再跑后仿真验证波形。它适合两类人一类是刚上手数字系统设计、需要照着走通「建工程→写状态机→搭 Test Bench→看后仿波形」这条完整链路的学生另一类是想找个小而完整的例子复习有限状态机在硬件里到底该怎么写的老手。整份材料的价值不在结论在过程可复现。2. 架构拆解PARWAN 的三段式数据通路与控制/寻址/运算分工PARWAN 这个名字听起来唬人结构其实很朴素。它是一台单总线的教学处理器所有部件挂在同一组数据总线和地址总线上同一时刻只允许一个部件驱动总线。理解它的关键不是记住每个寄存器的名字而是先接受一个约束总线是共享资源所以每个动作必须分拍完成。这条约束直接决定了后面状态机要有多少个状态。2.1 三段式划分运算段、控制段、寻址段各自管什么原文对三段的描述很精炼——左边运算中间控制右边寻址取数。展开来说运算段是 ALU、SHU移位单元、SR状态寄存器这一组。ALU 负责加减与逻辑运算SHU 负责左移右移SR 记录运算产生的标志位零标志、进位、符号位。这三者的输出都会回流到总线或者控制段供后续判断。控制段是 IR指令寄存器加 CONTROLLER。IR 保存当前指令的操作码和操作数字段CONTROLLER 就是这份大作业要设计的那个状态机。它读 IR 的操作码决定接下来该发哪几根控制线是整个处理器的「决策中心」。寻址段是 PC程序计数器、MAR内存地址寄存器、MDR内存数据寄存器加上地址总线 ADBUS。取指令时 PC 的值送进 MARMAR 驱动地址总线内存返回的数据落到 MDR最后进 IR。三段之间的耦合点只有一个控制段发出的控制信号。这也是为什么大作业要求「针对 CONTROL-SECTION 的某一状态给出 VHDL 设计和后仿结果」——把这个决策中心拆开看整台机器的行为就透明了。常见的做法是把这三段画成一张带箭头的框图箭头方向就是数据流方向控制段往下发控制信号寻址段和运算段往上回报数据。画图这一步别偷懒后面写 VHDL 端口列表时端口的名字和方向基本就是照抄这张图。2.2 取址链路PC→MAR→ADBUS→MDR→IR 这条线怎么走取址这条链是理解 PARWAN 时序的最好入口因为它把「共享总线」的代价暴露得最彻底。一条指令的取指至少要拆成三拍第一拍PC 的值送上总线MAR 锁存。这一拍里 PC 是驱动方MAR 是接收方其他所有可能驱动总线的部件必须处于高阻态否则总线冲突。第二拍MAR 驱动 ADBUS 发起读请求内存把数据放到数据总线MDR 锁存。这一拍 PC 不参与但 PC 需要准备自增为下一次取指做准备。第三拍MDR 的值送上总线IR 锁存。同时 PC 完成加一。这一拍结束后指令才算真正进入了控制段状态机可以开始译码。如果把这三拍压成一拍硬件上直接的表现就是总线冲突综合工具会报多驱动仿真波形上是一片 X。这种「必须分拍」的思维方式是软件背景的人转硬件时最需要建立的习惯。我在写这段的时候习惯先列一张微操作表把每一拍谁驱动、谁锁存、发哪几根控制线写清楚再翻译成 VHDL 的状态。表列完了状态机的骨架也就有了剩下的只是语法。2.3 控制信号表把结构图上的每根线落到信号名和有效电平大作业原文里提到控制段信号要按「施加对象、类别、信号名、功能」分类这是很实用的组织方式。落到代码里我一般会先定一张这样的表再写实体声明信号名施加对象功能有效电平pc_oePC把 PC 值送上内部总线高pc_incPCPC 自增指向下一条指令高mar_oeMARMAR 锁存总线上的地址高mdr_oeMDRMDR 把数据送上总线高mem_rd存储器发起读周期高mem_wr存储器发起写周期高ir_oeIRIR 锁存总线上的指令高alu_op[2:0]ALU选择运算类型编码sr_weSR写状态寄存器高shu_op[1:0]SHU选择移位方向与模式编码这张表最重要的两列是「施加对象」和「有效电平」。前者决定了这根线归哪个进程管后者决定了复位时该把它拉成什么值。我见过太多人把复位值写成0结果那个信号恰好是低有效一上电就误触发。并行地把所有控制线声明成一个记录类型record在端口和内部信号之间传递会清爽很多但要注意老版本综合工具对 record 端口的支持参差不齐保守起见还是平铺成单独信号更稳。3. 工程落地Spartan3E 上从建工程到跑通后仿这一章是纯操作层面的事。原文给的步骤是建工程、选芯片、加 Test Bench、跑仿真顺序没错但每一步都有几个参数不写清楚就会在后面翻车。3.1 工程创建XC3S500E-FG320 的选型与顶层文件组织芯片选型这一步原文写得很明确Spartan3E 系列的 XC3S500EFG320 封装。这两个信息缺一不可因为同一个型号不同封装引脚编号完全不同UCF 约束文件没法通用。工程建好之后我一般会按下面这个结构组织文件而不是把所有东西堆在顶层parwan_ctrl/ ├── src/ │ ├── par_control_unit.vhd -- 控制段状态机本次作业主体 │ ├── par_datapath.vhd -- 运算段 寻址段的简化模型 │ └── par_top.vhd -- 顶层只做例化和连线 ├── sim/ │ └── par_control_unit_tb.vhd -- 只测控制段的单元 Test Bench ├── constr/ │ └── par_top.ucf -- 引脚与时钟约束 └── par_top.xise -- 工程文件分成三层的好处是仿真粒度可控。调状态机逻辑时只跑par_control_unit_tb几秒钟出结果等状态机稳了再上顶层跑整体避免每次改一行都要等整个数据通路过一遍。工程属性里有一项容易被忽略综合工具的 VHDL 标准。默认可能是 VHDL-93如果你想用process(all)这种简化敏感列表的写法得手动切到 VHDL-2008。版本不匹配时的报错信息很含糊通常只说语法错误不告诉你标准不对这是新手最容易卡住的地方之一。3.2 Test Bench给状态机喂一段能覆盖所有分支的激励Test Bench 不是把时钟一接就完事激励要能逼着状态机走完每一条分支。原文只说了新建 VHDL Test Bench 文件、命名par_control_unit_tb、关联源文件这里补上激励的实际写法-- 时钟 20 ns 周期对应开发板 50 MHz 晶振 constant CLK_PERIOD : time : 20 ns; -- 被测设计例化 uut : entity work.par_control_unit port map ( clk clk, rst rst, ir_op ir_op, -- 指令操作码由 TB 驱动 alu_zero alu_zero, -- ALU 零标志用于条件跳转分支 pc_oe pc_oe, mar_oe mar_oe, mem_rd mem_rd, ir_oe ir_oe ); -- 激励主流程 stim_proc : process begin rst 1; -- 先复位两个周期把状态机摁回 S_RESET ir_op (others 0); wait for CLK_PERIOD * 2; rst 0; ir_op OP_LOAD; -- 走算术类分支 wait for CLK_PERIOD * 6; ir_op OP_STORE; -- 走访存类分支 wait for CLK_PERIOD * 6; ir_op OP_JZ; alu_zero 1; -- 置零标志逼状态机走条件跳转 wait for CLK_PERIOD * 6; assert false report simulation finished severity note; wait; end process;这段激励的逻辑说明复位拉高两个周期是为了让S_RESET状态至少被采到一次否则有些综合结果里状态机会停在一个未定义编码上波形全是 U。之后依次切换ir_op走三条不同的执行分支每条分支停留 6 个周期足够摩尔型状态机完成「进入执行态→发控制线→回取指态」的完整循环。最后用assert false ... severity note主动结束仿真比手动设结束时间可靠。参数上唯一要调的是每个分支的等待周期数。如果状态机在执行态停留的拍数更多就把 6 改大改小了会出现「还没走完就被切走」的假象波形上看起来像状态机漏跳实际是激励给早了。3.3 后仿真波形从状态名而不是状态编码读结果后仿真Post-Route Simulation和功能仿真最大的差别是状态编码被综合工具重新分配过了。你代码里写的是S_FETCH_0、S_FETCH_1波形里看到的是000、011这种二进制。很多人第一次跑后仿对着波形一脸懵就是因为还在按源码里的顺序去数状态。常见做法是在波形窗口里把状态信号加一个「 radix 转换」或者手动建一个编码对照表逐段核对。更省事的做法是让综合工具保留状态名的映射信息在波形工具里直接按符号显示但这依赖工具链版本不一定都能用。后仿真正要盯的是三件事一是状态跳转顺序跟功能仿真是否一致二是每根控制线的翻转时刻是否对得上时钟沿三是没有出现意料之外的毛刺。毛刺在后仿里特别容易出现因为它带上了布线延迟功能仿真里看不见。如果某个组合输出的毛刺刚好被下游的时钟沿采到那就是一个潜伏的时序 bug功能仿真永远查不出来。后仿的运行时间通常是功能仿真的几十倍别拿它做日常调试只在功能仿真全绿之后跑一次做最终确认就够了。4. CONTROL-SECTION 状态机VHDL 逐段实现到了核心部分。控制段状态机是整个大作业的主体也是最能体现硬件思维的地方。原文只列了实验步骤没给完整代码结构这里按我实际写下来最稳的三进程结构展开——一个时序进程管状态寄存器一个组合进程算次态一个组合进程做输出译码。4.1 状态枚举与端口声明先定状态。状态划分的原则是「一拍一个状态」凡是需要占用一个时钟周期才能完成的微操作就单独给一个状态不要试图在一个状态里干两件事-- 控制段状态机的状态枚举 -- 按取指 / 译码 / 执行 / 写回四段切分每个状态对应一拍微操作 type ctrl_state_t is ( S_RESET, -- 复位态所有控制线置无效 S_FETCH_0, -- PC 送总线MAR 锁存 S_FETCH_1, -- MAR 发起读MDR 锁存 S_FETCH_2, -- MDR 送总线IR 锁存PC 自增 S_DECODE, -- 译码等 IR 稳定一拍 S_EXEC_ARITH, -- 算术类指令驱动 ALU S_EXEC_MEM, -- 访存类指令驱动 MAR / 读写线 S_WB -- 写回更新 SR 或目标寄存器 ); signal cur_state, nxt_state : ctrl_state_t;状态设计里有几个取舍值得说清楚。S_DECODE单独占一拍看起来浪费但它保证了 IR 在进入执行态之前已经稳定避免组合逻辑在指令字段还没锁存好时就抢跑。S_EXEC_ARITH和S_EXEC_MEM分开而不是合并成一个执行态是因为这两类指令发出的控制线完全不同合并之后输出译码里要再套一层 if反而不如拆开清晰。枚举类型在综合后会被编码成二进制编码方式由工具决定可能是顺序编码、格雷码或者独热码。写代码时不要依赖具体的编码值任何地方需要判断状态就用枚举名这样换工具、换综合选项都不用改逻辑。端口方向的划分遵循一条原则凡是状态机要驱动外部的都是输出凡是状态机要读来判断的都是输入。输入侧最关键的是ir_op操作码和alu_zero零标志条件跳转要用。4.2 主状态机时序进程 组合进程的双进程写法状态机主体拆成两个进程一个只管触发器一个只管查表职责不混-- 进程 1时序进程只负责状态寄存器在时钟沿翻转 p_state_reg : process(clk, rst) begin if rst 1 then cur_state S_RESET; elsif rising_edge(clk) then cur_state nxt_state; end if; end process; -- 进程 2组合进程纯查表算次态 p_next_state : process(cur_state, ir_op, alu_zero) begin nxt_state cur_state; -- 默认保持防止综合出锁存器 case cur_state is when S_RESET nxt_state S_FETCH_0; when S_FETCH_0 nxt_state S_FETCH_1; when S_FETCH_1 nxt_state S_FETCH_2; when S_FETCH_2 nxt_state S_DECODE; when S_DECODE -- 按操作码分流访存类走 S_EXEC_MEM其余走算术 if ir_op OP_LOAD or ir_op OP_STORE then nxt_state S_EXEC_MEM; else nxt_state S_EXEC_ARITH; end if; when S_EXEC_ARITH -- 条件跳转零标志有效才真的跳否则直接写回 if ir_op OP_JZ and alu_zero 1 then nxt_state S_FETCH_0; else nxt_state S_WB; end if; when S_EXEC_MEM nxt_state S_WB; when S_WB nxt_state S_FETCH_0; -- 写回完成回到取指形成闭环 when others nxt_state S_RESET; -- 兜底防止非法编码把状态机锁死 end case; end process;逻辑说明进程 1 只有两个分支——异步复位和时钟上升沿这是最标准的触发器模板综合工具能直接映射到一个寄存器组。注意复位写的是异步复位如果板上复位信号是同步的这里要改成在rising_edge内部判断否则会出现复位释放时刻的亚稳态。进程 2 的第一行nxt_state cur_state是整个代码里最关键的一行。它保证了在case没有覆盖到的分支上次态默认等于现态而不是保持上一次的赋值从而避免综合出锁存器。所有if都配了else所有case都配了when others这是组合进程的两条铁律。参数上要调的是S_EXEC_ARITH里条件跳转的处理。我把它写成「满足条件直接回取指」跳过了写回态因为跳转指令本身没有结果要写回。如果你的指令集里跳转还要更新某些标志那这里就得改成先进S_WB再回取指。4.3 输出译码把微操作绑定到控制线上第三个进程负责把当前状态翻译成具体控制线。写法上有个固定套路先把所有输出拉到无效电平再在case里挑需要置位的拉高-- 进程 3输出译码摩尔型输出只跟当前状态有关 p_output : process(cur_state) begin -- 第一步全部拉无效避免上一拍的残留值影响本拍 pc_oe 0; pc_inc 0; mar_oe 0; mdr_oe 0; mem_rd 0; mem_wr 0; ir_oe 0; sr_we 0; alu_op ALU_PASS; -- 第二步按状态置位需要的那几根 case cur_state is when S_FETCH_0 pc_oe 1; -- PC 上总线 mar_oe 1; -- MAR 锁存地址 when S_FETCH_1 mem_rd 1; -- 发起读周期 when S_FETCH_2 mdr_oe 1; -- MDR 上总线 ir_oe 1; -- IR 锁存指令 pc_inc 1; -- PC 指向下一条 when S_EXEC_ARITH alu_op ALU_ADD; -- 这里按操作码细分更合适 when S_EXEC_MEM mar_oe 1; mem_wr 1; when S_WB sr_we 1; -- 更新状态寄存器 when others null; -- 其余状态保持全无效 end case; end process;逻辑说明这个进程是纯组合逻辑敏感列表里只有cur_state说明输出只跟当前状态有关跟输入无关——这就是摩尔型状态机。摩尔型的优点是输出稳定、没有组合毛刺沿输入传播缺点是输出比状态晚一个传播延迟后仿时要留意。参数说明alu_op在S_EXEC_ARITH里被写死成ALU_ADD是个简化真实实现应该在这里再读一次ir_op做二级译码把加减、逻辑与或、移位都分出来。但要注意一旦在输出进程里引入ir_op敏感列表就得同步加上否则功能仿真对、后仿挂这是第 5 章要专门讲的一个坑。5. 避坑与排查ISE 时代和状态机设计里的典型翻车这一章集中说几个我实际踩过的坑现象、原因、解决办法都写清楚遇到了对着查。5.1 波形一片 X 或 U复位路径没走通现象功能仿真跑起来状态信号和控制线全是 X偶尔是 U时钟倒是有正常翻转。原因九成是复位没生效。要么 Test Bench 里rst信号声明了但没给初值VHDL 里std_logic的默认值是U不是0要么状态机的复位分支写成了同步复位而激励给的复位脉冲太短没赶上时钟沿。解决Test Bench 里给rst声明初值: 1并在主流程开头显式拉高至少两个时钟周期同时检查状态机复位是异步还是同步异步复位的第一行应该是if rst 1 then不能嵌在rising_edge里面。5.2 功能仿真通过后仿真反而挂掉现象Behavioral Simulation 波形完全正确切到 Post-Route Simulation 之后某些控制线的翻转时刻对不上或者状态跳转紊乱。原因两个高发点。一是组合进程里漏了默认赋值综合出锁存器后仿时锁存器的透明期把毛刺传了下去二是敏感列表不全VHDL-93 不会自动推导漏掉的信号在功能仿真里恰好没变化所以看不出来后仿加了延迟就暴露了。解决组合进程第一行永远写默认赋值敏感列表要么手写全要么切到 VHDL-2008 用process(all)。综合报告里搜一下latch关键字有就直接回去改别抱侥幸。5.3 状态少跳一拍或者多跳一拍现象波形上状态序列看着是对的但每个状态停留的时间比预期少一拍或者多一拍导致控制线宽度不对。原因多半是采样时机的问题。Test Bench 在时钟上升沿的同一时刻采样而状态机也在上升沿翻转两者存在竞争波形工具显示的是竞争后的结果看起来就像少了一拍。另外摩尔型输出本来就比状态晚一个传播延迟如果你拿输出信号去数拍数会天然差一拍。解决Test Bench 改成在时钟下降沿采样错开半个周期肉眼看到的就是稳定值判断时序关系时以状态信号为准不要拿输出信号当时间基准。5.4 综合通过、约束也加了下载到板子上没反应现象工程综合实现全绿灯比特流也生成了下载进去之后板上没有任何可见变化。原因UCF 约束文件的问题占绝大多数。要么时钟引脚没约束工具随便分配了一个引脚晶振信号根本进不来要么 IO 电平标准没写默认标准和板上电压不匹配。另外复位按键如果是低有效代码里却按高有效写一上电就处于常复位状态。解决UCF 里逐条核对时钟引脚编号、IOSTANDARD写成板上实际的电平标准复位信号的有效极性在实体声明处加注释标清楚别靠记忆。5.5 编译报「找不到 work 库里的设计单元」现象仿真工具编译 Test Bench 时报错说例化的实体在 work 库里找不到。原因编译顺序反了。Test Bench 例化了设计单元必须先编译设计单元再编译 TB否则例化语句解析不了。有些工具会自动排依赖有些不会。解决手动控制编译顺序先par_control_unit.vhd再par_top.vhd最后par_control_unit_tb.vhd。工程文件里调整文件顺序也能解决但换工具时不一定保留养成按依赖顺序手动编译的习惯更保险。6. 二次开发把这份作业改成能讲清原理的完整 CPU 模型这份材料的骨架已经完整但从「能跑通」到「能讲清」中间还差几步改造。我自己重做时是按下面这个顺序推进的每一步都有明确的验证手段改造点具体做法验证方法控制段硬布线改微程序把case里的次态表抽出来存进 ROM状态机退化成取微指令的循环改一条微指令内容看波形是否只影响对应那一拍补全数据通路把par_datapath.vhd从简化模型扩成带寄存器堆的版本跑一段连续指令检查寄存器值累加是否正确加指令跟踪用 UART 在每个S_WB把 PC 和结果打出去串口终端能打印出指令执行序列上板验证把跟踪输出接到板上 LED 或数码管肉眼能看出程序在跑而不是只看仿真微程序化那一步最值得做因为它是从「写死状态机」到「可编程控制器」的分水岭。做完之后你会发现改动一条指令不再需要改 VHDL 重新综合只需要改 ROM 初始化文件迭代速度是数量级的差别。加指令跟踪这条链路看起来跟核心逻辑无关但它解决了一个很实际的问题仿真波形只能看几百纳秒程序稍微长一点就看不完了。有了串口输出程序跑多久都能留下痕迹出错时直接定位到哪条指令、哪个 PC 值。最后说个我自己的教训。早些年做这类状态机我习惯功能仿真波形一顺就直接综合上板觉得后仿太慢没必要。结果有一次板上跑出来的行为和仿真完全对不上回头补跑后仿才发现是某个组合输出在布线延迟下产生了窄脉冲刚好被下一级时钟沿采到功能仿真里这个脉冲根本不存在。从那以后我每次改完状态机的输出译码都强制走一遍「功能仿真→看综合报告有没有 latch→后仿确认时序」这三步一步不跳。慢是慢点但比在板子上抓瞎强。希望帮到你。本文还有配套的精品资源点击获取