ARTICLE DETAIL

资讯详情

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

VHDL三段式状态机实战:PARWAN控制单元仿真与排错

VHDL三段式状态机实战:PARWAN控制单元仿真与排错

简介:围绕PARWAN处理器架构与CONTROL-SECTION状态机展开的FPGA数字系统设计课程大作业文档,面向电子信息、计算机与软件工程专业修读FPGA开发课程的学生,可用于课程作业参考、综合设计复盘及VHDL状态机入门练习。文档以PARWAN处理器的运算、控制、寻址三部分结构为主线,说明PC、MAR、ALU、SR、IR与CONTROLLER的职责和数据通路,并给出CONTROL-SECTION状态机的VHDL设计过程:在Spartan3E XC3S500E(FG320封装)上新建工程、编写par_control_unit_tb测试文件、编译综合并完成后仿真,配合状态跳转说明与仿真波形截图,便于理解取址、运算各阶段控制信号的变化。压缩包内为1个doc文件,约763KB,按目的、原理、步骤、结果、总结五部分组织,接近课程报告模板,可借鉴写作框架与实验记录方式。已有554人学习。

1. 仿真波形一直不动,问题出在状态机而不是板子

仿真跑了两分半,波形窗口里cur_state死活停在S_FETCH_MAR不动,改了三遍代码才发现是次态逻辑漏了一个分支。这是拆这份 FPGA 开发大作业资料时遇到的第一个坎。整份资源围绕 PARWAN 处理器展开,前半程讲架构综述,后半程要求用 VHDL 把 CONTROL-SECTION 状态机设计出来,给出功能仿真和后仿波形。它不大,但整条链路都串联起来了:数据通路、控制信号、有限状态机、测试平台,最后再做一个时序仿真。适合两类人:一类是刚上手 FPGA、想找一个能完整走通的练手项目;另一类是组合逻辑写得挺熟、但从没独立写过一版状态机的开发者。文档里的信号表和代码骨架,基本可以直接拿来当模板改。

2. PARWAN 架构拆解:控制、寻址、运算这三条链路怎么串联

PARWAN 是一个典型的教学型处理器,结构上按照「控制、寻址取数、运算」三块来划分,比 8086 那种庞大的指令系统清爽得多。理解它的顺序应该是先看数据往哪走,再看谁在指挥数据走,最后看指挥信号在什么时刻有效。这三件事想清楚了再看代码,VHDL 就只是把这些关系一行行翻译出来而已。

2.1 三部分结构:运算部分、控制部分、寻址部分

左侧是运算部分,核心是 ALU 和移位器 SHU,旁边放置状态寄存器 SR,用来保存零标志、进位标志这类结果信息。中间是控制部分,由 IR(指令寄存器)和 CONTROLLER(控制单元)组成,控制单元负责决定下一步该做什么。右侧是寻址取数部分,主要由 PC(程序计数器)和 MAR(存储器地址寄存器)承担,PC 保存当前指令地址,MAR 保存当前要访问的内存地址,中间通过地址总线 ADBUS 把地址送给存储体,取回的数据再进入运算部件。

从数据通路的角度描述就是:PC 的值送到 MAR,MAR 通过 ADBUS 发起读请求,存储体返回的数据落到 MBR(存储器缓冲寄存器),再从 MBR 进入 IR 完成取指。执行阶段 ALU 从寄存器堆或 MBR 取操作数,运算结果经过 SHU 移位后写回寄存器堆,同时更新 SR。整个环路可以概括成「控制、取址取数据、运算、回到控制」这四步的循环。

这种结构的好处是职责边界清楚。写控制状态机时,你只需要关心「当前状态应该把哪个 load 信号拉高」,完全不用管 ALU 内部是怎么做加减的。反过来,写数据通路时也不用操心状态怎么跳。后面要改代码,这个边界就是最重要的分层依据。

2.2 控制段的输入输出清单:一张表看清它指挥谁

控制单元的端口设计是整个实验的关键。输入端接收当前的「处境信息」,输出端发出「动作指令」。输入一般只有时钟、复位、操作码,以及来自 SR 的条件标志;输出则会很多,每个控制信号都要单独一根线。

