Introduction
A timer module is probably one of the most basic pieces of hardware. But even for a timer, there are some interresting things that you can do with SpinalHDL. This example will define a simple timer component which integrates a bus bridging utile.
Timer
So let’s start with the Timer component.
Specification
The Timer component will have a single construction parameter:
| Parameter Name | Type | Description |
|---|---|---|
| width | Int | Specify the bit width of the timer counter |
And also some inputs/outputs:
| IO Name | Direction | Type | Description |
|---|---|---|---|
| tick | in | Bool | When tick is True, the timer count up until limit. |
| clear | in | Bool | When tick is True, the timer is set to zero. clear has priority over tick. |
| limit | in | UInt(width bits) | When the timer value is equal to limit, the tick input is inhibited. |
| full | out | Bool | full is high when the timer value is equal to limit and tick is high. |
| value | out | UInt(width bits) | Wire out the timer counter value. |
Implementation
case class Timer(width : Int) extends Component{
val io = new Bundle{
val tick = in Bool
val clear = in Bool
val limit = in UInt(width bits)
val full = out Bool
val value = out UInt(width bits)
}
val counter = Reg(UInt(width bits))
when(io.tick && !io.full){
counter := counter + 1
}
when(io.clear){
counter := 0
}
io.full := counter === io.limit && io.tick
io.value := counter
}
Bridging function
Now we can start with the main purpose of this example: defining a bus bridging function. To do that we will use two techniques:
- Using the
BusSlaveFactorytool documented here - Defining a function inside the
Timercomponent which can be called from the parent component to drive theTimer’s IO in an abstract way.
Specification
This bridging function will take the following parameters:
| Parameter Name | Type | Description |
|---|---|---|
| busCtrl | BusSlaveFactory | The BusSlaveFactory instance that will be used by the function to create the bridging logic. |
| baseAddress | BigInt | The base address where the bridging logic should be mapped. |
| ticks | Seq[Bool] | A list of Bool sources that can be used as a tick signal. |
| clears | Seq[Bool] | A list of Bool sources that can be used as a clear signal. |
The register mapping assumes that the bus system is 32 bits wide:
| Name | Access | Width | Address offset | Bit offset | Description |
|---|---|---|---|---|---|
| ticksEnable | RW | len(ticks) | 0 | 0 | Each ticks bool can be actived if the corresponding ticksEnable bit is high. |
| clearsEnable | RW | len(clears) | 0 | 16 | Each clears bool can be actived if the corresponding clearsEnable bit is high. |
| limit | RW | width | 4 | 0 | Access the limit value of the timer component. When this register is written to, the timer is cleared. |
| value | R | width | 8 | 0 | Access the value of the timer. |
| clear | W | - | 8 | - | When this register is written to, it clears the timer. |
Implementation
Let’s add this bridging function inside the Timer component.
case class Timer(width : Int) extends Component{
val io = new Bundle{
val tick = in Bool
val clear = in Bool
val limit = in UInt(width bits)
val full = out Bool
val value = out UInt(width bits)
}
// Logic previously defined
// ....
// The function prototype uses Scala currying funcName(arg1,arg2)(arg3,arg3)
// which allow to call the function with a nice syntax later
// This function also returns an area, which allows to keep names of inner signals in the generated VHDL/Verilog.
def driveFrom(busCtrl : BusSlaveFactory,baseAddress : BigInt)(ticks : Seq[Bool],clears : Seq[Bool]) = new Area {
//Address 0 => clear/tick masks + bus
val ticksEnable = busCtrl.createReadWrite(Bits(ticks.length bits),baseAddress + 0,0) init(0)
val clearsEnable = busCtrl.createReadWrite(Bits(clears.length bits),baseAddress + 0,16) init(0)
val busClearing = False
io.clear := (clearsEnable & clears.asBits).orR | busClearing
io.tick := (ticksEnable & ticks.asBits ).orR
//Address 4 => read/write limit (+ auto clear)
busCtrl.driveAndRead(io.limit,baseAddress + 4)
busClearing setWhen(busCtrl.isWriting(baseAddress + 4))
//Address 8 => read timer value / write => clear timer value
busCtrl.read(io.value,baseAddress + 8)
busClearing setWhen(busCtrl.isWriting(baseAddress + 8))
}
}
Usage
Here is some demonstration code which is very close to the one used in the Pinsec SoC timer module. Basically it instantiates following elements:
- One 16 bit prescaler
- One 32 bit timer
- Three 16 bit timers
Then by using an Apb3SlaveFactory and functions defined inside the Timers, it creates bridging logic between the APB3 bus and all instantiated components.
val io = new Bundle{
val apb = Apb3(ApbConfig(addressWidth = 8, dataWidth = 32))
val interrupt = in Bool
val external = new Bundle{
val tick = Bool
val clear = Bool
}
}
//Prescaler is very similar to the timer, it mainly integrates a piece of auto reload logic.
val prescaler = Prescaler(width = 16)
val timerA = Timer(width = 32)
val timerB,timerC,timerD = Timer(width = 16)
val busCtrl = Apb3SlaveFactory(io.apb)
val prescalerBridge = prescaler.driveFrom(busCtrl,0x00)
val timerABridge = timerA.driveFrom(busCtrl,0x40)(
// The first element is True, which allows you to have a mode where the timer is always counting up.
ticks = List(True, prescaler.io.overflow),
// By looping the timer full to the clears, it allows you to create an autoreload mode.
clears = List(timerA.io.full)
)
val timerBBridge = timerB.driveFrom(busCtrl,0x50)(
//The external.tick could allow to create an impulsion counter mode
ticks = List(True, prescaler.io.overflow, io.external.tick),
//external.clear could allow to create an timeout mode.
clears = List(timerB.io.full, io.external.clear)
)
val timerCBridge = timerC.driveFrom(busCtrl,0x60)(
ticks = List(True, prescaler.io.overflow, io.external.tick),
clears = List(timerC.io.full, io.external.clear)
)
val timerDBridge = timerD.driveFrom(busCtrl,0x70)(
ticks = List(True, prescaler.io.overflow, io.external.tick),
clears = List(timerD.io.full, io.external.clear)
)
val interruptCtrl = InterruptCtrl(4)
val interruptCtrlBridge = interruptCtrl.driveFrom(busCtrl,0x10)
interruptCtrl.io.inputs(0) := timerA.io.full
interruptCtrl.io.inputs(1) := timerB.io.full
interruptCtrl.io.inputs(2) := timerC.io.full
interruptCtrl.io.inputs(3) := timerD.io.full
io.interrupt := interruptCtrl.io.pendings.orR