« SE4Binome2024-3 » : différence entre les versions

De projets-se.plil.fr
Aller à la navigation Aller à la recherche
Aucun résumé des modifications
 
(44 versions intermédiaires par 3 utilisateurs non affichées)
Ligne 1 : Ligne 1 :
[[Fichier:Titre page pico carte mère cart detrez.png|centré|sans_cadre|972x972px]]


= Réalisation d'un shield arduino =
== Description ==
L'objectif de ce projet est de réaliser un pico ordinateur. Nous allons traiter le développement de la carte mère, un shield arduino ainsi que la programmation d'un ordonnanceur.


== Schématique ==
Notre carte mère sera alimenté via une alimentation secteur en 5v et le code sera implanté par USB.
[[Fichier:Schematique shield CART DETREZ.pdf|sans_cadre]]


== Routage ==
== Liens ==
[[Fichier:Routage shield CART DETREZ.png|sans_cadre]]
Lien du git : https://gitea.plil.fr/vdetrez/SE4_PICO_DETREZ_CART
 
== Réalisation d'un shield arduino ==
 
=== Schématique et routage ===
{| class="wikitable"
|+
![[Fichier:PicoShield SCH Detrez Cart.pdf|sans_cadre|639x639px]]
![[Fichier:Routage shield CART DETREZ.png|sans_cadre|638x638px]]
|}


== PINOUT==
=== PINOUT ===
{| class="wikitable"
{| class="wikitable"
|+
|+
Ligne 27 : Ligne 37 :
|}
|}


==Brasage du shield==
=== Brasage du shield ===


=== shield nu===
==== shield nu ====
[[Fichier:Shield nu CART DETREZ 2.jpg|sans_cadre]]
[[Fichier:Shield nu CART DETREZ 2.jpg|sans_cadre]]


===shield fini===
==== shield fini ====
[[Fichier:Shield fini CART DETREZ.jpg|sans_cadre]]
[[Fichier:Shield fini CART DETREZ.jpg|sans_cadre]]


==Test du shield==
=== Test du shield ===
[[Fichier:Affichage 7 segments DETREZ CART.jpg|sans_cadre]]
[[Fichier:Affichage 7 segments DETREZ CART.jpg|sans_cadre]]


= Réalisation carte mère =
== Réalisation carte mère ==


== Schématique ==
=== Schématique et routage ===
{| class="wikitable"
|+
![[Fichier:Schématique Carte mere CART DETREZ.pdf|centré|sans_cadre|679x679px]]
![[Fichier:Schématique Carte mere .png|centré|sans_cadre|575x575px]]
|}


== Routage ==
=== Brasage carte mère ===


=Programmation ordonnanceur=
==== carte mère nue ====
[[Fichier:PCB_carte_mère_nu.jpg|sans_cadre]]


=== Code ordonnanceur ===
==== ajout ATmega 328p ====
<syntaxhighlight lang="c" line="1" start="1">
[[Fichier:Brasage ATmega 328p carte mère.jpg|sans_cadre]]
#include "Fonctions.h"


#define RESTORE_REGISTERS() \
==== ajout ATmega 8U2 ====
asm volatile ( \
[[Fichier:Carte mère G3 avec 8U2.jpg|sans_cadre]]
    "pop r31 \n\t" \
    "pop r30 \n\t" \
    "pop r29 \n\t" \
    "pop r28 \n\t" \
    "pop r27 \n\t" \
    "pop r26 \n\t" \
    "pop r25 \n\t" \
    "pop r24 \n\t" \
    "pop r23 \n\t" \
    "pop r22 \n\t" \
    "pop r21 \n\t" \
    "pop r20 \n\t" \
    "pop r19 \n\t" \
    "pop r18 \n\t" \
    "pop r17 \n\t" \
    "pop r16 \n\t" \
    "pop r15 \n\t" \
    "pop r14 \n\t" \
    "pop r13 \n\t" \
    "pop r12 \n\t" \
    "pop r11 \n\t" \
    "pop r10 \n\t" \
    "pop r9 \n\t" \
    "pop r8 \n\t" \
    "pop r7 \n\t" \
    "pop r6 \n\t" \
    "pop r5 \n\t" \
    "pop r4 \n\t" \
    "pop r3 \n\t" \
    "pop r2 \n\t" \
    "pop r1 \n\t" \
    "pop r0 \n\t" \
    "out __SREG__, r0 \n\t" \
    "pop r0 \n\t" \
);


#define SAVE_REGISTERS() \
==== mise en lien des ATmega et correction de conception ====
asm volatile ( \
La correction consiste en l'ajout d'un bouton reset et HWB pour l'ATmega8U2.
    "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 r5 \n\t" \
    "push r6 \n\t" \
    "push r7 \n\t" \
    "push r8 \n\t" \
    "push r9 \n\t" \
    "push r10 \n\t" \
    "push r11 \n\t" \
    "push r12 \n\t" \
    "push r13 \n\t" \
    "push r14 \n\t" \
    "push r15 \n\t" \
    "push r16 \n\t" \
    "push r17 \n\t" \
    "push r18 \n\t" \
    "push r19 \n\t" \
    "push r20 \n\t" \
    "push r21 \n\t" \
    "push r22 \n\t" \
    "push r23 \n\t" \
    "push r24 \n\t" \
    "push r25 \n\t" \
    "push r26 \n\t" \
    "push r27 \n\t" \
    "push r28 \n\t" \
    "push r29 \n\t" \
    "push r30 \n\t" \
    "push r31 \n\t" \
);


int currentTask = 0;
[[Fichier:Lien ATmega + correction.jpg|sans_cadre]]
//uint16_t SP = 0x700;


// FONCTIONS
==== carte finale ====
[[Fichier:Carte mere finale detrez cart 2.jpg|sans_cadre]]


