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)
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
}