How To Protect FIFOs Against Overflow – Part 1

Systems containing FIFOs face verification engineers with a “classic” black-box verification problem: how to protect FIFOs against overflow in order to avoid unpredictable loss of packets.

The difficulty in solving this problem comes from the lack of visibility into DUT’s internal states, which means the solution should count only on the events/packets driven/monitored on the DUT’s interfaces. That being said it means the protection mechanism is not cycle-accurate and the resource locking and freeing is done in a “pessimistic” way (i.e. lock resources early and free them late).

The complete solution can be downloaded from GitHub.

The Device Under Test

Let’s consider a simple DUT and verification environment like the one in Fig. 1.
Example Device Under Test

Fig. 1. An example of DUT and Verification Environment

One should keep in mind that the following specification covers a particular kind of design and it is not aiming to be valid for any DUT.

MESSAGE packets have a fixed size and they are written into the RAM at specific addresses. They can be buffered until RAM interface is available for MESSAGE write operations.

READ packets are forwarded as read requests on RAM bus, the RAM response is buffered until input interface becomes available and then sent back to input agent as a READ RESPONSE packet. In my case, the DUT prioritizes the accesses to RAM interface in favor of READ packets. The DUT is expected to receive READ packets at a very low pace.

The DUT contains two FIFO’s for different data paths:

  • the FIFO_MSGS stores up to 16 MESSAGE packet descriptors of fixed size
  • the FIFO_RESP stores the payload of READ responses (i.e. can vary from one READ to another), up to 4096 Bytes

The rest are UVM components or sequences.

The Solution(s)

The solution I propose uses a resource counting mechanism (i.e. SystemVerilog semaphore, uvm_objection) to keep track of consumed FIFO resources going through the system and a singleton object to provide a single point of access for the clients using the FIFO protection.

The solution can be implemented in at least two ways:

Aspect Solution 1 Solution 2
Singleton SystemVerilog class Inherits uvm_object
Resource Counting SystemVerilog semaphore uvm_objection

To keep it short this post details only the Solution 1 implementation, while the Solution 2 implementation will be presented in a follow-up post. Both implementations can be downloaded from GitHub.


Step 1. Define enumeration items that identify the two FIFOs.

typedef enum {FIFO_MSGS, FIFO_RESP} fifo_t;


Step 2. Implement the singleton object using a SystemVerilog class.

class fifo_protection;
   // singleton instance
   static local fifo_protection m_inst;

   // Constructor
   protected function new();
   endfunction

   // Retrieves the singleton instance
   static function fifo_protection get();
      if(m_inst == null)
         m_inst = new();
      return m_inst;
   endfunction
endclass


Step 3. Add the semaphores

Add the semaphore list which contains one semaphore for each FIFO. Add other utility fields which enable the semaphores or specify their resource limit.

// list of semaphores: one for each FIFO
semaphore semaphores[fifo_t];
// enable flag that allows us to enable/disable fifo protections
bit       sm_en[fifo_t];
// list of fifo limits
int       sm_limit[fifo_t];


Step 4. Add semaphore initialization function

function void init_protection(fifo_t kind, int limit, bit en);
   semaphores[kind] = new(limit);
   sm_limit[kind] = limit;
   sm_en[kind] = en;
endfunction
// Enables/disables the given FIFO semaphore
function void set_enable(fifo_t kind, bit en);
   sm_en[kind] = en;
endfunction


Step 5. Add resource locking/freeing API

function void free(fifo_t kind, int nof_keys);
   if (!sm_en[kind])
      `uvm_warning("FIFO_PROT_FREE_WRN", $sformatf("The semaphore %s isn't enabled.",kind.name()))
   semaphores[kind].put(nof_keys);
endfunction

task lock(fifo_t kind, int nof_keys);
   if (!sm_en[kind])
      `uvm_warning("FIFO_PROT_LOCK_WRN", $sformatf("The semaphore %s isn't enabled.",kind.name()))
   semaphores[kind].get(nof_keys);
endtask
 