方向信号名位宽作用
输入clk1系统时钟,状态迁移的节拍源
输入rst_n1低电平异步复位,强制回到取指首态
输入opcode4来自 IR 的操作码,决定译码分支
输入z_flag1SR 的零标志,条件跳转要用
输出pc_inc1PC 自增,取指完成后加一
输出pc_load1PC 装载,跳转指令时用
输出mar_load1把 PC 或计算结果打进 MAR
输出mem_read1存储器读使能
输出mem_write1存储器写使能
输出mbr_load1读回的数据打入 MBR
输出ir_load1MBR 内容打入 IR,取指完成
输出reg_write1寄存器堆写使能,写回阶段拉高
输出alu_op3ALU 运算类型选择
输出shu_op2移位器模式选择
输出sr_load1更新状态寄存器
输出halt1停机标志

这张表我一般是先画在纸上再写代码的,因为 VHDL 的 entity 一旦漏了某个信号,后面 test bench 里想观测都没得观。

提示:输出信号的数量决定了控制段是 Moore 型还是 Mealy 型。Moore 型的输出只跟当前状态有关,波形干净好调;Mealy 型输出还跟 opcode 有关,能省状态但波形上容易出毛刺。教学项目建议一律用 Moore 型。

2.3 一条指令的节拍走查:从 PC 到写回

把状态机拆成状态之前,先在纸面上走一遍时序。以一条简单的加法指令为例,节拍是这样的:

第一个时钟沿到来时,进入送地址状态,mar_load拉高,PC 的当前值被打进 MAR。第二个节拍进入读存储器状态,mem_read和mbr_load同时有效,存储体在时钟沿前把数据准备好,沿到来时数据落进 MBR。第三个节拍ir_load有效,指令进入 IR,同时pc_inc拉高把 PC 加一,为下一条指令做准备。

第四个节拍是译码,此时opcode已经稳定,状态机根据它决定后面走哪条分支。第五个节拍执行,alu_op根据操作码给出运算类型,结果在时钟沿后稳定,同时sr_load更新状态标志。第六个节拍写回,reg_write拉高,结果写入目的寄存器,然后无条件回到第一个状态。

这套节拍是固定的,每一条指令都至少消耗这几个周期。真正的差异在译码之后:有的指令要多一拍去算有效地址,有的指令要判断条件标志来决定是否跳转。状态机的复杂度就长在这里,也正是在这里,从「一个状态」扩展到「全部状态」才是这份作业真正的训练价值。

3. 从建工程到出波形:一次完整的仿真流程

很多人卡住的不是代码,是工具。ISE 这套老工具的交互逻辑跟现在的 Vivado 差别挺大,第一次点错一个选项,后面仿真就一直提示找不到顶层。这一章把从空白工程到波形窗口弹出来的每一步都走一遍,照着做基本不会翻车。

3.1 新建工程与器件选型:为什么是 XC3S500E-FG320

打开 ISE,从 File 菜单进入 New Project,先填工程名和存放路径。路径这一项要注意,不要放在带中文或空格的目录下,ISE 对这类路径的兼容性一直不太好,编译到一半报「文件无法写入」的情况我见过不止一次。

下一步是器件配置。开发板用的是 Spartan-3E 系列,具体型号 XC3S500E,封装 FG320,速度等级通常是 -4。这三个参数必须跟板子丝印上一一对上,选错了综合能过,但引脚约束会全乱,最后生成不了能用的比特流。

Family : Spartan3E Device : XC3S500E Package : FG320 Speed Grade : -4 Synthesis Tool: XST (VHDL/Verilog) Simulator : ISim 或 ModelSim-SE

工程建好之后是新建源文件。这里有个容易忽略的点:控制状态机和测试平台要分成两个文件,主设计放par_control_unit.vhd,测试平台放par_control_unit_tb.vhd。放在同一个文件里也能编译,但综合的时候测试平台会被当成顶层一起综合,最后报一堆「信号无负载」的警告,白折腾。

3.2 用 VHDL Test Bench 模板搭一个仿真骨架

写测试平台不要从空白文件开始。从 Project 菜单进入 New Source,左侧选 VHDL Test Bench,右侧填文件名par_control_unit_tb,下一步会让你选择关联的设计文件。选中刚才写的控制单元,ISE 会自动帮你生成端口映射和被测模块的实例化代码,连时钟常量都给你写好了,这部分省下来的时间足够你多调两遍波形。

