USC-CSCE611-高级数字设计笔记-全-
USC CSCE611 高级数字设计笔记(全)
001:课程介绍与RISC-V ISA 🚀
概述
在本节课中,我们将学习高级数字系统设计课程(CSCE 611)的总体介绍,并回顾RISC-V指令集架构的基础知识。课程将涵盖硬件描述语言(HDL)和现场可编程门阵列(FPGA)的使用,目标是设计并实现一个CPU。
课程大纲与政策 📋
课程基本信息
本课程使用系内的Moodle/Dropbox系统(dropbox.cs.sc.edu)发布材料、提交作业和展示成绩。所有讲座将被录制并上传至YouTube频道。
课程安排与资源
课程在周一进行讲座。实验课根据学生所在的小组(Section 1或Section 2)分别在周三或周五进行,地点在3D22 Linux实验室。实验室有门禁,当前密码为45213。



我们将使用安装在系内Linux实验室的CAD软件。课程所需的特定硬件仅在三楼实验室提供。其他课程也会使用该实验室,具体时间表将发布在Dropbox上。

教材与学习成果
课程推荐教材为《Computer Organization and Design: The Hardware/Software Interface》。在本课程中,你将学习一种称为SystemVerilog的硬件描述语言,并在FPGA开发板上进行数字设计。
评分结构
课程评分基于四个实验项目、每周测验、期中考试和期末考试。总权重为117.5%,这为学生提供了一定的灵活性,例如可以选择不参加期末考试或放弃第四个项目。研究生或荣誉学分的学生需要完成额外的第五个实验。
学术诚信与协作
由于本课程使用的工业级CAD工具复杂且有时不稳定,鼓励学生之间互相帮助解决工具使用问题。然而,严禁抄袭设计代码。小组项目(占总成绩近50%)要求合作完成,但个人在测验和考试中的表现将独立评分。
项目提交与演示
项目通过Dropbox提交。评分主要通过课堂演示进行,学生需要向助教展示代码工作原理。即使项目未完全成功,也可以通过解释设计思路和遇到的问题获得部分分数。迟交罚分为每个上学日5%,最高扣30%。所有项目必须在12月8日(学期最后一天)前提交。


课程核心内容:从逻辑门到现代硬件设计 🔧
上一节我们介绍了课程的基本框架和政策,本节中我们来看看本课程要解决的核心工程问题及其背景。
在之前的数字逻辑课程(如CSCE 211)中,你通过布尔代数和原理图设计过逻辑电路,甚至可能在面包板上用TTL芯片搭建过它们。然而,用这种方法设计现代CPU是不切实际的。一个现代CPU包含数十亿晶体管,若用面包板搭建,将需要数百万块,绵延上千英里,且无法调试、复用或升级。

因此,本课程采用工业界实用的方法:
- 使用现场可编程门阵列(FPGA):FPGA是一种可电子编程的芯片。通过下载一个比特流文件,可以将其配置为任何你设计的数字逻辑电路,而无需实际制造芯片。这允许快速原型设计和测试。
- 使用硬件描述语言(HDL):使用类似高级编程语言的语法来描述硬件行为,然后通过逻辑综合工具将其转换为底层的数字逻辑网表。这比直接绘制原理图的抽象层级更高,更易于设计复杂系统。

FPGA vs. ASIC:FPGA的时钟速度通常比专用集成电路(ASIC)慢约10倍,且能容纳的逻辑门数量也少约10倍。但FPGA的优势在于无需制造,可快速迭代。许多公司在流片前会用FPGA验证设计。




HDL的并发语义:与Java、Python等顺序执行的语言不同,HDL具有并发语义。HDL中的每一行代码都代表一个同时工作的电路模块,代码的书写顺序不影响其功能。
为何要学习硬件设计?
有人质疑,既然所有计算问题都能通过软件在通用CPU上解决,且过去CPU性能遵循摩尔定律快速提升,为何还要学习定制硬件设计?原因如下:
- 性能与能效墙:通用CPU的性能提升已显著放缓(“后摩尔定律时代”)。同时,它们能效不高。专用硬件(如Google的TPU)通过为特定领域(如矩阵乘法)优化,可以获得比通用CPU高数个数量级的性能和能效。
- 现代芯片的构成:观察现代处理器(如苹果A系列、英特尔酷睿)的芯片显微照片可以发现,通用CPU核心所占面积比例越来越小(从早年的70%降至20%),大部分区域被GPU、AI加速器、视频编解码器等专用硬件占据。新功能(如人脸识别、计算摄影)主要依靠这些定制电路实现,而非软件。
- 职业价值:硬件设计是一项高度专业化的技能,相关岗位的薪酬通常显著高于软件岗位。
因此,本课程的目标是让你获得设计定制计算硬件的实践经验。
RISC-V指令集架构(ISA)回顾 💻
上一节我们探讨了定制硬件设计的重要性,本节中我们将回顾实现CPU所需理解的软件接口——指令集架构,特别是RISC-V。
ISA与微架构
- 指令集架构(ISA):是CPU呈现给软件(汇编语言)的视图。它定义了指令格式、寄存器、内存访问方式等。ISA通常是公开的(如RISC-V、x86、ARM)。
- 微架构:是ISA的具体硬件实现电路。它决定了指令如何被一步步执行(时钟周期级)。微架构通常是公司的核心机密。
本课程中,你将设计一个能执行RISC-V指令子集的微架构。
为何学习汇编语言?
虽然在实际工作中很少直接编写长篇汇编程序,但理解汇编对于硬件设计至关重要。因为CPU本质上就是一个执行汇编指令的电路。编译器将高级语言(如C)编译为汇编指令,CPU硬件则执行这些指令。
RISC-V基础
RISC-V是一种精简指令集计算机(RISC) 架构,与MIPS类似。RISC架构指令简单,每条指令完成的工作量小,导致程序指令条数多,但硬件实现可以更高效、快速。
核心组件:
- 32个通用寄存器:命名为
x0-x31。x0是硬连线到0的寄存器,常用于提供常数0或构造其他操作。 - 指令类型:我们将重点关注以下类型:
- 算术/逻辑/移位指令:例如
add x1, x2, x3(x1 = x2 + x3)。操作数都在寄存器中。 - 立即数指令:例如
addi x1, x2, 4(x1 = x2 + 4)。包含一个编码在指令中的常数。 - 分支指令:例如
beq x1, x2, label。比较两个寄存器,若相等则跳转到label处。用于实现if和循环。 - 跳转指令:例如
jal label。无条件跳转,用于函数调用。 - 加载/存储指令:例如
lw x1, offset(x2)和sw x1, offset(x2)。在寄存器和内存之间传输数据。RISC-V是加载-存储型架构,运算指令只能操作寄存器。 - 系统/控制寄存器指令:例如
csrrw。我们将用它来实现FPGA板上的输入/输出(如读取开关、驱动数码管)。
- 算术/逻辑/移位指令:例如
本课程的简化:为了让项目更易管理,我们设计的CPU将不实现加载/存储指令和内存子系统。所有计算仅通过寄存器完成,并通过特殊的IO指令与FPGA板交互。你的第一个实验就是编写一个在模拟器中运行的RISC-V程序(例如计算平方根),该程序后续将用于测试你设计的CPU硬件。
汇编编程的挑战
以下是编写汇编程序时的常见难点:
- 显式的加载与存储:所有数据必须先用
load指令从内存(或通过IO)读到寄存器,运算完成后再用store指令存回。无法直接对内存地址进行运算。 - 寄存器分配与管理:程序员必须手动跟踪哪个变量在哪个寄存器中,并在寄存器不够用时重复利用它们,这很容易出错。
- 条件逻辑的反转:在高级语言中,
if (a < b) { ... } else { ... }的逻辑是直接的。在汇编中,为了效率,通常测试相反条件并跳过else块。例如,汇编中可能实现为if (a >= b) jump to ELSE_LABEL; ... (if block) ... ELSE_LABEL: ... (else block)。这需要程序员进行逻辑转换。

总结 🎯
本节课我们一起学习了CSCE 611课程的整体介绍。我们了解了课程政策、评分方式以及项目提交的流程。更重要的是,我们探讨了从低层次逻辑门设计转向使用HDL和FPGA进行现代数字系统设计的必要性,并回顾了作为CPU设计基础的RISC-V指令集架构的核心概念。在接下来的课程中,我们将开始深入学习SystemVerilog硬件描述语言,并着手进行第一个实验项目。
002:RISC-V ISA与Lab1入门 🚀
在本节课中,我们将学习RISC-V汇编语言的基础知识,并开始为第一个实验做准备。我们将了解如何将高级语言结构(如条件语句和循环)转换为RISC-V汇编,熟悉RISC-V模拟器RARS的使用,并初步探讨如何在实验中实现定点数运算和平方根算法。
条件语句的实现 🔄
上一节我们介绍了RISC-V与MIPS的相似性。本节中我们来看看如何在汇编中实现if条件语句。
在汇编中实现if语句的一种常见方法是:取原条件语句的条件,将其取反。这样,如果取反后的条件为假,则程序会顺序执行(即不跳转),进入if语句的“真”部分代码块。
以下是实现if (a < b)的汇编逻辑:
# 假设 a 在寄存器 s1 中,b 在寄存器 s2 中
bge s1, s2, else_label # 如果 s1 >= s2 (即 a >= b),则跳转到 else 部分
# if 条件为真时执行的代码块
j exit_label # 执行完真部分后,跳过 else 部分
else_label:
# else 部分执行的代码块
exit_label:
这种结构的优势在于,可以将“真”代码块放在顶部,“假”代码块放在底部,并且只需要为else部分定义一个标签。
数组遍历与索引 🧮
在RISC-V和MIPS中,没有专门的索引寄存器字段。这意味着遍历数组时,需要手动计算每个元素的地址。
不能像某些指令集那样,直接使用“基地址 + 寄存器索引”的方式加载数据。例如,lw t0, array(t1)这样的指令在RISC-V中是非法的。


以下是遍历数组的正确方法:
la t1, array # 伪指令,将数组基地址加载到 t1
li t0, 0 # 循环计数器 i = 0
loop:
slli t2, t0, 2 # t2 = i * 4 (每个字4字节)
add t3, t1, t2 # t3 = 数组基地址 + 偏移量 (有效地址)
lw t4, 0(t3) # 从计算出的地址加载数据
# ... 处理数据 ...
addi t0, t0, 1 # i++
# ... 循环条件判断 ...
你需要维护一个基地址寄存器,并通过加法计算每个元素的地址。当需要回到数组开头时,必须重新加载基地址。
循环与地址计算 🔁
在C语言中,编译器知道数据类型的大小,因此i++在地址计算时会自动考虑元素大小(例如,int类型会加4)。在汇编中,你必须显式处理这一点。
有两种常见策略:
- 让循环计数器
i直接按元素大小(如4)递增,并相应调整循环上限。 - 独立维护循环计数器(每次加1)和地址偏移量(通过移位计算)。



以下是第二种策略的示例,它更清晰地分离了逻辑:
li t0, 0 # t0 = i (计数器,每次加1)
la t1, array # t1 = 数组基地址
loop:
slli t2, t0, 2 # t2 = i * 4
add t3, t1, t2 # t3 = &array[i] (源地址)
add t4, t1, t2 # t4 = &array[i] (目标地址,示例中相同)
# ... 使用 t3 和 t4 进行加载/存储 ...
addi t0, t0, 1 # i++
# ... 循环条件判断 ...
这比C代码需要更多的显式地址计算。

寄存器分配与使用 📝
在RISC-V汇编中,你必须手动进行寄存器分配。你需要决定每个变量在程序的每个阶段存放在哪个寄存器中。
一个变量通常在从内存加载时“存活”,在存储回内存时“死亡”。在这期间,对应的寄存器被占用。
跟踪寄存器用途最简单的方法是通过注释。但要注意,注释只代表程序在特定点的映射关系,因为寄存器会在不同代码段中重用。
RISC-V汇编中的注释以井号#开始。
与常量比较的限制 ⚠️
RISC-V的branch指令只能比较两个寄存器的值,不能直接与立即数(常量)比较。
例如,你不能写blt x1, 10, label。你必须先将常量10加载到一个寄存器中,然后再进行比较。
li t0, 10 # 将常量10加载到临时寄存器 t0
blt x1, t0, label # 现在可以比较了
虽然寄存器x0始终为0,可用于与0比较,但对于其他常量,就需要额外的指令来加载。
本课程实现的指令子集 🎯
在本课程的实验中,我们将实现RISC-V指令集的一个子集:
- 实验三:实现算术、移位、比较、立即数加载和
CSRRW指令。 - 实验四:增加分支和跳转指令。
尽管我们只实现子集,但你需要熟悉完整的RISC-V指令集,以便编写和测试程序。
RARS模拟器演示与使用 💻
现在,我们通过一个简单的冒泡排序示例来演示RARS模拟器的使用。这将帮助你熟悉编辑、汇编、运行和调试RISC-V程序的过程。
首先,我们定义数据段和数组:
.data
vowels: .word 5, 4, 3, 2, 1
n: .eqv 5
然后,在文本段编写排序代码。代码结构包括外层循环、内层循环、元素比较与交换。
在RARS中,你需要先保存文件(例如risc1.asm),然后点击“Assemble”进行汇编。汇编成功后,可以单步执行或运行程序,并在寄存器窗口和数据窗口观察值的变化。
使用系统调用可以方便地进行输入输出。例如:
# 打印整数
li a7, 1 # 系统调用号1:打印整数
mv a0, t2 # 将要打印的值(例如t2中的i)移动到a0
ecall
# 读取整数
li a7, 5 # 系统调用号5:读取整数
ecall # 读取的值会返回到a0寄存器
本课程使用的RARS版本经过了修改,包含了用于DE2-115开发板IO操作的特殊寄存器,这对于后续实验非常重要。
定点数表示法 🔢
我们的CPU没有浮点运算单元,为了进行小数运算,我们将使用定点数表示法。
定点数的核心思想是:约定小数点的位置。例如,在一个32位数中,我们可以约定高18位为整数部分,低14位为小数部分。
- 整数部分:决定数值的范围。
- 小数部分:决定数值的精度(这里是2^{-14})。
在定点数运算中:
- 加减法:需要确保两个操作数的小数点位置对齐。
- 乘法:结果的小数位数是两个操作数小数位数之和。因此,通常需要对结果进行移位来重新调整小数点的位置。
在本次实验中,我们将使用 32位,其中14位小数 的格式。
定点数乘法实现 ⚙️
当两个定点数(各14位小数)相乘时,会得到一个有28位小数的64位乘积。
RISC-V没有像MIPS那样的HI/LO寄存器。为了得到64位乘积,需要使用两条指令:
mul:获得乘积的低32位。mulhu:获得乘积的高32位(无符号乘法)。
为了将64位乘积转换回我们的32位14小数的格式,需要以下操作:
- 将低32位乘积逻辑右移14位,提取出其整数部分(4位)和部分小数。
- 将高32位乘积逻辑左移18位(32-14),提取出其整数部分。
- 将上述两部分结果进行或操作,合并成最终的32位14小数格式的数。
这个过程大约需要5条指令。
平方根算法:二分查找 🔍
由于我们的CPU没有除法指令,无法使用牛顿迭代法。我们将使用二分查找算法来求平方根。
算法思路如下:
- 设置初始猜测值
guess = 0。 - 设置初始步长
step为可能解范围的一半(例如,对于最大输入约262,000,其平方根最大约512,因此初始step = 256)。 - 循环:
- 计算
guess的平方(使用上述定点数乘法)。 - 将平方值与输入值
S比较。 - 如果
guess^2 == S,找到精确解,退出。 - 如果
guess^2 < S,猜测值过低,执行guess = guess + step。 - 如果
guess^2 > S,猜测值过高,执行guess = guess - step。 - 将步长减半:
step = step / 2(通过右移1位实现)。 - 当
step减小为0时,达到机器精度(2^{-14}),退出循环。
- 计算
这个算法将收敛到真实平方根的近似值,精度由小数位数决定。
Lab1 实验细节与测试 🧪
Lab1的目标是编写一个RISC-V程序,使用二分查找算法计算输入值的平方根。
在RARS中模拟的注意事项:
- 为了模拟FPGA上的定点数输入,你需要将“实际输入值”乘以 2^{14} 后,作为“原始输入”通过系统调用读入程序。
- 由于RARS的系统调用读取的是有符号32位整数,当你想测试大的正数(其高17位为1)时,直接输入可能会被解释为负数或溢出。此时,你需要按照实验手册中的公式,将大的正数转换为一个负数输入给RARS。
- 程序的输出也是缩放后的值,你需要手动将其除以 2^{14} 来得到实际结果。
实验手册提供了测试向量,例如:
- 输入
16384(即 1 * 2^{14}),程序应返回16384(即 1 * 2^{14})。 - 输入
0,应返回0。 - 输入
-16384(对应一个很大的正数),程序应返回一个特定的值。
你可以利用这些测试用例来验证程序的正确性。
总结 📚
本节课中我们一起学习了:
- RISC-V条件语句和循环的汇编实现方法。
- 数组遍历时手动计算地址的必要性。
- 寄存器分配的策略。
- 使用RARS模拟器编写、运行和调试汇编程序。
- 定点数表示法的原理,以及如何用整数运算来模拟小数。
- 定点数乘法的具体实现步骤(
mul/mulhu与移位组合)。 - 使用二分查找算法在无除法指令的CPU上计算平方根。
- Lab1的具体要求、在RARS中的测试方法以及处理大数输入的技巧。

