根据正点原子教程视频学习
单核RAM表示只有一个口,不能同时进行读写,只能分别执行
实验任务:
理解IP核中单核RAM使用方法,并在0-31的地址上写入0-31再读出。(使用AX7020)
实验思路:
我认为IP就类似于stm32中已经写好的库函数,可以直接进行配置调用(类似于cubemax)
上图位视频教程中的结构图,其中RAM ip是配置调用模块,ram_rw是我们自己写的模块用来将一些必要的信息输入至ip核调用的模块,最大的ip_1port_ram是调用这两个模块的顶层模块,由于本模块读出和写入均可由仿真软件见到,因此只输入时钟与复位信号即可,其余ip核需要的输入可写入ram_rw模块。
实验步骤:
1.调用ram IP核
首先打开vivado,点击左边的的IP catalog(可以理解为IP核目录),在搜索中搜RAM
在RAM这一大类中选择block memory generator,在port a options中可以修改ram的宽度以及深度,其余的高级配置可以点击documentation查看用户手册。
这样就会生成RAM的ip核,在IP sources——bl_men_gen_0——instantian template中可以查看历化的历程。
2.分析电路时序⭐
在最近的学习中我发现分析电路的逻辑时序极为重要,由于fpga是并行处理程序,因此我们的程序是按照一个信号一个信号来写的,并且线画出时序图也有助于后续代码的编写。
下面这个图是视频课程中老师画的,这个图以及下方输入输出都是相对于ram_rw模块来说的,我们可以把这个模块理解为ip核的驱动模块,用于提供驱动要用的信号。
clk:输入信号,时钟,来自于顶层模块的输入
rst_n:输入信号,复位,来自于顶层模块的输入。0系统复位,1系统正常进行
ram_en:输出信号,此信号对于我们写的驱动ip核模块是输出信号,因为他要输出该信号至ip核模块,用于使能ip核的运行。0时停止运行ip核,1时正常进行
rw_cnt:内部信号(既不输入也不输出,相当于模块内计数用,用于区分读or写)
ram_we:输出信号,用于分辨ip核内到底进行读还是写,1是写操作,0是读操作
ram_addr:用于指定读or写时候对应的地址
ram_wr_data:用于说明写入ram内的数据(本次我们写入0-31这些数据)
ram_rd_data:ram读到的数据
因此这个图的意思是复位信号停止后系统进入写状态,在地址0-31写入0-31这些数据,再在写入之后读取出这些数据。
3.代码编写
其实最重要的是分析电路的逻辑时序,代码编写可以自己根据时序来描述即可。
下面提供我的代码:
module ram_rw(
input clk, //时钟
input rst_n, //复位
input [7:0] ram_rd_data, //读到的数据
output reg ram_en, //ram使能
output reg ram_we, //读/写?
output reg[4:0] ram_addr, //数据地址
output reg[7:0] ram_wr_data //写入数据内容
);
reg [5:0] rw_cnt ; //计数(0——31写)
这里是首先的定义数据部分,其实我还是不是很明白wire类型核reg类型到底是什么时候定义,欢迎各位大佬指导一下。如果不指定reg类型将默认位wire类型,如果不指定位数将默认位1位,这里的[7:0]指的是0-7位一共有八位也就是可以表示2^8的数据,最大位255的数据。
下面对ram_en模块进行编写,该模块的作用是为了在复位结束后开启ram ip核的使能
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0)
ram_en <= 1'b0;
else
ram_en <= 1'b1;
end
注意<=是阻塞赋值用于reg类型的数据。
下面位计数器模块
always @(posedge clk or negedge rst_n) begin
if (rst_n == 1'b0)
rw_cnt <= 6'b0;
else if (ram_en == 1'b1 && rw_cnt < 6'd63)
rw_cnt <= rw_cnt + 6'b1 ;
else if (ram_en == 1'b1 && rw_cnt >=6'd63)
rw_cnt <= 6'b0;
end
计数器模块是用于计数0-63这64个数据,前面32位用于读,后面32位用于写,因此计数器这个变量本身没有输入输出,他是一个标记信号,用于方便判断读还是写
下面位读写判断模块
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
ram_we <= 1'b0;
else if(ram_en ==1'b1 && rw_cnt < 6'd31 )
ram_we <= 1'b1;
else
ram_we <= 1'b0;
end
用上述计数器中的方法进行判断到底是读还是写
下面是地址模块
always @(posedge clk or negedge rst_n ) begin
if (rst_n == 1'b0)
ram_addr <= 5'b0;
else if (ram_en == 1'b1 && ram_addr == 5'd31)
ram_addr <= 5'b0;
else if(ram_en == 1'b1&& ram_addr != 5'd31)
ram_addr <= ram_addr + 5'b1;
end
因为我们写需要0-31这些地址,读也需要0-31这些地址,因此我们程序让在地址为31时候进行重置,但是结合阻塞赋值以及上升沿触发,在再addr为31的下一周期才会复位,符合我们的需求。
下面为写入数据的模块
always @(posedge clk or negedge rst_n ) begin
if (rst_n == 1'b0)
ram_wr_data <= 8'b0 ;
else if (ram_en == 1'b1 && rw_cnt <5'd31 )
ram_wr_data <= ram_addr + 8'b1 ;
else
ram_wr_data <= 8'b0;
end
由于我们只需要在ram_we拉高的时候写数据,因此意味着计数器为0-31时候写数据,其余时间段清0即可。所有模块写完了,不要忘了加endmodule。
下面我们需要写一个顶层模块来调用我们刚才写的驱动模块以及IP_RAM本身:
//本模块是主模块,用来例化ram_rw和ip核
module ip_1port_ram(
input sys_clk,
input sys_rst
);
wire [7:0] ram_rd_data;
wire ram_en ;
wire ram_we ;
wire [4:0] ram_addr ;
wire [7:0] ram_wr_data;
ram_rw u_ram_rw(
.clk ( sys_clk ) ,
.rst_n ( sys_rst ) ,
.ram_rd_data ( ram_rd_data ) ,
.ram_en ( ram_en ) ,
.ram_we ( ram_we ) ,
.ram_addr ( ram_addr ) ,
.ram_wr_data ( ram_wr_data )
);
blk_mem_gen_0 u_blk_mem_gen_0 (
.clka (sys_clk), // input wire clka
.ena (ram_en), // input wire ena
.wea (ram_we), // input wire [0 : 0] wea
.addra (ram_addr), // input wire [4 : 0] addra
.dina (ram_wr_data), // input wire [7 : 0] dina
.douta (ram_rd_data) // output wire [7 : 0] douta
);
endmodule
由最开始的结构图 可以知道,只需要时钟与复为输入,再将对应的端口对接起来即可,这个模块仅仅用于调用并接线,因此所有的信号都用wire定义。(ip_ram模块调用的程序在1.调用ram ip核中就有)
4.仿真分析
编写仿真的tb代码,由于只需要模拟输入系统时钟与复位信号因此tb模块较为简单,代码如下:
`timescale 1ns/1ns
module tb_ip_1ram();
parameter clk_period = 20;
reg sys_clk;
reg sys_rst;
//初始化复位
initial begin
sys_clk <= 1'b0;
sys_rst <= 1'b0;
#200
sys_rst <= 1'b1;
end
//初始化时钟
always #(clk_period/2) sys_clk =~sys_clk;
ip_1port_ram u_ip_1port_ram(
.sys_clk (sys_clk),
.sys_rst (sys_rst)
);
endmodule
第一行是是用于定义下方延迟的量程与单位
由于系统晶振为50m,1mhz是10^6因此一个周期就是1/50mhz即为2e-8s
而1ns=10^-9s,即2e-8s就是20ns。所以我们可以记住50mhz对应的周期为20ns。
所以我们分别初始化复位功能与时钟(每10ns翻转一次)再接上端口。
但是在这里我有不理解的一点是为什么这里的复位与时钟定义的是reg类型?
整体代码写好后可以进行仿真,这里我们选择使用vivado自带的仿真器进行仿真。
直接启动仿真,再右键点击u_ram_rw,把这个模块中的参数导入观察列表
再右键点击这些变量再Radix中将其设置为无符号十进制数,设置好后点击重新仿真。
重新仿真的暗流就是一个左三角加上一个竖线,然后点击右边的三角符号进行运行,运行一小下即可暂停
我们选择部分进行观看,与我们上面所画的波形图符合
但需要注意,读的模块稍稍有写延迟,这属于正常的。
实验总结:
1.verilog语言编码能力:需要注意数据的表示,已经定义位宽的数据后面调用要注意位宽一致。
2.IP核调用
3.仿真软件的使用
4.独立进行时序逻辑的分析
还不懂的地方:wire类型属于与reg类型数据何时使用?