type
status
date
slug
summary
tags
category
icon
password
一个人的自我学习能力和态度决定着技术成就,不然只会陷入CRUD Boy或者API Caller的圈子里,终日忙忙碌碌却依旧原地踏步。IT人就是要时刻保持学习,如果要给这个保持学习的习惯加个期限,那就是“终身”。

计算机基础学习

notion image

第一关

芯片的两种设计结构CISC & RISC

notion image
图灵机
把计算提炼成最简单、基本、确定的动作,然后提出了一种简单的方法,用来描述机械性的计算程序,让任何程序都能对应上这些动作。
有一条无限长的纸带,纸带上有无限个小格子,小格子中写有相关的信息。纸带上有一个读头,读头能根据纸带小格子里的信息做相关的操作,并且能来回移动。
notion image
把读头及读头的运行规则理解为CPU,把纸带解释为内存,把纸带上信息理解为程序和数据,那这个模型就非常接近现代计算机了。
电子计算机
图灵机这种美好的抽象模型,如果没有好的实施方案,是做不出实际产品的,这将是一个巨大的遗憾。为此,人类进行了多次探索,可惜都没有结果。最后还是要感谢弗莱明和福雷斯特,尽管他们一个是英国人,一个是美国人。 不过,一个三极管虽然做不了计算机,但是许多个三极管组合起来形成的数字电路,就能够实现布尔代数中的逻辑运算,电子计算机的大门自此打开。 1946年,ENIAC成功研制,它诞生于美国宾夕法尼亚大学,是世界上第一台真正意义上的电子计算机。 1947年12月,美国贝尔实验室的肖克利、巴丁和布拉顿组成的研究小组,研制出了晶体管。 晶体三极管跟真空三极管功能一样,不过制造材料是半导体。它的特点在于响应速度快,准确性高,稳定性好,不易损坏。关键它可以做得非常小,一块集成电路即可容纳十几亿到几十亿个晶体管。 这样的器件用来做计算机就是天生的好材料。可以说,晶体管是后来几十年电子计算机飞速发展的基础。
芯片
把一定数量的常用电子元件(如电阻、电容、晶体管等),以及这些元件之间的连线,通过半导体工艺集成在一起的、具有特定功能的电路。 20世纪60年代,人们把硅提纯,切成硅片。想实现具备一定功能的电路,离不开晶体管、电阻、电容等元件及它们之间的连接导线,把这些集成到硅片上,再经过测试、封装,就成了最终的产品——芯片。相关的制造工艺(氧化、光刻、粒子注入等)极其复杂,是人类的制造极限。 芯片中的特例——CPU,它里面包括了控制部件和运算部件,即中央处理器。1971年,Intel将运算器和控制器集成在一个芯片上,称为4004微处理器,这标志着CPU的诞生。到了1978年,开发的8086处理器奠定了X86指令集架构。此后,8086系列处理器被广泛应用于个人计算机以及高性能服务器中。 CPU的工作流程分为以下 5 个阶段:取指令、指令译码、执行指令、访存读取数据和结果写回。 指令和数据统一存储在内存中,数据与指令需要从统一的存储空间中存取,经由共同的总线传输,无法并行读取数据和指令。这就是大名鼎鼎的冯诺依曼体系结构。
CISC
CPU的指令集越丰富、每个指令完成的功能越多,为该CPU编写程序就越容易,因为每一项简单或复杂的任务都有一条对应的指令,不需要软件开发人员写大量的指令。这就是复杂指令集计算机体系结构——CISC。 CISC的优势在于,用少量的指令就能实现非常多的功能,程序自身大小也会下降,减少内存空间的占用。但凡事有利就有弊,这些复杂指令集,包含的指令数量多而且功能复杂。 而想实现这些复杂指令,离不开CPU运算单元和控制单元的电路,硬件工程师要想设计制造这样的电路,难度非常高。 到了20世纪80年代,各种高级编程语言的出现,才大大简化了程序的开发难度。
RISC
20世纪80年代,编译器技术的发展,导致各种高级编程语言盛行。这些高级语言编译器生成的低级代码,比程序员手写的低级代码高效得多,使用的也是常用的几十条指令。 RISC设计方案非常简约,通常有20多条指令的简化指令集。每条指令长度固定,由专用的加载和储存指令用于访问内存,减少了内存寻址方式,大多数运算指令只能访问操作寄存器。 CPU中配有大量的寄存器,这些指令选取的都是工程中使用频率最高的指令。由于指令长度一致,功能单一,操作依赖于寄存器,这些特性使得CPU指令预取、分支预测、指令流水线等部件的效能大大发挥,几乎一个时钟周期能执行多条指令。 RISC的代表产品是ARM和RISC-V。其实到了现在,RISC与CISC早已没有明显界限,开始互相融合了,比如ARM中加入越来越多的指令,x86 CPU通过译码器把一条指令翻译成多条内部微码,相当于精简指令。x86这种外CISC内RISC的选择,正好说明了这一点。

