新闻详情

新闻详情

首页 / 资讯中心 / 详情

状态机与三态驱动:主模式总线控制器的Verilog RTL设计实战

发布时间:2026/9/28 1:19:02来源:尧图网络
状态机与三态驱动:主模式总线控制器的Verilog RTL设计实战
状态机是数字设计里最像“骨架”的东西而三态驱动又是很多新人在仿真里最头疼的地方。这一讲正好把两者串起来从零设计一个主模式Master总线控制器地址、读写控制都由我们发起数据总线用双向三态口跟外部从设备打交道。学完之后你不仅能写出一段可综合的RTL还能知道仿真里那些 X 态、高阻悬空到底是怎么来的以及综合工具面对 inout 端口时都在干什么。如果你正在做 FPGA 接口设计、MCU 外设控制器或者单纯想把状态机和三态总线的关系理顺这篇内容基本就是为你准备的。我们不讲空泛的概念直接从接口规划、状态跳转、三态生成逻辑写到 testbench 验证全程附带能直接落地的 Verilog 代码。1. 主模式总线控制器的功能边界先定接口再写代码不少同学写 RTL 的习惯是打开编辑器就开始敲状态机敲到一半发现信号缺了、时序错了又回头改。我的建议永远是先把模块当成一个黑盒子把接口和功能边界定义清楚再动手写状态机。1.1 主模式到底意味着什么“主模式”是相对于“从模式”说的。总线上谁发起读、写请求谁就是主设备谁被动响应这个请求谁就是从设备。就我们这次设计的外部总线控制器而言它内部的状态机就是整个读写字过程的总指挥地址什么时候输出、片选什么时候拉低、数据什么时候驱动或释放全由它控制。以 MCU 通过外部总线扩展 SRAM 为例CPU 发起一次读操作我们的控制器要完成输出目标地址产生片选和读使能信号释放数据总线等待 SRAM 把数据推上来在正确的时钟沿采样数据结束本次读操作释放所有控制信号写操作则多一步控制器主动把数据放到总线上并拉低写使能让从设备在指定时刻锁存数据。这里的关键认知是主模式控制器不仅要控制自己的行为还要管理总线的“占用权”。数据总线是双向的谁占有总线、谁释放总线都必须被状态机安排得明明白白。这也是这一讲为什么把状态机和三态驱动放在一起讲的原因。1.2 顶层接口定义与数据通路我习惯先把输入输出信号列成一张表相当于给模块画一张“身份证”。下面是我们这次设计用到的信号信号名方向位宽功能说明clk_iinput1系统时钟rst_n_iinput1异步复位低有效start_iinput1启动一次传输addr_iinput16目标地址wr_data_iinput8写数据rd_data_ooutput8读到的数据done_ooutput1传输完成标志err_ooutput1超时错误标志addr_ooutput16对外地址总线cs_n_ooutput1片选低有效wr_n_ooutput1写使能低有效rd_n_ooutput1读使能低有效data_ioinout8双向数据总线内部数据通路我把它拆成三块状态控制逻辑状态机的状态寄存器和次态组合逻辑负责产生各阶段的控制信号地址锁存与输出把 addr_i 锁存到 addr_o整个传输期间保持不变三态数据驱动根据当前状态决定 data_io 是输出数据、释放为高阻还是作为输入采样这种划分的好处是每个模块的责任单一调试时定位问题很快。如果你把状态机和三态控制写在一个 always 块里看起来省事但后面加超时检测、加总线仲裁时会非常痛苦。2. 状态机的具体定义从画圈到可综合代码状态机不是画一张漂亮的跳转图就算完关键是把每个状态的进入条件、退出条件、输出行为都定清楚。B 站上很多教程喜欢堆状态图但真正到了 RTL 层面我关心的只有三件事状态编码、次态逻辑、输出逻辑。2.1 状态设计与跳转条件以一次最简单的 8 位总线读写为例我定义了 5 个状态状态含义关键行为IDLE空闲总线全部释放等待 start_iADDR_PHASE地址阶段输出地址、拉低片选插入若干等待周期WRITE_PHASE写数据阶段驱动数据总线拉低写使能READ_PHASE读数据阶段释放数据总线等待从设备驱动采样数据DONE完成撤销所有控制信号回 IDLE跳转条件如下IDLE 收到 start_i 后进入 ADDR_PHASE否则保持ADDR_PHASE 等待固定周期我用 2 拍后写操作去 WRITE_PHASE读操作去 READ_PHASEWRITE_PHASE 维持 1 拍后进 DONEREAD_PHASE 维持 2 拍第 2 拍边沿采样数据之后进 DONEDONE 无条件回 IDLE同时拉高 done_o 一个周期有人会问读总线为什么要等 2 拍因为三态总线释放以后从设备驱动数据需要时间即便不考虑外部建立时间仿真器对高阻到有效数据的切换也需要一个周期来更新信号如果采样沿和释放沿太近很容易采到旧值或 X 态。很多新人第一次仿真读操作读回来全是 X十有八九就是这个原因。2.2 三段式状态机的 RTL 写法代码方面我推荐使用三段式状态机第一段同步状态切换第二段组合逻辑计算次态第三段根据当前状态或次态产生输出。下面是核心代码// 状态编码 localparam IDLE 3b000; localparam ADDR_PHASE 3b001; localparam WRITE_PHASE 3b010; localparam READ_PHASE 3b011; localparam DONE 3b100; reg [2:0] state_c; reg [2:0] state_n; // 第一段状态寄存器 always (posedge clk_i or negedge rst_n_i) begin if (!rst_n_i) state_c IDLE; else state_c state_n; end // 第二段次态组合逻辑 always (*) begin case (state_c) IDLE: begin if (start_i) state_n ADDR_PHASE; else state_n IDLE; end ADDR_PHASE: begin if (addr_cnt 2d2) state_n (wr_en_i) ? WRITE_PHASE : READ_PHASE; else state_n ADDR_PHASE; end WRITE_PHASE: state_n DONE; READ_PHASE: begin if (rd_cnt 2d2) state_n DONE; else state_n READ_PHASE; end DONE: state_n IDLE; default: state_n IDLE; endcase end // 第三段输出逻辑这里用组合逻辑时序紧张时建议寄存一拍 always (*) begin cs_n_o 1b1; wr_n_o 1b1; rd_n_o 1b1; done_o 1b0; case (state_c) ADDR_PHASE: begin cs_n_o 1b0; end WRITE_PHASE: begin cs_n_o 1b0; wr_n_o 1b0; end READ_PHASE: begin cs_n_o 1b0; rd_n_o 1b0; end DONE: done_o 1b1; endcase end这里我使用的是当前状态驱动输出也就是 Moore 型状态机好处是输出在某个状态内保持稳定不会受输入信号毛刺影响。如果你用次态驱动输出Mealy 型虽然能省一拍时序但组合逻辑路径长了很容易成为时序瓶颈。2.3 状态机的复位与初始化细节状态机的复位必须想清楚是异步复位还是同步复位。代码里我用了异步复位rst_n_i一旦拉低不管时钟在哪状态立刻回到 IDLE。这在 FPGA 里非常常见因为复位信号一般来自按键、上电监控芯片或 JTAG 调试器。但这里有个工程问题当你在 DFT可测试性设计模式下做扫描测试这条异步复位链怎么处理简单说DFT 会在扫描移位阶段强制让每个寄存器进入确定状态复位信号不能干扰扫描链所以综合脚本里通常会把复位树单独约束或者给复位加一个测试模式旁路。不过对新手来说你只要明白状态机的复位值必须保证上电后进入一个安全状态最好所有输出都是释放态不要让复位瞬间片选还拉着低电平——不然外部器件可能被误操作。状态编码也值得多说一句。IO 资源紧张的时候有人喜欢用独热码One-Hot有人用二进制编码。二进制编码状态寄存器最少但相邻状态跳转可能出现多位翻转在 FPGA 上没有太大影响ASIC 上如果时序余量紧张可以考虑格雷码。这里用了二进制编码没想到后面实际综合时资源占用非常友好逻辑层级也比独热码少一层。3. 三态驱动的实现与常见误区三态驱动看起来就一句话assign data_io oe ? data_out : 8bZ;但在真实项目中这句话前后要处理的事情很多。三态总线的本质是“谁在驱动总线谁在采样总线”以及“切换驱动权的时刻是否安全”。3.1 三态 IO 的 Verilog 表达方式先看正确的写法reg [7:0] wdata_reg; reg [7:0] rdata_reg; wire [7:0] data_from_slave_tmp; // 输出使能只有写数据阶段才驱动总线 wire data_oe (state_c WRITE_PHASE); // 三态驱动器 assign data_io data_oe ? wdata_reg : 8bZ; assign data_from_slave_tmp data_io; // 读数据阶段采样 always (posedge clk_i or negedge rst_n_i) begin if (!rst_n_i) rdata_reg 8h00; else if (state_c READ_PHASE rd_cnt 2d1) rdata_reg data_from_slave_tmp; end这里有几个细节必须注意inout 端口在 RTL 里必须声明为 wire 类型如果你用 SystemVeriloglogic不能直接用来做 inout 端口综合工具会报“output port must be a net”之类的错。这个坑我在项目里见人踩过不止一次。三态驱动器建议单独用 assign 语句不要写在 always 块里。一些旧式写法会用 bufif1 或 tran 原语但可综合风格里 assign 加条件表达式就是最清晰的做法。读取回环路径要显式表达。把 data_io 直接喂给采样寄存器也就是先assign data_from_slave_tmp data_io;再在采样沿打拍。不要图省事直接在 always 块里用 data_io有些仿真器对 inout 端口直读会有怪异行为。3.2 驱动权切换的时序窗口三态总线最容易出问题的时刻是主机从“自己驱动总线”切换到“释放总线等待从设备驱动”的瞬间。打个比方一条路本来你是司机现在你要下车让另一个人来开那么你得先解开安全带、离开驾驶位对方才能坐进来。如果你刚解安全带就踩油门两个人肯定打起来——也就是总线竞争。在我们的设计里从写操作结束到下一次读操作状态机至少要留出一拍让输出使能彻底释放主机才能安全地采样外部数据。我刚才在 READ_PHASE 里等了 2 拍其中第 1 拍本质上是“换司机”时间第 2 拍才是有效采样点。实际项目中这个间隔往往由外部存储器的读时序参数决定比如 SRAM 的地址建立时间、数据输出有效时间等。建议在状态机里把等待周期参数化成常量或者寄存器方便后续适配不同速度的外部器件。读操作结束也有类似问题。采样完成之后从设备还在驱动总线如果主机马上进入下一个写操作并开始驱动数据总线就会和从设备产生一个短暂的冲突窗口。所以 DONE 状态里我让所有输出使能都拉高实际上是在等待从设备撤销驱动。对高速片外器件这个释放延迟还得在时序约束里注明。3.3 仿真与综合中的悬空处理三态信号在仿真里最典型的表现是高阻状态Z如果没有外部驱动会被读成 X。这就导致很多人写 testbench 时发现读数据永远是“xxxxxx”第一反应是代码写错了。其实代码没问题是仿真环境里缺少上拉或从设备模型。有两种常见解决办法// 方法一在 testbench 里给总线加弱上拉 pullup(data_io); // 方法二用从设备模型驱动总线仿真时强制赋值 // 从设备模型内部维护一个 reg 数组读操作时把数据推上总线方法二更接近真实硬件。我们可以写一个简单的存储器行为模型它在片选和读使能有效时把对应地址的数据赋值给总线在写使能有效时采样总线上的数据存入内部数组。这样读到的数据就是确定值而不是 X。综合方面inout 引脚最终会映射到芯片的 IO Pad 结构上。FPGA 一般直接对应一个 IO BUFT 或可配置的 IO 单元ASIC 则是库里的 PAD cell带输出使能端。所以 RTL 中的三态驱动信号最终会被综合工具自动连接到 PAD 的 OE 端口上。你唯一要做的是在约束文件里告诉工具哪些引脚是双向的并设置合适的驱动强度和上拉配置。4. 仿真验证的重点与异常场景我见过不少工程师RTL 写完激动得不行结果 testbench 里一跑全是 X 态然后开始焦头烂额地一个一个查。这一节把仿真里最容易踩的坑和该关注的东西都过一遍。4.1 自检 testbench 的核心框架宁可 testbench 写得丑也不能不写。最简单的自检测试环境包含三部分时钟生成、复位释放、事务激励。下面是一个可运行的框架module tb_bus_ctrl; reg clk_i; reg rst_n_i; reg start_i; reg [15:0] addr_i; reg [7:0] wr_data_i; wire [7:0] rd_data_o; wire done_o; wire err_o; wire [15:0] addr_o; wire cs_n_o, wr_n_o, rd_n_o; wire [7:0] data_io; // 外部存储模型 reg [7:0] mem[0:255]; reg [7:0] data_from_mem; bus_ctrl u_dut( .clk_i(clk_i), .rst_n_i(rst_n_i), .start_i(start_i), .addr_i(addr_i), .wr_data_i(wr_data_i), .rd_data_o(rd_data_o), .done_o(done_o), .err_o(err_o), .addr_o(addr_o), .cs_n_o(cs_n_o), .wr_n_o(wr_n_o), .rd_n_o(rd_n_o), .data_io(data_io) ); // 从设备行为模型低有效读写SRAM 风格 always (*) begin if (!cs_n_o) begin if (!wr_n_o) begin // 写操作把总线数据写入 mem注意这里要用阻塞赋值特性模拟 mem[addr_o] data_io; data_from_mem 8bZ; end else if (!rd_n_o) begin // 读操作把存储数据驱动到总线 data_from_mem mem[addr_o]; end else begin data_from_mem 8bZ; end end else begin data_from_mem 8bZ; end end assign data_io (!cs_n_o !rd_n_o) ? data_from_mem : 8bZ; initial begin clk_i 0; forever #5 clk_i ~clk_i; end initial begin rst_n_i 1b0; #20 rst_n_i 1b1; // 写一个字节地址 0x0010数据 0x5A (posedge clk_i); start_i 1b1; addr_i 16h0010; wr_data_i 8h5A; (posedge clk_i); start_i 1b0; // 等待 done wait (done_o 1b1); $display(Write transaction done at time %t, $time); end endmodule这个框架的重点是从设备模型必须跟真实器件行为一致写的时候锁存数据读的时候把数据往总线上放其他时间输出高阻。如果你只是拉根线把数据和总线绑死那仿真也就失去了意义。4.2 读数据的 X 态陷阱我自己的调试经历里读数据出现 X 态大约有这几种来源复位后没有初始化内部 rdata_reg 没有复位值输出之后被其他逻辑读出 X。这个最简单加复位赋值就行。三态总线悬空被读取主机在释放总线后立刻采样从设备还没来得及驱动此时总线是 Z仿真器报告为 X。解决办法是把采样点往后移一拍。多驱动源冲突主机和从设备同时驱动总线总线上的值由多个驱动源共同作用仿真器报 X 或不定值。这在显示上很隐蔽因为波形可能看起来是稳定的但数值逻辑上不对。排查方法是对每个三态信号的 oe 信号单独拉出来看波形确认 oe 之间没有重叠。建议你在仿真波形里把 data_io、data_oe、rd_n_o、wr_n_o 这几根信号放一起观察。看到 oe 拉高和从设备驱动有效之间有任何一个时钟周期的重叠就要警惕。4.3 状态机死锁与超时保护真实世界里外部器件可能没有应答比如地址错误、器件掉电状态机如果一直在 READ_PHASE 里等整个控制器就卡死了。因此成熟的控制器都有一个超时计数器进入 READ_PHASE 后最多等 N 拍如果从设备一直没把数据驱动到总线上不是高阻状态就强制跳 DONE并置起 err_o。localparam TIMEOUT_CNT 8d50; reg [7:0] timeout_cnt; always (posedge clk_i or negedge rst_n_i) begin if (!rst_n_i) timeout_cnt 8d0; else if (state_c READ_PHASE) timeout_cnt timeout_cnt 1b1; else timeout_cnt 8d0; end always (*) begin if (timeout_cnt TIMEOUT_CNT) state_n DONE; // 同时用 err_o 标志超时 end我额外提个建议写 testbench 的时候专门构造一个“从设备不响应”的场景验证 err_o 是否能在指定周期内拉高。这类异常路径平时不测一旦上板出问题绝对让你加班到怀疑人生。5. 工程化落地与扩展思路到这里一个主模式总线控制器的核心 RTL 已经完整状态机负责时序三态驱动负责总线切换testbench 负责验证。最后聊几个工程化相关的问题方便你把代码移植到真实项目里。5.1 多主设备与总线仲裁的衔接我这套设计默认总线上只有一个主设备也就是咱们的控制器。但真实系统里很可能有 CPU、DMA、调试接口等多个主设备都要访问总线。这时不能简单地把几个三态驱动器直接并在一起否则就是“两个司机抢方向盘”。正确思路是给每个主设备增加一根总线请求信号和总线授权信号由一个仲裁器统一分配使用权。只有获得总线授权的主设备才能把 oe 拉高其他主设备即使内部有数据输出也必须保持在 Z 状态。仲裁逻辑一般会带优先级或轮询机制这部分以后可以单独开一篇讲。你的主设备侧只需要预留bus_grant_i输入在没拿到授权时强制让 oe 无效。5.2 用仿真波形反推约束的进阶方法也许你会发现同样的 RTL在功能仿真全部正确的情况下综合实现后上板就是偶发数据错误。这时候优先检查三态总线在综合后的时序约束是否准确。FPGA 里一般要把 inout 引脚的 max delay / min delay 约束到 IO buffer 上同时保证 oe 路径和输出数据路径的时钟偏斜不能太大否则在快速切换时总线会出现毛刺。具体做法可以参考你手里的开发板示例工程它的 XDC/SDC 文件里通常已经有 IO 约束模板重点看set_property IOSTANDARD、DRIVE、PULLUP这些参数。项目初期就把这些约束配齐后面省去大量排查时间。5.3 还可以往哪个方向扩展如果你已经吃透了这套设计下面几个方向随便挑一个练手增加突发burst传输连续读写多个地址状态机里加一个 burst_cnt每拍自动递增地址直到突发长度结束。流水线与 FIFO当前设计每笔读写都要等状态机走完一遍如果 CPU 频繁读写效率不高。把读写请求做成 FIFO 流水线状态机吞吐会明显提升。参数化总线位宽把 8 位数据总线改成可配置的 16/32 位你会发现三态驱动的数量、地址对齐方式、状态机等待周期全都跟着变对理解接口可复用性很有帮助。我个人在实际调试中的最大体会是三态驱动的问题从来不是三态本身而是总线的所有权切换没想清楚。每次你在代码里看到一个 inout第一反应就应该是问自己这一拍到底是谁在驱动它是不是存在两个驱动都处于使能状态的窗口把这个问题养成肌肉记忆你再回头看状态机的每个状态会突然觉得视野清晰很多。如果这一讲的内容你打算动手实现我的建议是先从最简单的 SRAM 模型开始把读写时序跑通之后再加超时和错误处理最后才考虑多主仲裁。一步一步来比一次写完整个复杂系统要稳得多。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Python实现批量替换文本文件内容并自动备份 2026/9/28 3:06:30

