这一篇在干嘛?

在 FPGA 里造 CPU:指令集设计、Nios 软核、自定义指令,以及 T-RISC/LISA/PDSP 三个微处理器案例。 原书代码为 VHDL,本篇所有代码已改写为 Verilog。

  • 第9章 微处理器设计
  • 9.1 微处理器设计概述
  • 9.2 微处理器的发展史
  • 9.3 指令集设计
    1. 隐式寻址
    1. 直接寻址
    1. 寄存器寻址
    1. 存储器寻址
    1. PDSP 专用寻址模式
    1. 栈机:零地址 CPU
    1. 累加器机:单地址 CPU
    1. 二地址 CPU
    1. 三地址 CPU
    1. 零地址、单地址、二地址和三地址 CPU 的比较
  • 例9.3 RISC寄存器文件
    1. ALU 指令
    1. 数据移动指令
    1. 程序流指令
  • 9.4 软件工具
  • 9.5 FPGA 微处理器内核
    1. Xilinx PowerPC
    1. Altera 的 ARM
  • 例9.4 Thumb指令编码
    1. Xilinx 与 Altera 器件上的 ARM Cortex-A9
    1. 8位处理器:Xilinx PicoBlaze
    1. 16 位处理器:Altera Nios
    1. 32位处理器:Xilinx MicroBlaze
  • 例 9.7 MicroBlaze 缓存配置
  • 9.6 案例研究
  • 例9.8 栈机
    1. LISA 18 位指令字 RISC 处理器
    1. LISA 可编程数字信号处理器
    1. LISA 真向量处理器
    1. LISA 处理器设计的比较
    1. 自定义逻辑模块的创建和集成
    1. 软件实现
    1. Altera 自定义逻辑模块的实例化
    1. 新的 CI 设计
    1. Nios 位反向性能结果
  • 9.7 练习
  • 9.2 按练习 9.1 所给分类方式对以下各项进行分类:

自测一下

第9章 微处理器设计

9.1 微处理器设计概述

提到”微处理器”(μP),很多人第一反应是拥有数亿晶体管的 Intel Itanium 这类高端 CPU。那么用 FPGA 能设计出这样的微处理器吗?坦率地说:单片 FPGA 做不出这种级别的高端处理器,但在低功耗、面向特定应用的微处理器方面,FPGA 大有可为。这里的关键在于一个取舍——微处理器用”硬布线方案的性能”换取了”门效率”:同样的算法,用软件跑(或让一个通用状态机去执行指令)速度往往较慢,但占用的硬件资源更少。所以用 FPGA 搭出来的微处理器,气质上更接近”微控制器”,而不是功能完整的 Pentium 或 VLIW 结构的 PDSP。

那为什么还要在 FPGA 里做微处理器呢?本章后续会讨论一个 DWT(离散小波变换)实现控制器的典型应用。有人会问:这种控制用 FSM(有限状态机)不就够了吗?没错——从根本上说,FPGA 微处理器就可以理解为”一个由程序存储器内容定义的 FSM”:FSM 的行为不再是硬布线写死的,而是由存储器中的指令决定,如图 9-1 所示。这样一来,改算法只需改程序,不必重改硬件。早期的 Xilinx PicoBlaze 处理器就叫 KCPSM(Ken Chapman 可编程状态机),名字里就带着这层”状态机血统”。


图 9-1 Xilinx KCPSM,也称作 PicoBlaze

完整的微处理器设计通常包含多个步骤:体系结构探索、指令集设计、开发工具等,本章将逐一展开。建议读者配合一些计算机体系结构教材(参考文献 [336-342])一起学习。在正式动手之前,先回顾一下微处理器是怎么一路发展过来的。

9.2 微处理器的发展史

微处理器通常分为三大类:多功能(CISC)处理器、精简指令集处理器(RISC)和可编程数字信号处理器(PDSP)。了解这三类的历史,有助于理解它们各自的指令集为什么长成那个样子。

9.2.1 多功能微处理器简史

1968 年,一个典型的多功能小型计算机是 16 位体系结构,单片电路板上要用大约 200 个 MSI 芯片(每个约 100 个晶体管)。当时的工程师提出了一个问题 [343]:能不能只用 150、80 甚至 25 个芯片造出一个完整的 CPU?

与此同时,Robert Noyce 和 Gordon Moore 离开 Fairchild 创办了一家以存储芯片为主的公司,先叫 NM 电子,后来更名为 Intel。1969 年,日本计算器厂商 Busicom 请 Intel 为其可编程计算器设计一套芯片。由于优秀 IC 设计人员稀缺,Intel 没有足够人力做 12 种不同的专用芯片,工程师 Ted Hoff 于是提议:做一种更通用的 4 芯片方案,指令从存储芯片中读取——带存储器的可编程状态机(PSM)就此诞生,也就是今天所说的微处理器。9 个月后,在 F. Faggin 协助下,Hoff 团队发布了 Intel 4004:一款用于 Busicom 计算器、可做 BCD 算术的 4 位 CPU。它采用 12 位程序地址、8 位指令,执行一条指令需要 5 个时钟周期。一个最小工作系统只需两块芯片:一个 4004 CPU 加一个程序 ROM。1 MHz 时钟下,每个数字以 80 ns 的速度完成多位 BCD 加法 [344]。

