Modeling RAMs as models rather than as transactors has advantages and disadvantages. A transactor implementation doesn't need to represent memory and can verify the correct address and data. In case of a write it's possible to identify the problem at the time the write access occurs, rather than the time at which reading back occurs. However a passive implementation does not need to know which test is executed and is in that respect more generic than the transactor version.
One of the problems of implementing RAM models is representing internal memory. A large array of std_logic_vectors (eg. 2Mx64 bits) needs a large chunk of memory on the machine it's simulated on.
A possible approach is to represent the memory cells with integers (of an appropriate range) rather than std_logic_vectors. (One such approach can be found on Ben Cohen's website.) The approach taken here is based on the observation that most tests only access a tiny fraction of the modeled memory. For each write access the data as well as the address is stored into the internal array. For a subsequent access the model searches the array and identifies whether the address is already present. So if there are only 32 different addresses which are accessed, then there is no reason to have more than that number of memory locations in the model. However searching the array is more time intensive than using the address as an index, therefore there is likely to be a cross over point, where searching requires more resources than having a large array.
Here is the VHDL for this memory representation:
-- Internal Memory type mem_add_type is array (integer range <>) of std_logic_vector(A'range); type mem_dat_type is array (integer range <>) of std_logic_vector(D'range); variable mem_add: mem_add_type(mem_words-1 downto 0); variable mem_dat: mem_dat_type(mem_words-1 downto 0); variable used_pnt: integer := 0;The parameter mem_words is a generic defined in the header, it can be set during instantiation for the required number of memory locations.
One of the additional tasks for the passive memory model, is to decide whether to call the read or the write procedure. The code below will take care of that:
D <= (others => 'Z'); wait until WE_L'event or RD_L'event; assert (WE_L /= 'X' and WE_L /= 'Z' and WE_L /= 'U' and WE_L /= '-') or no_reset_yet report "E@SRAM2: WE_L="& str(WE_L)& " invalid value" severity Error; assert (RD_L /= 'X' and RD_L /= 'Z' and RD_L /= 'U' and WE_L /= '-') or no_reset_yet report "E@SRAM2: RD_L="& str(RD_L)& " invalid value" severity Error; assert to_X01(RD_L) /= '0' or to_X01(WE_L) /= '0' report "E@SRAM2: both read and write are asserted"& "RD_L="& str(RD_L)& " WE_L="& str(WE_L) severity Error; -- decide whether read or write access if to_X01(WE_L) = '0' then write; end if; if to_X01(RD_L) = '0' then read; end if; end process test_proc;The process will wait until activity either on RD_L or WE_L occurs. Illegal values for these signals ('U', 'X', '-' and 'Z') are reported as errors, they should never occur during simulation. Also if both signals are asserted simultaneously an error is reported. The function to_X01 will convert 'H' and 'L' values to '1' and '0' respectively. This is useful on busses which are pulled up or down and reflects the actual behaviour of the SRAM.
The write process is similar to the one for the transactor version. However the address and data verification can no longer be performed. Also the write access now has to be stored in the memory array:
... wait until to_X01(WE_L) = '1'; -- Store written data for i in 0 to used_pnt loop if i = used_pnt then -- access to a new address mem_add(i) := A; mem_dat(i) := D; if used_pnt < mem_words - 1 then used_pnt := used_pnt + 1; else print("W@SRAM2: Simulation model can't handle additional addresses"); end if; end if; if mem_add(i) = A then -- access to an existing address mem_dat(i) := D; exit; end if; end loop;The code loops through the already written array until a match for the address is found. If no match can be found and there is still room in the array, the new address is entered and the usage pointer is incremented. If there is no more space available a warning is issued.
The counterpart of this code can be found in the read procedure:
-- Retrieve data from internal memory for i in 0 to used_pnt+1 loop if i = used_pnt+1 then -- access to a new address print("W@SRAM2: Address has not been written to yet"); print("I@SIMPLE_SRAM: "& hstr(xx)& " provided for "& hstr(A)& "h"); D <= (others => 'X'); exit; end if; if mem_add(i) = A then -- access to an existing address D <= mem_dat(i) after tRD; print("I@SIMPLE_SRAM: "& hstr(mem_dat(i))& "h provided for "& hstr(A)& "h"); exit; end if; end loop;The loop will investigate all written memory locations and try to establish a match. If successful it will take the data from that location and drive it onto the bus, otherwise it will issue a warning and drive all X's on the data bus.
Below are the files which have been simulated in this section:
txt_util.vhd | sram2.vhd | mp.vhd | tb_simple2.vhd |