Jump to content

proper layering in an ethernet transaction class

Recommended Posts

I am creating an "Ethernet packet" class, extended from uvm_seq_item.  My first implementation was to include of course the Ethernet header, and then a polymorphic base class which holds all the data following the Ethernet header.  In this case, it could be either an ARP datagram, or an IP packet.  ("mydata" in the following code)

class eth_pkt_txn extends my_base_seq_item;

  const string report_id = "eth_pkt_txn";

  // Ethernet header
  rand bit [47:0]  SrcMAC;
  rand bit [47:0]  DstMAC;
  rand enum bit [15:0] { ARP=16'h0806, IP=16'h0800 } EthType;

  // Ethernet data
  my_base_seq_item mydata;

So when this class is randomized, EthType randomly chooses either ARP, or IP, and then in post_randomize(), the child object is created, and the handle assigned to the base (mydata) handle:

  function void post_randomize();

    if (EthType==ARP) begin
      mydata = ARP_data::type_id::create("mydata"); // assign an ARP child to base class
    end else if (EthType==IP) begin
      mydata = IP_pkt::type_id::create("mydata"); // assign an IP_ESAP child to base class

This works just fine.  This layered approach could continue inside the IP packet again, as you might have TCP, UDP, etc.


The layering is a powerful feature, but at the same time, it seems to make the transaction class harder to create, and modify, either manually or during randomization.


For example, say I want to do a randomize with {...} in my virtual sequence, the simulator can't find "mydata.IP_packet".  

