Dual port ram with different data widths - Page 1
Forum section: electronics / general

author: ali_asadzadeh
author_rank: Super Contributor
timestamp: May 04, 2021, 07:07:04 am
content: Hi,I want to have a dual port ram with different data port widths, how can I impalement that, I want to model it in Modelsim, here is a simple code that I'm using for a dual port ram with the same data widths on both port A and port BCode:module DualPortRam #(//=============// Parameters//=============parameter RAM_DATA_WIDTH = 16, // width of the dataparameter RAM_ADDR_WIDTH = 4 // number of address bits) (//================// General Ports//================input wire rst,//=========// Port A//=========input wire a_clk,input wire a_wr, // pulse a 1 to write and 0 readsinput wire [RAM_ADDR_WIDTH-1:0] a_addr,input wire [RAM_DATA_WIDTH-1:0] a_data_in,output reg [RAM_DATA_WIDTH-1:0] a_data_out,//=========// Port B//=========input wire b_clk,input wire b_wr, // pulse a 1 to write and 0 readsinput wire [RAM_ADDR_WIDTH-1:0] b_addr,input wire [RAM_DATA_WIDTH-1:0] b_data_in,output reg [RAM_DATA_WIDTH-1:0] b_data_out);//===============// Local Params//===============localparam RAM_DATA_DEPTH = 2**RAM_ADDR_WIDTH; // depth of the ram, this is tied to the number of address bits//================// Shared memory//================reg [RAM_DATA_WIDTH-1:0] mem [RAM_DATA_DEPTH-1:0];//=========// Port A//=========always @(posedge a_clk) beginif (`ifdef ACTIVE_LOW_RST !rst `else rst `endif)a_data_out <= {RAM_DATA_WIDTH{1'b0}};else begina_data_out <= mem;if (a_wr) beginmem <= a_data_in;endendend//=========// Port B//=========always @(posedge b_clk) beginif (`ifdef ACTIVE_LOW_RST !rst `else rst `endif)b_data_out <= {RAM_DATA_WIDTH{1'b0}};else beginb_data_out <= mem;if (b_wr) beginmem <= b_data_in;endendendinitial beginmem = 16'hCD11;mem = 16'h6C28;mem = 16'h050B;mem = 16'h962E;mem = 16'h21D8;mem = 16'hAE7C;mem = 16'h409C;mem = 16'hDE95;mem = 16'h841B;endendmoduleI want to have a 8bit width on port A and a 16bit data width on port B

author: Daixiwen
author_rank: Frequent Contributor
timestamp: May 04, 2021, 07:46:15 am
content: Which FPGA are you targetting? Even if you want to write generic code, it can be a good idea to see what the FPGA synthesizer expects, to be sure that your code can be recognized correctly and will generate hard RAM blocks.If you are targetting Altera/Intel, there is a guide here:https://www.intel.com/content/dam/www/programmable/us/en/pdfs/literature/hb/qts/qts_qii51007.pdfPages 13-28 and 13-29 talk about implementing mixed width dual port RAMs. Apparently this is not possible in Verilog, because of the lack of support for multi-dimensional arrays, but there is a way in SystemVerilog.The whole "inferring memory from HDL code" chapter is interesting and gives lots of different examples.

author: ali_asadzadeh
author_rank: Super Contributor
timestamp: May 04, 2021, 07:57:25 am
content: my Target FPGA is Gowin, But for now and purely for simulation I want to write a generic code, how can I write it with SystemVerilog?

