SoC顶层(Pinsec)

简介

Pinsec 是一个专为FPGA设计的小型 SoC。它可以在SpinalHDL库中找到,并且可以在 这里 找到一些文档

它的顶层实现是一个有趣的例子,因为它混合了一些设计模式,使其非常容易修改。可以轻松实现向总线结构添加新的主设备或新的外设。

可以在以下链接中查阅顶层实现:https://github.com/SpinalHDL/SpinalHDL/blob/master/lib/src/main/scala/spinal/lib/soc/pinsec/Pinsec.scala

这是Pinsec顶层硬件图:

../../../_images/pinsec_hardware.svg

定义所有IO

val io = new Bundle {
  // Clocks / reset
  val asyncReset = in Bool()
  val axiClk     = in Bool()
  val vgaClk     = in Bool()

  // Main components IO
  val jtag       = slave(Jtag())
  val sdram      = master(SdramInterface(IS42x320D.layout))

  // Peripherals IO
  val gpioA      = master(TriStateArray(32 bits))   // Each pin has an individual output enable control
  val gpioB      = master(TriStateArray(32 bits))
  val uart       = master(Uart())
  val vga        = master(Vga(RgbConfig(5,6,5)))
}

时钟和复位

Pinsec有三个时钟输入:

  • axiClock

  • vgaClock

  • jtag.tck

以及一个复位输入:

  • asyncReset

最终将给出5个时钟域(ClockDomain)(时钟/复位对):

名称

时钟

描述

resetCtrlClockDomain

axiClock

由复位控制器使用,该时钟域的触发器由FPGA比特流初始化

axiClockDomain

axiClock

由连接到AXI和APB互连的所有组件使用

coreClockDomain

axiClock

与axiClockDomain的唯一区别是,复位也可以通过调试模块控制

vgaClockDomain

vgaClock

被VGA控制器后端用作像素时钟

jtagClockDomain

jtag.tck

用于为JTAG控制器的前端提供时钟

复位控制器

首先我们需要定义复位控制器时钟域,它没有复位线,而是使用FPGA比特流加载来设置触发器。

val resetCtrlClockDomain = ClockDomain(
  clock = io.axiClk,
  config = ClockDomainConfig(
    resetKind = BOOT
  )
)

然后我们可以在这个时钟域下定义一个简单的复位控制器。

val resetCtrl = new ClockingArea(resetCtrlClockDomain) {
  val axiResetUnbuffered  = False
  val coreResetUnbuffered = False

  // Implement an counter to keep the reset axiResetOrder high 64 cycles
  // Also this counter will automaticly do a reset when the system boot.
  val axiResetCounter = Reg(UInt(6 bits)) init(0)
  when(axiResetCounter =/= U(axiResetCounter.range -> true)) {
    axiResetCounter := axiResetCounter + 1
    axiResetUnbuffered := True
  }
  when(BufferCC(io.asyncReset)) {
    axiResetCounter := 0
  }

  // When an axiResetOrder happen, the core reset will as well
  when(axiResetUnbuffered) {
    coreResetUnbuffered := True
  }

  // Create all reset used later in the design
  val axiReset  = RegNext(axiResetUnbuffered)
  val coreReset = RegNext(coreResetUnbuffered)
  val vgaReset  = BufferCC(axiResetUnbuffered)
}

每个系统的时钟域设置

现在复位控制器已经实现,我们可以为Pinsec的所有子系统定义时钟域:

val axiClockDomain = ClockDomain(
  clock     = io.axiClk,
  reset     = resetCtrl.axiReset,
  frequency = FixedFrequency(50 MHz) // The frequency information is used by the SDRAM controller
)

val coreClockDomain = ClockDomain(
  clock = io.axiClk,
  reset = resetCtrl.coreReset
)

val vgaClockDomain = ClockDomain(
  clock = io.vgaClk,
  reset = resetCtrl.vgaReset
)

val jtagClockDomain = ClockDomain(
  clock = io.jtag.tck
)

此外,Pinsec的所有核心系统都将在一个 axi 时钟域里定义:

val axi = new ClockingArea(axiClockDomain) {
  // Here will come the rest of Pinsec
}

主要组件

Pinsec主要由4个主要组件构成:

  • 1个RISCV CPU

  • 1个SDRAM控制器

  • 1个片上存储器

  • 1个JTAG控制器

RISCV CPU

Pinsec中使用的RISCV CPU具有多种参数化可能性:

val core = coreClockDomain {
  val coreConfig = CoreConfig(
    pcWidth = 32,
    addrWidth = 32,
    startAddress = 0x00000000,
    regFileReadyKind = sync,
    branchPrediction = dynamic,
    bypassExecute0 = true,
    bypassExecute1 = true,
    bypassWriteBack = true,
    bypassWriteBackBuffer = true,
    collapseBubble = false,
    fastFetchCmdPcCalculation = true,
    dynamicBranchPredictorCacheSizeLog2 = 7
  )

  // The CPU has a systems of plugin which allow to add new feature into the core.
  // Those extension are not directly implemented into the core, but are kind of additive logic patch defined in a separated area.
  coreConfig.add(new MulExtension)
  coreConfig.add(new DivExtension)
  coreConfig.add(new BarrelShifterFullExtension)

  val iCacheConfig = InstructionCacheConfig(
    cacheSize =4096,
    bytePerLine =32,
    wayCount = 1,  // Can only be one for the moment
    wrappedMemAccess = true,
    addressWidth = 32,
    cpuDataWidth = 32,
    memDataWidth = 32
  )

  // There is the instantiation of the CPU by using all those construction parameters
  new RiscvAxi4(
    coreConfig = coreConfig,
    iCacheConfig = iCacheConfig,
    dCacheConfig = null,
    debug = true,
    interruptCount = 2
  )
}

片上RAM

AXI4片上RAM的实例化非常简单。

事实上,它不是AXI4,而是Axi4Shared,这意味着ARW通道取代了AR和AW通道。该解决方案占用的面积更少,同时可与完整的AXI4实现完全互操作。

val ram = Axi4SharedOnChipRam(
  dataWidth = 32,
  byteCount = 4 KiB,
  idWidth = 4     // Specify the AXI4 ID width.
)

SDRAM控制器

首先,您需要定义SDRAM设备的布局和时序。在DE1-SOC上,SDRAM型号是IS42x320D。

object IS42x320D {
  def layout = SdramLayout(
    bankWidth   = 2,
    columnWidth = 10,
    rowWidth    = 13,
    dataWidth   = 16
  )

  def timingGrade7 = SdramTimings(
    bootRefreshCount =   8,
    tPOW             = 100 us,
    tREF             =  64 ms,
    tRC              =  60 ns,
    tRFC             =  60 ns,
    tRAS             =  37 ns,
    tRP              =  15 ns,
    tRCD             =  15 ns,
    cMRD             =   2,
    tWR              =  10 ns,
    cWR              =   1
  )
}

然后您可以使用这些定义来参数化SDRAM控制器实例。

val sdramCtrl = Axi4SharedSdramCtrl(
  axiDataWidth = 32,
  axiIdWidth   = 4,
  layout       = IS42x320D.layout,
  timing       = IS42x320D.timingGrade7,
  CAS          = 3
)

JTAG控制器

JTAG控制器可用于在PC访问存储器并调试CPU。

val jtagCtrl = JtagAxi4SharedDebugger(SystemDebuggerConfig(
  memAddressWidth = 32,
  memDataWidth    = 32,
  remoteCmdWidth  = 1,
  jtagClockDomain = jtagClockDomain
))

外设

Pinsec有一些集成的外设:

  • GPIO

  • 计时器

  • 串口

  • VGA

GPIO

val gpioACtrl = Apb3Gpio(
  gpioWidth = 32
)

val gpioBCtrl = Apb3Gpio(
  gpioWidth = 32
)

计时器

Pinsec定时器模块包括:

  • 1个预分频器

  • 1个32位定时器

  • 三个16位定时器

所有这些都被打包到PinsecTimerCtrl组件中。

val timerCtrl = PinsecTimerCtrl()

UART控制器

首先我们需要为UART控制器定义一个配置:

val uartCtrlConfig = UartCtrlMemoryMappedConfig(
  uartCtrlConfig = UartCtrlGenerics(
    dataWidthMax      = 8,
    clockDividerWidth = 20,
    preSamplingSize   = 1,
    samplingSize      = 5,
    postSamplingSize  = 2
  ),
  txFifoDepth = 16,
  rxFifoDepth = 16
)

然后我们可以用它来实例化UART控制器

val uartCtrl = Apb3UartCtrl(uartCtrlConfig)

VGA控制器

首先我们需要定义VGA控制器的配置:

val vgaCtrlConfig = Axi4VgaCtrlGenerics(
  axiAddressWidth = 32,
  axiDataWidth    = 32,
  burstLength     = 8,           // In Axi words
  frameSizeMax    = 2048*1512*2, // In byte
  fifoSize        = 512,         // In axi words
  rgbConfig       = RgbConfig(5,6,5),
  vgaClock        = vgaClockDomain
)