这里不妨算一笔账:一条指令 5 个周期、时钟 1 MHz,即每秒最多执行 20 万条指令。以今天的眼光看慢得可怜,但它把”一台计算机”塞进了两块芯片——这正是微处理器的意义所在。

Hoff 梦想把 4004 的应用从计算器扩展到数字仪表、出租车计价器、汽油泵、电梯控制、医疗设备、自动贩卖机等领域,于是说服 Intel 从 Busicom 手中买下芯片的知识产权。1971 年 5 月,Intel 以价格让步换来了向计算器以外市场销售 4004 的权利。另一个关键配角是 Intel 工程师 Dov Frohman-Bentchkovsky 发明的 EPROM:有了它,开发者可以随时编程、擦除重写,不必再依赖 IC 工厂耗时制造一次性 ROM,4004 开发系统的市场由此打开。

Intel 的客户需要更强的 CPU,于是有了能处理 4 位 BCD 算术之上运算的 8 位 CPU。8008 支持标准 RAM 和 ROM 器件,不再像 4004 那样需要定制存储器;1974 年的 8080 约 4500 个晶体管,修复了 8008 的若干缺陷;1978 年第一个 16 位微处理器 8086 问世;1982 年的 80286 性能约为 8086 的 6 倍;1985 年的 80386 是第一个支持多任务的微处理器,80387 加入算术协处理器加速浮点运算;1989 年的 80486 在包含协处理器的同时引入了指令缓存和流水线;1993 年奔腾系列诞生,拥有两条执行流水线(超标量体系结构);1997 年的奔腾 II 加入 MMX 指令,最多可并行执行 4 个类似 MAC 的向量操作;奔腾 3、奔腾 4 则引入超线程和 SSE 等加速音视频处理的技术;2006 年的 Itanium 已达双核、5.92 亿晶体管、9 MB 独立 L3 缓存。整个系列见表 9-1。

表 9-1 Intel 系列微处理器 [345](IA = 指令集体系结构)

名称发布年份MHzIA处理技术晶体管数量
400419710.108410μ2300
800819720.2810μ3500
80801974286μ4500
808619785~10163μ29K
8028619826~12.5161.5μ134K
80386198516~33321μ275K
80486198925~50320.8μ1.2M
奔腾199360~66320.8μ3.1M
奔腾II1997200~300320.25μ7.5M
奔腾31999650~1400320.25μ9.5M
奔腾420001300~3800320.18μ42M
Xeon20031400~3600640.09μ178M
Itanium 220041000~1600640.13μ592M

从这张表能直观看到”摩尔定律”的轨迹:时钟从 0.1 MHz 涨到数 GHz,晶体管从 2300 个涨到近 6 亿。从图 9-2 的各半导体公司收入对比也可以发现一个有趣的事实:Intel 仅凭微处理器这一个产品线,多年来稳居业界榜首;同样以微处理器为主业的 TI、Motorola/Freescale、AMD 收入低了很多;以存储器为主的三星、东芝虽然常年领先,也难以企及 Intel 的业绩。


图 9-2 顶级半导体公司的收入

9.2.2 RISC 微处理器简史

Intel 体系结构也常被称为复杂指令集计算机(CISC)。从最早的 CPU 开始,后续设计一直背负”兼容”包袱——要能运行老程序。摩尔定律允许 CISC 不断叠加新特性(数字协处理器、缓存、MMX、SSE 及支持它们的指令),但随着数据和程序字节宽度增加,兼容性是以牺牲性能为代价的。Intel 处理器指令繁多、寻址模式多样,手册动辄 700 页以上。

1980 年前后,加州大学伯克利分校的 Patterson 教授、IBM(后来的 PowerPC)以及斯坦福大学的 Hennessy 教授(后来研究 MIPS——无互锁流水线级微处理器)等人分析后得出结论:从性能角度看,CISC 存在诸多问题。伯克利的新一代处理器被命名为 RISC-1 和 RISC-2,因为它们最重要的特征就是指令和寻址模式数量有限,故称精简指令集计算机(RISC)。早期 RISC 与 CISC 的典型差异如下:

  • CISC 有丰富的指令集,RISC 通常支持少于 100 条指令。
  • RISC 指令字长固定(通常 32 位),CISC 字长可变——Intel 指令长度在 1 到 15 字节之间。
  • CISC 支持丰富的寻址模式,典型 RISC 只支持直接寻址和基于寄存器的寻址。
  • CISC 的 ALU 操作数可以来自指令字、寄存器或存储器;RISC 没有直接的存储器操作数,只允许在寄存器与存储器之间加载/存储,因此 RISC 又称”加载/存储体系结构”。
  • CISC 通过栈在子程序间传递参数和数据;RISC 有大量寄存器,用寄存器完成这种链接。