author: ali_asadzadeh
author_rank: Super Contributor
timestamp: May 04, 2021, 08:05:32 am
content: I have written some code for now, and it's not working as expected, for some reasons, the modelsim would not detect posedge clocks after a few clock cycles! do you have any Idea?Dual port ram with 8bit data on port A and 16bit on port BCode:module myDualPortRam #(//=============// Parameters portA has always 8bit data and port B has 16 bit data//=============parameter RAM_DATA_WIDTHA = 8, // width of the data Aparameter RAM_ADDR_WIDTHA = 5, // number of address bits Aparameter RAM_DATA_WIDTHB = 16, // width of the data Aparameter RAM_ADDR_WIDTHB = 4 // number of address bits A) (//================// General Ports//================input wire rst,//=========// Port A//=========input wire a_clk,input wire a_wr, // pulse a 1 to write and 0 readsinput wire [RAM_ADDR_WIDTHA-1:0] a_addr,input wire [RAM_DATA_WIDTHA-1:0] a_data_in,output reg [RAM_DATA_WIDTHA-1:0] a_data_out,//=========// Port B//=========input wire b_clk,input wire b_wr, // pulse a 1 to write and 0 readsinput wire [RAM_ADDR_WIDTHB-1:0] b_addr,input wire [RAM_DATA_WIDTHB-1:0] b_data_in,output reg [RAM_DATA_WIDTHB-1:0] b_data_out);//===============// Local Params//===============localparam RAM_DATA_DEPTH = 2**RAM_ADDR_WIDTHA; // depth of the ram, this is tied to the number of address bits//================// Shared memory//================reg [RAM_DATA_WIDTHA-1:0] mem [RAM_DATA_DEPTH-1:0];//=========// Port A//=========always @(posedge a_clk) beginif (`ifdef ACTIVE_LOW_RST !rst `else rst `endif)a_data_out <= {RAM_DATA_WIDTHA{1'b0}};else begina_data_out <= mem;if (a_wr) beginmem <= a_data_in;endendend//=========// Port B//=========wire wAddrBL = {b_addr,1'b0};wire wAddrBH = {b_addr,1'b1};always @(posedge b_clk) beginif (`ifdef ACTIVE_LOW_RST !rst `else rst `endif)b_data_out <= {RAM_DATA_WIDTHB{1'b0}};else beginb_data_out <= {mem,mem};if (b_wr) beginmem <= b_data_in[7:0];mem <= b_data_in[15:8];endendendinitial beginmem = 8'h11;mem = 8'h28;mem = 8'h0B;mem = 8'h2E;mem = 8'hD8;mem = 8'h7C;mem = 8'h9C;mem = 8'h95;mem = 8'h1B;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;mem = 8'h00;endendmoduleThe simple test bench for reading from port BCode:`timescale 1ns/1nsmodule myDualPortRam_tb ();parameter MAIN_CLK_DELAY = 5; // 100 MHzreg r_Rst_L = 1'b0;reg r_Clk = 1'b0;// Clock Generators:always #(MAIN_CLK_DELAY) r_Clk = ~r_Clk;reg [4:0] r_a_addr;reg [7:0] r_a_data_in;wire [7:0] w_a_data_out;reg [3:0] r_b_addr;reg [15:0] r_b_data_in;wire [15:0] w_b_data_out;myDualPortRam dut(.rst(1'b0),.a_clk(r_Clk),.a_wr(1'b0),.a_addr(r_a_addr),.a_data_in(r_a_data_in),.a_data_out(w_a_data_out),.b_clk(r_Clk),.b_wr(1'b0),.b_addr(r_b_addr),.b_data_in(r_b_data_in),.b_data_out(w_b_data_out));initialbeginr_Rst_L = 1'b1;repeat(2) @(posedge r_Clk);r_Rst_L = 1'b0;repeat(1) @(posedge r_Clk);//read from port Br_b_addr = 0;repeat(1) @(posedge r_Clk);r_b_addr = 1;repeat(1) @(posedge r_Clk);r_b_addr = 2;repeat(1) @(posedge r_Clk);r_b_addr = 3;repeat(1) @(posedge r_Clk);r_b_addr = 4;repeat(1) @(posedge r_Clk);r_b_addr = 5;repeat(1) @(posedge r_Clk);r_b_addr = 6;repeat(1) @(posedge r_Clk);r_b_addr = 7;repeat(1) @(posedge r_Clk);end // initial beginendmoduleThe modelsim output, only showing 0x2811 as the output! I have stepped in the code and after chaining the r_b_addr = 0; in testbench, the dual port ram module would not detect any posedge clocks!

author: asmi
author_rank: Super Contributor
timestamp: May 04, 2021, 01:52:10 pm
content: There are so many things wrong with your code, I don't know where to start1. You allow your module to be instantiated with configurable widths yet the code clearly assumes that width of the port A is always half that of the port B. Think about what happens if you instantiate your module with RAM_DATA_WIDTHA of 16 and RAM_DATA_WIDTHB of 82. You use the same reset signal for both ports, which implies that it has to be synchronous to both clocks simultaneously, which is only possible if clocks are the same, or one is a phase-aligned multiple of the other.3. You use a single-bit addresses for your second port, which is why it only ever reads the first two values. That is why simulation doesn't do what you think it should.4. You attempt to read the same memory array from two different addresses in the same clock. This is going to have problems during synthesis because true three-port memory does not exist inside FPGAs.

