The Internet Protocol (IP) is the glue that holds an internet together. Here's a compact implementation of the IP layer for embedded C programmers.

As we've seen, the presence of a network interface like an Ethernet controller in a product makes it possible to send packets of data from one computer to another. To send the data, however, a destination hardware address must be attached to the packet before it is streamed out onto the network. ARP provides a mapping service between the hardware address of the destination computer and its IP address. So despite the fact that they are not part of the IP layer, both a network interface and ARP are required for network communications.

IP freely

The IP layer's main role is to provide for the routing of packets from one IP address to another. In other words, it can connect any machine on the Internet with any other, regardless of physical location. One of the communicating machines may be connected to an Ethernet LAN at a company in China, while the other could be a laptop in Maine that has just connected with a modem and a PPP link. Because of IP, their physical locations and underlying network interfaces don't matter. The IP layer software on each machine and similar software within routers and switches on the Internet backbone make this end-to-end communication possible.

The result of all this is a logical connection between two computers. This connection can be used to send and receive all kinds of data. For example, the two computers could exchange Web pages via HTTP over TCP, SNMP packets over UDP, telnet sessions, voice over IP, or any other type of data that can be sent in a stream of one or more digital packets.

Because so many types of data can be exchanged over IP, the Internet Protocol is full of features that are only used by a subset of connected hosts. In fact, the protocol itself is overdue for an upgrade from the current version, called IPv4, to the more capable IPv6. Since IPv6 is not yet widely deployed and even many of the IPv4 features are not necessary for a lot of applications, my implementation of the IP layer includes only a sort of bare-minimum of functionality to satisfy IPv4-style communications. Most commercial TCP/IP stacks for sale in the embedded systems marketplace are more complete.

To get a feel for what is and isn't supported, let's look at the data structures and constants defined in Listing 1. Of particular note here is that the NetIpHdr structure defines a header of a fixed size. In truth, the length of this header may be extended in 32-bit increments to support some less commonly used IP features. However, µC/Net never uses any of the special features that these extensions allow. So variable length IP headers are not generated by the stack and it will reject any incoming IP packets that have a header of length other than 20 bytes. The first field in any IP header, ver_hlen, gives the protocol version (4 for IPv4) in the first nibble and header length (five 32-bit words for the fixed size header) in the second nibble. All of the IP packets sent by µC/Net will, therefore, have this field set to the constant value 0x45. Similarly, it is a requirement that all packets received by the stack have that same value in ver_hlen.

/*
* IP Header
*/
typedef struct
{
uint8_t ver_hlen; /* Header version and length (dwords). */
uint8_t service; /* Service type. */
uint16_t length; /* Length of datagram (bytes). */
uint16_t ident; /* Unique packet identification. */
uint16_t fragment; /* Flags; Fragment offset. */
uint8_t timetolive; /* Packet time to live (in network). */
uint8_t protocol; /* Upper level protocol (UDP, TCP). */
uint16_t checksum; /* IP header checksum. */
uint32_t src_addr; /* Source IP address. */
uint32_t dest_addr; /* Destination IP address. */

} NetIpHdr;

#define IP_VER_HLEN 0x45
#define IP_HEADER_LEN sizeof(NetIpHdr)
#define IP_DATA_LEN (MTU - IP_HEADER_LEN)

/*
* IP Packet
*/
typedef struct
{
NetIpHdr ipHdr;
uint8_t data[IP_DATA_LEN];

} NetIpPkt;

Listing 1. Data structures and constants for the IP layer

In addition to this restriction, several other features of IP are either unsupported by µC/Net or the support is significantly scaled back. The only one of these that is likely to affect you is their lack of support for fragmentation and reassembly.

Fragmentation and Reassembly

Ethernet frames have a defined maximum size payload of 1,500 bytes each. This is termed the maximum transmission unit, or MTU, of the physical network. Included within that payload are the contents of the IP header, the UDP (or TCP) header, and the actual data. If all of the packets of data you want to send and receive fit within that maximum size-as is the case in the majority of embedded applications requiring only limited connectivity-µC/Net will do the job handily. If, however, you want to send even a single 1,501-byte frame, you'll run into trouble.

A more complete IP layer implementation would include a feature called fragmentation and reassembly. The idea that underlies this feature is simple: payloads too large to be sent over the physical network all at once should be split up across multiple frames (fragmented) and reassembled at the receiving end. The IP header field fragment can be used by the stacks on the two ends to coordinate this process.