Python实现批量替换文本文件内容并自动备份

整理课程文稿、项目说明或配置说明时,常会遇到同一个旧名称散落在几十个 Markdown 和 TXT 文件中的情况。逐个打开修改容易遗漏,直接运行一段“搜索后立即覆盖”的代码又难以确认改动范围。本文做一个小型应用脚本:先显示哪些文件会被修改,确认无误后才执行替换,并在写入前…

阅读更多 →
深圳龙岗网站制作避坑指南:3种技术栈对比与性能优化实战 2026/9/28 3:06:30

深圳龙岗网站制作避坑指南:3种技术栈对比与性能优化实战

深圳龙岗网站制作避坑指南:3种技术栈对比与性能优化实战 自己不会代码想做网站,却怕被坑?别慌。在龙岗这片创业热土,我见过太多老板花了几万块,做出来的网站打开像拨号上网一样慢,最后不仅没带来客户,还丢了老客。今天不聊虚的,直接拆解…

阅读更多 →
YOLO26改进 - C3k2 | C3k2融合HMHA分层多头注意力机制:优化模型在复杂场景下的目标感知能力 | CVPR 2025 2026/9/28 3:06:23

YOLO26改进 - C3k2 | C3k2融合HMHA分层多头注意力机制:优化模型在复杂场景下的目标感知能力 | CVPR 2025