author: SiliconWizard
author_rank: Super Contributor
timestamp: May 04, 2021, 04:34:56 pm
content: On top of what asmi said, be aware that implementing asymmetric DP memory in HDL is ultra likely to be synthesized in a very non-optimal way by the specific vendor tool you're going to use. And if you actually hope that it will be inferred as block RAM, your odds are even lower. Unless you strictly follow the HDL guidelines for implementing exactly this kind of memory on your specific platform, you're in for a lot of pain. Synthesis tools are particularly dumb at inferring this kind of thing properly.Actually, I spent a while trying just that a while ago, because, generic is better. I finally gave up and instantiated a vendor IP instead. All FPGA vendors (that I know of anyway) have IPs for true dual-port asymmetric RAM.Of course, if all you want is code for simulation, there's no problem. asmi pointed you to some of the source of issues you're having right now. But I just wanted to add that once you have something fully working in simulation, the work is most likely far from being done if you want something half-efficient on FPGA.

author: NorthGuy
author_rank: Super Contributor
timestamp: May 04, 2021, 05:30:19 pm
content: A typical way is to create a module which instantiates the RAM block, may be add some glue logic, and then use the module in your design. If you move to a different vendor, you just re-write the module to match whatever the vendor provides. If you never move to a different vendor, you have just spared lots of useless efforts.You may infer RAM for a specific vendor, but inferring RAM for any vendor, let alone for a future vendor that don't yet exist, may not work well.

author: ali_asadzadeh
author_rank: Super Contributor
timestamp: May 04, 2021, 05:51:00 pm
content: QuoteThanks for the tip, it solved the problem, actually I just want it for simulation, because gowin does not have anything to simulate it's IP cores, and I need to use some DP rams with 8 and 16bit widths, so I have to impalement something for simulation, so that I can test the rest of my own logic.

author: ali_asadzadeh
author_rank: Super Contributor
timestamp: May 04, 2021, 06:14:50 pm
content: My code is now simulating ok, the question is how do we really code a true dual port ram with different configs, like a FPGA vendor? I mean how do they implement their IP, do you have any clue?

author: Bassman59
author_rank: Super Contributor
timestamp: May 05, 2021, 05:12:04 pm
content: Quote from: ali_asadzadeh on May 04, 2021, 06:14:50 pmThey implement the IP as a hard block -- a black box. The vendor will provide a library which includes black-box component declarations for these things. You instantiate that component in your design. The instantiation must include generics which are necessary to configure the block. Also the library component has every possibly address bit and every possible data bit brought out to ports, and you have to connect the correct ports in your design. The other ports are unused.When simulating your design, the tool will use a functional model of the memory, which they generally provide in a pre-compiled library. The source for those models should be available, though you might have to dig, and when you look at them, you'll see that they are not at all synthesizable.

author: SiliconWizard
author_rank: Super Contributor
timestamp: May 05, 2021, 05:31:17 pm
content: Quote from: ali_asadzadeh on May 04, 2021, 06:14:50 pmAs Bassman59 said, they are very vendor-specific and instantiate low-level primitives to ensure an exact implementation, as opposed to using pure HDL and let the tools infer the right primitives. As I mentioned above, there is NO right and sure way of writing 100% generic HDL code for such memories. That bites, but yeah.One reason I wanted to do that (and possibly your rationale is the same) is that it's much easier to change any parameter (width, depth, ...), whereas using IPs forces you to re-edit IPs and re-generate them, which can be pretty annoying, but there's no real generic way of handling this.So yeah. Use vendor IPs, and as NorthGuy said, encapsulate them in your own components with your own interfaces if that helps you write code easier to port to another target.For very simple stuff, such as basic memory blocks or even simple symmetric dual-port RAM, clean generic HDL code usually works for most vendors, but even then, the tools may infer block RAM or distributed RAM based on their own criterions which you may not understand whatsoever. There usually are attributes you can declare to force some memory to be inferred as block RAM or distributed RAM, but those are usually slightly different from one vendor to the next, so even this can't be quite generic. Also, the tools may decide not to comply with your attributes if it decides it's best to implement memory another way. Now if you don't care how your memory is actually implemented (for instance, when said memory is small enough that you don't really care), generic HDL can be used. Just realize that the result may be surprising.Oh and lastly, I've already encountered inference bugs, at least with Xilinx tools, for memory implemented as generic HDL. I was implementing a write-through dual-port memory, and the write-through feature (reading on one port @the same address and same cycle as the other port would yield the just written value), that I implemented via some MUX, wouldn't be implemented properly, whereas the HDL code was perfectly correct.