生成的骨架里通常包含一个CLK_PERIOD常量和一个clk <= not clk after CLK_PERIOD/2;的语句,前者改成 20 ns 就是 50 MHz,跟板子的晶振对得上。复位信号在激励进程里先拉低若干个周期再释放,这个「若干个」不能太短,至少要覆盖两个时钟沿,否则状态机可能还没复位完就开始跑了。

如果用的是 ModelSim,比起在图形界面里一层层点,我更推荐写一个 do 脚本,一次跑完编译、加载、加波形、运行。脚本不占地方,改一行参数就能重跑,比每次重新点菜单舒服太多。

# sim.do —— 一键完成编译、加载与波形观察 vlib work vcom -93 ../src/par_control_unit.vhd vcom -93 ../sim/par_control_unit_tb.vhd vsim -t 1ns work.par_control_unit_tb # 状态信号单独一组,方便肉眼追踪迁移 add wave -divider "STATE" add wave -radix unsigned /par_control_unit_tb/dut/cur_state add wave -radix unsigned /par_control_unit_tb/dut/nxt_state # 控制信号按数据通路顺序排列 add wave -divider "FETCH" add wave /par_control_unit_tb/dut/mar_load add wave /par_control_unit_tb/dut/mem_read add wave /par_control_unit_tb/dut/mbr_load add wave /par_control_unit_tb/dut/ir_load add wave /par_control_unit_tb/dut/pc_inc add wave -divider "EXEC" add wave /par_control_unit_tb/dut/alu_op add wave /par_control_unit_tb/dut/sr_load add wave /par_control_unit_tb/dut/reg_write run 2 us

脚本里-t 1ns是把仿真精度设成 1 纳秒,比默认的皮秒级跑得快不少,教学项目这个精度完全够用。vcom -93指定 VHDL-93 标准,如果你的代码里用了process(all)这类 2008 语法,就得换成-2008,否则编译直接报语法错误。add wave -radix unsigned是让状态枚举以数字形式显示,比一串符号名更容易看出跳转顺序。

3.3 功能仿真与后仿真的差别,波形该看哪几条

功能仿真跑的是行为级代码,没有延迟信息,波形上信号是理想跳变的。这一步的目标只有一个:验证状态迁移逻辑对不对。具体看三件事——复位后是不是落在S_FETCH_MAR、每个状态的停留周期数是否符合设计、halt拉高之后是不是真的不再迁移。

后仿真是把综合、布局布线之后的网表拿来再跑一遍,并且反标 SDF 时序文件。这时候波形上会出现延迟,有些信号甚至会因为布线延迟出现窄毛刺。这一步验证的是「门级实现和你的意图一致」,重点看建立时间。在 ISE 里生成 SDF 的路径是:先跑完 Implement Design,然后在工程目录下找*_timesim.sdf,把它和网表文件一起加载进仿真器。

两个仿真的波形对比着看,如果功能仿真对、后仿不对,问题大概率在时序约束上,比如时钟周期约束写得太紧,或者某个组合路径太长。如果两个都不对,那就是代码本身有逻辑问题,乖乖回去改状态机,不用往下查。

4. 核心代码拆解:CONTROL-SECTION 状态机的 VHDL 实现

状态机的写法流派很多,教学代码里最常见的是两段式和三段式。两段式把时序和组合混在一起,代码短但可读性一般;三段式拆成状态寄存器、次态逻辑、输出逻辑三块,每块职责单一,出问题的时候能一眼定位到是哪一段。下面这份实现用的就是三段式。

4.1 实体声明:把数据通路的握手信号一次列全

entity 部分对应第 2 章那张信号表,一个不能少。端口命名建议加统一前缀或者后缀,比如所有装载信号都叫xxx_load,所有使能都叫xxx_en,回头在波形窗口里按名字过滤会很方便。

library ieee; use ieee.std_logic_1164.all; entity par_control_unit is port ( clk : in std_logic; rst_n : in std_logic; -- 低电平异步复位 opcode : in std_logic_vector(3 downto 0); -- 来自 IR 的操作码 z_flag : in std_logic; -- 来自 SR 的零标志 -- 取指通路控制 pc_inc : out std_logic; pc_load : out std_logic; mar_load : out std_logic; mem_read : out std_logic; mem_write : out std_logic; mbr_load : out std_logic; ir_load : out std_logic; -- 执行与写回通路控制 alu_op : out std_logic_vector(2 downto 0); shu_op : out std_logic_vector(1 downto 0); sr_load : out std_logic; reg_write : out std_logic; halt : out std_logic ); end entity par_control_unit;