值得注意的是,到 20 世纪 90 年代初,两种体系结构都偏离了”纯理论形态”:如今的 CISC 也大量利用寄存器和深流水线;而 RISC(如 MIPS、PowerPC、SUN Sparc、DEC Alpha)已有几百条指令、有些还需多重循环,早已名不副实。

9.2.3 PDSP 简史

1980 年,Intel 发布了 2930——一种面向控制系统的模拟信号处理器,片内含 ADC 和 DAC,以类似 DSP 的方式实现算法,如图 9-3 所示。


图 9-3 2920 功能模块原理图 [346]

2920 具有 40×25 位的暂存存储器、192 字的程序 EPROM、移位器和 ALU,乘法靠一系列移位加减完成。

例 9.1:用 2920 计算乘法。 先用 CSD 编码表示常数 ,得到 (其中 表示 −1),然后执行下列指令(原书汇编指令,非 HDL,照录如下):

ADD Y, X, L01;
SUB Y, X, R03;
ADD Y, X, R07;
SUB Y, X, R10;

其中最后的操作数 Rxx(Lxx)表示在加/减之前,把第二个操作数右(左)移 xx 位。逐条展开这个例子:把 拆成 ,于是

即左移 1 位加 (得 ),再减去右移 3 位的 ,加上右移 7 位的 ,减去右移 10 位的 ——四次移位加减就完成了一次与常数 的乘法,完全不需要硬件乘法器。

一些常用宏函数在 2920 中的开销见表 9-2。

表 9-2 可用在 2920 中的部分宏模块

功能指令
常数相乘1~5
变量相乘10~26
三角波发生器6~10
阈值检测器2~4
正弦波发生器8~12
单一实极点2~6

可以看到变量相乘要 10~26 条指令——没有硬件乘法器的代价一目了然。虽然还不能把 2920 称为 PDSP,但它具备了 PDSP 的许多重要特征,并发展成 20 世纪 80 年代初的第一代 PDSP(TI 的 TMS320C10、NEC 的 μPD7720),其标志是哈佛体系结构(程序和数据分存于独立存储器)和占用大量芯片面积的硬布线乘法器,可在一个时钟周期内完成单次乘法。第二代 PDSP(约 1985 年,TMS320C25、MC56000、DSP16)实现单周期 MAC 并引入零开销循环。含 μP 特性的第三代(1988 年后,TMS320C30、96002、DSP32C)支持 IEEE 单精度浮点的单周期 32 位浮点乘法。第四代(约 1992 年,TMS320C40、C80)包含多核 MAC。1997 年后的最新一代(TMS320C60x、飞利浦 Trimedia、Motorola Starcore)是长指令字(VLIW)计算机。如今的 PDSP 还有 SIMD(ADSP-2126x SHARC)、超标量(Renesas SH77xxx)、矩阵数学引擎(Intrinsity、FastMath)或它们的混合——PDSP 体系结构已变得高度多样化。

从图 9-4(a) 可见,过去 20 年 PDSP 表现出色,2010 年已可达每秒 3 万亿条指令 [347];从图 9-4(b) 的市场份额看,TI 长期占据大头(Agere 是从前的 Lucent/AT&T,Freescale 是从前的 Motorola)。最近一些 PDSP 内核已可做 FPGA 设计,如 Cast 公司(www.cast.com)的 TI TMS32025 和 Motorola/Freescale 的 56000 内核。不过 PDSP 中的许多 I/O 单元(计时器、DMA、UART 等)通常不在 FPGA 内核中实现。


图 9-4(a) PDSP 处理器收入


图 9-4(b) PDSP 市场份额

9.3 指令集设计

指令集描述了微处理器可执行的行为集合。设计者的第一个问题是:“我的应用需要哪些算法操作?“例如 DSP 应用,重点往往是对快速加法和乘法的支持——用一串小移位加法来算乘法对繁重的 DSP 处理来说并不是好选择。除 ALU 操作外,还需要数据移动指令和程序流指令(如 branch 或 goto)。

指令集设计还依赖于底层体系结构,脱离硬件组件很难完整定义指令集。这项工作很复杂,最好分解成多个步骤,依次回答以下问题:

  1. 微处理器支持什么样的寻址模式?
  2. 基础数据流体系结构是什么,即一条指令包含几个操作数?
  3. 在哪里可以找到这些操作数(寄存器、存储器还是端口)?
  4. 可以支持什么类型的运算?
  5. 在哪里可以找到下一条指令?

9.3.1 寻址模式

寻址模式回答的是”如何定位运算的操作数”。CISC 可能支持十几种模式,而面向性能的设计(RISC、PDSP)会把模式限制在最常用的几种。下面逐一介绍。

1. 隐式寻址

