1. 进阶提高篇

完成了“基础入门篇”和“学习强化篇”两个板块的学习,我们开始进入“进阶提高篇”,“进阶提高篇”板块涉及知识点较多、较难,但这不是诸位打退堂鼓的理由,我们应该英男迎难而上,相信读者完成学习后会有质的飞跃。

在本板块,笔者会带领读者“手撕”SDRAM控制器,为读者详细讲解SDRAM的相关知识,手把手带领读者设计、实现、验证、使用自己独立编写的SDRAM控制器;完成SDRAM控制器的编写后,将SDRAM控制器与显示器、摄像头、音频芯片结合,以SDRAM为数据缓存完成图片、实时图像的显示以及录音音频再现;同 时,笔者会带领读者设计、完成SD卡驱动器,与SDRAM控制器结合,完成SD存储图像显示、音乐播放;最后,学习以太网相关知识,设计、实现百兆以太网,与SDRAM控制器结合,实现以太网的图片、实时图像、音频信号的传输。

读者要认认真真、脚踏实地完成本板块的学习,认真完成学习后,相信读者会有所收获。

2. 乒乓操作

本章节为大家介绍一种FPGA的重要设计思想——乒乓操作。乒乓操作是一个经常用于数据流控制的处理技术,具有节约缓冲空间、对数据流无缝处理等特点。

2.1. 理论学习

2.1.1. 乒乓操作简介

典型的乒乓操作处如图 52‑1所示。

pingpa002

图 52‑1 典型乒乓操作示意图

根据上图为大家讲述乒乓操作的处理过程。外部输入数据流通过输入数据流选择单元将数据流输入到数据缓存模块,比较常用的存储单元有双口RAM,FIFO,SDRAM等。在第一个缓冲周期,数据流通过“输入数据流选择单元”将数据写入“数据缓冲模块1”。写完之后进入第二个缓冲周期,在第二个缓冲周期数据流通过“输入数 据流选择单元”将数据写入到“数据缓冲模块2”的同时“输出数据流选择单元”将“数据缓冲模块1”的数据流读出,此时进入第三个缓冲周期。在第三个缓冲周期数据流通过“输入数据流选择单元”将数据写入到“数据缓存模块1”的同时将“数据缓冲模块2”的数据读出。如此反复循环地操作,即为乒乓操作。

2.1.2. 乒乓操作特点

乒乓操作的最大特点是通过“输入数据流选择单元”和“输出数据选择单元”按节拍、相互配合的切换,将经过缓冲的数据流没有停顿地送到“数据流运算处理模块”进行运算与处理。把乒乓操作模块当做一个整体,站在这个模块的两端看数据,输入数据流和输出数据流都是连续不断的,没有任何停顿的,因此非常适合对数据流进行流水线 式处理。所以乒乓操作常常应用于流水线式算法,完成数据的无缝缓冲与处理。

乒乓操作的第二个特点是可以节约缓存空间,使用双存储单元比单存储单元更节省存储空间,这是很明显的。同时在某些数据处理时,必须要数据达到一定个数才能进行运算,故还可以达到数据缓存的目的。

乒乓操作还可以实现低速模块处理高速数据,这种处理方式可以实现数据的串并转换,就是数据位宽之间的转换,是面积与速度互换原则的体现。

下面通过实验带领大家进一步地理解乒乓操作的处理方式。

2.2. 实战演练

2.2.1. 实验目标

实验目标:使用两个读写时钟不同的双口RAM实现低速模块处理高速数据的乒乓操作。

2.3. 程序设计

2.3.1. 整体说明

首先我们先画出工程的整体模块框图,如图 52‑2所示。

pingpa003

图 52‑2 乒乓操作模块整体框图

如上图所示,ram为数据缓冲模块,这里我们使用两个双口RAM来缓存数据。clk_gen为时钟生成模块,使用PLL核来生成不同的读写时钟。data_gen为数据生成模块,产生输入数据(由于我们是举例为大家讲解乒乓操作,数据就由我们自己产生)。ram_ctrl为输入输出数据流选择模块。pingpang为 顶层模块,各模块简介如表格 52‑1所示。