if(!EthPkt.randomize() with {EthType == IP; mydata.IP_packet.IP_header.SrcIPaddr==4; }) `uvm_fatal(report_id,"EthPkt randomize() failed!")

It's not a member of the polymorphic base from which the children extend, so I wouldn't expect it to.  At the same time, I want to be able to quickly randomize and create a particular kind of packet.


In addition, method calls which are particular to a child class which are down in the class hierarchy (e.g. updateUDPchecksum() ) have to be accessed only by tunneling in through the heirarchy (cumbersome). 


Any thoughts on the best way to do this, and still have a convenient transaction class?  Should I forget about layering it and just have handles for everything I need at the top level of the transaction class?

Link to comment
Share on other sites

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;

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();

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

// create the corresponding 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.

Link to comment
Share on other sites

I definitely ran up against the exact types of limitations that you mentioned. 


(ideally) I'd like a single pass randomization which looks like the following (pseudocode):

EthPkt.randomize() with {EthType == IP; SrcIPaddr=='h12_34_56_78; IpType==UDP; UdpType==ESAP; EsapType==Set1553; etc }

In your example, I placed your randomize in a loop, and it fails after several iterations.  I believe the EthType randomizes to IPv4, hence arp_pload is a null pointer and the constraint fails: EDA playground


I am looking forward to your next post; if you could steer me towards efficacy with layering!

Link to comment
Share on other sites

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.).

Link to comment
Share on other sites

I found the semantics a little confusing; a low level protocol, such as Ethernet, is represented by a high abstraction level transaction.


I read all the material that you linked; I can see immediately some of the benefits of using a top-down approach...


1. translating from higher-level to lower-level should be much easier (generating checksums, lengths, etc)

2. the lower-level layers can be easily swapped out


I have a general understanding of how to implemented the hardware required to perform layering, I still have two concerns...

Using the top-down approach, I would be creating UDP packets, and starting them on my layering components.  How do I constrain fields at lower levels of the protocol stack, such as an Ethernet MAC address, or an IP address?


Lastly, would I be able to also start and execute ARP packets as well?  (E.g. when ARP, skip the 'C' sequencer, and manually hand the ARP packet to the 'B' sequencer, perhaps?)

Link to comment
Share on other sites

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.

Link to comment
Share on other sites

In response to the ARP question; awesome.  That will be very helpful.


Let me rephrase my other question:

When I create my top-level-protocol sequence item (e.g. UDP), I will randomize this object and apply constraints, and then start this transaction on the 'A' sequencer.  My work is done, yes?


Since I am no longer working with the bottom level transaction, how I randomize, constrain, or set values which are applied to lower levels of the protocol?

Link to comment
Share on other sites

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
  ip_packet = my_ip_packet::type_id::create();
  // convert from UDP/TCP to IP
  if (t_layer_item.protocol == UDP) begin
    // set IP address of 'ip_packet'
    // set contents, etc.
  // ...

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.

Link to comment
Share on other sites

right; but for example, in your area of code where you've written " // set IP address of 'ip_packet' "...


Perhaps on one run I would like to have it statically set to




Or on another run I would simply like to call





Is there a way to control this while creating/sending the UDP packet from the virtual sequence?  Surely it is common practice to modify the behavior of the translator sequences; how else would you affect the lower layers of the protocol on-the-fly?

Link to comment
Share on other sites

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);
      return convert_udp(packet);

  // 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

  // ... TCP packet conversion will be similar


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

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

Link to comment
Share on other sites

  • 2 weeks later...

Understood; I've got most of the layering agent already complete.  If I need to add any custom functionality, I will use inheritance as you mentioned.  It's really only a couple things I need to set, such as the MAC addresses at the Ethernet level.

The final piece of the puzzle involves the architecture of the layering.  On your great advice, the protocol layers have been distributed among several classes.  However, I now find myself at the top of the protocol stack, the application layer.  Specifically, our application level (i.e. the UDP data payload) consists of a header and a payload.  I'll call it the ESA_transaction.

The header contains a class and ID, and from these you can determine the format of the data payload.  These data payloads have radically different formats depending on the class/ID selected.  (just like if you have an ethernet header which indicates IP versus ARP, the data is completely different in length and content).  These various payload variations are defined in classes.

In the ESA transaction, would you define the payload as a general purpose data array (just like in all the other layers), or would you define the payload as an instantiation of a base class for the "payload" class messages (e.g. ESA_payload_base) , and use polymorphism?

class ESA_txn extends my_base_seq_item;

  rand esa_header  header;
  esa_payload_base payload ; // randomize manually in post_randomize


class ESA_txn extends my_base_seq_item;

  rand esa_header header;
  rand bit [7:0] payload[];

After having gone through the layering, I am leaning more towards the latter.  I like the flexibility of creating raw packets, and it by not using polymorphism, some of the complexity is further separated.

If I use this approach, at some point I will need unpack the contents of the "payload" into it's respective class object (assuming the data is valid), because I will need to collect coverage on their various data members.  My thought was to use the latter approach, and when say, a coverage collector needs to actual payload class object, it would call, say,


and the esa_transaction would return a pointer to the specific class object.  Sorry for the verbose description of a simple question.

Link to comment
Share on other sites

  • 4 weeks later...



A link of the layering agent I put together with your help; the sequencer path is optional; when enabled each sub-agent will place it's sequencer into the database.


The rightmost agent is my physical interface, LocalLink.


If any stage of the monitor path fails to unpack the contents of the prior stage, I simply halt the message and don't broadcast at that stage.  For example, if there is not enough information to build an IP header (malformed packet), or if no valid options are detected for a particular header entry.


Anyway this is leaps and bounds better than what I had before, just wanted to post this as a thank you.  Let me know if you have any further opinions on it.

Link to comment
Share on other sites

  • 6 months later...

Back in Februrary, we talked about the proper way providing layered protocols (like TCP/IP) to the DUT by using a layering agent.  This broke up the various stacks into different agents, each responsible for their portion of the stack.  As an example, I may create a UDP packet and start it on the UDP_2_IP sequencer.  It would flow up the pipe, being packaged into a payload, etc, until an Ethernet packet is created (header fields + generic data payload containing all the layers underneath), and then pass this transaction to the particular physical layer being used (in my case, LocalLink).

This has made stimulus generation easier in many ways.  The dividing up, however, has made some things harder.  For example, I would like to cover that I sent an IGMP Report (IGMP layer) to a particular destination address (IP layer).

How this plays out in reality is:
1. I send out the packet
2. The ethernet monitor detects a packet on bus, and broadcasts the transaction on analysis port. (not pertinent for my example)
3. The IP monitor detects a packet on bus, and broadcasts the IP transaction on analysis port. (this has my IP destination port, so I'll need this information in a covergroup!)
4. The IGMP monitor detects an IGMP packet on bus, and broadcasts the transaction on analysis port (has the "type"=REPORT which I also need.

So I have one coverage component with several write functions, each for a different layer of the protocol.  When the IGMP packet arrived, I simply sampled the igmp covergroup.  This covergroup needs to know the destination IP address so I can cross the IGMP Type with Destination IP.  Well, the IP transaction would have already arrived ( since the packet must go through that monitor to get to the IGMP monitor ), so I'll simply use a local copy of IP transaction to grab the destination IP.  

Wrong... the IP transaction can, and does arrive AFTER the IGMP transaction because of the scheduling algorithm (both analysis port writes occur in the same delta cycle).    

UGH! Is anything ever easy with UVM!  Am I supposed to now synchronize these things so that the IP transaction arrives first (common sense)?  If there was only a single transaction exchange occurring for the monitoring/coverage collection process, per packet, this obviously wouldn't be an issue; all the information required for coverage collection would be contained in the packet.

Link to comment
Share on other sites

In Specman there was a special event that happend at the end of a delta cycle, tick_end, which you could use for this. In SV/UVM there is a task that simulates this, uvm_wait_for_nba_region(), which you could call once you get your IGMP packet. This way you'd be sure that the write_ip(...) would also get called by the time you return from the wait. It's not pretty, but it's pragmatic.


Another thing you could do is define some field in your IGMP packet where you'd reference the originating transaction, in your case IP. You could make it of type uvm_sequence_item if you intend to be able to layer this protocol on top of anything else other than IP. Your monitor would handle assigning this field when it's also figuring out the values for other fields.

Link to comment
Share on other sites

In Specman there was a special event that happend at the end of a delta cycle, tick_end, which you could use for this. In SV/UVM there is a task that simulates this, uvm_wait_for_nba_region(), which you could call once you get your IGMP packet. This way you'd be sure that the write_ip(...) would also get called by the time you return from the wait. It's not pretty, but it's pragmatic.



This is interesting stuff to know; that having been said, I personally draw the line at waiting for the nba region of the SV scheduler.  That is a rabbit hole I won't go down; I choose the blue pill.


Another thing you could do is define some field in your IGMP packet where you'd reference the originating transaction, in your case IP. You could make it of type uvm_sequence_item if you intend to be able to layer this protocol on top of anything else other than IP. Your monitor would handle assigning this field when it's also figuring out the values for other fields.



This worked after a couple bumps.  First off, my IP transaction and igmp transaction both (now) have references to each other, so compilation was impossible.  I quickly looked to see if there was a class prototype feature, gave up, and then used uvm_sequence_item instead.  Then, in my coverage component write function, I downcast the uvm_sequence_item pointer to the required type (ip transaction).  Good idea Tudor.  Bailed me out again. 

    function void write_igmp(igmp_transaction t);
      ip_transaction ip_t;
      `uvm_info(report_id, "copying the IGMP pointer!", UVM_DEBUG)
      igmp_txn=t; // copy handle
      if (!$cast(ip_t, t.seq_item)) `uvm_fatal(report_id, "cast failed!") // downcast
Link to comment
Share on other sites

Even if two classes reference each other, you just need to add a forward typedef:

typedef class ip_packet;

class igmp_packet;
  // ...
  ip_packet ip_pkt;

This tells the compiler, "whenever you see 'ip_packet' treat it as a class and I'll describe it later". This is the class prototype feature you were looking for.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Create New...