// Déclaration des différentes tâches
=== Tests ===
process task[NB_TASKS] = {
 
    {0x700,Serial_Led,0},
==== Test de clignotement d'une LED (25ms) ====
    {0x600, LED1_blink,0},
[[Fichier:Test clignotement LED ATmega 328p .mp4|sans_cadre]]
    {0x500, Serial_Message,0}
 
};
=== Programmation des cartes ===
 
 
27/11/24 : pour le moment le µc atmega328p a réussi à être programmé par ISP en changeant les fuses grâce à avrdude. En revanche, impossible de programmer l'atmega8u2. Nous avons essayé de lui mettre un bootloader et de changer ses fuses mais c'est impossible dans les deux cas et la carte n'est donc pas détectée en USB.
 
4/12/24 : nous avons réussi à mettre un bootloader dans le 8u2, en revanche nous n'arrivons toujours pas à programmer le 328p grâce au port USB. La cause : nous n'arrivons pas à burn le bootloader du 328p pour qu'il puisse recevoir des données via usb_série ...
 
Solution trouvée : Nous avons changé le bootloader en mettant [https://github.com/Optiboot/optiboot/blob/master/optiboot/bootloaders/optiboot/optiboot_atmega328.hex celui-ci] (/usr/share/arduino/hardware/arduino/avr/bootloaders/optiboot) à la place pour augmenter la place de ce dernier. Maintenant le 328p peut être programmé grâce à l'USB.
 
== Programmation ordonnanceur ==


=== Initialisation de la pile ===
<syntaxhighlight lang="c">
void init_pile(int N){
void init_pile(int N){
     uint16_t tempSP = SP;
     uint16_t tempSP = SP;
Ligne 148 : Ligne 104 :
     SP = tempSP;
     SP = tempSP;
}
}
</syntaxhighlight>La fonction init_pile() sert à charger un processus dans la pile. Cette fonction est appelée dans une boucle au début du main pour initialiser tous les processus.
=== Interruption de l'ordonnanceur ===
L'ordonnanceur est appelé à un interval de temps régulier grâce à des interruptions du Timer1.


void init_minuteur(int diviseur,long periode){
Avant de passer d'une tâche à une autre (mode Round Robin) il faut d'abord effectuer quelques opérations sur la mémoire du micro-processeur.<syntaxhighlight lang="c">
TCCR1A=0;              // Le mode choisi n'utilise pas ce registre
ISR(TIMER1_COMPA_vect, ISR_NAKED){   // Procédure d'interruption
TCCR1B=(1<<CTC1);      // Réinitialisation du minuteur sur expiration
    /* Sauvegarde du contexte de la tâche interrompue */
switch(diviseur){
    SAVE_REGISTERS();
  case    8: TCCR1B |= (1<<CS11); break;
    task[currentTask].stackPointer = SP;
  case  64: TCCR1B |= (1<<CS11 | 11<<CS10); break;
    /* Appel à l'ordonnanceur */
  case  256: TCCR1B |= (1<<CS12); break;
    scheduler();
  case 1024: TCCR1B |= (1<<CS12 | 1<<CS10); break;
    /* Récupération du contexte de la tâche ré-activée */
  }
    SP = task[currentTask].stackPointer;
// Un cycle prend 1/F_CPU secondes.
    RESTORE_REGISTERS();
// Un pas de compteur prend diviseur/F_CPU secondes.
    asm volatile ( "reti" );
// 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
}
}
</syntaxhighlight>La routine d'interruption est générée par le '''Timer1''' avec comme paramètre ISR_NAKED qui permet de ne pas avoir le prologue et l'épilogue automatique du compilateur ce qui nous est très utile pour gérer manuellement ces interruptions afin d'éxecuter nos tâches.


void scheduler(){
La macro '''''SAVE_REGISTERS()''''' nous permet de sauvegarder les 32 registres sur la pile. Cela permet de s’assurer qu’au retour de l'interruption, la tâche interrompue pourra reprendre son exécution là où elle s’était arrêtée, sans perdre son contexte.
    currentTask ++;
 
    if(currentTask == NB_TASKS) currentTask = 0; // ordonnanceur en mode Round Robin
'''''task[currentTask].stackPointer = SP''''' permet de sauvegarder le pointeur de pile de la tâche associée.
}


// Déclaration d'un tableau de processus
'''''scheduler()''''' est expliqué [[SE4Binome2024-3#Code de l'ordonnanceur|ici]].
ISR(TIMER1_COMPA_vect, ISR_NAKED){    // Procédure d'interruption
/* Sauvegarde du contexte de la tâche interrompue */
SAVE_REGISTERS();
task[currentTask].stackPointer = SP;
/* Appel à l'ordonnanceur */
scheduler();
/* Récupération du contexte de la tâche ré-activée */
SP = task[currentTask].stackPointer;
RESTORE_REGISTERS();
asm volatile ( "reti" );
}


int main(void){
'''''SP = task[currentTask].stackPointer''''' permet de replacer le pointeur de pile à celui de la nouvelle tâche engendrée par le scheduler.
cli();
LED_init();
USART_Init(MYUBRR);
init_minuteur(256,PERIODE);
for(int i=1;i<NB_TASKS;i++) init_pile(i);
SP = task[0].stackPointer;
sei();
task[0].addressFunction();
}
</syntaxhighlight>


=== Code des tâches ===
La macro '''''RESTORE_REGISTERS()''''' remets la nouvelle fonction sur les 32 registres avec des Pop().
<syntaxhighlight lang="c" line="1" start="1">
#ifndef FONCTIONS_H
#define FONCTIONS_H


// Inclusions de bibliothèques nécessaires
'''''asm volatile ( "reti" )''''' : étant en ISR_NAKED, il faut dire manuellement la fin de l'interruption avec cette commande assembleur '''RETurn from Interrupt'''.
#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdint.h>
#include <util/delay.h>


#define CTC1            WGM12          // Meilleur nom pour le bit
=== Structure d'une tâche ===
<syntaxhighlight lang="c">
enum States{
    AWAKE,
    IDLE
};


#define PERIODE        20
enum IDLE_TYPE{
#define FOSC 16000000 // Clock Speed
    IDLE_TYPE_DELAY,
#define BAUD 9600
    IDLE_TYPE_STABLE
#define MYUBRR FOSC/16/BAUD-1
};


#define LED1 PC0
typedef union {
#define LED2 PC3
  int sleeping_time;
#define LED3 PD1
} Time;
#define LED4 PD4
#define LED5 PD7


#define NB_TASKS 3
typedef struct {
  enum IDLE_TYPE type;
  Time time;
} Etat ;


typedef struct process{
typedef struct{
     uint16_t stackPointer; // stack pointer, défini le début du process
     uint16_t stackPointer; // stack pointer, défini le début du process
     void (*addressFunction)(void); // adresse de l'instruction en cours
     void (*addressFunction)(void); // adresse de l'instruction en cours
     int state; // état de la fonction (prêt, en cours, terminé)
     enum States state; // état de la fonction (Awake ou Idle)
    Etat etat;
}process;
}process;
</syntaxhighlight>
Les tâches sont définies par leurs pointeurs de piles, leurs adresses, un état qui est soit IDLE ou AWAKE et enfin un dernier état qui lui gère permet d'avoir différent type d'IDLE.
L'utilisation d'[[wikipedia:Union_type#C/C++|unions]] n'est pas forcément justifiée ici mais permet de limiter la place que va prendre la structure dans l'optique de futures implémentations.
=== Code de l'ordonnanceur ===
<syntaxhighlight lang="c">
void scheduler(){
// décrémentation de l'attente de toutes les tâches en IDLE
for(int i=0;i<NB_TASKS;i++){
    if(task[i].state==IDLE && (task[i].etat.type==IDLE_TYPE_DELAY)){
        task[i].etat.time.sleeping_time -= PERIODE;
        if(task[i].etat.time.sleeping_time <= 0){
            task[i].etat.time.sleeping_time = 0;
            task[i].state = AWAKE;
        }
    }
}
do{
  currentTask ++;
  if(currentTask >= NB_TASKS) currentTask = 0; // ordonnanceur en mode Round Robin
  }while(task[currentTask].state == IDLE);
}
</syntaxhighlight>


void LED_init(void);
void USART_Init(unsigned int ubrr);
void LED1_blink(void);
void LED2_blink(void);
void LED3_blink(void);
void LED4_blink(void);
void LED5_blink(void);
void USART_Transmit(unsigned char data);
unsigned char USART_Receive(void);
void Serial_Led();
void Serial_Message();


#endif
L'ordonnanceur est en mode [https://fr.wikipedia.org/wiki/Round-robin_(informatique) Round Robin] (tourniquet) ce qui veut dire qu'il n'y a pas de priorité de tâche, elles sont toutes appelées une à une dans l'ordre avant de boucler et ainsi de suite...
</syntaxhighlight><syntaxhighlight lang="c" line="1" start="1">
#include "Fonctions.h"


void LED_init(){
L'un des ajouts a été de mettre un mode IDLE à nos tâches qui indique la veille de ces dernières. Si une tâche est en IDLE, elle ne sera pas appelée lors de la sélection des tâches et son temps de sommeil sera décrémenté. Cela permet de créer des attentes passives donc d'éviter d'attendre inutilement des tâches qui ne font qu'attendre. On bon exemple pour comprendre serait avec un [[SE4Binome2024-3#Exemple de tâches|clignotement de LED]]. Au lieu de mettre un _delay_ms(1000) qui éxecutera une attente à chaque fois que cette tâche est appelée, on la passe pendant 1000 ms ce qui permet d'éxécuter d'autres tâches entre temps.
     DDRC |= (1<<LED1) | (1<<LED2);
 
     DDRD |= (1<<LED3) | (1<<LED4) | (1<<LED5);
=== Fonction wait ===
     PORTC |= (1<<LED1)|(1<<LED2);
<syntaxhighlight lang="c" line="1">
     PORTD |= (1<<LED3)|(1<<LED4)|(1<<LED5);
void wait(int time_ms){
     task[currentTask].state = IDLE;
     task[currentTask].etat.type=IDLE_TYPE_DELAY;
     task[currentTask].etat.time.sleeping_time = time_ms;
     TCNT1 = 0;
    TIMER1_COMPA_vect();
}
}
</syntaxhighlight>


La fonction permet d'endormir un processus pendant un temps donné en ms.
Elle passe la tâche en IDLE, remet le '''Timer1''' à zéro pour éviter de garder un décalage de temps car le changement est surement fait en plein milieu d'une tâche.
Et ensuite elle appelle l'interruption pour qu'une nouvelle tâche soit en cours.
=== Exemple de tâches ===
<syntaxhighlight lang="c" line="1">
void USART_Init(unsigned int ubrr)
void USART_Init(unsigned int ubrr)
{
{
Ligne 261 : Ligne 223 :
/* Set frame format: 8data, 2stop bit */
/* Set frame format: 8data, 2stop bit */
UCSR0C = (1<<USBS0)|(3<<UCSZ00);
UCSR0C = (1<<USBS0)|(3<<UCSZ00);
}
</syntaxhighlight><syntaxhighlight lang="c">
void LED_init(){
    DDRC |= (1<<LED1) | (1<<LED2);
    DDRD |= (1<<LED3) | (1<<LED4) | (1<<LED5);
    PORTC |= (1<<LED1)|(1<<LED2);
    PORTD |= (1<<LED3)|(1<<LED4)|(1<<LED5);
}
}


Ligne 266 : Ligne 235 :
     while(1){
     while(1){
         PORTC ^= (1<<LED1);
         PORTC ^= (1<<LED1);
         _delay_ms(150);
         _delay_ms(1000);
     }
     }
}
}
Ligne 273 : Ligne 242 :
     while(1){
     while(1){
         PORTC ^= (1<<LED2);
         PORTC ^= (1<<LED2);
         _delay_ms(100);
         wait(100);
    }
}
 
void LED3_blink(){
    while(1){
        PORTD ^= (1<<LED3);
        _delay_ms(250);
    }
}
 
void LED4_blink(){
    while(1){
        PORTD ^= (1<<LED4);
        _delay_ms(350);
    }
}
 
void LED5_blink(){
    while(1){
        PORTD ^= (1<<LED5);
        _delay_ms(50);
    }
}
 
void Serial_Led(){
    while(1){
        if(USART_Receive() == 'a') LED2_blink();
 
     }
     }
}
}
</syntaxhighlight>


<syntaxhighlight lang="c">
void Serial_Message(){
void Serial_Message(){
     unsigned char data;
     unsigned char data;
Ligne 329 : Ligne 272 :
/* Get and return received data from buffer */
/* Get and return received data from buffer */
return UDR0;
return UDR0;
}
</syntaxhighlight><syntaxhighlight lang="c">
void spi_activer(void){                              // Activer le périphérique
    PORTD &= ~(1<<SPI_SS);                            // Ligne SS à l'état bas
}
void spi_desactiver(void){                          // Désactiver le périphérique
    PORTD |= (1<<SPI_SS);                            // 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
}
void seven_seg(){
    while(1){
    spi_activer();
    spi_echange(0x01);
    spi_desactiver();
    }
}
}
</syntaxhighlight>
</syntaxhighlight>
Ligne 340 : Ligne 305 :
[[Fichier:Affichage clavier minicom.mp4|sans_cadre]]
[[Fichier:Affichage clavier minicom.mp4|sans_cadre]]


=Liens=
== Création mémoire sur carte SD ==
Lien du git : https://gitea.plil.fr/vdetrez/SE4_PICO_DETREZ_CART
 
=== Schéma de fonctionnement de la mémoire ===
La mémoire de la carte SD est composé de plusieurs paquets d'octets qui s'appellent "Bloc". Nous pouvons alors dire qu'un bloc est semblable à :<syntaxhighlight lang="c">
uint8_t Bloc[512]
</syntaxhighlight>Nous pouvons donc organiser notre espace comme nous le voulons.
 
[[Fichier:Gestion memoire sd.png|sans_cadre|770x770px]]
 
Nous avons pris la liberté de changer légèrement le sujet en mettant le nom du fichier sur 14 octets au maximum et en disant qu'un bloc représente 512 octets, ce qui est d'ailleurs le cas sur nos cartes SD. Cela nous permet de décrire un fichier en 32 octets (nom + taille + indices) et de pouvoir mettre 16 description de fichier par bloc et d'ainsi utiliser tous les octets d'un bloc.
 
=== Récupération des commandes de base ===
 
Dans un premier temps, nous avons un programme qui permet à la carte de mère de reconnaitre 3 commandes: ls, rm et cp. Cela nous permettra par la suite de créer des fichiers, les supprimer et lire les fichiers présents dans le répertoire. <syntaxhighlight lang="c">
int main(){
    init_stdio();
    char data[Max_chaine],cmd[Max_chaine], arg1[Max_chaine],arg2[Max_chaine];
    while(1) {
        fgets(data,Max_chaine,stdin);
        fprintf(stdout,"\r");
        fflush(stdout);
        int taille = sscanf(data,"%s %s %s", cmd,arg1,arg2);
        if(taille != 0){
            switch(taille){
            case 1 :
                if(strcmp(cmd,"ls") == 0) fprintf(stdout,"je suis un ls\n");
                else fprintf(stdout,"je suis une fraude\n");
                break;
            case 2 :
                if(strcmp(cmd,"rm") == 0) fprintf(stdout,"je suis un rm\n mon arg est : %s \n",arg1);
                else fprintf(stdout,"je suis une fraude\n");
                break;
            case 3:
                if(strcmp(cmd,"cp") == 0) {
                    fprintf(stdout,"je suis un cp\n");
                    fprintf(stdout,"mon arg1 est : %s\n",arg1);
                    fprintf(stdout,"mon arg2 est : %s\n",arg2);}
                else fprintf(stdout,"je suis une fraude\n");
                break;
            }
        }
    }
}
</syntaxhighlight>
 
 
Lors de l'utilisation de minicom, le fgets ne s'exécute que lors d'un "/n", cependant, un entrée sur le clavier correspond à un "/r", nous avons donc converti chaque "/r" en "/n" dans notre fonction de scan pour fluidifier l'utilisation.
 
<syntaxhighlight lang="c">
static int get_serial_scanf(FILE *stream) {
static unsigned char nl=0;
if(stream==stdin){
  if(nl){ nl=0; return '\n'; }
  char c=get_serial();
  if(c=='\r') nl=1;
  return c;
}
return _FDEV_EOF;
}
</syntaxhighlight>
 
 
À l'inverse, lors d'un fprintf, il est naturel en C de placer un "/n" à  la fin d'une chaine de caractère. Cependant, la carte mère à besoin d'un "\r" pour effectuer ce retour à la ligne. Nous avons donc converti chaque "\n" en "\r" dans notre fonction d'impression.
 
<syntaxhighlight lang="c">
static int send_serial_printf(char c,FILE *stream){
if(stream==stdout){
  if(c=='\n') send_serial('\r');
  send_serial(c);
}
return 0;
}
</syntaxhighlight>
 
=== Accès mémoire de la carte SD ===
Pour pouvoir utiliser la carte SD, nous avons utilisé les fichiers Sd2Card qui sont une modification de la bibliothèque arduino. Ces fichiers permettent d'utiliser des fonctions comme readBlock et writeBlock afin de manipuler directement la mémoire de la carte SD par SPI.
 
== Primitive système ==
Plusieurs primitives systèmes ont été ajoutés afin de manipuler la mémoire de la carte SD. Vous pouvez trouver ces fichiers dans le répertoire SD/ de notre [https://gitea.plil.fr/vdetrez/SE4_PICO_DETREZ_CART dépôt git]. Toutes les primitives renvoient leurs informations via le port série.
 
=== FORMAT ===
Cette primitive permet de formater la carte SD d'un bloc à un autre précisé en paramètre. Pour ce faire, la fonction écrit des 0 dans les blocs.<syntaxhighlight lang="c">
void FORMAT(int debut,int fin){
    uint8_t buffer[BLOC_TAILLE];
    memset(buffer,0,BLOC_TAILLE);
    for(int i = debut; i<fin; i++) {writeBlock(&sd,i,buffer);}
}
</syntaxhighlight>
 
=== LS ===
Pour lister les fichiers, seul le Superbloc est important. Plus précisément, les blocs compris entre 0 et 13. Dans ces blocs, nous récupérons seulement les 14 premiers octets (TAILLE_NOM) de chaque fichiers et ces noms de fichiers sont espacés de 32 octets (voir [[SE4Binome2024-3#Schéma de fonctionnement de la mémoire|schéma]])
 
Dans notre fonction, i représente le numéro de fichier que l'on va regarder et k permet de savoir à quel fichier dans le bloc nous sommes. La fonction envoi alors sur la sortie standard tous les fichiers ne commençant pas par 0.<syntaxhighlight lang="c">
void LS(){
    uint8_t buffer[BLOC_TAILLE];
    int BlockNumber = 0;
    uint8_t filename[TAILLE_NOM];
    readBlock(&sd,BlockNumber,buffer);
    for(uint8_t i=0, k=0; i<NB_FILE_MAX; i++,k++){ // on parcours tous les emplacements potentiels des desc de fichiers
        memset(filename,0,TAILLE_NOM); // reset du tampon qui contient le nom du fichier
        if(k>((BLOC_TAILLE/sizeof(SD_files_desc))-1)){ // reset de k et passage au bloc suivant
            BlockNumber++;
            readBlock(&sd,BlockNumber,buffer);
            k=0;
        }
        for(int j=0; j<TAILLE_NOM;j++){// Copie du nom dans filename
            if(buffer[j+k*sizeof(SD_files_desc)] == 0) break; // Comparaison avec le caractère \0
            else filename[j] = buffer[j+k*sizeof(SD_files_desc)];
        }
        if(filename[0] != 0)fprintf(stdout,"%s\n", filename);
    }
}
</syntaxhighlight>
 
=== APPEND ===
Crée un nouveau fichier ou le modifie s'il est déjà existant. Dans notre application, append() demande la taille du fichier et propose à l'utilisateur d'écrire dans son fichier bloc par bloc.
 
=== READ ===
Lis les blocs de données d'un fichier sous forme de chaîne de caractères.
 
=== CP ===
Permet de copier un fichier source dans un fichier de destination. Il récupère d'abord la taille et les indices de bloc des fichiers, regarde quels sont les blocs disponibles puis réécrit ces blocs avec la data du fichier src.
=== RM ===
 
Supprime le fichier spécifié en supprimant le descripteur du SuperBloc (nom + taille + indices) et passe à 0 dans la bitmap les octets représentant les blocs de données du fichier. Cependant, RM ne supprime pas les données du fichier : les fichiers créés nettoient d'abord les blocs avant d'écrire dessus. On évite donc de faire deux fois l'opération.
 
== Fonctionnement des primitives ==
[[Fichier:Test des primitives.mp4|centré|sans_cadre|0x0px]]
 
 
== Bilan de puissance ==
Afin d'être sûr de l'alimentation de toutes les cartes, nous utilisons une alimentation +5V d'une Raspberry PI branchée sur secteur.
{| class="wikitable"
|+
!
{| class="wikitable"
|+
! colspan="3" |Bilan de puissance (VCC = 3.3V)
|-
!Composant
!Courant
!Puissance
|-
|74LVC125
|40 uA
|0.2mW
|-
|Carte SD
|100 mA
|330 mW
|-
|'''TOTAL'''
|'''100,04 mA'''
|'''330.2 mW'''
|}
!
{| class="wikitable"
|+
! colspan="3" |Bilan de puissance (Vcc = 5V)
|-
!Composant
!Courant
!Puissance
|-
|AtMega328p
|14mA
|70mW
|-
|ATMega8U2
|21mA
|105mW
|-
|5xLED
|5*5mA
|5*25 mW
|-
|'''TOTAL'''
|'''60mA'''
|'''300 mW'''
|}
!
{| class="wikitable"
|+
! colspan="2" |Bilan de puissance général du groupe
|-
!CARTE
!PUISSANCE (maximale)
|-
|Mère
|0,63W
|-
|Clavier
|5,55W
|-
|Réseau
|0,15W
|-
|Ecran
|0,17W
|-
|'''TOTAL'''
|'''2,685W'''
|}
|}
 
== CONCLUSION ==
Dans l’état actuel, les différentes cartes filles et la carte mère ne sont malheureusement pas encore en mesure de communiquer entre elles. Cependant, ce projet s’est révélé particulièrement enrichissant, car il nous a permis de mobiliser un large éventail de compétences acquises au cours de notre cursus, telles que la conception de PCB ou encore l’ordonnancement des tâches. En outre, il a constitué une excellente opportunité de développer nos capacités en gestion d’équipe, notamment à travers la coordination entre les différents groupes travaillant sur les cartes filles. Parmi les principaux défis rencontrés, la gestion de la mémoire de la carte SD a été particulièrement complexe, qu’il s’agisse de comprendre les mécanismes de communication avec cette dernière ou de définir une stratégie efficace pour la gestion des fichiers. Nous espérons sincèrement que ce wiki servira de guide utile aux futures promotions, en leur permettant d’éviter les erreurs que nous avons commises, et pourquoi pas, d’achever complètement ce projet ambitieux.

Version actuelle datée du 26 janvier 2025 à 17:38

Titre page pico carte mère cart detrez.png

Description

L'objectif de ce projet est de réaliser un pico ordinateur. Nous allons traiter le développement de la carte mère, un shield arduino ainsi que la programmation d'un ordonnanceur.

Notre carte mère sera alimenté via une alimentation secteur en 5v et le code sera implanté par USB.

Liens

Lien du git : https://gitea.plil.fr/vdetrez/SE4_PICO_DETREZ_CART

Réalisation d'un shield arduino

Schématique et routage

PicoShield SCH Detrez Cart.pdf Routage shield CART DETREZ.png

PINOUT

LED1 PC0
LED2 PC3
LED3 PD1
LED4 PD4
LED5 PD7

Brasage du shield

shield nu

Shield nu CART DETREZ 2.jpg

shield fini

Shield fini CART DETREZ.jpg

Test du shield

Affichage 7 segments DETREZ CART.jpg

Réalisation carte mère

Schématique et routage

Schématique Carte mere CART DETREZ.pdf
Schématique Carte mere .png

Brasage carte mère

carte mère nue

PCB carte mère nu.jpg

ajout ATmega 328p

Brasage ATmega 328p carte mère.jpg

ajout ATmega 8U2

Carte mère G3 avec 8U2.jpg

mise en lien des ATmega et correction de conception

La correction consiste en l'ajout d'un bouton reset et HWB pour l'ATmega8U2.

Lien ATmega + correction.jpg

carte finale

Carte mere finale detrez cart 2.jpg

Tests

Test de clignotement d'une LED (25ms)

Programmation des cartes

27/11/24 : pour le moment le µc atmega328p a réussi à être programmé par ISP en changeant les fuses grâce à avrdude. En revanche, impossible de programmer l'atmega8u2. Nous avons essayé de lui mettre un bootloader et de changer ses fuses mais c'est impossible dans les deux cas et la carte n'est donc pas détectée en USB.

4/12/24 : nous avons réussi à mettre un bootloader dans le 8u2, en revanche nous n'arrivons toujours pas à programmer le 328p grâce au port USB. La cause : nous n'arrivons pas à burn le bootloader du 328p pour qu'il puisse recevoir des données via usb_série ...

Solution trouvée : Nous avons changé le bootloader en mettant celui-ci (/usr/share/arduino/hardware/arduino/avr/bootloaders/optiboot) à la place pour augmenter la place de ce dernier. Maintenant le 328p peut être programmé grâce à l'USB.

Programmation ordonnanceur

Initialisation de la pile

void init_pile(int N){
    uint16_t tempSP = SP;
    SP = task[N].stackPointer;
    uint16_t adresse=(uint16_t)task[N].addressFunction;
    asm volatile("push %0" : : "r" (adresse & 0x00ff) );
    asm volatile("push %0" : : "r" ((adresse & 0xff00)>>8) );
    SAVE_REGISTERS();
    task[N].stackPointer = SP;
    SP = tempSP;
}

La fonction init_pile() sert à charger un processus dans la pile. Cette fonction est appelée dans une boucle au début du main pour initialiser tous les processus.

Interruption de l'ordonnanceur

L'ordonnanceur est appelé à un interval de temps régulier grâce à des interruptions du Timer1.

Avant de passer d'une tâche à une autre (mode Round Robin) il faut d'abord effectuer quelques opérations sur la mémoire du micro-processeur.

ISR(TIMER1_COMPA_vect, ISR_NAKED){    // Procédure d'interruption
    /* Sauvegarde du contexte de la tâche interrompue */
    SAVE_REGISTERS();
    task[currentTask].stackPointer = SP;
    /* Appel à l'ordonnanceur */
    scheduler();
    /* Récupération du contexte de la tâche ré-activée */
    SP = task[currentTask].stackPointer;
    RESTORE_REGISTERS();
    asm volatile ( "reti" );
}

La routine d'interruption est générée par le Timer1 avec comme paramètre ISR_NAKED qui permet de ne pas avoir le prologue et l'épilogue automatique du compilateur ce qui nous est très utile pour gérer manuellement ces interruptions afin d'éxecuter nos tâches.

La macro SAVE_REGISTERS() nous permet de sauvegarder les 32 registres sur la pile. Cela permet de s’assurer qu’au retour de l'interruption, la tâche interrompue pourra reprendre son exécution là où elle s’était arrêtée, sans perdre son contexte.

task[currentTask].stackPointer = SP permet de sauvegarder le pointeur de pile de la tâche associée.

scheduler() est expliqué ici.

SP = task[currentTask].stackPointer permet de replacer le pointeur de pile à celui de la nouvelle tâche engendrée par le scheduler.

La macro RESTORE_REGISTERS() remets la nouvelle fonction sur les 32 registres avec des Pop().

asm volatile ( "reti" ) : étant en ISR_NAKED, il faut dire manuellement la fin de l'interruption avec cette commande assembleur RETurn from Interrupt.

Structure d'une tâche

enum States{
    AWAKE,
    IDLE
};

enum IDLE_TYPE{
    IDLE_TYPE_DELAY,
    IDLE_TYPE_STABLE
};

typedef union {
  int sleeping_time;
} Time;

typedef struct {
  enum IDLE_TYPE type;
  Time time;
} Etat ;

typedef struct{
    uint16_t stackPointer; // stack pointer, défini le début du process
    void (*addressFunction)(void); // adresse de l'instruction en cours
    enum States state; // état de la fonction (Awake ou Idle)
    Etat etat;
}process;


Les tâches sont définies par leurs pointeurs de piles, leurs adresses, un état qui est soit IDLE ou AWAKE et enfin un dernier état qui lui gère permet d'avoir différent type d'IDLE.

L'utilisation d'unions n'est pas forcément justifiée ici mais permet de limiter la place que va prendre la structure dans l'optique de futures implémentations.

Code de l'ordonnanceur

void scheduler(){

// décrémentation de l'attente de toutes les tâches en IDLE
for(int i=0;i<NB_TASKS;i++){
    if(task[i].state==IDLE && (task[i].etat.type==IDLE_TYPE_DELAY)){
        task[i].etat.time.sleeping_time -= PERIODE;
        if(task[i].etat.time.sleeping_time <= 0){
            task[i].etat.time.sleeping_time = 0;
            task[i].state = AWAKE;
        }
    }
}
do{
  currentTask ++;
  if(currentTask >= NB_TASKS) currentTask = 0; // ordonnanceur en mode Round Robin
  }while(task[currentTask].state == IDLE);
}


L'ordonnanceur est en mode Round Robin (tourniquet) ce qui veut dire qu'il n'y a pas de priorité de tâche, elles sont toutes appelées une à une dans l'ordre avant de boucler et ainsi de suite...

L'un des ajouts a été de mettre un mode IDLE à nos tâches qui indique la veille de ces dernières. Si une tâche est en IDLE, elle ne sera pas appelée lors de la sélection des tâches et son temps de sommeil sera décrémenté. Cela permet de créer des attentes passives donc d'éviter d'attendre inutilement des tâches qui ne font qu'attendre. On bon exemple pour comprendre serait avec un clignotement de LED. Au lieu de mettre un _delay_ms(1000) qui éxecutera une attente à chaque fois que cette tâche est appelée, on la passe pendant 1000 ms ce qui permet d'éxécuter d'autres tâches entre temps.

Fonction wait

void wait(int time_ms){
    task[currentTask].state = IDLE;
    task[currentTask].etat.type=IDLE_TYPE_DELAY;
    task[currentTask].etat.time.sleeping_time = time_ms;
    TCNT1 = 0;
    TIMER1_COMPA_vect();
}

La fonction permet d'endormir un processus pendant un temps donné en ms.

Elle passe la tâche en IDLE, remet le Timer1 à zéro pour éviter de garder un décalage de temps car le changement est surement fait en plein milieu d'une tâche.

Et ensuite elle appelle l'interruption pour qu'une nouvelle tâche soit en cours.

Exemple de tâches

void USART_Init(unsigned int ubrr)
{
/*Set baud rate */
UBRR0H = (unsigned char)(ubrr>>8);
UBRR0L = (unsigned char)ubrr;
/*Enable receiver and transmitter */
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
/* Set frame format: 8data, 2stop bit */
UCSR0C = (1<<USBS0)|(3<<UCSZ00);
}
void LED_init(){
    DDRC |= (1<<LED1) | (1<<LED2);
    DDRD |= (1<<LED3) | (1<<LED4) | (1<<LED5);
    PORTC |= (1<<LED1)|(1<<LED2);
    PORTD |= (1<<LED3)|(1<<LED4)|(1<<LED5);
}

void LED1_blink(){
    while(1){
        PORTC ^= (1<<LED1);
        _delay_ms(1000);
    }
}

void LED2_blink(){
    while(1){
        PORTC ^= (1<<LED2);
        wait(100);
    }
}
void Serial_Message(){
    unsigned char data;
    while(1){
         data = USART_Receive();
        USART_Transmit(data);
    }
}

void USART_Transmit(unsigned char data)
{
/* Wait for empty transmit buffer */
while (!(UCSR0A & (1<<UDRE0)))
;
/* Put data into buffer, sends the data */
UDR0 = data;
}

unsigned char USART_Receive(void)
{
/* Wait for data to be received */
while (!(UCSR0A & (1<<RXC0)))
;
/* Get and return received data from buffer */
return UDR0;
}
void spi_activer(void){                              // Activer le périphérique
    PORTD &= ~(1<<SPI_SS);                            // Ligne SS à l'état bas
}

void spi_desactiver(void){                           // Désactiver le périphérique
    PORTD |= (1<<SPI_SS);                             // 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
}

void seven_seg(){
    while(1){
    spi_activer();
    spi_echange(0x01);
    spi_desactiver();
    }
}

Exemple d'utilisation

Clignotement de 5 Leds asynchrones

Affichage clavier avec minicom

Création mémoire sur carte SD

Schéma de fonctionnement de la mémoire

La mémoire de la carte SD est composé de plusieurs paquets d'octets qui s'appellent "Bloc". Nous pouvons alors dire qu'un bloc est semblable à :

 uint8_t Bloc[512]

Nous pouvons donc organiser notre espace comme nous le voulons.

Gestion memoire sd.png

Nous avons pris la liberté de changer légèrement le sujet en mettant le nom du fichier sur 14 octets au maximum et en disant qu'un bloc représente 512 octets, ce qui est d'ailleurs le cas sur nos cartes SD. Cela nous permet de décrire un fichier en 32 octets (nom + taille + indices) et de pouvoir mettre 16 description de fichier par bloc et d'ainsi utiliser tous les octets d'un bloc.

Récupération des commandes de base

Dans un premier temps, nous avons un programme qui permet à la carte de mère de reconnaitre 3 commandes: ls, rm et cp. Cela nous permettra par la suite de créer des fichiers, les supprimer et lire les fichiers présents dans le répertoire.

int main(){
    init_stdio();
    char data[Max_chaine],cmd[Max_chaine], arg1[Max_chaine],arg2[Max_chaine];
    while(1) {
        fgets(data,Max_chaine,stdin);
        fprintf(stdout,"\r");
        fflush(stdout);
        int taille = sscanf(data,"%s %s %s", cmd,arg1,arg2);
        if(taille != 0){
            switch(taille){
            case 1 :
                if(strcmp(cmd,"ls") == 0) fprintf(stdout,"je suis un ls\n");
                else fprintf(stdout,"je suis une fraude\n");
                break;
            case 2 :
                if(strcmp(cmd,"rm") == 0) fprintf(stdout,"je suis un rm\n mon arg est : %s \n",arg1);
                else fprintf(stdout,"je suis une fraude\n");
                break;
            case 3:
                if(strcmp(cmd,"cp") == 0) {
                    fprintf(stdout,"je suis un cp\n");
                    fprintf(stdout,"mon arg1 est : %s\n",arg1);
                    fprintf(stdout,"mon arg2 est : %s\n",arg2);}
                else fprintf(stdout,"je suis une fraude\n");
                break;
            }
        }
    }
}


Lors de l'utilisation de minicom, le fgets ne s'exécute que lors d'un "/n", cependant, un entrée sur le clavier correspond à un "/r", nous avons donc converti chaque "/r" en "/n" dans notre fonction de scan pour fluidifier l'utilisation.

static int get_serial_scanf(FILE *stream) {
static unsigned char nl=0;
if(stream==stdin){
  if(nl){ nl=0; return '\n'; }
  char c=get_serial();
  if(c=='\r') nl=1;
  return c;
}
return _FDEV_EOF;
}


À l'inverse, lors d'un fprintf, il est naturel en C de placer un "/n" à la fin d'une chaine de caractère. Cependant, la carte mère à besoin d'un "\r" pour effectuer ce retour à la ligne. Nous avons donc converti chaque "\n" en "\r" dans notre fonction d'impression.

static int send_serial_printf(char c,FILE *stream){
if(stream==stdout){
  if(c=='\n') send_serial('\r');
  send_serial(c);
}
return 0;
}

Accès mémoire de la carte SD

Pour pouvoir utiliser la carte SD, nous avons utilisé les fichiers Sd2Card qui sont une modification de la bibliothèque arduino. Ces fichiers permettent d'utiliser des fonctions comme readBlock et writeBlock afin de manipuler directement la mémoire de la carte SD par SPI.

Primitive système

Plusieurs primitives systèmes ont été ajoutés afin de manipuler la mémoire de la carte SD. Vous pouvez trouver ces fichiers dans le répertoire SD/ de notre dépôt git. Toutes les primitives renvoient leurs informations via le port série.

FORMAT

Cette primitive permet de formater la carte SD d'un bloc à un autre précisé en paramètre. Pour ce faire, la fonction écrit des 0 dans les blocs.

void FORMAT(int debut,int fin){
    uint8_t buffer[BLOC_TAILLE];
    memset(buffer,0,BLOC_TAILLE);
    for(int i = debut; i<fin; i++) {writeBlock(&sd,i,buffer);}
}

LS

Pour lister les fichiers, seul le Superbloc est important. Plus précisément, les blocs compris entre 0 et 13. Dans ces blocs, nous récupérons seulement les 14 premiers octets (TAILLE_NOM) de chaque fichiers et ces noms de fichiers sont espacés de 32 octets (voir schéma)

Dans notre fonction, i représente le numéro de fichier que l'on va regarder et k permet de savoir à quel fichier dans le bloc nous sommes. La fonction envoi alors sur la sortie standard tous les fichiers ne commençant pas par 0.

void LS(){
    uint8_t buffer[BLOC_TAILLE];
    int BlockNumber = 0;
    uint8_t filename[TAILLE_NOM];
    readBlock(&sd,BlockNumber,buffer);
    for(uint8_t i=0, k=0; i<NB_FILE_MAX; i++,k++){ // on parcours tous les emplacements potentiels des desc de fichiers
        memset(filename,0,TAILLE_NOM); // reset du tampon qui contient le nom du fichier
        if(k>((BLOC_TAILLE/sizeof(SD_files_desc))-1)){ // reset de k et passage au bloc suivant
            BlockNumber++;
            readBlock(&sd,BlockNumber,buffer);
            k=0;
        }
        for(int j=0; j<TAILLE_NOM;j++){// Copie du nom dans filename
            if(buffer[j+k*sizeof(SD_files_desc)] == 0) break; // Comparaison avec le caractère \0
            else filename[j] = buffer[j+k*sizeof(SD_files_desc)];
        }
        if(filename[0] != 0)fprintf(stdout,"%s\n", filename);
    }
}

APPEND

Crée un nouveau fichier ou le modifie s'il est déjà existant. Dans notre application, append() demande la taille du fichier et propose à l'utilisateur d'écrire dans son fichier bloc par bloc.

READ

Lis les blocs de données d'un fichier sous forme de chaîne de caractères.

CP

Permet de copier un fichier source dans un fichier de destination. Il récupère d'abord la taille et les indices de bloc des fichiers, regarde quels sont les blocs disponibles puis réécrit ces blocs avec la data du fichier src.

RM

Supprime le fichier spécifié en supprimant le descripteur du SuperBloc (nom + taille + indices) et passe à 0 dans la bitmap les octets représentant les blocs de données du fichier. Cependant, RM ne supprime pas les données du fichier : les fichiers créés nettoient d'abord les blocs avant d'écrire dessus. On évite donc de faire deux fois l'opération.

Fonctionnement des primitives


Bilan de puissance

Afin d'être sûr de l'alimentation de toutes les cartes, nous utilisons une alimentation +5V d'une Raspberry PI branchée sur secteur.

Bilan de puissance (VCC = 3.3V)
Composant Courant Puissance
74LVC125 40 uA 0.2mW
Carte SD 100 mA 330 mW
TOTAL 100,04 mA 330.2 mW
Bilan de puissance (Vcc = 5V)
Composant Courant Puissance
AtMega328p 14mA 70mW
ATMega8U2 21mA 105mW
5xLED 5*5mA 5*25 mW
TOTAL 60mA 300 mW
Bilan de puissance général du groupe
CARTE PUISSANCE (maximale)
Mère 0,63W
Clavier 5,55W
Réseau 0,15W
Ecran 0,17W
TOTAL 2,685W

CONCLUSION

Dans l’état actuel, les différentes cartes filles et la carte mère ne sont malheureusement pas encore en mesure de communiquer entre elles. Cependant, ce projet s’est révélé particulièrement enrichissant, car il nous a permis de mobiliser un large éventail de compétences acquises au cours de notre cursus, telles que la conception de PCB ou encore l’ordonnancement des tâches. En outre, il a constitué une excellente opportunité de développer nos capacités en gestion d’équipe, notamment à travers la coordination entre les différents groupes travaillant sur les cartes filles. Parmi les principaux défis rencontrés, la gestion de la mémoire de la carte SD a été particulièrement complexe, qu’il s’agisse de comprendre les mécanismes de communication avec cette dernière ou de définir une stratégie efficace pour la gestion des fichiers. Nous espérons sincèrement que ce wiki servira de guide utile aux futures promotions, en leur permettant d’éviter les erreurs que nous avons commises, et pourquoi pas, d’achever complètement ce projet ambitieux.