隐式寻址中,操作数的位置不由指令显式给出,而是约定俗成,如图 9-5 所示。例如不带操作数的 ADD 用的是栈机——栈机所有算术运算都作用于栈顶前两个单元;又如 TMS320 PDSP 中的 ZAC 指令用于清空累加器。表 9-3 列出了一些典型例子。

表 9-3 不同微处理器的隐式寻址模式

指令说明μP
ZAT清空累加器和 T 寄存器TMS320C50
APAC将 P 寄存器的内容加到累加器并替换累加器的值TMS320C50
RET将 ra 寄存器的值加载到 PC 中Nios II
BRET将 b 状态复制到状态寄存器并将 ba 寄存器的值加载到 PC 中Nios II


图 9-5 隐式寻址、直接寻址和寄存器寻址

2. 直接寻址

直接寻址中,操作数(如常数)就包含在指令里。问题是指令字通常比完整数据字短几位,塞不下全长常数。常用解决办法有四种:

(a) 符号扩展。程序中的大部分常数(增量、循环计数)都很小,用不到全长;扩展时复制 MSB 而不是补零,这样 −1 的扩展才是正确的(见下面的 MPY-5 示例)。

(b) 分两次加载。用两条指令分别加载常数的低位和高位部分,再拼接成完整字长(见 LPH DAT0)。

(c) 双长度指令。用第二个字扩展默认长度,即可装下全精度常数(见 RPT 1111h)。

(d) 筒形移位分配。将常数按所需形式移位后对齐(见 ADD 11h, 2)。

表 9-4 列出了不同微处理器上的直接寻址示例。

表 9-4 不同微处理器的直接寻址模式

指令说明μP
CNTR=10;把循环计数设置为 10ADSP
MPY-5将 13 位常数符号扩展到 16 位,与 TREG0 寄存器相乘,结果存入 P 寄存器TMS320C50
LPH DAT0把存储器地址 DAT0 中的数据加载到 32 位乘积寄存器的上半部分TMS320C50
RPT 1111h重复下一条指令 次。这是双字指令,常数为 16 位TMS320C50
ADD 11h,2将 11h 移位 2 位后加到累加器中TMS320C50

为避免两次访问存储器,可以把方法 (a) 与 (b) 结合:只要保证高位加载发生在低位指令符号扩展之后即可。

3. 寄存器寻址

操作数从 CPU 内部寄存器取得,不做外部存储访问,如图 9-5 所示。表 9-5 给出了一些示例。

表 9-5 不同微处理器的寄存器寻址模式

指令说明μP
MR=MX0*MY0(RND)将 MX0 与 MY0 寄存器中的数相乘(带舍入)并存入 MR 寄存器ADSP
SUB sA,sB将 sA 减去 sB 并存回 sAPicoBlaze
XOR r6,r7,r8计算 r7 与 r8 的 XOR,结果存入 r6Nios II
LAR AR3,#05h用值 5 加载辅助寄存器 AR3TMS320C50
OR %i1,%i2对 i1、i2 做 OR,结果替换 i1Nios
SWAP %g2交换 32 位寄存器 g2 中的 16 位半字并放回 g2Nios

寄存器访问比内存访问更快、功耗更低,因此 RISC 计算机大量使用寄存器寻址:所有算术操作基本只在 CPU 寄存器内完成,对存储器的访问仅通过单独的加载/存储指令。

4. 存储器寻址

对存储器的直接、间接或混合访问是最常用的模式。直接存储器寻址中,指令字的一部分给出要访问的地址,如图 9-6 及 FETCH 示例所示。与直接常数寻址同样的问题再次出现:指令字里能放的地址位数不足以表达完整地址。解决办法是借助辅助寄存器:若把辅助寄存器加到直接地址上,称为基址寻址(如 LDBU 示例);若辅助寄存器只提供缺失的高位 MSB,称为分页寻址(如 AND 示例,访问页外数据需先更新页指针);由于基址寄存器代表完整地址,也可以完全不给出直接地址、只用寄存器访问,这就是间接寻址(如 PM(I6,M6))。


图 9-6 存储器寻址:直接、基址、页方式和间接寻址

表 9-6 列出了四种典型存储器寻址示例。

表 9-6 不同微处理器的存储器寻址模式

指令说明μP
FETCH sX,ss将临时 RAM 位置 ss 读取到寄存器 sX 中PicoBlaze
LDBU r6,100(r5)计算 100 与 r5 的和,并从这一地址把数据加载到 r6Nios II
AND DAT16设 9 位数据页寄存器指向第 4 页,则从内存位置 访问数据字并在累加器中做 ANDTMS320C50
PM(I6,M6)=AR以 I6 为地址把 AR 存入程序存储器;访问后按修正寄存器 M6 的值更新 I6ADSP

间接寻址只需要少量变址寄存器,可显著缩短指令字,因此是 PDSP 中最常用的方式;而 RISC 更偏爱基址寻址——用”基址+偏移”可以方便地访问阵列元素(如上面的 LDBU)。

5. PDSP 专用寻址模式

