03 硬件语言筑基(一):从硬件语言开启手写CPU之旅
你好,我是LMOS。
我们都知道,自己国家的芯片行业被美国“吊打”这件事了吧?尤其是像高端CPU这样的芯片。看到相关的报道,真有一种恨铁不成钢的感觉。你是否也有过想亲自动手设计一个CPU的冲动呢?
万丈高楼从地起,欲盖高楼先打地基,芯片是万世之基,这是所有软件基础的开始,这个模块我会带你一起设计一个迷你RISC-V处理器(为了简单起见,我选择了最火热的RISCV芯片)。哪怕未来你不从事芯片设计工作,了解芯片的工作机制,也对写出优秀的应用软件非常重要。
这个处理器大致是什么样子呢?我们将使用Verilog硬件描述语言,基于RV32I指令集,设计一个32位五级流水线的处理器核。该处理器核包括指令提取单元、指令译码单元、整型执行单元、访问存储器和写回结果等单元模块,支持运行大多数RV32I的基础指令。最后,我们还会编写一些简单汇编代码,放在设计出来的处理器上运行。
我会通过两节课的篇幅,带你快速入门Verilog,为后续设计迷你CPU做好准备。这节课我们先来学习硬件描述语言基础,芯片内部的数字电路设计正是由硬件语言完成的。
一个芯片的内部电路是怎么样的?
作为开发,你日常最常用的编程语言是什么?也许是C语言、Java、Go、PHP……这些高级编译语言吧。而硬件设计领域里,也有专门的硬件描述语言。为什么会出现专门的硬件描述语言呢?这还要先从芯片的内部结构说起。
一般情况下,你所接触到的处理器芯片,已经不是传统意义上的CPU了,比如在业界很有名的国产手机芯片华为麒麟990芯片。我把麒麟990的功能模块图贴在了后面,对照图片会更直观。这样一款芯片,包含了CPU核、高速缓存、NPU、GPU、DDR、PMU等模块。
而在芯片设计时,根据不同模块的功能特点,通常把它们分为数字电路模块和模拟电路模块。
模拟电路还是像早期的半导体电路那样,处理的是连续变化的模拟信号,所以只能用传统的电路设计方法。而数字电路处理的是已经量化的数字信号,往往用来实现特定的逻辑功能,更容易被抽象化,所以就产生了专门用于设计数字电路的硬件描述语言。
硬件描述语言从发明到现在,已经有20多年历史。硬件描述语言可以让你更直观地去理解数字电路的逻辑关系,从而更方便地去设计数字电路。
现在业界的 IEEE标准主要有VHDL和Verilog HDL 这两种硬件描述语言。在高层次数字系统设计领域,大部分公司都采用 Verilog HDL完成设计,我们后面的实现也会用到Verilog。
千里之行,始于足下。在Verilog学习之前,我们需要先完成思路转换,也就是帮你解决这个问题:写软件代码和写硬件代码的最大区别是什么?搞明白了这个问题,你才能更好地领会Verilog语言的设计思想。
Verilog代码和C语言、Java等这些计算机编程语言有本质的不同,在可综合(这里的“可综合”和代码“编译”的意思差不多)的Verilog代码里,基本所有写出来的东西都对应着实际的电路。
所以,我们用Verilog的时候,必须理解每条语句实质上对应着什么电路,并且要从电路的角度来思考它为何要这样设计。而高级编程语言通常只要功能实现就行。
我再举几个例子来说明:声明变量的时候,如果指定是一个reg,那么这个变量就有寄存数值的功能,可以综合出来一个实际的寄存器;如果指定是一段wire,那么它就只能传递数据,只是表示一条线。在Verilog里写一个判断语句,可能就对应了一个MUX(数据选择器),写一个for可能就是把一段电路重复好几遍。
最能体现电路设计思想的就是always块了,它可以指定某一个信号在某个值或某个跳变的时候,执行块里的代码。通过使用Verilog语言,我们就能完成芯片的数字电路设计工作了。没错,芯片前端设计工程师写Verilog代码的目的,就是把一份电路用代码的形式表示出来,然后由计算机把代码转换为所对应的逻辑电路。
芯片如何设计?
说到这里你可能还有疑惑,听起来芯片设计也没那么复杂啊?其实这事儿说起来简单,但实践起来却相当复杂。接下来,我就说说,一个工业级的芯片在设计阶段大致会怎么规划。
在开始一个大的芯片设计时,往往需要先从整个芯片系统做好规划,在写具体的Verilog代码之前,把系统划分成几个大的基本的功能模块。之后,每个功能模块再按一定的规则,划分出下一个层次的基本单元。
这和Verilog语言的module模块化设计思想是一致的,上一层模块对下一层子模块进行例化,就像其他编程语言的函数调用一样。根据包含的子功能模块一直例化下去,最终就能形成hierarchy结构。
这种自顶向下的设计方法,可以用后面的树状结构图来表示:
从上图我们也可以看出,Verilog都是基于模块进行编写的,一个模块实现一个基本功能,大部分的Verilog逻辑语句都放在模块内部。
从一段代码入门Verilog
说完语言思路和硬件的模块化设计,接下来,我带你学习一下Verilog的基本模块和逻辑语句是怎么写的。
很多Verilog初学者刚开始都是从一些基础知识慢慢去看,比如基本语法、数据类型、赋值语句、条件语句……总想着把Verilog的全部基础知识看完了,再开始动手写代码。
但是你有没有想过,这些详细的基础知识,一两天自然是看不完的。而当你坚持了一段时间把它看完,以为可以上手写代码的时候,又会发现前面的基本语句全都忘了。这样的学习方法并不可取,效果也不好,所以我换个方法带你入门。我们先不去罗列各种详细的基础知识,而是从学习一段代码开始。
我会以一个4位十进制计数器模块为例,让你对Verilog模块代码有更直观的认识,然后根据这段代码模块,给你讲讲Verilog语言基础。这里先把完整代码列出来,后面再详细拆解。
module counter(
//端口定义
input reset_n, //复位端,低有效
input clk, //输入时钟
output [3:0] cnt, //计数输出
output cout //溢出位
);
reg [3:0] cnt_r ; //计数器寄存器
always@(posedge clk or negedge reset_n) begin
if(!reset_n) begin //复位时,计时归0
cnt_r <= 4'b0000 ;
end
else if (cnt_r==4'd9) begin //计时10个cycle时,计时归0
cnt_r <=4'b0000;
end
else begin
cnt_r <= cnt_r + 1'b1 ; //计时加1
end
end
assign cout = (cnt_r==4'd9) ; //输出周期位
assign cnt = cnt_r ; //输出实时计时器
endmodule
看了这段代码,也许你云里雾里,或者之前没接触过硬件语言,心里有点打鼓。不过别担心,入门硬件语言并不难,我们按照代码顺序依次来看。
模块结构
首先,让我们看一看这段代码的第一行和最后一行。没错,一个模块的定义是以关键字module开始,以endmodule结束。module关键字后面跟的counter就是这个模块的名称。
看着有没有熟悉的感觉?你可能觉得,这个看着跟其他编程语言的函数定义也没多大区别吧?别急着下结论,再仔细看看接口部分,发现没有?这就和函数传入的参数很不一样了。
module counter(
//接口部分
input reset_n,
input clk,
output [3:0] cnt,
output cout
);
…… //逻辑功能部分
endmodule
Verilog模块的接口必须要指定它是输入信号还是输出信号。
输入信号用关键字input来声明,比如上面第4行代码的 input clk;输出信号用关键字output来声明,比如代码第5行的output [3:0] cnt;还有一种既可以输入,又可以输出的特殊端口信号,这种双向信号,我们用关键字inout来声明。
数据类型
前面我提到过,在可综合的Verilog代码里,基本所有写出来的东西都会对应实际的某个电路。而Verilog代码中定义的数据类型就能充分体现这一点。
parameter SIZE = 2’b01;
reg [3:0] cnt_r;
wire [1:0] cout_w;
比如上面代码的第9行,表示定义了位宽为4 bit 的寄存器reg类型信号,信号名称为cnt_r。
寄存器reg类型表示抽象数据存储单元,它对应的就是一种寄存器电路。reg默认初始值为X(不确定值),换句话说就是,reg电路在上电之后,输出高电平还是低电平是不确定的,一般是在系统复位信号有效时,给它赋一个确定值。比如例子中的cnt_r,在复位信号reset_n等于低电平时,就会给cnt_r赋“0”值。
reg类型只能在always和inital语句中被赋值,如果描述语句是时序逻辑,即always语句中带有时钟信号,寄存器变量对应为触发器电路。比如上述定义的cnt_r,就是在带clk时钟信号的always块中被赋值,所以它对应的是触发器电路;如果描述语句是组合逻辑,即always语句不带有时钟信号,寄存器变量对应为锁存器电路。
我们常说的电子电路,也叫电子线路,所以电路中的互连线是必不可少的。Verilog代码用线网wire类型表示结构实体(例如各种逻辑门)之间的物理连线。wire类型不能存储数值,它的值是由驱动它的元件所决定的。驱动线网类型变量的有逻辑门、连续赋值语句、assign等。如果没有驱动元件连接到线网上,线网就默认为高阻态“Z”。
为了提高代码的可读性和可维护性,Verilog还定义了一种参数类型,通过parameter来声明一个标识符,用来代表一个常量参数,我们称之为符号常量,即标识符形式的常量。这个常量,实际上就是电路中一串由高低电平排列组成的一个固定数值。
数值表达
说到数值,我们再了解一下Verilog中的数值表达。还是以前面的4位十进制计数器代码为例,我们定位到第13行代码:
cnt_r <= 4’b0000;
这行代码的意思是,给寄存器cnt_r赋以4’b0000的值。
这个值怎么来的呢?其中的逻辑“0”低电平,对应电路接地(GND)。同样的,逻辑“1”则表示高电平,对应电路接电源VCC。除此之外,还有特殊的“X”和“Z”值。逻辑“X”表示电平未知,输入端存在多种输入情况,可能是高电平,也可能是低电平;逻辑“Z”表示高阻态,外部没有激励信号,是一个悬空状态。
当然,为了代码的简洁明了,Verilog可以用不同的格式,表示同样的数值。比如要表示4位宽的数值“10”,二进制写法为4’b1010,十进制写法为4’d10,十六进制写法为4’ha。这里我需要特殊说明一下,数据在实际存储时还是用二进制,位宽表示储存时二进制占用宽度。
运算符
接下来我们看看Verilog的运算符,对于运算符,Verilog和大部分的编程语言的表示方法是一样的。
比如算术运算符 + - * / % ,关系运算符 > < <= >= == !=,逻辑运算符 && || !(与或非),还有条件运算符 ? ,也就是C语言中的三目运算符。例如a?b:c,表示a为真时输出b,反之为c。
但在硬件语言里,位运算符可能和一些高级编程语言不一样。其中包括 ~ & | ^(按位取反、按位与,按位或,以及异或);还有移位运算符,左移 << 和右移>> ,在生成实际电路时,左移会增加位宽,右移位宽保存不变。
条件、分支、循环语句
还有就是条件语句if和分支语句case,由于它们的写法和其它高级编程语言几乎一样,基本上你掌握了某个语言都能理解。
这里我们重点来对比不同之处,也就是用Verilog实现条件、分支语句有什么不同。用if设计的语句所对应电路是有优先级的,也就是多级串联的MUX电路。而case语句对应的电路是没有优先级的,是一个多输入的MUX电路。设计时,只要我们合理使用这两个语句,就可以优化电路时序或者节省硬件电路资源。
此外,还有循环语句,一共有 4 种类型,分别是 while,for,repeat和 forever 循环。注意,循环语句只能在 always 块或 initial 块中使用。
过程结构
下面我们来说说过程结构,最能体现数字电路中时序逻辑的就是always语句了。always 语句块从 0 时刻开始执行其中的行为语句;每当满足设定的always块触发条件时,便再次执行语句块中的语句,如此循环反复。
因为always 语句块的这个特点,芯片设计师通常把always块的触发条件,设置为时钟信号的上升沿或者下降沿。这样,每次接收到一个时钟信号,always块内的逻辑电路都会执行一次。
前面代码例子第11行的always语句,就是典型的时序电路设计方法,有没有感觉到很巧妙?
always@(posedge clk or negedge rstn) begin
…… //逻辑语句
end
还有一种过程结构就是initial 语句。它从 0 时刻开始执行,且内部逻辑语句只按顺序执行一次,多个 initial 块之间是相互独立的。理论上,initial 语句是不可以综合成实际电路的,多用于初始化、信号检测等,也就是在编写验证代码时使用。
到这里,在我看来比较重要的Verilog基础知识就讲完了,这门语言的知识脉络我也为你搭起了骨架。当然了,Verilog相关知识远远不止这些。如果你对深入学习它很感兴趣,推荐你翻阅《Verilog HDL高级数字设计》等相关资料拓展学习。
总结回顾
今天是芯片模块的第一节课,我们先了解了芯片的内部电路结构。一个芯片的内部电路往往分为数字电路模块和模拟电路模块。对于数字电路模块,可以使用Verilog硬件描述语句进行设计。
尽管Verilog这样的硬件语言你可能不大熟悉,但只要抓住本质,再结合代码例子建立知识脉络,学起来就能事半功倍。
要想熟悉硬件语言,我们最关键的就是做好思路转换。硬件语言跟高级编程语言本质的不同就是,使用Verilog的时候,必须理解每条语句实质上对应的什么电路,并且要从电路的角度来思考它为何要这样设计,而高级编程语言通常只要实现功能就行。
我再带你回顾一下,Verilog语言和高级编程语言具体有哪些不同:
1.模块结构:Verilog的模块结构和其他语言的函数定义不一样,它既可以有多个输入信号,也可以输出多个结果。而且,模块上的接口信号,必须要指定是输入信号和输出信号。
2.数据类型:跟我们在高级编程语言见到的变量类型相比,Verilog定义的数据类型也有很大不同。reg类型对应的是寄存器电路,wire类型对应的是电路上的互连线,标识符对应的是一串固定的高低电平信号。
3.数据表达:Verilog代码中的数据,本质上就是高低电平信号。“0”代表低电平,“1”代表高电平,不能确定高低电平的就用“X”来表示。
4.运算符:Verilog中的大部分运算符和其他语言是一样的,但是要注意位操作运算符,它们对应的是每一位电平按指定逻辑跳变,还有移位操作,一定要注意移位信号的数据位宽。
5.条件、分支、循环语句:Verilog中的条件if语句是有优先级的,而case语句则没有优先级,合理利用它们可以优化电路时序或节省硬件电路资源。循环语句则是把相同的电路重复好几遍。
6.过程结构:这是实现时序电路的关键。我们可以利用alway块语句设定一个时钟沿,用来触发相应逻辑电路的执行。这样,我们就可以依据时钟周期来分析电路中各个信号的逻辑跳变。而initial语句常在验证代码中使用,它可以从仿真的0时刻开始设置相关信号的值,并将这些值传输到待验证模块的端口上。
下节课,我会带你设计一个简单的电路模块,既能帮你复习今天学到的知识,还能通过实践体会一下代码是怎样生成电路的,敬请期待。
思考题
为什么很多特定算法,用Verilog设计并且硬件化之后,要比用软件实现的运算速度快很多?
欢迎你在留言区跟我交流互动,也推荐你把这节课分享给更多朋友。
- 青玉白露 👍(17) 💬(1)
感觉大家很吃力,做了一个关于Verilog的初学者笔记,里面包含在线学习网站https://zhuanlan.zhihu.com/p/550710744
2022-08-06 - 肖水平 👍(14) 💬(1)
思考题: 根本原因就是专用芯片运算速度比GPU快,GPU运算速度比CPU快,而软件实现的算法是运行在CPU上的。 具体原因如下: 1、指令数多:软件实现的算法最终要编译成CPU可执行的机器指令,中间还会有很多控制指令; 2、CPU指令执行步骤多:CPU执行一条指令需要经过取指、译码、执行、访存、写回步骤; 3、多位运算需要拆分:比如128位的运算在32/64位CPU上执行编译后会拆分成多条指令; 4、将算法硬件化后可以直接执行运算,不需要通过指令来控制,多位运算也可以实现多位运算模块来直接运算; 总之软件实现的算法在CPU消耗的时钟数要多很多,而CPU也只擅长控制,不擅长运算。
2022-08-03 - 爱酱大胜利 👍(8) 💬(1)
思考题: 1.设计的目的 CPU属于“通用性”设计 一个CPU能运行这个算法也能运行那个算法 改改代码就可以 代价就是翻译过程因为通用性所带来的迁就 而硬件化的算法属于“专用性”设计 也就是说它只能做着一件事所以怎么设计方便怎么来 反正以后不用为了实现其他算法再改电路了 这也是软硬件实现的区别之一 2.运行效率 还是由于上一条的设计理念 导致软件的运行依赖于CPU实现的特定指令集 原本算法中一个硬件周期的动作可能被转化为多条指令 而每条指令可能会依赖多个运行周期 即使运行会得到优化 也远远比不上一个周期来的快 而且CPU为了通用性很可能不会将这种特殊性动作添加到一个不常用CISC里
2022-08-04 - peter 👍(8) 💬(3)
请教老师几个问题: Q1:代码转化后的逻辑电路,只是电子版的“设计电路”,并不是实际的电路,对吗? 文中有这样一句:“把一份电路用代码的形式表示出来,然后由计算机把代码转换为所对应的逻辑电路。”,此处,“转换后的逻辑电路”并不是实际的电路,应该只是“设计电路”。 Q2:“cnt_r <=4'b000;”? 此处应该是四个0吧,为什么写成三个0?笔误吗? Q3:芯片中的模拟电路是用什么设计的? Q4:芯片前端设计的代码,就是用verilog写的代码,对于一个芯片来说,一般规模多大?比如十万行代码?
2022-08-01 - 哈哈 👍(5) 💬(1)
思考题我认为,软件实现要在通用的硬件上执行就势必要执行一些逻辑上的模拟或者转换。而专用的硬件则没有这些转换过程,并且可以针对性的优化执行路径,所以要快许多。
2022-08-01 - LockedX 👍(3) 💬(2)
老师,有个问题一直没有想明白 reg [3:0] cnt_r; wire [1:0] cout_w; reg和wire的声明,冒号前面是数字代表位宽,冒号后面的0含义是什么?
2022-08-01 - 青蓝 👍(2) 💬(1)
FPGA编程和今天讲得编程有什么区别吗?
2022-08-01 - 可爱因子1/n 👍(1) 💬(1)
针对我自己的理解,打个比喻:在性能和单位时间以及油门相同的情况下,我开着一辆车的发动机的转速和一个直接裸的发动机的转速比较,裸的发动机的转速按常理来说应该会更快一点,不知道我的比喻是否准确?
2022-08-06 - 111 👍(1) 💬(1)
关于思考题: 我的理解是 Verilog更多面向的是硬件,通过它设计完成之后,可以直接参照其代码流程组合相关硬件,在针对特定算法的设计后,组合出来的就是该算法最“合身”的底层电路模型。 而对于一些软件语言的实现,更多的是业务层面的实现,业务实现之后还要面临“翻译”,翻译成计算机能够执行的指令集,这一步就已经开始出现了效率损耗,同时因为“翻译”动作,也就可能出现为了实现某一功能,底层指令可能要绕一些“弯路”才能实现,毕竟不能像Verilog那样,做到“完美贴切”的电路设计
2022-08-02 - nmg 👍(1) 💬(1)
是因为用硬件实现是算法最优化电路设计,而软件实现可能是芯片现有资源的组合,不是最优电路,所以会慢吗?
2022-08-01 - Geek_785f19 👍(1) 💬(2)
请教老师: 平时做的是业务系统,对底层不熟悉,有兴趣多了解底层知识,学习本课程需要什么前置知识吗
2022-08-01 - 苏流郁宓 👍(1) 💬(1)
用 Verilog 设计并且硬件化之后,要比用软件实现的运算速度快很多? 俺觉得软件需要译码,消耗时间。还有软件功能化设计,需要用到硬件的不同模块,光是沟通自然也消耗时间的。更不用说分支预测失败,重来也消耗时间的 但硬件有个难度,就是越复杂的东西,用软件高级语言开发更容易,比如高级语言开发浏览器比光靠汇编等低级语言开发难度容易得多! 软件与硬件取平衡,开发看那个性价比高,更贴近实用的啊
2022-08-01 - IgoZhang 👍(0) 💬(1)
老师,我发现一个重要问题没讲,导致没法连贯: Verilog 是啥,我的意思是它干啥用,它的输出是啥? 他是一个硬件描述语言,但是的干啥用呢? 是否可以举例说明什么时候使用Verilog写了啥,然后写出来的结果用于做啥? 我还以为是汇编呢? 有点理解不好
2023-03-10 - 南总无敌 👍(0) 💬(1)
思考题:为什么很多特定算法,用 Verilog 设计并且硬件化之后,要比用软件实现的运算速度快很多? 答:因为用verilog设计硬件化之后,算法能够用更少的指令和SIMD(单指令多数据)的方式执行,例如NPU执行卷积运算时候只要配置好卷积参数就能一条指令执行,并且数据能够实现部分复用。但CPU需要拆解成为多条指令,每条指令都必须完整走完5个执行步骤,这样花费的cycles数就会成倍上涨
2022-10-07 - xhh 👍(0) 💬(1)
请问老师在Linux中的Verilog编辑器是VSCode吗还是什么
2022-09-19