标签: ROM

  • Verilog 功能模块–RAM 和 ROM(03)–自编 RAM 与 Vivado RAM IP 功能对比实测

    Verilog 功能模块–RAM 和 ROM(03)–自编 RAM 与 Vivado RAM IP 功能对比实测

    相关文章:

    Xilinx IP 解析之 Block Memory Generator v8.4 ——01-手册重点解读(仅Native RAM) – 徐晓康的博客

    Xilinx IP 解析之 Block Memory Generator v8.4 ——02-如何配置 IP(仅 Native RAM) – 徐晓康的博客

    Verilog功能模块–RAM和ROM(01)–功能说明与关键代码解析 – 徐晓康的博客

    Verilog功能模块–RAM和ROM(02)–同步写-写冲突与读-写冲突实测 – 徐晓康的博客

    Verilog 功能模块–RAM 和 ROM(03)–自编 RAM 与 Vivado RAM IP 功能对比实测 – 徐晓康的博客

    Verilog功能模块--RAM和ROM(01)--功能说明与关键代码解析

    前言

    前面文章已经说明了自编 RAM 和 Vivado RAM 的冲突特性,本文将进行两者的功能对比。主要包括初始化功能、使能功能、输出寄存器功能、操作模式功能等。

    主要进行 TDPRAM 的测试,对于 SDPRAM、SPRAM、DPROM、SROM 仅进行了少量实验。

    部分实验在 300MHz 的条件下进行,所以说也验证了本模块的最大运行频率能达到 300MHz。

    一、冲突抑制

    因为写-写冲突和读-写冲突会可能导致写入和读取的错乱,所以,本章所有测试都在顶层测试模块,避免了冲突的发生,相关冲突抑制的代码如下:

    reg ena_tmp;
    always @(posedge clka) begin
      ena_tmp <= random_num[14];
      wea     <= random_num[13];
      addra   <= random_num[ADDR_WIDTH-1+8 : 8];
      dina    <= random_num[DATA_WIDTH-1+2 : 2];
    end

    reg enb_tmp;
    always @(posedge clkb) begin
      enb_tmp <= random_num[7];
      web     <= random_num[5];
      addrb   <= random_num[ADDR_WIDTH-1 : 0];
      dinb    <= random_num[DATA_WIDTH-1 : 0];
    end

    // //* 避免写-写冲突 已被读写冲突覆盖
    // always @(*) begin
    //   if (addra == addrb && wea && ena && enb)
    //     web = 1'b0;
    //   else
    //     web = web_tmp;
    // end

    //* 避免A读-B写冲突
    always @(*) begin
      if (addra == addrb && web && enb_tmp)
        ena 
    1'b0;
      else
        ena = ena_tmp;
    end

    //* 避免B读-A写冲突
    always @(*) begin
      if (addra == addrb && wea && ena_tmp)
        enb 
    1'b0;
      else
        enb = enb_tmp;
    end

    //* 两RAM读数据是否一致?
    (* mark_debug *)reg douta_is_not_equal_vivado_douta;
    (* mark_debug *)reg doutb_is_not_equal_vivado_doutb;
    always @(posedge clka) begin
      douta_is_not_equal_vivado_douta <= douta != vivado_douta;
    end
    always @(posedge clkb) begin
      doutb_is_not_equal_vivado_doutb <= doutb != vivado_doutb;
    end

    其中,还包括了判断两 RAM 输出是否一致的代码。

    二、TDPRAM 功能对比实验设计

    2.1 实验设计

    序号 实测实验条件 波形图 通过 ILA 观测,自编 BRAM 和 Vivado BRAM IP 写入/读取是否一致
    1 1.TDPRAM
    2.同步时钟–100MHz
    3.A 端口和 B 端口均 Write First 模式
    4.无输出寄存器
    5.带使能信号 ena 和 enb
    6.使用相同的初始化文件 COE
    TDPRAM-ILA-1 完全一致
    功能结论:
    1.写优先功能正常
    2.使能功能正常
    3.初始化功能正常
    2 1.TDPRAM
    2.同步时钟–300MHz
    其余不变
    TDPRAM-ILA-2 完全一致
    功能结论:
    1.工作时钟频率可达 300MHz
    3 1.TDPRAM
    2.同步时钟–300MHz
    3.A 端口 Write First 模式,B 端口 Read First 模式
    其余不变
    TDPRAM-ILA-3 完全一致
    功能结论:
    Write First 模式和 Read First 模式工作正常
    4 1.TDPRAM
    2.同步时钟–300MHz
    3.A 端口 Write First 模式,B 端口 No Change 模式
    其余不变
    TDPRAM-ILA-4 完全一致
    功能结论:
    Write First 模式和 No Change 模式工作正常
    5 1.TDPRAM
    2.异步时钟,clka(300MHz),clkb(120MHz)
    3.A 端口和 B 端口均 Write First 模式
    其余不变
    TDPRAM-ILA-5 完全一致
    功能结论:
    异步时钟下工作正常
    6 1.TDPRAM
    2.异步时钟,clka(300MHz),clkb(120MHz)
    3.A 端口和 B 端口均 Write First 模式
    4.A 端口添加一个输出寄存器(Primitives Output Register)
    5.B 端口均添加两个输出寄存器(Primitives Output Register + Core Output Register)

    其余不变
    TDPRAM-ILA-6 完全一致
    功能结论:
    输出寄存器功能工作正常

    2.2 实验波形

    TDPRAM-ILA-1

    两 RAM 输出一致。

    TDPRAM-ILA-2

    TDPRAM-ILA-3

    TDPRAM-ILA-4

    TDPRAM-ILA-5

    TDPRAM-ILA-6

    2.3 实验结论

    1. 自编 TDPRAM 和 Vivado TDPRAM IP 在各个条件下的行为完全一致。
    2. 自编 TDPRAM 支持最大时钟可达 300MHz,更高频率没有经过测试
    3. 自编 TDPRAM 支持同步时钟与异步时钟
    4. 自编 TDPRAM 能支持 WF、RF、NC 三种操作模式
    5. 自编 TDPRAM 使能功能正常
    6. 自编 TDPRAM 初始化功能正常,能正常读取 COE 文件,也能够指定初始值
    7. 如果选择一级寄存器,Vivado TDPRAM IP必须需要选择Primitives Output Register,此寄存器对应一级寄存器,不能只选择 Core Output Register 作为一级寄存器,实测发现,只选择 Core Output Register 作为一级寄存器的行为与自编 TDPRAM 选择一级寄存器行为有小概率不同

    三、SDPRAM 功能对比实验设计

    3.1 SDPRAM 的三种操作模式

    当选择 SDPRAM 时,端口 A 固定为写端口,端口 B 固定为读端口。此时端口 A 的操作模式还是有三种可以选择:WF、RF 和 NC,但是,依据写优先的定义,是当某一端口同时进行读和写时,读数据等于写数据;其它操作模式也都是针对同一端口同时读写的,但 SDPRAM 读写是不同端口,所以,从操作模式的定义来说,无论哪种操作模式对 SDPRAM 应该是无意义的。我不理解的是,为什么Vivado的BRAM IP还保留了SDPRAM的A端口的三种操作模式选择,不同操作模式对SDPRAM究竟有什么不同? 下面进行实测分析。

    为了同时比较三种模式的 Vivado SDPRAM IP 输出,将每种模式的 SDPRAM 都实例化,再用 ILA 进行观测,相关代码如下:

    blk_mem_gen_0 blk_mem_gen_0_wf (
      .clka  (clka ), // input wire clka
      .ena   (ena  ),
      .wea   (wea  ), // input wire [0  : 0] wea
      .addra (addra), // input wire [3  : 0] addra
      .dina  (dina ), // input wire [15 : 0] dina
      .clkb  (clkb ), // input wire clkb
      .enb   (enb  ),
      .addrb (addrb), // input wire [3  : 0] addrb
      .doutb (vivado_doutb_wf)// output wire [15 : 0] doutb
    )
    ;

    blk_mem_gen_1 blk_mem_gen_1_rf (
      .clka  (clka ), // input wire clka
      .ena   (ena  ),
      .wea   (wea  ), // input wire [0  : 0] wea
      .addra (addra), // input wire [3  : 0] addra
      .dina  (dina ), // input wire [15 : 0] dina
      .clkb  (clkb ), // input wire clkb
      .enb   (enb  ),
      .addrb (addrb), // input wire [3  : 0] addrb
      .doutb (vivado_doutb_rf)// output wire [15 : 0] doutb
    )
    ;

    blk_mem_gen_2 blk_mem_gen_2_nc (
      .clka  (clka ), // input wire clka
      .ena   (ena  ),
      .wea   (wea  ), // input wire [0  : 0] wea
      .addra (addra), // input wire [3  : 0] addra
      .dina  (dina ), // input wire [15 : 0] dina
      .clkb  (clkb ), // input wire clkb
      .enb   (enb  ),
      .addrb (addrb), // input wire [3  : 0] addrb
      .doutb (vivado_doutb_nc)// output wire [15 : 0] doutb
    )
    ;

    再顶层模块修改一下输出一致与否的判断逻辑:

    (* mark_debug *)reg doutb_is_not_equal_vivado_doutb;
    always @(posedge clkb) begin
      if (doutb != vivado_doutb_wf
          || doutb != vivado_doutb_rf
          || doutb != vivado_doutb_nc
          )

        doutb_is_not_equal_vivado_doutb <
    1'b1;
      else
        doutb_is_not_equal_vivado_doutb <= 1'b0;
    end

    3.2 实验设计

    序号 实测实验条件 波形图 通过 ILA 观测,自编 BRAM 和 Vivado BRAM IP 写入/读取是否一致
    1 1.SDPRAM
    2.同步时钟–300MHz
    3.无输出寄存器
    5.带使能信号 ena 和 enb
    6.使用相同的初始化文件 COE
    SDPRAM-ILA-1 完全一致
    功能结论:
    SDPRAM 同步时钟下
    工作频率可到 300MHz
    使能信号工作正常
    初始化功能正常
    三种操作模式的 Vivado BRAM IP 输出一致
    2 1.SDPRAM
    2.同步时钟–300MHz
    3.输出一级寄存器
    其余不变
    SDPRAM-ILA-2 完全一致
    功能结论:
    SDPRAM 同步时钟下,
    输出一级寄存器工作正常
    3 1.SDPRAM
    2.同步时钟–300MHz
    3.输出二级寄存器
    其余不变
    SDPRAM-ILA-3 完全一致
    功能结论:
    SDPRAM 同步时钟下
    输出二级寄存器工作正常
    4 1.SDPRAM
    2.异步时钟,clka(300MHz),clkb(120MHz)
    3.输出二级寄存器
    其余不变
    SDPRAM-ILA-4 完全一致
    功能结论:
    SDPRAM 异步时钟下
    输出二级寄存器工作正常

    3.3 实验波形

    SDPRAM-ILA-1

    显然,三种模式输出没有区别。

    同步时钟不变,寄存器设为一级,进行实验,SDPRAM-ILA-2

    同步时钟不变,寄存器设为二级,进行实验,SDPRAM-ILA-3

    异步时钟,寄存器仍为二级,进行实验,SDPRAM-ILA-4

    3.4 实验结论

    从各种条件的 ILA 波形看出,三种模式输出没有区别。到这里我们得出结论:Vivado SDPRAM IP的三种操作模式没有意义,每种都一样,所以,自编的 SDPRAM 不设置操作模式选择。

    初始化、使能、寄存器等功能均能正常工作。

    自编 SDPRAM 的行为与 Vivado SDPRAM IP 完全一致。

    这里就有个疑问,如果说 Vivado SDPRAM IP 的三种操作模式之间没有任何区别的话,为什么 Vivado 中会保留模式选择呢,且 PG058 手册中还有 SDPRAM 的不同模式的说明和推荐,这是怎么回事?有懂的同学可以评论区解答一下。

    四、SPRAM 功能对比实验设计

    4.1 实验设计

    序号 实测实验条件 波形图 通过 ILA 观测,自编 BRAM 和 Vivado BRAM IP 写入/读取是否一致
    1 1.SPRAM
    2.时钟–300MHz
    3.A 端口 Write First 模式
    4.无输出寄存器
    5.带使能信号 ena 和 enb
    6.使用相同的初始化文件 COE
    SPRAM-ILA-1 完全一致
    功能结论:
    SPRAM 在 300MHz 时钟下工作正常
    WF 模式工作正常
    使能信号工作正常
    初始化功能正常
    2 1.SPRAM
    2.时钟–300MHz
    3.A 端口 Read First 模式
    4.一级输出寄存器
    其余不变
    SPRAM-ILA-2 完全一致
    功能结论:
    RF 模式工作正常
    SPRAM 一级寄存器功能正常
    3 1.SPRAM
    2.时钟–300MHz
    3.A 端口 No Change 模式
    4.二级输出寄存器
    其余不变
    SPRAM-ILA-3 完全一致
    功能结论:
    NC 模式工作正常
    SPRAM 二级寄存器功能正常

    4.2 实验波形

    SPRAM-ILA-1

    其余波形略。

    4.3 实验结论

    自编 SPRAM 和 Vivado SPRAM IP 行为完全一致。

    五、DPROM 功能对比实验设计

    5.1 实验设计

    序号 实测实验条件 波形图 通过 ILA 观测,自编 BRAM 和 Vivado BRAM IP 写入/读取是否一致
    1 1.DPROM
    2.同步时钟–300MHz
    3.无输出寄存器
    4.带使能信号 ena 和 enb
    5.使用相同的初始化文件 COE
    DPROM-ILA-1 完全一致
    功能结论:
    DPROM 同步时钟下工作正常
    使能信号工作正常
    初始化功能正常
    2 1.DPROM
    2.同步时钟–300MHz
    3.A 端口一级输出寄存器,B 端口二级输出寄存器
    其余不变
    DPROM-ILA-2 完全一致
    功能结论:
    DPROM 同步时钟下工作正常
    输出寄存器功能正常
    3 1.DPROM
    2.异步时钟,clka(300MHz),clkb(120MHz)
    3.A 端口二级输出寄存器,A 端口一级输出寄存器
    其余不变
    DPROM-ILA-3 完全一致
    功能结论:
    DPROM 异步时钟下工作正常

    5.2 实验波形

    DPROM-ILA-1

    DPROM-ILA-2

    DPROM-ILA-3

    5.3 实验结论

    DPROM 功能正常,与 Vivado DPROM IP 行为完全一致。

    六、SPROM 功能对比实验设计

    6.1 实验设计

    序号 实测实验条件 波形图 通过 ILA 观测,自编 BRAM 和 Vivado BRAM IP 写入/读取是否一致
    1 1.SPROM
    2.时钟–300MHz
    3.无输出寄存器
    4.带使能信号 ena
    5.使用相同的初始化文件 COE
    SPROM-ILA-1 完全一致
    功能结论:
    SROM 工作正常
    使能信号正常工作
    初始化功能正常
    2 1.SPROM
    2.时钟–300MHz
    3.一级输出寄存器
    其它不变
    SPROM-ILA-2 完全一致
    功能结论:
    SROM 工作正常
    寄存器功能正常
    3 1.SPROM
    2.时钟–300MHz
    3.二级输出寄存器
    其它不变
    SPROM-ILA-3 完全一致
    功能结论:
    SROM 工作正常
    寄存器功能正常

    6.2 实验波形

    SPROM-ILA-1

    SPROM-ILA-2

    SPROM-ILA-3

    6.3 实验结论

    SPROM 功能正常,与 Vivado SPROM IP 行为完全一致。

    七、总结与分享

    本文对自编 RAM/ROM 与 Vivado BRAM/BROM IP 进行了全面的对比,最终结论是:自编 RAM/ROM 模块的行为和 Vivado 相关 IP 是一样的。

    本模块开源,两处仓库同步。

    Gitee: Verilog 功能模块–RAM 和 ROM https://gitee.com/xuxiaokang/verilog-function-module–RAM_ROM

    Github: zhengzhideakang/verilog-function-module–RAM_ROM https://github.com/zhengzhideakang/verilog-function-module–RAM_ROM

    仿真和实测的 Vivado 2024.2 工程通过网盘分享(本系列文章所有分享均相同,均需获取一次即可):

    欢迎大家关注我的微信公众号:徐晓康的博客,回复以下 6 位数字获取网盘链接。

    402368

    建议复制过去不会码错字!


    如果本文对你有所帮助,欢迎点赞、转发、收藏、评论让更多人看到,赞赏支持就更好了。

    如果对文章内容有疑问,请务必清楚描述问题,留言评论或私信告知我,我看到会回复。


    徐晓康的博客持续分享高质量硬件、FPGA与嵌入式知识,软件,工具等内容,欢迎大家关注。

  • Verilog功能模块–RAM和ROM(02)–同步写-写冲突与读-写冲突实测

    Verilog功能模块–RAM和ROM(02)–同步写-写冲突与读-写冲突实测

    相关文章:

    Xilinx IP 解析之 Block Memory Generator v8.4 ——01-手册重点解读(仅Native RAM) – 徐晓康的博客

    Xilinx IP 解析之 Block Memory Generator v8.4 ——02-如何配置 IP(仅 Native RAM) – 徐晓康的博客

    Verilog功能模块–RAM和ROM(01)–功能说明与关键代码解析 – 徐晓康的博客

    Verilog功能模块–RAM和ROM(02)–同步写-写冲突与读-写冲突实测 – 徐晓康的博客

    Verilog 功能模块–RAM 和 ROM(03)–自编 RAM 与 Vivado RAM IP 功能对比实测 – 徐晓康的博客

    Verilog功能模块--RAM和ROM(01)--功能说明与关键代码解析

    前言

    要理解Vivado RAM IP,并在自编RAM时保证读写行为和Vivado RAM IP完全一致,一个重点就是理解冲突行为。根据pg058的描述,Vivado RAM并没有任何的冲突抑制逻辑,它只是在仿真时会报一个冲突警告,同样自编RAM也没有冲突处理,那么,当冲突发生时,两个RAM的行为是否一致呢?或者说,如果冲突时写入数据或读取数据可能是随机的,那么要做的就不是去保证自编RAM的冲突行为和Vivado RAM一致了,而应该是在调用时避免冲突发生。
    因为仿真有局限性,并不能准确反应真实的硬件行为,这里选择上板实测的方式,通过ILA抓取信号,来研究一下RAM的冲突行为。

    异步时钟没必要测,冲突时数据必然是随机的,其它类型的RAM冲突行为应和TDPRAM相同,所以,本文仅实测了同步时钟下的TDPRAM的冲突行为,包括写-写冲突和读-写冲突。

    一、上板实测实验设计思路

    1.1 需要进行哪些实验?

    主要进行 TDPRAM 的实验测试,它的功能是最复杂的,其它类型 RAM 或 ROM 的底层逻辑和 TDPRAM 基本相同。

    对于 SPRAM、SDPRAM、SPROM、DPROM 仅进行一次或两次实验验证。

    几个需要实验验证的点:

    • 运行尽量高的频率,以验证高频性能
    • 验证 en 信号是否能正常工作
    • 验证 WF、RD、NC 三种操作模式是否能正常工作
    • 验证无输出寄存、一级输出寄存、二级输出寄存是否能正常工作
    • 验证 COE 文件初始化功能是否能正常工作
    • 验证初始化默认值功能(无 COE 或 COE 数据少于存储深度)是否能正常工作

    1.2 需要观察哪些实验结果?

    1.2.1 写-写冲突时写入数据是什么?

    顶层文件设计了两个用于观测的指示信号

    //* 检测写冲突后,没有对地址写,而是去读此地址的情况
    (* mark_debug *)reg write_collision_clka_locked;
    (* mark_debug *)reg [ADDR_WIDTH-1:0] addr_locked;
    (* mark_debug *)reg rd_collision_addr_flag;
    always @(posedge clka) begin
      if (addra == addrb && (true_ena && wea) && (true_enb && web)) begin
        write_collision_clka_locked <
    1'b1;
        addr_locked <= addra;
      end
      if (write_collision_clka_locked) begin
        if (addr_locked == addra)
          if (true_ena && wea)
            write_collision_clka_locked <
    1'b0;
          else if (true_ena && ~wea)
            rd_collision_addr_flag <= 1'b1;
          else
            ;
        else if (addr_locked == addrb)
          if (true_enb && web)
            write_collision_clka_locked <= 1'b0;
          else if (true_enb && ~web)
            rd_collision_addr_flag <= 1'b1;
          else
            ;
      end
      else
        rd_collision_addr_flag <= 1'b0;
      if (rd_collision_addr_flag)
        write_collision_clka_locked <= 1'b0;
    end

    其中,write_collision_clka_locked 为 1 表示写冲突发生了,但没有再一次对相同地址的写入;

    rd_collision_addr_flag 为 1,表示写冲突发生后,相同地址没有被写入,而是先被读取了,这样观察读数据就能知道到底写入了什么数据。

    在 ILA 中设置 rd_collision_addr_flag = 1 为触发条件即可观测写-写冲突的情况。

    注意,这里仅使用了 clka,所以,实验仅做了同步时钟写-写冲突的情况;对于异步时钟的写-写冲突,这里认为没有任何规律,到底写入什么数据是随机的,没有设置信号去判断是否发生了异步的写-写冲突。

    1.2.2 读-写冲突时,读出数据是什么?

    设计了 a_wr_b_rd_collision 与 a_rd_b_wr_collision 信号用于指示读写冲突

    always @(posedge clka) begin
      if (addra == addrb && (true_ena && wea)) a_wr_b_rd_collision <
    1'b1;
      else a_wr_b_rd_collision <= 1'b0;
    end

    always @(posedge clkb) begin
      if (addra == addrb && (true_enb && web)) a_rd_b_wr_collision <
    1'b1;
      else a_rd_b_wr_collision <= 1'b0;
    end

    1.2.3 自编 RAM 读数据是否和 Vivado IP 读数据一直保持一致?

    设计了一下两个信号,用于判断读数据是否一致。

    (* mark_debug *)wire douta_is_not_equal_vivado_douta = douta != vivado_douta;
    (* mark_debug *)wire doutb_is_not_equal_vivado_doutb = doutb != vivado_doutb;

    1.3 如何观测实验波形

    通过 ILA 观测波形图,并保证用高频时钟作为唯一的采样时钟,这样所有波形才能在一个 ILA 窗口,如 300MHz 和 120MHz 都使用时,选择 300MHz 作为采样时钟。

    1.4 如何保证 en、we、din、addr 信号的随机性

    使用线性反馈移位寄存器(Linear Feedback Shift Register,简称 lfsr)作为伪随机数发生器,代码如下,此模块可综合。可通过VIO控制复位和seed,以方便在运行阶段实时改变seed

    module lfsr (
      output reg  [15:0] random_num,
      input  wire [15:0] seed,
      input wire clk,
      input wire rstn // 复位时设置种子
    )
    ;

    // LFSR反馈多项式:x^16 + x^14 + x^13 + x^11 + 1
    wire feedback = random_num[15] ^ random_num[13] ^ random_num[12] ^ random_num[10];

    always @(posedge clk) begin
      if (~rstn)
        if (seed == 0)
          random_num <
    16'hACE1;  // 初始化种子(非零值)
        else
          random_num <= seed;
      else
        random_num <= {random_num[14:0], feedback};
    end

    endmodule

    用随机数的部分位给 en、we、din、addr 赋值,保证输入是随机的。

    二、同步时钟下的写-写冲突实验设计

    pg058对于同步写-写冲突的描述:

    同步写-写冲突:如果两个端口都尝试向内存中的同一位置写入数据,就会发生写-写冲突。此时该内存位置最终的内容是未知的。请注意,写-写冲突会影响内存内容,而与之不同的是,写-读冲突仅会影响数据输出。

    本章通过实测自编RAM和Vivado RAM的同步写-写冲突行为,看实际情况是否和pg058描述相符,并加深对同步写-写冲突的理解。

    序号 实测实验条件 波形图 结论
    1 1.TDPRAM
    2.同步时钟–50MHz
    3.A 端口和 B 端口均 Write First 模式
    4.无输出寄存器
    5.无 en 信号
    6.不使用 COE
    同步写-写冲突波形图-1 同步写-写冲突时:
    自编 RAM 写入数据是 A 端口的
    Vivado RAM 写入数据是端口 B 的
    2 2.同步时钟–100MHz
    其余不变
    同步写-写冲突波形图-2 结论与 1 一样
    3 2.同步时钟–200MHz
    其余不变
    同步写-写冲突波形图-3 同步写-写冲突时:
    自编 RAM 写入数据是 B 端口的
    Vivado RAM 写入数据是端口 A 的
    结论与 1 相反
    4 2.同步时钟–300MHz
    其余不变
    同步写-写冲突波形图-4
    同步写-写冲突波形图-4-1
    结论与 1 相反;
    小概率情形:自编 RAM 在冲突发生时,写入数据随机,既不是 A 输入也不是 B 输入
    5 2.同步时钟–150MHz
    其余不变
    同步写-写冲突波形图-5 结论与 1 相反;
    小概率情形:Vivado RAM 在冲突发生时,写入数据随机,既不是 A 输入也不是 B 输入
    6 2.同步时钟–120MHz
    其余不变
    同步写-写冲突波形图-6 结论与 1 相反;
    小概率情形:Vivado RAM 在冲突发生时,写入数据随机,既不是 A 输入也不是 B 输入
    7 2.同步时钟–110MHz
    其余不变
    同步写-写冲突波形图-7 结论与 1 一样
    8 2.同步时钟–115MHz
    其余不变
    同步写-写冲突波形图-8
    同步写-写冲突波形图-8-1
    某些冲突点两个RAM写入一样,但总是可能在冲突发生时写入错乱值。结论:
    同步写-写冲突没有规律,可能写入A端口的值,也可能写入B端口的值,还可能写入错乱值。
    应在调用TDPRAM的上层模块来控制避免发生写-写冲突

    相关代码:

    always @(posedge clka) begin
      if (write_collision_clka_locked)
        wea <
    1'b0;
      else
        wea <= random_num[0];
      addra <= random_num[ADDR_WIDTH-1 : 0];
      dina  <= random_num[DATA_WIDTH-1 : 0];
    end

    always @(posedge clkb) begin
      if (write_collision_clka_locked)
        web <
    1'b0;
      else
        web <= random_num[8];
      addrb <= random_num[ADDR_WIDTH-1+8 : 8];
      dinb  <= random_num[DATA_WIDTH-1+2 : 2];
    end

    当 write_collision_clka_locked 检测到写-写冲突发生时,we 信号始终为 0,直到读此地址的动作发生,这是为了保证,写-写冲突后,不会发生再一次的同地址写,防止写-写冲突被覆盖。

    同步写-写冲突波形图-1

    箭头 1 处,A 和 B 同时对地址 1 进行写入,dina = 0x161,dinb = 0x058;

    箭头 2 处,B 端口读取地址 1,doutb = 0x161,vivado_doutb = 0x058。

    这说明,这一次的实验,同时写-写冲突发生时,自编 RAM 写入的是 A 端口数据,而 Vivado RAM 写入的是 B 端口数据。

    我们继续看 ILA 抓取到的波形,看下一次发生写-写冲突的情况:

    箭头 3 处,A 和 B 同时对地址 d 进行写入,dina = 0x13d,dinb = 0x34f;

    箭头 4 处,B 端口读取地址 d,doutb = 0x13d,vivado_doutb = 0x34f。

    还是相同的结论:同时写-写冲突发生时,自编 RAM 写入的是 A 端口数据,而 Vivado RAM 写入的是 B 端口数据。

    继续观察下一次写-写冲突的情况,如下图所示,结论还是一样。

    那是不是能说明,写-写冲突有规律呢?自编 RAM 的 A 端口写优先级高于 B,而 Vivado RAM 则反过来呢?其实不是,这只是当前布局布线是这样的情况,什么条件都不变,重新生成 bit 文件试一下。结论还是一样。

    接下来改变同步时钟频率,从 50MHz 改为 100MHz。

    同步写-写冲突波形图-2:结论一样。

    什么条件都不变,重新生成 bit 文件试一下。结论还是一样。

    继续改变同步时钟频率,从 100MHz 改为 200MHz。

    同步写-写冲突波形图-3

    箭头 1 处,A 和 B 同时对地址 1 进行写入,dina = 0x161,dinb = 0x058;

    箭头 2 处,B 端口读取地址 1,doutb = 0x058,vivado_doutb = 0x161。

    好吧,结论反过来了,自编 RAM 的 A 端口优先级低于 B,Vivado RAM 的 A 端口优先级高于 B。

    箭头 3 和 4 处,结论也是一样反过来。什么条件都不变,重新生成 bit 文件试一下。结论还是一样反过来。

    继续改变同步时钟频率,从 200MHz 改为 300MHz。

    同步写-写冲突波形图-4

    这里可能因为频率过高,组合逻辑的 doutb_is_not_equal_vivado_doutb 信号有问题,从 ILA 看两个输出是一样的。重新将 doutb_is_not_equal_vivado_doutb 和 douta_is_not_equal_vivado_douta 都改为时序逻辑:

    (* mark_debug *)reg douta_is_not_equal_vivado_douta;
    (* mark_debug *)reg doutb_is_not_equal_vivado_doutb;
    always @(posedge clka) begin
      douta_is_not_equal_vivado_douta = douta != vivado_douta;
      doutb_is_not_equal_vivado_doutb = doutb != vivado_doutb;
    end

    重新生成 bit,再观察 ILA,同步写-写冲突波形图-4-1

    结论与 3 一样,与 1 相反。什么条件都不变,重新生成 bit 文件试一下。同步写-写冲突波形图-4-2

    前 2 个箭头处,输出与之前一样,注意第四个箭头,doutb = 0x13f,而第三个箭头处 A 端口的 dina = 13d,显然这个 13f 既不是 A 端口写入数据,也不是 B 端口写入数据 0x34f,显然数据错乱了。

    这个数据错乱只发生在了写-写冲突的位置,其它位置数据是正常的,说明,写-写冲突的实际写入是不确定的,可能既不是 dina 也不是 dinb,这和 Xilinx pg058 BRAM 手册上关于同步时钟下的写-写冲突的描述是一样的,即实际写入数据随机。

    再重新生成 bit 文件试一下,同步写-写冲突波形图-4-3:又回到了 同步写-写冲突波形图-4-1 的状态。

    结论:同步写-写冲突发生时,写入有小概率是随机的。

    基于之前的结论,100MHz 的结论与 200MHz 相反,那么 100~200MHz,是否有个区间,自编 RAM 的在写入冲突时的写入值与 Vivado RAM 相同呢?基于这个思路,改变时钟频率为 150MHz,同步写-写冲突波形图-5

    注意,第二个箭头处,Vivado_doutb 的输出为 0x169,它既不等于写-写冲突时,A 端口输入 0x161 也不等于 B 端口输入 0x058,可见,之前发现的现象写-写冲突时,自编 RAM 写入错乱并不是自编 RAM 才有,Vivado RAM 也是有的。这里几乎能得到结论,同步写-写冲突,写入数据是随机的,并不一定等于A或B的输入

    什么条件都不变,重新生成 bit 文件试一下。结论:Vivado_doutb 还是出现了输出为 0x169 的现象。

    继续改变时钟频率为 120MHz,同步写-写冲突波形图-6

    注意第二个箭头,Vivado_doutb 的输出为 0x149,发生了错乱。结论与 150MHz 时相同。重新生成 bit 文件,结论还是一样。

    继续改变时钟频率为 110MHz,同步写-写冲突波形图-7

    结论与100MHz时相同,与120MHz时相反。这时就考虑是不是110~120MHz的某个频率,自编RAM和Vivado RAM的写-写冲突行为会一致呢?虽然说这个一致与否并不是很有意义,因为从理论上来说写-写冲突的写入数据是随机的,一致也可能只是这一次布局布线凑巧而已,但是可以尝试一下,找到一致的频率点。

    继续更改时钟频率为为115MHz,同步写-写冲突波形图-8

    从波形图上看好像两个RAM的写-写冲突写入值是一样的,但也是某些时刻一样,在其它时刻又会不一样,同步写-写冲突波形图-8-1

    显然,第二个箭头处,Vivado_doutb的值为0x03e,这个也是随机的错乱值,所以说,想找到一个频率点,让两个RAM对写-写冲突的写入是一样的,是不可能的,某几次冲突可能一样,但当写-写冲突导致写入随机值时,就会不一样了。

    到这里,我们可以得到结论,无论是自编TDPRAM,还是Vivado的TDPRAM IP,只要发生写-写冲突,就可能写入错乱值,也就是说不需要去设定某些逻辑去限制当写-写冲突发生时,应该以A端口为准还是B端口为准,而应该在调用TDPRAM的上层逻辑去避免发生写-写冲突,因为写-写冲突可能造成该地址的存储值错乱。

    三、同步时钟下的读-写冲突实验设计

    PG058对于同步读写冲突的描述:

    同步写-读冲突:如果一个端口尝试向某一内存位置写入数据,而另一个端口读取该相同位置的数据,就可能会发生同步写-读冲突。在写-读冲突中,虽然内存内容不会被破坏,但输出数据的有效性取决于写端口的操作模式。

    • 如果写端口处于读优先(READ_FIRST)模式,另一个端口能够可靠地读取旧的内存内容。

    • 如果写端口处于写优先(WRITE_FIRST)模式或无变化(NO_CHANGE)模式,读端口输出的数据是无效的。

    本章通过实测自编RAM和Vivado RAM的同步读-写冲突行为,看实际情况是否和pg058描述相符,并加深对同步读-写冲突的理解。

    本章只研究同步读-写冲突,需要避免同步写-写冲突干扰观察,怎么避免呢?代码如下:

    always @(posedge clka) begin
      wea   <= random_num[13];
      addra <= random_num[ADDR_WIDTH-1+8 : 8];
      dina  <= random_num[DATA_WIDTH-1+2 : 2];
    end

    reg web_tmp;
    always @(posedge clkb) begin
      web_tmp <= random_num[5];
      addrb   <= random_num[ADDR_WIDTH-1 : 0];
      dinb    <= random_num[DATA_WIDTH-1 : 0];
    end

    always @(*) begin
      if (addra == addrb && wea)
        web 
    1'b0;
      else
        web = web_tmp;
    end

    A端口信号保持随机,B端口的写使能web在检测到A端口正在写时赋值0,避免写-写冲突发生。

    这里必须使用组合逻辑进行判断,否则判断延迟一个时钟周期,写-写冲突还是可能发生。

    同步读-写冲突实验设计:

    序号 实测实验条件 波形图 结论
    1 1.TDPRAM
    2.同步时钟–50MHz
    3.A 端口和 B 端口均 Write First 模式
    4.无输出寄存器
    5.无 en 信号
    6.不使用 COE
    同步读-写冲突波形图-1 同步读-写冲突时:
    Vivado RAM的A端口读数据读到了B端口的最新输入,而自编RAM没有;
    但自编RAM的B端口读到了A端口的最新输入,而Vivado RAM没有
    2 2.同步时钟–100MHz
    其余不变
    同步读-写冲突波形图-2
    同步读-写冲突波形图-2-1
    同步读-写冲突时:
    小概率情形:
    Vivado RAM可能读数据错乱
    自编RAM也可能读数据错乱
    3 2.同步时钟–100MHz
    3.A端口 Read First模式,B端口No Change模式
    其余不变
    同步读-写冲突波形图-3
    同步读-写冲突波形图-3-1
    B端口读正常,
    A端口读可能出错,
    与PG058对读-写冲突的描述吻合
    4 2.同步时钟–100MHz
    3.A端口 Read First模式,B端口Read First模式
    其余不变
    同步读-写冲突波形图-4 A,B端口读均正常
    与PG058对读-写冲突的描述吻合

    同步读-写冲突波形图-1

    箭头1:A读B写,地址c,douta=0x338,vivado_douta=0x03c=dinb,说明,Vivado RAM的A端口读数据读到了B端口的最新输入,而自编RAM没有;

    箭头2:A读B写,地址d,douta=0x245,vivado_douta=0x13d=dinb,说明,与箭头1相同;

    箭头3:A写B读,地址a,doutb=0x29e=dina,vivado_doutb=0x03a,说明,自编RAM的B端口读到了A端口的最新输入,而Vivado RAM没有,与箭头1相反;

    箭头4,A写B读,地址4,doutb=0x13d=dina,vivado_doutb=0x074,说明,与箭头3相反

    箭头5,A写B读,地址9,doutb=0x27a=dina,vivado_doutb=0x0e9,说明,与箭头3相反

    结论,Vivado RAM的A端口读数据读到了B端口的最新输入,而自编RAM没有;但自编RAM的B端口读到了A端口的最新输入,而Vivado RAM没有。

    这应该就是pg058的Vivado RAM IP手册中对同步读-写冲突的描述:如果写端口处于写优先(WRITE_FIRST)模式或无变化(NO_CHANGE)模式,读端口输出的数据是无效的

    最终结论可能是,同步读-写冲突发生时,读数据是无效的,可能是存储器旧数据,也可能是新写入数据,或者是随机数据?这个还需要再观察。

    改变时钟频率为100MHz,ILA波形图,同步读-写冲突波形图-2

    箭头1,A读B写,地址d,douta=0x205,vivado_douta=0x13d=dinb,说明,Vivado RAM的A端口能读数据能读出B端口最新输入,而自编RAM不行;

    箭头2,A写B读,地址a,doutb=0x29e=vivado_doutb=dina,说明Vivado RAM和自编RAM的B端口都能读出A端口的最新输入;

    箭头3,A写B读,地址4,doutb=0x13d=dina,vivado_doutb=0x034,说明自编RAM的B端口能读出A端口的最新输入,而Vivado RAM不行;

    箭头4,结论和箭头3相同。

    再继续观察ILA,同步读-写冲突波形图-2-1

    箭头1:端口A写入地址2,数据0x0af;

    箭头2,端口B写入地址1,数据0x1e1;

    箭头3,A写B读,地址1,doutb=0x054=dina,而vivado_doutb=0x044,这既不等于dina也不等于旧存储器值0x1e1,这就是所谓的读取数据错乱,说明,vivado RAM的B端口在读-写冲突时发生了读数据错乱;

    箭头4,A读B写,地址2,vivado_dina=0x2a2=dinb,而dina=0x0a2,这既不等于dinb也不等于旧存储器值0x0af,说明,自编RAM的A端口在读-写冲突时发生了读数据错乱。

    根据PG058对于同步读-写冲突的描述,当写端口设置为Read First时,读端口应能稳定读取到存储器旧数据;当写端口设置为No Change时,读端口读数据无效。为此将A端口模式改为Read First,B端口改为No Change,观察ILA,同步读-写冲突波形图-3

    这是一个整理视图,可以看到douta_is_not_equal_vivado_douta有置高的,说明A端口读数据时,自编RAM和Vivado RAM有不同的;而doutb_is_not_equal_vivado_doutb没有置高,说明B端口读数据时,自编RAM和Vivado RAM始终相同,上述将A端口设置为了Read First,这个设置保证了B端口读数据始终能读到存储器旧数据,实测证明了PG058的这一说法。

    继续看其他读-写冲突时刻,同步读-写冲突波形图-3-1

    第一个箭头,B端口写入地址7,dinb=0x177;

    第二个箭头,B写A读,B端口因为是No Change所以读数据不变,没问题;A端口读数据,doutb=0x377,vivado_doutb=0x177,自编RAM读出的是B正在写入的数据,而Vivado RAM读出的是旧存储器数据,这里就不一样了。这说明了,B端口设置为No_Change时功能正常,当读-写冲突发生时,写端口为No Change时,读端口数据是无效的,进一步证明了PG058描述是符合实际的。

    那么继续进行实现,将A和B端口都设置为Read First,那么根据PG058描述,即使冲突发生,RAM读数据应该是稳定的旧存储器数据,那么两个RAM应该不会出现读数据不一样的情况,观察ILA,同步读-写冲突波形图-4

    可以看到,再改了Read First之后,两个RAM的读取数据始终一致了。可见,PG058关于同步读-写冲突的描述是准确的,我们实测也是一样的结果,还说明了自编RAM的同步读-写冲突行为和Vivado RAM也是一致的。

    到这里我们得到结论

    • 如果写端口处于读优先(READ_FIRST)模式,另一个端口能够可靠地读取旧的内存内容。

    • 如果写端口处于写优先(WRITE_FIRST)模式或无变化(NO_CHANGE)模式,读端口输出的数据是无效的。

    • 自编RAM和Vivado RAM对于同步读-写冲突的行为完全一致。

    四、总结与分享

    通过对同步时钟下的自编TDPRAM和Vivado TDPRAM IP的上板实测观察,我们可以得到以下结论:

    1. 同步写-写冲突发生时,两个TDPRAM写入值都可能是错乱的,它可能是A端口输入,也可能是B端口输入,也可能是随机值;

    2. 同步读-写冲突发生时,写入没有问题,但读数据可能是错乱的,读端口数据可能等于写端口正在写入的数据,可能等于旧存储器数据,也可能是随机值。

    所以,在使用TDPRAM时,应在调用模块保证不会发生同步冲突,同步写-写冲突可能破坏存储器数据,应避免;读-写冲突可能导致读数据出错,也应该避免。

    这里没有去实测异步时钟,显然,无论是根据Vivado RAM IP的手册pg058,还是自编RAM的代码逻辑,异步时钟下,也一样会发生写-写或读-写冲突,也需要在调用时避免冲突的发生,但异步时钟的冲突控制会更困难一些。

    本模块开源,两处仓库同步。

    Gitee: Verilog 功能模块–RAM 和 ROM https://gitee.com/xuxiaokang/verilog-function-module–RAM_ROM

    Github: zhengzhideakang/verilog-function-module–RAM_ROM https://github.com/zhengzhideakang/verilog-function-module–RAM_ROM

    仿真和实测的Vivado 2024.2工程通过网盘分享(本系列文章所有分享均相同,均需获取一次即可):

    欢迎大家关注我的微信公众号:徐晓康的博客,回复以下6位数字获取网盘链接。

    402368

    建议复制过去不会码错字!


    如果本文对你有所帮助,欢迎点赞、转发、收藏、评论让更多人看到,赞赏支持就更好了。

    如果对文章内容有疑问,请务必清楚描述问题,留言评论或私信告知我,我看到会回复。


    徐晓康的博客持续分享高质量硬件、FPGA与嵌入式知识,软件,工具等内容,欢迎大家关注。

  • Verilog功能模块–RAM和ROM(01)–功能说明与关键代码解析

    Verilog功能模块–RAM和ROM(01)–功能说明与关键代码解析

    相关文章:

    Xilinx IP 解析之 Block Memory Generator v8.4 ——01-手册重点解读(仅Native RAM) – 徐晓康的博客

    Xilinx IP 解析之 Block Memory Generator v8.4 ——02-如何配置 IP(仅 Native RAM) – 徐晓康的博客

    Verilog功能模块–RAM和ROM(01)–功能说明与关键代码解析 – 徐晓康的博客

    Verilog功能模块–RAM和ROM(02)–同步写-写冲突与读-写冲突实测 – 徐晓康的博客

    Verilog 功能模块–RAM 和 ROM(03)–自编 RAM 与 Vivado RAM IP 功能对比实测 – 徐晓康的博客

    Verilog功能模块--RAM和ROM(01)--功能说明与关键代码解析

    前言

    RAM和ROM是FPGA中用于数据交互的基础组件,应用非常广泛。

    所有的FPGA厂家在其开发工具中都提供可直接使用的RAM/ROM IP,这些IP大都经过详细测试,功能完善,可直接使用,那为什么还需要自编RAM/ROM呢?主要理由是:

    1. 自编RAM/ROM能显著增强代码可移植性,例如,我的某些自编IP中需要RAM/ROM,此时如果使用某一厂家RAM/ROM IP,那么换另外厂家的芯片,自编IP就不能用了;或者要为每个厂家的芯片都重写一下自编IP,并且还要在工程中添加RAM/ROM IP,这就使得自编IP的适用性变得很差,难以接受;
    2. 在很多应用场景中,并不需要追求大容量的RAM/ROM,也不要求RAM/ROM的高性能,而只是需要RAM/ROM能实现基本的功能就行了,这时使用自编RAM/ROM比用厂家IP就要方便很多了。也不用担心厂家IP升级收费之类的变化。

    当然,如果需要大容量的RAM/ROM,或者高性能的RAM/ROM,还是建议用厂家的RAM/ROM IP,毕竟这些IP都经过严格的测试,也能更好的适应特定的芯片,在可靠性和性能上都会优于自编IP。

    本文主要介绍自编RAM/ROM IP的功能与信号接口,并对一些关键功能的实现代码进行解析。

    一、自编RAM/ROM IP的功能

    1. 自编RAM/ROM IP的接口信号和处理逻辑都参考Vivado的BRAM IP。
    2. 实现了三种RAM类型:1.单端口RAM(SPRAM);2.简单双端口RAM(SDPRAM);3.真双端口RAM(TDPRAM)。
    3. 实现了两种ROM类型:1.单端口ROM(SPROM);2.双端口ROM(DPROM)。
    4. RAM需要实现三种操作模式:1.写优先(Write First);2.读优先(Read First);3.无变化(No Change)。
    5. 实现了端口使能引脚en,功能等价于Vivado BRAM IP中的ena和enb。
    6. 实现参数控制的单个/两个输出寄存器,功能等价于Vivado BRAM IP中的Primitives Output Register和Core Output Register。
    7. 实现了RAM/ROM使用COE文件进行初始化的功能,功能等价于Vivado BRAM IP中的Memory Initialization功能。
    8. 实现了指定RAM/ROM初始默认值的功能,当未指定COE文件或COE文件数据数小于存储器深度时,此默认值起作用,功能等价于Vivado BRAM IP中的Fill Remaining Memory Locations。
    9. 暂未实现字节写功能,即Vivado BRAM IP中的Byte Write Enable选项,此功能实现有些复杂,暂不实现。
    10. 暂未实现读/写数据位宽转换功能,如果需要此功能,可外部添加我开源的带位宽转换功能的FIFO模块。
    11. 暂未实现两端口数据位宽转换功能,如果需要此功能,可外部添加我开源的带位宽转换功能的FIFO模块。
    12. 暂未实现复位功能,一般应用通常不需要复位,也难以去对应Vivado BRAM IP中的复位功能,此IP复位功能还需要增加regcn引脚,rst_busy引脚,还有复位优先级区分,以及是否复位存储器值的选项,有些太繁琐了。

    总结:实现了Vivado BRAM IP的绝大部分功能,且保证了信号名称完全一致,在大部分应用场景可替换Vivado BRAM IP以增强代码适用性。

    二、模块框图与信号说明

    TDPRAM模块框图如下,其余模块SDPRAM、SPRAM、DPROM、SPROM略。

    参数说明:

    参数名 描述
    RAM_STYLE RAM类型, 可选”block”(默认), “distributed”
    DATA_WIDTH 数据位宽, 可选1, 2, 3, …, 默认为8
    ADDR_WIDTH RAM地址位宽, 对应RAM深度, 可选1, 2, 3, …, 默认为6, 对应深度2**6=64
    OPERATING_MODE_A 可选”WF”(默认), “RF”, “NC”
    WF表示Write First,RF表示Read First,NC表示No Change
    OPERATING_MODE_B 可选”WF”(默认), “RF”, “NC”
    WF表示Write First,RF表示Read First,NC表示No Change
    USE_ENA 是否启用ENA信号,0(默认)表示不启用,1表示启用
    USE_ENB 是否启用ENB信号,0(默认)表示不启用,1表示启用
    OUTPUT_REG_NUM_A A端口输出寄存器数量,可选0(默认), 1, 2
    0表示输出不添加寄存器,直接输出
    1表示输出添加一个寄存器,输出延时一个时钟周期
    2表示输出添加2个寄存器,输出延时两个时钟周期
    OUTPUT_REG_NUM_B B端口输出寄存器数量,与OUTPUT_REG_NUM_A一样
    INIT_FILE 初始化文件路径,,空“”(默认)表示不初始化,示例目录C:/ram_init.coe,注意斜杆为除法符号
    INIT_VALUE_HEX 默认初始值, 在未指定初始化文件或初始化文件行数比RAM深度小时起作用, 使用16进制表示, 默认值0,

    三、使用Verilog对RAM进行初始化

    参考:ug901-vivado-synthesis-en-us-2024.2.pdf–Chapter 5: HDL Coding Techniques:

    • RAM HDL Coding Techniques(随机存取存储器(RAM)的硬件描述语言(HDL)编码技术)
    • Inferring UltraRAM in Vivado Synthesis(在Vivado综合工具中推断UltraRAM(超高速随机存取存储器))
    • RAM HDL Coding Guidelines(RAM的HDL编码指南)
    • Initializing RAM Contents(初始化RAM内容)
    • 3D RAM Inference(3D RAM推断)
    • Black Boxes(黑盒)
    • FSM Components(有限状态机组件)
    • ROM HDL Coding Techniques(只读存储器(ROM)的HDL编码技术)

    3.1 在HDL源代码中指定RAM初始内容

    RAM所有存储值都初始化为相同的值,示例代码如下:

    reg [DATA_WIDTH-1:0] ram [DEPTH-1:0];
    integer i;
    initial begin 
      for (i=0; i<DEPTH; i=i+1) ram[i] = 0;
    end

    RAM所有存储值都初始化为特定的值,示例代码如下:

    reg [19:0] ram [15:0];
    initial begin
      ram[15] = 20'h00102; ram[14] = 20'h02137; ram[13] = 20'h02036;
      ram[12] = 20'h00301; ram[11] = 20'h00102; ram[10] = 20'h02237;
      ram[9] = 20'h04004; ram[8] = 20'h00304; ram[7] = 20'h04040;
      ram[6] = 20'h02500; ram[5] = 20'h02500; ram[4] = 20'h02500;
      ram[3] = 20'h0030D; ram[2] = 20'h02341; ram[1] = 20'h08201;
      ram[0] = 20'h0400D;
    end

    3.2 在外部数据文件中指定RAM初始内容

    在硬件描述语言(HDL)源代码里,可借助文件读取功能,从外部数据文件加载随机存取存储器(RAM)的初始内容。

    • 外部数据文件要求:它是一个ASCII文本文件,文件名可任意设定。
    • 数据组织方式:该文件中的每一行都对应着随机存取存储器(RAM)中一个地址位置的初始内容。
    • 行数匹配规则:外部数据文件的行数必须和随机存取存储器(RAM)阵列的行数相同。若行数不足,系统会发出标记提示。
    • 地址映射机制:文件中每一行数据对应的随机存取存储器(RAM)地址位置,由对该随机存取存储器(RAM)进行建模的信号主范围方向来确定。
    • 数据格式规范:随机存取存储器(RAM)的内容可以用二进制或者十六进制来表示,但这两种格式不能在同一文件中混用。
    • 内容纯净性要求:外部数据文件中不能包含任何其他内容,像注释之类的都不允许存在。

    RAM初始化Verilog代码示例:

    reg [31:0] ram [0:63];
    initial begin
        // 地址范围0~63,对应文件的0~63行
        $readmemb("rams_20c.data", ram, 063);
    end

    或者

    reg [31:0] ram [0:63];
    initial begin
        // 不指定地址范围,则顺序读取每一行,直到出现空行或者RAM满
        $readmemb("rams_init_file.data", ram);
    end

    注意:用于初始化随机存取存储器(RAM)的外部文件必须采用位向量(bit vector)格式。整数格式或者十六进制格式的外部文件无法正常使用。

    rams_init_file.data示例(10bit X 8行):

    0000000000
    0010101010
    0101010101
    0111111111
    1000100010
    1010101010
    1100110011
    1110100000

    不允许添加任何注释和标点符号,经过测试,上述数字可以插入下划线,如11_1111_1111,但也建议不要加,因为Vivado BRAM IP的COE文件不允许数字间添加下划线,保持一致。

    Vivado BRAM IP支持的COE文件格式示例如下:

    memory_initialization_radix = 2;
    memory_initialization_vector =
    0000000000,
    0010101010,
    0101010101,
    0111111111,
    1000100010,
    1010101010,
    1100110011,
    1110100000;

    memory_initialization_radix可以等于2/10/16,分别对应2/10/16进制,memory_initialization_vector需满足radix指定的进制。

    3.3 自编RAM/ROM IP中的初始化代码

    与初始化相关的参数:

      //* 初始化文件名,空(默认)表示不初始化,示例目录C:/_myJGY/ram_init.coe
      parameter INIT_FILE = ""
      /*
      * 默认初始值, 在未指定初始化文件或初始化文件行数比RAM深度小时起作用, 使用16进制表示, 默认值0,
      * 对应Vivado BRAM IP的功能Fill Remaining Memory Locations
      */

      parameter [DATA_WIDTH-1:0] INIT_VALUE_HEX = 'h0

    初始化功能代码:

    //++ 初始化RAM ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    integer i;
    generate
    if (INIT_FILE != "") begin
      initial begin
        for (i = 0; i < DEPTH; i = i + 1) mem[i] 
    = INIT_VALUE_HEX; //* 默认初始化值
        /*
        * 使用 $readmemb 从 .coe 文件加载初始值,
        * ug901中说明只有二进制格式的初始化文件才是可用Verilog代码进行初始化的
        */

        $readmemb(INIT_FILE, mem);
      end
    end else begin
      initial begin : ram_init_all_0 //* 将每个存储单元初始化为设定的初始值
        for (i = 0; i < DEPTH; i = i + 1) mem[i] = INIT_VALUE_HEX;
      end
    end
    endgenerate
    //-- 初始化RAM ------------------------------------------------------------

    四、三种操作模式–写优先、读优先与无变化模式的实现

    这三种模式影响RAM的输出值,相关参数:

    //* 可选"Write First"(默认), "Read First", "No Change"
    parameter OPERATING_MODE_A = "WF"
    parameter OPERATING_MODE_B = "WF"

    实现代码:

    reg [DATA_WIDTH-1:0] douta_tmp = 0//* 仿真默认值, 对综合无效

    generate
    if (OPERATING_MODE_A == "WF") begin
      always @(posedge clka) begin
        if (true_ena)
          if (wea)
            douta_tmp <
    = dina;
          else
            douta_tmp <= mem[addra];
      end
    end else if (OPERATING_MODE_A == "RF") begin
      always @(posedge clka) begin
        if (true_ena)
          douta_tmp <
    = mem[addra];
      end
    end else begin
      always @(posedge clka) begin
        if (true_ena)
          if (~wea)
            douta_tmp <= mem[addra];
      end
    end
    endgenerate

    代码解析:

    1. WF,写优先,写正在进行时,读端口输出写入值
    2. RF,读优先,读端口始终输出存储器值
    3. NC,无变化,写正在进行时,读端口输出不变

    五、输出一/二级寄存器的实现

    本自编模块的输出寄存器对RAM/ROM的性能没有影响,增加此功能只是为了与Vivado BRAM IP在增加寄存器时的输出时序保持一致。

    输出寄存器相关参数:

    parameter [1:0] OUTPUT_REG_NUM = 0//* 可选0(默认), 1, 2

    相关代码:

    generate
    if (OUTPUT_REG_NUM == 0) begin
      assign douta 
    = douta_tmp;
    end else if (OUTPUT_REG_NUM == 1) begin
      reg [DATA_WIDTH-1:0] douta_tmp_r1 
    'd0;
      always @(posedge clka) begin
        douta_tmp_r1 <= douta_tmp;
      end
      assign douta = douta_tmp_r1;
    end else begin
      reg [DATA_WIDTH-1:0] douta_tmp_r1 = '
    d0;
      reg [DATA_WIDTH-1:0] douta_tmp_r2 = 'd0;
      always @(posedge clka) begin
        douta_tmp_r1 <= douta_tmp;
        douta_tmp_r2 <= douta_tmp_r1;
      end
      assign douta = douta_tmp_r2;
    end
    endgenerate

    六、总结与分享

    本文对自编RAM/ROM IP的功能、信号接口,关键代码进行了说明,关于模块的仿真验证和上板实测请参考下一篇文章。

    本模块开源,两处仓库同步。

    Gitee: Verilog 功能模块–RAM 和 ROM https://gitee.com/xuxiaokang/verilog-function-module–RAM_ROM

    Github: zhengzhideakang/verilog-function-module–RAM_ROM https://github.com/zhengzhideakang/verilog-function-module–RAM_ROM

    仿真和实测的Vivado 2024.2工程通过网盘分享(本系列文章所有分享均相同,均需获取一次即可):

    欢迎大家关注我的微信公众号:徐晓康的博客,回复以下6位数字获取网盘链接。

    402368

    建议复制过去不会码错字!


    如果本文对你有所帮助,欢迎点赞、转发、收藏、评论让更多人看到,赞赏支持就更好了。

    如果对文章内容有疑问,请务必清楚描述问题,留言评论或私信告知我,我看到会回复。


    徐晓康的博客持续分享高质量硬件、FPGA与嵌入式知识,软件,工具等内容,欢迎大家关注。