SpinalHDL入门教程(序言)

SpinalHDL入门教程(序言)

发布者:RedRustacean
阅读时长:17分钟 词数:3293

分类: 数字电路

本文是SpinalHDL的详细入门教程(序言),在包含官方文档内容之余,对可能语焉不详的部分进行补充。本系列将SpinalHDL视作半个全新的语言,也会用到Verilog中的一些知识,但不会特别深奥。建议读者有数字电路的预备知识,这对于学习任何一门HDL都是必要的。如果读者有Verilog基础,那么本系列的难度应该恰好合适;如果读者仅有一些其它编程语言的经历,也可以尝试阅读本系列。

什么是SpinalHDL

SpinalHDL是基于Scala语言框架设计的硬件描述语言(Hardware Description Language)。HDL指的是一类语言,而非某一门特定的语言。其中比较经典的有VHDLVerilog HDLSystemVerilog,近几年出现的Chisel和SpinalHDL也吸引了众多数字电路设计者。它们都能描述数字电路设计,并最终落定到物理实现。

为什么要有新的HDL

我们可以回顾一下传统派的HDL的特点。传统的HDL都支持从门级行为级的电路功能描述,门级描述很好理解,比如在Verilog中,我们要搭建一个异或门,可以这样写:

module verilog_xor(
    input  a,
    input  b,
    output out
);

assign out = a ^ b;

endmodule

这,就是一个用门级概念编写的组合逻辑模块。门级描述是HDL需要支持的最基础功能。然而,当我们需要描述一个较为复杂的组合逻辑时,例如3-8译码器,我们可以这样写:

module decoder_3_8(
    input  [2:0] in_3bit,
    output [7:0] out_8bit_onehot
);

assign out_8bit_onehot[0] = in_3bit == 3'b000;
assign out_8bit_onehot[1] = in_3bit == 3'b001;
assign out_8bit_onehot[2] = in_3bit == 3'b010;
assign out_8bit_onehot[3] = in_3bit == 3'b011;
assign out_8bit_onehot[4] = in_3bit == 3'b100;
assign out_8bit_onehot[5] = in_3bit == 3'b101;
assign out_8bit_onehot[6] = in_3bit == 3'b110;
assign out_8bit_onehot[7] = in_3bit == 3'b111;

endmodule

该书写形式其实很好理解:根据3-8译码器的特点,输出变量out_8bit_onehot应该是独热码的形式,所以out_8bit_onehot[0]应该在in_3bit等于3'b000的情况下置高电平;同理,out_8bit_onehot[1]应该在in_3bit等于3'b001时置高电平。以此类推,out_8bit_onehot所有引脚都接上了对应的判等表达式。判等表达式的结果是布尔值,是一位宽信号。为out_8bit_onehot的每个引脚分配输出后,该模块的功能就完备了。

那么这段代码最终能否综合为门级电路实现呢?当然可以。对比于直接用门构建模块,行为级的书写方式更易读,而且也能实现期望的功能。这种抽象的思想使我们更容易构建复杂的电路,因为综合器会帮我们落实电路实现,也方便工程师更好地审阅代码。

抽象以及改进抽象

我们回顾一下刚才的案例,代码的可读性看上去还能接受。提高一点需求,要编写一个4-16译码,我们级联两个3-8译码器即可。但是在实际的应用中,这样的译码情景还是太简单了。我们拿RISC-V处理器的指令译码举例,其基本原理同样是位匹配1,只不过匹配模式变得更复杂,而且译码结果需要给出指令类型、操作码、函数码、寄存器编号、立即数等信息。此处不详细展示实际代码,读者只需要知道:根据不同的抽象方法,实现指令译码器有难易之分。

