SE4Binome2024-4

De projets-se.plil.fr
Aller à la navigation Aller à la recherche

Lien du GIT : https://gitea.plil.fr/jwacquet/Pico_Binome4_Justin_Ibrahim.git

Shield Arduino

Projet Kicad : Fichier:Projet Kicad Shield.zip

Le shield va nous permettre de tester nos cartes filles sans la carte mère du Pico. Pour faire nos tests, le shield doit posséder un lecteur de carte SD pour tester la mémoire, des LEDs pour tester des processus et des ports He-10 pour y brancher des cartes filles

Carte Shield Soudé :

Carte shield soudée face


Petite erreur de conception, la carte a été routée à l'envers... Ce n'est pas catastrophique (un peu quand même) et c'est rattrapable en rajoutant des rallonges pour avoir assez d'espace pour brancher les He-10.

Maintenant que la carte est soudé nous devons tester les différents éléments de la carte.

Test LED :

Pour tester les Leds nous avons réalisé un programme sur arduino qui allume les Leds.


Test carte SD :

Nous avons placé la carte SD dans le lecteur puis nous avons utilisé le programme info-carte sur l'IDE Arduino pour vérifier si le lecteur est fonctionnel.

Capture d'écran de l'IDE Arduino lors du test du lecteur de carte SD


Test Port He-10 :

Pour tester les ports He-10, nous avons connecté un afficheur 7-segments pour vérifier qu'ils fonctionnent.


Malgré des difficultés à connecter les afficheurs 7-segments à cause du défaut de conception, les 5 ports HE-10 sont fonctionnels.

Ordonnanceur :

Ordonnanceur (No Naked):


Le principe de l'ordonnanceur est de simuler une exécution en parallèle des tâches.


Pour cela, on génère un minuteur qui va nous donner une durée d'exécution d'un programme avant que ce-dernier ne se fasse interrompre pour qu'une autre tâche s'exécute.

Lorsqu'une tâche précédemment interrompue se réexécute, elle reprend là où elle en était dans son processus.

Ici la fonction d'interruption ISR fait clignoter les LEDs lors d'une interruption, la fonction sei() active les interruptions.

#include <avr/io.h>
#include <avr/interrupt.h>

#define CTC1 WGM12

#define PERIODE         1000

ISR(TIMER1_COMPA_vect){    // Procédure d'interruption
int led1=(PORTC & 0x0f);
led1 >>= 1; if(led1==0) led1=0b00001001;
PORTC &= 0xf0; PORTC |= led1;

int led2=(PORTD & 0x0f);
led2 >>= 1; if(led2==0) led2=0b10010010;
PORTD &= 0xf0; PORTD |= led2;
}


void init_minuteur(int diviseur,long periode){
TCCR1A=0;               // Le mode choisi n'utilise pas ce registre
TCCR1B=(1<<CTC1);       // Réinitialisation du minuteur sur expiration
switch(diviseur){
  case    8: TCCR1B |= (1<<CS11); break;
  case   64: TCCR1B |= (1<<CS11 | 11<<CS10); break;
  case  256: TCCR1B |= (1<<CS12); break;
  case 1024: TCCR1B |= (1<<CS12 | 1<<CS10); break;
}
// Un cycle prend 1/F_CPU secondes.
// Un pas de compteur prend diviseur/F_CPU secondes.
// Pour une periode en millisecondes, il faut (periode/1000)/(diviseur/F_CPU) pas
// soit (periode*F_CPU)/(1000*diviseur)
OCR1A=F_CPU/1000*periode/diviseur;  // Calcul du pas
TCNT1=0;                // Compteur initialisé
TIMSK1=(1<<OCIE1A);     // Comparaison du compteur avec OCR1A
}

int main(){
    init_minuteur(1024,PERIODE);
    DDRC |= 0b00001001;
    DDRD |= 0b10010010;
    PORTC &= ~0b00001001;
    PORTD &= ~0b10010010;
    init_minuteur(256,PERIODE);
    sei();    // Autorisation des interruptions
    while(1);
}

Ordonnanceur Naked en Round-Robin ( Tourniquet en patois ) :

Maintenant, nous allons utiliser l'ISR en mode Naked, c'est-à-dire qu'il ne gère plus la sauvegarde et la restauration du contexte des tâches lors de leur interruption.

Pour commencer, nous avons écrit le bout de code qui sauvegarde le contexte sur la pile et celui qui le restaure.