opcode定成 4 位是留了余量的,教学指令集一般 8 到 16 条指令,4 位刚好覆盖。alu_op给 3 位,能表示 8 种运算,加减与或加移位就够了。如果后面要扩展乘法,得把这个位宽加到 4 位,同时记得同步改 test bench 里的观测信号,不然波形上会显示成截断值。

4.2 状态枚举与三段式写法

状态用type ... is (...)枚举出来,可读性比直接写"000"强太多,综合工具也会自动给每个状态分配编码。这里定义七个状态,覆盖从取指到写回的完整流程。

architecture rtl of par_control_unit is type state_t is ( S_FETCH_MAR, -- 送地址:MAR <- PC S_FETCH_MEM, -- 读存储:MBR <- Mem[MAR] S_FETCH_IR, -- 取指完成:IR <- MBR, PC <- PC+1 S_DECODE, -- 译码:按 opcode 分流 S_EXEC, -- 执行:ALU / SHU 运算,更新 SR S_WB, -- 写回:Rd <- 结果 S_HALT -- 停机 ); signal cur_state, nxt_state : state_t; begin -- 第一段:状态寄存器,只做一件事——在时钟沿把次态搬进现态 seq_reg : process(clk, rst_n) begin if rst_n = '0' then cur_state <= S_FETCH_MAR; elsif rising_edge(clk) then cur_state <= nxt_state; end if; end process;

这一段看着简单,但有两个细节值得说。一是复位用的是异步复位,rst_n不在rising_edge的敏感列表里,所以复位一拉低立刻生效,不用等时钟沿。二是复位值直接给了S_FETCH_MAR,不是S_IDLE,省掉一个空转周期,波形上也更好看。

次态逻辑用case展开,关键是开头一定要给nxt_state赋默认值,否则综合工具会推断出锁存器(latch),后面后仿会出现莫名其妙的保持行为。

-- 第二段:次态逻辑,纯组合,把所有分支写全 comb_next : process(cur_state, opcode, z_flag) begin nxt_state <= cur_state; -- 默认保持,杜绝 latch case cur_state is when S_FETCH_MAR => nxt_state <= S_FETCH_MEM; when S_FETCH_MEM => nxt_state <= S_FETCH_IR; when S_FETCH_IR => nxt_state <= S_DECODE; when S_DECODE => if opcode = "1111" then nxt_state <= S_HALT; -- 停机指令 else nxt_state <= S_EXEC; end if; when S_EXEC => nxt_state <= S_WB; when S_WB => nxt_state <= S_FETCH_MAR; -- 无条件回到取指 when S_HALT => nxt_state <= S_HALT; -- 停机后锁死 when others => nxt_state <= S_FETCH_MAR; -- 兜底,防止非法编码跑飞 end case; end process;

when others这一支很多人写代码时会省掉,仿真也照样能过。但如果综合时状态编码用了 one-hot,未定义编码是有可能出现的,加一个兜底分支成本几乎为零。

4.3 输出逻辑:把控制信号表翻译成 case 分支

输出这一段是 Moore 型,只依赖cur_state,不读opcode。这样波形上每个控制信号的宽度都是整数个时钟周期,一眼就能看出哪拍该干什么。

-- 第三段:输出逻辑,Moore 型,先全清零再按状态拉高 comb_out : process(cur_state) begin pc_inc <= '0'; pc_load <= '0'; mar_load <= '0'; mem_read <= '0'; mem_write <= '0'; mbr_load <= '0'; ir_load <= '0'; reg_write <= '0'; sr_load <= '0'; halt <= '0'; alu_op <= "000"; shu_op <= "00"; case cur_state is when S_FETCH_MAR => mar_load <= '1'; -- MAR 装载 PC 值 when S_FETCH_MEM => mem_read <= '1'; mbr_load <= '1'; -- 存储器读,数据落 MBR when S_FETCH_IR => ir_load <= '1'; pc_inc <= '1'; -- 指令进 IR,PC 自增 when S_DECODE => null; -- 译码拍不驱动任何信号 when S_EXEC => alu_op <= opcode(2 downto 0); -- 运算类型直取操作码低三位 sr_load <= '1'; -- 更新状态寄存器 when S_WB => reg_write <= '1'; -- 写回寄存器堆 when S_HALT => halt <= '1'; when others => null; end case; end process; end architecture rtl;