PDSP 之所以处理 DSP 算法比标准 GPP/RISC 更高效,关键在于三种专用寻址模式:

  • 自动增/减量寻址
  • 循环寻址
  • 位倒序寻址

前两种服务于卷积和相关运算,位倒序寻址用于 FFT。

(1)自动增/减量寻址。 卷积和相关本质上是重复的乘累加(MAC)运算,即两个向量的内积(参见式 (3-2))。每次 MAC 之后,必须更新数据/系数指针指向下一对操作数。PDSP 在每次存储器访问后自动执行指针增/减量——地址更新不占 CPU,由单独的地址寄存器文件完成,如图 9-7 所示,从而实现单循环 MAC。用 ADSP 汇编编写的 FIR滤波(原书汇编,照录):

CNTR = 256;
MR = 0, MX0 = DM(I0, M1), MY0(I4, M5)
DO FIR UNTIL CE;
FIR: MR= MR+MX0*MY0(SS), MX0=DM(I0, M1), MY0=PM(I4, M5)

初始化循环计数器 CNTR 以及来自数据存储器 DM 的 x 指针和来自程序存储器 PM 的 y 指针后,DO UNTIL 循环在一个时钟周期内完成一条 MAC 并同时加载两个数据,随后用 M1、M5 分别更新指针 I0、I4。注意这里的另一个 PDSP 特性——零开销循环:循环计数器的更新与判断在循环尾部自动完成,不需要额外周期,这与 GPP/RISC 需要专门指令处理循环不同。

(2)循环寻址。 先看 FIR 计算中数据排列的演化。计算第一个输出 时,数据 与系数 按如下方式对齐(同一列相乘后累加):

计算 时需要的数据是:

一种做法是每次 MAC 后移动每个数据字——TMS320 系列为此提供了 MACD 指令(MAC 加数据移动)[349],N 次 MACD 后整个向量 x 移动一个位置。另一种更聪明的观察是:其实系数不用动,把新样本 写到最旧样本 的位置即可:

| x[L] | x[1] | x[2] | … | x[L-2] | x[L-1] |

数据指针从 x[1] 开始照常工作,只有走到缓冲区结尾 x[L-1] 时才需要把指针绕回开头。设缓冲区由 4 个参数描述: = 缓冲器长度, = 当前地址, = 修改值(有符号数), = 缓冲器基址,则所需地址计算为:

这就是循环寻址:每次地址修改后都核对结果是否仍落在有效范围内。图 9-7 给出了 ADSP PDSP 按式 (9-1) 实现的地址生成器。


图 9-7 ADSP 可编程数字信号处理器中地址的生成

(3)位倒序寻址。 第 6 章讲过,基 2 FFT 的输入/输出呈位倒序次序(图 6-14)。用软件计算位倒序变址通常要很多时钟周期,因为每一位都要重排。PDSP 通过专用寻址模式硬件化了这个过程。

例 9.2: ADSP [350] 和 TMS320C50 [349] 都支持位倒序寻址。TMS320 系列的汇编示例(原书汇编,照录):

先把当前辅助寄存器减去 INDX 寄存器的内容,再对地址值执行位倒序以定位操作数;取回的数据字移位 8 位后加到累加器中。

表 9-7 总结了 PDSP 及其支持的寻址模式:所有 PDSP 都支持自动增/减量,大多数也支持循环缓冲器和位倒序寻址。

表 9-7 在卷积中有用的 PDSP 属性(© Springer 出版社文献 [5])

供应商类型累加位超级哈佛结构Modulo 地址位倒序硬件循环MAC 速度(MHz)
PDSP 16 位×16 位整型数乘法器
Analog DeviceADSP-218740√√√√52
Analog DeviceADSP-21csp0140√√——50
LucentDSP162036√—√√120
MotorolaDP5616636√√√√60
NECμPD7701540√√√√33
TITMS320F20632√—√√80
TITMS320C5132√√√√100
TITMS320C54940√√√√200
TITMS320C601402 MAC√√√500
TITMS320C80322 MAC√√√50
PDSP 24 位×24 位整型数乘法器
MotorolaDSP5601156√√√√80
MotorolaDSP5630556√√√√100
PDSP 24 位×24 位浮点数乘法器
Analog DeviceSHARC 2106180√√√√40
MotorolaDSP9600296√√√√60
TITMS320C3140√√√√60
TITMS320C4040√√√√60

9.3.2 数据流:零地址、单地址、二地址和三地址设计

指令的典型汇编编码是”操作码 + 操作数”。一个 ALU 运算通常需要两个操作数,可能还要指明结果的存放位置。对程序员最自然的方式是一条指令含一个操作码加三个操作数,但这需要很长的指令字。算一笔账:假设 FPGA 嵌入式存储器有 1K 字,不考虑操作码的寻址方式至少需要 30 位;现代 CPU 直接寻址 4GB 需要 32 位地址,三操作数直接寻址的指令字至少要 96 位。所以,限制操作数数量可以缩短指令字、节约资源——零地址的栈机在这方面做到极致;另一种方式是用寄存器文件代替直接存储器访问,RISC 就是只允许加载/存储单个存储器操作数:对于 8 个寄存器的 CPU,只需 9 位就能规定三个(寄存器)操作数,代价是需要额外的指令在存储器和寄存器文件之间搬数据。

