SoC顶层(Pinsec)
简介
Pinsec
是一个专为FPGA设计的小型 SoC。它可以在SpinalHDL库中找到,并且可以在 这里 找到一些文档
它的顶层实现是一个有趣的例子,因为它混合了一些设计模式,使其非常容易修改。可以轻松实现向总线结构添加新的主设备或新的外设。
可以在以下链接中查阅顶层实现:https://github.com/SpinalHDL/SpinalHDL/blob/master/lib/src/main/scala/spinal/lib/soc/pinsec/Pinsec.scala
这是Pinsec顶层硬件图:
定义所有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
}