SE4Binome2024-1

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

Lien git : https://gitea.plil.fr/lgrevin/PICO_Binome1.git

Réalisation d'un shield arduino

Nous avons réalisé un bouclier pour Arduino Uno afin d'implémenter un système d'ordonnancement, ce qui nous permettra de simuler le fonctionnement d'une carte mère.

Ce bouclier est conçu pour connecter jusqu’à 5 périphériques SPI via des connecteurs IDC HE10 (8 broches), en intégrant des lignes spécifiques pour la réinitialisation et l’interruption de chaque périphérique, assurant ainsi un contrôle optimal.

En plus des connexions SPI, le bouclier comprend également une mémoire, avec deux options de stockage possibles : une carte micro-SD via un connecteur Molex 10431 ou une puce mémoire AT45DB641E, laissant la flexibilité de choisir celle qui sera soudée selon les besoins. Un convertisseur de niveau (74LV125) est également intégré pour assurer la compatibilité de tension entre l’Arduino (5V) et les mémoires (3,3V), garantissant une communication stable entre les composants.

Schématique et Routage

Schématique du PicoShield
Routage du PicoShield

Shield Brasé

Shield fini

Shield sur une carte Arduino

Vérification des Leds et de la Carte SD

Code Arduino pour vérifier si les Leds fonctionnent :

https://gitea.plil.fr/lgrevin/PICO_Binome1/src/branch/main/VerificationArduino/verif.c

Nous avons écris un petit programme Arduino pour allumer les leds alternativement et donc vérifier leurs fonctionnement.

Vérification de la détection de la carte SD :

Pour vérifier si la carte SD est bien détecté nous avons utilisé un programme exemple de l'arduino :

https://gitea.plil.fr/lgrevin/PICO_Binome1/src/branch/main/Verification_Shield_et_Arduino/SD.ino

Nous avons rencontré des difficultés pour détecter la carte SD. Nous avons essayé plusieurs solutions, notamment en vérifiant l’horloge de notre Shield. Pour cela, nous avons soudé des fils aux ports GND et SCK de la puce mémoire et envoyé un code SPI à l’Arduino pour analyser le signal à l’oscilloscope, mais cela n’a pas permis d’identifier le problème. Après avoir revérifié les soudures et repassé le fer à souder sur plusieurs connexions, nous avons finalement découvert qu’une résistance mal soudée était la source du problème.


Carte SD détectée
Carte SD détectée

Code ordonnanceur

Un ordonnanceur sert à gérer l'exécution des tâches dans un système temps réel, en assurant qu'elles s'exécutent dans un ordre optimal et respectent les délais. Cela garantit que les processus critiques reçoivent les ressources nécessaires pour fonctionner sans interruption, optimisant la performance et la réactivité du système. La gestion des processus repose sur une structure qui définit chaque tâche du système. Cette structure inclut les informations nécessaires pour suivre l'état et le comportement des processus, comme indiqué ci-dessous :

typedef struct {
    uint16_t functionAdress;  // Adresse de la fonction
    uint16_t stackPointer;    // Pointeur de pile
    bool state;               // État (actif ou en sommeil)
    uint16_t sleepTime;       // Temps restant en sommeil (ms)
    uint16_t reason;          // Raison de suspension
} Process;

Voici une fonction basique que nous utilisons pour faire clignoter une LED. On peut noter l'utilisation d'une fonction delay, qui est liée à notre méthode pour endormir un processus, comme nous le verrons un peu plus tard.

void LED1() {
    while (1) {
        PORTD ^= (1 << PD1);
        delay(REASON_DELAY,DELAY1);
    }
}

Notre scheduler a deux objectifs : d'abord, décrémenter le temps de sommeil des processus endormis, puis s'assurer que "taskIndex" pointe uniquement vers un processus qui n'est pas en sommeil.

// Gestion de l'ordonnanceur
void scheduler() {
    for(int i=0; i<NB_PROCESS; i++){
        if (tableauProcess[i].state == SLEEP_STATE && tableauProcess[i].reason == REASON_DELAY){
            if (PERIODE<tableauProcess[i].sleepTime)
                tableauProcess[i].sleepTime -= PERIODE;
            else
                tableauProcess[i].sleepTime = 0;

            if(tableauProcess[i].sleepTime == 0){
                tableauProcess[i].state = AWAKE_STATE;
                tableauProcess[i].reason = NO_REASON;
            }
        }
    }

    do {
        taskIndex++;
        if (taskIndex == NB_PROCESS) taskIndex = 0;
    } while (tableauProcess[taskIndex].state == SLEEP_STATE);
}

