Skip to content

Latest commit

 

History

History
3063 lines (1997 loc) · 123 KB

200920c_verilog.md

File metadata and controls

3063 lines (1997 loc) · 123 KB

数字电路基础以及 Verilog FPGA 开发入门笔记

有关数字电路的一些较为底层的理论知识,Verilog语法、仿真以及FPGA开发入门

包含了IEEE754浮点标准解读

参考书籍

Fundamentals of Digital Logic with Verilog Design (Second Edition), Stephen Brown, Zvonko Vranesic, Canada, 2007

CPU自作入門, Kazutoshi Suito, Ryo Yonezawa, Yuji Fujita, Japan, 2012

开发平台

EDA: Intel Quartus Prime Lite

FPGA: Intel Altera Cyclone IV EP4CE10E22C8N

目录

  • 0 先行预备知识
    • 0.1 半导体:N型和P型
    • 0.2 晶体管:NMOS和PMOS以及工作原理
    • 0.3 工艺:TTL和CMOS
  • 1 数字电路基础
    • 1.1 布尔(逻辑)代数公理
    • 1.2 触发器,寄存器,计数器
    • 1.3 组合逻辑表达式的推导
    • 1.4 组合逻辑电路的优化
      • 1.4.1 一般化简
      • 1.4.2 优化理论概要:卡诺图法,列表法和立方体表示法
      • 1.4.3 图表法(卡诺图法,适用于人工作业)
      • 1.4.4 列表法/立方体表示法简介
      • 1.4.5 列表法化简
      • 1.4.6 立方体表示法化简
    • 1.5 同步时序电路设计与分析
      • 1.5.1 Moore型电路基本设计步骤
      • 1.5.2 Mealy型电路设计步骤
      • 1.5.3 状态最小化
      • 1.5.4 同步时序电路设计小结
    • 1.6 异步时序电路设计与分析
      • 1.6.1 异步行为
      • 1.6.2 异步电路分析
      • 1.6.3 异步电路的综合(设计)
      • 1.6.4 状态最小化
      • 1.6.5 状态最小化通用步骤:使用合并图
      • 1.6.6 状态赋值通用步骤:重标记流程表
      • 1.6.7 冒险(Hazard)简介
      • 1.6.8 静态冒险
      • 1.6.9 动态冒险
    • 1.7 其他实用知识补充
      • 1.7.1 触发器亚稳态问题
  • 2 Verilog HDL开发环境
    • 2.1 类Unix下的Verilog编译和仿真环境:使用Verilator或Icarus Verilog,配合gtkwave
      • 2.1.1 源文件编写与组织
      • 2.1.2 编译以及仿真,使用Icarus Verilog
      • 2.1.3 查看波形,基于gtkwave
    • 2.2 Intel Quartus Prime 使用简介,仿真基于ModelSim
  • 3 Verilog HDL语言基础
  • 4 常用的算术硬件以及计算方法
    • 4.1 加法器
    • 4.2 乘法器
      • 4.2.1 时序乘法器
      • 4.2.2 阵列乘法器
      • 4.2.3 符号扩展乘法运算
    • 4.3 除法器
    • 4.4 二进制比较器
    • 4.5 浮点运算
    • 4.6 二进制转BCD
    • 4.7 运算负数/零值/进位/溢出标志位
    • 4.8 立即数的循环移位表示
  • 5 补充
    • 5.1 IEEE754浮点数标准
      • 5.1.1 浮点数的表示
      • 5.1.2 无穷
      • 5.1.3 NaN
      • 5.1.4 舍入(Rounding)
      • 5.1.5 操作(Operations)简介
      • 5.1.6 同类通用运算操作(Homogeneous General-computational operations)
      • 5.1.7 formatOf通用运算操作(formatOf General-computational operations)
      • 5.1.8 Quiet-computational operations
      • 5.1.9 Signaling-computational operations
      • 5.1.10 非运算操作(Non-computational operations)
      • 5.1.11 异常及其处理
      • 5.1.12 IEEE754推荐实现的函数

0 先行预备知识

只补充集成电路的物理构成与原理

0.1 半导体:N型和P型

N型半导体:通过粒子加速向硅等4价元素单质(本征半导体)掺杂高价元素如5价磷、5价砷等形成,呈电中性,自由电子为多数载流子(空穴为少数载流子)

P型半导体:掺杂低价元素如3价硼,3价铟等形成,呈电中性,自由空穴为多数载流子(电子为少数载流子)

掺杂工艺:通过高能电场加速将要掺杂的杂质粒子嵌入到硅基板,之后通过退火(Anneal)热处理,使得不在晶格处的杂质粒子归位并产生自由载流子,并修复掺杂过程中产生的晶格缺陷。杂质浓度越高,导电性越强

PN结(PN junction):将一个P型半导体和N型半导体放在一起就可以形成一个PN结。由于P和N型半导体的载流子浓度差异巨大,P半导体的空穴会向N半导体扩散,N半导体的电子会向P半导体扩散,这样P半导体交界处会带上负电,N半导体交界处会带上正电,形成一个由N半导体指向P半导体的阻止粒子继续扩散的电场,最终达到动态平衡。这片区域为空间电荷层,由于此处双方都缺少多数载流子,也被称为耗尽层

想要使得一个PN节导电,只要添加外部电场消除耗尽层的电场即可。方法就是将P半导体接到高电位,将N半导体接到低电位,这样PN结处就会开始导电。如果添加相反电场则会使得耗尽层变厚,这也是PN结单向导电的原理

0.2 晶体管:NMOS和PMOS以及工作原理

现代硅基数字集成电路都是由MOSFET构成,MOSFET分为NMOS(N沟道)和PMOS(P沟道),NMOS结构如下图

NMOS

在集成电路中,NMOS的G极作输入端,和衬底以及SD之间有二氧化硅隔开,输入电阻理想状态是无穷大,S和D极是完全相同的两端,当G极输入高电平时S和D导通。S和D极为N型半导体,衬底为P型半导体,连接低电势位。S和D极电势一定大于等于衬底

当NMOS的G极输入高电平时,可以将P型衬底的少数电子全部吸引到靠近G极处,在二氧化硅层下形成电子载流区,连通S和D极,此时S和D极之间导通

PMOS和NMOS原理相反,S和G极为P型半导体,衬底为N型半导体,连接高电势位。当G极输入低电平时导通,如下图

PMOS

0.3 工艺:TTL和CMOS

一般常见的74系列数字电路分为"LS"TTL型和"HC"CMOS型两大系列。TTL型电路一般只使用单一极性BJT三极管或MOS管构建,而CMOS全称为"Complementary Metal Oxide Semiconductor",顾名思义CMOS使用了NMOS和PMOS两种极性互补的MOS管构建电路

TTL一般只由单一极性晶体管(BJT或MOSFET)配合电阻构建,由于工作需要持续电流,功耗较大(同时也比较恒定),以非门为例

TTL非门

而CMOS使用互补晶体管,同一时间只有其中一个晶体管导通,并且MOSFET的G极输入阻抗可以近乎视为无穷大,这些特性是CMOS电路省电的关键。CMOS电路一般只在动态切换(switching),处于中间状态时会产生漏电增加功耗,它的静态功耗很小可忽略不计,这也是为什么CMOS电路功耗会随运行频率提高而显著增大。同样以非门为例

CMOS非门

1 数字电路基础

记录比较常用的一些理论以及解决问题的方法

1.1 布尔(逻辑)代数公理

用于基本与或非电路的设计

名称 公式
双否律 $ \overline{\overline{A}} = A $
交换律 $ A \cdot B = B \cdot A, A + B = B + A $
结合律 $ (A \cdot B) \cdot C = A \cdot (B \cdot C), (A + B) + C = A + (B + C) $
分配律 $ (A \cdot B) + C = (A + C) \cdot (B + C), (A + B) \cdot C = (A \cdot C) + (B \cdot C) $
德摩根律 $ \overline{A + B} = \overline{A} \cdot \overline{B}, \overline{A \cdot B} = \overline{A} + \overline{B} $
吸收律 $ A \cdot (A + B) = A, A + (A \cdot B) = A $
幂等律 $ A + A = A, A \cdot A = A $
同一律 $ A + 0 = A, A \cdot 1 = A $
零一律 $ A \cdot 0 = 0, A + 1 = 1 $
排中律 $ \overline{A} + A = 1 $
矛盾律 $ \overline{A} \cdot A = 0 $
异或等价公式 $ A \oplus B = (B \cdot \overline{A}) + (A \cdot \overline{B}) $
补充吸收律(在之后的异步电路的设计中涉及到冒险问题,可吸收的那一项被称为冗余项) $ A \cdot B + \overline{A} \cdot C + B \cdot C = A \cdot B + \overline{A} \cdot C $

补充吸收律可以由吸收律推导而来:

$$ \begin{align*} AB + \overline{A}C + BC &= AB+\overline{A}C + BC(\overline{A} + A) \\ &= AB+\overline{A}C + \overline{A}BC + ABC \\ &= A(B + BC) + \overline{A}(C + BC) \\ &= AB + \overline{A}C \\ \end{align*} $$

1.2 触发器,寄存器,计数器

逻辑电路中,触发器为重要组成部分,带入了同步时序(异步时序使用反馈回环实现),将逻辑电路的行为从空间扩展到了时间,从组合逻辑到时序逻辑

1.2.1 RS锁存器

RS锁存器 电路如下

RS锁存器

可以看作由两个非门的电路演变而来

非门锁存器

RS锁存器行为可定义如下

$$ \begin{array}{| c | c | c | c |} S & R & Q & \overline{Q} \ \hline 0 & 0 & 0/1 & 1/0 \\ 1 & 0 & 1 & 0 \\ 0 & 1 & 0 & 1 \\ 1 & 1 & 0 & 0 \end{array} $$

当$S,R$都为$0$时输出状态保持不变。$S$高电平$R$低电平时置位($Q$变高电平),$R$高电平$S$低电平时复位($Q$变低电平)。如果$RS$都为高电平,输出$Q,\overline{Q}$都为低电平

注意:RS触发器在$RS$端都为0时上电瞬间,以及$RS$端同时从1跳变为0时可能会出现短时间振荡

门控RS锁存器

门控RS锁存器

就是在RS触发器基础上加一个输入控制门

全与非门实现的门控RS锁存器

与非门实现的门控RS锁存器

表示符号如下

门控RS锁存器符号

1.2.2 D锁存器

经典D锁存器

$D$的值会在$Clk$高电平脉冲到来时被锁存

D锁存器

电路符号如下

D锁存器符号

Earle锁存器

Earle锁存器是D锁存器的改进,可以保证任意时候的输出跳变维持在2个门延迟

$E_H$相当于$E$,$E_L$相当于$\overline{E}$,是互补时钟信号输入,必须互为反相且严格对齐,才能保证没有冒险发生

主从D触发器

主从D触发器结构如下图

主从D触发器

$D$在$Clk$高电平时被主触发器取样,输出不变,$Clk$变为低电平后从从触发器输出

电路符号如下

主从D触发器符号

边沿D触发器

边沿D触发器结构如下图

沿触发D触发器

由上图分析,当$Clk$为低电平时,$P1 = P2 = 1, P3 = D, P4 = \overline{D}$,触发器状态不变。若$Clk$正跳变时$D$为$0$,$P1 = 1, P4 = 1, P2 = 0, P3 = 0, Q = 0$,此时若$D$变为$1$,由于$P2 = 0$锁死了$P4 = 1$,所以不会有变化。若$Clk$正跳变时D为1,$P1 = 0, P4 = 0, P2 = 1, P3 = 1$,此时若$D$变为$0$,由于$P1 = 0$锁死了$P3 = 1, P2 = 1$,所以也不会有变化。

边沿D触发器可以保证在$Clk$高电平时$D$发生改变而输出状态不变

电路符号如下

沿触发D触发器符号

有清零端和预置信号的D触发器

带清零端和置位端的主从D触发器

带清零端和置位端的主从D触发器

使用时,需要保持清零端和置位端为高电平。清零和置位为异步于时钟,低电平有效

带清零端和置位端的主从D触发器符号

带清零端和置位端的沿触发D触发器

带清零端和置位端的沿触发D触发器

使用时,需要保持清零端和置位端为高电平。清零和置位为异步于时钟,低电平有效

带清零端和置位端的沿触发D触发器符号

同步清零的实现:

如下,实现同步清零直接在数据输入端加一个与门即可

同步清零沿触发D触发器

1.2.3 T触发器

T触发器结构如下图

T触发器

T触发器符号

T触发器符号

T触发器行为可以定义如下:如果$T = 0$则不跳变;如果$T = 1$则不停跳变

1.2.4 JK触发器

JK触发器结构如下图

JK触发器

JK触发器有两个输入端$J$和$K$,既可以当门控RS触发器使用,也可以当T触发器使用(T触发器就是由JK触发器演变而来)。$R = S = 1$在RS触发器中是不允许的状态,此时JK触发器可以相当于T触发器$T = 1$,输入时钟后输出$Q,\overline{Q}$不停跳变;其他状态相当于$J = S, K = R$,相当于RS触发器的$RS$端

1.2.5 寄存器

移位寄存器

移位寄存器

移位寄存器由沿触发D锁存器组成,一个D触发器的Q输出端接下一个D触发器D输入端,原理好理解

并行存取移位寄存器

并行存取移位寄存器

并行存取移位寄存器结合了并行寄存器以及移位寄存器的功能,加入了外部电路,实现$\overline{SHIFT}/LOAD$端低电平时串行移位,高电平时并行置位,如图原理好理解

1.2.6 计数器

异步计数器(Asynchronous Timer)

异步计数器

异步计数器由T触发器组成,上一个触发器的反相输出端连接下一个触发器的时钟输入端,输入端$T$接高电平

以上是递增计数器,递减计数器如下

递减异步计数器

异步计数器由于时钟经过每一级都会有延迟,所以不能用于对时序要求高的高频场合

同步计数器(Synchronous Timer)

同步计数器

同步计数器保证了时钟的同步,所有位延迟相同

同步计数器添加复位和使能端后如图,复位为异步复位,使能端即第一个触发器的$T$端

带复位和使能端的计数器

D触发器实现的同步计数器

由T触发器的实现电路简化而来(使用一个异或门和一个D触发器同样可以等价实现T触发器)

D触发器实现的同步计数器

支持并行加载的计数器

计数器可以使用触发器自带的异步Preset以及Clear端进行置位或复位,这里使用另外一种同步并行加载法

支持并行加载的计数器

并行加载计数器的应用:并行加载的计数器可以添加少量外部器件构建任意模数以及复位初始值的计数器,可以使用与门使计数器计数到特定值后输出高电平到$Load$端,加载预置输入,如下图实现了一个模6(输出0~5)计数器

模6计数器

该模6计数器时序图如下

模6计数器时序

由上图分析,在计数器计数到5后,$Load$变为高电平,下一次时钟到来时会直接将预置值加载