RISC特性与发展

notion image
为什么RISC-V要定义特权级
RISC-V定义特权级是为了实现操作系统和应用程序之间的隔离和保护。特权级别是一种硬件机制,用于限制不同软件实体(如操作系统和应用程序)对系统资源的访问权限。 通过定义特权级,RISC-V可以提供多个特权级别,例如用户态和内核态。在用户态下运行的应用程序只能访问受限资源,并受到操作系统的保护。而在内核态下运行的操作系统则可以访问系统的所有资源和功能。 特权级别的定义还可以实现一些重要的系统功能,如异常处理和中断处理。当系统发生异常或中断时,特权级别的切换可以确保正确地处理这些事件,并提供必要的系统保护。 总之,RISC-V定义特权级别是为了提供安全、可靠和可扩展的计算平台,同时实现操作系统和应用程序之间的隔离和保护。
RISC-V
用RISC-V来命名该指令集架构,有两层意思:RISC-V中的“V”,一方面代表第5代RISC;另一方面,“V”取“ Variation”之意,代表变化。 RISC-V是一套开放许可证书、免费的、由基金会维护的、一个整数运算指令集外加多个扩展指令集的CPU架构规范。
指令集命名方式
以RV为前缀,然后是位宽,最后是代表指令集的字母集合,具体形式如下:
notion image
RV64IMAC,就表示64位的RISC-V,支持整数指令、乘除法指令、原子指令和压缩指令。 RISC-V流行起来,肯定有其优势:一是RISC-V完全开放,二是RISC-V指令简单,三是RISC-V实行模块化设计,易于扩展。
软硬件开源的例子😏
之前硬件和软件一样,都是小心地保护自己的“源代码”,因为那是自己的命脉。
直到后来,软件界出现了开源的Linux,一经开源就迅速席卷了全球。在今天的互联网、云计算、手机等领域Linux已经无处不在。但是硬件依然保护着自己的“源代码”,Intel和AMD还是以售卖x86芯片为主,而ARM直接售卖ARM CPU的“源代码”,连生产芯片的步骤都省了。
这种模式下,无论厂商还是个人,要获得CPU都要付出昂贵的代价。这时RISC-V应运而生,它完全毫无保留地开放了CPU设计标准,任何人都可以使用该标准,自由地设计生产CPU,不需要支付任何费用,也没有任何法律问题。这相当于硬件界的“Linux”,推动了开放硬件的运动和发展。
指令简单😎
为什么说RISC-V很简单?RISC-V提供了一个非常强大且开放的精简指令集架构,只有32个通用寄存器、40多条常用指令、4个特权级。如果需要其它功能,则要进行指令集的扩展,单核心的规范文档才不到300页,一个人在一周之内就能搞清楚。
相比ARM、x86动不动就有8000多页的规范文档,这实在是太简单了。其实,简单也意味着可靠和高效,同时可以让学生或者硬件开发者迅速入手,降低学习和开发成本。
模块化设计🥵
RISC-V虽然简单,但这并不意味着功能的缺失。通过模块化的设计,就能实现对各种功能组件的剪裁和扩展。
事实上,现代IT架构已经发生了巨大的改变。举几个我们身边的例子吧。你正在使用的网卡,上面越来越多的网络处理任务和功能,都从主处理器上移到了网卡中,由网卡芯片自己来处理了。
数据处理器 (DPU) 也体现了这一点。由于通用处理器对大规模数据处理能力的限制,所以我们需要专用的数据处理器。而人工智能领域,现在也已经开始通过GPU运行相关算法。
这些例子都在告诉我们,专用处理器芯片的需求在大量激增,而这正是RISC-V的用武之地。RISC-V的标准开放,指令功能模块可以自由组合,所以用RISC-V就能定制一款满足特殊用途的处理器。芯片工程师会自由组合RISC-V现有的指令功能模块,按需对齐进行修改优化,或者实现一个新的指令功能模块,就像你根据需要修改和使用Linux内核一样。
正是因为RISC-V开放、简单和模块化这三大特点,硬件工程师和软件工程师才能站在巨人的肩膀上开发,自由地调用和组装功能模块,快速去实现特定业务场景下的芯片需求。
指令集模块
指令集是一款CPU架构的主要组成部分,是CPU和上层软件交互的核心,也是CPU主要功能的体现。 RISC-V规范只定义了CPU需要包含基础整形操作指令,如整型的储存、加载、加减、逻辑、移位、分支等。其它的指令称为可选指令或者用户扩展指令,比如乘、除、取模、单精度浮点、双精度浮点、压缩、原子指令等,这些都是扩展指令。扩展指令需要芯片工程师结合功能需求自定义。 所以RISC-V采用的是模块化的指令集,易于扩展、组装。它适应于不同的应用场景,可以降低CPU的实现成本。
notion image
 
