Improving the trivial IPv4 switching program
This tutorial adds per-ingress-port counters to the IPv4 program on the X-Switch ISA (XISA). It defines an atomic counter table, computes each counter's index from the ingress port number and a packet-type code, and increments the matching packet — and optionally byte — counter for good packets, for each drop reason, and for IPv4 LPM hits and misses.
Welcome back network enthusiasts! We're Xsight-ed to continue our deep dive into the inner workings of programmable network switches using the X-Switch Instruction Set Architecture (XISA).
Our journey began with the fundamentals – exploring a basic cross-connect example to understand how packet forwarding works at the microcode (uCode) level. We then took a significant step forward by tackling a Simple IPv4 switching program, which introduced concepts such as packet parsing to identify headers and fields, preloading data into MAP registers, and performing Longest Prefix Match (LPM) lookups incorporating VRF functionality.
Later, we added IPv4 header validation code and learned to modify the TTL and update the header checksum. Our code became more robust, detecting and subsequently dropping many kinds of packets, thereby preventing them from leaking into the network or being mis-forwarded.
Now, let's place ourselves in the shoes of a system administrator. If all they have to go on are the standard SNMP MAC counters, then any discrepancy between the total number of packets sent or received would indicate a problem — but wouldn't reveal its nature. Ideally, we would want to make sure that packets didn't just vanish without leaving a trace. The best way to achieve such visibility is by counting the packets — especially the ones that were dropped (for example, because of a failed validity check, like the one we introduced in our previous article).
This is precisely the functionality we want to add. In today's example we'll walk you through how to add a counter table to a program written using XISA. We'll go step-by-step — starting with the necessary definitions and code updates — and also discuss a number of different ways to organize and use counters efficiently.
To begin with, let's define the functionality that we want to add to our program.
We want to add several counters per ingress port to track the following:
In addition to counting the number of packets we will also show how to count the number of bytes in them – useful when billing the customers for their internet traffic.
Overall, this example's program structure will be the same as in the previous example; here we'll concentrate mostly on adding the counters.
First, let us schematically show how we'd like to have our counters organized.

Figure 1. Conceptual counter organization[1]
It seems natural to organize the counters in a sort of two-dimensional array (matrix) with the rows corresponding to the port numbers and the columns corresponding to the various events (causes) that can be enumerated.
Here is one way to enumerate the packet types:
Table 1. Packet type enumeration
To implement the counters, we'll also need several primitives with the ability to:
XISA architecture provides specialized counter tables that offer both the persistent counter storage and the ability to increment counters atomically[2], a very important feature given the highly parallel nature of packet processing in XISA.
XISA Counter tables can be thought of as arrays of counter entries, accessible via a counter index, with the number of entries (aka rows) controlled by programmer. The entry format is also flexible: each entry can contain either a single value, or a pair of values and those values can be either 32 or 64-bit wide. Besides that, no special semantics are assigned. For example, it's up to the programmer to decide which value in a pair will contain the packet counter and which one will contain a byte counter. In fact, one can think of them not as specialized counter objects, but as generic registers that support store, add, subtract and even retrieve operations[3].
Based on the description above, we can easily imagine XISA counter tables as one-dimensional arrays, as depicted in Figure 2 below:

Figure 2. Single and double counters
There are two options we can use to implement a two-dimensional array, as depicted in Figure 1. We could create multiple counter arrays (one per column), but that option may be a bit heavy and does not scale well. A better way would be to linearize the two-dimensional array, converting it into a single-dimensional one by first placing the cells from the first row, then placing the cells from the next row, etc.
In this case, the index I, corresponding to the cell (the element) from row R and column C, can be calculated using a very simple formula I = R × N + C, where N is the number of elements in the row (i.e., the number of different counters we want to collect). This is a classic technique that is used to implement multi-dimensional arrays in traditional programming languages, such as C.
As a result, our algorithm will begin to look like this:

