4、SPI控制器顶层架构:系统框图、AHB/APB总线接口、时钟域划分、FIFO深度设计考量

好,咱们今天聊聊SPI控制器的顶层架构。这部分内容,说白了就是整个芯片的“骨架”和“血管”。你想想看,一个SPI模块要正常工作,它得跟CPU打交道,得跟外部设备通信,还得处理好数据流。这三件事,分别对应着总线接口、SPI物理协议和内部的数据通路。

我个人习惯,在设计顶层架构之前,先画一张系统框图。这张图能帮你理清所有模块的边界和交互关系。来,我们先看一张我画的SPI控制器顶层架构图。

HCLK 时钟域 (APB/AHB) SCLK 时钟域 (SPI Core) APB 从机接口 PADDR, PWDATA, PRDATA PSEL, PENABLE, PWRITE 寄存器组 控制/状态/数据寄存器 时钟分频器 HCLK -> SCLK 异步 FIFO 深度: 8/16/32 写指针: HCLK 读指针: SCLK SPI 核心 状态机 (FSM) 移位寄存器 输出驱动 sclk, mosi, cs_n 输入采样 miso 同步与采样 写数据 读数据 分频系数 SCLK 读回数据 外部引脚 sclk, mosi, miso, cs 时钟域边界

嗯,这张图里有两个虚线框,代表两个不同的时钟域。左边是HCLK域,右边是SCLK域。中间那个FIFO,就是连接两个世界的桥梁。这个设计思路,我在好几个项目里都用过,效果不错。

4.1 系统框图与模块划分

从上面这张图你能看到,SPI控制器主要包含这几个模块:

  • 总线接口模块:负责跟AHB或APB总线握手。我个人更倾向于用APB,因为它协议简单,面积小。但如果你需要高带宽的配置,比如DMA方式搬运数据,那AHB是更好的选择。
  • 寄存器组:所有可编程的配置位都在这儿。包括控制寄存器、状态寄存器、数据寄存器。我记得有一次,一个同事把数据寄存器的地址译码搞错了,结果CPU写进去的数据总是对不上号。排查了半天,最后发现是地址位宽少算了一位。
  • 时钟分频器:从系统时钟HCLK分频出SPI的串行时钟SCLK。这里要注意,分频系数必须是偶数,否则占空比会出问题。
  • 异步FIFO:跨时钟域数据交换的核心。后面我会详细讲它的深度设计。
  • SPI核心状态机:这是整个模块的大脑。它控制着数据移位的节奏、片选信号的拉高拉低、以及传输模式的切换。
  • 输入/输出驱动:负责把内部信号驱动到芯片引脚上,同时把外部进来的信号同步到内部时钟域。

4.2 AHB/APB总线接口设计

总线接口这块,其实没什么花哨的。关键是要把协议翻译成寄存器读写操作。我拿APB举个例子,它的状态机就三个状态:IDLE、SETUP、ACCESS。

// APB 从机接口状态机示例
always @(posedge HCLK or negedge HRESETn) begin
    if (!HRESETn) begin
        apb_state <= IDLE;
        reg_we     <= 1'b0;
        reg_addr   <= 'b0;
        reg_wdata  <= 'b0;
    end else begin
        case (apb_state)
            IDLE: begin
                if (PSEL & !PENABLE) begin
                    apb_state <= SETUP;
                    reg_addr  <= PADDR;
                    reg_wdata <= PWDATA;
                    reg_we    <= PWRITE;
                end
            end
            SETUP: begin
                apb_state <= ACCESS;
            end
            ACCESS: begin
                // 这里产生寄存器写使能或读数据
                if (PENABLE & PREADY) begin
                    apb_state <= IDLE;
                end
            end
        endcase
    end
end

这里有个小细节:PREADY信号。如果你的寄存器读取需要多个周期(比如从FIFO读数据),你得拉低PREADY来插入等待周期。否则,总线会认为你一次就完成了传输。

我的经验: 对于APB接口,尽量让PREADY在ACCESS状态的下一个周期就拉高。除非你确实需要等待FIFO的数据。否则,过多的等待周期会拖慢整个系统的性能。

4.3 时钟域划分与同步策略

时钟域划分,这是跨时钟域设计的核心问题。SPI控制器天然就有两个时钟:系统时钟HCLK和串行时钟SCLK。HCLK通常跑得很快(比如100MHz),而SCLK取决于你的SPI通信速率(可能只有10MHz甚至更低)。

这两个时钟之间没有任何相位关系。所以,所有跨时钟域的信号都必须经过同步处理。我常用的方法有两种:

  1. 双级触发器同步:用于同步单比特控制信号,比如中断请求、FIFO空满标志。
  2. 异步FIFO:用于传输多比特数据,比如你要发送的字节。

我曾经在一个项目里犯过傻——直接用双级触发器去同步一个8位的数据总线。结果数据偶尔会错位,因为每个比特到达的时间不一样。后来改成异步FIFO,问题就解决了。说白了,多比特数据必须用FIFO或者握手协议,不能偷懒。

注意: 千万不要在SCLK域里直接采样HCLK域的信号!除非你做了充分的同步处理。否则,亚稳态会让你抓狂。我见过一个案例,因为没做同步,芯片在高温下偶尔会误触发SPI传输,排查了两个月才找到根因。

4.4 FIFO深度设计考量

FIFO深度,这是个经典问题。深度太浅,容易溢出;深度太深,浪费面积。那到底选多深合适?

我个人习惯从两个角度来考虑:

  • CPU响应延迟:假设CPU每100个时钟周期才能响应一次中断并读取FIFO。那么在这100个周期内,SPI可能已经接收了N个字节。FIFO深度至少要比N大,否则数据会丢。
  • SPI传输速率:如果SCLK是HCLK的1/10,那么每10个HCLK周期,SPI才能移出一个比特。一个字节需要80个HCLK周期。如果CPU响应延迟是100个周期,那FIFO深度设为2就够用了。

我整理了一个表格,方便你参考:

应用场景 HCLK频率 SCLK频率 CPU响应延迟 推荐FIFO深度
低速传感器 50 MHz 1 MHz 200 cycles 4
中速ADC 100 MHz 10 MHz 100 cycles 8
高速Flash 200 MHz 50 MHz 50 cycles 16
DMA模式 200 MHz 100 MHz DMA延迟 32

你看,对于低速场景,深度4就绰绰有余了。但对于高速Flash,深度16是起步。如果你用DMA模式,深度32会更安全,因为DMA的启动延迟比CPU中断要长一些。

核心原则: FIFO深度 = (CPU/DMA最大响应延迟 / 每字节传输所需时钟周期数) + 2。加2是为了留点余量,防止边界情况。

嗯,关于顶层架构,我就讲这么多。记住,系统框图是设计的蓝图,时钟域划分是安全的保障,FIFO深度是性能的平衡点。把这三点想清楚,你的SPI控制器就成功了一大半。

可可精品资料库