下图示例另外一种使用异步复位端的模5计数器以及其时序图,并说明这种设计的缺陷 模5计数器

这个计数器设计和以上同步加载的相同,区别是它只能作为模5(输出0~4)计数器使用,由时序图可知,其输出为5的时间非常短,但是确实会输出5用于复位(输出为5后立即强制复位),和时钟无关,只和门电路固有延迟有关。虽然这个电路相对来说更简单,但是这在一个数字系统中,尤其是使用同步时钟的系统,是可能带来不可预知的行为的,要尽量避免。

BCD(8421码计数器)

BCD计数器

由同步加载计数器可以设计出以上2位BCD码计数器。计数器巧妙应用了第2位计数器的Enable端,可以实现逢十进一。

环形计数器

环形计数器原理和移位寄存器差不多,区别于一般二进制计数器,环形计数器在同一时刻只有一个输出为高电平(独热码)。

模n环形计数器

如图,模n环形计数器有一个异步$Start$端,相当于是这个计数器的复位端,$Start$高电平将$Q_0$置位为$1$,其他复位为$0$,时钟触发后$Q_0$逐个移位作为计数功能

环形计数器也可以使用一般二进制计数器加上译码器组成,适用于较大的环形计数器

Johnson计数器

Johnson计数器

Johnson计数器和环形计数器差不多,区别就是Johnson计数器复位后所有位为$0$,且最后一位的反相输出端连接到第一个触发器的输入端,Johnson计数器工作输出规律如下:$0000 \rightarrow 1000 \rightarrow 1100 \rightarrow 1110 \rightarrow 1111 \rightarrow 0111 \rightarrow 0011 \rightarrow 0001 \rightarrow 0000$

1.3 组合逻辑表达式的推导

有两种方法,所有真值为$1$的项的主析取范式(积之和),或所有真值为$0$的项的主合取范式(和之积),具体视情况而定,尽量使用较简单的方法

示例

有如下真值表,$ABC$为输入,$X$为输出

$$ \begin{array}{| c | c | c | c |} A & B & C & X \ \hline 0 & 0 & 0 & 1 \\ 1 & 0 & 0 & 1 \\ 0 & 1 & 0 & 0 \\ 1 & 1 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ 1 & 0 & 1 & 1 \\ 0 & 1 & 1 & 0 \\ 1 & 1 & 1 & 0 \end{array} $$

取1项作积之和(每一个积都是最小项)

$$ X = \overline{ABC} + A \overline{BC} + A B \overline{C} + A \overline{B}C $$

取0项作和之积(每一个和都是最大项)

$$ X = (A + \overline{B} + C)(A + B + \overline{C})(A + \overline{B} + \overline{C})(\overline{A} + \overline{B} + \overline{C}) $$

积之和/和之积互相转换

以上为例,积之和为

$$ X = \overline{ABC} + A \overline{BC} + AB \overline{C} + A \overline{B}C $$

没有出现的项为

$$ ABC, \overline{A}BC, \overline{AB}C, \overline{A}B\overline{C} $$

那么

$$ \overline{X} = ABC + \overline{A}BC + \overline{AB}C + \overline{A}B\overline{C} $$

由双否律以及德摩根律又可以推出

$$ \begin{align*} X &= \overline{\overline{X}} = \overline{ABC + \overline{A}BC + \overline{AB}C + \overline{A}B\overline{C}} \\ \Rightarrow X &= (\overline{A} + \overline{B} + \overline{C})(A + \overline{B} + \overline{C})(A + B + \overline{C})(A + \overline{B} + C) \end{align*} $$

反之求和之积的积之和同理

补充:积之和/和之积一般表示方式

积之和

$$ X = m_0 + m_1 + m_3 + m_5 = \sum m(0, 1, 3, 5) $$

和之积

$$ X = M_2 \cdot M_4 \cdot M_6 \cdot M_7 = \prod M(2, 4, 6, 7) $$

最小项与其对应的最大项关系$m_2 = \overline{M_2}$(德摩根)

1.4 组合逻辑电路的优化

在逻辑电路的设计中,同一个电路一般有多种具体实现,考虑到成本,设计复杂度以及功耗,需要对电路进行化简

门电路数以及门电路输入端数量之和为衡量电路成本的重要因素

1.4.1 一般化简

简单情况下,逻辑电路的化简,就是使用布尔代数公理,化简积之和/和之积的过程

在之前的示例中,可以化简积之和如下

$$ X = \overline{BC} + \overline{B}A + \overline{C}A = \overline{B}A + \overline{C}A $$

1.4.2 优化理论概要:卡诺图法,列表法和立方体表示法

在大规模电路中,一般的根据布尔代数公理推导会非常复杂,因此引入一套完整的理论用于逻辑代数的优化,也可以看作是通用的高级优化方法。这些方法被广泛应用于集成电路设计以及FPGA的EDA中。常见的有卡诺图法、列表法以及立方体化简法。卡诺图法和列表法适合于简单逻辑的设计,适合手动操作,不适用于复杂的电路;立方体法经过改良可以用于计算机算法。目前在数字电路设计的领域已经开发出了很多相关算法(启发式),有兴趣的可以看看国外论文

1.4.3 图表法(卡诺图法,适用于人工作业)

可以使用积之和,也可以和之积,原理相同

二变量卡诺图

真值表

$$ \begin{array}{| c | c | c |} X_1 & X_2 & X \ \hline 0 & 0 & m_0 \\ 0 & 1 & m_1 \\ 1 & 0 & m_2 \\ 1 & 1 & m_3 \\ \end{array} $$

卡诺图

$$ \begin{array}{| c c | c | c |} & X_1 & 0 & 1 \\ X_2 & & & \ \hline 0 & & m_0 & m_2 \\ 1 & & m_1 & m_3 \end{array} $$

这里设 $m_0 = 1, m_1 = 1, m_2 = 0, m_3 = 1$

$$ \begin{array}{| c c | c | c |} & X_1 & 0 & 1 \\ X_2 & & & \ \hline 0 & & 1 & 0 \\ 1 & & 1 & 1 \end{array} $$

分析:由上图可以发现,$X_1 = 0$对应的列全为$1$,$X_2 = 1$对应的行全为$1$,直接可以得出

$$ X = \overline{X_1} + X_2 $$

三变量卡诺图

真值表

$$ \begin{array}{| c | c | c | c |} X_1 & X_2 & X_3 & X \ \hline 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 1 \\ 0 & 1 & 0 & 0 \\ 0 & 1 & 1 & 0 \\ 1 & 0 & 0 & 1 \\ 1 & 0 & 1 & 1 \\ 1 & 1 & 0 & 1 \\ 1 & 1 & 1 & 0 \end{array} $$

卡诺图,三维表格

$$ \begin{array}{| c c | c | c | c | c |} & X_1X_2 & 00 & 01 & 11 & 10 \\ X_3 & & & & & \ \hline 0 & & 0 & 0 & 1 & 1 \\ 1 & & 1 & 0 & 0 & 1 \end{array} $$

这里$X_1X_2$需要使用格雷码

分析:由上图,可以把表想象成可以卷起来的圆筒,$X_1X_2$行构成循环,所以$X_1X_2 = 00,X_3 = 1$以及$X_1X_2 = 10 ,X_3 = 1$组合也要考虑

$$ X = X_1\overline{X_3} + \overline{X_2}X_3 $$

卡诺图优化一般规律总结

找到卡诺图里面相邻$2^n(2, 4, 8, 16 \cdots)$个都是1的格子,可以合并成一个项,在拐角处相交重合的也可以优化,正方形的也可以优化成一个项,总之基本原则就是使用尽量少的优化次数优化尽量多的项,并且一次优化的数量在允许范围内要尽可能多,以减少单个项的元素数,例如在有正方形存在情况下就不要使用相邻两个优化了

再举例:四变量卡诺图

四维表格

$$ \begin{array}{| c c | c | c | c | c |} & X_1X_2 & 00 & 01 & 11 & 10 \\ X_3X_4 & & & \ \hline 00 & & 1 & 0 & 0 & 1 \\ 01 & & 0 & 0 & 0 & 0 \\ 11 & & 1 & 1 & 1 & 0 \\ 10 & & 1 & 1 & 0 & 1 \end{array} $$

通过观察

$$ X = \overline{X_1}X_3 + \overline{X_2X_4} + X_2X_3X_4 $$

扩展:五变量卡诺图

五维表格,可以使用两张四维表格表示

$X_5 = 0$

$$ \begin{array}{| c c | c | c | c | c |} & X_1X_2 & 00 & 01 & 11 & 10 \\ X_3X_4 & & & & & \ \hline 00 & & 0 & 0 & 0 & 0 \\ 01 & & 0 & 0 & 1 & 1 \\ 11 & & 1 & 1 & 0 & 0 \\ 10 & & 1 & 1 & 0 & 0 \end{array} $$

$X_5 = 1$

$$ \begin{array}{| c c | c | c | c | c |} & X_1X_2 & 00 & 01 & 11 & 10 \\ X_3X_4 & & & & & \ \hline 00 & & 0 & 0 & 0 & 1 \\ 01 & & 0 & 0 & 1 & 1 \\ 11 & & 1 & 1 & 0 & 0 \\ 10 & & 1 & 1 & 0 & 0 \end{array} $$

通过观察,得出如下结果

$$ X = \overline{X_1}X_3 + X_1\overline{X_3}X_4 + X1\overline{X_2X_3}X_5 $$

卡诺图法小记

卡诺图法只适用于少量变量的情况

介绍几个概念:

蕴含项:可以使函数输出真值$f = 1$的项

质蕴含项:不可再次合并为项数更少的项

本质蕴含项:不可缺少,必须包含的质蕴含项,其他可有可无的为非本质蕴含项

覆盖:指一个蕴含项的集合能够说明函数输出$f = 1$的所有情况

成本:假设求反成本为零,门电路数与输入端数之和

在优化中,要在可能的蕴含项集合中凑出相比其他大部分方案更节约成本的方案,因为可能的解法一般远远不止一种,而且这些方案成本一般会不同

有时候会碰到根本不存在本质蕴含项的情况,这种情况只能逐个方案计算比较(一般如果图中所有蕴含项排列正好形成一个闭环首尾相接,就会出现这种情况)

补充:非完全指定函数

有时候一个逻辑电路的所有输入情况并不会全部出现,这种情况下就会形成非完全指定函数,如下例

$$ \begin{array}{| c c | c | c | c | c |} & X_1X_2 & 00 & 01 & 11 & 10 \\ X_3X_4 & & & & & \ \hline 00 & & 0 & 1 & d & 0 \\ 01 & & 0 & 1 & d & 0 \\ 11 & & 0 & 0 & d & 0 \\ 10 & & 1 & 1 & d & 1 \end{array} $$

其中,$d$代表的输入永远不会出现。这样,可以把部分$d$补为$1$,以实现最简电路

$$ \begin{array}{| c c | c | c | c | c |} & X_1X_2 & 00 & 01 & 11 & 10 \\ X_3X_4 & & & & & \ \hline 00 & & 0 & 1 & 1 & 0 \\ 01 & & 0 & 1 & 1 & 0 \\ 11 & & 0 & 0 & d & 0 \\ 10 & & 1 & 1 & 1 & 1 \end{array} $$

最后得出以下结果

$$ X = X_2\overline{X_3} + X_3\overline{X_4} $$

补充:多输出电路

很多时候要求一个电路输出端不止一个,这时候就要求设计共用尽量多的电路和晶体管,如下示例

$f_1$定义

$$ \begin{array}{| c c | c | c | c | c |} & X_1X_2 & 00 & 01 & 11 & 10 \\ X_3X_4 & & & & & \ \hline 00 & & & & & \\ 01 & & 1 & 1 & 1 & \\ 11 & & 1 & 1 & 1 & \\ 10 & & & 1 & & \end{array} $$

$f_2$定义

$$ \begin{array}{| c c | c | c | c | c |} & X_1X_2 & 00 & 01 & 11 & 10 \\ X_3X_4 & & & & & \ \hline 00 & & & & & \\ 01 & & 1 & & 1 & 1 \\ 11 & & 1 & & 1 & 1 \\ 10 & & & 1 & & \end{array} $$

得到

$$ f_1 = X_1X_2X_4 + \overline{X_1}X_2X_3\overline{X_4} + \overline{X_1}X_4 $$

$$ f_2 = X_1X_2X_4 + \overline{X_1}X_2X_3\overline{X_4} + \overline{X_2}X_4 $$

可以达到共用的目的

补充:多级综合问题

有时候当一个电路的输入端过多时,需要解决数字电路的扇入问题

在一般的逻辑电路中,与门输入端一般不超过8个,FPGA中单个LUT的输入也是相当有限的,所以要将多输入的逻辑函数分解成多级以适应具体的电路限制

示例:

$$ f = X_1X_3\overline{X_6} + X_1X_4X_5\overline{X_6} + X_2X_3X_7 + X_2X_4X_5X_7 $$

提取公因式

$$ f = X_1\overline{X_6}(X_3 + X_4X_5) + X_2X_7(X_3 + X_4X_5) $$

可以用于2输入端逻辑单元的FPGA

补充:子函数分解

有时候可以将某些部分看作一个完整的子函数,可以大大降低电路成本

示例:

$$ \begin{align*} f &= \overline{X_1}X_2X_3 + X_1\overline{X_2}X_3 + X_1X_2X_4 + \overline{X_1} \cdot \overline{X_2} \cdot X_4 \\ \Rightarrow f &= (\overline{X_1}X_2 + X_1\overline{X_2})X_3 + (X_1X_2 + \overline{X_1} \cdot \overline{X_2})X_4 \end{align*} $$

已知

$$ \begin{align*} \overline{X_1}X_2 + X_1\overline{X_2} &= \overline{\overline{(\overline{X_1}X_2)} \cdot \overline{(X_1\overline{X_2})}} \\ &= \overline{(X_1 + \overline{X_2}) \cdot (\overline{X_1} + X_2)} \\ &= \overline{0 + X_1X_2 + \overline{X_1} \cdot \overline{X_2} + 0} \\ &= \overline{X_1X_2 + \overline{X_1} \cdot \overline{X_2}} \end{align*} $$

可以将$\overline{X_1}X_2 + X_1\overline{X_2}$看作一个子函数$g(X_1, X_2)$

那么

$$ f = g(X_1, X_2) \cdot X_3 + \overline{g(X_1, X_2)} \cdot X_4 $$

1.4.4 列表法/立方体表示法简介

所谓立方体实际是一个抽象概念

设想这样一个由3位格雷码生成的立方体

图片取自网络

所有边连接的两个顶点都只有一个比特不同,一条边代表有两个比特相同的项,比如连接$000, 010$的边代表项$\overline{X_1} \cdot \overline{X_3}$,可以表示为$0X0$。相似的,一个面$000, 100, 010, 110$代表项$\overline{X_3}$,可以表示为$XX0$。立方体中每个顶点都和另外3个顶点相连,这另外3个顶点正好就只差一个比特。所谓立方体就是指的这种互联结构。这种互联结构可以看作多维立方体($2^n$个顶点),内部包含低于其维度的子立方体。

