第九章 FPGA至简设计法规范
第 1 节 波形图规则
至简设计法曾在 D 触发器的相关知识讲解中,详细的描述了如何看 FPGA 信号的波形。针对波形的规则,只需记住一点:时钟上升沿看信号,看到的是信号变化前的值。这是什么意思呢?就是说在每一个时钟上升沿查看信号时,如果信号的值发生了改变,那么此时的信号值为信号改变前的值。还没有理解或者非常感兴趣的朋友,可以查找对应章节仔细学习相关内容。
下面来举例说明一下,观察图 2.4-39 中第 4 个时钟上升沿相应的信号变化可以看到在第 4 个时钟上升沿,信号 a 和信号 c 发生了改变。信号 a 由 1 变为 0,信号 c 由 0 变为 1,此时的信号值应该看变化前的值,因此在第 4 个时钟上升沿信号 a 的值为 1,信号 c 的值为 0。再来观察第 5 个时钟的上升沿,可以看出信号 b 和信号 c 发生了改变。同样的,此时信号值为变化前的值,即信号 b 的值为1,信号 c的值也为1。
再来看图 2.4-40,在第 5 个时钟上升沿处观察信号 dout 和信号 cnt 的值。根据规则可知信号要取变化之前的值,即信号 dout 取值为 1,同样的道理,信号 cnt 的取值同样为 1,并不是 2。
以上就是观察波形的规则,只要记住在不确定的信号处均选择前面的信号,就一定不会出错。当然,该方法的使用前提是所有信号都是同步信号且波形是理想的波形,不然一切都是空谈。该方法的具体原理解释可以参考学习至简设计法 D 触发器中波形查看的相关内容。
第2节 计数器规范
计数器设计是 FPGA 设计的核心,计数器架构是 FPGA 设计最常用的架构之一,也是至简设计法的核心部分。计数器架构并不难,只要认真学习这部分内容,相信广大初学者也能掌握 FPGA 设计的精髓。本书在这里也会提供通过一些项目练习来进行巩固学习。
本章主要介绍至简设计法的计数器规则。
下面是至简设计法的计数器模板样式,其包括三段:计数器的 always 语句,加 1 条件的定义以及结束条件的定义。
计数器规则 1:计数器逐一考虑三要素,初值、加 1 条件和结束值:
任何计数器都有三个要素:初值、加 1 条件和结束值。
初值:计数器的默认值或者开始计数的值。
加 1 条件:计数器执行加 1 的条件。
结束值:计数器计数周期的最后一个值。
设计计数器,要逐一考虑这三个要素,一般是先考虑初值、再考虑加 1 条件,最后再考虑结
束值。
计数器规则 2:计数初值必须为 0:
计数器的默认值和计数器的初始值一定要为 0,这是至简设计法规范的统一要求。众所周知,一般编程语言计数都是从 0 开始的,0 计第 1 个数,1 计第 2 个数,以此类推。至简设计法也参考这种做法,计数器从 0 开始进行计数。
所有计数器都统一从 0 开始计数有助于同学们阅读理解、方便使用,如此一来,同学们不需要从头看具体代码就能清楚这个数值的含义。
计数器规则 3:只有在加 1 条件有效时,才能表示计数器的计数值。
假定加 1 条件为 add_cnt,计数器当前值为 cnt,则
add_cnt&&cnt==x-1 表示计数器计数到 x 个。
cnt==x-1 不能表示计数器计数到 x 个。
计数器是从 0 开始计数的,因此计数器的默认值即初始值是 0。那么当计数器的值为 0 时要如何区分这是开始计数的第 1 个值,还是并未计数的默认值呢?
这种情况下可以通过加 1 条件来进行区分。当加 1 条件无效时,计数器值为 0 表示未开始计数,此时的 0 为默认值;当加 1 条件有效时,计数器值为 0 表示计的第 1 个数。同理,当 cnt==x-1,不能表示计数到 x;只有当 cnt==x-1 且加 1 条件有效时,才表示计数到 x。
因此,当 add_cnt&&cnt==5-1 时,表示计数到 5 个。而当 add_cnt==0 &&cnt==5-1 时,不能表示计数到 5 个。
计数器规则 4:结束条件必须同时满足加 1 条件,且结束值必须是x-1 的形式:
计数器的结束条件必须同时满足加 1 条件。假定 add_cnt 为加 1 条件,cnt 为当前计数器值,如果要计 5 个数,那么结束值应该是 4,但是结束条件并不是 cnt==4,而是 add_cnt&&cnt==4。因为根据规则 3 可知 cnt==4 不能表示计数到 5 个,只有 add_cnt&&cnt==4 时,才表示计数到 5 个。
为了更好地阅读代码,至简设计法规定结束值必须是 x-1 的形式。即 add_cnt&&cnt==4 要写成 add_cnt&&cnt==5-1。这里的“5”表示计数数量,“-1”则是固定格式。有了这个约定后计数的边界直观清晰。至简设计法多年的项目实践发现:无论多么有经验的工程师,对于边界值总会要多花一些心思来考虑。因此,至简设计法索性就制定了这个规则使得工程师不需要再考虑边界条件。
计数器规则 5:当取某个数时,assign 形式必须为:(加 1 条件)&&(cnt计数值-1);
当要从计数器取某个数时,例如要取计数器第 5 个数,很容易就写成 cnt==5-1,这一写法是不正确的。正确的写法是(加 1 条件)&&(cnt==计数值-1),如 add_cnt&&cnt==5-1。原因可以参考计数器规则 3 的说明。
计数器规则 6:计数结束后,计数器的值必须恢复默认值 0;
每轮计数器周期结束后,计数器必须变回0,这是为了使计数器能够循环重复计数。
计数器规则 7:若需要限定范围,推荐使用“>=”和“<”两种符号:
边界值的设计通常都要花费一些心思,而且容易出错。为此,至简设计法规范约定,如果需要限定范围时,推荐使用“>=”和“<”两种符号。例如要取前 8 个数,那么就取 cnt>=0 &&cnt<8。注意,一定是“大于等于”和“小于”符号,而不能使用“大于”、“小于等于”符号。
该规则参考编程里的 for 循环语句。假如要循环 8 次,for 循环的条件通常会写成“i=0;i<8;i++”,0为开始值,8 为循环个数。当然,也可以写成“i=0;i<=7;i++”,但这些数字的意义就变得令人费解了,虽然知道 7 是从 8-1 得来的,但此时“-1”就显得画蛇添足,在设计中不必多此一举。
计数器规则 8:设计步骤是,先写计数器的 always 段,条件用名字代替;然后用 assign 写出加1 条件;最后用 assign 写出结束条件。
至简设计法规定计数器代码包括三段。第一段,写出计数器的 always 段。
模板如下所示:
同学们有没有发现上述模板的特点?这个模板只需要填两项内容:加 1 条件和结束条件。假定加 1 条件为 add_cnt,结束条件为 end_cnt,则代码变成:
至此就完成了该 always 的设计,是不是很简单?只要确定好条件命名,就完成了这段设计代码。第二段,用 assign 写出加 1 条件。
在此阶段,只需要确认好计数器的加 1 条件。假设计数器的加 1 条件为 a=2,则代码为:
第三段,用 assign 写出结束条件。在此阶段只需要确认好计数器的结束值。参考计数器规则 5 的要求,结束条件的形式一定是:
(加 1 条件)&&(cnt==计数值-1)。假设计数器的要计数 10 个,则代码为:
至此就完成了计数器代码的设计。回顾一下,这段代码特点就是:每次只考虑一件事,按照这一要求可以很容易地完成代码设计。
计数器规则 9:加 1 条件必须与计数器严格对齐,其他信号一律与计数器对齐。
在设计中虽然对计数器进行编码,但一般计数器并不是最终目的,最终目的是各种输出信号。设计计数器是为了方便产生这些输出信号(包括中间信号),并且能够从计数器获取变化条件。例如信号 dout 在计数到 6 时拉高,则其变 1 条件是:add_cnt&&cnt==6-1。
假设有两个信号:dout0 在计数到 6 时拉高,dout1 在计数到 7 时拉高。一种设计方法是 dout0的变 1 条件是 add_cnt&&cnt==6-1,dout1 的变 1 条件设定为 dout0==1,这样 dout1 就是间接与计数器对齐。至简设计法不建议采用这种方法,在设计中建议信号一律与计数器对齐,dout1 的变 1 条件应该改为:add_cnt&&cnt==7-1。
计数器规则 10:命名必须符合规范,比如:add_cnt 表示加 1 条件;end_cnt 表示结束条件。
如无特别说明计数器的命名都要符合规范,加 1 条件的前缀为“add_”,结束条件的前缀为“end_”。
计数器规则 11:减 1 计数器暂时不用。
减 1 计数器在项目中应用也较为广泛。但至简设计法建议在学习阶段暂时不用减 1 计数器,进入
公司或者已经在做项目的情况下根据公司具体要求而定。
以上就是至简设计法的计数器规则。计数看似简单,但要用好却并非易事。至简设计法强调,真正掌握计数器的使用需要多实践,至简设计法也会为提供大量的实践项目进行练习,从而让同学们掌握、搞透计数器的设计。
第3节 状态机规范
状态机是数字电路设计中的一个非常重要组成部分,也是贯穿于整个设计始终的最基本设计思想和设计方法。在现代数字系统设计中状态机的设计对系统的高速性能、高可靠性、高稳定性都具有决定性的作用。熟练掌握状态机的设计后在数字电路的设计中必能达到事半功倍的效果。
前一节中介绍了计数器的规范。其实计数器本质上也可以认为是一个状态机,只不过计数器是用数字来区分不同状态而已。那么在设计中什么时候使用计数器,什么时候会使用状态机呢?
如果是顺序处理或是简单的流程控制,例如其步骤是 0->1->2->3->0,这个时候用计数器实现是最便捷的。但是在复杂的流程控制场合,例如其步骤是 0->1->5->2->4,其跳转顺序是乱序的时候,就应该利用状态机来设计。
规范的状态机代码可以极大地提高设计效率,在减少状态出错可能的同时缩短调试时间,从而设计出稳健的系统。至简设计法从项目实践和培训经验出发,总结出一套科学的、适用于状态机设计的方法,也称之为状态机架构八步法。
状态机架构八步法具有如下优点:
通用的设计方法,无论是简单还是复杂的状态机均能按照此法进行设计;
步骤清晰易懂,每步只考虑一个问题,易于掌握和使用;
状态机代码严谨规范,不容易出错;
设计出的状态机结构简单且稳健。
状态机规则 1:使用四段式写法。
四段式不是指四段 always 代码,而是指四段程序。另外需要注意四段式的状态机并非一成不变,而是会根据输出信号的个数进行调整。四段式的写法可参照至简设计法 GVIM 特色指令 Ztj 产生的状态机模板。
第一段,同步时序的 always 模块,格式化描述次态迁移到现态寄存器。
第二段,组合逻辑的 always 模块,描述状态转移判断条件。注意,转移条件用信号来表示,信号名要按至简设计法规则来命名。
第三段,用 assign 定义转移条件。注意,条件一定要加上现态。
第四段,设计输出信号。至简设计法规范要求一段 always 代码设计一个信号,因此有多少个输出信号就有多少段 always 代码。
状态机规则 2:四段式状态机第一段写法不变。
设计状态机时所有四段式状态机模板的第一段除了名字外的代码都可以直接用,不需要进行改动。
状态机规则3:第二段的状态转移条件用信号来表示。
设计状态机时,至简设计法要求四段式状态机的第二段中用信号名来表示转移条件,而无须直接写出具体的转移条件。
用信号名表示的好处是后续修改时只需改动信号的名字,并且方便根据状态机的命名修改对应的跳转条件。
状态机规则 4:用 assign 将状态转移条件写成 XX2XX_start 的形式。
状态机规则 3 要求转移条件用信号名来表示,这样一来设计师就要编写很多信号名称,这也是设计师工作中的一大困扰。因此至简设计法制定此规则:将状态转移的条件信号用 xx2xx_start 的形式表示。
例如有三个状态 IDLE、READ、WRITE,若从 IDLE 跳转到 READ 状态,其跳转条件可以命名为:idle2read_start;若从 IDLE 跳转到 WRITE,其跳转条件可以命名为:idle2write_start。这个命名方式既能够解决命名的困扰,又能直接从信号名看出信号的作用。至简设计法提出并主张所有信号都用对应意义的字母来命名,达到“见名知义”的效果。后续章节中还会提及至简设计法关于信号命名的规范,需要多加注意。
状态机规则 5:assign 定义状态转移条件信号时,必须加上当前状态。
状态机的第二段代码中使用信号名来表示转移条件,在代码后则需用 assign 对相应信号进行定义。注意,定义这个转移条件信号时必须加上当前状态,以避免因两个不同状态由同一种变化条件发生转移而导致错误。例如:
状态机规则 6:状态不变时使用 state_n =state_c。
至简设计法项目和培训实践统计发现:编写状态机代码时有很大一部分错误是复制粘贴过程出错造成的,很多同学会出现复制其他状态的代码时忘记修改状态的错误。此外,也有一部分同学写第二段状态机时,容易把状态保持不变写与 state_n = state_n。这个写法是错误的,因为组合逻辑只有锁存器才能有保持电路,而数字电路中通常不希望出现锁存器。
为此,至简设计法规定,其四段式状态机的第二段,状态不变时使用 state_n = state_c。如下所示,可以自行对比。这样写不但可以减少出错的可能,还可以减少调试的时间。
第4节 接口规范(建议多看几遍视频)
在确定模块划分后需要明确模块的端口以及模块间的数据交互。至简设计法在实际项目经验中总结得出了一般模块端口的信号规范。完成项目模块划分后,可以在确定端口及数据流向时参考使用。
(这部分最好结合视频)
第5节 FIFO规范
FIFO(First Input First Output),即先入先出队列。在计算机中先入先出队列是一种传统的按序执行方法,先完成并引退先进入的指令,随后跟着执行第二条指令(这些指令是指计算机在响应用户操作的程序代码,对用户而言是透明的)。
在数字电路设计中提到的 FIFO 实际是指 FIFO 存储器,其主要用于数据缓存和异步处理。当然FIFO 存储器缓存数据也遵循先入先出的原则。由于微电子技术的飞速发展,新一代 FIFO 芯片容量越来越大,体积越来越小,价格越来越便宜。因其灵活、方便、高效的特性,FIFO 芯片逐渐在高速数据采集、高速数据处理、高速数据传输以及多机处理系统中得到越来越广泛的应用。
FIFO 本质是一个 RAM,其与普通存储器的区别是没有外部读/写地址线,使用起来非常简单。但 FIFO 只能顺序的写入数据,再顺序的读出数据,其数据地址由内部读写指针自动加 1 完成,无法像普通存储器那样可以由地址线来对某个指定地址的数据进行读取或写入。
FIFO 规则 1:使用 Show-ahead 读模式。
FIFO 读操作一般有两种使用模式:Normal 和 Show-ahead 模式。其中 Normal 模式是读使能有效后的下一拍读出相应数据,如下图所示。
而 Show-ahead 模式是先进行数据输出,在读使能有效时对 FIFO 输出数据进行更新。即 FIFO中的第一个数据输出在总线上,在读使能信号到来的下一拍直接输出第二个数据,如下图所示。
至简设计法推荐使用 Show-ahead 模式,因为在这种模式下可以将读使能信号与读出数据当做有效信号和数据来使用,只要读使能有效则对应的数据就始终有效。
FIFO 规则 2:读写隔离规则。
FIFO 读写隔离规则是至简设计法经过长期的实践总结出来的经验规则。读写隔离规则是指:FIFO 的读、写控制是独立的,两者之间除了共用 FIFO 进行信息操作外,不能有任何信息传递。因此,既不能根据 FIFO 的读状态或读出的数据来决定写行为,也不能根据 FIFO 的写状态和写入的数据来决定读行为,如下图所示。
下面通过举例来帮助理解读写隔离规则。
假定需要设计一个模块将输入的报文保存到 FIFO 中,报文完整保存后再将其读出,并输给下游模块。那么如何确定何时才能保存完一个完整报文呢?可以知道收到 din_eop 时表示报文已经完整保存,那么能否可以使用 din_eop 作为读 FIFO 的开始条件呢?答案是不允许的。
如果以 din_eop 作为读开始的条件,如下图所示。先后输入一个 6 字节和 3 字节的报文,当收到第 1 个报文的 din_eop 时开始读 FIFO 数据并输出。由于输入数据量为 6,则输出数据量也 6,那么就要 6 个时钟来处理。然而在第 2 个报文的 din_eop 有效时,按照原理判断应该可以读取第 2 个报文并输出了,但是此时由于第 1 个报文还没有处理完,无法处理第 2 个报文,导致第 2 个报文被” 舍弃”,数据出错。
可以看出不可以使用 din_eop 来作为读开始的条件,其根本的原因是:数据是要缓存后输出的,同样的,din_eop 这个信号也需要缓存。
基于这一原因提出用双 FIFO 架构的概念,如图 2.4-45。感兴趣的同学可以对至简设计法中双 FIFO 架构的具体介绍进行学习。
FIFO 规则 3:读使能必须判断空状态,并且用组合逻辑产生。
rdreq 必须由组合逻辑产生,其原因与 empty 有关。下面来说明 rdreq 和 empty 的关系,假设 FIFO 存有 3 个字节数据,现采用 Show-ahead 模式对其进行读操作,将所存的所有数据读出来。相应信号波形下图所示。
从图中可以看出在 a 时刻 FIFO 已经为空,但由于电路存在一定延时,empty 变为高电平需要经历一段时间。如果用时序逻辑产生 rdreq,如图中虚线所示。在 a 时刻 empty 为 0 则表示 FIFO 中还有数据(但实际已经为空),因此 rdreq 还要保持一个时钟周期,在 FIFO 为空的情况下要再读取一个数据,读操作就会出错。如果用组合逻辑产生 rdreq,如图中实线所示。当 empty 为 1 时,rdreq 马上拉低则不会出现读取空 FIFO 这样的错误。
FIFO 规则 4:处理报文时,把指示信号与数据一起存入 FIFO。
通过一个例子来说明该规则。现要求设计一个模块,需先将输入报文存储到内部 FIFO 中,等待适当时机将报文数据原封不动地送给下游模块。注意,传送时需要同时输出报文数据、报文头和报文尾指示信号,时序如下图所示。
假定输入的报文 din 是 8’h12,8’h34,8’h56,8’h78 和 8’h9a,其中 8’h12 是报文头数据,8’h9a 是报文尾数据。则输出的报文 dout 同样应为 8’h12,8’h34,8’h56,8’h78 和 8’h9a,且 8’h12 是报文头数据,8’h9a 是报文尾数据。
根据一般思路,可以生成一个 8 位宽度的 FIFO,然后将 din 数据保存到 FIFO 中。读出 FIFO 的数据赋给 dout。但是怎么产生 dout_sop 和 dout_eop 信号呢?
实际上 FIFO 不仅可以保存“数据”,也可以保存“指示信号”,因此可以将数据和对应的“指示信号值”一起写入 FIFO。如要生成一个 10 位宽度的 FIFO,保存到 FIFO 的数据应是{din,din_sop,din_eop},如下图所示。
FIFO 写入的第 1 个数据是 10’b0001001010,即第 1 个数 8’h12 以及此时的 din_sop(值为 1)和 din_eop(值为 0)。第 2 个数据是 10’b0011010000,即第 2 个数 8’h34 以及此时的 din_sop(值为 0)和 din_eop(值为 0)。依此类推,最后一个数是 10’b1001101001,即 8’h9a 以及此时的 din_sop(值为 0)和 din_eop(值为 1)。
利用该方法写入数据很容易产生 dout_sop 和 dout_eop 信号。如果 FIFO 读出的数据最低位为 1则表示这是报文的最后一个数据,此时 dout_eop 为 1;如果其次低位为 1 则表示这是报文的第一个数据,此时 dout_sop 为 1。
此种方式还可以判断报文的开始和结束,从而用于其他判断。
FIFO 规则 4:读写时钟不同时,必须用异步 FIFO。
FIFO 按时钟分可以分为同步 FIFO 和异步 FIFO。同步 FIFO:指读时钟和写时钟都相同的 FIFO。同步 FIFO 内部没有异步处理,因此结构简单,资源占用较少。
异步 FIFO 是指读时钟和写时钟可以不同的 FIFO。异步 FIFO 内部有专门的异步处理电路,处理读、写信号的交互,因此异步 FIFO 结构复杂,占用资源较大。