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)

将AXI4主端和从端互连在一起的AXI4交叉开关是使用生成器(factory)生成的。这个生成器的概念是先创建它,然后调用它的许多函数来配置,最后调用 build 函数来使生成器生成相应的硬件:

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
}