要找出一个电路的优化,首先就要找出所有使输出$f = 1$的边。

立方体的合并:两个小立方体可以合并成为一个大立方体。例如:$0XX1X$和$0XX0X$可以合并为$0XXXX$

1.4.5 列表法化简

设$f = \sum m(0, 4, 8, 10, 11, 12, 13, 15)$

可以列表如下

$$ \begin{array}{| c | c |} m & XXXX \ \hline 0 & 0 0 0 0 \\ 4 & 0 1 0 0 \\ 8 & 1 0 0 0 \\ 10 & 1 0 1 0 \\ 12 & 1 1 0 0 \\ 11 & 1 0 1 1 \\ 13 & 1 1 0 1 \\ 15 & 1 1 1 1 \\ \end{array} $$

最终合并如下

$$ \begin{array}{| c | c |} m & XXXX \ \hline 8,10 & 1 0 X 0 \\ 10,11 & 1 0 1 X \\ 12,13 & 1 1 0 X \\ 11,15 & 1 X 1 1 \\ 13,15 & 1 1 X 1 \\ \end{array} $$

$$ \begin{array}{| c | c |} m & XXXX \ \hline 0,4,8,12 & X X 0 0 \\ \end{array} $$

设$P = {10X0, 101X, 110X, 1X11, 11X1, XX00} = {p_1, p_2, p_3, p_4, p_5, p_6}$,可以列出蕴含列表如下

$$ \begin{array}{| c | c | c | c | c | c | c | c | c |} P & 0 & 4 & 8 & 10 & 11 & 12 & 13 & 15 \ \hline p_1 & & & \sqrt{} & \sqrt{} & & & & \ \hline p_2 & & & & \sqrt{} & \sqrt{} & & & \ \hline p_3 & & & & & & \sqrt{} & \sqrt{} & \ \hline p_4 & & & & & \sqrt{} & & & \sqrt{} \ \hline p_5 & & & & & & & \sqrt{} & \sqrt{} \ \hline p_6 & \sqrt{} & \sqrt{} & \sqrt{} & & & \sqrt{} & & \ \hline \end{array} $$

由上表,$p_6$唯一覆盖了$0$和$4$,必须包含

我们去掉$p_6$行以及对应的$0,4,8,12$列(同时将$p_6$标记为必须包含的项),得到下表

$$ \begin{array}{| c | c | c | c | c |} P & 10 & 11 & 13 & 15 \ \hline p_1 & \sqrt{} & & & \ \hline p_2 & \sqrt{} & \sqrt{} & & \ \hline p_3 & & & \sqrt{} & \ \hline p_4 & & \sqrt{} & & \sqrt{} \ \hline p_5 & & & \sqrt{} & \sqrt{} \ \hline \end{array} $$

支配行:由上表可以看到$p_2$包含了$p_1$的情况,称为$p_2$支配$p_1$,并且$p_2$和$p_1$实现成本相同,因此可以去掉$p_1$(如果$p_2$实现成本高于$p_1$则不可删除),同理可以去掉$p_3$得到下表

另外还有支配列的概念,和支配行类似,不同的是支配列去掉的是支配列而不是被支配列,好理解

$$ \begin{array}{| c | c | c | c | c |} P & 10 & 11 & 13 & 15 \ \hline p_2 & \sqrt{} & \sqrt{} & & \ \hline p_4 & & \sqrt{} & & \sqrt{} \ \hline p_5 & & & \sqrt{} & \sqrt{} \ \hline \end{array} $$

发现$p_2$和$p_5$不可删除,$p_4$包含在$p_2$和$p_5$里面,也要去掉

所以最终覆盖$C = {p_2, p_5, p_6} = {101X, 11X1, XX00}$

列表法算法小结

  1. 首先根据真值表列出所有$f = 1$的最小项,使用立方体比较产生$f$的所有质蕴含项
  2. 列出质蕴涵项覆盖表
  3. 将当前已经发现的本质蕴含项包含到最终覆盖表并去除对应行和列
  4. 使用行支配以及列支配进一步化简,注意先比较成本再决定是否删去
  5. 重复3至4,直到覆盖表变空或不可化简
  6. 若化简后不为空,用分支法确定哪些项应该包含在最终覆盖表中(一个一个算并对比)

列表法局限性:一般逻辑函数很少以最小项形式给出,而是以代数表达式或立方体集合给出。这样就需要先将代数表达式和立方体集合扩展成为最小项形式。这样的覆盖表会变得非常大,立方体简化也会异常复杂,算法速度很慢

1.4.6 立方体表示法化简

这种算法复杂度依然非常高。有兴趣可以了解一下加州大学伯克利分校的Espresso,使用启发式的算法

这些算法一般会在EDA里面集成用于电路的优化,一般用户不用考虑这些算法,了解一下就可以(除非你要开发一个EDA

实际应用中,EDA一般得到的用户输入都是蕴含项之和的形式,这些蕴含项不一定是最小项或者质蕴含项。

这里定义星积*)运算:

星积运算一般用于寻找质蕴涵项,运算结果如果是维数大的立方体则保留,维数相同看包含关系决定是否删除(其实就是看生成的立方体是否被原来的包含)

设$A,B$为n变量函数中的两个蕴含项

$$ A = A_1A_2 \dots A_n \\ B = B_1B_2 \dots B_n $$

$A_i$或$B_i$可以为$0,1,x$

设$C = A * B$

*运算规律如下

$$ \begin{array}{| c c | c | c | c |} & B_i & 0 & 1 & x \\ A_i & & & & \ \hline 0 & & 0 & \varnothing & 0 \\ 1 & & \varnothing & 1 & 1 \\ x & & 0 & 1 & x \\ \end{array} $$

生成$C$规则:

  1. 若存在多于一个$i$使得$A_i * B_i = \varnothing$,则$C = \varnothing$
  2. 以上条件不满足,则当$A_i * B_i = \varnothing$时$C_i = x$;$A_i * B_i \neq \varnothing$时$C_i = A_i * B_i$

计算$f$的质蕴涵项步骤

  1. 设$C^k$为$f$的一个覆盖,对$C^k$中所有项两两配对进行*运算后生成$G^{k+1}$
  2. 设$C^{k+1} = C^k \cup G^{k+1} - R$($R$为冗余立方体。冗余立方体定义:若$A$被$B$所包含,则去掉$A$,比如$00X1$和$00XX$应该去掉$00X1$)
  3. 重复以上运算直到$C^{k+1} = C^k$
  4. 最终的$C$就是质蕴含项集合

再定义一个#)运算:

飒运算用于确定本质蕴含项,直接功能是用来判断一个立方体是否包含另一个立方体

#运算规律如下,该运算具有反对称性,不支持交换律。$\epsilon$大概表示覆盖的意思,好理解

$$ \begin{array}{| c c | c | c | c |} & B_i & 0 & 1 & x \\ A_i & & & & \ \hline 0 & & \epsilon & \varnothing & \epsilon \\ 1 & & \varnothing & \epsilon & \epsilon \\ x & & 1 & 0 & \epsilon \\ \end{array} $$

生成$C$规则:

  1. 若存在$A_i # B_i = \varnothing$,则$C = A$(不相交)
  2. 若所有$Ai # Bi = \epsilon$,则$C = \varnothing$(此时$B$完全覆盖$A$)
  3. 除以上情况,如果是部分覆盖可能生成一个或多个立方体,即对于每个出现的$A_i = x$,$B_i = 0$或$1$的情况,都会生成一个$C = \cup_i(A_1, A_2, \dots \overline{B_i} \dots, A_n)$(此时$B$部分覆盖$A$)。如果生成多个立方体,那么这所有的立方体都要分别进行剩下的运算

获取本质蕴含项步骤

  1. 直接套公式:如果$p^i # (P - p^i) # DC \neq \varnothing$成立,那么$p^i$就是本质蕴含项

其中,$P$为质蕴含项总集合(由之前的星积运算得出),$p^i$为其中一个质蕴含项,$DC$为无关项总集合(即非完全指定函数中真值为$d$的项的集合)

使用飒运算的方法举例

设通过星运算得出的质蕴含项集合$P = {X01X, X101, 01X1, 0X1X, XX10}$

解:

没有无关项集合$DC$,首先拿$X01X$运算,$X01X # X101 = X01X, X01X # 01X1 = X01X, X01X # 0X1X = 101X,101X # XX10 = 1011$

则最终$X01X # (P - X01X) = 1011 \neq \varnothing$

其他$X101 # (P - X101) = 1101 \neq \varnothing, 01X1 # (P - 01X1) = \varnothing, 0X1X # ( P - 0X1X ) = \varnothing, XX10 # ( P - XX10 ) = 1110 \neq \varnothing$

最终求得本质蕴含项为$X01X, X101, XX10$

总结:使用星运算和飒运算求解最小覆盖

函数的最初覆盖为$DC$(无关项的集合)和$ON$(使$f=1$的集合)的并集

  1. 设初始覆盖$C^0 = DC \cup ON$
  2. 应用星积运算求出所有质蕴含项,形成集合$P$
  3. 应用星积运算求出所有本质蕴含项。若本质蕴含项包含了开集$ON$,则这个本质蕴含项构成最小化覆盖
  4. 如果质蕴含项$p^i$成本比$p^j$高,并且$p^i # DC # p^j = \varnothing$,那么要把$p^i$删掉
  5. 找出覆盖所有剩余顶点成本最低的质蕴含项,如果找出质蕴含项成本相同,则使用启发式分支算法求出最终解

1.5 同步时序电路设计与分析

时序电路就是硬件实现的有限状态机。由于异步时序设计的复杂性,一般同一模块的电路都使用同步时序设计,而在使用不相关时钟的同步电路之间会涉及到异步时序设计

同步时序电路由基本组合逻辑电路以及触发器组成,使用时钟信号控制,如下

同步时序电路基本框图

$W$为输入端,$Z$为输出端。电路的状态可以取决于当前触发器的输出$Q$(反馈至输入端)以及电路的输入$W$。其中触发器应当是边沿触发器,而非普通的D触发器。

电路输出$Z$仅取决于当前电路状态的电路称为Moore型电路,同时取决于当前状态以及主输入的称为Mealy型电路

1.5.1 Moore型电路基本设计步骤

设想要设计这样一个电路,在连续两个时钟输入1后输出会变成1,一旦输入变为0之后输出变回0。(这样的电路被称为序列检测器)

输出状态变化如下图所示,输出取决于之前的输入

输出状态表

设计这样的电路涉及到状态机理论。可以选择一个状态作为初始态,比如上电复位态。

由以上分析,该状态机需要3个状态:上电且输入$W$为$0$,设为状态$A$;一旦输入$1$,转为状态$B$;再次输入$1$,转到状态$C$,$Z$输出$1$;一旦输入$0$,就转回$A$,$Z$输出$0$。

假设这里先使用D触发器的方案,使用其他触发器的方案之后讨论

可以得到状态转移图如下图

状态转移图

另外注意异步Reset也可以将所有状态强制复位到$A$,这里省略。

将以上状态转移图转化成为状态转移表如下

状态转移表

基本的设计方法如下:

先进行状态赋值

由于只要实现3个状态,使用2个D触发器(状态变量)即可,输出分别为$y_1$和$y_2$。(最多可以表示4种状态)基本框图如下:

框图

其中,输出$Z$只和当前电路状态有关(Moore型电路特性),是$y_1$、$y_2$的函数。$y_1$和$y_2$的输出反馈至输入一端,和输入$W$共同决定下一状态。可以列出状态赋值表如下图:

状态赋值表

其中,$y_1 = y_2 = 1$的情况未被使用到,可以设为任意量利于后续电路简化。根据输入$y_1$、$y_2$、$W$以及输出下一状态$Y_1$、$Y_2$列表。

接下来就可以使用之前在组合逻辑部分讲述的方法构建并简化逻辑表达式,其中$d$表示可以使用随意真值

逻辑表达式

最终得到的电路如下图所示

最终电路

波形图如下,注意观察$W$是在下一次变化周期到来之前被采样,所以看上去好像$y_1$和$y_2$是在$W$之后一个周期发生变化,$y_1$和$y_2$在$W$被采样后的下一个周期内才变换到上一个周期采样应当变换到的状态,有一个时钟的延迟,之后会讲述时钟同步问题

波形图

补充:最优状态赋值

获得最优状态赋值没有特殊方法,一般只能暴力穷解,如上例,重新赋值如下,可以得到更简的电路

状态赋值表

对于大规模电路来说,基本无法寻找一种最优的赋值(是个NP完全问题),一般EDA厂商使用专用的启发式算法,不公开

补充:使用一位有效编码(独热码)

以上方案都使用了二进制编码赋值的方案,优点是使用的触发器比较少。这里介绍一种独热码的方案,使用和状态数相同数量的触发器,这种方案很多时候虽然不是最优,但是很多时候还是有优势的,将上例更改如下

独热码状态赋值

可以发现没有用到$y_2$,EDA会自动检测并删除

有时候使用独热码可以使表达式更简单

1.5.2 Mealy型电路设计步骤

Mealy型电路输出和当前电路状态以及当前电路的输入有关

对以上例子进行更改,现在要求状态机在第一次$W=1$时由$A$状态转到$B$状态,之后若$W$保持为$1$,输出$Z$立即变为$1$,相当于将总的输出时序提前一个周期,在第一次输入$W=1$之后第二次采样到来之前(此时$W=1$)$Z$即输出$1$

这样状态机只需要两种状态,如下图

状态转换图

注意在这里输出$Z$被标在了弧线上,而不是状态上,$Z$需要由当前$W$的状态决定

这个电路和之前的电路最主要的区别就是输出同时取决于输入,所以列出的状态表输出Z会有$W=1$和$W=0$之分,它的状态表以及状态赋值表可以表示为如下

状态赋值表

设计出的电路如下图

Mealy电路

Mealy型电路有更好的灵活性,可以加一个D触发器转换成为如下的Moore型电路

转换成的Moore型电路

这个电路很熟悉,它就是上面状态赋值中得到的优化电路

Mealy型电路不一定更简,但是由于消除了延迟的一个周期,所以执行同样的操作会比Moore型电路短一个周期。

1.5.3 状态最小化

一般设计电路时使用的状态数都会多于最后实际使用的状态数,对状态数进行最小化有利于简化电路

如果一个有限状态机状态可以减少,那么这些状态里面一定有一些状态对外在表现的贡献是等价的

这里先引入两个定义

定义1:如果对于状态$S_i$和$S_j$,任何输入序列都产生相同的输出序列,则这两种状态称为等价

如果$S_i$通过$w=1$到达$S_u$,则称$S_u$为$S_i$的1继承(1-successor),如果$S_j$通过$w=0$到达$S_v$,则称$S_v$为$S_j$的0继承(0-successor)。如果输入有多种状态(比如$w=10$),则称为10继承