过去的工程师也意识到抽象困难这一问题,于是提出了一些解决方案,例如在编写Verilog时使用很多宏定义参数来加强表意,或者用generate代码块来例化2多个相似的模块。这些做法一方面增加了易读性,另一方面也为参数化模块设计奠定基础。然后一些工程师直接想到推出一门新语言:SystemVerilog(以下简称SV),将宏3和函数的思想系统地用来表达电路设计。SV提升了抽象能力,确实解决了一些代码审阅和迭代的困难。SV和Verilog的代际关系很近,因此老练的Verilog工程师即便从未接触过SV,也能大致读懂SV代码。

1位匹配:对一个多位宽的变量,我们需要拿一个和它同位宽的常量或变量进行判等,这种操作称为“位匹配”。 2例化:将编写的模块实际放置在电路中,为其分配实际的晶体管资源。可类比Java等语言中“实例化”来帮助理解。 3宏:一个面向编译器(硬件设计中往往叫综合器)的语言概念,可以控制编译器行为,或作为代码功能的一部分。

HDL也开始进化

时间飞速流转,如同软件行业对编程语言的探索,硬件这一块也开始有人注重HDL的发展了。一门硬件描述语言的含义,不仅包含语言规范,还包含对应的工具链。大家发现,伴随着对大规模数字电路迭代的需求,传统派语言逐渐显现出至少以下几个弊端:

  • 查错困难:比如Verilog中的assign语句在位宽不匹配时:
wire [2:0] data_src;
wire [5:0] data_dst;
assign data_dst = data_src;

有的综合器并不会报错,因为Verilog也承认此类写法。这将把data_dst的高三位补为3'bxxx,而不是3'b000。而我们知道绝大多数数字电路设计中都会规避不定态的传播,因为它很可能导致意料之外的运算结果。当然此类错误也归咎于Verilog较宽松的语法限制。

  • 工具链不够便捷:比如大多数工具对Verilog的变量追踪以及自动补全等都比较孱弱,甚至有的大型工业软件对Verilog的高亮都不一定做得好 (说的就是Vivado,高亮不够有层次,而且这货的自动补全也很奇怪,就不展开说了)
  • 迭代困难:例如增减模块、重构流水线等事务,受限于传统派HDL的语言特性,工程师很难瞻前顾后。

然后,工程师开始了对新语言范式的探索。事实证明,人们在工程中积累的经验确实有所成效;犹如大浪淘沙,人们萃取出了设计语言中的核心理念,然后把它们重新放回实践。上一个成功的案例是Rust语言(这里有我自身的主观色彩,有必要点出),通过严谨的语法定义打通上层范式和底层范式。这样的理论与实践的反复腾挪也让各种编程语言趋于完备、易用。体现在HDL上,便是以Scala为基础的Chisel和SpinalHDL。

Chisel和SpinalHDL的故事

先贴出两者的官网,这是Chisel的,这是SpinalHDL的(没有专门的官网,这里放的是GitHub仓库)。两者都是基于Scala语言开发的HDL。

让我猜猜,可能有人会这样问:为啥基于已有的一门软件语言来设计?结合各种说法和笔者自身体会,我认为可能的原因有:

  • 从头搭建很麻烦:学过编译原理的朋友应该知道,编程语言的设计是一个繁琐的过程,落实到工程上还有不少细枝末节之处,更不用说持续维护工具链了;
  • Scala很灵活:根据一些熟悉Scala语言的程序员所说,Scala很适合“伪装”其它语言,在后面的文章中将详解“伪装”的含义;
  • Scala的思想适合硬件设计:该语言是面向对象的,而硬件设计天然面向对象;
  • Scala的靠山很稳:Java好歹也是工业领域成熟的解决方案了,Scala基于Java,所以其地基稳固,语言开发者几乎不需要考虑多平台兼容。

但是,我们仍然没弄明白为啥会出现Chisel和SpinalHDL,是以前的语言不够好吗?

