对VHDL的支持(Help for VHDL people)
一、VHDL对比(VHDL comparison)
简介(Introduction)
这部分展示了VHDL与SpinalHDL的主要区别, 并不会太深入地解释
处理(Process)
编写RTL的过程也是一个数据处理的过程, 然而VHDL的语法太过于笨重。由于语法在VHDL中的工作方式, 语句会迫使你分隔你的代码块并造成冗余。
例如, 为了产生下述RTL:
在VHDL中书写格式如下:
signal mySignal : std_logic; signal myRegister : std_logic_vector(3 downto 0); signal myRegister With Reset : std_logic_vector(3 downto 0); begin process(cond) begin mySignal N= '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 }
上述代码生成的Verilog如下所示:
always @(*) begin mySignal = 1'b0; if(io_cond) begin mySignal = 1'b1; end end assign io_reg2out = myRegisterWithReset; always @(posedge clk or posedge reset) begin if(reset) begin myRegisterWithReset <= 4'b0000; end else begin if(io_cond) begin myRegisterWithReset <= (myRegisterWithReset + 4'b0001); end end end always @(posedge clk) begin if(io_cond) begin myRegister <= (myRegister + 4'b0001); end end
清晰定义 vs 不清晰的定义(Implicit vs explicit definitions)
在VHDL中, 当你声明一个信号, 你不会指定它是组合逻辑的信号还是寄存器。赋值的位置和方式决定它的数据类型。
在SpinalHDL中, 这些信号的类型是清晰的, 寄存器以他们的定义方式直接定义为寄存器。
时钟域(Clock domains)
在VHDL中, 每当你定义一组寄存器, 你需要一并定义时钟和他们的复位信号。此外你还需要定义这些时钟和复位信号的行为(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会被应用到这个区域内所有的子模块中 // ... }
上述代码生成的Verilog如下所示:
always @(posedge io_coreClk or posedge io_coreReset) begin if(io_coreReset) begin coreArea_myCoreClockedRegister <= 8'h0; end else begin coreArea_myCoreClockedRegister <= io_grey; end end
模块的内部组织(Component’s internal organization)
在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 } }
上述代码生成的Verilog如下:
always @(*) begin io_overflow = 1'b0; if(!when_Main_l11) begin io_overflow = 1'b1; end end assign when_Main_l11 = (timeout_counter != 8'h64); assign io_counter_num = timeout_counter; always @(posedge clk or posedge reset) begin if(reset) begin timeout_counter <= 8'h0; end else begin if(when_Main_l11) begin timeout_counter <= (timeout_counter + 8'h01); end if(io_overflow) begin timeout_counter <= 8'h0; end end end
定义在
Area
中的信号和变量在这个组分内的任何位置都是可以访问的, 也包括其他的Area
区域安全性(Safety)
在VHDL和SpinalHDL中, 很容易在无意间写出组合逻辑环, 或者因为忘记驱动处理流路径上的信号而引入latch。
那么, 为了检测这些问题, 你可以用
lint
工具帮你分析VHDL, 但是这些工具都是付费的。在SpinalHDL中,lint
处理过程集成到了编译器内部, 除非所有的问题都被排查才会生成RTL, 这个编译器还会检查时钟域交叉;函数和程序(Functions and procedures)
因为函数和程序在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) }
上述代码生成的Verilog如下:(与上述代码有所不同的是增加了位宽对其操作, 即(io.a+io.b).resize(8 bits)
wire [3:0] _zz_result; wire [3:0] _zz_result_1; reg [7:0] result; assign _zz_result = (io_a + io_b); assign _zz_result_1 = (io_a - io_b); assign io_c = result; always @(posedge clk) begin case(io_op) 4'b0000 : begin result <= {4'd0, _zz_result}; end 4'b0001 : begin result <= {4'd0, _zz_result_1}; end 4'b0010 : begin result <= (io_a * io_b); end default : begin end endcase end
下面是将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() }
上述代码生成的Verilog如下:
reg [7:0] counter; wire when_Main_l15; assign when_Main_l15 = (8'h2a < counter); assign io_cnt = counter; always @(posedge clk or posedge reset) begin if(reset) begin counter <= 8'h0; end else begin counter <= (counter + 8'h01); if(when_Main_l15) begin counter <= 8'h0; end end end
总线和接口(Buses and Interfaces)
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);(
(2) 可以使用记录(records)但不能参数化(在每个代码包中所静态固定的), 并且你需要定义双向:
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,regFileReadKind =sync, branchPrediction = dynamic, bypassExecute0 = true, bypassExecute1 = true, bypassWriteBack = true, bypassWriteBackBuffer = true, collapseBuddle = false, fastFetchCmdPcCalculation = true, dynamicBranchPredictorCacheSizeLog2 = 7 ) //CPU有一套能够在内核中增加新特征的接口 //这些拓展不是直接实现在内核中, 而是在一个分离的区域(block)中定义的额外逻辑 coreConfig.add(new MulExtension) coreConfig.add(new DivExtension) coreConfig.add(new BarrelShifterFullExtension) val iCacheConfig = InstructionCacheConfig( cacheSize = 4096, bytePerLine = 32, wayCount = 1, //现在只能是4 wrappedMemAccess = true, addressWidth = 32, cpuDataWidth = 32, memDataWidth = 32 ) new RiscvCoreAxi4( coreConfig = coreConfig, iCacheConfig = iCacheConfig, dCacheConfig = null, debug = debug, interruptCount = interruptCount )
信号声明(Signal declaration)
令人烦恼的是, VHDL迫使你在架构描述的最顶端定义所有的信号。
..
..(许多信号声明)
..
signal a : std_logic;
..
..(许多信号声明)
..
a <= x & y
..
..(许多信号声明)
..
在信号声明的方面, SpinalHDL则具有更好的灵活性
val a = Bool
a := x & y
也可以一行完成定义
val a = x & y
块例化(Component instantiation)
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()
// 如果你想要访问除法器的IO信号
divider.io.cmd.valid := True
divider.io.cmd.numerator := 42
转换关系(Casting)
在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返回布尔值
上述代码生成Verilog如下:
assign io_valBiggerThanTwo = (8'h02 < io_value);
unsigned/integer:
val array = Vec(UInt(4 bits), 8)
val sel = UInt(3 bits)
val arraySel = array(sel) //数组直接用UInt标号
上述代码生成Verilog如下:
reg [3:0] _zz_arraySel;
wire [3:0] array_0;
wire [3:0] array_1;
wire [3:0] array_2;
wire [3:0] array_3;
wire [3:0] array_4;
wire [3:0] array_5;
wire [3:0] array_6;
wire [3:0] array_7;
wire [2:0] sel;
wire [3:0] arraySel;
always @(*) begin
case(sel)
3'b000 : _zz_arraySel = array_0;
3'b001 : _zz_arraySel = array_1;
3'b010 : _zz_arraySel = array_2;
3'b011 : _zz_arraySel = array_3;
3'b100 : _zz_arraySel = array_4;
3'b101 : _zz_arraySel = array_5;
3'b110 : _zz_arraySel = array_6;
default : _zz_arraySel = array_7;
endcase
end
assign io_valBiggerThanTwo = (8'h02 < io_value);
assign arraySel = _zz_arraySel;
改变大小(Resizing)
实际上, VHDL对bit宽度的严谨要求是很好的
my8BitsSignal <= resize(my4BitsSignal, 8);
在SpinalHDL中, 有两种方法可以做到对bit宽度的精准定义
//传统方法
my8BitsSignal := my4BitsSignal.resize(8)
//更好的方法
my8BitsSignal := my4BitsSignal.resized
上述代码均会生成如下Verilog:
assign my8BitsSignal = {4'd0, my4BitsSignal};
参数化(Parameterization)
VHDL在2008版本修订之前, 泛型(generics)上存在很多问题。例如, 你不能参数化记录, 你不能在实例中参数化数组, 并且不能有变量类型。
之后VHDL2008出现并且解决了这些问题。但是出于供应商的原因, RTL工具对VHDL2008的支持十分不友好。
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)
cal sink = master(Stream(payloadType))
}
// ...
}
大型硬件电路描述(Meta hardware description)
VHDL更像是一种封闭的语法, 你无法在它的顶层增加抽象层。
而因为SpinalHDL所建立在Scala的基础上的, 非常的灵活, 允许编程人员很容易地定义新的抽象层。
可以在FSM库, BusSlaveFactory库, JTAG库中体验这种灵活性。
二、VHDL等效(VHDL equivalences)
架构与实例(Entity and architecture)
在SpinalHDL中, VHDL实例和架构都定义在
Component
里。以下是一个有着三输入(a, b, c)和一个输出(result)的模块的例子。这个模块也有
offset
结构体参数(如同VHDL中的类指)case class Mycomponent(offset: Int) extends Component { val io = new Bundle{ val a, b, c = in UInt(8 bits) val result = out UInt(8 bits) } io.result := io.a + io.b + io.c + offset }
之后如要实例化这个模块, 你不需要再对端口逐一绑定:
case class TopLevel extends Component { ... val mySubCompoent = MyComponent(offset = 5) ... mySubComponent.io.a := 1 mySubComponent.io.b := 2 mySubComponent.io.c := 3 ??? := mySubComponent.io.result ... }
上述代码生成的Verilog如下所示:(方便起见增加了一些额外信号, 不影响上述代码逻辑)
module(...); ... wire [7:0] mySubComponent_io_result; MyComponent mySubComponent ( .io_a (8'h01 ), //i .io_b (8'h02 ), //i .io_c (8'h03 ), //i .io_result (mySubComponent_io_result[7:0]) //o ); assign io_grey = (_zz_io_grey + mySubComponent_io_result); endmodule module MyComponent ( input [7:0] io_a, input [7:0] io_b, input [7:0] io_c, output [7:0] io_result ); wire [7:0] _zz_io_result; wire [7:0] _zz_io_result_1; assign _zz_io_result = (_zz_io_result_1 + io_c); assign _zz_io_result_1 = (io_a + io_b); assign io_result = (_zz_io_result + 8'h05); endmodule
数据类型(Data types)
Spinal的数据类型类似于VHDL的:
| VHDL | SpinalHDL | | :————–: | :——-: | | std_logic | Bool | | std_logic_vector | Bits | | unsigned | UInt | | signed | SInt |
在VHDL中, 为了定义一个8bit
unsigned
你需要给定其范围, 即unsigned(7 downto 0)
, 然而在SpinalHDL中你只需要声明其bit位数即可UInt(8 bits)
VHDL | SpinalHDL |
---|---|
records | Bundle |
array | Vec |
enum | SpinalEnum |
以下是SpinalHDL中Bundle
的定义, channelWidth
是结构体参数, 如同VHDL的泛型:
case class RGB(channelWidth: Int) extends Bundle {
val r, g, b = UInt(ChannelWidth bits)
}
之后如果想实例化一个Bundle
, 需要写``val myColor = RGB(channelWidth=8)`.
信号(Signal)
这是一个信号实例化的例子:
case class Mycomponent(offset: Int) extends Component { val io = new Bundle { val a, b, c = UInt(8 bits) val result = UInt(8 bits) } val ab = UInt(8 bits) ab := io.a + io.b val abc = ab + io.c //能够直接用上一步的值定义信号 io.result := abc + offset }
上述代码生成的Verilog如下:
module MyComponent ( input [7:0] io_a, input [7:0] io_b, input [7:0] io_c, output [7:0] io_result ); wire [7:0] ab; wire [7:0] abc; assign ab = (io_a + io_b); assign abc = (ab + io_c); assign io_result = (abc + 8'h05); endmodule
赋值(Assignments)
在SpinalHDL中,
:=
赋值操作符等价于VHDL中的信号赋值<=
:val myUInt = UInt(8 bits) myUInt := 6
条件赋值可以通过
if
/case
声明实现, 如同VHDL一样:val clear = Bool() val counter = Reg(UInt(8 bits)) when(clear) { counter := 0 }.elsewhen(counter === 76) { counter := 79 }.otherwise { counter(7) := ! counter(7) } switch(counter) { is(42) { counter := 65 } default { counter := counter + 1 } }
上述代码生成的Verilog如下所示:(上述逻辑直接编译汇报错, 因为counter赋值有冲突, 稍作更改生成如下同等逻辑代码)
reg [7:0] counter; wire when_Main_l21; wire when_Main_l23; assign when_Main_l21 = (counter == 8'h4c); assign when_Main_l23 = (counter == 8'h2a); always @(posedge clk or posedge reset) begin if(reset) begin counter <= 8'h0; end else begin if(io_clear) begin counter <= 8'h0; end else begin if(when_Main_l21) begin counter <= 8'h4f; end else begin if(when_Main_l23) begin counter <= 8'h41; end else begin counter[7] <= (! counter[7]); counter[6 : 0] <= (counter[6 : 0] + 7'h01); end end end end end
程序的语言描述(Literals)
和VHDL相比程序的语言描述形式有一些不同:
val myBool = Bool() myBool := False myBool := True myBool := Bool(4 > 7) val myUInt = UInt(8 bits) myUInt := "0001_1100" myUInt := "xEE" myUInt := 42 myUInt := U(54, 8 bits) myUInt := ((3 downto 0) -> myBool, default -> true) //有所不同 when(myUInt === U(myUInt.range -> true)) { //也有所不同 myUInt(3) := False }
上述代码生成的Verilog如下所示:(以下assign语句均相互独立生成, 只是集中呈现在这里)
wire myBool; wire [7:0] myUInt; assign myBool = 1'b0; assign myUInt = 8'h1c; assign myBool = 1'b1; assign myUInt = 8'hee; assign myBool = 1'b0; assign myUInt = 8'h2a; assign myUInt = 8'h36; //有所不同 wire myBool; reg [7:0] myUInt; assign myBool = 1'b0; always @(*) begin myUInt = 8'hff; myUInt[3] = myBool; myUInt[2] = myBool; myUInt[1] = myBool; myUInt[0] = myBool; end //也有所不同 wire myBool; reg [7:0] myUInt; wire [7:0] _zz_when_Main_l17; wire when_Main_l17; assign myBool = 1'b0; assign _zz_when_Main_l17[7 : 0] = 8'hff; assign when_Main_l17 = (myUInt == _zz_when_Main_l17); assign io_output = myBool; always @(posedge clk or posedge reset) begin if(reset) begin myUInt <= 8'h0; end else begin if(when_Main_l17) begin myUInt[3] <= 1'b0; end end end
备注:需要注意的是, SpinalHDL与Scala对大小写的区分尤为严格, True代表Bool(), true代表Boolean(), false同理。
寄存器(Registers)
在SpinalHDL中, 寄存器是被清晰地指定的, 而VHDL中则是根据赋值方式推断出的。以下是SpinalHDL的寄存器例子:
val counter = Reg(UInt(8 bits)) init(0) counter := counter + 1 //每周期自增1 //init(0)意味着当复位信号有效, 寄存器初始化为0
上述代码生成的Verilog如下:
reg [7:0] counter; always @(posedge clk or posedge reset) begin if(reset) begin counter <= 8'h00; end else begin counter <= (counter + 8'h01); end end
进程块(Process blocks)
进程块(process block)是一种仿真特征, 在RTL设计中并非必要。这就是为什么SpinalHDL没有包含任何对进程块的类, 所以你可以按照你所想的去给任何地方赋值。
val cond = Bool() val myCombinatorial = Bool() val myRegister = UInt(8 bits) myCombinatorial := False when(cond) { myCombinatorial := True myRegister := myRegister + 1 }
上述代码生成的Verilog如下所示:
wire cond; reg myCombinatorial; reg [7:0] myRegister; always @(*) begin myCombinatorial = 1'b0; if(cond) begin myCombinatorial = 1'b1; end end assign cond = io_input; always @(posedge clk or posedge reset) begin if(reset) begin myRegister <= 8'h0; end else begin if(cond) begin myRegister <= (myRegister + 8'h01); end end end