Applied to Embedded Systems

You may be wondering how all this information on network protocols relates to embedded systems firmware. The answer is that, because I am building the entire interface, I must deal with incoming packets at the Ethernet level. The Ethernet device itself guarantees that only packets destined for its own MAC address are accepted. The driver firmware is then usually responsible for dropping all packets, excluding broadcasts, that are not addressed to the local IP address.

To write the driver code, you need to understand how to detect when an incoming packet has arrived and how to retrieve the packet from the Ethernet controller. Both operations are very specific to the device itself, so you need to refer to the device documentation. Generally, an Ethernet device looks like a FIFO or uses buffer descriptors where each descriptor set describes an incoming or outgoing buffer (or packet).

The buffer descriptor model is pretty common and quite easy to work with once you get the hang of it. Each device imposes its own definition of what a buffer descriptor looks like. The buffer descriptor typically contains a command/status field, a length field, a field containing a pointer to the actual buffer, and a field pointing to the next buffer descriptor. This definition implies a linked list of structures where each structure points to a buffer.

image from book
Figure 9.1: Ethernet Driver Buffer Descriptors.

It is common for Ethernet controllers to communicate with applications about the location of data using a linked chain of buffer descriptors. Notice that the descriptors form a circularly linked list.

Figure 9.1 shows a typical buffer descriptor and buffer configuration. The number of descriptors and buffers depends entirely on the application and the amount of memory space available in the target system. The Ethernet device is usually given a pointer to the first buffer descriptor in the list, and, when data transfer begins, the device assumes that the buffer descriptors have been properly initialized .

image from book
Using ˜C Structures to Access Peripherals

It is very common in C to interface to a peripheral device by creating a structure that looks like the device and then overlaying that structure onto the address space that is occupied by the device. When doing this, you must be aware of some code generation issues that can otherwise be taken for granted. When using structures in normal programs, the compiler/CPU combination determines pre cisely how the structure is mapped on to memory. When you write a function to access a member in this structure, you can be assured that the underlying code will access the proper member at the appropriate offset from the base of the struc ture. You dont really need to know exactly how each member in the structure is mapped onto memory.

When using a C structure as a clean way to interface to a peripheral, you cant take anything for granted. The peripheral has a fixed format onto which you are building a structure that must match the peripherals hardware. Suddenly, you must know exactly how the structure is formatted; otherwise, it will not properly align with the peripherals hardware.

Without going into the various reasons why, the compiler may add padding between members of the structure. This hidden padding of the structure can confuse access to the device because although the hardware and software struc tures appear to match, the unseen padding causes a mismatch. In most cases, the solution to this problem is simple awareness. You need to tell the compiler that you dont want it to insert any padding in the structure definition. You essentially force the compiler to not do something that it would otherwise consider natural. If you investigate the cross-compiler documentation, you are likely to find some some special syntax (often called pragmas ) that will direct the compiler to not insert any invisible padding into the relevant structure.

Listing 9.1: Initializing a Block of Buffer Descriptors.
image from book
 struct rdesc {     ulong   cmdstat;        /* Command/Status             */     uchar   *bp;            /* Pointer to buffer          */     ushort  bsize;          /* Size of buffer             */     ushort  psize;          /* Size of received packet    */     struct  rdesc  *ndp;    /* Pointer to next descriptor */ }; unsigned char RbufBase[BUF_SIZE * RX_DESCRIPTOR_COUNT]; struct  rdesc RdescBase[RX_DESCRIPTOR_COUNT]; for(i=0;i<RX_DESCRIPTOR_CNT;i++) {     RdescBase[i].cmdstat = DEVICE_IS_IDLE;     RdescBase[i].bsize = BUF_SIZE;     RdescBase[i].psize = 0;     RdescBase[i].bp = (uchar *)&RbufBase[i * BUF_SIZE];     if (i == RX_DESCRIPTOR_CNT-1) {         RdescBase[i].ndp = RdescBase;     }     else {         RdescBase[i].ndp = &RdescBase[i+1];     } } 
image from book
 
image from book
 

The example code in Listing 9.1 shows how one might initialize a block of buffer descriptors. The structure rdesc is used as the buffer descriptor and is declared to match the format of the descriptor as it would be defined by the device. One block of memory ( RbufBase ) is used for buffer allocation, and one block of memory ( RdescBase ) is used for buffer descriptor allocation. The for loop initializes the linked list. Note that this list is circular; the last item in the list points to the first.

The processPACKET() Function

In the case of packet reception , the firmware must retrieve the Ethernet payload and extract the packet type to determine if the packet is ARP or IP. If the packet is neither ARP nor IP, it is thrown away. Thats about all the Ethernet code does. Next, the packet is passed to either the IP or ARP packet-processing function. If the packet is ARP, the appropriate response is sent, and handling of the packet is complete. If the packet is IP, the software must further parse the header to determine which packet-processing subsection should process it. The interface handles UDP, DHCP/BOOTP, ICMP, and bits of TCP. So, based on the contents of the incoming packet, one of several different higher layer packet processors is called.