是,也不是。先说“是”的一面:大家期望大规模芯片也能快速迭代,最好像服务器机柜那样高度模块化。举个例子,假如我们设计了一块CPU,我们希望划分出小、中、大三个规模的设计,但又不愿意重复编写代码,那么模块化和参数化的设计就便于快速生成具有不同核心数、不同子部件(如MMU、FPU、缓存等)的三档CPU。再大胆一些,我们希望像订购家具那样,将需要的参数写成表单,然后厂家按照要求生产需要的家具即可;而此时的“订单”等于配置文件,是参数化模块设计的新思路。

再说“不是”的一面:虽然VHDL和Verilog这类传统派的HDL显得原始且事无巨细,但它们在长期的数字电路开发过程中扮演了重要角色,有不少经典IP都用它们编写,我们没有必要用新生语言去重构那些“古董”。而且VHDL和Verilog更贴近实际电路实现,是EDA软件能直接开始综合的语言,也是纠正某些底层错误的直接途径(例如时序违例)。

Chisel在风口,为啥还要看SpinalHDL

简单来说,Chisel好,但不够好。

Chisel在2012年初出茅庐,虽然也有十余年的历史了,但是相对硬件工业这样缓慢的领域,它还是个毛头小子 (这么说来,SpinalHDL更是愣头青)。笔者印象中真正让Chisel在国内火出圈的项目是香山处理器。也有其它代表,例如更早一些的RocketChip。Chisel语言最早是Berkeley大学提出的,该语言催生出了众多开源项目,RISC-V处理器设计也算得上“王婆卖瓜,自卖自夸”了(RISC-V也是Berkeley大学提出的)。

开源项目风声水起,但眼下工业界却不见得很乐意采纳Chisel语言。Chisel的范式并非所有人都接受,而且在设计上也有一些欠缺考虑的痛点,例如时钟域的划分,显然是不如SpinalHDL那么优雅的。再者,从笔者对Chisel和SpinalHDL的比较来看,两者生成的Verilog代码,论可读性,SpinalHDL更胜一筹。之所以要关注生成的Verilog代码,是因为我们在查错时需要在SpinalHDL与Verilog中辗转;SpinalHDL生成的代码更干净易读,因此更易于查错(其实也是有代价的,那就是某些中间变量名可能非常长)。

参考此文:「SpinalHDL(一):此CHISEL非彼Chisel」,其中对Chisel与SpinalHDL的关系描述得足够准确,也解释了一部分选用SpinalHDL的原因。

当然,话说回来,基于Chisel语言的项目也有很多可借鉴的内容,只不过笔者更偏向于用SpinalHDL开发。都什么年代了,大家并不需要为某一个语言或工具“效忠”,兼容并包嘛。本系列教程也是一种探索,尝试发现SpinalHDL的亮点、痛点,着力于把概念讲清楚。好在SpinalHDL并非一门架空的语言,它有自己的招牌工程:VexRiscv,适合学习SpinalHDL的用法和思路(严格说这个项目的思路是插件化设计,是SpinalHDL能实现的多种范式的一种,笔者认为它的设计思路极其新颖,也会在后面单出一个系列讲VexRiscv)。

老派语言并未远去

Chisel和SpinalHDL都不是直接面对综合器、仿真器的语言。严谨地讲,它们是代码生成框架。在随后的教程中,笔者将介绍SpinalHDL如何落地到FPGA开发板上。其中关键一步就是在SpinalHDL代码中告诉编译器“我们需要生成Verilog或VHDL代码”,然后将编译所得Verilog或VHDL代码导入EDA软件(比如Intel Quartus或AMD Vivado),再制定时钟约束、管脚约束、综合,最后烧录到FPGA上即可(这是比较理想的顺境)。

但是这并不意味着SpinalHDL这类语言是无意义的。它们在语法层面减少了出错的可能性,也让电路设计的意图更清晰易读,最重要的是极大推动了参数化设计。笔者认为,“工欲善其事,必先利其器”,这句话放在HDL上也颇有道理。当语言框架足够好,那么开发者将省心不少,何尝不是提升了效率呢?

下期预告

下一篇文档讲SpinalHDL的部署,以及硬件的“Hello World”项目搭建。敬请期待。