Skip to content

Commit

Permalink
fpga: working on waveform lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
zephray committed Aug 15, 2021
1 parent 089fb40 commit c21039a
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 30 deletions.
25 changes: 25 additions & 0 deletions fpga/src/main/scala/caster/BlockRAM.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// Project Caster
// Copyright 2021 EI-2030
//
// This project is licensed with the CERN Open Hardware Licence version 2. You
// may redistribute and modify this project under the terms of the CERN-OHL-S v2
// (http://ohwr.org/cernohl).
// This project is distributed WITHOUT ANY EXPRESS OR IMPLIED WARRANTY,
// INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS FOR A
// PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable Conditions.
//
package caster

import spinal.core._

case class Bram32K() extends BlackBox{
val addra = in UInt(14 bits)
val dina = in UInt(32 bits)
val douta = out UInt(2 bits)
val wea = in Bool()
val addrb = in UInt(14 bits)
val dinb = in UInt(32 bits)
val doutb = out UInt(2 bits)
val web = in Bool()
}
65 changes: 37 additions & 28 deletions fpga/src/main/scala/caster/Caster.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ object Caster {
useLock = false,
useRegion = false,
useBurst = false,
useLock = false,
useQos = false,
useResp = false
)
Expand All @@ -53,7 +52,7 @@ object Caster {
addressWidth = 32,
dataWidth = 32,
selWidth = 1,
useSlaveError = true
useSlaveError = false
)

def getFetcherConfig(config: CasterConfig, clock: ClockDomain) = FetcherGenerics(
Expand All @@ -62,9 +61,18 @@ object Caster {
burstLength = config.burstBytes,
frameSizeMax = 2560 * 2560,
fifoSize = config.burstBytes * 4,
pixelWidth = 8,
pixelWidth = 32, // 8x 4bit pixels
pixelClock = clock
)

def getWaveformConfig(config: CasterConfig) = WaveformGenerics(
waveformSize = 4096,
lookupWidth = 8,
pixelWidth = 4,
maxFrame = 64,
bramLatency = 2,
apbConfig = getApbConfig(config)
)
}

class Caster(config: CasterConfig) extends Component {
Expand All @@ -83,45 +91,46 @@ class Caster(config: CasterConfig) extends Component {

val pixelClockDomain = ClockDomain.external("pixel")

val fetcherControl = FetcherControl(
Caster.getFetcherConfig(config, pixelClockDomain),
Caster.getWaveformConfig(config),
Caster.getAxiConfig(config),
Caster.getApbConfig(config))

val gpioCtrl = Apb3Gpio(
gpioWidth = 32,
withReadSync = true
)

val miscRegApb = slave(Apb3(Caster.getApbConfig(config)))
val miscRegApbCtrl = Apb3SlaveFactory(miscRegApb)
val apb3cc = Apb3CC(
config = Caster.getApbConfig(config),
inputClock = ClockDomain.current,
outputClock = pixelClockDomain
)

val apbDecoder = Apb3Decoder(
master = io.apb,
slaves = List(
miscRegApb.io.apb -> (0xff000000L, 4 KiB)
gpioCtrl.io.apb -> (0xff001000L, 4 KiB)
fetcherControl.io.apb -> (0xff000000L, 4 KiB),
gpioCtrl.io.apb -> (0xff001000L, 4 KiB),
apb3cc.io.input -> (0xff002000L, 4 KiB)
)
)

val fetcherA = Fetcher(Caster.getFetcherConfig(config, pixelClockDomain))
val fetcherB = Fetcher(Caster.getFetcherConfig(config, pixelClockDomain))

miscRegApbCtrl.drive(fetcherA.io.base, 0x00, 32)
miscRegApbCtrl.drive(fetcherA.io.size, 0x04, 32)
miscRegApbCtrl.drive(fetcherB.io.base, 0x08, 32)
miscRegApbCtrl.drive(fetcherB.io.size, 0x0c, 32)

val pixelClockArea = new ClockingArea(pixelClockDomain) {
fetcherA.io.trigger := False
fetcherA.io.pixel.ready := True
fetcherB.io.trigger := False
fetcherB.io.pixel.ready := True
}

val axiArbiter = Axi4ReadOnlyArbiter(
outputConfig = Caster.getAxiConfig(config),
inputsCount = 2
)
fetcherControl.io.trigger := False

val waveform = Waveform(
Caster.getWaveformConfig(config)
)

io.axi <> axiArbiter.io.output
axiArbiter.io.inputs(0) << fetcherA.io.axi
axiArbiter.io.inputs(1) << fetcherB.io.axi
waveform.io.apb <> apb3cc.io.output
waveform.io.sourceLevel <> fetcherControl.io.sourceLevel
waveform.io.targetLevel <> fetcherControl.io.targetLevel
waveform.io.frameSeq := U(0)
waveform.io.pixelDrive.ready := True
}

io.gpio <> gpioCtrl.io.gpio
io.axi <> fetcherControl.io.axi
}
4 changes: 2 additions & 2 deletions fpga/src/main/scala/caster/Fetcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ case class FetcherGenerics(
def bytePerAddress = axiDataWidth/8 * burstLength
}