Listing 9.2: processPACKET() .
image from book
 /* processPACKET():  *  This is the top level of the message processing after a complete  *  packet has been received over ethernet.  It's all just a lot of  *  parsing to determine whether the message is for this board's IP  *  address (broadcast reception may be enabled), and the type of  *  incoming protocol.  Once that is determined, the packet is either  *  processed (TFTP, DHCP, ARP, ICMP-ECHO, etc...) or discarded.  */ void processPACKET(struct ether_header *ehdr, ushort size) {     int i;     ushort  *datap, udpport;     ulong   csum;     struct ip *ihdr;     struct Udphdr *uhdr;     if (ehdr->ether_type == htons(ETHERTYPE_ARP)) {         processARP(ehdr,size);         return;     }     else if (ehdr->ether_type != htons(ETHERTYPE_IP)) {         printPkt(ehdr,size,ETHER_INCOMING);         return;     }     /* If we are NOT in the middle of a DHCP or BOOTP transaction, then      * if destination MAC address is broadcast, return now.      */     if ((DHCPState == DHCPSTATE_NOTUSED) &&         (!memcmp((char *)&(ehdr->ether_dhost),BroadcastAddr,6))) {         return;     }     /* If source MAC address is this board, then assume we received our      * own outgoing broadcast message...      */     if (!memcmp((char *)&(ehdr->ether_shost),BinEnetAddr,6)) {         return;     } 
image from book
 

The processPACKET() function starts with Listing 9.2. The incoming parameters are the raw packet from the Ethernet driver and the size of the packet. The Ethernet header is tested immediately to see if the packet is ARP or IP. If the packet is ARP, execution branches to the ARP processing code. If the packet is some unrecognized protocol, it is printed to the console (as a diagnostic aid) before discarding it. The remaining code in this function assumes that the packet is IP. Any incoming broadcasts not associated with DHCP/BOOTP are dropped. Also, because the incoming packet might be the outgoing packet just sent by this target (depends on how the Ethernet interface device is configured), if the Ethernet source address of the incoming packet is the same as this targets address, the packet is dropped.

Listing 9.3: Verifying the Packet Integrity.
image from book
 ihdr = (struct ip *) (ehdr + 1);     /* If not version # 4, return now... */     if (getIP_V(ihdr->ip_vhl) != 4)         return;     /* IP address filtering:      * At this point, the only packets accepted are those destined for this      * board's IP address, plus, DHCP, if active.      */     if (memcmp((char *)&(ihdr->ip_dst),BinIpAddr,4)) {         if (DHCPState == DHCPSTATE_NOTUSED)             return;         if (ihdr->ip_p != IP_UDP)             return;         uhdr = (struct Udphdr *)(ihdr+1);         if (uhdr->uh_dport != htons(DhcpClientPort)) {             return;         }     }     /* Verify incoming IP header checksum...      */     csum = 0;     datap = (ushort *) ihdr;     for (i=0;i<(sizeof(struct ip)/sizeof(ushort));i++,datap++)         csum += *datap;     csum = (csum & 0xffff) + (csum >> 16);     if (csum != 0xffff) {         EtherIPERRCnt++;         return;     }          printPkt(ehdr,size,ETHER_INCOMING); 
image from book
 

The next phase is to overlay the IP structure (see Listing 9.3) onto the incoming packet and perform some validation. First, the code verifies that the packet conforms to IP, then that the incoming packet is addressed to the local IP address, and, finally, that the checksum matches the packet. If the packet passes all of this validation, I call printPkt() to display the packet ( printPkt() only prints if verbosity has been enabled).

Listing 9.4: Dispatching for Protocol-Specific Processing.
image from book
 if (ihdr->ip_p == IP_ICMP) {         processICMP(ehdr,size);         return;     }     else if (ihdr->ip_p == IP_TCP) {         processTCP(ehdr,size);         return;     }     else if (ihdr->ip_p != IP_UDP) {         int j;         SendICMPUnreachable(ehdr,ICMP_UNREACHABLE_PROTOCOL);         if (!(EtherVerbose & SHOW_INCOMING))             return;         for(j=0;protocols[j].pname;j++) {             if (ihdr->ip_p == protocols[j].pnum) {                 printf("%s not supported\n",                     protocols[j].pname);                 return;             }         }         printf("<%02x> protocol unrecognized\n", ihdr->ip_p);         return;     } 
image from book
 

The code in Listing 9.4 checks the IP packet type to determine how (if at all) to process the incoming IP packet. These checks are similar to the checks in Listing 9.2 for the incoming Ethernet frames . MicroMonitor supports some UDP, ICMP, and minimal TCP, so the logic calls processICMP() for incoming ICMP and processTCP() for incoming TCP. MicroMonitor then sends an ICMP Unreachable error response if the incoming packet is not UDP.

Listing 9.5: Processing UDP Packets.
image from book
 uhdr = (struct Udphdr *)(ihdr+1);     /* If non-zero, verify incoming UDP packet checksum...      */     if (uhdr->uh_sum) {         int     len;         struct  UdpPseudohdr    pseudohdr;         memcpy((char *)&pseudohdr.ip_src.s_addr,             (char *)&ihdr->ip_src.s_addr,4);         memcpy((char *)&pseudohdr.ip_dst.s_addr,             (char *)&ihdr->ip_dst.s_addr,4);         pseudohdr.zero = 0;         pseudohdr.proto = ihdr->ip_p;         pseudohdr.ulen = uhdr->uh_ulen;         csum = 0;         datap = (ushort *) &pseudohdr;         for (i=0;i<(sizeof(struct UdpPseudohdr)/sizeof(ushort));i++)             csum += *datap++;         /* If length is odd, pad and add one. */         len = ntohs(uhdr->uh_ulen);         if (len & 1) {             uchar   *ucp;             ucp = (uchar *)uhdr;             ucp[len] = 0;             len++;         }         len >>= 1;         datap = (ushort *) uhdr;         for (i=0;i<len;i++)             csum += *datap++;         csum = (csum & 0xffff) + (csum >> 16);         if (csum != 0xffff) {             EtherUDPERRCnt++;             return;         }     }     udpport = ntohs(uhdr->uh_dport);     if (udpport == MoncmdPort)         processMONCMD(ehdr,size);     else if (udpport == DhcpClientPort)         processDHCP(ehdr,size);     else if ((udpport == TftpPort)  (udpport == TftpSrcPort))         processTFTP(ehdr,size);     else {         if (EtherVerbose & SHOW_INCOMING) {             uchar *cp;             cp = (uchar *)&(ihdr->ip_src);             printf("  Unexpected IP pkt from %d.%d.%d.%d ",                 cp[0],cp[1],cp[2],cp[3]);             printf("(sport=0x%x,dport=0x%x)\n",                 ntohs(uhdr->uh_sport),ntohs(uhdr->uh_dport));         }         SendICMPUnreachable(ehdr,ICMP_UNREACHABLE_PORT);     } } 