前言 本文介绍了分层多头注意力驱动的Transformer模型HINT中的核心模块HMHA,并将其集成到YOLO26中。传统多头注意力机制(MHA)存在冗余问题,HMHA通过“通道重排序+分层子空间划分”,使注意力头在不同子空间学习,避免冗余,提取多样化上下文特征。其流程包括通道重排序、分…

阅读更多 →
YOLO26改进 - C3k2 | C3k2融合LWGA轻量分组注意力(Light-Weight Grouped Attention):四路径并行架构破解通道冗余难题 | AAAI 2026 2026/9/28 3:06:23

YOLO26改进 - C3k2 | C3k2融合LWGA轻量分组注意力(Light-Weight Grouped Attention):四路径并行架构破解通道冗余难题 | AAAI 2026

前言 本文介绍了轻量级骨干网LWGANet及其核心模块LWGA在YOLO26中的结合。现有用于遥感(RS)视觉质量分析的轻量级神经网络存在空间初始冗余和通道冗余问题,无法应对RS场景挑战。LWGA采用异构分组策略,将通道划分为4个不重叠子集,每个子集对应特定特征尺度,通过专用子模块…

阅读更多 →
YOLO26改进 - C3k2 | C3k2融合 EVA Block高效视觉注意力块:融合多尺度特征自适应融合与通道级特征精炼 | ICIP 2025 2026/9/28 3:06:22

