VHDL comparison
Introduction
This page will show the main differences between VHDL and SpinalHDL. Things will not be explained in depth.
Process
Processes are often needed when you write RTL, however, their semantics can be clunky to work with. Due to how they work in VHDL, they can force you to split your code and duplicate things.
To produce the following RTL:
You will have to write the following VHDL:
signal mySignal : std_logic;
signal myRegister : std_logic_vector(3 downto 0);
signal myRegisterWithReset : std_logic_vector(3 downto 0);
begin
process(cond)
begin
mySignal <= '0';
if cond = '1' then
mySignal <= '1';
end if;
end process;
process(clk)
begin
if rising_edge(clk) then
if cond = '1' then
myRegister <= myRegister + 1;
end if;
end if;
end process;
process(clk,reset)
begin
if reset = '1' then
myRegisterWithReset <= (others => '0');
elsif rising_edge(clk) then
if cond = '1' then
myRegisterWithReset <= myRegisterWithReset + 1;
end if;
end if;
end process;
While in SpinalHDL, it’s:
val mySignal = Bool()
val myRegister = Reg(UInt(4 bits))
val myRegisterWithReset = Reg(UInt(4 bits)) init(0)
mySignal := False
when(cond) {
mySignal := True
myRegister := myRegister + 1
myRegisterWithReset := myRegisterWithReset + 1
}
Implicit vs explicit definitions
In VHDL, when you declare a signal, you don’t specify if it is a combinatorial signal or a register. Where and how you assign to it decides whether it is combinatorial or registered.
In SpinalHDL these kinds of things are explicit. Registers are defined as registers directly in their declaration.
Clock domains
In VHDL, every time you want to define a bunch of registers, you need the carry the clock and the reset wire to them. In addition, you have to hardcode everywhere how those clock and reset signals should be used (clock edge, reset polarity, reset nature (async, sync)).
In SpinalHDL you can define a ClockDomain
, and then define the area of your hardware that uses it.
For example:
val coreClockDomain = ClockDomain(
clock = io.coreClk,
reset = io.coreReset,
config = ClockDomainConfig(
clockEdge = RISING,
resetKind = ASYNC,
resetActiveLevel = HIGH
)
)
val coreArea = new ClockingArea(coreClockDomain) {
val myCoreClockedRegister = Reg(UInt(4 bit))
// ...
// coreClockDomain will also be applied to all sub components instantiated in the Area
// ...
}
Component’s internal organization
In VHDL, there is a block
feature that allows you to define sub-areas of logic inside your component. However, almost no one uses this feature, because most people don’t know about them, and also because all signals defined inside these regions are not readable from the outside.
In SpinalHDL you have an Area
feature that does this concept much more nicely:
val timeout = new Area {
val counter = Reg(UInt(8 bits)) init(0)
val overflow = False
when(counter =/= 100) {
counter := counter + 1
} otherwise {
overflow := True
}
}
val core = new Area {
when(timeout.overflow) {
timeout.counter := 0
}
}
Variables and signals defined inside of an Area
are accessible elsewhere in the component, including in other Area
regions.
Safety
In VHDL as in SpinalHDL, it’s easy to write combinatorial loops, or to infer a latch by forgetting to drive a signal in the path of a process.
Then, to detect those issues, you can use some lint
tools that will analyze your VHDL, but those tools aren’t free. In SpinalHDL the lint
process in integrated inside the compiler, and it won’t generate the RTL code until everything is fine. It also checks clock domain crossing.
Functions and procedures
Functions and procedures are not used very often in VHDL, probably because they are very limited:
You can only define a chunk of combinational hardware, or only a chunk of registers (if you call the function/procedure inside a clocked process).
You can’t define a process inside them.
You can’t instantiate a component inside them.
The scope of what you can read/write inside them is limited.
In SpinalHDL, all those limitations are removed.
An example that mixes combinational logic and a register in a single function:
def simpleAluPipeline(op: Bits, a: UInt, b: UInt): UInt = {
val result = UInt(8 bits)
switch(op) {
is(0){ result := a + b }
is(1){ result := a - b }
is(2){ result := a * b }
}
return RegNext(result)
}
An example with the queue function inside the Stream Bundle (handshake). This function instantiates a FIFO component:
class Stream[T <: Data](dataType: T) extends Bundle with IMasterSlave with DataCarrier[T] {
val valid = Bool()
val ready = Bool()
val payload = cloneOf(dataType)
def queue(size: Int): Stream[T] = {
val fifo = new StreamFifo(dataType, size)
fifo.io.push <> this
fifo.io.pop
}
}
An example where a function assigns a signal defined outside of itself:
val counter = Reg(UInt(8 bits)) init(0)
counter := counter + 1
def clear() : Unit = {
counter := 0
}
when(counter > 42) {
clear()
}
Buses and Interfaces
VHDL is very boring when it comes to buses and interfaces. You have two options:
Define buses and interfaces wire-by-wire, each time and everywhere:
PADDR : in unsigned(addressWidth-1 downto 0);
PSEL : in std_logic
PENABLE : in std_logic;
PREADY : out std_logic;
PWRITE : in std_logic;
PWDATA : in std_logic_vector(dataWidth-1 downto 0);
PRDATA : out std_logic_vector(dataWidth-1 downto 0);
Use records but lose parameterization (statically fixed in the package), and you have to define one for each directions:
P_m : in APB_M;
P_s : out APB_S;
SpinalHDL has very strong support for bus and interface declarations with limitless parameterizations:
val P = slave(Apb3(addressWidth, dataWidth))
You can also use object oriented programming to define configuration objects:
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 system of plugins which allows adding new features into the core.
// Those extensions are not directly implemented in the core, but are kind of an additive logic patch defined in a separate 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
)
new RiscvCoreAxi4(
coreConfig = coreConfig,
iCacheConfig = iCacheConfig,
dCacheConfig = null,
debug = debug,
interruptCount = interruptCount
)
Signal declaration
VHDL forces you to define all signals at the top of your architecture description, which is annoying.
..
.. (many signal declarations)
..
signal a : std_logic;
..
.. (many signal declarations)
..
begin
..
.. (many logic definitions)
..
a <= x & y
..
.. (many logic definitions)
..
SpinalHDL is flexible when it comes to signal declarations.
val a = Bool
a := x & y
It also allows you to define and assign signals in a single line.
val a = x & y
Component instantiation
VHDL is very verbose about this, as you have to redefine all signals of your sub-component entity, and then bind them one-by-one when you instantiate your component.
divider_cmd_valid : in std_logic;
divider_cmd_ready : out std_logic;
divider_cmd_numerator : in unsigned(31 downto 0);
divider_cmd_denominator : in unsigned(31 downto 0);
divider_rsp_valid : out std_logic;
divider_rsp_ready : in std_logic;
divider_rsp_quotient : out unsigned(31 downto 0);
divider_rsp_remainder : out unsigned(31 downto 0);
divider : entity work.UnsignedDivider
port map (
clk => clk,
reset => reset,
cmd_valid => divider_cmd_valid,
cmd_ready => divider_cmd_ready,
cmd_numerator => divider_cmd_numerator,
cmd_denominator => divider_cmd_denominator,
rsp_valid => divider_rsp_valid,
rsp_ready => divider_rsp_ready,
rsp_quotient => divider_rsp_quotient,
rsp_remainder => divider_rsp_remainder
);
SpinalHDL removes that, and allows you to access the IO of sub-components in an object-oriented way.
val divider = new UnsignedDivider()
// And then if you want to access IO signals of that divider:
divider.io.cmd.valid := True
divider.io.cmd.numerator := 42
Casting
There are two annoying casting methods in VHDL:
boolean <> std_logic (ex: To assign a signal using a condition such as
mySignal <= myValue < 10
is not legal)unsigned <> integer (ex: To access an array)
SpinalHDL removes these casts by unifying things.
boolean/std_logic:
val value = UInt(8 bits)
val valueBiggerThanTwo = Bool
valueBiggerThanTwo := value > 2 // value > 2 return a Bool
unsigned/integer:
val array = Vec(UInt(4 bits),8)
val sel = UInt(3 bits)
val arraySel = array(sel) // Arrays are indexed directly by using UInt
Resizing
The fact that VHDL is strict about bit size is probably a good thing.
my8BitsSignal <= resize(my4BitsSignal, 8);
In SpinalHDL you have two ways to do the same:
// The traditional way
my8BitsSignal := my4BitsSignal.resize(8)
// The smart way
my8BitsSignal := my4BitsSignal.resized
Parameterization
SpinalHDL has full support for generics integrated natively in its compiler, and it doesn’t rely on VHDL generics.
Here is an example of parameterized data structures:
val colorStream = Stream(Color(5, 6, 5)))
val colorFifo = StreamFifo(Color(5, 6, 5), depth = 128)
colorFifo.io.push <> colorStream
Here is an example of a parameterized component:
class Arbiter[T <: Data](payloadType: T, portCount: Int) extends Component {
val io = new Bundle {
val sources = Vec(slave(Stream(payloadType)), portCount)
val sink = master(Stream(payloadType))
}
// ...
}
Meta hardware description
VHDL has kind of a closed syntax. You can’t add abstraction layers on top of it.
SpinalHDL, because it’s built on top of Scala, is very flexible, and allows you to define new abstraction layers very easily.
Some examples of this flexibility are the FSM library, the BusSlaveFactory library, and also the JTAG library.