开头那一串清零是整个写法的核心。如果把清零省掉,case里没提到的信号就会保持上一次的值,综合工具直接给你推断出一堆锁存器,后仿波形上会看到本该是低电平的信号莫名其妙地吊在高位。这个坑我在别的项目上踩过一次,查了大半天才发现是漏了默认赋值。

alu_op <= opcode(2 downto 0)这一行是个简化处理,把操作码的低三位直接当 ALU 控制码用,实际工程里通常要做一个译码表。如果指令集里 ALU 操作和操作码不呈这种线性对应关系,就要改成case opcode is的映射写法,多写几行但不会出岔子。

4.4 激励怎么写:让状态机自己跑完一圈

测试平台的作用是给足激励,让状态机把所有分支都走一遍。下面的激励进程分三段:先复位,再给一条普通指令跑完整个取指执行流程,最后给停机指令验证halt是否正确锁死。

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 opcode : std_logic_vector(3 downto 0) := (others => '0'); signal z_flag : std_logic := '0'; signal pc_inc, pc_load, mar_load : std_logic; signal mem_read, mem_write, mbr_load : std_logic; signal ir_load, reg_write, sr_load, halt : std_logic; signal alu_op : std_logic_vector(2 downto 0); signal shu_op : std_logic_vector(1 downto 0); begin -- 时钟自由运行 clk <= not clk after CLK_PERIOD / 2; dut : entity work.par_control_unit port map ( clk => clk, rst_n => rst_n, opcode => opcode, z_flag => z_flag, pc_inc => pc_inc, pc_load => pc_load, mar_load => mar_load, mem_read => mem_read, mem_write => mem_write, mbr_load => mbr_load, ir_load => ir_load, reg_write => reg_write, sr_load => sr_load, halt => halt, alu_op => alu_op, shu_op => shu_op ); stim : process begin rst_n <= '0'; -- 复位保持 3 个周期 wait for CLK_PERIOD * 3; rst_n <= '1'; opcode <= "0001"; -- 一条普通运算指令 wait for CLK_PERIOD * 8; -- 跑完取指到写回 opcode <= "1111"; -- 停机指令 wait for CLK_PERIOD * 12; assert halt = '1' report "停机指令未生效,检查 S_DECODE 分支" severity error; wait; -- 挂起,避免进程反复重启 end process; end architecture sim;

最后那个wait;很关键。VHDL 的进程如果执行到末尾没有挂起语句,会自动从头再来一遍,激励就会不停地重复复位,波形上看到的是一段段重复的周期,很容易误判成状态机跑飞。assert ... severity error是给自动化验证埋的钩子,后面接 CI 或者批量跑回归脚本时能直接拿它当判据。

5. 避坑与排查:波形不对时先查这几处

调试状态机的痛苦程度跟代码量不成正比,往往一行漏写能耗掉一晚上。下面这五条是我在这类项目上反复撞见的,按现象、原因、解决三段来写,遇到问题时按顺序对一遍基本能定位。

5.1 波形全是 U 或 X:复位和初值没给

现象:仿真一开跑,cur_state显示一段红色横线,nxt_state也是未知态,所有输出信号都在 X 状态。原因通常是测试平台里rst_n初值给的是'U'而不是'0',或者设计里复位用的是同步复位但时钟还没起来。解决办法是给信号声明时直接赋初值,比如signal rst_n : std_logic := '0';,同时在激励进程里保证复位至少维持两个时钟沿。综合后的网表里复位也有一个释放的时序要求,仿真时间给得太短,状态机可能还没从复位态出来就被驱动了。

5.2 状态卡死不动:次态逻辑漏了分支或推断出锁存器

现象:cur_state停在某个状态好几个周期不动,nxt_state却已经变了,或者干脆保持原值。原因有两类,一类是case里真的漏了某个状态的分支,when others又把兜底写成了原地保持;另一类是次态进程的敏感列表没写全,opcode变了但进程没被触发,nxt_state就停在旧值上。解决办法是把case的每个分支都对着状态枚举逐个核对,敏感列表里该有的信号一个不能少,或者在 VHDL-2008 下直接用process(all)让工具自动推导。