YOLO26改进 - C3k2 | C3k2融合 EVA Block高效视觉注意力块:融合多尺度特征自适应融合与通道级特征精炼 | ICIP 2025

前言 本文介绍了双边高效视觉注意力网络(BEVANet),并将其核心特征提取单元EVA集成进YOLO26。实时语义分割面临捕捉大感受野和细化精细轮廓的挑战,BEVANet通过SDLSKA、CKS、CFFN等模块解决这些问题,扩大感受野、提升性能、丰富上下文特征。我们将EVA相关代码集成到YOLO26中…

阅读更多 →
YOLO26改进 - C3k2融合 | C3k2融合Mona多认知视觉适配器:打破全参数微调的性能枷锁:即插即用的提点神器 | CVPR 2025 2026/9/28 3:06:21

YOLO26改进 - C3k2融合 | C3k2融合Mona多认知视觉适配器:打破全参数微调的性能枷锁:即插即用的提点神器 | CVPR 2025

前言 本文介绍了新型视觉适配器微调方法Mona,并将其集成到YOLO26中。传统全参数微调成本高、存储负担重且有过拟合风险,现有PEFT方法性能落后。Mona仅调整5%以内的骨干网络参数,在多个视觉任务中超越全参数微调。其核心亮点包括参数效率高、性能突破和即插即用。适配器模块…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