2. SPI控制器架构设计:顶层模块划分、AHB/APB总线接口设计、时钟域划分与同步策略、寄存器列表定义

好,咱们进入正题。这一章聊的是SPI控制器的骨架——架构设计。说白了,就是决定这个模块长什么样,各个部分怎么分工,数据怎么流动。

我个人习惯,在设计任何IP之前,先画一张顶层框图。这张图能帮你理清思路,也能让团队里的其他人一眼看懂你的设计意图。来,先看这张我画的架构图:

SPI控制器顶层架构图 AHB/APB总线接口 寄存器读写控制 地址译码 & 数据对齐 寄存器模块 控制/状态/数据寄存器 中断控制逻辑 时钟域同步 双级同步器 握手协议 SPI核心控制 主/从模式状态机 时钟极性/相位控制 数据移位逻辑 TX FIFO 8x32bit RX FIFO 8x32bit IO Pad sclk_o mosi_o miso_i cs_o[3:0] 中断输出 HCLK域 异步桥接 SCLK域 芯片管脚

2.1 顶层模块划分

从图上你能看到,我把SPI控制器分成了几个核心模块。为什么这么分?其实核心原则就一条——高内聚、低耦合。每个模块只干自己那摊事,模块之间通过清晰的接口通信。

我列一下主要模块的职责:

  • AHB/APB总线接口模块:负责和系统总线打交道。CPU通过它来读写寄存器。说白了,这就是个"翻译官",把总线协议翻译成寄存器读写操作。
  • 寄存器模块:存放所有配置、状态和数据。包括控制寄存器、状态寄存器、数据寄存器,还有中断相关的逻辑。
  • 时钟域同步模块:处理跨时钟域的数据传递。这个模块虽然不大,但要是出问题,整个芯片都可能挂掉。
  • SPI核心控制模块:这是整个控制器的"大脑"。它负责状态机跳转、数据移位、时钟极性和相位的控制。
  • 发送/接收FIFO:缓冲数据用的。CPU可以一次性写入多个字节,然后SPI慢慢发出去,不用CPU一直等着。
  • IO Pad模块:处理芯片管脚的电平信号。包括SCLK、MOSI、MISO、片选信号CS,还有中断输出。
我的经验: 模块划分时,一定要考虑验证的便利性。每个模块的接口越简单,写testbench就越轻松。我曾经见过一个项目,把所有逻辑塞进一个模块里,结果验证工程师天天加班改激励,那叫一个痛苦。

2.2 AHB/APB总线接口设计

总线接口这块,我建议用APB。为什么?因为SPI控制器的寄存器访问频率不高,APB协议简单,面积也小。当然,如果你的系统总线是AHB,那也可以直接挂AHB从接口。

APB接口的核心信号就这几个:

信号名方向描述
PCLK输入APB时钟,通常和HCLK同源
PRESETn输入异步复位,低有效
PSEL输入从设备选择信号
PENABLE输入使能信号,高电平表示有效传输
PWRITE输入读写指示,1为写,0为读
PADDR输入地址总线,32位
PWDATA输入写数据总线,32位
PRDATA输出读数据总线,32位
PREADY输出准备好信号,表示可以完成传输

APB的传输时序其实很简单。PSEL拉高后,PENABLE拉高,一个周期内完成读写。我一般会在PREADY上做个小处理——如果寄存器还没准备好,就拉低PREADY插入等待周期。不过SPI控制器通常不需要等待,直接一个周期搞定。

避坑指南: 我曾经遇到过一个bug,APB地址没有按32位对齐。PADDR[1:0]不是00,导致寄存器地址错位。后来我在地址译码时强制忽略低2位,问题就解决了。记住,APB地址必须按数据宽度对齐。

2.3 时钟域划分与同步策略

时钟域划分,这是SPI控制器设计里最容易出问题的地方。你想想看,系统总线跑在HCLK上,SPI接口跑在SCLK上,这两个时钟频率可能完全不同,甚至SCLK是外部输入的异步时钟。

