首页 / 资讯中心 / 文章详情

Wishbone总线入门与实战:FPGA片上互联握手、带宽与从机设计

Wishbone总线入门与实战:FPGA片上互联握手、带宽与从机设计 ★ FEATURED ARTICLE
去年冬天帮一位朋友处理一个FPGA板上的问题他那个设计里挂着一个自制的图像预处理模块、一个存储控制器和一个软核处理器三者之间的粘合逻辑是自己手写的一堆握手信号。仿真跑得干干净净一上板就随机丢数据而且丢得毫无规律。我把代码翻出来看了半小时逻辑本身没错问题出在几个模块的接口边界上谁该在哪一拍拉高应答、谁在应答有效的那一拍照样往下走各处写法都不一样拼在一起就出一个隐蔽的时序空洞。后来我把这三个模块的接口统一改成Wishbone总线的从机形态中间插一个极简的地址译码器两天之内把问题收干净了。这件事让我一直想写点Wishbone总线的东西。它不像AXI那样动辄几十页协议、一堆通道和乱序规则也不像有些商用总线那样资料藏在合同后面。一份规范几十页信号线两只手数得过来一个星期能读到能动手的程度特别适合刚接触数字系统集成的朋友以及那些只想给自己的小IP核找个统一接口的独立开发者。下面这些内容我按“搞懂定位、吃透握手、算清带宽、写一遍代码、记下坑”的顺序来铺能照着复现的地方我都放了可跑的片段读完之后你应该能独立把一个自制模块改造成标准从机也能看懂别人给你的Wishbone IP核该怎么接。1. Wishbone总线到底是什么从一根线到一套规矩1.1 它的出身和当下还值不值得学Wishbone最早是一套开放的片上互联规范由开源硬件社区推动成形后来迭代到B4版本成了很多开源IP核默认的接口形态。它的出发点很朴素给一堆各自为政的模块定一套统一的、不收费的、实现代价极低的对话方式。我第一次认真读它的时候印象最深的是篇幅规范正文并不长重点全在几张时序图和一张信号表上没有隐藏章节也没有需要另外签协议才能看的内容。它解决的问题说穿了就是“接口碎片化”。一个稍微像样的片上系统里面至少有处理器、存储器、外设寄存器组、DMA引擎这几类东西如果每一对之间都用私有握手模块数量一多组合数量就爆炸复用基本不可能。统一到一套主从模型之后任何模块只要实现从机侧那几个信号就能挂到任何主设备下面中间换个译码器就行。这种“接口即契约”的思路是它至今还有生命力的根本原因。值不值得现在学我的看法是如果你做的是中小规模的FPGA系统、教学项目、开源IP集成或者你需要在几个自制模块之间快速搭一条可靠的通道那它非常划算学习成本低于任何主流商用总线如果你做的是高性能计算加速、需要乱序访存和QoS保障的大型SoC那它撑不住该上更重的方案。把它当成“数字系统集成的入门总线”来学性价比极高很多握手、应答、突发、原子操作的概念在它身上看得最清楚学完之后再看AXI那些东西理解速度会快一大截。1.2 和APB、AHB、AXI-Lite摆在一起比一比光说它好没意义放在同类里对比才知道边界在哪。我按自己实际用过的感受列一张表维度包括信号复杂度、吞吐能力、典型用途和上手难度。需要说明的是这里的对比只讨论常见配置下的典型特征具体实现会因厂商和版本有差异。总线形态信号规模是否支持突发典型场景上手成本Wishbone B4少主从各约十余根支持靠CTI/BTE扩展开源IP互联、中小规模片上系统低一周可上手APB类外设总线很少不支持低速外设寄存器访问极低AHB类高性能总线中等支持处理器与存储器之间中等AXI-Lite中等偏多五通道不支持寄存器配置、控制通路中等完整AXI多通道独立支持支持乱序高带宽数据通路高从这张表能看出Wishbone的位置它的信号量比APB多但比完整AXI少得多能力上比APB强能玩突发和原子操作但又没有AXI那一整套通道分离和乱序机制。它的甜点区就在“中等复杂度、要复用、但不需要极致带宽”的那一段。我个人的判断标准很直接如果一个系统里的模块数量超过五个而且这些模块将来可能要换平台、换位宽、换时钟那就值得统一接口统一的时候如果带宽需求在每秒几百兆字节这个量级Wishbone完全够用硬上AXI只会让验证工作量翻倍。反过来如果单条通路要跑到每秒几个GB那从一开始就别考虑它。1.3 哪些项目适合上Wishbone说几个我实际见过、也确实跑得不错的场景。第一类是教学和自研IP的验证平台一个软核处理器加一块片上存储器加几个自制外设全部挂在一条Wishbone上地址空间划好仿真里能一口气跑通调试时抓波形也清爽。第二类是传感器数据汇聚几个低速采集模块往一个共享寄存器组里写主设备定时轮询读走数据量不大但对确定性要求高这套模型刚好合适。第三类是老IP复用。开源社区积累了相当数量的Wishbone接口模块图像缩放、CRC校验、简易DMA、UART控制器这类东西拿来就能挂你自己重写一遍未必有这个质量。我曾经把一个现成的图像行缓存模块直接接到自制系统里只改了地址译码半天就通了这种复用效率是私有接口完全给不了的。不适合的场景也很明确。需要多个主设备高并发访问同一块存储、并且要保证公平性和延迟上界的系统Wishbone本身不定义仲裁策略你得自己在外面搭仲裁器做起来不难但容易出问题需要读写通道完全独立、并行传输的数据通路它做不到因为读写共用一套地址和数据线对时序收敛极其苛刻的高频设计它的信号虽然不多但握手路径是组合的稍微不注意就会变成关键路径。这几种情况我的建议是别硬扛换方案比改方案省时间。2. 信号清单与握手规矩读时序图不如背一张流程单2.1 主从两侧的连线到底怎么命名新手最容易懵的地方是命名。Wishbone的信号名后面常带_I和_O后缀这个后缀是相对于主设备说的不是相对于你正在写的那个模块。也就是说ADR_O表示“主设备输出的地址”ACK_I表示“主设备输入的应答”。当你站在从机一侧写代码时这些方向全部要反过来理解从机的地址口是输入应答口是输出。我第一次接别人IP的时候就是因为这个把方向接反了综合不报错仿真全是X。按主设备的视角整理信号大致分三组。第一组是全局的时钟和复位CLK_I和RST_I所有传输都以此时钟为基准复位是高有效还是低有效由具体实现约定规范里不强制。第二组是主到从ADR_O地址、DAT_O写数据、SEL_O字节选通、WE_O写使能、STB_O选通、CYC_O总线周期、CTI_O周期类型、BTE_O突发类型扩展、TGD_O自定义边带数据。第三组是从到主DAT_I读数据、ACK_I正常应答、ERR_I错误应答、RTY_I重试应答、TGD_I自定义边带数据。这里有几个必须记住的细节。ADR_O是字节地址不是字地址访问一个32位寄存器组时相邻寄存器地址差4这个和大多数处理器的习惯一致但自己写译码器时特别容易按字地址算导致地址空间整体错位。SEL_O是字节选通宽度等于数据位宽除以8写操作时它决定哪几个字节真正落地读操作时按规范它应当被忽略。CYC_O表示一个总线周期的持续它比STB_O拉得早、撤得晚STB_O才是单次传输的有效标志。这三个信号的关系是整个协议的地基后面所有时序都从它们推出来。注意规范里有一条硬性要求所有未使用的输入端口必须接到固定电平不能悬空。比如从机不实现错误应答ERR_O就要固定接0主设备不关心重试RTY_I也要接0。悬空在仿真里可能表现正常综合之后行为就变成不确定的这类问题极难排查。2.2 一次读操作拆成四拍来看我把一次最朴素的单次读操作拆成四拍用文字画出来最直观。假设从机需要一拍内部延迟才能给出数据也就是所谓的“寄存器输出型应答”。第一拍主设备把地址放上ADR_O同时把CYC_O拉高表示“我占用总线了”此时STB_O还是低从机不该做任何动作。第二拍主设备拉高STB_O这是明确的“请求有效”从机在这一拍的时钟沿采样到STB_O和CYC_O同时为高开始处理。第三拍从机把ACK_I拉高同时把读到的数据放上DAT_I主设备在这个时钟沿采样拿到数据。第四拍主设备撤销STB_O随后撤销CYC_O一次传输结束。关键在于“撤销”的时机。STB_O必须在ACK_I有效之后才能撤CYC_O要在STB_O之后撤。如果主设备性子急在没等到应答就把STB_O撤了从机侧可能还在处理处理完发现请求没了状态机就卡在那里总线看起来像死了一样。我见过最典型的死锁就是这个原因主设备侧写成了“发一拍就撤”从机侧写成了“等应答才走”两边各说各话。反过来从机侧也有规矩。ACK_I不能在STB_O还是低的时候就拉高否则主设备会误以为上一笔传输的应答来了数据直接错位。有些图省事的从机写成“地址命中就应答”完全不等STB_O在单主单从的简单系统里可能侥幸能跑一旦中间加了译码器或者接了多个从机立刻出问题。这条线我建议你在代码里写成显式的判断条件CYC STB 地址命中三个同时成立才进处理不要图省事。2.3 四种握手模式的选择Wishbone定义了不止一种握手方式选错了会直接影响吞吐和时序收敛。最常见的是标准模式也就是上面拆解的那种STB_O和ACK_I完整握手一次传输至少两拍从机可以在这之间插入任意多拍的等待。它的好处是简单可靠从机想慢就慢主设备老老实实等适合访问慢速外设、寄存器组、带内部状态机的模块。第二种是流水线模式。这里的“流水线”指的是应答与请求可以重叠从机在返回上一笔数据的同时已经可以接收下一笔请求。这种模式要求从机内部有足够的缓冲能力好处是吞吐能接近每拍一笔。我实测过在同样100MHz时钟下标准模式访问一块片上RAM大约每秒两亿字节出头换成流水线模式的从机能做到接近每秒四亿字节差距主要就来自那些被等待周期吃掉的空档。第三种是异步模式用于主从两侧时钟不同源的情况。它的做法是不依赖共同的时钟沿来判定握手而是靠STB和ACK的电平变化配合同步器完成跨时钟域传递。这种模式实现起来最麻烦组合路径也最长但对确实需要跨时钟域的系统来说是必需的。第四种是带标签的流水线模式用TAG信号给每笔请求编号让应答可以按标签对应回去本质上是为了支持多笔请求同时在途。这个我实际用得少除非从机访问延迟很大又需要高吞吐否则没必要上。3. 把带宽算清楚块传输、位宽与真实吞吐3.1 CTI与BTE块传输的语法糖单次传输一次只能搬一个数据搬一大块数据时地址要反复给出效率低。Wishbone用两个信号解决这个问题CTI_O周期类型标识和BTE_O突发类型扩展。这两个信号一起用告诉从机“接下来这串访问属于同一个块地址按某种规律变化你可以提前准备”。CTI_O是3位。全0表示经典周期也就是普通的单次传输001表示恒定地址突发地址不变适合往同一个FIFO入口连续灌数据010表示递增地址突发地址按数据宽度自增最常用111表示突发结束告诉从机“这一串到此为止”。BTE_O是2位描述地址回绕方式00线性递增01四拍回绕10八拍回绕11十六拍回绕。回绕的意思是地址涨到块边界就折回块首这在缓存行填充里很有用。实际写代码时BTE_O用得比CTI_O少得多。我做过的大部分项目里从机只判断CTI_O是否为010然后内部维护一个地址计数器自动递增主机侧只要保持CYC_O不撤、连续发STB_O就行。这样能省掉主机反复计算地址的逻辑也让从机有机会做预取。要注意的是一旦用了递增突发主机侧的地址计数器在每笔传输后也要自己加不能指望从机回传地址两边必须对同一个规律有共识。提示突发传输期间CYC_O必须保持高中途撤销会被从机理解为异常终止。有些从机会因为这种情况进入错误状态需要复位才能恢复。3.2 位宽、时钟、效率手算一遍就明白带宽这件事不能凭感觉得算。理论峰值的公式是时钟频率乘以数据位宽再除以8单位是字节每秒。比如100MHz时钟、32位数据位宽峰值就是 100M × 4 400MB/s。这个数字是天花板实际能到多少全看效率。效率怎么估标准模式下一次单次传输如果从机一拍就应答实际占用两拍一拍请求建立加一拍应答确认效率就是50%对应200MB/s。如果从机需要两拍才应答占用三拍效率降到33%只剩133MB/s左右。这个衰减很吓人所以从机内部的读延迟必须盯着寄存器型从机尽量做成一拍应答。换成流水线模式从机可以在请求进来的下一拍就给出应答并且同时接收下一笔请求稳态下每拍一笔效率接近100%400MB/s。如果数据位宽提到64位同样100MHz峰值就是800MB/s。这里要提醒一句位宽翻倍意味着逻辑资源、布线压力、时序收敛难度都往上走我见过不少设计在64位下跑不到目标频率最后反而比32位慢位宽不是越大越好。再算一个实际例子。假设你要在100MHz、32位、标准模式的配置下把一块8MB的数据从片外搬进片内按50%效率算有效带宽200MB/s理论上40毫秒左右。如果换成递增突发加流水线从机效率提到90%同样数据量大约23毫秒。差出来的17毫秒在实时性要求高的场景里就是能不能接受的分界线。所以每次定方案我都会先把这几个数字列出来而不是等做完发现慢再回头改。3.3 多主互联与地址译码单主单从的系统太理想了真实项目里往往有多个主设备要访问多个从设备。Wishbone本身不规定互联方式只规定了两件事一是地址译码器要把请求路由到正确的从机二是仲裁器在授权切换时必须保证当前周期已经结束。地址译码器写起来不难。每个从机分配一段连续地址空间译码器把所有从机的ADR_O接在一起把自己的STB_O和CYC_O按地址区段分发给命中的那个从机再把选中从机的ACK_I和DAT_I回传给主设备。这里有个容易忽略的点未被选中的从机它们的STB_O必须被拉低不能悬空也不能保持高否则从机会误以为有请求进来。仲裁器就麻烦一些。多个主设备同时请求时你得决定谁先走。固定优先级最简单但会饿死低优先级的主设备轮询仲裁公平实现上需要一个指针在每次授权后移动。不管用哪种有一条铁律授权只能在当前主设备的CYC_O为低、或者当前传输已经收到ACK之后才能切换。中途换人会导致从机收到半截请求行为完全不可预期。我自己写仲裁器时会在里面加一个断言一旦违反就立刻报错上板前跑几千次随机请求能抓到大部分问题。4. 手写一对最小可用的Wishbone主从可复现4.1 环境与文件组织这套代码我用的是通用仿真器加综合工具的标准流程不依赖特定厂商的库。目录结构建议保持简单一个源文件目录放主从模块和顶层一个仿真目录放测试平台一个脚本目录放编译和跑仿真的命令。文件名和模块名保持一致别搞大小写混合跨平台容易出问题。我习惯先写从机再写主机因为从机的行为更被动把它的应答时序定死之后主机照着这个时序写就行。反过来先写主机的话调试时很难判断到底是谁的问题。这个小习惯帮我省过不少时间。下面给的代码是最小可读版本去掉了突发、错误应答、跨时钟域这些扩展目的是让握手关系清清楚楚。你可以在此基础上加CTI支持、加字节选通、加错误返回逐层往上叠。4.2 从机带地址译码的寄存器组这个从机实现了四个32位可读写寄存器地址范围从BASE_ADDR开始共16字节。应答是标准的“一拍延迟型”检测到有效的CYC、STB和地址命中后下一个时钟沿拉高ACK同时把读数据放进输出寄存器。module wb_slave_regs #( parameter [31:0] BASE_ADDR 32h0000_1000, parameter [31:0] SPAN 32h0000_0010 )( input wire clk_i, input wire rst_i, input wire [31:0] adr_i, input wire [31:0] dat_i, output reg [31:0] dat_o, input wire [3:0] sel_i, input wire we_i, input wire stb_i, input wire cyc_i, output reg ack_o ); reg [31:0] regs [0:3]; integer i; wire hit cyc_i stb_i (adr_i BASE_ADDR) (adr_i (BASE_ADDR SPAN)); always (posedge clk_i) begin if (rst_i) begin ack_o 1b0; dat_o 32h0; for (i 0; i 4; i i 1) regs[i] 32h0; end else begin ack_o 1b0; if (hit) begin ack_o 1b1; if (we_i) begin regs[adr_i[3:2]] dat_i; end else begin dat_o regs[adr_i[3:2]]; end end end end endmodule这段代码有几个设计选择值得说明。ack_o和dat_o都放在同一个时序块里用非阻塞赋值保证它们在同一个时钟沿更新主机采样ack时读到的数据一定是新的。地址译码用比较器而不是切片是为了让参数化更灵活代价是综合出稍微多一点的逻辑但在这种小规模从机上完全无所谓。SEL信号这里只用到了接口声明实际写寄存器时没有按字节处理这是为了代码简洁真实项目里如果要支持按字节写得展开成逐字节判断否则16位访问会覆盖掉不该动的字节。注意dat_o在复位后必须有初值否则仿真里是X综合后可能是任意值。我一般把所有输出寄存器都写进复位分支宁可多几个触发器。4.3 主机两态保守状态机主机这边我写成非常保守的状态机只有空闲和请求两个状态。宁可慢一点也要保证握手完整。done_o信号在传输完成后拉高一拍方便上层逻辑判断。module wb_master_fsm ( input wire clk_i, input wire rst_i, input wire go_i, input wire [31:0] addr_i, input wire [31:0] wdata_i, input wire we_i, output reg [31:0] rdata_o, output reg done_o, output reg [31:0] adr_o, output reg [31:0] dat_o, output reg [3:0] sel_o, output reg we_o, output reg stb_o, output reg cyc_o, input wire [31:0] dat_i, input wire ack_i ); localparam S_IDLE 1b0, S_REQ 1b1; reg state; always (posedge clk_i) begin if (rst_i) begin state S_IDLE; stb_o 1b0; cyc_o 1b0; we_o 1b0; sel_o 4h0; adr_o 32h0; dat_o 32h0; rdata_o 32h0; done_o 1b0; end else begin case (state) S_IDLE: begin done_o 1b0; if (go_i) begin adr_o addr_i; dat_o wdata_i; we_o we_i; sel_o 4hF; cyc_o 1b1; stb_o 1b1; state S_REQ; end end S_REQ: begin if (ack_i) begin rdata_o dat_i; stb_o 1b0; cyc_o 1b0; we_o 1b0; sel_o 4h0; done_o 1b1; state S_IDLE; end end default: state S_IDLE; endcase end end endmodule这个主机有一个隐性约束go_i不能长时间保持高否则一次传输完成后会立刻开始下一次看起来像在反复读写同一地址。我在实际使用时会加一个边沿检测只在go_i的上升沿触发或者让上层逻辑在done_o之后才撤销go_i。这个细节在测试平台里无所谓一旦接到真实的控制逻辑上就必须处理。4.4 仿真与波形检查点测试平台把主从直接对接跑一次写、一次读比对结果。我额外加了一个超时看门狗防止状态机卡死时仿真无限跑下去。timescale 1ns/1ps module tb_wb_top; reg clk 1b0; reg rst 1b1; always #5 clk ~clk; reg go; reg [31:0] addr, wdata; reg we; wire [31:0] rdata; wire done; wire [31:0] adr, m_dat_o, s_dat_o; wire [3:0] sel; wire we_o, stb, cyc, ack; wb_master_fsm u_m ( .clk_i(clk), .rst_i(rst), .go_i(go), .addr_i(addr), .wdata_i(wdata), .we_i(we), .rdata_o(rdata), .done_o(done), .adr_o(adr), .dat_o(m_dat_o), .sel_o(sel), .we_o(we_o), .stb_o(stb), .cyc_o(cyc), .dat_i(s_dat_o), .ack_i(ack) ); wb_slave_regs u_s ( .clk_i(clk), .rst_i(rst), .adr_i(adr), .dat_i(m_dat_o), .dat_o(s_dat_o), .sel_i(sel), .we_i(we_o), .stb_i(stb), .cyc_i(cyc), .ack_o(ack) ); initial begin go 1b0; addr 32h0; wdata 32h0; we 1b0; repeat (5) (posedge clk); rst 1b0; (posedge clk); addr 32h0000_1000; wdata 32hDEAD_BEEF; we 1b1; go 1b1; (posedge clk); go 1b0; wait (done); (posedge clk); addr 32h0000_1000; wdata 32h0; we 1b0; go 1b1; (posedge clk); go 1b0; wait (done); (posedge clk); if (rdata ! 32hDEAD_BEEF) $display(MISMATCH: got %h, rdata); else $display(PASS); $finish; end initial begin #20000; $display(TIMEOUT); $finish; end endmodule波形上有几个检查点必须逐一确认。第一cyc拉高的那一拍stb是否同时拉高如果不一致说明主机的赋值顺序有问题。第二ack拉高的那一拍s_dat_o上是否已经是正确数据如果晚一拍说明从机的输出寄存器和应答信号不在同一个时序块里。第三ack拉高之后stb和cyc是否依次撤销如果cyc先撤了从机可能收不到完整的周期结束信号。第四done脉冲的宽度是否恰好一拍宽了会导致上层重复触发。这四个点我每次新写一版主从都会看一遍看起来啰嗦但比事后调试省时间得多。5. 踩坑实录与排查手册5.1 永远等不到ACK这是出现频率最高的问题。现象是仿真跑着跑着不动了波形上stb一直高ack一直是低。原因基本集中在三处地址没命中、从机的使能条件写错、从机的状态机根本没进到处理分支。排查顺序我固定从地址开始。把主设备的adr_o、从机的BASE_ADDR和SPAN三个值打到仿真日志里看地址是不是落在区间内。这一步能抓掉相当一部分问题尤其是字节地址和字地址混淆导致整体偏移四倍的那种看起来地址像是命中了其实差了好几个位置。第二步看使能条件。从机里那个hit信号是不是用了组合逻辑且包含cyc和stb很多新手只判断了地址忘了加cyc stb结果地址命中的那一拍就应答了但主设备可能还没准备好反而在别的地方出错。第三步看从机的复位是不是真的拉过我遇到过一次是从机的复位信号接了一个常数0模块从来没被复位过内部状态机停在X态ack永远是X看起来就像低电平。还有一种隐蔽情况从机的ack_o是寄存器输出但它的时钟和主设备的时钟不是同一个或者虽然同一个但中间隔了缓冲导致相位偏移。这种问题在仿真里可能看不出来上板才暴露。解决方法是在仿真里把时钟路径的延迟也建出来或者在设计上就保证主从同源。5.2 读回的数据差一拍或错位数据错位通常有两种表现读回来的值总是上一个地址的内容或者总是比预期晚一个周期才有效。第一种是地址锁存时机不对主设备在ack有效的同一拍才更新地址从机在那一刻采到的还是旧地址。修法是把地址更新提前到stb拉高之前或者让从机在ack有效的下一拍再采样地址。第二种是数据有效窗口和采样沿对不上。主设备如果在ack拉高的同一个上升沿去采样dat_i由于非阻塞赋值的更新发生在沿之后采到的会是旧值。正确的做法是让主设备在ack有效后的下一个上升沿采样或者从机改用组合逻辑输出数据让数据在ack拉高的整个半周期内保持稳定。我倾向后者虽然组合路径长一点但时序关系直观不容易出错。还有一种特别坑的情况从机有多个读数据源比如不同地址返回不同寄存器的值而代码里只写了if (addr X) dat_o a;没有给dat_o一个默认值综合工具推断出锁存器读其他地址时返回的是上次的值。这类问题在仿真里因为初值是X而暴露但如果仿真时恰好先读了那个地址X被覆盖问题就藏起来了。我的习惯是每次读操作前先给dat_o赋一个默认值哪怕多花一点逻辑。5.3 跨时钟域与综合前后不一致跨时钟域是Wishbone用起来最容易翻车的地方。标准模式和流水线模式都假设主从同源时钟一旦不同源stb和ack的握手就必须走同步器。我见过直接用两级触发器同步stb的做法单次传输勉强能跑连续传输立刻出错因为脉冲宽度可能小于目标时钟周期被同步器吃掉了。正确的做法是把手握信号做成电平保持型源端拉高后一直保持直到看到目标端的应答回来才撤销目标端检测到电平后产生自己的应答并保持源端同步回来之后再撤。这样即使两边频率差很多也不会丢事件。实现上需要四个信号来回穿代码量比想象的大但这是唯一稳妥的路子。综合前后不一致的另一个常见原因是未使用端口悬空。仿真器对悬空输入默认给X很多逻辑在X下表现“正常”因为条件判断为假综合工具则可能把它优化成常数0或1行为就变了。我现在的习惯是每个模块实例化时把所有端口都显式连上不用的接常数一个都不留。这个习惯养成之后这类问题基本绝迹。5.4 问题速查表把上面这些整理成一张表出问题的时候按行往下对比从头读波形快得多。现象最可能原因快速验证方法处理方式仿真卡死stb长期为高地址未命中或从机未复位打印addr与地址区间比对修正译码区间或复位连接ack从不拉高从机使能条件缺少cyc/stb检查hit信号表达式补上cyc与stb判断读回值总是上一地址内容地址锁存晚于应答看ack有效拍与adr变化拍提前更新地址或延后采样读回值比预期晚一拍非阻塞赋值与采样沿冲突检查dat_o赋值方式改组合输出或延后采样沿部分地址读到X或固定值dat_o缺默认值推断出锁存器换地址读一遍看是否变化给dat_o加默认赋值上板异常仿真正常端口悬空或跨时钟域检查所有实例化端口连接未用端口全部接固定电平突发传输中途出错cyc被提前撤销抓cyc与stb的撤销顺序保证cyc最后撤多主系统偶发丢数据仲裁切换时机不对抓grant变化与cyc关系仅在cyc为低或ack后切换这张表里的每一条我都至少踩过一次其中“读回值晚一拍”和“端口悬空”这两条反复出现跨项目、跨平台都会遇到值得单独记在笔记本第一页。我在实际使用中的体会是Wishbone这种东西本身不复杂复杂的是人对它的假设不一致。两个人各写一个模块各自心里都有一套“应答应该在什么时候来”的默认预期单独仿真都对接起来就错。所以我现在的做法是凡是新写的从机先在文件头用注释把它的应答时序写清楚几拍应答、数据在哪一拍有效、是否支持突发、未命中时返回什么然后才动笔写逻辑。这份注释比任何文档都管用半年后回来看自己的代码也能一分钟内捡起来。
阅读完成 · 觉得有帮助?
咨询建站