ZYNQ-FPGA部分之IP库 单核RAM读写数据

根据正点原子教程视频学习

单核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类型数据何时使用?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值