定义2:一个划分可以由一个或者多个块构成,状态中每个块包含一个状态子集,这些状态可能等价,但是给定块中的状态和其他状态不可以等价

一个正则表达式,可以先转换为NFA后转换为DFA,再对DFA进行最小化。电路的状态最小化其实和DFA最小化过程非常相似,基本是相同的原理。都是状态机

这里直接拿实例讲解最小化过程,一个单一变量输入的有限状态机化简之前的状态表如下

状态转移表

有限状态机的最小化需要根据输出将状态分成两大组

$P_1 = (ABCDEFG)$

$P_2 = (ABD)(CEFG)$

分成$ABD$和$CEFG$两组

分析,$(ABD)$的1继承为$(CFG)$,0继承为$(BDB)$;$(CEFG)$的1继承为$(ECDG)$,0继承为$(FFEF)$。由于$(ABD)$的1继承全部映射到$(CEFG)$以内,而0继承全部映射到$(ABD)$以内,所以$(ABD)$暂时不拆。而$(CEFG)$的1继承映射到$(ABD)$和$(CEFG)$,0继承映射到$(CEFG)$,显然$(CEFG)$需要拆开,拆成$(CEG)(F)$。这样就需要继续重新检查拆解

$P_3 = (ABD)(CEG)(F)$

$P_4 = (AD)(B)(CEG)(F)$

$P_5 = (AD)(B)(CEG)(F)$无法继续拆解

所以最终将$(AD)$设为$A$,$(B)$设为$B$,$(CEG)$设为$C$,$(F)$设为$F$。(习惯上取第一个字母,随便命名也可以)

最终得到最小化的状态转移表如下

最简状态转移

1.5.4 同步时序电路设计小结

  1. 首先进行要求分析
  2. 推导状态机所有状态,创建状态图
  3. 将状态图转换成为状态表
  4. 对状态数量进行最小化,得到最简推导结果
  5. 对需要的状态进行赋值,注意赋值方案也有最简,要使用最简的
  6. 选择要使用的触发器类型,推导下一状态的逻辑表达式
  7. 实现电路设计

补充:同步时序电路的分析

分析一个同步时序电路其实基本就是设计的逆过程,可以先根据电路列出逻辑表达式,推导出状态赋值表,如下例

电路图以及逻辑表达式

赋值状态表

赋值状态表

基于JK触发器的电路的分析同理

补充:算法状态机图(FSM)

算法状态机图和普通的流程图相似,用于较为复杂的电路,这里不做描述了

1.6 异步时序电路设计与分析

异步时序电路状态不由时钟脉冲触发,而是由电路各输入端为$0$或$1$决定,不使用触发器表示电路状态

定义:基本模式 是指异步时序电路所有输入同时只有一个发生变化,在两个单次变化之间保证有足够的时间可以使电路达到稳态。这也是异步电路正常工作的必要条件(一个电路在特定输入下如果会产生无法预测的行为,是非常糟糕的)

异步时序电路的特征之一就是在有两个输入同时发生变化时可能产生不确定的结果,一个典型的栗子就是RS触发器的RS输入同时从高电平转为低电平时

1.6.1 异步行为

如下,可以对RS触发器进行分析(这里只取输出其中一端分析,实际当中最好对所有反馈回环进行分析)

RS触发器异步行为分析

设两个或非门都是理想的没有延迟,取RS触发器的$Q$进行分析,在反馈回环添加一个断点,断点前后延迟就是两个逻辑门的等价延迟,设当前输出为$y$,输入改变后经过$\Delta$时间以后输出下一状态$Y$

假设当前$Q$输出$y=0$,输入$S=R=0$;此时若$R$变为$1$,$S$保持$0$,输出$Q$不变;接下来$S$也变为$1$,输出依然不变(此时电路状态其实是有变化的,或非回环的另一部分变为$0$);此时若$R$再变回$0$,输出$Q$会发生变化,此时对应的$Y=1 \neq y$,电路处于非稳态,经过$\Delta$延时以后$y$变为$1$

观察上表,表中列出了在$y$当前输出为$0$和$1$时,输入$SR$后对应的期望输出$Y$。其中,若期望输出$Y$和当前$y$相同,称电路处于稳态,用圆圈圈出;其余会发生变化的称为非稳态

由上表推导,可以得出$Y=\overline{R} \cdot (S+y)$

可以得出状态转移表和状态转移图如下

状态转移表以及图

其中,设输出$0$为状态$A$,$1$为状态$B$,该电路为Moore型有限状态机

和同步时序电路不同,异步时序电路不使用触发器表示状态,而是直接将逻辑门连接,一般在电路中会形成回环

可以发现,异步电路综合的方法和同步电路还是有相似之处的

下来换一种分析模型,换成Mealy模型,状态表和状态转移图如下

Mealy模型分析

上图中,和之前同步电路相同,输出$Q$对于每个输入$SR$都有对应的值,但是在状态发生变化时,例如从$00$变为$10$,会发生状态的改变,转变为$B$。但是根据Mealy型电路的定义,Mealy型电路的输出同时取决于输入和电路状态,也就是说输出和输入之间有直接的逻辑门连接。而在这里输出直接取决于电路的状态,无需再将输入和输出使用逻辑门直接连接。在这里应该将其标记为$-$,可以任意赋值$0$或$1$,以简化电路

术语补充

流程表:相当于异步电路的状态表

状态转换表/激励表:相当于异步电路的状态赋值表

1.6.2 异步电路分析

直接使用例子讲述(可以跳过例1例2直接看例3)

例1:对一个D触发器进行分析

D触发器电路

$Y = CD + \overline{C}y + Dy = CD + \overline{C}y$(补充吸收律)(注意,在这里$Dy$的存在与否形成的电路并不是等价的,这里存在冒险竞争的问题,在之后章节会解释。该项被称为冗余项,会影响到电路的冒险竞争行为。这个示例去除了该冗余项)

使用以上式子对所有输入状态进行分析,可以得出激励表和流程表如下

激励表和流程表

例2:对主从D触发器进行分析,使用分析异步电路的方法分析同步电路

主从D触发器

在上图中,$Y_m = CD + \overline{C}y_m (+ Dy_m), Y_s = \overline{C}y_m + Cy_s (+ y_m y_s)$(最后一项都是冗余项)

其中$m$和$s$分别代表触发器主级和从级,将这4个状态设为$S_1 S_2 S_3 S_4$,可以用这两个变量构建激励表和流程表

激励表和状态流程表

解析:注意看上图,绘制了两张流程表,一张带不定项。其中第一张流程表是直接将式子代入求出来的

但是由于异步电路的基本工作条件,所有输入在同一时刻只能有一个发生变化。这样我们需要重新审视一下这个流程表之后被设为未定项的位置,可以发现在$S_2$状态时,输入一定是$C=1$,$D=0$,而与此对应的$CD=01$项就会导致$C$和$D$同时发生变化,变化结果是未确定的(也可以说这种状态转移在电路中是不可能发生的,这种情况在异步电路中是普遍的)。所以要将第二张流程表中这两项设为未定项(不可能发生的转移)

可以得出以下状态转移图

状态转移图

而在最后可以使用最上面的激励表反推逻辑表达式,可以得到最初的带冗余项的逻辑表达式

例3:再拿一个售货机电路分析,通过这个例子可以更加明确的理解异步电路的分析过程,前面两个例子随便看看就行

售货机电路

以上为售货机电路以及其状态逻辑表达式。可以直接得到激励表和流程表如下

激励表

在上图中,和之前一样,所有状态不变的项(稳态)都已经圈出。而最后带不定项的流程表如下

带不定项的流程表

解析:如果使用之前两例的思维方式分析,比如在$A$状态下稳态为$w_2w_1=00$,那么根据异步电路每次只能有一个输入变化的原理,在$A$状态下不能输入$w_2w_1=11$,也就是$w_2w_1=11$项需要设为未定,这没有问题。但是如果拿$D$状态为例,按理说$w_2w_1=11$的状态也要设为未定,但是这里却没有这么做,这有点令人疑惑

其实我们可以从$B$状态的$w_2w_1=11$输入入手。这时状态会从$B$跳到$D$状态,正好对应$D$的$w_2w_1=11$输入,而这又是一个不稳定的状态,状态又会瞬间从$D$跳到$C$对应的$w_2w_1=11$输入态。这种中间状态现象被称为不稳定态。在此例中,起始态$B$输出$0$,中间态$D$输出$0$,终态输出$1$,所以这样的中间态是可以接受的,可以保留,因为它并不会产生不想要的输出。但是假设在另一种情况下,比如起始态输出$0$,中间态输出$1$,终态输出$0$,那这种状态转移就需要尽量避免,需要舍去,因为这种状态会产生瞬时的信号毛刺,这在实际电路中会导致意外状况的发生。电路设计还是尽量严格遵循规范。当然,一次状态跳变中可能经历不止一个中间状态

最终得出的状态机图如下

最终状态机图

总结:异步电路的分析步骤

  1. 将电路内每一个反馈回路切断,加入延迟元件以及观测变量。变量数量最好最小化,不要重复切割
  2. 建立逻辑表达式
  3. 创建激励表
  4. 创建并优化流程表
  5. 根据流程表绘制相应状态图

1.6.3 异步电路的综合(设计)

异步电路设计步骤

  1. 根据需求画出状态机图
  2. 推导出流程表,并尽量减少状态个数
  3. 分配状态,得出激励表
  4. 求逻辑表达式
  5. 构建电路

构建电路时,需要确保电路在各稳态下可以产生正确的输出。如果有不稳定态,就需要保证不稳定态不会产生意外的输出

这里同样直接使用示例进行说明

例1:设计一个串行奇偶校验发生器

解析:首先设输入为$w$,输出为$z$。若输入的脉冲数为偶数那么输出为$0$,如果为奇数那么输出为$1$

所谓脉冲,就是完整的一次从低电平跳转到高电平再到低电平的过程。可以构建状态图以及相应流程表如下

状态图和流程表

分析上图,一共定义了4个状态。设初态为$A$,输出$0$,在输入变为$1$之后立即进入状态$B$,代表输入奇数个脉冲,同时输出变为$1$。此时再次输入$0$,进入状态$C$,代表奇数个脉冲输入完成,输出不变。再次输入$1$,进入状态$D$,此时代表输入了偶数个脉冲,同时输出变为$0$。最后输入$0$,回到状态$A$,输出不变,此时代表输入偶数个脉冲完成

接下来进行状态赋值,注意不是所有的赋值方式都是好的,以下就对比了两种不同的赋值方式(可以很明显的观察到,较好的赋值使用了格雷码,这是格雷码又一重要应用)

赋值得到的激励表

其中第一种赋值不好的原因,拿$D$($y_2y_1=11$)做分析就可以:如果在$D$状态下输入$w=0$,那么状态就需要从当前的$y_2y_1=11$直接跳转到$y_2y_1=00$的状态,这时会出现同时有两个变量变化的情况。而这两个状态变量由于逻辑门的延迟,在实际中一定会有一个变量先发生变化。假设首先发生变化的变量是$y_1$,那么状态会首先跳转到$y_2y_1=10$并停留在C状态稳定下来,显然这并不是我们想要的结果。

所以,在这种环形状态图中,最终的赋值需要使用格雷码(相邻两个状态之间只有一个状态变量发生变化)。这同时也补充了一条异步电路设计的准则:只有一个输入发生变化时,同时只有一个状态变量发生变化

最终得出的逻辑表达式如下,注意这里保留了冗余项:

最终逻辑表达式

依据逻辑表达式构建的电路

电路

例2:设计一个简单的仲裁器

在计算机中,某个设备如果想要请求某个共享设备,就需要向仲裁器提出请求。大概的结构如下

仲裁器框图

解析:上图为控制两个设备共享的仲裁器。仲裁器和每一个设备有一个$Grant$输出和一个$Request$输入连接。当设备需要请求资源时,可以将$Request$信号线拉高,而仲裁器想准许设备使用只需将$Grant$信号线拉高即可。这种异步电路的优势在于不需要时钟,而且响应性能良好

所以可以画出状态图如下

状态图

其中,设仲裁器有两个请求输入$r_2r_1$,两个输出$g_2g_1$。初态为$A$,此时为起始态,输出$g_2g_1=00$。如果这时设备1将$r_1$拉高,那么仲裁器进入$B$状态,输出$g_2g_1=01$,表示准许设备1使用设备。此时若设备2想要使用设备,将$r_2$拉高,那么仲裁器依然会处于$B$状态,直到设备1拉低$r_1$,仲裁器会进入$C$状态输出$g_2g_1=10$准许设备2使用共享设备。

但是这里存在一个很明显的问题:正如上面例1所表示的,异步电路的状态变量同时只能有一个发生变化。而在状态图中,假设当前处于B状态并且输入为$r_2r_1=11$,如果此时输入变为$r_2r_1=10$,那么状态需要从$B$跳转至$C$,但是这也意味着$g_2g_1$必须经历从$01$到$10$的转变。而实际中$g_2g_1$一定有一个比另一个先发生变化。假设$g_1$先变化,也就是说先变为$g_2g_1=00$,跳转到$A$状态,这时输入依然为$r_2r_1=10$,状态立即再跳转到$C$,会经历一个中间态,并且不会产生不期望的输出。而如果是$g_2$先变化,也就是说先变为$g_2g_1=11$(这在实际中是不可能存在的状态,可以看作一个非稳态),那么就需要再创建一个$g_2g_1=11$的非稳态$D$(没有圈)来解决。最终得到的流程表和激励表如下:

流程表和激励表

可以注意到,在$B$状态下,输入$r_2r_1=11$的状态被设为稳态,同时$r_2r_1=10$输入时直接跳转到到$C$状态。而另外添加的$D$非稳态只在$r_2r_1=01$和$10$时有状态赋值,是作为补充,保证可以正常跳转状态

现在再次重新审视一下添加的$D$状态:如果真的添加$D$状态,此时如果从$B$状态跳转到$C$状态中间出现了$g_2g_1=11$的情况,似乎会导致两个设备同时使用资源,但是有设备先释放资源再拉低$Request$这个前提,状态转移之前资源一定已经被释放,所以$D$的加入不会导致两个设备同时使用资源

虽然在这里,多余的状态的加入不会导致异常的出现,但是并不是所有中间状态的加入都是这样。所以在异步电路的设计中如果要添加中间态,一定要注意检查添加的中间态是否有可能导致不想要的行为的发生。虽然这个中间态只是发生在一瞬间,但是它还是确确实实发生了

最终得到的逻辑表达式如下

仲裁器逻辑表达式

化简下一个状态表达式,得到

化简结果

最终得到的电路如下

最终电路

补充1

其实解决状态转移时可能发生的竞争冒险还有一个方法,就是不添加多余状态,直接防止其进入不确定的状态。在这个例子中,可以直接强制其经由$A$状态进行跳转。修改后的流程表、激励表如下

另一种解决方案

补充2

