Connectez vos applications embarquées à Internet
Acquisitions sur le terrain, télégestion, domotique… De nombreuses applications s’accommodent très bien d’une connexion à l’Internet. Qu’il s’agisse d’envoyer des mesures dans une base de données ou de piloter un système à distance: les idées ne manquent pas.
Je vous propose aujourd’hui une petite carte comprenant un microcontrôleur et l’interface réseau qui va lui permettre de dialoguer avec un modem.
Ce projet est rendu possible par Microchip qui propose un contrôleur réseau en boitier DIP28 simplement commandé par un bus SPI. Ce fameux composant se nomme ENC28J60. Il ne requiert que très peu de composants annexes: quelques condensateurs et résistances, un quartz à 25.000 MHz et, bien évidemment, une prise RJ45 et son transformateur de couplage.
Le schéma
Assez palabré, jetons un coup d’œil au schéma.
L’alimentation
J’ai choisi d’utiliser un PIC 18F qui fonctionne sous 5V, mais l’ENC fonctionne lui en 3.3V. Il y a donc deux régulateurs de tension: le premier fournit le 5V au micro et le second fournit le 3.3V à l’ENC. J’ai d’abord monté ces deux régulateurs en parallèle alimentés par le 12Vdc, mais pour limiter l’échauffement du LM317, je l’ai ensuite monté en cascade derrière le régulateur 5V (passant du même coup d’un classique 7805 à un PT78ST105 à découpage).
Il serait peut-être plus avantageux d’utiliser un PIC18LF alimenté directement en 3.3V. Mais vu la consommation de l’ENC (250 mA), il ne semble pas utilise de gratter quelques mA sur le PIC.
L’ENC28J60
L’ENC28J60 est un microcontrôleur, certes un peu particulier, mais qui a les mêmes besoins: une horloge, une alimentation découplée et un Reset.
Le quartz imposé par Microchip doit résonner à 25 MHz. C’est une fréquence courante pour les contrôleurs réseau: on peut donc récupérer de tels quartz dans de vieux routeurs, par exemple. Autrement, ils sont disponibles en neuf chez Conrad.
La connexion à la prise réseau doit obligatoirement passer par un transformateur de couplage. Là encore, il est possible d’en récupérer dans de vieux routeurs. Il existe aussi des prises réseaux intégrant le transformateur appelées « magjack ». C’est une prise de ce type que j’ai utilisé (récupérée dans une vielle Livebox Sagem).
La connexion avec le PIC passe par un bus SPI. Ce dernier transporte les informations sur deux fils (SDI et SDO) accompagnées d’un signal d’horloge (SCK). Il suffit donc de relier la broche SDO du PIC à la broche SI de l’ENC, SDI sur SO et SCK sur SCK. Il faut aussi compléter ce bus des deux lignes /RESET et /CS (chip select), qui permettent d’initialiser le composant et de le choisir (si le bus SPI est relié à d’autres périphériques).
Enfin, Une self permet d’isoler l’alimentation de la carte avec la liaison Ethernet (très bruitée).
ATTENTION: comme je l’ai déjà signalé, l’ENC fonctionne en 3.3V et le PIC en 5V. Dans le cas d’un PIC, les entrées du bus SPI sont dites à « trigger de Schmitt ». En gros, cela signifie qu’elles sont compatibles avec les signaux 3.3V. Pour d’autres microcontrôleurs, il peut être nécessaire d’ajouter un circuit d’adaptation de niveau sur les lignes SDI et /INT. Par contre les entrées de l’ENC sont compatibles TTL 5V. Il n’est donc pas nécessaire d’abaisser le niveau 5V à 3.3V avant d’entrer dans l’ENC.
Le Microcontrôleur
Je suis d’abord parti sur un PIC18F4550, bien connu, qui offre la possibilité d’utiliser le bootloader USB de l’Interface HID Universelle. Malheureusement, bien que milieu de gamme, ce contrôleur n’offre presque pas suffisamment de ressources pour utiliser une stack TCP/IP ou UDP/IP. J’ai donc migré vers un PIC18F46K80. Avec ses deux UART et son bus CAN, ce microcontrôleur est parfaitement adapté à la communication sur réseaux de terrain (je le réservais d’ailleurs à la réalisation d’un espion RS232 …). Il offre cependant 64 ko de flash et 3.6 ko de RAM (contre 32ko de flash et 2ko de RAM pour le 4550) et des convertisseurs A/N 12 bits (contre 10 bits pour le 4550).
Le PIC18F46K80 permet de programmer un mini serveur web ou d’implanter une petite stack TCP/IP pour envoyer des infos sur une base de données (par exemple).
ATTENTION: le 46K80 n’est pas entièrement compatible avec le 4550: les broches d’alimentations sont identiques, mais le bus SPI n’est pas à la même place. En outre, comme le module USB disparaît, la configuration de l’horloge diffère et il faut ajouter un connecteur ICSP (pas de bootloader pour le 46K80, sauf peut-être un en Ethernet …).
Overclocking
Pour les néophytes, l’overclocking est l’art de faire fonctionner un système informatique plus rapidement que ce que préconise le fabricant. Fort populaire sur PC, l’overclocking peut aussi s’appliquer à nos chers microcontrôleurs.
Dans le cas du 18F46K80, il s’agit surtout de conserver le quartz de 20MHz que j’avais mis pour le 4550. Le 46K80 offre une configuration d’horloge complètement différente du 4550. Il est notamment possible de configurer l’utilisation d’une horloge secondaire plus lente pour le mode basse consommation. Par contre, il ne propose qu’une seule PLL d’un coefficient de 4. On a donc le choix entre une horloge à 20MHZ ou une horloge à 80MHz. Je vous laisse imaginer quelle configuration j’utilise ;). Par contre, cette fréquence dépasse de 25% les spécifications constructeur, donc si votre micro a un comportement anormal, désactivez la PLL et contentez-vous d’une horloge à 20MHz.
NB: Il est bien connu qu’un microprocesseur de PC chauffe plus quand on l’overclocke. Un PIC n’est pas différent: si le 46K80 est froid à 20MHz, il tiédit légèrement à 80MHz (mais rien de comparable avec l’ENC28J60, qui lui tiédit franchement).
Utilisation
Une fois n’est pas coutume, j’utilise l’environnement MikroC pour programmer cette carte. En effet, MikroC offre une bibliothèque dédiée à l’utilisation de l’ENC28J60. Je vous invite à essayer leurs exemples, notamment leur serveur web.
Une version modifiée et améliorée de leur stack TCP/IP est disponible sur leur dépôt Libstock.
Voici un exemple de code :
#include "__NetEthEnc28j60.h" #include "recources.h" #define TMR0_PRE 34286 //une IT toutes les 200ms à 80MHz // mE ehternet NIC pinout sfr sbit Net_Ethernet_28j60_Rst at LATD3_bit; // for writing to output pin always use latch sfr sbit Net_Ethernet_28j60_CS at LATD2_bit; // for writing to output pin always use latch sfr sbit Net_Ethernet_28j60_Rst_Direction at TRISD3_bit; sfr sbit Net_Ethernet_28j60_CS_Direction at TRISD2_bit; // end ethernet NIC definitions const code unsigned char httpHeader[] = "HTTP/1.1 200 OK\nConnection: close"; // HTTP header const code unsigned char httpMimeTypeHTML[] = "\nContent-type: text/html\n\n"; // HTML MIME type const code unsigned char httpImage[] = "HTTP/1.1 200 OK\nConnection: keep-alive\nContent-type: image/jpg\n\n"; // TEXT MIME type unsigned char httpMethod[] = "GET /"; unsigned char httpRequ[] = "GET / HTTP/1.1"; unsigned char myMacAddr[6] = "ENC28"; // my MAC address unsigned char myIpAddr[4] = {0, 0, 0, 0}; // my IP address: 0.0.0.0 => utilise le DHCP pour obtenir une IP unsigned char gwIpAddr[4] = {192, 168, 1, 254}; // gateway (router) IP address unsigned char ipMask[4] = {255, 255, 255, 0 }; // network mask (for example : 255.255.255.0) unsigned char dnsIpAddr[4] = {192, 168, 1, 254}; // DNS server IP address unsigned char getRequest[15]; // HTTP request buffer unsigned char dyna[31] ; // buffer for dynamic response char led0status = 0; int nb200ms = 0; //////////////////////////////////////////////////////////////////////////////// // // User must set values of global variables in _Lib_NetEthEnc24j600_Defs.c file: !!!!! /* NUM_OF_SOCKET_28j60 = 7; // Max number of socket We can open. TCP_TX_SIZE_28j60 = 256; // Size of Tx buffer in RAM. MY_MSS_28j60 = 30; // Our maximum segment size. SYN_FIN_WAIT_28j60 = 1; // Wait-time (in second) on remote SYN/FIN segment. RETRANSMIT_WAIT_28j60 = 1; // Wait-time (in second) on ACK which we expect. */ //////////////////////////////////////////////////////////////////////////////// char sendHTML_mark = 0; SOCKET_28j60_Dsc *socketHTML; unsigned int pos[10]; // Initialization of Timer1 unsigned int cnt; void Net_Ethernet_28j60_UserTCP(SOCKET_28j60_Dsc *socket) { unsigned int len; char content_length[32]; asm CLRWDT; // I listen only to web request on port 80 for web server if(socket->destPort != 80) return ; // get 10 first bytes only of the request, the rest does not matter here for(len = 0; len < 10; len++){ getRequest[len] = Net_Ethernet_28j60_getByte(); } getRequest[len] = 0; // only GET method is supported here if(memcmp(getRequest, httpMethod, 5)&&(socket->state != 3)){ return; } if(memcmp(getRequest, httpRequ, 9)==0){ sendHTML_mark = 1; socketHTML = socket; } //............................................................................ // Send html page. if((sendHTML_mark == 1)&&(socketHTML == socket)) { if(pos[socket->ID]==0) { LATD.F1 = 1; // Send HTTP header. sprintf(content_length, "Content-Length: %d", strlen(html_code)); Net_Ethernet_28j60_putConstStringTCP(httpHeader, socket); Net_Ethernet_28j60_putStringTCP(content_length, socket); Net_Ethernet_28j60_putConstStringTCP(httpMimeTypeHTML, socket); Net_Ethernet_28j60_putConstStringTCP("<script>\n", socket); sprintf(dyna, "var kimoTemp=%2.1f\n",getTempKimo(KIMO_TEMP)); Net_Ethernet_28j60_putStringTCP(dyna, socket); sprintf(dyna, "var kimoHygro=%2.1f\n",getHygroKimo(KIMO_HYGRO)); Net_Ethernet_28j60_putStringTCP(dyna, socket); Net_Ethernet_28j60_putConstStringTCP("</script>\n", socket); } while(pos[socket->ID] < strlen(html_code)) { asm CLRWDT; if(Net_Ethernet_28j60_putByteTCP(html_code[pos[socket->ID]++], socket) == 0) { pos[socket->ID]--; break; } } if( Net_Ethernet_28j60_bufferEmptyTCP(socket) && (pos[socket->ID] >= strlen(html_code)) ) { Net_Ethernet_28j60_disconnectTCP(socket); LATD.F1 = 0; sendHTML_mark = 0; sendDATA_mark = 0; pos[socket->ID] = 0; } } } //////////////////////////////////////////////////////////////////////////////// int ReadSampleADC (char channel) { /* Fonction de lecture des voies Analogiques */ /* Les fonctions de MikroC ne semble pas s'accomoder des entrées différentielles */ ADCON0 = channel * 4; ADCON0.f0 = 1; ADCON0.f1 = 1; while(ADCON0.f1 == 1); ADCON0.f0 = 0; return (ADRESH*256 + ADRESL)%4096; } unsigned int Net_Ethernet_28j60_UserUDP(UDP_28j60_Dsc *udpDsc) { unsigned int len = 0; // my reply length // reply is made of the remote host IP address in human readable format ByteToStr(udpDsc->remoteIP[0], dyna); // first IP address byte dyna[3] = '.'; ByteToStr(udpDsc->remoteIP[1], dyna + 4); // second dyna[7] = '.'; ByteToStr(udpDsc->remoteIP[2], dyna + 8); // third dyna[11] = '.'; ByteToStr(udpDsc->remoteIP[3], dyna + 12); // fourth dyna[15] = ':'; // add separator // then remote host port number WordToStr(udpDsc->remotePort, dyna + 16); dyna[21] = '['; WordToStr(udpDsc->destPort, dyna + 22); dyna[27] = ']'; dyna[28] = 0; // the total length of the request is the length of the dynamic string plus the text of the request len = 28 + udpDsc->dataLength; // puts the dynamic string into the transmit buffer Net_Ethernet_28j60_putBytes(dyna, 28); // then puts the request string converted into upper char into the transmit buffer while(udpDsc->dataLength--) { Net_Ethernet_28j60_putByte(toupper(Net_Ethernet_28j60_getByte())); } return(len); } void interrupt (void) { if (INTCON.T0IF == 1) { TMR0H = TMR0_PRE / 256; TMR0L = TMR0_PRE % 256; nb200ms++; if (nb200ms%5 == 0) { nb200ms = 0; Net_Ethernet_28j60_UserTimerSec ++; } if(led0Status == 0) { LATD.f0 = 0; } else if(led0Status == 1) { LATD.f0 = 1; } else if(led0Status == 2) { LATD.f0 = !LATD.f0; } INTCON.T0IF = 0; } } void MCUInit() { ADCON1 = 0; ADCON2 = 0b10101010; ANCON0 = 0xff; CM1CON = 0; CM2CON = 0; CVRCON = 0; PORTA = 0 ; TRISA = 0xff ; // set PORTA as input for ADC PORTB = 0 ; TRISB = 0xff ; // set PORTB as input for buttons LATD = 0 ; TRISD = 0 ; // set PORTD as output INTCON.GIE = 1; T0CON = 0b10000110; TMR0H = TMR0_PRE / 256; TMR0L = TMR0_PRE % 256; INTCON.T0IE = 1; } void main() { char useDHCP = 0; int i = 0; MCUInit(); for(i = 0; i < NUM_OF_SOCKET_28j60; i++) pos[i] = 0; Net_Ethernet_28j60_stackInitTCP(); SPI1_Init(); SPI_Rd_Ptr = SPI1_Read; Net_Ethernet_28j60_Init(myMacAddr, myIpAddr, 1); // init ethernet board if(Net_Ethernet_28j60_getIpAddress()[0] == 0) useDHCP = 1; LATD.f1 = 0; led0Status = 2; asm CLRWDT; if(useDHCP == 1) { while(Net_Ethernet_28j60_initDHCP(10) == 0) // try to get one from DHCP until it works { Net_Ethernet_28j60_Init(myMacAddr, myIpAddr, 1); asm CLRWDT; } memcpy(myIpAddr, Net_Ethernet_28j60_getIpAddress(), 4) ; // get assigned IP address memcpy(ipMask, Net_Ethernet_28j60_getIpMask(), 4) ; // get assigned IP mask memcpy(gwIpAddr, Net_Ethernet_28j60_getGwIpAddress(), 4) ; // get assigned gateway IP address memcpy(dnsIpAddr, Net_Ethernet_28j60_getDnsIpAddress(), 4) ; // get assigned dns IP address } else { Net_Ethernet_28j60_confNetwork(ipMask, gwIpAddr, dnsIpAddr) ; // use configured IP address } led0Status = 1; while(1) { // Process incoming Ethernet packets Net_Ethernet_28j60_doPacket(); asm CLRWDT; for(i = 0; i < NUM_OF_SOCKET_28j60; i++) { if(socket_28j60[i].open == 0) pos[i] = 0; } if(Net_Ethernet_28j60_doDHCPLeaseTime()) Net_Ethernet_28j60_renewDHCP(5); // it's time to renew the IP address lease, with 5 secs for a reply } }
Les actions à effectuer dans l’ordre:
- Définir les paramètres réseau:
- Adresse IP
- Adresse passerelle
- Masque de réseau
- Initialiser le module SPI
- Initialiser le module Ethernet
- Configurer le réseau (ou demander une IP par DHCP)
- Définir les deux fonctions User_UDP et User_TCP qui sont appelées pour chaque requête reçue ou envoyée
- Appeler la fonction doPacket() dans la boucle principale
Et voilà, vous êtes prêt à créer un mini serveur web basse consommation.