RISC-V寄存器
指令的操作数是来源于寄存器,精简指令集架构的CPU,都会提供大量的寄存器,RISC-V当然也不例外。RISC-V 的规范定义了32个通用寄存器以及一个 PC寄存器,这对于RV32I、RV64I、RV128I指令集都是一样的,只是寄存器位宽不一样。 如果实现支持 F/D 扩展指令集的CPU,则需要额外支持32个浮点寄存器。而如果实现只支持RV32E指令集的嵌入式CPU,则可以将32个通用寄存器缩减为16个通用寄存器。
常用的寄存器列表
常用的寄存器列表
RISC-V特权级
不同的特权级能访问的系统资源不同,高特权级能访问低特权级的资源,反之则不行。RISC-V 的规范文档定义了四个特权级。
privilege level
privilege level
一个RISC-V硬件线程(hart),相当于一个CPU内的独立的可执行核心,在任一时刻,只能运行在某一个特权级上。具体分级(MHSU)如下:
  1. 机器特权级(M):RISC-V中hart可以执行的最高权限模式。在M模式下运行的hart,对内存、I/O和一些必要的底层功能(启动和系统配置)有着完全的控制权。因此,它是唯一一个所有标准RISC-V CPU都必须实现的权限级。实际上,普通的RISC-V微控制器仅支持机器特权级。
  1. 虚拟机监视特权级(H):为了支持虚拟机监视器而定义的特权级。
  1. 管理员特权级(S):主要用于支持现代操作系统,如Linux、FreeBSD和Windows。
  1. 用户应用特权级(U):用于运行应用程序,同样也适用于嵌入式系统。
有了特权级的存在,就给指令加上了权力,从而去控制用指令编写的程序。应用程序只能干应用程序该干的事情,不能越权操作。操作系统则拥有更高的权力,能对系统的资源进行管理。
 

硬件语言

万丈高楼从地起,欲盖高楼先打地基,芯片是万世之基。
为什么很多特定算法,用Verilog设计并且硬件化之后,要比用软件实现的运算速度快很多?
  1. 并行性:硬件可以利用并行性进行高效的计算。在硬件中,可以同时执行多个操作,因为硬件中的电路可以并行处理多个操作。这使得硬件实现能够在同一时钟周期内完成更多的计算,从而提高了运算速度。
  1. 定制化:硬件可以根据特定算法的需求进行定制化设计。通过将算法的关键部分硬件化,可以使用专门的电路和逻辑来实现高效的计算。相比之下,软件实现通常是通用的,需要考虑更多的兼容性和灵活性,因此可能会有额外的开销。
  1. 低延迟:硬件实现通常具有更低的延迟。由于硬件电路的特性,信号可以在电路中快速传播,从而减少了处理时间。相比之下,软件实现通常需要进行指令解码、内存访问等操作,这些操作会引入额外的延迟。
  1. 能量效率:硬件实现通常比软件实现更能够实现能量效率。硬件电路可以通过优化电路结构和电源管理来降低功耗,从而提高能量效率。而软件实现则需要在通用处理器上运行,通常需要更多的能量来完成相同的计算任务。