还可以使用Mealy规则设计同样的电路

可以对仲裁器进行分析。从Mealy型电路的构成特征入手,任意时间如果输入$r_2r_1=00$,那么输出一定为$g_2g_1=00$,所以$A$状态其实是可以和$BC$状态合并的,而在输入为$00$时直接输出为$00$,状态变量可以减少为1个

合并之后的Mealy型状态图如下

Mealy型状态图

可以得出激励表和流程表如下

激励表和流程表

最终得到的逻辑表达式如下,这个电路同样需要小心赋值,防止状态变量的竞争

Mealy型逻辑表达式

1.6.4 状态最小化

和同步时序电路的设计一样,异步电路状态的简化同样有利于电路的设计,本章引入一种两个阶段的化简方法

同样使用例子讲解

这里首先引入一个定义:如果一个流程表每一行都有一个稳态,那么这个流程表就被称为基本流程表。

合并的两个阶段:第一阶段和同步电路的状态简化相似,而第二阶段是异步电路简化特有的步骤

例1:设计这样一个售货机电路:每颗糖果为1角,售货机接受5分和1角硬币,多给不找零

由需求分析,设初始状态为$A$,输出$0$。有两个输入$N$和$D$,分别代表掷入5分和1角。由于不可能同时掷入1角和5分,所以$DN=11$是不可能出现的输入,因此$DN=11$输入可以全部设为无关项。掷入一次5分后,状态立即转变到$B$。由于此时一定是输入$DN=00$,可以将$DN=00$输入设为中间态,直接跳转到$D$状态。最后投入5角或1分,状态分别转移到$E$和$F$态,最终售货机输出1放出糖果。此时输入$DN=00$,直接跳转回$A$状态

售货机状态图以及流程表

由以上流程表分析,首先采取第一阶段的简化。首先将输出为$0$或$1$的行合并,得到$P_0=(ABD)(CEF)$,进而推得$P_1=(AD)(B)(CF)(E)$,$P_2=(A)(D)(B)(CF)(E)$,$P_3=P_2$,代表$F$需要和$C$合并,同时需要替换掉所有项。注意合并时要考虑无关项,这里没有碰到,遇到同一列有无关项和确定项时要拆分。最终合并后的流程表如下

合并后的流程表

接下来进入第二阶段,利用无关项对行进行合并

观察流程表,可以发现$C$状态包含了$A$和$E$状态,而其他都存在冲突(所谓冲突就是指在两行的同一列,如果同时非无关项且不同,比如一个为状态$A$一个为状态$E$,则称之为冲突)。所以这里考虑尝试将$C$和$A$或$E$合并为一个状态。

但是观察$A$和$C$的输出,发现$A$和$C$输出不同,说明只能通过Mealy模型实现。所以决定合并$E$和$C$

最终合并以后的流程表如下

最终流程表

上表中,所有$E$都要改成$C$,并且$C$原先的不定项都要按照$E$修改

1.6.5 状态最小化通用步骤:使用合并图

首先引入一个定义

定义1:对于相容(不冲突)的两个状态Si和Sj的每组输入,以下条件之一必须为真

  1. $S_i$和$S_j$有相同后续
  2. $S_i$和$S_j$都是稳定的
  3. $S_i$和$S_j$的后继之一为无关项或两者都是无关项

如果是使用的Moore模型,有一个前提是$S_i$和$S_j$输出相同,这点一定要注意,反之Mealy模型则没有

例1:化简以下流程表

使用Moore模型

例1基本流程表

由上表,可以通过观察得到状态对$(A,H)(B,F)(B,G)(D,E)(F,G)(G,H)$,可以合并

直接观察合并的难度比较高,可以构建合并图如下

合并图

上图中,$BFG$两两相容,可以合并为一个项。而$A$和$H,D,E$同样可以合并。和之前在组合逻辑章节中讲到的化简同理,合并图的化简的基本目标也是最后包含尽量少的状态数

所以最后得到的化简后的流程表如下,原来有稳态的列在合并后都必须为稳态

流程表

使用Mealy模型

使用Mealy模型不需要关注输出,可以直接得到合并图如下

Mealy合并图

例2:化简以下流程表,使用Mealy模型

例2基本流程表

先划分合并等价状态,过程省略

步骤1

直接将上表的FSM转换为Mealy模型,可以得到流程表如下

转Mealy模型

观察上表。可以总结出这样的转化规律:如果从一个状态跳转到另一个状态输出不变,那么对应该项输出$z$设为与前一状态相同;如果前后状态输出不同或为不定状态,那么$z$可以设为不定项

小结:状态化简的基本步骤

  1. 对最初的流程表进行最小化合并等价状态
  2. 建立合并图
  3. 依据尽量减少子集数量的原则合并相容状态
  4. 合并行,得到简化后流程表
  5. 换一种合并相容项方式,看是否可以得到最佳解

1.6.6 状态赋值通用步骤:重标记流程表

异步FSM的状态赋值非常复杂,需要保证同一时刻只有一个位发生改变

定义:汉明距离:汉明距离即两个码之间不同的比特数。比如$1101$和$0110$的汉明距离是$3$

立方体任意两相邻顶点之间的汉明距离都是$1$

理想的状态分配之间所有转移的汉明距离都是$1$

若理想的状态不可能实现,那么就一定要经过未分配状态或非稳态

使用状态转移图(状态邻接图),以及重标记流程表实现。一般有三种技巧,使用未指定项,添加附加状态,以及使用独热码

转移图(邻接图)用于寻找合适的状态分配方案

合适的状态转移图一定可以嵌入到一个$k$维的立方体,所谓的嵌入立方体就是说一个转移图中,不存在连接对角线的边(汉明距离大于等于$2$)

例1:对之前的仲裁器流程图进行重标记,并构建转移图

原先的状态转移图以及添加$D$项后的转移图如下

原始状态转移图

现在对状态表进行重标记

重标记的状态表

重标记状态的基本步骤就是,首先按顺序将所有画圈的稳态标记为$1,2,3,4$等,然后将所有其他非稳态使用对应已标记状态替换(替换规则见小结)

之后可以重新构建转移图如下

重构建的转移图1

上图中,每条线上都标记了状态编号。标记的规律就是,比如直接从$C$到$A$,会到达$A$的稳定状态$1$,那么在相应边上标记$1$,同理如果从$A$到$C$,就应该在边上标记$4$

之前在仲裁器的设计中已经说过,$B$和$C$之间的转换可以通过$A$中转。接下来就可以继续标记,如下图

重构建的转移图2

相比上一张图,这里的转换图使用灰色字体添加了几个状态。由于$C$可能通过$A$到达$B$的稳态$2$,所以这里在$AC$边使用灰色字体添加了$2$以表示这种可能性。黑色字体代表直接的转移,灰色字体代表间接的转移。但是$B$到$C$的过程经过了对角线,不能嵌入到立方体

重构建的转移图3

只要两个状态在重新标记的流程表中有相同的未画圈的标记,那么这两个状态之间就存在状态转移的可能路径

小结:转移图的推导步骤

  1. 首先推导出重新标记后的流程表。经过不稳定状态最终到达稳定状态的转移使用与最终到达的稳定状态等价的数字做标记
  2. 使用顶点表示流程表相应行
  3. 若流程表中任意两个顶点$V_i$和$V_j$在任意列有相同数字,则连接$V_i$和$V_j$
  4. 对于$V_i$和$V_j$有相同数字的每一列,用数字对边进行标记。使用黑色字体标记直接到稳定状态的转移,使用灰色字体标记下一状态为非稳态的项

使用未指定项

使用未指定项的方法非常复杂,最终需要使用Mealy模型解决,随便看看就行

例2:对以下流程表进行状态分配

流程表重新标记

可以先随便分配一下,构建一个状态转移图

状态转移图1

分析以上转移图。可以发现两条对角线都不可以去除,$C$直接到达$A$只有唯一的一条$1$将两者连接,而$D$到$B$只有唯一的$3$连接,所以两条对角线都无法去除

接下来换一种分配方法,构建状态转移图

状态转移图2

分析以上转移图,可以发现两条对角线都可以去除。$AB$可以去除,是因为利用了无关项,将其设为$4$,$A$到$B$就可以经过$D$中转。而$CD$是因为$D$到$C$可以通过$B$中转

根据化简以后的状态转换图,最终将流程表转为Mealy形式,以保证输出正确性

Mealy流程表

流程表的转换,就是根据变化对应的进行处理(一般情况下最好对整张表进行重构建)。由于现在$w_2w_1=01$时,$A$需要通过$D$跳转到$B$,所以相应的$w_2w_1=01$列要表现出来。同理,$w_2w_1=10$时,$C$只能通过$B$跳到$D$,所以相应的$w_2w_1=10$列也要表现出来。

最后是有关于输出$z_2z_1$的填写。填写的一般规律和上面化简章节讲的一样,不同的是这里输出和状态赋值已经不一样了。首先填写稳态(带圈项)的输出,其次根据变化填写其他非稳态项输出(非稳态输出只和始末状态差别有关,在Mealy模型下和非稳态本身应该的输出无关)。拿$A$到$B$的转换为例,中间会经过$D$,而$A$输出$z_2z_1=00$,$B$输出$z_2z_1=01$,如果使用的是Moore模型就会导致$D$输出$z_2z_1=11$,$z_2$出现毛刺,此时就要强制$D$在$w_2w_1=01$时输出$z_2=0$。而在$z_2z_1=10$列,$B$到$D$需要强制$z_1=1$,应当设$B$行$w_2w_1=10$列为$-1$,然而$C$需要通过$B$跳到$D$,所以又要强制$z_2=1$,所以最终$B$行$w_2w_1=10$列为$11$

使用附加状态

可以利用未指定项解决问题的情况其实很少见,更多的情况下需要添加不稳定的附加状态。利用附加非稳态是在转换图边上添加顶点,利用等价状态对则是将一个状态分裂成为两个或多个

例3:(利用附加非稳态)分配以下流程表

流程表重新标记

可以构建转移图如下

状态转换图1

可以发现这个转移图无法简化到可以嵌入到立方体

现在可以添加一个状态变量,使得变量变成3bit变量。此时,假设令$A=000, B=001, C=100, D=010$,那么$(B,D), (B,C), (C,D)$之间的汉明距离就不再是$1$,变成了$2$,需要在边上添加1个顶点,形成如下状态转换图,而这个状态转换图可以嵌入到立方体中

状态转换图2

最终可以重新构建Moore流程表如下。为防止毛刺干扰的发生,可以和例2一样转为Mealy模型处理,这里省略过程

Moore流程表

例4:(利用等价状态对)分配例3中的流程表

将$ABCD$各分为两个状态,添加后缀如下,各自输出和原来一致

状态转换图

分配的规律其实还是比较容易观察出来的。对于有两个数字的边,一般会构建两条平行的,正如上图中的$A_1B_1, A_2B_2$以及$C1D1, C_2D_2$边,并且保证分顶点汉明距离为$1$

于是构建流程表如下

Moore流程表

使用独热码

使用独热码可以降低状态赋值难度,但是会增加电路实现复杂度

例5:(利用独热码)分配例3中的流程表

使用独热码,就要对每一条边都创建一个无竞争的中间状态,比如$A$到$B$的中间态$E$赋值为$0011$

独热码流程表

1.6.7 冒险(Hazard)简介

非理想门电路的传播是有延迟的,由于电路的结构或电路中的传播延迟引起的瞬时脉冲就称为冒险。冒险分为静态冒险动态冒险

1.6.8 静态冒险

静态冒险的一般波形表现如下,在本该保持稳定的输出时出现了毛刺

静态冒险波形

可以观察以下电路

静态冒险电路以及解决方法

上图中,存在冒险的电路逻辑表达式为$f = X_1X_2 + \overline{X_1}X_3$。由于$X_1$取反引入延迟,会导致$p$先于$q$发生变化。在$X_1X_2X_3$同时为$1$时,如果$X_1$变为$0$,那么就会发生静态冒险,$f$输出会出现一个瞬时的负脉冲

由卡诺图分析,可以说明这样的规律:如果卡诺图中有相邻的两个$1$未被乘积项覆盖,那么这样的电路就可能导致静态冒险

解决静态冒险的方法就是覆盖所有相邻的$1$,比如在上图中添加了一个冗余项

静态冒险可能导致异步时序电路进入错误状态。以下为例:

会进入错误状态的电路

如果$Y_m$和$Y_s$的卡诺图都不使用冗余项覆盖,得到的逻辑表达式如下

会导致冒险的表达式

假设这样的情况:设$Y_s=1, Y_m=1, C=D=1$,此时若$C$变为$0$,$Y_s$本该一直保持$1$,但是$C$和$\overline{C}$会在一瞬间变为$00$,导致出现$Y_s=Y_m=0$的情况,并停留在$Y_s=0$的错误状态中。所以引入冗余项后的逻辑表达式如下

避免冒险的表达式

以上表达式避免了冒险,可以防止状态转移时进入错误状态

小结:避免静态冒险的方法

避免静态冒险,基本方法就是要在积之和中包含所有的质蕴含项,但不代表必须包含所有的质蕴含项,比如在有无关项存在的情况下,可以不将无关项计入

1.6.9 动态冒险

动态冒险的一般波形表现如下,在变化状态时出现了毛刺

动态冒险波形

以下电路就存在动态冒险

动态冒险电路

动态冒险一般由电路结构引起。动态冒险出现的一般规律就是,一个信号如果对于某个输出有多条传播路径,那么就会产生动态冒险;单次变化的次数代表了最少的传播路径数。

存在动态冒险的电路一定在某部分存在静态冒险

可以通过限制电路到两级门,并且确保没有静态冒险的方法规避动态冒险

在上例中,就可以将表达式限制到两级以规避动态冒险,如下

两级逻辑表达式

小结:避免动态冒险的方法

在异步时序电路中避免冒险,就要保证产生下一状态的电路无冒险,可以通过限制电路级数并排除静态冒险实现

1.7 其他实用知识补充

下面补充在日常实践中容易遇到的问题,实用的技巧以及解决方案

1.7.1 触发器亚稳态问题

触发器的亚稳态,就是指一个触发器无法在一定时间以内到达一个确定的状态。触发器会输出一些中间电平,这些中间电平无法预测,可能会出现振荡的情况,并且这种中间电平会传递给下一级触发器导致后级触发器也出现亚稳态,如下图中B由于AB时钟的不同步,取到了A触发器的中间输出,导致了B的亚稳态

触发器亚稳态

这里引入一个术语:MTBF,即平均故障间隔时间,是故障率的倒数,指的是下一级D触发器发生故障的时间间隔。

一般通过使用多级D触发器的形式改善亚稳态的问题

2 Verilog HDL开发环境

Windows下基于Quartus Prime的FPGA开发,以及类Unix下的Verilog设计仿真环境,和基本的设计流程

2.1 类Unix下的Verilog编译和仿真环境:使用Verilator或Icarus Verilog,配合gtkwave

ArchLinux下安装:

