You're reading an pre-release version of this documentation.
For the latest stable release version, please have a look at master.

与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 中,这些事情是明确的。寄存器直接在其声明中就定义为寄存器。

时钟域

In VHDL, every time you want to define a bunch of registers, you need the carry the clock and the reset signal 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)).

在 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 库。