learn-tech/专栏/计算机基础实战课/03硬件语言筑基(一):从硬件语言开启手写CPU之旅.md
2024-10-16 10:18:29 +08:00

218 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
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 = 2b01;-
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 <= 4b0000;
这行代码的意思是给寄存器cnt_r赋以4b0000的值。
这个值怎么来的呢其中的逻辑“0”低电平对应电路接地GND。同样的逻辑“1”则表示高电平对应电路接电源VCC。除此之外还有特殊的“X”和“Z”值。逻辑“X”表示电平未知输入端存在多种输入情况可能是高电平也可能是低电平逻辑“Z”表示高阻态外部没有激励信号是一个悬空状态。
当然为了代码的简洁明了Verilog可以用不同的格式表示同样的数值。比如要表示4位宽的数值“10”二进制写法为4b1010十进制写法为4d10十六进制写法为4ha。这里我需要特殊说明一下数据在实际存储时还是用二进制位宽表示储存时二进制占用宽度。
运算符
接下来我们看看Verilog的运算符对于运算符Verilog和大部分的编程语言的表示方法是一样的。
比如算术运算符 + - * / % ,关系运算符 > < <= >= == !=,逻辑运算符 && || !(与或非),还有条件运算符 也就是C语言中的三目运算符。例如a?b:c表示a为真时输出b反之为c。
但在硬件语言里,位运算符可能和一些高级编程语言不一样。其中包括 ~ & | ^(按位取反、按位与,按位或,以及异或);还有移位运算符,左移 << 和右移>> ,在生成实际电路时,左移会增加位宽,右移位宽保存不变。
条件、分支、循环语句
还有就是条件语句if和分支语句case由于它们的写法和其它高级编程语言几乎一样基本上你掌握了某个语言都能理解。
这里我们重点来对比不同之处也就是用Verilog实现条件、分支语句有什么不同。用if设计的语句所对应电路是有优先级的也就是多级串联的MUX电路。而case语句对应的电路是没有优先级的是一个多输入的MUX电路。设计时只要我们合理使用这两个语句就可以优化电路时序或者节省硬件电路资源。
此外,还有循环语句,一共有 4 种类型,分别是 whileforrepeat和 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语言和高级编程语言具体有哪些不同
模块结构Verilog的模块结构和其他语言的函数定义不一样它既可以有多个输入信号也可以输出多个结果。而且模块上的接口信号必须要指定是输入信号和输出信号。-
数据类型跟我们在高级编程语言见到的变量类型相比Verilog定义的数据类型也有很大不同。reg类型对应的是寄存器电路wire类型对应的是电路上的互连线标识符对应的是一串固定的高低电平信号。-
数据表达Verilog代码中的数据本质上就是高低电平信号。“0”代表低电平“1”代表高电平不能确定高低电平的就用“X”来表示。-
运算符Verilog中的大部分运算符和其他语言是一样的但是要注意位操作运算符它们对应的是每一位电平按指定逻辑跳变还有移位操作一定要注意移位信号的数据位宽。-
条件、分支、循环语句Verilog中的条件if语句是有优先级的而case语句则没有优先级合理利用它们可以优化电路时序或节省硬件电路资源。循环语句则是把相同的电路重复好几遍。-
过程结构这是实现时序电路的关键。我们可以利用alway块语句设定一个时钟沿用来触发相应逻辑电路的执行。这样我们就可以依据时钟周期来分析电路中各个信号的逻辑跳变。而initial语句常在验证代码中使用它可以从仿真的0时刻开始设置相关信号的值并将这些值传输到待验证模块的端口上。-
下节课,我会带你设计一个简单的电路模块,既能帮你复习今天学到的知识,还能通过实践体会一下代码是怎样生成电路的,敬请期待。
思考题
为什么很多特定算法用Verilog设计并且硬件化之后要比用软件实现的运算速度快很多
欢迎你在留言区跟我交流互动,也推荐你把这节课分享给更多朋友。