case class Fetcher(g: FetcherGenerics) extends Component{
case class Fetcher(g: FetcherGenerics) extends Component {
import g._

val io = new Bundle{
val io = new Bundle {
val axi = master(Axi4ReadOnly(axi4Config))
val base = in UInt(dmaGenerics.addressWidth bits)
val size = in UInt(dmaGenerics.sizeWidth bits)
Expand Down
79 changes: 79 additions & 0 deletions fpga/src/main/scala/caster/FetcherControl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// Project Caster
// Copyright 2021 EI-2030
//
// This project is licensed with the CERN Open Hardware Licence version 2. You
// may redistribute and modify this project under the terms of the CERN-OHL-S v2
// (http://ohwr.org/cernohl).
// This project is distributed WITHOUT ANY EXPRESS OR IMPLIED WARRANTY,
// INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS FOR A
// PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable Conditions.
//
package caster

import spinal.core._
import spinal.lib._
import spinal.lib.bus.amba3.apb._
import spinal.lib.bus.amba4.axi._

case class FetcherControl(
fg: FetcherGenerics, wg: WaveformGenerics,
axiConfig: Axi4Config, apbConfig: Apb3Config)
extends Component {

val io = new Bundle {
val axi = master(Axi4ReadOnly(axiConfig))
val apb = slave(Apb3(apbConfig))
val sourceLevel = master(Stream(Vec(UInt(wg.pixelWidth bits), wg.lookupWidth)))
val targetLevel = master(Stream(Vec(UInt(wg.pixelWidth bits), wg.lookupWidth)))
val trigger = in Bool()
val busy = out Bool()
}

assert(wg.pixelWidth * wg.lookupWidth == fg.pixelWidth,
"Fetcher width should match LUT unit width")

val regApbCtrl = Apb3SlaveFactory(io.apb)

val fetcherA = Fetcher(fg)
val fetcherB = Fetcher(fg)

val axiArbiter = Axi4ReadOnlyArbiter(
outputConfig = axiConfig,
inputsCount = 2
)

regApbCtrl.drive(fetcherA.io.base, 0x00, 0)
regApbCtrl.drive(fetcherA.io.size, 0x04, 0)
regApbCtrl.drive(fetcherB.io.base, 0x08, 0)
regApbCtrl.drive(fetcherB.io.size, 0x0c, 0)

io.axi <> axiArbiter.io.output
axiArbiter.io.inputs(0) << fetcherA.io.axi
axiArbiter.io.inputs(1) << fetcherB.io.axi

val pixelClockArea = new ClockingArea(fg.pixelClock) {
fetcherA.io.trigger := io.trigger
fetcherB.io.trigger := io.trigger
io.busy := fetcherA.io.busy || fetcherB.io.busy

io.sourceLevel.valid := fetcherA.io.pixel.valid
fetcherA.io.pixel.ready := io.sourceLevel.ready

io.targetLevel.valid := fetcherB.io.pixel.valid
fetcherB.io.pixel.ready := io.targetLevel.ready

val pixelA = UInt(fg.pixelWidth bits)
val pixelB = UInt(fg.pixelWidth bits)
pixelA := fetcherA.io.pixel.payload.fragment
pixelB := fetcherB.io.pixel.payload.fragment

for (i <- 0 until wg.lookupWidth) {
io.sourceLevel.payload(i) :=
pixelA((i + 1) * wg.pixelWidth - 1 downto i * wg.pixelWidth)
io.targetLevel.payload(i) :=
pixelB((i + 1) * wg.pixelWidth - 1 downto i * wg.pixelWidth)
}
}

}
36 changes: 36 additions & 0 deletions fpga/src/main/scala/caster/Timing.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Project Caster
// Copyright 2021 EI-2030
//
// This project is licensed with the CERN Open Hardware Licence version 2. You
// may redistribute and modify this project under the terms of the CERN-OHL-S v2
// (http://ohwr.org/cernohl).
// This project is distributed WITHOUT ANY EXPRESS OR IMPLIED WARRANTY,
// INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS FOR A
// PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable Conditions.
//
package caster

import spinal.core._
import math._
import spinal.lib._
import spinal.lib.bus.amba3.apb._

// Target is 16 bit wide / 8 pixel per clock
// BRAM is capable of doing 2 read ports (2 pixel lookup per clock)
// Use 4 4K BRAMs in parallel to provide 8 read ports
case class TimingGenerics(
outputWidth: Int = 16, // In bits
apbConfig:Apb3Config) {

}

case class Timing(g: TimingGenerics) extends Component {
import g._

val io = new Bundle {
val apb = slave(Apb3(apbConfig))
val pixelDrive = slave(Stream(Vec(UInt(2 bits))))
}

}
104 changes: 104 additions & 0 deletions fpga/src/main/scala/caster/Waveform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// Project Caster
// Copyright 2021 EI-2030
//
// This project is licensed with the CERN Open Hardware Licence version 2. You
// may redistribute and modify this project under the terms of the CERN-OHL-S v2
// (http://ohwr.org/cernohl).
// This project is distributed WITHOUT ANY EXPRESS OR IMPLIED WARRANTY,
// INCLUDING OF MERCHANTABILITY, SATISFACTORY QUALITY AND FITNESS FOR A
// PARTICULAR PURPOSE. Please see the CERN-OHL-S v2 for applicable Conditions.
//
package caster

import spinal.core._
import math._
import spinal.lib._
import spinal.lib.bus.amba3.apb._

import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer

/* Waveform Format:
* 4 KB waveform RAM, holds 16K values, 14 bit address
*
* 13 12 11 10 09 08 07 06 05 04 03 02 01 00
* [ Frame Count ] [ Src Lvl ] [ Tgt Lvl ] -> 2 bit driving value
*/

case class WaveformGenerics(
waveformSize: Int = 4096, // In bytes
lookupWidth: Int = 8, // In pixels
pixelWidth: Int = 4, // In bits
maxFrame: Int = 63, // In frames
bramLatency: Int = 2, // In cycles
apbConfig: Apb3Config) {

def addressWidth = log2Up(waveformSize)
def frameWidth = log2Up(maxFrame)
def BRAMCount = lookupWidth / 2

// Make sure the waveform RAM is big enough to hold the table
require((log2Up(waveformSize) + 2) >= log2Up(maxFrame) + pixelWidth * 2)
}

case class Waveform(g: WaveformGenerics) extends Component {
import g._

val io = new Bundle {
val apb = slave(Apb3(apbConfig))
val sourceLevel = slave(Stream(Vec(UInt(pixelWidth bits), lookupWidth)))
val targetLevel = slave(Stream(Vec(UInt(pixelWidth bits), lookupWidth)))
val frameSeq = in UInt(frameWidth bits)
val pixelDrive = master(Stream(Vec(UInt(2 bits), lookupWidth)))
}

val waveRAMs = ArrayBuffer[Bram32K]()

// Synchornize 2 inputs, take ready signal
val inputValid = io.sourceLevel.valid && io.targetLevel.valid
val pixelValid = inputValid && io.pixelDrive.ready
io.sourceLevel.ready := io.pixelDrive.ready
io.targetLevel.ready := io.pixelDrive.ready

val outputValid = Delay(pixelValid, bramLatency)

// APB slave to BRAM write master
// READ is not supported
io.apb.PREADY := True
io.apb.PRDATA := B(0)
val bramWriteEnable = io.apb.PSEL(0) && io.apb.PENABLE && io.apb.PWRITE
val bramWriteData = io.apb.PWDATA
val bramWriteAddr = io.apb.PADDR(addressWidth - 1 downto 0)

for (i <- 0 until g.BRAMCount) {
// Each BRAM provides 2 read ports and 1 write port
val blockRAM = new Bram32K()
waveRAMs += blockRAM

// Lookup Input
val bramReadAddrA = U(Cat(
io.frameSeq,
io.sourceLevel.payload(i * 2),
io.targetLevel.payload(i * 2)))
val bramReadAddrB = U(Cat(
io.frameSeq,
io.sourceLevel.payload(i * 2 + 1),
io.targetLevel.payload(i * 2 + 1)))

// Connect Port A input to APB slave
blockRAM.addra := bramWriteEnable ? bramWriteAddr | bramReadAddrA
blockRAM.dina := U(bramWriteData)
blockRAM.wea := bramWriteEnable

// Connect Port B input
blockRAM.addrb := bramReadAddrB
blockRAM.dinb := U(0)
blockRAM.web := False

io.pixelDrive.payload(i * 2) := blockRAM.douta
io.pixelDrive.payload(i * 2 + 1) := blockRAM.doutb
}

io.pixelDrive.valid := outputValid
}

0 comments on commit c21039a

Please sign in to comment.