下面依次讨论零到三操作数设计。作为统一的对照,四种机器都要计算同一个表达式:

1. 栈机:零地址 CPU

零地址计算机怎么工作?回顾 9.3.1 节:操作数可以隐式给出。与 TI PDSP 把所有乘积固定放入乘积寄存器 P 类似,栈机的所有二操作数算术指令都作用于栈顶的两个元素 [351]。栈是后入先出(LIFO)结构:PUSH 放入的元素在 POP 时最先出来。式 (9-2) 的栈机计算过程见表 9-8,左侧是指令,右侧是各步执行后栈的内容(栈顶在左)。

表 9-8 栈运算指令

指令栈顶234
PUSH 55———
PUSH aA5——
SUB5 - a———
PUSH bB5 - a——
PUSH cCb5 - a—
MULc × b5 - a——
ADDc × b + 5 - a———
POP d————

所有算术运算(ADD、SUB、MUL)都用隐式的栈顶和次栈顶,这就是”零地址”;但存储器操作 PUSH 和 POP 仍需要一个操作数。

由于操作数先于运算出现,栈机代码称为后缀表示法(逆波兰表示法)。式 (9-2) 的中缀与后缀写法对照如下:

一些读者可能记得,HP41C 袖珍计算器用的正是后缀表示法,它也使用 4 个值的栈。图 9-8(a) 给出了这种体系结构。

2. 累加器机:单地址 CPU

给 CPU 加一个累加器,让它固定充当一个操作数的来源和结果的去处,算术运算形式为:

其中 □ 代表一种 ALU 运算(ADD、MUL、AND 等)。TI TMS320 系列的底层体系结构就是这种类型,如图 9-8(b) 所示。式 (9-2) 在 TMS320C50 [349] 上的汇编代码见表 9-9。

表 9-9 TMS320C50 的汇编程序代码

指令说明
ZAP清空 accu 和 P 寄存器
ADD 5h加 5 到 accu
SUB DAT1从 accu 减 DAT1
LT DAT2在 T 寄存器中加载 DAT2
MPY DAT3将 T 寄存器与 DAT3 相乘,结果存入 P 寄存器
APAC将 P 寄存器内容加到 accu
SACL DAT4将 accu 存储到地址 DAT4


图 9-8(a) 栈 CPU 体系结构


图 9-8(b) 累加器机体系结构

这一示例假定变量 ad 已映射到数据存储器 DAT1DAT4。比较栈机和累加器机可以得到两个结论:

  • 指令字的规模没有减小——栈机同样需要带操作数的 PUSH/POP。
  • 编码表达式的指令条数没有明显减少(累加器机 7 条,栈机 8 条)。

要显著减少指令条数,就得指望二操作数机了。

3. 二地址 CPU

二地址机允许规定两个独立操作数,目的操作数兼作第一个操作数,运算形式为:

其中 □ 代表一种 ALU 运算(SUB、DIV、NAND 等)。Xilinx PicoBlaze [335,352] 和 Altera 的 Nios 处理器 [353] 采用这种数据流,如图 9-9(a) 所示。受二操作数限制,这些机器使用 16 位指令字格式。式 (9-2) 在 PicoBlaze 上的汇编代码见表 9-10。

表 9-10 PicoBlaze 的汇编程序代码

指令说明
LOAD sD, sB;将寄存器 B 存入寄存器 D
MUL sD, sC;将 D 与寄存器 C 相乘
ADD sD, 5;加 5 到 D
SUB sD, sA;从 D 中减 A

注意运算顺序被重排了:先算 (对应 的顺序),以避免产生乘积的中间结果。另外要说明:PicoBlaze 实际没有单独的 MUL 运算,这段代码仅用于说明二操作数的编码法则。PicoBlaze 有 16 个 8 位寄存器;两个操作数(各 4 位)加 8 位常数,正好能把操作码和操作数放进一个 16 位指令字。可以看到,与栈机和累加器机相比,二操作数编码显著减少了运算条数。

4. 三地址 CPU

三地址机最灵活:两个源操作数和目的操作数可以各不相同,运算形式为:

大多数现代 RISC 计算机(PowerPC、MicroBlaze、Nios II)都采用这种编码 [354~356],但操作数通常是寄存器操作数,或至多一个操作数来自数据存储器。数据流如图 9-9(b) 所示。


图 9-9(a) 二地址 CPU 体系结构


图 9-9(b) 三地址机体系结构

三地址机的汇编编程简单直接。式 (9-2) 按 Nios II 指令编码见表 9-11。

表 9-11 式(9-2)中的算术示例用到的指令