芯片的内部电路
处理器芯片
处理器芯片
在芯片设计时,根据不同模块的功能特点,通常把它们分为数字电路模块和模拟电路模块。 模拟电路还是像早期的半导体电路那样,处理的是连续变化的模拟信号,所以只能用传统的电路设计方法。而数字电路处理的是已经量化的数字信号,往往用来实现特定的逻辑功能,更容易被抽象化,所以就产生了专门用于设计数字电路的硬件描述语言。 现在业界的 IEEE标准主要有VHDL和Verilog HDL 这两种硬件描述语言。 Verilog代码和C语言、Java等这些计算机编程语言有本质的不同,在可综合(这里的“可综合”和代码“编译”的意思差不多)的Verilog代码里,基本所有写出来的东西都对应着实际的电路。 用Verilog的时候,必须理解每条语句实质上对应着什么电路,并且要从电路的角度来思考它为何要这样设计。而高级编程语言通常只要功能实现就行。 声明变量的时候,如果指定是一个reg,那么这个变量就有寄存数值的功能,可以综合出来一个实际的寄存器;如果指定是一段wire,那么它就只能传递数据,只是表示一条线。在Verilog里写一个判断语句,可能就对应了一个MUX(数据选择器),写一个for可能就是把一段电路重复好几遍。 最能体现电路设计思想的就是always块了,它可以指定某一个信号在某个值或某个跳变的时候,执行块里的代码。通过使用Verilog语言,我们就能完成芯片的数字电路设计工作了。没错,芯片前端设计工程师写Verilog代码的目的,就是把一份电路用代码的形式表示出来,然后由计算机把代码转换为所对应的逻辑电路
 
芯片如何设计?
设计树状结构图
设计树状结构图
在开始一个大的芯片设计时,往往需要先从整个芯片系统做好规划,在写具体的Verilog代码之前,把系统划分成几个大的基本的功能模块。之后,每个功能模块再按一定的规则,划分出下一个层次的基本单元。 这和Verilog语言的module模块化设计思想是一致的,上一层模块对下一层子模块进行例化,就像其他编程语言的函数调用一样。根据包含的子功能模块一直例化下去,最终就能形成hierarchy结构。
Verilog都是基于模块进行编写的,一个模块实现一个基本功能,大部分的Verilog逻辑语句都放在模块内部。
从一段代码入门
Verilog的基本模块和逻辑语句
模块结构
看一看这段代码的第一行和最后一行。没错,一个模块的定义是以关键字module开始,以endmodule结束。module关键字后面跟的counter就是这个模块的名称。
Verilog模块的接口必须要指定它是输入信号还是输出信号。
输入信号用关键字input来声明,比如上面第4行代码的 input clk;输出信号用关键字output来声明,比如代码第5行的output [3:0] cnt;还有一种既可以输入,又可以输出的特殊端口信号,这种双向信号,我们用关键字inout来声明。
 
