Wishbone总线实战:握手时序、主从实现与多主互联

发布时间:2026/11/3 5:42:49
Wishbone总线实战:握手时序、主从实现与多主互联 第一次在 FPGA 工程里见到 Wishbone 总线是在一个把十几个 IP 核硬拼在一起的项目上。那会儿每个 IP 的接口都自成一派有的用addr wr_en rd_en三根脉冲有的用valid/ready握手还有的干脆把读写时序焊死在状态机里。想把它们连起来光胶水逻辑就写了四百多行改一处牵动全身。后来有人提议统一到 Wishbone 总线接口一下收敛成“请求—应答”两句话胶水代码砍掉了三分之二。这件事让我意识到Wishbone 总线的价值不在于性能有多强而在于它把“一次片上访问”这件事抽象得足够干净——干净到你可以在半小时内看懂一小时内写出第一个能跑的从设备。这篇内容面向的是刚接触片上总线互联的开发者如果你正在给自研模块设计一个通用接口、正准备把一堆现成 IP 拼成 SoC、或者单纯想搞明白“总线握手”到底在握什么那接下来这些内容应该能直接省掉你翻规范的时间。我会从信号语义讲到代码实现再到实际连接中那些规范上不会写、但一上板就让你抓狂的细节。1. 先给 Wishbone 定位它规定的到底是什么不规定什么1.1 一份只管“单次访问怎么完成”的轻量约定Wishbone 最早由一个开源硬件社区提出并公开维护目标是给各类 IP 核之间定一个最小公共接口。它的规范篇幅不长核心内容其实就一件事主设备发起一次传输从设备给出一个回应双方在这两个动作上达成共识。至于内部怎么实现、有没有流水线、缓存多大、时钟怎么跨规范一概不管。这种“只管接口、不管实现”的取舍带来两个直接后果。好的一面是门槛极低我见过有同事照着信号表半天就写出了能跑通的功能模型不好的一面是它把大量工程决策留给了使用者——地址怎么译码、超时怎么处理、多主怎么仲裁这些都需要你在互联层自己补上。所以你会看到几乎每个用 Wishbone 的工程都有一套私有的互连封装这很正常不是谁跑偏了。需要特别说明的是Wishbone 定义的是逻辑接口不是物理总线。它描述的是信号之间的关系与时序约束落到 RTL 里就是一组端口。所以它既可以用在单芯片内部的模块互联也可以作为片间通信的协议基础但具体电气特性、走线约束都不在它的讨论范围内。1.2 和另外几类常见片上接口摆在一起看很多人纠结的第一个问题是“我该用 Wishbone 还是别的”。我把常见的几类接口按特征摆在一起方便你按场景挑接口类型通道结构事务并发能力规范复杂度典型落地场景Wishbone 经典周期地址数据复用一套无严格串行低寄存器配置、低速外设Wishbone 流水线一套信号、多笔在途有限顺序完成中中等吞吐的数据搬运三通道分离式总线地址/数据/响应各成一组强支持乱序与多笔未完成高高带宽计算子系统简单的 valid/ready 点对点单通道取决于实现极低模块内部、固定拓扑我的判断标准很朴素如果这个模块一辈子只跟一个固定邻居说话别上总线直接 valid/ready 点对点最省事如果它要被多个主设备访问、或者地址空间需要统一编址那 Wishbone 是性价比最高的选择。三通道分离式总线当然更强但它的验证成本、时序收敛难度、IP 适配工作量都是数量级的差距用在只需要读几个寄存器的场景上属于杀鸡用牛刀。1.3 哪些场景不建议硬套 Wishbone有一点必须提前说清楚Wishbone 的经典周期模型里一笔传输从发起到完成至少要经过“拉高请求—等待应答—撤销请求”这几个节拍它天生不适合做超高并发的访存。如果你在设计一个需要同时挂起几十笔未完成读请求的计算加速器硬套 Wishbone 会非常别扭——你会发现自己不停地在外面加 FIFO、加标签管理最后做出来的东西本质上是在 Wishbone 外面又包了一层自己的协议。另一个不太适合的场景是长延迟从设备。比如访问一个需要几十个周期才能返回数据的存储控制器在经典周期下主设备只能干等。这时候要么升级到流水线模式要么在从设备侧加预取否则整个系统的效率会被这一个慢设备拖垮。我踩过这个坑一个图像处理链路里帧缓存的读延迟把整条流水线的吞吐压到了理论值的三分之一后来改成流水线模式才缓过来。2. 四个信号撑起一次交易把握手时序掰开看2.1 一次单字写在波形上究竟长什么样先看最朴素的场景主设备要把一个 32 位数据写进从设备的某个寄存器。整条时间线上发生的事只有这么几步。主设备在第一个时钟上升沿把CYC_O和STB_O同时拉高同时把WE_O置 1把目标地址放到ADR_O数据放到DAT_O字节选通SEL_O按需要置位。从设备在随后的某个时钟沿采样到这两个信号都为高、且地址落在自己的译码范围内就在下一个时钟沿把ACK_I拉高。主设备采样到ACK_I为高确认这笔传输完成随后撤销STB_O事务结束最后撤销CYC_O。这里有两个容易被忽略的时序约束。第一STB_O一旦拉高在收到应答之前绝不能撤销否则从设备会认为自己收到了一笔半截的请求。第二CYC_O的撤销时机可以晚于STB_O它的语义是“我这个主设备还在占用总线”如果你打算紧接着发下一笔保持CYC_O拉高、只翻转STB_O是可以的这叫背靠背传输。很多人第一次写主设备时会犯一个错把STB_O写成单周期脉冲觉得“我发一个脉冲出去从设备自然就收到了”。这在实际时序里行不通因为从设备的应答有延迟如果STB_O只在一个周期有效而从设备三个周期后才回ACK那主设备早就不知道这笔传输对应的是哪次请求了。STB_O的宽度是由从设备的响应速度决定的不由主设备自己拍脑袋定。2.2 从设备为什么必须“两个信号都看”初学者常问既然STB_O已经表示“有一笔传输”为什么还要额外看CYC_O答案是这两个信号回答的是两个不同的问题。CYC_O回答“总线现在归不归我用”STB_O回答“这一拍是不是有效传输”。在流水线模式下这个区别尤其重要。主设备可能在CYC_O持续拉高的期间连续发出多笔传输每笔对应STB_O的一个有效周期。从设备只有同时判断两者才能正确区分“事务边界”和“传输边界”。如果从设备只判断STB_O在跨事务的间隙里就可能误触发如果只判断CYC_O那一个事务里就只能做一笔传输等于把流水线的能力浪费了。规范里对应的一条硬性要求是从设备只有在CYC_O和STB_O同时有效时才被允许驱动应答信号。这条不是建议是必须遵守的约束。我在审查别人代码时见过从设备直接用STB做使能而忽略CYC单独仿真没问题一旦接上多主仲裁器就出乱子——仲裁器切换总线归属时STB可能还有残留高电平从设备就会在别人占用总线的时候错误应答。2.3 ACK、ERR、RTY、STALL 四条回线各管什么从设备返回给主设备的信号有四条它们的分工经常被搞混我列个表说清楚信号语义主设备该怎么做ACK_I传输正常完成采样读数据撤销 STBERR_I传输出错地址越界、校验失败等撤销 STB上报错误可选重试RTY_I暂时无法完成请重试保持 STB重新发起同一笔STALL_I从设备当前无法接收新请求仅流水线暂停推进保持当前请求ACK和ERR是互斥的同一拍绝不能同时为高否则主设备会陷入语义歧义。RTY的语义是“我知道你要干什么但现在做不了你等会再来”——它和ERR的区别在于ERR通常意味着这笔传输永远无法成功重试也没意义RTY则是临时状态重试大概率能成。STALL只在流水线模式下有明确意义它的作用是让从设备能够反压主设备避免请求堆积溢出。经典周期下用不到这个信号因为一次只有一笔在途天然不存在堆积问题。实际工程里我见过不少从设备把STALL拉起来却从不撤销结果主设备永久卡死——这个信号的撤销条件一定要想清楚再写。3. 地址、字节选通与数据宽度最容易糊弄过去的一组细节3.1 字节地址配上 SEL才是一次完整的访问描述Wishbone 的地址是字节地址这一点和很多人的直觉不同。当你看到ADR_O 32h0000_1004它的含义是“第 4096 字节开始的第 5 个字节位置的地址”而不是“第 1004 个 32 位字”。真正的访问粒度由SEL_O决定。SEL_O的每一位对应数据总线上的一个字节。以 32 位数据总线为例SEL_O[0]管DAT_O[7:0]SEL_O[1]管DAT_O[15:8]以此类推。当你要写一个完整的 32 位字时SEL_O 4b1111只写最低字节时SEL_O 4b0001。这个设计的好处是天然支持非对齐和部分写代价是实现方必须认真处理每一位。我见过太多从设备偷懒直接if (wr_en) reg dat_in;一行搞定完全不看SEL。单字测试永远发现不了问题等到软件层做字节操作时整个 32 位寄存器被莫名其妙地覆盖排查起来相当费劲。正确的写法是逐字节判断if (access_hit wb_we) begin if (wb_sel[0]) reg_file[7:0] wb_dat_o[7:0]; if (wb_sel[1]) reg_file[15:8] wb_dat_o[15:8]; if (wb_sel[2]) reg_file[23:16] wb_dat_o[23:16]; if (wb_sel[3]) reg_file[31:24] wb_dat_o[31:24]; end啰嗦是啰嗦了点但这几行代码能帮你省掉后面好几天。3.2 端口宽度与地址对齐的连带关系SEL_O的位宽等于数据总线字节数也就是说 8 位端口配 1 位SEL16 位端口配 2 位32 位配 4 位。当系统中存在不同位宽的端口时互联层就要做宽度转换这个转换不是简单截断涉及地址的重新对齐。举个具体例子。一个 32 位主设备要访问一个 16 位从设备上的地址0x1002。主设备发的是字节地址0x1002SEL_O 4b1100高两字节有效。互联层需要把它转换成从设备侧的地址0x100116 位端口地址按 2 字节步进即0x1002 / 2 0x801这里按字节地址表述为0x1002对应的 16 位字地址同时SEL降到 2 位。这类转换逻辑写错了表现就是“读出来的数高半字和低半字反了”而且是间歇性的只在特定地址触发。我的经验是系统里尽量统一端口宽度非必要不做混合位宽互联。如果实在躲不掉就在互联层用参数化的转换模块别在从设备里各写一套。3.3 端序规范化不了只能靠约定Wishbone 规范对端序的处理比较宽松它承认不同设备可能采用不同端序但不强制统一。这在规范层面是合理的在工程层面是灾难——因为一端序错整个数据通路上的所有数据都是反的而且很难从波形上直接看出来。我的做法是在项目文档里写死一条全系统统一小端任何引入的 IP 若为大端必须在适配层完成转换不许把差异透传到总线上。这条约定看起来土但它把问题挡在了架构层比事后排查要省事得多。顺带提一个端序相关的隐蔽坑。当从设备是 8 位端口、主设备是 32 位时端序的影响会体现在字节的排列顺序上。如果从设备把接收到的第一个字节当成最低字节而主设备发的是最高字节在前那读回来拼起来就是反的。这种情况在做外部存储器接口时特别容易遇到我建议在任何跨端序的适配层里都加一条断言检查读回的数据与写入数据的一致性。4. 从经典周期到流水线与突发四种模式该用在哪4.1 经典周期慢但几乎不会出错经典周期是 Wishbone 最基础的模式一笔传输完成之后才发起下一笔任何时刻总线上最多只有一笔在途。它的实现成本极低主设备只需要一个三状态的机就能跑起来从设备也可以做成纯组合应答。这个模式的短板非常直接每笔传输都要浪费至少一个节拍在握手的往返上。假设从设备零等待应答主设备拉高STB的同一拍从设备就回ACK那确实可以做到一拍一笔。但绝大多数从设备做不到零等待只要它需要寄存一拍吞吐率就掉到一半。当从设备需要多拍才能返回数据时效率衰减更明显。尽管效率不高经典周期在很多场景里仍然是正确选择。寄存器配置、模式设置、状态轮询这些操作的频次本来就不高为了它们去实现流水线纯属过度设计。我在大部分项目里的做法是控制通路全部用经典周期数据通路再评估要不要上流水线这条分界线划下来复杂度控制得比较好。4.2 流水线模式把地址阶段和数据阶段叠起来流水线的核心思路是把“地址发出”和“数据返回”这两个阶段在时间上重叠。主设备在保持CYC_O拉高的同时每个时钟周期推进一个新的地址STB_O保持高电平从设备则按顺序返回对应的应答和数据。这样做的好处是吞吐率能接近每拍一笔代价是主设备必须维护一个“在途请求”的计数器或队列因为发出第 N 笔的时候回来的可能是第 N-1 笔的数据。从设备侧也需要有能力暂存或按序处理多个请求否则就会乱序。规范给出的解决方案是用STALL_I做反压从设备忙不过来就把STALL拉高主设备暂停推进。这里有个必须强调的点流水线模式下的应答顺序必须与请求顺序一致。Wishbone 没有事务 ID 的概念它无法区分两个同时返回的应答分别属于哪笔请求。所以从设备不能自作聪明地乱序返回哪怕它内部有多个并行的处理单元。这一条限制让 Wishbone 在高并发场景下天然吃亏但也是它简单的原因之一。4.3 突发传输地址只发一次数据连着走突发是在经典或流水线的基础上再加一层优化连续访问一片地址时只发第一个地址后续地址由从设备自己递增或按回绕规则计算主设备不再逐个发送。Wishbone 用两个扩展字段来描述突发行为。一个是周期类型标识用来区分经典周期、恒定地址突发、增量突发、突发结束这几种情况另一个是突发类型扩展用来指定地址的递增方式包括线性递增、4 拍回绕、8 拍回绕、16 拍回绕。回绕模式在缓存行填充的场景里很实用因为它能保证一次突发的数据恰好填满一个对齐的行。模式组合地址行为适用场景实现代价经典 单次每笔都发地址寄存器访问极低经典 增量突发首地址 内部递增连续内存搬运低流水线 增量突发流水推进 地址递增DMA、图像行搬运中流水线 回绕突发按行对齐回绕缓存行填充中高流水线 恒定地址地址不变FIFO 批量读写低需要提醒的是这两个扩展字段在规范里属于可选实现。你写的从设备可以不支持突发你写的主设备也可以不发突发。互联的时候一定要确认双方的实现范围否则会出现“主设备以为在发突发、从设备当成一串独立传输”的错位——表现出来就是从设备每次都重新译码同一个地址数据全挤在一个位置。我在一个第三方 IP 上遇到过这个问题最后靠抓波形才定位到是突发类型字段没被识别。5. 手写一个能综合的主设备状态机与超时保护5.1 状态怎么划分才够用一个最简主设备其实只需要三个状态空闲、等待应答、完成。但实际工程里我会额外加一个超时分支理由后面说。信号驱动的原则是所有输出都必须是寄存器输出不能有组合路径从输入直达输出。这不是洁癖是为了时序收敛和避免组合环。如果你的STB_O是组合出来的而从设备的ACK_I又依赖STB_O那就形成了“输出依赖输入、输入依赖输出”的环路综合工具会给出警告仿真时可能看到振荡。状态迁移的逻辑非常直白空闲态检测到用户侧请求装载地址、数据、读写标志拉高CYC和STB进入等待态等待态里一旦看到应答信号锁存读数据撤销STB和CYC回到空闲。module wb_master_simple #( parameter integer TIMEOUT_MAX 1024 // 超时门限,单位:时钟周期 )( input wire clk, input wire rst_n, // 用户侧接口 input wire req, // 单周期请求脉冲 input wire we, // 1写, 0读 input wire [31:0] addr, input wire [31:0] wdata, output reg [31:0] rdata, output reg busy, output reg done, // 完成脉冲,与应答同拍 output reg err, // 错误或超时脉冲 // Wishbone 主设备接口 output reg wb_cyc, output reg wb_stb, output reg wb_we, output reg [31:0] wb_adr, output reg [31:0] wb_dat_o, output reg [3:0] wb_sel, input wire wb_ack, input wire wb_err, input wire wb_rty, input wire [31:0] wb_dat_i ); localparam [1:0] S_IDLE 2d0, S_WAIT 2d1; reg [1:0] state; reg [15:0] wd_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_IDLE; wb_cyc 1b0; wb_stb 1b0; wb_we 1b0; wb_adr 32h0; wb_dat_o 32h0; wb_sel 4h0; rdata 32h0; busy 1b0; done 1b0; err 1b0; wd_cnt 16d0; end else begin // 默认拉低脉冲类输出 done 1b0; err 1b0; case (state) S_IDLE: begin busy 1b0; wd_cnt 16d0; if (req) begin wb_cyc 1b1; wb_stb 1b1; wb_we we; wb_adr addr; wb_dat_o wdata; wb_sel 4hF; busy 1b1; state S_WAIT; end end S_WAIT: begin if (wb_ack) begin wb_stb 1b0; wb_cyc 1b0; rdata wb_dat_i; done 1b1; busy 1b0; state S_IDLE; end else if (wb_err) begin wb_stb 1b0; wb_cyc 1b0; err 1b1; busy 1b0; state S_IDLE; end else if (wb_rty) begin wd_cnt 16d0; // 重试,重新计时 end else if (wd_cnt TIMEOUT_MAX[15:0]) begin wb_stb 1b0; wb_cyc 1b0; err 1b1; // 超时按错误上报 busy 1b0; state S_IDLE; end else begin wd_cnt wd_cnt 16d1; end end endcase end end endmodule这段代码可以直接综合、可以直接仿真。我刻意保持了最少的逻辑没有流水线、没有突发、没有多笔在途。把它作为起点需要扩展时再往上加比一上来就写一个全能主设备要稳得多。5.2 为什么超时看门狗是非加不可的刚写总线逻辑的人经常跳过超时保护理由是“从设备一定会应答”。现实是从设备不应答的原因太多了地址没译码上、译码写错、状态机卡死、时钟域没对齐、复位没接上。每一种都会让主设备永久停在等待态而在系统层面一个卡死的主设备通常会拖垮整条流水线。超时的门限值怎么定我的经验是按最慢从设备的最大合法响应延迟来算再留 2 到 4 倍余量。假设系统里最慢的是一个需要 64 个周期才能返回的存储控制器那门限定在 256 左右比较稳妥。定得太小会误杀正常的长延迟访问定得太大则失去排查价值。还有一个细节超时后必须立即撤销STB和CYC不能只是报个错继续等。因为你无法确定从设备是真死还是只是慢撤销请求能把它从等待状态里解放出来避免后续事务继续被拖住。同时要把错误信号上报给上层让软件知道这笔访问失败了而不是静默丢弃。5.3 背靠背传输能省下什么在上面的状态机里每次传输结束都会撤销CYC。如果改成“完成一笔后不撤CYC直接装载下一笔”就能做背靠背传输省掉CYC的建立和撤销开销。这个优化在单笔延迟大的场景下收益明显。假设每笔传输从拉到撤需要 4 个周期其中 2 个周期是纯粹的总线占用开销那背靠背就能把有效吞吐提升接近一倍。实现上只需要在完成态判断“是否有下一笔请求待发”有就保持CYC高电平、只翻转一次STB。代价是从设备必须能正确处理“CYC持续有效期间的多次STB脉冲”也就是前面提过的两信号联合判断。如果你的从设备实现比较凑合我建议先在主设备侧老老实实撤销CYC等系统稳定了再优化。6. 从设备侧的实现译码、字节写与应答位置6.1 地址译码的命中判断从设备要做的第一件事是判断“这笔访问是不是冲我来的”。标准做法是用地址掩码比较把总线地址和设备基地址都按窗口大小掩码相等即命中。wire hit ((wb_adr ADDR_MASK) (BASE_ADDR ADDR_MASK)); wire access wb_cyc wb_stb hit;这里有两个坑。第一个是掩码必须和窗口大小对应。一个 256 字节窗口对应掩码32hFFFF_FF00如果你写成32hFFFF_F000那窗口就变成了 4KB会和相邻设备的地址空间重叠。重叠的后果是两台设备同时命中、同时驱动应答和数据线仿真里表现为数据线出现 X实际板子上就是总线争用。第二个坑是基地址必须按窗口大小对齐。基地址0x1000配 256 字节窗口是对的基地址0x1080配 256 字节窗口就会跨界因为它的实际覆盖范围是0x1080到0x117F跨越了两个自然窗口。这种地址在软件层看起来没问题硬件译码时就会出怪事。我在一个项目里见过有人把外设基地址定在0x4000_0100窗口 4KB结果前 256 字节的访问正常后面全乱——原因就是没对齐。6.2 SEL 逐字节写与读数据通路写逻辑要按SEL逐字节处理前面已经给过代码。读逻辑则要注意一点读数据必须在应答有效的那个周期就已经稳定。不能等到应答信号拉起来之后才开始从寄存器里取数那样数据来不及。所以读数据通路通常做成组合选择always (*) begin case (wb_adr[7:0]) 8h00: wb_dat_i ctrl_reg; 8h04: wb_dat_i div_reg; 8h08: wb_dat_i status_reg; 8h0C: wb_dat_i data_in_reg; default: wb_dat_i 32h0000_0000; endcase end如果从设备内部有需要多周期才能取回的数据比如从一块 RAM 里读那就不能做成零等待必须在access有效后先发一个内部读请求等数据回来再把应答拉起来。这时候读数据的选择就要基于锁存后的地址而不是当前总线地址因为总线地址可能已经切到下一笔了。6.3 应答信号放组合还是放寄存器这是从设备实现里分歧最大的一处两种做法各有代价。组合应答的写法是assign wb_ack access;好处是零等待、吞吐高、时序关系一目了然。坏处是地址译码的组合延迟会直接串到主设备的应答通路上在高速时钟下容易成为关键路径另外如果多个从设备的应答用或逻辑合并组合路径会进一步拉长。寄存器应答的写法是把access打一拍再输出好处是时序干净坏处是多了一个周期的延迟而且引出一个必须处理的问题STB撤销之后寄存器的应答还会多存在一个周期。这时候主设备已经回到空闲态如果它立刻发起一笔新传输并且地址不同这个残留的应答可能被误采。// 寄存器应答的正确写法:用状态保持,确保应答跟随请求 reg ack_r; always (posedge clk or negedge rst_n) begin if (!rst_n) ack_r 1b0; else ack_r access ~ack_r; // 只在需要时拉高一拍 end assign wb_ack ack_r;我个人的偏好是低速外设用组合应答高速或长延迟设备用寄存器应答加状态机。分界线大概在 100MHz低于这个频率组合应答基本不会成为瓶颈。6.4 一个完整的寄存器文件从设备把上面几块拼起来就是一个可以直接复用的从设备模板module wb_slave_regfile #( parameter [31:0] BASE_ADDR 32h0000_1000, parameter [31:0] ADDR_MASK 32hFFFF_FF00 )( input wire clk, input wire rst_n, input wire wb_cyc, input wire wb_stb, input wire wb_we, input wire [31:0] wb_adr, input wire [31:0] wb_dat_o, input wire [3:0] wb_sel, output wire wb_ack, output reg [31:0] wb_dat_i, output reg [31:0] ctrl_reg, output reg [31:0] div_reg, input wire [31:0] status_reg, input wire [31:0] data_in_reg ); wire hit ((wb_adr ADDR_MASK) (BASE_ADDR ADDR_MASK)); wire access wb_cyc wb_stb hit; // 写通路:逐字节处理 SEL always (posedge clk or negedge rst_n) begin if (!rst_n) begin ctrl_reg 32h0; div_reg 32h0; end else if (access wb_we) begin case (wb_adr[7:0]) 8h00: begin if (wb_sel[0]) ctrl_reg[7:0] wb_dat_o[7:0]; if (wb_sel[1]) ctrl_reg[15:8] wb_dat_o[15:8]; if (wb_sel[2]) ctrl_reg[23:16] wb_dat_o[23:16]; if (wb_sel[3]) ctrl_reg[31:24] wb_dat_o[31:24]; end 8h04: begin if (wb_sel[0]) div_reg[7:0] wb_dat_o[7:0]; if (wb_sel[1]) div_reg[15:8] wb_dat_o[15:8]; if (wb_sel[2]) div_reg[23:16] wb_dat_o[23:16]; if (wb_sel[3]) div_reg[31:24] wb_dat_o[31:24]; end default: ; endcase end end // 读通路:组合选择,保证应答同拍数据有效 always (*) begin case (wb_adr[7:0]) 8h00: wb_dat_i ctrl_reg; 8h04: wb_dat_i div_reg; 8h08: wb_dat_i status_reg; 8h0C: wb_dat_i data_in_reg; default: wb_dat_i 32h0000_0000; endcase end // 应答:零等待直通 assign wb_ack access; endmodule这个模板我在好几个项目里直接复制粘贴改地址映射省了大量重复劳动。需要加等待时把assign wb_ack access换成状态机输出即可其他部分不用动。7. 多主多从互联仲裁与译码该怎么搭7.1 共享总线结构的组成当系统里不止一个主设备时需要一个互连层负责三件事把多个主设备的请求收拢、按策略选一个播出、把从设备的应答路由回发起者。共享总线的结构最省资源所有主设备共享同一组地址和数据线由仲裁器决定当下谁有使用权。它的优点是面积小、验证简单缺点也很明显——同一时刻只能有一个主设备在跑其余排队。当主设备的数量不多三个以内且访问不频繁时这个方案完全够用。// 简化版轮询仲裁器 module wb_arbiter #(parameter N 4)( input wire clk, input wire rst_n, input wire [N-1:0] req, // 各主设备请求 output reg [N-1:0] gnt, // 授权 input wire [N-1:0] done // 各主设备事务结束 ); reg [$clog2(N)-1:0] ptr; // 轮询优先级计算 function [N-1:0] mask_from; input [$clog2(N)-1:0] p; integer i; begin for (i 0; i N; i i 1) mask_from[i] (i p); end endfunction wire [N-1:0] hi_pri req mask_from(ptr); wire [N-1:0] lo_pri req ~mask_from(ptr); wire [N-1:0] winner (hi_pri ! {N{1b0}}) ? hi_pri : (lo_pri ! {N{1b0}}) ? lo_pri : {N{1b0}}; always (posedge clk or negedge rst_n) begin if (!rst_n) begin gnt {N{1b0}}; ptr 0; end else if (|gnt) begin if (|done) begin gnt {N{1b0}}; ptr ptr 1b1; // 简单轮询,粒度为主设备编号 end end else if (|winner) begin gnt winner (~winner 1b1); // 取最低有效位 end end endmodule这段仲裁逻辑是示意性质的实际使用时要处理“授权期间的请求保持”“事务完成后的指针推进”“请求在授权前撤销”等边界情况。我把它列出来主要是想说明一件事仲裁器是互连层里最容易出 bug 的部件因为它的状态跨越了多个主设备的事务任何一处时序没对齐都会造成授权丢失或双重授权。7.2 交叉开关什么时候才值得上交叉开关给每个主设备配一条独立通路多个主设备可以同时访问不同的从设备。它的带宽优势很明显代价是面积和布线复杂度随主从数量乘积增长。我的经验判断是当出现“多个主设备同时访问不同从设备”的真实需求时才考虑上交叉开关。很多项目一开始就上交叉开关结果发现实际负载根本没有并发白白浪费了资源还引入了额外的时序收敛难度。一个折中的做法是分组建交叉。比如把高带宽的主设备数据搬运引擎和从设备存储器接口单独连成一组交叉开关把低速主设备配置接口继续挂在共享总线上。这样既保住了关键路径的带宽又控制了整体复杂度。7.3 错误与重试怎么路由多主场景下错误的处理比单主复杂得多。当某个从设备返回错误时互连层需要做两件事把错误路由回发起该事务的主设备同时确保其他主设备不受影响。实现上互连层要为每个从设备维护一个“当前访问者标识”在转发请求时记录下来在应答回来时按这个标识路由。这个标识在共享总线结构里可以用仲裁器的授权信号直接推导在交叉开关结构里则需要为每条通路单独维护。还有一个容易被忽略的问题从设备的应答必须在同一事务周期内被消费掉。如果主设备在这个时刻恰好处在复位或者被抢占状态应答就丢了主设备会一直等下去直到超时。所以互连层最好加一个小的应答缓冲或者保证授权切换只在事务边界发生。我见过一个多主系统主设备切换时没等事务结束结果新主设备收到了上一笔的应答数据直接错配。8. 仿真验证与死锁排查一次真实卡死问题的排查链路8.1 最小仿真台怎么搭写总线逻辑仿真台是必须的而且要能主动制造异常。我通常会搭一个这样的台子一个主设备、两个从设备一个零等待、一个三周期延迟加上一个可以手动注入错误的从设备模型。module tb_wb_basic; reg clk 1b0; reg rst_n 1b0; always #5 clk ~clk; // 主设备 reg req 1b0; reg we 1b0; reg [31:0] addr 32h0; reg [31:0] wdat 32h0; wire [31:0] rdat; wire busy, done, err; wire wb_cyc, wb_stb, wb_we; wire [31:0] wb_adr, wb_dat_o, wb_dat_i; wire [3:0] wb_sel; wire wb_ack, wb_err, wb_rty; wb_master_simple #(.TIMEOUT_MAX(200)) u_mst ( .clk(clk), .rst_n(rst_n), .req(req), .we(we), .addr(addr), .wdata(wdat), .rdata(rdat), .busy(busy), .done(done), .err(err), .wb_cyc(wb_cyc), .wb_stb(wb_stb), .wb_we(wb_we), .wb_adr(wb_adr), .wb_dat_o(wb_dat_o), .wb_sel(wb_sel), .wb_ack(wb_ack), .wb_err(wb_err), .wb_rty(wb_rty), .wb_dat_i(wb_dat_i) ); // 寄存器文件从设备 wire [31:0] ctrl, divr; reg [31:0] status 32h1234_5678; wb_slave_regfile #( .BASE_ADDR(32h0000_1000), .ADDR_MASK(32hFFFF_FF00) ) u_slv ( .clk(clk), .rst_n(rst_n), .wb_cyc(wb_cyc), .wb_stb(wb_stb), .wb_we(wb_we), .wb_adr(wb_adr), .wb_dat_o(wb_dat_o), .wb_sel(wb_sel), .wb_ack(wb_ack), .wb_dat_i(wb_dat_i), .ctrl_reg(ctrl), .div_reg(divr), .status_reg(status), .data_in_reg(32hCAFE_BABE) ); // 简单的读写任务 task wb_write(input [31:0] a, input [31:0] d); begin (posedge clk); req 1b1; we 1b1; addr a; wdat d; (posedge clk); req 1b0; wait (done || err); (posedge clk); end endtask task wb_read(input [31:0] a, output [31:0] d); begin (posedge clk); req 1b1; we 1b0; addr a; (posedge clk); req 1b0; wait (done || err); d rdat; (posedge clk); end endtask reg [31:0] tmp; initial begin rst_n 1b0; repeat (5) (posedge clk); rst_n 1b1; repeat (3) (posedge clk); wb_write(32h0000_1000, 32hDEAD_BEEF); wb_read (32h0000_1000, tmp); if (tmp ! 32hDEAD_BEEF) $display([FAIL] ctrl readback mismatch: %h, tmp); else $display([PASS] ctrl readback ok); wb_read (32h0000_1008, tmp); // 读状态寄存器

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询