第四章:顶层模块架构设计

好,咱们今天聊聊SPI控制器的顶层架构。说实话,这块内容是我觉得整个设计里最需要想清楚的部分。你想想看,模块划分不好,后面改起来那叫一个痛苦。我在项目里见过太多人上来就写代码,写到一半发现时钟域没理清,寄存器接口又跟总线对不上,最后只能推倒重来。

4.1 模块划分:别让代码变成一团乱麻

我个人习惯,先把SPI控制器拆成几个功能块。说白了,就是按职责分家。一个典型的SPI控制器,我通常会分成这么几块:

  • 总线接口模块:负责跟APB或AHB-Lite打交道
  • 寄存器模块:管理所有控制/状态寄存器
  • 时钟分频模块:生成SPI的SCK时钟
  • 发送FIFO与接收FIFO:数据缓冲,这个很关键
  • 主状态机模块:控制SPI传输的整个流程
  • 移位寄存器模块:负责数据的串并转换
  • IO接口模块:处理片选、时钟、数据线的输出

嗯,这里要注意,FIFO深度得根据你的应用场景来定。我曾经做过一个项目,客户要求连续传输大块数据,结果FIFO深度只配了8,导致频繁溢出。后来改成64深度,问题就解决了。

核心原则:每个模块只做一件事,并且做好。模块间的接口要尽量简单,信号数量控制在合理范围内。

4.2 时钟域与复位策略:别让时序问题坑了你

时钟域这块,我得好好说说。SPI控制器通常涉及两个时钟域:一个是系统总线时钟域(比如APB的PCLK),另一个是SPI自身的SCK时钟域。这两个时钟频率可能完全不同,甚至异步。

我建议的做法是:

  • 总线接口和寄存器模块跑在PCLK时钟域
  • SPI核心逻辑(状态机、移位寄存器)跑在SCK时钟域
  • 两个时钟域之间用双级同步器做跨时钟域处理

为什么会这样?因为如果你把所有逻辑都放在PCLK下,那SCK的生成和采样就会受限于PCLK的频率,灵活性大打折扣。反过来,如果全放在SCK下,那总线访问又可能跟不上。

避坑指南:我曾经在一个项目里,直接把寄存器输出信号送到了SCK时钟域,没做同步。结果仿真没问题,上板子就随机出错。查了两天才发现是跨时钟域的问题。从那以后,我所有跨时钟域信号都老老实实加同步器。

复位策略呢?我一般用异步复位、同步释放的方式。这样既保证了复位信号的及时响应,又避免了异步复位带来的时序问题。复位信号覆盖所有寄存器,但FIFO的复位可以单独控制,方便调试。

4.3 寄存器接口规划:APB还是AHB-Lite?

寄存器接口这块,其实取决于你的系统总线。APB适合低带宽、控制型的访问,AHB-Lite则适合需要高吞吐的场景。我个人更常用APB,因为SPI控制器的寄存器访问频率通常不高,APB的简单性正好合适。

一个典型的SPI控制器寄存器列表大概长这样:

地址偏移 寄存器名称 读写属性 描述
0x00 CR R/W 控制寄存器:使能、模式选择、LSB/MSB等
0x04 SR R 状态寄存器:忙标志、FIFO空/满等
0x08 DR R/W 数据寄存器:读写FIFO
0x0C CLKDIV R/W 时钟分频系数
0x10 CSR R/W 片选控制寄存器

你想想看,如果每个寄存器都单独做一套读写逻辑,那代码量会很大。我习惯写一个统一的寄存器读写模块,通过地址译码来分发读写操作。这样既简洁,又容易维护。

小技巧:在寄存器模块里,我通常会加一个软复位位。这样软件可以在不触发硬件复位的情况下,重置SPI控制器的内部状态。调试的时候特别有用。

4.4 顶层架构图

下面这张图是我画的SPI控制器顶层架构。你可以看到各个模块之间的连接关系,以及时钟域的分界。

时钟域边界 APB接口 寄存器模块 时钟分频 发送FIFO 接收FIFO 主状态机 移位寄存器 IO接口模块 (SCK, MOSI, MISO, CS) 数据/控制信号 状态/数据 时钟配置 PCLK时钟域 SCK时钟域

从图上你可以看到,APB接口和寄存器模块在PCLK时钟域,而FIFO、状态机、移位寄存器在SCK时钟域。两个时钟域之间通过同步器交换数据。IO接口模块则负责把内部信号驱动到芯片引脚上。

经验之谈:我建议在顶层模块里,把时钟域相关的信号名加上后缀,比如用_pclk和_sck来区分。这样看代码的时候一目了然,不容易搞混。

好了,关于顶层架构设计,我就说这么多。记住一句话:架构设计花的时间越多,后面写代码和调试就越省心。别急着动手写RTL,先把架构想清楚。

可可精品资料库