image from book
 

The processing in Listing 9.5 is similar to that in Listing 9.3 but one level up in the protocol. This code overlays the UDP structure onto the IP payload and runs a checksum verification of the UDP header and pseudoheader . This checksum might not be necessary if the incoming packet has the checksum value set to zero. Finally, the handler processes the incoming UDP port number and branches to the appropriate code deeper in the MicroMonitor software (TFTP, DHCP, or MONCMD). If the port is not one that MicroMonitor supports, the handler returns an ICMP error response.

Note 

MONCMD is port 777 in the monitor. This port supports the ability to execute CLI commands over UDP.

In General

The implementation of the driver code is in C, and structures are overlaid onto the incoming packet to simplify the packet parsing. For example, the structure shown in Listing 9.6 could be used for the IP packet processing.

Listing 9.6 contains a few details of which you should be aware. First, depending on where the packet ends up in the memory space of the target, you need to consider different CPU alignment requirements. For example, some CPUs do not like to look at a two- or four-byte integer ( short or long , respectively) unless the integer is aligned on an even address. If the data is on an odd address and C code attempts to access it, the processor can throw an alignment exception . Be aware of this issue, and make adjustments where necessary. If you cant predict the alignment of your memory buffer, you have no choice but to copy the two- or four-byte datum to an aligned space before accessing it.

Listing 9.6: A Structure Declaration for the IP Header.
image from book
 struct ip_header {     uchar  ip_vhl;      /* version and header length */     uchar  ip_tos;      /* type of service           */     ushort ip_len;      /* length of packet          */     ushort ip_id;       /* identification            */     ushort ip_offset;   /* fragment offset field     */     uchar  ip_ttl;      /* time to live              */     uchar  ip_proto;    /* protocol                  */     ushort ip_csum;     /* checksum                  */     ulong  ip_source;   /* source IP address         */     ulong  ip_dest;     /* destination IP address    */ } 
image from book
 

An added complication is the fact that the CPU of the target might be big or little endian. All data on a network is transferred in network-byte order, which is big endian. If you are lucky enough to be running with a big-endian CPU, big-endian data is no issue; if the CPU is little endian, things like packet length, checksum, and other multi-byte quantities in the headers must be endian- reversed . The network-to-host long ( ntohl ) host-to-network long ( htonl ), network-to-host short ( ntohs ) and host-to-network short ( htons ) conversion macros perform this endian reversal. On a big-endian machine, these macros do nothing; on a little-endian machine, they perform the conversion.



Embedded Systems Firmware Demystified
Embedded Systems Firmware Demystified (With CD-ROM)
ISBN: 1578200997
EAN: 2147483647
Year: 2002
Pages: 118
Authors: Ed Sutter

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net