
Checksum
Il checksum è un metodo per controllare l'integrità di un pacchetto, ovvero permette di stabilire se questo contiene errori (ed è quindi inutilizzabile).
Di per sé, la parola checksum non identifica un particolare algoritmo, ma quello utilizzato per IP ed altri proptocolli è
il complemento a 1 della somma dei complementi a 1 delle parole a 16bit dei dati su cui effettuare il calcolo; ovvero, si sommano tutte le parole a 16bit, sommando anche l'eventuale overflow ed infine si esegue il complemento a 1.
Prima è stato illustrato come il checksum, in fase di invio, venga calcolato e poi scritto nel pacchetto; ciò che non è ancora stato illustrato è il controllo del checksum all'arrivo di un pacchetto. Per verificare che il pacchetto sia integro, bisognerebbe salvare temporaneamente il campo checksum del pacchetto e mettere zero al suo posto, poi si esegue il calcolo e si confronta il risultato con quello salvato; se sono uguali, ovviamente, il pacchetto è corretto.
In realtà , per come è fatto l'algoritmo, la verifica è molto più semplice: infatti basta calcolare il checksum sul pacchetto senza modificarlo in alcun modo; se il risultato è zero, allora il pacchetto è OK, altrimenti è da scartare.
L'ENC28J60 integra, nel modulo DMA, una funzione abbastanza rapida per il calcolo del checksum, ed è ciò che utilizzeremo. Sfortunatamente, pero, nella revisione B5 del chip, tale funzione da dei grossi problemi alla ricezione dei pacchetti, quindi in questo caso il calcolo verrà eseguito via software.
Innanzitutto vediamo il prototipo della funzione DMAChecksum:
u16 DMAChecksum(u16 start, u16 len, BOOL rx)
il primo parametro indica l'indirizzo al quale iniziano i dati, len la lunghezza in byte di questi dati, infine rx serve ad indicare se l'indirizzo si riferisce alla memoria RX o TX. (L'indirizzo è un offset relativo, rispetto all'inizio del campo dati del pacchetto MAC)
Come dicevo prima, questo metodo ha due diverse implementazioni per le revisioni B1-B4 e B5, vediamo la prima:
u16 DMAChecksum(u16 start, u16 len, BOOL rx){ // calcola il checksum utilizzando l'apposita funzione del chip u16 tmp; u8 L,H; if (rx) { tmp = TX_BUF_START + 1 + sizeof(MAC_Header) + start; } else { tmp = packetStart + 6 + sizeof(MAC_Header) + start; if (tmp > RX_BUF_END) tmp = tmp - RX_BUF_END + RX_BUF_START - 1; } setBank(0); writeReg(EDMASTL, LOW(tmp)); writeReg(EDMASTH, HIGH(tmp)); tmp = tmp+len-1; // fine pacchetto if (!rx && tmp > RX_BUF_END) tmp = tmp - RX_BUF_END + RX_BUF_START - 1; writeReg(EDMANDL, LOW(tmp)); writeReg(EDMANDH, HIGH(tmp)); BFSReg(ECON1, 0b00110000); // inizio calcolo while(readETH(ECON1) & 0b00100000); // attende fine calcolo tmp = (u16)readETH(EDMACSL) << 8; tmp = tmp | readETH(EDMACSH); return tmp; // ritorna il checksum calcolato }
Prima di tutto, vengono calcolati e scritti negli appositi registri gli indirizzi assoluti di inizio e fine
dei dati su cui effettuare l'operazione. Poi viene avviato il calcono e se ne attende il termine.
Nel secondo caso, si esegue il calcolo via software, il che rende l'operazione molto più lenta:
u16 DMAChecksum(u16 start, u16 len, BOOL rx){ // calcola il checksum via software u16 tmp; u16 reg[2]; u32 sum; u16 len2; int i; if (rx) { tmp = TX_BUF_START + 1 + sizeof(MAC_Header) + start; } else { tmp = packetStart + 6 + sizeof(MAC_Header) + start; if (tmp > RX_BUF_END) tmp = tmp - RX_BUF_END + RX_BUF_START - 1; } // salva ERDPT setBank(0); reg[0] = readETH(ERDPTL); reg[1] = readETH(ERDPTH); writeReg(ERDPTL,LOW(tmp)); writeReg(ERDPTH,HIGH(tmp)); sum = 0; len2 = len & 0xFE; CS = 0; spiWrite(RBM); for (i=0; i<len2; i=i+2){ tmp = ((u16)spiRead()) << 8 | spiRead(); sum = sum + (u32) tmp; } if (len2!=len) sum += ((u32)spiRead()) << 8; // se il pacchetto ha lunghezza dispari CS = 1; while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16); tmp = ~sum; // ripristina ERDPT; setBank(0); writeReg(ERDPTL,reg[0]); writeReg(ERDPTH,reg[1]); return htons(tmp); // ritorna il checksum calcolato }
Prima di effettuare l'operazione, viene salvato il registro ERDPT (ripristinato alla fine), perché¨ verranno letti tutti i byte necessari al calcolo.
Vediamo anche il metodo putChecksum utilizzato in precedenza:
void putChecksum(u16 offset, u16 sum){ // scrive il checksum "sum" all'indirizzo "offset" all'interno del buffer di scrittura u16 tmp; u16 addr[2]; setBank(0); addr[0] = readETH(EWRPTL); // salva temporaneamente il puntatore addr[1] = readETH(EWRPTH); // di scrittura tmp = 1+sizeof(MAC_Header)+offset; // nuovo indirizzo writeReg(EWRPTL,LOW(TX_BUF_START+tmp)); writeReg(EWRPTH,HIGH(TX_BUF_START+tmp)); // carica il puntatore encPut(LOW(sum)); encPut(HIGH(sum)); // scrive il checksum writeReg(EWRPTL,addr[0]); writeReg(EWRPTH,addr[1]); // ripristina il vecchio puntatore }
ICMP
L'ICMP è un protocollo di servizio (descritto dalla RFC 792), utilizzato per effettuare controlli e segnalazioni all'interno
di una rete. Un pacchetto ICMP può trasportare diversi tipi di messaggi, ma quello più conosciuto e l'unico trattato qui, è la richiesta di Echo, conosciuto anche come Ping.
Nel modello ISO/OSI questo protocollo viene solitamente inserito al terzo livello, non essendo propriamente un protocollo di trasporto,
ma si trova comunque al di sopra del protocollo IP all'interno del quale viene incapsulato.
Struttura del pacchetto
La struttura di un pacchetto ICMP varia a seconda del messaggio trasportato, salvo un'intestazione comune a tutti i tipi.
Vediamo il caso particolare di una richiesta di eco:

- Type: assume il valore 8 per la richiesta, mentre nel pacchetto di risposta vale 0.
- Code: inutilizzato.
- Checksum: L'ormai noto campo di controllo. L'algoritmo utilizzato è lo stesso del protocollo IP.
- Identifier e Sequence Number: vengono generati dal richiedente per riconoscere la risposta. Vengono lasciati inalterati da chi risponde.
- Data: contiene un numero variabile di dati che vengono restituiti tali e quali nella risposta.
Il codice
Prima di tutto vediamo il contenuto del file icmp.h il quale contiene alcune define, ma soprattutto
la definizione del pacchetto ICMP.
#define ICMP_ECHO 8 #define ICMP_ECHO_REPLY 0 #define MAX_ICMP_DATA 32 // dimensione massima del campo dati typedef struct { u8 type; u8 code; u16 checksum; u16 id; u16 sn; u8 data[MAX_ICMP_DATA]; } ICMPPacket; void processICMP(IP_Header ipHeader);
Come si può vedere sono presenti i vari campi descritti nel paragrafo precedente.
Il file icmp.c contiene il codice che permette di rispondere ad un ping.
#include "stack.h" #define ICMP_Offset sizeof(IP_Header) void processICMP(IP_Header ipHeader){ ICMPPacket packet; u8 size; size = ipHeader.totalLength - (ipHeader.verlen & 0x0F)*4; if (size > sizeof(packet)) size = sizeof(packet); encGetArray((u8*)&packet, size); if (packet.type == ICMP_ECHO){ packet.type = ICMP_ECHO_REPLY; packet.checksum = 0; IPPutHeader(ipHeader.sourceIP, IPPROTO_ICMP, (u8*)&packet, size, size); putChecksum(ICMP_Offset+2,DMAChecksum(ICMP_Offset,size,TRUE)); MACSend(); } }
Attraverso il metodo encGetArray il pacchetto viene letto e salvato nella variabile packet. Poi, una volta
esaminato il campo type, se questo risulta essere una richiesta di eco, viene costruito il pacchetto di risposta:
il campo type viene settato a 0 (Echo Reply), il checksum azzerato e ricalcolato una volta che il pacchetto
viene scritto nel buffer; infine si invia il tutto con MACSend.
ProcessPacket
Perchè tutto funzioni, bisogna implementare un metodo che esamini i pacchetti in arrivo
e li mandi ai relativi gestori (IP e ARP per il terzo livello).
Questo metodo è processPacket ed è richiamato all'interno di un loop infinito nel metodo main:
void main() { encInit(); while (1) processPacket(); }
Vediamo il file stack.c
#include "stack.h" MACAddr remoteAddr; void processPacket(){ MAC_Header header; setBank(1); while (readETH(EPKTCNT)) { // se c'è almeno un pacchetto MACGetHeader(&header); if (header.type == TYPE_IP){ remoteAddr = header.sourceMAC; processIP(); } else if (header.type == TYPE_ARP) processARP(); freeRxSpace(); // libera lo spazio nel buffer RX } }
Leggendo il registro EPKCNT verifichiamo che ci sia almeno un pacchetto in attesa di essere letto, poi con MACGetHeader viene
letta l'intestazione MAC ed attraverso il campo type viene scelto se chiamare processIP o proccessARP (o anche nessuno dei due).
Questi metodi sono stati già trattati nelle pagine precedenti.