La fonction delay met le processus en pause pour une durée donnée. Elle désactive les interruptions (cli()), passe le processus en SLEEP_STATE, enregistre la raison et la durée, réinitialise le compteur de Timer1 (TCNT1 = 0), puis réactive les interruptions (sei()). Cela permet de gérer les délais avec précision.

void delay(uint16_t reason, uint16_t ms)
{
  cli(); //Désactive les interruptions
  tableauProcess[taskIndex].state = SLEEP_STATE;
  tableauProcess[taskIndex].reason = reason;
  tableauProcess[taskIndex].sleepTime = ms;
  TCNT1 = 0; //Valeur actuelle du compteur de Timer1
  sei(); //Réactive les interruptions
  TIMER1_COMPA_vect();
}

Au final, avec notre ordonnanceur, on peut faire tourner plusieurs processus simultanément. Cela nous a permis, par exemple, de faire clignoter les LEDs de manière asynchrone, en gérant indépendamment le temps de sommeil de chaque processus.

Un problème qu'on a pu rencontrer, c'est que si tous les processus sont en état de sommeil, on se retrouve dans une boucle infinie où le temps de sommeil des processus ne se décrémente pas, ce qui bloque tout.

Après avoir pris du temps pour comprendre ça, j'ai ajouté un processus IDLE qui ne fait rien. Grâce à lui, si tous les autres processus sont endormis, celui-là ne l'est pas, et cela empêche de rester bloqué dans une boucle infinie.

void IDLE(){
    while(1);
}

Pour complexifier cet ordonnanceur, nous avons ajouté une fonctionnalité en nous inspirant (très fortement) du cours : la lecture et l’écriture sur les ports séries. Par exemple, on peut lire des caractères (comme un chiffre) via le port série, et l’objectif est ensuite d’afficher ce caractère sur un afficheur 7 segments branché sur un des port HE10.

https://rex.plil.fr/Enseignement/Systeme/Systeme.PSE/systeme013.html et https://rex.plil.fr/Enseignement/Systeme/Systeme.PSE/systeme014.html

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 SevenSeg() {
    while (1) {
        spi_activer();

        for (int i = 0; i < 4; i++) {
            spi_echange(valeur);
        }

        spi_desactiver();

        delay(REASON_DELAY, 100);
    }
}

https://gitea.plil.fr/lgrevin/PICO_Binome1/src/branch/main/code/Ordonnanceur

Réalisation d'une carte clavier

Nous avons choisi de faire une matrice de touches pour réaliser la carte clavier.

Cette matrice contient 8 colonnes et 4 lignes soit un total de 32 boutons. Nous avons également choisi d'ajouter des leds RGB, une sur chaque ligne et chaque colonne qui seront commandés par 2 controleurs de leds (TLC5947 [1]) à l'aide des TLC nous pouvons jouer sur l'intensités et les couleurs des leds.

Datasheet des LEDs RGB:

https://docs.rs-online.com/988a/0900766b80e2903e.pdf

Consommation de la carte clavier

La Puissance totale maximale est de 5,55W.

Bilan de puissance (VCC = 5V)
Composant Courant Puissance
12xLEDS RGB 12x(30x3) mA 5,4W
ATMega328p 15 mA 75 mW
LED d'alim 15 mA 75 mW
TOTAL 1,11A 5,55W

Schématique & Routage

Carte Clavier Schématique
Carte Clavier Schématique
Carte Clavier routé

Brasure de la carte Clavier

Carte Clavier brasé

Programmation de la carte Clavier

Nous voulions un clavier qui lorsqu'on appui sur un bouton on allume la LED de la ligne et de la colonne correspondante. Par la suite nous avons aussi pensé à faire plusieurs modes possibles pour un même clavier c'est à dire que selon le mode du clavier on a un clavier minuscule (par défault), un avec les lettres majuscules et un dernier avec les chiffres et caractères spéciaux. Nous avons donc codé les modes par 2 boutons ( Alt et Maj ) et les modes sont représentés par des couleurs, le clavier par défaut est en bleu, le clavier lettre majuscule en rouge et le dernier en vert.

https://gitea.plil.fr/lgrevin/PICO_Binome1/src/branch/main/code/Carte_clavier


Clavier par default

a b c d e f g
h i j k l m n
o p q r s t u ,
v w x y z Alt

Clavier lettre majuscule