表格 52‑1 工程模块简介

模块名称

功能描述

clk_gen

时钟生成模块

ram

数据缓冲模块

data_gen

数据生成模块

ram_ctrl

ram控制模块,对输入输出数据进行选择

pingpang

乒乓操作顶层模块

下面分模块为大家讲解。

2.3.1.1. 时钟生成模块

这里我们调用PLL核来生成RAM的读写时钟,这里我们生成一个50MHz时钟(写时钟),一个25MHz时钟(读时钟)。使用50MHz时钟输入数据,25MHz时钟输出数据。具体的调用方法在IP核章节已有详细的讲解,这里就不再过多说明了。

2.3.1.2. 数据缓冲模块

该实验我们调用RAM IP核作为存储单元来完成乒乓操作。这里我们调用RAM需要注意的是:由于我们是使用不同时钟进行数据输入输出,所以我们需要使用不同的RAM读写时钟,由于我们是使用50MHz时钟输入数据,25MHz时钟输出数据,所以这里我们需要设置RAM的写入时钟为50MHz,读出时钟为25MHz。

由于我们设置的读写时钟不一致,而要实现乒乓操作的无缝缓存与处理,这里我们就需要设置不同的写入数据位宽与不同的读出数据位宽才能与读写时钟相匹配。这里就是我们前面理论部分所说的面积与速度互换原则的体现,这里我们设置的输入时钟的频率是输出时钟的两倍,即输入数据的速度是输出数据速度的两倍,所以这里我们需要设 置输出数据的位宽是输入数据位宽的两倍,即面积的两倍。换句话说就是我们输入速度与面积的乘积与输出速度与面积的乘积要相等,即输入和输出的时间相等,这样才能保证在“数据缓冲模块1”读/写完的同时“数据缓冲模块2”也写/读完,才能保证输入与输出数据的无缝传输与处理。这就是其低高速数据传输特点的原理,只要我们 遵从输入与输出数据的频率与位宽的乘积相等,那么我们就可以实现不同模块频率之间的数据传输。

这里我们设置写入RAM的数据位宽为8位,读出RAM的数据位宽为16位,深度都设置为128。当然大家也可自行设置时钟频率与数据位宽,只要频率与位宽的乘积相等即可。同样的原理若接收模块为低速模块,需输出的数据为串行数据(1bit)时,我们设置相应的频率和位宽即可实现数据的并行输入串行输出的无缝处理。

这里我们调用简单双口RAM即可满足该功能,具体的调用方法在IP核章节已有详细的讲解,大家跟着步骤讲解结合自己的需求去调用即可,这里就不再过多说明了。

2.3.1.3. 数据生成模块

该模块需要生成输入RAM中的数据,这里为了方便产生,我们循环生成数据8’d0~8’d199。8’d0~8’d99作为第一包数据写入第一个缓冲模块,8’d100~8’d199作为第二包数据写入第二个缓冲模块,依次循环写入。当然大家也可生成不同的数据流,只要满足我们RAM中设置的深度及位宽即可。模块框图 如图 52‑3所示。

模块框图

pingpa004

图 52‑3 数据生成模块框图

模块输入输出信号简介如表格 52‑2所示。

表格 52‑2 数据生成模块输入输出信号描述

信号

位宽

类型

功能描述

clk_50m

1bit

input

数据生成时钟,50MHz

rst_n

1bit

input

复位信号,低有效

data_en

1bit

output

数据使能信号,高有效

data_in

8bit

output

输出数据

这里我们生成一个数据的使能信号,当使能信号为高时,我们的输出数据有效,具体的产生时序我们通过绘制波形图进行介绍。

波形图绘制

pingpa005

图 52‑4 数据生成波形图

如上图所示,复位之后我们就开始拉高使能信号(data_en),让其开始传输数据。所以我们只需在使能为高时让数据像计数器一样一直加即可,加到199时让其归0从头开始相加,这样就能产生循环的数据了。

代码编写

参照绘制波形图,编写模块代码。模块参考代码,具体见代码清单 52‑1。

