实验3:计数器与移位器
在本次实验中,我们学习掌握计数器与移位器的功能及其应用:定时、分频、开关输入去抖动、数码管动态扫描显示、串行通信等
本次实验主要任务如下:
- 设计定时器和分频器,实现数码管动态扫描显示(第一周)
- 设计串行通讯模块,使得开发板可以和PC机进行通讯(第二周,尚未发布,可能更改)
1 计数器及其衍生物
1.1 计数器
计数器是最为基础的电路元件之一,它可以实现对于计数的功能。对于一个计数器,一般需要包含以下几个接口:
- din:输入数据,即强制置数的数据
- pe:置数使能,当其有效时,计数器将会将din的值赋值给计数器的当前值
- ce:计数使能,当其有效时,计数器将会在时钟上升沿将自身值减一
- dout: 输出数据,即当前计数器的值
- clk:时钟信号,计数器在时钟上升沿进行计数
- rstn:复位信号,当其有效时,计数器将会被复位为0
我们在实验中也给出了这样的计数器模板,你可以参考这个模板来实现你的计数器。
module counter #(
parameter WIDTH = 32,
RST_VLU = 0
)(
input clk, rstn,
input pe, ce,
input [WIDTH-1:0] d,
output reg [WIDTH-1:0] q
);
always @(posedge clk) begin
if (!rstn) q <= RST_VLU;
else if (pe) q <= d;
else if (ce) q <= q - 1;
end
endmodule
1.2 定时器
定时器是一种特殊的计数器,它可以实现对于时间的计数。
在本次实验中,我们提到的定时器内部例化了FSM、计数器和一个相等比较器。在相等比较器中,计数器的计数结果与内置的固定数据(对应计时结束,一般是0,取决于你的设计)比较后,将比较结果(0或1)输入给FSM参与状态转移控制。
作为一个定时器而言,我们需要实现以下几个功能:
- 可以给定时器设置定时周期数
- 当定时器值为0时,输出定时器到时信号,并不再继续计数
因此,一个定时器的设计应当如下:
善用ce和pe接口
在这个设计中,我们不要求大家一定要对第一节中给出的计数器进行例化,你可以重新用always@(posedge clk)来实现你的定时器。但是,我们要求绝对不可以将非时钟信号的边沿作为敏感变量,否则你的设计将会被认为是错误的。为了解决这个问题,ce和pe两个接口的原理将会给你很大的帮助。
1.3 分频器
分频器也是一种特殊的计数器,它通过对时钟进行计数,输出一个较低时钟频率的新时钟,从而实现对于时钟频率的控制。作为一个分频器而言,我们需要实现以下几个功能:
- 可以给分频器设置分频系数
- 能够提供方波和脉冲两种输出模式
方波就是占空比为50%的方波,脉冲就是占空比为1/(k+1)的方波(k位分频比)。也就是说,方波在分频后一个周期内0和1的时间相等,而脉冲仅仅输出一个持续时间为原时钟周期的脉冲。
分频器的k一般都比较大,比如32'd20_000_000可将20MHz分频为1Hz,因此你不需要花费太多精力考虑奇偶问题和k过小引发的电平不动问题。
因此,一个分频器的设计应当如下:
不厌其烦的提示
请理解助教在这里不厌其烦的提示:
绝对不可以将非时钟信号的边沿作为敏感变量,否则你的设计将会被认为是错误的。
绝对不可以将非时钟信号的边沿作为敏感变量,否则你的设计将会被认为是错误的。
绝对不可以将非时钟信号的边沿作为敏感变量,否则你的设计将会被认为是错误的。
同样地,为了解决这个问题,ce和pe两个接口的原理将会给你很大的帮助。
怎么这么烦啊,为什么不能用非时钟信号的上升沿啊!
在我们的开发板上,有且仅有时钟的信号可以被认为是绝对稳定的信号(也就是说极难出现毛刺),其他的信号因为组合延迟的原因,极易出现毛刺信号,导致整个设计的错误。这种错误在仿真里是完全无法被发现的,但上板就会出现问题。所以,绝对不可以将非时钟信号的边沿作为敏感变量,否则你的设计将会被认为是错误的。
那么,分频器的输出可以当做别的模块的时钟信号吗?
答案是几乎可以的。因为分频器的设计非常简单,组合延迟也比较低。所以,我们可以认为分频器的输出是一个相对稳定的信号。但这里一定要注意的是,对于脉冲的输出,助教建议不要使用以下语法:
```verilog
assign dout = (cnt == k)
```
当cnt位宽比较大的时候,这种描述容易出现毛刺——这是因为vivado并不会对assign的逻辑进行优化。不过,如果你把这个描述换成always_comb(即always@(*)),那么viavdo就会对这个逻辑进行优化,从而避免毛刺的出现。
1.4 按键、开关去抖动
当我们按下开发板上的按键或按钮的时候,极有可能出现一些抖动的信号(相信大家在第三次实验已经大有体会了)。这个问题依然可以通过计数器来解决:
也就是说,只有当按键或者开关的状态保持一定时间后,我们才认为这个按键或者开关的状态是稳定的。这个时间可以根据实际情况进行调整,但一般来说,100ms是一个比较合适的时间。
2 有限状态机FSM
本次实验中要求设计一个有限状态机FSM。比如有待机、置数、计数三个状态的FSM,接受输入信号st/rst/相等比较器输出的控制,更改状态并输出置数信号pe/计数信号ce/定时结束信号td。
注意下图的FSM和CNT以及没有画出的相等比较器实际上是TM的内部模块。在相等比较器中,计数器的计数结果与内置的固定数据(对应计时结束,一般是0,取决于你的设计)比较后,将比较结果(0或1)输入给FSM参与状态转移控制。
要求td信号的产生写成三段式,pc/ce信号的产生写成两段式。
2.1 有限状态机的基本概念
有限状态机是一种计算模型,它可以根据输入信号和当前状态,通过状态转移逻辑计算出下一个状态和输出信号。有限状态机由状态寄存器和组合逻辑电路组成,其中状态寄存器用于存储当前状态,组合逻辑电路用于计算下一个状态和输出信号。
2.1.1 状态的概念
状态是有限状态机的核心概念,它是指有限状态机在某一时刻所处的状态,这个状态往往是通过一个寄存器的值来表示的。例如,对于一个4'b1000序列检测器,其状态就可以定义为:
- S0:初始状态,输入序列为0
- S1:输入序列为1
- S2:输入序列为10
- S3:输入序列为100
- S4:输入序列为1000
由此,我们可以对所有的状态进行编码,编码有两种方式:
- 顺序编码:状态的编码按照状态的顺序进行编码,例如上述状态的顺序编码为S0=3'b000,S1=3'b001,S2=3'b010,S3=3'b011,S4=3'b100
- 独热编码:状态的编码只有一个位为1,其他位为0,例如上述状态的独热编码为S0=5'b00001,S1=5'b00010,S2=5'b00100,S3=5'b01000,S4=5'b10000
独热编码 vs 顺序编码
独热编码和顺序编码各有优劣,独热编码的优势在于输出信号的选择逻辑会减少,但是其缺点在于状态数较多时,编码的位数会很多,而且状态之间的转移逻辑也会变得复杂。顺序编码的优势在于状态数较多时,编码的位数较少,而且状态之间的转移逻辑也会变得简单,但是其缺点在于输出信号的编码器会更加复杂。
2.1.2 状态转移逻辑
状态转移逻辑是有限状态机的核心,它用于计算下一个状态,而下一个状态就是有限状态机的状态寄存器下一个周期会变成的值。一般情况下,我们可以用一个状态转移图来直观地描述状态转移的逻辑,例如下图所示的状态转移图:
上图的状态转移图中有两个状态:S0和S1。当状态为S0时,若输入为1,则转移到S1状态,否则转移到S0状态;当状态为S1时,若输入为0,则转移到S0状态,否则转移到S1状态。我们可以用一个状态转移表来描述这个状态转移逻辑:
状态 | 输入 | 下一个状态 |
---|---|---|
S0 | 0 | S0 |
S0 | 1 | S1 |
S1 | 0 | S0 |
S1 | 1 | S1 |
2.1.3 输出逻辑
输出逻辑是有限状态机的对外接口,有限状态机需要把自己状态反映的内容通过输出信号告知外部模块(例如已经检测到了对应的序列等)。输出逻辑的计算方式有两种:
- Moore型:输出逻辑只与当前状态有关,与输入信号无关
- Melay型:输出逻辑与当前状态和输入信号有关
输出逻辑可能会比较复杂,因此我们为了优化时序,也衍生出了两种输出方式:
- 直接输出:输出逻辑直接使用组合逻辑进行输出。
- 寄存器输出:输出信号先输入到寄存器里,下个周期通过寄存器来输出。
这几种输出方式各有千秋,下面我们就对其中几种常见的组合进行介绍。
2.2 一段式状态机
在使用verilog进行状态描述的时候,我们主要分为以下三个部分进行描述:
- 状态寄存器
- 状态转移逻辑
- 输出逻辑
一段式状态机,就是将这三部分内容全部用一个always@(posedge clk)进行描述,其电路图可以描述如下:
其中,蓝框就表示了将所有逻辑放在同一个always中进行描述,其模板代码如下:
always @(posedge clk, negedge rstn) begin
if (!rstn) begin
cs <= 复位状态;
out <= 复位值;
end
else begin
case (cs)
S0: begin
if (in条件) begin
cs <= Si;
out <= 表达式
end
……
end
一段式状态机将所有的内容都“混为一谈”,但却深受大家的喜爱。
2.3 两段式状态机
两段式状态机,就是将状态状态更新和状态转移、输出分开进行描述,其电路图可以描述如下:
这种状态机使用当前状态决定输出信号,由于输出信号直接通过当前状态和所有输出候选值通过多选器决定,因此输出逻辑的组合延迟比较大。但两段式状态机同时也在很多场合是非常必要的,因为它给出输出很及时,往往在很多场合会有较大的应用价值。其verilog代码如下:
//时序描述CS
always @(posedge clk, negedge rstn) begin
if (!rstn) cs <= 复位状态;
else cs <= ns;
end
//组合描述OUT和NS
always @* begin
out = 默认值;
ns = cs;
case (cs)
S0: begin
if (in条件) begin
out = 表达式;
ns = Si;
end
……
end
需要注意的是,两段式状态机,也可以使用三个always来实现,但第三段的输出逻辑一定是always @(*)的组合描述:
//时序描述CS
always @(posedge clk, negedge rstn) begin
if (!rstn) cs <= 复位状态;
else cs <= ns;
end
//组合描述NS
always @* begin
ns = cs;
case (cs)
S0: begin
if (in条件) begin
ns = Si;
end
……
end
//组合描述OUT
always @* begin
out = 默认值;
case (cs)
S0: begin
if (in条件) begin
out = 表达式;
end
……
end
虽然这样的实现好像也是“三段”,其很多网络教程也称之为三段式状态机,但其和三段式状态机希望让大家达到的效果是不一样的——最后两个always当然可以合并成同一个always,因此我们依然称之为两段式状态机。
2.4 三段式状态机
和两段式状态机有所区别,三段式状态机使用寄存器进行输出,也就是说**第三段输出描述依然使用always@(posedge clk)。在这里,我们衍生出来两种三段式状态机:
- 根据当前状态进行输出的三段式状态机:这种状态机的第三段输出是根据当前状态进行输出的,其电路图如下:
细心的同学可能发现了:这样的输出会延迟一个周期给出——这在很多情况下是完全无法接受的。很多电路要求输出信号的及时性,一个周期的差距就可能存在很多问题。因此,我们还有另一种三段式状态机:
- 根据下一个状态进行输出的三段式状态机:这种状态机的第三段输出是根据下一个状态进行输出的,其电路图如下:
这种状态机是通过下一个状态来决定输出的。如果状态机是Moore型状态机,那么输出不会出现任何延迟,且时序较好。但如果状态机是Mealy型状态机,这种设计就会出现问题。如果一定需要当前状态和当前输入同时决定输出,那么只能使用两段式状态机进行设计。
注意不同的赋值
在本次实验中,我们接触到了时序电路中的赋值,它和组合电路不同,需要使用<=进行赋值。这个赋值是非阻塞的,也是就是同一个分支中所有赋值没有先后关系,同时完成赋值。在三段式状态机的状态寄存器和输出逻辑中,我们需要使用<=进行赋值,而不是=进行赋值。
这一点是很多同学在实验中出现问题的地方,需要大家注意。(提示:这是普通班实验检查扣分的重点)一个不需要动脑子的真理就是:
always@(posedge clk)中的赋值一定是<=,其他地方的赋值一定是=
借助这个非阻塞赋值的特点,我们可以完成一些“骚操作”,例如在同一时间交换两个寄存器的值:
注意
本实验分为两周完成,第一周完成定时器部分(以上内容),在10月31日验收;第二周完成串口部分(以下内容),在11月7日验收。 串口部分尚未发布,实验要求可能有更改。
3 移位器及其应用
移位器事实上就是一个很长的寄存器,当移位使能有效时,它会将自身的值左移或右移一个固定的位宽。移位器的输出就是这个长寄存器的某些位(一般是低若干位)。
移位器一般没有必要真的使用右移运算符来实现——这样的开销是比较大的。我们可以直接使用位拼接来实现移位器的功能。
3.1 串口
串口是最简单的通讯方式之一,我们通过将一个字节的数据逐个位顺序发送,从而实现对于数据的传输。只要通讯双方都能“理解”彼此的数据格式,就可以实现通讯。
那么,串口是如何约定数据格式的呢?我们这里介绍本实验中要求实现的最简单的协议:
对于发送方,面向接收方的这个单比特数据通道在空闲时应当保持为1,这样接收方就知道此刻不是有效数据了。当需要发送数据时,这个通道里的数据会被突然置为0,并保持一个周期——这个周期在接收方看来,就是“全体目光向我看齐,我宣布个事”,这一位数据也被称为起始位。从下一个周期开始,发送方会把要传输的8bit数据从低位到高位依次发送到数据通道里,这个过程会持续8个周期(因为是8位)。
传输之后,串口不能立刻传输下一个数据,而是要强行把数据通道置为1,用来告知发送方“我说完了,你可以走了”。这个过程也会持续一个周期。
在这个过程中,发送方和接收方约定了以下几件事:
- 起始位保持1个周期
- 传输8bit数据
- 结束位保持1个周期
- 传输速率都是每秒9600个bit
只要双方都按照这个规则来发送、接收、拼接数据,就可以得到一个完整的8bit数据。
3.2 串行通信发送接口
发送接口左侧是面向需要发送数据的模块的,右侧是面向FPGA开发板上的串行通讯外设的。我们需要实现的功能是:
- 当需要发送数据的模块将tx_vld置为1时,TIF会从din中接收到需要发送的数据,并将其拼接好起始位和停止位,写入内部的移位寄存器中
- 移位寄存器每个周期会将最低位的数据发送到txd上,并右移一个bit
- 当移位寄存器把所有位全部发送完毕后,tx_rdy会被置为1,表示发送完毕,外部模块就可以向TIF发送下一个待发送数据了
发送时序
- tx_vld和tx_rdy均有效时,数据din前后附加启动和停止位后存入SOR,tx_rdy清零,移位计数器CNT加载常数8
- 随后SOR通过右移实现串行输出,同时CNT递减
- CNT等于0时,停止移位和计数,tx_rdy置1
3.3 串行通信接收接口
接收模块的总体逻辑和发送模块是比较类似的,但接受的逻辑更加复杂一点点,涉及到一个重要的概念——采样。
何谓“采样”
对于任何一个通信模块而言,我们都不能期望在确定的时钟周期中检测到确定的值。我们通常会用一个速度较快的时钟,对接收到的信号做检测。由于发送接收的模块的时钟是较慢的,而接收模块的时钟较快,所以我们肯定能检测到被发送的信号。同样地,为了避免发送通道中的“杂音”,接收模块需要“确认”收到了正确的数据,这个过程就可以通过稳定检测到信号持续n个周期来实现。
接收时序
- 当检测到rxd信号由1到0跳变,且8个接收时钟(clk_rx)后检测仍为0,则确认为起始位
- 接着每隔16个接收时钟,对rxd检测一次,依次作为数据位、校验位和停止位,存入SIR
- 若未发生停止位或奇偶校验位出错,则将SIR中数据位存入输入缓冲寄存器(Input Buffer Register, IBR),并设置rx_vld有效
只要连续16个周期信号都是稳定的,那么我们就可以认为接收到了1bit数据。我们再通过一个移位器将其拼接好,当所有的数据都返回后,将rxd_valid置为1,表示接收完毕。
注意xdc文件中的信号
在xdc文件中,rxd和txd的接口是如下两行:
set_property -dict { PACKAGE_PIN C4 IOSTANDARD LVCMOS33 } [get_ports { UART_TXD_IN }]; #IO_L7P_T1_AD6P_35 Sch=uart_txd_in
#et_property -dict { PACKAGE_PIN D4 IOSTANDARD LVCMOS33 } [get_ports { UART_RXD_OUT }]; #IO_L11N_T1_SRCC_35 Sch=uart_rxd_out
特别注意,rxd应该接入到UART_TXD_IN, txd应该接入到UART_RXD_OUT。这是因为我们的开发板上的串口是相对PC机而言的,所以我们的接口也应该是相对PC机而言的。