#define portSAVE_REGISTER() \
asm volatile ( \
"push r0 \n\t" \
"in r0, __SREG__ \n\t" \
"push r0 \n\t" \
"push r1 \n\t" \
"push r2 \n\t" \
"push r3 \n\t" \
"push r4 \n\t" \
[...]
"push r31 \n\t" \
);


#define portRESTORE_REGISTER() \
asm volatile ( \
"pop r31 \n\t" \
"pop r30 \n\t" \
"pop r29 \n\t" \
[...]
"pop r1 \n\t" \
"pop r0 \n\t" \
"out __SREG__, r0 \n\t" \
"pop r0 \n\t" \
);


ISR(TIMER1_COMPA_vect,ISR_NAKED){    // Procédure d'interruption
  portSAVE_REGISTER();

  scheduler();

  portRESTORE_REGISTER();

  asm volatile ( "reti" );
}

Puis, nous avons créé une structure de processus, pour que chaque processus soit décrit par un pointeur de fonction, un StackPointer et un état (fini ou en cours).

Afin de pouvoir créer un tableau de processus en global. De plus, on initialise une variable globale "Cprocess" qui représente le processus courant.

typedef struct tab_process{
                uint16_t StackPointer;
                void (*process_add)(void);
              	uint8_t  state;
                }
                tab_process_t;

tab_process_t tab_process[NB_PROCESS];
uint8_t Cprocess=0;

Ensuite, nous avons séparé le programme de clignotement des LEDs en 5 programmes, 1 pour chaque LED. Et les programmes sont appelés par le scheduler chacun leur tour.

void Led1(void)
{
  while (1)
  {
    PORTC ^= 0b00000001;
    _delay_ms(100);
  }
}

void Led2(void)
{
  while (1)
  {
    PORTD ^= 0b00000010;
    _delay_ms(200);
  }
}

void Led3(void)
{
  while (1)
  {
    PORTC ^= 0b00001000;
    _delay_ms(300);
  }
}

void Led4(void)
{
  while (1)
  {
    PORTD ^= 0b00010000;
    _delay_ms(400);
  }
}

void Led5(void)
{
  while (1)
  {
    PORTD ^= 0b10000000;
    _delay_ms(500);
  }
}


void scheduler(void)
{
   if (Cprocess < NB_PROCESS-1)
     Cprocess+=1;
   else
     Cprocess=0;
}

Nous avons aussi fait une fonction d'initialisation des piles d'exécution des programmes qui seront lancés après la première interruption.

void InitialisationPile(int Cprocess){
    int save = SP;
    SP = tab_process[Cprocess].StackPointer;
    uint16_t address = (uint16_t)tab_process[Cprocess].process_add;
    asm volatile("push %0" : : "r" (address & 0x00ff) );
    asm volatile("push %0" : : "r" ((address & 0xff00)>>8) );
    portSAVE_REGISTER();
    tab_process[Cprocess].StackPointer = SP;
    SP = save;
}

Il ne reste plus qu'à faire le setup, associer un processus à un emplacement du tableau de processus; lui associer une adresse de StackPointer et un état, démarrer le minuteur, mettre en place les entrées/sorties , initialiser les piles puis activer les interruptions .

Finalement, on peut lancer la première tâche.

Avec la façon dont nous avons organisé le scheduler, l'ordonnanceur fonctionne en tourniquet.

void setup()
{
    tab_process[0].process_add = Led1;
    tab_process[1].process_add = Led2;
    tab_process[2].process_add = Led3;
    tab_process[3].process_add = Led4;
    tab_process[4].process_add = Led5;
    tab_process[0].StackPointer = 0x6ff;
    tab_process[1].StackPointer = 0x680;
    tab_process[2].StackPointer = 0x5ff;
    tab_process[3].StackPointer = 0x580;
    tab_process[4].StackPointer = 0x4ff;
    tab_process[0].state = 0;
    tab_process[1].state = 0;
    tab_process[2].state = 0;
    tab_process[3].state = 0;
    tab_process[4].state = 0;
    init_minuteur(1024,PERIODE);
    DDRC |= 0b00001001;
    DDRD |= 0b10010010;
    InitialisationPile(1);
    InitialisationPile(2);
    InitialisationPile(3);
    InitialisationPile(4);
    sei();    // Autorisation des interruptions
}



int main()
{
    setup();
    SP = tab_process[Cprocess].StackPointer;
    tab_process[Cprocess].process_add();
}
Video de démonstration de l'ordonnanceur en tourniquet

Ordonnanceur Naked avec fonction d'endormissement des tâches :

Maintenant, on veut pouvoir endormir des tâches pendant une durée définie. Ces tâches doivent être ignorées par le scheduler.

