芯片项目里,凡是跟低速外设打交道的工程师,几乎绕不开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 | 复位 | 输入 | 低有效异步复位,必须保证所有状态可回到IDLE |
| PADDR | 主机(桥) | 输入 | 地址总线,SETUP阶段开始有效,ACCESS期间保持 |
| PSELx | 地址译码 | 输入 | 从机选择信号,由桥根据地址译码产生 |
| PENABLE | 主机(桥) | 输入 | 传输使能,SETUP为0,ACCESS为1 |
| PWRITE | 主机(桥) | 输入 | 读写方向,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拍T0:PCLK上升沿后,桥把PADDR驱动为目标地址,PWRITE拉高,PSEL拉高,PENABLE保持低。外设看到PSEL有效且PENABLE为低,知道新传输开始,状态从IDLE进入SETUP。
- 第2拍T1:PCLK上升沿,桥把PENABLE拉高,PWDATA已经带上有效数据。外设此时看到PSEL为高、PENABLE为高,进入ACCESS阶段,开始准备写操作。
- 第3拍T2:PCLK上升沿,桥采样PREADY。如果从机没有插入等待,PREADY为高,本次传输完成。桥可以撤掉PSEL回到IDLE,也可以紧接着把下一笔地址放上总线,直接发起新的SETUP。
如果从机内部需要更长时间才能完成写操作,比如内部有个同步FIFO要处理,或者时钟域跨越还没结束,那么从机可以在ACCESS阶段把PREADY拉低。桥看到PREADY为低,就保持当前状态不动,一直等到PREADY拉高后才认为传输完成。
用表格看会更直观:
| 时钟沿 | 状态 | PSEL | PENABLE | PREADY | 发生的事 |
|---|---|---|---|---|---|
| T0后 | SETUP | 1 | 0 | 高 | 地址、方向就绪 |
| T1后 | ACCESS | 1 | 1 | 高 | 数据有效,从机开始处理 |
| T2后 | 完成 | 0 | 0 | 高 | 桥采样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切到reg4,PENABLE拉低,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三种主流总线的定位差异非常清晰:
| 特性 | AXI | AHB | APB |
|---|---|---|---|
| 通道结构 | 5个独立通道,可乱序 | 单通道,流水线化 | 单通道,两拍完成 |
| 突发传输 | 支持多种突发类型 | 支持固定长度突发 | 不支持 |
| 多主机仲裁 | 支持 | 支持 | 不支持,单主机 |
| 从机协议复杂度 | 高 | 中 | 低 |
| 典型应用 | DDR控制器、高性能IP互联 | SRAM、DMA、CPU子系统内部 | UART、GPIO、Timer等外设 |
AXI的优势在带宽和并发,通道分离意味着读写可以同时进行,地址可以提前发出去,多个未完成事务可以乱序返回。AHB是AXI的简化版,保留流水线和突发,但仍然是单通道,读和写不能同时进行。APB则彻底放弃了性能追求,把协议复杂度降到最低。
实际芯片里,这三种总线经常串成一条链:CPU用AXI访问DDR,DMA挂在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侧是拉低HREADY,AXI侧则要暂缓发出BVALID或RVALID。如果桥没有正确传递这种反压,上游处理器会以为访问已完成,实际外设根本没收到数据,时序错乱非常隐蔽。
对于突发传输,桥需要把一次AXI突发拆成多个APB单次传输。比如一次INCR4的写突发,桥要依次发起4笔APB写,每笔都是独立的SETUP+ACCESS序列。这种模式下,桥内部实际上是一个有限状态机,负责“突发计数”和“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 = 2'b00, SETUP = 2'b01, ACCESS = 2'b10 } 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 = 16'h0000; localparam logic [ADDR_WIDTH-1:0] ADDR_STATUS = 16'h0004; // 状态机 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 = 1'b1; assign PSLVERR = 1'b0; // 写数据通路 always_ff @(posedge PCLK or negedge PRESETn) begin if (!PRESETn) begin reg_ctrl <= 32'h0; reg_status <= 32'h0; 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 = 1'b0; else PREADY = 1'b1; end这段逻辑的关键在于,PREADY本身是组合逻辑从内部busy信号推导出来的,不能有寄存器延迟。因为桥采样PREADY是本周期的事,你多打一拍,外设就意味着多等一个周期,整个访问链条变慢。
另一个常见场景是读数据需要多拍准备。比如外设内部有一个ADC转换结果,CPU访问时需要等待转换完成,那么从机可以这样设计:第一个ACCESS阶段发现结果还没出来,把PREADY拉低;内部转换完成后,PREADY拉高,同时PRDATA输出最新结果,桥在此时采样。整个过程从外部看就是一次被拉长的APB访问。
PSLVERR的组合逻辑也类似,只能在ACCESS阶段根据错误条件产生有效电平,其他阶段必须保持0,避免把无效错误上报给桥。
5.3 测试与仿真验证的关注点
APB从机的功能仿真,我建议至少覆盖下面这些场景:
- 复位后状态机回到IDLE,PREADY输出为高,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分支必须返回PREADY=1 |
| 总线错误误报 | 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偶尔访问一次,但未来可能会接DMA,DMA驱动会连续读写外设寄存器,如果状态机不支持ACCESS直接转SETUP,到时候改起来就不是加一个case分支那么简单了。
第五,仿真验证时一定要覆盖“PREADY拉低至少一个周期”的场景。你可以设计一个测试模式,强制从机在第一次访问时拉低PREADY,验证桥真的能等待、状态机真的能保持ACCESS。很多从机代码看着没问题,一上FPGA带真实外设就露馅,就是因为仿真时PREADY从来没拉低过,等待路径完全没有被验证到。
APB协议本身不难,难的是在真实系统里把每个时序窗口都对齐。你只要把“采样发生在PENABLE有效且PREADY为高的那个时钟沿”这句话刻进脑子里,绝大多数问题都能迎刃而解。