Figure 3. Collecting counters in a linearized counter array
A careful reader might have noticed by now that the counter table (let's call it Ingress Port Counter Table or IG_PORT_CTR for short) might be updated more than once per packet.
Indeed, if the packet is a valid one, the algorithm calls for updating the PACKET_TYPE_GOOD_IPV4 counter for the ingress port. Then, after completing the IPV4_LPM lookup, we'll need to update either the PACKET_TYPE_IPV4_LPM_HIT or PACKET_TYPE_IPV4_LPM_MISS counter for the same port based on the lookup outcome.
This might look like a problem to those used to working with pipeline-based programmable devices where it's very typical to allow accessing of any table, counter or similar resource, no more than once for a given packet[4]. In those systems, we would need to use two separate counter tables: one to count good and bad IPv4 packets and another one to count the packets that hit or missed the IPv4_LPM table.
Fortunately, XISA allows multiple accesses to any object, such that we can easily place all the counters in a single table.
Now let's explore the required microcode updates.
XISA processes packet switching first via the Parser, and then via the Match-Action Processor (MAP). Since we are not changing the packet format(s) processed by the program, and since the previously published Parser code already preloads the contents of the entire IPv4 header into the MAP registers, this program will not require any changes in the parser microcode.
The first thing we need to do is to define a new counter table that can be used to count the packets. These and all the other specialized tables are defined using two special definition files, xdefs.json and xdefs_tables.json. The former is used to define the layout of the table entries and various constants, such as table IDs, while the second is used to define the details of the table placement inside the device.
We'll choose a number between 0 and 31 as the ingress port counter table ID (e.g., 4) and add a corresponding record to the "enumerator" section of the xdefs.json file:
Figure 4. Defining the table ID for the ingress port counter table
{ "name": "ig_port_ctr_table", "values": [ {"name": "id", "value": 4} ]},Next, let's define the layout of the entries. As always, they consist of a key and data.
Since the port numbers in the X2 device are 8 bits wide, and since we defined 8 counter categories, the key can be 11-bits-wide. We can add its definition to the "struct" section of xdefs.json.
Figure 5. Defining the entry key format for the ingress port counter table
{ "name": "IgPortCtrKey", "description": "Ingress Port Counter Entry Key (port * N + type)", "fields": [ { "type": "BitField", "name": "ctr_index", "size": 11, "value": 0 } ]},We should also define the entry contents (the value), which is comprised of two 64-bit fields (counters), one for the packets and one for the bytes. The names of these fields can be arbitrary, but the order must match the program code:
Figure 6. Defining the entry value format for the ingress port counter table
{ "name": "IgPortCtrValue", "description": "Ingress Port Counter Table Entry Value", "fields": [ { "type": "BitField", "name": "packets", "size": 64, "value": 0 }, { "type": "BitField", "name": "bytes", "size": 64, "value": 0 } ]}After these preparations we can add the table definition to xdefs_table.json.
Figure 7. Defining the ingress port counter table
{ "name": "IG_PORT_CTR", "description": "Ingress Port Counter Table. The index is computed using the formula port_number * PACKET_TYPE_MAX + packet_type", "id": "IG_PORT_CTR_TABLE_ID", "type": "Counters", "key_size": 11, "size": 2048, "ctr_type": "CTR_TYPE_DOUBLE", "ctr_size": "CTR_SIZE_8_BYTES", "key": "IgPortCtrKey", "value": "IgPortCtrValue"}That's it! We've added a new table to our data plane program, and it can now be used in the microcode as well as accessed by the control plane APIs.
Before we can increment a counter corresponding to the given ingress port, we need to compute the base counter index. As discussed above and depicted in Figure 3, we need to multiply the ingress port number by the number of different counters we are collecting.
In our case, this is easy. Since the total number of counters we want to collect per port is equal to 8 – we can simply shift the port number by three bits and get the desired number.
The ingress port number is always available to the MAP programs in bits 7:0 of the standard metadata (SMD) that is automatically passed with each packet in register R7.0 as depicted in Figure 8.

Figure 8. XISA standard metadata (SMD)
Thus, we can easily shift that value using the SHLI (SHift Left Immediate) instruction, available to us in the XISA, and store it for future use, e.g. in a register R1.2 as depicted below:
Figure 9. Computing the ingress port counter index by shifting the ingress port number
S1 SHLI.CD R1.2, R7.0, 0, 8, 3However, what should we do if the number of counters is not a power of two?
In following the XISA documentation, you'll quickly realize that XISA does not offer a multiply instruction[5]. We can, of course simulate multiplication through a series of shifts, adds and subtracts, but that will require us to change this multiplication code every time we need to change the number of counters we collect.
Fortunately, there is a better way – we can precompute those values in the control plane! Remember that every time a packet is passed to the MAP, the R1 register is pre-populated with the user-defined port metadata and its values are specific to each port. We already used this mechanism to pass the VRF value in our previous programs. What if we use some of the remaining bits to pass the port number already multiplied by the desired constant?
We can easily do that – all we need to do is to change the definition of the PortMetadataValue structure in the xdefs.json file that defines the layout of the port metadata table entry.
Here's our new definition:
Figure 10. Defining port metadata layout
{ "name": "PortMetadataValue", "description": "Port Attr Table Value", "fields": [ { "type": "BitField", "name": "reserved", "size": 100, "value": 0 }, { "type": "BitField", "name": "vrf", "size": 12, "value": 0 }, { "type": "BitField", "name": "reserved_2", "size": 5, "value": 0 }, { "type": "BitField", "name": "ig_port_ctr_base", "size": 11, "value": 0 } ]},It corresponds to this register layout (Figure 11):

Figure 11. Port metadata register layout
All that's needed now is to populate the ig_port_ctr_base field in the control plane as shown in in the Python script in Figure 12 below:
Figure 12. Sample Python script initializing ig_port_counter_base
# Program Ingress Port Counter base in the Port Metadatafor port in all_ports: set_port_metadata(dev_id, port, PortMetadataValue(vrf=get_vrf(port), ig_port_ctr_base=port * PACKET_TYPE_MAX))Going forward, we'll use this method, since it is more general and is somewhat faster as well, not to mention that no code to calculate ig_port_counter_base will be required in the program.
We'll need some additional registers to hold the new variables. Given the relative simplicity of the program, we have plenty of registers available for the allocation. Table 2 lists what we'll be using:
Table 2. Additional register allocation
We are now ready to start coding in assembly!
First, let's start by initializing the registers that will hold the packet type and the packet count increment.
Figure 13. Register initialization
i1 MOVI R0.0, 0 # packet_type = PACKET_TYPE_GOOD_IPV4i2 MOVI.CD R5.1, 1 # ctr_delta_packets = 1Let's review the IPv4 header validation code that we wrote previously.
Figure 14. Original IPv4 validation code
v1 BRBTSTCLR R11.3, 1, not_ipv4_packetv2 CMPI R4.0, 28, 4, 4v3 BRINEQ ipv4_version_invalidv4 CMPI R4.0, 24, 5, 4v5 BRILT ipv4_ihl_invalidv6 SUBI.F R0.1, R4.2, 24, 8, 1v7 BRILE ipv4_ttl_too_smallv8 SYNC.N 2, bad_ipv4_checksumv9 ###### Fall through to handle valid IPv4 packetsh1 not_ipv4_packet:h2 ipv4_version_invalid:h3 ipv4_ihl_invalid:h4 bad_ipv4_checksum:h5 ipv4_ttl_too_small: …d1 drop_and_halt:d2 SYNCALL 255d3 DROP.H 0We see that the validation logic is concentrated in lines v1..v8.
Upon any error, the code jumps to one of the labels (lines h1..h5) and then falls through to perform packet dropping.
This means that we can leave the actual validation code intact and simply insert the code that will set the proper packet type and increment the counter just before dropping the packet.
Our new code will look like this:
Figure 15. Accounting for the IPv4 header validation results
v8 SYNC.N 2, bad_ipv4_checksumv* BRI ig_port_count # use PACKET_TYPE_GOOD_IPV4h1 not_ipv4_packet:c1 MOVI R0.0, 1 # packet_type = PACKET_TYPE_NON_IPV4c2 BRI ig_port_counth2 ipv4_version_invalid: MOVI R0.0, 2 # packet_type = PACKET_TYPE_NON_IPV4 BRI ig_port_counth3 ipv4_ihl_invalid: MOVI R0.0, 3 # packet_type = PACKET_TYPE_IPV4_BAD_IHL BRI ig_port_counth4 ipv4_ttl_too_small: MOVI R0.0, 4 # packet_type = PACKET_TYPE_IPV4_BAD_TTL BRI ig_port_counth5 bad_ipv4_checksum: MOVI R0.0, 5 # packet_type = PACKET_TYPE_IPV4_BAD_CHKSUMc3 ig_port_count:c4 ADD.SH R0.2, R1.3, 0, 11, R0.0, 0, 11c5 COUNTER RN, R5, R0.2, 4, 8, 2c6 CMPI R0.0, 0, 0, 4c7 BRINEQ drop_and_haltv9 ###### Fall through to handle valid IPv4 packetsd1 drop_and_halt:d2 SYNCALL 255d3 DROP.H 0Let us discuss it section by section.
First, we can see that the code after each label, corresponding to various failed checks has now been populated by two instructions:
We could have followed the same model and added the code used to increment the counters for the packets that hit or missed the IPv4 LPM table. However, we will demonstrate a slightly different coding style that will allow us to slightly speed up the most important case, i.e. the one where we forward the packets.
Figure 16. Accounting for the IPv4 LPM lookup results
5 SYNC.N 32, ipv4_lpm_miss 6 AQMEG.LF3.NOMIRR R1.0, R3.3m1 ADDI.SX.SH R0.2, R1.3, 0, 11, 6m2 COUNTER RN, R5, R0.2, 4, 8, 2 7 MOVI R3.2, 0 8 MOVI.CD R0.3, 0 9 SYNC 810 BRBTSTSET R1.0, 0, aqm_drop11 SENDOUT.H R3, R0, 013 ipv4_lpm_miss:m3 ADDI.SX.SH R0.2, R1.3, 0, 11, 7m4 COUNTER RN, R5, R0.2, 4, 8, 2m5 BRI drop_and_haltSo far, we only counted the number of packets corresponding to various conditions (or packet types). While adding this capability has greatly enhanced the program, it would be very useful if we could also count the number of bytes in those packets.
To do that we need to know how long each packet is and the answer to this question is not simple. High-speed switches often start processing packets long before their length is known. This happens because the standard Ethernet-II header (used to carry most of the traffic in modern networks) lacks an explicit packet length indication. As a result, the packet length is determined only later, when the MAC finishes receiving the frame. In many cases, the MAP might already finish processing a jumbo packet, while the MAC continues receiving the tail of the same packet.
XISA devices provide the data plane programmer with two fundamental mechanisms to calculate the packet length:
We will use the first method to calculate the length of IPv4 packets that passed header validation (corresponding to GOOD_IPV4, IPV4_HIT and IPV4_MISS packet types) and we'll use the second method to calculate the length of all the packets that do not have a valid IPv4 header. Even though that method is slower, it should not affect our program performance, since we are going to use it only for the error cases and (by extension) only for the packets that we intend to drop.
To use the first method, we can use the IPv4 total_length field which is located in bits 15:0 of register R4.0 (see Figure 2 in the previous post). However, to know the full length of the packet, we need to know the offset of the IPv4 header from the beginning of the packet. This can be easily calculated if we remember that register R12 contains the offsets of the individual headers.
Here is how we can calculate the total length of a packet with the valid(ated) IPv4 header using a single instruction that can be added right after the code that checks the validity of IPv4 checksum:
Figure 17. Calculating packet length by using the IPv4 “total length” field
SYNC.N 2, bad_ipv4_checksumL1 ADD R5.3, R4.0, 0, 16, R12.0, 16, 8 BRI ig_port_countNote how powerful the ADD instruction is. It lets us add values of different widths — while IPv4 total length is a 16-bit field, the IPv4 header offset is only 8 bits wide!
Calculating the packet lengths without a valid IPv4 header is optional – if we do not do it, the program will just count the number of packets. This is another powerful feature of XISA architecture: even though we use pairs of counters we are under no obligation to increment both of them.
Nevertheless, let's demonstrate this code for completeness:
Figure 18. Calculating the size of a non-IPv4 packet
Q1 SIZEQUERY.LF0 R5.3Q2 SYNC 1This example demonstrates several new features and capabilities of the XISA architecture. We learned how to define a new table, calculate packet lengths and perform counter updates using special XISA instructions. We also learned that in XISA architecture counters can be defined and updated in a very flexible way, often exceeding the capabilities of the best pipeline-based architectures. Even more importantly we have shown how to easily modify an XISA data plane program to ensure that it can account for all packet drop scenarios.
XISA is capable of handling much more sophisticated networking tasks. Any standard routing and/or bridging feature, and more importantly, any other unique or customer-driven feature can be implemented thanks to the X-Switch's extreme flexibility and programmability.
For more information, contact us.
We, at Xsight Labs, believe this transparency will drive innovation and facilitate the development of future networking technologies.
Stay tuned for further insights into the capabilities of the X-Switch ISA and the exciting possibilities it unlocks.
This is the finished program, with every change from this article applied. The figures above are snapshots taken along the way, so assembling them in order will not produce a working program — use this listing.
ingress: CONCAT.CD R2.2, 0, R1.3, 16, 12 LKPLPM.LF5.R R3.3, R3.3, -1, -1, R2, R2, 0, 4i1 MOVI R0.0, 0i2 MOVI.CD R5.1, 1 BRBTSTCLR R11.3, 1, not_ipv4_packet CHKSUMTST.LF1 1 CMPI R4.0, 28, 4, 4 BRINEQ ipv4_version_invalid CMPI R4.0, 24, 5, 4 BRILT ipv4_ihl_invalid SUBI.F R0.1, R4.2, 24, 8, 1L1 BRILE ipv4_ttl_too_small SYNC.N 2, bad_ipv4_checksum ADD R5.3, R4.0, 0, 16, R12.0, 16, 8v* BRI ig_port_countnot_ipv4_packet:c1 MOVI R0.0, 1c2 BRI ig_port_count_badipv4_version_invalid: MOVI R0.0, 2 BRI ig_port_count_badipv4_ihl_invalid: MOVI R0.0, 3 BRI ig_port_count_badipv4_ttl_too_small:Q1 MOVI R0.0, 4Q2 BRI ig_port_count_badbad_ipv4_checksum: MOVI R0.0, 5ig_port_count_bad: SIZEQUERY.LF0 R5.3 SYNC 1c3 ig_port_count:c4 ADD.SH R0.2, R1.3, 0, 11, R0.0, 0, 11c5 COUNTER RN, R5, R0.2, 4, 8, 2c6 CMPI R0.0, 0, 0, 4c7 BRINEQ drop_and_halt STH.SYNC R0.1, 1, 8, 1 CHKSUMUPD.LF1 1 SYNC 2 SYNC.N 32, ipv4_lpm_miss AQMEG.LF3.NOMIRR R1.0, R3.3m1 ADDI.SX.SH R0.2, R1.3, 0, 11, 6m2 COUNTER RN, R5, R0.2, 4, 8, 2 MOVI R3.2, 0 MOVI.CD R0.3, 0 SYNC 8 BRBTSTSET R1.0, 0, aqm_drop SENDOUT.H R3, R0, 0m3 ipv4_lpm_miss:m4 ADDI.SX.SH R0.2, R1.3, 0, 11, 7m5 COUNTER RN, R5, R0.2, 4, 8, 2aqm_drop:drop_and_halt: SYNCALL 255 DROP.H 0host_eth:host_pss:exception:loopback:parser_congestion:trap: DROP.H 0Previously: IPv4 header validation and rewrite. Next: the finale, a network calculator that computes inside the switch.
XISA (the X-Switch Instruction Set Architecture) is Xsight Labs' open instruction set for programming packet processing on the X-Switch family, published under the Mozilla Public License version 2. Programs run across two stages: a Programmable Parser that identifies packet headers, and a Match-Action Processor (MAP) that performs lookups, edits and forwarding.