function int try_lock(fifo_t kind, int nof_keys);
   if (!sm_en[kind])
      `uvm_warning("FIFO_PROT_TRYLOCK_WRN",$sformatf("The semaphore %s isn't enabled.",kind.name()))
   return semaphores[kind].try_get(nof_keys);
endfunction


Step 6. Add semaphore status API

function int get_resource_count(fifo_t kind);
   get_resource_count = 0;
   for (int i=1; i<=sm_limit[kind]; i++) begin
      if (semaphores[kind].try_get(i) == 0)
         break;
      get_resource_count = i;
      semaphores[kind].put(i);
   end
endfunction
   
function bit are_all_free();
   int is_free[$];
   fifo_t ft = ft.first();
   are_all_free = 1;
   forever begin
      are_all_free &= (get_resource_count(ft) == sm_limit[ft]);
      if ( ft == ft.last )
         break;
      ft = ft.next;
   end
endfunction


Step 7. Add debug API

function string dump();
   fifo_t ft = ft.first();
   dump = "";
   forever begin
      dump=$sformatf("%s%s=%d/%d, ", dump, ft.name(), get_resource_count(ft), sm_limit[ft]);
      if ( ft == ft.last )
         break;
      ft = ft.next;
   end
   dump=$sformatf("fifo_protection available resources: %s", dump);
endfunction


Step 8. Create a global variable for the singleton

fifo_protection fifo_prot_ston = fifo_protection::get();


Step 9. Initialize protections

One can initialize protections for various FIFOs in the build_phase() of one's verification environment:

fifo_prot_ston.init_protection(FIFO_MSGS, 16, 1);
fifo_prot_ston.init_protection(FIFO_RESP, 4096, 1);

The limit argument (2nd argument) represents the upper limit of the FIFO.
The granularity is either packets or bytes/words/etc:

  • FIFO_MSGS contains packet descriptors of fixed size so the semaphore limit represents packets (in our case 16 packets)
  • FIFO_RESP contains whole packets whose size can vary so the limit argument represents bytes/words/etc (in our case 4096 Bytes)


Step 10. Lock and free resources

In order to protect the FIFOs there are two important steps:

  • lock resources before the packet gets into its FIFO
  • free resources after the data flow corresponding to the FIFO is checked

The points in time when the resources are locked/freed are critical for this mechanism to provide protection against overflow, so one should do a data flow analysis to determine the right moments( see more here). In Fig. 2 I represented graphically the data flows for READ and MESSAGE packets, the calls to scoreboard and to the fifo_protection object in order to ease the understanding of data flows and of the points in time were the resources are locked/freed.

Data Flows
Fig. 2. Data Flow Analysis

For MESSAGE data flow one needs to:

  • lock 1 resource just before sending a MESSAGE (in MESSAGE sequence, just before `uvm_do)
  • release 1 resource as soon as the RAM write access is verified against the original MESSAGE packet
// lock resources in MESSAGE sequence, just before `uvm_do
fifo_prot_ston.lock(FIFO_MSGS, 1); 
`uvm_do(msg_item)
……………………
// free resources in scoreboard, right after the RAM-write access is verified
fifo_prot_ston.free(FIFO_MSGS, 1);

Similarly for READ data flow one needs to:

  • lock “number of READ bytes” resources as soon as the input monitor detects a READ request. Given my particular example the resource locking could be done later than MESSAGE resource lock.
  • Free “number of READ bytes” resources after scoreboard checks the READ response going out on the input interface
// lock resources in scoreboard as soon as a READ request is detected on the input interface by the input monitor
fifo_prot_ston.lock(FIFO_RESP, read_req.size);
……………………
// free resources in scoreboard, right-after the READ-response on input interface is verified
fifo_prot_ston.free(FIFO_RESP, read_resp.size);


How to disable the protection

There might be scenarios that require to overfill one or more of the FIFOs. In that case it is sufficient to disable the semaphores for the given FIFO using the dedicated API:

fifo_prot_ston.set_enable(FIFO_RESP, 0);

It is recommended to disable one FIFO at a time and have different tests for each FIFO overflow scenario.

To be continued...until then feel free to comment or present your own approach for this kind of problems.

Comments

2 Responses

  1. Hi Stefan,

    I think the fact that all your functions take ‘kind’ as an argument is a hint that you have multiple objects trying to come out. Instead of using the singleton, you could have a ‘fifo_protection’ class that is instantiated multiple times. There’s no need to define an enumeration to identify the FIFO being accessed and the approach can be extended to any number of FIFOs (without using parameterization of the ‘kind’ type or any other tricks).

    Is the idea behind the singleton to allow for easier debug? This can probably still be achieved by having a granular API for each FIFO protection object and a simply looping over all objects.

  2. Hi Tudor,
    You’re right: I could get a similar effect by using a list of semaphore/objection-wrapper objects (i.e. fifo_protection as you called them) instantiated multiple times either in a list or in various verification components/environment.

    Why I went the path presented in the post? Well, well, here we go:

    1. in my particular implementation the singleton contained a coverage group that sampled the resource usage as a cross between various resources, so I needed all protections in the same place
    2. to enforce some discipline: all developers working on the environment had a single place to add/access protection mechanisms. no chance of spilling the protection mechanisms all over the place :))
    3. to keep a clean/clear resource access syntax. if you have a list of fifo_protection you still need some way, index/integer- or enumeration-based, to access a given protection. an enumeration item is much easier to grasp than an integer. the verification engineer can define the enumeration type correctly as soon as one knows which FIFOs wants to protect.
    4. using the list search mechanism(e.g. list.first(it) with (it.kind == CEVA) ) to access a list entry requires to first retrieve the object and then call it’s method. this means an extra variable given that lists API/simulator don’t allow chained calls (i.e. list.first(it).lock(3) )

    A side comment regarding resource usage cross coverage: it’s not cycle-accurate, but it’s good enough for scenario debug purposes.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe to our newsletter

Do you want to be up to date with our latest articles?