« SE4Binome2025-1 » : différence entre les versions
Aucun résumé des modifications |
|||
| (33 versions intermédiaires par 3 utilisateurs non affichées) | |||
| Ligne 1 : | Ligne 1 : | ||
= Shield Arduino = | = Shield Arduino = | ||
== Hardware == | == Hardware == | ||
Afin de tester notre futur carte réseau, nous avons fait un | Afin de tester notre futur carte réseau, nous avons fait un shield d'arduino qui prendra le rôle de la carte mère. | ||
=== Schématique === | === Schématique === | ||
Nous nous sommes évidemment | Nous nous sommes évidemment aidés de la datasheet de l'atmega328p ainsi que celle de l'arduino uno afin de sélectionner les bon pins. | ||
[[Fichier:Atmel-7810-Automotive-Microcontrollers-ATmega328P Datasheet.pdf|centré|vignette|Datasheet ATmega328p]] | [[Fichier:Atmel-7810-Automotive-Microcontrollers-ATmega328P Datasheet.pdf|centré|vignette|Datasheet ATmega328p]] | ||
[[Fichier:A000066-datasheet.pdf|centré|vignette|Datasheet Arduino Uno R3]] | [[Fichier:A000066-datasheet.pdf|centré|vignette|Datasheet Arduino Uno R3]] | ||
Plus précisément nous nous | Plus précisément nous nous sommes inspirés très fortement du schéma page 10 de la datasheet de l'arduino : | ||
[[Fichier:Screenshot 2025-11-07 11-08-46.png|centré|vignette|Schéma pinouts]] | [[Fichier:Screenshot 2025-11-07 11-08-46.png|centré|vignette|Schéma pinouts]] | ||
Nous sommes donc parti de l'exemple fourni par Mr Redon. Mais nous avons utilisé une template disponible dans kicad permettant de créer des | Nous sommes donc parti de l'exemple fourni par Mr Redon. Mais nous avons utilisé une template disponible dans kicad permettant de créer des shields d'arduino Uno R3.[[Fichier:Tyetyuy.png|vignette|Schématique shield arduino|centré|438x438px]] | ||
=== Routage === | === Routage === | ||
Au niveau du routage, nous avons tenté de faire simple (sans trop abuser | Au niveau du routage, nous avons tenté de faire simple (sans trop abuser des vias). Nous avons positionné le lecteur de carte SD au bord de la carte afin de ne pas avoir de problèmes avec d'autres composants lorsque nous voudrons mettre ou enlever la carte SD. | ||
[[Fichier:Routage shield arduino.png|vignette|Routage shield arduino|centré|434x434px]] | [[Fichier:Routage shield arduino.png|vignette|Routage shield arduino|centré|434x434px]] | ||
===Soudage=== | ===Soudage=== | ||
La schématique et le routage terminés, nous avons soudé les différents pins permettant de | La schématique et le routage terminés, nous avons soudé les différents pins permettant de connecter la carte shield à l'arduino, le reste n'étant pas forcément utile pour le moment (il est important de noter que notre shield est arrivé avec des composants pré-soudés). | ||
[[Fichier:Shield soudage de base .jpg|centré|vignette]] | [[Fichier:Shield soudage de base .jpg|centré|vignette]] | ||
===Tests=== | ===Tests=== | ||
====Leds==== | |||
Il faudra ensuite faire des test d'allumage des leds, afin de vérifier le bon fonctionnement du shield | Il faudra ensuite faire des test d'allumage des leds, afin de vérifier le bon fonctionnement du shield | ||
[[Fichier:VID leds.mp4|centré|vignette]] | [[Fichier:VID leds.mp4|centré|vignette]] | ||
Les leds | Les leds fonctionnent à merveille. | ||
====Carte SD==== | |||
Nous vérifions maintenant la carte SD (même si elle nous sera inutile pour notre projet). | |||
Nous utilisons le programme fourni par arduino qui permet de récupérer les informations de la carte SD. | |||
[[Fichier:Test sd card.png|centré|vignette]] | |||
La carte SD est bien identifié, le lecteur de carte fonctionne donc normalement. | |||
== Software == | == Software == | ||
=== Ordonnanceur basique === | |||
La première étape consiste à définir la structure d'un process, il sera composé : | |||
structure d'un process<syntaxhighlight lang="c"> | |||
*D'un pointeur de pile | |||
*De l'adresse de la fonction | |||
*D'un temps de sommeil | |||
*D'un état : 0 pour en cours et 1 pour terminé ou vide | |||
Ces deux derniers points nous serviront plus tard.<syntaxhighlight lang="c" line="1"> | |||
typedef struct { | typedef struct { | ||
uint16_t stackPointer; | uint16_t stackPointer; | ||
| Ligne 34 : | Ligne 52 : | ||
} process; | } process; | ||
</syntaxhighlight>tableau | </syntaxhighlight>On initialise donc notre tableau de processus de la façon suivante : <syntaxhighlight lang="c" line="1"> | ||
process Tasks_list[NBTASKS] = { | process Tasks_list[NBTASKS] = { | ||
{0, led1_blink, 0,0}, | |||
... | ... | ||
}; | }; | ||
</syntaxhighlight> | </syntaxhighlight>Concernant les fonction liées aux leds, nous utilisons une simple boucle while contenant un changement d'état. <syntaxhighlight lang="c" line="1"> | ||
void led_init() { | void led_init() { | ||
... | ... | ||
| Ligne 52 : | Ligne 71 : | ||
void led2_blink() { | void led2_blink() { | ||
... | ... | ||
</syntaxhighlight> | </syntaxhighlight>La fonction init_stackPointer_tasks() permet de diviser la pile "réel" en plusieurs piles de taille fixe pour chacun des processus. Ces sous piles commencent à la position FIRST_STACK_POSITION, afin de ne pas réécrire par dessus des données. Toutes les sous piles disposent d'une même taille de pile STACK_LENGTH. La fonction init_piles() initialise correctement la pile avec le tableau de processus si le processus est actif. Elle sauvegarde d'abord le pointeur de pile actuel (pas celui du processus) afin de revenir là où elle en était. Ensuite l'adresse de retour de la fonction (sur 16 bits) dans la pile de la tâche, sauvegarde les registres, puis restaure le pointeur de pile initial.<syntaxhighlight lang="c" line="1"> | ||
void init_stackPointer_tasks(int Cprocess) { //initialise le pointer de pile | void init_stackPointer_tasks(int Cprocess) { //initialise le pointer de pile | ||
if(Tasks_list[Cprocess].state == 0) { | if(Tasks_list[Cprocess].state == 0) { | ||
| Ligne 74 : | Ligne 93 : | ||
</syntaxhighlight>scheduler | </syntaxhighlight>Le scheduler permet de passer à la tâche suivante<syntaxhighlight lang="c" line="1"> | ||
void scheduler() { | void scheduler() { | ||
Current_task++; | Current_task++; | ||
| Ligne 81 : | Ligne 100 : | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight>L'atmega328p ne dispose pas de fonction en assembleur permettant de push ou de pop tout les registres, on doit donc en créer par nous mêmes. On défini alors les macros suivantes : <syntaxhighlight lang="c" line="1"> | ||
#define SAVE_REGISTERS() \ | #define SAVE_REGISTERS() \ | ||
asm volatile ( \ | asm volatile ( \ | ||
| Ligne 95 : | Ligne 114 : | ||
) | ) | ||
</syntaxhighlight>ISR<syntaxhighlight lang="c"> | </syntaxhighlight>L'ISR gère les interruptions, en commençant par sauvegarder l'état de la pile ainsi que le stack pointeur propre au processus qui s'interrompt. L'appel au schedruler permet de changer de processus pour cela il faut restaurer le stack pointer propre au nouveau processus actif et de la pile liée à ce processus. La dernière ligne arrête l'ISR.<syntaxhighlight lang="c" line="1"> | ||
ISR(TIMER1_COMPA_vect, ISR_NAKED) { | ISR(TIMER1_COMPA_vect, ISR_NAKED) { | ||
SAVE_REGISTERS(); | SAVE_REGISTERS(); | ||
| Ligne 105 : | Ligne 124 : | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==== | ==== Test ==== | ||
fonction wait<syntaxhighlight lang="c"> | Les processus de clignotement de leds ont tous un délai d'attente différent, l'interruption se produit toutes les millisecondes. On obtient ceci :[[Fichier:Ordonnanceur basique.mp4|centré|vignette]]Il est intéressant de remarqué que la vitesse à laquelle les leds changent d'état ne correspond pas au délai qui leur a été donnée. Cela parait logique puisque l'interruption met la fonction _delay_ms en pause, ainsi la vitesse se voit divisé par le nombre de processus actif. | ||
=== Ordonnanceur plus complexe === | |||
==== Ajout d'un système d'endormissement des processus ==== | |||
La fonction wait va permettre d'endormir un processus, elle va donné un temps de sommeil au processus puis lancer une interruption (car on veut changer de processus directement après que le précédent ait été endormi).<syntaxhighlight lang="c" line="1"> | |||
void wait(int time) { // met un process en veille pour une durée donné | void wait(int time) { // met un process en veille pour une durée donné | ||
// valeur max de time 32000 car int sur atmega328p code sur 16 bits donc 32000 max | // valeur max de time 32000 car int sur atmega328p code sur 16 bits donc 32000 max | ||
| Ligne 119 : | Ligne 143 : | ||
TIMER1_COMPA_vect(); | TIMER1_COMPA_vect(); | ||
</syntaxhighlight>scheduler | </syntaxhighlight>Pour vérifier si un processus a besoin d'être lancé, on ajoute une boucle while dans le scheduler afin de vérifier si il y a un temps de sommeil. Le scheduler va aussi servir à diminuer le temps de sommeil de tout les processus (avec un minimum de 0).<syntaxhighlight lang="c" line="1"> | ||
void scheduler() { | void scheduler() { | ||
for (int i=0 ; i<NBTASKS ; i++) { | for (int i=0 ; i<NBTASKS ; i++) { | ||
| Ligne 141 : | Ligne 165 : | ||
====Tableau dynamique des tâches==== | ====Tableau dynamique des tâches==== | ||
En premier lieu on va initialiser les état de tout les cases du tableau de processus. On choisit 1 la case est vide, 0 sinon.<syntaxhighlight lang="c" line="1"> | |||
void init_state() { //initialise state | void init_state() { //initialise state | ||
for (int i= 0; i<NBTASKS; i++) { | for (int i= 0; i<NBTASKS; i++) { | ||
| Ligne 152 : | Ligne 176 : | ||
} | } | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight>Nous avons programmé deux fonction d'arrêt et une fonction d'ajout de processus : | ||
* finish : stop le processus en cours et le supprime de tableau, puis lance une interruption | |||
* kill : stop un processus donné et le supprime du tableau | |||
* add : ajoute un processus sur la première case vide du tableau, initialise son pointeur de position et sa pile. | |||
Pour finish et kill, on met le temps de sommeil à 0 au cas où on arrête un processus endormi. C'est une mesure de sécurité pour finish, mais une obligation pour kill.<syntaxhighlight lang="c" line="1"> | |||
void finish() { //supprime un process lorsque celui-ci est terminé | void finish() { //supprime un process lorsque celui-ci est terminé | ||
cli(); | cli(); | ||
| Ligne 189 : | Ligne 223 : | ||
sei(); | sei(); | ||
} | |||
</syntaxhighlight>Il suffit maintenant de modifier le scheduler afin de ne pas prendre les processus terminé, puis utiliser add, finish et kill dans les fonctions choisies. | |||
==== Test ==== | |||
Pour tester les fonction add et finish, on réalise le test suivant : | |||
* on créer un process qui ajoute le process led2_blink (avec add) au bout d'un certain temps | |||
* le process led2_blink s'arrête (avec finish) au bout de 20 changement d'état (c'est à dire 10 clignotement). | |||
On obtient le résultat escompté qui est le suivant :[[Fichier:Demo fonction add et finish.mp4|centré|vignette]] | |||
====Lecture et écriture sur le port série==== | |||
Afin de lire et écrire sur le port série, on utilise les fonctions USART_Init, USART_Transmit et USART_Receive vu en cours de microP l'année précédente. Le but sera donc d'écrire le message qui vient d'être reçu, on utilise donc le processus suivant :<syntaxhighlight lang="c" line="1"> | |||
void Serial_Message(){ | |||
unsigned char data; | |||
while(1){ | |||
data = USART_Receive(); | |||
USART_Transmit(data); | |||
} | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==== | |||
[[Fichier: | ==== Test ==== | ||
==== | |||
===== | Pour tester notre processus de communication, on utilise minicom.[[Fichier:Test lecture et écriture port série.mp4|centré|vignette]]On notera que si la période entre les interruptions est trop élevé, certains caractères ne seront pas lus. | ||
[[Fichier: | |||
==== | ====Afficheur 7 segments==== | ||
===== | <syntaxhighlight lang="c" line="1"> | ||
void seven_seg() { | |||
while (1) { | |||
spi_activer((uint16_t)&PORTB, CS4); | |||
spi_echange(0x7E); | |||
spi_echange(0b1101111); | |||
spi_desactiver((uint16_t)&PORTB, CS4); | |||
wait(500); | |||
spi_activer((uint16_t)&PORTB, CS4); | |||
spi_echange(0x76); | |||
spi_desactiver((uint16_t)&PORTB, CS4); | |||
wait(500); | |||
} | |||
} | |||
</syntaxhighlight>Le processus seven_seg est ajouté au tableau des processus. Avant d'échanger en spi il faut activer la communication, et la désactiver après l'échange. La ligne 4 permet de sélectionner le 7 seg sur lequel on souhaite modifier l'affichage. La ligne 5 permet d'afficher le chiffre 9. Enfin la ligne 11 permet de clear le display. Voir tableau ci-dessous. | |||
[[Fichier:Tab 7 seg sparkfun.png|centré|vignette]] | |||
<syntaxhighlight lang="c" line="1"> | |||
void spi_init(void) { // Initialisation du bus SPI | |||
SPI_DDR |= (1<<SPI_MOSI)|(1<<SPI_SCK); // Définition des sorties | |||
SPI_DDR &= ~(1<<SPI_MISO); // Définition de l'entrée | |||
SPCR = (1<<SPE)|(1<<MSTR)|(1<<SPR1); // Activation SPI (SPE) en état maître (MSTR) | |||
// horloge F_CPU/64 (SPR1=1,SPR0=0) | |||
SPI_DDR |= (1<<CS1) | (1<<CS4);; | |||
DDRD |= (1<<CS5) | (1<<CS6); | |||
DDRC |= (1<<CS2) | (1<<CS3); | |||
} | |||
void spi_activer(uint16_t port, uint8_t cs) { // Activer le périphérique | |||
volatile uint8_t *reg = (volatile uint8_t*)port; | |||
*reg &= ~(1<<cs); // Ligne SS à l'état bas | |||
} | |||
void spi_desactiver(uint16_t port, uint8_t cs) { // Désactiver le périphérique | |||
volatile uint8_t *reg = (volatile uint8_t*)port; | |||
*reg |= (1<<cs); // Ligne SS à l'état haut | |||
} | |||
uint8_t spi_echange(uint8_t envoi) { // Communication sur le bus SPI | |||
SPDR = envoi; // Octet a envoyer | |||
while(!(SPSR & (1<<SPIF))); // Attente fin envoi (drapeau SPIF du statut) | |||
return SPDR; // Octet reçu | |||
} | |||
</syntaxhighlight>Afin de rendre nos fonctions les plus réutilisables possibles nous avons passer en paramètre le port ainsi que le CS qui correspond au slave select. Le type réel du port est volatile uint8* cependant le compilateur n'accepte que le uint16* ce qui explique le cast un peu étrange. | |||
=====Implémentation===== | |||
[[Fichier:Video7segvideo.mp4|vignette|centré]]L'implémentation correspond à nos attentes, seul un 7 seg affiche le chiffre 9. | |||
====Choix du processus sur le port série==== | |||
Afin de pouvoir démontrer le bon fonctionnement de la partie dynamique, on décide de pouvoir ajouter et tuer des processus à partir des touches du clavier. On utilisera donc les fonction de l'USART défini plus au au travers de la fonction suivante :<syntaxhighlight lang="c"> | |||
void call_process() { | |||
unsigned char data; | |||
while(1) { | |||
data = USART_Receive(); | |||
if(data == '1') { | |||
add(led1_blink); | |||
} | |||
... | |||
else if(data == '6') { | |||
kill(led1_blink); | |||
} | |||
... | |||
USART_Transmit(data); | |||
} | |||
} | |||
</syntaxhighlight>On remarquera que c'est une sorte d'amélioration de la fonction Serial_Message vu plus haut. | |||
Cette fonction aura la fonction de process de base, toujours présent dans l'ordonnaceur. | |||
==== Démonstation ==== | |||
[[Fichier:Process via USART.mp4|centré|vignette]] | |||
Ici on lance les processus ledN_blink avec les touches 1 à 5 et on les tues avec les touches 6 à 0. Pour le 7 segments, on l'ajoute avec s et le tue avec d. | |||
=Carte Réseau= | =Carte Réseau= | ||
Lors de la répartition des tâches, nous avons opté pour | Lors de la répartition des tâches, nous avons opté pour la carte réseau. | ||
==Description== | ==Description== | ||
Le sujet nous permettait de choisir parmi plusieurs microcontrôleurs. Nous avons retenu l’ATmega32U4, qui nous semble représenter un juste milieu entre l’ATmega16U2 et l’AT90USB. | Le sujet nous permettait de choisir parmi plusieurs microcontrôleurs. Nous avons retenu l’ATmega32U4, qui nous semble représenter un juste milieu entre l’ATmega16U2 et l’AT90USB. Cependant ce choix se révélera peu judicieux par la suite du projet. En effet la capacité de la mémoire est un point clé pour une carte réseau qui gère des paquets souvent de tailles conséquentes. | ||
==Hardware== | ==Hardware== | ||
Notre carte intègre des LED permettant d’afficher différents états de communication (que nous définirons ultérieurement). | Notre carte intègre des LED permettant d’afficher différents états de communication (que nous définirons ultérieurement). | ||
À la demande de M. Redon, nous avons ajouté un MAX232 ainsi que des connecteurs DB9 et DB25. Nous | À la demande de M. Redon, nous avons ajouté un MAX232 ainsi que des connecteurs DB9 et DB25. Nous savons que ces éléments pourront être utiles et servirons à utiliser la carte sur un terminal. | ||
=== | |||
[[Fichier:Schématique CReseau.png|vignette|540x540px|centré]] | En dehors de ça, la carte reste assez basique car son composant principal reste l'ATmega. | ||
===Schématique=== | |||
les broches PF n'étant pas utilisé, on les branches à un connecteur 6x1, notre carte pourra donc être utiliser pour d'autre projet si nécéssaire. | |||
On fait le choix d'utiliser 5 leds pour la connections, 1 led de power et 1 pour la connection ISP.[[Fichier:Schématique CReseau.png|vignette|540x540px|centré]] | |||
===Routage=== | ===Routage=== | ||
[[Fichier:Routage Creseau.png|vignette|521x521px|centré]] | [[Fichier:Routage Creseau.png|vignette|521x521px|centré]]Après avoir soudé les composants principaux, il est temps de passer à la partie programmation de la carte. | ||
==Programmation== | |||
Notre carte utilisant un connecteur USB, nous allons donc faire passer notre carte pour une carte réseau en utilisant la demo RNDIS de la LUFA. | |||
Il est important de noter que nous avons choisi un Atmega32u4. Ainsi des modification devrons être apporté à la taille des paquets ethernet et sur le choix des protocoles. | |||
===Protocoles impémentés=== | |||
La demo RNDISEthernet de la LUFA inclut les protocole suivant | |||
* ARP : Résolution d'adresses MAC. | |||
* IP : Gestion des paquets IPv4. | |||
* ICMP : Messages de contrôle (ping). | |||
* TCP : Communication fiable (exemple : serveur TCP). | |||
* UDP : Communication non fiable (exemple : serveur UDP). | |||
* DHCP : Attribution dynamique d'adresse IP. | |||
Aussi, on remarquera que la démo utilise un modèle en quatre couche : Le modèle TCP/IP. | |||
le sujet nous propose trois possibilité : | |||
* viser Ethernet | |||
* viser UDP | |||
* viser TCP | |||
En regardant ce qui à été fait les années précédentes, on a remarqué que les groupes partaient principalement sur la première option. Nous nous avons décidé de viser UDP. | |||
Ainsi, afin de d'alléger notre code pour le faire rentrer dans notre carte, nous avons choisi de ne pas inclure le protocole TCP et de modifier la taille max de paquets ethernet à 400 octets. | |||
Il nous restera donc de l'espace pour recevoir des paquets Ethernet :<syntaxhighlight> | |||
avr-size --mcu=atmega32u4 --format=avr RNDISEthernet.elf | |||
AVR Memory Usage | |||
---------------- | |||
Device: atmega32u4 | |||
Program: 9434 bytes (28.8% Full) | |||
(.text + .data + .bootloader) | |||
Data: 1037 bytes (40.5% Full) | |||
(.data + .bss + .noinit) | |||
</syntaxhighlight>Une fois la LUFA réduite et flashé, on donne une adresse IP à notre interface avec ip link eth2 up et ip address add 10.0.0.1/24 dev eth2. on test un ping sur l'adresse IP de la carte (10.0.0.2).[[Fichier:Ping creseau.mp4|centré|vignette]]Le premier test que nous décidons de faire, afin de vérifié notre compréhension du code de la LUFA, est de faire clignoter une LED si la carte reçoit un paquet ethernet. Pour cela on ajoute l'allumage de la LED dans la fonction EthernetTask du fichier principal (RNDISEthernet.c) :[[Fichier:Ping allume led.mov|vignette|La led s'éteint à chaque ping reçue|centré]] | |||
===Création du protocole=== | |||
En plus de pouvoir communiquer, notre carte dois être capable d'effectuer 3 fonction. | |||
On décide de créer notre propre protocole ProtocolAntique décrit ci-dessous : | |||
ProtocolAntique (256 octets) | |||
Entete du ProtocolAntique (3 octets) | |||
operation (1 octet) | |||
longueur (1 octet) | |||
numero (1 octet) | |||
Contenu du ProtocolAntique (256 - 3 = 253 octets) | |||
Exemples : | |||
LS : | |||
*Carte reseau : | |||
**operation = 0x01 // requete | |||
**longueur = [0-252] | |||
**numero = 0 // la requete n'est pas fragmentée | |||
**contenu // chemin ou effectuer le ls | |||
*Serveur : | |||
**operation = 0x11 // reponse au LS | |||
**longueur = [0-252] | |||
**numero = [0-255] // entre 1 et 255 => numero du paquet => reponse fragmentée, si 0 dernier paquet | |||
**contenu // nom de fichier ou rien si numero = 0 | |||
READ : | |||
Carte reseau : | |||
-operation = 0x02 // requete READ | |||
-longueur = [0-252] | |||
-numero = 0 // la requete n'est pas fragmentée | |||
-contenu // chemin/nom_fichier | |||
Serveur : | |||
-operation = 0x12 // reponse au READ | |||
-longueur = [0-252] | |||
-numero = [0-255] // entre 1 et 255 => numero du paquet => reponse fragmentée, si 0 dernier paquet | |||
-contenu // contenu d'une ligne ou rien si numero = 0 | |||
WRITE : | |||
Carte reseau : | |||
-operation = 0x03 // requete WRITE creation du fichier | |||
-longueur = [0-252] | |||
-numero = 0 // entre 1 et 255 => numero du paquet => reponse fragmentée, si 0 dernier paquet | |||
-contenu // Chemin pour ouvrir le fichier avce le nom du fichier : chemin/fichier | |||
Carte reseau : | |||
-operation = 0x13 // requete WRITE ecriture dans le fichier | |||
-longueur = [0-252] | |||
-numero = [1-252] // entre 1 et 255 => numero du paquet => reponse fragmentée, si 0 dernier paquet | |||
-contenu // ligne a ecrire | |||
Carte reseau : | |||
-operation = 0x23 // requete WRITE fin | |||
-longueur = [0-252] | |||
-numero = 0 // 0 dernier paquet | |||
-contenu // rien | |||
Comme nous avons choisit de viser UDP, le protocole doit pocédé un numéro de port, on choisit un numéro supérieur à 1023 pour ne pas devoir passer en root, Le port choisi est 3333. | |||
Dans le programme UDP.c on peut maintenant inclure notre protocole de la façon suivante :<syntaxhighlight lang="c"> | |||
switch (SwapEndian_16(UDPHeaderIN->DestinationPort)) | |||
{ | |||
/* | |||
case UDP_PORT_DHCP_REQUEST: | |||
RetSize = DHCP_ProcessDHCPPacket(IPHeaderInStart, | |||
&((uint8_t*)UDPHeaderInStart)[sizeof(UDP_Header_t)], | |||
&((uint8_t*)UDPHeaderOutStart)[sizeof(UDP_Header_t)]); | |||
break; | |||
*/ | |||
case UDP_PORT_PROTOCOL_ANTIQUE: | |||
RetSize = ProtocolAntique_ProcessProtocolAntiquePacket(IPHeaderInStart, | |||
&((uint8_t*)UDPHeaderInStart)[sizeof(UDP_Header_t)], | |||
&((uint8_t*)UDPHeaderOutStart)[sizeof(UDP_Header_t)]); | |||
break; | |||
</syntaxhighlight> | |||
=== Partie serveur === | |||
Lorsque la carte mère a besoin de faire une action (ls, read, write) sur l'ordinateur qui est connecté en USB via la carte réseau elle transmet son besoin à la carte réseau. Cette dernière créer un requête UDP qui encapsule notre protocole (ProtocolAntique) et la transmet donc au PC. Sur ce dernier un serveur doit donc fonctionner afin de recevoir les rêquetes de la carte réseau, les traiter et y répondre. Cette partie est donc consacrée au code ainsi qu'aux tests du serveur. | |||
==== Code du serveur ==== | |||
Pour pouvoir fonctionner correctement le serveur doit initialiser une socket UDP et ensuite doit lancer un fonctionne qui boucle indéfiniment afin de recevoir chaque message UDP. Cette partie n'a pas été codé par nous et est trouvable sur rex.plil.fr (Département IMA -> support de cours de la programmation réseau -> exemple de serveur 2/2). Il en est de même du code la fonction pour envoyer un message UDP qui est trouvable au même endroit section exemple de client 2/2. | |||
La carte réseau est toujours à l'initiative des requêtes, par conséquent le serveur commence par recevoir un requête et la traiter. Pour ce faire à chaque requête reçue par la fonction boucleServeurUDP la fonction traitementMessageUDP est appelée.<syntaxhighlight lang="c" line="1"> | |||
int traitementMessageUDP (unsigned char* message, int nboctets); | |||
</syntaxhighlight>Cette fonction s'occupe d'extraire la partie opération de l'entête de protocolAntique et d'appeler la fonction de traitement correspondante à l'opération | |||
{| class="wikitable" | |||
!Opération | |||
!Requête | |||
!Fonction appelée | |||
|- | |||
|0x01 | |||
|LS | |||
|void traitement_ls (unsigned char*); | |||
|- | |||
|0x02 | |||
|READ | |||
|void traitement_read (unsigned char*); | |||
|- | |||
|0x03 | |||
|WRITE (début) | |||
|void traitement_write (unsigned char*); | |||
|- | |||
|0x13 | |||
|WRITE (données et fin) | |||
|void traitement_write (unsigned char*); | |||
|} | |||
===== Traitement LS ===== | |||
La fonction traitement_ls commence par récupérer le chemin (relatif ou absolu) où effectuer le LS. Si la section longueur de l'entête de la requête est à 0 alors le chemin par défaut est celui depuis où le serveur à été lancé. Le code pour récupérer le chemin est le suivant :<syntaxhighlight lang="c" line="1"> | |||
unsigned char chemin[longueur+1]; | |||
for (int i=0 ; i<longueur ; i++) { | |||
chemin[i] = message[i+3]; // Recuperation du chemin | |||
} | |||
chemin[longueur] = '\0'; | |||
</syntaxhighlight>Par la suite le dossier est correspondant au chemin est ouvert via la fonction opendir puis chaque entrée du dossier est récupérée et engendre un message de réponse du serveur (un message par entrée).<syntaxhighlight lang="c" line="1"> | |||
while ((entree = readdir(dossier)) != NULL) { | |||
// Message UDP pour chaque entree | |||
strcpy(contenu, entree->d_name); | |||
printf(" %s ", contenu); | |||
reponse[OPERATION] = REPONSE_LS; | |||
reponse[LONGUEUR] = strlen(contenu); | |||
reponse[NUMERO] = num++; | |||
for (int i=0 ; i<reponse[LONGUEUR] ; i++) { | |||
reponse[i+3] = contenu[i]; | |||
} | |||
messageUDP(IP_CARTE, PORT_CARTE, (unsigned char *)reponse, (int)reponse[LONGUEUR] + LONGUEUR_ENTETE); | |||
} | |||
</syntaxhighlight>OPERATION est la premiére section et vaut donc 0, LONGUEUR 2 et NUMERO 3. OPERATION est une réponse à un LS donc 0x11, LONGUEUR dépend de l'entrée du dossier et NUMERO augmente de 1 à chaque entrée. Enfin on remplie la requête du nom de l'entrée et on envoie le message. | |||
Enfin une fois tous les entrées envoyées on envoie un dernier message UDP de type reponse_LS de longueur 0 et de numéro 0 afin d'indiquer que la transmission est terminée et ce correctement. | |||
===== Traitement READ ===== | |||
Cette requête est très proche de celle du LS. On commence par récupérer le chemin avec le nom du fichier de la même façon que pour le LS. Cependant ici si le message ne contient pas de chemin aucun traitement ne sera effectué. Pour ouvrir le fichier on utilise la fonction fopen. Chaque ligne, récupérée via fgets, engendre une réponse UDP formulée de la même façon que pour les entrées de dossier. Evidemment l'opération est de type réponse read soit 0x12. Enfin un message similaire de fin de transmission est envoyé seulement si il n'y a pas eu d'erreur et que la dernière ligne à été lue. | |||
===== Traitement WRITE ===== | |||
Cette requête est un peu plus complexe à gérer niveau serveur par conséquent que je vais commencer par décrire le principe de la fonction. | |||
* La carte réseau envoie un message de type début de write (OPERATION 0x03) avec le chemin et nom du fichier que le serveur va récupérer et mémoriser. | |||
* La carte réseau envoie un massage pour chaque ligne à écrite (OPERATION 0x13). Le serveur va vérifier si la section NUMERO des entêtes se suivent afin de s'assurer que aucun message n'est été perdu. | |||
* La carte réseau envoie un message de type fin de write (OPERATION 0x013) mais de numéro 0. Le serveur indique que tout s'est bien passé en envoyant une réponse au write (OPERATION 0x23) donc le contenu est 0x00. | |||
* Si un message a été perdu un message d'erreur est envoyé de la par du serveur (OPERATION 0x23) de contenu 0x01. | |||
La récupération du contenu de la requête est du même type que celle du LS. Les messages de retour sont construits de la même façon que pour le message de retour du LS. Enfin on utilise fopen une nouvelle fois pour ouvrir le fichier, fwrite pour y écrire et fclose pour le fermer. | |||
==== Préparation du test ==== | |||
Avant de pouvoir tester notre serveur avec la carte réseau (ce qui n'aura jamais été fait car la carte réseau n'envoie rien) nous avons souhaité validé le fonctionnement du serveur en simulant des requêtes de la carte réseau. On voit ce que le serveur fait via des printf et en créant un autre serveur (plus rudimentaire) on peut vérifier que le serveur répond correctement. Sur notre git le dossier testUDP est consacré à cette phase de tests. | |||
* Les requêtes simulées de la carte réseau sont envoyées sur l'IP loopback de la machine et sur le port du serveur (ici 4242) | |||
* Le serveur écoute le port serveur (4242) et répond en loopback sur le port du serveur qui simule la carte (ici 3333) | |||
* Le serveur qui simule la carte écoute le port 3333 | |||
On lance ces différents serveurs et les requêtes via les commandes du fichiers fonctionnement.txt de notre git. | |||
==== Tests ==== | |||
Le test est réalisé via 3 commandes : | |||
* LS sans chemin | |||
* WRITE de 3 lignes dans un fichier test.txt créé par la commande | |||
* READ du fichier test.txt | |||
[[Fichier:Test1.png|centré|vignette|Requêtes envoyées]] | |||
[[Fichier:Test2.png|centré|vignette|Traitement du serveur]] | |||
[[Fichier:Test3.png|centré|vignette|Réponse reçu du serveur]]On peut constater que les tests fonctionnent. En effet le serveur a envoyé 9 res ls (8 entrées + fin de transmissions), 1 res write (fin d'écriture correcte) et 6 res read (5 lignes + 1 fin de transmissions). | |||
Enfin on décide de tester la réception des paquets UDP, on fait à nouveau clignoter une led quand un paquet UDP et reçu. Ici on envoi la réponse d'un LS, d'où la reception de plusieurs paquets: | |||
[[Fichier:Test udp.mp4|centré|vignette]] | |||
===Envoi d'un paquet à partir de la carte=== | |||
On définit un paquet Ethernet complet en hexadécimal dans un tableau d'octets (packet), incluant tous les en-têtes (Ethernet, IP, UDP) et les données ( pour nous aider nous avons repris celui que créait le programme testUDP avec l'instruction LS . ) . | |||
memcpy copie ce paquet dans le buffer de sortie global FrameOUT qui est utilisé par le système pour envoyer des trames.<syntaxhighlight lang="c"> | |||
void SendRawEthernetPacket() { | |||
uint8_t packet[] = { | |||
0x00, 0x01, 0x00, 0x01, 0x00, 0x01, // Destination MAC | |||
0x02, 0x00, 0x02, 0x00, 0x02, 0x00, // Source MAC | |||
0x08, 0x00, // EtherType (IPv4) | |||
... | |||
}; | |||
memcpy(FrameOUT.FrameData, packet, sizeof(packet)); | |||
FrameOUT.FrameLength = sizeof(packet); | |||
... | |||
} | |||
</syntaxhighlight>Cette fonction est appeler dans le main de main du fichier RNDISEthernet.c. | |||
Pour vérifier la reception des paquets UDP, on va essayer de les capturer avec Wireshark sur notre interface :[[Fichier:Wireshark.png|centré|vignette]] | |||
On récupère bien les paquet dans leurs entièreté. | |||
==Conclusion== | |||
La partie shield et ordonnanceur est finie et testée. La carte réseau n'a pas pu être finie par manque de temps. En faisant allumer une LED lorsqu'elle reçoit une trame Ethernet, UDP, STMP nous avons constaté qu'elle reçoit correctement ces protocoles. Cependant lorsque que nous avons voulu modifier la LUFA pour qu'une LED en plus s'allume lorsque UDP contient protocole antique cela ne fonctionnait pas. La carte ne trouvait jamais le protocole Antique. La communication avec la carte mère (shield) n'a pas été faite par manque de temps (cependant la communication SPI est vérifiée à l'étape de l'ordonnanceur). Le serveur côté PC qui doit communiquer avec la carte réseau est fini et testé. | |||
Notre problème peut venir du fait que l'atmega32u4 n'a pas assez de mémoire. Il ne faut donc pas choisir ce micro contrôleur pour ce projet mais un avec plus de mémoire. | |||
Version actuelle datée du 27 janvier 2026 à 11:45
Shield Arduino
Hardware
Afin de tester notre futur carte réseau, nous avons fait un shield d'arduino qui prendra le rôle de la carte mère.
Schématique
Nous nous sommes évidemment aidés de la datasheet de l'atmega328p ainsi que celle de l'arduino uno afin de sélectionner les bon pins.
Plus précisément nous nous sommes inspirés très fortement du schéma page 10 de la datasheet de l'arduino :
Nous sommes donc parti de l'exemple fourni par Mr Redon. Mais nous avons utilisé une template disponible dans kicad permettant de créer des shields d'arduino Uno R3.
Routage
Au niveau du routage, nous avons tenté de faire simple (sans trop abuser des vias). Nous avons positionné le lecteur de carte SD au bord de la carte afin de ne pas avoir de problèmes avec d'autres composants lorsque nous voudrons mettre ou enlever la carte SD.
Soudage
La schématique et le routage terminés, nous avons soudé les différents pins permettant de connecter la carte shield à l'arduino, le reste n'étant pas forcément utile pour le moment (il est important de noter que notre shield est arrivé avec des composants pré-soudés).
Tests
Leds
Il faudra ensuite faire des test d'allumage des leds, afin de vérifier le bon fonctionnement du shield
Les leds fonctionnent à merveille.
Carte SD
Nous vérifions maintenant la carte SD (même si elle nous sera inutile pour notre projet).
Nous utilisons le programme fourni par arduino qui permet de récupérer les informations de la carte SD.
La carte SD est bien identifié, le lecteur de carte fonctionne donc normalement.
Software
Ordonnanceur basique
La première étape consiste à définir la structure d'un process, il sera composé :
- D'un pointeur de pile
- De l'adresse de la fonction
- D'un temps de sommeil
- D'un état : 0 pour en cours et 1 pour terminé ou vide
Ces deux derniers points nous serviront plus tard.
typedef struct {
uint16_t stackPointer;
void (*functionAddress)(void);
int sleep_time;
int state;
} process;
On initialise donc notre tableau de processus de la façon suivante :
process Tasks_list[NBTASKS] = {
{0, led1_blink, 0,0},
...
};
Concernant les fonction liées aux leds, nous utilisons une simple boucle while contenant un changement d'état.
void led_init() {
...
}
void led1_blink() {
while(1) {
PORTC ^= (1<<PLED1);
_delay_ms(100);
}
}
void led2_blink() {
...
La fonction init_stackPointer_tasks() permet de diviser la pile "réel" en plusieurs piles de taille fixe pour chacun des processus. Ces sous piles commencent à la position FIRST_STACK_POSITION, afin de ne pas réécrire par dessus des données. Toutes les sous piles disposent d'une même taille de pile STACK_LENGTH. La fonction init_piles() initialise correctement la pile avec le tableau de processus si le processus est actif. Elle sauvegarde d'abord le pointeur de pile actuel (pas celui du processus) afin de revenir là où elle en était. Ensuite l'adresse de retour de la fonction (sur 16 bits) dans la pile de la tâche, sauvegarde les registres, puis restaure le pointeur de pile initial.
void init_stackPointer_tasks(int Cprocess) { //initialise le pointer de pile
if(Tasks_list[Cprocess].state == 0) {
Tasks_list[Cprocess].stackPointer = FIRST_STACK_POSITION - (Cprocess * STACK_LENGTH);
}
}
void init_piles(int Cprocess){ //initilalise la pile
if(Tasks_list[Cprocess].state == 0) {
int save = SP;
SP = Tasks_list[Cprocess].stackPointer;
uint16_t address = (uint16_t)Tasks_list[Cprocess].functionAddress;
asm volatile("push %0" : : "r" (address & 0x00ff) );
asm volatile("push %0" : : "r" ((address & 0xff00)>>8) );
SAVE_REGISTERS();
Tasks_list[Cprocess].stackPointer = SP;
SP = save;
}
}
Le scheduler permet de passer à la tâche suivante
void scheduler() {
Current_task++;
if (Current_task >= NBTASKS) {
Current_task = 0;
}
L'atmega328p ne dispose pas de fonction en assembleur permettant de push ou de pop tout les registres, on doit donc en créer par nous mêmes. On défini alors les macros suivantes :
#define SAVE_REGISTERS() \
asm volatile ( \
"push r0 \n\t" \
...\
)
#define RESTORE_REGISTERS() \
asm volatile ( \
"pop r31 \n\t" \
...\
)
L'ISR gère les interruptions, en commençant par sauvegarder l'état de la pile ainsi que le stack pointeur propre au processus qui s'interrompt. L'appel au schedruler permet de changer de processus pour cela il faut restaurer le stack pointer propre au nouveau processus actif et de la pile liée à ce processus. La dernière ligne arrête l'ISR.
ISR(TIMER1_COMPA_vect, ISR_NAKED) {
SAVE_REGISTERS();
Tasks_list[Current_task].stackPointer = SP;
scheduler();
SP = Tasks_list[Current_task].stackPointer;
RESTORE_REGISTERS();
asm volatile("reti");
}
Test
Les processus de clignotement de leds ont tous un délai d'attente différent, l'interruption se produit toutes les millisecondes. On obtient ceci :
Il est intéressant de remarqué que la vitesse à laquelle les leds changent d'état ne correspond pas au délai qui leur a été donnée. Cela parait logique puisque l'interruption met la fonction _delay_ms en pause, ainsi la vitesse se voit divisé par le nombre de processus actif.
Ordonnanceur plus complexe
Ajout d'un système d'endormissement des processus
La fonction wait va permettre d'endormir un processus, elle va donné un temps de sommeil au processus puis lancer une interruption (car on veut changer de processus directement après que le précédent ait été endormi).
void wait(int time) { // met un process en veille pour une durée donné
// valeur max de time 32000 car int sur atmega328p code sur 16 bits donc 32000 max
cli();
Tasks_list[Current_task].sleep_time = time;
TCNT1=0;
sei();
TIMER1_COMPA_vect();
Pour vérifier si un processus a besoin d'être lancé, on ajoute une boucle while dans le scheduler afin de vérifier si il y a un temps de sommeil. Le scheduler va aussi servir à diminuer le temps de sommeil de tout les processus (avec un minimum de 0).
void scheduler() {
for (int i=0 ; i<NBTASKS ; i++) {
if (Tasks_list[i].sleep_time - SLEEP_DEC < 0) {
Tasks_list[i].sleep_time = 0;
} else {
Tasks_list[i].sleep_time -= SLEEP_DEC;
}
}
do {
Current_task++;
if (Current_task >= NBTASKS) {
Current_task = 0;
}
} while (Tasks_list[Current_task].sleep_time > 0);
}
Tableau dynamique des tâches
En premier lieu on va initialiser les état de tout les cases du tableau de processus. On choisit 1 la case est vide, 0 sinon.
void init_state() { //initialise state
for (int i= 0; i<NBTASKS; i++) {
if (Tasks_list[i].functionAddress == NULL) {
Tasks_list[i].state = 1;
}
else {
Tasks_list[i].state = 0;
}
}
}
Nous avons programmé deux fonction d'arrêt et une fonction d'ajout de processus :
- finish : stop le processus en cours et le supprime de tableau, puis lance une interruption
- kill : stop un processus donné et le supprime du tableau
- add : ajoute un processus sur la première case vide du tableau, initialise son pointeur de position et sa pile.
Pour finish et kill, on met le temps de sommeil à 0 au cas où on arrête un processus endormi. C'est une mesure de sécurité pour finish, mais une obligation pour kill.
void finish() { //supprime un process lorsque celui-ci est terminé
cli();
Tasks_list[Current_task].state =1;
Tasks_list[Current_task].sleep_time = 0;
Tasks_list[Current_task].functionAddress = NULL;
TCNT1=0;
sei();
TIMER1_COMPA_vect();
}
void kill(void (*function)(void)) { //tue le process passé en paramètre
cli();
for(int i = 0; i<NBTASKS; i++) {
if(Tasks_list[i].functionAddress == function) {
Tasks_list[i].state =1;
Tasks_list[i].sleep_time = 0;
Tasks_list[i].functionAddress = NULL;
}
}
sei();
}
void add(void (*newFunction)(void)) { //ajoute un process
cli();
int i =0;
while(Tasks_list[i].state != 1) {
i++;
}
Tasks_list[i].functionAddress = newFunction;
Tasks_list[i].state = 0;
init_stackPointer_tasks(i);
init_piles(i);
sei();
}
Il suffit maintenant de modifier le scheduler afin de ne pas prendre les processus terminé, puis utiliser add, finish et kill dans les fonctions choisies.
Test
Pour tester les fonction add et finish, on réalise le test suivant :
- on créer un process qui ajoute le process led2_blink (avec add) au bout d'un certain temps
- le process led2_blink s'arrête (avec finish) au bout de 20 changement d'état (c'est à dire 10 clignotement).
On obtient le résultat escompté qui est le suivant :
Lecture et écriture sur le port série
Afin de lire et écrire sur le port série, on utilise les fonctions USART_Init, USART_Transmit et USART_Receive vu en cours de microP l'année précédente. Le but sera donc d'écrire le message qui vient d'être reçu, on utilise donc le processus suivant :
void Serial_Message(){
unsigned char data;
while(1){
data = USART_Receive();
USART_Transmit(data);
}
}
Test
Pour tester notre processus de communication, on utilise minicom.
On notera que si la période entre les interruptions est trop élevé, certains caractères ne seront pas lus.
Afficheur 7 segments
void seven_seg() {
while (1) {
spi_activer((uint16_t)&PORTB, CS4);
spi_echange(0x7E);
spi_echange(0b1101111);
spi_desactiver((uint16_t)&PORTB, CS4);
wait(500);
spi_activer((uint16_t)&PORTB, CS4);
spi_echange(0x76);
spi_desactiver((uint16_t)&PORTB, CS4);
wait(500);
}
}
Le processus seven_seg est ajouté au tableau des processus. Avant d'échanger en spi il faut activer la communication, et la désactiver après l'échange. La ligne 4 permet de sélectionner le 7 seg sur lequel on souhaite modifier l'affichage. La ligne 5 permet d'afficher le chiffre 9. Enfin la ligne 11 permet de clear le display. Voir tableau ci-dessous.
void spi_init(void) { // Initialisation du bus SPI
SPI_DDR |= (1<<SPI_MOSI)|(1<<SPI_SCK); // Définition des sorties
SPI_DDR &= ~(1<<SPI_MISO); // Définition de l'entrée
SPCR = (1<<SPE)|(1<<MSTR)|(1<<SPR1); // Activation SPI (SPE) en état maître (MSTR)
// horloge F_CPU/64 (SPR1=1,SPR0=0)
SPI_DDR |= (1<<CS1) | (1<<CS4);;
DDRD |= (1<<CS5) | (1<<CS6);
DDRC |= (1<<CS2) | (1<<CS3);
}
void spi_activer(uint16_t port, uint8_t cs) { // Activer le périphérique
volatile uint8_t *reg = (volatile uint8_t*)port;
*reg &= ~(1<<cs); // Ligne SS à l'état bas
}
void spi_desactiver(uint16_t port, uint8_t cs) { // Désactiver le périphérique
volatile uint8_t *reg = (volatile uint8_t*)port;
*reg |= (1<<cs); // Ligne SS à l'état haut
}
uint8_t spi_echange(uint8_t envoi) { // Communication sur le bus SPI
SPDR = envoi; // Octet a envoyer
while(!(SPSR & (1<<SPIF))); // Attente fin envoi (drapeau SPIF du statut)
return SPDR; // Octet reçu
}
Afin de rendre nos fonctions les plus réutilisables possibles nous avons passer en paramètre le port ainsi que le CS qui correspond au slave select. Le type réel du port est volatile uint8* cependant le compilateur n'accepte que le uint16* ce qui explique le cast un peu étrange.
Implémentation
L'implémentation correspond à nos attentes, seul un 7 seg affiche le chiffre 9.
Choix du processus sur le port série
Afin de pouvoir démontrer le bon fonctionnement de la partie dynamique, on décide de pouvoir ajouter et tuer des processus à partir des touches du clavier. On utilisera donc les fonction de l'USART défini plus au au travers de la fonction suivante :
void call_process() {
unsigned char data;
while(1) {
data = USART_Receive();
if(data == '1') {
add(led1_blink);
}
...
else if(data == '6') {
kill(led1_blink);
}
...
USART_Transmit(data);
}
}
On remarquera que c'est une sorte d'amélioration de la fonction Serial_Message vu plus haut.
Cette fonction aura la fonction de process de base, toujours présent dans l'ordonnaceur.
Démonstation
Ici on lance les processus ledN_blink avec les touches 1 à 5 et on les tues avec les touches 6 à 0. Pour le 7 segments, on l'ajoute avec s et le tue avec d.
Carte Réseau
Lors de la répartition des tâches, nous avons opté pour la carte réseau.
Description
Le sujet nous permettait de choisir parmi plusieurs microcontrôleurs. Nous avons retenu l’ATmega32U4, qui nous semble représenter un juste milieu entre l’ATmega16U2 et l’AT90USB. Cependant ce choix se révélera peu judicieux par la suite du projet. En effet la capacité de la mémoire est un point clé pour une carte réseau qui gère des paquets souvent de tailles conséquentes.
Hardware
Notre carte intègre des LED permettant d’afficher différents états de communication (que nous définirons ultérieurement).
À la demande de M. Redon, nous avons ajouté un MAX232 ainsi que des connecteurs DB9 et DB25. Nous savons que ces éléments pourront être utiles et servirons à utiliser la carte sur un terminal.
En dehors de ça, la carte reste assez basique car son composant principal reste l'ATmega.
Schématique
les broches PF n'étant pas utilisé, on les branches à un connecteur 6x1, notre carte pourra donc être utiliser pour d'autre projet si nécéssaire.
On fait le choix d'utiliser 5 leds pour la connections, 1 led de power et 1 pour la connection ISP.
Routage
Après avoir soudé les composants principaux, il est temps de passer à la partie programmation de la carte.
Programmation
Notre carte utilisant un connecteur USB, nous allons donc faire passer notre carte pour une carte réseau en utilisant la demo RNDIS de la LUFA.
Il est important de noter que nous avons choisi un Atmega32u4. Ainsi des modification devrons être apporté à la taille des paquets ethernet et sur le choix des protocoles.
Protocoles impémentés
La demo RNDISEthernet de la LUFA inclut les protocole suivant
- ARP : Résolution d'adresses MAC.
- IP : Gestion des paquets IPv4.
- ICMP : Messages de contrôle (ping).
- TCP : Communication fiable (exemple : serveur TCP).
- UDP : Communication non fiable (exemple : serveur UDP).
- DHCP : Attribution dynamique d'adresse IP.
Aussi, on remarquera que la démo utilise un modèle en quatre couche : Le modèle TCP/IP.
le sujet nous propose trois possibilité :
- viser Ethernet
- viser UDP
- viser TCP
En regardant ce qui à été fait les années précédentes, on a remarqué que les groupes partaient principalement sur la première option. Nous nous avons décidé de viser UDP.
Ainsi, afin de d'alléger notre code pour le faire rentrer dans notre carte, nous avons choisi de ne pas inclure le protocole TCP et de modifier la taille max de paquets ethernet à 400 octets.
Il nous restera donc de l'espace pour recevoir des paquets Ethernet :
avr-size --mcu=atmega32u4 --format=avr RNDISEthernet.elf
AVR Memory Usage
----------------
Device: atmega32u4
Program: 9434 bytes (28.8% Full)
(.text + .data + .bootloader)
Data: 1037 bytes (40.5% Full)
(.data + .bss + .noinit)Une fois la LUFA réduite et flashé, on donne une adresse IP à notre interface avec ip link eth2 up et ip address add 10.0.0.1/24 dev eth2. on test un ping sur l'adresse IP de la carte (10.0.0.2).
Le premier test que nous décidons de faire, afin de vérifié notre compréhension du code de la LUFA, est de faire clignoter une LED si la carte reçoit un paquet ethernet. Pour cela on ajoute l'allumage de la LED dans la fonction EthernetTask du fichier principal (RNDISEthernet.c) :
Création du protocole
En plus de pouvoir communiquer, notre carte dois être capable d'effectuer 3 fonction.
On décide de créer notre propre protocole ProtocolAntique décrit ci-dessous :
ProtocolAntique (256 octets)
Entete du ProtocolAntique (3 octets)
operation (1 octet)
longueur (1 octet)
numero (1 octet)
Contenu du ProtocolAntique (256 - 3 = 253 octets)
Exemples :
LS :
- Carte reseau :
- operation = 0x01 // requete
- longueur = [0-252]
- numero = 0 // la requete n'est pas fragmentée
- contenu // chemin ou effectuer le ls
- Serveur :
- operation = 0x11 // reponse au LS
- longueur = [0-252]
- numero = [0-255] // entre 1 et 255 => numero du paquet => reponse fragmentée, si 0 dernier paquet
- contenu // nom de fichier ou rien si numero = 0
READ :
Carte reseau :
-operation = 0x02 // requete READ
-longueur = [0-252]
-numero = 0 // la requete n'est pas fragmentée
-contenu // chemin/nom_fichier
Serveur :
-operation = 0x12 // reponse au READ
-longueur = [0-252]
-numero = [0-255] // entre 1 et 255 => numero du paquet => reponse fragmentée, si 0 dernier paquet
-contenu // contenu d'une ligne ou rien si numero = 0
WRITE :
Carte reseau :
-operation = 0x03 // requete WRITE creation du fichier
-longueur = [0-252]
-numero = 0 // entre 1 et 255 => numero du paquet => reponse fragmentée, si 0 dernier paquet
-contenu // Chemin pour ouvrir le fichier avce le nom du fichier : chemin/fichier
Carte reseau :
-operation = 0x13 // requete WRITE ecriture dans le fichier
-longueur = [0-252]
-numero = [1-252] // entre 1 et 255 => numero du paquet => reponse fragmentée, si 0 dernier paquet
-contenu // ligne a ecrire
Carte reseau :
-operation = 0x23 // requete WRITE fin
-longueur = [0-252]
-numero = 0 // 0 dernier paquet
-contenu // rien
Comme nous avons choisit de viser UDP, le protocole doit pocédé un numéro de port, on choisit un numéro supérieur à 1023 pour ne pas devoir passer en root, Le port choisi est 3333.
Dans le programme UDP.c on peut maintenant inclure notre protocole de la façon suivante :
switch (SwapEndian_16(UDPHeaderIN->DestinationPort))
{
/*
case UDP_PORT_DHCP_REQUEST:
RetSize = DHCP_ProcessDHCPPacket(IPHeaderInStart,
&((uint8_t*)UDPHeaderInStart)[sizeof(UDP_Header_t)],
&((uint8_t*)UDPHeaderOutStart)[sizeof(UDP_Header_t)]);
break;
*/
case UDP_PORT_PROTOCOL_ANTIQUE:
RetSize = ProtocolAntique_ProcessProtocolAntiquePacket(IPHeaderInStart,
&((uint8_t*)UDPHeaderInStart)[sizeof(UDP_Header_t)],
&((uint8_t*)UDPHeaderOutStart)[sizeof(UDP_Header_t)]);
break;
Partie serveur
Lorsque la carte mère a besoin de faire une action (ls, read, write) sur l'ordinateur qui est connecté en USB via la carte réseau elle transmet son besoin à la carte réseau. Cette dernière créer un requête UDP qui encapsule notre protocole (ProtocolAntique) et la transmet donc au PC. Sur ce dernier un serveur doit donc fonctionner afin de recevoir les rêquetes de la carte réseau, les traiter et y répondre. Cette partie est donc consacrée au code ainsi qu'aux tests du serveur.
Code du serveur
Pour pouvoir fonctionner correctement le serveur doit initialiser une socket UDP et ensuite doit lancer un fonctionne qui boucle indéfiniment afin de recevoir chaque message UDP. Cette partie n'a pas été codé par nous et est trouvable sur rex.plil.fr (Département IMA -> support de cours de la programmation réseau -> exemple de serveur 2/2). Il en est de même du code la fonction pour envoyer un message UDP qui est trouvable au même endroit section exemple de client 2/2.
La carte réseau est toujours à l'initiative des requêtes, par conséquent le serveur commence par recevoir un requête et la traiter. Pour ce faire à chaque requête reçue par la fonction boucleServeurUDP la fonction traitementMessageUDP est appelée.
int traitementMessageUDP (unsigned char* message, int nboctets);
Cette fonction s'occupe d'extraire la partie opération de l'entête de protocolAntique et d'appeler la fonction de traitement correspondante à l'opération
| Opération | Requête | Fonction appelée |
|---|---|---|
| 0x01 | LS | void traitement_ls (unsigned char*); |
| 0x02 | READ | void traitement_read (unsigned char*); |
| 0x03 | WRITE (début) | void traitement_write (unsigned char*); |
| 0x13 | WRITE (données et fin) | void traitement_write (unsigned char*); |
Traitement LS
La fonction traitement_ls commence par récupérer le chemin (relatif ou absolu) où effectuer le LS. Si la section longueur de l'entête de la requête est à 0 alors le chemin par défaut est celui depuis où le serveur à été lancé. Le code pour récupérer le chemin est le suivant :
unsigned char chemin[longueur+1];
for (int i=0 ; i<longueur ; i++) {
chemin[i] = message[i+3]; // Recuperation du chemin
}
chemin[longueur] = '\0';
Par la suite le dossier est correspondant au chemin est ouvert via la fonction opendir puis chaque entrée du dossier est récupérée et engendre un message de réponse du serveur (un message par entrée).
while ((entree = readdir(dossier)) != NULL) {
// Message UDP pour chaque entree
strcpy(contenu, entree->d_name);
printf(" %s ", contenu);
reponse[OPERATION] = REPONSE_LS;
reponse[LONGUEUR] = strlen(contenu);
reponse[NUMERO] = num++;
for (int i=0 ; i<reponse[LONGUEUR] ; i++) {
reponse[i+3] = contenu[i];
}
messageUDP(IP_CARTE, PORT_CARTE, (unsigned char *)reponse, (int)reponse[LONGUEUR] + LONGUEUR_ENTETE);
}
OPERATION est la premiére section et vaut donc 0, LONGUEUR 2 et NUMERO 3. OPERATION est une réponse à un LS donc 0x11, LONGUEUR dépend de l'entrée du dossier et NUMERO augmente de 1 à chaque entrée. Enfin on remplie la requête du nom de l'entrée et on envoie le message.
Enfin une fois tous les entrées envoyées on envoie un dernier message UDP de type reponse_LS de longueur 0 et de numéro 0 afin d'indiquer que la transmission est terminée et ce correctement.
Traitement READ
Cette requête est très proche de celle du LS. On commence par récupérer le chemin avec le nom du fichier de la même façon que pour le LS. Cependant ici si le message ne contient pas de chemin aucun traitement ne sera effectué. Pour ouvrir le fichier on utilise la fonction fopen. Chaque ligne, récupérée via fgets, engendre une réponse UDP formulée de la même façon que pour les entrées de dossier. Evidemment l'opération est de type réponse read soit 0x12. Enfin un message similaire de fin de transmission est envoyé seulement si il n'y a pas eu d'erreur et que la dernière ligne à été lue.
Traitement WRITE
Cette requête est un peu plus complexe à gérer niveau serveur par conséquent que je vais commencer par décrire le principe de la fonction.
- La carte réseau envoie un message de type début de write (OPERATION 0x03) avec le chemin et nom du fichier que le serveur va récupérer et mémoriser.
- La carte réseau envoie un massage pour chaque ligne à écrite (OPERATION 0x13). Le serveur va vérifier si la section NUMERO des entêtes se suivent afin de s'assurer que aucun message n'est été perdu.
- La carte réseau envoie un message de type fin de write (OPERATION 0x013) mais de numéro 0. Le serveur indique que tout s'est bien passé en envoyant une réponse au write (OPERATION 0x23) donc le contenu est 0x00.
- Si un message a été perdu un message d'erreur est envoyé de la par du serveur (OPERATION 0x23) de contenu 0x01.
La récupération du contenu de la requête est du même type que celle du LS. Les messages de retour sont construits de la même façon que pour le message de retour du LS. Enfin on utilise fopen une nouvelle fois pour ouvrir le fichier, fwrite pour y écrire et fclose pour le fermer.
Préparation du test
Avant de pouvoir tester notre serveur avec la carte réseau (ce qui n'aura jamais été fait car la carte réseau n'envoie rien) nous avons souhaité validé le fonctionnement du serveur en simulant des requêtes de la carte réseau. On voit ce que le serveur fait via des printf et en créant un autre serveur (plus rudimentaire) on peut vérifier que le serveur répond correctement. Sur notre git le dossier testUDP est consacré à cette phase de tests.
- Les requêtes simulées de la carte réseau sont envoyées sur l'IP loopback de la machine et sur le port du serveur (ici 4242)
- Le serveur écoute le port serveur (4242) et répond en loopback sur le port du serveur qui simule la carte (ici 3333)
- Le serveur qui simule la carte écoute le port 3333
On lance ces différents serveurs et les requêtes via les commandes du fichiers fonctionnement.txt de notre git.
Tests
Le test est réalisé via 3 commandes :
- LS sans chemin
- WRITE de 3 lignes dans un fichier test.txt créé par la commande
- READ du fichier test.txt
On peut constater que les tests fonctionnent. En effet le serveur a envoyé 9 res ls (8 entrées + fin de transmissions), 1 res write (fin d'écriture correcte) et 6 res read (5 lignes + 1 fin de transmissions).
Enfin on décide de tester la réception des paquets UDP, on fait à nouveau clignoter une led quand un paquet UDP et reçu. Ici on envoi la réponse d'un LS, d'où la reception de plusieurs paquets:
Envoi d'un paquet à partir de la carte
On définit un paquet Ethernet complet en hexadécimal dans un tableau d'octets (packet), incluant tous les en-têtes (Ethernet, IP, UDP) et les données ( pour nous aider nous avons repris celui que créait le programme testUDP avec l'instruction LS . ) .
memcpy copie ce paquet dans le buffer de sortie global FrameOUT qui est utilisé par le système pour envoyer des trames.
void SendRawEthernetPacket() {
uint8_t packet[] = {
0x00, 0x01, 0x00, 0x01, 0x00, 0x01, // Destination MAC
0x02, 0x00, 0x02, 0x00, 0x02, 0x00, // Source MAC
0x08, 0x00, // EtherType (IPv4)
...
};
memcpy(FrameOUT.FrameData, packet, sizeof(packet));
FrameOUT.FrameLength = sizeof(packet);
...
}
Cette fonction est appeler dans le main de main du fichier RNDISEthernet.c. Pour vérifier la reception des paquets UDP, on va essayer de les capturer avec Wireshark sur notre interface :
On récupère bien les paquet dans leurs entièreté.
Conclusion
La partie shield et ordonnanceur est finie et testée. La carte réseau n'a pas pu être finie par manque de temps. En faisant allumer une LED lorsqu'elle reçoit une trame Ethernet, UDP, STMP nous avons constaté qu'elle reçoit correctement ces protocoles. Cependant lorsque que nous avons voulu modifier la LUFA pour qu'une LED en plus s'allume lorsque UDP contient protocole antique cela ne fonctionnait pas. La carte ne trouvait jamais le protocole Antique. La communication avec la carte mère (shield) n'a pas été faite par manque de temps (cependant la communication SPI est vérifiée à l'étape de l'ordonnanceur). Le serveur côté PC qui doit communiquer avec la carte réseau est fini et testé.
Notre problème peut venir du fait que l'atmega32u4 n'a pas assez de mémoire. Il ne faut donc pas choisir ce micro contrôleur pour ce projet mais un avec plus de mémoire.