5.3 后仿和功能仿真对不上:SDF 没反标或时钟约束缺失

现象:功能仿真波形完全正确,网表仿真跑出来的状态跳转错位,某些控制信号出现一两个纳秒的毛刺。原因通常是 SDF 时序文件没加载,或者加载了但路径不对,仿真器根本没读到延迟信息。另一种可能是约束文件里没写时钟周期,布局布线工具按默认值优化,实际路径延迟超出了你的预期。解决办法是先确认仿真日志里有没有「SDF annotation succeeded」这类提示,再去 UCF 里补上NET "clk" TNM_NET = "clk"; TIMESPEC "TS_clk" = PERIOD "clk" 20 ns HIGH 50%;,重新布线再跑一次。

5.4 综合报「信号无负载」:测试平台被当成顶层综合了

现象:综合日志里刷出一屏 warning,说某个信号没有任何负载,或者提示找到多个顶层模块。原因是测试平台文件也加进了工程,而综合工具默认把层次结构里最上面那个当成顶层,恰好点到了_tb文件。解决办法是在 Synthesize 的属性里显式指定顶层模块名,或者在工程里把测试平台的综合选项关掉,只保留仿真用途。这个错误不影响编译,所以特别容易漏过去,直到生成比特流失败才发现。

5.5 板子上跑飞但仿真正常:按键抖动和跨时钟域

现象:下载到 Spartan-3E 板子上之后,按下复位键有时候能正常启动,有时候直接停在乱码状态,而仿真里从来没出现过。原因是机械按键的抖动被当成了多次复位信号,或者板上的晶振时钟和内部逻辑之间没有做同步处理。解决办法是加一个简单的按键消抖模块,用两级触发器同步加计数延时,二十毫秒左右就够;跨时钟域的信号一律先过两级同步器再使用。这类问题仿真里几乎不可能复现,只能靠上板经验。

6. 二次开发:把单状态验证扩成一套完整控制通路

这份资料给的是一个状态的示范,真正有价值的是把它扩成完整的控制通路。我一般按三个方向改。第一是把条件跳转补上,S_DECODE里读z_flag,为真时把次态指向一个新增的S_BRANCH,那一拍拉高pc_load而不是pc_inc。第二是把状态机参数化,用generic把 ALU 位宽和状态编码方式暴露出来,改指令集的时候不用动架构。第三是换成微码写法,把每个状态的控制信号打包成一个常量数组,用状态当索引去查表,新增指令只是往 ROM 里多写一行。

验证方法上,我习惯做逐拍比对。先手写一份「状态到控制信号」的期望表,再用 test bench 在每个时钟沿采样实际输出,用assert逐项对比,对不上就报severity error并打印当前状态。

-- 逐拍自检:期望表直接写在进程里,改设计时同步改这里 check : process(clk) begin if rising_edge(clk) then case cur_state is when S_FETCH_MAR => assert mar_load = '1' and mem_read = '0' report "S_FETCH_MAR: mar_load 未拉高" severity error; when S_FETCH_MEM => assert mem_read = '1' and mbr_load = '1' report "S_FETCH_MEM: 读使能缺失" severity error; when S_WB => assert reg_write = '1' report "S_WB: 写回使能缺失" severity error; when others => null; end case; end if; end process;

这套自检的好处是,以后改状态机不用再盯着波形一格格数,跑一遍仿真日志里没报错就说明行为没跑偏。覆盖率方面,至少保证每个状态都被访问过、每个case分支都被走过,用 ModelSim 的coverage report能直接看到哪些分支是空的。

改造方向改动量风险点适合场景
补条件跳转小,加一个状态z_flag采样时机要对齐指令集扩展
generic 参数化中,改 entity位宽不一致导致截断多平台复用
微码查表大,重构输出段常量数组初始化易错指令数量多

踩过的坑里印象最深的一次,是改完状态机忘了同步改期望表,仿真一直报错,我以为是设计写错了,回头翻了两个小时代码,最后发现是自检进程里的旧断言没删。从那以后我每次动状态机,都强制先把期望表改完再动 RTL,两边的 diff 对着看一遍才提交。希望这些能帮到你,少走几段弯路。

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

返回列表