掌握这些内容是完成第一个实验的基础。接下来,你可以开始动手编写和测试你的平方根计算程序了。
003:逻辑电路与SystemVerilog
概述
在本节课中,我们将学习数字逻辑电路的基本概念,以及如何使用SystemVerilog硬件描述语言来描述和设计这些电路。我们将从布尔代数和逻辑门开始,逐步深入到模块化设计、层次结构和硬件描述语言的核心思想。
逻辑电路与硬件描述语言
上一节我们回顾了布尔代数与逻辑电路的关系。本节中,我们来看看硬件描述语言(HDL)如何作为现代数字设计的核心工具。
在数字逻辑设计中,我们通常使用三种表示方式:
- 行为描述:使用布尔代数或高级语言(如SystemVerilog)描述电路的功能。
- 结构描述:使用逻辑门和连线构成的原理图来表示电路。
- 物理实现:将结构描述映射到实际的物理器件上,如芯片或FPGA。
在本课程中,我们将使用SystemVerilog作为行为描述语言。你编写的SystemVerilog代码会被一个称为逻辑综合器的工具处理,该工具类似于编译器,它会自动将你的高级描述转换为优化后的布尔逻辑(结构描述),并最终映射到FPGA的物理资源上。
HDL设计的核心概念:抽象、层次与规整
硬件描述语言围绕三个核心概念构建:抽象、层次和规整。
- 抽象:通过将复杂设计分解为模块来隐藏内部细节。每个模块只暴露必要的接口,使得顶层设计保持简洁。
- 层次:设计被组织成树状结构。顶层模块实例化子模块,子模块可能进一步实例化更底层的模块,直到最底层的逻辑门和触发器。
- 规整:通过最大化模块的复用,减少设计中独特模块的数量。例如,一个CPU中的多个核心可以是同一个模块的多个实例。
以下是一个从CPU到逻辑门的层次结构示例:
- 顶层:FPGA设计(包含CPU)。
- CPU:可能包含多个处理器核心。
- 处理器核心:包含浮点运算单元、寄存器堆等。
- 浮点加法器:包含比较器、移位器和加法器。
- 整数减法器:内部包含一个加法器。
- 加法器:由多个全加器构成。
- 全加器:由异或门等基本逻辑门构成。
这种层次结构纯粹是为了方便人类理解和设计。在综合并部署到FPGA后,所有层次都会被“展平”,变成由大量逻辑门和连线构成的网状结构。
逻辑电路基础回顾
在深入SystemVerilog之前,让我们快速回顾一些逻辑电路的基础知识。
逻辑电路分为两大类:
- 组合逻辑电路:输出仅取决于当前输入值的电路,不包含记忆元件(如触发器)。例如:与门、或门、加法器。当输入变化时,输出会随之更新。
- 时序逻辑电路:输出取决于当前输入和电路历史状态(即包含记忆)的电路。例如:触发器、寄存器、计数器。
在阅读原理图(结构描述)时,需要注意连线规则:
- 两条线呈 T型连接 时,表示它们电气相连。
- 两条线 交叉 时,默认不相连。如果相连,会在交叉点画一个 实心圆点。
在SystemVerilog中,每个命名的变量(信号)都代表一根连线。关键规则是:一根连线只能有一个驱动源(即一个电路负责设置它的值),但可以被多个电路读取(扇出)。如果多个驱动源同时驱动一根线,会导致信号冲突(X),在物理上可能造成短路并损坏芯片。
SystemVerilog使用四态逻辑来模拟这些情况:
0:逻辑低电平1:逻辑高电平X:未知或冲突值Z:高阻态,表示没有驱动源
SystemVerilog简介与历史
目前广泛使用的硬件描述语言主要有两种:VHDL和Verilog/SystemVerilog。
- VHDL:起源于20世纪80年代初,由美国国防部主导开发,基于Pascal语言,语法冗长但可读性强,在学术界应用较多。
- Verilog/SystemVerilog:起源于20世纪80年代中期的工业界,语法更接近C语言,简洁但稍难阅读,是工业界的首选。SystemVerilog是其功能扩展版本。
这两种语言非常相似,学会一种后很容易掌握另一种。本课程将使用SystemVerilog。
SystemVerilog模块入门
SystemVerilog的设计围绕模块展开,模块类似于Java中的类,是实现抽象、层次和规整的基本单元。
一个简单的模块示例如下:
module example (
input logic A, B, C,
output logic Y
);
assign Y = (A & B & C) | (A & ~B & ~C) | (~A & B & ~C) | (~A & ~B & C);
endmodule
代码解析:
module example定义了一个名为example的模块。input logic A, B, C声明了三个输入端口,类型为logic。output logic Y声明了一个输出端口。assign Y = ...是一个连续赋值语句,它描述了一个组合逻辑电路,将右侧的布尔表达式结果持续驱动到信号Y上。这创建了一个组合逻辑电路。
模块通常保存在与模块同名的文件中(例如 example.sv)。
测试与仿真
我们无法像运行软件程序一样直接运行硬件描述代码,必须使用仿真器。仿真器会模拟电路的行为,并生成时序图供我们分析。

测试一个模块的基本方法是:
- 编写一个测试平台,为模块的输入提供一系列激励信号(测试向量)。
- 在仿真器中运行,观察输出信号是否与预期值匹配。
- 对于小型电路,可以进行穷举测试(遍历所有输入组合)。对于大型电路,则需要设计有针对性的测试用例。



在仿真器中,你可以看到信号随时间变化的波形图,通过设置输入信号(如使用时钟信号模拟变化),可以验证输出逻辑是否正确。

层次化设计:实例化模块

真正的设计通常包含多个模块层次。要在上层模块中使用下层模块,需要进行实例化。
假设我们有一个已有的 and3(三输入与门)模块:
module and3 (
input logic A, B, C,
output logic Y
);
assign Y = A & B & C;
endmodule
现在,我们在一个顶层模块 my_design 中实例化它:
module my_design (
input logic A, B, C,
output logic Y
);
logic n1; // 内部连线声明
// 实例化 and3 模块
and3 and_gate_inst (
.A(A), // 将顶层端口 A 连接到实例的端口 A
.B(B), // 将顶层端口 B 连接到实例的端口 B
.C(C), // 将顶层端口 C 连接到实例的端口 C
.Y(n1) // 将实例的输出端口 Y 连接到内部连线 n1
);
// 可以继续使用 n1 驱动其他逻辑...
assign Y = ~n1; // 例如,反相后输出
endmodule
实例化要点:
and3是模块名(要实例化的模板)。and_gate_inst是实例名(这个特定副本的标识符,用于仿真和调试)。- 使用
.端口名(连接线)的语法进行连接。连接顺序无关。 - 通过内部连线(如
n1)可以将一个模块的输出传递给另一个模块的输入。
多比特信号与向量
在实际设计中,我们经常需要处理多比特数据(如32位加法器)。在SystemVerilog中,可以使用向量来表示。
声明一个4位向量的语法如下:
module vector_example (
input logic [3:0] A, // 4位输入,位宽为3:0(MSB:LSB)
input logic [3:0] B,
output logic [3:0] Sum
);
assign Sum = A + B; // 4位加法
endmodule
注意:
- 位宽声明
[3:0]放在数据类型logic之后、信号名之前。 [3:0]表示最高位(MSB)索引为3,最低位(LSB)索引为0。虽然[0:3]在语法上也有效,但约定俗成使用降序。A和B现在各自代表4根并行的导线。
总结

本节课我们一起学习了数字逻辑设计与SystemVerilog的基础。我们回顾了逻辑电路的组合与时序特性,理解了硬件描述语言中抽象、层次和规整的重要性。我们介绍了SystemVerilog模块的基本结构,包括端口声明、连续赋值语句 assign,以及如何通过实例化来构建层次化设计。我们还学习了如何使用向量处理多比特信号,并了解了通过仿真器验证设计的基本流程。掌握这些概念是后续设计复杂CPU的基石。
004:SystemVerilog 语法深入与工具使用 🧠

在本节课中,我们将深入学习 SystemVerilog 的更多语法细节,包括多比特信号、常用运算符、常量表示以及 always 语句。同时,我们也将介绍用于仿真和综合的关键工具:ModelSim 和 Quartus。掌握这些内容是完成后续实验的基础。
上一节我们介绍了 SystemVerilog 的基本模块结构和数据类型。本节中,我们来看看如何处理多比特信号以及 SystemVerilog 中丰富的运算符。
多比特信号与向量操作
在硬件设计中,我们经常需要处理多位宽的数据,例如32位的寄存器或8位的数据总线。SystemVerilog 使用一种独特的语法来声明向量。
代码示例:声明4位输入和输出
module example (
input logic [3:0] a, // 4位输入a
input logic [3:0] b, // 4位输入b
output logic [3:0] y // 4位输出y
);
需要注意的是,向量索引范围写作 [n-1:0](例如 [3:0]),这表示最高有效位(MSB)在左侧。虽然也可以写作 [0:3],但前者是更常见的惯例。
常用运算符与常量表示
SystemVerilog 的运算符大多与 C/Java 类似,但专为硬件设计增加了一些特性。


以下是需要特别注意的运算符:
- 条件运算符 (
? :):它直接对应一个多路选择器(MUX),是描述数据通路的关键。assign y = (sel == 1‘b1) ? in1 : in0; // 一个2选1 MUX - 移位运算符:
<<和>>是逻辑移位;<<<和>>>是算术移位(考虑符号位)。 - 相等运算符:
==和!=用于可综合代码的比较;===和!==用于仿真测试台,可以比较X和Z状态。 - 缩减运算符:对向量的所有位进行运算,产生一位结果。例如,
&a会对向量a的所有位执行与操作。

对于常量,建议始终指定位宽和基数,格式为:<位宽>'<基数><数值>。下划线 _ 可用于提高可读性。
公式/代码示例:常量与位拼接
logic [7:0] data;
assign data = 8‘b1100_0011; // 8位二进制常量


logic [15:0] extended;
assign extended = { {16{imm[11]}}, imm[11:0] }; // 12位立即数符号扩展至28位
// {16{imm[11]}} 表示将 imm[11] 这位重复16次
// 整体表示:高16位为符号位,低12位为原始立即数

理解了基本运算符后,我们需要一种方法来描述更复杂的行为逻辑,而不仅仅是简单的连线或表达式。这就是 always 语句的用武之地。

行为建模:always 语句

assign 语句适合描述组合逻辑,但它是单行的。对于复杂的组合逻辑或需要记忆功能的时序逻辑,我们使用 always 语句块。
SystemVerilog 引入了三种主要的 always 变体,以帮助避免常见错误:
always_comb:用于描述组合逻辑。编译器会检查是否在所有条件下都对输出进行了赋值,防止意外推断出锁存器。always_ff:用于描述时序逻辑(触发器)。明确指出设计意图是创建边沿触发的寄存器。always:通用版本,但使用它需要更小心,以免推断出非预期的硬件。
使用 always_comb 描述组合逻辑
在 always_comb 块内,可以使用 if-else 和 case 等过程语句,它们会被综合成对应的组合电路。
代码示例:使用 always_comb 实现多路选择器
logic out;
always_comb begin
if (sel == 1‘b1) begin
out = in1;
end else begin
out = in0;
end
end
此代码与 assign out = (sel) ? in1 : in0; 综合出的硬件是相同的。

代码示例:使用 case 语句实现译码器(如七段数码管)
logic [6:0] segments;
always_comb begin
case (data)
4‘d0: segments = 7‘b0111111;
4‘d1: segments = 7‘b0000110;
// ... 其他 cases
default: segments = 7‘b0000000;
endcase
end


使用 always_ff 描述时序逻辑



时序逻辑依赖于时钟信号来更新状态。这是构建寄存器、计数器和状态机的核心。
代码示例:一个简单的 D 触发器
logic q;
always_ff @(posedge clk) begin
q <= d; // 在时钟上升沿,将输入d的值捕获到输出q
end
注意,在描述时序逻辑时,通常使用非阻塞赋值符 <=,这更符合寄存器并行更新的硬件行为。而在 always_comb 中,则使用阻塞赋值符 =。
掌握了描述硬件行为的语言后,我们需要工具来验证其功能是否正确,并将其转化为实际的电路。接下来我们将介绍两个核心工具。
仿真工具:ModelSim/QuestaSim
ModelSim(现称为 Questa Sim)是业界标准的 HDL 仿真器。我们用它来验证代码的行为是否符合预期,主要进行功能验证,而非性能(时序)验证。
仿真流程概述:
- 编译:使用
vlog命令编译你的.sv文件。例如:vlog *.sv。编译后的设计会存入work库。 - 启动仿真:使用
vsim命令加载顶层模块。例如:vsim top_module。 - 调试与查看:
- 结构窗口:以树状图显示设计的层次结构。
- 对象窗口:显示当前层次的所有信号(变量)。
- 波形窗口:将信号拖入此处,运行仿真后可以查看信号随时间变化的波形图。
- 运行控制:可以使用
run <时间>命令(如run 100ns)控制仿真时间,或在测试台中用代码自动产生激励。
重要提示:为了在波形中看到所有内部信号,需要在仿真开始时(Simulate -> Start Simulation...)在“Optimization Options”中启用“Full debug mode”或取消优化选项。
综合与实现工具:Quartus
Quartus 是 Intel(Altera)的 FPGA 开发软件。它负责将 SystemVerilog 代码:
- 综合:将行为描述转换为由基本逻辑单元(如LUT、寄存器)组成的网表。
- 布局布线:将网表中的逻辑单元映射到 FPGA 芯片的物理位置,并用芯片内部的连线资源连接它们。
- 时序分析:确保设计能在指定的时钟频率(本课程为 50 MHz)下稳定工作。
- 生成编程文件:产生可以下载到 FPGA 板上的配置文件。
在本课程中,我们提供了自动化脚本(csce611.sh)来简化 Quartus 的编译和编程流程,你通常不需要直接操作 Quartus GUI。但了解其作用至关重要。
总结与实验指引 🚀
本节课中我们一起学习了:
- SystemVerilog 中多比特向量的声明和操作,包括位拼接和复制运算符。
- 关键的运算符,如条件运算符(对应MUX)、移位运算符和缩减运算符。
always_comb和always_ff语句块,分别用于描述组合逻辑和时序逻辑,并了解了使用它们的最佳实践。- 硬件设计流程中的两个核心工具:用于功能仿真的 ModelSim 和用于综合实现到 FPGA 的 Quartus。
关于实验二:你将开始使用 SystemVerilog 进行设计。实验材料包中提供了项目框架、引脚约束文件和自动化脚本。你的任务之一是完成一个十六进制到七段数码管的译码器。请参考讲义中的示例代码,并注意开发板上数码管是共阳极(低电平点亮)且段序可能不同,你需要相应调整代码。

