第4章:AHB总线详解

4.1 AHB总线架构与组件

AHB,全称Advanced High-performance Bus。说白了,它是ARM公司AMBA总线体系里的「高速公路」。我刚开始接触SoC设计时,总觉得总线这东西不就是连来连去嘛。后来踩了坑才明白——AHB的设计哲学,其实是在性能和复杂度之间找平衡。

一个典型的AHB系统,由这么几个核心组件构成:

  • 主机(Master):发起传输的模块。比如CPU、DMA控制器。我习惯把主机想象成「发号施令的人」。
  • 从机(Slave):响应传输的模块。比如SRAM、外设寄存器。它们是被动方,听主机指挥。
  • 仲裁器(Arbiter):决定哪个主机能占用总线。嗯,这里有个坑——仲裁策略选不好,系统性能直接腰斩。
  • 译码器(Decoder):根据地址判断该访问哪个从机。说白了就是「指路牌」。
  • 多路选择器(Mux):把多个主机的信号和多个从机的响应,正确地连到一起。

你想想看,如果总线上只有一个主机,那仲裁器和Mux都可以省掉。但现实SoC里,CPU、GPU、DSP、DMA抢着用总线,没有仲裁器就乱套了。

4.2 AHB传输操作

4.2.1 单次传输

单次传输是最基本的操作。一次传一个数据,一拍地址,一拍数据。时序上分两个阶段:

  • 地址阶段:主机把地址、控制信号(比如读写方向、传输大小)放到总线上。
  • 数据阶段:从机响应,数据在总线上传输。如果从机来不及处理,可以插入等待周期(HREADY拉低)。

我在项目中遇到过一个问题:某个从机响应特别慢,每次都要等好几个周期。结果整个总线都被拖慢了。后来我加了个FIFO做缓冲,才把问题解决。

4.2.2 突发传输

突发传输,说白了就是一次地址,连续传多笔数据。AHB支持4拍、8拍、16拍,以及未定长度的增量突发。

突发类型有几种:

  • INCR4/INCR8/INCR16:固定长度的增量突发。地址依次递增。
  • WRAP4/WRAP8/WRAP16:回环突发。地址递增到边界后折回。常用于Cache line fill。
  • INCR:未定长度突发。从机不知道什么时候结束,直到主机发LAST信号。

举个例子,CPU做Cache line fill时,通常用WRAP4突发。一次读4个字,地址从某个对齐边界开始,到边界后折回。这样做的好处是——地址计算简单,硬件实现也省资源。

关键点:突发传输中,地址只发一次,后续数据按固定规则递增。这能大幅减少地址阶段的占用,提升总线利用率。

4.2.3 拆分传输

拆分传输是AHB里一个比较高级的特性。当从机需要很长时间准备数据时,它可以把当前传输「拆开」,让总线先去服务其他主机。

具体流程是这样的:

  1. 主机发起传输,从机回复SPLIT响应。
  2. 仲裁器把总线让给其他主机。
  3. 从机准备好后,通知仲裁器。
  4. 仲裁器重新把总线交给原主机,继续未完成的传输。

我曾经在一个多主机的视频处理芯片里用过SPLIT。当时有个DMA要从DDR读数据,DDR延迟很大。如果不做SPLIT,CPU就得一直等着。用了SPLIT后,CPU可以在DMA等数据的时候继续干活,系统吞吐量提升了将近30%。

注意:SPLIT虽然好用,但实现起来比较复杂。仲裁器需要记住哪个主机被拆分了,从机也要有额外的逻辑来通知仲裁器。小系统不建议用,性价比不高。

4.3 AHB仲裁与优先级

仲裁,说白了就是「谁先上」的问题。AHB支持多种仲裁策略:

  • 固定优先级:每个主机有固定优先级。高优先级的主机永远先获得总线。简单,但不公平。
  • 轮询(Round-Robin):主机轮流使用总线。公平,但高优先级任务可能被延迟。
  • 混合策略:比如高优先级用固定,低优先级用轮询。我比较喜欢这种,灵活。

仲裁的时机也很关键。AHB规定,仲裁结果在地址阶段决定。也就是说,当前传输的数据阶段还没结束,仲裁器就已经在决定下一个传输归谁了。这叫「流水线仲裁」,能提高总线利用率。

嗯,这里要注意——仲裁器不能频繁切换主机。如果每个传输都换人,总线效率反而会下降。因为每次切换都有「气泡」(空闲周期)。我建议在仲裁器里加个「保持计数器」,让同一个主机连续做几次传输再切换。

4.4 AHB从机与主机接口设计

4.4.1 主机接口

设计AHB主机接口,核心是处理好这几个信号:

信号 方向 说明
HADDR 输出 地址总线,32位
HWDATA 输出 写数据总线
HRDATA 输入 读数据总线
HWRITE 输出 1表示写,0表示读
HSIZE 输出 传输大小(字节、半字、字)
HBURST 输出 突发类型
HREADY 输入 从机准备好信号
HRESP 输入 从机响应(OKAY、ERROR、RETRY、SPLIT)

设计主机接口时,我习惯用状态机来控制。状态机至少要有这几个状态:

  • IDLE:空闲,没有传输请求。
  • ADDR:发送地址和控制信号。
  • DATA:等待数据阶段完成。
  • WAIT:从机插入等待周期。

代码示例(简化版):

// AHB Master FSM - 简化版
always @(posedge HCLK or negedge HRESETn) begin
  if (!HRESETn) begin
    state <= IDLE;
  end else begin
    case (state)
      IDLE: if (req) state <= ADDR;
      ADDR: state <= DATA;
      DATA: if (HREADY) begin
              if (burst_done) state <= IDLE;
              else state <= ADDR; // 继续下一拍突发
            end
    endcase
  end
end

个人经验:设计主机接口时,别忘了处理从机的ERROR响应。我曾经有个项目,从机返回ERROR后主机没做任何处理,结果数据写错了都不知道。建议至少加个中断或者状态寄存器,记录错误信息。

4.4.2 从机接口

从机接口相对简单一些。核心是响应主机的请求,返回数据和状态。

设计从机接口时,有几个要点:

  • 地址译码:判断当前访问是否在自己的地址范围内。
  • 等待机制:如果来不及响应,拉低HREADY插入等待周期。
  • 响应生成:正常返回OKAY,出错返回ERROR。
  • 数据对齐:根据HSIZE和HADDR,正确选择数据字节。

举个例子,一个简单的SRAM从机接口:

// AHB Slave - SRAM接口简化版
assign HSEL = (HADDR >= BASE_ADDR) && (HADDR < BASE_ADDR + SIZE);
assign HREADY = 1'b1; // 假设SRAM可以零等待
assign HRESP = OKAY;

// 写操作
always @(posedge HCLK) begin
  if (HSEL && HWRITE && HREADY)
    mem[HADDR[ADDR_WIDTH-1:2]] <= HWDATA;
end

// 读操作
assign HRDATA = (HSEL && !HWRITE) ? mem[HADDR[ADDR_WIDTH-1:2]] : 32'b0;

避坑指南:我曾经设计过一个从机,忘了处理HSIZE信号。结果主机发半字写操作时,从机把整个字都写了。相邻的寄存器被意外修改,查了两天才找到原因。记住——一定要根据HSIZE和HADDR的低位,正确屏蔽不需要的字节。

好了,AHB总线的核心内容就这些。下一章我们会讲AHB的时序分析和常见问题排查。到时候我会分享一个实际项目中的总线死锁案例,挺有意思的。