资讯动态

主模式RTL设计实战:三段式状态机与三态驱动详解

发布时间:2026/10/1 1:39:20 来源:尧图企业网站定制
1. 主模式到底主在哪先搞懂主/从对RTL设计的影响1.1 主设备 vs 从设备谁说话、谁听话网上关于状态机的讨论很多从状态机java到MCU状态机编程再到给初学者画的环岛状态机比喻思路其实都是同一个把复杂流程拆成若干稳定的位置每个位置只做固定的事。但落到RTL设计里真正让状态机变得绕不开的场景是总线上的主模式Master模块。我给不少初学者改过代码发现很多人卡住不是因为状态机不会写而是没想明白一个问题我这个模块在系统里到底是主还是从。这个定位直接决定状态机的设计思路也决定后面要不要碰三态驱动。主设备是什么它是总线上主动发起事务的一方。比如CPU要从外部存储器读数据CPU就是主存储器是从。主设备必须自己管理整条时序链路什么时候拉高请求、什么时候输出地址、等多久采样数据、什么时候结束事务、失败要重试多少次。从设备就简单多了它只需要盯着别人给的控制信号准备好数据或者接收数据。这个差别反映到RTL上很直接主模式的模块状态机是骨架三态驱动是手臂。状态机决定下一步干什么三态驱动解决在共享数据总线上怎么该听谁的话。两者缺一不可。1.2 主模式控制器的共同形态请求、事务、总线我推荐大家用外部总线接口主控制器来练习主模式RTL比如读一个并行SRAM或者并口Flash。这个项目麻雀虽小五脏俱全包含了主模式设计的三个关键要素请求接口别人比如CPU逻辑发来读/写请求主控制器接收后转入忙状态。事务状态机管理地址建立、读写时序、数据锁存这一整条流程。共享数据总线数据引脚是inout读的时候必须释放成高阻让外设驱动写的时候才由主控制器驱动。这三个要素挨个拆开都不难但组合起来就会出现经典问题状态转移是写对了可三态使能时序错了仿真波形里数据总线一会儿是高阻一会儿又是未知态连挂在同一条总线上的其他器件都被带坏了。所以这一讲我打算用一个实际的SRAM主控制器例子把状态机和三态放在同一条设计链路里讲清楚。2. 三段式状态机为什么是主模式设计的主心骨2.1 一段式、二段式、三段式到底差在哪很多教材喜欢把状态机分成一段式、二段式、三段式考试也爱考。但实际工程里三段的优势要在稍微复杂一点的时序场景里才体现得出来比如我们正在做的这个主模式读写控制器。一段式单进程把状态跳转和输出写在一个always块里。写法直接状态少的demo没问题但一旦状态多了组合逻辑和时序逻辑混在一起每个状态改输出时都容易搞出锁存器仿真后仿一乱很难查。二段式第一段是状态寄存器第二段是组合逻辑同时处理次态计算和输出译码。二段式比一段式清晰但输出是组合逻辑容易产生毛刺。主模式控制器的控制信号比如OE_n、WE_n直接连外部器件毛刺是致命的。三段式状态寄存器一段次态逻辑一段输出一段。输出那段如果是组合逻辑可以在时序上打一拍来消除毛刺时序上允许的话直接把输出写成寄存器连外部信号更干净。这么一对比就清楚了。正因为主模式控制器要对时序敏感的外部器件发号施令输出信号必须干净、可预测三段式是最稳的选择。还有一点三段式的第三段写起来很直白可读性也高后续别人接手好维护。2.2 一个可以直接改用的三段式状态机骨架下面这个例子是一个典型的SRAM主读状态机骨架三段式结构。我删掉了一些细节方便看清楚三段的分工module sram_master #( parameter ADDR_W 16, parameter DATA_W 16 )( input wire clk, input wire rst_n, // 主模式请求接口 input wire req, input wire wr_n, input wire [ADDR_W-1:0] addr, output reg busy, // 外部SRAM接口 output reg [ADDR_W-1:0] a_out, output reg ce_n, output reg oe_n, output reg we_n, output reg [DATA_W-1:0] d_out, output reg d_out_en, inout wire [DATA_W-1:0] d_io ); localparam [2:0] IDLE 3d0, READ_SETUP 3d1, READ_ACCESS 3d2, READ_LATCH 3d3, WRITE_SETUP 3d4, WRITE_DRIVE 3d5, TURN_AROUND 3d6; reg [2:0] state, next_state; // 第一段状态寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end // 第二段次态逻辑 always (*) begin next_state state; case (state) IDLE: if (req !wr_n) next_state WRITE_SETUP; else if (req wr_n) next_state READ_SETUP; READ_SETUP: next_state READ_ACCESS; READ_ACCESS: next_state READ_LATCH; READ_LATCH: next_state TURN_AROUND; WRITE_SETUP: next_state WRITE_DRIVE; WRITE_DRIVE: next_state TURN_AROUND; TURN_AROUND: next_state IDLE; default: next_state IDLE; endcase end // 第三段输出与时序逻辑寄存器输出 always (posedge clk or negedge rst_n) begin // 下面接着写 end endmodule骨架里第二段有个容易被忽略的细节case里默认先赋值next_state state再写转移。这样可以避免组合逻辑产生锁存器也省得每个分支都写一堆不跳转的默认路径。我用这个习惯好多年了坑少很多。第三段输出我放到下一节跟读写事务一起讲因为光给状态机不给时序配合还是看不出主模式控制器到底在忙什么。3. 读写事务的时序拆解状态图与数据通路的配合3.1 读事务从IDLE到数据锁存的完整路径读一个地址单元主控制器要做的表面动作是拉低片选、拉低读使能其实内部数据总线的方向切换比这微妙得多。我把读路径逐一拆开IDLE所有控制信号复位到无效电平d_out_en拉低数据总线是高阻。这一步的意义是在没有任何事务的时候主控制器不能占着数据总线。READ_SETUP输出地址拉低CE_n。此时数据总线仍然保持高阻。容易犯的错是在这个状态就把OE_n拉了甚至拉低d_out_en去驱动数据总线——都早了。READ_ACCESS拉低OE_n数据总线继续保持高阻等外部存储器驱动数据。这个等待周期长短取决于存储器的访问时间短了读到的数据是错的。这就是为什么状态机里要有独立的等待状态。READ_LATCH高阻状态下把d_io上的数据采样进内部寄存器。这个是读事务最核心的时刻。采样一定要在OE_n有效且数据稳定之后不能跟OE_n同时发生。TURN_AROUND撤销OE_n、CE_n、地址。这个周期就是总线方向切换的缓冲期防止后面紧接着写事务时外部存储器还在驱动总线而这边已经切到输出了。对应第三段输出逻辑always (posedge clk or negedge rst_n) begin if (!rst_n) begin busy 1b0; ce_n 1b1; oe_n 1b1; we_n 1b1; d_out_en 1b0; d_out {DATA_W{1b0}}; a_out {ADDR_W{1b0}}; end else begin case (state) IDLE: begin busy 1b0; ce_n 1b1; oe_n 1b1; we_n 1b1; d_out_en 1b0; end READ_SETUP: begin busy 1b1; ce_n 1b0; a_out addr; end READ_ACCESS: begin oen 1b0; end READ_LATCH: begin // data_reg d_io; 内部数据寄存器的采样 end TURN_AROUND: begin ce_n 1b1; oe_n 1b1; end // 写事务分支见下文 endcase end end这段代码里有个原则每个状态只改变该改变的信号别的信号保持默认值。这样做的好处是一个状态的输出行为看一小段代码就能完全确定不会出现这个信号为什么在这里被莫名其妙改掉的困惑。3.2 写事务与读写切换里的隐藏陷阱写事务比读事务多一个麻烦主设备既要输出目标地址又要在数据总线上驱动数据还要用WE_n做节拍控制。WRITE_SETUP状态里输出地址、CE_n拉低数据总线保持高阻WRITE_DRIVE状态里d_out_en拉高把d_out推到总线上同时拉低WE_n再放一段时间让外设可靠采样。看起来顺利但真正让我吃过亏的是读写切换。如果上一个周期是读事务外部器件还在驱动数据总线下个周期立刻进入写事务你瞬间拉高d_out_en同一个net上就有两个驱动源仿真波形直接出X综合后则是总线冲突严重的情况长期拉低外设内部信号把存储器接口烧了。所以主模式状态机的最后一个状态几乎都是TURN_AROUND。这个状态存在的意义不是说我已经干完了准备回家而是给总线释放方向切换留出台面时间。我见过有人在TURN_AROUND里什么都懒得做就空转一个周期其实这恰恰是它的作用。还有一个细节TURN_AROUND里要把CE_n、OE_n、WE_n全部回到无效电平否则外设可能认为又启动了一次新的读写。切换严苛的时候IDLE里都要保留一个完整周期再响应下一笔请求。4. 三态驱动inout端口不是简单的三选一4.1 三态门、高阻与双向总线的物理意义三态驱动这个概念很多初学者是在芯片引脚上第一次见到的一个引脚怎么既能输入又能输出奥秘在于高阻态它既不算逻辑0也不完全等于逻辑1而是让引脚处于断开连接的状态相当于这个引脚从电路上主动撤退把总线让给别人。一位网友的说法很形象三态驱动就像你站在一条单行道上别人要用这条路的时候你就退到路边你要走这条路的时候你得确认别人已经退开了再迈步。RTL里对应退到路边的动作就是把输出使能信号d_out_en拉低把inout引脚和对外的驱动寄存器断开。物理上FPGA的引脚内部是基于三态缓冲器实现的一个输入通路、一个输出通路、一个输出使能控制。RTL层面我们只是表达控制逻辑综合工具把它映射到真正的三态缓冲器上。你要做的是把方向控制逻辑写对剩下的交给工具。assign d_io d_out_en ? d_out : {DATA_W{1bz}};这一行assign就是三态驱动配置的核心。注意几点d_out_en必须是明确的时序信号不能有毛刺否则总线会出现瞬间错位。读事务时d_out_en保持0所以外部器件驱动的数据会直接出现在d_io上内部通过read_data d_io采样。写事务时d_out_en为1d_io上就是d_out的值外部器件去采样就行。4.2 inout代码的三种常见错误与正确写法我在给初学者看代码时inout相关的错误出现频率最高而且错误模式非常集中。我列一个对照表错误做法现象正确思路把inout直接用reg声明编译报错或仿真结果完全对不上inout必须是wire/net类型驱动通过assign完成忘记拉低d_out_en就进入读事务总线双驱动仿真出现X硬件可能冒烟读事务前必须有一个周期把d_out_en设0读写切换间没有TURN_AROUND周期仿真波形有毛刺长时间运行后外设行为异常状态机末尾插入至少一个总线方向切换周期还有一类隐藏很深的错误内部信号通路写对了但顶层例化的时候inout引脚既没在顶层assign也没连到真正的pad仿真里看d_io波形永远是Z。用内部信号做inout端口必须在顶层用正确方式接线不能只把它悬空。代码层面关键是让d_out_en和状态机严格对齐。不要单独去设计一个当前忙我就输出的逻辑那样输出方向会和状态机脱节时序上容易出孔。正确做法是直接在状态机第三段里根据状态给d_out_en赋值WRITE_DRIVE给1其他状态给0。这样状态机既是流程控制中心也是总线方向控制的唯一来源。5. 在Modelsim里把状态机和三态调到明明白白5.1 Modelsim能不能看RTL电路图能而且该这样用搜索modelsim中能不能查看rtl电路图的人很多说明大家都有这个需求。答案是能ModelSim确实提供RTL视图查看功能编译完设计之后在源码窗口或者Workspace里选中模块通过View菜单里的RTL Viewer就能打开综合前的寄存器传输级电路结构。你在这里能看到状态机的实现结构、各个always块对应的逻辑、数据通路的方向以及inout端口是怎么挂在三态缓冲器上的。不过我要说句实在话RTL Viewer这个东西用来快速审查模块连接关系是有用的例如检查一个信号有没有被莫名其妙多驱动看inout引脚旁边有没有挂正确的三态缓冲器。但如果指望它帮你查状态机为什么跳错效率反而低。更直接的工具是ModelSim的状态机视图State Machine Viewer它能把状态机的寄存器状态和跳转条件画成状态转移图比对着代码翻case清晰得多。你会发现前面讲的三段式结构在状态机视图里是一目了然的一张图。需要注意的是ModelSim不同版本里这个功能的位置有差异有些面向教育的基础版本只保留部分查看能力如果你打开没看到相关菜单先确认版本和授权不要以为代码错了。5.2 用波形把三态总线看穿的实操方法查状态机和三态问题真正高效的做法永远是分两层看波形一层看状态机控制流一层看总线电平。先在工作区把关键信号摆到一组clk、state、next_state、busy、ce_n、oe_n、we_n、d_out_en、d_io以及一个内部采样寄存器。这样摆的目的是让状态和引脚行为在时间上对齐一眼能看出每个状态对应的总线动作。我调试时有一个判断顺序分享给大家检查状态是否顺序跳如果state卡在某个状态不动优先看第二段次态逻辑的转移条件注意是不是请求信号req没进来或者被自己拉起的busy挡住了。这是主模式最常见的自锁问题。检查读事务里d_io应该先是Z然后外部器件驱动的数据稳定出现。如果d_io上全程是Z说明存储器那边没有工作——去看CE_n和OE_n时序如果d_io上出现X极有可能是d_out_en没有在正确状态归零内部驱动和外设驱动打架了。检查写事务里d_out_en拉高的时间必须与WE_n的有效区间匹配。如果d_out_en在IDLE态还保持高电平总线就一直被内部逻辑占着接下来任何人的读操作都被你害了。多调几次你会发现几乎所有三态问题在波形上都有非常明显的特征该Z的地方出了X或者该X的地方出了持续的高电平。前者基本是多驱动后者基本是忘释放总线。对着波形按这几条查比在代码里空想要快得多。另外一个小技巧把state信号用符号化显示即把3d0映射成IDLE的名字这样波形窗里看到的不是一列数字而是可读的状态名。摆波形的时候顺手做掉排查速度能再快一截。ModelSim这种仿真工具核心价值不在于画了一张多漂亮的电路图而在于能让你把时间维度的信号行为看清楚。状态机和三态驱动的联合调试恰恰是时间关系最容易混乱的场景。状态机负责什么时候三态负责谁来驱动用波形把这两件事对齐了主模式RTL设计里最难啃的骨头也就啃完了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