然后我们可以用它来实例化VGA控制器

val vgaCtrl = Axi4VgaCtrl(vgaCtrlConfig)

总线互连

共有三个互连组件:

  • AXI4交叉开关(crossbar)

  • AXI4桥接到APB3

  • APB3解码器

AXI4桥接到APB3

该桥将用于将低带宽外设连接到AXI交叉开关。

val apbBridge = Axi4SharedToApb3Bridge(
  addressWidth = 20,
  dataWidth    = 32,
  idWidth      = 4
)

AXI4交叉开关(crossbar)

The AXI4 crossbar that interconnect AXI4 masters and slaves together is generated by using an factory. The concept of this factory is to create it, then call many function on it to configure it, and finally call the build function to ask the factory to generate the corresponding hardware :

val axiCrossbar = Axi4CrossbarFactory()
// Where you will have to call function the the axiCrossbar factory to populate its configuration
axiCrossbar.build()

首先,您需要添加从端接口:

//          Slave  -> (base address,  size) ,

axiCrossbar.addSlaves(
  ram.io.axi       -> (0x00000000L,   4 KiB),
  sdramCtrl.io.axi -> (0x40000000L,  64 MiB),
  apbBridge.io.axi -> (0xF0000000L,   1 MiB)
)

然后,您需要添加从端和主端之间的互连矩阵(这展现可见性):

//         Master -> List of slaves which are accessible

axiCrossbar.addConnections(
  core.io.i       -> List(ram.io.axi, sdramCtrl.io.axi),
  core.io.d       -> List(ram.io.axi, sdramCtrl.io.axi, apbBridge.io.axi),
  jtagCtrl.io.axi -> List(ram.io.axi, sdramCtrl.io.axi, apbBridge.io.axi),
  vgaCtrl.io.axi  -> List(            sdramCtrl.io.axi)
)

然后,为了减少组合路径长度并拥有良好的设计FMax,您可以要求生成器在给定的主端或从端之间插入流水线级:

备注

以下代码中的 halfPipe / >> / << / >/-> 由反压流(Stream)总线库提供。
可以在 这里 找到一些文档。简而言之,这只是一些流水线和互连的东西。
// Pipeline the connection between the crossbar and the apbBridge.io.axi
axiCrossbar.addPipelining(apbBridge.io.axi,(crossbar,bridge) => {
  crossbar.sharedCmd.halfPipe() >> bridge.sharedCmd
  crossbar.writeData.halfPipe() >> bridge.writeData
  crossbar.writeRsp             << bridge.writeRsp
  crossbar.readRsp              << bridge.readRsp
})

// Pipeline the connection between the crossbar and the sdramCtrl.io.axi
axiCrossbar.addPipelining(sdramCtrl.io.axi,(crossbar,ctrl) => {
  crossbar.sharedCmd.halfPipe()  >>  ctrl.sharedCmd
  crossbar.writeData            >/-> ctrl.writeData
  crossbar.writeRsp              <<  ctrl.writeRsp
  crossbar.readRsp               <<  ctrl.readRsp
})

APB3解码器

APB3桥和所有外设之间的互连是通过APB3Decoder完成的:

val apbDecoder = Apb3Decoder(
  master = apbBridge.io.apb,
  slaves = List(
    gpioACtrl.io.apb -> (0x00000, 4 KiB),
    gpioBCtrl.io.apb -> (0x01000, 4 KiB),
    uartCtrl.io.apb  -> (0x10000, 4 KiB),
    timerCtrl.io.apb -> (0x20000, 4 KiB),
    vgaCtrl.io.apb   -> (0x30000, 4 KiB),
    core.io.debugBus -> (0xF0000, 4 KiB)
  )
)

杂项

要将所有顶层IO连接到组件,需要以下代码:

io.gpioA <> axi.gpioACtrl.io.gpio
io.gpioB <> axi.gpioBCtrl.io.gpio
io.jtag  <> axi.jtagCtrl.io.jtag
io.uart  <> axi.uartCtrl.io.uart
io.sdram <> axi.sdramCtrl.io.sdram
io.vga   <> axi.vgaCtrl.io.vga

最后需要组件之间的一些连接,例如中断和核心调试模块复位

core.io.interrupt(0) := uartCtrl.io.interrupt
core.io.interrupt(1) := timerCtrl.io.interrupt

core.io.debugResetIn := resetCtrl.axiReset
when(core.io.debugResetOut) {
  resetCtrl.coreResetUnbuffered := True
}