author: ali_asadzadeh
author_rank: Super Contributor
timestamp: May 06, 2021, 05:27:04 am
content: Thanks, I did not mean to use vendors IP cores, every body knows that they should use them, I meant that how we can model or code a piece of Dual port ram with different data widths for example to be implemented in an ASIC or to know how it's internals should work out of curiosity!

author: Bassman59
author_rank: Super Contributor
timestamp: May 06, 2021, 06:30:15 pm
content: Quote from: ali_asadzadeh on May 06, 2021, 05:27:04 amLook at any vendor's block RAM simulation model that supports different aspect ratios for the memory access. Be warned: some of those models are written in ancient dialects of the languages.That said, it's not all that difficult. (I use VHDL, so the points here are made for that language. I'm sure SystemVerilog has similar.)For a single-port memory, or a dual-port with only one write side, you use a signal to model the memory. The trick is that each side of a true dual port memory has its own clock, and each clock domain means separate processes, one for each. And remember how HDL processes (or always blocks) work: the process creates a driver for each signal assigned within. If you have more than one process assigning to a signal, you get more than one driver for that signal. And generally that's bad. You'll get a complier complaint.So the usual way to manage multiple processes assigning to a signal is to use a shared variable. Remember in VHDL, a variable normally exists only within a process. That means it can only be assigned in the process in which it is declared, and it can only be read in that process. A shared variable removes that process-only restriction: it can be accessed anywhere in the architecture of the entity in which it is declared.Old vendor models use that. There are processes for each domain which can assign to the shared variable memory array and read from it. But there's an obvious danger. What happens when both sides attempt to write to the memory simultaneously? (This is a problem in the real memory, which is where there is a distinction about "read first," "write first," and so on.)This was recognized by the maintainers of VHDL, so in modern VHDL-2008, the idea of a "protected type" was introduced. And you model the memory as a protected type. The cool thing about protected types is that they are like a C++ class. You use setters and getters (procedures) to access the memory.The gotcha is that with VHDL-2008 and later, all shared variables must be a protected type, and if you analyze old code with 2008, the compile will complain.For a dual-port memory model, you create a protected type that includes the data and the accessor methods, and then you declare a shared variable of that type. Then all accesses to that variable's data are done with the accessor methods, and those methods can be called from anywhere within the architecture.This is actually a very cool feature. Don't expect much of it to be synthesizable, but for modeling and verification it's great.

author: ali_asadzadeh
author_rank: Super Contributor
timestamp: May 07, 2021, 07:58:36 am
content: Thanks for the info

author: SiliconWizard
author_rank: Super Contributor
timestamp: May 07, 2021, 04:59:21 pm
content: Quote from: ali_asadzadeh on May 06, 2021, 05:27:04 amIf you're designing a digital ASIC, it's likely that you will also use some IP for RAM blocks, or at least some blocks from the PDK libraries. So in the end, you're also likely to instantiate black boxes rather than implementing this in pure HDL in a behavioral way, and then merely interconnect them with some glue logic.This doesn't really answer your question, which in the end is getting to know how this is implemented on a low level, but just wanted to point out that unless you're designing at a very low level, you may not have to care much more than with FPGAs if you're designing ASICs. Really depends on your requirements and specific workflow.So anyway, considering the case for which the larger width (Wb) is a multiple of the smaller width (Wa): one way to implement this with symmetric dual-port RAM blocks is to use MUXes to further split the larger width in Wb/Wa chunks, as you basically did with your simulation code. One further "exercise" would be to take your code, and replace the behavioral memory with a black box implementing the equivalent memory block. Then you can implement this black box as behavioral memory, or implement it instantiating a low-level primitive for a dual-port RAM. You'll get closer to how it can be done for an ASIC.