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

AHB协议实战:流水线结构、突发传输与从机设计

AHB协议实战:流水线结构、突发传输与从机设计 ★ FEATURED ARTICLE
1. 先想清楚AHB为什么非要把一次传输劈成两拍第一次看AHB协议时序图的人十有八九会有一个疑问明明一次读写就那么点事为什么非要拆成两个相位、中间还夹个HREADY我最早接触这套总线的时候也觉得这是为了复杂而复杂。直到我自己动手写从机、跑仿真、看波形把一次传输从头到尾走了一遍才明白这套结构不是设计者拍脑袋想出来的而是被流水线和突发传输这两个目标硬生生逼出来的。AHB协议实战里踩过的坑有一半都能追溯到对这两个字理解不到位。我先把话说在前面这篇内容不讲AHB是什么这种百科式的东西而是站在实际写RTL、调波形、做验证的角度把AHB的流水线结构和突发传输机制拆开讲透告诉你每一根信号为什么长这样、什么时候必须这样、不这样会出什么问题。适合已经看过协议手册但一看就困、或者代码写完了仿真跑不通的读者。如果你是刚入门数字前端的新人建议先把最基本的握手概念过一遍再来如果你已经做过几个AHB从机项目这篇文章里的边界计算和异常响应部分应该有你能直接拿去用的东西。1.1 地址相位与数据相位分离的真实动机AHB把一次传输切成两个阶段地址相位Address Phase和数据相位Data Phase。地址相位里主机把HADDR、HTRANS、HWRITE、HSIZE、HBURST这些控制信息摆上总线数据相位里写操作主机给出HWDATA读操作从机给出HRDATA。一个相位占一个HCLK周期。这么做最直接的好处是总线上的控制通路和数据通路可以同时工作。地址相位在前数据相位在后当第N次传输的数据相位正在进行时第N1次传输的地址相位已经可以摆上来了。也就是说总线在同一时刻同时在处理上一次的数据和这一次的地址。这跟工厂流水线一个道理工位A在拧螺丝工位B在装外壳两个工位同时干活整体吞吐率才能上去。如果不用流水线一次传输必须等数据全部送完才能开始下一次的地址那么总线每个传输都要花两拍甚至更多吞吐率直接砍半。对于当年那些跑在几十兆赫兹、但要求高带宽的嵌入式系统来说这个差距是致命的。所以AHB的设计目标从一开始就很明确用结构上的复杂度换取吞吐率。但流水线是有代价的。代价就是时序上的耦合第N次传输的数据相位能不能顺利结束取决于第N1次传输的地址相位有没有准备好而从机能不能接收第N1次的地址又取决于它当前数据相位处理完没有。这个互相牵制的关系全部压在HREADY这根信号上。理解了这一点后面所有关于等待周期、突发延展的内容就都顺了。1.2 流水线带来的重叠到底重叠了什么我见过不少人在写从机的时候把地址相位和数据相位当成完全独立的两件事来处理结果就是——单次传输能跑通一上突发就全乱。问题出在哪出在他们没意识到这两个相位是同一条时间轴上的相邻两拍且共用同一套状态。具体来说在一个理想的无等待传输中第1拍地址相位主机给出HADDR、HTRANSNONSEQ、HWRITE等从机在HCLK上升沿采样。第2拍数据相位主机给出HWDATA写或从机给出HRDATA读同时主机已经在地址相位摆出下一次传输的地址。注意第2拍这个时刻总线上的HADDR已经变成了下一次传输的地址而不是当前这次。很多新手写从机时看到HADDR变了就以为出错了其实这是完全正常的。从机必须在第1拍把地址锁下来第2拍用锁存的地址去访问内部寄存器。如果你在第2拍还去直接看HADDR那你拿到的是下一个地址读出来的数据自然是错的——这是我在实际项目里见过的最高频的错误之一。再叠一层如果从机在第2拍无法完成数据交换比如内部SRAM要等一个周期它就把HREADYOUT拉低此时整个总线被冻住——地址相位保持不变主机也不能推进到下一次传输。这个冻结是全局的不只是影响当前从机而是影响挂在这条总线上的所有设备。这就是为什么AHB的等待周期设计要格外谨慎你多插一拍整条总线的吞吐率就掉一截。1.3 单周期握手与流水线结构的取舍对比有人会问既然流水线这么麻烦为什么不干脆用简单的请求-应答两相握手我用一张表把这笔账算清楚各位自己判断。对比维度单周期握手无流水AHB两相流水单次传输耗时地址数据串行至少2拍稳态下平均1拍/次控制逻辑复杂度低状态少高需处理相位重叠从机设计难度低直接响应即可中高需锁存地址相位突发传输效率每次都要重新发地址突发内地址自增效率高时序收敛压力小大组合路径经过HREADY调试难度波形直观波形需要对照相位理解从表里能看出来AHB是在效率和复杂度之间做了一次明确的取舍它选择了效率把复杂度甩给了设计者。这也是为什么AHB的从机代码看起来比SPI、I2C这类总线的从机复杂得多——不是因为协议设计得不好而是因为它把流水线的收益做到了极致代价就是你必须理解流水线。还有一点容易被忽略AHB的流水线结构对时序收敛提出了额外要求。HREADY这根信号要从从机出发经过多路选择器回到主机路径上可能串了好几级逻辑。如果这条路径太长整个总线的主频就上不去。所以实际项目里HREADY路径往往是要重点优化的对象必要时得插入寄存器打拍这会引入额外延迟需要协议层面配合。这个权衡在后面的时序章节我会细说。2. 一个读传输从HSEL拉高到HRDATA采样的完整时序链条光讲结构太虚我把一次完整的读传输拆成时钟拍来走一遍。这一段是整篇的基础后面讲突发、讲异常响应用的都是同一套时序语言。如果你能把这一节的时序在脑子里跑通剩下的内容基本就是在这套骨架上加分支。2.1 控制相位的采样时刻谁在时钟沿看什么AHB的采样规则很简单但很多人记不住所有信号都在HCLK的上升沿被采样且采样的前提是HREADY为高。这句话有两个要点一是上升沿二是HREADY为高。在地址相位这一拍主机摆出HSEL选中目标从机由地址译码器产生HADDR32位地址HTRANS传输类型NONSEQ表示一次新传输的开始SEQ表示突发中的后续拍IDLE表示空闲BUSY表示主机忙但不推进HWRITE1为写0为读HSIZE传输宽度3位编码从8位到1024位HBURST突发类型HPROT保护属性实际项目里经常被忽略从机在上升沿看到HSEL和HREADY同时为高、且HTRANS为NONSEQ或SEQ就认为有一次合法传输的地址相位被采到了此时必须把HADDR、HWRITE、HSIZE等锁进寄存器。注意这里是锁存不是立即使用。因为下一拍地址就变了。我这里插一句实操经验写从机代码的时候地址锁存的条件判断一定要写完整不能只写hsel hready。因为HTRANS可能是IDLE这时候地址是无效的你锁进去就是垃圾数据。我见过有人因为这个漏判在空闲周期之后的第一笔传输读到上一次的残留地址查了两天才找到原因。2.2 HREADY如何把一次传输拉长成多拍HREADY是AHB里唯一一根全局节奏控制信号它的作用可以粗暴理解为只要它为低整个总线就停在当前拍不动。从机的输出叫HREADYOUT所有从机的HREADYOUT经过一个多路选择器由HSEL决定选谁汇成全局的HREADY再回送给主机。这个结构意味着只要有一个从机把HREADYOUT拉低全局HREADY就是低主机就被迫等待。一个典型的等待场景是这样走的第1拍地址相位HREADY为高从机锁存地址第2拍数据相位开始但从机内部还没准备好数据HREADYOUT拉低第2拍继续还是同一个时钟拍HREADY为低主机保持所有信号不变第3拍从机数据准备好HREADYOUT拉高HRDATA被采样第4拍正常推进到下一次传输注意第2拍和第3拍其实是同一次数据相位被拆成了两拍地址相位并没有重新发。这就是HREADY的核心作用延展数据相位而不是延展地址相位。地址相位在HREADY为高的那一拍就已经结束了锁存好的地址在整个等待期间保持不变。这里有个细节很多人搞混在等待周期里HADDR会保持住不变看起来像是地址相位还在继续其实不是那只是因为主机在HREADY为低时不推进所以信号自然保持在原值。理解这一点后你在波形上看到地址不变就知道是等待而不是又发了一次地址。2.3 从机的零等待与等待插入设计从机要不要插等待取决于它内部的处理延迟。我把常见的几类从机列一下从机类型典型延迟是否插等待设计要点纯组合逻辑寄存器组0拍不插HREADYOUT恒高单口SRAM同步读1拍插1拍读数据相位延展外设寄存器有副作用视情况可能插注意别重复触发跨时钟域桥多拍插多拍需处理异步握手零等待从机是最省事的HREADYOUT直接接1b1所有传输一拍完成。但零等待的前提是你内部逻辑真的能在一个周期内出数据。如果是同步读SRAM读地址给进去数据要到下一个上升沿才出来那从机就必须在数据相位插一拍等待否则主机会采到旧数据。这里有个经常被忽视的坑插等待时HRDATA的驱动时机。如果你在等待期间就让HRDATA变化而HREADY还是低理论上主机不会采样但很多仿真模型的断言会报错而且如果后端综合出毛刺可能引发误采样。稳妥做法是等HREADYOUT拉高的那一拍HRDATA才稳定输出。还有一个更隐蔽的问题写传输的HWDATA。写操作时主机在数据相位给出HWDATA如果从机插了等待主机必须在整个等待期间保持HWDATA不变。这一点协议里写得很清楚但如果你自己写主机模型忘了保持从机就可能写进错误的数据。双向都要守规矩总线才能正常工作。3. HBURST编码背后的边界计算与回绕逻辑突发传输是AHB效率的来源也是坑最多的地方。单次传输跑通容易突发跑通难难就难在地址推进和边界约束这两件事上。这一节我把HBURST的八种编码掰开重点讲清楚WRAP的回绕怎么算、1KB边界为什么存在。3.1 固定长度突发的四种长度与地址递增HBURST是3位编码对应8种类型HBURST类型传输次数地址行为3b000SINGLE1不递增3b001INCR未定义每拍递增3b010WRAP44递增到边界后回绕3b011INCR44每拍递增3b100WRAP88递增到边界后回绕3b101INCR88每拍递增3b110WRAP1616递增到边界后回绕3b111INCR1616每拍递增INCR是个未定义长度类型主机可以随时用IDLE或NONSEQ结束它长度不受限制但同样不能跨1KB边界。固定长度突发INCR4/8/16的好处是从机可以提前知道要收多少拍做流水线优化WRAP则主要用于cache行填充这类场景。地址递增的步长由HSIZE决定HSIZE3b000是1字节001是2字节010是4字节……一直到3b111是1024位即128字节。递增时是字节地址递增不是第几个元素递增。比如HSIZE24字节、INCR4起始地址0x1000地址序列是0x1000、0x1004、0x1008、0x100C。这个不难但初学者常在HSIZE与地址对齐上翻车——协议规定传输地址必须对齐到HSIZE对应的边界0x1002配HSIZE2就是非法配置严格的从机会报ERROR。3.2 WRAP突发的回绕机制与地址掩码计算WRAP是真正需要动笔算的地方。它的规则是突发长度固定起始地址不一定是突发的起始边界传输会一路递增到回绕边界后跳回突发的开头。计算方式我总结成三步先算突发的总字节数 传输次数 × 每拍字节数由HSIZE决定把起始地址向下对齐到总字节数的整数倍得到回绕边界从起始地址开始递增越过总字节数范围就减回去举个例子HADDR 0x100CHSIZE 3b0104字节HBURST WRAP4。总字节数 4 × 4 16 0x10。回绕边界 0x100C ~0xF 0x1000。地址序列是0x100C → 0x1000 → 0x1004 → 0x1008四拍结束。注意第一拍不是从边界开始是从0x100C开始绕一圈回来。再举个例子HADDR 0x1014HSIZE 3b0104字节HBURST WRAP8。总字节数 8 × 4 32 0x20。回绕边界 0x1014 ~0x1F 0x1000。序列是0x1014 → 0x1018 → 0x101C → 0x1000 → 0x1004 → 0x1008 → 0x100C → 0x1010。八拍结束。这里的关键是那个掩码运算boundary addr ~(total_bytes - 1)。只要total_bytes是2的幂WRAP4/8/16都是这个式子就成立。写代码实现的时候这个掩码可以直接用位与实现非常省资源。WRAP的存在意义是什么主要是配合cache的行填充。cache行通常是16字节或32字节对齐的CPU可能从行的中间开始访问WRAP保证一次突发就能把整行填满且刚好绕回行首。这个设计思路在今天的AXI里还保留着只是名字换成了WRAP burst。3.3 1KB边界限制与它引发的一连串设计约束协议里有一条硬性规定任何突发传输都不能跨越1KB0x400的地址边界。这条规则不是随便定的背后是地址译码的实现方式。AHB的地址译码器通常只译码高位地址低10位直接透传给从机内部使用。如果一次突发跨越了1KB边界那么突发的后半段可能落到另一个从机的地盘上而AHB的架构里一次传输只能选中一个从机HSEL在突发期间保持不变所以就冲突了。为了简化译码逻辑协议干脆规定不让跨。这条约束给主机设计带来了实际负担。比如你要做一个DMA要把一大块数据搬到某地址起始地址和长度是软件配置的你怎么保证不跨1KB常见做法是在主机内部把长传输拆成多个突发每个突发的起点对齐到1KB长度不超过剩余的1KB空间。举个具体算法假设要写1000字节到起始地址0x13F0用INCR突发、HSIZE4字节。第一个突发从0x13F0到0x13FF还剩16字节最多4拍边界到了0x1400重新开始第二个突发从0x1400开始剩下的984字节可以拆成多个INCR16每拍4字节16拍64字节15个完整的INCR16共960字节到0x17C0剩下24字节用INCR4加INCR2……等等INCR2不存在得用INCR或SINGLE凑这个拆分逻辑看起来琐碎但实际项目里必须写对否则跨边界的突发会被从机直接拒绝或者行为未定义。我建议把这段拆分封装成一个独立的小模块单独做覆盖率验证别混在主机的状态机里。还有一点WRAP突发天然不会跨1KB边界因为它的总跨度最大也就是16×1282048字节……等等WRAP16配上HSIZE3b111128字节总跨度是2048字节超过1KB了。所以协议里对WRAP还有额外限制WRAP突发的总字节数不能超过1KB实际使用中HSIZE配到这么大配WRAP16是非法组合。这一点在多份协议文档里描述得不太一样我建议在自己的从机里加个断言遇到这种配置直接报错。3.4 提前终止突发HTRANSBUSY与主机的收尾姿势突发跑一半主机想停下了怎么收场这是设计里必须处理的分支但很多代码只写了正常跑完的路径。第一种方式用IDLE结束。主机在某一拍把HTRANS从SEQ改成IDLE表示这个突发我不要再传了。这种情况下从机不能假设突发一定跑满长度必须能够接受半路终止。所以从机的状态机设计里判断突发结束的条件应该是HTRANS ! SEQ而不是数够N拍。第二种方式用BUSY插入空拍。BUSY的意思是我这个周期没法传数据但突发还没结束。典型场景是主机在突发中间需要去处理别的紧急事情又不能丢掉当前突发的上下文就先插个BUSY保命。注意BUSY拍里地址不推进从机也不能执行任何操作。这里有个非常容易踩的坑BUSY后必须跟SEQ。协议规定BUSY只能出现在突发的中间它的前后必须是SEQ。如果主机用BUSY之后跟了一个IDLE那就是非法序列。很多主机模型写得随意仿真能跑上板就出问题。我建议在从机里加一条断言专门盯这个。还有一个不容易注意到的点某些老版本的AHB实现里BUSY可以用来暂停突发而不影响从机的状态机。但如果从机设计得比较激进在BUSY拍里已经开始预取下一拍的数据可能会读到不该读的东西。安全做法是从机在BUSY拍里什么都不做完全静默。4. 异常响应与突发中断ERROR、RETRY、SPLIT三条路怎么选正常传输跑通只是及格线真正考验设计水平的是异常路径。AHB提供三种非OKAY响应ERROR、RETRY、SPLIT。这三种响应在今天的AHB-Lite里已经砍掉了SPLITRETRY也比较少见但理解它们的设计意图对你写出健壮的从机非常有帮助。4.1 两拍ERROR响应的时序要求与常见违例ERROR响应的时序非常特殊它必须占两拍而且这两拍的HRESP都是ERROR但HREADY的取值不同第1拍HRESP ERRORHREADY 0也就是HREADYOUT拉低第2拍HRESP ERRORHREADY 1为什么必须两拍因为主机需要在第一拍看到出错了这个信息然后在第二拍才结束这次传输。如果只给一拍主机可能来不及反应。这个设计在协议里叫two-cycle ERROR response。实际写从机时最常见的违例是只给了一拍ERROR。这种错误在简单仿真里可能不报因为主机模型宽容但换个严格的主机模型或者上FPGA联调就会出现主机状态机卡死。我建议从机的ERROR路径单独写一个两态小状态机确保两拍一定给足。第二个常见违例是在数据相位才决定报错但地址相位已经ACK了。严格来说从机可以在数据相位报ERROR协议允许。但要注意如果是写传输主机在数据相位已经把HWDATA送上来了这时候报错意味着这次写没生效从机必须保证内部的写操作确实没有被执行。很多从机实现里写使能在数据相位一上来就拉高等发现地址非法时数据已经写进去了——这就属于报错了但副作用还在非常危险。正确做法是先做合法性检查通过了才真正执行写。4.2 RETRY与SPLIT的区别以及SPLIT退场后的现实选择RETRY和SPLIT都是为了解决从机暂时无法服务的问题区别在于RETRY从机告诉主机我现在忙你过会儿重发一次。主机收到RETRY后会重新发起这次传输地址相位重来一遍。SPLIT从机告诉主机我处理不了先把这次传输挂起等我处理完了再通知你。主机收到SPLIT后必须释放总线让别的从机用等从机发来SPLIT完成信号再重试。SPLIT的意图很明确提升多主机系统下的总线利用率。一个慢速从机处理传输要几十个周期如果一直占着总线其他主机的传输全被卡住。SPLIT允许在等待期间把总线让出来。但SPLIT实现起来极其复杂需要额外的SPLIT完成信号通路HSPLIT、需要跟踪未完成的传输、需要处理多个挂起请求的仲裁。结果就是绝大多数实际项目里根本不用。AHB-Lite直接把它砍了只保留ERROR和RETRY甚至RETRY在新设计里也很少用。现实中的替代方案是用HREADYOUT拉长等待周期。虽然这样会挡住总线但如果从机的处理延迟在几十拍以内直接等反而比SPLIT更简单可靠。我个人的经验是只要从机响应延迟不超过50拍优先用HREADYOUT等待超过这个量级才考虑上更复杂的架构比如加个FIFO缓冲。4.3 从机在突发中途报错的正确姿势突发跑到一半从机突然发现某个地址非法、或者内部FIFO满了怎么报错这个问题比单次传输报错麻烦得多。协议规定突发中途报ERROR会中止整个突发。也就是说从机在第N拍给出ERROR两拍响应后主机必须停止后续传输整个突发作废。这里的关键是作废——之前已经成功的N-1拍数据是有效的但从机不能指望后面的拍还会来。实现时要注意几点第一ERROR响应的两拍里突发上下文不能丢。从机要清楚自己处在哪个突发的第几拍因为主机可能在ERROR后立刻发起新的传输从机需要正确重置内部计数器。第二写突发的部分写入问题。如果突发写了3拍后第4拍报错前3拍的数据已经写进去了从机内部的数据处于半更新状态。这在功能上是不是可接受取决于你的设计约定。如果不可接受就必须引入缓冲整个突发先写到缓冲区全部成功后再一次性提交任何一拍出错就整体丢弃。这个做法成本不低但能保证数据一致性。第三读突发的部分读出问题。读突发中途报错前面读出来的数据主机可能已经用掉了后面的没了主机得自己处理这个不一致。这属于系统层面的事从机管不了但设计文档里必须写清楚。我在一个项目里遇到过这样的场景从机内部有个FIFO读突发的时候第5拍FIFO空了从机就报了ERROR。结果主机的DMA把前面4拍数据当成了完整的块处理后面出现数据错位查了很久才发现是部分读导致的。后来改成从机在突发开始前先检查FIFO里够不够整个突发的数据量不够就提前报错问题才解决。这个教训我记到现在要么全给要么早拒绝别给一半。5. 动手写一个带突发支持的AHB从机状态机与握手细节理论讲够了直接上代码。这一节我写一个最小可用的AHB从机支持突发读写、支持等待周期插入、支持ERROR响应。代码不多但每一行都有讲究我会逐段解释。5.1 从机接口信号梳理与寄存器映射思路先定接口。这是一个简化的AHB-Lite从机32位地址、32位数据module ahb_slave_demo #( parameter AW 32, parameter DW 32, parameter DEPTH 256 )( input wire hclk, input wire hresetn, // 地址相位信号 input wire hsel, input wire [AW-1:0] haddr, input wire [1:0] htrans, input wire hwrite, input wire [2:0] hsize, input wire [2:0] hburst, input wire [3:0] hprot, // 数据相位信号 input wire [DW-1:0] hwdata, output reg [DW-1:0] hrdata, output reg hreadyout, output reg [1:0] hresp );HSEL这里假设已经由外部译码器产生从机内部不再做地址译码。实际项目里通常会在从机内部再按高位地址分几个子区域这个看需求。寄存器映射我按最简单的来做haddr[AW-1:2]作为字索引直接索引到一个寄存器数组。实际项目里通常会有控制寄存器、状态寄存器、数据窗口几类需要更细的译码逻辑但骨架是一样的。核心寄存器reg [DW-1:0] mem [0:DEPTH-1]; // 地址相位锁存 reg [AW-1:0] addr_q; reg write_q; reg [2:0] size_q; reg [2:0] burst_q; reg [1:0] trans_q;这几个锁存寄存器是整个从机的记忆地址相位采到什么数据相位就用什么。5.2 从状态机设计IDLE、数据相位、等待插入从机的状态机其实很简单因为AHB的大部分复杂性都在相位这个概念里而不是在状态跳转上。我把它拆成三个状态状态含义跳转条件ST_IDLE空闲等待地址相位采到合法地址相位 → ST_DATAST_DATA数据相位处理中无等待 → 回ST_IDLE有等待 → ST_WAITST_WAIT插入等待周期内部就绪 → 回ST_IDLE这里有个设计选择突发过程中不回到IDLE。因为突发是连续的每一拍地址相位采完立刻进入数据相位中间没有空档。如果为了省事每拍都回IDLE功能上没错但会浪费一个周期吞吐率掉一半。所以我把状态机改成两态ST_IDLE和ST_DATA突发期间一直待在ST_DATA里靠内部计数器区分当前是第几拍。localparam [1:0] HTRANS_IDLE 2b00; localparam [1:0] HTRANS_BUSY 2b01; localparam [1:0] HTRANS_NONSEQ 2b10; localparam [1:0] HTRANS_SEQ 2b11;状态机主体// 数据相位是否在等待 reg wait_state; // 地址相位采样条件 wire addr_phase_valid hsel hreadyout (htrans HTRANS_NONSEQ || htrans HTRANS_SEQ); // 写操作在数据相位完成 wire write_fire addr_phase_valid write_q !wait_state hready; // 读数据在数据相位给出 wire read_fire addr_phase_valid !write_q !wait_state hready;注意addr_phase_valid里用了hreadyout而不是全局HREADY。在单从机系统里两者等价但在多从机系统里从机只能看到自己的HREADYOUT全局HREADY是外部产生后回送的。为了代码通用从机内部应该引用外部输入的HREADY这里简化成HREADYOUT直连。还有个细节write_fire里同时判断了addr_phase_valid和write_q。为什么不是write_q直接用地址相位的hwrite因为写操作发生在数据相位而数据相位对应的地址相位是上一拍采的必须用锁存值。这是流水线设计的核心一定要想明白。5.3 一个可综合的从机代码骨架与逐行说明把上面拼起来完整的实现大概是这个结构always (posedge hclk or negedge hresetn) begin if (!hresetn) begin addr_q {AW{1b0}}; write_q 1b0; size_q 3b010; burst_q 3b000; trans_q HTRANS_IDLE; hreadyout 1b1; hresp 2b00; end else begin // 默认值 hreadyout 1b1; hresp 2b00; // 地址相位锁存 if (addr_phase_valid) begin addr_q haddr; write_q hwrite; size_q hsize; burst_q hburst; trans_q htrans; end // 数据相位处理写 if (write_fire) begin mem[addr_q[AW-1:2]] hwdata; end // 数据相位处理读 if (read_fire) begin hrdata mem[addr_q[AW-1:2]]; end // 等待插入示例如果地址越界插一拍等待再报错 if (addr_phase_valid (haddr[AW-1:8] ! 0)) begin hreadyout 1b0; end end end这段代码有几个地方值得单独说。第一hreadyout默认给1。这是AHB从机的标准写法只有在需要等待时才拉低。这样代码简洁综合出来的逻辑也简单。第二hrdata用非阻塞赋值。读数据在数据相位的那一拍结束时更新主机在下一个上升沿采样正好匹配。如果用阻塞赋值时序上会差一拍读出来的数据错位。第三mem的写使能。注意write_fire的判断它要求地址相位有效、当前是写操作、不处于等待状态。三个条件缺一不可漏一个就会产生意外写入。第四那个haddr[AW-1:8] ! 0的越界检测是个示意。实际项目里地址合法性检查会比这个复杂要覆盖对齐、大小、突发类型等各种情况。还有一个综合相关的注意点如果DEPTH比较大mem会综合成RAM此时读数据有一拍延迟hrdata的赋值方式要跟着改可能会引入额外的等待周期。这个我在下一小节细说。5.4 常见综合与仿真不一致的地方仿真跑通综合后功能不对这是数字设计里的经典问题。AHB从机这块我踩过的坑主要有三类。第一类异步复位与同步释放。hresetn如果是异步复位综合工具会按异步处理但如果复位释放的时机不满足恢复/移除时间可能进入亚稳态。稳妥做法是加个复位同步器把异步复位转成同步释放。这个不是AHB特有的问题但总线作为系统级模块对复位质量要求更高。第二类存储器的读写冲突。如果mem综合成单口RAM同一拍既要读又要写就会冲突。AHB的读写是分时的HWRITE在地址相位决定所以正常情况下不会冲突但如果从机内部有预取逻辑就可能。我建议在存储器外包一层简单的读写仲裁冲突时优先后写读操作插入等待。第三类组合逻辑路径过长。addr_phase_valid这个信号如果组合路径太长可能成为时序瓶颈。它依赖HSEL、HREADYOUT、HTRANS三个信号如果HSEL来自复杂的地址译码路径会更长。实际项目里常见做法是把HSEL提前译码好、寄存一拍代价是多一拍延迟但时序好收敛。还有一个仿真与综合的差异点容易被忽略X态传播。仿真时未初始化的寄存器是X综合后是0或者不确定值。从机里如果有只在某些条件才赋值的寄存器仿真可能因为X态而恰好功能正确综合后就露馅。我建议所有寄存器在复位时都给明确初值别指望综合工具的默认行为。6. 验证与调试那些仿真波形里才看得见的问题代码写完只是开始能不能跑对靠验证。AHB的验证难点在于相位关系和时序窗口这两样东西在普通的功能测试里很容易漏。6.1 断言清单写哪些SVA能抓住80%的协议违例断言是AHB验证的利器。我把我常用的几条整理成表各位可以直接拿去用断言名检查内容触发场景a_addr_stableHREADY为低时HADDR保持不变等待周期a_wdata_stable写传输等待期间HWDATA不变写等待a_busy_followBUSY后必须跟SEQ突发中断a_error_two_cycleERROR响应必须持续两拍异常响应a_hsel_stable突发期间HSEL保持不变跨从机突发a_size_alignHADDR必须按HSIZE对齐非法配置a_burst_1k突发不跨1KB边界长突发拿a_addr_stable举例SVA写出来是property p_addr_stable; (posedge hclk) disable iff (!hresetn) (!hready) | ($stable(haddr) $stable(htrans) $stable(hwrite)); endproperty a_addr_stable: assert property (p_addr_stable) else $error(HADDR changed during wait state!);这条断言能抓住绝大多数等待期间信号跳变的问题。我遇到过主机模型自己写错等待期间HADDR跳了导致从机读错地址的情况就是靠这条断言发现的。a_error_two_cycle稍微复杂一点因为它要检查连续性property p_error_two_cycle; (posedge hclk) disable iff (!hresetn) (hresp 2b01 !hready) | (hresp 2b01 hready); endproperty注意第一拍是hrespERROR !hready第二拍是hrespERROR hready。这个组合不能反反了就说明你的从机时序拍数错了。6.2 典型波形排查HREADY提前拉低、HTRANS跳变断言能抓住的是一部分还有一些问题得靠看波形。我列几个典型的波形长相和对应的病因。症状一从机读出的数据总是慢一拍。波形上看第2拍HREADY拉高、HRDATA更新但主机采到的还是第1拍的值。病因通常是HRDATA的驱动时机错了用阻塞赋值或者在数据相位开始才组合输出。改成在数据相位结束时用非阻塞赋值问题消失。症状二突发跑到一半地址不递增了。波形上看HTRANS一直是SEQ但HADDR卡住不动。这种情况一般是主机内部的地址计数器没有在HREADY为高时更新或者更新条件写成了hready !wait多加了不必要的判断。正确条件就一个hready为高且当前HTRANS为SEQ。症状三写传输的数据写进去了但地址错位。波形上看HWDATA序列是对的但读回来错开一个位置。病因基本可以锁定在从机的地址锁存上可能是addr_phase_valid用了错误的条件导致地址相位晚采或早采。检查一下HREADY的采样时刻对不对。症状四ERROR之后总线卡死。这个通常是ERROR响应少给了一拍主机在等第二拍没等到。也可能反过来从机给了两拍但主机的状态机只处理一拍多出来的那拍被当成了新传输。两种情况都要看不能只怪从机。我个人的习惯是每写一个从机先把单次读、单次写、突发读、突发写、错误响应这五个场景的波形截下来存档作为基准。后面改代码出问题拿去对比差异一眼就能看出来。6.3 时序收敛与跨时钟域处理最后聊两个工程性问题。时序收敛方面AHB最容易出问题的路径是HREADY。它从从机的HREADYOUT出发经过地址译码多路选择器回到主机路径可能跨好几个模块。如果主频要求高比如200MHz以上这条路径肯定要优化。常见做法有两个一是在多路选择器后加一级寄存器打拍代价是总线多一拍延迟需要主机和从机都配合二是把从机的HREADYOUT提前一拍算出来用寄存输出代替组合输出。第二种做法更常见代价是等待周期的判断要往前提一拍逻辑稍复杂。跨时钟域方面AHB本身是同步总线所有设备挂在同一个HCLK下。但实际系统里经常有异步的从机比如外部存储器接口、低速外设它们的时钟跟总线时钟不一样。这时候需要加一个AHB桥把同步协议转成异步握手。桥的设计要点是地址相位和数据相位都要做跨时钟域同步用握手信号保证不丢不重。这块的坑很多尤其是突发传输跨时钟域时地址序列必须完整传递过去不能只传首地址。我在一个项目里就是因为桥只传了首地址和长度WRAP突发的回绕信息丢了从机那边算出来的地址序列跟主机完全对不上数据全乱。如果你正在做AHB相关的项目我的建议是先把单次传输的波形吃透再上突发先把零等待从机跑通再加等待先把正常路径验证完再造异常。这个顺序别跳跳了就容易在某个环节卡很久。协议文档要看但别一上来就通读容易看晕等有了具体问题再回去查对应章节效率高得多。
阅读完成 · 觉得有帮助?
咨询建站