Pour cela, nous avons rajouté une durée d'endormissement dans la structure tab_process et un programme qui réduit le temps de sommeil à chaque interruption puis un programme qui endort la tâche ciblée.

typedef struct tab_process{
                uint16_t StackPointer;
                void (*process_add)(void);
              	uint8_t  state;
                uint16_t sleep;
                }
                tab_process_t;


#define TIME_INTERRUPT 20

void DecreaseSleep(void)
{
   int nb_sleep = 0;
   for (uint8_t i=1;i<NB_PROCESS-1;i++)
   {
     if (tab_process[i].sleep > 0)
      {
        tab_process[i].sleep -= TIME_INTERRUPT;
        nb_sleep++;
      }
   }
}

void setTimeDelay(uint16_t time, uint8_t process)
{
  tab_process[process].sleep=time;
}

Nous avons aussi modifié le scheduler :

void scheduler(void)
{
startScheduler:
   if (Cprocess < NB_PROCESS-1)
     Cprocess+=1;
   else
     Cprocess=1;
  if (tab_process[Cprocess].sleep > 0)
  {
//    scheduler();
	goto startScheduler;
  }
}

On a rajouté une condition au passage à la tâche suivante : si la prochaine tâche est endormie, on l'ignore et on regarde l'état de la prochaine jusqu'à avoir une tache éveillée.

On peut repérer un problème : que se passe-t-il si toutes les tâches sont endormies ? On pourrait faire confiance à l'utilisateur en priant pour qu'il n'endorme pas tout le monde mais nous avons préféré ne pas lui faire confiance.

Nous avons donc crée un processus qui est naturellement endormi mais qui se réveille lorsque tout le monde dort, pour éviter une boucle infinie.

void Anti_sleep_boucle(void)
{
  while(1);
}

// ajoute à setup
void setup()
{
    tab_process[NB_PROCESS-1].process_add = Anti_sleep_boucle;
    tab_process[NB_PROCESS-1].sleep = 1;
    tab_process[NB_PROCESS-1].StackPointer = 0x3ff;
    tab_process[NB_PROCESS-1].state = 1;
}
 // ajoute à DecreaseSleep 

if (nb_sleep ==  NB_PROCESS-2)
   {
     tab_process[NB_PROCESS-1].sleep = 0;
   }
   else
     tab_process[NB_PROCESS-1].sleep = 1;

On accorde les autres programmes à Anti_sleep_boucle, on a décidé qu'il sera le dernier processus par soucis de simplicité.


Bonus :

Nous avons amélioré le makefile donné par Mr Boé l'année dernière :

// modification pour un nettoyer plus efficace
clean:
	rm -f *.o
	rm -f *.hex
	rm -f *.elf
// modification pour pouvoir upload directement les modfications sur la carte
upload: $(TARGET).hex
	stty -F $(TERM) hupcl # reset
	avrdude -p atmega328p -c arduino -P /dev/ttyUSB0 \ -b 115200 -D -U flash:w:ordonnanceur.hex:i

Carte Réseau

Projet Kicad:Fichier:Kicad CReseau.zip

Conception

Nous allons réaliser la carte fille réseau. Nous avons décidé de partir sur la carte RNDIS, avec un AT90USB1287 car il a une plus grande capacité de stockage que l'AtMega32u4. La carte communiquera avec l'extérieur par mini USB et pourra être alimenté de deux manières différentes: Soit par la carte mère ou soit par le port USB directement grâce à un jumper. On a également ajouté des LEDs qui vont servir de témoins lorsqu'il y a communication.

Schématique de la carte réseau
Schéma du PCB



Bilan de puissance

Suite à une demande de la part de l'équipe Mère, nous allons effectuer un bilan de puissance de notre carte :

Le composant majeur de notre carte est le microprocesseur, l'AT90USB.

Carte d'écran du courant de l'AT90USB

Avec un tension d'entrée de 5V et une fréquence de 16MHz, le microprocesseur consomme : 0,030A x 5V =0,15 W.

Brasure

Le PCB de la Pipelette v1 est arrivée, on peut donc aller braser sans plus tarder.

PCB en manque de composants.


La brasure se passe bien sauf au moment où l'on a remarqué qu'on a confondu l'empreinte du jumper avec celle d'un connecteur de batterie... On a rattrapé l'erreur en brasant un interupteur à l'arrière de la carte.

Interrupteur remplacant le jumper à l'arrière.

Mise à part ce souci, tout est en ordre et la carte est brasée.

PCB plus en manque de composants.

Programmation de la carte