Unfortunately, the implementation of fragmentation and reassembly is extremely cumbersome. Adding just this one feature to the IP layer would approximately double the amount of code (and code complexity) of the entire µC/Net stack! For that reason, I've made the decision not to support fragmentation and reassembly at all at this time. If the UDP layer attempts to send a packet that is too large, the IP layer will simply truncate the data to IP_DATA_LEN bytes and send that instead. If a remote computer fragments and then sends a large packet to a computer running µC/Net, the incoming data will appear to the client or server application as though it were actually multiple unrelated packets of information, rather than one really large one. (A poorly coded application could get confused by this, so beware.)

Sending and receiving

By now you must be wondering what IP features I have implemented. Ultimately, the IP layer is just responsible for sending and receiving IP packets. Listing 2 shows the code for NetIpSnd(), which does the sending. The IP packets resulting from this function call should make sense to any IPv4 system to which they are sent. That's the beauty of a more limited implementation of the protocol: it's small, easily coded and understood, and yet-because it implements a subset of the full protocol-can communicate with any system on the network.

int
NetIpSnd(NetIpPkt * pIpPkt, uint16_t len)
{
uint16_t ident;


/*
* Assign a unique ID to the outgoing packet.
*/
// Enter critical section here.
ident = gNextPacketID++;
// Leave critical section here.

/*
* Fill in the remaining IP header fields.
* (Some fields were pre-filled by the UDP layer.)
*/
pIpPkt->ipHdr.ver_hlen = IP_VER_HLEN;
pIpPkt->ipHdr.service = 0x00;
pIpPkt->ipHdr.length = htons(len);
pIpPkt->ipHdr.ident = htons(ident);
pIpPkt->ipHdr.fragment = 0x00;
pIpPkt->ipHdr.timetolive = 0x10;

/*
* Compute and fill in the IP header checksum.
*/
pIpPkt->ipHdr.checksum = NetIpChecksum((INT16U *) pIpPkt, IP_HEADER_LEN);

/*
* Forward the IP packet to the network interface driver.
*/
return (NetPhySnd(htons(PROTO_IP), (uint8_t *) pIpPkt, len));

} /* NetIpSnd() */

Listing 2. A function to send an IP packet

Some fields of the IP header are filled in by the UDP layer above. Specifically, the UDP layer fills in the IP header's source and destination IP addresses, which it combines with the UDP header and payload to compute a UDP checksum. The UDP sending function then passes the entire packet to NetIpSnd(). After filling in the remaining fields of the IP header, NetIpSnd() computes its own checksum(of the IP header contents only) and passes the completed IP packet on to the network driver, to be sent out over the physical network.

The NetIpRcv() function, shown in Listing 3, handles incoming IP packets. This function is called from within the context of a task that's part of the network driver. The driver task becomes active whenever a new packet arrives over the network. It then sees what type of packet it is (ARP, IP, and so on) and routes it to the appropriate function. NetIpRcv() processes all incoming IP packets.

int
NetIpRcv(NetIpPkt * pIpPkt)
{
uint16_t checksum;


/*
* Check IP header version and length.
*/
if (pIpPkt->ipHdr.ver_hlen != IP_VER_HLEN)
{
/*
* Unsupported header version or length.
*/
return (NET_ERROR);
}

/*
* Move the IP header checksum out of the header.
*/
checksum = pIpPkt->ipHdr.checksum;
pIpPkt->ipHdr.checksum = 0;

/*
* Compute checksum and compare with received value.
*/
if (checksum != NetIpChecksum((uint16_t *) pIpPkt, IP_HEADER_LEN))
{
return (NET_ERROR); //Bad checksum
}

/*
* Route the packet to the appropriate Layer 3 protocol.
*/
switch (pIpPkt->ipHdr.protocol)
{
case PROTO_UDP:
return (NetUdpRcv((NetUdpPkt *) pIpPkt, ntohs(pIpPkt->ipHdr.length)
- IP_HEADER_LEN));

#if defined (NET_TCP_EN)
case PROTO_TCP:
return (NetTcpRcv((NetTcpPkt *) pIpPkt, ntohs(pIpPkt->ipHdr.length)
- IP_HEADER_LEN));
#endif

default:
return (NET_ERROR); // Unsupported protocol
}

} /* NetIpRcv() */

Listing 3. A function to receive an IP packet

After checking the IP version, header length, and checksum, each incoming IP packet is routed to the layer above. If it is a UDP packet, NetUdpRcv() is called. If it is a TCP packet and TCP support is included, NetTcpRcv() is called instead.

0 comments