指令说明
SUBI r4,r1,5;从 r1 中减 5,结果存入 r4
MUL r5,r2,r3;将 r2 与 r3 相乘,结果存入 r5
ADD r4,r4,r5;将 r4 与 r5 相加,结果存入 r4

这里假定寄存器 r1r4 保存变量 ad 的值。这是四种机器中最短的代码——只有 3 条指令,代价是指令字更长。就硬件实现而言,二地址机与三地址机没有明显差别,因为寄存器文件都需要相同的多路复用器和信号分离器。

5. 四种 CPU 的比较

对本节加以总结:

  • 栈机程序最长,但单条指令最短。
  • 即使栈机也需要带地址的单地址指令访问存储器。
  • 三地址机代码最短,但每条指令所需位数最大。
  • 用寄存器代替直接存储器地址可以缩短指令字。三地址机通常用两个寄存器加一个存储器操作数。
  • 加载/存储机只允许数据在存储器和寄存器之间移动,所有 ALU 运算都通过寄存器文件完成。
  • 大多数设计假设寄存器访问比存储器访问快——这对使用外部存储器的 CBIC 或 FPGA 成立。在 FPGA 中,寄存器文件内访问与嵌入式存储器访问的时间在同一量级,因此也可以选择用嵌入式(三端)存储器实现寄存器文件。

看起来没有”完美答案”,实践中各种寻址方式都在使用。为什么没有一种最佳数据流类型?因为这取决于设计目标。表 9-12 从多个维度做了对比。

表 9-12 0 至 3 操作数 CPU 的不同设计目标的比较

设计目标0 操作数123 操作数
汇编程序容易程度最差……最佳
简单 C 编译器最佳……最差
代码字数量最差……最佳
指令长度最佳……最差
直接范围最差……最佳
快速操作数存取和解码最佳……最差
硬件规模最佳……最差

具体展开说:三地址汇编代码比栈机的大量 PUSH/POP 更易读易写,所以”汇编容易程度”三地址最佳;而栈机最适合简单 C 编译器——中缀表达式转后缀很机械,解析简单,但高效管理寄存器文件对编译器是艰巨任务。中间结果可以留在寄存器里,所以操作数越多”代码字数量”越少。指令长度与操作数数量成正比;指令字越短,能嵌入的常数也越短,就需要多次加载或双字指令(参见图 9-6 存储器寻址)。操作数越少,提取和解码越快——栈机总用栈的前两个元素,没有寄存器文件长 MUX/DEMUX 的延迟。硬件规模主要取决于寄存器文件:三操作数机要求最高,栈机最低,而 ALU 和控制单元的规模彼此接近。

总之,每种体系结构都有强项和弱项,必须与设计者的工具、技能、设计目标(规模/速度/功耗)以及开发工具(指令集仿真器、C 编译器)相匹配。

9.3.3 寄存器文件和存储器体系结构

计算机发展早期存储器非常昂贵,冯·诺依曼提出了著名的创新:把数据和程序放进同一存储器,如图 9-10(a) 所示——那个年代程序通常硬布线在 FSM 里,只有数据用 RAM。如今情况反转:存储器便宜了,但典型 RISC 计算机的主存访问远慢于 CPU 寄存器。于是三地址机面临取舍:三个操作数全部来自主存?还是只有部分来自主存?是否所有 ALU 运算都该由 CPU 寄存器执行(加载/存储体系结构的立场)?VAX PDP-11 是这方面的”冠军”,允许多个存储器和多个寄存器运算。

对 FPGA 设计还有一条额外约束:指令字数量通常以千计,若程序和数据混在同一存储器(冯·诺依曼方式),复用数据和程序字会浪费时间,因此 FPGA 微处理器应使用独立的程序存储器和数据存储器——这就是哈佛体系结构,如图 9-10(b) 所示。对 PDSP 来说还可以更进一步:系数和数据来自两个不同的数据存储器 x 和 y,累加结果保存在 CPU 寄存器中(回忆 FIR 滤波器的用法),程序放在第三个存储器——由于 DSP 算法往往很短,一些 PDSP(如 ADSP)用小规模缓存替代第三条总线:第一遍循环后指令已存入缓存,程序存储器可当第二个数据存储器用。这种三总线结构如图 9-10(c) 所示,称为”超级哈佛体系结构”。


图 9-10 存储器体系结构

GPP(如 Intel 奔腾)和 RISC 计算机通常采用存储器分层结构(寄存器 → L1/L2 缓存 → 主存 DRAM → CD-ROM/磁带等外部介质),为 CPU 提供连续数据流的同时使用较便宜的大容量存储器。这种存储系统的设计远比 FPGA 内部能实现的复杂。

从硬件角度看,CPU 可以拆成三个主要部分:

  • 控制路径,即有限状态机
  • ALU
  • 寄存器文件