Cette section est dédiée à la programmation de la carte. Tout d'abord, en utilisant les codes RNDIS de la LUFA, notre carte est détectée comme on peut le voir avec un lsusb.

Lsusb qui détecte la carte.


Ensuite, on lui a attribué une adresse IP 10.0.0.1 qui est l'IP client défini dans l'exemple LUFA.

Enfin, avec un ip a, on remarque qu'elle a son interface usb0 et qu'elle est DOWN. Avec un ip link set usb0 up, la carte est prête à communiquer.

ip a où l'on voit la carte (interface usb0)

En effet elle est belle est bien prête à communiquer car on arrive à la ping comme le montre la vidéo.

Ping de la carte (et la LED qui clignote lorsqu'elle reçoit le ping)

Le ping fonctionne par usb donc on peut communiquer avec l'extérieur, maintenant il faut le faire aussi par port SPI pour communiquer avec la carte mère.

Vidéo test de communication par SPI
Pong

Pour cela, on a pris le code pong.c fourni par Mr Redon qui permet de tester la communication par SPI. Le premier essai s'avère être un échec. Le problème venait d'une erreur de conception du shield. On a eu le même problème que le binôme 7 donc on a fait la même manipulation et la communication par SPI fonctionne ! On arrive donc à communiquer avec l'Arduino.

Protocole:

Pour envoyer et recevoir des paquets Ethernet avec le PC et la carte, on doit développer notre propre protocole. On a développé un premier protocole test appelé GPOAT (Greatest Protocol Of All Time) qui est composé de la manière suivante:

Structure d'un paquet GPOAT

Le but de ce paquet est de recevoir 2 nombres de 16 octets et y effectuer une opération qui dépend du bit de point faible de Operation.

Pour reconnaitre ce protocole, on a fait un nouvel Ether Type "GPOAT" référencé à 0x2706 qu'on déclare dans le fichier EthernetProtocole.h.

La fonction de traitement du code et tiré de celle du protocole ARP .

Voici comment est traité le paquet et comment la réponse est forgée:

GPOATHeaderOUT->TMA = GPOATHeaderIN->SMA;
			GPOATHeaderOUT->TIA = GPOATHeaderIN->SIA;

			/* Copy over the new sender MAC/IP - MAC and IP addresses of the virtual webserver */
			GPOATHeaderOUT->SMA = ServerMACAddress;
			GPOATHeaderOUT->SIA = ServerIPAddress;
			/*Compute the result of the equation */
			uint32_t tmp_d1 = 0;
			uint32_t tmp_d2 = 0; //j'utilise des tampons 2 fois plus grand pour éviter l'overflow lors des calculs
			tmp_d1 |=GPOATHeaderIN->Data1;
			tmp_d2 |=GPOATHeaderIN->Data2;
			switch(GPOATHeaderIN->Operation)
			{
				case 0x01:/*addition */
					GPOATHeaderOUT->Data1 = SwapEndian_16((tmp_d1 + tmp_d2) >> 16);//on garde la moitié "gauche"
					GPOATHeaderOUT->Data2 = (tmp_d1 + tmp_d2) & 0xffff;//on garde la moitié "droite"
					break;
				case 0x02:/*soustraction*/
					GPOATHeaderOUT->Data1 = SwapEndian_16((tmp_d1 - tmp_d2) >> 16);//on garde la moitié "gauche"
					GPOATHeaderOUT->Data2 = SwapEndian_16((tmp_d1 - tmp_d2) & 0xffff);//on garde la moitié "droite"
					break;
				case 0x03:/*multiple*/
					GPOATHeaderOUT->Data1 = SwapEndian_16((tmp_d1 * tmp_d2) >> 16);//on garde la moitié "gauche"
					GPOATHeaderOUT->Data2 = SwapEndian_16((tmp_d1 * tmp_d2) & 0xffff);//on garde la moitié "droite"
					break;
				case 0x04:/*division*/
					GPOATHeaderOUT->Data1 = SwapEndian_16((tmp_d1 / tmp_d2) >> 16);  //on garde la moitié "gauche"
					GPOATHeaderOUT->Data2 = SwapEndian_16((tmp_d1 / tmp_d2) & 0xffff);//on garde la moitié "droite"
					break;
			}
			GPOATHeaderOUT->Operation = GPOATHeaderIN->Operation | 0x10;

On inverse les adresses sources et destinataires, après on vérifie qu'on a une question. Puis on fait les calculs en passant par un tampon et on indique qu'on fait une réponse.Dans l'exemple ci dessous, la carte reçoit 2 valeurs qui sont "ff ff" et il lui est demandé de les diviser entre eux, ce à quoi la carte répond avec comme valeur "1".

Test d'envoi et réception d'un GPOAT (le premier est le paquet envoyé par le PC et le 2ème est la réponse de la carte)

Si vous voulez tester le protocole, il y a un dossier GPOAT qui génère un paquet GPOAT. Il faudra juste changer les data à la main.


Maintenant que l'on sait comment créer un protocole, on peut créer un protocole qui va envoyer les commandes de la carte mère à un dépot distant ( un PC ) et qui va récupérer la réponse du PC : le protocole KIFF. référencé par l'Ethertype 0x2911.

Malheureusement, nous n'avons pas pu tester ce protocole.

Voici la structure du protocole

typedef struct
		{
			MAC_Address_t SMA; /**< Sender's Mac address */
			MAC_Address_t TMA; /**< Target's Mac address */
			uint8_t Operation; /* Reception 0x01 ou Envoi 0x00 */
			uint8_t Message; // Bit de point faible = 1 : ls, 2 = récuperer un fichier, 3 = envoyer un fichier
			uint8_t Data; /* Pour la réponse ( en uint8_t car la réponse va être envoyé par SPI à la carte mere)*/
		} KIFF_Header_t;


Avec la structure, on peut maintenant écrire la fonction de traitement des paquets :

	KIFF_Header_t* KIFFHeaderIN  = (KIFF_Header_t*)InDataStart;
	KIFF_Header_t* KIFFHeaderOUT = (KIFF_Header_t*)OutDataStart;
		/* Ensure that the KIFF request is a request packet */
	if ((KIFFHeaderIN->Operation) == KIFF_OPERATION_REPLY)
	{
		// Fonction qui envoit la data a la carte Mere
		SPDR = KIFFHeaderIN->data;

			return sizeof(KIFF_Header_t);

Cette fois au lieu de reformuler une réponse, on récupère les données et on l'envoie sur le bus SPI.

Ether :

Maintenant que l'on a un protocole qui sert à faire la communication entre la carte mère et le PC, il faut que le PC puisse interpréter le paquet et reformuler la réponse à la carte mère.

Dans un premier temps on va stocker les paquets que l'utilitaire ether va afficher.

Tout d'abord, il faut que ether affiche nos paquets, pour cela on ajoute les protocoles dans le filtre de bpf.c :

machineLanguage filterProgram[]={
 
 {NULL,      MLCOND_ANY,
	      BPF_LD|BPF_H|BPF_ABS, NULL, NULL, "12"},
  {NULL,      MLCOND_ANY,
	      BPF_JMP|BPF_JEQ,      "srcaddr", "ipv6", "0x800"},
  {"ipv6",    MLCOND_ANY,
	      BPF_JMP|BPF_JEQ,      "srcaddr", "gpoat", "0x2706"},
  {"gpoat",    MLCOND_ANY,
	      BPF_JMP|BPF_JEQ,      "srcaddr", "arp", "0x86dd"},
  {"arp",     MLCOND_ANY, ....

Puis lors de l'affiche des paquets, on en profite pour envoyer les paquets dans un fichier de traitements :

void displayPacket(FILE *stream,char *packet,int size){
  int i,count=MAX_COLUMNS;
  FILE * fichier = fopen(FICHIERPATH,"w");
  for(i=0;i<size;i++){
    fprintf(stream,"%2.2x ",(unsigned char)packet[i]);
    fprintf(fichier,"%2.2x ",(unsigned char) packet[i]);
    count--;
    if(count<=0){ fprintf(stream,"\n"); count=MAX_COLUMNS; }
    }
  fclose(fichier);
  if(count!=MAX_COLUMNS) fprintf(stream,"\n");
  }


Malheureusement nous avons pas pu aller plus loin.

Bilan :

Nous avons conçu un shield arduino sur lequel nous avons programmé un programme d'ordonnancement, qui peut exécuter en tourniquet plusieurs dont des tâches de communiquer par minicom. Nous avons aussi conçu une carte fille réseau RNDIS et utilisé la lib LUFA pour la programmer. Cette carte peut répondre au paquet classique ( ARP, IP, ... ) mais aussi à un protocole que l'on a crée.

Par la suite, nous aurions pu faire un deuxième protocole, que transmet les infos qu'il reçoit à la carte mère via SPI, une fonction qui reçoit les commandes de la carte mère qu'elle envoie dans un paquet ethernet au dépot distant ( un pc ), une application sur le pc qui lors de la réception d'un paquet du protocole renvoie un paquet avec les données demandées.