数据类型
在可综合的Verilog代码里,基本所有写出来的东西都会对应实际的某个电路。而Verilog代码中定义的数据类型就能充分体现这一点。
比如上面代码,表示定义了位宽为4bit 的寄存器reg类型信号,信号名称为cnt_r。 寄存器reg类型表示抽象数据存储单元,它对应的就是一种寄存器电路。reg默认初始值为X(不确定值),换句话说就是,reg电路在上电之后,输出高电平还是低电平是不确定的,一般是在系统复位信号有效时,给它赋一个确定值。比如例子中的cnt_r,在复位信号reset_n等于低电平时,就会给cnt_r赋“0”值。 reg类型只能在alwaysinital语句中被赋值,如果描述语句是时序逻辑,即always语句中带有时钟信号,寄存器变量对应为触发器电路。比如上述定义的cnt_r,就是在带clk时钟信号的always块中被赋值,所以它对应的是触发器电路;如果描述语句是组合逻辑,即always语句不带有时钟信号,寄存器变量对应为锁存器电路。 我们常说的电子电路,也叫电子线路,所以电路中的互连线是必不可少的。Verilog代码用线网wire类型表示结构实体(例如各种逻辑门)之间的物理连线。wire类型不能存储数值,它的值是由驱动它的元件所决定的。驱动线网类型变量的有逻辑门、连续赋值语句、assign等。如果没有驱动元件连接到线网上,线网就默认为高阻态“Z”。 为了提高代码的可读性和可维护性,Verilog还定义了一种参数类型,通过parameter来声明一个标识符,用来代表一个常量参数,我们称之为符号常量,即标识符形式的常量。这个常量,实际上就是电路中一串由高低电平排列组成的一个固定数值。
数值表达
再了解一下Verilog中的数值表达。还是以前面的4位十进制计数器代码为例
这行代码的意思是,给寄存器cnt_r赋以4’b0000的值。
这个值怎么来的呢?其中的逻辑“0”低电平,对应电路接地(GND)。同样的,逻辑“1”则表示高电平,对应电路接电源VCC。除此之外,还有特殊的“X”和“Z”值。逻辑“X”表示电平未知,输入端存在多种输入情况,可能是高电平,也可能是低电平;逻辑“Z”表示高阻态,外部没有激励信号,是一个悬空状态。 为了代码的简洁明了,Verilog可以用不同的格式,表示同样的数值。比如要表示4位宽的数值“10”,二进制写法为4’b1010,十进制写法为4’d10,十六进制写法为4’ha。
运算符
对于运算符,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块内的逻辑电路都会执行一次。
前面代码例子的always语句,就是典型的时序电路设计方法
还有一种过程结构就是initial 语句。它从 0 时刻开始执行,且内部逻辑语句只按顺序执行一次,多个 initial 块之间是相互独立的。理论上,initial 语句是不可以综合成实际电路的,多用于初始化、信号检测等,也就是在编写验证代码时使用。
 
Verilog语言
  1. 模块结构:Verilog的模块结构和其他语言的函数定义不一样,它既可以有多个输入信号,也可以输出多个结果。而且,模块上的接口信号,必须要指定是输入信号和输出信号。
  1. 数据类型:跟我们在高级编程语言见到的变量类型相比,Verilog定义的数据类型也有很大不同。reg类型对应的是寄存器电路,wire类型对应的是电路上的互连线,标识符对应的是一串固定的高低电平信号。
  1. 数据表达:Verilog代码中的数据,本质上就是高低电平信号。“0”代表低电平,“1”代表高电平,不能确定高低电平的就用“X”来表示。
  1. 运算符:Verilog中的大部分运算符和其他语言是一样的,但是要注意位操作运算符,它们对应的是每一位电平按指定逻辑跳变,还有移位操作,一定要注意移位信号的数据位宽。
  1. 条件、分支、循环语句:Verilog中的条件if语句是有优先级的,而case语句则没有优先级,合理利用它们可以优化电路时序或节省硬件电路资源。循环语句则是把相同的电路重复好几遍。
  1. 过程结构:这是实现时序电路的关键。我们可以利用alway块语句设定一个时钟沿,用来触发相应逻辑电路的执行。这样,我们就可以依据时钟周期来分析电路中各个信号的逻辑跳变。而initial语句常在验证代码中使用,它可以从仿真的0时刻开始设置相关信号的值,并将这些值传输到待验证模块的端口上。
    1. notion image
Verilog代码编写
利用Verilog,设计一个包含加、减、与、或、非等功能的简单ALU模块
通过上面的代码,我们实现了一个8位二进制的简单运算模块。其中,a和b是输入的两个8位二进制数,cin是a和b做加法运算时输入的进位值,4bit位宽的sel[3:0] 则是CPU中通常所说的指令操作码。 在这个ALU模块中,逻辑功能代码我们把它分成三个部分,分别是运算单元、逻辑处理单元和输出选择单元。运算单元是根据输入指令的低三位sel[2:0],来选择执行加减等运算。同理,逻辑处理单元执行与或非门等操作。最后,根据指令的最高位sel[3],来选择Y输出的是加减运算单元结果,还是逻辑处理的结果。
如何通过仿真验证代码
Iverilog是一个对Verilog进行编译和仿真的工具,而GTKWave是一个查看仿真数据波形的工具。 Iverilog运行于终端模式下,安装完成之后,就能通过Iverilog对verilog执行编译,再对生成的文件通过vvp命令执行仿真,配合GTKWave即可显示和查看图形化的波形。 以Ubuntu为例,安装Iverilog
安装GTKWave