请记住,仿真是确保逻辑正确的关键步骤,务必在将设计下载到板卡之前进行充分的仿真测试。
005:第5讲 - 测试平台与FPGA部署 🚀
在本节课中,我们将回顾上周的测验,并继续学习SystemVerilog,重点讲解always语句、测试平台的编写以及如何将设计部署到FPGA上。课程最后会有一个关于Lab 2的演示。
测验回顾 📝
上一节我们介绍了SystemVerilog和RISC-V汇编的基础知识。本节中,我们来看看上周测验中的一些关键题目。
循环控制指令
考虑以下C代码及其对应的RISC-V汇编翻译。问题在于缺少了控制循环的指令。
# 初始化循环变量 i 为 8(因为数组索引以4字节为单位)
# x10 存储循环上限 40(对应10次迭代,10*4)
loop_start:
# 缺少的指令:检查是否应退出循环
# 应比较 x2 (i) 和 x10 (上限)
bge x2, x10, exit # 如果 i >= 40,则跳转到 exit
# ... 循环体代码 ...
addi x2, x2, 4 # i = i + 4
j loop_start
exit:
缺少的指令是比较x2(循环变量i)和x10(循环上限40)的bge(分支大于或等于)指令,用于判断是否应退出循环。
位操作:交换半字
以下代码执行什么操作?
assign result = (input_val << 16) | (input_val >> 16);
这段代码将输入值input_val的高16位和低16位进行交换。左移操作将低16位移至高16位,右移操作将高16位移至低16位,然后通过OR操作合并结果。
信号冲突问题
以下SystemVerilog模块存在什么问题?
module problematic (input a, b, output z);
assign z = a & b;
assign z = a | b; // 错误:对信号 z 进行了多次驱动
endmodule
主要问题有两个:
- 信号
z被两个assign语句驱动,导致冲突。 - 如果存在类似
assign z = z & a;的语句,会形成组合逻辑反馈环,可能导致仿真器陷入无限循环或产生未知状态x。
模块实例与信号作用域
考虑以下模块实例化,my_thing模块内部的a和y值是多少?
module top;
logic y = 1'b1;
my_thing u1 (.a(y), .y(y)); // 将 top.y 连接到 my_thing.a,将 my_thing.y 连接到 top.y
endmodule
module my_thing (input a, output y);
assign y = ~a; // y 是 a 的反相
endmodule
在my_thing模块内部:
- 输入端口
a的值为1(来自top.y)。 - 输出端口
y被赋值为~a,因此其值为0。
需要注意的是,top模块中的y和my_thing模块中的y是两个不同的信号,虽然名称相同。top.y与my_thing.a是同一个网络。
运算符优先级与表达式求值
以下代码中,信号a的值是多少?
logic [2:0] a;
assign a = (2'b10 << 1) == 4 ? 1 : 0;
运算顺序如下:
2'b10 << 1结果为3'b100(十进制4)。3'b100 == 4判断为真(1‘b1)。- 三元运算符返回真值子句
1。
因此,a的最终值为1。
结构性与行为性代码冲突
以下代码中,信号a的值是多少?
module top;
logic a;
assign a = 1'b1; // 行为性驱动,将 a 设为 1
my_thing u1 (.a(1'b0), .y(a)); // 结构性驱动,my_thing.y 输出 0 到 a
endmodule
module my_thing (input a, output y);
assign y = ~a; // 简单的反相器
endmodule
信号a同时被行为性代码(assign a = 1‘b1;)和结构性代码(连接到my_thing的输出端口y,其值为0)驱动。这是一个驱动冲突,在仿真中a的值将是未知态x。
SystemVerilog 核心概念深入 ⚙️
上一节我们回顾了测验题目,本节中我们来看看SystemVerilog中两个强大的概念:always语句和测试平台。
always 语句
assign语句适用于简单的组合逻辑赋值。但当需要更复杂的逻辑,如多输出赋值、条件分支(if/case)、循环或需要创建存储器(触发器、RAM)时,就需要使用always语句。
always语句可以看作一个更强大的、过程式的assign语句块。
组合逻辑 always 块
用于描述组合逻辑(输出仅取决于当前输入)。
// 使用旧式 Verilog 敏感列表
always @* begin // @* 表示对块内所有输入信号敏感
y = a & b;
z = c | d;
end
// 使用 SystemVerilog 关键字,更安全,能避免意外锁存器
always_comb begin
y = a & b;
z = c | d;
end
关键点:在组合逻辑always块中,使用阻塞赋值 (=)。必须为所有可能的输入条件指定输出值,否则会推断出锁存器,这在FPGA设计中通常是要避免的。
// 错误示例:会生成锁存器
always_comb begin
if (sel) begin
out = in1;
end
// 当 sel 为 0 时,out 未赋值,保持原值 -> 锁存器!
end
// 正确示例:使用默认赋值
always_comb begin
out = in2; // 默认值
if (sel) begin
out = in1;
end
end
时序逻辑 always 块
用于描述时序逻辑(输出取决于当前输入和过去状态),如触发器。
// 带同步复位的 D 触发器
always_ff @(posedge clk) begin
if (reset) begin
q <= 1'b0;
end else begin
q <= d;
end
end
// 使用 SystemVerilog 关键字
always_ff @(posedge clk) begin
if (reset) q <= 1'b0;
else q <= d;
end
关键点:在时序逻辑always块中,使用非阻塞赋值 (<=)。这确保了在时钟沿触发的同一时刻,所有寄存器更新都基于“旧”的值,这对于描述正确的寄存器行为至关重要。
// 非阻塞赋值示例:创建一个2级移位寄存器
always_ff @(posedge clk) begin
n1 <= d; // n1 获取上一时钟周期的 d
q <= n1; // q 获取上一时钟周期的 n1 (即前两个周期的 d)
end
// 结果:q 滞后 d 两个时钟周期。
// 如果错误地使用阻塞赋值 (=):
always_ff @(posedge clk) begin
n1 = d; // n1 立即更新为当前 d
q = n1; // q 立即更新为当前 n1 (即当前 d)
end
// 结果:q 和 n1 在同一周期都等于 d,只实现了一个触发器。
简单规则:在always_comb块中用=,在always_ff块中用<=。
测试平台 (Testbench)


手动在仿真器中设置输入和检查输出非常繁琐。测试平台是一个封装被测设计的SystemVerilog模块,用于自动施加测试激励并检查响应。
测试平台的特点:
- 通常没有输入输出端口。
- 使用
initial块设置初始条件和激励序列。 - 使用延时操作符
#来控制仿真时间。 - 可以包含自检查逻辑,使用
$display打印信息。 - 不可综合(仅用于仿真)。
简单测试平台示例

`timescale 1ns / 1ps
module testbench_simple();
logic a, b, c, y;
silly_function dut (.a(a), .b(b), .c(c), .y(y)); // 实例化被测设计
initial begin
// 测试用例 1
a = 0; b = 0; c = 0; #10;
if (y !== 1'b1) $display("Error at t=%0t: a=%b,b=%b,c=%b, y=%b, expected 1", $time, a,b,c,y);
// 测试用例 2
a = 0; b = 0; c = 1; #10;
if (y !== 1'b0) $display("Error at t=%0t: a=%b,b=%b,c=%b, y=%b, expected 0", $time, a,b,c,y);
// ... 更多测试用例 ...
$display("Testbench finished.");
$finish;
end
endmodule

使用测试向量的高级测试平台
对于大量测试用例,可以将输入和预期输出存储在外部文件或内部数组中。
module testbench_vectors();
logic a, b, c, y;
logic y_expected;
logic [3:0] test_vectors [0:9999]; // 注意:Verilog数组声明是 [宽度] 名称 [深度]
// 这表示 10000 行,每行 4 比特。与C语言的行列顺序相反。
integer i, errors=0;
silly_function dut (.a(a), .b(b), .c(c), .y(y));
initial begin
$readmemb("test_vectors.txt", test_vectors); // 从文件读取二进制数据到数组
for (i=0; i<10000; i=i+1) begin
{a, b, c, y_expected} = test_vectors[i]; // 拆分向量
#10;
if (y !== y_expected) begin
$display("Error at vector %0d: inputs=%b%b%b, got y=%b, expected %b", i, a,b,c, y, y_expected);
errors = errors + 1;
end
end
$display("Test completed with %0d errors.", errors);
$finish;
end
endmodule
Lab 2 介绍与演示 🛠️
上一节我们深入学习了always语句和测试平台。本节中,我们来看看本次实验的具体内容,并做一个简单的FPGA交互演示。
Lab 2 目标:七段数码管译码器
实验目标是创建一个将4位二进制数转换为七段数码管显示码的译码器,并将其部署到FPGA开发板上。
任务概述:
- 编写
hex_driver模块,实现4位输入到7位输出的译码逻辑(输出低电平有效)。 - 在顶层模块
top.sv中,将18个拨码开关分成4组(最后一组只有2位),分别连接到4个(或5个)hex_driver实例。 - 将译码器的输出连接到开发板上的
HEX0至HEX7显示管。 - 通过仿真和上板测试验证功能。
FPGA 交互演示
以下是一个修改后的顶层模块示例,它使得拨动开关能控制LED的移动方向。
// 部分代码示例:在 always_ff 块中检测开关变化并控制LED
logic [17:0] sw_old, sw_diff;
logic [25:0] leds;
logic led_state; // 0=左移,1=右移
always_ff @(posedge slow_clock) begin
sw_old <= sw; // 保存开关上一状态
sw_diff <= sw ^ sw_old; // 检测变化的开关位
if (sw_diff != 0) begin
// 有开关变化
leds <= {26{1'b0}}; // 先清空LED
// 将变化的开关位置1(简单示例,实际可映射到LED)
// 这里简化处理,仅用变化检测位控制方向
if ((sw_diff >> 13) == 0) begin
// 变化发生在低13位(右侧),设置左移
led_state <= 1'b0;
leds[13] <= 1'b1; // 在中间附近点亮一个LED
end else begin
// 变化发生在高5位(左侧),设置右移
led_state <= 1'b1;
leds[13] <= 1'b1;
end
end else begin
// 无开关变化,执行移动
if (led_state == 0) begin
leds <= leds << 1; // 左移
end else begin
leds <= leds >> 1; // 右移
end
end
end
// 将 leds 连接到实际的LED引脚
assign led_r = leds[25:8];
assign led_g = leds[7:0];
这个演示展示了如何:
- 使用时序逻辑 (
always_ff)。 - 检测输入信号的变化边沿。
- 根据输入条件改变状态和行为。
- 将内部信号映射到FPGA的外部引脚。
总结 🎯


本节课中我们一起学习了以下内容:
- 测验回顾:分析了RISC-V循环控制、位操作、SystemVerilog信号冲突、模块作用域和运算符优先级等关键题目。
always语句:区分了用于组合逻辑的always_comb(使用阻塞赋值=)和用于时序逻辑的always_ff(使用非阻塞赋值<=),并强调了避免意外锁存器的重要性。- 测试平台:学习了如何编写自动化的测试平台来验证设计,包括简单的激励施加和从文件读取测试向量的高级方法。
- Lab 2 与 FPGA 部署:了解了本次实验的目标,并通过一个交互式演示,查看了如何将SystemVerilog代码与FPGA外设(开关、LED)结合,实现具体功能。

请记住,SystemVerilog的语法可能需要经常查阅参考,重点是理解其并发硬件描述的本质。实验时请充分利用提供的框架代码和工具脚本。
006:微架构设计入门
概述
在本节课中,我们将开始学习RISC-V处理器的微架构设计。微架构是处理器的内部实现细节,是公司的核心技术秘密。我们将从复习RAM和寄存器文件开始,逐步构建一个能够执行核心RISC-V指令集的流水线处理器。
复习:RAM与寄存器文件
上一节我们讨论了处理器架构,本节我们来看看其内部实现的基础组件:RAM和寄存器文件。
RAM基础
RAM是一个可以随机访问的存储器阵列。它有两个关键参数:深度和宽度。
- 深度:存储器中可寻址的位置数量。
- 宽度:每个位置存储的比特数。


一个RAM的规格通常表示为 深度 x 宽度。例如,一个1024 x 32的RAM有1024个地址,每个地址存储32位数据。地址线的数量由深度决定,公式为:地址线数量 = log₂(深度)。
在SystemVerilog中实现RAM
在SystemVerilog中,我们可以用行为描述来定义一个RAM。以下是声明一个RAM数组的语法:
logic [31:0] mem [8191:0]; // 宽度32位,深度8192
注意:在SystemVerilog声明中,顺序是 宽度在前,深度在后,这与通常的“深度 x 宽度”表述习惯相反。
以下是实现一个具有异步读、同步写功能的单端口RAM的示例代码:
module ram (
input logic clk,
input logic we, // 写使能
input logic [12:0] addr, // 13位地址 (log2(8192)=13)
input logic [31:0] din,
output logic [31:0] dout
);
logic [31:0] mem [8191:0];
// 异步读:地址变化,输出立即跟随
assign dout = mem[addr];
// 同步写:仅在时钟上升沿且写使能有效时写入
always_ff @(posedge clk) begin
if (we) begin
mem[addr] <= din;
end
end
endmodule
关键点:
- 异步读:读取操作不需要时钟,地址变化后数据立即出现在输出端。这在小型存储器(如寄存器文件)中可行,但在大型RAM中会导致时序问题。
- 同步写:写入操作需要一个时钟边沿来锁存数据,这是所有RAM的典型行为。
寄存器文件的特殊之处
寄存器文件是CPU内部的小型、多端口RAM。在RISC-V中,典型的寄存器文件是32 x 32(32个寄存器,每个32位),具有三个端口:两个读端口和一个写端口。
寄存器文件有两个特殊设计:
- 零寄存器:RISC-V架构规定寄存器x0的值恒为0。这可以在读逻辑中实现:当读地址为0时,直接输出0。
- 写后读旁路:当在同一周期内对同一寄存器进行写和读操作时(例如,流水线中前一条指令写回,后一条指令读取),需要将正在写入的数据直接“旁路”给读端口,以确保读到的是新值。否则,读操作会得到旧值。
以下是实现这些特性的寄存器文件读逻辑片段:
// 读端口1的逻辑示例
assign rd1 = (raddr1 == 5'b0) ? 32'b0 : // 零寄存器
((raddr1 == waddr) && we) ? wdata : // 写后读旁路
mem[raddr1]; // 正常读取
算术逻辑单元
接下来,我们看看处理器的计算核心:算术逻辑单元。
ALU是一个组合逻辑电路,它接收两个32位操作数和一个操作码,然后输出一个32位结果和一个标志位。在我们的设计中,ALU支持以下核心操作:
- 算术运算:加法 (
ADD)、减法 (SUB) - 逻辑运算:按位与 (
AND)、或 (OR)、异或 (XOR) - 移位运算:逻辑左移 (
SLL)、逻辑右移 (SRL)、算术右移 (SRA) - 比较运算:有符号小于 (
SLT)、无符号小于 (SLTU) - 乘法运算:获取乘积的低32位 (
MULL)、获取有符号/无符号乘积的高32位 (MULH,MULHU)
零标志:当ALU执行减法操作且结果为0时,零标志置位,这用于实现相等分支判断。
在实现时,需要注意SystemVerilog中有符号数和无符号数操作的区别,通常需要使用类型转换,例如 $signed()。
RISC-V指令编码格式
要设计控制单元,必须理解指令是如何编码成二进制机器码的。RISC-V有几种主要的整数指令格式,与MIPS有所不同。
以下是核心的指令格式:
- R-type (寄存器-寄存器):用于算术和逻辑运算。
- 字段:
funct7 | rs2 | rs1 | funct3 | rd | opcode
- 字段:
- I-type (立即数):用于加载、立即数运算、跳转和链接(
JALR)等。- 字段:
imm[11:0] | rs1 | funct3 | rd | opcode
- 字段:
- U-type (上立即数):用于加载大立即数到寄存器的高位(
LUI,AUIPC)。- 字段:
imm[31:12] | rd | opcode
- 字段:
关键变化:
- 字段位置:RISC-V的
opcode在最后,而MIPS在最前。功能码(funct)被拆分到多个字段。 - 寄存器命名:使用
rs1(源寄存器1)、rs2(源寄存器2)、rd(目的寄存器),比MIPS的rs,rt,rd更清晰。 - 立即数字段:I-type只有12位立即数,要加载32位常数需要结合
LUI(加载高20位)和ADDI(加载低12位)。 - 移位指令:在RISC-V中,普通移位指令(
SLL,SRL,SRA)的移位量来自寄存器(rs2),而立即数移位(SLLI,SRLI,SRAI)是I-type格式,移位量来自指令中的立即数字段。
处理器核心设计框架
现在,我们将各个部分组合起来,勾勒出处理器核心的设计框架。
顶层模块:CPU
我们从顶层的CPU模块开始。它包含指令存储器、程序计数器和基本的取指逻辑。
module cpu (
input logic clk,
input logic reset_n // 低电平有效的复位,通常连接板载按钮
);
// 指令存储器:4096深度 x 32宽度
logic [31:0] instr_mem [4095:0];
initial begin
$readmemh("program.hex", instr_mem); // 从文件初始化指令
end
// 程序计数器
logic [31:0] pc;
// 当前指令
logic [31:0] instr;
// 取指逻辑
always_ff @(posedge clk, negedge reset_n) begin
if (~reset_n) begin
pc <= 32'b0;
instr <= 32'b0;
end else begin
instr <= instr_mem[pc[31:2]]; // 按字寻址 (pc[1:0]恒为0)
pc <= pc + 4; // 默认顺序执行,下一条指令地址
end
end
// 后续将在这里添加译码、执行、访存、写回等阶段
// ...
endmodule
核心组件与数据通路
一个简化的CPU数据通路包含以下组件,它们将在后续的译码和执行阶段被连接起来:
- 寄存器文件:提供源操作数,接收写回结果。
- ALU:执行计算、比较等操作。
- 控制单元:根据
instr中的opcode和funct字段,生成所有控制信号(如ALUOp,RegWrite,MemWrite等),以控制数据通路的多路选择器、使能信号等。 - 立即数生成单元:根据指令格式(I, S, B, U, J),从
instr中提取并符号扩展出正确的32位立即数。
关于CSRRW指令
CSRRW (控制状态寄存器读写) 指令是RISC-V用于访问系统级寄存器的指令,在我们的实验中,它将用于访问FPGA板上的开关和七段数码管。
- 该指令是I-type格式。
- 它读取一个由12位立即数指定的CSR的值,并将其写入通用寄存器(
rd)。 - 同时,它将通用寄存器(
rs1)的值写入该CSR。 - 在我们的实现中,这是一个“hack”:读开关时只执行读操作,写数码管时只执行写操作。
总结
本节课我们一起学习了处理器微架构设计的基础。
- 我们回顾了RAM的工作原理及其在SystemVerilog中的实现方式,区分了同步和异步读写的差异。
- 我们深入了解了寄存器文件的特殊设计,包括零寄存器和写后读旁路机制。
- 我们介绍了ALU的功能和它支持的核心操作。
- 我们分析了RISC-V指令的编码格式,理解了R-type、I-type、U-type等格式的字段组成,这是设计控制单元的关键。
- 最后,我们勾勒出了处理器核心的设计框架,从取指逻辑开始,并概述了数据通路的主要组件。

在接下来的实验中,你们的任务就是连接这些组件(寄存器文件、ALU等),并设计控制单元,最终构建出一个能够执行一组核心RISC-V指令的流水线处理器。虽然这是一个挑战,但提供了ALU和寄存器文件的代码,将帮助大家专注于控制逻辑和数据通路的集成。
007:微架构设计(第二部分) 🧠

在本节课中,我们将继续学习RISC-V处理器的微架构设计,重点关注如何实现一个包含22条指令的流水线CPU。我们将从指令编码格式开始,逐步构建数据通路和控制单元,并最终完成一个可以运行在FPGA开发板上的处理器。
指令编码格式回顾

上一节我们介绍了Lab 3的目标是实现22条RISC-V指令。本节中,我们来看看这些指令的具体编码格式,这是设计控制单元的基础。
RISC-V的指令编码与MIPS不同,主要分为三种类型:R型、I型和U型。

- R型指令:用于寄存器-寄存器操作。与MIPS不同,它没有独立的移位量字段,但在指令中间增加了3位的
funct3字段,末尾有7位的funct7字段。- 公式表示:
指令[31:25] = funct7,指令[24:20] = rs2,指令[19:15] = rs1,指令[14:12] = funct3,指令[11:7] = rd,指令[6:0] = opcode
- 公式表示:
- I型指令:用于立即数操作或加载。它包含一个12位的立即数字段。
- 公式表示:
指令[31:20] = imm[11:0],指令[19:15] = rs1,指令[14:12] = funct3,指令[11:7] = rd,指令[6:0] = opcode
- 公式表示:
- U型指令:用于加载大立即数(如
LUI)。它包含一个20位的立即数字段。- 公式表示:
指令[31:12] = imm[31:12],指令[11:7] = rd,指令[6:0] = opcode
- 公式表示:
一个重要的设计简化是,在所有三种格式中,目标寄存器rd的地址都固定在指令的第11到第7位。这意味着我们可以直接将这几位连线到寄存器文件的写地址端口,而无需像MIPS那样需要一个多路选择器来选择是写入rd还是rt字段。
对于需要32位常量的情况,可以组合使用U型指令(提供高20位)和I型指令(提供低12位)。
特殊指令:CSRRW
在22条指令中,CSRRW指令较为特殊,它用于输入/输出操作。我们需要理解它的工作原理。
CSRRW指令执行一次“交换”操作。它将控制状态寄存器中的值读入目标通用寄存器rd,同时将源通用寄存器rs1中的值写入控制状态寄存器。
在我们的实验环境中,CSR寄存器IO0对应开发板上的开关(只读),IO2对应七段数码管(只写)。因此,实际实现时:
- 当对
IO0执行CSRRW时,我们只执行“读”操作,将开关值读入rd。 - 当对
IO2执行CSRRW时,我们只执行“写”操作,将rs1的值输出到数码管。
CSRRW是I型指令,其CSR寄存器地址编码在12位的立即数字段中,理论上可以寻址4096个CSR。
三阶段流水线设计概述
在计算机体系结构中,指令执行通常被划分为多个阶段。我们采用一个简化的三阶段流水线设计,以平衡复杂度和性能。
以下是三个阶段的划分:
- 取指阶段:从指令存储器中读取指令。由于我们使用同步读取的RAM,此操作需要一个完整的时钟周期。
- 执行阶段:此阶段合并了译码、执行和(本实验不涉及的)内存访问操作。它包含寄存器文件读取、控制单元、ALU运算等所有核心数据通路。
- 写回阶段:将结果写回寄存器文件。
这种设计意味着在任何时刻,CPU内部最多有三条指令处于不同的执行阶段。为了清晰地区分不同阶段的信号,建议为信号名添加后缀,例如:
PC_fetch:取指阶段的程序计数器。instruction_EX:执行阶段正在处理的指令。RegWrite_WB:写回阶段的寄存器写使能信号。

CPU顶层模块与启动代码
为了帮助大家开始,我们提供了一个CPU设计的起点。以下是对启动代码中关键部分的分析。
CPU的顶层模块(CPU.sv)已经包含了取指阶段的基本逻辑。指令存储器被实例化在CPU模块内部,并通过initial语句从文件加载程序。
always_ff @(posedge clock) begin
PC_fetch <= PC_fetch + 1; // 每个周期PC加1
instruction_EX <= instruction_ram[PC_fetch]; // 读取指令
end
重要提示:上述代码中使用了非阻塞赋值<=。这意味着PC_fetch的更新和instruction_ram的读取是同时发生的。因此,在某个时钟周期,instruction_EX输出的是上一个周期的PC_fetch所指向的指令。这实际上在取指和执行之间引入了一个周期的流水线寄存器。
CPU通过一个顶层包装模块(top.sv)与FPGA板载资源连接:
clock_50连接 CPU的clock。- 按键
KEY0连接 CPU的reset_n。 - 开关
SW连接 CPU的IO0_in(需要补零至32位)。 - CPU的
IO2_out输出连接到八个七段数码管解码器。
对于仿真,我们使用sim_top.sv,它内部实例化top并生成时钟和复位信号。
执行阶段数据通路构建
执行阶段是设计的核心。我们需要连接寄存器文件、ALU,并构建必要的数据路径和多路选择器。
以下是构建执行阶段的关键步骤:

-
连接寄存器文件:
- 将
instruction_EX[19:15]直接连接到寄存器文件的read_addr1端口(对应rs1)。 - 将
instruction_EX[24:20]直接连接到read_addr2端口(对应rs2)。 - 目标寄存器地址
instruction_EX[11:7](rd字段)需要被延迟一个周期,以匹配写回阶段。我们用一个寄存器来实现这个流水线延迟,产生RegDest_WB信号。
- 将
-
构建ALU输入多路选择器:
- ALU的第一个操作数总是来自寄存器文件的
read_data1。 - 第二个操作数可以是寄存器文件的
read_data2(用于R型指令),也可以是来自I型指令的符号扩展后的12位立即数。这需要一个由ALU_src信号控制的多路选择器。 - 注意:RISC-V规定,即使是用于逻辑操作的立即数(如
ANDI),也进行符号扩展,这与MIPS不同,简化了硬件设计。
- ALU的第一个操作数总是来自寄存器文件的


-
构建写回数据多路选择器:
- 需要写回寄存器文件的数据可能有三个来源:ALU的结果、来自开关的
IO0_in数据、或U型指令的立即数(经过移位和补零)。 - 这需要一个由
Reg_select信号控制的三选一多路选择器,该选择器位于写回阶段。
- 需要写回寄存器文件的数据可能有三个来源:ALU的结果、来自开关的
-
控制信号流水线寄存器:
- 由控制单元在执行阶段产生的控制信号,如
RegWrite(寄存器写使能)和Reg_select,也需要被延迟一个周期,才能用于写回阶段。这通过额外的寄存器实现。
- 由控制单元在执行阶段产生的控制信号,如


控制单元设计

控制单元是一个组合逻辑模块,其作用是将指令字解码为控制数据通路的各个信号。
控制单元的输入是指令的特定字段,输出是控制信号。设计时可以采用查找表或if-else语句的形式。

以下是控制单元设计的关键点:

- 主要输入:
opcode是首要解码字段。对于某些opcode,还需要结合funct3、funct7甚至CSR地址(对于CSRRW)来唯一确定指令。 - 核心输出信号:
ALU_op:指示ALU执行何种操作(加、减、与、或等)。ALU_src:选择ALU的第二个操作数来源(寄存器 or 立即数)。Reg_select:选择写回寄存器文件的数据来源(ALU结果、IO输入、立即数)。RegWrite:寄存器文件写使能信号。GPIO_write_enable:仅当执行向IO2(数码管)的CSRRW写操作时置位。
为了代码清晰和易于调试,可以创建一个单独的“解码器”模块,它仅用assign语句为指令的各个字段(如funct3, imm12)创建易于理解的别名信号(如funct3_EX, imm12_EX),而不添加任何额外逻辑。
控制信号表示例
为了具体说明,我们来看几条指令的控制信号设置。控制单元本质上是一个大的真值表。
以下是部分指令的控制信号示例:
| 指令 | opcode (hex) | funct3 | funct7 | ALU_op | ALU_src | Reg_select | RegWrite | GPIO_we |
|---|---|---|---|---|---|---|---|---|
ADD |
0x33 | 0x0 | 0x00 | 0011 (加) |
0 (Reg) | 2 (ALU) | 1 | 0 |
SUB |
0x33 | 0x0 | 0x20 | 0110 (减) |
0 (Reg) | 2 (ALU) | 1 | 0 |
AND |
0x33 | 0x7 | 0x00 | 0000 (与) |
0 (Reg) | 2 (ALU) | 1 | 0 |
ANDI |
0x13 | 0x7 | – | 0000 (与) |
1 (Imm) | 2 (ALU) | 1 | 0 |
LUI |
0x37 | – | – | – (不经过ALU) | X | 1 (Imm) | 1 | 0 |
CSRRW (读IO0) |
0x73 | 0x1 | – | – | X | 0 (GPIO) | 1 | 0 |
CSRRW (写IO2) |
0x73 | 0x1 | – | – | X | X | 0 | 1 |

说明:
–表示该字段在此指令中无效或为任意值。X表示具体值取决于实现,或在该指令中不关心。ALU_op的值对应我们提供的ALU模块的功能编码。Reg_select中,0代表GPIO输入,1代表U型立即数,2代表ALU结果。

你需要为所有22条指令创建完整的控制信号表。
测试与验证方法
设计完成后,我们需要验证CPU功能的正确性。我们提供两种测试程序。
以下是两种测试方法:
- 指令测试程序:这是一个包含所有22条指令的简单程序。它不执行有意义的计算,但能确保每条指令的基本功能正确。你可以在ModelSim中运行仿真,通过观察寄存器值的变化来验证。
- 二进制转十进制演示程序:这是一个有实际功能的程序,也将在FPGA板上演示。其功能是将开关输入的二进制数转换为十进制,并显示在数码管上。
- 算法思路:通过反复“除以10取余”来分离出每一位十进制数字。由于没有硬件除法指令,且除数是常数10,我们可以通过乘以10的倒数(0.1)的定点数表示来实现“除法”,再通过一些运算恢复余数。
- 循环问题:由于当前指令集不支持分支,我们需要将处理8个数码管的代码“展开”,重复写8次,而不是使用循环。
- 连续运行:程序计数器会在地址空间内循环(例如,从0到4095再回到0),因此程序会以很高的频率(约每秒上万次)重复执行,使得数码管显示看起来是实时更新的。
当这两个测试程序都能正确运行时,我们就可以确信CPU的核心功能已经实现。
总结与实验安排
本节课中,我们一起学习了RISC-V处理器微架构设计的关键部分。我们从指令编码入手,详细讲解了一个三阶段流水线CPU的数据通路设计,包括取指、执行和写回阶段的组件连接与控制信号传递。我们还深入分析了特殊的CSRRW指令以及控制单元的设计方法。

Lab 3要求你根据这些原理,实现一个支持22条指令的RISC-V CPU。实验材料包中提供了启动代码、寄存器文件和ALU模块。你有三周的时间来完成这个实验。请务必理解数据通路中每一处连接和多路选择器的意义,并仔细构建控制单元。如果在实现过程中遇到问题,请及时寻求帮助。
008:P08 - RISC-V HDL 设计 🧠
在本节课中,我们将回顾RISC-V微架构的关键概念,特别是寄存器文件、ALU设计以及控制单元的实现。我们将详细解释如何在硬件描述语言中实现这些组件,并构建一个简单的RISC-V CPU核心。
概述:RISC-V微架构回顾

上一节我们介绍了项目背景,本节中我们来看看RISC-V微架构的具体实现细节。我们将从存储阵列开始,逐步构建CPU的数据通路和控制逻辑。
RAM阵列与寄存器文件
RAM阵列用于实现我们的寄存器文件。一个RAM阵列包含一个地址输入,并支持读写操作。它被组织为一个二维比特阵列,其深度表示条目数量,宽度表示每个周期可访问的比特数。
一个RAM阵列可以有多个端口,每个端口允许在每个周期进行一次访问。这些端口有时可能仅限于读取,有时可以读写,具体取决于编码方式。
在Verilog中编码并综合后,我们希望FPGA上有基本的电路单元能够实现该行为。

对于寄存器文件,它需要三个端口:两个读端口和一个写端口,这与R型指令格式相匹配。
问题在于,当在FPGA上综合时,它实际上会生成一个由寄存器组成的二维阵列,并带有用于输出的多路选择器和用于写入部分的解码器。这种方式效率很低,因为FPGA的寄存器数量相对有限(例如约10万个),而使用RAM则有更多的存储比特可用。
但综合工具可能不会将我们的代码映射到FPGA上特定的RAM硬核,因为硬核RAM通常具有同步读取,并且端口数量有限(例如两个端口)。换句话说,你可以在Verilog中实现RAM行为,但这并不保证它会使用FPGA上特定的RAM硬核。有时你必须限制RAM的行为编码方式。
同步与异步读取
我们讨论了同步读取与异步读取的区别。
- 异步读取:当地址改变时,对应地址的数据会立即输出,无需等待时钟边沿。
- 寄存器文件需要异步读取,因为我们不希望等待整个周期来读取寄存器值,至少在我们的设计中不需要。在我们的三级流水线中,我们需要在同一周期内读取寄存器值并将其送入ALU。
- 写入操作总是同步的:写入必须等待时钟边沿才能发生。
下图展示了异步读取(上)和同步读取(下)的时序差异。主要区别在于读取到的数据:在异步读取中,数据输出更早;在同步读取中,必须等待上升沿才能执行读取操作,因此数据输出会延迟一个周期。
具体来说,在时钟上升沿同时改变地址时:
- 异步读取:读取的是新地址(例如地址5)对应的数据(数据5)。
- 同步读取:RAM看到的是地址改变前的旧值(例如地址4),因此输出的是旧地址的数据(数据4)。新地址(地址5)的数据(数据5)会在下一个周期输出。
寄存器文件设计实现
以下是寄存器文件的设计。它内部包含一个RAM阵列。
在声明RAM阵列时,参数的顺序是宽度在前,深度在后,这与C、Python或Java等语言的习惯相反。
两个读取操作通过assign语句实现异步读端口(在项目框架中可能使用always语句,但逻辑基本相同)。写操作通过一个always_ff块在下半部分实现。
你可以在设计中使用这个寄存器文件。寄存器0(x0)的值始终为0。如果你同时读取和写入同一个寄存器,应该得到正在写入的新值,这就是寄存器旁路。
算术逻辑单元设计
我们将使用一个能够执行约13种操作的ALU,但总共有16个操作码,因此有些操作码是冗余的。例如,1101、1110和1111都执行相同的操作:无符号数的小于比较(A < B unsigned)。1010、1011等都是算术右移。
在RISC-V和MIPS指令集中,每个操作对应一条R型指令,有些指令会间接使用这些操作。例如,分支指令会使用比较操作码来判断条件,尽管它本身不是set less than指令。
这些ALU功能主要用于R型和I型的算术、逻辑、移位和比较指令,即数值计算指令。
以下是ALU的代码。ALU本身主要包含在一个使用三元运算符的assign语句中,此外还有一些额外的代码来处理移位器。
关于算术右移:右移时,移入的比特是符号位(最高位)。Verilog中本应有操作符>>>来实现,但由于某些原因,它对我们不适用。因此,我们使用一个always块来手动实现:它检查移位量的每个比特,并使用阻塞赋值语句。如果第0位为1,则移位1位;如果第1位为1,则可能再移位2位。这样,如果移位量是3(二进制11),你将先移1位,再移2位,总共移3位。以此类推,可以根据移位量中设置的比特,移位1、2、4、8或16位,或它们的任意组合。
always_comb块用于告诉编译器设计意图是组合逻辑,不应包含存储器或时序逻辑。如果意外在其中引入了锁存器,将会产生错误。这与always @*类似,但多了额外的检查。begin和end用于包裹多行代码,如果只有一行代码,则不需要。
关于有符号乘法和有符号小于比较:在Verilog中,logic类型默认被视为无符号数。因此,如果需要进行有符号乘法或有符号小于比较,必须将其类型转换为signed。可能有一开始就将logic声明为signed的方法,但这里我们使用了类型转换。
指令格式与解码
在Lab 3中,我们将实现22条指令,涉及三种编码格式(Lab 4会有另一种格式):
- R型:所有操作涉及三个寄存器(目标寄存器和两个源寄存器)。大多数指令属于此类型。
- I型:指令包含一个源寄存器、一个目标寄存器和一个12位立即数。
- U型:包含一个20位立即数,目前仅用于一条指令:加载高位立即数(
lui)。
你可能想问为什么是12位和20位?原因是,如果你想将一个32位常量加载到寄存器中,可以结合使用lui(加载高20位)和一条I型指令(加载低12位),两者结合即可得到32位数据。
指令解码时,必须首先检查操作码,因为这是所有编码格式共有的字段。根据操作码,有时还需要查看funct3字段,有时甚至需要额外查看funct7字段,才能确定具体是哪条指令。这取决于指令本身。稍后我们将展示如何在代码中实现这种解码。
CSRRW 指令与 I/O 映射
我们讨论了CSRRW指令。我们有两个I/O寄存器:
IO0:对应板载开关。从IO0读取时,CPU读取开关值。IO2:对应七段数码管显示。向IO2写入时,CPU将数据输出到数码管。
使用CSRRW指令时,CSR寄存器编号(0或2)总是汇编代码中的第二个操作数。
- 读取开关:目标寄存器
rd用于存放读到的开关值,源寄存器rs无关。 - 写入显示:源寄存器
rs的内容将被送到数码管显示,目标寄存器rd无关。
实际上,在标准的RISC-V中,CSRRW执行交换操作:将CSR寄存器的值读入rd,同时将rs的值写入CSR。但为了与开发板配合,我们进行了一些修改,因为开关只能读,数码管只能写。这比我们之前在MIPS中使用的将特定寄存器(如$30)映射为特殊I/O寄存器的方法更规范。
顶层设计与控制单元
开始设计CPU时,可以从定义顶层模块开始。模块包含以下输入:
clockreset_n(低电平有效复位,便于连接按键)io0_in(连接开关,需将18位开关值零扩展到32位)io2_out(32位输出,每4位驱动一个七段数码管解码器)
复位信号的作用是:当复位有效时,将取指逻辑中的指令重置为零,并将程序计数器PC_fetch重置为零。指令零应设计为无操作指令。在RISC-V中,add x0, x0, x0常被用作无操作指令。



指令存储器可以声明为一定深度(如4096),并用包含机器码的文本文件初始化。

为了便于调试,可以为指令的各个字段定义有意义的信号名。例如:
logic [4:0] rd_EX;
assign rd_EX = instruction_EX[11:7];
这不会产生任何硬件开销,只是为指令的一部分起了别名。你也可以将其放入单独的模块,但需要定义许多输出端口。
另一个调试技巧是使用SystemVerilog的枚举类型来定义指令名称,这样在波形图中可以直接看到执行阶段的是哪条指令。


控制单元可以作为一个always_comb块实现。首先设置默认输出(例如instruction_in_EX为unknown)。然后,通过检查opcode_EX、funct3_EX和funct7_EX等字段,使用if-else或case语句来识别具体指令,并为该指令设置所有相应的控制信号。

这些控制信号包括:
ALU_source_EX:选择ALU第二个操作数来自寄存器(0)还是立即数(1)。ALU_op_EX:指定ALU执行的操作(如加法0011)。reg_write_EX:指示是否写回寄存器。reg_select_EX:选择写回数据的来源(如ALU输出)。gpio_write_enable_EX:专门用于CSRRW指令向IO2写入时使能。

控制单元也可以放在独立的模块中,使顶层代码更清晰。



测试程序与仿真

我们提供了一个测试程序,它依次测试每一条指令。为了验证CPU是否工作,可以:
- 在仿真中观察寄存器文件的内容,与程序注释中的预期值进行比对。
- 使用
CSRRW指令,通过观察外部可见的输出(如模拟的数码管输出)来判断。

另一个测试程序是二进制到十进制的转换程序。它将开关输入的二进制数转换为十进制并显示在数码管上,其中会使用定点数运算来近似除以10和取模10的操作。
在ModelSim等仿真工具中进行测试时,需要:
- 确保有时钟信号(“心跳”)。
- 观察程序计数器
PC_fetch是否按预期递增。 - 观察
instruction_EX流是否与预期指令匹配。 - 将寄存器文件的存储阵列信号添加到波形中,观察寄存器值的变化。
- 检查指令存储器是否被正确初始化。
总结与后续步骤
本节课中我们一起回顾并深入探讨了RISC-V微架构的HDL实现。我们涵盖了:
- RAM阵列与寄存器文件的实现及其在FPGA上的映射考量。
- 同步与异步读取的时序差异。
- 算术逻辑单元的设计细节,包括有符号操作的处理和移位器的实现。
- RISC-V的指令格式(R/I/U型)和解码逻辑。
- 通过
CSRRW指令实现I/O映射。 - CPU顶层模块的构建、控制单元的设计以及调试技巧。
- 测试程序的编写与仿真验证方法。
要完成这个CPU,接下来需要:
- 将ALU和寄存器文件实例化并连接到数据通路。
- 在控制单元中完善所有22条指令的识别逻辑,并为每条指令生成正确的控制信号组合。
- 进行充分的仿真测试,确保每条指令功能正确。
- 最终在FPGA开发板上进行验证。

通过本教程的学习,你应该对如何用HDL从头开始构建一个简单的RISC-V CPU核心有了清晰的认识。
009:RISC-V分支与跳转指令 🚀


在本节课中,我们将要学习RISC-V处理器设计中的分支与跳转指令。我们将回顾即将到来的考试和实验安排,并深入探讨如何为RISC-V CPU添加分支和跳转功能,包括理解其指令格式、计算目标地址以及处理相关的控制信号。

课程与实验安排概述
上一节我们介绍了RISC-V的基本流水线结构。本节中,我们来看看近期的课程安排。
- 第一次考试将于10月23日至27日这一周进行。考试将通过Dropbox发布,学生可在该周内任意时间登录并开始考试。考试开始后,有75分钟时间完成。考试包含15道选择题,允许查阅资料。
- 考试内容将涵盖:SystemVerilog语法、RISC-V指令集架构以及RISC-V微架构(重点是三级流水线)。Dropbox上有往届试题可供参考。
- 实验三的截止日期是10月13日(本周五)。实验四将于下周发布。
实验三目标回顾
以下是实验三的核心目标:设计一个能够执行特定指令集的RISC-V处理器。
该处理器需要能够执行22条指令,主要包括算术、逻辑、比较和一条I/O指令。此最小指令集足以编写一个实用程序:在DE2开发板的开关上输入一个数字,进行二进制到十进制的转换,并将十进制结果输出到七段数码管上显示。
实验涉及的RISC-V指令格式主要有三种(实际可视为两种,因为U型仅用于LUI指令):
- R型:用于寄存器-寄存器操作。
- I型:用于立即数操作和I/O访问。
- U型:用于长立即数加载(LUI)。
CPU数据通路与控制单元设计
理解了指令集后,我们来看看如何构建CPU。核心组件包括指令存储器、程序计数器、寄存器文件、ALU和控制单元。
控制单元的设计是关键,它通常是一个大型的if-else语句块,根据当前指令的操作码和功能码字段,生成控制数据通路的多路选择器、寄存器写使能等信号。
以下是控制信号解码的一个示例代码片段:
// 解码器示例:提取指令字段
assign funct3 = instruction[14:12];
assign funct7 = instruction[31:25];
assign csr_field = instruction[31:20]; // 与imm12共享高位
对于CSRRW这条特殊的I/O指令,需要特别注意。它根据12位立即数字段的值(0或2)来决定是读取开关输入还是写入数码管输出。在我们的实现中,对此指令的行为进行了定制:
- 当立即数字段为0时,从开关读取数据到指定寄存器,并不向CSR写入。
- 当立即数字段为2时,将指定寄存器的数据写入数码管,并不向通用寄存器写回数据。
三级流水线结构
我们的处理器采用三级流水线设计,这意味着在任何时刻,最多有三条指令处于不同的执行阶段。
- 取指阶段:主要包含程序计数器,负责生成下一条指令的地址。
- 执行阶段:这是最复杂的阶段,包含寄存器文件的读写端口、ALU的输入输出以及控制单元。指令在此阶段被解码并执行操作。
- 写回阶段:主要负责将结果写回寄存器文件。
数据在阶段之间通过寄存器(触发器)传递,以同步时序。例如,执行阶段产生的“寄存器写使能”信号需要延迟一个周期,在写回阶段才实际生效。
二进制转十进制程序示例
实验三要求编写一个将二进制数转换为十进制显示的程序。由于当前指令集不支持循环和分支,代码需要展开。
基本算法是重复进行“模10”和“除10”操作来提取每一位十进制数字。以下是该算法的核心思路描述:
结果寄存器 = 0
循环 8 次:
当前数字 = 输入值 % 10
输入值 = 输入值 / 10
结果寄存器 = (结果寄存器 >> 4) | (当前数字 << 28)
每次迭代提取出的数字被放置到结果寄存器的高4位,然后结果寄存器逻辑右移4位。经过8次迭代后,最早提取的数字(最高位)将移动到最低位,最终结果寄存器中存储的32位数可以直接输出到数码管显示。
引入分支与跳转指令
接下来,我们为下一阶段的实验做准备:添加分支和跳转指令。这与MIPS架构有重要区别。
首先,RISC-V的分支指令采用PC相对寻址,且偏移量的计算是相对于分支指令自身的地址,而不是其下一条指令的地址。这与MIPS不同。

为了在流水线中计算分支目标地址,我们需要知道当前执行阶段指令的PC值。一个简便的方法是添加一个PC_EX寄存器,它比PC_fetch延迟一个周期,正好存储着执行阶段指令的地址。
分支指令使用一种新的B型指令格式。其12位偏移量在指令编码中被拆分成多个不连续字段,需要按特定规则拼接和符号扩展。以下是计算分支偏移量的示例代码:
// 从B型指令中提取并拼接偏移量
assign branch_offset = { {19{instruction[31]}}, instruction[31], instruction[7], instruction[30:25], instruction[11:8], 1'b0 };
// 注意:由于我们采用字寻址,实际使用时常需忽略最低2位
跳转指令(如JAL)使用J型指令格式,同样采用PC相对寻址。其20位偏移量的编码方式也很特殊,需要类似的拼接操作。JAL指令总是会将返回地址(PC+4)写入目标寄存器,若想实现无条件跳转而不链接,可将目标寄存器设置为x0。
控制信号小测验解析
最后,我们通过一个小测验来巩固对控制信号的理解。问题涉及在流水线不同阶段,针对特定指令(如LUI, CSRRW, ADDI),控制信号RegWrite、RegSel和ALUSrc应如何设置。

关键点在于:
LUI指令不经过ALU,其ALUSrc为无关项,RegSel应选择立即数路径。CSRRW指令的行为取决于立即数。在标准RISC-V中,它总是写寄存器;但在我们定制的I/O处理中,向数码管写数据时RegWrite应为0。ADDI是I型指令,其ALUSrc应为1,以选择立即数作为ALU的第二个输入。


总结

本节课中我们一起学习了RISC-V处理器设计的多个重要主题。我们回顾了实验三的目标,即构建一个支持基础算术和I/O操作的CPU,并详细探讨了其数据通路、控制单元和三级流水线。我们还为接下来的实验做了铺垫,深入分析了RISC-V中独特的分支与跳转指令格式、PC相对寻址的计算方法,以及如何为流水线处理器实现这些功能。理解这些控制信号的生成与数据流向,是成功完成处理器设计的关键。
010:时序逻辑 🕰️
在本节课中,我们将要学习时序逻辑的核心概念。时序逻辑是包含记忆功能的数字逻辑,其输出不仅取决于当前输入,还取决于输入的历史序列。我们将探讨其与组合逻辑的区别、在Verilog中的实现方式,以及如何避免常见的设计陷阱。最后,我们将通过有限状态机和AXI总线协议等实例,了解时序逻辑在实际硬件设计中的应用。
时序逻辑基础
上一节我们介绍了数字逻辑的基本分类。本节中我们来看看时序逻辑的具体定义。
时序逻辑是包含记忆功能的数字逻辑。组合逻辑的输出仅取决于当前输入,而时序逻辑的输出则取决于当前输入和输入的历史序列。因此,时序逻辑电路中包含某种形式的记忆元件。
有几个术语容易混淆:
- 从技术上讲,时序逻辑是指输出取决于当前输入值以及过去输入序列的逻辑,这暗示了记忆功能的存在。
- 同步时序逻辑特指使用时序逻辑并包含时钟信号的情况。
理论上可以存在没有时钟的时序逻辑,即异步时序逻辑。但遗憾的是,异步时序逻辑在实际中难以稳定工作。如果能设计出实用的无时钟时序逻辑,将是一个价值数十亿美元的想法,因为它能显著降低芯片功耗。然而,更简单、更可靠的方法是使用时钟。时钟同步所有时序逻辑元件,确保它们在同一时刻更新。
我们还会频繁使用“状态”这个术语。状态是指电路中解释未来行为所必需的所有信息,本质上是电路内部必须寄存的记忆内容。这些记忆本身由锁存器或触发器构建。
Verilog中的记忆实现
上一节我们了解了时序逻辑的概念。本节中我们来看看在Verilog中如何描述具有记忆功能的逻辑电路。
在Verilog中,有两种主要方式可以描述具有记忆功能的逻辑电路,这两种方式都使用 always 语句。
第一种方式:
always @(posedge clock) begin
A <= B;
end
这个电路具有记忆功能,因为 B 是一个输入,但它不在敏感列表中。如果 B 在时钟上升沿之外发生变化,电路必须记住时钟沿到来时 B 的旧值。
第二种方式:
always @(*) begin
if (A) begin
B = C;
end
end
这看起来像一个组合逻辑的 always 块(* 表示对块内所有输入信号敏感)。然而,问题在于其中的 if 语句:如果条件为假,则输出 B 不会被赋值。这意味着 always 块可能被激活,但不会对其输出 B 进行赋值,因此 B 必须保持其上一次被赋值的旧值,从而产生了记忆功能。
总结来说,产生记忆功能有两种情况:
- 敏感列表未包含所有输入信号。
always块内的控制流导致并非每次激活时都对所有可能的输出进行赋值。
第一种方式(在时钟边沿触发)通常会创建一个触发器。第二种方式(使用信号控制输出赋值)则会创建一个锁存器。
锁存器与触发器
上一节我们看到了Verilog中产生记忆的两种方式。本节中我们通过时序图来具体看看锁存器和触发器的工作差异。
考虑以下时序图,包含时钟 clock、输入 D 和输出 Q。

对于锁存器:
always @(*) begin
if (clock == 1‘b1) begin
Q = D;
end
end
在锁存器情况下,只要时钟为高电平,D 输入就会直接传递到 Q 输出。换句话说,当时钟为高时,Q 总是等于 D。当时钟为低电平时,即使 D 发生变化,Q 也不会改变。
对于触发器:
always @(posedge clock) begin
if (clock == 1‘b1) begin
Q <= D;
end
end
在触发器情况下,只有在时钟的上升沿,才会对 D 进行“快照”采样。Q 只会在时钟上升沿根据采样到的 D 值发生变化。在其他时间,D 的变化不会影响 Q。



锁存器的问题在于它会使得时序分析复杂化。虽然FPGA可以支持锁存器行为,但为了最大化时钟频率,应该使用触发器。因此,在代码中应避免无意中产生锁存器行为,除非确实需要。在本课程中,应避免使用锁存器。
使用 always_comb(SystemVerilog)是好的做法,因为如果代码中意外产生了锁存器,编译器会报错。如果使用 always @(*) 并意外产生了锁存器,可能只会收到警告。
要避免产生锁存器,只需确保在所有控制路径下都对输出进行赋值。例如,为 if 语句添加 else 分支,或者在 if 语句之前为输出赋予一个默认值。

寄存器与状态机
上一节我们区分了锁存器和触发器。本节中我们来看看由多个触发器构成的寄存器,并引入有限状态机的概念。
多个并行的触发器共同构成一个多位触发器,通常被称为寄存器。但需要注意,这里存在术语混淆:
- 电路中的寄存器:指一个多位触发器,具有多位输入
D和输出Q。 - 汇编语言或寄存器文件中的寄存器:实际上是RAM(随机存取存储器)的一部分,用于存储数据。


在微架构设计中提到寄存器时,通常指的是一个多位触发器。
在Verilog中创建触发器非常简单:
always_ff @(posedge clock) begin
Q <= D;
end
always_ff 是可选的,用于向综合器提示设计意图。可以将其放在独立的模块中,也可以内联在其他模块中。
还可以创建带使能端的触发器,只有当使能信号为高时,才会在时钟边沿更新输出;否则,输出将保持原值。复位功能也很重要,例如程序计数器需要在复位时清零。复位可以是同步的(在时钟边沿检查复位信号)或异步的(复位信号独立于时钟立即生效)。在本课程中,通常使用异步复位。
除了简单的数据存储,时序逻辑还可用于构建计数器或有限状态机。
有限状态机是一种引入顺序性的电路。当某个硬件操作需要分解为多个顺序执行的步骤时,就必须使用状态机。在Verilog中,所有代码都是并发执行的,但状态机可以描述这种分阶段的行为。
有限状态机由输入、输出和一组状态(代表其可以处于的操作模式)定义。状态之间的转移由当前状态和输入决定。输出可以仅取决于当前状态(摩尔机),也可以同时取决于当前状态和输入(米利机)。
有限状态机实例:交通灯控制器
上一节我们引入了有限状态机的概念。本节中我们通过一个交通灯控制器的具体例子来理解其设计过程。
假设一个两条道路(学术大道和勇敢大道)的交叉路口。控制器有以下输入:
TA,TB: 两条道路上的车辆检测传感器(二进制)。
控制器有以下输出:LA,LB: 两条道路上的交通灯状态(三进制:红、黄、绿)。
我们定义四个状态:
S0: 学术大道绿灯,勇敢大道红灯(复位状态)。S1: 学术大道黄灯,勇敢大道红灯。S2: 学术大道红灯,勇敢大道绿灯。S3: 学术大道红灯,勇敢大道黄灯。
状态转移规则如下:
- 在
S0(学术大道绿灯):只要TA为真(有车),就保持在S0;当TA为假时,转移到S1。 - 在
S1(学术大道黄灯):无条件在下一个时钟周期转移到S2。 - 在
S2(勇敢大道绿灯):只要TB为真,就保持在S2;当TB为假时,转移到S3。 - 在
S3(勇敢大道黄灯):无条件在下一个时钟周期转移回S0。
在硬件实现中,需要创建状态转移表(真值表),将当前状态和输入映射到下一个状态。然后为每个状态分配一个二进制编码(例如,S0=00, S1=01, S2=10, S3=11)。接着,可以为每个状态位(S1, S0)推导出布尔代数表达式,实现下一状态逻辑。输出逻辑也需要一个真值表,将当前状态(或当前状态加输入)映射到输出信号。

在SystemVerilog中,可以使用枚举类型来定义状态,使代码更易读和调试。状态机代码通常分为三个部分:
- 时序逻辑部分:在时钟边沿更新当前状态。
- 下一状态逻辑部分:组合逻辑,根据当前状态和输入计算下一个状态。
- 输出逻辑部分:组合逻辑,根据当前状态(摩尔机)或当前状态和输入(米利机)产生输出。
高级实例:AXI总线接口状态机

上一节我们看了一个简单的状态机例子。本节中我们来看一个更复杂、更贴近实际应用的例子:AXI总线接口控制器。
AXI(高级可扩展接口)是ARM公司提出的一种高性能片上总线协议,广泛应用于现代嵌入式系统和SoC中,用于连接处理器、内存和外设。它支持多 outstanding 请求、全双工通信和突发传输。
AXI协议包含五个独立的通道:
AR: 读地址通道AW: 写地址通道R: 读数据通道W: 写数据通道B: 写响应通道
以写事务为例,其握手过程分为多个阶段:
- 主设备在
AW通道上发出地址和AWVALID信号,并等待从设备的AWREADY响应。 - 收到
AWREADY后,主设备在W通道上发出写数据、WVALID信号,并等待从设备的WREADY响应。对于单次写,同时置高WLAST。 - 主设备置高
BREADY,表示准备好接收响应,并等待从设备返回BVALID和响应码(如OKAY或ERROR)。

由于这些握手必须在单个时钟周期粒度内完成,速度太快无法用软件(位操作)实现,因此必须使用硬件状态机。



设计一个简化的AXI写接口模块,其用户侧接口非常简单:写使能、写地址、写数据,以及忙和错误状态信号。模块内部则需要一个状态机来管理复杂的AXI握手协议。



状态机可能包含以下状态:
IDLE: 空闲状态,等待用户请求。SEND_AW: 等待并发送写地址。WAIT_WREADY: 等待写数据通道就绪。SEND_W: 发送写数据。WAIT_BRESP: 等待写响应。ERROR: 错误处理状态。

状态机根据当前状态和来自AXI总线的握手信号(如 AWREADY, WREADY, BVALID)决定状态转移,并在每个状态产生相应的输出信号(如 AWVALID, WVALID, BREADY 等)。
在SystemVerilog实现中,同样使用枚举类型定义状态,并分为时序逻辑、下一状态逻辑和输出逻辑三个部分。下一状态逻辑通常使用 case 语句根据当前状态和输入条件跳转,输出逻辑则根据当前状态驱动总线信号和用户状态信号。
这个例子展示了状态机如何将复杂的、多阶段的通信协议转换为可靠的硬件控制逻辑。


时序分析简介
在本课程的最后,我们简要介绍时序分析的概念,这是数字电路设计从功能正确走向性能达标的关键一步。
到目前为止,在ModelSim中的仿真只关注功能正确性,没有考虑逻辑门的实际延迟。仿真中的 δ 延迟是无限小的。但在现实中,逻辑门存在输入变化到输出变化之间的传播延迟。


延迟的根本原因在于电阻电容(RC)电路。数字逻辑门使用电容来开关晶体管,导线本身也存在对地的寄生电容。延迟时间大致等于电阻乘以电容(RC时间常数)。导线越长,电容越大,延迟也越大。

有两种关键的延迟时间:
- 传播延迟
t_pd:从输入变化到输出可能变化的最长时间。 - 污染延迟
t_cd:从输入变化到输出可能变化的最短时间。

输出在 [t_cd, t_pd] 这个时间窗口内是不稳定的,可能发生改变。延迟受路径长度、负载(扇出)、温度等因素影响。
对于时序电路,最关键的是数据路径的延迟。数据路径是指从一个触发器的输出,经过组合逻辑,到达另一个触发器输入的路径。整个电路所能运行的最小时钟周期(最大时钟频率)受到最长的数据路径延迟的限制。这个延迟必须小于时钟周期减去触发器的建立时间等余量。
在FPGA设计中,综合和实现工具(如Quartus)会根据提供的时序约束文件(如SDC文件),尝试进行布局布线,在满足时钟频率要求的前提下,优化逻辑和走线。如果数据路径延迟过长,无法满足约束,设计就会失败或性能下降。这就是为什么需要将复杂操作(如单周期CPU)流水线化,将长路径拆分为多个时钟周期来完成的原因。

本节课中我们一起学习了时序逻辑的核心知识。我们明确了时序逻辑具有记忆功能,其输出取决于输入历史。我们深入探讨了在Verilog中实现记忆的两种方式及其对应的锁存器和触发器,并强调了避免意外锁存器的重要性。我们介绍了寄存器作为多位触发器的概念,并引入了强大的有限状态机设计方法,用于描述顺序性硬件行为。通过交通灯控制器和实际的AXI总线接口控制器例子,我们看到了状态机如何将复杂协议转换为可靠的硬件逻辑。最后,我们简要接触了时序分析的概念,理解了实际电路中延迟的来源以及时钟频率与路径延迟之间的关系,为后续深入学习性能优化打下了基础。
011:时序分析 🕒


在本节课中,我们将学习数字电路中的时序分析。我们将探讨延迟的概念、关键路径、时序约束以及如何确保电路在特定时钟频率下稳定工作。理解这些概念对于设计可靠、高性能的数字系统至关重要。
时序规范与延迟

上一节我们介绍了分支和跳转指令的实现。本节中,我们来看看数字电路中的基本时序概念。

任何数字电路都有延迟。延迟是指从输入变化到相应输出变化所需的时间。在真实硬件中,延迟由电阻和电容(RC延迟)引起。延迟通常测量为输入信号从0到1(或1到0)转换的中点,到输出信号相应转换的中点之间的时间。
需要注意的是,输出并不总是随输入变化而变化(例如,与门的一个输入为0时)。只有当输入变化导致输出变化时,才存在延迟。
传播延迟与污染延迟
对于单个逻辑门,实际上存在两个关键的延迟参数:传播延迟和污染延迟。
- 传播延迟 (TPD): 指输入变化后,输出最终稳定所需的最长时间。它定义了输出不再变化的时刻。
- 污染延迟 (TCD): 指输入变化后,输出可能开始变化的最早时间。因此,输出实际变化的时间是一个范围,从污染延迟到传播延迟。
这两个延迟不同的原因在于CMOS逻辑门的结构。CMOS门有负责将输出拉高的上拉晶体管和负责将输出拉低的下拉晶体管。通常,NMOS晶体管(下拉)的开关速度比PMOS晶体管(上拉)快,因此输出从1到0的转换可能比从0到1的转换更快,导致了延迟范围。
电路路径与关键路径
对于一个由多个门组成的电路,其总体延迟取决于信号经过的路径。
- 关键路径(长路径): 电路中输入到输出之间延迟最长的路径。计算整个电路的传播延迟时,需要将关键路径上所有门的传播延迟相加。
- 短路径: 电路中输入到输出之间延迟最短的路径。计算整个电路的污染延迟时,需要看短路径上所有门的污染延迟之和。
以下是一个示例电路的分析:
电路:Y = (A AND B) OR (C AND D)
假设:每个AND门的 Tpd = 2ns, Tcd = 1ns;OR门的 Tpd = 1.5ns, Tcd = 1ns。
- 关键路径: 例如信号A或B变化,需要经过一个AND门和OR门。
- 电路传播延迟 Tpd_circuit = Tpd_AND + Tpd_OR = 2ns + 1.5ns = 3.5ns
- 短路径: 例如信号D变化,只经过一个AND门(如果另一个AND门输出为0)。
- 电路污染延迟 Tcd_circuit = Tcd_AND = 1ns
因此,该电路的输出在输入变化后 1ns 到 3.5ns 之间可能发生变化,3.5ns 后才绝对稳定。


毛刺与动态约束

当电路中存在不平衡路径(即从同一输入源到汇合点的多条路径长度不同)时,可能会产生毛刺。毛刺是输出端一个短暂、非预期的脉冲。

毛刺产生原因: 输入变化通过不同路径以不同速度传播,导致在汇合点短暂出现非预期的逻辑组合。
如何解决:
- 逻辑冗余: 在卡诺图中添加冗余项(对应额外的与门),确保在状态转换时始终被一个乘积项覆盖,从而消除毛刺。
- 同步设计: 更实用的方法是利用动态约束。动态约束要求设计者确保:
- 在污染延迟之前,输出是稳定的旧值。
- 在传播延迟之后,输出是稳定的新值。
- 在污染延迟与传播延迟之间的“窗口期”,输出是无效的(可能包含毛刺),应被忽略。
通过使用时钟边沿触发寄存器在传播延迟之后采样稳定输出,可以有效地忽略毛刺,这是同步时序电路设计的核心思想。CAD工具(如FPGA编译工具)的“时序收敛”任务,就是为了确保电路中最长的路径延迟(关键路径延迟)小于时钟周期,从而满足动态约束。
总结
本节课中我们一起学习了数字系统时序分析的基础。
- 我们了解了传播延迟和污染延迟的定义与区别。
- 我们学习了如何识别电路的关键路径和短路径,并计算整体电路延迟。
- 我们探讨了由于路径不平衡导致的毛刺现象。
- 我们引入了动态约束的概念,它是确保同步电路可靠工作、避免毛刺影响的关键原则,也是时序分析的根本目标。

理解这些概念对于设计能在指定时钟频率(如我们项目中使用的50MHz)下稳定运行的CPU至关重要。在接下来的课程中,我们将更深入地探讨CMOS电路设计和时序优化的具体技术。
012:时序分析(第二部分)与数字构建模块

在本节课中,我们将继续学习时序分析,并开始探讨数字系统中的基本构建模块,特别是加法器。我们将了解如何确保电路满足时序约束,以及如何设计更高效的加法器。
时序分析回顾
上一节我们介绍了逻辑门延迟、建立时间和保持时间的概念。本节中,我们来看看如何将这些概念应用于包含寄存器的实际数据路径。
在CPU设计中,逻辑路径通常始于一个寄存器(或内存单元),并终于另一个寄存器。寄存器具有建立时间和保持时间的要求。这意味着,在时钟边沿到来之前和之后的一小段时间内,寄存器的输入必须保持稳定。这个时间段被称为孔径时间。
如果数据在孔径时间内发生变化,寄存器可能会进入亚稳态,其输出会在0和1之间振荡一段时间,然后才稳定到正确的值。为了避免亚稳态,我们必须遵守动态约束。
时序约束公式
为了满足动态约束,电路必须满足两个关键的不等式。
建立时间约束确保数据在下一个时钟边沿到来之前,有足够的时间通过逻辑传播并稳定下来。其公式为:
T_c >= t_pcq + t_pd + t_setup
其中:
T_c是时钟周期。t_pcq是寄存器从时钟到输出的传播延迟。t_pd是组合逻辑的传播延迟(最长路径)。t_setup是寄存器的建立时间要求。


这个公式可以重新排列,以确定逻辑路径允许的最大延迟:
t_pd <= T_c - t_setup - t_pcq
保持时间约束确保数据在时钟边沿之后不会变化得太快,从而不会干扰当前锁存的值。其公式为:
t_ccq + t_cd >= t_hold
其中:
t_ccq是寄存器从时钟到输出的污染延迟。t_cd是组合逻辑的污染延迟(最短路径)。t_hold是寄存器的保持时间要求。
这个公式可以重新排列,以确定逻辑路径所需的最小延迟:
t_cd >= t_hold - t_ccq
如果逻辑路径的延迟太短,违反了保持时间约束,通常可以通过插入缓冲器来增加延迟。
时钟偏移的影响
在实际芯片中,时钟信号到达不同寄存器的时间可能存在差异,这称为时钟偏移。时钟偏移会影响时序分析。
- 对于建立时间约束:最坏情况是源寄存器时钟较晚,而目的寄存器时钟较早。这减少了数据可用于传播的有效时间。因此,建立时间约束需加入最大正偏移
t_skew:T_c >= t_pcq + t_pd + t_setup + t_skew - 对于保持时间约束:最坏情况是源寄存器时钟较早,而目的寄存器时钟较晚。这可能导致数据过快到达目的寄存器。因此,保持时间约束需加入最大正偏移
t_skew:t_ccq + t_cd >= t_hold + t_skew
输入同步与亚稳态
对于来自外部(如按钮、网络数据)的异步输入,我们无法控制其变化时间,因此它们可能在寄存器的孔径时间内发生变化,导致亚稳态。
为了降低亚稳态传播到系统内部的风险,我们使用同步器。一个典型的同步器由两个级联的寄存器构成。第一个寄存器(同步级)可能进入亚稳态,但只要它在第二个寄存器的建立时间之前稳定下来,第二个寄存器的输出就是稳定的。
同步器失效的平均时间可以通过概率模型估算。公式涉及输入变化频率、时钟周期以及器件的技术参数(如时间常数 τ)。
数字构建模块:加法器
现在,让我们转向数字系统的基本构建模块。首先从加法器开始。
最基本的加法器是行波进位加法器。它由多个1位全加器串联而成,每个全加器的进位输出连接到下一个全加器的进位输入。
行波进位加法器的主要问题是速度慢。其关键路径从最低位的进位输入开始,穿过所有全加器,到达最高位的进位输出。对于n位加法器,总延迟大致为:
总延迟 ≈ n × 单个全加器的进位延迟
为了构建更快的加法器,我们使用超前进位加法器。其核心思想是提前计算出每一位的进位,而不是等待进位逐位传递。
这引入了两个信号:
- 生成:
G_i = A_i · B_i。如果本位的两个输入都为1,则无论进位输入如何,都会生成一个进位输出。 - 传播:
P_i = A_i + B_i。如果本位的两个输入至少有一个为1,则进位输入会被传播到进位输出。

利用这两个信号,第i位的进位输出 C_i 可以表示为:
C_i = G_i + P_i · C_{i-1}
通过递归展开这个公式,可以直接用原始的输入位 A、B 和初始进位 C_in 来表达每一位的进位,从而极大地提高了加法速度。


总结
本节课中我们一起学习了:
- 时序约束:深入理解了建立时间和保持时间约束的公式及其物理意义,这是保证数字系统可靠运行的基础。
- 时钟偏移:了解了时钟信号到达时间不一致对时序分析的影响,并学会了如何在约束公式中考虑时钟偏移。
- 亚稳态与同步器:认识了异步输入带来的亚稳态风险,以及如何使用同步器来降低该风险。
- 加法器设计:回顾了行波进位加法器的工作原理及其速度瓶颈,并引入了超前进位加法器的基本概念,通过生成和传播信号来并行计算进位,从而实现更快的加法运算。

这些知识是设计高性能、高可靠性数字系统的关键。在接下来的课程中,我们将继续探讨其他重要的数字构建模块和优化技术。
013:数字构建模块
在本节课中,我们将学习数字系统中的几种核心构建模块,包括加法器、减法器、比较器、移位器、乘法器和除法器。我们将重点分析它们的结构、工作原理和性能特点,特别是时序分析在其中的应用。
上一节我们介绍了时序分析的基本概念,本节中我们来看看如何将这些概念应用到具体的数字模块设计中。

时序分析测验回顾
本次测验包含五个关于时序分析的问题。
以下是第一个问题。
问题一:关键路径与时延计算
考虑以下电路和传播时延表,问题是计算电路的关键路径传播时延。


计算传播时延时,需要关注最坏情况下的路径。该电路基本没有扇出,所有门输出仅连接到一个输入,除了一个输出连接到同一门的两个输入。这使得分析相对简单,因为每个输入到输出基本只有一条路径。
由于所有门的时延不同,可能需要计算多条路径的时延。然而,输入D和E显然不在关键路径上,因为D跳过了A、B、C经过的一个门,E跳过了F、G经过的一个门。因此,实际上只需要计算从A/B/C到Y的路径和从F/G到Y的路径。
- A/B/C 到 Y 的路径:经过一个三输入与非门(30 ps)、一个两输入与非门(20 ps)、一个两输入或非门(30 ps)和一个两输入与非门(20 ps)。总时延为 30 + 20 + 30 + 20 = 100 ps。
- F/G 到 Y 的路径:经过一个两输入与门(30 ps)、一个两输入或非门(30 ps)、另一个两输入或非门(30 ps)和一个两输入与非门(20 ps)。总时延为 30 + 30 + 30 + 20 = 110 ps。
取两者中的最大值,电路的关键路径传播时延 T0 = 110 ps。
问题二:平均故障间隔时间计算
系统时钟频率为1 GHz,使用采样建立时间 τ = 100 ps 的触发器。T0 = 110 ps,T_setup = 70 ps。同步器平均每4秒(即0.25次/秒)接收一次新的异步输入。求平均故障间隔时间。
平均故障间隔时间的计算公式为:
MTBF = Tc / (N * T0 * e^(-(Tc - T_setup)/τ))

其中:
- Tc 是时钟周期(1 GHz对应 1 ns 或 1000 ps)。
- N 是每秒切换次数(0.25)。
- T0 是关键路径时延(110 ps)。
- T_setup 是建立时间(70 ps)。
- τ 是技术参数(100 ps)。
代入计算后,得到平均故障间隔时间约为 110小时。
问题三:时钟偏移容限计算
考虑给定电路和时序参数,假设时钟频率为4 GHz,计算电路能容忍的时钟偏移。
时钟偏移会影响建立时间和保持时间约束。
-
建立时间约束:最大路径时延(T_PCQ + 逻辑时延 + T_setup + 时钟偏移)必须小于等于时钟周期 Tc。
- Tc = 1 / 4 GHz = 250 ps。
- T_PCQ = 30 ps。
- 逻辑时延(经过3个门):3 * 15 ps = 45 ps。
- T_setup = 10 ps。
- 设时钟偏移为 S。
- 不等式:30 + 45 + 10 + S ≤ 250 => S ≤ 165 ps。
-
保持时间约束:最小路径时延(T_CCQ + 逻辑污染时延 - 时钟偏移)必须大于等于保持时间 T_hold。
- T_CCQ = 20 ps。
- 逻辑污染时延(最短路径经过1个门):10 ps。
- T_hold = 30 ps。
- 不等式:20 + 10 - S ≥ 30 => S ≤ 0 ps。
对于保持时间,电路无法容忍任何正时钟偏移。因此,电路能容忍的时钟偏移为 0 ps(由保持时间约束决定)。
问题四:多比特加法器时序分析
设计一个由两个全加器构成的2位加法器,输入输出均有寄存器,在一个时钟周期内完成加法。计算最大时钟频率时,需同时考虑建立时间和保持时间约束,方法同问题三。
问题五:保持时间约束验证
考虑给定电路和时序参数,时钟频率1 GHz。验证电路是否满足保持时间约束。
保持时间约束与时钟周期无关。需要检查最小路径时延(T_CCQ + 逻辑污染时延 - 时钟偏移)是否大于等于 T_hold。
- T_CCQ = 5 ps。
- 逻辑污染时延(经过一个与门和一个或门):25 ps + 30 ps = 55 ps。
- 给定时钟偏移 S = 10 ps。
- T_hold = 50 ps。
- 计算:5 + 55 - 10 = 50 ps。该值等于(而非大于)T_hold。
由于约束要求 严格大于(>),而计算结果等于50 ps,因此该电路 不满足 保持时间约束。
加法器设计
上一节我们回顾了时序分析,本节中我们来看看几种重要的加法器设计。
行波进位加法器
行波进位加法器将多个全加器串联,每个全加器负责一个比特位的加法,其进位输出连接到下一个全加器的进位输入。
主要问题是速度慢,总传播时延与位数成正比:
T_ripple ≈ N * T_FA
其中 T_FA 是一个全加器的时延。
超前进位加法器
为了打破进位传递的长链,超前进位加法器为每个全加器生成“传播”和“生成”信号:
- P_i = A_i XOR B_i (如果A_i, B_i能传播进位)
- G_i = A_i AND B_i (如果A_i, B_i能生成进位)
则进位 C_i 可以表示为:
C_i = G_{i-1} OR (P_{i-1} AND C_{i-1})
通过递归展开,可以用低位的所有 P 和 G 信号直接计算高位的进位,而无需等待进位逐级传递。
但直接为所有位实现完全的超前进位逻辑会导致门电路输入过多,不具可扩展性。因此,通常采用分层结构:先计算小组(如4位一组)内的超前进位,再在组间使用类似的逻辑传递进位。
其时延公式大致为:
T_CLA ≈ T_PG + (N/K - 1) * T_AND-OR + T_FA_ripple
其中 K 是小组大小。时延随位数 N 线性增长,但系数比行波进位小。
前缀加法器
前缀加法器是速度最快的加法器结构之一。它也使用传播和生成信号,但以不同的方式组织计算。
核心思想是:通过合并相邻的、更小的位组(P,G)信号,来计算更大位组的(P,G)信号。计算过程组织成树状结构(前缀树),每合并一次增加约2级门时延。
总时延与位数的对数成正比:
T_prefix ≈ T_PG + log₂(N) * T_merge + T_XOR
对于大规模加法器,前缀加法器性能最优。
以下是三种32位加法器的性能对比示例(假设条件):
- 行波进位加法器:9.6 ns
- 超前进位加法器(4位块):3.4 ns
- 前缀加法器:1.2 ns
其他数字构建模块
上一节我们深入探讨了各种加法器,本节中我们来看看减法器、比较器、移位器、乘法器和除法器。
减法器
减法器可以通过加法器实现。对减数 B 取二进制补码(按位取反后加1),然后与 A 相加即可。
A - B = A + (~B + 1)
巧妙之处在于,可以将“加1”操作通过加法器的进位输入 Cin 设置为1来实现,无需额外电路。
比较器
- 相等比较:对两个数的每一位进行异或操作,然后将所有结果进行与操作。若所有位都相等,则输出为1。
- Equality = (A[0] XNOR B[0]) AND (A[1] XNOR B[1]) AND ...
- 小于比较:可以通过减法实现。计算 A - B,如果结果为负(符号位为1),则 A < B。需要注意有符号数的情况。
移位器
- 桶形移位器:可以单周期内移动任意位数。其核心是一个多路选择器阵列,每个输出位可以从任何一个输入位中选择。一个 N 位桶形移位器需要 N 个 N:1 多路选择器,硬件开销较大。
- 移位寄存器:由一系列触发器串联而成,每个时钟周期将数据移动一位。可以实现串行到并行的转换。若加入并行加载功能,则可初始化为任意值后再进行移位。
移位操作也可用于乘除法:
- 左移1位等价于乘以2。
- 右移1位等价于除以2(向下取整)。
但这仅限于2的幂次。
乘法器
二进制乘法比十进制简单。基于“移位相加”原理:
将被乘数根据乘数的每一位进行移位(如果该位为1),然后将所有移位后的结果相加。
以下是实现方式之一:
使用与门阵列生成部分积(被乘数与乘数每一位相与),然后用加法器阵列(如Wallace树)将所有部分积相加。乘法器硬件规模较大,但它是组合逻辑,不涉及条件判断。
除法器
除法是最复杂的运算之一,通常基于“恢复余数法”:
- 将除数与当前部分被除数(初始为被除数的高位)对齐。
- 尝试相减。
- 如果结果为负,说明减多了,恢复原来的部分被除数,并在商中对应位置0。
- 如果结果非负,保留相减后的结果作为新的余数,并在商中对应位置1。
- 从被除数中移入下一位,重复步骤2-4。
由于需要尝试相减并根据结果决定是否恢复,除法器包含反馈和决策逻辑,硬件实现复杂且速度慢。因此,许多早期处理器没有硬件除法单元。
总结

本节课中我们一起学习了数字系统的核心构建模块。我们从时序分析测验入手,复习了关键路径、MTBF、时钟偏移容限和保持时间验证。然后,我们深入探讨了行波进位、超前进位和前缀三种加法器,分析了它们的结构、工作原理和性能优劣。最后,我们介绍了减法器、比较器、移位器、乘法器和除法器的基本实现原理,了解了乘法器通过“移位相加”实现,而除法器则基于复杂的“恢复余数”过程,这解释了为什么除法在硬件中更为昂贵和少见。掌握这些模块的设计与特性对于构建高效、可靠的数字系统至关重要。
014:CMOS设计基础


在本节课中,我们将学习数字逻辑电路在晶体管层面的实现,即CMOS逻辑设计。我们将从回顾上一讲的关键数字模块开始,然后深入探讨CMOS技术的基本原理、逻辑电平、硅半导体特性以及如何用晶体管构建基本逻辑门。
回顾:数字构建模块
上一讲我们介绍了多种数字构建模块。本节中,我们快速回顾一下核心概念。
加法器
我们讨论了行波进位加法器,以及更快的超前进位加法器。超前进位加法器的核心思想是提前计算进位,其关键路径延迟与加法器的位数成对数关系,而非线性关系。
以下是超前进位加法器中用于计算进位生成(G)和传播(P)信号的基本公式:
- 生成信号:
G_i = A_i & B_i - 传播信号:
P_i = A_i ^ B_i - 进位信号:
C_{i+1} = G_i | (P_i & C_i)
前缀加法器进一步优化了这一过程,通过树状结构组合不同范围的生成和传播信号,将延迟降低到 O(log n)。
比较器与移位器
比较器可以通过逐位异或非(XNOR)后接一个多输入与门来实现,但这会带来较大的扇入延迟。另一种常见方法是使用减法并检查结果是否为零。


移位器分为两种主要类型:
- 桶形移位器:使用多路选择器(MUX)实现任意位数的移位,硬件开销较大。
- 移位寄存器:由一系列触发器串联而成,每次只能移位一位,常用于串行/并行转换。

乘法器与除法器
乘法器通过生成部分积并相加来实现。一个N位乘法器大致需要N-1个加法器。
除法器(如恢复除法器)是计算中最昂贵的操作之一,因为它需要一系列的减法、比较和可能的恢复操作。一个N位组合除法器需要N个减法器及相关逻辑。
晶体管级逻辑设计
现在,我们进入一个新的主题:晶体管级逻辑,即CMOS逻辑设计。这部分知识对于理解如何将HDL代码映射到实际的硅芯片上至关重要。
逻辑电平与噪声容限
在理想情况下,数字逻辑电平是明确的:低于 VDD/2 为逻辑0,高于 VDD/2 为逻辑1。然而,现实中的逻辑门具有非理想的电压传输特性。
关键参数定义如下:
V_IL:输入低电平的最大电压。只要输入低于此值,就被可靠地识别为逻辑0。V_IH:输入高电平的最小电压。只要输入高于此值,就被可靠地识别为逻辑1。V_OL:输出低电平的最大电压。当门输出逻辑0时,电压不高于此值。V_OH:输出高电平的最小电压。当门输出逻辑1时,电压不低于此值。
噪声容限衡量电路抗干扰的能力:
- 低电平噪声容限:
NM_L = V_IL - V_OL - 高电平噪声容限:
NM_H = V_OH - V_IH
这些参数确保了即使输入信号带有一定噪声,经过逻辑门后输出信号也能被“净化”,从而保证数字系统的稳定运行。
硅半导体基础
硅是制造芯片的基础材料,它是一种半导体。纯硅的导电性很差。通过掺杂可以改变其导电特性:
- P型硅:掺入三价元素(如硼)。多数载流子是带正电的“空穴”。
- N型硅:掺入五价元素(如砷)。多数载流子是带负电的电子。
将P型硅和N型硅结合,就形成了PN结二极管。二极管具有单向导电性。
MOS晶体管工作原理
MOS晶体管是构建CMOS逻辑门的基本开关。主要有两种类型:
- NMOS晶体管:当栅极(Gate)施加高电压(逻辑1)时,在P型衬底中吸引电子形成导电沟道,从而在源极(Source)和漏极(Drain)之间导通。
- PMOS晶体管:当栅极施加低电压(逻辑0)时,在N型衬底(或N阱)中吸引空穴形成导电沟道,从而导通。
晶体管的符号中,PMOS的栅极通常带有一个小圆圈,表示低电平有效(导通)。
CMOS逻辑门结构
CMOS逻辑门的核心设计原则是使用互补的上拉网络和下拉网络:
- 上拉网络:由PMOS晶体管组成,负责在输出应为逻辑1时,将输出节点连接到电源
VDD。 - 下拉网络:由NMOS晶体管组成,负责在输出应为逻辑0时,将输出节点连接到地
GND。
这两个网络是互斥的,对于任何输入组合,只有一个网络导通,从而避免电源和地之间的直接短路(称为“穿通”电流)。
以下是构建基本CMOS门的规则:
反相器(NOT Gate)
这是最简单的CMOS门。
- 上拉网络:一个PMOS晶体管。输入为0时导通。
- 下拉网络:一个NMOS晶体管。输入为1时导通。
- 功能:
Y = NOT A
与非门(NAND Gate)
一个两输入与非门的结构如下:
- 上拉网络:两个PMOS晶体管并联。只要A或B中有一个为0,就能将输出上拉到1。
- 下拉网络:两个NMOS晶体管串联。只有A和B同时为1,才能将输出下拉到0。
- 功能:
Y = NOT (A AND B)
或非门(NOR Gate)
一个两输入或非门的结构如下:
- 上拉网络:两个PMOS晶体管串联。只有A和B同时为0,才能将输出上拉到1。
- 下拉网络:两个NMOS晶体管并联。只要A或B中有一个为1,就能将输出下拉到0。
- 功能:
Y = NOT (A OR B)
一个重要限制:由于PMOS用低电平(0)导通,NMOS用高电平(1)导通,单个CMOS门只能直接实现“负逻辑”功能(如NAND、NOR)。要实现标准的与门(AND)或或门(OR),需要在NAND或NOR门后面级联一个反相器。
总结

本节课中我们一起学习了CMOS数字设计的基础知识。我们首先回顾了加法器、比较器、移位器等关键数字模块。然后,我们深入探讨了晶体管级设计的核心:包括逻辑电平与噪声容限的概念、硅半导体的掺杂原理、NMOS和PMOS晶体管的工作机制。最后,我们掌握了CMOS逻辑门的基本结构,理解了上拉网络(PMOS)和下拉网络(NMOS)互补工作的原理,并学会了如何构建反相器、与非门和或非门。这些知识是理解现代数字集成电路如何从门级描述转化为物理芯片的基石。
015:CMOS设计(二)🔌

在本节课中,我们将继续学习晶体管级逻辑,即CMOS逻辑设计。我们将深入探讨CMOS的工作原理、功耗计算以及晶体管在不同工作区的行为模型。
逻辑电平与噪声容限回顾
上一节我们介绍了逻辑电平和噪声容限。逻辑电平是用于表示0和1的电压值,这些电压值随着技术进步而不断降低。
逻辑门具有“净化”输入噪声的特性。逻辑门将0到VIL之间的任何输入电压视为逻辑0,而将0到VOL之间的任何输出电压视为逻辑0。由于VIL通常高于VOL,因此输入端的噪声在输出端会被衰减,使其更接近理想的逻辑电平。
高电平侧也有类似情况,即VIH和VOH。由此产生的噪声容限是输出低电平与输入高电平(或输出高电平与输入低电平)之间的差值。只要噪声小于这个容限,电路就能正常工作。
半导体与掺杂
现在,我们来看看半导体基础。硅形成晶体晶格,可以通过掺杂杂质来改变其性质。
- N型硅:掺入砷(第5族元素)等施主杂质。砷多出一个电子,这个自由电子成为电荷载流子,使硅的导电性增强。
- P型硅:掺入硼(第3族元素)等受主杂质。硼缺少一个电子,形成一个可移动的“空穴”,它也能作为载流子传导电流,增强导电性。
将P型硅和N型硅结合在一起,就形成了PN结二极管。当P端(阳极)电压高于N端(阴极)时,载流子被吸引至结区,电流从阳极流向阴极(正向偏置)。反之,则形成反向偏置,二极管不导电,但会形成一个电容。
CMOS晶体管结构与制造
CMOS晶体管制造始于P型衬底。通过光刻和离子注入工艺,在衬底上形成N型扩散区(有源区)。
以下是制造关键步骤:
- 从P型衬底开始。
- 使用掩膜覆盖不需要改变的区域。
- 通过注入砷蒸气等N型杂质,在未掩膜区域形成N型扩散区,从而“反转”该区域的掺杂类型。
- 在扩散区之间制作栅极,栅极与衬底之间由二氧化硅绝缘层隔开。
当栅极施加正电压(相对于衬底)时,会在栅极下方的P型衬底中吸引少数载流子(电子),形成导电沟道,从而连接两个N型扩散区,使晶体管导通,成为一个电子开关。
CMOS逻辑门设计
晶体管作为开关,可以通过串联和并联组合来构建逻辑门,形成在特定输入组合下导通或关断的路径。
- 串联:两个NMOS晶体管串联,两者的栅极都必须为高电平,整个网络才会导通。
- 并联:两个NMOS晶体管并联,任一栅极为高电平,网络即可导通。
以与非门(NAND)为例:
- 上拉网络:两个PMOS晶体管并联,连接在输出端和VDD之间。
- 下拉网络:两个NMOS晶体管串联,连接在输出端和地之间。
只有当两个输入均为高电平时,下拉网络的两个NMOS才全部导通,将输出拉低至地(逻辑0)。对于其他任何输入组合,至少有一个PMOS导通,将输出拉高至VDD(逻辑1)。上拉和下拉网络是互斥的,稳态下输出不会同时连接到VDD和地。
动态功耗主要发生在输入切换的瞬间,此时上拉和下拉网络可能短暂同时导通,产生从电源到地的电流尖峰。静态功耗则是指稳态下的泄漏电流,现代芯片因工作电压接近阈值电压,泄漏问题更为突出。
传输门与功耗计算
由于PMOS传递低电平、NMOS传递高电平的效果不佳,传输门将两者并联使用,以确保无论传递高电平还是低电平,都有良好的导通性。
功耗计算至关重要,主要包括两部分:
-
动态功耗:由电路切换时对电容充放电引起。
- 公式:
P_dynamic = (1/2) * C * VDD² * f - 其中,
C为总负载电容,VDD为电源电压,f为开关频率。功耗与电压的平方成正比,因此降低电压是减少动态功耗的有效方法。
- 公式:
-
静态功耗:由晶体管关断时的泄漏电流引起。
- 公式:
P_static = I_leakage * VDD - 其中,
I_leakage为总泄漏电流。
- 公式:
总功耗为两者之和:P_total = P_dynamic + P_static。
晶体管工作区与电流模型
理解晶体管行为需要分析其三个工作区:
- 截止区:栅源电压
VGS低于阈值电压VT,无导电沟道,漏源电流IDS几乎为零。 - 线性区(三极管区):
VGS > VT且VDS较小。晶体管像可变电阻,IDS随VDS线性增加。- 电流公式:
IDS = β * [(VGS - VT) * VDS - (1/2) * VDS²](近似)
- 电流公式:
- 饱和区:
VGS > VT且VDS增大到一定程度(VDS > VGS - VT)。沟道在漏端出现夹断,IDS基本保持恒定,不再随VDS增加,晶体管如同恒流源。- 电流公式:
IDS = (1/2) * β * (VGS - VT)²
- 电流公式:
其中,β 是工艺跨导参数,β = μ * Cox * (W/L)。μ 为载流子迁移率,Cox 为单位面积栅氧电容,W 和 L 分别为沟道宽度和长度。
由于空穴迁移率低于电子迁移率,PMOS的 μ 约为NMOS的一半。为了平衡上升和下降时间,通常将PMOS的沟道宽度 W 设计为NMOS的2倍。
总结

本节课我们一起深入学习了CMOS设计的核心内容。我们回顾了噪声容限的概念,探讨了半导体掺杂与晶体管制造原理。我们掌握了如何使用NMOS和PMOS晶体管构建基本逻辑门(如与非门),并理解了上拉和下拉网络的工作原理。此外,我们还学习了传输门的用途,以及如何计算CMOS电路的动态功耗和静态功耗。最后,我们分析了MOSFET的三个工作区(截止、线性、饱和)及其电流-电压特性模型,并了解了为何PMOS晶体管通常需要更宽的沟道宽度。在接下来的课程中,我们将利用这些知识来估算电路的延迟。
016:CMOS设计(三)🚀



在本节课中,我们将深入学习CMOS逻辑设计的核心概念,包括噪声容限、晶体管工作原理、RC延迟模型、复杂门电路的延迟计算以及存储器阵列的基础知识。我们将通过公式和代码来精确描述关键概念,确保内容简单易懂。



噪声容限回顾 📊
上一节我们介绍了CMOS逻辑的基本原理。本节中,我们来看看噪声容限的概念。
噪声容限是指电路在输出信号已受噪声影响的情况下,其输入信号所能容忍的额外噪声幅度,以避免输入电压进入“禁止区”。


- 输入逻辑低电平 (VIL):低于或等于此电压的输入被视为逻辑0。
- 输入逻辑高电平 (VIH):高于或等于此电压的输入被视为逻辑1。
- 输出逻辑低电平 (VOL):逻辑门输出为逻辑0时的电压。
- 输出逻辑高电平 (VOH):逻辑门输出为逻辑1时的电压。
禁止区是VIL和VIH之间的电压范围。由于逻辑门的工作原理,这个范围通常很窄。噪声容限的计算公式如下:





- 低电平噪声容限 (NML) =
VIL - VOL - 高电平噪声容限 (NMH) =
VOH - VIH
为了使电路可靠工作,导线上的噪声必须低于这两个值。


晶体管结构与工作区域 🔬



理解了外部电气特性后,我们需要深入晶体管内部。MOS晶体管(包括NMOS和PMOS)由栅极、沟道、源极和漏极构成,还有一个被称为“体”或“衬底”的第四端。



当在栅极施加电压时,会在沟道中感应出少数载流子,形成反型层,从而将源极和漏极连接起来。晶体管有三种工作区域:
- 截止区:栅源电压
VGS小于阈值电压VT。晶体管关闭,源漏电流IDS近似为0。公式为:IDS ≈ 0 (当 VGS < VT) - 线性区(欧姆区):
VGS > VT且漏源电压VDS较小。晶体管导通,IDS随VDS近似线性增加。公式为:IDS = β * [(VGS - VT) * VDS - (1/2) * VDS²]- 其中
β = (μ * Cox * W) / L,μ是载流子迁移率,Cox是单位面积栅氧电容,W和L是沟道的宽度和长度。
- 其中
- 饱和区:
VGS > VT且VDS较大(VDS > VGS - VT)。沟道在漏极附近被“夹断”,IDS基本保持恒定,不再随VDS增加。公式为:IDS = (1/2) * β * (VGS - VT)²

对于PMOS晶体管,上述公式中的电压均为负值。


晶体管的驱动能力(电流大小)与宽长比 W/L 成正比。增加宽度 W 可以获得更大的电流,从而更快地对负载电容进行充放电,但也会增加晶体管自身的寄生电容。

CMOS逻辑门与RC延迟模型 ⏱️


CMOS逻辑门由上拉网络(PMOS)和下拉网络(NMOS)组成,两者在功能上互补。例如,对于与非门(NAND),上拉网络是PMOS并联,下拉网络是NMOS串联。
为了估算逻辑门的延迟,我们使用简化的RC模型。在这个模型中:
- 晶体管被等效为一个电阻,其阻值
R_eq = R / K,其中R是最小尺寸晶体管的等效电阻,K是晶体管宽度相对于最小宽度的倍数。 - 晶体管的寄生电容(栅电容、源/漏扩散区电容)被建模为集总电容
C。通常,最小尺寸NMOS的栅电容记为C,而由于载流子迁移率差异,最小尺寸PMOS的宽度通常是NMOS的两倍,因此其栅电容为2C。
以下是一个反相器驱动另一个相同反相器的RC模型简化步骤:
- 对于驱动门(第一个反相器),我们考虑其输出节点
Y所连接的所有电容:- 其自身NMOS的漏极扩散电容(到地):
C - 其自身PMOS的漏极扩散电容(到VDD):
2C - 负载门(第二个反相器)的输入栅电容:NMOS栅电容
C+ PMOS栅电容2C=3C
- 其自身NMOS的漏极扩散电容(到地):
- 总负载电容
C_total = C + 2C + 3C = 6C - 驱动电阻取上拉和下拉电阻的典型值(例如,对于最小尺寸,下拉NMOS电阻为
R,上拉PMOS电阻为2R)。考虑最坏情况(如下拉),延迟τ ≈ R * 6C。




这个 RC 乘积就是时间常数,单位是秒(法拉 * 欧姆)。
复杂门延迟与Elmore延迟计算 🧮
对于像多输入与非门这样具有串联晶体管结构的复杂门,延迟计算需要使用Elmore延迟模型。因为信号路径上有多个电阻和电容分支。
Elmore延迟的计算方法是:从信号源到每个电容节点的路径上,将所有电阻求和,再乘以该节点的电容,最后对所有节点的这类乘积求和。
以三输入与非门(NMOS串联,PMOS并联)的下拉延迟(输出从高到低变化)为例:
- 假设三个串联的NMOS尺寸均为
K=3(以获得较低电阻),每个电阻为R/3,每个源/漏电容为3C。 - 从地(源)开始,向上到输出节点
Y,路径上有三个电阻。 - 计算到达每个电容节点的路径电阻和:
- 最下方NMOS的源电容(3C):路径电阻 =
R/3 - 中间NMOS的源电容(3C):路径电阻 =
R/3 + R/3 - 最上方NMOS的漏电容及负载电容(9C + 5H*C,H是扇出数):路径电阻 =
R/3 + R/3 + R/3
- 最下方NMOS的源电容(3C):路径电阻 =
- 总下拉延迟
τ_fall ≈ (R/3)*(3C) + (2R/3)*(3C) + (R)*(9C + 5H*C) = (12 + 5H) * RC
相比之下,上拉延迟(只要一个PMOS导通)为 τ_rise ≈ (9 + 5H) * RC。这解释了为什么传播延迟和污染延迟可能不同,并且上升和下降时间也可能不同。

存储器阵列基础 🧠
最后,我们来看看如何用CMOS技术构建存储器。存储器通常组织成二维阵列,包含深度(字数)和宽度(字长)。
- 字线:水平走向,用于选择要访问的行(字)。
- 位线:垂直走向,用于读取或写入数据位。
主要存储器类型:
- DRAM(动态随机存取存储器):
- 每个存储单元由一个晶体管和一个电容构成(1T1C)。
- 密度高、成本低,常用于主内存。
- 电容会漏电,需要定期刷新。
- 与逻辑芯片工艺不同,通常位于独立的芯片上。
- SRAM(静态随机存取存储器):
- 每个存储单元由6个晶体管构成(6T),通常是两个交叉耦合的反相器形成锁存器,外加两个访问晶体管。
- 速度快,无需刷新,但密度较低,面积大。
- 与逻辑电路工艺兼容,常用于CPU片上缓存。
- Flash存储器(闪存):
- 非易失性存储器,断电后数据不丢失。
- 利用浮栅晶体管存储电荷,读写机制不同。

“内存墙”是指CPU与DRAM主存之间的速度差距和通信瓶颈,因为两者位于不同芯片,通过有限带宽的通道连接。


总结 📝
本节课我们一起深入学习了CMOS设计的多个高级主题。我们从噪声容限的定义出发,回顾了保证数字电路可靠性的电气条件。接着,我们深入晶体管内部,分析了其结构、工作区域(截止、线性、饱和)以及描述其行为的IV特性公式。
为了进行电路延迟估算,我们引入了简化的RC模型,将晶体管等效为电阻和电容。我们详细分析了反相器的延迟计算,并扩展到具有串联晶体管结构的复杂门(如与非门),引入了Elmore延迟模型来精确计算分支RC网络的延迟。
最后,我们探讨了存储器阵列的基本原理,比较了DRAM和SRAM在单元结构、密度、速度和工艺集成度上的关键区别,并简要提及了“内存墙”这一系统级挑战。

掌握这些概念对于深入理解数字系统的性能、功耗和物理实现至关重要。
017:期末考试复习


在本节课中,我们将对课程的核心内容进行一次全面的复习,涵盖RISC-V ISA、SystemVerilog、流水线微架构、时序分析、数字逻辑构建模块等主题。我们将通过分析示例问题来巩固理解,确保大家为期末考试做好准备。
RISC-V ISA与汇编
上一节我们介绍了课程的整体结构,本节中我们来看看RISC-V指令集架构相关的复习要点。
考虑以下指令序列及其对应的内存地址(假设代码在字节寻址的指令内存中运行,地址以4递增)。


地址(十六进制) 指令
400000 li x1, 5
400004 label1: addi x1, x1, -1
400008 bge x1, x0, label1
40000C nop
400010 j label2
400014 label2: jr x0
以下是关于伪指令和程序执行的分析:
- 地址400000处的指令是伪指令吗? 是的。
li(立即数加载)是伪指令。它会被翻译为addi x1, x0, 5,因为寄存器x0的值恒为0。 - 地址400010处的指令是伪指令吗? 是的。
j(跳转)是伪指令。在RISC-V中,它被翻译为jal x0, label2,即将链接寄存器设置为x0(不保存返回地址)的跳转并链接指令。 - 地址400014处的指令是伪指令吗? 不是。
jr(跳转至寄存器)是真实指令jalr x0, x0, 0的别名。 - 地址400008处的分支指令会被执行多少次? 6次。循环体(
addi)执行5次(x1从5递减到1)。第6次执行bge时,x1为0,条件x1 >= x0成立,分支再次被采取。第7次时x1变为-1,条件不成立,分支不被采取,循环结束。 - 假设分支/跳转导致一个周期的停顿,这段代码的取指序列是什么? 序列为:0, 4, 8, C, 4, 8, C, 4, 8, C, 4, 8, C, 4, 8, C, 10, 14, 18。每次分支被采取后,下一条指令(地址C)被取出但作为停顿无效化。最后一次分支不采取,顺序执行到
j指令,j之后同样有一个停顿周期。
SystemVerilog 硬件描述语言
上一节我们回顾了指令集,本节中我们来看看如何用SystemVerilog描述硬件。
问题:找出以下模块中的问题。
module example(input logic [2:0] a, b, output logic [3:0] c);
always_comb begin
assign c = a + b; // 错误:不能在always块内使用assign
end
endmodule
解答:
assign语句不应在always_comb过程块内使用。- 位宽不匹配:
a和b是3位,相加结果最多4位,与4位的c匹配。但若此assign在块外,则正确。原代码主要错误是assign在always_comb内。
问题:当输入 a = 4‘b1011 时,以下代码将信号b设置为何值?
module scramble(input logic [3:0] a, output logic [3:0] b);
assign b = {a[0], a[1], a[2], a[3]};
endmodule
解答: b 的值为 4‘b1100。代码将a的位顺序反序。
问题:编写一个名为 parity 的模块,接收三个1位输入 a, b, c,产生一个1位输出 y,当奇数个输入为真时 y 为真。
module parity(input logic a, b, c, output logic y);
assign y = a ^ b ^ c; // 异或操作
endmodule
问题:将以下Verilog模块的行为转换为真值表。
module mystery(input logic [2:0] a, output logic [1:0] y);
if (a[2])
y = 2‘b11;
else if (a[1])
y = 2‘b10;
else
y = 2‘b00;
endmodule
解答: 这是一个优先级编码逻辑。
| a[2:0] | y[1:0] | 说明 |
|---|---|---|
| 000 | 00 | a[2]和a[1]均为0,执行else |
| 001 | 00 | a[2]为0,a[1]为0,执行else |
| 010 | 10 | a[2]为0,a[1]为1,执行else if |
| 011 | 10 | a[2]为0,a[1]为1,执行else if |
| 100 | 11 | a[2]为1,执行if |
| 101 | 11 | a[2]为1,执行if |
| 110 | 11 | a[2]为1,执行if |
| 111 | 11 | a[2]为1,执行if |
问题:阻塞赋值与非阻塞赋值的区别是什么?
- 阻塞赋值 (
=):在always块中按顺序立即执行,赋值语句完成后才执行下一条语句。常用于组合逻辑建模。 - 非阻塞赋值 (
<=):在always块中所有语句同时计算,在时间步结束时才统一更新赋值。用于时序逻辑建模,特别是寄存器。
问题:以下代码在输入in和输出D之间引入了多少个时钟周期的延迟?
always_ff @(posedge clk) begin
A = in; // 阻塞赋值
B <= A; // 非阻塞赋值
C <= B; // 非阻塞赋值
D = C; // 阻塞赋值
end
解答: 2个周期延迟。A = in立即更新,B <= A在周期结束时捕获当前的A(即in)。下一个周期,C <= B捕获上一个周期的B(即in),D = C立即捕获当前的C(即上一周期的in)。因此,in的变化在两个周期后出现在D。
流水线微架构
上一节我们讨论了SystemVerilog编码,本节中我们聚焦于CPU流水线的具体实现。
问题:为三阶段流水线编写SystemVerilog代码片段以实现符号扩展块。
// 假设 inst_EX 是来自指令解码的32位指令,imm_src 控制扩展类型
// 输出 sign_extended_imm
logic [31:0] sign_extended_imm;
always_comb begin
case (imm_src)
`I_TYPE: sign_extended_imm = {{20{inst_EX[31]}}, inst_EX[31:20]};
`S_TYPE: sign_extended_imm = {{20{inst_EX[31]}}, inst_EX[31:25], inst_EX[11:7]};
`B_TYPE: sign_extended_imm = {{20{inst_EX[31]}}, inst_EX[7], inst_EX[30:25], inst_EX[11:8], 1‘b0};
`U_TYPE: sign_extended_imm = {inst_EX[31:12], 12‘b0};
`J_TYPE: sign_extended_imm = {{12{inst_EX[31]}}, inst_EX[19:12], inst_EX[20], inst_EX[30:21], 1‘b0};
default: sign_extended_imm = 32‘b0;
endcase
end
问题:在什么条件下会产生流水线停顿(Stall)信号?为什么需要停顿?
- 条件:在检测到分支(Branch)或跳转(Jump)指令时,且其目标地址在指令解码(ID)阶段才能确定。
- 原因:当CPU取回一条分支/跳转指令时,它已经按顺序取回了下一条指令(PC+4)。一旦在ID阶段确定分支被采取或必须跳转,那条已取出的指令(PC+4)就是无效的。停顿信号的作用就是阻止这条无效指令进入执行阶段,并清空其产生的效果,同时将正确的目标地址加载到程序计数器(PC)中。
问题:编写两条能触发寄存器文件旁路(Bypass)的RISC-V指令序列。
addi x2, x1, 10 # 指令1:将结果写入 x2
add x3, x2, x4 # 指令2:需要读取 x2 的值
当指令1处于回写(WB)阶段,指令2处于执行(EX)阶段时,指令2需要指令1刚刚计算出来但尚未写回寄存器文件的结果。此时,数据旁路逻辑将指令1在EX阶段的结果直接转发给指令2的EX阶段作为输入,避免因等待写回而产生的数据冒险。
时序分析
上一节我们探讨了流水线冒险,本节中我们进行关键的时序分析。
问题:考虑以下电路和延迟参数,该电路能在2GHz(时钟周期=500ps)下工作吗?
T_{pcq} = 50ps(触发器传播延迟)T_{setup} = 100ps(触发器建立时间)T_{hold} = 20ps(触发器保持时间)T_{skew} = 10ps(时钟偏移)- 与门延迟:
T_{and} = 80ps - 或门延迟:
T_{or} = 30ps
关键路径(决定最小时钟周期):
T_{clk} >= T_{pcq} + T_{or} + T_{and} + T_{setup} + T_{skew}
T_{clk} >= 50 + 30 + 80 + 100 + 10 = 270ps
270ps < 500ps,满足建立时间要求。
最短路径(决定是否违反保持时间):
T_{ccq} + T_{and} >= T_{hold} + T_{skew} (假设T_{ccq}为触发器污染延迟,约5-10ps)
取 T_{ccq}=5ps, T_{and}=25ps (污染延迟): 5 + 25 = 30ps
T_{hold} + T_{skew} = 20 + 10 = 30ps
30ps >= 30ps,刚好满足保持时间要求。因此,该电路可以在2GHz下工作,但保持时间裕度非常小。
数字逻辑构建模块
上一节我们完成了时序分析,本节我们复习一些重要的数字逻辑模块。
问题:给出一个32位比较器的设计,当A > B时输出1,否则输出0。
最直接的方法是使用减法器并检查结果的符号位和零标志。(A > B) 等价于 (A - B) 为正且非零。对于有符号数,需要同时考虑符号位SF和零标志ZF:A > B = ~(SF ^ OF) & ~ZF,其中OF是溢出标志。一个简单的实现是:
logic [31:0] diff;
logic gt;
assign diff = A - B;
assign gt = ~diff[31] & (|diff); // 最高位为0(正)且差值不为零
问题:对于32位输入A,执行算术右移4位需要多少级门延迟?
如果移位量是常数(如固定右移4位),这只是一个连线的重新排列,不需要任何逻辑门,延迟为0。硬件上只是将位A[31:4]输出到结果的[27:0],并将A[31]复制到结果的高4位[31:28](符号扩展)。
如果移位量是变量,则需要一个桶形移位器,其延迟与数据位宽和选择信号有关。
总结

本节课中我们一起学习了期末考试的核心复习内容。我们回顾了RISC-V伪指令与程序分析、SystemVerilog语法与常见错误、流水线微架构的数据通路与控制信号、时序分析中建立时间和保持时间的计算,以及加法器、比较器、移位器等数字构建模块。请务必同时复习课堂上关于CMOS晶体管级逻辑的内容。祝大家在期末考试中取得好成绩!

浙公网安备 33010602011771号