A B C D E F G
H I J K L M N
O P Q R S T U .
V W X Y Z Alt

Clavier chiffre et caractère spéciaux

7 8 9
4 5 6 ( ) " -
1 2 3 + * / = ?
0 ! Alt

Programmation des leds et des boutons

Tout d'abord on a commencé par coder les TLCs :

  1. init_LED_Drivers(int nb) : Cette fonction initialise les pilotes LED en configurant les broches nécessaires comme sorties et en mettant la sortie LATCH à bas. En d'autres termes, elle prépare les pilotes LED pour qu'ils puissent recevoir des signaux de commande.
  2. set_LED_Drivers(int pwm[], int nb) : Cette fonction envoie les valeurs PWM (modulation de largeur d'impulsion) aux pilotes LED. Elle parcourt chaque canal et bit pour définir les sorties CLOCK et DATA en conséquence, puis bascule la sortie LATCH pour mettre à jour les LED avec les nouvelles valeurs PWM. En résumé, elle contrôle la luminosité des LED en fonction des valeurs PWM fournies.

Ces fonctions permettent de gérer et de contrôler les LED en utilisant les pilotes TLC5947.


Nous avons ensuite créer des fonctions qui permettent de contrôler les LED en définissant les couleurs et les intensités lumineuses souhaitées.

Ensuite, nous avons développé les fonctions pour la matrice de boutons :

  1. configuration_bouton() : Cette fonction configure les lignes comme entrées et les colonnes comme sorties pour le clavier. Elle active également les résistances pull-up internes pour les lignes.
  2. test_boutons() : retourne l'index du bouton pressé ou -1 si aucun bouton n'est pressé.
  3. lecture_touche() : Cette fonction lit la touche actuellement pressée sur le clavier. Elle utilise la fonction test_boutons() pour obtenir l'index du bouton pressé et retourne le caractère correspondant à cet index.
  4. led_bouton_appuye() : Cette fonction allume les LED correspondant au bouton actuellement pressé. Elle utilise la fonction test_boutons() pour obtenir l'index du bouton pressé et met à jour les valeurs PWM des LED en conséquence.

Explication de test_boutons :

typedef struct {
    volatile uint8_t *port;
    int pin;
} couple;

couple lignes[NB_LIGNES]={
    {&PINB,PB0},{&PINC,PC0},{&PINC,PC1},{&PINC,PC2}

};

couple colonnes[NB_COLS]={
    {&PORTC,PC3},{&PORTC,PC4},{&PORTC,PC5},{&PORTD,PD0},
    {&PORTD,PD1},{&PORTD,PD2},{&PORTD,PD3},{&PORTD,PD4}
};

lignes et colonnes sont des tableaux de couple qui représentent respectivement les lignes et les colonnes d'une matrice de boutons. Chaque entrée de ces tableaux contient :

  • Un pointeur vers un registre de port (par exemple, PINB, PINC, PORTC, etc.).
  • Le numéro de la broche spécifique à cet élément.
int test_boutons(){
    int bouton=-1;
    for(int c=0;c<NB_COLS;c++){
        *colonnes[c].port &= ~(1<<colonnes[c].pin); // 0 sur la colonne courante
        for(int l=0;l<NB_LIGNES;l++){
            unsigned char test=(*lignes[l].port)&(1<<lignes[l].pin);
            if(test==0){ bouton=c+l*NB_COLS; break; }
        }
        *colonnes[c].port |= (1<<colonnes[c].pin); // retourn à 1 sur la colonne courante
        if(bouton>=0) break;
    }

    return bouton;
}

Cette fonction teste chaque bouton dans la matrice pour vérifier si un bouton a été pressé.

  • Initialisation de bouton :
    • int bouton=-1; initialise la variable qui enregistrera l'indice du bouton pressé. Si aucun bouton n'est pressé, cette valeur restera -1.
  • Boucle des colonnes (parcourir chaque colonne) :
    • *colonnes[c].port &= ~(1<<colonnes[c].pin); : Cette ligne met à 0 la broche de la colonne courante. Cela active la colonne en la mettant à "0", ce qui permet de tester si l'un des boutons dans cette colonne est pressé (dans une matrice, on active une colonne en mettant la broche à "0").
  • Boucle des lignes (parcourir chaque ligne) :
    • unsigned char test=(*lignes[l].port)&(1<<lignes[l].pin); : Cette ligne vérifie l'état de la ligne correspondante. Si le bouton est pressé, la ligne devrait être mise à "0" (car une connexion au sol active le bouton dans une matrice de boutons).
    • Si le test montre que la ligne correspondante est à "0", cela signifie qu'un bouton est pressé. bouton=c+l*NB_COLS; calcule l'indice du bouton pressé en fonction de la colonne (c) et de la ligne (l).
    • break; : Si un bouton a été détecté dans cette colonne, on sort immédiatement de la boucle des lignes.
  • Retour à l'état normal de la colonne :
    • *colonnes[c].port |= (1<<colonnes[c].pin); : Cette ligne remet à 1 la broche de la colonne courante pour revenir à l'état normal après avoir testé cette colonne.
  • Retourner l'indice du bouton pressé :
    • Si un bouton a été détecté, la fonction retourne l'indice du bouton pressé. Sinon, la fonction retourne -1.

Résumé :

  • La fonction parcourt chaque colonne de la matrice de boutons, met la colonne à 0 (active la colonne) et vérifie chaque ligne pour voir si un bouton a été pressé (en vérifiant si la ligne est à 0).
  • Dès qu'un bouton est détecté, l'indice du bouton est calculé et retourné.
  • Si aucun bouton n'est pressé, la fonction retourne -1.

Envoi des données via le port SPI

Utilisation du Picoshield qui jouera le rôle de la carte mère.


update_lettreMemorise() : Cette fonction met à jour la lettre en attente d'être envoyée lors de l'interruption. Elle vérifie si une nouvelle touche est pressée et différente de la dernière lettre envoyée, puis met à jour les variables correspondantes.

void update_lettreMemorise() {
    char toucheLue = lecture_touche();

    // Si une nouvelle touche est pressée et différente de la dernière lettre envoyée
    if (toucheLue != '\0' && toucheLue != lettreActuelle) {
        lettreEnAttente = toucheLue;         // Mettre à jour la lettre en attente
        nouvelleLettreDisponible = 1;     // Indiquer qu'une nouvelle lettre est disponible
        lettreActuelle = toucheLue;          // Mettre à jour la lettre actuelle
    } else if (toucheLue == '\0') {
        lettreActuelle = '\0';               // Réinitialiser la lettre actuelle quand aucune touche n'est pressée
    }

}

testcom() : Cette fonction gère la communication SPI. Elle attend une commande SPI et envoie la lettre en attente si une nouvelle lettre est disponible.

void testcom() {

    // SPI reçoit une commande
    uint8_t received_char = spi_wait();

    if (nouvelleLettreDisponible){
        spi_sendback(lettreEnAttente);
        nouvelleLettreDisponible  = 0;
        lettreEnAttente  = '\0';
    }
}

config_interrupt_PB1() : Cette fonction configure l'interruption sur la broche PB1. Elle met PB1 en entrée, active la résistance pull-up interne et configure les registres d'interruption.

ISR(PCINT0_vect) : Cette fonction est la routine d'interruption pour l'interruption sur la broche PB1. Elle appelle la fonction testcom() après un délai de 10 ms.

La communication fonctionne : quand on appuie sur une touche, c'est le bon caractère qui est affiché sur Minicom. Le saut de ligne et le changement de mode fonctionnent bien.

Capture de Minicom lors des tests du clavier

Conclusion

Comme on peut le voir dans les vidéos, notre clavier est fonctionnel, en tout cas lorsqu'il est utilisé via Minicom sur l'Arduino du Picoshield. Bien qu’il n’y ait pas de gros problèmes de fonctionnement, il reste des points à améliorer pour rendre le système plus robuste et complet.

Actuellement, la communication SPI est très minimaliste : on se limite à envoyer la lettre de la touche appuyée. Cela fonctionne bien grâce à une fréquence d’interruption élevée, mais si deux touches différentes sont pressées entre deux interruptions, seule la dernière sera prise en compte. Dans notre cas, ce genre de situation est rare à cause de la fréquence élevée, mais pour corriger cela de manière plus élégante, il serait préférable de stocker une liste des touches appuyées. Lors de la communication SPI, on pourrait alors transmettre la taille de cette liste et envoyer les données une par une, ce qui permettrait de mieux gérer les cas où plusieurs touches sont pressées simultanément.

Le clavier permet déjà d’écrire via Minicom de manière minimaliste. Cependant, pour aller plus loin, il serait nécessaire de développer davantage la partie logicielle de la carte Pico afin d’obtenir une solution plus complète et mieux adaptée à des usages plus avancés.