我一般把整个设计分成三个时钟域:

  1. HCLK域:总线接口、寄存器模块、中断逻辑。这些逻辑和系统时钟同步。
  2. SCLK域:SPI核心控制、移位寄存器、FIFO的读写指针。这些逻辑和SPI时钟同步。
  3. 异步桥接:跨时钟域的数据传递,包括控制信号和状态信号。

跨时钟域同步,我常用的方法就两种:

  • 双级同步器:用于单比特控制信号。比如使能信号、中断请求。两个触发器串联,降低亚稳态概率。
  • 异步FIFO:用于多比特数据。比如从HCLK域写到SCLK域的发送数据。用格雷码指针和双级同步器实现。

这里有个细节要注意——同步器的建立时间。双级同步器需要两个时钟周期才能完成同步。所以设计状态机时,一定要留出足够的等待时间。我习惯在状态机里加一个"同步等待"状态,确保信号稳定后再操作。

警告: 千万不要用组合逻辑直接跨时钟域!我曾经见过一个新手,把HCLK域的寄存器输出直接连到SCLK域的组合逻辑上,结果仿真没问题,流片回来就随机出错。查了两个月才发现是亚稳态问题。跨时钟域必须用同步器,这是铁律。

2.4 寄存器列表定义

寄存器定义,说白了就是给CPU一个操作SPI控制器的接口。我一般把寄存器分成三类:控制类、状态类、数据类。

下面是我常用的寄存器列表:

偏移地址寄存器名称类型位宽描述
0x00SPI_CR1读写32控制寄存器1:使能、主从模式、时钟极性/相位、波特率分频
0x04SPI_CR2读写32控制寄存器2:中断使能、FIFO复位、数据帧格式
0x08SPI_SR只读32状态寄存器:忙标志、发送空、接收非空、溢出错误
0x0CSPI_DR读写32数据寄存器:发送/接收数据,共用地址
0x10SPI_CRCPR读写16CRC多项式寄存器(可选)
0x14SPI_RXCRCR只读16接收CRC寄存器(可选)
0x18SPI_TXCRCR只读16发送CRC寄存器(可选)
0x1CSPI_IER读写32中断使能寄存器
0x20SPI_IFR读写32中断标志寄存器,写1清除

控制寄存器SPI_CR1的位定义,我习惯这样安排:

位域名称描述
[0]SPESPI使能,1使能,0关闭
[1]MSTR主从模式,1主模式,0从模式
[3:2]CPOL_CPHA时钟极性/相位:00模式0,01模式1,10模式2,11模式3
[7:4]BR波特率分频:0000=2分频,0001=4分频,...,1111=256分频
[8]LSBFIRST数据顺序:0 MSB先发,1 LSB先发
[9]SSM软件片选使能
[10]SSI软件片选值
[31:11]RESERVED保留位,读返回0

状态寄存器SPI_SR的位定义:

位域名称描述
[0]BSY忙标志,1表示正在传输
[1]TXE发送FIFO空,1为空
[2]RXNE接收FIFO非空,1有数据可读
[3]OVR溢出错误,接收FIFO满时又有新数据
[4]MODF模式错误,多主冲突
[5]CRCERRCRC错误
[31:6]RESERVED保留位
我的习惯: 数据寄存器SPI_DR我设计成读写共用地址。写操作时数据进入TX FIFO,读操作时从RX FIFO取出数据。这样CPU操作起来很方便,一个地址搞定收发。不过要注意,读空FIFO或写满FIFO时要有保护机制,不然会出问题。

嗯,寄存器定义这块,我建议你留一些保留位。为什么?因为后续可能增加功能。比如加个DMA支持、加个多片选控制。保留位就是给未来留的"后门"。我吃过这个亏——第一次设计时寄存器位全用满了,后来加功能只能改地址映射,兼容性搞得一团糟。

好了,这一章的内容就这些。架构设计是基础,基础打好了,后面的实现和验证才能顺风顺水。


可可精品资料库