代码清单 52‑1 数据生成模块参考代码(data_gen.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
module data_gen
(
input wire clk_50m , //模块时钟,频率50MHz
input wire rst_n , //复位信号,低电平有效

output reg data_en , //数据使能信号,高电平有效
output reg [7:0] data_in //输出数据

);

 ////
 //\* Main Code \//
 ////

 //data_en:让其一直为高电平,一直输出数据
 always@(posedge clk_50m or negedge rst_n)
 if(rst_n == 1'b0)
 data_en <= 1'b0;
 else
 data_en <= 1'b1;

 //data_in:循环生成写入的数据(8'd0 ~ 8'd199)
 always@(posedge clk_50m or negedge rst_n)
 if(rst_n == 1'b0)
 data_in <= 8'd0;
 else if(data_in == 8'd199)
 data_in <= 8'd0;
 else if(data_en == 1'b1)
 data_in <= data_in + 1'b1;
 else
 data_in <= data_in;

 endmodule

2.3.1.4. 输入输出数据选择模块

该模块是乒乓操作的核心模块,我们需要通过该模块对输入输出数据进行选择,从而达到乒乓操作的处理效果。模块框图如图 52‑5所示。

pingpa006

图 52‑5 输入输出数据选择模块框图

该模块我们需要产生控制两个双口RAM的读写相关信号,同时将两个RAM中读出的数据作为输入,通过简单的处理后对读出的数据进行输出。该模块的各个信号简介,如表格 52‑3所示。

表格 52‑3 数据选择模块输入输出信号描述

信号

位宽

类型

功能描述

clk_50m

1bit

input

ram写数据时钟,频率50MHz

clk_25m

1bit

input

ram读数据时钟,频率25MHz

rst_n

1bit

input

复位信号,低有效

ram1_rd_data

16bit

input

ram1读出数据

ram2_rd_data

16bit

input

ram2读出数据

data_en

1bit

input

输入数据使能信号

data_in

8bti

input

输入数据

ram1_wr_en

1bit

output

ram1写使能

ram1_wr_addr

7bit

output

ram1写地址

ram1_wr_data

8bit

output

ram1写数据

ram1_rd_en

1bit

output

ram1读使能

ram1_rd_addr

6bit

output

ram1读地址

data_otu

16bit

output

输出乒乓操作数据

ram2_wr_en

1bit

output

ram2写使能

ram2_wr_addr

7bit

output

ram2写地址

ram2_wr_data

8bit

output

ram2写数据

ram2_rd_en

1bit

output

ram2读使能

ram2_rd_addr

6bit

output

ram2读地址

该模块要实现的功能,我们通过一个简单的状态机去进行了解,如图 52‑6所示。

pingpa007

图 52‑6 数据选择状态跳转图

IDLE:初始状态,在不工作或复位时就让状态机置为初始状态。

WRAM1:写RAM1状态。该状态我们开始往RAM1中写入数据,此时由于RAM2中并没有写入数据,所以我们不用对RAM2进行读取。那什么时候跳转到这个状态呢?从前面的数据生成模块中我们可知,当输入数据使能为高时,数据有效开始传输,所以当数据使能为高时我们让状态跳转到写RAM1状态,在该状态下将第一个 数据包(8’d0~8’d99)写入RAM1之中。

WRAM2_RRAM1:写RAM2读RAM1状态,当第一包数据写入完毕之后,马上跳到该状态,将第二包数据写入到RAM2中的同时读出RAM1中的写入的第一包数据。当第二包数据写完之后,我们的第一包数据应该也是刚好读完的,此时我们跳转到下一状态。

WRAM1_RRAM2:写RAM1读RAM2状态。在该状态下我们开始向RAM1中写入第三包数据,此时第三包数据会把第一包数据覆盖,而我们的第一包数据已经读取出来了,并不会使数据丢失。在往RAM1中写入第三包数据的同时,我们读出RAM2中的第二包数据,当读写完成之后,跳回WRAM2_RRAM1状态开始 下一包的数据写入以及读取,如此循环我们就能无缝地将连续的输入数据读取出来了。

通过状态机的讲解,相信大家对大概的控制流程都已了解,下面我们通过绘制波形图去具体讲解各个信号的时序逻辑。

波形图绘制

首先我们先看看RAM的写入相关信号的时序波形图,如图 52‑7所示。

pingpa008

图 52‑7 输入输出数据选择模块波形图(一)

前面说到们输入数据是用的50MHz时钟,所以我们我们使用50MHz时钟进行对各输入相关信号进行控制。

如上图所示,当检测到数据使能信号(data_en)为高时,状态跳转到WRAM1状态,这里我们使用时钟的下降沿进行检测触发跳转,这是为什么呢?在前面章节对RAM的学习我们知道,无论是写入还是读取都是时钟的上升沿进行的,而如果我们使用时钟的上升沿产生使能、地址、数据的话、写入或读取时上升沿采到的就是数据 变化的那一刻,这样采到的信号可能就是不稳定的状态,从而导致数据出错,所以这里我们使用时钟的下降沿去产生这些信号的话,上升沿就能采到数据的稳定状态了。

要往ram里写入数据,我们需要产生写使能,写地址,写数据。

ram1_wr_en:ram1写使能,初始值为0。当状态机为写RAM1状态时,我们让ram1写使能为高,这里我们可以使用组合逻辑赋值。

ram1_wr_addr:ram1写地址,初使值为0。当ram1写使能为高时让写地址开始相加,一个时钟写一个数据,同样采用时钟的下降沿触发。当地址加到8’d99时说明100个数据已经写完,。写完之后地址归0,状态机跳到下一状态。

ram1_wr_data:ram1写数据。ram1和ram2中的写数据都是由数据生成模块传过来的,而上一模块数据是由时钟上升沿产生的,所以这里我们需先对传来的数据,使用下降沿先进行寄存,当写使能为1时,让写入的数据为寄存的数据即可。这里我们使用组合逻辑赋值,这样使能、地址、数据在同一时钟沿下才能相互 对应。

当状态机跳转到WRAM2_RRAM1状态时,我们需要往ram2中写入数据的同时读取ram1中的数据(读取时序下一段为大家讲解)。往ram2中写入数据时:使能、地址和数据的时序产生方法与ram1的使能、地址和数据的时序产生方法是一致的,这里我们不在过多讲解了。

ram2写完之后状态机跳到WRAM1_RRAM2状态,在该状态我们需对ram1写,ram2读,相关信号的时序与前面状态的产生方法一致。写完之后又跳回WRAM2_RRAM1状态,如此循环。

ram写使能相关信号的时序讲解完之后,下面我们看看他们的读相关信号的时序该如何产生,如图 52‑8所示。

pingpa009

图 52‑8 输入输出数据选择模块波形图(二)

同写相关信号一样,读相关信号也使用时钟下降沿去进行产生,这样读数据时能采到稳定的读地址。我们使用的读时钟是25MHz时钟,所以读相关信号我们也使用该时钟去产生。

如上图所示:状态机是在clk_50m时钟的下降沿处变化的,在WRAM2_RRAM1状态时我们需要读取ram1里的数据,我们就需要产生读使能和读地址。在该状态下我们让读使能为1,此时我们不能用组合逻辑去进行产生读使能,而需要使用clk_25m时钟下降沿触发去产生,这样我们读使能才能与读时钟对应。

ram1_rd_addr:在读ram1使能信号为高时让其一直加即可,因为我们设置的读取数据位宽是16bit,是输入数据位宽的两倍,即读出的一个数据为写入的两个数据。所以当其地址加到49时,表明读出了50个16bit数据,这说明写入的100个8bit数据已读完。这个时候我们让地址归0,等待下一次的读取 。

当状态为WRAM1_RRAM2时,需要读取ram2中的数据,使能和地址的产生方法与ram1一致。

读使能信号和地址产生了之后,读时钟的上升沿采到使能和地址信号后就会读出数据,每个数据为16bit,为两个写入数据。即读取的第一个数据为写入的前两个数据16’h0100,写入ram1的最后两个数据为十进制的98、99,转换为16进制就是62、63,所以读取的最后一个数据为16’h6362。同理ram2 读取的第一个数据为16’h6564,最后一个数据为16’hc7c6。

data_out:乒乓操作输出的数据。当读ram1使能为高时,输出读ram1的值;读ram2使能为高时,输出读ram2的值。因为读数据是在读时钟上升出产生的,我们使用读时钟下降沿将读出的数据给data_out输出,这样就能无缝地把写入的数据全部输出。

代码编写

参照绘制波形图,编写模块代码。模块参考代码,具体见代码清单 52‑2。

代码清单 52‑2 输入输出数据选择模块参考代码(ram_ctrl.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
module ram_ctrl
(
input wire clk_50m , //写ram时钟,50MHz
input wire clk_25m , //读ram时钟,25MHz
input wire rst_n , //复位信号,低有效
input wire [15:0] ram1_rd_data, //ram1读数据
input wire [15:0] ram2_rd_data, //ram2读数据
input wire data_en , //输入数据使能信号
input wire [7:0] data_in , //输入数据

output reg ram1_wr_en , //ram1写使能
output reg ram1_rd_en , //ram1读使能
output reg [6:0] ram1_wr_addr, //ram1写地址
output reg [5:0] ram1_rd_addr, //ram1读地址
output wire [7:0] ram1_wr_data, //ram1写数据
output reg ram2_wr_en , //ram2写使能
output reg ram2_rd_en , //ram2读使能
output reg [6:0] ram2_wr_addr, //ram2写地址
output reg [5:0] ram2_rd_addr, //ram2读地址
output wire [7:0] ram2_wr_data, //ram2写数据
output reg [15:0] data_out //输出乒乓操作数据

);

////
//\* Parameter and Internal Signal \//
////

//parameter define
parameter IDLE = 4'b0001, //初始状态
WRAM1 = 4'b0010, //写RAM1状态
WRAM2_RRAM1 = 4'b0100, //写RAM2读RAM1状态
WRAM1_RRAM2 = 4'b1000; //写RAM1读RAM2状态

//reg define
reg [3:0] state ; //状态机状态
reg [7:0] data_in_reg ; //数据寄存器

////
//\* Main Code \//
////

//使用组合逻辑赋值,这样使能和数据地址才能对应
assign ram1_wr_data = (ram1_wr_en == 1'b1) ? data_in_reg: 8'd0;
assign ram2_wr_data = (ram2_wr_en == 1'b1) ? data_in_reg: 8'd0;

//使用写数据时钟下降沿寄存数据,使数据写入存储器时上升沿能踩到稳定的数据
always@(negedge clk_50m or negedge rst_n)
if(rst_n == 1'b0)
data_in_reg <= 8'd0;
else
data_in_reg <= data_in;

//状态机状态跳转
always@(negedge clk_50m or negedge rst_n)
if(rst_n == 1'b0)
state <= IDLE;
else case(state)
IDLE://检测到数据使能信号为高时,跳转到下一状态将数据写到RAM1
if(data_en == 1'b1)
state <= WRAM1;
WRAM1://RAM1数据写完之后,跳转到写RAM2读RAM1状态
if(ram1_wr_addr == 7'd99)
state <= WRAM2_RRAM1;
WRAM2_RRAM1://RAM2数据写完之后,跳转到写RAM1读RAM2状态
if(ram2_wr_addr == 7'd99)
state <= WRAM1_RRAM2;
WRAM1_RRAM2://RAM1数据写完之后,跳转到写RAM2读RAM1状态
if(ram1_wr_addr == 7'd99)
state <= WRAM2_RRAM1;
default:
state <= IDLE;
endcase

//RAM1,RAM2写使能赋值
always@(*)
case(state)
IDLE:
begin
ram1_wr_en = 1'b0;
ram2_wr_en = 1'b0;
end
WRAM1:
begin
ram1_wr_en = 1'b1;
ram2_wr_en = 1'b0;
end
WRAM2_RRAM1:
begin
ram1_wr_en = 1'b0;
ram2_wr_en = 1'b1;
end
WRAM1_RRAM2:
begin
ram1_wr_en = 1'b1;
ram2_wr_en = 1'b0;
end
default:;
endcase

 //RAM1读使能,使用读时钟赋值
 always@(negedge clk_25m or negedge rst_n)
 if(rst_n == 1'b0)
 ram1_rd_en <= 1'b0;
 else if(state == WRAM2_RRAM1)
 ram1_rd_en <= 1'b1;
 else
 ram1_rd_en <= 1'b0;

 //RAM2读使能,使用读时钟赋值
 always@(negedge clk_25m or negedge rst_n)
 if(rst_n == 1'b0)
 ram2_rd_en <= 1'b0;
 else if(state == WRAM1_RRAM2)
 ram2_rd_en <= 1'b1;
 else
 ram2_rd_en <= 1'b0;

 //RAM1写地址
 always@(negedge clk_50m or negedge rst_n)
 if(rst_n == 1'b0)
 ram1_wr_addr <= 7'd0;
 else if(ram1_wr_addr == 7'd99)
 ram1_wr_addr <= 7'd0;
 else if(ram1_wr_en == 1'b1)
 ram1_wr_addr <= ram1_wr_addr + 1'b1;

 //RAM2写地址
 always@(negedge clk_50m or negedge rst_n)
 if(rst_n == 1'b0)
 ram2_wr_addr <= 7'b0;
 else if(ram2_wr_addr == 7'd99)
 ram2_wr_addr <= 7'b0;
 else if(ram2_wr_en == 1'b1)
 ram2_wr_addr <= ram2_wr_addr + 1'b1;

 //RAM1读地址
 always@(negedge clk_25m or negedge rst_n)
 if(rst_n == 1'b0)
 ram1_rd_addr <= 6'd0;
 else if(ram1_rd_addr == 6'd49)
 ram1_rd_addr <= 6'b0;
 else if(ram1_rd_en == 1'b1)
 ram1_rd_addr <= ram1_rd_addr + 1'b1;

 //RAM2读地址
 always@(negedge clk_25m or negedge rst_n)
 if(rst_n == 1'b0)
 ram2_rd_addr <= 6'd0;
 else if(ram2_rd_addr == 6'd49)
 ram2_rd_addr <= 6'b0;
 else if(ram2_rd_en == 1'b1)
 ram2_rd_addr <= ram2_rd_addr + 1'b1;

 //将乒乓操作读出的数据选择输出
 always@(negedge clk_25m or negedge rst_n)
 if(rst_n == 1'b0)
 data_out <= 16'd0;
 else if(ram1_rd_en == 1'b1)
 data_out <= ram1_rd_data;
 else if(ram2_rd_en == 1'b1)
 data_out <= ram2_rd_data;
 else
 data_out <= 16'd0;

 endmodule

代码是根据绘制的波形图进行讲解的,在波形图讲解对各个信号都有了详细的讲解,这里就不再过多叙述了。本设计思路只做参考,并非唯一方法,读者也可利用所学知识,按照自己思路进行设计。

2.3.1.5. 顶层模块

模块框图

pingpa010

图 52‑9 乒乓操作顶层模块

该工程我们只需要输入时钟复位即可。

代码编写

顶层代码编写较为容易,无需波形图的绘制。顶层参考代码,具体见代码清单 52‑3。

代码清单 52‑3 乒乓操作顶层模块参考代码(pingpang.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
module pingpang
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n //复位信号,低有效

);

////
//\* Parameter And Internal Signal \//
 ////

 //wire define
 wire clk_50m ; //50MHz时钟
 wire clk_25m ; //100MHz时钟
 wire rst_n ; //复位信号
 wire [15:0] ram1_rd_data ; //ram1读数据
 wire [15:0] ram2_rd_data ; //ram2读数据
 wire data_en ; //输入数据使能信号
 wire [7:0] data_in ; //输入数据
 wire ram1_wr_en ; //ram1写使能
 wire ram1_rd_en ; //ram1读使能
 wire [6:0] ram1_wr_addr ; //ram1写地址
 wire [5:0] ram1_rd_addr ; //ram1写地址
 wire [7:0] ram1_wr_data ; //ram1写数据
 wire ram2_wr_en ; //ram2写使能
 wire ram2_rd_en ; //ram2读使能
 wire [6:0] ram2_wr_addr ; //ram2写地址
 wire [5:0] ram2_rd_addr ; //ram2写地址
 wire [7:0] ram2_wr_data ; //ram2写数据
 wire [15:0] data_out ; //输出乒乓操作数据
 wire locked ; //PLl核输出稳定时钟标志信号,高有效

 //时钟不稳定时视为复位
 assign rst_n = sys_rst_n & locked;

 ////
 //\* Instantiation \//
 ////

 //----------- ram_ctrl_inst -----------
 ram_ctrl ram_ctrl_inst
 (
 .clk_50m (clk_50m ), //写ram时钟,50MHz
 .clk_25m (clk_25m ), //读ram时钟,25MHz
 .rst_n (rst_n ), //复位信号,低有效
 .ram1_rd_data(ram1_rd_data ), //ram1读数据
 .ram2_rd_data(ram2_rd_data ), //ram2读数据
 .data_en (data_en ), //输入数据使能信号
 .data_in (data_in ), //输入数据

 .ram1_wr_en (ram1_wr_en ), //ram1写使能
 .ram1_rd_en (ram1_rd_en ), //ram1读使能
 .ram1_wr_addr(ram1_wr_addr ), //ram1读写地址
 .ram1_rd_addr(ram1_rd_addr ), //ram1读地址
 .ram1_wr_data(ram1_wr_data ), //ram1写数据
 .ram2_wr_en (ram2_wr_en ), //ram2写使能
 .ram2_rd_en (ram2_rd_en ), //ram2读使能
 .ram2_wr_addr(ram2_wr_addr ), //ram2写地址
 .ram2_rd_addr(ram2_rd_addr ), //ram2读地址
 .ram2_wr_data(ram2_wr_data ), //ram2写数据
 .data_out (data_out ) //输出乒乓操作数据

 );

 //----------- data_gen_inst -----------
 data_gen data_gen_inst
 (
 .clk_50m (clk_50m ), //模块时钟,频率50MHz
 .rst_n (rst_n ), //复位信号,低电平有效

 .data_en (data_en ), //数据使能信号,高电平有效
 .data_in (data_in ) //输出数据

 );

 //----------- clk_gen_inst -----------
 clk_gen clk_gen_inst
 (
 .areset (~sys_rst_n ), //异步复位
 .inclk0 (sys_clk ), //输入时钟

 .c0 (clk_50m ), //输出时钟,频率50MHz
 .c1 (clk_25m ), //输出时钟,频率25MHz
 .locked (locked ) //时钟稳定输出标志信号

 );

 //------------ dq_ram1-------------
 dp_ram dp_ram1
 (
 .data (ram1_wr_data ),
 .rdaddress (ram1_rd_addr ),
 .rdclock (clk_25m ),
 .rden (ram1_rd_en ),
 .wraddress (ram1_wr_addr ),
 .wrclock (clk_50m ),
 .wren (ram1_wr_en ),

 .q (ram1_rd_data )

 );

 //------------ dq_ram2-------------
 dp_ram dp_ram2
 (
 .data (ram2_wr_data ),
 .rdaddress (ram2_rd_addr ),
 .rdclock (clk_25m ),
 .rden (ram2_rd_en ),
 .wraddress (ram2_wr_addr ),
 .wrclock (clk_50m ),
 .wren (ram2_wr_en ),

 .q (ram2_rd_data )

 );
 endmodule

我们乒乓操作使用的是两个双口RAM,这里我们调用双口RAM例化两次,例化名不一样即可。

2.3.1.6. RTL视图

编译完成后,我们查看一下RTL视图,仔细看RTL视图展示信息与顶层模块框图是一致,具体见图 52‑10。

pingpa011

图 52‑10 实验工程RTL视图

2.3.1.7. 仿真验证

仿真代码编写

编写仿真代码,对参考代码进行仿真验证。仿真参考代码,具体见代码清单 52‑4。

代码清单 52‑4 乒乓操作仿真参考代码(tb_pingpang.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
module tb_pingpang();

//reg define
reg sys_clk ;
reg sys_rst_n ;

////
//\* Main Code \//
////

 initial
 begin
 sys_clk = 1'b1;
 sys_rst_n <= 1'b0;
 #200
 sys_rst_n <= 1'b1;
 end

 //sys_clk:模拟系统时钟,每10ns电平取反一次,周期为20ns,频率为50MHz
 always #10 sys_clk = ~sys_clk;

 ////
 //\* Instantiation \//
 ////

 //-------------pingpang_inst-------------
 pingpang pingpang_inst
 (
 .sys_clk (sys_clk ), //系统时钟
 .sys_rst_n (sys_rst_n ) //复位信号,低有效

 );

 endmodule

该工程的仿真代码只需要产生时钟和复位即可。

仿真波形分析

首先我们看看数据生成模块是否能正确生成我们实验所设计的数据。

pingpa012

图 52‑11 数据生成仿真波形图(一)

pingpa013

图 52‑12 数据生成仿真波形图(二)

如图 52‑11、图 52‑12所示抓取的是数据生成模块的一次循环数据的开头和结尾波形图。8’hc7即为十进制的199,可以看到该模块的波形图与我们所绘制的数据生成模块的波形图是一致的,能达到我们的数据生成要求。

pingpa014

图 52‑13 WRAM1状态仿真波形图(一)

pingpa015

图 52‑14 WRAM1状态仿真波形图(二)

如图 52‑13、图 52‑14抓取的是状态机为WRAM1(写RAM1)状态时的开头部分以及结尾部分的波形图。方框中为写RAM1相关信号的波形图,可以看到这与我们所绘制的波形图是一致的,ram1的使能、地址、数据也能达到我们的设计要求。

pingpa016

图 52‑15 WRAM2_RRAM1状态仿真波形图(一)

pingpa017

图 52‑16 WRAM2_RRAM1状态仿真波形图(二)

如图 52‑15、图 52‑16抓取的是状态机为WRAM2_RRAM1状态的开头及结尾部分的仿真波形图。其中框为写RAM2的相关信号时序,框为读RAM1的相关信号的时序。可以看到这些信号的时序关系与我们所绘制的波形图是一致的。

pingpa018

图 52‑17 WRAM1_RRAM2状态仿真波形图(一)

pingpa019

图 52‑18 WRAM1_RRAM2状态仿真波形图(二)

如图 52‑17、图 52‑18抓取的是状态机为WRAM1_RRAM2状态的开头及结尾部分的仿真波形图。其中框为写RAM1的相关信号时序,框为读RAM2的相关信号的时序。可以看到这些信号的时序关系与我们所绘制的波形图是一致的。

各状态的ram相关信号都与我们设计的一致,下面我们看看乒乓操作输出的数据(data_out)正不正确。

pingpa020

图 52‑19 乒乓操作输出数据仿真波形图(一)

pingpa021

图 52‑20 乒乓操作输出数据仿真波形图(二)

如图 52‑19、图 52‑20所示抓取的是读一次RAM1与RAM2输出的数据的开头及结尾部分波形图,可以看到输出数据的位宽为16bit。其中16’hc7c6即为十进制198、199拼接后的数据,这说明输出的数据即为我们输入的数据。

2.3.1.8. SignalTap波形抓取

由于该工程上板没有实际的效果显示,我们就使用Quartus软件的SignalTap工具实时抓取一下输出数据,看看其是否与我们仿真输出的数据一致。

pingpa022

图 52‑21 SignalTap仿真波形(一)

pingpa023

图 52‑22 SignalTap仿真波形(二)

由图 52‑21、图 52‑22可以看到输出的数据与我们仿真时输出的数据是一致的。这说明我们设计的工程是正确的,能达到我们的实验要求。

2.4. 章末总结

乒乓操作较为显著的特点是数据的无缝处理,只要大家理解了乒乓操作的处理技巧,那么当我们使用不同的存储单元作为缓冲模块时,操作起来也能得心应手。