Jump to content

tudor.timi

Members
  • Posts

    289
  • Joined

  • Last visited

  • Days Won

    33

Posts posted by tudor.timi

  1. You're on the right track with get_full_name(), but you should do one small change:

    this.cd = command_descriptor::type_id::create("cd", null, get_full_name());
    

    This way you pass a short name ("cd") as an object name and no one can complain. You don't have any component to pass in as a parent if you're starting the item in a sequence (as sequences are also objects), which is why we pass null. Instead of getting the context from a component, you can just pass it in directly with the third argument. In order for this to work, the sequence also has to have been created with a context.

  2. I'd be careful when comparing frontdoor with backdoor, seeing as how frontdoor accesses will take simulation cycles to complete (backdoor is done in 0 time). That extra time that needs to be simulated will also affect the total job length. The frontdoor also has the extra overhead of triggering your bus UVC(s) and the sequencers/drivers/etc. will also slow down your simulation.

     

    A fair comparison in terms of simulation length would be with "RAL and frontdoor" against "no RAL and native bus sequences" that do exactly the same thing. It's probably going to run faster without the RAL, but "how much faster?" is the question. Also keep in mind that by not having the RAL you'll lose productivity (no checks => implement your own) and maintanability (you'll pass register addresses around). I'd think this is a trade-off I wouldn't want to make.

  3. I'm trying to declare a memory like this:

    class some_mem extends uvm_mem;
      function new(string name = "some_mem");
        super.new(name, 2 ** 24, 8, "RO");
      endfunction
    endfunction
    

    Later on, I'm trying to add this memory to an address map that was created like this:

    default_map = create_map("some_map", 0, 4, UVM_NO_ENDIAN);
    

    By using the default value for the byte_addressing argument I want each address to represent a one byte location. The problem I'm seeing is that the uvm_reg_map_info associated with this memory shows an end address of 32'03ff_ffff and a stride of 4, instead of 32'00ff_ffff and a stride of 1. I've narrowed it down to the function uvm_reg_map::get_physical_address(...), at line 1378 of uvm_reg_map.svh:

    int multiplier = m_byte_addressing ? bus_width : 1;
    

    I guess this should be the other way around:

    int multiplier = m_byte_addressing ? 1: bus_width;

    Even better would be to use:

    int multiplier = get_addr_unit_bytes();
  4. I don't see why doing a read would be bad form, seeing as how this is most probably how the real software would work. The fact that you have the state of a register stored and can rely on it is a "nice-to-have". If you want to do RMW, what you want to ensure is that this is treated as an atomic operation, i.e. there are no other accesses in between. To do this you can grab your register sequencer before doing the read and ungrab it after doing the write.

  5. I'm not sure how arbitration works at the register read/write level, but if you have 2 writes scheduled because the bus sequencer is busy with some other transaction, I could imagine that you can run into some "unexpected" behavior. I could see a scenario where the following happens:

     

    - bus sequencer is busy doing some item

    - thread 1 does field1.set() and starts waiting

    - thread 2 does field2.set() and starts waiting

    - item finishes on bus sequencer

    - thread 1 gets the sequencer, writes the val (reg with both fields set)

    - thread 2 gets the sequencer, writes the same val as thread1 wrote

     

    This won't "clobber" any field value, but it will just do the same write twice, which shouldn't be a problem.

  6. Using the UVM pools is a shortcut you can take, but it might hurt you further down the line. Consider the case where you wouldn't pass the monitor handle to your sequence and use the uvm_event_pool instead. You wouldn't be able to tell by just looking at the class declaration that it has an external dependency. You'd have to read the body() method to see that at some point it's waiting on event from outside. Even in your current setup, it might be cleaner to get a uvm_event handle from the monitor and not the monitor itself, as that would show that you only need the event and nothing more.

  7. Now that Robert mentioned it, I also remember seeing VIP from one vendor that did such a separation of concerns. An active agent would be used to drive stimulus on an interface with a handshake mechanism. A passive agent hooked onto the same interface would monitor the DUT's handshakes and check that everything is in order. You could tap any of the monitors to feed the scoreboard, since they both collect the same thing.

  8. It's unclear to me what you mean. Do you have a device with two (almost) identical interfaces, where traffic flow only in one direction?

          _______
    IF1  |       |  IF2
    ---->|  DUT  |---->
         |_______|
    

    In this case it makes sense to instantiate 2 agents, because you have 2 different wire bundles. 

     

    If not, do you have only one bidirectional interface?

          _______
    IF1  |       |
    <--->|  DUT  |
         |_______|
    

    In this other case you only need one agent. You can have one monitor feed the scoreboard through an analysis port and the scoreboard itself would figure out what is RX and what is TX (based on the item direction).

  9. I was just reading the 1800-2012 standard and I've come across something that strikes me as a possible contradiction. In section 18.8 Disabling random variables with rand_mode() it states that

     

    The syntax for the rand_mode() method is as follows:

    task object[.random_variable]::rand_mode( bit on_off );
     
    [...]
     
    If the random variable is an object handle, only the mode of the variable is changed, not the mode of random
    variables within that object (see global constraints in 18.5.9).
     
    Later on, when an example is presented, I see the following code snipped:

     

    class Packet;

    rand integer source_value, dest_value;
    ... other declarations
    endclass
    int ret;
    Packet packet_a = new;
    // Turn off all variables in object
    packet_a.rand_mode(0);
    // ... other code
    // Enable source_value
    packet_a.source_value.rand_mode(1);
    ret = packet_a.dest_value.rand_mode();

     

    The comment states that packet_a.rand_mode(0) disables randomization for all variables inside the object, which is against the statement from above.

     

    Could anyone who's part of the SV committee shed some light on the issue?

     

  10. I don't really get why you would like to statically set the IP address in one run, but of course you could do that. You can just use inheritance and the factory to set an override to a type that does your desired functionality.

     

    For example, I'd do something like this:

    class transport_to_ip_sequence extends uvm_sequence;
      
      // ... body method and other stuff
    
      // the body method calls this function to get an IP packet from a
      // TCP/UDP packet
    
      function ip_packet convert_transport(transport_packet packet);
        if (packet.protocol == TCP)
          return convert_tcp(packet);
        else
          return convert_udp(packet);
      endfunction
    
      // this function converts from a UDP packet to an IP packet
      function ip_packet convert_udp(udp_packet packet);
        // ... look up the IP address based on the socket
        // ... pack the packet inside the IP payload
      endfunction
    
      // ... TCP packet conversion will be similar
    
    endclass
    

    Note that the code is missing some casts to keep it simple. Converting from a UDP packet to an IP packet should be a deterministic process (or?). The convert_udp(..) function would handle that. You can also implement it as a big randomize in case it's not fully deterministic (i.e. some fields are set depending on other factors in the system), where you constrain the fields of the IP packet that you can figure out based on the UDP packet's fields.

     

    If for some reason you want to change the conversion function inside a specific test, you can just extend this class and override the methods you want:

    class static_ip_transport_to_ip_sequence extends transport_to_ip_sequence;
      function ip_packet convert_udp(transport_item packet);
        // some other conversion logic you want here
      endfunction
    endclass
    

    You'd just set a type override in your test to choose this other sequence.

  11. We're also comparing apples with oranges here in this thread. A reference in C++ is a different concept from a reference (to an object) in SystemVerilog (which is more like a reference in Java). SystemVerilog decided to take elements from both languages (the pass by whatever semantics of Java when passing objects together with allowing pass by value and by reference as in C++) and mix them into a nice soup of confusion.

     

    Java doesn't allow immutable arguments (because there "the general ideology is that access policy to the state of an object is controlled by the class" - according to this site). C++ does provide this, because it doesn't discourage designs with a lot of public fields (according to the same site). SystemVerilog also encourages such designs (think of all the nice fields that are declared as rand), but doesn't allow any concept of immutable arguments.

     

    Maybe in a future version of the standard we could get something like some_function(const some_class arg) which would be equivalent to a constant reference in C++. Or maybe if the developers do implement it, we'll see the less useful behavior of Java's final arguments instead.

  12. @puneet You're half right with your hierarchical immutability statement. The C++ compiler doesn't follow through pointers and references, but if I have a data member defined that is an object, then I can't change any of its fields. Here's a snippet of C++ code I've tried this out with:

    #include <iostream>
    
    struct some_inner_struct {
      int some_inner_field;
    
      some_inner_struct() {
        some_inner_field = 0;
      }
      
      void print() {
        std::cout<<"some_inner_field = "<<some_inner_field<<std::endl;
      }
    };
    
    struct some_struct {
      int some_field;
      some_inner_struct some_obj;
      some_inner_struct& some_ref;
      some_inner_struct* some_pointer;
    
      some_struct() : some_ref(some_obj) {
        some_field = 0;
      }
      
      void print() {
        std::cout<<"some_field = "<<some_field<<std::endl;
        std::cout<<"some_obj: ";
        some_obj.print();
    
        std::cout<<"some_ref: ";
        some_ref.print();
        
        if (some_pointer != NULL) {
          std::cout<<"some_pointer: ";
          some_pointer->print();
        }
      }
    };
    
    void some_function(const some_struct& arg) {
      // this isn't allowed because it's trying to modify a data member of
      // the argument
      //arg.some_field = 5;
    
      // this also isn't allowed, because the compiler knows that 'some_obj'
      // is a data member of 'arg'
      //arg.some_obj.some_inner_field = 5;
    
      // the following are allowed because the compiler doesn't follow refs and
      // pointers, i.e. it only treats the contents as data members
      arg.some_ref.some_inner_field = 5;
      arg.some_pointer->some_inner_field = 5;
    }
    
    int main() {
      some_struct obj;
      some_inner_struct inner_obj;
      obj.some_pointer = &inner_obj;
      
      std::cout<<"before"<<std::endl;
      obj.print();
        
      some_function(obj);
      
      std::cout<<"after"<<std::endl;
      obj.print();
      
      return 0;
    }
    

    Notice that if I try to change any of the fields of some_obj in some_function(...), I'd get a compile failure.

  13. Oh, now I get what you meant.

     

    Yes, you just randomize your UDP packets and start them on the 'A' sequencer. From that sequencer it's going to be picked up by the translation sequence running on the 'B' sequencer. In your translation sequence you'd have something like:

    forever begin
      // get a UDP/TCP item from the 'A' sequencer
      up_sequencer.get_next_item(t_layer_item);
      ip_packet = my_ip_packet::type_id::create();
      start_item(ip_packet);
      
      // convert from UDP/TCP to IP
      if (t_layer_item.protocol == UDP) begin
        // set IP address of 'ip_packet'
        // set contents, etc.
      end
      // ...
      
      finish_item(ip_packet);
      up_sequencer.item_done();
    end
    

    Something similar is going to happen on the IP to Ethernet path.

     

    Your infrastructure work is mostly going to be implementing these translation sequences and the reconstruction monitors.

  14. Let's start with question 2 first since it's easier to answer. As you correctly pointed out, you can start items on any of the A/B/C sequencers, allowing you to work at the level of abstraction you need. For example, you might figure out that you need to write some raw Ethernet traffic where you send some corrupted frames to check the DUT's error handling. Same for IP, etc. This also allows you to run other protocol over Ethernet that aren't as abstract as TCP/UDP (as you mentioned, ARP).

     

    Regarding question 1, I don't really know all of the details. I don't work with networking chips and the details are a little fuzzy from college, but I do remember there was a 1 to 1 correspondence between an IP address and a MAC address within a network. Some details you can read here. You basically have to store an ARP table inside your UVC. When going from UDP to IP, I remember there was a relationship between a UDP socket and the IP address. I don't have anymore details here, unfortunately. Your system architect could probably shed some more light on these topics.

  15. Normally, an Ethernet frame doesn't care what it's transporting (i.e. what's in its payload). The moment you start saying that you want an Ethernet frame that contains an IP packet with a certain IP address and so on it's clear that you're thinking at a higher level of abstraction. What you're interested more is in the IP fields and it makes sense to separate these from the Ethernet fields.

     

    In your context, layering would mean having individual items for each layer. At the top you'd have TCP and UDP items. The middle layer would consist of IP (v4 and/or v6) item(s), ARP, etc. At the very bottom you'd have the Ethernet frame. You'd also have some infrastructure in there to deal with the conversion from one layer to each other - encapsulating TCP inside and IP packet and encapsulating that inside an Ethernet frame. This is what basically goes on in a real system.

     

    Now, IP packets can be sent over Ethernet, but also over WiFi and other protocols. If I write a sequence that starts a few IP packets, I don't really care through what link those packets get sent and it should work if I swap out the old Ethernet interface with a new WiFi interface (or maybe add this as a new interface). Doing things bottom up, like you started (where the items you start are Ethernet frames and the payload gets specialized) won't work allow you to do this easy swapping, even though they might allow you to think at a higher level of abstraction.

     

    Here are some links that describe how to implement layering in UVM:

    http://verificationhorizons.verificationacademy.com/volume-7_issue-3/articles/stream/layering-in-uvm_vh-v7-i3.pdf (in your case the A layer would be TCP/UDP, B would IP (and ARP), C would be Ethernet)


     

    I remember seeing an example exactly for Ethernet and IP, but I can't find it anymore unfortunately.

     

    P.S. I've also had a look at the AMIQ package. It's also constructed bottom-up, but they use inheritance to create various Ethernet subtypes (ARP, IP, etc.).

  16. It's a well known limitation of SystemVerilog that you can't do any kind of casting inside constraints, which means that it won't suffice to just have a handle for all payloads. You'd need one handle to each type of payload to be able to reference the fields defined under each specific payload type:

    class ethernet_frame;
      // ...
    
      rand ip_payload ip_pload;
      rand arp_payload arp_pload;
    endclass
    

    Afterwards, we hit the next limitation, due to which we're not allowed to create objects during randomization. I wouldn't do this in post_randomize() as this would hurt us later:

    class ethernet_frame;
      // ...
      
      function void update_payload();
        ip_pload = null;
        arp_pload = null;
        
        case (ether_type)
          IPv4 : ip_pload = new();
          ARP : arp_pload = new();
        endcase
      endfunction
    endclass

    The problem with creating the objects like this is that we can't apply any inline constraints in the first pass (when we also randomize the ether_type field). One thing we could do is always create both payloads regardless of the type of frame, but that would be wasteful. The other thing we can do is have another call to randomize() only on the payload.

     

    The best thing to do in this second case is to randomize both payloads in the scope of their parent:

    // randomize eth_type
    frame.randomize();
    
    // create the corresponding payload
    frame.update_payload();
    
    // randomize payload contents inside the scope of 'frame'
    // - this is why I don't want to use post_randomize to update the payload handles as
    //   they would get overwritten here
    frame.randomize(ip_payload, arp_payload) with { ether_type == ARP -> arp_pload.htype == 1; });
    

    This way any global constraints defined inside the ethernet_frame class that reference payload fields would still hold. You can find more info on this syntax in Section 18.11 In-line random variable control of the standard. Here's a more complete example on EDA Playground if you want to play with it.

     

    You could make it work like this, but it's rather clumsy. You have to work around a lot of limitations of the language.

     

    ***e (IEEE 1647) plug: All of this is very easy and neat to achieve in e***

     

    Take this post as a side-note to your question. This isn't the proper way to implement layering. I'll discuss this in the next post.

  17. Oops, I opened my mouth too soon and I didn't notice we were talking about constructors. Here's what the standard states here:

     

     

    A super.new call shall be the first statement executed in the constructor. This is because the superclass

    shall be initialized before the current class and, if the user code does not provide an initialization, the
    compiler shall insert a call to super.new automatically.

     

    This means you can't pass the length to the constructor. I see to ways around it:

     

    1. Use the same `define you're using in the top level TB in the constructor call:

    super.new(name, `NUM_TIMERS, UVM_NO_COVERAGE);
    

    2. Use a type override. Define a generic regs_counter_e_reg and override it with a specific version where the length is set by a parameter:

    class regs_counter_e_reg #(int length) extends abstract_regs_counter_e_reg;
      // ... registration ...
    
      function new(name);
        super.new(name, length, UVM_NO_COVERAGE);
      endfunction
    endclass
    

    This means you have to register both classes with the factory and use create(...) to instantiate them, which is more involved.

     

    If Erling's suggestion works for you, you should use that one as that seems the most cleanest. You can worry about portability later, once you're done with your project. Out of experience, any tool migration is anyway going to cause problems (due to all of the gray areas in the standard or just plain inconsistencies) so I wouldn't sweat this.

×
×
  • Create New...