APB协议详解:从两拍时序到RTL从机实现

APB协议详解:从两拍时序到RTL从机实现 芯片项目里凡是跟低速外设打交道的工程师几乎绕不开AMBA总线家族里的APB协议。APB的全称是Advanced Peripheral Bus在AMBA体系里它最容易理解、也最常被低估——你以为它简单到一眼能看穿真做RTL时才发现握手、等待、错误响应这些细节一个比一个刁钻。这篇总结面向刚入门的芯片设计/验证工程师也适合想把外设总线彻底吃透的嵌入式开发者我尽量把协议定位、读写时序、关键机制、RTL落地和踩坑记录一次性讲清楚。1. APB协议定位AMBA家族里的轻量级选手1.1 为什么AMBA要专门做一条简单总线AMBA总线家族里AXI和AHB是给“高性能模块”用的DDR控制器、DMA引擎、CPU核间互联它们需要高带宽、低延迟、并发传输于是协议做得越来越复杂——通道分离、乱序完成、突发传输、outstanding机制每一层都在为性能服务。但芯片里数量最多的模块恰恰是不需要这种性能的UART、SPI、I2C、GPIO、Timer、Watchdog、RTC、中断控制器。这些外设的寄存器访问频率极低一次访问也就是配置一个控制位、读一个状态位对延迟完全不敏感对带宽的要求趋近于零。如果强行让它们都挂AXI或AHB一是从机侧要实现的握手逻辑复杂面积和功耗白白浪费二是AXI那套多通道、多outstanding的特性在外设场景里根本没有发挥作用的机会。APB就是为了这类“慢速配置型外设”设计的。它的核心设计哲学只有三个词简单、稳定、省面积。整个协议只有两拍传输、没有突发、没有流水线、没有仲裁从机端甚至不需要实现请求/授权那套握手只要按状态机把PSEL和PENABLE一配合读写就能完成。1.2 APB信号全家福每个引脚是干什么的APB的信号数量在AMBA家族里是最少的一个标准APB从机接口大概就是下面这张表信号来源方向作用PCLK时钟输入总线时钟所有传输发生在PCLK上升沿PRESETn复位输入低有效异步复位必须保证所有状态可回到IDLEPADDR主机桥输入地址总线SETUP阶段开始有效ACCESS期间保持PSELx地址译码输入从机选择信号由桥根据地址译码产生PENABLE主机桥输入传输使能SETUP为0ACCESS为1PWRITE主机桥输入读写方向1为写0为读PWDATA主机桥输入写数据总线ACCESS阶段携带有效数据PRDATA从机输出读数据总线读传输在ACCESS阶段有效PREADY从机输出从机就绪信号低电平表示需要插入等待周期PSLVERR从机输出传输错误指示在传输完成时被桥采样其中PSELx是关键中的关键。APB上只有唯一一个主机也就是桥Bridge每个外设拿到一个独立的PSEL信号。桥根据PADDR的高位地址译码决定本次访问落到哪个外设然后只把对应的PSEL拉高。从机收到PSEL为高后就知道“轮到我干活了”不需要自己去判断地址有没有命中本设备。PWRITE在整个传输期间保持稳定。PENABLE则是区分“SETUP阶段”和“ACCESS阶段”的标志PSEL先拉高PENABLE保持低这是SETUP下一个周期PENABLE拉高进入ACCESS。这套“先选择、再使能”的顺序就是APB所有时序的核心。APB4规范里还增加了PPROT保护控制、PSTRB写选通等信号用于安全访问控制和窄位宽传输但对大多数设计者来说核心读写流程没有变化。1.3 一次传输的最基本框架为什么最少要两拍APB一次完整传输标准状态机的路径是IDLE - SETUP - ACCESS。IDLE是不传输的状态SETUP持续一个时钟周期在这个阶段桥把地址、读写方向、写数据都放到总线上PENABLE保持低ACCESS阶段PENABLE拉高从机在时钟沿采样完成读写。那为什么不能一拍搞定呢假设只有一拍地址和使能同拍到达从机就需要用组合逻辑判断“这一拍到底是不是有效访问”毛刺和竞争风险都会放大。APB故意把过程拆成两拍SETUP先让地址和控制信号稳定下来再在下一拍用PENABLE给出明确的“可以采样”信号。这样从机看到PENABLE为高时总线上地址和数据早就稳定了采样窗口干净利落。代价是每次访问最少两个周期。但前面说了外设访问频率极低这两个周期的开销完全可以接受。芯片里大量功耗来自总线翻转APB用最少的状态切换和信号活动换取极低的动态功耗这笔账非常划算。2. 时序拆解把读写过程一帧一帧看清楚2.1 写传输时序SETUP、ACCESS、PREADY怎么配合先看最简单的无等待写传输目标是把一个数据写到外设的配置文件寄存器里整个过程可以这样拆第1拍T0PCLK上升沿后桥把PADDR驱动为目标地址PWRITE拉高PSEL拉高PENABLE保持低。外设看到PSEL有效且PENABLE为低知道新传输开始状态从IDLE进入SETUP。第2拍T1PCLK上升沿桥把PENABLE拉高PWDATA已经带上有效数据。外设此时看到PSEL为高、PENABLE为高进入ACCESS阶段开始准备写操作。第3拍T2PCLK上升沿桥采样PREADY。如果从机没有插入等待PREADY为高本次传输完成。桥可以撤掉PSEL回到IDLE也可以紧接着把下一笔地址放上总线直接发起新的SETUP。如果从机内部需要更长时间才能完成写操作比如内部有个同步FIFO要处理或者时钟域跨越还没结束那么从机可以在ACCESS阶段把PREADY拉低。桥看到PREADY为低就保持当前状态不动一直等到PREADY拉高后才认为传输完成。用表格看会更直观时钟沿状态PSELPENABLEPREADY发生的事T0后SETUP10高地址、方向就绪T1后ACCESS11高数据有效从机开始处理T2后完成00高桥采样PREADY传输结束从机的写采样时机是PSEL为高、PENABLE为高、PREADY为高、PWRITE为高四者同时满足的那个时钟沿。很多刚上手的人只看PENABLE就写入一旦PREADY插入等待周期数据可能还没稳定就被写进寄存器这是最典型的APB写错误之一。2.2 读传输时序数据什么时候才算真正有效读传输的框架和写很像区别在于数据流向和采样对象不同。读操作的流程是SETUP阶段放地址和方向PWRITE为低ACCESS阶段PENABLE拉高外设把读数据驱动到PRDATA上桥在下一个PCLK上升沿采样PRDATA和PREADY。关键点在于PRDATA并不是从机一看到PENABLE为低就开始输出的而是在整个ACCESS阶段保持稳定。如果从机的读逻辑是组合逻辑比如用一个case语句根据PADDR选择寄存器值输出到PRDATA那么只要PADDR稳定、寄存器内容不变读数据在ACCESS阶段就是真实有效的。如果从机内部需要用一拍以上时间准备读数据比如内部有个状态机根据访问次数动态返回结果那就必须把PREADY拉低把ACCESS阶段拉长直到PRDATA真正稳定后再拉高PREADY。桥只会在PREADY为高的那个时钟沿取数所以数据迟到一会儿没关系只要说清楚“准备好了”就行。我见过不少人在写读数据通路时把PRDATA在SETUP阶段就给出去结果桥在ACCESS结束时采样的数据反而是旧的或者干脆是X态。记住读数据只保证在PREADY为高、PENABLE为高的那个窗口内有效之前的任何状态都不需要理会。2.3 背靠背传输上一笔结束和下一笔开始之间发生了什么APB传输结束之后桥不一定要回到IDLE再开新传输。规范允许桥在当前传输完成后的同一个节点直接把PSEL保持有效、PENABLE拉低进入新的SETUP阶段。这就是“背靠背”传输中间不需要气泡周期。举个例子连续写两个寄存器地址分别是reg0和reg4。第一笔在ACCESS完成随后桥立即把PADDR切到reg4PENABLE拉低PSEL保持高下一笔传输的SETUP就开始。从机看到的信号变化是PENABLE从1变成0但PSEL一直没有掉过。从机的状态机必须能处理这种“ACCESS直接转SETUP”的路径很多第一次写APB从机的人状态机只写了ACCESS回IDLE结果碰到背靠背传输整个状态机卡死或者丢拍。这个细节看似不起眼实际在CPU连续访问多个外设寄存器时非常常见。编译器生成的代码经常是连续读写多个状态寄存器桥为了效率不会在中间插入IDLE气泡。从机端一定要把“ACCESS完成后PSEL仍有效”当成正常情况来设计。3. 关键特性深挖PREADY、PSLVERR、无突发这些设计取舍3.1 PREADY到底解决了什么问题早期APB规范里没有PREADY从机被假定为固定两拍完成传输。这在简单外设上没问题但遇到需要更长处理时间的外设就麻烦了要么外设强行把处理逻辑压缩到两个周期内要么桥侧只能通过插入固定等待周期来妥协灵活性很差。PREADY是APB3引入的“从机就绪”机制通俗讲就是给从机一个发言权当从机说“我还没准备好”桥就乖乖等在ACCESS状态直到从机点头。这个机制让APB从机可以按自己的节奏工作内部处理慢一点没关系只要在PREADY上表达清楚就行。从机实现PREADY的一个常见误区是把PREADY定义为“仅在ACCESS阶段才可能为低”这是正确的但复位后PREADY如果保持低电平桥可能会误以为从机一直忙造成整个总线段访问挂死。正确做法是让PREADY默认输出高只有确实需要插入等待时才在ACCESS阶段拉低并且保证不会无限期拉低。另一个我踩过的坑有些从机把PREADY打了两拍寄存器输出结果本来两拍的传输变成三拍。PREADY本质是一个电平信号不是脉冲不需要打拍去“同步”。你多打一拍CPU访问一次寄存器就多一个等待周期程序跑起来慢半拍调试性能问题时会非常难查。3.2 PSLVERR什么时候报错什么时候忍着PSLVERR是APB3新增的错误响应信号。从机在传输过程中发现异常情况比如访问了保留地址、访问方式与硬件设计不符、内部检测到安全性违规就可以在PREADY为高的那个周期把PSLVERR拉高通知桥“这笔传输有问题”。桥收到后会在上游AHB/AXI侧生成ERROR响应CPU通常会产生总线错误异常或返回错误状态。但PSLVERR不是必须实现的。如果一个从机逻辑简单没有可报错的场景直接把它绑0就行。强行实现反而容易出错。我见过一个外设因为地址译码的default分支没有处理访问到保留地址时PREADY一直为低总线直接挂死。关于该不该报错要根据下游软件的习惯来定。一般来说芯片里访问“未实现地址”时返回全0数据、PREADY正常拉高、不报错对软件更友好只有在安全关键场景下才需要PSLVERR把错误暴露给上层异常处理。如果软件是直接访问物理地址的BSP代码错误响应会让系统panic反而不如静默返回0来得稳。还有一点要特别注意PSLVERR必须在传输完成的那个窗口被采样也就是PENABLE有效且PREADY为高的时候。如果你把PSLVERR在SETUP阶段就拉高了桥压根不会理它错误就丢了。3.3 没有突发、没有流水线是落后还是合理APB不支持突发传输不支持流水线这在以性能为导向的AXI面前显得很“落后”。但站在外设的角度看这恰恰是正确取舍。突发传输适合连续地址、高吞吐的场景比如DMA搬运一段连续内存、CPU从DDR连续取指。外设的寄存器地址是离散的每个寄存器相隔4字节或8字节突发没有任何意义。而且外设内部逻辑简单你给它支持突发反而要增加地址自增逻辑、传输计数逻辑面积和功耗全浪费了。流水线也是一样。AXI允许地址通道提前发出后一拍的处理还不必等待前一拍完成这是为了隐藏延迟。APB外设一次访问就是读一个寄存器或写一个寄存器延迟本来就几个周期流水线带来的收益微乎其微。另外APB没有仲裁机制。总线上只有一个主机也就是桥所有外设都是从机。这样一来整个总线上不存在总线请求、总线授权、总线占用权切换这些复杂逻辑。从机不需要处理“别人在占用总线”的情况只要PSEL选中自己就干活没选中就闲着。这种设计把外设侧的控制逻辑压到了最低非常适合低功耗、小面积的芯片场景。4. APB和AXI/AHB怎么在芯片里共存4.1 三种总线横向对比AMBA三种主流总线的定位差异非常清晰特性AXIAHBAPB通道结构5个独立通道可乱序单通道流水线化单通道两拍完成突发传输支持多种突发类型支持固定长度突发不支持多主机仲裁支持支持不支持单主机从机协议复杂度高中低典型应用DDR控制器、高性能IP互联SRAM、DMA、CPU子系统内部UART、GPIO、Timer等外设AXI的优势在带宽和并发通道分离意味着读写可以同时进行地址可以提前发出去多个未完成事务可以乱序返回。AHB是AXI的简化版保留流水线和突发但仍然是单通道读和写不能同时进行。APB则彻底放弃了性能追求把协议复杂度降到最低。实际芯片里这三种总线经常串成一条链CPU用AXI访问DDRDMA挂在AXI上搬运数据CPU子系统内部用AHB连接SRAM和ROM最后通过AHB-to-APB桥接一条APB总线来挂外设。高性能模块上高速总线低速外设统一收编到APB互不干扰。4.2 桥接架构从AXI/AHB到APB的数据怎么落地APB自己没有主机它必须依赖一个桥把上游总线的传输转换成APB时序。桥通常有两种实现方式一种是AHB-to-APB桥常见于老的ARM系统。桥在AHB侧扮演一个从机收到AHB的单次传输后内部状态机启动APB序列先把地址和控制信号放到APB上拉起PSEL进入SETUP下一个周期拉起PENABLE进入ACCESS等待PREADY后返回完成同时给AHB侧回HREADY。另一种是AXI-to-APB桥现代SoC里更常见。AXI侧要处理五个通道的握手关系AW和W通道分别接收地址和写数据APB写传输完成后在B通道发出写响应读传输则是AR通道收地址APB读操作完成后在R通道返回读数据和RVALID。桥设计的核心难点在于等待周期的传递。APB侧PREADY为低时桥必须让上游总线也保持等待AHB侧是拉低HREADYAXI侧则要暂缓发出BVALID或RVALID。如果桥没有正确传递这种反压上游处理器会以为访问已完成实际外设根本没收到数据时序错乱非常隐蔽。对于突发传输桥需要把一次AXI突发拆成多个APB单次传输。比如一次INCR4的写突发桥要依次发起4笔APB写每笔都是独立的SETUPACCESS序列。这种模式下桥内部实际上是一个有限状态机负责“突发计数”和“APB两拍”的组合控制。4.3 实际项目里怎么选总线不同外设该挂什么总线判断标准其实很简单看这个模块的主营业务是什么。如果模块本质是“寄存器配置型”的比如GPIO、Timer、Watchdog、RTC、中断控制器纯配置加状态查询那挂APB就够了。这些模块的数据吞吐率低得可怜APB两个周期的访问延迟完全不影响功能。如果模块有数据面比如以太网MAC、USB控制器、SD卡主机控制器它们内部有大量数据搬运通常设计成两个接口配置面走APB数据面走AXI或AHB。配置面负责寄存器读写数据面负责DMA搬运互不干扰。这是非常经典的架构低功耗和性能兼顾。如果模块本身对延迟极其敏感比如CPU核间通信的Mailbox、需要快速响应的中断控制器GIC那就要认真评估APB的两个周期延迟是否可接受。通常这些低速模块挂APB也没问题但碰到极端时序要求还是值得考虑挂在AHB上。我个人的经验是如果拿不准可以先挂APB跑通功能后续再根据性能瓶颈决定是否迁移到AHB。APB从机的RTL改造到AHB从机核心寄存器读写逻辑基本能复用主要改的是握手时序部分改造成本可控。5. RTL实现从协议到一本能跑的Verilog状态机5.1 从机端状态机三种状态怎么跳APB从机的核心是一个状态机典型实现是IDLE、SETUP、ACCESS三态。下面这个例子用SystemVerilog写了一个基础的APB从机支持一个配置寄存器和一个状态寄存器固定两拍完成读写module apb_slave_example #( parameter int ADDR_WIDTH 16 )( input logic PCLK, input logic PRESETn, input logic PSEL, input logic PENABLE, input logic PWRITE, input logic [ADDR_WIDTH-1:0] PADDR, input logic [31:0] PWDATA, output logic [31:0] PRDATA, output logic PREADY, output logic PSLVERR ); typedef enum logic [1:0] { IDLE 2b00, SETUP 2b01, ACCESS 2b10 } state_e; state_e state, next_state; logic [31:0] reg_ctrl; logic [31:0] reg_status; localparam logic [ADDR_WIDTH-1:0] ADDR_CTRL 16h0000; localparam logic [ADDR_WIDTH-1:0] ADDR_STATUS 16h0004; // 状态机 always_comb begin next_state state; case (state) IDLE: begin if (PSEL !PENABLE) next_state SETUP; end SETUP: begin next_state ACCESS; end ACCESS: begin if (PREADY) next_state PSEL ? SETUP : IDLE; else next_state ACCESS; end default: next_state IDLE; endcase end always_ff (posedge PCLK or negedge PRESETn) begin if (!PRESETn) state IDLE; else state next_state; end // 固定两拍完成不插入等待 assign PREADY 1b1; assign PSLVERR 1b0; // 写数据通路 always_ff (posedge PCLK or negedge PRESETn) begin if (!PRESETn) begin reg_ctrl 32h0; reg_status 32h0; end else if (PSEL PENABLE PREADY PWRITE) begin case (PADDR) ADDR_CTRL: reg_ctrl PWDATA; ADDR_STATUS: reg_status PWDATA; default: ; endcase end end // 读数据通路组合逻辑输出 always_comb begin PRDATA 0; if (PSEL !PWRITE) begin case (PADDR) ADDR_CTRL: PRDATA reg_ctrl; ADDR_STATUS: PRDATA reg_status; default: PRDATA 0; endcase end end endmodule状态机的跳转逻辑里有一个细节ACCESS状态下如果PREADY为高next_state是PSEL ? SETUP : IDLE。这表示如果桥在传输完成后立刻发起了下一笔SETUP也就是PSEL仍然有效但PENABLE已经拉低从机就顺着进入SETUP不浪费周期回到IDLE再回来。这个分支就是背靠背传输的兼容逻辑千万别省。写数据通路的条件里我把PSEL、PENABLE、PREADY、PWRITE四者全部参与判断。PREADY在上面的例子里恒为高看起来没意义但一旦你需要给从机插入等待这个条件就必须带上只在传输真正完成的那个沿把数据写入寄存器否则在PREADY拉低期间误写数据就错了。5.2 外设侧响应逻辑怎么接常见插入等待的姿势真实外设往往不会像上面例子那样固定两拍完成。外设内部可能有自己的状态机比如一个SPI控制器收到“发送数据”命令后要花几十个周期才发完或者一个FIFO写满时不能再接收新数据。这时候PREADY不能恒为高得按实际情况拉低。插入等待的标准做法是在ACCESS阶段判断内部状态。如果内部状态允许采样PREADY输出高如果不允许PREADY输出低同时状态机停在ACCESS不动直到允许条件满足。always_comb begin if (state ACCESS internal_busy) PREADY 1b0; else PREADY 1b1; end这段逻辑的关键在于PREADY本身是组合逻辑从内部busy信号推导出来的不能有寄存器延迟。因为桥采样PREADY是本周期的事你多打一拍外设就意味着多等一个周期整个访问链条变慢。另一个常见场景是读数据需要多拍准备。比如外设内部有一个ADC转换结果CPU访问时需要等待转换完成那么从机可以这样设计第一个ACCESS阶段发现结果还没出来把PREADY拉低内部转换完成后PREADY拉高同时PRDATA输出最新结果桥在此时采样。整个过程从外部看就是一次被拉长的APB访问。PSLVERR的组合逻辑也类似只能在ACCESS阶段根据错误条件产生有效电平其他阶段必须保持0避免把无效错误上报给桥。5.3 测试与仿真验证的关注点APB从机的功能仿真我建议至少覆盖下面这些场景复位后状态机回到IDLEPREADY输出为高PSLVERR输出为低。单笔写操作确认目标寄存器在传输完成的时钟沿写入正确数据。写后读回验证读数据通路能够将寄存器值正确放到PRDATA上。插入PREADY等待周期确认ACCESS状态被保持传输在等待结束后正确完成。背靠背连续读写确认状态机能从ACCESS直接进入下一个SETUP不丢拍。访问未映射地址确认PREADY正常拉高PRDATA返回安全值总线不挂死。验证的时候有一个很重要的采样技巧不要在所有信号变化的同时检查结果而要在PCLK上升沿之后留一个小时间片再断言。否则组合逻辑毛刺会被断言误判为错误。// 伪代码体现验证思路 (posedge PCLK); #1; if (PSEL PENABLE PREADY PWRITE) assert(reg_ctrl expected_data);这个#1就是在时钟沿之后跳过组合逻辑延迟让信号进入稳定态。很多初学验证的人忘了这一步仿真结果一天到晚报错其实功能并没有问题。6. 实操中那些高发问题与排查技巧6.1 高发问题速查表我在多个项目里反复遇到过下面这些APB问题整理成一张速查表遇到类似现象可以直接对着查现象可能原因排查方向访问一挂死CPU卡住PREADY一直为低查看从机状态机是否困在ACCESS检查复位后PREADY是否恢复为高写寄存器不生效写脉冲采错阶段检查写条件是否包含PENABLE PREADY是否在SETUP阶段就写入读数据为X或不稳定在SETUP阶段采样PRDATA确认桥只在ACCESS完成时采样从机读数据在ACCESS阶段才须稳定复位后第一笔访问失败复位未覆盖PREADY/PSLVERR输出检查PRESETn是否复位所有输出寄存器PREADY复位值是否为高背靠背传输少一笔状态机ACCESS只能回IDLE加上PSEL仍然有效时直接转SETUP的分支访问保留地址挂死未映射地址没有拉PREADY译码default分支必须返回PREADY1总线错误误报PSLVERR在非ACCESS阶段被拉高确认PSLVERR只在PREADY采样窗口有效这些问题的共同根源几乎都是对“采样时机”的理解不够精确。APB的信号是电平型的什么时候有效、什么时候被采样规则比AXI简单得多但你只要在错误的时间沿上放了数据排查起来往往比AXI还费劲。6.2 排查思路与工具要点排查APB问题第一件事不是翻代码而是在波形里盯三个信号PSEL、PENABLE、PREADY。这三者共同构成一次传输的骨架。只要看清这三个信号在每个时钟沿的状态就能判断出状态机走到哪一步。一个清晰的排查顺序是这样找到PCLK上升沿观察PSEL是否按预期拉高。跟随PENABLE从0到1的变化确认进入ACCESS阶段。在PCLK上升沿采样PREADY如果为低说明从机要求等待如果持续为低立即检查从机内部的busy逻辑。传输结束时观察PSEL如何变化——是掉到0回到IDLE还是保持1直接进入下一个SETUP。实际项目里APB问题常常不是出在协议本身而是出在时钟域和复位上。APB外设的时钟通常是可关断的低功耗模式下PCLK停掉这时候总线上可能有残留的PSEL或PENABLE状态恢复时钟后如果从机没有正确复位就会出现第一笔访问异常。处理方式是在时钟关断之前确保总线回到IDLE或者在外设时钟域上加一个“总线上电有效”标志时钟恢复后再允许总线访问。6.3 一些实打实的经验做APB从机这么多年我有几条靠踩坑换来的经验分享在这里。第一PREADY默认拉高永远是最安全的选择。多数从机其实并不需要插入等待你只要在确实需要时才把它拉低就可以避免一大类总线挂死问题。PREADY常低比常高可怕得多——一次性让CPU访问卡死你连调试入口都找不到。第二不要把PREADY和PSLVERR打成寄存器输出。这两个信号天然应该由组合逻辑驱动一旦你为了“同步”加了一级寄存器就会把等待状态和错误状态都顺延一个周期协议窗口直接错位对端桥根本采不到正确的值。第三地址译码一定要有完备的default分支。主译码由桥完成但从机收到PSEL后还需要对PADDR的低位偏移做二次译码。这个二次译码的default分支必须处理返回0数据、PREADY为高、不写任何寄存器。否则你写一个驱动习惯性去读某个没实现的偏移地址总线一下子就挂了排查半天毫无头绪。第四哪怕从机逻辑再简单也要先写一个兼容背靠背传输的状态机。你的外设现在可能只被CPU偶尔访问一次但未来可能会接DMADMA驱动会连续读写外设寄存器如果状态机不支持ACCESS直接转SETUP到时候改起来就不是加一个case分支那么简单了。第五仿真验证时一定要覆盖“PREADY拉低至少一个周期”的场景。你可以设计一个测试模式强制从机在第一次访问时拉低PREADY验证桥真的能等待、状态机真的能保持ACCESS。很多从机代码看着没问题一上FPGA带真实外设就露馅就是因为仿真时PREADY从来没拉低过等待路径完全没有被验证到。APB协议本身不难难的是在真实系统里把每个时序窗口都对齐。你只要把“采样发生在PENABLE有效且PREADY为高的那个时钟沿”这句话刻进脑子里绝大多数问题都能迎刃而解。