三部分中寄存器文件的设计不难,但用 LE 实现时成本最高。因此要在”更多寄存器带来的好用性”与”寄存器文件的高实现成本”之间权衡(例如 32 个寄存器)。下面的例子给出典型 RISC 寄存器文件的编码。

例 9.3 RISC 寄存器文件

设计 RISC 寄存器文件时经常实现大量寄存器。为避免额外的清零指令或寄存器移动指令,通常把第一个寄存器永久置零。这看似浪费了一个寄存器,却实实在在简化了汇编编码,见表 9-13。

表 9-13 简化的指令

指令说明
ADD r3,r0,r0;将寄存器 r3 置零
ADD r4,r2,r0;将寄存器 r2 移到寄存器 r4
LDBU r5,100(r0);计算 100 与 r0=0 的和,并从这一地址把数据加载到 r5

请注意,这些伪指令只有在第一个寄存器 r0 恒为零的假设下才成立。

原书 VHDL 改写为 Verilog——下面是一个 W 位宽、N+1 个寄存器的通用寄存器文件(第一个寄存器恒为零):

// W x L bit register file, first register is hardwired to zero.
module reg_file #(
    parameter W = 7,   // 位宽-1(即数据 8 位)
    parameter N = 15   // 寄存器个数-1(即 16 个寄存器)
)(
    input  wire         clk,      // 系统时钟
    input  wire         reset,    // 异步复位
    input  wire         reg_ena,  // 写使能,高有效
    input  wire [W:0]   data,     // 写入数据
    input  wire [3:0]   rd,       // 写地址
    input  wire [3:0]   rs,       // 读地址 1
    input  wire [3:0]   rt,       // 读地址 2
    output wire [W:0]   s,        // 读出数据 1
    output wire [W:0]   t         // 读出数据 2
);
 
    reg [W:0] r [0:N];   // 寄存器堆
    integer k;
 
    // 写端口:同步写,异步清零;寄存器 0 不允许被改写
    always @(posedge clk or posedge reset) begin
        if (reset) begin
            for (k = 0; k <= N; k = k + 1)
                r[k] <= {(W+1){1'b0}};
        end else if (reg_ena && rd > 0) begin
            r[rd] <= data;
        end
    end
 
    // 两个组合读端口(读寄存器 0 时返回 0)
    assign s = (rs > 0) ? r[rs] : {(W+1){1'b0}};
    assign t = (rt > 0) ? r[rt] : {(W+1){1'b0}};
 
endmodule

时钟进程负责把输入 data 写入寄存器堆,注意 rd 为 0 时禁止写入,因为寄存器 0 永远为零;两个连续赋值语句对应原书第二个进程中的两个解码器,为 ALU 读出两个源操作数,读地址为 0 时直接给出 0。该设计用 226 个 LE,不使用嵌入式乘法器和 M9K;由于寄存器到寄存器之间没有路径,无法测量时序性能 Fmax。

对照图 9-11 的仿真结果理解行为:输入 data(配合写地址 rd)被连续写入寄存器堆;输出 s 由 rs 设为寄存器 2,输出 t 由 rt 设为寄存器 3。可以观察到寄存器写使能在 600 ns 到 800 ns 之间无效,因此寄存器 2 没有被值 12 覆盖;而使能在 800 ns 时再次变高,新值 14 写入了寄存器 3——这从 t 信号的跳变可以看到。信号图最后显示的是寄存器堆的内部变量 r。


图 9-11 寄存器文件的仿真结果

图 9-12 显示了寄存器个数从 4~32、位宽为 8/16/24/32 时各种配置的 LE 消耗,可以据此为容量和成本做折中。


图 9-12 不同寄存器文件配置的 LE

Xilinx 的 FPGA 还可以把 LE 用作双端存储器(参见第 1 章图 1-11)。而对 Altera 的 FPGA,唯一的省 LE 途径是把寄存器文件放进嵌入式存储器:可用一个三端存储器或两个嵌入式双端存储器模块——两个存储器写入相同的数据,从各自端口分别读出两个源操作数。Nios 微处理器就利用这一原则大幅降低了 LE 数量:只用较低的第 015(或 031)个寄存器,或者像 Nios 那样给寄存器开”窗口”。窗口可以移动,例如调用子例程时不必在栈或存储器中保存寄存器现场,直接切换到新窗口即可。

不过从时序角度还有一个问题:BlockRAM 是同步存储器,不能在同一个时钟沿上同时完成”取当前地址的数据供解码用”和”按当前信号写入新值”——也就是不能用同一时钟沿既读旧值又写新值。解决办法是:用上升沿提供要读取的操作数地址,再用下降沿存储新值并设置写使能,一拍拆成两个半拍使用。

9.3.4 操作支持

大多数计算机的指令至少属于以下三类之一:算术/逻辑单元(ALU)操作、数据移动和程序控制。这里只需简要记住分类即可。基本数据类型通常是多位整型(8、16、32 位);某些更高级的处理器还使用 32 位或 64 位 IEEE 浮点数据类型(参见 2.2.3 节)。

ALU 指令