pacman -S iverilog verilator gtkwave yosys

FreeBSD下通过pkg安装:

pkg install iverilog verilator gtkwave

yosys可以用于RTL的生成

2.1.1 源文件编写与组织

一般一个Verilog工程至少应当包含源代码(包含了设计好的电路,使用.v后缀),Testbench(用于对电路进行仿真测试,调用设计好的电路,使用.v后缀)。仿真以后还会生成波形文件(电路仿真的结果,使用gtkwave查看,有多种格式,见下)

2.1.2 编译以及仿真,使用Icarus Verilog

直接使用示例说明

假设创建了一个源文件innovate.v,一个Testbenchinnovate_tb.v,编译

iverilog -s innovate_tb -o innovate_tb.out innovate.v innovate_tb.v

其中-s指定topmodule,即Testbench中定义的模块名-o指定输出文件名

仿真使用vvp命令。输出文件名在Testbench中定义

vvp -n innovate_tb.out

可以指定输出文件的格式,可以使用.lxt2 .lxt .fst,这些文件和.vcd类似,区别是.lxt2体积小,.lxt速度快,.fst比较新

vvp -n innovate_tb.out -lxt2

2.1.3 查看波形,基于gtkwave

直接使用gtkwave打开.vcd文件(或其他比如.lxt2 .lxt.fst文件)

gtkwave innovate.vcd

2.2 Intel Quartus Prime 使用简介,仿真基于ModelSim

2.2.1 下载

免费版 Quartus Prime Lite 下载页面

另外根据需求下载器件包

2.2.2 启动主界面

启动主界面

左上区块下拉框可以选择查看工程文件

2.2.3 新建工程

File->New Project Wizard

指定工作目录和工程名称,顶层设计名保持和工程名相同,注意工程名不要有数字,否则之后仿真容易出错,可以直接点Finish

新建工程1

器件型号在Assignments->Device选择

2.2.4 添加文件

一般使用Verilog设计一个电路的过程需要一个Verilog源代码文件(.v),以及一个Modelsim波形文件(.vwf)。也可以使用到图形模块化设计图(.bdf),会使用到器件符号文件(.bsf),.bsf文件可以在一个Verilog编辑窗口下通过File->Create/Update->Create Symbol Files for Current File生成

文件新建在File->New,之后文件不会自动保存,需要手动File->Save As

新建文件

右键工程可以添加文件

添加文件

2.2.5 编译仿真

点击蓝色箭头编译

编译

编译之后才能仿真,直接在左上窗口双击打开.vwf波形文件,Quartus会启动Modelsim

首先一定要注意确保Simulation->Simulation Settings中选择的是正确的仿真语言

选择仿真语言

主界面如下

Modelsim主界面

通过Edit->Grid Size和Edit->Set End Time分别设置时间单位和模拟时间

仿真输入输出通过Edit->Insert->Insert Node or Bus设定,可以直接点击Node Finder操作

Node Finder

点击List,再点击中间的箭头可以调节选中的节点

编辑波形文件功能如下,可以选中一个信号的一片区域。可以置0,置1,设为高阻态z,时钟周期信号,总线指定值和随机值

编辑功能

最后可以点击功能仿真或时序仿真,会打开一个新窗口,可以查看仿真结果

3 Verilog HDL语言基础

记录Verilog的语法和设计模式

Verilog的IEEE官方文档IEEE1800-2012

3.1 模块

模块是Verilog中最基本的组成单位。模块声明如下示例,设计一个16位加法器。圆括号内声明输入输出,之后描述电路

module adder (
    input   wire [15:0] in_a,
    input   wire [15:0] in_b,
    output  wire [15:0] out
);

    assign out = in_a + in_b;

endmodule

输入为input,输出为output,双向输入输出为inout

变量比特位使用[MSB:LSB]格式描述

有效变量名格式为[a-zA-Z_][0-9a-zA-Z_-]*

模块实例化

模块实例化调用已定义的模块,以下实例化了一个adder。使用.运算符调用信号,并命名在实例中的名称

adder adder01 (
    .in_a   (adder01_in_a),
    .in_b   (adder01_in_b),
    .out    (adder01_out)
);

3.2 常量

常量的使用和其他一般编程语言有较大不同

逻辑值 描述
0 低电平
1 高电平
x 不定值
z 高阻态,High impedance

常量需要指出进制以及位宽,b为二进制,o为八进制,d为十进制,h为十六进制,示例如下

4'b1100
6'o75
4'd8
8'h4e

使用parameter声明常量

可以使用parameter在同一个地方声明常量,这样更改常量只需要修改一个地方

parameter MSB = 15;

3.3 变量及其类型

Verilog中的变量分为寄存器型和网络型两种。寄存器型可以根据情况生成锁存器或触发器,或其他组合电路。

3.3.1 寄存器型变量声明与赋值

寄存器型变量常用的有3种类型

名称 位宽 符号 描述
reg 1 一般寄存器变量
integer 32 整数
real 64 实数

寄存器型变量可以在initialalways语句中赋值,称为过程赋值。过程赋值又分为阻塞赋值(顺序赋值)和非阻塞赋值(并行赋值)且两者不能在同一个过程块中出现

以下为示例

reg             [15:0]  reg_1;  //16位无符号
reg             [15:0]  reg_2;
reg signed      [15:0]  reg_3;  //16位有符号
reg signed      [15:0]  reg_arr [15:0]; //16*16寄存器阵列
reg_1 <= reg_1 + 1; //并行赋值,若reg_1初始值为0,那么最后reg_1和reg_2都是1
reg_2 <= reg_1 + 1;
reg_1 = reg_1 + 1;  //顺序赋值,reg_1为1,reg_2为2
reg_2 = reg_1 + 1;

需要注意一个语句块中非阻塞赋值同一个左值时的处理方式,先看如下示例

reg_1 <= reg_2 + 1;
reg_1 <= reg_3 + 1;

这个语句块中,我们对同一个左值reg_1进行赋值。如果按照数字电路的常识,这里会出现竞争。然而verilog标准规定这里按照阻塞赋值的方式处理,也就是说最终reg_1永远为reg_3 + 1

3.3.2 网络型变量声明与赋值

网络型变量不持有数据,仅仅描述数据的传输

网络型变量常用的有5种类型

名称 位宽 符号 描述
wire/tri 1 线连接
wor/trior 1 线或
wand/triand 1 线与
tri1 1 带上拉的线
tri0 1 带下拉的线
supply1 1 直连高电平
supply0 1 直连低电平

如果使用了default_nettype指定了默认网络类型,变量可以不声明类型,比如wire等。网络型变量可以在assign语句中赋值,wire同样可以声明符号

以下为示例

wire            [15:0]  wire_1;         //16位无符号
wire            [7:0]   wordl, wordh;

/*  也可以在声明变量时赋值
wire            [7:0] wordl = wire_1 [7:0];
wire            [7:0] wordh = wire_1 [15:8];
*/

assign  wordl = wire_1[7:0];
assign  wordh = wire_2[15:8];

wire型变量有无符号的转换可以使用$signed()$unsigned()更改,这两个属于系统任务

wire    signed  [7:0] wire_s;
wire            [7:0] wire_u;

assign wire_u = $unsigned(wire_s);

由于网络变量可以不加声明直接使用,所以在实际使用中容易出现引用了不存在的变量名却无法检查出错误的问题。所以在RTL设计时最好指定default_nettypenone,而不是wire

`default_nettype none //verilog中使用`作为预处理命令

3.4 运算符

算术运算符

符号 描述 优先级
+ 加法 3
- 减法 3
* 乘法 2
/ 除法 2
% 求余 2

位运算符

符号 描述 优先级
~ 1
& 7
| 8
^ 异或 7
~^ 同或 7
<< 逻辑左移 4
>> 逻辑右移 4
<<< 算数左移 4
>>> 算数右移 4

缩减运算符

符号 描述 优先级
& 1
~& 与非 1
| 1
~| 或非 1
^ 异或 1
~^ 异或非 1

缩减运算符一般用于对多位量的每一个bit进行计算,并输出到一个单bit量中

示例如下

wire    [7:0]   byte_1;
wire    bit_1;

assign bit_1 = |byte_1; //相当于byte_1[7] | byte_1[6] | ... | byte_1[0]

比较运算符

符号 描述 优先级
== 相等 6
!= 不等 6
=== 相等,包括xz 6
!== 不等,包括xz 6
> 大于 5
< 小于 5
>= 大于等于 5
<= 小于等于 5

逻辑运算符

符号 描述 优先级
! 逻辑非 1
|| 逻辑或 9
&& 逻辑与 10

其他运算符

符号 描述 优先级
? : 条件运算 11
{} 拼接 N/A

拼接运算符用于将多个变量合成一个变量

示例如下

wire    [7:0]   byte_0, byte_1, byte_2, byte_3;
wire    [31:0]  word_1 = {byte_0, byte_1, byte_2, byte_3};  //可以合并不同变量
wire    [15:0]  word_2 = {2{byte_0}};   //也可以将变量重复n遍

3.5 条件

条件可以在initialalways中使用

3.5.1 if

示例

if ( a > b ) begin
    a = a + 1;
end else if ( a == b ) begin
    b = b + 1;
end else begin
    a = a + b;
end

3.5.2 case

示例

case (byte_1[7:0])
    8'h00   : begin
        a = a + 1;
    end
    8'he4, 8'hf6    : begin
        a = a + 2;
    end
    default : begin
        a = a + 3;
    end
endcase

3.6 循环

循环同样可以在initialalways中使用

3.6.1 for

示例

for (i = 0; i < 10; i = i + 1) begin
    a = a - 1;
end

3.6.2 while

示例

while (i < 10) begin
    i = i + 1;
end

3.6.3 repeat

repeat可以指定循环次数

示例

repeat (5) begin
    a = a + 1;
end

3.7 always过程块

always过程块是Verilog中用于描述流程最基本的组件

示例1

always @(*) begin
    a = ~a;
end

always @(posedge clk_1 or negedge clk_2) begin
    b = b + 1;
end

always语句中,@()之内填写触发事件。触发事件有常用的有两种类型,一种是posedge,一种是negedge,也可以使用*表示在信号发生任何变化时都执行语句,可以使用or分隔多个触发事件

示例2

always #10 begin
    a = a + 1;
end

always #(STEP / 4) begin
    b = b ^ a;
end

always语句也可以用于在Testbench中定时运行指令,使用#指定运行的时间间隔,单位为电路基本始终

默认行为与无关项

在Verilog中,使用always的语句如果缺少某些输入状态的行为描述,可能会导致锁存器的引入,如下

module mod_1 (
    input   wire    [3:0]   in;
    output  reg     [1:0]   out; 
);

    always  @(*)    begin
        case    (in)
            4'h0 : out = 2'b10;
            4'h1 : out = 2'b11;
        endcase
    end

endmodule

由于缺少一些输入状态的行为描述,所以在输入为其他状态时输出应该不变,此时就会引入异步时序电路,这不是想要的结果

解决的方法就是补全说明或使用default设置默认值,如果是无关项可以设为x

module mod_1 (
    input   wire    [3:0]   in;
    output  reg     [1:0]   out; 
);

    always  @(*)    begin
        case    (in)
            4'h0 : out = 2'b10;
            4'h1 : out = 2'b11;
            default : out = 2'b00;
            // 如果是无关项,可以设为 default : out = 2'bxx;
        endcase
    end

endmodule

示例3

使用Verilog描述一个D触发器如下

