与VHDL对比

简介

本页将讨论 VHDL 和 SpinalHDL 之间的主要区别。但不会深入解释。

过程(Process)

通常编写 RTL 时需要用到过程,但是它们的语义使用起来可能很笨拙。由于它们在 VHDL 中的工作方式,可能会迫使您拆分代码并重复编写。

要生成以下 RTL:

../../../_images/process_rtl.svg

您必须编写以下 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;

在 SpinalHDL 中,它是:

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
}

隐式与显式定义对比

在 VHDL 中,当声明一个信号时,您无需指定它是组合信号还是寄存器。给它赋值的位置和方式决定了它是组合电路还是寄存器。

在 SpinalHDL 中,这些事情是明确的。寄存器直接在其声明中就定义为寄存器。

时钟域

在 VHDL 中,每次想要定义一堆寄存器时,都需要将时钟和复位信号传递给它们。此外,您必须在各处硬编码如何使用这些时钟和复位信号(包括它们的属性时钟沿、复位极性、复位性质(异步、同步))。

在 SpinalHDL 中,您可以定义 ClockDomain,然后定义使用它的硬件区域。

例如:

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 bits))
  // ...
  // coreClockDomain will also be applied to all sub components instantiated in the Area
  // ...
}

组件的内部组织方式

在 VHDL 中,有一个 block 功能,允许您在组件内定义逻辑子区域。然而,几乎没有人使用这一功能,因为大多数人不了解它们,也因为这些区域内定义的所有信号都无法从外部读取、使用。

在 SpinalHDL 中,你有一个 Area 功能,可以更好地实现这个概念:

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

Area 内部定义的变量和信号可以在组件的其他地方访问,包括其他 Area 区域内。

安全性

在 VHDL 中,就像在 SpinalHDL 中一样,很容易编写出组合逻辑环,或者因为忘记给路径中的信号驱动而得到一个锁存器(latch)。

然后,为了检测这些问题,您可以使用一些 lint 工具来分析您的 VHDL,但这些工具不是免费的。在 SpinalHDL 中, lint 过程集成在编译器内部,并且在一切正常之前它不会生成 RTL 代码。此外,它还会检查跨时钟域信号。

功能与流程

函数和过程在 VHDL 中不经常使用,可能是因为它们的功能非常有限:

  • 您只能定义一块组合逻辑硬件,或者只能定义一块寄存器(如果您在时钟进程内调用函数/过程)。

  • 您无法在其中定义流程。

  • 您无法在其中实例化组件。

  • 在这里面,您可以读/写的范围是有限的。

在 SpinalHDL 中,所有这些限制都被消除了。

在单个函数中混合使用组合逻辑和寄存器的示例:

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

Stream线束内的队列函数示例(带握手)。该函数实例化一个 FIFO 组件:

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

为在外部定义的信号赋值的示例函数:

val counter = Reg(UInt(8 bits)) init(0)
counter := counter + 1

def clear() : Unit = {
  counter := 0
}

when(counter > 42) {
  clear()
}

总线和接口

当谈到总线和接口时,VHDL 非常无聊。您有两个选择:

  1. 随时随地逐线定义总线和接口:

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);
  1. 使用记录但无法参数化(静态固定在包中),并且您必须为每个信号定义方向:

P_m : in APB_M;
P_s : out APB_S;

SpinalHDL 对参数化总线和接口的声明提供非常强大的支持:

val P = slave(Apb3(addressWidth, dataWidth))

您还可以使用面向对象编程来定义专门的配置对象:

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
)

信号声明

VHDL 强制您在架构描述的顶部定义所有信号,这很烦人。

  ..
  .. (many signal declarations)
  ..
  signal a : std_logic;
  ..
  .. (many signal declarations)
  ..
begin
  ..
  .. (many logic definitions)
  ..
  a <= x & y
  ..
  .. (many logic definitions)
  ..

SpinalHDL 在信号声明方面非常灵活。

val a = Bool()
a := x & y

它还允许您在一行中定义和赋值信号。

val a = x & y

组件实例化

VHDL 对此非常冗长,因为您必须重新定义子组件实体的所有信号,然后在实例化组件时将它们一一绑定。

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 在这方面做出了很大提升,并允许您以面向对象的方式访问子组件的 IO。

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

类型转换

VHDL 中有两种烦人的转换方法:

  • boolean <> std_logic (例如:使用 mySignal <= myValue < 10 等条件赋值信号是不合法的)

  • unsigned <> integer(例如:访问数组)

SpinalHDL 通过统一化对象来转换这些类型。

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

调整位宽

VHDL 对位宽限制严格可能是一件好事。

my8BitsSignal <= resize(my4BitsSignal, 8);

在 SpinalHDL 中,您有两种方法可以实现相同的目的:

// The traditional way
my8BitsSignal := my4BitsSignal.resize(8)

// The smart way
my8BitsSignal := my4BitsSignal.resized

参数化

2008 年修订版之前的 VHDL 在泛型方面存在许多问题。例如,您不能参数化记录,不能参数化实体中的数组,并且不能具有类型参数。
然后 VHDL 2008 出现并解决了这些问题。但根据供应商的不同,RTL 工具对VHDL 2008 的支持确实很弱。

SpinalHDL 完全支持在其编译器中自然的集成泛型,并且它不依赖于 VHDL 泛型。

这是参数化数据结构的示例:

val colorStream = Stream(Color(5, 6, 5)))
val colorFifo   = StreamFifo(Color(5, 6, 5), depth = 128)
colorFifo.io.push <> colorStream

以下是参数化组件的示例:

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

元硬件描述

VHDL 具有某种封闭的语法。您无法在其上添加抽象层。

而SpinalHDL, 由于它构建在 Scala 之上,所以非常灵活,并且允许您非常轻松地定义新的抽象层。

这种灵活性在后面的库中表现突出: FSM 库、 BusSlaveFactory 库以及 JTAG 库。