module dff (
    input   wire    d_in;
    input   wire    reset_;
    input   wire    clk;
    output  reg     d_out;
);

    always @(posedge clk or negedge reset_) begin
        if (reset_ == 1'b0) begin   //异步复位
            d_out <= 1'b0;
        end else begin
            d_out <= d_in;
        end
    end

endmodule

3.8 Testbench构造

一般Testbench有8个部分构成

  1. 定义Timescale
  2. 引用头文件
  3. 声明测试模块
  4. 定义内部信号
  5. 生成时钟
  6. 实例化要测试的模块
  7. 编写测试用例
  8. 输出波形

3.8.1 Timescale定义

使用timescale宏设定仿真的时间单位和精度

`timescale 1us/1ns

以上设定仿真时间单位为1us,时间精度为1ns。单位时间必须大于等于时间精度

3.8.2 initial语句和延迟语句

initial只在仿真开始时会执行一次

示例

initial begin
    #0 begin        //时刻0执行
        a = a + 1;
    end

    #10 begin       //时刻10执行
        b = b + 1;
    end

    #10 begin       //时刻20执行
        c = c + 1;
    end
end

使用#指定延迟时间,这种语句称为延迟语句,一般只用在Testbench中

示例

always  @(*)    begin
    a = #10 b;  //10个时钟周期后赋值
end

initial begin
    #0  a = 1'b1;   //0刻a为1
    #10 a = 1'b0;   //10刻a为0
end

在实际仿真中,仿真器默认的延迟为0。这是理想状态下的输出延迟,但是实际中输出总是有延迟的,所以要使用延迟语句加入延迟,以得到真实的仿真结果

示例

module dff (
    input   wire    d_in;
    input   wire    reset_;
    input   wire    clk;
    output  reg     d_out;
);

    always @(posedge clk or negedge reset_) begin
        if (reset_ == 1'b0) begin   //异步复位
            d_out <= #1 1'b0;       //延迟一个时间单位
        end else begin
            d_out <= #1 d_in;       //延迟一个时间单位
        end
    end

endmodule

3.8.3 时钟生成

示例

always #10 begin
    clk <= ~clk;
end

initial begin
    #0 begin        //时钟初始化
        clk <= 1'b0;
    end
    //后接以后的仿真语句
end

3.8.4 仿真常用系统任务

输出字符串

$display("string %d", i);     //带回车符,可以使用类似c语言的格式化输出
$write("string %d", i);       //不带回车符

返回目前仿真时间

$time;

结束仿真

$finish;

载入存储映像

$readmemh("filename.dat", mem); //将filename读入到mem中,mem可以是一个reg阵列。数据文件使用十六进制文本记录

指定输出

initial begin
    $dumpfile("test1.vcd"); //指定输出文件名
    $dumpvars(0, test);     //从时刻0开始,模块名test的输出波形
end

3.9 补充

3.9.1 并行语句块

顺序执行的语句块一般用begin end声明。而如果块中的语句并行执行,可以使用fork join

fork        //使用fork join,即便使用阻塞赋值也会并行执行语句
    a = 1'b1;
    b = 1'b0;
join

4 常用的算术硬件以及计算方法

算术电路是CPU的核心部分,常见的一般有加法器,乘法器,除法器,以及针对定点浮点运算的算术逻辑电路

4.1 加法器

常见的CPU中加减法都是使用加法器实现

在一般的处理器中,加法指令不分有无符号(对于加法指令来说一个数也不分正负,0b10001100表示为140还是-116是软件层面解决的问题)。而减法指令同样不分有无符号,对减数所有位求补之后做加法

4.1.1 普通加法器

普通加法器使用全加器构成。全加器级联之后形成的加法器又被称为行波进位加法器

全加器具有三个输入$x_i$、$y_i$、$c_i$,以及两个输出$s_i$、$c_{i+1}$。$s_i$为当前位结果输出,$c_{i+1}$为进位输出。$x_i$、$y_i$为两个加数输入,$c_i$接上一位进位输出$c_{i-1}$

各位逻辑关系为:$s_i = x_i \oplus y_i \oplus c_i, c_{i+1} = x_i \cdot y_i + y_i \cdot c_i + x_i \cdot c_i$,示意图如下

普通全加器

这种加法器虽然实现简单,但是有一个致命的缺点:假设最低位发生了变化,进位要经过全部全加器的逻辑门才会传递到最高位,延迟过高,不适用于高速电路

4.1.2 快速加法器

快速加法器又称超前进位加法器,相比普通加法器具有更低的延迟,适用于高速电路

关于快速加法器可以作如下推导

之前的普通加法器主要问题出在进位的传递上。$c_{i+1}=x_i \cdot y_i + y_i \cdot c_i + x_i \cdot c_i$,可以将进位和$x_iy_i$分开,$c_{i+1} = x_i \cdot y_i + (x_i+y_i) \cdot c_i$。可以设$g_i = x_i \cdot y_i, p_i = x_i + y_i$,那么$c_{i+1} = g_i + p_i \cdot c_i$

而$c_i$又可以展开为$c_i = (g_{i - 1}) + (p_{i - 1}) \cdot (c_{i - 2})$,那么$c_{i+1} = g_i + p_i \cdot ((g_{i - 1}) + (p_{i - 1}) \cdot (c_{i - 2})) = g_i + p_i(g_{i - 1}) + p_i(p_{i - 1}) \cdot (c_{i - 2})$

以此类推,最终可以推导得出$c_i=(g_{i-1})+(p_{i-1})(g_{i-2})+(p_{i-1})(p_{i-2})(g_{i-3})+ \cdots +(p_{i-1})(p_{i-2}) \cdots p_2p_1g_0 + (p_{i-1})(p_{i-2}) \cdots p_2p_1p_0c_0$

例如

$c_1 = g_0 + p_0c_0$

$c_2 = g_1 + p_1g_0 + p_1p_0c_0$

$c_3 = g_2 + p_2g_1 + p_2p_1g_0 + p_2p_1p_0c_0$

由上可知,对于加法的每一位可以设置$g_i$和$p_i$。这样每一级的进位都可以直接由之前的输入决定

超前加法器最初两级的电路如下

超前进位加法器

但是由于电路的扇入问题,每个门输入信号是有限的,并且超前进位加法器的规模随输入增加复杂度呈$O(n^2)$,所以超前进位加法器一般级数都不多,而将多个加法器以模块形式级联

补充:超前进位加法器的超前进位级联

除一般的级联以外,超前进位加法器还可以将每一个模块都像超前进位那样连接。连接的电路被称为超前进位发生器(比如74LS182),并且这种电路呈现层次型

超前进位级联

分析可得

$c_8=g_7+p_7g_6+p_7p_6g_5+p_7p_6p_5g_4+p_7p_6p_5p_4g_3+p_7p_6p_5p_4p_3g_2+p_7p_6p_5p_4p_3p_2g_1+p_7p_6p_5p_4p_3p_2p_1g_0+p_7p_6p_5p_4p_3p_2p_1p_0c_0$

设$P_0=p_7p_6 \cdots p_0, G_0=g_7+p_7g_6+ \cdots +p_7p_6p_5p_4p_3p_2p_1g_0$

那么$c_8=G_0+P_0c_0$

同理$c_{16}=G_1+P_1G_0+P_1P_0c_0$,以此类推,实现了一个32位加法器

4.2 乘法器

和加减法不同,乘法指令分为有符号和无符号,使用不同的处理方法,无符号位运算直接乘即可,带符号位运算有多种处理方法。可以单独处理符号,并且将负数转为其绝对值正数,之后直接当作无符号运算处理;另外一种方案基于符号扩展

二进制乘法运算过程如下,可以看成移位相加

二进制乘法

4.2.1 时序乘法器

时序乘法器就是以上算法的直接实现,扫描乘数每一比特,移位同时相加即可。判断溢出也比较容易实现,观察超出寄存器最高位后是否有$1$即可。这里省略设计过程

4.2.2 阵列乘法器

阵列乘法器就是时序乘法的并行化,需要使用到大量加法器,速度较快,但随着位数增多电路复杂度呈现$O(n^2)$

阵列乘法器

$n$位的乘法需要做$(n-1)$次加法。上图中第一行单元直接将$q_0q_1$和输入与运算之后移位相加,最高位进位输入$0$

之后的每一级,都是将之前部分积以及其进位和当前部分积移位相加,原理好理解

4.2.3 符号扩展乘法运算

带符号乘法运算的符号扩展方案如下

符号扩展

在补码表示中,在正数前面添加$0$或在负数前面添加$1$都不会改变数的值

两个数都是正数的情况和无符号乘法相同,主要看被乘数是负数的情况。第一次相乘需要扩展两个符号位,扩展的符号位和最高位相同。第一个符号位是因为移位相加,需要扩展。第二个符号位是为了处理溢出问题,由当前部分积扩展,符号和最高位相同

随后的运算中,只需要扩展一位符号位(部分积符号位)即可

4.3 除法器

和乘法类似,除法同样分为带符号除法指令和无符号除法指令。二进制除法的符号同样可以单独处理。另外除数为$0$时除法应当产生异常,即Divide By Zero异常

整数除法会产生两个结果,一个是商一个是余数。有些ISA的除法指令可以处理余数,而有些不支持余数,需要软件实现

二进制无符号除法运算过程如下,可以看成移位相减

二进制长除

4.3.1 时序除法器

时序除法器同样可以使用移位寄存器、加法器以及二进制比较器构建,算法状态机图如下

算法状态机图

$R || A$表示的是这样一个移位寄存器,左边$n$bit是$R$,作除法结果输出;右边$n$bit是$A$,输入被除数。每次$A$左移一位之前比较器比较除数$B$和余数$R$大小,若$B$大于$R$说明不够除,商$0$左移;若$B$小于等于$R$,则商$1$,$R-B$之后左移

4.3.2 阵列除法器

阵列除法器构建原理和乘法器类似,都是使用空间换时间的方法。

4.4 二进制比较器

一个4位二进制比较器示例如下

二进制比较器

二进制比较器输入两个4bit二进制数$a$和$b$,输出$eq$代表相等,$lt$代表小于,$gt$代表大于

两个数相等好理解,直接使用同或逻辑即可实现

而大于或者小于其实只判断一个即可,如果$a$不相等也不大于$b$,那么$a$一定小于$b$。图中小于就是直接将大于输出和相等输出作或非运算

大于逻辑也依靠于相等逻辑。大于逻辑会对$ab$每一位对应的进行判断,若在某一位$a$大于$b$,与门就输出$1$(输入$b$取反)。

大于逻辑表达式如下,好理解

大于逻辑

4.5 浮点运算

有关IEEE规定的浮点数表示以及运算标准见补充章节

4.6 二进制转BCD

二进制转BCD是实现人机交互的关键运算。虽然二进制转BCD可以使用除法以及求余得到,但是效率比较低,尤其是在一些不支持硬件乘除的机器上。这里引入一种较为简单的基于移位的方法,这种方法同样可以空间换时间,使用一个逻辑电路直接实现

二进制转BCD(图片引自网络)

这种方法一般称为加三移位法。如果是8bit二进制编码,那么最多需要3位BCD码。在上图中,将输入的二进制码逐次左移,任何BCD位一旦超过了4,那么立即加3,之后继续左移,直到所有二进制位都移入BCD中

4.7 运算负数/零值/进位/溢出标志位

常见的CPU架构都集成了状态寄存器,例如在ARM的CPSR中高4位有N(负值Negative)Z(零值Zero)C(进位Carry)V(溢出Overflow)标志位,用于整数运算的状态标记。其他CPU架构里面这些标志位可能名称不同,但是作用基本是相同的

4.7.1 N标志位

N用于表示运算结果最高位为1

前面在加法器章节已经说过在一般的CPU设计中整数加减法指令本质都是不区分有无符号的。N可以用于在带符号数(补码表示)运算中,如果结果最高位为1则可以设置N标志位为1表示结果为负数

在ARM中,一个指令是否影响状态标志位是由指令之后的S决定的,比如ADDS R0, R1, R2,如果运算结果为负数,就会设置标志位N1

4.7.2 Z标志位

Z用于表示运算的结果为全0

在ARM中,比如SUBS R0, R1, R2,如果R1R2相等则最后会设置Z位为1。而一般的CMPCMN其实分别是SUBADD的无目标寄存器版,如果CMP R1, R2R1R2相等那么就会将Z设为1

4.7.3 C标志位

C用于表示无符号运算中的进位

C的本质是用于表示加减法运算在1111_11110000_0000边界处发生了进位。一般加法时不进位置0进位置1,减法时够减置1不够减置0

4.7.4 V标志位

V用于表示有符号运算中的溢出

V的本质是用于表示加减法运算在0111_11111000_0000边界处发生了溢出。一般加法时不溢出置0溢出置1,减法时不溢出置1溢出置0

4.8 立即数的循环移位表示

循环移位可以用于在有限的寄存器位宽下(比如12bit)表示完整的32bit立即数

比如想要表示0x0000f200,立即数域为12bit,表示为0xcf2,将0x000000f2循环右移24位

显然这种表示方法不能表示任意32bit立即数

5 补充

5.1 IEEE754浮点数标准

IEEE754-2008官方文档

IEEE754浮点数标准最初起源于Intel公司为其8087浮点协处理器设计的浮点数格式,由Intel公司聘请的数值分析专家设计,最终成为计算机业界标准

IEEE754主要定义了浮点数的格式,简单的基本操作以及异常的处理方法

由于浮点数只是拟合了实数代数系统,它只能表示实数集合的一个非常有限的子集,而在大多数情况下无法表示一个实数的精确值,或得到一个算式的精确计算结果。也是因此加法结合律等适用于一般实数的规则不适用于计算机中的浮点数(虽然这在一定误差允许范围内是成立的)。IEEE754规定的浮点数算法本身是可再现的

5.1.1 浮点数的表示

目前IEEE规定的浮点数中,常用的有32位单精度64位双精度。除此之外还有各浮点数格式对应的扩展精度16位半精度128位高双精度(一般用不到。8087协处理器最高支持到80位扩展精度)。扩展精度不是IEEE754强制要求的。一般要求一个平台至少要实现单精度浮点数以及各项配套支持

一个平台支持哪些浮点数格式,实质上是由软件(编译器与工具链)决定的,而不是硬件,很多没有浮点单元的机器如单片机照样可以使用整数指令结合移位等操作计算浮点数。硬件不是绝对的限制因素。x86平台支持80位扩展精度只是因为x87协处理器支持这种浮点格式

这里首先引入一些定义

exponent:一般使用e表示,浮点数的指数

significand:浮点数的尾数。尾数的实际长度为p,表示的值为m

radix:进制,使用b表示。二进制b=2

IEEE754定义了三种基本的二进制浮点数格式(另外有2种十进制格式,这里略),如下,划重点

IEEE754的浮点数由3个部分组成

最高位为S符号位,0表示正数,1表示负数。在一个函数中返回结果的符号最多只能和一个操作数不同,即不能出现++得-的情况,有NaN除外

中间的E为指数位,本质是一个无符号整数。在32位单精度浮点数中E占8位,表示一般规格化浮点数时可以取1到254。在64位双精度浮点数中E占11位,可以取1到2046。E表示的不是实际的指数e,实际的指数e需要添加一个偏移bias。32位单精度浮点数中bias为127,那么E=e+bias,实际可以表示的指数范围emax=127,emin=-126。64位双精度浮点数的bias为1023,可以表示emax=1023,emin=-1022(必须注意指数不是补码表示,和补码差了1,千万不要混淆

IEEE754使用的是隐含1开头的规格化浮点尾数表示法,T表示的是小数点之后的位,所以在单精度浮点数中尾数使用23位表示而实际有24位,双精度浮点数中尾数使用52位表示而实际有53位

十进制小数到二进制浮点数的转换首先需要将小数点两端分别转换,例如-9.625,转换成为二进制表示为-1001.101,之后需要移动小数点使得尾数处于1和2之间,那么转换成为单精度浮点就表示为1 10000010 00110100000000000000000,使用16进制表示为0xC11A0000

浮点数实际表示数值转换公式如下

另外,指数位E预留的值0用于表示正负0以及非规格化浮点数,预留的值255用于表示NaN和正负无穷

在E为255时,如果T全为0,S为0,就表示正无穷;如果S为1,就表示负无穷

在E为255时,如果T不全为0,就表示sNaNqNaN(和符号位S无关)

在E为0时,如果T全为0,S为0,就表示+0;如果S为1,就表示-0

在E为0时,如果T不全为0,就表示非规格化小数(此时e=emin)。这种非规格化数实际表示的值为(-1)^S * 2^(emin-t) * T(此时可以将T看作一个整数。当然换种思路也可以将尾数看作0开头的小数0.T,隐含了开头的0),例如在单精度浮点数中,如果S=0,E=0,T=1,那么表示的数就等于(2^-126) * (2^-23) * 1

非规格化数表示数值转换公式如下

IEEE754给出了用于数据信息交换的格式标准Binary interchange format,见下表,多出了半精度以及高于128位浮点数的定义

附:Intel文档中对于浮点数据格式的定义

5.1.2 无穷

一般情况下普通数字的运算不会抛出异常,包括以下操作

加减法其中一个操作数为无穷,如addition(x,inf)不会抛出异常

乘法中有一个或两个操作数都为无穷,multiplication(x,inf),且x不为0

除法中一个操作数为无穷,如division(x,inf)division(inf,x)

均方根正无穷squareRoot(+inf)

求解余数remainder(x,inf)(见下),结果永远为x

不同精度infinity的互相转换

以下操作会抛出异常,部分详细内容参考异常及其处理

如果一个操作中inf是无效数字,抛出invalid operation异常

两个有限数通过上、下溢(overflow,underflow)得到了inf

除数为0时得到inf,division by zero

计算remainder(subnormal,inf)时(使用了非规格化数),会抛出underflow

5.1.3 NaN

NaN分为sNaN(Signaling)和qNaN(Quiet)。sNaN可以用于表示未初始化的浮点数。qNaN可以用于表示无效的数字或结果

IEEE754规定,qNaN的尾数域T最高位应当为1,sNaN的尾数域T最高位应当为0(同时剩余位不能全为0)。具体的编码含义应当由浮点实现者规定(除T最高位以外其余位称为payload。如果一个函数有一个或多个操作数为NaN,需要返回NaN,那么就需要尽量维持payload不变)

所有抛出无效操作异常的操作(invalid operation)应当返回一个qNaN作为结果

需要返回浮点结果的函数,如果必要那么应该返回qNaN

sNaN一般只会作为操作数。当General或Signaling类型的函数中有sNaN作为操作数,那么就会抛出无效操作异常(部分格式转换操作除外)

当General或Quiet类型的函数的操作数中没有sNaN,那么就不应当抛出异常(融合乘加除外)

5.1.4 舍入(Rounding)

在本小节开始之前首先需要引入概念attribute,可以说attribute就是一个上下文当中用于控制计算行为的参数,比如舍入等行为

舍入是浮点运算中的重要操作,因为绝大多数实数运算结果都是有无限位小数的。在浮点运算中的舍入行为会影响到最终的运算结果,从而影响到精确度,也会影响多次运算的累积误差

IEEE754有一个原则性要求就是浮点算法的运算结果需要尽量接近实际的数学运算结果,要减少累积误差。一次运算得到的中间结果一般是无限小数,而无限小数无法使用浮点数格式表示。所以就要引入对于舍入的规定

在浮点数中主要的舍入操作有以下两类

最近数舍入(Rounding-direction attributes to nearest)

如果一个数大于规格化浮点数可以表示的最大数,正数应该被round到正无穷,负数应该被round到负无穷

roundTiesToEven:优先round到最接近的浮点数。如果该数和两个可表示浮点数的距离相等,优先round到尾数最低位(LSB)为0的浮点数(即尾数为偶数)

roundTiesToAway:优先round到最接近的浮点数。如果该数和两个可表示浮点数的距离相等,优先round到绝对值更大的浮点数

导向舍入(Directed rounding attributes)

roundTowardPositive:永远向正无穷方向round,可以round到正无穷。得到的结果永远不小于该数

roundTowardNegative:永远向负无穷方向round,可以round到负无穷。得到的结果永远不大于该数

roundTowardZero:永远向0方向round。得到结果的绝对值永远不大于该数绝对值

IEEE754规定在二进制浮点数的实现中,需要包含roundTiesToEven,roundTowardPositive,roundTowardNegative以及roundTowardZero。其中需要将roundTiesToEven作为默认的二进制浮点数舍入策略。另外如果一个函数如加法以及融合乘加得到结果正好为0,除roundTowardNegative(-0)外所有结果都应该为+0。均方根-0结果应当为-0

5.1.5 操作(Operations)简介

IEEE754将所有浮点数的操作分为4大类

General-computational operations:通用运算操作。这种操作既会产生运算结果,一般是结果舍入的浮点数或整数;也可以抛出浮点异常(exceptions)

Quiet-computational operations:会产生计算结果,一般是结果舍入的浮点数或整数,但是不会抛出浮点异常

Signaling-computational operations:不会产生计算结果,但是会抛出浮点异常。典型的运算操作有比较运算

Non-computational operations:不属于运算操作,不会产生运算结果或抛出异常,如寄存器数据搬运指令等

另外,根据操作数和运算结果的数据格式,可以将浮点数的操作分为2大类

Homogeneous operations:同类操作,使用的操作数以及运算结果的浮点数格式相同

formatOf operations:运算结果的格式和操作数的格式不相同

5.1.6 同类通用运算操作(Homogeneous General-computational operations)

浮点小数转浮点整数

浮点数到整数转换的关键中间步骤

函数名 解释
roundToIntegralTiesToEven(x) 将浮点数round到最近的浮点整数。如果差值相等(.5)则round到偶数(尾数LSB为0)
roundToIntegralTiesToAway(x) 将浮点数round到最近的浮点整数。如果差值相等(.5)则向绝对值大的方向round(和0相反方向)
roundToIntegralTowardZero(x) 将浮点数向0方向round
roundToIntegralTowardPositive(x) 将浮点数向正无穷方向round
roundToIntegralTowardNegative(x) 将浮点数向负无穷方向round

这些函数不依赖于attribute,round方法直接体现在函数名中

操作数为正负0或正负无穷时,结果就是操作数

只有在输入sNaN时,这些函数才会抛出异常。在其他情况下不会抛出异常

函数名 解释
roundToIntegralExact(x) 依据attribute上下文对浮点数进行round,可以是ToEven等各种方法

该函数依赖于Rounding-direction attributes

操作数为sNaN时会抛出无效操作(Invalid Operation)异常

而在转换结果和原始的操作数不相等时,抛出不准确(Inexact)异常。显然在绝大多数情况下该函数都会抛出异常

上/下一个数

函数名 解释
nextUp(x) 比输入浮点数大的相邻浮点数
nextDown(x) 比输入浮点数小的相邻浮点数

这两个函数用于求解一个浮点数表示集合中最接近的浮点数

对于nextUp()来说,如果输入是正负0,输出就是绝对值最小的规格化正数;如果输入是绝对值最小的规格化负数,那么输出是-0;如果输入是正无穷,那么输出是正无穷;如果输入是负无穷,那么输出是绝对值最大的规格化负数nextDown()则相反

只有输入是sNaN时函数才会抛出异常

Remainder余数

函数名 解释
remainder(x,y) 求解r=x-y*n,其中n为整数,x和y为浮点数,n取最接近x/y的整数值,如果|n-x/y|=0.5那么n取偶数。翻译一下其实就是凑一个整数n,使得y*n和x最接近。如果取得| y*n - x | = | x - y*(n+1) |,即取n和(n+1)时距离相同,那么n优先取偶数

这个函数可以应用于sind() cosd()等函数中。可以这样看,由于sind(x) = sin(x*pi/180),而其中的pi是个无理数,无法使用浮点数精确表达。这样的计算会带入系统误差,不仅会导致例如sind(360)计算结果不为0,在x值非常大时也会导致累积误差的增大。然而sind()是周期函数,使用x = remainder(x,360)函数处理x,可以使得x永远落在[-180,180],并且在x=360*n时计算结果为0。如果想要处理其他点处的特殊值,那么还可以计算remainder(x,180)

Max和Min函数

函数名 解释
minNum(x,y) 返回较小的数。如果输入一个是qNaN,一个是普通数字,那么返回其中规格化的数。其他情况可能返回规格化的x或y,视具体软件实现而定。如果x和y其中之一是sNaN那么应该抛出无效异常
maxNum(x,y) 返回较大的数
minNumMag(x,y) 返回绝对值较小的数。其他情况返回minNum(x,y)
maxNumMag(x,y) 返回绝对值较大的数。其他情况返回maxNum(x,y)

5.1.7 formatOf通用运算操作(formatOf General-computational operations)

所谓formatOf运算操作,就是操作数和运算结果数据格式可以不同。这些操作包含了最基本的算术运算操作。IEEE754规定一个浮点实现中这些函数需要支持所有数据格式的操作

算术运算

函数名 解释
addition(x,y) 加法
subtraction(x,y) 减法
multiplication(x,y) 乘法
division(x,y) 除法
squareRoot(x) 计算均方根。输入0以及正数时,一般返回的数符号为正(S=0),输入-0那么返回-0
fusedMultiplyAdd(x,y,z) 计算融合乘加x*y+z。整个运算过程只会round一次,并且异常只能由加法抛出
convertFromInt(x) 整数转浮点。转换出来的浮点数值是准确的。如果无法使用浮点表示,那么应当进行舍入,同时抛出inexact或floating-point overflow异常。如果输入是带符号0那么返回结果符号不变。如果输入无符号0那么返回+0
convertToIntegerTiesToEven(x) 浮点转整型,取最近的整数。结合之前的浮点整数转换,在将浮点数转换为整数时如果是.5,那么取偶数。如果输入是NaN或者无穷,或超出整数表示范围,会抛出invalid operation异常
convertToIntegerTowardZero(x) 浮点转整型。向0取整
convertToIntegerTowardPositive(x) 浮点转整型。向正无穷方向取整
convertToIntegerTowardNegative(x) 浮点转整型。向负无穷方向取整
convertToIntegerTiesToAway(x) 浮点转整型。遇到.5向无穷方向取整
convertToIntegerExactTiesToEven(x) 浮点转整型,遇到.5取偶数。不同的是如果转换得到的整数在数值上不等于原先的浮点数还会抛出inexact异常,以下函数同理
convertToIntegerExactTowardZero(x) 浮点转整型。向0取整
convertToIntegerExactTowardPositive(x) 浮点转整型。向正无穷方向取整
convertToIntegerExactTowardNegative(x) 浮点转整型。向负无穷方向取整
convertToIntegerExactTiesToAway(x) 浮点转整型。遇到.5向无穷方向取整

字符串转换

IEEE754规定一个浮点数实现需要支持所有二进制浮点数到十进制字符串的转换,同时需要保证转换不能损失精度。即一个浮点数转换为十进制字符串以后还可以完整还原,包括正负0,无穷,qNaN,这些可以使用特殊字符串表示,如inf,NaN,sNaN等,同时需要保留符号。转换过程如需round一般采用roundTiesToEven。

qNaN只能转换成为字符串NaN,而sNaN可以转换成为字符串NaN(需要抛出invalid operation异常)或sNaN

反过来字符串NaN只能转换成为qNaN,而字符串sNaN可以转换成为qNaN(需要抛出invalid operation异常)或sNaN

在将非无穷数字符串转换为二进制浮点数时,如果超出浮点指数表示范围,需要抛出overflow或underflow异常。如果该浮点字符串无法使用浮点数精确表示而需要round,那么需要根据情况抛出inexact等异常

函数名 解释
convertFormat(x) 用于不同精度浮点数的转换。如果是单精度转双精度,那么结果应当是精确的。如果是双精度转单精度,那么会损失精度
convertFromDecimalCharacter(x) 十进制字符串(Human readable)转二进制浮点数
convertToDecimalCharacter(x) 二进制浮点数转十进制字符串

十六进制字符串转换

函数名 解释
convertFromHexCharacter(x) 十六进制字符串转二进制浮点
convertToHexCharacter(x) 二进制浮点转十六进制字符串

5.1.8 Quiet-computational operations

函数名 解释
copy(x) 复制一个浮点数,符号不变
negate(x) 复制一个浮点数,符号取反
abs(x) 复制一个浮点数,符号置0
copySign(x,y) 复制一个浮点数x,将符号置y

5.1.9 Signaling-computational operations

比较操作

比较操作有3种不同的互斥关系,分别是LT(Less than小于),GT(Greater than大于),EQ(Equal等于)以及UN(Unordered有NaN作为操作数)

两个操作数中如果至少有一个为NaN那么比较结果就是Unordered。-0和+0相等。正无穷和正无穷相等,正无穷大于负无穷

函数名 返回真 解释
compareQuietEqual(x,y) EQ 遇到qNaN操作数不抛出invalid operation异常
compareQuietNotEqual(x,y) LT GT UN
compareSignalingEqual(x,y) EQ 遇到qNaN操作数抛出invalid operation异常
compareSignalingGreater(x,y) GT
compareSignalingGreaterEqual(x,y) GT EQ
compareSignalingLess(x,y) LT
compareSignalingLessEqual(x,y) LT EQ
compareSignalingNotEqual(x,y) LT GT UN
compareSignalingNotGreater(x,y) LT EQ UN
compareSignalingLessUnordered(x,y) LT UN
compareSignalingNotLess(x,y) GT EQ UN
compareSignalingGreaterUnordered(x,y) GT UN
compareQuietGreater(x,y) GT
compareQuietGreaterEqual(x,y) GT EQ
compareQuietLess(x,y) LT
compareQuietLessEqual(x,y) LT EQ
compareQuietUnordered(x,y) UN
compareQuietNotGreater(x,y) LT EQ UN
compareQuietLessUnordered(x,y) LT UN
compareQuietNotLess(x,y) EQ GT UN
compareQuietGreaterUnordered(x,y) GT UN
compareQuietOrdered(x,y) LT GT EQ

5.1.10 非运算操作(Non-computational operations)

通用操作

函数名 解释
class(x) 判断一个浮点数的类型。可以是signalingNaN,quietNaN,negativeInfinity,negativeNormal,negativeSubnormal,negativeZero,positiveZero,positiveSubnormal,positiveNormal,positiveInfinity
isSignMinus(x) 判断符号是否为负。可以用于NaN和0
isNormal(x) 数字是无穷,NaN,正负0,非规格化以外的数时返回真
isFinite(x) 数字是无穷,NaN以外的数时返回真
isZero(x)
isSubnormal(x)
isInfinite(x)
isNaN(x)
isSignaling(x) 如果是sNaN返回真
isCanonical(x) 判断是否是规格化浮点数
radix(x)
totalOrder(x,y) 全序关系。如果x不大于y那么返回真,否则返回假。如果x为-0而y为+0那么返回真。(-NaN,y),,(x,+NaN),(-qNaN,+qNaN),(-sNaN,+sNaN),(+sNaN,+qNaN)以及(-qNaN,-sNaN)返回真
totalOrderMag(x,y) 将xy符号置0以后比较totalOrder(x,y)

Flag操作

Flag用于指示一些当前的运算状态

函数名 解释
lowerFlags(exceptionGroup) 根据抛出的异常清除状态位
raiseFlags(exceptionGroup) 状态位置位
testFlags(exceptionGroup) 测试是否需要置位任一状态位
testSavedFlags(flags,exceptionGroup) 测试是否需要置位指定状态位
restoreFlags(flags,exceptionGroup) 恢复状态位
saveAllFlags() 返回当前的状态位

5.1.11 异常及其处理

基本的异常分为以下5种

无效操作(Invalid operation)

应当返回一个qNaN

General或Signaling函数的操作数中出现sNaN。部分数据格式转换函数除外

inf和0相乘,例如multiplication(0,inf)

融合乘加中inf和0相乘。加数c为qNaN除外,视具体实现而定

同号inf相减或异号inf相加,如addition(+inf,-inf)

00或无穷相除,如division(0,0) division(inf,inf)

remainder(x,0)remainder(inf,y),其中xy都不为NaN

squareRoot(x),其中x为负数

数据类型转换时无法使用目标格式表示

无返回浮点结果

浮点转整数时无法使用目标整数格式表示

Signaling类型比较函数,遇到NaN结果为unordered

除数为0(Division by zero)

除法中除数为0,被除数为非零有限数,返回inf,符号为两个操作数符号的异或(--得+,++得+,+-得-)

上溢出(Overflow)

一般出现在round操作中,默认应当抛出inexact异常

roundTiesToEven和roundTiesToAway时,上溢超出最大浮点变为inf

roundTowardZero,上溢返回最大可表示的浮点数

roundTowardNegative正上溢返回最大可表示的浮点数,负上溢返回-inf,roundTowardPositive相反同理

下溢出(Underflow)

下溢出一般需要规定一个最小的界限bmin,如果round后或round前的数小于正负bmin那么就应当抛出异常,同时返回一个结果,可以是0,bmin或subnormal。抛出inexact异常

不准确(Inexact)

任何不准确(无论是round还是下溢)都应当抛出inexact异常

5.1.12 IEEE754推荐实现的函数